Compare commits

..
400 Commits
Author SHA1 Message Date
tarsin d3d07f9e4e Release 1.8.7.0
Releases / Build (push) Has been cancelled
2023-02-27 15:53:49 +08:00
tarsin e5500b978c Apply alpha only to tag text, Resolve #894 2023-02-27 12:06:57 +08:00
FooIbar 2fea94ce9b fixup! Implement image request merging in standalone Interceptor 2023-02-27 02:32:33 +08:00
tarsin 90b6106fb4 Convert libarchive build to FetchContent 2023-02-26 17:31:59 +08:00
tarsin 6e15d1e480 Convert lzma build to FetchContent 2023-02-26 15:11:45 +08:00
tarsin d35588b993 Don't add custom optimization in debug build, and remove some flags already passed 2023-02-26 14:04:20 +08:00
tarsin 957ecf0fe1 Fix rating calculate 2023-02-25 23:47:10 +08:00
tarsin 83dc47c3b5 Fix SimpleRatingWidget state changed recompose and properly set theme 2023-02-25 23:40:01 +08:00
tarsin fa4d35487c Optimize SimpleRatingWidget 2023-02-25 23:19:09 +08:00
tarsin 624a20c9bf Move RatingView to Compose impl 2023-02-25 23:14:57 +08:00
tarsin 58cd5c2896 Rename .java to .kt 2023-02-25 23:14:57 +08:00
renovate[bot] bb8a802d65 Update dependency org.jetbrains.kotlinx:kotlinx-serialization-cbor to v1.5.0 2023-02-25 02:44:44 +08:00
FooIbar f757e98da0 Implement onNewIntent() in ReaderActivity 2023-02-25 02:36:37 +08:00
FooIbar 49661c0bf0 Implement tachiyomi.util.system.logcat 2023-02-25 02:36:37 +08:00
tarsin 42f36b8e74 Release 1.8.7.0-alpha
Releases / Build (push) Has been cancelled
2023-02-24 20:30:15 +08:00
tarsin 9bc3c9a902 Add Resource lock to avoid memory corruption 2023-02-24 20:02:35 +08:00
tarsin 1dd04740bb Use MADV_DONTNEED as fallback 2023-02-24 19:22:10 +08:00
FooIbar 35201e6615 Use natsort for better archive file sorting 2023-02-24 18:43:08 +08:00
tarsin 86d416bccf Don't populate archive 2023-02-24 18:33:44 +08:00
tarsin 979841665a Convert PreciselyClipDrawable to kt 2023-02-24 16:53:35 +08:00
tarsin c29b8c114b Rename .java to .kt 2023-02-24 16:53:35 +08:00
tarsin a03eae3c87 Refactor importDB! 2023-02-24 16:36:41 +08:00
tarsin 704c2a261e Use MAP_UNINITIALIZED to improve performance 2023-02-24 13:05:43 +08:00
tarsin 8fabdd1926 Suppress lint 2023-02-24 12:41:07 +08:00
tarsin f9df373a16 Some infeasible optimization 2023-02-24 12:39:58 +08:00
tarsin f959f98012 Minor cleanup 2023-02-24 12:02:42 +08:00
tarsin e12a93f36b Introduce new archive decompress mempool, Resolve #307 2023-02-24 11:37:30 +08:00
FooIbar c3aca0a88d Fix missing parentheses 2023-02-24 10:01:49 +08:00
FooIbar add7819945 Minor refactor
Revert "Fix RTL load"

This reverts commit 323d258b

Revert "Resolve #814"

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

* Initial Workmanager migration

* Fix notification

* *

* Add timepicker

* Fix TimePicker layout

* Use 12H clock

* *

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

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

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

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

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

This reverts commit 46c89534fd.

* Work KNI build script

* Load Kni library as well

* Fix lib name

* Setup Kotlin Compiler

* Setup Kotlin Compiler

* Setup Kotlin Compiler

* Fix kotlin native compiler not available

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

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

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

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

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

* Convert DownloadManager to kotlin

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

No files matched your search

+1 -1
View File
@@ -25,7 +25,7 @@ jobs:
- name: Install NDK
run: |
echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;25.1.8937393" --sdk_root=${ANDROID_SDK_ROOT} &> /dev/null
echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;25.2.9519653" --sdk_root=${ANDROID_SDK_ROOT} &> /dev/null
- name: Gradle Build
uses: gradle/gradle-build-action@v2
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
- name: Install NDK
run: |
echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;25.1.8937393" --sdk_root=${ANDROID_SDK_ROOT} &> /dev/null
echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;25.2.9519653" --sdk_root=${ANDROID_SDK_ROOT} &> /dev/null
- name: Gradle Build
uses: gradle/gradle-build-action@v2
+3 -2
View File
@@ -23,7 +23,7 @@ jobs:
- name: Install NDK
run: |
echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;25.1.8937393" --sdk_root=${ANDROID_SDK_ROOT} &> /dev/null
echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;25.2.9519653" --sdk_root=${ANDROID_SDK_ROOT} &> /dev/null
- name: Gradle Build
uses: gradle/gradle-build-action@v2
@@ -58,7 +58,7 @@ jobs:
cp app/build/outputs/apk/release/app-armeabi-v7a-release.apk app/build/outputs/apk/release/fdroid/EhViewer-v$GITHUB_REF_NAME-armv7.apk
- name: Push Apks
uses: cpina/github-action-push-to-another-repository@v1.5
uses: Ehviewer-Overhauled/github-action-push-to-another-repository@main
env:
SSH_DEPLOY_KEY: ${{ secrets.SSH_DEPLOY_KEY }}
with:
@@ -68,5 +68,6 @@ jobs:
user-name: 'github-actions[bot]'
user-email: 'github-actions[bot]@users.noreply.github.com'
target-branch: deploy
create-target-branch: true
commit-message: Add apks from $GITHUB_REF
target-directory: fdroid/tmp-apks
-6
View File
@@ -1,9 +1,3 @@
[submodule "app/src/main/cpp/libarchive"]
path = app/src/main/cpp/libarchive
url = https://github.com/Ehviewer-Overhauled/libarchive
[submodule "app/src/main/cpp/liblzma"]
path = app/src/main/cpp/liblzma
url = https://github.com/Ehviewer-Overhauled/xz
[submodule "app/src/main/cpp/nettle/nettle"]
path = app/src/main/cpp/nettle/nettle
url = https://github.com/Ehviewer-Overhauled/nettle
+3 -3
View File
@@ -1,10 +1,10 @@
<p align="right">
<strong>English<strong>
<strong>English</strong>
<span> | </span>
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/blob/1.8.x.x/docs/README/zh-cn.md">
简体中文
</a>
<span> | <span>
<span> | </span>
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/blob/1.8.x.x/docs/README/zh-tw.md">
正體中文
</a>
@@ -66,7 +66,7 @@ with Material Design 3 and Dynamic Color Support
# Download
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/releases"><img alt="Get it on GitHub" src="https://github.com/Ehviewer-Overhauled/Art/blob/master/get-it-on-github.svg" width="200px"/></a> <a href="https://github.com/Ehviewer-Overhauled/fdroid-repo"><img alt="Git it on F-Droid" src="https://github.com/Ehviewer-Overhauled/Art/blob/master/get-it-on-fdroid.svg" width="200px"/></a>
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/releases"><img alt="Get it on GitHub" src="https://github.com/Ehviewer-Overhauled/Art/blob/master/get-it-on-github.svg" width="200px"/></a> <a href="https://github.com/Ehviewer-Overhauled/fdroid-repo"><img alt="Get it on F-Droid" src="https://github.com/Ehviewer-Overhauled/Art/blob/master/get-it-on-fdroid.svg" width="200px"/></a>
# Screenshot
-169
View File
@@ -1,169 +0,0 @@
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'kotlin-parcelize'
id 'com.google.devtools.ksp'
}
android {
compileSdkVersion 33
buildToolsVersion "33.0.1"
ndkVersion "25.1.8937393"
splits {
abi {
enable = true
reset()
include 'arm64-v8a', 'x86_64', 'armeabi-v7a', 'x86'
universalApk = true
}
}
signingConfigs {
sign {
storeFile file(signStoreFile)
storePassword signStorePassword
keyAlias signkeyAlias
keyPassword signkeyPassword
}
}
defaultConfig {
applicationId "moe.tarsin.ehviewer"
minSdkVersion 28
targetSdkVersion 33
versionCode 180030
versionName "1.8.5.3-rc01"
resConfigs 'zh', 'zh-rCN', 'zh-rHK', 'zh-rTW', 'es', 'ja', 'ko', 'fr', 'de', 'th'
}
externalNativeBuild {
cmake {
path 'src/main/cpp/CMakeLists.txt'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_19
targetCompatibility JavaVersion.VERSION_19
}
kotlinOptions {
jvmTarget = '19'
freeCompilerArgs += [
// https://kotlinlang.org/docs/compiler-reference.html#progressive
"-progressive"
]
}
lint {
abortOnError true
checkReleaseBuilds false
disable 'MissingTranslation'
}
packagingOptions {
resources {
excludes += "/META-INF/CHANGES"
excludes += "/META-INF/README.md"
excludes += "/kotlin/**"
excludes += "**.txt"
excludes += "**.bin"
}
}
dependenciesInfo.includeInApk false
buildTypes {
release {
minifyEnabled true
shrinkResources true
proguardFiles 'proguard-rules.pro'
signingConfig signingConfigs.sign
}
debug {
applicationIdSuffix ".debug"
signingConfig signingConfigs.debug
}
}
buildFeatures {
buildConfig true
compose true
viewBinding true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.4.0"
}
namespace 'com.hippo.ehviewer'
}
dependencies {
// https://developer.android.com/jetpack/androidx/releases/activity
implementation "androidx.activity:activity-compose:1.6.1"
implementation "androidx.appcompat:appcompat:1.6.0"
implementation "androidx.biometric:biometric-ktx:1.2.0-alpha05"
implementation "androidx.browser:browser:1.4.0"
// https://developer.android.com/jetpack/androidx/releases/compose-material3
implementation platform("androidx.compose:compose-bom:2023.01.00")
implementation "androidx.compose.material:material-icons-extended"
implementation "androidx.compose.material3:material3"
implementation "androidx.constraintlayout:constraintlayout:2.2.0-alpha05"
implementation "androidx.coordinatorlayout:coordinatorlayout:1.2.0"
// https://developer.android.com/jetpack/androidx/releases/lifecycle
implementation "androidx.lifecycle:lifecycle-process:2.5.1"
// https://developer.android.com/jetpack/androidx/releases/navigation
def nav_version = "2.5.3"
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
implementation "androidx.navigation:navigation-compose:$nav_version"
// https://developer.android.com/jetpack/androidx/releases/paging
implementation "androidx.paging:paging-runtime-ktx:3.2.0-alpha03"
implementation 'androidx.preference:preference-ktx:1.2.0'
implementation 'androidx.recyclerview:recyclerview:1.3.0-rc01'
// https://developer.android.com/jetpack/androidx/releases/room
def room_version = "2.5.0"
ksp "androidx.room:room-compiler:$room_version"
implementation "androidx.room:room-paging:$room_version"
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01'
implementation 'com.drakeet.drawer:drawer:1.0.3'
implementation 'com.github.chrisbanes:PhotoView:2.3.0' // Dead Dependency
implementation 'com.github.tachiyomiorg:DirectionalViewPager:1.0.0' // Dead Dependency
// https://github.com/google/accompanist/releases
implementation "com.google.accompanist:accompanist-themeadapter-material3:0.28.0"
implementation 'com.google.android.material:material:1.8.0-rc01'
// https://square.github.io/okhttp/changelogs/changelog/
implementation 'com.squareup.okhttp3:okhttp-bom:5.0.0-alpha.11'
implementation 'com.squareup.okhttp3:okhttp-coroutines'
implementation 'com.squareup.okio:okio-jvm:3.3.0'
implementation 'dev.chrisbanes.insetter:insetter:0.6.1' // Dead Dependency
implementation 'dev.rikka.rikkax.core:core-ktx:1.4.1'
implementation 'dev.rikka.rikkax.insets:insets:1.3.0'
implementation 'dev.rikka.rikkax.layoutinflater:layoutinflater:1.2.0'
implementation 'dev.rikka.rikkax.preference:simplemenu-preference:1.0.3'
implementation 'dev.rikka.rikkax.material:material-preference:2.0.0'
// https://coil-kt.github.io/coil/changelog/
// implementation 'io.coil-kt:coil:2.2.2'
implementation 'com.github.Ehviewer-Overhauled.coil:coil:d65881d'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'
implementation 'org.jsoup:jsoup:1.15.3'
}
configurations.all {
exclude group: 'dev.rikka.rikkax.appcompat', module: 'appcompat'
exclude group: 'dev.rikka.rikkax.material', module: 'material'
exclude group: 'org.jetbrains.kotlin', module: 'kotlin-android-extensions-runtime'
exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-jdk7'
exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-jdk8'
}
+224
View File
@@ -0,0 +1,224 @@
import java.io.ByteArrayOutputStream
import java.time.Instant
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
plugins {
id("com.android.application")
kotlin("android")
id("kotlin-parcelize")
kotlin("plugin.serialization")
id("com.google.devtools.ksp")
}
android {
compileSdk = 33
buildToolsVersion = "33.0.2"
ndkVersion = "25.2.9519653"
splits {
abi {
isEnable = true
reset()
include("arm64-v8a", "x86_64", "armeabi-v7a", "x86")
isUniversalApk = true
}
}
val signConfig = signingConfigs.create("release") {
storeFile = File(projectDir.path + "/keystore/androidkey.jks")
storePassword = "000000"
keyAlias = "key0"
keyPassword = "000000"
enableV3Signing = true
enableV4Signing = true
}
val commitSha by lazy {
val stdout = ByteArrayOutputStream()
exec {
commandLine = "git rev-parse --short=7 HEAD".split(' ')
standardOutput = stdout
}
stdout.toString().trim()
}
val buildTime by lazy {
val formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm").withZone(ZoneOffset.UTC)
formatter.format(Instant.now())
}
defaultConfig {
applicationId = "moe.tarsin.ehviewer"
minSdk = 28
targetSdk = 33
versionCode = 180035
versionName = "1.8.7.0"
resourceConfigurations.addAll(
listOf(
"zh",
"zh-rCN",
"zh-rHK",
"zh-rTW",
"es",
"ja",
"ko",
"fr",
"de",
"th",
"tr",
"nb-rNO"
)
)
buildConfigField("String", "COMMIT_SHA", "\"$commitSha\"")
buildConfigField("String", "BUILD_TIME", "\"$buildTime\"")
}
externalNativeBuild {
cmake {
path = File("src/main/cpp/CMakeLists.txt")
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_19
targetCompatibility = JavaVersion.VERSION_19
}
kotlinOptions {
jvmTarget = "19"
freeCompilerArgs = listOf(
// https://kotlinlang.org/docs/compiler-reference.html#progressive
"-progressive",
"-opt-in=coil.annotation.ExperimentalCoilApi",
"-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi",
"-opt-in=androidx.compose.material3.ExperimentalMaterial3Api",
"-opt-in=androidx.compose.ui.ExperimentalComposeUiApi",
"-opt-in=androidx.compose.foundation.ExperimentalFoundationApi",
"-opt-in=androidx.compose.animation.ExperimentalAnimationApi",
"-opt-in=kotlin.contracts.ExperimentalContracts",
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
"-opt-in=kotlinx.coroutines.FlowPreview",
"-opt-in=kotlinx.coroutines.InternalCoroutinesApi",
"-opt-in=kotlinx.serialization.ExperimentalSerializationApi",
)
}
lint {
abortOnError = true
checkReleaseBuilds = false
disable.add("MissingTranslation")
}
packaging {
resources {
excludes += "/META-INF/CHANGES"
excludes += "/META-INF/README.md"
excludes += "/kotlin/**"
excludes += "**.txt"
excludes += "**.bin"
}
}
dependenciesInfo.includeInApk = false
buildTypes {
release {
isMinifyEnabled = true
isShrinkResources = true
proguardFiles("proguard-rules.pro")
signingConfig = signConfig
}
debug {
applicationIdSuffix = ".debug"
}
}
buildFeatures {
buildConfig = true
compose = true
viewBinding = true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.4.3"
}
namespace = "com.hippo.ehviewer"
}
dependencies {
// https://developer.android.com/jetpack/androidx/releases/activity
implementation("androidx.activity:activity-compose:1.6.1")
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("androidx.biometric:biometric-ktx:1.2.0-alpha05")
implementation("androidx.browser:browser:1.5.0")
implementation("androidx.collection:collection-ktx:1.3.0-alpha02")
// 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.core:core-ktx:1.10.0-beta01")
implementation("androidx.constraintlayout:constraintlayout:2.2.0-alpha07")
implementation("androidx.coordinatorlayout:coordinatorlayout:1.2.0")
// https://developer.android.com/jetpack/androidx/releases/lifecycle
implementation("androidx.lifecycle:lifecycle-process:2.5.1")
// https://developer.android.com/jetpack/androidx/releases/navigation
val nav_version = "2.5.3"
implementation("androidx.navigation:navigation-fragment-ktx:$nav_version")
implementation("androidx.navigation:navigation-ui-ktx:$nav_version")
implementation("androidx.navigation:navigation-compose:$nav_version")
// https://developer.android.com/jetpack/androidx/releases/paging
implementation("androidx.paging:paging-runtime-ktx:3.2.0-alpha04")
implementation("androidx.paging:paging-compose:1.0.0-alpha18")
implementation("androidx.preference:preference-ktx:1.2.0")
implementation("androidx.recyclerview:recyclerview:1.3.0-rc01")
// https://developer.android.com/jetpack/androidx/releases/room
val room_version = "2.5.0"
ksp("androidx.room:room-compiler:$room_version")
implementation("androidx.room:room-paging:$room_version")
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01")
implementation("androidx.work:work-runtime-ktx:2.8.0")
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")
// https://square.github.io/okhttp/changelogs/changelog/
implementation("com.squareup.okhttp3:okhttp-bom:5.0.0-alpha.11")
implementation("com.squareup.okhttp3:okhttp-coroutines")
implementation("com.squareup.okio:okio-jvm:3.3.0")
implementation("dev.chrisbanes.insetter:insetter:0.6.1") // Dead Dependency
implementation("dev.rikka.rikkax.core:core-ktx:1.4.1")
implementation("dev.rikka.rikkax.insets:insets:1.3.0")
implementation("dev.rikka.rikkax.layoutinflater:layoutinflater:1.2.0")
implementation("dev.rikka.rikkax.preference:simplemenu-preference:1.0.3")
implementation("dev.rikka.rikkax.material:material-preference:2.0.0")
// https://coil-kt.github.io/coil/changelog/
implementation("io.coil-kt:coil-compose:2.2.2")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-cbor:1.5.0")
implementation("org.jsoup:jsoup:1.15.4")
}
configurations.all {
exclude("dev.rikka.rikkax.appcompat", "appcompat")
exclude("dev.rikka.rikkax.material", "material")
exclude("org.jetbrains.kotlin", "kotlin-android-extensions-runtime")
exclude("org.jetbrains.kotlin", "kotlin-stdlib-jdk7")
exclude("org.jetbrains.kotlin", "kotlin-stdlib-jdk8")
}
+8
View File
@@ -6,8 +6,16 @@
native <methods>;
}
# https://issuetracker.google.com/issues/190382641
-keepclassmembers class kotlin.SafePublicationLazyImpl {
java.lang.Object _value;
}
-keep class androidx.viewpager.widget.ViewPager$LayoutParams { int position; }
# Ktor logger
-dontwarn org.slf4j.impl.StaticLoggerBinder
-keepattributes LineNumberTable,SourceFile
-renamesourcefileattribute SourceFile
+22
View File
@@ -0,0 +1,22 @@
<?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.
-->
<resources>
<string name="app_name" translatable="false">EhViewer Debug</string>
</resources>
+2 -2
View File
@@ -34,9 +34,9 @@
android:theme="@style/AppTheme"
tools:targetApi="tiramisu">
<activity
android:name=".ui.LoginActivity"
android:name=".ui.login.LoginActivity"
android:exported="false"
android:theme="@style/AppTheme"
android:launchMode="singleTask"
android:windowSoftInputMode="adjustResize"/>
<activity
android:name=".ui.MainActivity"
@@ -0,0 +1,34 @@
From fc9fb0ea245259e346ab46c334f03b557fe63b6b Mon Sep 17 00:00:00 2001
From: tarsin <yuanqingxiang233@163.com>
Date: Tue, 22 Nov 2022 12:37:12 +0800
Subject: [PATCH] Don't build other binarys
---
CMakeLists.txt | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 01c73cd..b461e68 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -592,7 +592,7 @@ check_symbol_exists(getopt_long getopt.h HAVE_GETOPT_LONG)
# xzdec
#############################################################################
-if(HAVE_GETOPT_LONG)
+if(0)
add_executable(xzdec
src/common/sysdefs.h
src/common/tuklib_common.h
@@ -630,7 +630,7 @@ endif()
# xz
#############################################################################
-if(NOT MSVC AND HAVE_GETOPT_LONG)
+if(0)
add_executable(xz
src/common/mythread.h
src/common/sysdefs.h
--
2.39.2
@@ -0,0 +1,38 @@
From 2a67861e6f06794f3671c4988bac12330d8422e3 Mon Sep 17 00:00:00 2001
From: tarsin <yuanqingxiang233@163.com>
Date: Fri, 27 May 2022 23:21:20 +0800
Subject: [PATCH 1/2] Don't build shared lib
---
libarchive/CMakeLists.txt | 8 +-------
1 file changed, 1 insertion(+), 7 deletions(-)
diff --git a/libarchive/CMakeLists.txt b/libarchive/CMakeLists.txt
index ff7ade00..00c2e170 100644
--- a/libarchive/CMakeLists.txt
+++ b/libarchive/CMakeLists.txt
@@ -242,12 +242,6 @@ ELSEIF(ARCHIVE_ACL_SUNOS)
LIST(APPEND libarchive_SOURCES archive_disk_acl_sunos.c)
ENDIF()
-# Libarchive is a shared library
-ADD_LIBRARY(archive SHARED ${libarchive_SOURCES} ${include_HEADERS})
-TARGET_INCLUDE_DIRECTORIES(archive PUBLIC .)
-TARGET_LINK_LIBRARIES(archive ${ADDITIONAL_LIBS})
-SET_TARGET_PROPERTIES(archive PROPERTIES SOVERSION ${SOVERSION})
-
# archive_static is a static library
ADD_LIBRARY(archive_static STATIC ${libarchive_SOURCES} ${include_HEADERS})
TARGET_LINK_LIBRARIES(archive_static ${ADDITIONAL_LIBS})
@@ -260,7 +254,7 @@ ENDIF(NOT WIN32 OR CYGWIN)
IF(ENABLE_INSTALL)
# How to install the libraries
- INSTALL(TARGETS archive archive_static
+ INSTALL(TARGETS archive_static
RUNTIME DESTINATION bin
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib)
--
2.39.2
@@ -0,0 +1,25 @@
From c9298fd6dc0a8b04dd4b2247118b9cb895c46e58 Mon Sep 17 00:00:00 2001
From: tarsin <yuanqingxiang233@163.com>
Date: Sat, 23 Jul 2022 16:05:36 +0800
Subject: [PATCH 2/2] Insert link libs
---
CMakeLists.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 713e3bc5..7247640d 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -415,7 +415,7 @@ IF(DEFINED __GNUWIN32PATH AND EXISTS "${__GNUWIN32PATH}")
# # endif
ENDIF(DEFINED __GNUWIN32PATH AND EXISTS "${__GNUWIN32PATH}")
-SET(ADDITIONAL_LIBS "")
+SET(ADDITIONAL_LIBS ${LIBARCHIVE_CUSTOM_LIBS})
#
# Find ZLIB
#
--
2.39.2
+30 -13
View File
@@ -1,7 +1,9 @@
project(ehviewer)
cmake_minimum_required(VERSION 3.4.1)
cmake_minimum_required(VERSION 3.14)
project(ehviewer C)
include(FetchContent)
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Ofast -fvisibility=hidden -fvisibility-inlines-hidden -ffunction-sections -fdata-sections -funroll-loops -flto=thin \
if (NOT CMAKE_BUILD_TYPE STREQUAL "Debug")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Ofast -fvisibility=hidden -fvisibility-inlines-hidden -funroll-loops -flto=thin \
-mllvm -polly \
-mllvm -polly-run-dce \
-mllvm -polly-run-inliner \
@@ -12,6 +14,20 @@ set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Ofast -fvisibility=hidden -fvisibility-inli
-mllvm -polly-vectorizer=stripmine \
-mllvm -polly-detect-profitability-min-per-loop-insts=40 \
-mllvm -polly-invariant-load-hoisting")
endif (NOT CMAKE_BUILD_TYPE STREQUAL "Debug")
# Change the default path of fetch content to avoid downloading the same dependencies per architecture
get_filename_component(deps "../_deps" REALPATH BASE_DIR "${CMAKE_BINARY_DIR}")
set(FETCHCONTENT_BASE_DIR ${deps})
FetchContent_Declare(liblzma
GIT_REPOSITORY https://git.tukaani.org/xz.git
GIT_TAG v5.4.1
PATCH_COMMAND git apply ${CMAKE_CURRENT_LIST_DIR}/0001-Don-t-build-other-binarys.patch || true
)
FetchContent_MakeAvailable(liblzma)
include_directories(${liblzma_SOURCE_DIR}/src/liblzma/api)
# Build GNUTLS libnettle
add_subdirectory(nettle)
@@ -27,11 +43,7 @@ set(HAVE_NETTLE_PBKDF2_H 1)
set(HAVE_NETTLE_RIPEMD160_H 1)
set(HAVE_NETTLE_SHA_H 1)
# Build liblzma
add_subdirectory(liblzma)
# Configure lzma support for libarchive
include_directories(liblzma/src/liblzma/api)
SET(HAVE_LIBLZMA 1)
SET(HAVE_LZMA_H 1)
SET(HAVE_LZMA_STREAM_ENCODER_MT 1)
@@ -39,12 +51,17 @@ SET(HAVE_LZMADEC_H 1)
SET(HAVE_LIBLZMADEC 1)
# Configure libarchive link's static lib
SET(LIBARCHIVE_CUSTOM_LIBS "liblzma" "nettle")
SET(LIBARCHIVE_CUSTOM_LIBS "nettle" "liblzma")
# Build libarchive
add_subdirectory(libarchive)
FetchContent_Declare(libarchive
GIT_REPOSITORY https://github.com/libarchive/libarchive
GIT_TAG v3.6.2
PATCH_COMMAND git apply ${CMAKE_CURRENT_LIST_DIR}/0001-Don-t-build-shared-lib.patch ${CMAKE_CURRENT_LIST_DIR}/0002-Insert-link-libs.patch || true
)
FetchContent_MakeAvailable(libarchive)
include_directories(${libarchive_SOURCE_DIR}/libarchive)
# Build and link our app's native lib
add_library(${PROJECT_NAME} SHARED archive.c fdutils.c gifutils.c)
target_include_directories(${PROJECT_NAME} PUBLIC libarchive/libarchive)
target_link_libraries(${PROJECT_NAME} archive_static log -Wl,--gc-sections)
add_library(${PROJECT_NAME} SHARED archive.c fdutils.c gifutils.c natsort/strnatcmp.c)
target_link_libraries(${PROJECT_NAME} archive_static log)
+95 -33
View File
@@ -17,14 +17,12 @@
* EhViewer. If not, see <https://www.gnu.org/licenses/>.
*/
#include <assert.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <pthread.h>
#include <sys/mman.h>
#include <syscall.h>
#include <jni.h>
#include <android/log.h>
@@ -34,6 +32,7 @@
#define LOG_TAG "libarchive_wrapper"
#include "natsort/strnatcmp.h"
#include "ehviewer.h"
typedef struct {
@@ -46,12 +45,25 @@ typedef struct {
typedef struct {
const char *filename;
int index;
size_t size;
} entry;
pthread_mutex_t ctx_lock;
static archive_ctx **ctx_pool;
#define CTX_POOL_SIZE 20
static void *mempool = NULL;
static size_t *mempoolofs = NULL;
#define PAGE_ALIGN(x) ((x + ~PAGE_MASK) & PAGE_MASK)
#define MEMPOOL_ADDR_BY_SORTED_IDX(x) (mempool + (index ? mempoolofs[index - 1] : 0))
#define MEMPOOL_SIZE (mempoolofs[entryCount - 1])
#define MEMPOOL_ENTITY_SIZE(x) PAGE_ALIGN(entries[x].size)
#define PROT_RW (PROT_WRITE | PROT_READ)
#define MAP_ANON_POOL (MAP_ANONYMOUS | MAP_NORESERVE | MAP_PRIVATE | MAP_UNINITIALIZED)
static bool need_encrypt = false;
static char *passwd = NULL;
static void *archiveAddr = NULL;
@@ -59,8 +71,6 @@ static size_t archiveSize = 0;
static entry *entries = NULL;
static size_t entryCount = 0;
#define POPULATE_THRESHOLD (512 * 1024 * 1024) // 512 MiB
const char supportExt[9][6] = {
"jpeg",
"jpg",
@@ -87,9 +97,7 @@ static inline int filename_is_playable_file(const char *name) {
static inline int compare_entries(const void *a, const void *b) {
const char *fa = ((entry *) a)->filename;
const char *fb = ((entry *) b)->filename;
int a_len = (int) strlen(fa);
int b_len = (int) strlen(fb);
return a_len == b_len ? strcmp(fa, fb) : a_len - b_len;
return strnatcmp(fa, fb);
}
static long archive_map_entries_index(archive_ctx *ctx) {
@@ -99,6 +107,7 @@ static long archive_map_entries_index(archive_ctx *ctx) {
if (filename_is_playable_file(name)) {
entries[count].filename = strdup(name);
entries[count].index = count;
entries[count].size = archive_entry_size(ctx->entry);
count++;
}
}
@@ -108,6 +117,48 @@ static long archive_map_entries_index(archive_ctx *ctx) {
return count;
}
static bool archive_prealloc_mempool() {
mempoolofs = calloc(entryCount, sizeof(size_t));
for (int i = 0; i < entryCount; ++i) {
if (!i)
mempoolofs[i] = PAGE_ALIGN(entries[i].size);
else
mempoolofs[i] = PAGE_ALIGN(entries[i].size) + mempoolofs[i - 1];
}
mempool = mmap(0, MEMPOOL_SIZE, PROT_RW, MAP_ANON_POOL, -1, 0);
if (mempool == MAP_FAILED) {
LOGE("%s%s", "mmap failed with error ", strerror(errno));
return false;
}
return true;
}
// Mincore does not support anon mapping yet
// Now that we use MADV_FREE, how can we detect whether pages is reclaimed?
static bool is_mempool_pages_present_and_lock(int index) {
void *addr = MEMPOOL_ADDR_BY_SORTED_IDX(index);
size_t size = MEMPOOL_ENTITY_SIZE(index);
mlock(addr, size);
#if 0
unsigned char vec[size / PAGE_SIZE];
unsigned char r = 1;
mincore(addr, size, vec);
for (int i = 0; i < size / PAGE_SIZE; ++i) r &= vec[i];
return r;
#endif
return false;
}
#define MEMPOOL_IN_CORE_AND_LOCK(x) is_mempool_pages_present_and_lock(x)
static void mempool_release_pages(void *addr, size_t size) {
size = PAGE_ALIGN(size);
munlock(addr, size);
int ret = madvise(addr, size, MADV_FREE);
if (ret == -EINVAL) madvise(addr, size, MADV_DONTNEED);
}
static long archive_list_all_entries(archive_ctx *ctx) {
long count = 0;
while (archive_read_next_header(ctx->arc, &ctx->entry) == ARCHIVE_OK)
@@ -158,7 +209,6 @@ static int archive_alloc_ctx(archive_ctx **ctxptr) {
}
static int archive_skip_to_index(archive_ctx *ctx, int index) {
assert(!(ctx->next_index > index));
while (archive_read_next_header(ctx->arc, &ctx->entry) == ARCHIVE_OK) {
if (!filename_is_playable_file(archive_entry_pathname(ctx->entry)))
continue;
@@ -233,8 +283,6 @@ Java_com_hippo_UriArchiveAccessor_openArchive(JNIEnv *env, jobject thiz, jint fd
EH_UNUSED(thiz);
archive_ctx *ctx = NULL;
int mmap_flags = MAP_PRIVATE;
if (size <= POPULATE_THRESHOLD)
mmap_flags |= MAP_POPULATE;
archiveAddr = mmap64(0, size, PROT_READ, mmap_flags, fd, 0);
if (archiveAddr == MAP_FAILED) {
LOGE("%s%s", "mmap64 failed with error ", strerror(errno));
@@ -255,7 +303,7 @@ Java_com_hippo_UriArchiveAccessor_openArchive(JNIEnv *env, jobject thiz, jint fd
LOGE("%s%s", "Archive open failed:", archive_error_string(ctx->arc));
} else {
entryCount = archive_list_all_entries(ctx);
LOGI("%s%ld%s", "Found ", r, " image entries in archive");
LOGI("%s%zu%s", "Found ", entryCount, " image entries in archive");
// We must read through the file|vm then we can know whether it is encrypted
int encryptRet = archive_read_has_encrypted_entries(ctx->arc);
@@ -292,41 +340,52 @@ Java_com_hippo_UriArchiveAccessor_openArchive(JNIEnv *env, jobject thiz, jint fd
} else {
entries = calloc(entryCount, sizeof(entry));
r = archive_map_entries_index(ctx);
if (!archive_prealloc_mempool()) {
r = 0;
for (int i = 0; i < entryCount; ++i) {
free((void *) entries[i].filename);
}
free(entries);
}
}
archive_release_ctx(ctx);
return r;
}
#pragma clang diagnostic push
#pragma ide diagnostic ignored "ConstantConditionsOC"
#pragma ide diagnostic ignored "UnreachableCode"
JNIEXPORT jobject JNICALL
Java_com_hippo_UriArchiveAccessor_extractToByteBuffer(JNIEnv *env, jobject thiz, jint index) {
EH_UNUSED(env);
EH_UNUSED(thiz);
index = entries[index].index;
archive_ctx *ctx = NULL;
int ret;
ret = archive_get_ctx(&ctx, index);
if (ret)
return 0;
long long size = archive_entry_size(ctx->entry);
void *buffer = malloc(size);
if (!buffer) {
void *addr = MEMPOOL_ADDR_BY_SORTED_IDX(index);
size_t size = entries[index].size;
if (!MEMPOOL_IN_CORE_AND_LOCK(index)) {
index = entries[index].index;
archive_ctx *ctx = NULL;
int ret;
ret = archive_get_ctx(&ctx, index);
if (ret) return 0;
ret = archive_read_data(ctx->arc, addr, size);
ctx->using = 0;
LOGE("Allocate buffer for decompression failed:ENOMEM");
return 0;
if (ret == size)
return (*env)->NewDirectByteBuffer(env, addr, size);
if (ret != size)
LOGE("%s", "No enough data read, WTF?");
if (ret < 0)
LOGE("%s%s", "Archive read failed:", archive_error_string(ctx->arc));
mempool_release_pages(addr, size);
} else {
return (*env)->NewDirectByteBuffer(env, addr, size);
}
ret = archive_read_data(ctx->arc, buffer, size);
ctx->using = 0;
if (ret == size)
return (*env)->NewDirectByteBuffer(env, buffer, size);
if (ret != size)
LOGE("%s", "No enough data read, WTF?");
if (ret < 0)
LOGE("%s%s", "Archive read failed:", archive_error_string(ctx->arc));
free(buffer);
return 0;
}
#pragma clang diagnostic pop
JNIEXPORT void JNICALL
Java_com_hippo_UriArchiveAccessor_closeArchive(JNIEnv *env, jobject thiz) {
EH_UNUSED(env);
@@ -339,6 +398,8 @@ Java_com_hippo_UriArchiveAccessor_closeArchive(JNIEnv *env, jobject thiz) {
passwd = NULL;
need_encrypt = false;
munmap(archiveAddr, archiveSize);
munmap(mempool, MEMPOOL_SIZE);
free(mempoolofs);
for (int i = 0; i < entryCount; ++i) {
free((void *) entries[i].filename);
}
@@ -415,5 +476,6 @@ JNIEXPORT void JNICALL
Java_com_hippo_UriArchiveAccessor_releaseByteBuffer(JNIEnv *env, jobject thiz, jobject buffer) {
EH_UNUSED(thiz);
void *addr = (*env)->GetDirectBufferAddress(env, buffer);
free(addr);
}
size_t size = (*env)->GetDirectBufferCapacity(env, buffer);
mempool_release_pages(addr, size);
}
+8 -15
View File
@@ -1,5 +1,7 @@
#include <jni.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/sendfile.h>
#include <android/log.h>
#include <string.h>
@@ -9,6 +11,8 @@
#include "ehviewer.h"
// TODO: Replace it with AFileDescriptor_getFd when minsdk 31
// https://developer.android.com/ndk/reference/group/file-descriptor
JNIEXPORT jint JNICALL
Java_com_hippo_Native_getFd(JNIEnv *env, jclass clazz, jobject fileDescriptor) {
jint fd = -1;
@@ -24,20 +28,9 @@ 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);
Java_com_hippo_Native_sendfile(JNIEnv *env, jclass clazz, jint from, jint to) {
struct stat st;
fstat(from, &st);
sendfile(to, from, 0, st.st_size);
}
+177
View File
@@ -0,0 +1,177 @@
/* -*- mode: c; c-file-style: "k&r" -*-
strnatcmp.c -- Perform 'natural order' comparisons of strings in C.
Copyright (C) 2000, 2004 by Martin Pool <mbp sourcefrog net>
This software is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any damages
arising from the use of this software.
Permission is granted to anyone to use this software for any purpose,
including commercial applications, and to alter it and redistribute it
freely, subject to the following restrictions:
1. The origin of this software must not be misrepresented; you must not
claim that you wrote the original software. If you use this software
in a product, an acknowledgment in the product documentation would be
appreciated but is not required.
2. Altered source versions must be plainly marked as such, and must not be
misrepresented as being the original software.
3. This notice may not be removed or altered from any source distribution.
*/
/* partial change history:
*
* 2004-10-10 mbp: Lift out character type dependencies into macros.
*
* Eric Sosman pointed out that ctype functions take a parameter whose
* value must be that of an unsigned int, even on platforms that have
* negative chars in their default char type.
*/
#include <stddef.h> /* size_t */
#include <ctype.h>
#include "strnatcmp.h"
/* These are defined as macros to make it easier to adapt this code to
* different characters types or comparison functions. */
static inline int
nat_isdigit(nat_char a)
{
return isdigit((unsigned char) a);
}
static inline int
nat_isspace(nat_char a)
{
return isspace((unsigned char) a);
}
static inline nat_char
nat_toupper(nat_char a)
{
return toupper((unsigned char) a);
}
static int
compare_right(nat_char const *a, nat_char const *b)
{
int bias = 0;
/* The longest run of digits wins. That aside, the greatest
value wins, but we can't know that it will until we've scanned
both numbers to know that they have the same magnitude, so we
remember it in BIAS. */
for (;; a++, b++) {
if (!nat_isdigit(*a) && !nat_isdigit(*b))
return bias;
if (!nat_isdigit(*a))
return -1;
if (!nat_isdigit(*b))
return +1;
if (*a < *b) {
if (!bias)
bias = -1;
} else if (*a > *b) {
if (!bias)
bias = +1;
} else if (!*a && !*b)
return bias;
}
return 0;
}
static int
compare_left(nat_char const *a, nat_char const *b)
{
/* Compare two left-aligned numbers: the first to have a
different value wins. */
for (;; a++, b++) {
if (!nat_isdigit(*a) && !nat_isdigit(*b))
return 0;
if (!nat_isdigit(*a))
return -1;
if (!nat_isdigit(*b))
return +1;
if (*a < *b)
return -1;
if (*a > *b)
return +1;
}
return 0;
}
static int
strnatcmp0(nat_char const *a, nat_char const *b, int fold_case)
{
int ai, bi;
nat_char ca, cb;
int fractional, result;
ai = bi = 0;
while (1) {
ca = a[ai]; cb = b[bi];
/* skip over leading spaces or zeros */
while (nat_isspace(ca))
ca = a[++ai];
while (nat_isspace(cb))
cb = b[++bi];
/* process run of digits */
if (nat_isdigit(ca) && nat_isdigit(cb)) {
fractional = (ca == '0' || cb == '0');
if (fractional) {
if ((result = compare_left(a+ai, b+bi)) != 0)
return result;
} else {
if ((result = compare_right(a+ai, b+bi)) != 0)
return result;
}
}
if (!ca && !cb) {
/* The strings compare the same. Perhaps the caller
will want to call strcmp to break the tie. */
return 0;
}
if (fold_case) {
ca = nat_toupper(ca);
cb = nat_toupper(cb);
}
if (ca < cb)
return -1;
if (ca > cb)
return +1;
++ai; ++bi;
}
}
int
strnatcmp(nat_char const *a, nat_char const *b) {
return strnatcmp0(a, b, 0);
}
/* Compare, recognizing numeric string and ignoring case. */
int
strnatcasecmp(nat_char const *a, nat_char const *b) {
return strnatcmp0(a, b, 1);
}
+38
View File
@@ -0,0 +1,38 @@
/* -*- mode: c; c-file-style: "k&r" -*-
strnatcmp.c -- Perform 'natural order' comparisons of strings in C.
Copyright (C) 2000, 2004 by Martin Pool <mbp sourcefrog net>
This software is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any damages
arising from the use of this software.
Permission is granted to anyone to use this software for any purpose,
including commercial applications, and to alter it and redistribute it
freely, subject to the following restrictions:
1. The origin of this software must not be misrepresented; you must not
claim that you wrote the original software. If you use this software
in a product, an acknowledgment in the product documentation would be
appreciated but is not required.
2. Altered source versions must be plainly marked as such, and must not be
misrepresented as being the original software.
3. This notice may not be removed or altered from any source distribution.
*/
#ifdef __cplusplus
extern "C" {
#endif
/* CUSTOMIZATION SECTION
*
* You can change this typedef, but must then also change the inline
* functions in strnatcmp.c */
typedef char nat_char;
int strnatcmp(nat_char const *a, nat_char const *b);
int strnatcasecmp(nat_char const *a, nat_char const *b);
#ifdef __cplusplus
}
#endif
@@ -1,17 +1,15 @@
package androidx.navigation.ui
import android.view.Menu
import android.view.MenuItem
import androidx.annotation.IdRes
import androidx.navigation.ActivityNavigator
import androidx.navigation.NavController
import androidx.navigation.NavDestination
import androidx.navigation.NavDestination.Companion.hierarchy
import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.NavOptions
fun onNavDestinationSelected2(item: MenuItem, navController: NavController): Boolean {
if (navController.currentDestination?.matchDestination(item.itemId) == true) return false
if (navController.currentDestination?.matchDestination(item.itemId) == true) return true
val builder = NavOptions.Builder().setLaunchSingleTop(true)
if (
navController.currentDestination!!.parent!!.findNode(item.itemId)
@@ -27,13 +25,6 @@ fun onNavDestinationSelected2(item: MenuItem, navController: NavController): Boo
.setPopEnterAnim(com.hippo.ehviewer.R.anim.scene_close_enter)
.setPopExitAnim(com.hippo.ehviewer.R.anim.scene_close_exit)
}
if (item.order and Menu.CATEGORY_SECONDARY == 0) {
builder.setPopUpTo(
navController.graph.findStartDestination().id,
inclusive = false,
saveState = false
)
}
val options = builder.build()
return try {
// TODO provide proper API instead of using Exceptions as Control-Flow.
@@ -0,0 +1,66 @@
@file:JvmName("GifDecodeUtils")
package coil.decode
import kotlin.experimental.and
import okio.BufferedSource
import okio.ByteString.Companion.encodeUtf8
// https://www.matthewflickinger.com/lab/whatsinagif/bits_and_bytes.asp
private val GIF_HEADER_87A = "GIF87a".encodeUtf8()
private val GIF_HEADER_89A = "GIF89a".encodeUtf8()
// https://developers.google.com/speed/webp/docs/riff_container
private val WEBP_HEADER_RIFF = "RIFF".encodeUtf8()
private val WEBP_HEADER_WEBP = "WEBP".encodeUtf8()
private val WEBP_HEADER_VPX8 = "VP8X".encodeUtf8()
// https://nokiatech.github.io/heif/technical.html
private val HEIF_HEADER_FTYP = "ftyp".encodeUtf8()
private val HEIF_HEADER_MSF1 = "msf1".encodeUtf8()
private val HEIF_HEADER_HEVC = "hevc".encodeUtf8()
private val HEIF_HEADER_HEVX = "hevx".encodeUtf8()
/**
* Return 'true' if the [source] contains a GIF image. The [source] is not consumed.
*/
fun DecodeUtils.isGif(source: BufferedSource): Boolean {
return source.rangeEquals(0, GIF_HEADER_89A) ||
source.rangeEquals(0, GIF_HEADER_87A)
}
/**
* Return 'true' if the [source] contains a WebP image. The [source] is not consumed.
*/
fun DecodeUtils.isWebP(source: BufferedSource): Boolean {
return source.rangeEquals(0, WEBP_HEADER_RIFF) &&
source.rangeEquals(8, WEBP_HEADER_WEBP)
}
/**
* Return 'true' if the [source] contains an animated WebP image. The [source] is not consumed.
*/
fun DecodeUtils.isAnimatedWebP(source: BufferedSource): Boolean {
return isWebP(source) &&
source.rangeEquals(12, WEBP_HEADER_VPX8) &&
source.request(17) &&
(source.buffer[16] and 0b00000010) > 0
}
/**
* Return 'true' if the [source] contains an HEIF image. The [source] is not consumed.
*/
fun DecodeUtils.isHeif(source: BufferedSource): Boolean {
return source.rangeEquals(4, HEIF_HEADER_FTYP)
}
/**
* Return 'true' if the [source] contains an animated HEIF image sequence. The [source] is not
* consumed.
*/
fun DecodeUtils.isAnimatedHeif(source: BufferedSource): Boolean {
return isHeif(source) &&
(source.rangeEquals(8, HEIF_HEADER_MSF1) ||
source.rangeEquals(8, HEIF_HEADER_HEVC) ||
source.rangeEquals(8, HEIF_HEADER_HEVX))
}
@@ -0,0 +1,88 @@
@file:Suppress("SameParameterValue")
package coil.decode
import okio.Buffer
import okio.ByteString
import okio.ByteString.Companion.decodeHex
import okio.ForwardingSource
import okio.Source
/**
* A [ForwardingSource] that rewrites the GIF frame delay in every graphics control block if it's
* below a threshold.
*/
internal class FrameDelayRewritingSource(delegate: Source) : ForwardingSource(delegate) {
// An intermediary buffer so we can read and alter the data before it's written to the destination.
private val buffer = Buffer()
override fun read(sink: Buffer, byteCount: Long): Long {
// Ensure our buffer has enough bytes to satisfy this read.
request(byteCount)
// Short circuit if there are no bytes in the buffer.
if (buffer.size == 0L) {
return if (byteCount == 0L) 0L else -1L
}
// Search through the buffer and rewrite any frame delays below the threshold.
var bytesWritten = 0L
while (true) {
val index = indexOf(FRAME_DELAY_START_MARKER)
if (index == -1L) break
// Write up until the end of the frame delay start marker.
bytesWritten += write(sink, index + FRAME_DELAY_START_MARKER_SIZE_BYTES)
// Check for the end of the graphics control extension block.
if (!request(5) || buffer[4] != 0.toByte()) continue
// Rewrite the frame delay if it is below the threshold.
// The frame delay is stored as two unsigned bytes in reverse order
// (i.e. the most significant bits are in the byte that's read second).
val frameDelay = (buffer[2].toUByte().toInt() shl 8) or buffer[1].toUByte().toInt()
if (frameDelay < MINIMUM_FRAME_DELAY) {
sink.writeByte(buffer[0].toInt())
sink.writeByte(DEFAULT_FRAME_DELAY)
sink.writeByte(0)
buffer.skip(3)
}
}
// Write anything left in the source.
if (bytesWritten < byteCount) {
bytesWritten += write(sink, byteCount - bytesWritten)
}
return if (bytesWritten == 0L) -1 else bytesWritten
}
private fun indexOf(bytes: ByteString): Long {
var index = -1L
while (true) {
index = buffer.indexOf(bytes[0], index + 1)
if (index == -1L) break
if (request(bytes.size.toLong()) && buffer.rangeEquals(index, bytes)) break
}
return index
}
private fun write(sink: Buffer, byteCount: Long): Long {
return buffer.read(sink, byteCount).coerceAtLeast(0)
}
private fun request(byteCount: Long): Boolean {
if (buffer.size >= byteCount) return true
val toRead = byteCount - buffer.size
return super.read(buffer, toRead) == toRead
}
private companion object {
// https://www.matthewflickinger.com/lab/whatsinagif/bits_and_bytes.asp
// See: "Graphics Control Extension"
private val FRAME_DELAY_START_MARKER = "0021F904".decodeHex()
private const val FRAME_DELAY_START_MARKER_SIZE_BYTES = 4
private const val MINIMUM_FRAME_DELAY = 2
private const val DEFAULT_FRAME_DELAY = 10
}
}
@@ -1,15 +1,8 @@
package com.davemorrissey.labs.subscaleview
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Rect
import android.net.Uri
import com.davemorrissey.labs.subscaleview.provider.AssetInputProvider
import com.davemorrissey.labs.subscaleview.provider.InputProvider
import com.davemorrissey.labs.subscaleview.provider.OpenStreamProvider
import com.davemorrissey.labs.subscaleview.provider.ResourceInputProvider
import com.davemorrissey.labs.subscaleview.provider.UriInputProvider
import java.io.InputStream
/**
* Helper class used to set the source and additional attributes from a variety of sources. Supports
@@ -83,38 +76,6 @@ class ImageSource {
}
companion object {
/**
* Create an instance from a resource. The correct resource for the device screen resolution will be used.
*
* @param resId resource ID.
* @return an [ImageSource] instance.
*/
@JvmStatic
fun resource(context: Context, resId: Int): ImageSource {
return ImageSource(ResourceInputProvider(context, resId))
}
/**
* Create an instance from an asset name.
*
* @param assetName asset name.
* @return an [ImageSource] instance.
*/
@JvmStatic
fun asset(context: Context, assetName: String): ImageSource {
return ImageSource(AssetInputProvider(context, assetName))
}
/**
* Create an instance from a URI.
*
* @param uri image URI.
* @return an [ImageSource] instance.
*/
fun uri(context: Context, uri: Uri): ImageSource {
return ImageSource(UriInputProvider(context, uri))
}
/**
* Create an instance from an input provider.
*
@@ -125,16 +86,6 @@ class ImageSource {
return ImageSource(provider)
}
/**
* Create an instance from an input stream.
*
* @param stream open input stream.
* @return an [ImageSource] instance.
*/
fun inputStream(stream: InputStream): ImageSource {
return ImageSource(OpenStreamProvider(stream))
}
/**
* Provide a loaded bitmap for display.
*
@@ -19,12 +19,9 @@ import android.view.MotionEvent
import android.view.View
import androidx.annotation.AnyThread
import androidx.annotation.IntDef
import com.davemorrissey.labs.subscaleview.ImageSource.Companion.asset
import com.davemorrissey.labs.subscaleview.ImageSource.Companion.resource
import com.davemorrissey.labs.subscaleview.decoder.Decoder
import com.davemorrissey.labs.subscaleview.decoder.ImageDecoder
import com.davemorrissey.labs.subscaleview.provider.InputProvider
import com.hippo.ehviewer.R
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@@ -303,42 +300,6 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
setMinimumDpi(160)
setDoubleTapZoomDpi(160)
setGestureDetector(context)
// Handle XML attributes
if (attr != null) {
val typedAttr = getContext().obtainStyledAttributes(attr, R.styleable.SubsamplingScaleImageView)
if (typedAttr.hasValue(R.styleable.SubsamplingScaleImageView_assetName)) {
val assetName = typedAttr.getString(R.styleable.SubsamplingScaleImageView_assetName)
if (assetName != null && assetName.isNotEmpty()) {
setImage(asset(context, assetName))
}
}
if (typedAttr.hasValue(R.styleable.SubsamplingScaleImageView_src)) {
val resId = typedAttr.getResourceId(R.styleable.SubsamplingScaleImageView_src, 0)
if (resId > 0) {
setImage(resource(context, resId))
}
}
if (typedAttr.hasValue(R.styleable.SubsamplingScaleImageView_panEnabled)) {
setPanEnabled(typedAttr.getBoolean(R.styleable.SubsamplingScaleImageView_panEnabled, true))
}
if (typedAttr.hasValue(R.styleable.SubsamplingScaleImageView_zoomEnabled)) {
isZoomEnabled = typedAttr.getBoolean(R.styleable.SubsamplingScaleImageView_zoomEnabled, true)
}
if (typedAttr.hasValue(R.styleable.SubsamplingScaleImageView_quickScaleEnabled)) {
isQuickScaleEnabled =
typedAttr.getBoolean(R.styleable.SubsamplingScaleImageView_quickScaleEnabled, true)
}
if (typedAttr.hasValue(R.styleable.SubsamplingScaleImageView_tileBackgroundColor)) {
setTileBackgroundColor(
typedAttr.getColor(
R.styleable.SubsamplingScaleImageView_tileBackgroundColor,
Color.argb(0, 0, 0, 0)
)
)
}
typedAttr.recycle()
}
quickScaleThreshold =
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 20F, context.resources.displayMetrics)
}
@@ -1953,20 +1914,20 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
val scaleHeight = scale * sHeight
when (panLimit) {
PAN_LIMIT_CENTER -> {
vTarget.top = -(vTranslate!!.y - height / 2).coerceAtLeast(0F)
vTarget.left = -(vTranslate!!.x - width / 2).coerceAtLeast(0F)
vTarget.top = (-(vTranslate!!.y - height / 2)).coerceAtLeast(0F)
vTarget.left = (-(vTranslate!!.x - width / 2)).coerceAtLeast(0F)
vTarget.bottom = (vTranslate!!.y - (height / 2 - scaleHeight)).coerceAtLeast(0F)
vTarget.right = (vTranslate!!.x - (width / 2 - scaleWidth)).coerceAtLeast(0F)
}
PAN_LIMIT_OUTSIDE -> {
vTarget.top = -(vTranslate!!.y - height).coerceAtLeast(0F)
vTarget.left = -(vTranslate!!.x - width).coerceAtLeast(0F)
vTarget.top = (-(vTranslate!!.y - height)).coerceAtLeast(0F)
vTarget.left = (-(vTranslate!!.x - width)).coerceAtLeast(0F)
vTarget.bottom = (vTranslate!!.y + scaleHeight).coerceAtLeast(0F)
vTarget.right = (vTranslate!!.x + scaleWidth).coerceAtLeast(0F)
}
else -> {
vTarget.top = -vTranslate!!.y.coerceAtLeast(0F)
vTarget.left = -vTranslate!!.x.coerceAtLeast(0F)
vTarget.top = (-vTranslate!!.y).coerceAtLeast(0F)
vTarget.left = (-vTranslate!!.x).coerceAtLeast(0F)
vTarget.bottom = (scaleHeight + vTranslate!!.y - height).coerceAtLeast(0F)
vTarget.right = (scaleWidth + vTranslate!!.x - width).coerceAtLeast(0F)
}
@@ -4,10 +4,8 @@ import android.content.Context
import android.graphics.Bitmap
import android.graphics.Point
import android.graphics.Rect
import kotlin.Throws
import com.davemorrissey.labs.subscaleview.provider.InputProvider
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import java.lang.Exception
import com.davemorrissey.labs.subscaleview.provider.InputProvider
class ImageDecoder @JvmOverloads constructor(
private val cropBorders: Boolean,
@@ -1,11 +0,0 @@
package com.davemorrissey.labs.subscaleview.provider
import android.content.Context
import java.io.IOException
class AssetInputProvider(context: Context, val assetName: String) : InputProvider {
private val assets = context.assets
@Throws(IOException::class)
override fun openStream() = assets.open(assetName)
}
@@ -1,10 +1,3 @@
package com.davemorrissey.labs.subscaleview.provider
import java.io.IOException
import java.io.InputStream
import kotlin.Throws
interface InputProvider {
@Throws(IOException::class)
fun openStream(): InputStream?
}
interface InputProvider
@@ -1,16 +0,0 @@
package com.davemorrissey.labs.subscaleview.provider
import java.io.IOException
import java.io.InputStream
import kotlin.Throws
/**
* Provider from an already open input stream.
*
*
* Please keep in mind this provider is not reusable.
*/
class OpenStreamProvider(private val stream: InputStream) : InputProvider {
@Throws(IOException::class)
override fun openStream() = stream
}
@@ -1,11 +0,0 @@
package com.davemorrissey.labs.subscaleview.provider
import android.content.Context
import java.io.IOException
class ResourceInputProvider(context: Context, val resource: Int) : InputProvider {
private val resources = context.resources
@Throws(IOException::class)
override fun openStream() = resources.openRawResource(resource)
}
@@ -1,13 +0,0 @@
package com.davemorrissey.labs.subscaleview.provider
import android.content.Context
import android.net.Uri
import java.io.IOException
import kotlin.Throws
class UriInputProvider(context: Context, val uri: Uri) : InputProvider {
private val resolver = context.contentResolver
@Throws(IOException::class)
override fun openStream() = resolver.openInputStream(uri)
}
+16 -4
View File
@@ -17,8 +17,8 @@
*/
package com.hippo
import android.os.ParcelFileDescriptor
import java.io.FileDescriptor
import java.nio.ByteBuffer
object Native {
fun initialize() {
@@ -29,8 +29,20 @@ object Native {
external fun getFd(fd: FileDescriptor?): Int
@JvmStatic
external fun mapFd(fd: Int, capability: Long): ByteBuffer?
external fun sendfile(from: Int, to: Int)
}
@JvmStatic
external fun unmapDirectByteBuffer(buffer: ByteBuffer)
val FileDescriptor.fd
get() = Native.getFd(this)
infix fun ParcelFileDescriptor.receivefrom(fd: FileDescriptor) {
Native.sendfile(fd.fd, getFd())
}
infix fun ParcelFileDescriptor.copyTo(fd: FileDescriptor) {
Native.sendfile(getFd(), fd.fd)
}
infix fun ParcelFileDescriptor.copyTo(fd: ParcelFileDescriptor) {
Native.sendfile(getFd(), fd.fd)
}
@@ -18,13 +18,16 @@
package com.hippo
import android.content.Context
import android.graphics.ImageDecoder
import android.net.Uri
import android.util.Log
import com.hippo.image.Image
import java.nio.ByteBuffer
class UriArchiveAccessor(ctx: Context, uri: Uri) {
class UriArchiveAccessor(ctx: Context, private val uri: Uri) {
val pfd by lazy { ctx.contentResolver.openFileDescriptor(uri, "r")!! }
fun open(): Int {
Log.d(DEBUG_TAG, "Open archive $uri")
return openArchive(pfd.fd, pfd.statSize)
}
@@ -38,16 +41,18 @@ class UriArchiveAccessor(ctx: Context, uri: Uri) {
fun close() {
closeArchive()
pfd.close()
Log.d(DEBUG_TAG, "Close archive $uri successfully!")
}
fun getImageSource(index: Int): Image.ByteBufferSource? {
fun getImageSource(index: Int): Image.CloseableSource? {
val buffer = extractToByteBuffer(index)
buffer ?: return null
check(buffer.isDirect)
return object : Image.ByteBufferSource {
override fun getByteBuffer(): ByteBuffer {
return buffer
}
Image.rewriteGifSource(buffer)
val source = ImageDecoder.createSource(buffer)
return object : Image.CloseableSource {
override val source: ImageDecoder.Source
get() = source
override fun close() {
releaseByteBuffer(buffer)
@@ -59,4 +64,6 @@ class UriArchiveAccessor(ctx: Context, uri: Uri) {
@JvmStatic
private external fun releaseByteBuffer(buffer: ByteBuffer)
}
}
}
private const val DEBUG_TAG = "UriArchiveAccessor"
@@ -26,9 +26,12 @@ import androidx.appcompat.app.AlertDialog
import com.hippo.ehviewer.R
import com.hippo.yorozuya.ViewUtils
class ListCheckBoxDialogBuilder constructor(
context: Context, items: Array<CharSequence>,
listener: OnItemClickListener?, checkText: String?, checked: Boolean
class ListCheckBoxDialogBuilder(
context: Context,
items: List<CharSequence>,
listener: (ListCheckBoxDialogBuilder?, AlertDialog?, Int) -> Unit,
checkText: String?,
checked: Boolean
) : BaseDialogBuilder(
context
) {
@@ -42,10 +45,6 @@ class ListCheckBoxDialogBuilder constructor(
return mDialog as AlertDialog
}
interface OnItemClickListener {
fun onItemClick(builder: ListCheckBoxDialogBuilder?, dialog: AlertDialog?, position: Int)
}
init {
val view =
LayoutInflater.from(getContext()).inflate(R.layout.dialog_list_checkbox_builder, null)
@@ -55,7 +54,7 @@ class ListCheckBoxDialogBuilder constructor(
listView.adapter = ArrayAdapter(getContext(), R.layout.item_select_dialog, items)
listView.onItemClickListener =
AdapterView.OnItemClickListener { _: AdapterView<*>?, _: View?, position: Int, _: Long ->
listener?.onItemClick(this@ListCheckBoxDialogBuilder, mDialog, position)
listener(this@ListCheckBoxDialogBuilder, mDialog, position)
mDialog?.dismiss()
}
mCheckBox.text = checkText
@@ -0,0 +1,40 @@
package com.hippo.compat
import com.hippo.ehviewer.spider.SpiderInfo
import com.hippo.yorozuya.IOUtils
import java.io.InputStream
fun readLegacySpiderInfo(inputStream: InputStream): SpiderInfo {
fun read(): String {
return IOUtils.readAsciiLine(inputStream)
}
fun readInt(): Int {
return read().toInt()
}
fun readLong(): Long {
return read().toLong()
}
repeat(2) { read() } // We assert that only info v2
val gid = readLong()
val token = read()
read()
val previewPages = readInt()
val previewPerPage = readInt()
val pages = read().toInt()
val pTokenMap = hashMapOf<Int, String>()
val info = SpiderInfo(gid, pages, pTokenMap, 0, token, previewPages, previewPerPage)
runCatching {
val line = read()
val pos = line.indexOf(" ")
if (pos > 0) {
val index = line.substring(0, pos).toInt()
val pToken = line.substring(pos + 1)
if (pToken.isNotEmpty()) {
pTokenMap[index] = pToken
}
}
}
return info
}
@@ -1,190 +0,0 @@
/*
* Copyright 2018 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.drawable;
import android.content.res.ColorStateList;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.graphics.Region;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import androidx.annotation.NonNull;
import androidx.core.graphics.drawable.DrawableCompat;
public class DrawableWrapper extends Drawable implements Drawable.Callback {
private Drawable mDrawable;
private Rect mBounds;
public DrawableWrapper(Drawable drawable) {
this.setWrappedDrawable(drawable);
}
public void draw(@NonNull Canvas canvas) {
if (mDrawable instanceof BitmapDrawable bitmapDrawable) {
canvas.drawBitmap(bitmapDrawable.getBitmap(), null, mBounds, bitmapDrawable.getPaint());
} else {
synchronized (mDrawable) {
mDrawable.setBounds(mBounds);
mDrawable.draw(canvas);
}
}
}
protected void onBoundsChange(Rect bounds) {
mBounds = bounds;
}
public int getChangingConfigurations() {
return this.mDrawable.getChangingConfigurations();
}
public void setChangingConfigurations(int configs) {
this.mDrawable.setChangingConfigurations(configs);
}
public void setDither(boolean dither) {
this.mDrawable.setDither(dither);
}
public void setFilterBitmap(boolean filter) {
this.mDrawable.setFilterBitmap(filter);
}
public void setAlpha(int alpha) {
this.mDrawable.setAlpha(alpha);
}
public void setColorFilter(ColorFilter cf) {
this.mDrawable.setColorFilter(cf);
}
public boolean isStateful() {
return this.mDrawable.isStateful();
}
public boolean setState(@NonNull int[] stateSet) {
return this.mDrawable.setState(stateSet);
}
@NonNull
public int[] getState() {
return this.mDrawable.getState();
}
public void jumpToCurrentState() {
this.mDrawable.jumpToCurrentState();
}
@NonNull
public Drawable getCurrent() {
return this.mDrawable.getCurrent();
}
public boolean setVisible(boolean visible, boolean restart) {
return super.setVisible(visible, restart) || this.mDrawable.setVisible(visible, restart);
}
public int getOpacity() {
return this.mDrawable.getOpacity();
}
public Region getTransparentRegion() {
return this.mDrawable.getTransparentRegion();
}
public int getIntrinsicWidth() {
return this.mDrawable.getIntrinsicWidth();
}
public int getIntrinsicHeight() {
return this.mDrawable.getIntrinsicHeight();
}
public int getMinimumWidth() {
return this.mDrawable.getMinimumWidth();
}
public int getMinimumHeight() {
return this.mDrawable.getMinimumHeight();
}
public boolean getPadding(@NonNull Rect padding) {
return this.mDrawable.getPadding(padding);
}
public void invalidateDrawable(@NonNull Drawable who) {
this.invalidateSelf();
}
public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when) {
this.scheduleSelf(what, when);
}
public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what) {
this.unscheduleSelf(what);
}
protected boolean onLevelChange(int level) {
return this.mDrawable.setLevel(level);
}
public boolean isAutoMirrored() {
return DrawableCompat.isAutoMirrored(this.mDrawable);
}
public void setAutoMirrored(boolean mirrored) {
DrawableCompat.setAutoMirrored(this.mDrawable, mirrored);
}
public void setTint(int tint) {
DrawableCompat.setTint(this.mDrawable, tint);
}
public void setTintList(ColorStateList tint) {
DrawableCompat.setTintList(this.mDrawable, tint);
}
public void setTintMode(@NonNull PorterDuff.Mode tintMode) {
DrawableCompat.setTintMode(this.mDrawable, tintMode);
}
public void setHotspot(float x, float y) {
DrawableCompat.setHotspot(this.mDrawable, x, y);
}
public void setHotspotBounds(int left, int top, int right, int bottom) {
DrawableCompat.setHotspotBounds(this.mDrawable, left, top, right, bottom);
}
public Drawable getWrappedDrawable() {
return this.mDrawable;
}
public void setWrappedDrawable(Drawable drawable) {
if (this.mDrawable != null) {
this.mDrawable.setCallback(null);
}
this.mDrawable = drawable;
if (drawable != null) {
drawable.setCallback(this);
}
}
}
@@ -1,104 +0,0 @@
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.drawable;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.drawable.Drawable;
import com.hippo.yorozuya.MathUtils;
/**
* Show a part of the original drawable
*/
public class PreciselyClipDrawable extends DrawableWrapper {
private final boolean mClip;
private RectF mScale;
private Rect mTemp;
public PreciselyClipDrawable(Drawable drawable, int offsetX, int offsetY, int width, int height) {
super(drawable);
float originWidth = drawable.getIntrinsicWidth();
float originHeight = drawable.getIntrinsicHeight();
if (originWidth <= 0 || originHeight <= 0) {
// Can not clip
mClip = false;
} else {
mClip = true;
mScale = new RectF();
mScale.set(MathUtils.clamp(offsetX / originWidth, 0.0f, 1.0f),
MathUtils.clamp(offsetY / originHeight, 0.0f, 1.0f),
MathUtils.clamp((offsetX + width) / originWidth, 0.0f, 1.0f),
MathUtils.clamp((offsetY + height) / originHeight, 0.0f, 1.0f));
mTemp = new Rect();
}
}
@Override
protected void onBoundsChange(Rect bounds) {
if (mClip) {
if (!mScale.isEmpty()) {
mTemp.left = (int) ((mScale.left * bounds.right - mScale.right * bounds.left) /
(mScale.left * (1 - mScale.right) - mScale.right * (1 - mScale.left)));
mTemp.right = (int) (((1 - mScale.right) * bounds.left - (1 - mScale.left) * bounds.right) /
(mScale.left * (1 - mScale.right) - mScale.right * (1 - mScale.left)));
mTemp.top = (int) ((mScale.top * bounds.bottom - mScale.bottom * bounds.top) /
(mScale.top * (1 - mScale.bottom) - mScale.bottom * (1 - mScale.top)));
mTemp.bottom = (int) (((1 - mScale.bottom) * bounds.top - (1 - mScale.top) * bounds.bottom) /
(mScale.top * (1 - mScale.bottom) - mScale.bottom * (1 - mScale.top)));
super.onBoundsChange(mTemp);
}
} else {
super.onBoundsChange(bounds);
}
}
@Override
public int getIntrinsicWidth() {
if (mClip) {
return (int) (super.getIntrinsicWidth() * mScale.width());
} else {
return super.getIntrinsicWidth();
}
}
@Override
public int getIntrinsicHeight() {
if (mClip) {
return (int) (super.getIntrinsicHeight() * mScale.height());
} else {
return super.getIntrinsicHeight();
}
}
@Override
public void draw(Canvas canvas) {
if (mClip) {
if (!mScale.isEmpty()) {
int saveCount = canvas.save();
canvas.clipRect(getBounds());
super.draw(canvas);
canvas.restoreToCount(saveCount);
}
} else {
super.draw(canvas);
}
}
}
@@ -0,0 +1,68 @@
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.drawable
import android.graphics.Canvas
import android.graphics.Rect
import android.graphics.RectF
import android.graphics.drawable.Drawable
import android.graphics.drawable.DrawableWrapper
import androidx.core.graphics.withClip
/**
* Show a part of the original drawable
*/
class PreciselyClipDrawable(
drawable: Drawable,
offsetX: Int,
offsetY: Int,
width: Int,
height: Int
) : DrawableWrapper(drawable) {
private val mScale: RectF
private val mTemp = Rect()
init {
val originWidth = drawable.intrinsicWidth.toFloat()
val originHeight = drawable.intrinsicHeight.toFloat()
mScale = RectF(
(offsetX / originWidth).coerceIn(0.0f, 1.0f),
(offsetY / originHeight).coerceIn(0.0f, 1.0f),
((offsetX + width) / originWidth).coerceIn(0.0f, 1.0f),
((offsetY + height) / originHeight).coerceIn(0.0f, 1.0f),
)
}
override fun onBoundsChange(bounds: Rect) {
mTemp.left = ((mScale.left * bounds.right - mScale.right * bounds.left) / (mScale.left * (1 - mScale.right) - mScale.right * (1 - mScale.left))).toInt()
mTemp.right = (((1 - mScale.right) * bounds.left - (1 - mScale.left) * bounds.right) / (mScale.left * (1 - mScale.right) - mScale.right * (1 - mScale.left))).toInt()
mTemp.top = ((mScale.top * bounds.bottom - mScale.bottom * bounds.top) / (mScale.top * (1 - mScale.bottom) - mScale.bottom * (1 - mScale.top))).toInt()
mTemp.bottom = (((1 - mScale.bottom) * bounds.top - (1 - mScale.top) * bounds.bottom) / (mScale.top * (1 - mScale.bottom) - mScale.bottom * (1 - mScale.top))).toInt()
super.onBoundsChange(mTemp)
}
override fun getIntrinsicWidth(): Int {
return (super.getIntrinsicWidth() * mScale.width()).toInt()
}
override fun getIntrinsicHeight(): Int {
return (super.getIntrinsicHeight() * mScale.height()).toInt()
}
override fun draw(canvas: Canvas) {
canvas.withClip(bounds) { super.draw(canvas) }
}
}
@@ -1,202 +0,0 @@
/*
* Copyright 2019 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Debug;
import androidx.annotation.NonNull;
import com.hippo.util.PackageUtils;
import com.hippo.util.ReadableTime;
import com.hippo.yorozuya.FileUtils;
import com.hippo.yorozuya.IOUtils;
import com.hippo.yorozuya.OSUtils;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Arrays;
public final class Crash {
private Crash() {
}
@NonNull
private static String avoidNull(String str) {
return null != str ? str : "null";
}
private static void collectInfo(Context context, FileWriter fw) throws IOException {
try {
PackageManager pm = context.getPackageManager();
PackageInfo pi = pm.getPackageInfo(context.getPackageName(), PackageManager.GET_ACTIVITIES);
if (pi != null) {
String versionName = pi.versionName == null ? "null" : pi.versionName;
String versionCode = String.valueOf(pi.versionCode);
fw.write("======== PackageInfo ========\r\n");
fw.write("PackageName=");
fw.write(pi.packageName);
fw.write("\r\n");
fw.write("VersionName=");
fw.write(versionName);
fw.write("\r\n");
fw.write("VersionCode=");
fw.write(versionCode);
fw.write("\r\n");
String signature = PackageUtils.getSignature(context, pi.packageName);
fw.write("Signature=");
fw.write(null != signature ? signature : "null");
fw.write("\r\n");
fw.write("\r\n");
}
} catch (PackageManager.NameNotFoundException e) {
fw.write("======== PackageInfo ========\r\n");
fw.write("Can't get package information\r\n");
fw.write("\r\n");
}
// Runtime
String topActivityClazzName = "null";
fw.write("======== Runtime ========\r\n");
fw.write("TopActivity=");
fw.write(avoidNull(topActivityClazzName));
fw.write("\r\n");
fw.write("\r\n");
fw.write("\r\n");
// Device info
fw.write("======== DeviceInfo ========\r\n");
fw.write("BOARD=");
fw.write(Build.BOARD);
fw.write("\r\n");
fw.write("BOOTLOADER=");
fw.write(Build.BOOTLOADER);
fw.write("\r\n");
fw.write("SUPPORTED_ABIS=");
fw.write(Arrays.toString(Build.SUPPORTED_ABIS));
fw.write("\r\n");
fw.write("DEVICE=");
fw.write(Build.DEVICE);
fw.write("\r\n");
fw.write("DISPLAY=");
fw.write(Build.DISPLAY);
fw.write("\r\n");
fw.write("FINGERPRINT=");
fw.write(Build.FINGERPRINT);
fw.write("\r\n");
fw.write("HARDWARE=");
fw.write(Build.HARDWARE);
fw.write("\r\n");
fw.write("HOST=");
fw.write(Build.HOST);
fw.write("\r\n");
fw.write("ID=");
fw.write(Build.ID);
fw.write("\r\n");
fw.write("MANUFACTURER=");
fw.write(Build.MANUFACTURER);
fw.write("\r\n");
fw.write("MODEL=");
fw.write(Build.MODEL);
fw.write("\r\n");
fw.write("PRODUCT=");
fw.write(Build.PRODUCT);
fw.write("\r\n");
fw.write("RADIO=");
fw.write(Build.getRadioVersion());
fw.write("\r\n");
fw.write("TAGS=");
fw.write(Build.TAGS);
fw.write("\r\n");
fw.write("TYPE=");
fw.write(Build.TYPE);
fw.write("\r\n");
fw.write("USER=");
fw.write(Build.USER);
fw.write("\r\n");
fw.write("CODENAME=");
fw.write(Build.VERSION.CODENAME);
fw.write("\r\n");
fw.write("INCREMENTAL=");
fw.write(Build.VERSION.INCREMENTAL);
fw.write("\r\n");
fw.write("RELEASE=");
fw.write(Build.VERSION.RELEASE);
fw.write("\r\n");
fw.write("SDK=");
fw.write(Integer.toString(Build.VERSION.SDK_INT));
fw.write("\r\n");
fw.write("MEMORY=");
fw.write(
FileUtils.humanReadableByteCount(OSUtils.getAppAllocatedMemory(), false));
fw.write("\r\n");
fw.write("MEMORY_NATIVE=");
fw.write(FileUtils.humanReadableByteCount(Debug.getNativeHeapAllocatedSize(), false));
fw.write("\r\n");
fw.write("MEMORY_MAX=");
fw.write(FileUtils.humanReadableByteCount(OSUtils.getAppMaxMemory(), false));
fw.write("\r\n");
fw.write("MEMORY_TOTAL=");
fw.write(FileUtils.humanReadableByteCount(OSUtils.getTotalMemory(), false));
fw.write("\r\n");
fw.write("\r\n");
}
private static void getThrowableInfo(Throwable t, FileWriter fw) {
PrintWriter printWriter = new PrintWriter(fw);
t.printStackTrace(printWriter);
Throwable cause = t.getCause();
while (cause != null) {
cause.printStackTrace(printWriter);
cause = cause.getCause();
}
}
public static void saveCrashLog(Context context, Throwable t) {
File dir = AppConfig.getExternalCrashDir();
if (dir == null) {
return;
}
String nowString = ReadableTime.getFilenamableTime(System.currentTimeMillis());
String fileName = "crash-" + nowString + ".log";
File file = new File(dir, fileName);
FileWriter fw = null;
try {
fw = new FileWriter(file);
fw.write("TIME=");
fw.write(nowString);
fw.write("\r\n");
fw.write("\r\n");
collectInfo(context, fw);
fw.write("======== CrashInfo ========\r\n");
getThrowableInfo(t, fw);
fw.write("\r\n");
fw.flush();
} catch (Exception e) {
file.delete();
} finally {
IOUtils.closeQuietly(fw);
}
}
}
@@ -0,0 +1,173 @@
/*
* Copyright 2019 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer
import android.os.Build
import android.os.Debug
import com.hippo.util.ReadableTime
import com.hippo.yorozuya.FileUtils
import com.hippo.yorozuya.IOUtils
import com.hippo.yorozuya.OSUtils
import java.io.File
import java.io.FileWriter
import java.io.IOException
import java.io.PrintWriter
import java.util.Arrays
object Crash {
@Throws(IOException::class)
private fun collectInfo(fw: FileWriter) {
fw.write("======== PackageInfo ========\r\n")
fw.write("PackageName=")
fw.write(BuildConfig.APPLICATION_ID)
fw.write("\r\n")
fw.write("VersionName=")
fw.write(BuildConfig.VERSION_NAME)
fw.write("\r\n")
fw.write("VersionCode=")
fw.write(BuildConfig.VERSION_CODE)
fw.write("\r\n")
fw.write("CommitSha=")
fw.write(BuildConfig.COMMIT_SHA)
fw.write("\r\n")
fw.write("BuildTime=")
fw.write(BuildConfig.BUILD_TIME)
fw.write("\r\n")
fw.write("\r\n")
// Runtime
val topActivityClazzName = EhApplication.application.topActivity?.javaClass?.name
fw.write("======== Runtime ========\r\n")
fw.write("TopActivity=")
fw.write(topActivityClazzName ?: "null")
fw.write("\r\n")
fw.write("\r\n")
fw.write("\r\n")
// Device info
fw.write("======== DeviceInfo ========\r\n")
fw.write("BOARD=")
fw.write(Build.BOARD)
fw.write("\r\n")
fw.write("BOOTLOADER=")
fw.write(Build.BOOTLOADER)
fw.write("\r\n")
fw.write("SUPPORTED_ABIS=")
fw.write(Arrays.toString(Build.SUPPORTED_ABIS))
fw.write("\r\n")
fw.write("DEVICE=")
fw.write(Build.DEVICE)
fw.write("\r\n")
fw.write("DISPLAY=")
fw.write(Build.DISPLAY)
fw.write("\r\n")
fw.write("FINGERPRINT=")
fw.write(Build.FINGERPRINT)
fw.write("\r\n")
fw.write("HARDWARE=")
fw.write(Build.HARDWARE)
fw.write("\r\n")
fw.write("HOST=")
fw.write(Build.HOST)
fw.write("\r\n")
fw.write("ID=")
fw.write(Build.ID)
fw.write("\r\n")
fw.write("MANUFACTURER=")
fw.write(Build.MANUFACTURER)
fw.write("\r\n")
fw.write("MODEL=")
fw.write(Build.MODEL)
fw.write("\r\n")
fw.write("PRODUCT=")
fw.write(Build.PRODUCT)
fw.write("\r\n")
fw.write("RADIO=")
fw.write(Build.getRadioVersion())
fw.write("\r\n")
fw.write("TAGS=")
fw.write(Build.TAGS)
fw.write("\r\n")
fw.write("TYPE=")
fw.write(Build.TYPE)
fw.write("\r\n")
fw.write("USER=")
fw.write(Build.USER)
fw.write("\r\n")
fw.write("CODENAME=")
fw.write(Build.VERSION.CODENAME)
fw.write("\r\n")
fw.write("INCREMENTAL=")
fw.write(Build.VERSION.INCREMENTAL)
fw.write("\r\n")
fw.write("RELEASE=")
fw.write(Build.VERSION.RELEASE)
fw.write("\r\n")
fw.write("SDK=")
fw.write(Build.VERSION.SDK_INT.toString())
fw.write("\r\n")
fw.write("MEMORY=")
fw.write(
FileUtils.humanReadableByteCount(OSUtils.getAppAllocatedMemory(), false)
)
fw.write("\r\n")
fw.write("MEMORY_NATIVE=")
fw.write(FileUtils.humanReadableByteCount(Debug.getNativeHeapAllocatedSize(), false))
fw.write("\r\n")
fw.write("MEMORY_MAX=")
fw.write(FileUtils.humanReadableByteCount(OSUtils.getAppMaxMemory(), false))
fw.write("\r\n")
fw.write("MEMORY_TOTAL=")
fw.write(FileUtils.humanReadableByteCount(OSUtils.getTotalMemory(), false))
fw.write("\r\n")
fw.write("\r\n")
}
private fun getThrowableInfo(t: Throwable, fw: FileWriter) {
val printWriter = PrintWriter(fw)
t.printStackTrace(printWriter)
var cause = t.cause
while (cause != null) {
cause.printStackTrace(printWriter)
cause = cause.cause
}
}
fun saveCrashLog(t: Throwable) {
val dir = AppConfig.getExternalCrashDir() ?: return
val nowString = ReadableTime.getFilenamableTime(System.currentTimeMillis())
val fileName = "crash-$nowString.log"
val file = File(dir, fileName)
var fw: FileWriter? = null
try {
fw = FileWriter(file)
fw.write("TIME=")
fw.write(nowString)
fw.write("\r\n")
fw.write("\r\n")
collectInfo(fw)
fw.write("======== CrashInfo ========\r\n")
getThrowableInfo(t, fw)
fw.write("\r\n")
fw.flush()
} catch (e: Exception) {
file.delete()
} finally {
IOUtils.closeQuietly(fw)
}
}
}
@@ -18,39 +18,27 @@ package com.hippo.ehviewer
import android.annotation.SuppressLint
import android.app.Activity
import android.app.ActivityManager
import android.app.Application
import android.content.ComponentCallbacks2
import android.text.Html
import android.text.method.LinkMovementMethod
import android.view.View
import android.widget.TextView
import androidx.appcompat.app.AppCompatDelegate
import androidx.collection.LruCache
import androidx.core.content.getSystemService
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner
import coil.ImageLoader
import coil.ImageLoaderFactory
import coil.disk.DiskCache
import coil.util.DebugLogger
import com.hippo.Native
import com.hippo.app.BaseDialogBuilder
import com.hippo.ehviewer.client.EhCookieStore
import com.hippo.ehviewer.client.EhDns
import com.hippo.ehviewer.client.EhRequestBuilder
import com.hippo.ehviewer.client.EhSSLSocketFactory
import com.hippo.ehviewer.client.EhTagDatabase
import com.hippo.ehviewer.client.EhUrl
import com.hippo.ehviewer.client.EhX509TrustManager
import com.hippo.ehviewer.client.data.GalleryDetail
import com.hippo.ehviewer.client.parser.EventPaneParser
import com.hippo.ehviewer.coil.MergeInterceptor
import com.hippo.ehviewer.dao.buildMainDB
import com.hippo.ehviewer.download.DownloadManager
import com.hippo.ehviewer.spider.SpiderDen
import com.hippo.ehviewer.ui.CommonOperations
import com.hippo.ehviewer.ui.EhActivity
import com.hippo.util.BitmapUtils
import com.hippo.util.ExceptionUtils
import com.hippo.util.ReadableTime
import com.hippo.yorozuya.FileUtils
@@ -59,15 +47,9 @@ import eu.kanade.tachiyomi.core.preference.AndroidPreferenceStore
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
import eu.kanade.tachiyomi.util.lang.launchIO
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import okhttp3.Cache
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import java.io.File
import java.net.Proxy
import java.security.KeyStore
import java.util.Arrays
import javax.net.ssl.TrustManagerFactory
import javax.net.ssl.X509TrustManager
@@ -95,7 +77,7 @@ class EhApplication : Application(), DefaultLifecycleObserver, ImageLoaderFactor
Thread.setDefaultUncaughtExceptionHandler { t, e ->
try {
if (Settings.getSaveCrashLog()) {
Crash.saveCrashLog(application, e)
Crash.saveCrashLog(e)
}
} catch (ignored: Throwable) {
}
@@ -107,8 +89,6 @@ class EhApplication : Application(), DefaultLifecycleObserver, ImageLoaderFactor
Settings.initialize()
ReadableTime.initialize(this)
AppConfig.initialize(this)
SpiderDen.initialize(this)
BitmapUtils.initialize(this)
EhTagDatabase.update()
AppCompatDelegate.setDefaultNightMode(Settings.getTheme())
launchIO {
@@ -116,14 +96,11 @@ class EhApplication : Application(), DefaultLifecycleObserver, ImageLoaderFactor
ehDatabase
}
launchIO {
downloadManager
DownloadManager.isIdle
}
launchIO {
cleanupDownload()
}
launchIO {
theDawnOfNewDay()
}
}
mIdGenerator.setNextId(Settings.getInt(KEY_GLOBAL_STUFF_NEXT_ID, 0))
}
@@ -150,60 +127,6 @@ class EhApplication : Application(), DefaultLifecycleObserver, ImageLoaderFactor
}
}
private fun theDawnOfNewDay() {
if (!Settings.getRequestNews()) {
return
}
val store = ehCookieStore
val eh = EhUrl.HOST_E.toHttpUrl()
if (store.contains(eh, EhCookieStore.KEY_IPD_MEMBER_ID) || store.contains(
eh,
EhCookieStore.KEY_IPD_PASS_HASH
)
) {
val referer = EhUrl.REFERER_E
val request = EhRequestBuilder(EhUrl.HOST_E + "news.php", referer).build()
val call = okHttpClient.newCall(request)
try {
call.execute().use { response ->
val responseBody = response.body
val body = responseBody.string()
val html = EventPaneParser.parse(body)
if (html != null) {
showEventPane(html)
}
}
} catch (e: Throwable) {
e.printStackTrace()
}
}
}
fun showEventPane(html: String) {
if (Settings.getHideHvEvents() && html.contains("You have encountered a monster!")) {
return
}
val activity = topActivity
activity?.runOnUiThread {
val dialog = BaseDialogBuilder(activity)
.setMessage(Html.fromHtml(html, Html.FROM_HTML_MODE_LEGACY))
.setPositiveButton(android.R.string.ok, null)
.create()
dialog.setOnShowListener {
val messageView = dialog.findViewById<View>(android.R.id.message)
if (messageView is TextView) {
messageView.movementMethod = LinkMovementMethod.getInstance()
}
}
try {
dialog.show()
} catch (t: Throwable) {
// ignore
}
}
}
private fun clearTempDir() {
var dir = AppConfig.getTempDir()
if (null != dir) {
@@ -259,15 +182,12 @@ class EhApplication : Application(), DefaultLifecycleObserver, ImageLoaderFactor
locked = true
}
@OptIn(ExperimentalCoroutinesApi::class)
override fun newImageLoader(): ImageLoader {
return ImageLoader.Builder(this).apply {
okHttpClient(okHttpClient)
allowRgb565(getSystemService<ActivityManager>()!!.isLowRamDevice)
// Coil spawns a new thread for every image load by default
fetcherDispatcher(Dispatchers.IO.limitedParallelism(8))
decoderDispatcher(Dispatchers.IO.limitedParallelism(2))
transformationDispatcher(Dispatchers.IO.limitedParallelism(2))
components { add(MergeInterceptor) }
crossfade(300)
if (BuildConfig.DEBUG) logger(DebugLogger())
}.build()
}
@@ -290,29 +210,16 @@ class EhApplication : Application(), DefaultLifecycleObserver, ImageLoaderFactor
val okHttpClient by lazy {
val builder = OkHttpClient.Builder()
.cookieJar(ehCookieStore)
.cache(Cache(File(application.cacheDir, "http_cache"), 50L * 1024L * 1024L))
.dns(EhDns)
.proxySelector(ehProxySelector)
if (Settings.getDF()) {
var trustManager: X509TrustManager
try {
val trustManagerFactory = TrustManagerFactory.getInstance(
TrustManagerFactory.getDefaultAlgorithm()
)
trustManagerFactory.init(null as KeyStore?)
val trustManagers = trustManagerFactory.trustManagers
check(!(trustManagers.size != 1 || trustManagers[0] !is X509TrustManager)) {
"Unexpected default trust managers:" + Arrays.toString(
trustManagers
)
}
trustManager = trustManagers[0] as X509TrustManager
} catch (e: Exception) {
e.printStackTrace()
trustManager = EhX509TrustManager
}
val factory =
TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())!!
factory.init(null as KeyStore?)
val manager = factory.trustManagers!!
val trustManager = manager.filterIsInstance<X509TrustManager>().first()
builder.sslSocketFactory(EhSSLSocketFactory, trustManager)
builder.proxy(Proxy.NO_PROXY)
}
@@ -328,15 +235,6 @@ class EhApplication : Application(), DefaultLifecycleObserver, ImageLoaderFactor
}
}
@JvmStatic
val spiderInfoCache by lazy {
DiskCache.Builder().directory(File(application.cacheDir, "spider_info"))
.maxSizeBytes(20 * 1024 * 1024).build()
}
@JvmStatic
val downloadManager by lazy { DownloadManager() }
@JvmStatic
val hosts by lazy { Hosts(application, "hosts.db") }
@@ -1,595 +0,0 @@
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.paging.PagingSource;
import androidx.room.Room;
import com.hippo.ehviewer.client.data.GalleryInfo;
import com.hippo.ehviewer.dao.BasicDao;
import com.hippo.ehviewer.dao.DownloadDirname;
import com.hippo.ehviewer.dao.DownloadDirnameDao;
import com.hippo.ehviewer.dao.DownloadInfo;
import com.hippo.ehviewer.dao.DownloadLabel;
import com.hippo.ehviewer.dao.DownloadLabelDao;
import com.hippo.ehviewer.dao.DownloadsDao;
import com.hippo.ehviewer.dao.EhDatabase;
import com.hippo.ehviewer.dao.Filter;
import com.hippo.ehviewer.dao.HistoryDao;
import com.hippo.ehviewer.dao.HistoryInfo;
import com.hippo.ehviewer.dao.LocalFavoriteInfo;
import com.hippo.ehviewer.dao.LocalFavoritesDao;
import com.hippo.ehviewer.dao.QuickSearch;
import com.hippo.ehviewer.dao.QuickSearchDao;
import com.hippo.ehviewer.download.DownloadManager;
import com.hippo.util.ExceptionUtils;
import com.hippo.yorozuya.IOUtils;
import com.hippo.yorozuya.ObjectUtils;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
public class EhDB {
private static final int CUR_DB_VER = 4;
private static final EhDatabase db = EhApplication.getEhDatabase();
private static void upgradeDB(SQLiteDatabase db, int oldVersion) {
switch (oldVersion) {
case 1: // 1 to 2, add FILTER
db.execSQL("CREATE TABLE IF NOT EXISTS \"FILTER\" (" + //
"\"_id\" INTEGER PRIMARY KEY ," + // 0: id
"\"MODE\" INTEGER NOT NULL ," + // 1: mode
"\"TEXT\" TEXT," + // 2: text
"\"ENABLE\" INTEGER);"); // 3: enable
case 2: // 2 to 3, add ENABLE column to table FILTER
db.execSQL("CREATE TABLE " + "\"FILTER2\" (" +
"\"_id\" INTEGER PRIMARY KEY ," +
"\"MODE\" INTEGER NOT NULL ," +
"\"TEXT\" TEXT," +
"\"ENABLE\" INTEGER);");
db.execSQL("INSERT INTO \"FILTER2\" (" +
"_id, MODE, TEXT, ENABLE)" +
"SELECT _id, MODE, TEXT, 1 FROM FILTER;");
db.execSQL("DROP TABLE FILTER");
db.execSQL("ALTER TABLE FILTER2 RENAME TO FILTER");
case 3: // 3 to 4, add PAGE_FROM and PAGE_TO column to QUICK_SEARCH
db.execSQL("CREATE TABLE " + "\"QUICK_SEARCH2\" (" +
"\"_id\" INTEGER PRIMARY KEY ," +
"\"NAME\" TEXT," +
"\"MODE\" INTEGER NOT NULL ," +
"\"CATEGORY\" INTEGER NOT NULL ," +
"\"KEYWORD\" TEXT," +
"\"ADVANCE_SEARCH\" INTEGER NOT NULL ," +
"\"MIN_RATING\" INTEGER NOT NULL ," +
"\"PAGE_FROM\" INTEGER NOT NULL ," +
"\"PAGE_TO\" INTEGER NOT NULL ," +
"\"TIME\" INTEGER NOT NULL );");
db.execSQL("INSERT INTO \"QUICK_SEARCH2\" (" +
"_id, NAME, MODE, CATEGORY, KEYWORD, ADVANCE_SEARCH, MIN_RATING, PAGE_FROM, PAGE_TO, TIME)" +
"SELECT _id, NAME, MODE, CATEGORY, KEYWORD, ADVANCE_SEARCH, MIN_RATING, -1, -1, TIME FROM QUICK_SEARCH;");
db.execSQL("DROP TABLE QUICK_SEARCH");
db.execSQL("ALTER TABLE QUICK_SEARCH2 RENAME TO QUICK_SEARCH");
}
}
public static synchronized List<DownloadInfo> getAllDownloadInfo() {
DownloadsDao dao = db.downloadsDao();
List<DownloadInfo> list = dao.list();
// Fix state
for (DownloadInfo info : list) {
if (info.state == DownloadInfo.STATE_WAIT || info.state == DownloadInfo.STATE_DOWNLOAD) {
info.state = DownloadInfo.STATE_NONE;
}
}
return list;
}
public static synchronized void moveDownloadInfo(int fromPosition, int toPosition) {
if (fromPosition == toPosition) {
return;
}
boolean reverse = fromPosition > toPosition;
int offset = reverse ? toPosition : fromPosition;
int limit = reverse ? fromPosition - toPosition + 1 : toPosition - fromPosition + 1;
DownloadsDao dao = db.downloadsDao();
List<DownloadInfo> list = dao.list(offset, limit);
int step = reverse ? 1 : -1;
int start = reverse ? limit - 1 : 0;
int end = reverse ? 0 : limit - 1;
long toTime = list.get(end).time;
for (int i = end; reverse ? i < start : i > start; i += step) {
list.get(i).time = list.get(i + step).time;
}
list.get(start).time = toTime;
dao.update(list);
}
// Insert or update
public static synchronized void putDownloadInfo(DownloadInfo downloadInfo) {
DownloadsDao dao = db.downloadsDao();
if (null != dao.load(downloadInfo.getGid())) {
// Update
dao.update(downloadInfo);
} else {
// Insert
dao.insert(downloadInfo);
}
}
public static synchronized void removeDownloadInfo(DownloadInfo downloadInfo) {
db.downloadsDao().delete(downloadInfo);
}
@Nullable
public static synchronized String getDownloadDirname(long gid) {
DownloadDirnameDao dao = db.downloadDirnameDao();
DownloadDirname raw = dao.load(gid);
if (raw != null) {
return raw.getDirname();
} else {
return null;
}
}
/**
* Insert or update
*/
public static synchronized void putDownloadDirname(long gid, String dirname) {
DownloadDirnameDao dao = db.downloadDirnameDao();
DownloadDirname raw = dao.load(gid);
if (raw != null) { // Update
raw.setDirname(dirname);
dao.update(raw);
} else { // Insert
raw = new DownloadDirname();
raw.setGid(gid);
raw.setDirname(dirname);
dao.insert(raw);
}
}
public static synchronized void removeDownloadDirname(long gid) {
DownloadDirnameDao dao = db.downloadDirnameDao();
dao.deleteByKey(gid);
}
public static synchronized void clearDownloadDirname() {
DownloadDirnameDao dao = db.downloadDirnameDao();
dao.deleteAll();
}
@NonNull
public static synchronized List<DownloadLabel> getAllDownloadLabelList() {
DownloadLabelDao dao = db.downloadLabelDao();
return dao.list();
}
public static synchronized DownloadLabel addDownloadLabel(String label) {
DownloadLabelDao dao = db.downloadLabelDao();
DownloadLabel raw = new DownloadLabel();
raw.setLabel(label);
raw.setTime(System.currentTimeMillis());
raw.setId(dao.insert(raw));
return raw;
}
public static synchronized DownloadLabel addDownloadLabel(DownloadLabel raw) {
// Reset id
raw.setId(null);
DownloadLabelDao dao = db.downloadLabelDao();
raw.setId(dao.insert(raw));
return raw;
}
public static synchronized void updateDownloadLabel(DownloadLabel raw) {
DownloadLabelDao dao = db.downloadLabelDao();
dao.update(raw);
}
public static synchronized void moveDownloadLabel(int fromPosition, int toPosition) {
if (fromPosition == toPosition) {
return;
}
boolean reverse = fromPosition > toPosition;
int offset = reverse ? toPosition : fromPosition;
int limit = reverse ? fromPosition - toPosition + 1 : toPosition - fromPosition + 1;
DownloadLabelDao dao = db.downloadLabelDao();
List<DownloadLabel> list = dao.list(offset, limit);
int step = reverse ? 1 : -1;
int start = reverse ? limit - 1 : 0;
int end = reverse ? 0 : limit - 1;
long toTime = list.get(end).getTime();
for (int i = end; reverse ? i < start : i > start; i += step) {
list.get(i).setTime(list.get(i + step).getTime());
}
list.get(start).setTime(toTime);
dao.update(list);
}
public static synchronized void removeDownloadLabel(DownloadLabel raw) {
DownloadLabelDao dao = db.downloadLabelDao();
dao.delete(raw);
}
public static synchronized List<GalleryInfo> getAllLocalFavorites() {
LocalFavoritesDao dao = db.localFavoritesDao();
List<LocalFavoriteInfo> list = dao.list();
return new ArrayList<>(list);
}
public static synchronized List<GalleryInfo> searchLocalFavorites(String query) {
LocalFavoritesDao dao = db.localFavoritesDao();
List<LocalFavoriteInfo> list = dao.list("%" + query + "%");
return new ArrayList<>(list);
}
public static synchronized void removeLocalFavorites(long gid) {
db.localFavoritesDao().deleteByKey(gid);
}
public static synchronized void removeLocalFavorites(long[] gidArray) {
LocalFavoritesDao dao = db.localFavoritesDao();
for (long gid : gidArray) {
dao.deleteByKey(gid);
}
}
public static synchronized boolean containLocalFavorites(long gid) {
LocalFavoritesDao dao = db.localFavoritesDao();
return null != dao.load(gid);
}
public static synchronized void putLocalFavorites(GalleryInfo galleryInfo) {
LocalFavoritesDao dao = db.localFavoritesDao();
if (null == dao.load(galleryInfo.getGid())) {
LocalFavoriteInfo info;
if (galleryInfo instanceof LocalFavoriteInfo) {
info = (LocalFavoriteInfo) galleryInfo;
} else {
info = new LocalFavoriteInfo(galleryInfo);
info.time = System.currentTimeMillis();
}
dao.insert(info);
}
}
public static synchronized void putLocalFavorites(List<GalleryInfo> galleryInfoList) {
for (GalleryInfo gi : galleryInfoList) {
putLocalFavorites(gi);
}
}
public static synchronized List<QuickSearch> getAllQuickSearch() {
QuickSearchDao dao = db.quickSearchDao();
return dao.list();
}
public static synchronized void insertQuickSearch(QuickSearch quickSearch) {
QuickSearchDao dao = db.quickSearchDao();
quickSearch.setId(null);
quickSearch.setTime(System.currentTimeMillis());
quickSearch.setId(dao.insert(quickSearch));
}
public static synchronized void importQuickSearch(List<QuickSearch> quickSearchList) {
QuickSearchDao dao = db.quickSearchDao();
for (QuickSearch quickSearch : quickSearchList) {
dao.insert(quickSearch);
}
}
public static synchronized void deleteQuickSearch(QuickSearch quickSearch) {
QuickSearchDao dao = db.quickSearchDao();
dao.delete(quickSearch);
}
public static synchronized void moveQuickSearch(int fromPosition, int toPosition) {
if (fromPosition == toPosition) {
return;
}
boolean reverse = fromPosition > toPosition;
int offset = reverse ? toPosition : fromPosition;
int limit = reverse ? fromPosition - toPosition + 1 : toPosition - fromPosition + 1;
QuickSearchDao dao = db.quickSearchDao();
List<QuickSearch> list = dao.list(offset, limit);
int step = reverse ? 1 : -1;
int start = reverse ? limit - 1 : 0;
int end = reverse ? 0 : limit - 1;
long toTime = list.get(end).getTime();
for (int i = end; reverse ? i < start : i > start; i += step) {
list.get(i).setTime(list.get(i + step).getTime());
}
list.get(start).setTime(toTime);
dao.update(list);
}
public static synchronized PagingSource<Integer, HistoryInfo> getHistoryLazyList() {
return db.historyDao().listLazy();
}
public static synchronized void putHistoryInfo(GalleryInfo galleryInfo) {
HistoryDao dao = db.historyDao();
HistoryInfo info;
if (galleryInfo instanceof HistoryInfo) {
info = (HistoryInfo) galleryInfo;
} else {
info = new HistoryInfo(galleryInfo);
}
info.time = System.currentTimeMillis();
if (null != dao.load(info.getGid())) {
dao.update(info);
} else {
dao.insert(info);
}
}
public static synchronized void putHistoryInfo(List<HistoryInfo> historyInfoList) {
HistoryDao dao = db.historyDao();
for (HistoryInfo info : historyInfoList) {
if (null == dao.load(info.getGid())) {
dao.insert(info);
}
}
}
public static synchronized void deleteHistoryInfo(HistoryInfo info) {
HistoryDao dao = db.historyDao();
dao.delete(info);
}
public static synchronized void clearHistoryInfo() {
HistoryDao dao = db.historyDao();
dao.deleteAll();
}
public static synchronized List<Filter> getAllFilter() {
return db.filterDao().list();
}
public static synchronized boolean addFilter(Filter filter) {
Filter existFilter;
try {
existFilter = db.filterDao().load(filter.text, filter.mode);
} catch (Exception e) {
existFilter = null;
}
if (existFilter == null) {
filter.setId(null);
filter.setId(db.filterDao().insert(filter));
return true;
} else {
return false;
}
}
public static synchronized void deleteFilter(Filter filter) {
db.filterDao().delete(filter);
}
public static synchronized void triggerFilter(Filter filter) {
filter.enable = !filter.enable;
db.filterDao().update(filter);
}
private static <T> void copyDao(BasicDao<T> from, BasicDao<T> to) {
List<T> list = from.list();
for (T item : list)
to.insert(item);
}
public static synchronized boolean exportDB(Context context, Uri uri) {
final String ehExportName = "eh.export.db";
// Delete old export db
context.deleteDatabase(ehExportName);
EhDatabase newDb = Room.databaseBuilder(context, EhDatabase.class, ehExportName).allowMainThreadQueries().build();
try {
// Copy data to a export db
copyDao(db.downloadsDao(), newDb.downloadsDao());
copyDao(db.downloadLabelDao(), newDb.downloadLabelDao());
copyDao(db.downloadDirnameDao(), newDb.downloadDirnameDao());
copyDao(db.historyDao(), newDb.historyDao());
copyDao(db.quickSearchDao(), newDb.quickSearchDao());
copyDao(db.localFavoritesDao(), newDb.localFavoritesDao());
copyDao(db.filterDao(), newDb.filterDao());
// Close export db so we can copy it
if (newDb.isOpen()) {
newDb.close();
}
// Copy export db to data dir
File dbFile = context.getDatabasePath(ehExportName);
if (dbFile == null || !dbFile.isFile()) {
return false;
}
InputStream is = null;
OutputStream os = null;
try {
is = new FileInputStream(dbFile);
os = context.getContentResolver().openOutputStream(uri);
IOUtils.copy(is, os);
return true;
} catch (IOException e) {
e.printStackTrace();
} finally {
IOUtils.closeQuietly(is);
IOUtils.closeQuietly(os);
}
// Delete failed file
return false;
} finally {
context.deleteDatabase(ehExportName);
}
}
/**
* @return error string, null for no error
*/
public static synchronized String importDB(Context context, Uri uri) {
try {
InputStream inputStream = context.getContentResolver().openInputStream(uri);
// Copy file to cache dir
File file = File.createTempFile("importDatabase", "");
FileOutputStream outputStream = new FileOutputStream(file);
byte[] buff = new byte[1024];
int read;
if (inputStream != null) {
while ((read = inputStream.read(buff, 0, buff.length)) > 0) {
outputStream.write(buff, 0, read);
}
} else {
return context.getString(R.string.cant_read_the_file);
}
inputStream.close();
outputStream.close();
// Check database version
SQLiteDatabase oldDB = SQLiteDatabase.openDatabase(
file.getPath(), null, SQLiteDatabase.NO_LOCALIZED_COLLATORS);
int newVersion = CUR_DB_VER;
int oldVersion = oldDB.getVersion();
if (oldVersion < newVersion) {
upgradeDB(oldDB, oldVersion);
oldDB.setVersion(newVersion);
} else if (oldVersion > newVersion) {
return context.getString(R.string.db_version_too_high);
}
// Crete temp room database from cache file
String tmpDBName = "tmp.db";
context.deleteDatabase(tmpDBName);
EhDatabase oldRoomDatabase = Room.databaseBuilder(context, EhDatabase.class, tmpDBName)
.createFromFile(file).allowMainThreadQueries().build();
// Download label
DownloadManager manager = EhApplication.getDownloadManager();
try {
List<DownloadLabel> downloadLabelList = oldRoomDatabase.downloadLabelDao().list();
manager.addDownloadLabel(downloadLabelList);
} catch (Exception ignored) {
}
// Downloads
try {
List<DownloadInfo> downloadInfoList = oldRoomDatabase.downloadsDao().list();
manager.addDownload(downloadInfoList, false);
} catch (Exception ignored) {
}
// Download dirname
try {
List<DownloadDirname> downloadDirnameList = oldRoomDatabase.downloadDirnameDao().list();
for (DownloadDirname dirname : downloadDirnameList) {
putDownloadDirname(dirname.getGid(), dirname.getDirname());
}
} catch (Exception ignored) {
}
// History
try {
List<HistoryInfo> historyInfoList = oldRoomDatabase.historyDao().list();
putHistoryInfo(historyInfoList);
} catch (Exception ignored) {
}
// QuickSearch
try {
List<QuickSearch> quickSearchList = oldRoomDatabase.quickSearchDao().list();
List<QuickSearch> currentQuickSearchList = db.quickSearchDao().list();
List<QuickSearch> importList = new ArrayList<>();
for (QuickSearch quickSearch : quickSearchList) {
String name = quickSearch.getName();
for (QuickSearch q : currentQuickSearchList) {
if (ObjectUtils.equal(q.getName(), name)) {
// The same name
name = null;
break;
}
}
if (null == name) {
continue;
}
importList.add(quickSearch);
}
importQuickSearch(importList);
} catch (Exception ignored) {
}
// LocalFavorites
try {
List<LocalFavoriteInfo> localFavoriteInfoList = oldRoomDatabase.localFavoritesDao().list();
for (LocalFavoriteInfo info : localFavoriteInfoList) {
putLocalFavorites(info);
}
} catch (Exception ignored) {
}
// Filter
try {
List<Filter> filterList = oldRoomDatabase.filterDao().list();
List<Filter> currentFilterList = db.filterDao().list();
for (Filter filter : filterList) {
if (!currentFilterList.contains(filter)) {
addFilter(filter);
}
}
} catch (Exception ignored) {
}
// Delete temp database
if (oldRoomDatabase.isOpen()) {
oldRoomDatabase.close();
}
context.deleteDatabase(tmpDBName);
return null;
} catch (Throwable e) {
ExceptionUtils.throwIfFatal(e);
// Ignore
return context.getString(R.string.cant_read_the_file);
}
}
}
@@ -0,0 +1,459 @@
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer
import android.content.Context
import android.net.Uri
import android.os.ParcelFileDescriptor
import android.os.ParcelFileDescriptor.MODE_READ_ONLY
import androidx.paging.PagingSource
import androidx.room.Room.databaseBuilder
import com.hippo.Native
import com.hippo.ehviewer.EhApplication.Companion.ehDatabase
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.dao.BasicDao
import com.hippo.ehviewer.dao.DownloadDirname
import com.hippo.ehviewer.dao.DownloadInfo
import com.hippo.ehviewer.dao.DownloadLabel
import com.hippo.ehviewer.dao.EhDatabase
import com.hippo.ehviewer.dao.Filter
import com.hippo.ehviewer.dao.HistoryInfo
import com.hippo.ehviewer.dao.LocalFavoriteInfo
import com.hippo.ehviewer.dao.QuickSearch
import com.hippo.ehviewer.download.DownloadManager
object EhDB {
private const val CUR_DB_VER = 4
private val db = ehDatabase
// Fix state
@get:Synchronized
val allDownloadInfo: List<DownloadInfo>
get() = db.downloadsDao().list().onEach {
if (it.state == DownloadInfo.STATE_WAIT || it.state == DownloadInfo.STATE_DOWNLOAD) {
it.state = DownloadInfo.STATE_NONE
}
}
@Synchronized
fun updateDownloadInfo(downloadInfos: List<DownloadInfo>) {
val dao = db.downloadsDao()
dao.update(downloadInfos)
}
@Synchronized
fun putDownloadInfo(downloadInfo: DownloadInfo) {
db.downloadsDao().run {
if (load(downloadInfo.gid) != null) {
update(downloadInfo)
} else {
insert(downloadInfo)
}
}
}
@Synchronized
fun removeDownloadInfo(downloadInfo: DownloadInfo) {
db.downloadsDao().delete(downloadInfo)
}
@Synchronized
fun getDownloadDirname(gid: Long): String? {
val dao = db.downloadDirnameDao()
val raw = dao.load(gid)
return raw?.dirname
}
@Synchronized
fun putDownloadDirname(gid: Long, dirname: String?) {
val dao = db.downloadDirnameDao()
var raw = dao.load(gid)
if (raw != null) {
raw.dirname = dirname
dao.update(raw)
} else {
raw = DownloadDirname(gid, dirname)
dao.insert(raw)
}
}
@Synchronized
fun removeDownloadDirname(gid: Long) {
val dao = db.downloadDirnameDao()
dao.deleteByKey(gid)
}
@Synchronized
fun clearDownloadDirname() {
val dao = db.downloadDirnameDao()
dao.deleteAll()
}
@get:Synchronized
val allDownloadLabelList: List<DownloadLabel>
get() = db.downloadLabelDao().list()
@Synchronized
fun addDownloadLabel(label: String): DownloadLabel {
val dao = db.downloadLabelDao()
val raw = DownloadLabel()
raw.label = label
raw.time = System.currentTimeMillis()
raw.id = dao.insert(raw)
return raw
}
@Synchronized
fun addDownloadLabel(raw: DownloadLabel): DownloadLabel {
// Reset id
raw.id = null
val dao = db.downloadLabelDao()
raw.id = dao.insert(raw)
return raw
}
@Synchronized
fun updateDownloadLabel(raw: DownloadLabel?) {
val dao = db.downloadLabelDao()
dao.update(raw!!)
}
@Synchronized
fun moveDownloadLabel(fromPosition: Int, toPosition: Int) {
if (fromPosition == toPosition) {
return
}
val reverse = fromPosition > toPosition
val offset = if (reverse) toPosition else fromPosition
val limit = if (reverse) fromPosition - toPosition + 1 else toPosition - fromPosition + 1
val dao = db.downloadLabelDao()
val list = dao.list(offset, limit)
val step = if (reverse) 1 else -1
val start = if (reverse) limit - 1 else 0
val end = if (reverse) 0 else limit - 1
val toTime = list[end].time
var i = end
while (if (reverse) i < start else i > start) {
list[i].time = list[i + step].time
i += step
}
list[start].time = toTime
dao.update(list)
}
@Synchronized
fun removeDownloadLabel(raw: DownloadLabel?) {
val dao = db.downloadLabelDao()
dao.delete(raw!!)
}
@get:Synchronized
val allLocalFavorites: List<GalleryInfo>
get() {
val dao = db.localFavoritesDao()
val list = dao.list()
return ArrayList<GalleryInfo>(list)
}
@Synchronized
fun searchLocalFavorites(query: String): List<GalleryInfo> {
val dao = db.localFavoritesDao()
val list = dao.list("%$query%")
return ArrayList<GalleryInfo>(list)
}
@Synchronized
fun removeLocalFavorites(gid: Long) {
db.localFavoritesDao().deleteByKey(gid)
}
@Synchronized
fun removeLocalFavorites(gidArray: LongArray) {
val dao = db.localFavoritesDao()
for (gid in gidArray) {
dao.deleteByKey(gid)
}
}
@JvmStatic
@Synchronized
fun containLocalFavorites(gid: Long): Boolean {
val dao = db.localFavoritesDao()
return null != dao.load(gid)
}
@Synchronized
fun putLocalFavorites(galleryInfo: GalleryInfo) {
val dao = db.localFavoritesDao()
if (null == dao.load(galleryInfo.gid)) {
val info: LocalFavoriteInfo
if (galleryInfo is LocalFavoriteInfo) {
info = galleryInfo
} else {
info = LocalFavoriteInfo(galleryInfo)
info.time = System.currentTimeMillis()
}
dao.insert(info)
}
}
@Synchronized
fun putLocalFavorites(galleryInfoList: List<GalleryInfo>) {
for (gi in galleryInfoList) {
putLocalFavorites(gi)
}
}
@get:Synchronized
val allQuickSearch: List<QuickSearch>
get() {
val dao = db.quickSearchDao()
return dao.list()
}
@Synchronized
fun insertQuickSearch(quickSearch: QuickSearch) {
val dao = db.quickSearchDao()
quickSearch.id = null
quickSearch.time = System.currentTimeMillis()
quickSearch.id = dao.insert(quickSearch)
}
@Synchronized
fun importQuickSearch(quickSearchList: List<QuickSearch?>) {
val dao = db.quickSearchDao()
for (quickSearch in quickSearchList) {
dao.insert(quickSearch!!)
}
}
@Synchronized
fun deleteQuickSearch(quickSearch: QuickSearch?) {
val dao = db.quickSearchDao()
dao.delete(quickSearch)
}
@Synchronized
fun moveQuickSearch(fromPosition: Int, toPosition: Int) {
if (fromPosition == toPosition) {
return
}
val reverse = fromPosition > toPosition
val offset = if (reverse) toPosition else fromPosition
val limit = if (reverse) fromPosition - toPosition + 1 else toPosition - fromPosition + 1
val dao = db.quickSearchDao()
val list = dao.list(offset, limit)
val step = if (reverse) 1 else -1
val start = if (reverse) limit - 1 else 0
val end = if (reverse) 0 else limit - 1
val toTime = list[end].time
var i = end
while (if (reverse) i < start else i > start) {
list[i].time = list[i + step].time
i += step
}
list[start].time = toTime
dao.update(list)
}
@get:Synchronized
val historyLazyList: PagingSource<Int, HistoryInfo>
get() = db.historyDao().listLazy()
@Synchronized
fun putHistoryInfo(galleryInfo: GalleryInfo?) {
val dao = db.historyDao()
val info: HistoryInfo = if (galleryInfo is HistoryInfo) {
galleryInfo
} else {
HistoryInfo(galleryInfo!!)
}
info.time = System.currentTimeMillis()
if (null != dao.load(info.gid)) {
dao.update(info)
} else {
dao.insert(info)
}
}
@Synchronized
fun putHistoryInfoNonRefresh(info: GalleryInfo) {
val dao = db.historyDao()
val i = dao.load(info.gid)
if (null != i) {
val historyInfo: HistoryInfo
if (info is HistoryInfo) {
historyInfo = info
} else {
historyInfo = HistoryInfo(info)
historyInfo.time = i.time
}
dao.update(historyInfo)
}
}
@Synchronized
fun putHistoryInfo(historyInfoList: List<HistoryInfo>) {
val dao = db.historyDao()
for (info in historyInfoList) {
if (null == dao.load(info.gid)) {
dao.insert(info)
}
}
}
@Synchronized
fun deleteHistoryInfo(info: HistoryInfo?) {
val dao = db.historyDao()
dao.delete(info!!)
}
@Synchronized
fun clearHistoryInfo() {
val dao = db.historyDao()
dao.deleteAll()
}
@get:Synchronized
val allFilter: List<Filter>
get() = db.filterDao().list()
@Synchronized
fun addFilter(filter: Filter): Boolean {
val existFilter: Filter? = try {
db.filterDao().load(filter.text!!, filter.mode)
} catch (e: Exception) {
null
}
return if (existFilter == null) {
filter.id = null
filter.id = db.filterDao().insert(filter)
true
} else {
false
}
}
@Synchronized
fun deleteFilter(filter: Filter) {
db.filterDao().delete(filter)
}
@Synchronized
fun triggerFilter(filter: Filter) {
filter.enable = filter.enable?.not() ?: false
db.filterDao().update(filter)
}
private fun <T> copyDao(from: BasicDao<T>, to: BasicDao<T>) {
val list = from.list()
for (item in list) to.insert(item)
}
@Synchronized
fun exportDB(context: Context, uri: Uri): Boolean {
val ehExportName = "eh.export.db"
runCatching {
context.deleteDatabase(ehExportName)
val newDb = databaseBuilder(context, EhDatabase::class.java, ehExportName).build()
copyDao(db.downloadsDao(), newDb.downloadsDao())
copyDao(db.downloadLabelDao(), newDb.downloadLabelDao())
copyDao(db.downloadDirnameDao(), newDb.downloadDirnameDao())
copyDao(db.historyDao(), newDb.historyDao())
copyDao(db.quickSearchDao(), newDb.quickSearchDao())
copyDao(db.localFavoritesDao(), newDb.localFavoritesDao())
copyDao(db.filterDao(), newDb.filterDao())
val dbFile = context.getDatabasePath(ehExportName)
context.contentResolver.openFileDescriptor(uri, "rw")!!.use { toFd ->
ParcelFileDescriptor.open(dbFile, MODE_READ_ONLY).use { fromFd ->
Native.sendfile(fromFd.fd, toFd.fd)
}
}
return true
}.onFailure {
it.printStackTrace()
}
return false
}
/**
* @return error string, null for no error
*/
@Synchronized
fun importDB(context: Context, uri: Uri): String? {
runCatching {
val oldDB = databaseBuilder(context, EhDatabase::class.java, "tmp.db")
.createFromInputStream { context.contentResolver.openInputStream(uri) }.build()
// Download label
val manager = DownloadManager
runCatching {
val downloadLabelList = oldDB.downloadLabelDao().list()
manager.addDownloadLabel(downloadLabelList)
}
// Downloads
runCatching {
val downloadInfoList = oldDB.downloadsDao().list()
manager.addDownload(downloadInfoList, false)
}
// Download dirname
runCatching {
oldDB.downloadDirnameDao().list().forEach {
putDownloadDirname(it.gid, it.dirname)
}
}
// History
runCatching {
val historyInfoList = oldDB.historyDao().list()
putHistoryInfo(historyInfoList)
}
// QuickSearch
runCatching {
val quickSearchList = oldDB.quickSearchDao().list()
val currentQuickSearchList = db.quickSearchDao().list()
val importList = quickSearchList.mapNotNull { newQS ->
newQS.takeIf { currentQuickSearchList.find { it.name == newQS.name } == null }
}
importQuickSearch(importList)
}
// LocalFavorites
runCatching {
oldDB.localFavoritesDao().list().forEach {
putLocalFavorites(it)
}
}
// Filter
runCatching {
val filterList = oldDB.filterDao().list()
val currentFilterList = db.filterDao().list()
filterList.forEach {
if (it !in currentFilterList) addFilter(it)
}
}
oldDB.close()
}.onFailure {
it.printStackTrace()
return context.getString(R.string.cant_read_the_file)
}
return null
}
}
@@ -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();
}
@@ -30,7 +30,7 @@ import com.hippo.ehviewer.client.data.FavListUrlBuilder;
import com.hippo.ehviewer.ui.CommonOperations;
import com.hippo.ehviewer.ui.scene.GalleryListScene;
import com.hippo.unifile.UniFile;
import com.hippo.yorozuya.AssertUtils;
import com.hippo.yorozuya.AssertUtilsKt;
import com.hippo.yorozuya.NumberUtils;
import java.util.Locale;
@@ -45,6 +45,9 @@ public class Settings {
public static final String KEY_THEME = "theme";
public static final String KEY_ACCOUNT = "account";
public static final String KEY_IMAGE_LIMITS = "image_limits";
public static final String KEY_U_CONFIG = "uconfig";
public static final String KEY_MY_TAGS = "mytags";
public static final String KEY_BLACK_DARK_THEME = "black_dark_theme";
public static final int THEME_SYSTEM = -1;
public static final String KEY_GALLERY_SITE = "gallery_site";
@@ -52,11 +55,28 @@ public class Settings {
public static final String KEY_DETAIL_SIZE = "detail_size";
public static final String KEY_THUMB_SIZE = "thumb_size_";
public static final String KEY_THUMB_RESOLUTION = "thumb_resolution";
public static final String KEY_SHOW_JPN_TITLE = "show_jpn_title";
public static final String KEY_SHOW_TAG_TRANSLATIONS = "show_tag_translations";
public static final String KEY_TAG_TRANSLATIONS_SOURCE = "tag_translations_source";
public static final String KEY_DEFAULT_CATEGORIES = "default_categories";
public static final int DEFAULT_DEFAULT_CATEGORIES = EhConfig.ALL_CATEGORY;
public static final String KEY_EXCLUDED_TAG_NAMESPACES = "excluded_tag_namespaces";
public static final String KEY_EXCLUDED_LANGUAGES = "excluded_languages";
public static final String KEY_REQUEST_NEWS = "request_news";
public static final String KEY_REQUEST_NEWS_TIMER = "request_news_timer";
public static final String KEY_REQUEST_NEWS_TIMER_HOUR = "request_news_timer_hour";
public static final String KEY_REQUEST_NEWS_TIMER_MINUTE = "request_news_timer_minute";
public static final String KEY_HIDE_HV_EVENTS = "hide_hv_events";
public static final String[] SIGN_IN_REQUIRED = {
KEY_IMAGE_LIMITS,
KEY_GALLERY_SITE,
KEY_U_CONFIG,
KEY_MY_TAGS,
KEY_SHOW_JPN_TITLE,
KEY_REQUEST_NEWS,
KEY_REQUEST_NEWS_TIMER,
KEY_HIDE_HV_EVENTS
};
/********************
****** Privacy and Security
********************/
@@ -85,9 +105,9 @@ 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 boolean DEFAULT_HIDE_HV_EVENTS = false;
private static final String KEY_SHOW_COMMENTS = "show_gallery_comments";
private static final boolean DEFAULT_SHOW_COMMENTS = true;
private static final String TAG = Settings.class.getSimpleName();
@@ -109,7 +129,6 @@ public class Settings {
private static final int DEFAULT_DETAIL_SIZE = 0;
private static final int DEFAULT_THUMB_SIZE = 120;
private static final int DEFAULT_THUMB_RESOLUTION = 0;
private static final String KEY_SHOW_JPN_TITLE = "show_jpn_title";
private static final boolean DEFAULT_SHOW_JPN_TITLE = false;
private static final String KEY_SHOW_GALLERY_PAGES = "show_gallery_pages";
private static final boolean DEFAULT_SHOW_GALLERY_PAGES = false;
@@ -179,6 +198,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";
@@ -189,8 +209,7 @@ public class Settings {
private static final int DEFAULT_CLIPBOARD_TEXT_HASH_CODE = 0;
private static final String KEY_DOWNLOAD_DELAY = "download_delay";
private static final int DEFAULT_DOWNLOAD_DELAY = 0;
private static final String KEY_REQUEST_NEWS = "request_news";
private static final boolean DEFAULT_REQUEST_NEWS = true;
private static final boolean DEFAULT_REQUEST_NEWS = false;
private static final String KEY_ARCHIVE_PASSWDS = "archive_passwds";
private static final String KEY_QS_SAVE_PROGRESS = "qs_save_progress";
private static final boolean DEFAULT_QS_SAVE_PROGRESS = true;
@@ -547,7 +566,7 @@ public class Settings {
}
public static void putFavCat(String[] value) {
AssertUtils.assertEquals(10, value.length);
AssertUtilsKt.assertEquals(10, value.length);
sSettingsPre.edit()
.putString(KEY_FAV_CAT_0, value[0])
.putString(KEY_FAV_CAT_1, value[1])
@@ -578,7 +597,7 @@ public class Settings {
}
public static void putFavCount(int[] count) {
AssertUtils.assertEquals(10, count.length);
AssertUtilsKt.assertEquals(10, count.length);
sSettingsPre.edit()
.putInt(KEY_FAV_COUNT_0, count[0])
.putInt(KEY_FAV_COUNT_1, count[1])
@@ -673,6 +692,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);
}
@@ -1,40 +0,0 @@
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.client;
public class EhCacheKeyFactory {
public static String getThumbKey(long gid) {
return "preview:large:" + gid + ":" + 0; // "thumb:" + gid;
}
public static String getNormalPreviewKey(long gid, int index) {
return "preview:normal:" + gid + ":" + index;
}
public static String getLargePreviewKey(long gid, int index) {
return "preview:large:" + gid + ":" + index;
}
public static String getLargePreviewSetKey(long gid, int index) {
return "large_preview_set:" + gid + ":" + index;
}
public static String getImageKey(long gid, int index) {
return "image:" + gid + ":" + index;
}
}
@@ -13,6 +13,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.client.exception
package com.hippo.ehviewer.client
class Image509Exception : EhException("509")
object EhCacheKeyFactory {
fun getImageKey(gid: Long, index: Int): String {
return "image:$gid:$index"
}
}
@@ -35,12 +35,12 @@ object EhCookieStore : CookieJar {
fun hasSignedIn(): Boolean {
val url = EhUrl.HOST_E.toHttpUrl()
return contains(url, KEY_IPD_MEMBER_ID) &&
contains(url, KEY_IPD_PASS_HASH)
return contains(url, KEY_IPB_MEMBER_ID) &&
contains(url, KEY_IPB_PASS_HASH)
}
const val KEY_IPD_MEMBER_ID = "ipb_member_id"
const val KEY_IPD_PASS_HASH = "ipb_pass_hash"
const val KEY_IPB_MEMBER_ID = "ipb_member_id"
const val KEY_IPB_PASS_HASH = "ipb_pass_hash"
const val KEY_IGNEOUS = "igneous"
private val sTipsCookie: Cookie = Cookie.Builder()
.name(EhConfig.KEY_CONTENT_WARNING)
@@ -183,7 +183,10 @@ object EhCookieStore : CookieJar {
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
for (cookie in cookies) {
addCookie(cookie)
// See https://github.com/Ehviewer-Overhauled/Ehviewer/issues/873
if (cookie.name != "__utmp") {
addCookie(cookie)
}
}
}
@@ -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())
}
}
@@ -20,7 +20,6 @@ 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
import com.hippo.ehviewer.Settings
@@ -41,6 +40,7 @@ import com.hippo.ehviewer.client.parser.GalleryNotAvailableParser
import com.hippo.ehviewer.client.parser.GalleryPageApiParser
import com.hippo.ehviewer.client.parser.GalleryPageParser
import com.hippo.ehviewer.client.parser.GalleryTokenApiParser
import com.hippo.ehviewer.client.parser.HomeParser
import com.hippo.ehviewer.client.parser.ProfileParser
import com.hippo.ehviewer.client.parser.RateGalleryParser
import com.hippo.ehviewer.client.parser.SignInParser
@@ -49,7 +49,8 @@ import com.hippo.ehviewer.client.parser.VoteCommentParser
import com.hippo.ehviewer.client.parser.VoteTagParser
import com.hippo.network.StatusCodeException
import com.hippo.util.ExceptionUtils
import com.hippo.yorozuya.AssertUtils
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import okhttp3.FormBody
import okhttp3.Headers
@@ -85,7 +86,7 @@ object EhEngine {
}
// Check sad panda(without panda)
if (headers != null && "text/html; charset=UTF-8" == headers["Content-Type"] && "0" == headers["Content-Length"]) {
if (headers != null && "text/html; charset=UTF-8" == headers["Content-Type"] && "0" == headers["Content-Length"] && EhUtils.isExHentai) {
throw EhException("Sad Panda\n(without panda)")
}
@@ -99,11 +100,14 @@ object EhEngine {
throw EhException(error)
}
}
if (code >= 400) {
throw StatusCodeException(code)
}
if (e is ParseException) {
if (body != null && !body.contains("<")) {
throw EhException(body)
} else if (TextUtils.isEmpty(body)) {
if (TextUtils.isEmpty(body)) {
throw EhException(GetText.getString(R.string.error_empty_html))
} else if (body != null && !body.contains("<")) {
throw EhException(body)
} else {
if (Settings.getSaveParseErrorBody()) {
AppConfig.saveParseErrorBody(e as ParseException?)
@@ -111,9 +115,6 @@ object EhEngine {
throw EhException(GetText.getString(R.string.error_parse_error))
}
}
if (code >= 400) {
throw StatusCodeException(code)
}
}
@Throws(Throwable::class)
@@ -319,11 +320,7 @@ object EhEngine {
code = response.code
headers = response.headers
body = response.body.string()
val html = EventPaneParser.parse(body)
if (html != null) {
application.showEventPane(html)
}
return GalleryDetailParser.parse(body)
return GalleryDetailParser.parse(body!!)
}
} catch (e: Throwable) {
ExceptionUtils.throwIfFatal(e)
@@ -348,8 +345,8 @@ object EhEngine {
headers = response.headers
body = response.body.string()
return Pair.create(
GalleryDetailParser.parsePreviewSet(body),
GalleryDetailParser.parsePreviewPages(body)
GalleryDetailParser.parsePreviewSet(body!!),
GalleryDetailParser.parsePreviewPages(body!!)
)
}
} catch (e: Throwable) {
@@ -563,7 +560,7 @@ object EhEngine {
gidArray: LongArray,
tokenArray: Array<String?>, dstCat: Int
): Void? {
AssertUtils.assertEquals(gidArray.size, tokenArray.size)
check(gidArray.size == tokenArray.size)
var i = 0
val n = gidArray.size
while (i < n) {
@@ -760,7 +757,7 @@ object EhEngine {
}
@Throws(Throwable::class)
suspend fun getFunds(): ArchiveParser.Funds? {
suspend fun getFunds(): HomeParser.Funds {
val url = EhUrl.URL_FUNDS
Log.d(TAG, url)
val request = EhRequestBuilder(url, null).build()
@@ -773,7 +770,93 @@ object EhEngine {
code = response.code
headers = response.headers
body = response.body.string()
return ArchiveParser.parseFunds(body!!)
return HomeParser.parseFunds(body!!)
}
} catch (e: Throwable) {
ExceptionUtils.throwIfFatal(e)
transformException(code, headers, body, e)
throw e
}
}
@Throws(Throwable::class)
private suspend fun getImageLimitsInternal(): HomeParser.Limits {
val url = EhUrl.URL_HOME
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 HomeParser.parse(body!!)
}
} catch (e: Throwable) {
ExceptionUtils.throwIfFatal(e)
transformException(code, headers, body, e)
throw e
}
}
@Throws(Throwable::class)
suspend fun getImageLimits(): HomeParser.Result = coroutineScope {
val limitsDeferred = async {
getImageLimitsInternal()
}
val fundsDeferred = async {
getFunds()
}
HomeParser.Result(limitsDeferred.await(), fundsDeferred.await())
}
@Throws(Throwable::class)
suspend fun resetImageLimits(): HomeParser.Limits? {
val builder = FormBody.Builder()
.add("act", "limits")
.add("reset", "Reset Limit")
val url = EhUrl.URL_HOME
Log.d(TAG, url)
val request = EhRequestBuilder(url, null)
.post(builder.build())
.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 HomeParser.parseResetLimits(body!!)
}
} catch (e: Throwable) {
ExceptionUtils.throwIfFatal(e)
transformException(code, headers, body, e)
throw e
}
}
@Throws(Throwable::class)
suspend fun getNews(parse: Boolean): String? {
val url = EhUrl.URL_NEWS
val referer = EhUrl.REFERER_E
Log.d(TAG, url)
val request = EhRequestBuilder(url, referer).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 if (parse) EventPaneParser.parse(body!!) else null
}
} catch (e: Throwable) {
ExceptionUtils.throwIfFatal(e)
@@ -830,8 +913,7 @@ object EhEngine {
}
@Throws(Throwable::class)
suspend fun getUConfig(): Void? {
val url = EhUrl.uConfigUrl
suspend fun getUConfig(url: String = EhUrl.uConfigUrl): Void? {
Log.d(TAG, url)
var request = EhRequestBuilder(url, null).build()
var call = okHttpClient.newCall(request)
@@ -1021,7 +1103,7 @@ object EhEngine {
@JvmStatic
@Throws(Throwable::class)
fun getGalleryPage(
suspend fun getGalleryPage(
url: String?,
gid: Long,
token: String?
@@ -1035,7 +1117,7 @@ object EhEngine {
var headers: Headers? = null
var code = -1
try {
call.execute().use { response ->
call.executeAsync().use { response ->
code = response.code
headers = response.headers
body = response.body.string()
@@ -1050,7 +1132,7 @@ object EhEngine {
@JvmStatic
@Throws(Throwable::class)
fun getGalleryPageApi(
suspend fun getGalleryPageApi(
gid: Long,
index: Int,
pToken: String?,
@@ -1080,7 +1162,7 @@ object EhEngine {
var headers: Headers? = null
var code = -1
try {
call.execute().use { response ->
call.executeAsync().use { response ->
code = response.code
headers = response.headers
body = response.body.string()
@@ -39,7 +39,7 @@ object EhFilter {
private val TAG = EhFilter::class.java.simpleName
init {
val list = EhDB.getAllFilter()
val list = EhDB.allFilter
var i = 0
val n = list.size
while (i < n) {
@@ -112,7 +112,7 @@ object EhFilter {
}
@Synchronized
fun triggerFilter(filter: Filter?) {
fun triggerFilter(filter: Filter) {
EhDB.triggerFilter(filter)
}
@@ -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.client;
import com.hippo.okhttp.ChromeRequestBuilder;
public class EhRequestBuilder extends ChromeRequestBuilder {
public EhRequestBuilder(String url, String referer) {
this(url, referer, null);
}
public EhRequestBuilder(String url, String referer, String origin) {
super(url);
if (referer != null) {
addHeader("Referer", referer);
}
if (origin != null) {
addHeader("Origin", origin);
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2019 Hippo Seven
* 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.
@@ -13,14 +13,17 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.client
package com.hippo.util;
import com.hippo.okhttp.ChromeRequestBuilder
public class MutableBoolean {
public boolean value;
public MutableBoolean(boolean value) {
this.value = value;
class EhRequestBuilder @JvmOverloads constructor(
url: String,
referer: String?,
origin: String? = null
) : ChromeRequestBuilder(url) {
init {
referer?.let { addHeader("Referer", it) }
origin?.let { addHeader("Origin", it) }
}
}
@@ -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",
@@ -20,10 +20,9 @@ import com.hippo.ehviewer.AppConfig
import com.hippo.ehviewer.EhApplication
import com.hippo.ehviewer.EhApplication.Companion.okHttpClient
import com.hippo.ehviewer.R
import com.hippo.util.ExceptionUtils
import com.hippo.util.HashCodeUtils
import com.hippo.yorozuya.FileUtils
import com.hippo.yorozuya.IOUtils
import com.hippo.yorozuya.copyToFile
import eu.kanade.tachiyomi.util.lang.launchIO
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.flow.Flow
@@ -37,41 +36,67 @@ import org.json.JSONException
import org.json.JSONObject
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.IOException
import java.nio.charset.StandardCharsets
import java.security.MessageDigest
import java.security.NoSuchAlgorithmException
private typealias TagGroup = Map<String, String>
private typealias TagGroups = Map<String, TagGroup>
object EhTagDatabase {
private lateinit var tags: JSONObject
private lateinit var tagList: ArrayList<Pair<String, String>>
private const val NAMESPACE_PREFIX = "n"
private lateinit var tagGroups: TagGroups
fun isInitialized(): Boolean {
return this::tags.isInitialized && this::tagList.isInitialized
return this::tagGroups.isInitialized
}
private fun JSONObject.toTagGroups(): TagGroups =
keys().asSequence().associateWith { getJSONObject(it).toTagGroup() }
private fun JSONObject.toTagGroup(): TagGroup =
keys().asSequence().associateWith { getString(it) }
private fun updateData(source: BufferedSource) {
try {
val tmpTags = JSONObject(source.readString(StandardCharsets.UTF_8))
val tmpTagList = arrayListOf<Pair<String, String>>()
tmpTags.keys().forEachRemaining { prefix: String ->
val values = tmpTags.optJSONObject(prefix)
values?.let {
it.keys().forEachRemaining { tag: String ->
tmpTagList.add(Pair("$prefix:$tag", values.optString(tag)))
}
}
}
tags = tmpTags
tagList = tmpTagList
tagGroups = JSONObject(source.readString(StandardCharsets.UTF_8)).toTagGroups()
} catch (e: JSONException) {
e.printStackTrace()
}
}
fun getTranslation(prefix: String?, tag: String?): String? {
return tags.optJSONObject(prefix)?.optString(tag)?.trim()?.takeIf { it.isNotEmpty() }
fun getTranslation(prefix: String? = NAMESPACE_PREFIX, tag: String?): String? {
return tagGroups[prefix]?.get(tag)?.trim()?.takeIf { it.isNotEmpty() }
}
private fun internalSuggestFlow(
tags: Map<String, String>,
keyword: String,
translate: Boolean,
exactly: Boolean
): Flow<Pair<String?, String>> = flow {
if (exactly) {
tags[keyword]?.let {
emit(Pair(it.takeIf { translate }, keyword))
}
} else {
if (translate) {
tags.forEach { (tag, hint) ->
if (tag != keyword &&
(tag.containsIgnoreSpace(keyword) || hint.containsIgnoreSpace(keyword))
) {
emit(Pair(hint, tag))
}
}
} else {
tags.keys.forEach { tag ->
if (tag != keyword && tag.containsIgnoreSpace(keyword)) {
emit(Pair(null, tag))
}
}
}
}
}
/* Construct a cold flow for tag database suggestions */
@@ -80,32 +105,32 @@ object EhTagDatabase {
translate: Boolean,
exactly: Boolean = false
): Flow<Pair<String?, String>> = flow {
tagList.forEach {
val tag = it.first
val hint = if (translate) it.second else null
val index = tag.indexOf(':')
if (tag.substring(index + 1).trim() == keyword) {
if (exactly)
emit(Pair(hint, tag))
return@forEach
val keywordPrefix = keyword.substringBefore(':')
val keywordTag = keyword.drop(keywordPrefix.length + 1)
val prefix = namespaceToPrefix(keywordPrefix) ?: keywordPrefix
val tags = tagGroups[prefix.takeIf { keywordTag.isNotEmpty() && it != NAMESPACE_PREFIX }]
tags?.let {
internalSuggestFlow(it, keywordTag, translate, exactly).collect { (hint, tag) ->
emit(Pair(hint, "$prefix:$tag"))
}
if (exactly)
return@forEach
val keywordMatches: Boolean =
if (index == -1 || index >= tag.length - 1 || keyword.length > 2) {
containsIgnoreSpace(tag, keyword)
} else {
containsIgnoreSpace(tag.substring(index + 1), keyword)
} ?: tagGroups.forEach { (prefix, tags) ->
if (prefix != NAMESPACE_PREFIX) {
internalSuggestFlow(tags, keyword, translate, exactly).collect { (hint, tag) ->
emit(Pair(hint, "$prefix:$tag"))
}
} else {
internalSuggestFlow(tags, keyword, translate, exactly).collect { (hint, tag) ->
emit(Pair(hint, "$tag:"))
}
if (keywordMatches || containsIgnoreSpace(hint, keyword)) {
emit(Pair(hint, tag))
}
}
}
private fun containsIgnoreSpace(text: String?, key: String): Boolean {
return text != null && text.replace(" ", "").contains(key.replace(" ", ""))
}
private fun String.removeSpace(): String = replace(" ", "")
private fun String.containsIgnoreSpace(other: String, ignoreCase: Boolean = true): Boolean =
removeSpace().contains(other.removeSpace(), ignoreCase)
private val NAMESPACE_TO_PREFIX = HashMap<String, String>().also {
it["artist"] = "a"
@@ -170,22 +195,21 @@ object EhTagDatabase {
private fun save(client: OkHttpClient, url: String, file: File): Boolean {
val request: Request = Request.Builder().url(url).build()
val call = client.newCall(request)
try {
runCatching {
call.execute().use { response ->
if (!response.isSuccessful) {
return false
}
val body = response.body
body.byteStream()
.use { ins -> FileOutputStream(file).use { os -> IOUtils.copy(ins, os) } }
response.body.use {
it.copyToFile(file)
}
return true
}
} catch (t: Throwable) {
FileUtils.delete(file)
t.printStackTrace()
ExceptionUtils.throwIfFatal(t)
return false
}.onFailure {
file.delete()
it.printStackTrace()
}
return false
}
@OptIn(DelicateCoroutinesApi::class)
@@ -44,11 +44,15 @@ object EhUrl {
const val URL_SIGN_IN = "https://forums.e-hentai.org/index.php?act=Login"
const val URL_REGISTER = "https://forums.e-hentai.org/index.php?act=Reg&CODE=00"
const val URL_FORUMS = "https://forums.e-hentai.org/"
const val URL_FUNDS = "https://e-hentai.org/exchange.php?t=gp"
const val URL_FUNDS = HOST_E + "exchange.php?t=gp"
const val URL_HOME = HOST_E + "home.php"
const val URL_NEWS = HOST_E + "news.php"
const val REFERER_EX = "https://$DOMAIN_EX"
const val ORIGIN_EX = REFERER_EX
const val REFERER_E = "https://$DOMAIN_E"
const val ORIGIN_E = REFERER_E
const val URL_PREFIX_THUMB_E = "https://ehgt.org/"
const val URL_PREFIX_THUMB_EX = "https://exhentai.org/t/"
val host: String
get() = when (Settings.getGallerySite()) {
@@ -161,4 +165,12 @@ object EhUrl {
fun getTagDefinitionUrl(tag: String): String {
return "https://ehwiki.org/wiki/" + tag.replace(' ', '_')
}
fun getThumbUrlPrefix(): String {
return when (Settings.getGallerySite()) {
SITE_E -> URL_PREFIX_THUMB_E
SITE_EX -> URL_PREFIX_THUMB_EX
else -> URL_PREFIX_THUMB_E
}
}
}
@@ -1,190 +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.putGallerySite(EhUrl.SITE_E);
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,162 @@
/*
* 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 }
val isExHentai: Boolean
get() = Settings.getGallerySite() == EhUrl.SITE_EX
@JvmStatic
fun getCategory(type: String?): Int {
for (entry in CATEGORY_STRINGS) {
for (str in entry.first)
if (str.equals(type, ignoreCase = true))
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)
Settings.putSelectSite(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
}.orEmpty()
}
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
}
}
}
@@ -15,6 +15,8 @@
*/
package com.hippo.ehviewer.client.data
import com.hippo.ehviewer.client.data.GalleryInfo.Companion.S_LANGS
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
@Parcelize
@@ -70,6 +72,33 @@ class GalleryDetail(
@JvmField
var previewPages: Int = 0,
@IgnoredOnParcel
@JvmField
var previewSet: PreviewSet? = null,
) : AbstractGalleryInfo by galleryInfo, GalleryInfo
) : AbstractGalleryInfo by galleryInfo, GalleryInfo {
override fun generateSLang() {
val index = LANGUAGES.indexOf(language)
if (index != -1) {
simpleLanguage = S_LANGS[index]
}
}
companion object {
private val LANGUAGES = arrayOf(
"English",
"Chinese",
"Spanish",
"Korean",
"Russian",
"French",
"Portuguese",
"Thai",
"German",
"Italian",
"Vietnamese",
"Polish",
"Hungarian",
"Dutch"
)
}
}
@@ -21,8 +21,6 @@ import kotlinx.parcelize.Parcelize
@Parcelize
class GalleryPreview(
@JvmField
var imageKey: String? = null,
@JvmField
var imageUrl: String? = null,
@JvmField
@@ -40,6 +38,6 @@ class GalleryPreview(
) : Parcelable {
fun load(view: LoadImageView) {
view.setClip(offsetX, offsetY, clipWidth, clipHeight)
view.load(imageKey!!, imageUrl!!)
view.load(imageUrl!!)
}
}
@@ -23,16 +23,4 @@ class GalleryTagGroup(
private val mTagList: ArrayList<String> = arrayListOf(),
@JvmField
var groupName: String? = null
) : Parcelable {
fun addTag(tag: String) {
mTagList.add(tag)
}
fun size(): Int {
return mTagList.size
}
fun getTagAt(position: Int): String {
return mTagList[position]
}
}
) : Parcelable, MutableList<String> by mTagList
@@ -15,12 +15,9 @@
*/
package com.hippo.ehviewer.client.data
import com.hippo.ehviewer.client.EhCacheKeyFactory
import com.hippo.widget.LoadImageView
import com.hippo.yorozuya.collect.IntList
import kotlinx.parcelize.Parcelize
@Parcelize
class LargePreviewSet(
private val mPositionList: IntList = IntList(),
private val mImageUrlList: ArrayList<String> = ArrayList(),
@@ -48,7 +45,6 @@ class LargePreviewSet(
override fun getGalleryPreview(gid: Long, index: Int): GalleryPreview {
val galleryPreview = GalleryPreview()
galleryPreview.position = mPositionList[index]
galleryPreview.imageKey = EhCacheKeyFactory.getLargePreviewKey(gid, galleryPreview.position)
galleryPreview.imageUrl = mImageUrlList[index]
galleryPreview.pageUrl = mPageUrlList[index]
return galleryPreview
@@ -57,7 +53,6 @@ class LargePreviewSet(
override fun load(view: LoadImageView, gid: Long, index: Int) {
view.resetClip()
view.load(
EhCacheKeyFactory.getLargePreviewKey(gid, mPositionList[index]),
mImageUrlList[index]
)
}
@@ -283,13 +283,6 @@ data class ListUrlBuilder(
if (this.category != EhUtils.NONE) {
ub.addQuery("f_cats", category.inv() and EhConfig.ALL_CATEGORY)
}
// Search key
mKeyword?.run {
val keyword = trim { it <= ' ' }
if (keyword.isNotEmpty()) {
ub.addQuery("f_search", encodeUTF8(this))
}
}
mSHash?.let {
ub.addQuery("f_shash", it)
}
@@ -302,6 +295,19 @@ data class ListUrlBuilder(
mNext?.let {
ub.addQuery("next", it)
}
// Search key
// the settings of ub:UrlBuilder may be overwritten by following Advance search
StringUtils.split(mKeyword, '|')?.forEachIndexed { idx, keyword ->
val keyword = keyword.trim { it <= ' ' }
when (idx) {
0 -> keyword.takeIf { it.isNotEmpty() }?.let { ub.addQuery("f_search", encodeUTF8(it)) }
else -> keyword.indexOf(':').takeIf { it >= 0 }?.run {
val key = keyword.substring(0, this).trim { it <= ' ' }
val value = keyword.substring(this + 1).trim { it <= ' ' }
ub.addQuery(key, encodeUTF8(value))
}
}
}
// Advance search
if (advanceSearch != -1) {
ub.addQuery("advsearch", "1")
@@ -17,12 +17,9 @@ package com.hippo.ehviewer.client.data
import com.hippo.widget.LoadImageView
import com.hippo.yorozuya.collect.IntList
import kotlinx.parcelize.Parcelize
@Parcelize
class NormalPreviewSet(
private var mPositionList: IntList = IntList(),
private var mImageKeyList: ArrayList<String> = ArrayList(),
private var mImageUrlList: ArrayList<String> = ArrayList(),
private var mOffsetXList: IntList = IntList(),
private var mOffsetYList: IntList = IntList(),
@@ -31,21 +28,11 @@ class NormalPreviewSet(
private var mPageUrlList: ArrayList<String> = ArrayList()
) : PreviewSet() {
private fun getImageKey(imageUrl: String): String {
val index = imageUrl.indexOf('/')
return if (index >= 0) {
imageUrl.substring(index + 1)
} else {
imageUrl
}
}
fun addItem(
position: Int, imageUrl: String, xOffset: Int, yOffset: Int, width: Int,
height: Int, pageUrl: String
) {
mPositionList.add(position)
mImageKeyList.add(getImageKey(imageUrl))
mImageUrlList.add(imageUrl)
mOffsetXList.add(xOffset)
mOffsetYList.add(yOffset)
@@ -55,7 +42,7 @@ class NormalPreviewSet(
}
override fun size(): Int {
return mPositionList.size()
return mPositionList.size
}
override fun getPosition(index: Int): Int {
@@ -69,7 +56,6 @@ class NormalPreviewSet(
override fun getGalleryPreview(gid: Long, index: Int): GalleryPreview {
val galleryPreview = GalleryPreview()
galleryPreview.position = mPositionList[index]
galleryPreview.imageKey = mImageKeyList[index]
galleryPreview.imageUrl = mImageUrlList[index]
galleryPreview.pageUrl = mPageUrlList[index]
galleryPreview.offsetX = mOffsetXList[index]
@@ -84,6 +70,6 @@ class NormalPreviewSet(
mOffsetXList[index], mOffsetYList[index],
mClipWidthList[index], mClipHeightList[index]
)
view.load(mImageKeyList[index], mImageUrlList[index])
view.load(mImageUrlList[index])
}
}
@@ -15,10 +15,9 @@
*/
package com.hippo.ehviewer.client.data
import android.os.Parcelable
import com.hippo.widget.LoadImageView
abstract class PreviewSet : Parcelable {
abstract class PreviewSet {
abstract fun size(): Int
abstract fun getPosition(index: Int): Int
abstract fun getPageUrlAt(index: Int): String
@@ -15,113 +15,71 @@
*/
package com.hippo.ehviewer.client.parser
import com.hippo.ehviewer.R
import com.hippo.ehviewer.client.exception.NoHAtHClientException
import org.jsoup.Jsoup
import java.util.function.IntFunction
import java.util.regex.Pattern
object ArchiveParser {
private val PATTERN_ARCHIVE_URL =
Pattern.compile("<strong>(.*)</strong>.*<a href=\"([^\"]*)\">Click Here To Start Downloading</a>")
Regex("<strong>(.*)</strong>.*<a href=\"([^\"]*)\">Click Here To Start Downloading</a>")
private val PATTERN_CURRENT_FUNDS =
Pattern.compile("<p>([\\d,]+) GP \\[[^]]*] &nbsp; ([\\d,]+) Credits \\[[^]]*]</p>")
private val PATTERN_FUNDS =
Pattern.compile("Available: ([\\d,]+) Credits.*Available: ([\\d,]+) kGP", Pattern.DOTALL)
Regex("<p>([\\d,]+) GP \\[[^]]*] &nbsp; ([\\d,]+) Credits \\[[^]]*]</p>")
private val PATTERN_HATH_FORM =
Pattern.compile("<form id=\"hathdl_form\" action=\"[^\"]*?or=([^=\"]*?)\" method=\"post\">")
Regex("<form id=\"hathdl_form\" action=\"[^\"]*?or=([^=\"]*?)\" method=\"post\">")
private val PATTERN_HATH_ARCHIVE =
Pattern.compile("<p><a href=\"[^\"]*\" onclick=\"return do_hathdl\\('([0-9]+|org)'\\)\">([^<]+)</a></p>\\s*<p>([\\w. ]+)</p>\\s*<p>([\\w. ]+)</p>")
Regex("<p><a href=\"[^\"]*\" onclick=\"return do_hathdl\\('([0-9]+|org)'\\)\">([^<]+)</a></p>\\s*<p>([\\w. ]+)</p>\\s*<p>([\\w. ]+)</p>")
private val PATTERN_NEED_HATH_CLIENT =
Pattern.compile("You must have a H@H client assigned to your account to use this feature\\.")
Regex("You must have a H@H client assigned to your account to use this feature\\.")
fun parse(body: String): Result? {
var m = PATTERN_HATH_FORM.matcher(body)
if (!m.find()) {
return null
}
val paramOr = m.group(1)
val m = PATTERN_HATH_FORM.find(body) ?: return null
val paramOr = m.groupValues[1]
val archiveList = ArrayList<Archive>()
val d = Jsoup.parse(body)
val es = d.select("#db>div>div")
for (e in es) {
if (e.childrenSize() > 0 && !e.attr("style").contains("color:#CCCCCC")) {
try {
val res = e.selectFirst("form>input")!!.attr("value")
val name = e.selectFirst("form>div>input")!!.attr("value")
val size = e.selectFirst("p>strong")!!.text()
val cost = e.selectFirst("div>strong")!!.text().replace(",", "")
val item = Archive(res, name, size, cost, false)
archiveList.add(item)
} catch (ex: NullPointerException) {
ex.printStackTrace()
Jsoup.parse(body).select("#db>div>div").forEach { element ->
if (element.childrenSize() > 0 && !element.attr("style").contains("color:#CCCCCC")) {
runCatching {
val res = element.selectFirst("form>input")!!.attr("value")
val name = element.selectFirst("form>div>input")!!.attr("value")
val size = element.selectFirst("p>strong")!!.text()
val cost = element.selectFirst("div>strong")!!.text().replace(",", "")
Archive(res, name, size, cost, false)
}.onSuccess {
archiveList.add(it)
}.onFailure {
it.printStackTrace()
}
}
}
m = PATTERN_HATH_ARCHIVE.matcher(body)
while (m.find()) {
val res = ParserUtils.trim(m.group(1))
val name = ParserUtils.trim(m.group(2))
val size = ParserUtils.trim(m.group(3))
val cost = ParserUtils.trim(m.group(4))
PATTERN_HATH_ARCHIVE.findAll(body).forEach { matchResult ->
val (res, name, size, cost) = matchResult.groupValues.slice(1..4)
.map { ParserUtils.trim(it) }
val item = Archive(res, name, size, cost, true)
archiveList.add(item)
}
var funds: Funds? = null
m = PATTERN_CURRENT_FUNDS.matcher(body)
if (m.find()) {
val fundsGP = ParserUtils.parseInt(m.group(1), 0)
val fundsC = ParserUtils.parseInt(m.group(2), 0)
funds = Funds(fundsGP, fundsC)
val result = Result(paramOr, archiveList, null)
PATTERN_CURRENT_FUNDS.find(body)?.groupValues?.run {
val fundsGP = ParserUtils.parseInt(get(1), 0)
val fundsC = ParserUtils.parseInt(get(2), 0)
val funds = HomeParser.Funds(fundsGP, fundsC)
result.funds = funds
}
return Result(paramOr, archiveList, funds)
return result
}
@Throws(NoHAtHClientException::class)
fun parseArchiveUrl(body: String): String? {
val m = PATTERN_NEED_HATH_CLIENT.matcher(body)
if (m.find()) {
throw NoHAtHClientException("No H@H client")
}
val d = Jsoup.parse(body)
val a = d.selectFirst("#continue>a[href]")
return if (a != null) {
a.attr("href") + "?start=1"
} else null
if (PATTERN_NEED_HATH_CLIENT.containsMatchIn(body)) throw NoHAtHClientException("No H@H client")
return Jsoup.parse(body).selectFirst("#continue>a[href]")
?.let { it.attr("href") + "?start=1" }
// TODO: Check more errors
}
fun parseFunds(body: String): Funds? {
val m = PATTERN_FUNDS.matcher(body)
return if (m.find()) {
val fundsC = ParserUtils.parseInt(m.group(1), 0)
val fundsGP = ParserUtils.parseInt(m.group(2), 0) * 1000
return Funds(fundsGP, fundsC)
} else null
}
class Archive(
val res: String,
val name: String,
val size: String,
val cost: String,
val isHAtH: Boolean
) {
fun format(getString: IntFunction<String?>): String {
return if (isHAtH) {
val costStr = if (cost == "Free") getString.apply(R.string.archive_free) else cost
"[H@H] $name [$size] [$costStr]"
} else {
val nameStr =
getString.apply(if (res == "org") R.string.archive_original else R.string.archive_resample)
val costStr = if (cost == "Free!") getString.apply(R.string.archive_free) else cost
"$nameStr [$size] [$costStr]"
}
}
}
)
class Funds(var fundsGP: Int, var fundsC: Int)
class Result(val paramOr: String?, val archiveList: List<Archive>, var funds: Funds?)
}
class Result(val paramOr: String?, val archiveList: List<Archive>, var funds: HomeParser.Funds?)
}
@@ -1,45 +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.client.parser;
import com.hippo.util.ExceptionUtils;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
public class EventPaneParser {
public static String parse(String body) {
String event = null;
try {
Document d = Jsoup.parse(body);
Element eventpane = d.getElementById("eventpane");
if (eventpane != null) {
event = eventpane.html();
}
} catch (Throwable e) {
ExceptionUtils.throwIfFatal(e);
e.printStackTrace();
}
return event;
}
}
@@ -15,23 +15,12 @@
* You should have received a copy of the GNU General Public License along with EhViewer.
* If not, see <https://www.gnu.org/licenses/>.
*/
package com.hippo.ehviewer.client
package com.hippo.ehviewer.client.parser
import android.annotation.SuppressLint
import java.security.cert.X509Certificate
import javax.net.ssl.X509TrustManager
import org.jsoup.Jsoup
@SuppressLint("CustomX509TrustManager")
object EhX509TrustManager : X509TrustManager {
@SuppressLint("TrustAllX509TrustManager")
override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) {
}
@SuppressLint("TrustAllX509TrustManager")
override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) {
}
override fun getAcceptedIssuers(): Array<X509Certificate?> {
return arrayOfNulls(0)
object EventPaneParser {
fun parse(body: String): String? {
return Jsoup.parse(body).getElementById("eventpane")?.html()
}
}
@@ -23,7 +23,7 @@ import com.hippo.ehviewer.client.exception.EhException;
import com.hippo.ehviewer.client.exception.ParseException;
import com.hippo.util.ExceptionUtils;
import com.hippo.util.JsoupUtils;
import com.hippo.yorozuya.AssertUtils;
import com.hippo.yorozuya.AssertUtilsKt;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
@@ -46,7 +46,7 @@ public class FavoritesParser {
//noinspection ConstantConditions
Elements fps = ido.getElementsByClass("fp");
// Last one is "fp fps"
AssertUtils.assertEquals(11, fps.size());
AssertUtilsKt.assertEquals(11, fps.size());
for (int i = 0; i < 10; i++) {
Element fp = fps.get(i);
@@ -17,7 +17,7 @@
package com.hippo.ehviewer.client.parser;
import com.hippo.ehviewer.client.EhUrl;
import com.hippo.ehviewer.client.exception.ParseException;
import com.hippo.ehviewer.client.exception.EhException;
import com.hippo.util.ExceptionUtils;
import org.jsoup.Jsoup;
@@ -26,7 +26,7 @@ import org.jsoup.nodes.Element;
public class ForumsParser {
public static String parse(String body) throws ParseException {
public static String parse(String body) throws EhException {
try {
Document d = Jsoup.parse(body, EhUrl.URL_FORUMS);
Element userlinks = d.getElementById("userlinks");
@@ -34,7 +34,7 @@ public class ForumsParser {
return child.attr("href");
} catch (Throwable e) {
ExceptionUtils.throwIfFatal(e);
throw new ParseException("Parse forums error", body);
throw new EhException("Not logged in");
}
}
}
@@ -1,695 +0,0 @@
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.client.parser;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.hippo.ehviewer.EhDB;
import com.hippo.ehviewer.client.EhFilter;
import com.hippo.ehviewer.client.EhUtils;
import com.hippo.ehviewer.client.data.BaseGalleryInfo;
import com.hippo.ehviewer.client.data.GalleryComment;
import com.hippo.ehviewer.client.data.GalleryCommentList;
import com.hippo.ehviewer.client.data.GalleryDetail;
import com.hippo.ehviewer.client.data.GalleryInfo;
import com.hippo.ehviewer.client.data.GalleryTagGroup;
import com.hippo.ehviewer.client.data.LargePreviewSet;
import com.hippo.ehviewer.client.data.NormalPreviewSet;
import com.hippo.ehviewer.client.data.PreviewSet;
import com.hippo.ehviewer.client.exception.EhException;
import com.hippo.ehviewer.client.exception.OffensiveException;
import com.hippo.ehviewer.client.exception.ParseException;
import com.hippo.ehviewer.client.exception.PiningException;
import com.hippo.util.ExceptionUtils;
import com.hippo.util.JsoupUtils;
import com.hippo.util.MutableBoolean;
import com.hippo.yorozuya.NumberUtils;
import com.hippo.yorozuya.StringUtils;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.nodes.Node;
import org.jsoup.select.Elements;
import org.jsoup.select.NodeTraversor;
import org.jsoup.select.NodeVisitor;
import java.time.Instant;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class GalleryDetailParser {
private static final Pattern PATTERN_ERROR = Pattern.compile("<div class=\"d\">\n<p>([^<]+)</p>");
private static final Pattern PATTERN_DETAIL = Pattern.compile("var gid = (\\d+);.+?var token = \"([a-f0-9]+)\";.+?var apiuid = ([\\-\\d]+);.+?var apikey = \"([a-f0-9]+)\";", Pattern.DOTALL);
private static final Pattern PATTERN_TORRENT = Pattern.compile("<a[^<>]*onclick=\"return popUp\\('([^']+)'[^)]+\\)\">Torrent Download[^<]+(\\d+)[^<]+</a");
private static final Pattern PATTERN_ARCHIVE = Pattern.compile("<a[^<>]*onclick=\"return popUp\\('([^']+)'[^)]+\\)\">Archive Download</a>");
private static final Pattern PATTERN_COVER = Pattern.compile("width:(\\d+)px; height:(\\d+)px.+?url\\((.+?)\\)");
private static final Pattern PATTERN_PAGES = Pattern.compile("<tr><td[^<>]*>Length:</td><td[^<>]*>([\\d,]+) pages</td></tr>");
private static final Pattern PATTERN_PREVIEW_PAGES = Pattern.compile("<td[^>]+><a[^>]+>([\\d,]+)</a></td><td[^>]+>(?:<a[^>]+>)?&gt;(?:</a>)?</td>");
private static final Pattern PATTERN_NORMAL_PREVIEW = Pattern.compile("<div class=\"gdtm\"[^<>]*><div[^<>]*width:(\\d+)[^<>]*height:(\\d+)[^<>]*\\((.+?)\\)[^<>]*-(\\d+)px[^<>]*><a[^<>]*href=\"(.+?)\"[^<>]*><img alt=\"([\\d,]+)\"");
private static final Pattern PATTERN_LARGE_PREVIEW = Pattern.compile("<div class=\"gdtl\".+?<a href=\"(.+?)\"><img alt=\"([\\d,]+)\".+?src=\"(.+?)\"");
private static final Pattern PATTERN_NEWER_DATE = Pattern.compile(", added (.+?)<br />");
private static final Pattern PATTERN_FAVORITE_SLOT = Pattern.compile("/fav.png\\); background-position:0px -(\\d+)px");
private static final GalleryTagGroup[] EMPTY_GALLERY_TAG_GROUP_ARRAY = new GalleryTagGroup[0];
private static final GalleryCommentList EMPTY_GALLERY_COMMENT_ARRAY = new GalleryCommentList(new GalleryComment[0], false);
private static final DateTimeFormatter WEB_COMMENT_DATE_FORMAT = DateTimeFormatter
.ofPattern("dd MMMM yyyy, HH:mm", Locale.US).withZone(ZoneOffset.UTC);
private static final String OFFENSIVE_STRING =
"<p>(And if you choose to ignore this warning, you lose all rights to complain about it in the future.)</p>";
private static final String PINING_STRING =
"<p>This gallery is pining for the fjords.</p>";
private static final String[] LANGUAGES = {
"English",
"Chinese",
"Spanish",
"Korean",
"Russian",
"French",
"Portuguese",
"Thai",
"German",
"Italian",
"Vietnamese",
"Polish",
"Hungarian",
"Dutch",
};
public static GalleryDetail parse(String body) throws EhException {
if (body.contains(OFFENSIVE_STRING)) {
throw new OffensiveException();
}
if (body.contains(PINING_STRING)) {
throw new PiningException();
}
// Error info
Matcher m = PATTERN_ERROR.matcher(body);
if (m.find()) {
throw new EhException(m.group(1));
}
GalleryDetail galleryDetail = new GalleryDetail();
Document document = Jsoup.parse(body);
parseDetail(galleryDetail, document, body);
galleryDetail.tags = parseTagGroups(document);
galleryDetail.comments = parseComments(document);
galleryDetail.previewPages = parsePreviewPages(document, body);
galleryDetail.previewSet = parsePreviewSet(document, body);
// Generate simpleLanguage for local favorites
for (int i = 0; i < LANGUAGES.length; i++) {
if (galleryDetail.language.equals(LANGUAGES[i])) {
galleryDetail.setSimpleLanguage(GalleryInfo.Companion.getS_LANGS()[i]);
break;
}
}
return galleryDetail;
}
@SuppressWarnings("ConstantConditions")
private static void parseDetail(GalleryDetail gd, Document d, String body) throws ParseException {
Matcher matcher = PATTERN_DETAIL.matcher(body);
if (matcher.find()) {
gd.setGid(NumberUtils.parseLongSafely(matcher.group(1), -1L));
gd.setToken(matcher.group(2));
gd.apiUid = NumberUtils.parseLongSafely(matcher.group(3), -1L);
gd.apiKey = matcher.group(4);
} else {
throw new ParseException("Can't parse gallery detail", body);
}
if (gd.getGid() == -1L) {
throw new ParseException("Can't parse gallery detail", body);
}
matcher = PATTERN_TORRENT.matcher(body);
if (matcher.find()) {
gd.torrentUrl = StringUtils.unescapeXml(StringUtils.trim(matcher.group(1)));
gd.torrentCount = NumberUtils.parseIntSafely(matcher.group(2), 0);
} else {
gd.torrentCount = 0;
gd.torrentUrl = "";
}
matcher = PATTERN_ARCHIVE.matcher(body);
if (matcher.find()) {
gd.archiveUrl = StringUtils.unescapeXml(StringUtils.trim(matcher.group(1)));
} else {
gd.archiveUrl = "";
}
try {
Element gm = JsoupUtils.getElementByClass(d, "gm");
// Thumb url
Element gd1 = gm.getElementById("gd1");
try {
gd.setThumb(parseCoverStyle(StringUtils.trim(gd1.child(0).attr("style"))));
} catch (Throwable e) {
ExceptionUtils.throwIfFatal(e);
gd.setThumb("");
}
// Title
Element gn = gm.getElementById("gn");
if (null != gn) {
gd.setTitle(StringUtils.trim(gn.text()));
} else {
gd.setTitle("");
}
// Jpn title
Element gj = gm.getElementById("gj");
if (null != gj) {
gd.setTitleJpn(StringUtils.trim(gj.text()));
} else {
gd.setTitleJpn("");
}
// Category
Element gdc = gm.getElementById("gdc");
try {
Element ce = JsoupUtils.getElementByClass(gdc, "cn");
if (ce == null) {
ce = JsoupUtils.getElementByClass(gdc, "cs");
}
gd.setCategory(EhUtils.getCategory(ce.text()));
} catch (Throwable e) {
ExceptionUtils.throwIfFatal(e);
gd.setCategory(EhUtils.UNKNOWN);
}
// Uploader
Element gdn = gm.getElementById("gdn");
if (null != gdn) {
gd.setDisowned(gdn.attr("style").contains("opacity:0.5"));
gd.setUploader(StringUtils.trim(gdn.text()));
} else {
gd.setUploader("");
}
Element gdd = gm.getElementById("gdd");
gd.setPosted("");
gd.parent = "";
gd.visible = "";
gd.size = "";
gd.setPages(0);
gd.favoriteCount = 0;
Elements es = gdd.child(0).child(0).children();
for (int i = 0, n = es.size(); i < n; i++) {
parseDetailInfo(gd, es.get(i));
}
// Rating count
Element rating_count = gm.getElementById("rating_count");
if (null != rating_count) {
gd.ratingCount = NumberUtils.parseIntSafely(
StringUtils.trim(rating_count.text()), 0);
} else {
gd.ratingCount = 0;
}
// Rating
Element rating_label = gm.getElementById("rating_label");
if (null != rating_label) {
String ratingStr = StringUtils.trim(rating_label.text());
if ("Not Yet Rated".equals(ratingStr)) {
gd.setRating(-1.0f);
} else {
int index = ratingStr.indexOf(' ');
if (index == -1 || index >= ratingStr.length()) {
gd.setRating(0f);
} else {
gd.setRating(NumberUtils.parseFloatSafely(ratingStr.substring(index + 1), 0f));
}
}
} else {
gd.setRating(-1.0f);
}
// isFavorited
Element gdf = gm.getElementById("gdf");
gd.isFavorited = false;
if (gdf != null) {
final String favoriteName = StringUtils.trim(gdf.text());
if (favoriteName.equals("Add to Favorites")) {
gd.setFavoriteName(null);
} else {
gd.isFavorited = true;
gd.setFavoriteName(StringUtils.trim(gdf.text()));
matcher = PATTERN_FAVORITE_SLOT.matcher(body);
if (matcher.find()) {
gd.setFavoriteSlot((NumberUtils.parseIntSafely(matcher.group(1), 2) - 2) / 19);
}
}
}
if (gd.getFavoriteSlot() == -2 && EhDB.containLocalFavorites(gd.getGid())) {
gd.setFavoriteSlot(-1);
}
} catch (Throwable e) {
ExceptionUtils.throwIfFatal(e);
throw new ParseException("Can't parse gallery detail", body);
}
// newer version
try {
Element gnd = d.getElementById("gnd");
if (gnd != null) {
matcher = PATTERN_NEWER_DATE.matcher(body);
ArrayList<String> dates = new ArrayList<>();
while (matcher.find()) {
dates.add(matcher.group(1));
}
Elements elements = gnd.select("a");
for (int i = 0; i < elements.size(); i++) {
Element element = elements.get(i);
GalleryInfo gi = new BaseGalleryInfo();
GalleryDetailUrlParser.Result result = GalleryDetailUrlParser.parse(element.attr("href"));
if (result != null) {
gi.setGid(result.gid);
gi.setToken(result.token);
gi.setTitle(StringUtils.trim(element.text()));
gi.setPosted(dates.get(i));
gd.newerVersions.add(gi);
}
}
}
} catch (Throwable e) {
e.printStackTrace();
}
}
// width:250px; height:356px; background:transparent url(https://exhentai.org/t/fe/1f/fe1fcfa9bf8fba2f03982eda0aa347cc9d6a6372-145921-1050-1492-jpg_250.jpg) 0 0 no-repeat
private static String parseCoverStyle(String str) {
Matcher matcher = PATTERN_COVER.matcher(str);
if (matcher.find()) {
return EhUtils.handleThumbUrlResolution(matcher.group(3));
} else {
return "";
}
}
private static void parseDetailInfo(GalleryDetail gd, Element e) {
Elements es = e.children();
if (es.size() < 2) {
return;
}
String key = StringUtils.trim(es.get(0).text());
String value = StringUtils.trim(es.get(1).ownText());
if (key.startsWith("Posted")) {
gd.setPosted(value);
} else if (key.startsWith("Parent")) {
Element a = es.get(1).children().first();
if (a != null) {
gd.parent = a.attr("href");
}
} else if (key.startsWith("Visible")) {
gd.visible = value;
} else if (key.startsWith("Language")) {
gd.language = value;
} else if (key.startsWith("File Size")) {
gd.size = value;
} else if (key.startsWith("Length")) {
int index = value.indexOf(' ');
if (index >= 0) {
gd.setPages(NumberUtils.parseIntSafely(value.substring(0, index), 1));
} else {
gd.setPages(1);
}
} else if (key.startsWith("Favorited")) {
switch (value) {
case "Never" -> gd.favoriteCount = 0;
case "Once" -> gd.favoriteCount = 1;
default -> {
int index = value.indexOf(' ');
if (index == -1) {
gd.favoriteCount = 0;
} else {
gd.favoriteCount = NumberUtils.parseIntSafely(value.substring(0, index), 0);
}
}
}
}
}
@Nullable
private static GalleryTagGroup parseTagGroup(Element element) {
try {
GalleryTagGroup group = new GalleryTagGroup();
String nameSpace = element.child(0).text();
// Remove last ':'
nameSpace = nameSpace.substring(0, nameSpace.length() - 1);
group.groupName = nameSpace;
Elements tags = element.child(1).children();
for (int i = 0, n = tags.size(); i < n; i++) {
String tag = tags.get(i).text();
// Sometimes parody tag is followed with '|' and english translate, just remove them
int index = tag.indexOf('|');
if (index >= 0) {
tag = tag.substring(0, index).trim();
}
group.addTag(tag);
}
return group.size() > 0 ? group : null;
} catch (Throwable e) {
ExceptionUtils.throwIfFatal(e);
e.printStackTrace();
return null;
}
}
/**
* Parse tag groups with html parser
*/
@NonNull
public static GalleryTagGroup[] parseTagGroups(Document document) {
try {
Element taglist = document.getElementById("taglist");
assert taglist != null;
Elements tagGroups = taglist.child(0).child(0).children();
return parseTagGroups(tagGroups);
} catch (Throwable e) {
ExceptionUtils.throwIfFatal(e);
e.printStackTrace();
return EMPTY_GALLERY_TAG_GROUP_ARRAY;
}
}
@NonNull
public static GalleryTagGroup[] parseTagGroups(Elements trs) {
try {
List<GalleryTagGroup> list = new ArrayList<>(trs.size());
for (int i = 0, n = trs.size(); i < n; i++) {
GalleryTagGroup group = parseTagGroup(trs.get(i));
if (null != group) {
list.add(group);
}
}
return list.toArray(new GalleryTagGroup[0]);
} catch (Throwable e) {
ExceptionUtils.throwIfFatal(e);
e.printStackTrace();
return EMPTY_GALLERY_TAG_GROUP_ARRAY;
}
}
@Nullable
@SuppressWarnings("ConstantConditions")
public static GalleryComment parseComment(Element element) {
try {
GalleryComment comment = new GalleryComment();
// Id
Element a = element.previousElementSibling();
String name = a.attr("name");
comment.id = Integer.parseInt(StringUtils.trim(name).substring(1));
// Editable, vote up and vote down
Element c4 = JsoupUtils.getElementByClass(element, "c4");
if (null != c4) {
if ("Uploader Comment".equals(c4.text())) {
comment.uploader = true;
}
for (Element e : c4.children()) {
switch (e.text()) {
case "Vote+" -> {
comment.voteUpAble = true;
comment.voteUpEd = !StringUtils.trim(e.attr("style")).isEmpty();
}
case "Vote-" -> {
comment.voteDownAble = true;
comment.voteDownEd = !StringUtils.trim(e.attr("style")).isEmpty();
}
case "Edit" -> comment.editable = true;
}
}
}
// Vote state
Element c7 = JsoupUtils.getElementByClass(element, "c7");
if (null != c7) {
comment.voteState = StringUtils.trim(c7.text());
}
// Score
Element c5 = JsoupUtils.getElementByClass(element, "c5");
if (null != c5) {
Elements es = c5.children();
if (!es.isEmpty()) {
comment.score = NumberUtils.parseIntSafely(StringUtils.trim(es.get(0).text()), 0);
}
}
// time
Element c3 = JsoupUtils.getElementByClass(element, "c3");
String temp = c3.ownText();
temp = temp.substring("Posted on ".length(), temp.length() - " by:".length());
comment.time = Instant.from(WEB_COMMENT_DATE_FORMAT.parse(temp)).toEpochMilli();
// user
comment.user = c3.child(0).text();
// comment
comment.comment = JsoupUtils.getElementByClass(element, "c6").html();
// filter comment
if (!comment.uploader) {
EhFilter sEhFilter = EhFilter.INSTANCE;
if (!sEhFilter.filterCommenter(comment.user) || !sEhFilter.filterComment(comment.comment)) {
return null;
}
}
// last edited
Element c8 = JsoupUtils.getElementByClass(element, "c8");
if (c8 != null) {
Element e = c8.children().first();
if (e != null) {
comment.lastEdited = Instant.from(WEB_COMMENT_DATE_FORMAT.parse(temp)).toEpochMilli();
}
}
return comment;
} catch (Throwable e) {
ExceptionUtils.throwIfFatal(e);
e.printStackTrace();
return null;
}
}
/**
* Parse comments with html parser
*/
@NonNull
public static GalleryCommentList parseComments(Document document) {
try {
Element cdiv = document.getElementById("cdiv");
assert cdiv != null;
Elements c1s = cdiv.getElementsByClass("c1");
List<GalleryComment> list = new ArrayList<>(c1s.size());
for (int i = 0, n = c1s.size(); i < n; i++) {
GalleryComment comment = parseComment(c1s.get(i));
if (null != comment) {
list.add(comment);
}
}
Element chd = cdiv.getElementById("chd");
MutableBoolean hasMore = new MutableBoolean(false);
assert chd != null;
NodeTraversor.traverse(new NodeVisitor() {
@Override
public void head(@NonNull Node node, int depth) {
if (node instanceof Element && ((Element) node).text().equals("click to show all")) {
hasMore.value = true;
}
}
@Override
public void tail(@NonNull Node node, int depth) {
}
}, chd);
return new GalleryCommentList(list.toArray(new GalleryComment[0]), hasMore.value);
} catch (Throwable e) {
ExceptionUtils.throwIfFatal(e);
e.printStackTrace();
return EMPTY_GALLERY_COMMENT_ARRAY;
}
}
/**
* Parse preview pages with html parser
*/
public static int parsePreviewPages(Document document, String body) throws ParseException {
try {
Element ptt = document.getElementsByClass("ptt").first();
assert ptt != null;
Elements elements = ptt.child(0).child(0).children();
return Integer.parseInt(elements.get(elements.size() - 2).text());
} catch (Throwable e) {
ExceptionUtils.throwIfFatal(e);
e.printStackTrace();
throw new ParseException("Can't parse preview pages", body);
}
}
/**
* Parse preview pages with regular expressions
*/
public static int parsePreviewPages(String body) throws ParseException {
Matcher m = PATTERN_PREVIEW_PAGES.matcher(body);
int previewPages = -1;
if (m.find()) {
previewPages = ParserUtils.parseInt(m.group(1), -1);
}
if (previewPages <= 0) {
throw new ParseException("Parse preview page count error", body);
}
return previewPages;
}
/**
* Parse pages with regular expressions
*/
public static int parsePages(String body) throws ParseException {
int pages = -1;
Matcher m = PATTERN_PAGES.matcher(body);
if (m.find()) {
pages = ParserUtils.parseInt(m.group(1), -1);
}
if (pages < 0) {
throw new ParseException("Parse pages error", body);
}
return pages;
}
public static PreviewSet parsePreviewSet(Document d, String body) throws ParseException {
try {
return parseLargePreviewSet(d, body);
} catch (ParseException e) {
return parseNormalPreviewSet(body);
}
}
public static PreviewSet parsePreviewSet(String body) throws ParseException {
try {
return parseLargePreviewSet(body);
} catch (ParseException e) {
return parseNormalPreviewSet(body);
}
}
/**
* Parse large previews with regular expressions
*/
private static LargePreviewSet parseLargePreviewSet(Document d, String body) throws ParseException {
try {
LargePreviewSet largePreviewSet = new LargePreviewSet();
Element gdt = d.getElementById("gdt");
assert gdt != null;
Elements gdtls = gdt.getElementsByClass("gdtl");
int n = gdtls.size();
if (n == 0) {
throw new ParseException("Can't parse large preview", body);
}
for (int i = 0; i < n; i++) {
Element element = gdtls.get(i).child(0);
String pageUrl = element.attr("href");
element = element.child(0);
String imageUrl = element.attr("src");
int index = Integer.parseInt(element.attr("alt")) - 1;
largePreviewSet.addItem(index, imageUrl, pageUrl);
}
return largePreviewSet;
} catch (Throwable e) {
ExceptionUtils.throwIfFatal(e);
e.printStackTrace();
throw new ParseException("Can't parse large preview", body);
}
}
/**
* Parse large previews with regular expressions
*/
private static LargePreviewSet parseLargePreviewSet(String body) throws ParseException {
Matcher m = PATTERN_LARGE_PREVIEW.matcher(body);
LargePreviewSet largePreviewSet = new LargePreviewSet();
while (m.find()) {
int index = ParserUtils.parseInt(m.group(2), 0) - 1;
if (index < 0) {
continue;
}
String imageUrl = ParserUtils.trim(m.group(3));
String pageUrl = ParserUtils.trim(m.group(1));
largePreviewSet.addItem(index, imageUrl, pageUrl);
}
if (largePreviewSet.size() == 0) {
throw new ParseException("Can't parse large preview", body);
}
return largePreviewSet;
}
/**
* Parse normal previews with regular expressions
*/
private static NormalPreviewSet parseNormalPreviewSet(String body) throws ParseException {
Matcher m = PATTERN_NORMAL_PREVIEW.matcher(body);
NormalPreviewSet normalPreviewSet = new NormalPreviewSet();
while (m.find()) {
int position = ParserUtils.parseInt(m.group(6), 0) - 1;
if (position < 0) {
continue;
}
String imageUrl = ParserUtils.trim(m.group(3));
int xOffset = ParserUtils.parseInt(m.group(4), 0);
int yOffset = 0;
int width = ParserUtils.parseInt(m.group(1), 0);
if (width <= 0) {
continue;
}
int height = ParserUtils.parseInt(m.group(2), 0);
if (height <= 0) {
continue;
}
String pageUrl = ParserUtils.trim(m.group(5));
normalPreviewSet.addItem(position, imageUrl, xOffset, yOffset, width, height, pageUrl);
}
if (normalPreviewSet.size() == 0) {
throw new ParseException("Can't parse normal preview", body);
}
return normalPreviewSet;
}
}
@@ -0,0 +1,572 @@
/*
* 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.EhDB
import com.hippo.ehviewer.client.EhFilter
import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.client.EhUtils.getCategory
import com.hippo.ehviewer.client.EhUtils.handleThumbUrlResolution
import com.hippo.ehviewer.client.data.BaseGalleryInfo
import com.hippo.ehviewer.client.data.GalleryComment
import com.hippo.ehviewer.client.data.GalleryCommentList
import com.hippo.ehviewer.client.data.GalleryDetail
import com.hippo.ehviewer.client.data.GalleryTagGroup
import com.hippo.ehviewer.client.data.LargePreviewSet
import com.hippo.ehviewer.client.data.NormalPreviewSet
import com.hippo.ehviewer.client.data.PreviewSet
import com.hippo.ehviewer.client.exception.EhException
import com.hippo.ehviewer.client.exception.OffensiveException
import com.hippo.ehviewer.client.exception.ParseException
import com.hippo.ehviewer.client.exception.PiningException
import com.hippo.util.ExceptionUtils
import com.hippo.util.JsoupUtils
import com.hippo.yorozuya.NumberUtils
import com.hippo.yorozuya.StringUtils
import com.hippo.yorozuya.trimAnd
import com.hippo.yorozuya.unescapeXml
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import org.jsoup.nodes.Node
import org.jsoup.select.Elements
import org.jsoup.select.NodeTraversor
import org.jsoup.select.NodeVisitor
import java.time.Instant
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
import java.util.Locale
object GalleryDetailParser {
private val PATTERN_ERROR = Regex("<div class=\"d\">\n<p>([^<]+)</p>")
private val PATTERN_DETAIL = Regex(
"var gid = (\\d+);.+?var token = \"([a-f0-9]+)\";.+?var apiuid = ([\\-\\d]+);.+?var apikey = \"([a-f0-9]+)\";",
RegexOption.DOT_MATCHES_ALL
)
private val PATTERN_TORRENT =
Regex("<a[^<>]*onclick=\"return popUp\\('([^']+)'[^)]+\\)\">Torrent Download[^<]+(\\d+)[^<]+</a")
private val PATTERN_ARCHIVE =
Regex("<a[^<>]*onclick=\"return popUp\\('([^']+)'[^)]+\\)\">Archive Download</a>")
private val PATTERN_COVER = Regex("width:(\\d+)px; height:(\\d+)px.+?url\\((.+?)\\)")
private val PATTERN_PAGES =
Regex("<tr><td[^<>]*>Length:</td><td[^<>]*>([\\d,]+) pages</td></tr>")
private val PATTERN_PREVIEW_PAGES =
Regex("<td[^>]+><a[^>]+>([\\d,]+)</a></td><td[^>]+>(?:<a[^>]+>)?&gt;(?:</a>)?</td>")
private val PATTERN_NORMAL_PREVIEW =
Regex("<div class=\"gdtm\"[^<>]*><div[^<>]*width:(\\d+)[^<>]*height:(\\d+)[^<>]*\\((.+?)\\)[^<>]*-(\\d+)px[^<>]*><a[^<>]*href=\"(.+?)\"[^<>]*><img alt=\"([\\d,]+)\"")
private val PATTERN_LARGE_PREVIEW =
Regex("<div class=\"gdtl\".+?<a href=\"(.+?)\"><img alt=\"([\\d,]+)\".+?src=\"(.+?)\"")
private val PATTERN_NEWER_DATE = Regex(", added (.+?)<br />")
private val PATTERN_FAVORITE_SLOT =
Regex("/fav.png\\); background-position:0px -(\\d+)px")
private val EMPTY_GALLERY_TAG_GROUP_ARRAY = arrayOf<GalleryTagGroup>()
private val EMPTY_GALLERY_COMMENT_ARRAY = GalleryCommentList(arrayOf(), false)
private val WEB_COMMENT_DATE_FORMAT = DateTimeFormatter
.ofPattern("dd MMMM yyyy, HH:mm", Locale.US).withZone(ZoneOffset.UTC)
private const val OFFENSIVE_STRING =
"<p>(And if you choose to ignore this warning, you lose all rights to complain about it in the future.)</p>"
private const val PINING_STRING = "<p>This gallery is pining for the fjords.</p>"
@Throws(EhException::class)
fun parse(body: String): GalleryDetail {
if (body.contains(OFFENSIVE_STRING)) {
throw OffensiveException()
}
if (body.contains(PINING_STRING)) {
throw PiningException()
}
// Error info
PATTERN_ERROR.find(body)?.run { throw EhException(groupValues[1]) }
val galleryDetail = GalleryDetail()
// Temporary workaround, see https://github.com/jhy/jsoup/issues/1850
val document = Jsoup.parse(body.replace("del>", "s>"))
parseDetail(galleryDetail, document, body)
galleryDetail.tags = parseTagGroups(document)
galleryDetail.comments = parseComments(document)
galleryDetail.previewPages = parsePreviewPages(document, body)
galleryDetail.previewSet = parsePreviewSet(document, body)
// Generate simpleLanguage for local favorites
galleryDetail.generateSLang()
return galleryDetail
}
@Throws(ParseException::class)
private fun parseDetail(gd: GalleryDetail, d: Document, body: String) {
PATTERN_DETAIL.find(body)?.apply {
gd.gid = groupValues[1].toLongOrNull() ?: -1L
gd.token = groupValues[2]
gd.apiUid = groupValues[3].toLongOrNull() ?: -1L
gd.apiKey = groupValues[4]
} ?: throw ParseException("Can't parse gallery detail", body)
if (gd.gid == -1L) {
throw ParseException("Can't parse gallery detail", body)
}
PATTERN_TORRENT.find(body)?.run {
gd.torrentUrl = groupValues[1].trim().unescapeXml()
gd.torrentCount = groupValues[2].toIntOrNull() ?: 0
}
PATTERN_ARCHIVE.find(body)?.run {
gd.archiveUrl = groupValues[1].trim().unescapeXml()
}
try {
val gm = d.getElementsByClass("gm")[0]
// Thumb url
gm.getElementById("gd1")?.child(0)?.attr("style")?.trim()?.let {
gd.thumb = PATTERN_COVER.find(it)?.run {
handleThumbUrlResolution(groupValues[3])
}
}
gd.title = gm.getElementById("gn")?.text()?.trim()
gd.titleJpn = gm.getElementById("gj")?.text()?.trim()
// Category
val gdc = gm.getElementById("gdc")
try {
var ce = JsoupUtils.getElementByClass(gdc, "cn")
if (ce == null) {
ce = JsoupUtils.getElementByClass(gdc, "cs")
}
gd.category = getCategory(ce!!.text())
} catch (e: Throwable) {
ExceptionUtils.throwIfFatal(e)
gd.category = EhUtils.UNKNOWN
}
// Uploader
val gdn = gm.getElementById("gdn")
if (null != gdn) {
gd.disowned = gdn.attr("style").contains("opacity:0.5")
gd.uploader = StringUtils.trim(gdn.text())
} else {
gd.uploader = ""
}
val gdd = gm.getElementById("gdd")
gd.posted = ""
gd.parent = ""
gd.visible = ""
gd.size = ""
gd.pages = 0
gd.favoriteCount = 0
val es = gdd!!.child(0).child(0).children()
es.forEach {
parseDetailInfo(gd, it)
}
// Rating count
val ratingCount = gm.getElementById("rating_count")
if (null != ratingCount) {
gd.ratingCount = NumberUtils.parseIntSafely(
StringUtils.trim(ratingCount.text()), 0
)
} else {
gd.ratingCount = 0
}
// Rating
val ratingLabel = gm.getElementById("rating_label")
if (null != ratingLabel) {
val ratingStr = StringUtils.trim(ratingLabel.text())
if ("Not Yet Rated" == ratingStr) {
gd.rating = -1.0f
} else {
val index = ratingStr.indexOf(' ')
if (index == -1 || index >= ratingStr.length) {
gd.rating = 0f
} else {
gd.rating = NumberUtils.parseFloatSafely(ratingStr.substring(index + 1), 0f)
}
}
} else {
gd.rating = -1.0f
}
// isFavorited
val gdf = gm.getElementById("gdf")
gd.isFavorited = false
if (gdf != null) {
val favoriteName = StringUtils.trim(gdf.text())
if (favoriteName == "Add to Favorites") {
gd.favoriteName = null
} else {
gd.isFavorited = true
gd.favoriteName = StringUtils.trim(gdf.text())
PATTERN_FAVORITE_SLOT.find(body)?.run {
gd.favoriteSlot = ((groupValues[1].toIntOrNull() ?: 2) - 2) / 19
}
}
}
if (gd.favoriteSlot == -2 && EhDB.containLocalFavorites(gd.gid)) {
gd.favoriteSlot = -1
}
} catch (e: Throwable) {
ExceptionUtils.throwIfFatal(e)
throw ParseException("Can't parse gallery detail", body)
}
// newer version
d.getElementById("gnd")?.run {
val dates = PATTERN_NEWER_DATE.findAll(body).map { it.groupValues[1] }.toList()
select("a").forEachIndexed { index, element ->
val gi = BaseGalleryInfo()
val result = GalleryDetailUrlParser.parse(element.attr("href"))
if (result != null) {
gi.gid = result.gid
gi.token = result.token
gi.title = StringUtils.trim(element.text())
gi.posted = dates[index]
gd.newerVersions.add(gi)
}
}
}
}
private fun parseDetailInfo(gd: GalleryDetail, e: Element) {
val es = e.children()
if (es.size < 2) {
return
}
val key = StringUtils.trim(es[0].text())
val value = StringUtils.trim(es[1].ownText())
if (key.startsWith("Posted")) {
gd.posted = value
} else if (key.startsWith("Parent")) {
val a = es[1].children().first()
if (a != null) {
gd.parent = a.attr("href")
}
} else if (key.startsWith("Visible")) {
gd.visible = value
} else if (key.startsWith("Language")) {
gd.language = value
} else if (key.startsWith("File Size")) {
gd.size = value
} else if (key.startsWith("Length")) {
val index = value.indexOf(' ')
if (index >= 0) {
gd.pages = NumberUtils.parseIntSafely(value.substring(0, index), 1)
} else {
gd.pages = 1
}
} else if (key.startsWith("Favorited")) {
when (value) {
"Never" -> gd.favoriteCount = 0
"Once" -> gd.favoriteCount = 1
else -> {
val index = value.indexOf(' ')
if (index == -1) {
gd.favoriteCount = 0
} else {
gd.favoriteCount = NumberUtils.parseIntSafely(value.substring(0, index), 0)
}
}
}
}
}
private fun parseTagGroup(element: Element): GalleryTagGroup? {
return try {
val group = GalleryTagGroup()
var nameSpace = element.child(0).text()
// Remove last ':'
nameSpace = nameSpace.substring(0, nameSpace.length - 1)
group.groupName = nameSpace
val tags = element.child(1).children()
tags.forEach {
var tag = it.text()
// Sometimes parody tag is followed with '|' and english translate, just remove them
val index = tag.indexOf('|')
if (index >= 0) {
tag = tag.substring(0, index).trim()
}
if (it.className() == "gtw") {
tag = "_$tag" // weak tag
}
group.add(tag)
}
if (group.size > 0) group else null
} catch (e: Throwable) {
ExceptionUtils.throwIfFatal(e)
e.printStackTrace()
null
}
}
/**
* Parse tag groups with html parser
*/
private fun parseTagGroups(document: Document): Array<GalleryTagGroup> {
return try {
val taglist = document.getElementById("taglist")!!
val tagGroups = taglist.child(0).child(0).children()
parseTagGroups(tagGroups)
} catch (e: Throwable) {
ExceptionUtils.throwIfFatal(e)
e.printStackTrace()
EMPTY_GALLERY_TAG_GROUP_ARRAY
}
}
private fun parseTagGroups(trs: Elements): Array<GalleryTagGroup> {
return try {
trs.mapNotNull { parseTagGroup(it) }.toTypedArray()
} catch (e: Throwable) {
ExceptionUtils.throwIfFatal(e)
e.printStackTrace()
EMPTY_GALLERY_TAG_GROUP_ARRAY
}
}
private fun parseComment(element: Element): GalleryComment? {
return try {
val comment = GalleryComment()
// Id
val a = element.previousElementSibling()
val name = a!!.attr("name")
comment.id = name trimAnd { substring(1).toInt().toLong() }
// Editable, vote up and vote down
val c4 = JsoupUtils.getElementByClass(element, "c4")
if (null != c4) {
if ("Uploader Comment" == c4.text()) {
comment.uploader = true
}
for (e in c4.children()) {
when (e.text()) {
"Vote+" -> {
comment.voteUpAble = true
comment.voteUpEd = StringUtils.trim(e.attr("style")).isNotEmpty()
}
"Vote-" -> {
comment.voteDownAble = true
comment.voteDownEd = StringUtils.trim(e.attr("style")).isNotEmpty()
}
"Edit" -> comment.editable = true
}
}
}
// Vote state
val c7 = JsoupUtils.getElementByClass(element, "c7")
if (null != c7) {
comment.voteState = StringUtils.trim(c7.text())
}
// Score
val c5 = JsoupUtils.getElementByClass(element, "c5")
if (null != c5) {
val es = c5.children()
if (!es.isEmpty()) {
comment.score = NumberUtils.parseIntSafely(
StringUtils.trim(es[0].text()), 0
)
}
}
// time
val c3 = JsoupUtils.getElementByClass(element, "c3")
val temp = c3!!.ownText()
val time = if (temp.endsWith(':')) {
// user
comment.user = c3.child(0).text()
temp.substring("Posted on ".length, temp.length - " by:".length)
} else {
temp.substring("Posted on ".length)
}
comment.time = Instant.from(WEB_COMMENT_DATE_FORMAT.parse(time)).toEpochMilli()
// comment
val c6 = JsoupUtils.getElementByClass(element, "c6")
// fix underline support
for (e in c6!!.children()) {
if ("span" == e.tagName() && "text-decoration:underline;" == e.attr("style")) {
e.tagName("u")
}
}
comment.comment = c6.html()
// filter comment
if (!comment.uploader) {
val sEhFilter = EhFilter
if (!sEhFilter.filterCommenter(comment.user) || !sEhFilter.filterComment(comment.comment)) {
return null
}
}
// last edited
val c8 = JsoupUtils.getElementByClass(element, "c8")
if (c8 != null) {
val e = c8.children().first()
if (e != null) {
comment.lastEdited =
Instant.from(WEB_COMMENT_DATE_FORMAT.parse(e.text())).toEpochMilli()
}
}
comment
} catch (e: Throwable) {
ExceptionUtils.throwIfFatal(e)
e.printStackTrace()
null
}
}
/**
* Parse comments with html parser
*/
fun parseComments(document: Document): GalleryCommentList {
return try {
val cdiv = document.getElementById("cdiv")!!
val c1s = cdiv.getElementsByClass("c1")
val list = c1s.mapNotNull { parseComment(it) }
val chd = cdiv.getElementById("chd")
var hasMore = false
NodeTraversor.traverse(object : NodeVisitor {
override fun head(node: Node, depth: Int) {
if (node is Element && node.text() == "click to show all") {
hasMore = true
}
}
override fun tail(node: Node, depth: Int) {}
}, chd!!)
GalleryCommentList(list.toTypedArray(), hasMore)
} catch (e: Throwable) {
ExceptionUtils.throwIfFatal(e)
e.printStackTrace()
EMPTY_GALLERY_COMMENT_ARRAY
}
}
/**
* Parse preview pages with html parser
*/
@Throws(ParseException::class)
fun parsePreviewPages(document: Document, body: String?): Int {
return try {
val ptt = document.getElementsByClass("ptt").first()!!
val elements = ptt.child(0).child(0).children()
elements[elements.size - 2].text().toInt()
} catch (e: Throwable) {
ExceptionUtils.throwIfFatal(e)
e.printStackTrace()
throw ParseException("Can't parse preview pages", body!!)
}
}
/**
* Parse preview pages with regular expressions
*/
@JvmStatic
@Throws(ParseException::class)
fun parsePreviewPages(body: String): Int {
return PATTERN_PREVIEW_PAGES.find(body)?.groupValues?.get(1)?.toIntOrNull()
?: throw ParseException("Parse preview page count error", body)
}
/**
* Parse pages with regular expressions
*/
@JvmStatic
@Throws(ParseException::class)
fun parsePages(body: String): Int {
return PATTERN_PAGES.find(body)?.groupValues?.get(1)?.toIntOrNull()
?: throw ParseException("Parse pages error", body)
}
@Throws(ParseException::class)
fun parsePreviewSet(d: Document, body: String): PreviewSet {
return try {
parseLargePreviewSet(d, body)
} catch (e: ParseException) {
parseNormalPreviewSet(body)
}
}
@JvmStatic
@Throws(ParseException::class)
fun parsePreviewSet(body: String): PreviewSet {
return try {
parseLargePreviewSet(body)
} catch (e: ParseException) {
parseNormalPreviewSet(body)
}
}
/**
* Parse large previews with regular expressions
*/
@Throws(ParseException::class)
private fun parseLargePreviewSet(d: Document, body: String): LargePreviewSet {
return try {
val largePreviewSet = LargePreviewSet()
val gdt = d.getElementById("gdt")!!
val gdtls = gdt.getElementsByClass("gdtl")
val n = gdtls.size
if (n == 0) {
throw ParseException("Can't parse large preview", body)
}
for (i in 0 until n) {
var element = gdtls[i].child(0)
val pageUrl = element.attr("href")
element = element.child(0)
val imageUrl = element.attr("src")
val index = element.attr("alt").toInt() - 1
largePreviewSet.addItem(index, imageUrl, pageUrl)
}
largePreviewSet
} catch (e: Throwable) {
ExceptionUtils.throwIfFatal(e)
throw ParseException("Can't parse large preview", body)
}
}
/**
* Parse large previews with regular expressions
*/
@Throws(ParseException::class)
private fun parseLargePreviewSet(body: String): LargePreviewSet {
val largePreviewSet = LargePreviewSet()
PATTERN_LARGE_PREVIEW.findAll(body).forEach {
val index = (it.groupValues[2].toIntOrNull() ?: return@forEach) - 1
val imageUrl = it.groupValues[3].trim()
val pageUrl = it.groupValues[1].trim()
largePreviewSet.addItem(index, imageUrl, pageUrl)
}
if (largePreviewSet.size() == 0) {
throw ParseException("Can't parse large preview", body)
}
return largePreviewSet
}
/**
* Parse normal previews with regular expressions
*/
@Throws(ParseException::class)
private fun parseNormalPreviewSet(body: String): NormalPreviewSet {
val normalPreviewSet = NormalPreviewSet()
PATTERN_NORMAL_PREVIEW.findAll(body).forEach {
val position = (it.groupValues[6].toIntOrNull() ?: return@forEach) - 1
val imageUrl = it.groupValues[3].trim()
val xOffset = it.groupValues[4].toIntOrNull() ?: 0
val yOffset = 0
val width = it.groupValues[1].toIntOrNull() ?: return@forEach
val height = it.groupValues[2].toIntOrNull() ?: return@forEach
val pageUrl = it.groupValues[5].trim()
normalPreviewSet.addItem(position, imageUrl, xOffset, yOffset, width, height, pageUrl)
}
if (normalPreviewSet.size() == 0) {
throw ParseException("Can't parse normal preview", body)
}
return normalPreviewSet
}
}
@@ -0,0 +1,44 @@
package com.hippo.ehviewer.client.parser
import com.hippo.ehviewer.client.exception.ParseException
import org.jsoup.Jsoup
object HomeParser {
private val PATTERN_FUNDS =
Regex("Available: ([\\d,]+) Credits.*Available: ([\\d,]+) kGP", RegexOption.DOT_MATCHES_ALL)
private const val RESET_SUCCEED = "Image limit was successfully reset."
fun parse(body: String): Limits {
Jsoup.parse(body).selectFirst("div.homebox")?.let {
val es = it.select("p > strong")
if (es.size == 3) {
val current = ParserUtils.parseInt(es[0].text(), 0)
val maximum = ParserUtils.parseInt(es[1].text(), 0)
val resetCost = ParserUtils.parseInt(es[2].text(), 0)
return Limits(current, maximum, resetCost)
}
}
throw ParseException("Parse image limits error", body)
}
fun parseResetLimits(body: String): Limits? {
return if (body.contains(RESET_SUCCEED)){
null
} else {
parse(body)
}
}
fun parseFunds(body: String): Funds {
PATTERN_FUNDS.find(body)?.groupValues?.run {
val fundsC = ParserUtils.parseInt(get(1), 0)
val fundsGP = ParserUtils.parseInt(get(2), 0) * 1000
return Funds(fundsGP, fundsC)
}
throw ParseException("Parse funds error", body)
}
data class Limits(val current: Int = 0, val maximum: Int, val resetCost: Int = 0)
data class Funds(val fundsGP: Int, val fundsC: Int)
class Result(val limits: Limits, val funds: Funds)
}
@@ -0,0 +1,21 @@
package com.hippo.ehviewer.coil
import coil.disk.DiskCache
inline fun DiskCache.edit(key: String, block: DiskCache.Editor.() -> Unit): Boolean {
val editor = edit(key) ?: return false
editor.runCatching {
block(this)
}.onFailure {
editor.abort()
throw it
}.onSuccess {
editor.commit()
}
return true
}
inline fun DiskCache.read(key: String, block: DiskCache.Snapshot.() -> Unit): Boolean {
(get(key) ?: return false).use { block(it) }
return true
}
@@ -0,0 +1,78 @@
package com.hippo.ehviewer.coil
import coil.intercept.Interceptor
import coil.request.ImageRequest
import coil.request.ImageResult
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
object MergeInterceptor : Interceptor {
private val pendingContinuationMap: HashMap<String, MutableList<Continuation<Unit>>> =
hashMapOf()
private val pendingContinuationMapLock = Mutex()
private val notifyScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
override suspend fun intercept(chain: Interceptor.Chain): ImageResult {
val originRequest = chain.request
val originListener = originRequest.listener
val key = originRequest.data as String
val newRequest = ImageRequest.Builder(originRequest).listener(
{},
{}, // Only original request's onStart and onCancel will get called
{ request, result ->
originListener?.onError(request, result)
// Wake all pending continuations since this request is to be failed
notifyScope.launch {
pendingContinuationMapLock.withLock {
pendingContinuationMap.remove(key)?.forEach { it.resume(Unit) }
}
}
},
{ request, result ->
originListener?.onSuccess(request, result)
// Wake all pending continuations shared with the same memory key since we have written it to memory cache
notifyScope.launch {
pendingContinuationMapLock.withLock {
pendingContinuationMap.remove(key)?.forEach { it.resume(Unit) }
}
}
}
).build()
pendingContinuationMapLock.lock()
val existPendingContinuations = pendingContinuationMap[key]
if (existPendingContinuations == null) {
pendingContinuationMap[key] = mutableListOf()
pendingContinuationMapLock.unlock()
} else {
existPendingContinuations.apply {
suspendCancellableCoroutine { continuation ->
add(continuation)
pendingContinuationMapLock.unlock()
continuation.invokeOnCancellation { remove(continuation) }
}
}
}
return withContext(originRequest.interceptorDispatcher) { chain.proceed(newRequest) }
}
fun onCancel(key: String) {
notifyScope.launch {
pendingContinuationMapLock.withLock {
// Wake up a pending continuation to continue executing task
val successor =
pendingContinuationMap[key]?.removeFirstOrNull()?.apply { resume(Unit) }
// If no successor, delete this entry from hashmap
successor ?: pendingContinuationMap.remove(key)
}
}
}
}
@@ -0,0 +1,105 @@
package com.hippo.ehviewer.dailycheck
import android.annotation.SuppressLint
import android.content.Context
import android.text.Html
import androidx.core.app.NotificationChannelCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.work.Constraints
import androidx.work.CoroutineWorker
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.NetworkType
import androidx.work.PeriodicWorkRequest
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.WorkerParameters
import com.hippo.ehviewer.EhApplication
import com.hippo.ehviewer.R
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.EhEngine
import eu.kanade.tachiyomi.util.lang.withIOContext
import java.time.Duration
import java.time.LocalDateTime
private val signedIn
get() = EhApplication.ehCookieStore.hasSignedIn()
private const val CHANNEL_ID = "DailyCheckNotification"
class DailyCheckWork(val context: Context, workerParams: WorkerParameters) :
CoroutineWorker(context, workerParams) {
override suspend fun doWork(): Result = withIOContext {
runCatching {
if (signedIn) {
EhEngine.getNews(true)?.let { showEventPane(it) }
}
}.onFailure {
it.printStackTrace()
return@withIOContext Result.retry()
}
Result.success()
}
@SuppressLint("MissingPermission")
private fun showEventPane(html: String) {
if (Settings.getHideHvEvents() && html.contains("You have encountered a monster!")) {
return
}
val notificationManager = NotificationManagerCompat.from(context)
val chan = NotificationChannelCompat
.Builder(CHANNEL_ID, NotificationManagerCompat.IMPORTANCE_LOW)
.setName(CHANNEL_ID)
.build()
notificationManager.createNotificationChannel(chan)
val msg = NotificationCompat.Builder(context, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_launcher_monochrome)
.setContentText(Html.fromHtml(html, Html.FROM_HTML_MODE_LEGACY))
.setStyle(NotificationCompat.BigTextStyle())
.build()
runCatching {
notificationManager.notify(1, msg)
}.onFailure {
it.printStackTrace()
}
}
}
val schedHour
get() = Settings.getInt(Settings.KEY_REQUEST_NEWS_TIMER_HOUR, -1).takeUnless { it == -1 }
val schedMinute
get() = Settings.getInt(Settings.KEY_REQUEST_NEWS_TIMER_MINUTE, -1).takeUnless { it == -1 }
private val whenToWork
get() = LocalDateTime.now()
.run { withHour(schedHour ?: hour) }
.run { withMinute(schedMinute ?: minute) }
private val initialDelay
get() = Duration.between(LocalDateTime.now(), whenToWork)
.run { if (isNegative) plusDays(1) else this }
private fun getDailyCheckWorkRequest(): PeriodicWorkRequest {
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
return PeriodicWorkRequestBuilder<DailyCheckWork>(Duration.ofDays(1))
.setConstraints(constraints)
.setInitialDelay(initialDelay)
.build()
}
private const val workName = "DailyCheckWork"
fun updateDailyCheckWork(context: Context) {
if (Settings.getRequestNews()) {
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
workName,
ExistingPeriodicWorkPolicy.UPDATE,
getDailyCheckWorkRequest()
)
} else {
WorkManager.getInstance(context).cancelUniqueWork(workName)
}
}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -25,11 +25,11 @@ import android.os.IBinder
import android.os.SystemClock
import android.util.Log
import androidx.annotation.IntDef
import androidx.collection.LongSparseArray
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationChannelCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import com.hippo.ehviewer.EhApplication
import com.hippo.ehviewer.R
import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.client.data.GalleryInfo
@@ -37,11 +37,10 @@ import com.hippo.ehviewer.dao.DownloadInfo
import com.hippo.ehviewer.ui.MainActivity
import com.hippo.ehviewer.ui.scene.DownloadsScene
import com.hippo.util.ReadableTime
import com.hippo.util.getParcelableExtraCompat
import com.hippo.yorozuya.FileUtils
import com.hippo.yorozuya.SimpleHandler
import com.hippo.yorozuya.collect.LongList
import com.hippo.yorozuya.collect.SparseJBArray
import com.hippo.yorozuya.collect.SparseJLArray
class DownloadService : Service(), DownloadManager.DownloadListener {
private var mNotifyManager: NotificationManagerCompat? = null
@@ -66,7 +65,7 @@ class DownloadService : Service(), DownloadManager.DownloadListener {
.setName(getString(R.string.download_service))
.build()
)
mDownloadManager = EhApplication.downloadManager
mDownloadManager = DownloadManager
mDownloadManager!!.setDownloadListener(this)
ensureDownloadingBuilder()
mDownloadingBuilder!!.setContentTitle(getString(R.string.download_service))
@@ -99,13 +98,13 @@ class DownloadService : Service(), DownloadManager.DownloadListener {
action = intent.action
}
if (ACTION_START == action) {
val gi = intent!!.getParcelableExtra<GalleryInfo>(KEY_GALLERY_INFO)
val gi = intent!!.getParcelableExtraCompat<GalleryInfo>(KEY_GALLERY_INFO)
val label = intent.getStringExtra(KEY_LABEL)
if (gi != null && mDownloadManager != null) {
mDownloadManager!!.startDownload(gi, label)
}
} else if (ACTION_START_RANGE == action) {
val gidList = intent!!.getParcelableExtra<LongList>(KEY_GID_LIST)
val gidList = intent!!.getParcelableExtraCompat<LongList>(KEY_GID_LIST)
if (gidList != null && mDownloadManager != null) {
mDownloadManager!!.startRangeDownload(gidList)
}
@@ -123,7 +122,7 @@ class DownloadService : Service(), DownloadManager.DownloadListener {
mDownloadManager!!.stopCurrentDownload()
}
} else if (ACTION_STOP_RANGE == action) {
val gidList = intent!!.getParcelableExtra<LongList>(KEY_GID_LIST)
val gidList = intent!!.getParcelableExtraCompat<LongList>(KEY_GID_LIST)
if (gidList != null && mDownloadManager != null) {
mDownloadManager!!.stopRangeDownload(gidList)
}
@@ -137,7 +136,7 @@ class DownloadService : Service(), DownloadManager.DownloadListener {
mDownloadManager!!.deleteDownload(gid)
}
} else if (ACTION_DELETE_RANGE == action) {
val gidList = intent!!.getParcelableExtra<LongList>(KEY_GID_LIST)
val gidList = intent!!.getParcelableExtraCompat<LongList>(KEY_GID_LIST)
if (gidList != null && mDownloadManager != null) {
mDownloadManager!!.deleteRangeDownload(gidList)
}
@@ -533,8 +532,8 @@ class DownloadService : Service(), DownloadManager.DownloadListener {
private const val ID_DOWNLOADING = 1
private const val ID_DOWNLOADED = 2
private const val ID_509 = 3
private val sItemStateArray = SparseJBArray()
private val sItemTitleArray = SparseJLArray<String>()
private val sItemStateArray = LongSparseArray<Boolean>()
private val sItemTitleArray = LongSparseArray<String>()
private var sFailedCount = 0
private var sFinishedCount = 0
private var sDownloadedCount = 0
@@ -1,351 +0,0 @@
/*
* Copyright 2019 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.gallery;
import android.content.Context;
import android.net.Uri;
import android.os.Handler;
import android.os.Process;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.hippo.Native;
import com.hippo.UriArchiveAccessor;
import com.hippo.ehviewer.GetText;
import com.hippo.ehviewer.R;
import com.hippo.ehviewer.Settings;
import com.hippo.image.Image;
import com.hippo.unifile.UniFile;
import com.hippo.yorozuya.FileUtils;
import com.hippo.yorozuya.thread.PVLock;
import com.hippo.yorozuya.thread.PriorityThread;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Stack;
import java.util.concurrent.atomic.AtomicInteger;
import eu.kanade.tachiyomi.ui.reader.loader.PageLoader;
public class ArchivePageLoader extends PageLoader2 {
private static final AtomicInteger sIdGenerator = new AtomicInteger();
public static Handler showPasswd;
public static String passwd;
public static PVLock pv = new PVLock(0);
private final UriArchiveAccessor archiveAccessor;
private final Stack<Integer> requests = new Stack<>();
private final LinkedHashMap<Integer, Image.ByteBufferSource> streams = new LinkedHashMap<>();
private final Thread[] decodeThread = new Thread[]{
new Thread(new DecodeTask()),
new Thread(new DecodeTask()),
new Thread(new DecodeTask()),
new Thread(new DecodeTask())
};
private Thread archiveThread;
private volatile int size = PageLoader.STATE_WAIT;
private String error;
public ArchivePageLoader(Context context, Uri uri) {
UriArchiveAccessor archiveAccessor1;
try {
archiveAccessor1 = new UriArchiveAccessor(context, uri);
} catch (Exception e) {
archiveAccessor1 = null;
e.printStackTrace();
}
archiveAccessor = archiveAccessor1;
}
@Override
public void start() {
int id = sIdGenerator.incrementAndGet();
archiveThread = new PriorityThread(
new ArchiveHostTask(), "ArchiveTask" + '-' + id, Process.THREAD_PRIORITY_BACKGROUND);
archiveThread.start();
for (Thread i : decodeThread) {
i.start();
}
}
@Override
public void stop() {
super.stop();
if (archiveThread != null) {
archiveThread.interrupt();
archiveThread = null;
}
for (Thread i : decodeThread) {
i.interrupt();
}
try {
archiveAccessor.close();
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public int size() {
return size;
}
@Override
protected void onRequest(int index) {
boolean inDecodeTask;
synchronized (streams) {
inDecodeTask = streams.containsKey(index);
}
synchronized (requests) {
boolean inArchiveTask = requests.contains(index);
if (!inArchiveTask && !inDecodeTask) {
requests.add(index);
requests.notify();
}
}
notifyPageWait(index);
}
@Override
protected void onForceRequest(int index) {
onRequest(index);
}
@Override
protected void onCancelRequest(int index) {
synchronized (requests) {
requests.remove(Integer.valueOf(index));
}
}
@Override
public String getError() {
return error;
}
@NonNull
@Override
public String getImageFilename(int index) {
return FileUtils.getNameFromFilename(getImageFilenameWithExtension(index));
}
@NonNull
@Override
public String getImageFilenameWithExtension(int index) {
return FileUtils.sanitizeFilename(archiveAccessor.getFilename(index));
}
@Override
public boolean save(int index, @NonNull UniFile file) {
int fd;
FileOutputStream stream;
try {
stream = (FileOutputStream) file.openOutputStream();
fd = Native.getFd(stream.getFD());
} catch (IOException e) {
e.printStackTrace();
return false;
}
archiveAccessor.extractToFd(index, fd);
try {
stream.close();
} catch (IOException e) {
e.printStackTrace();
return false;
}
return true;
}
@Nullable
@Override
public UniFile save(int index, @NonNull UniFile dir, @NonNull String filename) {
String extension = FileUtils.getExtensionFromFilename(getImageFilenameWithExtension(index));
UniFile dst = dir.subFile(null != extension ? filename + "." + extension : filename);
save(index, dst);
return dst;
}
@Override
protected void preloadPages(@NonNull List<Integer> pages) {
}
private class ArchiveHostTask implements Runnable {
public final Thread[] archiveThreads = new Thread[]{
new Thread(new ArchiveTask()),
new Thread(new ArchiveTask()),
new Thread(new ArchiveTask()),
new Thread(new ArchiveTask())
};
private void waitPasswd() throws InterruptedException {
showPasswd.sendEmptyMessage(0);
pv.p();
}
private void notifyError() {
showPasswd.sendEmptyMessage(1);
}
private void notifyDismiss() {
showPasswd.sendEmptyMessage(2);
}
@Override
public void run() {
try {
size = archiveAccessor.open();
} catch (Exception e) {
e.printStackTrace();
size = 0;
}
if (size <= 0) {
size = PageLoader.STATE_ERROR;
error = GetText.getString(R.string.error_reading_failed);
notifyDataChanged();
return;
}
// Update size and notify changed
notifyDataChanged();
if (archiveAccessor.needPassword()) {
boolean need_request = true;
Set<String> set = Settings.getArchivePasswds();
if (set != null) {
for (String passwd : set) {
if (archiveAccessor.providePassword(passwd)) {
need_request = false;
break;
}
}
}
while (!Thread.currentThread().isInterrupted() && need_request) {
try {
waitPasswd();
} catch (InterruptedException e) {
e.printStackTrace();
return;
}
if (!archiveAccessor.providePassword(passwd))
notifyError();
else {
Settings.putPasswdToArchivePasswds(passwd);
notifyDismiss();
break;
}
}
}
for (Thread i : archiveThreads) {
i.start();
}
Object o = new Object();
synchronized (o) {
try {
o.wait();
} catch (InterruptedException e) {
for (Thread i : archiveThreads) {
i.interrupt();
}
}
}
}
}
private class ArchiveTask implements Runnable {
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
int index;
synchronized (requests) {
if (requests.isEmpty()) {
try {
requests.wait();
} catch (InterruptedException e) {
// Interrupted
break;
}
continue;
}
index = requests.pop();
}
// Check index valid
if (index < 0 || index >= size) {
notifyPageFailed(index);
continue;
}
synchronized (streams) {
if (streams.get(index) != null) {
continue;
}
}
Image.ByteBufferSource src = archiveAccessor.getImageSource(index);
synchronized (streams) {
streams.put(index, src);
streams.notify();
}
}
}
}
private class DecodeTask implements Runnable {
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
int index;
Image.ByteBufferSource src;
synchronized (streams) {
if (streams.isEmpty()) {
try {
streams.wait();
} catch (InterruptedException e) {
break;
}
continue;
}
Iterator<Map.Entry<Integer, Image.ByteBufferSource>> iterator = streams.entrySet().iterator();
Map.Entry<Integer, Image.ByteBufferSource> entry = iterator.next();
iterator.remove();
index = entry.getKey();
src = entry.getValue();
}
Image image = null;
if (src != null) {
image = Image.decode(src);
}
if (image != null) {
notifyPageSucceed(index, image);
} else {
notifyPageFailed(index);
}
}
}
}
}
@@ -0,0 +1,168 @@
/*
* Copyright 2023 Tarsin Norbin
*
* 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.gallery
import android.content.Context
import android.net.Uri
import com.hippo.Native.getFd
import com.hippo.UriArchiveAccessor
import com.hippo.ehviewer.Settings
import com.hippo.image.Image
import com.hippo.unifile.UniFile
import com.hippo.yorozuya.FileUtils
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.sync.withPermit
import java.io.FileOutputStream
import java.io.IOException
class ArchivePageLoader(context: Context, uri: Uri, passwdFlow: Flow<String>) : PageLoader2(),
CoroutineScope {
override val coroutineContext = Dispatchers.IO + Job()
private val archiveAccessor by lazy { UriArchiveAccessor(context, uri) }
private val hostJob = launch(start = CoroutineStart.LAZY) {
size = archiveAccessor.open()
if (size == 0) {
return@launch
}
archiveAccessor.run {
if (needPassword()) {
Settings.getArchivePasswds()?.forEach {
if (providePassword(it)) return@launch
}
passwdFlow.collect {
if (providePassword(it)) {
Settings.putPasswdToArchivePasswds(it)
currentCoroutineContext().cancel()
}
}
}
}
}
override var size = 0
private set
override fun start() {
hostJob.start()
}
override fun stop() {
cancel()
archiveAccessor.close()
super.stop()
}
private val mJobMap = hashMapOf<Int, Job>()
private val mWorkerMutex by lazy { (0 until size).map { Mutex() } }
private val mSemaphore = Semaphore(4)
override fun onRequest(index: Int) {
synchronized(mJobMap) {
val current = mJobMap[index]
if (current?.isActive != true) {
mJobMap[index] = launch {
mWorkerMutex[index].withLock {
mSemaphore.withPermit {
doRealWork(index)
}
}
}
}
}
}
private suspend fun doRealWork(index: Int) {
val src = archiveAccessor.getImageSource(index) ?: return
runCatching {
currentCoroutineContext().ensureActive()
}.onFailure {
src.close()
throw it
}
val image = Image.decode(src) ?: return notifyPageFailed(index, null)
runCatching {
currentCoroutineContext().ensureActive()
}.onFailure {
image.recycle()
throw it
}
notifyPageSucceed(index, image)
}
override fun onForceRequest(index: Int) {
onRequest(index)
}
override suspend fun awaitReady(): Boolean {
hostJob.join()
return size != 0
}
override val isReady: Boolean
get() = size != 0
override fun onCancelRequest(index: Int) {
mJobMap[index]?.cancel()
}
override fun getImageFilename(index: Int): String {
return FileUtils.getNameFromFilename(getImageFilenameWithExtension(index))
}
override fun getImageFilenameWithExtension(index: Int): String {
return FileUtils.sanitizeFilename(archiveAccessor.getFilename(index))
}
override fun save(index: Int, file: UniFile): Boolean {
val fd: Int
val stream: FileOutputStream
try {
stream = file.openOutputStream() as FileOutputStream
fd = getFd(stream.fd)
} catch (e: IOException) {
e.printStackTrace()
return false
}
archiveAccessor.extractToFd(index, fd)
try {
stream.close()
} catch (e: IOException) {
e.printStackTrace()
return false
}
return true
}
override fun save(index: Int, dir: UniFile, filename: String): UniFile {
val extension = FileUtils.getExtensionFromFilename(getImageFilenameWithExtension(index))
val dst = dir.subFile(if (null != extension) "$filename.$extension" else filename)
save(index, dst!!)
return dst
}
override fun preloadPages(pages: List<Int>, pair: Pair<Int, Int>) {}
}
@@ -1,234 +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.gallery;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.hippo.ehviewer.client.data.GalleryInfo;
import com.hippo.ehviewer.spider.SpiderQueen;
import com.hippo.image.Image;
import com.hippo.unifile.UniFile;
import com.hippo.yorozuya.SimpleHandler;
import java.util.List;
import java.util.Locale;
import eu.kanade.tachiyomi.ui.reader.loader.PageLoader;
public class EhPageLoader extends PageLoader2 implements SpiderQueen.OnSpiderListener {
private final GalleryInfo mGalleryInfo;
@Nullable
private SpiderQueen mSpiderQueen;
public EhPageLoader(GalleryInfo galleryInfo) {
mGalleryInfo = galleryInfo;
}
@Override
public void start() {
mSpiderQueen = SpiderQueen.obtainSpiderQueen(mGalleryInfo, SpiderQueen.MODE_READ);
mSpiderQueen.addOnSpiderListener(this);
if (mSpiderQueen.size() > 0)
notifyDataChanged();
}
@Override
public void stop() {
super.stop();
if (mSpiderQueen != null) {
mSpiderQueen.removeOnSpiderListener(this);
// Activity recreate may called, so wait 3000s
SimpleHandler.getInstance().postDelayed(new ReleaseTask(mSpiderQueen), 3000);
mSpiderQueen = null;
}
}
@Override
public int getStartPage() {
if (mSpiderQueen != null) {
return mSpiderQueen.getStartPage();
} else {
return super.getStartPage();
}
}
@NonNull
@Override
public String getImageFilename(int index) {
return String.format(Locale.US, "%d-%s-%08d", mGalleryInfo.getGid(), mGalleryInfo.getToken(), index + 1);
}
@NonNull
@Override
public String getImageFilenameWithExtension(int index) {
if (null != mSpiderQueen) {
String extension = mSpiderQueen.getExtension(index);
if (extension != null) {
return String.format(Locale.US, "%d-%s-%08d.%s", mGalleryInfo.getGid(), mGalleryInfo.getToken(), index + 1, extension);
}
}
return String.format(Locale.US, "%d-%s-%08d", mGalleryInfo.getGid(), mGalleryInfo.getToken(), index + 1);
}
@Override
public boolean save(int index, @NonNull UniFile file) {
if (null != mSpiderQueen) {
return mSpiderQueen.save(index, file);
} else {
return false;
}
}
@Nullable
@Override
public UniFile save(int index, @NonNull UniFile dir, @NonNull String filename) {
if (null != mSpiderQueen) {
return mSpiderQueen.save(index, dir, filename);
} else {
return null;
}
}
@Override
public void putStartPage(int page) {
if (mSpiderQueen != null) {
mSpiderQueen.putStartPage(page);
}
}
@Override
public int size() {
if (mSpiderQueen != null) {
return mSpiderQueen.size();
} else {
return PageLoader.STATE_ERROR;
}
}
@Override
protected void onRequest(int index) {
if (mSpiderQueen != null) {
Object object = mSpiderQueen.request(index, true);
if (object instanceof Float) {
notifyPagePercent(index, (Float) object);
} else if (object instanceof String) {
notifyPageFailed(index);
} else if (object == null) {
notifyPageWait(index);
}
}
}
@Override
protected void onForceRequest(int index) {
if (mSpiderQueen != null) {
Object object = mSpiderQueen.forceRequest(index);
if (object instanceof Float) {
notifyPagePercent(index, (Float) object);
} else if (object instanceof String) {
notifyPageFailed(index);
} else if (object == null) {
notifyPageWait(index);
}
}
}
@Override
protected void onCancelRequest(int index) {
if (mSpiderQueen != null) {
mSpiderQueen.cancelRequest(index);
}
}
@Override
public String getError() {
if (mSpiderQueen != null) {
return mSpiderQueen.getError();
} else {
return "Error"; // TODO
}
}
@Override
public void onGetPages(int pages) {
notifyDataChanged();
}
@Override
public void onGet509(int index) {
// TODO
}
@Override
public void onPageDownload(int index, long contentLength, long receivedSize, int bytesRead) {
if (contentLength > 0) {
notifyPagePercent(index, (float) receivedSize / contentLength);
}
}
@Override
public void onPageSuccess(int index, int finished, int downloaded, int total) {
if (mSpiderQueen != null) {
mSpiderQueen.request(index, false);
}
}
@Override
public void onPageFailure(int index, String error, int finished, int downloaded, int total) {
notifyPageFailed(index);
}
@Override
public void onFinish(int finished, int downloaded, int total) {
}
@Override
public void onGetImageSuccess(int index, Image image) {
notifyPageSucceed(index, image);
}
@Override
public void onGetImageFailure(int index, String error) {
notifyPageFailed(index);
}
@Override
protected void preloadPages(@NonNull List<Integer> pages) {
if (mSpiderQueen != null) {
mSpiderQueen.preloadPages(pages);
}
}
private static class ReleaseTask implements Runnable {
private SpiderQueen mSpiderQueen;
public ReleaseTask(SpiderQueen spiderQueen) {
mSpiderQueen = spiderQueen;
}
@Override
public void run() {
if (null != mSpiderQueen) {
SpiderQueen.releaseSpiderQueen(mSpiderQueen, SpiderQueen.MODE_READ);
mSpiderQueen = null;
}
}
}
}
@@ -0,0 +1,155 @@
/*
* 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.gallery
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.spider.SpiderQueen
import com.hippo.ehviewer.spider.SpiderQueen.Companion.obtainSpiderQueen
import com.hippo.ehviewer.spider.SpiderQueen.Companion.releaseSpiderQueen
import com.hippo.ehviewer.spider.SpiderQueen.OnSpiderListener
import com.hippo.image.Image
import com.hippo.unifile.UniFile
import com.hippo.yorozuya.SimpleHandler
import java.util.Locale
class EhPageLoader(private val mGalleryInfo: GalleryInfo) : PageLoader2(), OnSpiderListener {
private lateinit var mSpiderQueen: SpiderQueen
override fun start() {
mSpiderQueen = obtainSpiderQueen(mGalleryInfo, SpiderQueen.MODE_READ)
mSpiderQueen.addOnSpiderListener(this)
}
override fun stop() {
super.stop()
mSpiderQueen.removeOnSpiderListener(this)
// Activity recreate may called, so wait 3000s
SimpleHandler.getInstance().postDelayed(ReleaseTask(mSpiderQueen), 3000)
}
override val startPage
get() = mSpiderQueen.startPage
override fun getImageFilename(index: Int): String {
return String.format(
Locale.US,
"%d-%s-%08d",
mGalleryInfo.gid,
mGalleryInfo.token,
index + 1
)
}
override fun getImageFilenameWithExtension(index: Int): String {
val extension = mSpiderQueen.getExtension(index)
if (extension != null) {
return String.format(
Locale.US,
"%d-%s-%08d.%s",
mGalleryInfo.gid,
mGalleryInfo.token,
index + 1,
extension
)
}
return String.format(
Locale.US,
"%d-%s-%08d",
mGalleryInfo.gid,
mGalleryInfo.token,
index + 1
)
}
override fun save(index: Int, file: UniFile): Boolean {
return mSpiderQueen.save(index, file)
}
override fun save(index: Int, dir: UniFile, filename: String): UniFile? {
return mSpiderQueen.save(index, dir, filename)
}
override fun putStartPage(page: Int) {
mSpiderQueen.putStartPage(page)
}
override val size: Int
get() = mSpiderQueen.size
override fun onRequest(index: Int) {
mSpiderQueen.request(index)
}
override fun onForceRequest(index: Int) {
mSpiderQueen.forceRequest(index)
}
override suspend fun awaitReady(): Boolean {
return mSpiderQueen.awaitReady()
}
override val isReady: Boolean
get() = mSpiderQueen.isReady
override fun onCancelRequest(index: Int) {
mSpiderQueen.cancelRequest(index)
}
override fun onGetPages(pages: Int) {}
override fun onGet509(index: Int) {}
override fun onPageDownload(
index: Int,
contentLength: Long,
receivedSize: Long,
bytesRead: Int
) {
if (contentLength > 0) {
notifyPagePercent(index, receivedSize.toFloat() / contentLength)
}
}
override fun onPageSuccess(index: Int, finished: Int, downloaded: Int, total: Int) {}
override fun onPageFailure(
index: Int,
error: String?,
finished: Int,
downloaded: Int,
total: Int
) {
notifyPageFailed(index, error)
}
override fun onFinish(finished: Int, downloaded: Int, total: Int) {}
override fun onGetImageSuccess(index: Int, image: Image?) {
notifyPageSucceed(index, image!!)
}
override fun onGetImageFailure(index: Int, error: String?) {
notifyPageFailed(index, error)
}
override fun preloadPages(pages: List<Int>, pair: Pair<Int, Int>) {
mSpiderQueen.preloadPages(pages, pair)
}
private class ReleaseTask(private var mSpiderQueen: SpiderQueen?) : Runnable {
override fun run() {
mSpiderQueen?.let { releaseSpiderQueen(it, SpiderQueen.MODE_READ) }
mSpiderQueen = null
}
}
}
@@ -1,62 +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.gallery;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.hippo.unifile.UniFile;
import eu.kanade.tachiyomi.ui.reader.loader.PageLoader;
public abstract class PageLoader2 extends PageLoader {
// With dot
public static final String[] SUPPORT_IMAGE_EXTENSIONS = {
".jpg", // Joint Photographic Experts Group
".jpeg",
".png", // Portable Network Graphics
".gif", // Graphics Interchange Format
};
public int getStartPage() {
return 0;
}
public void putStartPage(int page) {
}
/**
* @return without extension
*/
@NonNull
public abstract String getImageFilename(int index);
/**
* @return with extension
*/
@NonNull
public abstract String getImageFilenameWithExtension(int index);
public abstract boolean save(int index, @NonNull UniFile file);
/**
* @param filename without extension
*/
@Nullable
public abstract UniFile save(int index, @NonNull UniFile dir, @NonNull String filename);
}
@@ -0,0 +1,48 @@
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.gallery
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.ui.reader.loader.PageLoader
abstract class PageLoader2 : PageLoader() {
open val startPage: Int
get() = 0
open fun putStartPage(page: Int) {}
/**
* @return without extension
*/
abstract fun getImageFilename(index: Int): String
/**
* @return with extension
*/
abstract fun getImageFilenameWithExtension(index: Int): String
abstract fun save(index: Int, file: UniFile): Boolean
/**
* @param filename without extension
*/
abstract fun save(index: Int, dir: UniFile, filename: String): UniFile?
}
val SUPPORT_IMAGE_EXTENSIONS = arrayOf(
".jpg", // Joint Photographic Experts Group
".png", // Portable Network Graphics
".gif"
)
@@ -53,15 +53,15 @@ class AccountPreference @JvmOverloads constructor(
while (i < n) {
val cookie = cookies[i]
when (cookie.name) {
EhCookieStore.KEY_IPD_MEMBER_ID -> ipbMemberId = cookie.value
EhCookieStore.KEY_IPD_PASS_HASH -> ipbPassHash = cookie.value
EhCookieStore.KEY_IPB_MEMBER_ID -> ipbMemberId = cookie.value
EhCookieStore.KEY_IPB_PASS_HASH -> ipbPassHash = cookie.value
EhCookieStore.KEY_IGNEOUS -> igneous = cookie.value
}
i++
}
if (ipbMemberId != null || ipbPassHash != null || igneous != null) {
message = (EhCookieStore.KEY_IPD_MEMBER_ID + ": " + ipbMemberId + "<br>"
+ EhCookieStore.KEY_IPD_PASS_HASH + ": " + ipbPassHash + "<br>"
message = (EhCookieStore.KEY_IPB_MEMBER_ID + ": " + ipbMemberId + "<br>"
+ EhCookieStore.KEY_IPB_PASS_HASH + ": " + ipbPassHash + "<br>"
+ EhCookieStore.KEY_IGNEOUS + ": " + igneous)
setDialogMessage(
Html.fromHtml(
@@ -1,118 +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.preference;
import android.content.Context;
import android.util.AttributeSet;
import androidx.annotation.NonNull;
import com.google.android.material.snackbar.Snackbar;
import com.hippo.ehviewer.EhApplication;
import com.hippo.ehviewer.R;
import com.hippo.ehviewer.Settings;
import com.hippo.ehviewer.download.DownloadManager;
import com.hippo.unifile.UniFile;
import com.hippo.yorozuya.NumberUtils;
public class CleanRedundancyPreference extends TaskPreference {
public CleanRedundancyPreference(Context context) {
super(context);
}
public CleanRedundancyPreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
public CleanRedundancyPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@NonNull
@Override
protected Task onCreateTask() {
return new ClearTask(getContext());
}
private static class ClearTask extends Task {
private final DownloadManager mManager;
public ClearTask(@NonNull Context context) {
super(context);
mManager = EhApplication.getDownloadManager();
}
// True for cleared
private boolean clearFile(UniFile file) {
String name = file.getName();
if (name == null) {
return false;
}
int index = name.indexOf('-');
if (index >= 0) {
name = name.substring(0, index);
}
long gid = NumberUtils.parseLongSafely(name, -1L);
if (-1L == gid) {
return false;
}
if (mManager.containDownloadInfo(gid)) {
return false;
}
file.delete();
return true;
}
@Override
protected Object doInBackground(Void... params) {
UniFile dir = Settings.getDownloadLocation();
if (null == dir) {
return 0;
}
UniFile[] files = dir.listFiles();
if (null == files) {
return 0;
}
int count = 0;
for (UniFile f : files) {
if (clearFile(f)) {
++count;
}
}
return count;
}
@Override
protected void onPostExecute(Object o) {
int count;
if (o instanceof Integer) {
count = (Integer) o;
} else {
count = 0;
}
showTip(0 == count ?
mApplication.getString(R.string.settings_download_clean_redundancy_no_redundancy) :
mApplication.getString(R.string.settings_download_clean_redundancy_done, count), Snackbar.LENGTH_SHORT);
super.onPostExecute(o);
}
}
}
@@ -0,0 +1,74 @@
/*
* 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.preference
import android.content.Context
import android.util.AttributeSet
import com.hippo.ehviewer.download.DownloadManager as downloadManager
import com.hippo.ehviewer.GetText
import com.hippo.ehviewer.R
import com.hippo.ehviewer.Settings
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.lang.withUIContext
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
private val NO_REDUNDANCY =
GetText.getString(R.string.settings_download_clean_redundancy_no_redundancy)
private val CLEAR_REDUNDANCY_DONE =
{ cnt: Int -> GetText.getString(R.string.settings_download_clean_redundancy_done, cnt) }
private val FINAL_CLEAR_REDUNDANCY_MSG =
{ cnt: Int -> if (cnt == 0) NO_REDUNDANCY else CLEAR_REDUNDANCY_DONE(cnt) }
class CleanRedundancyPreference @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
) : TaskPreference(context, attrs) {
private fun clearFile(file: UniFile): Boolean {
var name = file.name ?: return false
val index = name.indexOf('-')
if (index >= 0) {
name = name.substring(0, index)
}
val gid = name.toLongOrNull() ?: return false
if (downloadManager.containDownloadInfo(gid)) {
return false
}
file.delete()
return true
}
private fun doRealWork(): Int {
return Settings.getDownloadLocation()?.listFiles()?.sumOf { clearFile(it).compareTo(false) } ?: 0
}
override fun launchJob() {
if (singletonJob?.isActive == true) singletonJob?.invokeOnCompletion {
launchUI {
dialog.dismiss()
}
}
else singletonJob = launch {
val cnt = doRealWork()
withUIContext {
showTip(FINAL_CLEAR_REDUNDANCY_MSG(cnt))
dialog.dismiss()
}
}
}
}
private var singletonJob: Job? = null
@@ -0,0 +1,86 @@
package com.hippo.ehviewer.preference
import android.content.Context
import android.content.DialogInterface
import android.util.AttributeSet
import android.view.View
import android.widget.Button
import androidx.appcompat.app.AlertDialog
import androidx.lifecycle.lifecycleScope
import com.hippo.ehviewer.R
import com.hippo.ehviewer.ui.SettingsActivity
import com.hippo.preference.DialogPreference
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.withUIContext
class ImageLimitsPreference(context: Context, attrs: AttributeSet) :
DialogPreference(context, attrs), View.OnClickListener {
private val mActivity = context as SettingsActivity
private val placeholder = context.getString(R.string.please_wait)
private lateinit var resetButton: Button
private lateinit var mDialog: AlertDialog
init {
updateSummary()
}
override fun onPrepareDialogBuilder(builder: AlertDialog.Builder) {
super.onPrepareDialogBuilder(builder)
builder.setMessage(placeholder)
}
override fun onDialogCreated(dialog: AlertDialog) {
super.onDialogCreated(dialog)
mDialog = dialog
resetButton = dialog.getButton(DialogInterface.BUTTON_POSITIVE)
resetButton.setOnClickListener(this)
resetButton.isEnabled = false
launchIOCatching {
mActivity.getImageLimits()
}
}
override fun onDialogClosed(positiveResult: Boolean) {
super.onDialogClosed(positiveResult)
updateSummary()
}
private fun updateSummary() {
summary = mActivity.mLimits?.run {
mActivity.getString(R.string.image_limits_summary, current, maximum)
} ?: mActivity.getString(R.string.image_limits_summary, 0, 0)
}
private fun launchIOCatching(block: suspend () -> Unit) = mActivity.lifecycleScope.launchIO {
runCatching {
block()
}.onFailure {
it.printStackTrace()
withUIContext {
mDialog.setMessage(it.message)
}
}.onSuccess {
withUIContext {
bind()
}
}
}
private fun bind() {
val (current, maximum, resetCost) = mActivity.mLimits!!
val (fundsGP, fundsC) = mActivity.mFunds!!
val cost = if (fundsGP >= resetCost) "$resetCost GP" else "$resetCost Credits"
val message = mActivity.getString(R.string.current_limits, "$current / $maximum", cost) +
"\n" + mActivity.getString(R.string.current_funds, "$fundsGP+", fundsC)
mDialog.setMessage(message)
resetButton.isEnabled = resetCost in 1..maxOf(fundsGP, fundsC)
}
override fun onClick(v: View) {
resetButton.isEnabled = false
mDialog.setMessage(placeholder)
launchIOCatching {
mActivity.resetImageLimits()
}
}
}
@@ -15,151 +15,111 @@
*/
package com.hippo.ehviewer.preference
import android.app.Activity
import android.content.Context
import android.util.AttributeSet
import androidx.preference.Preference
import com.hippo.ehviewer.EhApplication.Companion.downloadManager
import com.hippo.ehviewer.download.DownloadManager as downloadManager
import com.hippo.ehviewer.EhDB
import com.hippo.ehviewer.GetText
import com.hippo.ehviewer.R
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.EhEngine.fillGalleryListByApi
import com.hippo.ehviewer.client.EhUrl
import com.hippo.ehviewer.client.data.BaseGalleryInfo
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.download.DownloadManager
import com.hippo.ehviewer.spider.SpiderInfo
import com.hippo.ehviewer.spider.SpiderQueen
import com.hippo.ehviewer.ui.scene.BaseScene
import com.hippo.ehviewer.spider.readCompatFromUniFile
import com.hippo.unifile.UniFile
import com.hippo.util.ExceptionUtils
import com.hippo.yorozuya.IOUtils
import kotlinx.coroutines.runBlocking
import java.io.IOException
import java.io.InputStream
import java.util.Collections
import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.lang.withUIContext
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import moe.tarsin.coroutines.runSuspendCatching
class RestoreDownloadPreference constructor(
context: Context, attrs: AttributeSet?
private val RESTORE_NOT_FOUND = GetText.getString(R.string.settings_download_restore_not_found)
private val RESTORE_FAILED = GetText.getString(R.string.settings_download_restore_failed)
private val RESTORE_COUNT_MSG =
{ cnt: Int -> if (cnt == 0) RESTORE_NOT_FOUND else GetText.getString(R.string.settings_download_restore_successfully, cnt) }
class RestoreDownloadPreference @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
) : TaskPreference(context, attrs) {
override fun onCreateTask(): Task {
return RestoreTask(context)
}
private class RestoreTask(context: Context) : Task(context) {
private val mManager: DownloadManager = downloadManager
private var restoreDirCount = 0
private fun getRestoreItem(file: UniFile?): RestoreItem? {
if (null == file || !file.isDirectory) {
private val mManager: DownloadManager = downloadManager
private var restoreDirCount = 0
private fun getRestoreItem(file: UniFile?): RestoreItem? {
if (null == file || !file.isDirectory) return null
val siFile = file.findFile(SpiderQueen.SPIDER_INFO_FILENAME) ?: return null
return runCatching {
val spiderInfo = readCompatFromUniFile(siFile) ?: return null
var gid = spiderInfo.gid
var dirname = file.name
if (mManager.containDownloadInfo(gid)) {
// Restore download dir to avoid redownload
val dbdirname = EhDB.getDownloadDirname(gid)
if (null == dbdirname || dirname != dbdirname) {
EhDB.putDownloadDirname(gid, dirname)
restoreDirCount++
}
return null
}
val siFile = file.findFile(SpiderQueen.SPIDER_INFO_FILENAME) ?: return null
var `is`: InputStream? = null
return try {
`is` = siFile.openInputStream()
val spiderInfo = SpiderInfo.read(`is`) ?: return null
val gid = spiderInfo.gid
val dirname = file.name
if (mManager.containDownloadInfo(gid)) {
// Restore download dir to avoid redownload
val dbdirname = EhDB.getDownloadDirname(gid)
if (null == dbdirname || dirname != dbdirname) {
EhDB.putDownloadDirname(gid, dirname)
restoreDirCount++
}
return null
}
val token = spiderInfo.token
val restoreItem = RestoreItem()
restoreItem.gid = gid
restoreItem.token = token
restoreItem.dirname = dirname
restoreItem
} catch (e: IOException) {
null
} finally {
IOUtils.closeQuietly(`is`)
RestoreItem().apply {
gid = spiderInfo.gid
token = spiderInfo.token
dirname = file.name
}
}
}.getOrNull()
}
override fun doInBackground(vararg params: Void): Any? {
val dir = Settings.getDownloadLocation() ?: return null
val restoreItemList: MutableList<RestoreItem> = ArrayList()
val files = dir.listFiles() ?: return null
for (file in files) {
val restoreItem = getRestoreItem(file)
if (null != restoreItem) {
restoreItemList.add(restoreItem)
}
}
return if (0 == restoreItemList.size) {
Collections.EMPTY_LIST
} else try {
runBlocking {
fillGalleryListByApi(
ArrayList(restoreItemList),
EhUrl.referer
)
}
} catch (e: Throwable) {
ExceptionUtils.throwIfFatal(e)
e.printStackTrace()
null
}
}
override fun onPostExecute(o: Any) {
if (o !is List<*>) {
mActivity.showTip(R.string.settings_download_restore_failed, BaseScene.LENGTH_SHORT)
} else {
val list = o as List<RestoreItem>
if (list.isEmpty()) {
mActivity.showTip(
if (restoreDirCount == 0)
mActivity.getString(R.string.settings_download_restore_not_found)
else
mActivity.getString(
R.string.settings_download_restore_successfully,
restoreDirCount
),
BaseScene.LENGTH_SHORT
)
} else {
var count = 0
var i = 0
val n = list.size
while (i < n) {
val item = list[i]
// Avoid failed gallery info
if (null != item.title) {
// Put to download
mManager.addDownload(item, null)
// Put download dir to DB
EhDB.putDownloadDirname(item.gid, item.dirname)
count++
}
i++
}
showTip(
mActivity.getString(
R.string.settings_download_restore_successfully,
count + restoreDirCount
),
BaseScene.LENGTH_SHORT
)
val preference: Preference? = preference
if (null != preference) {
val context = preference.context
if (context is Activity) {
context.setResult(Activity.RESULT_OK)
}
}
}
}
super.onPostExecute(o)
}
private suspend fun doRealWork(): List<GalleryInfo>? {
val dir = Settings.getDownloadLocation() ?: return null
val files = dir.listFiles() ?: return null
val restoreItemList = files.mapNotNull { getRestoreItem(it) }
return runSuspendCatching {
fillGalleryListByApi(restoreItemList, EhUrl.referer)
}.getOrNull()
}
private class RestoreItem : BaseGalleryInfo() {
var dirname: String? = null
}
}
override fun launchJob() {
if (singletonJob?.isActive == true) singletonJob?.invokeOnCompletion {
launchUI {
dialog.dismiss()
}
}
else singletonJob = launch {
val result = doRealWork()
withUIContext {
if (result == null) {
showTip(RESTORE_FAILED)
} else {
if (result.isEmpty()) {
showTip(RESTORE_COUNT_MSG(restoreDirCount))
} else {
var count = 0
var i = 0
val n = result.size
while (i < n) {
val item = result[i]
// Avoid failed gallery info
if (null != item.title) {
// Put to download
mManager.addDownload(item, null)
// Put download dir to DB
EhDB.putDownloadDirname(item.gid, (item as RestoreItem).dirname)
count++
}
i++
}
showTip(RESTORE_COUNT_MSG(count + restoreDirCount))
}
}
dialog.dismiss()
}
}
}
}
private var singletonJob: Job? = null
@@ -1,75 +0,0 @@
/*
* Copyright 2019 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.preference;
import android.annotation.SuppressLint;
import android.content.Context;
import android.util.AttributeSet;
import androidx.preference.Preference;
import com.hippo.ehviewer.EhApplication;
import com.hippo.ehviewer.R;
import com.hippo.ehviewer.client.EhCookieStore;
import com.hippo.ehviewer.client.EhUrl;
import com.hippo.ehviewer.ui.SettingsActivity;
import com.hippo.ehviewer.ui.scene.BaseScene;
import okhttp3.HttpUrl;
public class SignInRequiredPreference extends Preference implements Preference.OnPreferenceClickListener {
@SuppressLint("StaticFieldLeak")
private SettingsActivity mActivity;
public SignInRequiredPreference(Context context) {
super(context);
init(context);
}
public SignInRequiredPreference(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public SignInRequiredPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
private void init(Context context) {
mActivity = (SettingsActivity) context;
setOnPreferenceClickListener(this);
}
@Override
public boolean onPreferenceClick(Preference preference) {
EhCookieStore store = EhApplication.getEhCookieStore();
HttpUrl e = HttpUrl.get(EhUrl.HOST_E);
HttpUrl ex = HttpUrl.get(EhUrl.HOST_EX);
if (store.contains(e, EhCookieStore.KEY_IPD_MEMBER_ID) ||
store.contains(e, EhCookieStore.KEY_IPD_PASS_HASH) ||
store.contains(ex, EhCookieStore.KEY_IPD_MEMBER_ID) ||
store.contains(ex, EhCookieStore.KEY_IPD_PASS_HASH)) {
return false;
} else {
mActivity.showTip(R.string.error_please_login_first, BaseScene.LENGTH_SHORT);
return true;
}
}
}
@@ -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.preference;
import android.annotation.SuppressLint;
import android.app.Dialog;
import android.content.Context;
import android.os.AsyncTask;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.AttributeSet;
import android.widget.Toast;
import androidx.annotation.CallSuper;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.appcompat.app.AlertDialog;
import androidx.customview.view.AbsSavedState;
import com.hippo.ehviewer.EhApplication;
import com.hippo.ehviewer.R;
import com.hippo.ehviewer.ui.SettingsActivity;
import com.hippo.preference.DialogPreference;
import com.hippo.util.IoThreadPoolExecutor;
import com.hippo.yorozuya.IntIdGenerator;
public abstract class TaskPreference extends DialogPreference {
private Task mTask;
private int mTaskId = IntIdGenerator.INVALID_ID;
public TaskPreference(Context context) {
super(context);
}
public TaskPreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
public TaskPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
builder.setTitle(null);
builder.setView(R.layout.preference_dialog_task);
builder.setCancelable(false);
}
@Override
protected void onDialogCreated(AlertDialog dialog) {
if (null == mTask) {
mTask = onCreateTask();
mTask.setPreference(this);
mTaskId = ((EhApplication) getContext().getApplicationContext()).putGlobalStuff(mTask);
mTask.executeOnExecutor(IoThreadPoolExecutor.getInstance());
}
}
protected void onTaskEnd() {
// Dismiss dialog
Dialog dialog = getDialog();
try {
dialog.dismiss();
} catch (Exception e) {
e.printStackTrace();
}
// Clear async
mTask = null;
mTaskId = IntIdGenerator.INVALID_ID;
}
@NonNull
protected abstract Task onCreateTask();
@Override
protected Parcelable onSaveInstanceState() {
final Parcelable superState = super.onSaveInstanceState();
final SavedState myState = new SavedState(superState);
myState.asyncTaskId = mTaskId;
return myState;
}
@Override
protected void onRestoreInstanceState(Parcelable state) {
if (state == null || !state.getClass().equals(SavedState.class)) {
// Didn't save state for us in onSaveInstanceState
super.onRestoreInstanceState(state);
return;
}
SavedState myState = (SavedState) state;
mTaskId = myState.asyncTaskId;
if (IntIdGenerator.INVALID_ID != mTaskId) {
Object o = ((EhApplication) getContext().getApplicationContext()).getGlobalStuff(mTaskId);
if (o instanceof Task) {
mTask = (Task) o;
mTask.setPreference(this);
}
}
if (null == mTask) {
mTaskId = IntIdGenerator.INVALID_ID;
}
// TODO if not task, show not reopen dialog
super.onRestoreInstanceState(myState.getSuperState());
}
private static class SavedState extends AbsSavedState {
public static final Parcelable.Creator<SavedState> CREATOR =
new Parcelable.Creator<SavedState>() {
@Override
public SavedState createFromParcel(Parcel in) {
return new SavedState(in);
}
@Override
public SavedState[] newArray(int size) {
return new SavedState[size];
}
};
int asyncTaskId;
public SavedState(Parcel source) {
super(source, SavedState.class.getClassLoader());
asyncTaskId = source.readInt();
}
public SavedState(Parcelable superState) {
super(superState);
}
@Override
public void writeToParcel(Parcel dest, int flags) {
super.writeToParcel(dest, flags);
dest.writeInt(asyncTaskId);
}
}
public abstract static class Task extends AsyncTask<Void, Void, Object> {
protected final EhApplication mApplication;
@SuppressLint("StaticFieldLeak")
protected final SettingsActivity mActivity;
@Nullable
private TaskPreference mPreference;
public Task(@NonNull Context context) {
mActivity = (SettingsActivity) context;
mApplication = (EhApplication) context.getApplicationContext();
}
public EhApplication getApplication() {
return mApplication;
}
@Nullable
public TaskPreference getPreference() {
return mPreference;
}
public void setPreference(@Nullable TaskPreference preference) {
mPreference = preference;
}
@CallSuper
@Override
protected void onPostExecute(Object o) {
mApplication.removeGlobalStuff(this);
if (null != mPreference) {
mPreference.onTaskEnd();
}
}
protected void showTip(@StringRes int id, int length) {
try {
mActivity.showTip(id, length);
} catch (Exception e) {
Toast.makeText(mApplication, id, length).show();
}
}
protected void showTip(CharSequence message, int length) {
try {
mActivity.showTip(message, length);
} catch (Exception e) {
Toast.makeText(mApplication, message, length).show();
}
}
}
}
@@ -0,0 +1,53 @@
/*
* 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.preference
import android.content.Context
import android.util.AttributeSet
import androidx.appcompat.app.AlertDialog
import com.google.android.material.snackbar.Snackbar
import com.hippo.ehviewer.R
import com.hippo.ehviewer.ui.SettingsActivity
import com.hippo.preference.DialogPreference
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
abstract class TaskPreference @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
) : DialogPreference(context, attrs), CoroutineScope {
override val coroutineContext = Dispatchers.IO + Job()
protected lateinit var dialog: AlertDialog
override fun onPrepareDialogBuilder(builder: AlertDialog.Builder) {
builder.setTitle(null)
builder.setView(R.layout.preference_dialog_task)
builder.setCancelable(false)
}
abstract fun launchJob()
override fun onDialogCreated(dialog: AlertDialog) {
this.dialog = dialog
launchJob()
}
protected fun showTip(msg: String) {
(context as SettingsActivity).showTip(
msg,
Snackbar.LENGTH_SHORT
)
}
}
@@ -16,9 +16,9 @@
package com.hippo.ehviewer.preference
import android.content.Context
import android.content.pm.PackageManager
import android.util.AttributeSet
import androidx.preference.Preference
import com.hippo.ehviewer.BuildConfig
import com.hippo.ehviewer.R
class VersionPreference @JvmOverloads constructor(
@@ -26,13 +26,7 @@ class VersionPreference @JvmOverloads constructor(
) : Preference(context, attrs) {
init {
setTitle(R.string.settings_about_version)
@Suppress("DEPRECATION") val version: String = try {
val pInfo = context.packageManager.getPackageInfo(context.packageName, 0)
pInfo.versionName
} catch (e: PackageManager.NameNotFoundException) {
e.printStackTrace()
context.getString(R.string.error_unknown)
}
summary = version
summary = "${BuildConfig.VERSION_NAME} (${BuildConfig.COMMIT_SHA})\n" +
context.getString(R.string.settings_about_build_time, BuildConfig.BUILD_TIME)
}
}
@@ -1,483 +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.spider;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
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;
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 DiskCache sCache; // We use data to store image file, and metadata for image type
private final GalleryInfo mGalleryInfo;
private final long mGid;
@Nullable
private UniFile mDownloadDir;
private volatile int mMode = SpiderQueen.MODE_READ;
public SpiderDen(GalleryInfo galleryInfo) {
mGalleryInfo = galleryInfo;
mGid = galleryInfo.getGid();
mDownloadDir = getGalleryDownloadDir(galleryInfo.getGid());
}
public static void initialize(Context context) {
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) {
UniFile dir = Settings.getDownloadLocation();
// Read from DB
String dirname = EhDB.getDownloadDirname(gid);
if (dir != null && dirname != null) {
// Some dirname may be invalid in some version
dirname = FileUtils.sanitizeFilename(dirname);
EhDB.putDownloadDirname(gid, dirname);
return dir.subFile(dirname);
} else {
return null;
}
}
/**
* @param extension with dot
*/
public static String generateImageFilename(int index, String extension) {
return String.format(Locale.US, "%08d%s", index + 1, extension);
}
@Nullable
private static UniFile findImageFile(UniFile dir, int index) {
for (String extension : PageLoader2.SUPPORT_IMAGE_EXTENSIONS) {
String filename = generateImageFilename(index, extension);
UniFile file = dir.findFile(filename);
if (file != null) {
return file;
}
}
return null;
}
public void setMode(@SpiderQueen.Mode int mode) {
mMode = mode;
if (mode == SpiderQueen.MODE_DOWNLOAD) {
ensureDownloadDir();
}
}
private boolean ensureDownloadDir() {
if (mDownloadDir == null) {
var dirname = FileUtils.sanitizeFilename(mGid + "-" + EhUtils.getSuitableTitle(mGalleryInfo));
EhDB.putDownloadDirname(mGid, dirname);
mDownloadDir = getGalleryDownloadDir(mGid);
}
return mDownloadDir != null && mDownloadDir.ensureDir();
}
public boolean isReady() {
return switch (mMode) {
case SpiderQueen.MODE_READ -> sCache != null;
case SpiderQueen.MODE_DOWNLOAD -> mDownloadDir != null && mDownloadDir.isDirectory();
default -> false;
};
}
@Nullable
public UniFile getDownloadDir() {
if (mDownloadDir == null) {
mDownloadDir = getGalleryDownloadDir(mGid);
}
return mDownloadDir != null && mDownloadDir.isDirectory() ? mDownloadDir : null;
}
private boolean containInCache(int index) {
if (sCache == null) {
return false;
}
String key = EhCacheKeyFactory.getImageKey(mGid, index);
var snapshot = sCache.get(key);
var exist = snapshot != null;
if (exist) snapshot.close();
return exist;
}
private boolean containInDownloadDir(int index) {
UniFile dir = getDownloadDir();
if (dir == null) {
return false;
}
// Find image file in download dir
return findImageFile(dir, index) != null;
}
/**
* @param extension with dot
*/
private String fixExtension(String extension) {
if (Utilities.contain(PageLoader2.SUPPORT_IMAGE_EXTENSIONS, extension)) {
return extension;
} else {
return PageLoader2.SUPPORT_IMAGE_EXTENSIONS[0];
}
}
private boolean copyFromCacheToDownloadDir(int index) {
if (sCache == null) {
return false;
}
UniFile dir = getDownloadDir();
if (dir == null) {
return false;
}
// Find image file in cache
String key = EhCacheKeyFactory.getImageKey(mGid, index);
var snapshot = sCache.get(key);
if (snapshot == null) {
return false;
}
OutputStream os = null;
try (snapshot; FileInputStream is = new FileInputStream(snapshot.getData().toFile()); BufferedSource buf = Okio.buffer(Okio.source(snapshot.getMetadata().toFile()))) {
// Get 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(is, os);
return true;
} catch (IOException e) {
return false;
} finally {
IOUtils.closeQuietly(os);
}
}
public boolean contain(int index) {
if (mMode == SpiderQueen.MODE_READ) {
return containInCache(index) || containInDownloadDir(index);
} else if (mMode == SpiderQueen.MODE_DOWNLOAD) {
return containInDownloadDir(index) || copyFromCacheToDownloadDir(index);
} else {
return false;
}
}
private boolean removeFromCache(int index) {
if (sCache == null) {
return false;
}
String key = EhCacheKeyFactory.getImageKey(mGid, index);
return sCache.remove(key);
}
private boolean removeFromDownloadDir(int index) {
UniFile dir = getDownloadDir();
if (dir == null) {
return false;
}
boolean result = false;
for (int i = 0, n = PageLoader2.SUPPORT_IMAGE_EXTENSIONS.length; i < n; i++) {
String filename = generateImageFilename(index, PageLoader2.SUPPORT_IMAGE_EXTENSIONS[i]);
UniFile file = dir.subFile(filename);
if (file != null) {
result |= file.delete();
}
}
return result;
}
public boolean remove(int index) {
boolean result = removeFromCache(index);
result |= removeFromDownloadDir(index);
return result;
}
@Nullable
private OutputStreamPipe openCacheOutputStreamPipe(int index, @Nullable String extension) {
if (sCache == null) {
return null;
}
String key = EhCacheKeyFactory.getImageKey(mGid, index);
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);
}
};
}
/**
* @param extension without dot
*/
@Nullable
private OutputStreamPipe openDownloadOutputStreamPipe(int index, @Nullable String extension) {
UniFile dir = getDownloadDir();
if (dir == null) {
return null;
}
extension = fixExtension('.' + extension);
UniFile file = dir.createFile(generateImageFilename(index, extension));
if (file != null) {
return new UniFileOutputStreamPipe(file);
} else {
return null;
}
}
@Nullable
public OutputStreamPipe openOutputStreamPipe(int index, @Nullable String extension) {
if (mMode == SpiderQueen.MODE_READ) {
// Return the download pipe is the gallery has been downloaded
OutputStreamPipe pipe = openDownloadOutputStreamPipe(index, extension);
if (pipe == null) {
pipe = openCacheOutputStreamPipe(index, extension);
}
return pipe;
} else if (mMode == SpiderQueen.MODE_DOWNLOAD) {
return openDownloadOutputStreamPipe(index, extension);
} else {
return null;
}
}
@Nullable
private InputStreamPipe openCacheInputStreamPipe(int index) {
if (sCache == null) {
return null;
}
String key = EhCacheKeyFactory.getImageKey(mGid, index);
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
public InputStreamPipe openDownloadInputStreamPipe(int index) {
UniFile dir = getDownloadDir();
if (dir == null) {
return null;
}
for (int i = 0; i < 2; i++) {
UniFile file = findImageFile(dir, index);
if (file != null) {
return new UniFileInputStreamPipe(file);
} else if (!copyFromCacheToDownloadDir(index)) {
return null;
}
}
return null;
}
@Nullable
public InputStreamPipe openInputStreamPipe(int index) {
if (mMode == SpiderQueen.MODE_READ) {
InputStreamPipe pipe = openCacheInputStreamPipe(index);
if (pipe == null) {
pipe = openDownloadInputStreamPipe(index);
}
return pipe;
} else if (mMode == SpiderQueen.MODE_DOWNLOAD) {
return openDownloadInputStreamPipe(index);
} else {
return null;
}
}
@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;
}
}
@@ -0,0 +1,371 @@
/*
* 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.spider
import android.graphics.ImageDecoder
import android.os.ParcelFileDescriptor
import android.os.ParcelFileDescriptor.MODE_READ_WRITE
import coil.decode.DecodeUtils
import coil.decode.FrameDelayRewritingSource
import coil.decode.isGif
import coil.disk.DiskCache
import com.hippo.copyTo
import com.hippo.ehviewer.EhApplication
import com.hippo.ehviewer.EhApplication.Companion.application
import com.hippo.ehviewer.EhDB
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.EhCacheKeyFactory
import com.hippo.ehviewer.client.EhRequestBuilder
import com.hippo.ehviewer.client.EhUtils.getSuitableTitle
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.coil.edit
import com.hippo.ehviewer.coil.read
import com.hippo.ehviewer.gallery.SUPPORT_IMAGE_EXTENSIONS
import com.hippo.image.Image.CloseableSource
import com.hippo.unifile.RawFile
import com.hippo.unifile.UniFile
import com.hippo.yorozuya.FileUtils
import com.hippo.yorozuya.MathUtils
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.ensureActive
import moe.tarsin.coroutines.runSuspendCatching
import okhttp3.executeAsync
import okio.buffer
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.IOException
import java.nio.channels.FileChannel
import java.util.Locale
import kotlin.io.path.readText
private val client = EhApplication.okHttpClient
private val contentResolver = application.contentResolver
class SpiderDen(private val mGalleryInfo: GalleryInfo) {
private val mGid: Long = mGalleryInfo.gid
private var mDownloadDir: UniFile? = getGalleryDownloadDir(mGid)
@Volatile
private var mMode = SpiderQueen.MODE_READ
fun setMode(@SpiderQueen.Mode mode: Int) {
mMode = mode
if (mode == SpiderQueen.MODE_DOWNLOAD) {
ensureDownloadDir()
}
}
private fun ensureDownloadDir(): Boolean {
mDownloadDir?.let { return it.ensureDir() }
val title = getSuitableTitle(mGalleryInfo)
val dirname = FileUtils.sanitizeFilename("$mGid-$title")
EhDB.putDownloadDirname(mGid, dirname)
mDownloadDir = getGalleryDownloadDir(mGid)
return mDownloadDir?.ensureDir() ?: false
}
val isReady: Boolean
get() = when (mMode) {
SpiderQueen.MODE_READ -> true
SpiderQueen.MODE_DOWNLOAD -> mDownloadDir?.isDirectory ?: false
else -> false
}
val downloadDir: UniFile?
get() {
if (mDownloadDir == null) {
mDownloadDir = getGalleryDownloadDir(mGid)
}
return mDownloadDir?.takeIf { it.isDirectory }
}
private fun containInCache(index: Int): Boolean {
val key = EhCacheKeyFactory.getImageKey(mGid, index)
return sCache[key]?.apply { close() } != null
}
private fun containInDownloadDir(index: Int): Boolean {
val dir = downloadDir ?: return false
return findImageFile(dir, index) != null
}
/**
* @param extension with dot
*/
private fun fixExtension(extension: String): String {
return extension.takeIf { SUPPORT_IMAGE_EXTENSIONS.contains(it) }
?: SUPPORT_IMAGE_EXTENSIONS[0]
}
private fun copyFromCacheToDownloadDir(index: Int): Boolean {
val dir = downloadDir ?: return false
val key = EhCacheKeyFactory.getImageKey(mGid, index)
return runCatching {
sCache.read(key) {
val extension = fixExtension("." + metadata.toFile().readText())
val file = dir.createFile(generateImageFilename(index, extension)) ?: return false
(file.openOutputStream() as FileOutputStream).use { outputStream ->
ParcelFileDescriptor.open(data.toFile(), MODE_READ_WRITE).use {
it copyTo outputStream.fd
}
}
}
}.getOrElse {
it.printStackTrace()
false
}
}
operator fun contains(index: Int): Boolean {
return when (mMode) {
SpiderQueen.MODE_READ -> {
containInCache(index) || containInDownloadDir(index)
}
SpiderQueen.MODE_DOWNLOAD -> {
containInDownloadDir(index) || copyFromCacheToDownloadDir(index)
}
else -> {
false
}
}
}
private fun removeFromCache(index: Int): Boolean {
val key = EhCacheKeyFactory.getImageKey(mGid, index)
return sCache.remove(key)
}
private fun removeFromDownloadDir(index: Int): Boolean {
return downloadDir?.let { findImageFile(it, index)?.delete() } ?: false
}
fun remove(index: Int): Boolean {
return removeFromCache(index) or removeFromDownloadDir(index)
}
private fun findDownloadFileForIndex(index: Int, extension: String): UniFile? {
val dir = downloadDir ?: return null
val ext = fixExtension(".$extension")
return dir.createFile(generateImageFilename(index, ext))
}
@Throws(IOException::class)
suspend fun makeHttpCallAndSaveImage(
index: Int,
url: String,
referer: String?,
notifyProgress: (Long, Long, Int) -> Unit
): Boolean {
val call = client.newCall(EhRequestBuilder(url, referer).build())
call.executeAsync().use { response ->
if (response.code >= 400) return false
val body = response.body
val extension = body.contentType()?.subtype ?: "jpg"
val length = body.contentLength()
suspend fun doSave(chan: FileChannel): Long {
var receivedSize: Long = 0
body.source().use { responseSource ->
val source = if (DecodeUtils.isGif(responseSource)) FrameDelayRewritingSource(
responseSource
).buffer() else responseSource
source.use {
while (true) {
currentCoroutineContext().ensureActive()
val bytesRead = chan.transferFrom(it, receivedSize, TRANSFER_BLOCK)
receivedSize += bytesRead
notifyProgress(body.contentLength(), receivedSize, bytesRead.toInt())
if (bytesRead.toInt() == 0) break
}
}
}
return receivedSize
}
findDownloadFileForIndex(index, extension)?.runSuspendCatching {
openOutputStream().use { outputStream ->
(outputStream as FileOutputStream).channel.use { return doSave(it) == length }
}
}?.onFailure {
it.printStackTrace()
return false
}
// Read Mode, allow save to cache
if (mMode == SpiderQueen.MODE_READ) {
val key = EhCacheKeyFactory.getImageKey(mGid, index)
var received: Long = 0
runSuspendCatching {
sCache.edit(key) {
metadata.toFile().writeText(extension)
data.toFile().outputStream().use { outputStream ->
outputStream.channel.use { received = doSave(it) }
}
}
}.onFailure {
it.printStackTrace()
}.onSuccess {
return received == length
}
}
return false
}
}
fun saveToUniFile(index: Int, file: UniFile): Boolean {
(file.openOutputStream() as FileOutputStream).use { outputStream ->
outputStream.channel.use { outChannel ->
val key = EhCacheKeyFactory.getImageKey(mGid, index)
fun copy(inputStream: FileInputStream) {
inputStream.channel.use {
outChannel.transferFrom(it, 0, it.size())
}
}
// Read from diskCache first
sCache[key]?.use { snapshot ->
runCatching {
snapshot.data.toFile().inputStream().use { copy(it) }
}.onSuccess {
return true
}.onFailure {
it.printStackTrace()
return false
}
}
downloadDir?.let { uniFile ->
runCatching {
findImageFile(uniFile, index)?.openInputStream()?.use {
copy(it as FileInputStream)
}
}.onFailure {
it.printStackTrace()
return false
}.onSuccess {
return true
}
}
return false
}
}
}
fun checkPlainText(index: Int): Boolean {
return false
}
fun getExtension(index: Int): String? {
val key = EhCacheKeyFactory.getImageKey(mGid, index)
return sCache[key]?.use { it.metadata.toNioPath().readText() }
?: downloadDir?.let { findImageFile(it, index) }
?.name.let { FileUtils.getExtensionFromFilename(it) }
}
fun getImageSource(index: Int): CloseableSource? {
if (mMode == SpiderQueen.MODE_READ) {
val key = EhCacheKeyFactory.getImageKey(mGid, index)
val snapshot: DiskCache.Snapshot? = sCache[key]
if (snapshot != null) {
val source = ImageDecoder.createSource(snapshot.data.toFile())
return object : CloseableSource {
override val source: ImageDecoder.Source
get() = source
override fun close() {
snapshot.close()
}
}
}
}
val dir = downloadDir ?: return null
var file = findImageFile(dir, index)
if (file != null && mMode == SpiderQueen.MODE_DOWNLOAD) {
if (copyFromCacheToDownloadDir(index)) {
file = findImageFile(dir, index)
}
}
if (file is RawFile) {
val source = ImageDecoder.createSource(file.mFile)
return object : CloseableSource {
override val source: ImageDecoder.Source
get() = source
override fun close() {}
}
}
if (file != null) {
val source = ImageDecoder.createSource(contentResolver, file.uri)
return object : CloseableSource {
override val source: ImageDecoder.Source
get() = source
override fun close() {}
}
}
return null
}
companion object {
private const val TRANSFER_BLOCK: Long = 8192
// We use data to store image file, and metadata for image type
private val sCache by lazy {
DiskCache.Builder().directory(File(application.cacheDir, "gallery_image"))
.maxSizeBytes(
MathUtils.clamp(Settings.getReadCacheSize(), 40, 1280).toLong() * 1024 * 1024
).build()
}
private val COMPAT_IMAGE_EXTENSIONS = SUPPORT_IMAGE_EXTENSIONS + ".jpeg"
fun getGalleryDownloadDir(gid: Long): UniFile? {
val dir = Settings.getDownloadLocation()
// Read from DB
var dirname = EhDB.getDownloadDirname(gid)
return if (dir != null && dirname != null) {
// Some dirname may be invalid in some version
dirname = FileUtils.sanitizeFilename(dirname)
EhDB.putDownloadDirname(gid, dirname)
dir.subFile(dirname)
} else {
null
}
}
/**
* @param extension with dot
*/
fun generateImageFilename(index: Int, extension: String?): String {
return String.format(Locale.US, "%08d%s", index + 1, extension)
}
private fun findImageFile(dir: UniFile, index: Int): UniFile? {
for (extension in COMPAT_IMAGE_EXTENSIONS) {
val filename = generateImageFilename(index, extension)
val file = dir.findFile(filename)
if (file != null) {
return file
}
}
return null
}
}
}
@@ -1,207 +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.spider;
import android.text.TextUtils;
import android.util.Log;
import android.util.SparseArray;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.hippo.unifile.UniFile;
import com.hippo.yorozuya.IOUtils;
import com.hippo.yorozuya.NumberUtils;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
public class SpiderInfo {
static final String TOKEN_FAILED = "failed";
private static final String TAG = SpiderInfo.class.getSimpleName();
private static final String VERSION_STR = "VERSION";
private static final int VERSION = 2;
public int startPage = 0;
public long gid = -1;
public String token = null;
public int pages = -1;
public int previewPages = -1;
public int previewPerPage = -1;
public SparseArray<String> pTokenMap = null;
public static SpiderInfo read(@Nullable UniFile file) {
if (file == null) {
return null;
}
InputStream is = null;
try {
is = file.openInputStream();
return read(is);
} catch (IOException e) {
return null;
} finally {
IOUtils.closeQuietly(is);
}
}
private static int getStartPage(String str) {
if (null == str) {
return 0;
}
int startPage = 0;
for (int i = 0, n = str.length(); i < n; i++) {
startPage *= 16;
char ch = str.charAt(i);
if (ch >= '0' && ch <= '9') {
startPage += ch - '0';
} else if (ch >= 'a' && ch <= 'f') {
startPage += ch - 'a' + 10;
}
}
return Math.max(startPage, 0);
}
private static int getVersion(String str) {
if (null == str) {
return -1;
}
if (str.startsWith(VERSION_STR)) {
return NumberUtils.parseIntSafely(str.substring(VERSION_STR.length()), -1);
} else {
return 1;
}
}
@Nullable
@SuppressWarnings("InfiniteLoopStatement")
public static SpiderInfo read(@Nullable InputStream is) {
if (null == is) {
return null;
}
SpiderInfo spiderInfo = null;
try {
spiderInfo = new SpiderInfo();
// Get version
String line = IOUtils.readAsciiLine(is);
int version = getVersion(line);
if (version == VERSION) {
// Read next line
line = IOUtils.readAsciiLine(is);
} else if (version == 1) {
// pass
} else {
// Invalid version
return null;
}
// Start page
spiderInfo.startPage = getStartPage(line);
// Gid
spiderInfo.gid = Long.parseLong(IOUtils.readAsciiLine(is));
// Token
spiderInfo.token = IOUtils.readAsciiLine(is);
// Deprecated, mode, skip it
IOUtils.readAsciiLine(is);
// Preview pages
spiderInfo.previewPages = Integer.parseInt(IOUtils.readAsciiLine(is));
// Preview pre page
line = IOUtils.readAsciiLine(is);
if (version == 1) {
// Skip it
} else {
spiderInfo.previewPerPage = Integer.parseInt(line);
}
// Pages
spiderInfo.pages = Integer.parseInt(IOUtils.readAsciiLine(is));
// Check pages
if (spiderInfo.pages <= 0) {
return null;
}
// PToken
spiderInfo.pTokenMap = new SparseArray<>(spiderInfo.pages);
while (true) { // EOFException will raise
line = IOUtils.readAsciiLine(is);
int pos = line.indexOf(" ");
if (pos > 0) {
int index = Integer.parseInt(line.substring(0, pos));
String pToken = line.substring(pos + 1);
if (!TextUtils.isEmpty(pToken)) {
spiderInfo.pTokenMap.put(index, pToken);
}
} else {
Log.e(TAG, "Can't parse index and pToken, index = " + pos);
}
}
} catch (IOException | NumberFormatException e) {
// Ignore
}
if (spiderInfo == null || spiderInfo.gid == -1 || spiderInfo.token == null ||
spiderInfo.pages == -1 || spiderInfo.pTokenMap == null) {
return null;
} else {
return spiderInfo;
}
}
public void write(@NonNull OutputStream os) {
OutputStreamWriter writer = null;
try {
writer = new OutputStreamWriter(os);
writer.write(VERSION_STR);
writer.write(Integer.toString(VERSION));
writer.write("\n");
writer.write(String.format("%08x", Math.max(startPage, 0))); // Avoid negative
writer.write("\n");
writer.write(Long.toString(gid));
writer.write("\n");
writer.write(token);
writer.write("\n");
writer.write("1");
writer.write("\n");
writer.write(Integer.toString(previewPages));
writer.write("\n");
writer.write(Integer.toString(previewPerPage));
writer.write("\n");
writer.write(Integer.toString(pages));
writer.write("\n");
for (int i = 0; i < pTokenMap.size(); i++) {
Integer key = pTokenMap.keyAt(i);
String value = pTokenMap.valueAt(i);
if (TOKEN_FAILED.equals(value) || TextUtils.isEmpty(value)) {
continue;
}
writer.write(Integer.toString(key));
writer.write(" ");
writer.write(value);
writer.write("\n");
}
writer.flush();
} catch (IOException e) {
// Ignore
} finally {
IOUtils.closeQuietly(writer);
IOUtils.closeQuietly(os);
}
}
}
@@ -0,0 +1,75 @@
package com.hippo.ehviewer.spider
import coil.disk.DiskCache
import com.hippo.compat.readLegacySpiderInfo
import com.hippo.ehviewer.EhApplication
import com.hippo.unifile.UniFile
import kotlinx.serialization.Serializable
import kotlinx.serialization.cbor.Cbor
import kotlinx.serialization.decodeFromByteArray
import kotlinx.serialization.encodeToByteArray
import java.io.File
@Serializable
class SpiderInfo @JvmOverloads constructor(
val gid: Long,
val pages: Int,
val pTokenMap: MutableMap<Int, String> = hashMapOf(),
var startPage: Int = 0,
var token: String? = null,
var previewPages: Int = -1,
var previewPerPage: Int = -1
)
fun SpiderInfo.write(file: UniFile) {
file.openOutputStream().use {
it.write(Cbor.encodeToByteArray(this))
}
}
fun SpiderInfo.saveToCache() {
val entry = spiderInfoCache.edit(gid.toString()) ?: return
runCatching {
entry.data.toFile().writeBytes(Cbor.encodeToByteArray(this))
}.onFailure {
it.printStackTrace()
entry.abort()
}.onSuccess {
entry.commit()
}
}
private val spiderInfoCache by lazy {
DiskCache.Builder()
.directory(File(EhApplication.application.cacheDir, "spider_info_v2_1"))
.maxSizeBytes(20 * 1024 * 1024).build()
}
fun readFromCache(gid: Long): SpiderInfo? {
val snapshot = spiderInfoCache[gid.toString()] ?: return null
return runCatching {
snapshot.use {
return Cbor.decodeFromByteArray(it.data.toFile().readBytes())
}
}.onFailure {
it.printStackTrace()
}.getOrNull()
}
fun readCompatFromUniFile(file: UniFile): SpiderInfo? {
return runCatching {
file.openInputStream().use {
Cbor.decodeFromByteArray<SpiderInfo>(it.readBytes())
}
}.getOrNull() ?: runCatching {
file.openInputStream().use { readLegacySpiderInfo(it) }
}.getOrNull()
}
const val TOKEN_FAILED = "failed"
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,830 @@
/*
* Copyright 2016 Hippo Seven
* Rewrite with Kotlin coroutines, Tarsin Norbin 2023
*
* 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.spider
import android.util.Log
import androidx.annotation.IntDef
import androidx.collection.LongSparseArray
import androidx.collection.set
import com.hippo.ehviewer.EhApplication.Companion.okHttpClient
import com.hippo.ehviewer.GetText
import com.hippo.ehviewer.R
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.EhEngine
import com.hippo.ehviewer.client.EhRequestBuilder
import com.hippo.ehviewer.client.EhUrl
import com.hippo.ehviewer.client.EhUrl.getGalleryDetailUrl
import com.hippo.ehviewer.client.EhUrl.getGalleryMultiPageViewerUrl
import com.hippo.ehviewer.client.EhUrl.referer
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.client.exception.ParseException
import com.hippo.ehviewer.client.parser.GalleryDetailParser.parsePages
import com.hippo.ehviewer.client.parser.GalleryDetailParser.parsePreviewPages
import com.hippo.ehviewer.client.parser.GalleryDetailParser.parsePreviewSet
import com.hippo.ehviewer.client.parser.GalleryMultiPageViewerPTokenParser
import com.hippo.ehviewer.client.parser.GalleryPageUrlParser
import com.hippo.image.Image
import com.hippo.unifile.UniFile
import com.hippo.util.ExceptionUtils
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.launchNonCancellable
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.delay
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.sync.withPermit
import moe.tarsin.coroutines.runSuspendCatching
import okhttp3.executeAsync
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicInteger
class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineScope {
override val coroutineContext = Dispatchers.IO + Job()
@Volatile
lateinit var mPageStateArray: IntArray
lateinit var mSpiderInfo: SpiderInfo
val mSpiderDen: SpiderDen = SpiderDen(galleryInfo)
val mPagePercentMap = ConcurrentHashMap<Int, Float>()
private val mPageStateLock = Any()
private val mDownloadedPages = AtomicInteger(0)
private val mFinishedPages = AtomicInteger(0)
private val mPageErrorMap = ConcurrentHashMap<Int, String>()
private val mSpiderListeners: MutableList<OnSpiderListener> = ArrayList()
private var mReadReference = 0
private var mDownloadReference = 0
fun addOnSpiderListener(listener: OnSpiderListener) {
synchronized(mSpiderListeners) { mSpiderListeners.add(listener) }
}
fun removeOnSpiderListener(listener: OnSpiderListener) {
synchronized(mSpiderListeners) { mSpiderListeners.remove(listener) }
}
private fun notifyGetPages(pages: Int) {
synchronized(mSpiderListeners) {
mSpiderListeners.forEach { it.onGetPages(pages) }
}
}
fun notifyGet509(index: Int) {
synchronized(mSpiderListeners) {
mSpiderListeners.forEach { it.onGet509(index) }
}
}
fun notifyPageDownload(index: Int, contentLength: Long, receivedSize: Long, bytesRead: Int) {
synchronized(mSpiderListeners) {
mSpiderListeners.forEach {
it.onPageDownload(
index,
contentLength,
receivedSize,
bytesRead
)
}
}
}
private fun notifyPageSuccess(index: Int) {
synchronized(mSpiderListeners) {
mSpiderListeners.forEach {
it.onPageSuccess(
index,
mFinishedPages.get(),
mDownloadedPages.get(),
mPageStateArray.size
)
}
}
}
private fun notifyPageFailure(index: Int, error: String?) {
synchronized(mSpiderListeners) {
mSpiderListeners.forEach {
it.onPageFailure(
index,
error,
mFinishedPages.get(),
mDownloadedPages.get(),
mPageStateArray.size
)
}
}
}
private fun notifyAllPageDownloaded() {
synchronized(mSpiderListeners) {
mSpiderListeners.forEach {
it.onFinish(
mFinishedPages.get(),
mDownloadedPages.get(),
mPageStateArray.size
)
}
}
}
fun notifyGetImageSuccess(index: Int, image: Image) {
synchronized(mSpiderListeners) {
mSpiderListeners.forEach {
it.onGetImageSuccess(index, image)
}
}
}
fun notifyGetImageFailure(index: Int, error: String) {
synchronized(mSpiderListeners) {
mSpiderListeners.forEach {
it.onGetImageFailure(index, error)
}
}
}
private var downloadMode = false
val isReady
get() = this::mSpiderInfo.isInitialized && this::mPageStateArray.isInitialized
@Synchronized
private fun updateMode() {
if (!isReady) return
val mode: Int = if (mDownloadReference > 0) {
MODE_DOWNLOAD
} else {
MODE_READ
}
mSpiderDen.setMode(mode)
// Update download page
val intoDownloadMode = mode == MODE_DOWNLOAD
if (intoDownloadMode && !downloadMode) {
// Clear download state
synchronized(mPageStateLock) {
val temp: IntArray = mPageStateArray
var i = 0
val n = temp.size
while (i < n) {
val oldState = temp[i]
if (STATE_DOWNLOADING != oldState) {
temp[i] = STATE_NONE
}
i++
}
mDownloadedPages.lazySet(0)
mFinishedPages.lazySet(0)
mPageErrorMap.clear()
mPagePercentMap.clear()
}
mWorkerScope.enterDownloadMode()
}
downloadMode = intoDownloadMode
}
private fun setMode(@Mode mode: Int) {
when (mode) {
MODE_READ -> mReadReference++
MODE_DOWNLOAD -> mDownloadReference++
}
check(mDownloadReference <= 1) { "mDownloadReference can't more than 0" }
}
private fun clearMode(@Mode mode: Int) {
when (mode) {
MODE_READ -> mReadReference--
MODE_DOWNLOAD -> mDownloadReference--
}
check(!(mReadReference < 0 || mDownloadReference < 0)) { "Mode reference < 0" }
}
private val prepareJob = launchIO { doPrepare() }
private suspend fun doPrepare() {
mSpiderInfo = readSpiderInfoFromLocal() ?: readSpiderInfoFromInternet() ?: return
mPageStateArray = IntArray(mSpiderInfo.pages)
notifyGetPages(mSpiderInfo.pages)
}
suspend fun awaitReady(): Boolean {
prepareJob.join()
return isReady
}
suspend fun awaitStartPage(): Int {
prepareJob.join()
if (!isReady) return 0
return mSpiderInfo.startPage
}
private fun stop() {
launchNonCancellable { runCatching { writeSpiderInfoToLocal() } }
cancel()
}
val size
get() = mPageStateArray.size
val error: String?
get() = null
fun forceRequest(index: Int) {
request(index, true)
}
fun request(index: Int) {
request(index, false)
}
private fun getPageState(index: Int): Int {
synchronized(mPageStateLock) {
return if (index >= 0 && index < mPageStateArray.size) {
mPageStateArray[index]
} else {
STATE_NONE
}
}
}
fun cancelRequest(index: Int) {
mWorkerScope.cancelDecode(index)
}
fun preloadPages(pages: List<Int>, pair: Pair<Int, Int>) {
mWorkerScope.updateRAList(pages, pair)
}
private fun request(index: Int, force: Boolean) {
// Get page state
val state = getPageState(index)
// Fix state for force
if (force && (state == STATE_FINISHED || state == STATE_FAILED) || state == STATE_FAILED) {
// Update state to none at once
updatePageState(index, STATE_NONE)
}
mWorkerScope.launch(index, force)
}
fun save(index: Int, file: UniFile): Boolean {
val state = getPageState(index)
return if (STATE_FINISHED != state) {
false
} else mSpiderDen.saveToUniFile(index, file)
}
fun save(index: Int, dir: UniFile, filename: String): UniFile? {
val state = getPageState(index)
if (STATE_FINISHED != state) {
return null
}
val ext = mSpiderDen.getExtension(index)
val dst = dir.subFile(if (null != ext) "$filename.$ext" else filename) ?: return null
return if (!mSpiderDen.saveToUniFile(index, dst)) null else dst
}
fun getExtension(index: Int): String? {
val state = getPageState(index)
return if (STATE_FINISHED != state) {
null
} else mSpiderDen.getExtension(index)
}
val startPage: Int
get() = mSpiderInfo.startPage
fun putStartPage(page: Int) {
mSpiderInfo.startPage = page
}
private fun readSpiderInfoFromLocal(): SpiderInfo? {
return mSpiderDen.downloadDir?.run {
findFile(SPIDER_INFO_FILENAME)?.let { file ->
readCompatFromUniFile(file)?.takeIf {
it.gid == galleryInfo.gid && it.token == galleryInfo.token
}
}
}
?: readFromCache(galleryInfo.gid)?.takeIf { it.gid == galleryInfo.gid && it.token == galleryInfo.token }
}
@Throws(ParseException::class)
private fun readPreviews(body: String, index: Int, spiderInfo: SpiderInfo) {
spiderInfo.previewPages = parsePreviewPages(body)
val previewSet = parsePreviewSet(body)
if (previewSet.size() > 0) {
if (index == 0) {
spiderInfo.previewPerPage = previewSet.size()
} else {
spiderInfo.previewPerPage = previewSet.getPosition(0) / index
}
}
var i = 0
val n = previewSet.size()
while (i < n) {
val result = GalleryPageUrlParser.parse(previewSet.getPageUrlAt(i))
if (result != null) {
spiderInfo.pTokenMap[result.page] = result.pToken
}
i++
}
}
private suspend fun readSpiderInfoFromInternet(): SpiderInfo? {
val request = EhRequestBuilder(
getGalleryDetailUrl(
galleryInfo.gid, galleryInfo.token, 0, false
), referer
).build()
return runSuspendCatching {
okHttpClient.newCall(request).executeAsync().use { response ->
val body = response.body.string()
val pages = parsePages(body)
val spiderInfo = SpiderInfo(galleryInfo.gid, pages)
spiderInfo.token = galleryInfo.token
readPreviews(body, 0, spiderInfo)
spiderInfo
}
}.onFailure {
it.printStackTrace()
}.getOrNull()
}
fun getPTokenFromMultiPageViewer(index: Int): String? {
val spiderInfo = mSpiderInfo
val url = getGalleryMultiPageViewerUrl(
galleryInfo.gid, galleryInfo.token!!
)
val referer = referer
val request = EhRequestBuilder(url, referer).build()
try {
okHttpClient.newCall(request).execute().use { response ->
val body = response.body.string()
val list = GalleryMultiPageViewerPTokenParser.parse(body)
for (i in list.indices) {
spiderInfo.pTokenMap[i] = list[i]
}
return spiderInfo.pTokenMap[index]
}
} catch (e: Throwable) {
ExceptionUtils.throwIfFatal(e)
return null
}
}
fun getPTokenFromInternet(index: Int): String? {
val spiderInfo = mSpiderInfo
// Check previewIndex
var previewIndex: Int
previewIndex = if (spiderInfo.previewPerPage >= 0) {
index / spiderInfo.previewPerPage
} else {
0
}
if (spiderInfo.previewPages > 0) {
previewIndex = previewIndex.coerceAtMost(spiderInfo.previewPages - 1)
}
val url = getGalleryDetailUrl(
galleryInfo.gid, galleryInfo.token, previewIndex, false
)
val referer = referer
val request = EhRequestBuilder(url, referer).build()
try {
okHttpClient.newCall(request).execute().use { response ->
val body = response.body.string()
readPreviews(body, previewIndex, spiderInfo)
return spiderInfo.pTokenMap[index]
}
} catch (e: Throwable) {
ExceptionUtils.throwIfFatal(e)
return null
}
}
@Synchronized
private fun writeSpiderInfoToLocal() {
if (!isReady) return
mSpiderDen.downloadDir?.run { createFile(SPIDER_INFO_FILENAME).also { mSpiderInfo.write(it) } }
mSpiderInfo.saveToCache()
}
private fun isStateDone(state: Int): Boolean {
return state == STATE_FINISHED || state == STATE_FAILED
}
@JvmOverloads
fun updatePageState(index: Int, @State state: Int, error: String? = null) {
var oldState: Int
synchronized(mPageStateLock) {
oldState = mPageStateArray[index]
mPageStateArray[index] = state
if (!isStateDone(oldState) && isStateDone(state)) {
mDownloadedPages.incrementAndGet()
} else if (isStateDone(oldState) && !isStateDone(state)) {
mDownloadedPages.decrementAndGet()
}
if (oldState != STATE_FINISHED && state == STATE_FINISHED) {
mFinishedPages.incrementAndGet()
} else if (oldState == STATE_FINISHED && state != STATE_FINISHED) {
mFinishedPages.decrementAndGet()
}
// Clear
if (state == STATE_DOWNLOADING) {
mPageErrorMap.remove(index)
} else if (isStateDone(state)) {
mPagePercentMap.remove(index)
}
// Get default error
if (state == STATE_FAILED) {
mPageErrorMap[index] = error ?: GetText.getString(R.string.error_unknown)
}
}
// Notify listeners
if (state == STATE_FAILED) {
notifyPageFailure(index, error)
} else if (state == STATE_FINISHED) {
notifyPageSuccess(index)
}
if (mDownloadedPages.get() == size) notifyAllPageDownloaded()
}
@IntDef(MODE_READ, MODE_DOWNLOAD)
@Retention
annotation class Mode
@IntDef(STATE_NONE, STATE_DOWNLOADING, STATE_FINISHED, STATE_FAILED)
@Retention(AnnotationRetention.SOURCE)
annotation class State
interface OnSpiderListener {
fun onGetPages(pages: Int)
fun onGet509(index: Int)
fun onPageDownload(index: Int, contentLength: Long, receivedSize: Long, bytesRead: Int)
fun onPageSuccess(index: Int, finished: Int, downloaded: Int, total: Int)
fun onPageFailure(index: Int, error: String?, finished: Int, downloaded: Int, total: Int)
fun onFinish(finished: Int, downloaded: Int, total: Int)
fun onGetImageSuccess(index: Int, image: Image?)
fun onGetImageFailure(index: Int, error: String?)
}
companion object {
const val MODE_READ = 0
const val MODE_DOWNLOAD = 1
const val STATE_NONE = 0
const val STATE_DOWNLOADING = 1
const val STATE_FINISHED = 2
const val STATE_FAILED = 3
const val SPIDER_INFO_FILENAME = ".ehviewer"
private val sQueenMap = LongSparseArray<SpiderQueen>()
@JvmStatic
fun obtainSpiderQueen(galleryInfo: GalleryInfo, @Mode mode: Int): SpiderQueen {
val gid = galleryInfo.gid
return (sQueenMap[gid] ?: SpiderQueen(galleryInfo).also { sQueenMap[gid] = it }).apply {
setMode(mode)
launchIO { if (awaitReady()) updateMode() }
}
}
@JvmStatic
fun releaseSpiderQueen(queen: SpiderQueen, @Mode mode: Int) {
queen.run {
clearMode(mode)
if (mReadReference == 0 && mDownloadReference == 0) {
stop()
sQueenMap.remove(galleryInfo.gid)
} else {
launchIO { if (awaitReady()) updateMode() }
}
}
}
}
private val mWorkerScope = object {
private val mFetcherJobMap = hashMapOf<Int, Job>()
private val mSemaphore = Semaphore(Settings.getMultiThreadDownload())
private val pTokenLock = Mutex()
private var showKey: String? = null
private val showKeyLock = Mutex()
private val mDownloadDelay = Settings.getDownloadDelay()
private var isDownloadMode = false
fun cancelDecode(index: Int) {
decoder.cancel(index)
}
@Synchronized
fun enterDownloadMode() {
if (isDownloadMode) return
updateRAList((0 until size).toList())
isDownloadMode = true
}
fun updateRAList(list: List<Int>, cancelBounds: Pair<Int, Int> = 0 to Int.MAX_VALUE) {
if (isDownloadMode) return
synchronized(mFetcherJobMap) {
mFetcherJobMap.forEach { (i, job) ->
if (i < cancelBounds.first || i > cancelBounds.second)
job.cancel()
}
list.forEach {
if (mFetcherJobMap[it]?.isActive != true)
doLaunchDownloadJob(it, false)
}
}
}
private fun doLaunchDownloadJob(index: Int, force: Boolean) {
val state = mPageStateArray[index]
if (!force && state == STATE_FINISHED) return
val currentJob = mFetcherJobMap[index]
if (force) currentJob?.cancel()
if (currentJob?.isActive != true) {
mFetcherJobMap[index] = launch {
mSemaphore.withPermit {
doInJob(index, force)
}
}.apply {
invokeOnCompletion {
if (getPageState(index) == STATE_DOWNLOADING) {
Log.d(WORKER_DEBUG_TAG, "Download image cancelled $index")
mSpiderDen.remove(index)
}
}
}
}
}
fun launch(index: Int, force: Boolean = false) {
check(index in 0 until size)
if (!isDownloadMode) synchronized(mFetcherJobMap) { doLaunchDownloadJob(index, force) }
if (force) decoder.cancel(index)
decoder.launch(index)
}
private suspend fun doInJob(index: Int, force: Boolean) {
fun getPToken(index: Int): String? {
if (index !in 0 until size) return null
return mSpiderInfo.pTokenMap[index].takeIf { it != TOKEN_FAILED }
?: getPTokenFromInternet(index)
?: getPTokenFromInternet(index)
?: getPTokenFromMultiPageViewer(index)
}
updatePageState(index, STATE_DOWNLOADING)
if (!force && index in mSpiderDen) {
return updatePageState(index, STATE_FINISHED)
}
if (force) {
pTokenLock.withLock {
val pToken = mSpiderInfo.pTokenMap[index]
if (pToken == TOKEN_FAILED) mSpiderInfo.pTokenMap.remove(index)
}
}
val previousPToken: String?
val pToken: String
pTokenLock.withLock {
pToken = getPToken(index) ?: return updatePageState(
index,
STATE_FAILED,
PTOKEN_FAILED_MESSAGE
).also {
mSpiderInfo.pTokenMap[index] = TOKEN_FAILED
}
previousPToken = getPToken(index - 1)
}
var skipHathKey: String? = null
val skipHathKeys = mutableListOf<String>()
var originImageUrl: String? = null
var error: String? = null
var forceHtml = false
var leakSkipHathKey = false
repeat(3) {
var imageUrl: String? = null
var localShowKey: String?
showKeyLock.withLock {
localShowKey = showKey
if (localShowKey == null || forceHtml) {
if (leakSkipHathKey) return@repeat
var pageUrl = EhUrl.getPageUrl(mSpiderInfo.gid, index, pToken)
// Add skipHathKey
if (skipHathKey != null) {
pageUrl += if ("?" in pageUrl) {
"&nl=$skipHathKey"
} else {
"?nl=$skipHathKey"
}
}
runSuspendCatching {
EhEngine.getGalleryPage(pageUrl, mSpiderInfo.gid, mSpiderInfo.token)
.also {
if (check509(it.imageUrl)) {
// Get 509
notifyGet509(index)
error = ERROR_509
return@repeat
}
}
}.onSuccess { result ->
imageUrl = result.imageUrl
skipHathKey = result.skipHathKey.takeIf { it.isNotBlank() }
originImageUrl = result.originImageUrl
localShowKey = result.showKey
if (skipHathKey != null) {
if (skipHathKey in skipHathKeys) {
// Duplicate skip hath key
leakSkipHathKey = true
} else {
skipHathKeys.add(skipHathKey!!)
}
} else {
leakSkipHathKey = true
}
showKey = result.showKey
}.onFailure {
if (it is ParseException && "Key mismatch" == it.message) {
// Show key is wrong, enter a new loop to get the new show key
if (showKey == localShowKey) showKey = null
return@repeat
} else {
error = ExceptionUtils.getReadableString(it)
return@repeat
}
}
}
}
if (imageUrl == null) {
if (localShowKey == null) {
error = "ShowKey error"
return@repeat
}
runSuspendCatching {
EhEngine.getGalleryPageApi(
mSpiderInfo.gid,
index,
pToken,
localShowKey,
previousPToken
).also {
if (check509(it.imageUrl)) {
// Get 509
notifyGet509(index)
error = ERROR_509
return@repeat
}
}
}.onFailure {
if (it is ParseException && "Key mismatch" == it.message) {
// Show key is wrong, enter a new loop to get the new show key
if (showKey == localShowKey) showKey = null
} else {
error = ExceptionUtils.getReadableString(it)
}
return@repeat
}.onSuccess {
imageUrl = it.imageUrl
skipHathKey = it.skipHathKey
originImageUrl = it.originImageUrl
}
}
val targetImageUrl: String?
val referer: String?
if (Settings.getDownloadOriginImage() && !originImageUrl.isNullOrBlank()) {
targetImageUrl = originImageUrl
referer = EhUrl.getPageUrl(mSpiderInfo.gid, index, pToken)
} else {
targetImageUrl = imageUrl
referer = null
}
if (targetImageUrl == null) {
error = "TargetImageUrl error"
return@repeat
}
Log.d(WORKER_DEBUG_TAG, targetImageUrl)
runSuspendCatching {
Log.d(WORKER_DEBUG_TAG, "Start download image $index")
val success: Boolean = mSpiderDen.makeHttpCallAndSaveImage(
index,
targetImageUrl,
referer
) { contentLength: Long, receivedSize: Long, bytesRead: Int ->
mPagePercentMap[index] = receivedSize.toFloat() / contentLength
notifyPageDownload(index, contentLength, receivedSize, bytesRead)
}
if (!success) {
Log.e(WORKER_DEBUG_TAG, "Can't download all of image data")
error = "Incomplete"
forceHtml = true
return@repeat
}
if (mSpiderDen.checkPlainText(index)) {
error = ""
forceHtml = true
return@repeat
}
Log.d(WORKER_DEBUG_TAG, "Download image succeed $index")
updatePageState(index, STATE_FINISHED)
delay(mDownloadDelay.toLong())
return
}.onFailure {
it.printStackTrace()
error = NETWORK_ERROR
forceHtml = true
}
Log.d(WORKER_DEBUG_TAG, "End download image $index")
}
mSpiderDen.remove(index)
updatePageState(index, STATE_FAILED, error)
}
private val decoder = object {
private val mSemaphore = Semaphore(4)
private val mDecodeJobMap = hashMapOf<Int, Job>()
fun cancel(index: Int) {
synchronized(mDecodeJobMap) {
mDecodeJobMap.remove(index)?.cancel()
}
}
fun launch(index: Int) {
synchronized(mDecodeJobMap) {
val currentJob = mDecodeJobMap[index]
if (currentJob?.isActive != true) {
mDecodeJobMap[index] = launch {
doInJob(index)
}
}
}
}
private suspend fun doInJob(index: Int) {
mFetcherJobMap[index]?.takeIf { it.isActive }?.join()
val src = mSpiderDen.getImageSource(index) ?: return
val image = mSemaphore.withPermit { Image.decode(src) }
runCatching {
currentCoroutineContext().ensureActive()
}.onFailure {
image?.recycle()
throw it
}
if (image == null) {
notifyGetImageFailure(index, DECODE_ERROR)
} else {
notifyGetImageSuccess(index, image)
}
}
}
}
}
private val PTOKEN_FAILED_MESSAGE = GetText.getString(R.string.error_get_ptoken_error)
private val ERROR_509 = GetText.getString(R.string.error_509)
private val NETWORK_ERROR = GetText.getString(R.string.error_socket)
private val DECODE_ERROR = GetText.getString(R.string.error_decoding_failed)
private val URL_509_ARRAY = arrayOf(
"https://ehgt.org/g/509.gif",
"https://ehgt.org/g/509s.gif",
"https://exhentai.org/img/509.gif",
"https://exhentai.org/img/509s.gif"
)
private const val WORKER_DEBUG_TAG = "SpiderQueenWorker"
private fun check509(url: String) = url in URL_509_ARRAY
Loaded 100 of 228 files, more files were not shown because too many files have changed in this diff. Show more