Compare commits

...
191 Commits
Author SHA1 Message Date
tarsin d7fcda1f83 Release 1.8.5.3
Releases / Build (push) Has been cancelled
2023-01-23 09:26:25 +08:00
FooIbar b832c24185 Remove app:navGraph from xml
Since we set it through setGraph()
2023-01-23 08:16:20 +08:00
FooIbar 3b04f8c695 Try to bypass VPN when domain fronting is enabled
Note VPN may disallow bypass
2023-01-22 21:09:44 +08:00
tarsin 0ef11b58d2 Move route out of object 2023-01-22 16:27:36 +08:00
tarsin bf13bbf506 Tweak directory structure 2023-01-22 16:24:02 +08:00
tarsin 92c94aa22a Fix typo 2023-01-22 14:04:28 +08:00
tarsin 1c0785432b Avoid unnecessary domain fronting, #711 2023-01-22 13:47:12 +08:00
Allan Nordhøy dcb9a50061 Translated using Weblate (Norwegian Bokmål)
Currently translated at 46.0% (282 of 613 strings)

Translation: Ehviewer-Overhauled/Android strings
Translate-URL: https://hosted.weblate.org/projects/ehviewer-overhauled/android-strings/nb_NO/
2023-01-22 09:09:19 +08:00
FooIbar de7f4bdb6e Translated using Weblate (Chinese (Simplified))
Currently translated at 99.8% (612 of 613 strings)

Translation: Ehviewer-Overhauled/Android strings
Translate-URL: https://hosted.weblate.org/projects/ehviewer-overhauled/android-strings/zh_Hans/
2023-01-22 09:09:19 +08:00
Allan Nordhøy 73d636dba6 Translated using Weblate (English)
Currently translated at 99.8% (612 of 613 strings)

Translation: Ehviewer-Overhauled/Android strings
Translate-URL: https://hosted.weblate.org/projects/ehviewer-overhauled/android-strings/en/
2023-01-22 09:09:19 +08:00
FooIbar 3f1c453ff6 Update strings.xml 2023-01-21 22:48:47 +08:00
tarsin ecb6baaebb Update arrays.xml 2023-01-21 10:03:59 +08:00
tarsin e1cba45486 Translated using Weblate (Chinese (Simplified))
Currently translated at 99.3% (609 of 613 strings)

Translation: Ehviewer-Overhauled/Android strings
Translate-URL: https://hosted.weblate.org/projects/ehviewer-overhauled/android-strings/zh_Hans/
2023-01-21 09:50:49 +08:00
Allan Nordhøy ea4181b243 Translated using Weblate (English)
Currently translated at 99.8% (612 of 613 strings)

Translation: Ehviewer-Overhauled/Android strings
Translate-URL: https://hosted.weblate.org/projects/ehviewer-overhauled/android-strings/en/
2023-01-21 09:50:49 +08:00
tarsin 6c5b9f8851 Add nb-rNO to supported languages 2023-01-21 09:10:57 +08:00
c1fccaf240 Translations update from Hosted Weblate (#701)
* Translated using Weblate (English)

Currently translated at 99.8% (610 of 611 strings)

Translation: Ehviewer-Overhauled/Android strings
Translate-URL: https://hosted.weblate.org/projects/ehviewer-overhauled/android-strings/en/

* Added translation using Weblate (Norwegian Bokmål)

* Translated using Weblate (English)

Currently translated at 99.8% (611 of 612 strings)

Translation: Ehviewer-Overhauled/Android strings
Translate-URL: https://hosted.weblate.org/projects/ehviewer-overhauled/android-strings/en/

* Translated using Weblate (Norwegian Bokmål)

Currently translated at 19.7% (121 of 612 strings)

Translation: Ehviewer-Overhauled/Android strings
Translate-URL: https://hosted.weblate.org/projects/ehviewer-overhauled/android-strings/nb_NO/

Co-authored-by: tarsin <yuanqingxiang233@163.com>
Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
2023-01-21 08:32:21 +08:00
FooIbar e7c223da7d fixup! Resolve #604 2023-01-21 08:12:22 +08:00
FooIbar 6bfe1ba876 Resolve #703 2023-01-20 23:11:23 +08:00
FooIbar 574e5dab18 Remove minimum rating checkbox in advanced search 2023-01-20 21:23:30 +08:00
tarsin 5dc9b83831 Update README.md 2023-01-20 19:22:21 +08:00
FooIbar 57a214a9a4 Fix thumb url for Downloads/History/LocalFavorite 2023-01-20 18:58:29 +08:00
FooIbar cf48eb94af Convert EhUtils to kotlin 2023-01-20 18:58:29 +08:00
FooIbar 683f1e488a Rename .java to .kt 2023-01-20 18:58:29 +08:00
FooIbar e888ae4da7 Use different app name for debug variant 2023-01-20 18:58:29 +08:00
tarsin 4512f2a375 Bump Versio
Releases / Build (push) Has been cancelled
2023-01-20 18:07:19 +08:00
tarsin 68442afa0f Update coil 2023-01-20 12:31:46 +08:00
tarsin ddf4d0cbc3 Use our modified coil temporarily 2023-01-20 10:59:38 +08:00
FooIbar 4be185e428 Resolve #696 2023-01-19 21:30:24 +08:00
tarsin f75e66ac19 Upgrade compose compiler 2023-01-18 21:21:40 +08:00
tarsin abda781f02 Cleanup code 2023-01-18 18:50:38 +08:00
tarsin c1533ec66d Avoid unnecessary lambda object create 2023-01-18 18:23:46 +08:00
tarsin c8319446b0 Bump Version
Releases / Build (push) Has been cancelled
2023-01-18 17:44:45 +08:00
tarsin 7ae452cf13 Fixup! Use FileChannel transferFrom 2023-01-18 12:29:19 +08:00
tarsin 4c024c5faa Fixup! Use FileChannel transferFrom 2023-01-18 12:24:10 +08:00
tarsin 9d16e9d98c Use FileChannel transferFrom, Resolve #682 2023-01-18 12:18:43 +08:00
tarsin 48357d08f7 Add TODOs 2023-01-18 10:47:15 +08:00
tarsin 8e0bcf40db Rewrite the GIF frame delay if it's below a threshold, reland, Resolve #641 2023-01-18 10:46:21 +08:00
renovate[bot] 85ef5f04dd Update plugin com.android.application to v8.0.0-beta01 2023-01-18 09:08:12 +08:00
tarsin 18e0748d14 Revert "Update dependency gradle to v8.0-rc-2"
This reverts commit cdde5b098e.
2023-01-18 09:08:01 +08:00
renovate[bot] cdde5b098e Update dependency gradle to v8.0-rc-2 2023-01-17 21:19:03 +08:00
FooIbar f83964752d Set gallery site to eh when sign out 2023-01-17 16:49:16 +08:00
tarsin 0c3e34bbf9 Fix type warning 2023-01-17 10:50:56 +08:00
tarsin 6dea7faac8 Introduce gif rewriter stub 2023-01-17 10:50:11 +08:00
tarsin 726f482c33 Introduce new file read path 2023-01-17 10:40:20 +08:00
tarsin cb242f664a Convert Archive load to ByteBufferSource path 2023-01-17 09:29:44 +08:00
tarsin edc0345970 Introduce out image source type 2023-01-17 09:15:48 +08:00
tarsin a50535dd76 Fix crash 2023-01-17 09:00:10 +08:00
tarsin 505942aa25 Explicitly depend latest okio 2023-01-17 08:56:05 +08:00
tarsin 38a593a9de Resolve #686 2023-01-17 08:51:51 +08:00
tarsin da87fb2551 Fix image save 2023-01-17 08:20:57 +08:00
tarsin d4dc65a4ba Drop old disklrucache (#685)
* Drop Old DiskLruCache

* Avoid being interrupted when completing editor

* Protect all journal operations

* Revert "Protect all journal operations"

This reverts commit 5b7c7e3b33a515ee3e92826a6b76c2dfb3040c5d.

* Revert "Avoid being interrupted when completing editor"

This reverts commit cf4fbed1281eff766c6e65b6f685dfc355208a0b.

* Shutdown Thread gracefully

Test: m, Check no thread leaks

* Fix download extensions
2023-01-16 21:44:25 +08:00
Edward Wu 53463caedd Ehviewer: Translate: Update zh-rTW Translate (01/06)
* Update strings translate
* Reformat to be consistent with Simplified Chinese xml

Signed-off-by: Edward Wu <bluehome.wu@gmail.com>
2023-01-16 18:34:37 +08:00
tarsin 0707a0fad8 Report DiskLruCache corruption if met 2023-01-16 17:42:55 +08:00
tarsin dd1024af27 Use Tonal Button for register 2023-01-16 17:38:24 +08:00
FooIbar 7edc58a6da Get funds from GP Exchange when unavailable from the archive page 2023-01-16 17:08:03 +08:00
FooIbar 5a5da0cace Convert EhUrl to kotlin 2023-01-16 17:08:03 +08:00
FooIbar 53f02ed366 Rename .java to .kt 2023-01-16 17:08:03 +08:00
tarsin 27cfa885e7 Resolve #681 2023-01-16 12:11:47 +08:00
tarsin beb132d604 Revert "Rewrite the GIF frame delay if it's below a threshold (coil-kt/coil#783) (#672)"
This reverts commit 0ebb55551c.
2023-01-16 11:18:06 +08:00
tarsin b1b10298b4 Revert "Avoid unnecessary overhead"
This reverts commit eada1bc023.
2023-01-16 11:17:53 +08:00
tarsin b1993ff7d2 Fix Gif LruCache size 2023-01-16 10:56:02 +08:00
FooIbar 2afe5fe5b2 Show current funds in archive dialog 2023-01-15 22:19:41 +08:00
FooIbar 52fa8f64cc Convert ArchiveParser to kotlin 2023-01-15 22:19:41 +08:00
FooIbar 4f7978070f Rename .java to .kt 2023-01-15 22:19:41 +08:00
tarsin a909983822 Don't exclude compose metadata 2023-01-15 16:45:43 +08:00
小白-白 5267cad896 Fix wrong extension when downloading 2023-01-15 14:34:03 +08:00
小白-白 ce274cf65b Hide unavailable archive 2023-01-15 14:34:03 +08:00
tarsin 55b6b2e34d Cleanup res 2023-01-15 13:29:18 +08:00
tarsin 5e4e2971b5 Fix code style 2023-01-15 13:24:09 +08:00
tarsin a933bcfb1e Resolve #677 2023-01-15 13:23:05 +08:00
tarsin b4b38e27f3 Resolve #675 2023-01-15 13:17:13 +08:00
tarsin bc70824b57 Fixup! Fix stuck when loading preview 2023-01-15 12:55:05 +08:00
tarsin 87ecaaa6a4 Fixup! Use viewLifecycleOwner 2023-01-15 12:52:21 +08:00
tarsin c4cd9cd802 Fix stuck when loading preview 2023-01-15 12:49:18 +08:00
tarsin bbe0e8c5a9 Remove preview limit
At least it works well on my MT6765
2023-01-15 12:42:04 +08:00
tarsin 3f8984d2dc Resolve #668 2023-01-15 10:50:55 +08:00
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
154 changed files with 8851 additions and 17099 deletions

No files matched your search

+6
View File
@@ -73,6 +73,12 @@ with Material Design 3 and Dynamic Color Support
![screenshot-01](https://github.com/Ehviewer-Overhauled/Art/blob/master/screenshot-01.png)
![screenshot-02](https://github.com/Ehviewer-Overhauled/Art/blob/master/screenshot-02.png)
# Localization
Thank you to each of the **Ehviewer Overhauled** translators, and if you would like to contribute, please submit a translation via [Weblate](https://hosted.weblate.org/engage/ehviewer-overhauled/).
[<img src="https://hosted.weblate.org/widgets/ehviewer-overhauled/-/horizontal-auto.svg" alt="" />](https://hosted.weblate.org/engage/ehviewer-overhauled/)
# Thanks
Here is the libraries
+31 -66
View File
@@ -32,9 +32,9 @@ android {
applicationId "moe.tarsin.ehviewer"
minSdkVersion 28
targetSdkVersion 33
versionCode 180030
versionName "1.8.5.3-alpha03"
resConfigs 'zh', 'zh-rCN', 'zh-rHK', 'zh-rTW', 'es', 'ja', 'ko', 'fr', 'de', 'th'
versionCode 180031
versionName "1.8.5.3"
resConfigs 'zh', 'zh-rCN', 'zh-rHK', 'zh-rTW', 'es', 'ja', 'ko', 'fr', 'de', 'th', 'nb-rNO'
}
externalNativeBuild {
@@ -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"
]
}
@@ -66,7 +64,8 @@ android {
packagingOptions {
resources {
excludes += "/META-INF/**"
excludes += "/META-INF/CHANGES"
excludes += "/META-INF/README.md"
excludes += "/kotlin/**"
excludes += "**.txt"
excludes += "**.bin"
@@ -95,109 +94,75 @@ android {
}
composeOptions {
kotlinCompilerExtensionVersion = "1.4.0-alpha02"
kotlinCompilerExtensionVersion = "1.4.0"
}
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.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 'com.squareup.okio:okio-jvm:3.3.0'
implementation 'dev.chrisbanes.insetter:insetter:0.6.1' // Dead Dependency
implementation 'dev.rikka.rikkax.core:core-ktx:1.4.1'
implementation 'dev.rikka.rikkax.insets:insets:1.3.0'
implementation 'dev.rikka.rikkax.layoutinflater:layoutinflater:1.2.0'
implementation 'dev.rikka.rikkax.preference:simplemenu-preference:1.0.3'
implementation 'dev.rikka.rikkax.material:material-preference:2.0.0'
implementation 'io.coil-kt:coil:2.2.2'
// https://coil-kt.github.io/coil/changelog/
// implementation 'io.coil-kt:coil:2.2.2'
implementation 'com.github.Ehviewer-Overhauled.coil:coil:d65881d'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'
implementation 'org.jsoup:jsoup:1.15.3'
// 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'
@@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8"?><!--
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2016 Hippo Seven
~
~ Licensed under the Apache License, Version 2.0 (the "License");
@@ -14,14 +15,8 @@
~ limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="256dp"
android:height="256dp"
android:tint="@color/brown_500"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="@string/pd_cookie" />
<resources>
</vector>
<string name="app_name" translatable="false">EhViewer Debug</string>
</resources>
+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.login.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)
+28 -1
View File
@@ -1,4 +1,13 @@
#include <jni.h>
#include <sys/mman.h>
#include <android/log.h>
#include <string.h>
#include <errno.h>
#define LOG_TAG "mmap_utils"
#include "ehviewer.h"
JNIEXPORT jint JNICALL
Java_com_hippo_Native_getFd(JNIEnv *env, jclass clazz, jobject fileDescriptor) {
@@ -13,4 +22,22 @@ Java_com_hippo_Native_getFd(JNIEnv *env, jclass clazz, jobject fileDescriptor) {
}
return fd;
}
}
JNIEXPORT jobject JNICALL
Java_com_hippo_Native_mapFd(JNIEnv *env, jclass clazz, jint fd, jlong capability) {
void *addr = mmap(0, capability, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0);
if (addr != MAP_FAILED) {
return (*env)->NewDirectByteBuffer(env, addr, capability);
} else {
LOGE("%s%s", "mmap failed with error ", strerror(errno));
return NULL;
}
}
JNIEXPORT void JNICALL
Java_com_hippo_Native_unmapDirectByteBuffer(JNIEnv *env, jclass clazz, jobject buffer) {
void *addr = (*env)->GetDirectBufferAddress(env, buffer);
size_t size = (*env)->GetDirectBufferCapacity(env, buffer);
if (munmap(addr, size)) LOGE("munmap addr:%p, size%zu failed!", addr, size);
}
+44
View File
@@ -0,0 +1,44 @@
#include <jni.h>
#include <string.h>
#include <stdbool.h>
#include <android/log.h>
#define LOG_TAG "gifUtils"
#include "ehviewer.h"
#define GIF_HEADER_87A "GIF87a"
#define GIF_HEADER_89A "GIF89a"
static int FRAME_DELAY_START_MARKER = 0x0021F904;
typedef signed char byte;
#define FRAME_DELAY_START_MARKER ((byte*)(&FRAME_DELAY_START_MARKER))
#define MINIMUM_FRAME_DELAY 2
#define DEFAULT_FRAME_DELAY 10
static inline bool isGif(void *addr) {
return !memcmp(addr, GIF_HEADER_87A, 6) || !memcmp(addr, GIF_HEADER_89A, 6);
}
JNIEXPORT void JNICALL
Java_com_hippo_image_Image_rewriteGifSource(JNIEnv *env, jclass clazz, jobject buffer) {
byte *addr = (*env)->GetDirectBufferAddress(env, buffer);
size_t size = (*env)->GetDirectBufferCapacity(env, buffer);
if (size < 7 || !isGif(addr)) return;
for (size_t i = 0; i < size - 8; i++) {
// TODO: Optimize this hex find with SIMD?
if (addr[i] == FRAME_DELAY_START_MARKER[3] && addr[i + 1] == FRAME_DELAY_START_MARKER[2] &&
addr[i + 2] == FRAME_DELAY_START_MARKER[1] &&
addr[i + 3] == FRAME_DELAY_START_MARKER[0]) {
byte *end = addr + i + 4;
if (end[4] != 0) continue;
int frameDelay = end[2] << 8 | end[1];
if (frameDelay > MINIMUM_FRAME_DELAY)
break; // Quit if the first block looks normal, for performance
end[1] = DEFAULT_FRAME_DELAY;
end[2] = 0;
}
}
}
@@ -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
@@ -476,10 +476,8 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
private fun setGestureDetector(context: Context) {
detector = GestureDetector(context, object : GestureDetector.SimpleOnGestureListener() {
@Suppress("SENSELESS_COMPARISON")
override fun onFling(e1: MotionEvent, e2: MotionEvent, velocityX: Float, velocityY: Float): Boolean {
// The e1 can be null, that's weird and cause crash
if (panEnabled && isReady && vTranslate != null && e1 != null && e2 != null && (abs(e1.x - e2.x) > 50 || abs(e1.y - e2.y) > 50) && (abs(velocityX) > 500 || abs(velocityY) > 500) && !isZooming) {
if (panEnabled && isReady && vTranslate != null && (abs(e1.x - e2.x) > 50 || abs(e1.y - e2.y) > 50) && (abs(velocityX) > 500 || abs(velocityY) > 500) && !isZooming) {
val vTranslateEnd = PointF(vTranslate!!.x + velocityX * 0.25F, vTranslate!!.y + velocityY * 0.25F)
val sCenterXEnd = (width / 2 - vTranslateEnd.x) / scale
val sCenterYEnd = (height / 2 - vTranslateEnd.y) / scale
@@ -607,12 +605,16 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
return true
}
// Detect flings, taps and double taps
if (!isQuickScaling && detector?.onTouchEvent(event) != false) {
isZooming = false
isPanning = false
maxTouchCount = 0
return true
// May throw NPE
runCatching {
if (!isQuickScaling && detector?.onTouchEvent(event) != false) {
isZooming = false
isPanning = false
maxTouchCount = 0
return true
}
}
if (vTranslateStart == null) {
vTranslateStart = PointF(0F, 0F)
}
+8 -1
View File
@@ -18,6 +18,7 @@
package com.hippo
import java.io.FileDescriptor
import java.nio.ByteBuffer
object Native {
fun initialize() {
@@ -26,4 +27,10 @@ object Native {
@JvmStatic
external fun getFd(fd: FileDescriptor?): Int
}
@JvmStatic
external fun mapFd(fd: Int, capability: Long): ByteBuffer?
@JvmStatic
external fun unmapDirectByteBuffer(buffer: ByteBuffer)
}
@@ -19,6 +19,7 @@ package com.hippo
import android.content.Context
import android.net.Uri
import com.hippo.image.Image
import java.nio.ByteBuffer
class UriArchiveAccessor(ctx: Context, uri: Uri) {
@@ -28,7 +29,7 @@ class UriArchiveAccessor(ctx: Context, uri: Uri) {
}
private external fun openArchive(fd: Int, size: Long): Int
external fun extractToByteBuffer(index: Int): ByteBuffer?
private external fun extractToByteBuffer(index: Int): ByteBuffer?
external fun extractToFd(index: Int, fd: Int)
external fun getFilename(index: Int): String
external fun needPassword(): Boolean
@@ -39,8 +40,23 @@ class UriArchiveAccessor(ctx: Context, uri: Uri) {
pfd.close()
}
fun getImageSource(index: Int): Image.ByteBufferSource? {
val buffer = extractToByteBuffer(index)
buffer ?: return null
check(buffer.isDirect)
return object : Image.ByteBufferSource {
override fun getByteBuffer(): ByteBuffer {
return buffer
}
override fun close() {
releaseByteBuffer(buffer)
}
}
}
companion object {
@JvmStatic
external fun releaseByteBuffer(buffer: ByteBuffer)
private external fun releaseByteBuffer(buffer: ByteBuffer)
}
}
@@ -1,985 +0,0 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* 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.beerbelly;
import java.io.BufferedWriter;
import java.io.Closeable;
import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* A cache that uses a bounded amount of space on a filesystem. Each cache
* entry has a string key and a fixed number of values. Each key must match
* the regex <strong>[a-z0-9_-]{1,120}</strong>. Values are byte sequences,
* accessible as streams or files. Each value must be between {@code 0} and
* {@code Integer.MAX_VALUE} bytes in length.
*
* <p>The cache stores its data in a directory on the filesystem. This
* directory must be exclusive to the cache; the cache may delete or overwrite
* files from its directory. It is an error for multiple processes to use the
* same cache directory at the same time.
*
* <p>This cache limits the number of bytes that it will store on the
* filesystem. When the number of stored bytes exceeds the limit, the cache will
* remove entries in the background until the limit is satisfied. The limit is
* not strict: the cache may temporarily exceed it while waiting for files to be
* deleted. The limit does not include filesystem overhead or the cache
* journal so space-sensitive applications should set a conservative limit.
*
* <p>Clients call {@link #edit} to create or update the values of an entry. An
* entry may have only one editor at one time; if a value is not available to be
* edited then {@link #edit} will return null.
* <ul>
* <li>When an entry is being <strong>created</strong> it is necessary to
* supply a full set of values; the empty value should be used as a
* placeholder if necessary.
* <li>When an entry is being <strong>edited</strong>, it is not necessary
* to supply data for every value; values default to their previous
* value.
* </ul>
* Every {@link #edit} call must be matched by a call to {@link Editor#commit}
* or {@link Editor#abort}. Committing is atomic: a read observes the full set
* of values as they were before or after the commit, but never a mix of values.
*
* <p>Clients call {@link #get} to read a snapshot of an entry. The read will
* observe the value at the time that {@link #get} was called. Updates and
* removals after the call do not impact ongoing reads.
*
* <p>This class is tolerant of some I/O errors. If files are missing from the
* filesystem, the corresponding entries will be dropped from the cache. If
* an error occurs while writing a cache value, the edit will fail silently.
* Callers should handle other problems by catching {@code IOException} and
* responding appropriately.
*/
public final class DiskLruCache implements Closeable {
static final String JOURNAL_FILE = "journal";
static final String JOURNAL_FILE_TEMP = "journal.tmp";
static final String JOURNAL_FILE_BACKUP = "journal.bkp";
static final String MAGIC = "libcore.io.DiskLruCache";
static final String VERSION_1 = "1";
static final long ANY_SEQUENCE_NUMBER = -1;
static final String STRING_KEY_PATTERN = "[a-z0-9_-]{1,120}";
static final Pattern LEGAL_KEY_PATTERN = Pattern.compile(STRING_KEY_PATTERN);
private static final String CLEAN = "CLEAN";
private static final String DIRTY = "DIRTY";
private static final String REMOVE = "REMOVE";
private static final String READ = "READ";
/*
* This cache uses a journal file named "journal". A typical journal file
* looks like this:
* libcore.io.DiskLruCache
* 1
* 100
* 2
*
* CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054
* DIRTY 335c4c6028171cfddfbaae1a9c313c52
* CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342
* REMOVE 335c4c6028171cfddfbaae1a9c313c52
* DIRTY 1ab96a171faeeee38496d8b330771a7a
* CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234
* READ 335c4c6028171cfddfbaae1a9c313c52
* READ 3400330d1dfc7f3f7f4b8d4d803dfcf6
*
* The first five lines of the journal form its header. They are the
* constant string "libcore.io.DiskLruCache", the disk cache's version,
* the application's version, the value count, and a blank line.
*
* Each of the subsequent lines in the file is a record of the state of a
* cache entry. Each line contains space-separated values: a state, a key,
* and optional state-specific values.
* o DIRTY lines track that an entry is actively being created or updated.
* Every successful DIRTY action should be followed by a CLEAN or REMOVE
* action. DIRTY lines without a matching CLEAN or REMOVE indicate that
* temporary files may need to be deleted.
* o CLEAN lines track a cache entry that has been successfully published
* and may be read. A publish line is followed by the lengths of each of
* its values.
* o READ lines track accesses for LRU.
* o REMOVE lines track entries that have been deleted.
*
* The journal file is appended to as cache operations occur. The journal may
* occasionally be compacted by dropping redundant lines. A temporary file named
* "journal.tmp" will be used during compaction; that file should be deleted if
* it exists when the cache is opened.
*/
private static final OutputStream NULL_OUTPUT_STREAM = new OutputStream() {
@Override
public void write(int b) {
// Eat all writes silently. Nom nom.
}
};
/**
* This cache uses a single background thread to evict entries.
*/
final ThreadPoolExecutor executorService =
new ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<>());
private final File directory;
private final File journalFile;
private final File journalFileTmp;
private final File journalFileBackup;
private final int appVersion;
private final int valueCount;
private final LinkedHashMap<String, Entry> lruEntries =
new LinkedHashMap<>(0, 0.75f, true);
private long maxSize;
private long size = 0;
private Writer journalWriter;
private int redundantOpCount;
/**
* To differentiate between old and current snapshots, each entry is given
* a sequence number each time an edit is committed. A snapshot is stale if
* its sequence number is not equal to its entry's sequence number.
*/
private long nextSequenceNumber = 0;
private DiskLruCache(File directory, int appVersion, int valueCount, long maxSize) {
this.directory = directory;
this.appVersion = appVersion;
this.journalFile = new File(directory, JOURNAL_FILE);
this.journalFileTmp = new File(directory, JOURNAL_FILE_TEMP);
this.journalFileBackup = new File(directory, JOURNAL_FILE_BACKUP);
this.valueCount = valueCount;
this.maxSize = maxSize;
}
/**
* Opens the cache in {@code directory}, creating a cache if none exists
* there.
*
* @param directory a writable directory
* @param valueCount the number of values per cache entry. Must be positive.
* @param maxSize the maximum number of bytes this cache should use to store
* @throws IOException if reading or writing the cache directory fails
*/
public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
throws IOException {
if (maxSize <= 0) {
throw new IllegalArgumentException("maxSize <= 0");
}
if (valueCount <= 0) {
throw new IllegalArgumentException("valueCount <= 0");
}
// If a bkp file exists, use it instead.
File backupFile = new File(directory, JOURNAL_FILE_BACKUP);
if (backupFile.exists()) {
File journalFile = new File(directory, JOURNAL_FILE);
// If journal file also exists just delete backup file.
if (journalFile.exists()) {
backupFile.delete();
} else {
renameTo(backupFile, journalFile, false);
}
}
// Prefer to pick up where we left off.
DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
if (cache.journalFile.exists()) {
try {
cache.readJournal();
cache.processJournal();
return cache;
} catch (IOException journalIsCorrupt) {
System.out
.println("DiskLruCache "
+ directory
+ " is corrupt: "
+ journalIsCorrupt.getMessage()
+ ", removing");
cache.delete();
}
}
// Create a new empty cache.
directory.mkdirs();
cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
cache.rebuildJournal();
return cache;
}
private static void deleteIfExists(File file) throws IOException {
if (file.exists() && !file.delete()) {
throw new IOException();
}
}
private static void renameTo(File from, File to, boolean deleteDestination) throws IOException {
if (deleteDestination) {
deleteIfExists(to);
}
if (!from.renameTo(to)) {
throw new IOException();
}
}
private static String inputStreamToString(InputStream in) throws IOException {
return Util.readFully(new InputStreamReader(in, Util.UTF_8));
}
private void readJournal() throws IOException {
StrictLineReader reader = new StrictLineReader(new FileInputStream(journalFile), Util.US_ASCII);
try {
String magic = reader.readLine();
String version = reader.readLine();
String appVersionString = reader.readLine();
String valueCountString = reader.readLine();
String blank = reader.readLine();
if (!MAGIC.equals(magic)
|| !VERSION_1.equals(version)
|| !Integer.toString(appVersion).equals(appVersionString)
|| !Integer.toString(valueCount).equals(valueCountString)
|| !"".equals(blank)) {
throw new IOException("unexpected journal header: [" + magic + ", " + version + ", "
+ valueCountString + ", " + blank + "]");
}
int lineCount = 0;
while (true) {
try {
readJournalLine(reader.readLine());
lineCount++;
} catch (EOFException endOfJournal) {
break;
}
}
redundantOpCount = lineCount - lruEntries.size();
// If we ended on a truncated line, rebuild the journal before appending to it.
if (reader.hasUnterminatedLine()) {
rebuildJournal();
} else {
journalWriter = new BufferedWriter(new OutputStreamWriter(
new FileOutputStream(journalFile, true), Util.US_ASCII));
}
} finally {
Util.closeQuietly(reader);
}
}
private void readJournalLine(String line) throws IOException {
int firstSpace = line.indexOf(' ');
if (firstSpace == -1) {
throw new IOException("unexpected journal line: " + line);
}
int keyBegin = firstSpace + 1;
int secondSpace = line.indexOf(' ', keyBegin);
final String key;
if (secondSpace == -1) {
key = line.substring(keyBegin);
if (firstSpace == REMOVE.length() && line.startsWith(REMOVE)) {
lruEntries.remove(key);
return;
}
} else {
key = line.substring(keyBegin, secondSpace);
}
Entry entry = lruEntries.get(key);
if (entry == null) {
entry = new Entry(key);
lruEntries.put(key, entry);
}
if (secondSpace != -1 && firstSpace == CLEAN.length() && line.startsWith(CLEAN)) {
String[] parts = line.substring(secondSpace + 1).split(" ");
entry.readable = true;
entry.currentEditor = null;
entry.setLengths(parts);
} else if (secondSpace == -1 && firstSpace == DIRTY.length() && line.startsWith(DIRTY)) {
entry.currentEditor = new Editor(entry);
} else if (secondSpace == -1 && firstSpace == READ.length() && line.startsWith(READ)) {
// This work was already done by calling lruEntries.get().
} else {
throw new IOException("unexpected journal line: " + line);
}
}
/**
* Computes the initial size and collects garbage as a part of opening the
* cache. Dirty entries are assumed to be inconsistent and will be deleted.
*/
private void processJournal() throws IOException {
deleteIfExists(journalFileTmp);
for (Iterator<Entry> i = lruEntries.values().iterator(); i.hasNext(); ) {
Entry entry = i.next();
if (entry.currentEditor == null) {
for (int t = 0; t < valueCount; t++) {
size += entry.lengths[t];
}
} else {
entry.currentEditor = null;
for (int t = 0; t < valueCount; t++) {
deleteIfExists(entry.getCleanFile(t));
deleteIfExists(entry.getDirtyFile(t));
}
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
* current journal if it exists.
*/
private synchronized void rebuildJournal() throws IOException {
if (journalWriter != null) {
journalWriter.close();
}
try (Writer writer = new BufferedWriter(
new OutputStreamWriter(new FileOutputStream(journalFileTmp), Util.US_ASCII))) {
writer.write(MAGIC);
writer.write("\n");
writer.write(VERSION_1);
writer.write("\n");
writer.write(Integer.toString(appVersion));
writer.write("\n");
writer.write(Integer.toString(valueCount));
writer.write("\n");
writer.write("\n");
for (Entry entry : lruEntries.values()) {
if (entry.currentEditor != null) {
writer.write(DIRTY + ' ' + entry.key + '\n');
} else {
writer.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
}
}
}
if (journalFile.exists()) {
renameTo(journalFile, journalFileBackup, true);
}
renameTo(journalFileTmp, journalFile, false);
journalFileBackup.delete();
journalWriter = new BufferedWriter(
new OutputStreamWriter(new FileOutputStream(journalFile, true), Util.US_ASCII));
}
/**
* Return {@code true} if it contains the key
*/
public synchronized boolean contain(String key) {
checkNotClosed();
validateKey(key);
Entry entry = lruEntries.get(key);
return entry != null && entry.readable;
}
/**
* Returns a snapshot of the entry named {@code key}, or null if it doesn't
* exist is not currently readable. If a value is returned, it is moved to
* the head of the LRU queue.
*/
public synchronized Snapshot get(String key) throws IOException {
checkNotClosed();
validateKey(key);
Entry entry = lruEntries.get(key);
if (entry == null) {
return null;
}
if (!entry.readable) {
return null;
}
// Open all streams eagerly to guarantee that we see a single published
// snapshot. If we opened streams lazily then the streams could come
// from different edits.
InputStream[] ins = new InputStream[valueCount];
try {
for (int i = 0; i < valueCount; i++) {
ins[i] = new FileInputStream(entry.getCleanFile(i));
}
} catch (FileNotFoundException e) {
// A file must have been deleted manually!
for (int i = 0; i < valueCount; i++) {
if (ins[i] != null) {
Util.closeQuietly(ins[i]);
} else {
break;
}
}
return null;
}
redundantOpCount++;
journalWriter.append(READ + ' ').append(key).append(String.valueOf('\n'));
if (journalRebuildRequired()) {
executorService.submit(cleanupCallable);
}
return new Snapshot(key, entry.sequenceNumber, ins, entry.lengths);
}
/**
* Returns an editor for the entry named {@code key}, or null if another
* edit is in progress.
*/
public Editor edit(String key) throws IOException {
return edit(key, ANY_SEQUENCE_NUMBER);
}
private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException {
checkNotClosed();
validateKey(key);
Entry entry = lruEntries.get(key);
if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER && (entry == null
|| entry.sequenceNumber != expectedSequenceNumber)) {
return null; // Snapshot is stale.
}
if (entry == null) {
entry = new Entry(key);
lruEntries.put(key, entry);
} else if (entry.currentEditor != null) {
return null; // Another edit is in progress.
}
Editor editor = new Editor(entry);
entry.currentEditor = editor;
// Flush the journal before creating files to prevent file leaks.
journalWriter.write(DIRTY + ' ' + key + '\n');
journalWriter.flush();
return editor;
}
/**
* Returns the directory where this cache stores its data.
*/
public File getDirectory() {
return directory;
}
/**
* Returns the maximum number of bytes that this cache should use to store
* its data.
*/
public synchronized long getMaxSize() {
return maxSize;
}
/**
* Changes the maximum number of bytes the cache can store and queues a job
* to trim the existing store, if necessary.
*/
public synchronized void setMaxSize(long maxSize) {
this.maxSize = maxSize;
executorService.submit(cleanupCallable);
}
/**
* Returns the number of bytes currently being used to store the values in
* this cache. This may be greater than the max size if a background
* deletion is pending.
*/
public synchronized long size() {
return size;
}
private synchronized void completeEdit(Editor editor, boolean success) throws IOException {
Entry entry = editor.entry;
if (entry.currentEditor != editor) {
throw new IllegalStateException();
}
// If this edit is creating the entry for the first time, every index must have a value.
if (success && !entry.readable) {
for (int i = 0; i < valueCount; i++) {
if (!editor.written[i]) {
editor.abort();
throw new IllegalStateException("Newly created entry didn't create value for index " + i);
}
if (!entry.getDirtyFile(i).exists()) {
editor.abort();
return;
}
}
}
for (int i = 0; i < valueCount; i++) {
File dirty = entry.getDirtyFile(i);
if (success) {
if (dirty.exists()) {
File clean = entry.getCleanFile(i);
dirty.renameTo(clean);
long oldLength = entry.lengths[i];
long newLength = clean.length();
entry.lengths[i] = newLength;
size = size - oldLength + newLength;
}
} else {
deleteIfExists(dirty);
}
}
redundantOpCount++;
entry.currentEditor = null;
if (entry.readable | success) {
entry.readable = true;
journalWriter.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
if (success) {
entry.sequenceNumber = nextSequenceNumber++;
}
} else {
lruEntries.remove(entry.key);
journalWriter.write(REMOVE + ' ' + entry.key + '\n');
}
journalWriter.flush();
if (size > maxSize || journalRebuildRequired()) {
executorService.submit(cleanupCallable);
}
}
/**
* We only rebuild the journal when it will halve the size of the journal
* and eliminate at least 2000 ops.
*/
private boolean journalRebuildRequired() {
final int redundantOpCompactThreshold = 2000;
return redundantOpCount >= redundantOpCompactThreshold //
&& redundantOpCount >= lruEntries.size();
}
/**
* Drops the entry for {@code key} if it exists and can be removed. Entries
* actively being edited cannot be removed.
*
* @return true if an entry was removed.
*/
public synchronized boolean remove(String key) throws IOException {
checkNotClosed();
validateKey(key);
Entry entry = lruEntries.get(key);
if (entry == null || entry.currentEditor != null) {
return false;
}
for (int i = 0; i < valueCount; i++) {
File file = entry.getCleanFile(i);
if (file.exists() && !file.delete()) {
throw new IOException("failed to delete " + file);
}
size -= entry.lengths[i];
entry.lengths[i] = 0;
}
redundantOpCount++;
journalWriter.append(REMOVE + ' ').append(key).append(String.valueOf('\n'));
lruEntries.remove(key);
if (journalRebuildRequired()) {
executorService.submit(cleanupCallable);
}
return true;
}
/**
* Returns true if this cache has been closed.
*/
public synchronized boolean isClosed() {
return journalWriter == null;
}
private void checkNotClosed() {
if (journalWriter == null) {
throw new IllegalStateException("cache is closed");
}
}
/**
* Force buffered operations to the filesystem.
*/
public synchronized void flush() throws IOException {
checkNotClosed();
trimToSize();
journalWriter.flush();
}
/**
* Closes this cache. Stored values will remain on the filesystem.
*/
public synchronized void close() throws IOException {
if (journalWriter == null) {
return; // Already closed.
}
for (Entry entry : new ArrayList<>(lruEntries.values())) {
if (entry.currentEditor != null) {
entry.currentEditor.abort();
}
}
trimToSize();
journalWriter.close();
journalWriter = null;
}
private void trimToSize() throws IOException {
while (size > maxSize) {
Map.Entry<String, Entry> toEvict = lruEntries.entrySet().iterator().next();
remove(toEvict.getKey());
}
}
/**
* Closes the cache and deletes all of its stored values. This will delete
* all files in the cache directory including files that weren't created by
* the cache.
*/
public void delete() throws IOException {
close();
Util.deleteContents(directory);
}
private void validateKey(String key) {
Matcher matcher = LEGAL_KEY_PATTERN.matcher(key);
if (!matcher.matches()) {
throw new IllegalArgumentException("keys must match regex "
+ STRING_KEY_PATTERN + ": \"" + key + "\"");
}
}
/**
* A snapshot of the values for an entry.
*/
public final class Snapshot implements Closeable {
private final String key;
private final long sequenceNumber;
private final InputStream[] ins;
private final long[] lengths;
private Snapshot(String key, long sequenceNumber, InputStream[] ins, long[] lengths) {
this.key = key;
this.sequenceNumber = sequenceNumber;
this.ins = ins;
this.lengths = lengths;
}
/**
* Returns an editor for this snapshot's entry, or null if either the
* entry has changed since this snapshot was created or if another edit
* is in progress.
*/
public Editor edit() throws IOException {
return DiskLruCache.this.edit(key, sequenceNumber);
}
/**
* Returns the unbuffered stream with the value for {@code index}.
*/
public InputStream getInputStream(int index) {
return ins[index];
}
/**
* Returns the string value for {@code index}.
*/
public String getString(int index) throws IOException {
return inputStreamToString(getInputStream(index));
}
/**
* Returns the byte length of the value for {@code index}.
*/
public long getLength(int index) {
return lengths[index];
}
public void close() {
for (InputStream in : ins) {
Util.closeQuietly(in);
}
}
}
/**
* Edits the values for an entry.
*/
public final class Editor {
private final Entry entry;
private final boolean[] written;
private boolean hasErrors;
private boolean committed;
private Editor(Entry entry) {
this.entry = entry;
this.written = (entry.readable) ? null : new boolean[valueCount];
}
/**
* Returns an unbuffered input stream to read the last committed value,
* or null if no value has been committed.
*/
public InputStream newInputStream(int index) {
synchronized (DiskLruCache.this) {
if (entry.currentEditor != this) {
throw new IllegalStateException();
}
if (!entry.readable) {
return null;
}
try {
return new FileInputStream(entry.getCleanFile(index));
} catch (FileNotFoundException e) {
return null;
}
}
}
/**
* Returns the last committed value as a string, or null if no value
* has been committed.
*/
public String getString(int index) throws IOException {
InputStream in = newInputStream(index);
return in != null ? inputStreamToString(in) : null;
}
/**
* Returns a new unbuffered output stream to write the value at
* {@code index}. If the underlying output stream encounters errors
* when writing to the filesystem, this edit will be aborted when
* {@link #commit} is called. The returned output stream does not throw
* IOExceptions.
*/
public OutputStream newOutputStream(int index) throws IOException {
if (index < 0 || index >= valueCount) {
throw new IllegalArgumentException("Expected index " + index + " to "
+ "be greater than 0 and less than the maximum value count "
+ "of " + valueCount);
}
synchronized (DiskLruCache.this) {
if (entry.currentEditor != this) {
throw new IllegalStateException();
}
if (!entry.readable) {
written[index] = true;
}
File dirtyFile = entry.getDirtyFile(index);
FileOutputStream outputStream;
try {
outputStream = new FileOutputStream(dirtyFile);
} catch (FileNotFoundException e) {
// Attempt to recreate the cache directory.
directory.mkdirs();
try {
outputStream = new FileOutputStream(dirtyFile);
} catch (FileNotFoundException e2) {
// We are unable to recover. Silently eat the writes.
return NULL_OUTPUT_STREAM;
}
}
return new FaultHidingOutputStream(outputStream);
}
}
/**
* Sets the value at {@code index} to {@code value}.
*/
public void set(int index, String value) throws IOException {
Writer writer = null;
try {
writer = new OutputStreamWriter(newOutputStream(index), Util.UTF_8);
writer.write(value);
} finally {
Util.closeQuietly(writer);
}
}
/**
* Commits this edit so it is visible to readers. This releases the
* edit lock so another edit may be started on the same key.
*/
public void commit() throws IOException {
if (hasErrors) {
completeEdit(this, false);
remove(entry.key); // The previous entry is stale.
} else {
completeEdit(this, true);
}
committed = true;
}
/**
* Aborts this edit. This releases the edit lock so another edit may be
* started on the same key.
*/
public void abort() throws IOException {
completeEdit(this, false);
}
public void abortUnlessCommitted() {
if (!committed) {
try {
abort();
} catch (IOException ignored) {
}
}
}
private class FaultHidingOutputStream extends FilterOutputStream {
private FaultHidingOutputStream(OutputStream out) {
super(out);
}
@Override
public void write(int oneByte) {
try {
out.write(oneByte);
} catch (IOException e) {
hasErrors = true;
}
}
@Override
public void write(byte[] buffer, int offset, int length) {
try {
out.write(buffer, offset, length);
} catch (IOException e) {
hasErrors = true;
}
}
@Override
public void close() {
try {
out.close();
} catch (IOException e) {
hasErrors = true;
}
}
@Override
public void flush() {
try {
out.flush();
} catch (IOException e) {
hasErrors = true;
}
}
}
}
private final class Entry {
private final String key;
/**
* Lengths of this entry's files.
*/
private final long[] lengths;
/**
* True if this entry has ever been published.
*/
private boolean readable;
/**
* The ongoing edit or null if this entry is not being edited.
*/
private Editor currentEditor;
/**
* The sequence number of the most recently committed edit to this entry.
*/
private long sequenceNumber;
private Entry(String key) {
this.key = key;
this.lengths = new long[valueCount];
}
public String getLengths() {
StringBuilder result = new StringBuilder();
for (long size : lengths) {
result.append(' ').append(size);
}
return result.toString();
}
/**
* Set lengths using decimal numbers like "10123".
*/
private void setLengths(String[] strings) throws IOException {
if (strings.length != valueCount) {
throw invalidLengths(strings);
}
try {
for (int i = 0; i < strings.length; i++) {
lengths[i] = Long.parseLong(strings[i]);
}
} catch (NumberFormatException e) {
throw invalidLengths(strings);
}
}
private IOException invalidLengths(String[] strings) throws IOException {
throw new IOException("unexpected journal line: " + java.util.Arrays.toString(strings));
}
public File getCleanFile(int i) {
return new File(directory, key + "." + i);
}
public File getDirtyFile(int i) {
return new File(directory, key + "." + i + ".tmp");
}
}
}
@@ -1,504 +0,0 @@
/*
* Copyright 2015-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.beerbelly;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.hippo.streampipe.InputStreamPipe;
import com.hippo.streampipe.OutputStreamPipe;
import com.hippo.util.HashCodeUtils;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class SimpleDiskCache {
private static final String TAG = SimpleDiskCache.class.getSimpleName();
/**
* Disk cache is free
*/
private static final int STATE_DISK_CACHE_NONE = 0;
/**
* Disk cache is flushing or clearing
*/
private static final int STATE_DISK_CACHE_IN_USE = 1;
/**
* Disk cache is reading or writing
*/
private static final int STATE_DISK_CACHE_BUSY = 2;
private final File mCacheDir;
private final int mSize;
@NonNull
private final Map<String, CounterLock> mLockMap = new HashMap<>();
@NonNull
private final LockPool mLockPool = new LockPool();
private int mDiskCacheState;
@Nullable
private DiskLruCache mDiskLruCache;
public SimpleDiskCache(File cacheDir, int size) {
mCacheDir = cacheDir;
mSize = size;
try {
init();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* A hashing method that changes a string (like a URL) into a hash suitable
* for using as a disk filename.
*
* @param key The key used to store the file
*/
private static String hashKeyForDisk(final String key) {
String cacheKey;
try {
final MessageDigest digest = MessageDigest.getInstance("MD5");
digest.update(key.getBytes());
cacheKey = HashCodeUtils.bytesToHexString(digest.digest());
} catch (final NoSuchAlgorithmException e) {
cacheKey = String.valueOf(key.hashCode());
}
return cacheKey;
}
public synchronized boolean isValid() {
return mDiskLruCache != null;
}
private synchronized void init() throws IOException {
if (!isValid()) {
mDiskLruCache = DiskLruCache.open(mCacheDir, 1, 1, mSize);
}
}
public synchronized long size() {
if (null != mDiskLruCache) {
return mDiskLruCache.size();
} else {
return -1L;
}
}
public synchronized void flush() {
// Wait for cache available
while (mDiskCacheState != STATE_DISK_CACHE_NONE) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
mDiskCacheState = STATE_DISK_CACHE_BUSY;
if (null != mDiskLruCache) {
try {
mDiskLruCache.flush();
} catch (IOException e) {
Log.e(TAG, "SimpleDiskCache flush", e);
}
}
mDiskCacheState = STATE_DISK_CACHE_NONE;
notifyAll();
}
public synchronized boolean clear() {
boolean result = true;
if (null == mDiskLruCache) {
return false;
}
// Wait for cache available
while (mDiskCacheState != STATE_DISK_CACHE_NONE) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
mDiskCacheState = STATE_DISK_CACHE_BUSY;
try {
mDiskLruCache.delete();
} catch (IOException e) {
Log.e(TAG, "SimpleDiskCache clearCache", e);
}
mDiskLruCache = null;
try {
init();
} catch (IOException e) {
Log.e(TAG, "SimpleDiskCache init", e);
result = false;
}
mDiskCacheState = STATE_DISK_CACHE_NONE;
notifyAll();
return result;
}
private synchronized CounterLock obtainLock(String key) {
// Wait for clear over
while (mDiskCacheState == STATE_DISK_CACHE_BUSY) {
try {
wait();
} catch (InterruptedException e) {
// Empty
}
}
mDiskCacheState = STATE_DISK_CACHE_IN_USE;
// Get lock from key
CounterLock lock;
lock = mLockMap.get(key);
if (lock == null) {
lock = mLockPool.get();
mLockMap.put(key, lock);
}
lock.obtain();
return lock;
}
private synchronized void releaseLock(String key, CounterLock lock) {
lock.release();
if (lock.isFree()) {
mLockMap.remove(key);
mLockPool.push(lock);
}
if (mLockMap.isEmpty()) {
mDiskCacheState = STATE_DISK_CACHE_NONE;
notifyAll();
}
}
public boolean contain(@NonNull String key) {
String diskKey = hashKeyForDisk(key);
CounterLock lock = obtainLock(diskKey);
if (null == mDiskLruCache) {
releaseLock(diskKey, lock);
return false;
}
lock.readLock().lock();
boolean result = mDiskLruCache.contain(diskKey);
lock.readLock().unlock();
releaseLock(diskKey, lock);
return result;
}
public boolean remove(@NonNull String key) {
String diskKey = hashKeyForDisk(key);
CounterLock lock = obtainLock(diskKey);
if (null == mDiskLruCache) {
releaseLock(diskKey, lock);
return false;
}
lock.writeLock().lock();
boolean result;
try {
result = mDiskLruCache.remove(diskKey);
} catch (IOException e) {
e.printStackTrace();
result = false;
}
lock.writeLock().unlock();
releaseLock(diskKey, lock);
return result;
}
public boolean put(@NonNull String key, @NonNull InputStream is) {
String diskKey = hashKeyForDisk(key);
CounterLock lock = obtainLock(diskKey);
if (null == mDiskLruCache) {
releaseLock(diskKey, lock);
return false;
}
lock.writeLock().lock();
boolean result = false;
if (isValid()) {
result = putToDisk(diskKey, is);
}
lock.writeLock().unlock();
releaseLock(diskKey, lock);
return result;
}
private boolean putToDisk(String key, InputStream is) {
DiskLruCache.Editor editor = null;
OutputStream os = null;
boolean completeEdit = false;
try {
//noinspection ConstantConditions
editor = mDiskLruCache.edit(key);
if (editor == null) {
// The editor is in progress
return false;
}
os = editor.newOutputStream(0);
if (os != null) {
Util.copy(is, os);
completeEdit = true;
editor.commit();
return true;
} else {
// Can't get OutputStream
completeEdit = true;
editor.abort();
return false;
}
} catch (IOException e) {
Util.closeQuietly(os);
try {
if (!completeEdit && editor != null) {
editor.abort();
}
} catch (IOException ignored) {
}
return false;
}
}
/**
* @param key the key of the target
* @return the InputStreamPipe, <code>null</code> for missing
*/
@Nullable
public InputStreamPipe getInputStreamPipe(@NonNull String key) {
if (contain(key)) {
String diskKey = hashKeyForDisk(key);
return new CacheInputStreamPipe(diskKey);
} else {
return null;
}
}
@NonNull
public OutputStreamPipe getOutputStreamPipe(@NonNull String key) {
String diskKey = hashKeyForDisk(key);
return new CacheOutputStreamPipe(diskKey);
}
private class CacheInputStreamPipe implements InputStreamPipe {
private final String mKey;
private CounterLock mLock;
private DiskLruCache.Snapshot mCurrentSnapshot;
private CacheInputStreamPipe(String key) {
mKey = key;
}
@Override
public void obtain() {
if (mLock == null) {
mLock = obtainLock(mKey);
mLock.readLock().lock();
}
}
@Override
public void release() {
if (mCurrentSnapshot != null) {
throw new IllegalStateException("Please close it first");
}
if (mLock != null) {
mLock.readLock().unlock();
releaseLock(mKey, mLock);
mLock = null;
}
}
@Override
public @NonNull
InputStream open() throws IOException {
if (mLock == null) {
throw new IllegalStateException("Please obtain it first");
}
if (mCurrentSnapshot != null) {
throw new IllegalStateException("Please close it before reopen");
}
if (null == mDiskLruCache) {
throw new IOException("Can't find disk lru cache");
}
DiskLruCache.Snapshot snapshot;
snapshot = mDiskLruCache.get(mKey);
if (snapshot == null) {
throw new IOException("Miss the key " + mKey);
}
mCurrentSnapshot = snapshot;
return snapshot.getInputStream(0);
}
@Override
public void close() {
Util.closeQuietly(mCurrentSnapshot);
mCurrentSnapshot = null;
}
}
private class CacheOutputStreamPipe implements OutputStreamPipe {
private final String mKey;
private CounterLock mLock;
private DiskLruCache.Editor mCurrentEditor;
private CacheOutputStreamPipe(String key) {
mKey = key;
}
@Override
public void obtain() {
if (mLock == null) {
mLock = obtainLock(mKey);
mLock.writeLock().lock();
}
}
@Override
public void release() {
if (mCurrentEditor != null) {
throw new IllegalStateException("Please close it first");
}
if (mLock != null) {
mLock.writeLock().unlock();
releaseLock(mKey, mLock);
mLock = null;
}
}
@Override
public @NonNull
OutputStream open() throws IOException {
if (mLock == null) {
throw new IllegalStateException("Please obtain it first");
}
if (mCurrentEditor != null) {
throw new IllegalStateException("Please close it before reopen");
}
if (null == mDiskLruCache) {
throw new IOException("Can't find disk lru cache");
}
DiskLruCache.Editor editor = mDiskLruCache.edit(mKey);
if (editor == null) {
throw new IOException("Miss the key " + mKey);
}
OutputStream os;
try {
os = editor.newOutputStream(0);
mCurrentEditor = editor;
return os;
} catch (IOException e) {
// Get trouble
try {
editor.abort();
} catch (IOException ex) {
// Ignore
}
throw e;
}
}
@Override
public void close() {
if (mCurrentEditor != null) {
try {
mCurrentEditor.commit();
} catch (IOException e) {
// Empty
}
mCurrentEditor = null;
}
}
}
private class CounterLock extends ReentrantReadWriteLock {
private int mCount;
public boolean isFree() {
return mCount == 0;
}
public void release() {
if (--mCount < 0) {
throw new IllegalStateException("Release time is more than occupy time");
}
}
public void obtain() {
++mCount;
}
}
private class LockPool {
private static final int MAX_SIZE = 10;
private final CounterLock[] mArray = new CounterLock[MAX_SIZE];
private final int mMaxSize = MAX_SIZE;
private int mSize;
public void push(CounterLock lock) {
if (lock != null && mSize < mMaxSize) {
mArray[mSize++] = lock;
}
}
public CounterLock get() {
if (mSize > 0) {
CounterLock lock = mArray[--mSize];
mArray[mSize] = null;
return lock;
} else {
return new CounterLock();
}
}
}
}
@@ -1,193 +0,0 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* 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.beerbelly;
import androidx.annotation.NonNull;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
/**
* Buffers input from an {@link InputStream} for reading lines.
*
* <p>This class is used for buffered reading of lines. For purposes of this class, a line ends
* with "\n" or "\r\n". End of input is reported by throwing {@code EOFException}. Unterminated
* line at end of input is invalid and will be ignored, the caller may use {@code
* hasUnterminatedLine()} to detect it after catching the {@code EOFException}.
*
* <p>This class is intended for reading input that strictly consists of lines, such as line-based
* cache entries or cache journal. Unlike the {@link java.io.BufferedReader} which in conjunction
* with {@link java.io.InputStreamReader} provides similar functionality, this class uses different
* end-of-input reporting and a more restrictive definition of a line.
*
* <p>This class supports only charsets that encode '\r' and '\n' as a single byte with value 13
* and 10, respectively, and the representation of no other character contains these values.
* We currently check in constructor that the charset is one of US-ASCII, UTF-8 and ISO-8859-1.
* The default charset is US_ASCII.
*/
class StrictLineReader implements Closeable {
private static final byte CR = (byte) '\r';
private static final byte LF = (byte) '\n';
private final InputStream in;
private final Charset charset;
/*
* Buffered data is stored in {@code buf}. As long as no exception occurs, 0 <= pos <= end
* and the data in the range [pos, end) is buffered for reading. At end of input, if there is
* an unterminated line, we set end == -1, otherwise end == pos. If the underlying
* {@code InputStream} throws an {@code IOException}, end may remain as either pos or -1.
*/
private byte[] buf;
private int pos;
private int end;
/**
* Constructs a new {@code LineReader} with the specified charset and the default capacity.
*
* @param in the {@code InputStream} to read data from.
* @param charset the charset used to decode data. Only US-ASCII, UTF-8 and ISO-8859-1 are
* supported.
* @throws NullPointerException if {@code in} or {@code charset} is null.
* @throws IllegalArgumentException if the specified charset is not supported.
*/
public StrictLineReader(InputStream in, Charset charset) {
this(in, 8192, charset);
}
/**
* Constructs a new {@code LineReader} with the specified capacity and charset.
*
* @param in the {@code InputStream} to read data from.
* @param capacity the capacity of the buffer.
* @param charset the charset used to decode data. Only US-ASCII, UTF-8 and ISO-8859-1 are
* supported.
* @throws NullPointerException if {@code in} or {@code charset} is null.
* @throws IllegalArgumentException if {@code capacity} is negative or zero
* or the specified charset is not supported.
*/
public StrictLineReader(InputStream in, int capacity, Charset charset) {
if (in == null || charset == null) {
throw new NullPointerException();
}
if (capacity < 0) {
throw new IllegalArgumentException("capacity <= 0");
}
if (!(charset.equals(Util.US_ASCII))) {
throw new IllegalArgumentException("Unsupported encoding");
}
this.in = in;
this.charset = charset;
buf = new byte[capacity];
}
/**
* Closes the reader by closing the underlying {@code InputStream} and
* marking this reader as closed.
*
* @throws IOException for errors when closing the underlying {@code InputStream}.
*/
public void close() throws IOException {
synchronized (in) {
if (buf != null) {
buf = null;
in.close();
}
}
}
/**
* Reads the next line. A line ends with {@code "\n"} or {@code "\r\n"},
* this end of line marker is not included in the result.
*
* @return the next line from the input.
* @throws IOException for underlying {@code InputStream} errors.
* @throws EOFException for the end of source stream.
*/
public String readLine() throws IOException {
synchronized (in) {
if (buf == null) {
throw new IOException("LineReader is closed");
}
// Read more data if we are at the end of the buffered data.
// Though it's an error to read after an exception, we will let {@code fillBuf()}
// throw again if that happens; thus we need to handle end == -1 as well as end == pos.
if (pos >= end) {
fillBuf();
}
// Try to find LF in the buffered data and return the line if successful.
for (int i = pos; i != end; ++i) {
if (buf[i] == LF) {
int lineEnd = (i != pos && buf[i - 1] == CR) ? i - 1 : i;
String res = new String(buf, pos, lineEnd - pos, charset);
pos = i + 1;
return res;
}
}
// Let's anticipate up to 80 characters on top of those already read.
ByteArrayOutputStream out = new ByteArrayOutputStream(end - pos + 80) {
@NonNull
@Override
public String toString() {
int length = (count > 0 && buf[count - 1] == CR) ? count - 1 : count;
return new String(buf, 0, length, charset);
}
};
while (true) {
out.write(buf, pos, end - pos);
// Mark unterminated line in case fillBuf throws EOFException or IOException.
end = -1;
fillBuf();
// Try to find LF in the buffered data and return the line if successful.
for (int i = pos; i != end; ++i) {
if (buf[i] == LF) {
if (i != pos) {
out.write(buf, pos, i - pos);
}
pos = i + 1;
return out.toString();
}
}
}
}
}
public boolean hasUnterminatedLine() {
return end == -1;
}
/**
* Reads new input data into the buffer. Call only with pos == end or end == -1,
* depending on the desired outcome if the function throws.
*/
private void fillBuf() throws IOException {
int result = in.read(buf, 0, buf.length);
if (result == -1) {
throw new EOFException();
}
pos = 0;
end = result;
}
}
@@ -1,102 +0,0 @@
/*
* Copyright (C) 2010 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* 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.beerbelly;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Reader;
import java.io.StringWriter;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
/**
* Junk drawer of utility methods.
*/
final class Util {
static final Charset US_ASCII = StandardCharsets.US_ASCII;
static final Charset UTF_8 = StandardCharsets.UTF_8;
private static final int EOF = -1;
private static final int DEFAULT_BUFFER_SIZE = 1024 * 4;
private Util() {
}
static String readFully(Reader reader) throws IOException {
try (reader) {
StringWriter writer = new StringWriter();
char[] buffer = new char[1024];
int count;
while ((count = reader.read(buffer)) != -1) {
writer.write(buffer, 0, count);
}
return writer.toString();
}
}
/**
* Deletes the contents of {@code dir}. Throws an IOException if any file
* could not be deleted, or if {@code dir} is not a readable directory.
*/
static void deleteContents(File dir) throws IOException {
File[] files = dir.listFiles();
if (files == null) {
throw new IOException("not a readable directory: " + dir);
}
for (File file : files) {
if (file.isDirectory()) {
deleteContents(file);
}
if (!file.delete()) {
throw new IOException("failed to delete file: " + file);
}
}
}
static void closeQuietly(/*Auto*/Closeable closeable) {
if (closeable != null) {
try {
closeable.close();
} catch (RuntimeException rethrown) {
throw rethrown;
} catch (Exception ignored) {
}
}
}
/**
* Copy bytes from an <code>InputStream</code> to an
* <code>OutputStream</code>.
*
* @param input the InputStream
* @param output the OutputStream
* @return the number of bytes copied
*/
public static long copy(InputStream input, OutputStream output)
throws IOException {
byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
long count = 0;
int n;
while (EOF != (n = input.read(buffer))) {
output.write(buffer, 0, n);
count += n;
}
return count;
}
}
@@ -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
@@ -49,7 +50,6 @@ 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
@@ -71,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>()
@@ -101,7 +101,7 @@ class EhApplication : SceneApplication(), DefaultLifecycleObserver, ImageLoaderF
}
handler?.uncaughtException(t, e)
}
super<SceneApplication>.onCreate()
super<Application>.onCreate()
Native.initialize()
GetText.initialize(this)
Settings.initialize()
@@ -281,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() }
@@ -111,6 +111,30 @@ 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();
@@ -291,7 +291,7 @@ public class Hosts {
}
}
private boolean contains(String host) {
public boolean contains(String host) {
try (Cursor cursor = db.rawQuery("SELECT * FROM " + TABLE_HOSTS + " WHERE " + COLUMN_HOST + " = ?;", new String[]{host})) {
return cursor.moveToNext();
}
@@ -85,13 +85,12 @@ public class Settings {
public static final int DEFAULT_READ_CACHE_SIZE = 320;
public static final String KEY_BUILT_IN_HOSTS = "built_in_hosts_2";
public static final String KEY_DOMAIN_FRONTING = "domain_fronting";
public static final String KEY_BYPASS_VPN = "bypass_vpn";
public static final String KEY_LIST_THUMB_SIZE = "list_tile_size";
private static final String KEY_HIDE_HV_EVENTS = "hide_hv_events";
private static final boolean DEFAULT_HIDE_HV_EVENTS = true;
private static final String KEY_SHOW_COMMENTS = "show_gallery_comments";
private static final boolean DEFAULT_SHOW_COMMENTS = true;
private static final String KEY_PREVIEW_NUM = "preview_num";
private static final int DEFAULT_PREVIEW_NUM = 60;
private static final String TAG = Settings.class.getSimpleName();
private static final String KEY_DISPLAY_NAME = "display_name";
private static final String DEFAULT_DISPLAY_NAME = null;
@@ -181,6 +180,7 @@ public class Settings {
private static final boolean DEFAULT_SAVE_CRASH_LOG = true;
private static final boolean DEFAULT_BUILT_IN_HOSTS = false;
private static final boolean DEFAULT_FRONTING = false;
private static final boolean DEFAULT_BYPASS_VPN = true;
private static final String KEY_PROXY_TYPE = "proxy_type";
private static final int DEFAULT_PROXY_TYPE = EhProxySelector.TYPE_SYSTEM;
private static final String KEY_PROXY_IP = "proxy_ip";
@@ -423,10 +423,6 @@ public class Settings {
putBoolean(KEY_SHOW_TAG_TRANSLATIONS, value);
}
public static int getPreviewNum() {
return getInt(KEY_PREVIEW_NUM, DEFAULT_PREVIEW_NUM);
}
public static int getDefaultCategories() {
return getInt(KEY_DEFAULT_CATEGORIES, DEFAULT_DEFAULT_CATEGORIES);
}
@@ -679,6 +675,10 @@ public class Settings {
putBoolean(KEY_DOMAIN_FRONTING, value);
}
public static boolean getBypassVPN() {
return getBoolean(KEY_BYPASS_VPN, DEFAULT_BYPASS_VPN);
}
public static int getProxyType() {
return getInt(KEY_PROXY_TYPE, DEFAULT_PROXY_TYPE);
}
@@ -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;
@@ -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
}
}
}
@@ -123,4 +123,8 @@ object EhDns : Dns {
return hosts[hostname] ?: builtInHosts[hostname].takeIf { Settings.getBuiltInHosts() }
?: Dns.SYSTEM.lookup(hostname)
}
}
fun isInHosts(hostname: String): Boolean {
return hosts.contains(hostname) || (builtInHosts.contains(hostname) && Settings.getBuiltInHosts())
}
}
@@ -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,8 +212,8 @@ object EhEngine {
}
@Throws(Throwable::class)
suspend fun getGalleryList(okHttpClient: OkHttpClient, url: String): GalleryListParser.Result {
val referer = EhUrl.getReferer()
suspend fun getGalleryList(url: String): GalleryListParser.Result {
val referer = EhUrl.referer
Log.d(TAG, url)
val request = EhRequestBuilder(url, referer).build()
val call = okHttpClient.newCall(request)
@@ -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
) {
@@ -281,8 +279,8 @@ object EhEngine {
}
json.put("gidlist", ja)
json.put("namespace", 1)
val url = EhUrl.getApiUrl()
val origin = EhUrl.getOrigin()
val url = EhUrl.apiUrl
val origin = EhUrl.origin
Log.d(TAG, url)
val request = EhRequestBuilder(url, referer, origin)
.post(json.toString().toRequestBody(MEDIA_TYPE_JSON))
@@ -307,8 +305,8 @@ object EhEngine {
}
@Throws(Throwable::class)
suspend fun getGalleryDetail(okHttpClient: OkHttpClient, url: String?): GalleryDetail {
val referer = EhUrl.getReferer()
suspend fun getGalleryDetail(url: String?): GalleryDetail {
val referer = EhUrl.referer
Log.d(TAG, url!!)
val request = EhRequestBuilder(url, referer).build()
val call = okHttpClient.newCall(request)
@@ -335,8 +333,8 @@ object EhEngine {
}
@Throws(Throwable::class)
suspend fun getPreviewSet(okHttpClient: OkHttpClient, url: String?): Pair<PreviewSet, Int> {
val referer = EhUrl.getReferer()
suspend fun getPreviewSet(url: String?): Pair<PreviewSet, Int> {
val referer = EhUrl.referer
Log.d(TAG, url!!)
val request = EhRequestBuilder(url, referer).build()
val call = okHttpClient.newCall(request)
@@ -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 {
@@ -375,9 +372,9 @@ object EhEngine {
json.put("token", token)
json.put("rating", ceil((rating * 2).toDouble()).toInt())
val requestBody: RequestBody = json.toString().toRequestBody(MEDIA_TYPE_JSON)
val url = EhUrl.getApiUrl()
val url = EhUrl.apiUrl
val referer = EhUrl.getGalleryDetailUrl(gid, token)
val origin = EhUrl.getOrigin()
val origin = EhUrl.origin
Log.d(TAG, url)
val request = EhRequestBuilder(url, referer, origin)
.post(requestBody)
@@ -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()
@@ -413,7 +409,7 @@ object EhEngine {
builder.add("commenttext_edit", comment)
builder.add("edit_comment", id)
}
val origin = EhUrl.getOrigin()
val origin = EhUrl.origin
Log.d(TAG, url!!)
val request = EhRequestBuilder(url, url, origin)
.post(builder.build())
@@ -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()
@@ -455,9 +451,9 @@ object EhEngine {
)
)
val requestBody: RequestBody = json.toString().toRequestBody(MEDIA_TYPE_JSON)
val url = EhUrl.getApiUrl()
val referer = EhUrl.getReferer()
val origin = EhUrl.getOrigin()
val url = EhUrl.apiUrl
val referer = EhUrl.referer
val origin = EhUrl.origin
Log.d(TAG, url)
val request = EhRequestBuilder(url, referer, origin)
.post(requestBody)
@@ -482,9 +478,9 @@ object EhEngine {
@Throws(Throwable::class)
suspend fun getFavorites(
okHttpClient: OkHttpClient, url: String
url: String
): FavoritesParser.Result {
val referer = EhUrl.getReferer()
val referer = EhUrl.referer
Log.d(TAG, url)
val request = EhRequestBuilder(url, referer).build()
val call = okHttpClient.newCall(request)
@@ -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) {
@@ -538,7 +534,7 @@ object EhEngine {
builder.add("submit", "Apply Changes")
builder.add("update", "1")
val url = EhUrl.getAddFavorites(gid, token)
val origin = EhUrl.getOrigin()
val origin = EhUrl.origin
Log.d(TAG, url)
val request = EhRequestBuilder(url, url, origin)
.post(builder.build())
@@ -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) {
@@ -601,7 +597,7 @@ object EhEngine {
builder.add("modifygids[]", gid.toString())
}
builder.add("apply", "Apply")
val origin = EhUrl.getOrigin()
val origin = EhUrl.origin
Log.d(TAG, url)
val request = EhRequestBuilder(url, url, origin)
.post(builder.build())
@@ -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)
@@ -673,7 +669,14 @@ object EhEngine {
code = response.code
headers = response.headers
body = response.body.string()
result = ArchiveParser.parse(body)
result = ArchiveParser.parse(body!!)!!
if (result.funds == null) {
try {
result.funds = getFunds()
} catch (e: Throwable) {
e.printStackTrace()
}
}
}
} catch (e: Throwable) {
ExceptionUtils.throwIfFatal(e)
@@ -685,7 +688,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()) {
@@ -707,7 +710,7 @@ object EhEngine {
}
val url = EhUrl.getDownloadArchive(gid, token, or)
val referer = EhUrl.getGalleryDetailUrl(gid, token)
val origin = EhUrl.getOrigin()
val origin = EhUrl.origin
Log.d(TAG, url)
val request = EhRequestBuilder(url, referer, origin)
.post(builder.build())
@@ -715,6 +718,7 @@ object EhEngine {
val call = okHttpClient.newCall(request)
var body: String? = null
var headers: Headers? = null
var result: String?
var code = -1
try {
call.executeAsync().use { response ->
@@ -722,15 +726,16 @@ object EhEngine {
headers = response.headers
body = response.body.string()
transformException(code, headers, body, null)
result = ArchiveParser.parseArchiveUrl(body!!)
}
} catch (e: Throwable) {
ExceptionUtils.throwIfFatal(e)
transformException(code, headers, body, e)
throw e
}
var result = ArchiveParser.parseArchiveUrl(body)
if (!isHAtH) {
if (result == null) {
// Wait for the server to prepare archives
delay(1000)
try {
call.clone().executeAsync().use { response ->
@@ -738,13 +743,13 @@ object EhEngine {
headers = response.headers
body = response.body.string()
transformException(code, headers, body, null)
result = ArchiveParser.parseArchiveUrl(body!!)
}
} catch (e: Throwable) {
ExceptionUtils.throwIfFatal(e)
transformException(code, headers, body, e)
throw e
}
result = ArchiveParser.parseArchiveUrl(body)
if (result == null) {
throw EhException("Archive unavailable")
}
@@ -754,9 +759,31 @@ object EhEngine {
return null
}
@Throws(Throwable::class)
suspend fun getFunds(): ArchiveParser.Funds? {
val url = EhUrl.URL_FUNDS
Log.d(TAG, url)
val request = EhRequestBuilder(url, null).build()
val call = okHttpClient.newCall(request)
var body: String? = null
var headers: Headers? = null
var code = -1
try {
call.executeAsync().use { response ->
code = response.code
headers = response.headers
body = response.body.string()
return ArchiveParser.parseFunds(body!!)
}
} catch (e: Throwable) {
ExceptionUtils.throwIfFatal(e)
transformException(code, headers, body, e)
throw e
}
}
@Throws(Throwable::class)
private suspend fun getProfileInternal(
okHttpClient: OkHttpClient,
url: String, referer: String
): ProfileParser.Result {
Log.d(TAG, url)
@@ -780,9 +807,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 +820,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,10 +830,8 @@ object EhEngine {
}
@Throws(Throwable::class)
suspend fun getUConfig(
okHttpClient: OkHttpClient
): Void? {
val url = EhUrl.getUConfigUrl()
suspend fun getUConfig(): Void? {
val url = EhUrl.uConfigUrl
Log.d(TAG, url)
var request = EhRequestBuilder(url, null).build()
var call = okHttpClient.newCall(request)
@@ -852,7 +875,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()
@@ -864,9 +887,9 @@ object EhEngine {
json.put("comment_id", commentId)
json.put("comment_vote", commentVote)
val requestBody: RequestBody = json.toString().toRequestBody(MEDIA_TYPE_JSON)
val url = EhUrl.getApiUrl()
val referer = EhUrl.getReferer()
val origin = EhUrl.getOrigin()
val url = EhUrl.apiUrl
val referer = EhUrl.referer
val origin = EhUrl.origin
Log.d(TAG, url)
val request = EhRequestBuilder(url, referer, origin)
.post(requestBody)
@@ -891,7 +914,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()
@@ -903,9 +926,9 @@ object EhEngine {
json.put("tags", tags)
json.put("vote", vote)
val requestBody: RequestBody = json.toString().toRequestBody(MEDIA_TYPE_JSON)
val url = EhUrl.getApiUrl()
val referer = EhUrl.getReferer()
val origin = EhUrl.getOrigin()
val url = EhUrl.apiUrl
val referer = EhUrl.referer
val origin = EhUrl.origin
Log.d(TAG, url)
val request = EhRequestBuilder(url, referer, origin)
.post(requestBody)
@@ -933,7 +956,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()
@@ -967,9 +990,9 @@ object EhEngine {
Headers.headersOf("Content-Disposition", "form-data; name=\"f_sfile\""),
"File Search".toRequestBody()
)
val url = EhUrl.getImageSearchUrl()
val referer = EhUrl.getReferer()
val origin = EhUrl.getOrigin()
val url = EhUrl.imageSearchUrl
val referer = EhUrl.referer
val origin = EhUrl.origin
Log.d(TAG, url)
val request = EhRequestBuilder(url, referer, origin)
.post(builder.build())
@@ -992,14 +1015,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 +1051,6 @@ object EhEngine {
@JvmStatic
@Throws(Throwable::class)
fun getGalleryPageApi(
okHttpClient: OkHttpClient,
gid: Long,
index: Int,
pToken: String?,
@@ -1043,12 +1064,12 @@ object EhEngine {
json.put("imgkey", pToken)
json.put("showkey", showKey)
val requestBody: RequestBody = json.toString().toRequestBody(MEDIA_TYPE_JSON)
val url = EhUrl.getApiUrl()
val url = EhUrl.apiUrl
var referer: String? = null
if (index > 0 && previousPToken != null) {
referer = EhUrl.getPageUrl(gid, index - 1, previousPToken)
}
val origin = EhUrl.getOrigin()
val origin = EhUrl.origin
Log.d(TAG, url)
val request = EhRequestBuilder(url, referer, origin)
.post(requestBody)
@@ -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.getTitle();
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.getUploader();
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.getSimpleTags();
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.getSimpleTags();
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) {
@@ -34,8 +34,8 @@ object EhSSLSocketFactory : SSLSocketFactory() {
}
override fun createSocket(s: Socket, host: String, port: Int, autoClose: Boolean): Socket {
val address = s.inetAddress.hostAddress
val socket = sslSocketFactory.createSocket(s, address, port, autoClose) as SSLSocket
val address = s.inetAddress.hostAddress.takeIf { EhDns.isInHosts(host) }
val socket = sslSocketFactory.createSocket(s, address ?: host, port, autoClose) as SSLSocket
val sslSession = socket.session
Log.d(
"EhSSLSocketFactory",
@@ -1,196 +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 androidx.annotation.NonNull;
import com.hippo.ehviewer.Settings;
import com.hippo.network.UrlBuilder;
public class EhUrl {
public static final int SITE_E = 0;
public static final int SITE_EX = 1;
public static final String DOMAIN_EX = "exhentai.org";
public static final String DOMAIN_E = "e-hentai.org";
public static final String DOMAIN_LOFI = "lofi.e-hentai.org";
public static final String HOST_EX = "https://" + DOMAIN_EX + "/";
public static final String API_EX = HOST_EX + "api.php";
public static final String URL_FAVORITES_EX = HOST_EX + "favorites.php";
public static final String URL_UCONFIG_EX = HOST_EX + "uconfig.php";
public static final String URL_MY_TAGS_EX = HOST_EX + "mytags";
public static final String URL_WATCHED_EX = HOST_EX + "watched";
public static final String HOST_E = "https://" + DOMAIN_E + "/";
public static final String API_E = HOST_E + "api.php";
public static final String URL_FAVORITES_E = HOST_E + "favorites.php";
public static final String URL_UCONFIG_E = HOST_E + "uconfig.php";
public static final String URL_MY_TAGS_E = HOST_E + "mytags";
public static final String URL_WATCHED_E = HOST_E + "watched";
public static final String API_SIGN_IN = "https://forums.e-hentai.org/index.php?act=Login&CODE=01";
public static final String URL_POPULAR_E = "https://e-hentai.org/popular";
public static final String URL_POPULAR_EX = "https://exhentai.org/popular";
public static final String URL_IMAGE_SEARCH_E = "https://upload.e-hentai.org/image_lookup.php";
public static final String URL_IMAGE_SEARCH_EX = "https://exhentai.org/upload/image_lookup.php";
public static final String URL_SIGN_IN = "https://forums.e-hentai.org/index.php?act=Login";
public static final String URL_REGISTER = "https://forums.e-hentai.org/index.php?act=Reg&CODE=00";
public static final String URL_FORUMS = "https://forums.e-hentai.org/";
public static final String REFERER_EX = "https://" + DOMAIN_EX;
public static final String ORIGIN_EX = REFERER_EX;
public static final String REFERER_E = "https://" + DOMAIN_E;
public static final String ORIGIN_E = REFERER_E;
public static String getGalleryDetailUrl(long gid, String token) {
return getGalleryDetailUrl(gid, token, 0, false);
}
public static String getHost() {
switch (Settings.getGallerySite()) {
default:
case SITE_E:
return HOST_E;
case SITE_EX:
return HOST_EX;
}
}
public static String getFavoritesUrl() {
switch (Settings.getGallerySite()) {
default:
case SITE_E:
return URL_FAVORITES_E;
case SITE_EX:
return URL_FAVORITES_EX;
}
}
public static String getApiUrl() {
switch (Settings.getGallerySite()) {
default:
case SITE_E:
return API_E;
case SITE_EX:
return API_EX;
}
}
public static String getReferer() {
switch (Settings.getGallerySite()) {
default:
case SITE_E:
return REFERER_E;
case SITE_EX:
return REFERER_EX;
}
}
public static String getOrigin() {
switch (Settings.getGallerySite()) {
default:
case SITE_E:
return ORIGIN_E;
case SITE_EX:
return ORIGIN_EX;
}
}
public static String getUConfigUrl() {
switch (Settings.getGallerySite()) {
default:
case SITE_E:
return URL_UCONFIG_E;
case SITE_EX:
return URL_UCONFIG_EX;
}
}
public static String getMyTagsUrl() {
switch (Settings.getGallerySite()) {
default:
case SITE_E:
return URL_MY_TAGS_E;
case SITE_EX:
return URL_MY_TAGS_EX;
}
}
public static String getGalleryDetailUrl(long gid, String token, int index, boolean allComment) {
UrlBuilder builder = new UrlBuilder(getHost() + "g/" + gid + '/' + token + '/');
if (index != 0) {
builder.addQuery("p", index);
}
if (allComment) {
builder.addQuery("hc", 1);
}
return builder.build();
}
public static String getGalleryMultiPageViewerUrl(long gid, String token) {
UrlBuilder builder = new UrlBuilder(getHost() + "mpv/" + gid + '/' + token + '/');
return builder.build();
}
public static String getPageUrl(long gid, int index, String pToken) {
return getHost() + "s/" + pToken + '/' + gid + '-' + (index + 1);
}
public static String getAddFavorites(long gid, String token) {
return getHost() + "gallerypopups.php?gid=" + gid + "&t=" + token + "&act=addfav";
}
public static String getDownloadArchive(long gid, String token, String or) {
return getHost() + "archiver.php?gid=" + gid + "&token=" + token + "&or=" + or;
}
public static String getTagDefinitionUrl(String tag) {
return "https://ehwiki.org/wiki/" + tag.replace(' ', '_');
}
@NonNull
public static String getPopularUrl() {
switch (Settings.getGallerySite()) {
default:
case SITE_E:
return URL_POPULAR_E;
case SITE_EX:
return URL_POPULAR_EX;
}
}
@NonNull
public static String getImageSearchUrl() {
switch (Settings.getGallerySite()) {
default:
case SITE_E:
return URL_IMAGE_SEARCH_E;
case SITE_EX:
return URL_IMAGE_SEARCH_EX;
}
}
@NonNull
public static String getWatchedUrl() {
switch (Settings.getGallerySite()) {
default:
case SITE_E:
return URL_WATCHED_E;
case SITE_EX:
return URL_WATCHED_EX;
}
}
}
@@ -0,0 +1,174 @@
/*
* 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.Settings
import com.hippo.network.UrlBuilder
object EhUrl {
const val SITE_E = 0
const val SITE_EX = 1
const val DOMAIN_EX = "exhentai.org"
const val DOMAIN_E = "e-hentai.org"
const val DOMAIN_LOFI = "lofi.e-hentai.org"
const val HOST_EX = "https://$DOMAIN_EX/"
const val API_EX = HOST_EX + "api.php"
const val URL_FAVORITES_EX = HOST_EX + "favorites.php"
const val URL_UCONFIG_EX = HOST_EX + "uconfig.php"
const val URL_MY_TAGS_EX = HOST_EX + "mytags"
const val URL_WATCHED_EX = HOST_EX + "watched"
const val HOST_E = "https://$DOMAIN_E/"
const val API_E = HOST_E + "api.php"
const val URL_FAVORITES_E = HOST_E + "favorites.php"
const val URL_UCONFIG_E = HOST_E + "uconfig.php"
const val URL_MY_TAGS_E = HOST_E + "mytags"
const val URL_WATCHED_E = HOST_E + "watched"
const val API_SIGN_IN = "https://forums.e-hentai.org/index.php?act=Login&CODE=01"
const val URL_POPULAR_E = "https://e-hentai.org/popular"
const val URL_POPULAR_EX = "https://exhentai.org/popular"
const val URL_IMAGE_SEARCH_E = "https://upload.e-hentai.org/image_lookup.php"
const val URL_IMAGE_SEARCH_EX = "https://exhentai.org/upload/image_lookup.php"
const val URL_SIGN_IN = "https://forums.e-hentai.org/index.php?act=Login"
const val URL_REGISTER = "https://forums.e-hentai.org/index.php?act=Reg&CODE=00"
const val URL_FORUMS = "https://forums.e-hentai.org/"
const val URL_FUNDS = "https://e-hentai.org/exchange.php?t=gp"
const val REFERER_EX = "https://$DOMAIN_EX"
const val ORIGIN_EX = REFERER_EX
const val REFERER_E = "https://$DOMAIN_E"
const val ORIGIN_E = REFERER_E
const val URL_PREFIX_THUMB_E = "https://ehgt.org/"
const val URL_PREFIX_THUMB_EX = "https://exhentai.org/t/"
val host: String
get() = when (Settings.getGallerySite()) {
SITE_E -> HOST_E
SITE_EX -> HOST_EX
else -> HOST_E
}
val favoritesUrl: String
get() = when (Settings.getGallerySite()) {
SITE_E -> URL_FAVORITES_E
SITE_EX -> URL_FAVORITES_EX
else -> URL_FAVORITES_E
}
val apiUrl: String
get() = when (Settings.getGallerySite()) {
SITE_E -> API_E
SITE_EX -> API_EX
else -> API_E
}
@JvmStatic
val referer: String
get() = when (Settings.getGallerySite()) {
SITE_E -> REFERER_E
SITE_EX -> REFERER_EX
else -> REFERER_E
}
val origin: String
get() = when (Settings.getGallerySite()) {
SITE_E -> ORIGIN_E
SITE_EX -> ORIGIN_EX
else -> ORIGIN_E
}
@JvmStatic
val uConfigUrl: String
get() = when (Settings.getGallerySite()) {
SITE_E -> URL_UCONFIG_E
SITE_EX -> URL_UCONFIG_EX
else -> URL_UCONFIG_E
}
@JvmStatic
val myTagsUrl: String
get() = when (Settings.getGallerySite()) {
SITE_E -> URL_MY_TAGS_E
SITE_EX -> URL_MY_TAGS_EX
else -> URL_MY_TAGS_E
}
val popularUrl: String
get() = when (Settings.getGallerySite()) {
SITE_E -> URL_POPULAR_E
SITE_EX -> URL_POPULAR_EX
else -> URL_POPULAR_E
}
val imageSearchUrl: String
get() = when (Settings.getGallerySite()) {
SITE_E -> URL_IMAGE_SEARCH_E
SITE_EX -> URL_IMAGE_SEARCH_EX
else -> URL_IMAGE_SEARCH_E
}
val watchedUrl: String
get() = when (Settings.getGallerySite()) {
SITE_E -> URL_WATCHED_E
SITE_EX -> URL_WATCHED_EX
else -> URL_WATCHED_E
}
fun getGalleryDetailUrl(gid: Long, token: String?): String {
return getGalleryDetailUrl(gid, token, 0, false)
}
@JvmStatic
fun getGalleryDetailUrl(gid: Long, token: String?, index: Int, allComment: Boolean): String {
val builder = UrlBuilder(host + "g/" + gid + '/' + token + '/')
if (index != 0) {
builder.addQuery("p", index)
}
if (allComment) {
builder.addQuery("hc", 1)
}
return builder.build()
}
@JvmStatic
fun getGalleryMultiPageViewerUrl(gid: Long, token: String): String {
val builder = UrlBuilder(host + "mpv/" + gid + '/' + token + '/')
return builder.build()
}
@JvmStatic
fun getPageUrl(gid: Long, index: Int, pToken: String): String {
return host + "s/" + pToken + '/' + gid + '-' + (index + 1)
}
fun getAddFavorites(gid: Long, token: String?): String {
return host + "gallerypopups.php?gid=" + gid + "&t=" + token + "&act=addfav"
}
fun getDownloadArchive(gid: Long, token: String?, or: String): String {
return host + "archiver.php?gid=" + gid + "&token=" + token + "&or=" + or
}
fun getTagDefinitionUrl(tag: String): String {
return "https://ehwiki.org/wiki/" + tag.replace(' ', '_')
}
fun getThumbUrlPrefix(): String {
return when (Settings.getGallerySite()) {
SITE_E -> URL_PREFIX_THUMB_E
SITE_EX -> URL_PREFIX_THUMB_EX
else -> URL_PREFIX_THUMB_E
}
}
}
@@ -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;
}
}
@@ -1,189 +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.graphics.Color;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.hippo.ehviewer.EhApplication;
import com.hippo.ehviewer.Settings;
import com.hippo.ehviewer.client.data.GalleryInfo;
import java.util.regex.Pattern;
public class EhUtils {
public static final int NONE = -1; // Use it for homepage
public static final int PRIVATE = 0x400;
public static final int UNKNOWN = 0x800;
public static final int BG_COLOR_DOUJINSHI = 0xfff44336;
public static final int BG_COLOR_MANGA = 0xffff9800;
public static final int BG_COLOR_ARTIST_CG = 0xfffbc02d;
public static final int BG_COLOR_GAME_CG = 0xff4caf50;
public static final int BG_COLOR_WESTERN = 0xff8bc34a;
public static final int BG_COLOR_NON_H = 0xff2196f3;
public static final int BG_COLOR_IMAGE_SET = 0xff3f51b5;
public static final int BG_COLOR_COSPLAY = 0xff9c27b0;
public static final int BG_COLOR_ASIAN_PORN = 0xff9575cd;
public static final int BG_COLOR_MISC = 0xfff06292;
public static final int BG_COLOR_UNKNOWN = Color.BLACK;
// Remove [XXX], (XXX), {XXX}, ~XXX~ stuff
public static final Pattern PATTERN_TITLE_PREFIX = Pattern.compile(
"^(?:\\([^)]*\\)|\\[[^]]*]|\\{[^}]*\\}|~[^~]*~|\\s+)*");
// Remove [XXX], (XXX), {XXX}, ~XXX~ stuff and something like ch. 1-23
public static final Pattern PATTERN_TITLE_SUFFIX = Pattern.compile(
"(?:\\s+ch.[\\s\\d-]+)?(?:\\([^)]*\\)|\\[[^]]*]|\\{[^}]*\\}|~[^~]*~|\\s+)*$",
Pattern.CASE_INSENSITIVE);
private static final int[] CATEGORY_VALUES = {
EhConfig.MISC,
EhConfig.DOUJINSHI,
EhConfig.MANGA,
EhConfig.ARTIST_CG,
EhConfig.GAME_CG,
EhConfig.IMAGE_SET,
EhConfig.COSPLAY,
EhConfig.ASIAN_PORN,
EhConfig.NON_H,
EhConfig.WESTERN,
PRIVATE,
UNKNOWN};
private static final String[][] CATEGORY_STRINGS = {
new String[]{"misc"},
new String[]{"doujinshi"},
new String[]{"manga"},
new String[]{"artistcg", "Artist CG Sets", "Artist CG"},
new String[]{"gamecg", "Game CG Sets", "Game CG"},
new String[]{"imageset", "Image Sets", "Image Set"},
new String[]{"cosplay"},
new String[]{"asianporn", "Asian Porn"},
new String[]{"non-h"},
new String[]{"western"},
new String[]{"private"},
new String[]{"unknown"}
};
public static int getCategory(String type) {
int i;
for (i = 0; i < CATEGORY_STRINGS.length - 1; i++) {
for (String str : CATEGORY_STRINGS[i])
if (str.equalsIgnoreCase(type))
return CATEGORY_VALUES[i];
}
return CATEGORY_VALUES[i];
}
@NonNull
public static String getCategory(int type) {
int i;
for (i = 0; i < CATEGORY_VALUES.length - 1; i++) {
if (CATEGORY_VALUES[i] == type)
break;
}
return CATEGORY_STRINGS[i][0];
}
public static int getCategoryColor(int category) {
return switch (category) {
case EhConfig.DOUJINSHI -> BG_COLOR_DOUJINSHI;
case EhConfig.MANGA -> BG_COLOR_MANGA;
case EhConfig.ARTIST_CG -> BG_COLOR_ARTIST_CG;
case EhConfig.GAME_CG -> BG_COLOR_GAME_CG;
case EhConfig.WESTERN -> BG_COLOR_WESTERN;
case EhConfig.NON_H -> BG_COLOR_NON_H;
case EhConfig.IMAGE_SET -> BG_COLOR_IMAGE_SET;
case EhConfig.COSPLAY -> BG_COLOR_COSPLAY;
case EhConfig.ASIAN_PORN -> BG_COLOR_ASIAN_PORN;
case EhConfig.MISC -> BG_COLOR_MISC;
default -> BG_COLOR_UNKNOWN;
};
}
public static void signOut() {
EhApplication.getEhCookieStore().signOut();
Settings.putAvatar(null);
Settings.putDisplayName(null);
Settings.putNeedSignIn(true);
}
public static boolean needSignedIn() {
return Settings.getNeedSignIn() && !EhApplication.getEhCookieStore().hasSignedIn();
}
public static String getSuitableTitle(GalleryInfo gi) {
if (Settings.getShowJpnTitle()) {
return TextUtils.isEmpty(gi.getTitleJpn()) ? gi.getTitle() : gi.getTitleJpn();
} else {
return TextUtils.isEmpty(gi.getTitle()) ? gi.getTitleJpn() : gi.getTitle();
}
}
@Nullable
public static String extractTitle(String title) {
if (null == title) {
return null;
}
title = PATTERN_TITLE_PREFIX.matcher(title).replaceFirst("");
title = PATTERN_TITLE_SUFFIX.matcher(title).replaceFirst("");
// Sometimes title is combined by romaji and english translation.
// Only need romaji.
// TODO But not sure every '|' means that
int index = title.indexOf('|');
if (index >= 0) {
title = title.substring(0, index);
}
if (title.isEmpty()) {
return null;
} else {
return title;
}
}
public static String handleThumbUrlResolution(String url) {
if (null == url) {
return null;
}
String resolution;
switch (Settings.getThumbResolution()) {
default:
case 0: // Auto
return url;
case 1: // 250
resolution = "250";
break;
case 2: // 300
resolution = "300";
break;
}
int index1 = url.lastIndexOf('_');
int index2 = url.lastIndexOf('.');
if (index1 >= 0 && index2 >= 0 && index1 < index2) {
return url.substring(0, index1 + 1) + resolution + url.substring(index2);
} else {
return url;
}
}
}
@@ -0,0 +1,158 @@
/*
* 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.text.TextUtils
import com.hippo.ehviewer.EhApplication.Companion.ehCookieStore
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.data.GalleryInfo
import java.util.regex.Pattern
object EhUtils {
const val NONE = -1 // Use it for homepage
const val PRIVATE = 0x400
const val UNKNOWN = 0x800
// https://youtrack.jetbrains.com/issue/KT-4749
private const val BG_COLOR_DOUJINSHI = 0xfff44336u
private const val BG_COLOR_MANGA = 0xffff9800u
private const val BG_COLOR_ARTIST_CG = 0xfffbc02du
private const val BG_COLOR_GAME_CG = 0xff4caf50u
private const val BG_COLOR_WESTERN = 0xff8bc34au
private const val BG_COLOR_NON_H = 0xff2196f3u
private const val BG_COLOR_IMAGE_SET = 0xff3f51b5u
private const val BG_COLOR_COSPLAY = 0xff9c27b0u
private const val BG_COLOR_ASIAN_PORN = 0xff9575cdu
private const val BG_COLOR_MISC = 0xfff06292u
private const val BG_COLOR_UNKNOWN = 0xff000000u
// Remove [XXX], (XXX), {XXX}, ~XXX~ stuff
private val PATTERN_TITLE_PREFIX = Pattern.compile(
"^(?:\\([^)]*\\)|\\[[^]]*]|\\{[^}]*\\}|~[^~]*~|\\s+)*"
)
// Remove [XXX], (XXX), {XXX}, ~XXX~ stuff and something like ch. 1-23
private val PATTERN_TITLE_SUFFIX = Pattern.compile(
"(?:\\s+ch.[\\s\\d-]+)?(?:\\([^)]*\\)|\\[[^]]*]|\\{[^}]*\\}|~[^~]*~|\\s+)*$",
Pattern.CASE_INSENSITIVE
)
private val CATEGORY_VALUES = hashMapOf(
EhConfig.MISC to arrayOf("misc"),
EhConfig.DOUJINSHI to arrayOf("doujinshi"),
EhConfig.MANGA to arrayOf("manga"),
EhConfig.ARTIST_CG to arrayOf("artistcg", "Artist CG Sets", "Artist CG"),
EhConfig.GAME_CG to arrayOf("gamecg", "Game CG Sets", "Game CG"),
EhConfig.IMAGE_SET to arrayOf("imageset", "Image Sets", "Image Set"),
EhConfig.COSPLAY to arrayOf("cosplay"),
EhConfig.ASIAN_PORN to arrayOf("asianporn", "Asian Porn"),
EhConfig.NON_H to arrayOf("non-h"),
EhConfig.WESTERN to arrayOf("western"),
PRIVATE to arrayOf("private"),
UNKNOWN to arrayOf("unknown")
)
private val CATEGORY_STRINGS = CATEGORY_VALUES.entries.map { (k, v) -> v to k }
@JvmStatic
fun getCategory(type: String?): Int {
for (entry in CATEGORY_STRINGS) {
for (str in entry.first)
if (str.equals(type, ignoreCase = true))
return entry.second
}
return UNKNOWN
}
fun getCategory(type: Int): String {
return CATEGORY_VALUES.getOrDefault(type, CATEGORY_VALUES[UNKNOWN])!![0]
}
fun getCategoryColor(category: Int): Int {
return when (category) {
EhConfig.DOUJINSHI -> BG_COLOR_DOUJINSHI
EhConfig.MANGA -> BG_COLOR_MANGA
EhConfig.ARTIST_CG -> BG_COLOR_ARTIST_CG
EhConfig.GAME_CG -> BG_COLOR_GAME_CG
EhConfig.WESTERN -> BG_COLOR_WESTERN
EhConfig.NON_H -> BG_COLOR_NON_H
EhConfig.IMAGE_SET -> BG_COLOR_IMAGE_SET
EhConfig.COSPLAY -> BG_COLOR_COSPLAY
EhConfig.ASIAN_PORN -> BG_COLOR_ASIAN_PORN
EhConfig.MISC -> BG_COLOR_MISC
else -> BG_COLOR_UNKNOWN
}.toInt()
}
fun signOut() {
ehCookieStore.signOut()
Settings.putAvatar(null)
Settings.putDisplayName(null)
Settings.putGallerySite(EhUrl.SITE_E)
Settings.putNeedSignIn(true)
}
fun needSignedIn(): Boolean {
return Settings.getNeedSignIn() && !ehCookieStore.hasSignedIn()
}
@JvmStatic
fun getSuitableTitle(gi: GalleryInfo): String? {
return if (Settings.getShowJpnTitle()) {
if (TextUtils.isEmpty(gi.titleJpn)) gi.title else gi.titleJpn
} else {
if (TextUtils.isEmpty(gi.title)) gi.titleJpn else gi.title
}
}
fun extractTitle(fullTitle: String?): String? {
var title: String = fullTitle ?: return null
title = PATTERN_TITLE_PREFIX.matcher(title).replaceFirst("")
title = PATTERN_TITLE_SUFFIX.matcher(title).replaceFirst("")
// Sometimes title is combined by romaji and english translation.
// Only need romaji.
// TODO But not sure every '|' means that
val index = title.indexOf('|')
if (index >= 0) {
title = title.substring(0, index)
}
return title.ifEmpty { null }
}
fun fixThumbUrl(url: String): String {
return EhUrl.getThumbUrlPrefix() +
url.removePrefix(EhUrl.URL_PREFIX_THUMB_E).removePrefix(EhUrl.URL_PREFIX_THUMB_EX)
}
@JvmStatic
fun handleThumbUrlResolution(url: String?): String? {
if (null == url) {
return null
}
val resolution = when (Settings.getThumbResolution()) {
0 -> return url
1 -> "250"
2 -> "300"
else -> return url
}
val index1 = url.lastIndexOf('_')
val index2 = url.lastIndexOf('.')
return if (index1 >= 0 && index2 >= 0 && index1 < index2) {
url.substring(0, index1 + 1) + resolution + url.substring(index2)
} else {
url
}
}
}
@@ -35,7 +35,7 @@ class FavListUrlBuilder(
}
fun build(): String {
val ub = UrlBuilder(EhUrl.getFavoritesUrl())
val ub = UrlBuilder(EhUrl.favoritesUrl)
if (isValidFavCat(favCat)) {
ub.addQuery("favcat", favCat.toString())
} else if (favCat == FAV_CAT_ALL) {
@@ -275,9 +275,9 @@ data class ListUrlBuilder(
return when (mode) {
MODE_NORMAL, MODE_SUBSCRIPTION -> {
val url: String = if (mode == MODE_NORMAL) {
EhUrl.getHost()
EhUrl.host
} else {
EhUrl.getWatchedUrl()
EhUrl.watchedUrl
}
val ub = UrlBuilder(url)
if (this.category != EhUtils.NONE) {
@@ -326,7 +326,7 @@ data class ListUrlBuilder(
}
MODE_UPLOADER -> {
val sb = StringBuilder(EhUrl.getHost())
val sb = StringBuilder(EhUrl.host)
mKeyword?.let {
sb.append("uploader/")
sb.append(encodeUTF8(it))
@@ -344,7 +344,7 @@ data class ListUrlBuilder(
}
MODE_TAG -> {
val sb = StringBuilder(EhUrl.getHost())
val sb = StringBuilder(EhUrl.host)
mKeyword?.let {
sb.append("tag/")
sb.append(encodeUTF8(it))
@@ -361,8 +361,8 @@ data class ListUrlBuilder(
sb.toString()
}
MODE_WHATS_HOT -> EhUrl.getPopularUrl()
MODE_IMAGE_SEARCH -> EhUrl.getImageSearchUrl()
MODE_WHATS_HOT -> EhUrl.popularUrl
MODE_IMAGE_SEARCH -> EhUrl.imageSearchUrl
MODE_TOPLIST -> {
val sb = StringBuilder(EhUrl.HOST_E)
sb.append("toplist.php?tl=")
@@ -1,208 +0,0 @@
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.client.parser;
import androidx.annotation.NonNull;
import com.hippo.ehviewer.R;
import com.hippo.ehviewer.client.exception.NoHAtHClientException;
import org.jsoup.Jsoup;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.function.IntFunction;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class ArchiveParser {
private static final Pattern PATTERN_ARCHIVE_URL = Pattern.compile("<strong>(.*)</strong>.*<a href=\"([^\"]*)\">Click Here To Start Downloading</a>");
private static final Pattern PATTERN_HATH_FORM = Pattern.compile("<form id=\"hathdl_form\" action=\"[^\"]*?or=([^=\"]*?)\" method=\"post\">");
private static final Pattern PATTERN_HATH_ARCHIVE = Pattern.compile("<p><a href=\"[^\"]*\" onclick=\"return do_hathdl\\('([0-9]+|org)'\\)\">([^<]+)</a></p>\\s*<p>([\\w. ]+)</p>\\s*<p>([\\w. ]+)</p>");
private static final Pattern PATTERN_NEED_HATH_CLIENT = Pattern.compile("You must have a H@H client assigned to your account to use this feature\\.");
public static Result parse(String body) {
Matcher m = PATTERN_HATH_FORM.matcher(body);
if (!m.find()) {
return new Result(null, null);
}
String paramOr = m.group(1);
var archiveList = new ArrayList<Archive>();
var d = Jsoup.parse(body);
var es = d.select("#db>div>div");
for (var e : es) {
try {
var res = e.selectFirst("form>input").attr("value");
var name = e.selectFirst("form>div>input").attr("value");
var size = e.selectFirst("p>strong").text();
var cost = e.selectFirst("div>strong").text();
var item = new Archive(res, name, size, cost, false);
archiveList.add(item);
} catch (NullPointerException ex) {
ex.printStackTrace();
}
}
m = PATTERN_HATH_ARCHIVE.matcher(body);
while (m.find()) {
String res = ParserUtils.trim(m.group(1));
String name = ParserUtils.trim(m.group(2));
String size = ParserUtils.trim(m.group(3));
String cost = ParserUtils.trim(m.group(4));
var item = new Archive(res, name, size, cost, true);
archiveList.add(item);
}
return new Result(paramOr, archiveList);
}
public static String parseArchiveUrl(String body) throws NoHAtHClientException {
Matcher m = PATTERN_NEED_HATH_CLIENT.matcher(body);
if (m.find()) {
throw new NoHAtHClientException("No H@H client");
}
var d = Jsoup.parse(body);
var a = d.selectFirst("#continue>a[href]");
if (a != null) {
return a.attr("href") + "?start=1";
}
// TODO: Check more errors
return null;
}
public static final class Archive {
private final String res;
private final String name;
private final String size;
private final String cost;
private final boolean isHAtH;
public Archive(String res, String name, String size, String cost, boolean isHAtH) {
this.res = res;
this.name = name;
this.size = size;
this.cost = cost;
this.isHAtH = isHAtH;
}
public String format(IntFunction<String> getString) {
if (isHAtH) {
var costStr = cost.equals("Free") ? getString.apply(R.string.archive_free) : cost;
return String.format("[H@H] %s [%s] [%s]", name, size, costStr);
} else {
var nameStr = getString.apply(res.equals("org") ? R.string.archive_original : R.string.archive_resample);
var costStr = cost.equals("Free!") ? getString.apply(R.string.archive_free) : cost;
return String.format("%s [%s] [%s]", nameStr, size, costStr);
}
}
public String res() {
return res;
}
public String name() {
return name;
}
public String size() {
return size;
}
public String cost() {
return cost;
}
public boolean isHAtH() {
return isHAtH;
}
@Override
public boolean equals(Object obj) {
if (obj == this) return true;
if (obj == null || obj.getClass() != this.getClass()) return false;
var that = (Archive) obj;
return Objects.equals(this.res, that.res) &&
Objects.equals(this.name, that.name) &&
Objects.equals(this.size, that.size) &&
Objects.equals(this.cost, that.cost) &&
this.isHAtH == that.isHAtH;
}
@Override
public int hashCode() {
return Objects.hash(res, name, size, cost, isHAtH);
}
@NonNull
@Override
public String toString() {
return "Archive[" +
"res=" + res + ", " +
"name=" + name + ", " +
"size=" + size + ", " +
"cost=" + cost + ", " +
"isHAtH=" + isHAtH + ']';
}
}
public static final class Result {
private final String paramOr;
private final List<Archive> archiveList;
public Result(String paramOr, List<Archive> archiveList) {
this.paramOr = paramOr;
this.archiveList = archiveList;
}
public String paramOr() {
return paramOr;
}
public List<Archive> archiveList() {
return archiveList;
}
@Override
public boolean equals(Object obj) {
if (obj == this) return true;
if (obj == null || obj.getClass() != this.getClass()) return false;
var that = (Result) obj;
return Objects.equals(this.paramOr, that.paramOr) &&
Objects.equals(this.archiveList, that.archiveList);
}
@Override
public int hashCode() {
return Objects.hash(paramOr, archiveList);
}
@NonNull
@Override
public String toString() {
return "Result[" +
"paramOr=" + paramOr + ", " +
"archiveList=" + archiveList + ']';
}
}
}
@@ -0,0 +1,127 @@
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.client.parser
import com.hippo.ehviewer.R
import com.hippo.ehviewer.client.exception.NoHAtHClientException
import org.jsoup.Jsoup
import java.util.function.IntFunction
import java.util.regex.Pattern
object ArchiveParser {
private val PATTERN_ARCHIVE_URL =
Pattern.compile("<strong>(.*)</strong>.*<a href=\"([^\"]*)\">Click Here To Start Downloading</a>")
private val PATTERN_CURRENT_FUNDS =
Pattern.compile("<p>([\\d,]+) GP \\[[^]]*] &nbsp; ([\\d,]+) Credits \\[[^]]*]</p>")
private val PATTERN_FUNDS =
Pattern.compile("Available: ([\\d,]+) Credits.*Available: ([\\d,]+) kGP", Pattern.DOTALL)
private val PATTERN_HATH_FORM =
Pattern.compile("<form id=\"hathdl_form\" action=\"[^\"]*?or=([^=\"]*?)\" method=\"post\">")
private val PATTERN_HATH_ARCHIVE =
Pattern.compile("<p><a href=\"[^\"]*\" onclick=\"return do_hathdl\\('([0-9]+|org)'\\)\">([^<]+)</a></p>\\s*<p>([\\w. ]+)</p>\\s*<p>([\\w. ]+)</p>")
private val PATTERN_NEED_HATH_CLIENT =
Pattern.compile("You must have a H@H client assigned to your account to use this feature\\.")
fun parse(body: String): Result? {
var m = PATTERN_HATH_FORM.matcher(body)
if (!m.find()) {
return null
}
val paramOr = m.group(1)
val archiveList = ArrayList<Archive>()
val d = Jsoup.parse(body)
val es = d.select("#db>div>div")
for (e in es) {
if (e.childrenSize() > 0 && !e.attr("style").contains("color:#CCCCCC")) {
try {
val res = e.selectFirst("form>input")!!.attr("value")
val name = e.selectFirst("form>div>input")!!.attr("value")
val size = e.selectFirst("p>strong")!!.text()
val cost = e.selectFirst("div>strong")!!.text().replace(",", "")
val item = Archive(res, name, size, cost, false)
archiveList.add(item)
} catch (ex: NullPointerException) {
ex.printStackTrace()
}
}
}
m = PATTERN_HATH_ARCHIVE.matcher(body)
while (m.find()) {
val res = ParserUtils.trim(m.group(1))
val name = ParserUtils.trim(m.group(2))
val size = ParserUtils.trim(m.group(3))
val cost = ParserUtils.trim(m.group(4))
val item = Archive(res, name, size, cost, true)
archiveList.add(item)
}
var funds: Funds? = null
m = PATTERN_CURRENT_FUNDS.matcher(body)
if (m.find()) {
val fundsGP = ParserUtils.parseInt(m.group(1), 0)
val fundsC = ParserUtils.parseInt(m.group(2), 0)
funds = Funds(fundsGP, fundsC)
}
return Result(paramOr, archiveList, funds)
}
@Throws(NoHAtHClientException::class)
fun parseArchiveUrl(body: String): String? {
val m = PATTERN_NEED_HATH_CLIENT.matcher(body)
if (m.find()) {
throw NoHAtHClientException("No H@H client")
}
val d = Jsoup.parse(body)
val a = d.selectFirst("#continue>a[href]")
return if (a != null) {
a.attr("href") + "?start=1"
} else null
// TODO: Check more errors
}
fun parseFunds(body: String): Funds? {
val m = PATTERN_FUNDS.matcher(body)
return if (m.find()) {
val fundsC = ParserUtils.parseInt(m.group(1), 0)
val fundsGP = ParserUtils.parseInt(m.group(2), 0) * 1000
return Funds(fundsGP, fundsC)
} else null
}
class Archive(
val res: String,
val name: String,
val size: String,
val cost: String,
val isHAtH: Boolean
) {
fun format(getString: IntFunction<String?>): String {
return if (isHAtH) {
val costStr = if (cost == "Free") getString.apply(R.string.archive_free) else cost
"[H@H] $name [$size] [$costStr]"
} else {
val nameStr =
getString.apply(if (res == "org") R.string.archive_original else R.string.archive_resample)
val costStr = if (cost == "Free!") getString.apply(R.string.archive_free) else cost
"$nameStr [$size] [$costStr]"
}
}
}
class Funds(var fundsGP: Int, var fundsC: Int)
class Result(val paramOr: String?, val archiveList: List<Archive>, var funds: Funds?)
}
@@ -22,11 +22,11 @@ import androidx.annotation.Nullable;
import com.hippo.ehviewer.EhDB;
import com.hippo.ehviewer.client.EhFilter;
import com.hippo.ehviewer.client.EhUtils;
import com.hippo.ehviewer.client.data.BaseGalleryInfo;
import com.hippo.ehviewer.client.data.GalleryComment;
import com.hippo.ehviewer.client.data.GalleryCommentList;
import com.hippo.ehviewer.client.data.GalleryDetail;
import com.hippo.ehviewer.client.data.GalleryInfo;
import com.hippo.ehviewer.client.data.BaseGalleryInfo;
import com.hippo.ehviewer.client.data.GalleryTagGroup;
import com.hippo.ehviewer.client.data.LargePreviewSet;
import com.hippo.ehviewer.client.data.NormalPreviewSet;
@@ -477,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,8 +23,8 @@ import androidx.annotation.NonNull;
import com.hippo.ehviewer.EhDB;
import com.hippo.ehviewer.client.EhUtils;
import com.hippo.ehviewer.client.data.GalleryInfo;
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;
import com.hippo.util.JsoupUtils;
@@ -3,8 +3,8 @@ package com.hippo.ehviewer.dao
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.Ignore
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.client.data.BaseGalleryInfo
import com.hippo.ehviewer.client.data.GalleryInfo
@Entity(tableName = "DOWNLOADS")
class DownloadInfo() : BaseGalleryInfo() {
@@ -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)
@@ -2,8 +2,8 @@ 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
import com.hippo.ehviewer.client.data.GalleryInfo
@Entity(tableName = "HISTORY")
class HistoryInfo() : BaseGalleryInfo() {
@@ -2,8 +2,8 @@ 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
import com.hippo.ehviewer.client.data.GalleryInfo
@Entity(tableName = "LOCAL_FAVORITES")
class LocalFavoriteInfo() : BaseGalleryInfo() {
@@ -24,8 +24,8 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.hippo.ehviewer.EhDB;
import com.hippo.ehviewer.client.data.GalleryInfo;
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;
import com.hippo.ehviewer.spider.SpiderDen;
@@ -54,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
@@ -158,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;
@@ -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"
@@ -17,7 +17,6 @@
package com.hippo.ehviewer.gallery;
import android.content.Context;
import android.graphics.ImageDecoder;
import android.net.Uri;
import android.os.Handler;
import android.os.Process;
@@ -38,7 +37,6 @@ import com.hippo.yorozuya.thread.PriorityThread;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
@@ -56,7 +54,7 @@ public class ArchivePageLoader extends PageLoader2 {
public static PVLock pv = new PVLock(0);
private final UriArchiveAccessor archiveAccessor;
private final Stack<Integer> requests = new Stack<>();
private final LinkedHashMap<Integer, ByteBuffer> streams = new LinkedHashMap<>();
private final LinkedHashMap<Integer, Image.ByteBufferSource> streams = new LinkedHashMap<>();
private final Thread[] decodeThread = new Thread[]{
new Thread(new DecodeTask()),
new Thread(new DecodeTask()),
@@ -305,10 +303,10 @@ public class ArchivePageLoader extends PageLoader2 {
}
}
ByteBuffer buffer = archiveAccessor.extractToByteBuffer(index);
Image.ByteBufferSource src = archiveAccessor.getImageSource(index);
synchronized (streams) {
streams.put(index, buffer);
streams.put(index, src);
streams.notify();
}
}
@@ -320,7 +318,7 @@ public class ArchivePageLoader extends PageLoader2 {
public void run() {
while (!Thread.currentThread().isInterrupted()) {
int index;
ByteBuffer buffer;
Image.ByteBufferSource src;
synchronized (streams) {
if (streams.isEmpty()) {
try {
@@ -331,21 +329,16 @@ public class ArchivePageLoader extends PageLoader2 {
continue;
}
Iterator<Map.Entry<Integer, ByteBuffer>> iterator = streams.entrySet().iterator();
Map.Entry<Integer, ByteBuffer> entry = iterator.next();
Iterator<Map.Entry<Integer, Image.ByteBufferSource>> iterator = streams.entrySet().iterator();
Map.Entry<Integer, Image.ByteBufferSource> entry = iterator.next();
iterator.remove();
index = entry.getKey();
buffer = entry.getValue();
src = entry.getValue();
}
Image image = null;
if (buffer != null) {
try {
image = Image.decode(buffer.asReadOnlyBuffer());
} catch (ImageDecoder.DecodeException e) {
UriArchiveAccessor.releaseByteBuffer(buffer);
e.printStackTrace();
}
if (src != null) {
image = Image.decode(src);
}
if (image != null) {
notifyPageSucceed(index, image);
@@ -20,7 +20,6 @@ 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
@@ -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,9 +96,8 @@ class RestoreDownloadPreference constructor(
} else try {
runBlocking {
fillGalleryListByApi(
mHttpClient,
ArrayList(restoreItemList),
EhUrl.getReferer()
EhUrl.referer
)
}
} catch (e: Throwable) {
@@ -17,22 +17,24 @@
package com.hippo.ehviewer.spider;
import android.content.Context;
import android.graphics.BitmapFactory;
import android.webkit.MimeTypeMap;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.hippo.beerbelly.SimpleDiskCache;
import com.hippo.Native;
import com.hippo.ehviewer.EhApplication;
import com.hippo.ehviewer.EhDB;
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.gallery.PageLoader2;
import com.hippo.image.Image;
import com.hippo.io.UniFileInputStreamPipe;
import com.hippo.io.UniFileOutputStreamPipe;
import com.hippo.streampipe.InputStreamPipe;
import com.hippo.streampipe.OutputStreamPipe;
import com.hippo.unifile.RawFile;
import com.hippo.unifile.UniFile;
import com.hippo.yorozuya.FileUtils;
import com.hippo.yorozuya.IOUtils;
@@ -40,14 +42,24 @@ import com.hippo.yorozuya.MathUtils;
import com.hippo.yorozuya.Utilities;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.Locale;
import coil.disk.DiskCache;
import okio.BufferedSource;
import okio.Okio;
public final class SpiderDen {
@Nullable
private static SimpleDiskCache sCache;
private static DiskCache sCache; // We use data to store image file, and metadata for image type
private final GalleryInfo mGalleryInfo;
private final long mGid;
@Nullable
@@ -61,8 +73,8 @@ public final class SpiderDen {
}
public static void initialize(Context context) {
sCache = new SimpleDiskCache(new File(context.getCacheDir(), "image"),
MathUtils.clamp(Settings.getReadCacheSize(), 40, 1280) * 1024 * 1024);
sCache = new DiskCache.Builder().directory(new File(context.getCacheDir(), "gallery_image"))
.maxSizeBytes((long) MathUtils.clamp(Settings.getReadCacheSize(), 40, 1280) * 1024 * 1024).build();
}
public static UniFile getGalleryDownloadDir(long gid) {
@@ -137,7 +149,10 @@ public final class SpiderDen {
}
String key = EhCacheKeyFactory.getImageKey(mGid, index);
return sCache.contain(key);
var snapshot = sCache.get(key);
var exist = snapshot != null;
if (exist) snapshot.close();
return exist;
}
private boolean containInDownloadDir(int index) {
@@ -171,42 +186,27 @@ public final class SpiderDen {
}
// Find image file in cache
String key = EhCacheKeyFactory.getImageKey(mGid, index);
InputStreamPipe pipe = sCache.getInputStreamPipe(key);
if (pipe == null) {
var snapshot = sCache.get(key);
if (snapshot == null) {
return false;
}
OutputStream os = null;
try {
try (snapshot; FileInputStream is = new FileInputStream(snapshot.getData().toFile()); BufferedSource buf = Okio.buffer(Okio.source(snapshot.getMetadata().toFile()))) {
// Get extension
String extension;
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
pipe.obtain();
BitmapFactory.decodeStream(pipe.open(), null, options);
pipe.close();
extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(options.outMimeType);
if (extension != null) {
extension = '.' + extension;
} else {
return false;
}
// Fix extension
extension = fixExtension(extension);
String extension = fixExtension("." + buf.readUtf8Line());
// Copy from cache to download dir
UniFile file = dir.createFile(generateImageFilename(index, extension));
if (file == null) {
return false;
}
os = file.openOutputStream();
IOUtils.copy(pipe.open(), os);
IOUtils.copy(is, os);
return true;
} catch (IOException e) {
return false;
} finally {
IOUtils.closeQuietly(os);
pipe.close();
pipe.release();
}
}
@@ -253,13 +253,40 @@ public final class SpiderDen {
}
@Nullable
private OutputStreamPipe openCacheOutputStreamPipe(int index) {
private OutputStreamPipe openCacheOutputStreamPipe(int index, @Nullable String extension) {
if (sCache == null) {
return null;
}
String key = EhCacheKeyFactory.getImageKey(mGid, index);
return sCache.getOutputStreamPipe(key);
var editor = sCache.edit(key);
if (editor == null) return null;
try (var sink = Okio.buffer(Okio.sink(editor.getMetadata().toFile()))) {
sink.writeUtf8(extension);
} catch (IOException e) {
e.printStackTrace();
}
return new OutputStreamPipe() {
private OutputStream os = null;
@Override
public void release() {
editor.commit();
}
@NonNull
@Override
public OutputStream open() throws IOException {
if (os != null) IOUtils.closeQuietly(os);
os = new FileOutputStream(editor.getData().toFile());
return os;
}
@Override
public void close() {
if (os != null) IOUtils.closeQuietly(os);
}
};
}
/**
@@ -287,7 +314,7 @@ public final class SpiderDen {
// Return the download pipe is the gallery has been downloaded
OutputStreamPipe pipe = openDownloadOutputStreamPipe(index, extension);
if (pipe == null) {
pipe = openCacheOutputStreamPipe(index);
pipe = openCacheOutputStreamPipe(index, extension);
}
return pipe;
} else if (mMode == SpiderQueen.MODE_DOWNLOAD) {
@@ -304,7 +331,29 @@ public final class SpiderDen {
}
String key = EhCacheKeyFactory.getImageKey(mGid, index);
return sCache.getInputStreamPipe(key);
var snapshot = sCache.get(key);
if (snapshot == null) return null;
return new InputStreamPipe() {
private InputStream is = null;
@Override
public void release() {
snapshot.close();
}
@NonNull
@Override
public InputStream open() throws IOException {
if (is != null) IOUtils.closeQuietly(is);
is = new FileInputStream(snapshot.getData().toFile());
return is;
}
@Override
public void close() {
if (is != null) IOUtils.closeQuietly(is);
}
};
}
@Nullable
@@ -341,4 +390,94 @@ public final class SpiderDen {
}
}
@Nullable
public Image.ByteBufferSource getImageSource(int index) {
if (mMode == SpiderQueen.MODE_READ) {
String key = EhCacheKeyFactory.getImageKey(mGid, index);
if (sCache != null) {
var snapshot = sCache.get(key);
if (snapshot != null) {
try {
var file = new RandomAccessFile(snapshot.getData().toFile(), "rw");
var chan = file.getChannel();
var map = chan.map(FileChannel.MapMode.PRIVATE, 0, file.length());
return new Image.ByteBufferSource() {
@NonNull
@Override
public ByteBuffer getByteBuffer() {
return map;
}
@Override
public void close() throws Exception {
chan.close();
file.close();
snapshot.close();
}
};
} catch (Throwable e) {
e.printStackTrace();
}
}
}
}
UniFile dir = getDownloadDir();
if (dir == null) {
return null;
}
UniFile file = findImageFile(dir, index);
if (file != null && mMode == SpiderQueen.MODE_DOWNLOAD) {
if (copyFromCacheToDownloadDir(index)) {
file = findImageFile(dir, index);
}
}
if (file instanceof RawFile rawFile) {
try {
var file2 = new RandomAccessFile(rawFile.mFile, "rw");
var chan = file2.getChannel();
var map = chan.map(FileChannel.MapMode.PRIVATE, 0, file2.length());
return new Image.ByteBufferSource() {
@NonNull
@Override
public ByteBuffer getByteBuffer() {
return map;
}
@Override
public void close() throws Exception {
chan.close();
file2.close();
}
};
} catch (Throwable e) {
e.printStackTrace();
}
}
if (file != null) {
try {
var pfd = EhApplication.getApplication().getContentResolver().openFileDescriptor(file.getUri(), "rw");
var map = Native.mapFd(pfd.getFd(), pfd.getStatSize());
if (map != null) {
return new Image.ByteBufferSource() {
@NonNull
@Override
public ByteBuffer getByteBuffer() {
return map;
}
@Override
public void close() throws Exception {
Native.unmapDirectByteBuffer(map);
pfd.close();
}
};
} else {
pfd.close();
}
} catch (Throwable e) {
e.printStackTrace();
}
}
return null;
}
}
@@ -17,7 +17,6 @@
package com.hippo.ehviewer.spider;
import android.graphics.BitmapFactory;
import android.graphics.ImageDecoder;
import android.os.AsyncTask;
import android.os.Process;
import android.text.TextUtils;
@@ -46,7 +45,6 @@ import com.hippo.ehviewer.client.parser.GalleryMultiPageViewerPTokenParser;
import com.hippo.ehviewer.client.parser.GalleryPageApiParser;
import com.hippo.ehviewer.client.parser.GalleryPageParser;
import com.hippo.ehviewer.client.parser.GalleryPageUrlParser;
import com.hippo.ehviewer.gallery.PageLoader2;
import com.hippo.image.Image;
import com.hippo.streampipe.InputStreamPipe;
import com.hippo.streampipe.OutputStreamPipe;
@@ -57,7 +55,6 @@ import com.hippo.yorozuya.IOUtils;
import com.hippo.yorozuya.MathUtils;
import com.hippo.yorozuya.OSUtils;
import com.hippo.yorozuya.StringUtils;
import com.hippo.yorozuya.Utilities;
import com.hippo.yorozuya.collect.SparseJLArray;
import com.hippo.yorozuya.thread.PriorityThread;
import com.hippo.yorozuya.thread.PriorityThreadFactory;
@@ -103,7 +100,7 @@ public final class SpiderQueen implements Runnable {
public static final String SPIDER_INFO_FILENAME = ".ehviewer";
private static final String TAG = SpiderQueen.class.getSimpleName();
private static final AtomicInteger sIdGenerator = new AtomicInteger();
private static final boolean DEBUG_LOG = false;
private static final boolean DEBUG_LOG = true;
private static final boolean DEBUG_PTOKEN = true;
private static final String[] URL_509_SUFFIX_ARRAY = {
"/509.gif",
@@ -156,6 +153,8 @@ public final class SpiderQueen implements Runnable {
// For download, when it go to mPageStateArray.size(), done
private volatile int mDownloadPage = -1;
private boolean mStoped = false;
private SpiderQueen(@NonNull GalleryInfo galleryInfo) {
mHttpClient = EhApplication.getOkHttpClient();
mSpiderInfoCache = EhApplication.getSpiderInfoCache();
@@ -388,10 +387,15 @@ public final class SpiderQueen implements Runnable {
}
private void stop() {
Thread queenThread = mQueenThread;
if (queenThread != null) {
queenThread.interrupt();
mQueenThread = null;
mStoped = true;
synchronized (mQueenLock) {
mQueenLock.notifyAll();
}
synchronized (mDecodeRequestQueue) {
mDecodeRequestQueue.notifyAll();
}
synchronized (mWorkerLock) {
mWorkerLock.notifyAll();
}
}
@@ -579,7 +583,6 @@ public final class SpiderQueen implements Runnable {
OutputStream os = null;
try {
os = file.openOutputStream();
pipe.obtain();
IOUtils.copy(pipe.open(), os);
return true;
} catch (IOException e) {
@@ -605,8 +608,6 @@ public final class SpiderQueen implements Runnable {
OutputStream os = null;
try {
pipe.obtain();
// Get dst file
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
@@ -645,8 +646,6 @@ public final class SpiderQueen implements Runnable {
}
try {
pipe.obtain();
// Get dst file
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
@@ -713,7 +712,7 @@ public final class SpiderQueen implements Runnable {
return spiderInfo;
}
} catch (IOException e) {
// Ignore
e.printStackTrace();
}
return null;
@@ -868,6 +867,7 @@ public final class SpiderQueen implements Runnable {
editor.commit();
} catch (IOException e) {
editor.abort();
e.printStackTrace();
}
}
@@ -875,8 +875,8 @@ public final class SpiderQueen implements Runnable {
// Read spider info
SpiderInfo spiderInfo = readSpiderInfoFromLocal();
// Check interrupted
if (Thread.currentThread().isInterrupted()) {
// Check Stopped
if (mStoped) {
return;
}
@@ -891,16 +891,16 @@ public final class SpiderQueen implements Runnable {
}
mSpiderInfo.lazySet(spiderInfo);
// Check interrupted
if (Thread.currentThread().isInterrupted()) {
// Check Stopped
if (mStoped) {
return;
}
// Write spider info to file
writeSpiderInfoToLocal(spiderInfo);
// Check interrupted
if (Thread.currentThread().isInterrupted()) {
// Check Stopped
if (mStoped) {
return;
}
@@ -924,17 +924,19 @@ public final class SpiderQueen implements Runnable {
}
// handle pToken request
while (!Thread.currentThread().isInterrupted()) {
while (!mStoped) {
Integer index = mRequestPTokenQueue.poll();
if (index == null) {
// No request index, wait here
synchronized (mQueenLock) {
if (mStoped) break;
try {
mQueenLock.wait();
} catch (InterruptedException e) {
break;
}
if (mStoped) break;
}
continue;
}
@@ -989,16 +991,9 @@ public final class SpiderQueen implements Runnable {
// Set mQueenThread null
mQueenThread = null;
// Interrupt decoder
for (Thread decoderThread : mDecodeThreadArray) {
if (decoderThread != null) {
decoderThread.interrupt();
}
}
// Interrupt all workers
// Stop all workers
synchronized (mWorkerLock) {
mWorkerPoolExecutor.shutdownNow();
mWorkerPoolExecutor.shutdown();
mWorkerPoolExecutor = null;
}
notifyFinish();
@@ -1115,7 +1110,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.getGid(), mGalleryInfo.getToken());
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 +1122,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
@@ -1146,7 +1141,6 @@ public final class SpiderQueen implements Runnable {
String pageUrl = null;
String error = null;
boolean forceHtml = false;
boolean interrupt = false;
boolean leakSkipHathKey = false;
for (int i = 0; i < 5; i++) {
@@ -1191,10 +1185,9 @@ public final class SpiderQueen implements Runnable {
break;
}
// Check interrupted
if (Thread.currentThread().isInterrupted()) {
// Check Stopped
if (mStoped) {
error = "Interrupted";
interrupt = true;
break;
}
}
@@ -1226,10 +1219,9 @@ public final class SpiderQueen implements Runnable {
}
}
// Check interrupted
if (Thread.currentThread().isInterrupted()) {
// Check Stopped
if (mStoped) {
error = "Interrupted";
interrupt = true;
break;
}
}
@@ -1252,7 +1244,6 @@ public final class SpiderQueen implements Runnable {
}
// Download image
InputStream is = null;
try {
if (DEBUG_LOG) {
Log.d(TAG, "Start download image " + index);
@@ -1276,10 +1267,6 @@ public final class SpiderQueen implements Runnable {
if (mediaType != null) {
extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mediaType.toString());
}
// Ensure extension
if (!Utilities.contain(PageLoader2.SUPPORT_IMAGE_EXTENSIONS, extension)) {
extension = PageLoader2.SUPPORT_IMAGE_EXTENSIONS[0];
}
OutputStreamPipe osPipe = null;
try {
@@ -1293,29 +1280,43 @@ public final class SpiderQueen implements Runnable {
}
long contentLength = responseBody.contentLength();
is = responseBody.byteStream();
osPipe.obtain();
OutputStream os = osPipe.open();
final byte[] data = new byte[1024 * 4];
long receivedSize = 0;
while (!Thread.currentThread().isInterrupted()) {
int bytesRead = is.read(data);
if (bytesRead == -1) {
response.close();
break;
OutputStream os = osPipe.open();
if (os instanceof FileOutputStream fileOutputStream) {
try (var channel = fileOutputStream.getChannel(); var source = responseBody.source(); responseBody; response) {
while (!mStoped) {
// Is 40k a good size ?
long bytesRead = channel.transferFrom(source, receivedSize, 40960);
if (bytesRead == 0) {
break;
}
receivedSize += bytesRead;
if (contentLength > 0) {
mPagePercentMap.put(index, (float) receivedSize / contentLength);
}
notifyPageDownload(index, contentLength, receivedSize, (int) bytesRead);
}
}
os.write(data, 0, bytesRead);
receivedSize += bytesRead;
// Update page percent
if (contentLength > 0) {
mPagePercentMap.put(index, (float) receivedSize / contentLength);
} else {
final byte[] data = new byte[1024 * 4];
try (var is = responseBody.byteStream(); responseBody; response) {
while (!mStoped) {
int bytesRead = is.read(data);
if (bytesRead == -1) {
break;
}
os.write(data, 0, bytesRead);
receivedSize += bytesRead;
// Update page percent
if (contentLength > 0) {
mPagePercentMap.put(index, (float) receivedSize / contentLength);
}
// Notify listener
notifyPageDownload(index, contentLength, receivedSize, bytesRead);
}
os.flush();
}
// Notify listener
notifyPageDownload(index, contentLength, receivedSize, bytesRead);
}
os.flush();
// check download size
if (contentLength >= 0) {
@@ -1346,7 +1347,6 @@ public final class SpiderQueen implements Runnable {
}
// Check plain txt
isPipe.obtain();
InputStream inputStream = new BufferedInputStream(isPipe.open());
boolean isPlainTxt = true;
int j = 0;
@@ -1377,9 +1377,8 @@ public final class SpiderQueen implements Runnable {
}
}
// Check interrupted
if (Thread.currentThread().isInterrupted()) {
interrupt = true;
// Check Stopped
if (mStoped) {
error = "Interrupted";
break;
}
@@ -1401,8 +1400,6 @@ public final class SpiderQueen implements Runnable {
error = GetText.getString(R.string.error_socket);
forceHtml = true;
} finally {
IOUtils.closeQuietly(is);
if (DEBUG_LOG) {
Log.d(TAG, "End download image " + index);
}
@@ -1413,7 +1410,7 @@ public final class SpiderQueen implements Runnable {
mSpiderDen.remove(index);
updatePageState(index, STATE_FAILED, error);
return !interrupt;
return !mStoped;
}
// false for stop
@@ -1484,7 +1481,7 @@ public final class SpiderQueen implements Runnable {
String pToken = null;
// Get token
while (!Thread.currentThread().isInterrupted()) {
while (!mStoped) {
synchronized (mPTokenLock) {
pToken = spiderInfo.pTokenMap.get(index);
}
@@ -1496,6 +1493,7 @@ public final class SpiderQueen implements Runnable {
}
// Wait
synchronized (mWorkerLock) {
if (mStoped) break;
try {
mWorkerLock.wait();
} catch (InterruptedException e) {
@@ -1505,6 +1503,7 @@ public final class SpiderQueen implements Runnable {
}
break;
}
if (mStoped) break;
}
} else {
break;
@@ -1521,7 +1520,7 @@ public final class SpiderQueen implements Runnable {
String previousPToken = null;
int previousIndex = index - 1;
// Get token
while (previousIndex >= 0 && !Thread.currentThread().isInterrupted()) {
while (previousIndex >= 0 && !mStoped) {
synchronized (mPTokenLock) {
previousPToken = spiderInfo.pTokenMap.get(previousIndex);
}
@@ -1533,6 +1532,7 @@ public final class SpiderQueen implements Runnable {
}
// Wait
synchronized (mWorkerLock) {
if (mStoped) break;
try {
mWorkerLock.wait();
} catch (InterruptedException e) {
@@ -1542,6 +1542,7 @@ public final class SpiderQueen implements Runnable {
}
break;
}
if (mStoped) break;
}
} else {
break;
@@ -1565,8 +1566,7 @@ public final class SpiderQueen implements Runnable {
Log.i(TAG, Thread.currentThread().getName() + ": start");
}
while (mSpiderDen.isReady() && !Thread.currentThread().isInterrupted() && runInternal())
;
while (mSpiderDen.isReady() && !mStoped && runInternal()) ;
boolean finish;
// Clear in spider worker array
@@ -1609,9 +1609,10 @@ public final class SpiderQueen implements Runnable {
Log.i(TAG, Thread.currentThread().getName() + ": start");
}
while (!Thread.currentThread().isInterrupted()) {
while (!mStoped) {
int index;
synchronized (mDecodeRequestQueue) {
if (mStoped) break;
if (mDecodeRequestQueue.isEmpty()) {
try {
mDecodeRequestQueue.wait();
@@ -1619,6 +1620,7 @@ public final class SpiderQueen implements Runnable {
// Interrupted
break;
}
if (mStoped) break;
continue;
}
index = mDecodeRequestQueue.remove();
@@ -1632,8 +1634,8 @@ public final class SpiderQueen implements Runnable {
continue;
}
InputStreamPipe pipe = mSpiderDen.openInputStreamPipe(index);
if (pipe == null) {
Image.ByteBufferSource src = mSpiderDen.getImageSource(index);
if (src == null) {
resetDecodeIndex();
// Can't find the file, it might be removed from cache,
// Reset it state and request it
@@ -1642,36 +1644,11 @@ public final class SpiderQueen implements Runnable {
continue;
}
Image image = null;
Image image = Image.decode(src);
String error = null;
InputStream is;
pipe.obtain();
try {
is = pipe.open();
} catch (IOException e) {
// Can't open pipe
error = GetText.getString(R.string.error_reading_failed);
is = null;
pipe.close();
pipe.release();
}
if (is != null) {
try {
image = Image.decode(((FileInputStream) is));
} catch (ImageDecoder.DecodeException e) {
e.printStackTrace();
} finally {
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (image == null) {
error = GetText.getString(R.string.error_decoding_failed);
}
if (image == null) {
error = GetText.getString(R.string.error_decoding_failed);
}
// Notify
@@ -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
@@ -23,116 +24,72 @@ import android.content.pm.verify.domain.DomainVerificationManager
import android.content.pm.verify.domain.DomainVerificationUserState
import android.graphics.Bitmap
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest
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.util.Log
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.content.getSystemService
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.login.LoginActivity
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 lateinit var connectivityManager: ConnectivityManager
private val availableNetworks: MutableList<Network> = mutableListOf()
private fun saveImageToTempFile(file: UniFile?): File? {
file ?: return null
@@ -166,16 +123,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 +192,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 +207,71 @@ 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
override fun onCreate(savedInstanceState: Bundle?) {
connectivityManager = getSystemService()!!
if (Settings.getDF() && Settings.getBypassVPN()) {
bypassVPN()
}
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()
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 +284,19 @@ class MainActivity : StageActivity(), NavigationView.OnNavigationItemSelectedLis
}
}
}
} else {
onRestore(savedInstanceState)
}
}
private fun bypassVPN() {
val network = connectivityManager.activeNetwork
val capabilities = connectivityManager.getNetworkCapabilities(network)
capabilities?.let {
if (it.hasTransport(NetworkCapabilities.TRANSPORT_VPN)) {
val builder = NetworkRequest.Builder()
.addCapability(NetworkCapabilities.NET_CAPABILITY_FOREGROUND)
.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
connectivityManager.registerNetworkCallback(builder.build(), mNetworkCallback)
}
}
}
@@ -333,11 +358,10 @@ 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 (connectivityManager.isActiveNetworkMetered) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
Snackbar.make(
mDrawerLayout!!,
binding.drawView,
R.string.metered_network_warning,
Snackbar.LENGTH_LONG
)
@@ -353,110 +377,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 +450,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,17 +484,6 @@ 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)
}
}
}
@JvmOverloads
fun showTip(@StringRes id: Int, length: Int, useToast: Boolean = false) {
showTip(getString(id), length, useToast)
@@ -537,77 +505,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) {
@@ -619,25 +527,24 @@ class MainActivity : StageActivity(), NavigationView.OnNavigationItemSelectedLis
override fun onDrawerStateChanged(newState: Int) {}
}
companion object {
private const val KEY_NAV_CHECKED_ITEM = "nav_checked_item"
private val mNetworkCallback = object : ConnectivityManager.NetworkCallback() {
private val TAG = "mNetworkCallback"
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)
override fun onAvailable(network: Network) {
Log.d(TAG, "onAvailable: $network")
connectivityManager.bindProcessToNetwork(network)
availableNetworks.add(network)
}
override fun onLost(network: Network) {
Log.d(TAG, "onLost: $network")
val activeNetwork = availableNetworks.last()
availableNetworks.remove(network)
if (network == activeNetwork) {
connectivityManager.bindProcessToNetwork(
availableNetworks.takeIf { it.isNotEmpty() }?.last()
)
}
}
}
}
@@ -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 {
}
@@ -231,7 +231,6 @@ class AdvancedFragment : BasePreferenceFragment() {
addPreferencesFromResource(R.xml.advanced_settings)
val dumpLogcat = findPreference<Preference>(KEY_DUMP_LOGCAT)
val appLanguage = findPreference<Preference>(KEY_APP_LANGUAGE)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) appLanguage!!.isVisible = false
val importData = findPreference<Preference>(KEY_IMPORT_DATA)
val exportData = findPreference<Preference>(KEY_EXPORT_DATA)
val backupFavorite = findPreference<Preference>(KEY_BACKUP_FAVORITE)
@@ -378,7 +377,7 @@ class AdvancedFragment : BasePreferenceFragment() {
override fun onPreferenceChange(preference: Preference, newValue: Any): Boolean {
val key = preference.key
if (KEY_APP_LANGUAGE == key && Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
if (KEY_APP_LANGUAGE == key) {
if ("system" == newValue) {
AppCompatDelegate.setApplicationLocales(LocaleListCompat.getEmptyLocaleList())
} else {
@@ -25,7 +25,9 @@ import androidx.preference.Preference
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.EhTagDatabase
import eu.kanade.tachiyomi.util.lang.launchIO
class EhFragment : BasePreferenceFragment() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
@@ -67,6 +69,7 @@ class EhFragment : BasePreferenceFragment() {
return true
} else if (Settings.KEY_GALLERY_SITE == key) {
requireActivity().setResult(Activity.RESULT_OK)
launchIO { runCatching { EhEngine.getUConfig() } }
return true
} else if (Settings.KEY_LIST_MODE == key) {
requireActivity().setResult(Activity.RESULT_OK)
@@ -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)
}
@@ -0,0 +1,308 @@
package com.hippo.ehviewer.ui.login
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.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,51 @@
package com.hippo.ehviewer.ui.login
import android.os.Bundle
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.EhActivity
class LoginActivity : EhActivity() {
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)
}
}
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"
@@ -0,0 +1,143 @@
package com.hippo.ehviewer.ui.login
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)
}
EhEngine.getUConfig()
}
}
@@ -0,0 +1,294 @@
package com.hippo.ehviewer.ui.login
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.FilledTonalButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
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.UrlOpener
import com.hippo.ehviewer.client.EhEngine
import com.hippo.ehviewer.client.EhUrl
import com.hippo.ehviewer.client.EhUtils
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 {
FilledTonalButton(
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,107 @@
package com.hippo.ehviewer.ui.login
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.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,
forcePersistent = true,
forceLongLive = true,
forceNotHostOnly = 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)
}
})
}
@@ -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();
}
}
}
@@ -34,7 +34,6 @@ import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.Toolbar
import androidx.core.content.ContextCompat
import androidx.core.view.GravityCompat
import androidx.core.view.ViewCompat
import androidx.drawerlayout.widget.DrawerLayout
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.ItemTouchHelper
@@ -65,7 +64,6 @@ import com.hippo.ehviewer.download.DownloadService
import com.hippo.ehviewer.download.DownloadService.Companion.clear
import com.hippo.ehviewer.spider.SpiderDen
import com.hippo.ehviewer.widget.SimpleRatingView
import com.hippo.scene.Announcer
import com.hippo.unifile.UniFile
import com.hippo.view.ViewTransition
import com.hippo.widget.FabLayout
@@ -109,15 +107,13 @@ class DownloadsScene : BaseToolbarScene(), DownloadInfoListener, OnClickFabListe
private var mLabelAdapter: DownloadLabelAdapter? = null
private lateinit var mLabels: MutableList<String>
private var mType = -1
override fun getNavCheckedItem(): Int {
return R.id.nav_downloads
}
private fun initLabels() {
context ?: return
val listLabel = downloadManager.labelList
mLabels = ArrayList(listLabel.size + 1)
// Add default label name
mLabels = ArrayList(listLabel.size + LABEL_OFFSET)
// Add "All" and "Default" label names
mLabels.add(getString(R.string.download_all))
mLabels.add(getString(R.string.default_download_label_name))
for ((_, label) in listLabel) {
mLabels.add(label!!)
@@ -154,10 +150,6 @@ class DownloadsScene : BaseToolbarScene(), DownloadInfoListener, OnClickFabListe
return false
}
override fun onNewArguments(args: Bundle) {
handleArguments(args)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val context = context
@@ -193,12 +185,14 @@ class DownloadsScene : BaseToolbarScene(), DownloadInfoListener, OnClickFabListe
}
var list: MutableList<DownloadInfo>?
if (mLabel == null) {
list = mDownloadManager!!.allDownloadInfoList
} else if (mLabel == getString(R.string.default_download_label_name)) {
list = mDownloadManager!!.defaultDownloadInfoList
} else {
list = mDownloadManager!!.getLabelDownloadInfoList(mLabel)
if (list == null) {
mLabel = null
list = mDownloadManager!!.defaultDownloadInfoList
list = mDownloadManager!!.allDownloadInfoList
}
}
if (mType != -1) {
@@ -221,7 +215,7 @@ class DownloadsScene : BaseToolbarScene(), DownloadInfoListener, OnClickFabListe
setTitle(
getString(
R.string.scene_download_title,
if (mLabel != null) mLabel else getString(R.string.default_download_label_name)
if (mLabel != null) mLabel else getString(R.string.download_all)
)
)
}
@@ -294,6 +288,7 @@ class DownloadsScene : BaseToolbarScene(), DownloadInfoListener, OnClickFabListe
mRecyclerView!!.scrollToPosition(mInitPosition)
mInitPosition = -1
}
itemTouchHelper.attachToRecyclerView(mRecyclerView)
mFastScroller!!.attachToRecyclerView(mRecyclerView)
val handlerDrawable = HandlerDrawable()
handlerDrawable.setColor(theme.resolveColor(com.google.android.material.R.attr.colorPrimary))
@@ -640,10 +635,10 @@ class DownloadsScene : BaseToolbarScene(), DownloadInfoListener, OnClickFabListe
}
override fun onUpdate(info: DownloadInfo, list: List<DownloadInfo>) {
if (mList !== list) {
if (mLabel != null && mList !== list) {
return
}
val index = list.indexOf(info)
val index = mList!!.indexOf(info)
if (index >= 0 && mAdapter != null) {
mAdapter!!.notifyItemChanged(index)
}
@@ -791,7 +786,36 @@ class DownloadsScene : BaseToolbarScene(), DownloadInfoListener, OnClickFabListe
private inner class DownloadLabelAdapter(private val mInflater: LayoutInflater) :
RecyclerView.Adapter<DownloadLabelHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DownloadLabelHolder {
return DownloadLabelHolder(mInflater.inflate(R.layout.item_drawer_list, parent, false))
val holder =
DownloadLabelHolder(mInflater.inflate(R.layout.item_drawer_list, parent, false))
holder.itemView.setOnClickListener {
val position = holder.bindingAdapterPosition
val label1: String? = if (position == 0) {
null
} else {
mLabels[position]
}
if (!ObjectUtils.equal(label1, mLabel)) {
mLabel = label1
updateForLabel()
updateView()
closeDrawer(GravityCompat.END)
}
}
holder.edit.setOnClickListener {
val context = context
val label = mLabels[holder.bindingAdapterPosition]
if (context != null) {
val builder = EditTextDialogBuilder(
context, label, getString(R.string.download_labels)
)
builder.setTitle(R.string.rename_label_title)
builder.setPositiveButton(android.R.string.ok, null)
val dialog = builder.show()
RenameLabelDialogHelper(builder, dialog, label)
}
}
return holder
}
@SuppressLint("SetTextI18n")
@@ -805,10 +829,18 @@ class DownloadsScene : BaseToolbarScene(), DownloadInfoListener, OnClickFabListe
}
var list: List<DownloadInfo?>? = null
if (mDownloadManager != null) {
list = if (position == 0) {
mDownloadManager!!.defaultDownloadInfoList
} else {
mDownloadManager!!.getLabelDownloadInfoList(label)
list = when (position) {
0 -> {
mDownloadManager!!.allDownloadInfoList
}
1 -> {
mDownloadManager!!.defaultDownloadInfoList
}
else -> {
mDownloadManager!!.getLabelDownloadInfoList(label)
}
}
}
if (list != null) {
@@ -816,39 +848,17 @@ class DownloadsScene : BaseToolbarScene(), DownloadInfoListener, OnClickFabListe
} else {
holder.label.text = label
}
holder.itemView.setOnClickListener {
val label1: String? = if (position == 0) {
null
} else {
mLabels[position]
}
if (!ObjectUtils.equal(label1, mLabel)) {
mLabel = label1
updateForLabel()
updateView()
closeDrawer(GravityCompat.END)
}
}
if (position > 0) {
holder.edit.visibility = View.VISIBLE
holder.edit.setOnClickListener {
if (context != null) {
val builder = EditTextDialogBuilder(
context, label, getString(R.string.download_labels)
)
builder.setTitle(R.string.rename_label_title)
builder.setPositiveButton(android.R.string.ok, null)
val dialog = builder.show()
RenameLabelDialogHelper(builder, dialog, label)
}
}
} else {
if (position < LABEL_OFFSET) {
holder.edit.visibility = View.GONE
holder.option.visibility = View.GONE
} else {
holder.edit.visibility = View.VISIBLE
holder.option.visibility = View.VISIBLE
}
}
override fun getItemId(position: Int): Long {
return mLabels[position].hashCode().toLong()
return (if (position < LABEL_OFFSET) position else mLabels[position].hashCode()).toLong()
}
override fun getItemCount(): Int {
@@ -969,8 +979,7 @@ class DownloadsScene : BaseToolbarScene(), DownloadInfoListener, OnClickFabListe
GalleryDetailScene.ACTION_GALLERY_INFO
)
args.putParcelable(GalleryDetailScene.KEY_GALLERY_INFO, list[index])
val announcer = Announcer(GalleryDetailScene::class.java).setArgs(args)
startScene(announcer)
navigate(R.id.galleryDetailScene, args)
} else if (start === v) {
val intent = Intent(activity, DownloadService::class.java)
intent.action = DownloadService.ACTION_START
@@ -1010,6 +1019,8 @@ class DownloadsScene : BaseToolbarScene(), DownloadInfoListener, OnClickFabListe
lp.width = mListThumbWidth
lp.height = mListThumbHeight
holder.thumb.layoutParams = lp
holder.itemView.setOnClickListener { onItemClick(holder.bindingAdapterPosition) }
holder.itemView.setOnLongClickListener { onItemLongClick(holder.bindingAdapterPosition) }
return holder
}
@@ -1018,8 +1029,8 @@ class DownloadsScene : BaseToolbarScene(), DownloadInfoListener, OnClickFabListe
return
}
val info = mList!![position]
if (info.thumb != null) {
holder.thumb.load(EhCacheKeyFactory.getThumbKey(info.gid), info.thumb!!)
info.thumb?.let {
holder.thumb.load(EhCacheKeyFactory.getThumbKey(info.gid), EhUtils.fixThumbUrl(it))
}
holder.title.text = EhUtils.getSuitableTitle(info)
holder.uploader.text = info.uploader
@@ -1031,14 +1042,6 @@ class DownloadsScene : BaseToolbarScene(), DownloadInfoListener, OnClickFabListe
category.setBackgroundColor(EhUtils.getCategoryColor(info.category))
}
bindForState(holder, info)
// Update transition name
ViewCompat.setTransitionName(
holder.thumb,
TransitionNameFactory.getThumbTransitionName(info.gid)
)
holder.itemView.setOnClickListener { onItemClick(position) }
holder.itemView.setOnLongClickListener { onItemLongClick(position) }
}
override fun getItemCount(): Int {
@@ -1150,13 +1153,17 @@ class DownloadsScene : BaseToolbarScene(), DownloadInfoListener, OnClickFabListe
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder
): Int {
return makeMovementFlags(
ItemTouchHelper.UP or ItemTouchHelper.DOWN,
ItemTouchHelper.LEFT
)
val position = viewHolder.bindingAdapterPosition
return if (position < LABEL_OFFSET) {
makeMovementFlags(0, 0)
} else {
makeMovementFlags(
ItemTouchHelper.UP or ItemTouchHelper.DOWN,
ItemTouchHelper.LEFT
)
}
}
@SuppressLint("NotifyDataSetChanged")
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
@@ -1165,37 +1172,87 @@ class DownloadsScene : BaseToolbarScene(), DownloadInfoListener, OnClickFabListe
val fromPosition = viewHolder.bindingAdapterPosition
val toPosition = target.bindingAdapterPosition
val context = context
if (null == context || fromPosition == toPosition || toPosition == 0 || fromPosition == 0) {
if (null == context || fromPosition == toPosition || toPosition < LABEL_OFFSET) {
return false
}
downloadManager.moveLabel(fromPosition - 1, toPosition - 1)
downloadManager.moveLabel(fromPosition - LABEL_OFFSET, toPosition - LABEL_OFFSET)
val item = mLabels.removeAt(fromPosition)
mLabels.add(toPosition, item)
mLabelAdapter!!.notifyDataSetChanged()
mLabelAdapter!!.notifyItemMoved(fromPosition, toPosition)
return true
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
val position = viewHolder.bindingAdapterPosition
if (mDownloadManager == null) return
lifecycleScope.launchIO {
if (position != 0) {
val label = mLabels[position]
mDownloadManager!!.deleteLabel(label)
}
mLabels.removeAt(position)
withUIContext {
mLabelAdapter!!.notifyItemRemoved(position)
}
}
val label = mLabels[position]
BaseDialogBuilder(context!!)
.setMessage(getString(R.string.delete_label, label))
.setPositiveButton(R.string.delete) { _, _ ->
lifecycleScope.launchIO {
mDownloadManager!!.deleteLabel(label)
mLabels.removeAt(position)
withUIContext {
mLabelAdapter!!.notifyItemRemoved(position)
}
}
}.setNegativeButton(android.R.string.cancel) { dialog, _ ->
dialog.cancel()
}.setOnCancelListener {
mLabelAdapter!!.notifyItemChanged(position)
}.show()
}
}
private val itemTouchHelper = ItemTouchHelper(object : ItemTouchHelper.Callback() {
override fun getMovementFlags(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder
): Int {
return makeMovementFlags(
ItemTouchHelper.UP or ItemTouchHelper.DOWN,
0
)
}
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
val fromPosition = viewHolder.bindingAdapterPosition
val toPosition = target.bindingAdapterPosition
if (fromPosition == toPosition) {
return false
}
val allInfoList = mDownloadManager!!.allDownloadInfoList
val dbFromPosition = allInfoList.indexOf(mList!![fromPosition])
val dbToPosition = allInfoList.indexOf(mList!![toPosition])
lifecycleScope.launchIO {
EhDB.moveDownloadInfo(dbFromPosition, dbToPosition)
val item = allInfoList.removeAt(dbFromPosition)
allInfoList.add(dbToPosition, item)
if (mLabel != null) {
val item1 = mList!!.removeAt(fromPosition)
mList!!.add(toPosition, item1)
}
withUIContext {
mAdapter!!.notifyItemMoved(fromPosition, toPosition)
}
}
return true
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
}
})
companion object {
const val KEY_GID = "gid"
const val KEY_ACTION = "action"
const val ACTION_CLEAR_DOWNLOAD_SERVICE = "clear_download_service"
private val TAG = DownloadsScene::class.java.simpleName
private const val KEY_LABEL = "label"
private const val LABEL_OFFSET = 2
}
}
@@ -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.getGid()), gi.getThumb());
holder.title.setText(EhUtils.getSuitableTitle(gi));
holder.uploader.setText(gi.getUploader());
holder.uploader.setAlpha(gi.getDisowned() ? .5f : 1f);
holder.rating.setRating(gi.getRating());
TextView category = holder.category;
String newCategoryText = EhUtils.getCategory(gi.getCategory());
if (!newCategoryText.equals(category.getText().toString())) {
category.setText(newCategoryText);
category.setBackgroundColor(EhUtils.getCategoryColor(gi.getCategory()));
}
holder.posted.setText(gi.getPosted());
if (gi.getPages() == 0 || !Settings.getShowGalleryPages()) {
holder.pages.setText(null);
holder.pages.setVisibility(View.GONE);
} else {
holder.pages.setText(gi.getPages() + "P");
holder.pages.setVisibility(View.VISIBLE);
}
if (TextUtils.isEmpty(gi.getSimpleLanguage())) {
holder.simpleLanguage.setText(null);
holder.simpleLanguage.setVisibility(View.GONE);
} else {
holder.simpleLanguage.setText(gi.getSimpleLanguage());
holder.simpleLanguage.setVisibility(View.VISIBLE);
}
holder.favourited.setVisibility((mShowFavourited && gi.getFavoriteSlot() >= -1 && gi.getFavoriteSlot() <= 10) ? View.VISIBLE : View.GONE);
holder.downloaded.setVisibility(mDownloadManager.containDownloadInfo(gi.getGid()) ? View.VISIBLE : View.GONE);
}
case TYPE_GRID -> {
((TileThumb) holder.thumb).setThumbSize(gi.getThumbWidth(), gi.getThumbHeight());
holder.thumb.load(EhCacheKeyFactory.getThumbKey(gi.getGid()), gi.getThumb());
View category = holder.category;
Drawable drawable = category.getBackground();
int color = EhUtils.getCategoryColor(gi.getCategory());
if (!(drawable instanceof TriangleDrawable)) {
drawable = new TriangleDrawable(color);
category.setBackground(drawable);
} else {
((TriangleDrawable) drawable).setColor(color);
}
holder.simpleLanguage.setText(gi.getSimpleLanguage());
}
}
// Update transition name
ViewCompat.setTransitionName(holder.thumb, TransitionNameFactory.getThumbTransitionName(gi.getGid()));
}
@IntDef({TYPE_LIST, TYPE_GRID})
@Retention(RetentionPolicy.SOURCE)
public @interface Type {
}
}
@@ -0,0 +1,235 @@
/*
* 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.dao.LocalFavoriteInfo
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
if (gi is LocalFavoriteInfo) {
gi.thumb = EhUtils.fixThumbUrl(gi.thumb!!)
}
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)
}
@@ -92,8 +92,6 @@ import com.hippo.ehviewer.ui.CommonOperations
import com.hippo.ehviewer.ui.dialog.SelectItemWithIconAdapter
import com.hippo.ehviewer.widget.GalleryInfoContentHelper
import com.hippo.ehviewer.widget.SearchLayout
import com.hippo.scene.Announcer
import com.hippo.scene.SceneFragment
import com.hippo.view.BringOutTransition
import com.hippo.view.ViewTransition
import com.hippo.widget.FabLayout
@@ -206,32 +204,20 @@ class GalleryListScene : SearchBarScene(), OnDragHandlerListener, SearchLayout.H
return R.menu.scene_gallery_list_searchbar_menu
}
override fun getNavCheckedItem(): Int {
return mNavCheckedId
}
private fun handleArgs(args: Bundle?) {
args ?: return
mUrlBuilder = when (args.getString(KEY_ACTION)) {
val action = args?.getString(KEY_ACTION) ?: ACTION_HOMEPAGE
mUrlBuilder = when (action) {
ACTION_HOMEPAGE -> ListUrlBuilder()
ACTION_SUBSCRIPTION -> ListUrlBuilder(MODE_SUBSCRIPTION)
ACTION_WHATS_HOT -> ListUrlBuilder(MODE_WHATS_HOT)
ACTION_TOP_LIST -> ListUrlBuilder(MODE_TOPLIST, mKeyword = "11")
ACTION_LIST_URL_BUILDER -> args.getParcelable<ListUrlBuilder>(KEY_LIST_URL_BUILDER)
ACTION_LIST_URL_BUILDER -> args?.getParcelable<ListUrlBuilder>(KEY_LIST_URL_BUILDER)
?.copy() ?: ListUrlBuilder()
else -> throw IllegalStateException("Wrong KEY_ACTION:${args.getString(KEY_ACTION)} when handle args!")
else -> throw IllegalStateException("Wrong KEY_ACTION:${args?.getString(KEY_ACTION)} when handle args!")
}
}
override fun onNewArguments(args: Bundle) {
handleArgs(args)
onUpdateUrlBuilder()
mHelper?.refresh()
setState(STATE_NORMAL)
showSearchBar()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mDownloadManager.addDownloadInfoListener(mDownloadInfoListener)
@@ -355,18 +341,6 @@ class GalleryListScene : SearchBarScene(), OnDragHandlerListener, SearchLayout.H
title = resources.getString(R.string.search)
}
setSearchBarHint(title)
// Update nav checked item
val checkedItemId: Int = when (mode) {
ListUrlBuilder.MODE_NORMAL -> if (EhUtils.NONE == category && TextUtils.isEmpty(keyword)) R.id.nav_homepage else 0
MODE_SUBSCRIPTION -> R.id.nav_subscription
MODE_WHATS_HOT -> R.id.nav_whats_hot
MODE_TOPLIST -> R.id.nav_toplist
ListUrlBuilder.MODE_TAG, ListUrlBuilder.MODE_UPLOADER, ListUrlBuilder.MODE_IMAGE_SEARCH -> 0
else -> throw IllegalStateException("Unexpected value: $mode")
}
navCheckedItem = checkedItemId
mNavCheckedId = checkedItemId
}
override fun onCreateViewWithToolbar(
@@ -427,12 +401,7 @@ class GalleryListScene : SearchBarScene(), OnDragHandlerListener, SearchLayout.H
val colorID = theme.resolveColor(com.google.android.material.R.attr.colorOnSurface)
mActionFabDrawable = AddDeleteDrawable(context, colorID)
binding.fabLayout.primaryFab!!.setImageDrawable(mActionFabDrawable)
mSearchFab!!.setOnClickListener {
if (STATE_NORMAL != mState) {
onApplySearch()
hideSoftInput()
}
}
mSearchFab!!.setOnClickListener { onApplySearch() }
// Update list url builder
onUpdateUrlBuilder()
@@ -619,8 +588,7 @@ class GalleryListScene : SearchBarScene(), OnDragHandlerListener, SearchLayout.H
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)
}
override fun onClickPrimaryFab(view: FabLayout, fab: FloatingActionButton) {
@@ -728,7 +696,7 @@ class GalleryListScene : SearchBarScene(), OnDragHandlerListener, SearchLayout.H
override fun onSearchViewExpanded() {
super.onSearchViewExpanded()
selectSearchFab(true)
if (mState == STATE_NORMAL) selectSearchFab(true)
}
override fun onSearchViewHidden() {
@@ -805,13 +773,13 @@ class GalleryListScene : SearchBarScene(), OnDragHandlerListener, SearchLayout.H
CommonOperations.removeFromFavorites(
activity,
gi,
RemoveFromFavoriteListener(context, activity.stageId, tag)
RemoveFromFavoriteListener(context)
)
} else {
CommonOperations.addToFavorites(
activity,
gi,
AddToFavoriteListener(context, activity.stageId, tag)
AddToFavoriteListener(context)
)
}
@@ -1102,30 +1070,25 @@ class GalleryListScene : SearchBarScene(), OnDragHandlerListener, SearchLayout.H
@IntDef(STATE_NORMAL, STATE_SIMPLE_SEARCH, STATE_SEARCH, STATE_SEARCH_SHOW_LIST)
@Retention(AnnotationRetention.SOURCE)
private annotation class State
private class GetGalleryListListener(
private inner class GetGalleryListListener(
context: Context,
stageId: Int,
sceneTag: String?,
private val mTaskId: Int
) : EhCallback<GalleryListScene, GalleryListParser.Result>(context, stageId, sceneTag) {
) : EhCallback<GalleryListScene, GalleryListParser.Result>(context) {
override fun onSuccess(result: GalleryListParser.Result) {
val scene = scene
scene?.onGetGalleryListSuccess(result, mTaskId)
val scene = this@GalleryListScene
scene.onGetGalleryListSuccess(result, mTaskId)
}
override fun onFailure(e: Exception) {
val scene = scene
scene?.onGetGalleryListFailure(e, mTaskId)
val scene = this@GalleryListScene
scene.onGetGalleryListFailure(e, mTaskId)
}
override fun onCancel() {}
override fun isInstance(scene: SceneFragment): Boolean {
return scene is GalleryListScene
}
}
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)
}
@@ -1135,13 +1098,10 @@ class GalleryListScene : SearchBarScene(), OnDragHandlerListener, SearchLayout.H
}
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)
}
@@ -1151,9 +1111,6 @@ class GalleryListScene : SearchBarScene(), OnDragHandlerListener, SearchLayout.H
}
override fun onCancel() {}
override fun isInstance(scene: SceneFragment): Boolean {
return scene is GalleryListScene
}
}
private class QsDrawerHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
@@ -1184,17 +1141,13 @@ class GalleryListScene : SearchBarScene(), OnDragHandlerListener, SearchLayout.H
private inner class QsDrawerAdapter(private val mInflater: LayoutInflater) :
RecyclerView.Adapter<QsDrawerHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): QsDrawerHolder {
return QsDrawerHolder(mInflater.inflate(R.layout.item_drawer_list, parent, false))
}
override fun onBindViewHolder(holder: QsDrawerHolder, position: Int) {
val holder = QsDrawerHolder(mInflater.inflate(R.layout.item_drawer_list, parent, false))
if (!mIsTopList) {
holder.key.text = mQuickSearchList[position].name
holder.itemView.setOnClickListener {
if (null == mHelper) {
return@setOnClickListener
}
val q = mQuickSearchList[position]
val q = mQuickSearchList[holder.bindingAdapterPosition]
mUrlBuilder.set(q)
onUpdateUrlBuilder()
val i = q.name!!.lastIndexOf("@")
@@ -1204,6 +1157,24 @@ class GalleryListScene : SearchBarScene(), OnDragHandlerListener, SearchLayout.H
}
} else {
val keywords = intArrayOf(11, 12, 13, 15)
holder.itemView.setOnClickListener {
if (null == mHelper) {
return@setOnClickListener
}
mUrlBuilder.keyword = keywords[holder.bindingAdapterPosition].toString()
onUpdateUrlBuilder()
mHelper!!.refresh()
setState(STATE_NORMAL)
closeDrawer(GravityCompat.END)
}
}
return holder
}
override fun onBindViewHolder(holder: QsDrawerHolder, position: Int) {
if (!mIsTopList) {
holder.key.text = mQuickSearchList[position].name
} else {
val toplists = intArrayOf(
R.string.toplist_alltime,
R.string.toplist_pastyear,
@@ -1212,16 +1183,6 @@ class GalleryListScene : SearchBarScene(), OnDragHandlerListener, SearchLayout.H
)
holder.key.text = getString(toplists[position])
holder.option.visibility = View.GONE
holder.itemView.setOnClickListener {
if (null == mHelper) {
return@setOnClickListener
}
mUrlBuilder.keyword = keywords[position].toString()
onUpdateUrlBuilder()
mHelper!!.refresh()
setState(STATE_NORMAL)
closeDrawer(GravityCompat.END)
}
}
}
@@ -1256,7 +1217,7 @@ class GalleryListScene : SearchBarScene(), OnDragHandlerListener, SearchLayout.H
}
override fun onClick() {
startScene(createAnnouncer())
navigate(getDestination(), getArgs())
if (mState == STATE_SIMPLE_SEARCH) {
setState(STATE_NORMAL)
} else if (mState == STATE_SEARCH_SHOW_LIST) {
@@ -1264,19 +1225,25 @@ class GalleryListScene : SearchBarScene(), OnDragHandlerListener, SearchLayout.H
}
}
abstract fun createAnnouncer(): Announcer?
abstract fun getDestination(): Int
abstract fun getArgs(): Bundle
}
private inner class GalleryDetailUrlSuggestion(
private val mGid: Long,
private val mToken: String
) : UrlSuggestion() {
override fun createAnnouncer(): Announcer? {
override fun getDestination(): Int {
return R.id.galleryDetailScene
}
override fun getArgs(): Bundle {
val args = Bundle()
args.putString(GalleryDetailScene.KEY_ACTION, GalleryDetailScene.ACTION_GID_TOKEN)
args.putLong(GalleryDetailScene.KEY_GID, mGid)
args.putString(GalleryDetailScene.KEY_TOKEN, mToken)
return Announcer(GalleryDetailScene::class.java).setArgs(args)
return args
}
}
@@ -1285,13 +1252,17 @@ class GalleryListScene : SearchBarScene(), OnDragHandlerListener, SearchLayout.H
private val mPToken: String,
private val mPage: Int
) : UrlSuggestion() {
override fun createAnnouncer(): Announcer? {
override fun getDestination(): Int {
return R.id.progressScene
}
override fun getArgs(): Bundle {
val args = Bundle()
args.putString(ProgressScene.KEY_ACTION, ProgressScene.ACTION_GALLERY_TOKEN)
args.putLong(ProgressScene.KEY_GID, mGid)
args.putString(ProgressScene.KEY_PTOKEN, mPToken)
args.putInt(ProgressScene.KEY_PAGE, mPage)
return Announcer(ProgressScene::class.java).setArgs(args)
return args
}
}
@@ -1303,11 +1274,11 @@ class GalleryListScene : SearchBarScene(), OnDragHandlerListener, SearchLayout.H
return if (null != mHelper) mHelper!!.size() else 0
}
public override fun onItemClick(view: View, position: Int) {
override fun onItemClick(view: View, position: Int) {
this@GalleryListScene.onItemClick(position)
}
public override fun onItemLongClick(view: View, position: Int): Boolean {
override fun onItemLongClick(view: View, position: Int): Boolean {
return this@GalleryListScene.onItemLongClick(position)
}
@@ -1340,7 +1311,7 @@ class GalleryListScene : SearchBarScene(), OnDragHandlerListener, SearchLayout.H
request.setCallback(
GetGalleryListListener(
context,
activity.stageId, tag, taskId
taskId
)
)
request.setArgs(
@@ -1356,7 +1327,7 @@ class GalleryListScene : SearchBarScene(), OnDragHandlerListener, SearchLayout.H
request.setCallback(
GetGalleryListListener(
context,
activity.stageId, tag, taskId
taskId
)
)
request.setArgs(url)
@@ -1434,13 +1405,21 @@ class GalleryListScene : SearchBarScene(), OnDragHandlerListener, SearchLayout.H
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
val position = viewHolder.bindingAdapterPosition
val quickSearch = mQuickSearchList[position]
lifecycleScope.launchIO {
EhDB.deleteQuickSearch(quickSearch)
mQuickSearchList.removeAt(position)
withUIContext {
mAdapter.notifyItemRemoved(position)
}
}
BaseDialogBuilder(context!!)
.setMessage(getString(R.string.delete_quick_search, quickSearch.name))
.setPositiveButton(R.string.delete) { _, _ ->
lifecycleScope.launchIO {
EhDB.deleteQuickSearch(quickSearch)
mQuickSearchList.removeAt(position)
withUIContext {
mAdapter.notifyItemRemoved(position)
}
}
}.setNegativeButton(android.R.string.cancel) { dialog, _ ->
dialog.cancel()
}.setOnCancelListener {
mAdapter.notifyItemChanged(position)
}.show()
}
}
@@ -1491,16 +1470,11 @@ class GalleryListScene : SearchBarScene(), OnDragHandlerListener, SearchLayout.H
}
}
@JvmStatic
fun startScene(scene: SceneFragment, lub: ListUrlBuilder?) {
scene.startScene(getStartAnnouncer(lub))
}
fun getStartAnnouncer(lub: ListUrlBuilder?): Announcer {
fun ListUrlBuilder.toStartArgs(): Bundle {
val args = Bundle()
args.putString(KEY_ACTION, ACTION_LIST_URL_BUILDER)
args.putParcelable(KEY_LIST_URL_BUILDER, lub)
return Announcer(GalleryListScene::class.java).setArgs(args)
args.putParcelable(KEY_LIST_URL_BUILDER, this)
return args
}
}
}
@@ -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.getGid(), 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.getGid(), mGalleryInfo.getToken(), 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,358 @@
/*
* 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
private var mNextPage: Boolean = 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)
mNextPage = args.getBoolean(KEY_NEXT_PAGE)
}
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)
}
runCatching {
mHelper!!.mPages = (mGalleryInfo as GalleryDetail).previewPages
if (mNextPage && mHelper!!.pages > 1) mHelper!!.goTo(1)
}.onFailure {
it.printStackTrace()
}
}
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"
const val KEY_NEXT_PAGE = "next_page"
private const val KEY_HAS_FIRST_REFRESH = "has_first_refresh"
}
}
@@ -48,8 +48,6 @@ 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
@@ -114,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)
@@ -223,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
}
@@ -293,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)
)
}
@@ -325,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)
}
@@ -336,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)
}
@@ -352,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) {
@@ -409,7 +396,9 @@ class HistoryScene : BaseToolbarScene() {
override fun onBindViewHolder(holder: HistoryHolder, position: Int) {
val gi: GalleryInfo? = getItem(position)
gi ?: return
gi.thumb?.let { holder.thumb.load(EhCacheKeyFactory.getThumbKey(gi.gid), it) }
gi.thumb?.let {
holder.thumb.load(EhCacheKeyFactory.getThumbKey(gi.gid), EhUtils.fixThumbUrl(it))
}
holder.title.text = EhUtils.getSuitableTitle(gi)
holder.uploader.text = gi.uploader
holder.rating.rating = gi.rating
@@ -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()
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"
}
}
@@ -53,8 +53,6 @@ abstract class SearchBarScene : BaseScene(), ToolBarScene {
updateSuggestions()
}
binding.searchview.editText.setOnEditorActionListener { _, _, _ ->
binding.toolbar.text = binding.searchview.text
binding.searchview.hide()
onApplySearch()
true
}
@@ -157,6 +155,8 @@ abstract class SearchBarScene : BaseScene(), ToolBarScene {
}
fun onApplySearch() {
binding.toolbar.text = binding.searchview.text
binding.searchview.hide()
val query = binding.toolbar.text.toString().trim()
if (!mAllowEmptySearch && query.isEmpty()) {
return
@@ -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();
}
}
}
}
}
@@ -35,7 +35,6 @@ class AdvanceSearchTable @JvmOverloads constructor(
) : LinearLayout(context, attrs) {
private var mSh: CheckBox
private var mSto: CheckBox
private var mSr: CheckBox
private var mMinRating: TextInputLayout
private var mSp: CheckBox
private var mSpf: EditText
@@ -53,8 +52,7 @@ class AdvanceSearchTable @JvmOverloads constructor(
mSh = row0.getChildAt(0) as CheckBox
mSto = row0.getChildAt(1) as CheckBox
val row1 = getChildAt(1) as ViewGroup
mSr = row1.getChildAt(0) as CheckBox
mMinRating = row1.getChildAt(1) as TextInputLayout
mMinRating = row1.getChildAt(0) as TextInputLayout
val row2 = getChildAt(2) as ViewGroup
mSp = row2.getChildAt(0) as CheckBox
mSpf = row2.getChildAt(1) as EditText
@@ -72,11 +70,6 @@ class AdvanceSearchTable @JvmOverloads constructor(
true
}
mMinRating.isEnabled = mSr.isChecked
mSr.setOnCheckedChangeListener { _: CompoundButton, isChecked: Boolean ->
mMinRating.isEnabled = isChecked
}
mSpt.isEnabled = mSp.isChecked
mSpf.isEnabled = mSp.isChecked
mSp.setOnCheckedChangeListener { _: CompoundButton, isChecked: Boolean ->
@@ -105,22 +98,17 @@ class AdvanceSearchTable @JvmOverloads constructor(
var minRating: Int
get() {
val position = mArray.indexOf(mMinRating.editText!!.text.toString())
return if (mSr.isChecked && position >= 0) {
position + 2
return if (position > 0) {
position + 1
} else {
-1
}
}
set(minRating) {
if (minRating in 2..5) {
mSr.isChecked = true
(mMinRating.editText!! as AutoCompleteTextView).setText(
mArray[minRating - 2],
false
)
} else {
mSr.isChecked = false
}
(mMinRating.editText!! as AutoCompleteTextView).setText(
mArray[if (minRating in 2..5) minRating - 1 else 0],
false
)
}
var pageFrom: Int
get() = if (mSp.isChecked) {
@@ -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();
}
}
}
}
}
@@ -103,8 +103,8 @@ class SearchLayout @JvmOverloads constructor(
mChip.isChecked = NumberUtils.int2boolean(mPair.first and mCategoryStored)
mChip.setOnLongClickListener {
if (mChip.isChecked) {
for (i in 1 until mCategoryGroup.childCount + 1) {
mCategoryGroup.check(i)
for (i in 0 until mCategoryGroup.childCount) {
(mCategoryGroup.getChildAt(i) as Chip).isChecked = true
}
mChip.isChecked = false
} else {
+38 -43
View File
@@ -19,51 +19,44 @@ package com.hippo.image
import android.graphics.ColorSpace
import android.graphics.ImageDecoder
import android.graphics.ImageDecoder.DecodeException
import android.graphics.ImageDecoder.ImageInfo
import android.graphics.ImageDecoder.Source
import android.graphics.drawable.Animatable
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
import com.hippo.UriArchiveAccessor
import com.hippo.ehviewer.EhApplication
import java.io.FileInputStream
import java.nio.ByteBuffer
import java.nio.channels.FileChannel
import kotlin.math.min
class Image private constructor(source: Source, private val byteBuffer: ByteBuffer? = null) {
var mObtainedDrawable: Drawable?
init {
mObtainedDrawable =
ImageDecoder.decodeDrawable(source) { decoder: ImageDecoder, info: ImageInfo, _: Source ->
decoder.setTargetColorSpace(colorSpace)
decoder.setTargetSampleSize(
min(
info.size.width / (2 * screenWidth),
info.size.height / (2 * screenHeight)
).coerceAtLeast(1)
)
class Image private constructor(source: Source, private var src: ByteBufferSource?) {
var mObtainedDrawable: Drawable? =
ImageDecoder.decodeDrawable(source) { decoder: ImageDecoder, info: ImageInfo, _: Source ->
decoder.setTargetColorSpace(colorSpace)
decoder.setTargetSampleSize(
min(
info.size.width / (2 * screenWidth),
info.size.height / (2 * screenHeight)
).coerceAtLeast(1)
)
}.also {
(it as? BitmapDrawable)?.run {
src?.close()
src = null
}
if (mObtainedDrawable is BitmapDrawable)
byteBuffer?.let { UriArchiveAccessor.releaseByteBuffer(it) }
}
}
private set
val width: Int
get() = mObtainedDrawable?.intrinsicWidth ?: 0
val height: Int
get() = mObtainedDrawable?.intrinsicHeight ?: 0
val size: Int
get() = mObtainedDrawable!!.run { intrinsicHeight * intrinsicWidth * 4 * if (this is Animatable) 4 else 1 }
@Synchronized
fun recycle() {
(mObtainedDrawable as? Animatable)?.run {
stop()
byteBuffer?.let { UriArchiveAccessor.releaseByteBuffer(it) }
}
(mObtainedDrawable as? BitmapDrawable)?.run { bitmap.recycle() }
(mObtainedDrawable as? Animatable)?.stop()
(mObtainedDrawable as? BitmapDrawable)?.bitmap?.recycle()
mObtainedDrawable?.callback = null
mObtainedDrawable = null
src?.close()
src = null
}
companion object {
@@ -77,23 +70,25 @@ class Image private constructor(source: Source, private val byteBuffer: ByteBuff
) ColorSpace.Named.DISPLAY_P3 else ColorSpace.Named.SRGB
)
@Throws(DecodeException::class)
@JvmStatic
fun decode(stream: FileInputStream): Image {
val src = ImageDecoder.createSource(
stream.channel.map(
FileChannel.MapMode.READ_ONLY, 0,
stream.available().toLong()
)
)
return Image(src)
fun decode(src: ByteBufferSource): Image? {
val directBuffer = src.getByteBuffer()
check(directBuffer.isDirect)
rewriteGifSource(directBuffer)
return runCatching {
Image(ImageDecoder.createSource(directBuffer), src)
}.onFailure {
src.close()
it.printStackTrace()
}.getOrNull()
}
@Throws(DecodeException::class)
@JvmStatic
fun decode(buffer: ByteBuffer): Image {
val src = ImageDecoder.createSource(buffer)
return Image(src, buffer)
}
private external fun rewriteGifSource(buffer: ByteBuffer)
}
interface ByteBufferSource : AutoCloseable {
// A read-write direct bytebuffer, it may in native heap or file mapping
fun getByteBuffer(): ByteBuffer
}
}
@@ -34,11 +34,6 @@ public class UniFileInputStreamPipe implements InputStreamPipe {
mFile = file;
}
@Override
public void obtain() {
// Empty
}
@Override
public void release() {
// Empty
Loaded 100 of 154 files, more files were not shown because too many files have changed in this diff. Show more