Repository: xb2016/EhViewer-NekoInverter
Branch: master
Commit: 8b8607e6b4b8
Files: 615
Total size: 2.8 MB
Directory structure:
gitextract_k4e444b1/
├── .editorconfig
├── .github/
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug-report.yml
│ │ └── config.yml
│ └── workflows/
│ ├── ci.yml
│ └── releases.yml
├── .gitignore
├── LICENSE
├── NOTICE
├── README.md
├── app/
│ ├── .gitignore
│ ├── build.gradle.kts
│ ├── keystore/
│ │ └── androidkey.jks
│ ├── proguard-rules.pro
│ ├── schemas/
│ │ └── com.hippo.network.CookiesDatabase/
│ │ ├── 1.json
│ │ └── 2.json
│ └── src/
│ ├── debug/
│ │ └── res/
│ │ └── values/
│ │ └── strings.xml
│ └── main/
│ ├── AndroidManifest.xml
│ ├── cpp/
│ │ ├── 0001-Insert-link-libs.patch
│ │ ├── 0002-Fix-zip_time-performance.patch
│ │ ├── 0003-Use-UTF-8-as-default-charset-on-bionic.patch
│ │ ├── CMakeLists.txt
│ │ ├── archive.c
│ │ ├── ehviewer.h
│ │ ├── gifutils.c
│ │ ├── hash.c
│ │ ├── image.c
│ │ ├── natsort/
│ │ │ ├── strnatcmp.c
│ │ │ └── strnatcmp.h
│ │ └── nettle/
│ │ ├── .gitignore
│ │ ├── CMakeLists.txt
│ │ ├── config.h
│ │ ├── keymap.h
│ │ ├── rotors.h
│ │ └── version.h
│ ├── java/
│ │ ├── com/
│ │ │ └── hippo/
│ │ │ ├── app/
│ │ │ │ ├── CheckBoxDialogBuilder.kt
│ │ │ │ ├── EditTextCheckBoxDialogBuilder.kt
│ │ │ │ ├── EditTextDialogBuilder.kt
│ │ │ │ └── ListCheckBoxDialogBuilder.kt
│ │ │ ├── database/
│ │ │ │ ├── MSQLiteBuilder.kt
│ │ │ │ └── MSQLiteOpenHelper.kt
│ │ │ ├── drawable/
│ │ │ │ ├── AddDeleteDrawable.kt
│ │ │ │ ├── BatteryDrawable.kt
│ │ │ │ ├── DrawerArrowDrawable.kt
│ │ │ │ ├── PreciselyClipDrawable.kt
│ │ │ │ ├── TriangleDrawable.kt
│ │ │ │ ├── UnikeryDrawable.kt
│ │ │ │ └── WrapDrawable.kt
│ │ │ ├── easyrecyclerview/
│ │ │ │ ├── EasyRecyclerView.kt
│ │ │ │ ├── FastScroller.kt
│ │ │ │ ├── HandlerDrawable.kt
│ │ │ │ ├── LayoutManagerUtils.kt
│ │ │ │ ├── LinearDividerItemDecoration.kt
│ │ │ │ ├── MarginItemDecoration.kt
│ │ │ │ ├── SimpleHolder.kt
│ │ │ │ └── SimpleSmoothScroller.kt
│ │ │ ├── ehviewer/
│ │ │ │ ├── AppConfig.java
│ │ │ │ ├── Crash.kt
│ │ │ │ ├── EhApplication.kt
│ │ │ │ ├── EhDB.kt
│ │ │ │ ├── EhProxySelector.kt
│ │ │ │ ├── FavouriteStatusRouter.java
│ │ │ │ ├── GetText.kt
│ │ │ │ ├── Settings.kt
│ │ │ │ ├── UrlOpener.kt
│ │ │ │ ├── WindowInsetsAnimationHelper.java
│ │ │ │ ├── client/
│ │ │ │ │ ├── EhCacheKeyFactory.kt
│ │ │ │ │ ├── EhClient.kt
│ │ │ │ │ ├── EhCookieStore.kt
│ │ │ │ │ ├── EhEngine.kt
│ │ │ │ │ ├── EhFilter.kt
│ │ │ │ │ ├── EhRequest.kt
│ │ │ │ │ ├── EhRequestBuilder.kt
│ │ │ │ │ ├── EhTagDatabase.kt
│ │ │ │ │ ├── EhUrl.kt
│ │ │ │ │ ├── EhUrlOpener.kt
│ │ │ │ │ ├── EhUtils.kt
│ │ │ │ │ ├── data/
│ │ │ │ │ │ ├── AbstractGalleryInfo.kt
│ │ │ │ │ │ ├── BaseGalleryInfo.kt
│ │ │ │ │ │ ├── FavListUrlBuilder.kt
│ │ │ │ │ │ ├── GalleryComment.kt
│ │ │ │ │ │ ├── GalleryCommentList.kt
│ │ │ │ │ │ ├── GalleryDetail.kt
│ │ │ │ │ │ ├── GalleryInfo.kt
│ │ │ │ │ │ ├── GalleryPreview.kt
│ │ │ │ │ │ ├── GalleryTagGroup.kt
│ │ │ │ │ │ ├── LargePreviewSet.kt
│ │ │ │ │ │ ├── ListUrlBuilder.kt
│ │ │ │ │ │ ├── NormalPreviewSet.kt
│ │ │ │ │ │ └── PreviewSet.kt
│ │ │ │ │ ├── exception/
│ │ │ │ │ │ ├── CloudflareBypassException.kt
│ │ │ │ │ │ ├── EhException.kt
│ │ │ │ │ │ ├── InsufficientFundsException.kt
│ │ │ │ │ │ ├── NoHAtHClientException.kt
│ │ │ │ │ │ ├── NotLoggedInException.kt
│ │ │ │ │ │ ├── OffensiveException.kt
│ │ │ │ │ │ ├── ParseException.kt
│ │ │ │ │ │ ├── PiningException.kt
│ │ │ │ │ │ └── QuotaExceededException.kt
│ │ │ │ │ └── parser/
│ │ │ │ │ ├── ArchiveParser.kt
│ │ │ │ │ ├── EventPaneParser.kt
│ │ │ │ │ ├── FavoritesParser.kt
│ │ │ │ │ ├── ForumsParser.kt
│ │ │ │ │ ├── GalleryApiParser.kt
│ │ │ │ │ ├── GalleryDetailParser.kt
│ │ │ │ │ ├── GalleryDetailUrlParser.kt
│ │ │ │ │ ├── GalleryListParser.kt
│ │ │ │ │ ├── GalleryListUrlParser.kt
│ │ │ │ │ ├── GalleryMultiPageViewerParser.kt
│ │ │ │ │ ├── GalleryNotAvailableParser.kt
│ │ │ │ │ ├── GalleryPageApiParser.kt
│ │ │ │ │ ├── GalleryPageParser.kt
│ │ │ │ │ ├── GalleryPageUrlParser.kt
│ │ │ │ │ ├── GalleryTokenApiParser.kt
│ │ │ │ │ ├── HomeParser.kt
│ │ │ │ │ ├── ParserUtils.kt
│ │ │ │ │ ├── ProfileParser.kt
│ │ │ │ │ ├── RateGalleryParser.kt
│ │ │ │ │ ├── SignInParser.kt
│ │ │ │ │ ├── TorrentParser.kt
│ │ │ │ │ ├── UserConfigParser.kt
│ │ │ │ │ ├── VoteCommentParser.kt
│ │ │ │ │ └── VoteTagParser.kt
│ │ │ │ ├── coil/
│ │ │ │ │ ├── DiskCache.kt
│ │ │ │ │ ├── DownloadThumbInterceptor.kt
│ │ │ │ │ ├── LockPool.kt
│ │ │ │ │ ├── MergeInterceptor.kt
│ │ │ │ │ └── NamedMutex.kt
│ │ │ │ ├── dao/
│ │ │ │ │ ├── BasicDao.kt
│ │ │ │ │ ├── BookmarkInfo.kt
│ │ │ │ │ ├── BookmarksDao.kt
│ │ │ │ │ ├── DownloadDirname.kt
│ │ │ │ │ ├── DownloadDirnameDao.kt
│ │ │ │ │ ├── DownloadInfo.kt
│ │ │ │ │ ├── DownloadLabel.kt
│ │ │ │ │ ├── DownloadLabelDao.kt
│ │ │ │ │ ├── DownloadsDao.kt
│ │ │ │ │ ├── EhDatabase.kt
│ │ │ │ │ ├── Filter.kt
│ │ │ │ │ ├── FilterDao.kt
│ │ │ │ │ ├── HistoryDao.kt
│ │ │ │ │ ├── HistoryInfo.kt
│ │ │ │ │ ├── LocalFavoriteInfo.kt
│ │ │ │ │ ├── LocalFavoritesDao.kt
│ │ │ │ │ ├── QuickSearch.kt
│ │ │ │ │ └── QuickSearchDao.kt
│ │ │ │ ├── download/
│ │ │ │ │ ├── DownloadManager.kt
│ │ │ │ │ └── DownloadService.kt
│ │ │ │ ├── gallery/
│ │ │ │ │ ├── ArchiveGalleryProvider.kt
│ │ │ │ │ ├── EhGalleryProvider.kt
│ │ │ │ │ └── GalleryProvider2.kt
│ │ │ │ ├── jni/
│ │ │ │ │ ├── Archive.kt
│ │ │ │ │ ├── GifUtils.kt
│ │ │ │ │ ├── Hash.kt
│ │ │ │ │ └── Image.kt
│ │ │ │ ├── preference/
│ │ │ │ │ ├── AccountPreference.kt
│ │ │ │ │ ├── CleanRedundancyPreference.kt
│ │ │ │ │ ├── ClearSearchHistoryPreference.kt
│ │ │ │ │ ├── ImageLimitsPreference.kt
│ │ │ │ │ ├── ProxyPreference.kt
│ │ │ │ │ ├── RestoreDownloadPreference.kt
│ │ │ │ │ ├── TaskPreference.kt
│ │ │ │ │ ├── UserAgentPreference.kt
│ │ │ │ │ └── VersionPreference.kt
│ │ │ │ ├── shortcuts/
│ │ │ │ │ └── ShortcutsActivity.kt
│ │ │ │ ├── spider/
│ │ │ │ │ ├── DownloadInfoMagics.kt
│ │ │ │ │ ├── SpiderDen.kt
│ │ │ │ │ ├── SpiderInfo.kt
│ │ │ │ │ └── SpiderQueen.kt
│ │ │ │ ├── ui/
│ │ │ │ │ ├── CommonOperations.kt
│ │ │ │ │ ├── EhActivity.kt
│ │ │ │ │ ├── GalleryActivity.kt
│ │ │ │ │ ├── MainActivity.kt
│ │ │ │ │ ├── SettingsActivity.kt
│ │ │ │ │ ├── WebViewActivity.kt
│ │ │ │ │ ├── dialog/
│ │ │ │ │ │ └── SelectItemWithIconAdapter.kt
│ │ │ │ │ ├── fragment/
│ │ │ │ │ │ ├── AboutFragment.kt
│ │ │ │ │ │ ├── AdvancedFragment.kt
│ │ │ │ │ │ ├── BaseFragment.kt
│ │ │ │ │ │ ├── BasePreferenceFragment.kt
│ │ │ │ │ │ ├── DownloadFragment.kt
│ │ │ │ │ │ ├── EhFragment.kt
│ │ │ │ │ │ ├── FilterFragment.kt
│ │ │ │ │ │ ├── MyTagsFragment.kt
│ │ │ │ │ │ ├── PrivacyFragment.kt
│ │ │ │ │ │ ├── ReadFragment.kt
│ │ │ │ │ │ ├── SetSecurityFragment.kt
│ │ │ │ │ │ ├── SettingsFragment.kt
│ │ │ │ │ │ └── UConfigFragment.kt
│ │ │ │ │ └── scene/
│ │ │ │ │ ├── BaseScene.kt
│ │ │ │ │ ├── CookieSignInScene.kt
│ │ │ │ │ ├── DownloadsScene.kt
│ │ │ │ │ ├── EhCallback.kt
│ │ │ │ │ ├── EnterGalleryDetailTransaction.kt
│ │ │ │ │ ├── FavoritesScene.kt
│ │ │ │ │ ├── GalleryAdapter.kt
│ │ │ │ │ ├── GalleryCommentsScene.kt
│ │ │ │ │ ├── GalleryDetailScene.kt
│ │ │ │ │ ├── GalleryHolder.kt
│ │ │ │ │ ├── GalleryInfoScene.kt
│ │ │ │ │ ├── GalleryListScene.kt
│ │ │ │ │ ├── GalleryPreviewsScene.kt
│ │ │ │ │ ├── HistoryScene.kt
│ │ │ │ │ ├── ProgressScene.kt
│ │ │ │ │ ├── SecurityScene.kt
│ │ │ │ │ ├── SelectSiteScene.kt
│ │ │ │ │ ├── SignInScene.kt
│ │ │ │ │ ├── SolidScene.kt
│ │ │ │ │ ├── ToolbarScene.kt
│ │ │ │ │ ├── TransitionNameFactory.kt
│ │ │ │ │ └── WebViewSignInScene.kt
│ │ │ │ ├── util/
│ │ │ │ │ └── WebViewExtensions.kt
│ │ │ │ └── widget/
│ │ │ │ ├── AdvanceSearchTable.kt
│ │ │ │ ├── CategoryTable.kt
│ │ │ │ ├── DialogWebChromeClient.java
│ │ │ │ ├── EhStageLayout.kt
│ │ │ │ ├── FixedThumb.kt
│ │ │ │ ├── GalleryGuideView.java
│ │ │ │ ├── GalleryHeader.java
│ │ │ │ ├── GalleryInfoContentHelper.kt
│ │ │ │ ├── GalleryRatingBar.java
│ │ │ │ ├── ImageSearchLayout.kt
│ │ │ │ ├── ResizeableFixedThumb.kt
│ │ │ │ ├── ReversibleSeekBar.java
│ │ │ │ ├── SearchBar.kt
│ │ │ │ ├── SearchDatabase.java
│ │ │ │ ├── SearchEditText.java
│ │ │ │ ├── SearchLayout.kt
│ │ │ │ ├── SeekBarPanel.kt
│ │ │ │ ├── SimpleRatingView.java
│ │ │ │ └── TileThumb.java
│ │ │ ├── glgallery/
│ │ │ │ ├── DownUpDetector.java
│ │ │ │ ├── Fling.java
│ │ │ │ ├── GalleryPageView.java
│ │ │ │ ├── GalleryProvider.kt
│ │ │ │ ├── GalleryView.java
│ │ │ │ ├── GestureRecognizer.java
│ │ │ │ ├── ImageView.java
│ │ │ │ ├── PagerLayoutManager.java
│ │ │ │ ├── ScrollLayoutManager.java
│ │ │ │ └── SimpleAdapter.java
│ │ │ ├── glview/
│ │ │ │ ├── anim/
│ │ │ │ │ ├── AlphaAnimation.java
│ │ │ │ │ ├── Animation.java
│ │ │ │ │ ├── CanvasAnimation.java
│ │ │ │ │ └── FloatAnimation.java
│ │ │ │ ├── glrenderer/
│ │ │ │ │ ├── BasicTexture.java
│ │ │ │ │ ├── CanvasTexture.java
│ │ │ │ │ ├── GLCanvas.java
│ │ │ │ │ ├── GLES11Canvas.java
│ │ │ │ │ ├── GLES11IdImpl.java
│ │ │ │ │ ├── GLES20Canvas.java
│ │ │ │ │ ├── GLES20IdImpl.java
│ │ │ │ │ ├── GLId.java
│ │ │ │ │ ├── GLPaint.java
│ │ │ │ │ ├── MovableTextTexture.java
│ │ │ │ │ ├── NativeTexture.java
│ │ │ │ │ ├── RawTexture.java
│ │ │ │ │ ├── SpriteTexture.java
│ │ │ │ │ ├── StringTexture.java
│ │ │ │ │ ├── Texture.java
│ │ │ │ │ ├── TiledTexture.java
│ │ │ │ │ └── UploadedTexture.java
│ │ │ │ ├── image/
│ │ │ │ │ ├── GLImageMovableTextView.java
│ │ │ │ │ ├── ImageMovableTextTexture.java
│ │ │ │ │ ├── ImageSpriteTexture.java
│ │ │ │ │ ├── ImageTexture.java
│ │ │ │ │ └── ImageWrapper.java
│ │ │ │ ├── util/
│ │ │ │ │ └── GalleryUtils.java
│ │ │ │ ├── view/
│ │ │ │ │ ├── AnimationTime.java
│ │ │ │ │ ├── GLRoot.java
│ │ │ │ │ ├── GLRootView.java
│ │ │ │ │ ├── GLView.java
│ │ │ │ │ ├── Gravity.java
│ │ │ │ │ ├── OrientationSource.java
│ │ │ │ │ ├── TouchHelper.java
│ │ │ │ │ └── TouchOwner.java
│ │ │ │ └── widget/
│ │ │ │ ├── GLFrameLayout.java
│ │ │ │ ├── GLLinearLayout.java
│ │ │ │ ├── GLProgressView.java
│ │ │ │ └── GLTextureView.java
│ │ │ ├── image/
│ │ │ │ └── Image.kt
│ │ │ ├── network/
│ │ │ │ ├── CookieDatabase.kt
│ │ │ │ ├── CookieSet.kt
│ │ │ │ ├── InetValidator.kt
│ │ │ │ ├── StatusCodeException.kt
│ │ │ │ └── UrlBuilder.kt
│ │ │ ├── okhttp/
│ │ │ │ └── ChromeRequestBuilder.kt
│ │ │ ├── preference/
│ │ │ │ ├── DialogPreference.kt
│ │ │ │ └── UrlPreference.kt
│ │ │ ├── scene/
│ │ │ │ ├── Announcer.kt
│ │ │ │ ├── SceneApplication.kt
│ │ │ │ ├── SceneFragment.kt
│ │ │ │ ├── StageActivity.kt
│ │ │ │ ├── StageLayout.kt
│ │ │ │ └── TransitionHelper.kt
│ │ │ ├── text/
│ │ │ │ └── URLImageGetter.kt
│ │ │ ├── unifile/
│ │ │ │ ├── Contracts.kt
│ │ │ │ ├── DocumentsContractApi19.kt
│ │ │ │ ├── DocumentsContractApi21.kt
│ │ │ │ ├── FilenameFilter.kt
│ │ │ │ ├── MediaContract.kt
│ │ │ │ ├── MediaFile.kt
│ │ │ │ ├── RawFile.kt
│ │ │ │ ├── SingleDocumentFile.kt
│ │ │ │ ├── TreeDocumentFile.kt
│ │ │ │ ├── UniFile.kt
│ │ │ │ ├── UniFileExtensions.kt
│ │ │ │ ├── UriHandler.kt
│ │ │ │ └── Utils.kt
│ │ │ ├── util/
│ │ │ │ ├── AppHelper.kt
│ │ │ │ ├── BBCode.kt
│ │ │ │ ├── ClipboardUtil.kt
│ │ │ │ ├── CoroutinesExtensions.kt
│ │ │ │ ├── DateTimeUtil.kt
│ │ │ │ ├── ExceptionUtils.kt
│ │ │ │ ├── FDUtils.kt
│ │ │ │ ├── HtmlCompat.kt
│ │ │ │ ├── JsoupUtils.java
│ │ │ │ ├── LogCat.java
│ │ │ │ ├── ParcelableCompat.kt
│ │ │ │ ├── ReadableTime.kt
│ │ │ │ ├── SDKUtils.kt
│ │ │ │ ├── SqlUtils.java
│ │ │ │ ├── TextUrl.java
│ │ │ │ └── URLEncoderCompat.kt
│ │ │ ├── view/
│ │ │ │ ├── BringOutTransition.java
│ │ │ │ └── ViewTransition.java
│ │ │ ├── widget/
│ │ │ │ ├── AutoWrapLayout.java
│ │ │ │ ├── BatteryView.kt
│ │ │ │ ├── CheckTextView.kt
│ │ │ │ ├── ColorView.java
│ │ │ │ ├── ContentLayout.kt
│ │ │ │ ├── CuteSpinner.java
│ │ │ │ ├── DateUtils.java
│ │ │ │ ├── DrawerView.java
│ │ │ │ ├── FabLayout.kt
│ │ │ │ ├── FixedAspectImageView.kt
│ │ │ │ ├── IgnoreFitsSystemWindowsFullyDraggableDrawerContentLayout.kt
│ │ │ │ ├── IndicatingListView.java
│ │ │ │ ├── LinkifyTextView.java
│ │ │ │ ├── LoadImageView.kt
│ │ │ │ ├── MaxSizeContainer.java
│ │ │ │ ├── ObservedTextView.java
│ │ │ │ ├── RadioGridGroup.java
│ │ │ │ ├── SearchBarMover.java
│ │ │ │ ├── SimpleGridAutoSpanLayout.java
│ │ │ │ ├── SimpleGridLayout.java
│ │ │ │ ├── Slider.java
│ │ │ │ ├── TextClock.java
│ │ │ │ ├── lockpattern/
│ │ │ │ │ ├── LockPatternUtils.java
│ │ │ │ │ └── LockPatternView.java
│ │ │ │ └── recyclerview/
│ │ │ │ ├── AutoGridLayoutManager.java
│ │ │ │ └── AutoStaggeredGridLayoutManager.java
│ │ │ └── yorozuya/
│ │ │ ├── AnimationUtils.java
│ │ │ ├── AssertError.java
│ │ │ ├── AssertException.java
│ │ │ ├── AssertUtils.java
│ │ │ ├── ConcurrentPool.java
│ │ │ ├── FileUtils.java
│ │ │ ├── IOUtils.java
│ │ │ ├── IOUtils.kt
│ │ │ ├── IntIdGenerator.java
│ │ │ ├── LayoutUtils.java
│ │ │ ├── MathUtils.java
│ │ │ ├── NumberUtils.java
│ │ │ ├── OSUtils.java
│ │ │ ├── ObjectUtils.java
│ │ │ ├── Pool.java
│ │ │ ├── ResourcesUtils.java
│ │ │ ├── SimpleAnimatorListener.java
│ │ │ ├── SimpleHandler.java
│ │ │ ├── StringUtils.java
│ │ │ ├── StringUtils.kt
│ │ │ ├── Utilities.java
│ │ │ ├── ViewUtils.java
│ │ │ ├── collect/
│ │ │ │ ├── IntList.kt
│ │ │ │ └── LongList.kt
│ │ │ └── thread/
│ │ │ ├── InfiniteThreadExecutor.java
│ │ │ ├── PriorityThread.java
│ │ │ └── PriorityThreadFactory.java
│ │ └── eu/
│ │ └── kanade/
│ │ └── tachiyomi/
│ │ └── network/
│ │ └── interceptor/
│ │ ├── CloudflareInterceptor.kt
│ │ └── WebViewInterceptor.kt
│ └── res/
│ ├── anim/
│ │ ├── accelerate_quart.xml
│ │ ├── decelerate_quart.xml
│ │ ├── decelerate_quint.xml
│ │ ├── scene_close_exit.xml
│ │ ├── scene_open_enter.xml
│ │ ├── scene_open_enter_horizontal.xml
│ │ └── scene_open_exit.xml
│ ├── color/
│ │ ├── content_reactive.xml
│ │ ├── content_reactive_black.xml
│ │ └── primary_text_material_black.xml
│ ├── drawable/
│ │ ├── big_download.xml
│ │ ├── big_filter.xml
│ │ ├── big_history.xml
│ │ ├── big_sad_pandroid.xml
│ │ ├── category_background.xml
│ │ ├── check_text_view_foreground.xml
│ │ ├── default_avatar.xml
│ │ ├── divider_gallery_detail.xml
│ │ ├── divider_gallery_detail_dark.xml
│ │ ├── ic_baseline_dark_mode_24.xml
│ │ ├── ic_baseline_format_list_numbered_24.xml
│ │ ├── ic_baseline_menu_24.xml
│ │ ├── ic_baseline_reorder_24.xml
│ │ ├── ic_baseline_warning_24.xml
│ │ ├── ic_launcher_foreground.xml
│ │ ├── ic_launcher_monochrome.xml
│ │ ├── ic_pause_108dp.xml
│ │ ├── ic_play_arrow_108dp.xml
│ │ ├── image_failed.xml
│ │ ├── round_side_rect.xml
│ │ ├── spacer_keyline.xml
│ │ ├── spacer_x6.xml
│ │ ├── tile_background.xml
│ │ ├── v_adb_primary_x24.xml
│ │ ├── v_archive_primary_x48.xml
│ │ ├── v_arrow_left_dark_x24.xml
│ │ ├── v_book_open_primary_x24.xml
│ │ ├── v_book_open_x24.xml
│ │ ├── v_check_all_dark_x24.xml
│ │ ├── v_check_dark_x24.xml
│ │ ├── v_clear_all_dark_x24.xml
│ │ ├── v_close_dark_x24.xml
│ │ ├── v_cookie_brown_x48.xml
│ │ ├── v_delete_x24.xml
│ │ ├── v_dots_vertical_secondary_dark_x24.xml
│ │ ├── v_download_box_dark_x24.xml
│ │ ├── v_download_primary_x24.xml
│ │ ├── v_download_x16.xml
│ │ ├── v_download_x24.xml
│ │ ├── v_eh_subscription_black_x24.xml
│ │ ├── v_filter_dark_x24.xml
│ │ ├── v_fire_black_x24.xml
│ │ ├── v_folder_move_x24.xml
│ │ ├── v_go_to_dark_x24.xml
│ │ ├── v_heart_box_dark_x24.xml
│ │ ├── v_heart_broken_x24.xml
│ │ ├── v_heart_outline_primary_x48.xml
│ │ ├── v_heart_primary_x48.xml
│ │ ├── v_heart_x16.xml
│ │ ├── v_heart_x24.xml
│ │ ├── v_help_circle_x24.xml
│ │ ├── v_history_black_x24.xml
│ │ ├── v_homepage_black_x24.xml
│ │ ├── v_info_outline_dark_x24.xml
│ │ ├── v_info_primary_x24.xml
│ │ ├── v_last_page_x24.xml
│ │ ├── v_magnify_x24.xml
│ │ ├── v_pause_x24.xml
│ │ ├── v_pencil_dark_x24.xml
│ │ ├── v_pin_top_24.xml
│ │ ├── v_play_x24.xml
│ │ ├── v_plus_dark_x24.xml
│ │ ├── v_refresh_dark_x24.xml
│ │ ├── v_reply_dark_x24.xml
│ │ ├── v_sad_panda_primary_x24.xml
│ │ ├── v_sec_primary_x24.xml
│ │ ├── v_send_dark_x24.xml
│ │ ├── v_settings_black_x24.xml
│ │ ├── v_share_primary_x48.xml
│ │ ├── v_similar_primary_x48.xml
│ │ ├── v_slider_bubble.xml
│ │ ├── v_star_half_x16.xml
│ │ ├── v_star_outline_x16.xml
│ │ ├── v_star_x16.xml
│ │ └── v_utorrent_primary_x48.xml
│ ├── drawable-v25/
│ │ ├── ic_shortcut_start.xml
│ │ └── ic_shortcut_stop.xml
│ ├── drawable-v26/
│ │ ├── ic_shortcut_start.xml
│ │ └── ic_shortcut_stop.xml
│ ├── layout/
│ │ ├── activity_filter.xml
│ │ ├── activity_gallery.xml
│ │ ├── activity_main.xml
│ │ ├── activity_preference.xml
│ │ ├── activity_set_security.xml
│ │ ├── activity_webview.xml
│ │ ├── dialog_add_filter.xml
│ │ ├── dialog_archive_list.xml
│ │ ├── dialog_checkbox_builder.xml
│ │ ├── dialog_edittext_builder.xml
│ │ ├── dialog_edittextcheckbox_builder.xml
│ │ ├── dialog_gallery_menu.xml
│ │ ├── dialog_go_to.xml
│ │ ├── dialog_item_select_with_icon.xml
│ │ ├── dialog_js_prompt.xml
│ │ ├── dialog_list_checkbox_builder.xml
│ │ ├── dialog_rate.xml
│ │ ├── dialog_recycler_view.xml
│ │ ├── dialog_torrent_list.xml
│ │ ├── drawer_list_rv.xml
│ │ ├── gallery_detail_actions.xml
│ │ ├── gallery_detail_comments.xml
│ │ ├── gallery_detail_content.xml
│ │ ├── gallery_detail_header.xml
│ │ ├── gallery_detail_info.xml
│ │ ├── gallery_detail_previews.xml
│ │ ├── gallery_detail_tags.xml
│ │ ├── gallery_tag_group.xml
│ │ ├── item_cute_spinner_item.xml
│ │ ├── item_download.xml
│ │ ├── item_drawer_favorites.xml
│ │ ├── item_drawer_list.xml
│ │ ├── item_filter.xml
│ │ ├── item_filter_header.xml
│ │ ├── item_gallery_comment.xml
│ │ ├── item_gallery_comment_more.xml
│ │ ├── item_gallery_comment_progress.xml
│ │ ├── item_gallery_grid.xml
│ │ ├── item_gallery_info_data.xml
│ │ ├── item_gallery_info_header.xml
│ │ ├── item_gallery_list.xml
│ │ ├── item_gallery_list_thumb_height.xml
│ │ ├── item_gallery_preview.xml
│ │ ├── item_gallery_tag.xml
│ │ ├── item_history.xml
│ │ ├── item_hosts.xml
│ │ ├── item_select_dialog.xml
│ │ ├── item_simple_list_2.xml
│ │ ├── nav_header_main.xml
│ │ ├── preference_dialog_proxy.xml
│ │ ├── preference_dialog_task.xml
│ │ ├── preference_recyclerview.xml
│ │ ├── scene_cookie_sign_in.xml
│ │ ├── scene_download.xml
│ │ ├── scene_favorites.xml
│ │ ├── scene_gallery_comments.xml
│ │ ├── scene_gallery_detail.xml
│ │ ├── scene_gallery_info.xml
│ │ ├── scene_gallery_list.xml
│ │ ├── scene_gallery_previews.xml
│ │ ├── scene_history.xml
│ │ ├── scene_login.xml
│ │ ├── scene_progress.xml
│ │ ├── scene_security.xml
│ │ ├── scene_select_site.xml
│ │ ├── scene_toolbar.xml
│ │ ├── search_action.xml
│ │ ├── search_advance.xml
│ │ ├── search_category.xml
│ │ ├── search_image.xml
│ │ ├── search_normal.xml
│ │ ├── widget_advance_search_table.xml
│ │ ├── widget_category_table.xml
│ │ ├── widget_content_layout.xml
│ │ ├── widget_gallery_guide_1.xml
│ │ ├── widget_gallery_guide_2.xml
│ │ ├── widget_image_search.xml
│ │ └── widget_search_bar.xml
│ ├── layout-land/
│ │ └── activity_set_security.xml
│ ├── menu/
│ │ ├── activity_filter.xml
│ │ ├── activity_u_config.xml
│ │ ├── context_comment.xml
│ │ ├── download_label_option.xml
│ │ ├── drawer_download.xml
│ │ ├── drawer_favorites.xml
│ │ ├── drawer_gallery_list.xml
│ │ ├── nav_drawer_main.xml
│ │ ├── quicksearch_option.xml
│ │ ├── scene_download.xml
│ │ ├── scene_gallery_detail.xml
│ │ ├── scene_gallery_previews.xml
│ │ └── scene_history.xml
│ ├── mipmap-anydpi-v26/
│ │ ├── ic_launcher.xml
│ │ └── ic_launcher_round.xml
│ ├── raw/
│ │ └── isrgrootx1
│ ├── transition/
│ │ ├── trans_fade.xml
│ │ └── trans_move.xml
│ ├── values/
│ │ ├── arrays.xml
│ │ ├── attrs.xml
│ │ ├── bools.xml
│ │ ├── color_launcher.xml
│ │ ├── colors.xml
│ │ ├── dimens.xml
│ │ ├── drawables.xml
│ │ ├── ids.xml
│ │ ├── pathdata.xml
│ │ ├── strings.xml
│ │ ├── styles.xml
│ │ ├── themes.xml
│ │ └── themes_override.xml
│ ├── values-ja/
│ │ └── strings.xml
│ ├── values-night/
│ │ ├── color_launcher.xml
│ │ ├── colors.xml
│ │ └── styles.xml
│ ├── values-sw600dp/
│ │ └── dimens.xml
│ ├── values-sw720dp-land/
│ │ └── dimens.xml
│ ├── values-v24/
│ │ └── arrays.xml
│ ├── values-zh-rCN/
│ │ ├── bools.xml
│ │ └── strings.xml
│ ├── values-zh-rHK/
│ │ ├── bools.xml
│ │ └── strings.xml
│ ├── values-zh-rTW/
│ │ ├── bools.xml
│ │ └── strings.xml
│ ├── xml/
│ │ ├── about_settings.xml
│ │ ├── advanced_settings.xml
│ │ ├── backup_scheme.xml
│ │ ├── data_extraction_rules.xml
│ │ ├── download_settings.xml
│ │ ├── eh_settings.xml
│ │ ├── filepaths.xml
│ │ ├── locale_config.xml
│ │ ├── privacy_settings.xml
│ │ ├── read_settings.xml
│ │ └── settings_headers.xml
│ └── xml-v25/
│ └── shortcuts.xml
├── build.gradle.kts
├── docs/
│ ├── CHANGELOG/
│ │ └── zh-cn.md
│ └── README/
│ └── zh-cn.md
├── gradle/
│ ├── libs.versions.toml
│ └── wrapper/
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
└── settings.gradle.kts
================================================
FILE CONTENTS
================================================
================================================
FILE: .editorconfig
================================================
[*.{kt,kts}]
ij_kotlin_imports_layout = *
ij_kotlin_line_break_after_multiline_when_entry = false
ktlint_code_style = intellij_idea
ktlint_function_naming_ignore_when_annotated_with = Composable
ktlint_standard_class-signature = disabled
ktlint_standard_mixed-condition-operators = disabled
================================================
FILE: .github/ISSUE_TEMPLATE/bug-report.yml
================================================
name: Bug 反馈 / Bug report
description: 提交一个问题报告 / Create a bug report
labels:
- "bug"
body:
- type: markdown
attributes:
value: |
感谢您愿意为 Ehviewer-NekoInverter 做出贡献!
提交问题报告前,还请首先完成文末的自查步骤
Thanks for your contribution to the Ehviewer-NekoInverter Project!
Please complete the self-review steps at the end of the article before submitting the bug report
- type: textarea
id: reproduce
attributes:
label: 复现步骤 / Steps to reproduce
description: |
在此处写下复现的方式,请详细描述每一个步骤,包括画廊链接、相关设置等
Describe how to reproduce here, please describe each step in detail, include gallery links or settings
placeholder: |
1.
2.
3.
validations:
required: true
- type: textarea
id : expected
attributes:
label: 预期行为 / Expected behaviour
description: |
在此处说明正常情况下应用的预期行为
Describe what should be happened here
placeholder: |
它应该 ...
It should be ...
validations:
required: true
- type: textarea
id: actual
attributes:
label: 实际行为 / Actual behaviour
description: |
在此处描绘应用的实际行为,最好附上截图或录屏
Describe what actually happened here, screenshots or screen recordings are better
placeholder: |
实际上它 ...
Actually it ...
[截图或录屏] / [Screenshots or screen recordings]
validations:
required: true
- type: textarea
id: log
attributes:
label: 应用日志 / App logs
description: |
您可以通过设置-高级-导出日志 来获得日志文件,请确保日志完整,过长的日志请以文件形式上传
You can get logs file in Settings - Advanced - Dump logcat
placeholder: 06-15 17:44:53.704 23382 23382 E ...
validations:
required: true
- type: textarea
id: more
attributes:
label: 备注 / Additional details
description: |
在此处写下其他您想说的内容
Describe additional details here
placeholder: |
其他有用的信息与附件
Additional details and attachments
validations:
required: false
- type: input
id: site
attributes:
label: 浏览站点 / Browsing site
description: E-Hentai / ExHentai
placeholder: E-Hentai
validations:
required: true
- type: input
id: version
attributes:
label: EhViewer 版本号 / EhViewer version code
description: |
您可以在设置 - 关于处找到版本号
You can get version code in Settings - About
placeholder: 1.7.28
validations:
required: true
- type: input
id: ci
attributes:
label: EhViewer CI 版本 / EhViewer CI version
description: |
请确保您已经使用 [最新 CI 版本](https://github.com/EhViewer-NekoInverter/EhViewer/actions/workflows/ci.yml) 测试,请填入您使用的 CI 版本网址
Please make sure you have tested with the [latest CI version](https://github.com/EhViewer-NekoInverter/EhViewer/actions/workflows/ci.yml), simply drop GitHub Action CI download page url here
placeholder: https://github.com/EhViewer-NekoInverter/EhViewer/actions/runs/XXXXXXXXXX
validations:
required: true
- type: input
id: system
attributes:
label: Android 系统版本 / Android version
description: Android 分支名称 + 版本号 / AOSP fork name + version code
placeholder: MIUI 12.5, ArrowOS 12.1
validations:
required: true
- type: input
id: device
attributes:
label: 设备型号 / Device model
description: 在此填入设备型号 / Put device model here
placeholder: OnePlus 7 Pro, Xiaomi 12 Ultra
validations:
required: true
- type: input
id: SoC
attributes:
label: SoC 型号 / SoC model
description: 在此填入 SoC 型号 / Put SoC model here
placeholder: 骁龙 8+ Gen 1, Snapdragon 8+ Gen 1
validations:
required: true
- type: checkboxes
id: check
attributes:
label: 自查步骤 / Self-review steps
description: |
请确保您已经遵守以下所有必选项,否则 issue 会被立即关闭
Please ensure you have obtained all needed options, otherwise the issue will be closed immediately
options:
- label: 如果您有足够的时间和能力,并愿意为修复此问题提交 PR ,请勾上此复选框 / Pull request is welcome. Check this if you want to start a pull request
required: false
- label: 您已仔细查看并知情 [Q&A](https://github.com/EhViewer-NekoInverter/EhViewer/issues/18) 中的内容 / You have checked [Q&A](https://github.com/EhViewer-NekoInverter/EhViewer/issues/18) carefully
required: true
- label: 您已搜索过 [Issue Tracker](https://github.com/EhViewer-NekoInverter/EhViewer/issues),没有找到类似的问题 / I have searched on [Issue Tracker](https://github.com/EhViewer-NekoInverter/EhViewer/issues), No duplicate or related open issue has been found
required: true
- label: 您确保这个 Issue 只提及一个问题。如果您有多个问题报告,烦请发起多个 Issue / Ensure there is only one bug report in this issue. Please make mutiply issue for mutiply bugs
required: true
- label: 您确保已使用 [最新 CI 版本](https://github.com/EhViewer-NekoInverter/EhViewer/actions/workflows/ci.yml) 测试,并且该问题在最新 CI 版本中并未解决 / This bug have not solved in [latest CI version](https://github.com/EhViewer-NekoInverter/EhViewer/actions/workflows/ci.yml)
required: true
================================================
FILE: .github/ISSUE_TEMPLATE/config.yml
================================================
blank_issues_enabled: false
contact_links:
- name: 所有其他问题 / All other questions
url: https://www.google.com/
about: 咨询谷歌 / Ask Google for help
================================================
FILE: .github/workflows/ci.yml
================================================
name: CI
on:
push:
branches:
- '*'
pull_request:
workflow_dispatch:
jobs:
linux:
name: Build
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 21
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
- name: Spotless Check
run: ./gradlew spotlessCheck
- name: Gradle Build
run: ./gradlew assembleRelease
- name: Upload Universal
uses: actions/upload-artifact@v4
with:
name: universal-${{ github.sha }}
path: app/build/outputs/apk/release/app-universal-release.apk
- name: Upload ARM64
uses: actions/upload-artifact@v4
with:
name: arm64-v8a-${{ github.sha }}
path: app/build/outputs/apk/release/app-arm64-v8a-release.apk
- name: Upload ARM32
uses: actions/upload-artifact@v4
with:
name: armeabi-v7a-${{ github.sha }}
path: app/build/outputs/apk/release/app-armeabi-v7a-release.apk
- name: Upload x86_64
uses: actions/upload-artifact@v4
with:
name: x86_64-${{ github.sha }}
path: app/build/outputs/apk/release/app-x86_64-release.apk
- name: Upload mapping
uses: actions/upload-artifact@v4
with:
name: mapping-${{ github.sha }}
path: app/build/outputs/mapping/release/mapping.txt
- name: Upload native debug symbols
uses: actions/upload-artifact@v4
with:
name: native-debug-symbols-${{ github.sha }}
path: app/build/outputs/native-debug-symbols/release/native-debug-symbols.zip
================================================
FILE: .github/workflows/releases.yml
================================================
name: Releases
on:
push:
tags:
- "*"
jobs:
linux:
name: Build
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 21
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
- name: Gradle Build
run: ./gradlew assembleRelease
- name: Rename Apks
run: |
mv app/build/outputs/apk/release/app-universal-release.apk EhViewer-${{ github.ref_name }}-universal.apk
mv app/build/outputs/apk/release/app-arm64-v8a-release.apk EhViewer-${{ github.ref_name }}-arm64-v8a.apk
mv app/build/outputs/apk/release/app-armeabi-v7a-release.apk EhViewer-${{ github.ref_name }}-armeabi-v7a.apk
mv app/build/outputs/apk/release/app-x86_64-release.apk EhViewer-${{ github.ref_name }}-x86_64.apk
mv app/build/outputs/mapping/release/mapping.txt EhViewer-${{ github.ref_name }}-mapping.txt
mv app/build/outputs/native-debug-symbols/release/native-debug-symbols.zip EhViewer-${{ github.ref_name }}-native-debug-symbols.zip
- name: Releases
uses: softprops/action-gh-release@v2
with:
body: Bump Version
files: |
EhViewer-${{ github.ref_name }}-universal.apk
EhViewer-${{ github.ref_name }}-arm64-v8a.apk
EhViewer-${{ github.ref_name }}-armeabi-v7a.apk
EhViewer-${{ github.ref_name }}-x86_64.apk
EhViewer-${{ github.ref_name }}-mapping.txt
EhViewer-${{ github.ref_name }}-native-debug-symbols.zip
================================================
FILE: .gitignore
================================================
.gradle
/local.properties
.DS_Store
/build
/src
*.iml
.idea
/captures
release
google-services.json
/.kotlin
================================================
FILE: LICENSE
================================================
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc.
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
Copyright (C)
This program 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.
This program 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 this program. If not, see .
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
Copyright (C)
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
.
================================================
FILE: NOTICE
================================================
EhViewer
Copyright 2014-2016 Hippo Seven
An Unofficial E-Hentai Application for Android.
香风智乃是最可爱的女孩子。
chino kafuu is the cutest girl.
================================================
FILE: README.md
================================================
English
|
简体中文
EhViewer
# Description
An EhViewer fork with classic Material Design 2 style
This fork is for personal use and does not accept feature requests; for common usage issues, please refer to the [Q&A](https://github.com/EhViewer-NekoInverter/EhViewer/issues/18)
If you prefer Material Design 3, consider using [EhViewer-Overhauled](https://github.com/FooIbar/EhViewer)
# Download
| Android Version | Notes |
|-----------------|------------------------------|
| 6.0-8.1 | No support for animated WebP |
| 9.0+ | Full support |
- Please go to **[GitHub Releases](https://github.com/EhViewer-NekoInverter/EhViewer/releases)** to download the release version
- If the release version has unresolved issues, go to **[GitHub Actions](https://github.com/EhViewer-NekoInverter/EhViewer/actions/workflows/ci.yml?query=branch%3Amaster)** to download the CI version (GitHub account login required)
# Screenshot


# Thanks
Here are the libraries
- [AOSP & AndroidX](https://source.android.com/)
- [Kotlin & KotlinX](https://kotlinlang.org/)
- [Coil](https://coil-kt.github.io/coil/)
- [FullDraggableDrawer](https://github.com/PureWriter/FullDraggableDrawer)
- [Jsoup](https://jsoup.org/)
- [Ktor](https://ktor.io/)
- [Libarchive](http://www.libarchive.org/)
- [MDC-Android](https://github.com/material-components/material-components-android)
- [OkHttp](https://square.github.io/okhttp/)
- [RikkaX](https://github.com/RikkaApps/RikkaX)
Tag translation
- [EhTagTranslation](https://github.com/EhTagTranslation/Database)
Translators
- ja: [Re*Index. (ot_inc)](https://github.com/reindex-ot)
# License
Copyright 2014-2019 Hippo Seven
Copyright 2020-2022 NekoInverter
Copyright 2022-2025 Moedog
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 .
================================================
FILE: app/.gitignore
================================================
/build
/src/main/java-gen
manifest-merger-release-report.txt
/src/main/libs
/src/main/obj
# Intellij
*.iml
/.cxx
================================================
FILE: app/build.gradle.kts
================================================
import java.time.Instant
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
val isRelease: Boolean
get() = gradle.startParameter.taskNames.any { it.contains("Release") }
val supportedAbis = arrayOf("arm64-v8a", "x86_64", "armeabi-v7a")
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.parcelize)
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.ksp)
alias(libs.plugins.spotless)
}
@Suppress("UnstableApiUsage")
android {
androidResources {
localeFilters += listOf(
"zh",
"zh-rCN",
"zh-rHK",
"zh-rTW",
"ja",
)
}
splits {
abi {
isEnable = true
reset()
if (isRelease) {
include(*supportedAbis)
isUniversalApk = true
} else {
include("x86_64", "x86")
}
}
}
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 = providers.exec {
commandLine = "git rev-parse --short=7 HEAD".split(' ')
}.standardOutput
stdout.asText.get().trim()
}
val buildTime by lazy {
val formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm").withZone(ZoneOffset.UTC)
formatter.format(Instant.now())
}
defaultConfig {
applicationId = "org.moedog.ehviewer"
versionCode = 180014
versionName = "1.8.13"
buildConfigField("String", "VERSION_CODE", "\"${defaultConfig.versionCode}\"")
buildConfigField("String", "COMMIT_SHA", "\"$commitSha\"")
ndk {
if (isRelease) {
abiFilters.addAll(supportedAbis)
}
debugSymbolLevel = "FULL"
}
}
externalNativeBuild {
cmake {
path = File("src/main/cpp/CMakeLists.txt")
}
}
compileOptions {
isCoreLibraryDesugaringEnabled = true
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}
lint {
abortOnError = true
checkReleaseBuilds = false
disable.add("MissingTranslation")
}
packaging {
resources {
excludes += "/META-INF/**"
excludes += "/kotlin/**"
excludes += "**.txt"
excludes += "**.bin"
}
}
dependenciesInfo.includeInApk = false
buildTypes {
release {
isMinifyEnabled = true
isShrinkResources = true
proguardFiles("proguard-rules.pro")
signingConfig = signConfig
buildConfigField("String", "BUILD_TIME", "\"$buildTime\"")
}
debug {
applicationIdSuffix = ".debug"
buildConfigField("String", "BUILD_TIME", "\"\"")
}
}
buildFeatures {
buildConfig = true
}
namespace = "com.hippo.ehviewer"
}
dependencies {
// https://developer.android.com/jetpack/androidx/releases/activity
implementation(libs.androidx.activity)
implementation(libs.androidx.appcompat)
implementation(libs.androidx.biometric)
implementation(libs.androidx.browser)
implementation(libs.androidx.collection)
implementation(libs.androidx.webkit)
implementation(libs.androidx.core)
implementation(libs.androidx.coordinatorlayout)
implementation(libs.androidx.fragment)
// https://developer.android.com/jetpack/androidx/releases/lifecycle
implementation(libs.androidx.lifecycle.process)
// https://developer.android.com/jetpack/androidx/releases/paging
implementation(libs.androidx.paging.runtime)
implementation(libs.androidx.preference)
implementation(libs.androidx.recyclerview)
// https://developer.android.com/jetpack/androidx/releases/room
ksp(libs.androidx.room.compiler)
implementation(libs.androidx.room.paging)
implementation(libs.androidx.swiperefreshlayout)
implementation(libs.drawer)
implementation(libs.material)
// https://square.github.io/okhttp/changelogs/changelog/
implementation(platform(libs.okhttp.bom))
implementation(libs.okhttp.coroutines)
implementation(libs.okhttp.tls)
implementation(libs.okio.jvm)
// https://github.com/RikkaApps/RikkaX
implementation(libs.bundles.rikkax)
// https://coil-kt.github.io/coil/changelog/
implementation(platform(libs.coil.bom))
implementation(libs.bundles.coil)
implementation(libs.kotlinx.coroutines.android)
implementation(libs.kotlinx.datetime)
implementation(libs.kotlinx.serialization.cbor)
implementation(libs.ktor.utils)
implementation(libs.jsoup)
coreLibraryDesugaring(libs.desugar)
}
kotlin {
compilerOptions {
jvmTarget = JvmTarget.JVM_21
progressiveMode = true
optIn.addAll(
"coil3.annotation.ExperimentalCoilApi",
"kotlin.contracts.ExperimentalContracts",
"kotlin.time.ExperimentalTime",
"kotlinx.coroutines.ExperimentalCoroutinesApi",
"kotlinx.coroutines.FlowPreview",
"kotlinx.coroutines.InternalCoroutinesApi",
"kotlinx.serialization.ExperimentalSerializationApi",
)
}
}
configurations.all {
exclude("dev.rikka.rikkax.appcompat", "appcompat")
}
ksp {
arg("room.schemaLocation", "$projectDir/schemas")
arg("room.generateKotlin", "true")
}
val ktlintVersion = libs.ktlint.get().version
spotless {
kotlin {
// https://github.com/diffplug/spotless/issues/111
target("src/**/*.kt")
ktlint(ktlintVersion)
}
kotlinGradle {
ktlint(ktlintVersion)
}
}
================================================
FILE: app/proguard-rules.pro
================================================
-keepclassmembers class * implements android.os.Parcelable {
public static final ** CREATOR;
}
-keepclasseswithmembernames,includedescriptorclasses class * {
native ;
}
-keepattributes LineNumberTable,SourceFile
-renamesourcefileattribute SourceFile
-repackageclasses
-allowaccessmodification
-overloadaggressively
================================================
FILE: app/schemas/com.hippo.network.CookiesDatabase/1.json
================================================
{
"formatVersion": 1,
"database": {
"version": 1,
"identityHash": "c801ee62729ba20a203b150211234d5c",
"entities": [
{
"tableName": "OK_HTTP_3_COOKIE",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`NAME` TEXT, `VALUE` TEXT, `EXPIRES_AT` INTEGER NOT NULL, `DOMAIN` TEXT, `PATH` TEXT, `SECURE` INTEGER NOT NULL, `HTTP_ONLY` INTEGER NOT NULL, `PERSISTENT` INTEGER NOT NULL, `HOST_ONLY` INTEGER NOT NULL, `_id` INTEGER NOT NULL, PRIMARY KEY(`_id`))",
"fields": [
{
"fieldPath": "name",
"columnName": "NAME",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "value",
"columnName": "VALUE",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "expiresAt",
"columnName": "EXPIRES_AT",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "domain",
"columnName": "DOMAIN",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "path",
"columnName": "PATH",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "secure",
"columnName": "SECURE",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "httpOnly",
"columnName": "HTTP_ONLY",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "persistent",
"columnName": "PERSISTENT",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "hostOnly",
"columnName": "HOST_ONLY",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "id",
"columnName": "_id",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"_id"
]
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'c801ee62729ba20a203b150211234d5c')"
]
}
}
================================================
FILE: app/schemas/com.hippo.network.CookiesDatabase/2.json
================================================
{
"formatVersion": 1,
"database": {
"version": 2,
"identityHash": "1b80cd29939b0f43934721f1289ba94d",
"entities": [
{
"tableName": "OK_HTTP_3_COOKIE",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`NAME` TEXT NOT NULL, `VALUE` TEXT NOT NULL, `EXPIRES_AT` INTEGER NOT NULL, `DOMAIN` TEXT NOT NULL, `PATH` TEXT NOT NULL, `SECURE` INTEGER NOT NULL, `HTTP_ONLY` INTEGER NOT NULL, `PERSISTENT` INTEGER NOT NULL, `HOST_ONLY` INTEGER NOT NULL, `_id` INTEGER, PRIMARY KEY(`_id`))",
"fields": [
{
"fieldPath": "name",
"columnName": "NAME",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "value",
"columnName": "VALUE",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "expiresAt",
"columnName": "EXPIRES_AT",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "domain",
"columnName": "DOMAIN",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "path",
"columnName": "PATH",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "secure",
"columnName": "SECURE",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "httpOnly",
"columnName": "HTTP_ONLY",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "persistent",
"columnName": "PERSISTENT",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "hostOnly",
"columnName": "HOST_ONLY",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "id",
"columnName": "_id",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"_id"
]
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '1b80cd29939b0f43934721f1289ba94d')"
]
}
}
================================================
FILE: app/src/debug/res/values/strings.xml
================================================
EhViewer Debug
================================================
FILE: app/src/main/AndroidManifest.xml
================================================
================================================
FILE: app/src/main/cpp/0001-Insert-link-libs.patch
================================================
From aee8f7147ddb262fd9e5feee8d5b17094cf3470f Mon Sep 17 00:00:00 2001
From: FooIbar <118464521+FooIbar@users.noreply.github.com>
Date: Tue, 26 Sep 2023 00:09:25 +0800
Subject: [PATCH] Insert link libs
---
CMakeLists.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/CMakeLists.txt b/CMakeLists.txt
index ec97e4c7..420f204c 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -438,7 +438,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.34.1
================================================
FILE: app/src/main/cpp/0002-Fix-zip_time-performance.patch
================================================
From e971b9f23727833cc39b9e325db30f383e5bfc30 Mon Sep 17 00:00:00 2001
From: Dude so hot
Date: Thu, 14 Nov 2024 00:28:13 +0800
Subject: [PATCH] Fix zip_time performance
Signed-off-by: Dude so hot
---
libarchive/archive_time.c | 1 +
1 file changed, 1 insertion(+)
diff --git a/libarchive/archive_time.c b/libarchive/archive_time.c
index 3352c809..9289bf1e 100644
--- a/libarchive/archive_time.c
+++ b/libarchive/archive_time.c
@@ -53,6 +53,7 @@ FILETIME_to_ntfs(const FILETIME* filetime)
int64_t
dos_to_unix(uint32_t dos_time)
{
+ return 0;
uint16_t msTime, msDate;
struct tm ts;
time_t t;
--
2.47.0
================================================
FILE: app/src/main/cpp/0003-Use-UTF-8-as-default-charset-on-bionic.patch
================================================
From 86d199429b3fabc83f7dcc9afd72e7b4c7750b1a Mon Sep 17 00:00:00 2001
From: FooIbar <118464521+FooIbar@users.noreply.github.com>
Date: Thu, 14 Nov 2024 20:22:52 +0800
Subject: [PATCH] Use UTF-8 as default charset on bionic
---
libarchive/archive_string.c | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/libarchive/archive_string.c b/libarchive/archive_string.c
index abf7ad66..63a73c58 100644
--- a/libarchive/archive_string.c
+++ b/libarchive/archive_string.c
@@ -423,7 +423,9 @@ static const char *
default_iconv_charset(const char *charset) {
if (charset != NULL && charset[0] != '\0')
return charset;
-#if HAVE_LOCALE_CHARSET && !defined(__APPLE__)
+#ifdef __BIONIC__
+ return "UTF-8";
+#elif HAVE_LOCALE_CHARSET && !defined(__APPLE__)
/* locale_charset() is broken on Mac OS */
return locale_charset();
#elif HAVE_NL_LANGINFO
--
2.43.0
================================================
FILE: app/src/main/cpp/CMakeLists.txt
================================================
cmake_minimum_required(VERSION 3.14)
project(ehviewer C)
include(FetchContent)
if (NOT CMAKE_BUILD_TYPE STREQUAL "Debug")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Ofast -fvisibility=hidden -fvisibility-inlines-hidden -funroll-loops -flto \
-mllvm -polly \
-mllvm -polly-run-dce \
-mllvm -polly-run-inliner \
-mllvm -polly-isl-arg=--no-schedule-serialize-sccs \
-mllvm -polly-ast-use-context \
-mllvm -polly-detect-keep-going \
-mllvm -polly-position=before-vectorizer \
-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")
option(BUILD_TESTING OFF)
option(XZ_DOC OFF)
option(XZ_LZIP_DECODER OFF)
option(XZ_MICROLZMA_DECODER OFF)
option(XZ_MICROLZMA_ENCODER OFF)
option(XZ_TOOL_LZMADEC OFF)
option(XZ_TOOL_LZMAINFO OFF)
option(XZ_TOOL_XZ OFF)
option(XZ_TOOL_XZDEC OFF)
FetchContent_Declare(
liblzma
GIT_REPOSITORY https://github.com/tukaani-project/xz.git
GIT_TAG v5.8.1
GIT_SHALLOW 1
)
FetchContent_MakeAvailable(liblzma)
include_directories(${liblzma_SOURCE_DIR}/src/liblzma/api)
# Build GNUTLS libnettle
FetchContent_Declare(
nettle
URL https://ftp.gnu.org/gnu/nettle/nettle-3.10.2.tar.gz
URL_MD5 b28bcbf6f045ff007940a9401673600d
SOURCE_DIR ${CMAKE_CURRENT_LIST_DIR}/nettle/nettle
)
FetchContent_MakeAvailable(nettle)
add_subdirectory(nettle)
# Configure libnettle support for libarchive
include_directories(nettle)
include_directories(.)
set(HAVE_LIBNETTLE 1)
set(HAVE_NETTLE_AES_H 1)
set(HAVE_NETTLE_HMAC_H 1)
set(HAVE_NETTLE_MD5_H 1)
set(HAVE_NETTLE_PBKDF2_H 1)
set(HAVE_NETTLE_RIPEMD160_H 1)
set(HAVE_NETTLE_SHA_H 1)
# Configure lzma support for libarchive
SET(HAVE_LIBLZMA 1)
SET(HAVE_LZMA_H 1)
SET(HAVE_LZMA_STREAM_ENCODER_MT 1)
SET(HAVE_LZMADEC_H 1)
SET(HAVE_LIBLZMADEC 1)
option(ENABLE_OPENSSL OFF)
option(ENABLE_TAR OFF)
option(ENABLE_CPIO OFF)
option(ENABLE_CAT OFF)
option(ENABLE_UNZIP OFF)
option(ENABLE_TEST OFF)
# Configure libarchive link's static lib
SET(LIBARCHIVE_CUSTOM_LIBS "nettle" "liblzma")
set(LIBARCHIVE_PATCH
${CMAKE_CURRENT_LIST_DIR}/0001-Insert-link-libs.patch
${CMAKE_CURRENT_LIST_DIR}/0002-Fix-zip_time-performance.patch
${CMAKE_CURRENT_LIST_DIR}/0003-Use-UTF-8-as-default-charset-on-bionic.patch
)
FetchContent_Declare(
libarchive
GIT_REPOSITORY https://github.com/libarchive/libarchive.git
GIT_TAG v3.8.1
GIT_SHALLOW 1
PATCH_COMMAND git apply --check -R ${LIBARCHIVE_PATCH} || git apply ${LIBARCHIVE_PATCH}
)
FetchContent_MakeAvailable(libarchive)
include_directories(${libarchive_SOURCE_DIR}/libarchive)
# Build and link our app's native lib
add_library(${PROJECT_NAME} SHARED archive.c image.c gifutils.c hash.c natsort/strnatcmp.c)
target_link_libraries(${PROJECT_NAME} archive_static log jnigraphics GLESv3 -Wl,--exclude-libs,ALL)
================================================
FILE: app/src/main/cpp/archive.c
================================================
/*
* Copyright 2022-2024 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 .
*/
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define LOG_TAG "libarchive_wrapper"
#include "natsort/strnatcmp.h"
#include "ehviewer.h"
typedef struct {
int using;
int next_index;
struct archive *arc;
struct archive_entry *entry;
} archive_ctx;
typedef struct {
const char *filename;
int index;
ssize_t size;
void *addr;
} entry;
#define CTX_POOL_SIZE 20
#define MAX_PARALLEL_DECOMP 4
#define max(a, b) ((a) > (b) ? (a) : (b))
static pthread_mutex_t ctx_pool_mutex = PTHREAD_MUTEX_INITIALIZER;
static archive_ctx **ctx_pool = NULL;
static pthread_mutex_t buffer_mutex = PTHREAD_MUTEX_INITIALIZER;
static void *decode_buffer[MAX_PARALLEL_DECOMP];
static bool need_encrypt = false;
static char *passwd = NULL;
static void *archiveAddr = MAP_FAILED;
static size_t archiveSize = 0;
static entry *entries = NULL;
static size_t entryCount = 0;
static ssize_t max_file_size = 0;
#define SUPPORT_EXT_COUNT 11
const char supportExt[SUPPORT_EXT_COUNT][5] = {
"jpeg",
"jpg",
"png",
"gif",
"webp",
"bmp",
"ico",
"wbmp",
"heic",
"heif",
"avif"
};
static inline int filename_is_playable_file(const char *name) {
if (!name)
return false;
const char *dotptr = strrchr(name, '.');
if (!dotptr++)
return false;
int i;
for (i = 0; i < SUPPORT_EXT_COUNT; i++)
if (strcmp(dotptr, supportExt[i]) == 0)
return true;
return false;
}
static inline bool archive_entry_is_file(struct archive_entry *entry) {
return archive_entry_filetype(entry) == AE_IFREG;
}
static inline bool archive_entry_is_playable(struct archive_entry *entry) {
return archive_entry_is_file(entry) &&
filename_is_playable_file(archive_entry_pathname(entry));
}
static inline int compare_entries(const void *a, const void *b) {
const char *fa = ((entry *) a)->filename;
const char *fb = ((entry *) b)->filename;
return strnatcmp(fa, fb);
}
#define ADDR_IN_FILE_MAPPING(addr) (addr >= archiveAddr && addr < archiveAddr + archiveSize)
static bool fill_entry_zero_copy(struct archive *arc, entry *entry) {
void *buffer = NULL;
size_t buffer_size = 0;
la_int64_t output_ofs = 0;
archive_read_data_block(arc, (const void **) &buffer, &buffer_size, &output_ofs);
bool zero_copy = ADDR_IN_FILE_MAPPING(buffer) && !output_ofs && buffer_size == entry->size;
entry->addr = zero_copy ? buffer : NULL;
return zero_copy;
}
static void archive_map_entries_index(archive_ctx *ctx, bool sort) {
int count = 0;
bool zero_copy = true;
while (archive_read_next_header(ctx->arc, &ctx->entry) == ARCHIVE_OK) {
const char *name = archive_entry_pathname(ctx->entry);
if (archive_entry_is_file(ctx->entry) && filename_is_playable_file(name)) {
entries[count].filename = strdup(name);
entries[count].index = count;
ssize_t size = archive_entry_size(ctx->entry);
max_file_size = max(size, max_file_size);
entries[count].size = size;
// We don't expect zero copy if first content can't do zero copy
if (zero_copy) zero_copy = fill_entry_zero_copy(ctx->arc, &entries[count]);
count++;
}
}
if (sort) qsort(entries, entryCount, sizeof(entry), compare_entries);
}
static void *acquire_decode_buffer() {
void *addr = NULL;
pthread_mutex_lock(&buffer_mutex);
for (int i = 0; i < MAX_PARALLEL_DECOMP; ++i) {
addr = decode_buffer[i];
if (addr) {
decode_buffer[i] = NULL;
break;
}
}
pthread_mutex_unlock(&buffer_mutex);
if (!addr) addr = malloc(max_file_size);
return addr;
}
static void release_decode_buffer(void *buffer) {
pthread_mutex_lock(&buffer_mutex);
for (int i = 0; i < MAX_PARALLEL_DECOMP; ++i) {
void *addr = decode_buffer[i];
if (!addr) {
decode_buffer[i] = buffer;
pthread_mutex_unlock(&buffer_mutex);
return;
}
}
pthread_mutex_unlock(&buffer_mutex);
free(buffer);
}
static int archive_list_all_entries(archive_ctx *ctx) {
int count = 0;
while (archive_read_next_header(ctx->arc, &ctx->entry) == ARCHIVE_OK)
if (archive_entry_is_playable(ctx->entry))
count++;
return count;
}
static void archive_release_ctx(archive_ctx *ctx) {
if (ctx) {
archive_read_close(ctx->arc);
archive_read_free(ctx->arc);
free(ctx);
}
}
static archive_ctx *archive_alloc_ctx() {
archive_ctx *ctx = calloc(1, sizeof(archive_ctx));
ctx->arc = archive_read_new();
ctx->using = 1;
archive_read_support_format_tar(ctx->arc);
archive_read_support_format_7zip(ctx->arc);
archive_read_support_format_rar5(ctx->arc);
archive_read_support_format_zip(ctx->arc);
archive_read_support_filter_gzip(ctx->arc);
archive_read_support_filter_xz(ctx->arc);
archive_read_set_option(ctx->arc, "zip", "ignorecrc32", "1");
if (passwd)
archive_read_add_passphrase(ctx->arc, passwd);
int err = archive_read_open_memory(ctx->arc, archiveAddr, archiveSize);
if (err < ARCHIVE_OK) {
LOGE("%s%s", "Open archive failed: ", archive_error_string(ctx->arc));
archive_read_free(ctx->arc);
free(ctx);
return NULL;
}
return ctx;
}
static int archive_skip_to_index(archive_ctx *ctx, int index) {
while (archive_read_next_header(ctx->arc, &ctx->entry) == ARCHIVE_OK) {
if (!archive_entry_is_playable(ctx->entry))
continue;
if (ctx->next_index++ == index) {
return ctx->next_index - 1;
}
}
return ARCHIVE_FATAL;
}
static int archive_get_ctx(archive_ctx **ctxptr, int idx) {
int ret;
archive_ctx *ctx = NULL;
pthread_mutex_lock(&ctx_pool_mutex);
for (int i = 0; i < CTX_POOL_SIZE; i++) {
if (!ctx_pool[i])
continue;
if (ctx_pool[i]->using)
continue;
if (ctx_pool[i]->next_index > idx)
continue;
if (!ctx || ctx_pool[i]->next_index > ctx->next_index)
ctx = ctx_pool[i];
if (ctx->next_index == idx)
break;
}
if (ctx)
ctx->using = 1;
pthread_mutex_unlock(&ctx_pool_mutex);
if (!ctx) {
archive_ctx *victimCtx = NULL;
int victimIdx = 0;
int replace = 1;
ctx = archive_alloc_ctx();
pthread_mutex_lock(&ctx_pool_mutex);
for (int i = 0; i < CTX_POOL_SIZE; i++) {
if (!ctx_pool[i]) {
ctx_pool[i] = ctx;
replace = 0;
break;
}
if (ctx_pool[i]->using)
continue;
if (!victimCtx || ctx_pool[i]->next_index > victimCtx->next_index) {
victimCtx = ctx_pool[i];
victimIdx = i;
}
}
if (replace) ctx_pool[victimIdx] = ctx;
pthread_mutex_unlock(&ctx_pool_mutex);
if (replace) archive_release_ctx(victimCtx);
}
ret = archive_skip_to_index(ctx, idx);
if (ret != idx) {
ret = archive_errno(ctx->arc);
LOGE("Skip to index failed: %s", archive_error_string(ctx->arc));
archive_release_ctx(ctx);
return ret;
}
*ctxptr = ctx;
return 0;
}
JNIEXPORT jint JNICALL
Java_com_hippo_ehviewer_jni_ArchiveKt_openArchive(JNIEnv *env, jclass thiz, jint fd, jlong size, jboolean sort_entries) {
EH_UNUSED(env);
EH_UNUSED(thiz);
archive_ctx *ctx = NULL;
archiveAddr = mmap(0, size, PROT_READ, MAP_PRIVATE, fd, 0);
if (archiveAddr == MAP_FAILED) {
LOGE("%s%s", "mmap failed with error ", strerror(errno));
return 0;
}
archiveSize = size;
ctx_pool = calloc(CTX_POOL_SIZE, sizeof(archive_ctx **));
ctx = archive_alloc_ctx();
if (!ctx) return 0;
entryCount = archive_list_all_entries(ctx);
LOGI("%s%zu%s", "Found ", entryCount, " images in archive");
if (!entryCount) {
LOGE("%s%s", "Archive read failed: ", archive_error_string(ctx->arc));
archive_release_ctx(ctx);
return 0;
}
// We must read through the file|vm then we can know whether it is encrypted
int encryptRet = archive_read_has_encrypted_entries(ctx->arc);
switch (encryptRet) {
case 1: // At lease 1 encrypted entry
need_encrypt = true;
break;
case 0: // format supports but no encrypted entry found
default:
need_encrypt = false;
}
int format = archive_format(ctx->arc);
switch (format) {
case ARCHIVE_FORMAT_ZIP:
case ARCHIVE_FORMAT_RAR_V5:
madvise_log_if_error(archiveAddr, archiveSize, MADV_SEQUENTIAL);
break;
case ARCHIVE_FORMAT_7ZIP: // Seek is bad
madvise_log_if_error(archiveAddr, archiveSize, MADV_RANDOM);
break;
default:;
}
archive_release_ctx(ctx);
ctx = archive_alloc_ctx();
if (!ctx) return 0;
entries = calloc(entryCount, sizeof(entry));
archive_map_entries_index(ctx, sort_entries);
archive_release_ctx(ctx);
return (int) entryCount;
}
JNIEXPORT jobject JNICALL
Java_com_hippo_ehviewer_jni_ArchiveKt_extractToByteBuffer(JNIEnv *env, jclass thiz, jint index) {
EH_UNUSED(env);
EH_UNUSED(thiz);
entry *entry = &entries[index];
ssize_t size = entry->size;
if (entry->addr) {
return (*env)->NewDirectByteBuffer(env, entry->addr, size);
} else {
archive_ctx *ctx = NULL;
if (!archive_get_ctx(&ctx, entry->index)) {
void *addr = acquire_decode_buffer();
ssize_t bytes = archive_read_data(ctx->arc, addr, size);
ctx->using = 0;
if (bytes == size) {
return (*env)->NewDirectByteBuffer(env, addr, size);
} else {
if (bytes < 0) {
LOGE("%s%s", "Archive read failed: ", archive_error_string(ctx->arc));
} else {
LOGE("%s", "No enough data read, WTF?");
}
}
release_decode_buffer(addr);
}
}
return 0;
}
JNIEXPORT void JNICALL
Java_com_hippo_ehviewer_jni_ArchiveKt_closeArchive(JNIEnv *env, jclass thiz) {
EH_UNUSED(env);
EH_UNUSED(thiz);
if (ctx_pool) {
for (int i = 0; i < CTX_POOL_SIZE; i++)
archive_release_ctx(ctx_pool[i]);
free(ctx_pool);
ctx_pool = NULL;
}
free(passwd);
passwd = NULL;
need_encrypt = false;
if (archiveAddr != MAP_FAILED) {
munmap(archiveAddr, archiveSize);
archiveAddr = MAP_FAILED;
}
for (int i = 0; i < MAX_PARALLEL_DECOMP; ++i) {
free(decode_buffer[i]);
decode_buffer[i] = NULL;
}
max_file_size = 0;
if (entries) {
for (int i = 0; i < entryCount; ++i) {
free((void *) entries[i].filename);
}
free(entries);
entries = NULL;
}
}
JNIEXPORT jboolean JNICALL
Java_com_hippo_ehviewer_jni_ArchiveKt_needPassword(JNIEnv *env, jclass thiz) {
EH_UNUSED(env);
EH_UNUSED(thiz);
return need_encrypt;
}
JNIEXPORT jboolean JNICALL
Java_com_hippo_ehviewer_jni_ArchiveKt_providePassword(JNIEnv *env, jclass thiz, jstring str) {
EH_UNUSED(thiz);
struct archive_entry *entry;
archive_ctx *ctx;
jboolean ret = true;
int len = (*env)->GetStringUTFLength(env, str);
passwd = realloc(passwd, len + 1);
(*env)->GetStringUTFRegion(env, str, 0, len, passwd);
passwd[len] = 0;
ctx = archive_alloc_ctx();
char tmpBuf[4096];
while (archive_read_next_header(ctx->arc, &entry) == ARCHIVE_OK) {
if (!archive_entry_is_playable(entry))
continue;
if (!archive_entry_is_encrypted(entry))
continue;
if (archive_read_data(ctx->arc, tmpBuf, 4096) < ARCHIVE_OK) {
LOGE("%s%s", "Archive read failed: ", archive_error_string(ctx->arc));
ret = false;
}
break;
}
archive_release_ctx(ctx);
return ret;
}
JNIEXPORT jstring JNICALL
Java_com_hippo_ehviewer_jni_ArchiveKt_getFilename(JNIEnv *env, jclass thiz, jint index) {
EH_UNUSED(env);
EH_UNUSED(thiz);
index = entries[index].index;
archive_ctx *ctx = NULL;
int ret;
ret = archive_get_ctx(&ctx, index);
if (ret)
return NULL;
jstring str = (*env)->NewStringUTF(env, archive_entry_pathname(ctx->entry));
ctx->using = 0;
return str;
}
JNIEXPORT jboolean JNICALL
Java_com_hippo_ehviewer_jni_ArchiveKt_extractToFd(JNIEnv *env, jclass thiz, jint index, jint fd) {
EH_UNUSED(env);
EH_UNUSED(thiz);
index = entries[index].index;
archive_ctx *ctx = NULL;
int ret;
ret = archive_get_ctx(&ctx, index);
if (!ret) {
ret = archive_read_data_into_fd(ctx->arc, fd);
ctx->using = 0;
}
return ret == ARCHIVE_OK;
}
JNIEXPORT void JNICALL
Java_com_hippo_ehviewer_jni_ArchiveKt_releaseByteBuffer(JNIEnv *env, jclass thiz, jobject buffer) {
EH_UNUSED(thiz);
void *addr = (*env)->GetDirectBufferAddress(env, buffer);
if (!ADDR_IN_FILE_MAPPING(addr)) {
release_decode_buffer(addr);
}
}
================================================
FILE: app/src/main/cpp/ehviewer.h
================================================
/*
* 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 .
*/
#ifndef EHVIEWER_EHVIEWER_H
#define EHVIEWER_EHVIEWER_H
#define EH_UNUSED(x) (void)x
#define LOGV(...) __android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG ,__VA_ARGS__)
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG ,__VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG ,__VA_ARGS__)
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG ,__VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG ,__VA_ARGS__)
#define LOGF(...) __android_log_print(ANDROID_LOG_FATAL, LOG_TAG ,__VA_ARGS__)
#define madvise_log_if_error(addr, len, advice) \
if (madvise(addr, len, advice)) \
LOGE("%s%p%s%zu%s%d%s%s%s", "madvise addr:", addr, "len:", len, "with advice ", advice, " failed with error: ", strerror(errno), ", Ignored")
#endif /* EHVIEWER_EHVIEWER_H */
================================================
FILE: app/src/main/cpp/gifutils.c
================================================
/*
* Copyright 2023 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 .
*/
#include
#include
#include
#include
#include
#include
#include
#define LOG_TAG "gifUtils"
#include "ehviewer.h"
#define GIF_HEADER_87A "GIF87a"
#define GIF_HEADER_89A "GIF89a"
#define GIF_HEADER_LENGTH 6
static int FRAME_DELAY_START_MARKER = 0x0021F904;
typedef signed char byte;
#define FRAME_DELAY_START_MARKER ((byte*)(&FRAME_DELAY_START_MARKER))
#define MINIMUM_FRAME_DELAY 2
#define DEFAULT_FRAME_DELAY 10
static inline bool isGif(void *addr) {
return !memcmp(addr, GIF_HEADER_87A, GIF_HEADER_LENGTH) ||
!memcmp(addr, GIF_HEADER_89A, GIF_HEADER_LENGTH);
}
static void doRewrite(byte *addr, size_t size) {
if (size < 7 || !isGif(addr)) return;
for (size_t i = 0; i < size - 8; i++) {
// TODO: Optimize this hex find with SIMD?
if (addr[i] == FRAME_DELAY_START_MARKER[3] && addr[i + 1] == FRAME_DELAY_START_MARKER[2] &&
addr[i + 2] == FRAME_DELAY_START_MARKER[1] &&
addr[i + 3] == FRAME_DELAY_START_MARKER[0]) {
byte *end = addr + i + 4;
if (end[4] != 0) continue;
int frameDelay = end[2] << 8 | end[1];
if (frameDelay >= MINIMUM_FRAME_DELAY)
break; // Quit if the first block looks normal, for performance
end[1] = DEFAULT_FRAME_DELAY;
end[2] = 0;
}
}
}
JNIEXPORT jboolean JNICALL
Java_com_hippo_ehviewer_jni_GifUtilsKt_isGif(JNIEnv *env, jclass clazz, jint fd) {
byte buffer[GIF_HEADER_LENGTH];
return read(fd, buffer, GIF_HEADER_LENGTH) == GIF_HEADER_LENGTH &&
isGif(buffer);
}
JNIEXPORT void JNICALL
Java_com_hippo_ehviewer_jni_GifUtilsKt_rewriteGifSource(JNIEnv *env, jclass clazz, jobject buffer) {
byte *addr = (*env)->GetDirectBufferAddress(env, buffer);
size_t size = (*env)->GetDirectBufferCapacity(env, buffer);
doRewrite(addr, size);
}
JNIEXPORT jobject JNICALL
Java_com_hippo_ehviewer_jni_GifUtilsKt_mmap(JNIEnv *env, jclass clazz, jint fd) {
struct stat64 st;
fstat64(fd, &st);
size_t size = st.st_size;
byte *addr = mmap64(0, size, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0);
if (addr == MAP_FAILED) return NULL;
return (*env)->NewDirectByteBuffer(env, addr, size);
}
JNIEXPORT void JNICALL
Java_com_hippo_ehviewer_jni_GifUtilsKt_munmap(JNIEnv *env, jclass clazz, jobject buffer) {
byte *addr = (*env)->GetDirectBufferAddress(env, buffer);
size_t size = (*env)->GetDirectBufferCapacity(env, buffer);
munmap(addr, size);
}
================================================
FILE: app/src/main/cpp/hash.c
================================================
/*
* Copyright 2022-2024 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 .
*/
#include
#include
#include
#include "ehviewer.h"
#define BUFFER_SIZE 8192
typedef uint8_t byte;
const char hex_digits[] = "0123456789abcdef";
JNIEXPORT jstring JNICALL
Java_com_hippo_ehviewer_jni_HashKt_sha1(JNIEnv *env, jclass clazz, jint fd) {
EH_UNUSED(clazz);
struct sha1_ctx ctx;
sha1_init(&ctx);
size_t bytes_read;
byte buffer[BUFFER_SIZE];
while ((bytes_read = read(fd, buffer, BUFFER_SIZE)) > 0) {
sha1_update(&ctx, bytes_read, buffer);
}
byte digest[SHA1_DIGEST_SIZE];
sha1_digest(&ctx, SHA1_DIGEST_SIZE, digest);
byte byte;
char hex_digest[2 * SHA1_DIGEST_SIZE + 1];
for (int i = 0; i < SHA1_DIGEST_SIZE; i++) {
byte = digest[i];
hex_digest[2 * i] = hex_digits[byte >> 4 & 0xF];
hex_digest[2 * i + 1] = hex_digits[byte & 0xF];
}
hex_digest[2 * SHA1_DIGEST_SIZE] = '\0';
return (*env)->NewStringUTF(env, hex_digest);
}
================================================
FILE: app/src/main/cpp/image.c
================================================
/*
* 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 .
*/
#include
#include
#include
#include
#include
#include
#include
#define TAG "ImageDecoder_wrapper"
#include "ehviewer.h"
#define IMAGE_TILE_MAX_SIZE (512 * 512)
static char tile_buffer[IMAGE_TILE_MAX_SIZE * 8];
bool copy_pixels(const void *src, int src_w, int src_h, int src_x, int src_y,
void *dst, int dst_w, int dst_h, int dst_x, int dst_y,
int width, int height, int stride) {
int left;
int line;
size_t line_stride;
int src_stride;
int src_pos;
int dst_pos;
size_t dst_blank_length;
// Sanitize
if (src_x < 0) {
width -= src_x;
dst_x -= src_x;
src_x = 0;
}
if (dst_x < 0) {
width -= dst_x;
src_x -= dst_x;
dst_x = 0;
}
if (width <= 0) {
return false;
}
if (src_y < 0) {
height -= src_y;
dst_y -= src_y;
src_y = 0;
}
if (dst_y < 0) {
height -= dst_y;
src_y -= dst_y;
dst_y = 0;
}
if (height <= 0) {
return false;
}
left = src_x + width - src_w;
if (left > 0) {
width -= left;
}
left = dst_x + width - dst_w;
if (left > 0) {
width -= left;
}
if (width <= 0) {
return false;
}
left = src_y + height - src_h;
if (left > 0) {
height -= left;
}
left = dst_y + height - dst_h;
if (left > 0) {
height -= left;
}
if (height <= 0) {
return false;
}
// Init
line_stride = (size_t) (width * stride);
src_stride = src_w * stride;
src_pos = src_y * src_stride + src_x * stride;
dst_pos = 0;
dst_blank_length = (size_t) (dst_y * dst_w + dst_x) * stride;
// First line
dst_pos += (int) dst_blank_length;
memcpy(dst + dst_pos, src + src_pos, line_stride);
dst_pos += (int) line_stride;
src_pos += src_stride;
// Other lines
dst_blank_length = (size_t) ((dst_w - width) * stride);
for (line = 1; line < height; line++) {
dst_pos += (int) dst_blank_length;
memcpy(dst + dst_pos, src + src_pos, line_stride);
dst_pos += (int) line_stride;
src_pos += src_stride;
}
return true;
}
JNIEXPORT void JNICALL
Java_com_hippo_ehviewer_jni_ImageKt_nativeTexImage(JNIEnv *env, jclass clazz, jobject bitmap, jboolean init,
jint offset_x, jint offset_y, jint width, jint height) {
if (width * height > IMAGE_TILE_MAX_SIZE)
return;
AndroidBitmapInfo info;
void *pixels = NULL;
AndroidBitmap_lockPixels(env, bitmap, &pixels);
AndroidBitmap_getInfo(env, bitmap, &info);
bool is_f16 = info.format == ANDROID_BITMAP_FORMAT_RGBA_F16;
copy_pixels(pixels, info.width, info.height, offset_x, offset_y, tile_buffer, width, height, 0, 0, width, height, is_f16 ? 8 : 4);
AndroidBitmap_unlockPixels(env, bitmap);
if (init) {
glTexImage2D(GL_TEXTURE_2D, 0, is_f16 ? GL_RGBA16F : GL_RGBA8, width, height, 0, GL_RGBA,
is_f16 ? GL_HALF_FLOAT : GL_UNSIGNED_BYTE, tile_buffer);
} else {
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height, GL_RGBA,
is_f16 ? GL_HALF_FLOAT : GL_UNSIGNED_BYTE, tile_buffer);
}
}
================================================
FILE: app/src/main/cpp/natsort/strnatcmp.c
================================================
/* -*- mode: c; c-file-style: "k&r" -*-
strnatcmp.c -- Perform 'natural order' comparisons of strings in C.
Copyright (C) 2000, 2004 by Martin Pool
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
#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(const char *a)
{
return isdigit((unsigned char) *a);
}
static inline int
nat_isspace(const char *a)
{
return isspace((unsigned char) *a) || *a == '0' && nat_isdigit(a + 1);
}
static int
compare_right(const char *a, const char *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;
}
}
int
strnatcmp(const char *a, const char *b)
{
int result;
while (1) {
/* skip over leading spaces or zeros */
while (nat_isspace(a))
a++;
while (nat_isspace(b))
b++;
/* process run of digits */
if (nat_isdigit(a) && nat_isdigit(b)) {
if ((result = compare_right(a, b)) != 0)
return result;
}
if (!*a && !*b) {
/* The strings compare the same. Perhaps the caller
will want to call strcmp to break the tie. */
return 0;
}
if (*a < *b)
return -1;
if (*a > *b)
return +1;
a++; b++;
}
}
================================================
FILE: app/src/main/cpp/natsort/strnatcmp.h
================================================
/* -*- mode: c; c-file-style: "k&r" -*-
strnatcmp.c -- Perform 'natural order' comparisons of strings in C.
Copyright (C) 2000, 2004 by Martin Pool
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
int strnatcmp(const char *a, const char *b);
#ifdef __cplusplus
}
#endif
================================================
FILE: app/src/main/cpp/nettle/.gitignore
================================================
/nettle
================================================
FILE: app/src/main/cpp/nettle/CMakeLists.txt
================================================
cmake_minimum_required(VERSION 3.4.1)
project(nettle C)
set(LIBNETTLE_DEFINITIONS
-DHAVE_CONFIG_H)
set(LIBNETTLE_SOURCES
nettle/aes-decrypt-internal.c
nettle/aes-decrypt.c
nettle/aes-decrypt-table.c
nettle/aes128-decrypt.c
nettle/aes192-decrypt.c
nettle/aes256-decrypt.c
nettle/aes-encrypt-internal.c
nettle/aes-encrypt.c
nettle/aes-encrypt-table.c
nettle/aes128-encrypt.c
nettle/aes192-encrypt.c
nettle/aes256-encrypt.c
nettle/aes-invert-internal.c
nettle/aes-set-key-internal.c
nettle/aes-set-encrypt-key.c
nettle/aes-set-decrypt-key.c
nettle/aes128-set-encrypt-key.c
nettle/aes128-set-decrypt-key.c
nettle/aes128-meta.c
nettle/aes192-set-encrypt-key.c
nettle/aes192-set-decrypt-key.c
nettle/aes192-meta.c
nettle/aes256-set-encrypt-key.c
nettle/aes256-set-decrypt-key.c
nettle/aes256-meta.c
nettle/nist-keywrap.c
nettle/arcfour.c
nettle/arctwo.c
nettle/arctwo-meta.c
nettle/blowfish.c
nettle/blowfish-bcrypt.c
nettle/base16-encode.c
nettle/base16-decode.c
nettle/base16-meta.c
nettle/base64-encode.c
nettle/base64-decode.c
nettle/base64-meta.c
nettle/base64url-encode.c
nettle/base64url-decode.c
nettle/base64url-meta.c
nettle/buffer.c
nettle/buffer-init.c
nettle/camellia-crypt-internal.c
nettle/camellia-table.c
nettle/camellia-absorb.c
nettle/camellia-invert-key.c
nettle/camellia128-set-encrypt-key.c
nettle/camellia128-crypt.c
nettle/camellia128-set-decrypt-key.c
nettle/camellia128-meta.c
nettle/camellia192-meta.c
nettle/camellia256-set-encrypt-key.c
nettle/camellia256-crypt.c
nettle/camellia256-set-decrypt-key.c
nettle/camellia256-meta.c
nettle/cast128.c
nettle/cast128-meta.c
nettle/cbc.c
nettle/cbc-aes128-encrypt.c
nettle/cbc-aes192-encrypt.c
nettle/cbc-aes256-encrypt.c
nettle/ccm.c
nettle/ccm-aes128.c
nettle/ccm-aes192.c
nettle/ccm-aes256.c
nettle/cfb.c
nettle/siv-cmac.c
nettle/siv-cmac-aes128.c
nettle/siv-cmac-aes256.c
nettle/cnd-memcpy.c
nettle/chacha-crypt.c
nettle/chacha-core-internal.c
nettle/chacha-poly1305.c
nettle/chacha-poly1305-meta.c
nettle/chacha-set-key.c
nettle/chacha-set-nonce.c
nettle/ctr.c
nettle/ctr16.c
nettle/des.c
nettle/des3.c
nettle/eax.c
nettle/eax-aes128.c
nettle/eax-aes128-meta.c
nettle/ghash-set-key.c
nettle/ghash-update.c
nettle/gcm.c
nettle/gcm-aes.c
nettle/gcm-aes128.c
nettle/gcm-aes128-meta.c
nettle/gcm-aes192.c
nettle/gcm-aes192-meta.c
nettle/gcm-aes256.c
nettle/gcm-aes256-meta.c
nettle/gcm-camellia128.c
nettle/gcm-camellia128-meta.c
nettle/gcm-camellia256.c
nettle/gcm-camellia256-meta.c
nettle/cmac.c
nettle/cmac64.c
nettle/cmac-aes128.c
nettle/cmac-aes256.c
nettle/cmac-des3.c
nettle/cmac-aes128-meta.c
nettle/cmac-aes256-meta.c
nettle/cmac-des3-meta.c
nettle/gost28147.c
nettle/gosthash94.c
nettle/gosthash94-meta.c
nettle/hmac.c
nettle/hmac-gosthash94.c
nettle/hmac-md5.c
nettle/hmac-ripemd160.c
nettle/hmac-sha1.c
nettle/hmac-sha224.c
nettle/hmac-sha256.c
nettle/hmac-sha384.c
nettle/hmac-sha512.c
nettle/hmac-streebog.c
nettle/hmac-sm3.c
nettle/hmac-gosthash94-meta.c
nettle/hmac-md5-meta.c
nettle/hmac-ripemd160-meta.c
nettle/hmac-sha1-meta.c
nettle/hmac-sha224-meta.c
nettle/hmac-sha256-meta.c
nettle/hmac-sha384-meta.c
nettle/hmac-sha512-meta.c
nettle/hmac-streebog-meta.c
nettle/hmac-sm3-meta.c
nettle/knuth-lfib.c
nettle/hkdf.c
nettle/md2.c
nettle/md2-meta.c
nettle/md4.c
nettle/md4-meta.c
nettle/md5.c
nettle/md5-compat.c
nettle/md5-meta.c
nettle/memeql-sec.c
nettle/memxor.c
nettle/memxor3.c
nettle/nettle-lookup-hash.c
nettle/nettle-meta-aeads.c
nettle/nettle-meta-armors.c
nettle/nettle-meta-ciphers.c
nettle/nettle-meta-hashes.c
nettle/nettle-meta-macs.c
nettle/pbkdf2.c
nettle/pbkdf2-hmac-gosthash94.c
nettle/pbkdf2-hmac-sha1.c
nettle/pbkdf2-hmac-sha256.c
nettle/pbkdf2-hmac-sha384.c
nettle/pbkdf2-hmac-sha512.c
nettle/poly1305-aes.c
nettle/poly1305-internal.c
nettle/realloc.c
nettle/ripemd160.c
nettle/ripemd160-compress.c
nettle/ripemd160-meta.c
nettle/salsa20-core-internal.c
nettle/salsa20-crypt-internal.c
nettle/salsa20-crypt.c
nettle/salsa20r12-crypt.c
nettle/salsa20-set-key.c
nettle/salsa20-set-nonce.c
nettle/salsa20-128-set-key.c
nettle/salsa20-256-set-key.c
nettle/sha1.c
nettle/sha1-compress.c
nettle/sha1-meta.c
nettle/sha256.c
nettle/sha256-compress-n.c
nettle/sha224-meta.c
nettle/sha256-meta.c
nettle/sha512.c
nettle/sha512-compress.c
nettle/sha384-meta.c
nettle/sha512-meta.c
nettle/sha512-224-meta.c
nettle/sha512-256-meta.c
nettle/sha3.c
nettle/sha3-permute.c
nettle/sha3-shake.c
nettle/sha3-224.c
nettle/sha3-224-meta.c
nettle/sha3-256.c
nettle/sha3-256-meta.c
nettle/sha3-384.c
nettle/sha3-384-meta.c
nettle/sha3-512.c
nettle/sha3-512-meta.c
nettle/shake128.c
nettle/shake256.c
nettle/sm3.c
nettle/sm3-meta.c
nettle/serpent-set-key.c
nettle/serpent-encrypt.c
nettle/serpent-decrypt.c
nettle/serpent-meta.c
nettle/streebog.c
nettle/streebog-meta.c
nettle/twofish.c
nettle/twofish-meta.c
nettle/umac-nh.c
nettle/umac-nh-n.c
nettle/umac-l2.c
nettle/umac-l3.c
nettle/umac-poly64.c
nettle/umac-poly128.c
nettle/umac-set-key.c
nettle/umac32.c
nettle/umac64.c
nettle/umac96.c
nettle/umac128.c
nettle/version.c
nettle/write-be32.c
nettle/write-le32.c
nettle/write-le64.c
nettle/yarrow256.c
nettle/yarrow_key_event.c
nettle/xts.c
nettle/xts-aes128.c
nettle/xts-aes256.c
)
set(LIBNETTLE_INCLUDES
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/nettle
)
add_library(nettle STATIC ${LIBNETTLE_SOURCES})
target_include_directories(nettle PUBLIC ${LIBNETTLE_INCLUDES})
target_compile_definitions(nettle PRIVATE ${LIBNETTLE_DEFINITIONS})
================================================
FILE: app/src/main/cpp/nettle/config.h
================================================
/* config.h. Generated from config.h.in by configure. */
/* config.h.in. Generated from configure.ac by autoheader. */
/* Define if building universal (internal helper macro) */
/* #undef AC_APPLE_UNIVERSAL_BUILD */
/* Define to 1 if using 'alloca.c'. */
/* #undef C_ALLOCA */
/* Define to 1 if you have 'alloca', as a function or macro. */
#define HAVE_ALLOCA 1
/* Define to 1 if works. */
#define HAVE_ALLOCA_H 1
/* Define if __builtin_bswap64 is available */
#define HAVE_BUILTIN_BSWAP64 1
/* Define if clock_gettime is available */
#define HAVE_CLOCK_GETTIME 1
/* Define to 1 if you have the header file. */
#define HAVE_DLFCN_H 1
/* Define to 1 if you have the `elf_aux_info' function. */
/* #undef HAVE_ELF_AUX_INFO */
/* Define if fcntl file locking is available */
#define HAVE_FCNTL_LOCKING 1
/* Define if the compiler understands __attribute__ */
#define HAVE_GCC_ATTRIBUTE 1
/* Define to 1 if you have the `getline' function. */
#define HAVE_GETLINE 1
/* Define to 1 if you have the header file. */
#define HAVE_INTTYPES_H 1
/* Define to 1 if you have dlopen (with -ldl). */
#define HAVE_LIBDL 1
/* Define to 1 if you have the `gmp' library (-lgmp). */
/* #undef HAVE_LIBGMP */
/* Define if compiler and linker supports __attribute__ ifunc */
#define HAVE_LINK_IFUNC 1
/* Define to 1 if you have the header file. */
#define HAVE_MALLOC_H 1
/* Define to 1 each of the following for which a native (ie. CPU specific)
implementation of the corresponding routine exists. */
/* #undef HAVE_NATIVE_memxor3 */
/* #undef HAVE_NATIVE_aes_decrypt */
/* #undef HAVE_NATIVE_aes_encrypt */
/* #undef HAVE_NATIVE_aes_invert */
/* #undef HAVE_NATIVE_aes128_decrypt */
/* #undef HAVE_NATIVE_aes128_encrypt */
/* #undef HAVE_NATIVE_aes128_invert_key */
/* #undef HAVE_NATIVE_aes128_set_decrypt_key */
/* #undef HAVE_NATIVE_aes128_set_encrypt_key */
/* #undef HAVE_NATIVE_aes192_decrypt */
/* #undef HAVE_NATIVE_aes192_encrypt */
/* #undef HAVE_NATIVE_aes192_invert_key */
/* #undef HAVE_NATIVE_aes192_set_decrypt_key */
/* #undef HAVE_NATIVE_aes192_set_encrypt_key */
/* #undef HAVE_NATIVE_aes256_decrypt */
/* #undef HAVE_NATIVE_aes256_encrypt */
/* #undef HAVE_NATIVE_aes256_invert_key */
/* #undef HAVE_NATIVE_aes256_set_decrypt_key */
/* #undef HAVE_NATIVE_aes256_set_encrypt_key */
/* #undef HAVE_NATIVE_cbc_aes128_encrypt */
/* #undef HAVE_NATIVE_cbc_aes192_encrypt */
/* #undef HAVE_NATIVE_cbc_aes256_encrypt */
/* #undef HAVE_NATIVE_chacha_core */
/* #undef HAVE_NATIVE_chacha_2core */
#define HAVE_NATIVE_chacha_3core 1
/* #undef HAVE_NATIVE_chacha_4core */
/* #undef HAVE_NATIVE_fat_chacha_2core */
/* #undef HAVE_NATIVE_fat_chacha_3core */
/* #undef HAVE_NATIVE_fat_chacha_4core */
/* #undef HAVE_NATIVE_ecc_curve25519_modp */
/* #undef HAVE_NATIVE_ecc_curve448_modp */
/* #undef HAVE_NATIVE_ecc_secp192r1_modp */
/* #undef HAVE_NATIVE_ecc_secp192r1_redc */
/* #undef HAVE_NATIVE_ecc_secp224r1_modp */
/* #undef HAVE_NATIVE_ecc_secp224r1_redc */
/* #undef HAVE_NATIVE_ecc_secp256r1_modp */
/* #undef HAVE_NATIVE_ecc_secp256r1_redc */
/* #undef HAVE_NATIVE_ecc_secp384r1_modp */
/* #undef HAVE_NATIVE_ecc_secp384r1_redc */
/* #undef HAVE_NATIVE_ecc_secp521r1_modp */
/* #undef HAVE_NATIVE_ecc_secp521r1_redc */
/* #undef HAVE_NATIVE_poly1305_set_key */
/* #undef HAVE_NATIVE_poly1305_block */
/* #undef HAVE_NATIVE_poly1305_digest */
/* #undef HAVE_NATIVE_poly1305_blocks */
/* #undef HAVE_NATIVE_fat_poly1305_blocks */
/* #undef HAVE_NATIVE_ghash_set_key */
/* #undef HAVE_NATIVE_ghash_update */
/* #undef HAVE_NATIVE_gcm_aes_encrypt */
/* #undef HAVE_NATIVE_gcm_aes_decrypt */
/* #undef HAVE_NATIVE_salsa20_core */
#define HAVE_NATIVE_salsa20_2core 1
/* #undef HAVE_NATIVE_fat_salsa20_2core */
/* #undef HAVE_NATIVE_sha1_compress */
/* #undef HAVE_NATIVE_sha256_compress_n */
/* #undef HAVE_NATIVE_sha512_compress */
/* #undef HAVE_NATIVE_sha3_permute */
/* #undef HAVE_NATIVE_umac_nh */
/* #undef HAVE_NATIVE_umac_nh_n */
/* Define to 1 if you have the header file. */
/* #undef HAVE_OPENSSL_EC_H */
/* Define to 1 if you have the header file. */
/* #undef HAVE_OPENSSL_EVP_H */
/* Define to 1 if you have the header file. */
/* #undef HAVE_OPENSSL_RSA_H */
/* Define to 1 if you have the `secure_getenv' function. */
#define HAVE_SECURE_GETENV 1
/* Define to 1 if you have the header file. */
#define HAVE_STDINT_H 1
/* Define to 1 if you have the header file. */
#define HAVE_STDIO_H 1
/* Define to 1 if you have the header file. */
#define HAVE_STDLIB_H 1
/* Define to 1 if you have the header file. */
#define HAVE_STRINGS_H 1
/* Define to 1 if you have the header file. */
#define HAVE_STRING_H 1
/* Define to 1 if you have the header file. */
#define HAVE_SYS_STAT_H 1
/* Define to 1 if you have the header file. */
#define HAVE_SYS_TYPES_H 1
/* Define to 1 if you have the header file. */
#define HAVE_UNISTD_H 1
/* Define to 1 if you have the header file. */
/* #undef HAVE_VALGRIND_MEMCHECK_H */
/* Define to the address where bug reports for this package should be sent. */
#define PACKAGE_BUGREPORT "nettle-bugs@lists.lysator.liu.se"
/* Define to the full name of this package. */
#define PACKAGE_NAME "nettle"
/* Define to the full name and version of this package. */
#define PACKAGE_STRING "nettle 3.10.2"
/* Define to the one symbol short name of this package. */
#define PACKAGE_TARNAME "nettle"
/* Define to the home page for this package. */
#define PACKAGE_URL ""
/* Define to the version of this package. */
#define PACKAGE_VERSION "3.10.2"
/* The size of `long', as computed by sizeof. */
#define SIZEOF_LONG __SIZEOF_LONG__
/* The size of `size_t', as computed by sizeof. */
#define SIZEOF_SIZE_T __SIZEOF_SIZE_T__
/* If using the C implementation of alloca, define if you know the
direction of stack growth for your system; otherwise it will be
automatically deduced at runtime.
STACK_DIRECTION > 0 => grows toward higher addresses
STACK_DIRECTION < 0 => grows toward lower addresses
STACK_DIRECTION = 0 => direction of growth unknown */
/* #undef STACK_DIRECTION */
/* Define to 1 if all of the C90 standard headers exist (not just the ones
required in a freestanding environment). This macro is provided for
backward compatibility; new code need not use it. */
#define STDC_HEADERS 1
/* Defined to enable additional asserts */
/* #undef WITH_EXTRA_ASSERTS */
/* Defined if public key features are enabled */
/* #undef WITH_HOGWEED */
/* Define if you have openssl libcrypto (used for benchmarking) */
/* #undef WITH_OPENSSL */
/* Define WORDS_BIGENDIAN to 1 if your processor stores words with the most
significant byte first (like Motorola and SPARC, unlike Intel). */
#if defined AC_APPLE_UNIVERSAL_BUILD
# if defined __BIG_ENDIAN__
# define WORDS_BIGENDIAN 1
# endif
#else
# ifndef WORDS_BIGENDIAN
/* # undef WORDS_BIGENDIAN */
# endif
#endif
/* Define to empty if `const' does not conform to ANSI C. */
/* #undef const */
/* Define to `int' if doesn't define. */
/* #undef gid_t */
/* Define to `__inline__' or `__inline' if that's what the C compiler
calls it, or to nothing if 'inline' is not supported under any name. */
#ifndef __cplusplus
/* #undef inline */
#endif
/* Define to `unsigned int' if does not define. */
/* #undef size_t */
/* Define to `int' if doesn't define. */
/* #undef uid_t */
/* AIX requires this to be the first thing in the file. */
#ifndef __GNUC__
# if HAVE_ALLOCA_H
# include
# else
# ifdef _AIX
#pragma alloca
# else
# ifndef alloca /* predefined by HP cc +Olibcalls */
char *alloca ();
# endif
# endif
/* Needed for alloca on windows */
# if HAVE_MALLOC_H
# include
# endif
# endif
#else /* defined __GNUC__ */
# if HAVE_ALLOCA_H
# include
# else
/* Needed for alloca on windows, also with gcc */
# if HAVE_MALLOC_H
# include
# endif
# endif
#endif
#if __GNUC__ && HAVE_GCC_ATTRIBUTE
# define NORETURN __attribute__ ((__noreturn__))
# define PRINTF_STYLE(f, a) __attribute__ ((__format__ (__printf__, f, a)))
# define UNUSED __attribute__ ((__unused__))
#else
# define NORETURN
# define PRINTF_STYLE(f, a)
# define UNUSED
#endif
#if defined(__x86_64__) || defined(__arch64__)
# define HAVE_NATIVE_64_BIT 1
#else
/* Needs include of before use. */
# define HAVE_NATIVE_64_BIT (SIZEOF_LONG * CHAR_BIT >= 64)
#endif
================================================
FILE: app/src/main/cpp/nettle/keymap.h
================================================
================================================
FILE: app/src/main/cpp/nettle/rotors.h
================================================
================================================
FILE: app/src/main/cpp/nettle/version.h
================================================
/* version.h
Information about library version.
Copyright (C) 2015 Red Hat, Inc.
Copyright (C) 2015 Niels Möller
This file is part of GNU Nettle.
GNU Nettle is free software: you can redistribute it and/or
modify it under the terms of either:
* the GNU Lesser General Public License as published by the Free
Software Foundation; either version 3 of the License, or (at your
option) any later version.
or
* the GNU General Public License as published by the Free
Software Foundation; either version 2 of the License, or (at your
option) any later version.
or both in parallel, as here.
GNU Nettle 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 copies of the GNU General Public License and
the GNU Lesser General Public License along with this program. If
not, see http://www.gnu.org/licenses/.
*/
#ifndef NETTLE_VERSION_H_INCLUDED
#define NETTLE_VERSION_H_INCLUDED
#ifdef __cplusplus
extern "C" {
#endif
/* Individual version numbers in decimal */
#define NETTLE_VERSION_MAJOR 3
#define NETTLE_VERSION_MINOR 10
#define NETTLE_USE_MINI_GMP 0
/* We need a preprocessor constant for GMP_NUMB_BITS, simply using
sizeof(mp_limb_t) * CHAR_BIT is not good enough. */
#if NETTLE_USE_MINI_GMP
# define GMP_NUMB_BITS n/a
#endif
int
nettle_version_major (void);
int
nettle_version_minor (void);
#ifdef __cplusplus
}
#endif
#endif /* NETTLE_VERSION_H_INCLUDED */
================================================
FILE: app/src/main/java/com/hippo/app/CheckBoxDialogBuilder.kt
================================================
/*
* 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.app
import android.annotation.SuppressLint
import android.content.Context
import android.view.LayoutInflater
import android.widget.CheckBox
import androidx.appcompat.app.AlertDialog
import com.hippo.ehviewer.R
@SuppressLint("InflateParams")
class CheckBoxDialogBuilder(
context: Context,
message: String?,
checkText: String?,
checked: Boolean,
) : AlertDialog.Builder(
context,
) {
private val mCheckBox: CheckBox
val isChecked: Boolean
get() = mCheckBox.isChecked
init {
val view = LayoutInflater.from(getContext()).inflate(R.layout.dialog_checkbox_builder, null)
setView(view)
setMessage(message)
mCheckBox = view.findViewById(R.id.checkbox)
mCheckBox.text = checkText
mCheckBox.isChecked = checked
view.setOnClickListener { mCheckBox.toggle() }
}
}
================================================
FILE: app/src/main/java/com/hippo/app/EditTextCheckBoxDialogBuilder.kt
================================================
/*
* Copyright 2022 Moedog
*
* 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 .
*/
package com.hippo.app
import android.annotation.SuppressLint
import android.content.Context
import android.content.DialogInterface
import android.view.KeyEvent
import android.view.LayoutInflater
import android.widget.CheckBox
import android.widget.EditText
import android.widget.TextView
import android.widget.TextView.OnEditorActionListener
import androidx.appcompat.app.AlertDialog
import com.google.android.material.textfield.TextInputLayout
import com.hippo.ehviewer.R
@SuppressLint("InflateParams")
class EditTextCheckBoxDialogBuilder(
context: Context,
text: String?,
hint: String?,
checkText: String?,
checked: Boolean,
) : AlertDialog.Builder(
context,
),
OnEditorActionListener {
private val mCheckBox: CheckBox
val isChecked: Boolean
get() = mCheckBox.isChecked
private val mTextInputLayout: TextInputLayout
private val editText: EditText
private var mDialog: AlertDialog? = null
val text: String
get() = editText.text.toString()
fun setError(error: CharSequence?) {
mTextInputLayout.error = error
}
override fun create(): AlertDialog {
mDialog = super.create()
return mDialog as AlertDialog
}
override fun onEditorAction(v: TextView?, p1: Int, event: KeyEvent?): Boolean = if (mDialog != null) {
val button = mDialog!!.getButton(DialogInterface.BUTTON_POSITIVE)
button?.performClick()
true
} else {
false
}
init {
val view = LayoutInflater.from(getContext()).inflate(R.layout.dialog_edittextcheckbox_builder, null)
setView(view)
mCheckBox = view.findViewById(R.id.checkbox)
mCheckBox.text = checkText
mCheckBox.isChecked = checked
view.setOnClickListener { mCheckBox.toggle() }
editText = view.findViewById(R.id.edit_text)
editText.setText(text)
editText.setSelection(editText.text.length)
editText.setOnEditorActionListener(this)
mTextInputLayout = view.findViewById(R.id.text_input_layout)
mTextInputLayout.hint = hint
}
}
================================================
FILE: app/src/main/java/com/hippo/app/EditTextDialogBuilder.kt
================================================
/*
* 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.app
import android.annotation.SuppressLint
import android.content.Context
import android.content.DialogInterface
import android.view.KeyEvent
import android.view.LayoutInflater
import android.widget.EditText
import android.widget.TextView
import android.widget.TextView.OnEditorActionListener
import androidx.appcompat.app.AlertDialog
import com.google.android.material.textfield.TextInputLayout
import com.hippo.ehviewer.R
@SuppressLint("InflateParams")
class EditTextDialogBuilder(
context: Context,
text: String?,
hint: String?,
) : AlertDialog.Builder(
context,
),
OnEditorActionListener {
private val mTextInputLayout: TextInputLayout
val editText: EditText
private var mDialog: AlertDialog? = null
val text: String
get() = editText.text.toString()
fun setError(error: CharSequence?) {
mTextInputLayout.error = error
}
override fun create(): AlertDialog {
mDialog = super.create()
return mDialog as AlertDialog
}
override fun onEditorAction(v: TextView?, p1: Int, event: KeyEvent?): Boolean = if (mDialog != null) {
val button = mDialog!!.getButton(DialogInterface.BUTTON_POSITIVE)
button?.performClick()
true
} else {
false
}
init {
val view = LayoutInflater.from(getContext()).inflate(R.layout.dialog_edittext_builder, null)
setView(view)
mTextInputLayout = view as TextInputLayout
editText = view.findViewById(R.id.edit_text)
editText.setText(text)
editText.setSelection(editText.text.length)
editText.setOnEditorActionListener(this)
mTextInputLayout.hint = hint
}
}
================================================
FILE: app/src/main/java/com/hippo/app/ListCheckBoxDialogBuilder.kt
================================================
/*
* 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.app
import android.annotation.SuppressLint
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.CheckBox
import android.widget.ListView
import androidx.appcompat.app.AlertDialog
import com.hippo.ehviewer.R
import com.hippo.yorozuya.ViewUtils
@SuppressLint("InflateParams")
class ListCheckBoxDialogBuilder(
context: Context,
items: List,
listener: (ListCheckBoxDialogBuilder?, AlertDialog?, Int) -> Unit,
checkText: String?,
checked: Boolean,
) : AlertDialog.Builder(
context,
) {
private val mCheckBox: CheckBox
private var mDialog: AlertDialog? = null
val isChecked: Boolean
get() = mCheckBox.isChecked
override fun create(): AlertDialog {
mDialog = super.create()
return mDialog as AlertDialog
}
init {
val view =
LayoutInflater.from(getContext()).inflate(R.layout.dialog_list_checkbox_builder, null)
setView(view)
val listView = ViewUtils.`$$`(view, R.id.list_view) as ListView
mCheckBox = ViewUtils.`$$`(view, R.id.checkbox) as CheckBox
listView.adapter = ArrayAdapter(getContext(), R.layout.item_select_dialog, items)
listView.onItemClickListener =
AdapterView.OnItemClickListener { _: AdapterView<*>?, _: View?, position: Int, _: Long ->
listener(this@ListCheckBoxDialogBuilder, mDialog, position)
mDialog?.dismiss()
}
mCheckBox.text = checkText
mCheckBox.isChecked = checked
}
}
================================================
FILE: app/src/main/java/com/hippo/database/MSQLiteBuilder.kt
================================================
/*
* Copyright 2017 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.database
import android.content.Context
import android.database.sqlite.SQLiteOpenHelper
import android.util.SparseArray
class MSQLiteBuilder {
companion object {
const val COLUMN_ID = "_id"
private val JAVA_TYPE_TO_SQLITE_TYPE: MutableMap?, String> = HashMap()
private fun javaTypeToSQLiteType(clazz: Class<*>?): String = JAVA_TYPE_TO_SQLITE_TYPE[clazz]
?: throw IllegalStateException("Unknown type: $clazz")
init {
JAVA_TYPE_TO_SQLITE_TYPE[Boolean::class.javaPrimitiveType] =
"INTEGER NOT NULL DEFAULT 0"
JAVA_TYPE_TO_SQLITE_TYPE[Byte::class.javaPrimitiveType] =
"INTEGER NOT NULL DEFAULT 0"
JAVA_TYPE_TO_SQLITE_TYPE[Short::class.javaPrimitiveType] =
"INTEGER NOT NULL DEFAULT 0"
JAVA_TYPE_TO_SQLITE_TYPE[Int::class.javaPrimitiveType] = "INTEGER NOT NULL DEFAULT 0"
JAVA_TYPE_TO_SQLITE_TYPE[Long::class.javaPrimitiveType] =
"INTEGER NOT NULL DEFAULT 0"
JAVA_TYPE_TO_SQLITE_TYPE[Float::class.javaPrimitiveType] =
"REAL NOT NULL DEFAULT 0"
JAVA_TYPE_TO_SQLITE_TYPE[Double::class.javaPrimitiveType] = "REAL NOT NULL DEFAULT 0"
JAVA_TYPE_TO_SQLITE_TYPE[String::class.java] = "TEXT"
}
}
private val statementsMap = SparseArray?>()
private var version = 0
private var statements: MutableList? = null
/**
* Bump database version.
*/
fun version(version: Int): MSQLiteBuilder {
check(version > this.version) {
(
"New version must be bigger than current version. " +
"current version: " + this.version + ", new version: " + version + "."
)
}
this.version = version
statements = ArrayList()
statementsMap.put(version, statements)
return this
}
/**
* Creates a table with int [.COLUMN_ID] primary key.
*/
fun createTable(
table: String,
column: String = COLUMN_ID,
clazz: Class<*>? = Int::class.javaPrimitiveType,
): MSQLiteBuilder = statement("CREATE TABLE " + table + " (" + column + " " + javaTypeToSQLiteType(clazz) + " PRIMARY KEY);")
/**
* Drops a table.
*/
fun dropTable(table: String): MSQLiteBuilder = statement("DROP TABLE $table;")
/**
* Inserts a column to the table.
*/
fun insertColumn(table: String, column: String, clazz: Class<*>?): MSQLiteBuilder = statement(
"ALTER TABLE $table ADD COLUMN $column " + javaTypeToSQLiteType(
clazz,
) + ";",
)
/**
* Add a statement.
*/
fun statement(statement: String): MSQLiteBuilder {
check(!(version == 0 || statements == null)) { "Call version() first!" }
statements!!.add(statement)
return this
}
/**
* Build a SQLiteOpenHelper from it.
*/
fun build(context: Context?, name: String?, version: Int): SQLiteOpenHelper = MSQLiteOpenHelper(context, name, version, this)
fun getStatements(oldVersion: Int, newVersion: Int): List {
val result: MutableList = ArrayList()
for (i in oldVersion + 1..newVersion) {
val list = statementsMap[i]
if (list != null) {
result.addAll(list)
}
}
return result
}
}
================================================
FILE: app/src/main/java/com/hippo/database/MSQLiteOpenHelper.kt
================================================
/*
* Copyright 2017 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.database
import android.content.Context
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
import com.hippo.util.SqlUtils
internal class MSQLiteOpenHelper(
context: Context?,
name: String?,
private val version: Int,
private val builder: MSQLiteBuilder,
) : SQLiteOpenHelper(context, name, null, version) {
override fun onCreate(db: SQLiteDatabase) {
onUpgrade(db, 0, version)
}
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
for (command in builder.getStatements(oldVersion, newVersion)) {
db.execSQL(command)
}
}
override fun onDowngrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
SqlUtils.dropAllTable(db)
onCreate(db)
}
}
================================================
FILE: app/src/main/java/com/hippo/drawable/AddDeleteDrawable.kt
================================================
/*
* Copyright (C) 2015 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.drawable
import android.animation.ObjectAnimator
import android.content.Context
import android.graphics.Canvas
import android.graphics.ColorFilter
import android.graphics.Paint
import android.graphics.Path
import android.graphics.PixelFormat
import android.graphics.drawable.Drawable
import androidx.annotation.Keep
import androidx.core.graphics.withTranslation
import com.hippo.ehviewer.R
import com.hippo.yorozuya.MathUtils
import kotlin.math.roundToInt
/**
* @param context used to get the configuration for the drawable from
*/
class AddDeleteDrawable(context: Context, color: Int) : Drawable() {
private val mPaint = Paint()
private val mPath = Path()
private val mSize: Int
private var mProgress = 0f
private var mAutoUpdateMirror = false
private var mVerticalMirror = false
init {
val resources = context.resources
mSize = resources.getDimensionPixelSize(R.dimen.add_size)
val barThickness = resources.getDimension(R.dimen.add_thickness).roundToInt().toFloat()
mPaint.setColor(color)
mPaint.style = Paint.Style.STROKE
mPaint.strokeJoin = Paint.Join.MITER
mPaint.strokeCap = Paint.Cap.BUTT
mPaint.strokeWidth = barThickness
val halfSize = (mSize / 2).toFloat()
mPath.moveTo(0f, -halfSize)
mPath.lineTo(0f, halfSize)
mPath.moveTo(-halfSize, 0f)
mPath.lineTo(halfSize, 0f)
}
override fun draw(canvas: Canvas) {
val bounds = getBounds()
val canvasRotate: Float = if (mVerticalMirror) {
MathUtils.lerp(270f, 135f, mProgress)
} else {
MathUtils.lerp(0f, 135f, mProgress)
}
canvas.withTranslation(bounds.centerX().toFloat(), bounds.centerY().toFloat()) {
rotate(canvasRotate)
drawPath(mPath, mPaint)
}
}
fun setColor(color: Int) {
mPaint.setColor(color)
invalidateSelf()
}
override fun setAlpha(alpha: Int) {
mPaint.setAlpha(alpha)
}
override fun setColorFilter(cf: ColorFilter?) {
mPaint.setColorFilter(cf)
}
override fun getIntrinsicHeight(): Int = mSize * 6 / 5
override fun getIntrinsicWidth(): Int = mSize * 6 / 5
/**
* If set, canvas is flipped when progress reached to end and going back to start.
*/
private fun setVerticalMirror(verticalMirror: Boolean) {
mVerticalMirror = verticalMirror
}
@get:Keep
@set:Keep
var progress: Float
get() = mProgress
set(progress) {
if (mAutoUpdateMirror) {
if (progress == 1f) {
setVerticalMirror(true)
} else if (progress == 0f) {
setVerticalMirror(false)
}
}
mProgress = progress
invalidateSelf()
}
fun setAdd(duration: Long) {
setShape(false, duration)
}
fun setDelete(duration: Long) {
setShape(true, duration)
}
private fun setShape(delete: Boolean, duration: Long) {
if (!(!delete && mProgress == 0f || delete && mProgress == 1f)) {
val endProgress = if (delete) 1f else 0f
if (duration <= 0) {
progress = endProgress
} else {
val oa = ObjectAnimator.ofFloat(this, "progress", endProgress)
oa.setDuration(duration)
oa.setAutoCancel(true)
oa.start()
}
}
}
@Deprecated(
"Deprecated in Java",
ReplaceWith("PixelFormat.TRANSLUCENT", "android.graphics.PixelFormat"),
)
override fun getOpacity(): Int = PixelFormat.TRANSLUCENT
}
================================================
FILE: app/src/main/java/com/hippo/drawable/BatteryDrawable.kt
================================================
/*
* Copyright (C) 2014 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.Color
import android.graphics.ColorFilter
import android.graphics.Paint
import android.graphics.PixelFormat
import android.graphics.Rect
import android.graphics.drawable.Drawable
import com.hippo.yorozuya.MathUtils
import kotlin.math.sqrt
class BatteryDrawable : Drawable() {
private val mPaint: Paint = Paint(Paint.ANTI_ALIAS_FLAG or Paint.DITHER_FLAG)
private val mTopRect: Rect
private val mBottomRect: Rect
private val mRightRect: Rect
private val mHeadRect: Rect
private val mElectRect: Rect
private var mColor = Color.WHITE
private var mWarningColor = Color.RED
private var mElect = -1
private var mStart = 0
private var mStop = 0
init {
mPaint.style = Paint.Style.FILL
mTopRect = Rect()
mBottomRect = Rect()
mRightRect = Rect()
mHeadRect = Rect()
mElectRect = Rect()
updatePaint()
}
override fun onBoundsChange(bounds: Rect) {
val width = bounds.width()
val height = bounds.height()
val strokeWidth = (sqrt((width * width + height * height).toDouble()) * 0.06f).toInt()
val turn1 = width * 6 / 7
val turn2 = height / 3
val secBottom = height - strokeWidth
mStart = strokeWidth
mStop = turn1 - strokeWidth
mTopRect[0, 0, turn1] = strokeWidth
mBottomRect[0, secBottom, turn1] = height
mRightRect[turn1 - strokeWidth, strokeWidth, turn1] = secBottom
mHeadRect[turn1, turn2, width] = height - turn2
mElectRect[0, strokeWidth, mStop] = secBottom
}
/**
* How to draw:
* |------------------------------|
* |\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\|
* |------------------------------|---|
* |/////////////////| |//|\\\|
* |/////////////////| |//|\\\|
* |------------------------------|---|
* |\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\|
* |------------------------------|
*/
override fun draw(canvas: Canvas) {
if (mElect == -1) {
return
}
mElectRect.right = MathUtils.lerp(mStart, mStop, mElect / 100.0f)
canvas.drawRect(mTopRect, mPaint)
canvas.drawRect(mBottomRect, mPaint)
canvas.drawRect(mRightRect, mPaint)
canvas.drawRect(mHeadRect, mPaint)
canvas.drawRect(mElectRect, mPaint)
}
private val isWarn: Boolean
get() = mElect <= WARN_LIMIT
fun setColor(color: Int) {
if (mColor == color) {
return
}
mColor = color
if (!isWarn) {
mPaint.setColor(mColor)
invalidateSelf()
}
}
fun setWarningColor(color: Int) {
if (mWarningColor == color) {
return
}
mWarningColor = color
if (isWarn) {
mPaint.setColor(mWarningColor)
invalidateSelf()
}
}
fun setElect(elect: Int) {
if (mElect == elect) {
return
}
mElect = elect
updatePaint()
}
fun setElect(elect: Int, warn: Boolean) {
if (mElect == elect) {
return
}
mElect = elect
updatePaint(warn)
}
private fun updatePaint(warn: Boolean = isWarn) {
if (warn) {
mPaint.setColor(mWarningColor)
} else {
mPaint.setColor(mColor)
}
invalidateSelf()
}
override fun setAlpha(alpha: Int) {
mPaint.setAlpha(alpha)
}
override fun setColorFilter(cf: ColorFilter?) {
mPaint.setColorFilter(cf)
}
@Deprecated(
"Deprecated in Java",
ReplaceWith("PixelFormat.TRANSLUCENT", "android.graphics.PixelFormat"),
)
override fun getOpacity(): Int = PixelFormat.TRANSLUCENT
companion object {
const val WARN_LIMIT = 15
}
}
================================================
FILE: app/src/main/java/com/hippo/drawable/DrawerArrowDrawable.kt
================================================
/*
* Copyright (C) 2015 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.drawable
import android.animation.ObjectAnimator
import android.content.Context
import android.graphics.Canvas
import android.graphics.ColorFilter
import android.graphics.Paint
import android.graphics.Path
import android.graphics.PixelFormat
import android.graphics.drawable.Drawable
import androidx.annotation.ColorInt
import androidx.annotation.Keep
import androidx.core.graphics.withTranslation
import com.hippo.ehviewer.R
import com.hippo.yorozuya.MathUtils
import kotlin.math.cos
import kotlin.math.roundToInt
import kotlin.math.sin
/**
* A drawable that can draw a "Drawer hamburger" menu or an Arrow and animate between them.
* @param context used to get the configuration for the drawable from
*/
class DrawerArrowDrawable(context: Context, color: Int) : Drawable() {
private val mPaint = Paint()
private val mBarThickness: Float
// The length of top and bottom bars when they merge into an arrow
private val mTopBottomArrowSize: Float
// The length of middle bar
private val mBarSize: Float
// The length of the middle bar when arrow is shaped
private val mMiddleArrowSize: Float
// The space between bars when they are parallel
private val mBarGap: Float
// Whether bars should spin or not during progress
private val mSpin: Boolean
// Use Path instead of canvas operations so that if color has transparency, overlapping sections
// wont look different
private val mPath = Path()
// The reported intrinsic size of the drawable.
private val mSize: Int
// the amount that overlaps w/ bar size when rotation is max
private val mMaxCutForBarSize: Float
// Whether we should mirror animation when animation is reversed.
private var mVerticalMirror = false
// The interpolated version of the original progress
private var mProgress = 0f
init {
val resources = context.resources
mPaint.isAntiAlias = true
mPaint.setColor(color)
mSize = resources.getDimensionPixelSize(R.dimen.dad_drawable_size)
// round this because having this floating may cause bad measurements
mBarSize = resources.getDimension(R.dimen.dad_bar_size).roundToInt().toFloat()
// round this because having this floating may cause bad measurements
mTopBottomArrowSize =
resources.getDimension(R.dimen.dad_top_bottom_bar_arrow_size).roundToInt().toFloat()
mBarThickness = resources.getDimension(R.dimen.dad_thickness)
// round this because having this floating may cause bad measurements
mBarGap = resources.getDimension(R.dimen.dad_gap_between_bars).roundToInt().toFloat()
mSpin = resources.getBoolean(R.bool.dad_spin_bars)
mMiddleArrowSize = resources.getDimension(R.dimen.dad_middle_bar_arrow_size)
mPaint.style = Paint.Style.STROKE
mPaint.strokeJoin = Paint.Join.MITER
mPaint.strokeCap = Paint.Cap.BUTT
mPaint.strokeWidth = mBarThickness
mMaxCutForBarSize = (mBarThickness / 2 * cos(ARROW_HEAD_ANGLE.toDouble())).toFloat()
}
/**
* If set, canvas is flipped when progress reached to end and going back to start.
*/
private fun setVerticalMirror(verticalMirror: Boolean) {
mVerticalMirror = verticalMirror
}
override fun draw(canvas: Canvas) {
val bounds = getBounds()
// Interpolated widths of arrow bars
val arrowSize = MathUtils.lerp(mBarSize, mTopBottomArrowSize, mProgress)
val middleBarSize = MathUtils.lerp(mBarSize, mMiddleArrowSize, mProgress)
// Interpolated size of middle bar
val middleBarCut = MathUtils.lerp(0f, mMaxCutForBarSize, mProgress).roundToInt().toFloat()
// The rotation of the top and bottom bars (that make the arrow head)
val rotation = MathUtils.lerp(0f, ARROW_HEAD_ANGLE, mProgress)
// The whole canvas rotates as the transition happens
val canvasRotate = MathUtils.lerp(-180, 0, mProgress).toFloat()
val arrowWidth = (arrowSize * cos(rotation.toDouble())).roundToInt().toFloat()
val arrowHeight = (arrowSize * sin(rotation.toDouble())).roundToInt().toFloat()
mPath.rewind()
val topBottomBarOffset = MathUtils.lerp(
mBarGap + mBarThickness,
-mMaxCutForBarSize,
mProgress,
)
val arrowEdge = -middleBarSize / 2
// draw middle bar
mPath.moveTo(arrowEdge + middleBarCut, 0f)
mPath.rLineTo(middleBarSize - middleBarCut * 2, 0f)
// bottom bar
mPath.moveTo(arrowEdge, topBottomBarOffset)
mPath.rLineTo(arrowWidth, arrowHeight)
// top bar
mPath.moveTo(arrowEdge, -topBottomBarOffset)
mPath.rLineTo(arrowWidth, -arrowHeight)
mPath.close()
canvas.withTranslation(bounds.centerX().toFloat(), bounds.centerY().toFloat()) {
// Rotate the whole canvas if spinning, if not, rotate it 180 to get
// the arrow pointing the other way for RTL.
if (mSpin) {
rotate(canvasRotate * if (mVerticalMirror) -1 else 1)
}
drawPath(mPath, mPaint)
}
}
fun setColor(@ColorInt color: Int) {
mPaint.setColor(color)
invalidateSelf()
}
override fun setAlpha(i: Int) {
mPaint.setAlpha(i)
}
override fun setColorFilter(colorFilter: ColorFilter?) {
mPaint.setColorFilter(colorFilter)
}
override fun getIntrinsicHeight(): Int = mSize
override fun getIntrinsicWidth(): Int = mSize
@Deprecated(
"Deprecated in Java",
ReplaceWith("PixelFormat.TRANSLUCENT", "android.graphics.PixelFormat"),
)
override fun getOpacity(): Int = PixelFormat.TRANSLUCENT
@get:Keep
@set:Keep
var progress: Float
get() = mProgress
set(progress) {
if (progress == 1f) {
setVerticalMirror(true)
} else if (progress == 0f) {
setVerticalMirror(false)
}
mProgress = progress
invalidateSelf()
}
fun setMenu(duration: Long) {
setShape(false, duration)
}
fun setArrow(duration: Long) {
setShape(true, duration)
}
private fun setShape(arrow: Boolean, duration: Long) {
if (!(!arrow && mProgress == 0f || arrow && mProgress == 1f)) {
val endProgress = if (arrow) 1f else 0f
if (duration <= 0) {
progress = endProgress
} else {
val oa = ObjectAnimator.ofFloat(this, "progress", endProgress)
oa.setDuration(duration)
oa.setAutoCancel(true)
oa.start()
}
}
}
companion object {
// The angle in degrees that the arrow head is inclined at.
private val ARROW_HEAD_ANGLE = Math.toRadians(45.0).toFloat()
}
}
================================================
FILE: app/src/main/java/com/hippo/drawable/PreciselyClipDrawable.kt
================================================
/*
* 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 = (super.intrinsicWidth * mScale.width()).toInt()
override fun getIntrinsicHeight(): Int = (super.intrinsicHeight * mScale.height()).toInt()
override fun draw(canvas: Canvas) {
canvas.withClip(bounds) { super.draw(canvas) }
}
}
================================================
FILE: app/src/main/java/com/hippo/drawable/TriangleDrawable.kt
================================================
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.drawable
import android.graphics.Canvas
import android.graphics.ColorFilter
import android.graphics.Paint
import android.graphics.Path
import android.graphics.PixelFormat
import android.graphics.Rect
import android.graphics.drawable.Drawable
class TriangleDrawable(color: Int) : Drawable() {
private val mPaint: Paint = Paint(Paint.ANTI_ALIAS_FLAG or Paint.DITHER_FLAG)
private val mPath: Path
init {
mPaint.setColor(color)
mPath = Path()
}
fun setColor(color: Int) {
mPaint.setColor(color)
invalidateSelf()
}
override fun onBoundsChange(bounds: Rect) {
super.onBoundsChange(bounds)
mPath.reset()
mPath.moveTo(bounds.left.toFloat(), bounds.top.toFloat())
mPath.lineTo(bounds.right.toFloat(), bounds.top.toFloat())
mPath.lineTo(bounds.right.toFloat(), bounds.bottom.toFloat())
mPath.close()
}
override fun draw(canvas: Canvas) {
canvas.drawPath(mPath, mPaint)
}
override fun setAlpha(alpha: Int) {
mPaint.setAlpha(alpha)
}
override fun setColorFilter(colorFilter: ColorFilter?) {
mPaint.setColorFilter(colorFilter)
}
@Deprecated(
"Deprecated in Java",
ReplaceWith("PixelFormat.OPAQUE", "android.graphics.PixelFormat"),
)
override fun getOpacity(): Int = PixelFormat.OPAQUE
}
================================================
FILE: app/src/main/java/com/hippo/drawable/UnikeryDrawable.kt
================================================
/*
* Copyright 2015 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.drawable
import android.graphics.drawable.Animatable
import android.graphics.drawable.Drawable
import coil3.Image
import coil3.asDrawable
import coil3.imageLoader
import coil3.request.Disposable
import coil3.request.ImageRequest
import com.hippo.widget.ObservedTextView
class UnikeryDrawable(private val mTextView: ObservedTextView) :
WrapDrawable(),
ObservedTextView.OnWindowAttachListener {
private var mUrl: String? = null
private var task: Disposable? = null
init {
mTextView.setOnWindowAttachListener(this)
}
override fun onAttachedToWindow() {
load(mUrl)
}
override fun onDetachedFromWindow() {
if (task != null && !task!!.isDisposed) task!!.dispose()
clearDrawable()
}
fun load(url: String?) {
if (url != null) {
mUrl = url
val request =
ImageRequest.Builder(mTextView.context).data(url)
.memoryCacheKey(url)
.diskCacheKey(url)
.target(onSuccess = ::onGetValue)
.build()
task = mTextView.context.imageLoader.enqueue(request)
}
}
private fun clearDrawable() {
drawable = null
}
override var drawable: Drawable?
get() = super.drawable
set(newDrawable) {
// Remove old callback
val oldDrawable = drawable
oldDrawable?.callback = null
super.drawable = newDrawable
newDrawable?.callback = mTextView
updateBounds()
if (newDrawable != null) {
invalidateSelf()
}
}
override fun invalidateSelf() {
val cs = mTextView.getText()
mTextView.text = cs
}
private fun onGetValue(image: Image) {
clearDrawable()
drawable = image.asDrawable(mTextView.resources)
(drawable as? Animatable)?.start()
}
}
================================================
FILE: app/src/main/java/com/hippo/drawable/WrapDrawable.kt
================================================
/*
* Copyright 2015 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.drawable
import android.graphics.Canvas
import android.graphics.ColorFilter
import android.graphics.PixelFormat
import android.graphics.Rect
import android.graphics.drawable.Drawable
open class WrapDrawable : Drawable() {
open var drawable: Drawable? = null
fun updateBounds() {
setBounds(0, 0, intrinsicWidth, intrinsicHeight)
}
override fun draw(canvas: Canvas) {
drawable?.draw(canvas)
}
override fun setBounds(left: Int, top: Int, right: Int, bottom: Int) {
super.setBounds(left, top, right, bottom)
drawable?.setBounds(left, top, right, bottom)
}
override fun setBounds(bounds: Rect) {
super.bounds = bounds
drawable?.bounds = bounds
}
override fun getChangingConfigurations(): Int = drawable?.changingConfigurations ?: super.changingConfigurations
override fun setChangingConfigurations(configs: Int) {
super.changingConfigurations = configs
drawable?.changingConfigurations = configs
}
override fun setFilterBitmap(filter: Boolean) {
super.setFilterBitmap(filter)
drawable?.setFilterBitmap(filter)
}
override fun setAlpha(alpha: Int) {
drawable?.alpha = alpha
}
override fun setColorFilter(cf: ColorFilter?) {
drawable?.colorFilter = cf
}
@Deprecated(
"Deprecated in Java",
ReplaceWith("PixelFormat.TRANSLUCENT", "android.graphics.PixelFormat"),
)
override fun getOpacity(): Int = PixelFormat.TRANSLUCENT
override fun getIntrinsicWidth(): Int = drawable?.intrinsicWidth ?: super.intrinsicWidth
override fun getIntrinsicHeight(): Int = drawable?.intrinsicHeight ?: super.intrinsicHeight
}
================================================
FILE: app/src/main/java/com/hippo/easyrecyclerview/EasyRecyclerView.kt
================================================
/*
* Copyright (C) 2015 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.easyrecyclerview
import android.content.Context
import android.os.Parcel
import android.os.Parcelable
import android.util.AttributeSet
import android.util.SparseBooleanArray
import android.view.ActionMode
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.widget.Checkable
import androidx.collection.LongSparseArray
import androidx.core.util.isEmpty
import androidx.core.util.size
import androidx.recyclerview.widget.RecyclerView
import com.hippo.util.readParcelableCompat
import com.hippo.yorozuya.NumberUtils
/**
* Add setChoiceMode for RecyclerView
*/
// Get some code from twoway-view and AbsListView.
open class EasyRecyclerView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0,
) : RecyclerView(
context,
attrs,
defStyle,
) {
/**
* Wrapper for the multiple choice mode callback; AbsListView needs to perform
* a few extra actions around what application code does.
*/
private var mMultiChoiceModeCallback: MultiChoiceModeWrapper? = null
/**
* Controls if/how the user may choose/check items in the list
*/
private var mChoiceMode = CHOICE_MODE_NONE
/**
* Controls CHOICE_MODE_MULTIPLE_MODAL. null when inactive.
*/
private var mChoiceActionMode: ActionMode? = null
/**
* Listener for custom multiple choices
*/
private var mCustomChoiceListener: CustomChoiceListener? = null
var isInCustomChoice = false
private set
/**
* A lock, avoid OutOfCustomChoiceMode when doing OutOfCustomChoiceMode
*/
private var mOutOfCustomChoiceModing = false
private var mTempCheckStates: SparseBooleanArray? = null
/**
* Running count of how many items are currently checked
*/
var checkedItemCount = 0
private set
/**
* Running state of which positions are currently checked
*/
private var mCheckStates: SparseBooleanArray? = null
/**
* Running state of which IDs are currently checked.
* If there is a value for a given key, the checked state for that ID is true
* and the value holds the last known position in the adapter for that id.
*/
private var mCheckedIdStates: LongSparseArray? = null
private var mAdapter: Adapter<*>? = null
/**
* {@inheritDoc}
*/
override fun setAdapter(adapter: Adapter<*>?) {
super.setAdapter(adapter)
mAdapter = adapter
if (adapter != null &&
adapter.hasStableIds() &&
mChoiceMode != CHOICE_MODE_NONE &&
mCheckedIdStates == null
) {
mCheckedIdStates = LongSparseArray()
}
mCheckStates?.clear()
mCheckedIdStates?.clear()
}
override fun onChildAttachedToWindow(child: View) {
super.onChildAttachedToWindow(child)
if (mCheckStates != null) {
val position = getChildAdapterPosition(child)
if (position >= 0) {
setViewChecked(child, mCheckStates!![position])
}
}
}
/**
* Returns the checked state of the specified position. The result is only
* valid if the choice mode has been set to [.CHOICE_MODE_SINGLE]
* or [.CHOICE_MODE_MULTIPLE].
*
* @param position The item whose checked state to return
* @return The item's checked state or `false` if choice mode
* is invalid
* @see .setChoiceMode
*/
private fun isItemChecked(position: Int): Boolean = if (mChoiceMode != CHOICE_MODE_NONE && mCheckStates != null) {
mCheckStates!![position]
} else {
false
}
/**
* Returns the set of checked items in the list. The result is only valid if
* the choice mode has not been set to [.CHOICE_MODE_NONE].
*
* @return A SparseBooleanArray which will return true for each call to
* get(int position) where position is a checked position in the
* list and false otherwise, or `null` if the choice
* mode is set to [.CHOICE_MODE_NONE].
*/
val checkedItemPositions: SparseBooleanArray?
get() = if (mChoiceMode != CHOICE_MODE_NONE) {
mCheckStates
} else {
null
}
fun intoCustomChoiceMode() {
if (mChoiceMode == CHOICE_MODE_MULTIPLE_CUSTOM && !isInCustomChoice) {
isInCustomChoice = true
mCustomChoiceListener!!.onIntoCustomChoice(this)
}
}
fun outOfCustomChoiceMode() {
if (mChoiceMode == CHOICE_MODE_MULTIPLE_CUSTOM && isInCustomChoice && !mOutOfCustomChoiceModing) {
mOutOfCustomChoiceModing = true
// Copy mCheckStates
mTempCheckStates!!.clear()
run {
for (i in 0 until mCheckStates!!.size) {
mTempCheckStates!!.put(mCheckStates!!.keyAt(i), mCheckStates!!.valueAt(i))
}
}
// Uncheck remain checked items
for (i in 0 until mTempCheckStates!!.size) {
if (mTempCheckStates!!.valueAt(i)) {
setItemChecked(mTempCheckStates!!.keyAt(i), false)
}
}
isInCustomChoice = false
mCustomChoiceListener!!.onOutOfCustomChoice(this)
mOutOfCustomChoiceModing = false
}
}
/**
* Clear any choices previously set
*/
private fun clearChoices() {
mCheckStates?.clear()
mCheckedIdStates?.clear()
checkedItemCount = 0
updateOnScreenCheckedViews()
}
/**
* Sets all items checked.
*/
fun checkAll() {
if (mChoiceMode == CHOICE_MODE_NONE || mChoiceMode == CHOICE_MODE_SINGLE) {
return
}
// Check is intoCheckMode
check(!(mChoiceMode == CHOICE_MODE_MULTIPLE_CUSTOM && !isInCustomChoice)) { "Call intoCheckMode first" }
// Start selection mode if needed. We don't need to if we're unchecking something.
if (mChoiceMode == CHOICE_MODE_MULTIPLE_MODAL && mChoiceActionMode == null) {
check(
!(
mMultiChoiceModeCallback == null ||
!mMultiChoiceModeCallback!!.hasWrappedCallback()
),
) {
"EasyRecyclerView: attempted to start selection mode " +
"for CHOICE_MODE_MULTIPLE_MODAL but no choice mode callback was " +
"supplied. Call setMultiChoiceModeListener to set a callback."
}
mChoiceActionMode = startActionMode(mMultiChoiceModeCallback)
}
for (i in 0 until mAdapter!!.itemCount) {
val oldValue = mCheckStates!![i]
mCheckStates!!.put(i, true)
if (mCheckedIdStates != null && mAdapter!!.hasStableIds()) {
mCheckedIdStates!!.put(mAdapter!!.getItemId(i), i)
}
if (!oldValue) {
checkedItemCount++
}
if (mChoiceActionMode != null) {
val id = mAdapter!!.getItemId(i)
mMultiChoiceModeCallback!!.onItemCheckedStateChanged(
mChoiceActionMode!!,
i,
id,
true,
)
}
if (mChoiceMode == CHOICE_MODE_MULTIPLE_CUSTOM) {
val id = mAdapter!!.getItemId(i)
mCustomChoiceListener!!.onItemCheckedStateChanged(this, i, id, true)
}
}
updateOnScreenCheckedViews()
}
fun toggleItemChecked(position: Int) {
if (mCheckStates != null) {
setItemChecked(position, !mCheckStates!![position])
}
}
/**
* Sets the checked state of the specified position. The is only valid if
* the choice mode has been set to [.CHOICE_MODE_SINGLE] or
* [.CHOICE_MODE_MULTIPLE].
*
* @param position The item whose checked state is to be checked
* @param value The new checked state for the item
*/
private fun setItemChecked(position: Int, value: Boolean) {
if (mChoiceMode == CHOICE_MODE_NONE) {
return
}
// Check is intoCheckMode
check(!(mChoiceMode == CHOICE_MODE_MULTIPLE_CUSTOM && !isInCustomChoice)) { "Call intoCheckMode first" }
// Start selection mode if needed. We don't need to if we're unchecking something.
if (value && mChoiceMode == CHOICE_MODE_MULTIPLE_MODAL && mChoiceActionMode == null) {
check(
!(
mMultiChoiceModeCallback == null ||
!mMultiChoiceModeCallback!!.hasWrappedCallback()
),
) {
"EasyRecyclerView: attempted to start selection mode " +
"for CHOICE_MODE_MULTIPLE_MODAL but no choice mode callback was " +
"supplied. Call setMultiChoiceModeListener to set a callback."
}
mChoiceActionMode = startActionMode(mMultiChoiceModeCallback)
}
if (mChoiceMode == CHOICE_MODE_MULTIPLE || mChoiceMode == CHOICE_MODE_MULTIPLE_MODAL || mChoiceMode == CHOICE_MODE_MULTIPLE_CUSTOM) {
val oldValue = mCheckStates!![position]
mCheckStates!!.put(position, value)
if (mCheckedIdStates != null && mAdapter!!.hasStableIds()) {
if (value) {
mCheckedIdStates!!.put(mAdapter!!.getItemId(position), position)
} else {
mCheckedIdStates!!.remove(mAdapter!!.getItemId(position))
}
}
if (oldValue != value) {
if (value) {
checkedItemCount++
} else {
checkedItemCount--
}
}
if (mChoiceActionMode != null) {
val id = mAdapter!!.getItemId(position)
mMultiChoiceModeCallback!!.onItemCheckedStateChanged(
mChoiceActionMode!!,
position,
id,
value,
)
}
if (mChoiceMode == CHOICE_MODE_MULTIPLE_CUSTOM) {
val id = mAdapter!!.getItemId(position)
mCustomChoiceListener!!.onItemCheckedStateChanged(this, position, id, value)
}
} else {
val updateIds = mCheckedIdStates != null && mAdapter!!.hasStableIds()
// Clear all values if we're checking something, or unchecking the currently
// selected item
if (value || isItemChecked(position)) {
mCheckStates!!.clear()
if (updateIds) {
mCheckedIdStates!!.clear()
}
}
// this may end up selecting the value we just cleared but this way
// we ensure length of mCheckStates is 1, a fact getCheckedItemPosition relies on
if (value) {
mCheckStates!!.put(position, true)
if (updateIds) {
mCheckedIdStates!!.put(mAdapter!!.getItemId(position), position)
}
checkedItemCount = 1
} else if (mCheckStates!!.isEmpty() || !mCheckStates!!.valueAt(0)) {
checkedItemCount = 0
}
}
updateOnScreenCheckedViews()
}
/**
* Defines the choice behavior for the List. By default, Lists do not have any choice behavior
* ([.CHOICE_MODE_NONE]). By setting the choiceMode to [.CHOICE_MODE_SINGLE], the
* List allows up to one item to be in a chosen state. By setting the choiceMode to
* [.CHOICE_MODE_MULTIPLE], the list allows any number of items to be chosen.
*
* @param choiceMode One of [.CHOICE_MODE_NONE], [.CHOICE_MODE_SINGLE], or
* [.CHOICE_MODE_MULTIPLE]
*/
fun setChoiceMode(choiceMode: Int) {
mChoiceMode = choiceMode
if (mChoiceActionMode != null) {
mChoiceActionMode!!.finish()
mChoiceActionMode = null
}
if (mChoiceMode != CHOICE_MODE_NONE) {
if (mCheckStates == null) {
mCheckStates = SparseBooleanArray(0)
}
if (mCheckedIdStates == null && mAdapter != null && mAdapter!!.hasStableIds()) {
mCheckedIdStates = LongSparseArray(0)
}
// Modal multi-choice mode only has choices when the mode is active. Clear them.
if (mChoiceMode == CHOICE_MODE_MULTIPLE_MODAL) {
clearChoices()
isLongClickable = true
} else if (mChoiceMode == CHOICE_MODE_MULTIPLE_CUSTOM) {
if (mTempCheckStates == null) {
mTempCheckStates = SparseBooleanArray(0)
}
clearChoices()
}
}
}
fun setCustomCheckedListener(listener: CustomChoiceListener?) {
mCustomChoiceListener = listener
}
/**
* Perform a quick, in-place update of the checked or activated state
* on all visible item views. This should only be called when a valid
* choice mode is active.
*/
private fun updateOnScreenCheckedViews() {
for (i in 0 until childCount) {
val child = getChildAt(i)
val position = getChildAdapterPosition(child)
setViewChecked(child, mCheckStates!![position])
}
}
public override fun onSaveInstanceState(): Parcelable {
val ss = SavedState(super.onSaveInstanceState())
ss.choiceMode = mChoiceMode
ss.customChoice = isInCustomChoice
ss.checkedItemCount = checkedItemCount
ss.checkState = mCheckStates
ss.checkIdState = mCheckedIdStates
return ss
}
public override fun onRestoreInstanceState(state: Parcelable) {
val ss = state as SavedState
super.onRestoreInstanceState(ss.superState)
setChoiceMode(ss.choiceMode)
isInCustomChoice = ss.customChoice
checkedItemCount = ss.checkedItemCount
if (ss.checkState != null) {
mCheckStates = ss.checkState
}
if (ss.checkIdState != null) {
mCheckedIdStates = ss.checkIdState
}
if (mChoiceMode == CHOICE_MODE_MULTIPLE_MODAL && checkedItemCount > 0) {
mChoiceActionMode = startActionMode(mMultiChoiceModeCallback)
}
updateOnScreenCheckedViews()
}
/**
* A MultiChoiceModeListener receives events for [android.widget.AbsListView.CHOICE_MODE_MULTIPLE_MODAL].
* It acts as the [android.view.ActionMode.Callback] for the selection mode and also receives
* [.onItemCheckedStateChanged] events when the user
* selects and deselects list items.
*/
interface MultiChoiceModeListener : ActionMode.Callback {
/**
* Called when an item is checked or unchecked during selection mode.
*
* @param mode The [android.view.ActionMode] providing the selection mode
* @param position Adapter position of the item that was checked or unchecked
* @param id Adapter ID of the item that was checked or unchecked
* @param checked `true` if the item is now checked, `false`
* if the item is now unchecked.
*/
fun onItemCheckedStateChanged(
mode: ActionMode,
position: Int,
id: Long,
checked: Boolean,
)
}
/**
* Custom checked
*/
interface CustomChoiceListener {
fun onIntoCustomChoice(view: EasyRecyclerView)
fun onOutOfCustomChoice(view: EasyRecyclerView)
fun onItemCheckedStateChanged(
view: EasyRecyclerView,
position: Int,
id: Long,
checked: Boolean,
)
}
/**
* This saved state class is a Parcelable and should not extend
* [android.view.View.BaseSavedState] nor [android.view.AbsSavedState]
* because its super class AbsSavedState's constructor
* currently passes null
* as a class loader to read its superstate from Parcelable.
* This causes [android.os.BadParcelableException] when restoring saved states.
*
*
* The super class "RecyclerView" is a part of the support library,
* and restoring its saved state requires the class loader that loaded the RecyclerView.
* It seems that the class loader is not required when restoring from RecyclerView itself,
* but it is required when restoring from RecyclerView's subclasses.
*/
internal open class SavedState : Parcelable {
var choiceMode = 0
var customChoice = false
var checkedItemCount = 0
var checkState: SparseBooleanArray? = null
var checkIdState: LongSparseArray? = null
// This keeps the parent(RecyclerView)'s state
var superState: Parcelable?
constructor() {
superState = null
}
/**
* Constructor called from [.onSaveInstanceState]
*/
constructor(superState: Parcelable?) {
this.superState = if (superState !== EMPTY_STATE) superState else null
}
/**
* Constructor called from [.CREATOR]
*/
private constructor(`in`: Parcel) {
// Parcel 'in' has its parent(RecyclerView)'s saved state.
// To restore it, class loader that loaded RecyclerView is required.
val superState =
`in`.readParcelableCompat(RecyclerView::class.java.getClassLoader())
this.superState = superState ?: EMPTY_STATE
choiceMode = `in`.readInt()
customChoice = NumberUtils.int2boolean(`in`.readInt())
checkedItemCount = `in`.readInt()
checkState = `in`.readSparseBooleanArray()
val n = `in`.readInt()
if (n > 0) {
checkIdState = LongSparseArray()
(0 until n).forEach { i ->
val key = `in`.readLong()
val value = `in`.readInt()
checkIdState!!.put(key, value)
}
}
}
override fun describeContents(): Int = 0
override fun writeToParcel(out: Parcel, flags: Int) {
out.writeParcelable(superState, flags)
out.writeInt(choiceMode)
out.writeInt(NumberUtils.boolean2int(customChoice))
out.writeInt(checkedItemCount)
out.writeSparseBooleanArray(checkState)
val n = if (checkIdState != null) checkIdState!!.size() else 0
out.writeInt(n)
for (i in 0 until n) {
out.writeLong(checkIdState!!.keyAt(i))
out.writeInt(checkIdState!!.valueAt(i))
}
}
companion object CREATOR : Parcelable.Creator {
override fun createFromParcel(`in`: Parcel): SavedState = SavedState(`in`)
override fun newArray(size: Int): Array = arrayOfNulls(size)
}
}
inner class MultiChoiceModeWrapper : MultiChoiceModeListener {
private val mWrapped: MultiChoiceModeListener? = null
fun hasWrappedCallback(): Boolean = mWrapped != null
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
if (mWrapped!!.onCreateActionMode(mode, menu)) {
// Initialize checked graphic state?
isLongClickable = false
return true
}
return false
}
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean = mWrapped!!.onPrepareActionMode(mode, menu)
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean = mWrapped!!.onActionItemClicked(mode, item)
override fun onDestroyActionMode(mode: ActionMode) {
mWrapped!!.onDestroyActionMode(mode)
mChoiceActionMode = null
// Ending selection mode means deselecting everything.
clearChoices()
requestLayout()
isLongClickable = true
}
override fun onItemCheckedStateChanged(
mode: ActionMode,
position: Int,
id: Long,
checked: Boolean,
) {
mWrapped!!.onItemCheckedStateChanged(mode, position, id, checked)
// If there are no items selected we no longer need the selection mode.
if (checkedItemCount == 0) {
mode.finish()
}
}
}
companion object {
/**
* Normal list that does not indicate choices
*/
const val CHOICE_MODE_NONE = 0
/**
* The list allows up to one choice
*/
const val CHOICE_MODE_SINGLE = 1
/**
* The list allows multiple choices
*/
const val CHOICE_MODE_MULTIPLE = 2
/**
* The list allows multiple choices in a modal selection mode
*/
const val CHOICE_MODE_MULTIPLE_MODAL = 3
/**
* The list allows multiple choices in custom action
*/
const val CHOICE_MODE_MULTIPLE_CUSTOM = 4
private val EMPTY_STATE: SavedState = object : SavedState() {}
private fun setViewChecked(view: View, checked: Boolean) {
if (view is Checkable) {
(view as Checkable).isChecked = checked
} else {
view.isActivated = checked
}
}
}
}
================================================
FILE: app/src/main/java/com/hippo/easyrecyclerview/FastScroller.kt
================================================
/*
* Copyright 2015 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.easyrecyclerview
import android.animation.Animator
import android.animation.ObjectAnimator
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Canvas
import android.graphics.drawable.Drawable
import android.os.Handler
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import android.view.ViewConfiguration
import androidx.core.content.withStyledAttributes
import androidx.core.graphics.withTranslation
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver
import com.hippo.ehviewer.R
import com.hippo.yorozuya.AnimationUtils
import com.hippo.yorozuya.LayoutUtils
import com.hippo.yorozuya.MathUtils
import com.hippo.yorozuya.SimpleAnimatorListener
import com.hippo.yorozuya.SimpleHandler
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min
class FastScroller @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
) : View(
context,
attrs,
defStyleAttr,
) {
private var mSimpleHandler: Handler? = null
private var mDraggable = false
private var mMinHandlerHeight = 0
private var mRecyclerView: RecyclerView? = null
private var mOnScrollChangeListener: RecyclerView.OnScrollListener? = null
private var mAdapter: RecyclerView.Adapter<*>? = null
private var mAdapterDataObserver: AdapterDataObserver? = null
private var mHandler: Drawable? = null
private var mHandlerOffset = INVALID
private var mHandlerHeight = INVALID
private var mDownX = INVALID.toFloat()
private var mDownY = INVALID.toFloat()
private var mLastMotionY = INVALID.toFloat()
private var mDragged = false
private var mCantDrag = false
private var mTouchSlop = 0
private var mListener: OnDragHandlerListener? = null
private var mShowAnimator: ObjectAnimator? = null
private var mHideAnimator: ObjectAnimator? = null
private val mHideRunnable = Runnable { mHideAnimator!!.start() }
init {
init(context, attrs, defStyleAttr)
}
private fun init(context: Context, attrs: AttributeSet?, defStyleAttr: Int) {
mSimpleHandler = SimpleHandler.getInstance()
context.withStyledAttributes(attrs, R.styleable.FastScroller, defStyleAttr, 0) {
mHandler = getDrawable(R.styleable.FastScroller_handler)
mDraggable = getBoolean(R.styleable.FastScroller_draggable, true)
}
setAlpha(0.0f)
visibility = INVISIBLE
mMinHandlerHeight = LayoutUtils.dp2pix(context, MIN_HANDLER_HEIGHT_DP.toFloat())
mTouchSlop = ViewConfiguration.get(context).scaledTouchSlop
mShowAnimator = ObjectAnimator.ofFloat(this, "alpha", 1.0f)
mShowAnimator!!.interpolator = AnimationUtils.FAST_SLOW_INTERPOLATOR
mShowAnimator!!.setDuration(SCROLL_BAR_FADE_DURATION.toLong())
mHideAnimator = ObjectAnimator.ofFloat(this, "alpha", 0.0f)
mHideAnimator!!.interpolator = AnimationUtils.SLOW_FAST_INTERPOLATOR
mHideAnimator!!.setDuration(SCROLL_BAR_FADE_DURATION.toLong())
mHideAnimator!!.addListener(object : SimpleAnimatorListener() {
private var mCancel = false
override fun onAnimationCancel(animation: Animator) {
mCancel = true
}
override fun onAnimationEnd(animation: Animator) {
if (mCancel) {
mCancel = false
} else {
visibility = INVISIBLE
}
}
})
}
fun setOnDragHandlerListener(listener: OnDragHandlerListener?) {
mListener = listener
}
private fun updatePosition(show: Boolean) {
if (mRecyclerView == null) {
return
}
val paddingTop = paddingTop
val paddingBottom = paddingBottom
val height = height - paddingTop - paddingBottom
val offset = mRecyclerView!!.computeVerticalScrollOffset()
val extent = mRecyclerView!!.computeVerticalScrollExtent()
val range = mRecyclerView!!.computeVerticalScrollRange()
if (height <= 0 || extent >= range || extent <= 0) {
return
}
var endOffset = height.toLong() * offset / range
var endHeight = height * extent / range
endHeight = max(endHeight.toDouble(), mMinHandlerHeight.toDouble()).toInt()
endOffset = min(endOffset.toDouble(), (height - endHeight).toDouble()).toLong()
mHandlerOffset = (endOffset + paddingTop).toInt()
mHandlerHeight = endHeight
if (show) {
if (mHideAnimator!!.isRunning) {
mHideAnimator!!.cancel()
mShowAnimator!!.start()
} else if (visibility != VISIBLE && !mShowAnimator!!.isRunning) {
visibility = VISIBLE
mShowAnimator!!.start()
}
val handler = mSimpleHandler
handler!!.removeCallbacks(mHideRunnable)
if (!mDragged) {
handler.postDelayed(mHideRunnable, SCROLL_BAR_DELAY.toLong())
}
}
}
fun setHandlerDrawable(drawable: Drawable?) {
mHandler = drawable
invalidate()
}
var isDraggable: Boolean
get() = mDraggable
set(draggable) {
mDraggable = draggable
if (mDragged) {
mDragged = false
}
mSimpleHandler!!.removeCallbacks(mHideRunnable)
mHideRunnable.run()
}
val isAttached: Boolean
get() = mRecyclerView != null
fun attachToRecyclerView(recyclerView: RecyclerView?) {
if (recyclerView == null) {
return
}
check(mRecyclerView == null) {
"The FastScroller is already attached to a RecyclerView, " +
"call detachedFromRecyclerView first"
}
mRecyclerView = recyclerView
mOnScrollChangeListener = object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
updatePosition(true)
invalidate()
}
}
recyclerView.addOnScrollListener(mOnScrollChangeListener!!)
mAdapter = recyclerView.adapter
if (mAdapter != null) {
mAdapterDataObserver = object : AdapterDataObserver() {
override fun onItemRangeChanged(positionStart: Int, itemCount: Int) {
super.onItemRangeChanged(positionStart, itemCount)
updatePosition(false)
invalidate()
}
override fun onItemRangeChanged(positionStart: Int, itemCount: Int, payload: Any?) {
super.onItemRangeChanged(positionStart, itemCount, payload)
updatePosition(false)
invalidate()
}
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
super.onItemRangeInserted(positionStart, itemCount)
updatePosition(false)
invalidate()
}
override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {
super.onItemRangeRemoved(positionStart, itemCount)
updatePosition(false)
invalidate()
}
override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) {
super.onItemRangeMoved(fromPosition, toPosition, itemCount)
updatePosition(false)
invalidate()
}
}
mAdapter!!.registerAdapterDataObserver(mAdapterDataObserver!!)
}
}
fun detachedFromRecyclerView() {
if (mRecyclerView != null && mOnScrollChangeListener != null) {
mRecyclerView!!.removeOnScrollListener(mOnScrollChangeListener!!)
}
mRecyclerView = null
mOnScrollChangeListener = null
if (mAdapter != null && mAdapterDataObserver != null) {
mAdapter!!.unregisterAdapterDataObserver(mAdapterDataObserver!!)
}
mAdapter = null
mAdapterDataObserver = null
setAlpha(0.0f)
visibility = INVISIBLE
}
override fun onDraw(canvas: Canvas) {
if (mRecyclerView == null || mHandler == null) {
return
}
if (mHandlerHeight == INVALID) {
updatePosition(false)
}
if (mHandlerHeight == INVALID) {
return
}
val paddingLeft = getPaddingLeft()
canvas.withTranslation(paddingLeft.toFloat(), mHandlerOffset.toFloat()) {
mHandler!!.setBounds(0, 0, width - paddingLeft - getPaddingRight(), mHandlerHeight)
mHandler!!.draw(canvas)
}
}
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent): Boolean {
if (!mDraggable || visibility != VISIBLE || mRecyclerView == null || mHandlerHeight == INVALID) {
return false
}
val action = event.action
if (action == MotionEvent.ACTION_DOWN) {
mCantDrag = false
}
if (mCantDrag) {
return false
}
when (action) {
MotionEvent.ACTION_DOWN -> {
mDragged = false
mDownX = event.x
mDownY = event.y
if (mDownY < mHandlerOffset || mDownY > mHandlerOffset + mHandlerHeight) {
mCantDrag = true
return false
}
}
MotionEvent.ACTION_MOVE -> {
if (!mDragged) {
val x = event.x
val y = event.y
// Check touch slop
if (MathUtils.dist(x, y, mDownX, mDownY) < mTouchSlop) {
return true
}
if (abs((x - mDownX).toDouble()) > abs((y - mDownY).toDouble()) || y < mHandlerOffset || y > mHandlerOffset + mHandlerHeight) {
mCantDrag = true
return false
} else {
mDragged = true
mSimpleHandler!!.removeCallbacks(mHideRunnable)
// Update mLastMotionY
mLastMotionY =
if (mDownY < mHandlerOffset || mDownY >= mHandlerOffset + mHandlerHeight) {
// the point out of handler, make the point in handler center
mHandlerOffset + mHandlerHeight.toFloat() / 2
} else {
mDownY
}
// Notify
if (mListener != null) {
mListener!!.onStartDragHandler()
}
}
}
val range = mRecyclerView!!.computeVerticalScrollRange()
if (range <= 0) {
return true
}
val y = event.y
val scroll =
(range * (y - mLastMotionY) / (height - paddingTop - paddingBottom)).toInt()
mRecyclerView!!.scrollBy(0, scroll)
mLastMotionY = y
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
// Notify
if (mDragged && mListener != null) {
mListener!!.onEndDragHandler()
}
mDragged = false
mSimpleHandler!!.postDelayed(mHideRunnable, SCROLL_BAR_DELAY.toLong())
}
}
return true
}
interface OnDragHandlerListener {
fun onStartDragHandler()
fun onEndDragHandler()
}
companion object {
private const val INVALID = -1
private const val SCROLL_BAR_FADE_DURATION = 500
private const val SCROLL_BAR_DELAY = 1500
private const val MIN_HANDLER_HEIGHT_DP = 48
}
}
================================================
FILE: app/src/main/java/com/hippo/easyrecyclerview/HandlerDrawable.kt
================================================
/*
* Copyright 2015 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.easyrecyclerview
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.ColorFilter
import android.graphics.Paint
import android.graphics.PixelFormat
import android.graphics.RectF
import android.graphics.drawable.Drawable
class HandlerDrawable : Drawable() {
private val mPaint: Paint = Paint(Paint.ANTI_ALIAS_FLAG)
private val mTemp = RectF()
private var mColor = Color.BLACK
init {
mPaint.setColor(mColor)
mPaint.style = Paint.Style.FILL
}
override fun draw(canvas: Canvas) {
val width = getBounds().width()
val height = getBounds().height()
if (width > height) {
canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), mPaint)
} else {
mTemp[0f, 0f, width.toFloat()] = width.toFloat()
canvas.drawArc(mTemp, -180f, 180f, true, mPaint)
mTemp[0f, (height - width).toFloat(), width.toFloat()] = height.toFloat()
canvas.drawArc(mTemp, 0f, 180f, true, mPaint)
val halfWidth = width.toFloat() / 2.0f
canvas.drawRect(0f, halfWidth, width.toFloat(), height - halfWidth, mPaint)
}
}
fun setColor(color: Int) {
if (mColor != color) {
mColor = color
mPaint.setColor(color)
}
}
override fun setAlpha(alpha: Int) {
mPaint.setAlpha(alpha)
}
override fun setColorFilter(colorFilter: ColorFilter?) {
mPaint.setColorFilter(colorFilter)
}
@Deprecated("Deprecated in Java")
override fun getOpacity(): Int {
val alpha = Color.alpha(mColor)
return when (alpha) {
0xff -> {
PixelFormat.OPAQUE
}
0x00 -> {
PixelFormat.TRANSPARENT
}
else -> {
PixelFormat.TRANSLUCENT
}
}
}
}
================================================
FILE: app/src/main/java/com/hippo/easyrecyclerview/LayoutManagerUtils.kt
================================================
/*
* Copyright 2015 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.easyrecyclerview
import android.content.Context
import android.graphics.PointF
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.StaggeredGridLayoutManager
import com.hippo.yorozuya.MathUtils
import com.hippo.yorozuya.SimpleHandler
import java.lang.reflect.InvocationTargetException
import java.lang.reflect.Method
import kotlin.math.abs
object LayoutManagerUtils {
private var sCsdfp: Method? = null
init {
try {
sCsdfp = StaggeredGridLayoutManager::class.java.getDeclaredMethod(
"calculateScrollDirectionForPosition",
Int::class.javaPrimitiveType,
)
sCsdfp!!.isAccessible = true
} catch (e: NoSuchMethodException) {
// Ignore
e.printStackTrace()
}
}
fun scrollToPositionWithOffset(
layoutManager: RecyclerView.LayoutManager,
position: Int,
offset: Int,
) {
when (layoutManager) {
is LinearLayoutManager -> {
layoutManager.scrollToPositionWithOffset(position, offset)
}
is StaggeredGridLayoutManager -> {
layoutManager.scrollToPositionWithOffset(position, offset)
}
else -> {
throw IllegalStateException(
"Can't do scrollToPositionWithOffset for " +
layoutManager.javaClass.getName(),
)
}
}
}
fun smoothScrollToPosition(
layoutManager: RecyclerView.LayoutManager,
context: Context?,
position: Int,
millisecondsPerInch: Int = -1,
) {
val smoothScroller: SimpleSmoothScroller
when (layoutManager) {
is LinearLayoutManager -> {
smoothScroller =
object : SimpleSmoothScroller(context!!, millisecondsPerInch.toFloat()) {
override fun computeScrollVectorForPosition(targetPosition: Int): PointF? = layoutManager.computeScrollVectorForPosition(targetPosition)
}
}
is StaggeredGridLayoutManager -> {
smoothScroller =
object : SimpleSmoothScroller(context!!, millisecondsPerInch.toFloat()) {
override fun computeScrollVectorForPosition(targetPosition: Int): PointF? {
var direction = 0
try {
direction = sCsdfp!!.invoke(layoutManager, targetPosition) as Int
} catch (e: IllegalAccessException) {
e.printStackTrace()
} catch (e: InvocationTargetException) {
e.printStackTrace()
}
if (direction == 0) {
return null
}
return if (layoutManager.orientation == StaggeredGridLayoutManager.HORIZONTAL) {
PointF(direction.toFloat(), 0f)
} else {
PointF(0f, direction.toFloat())
}
}
}
}
else -> {
throw IllegalStateException(
"Can't do smoothScrollToPosition for " +
layoutManager.javaClass.getName(),
)
}
}
smoothScroller.targetPosition = position
layoutManager.startSmoothScroll(smoothScroller)
}
fun scrollToPositionProperly(
layoutManager: RecyclerView.LayoutManager,
context: Context?,
position: Int,
listener: OnScrollToPositionListener?,
) {
SimpleHandler.getInstance().postDelayed({
val first = getFirstVisibleItemPosition(layoutManager)
val last = getLastVisibleItemPosition(layoutManager)
val offset = abs((position - first).toDouble()).toInt()
val max = last - first
if (offset < max && max > 0) {
smoothScrollToPosition(
layoutManager,
context,
position,
MathUtils.lerp(100, 25, (offset / max).toFloat()),
)
} else {
scrollToPositionWithOffset(layoutManager, position, 0)
listener?.onScrollToPosition(position)
}
}, 200)
}
fun getFirstVisibleItemPosition(layoutManager: RecyclerView.LayoutManager): Int = when (layoutManager) {
is LinearLayoutManager -> {
layoutManager.findFirstVisibleItemPosition()
}
is StaggeredGridLayoutManager -> {
val positions =
layoutManager.findFirstVisibleItemPositions(null)
MathUtils.min(*positions)
}
else -> {
throw IllegalStateException(
"Can't do getFirstVisibleItemPosition for " +
layoutManager.javaClass.getName(),
)
}
}
fun getLastVisibleItemPosition(layoutManager: RecyclerView.LayoutManager): Int = when (layoutManager) {
is LinearLayoutManager -> {
layoutManager.findLastVisibleItemPosition()
}
is StaggeredGridLayoutManager -> {
val positions =
layoutManager.findLastVisibleItemPositions(null)
MathUtils.max(*positions)
}
else -> {
throw IllegalStateException(
"Can't do getLastVisibleItemPosition for " +
layoutManager.javaClass.getName(),
)
}
}
fun interface OnScrollToPositionListener {
fun onScrollToPosition(position: Int)
}
}
================================================
FILE: app/src/main/java/com/hippo/easyrecyclerview/LinearDividerItemDecoration.kt
================================================
/*
* Copyright (C) 2015 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.easyrecyclerview
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Rect
import android.view.View
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.ItemDecoration
/**
* Only work for [androidx.recyclerview.widget.LinearLayoutManager].
* Show divider between item, just like
* [android.widget.ListView.setDivider]
*/
class LinearDividerItemDecoration(orientation: Int, color: Int, thickness: Int) : ItemDecoration() {
private val mRect: Rect = Rect()
private val mPaint: Paint = Paint()
private var mShowFirstDivider = false
private var mShowLastDivider = false
private var mOrientation = 0
private var mThickness = 0
private var mPaddingStart = 0
private var mPaddingEnd = 0
private var mOverlap = false
private var mShowDividerHelper: ShowDividerHelper? = null
init {
mPaint.style = Paint.Style.FILL
setOrientation(orientation)
setColor(color)
setThickness(thickness)
}
fun setShowDividerHelper(showDividerHelper: ShowDividerHelper?) {
mShowDividerHelper = showDividerHelper
}
fun setOrientation(orientation: Int) {
require(!(orientation != HORIZONTAL && orientation != VERTICAL)) { "invalid orientation" }
mOrientation = orientation
}
fun setColor(color: Int) {
mPaint.setColor(color)
}
fun setThickness(thickness: Int) {
mThickness = thickness
}
fun setShowFirstDivider(showFirstDivider: Boolean) {
mShowFirstDivider = showFirstDivider
}
fun setShowLastDivider(showLastDivider: Boolean) {
mShowLastDivider = showLastDivider
}
fun setPadding(padding: Int) {
setPaddingStart(padding)
setPaddingEnd(padding)
}
fun setPaddingStart(paddingStart: Int) {
mPaddingStart = paddingStart
}
fun setPaddingEnd(paddingEnd: Int) {
mPaddingEnd = paddingEnd
}
fun setOverlap(overlap: Boolean) {
mOverlap = overlap
}
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State,
) {
if (parent.adapter == null) {
outRect[0, 0, 0] = 0
return
}
if (mOverlap) {
outRect[0, 0, 0] = 0
return
}
val position = parent.getChildLayoutPosition(view)
val itemCount = parent.adapter!!.itemCount
if (mShowDividerHelper != null) {
if (mOrientation == VERTICAL) {
if (position == 0 && mShowDividerHelper!!.showDivider(0)) {
outRect.top = mThickness
}
if (mShowDividerHelper!!.showDivider(position + 1)) {
outRect.bottom = mThickness
}
} else {
if (position == 0 && mShowDividerHelper!!.showDivider(0)) {
outRect.left = mThickness
}
if (mShowDividerHelper!!.showDivider(position + 1)) {
outRect.right = mThickness
}
}
} else {
if (mOrientation == VERTICAL) {
if (position == 0 && mShowFirstDivider) {
outRect.top = mThickness
}
outRect.bottom = mThickness
if (position == itemCount - 1 && !mShowLastDivider) {
outRect.bottom = 0
}
} else {
if (position == 0 && mShowFirstDivider) {
outRect.left = mThickness
}
outRect.right = mThickness
if (position == itemCount - 1 && !mShowLastDivider) {
outRect.right = 0
}
}
}
}
override fun onDrawOver(
c: Canvas,
parent: RecyclerView,
state: RecyclerView.State,
) {
val adapter = parent.adapter ?: return
val itemCount = adapter.itemCount
val overlap = mOverlap
if (mOrientation == VERTICAL) {
val isRtl = parent.layoutDirection == View.LAYOUT_DIRECTION_RTL
val mPaddingLeft: Int
val mPaddingRight: Int
if (isRtl) {
mPaddingLeft = mPaddingEnd
mPaddingRight = mPaddingStart
} else {
mPaddingLeft = mPaddingStart
mPaddingRight = mPaddingEnd
}
val left = parent.getPaddingLeft() + mPaddingLeft
val right = parent.width - parent.getPaddingRight() - mPaddingRight
val childCount = parent.childCount
for (i in 0 until childCount) {
val child = parent.getChildAt(i)
val lp = child.layoutParams as RecyclerView.LayoutParams
val position = parent.getChildLayoutPosition(child)
var show: Boolean
show = if (mShowDividerHelper != null) {
mShowDividerHelper!!.showDivider(position + 1)
} else {
position != itemCount - 1 || mShowLastDivider
}
if (show) {
var top = child.bottom + lp.bottomMargin
if (overlap) {
top -= mThickness
}
val bottom = top + mThickness
mRect[left, top, right] = bottom
c.drawRect(mRect, mPaint)
}
if (position == 0) {
show = if (mShowDividerHelper != null) {
mShowDividerHelper!!.showDivider(0)
} else {
mShowFirstDivider
}
if (show) {
var bottom = child.top + lp.topMargin
if (overlap) {
bottom += mThickness
}
val top = bottom - mThickness
mRect[left, top, right] = bottom
c.drawRect(mRect, mPaint)
}
}
}
} else {
val top = parent.paddingTop + mPaddingStart
val bottom = parent.height - parent.paddingBottom - mPaddingEnd
val childCount = parent.childCount
for (i in 0 until childCount) {
val child = parent.getChildAt(i)
val lp = child.layoutParams as RecyclerView.LayoutParams
val position = parent.getChildLayoutPosition(child)
var show: Boolean
show = if (mShowDividerHelper != null) {
mShowDividerHelper!!.showDivider(position + 1)
} else {
position != itemCount - 1 || mShowLastDivider
}
if (show) {
var left = child.right + lp.rightMargin
if (overlap) {
left -= mThickness
}
val right = left + mThickness
mRect[left, top, right] = bottom
c.drawRect(mRect, mPaint)
}
if (position == 0) {
show = if (mShowDividerHelper != null) {
mShowDividerHelper!!.showDivider(0)
} else {
mShowFirstDivider
}
if (show) {
var right = child.left + lp.leftMargin
if (overlap) {
right += mThickness
}
val left = right - mThickness
mRect[left, top, right] = bottom
c.drawRect(mRect, mPaint)
}
}
}
}
}
interface ShowDividerHelper {
fun showDivider(index: Int): Boolean
}
companion object {
const val HORIZONTAL = LinearLayoutManager.HORIZONTAL
const val VERTICAL = LinearLayoutManager.VERTICAL
}
}
================================================
FILE: app/src/main/java/com/hippo/easyrecyclerview/MarginItemDecoration.kt
================================================
/*
* Copyright (C) 2015 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.easyrecyclerview
import android.graphics.Rect
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.ItemDecoration
/**
* @param margin gap between two item
* @param paddingLeft gap between RecyclerView left and left item left
* @param paddingTop gap between RecyclerView top and top item top
* @param paddingRight gap between RecyclerView right and right item right
* @param paddingBottom gap between RecyclerView bottom and bottom item bottom
*/
class MarginItemDecoration(
margin: Int,
paddingLeft: Int,
paddingTop: Int,
paddingRight: Int,
paddingBottom: Int,
) : ItemDecoration() {
private var mMargin = 0
private var mPaddingLeft = 0
private var mPaddingTop = 0
private var mPaddingRight = 0
private var mPaddingBottom = 0
init {
val halfMargin = margin / 2
mMargin = halfMargin
mPaddingLeft = paddingLeft - halfMargin
mPaddingTop = paddingTop - halfMargin
mPaddingRight = paddingRight - halfMargin
mPaddingBottom = paddingBottom - halfMargin
}
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State,
) {
outRect[mMargin, mMargin, mMargin] = mMargin
}
fun applyPaddings(view: View) {
view.setPadding(mPaddingLeft, mPaddingTop, mPaddingRight, mPaddingBottom)
}
}
================================================
FILE: app/src/main/java/com/hippo/easyrecyclerview/SimpleHolder.kt
================================================
/*
* Copyright (C) 2015 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.easyrecyclerview
import android.view.View
import androidx.recyclerview.widget.RecyclerView
class SimpleHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
================================================
FILE: app/src/main/java/com/hippo/easyrecyclerview/SimpleSmoothScroller.kt
================================================
/*
* Copyright (C) 2015 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.easyrecyclerview
import android.content.Context
import androidx.recyclerview.widget.LinearSmoothScroller
import kotlin.math.abs
import kotlin.math.ceil
abstract class SimpleSmoothScroller(context: Context, millisecondsPerInch: Float) : LinearSmoothScroller(context) {
private val mMillisecondsPerPx: Float = millisecondsPerInch / context.resources.displayMetrics.densityDpi
override fun calculateTimeForScrolling(dx: Int): Int = if (mMillisecondsPerPx <= 0) {
super.calculateTimeForScrolling(dx)
} else {
ceil(abs(dx.toDouble()) * mMillisecondsPerPx).toInt()
}
}
================================================
FILE: app/src/main/java/com/hippo/ehviewer/AppConfig.java
================================================
/*
* 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.annotation.SuppressLint;
import android.content.Context;
import android.os.Environment;
import androidx.annotation.Nullable;
import com.hippo.ehviewer.client.exception.ParseException;
import com.hippo.util.ReadableTime;
import com.hippo.yorozuya.FileUtils;
import com.hippo.yorozuya.IOUtils;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
public class AppConfig {
public static final String APP_DIRNAME = "EhViewer";
private static final String DOWNLOAD = "download";
private static final String TEMP = "temp";
private static final String PARSE_ERROR = "parse_error";
private static final String CRASH = "crash";
@SuppressLint("StaticFieldLeak")
private static Context sContext;
public static void initialize(Context context) {
sContext = context.getApplicationContext();
}
@Nullable
public static File getExternalAppDir() {
if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {
File dir = sContext.getExternalFilesDir(null);
return FileUtils.ensureDirectory(dir) ? dir : null;
}
return null;
}
/**
* mkdirs and get
*/
@Nullable
public static File getDirInExternalAppDir(String filename) {
File appFolder = getExternalAppDir();
if (appFolder != null) {
File dir = new File(appFolder, filename);
return FileUtils.ensureDirectory(dir) ? dir : null;
}
return null;
}
@Nullable
public static File getFileInExternalAppDir(String filename) {
File appFolder = getExternalAppDir();
if (appFolder != null) {
File file = new File(appFolder, filename);
return FileUtils.ensureFile(file) ? file : null;
}
return null;
}
@Nullable
public static File getDefaultDownloadDir() {
return getDirInExternalAppDir(DOWNLOAD);
}
@Nullable
public static File getExternalTempDir() {
File dir = sContext.getExternalCacheDir();
File file;
if (null != dir && FileUtils.ensureDirectory(file = new File(dir, TEMP))) {
return file;
} else {
return null;
}
}
@Nullable
public static File getExternalCopyTempDir() {
File dir = sContext.getExternalCacheDir();
File file;
if (null != dir && FileUtils.ensureDirectory(file = new File(dir, "copy"))) {
return file;
} else {
return null;
}
}
@Nullable
public static File getExternalParseErrorDir() {
return getDirInExternalAppDir(PARSE_ERROR);
}
@Nullable
public static File getExternalCrashDir() {
return getDirInExternalAppDir(CRASH);
}
@Nullable
public static File getTempDir() {
File dir = sContext.getCacheDir();
File file;
if (null != dir && FileUtils.ensureDirectory(file = new File(dir, TEMP))) {
return file;
} else {
return null;
}
}
@Nullable
public static File createTempFile() {
return FileUtils.createTempFile(getTempDir(), null);
}
public static void saveParseErrorBody(ParseException e, String body) {
File dir = getExternalParseErrorDir();
if (null == dir) {
return;
}
File file = new File(dir, ReadableTime.INSTANCE.getFilenamableTime() + ".txt");
OutputStream os = null;
try {
os = new FileOutputStream(file);
String message = e.getMessage();
if (null != message) {
os.write(message.getBytes(StandardCharsets.UTF_8));
os.write('\n');
}
if (null != body) {
os.write(body.getBytes(StandardCharsets.UTF_8));
}
os.flush();
} catch (IOException e1) {
// Ignore
} finally {
IOUtils.closeQuietly(os);
}
}
@Nullable
public static File getFilesDir(String name) {
File dir = sContext.getFilesDir();
if (dir == null) {
return null;
}
dir = new File(dir, name);
if (dir.isDirectory() || dir.mkdirs()) {
return dir;
} else {
return null;
}
}
}
================================================
FILE: app/src/main/java/com/hippo/ehviewer/Crash.kt
================================================
/*
* 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.OSUtils
import java.io.File
import java.io.FileWriter
import java.io.PrintWriter
private fun joinIfStringArray(any: Any?): String = if (any is Array<*>) any.joinToString() else any.toString()
private fun collectClassStaticInfo(clazz: Class<*>): String = clazz.declaredFields.joinToString("\n") {
"${it.name}=${joinIfStringArray(it.get(null))}"
}
object Crash {
private fun collectInfo(fw: FileWriter) {
fw.write("======== PackageInfo ========\n")
fw.write("PackageName=${BuildConfig.APPLICATION_ID}\n")
fw.write("VersionName=${BuildConfig.VERSION_NAME}\n")
fw.write("VersionCode=${BuildConfig.VERSION_CODE}\n")
fw.write("CommitSha=${BuildConfig.COMMIT_SHA}\n")
fw.write("BuildTime=${BuildConfig.BUILD_TIME}\n")
fw.write("\n")
// Runtime
fw.write("======== Runtime ========\n")
fw.write("TopActivity=${EhApplication.application.topActivity?.javaClass?.name}\n")
fw.write("\n")
// Device info
fw.write("======== DeviceInfo ========\n")
fw.write("${collectClassStaticInfo(Build::class.java)}\n")
fw.write("${collectClassStaticInfo(Build.VERSION::class.java)}\n")
fw.write("MEMORY=")
fw.write(FileUtils.humanReadableByteCount(OSUtils.getAppAllocatedMemory(), false))
fw.write("\n")
fw.write("MEMORY_NATIVE=")
fw.write(FileUtils.humanReadableByteCount(Debug.getNativeHeapAllocatedSize(), false))
fw.write("\n")
fw.write("MEMORY_MAX=")
fw.write(FileUtils.humanReadableByteCount(OSUtils.getAppMaxMemory(), false))
fw.write("\n")
fw.write("MEMORY_TOTAL=")
fw.write(FileUtils.humanReadableByteCount(OSUtils.getTotalMemory(), false))
fw.write("\n")
fw.write("\n")
}
private fun getThrowableInfo(t: Throwable, fw: FileWriter) {
val printWriter = PrintWriter(fw)
t.printStackTrace(printWriter)
var cause = t.cause
while (cause != null) {
cause.printStackTrace(printWriter)
cause = cause.cause
}
}
fun saveCrashLog(t: Throwable) {
val dir = AppConfig.getExternalCrashDir() ?: return
val nowString = ReadableTime.getFilenamableTime()
val fileName = "crash-$nowString.log"
val file = File(dir, fileName)
runCatching {
FileWriter(file).use { fw ->
fw.write("TIME=${nowString}\n")
fw.write("\n")
collectInfo(fw)
fw.write("======== CrashInfo ========\n")
getThrowableInfo(t, fw)
fw.write("\n")
fw.flush()
}
}.onFailure {
it.printStackTrace()
file.delete()
}
}
}
================================================
FILE: app/src/main/java/com/hippo/ehviewer/EhApplication.kt
================================================
/*
* 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.app.Activity
import android.content.Context
import android.os.StrictMode
import android.text.method.LinkMovementMethod
import android.view.View
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatDelegate
import androidx.collection.LruCache
import coil3.ImageLoader
import coil3.SingletonImageLoader
import coil3.disk.DiskCache
import coil3.gif.AnimatedImageDecoder
import coil3.gif.GifDecoder
import coil3.network.ConnectivityChecker
import coil3.network.NetworkFetcher
import coil3.network.okhttp.asNetworkClient
import coil3.request.crossfade
import coil3.serviceLoaderEnabled
import coil3.util.DebugLogger
import com.hippo.ehviewer.client.EhCookieStore
import com.hippo.ehviewer.client.EhEngine
import com.hippo.ehviewer.client.EhTagDatabase
import com.hippo.ehviewer.client.data.GalleryDetail
import com.hippo.ehviewer.coil.DownloadThumbInterceptor
import com.hippo.ehviewer.coil.MergeInterceptor
import com.hippo.ehviewer.dao.buildMainDB
import com.hippo.ehviewer.download.DownloadManager
import com.hippo.ehviewer.ui.EhActivity
import com.hippo.ehviewer.ui.keepNoMediaFileStatus
import com.hippo.ehviewer.widget.SearchDatabase
import com.hippo.scene.SceneApplication
import com.hippo.util.ReadableTime
import com.hippo.util.isAtLeastP
import com.hippo.util.launchIO
import com.hippo.util.loadHtml
import com.hippo.yorozuya.FileUtils
import com.hippo.yorozuya.IntIdGenerator
import eu.kanade.tachiyomi.network.interceptor.CloudflareInterceptor
import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate
import kotlinx.coroutines.DelicateCoroutinesApi
import okhttp3.Cache
import okhttp3.OkHttpClient
import okhttp3.tls.HandshakeCertificates
import okio.FileSystem
import okio.Path.Companion.toOkioPath
class EhApplication :
SceneApplication(),
SingletonImageLoader.Factory {
private val mIdGenerator = IntIdGenerator()
private val mGlobalStuffMap = HashMap()
private val mActivityList = ArrayList()
val topActivity: EhActivity?
get() = if (mActivityList.isNotEmpty()) {
mActivityList[mActivityList.size - 1] as EhActivity
} else {
null
}
fun recreateAllActivity() {
mActivityList.forEach { it.recreate() }
}
@OptIn(DelicateCoroutinesApi::class)
override fun onCreate() {
application = this
val handler = Thread.getDefaultUncaughtExceptionHandler()
Thread.setDefaultUncaughtExceptionHandler { t, e ->
try {
if (Settings.saveCrashLog) {
Crash.saveCrashLog(e)
}
} catch (_: Throwable) {
}
handler?.uncaughtException(t, e)
}
super.onCreate()
System.loadLibrary("ehviewer")
Settings.initialize()
ReadableTime.initialize(this)
AppConfig.initialize(this)
AppCompatDelegate.setDefaultNightMode(Settings.theme)
launchIO {
launchIO {
nonCacheOkHttpClient
}
launchIO {
EhTagDatabase.read()
}
launchIO {
ehDatabase
}
launchIO {
DownloadManager.isIdle
}
launchIO {
cleanupDownload()
}
launchIO {
theDawnOfNewDay()
}
}
mIdGenerator.setNextId(Settings.getInt(KEY_GLOBAL_STUFF_NEXT_ID, 0))
if (BuildConfig.DEBUG) {
StrictMode.enableDefaults()
}
}
private suspend fun cleanupDownload() {
runCatching {
keepNoMediaFileStatus()
}.onFailure {
it.printStackTrace()
}
runCatching {
clearTempDir()
}.onFailure {
it.printStackTrace()
}
}
private suspend fun theDawnOfNewDay() {
runCatching {
if (Settings.requestNews && EhCookieStore.hasSignedIn()) {
EhEngine.getNews(true)?.let { showEventPane(it) }
}
}.onFailure {
it.printStackTrace()
}
}
fun showEventPane(html: String) {
if (Settings.hideHvEvents && html.contains("You have encountered a monster!")) {
return
}
val activity = topActivity
activity?.runOnUiThread {
val dialog = AlertDialog.Builder(activity)
.setMessage(loadHtml(html))
.setPositiveButton(android.R.string.ok, null)
.create()
dialog.setOnShowListener {
val messageView = dialog.findViewById(android.R.id.message)
if (messageView is TextView) {
messageView.movementMethod = LinkMovementMethod.getInstance()
}
}
try {
dialog.show()
} catch (_: Throwable) {
// ignore
}
}
}
private fun clearTempDir() {
var dir = AppConfig.getTempDir()
if (null != dir) {
FileUtils.deleteContent(dir)
}
dir = AppConfig.getExternalTempDir()
if (null != dir) {
FileUtils.deleteContent(dir)
}
}
fun putGlobalStuff(o: Any): Int {
val id = mIdGenerator.nextId()
mGlobalStuffMap[id] = o
Settings.putInt(KEY_GLOBAL_STUFF_NEXT_ID, mIdGenerator.nextId())
return id
}
fun containGlobalStuff(id: Int): Boolean = mGlobalStuffMap.containsKey(id)
fun removeGlobalStuff(id: Int): Any? = mGlobalStuffMap.remove(id)
fun removeGlobalStuff(o: Any) {
mGlobalStuffMap.values.removeAll(setOf(o))
}
fun registerActivity(activity: Activity) {
mActivityList.add(activity)
}
fun unregisterActivity(activity: Activity) {
mActivityList.remove(activity)
}
override fun newImageLoader(context: Context) = ImageLoader.Builder(context).apply {
serviceLoaderEnabled(false)
components {
if (isAtLeastP) {
add(AnimatedImageDecoder.Factory(false))
} else {
add(GifDecoder.Factory())
}
add(
NetworkFetcher.Factory(
networkClient = { coilOkHttpClient.asNetworkClient() },
connectivityChecker = { ConnectivityChecker.ONLINE },
),
)
add(MergeInterceptor)
add(DownloadThumbInterceptor)
}
crossfade(300)
diskCache(thumbCache)
if (BuildConfig.DEBUG) logger(DebugLogger())
}.build()
companion object {
private const val KEY_GLOBAL_STUFF_NEXT_ID = "global_stuff_next_id"
lateinit var application: EhApplication
private set
val cacheDir by lazy { application.cacheDir.toOkioPath() }
val ehProxySelector by lazy { EhProxySelector() }
val nonCacheOkHttpClient by lazy {
val cf = CertificateFactory.getInstance("X.509")
val cert = application.resources.openRawResource(R.raw.isrgrootx1).use {
cf.generateCertificates(it).first() as X509Certificate
}
val certs = HandshakeCertificates.Builder()
.addPlatformTrustedCertificates()
.addTrustedCertificate(cert)
.build()
OkHttpClient.Builder().apply {
cookieJar(EhCookieStore)
proxySelector(ehProxySelector)
sslSocketFactory(certs.sslSocketFactory(), certs.trustManager)
addInterceptor(CloudflareInterceptor(application))
}.build()
}
val noRedirectOkHttpClient by lazy {
nonCacheOkHttpClient.newBuilder()
.followRedirects(false)
.build()
}
val coilOkHttpClient by lazy {
nonCacheOkHttpClient.newBuilder()
.addInterceptor { chain ->
val request = chain.request()
val newRequest = request.newBuilder()
.header("User-Agent", Settings.userAgent!!)
.build()
chain.proceed(newRequest)
}
.build()
}
// Never use this okhttp client to download large blobs!!!
val okHttpClient by lazy {
nonCacheOkHttpClient.newBuilder()
.cache(Cache(FileSystem.SYSTEM, cacheDir / "http_cache", 20 * 1024 * 1024))
.build()
}
val galleryDetailCache by lazy {
LruCache(25).also {
favouriteStatusRouter.addListener { gid, slot ->
it[gid]?.favoriteSlot = slot
}
}
}
val favouriteStatusRouter by lazy { FavouriteStatusRouter() }
val ehDatabase by lazy { buildMainDB(application) }
val searchDatabase by lazy { SearchDatabase.getInstance(application)!! }
val thumbCache by lazy {
DiskCache.Builder()
.directory(cacheDir / "thumb")
.maxSizeBytes((Settings.readCacheSize / 5).coerceIn(64, 1024).toLong() * 1024 * 1024)
.build()
}
val imageCache by lazy {
DiskCache.Builder()
.directory(cacheDir / "image")
.maxSizeBytes((Settings.readCacheSize / 5 * 4).coerceIn(256, 4096).toLong() * 1024 * 1024)
.build()
}
}
}
================================================
FILE: app/src/main/java/com/hippo/ehviewer/EhDB.kt
================================================
/*
* 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 androidx.paging.PagingSource
import androidx.room.Room.databaseBuilder
import com.hippo.ehviewer.EhApplication.Companion.ehDatabase
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.dao.BasicDao
import com.hippo.ehviewer.dao.DownloadDirname
import com.hippo.ehviewer.dao.DownloadInfo
import com.hippo.ehviewer.dao.DownloadLabel
import com.hippo.ehviewer.dao.EhDatabase
import com.hippo.ehviewer.dao.Filter
import com.hippo.ehviewer.dao.HistoryInfo
import com.hippo.ehviewer.dao.LocalFavoriteInfo
import com.hippo.ehviewer.dao.QuickSearch
import com.hippo.ehviewer.download.DownloadManager
import com.hippo.unifile.UniFile
import com.hippo.util.sendTo
object EhDB {
private val db = ehDatabase
// Fix state
@get:Synchronized
val allDownloadInfo: List
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) {
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)
}
@get:Synchronized
val allDownloadDirname: List
get() = db.downloadDirnameDao().list()
@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)
}
@get:Synchronized
val allDownloadLabelList: List
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 > 0) {
val aTime = list[i].time
val bTime = list[i + step].time
list[i].time = if (aTime == bTime) bTime + step else bTime
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
get() {
val dao = db.localFavoritesDao()
val list = dao.list()
return ArrayList(list)
}
@Synchronized
fun searchLocalFavorites(query: String): List {
val dao = db.localFavoritesDao()
val list = dao.list("%$query%")
return ArrayList(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)
}
}
@Synchronized
fun containLocalFavorites(gid: Long): Boolean {
val dao = db.localFavoritesDao()
return dao.contains(gid)
}
@Synchronized
fun putLocalFavorites(galleryInfo: GalleryInfo) {
val dao = db.localFavoritesDao()
if (null == dao.load(galleryInfo.gid)) {
val info: LocalFavoriteInfo
if (galleryInfo is LocalFavoriteInfo) {
info = galleryInfo
} else {
info = LocalFavoriteInfo(galleryInfo)
info.time = System.currentTimeMillis()
}
dao.insert(info)
}
}
@Synchronized
fun putLocalFavorites(galleryInfoList: List) {
for (gi in galleryInfoList) {
putLocalFavorites(gi)
}
}
@get:Synchronized
val allQuickSearch: List
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) {
val dao = db.quickSearchDao()
for (quickSearch in quickSearchList) {
dao.insert(quickSearch!!)
}
}
@Synchronized
fun deleteQuickSearch(quickSearch: QuickSearch?) {
quickSearch ?: return
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 > 0) {
val aTime = list[i].time
val bTime = list[i + step].time
list[i].time = if (aTime == bTime) bTime + step else bTime
i += step
}
list[start].time = toTime
dao.update(list)
}
@get:Synchronized
val historyLazyList: PagingSource
get() = db.historyDao().listLazy()
@Synchronized
fun putHistoryInfo(galleryInfo: GalleryInfo) {
val dao = db.historyDao()
val info = galleryInfo as? HistoryInfo ?: HistoryInfo(galleryInfo)
info.time = System.currentTimeMillis()
if (null != dao.load(info.gid)) {
dao.update(info)
} else {
dao.insert(info)
}
}
@Synchronized
fun updateHistoryFavSlot(gid: Long, slot: Int) {
val dao = db.historyDao()
val info = dao.load(gid)
if (null != info) {
info.favoriteSlot = slot
dao.update(info)
}
}
@Synchronized
fun putHistoryInfo(historyInfoList: List) {
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
get() = db.filterDao().list()
@Synchronized
fun addFilter(filter: Filter): Boolean {
val existFilter: Filter? = try {
db.filterDao().load(filter.text!!, filter.mode)
} catch (_: 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() == true
db.filterDao().update(filter)
}
private fun copyDao(from: BasicDao, to: BasicDao) {
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 {
// Delete old export db
context.deleteDatabase(ehExportName)
val newDb =
databaseBuilder(context, EhDatabase::class.java, ehExportName).build()
// 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
newDb.close()
// Copy export db to data dir
val dbFile = context.getDatabasePath(ehExportName)
UniFile.fromFile(dbFile)!! sendTo UniFile.fromUri(context, uri)!!
return true
}.onFailure {
it.printStackTrace()
}
return false
}
/**
* @return error string, null for no error
*/
@Synchronized
fun importDB(context: Context, uri: Uri): String? {
val tmpDBName = "tmp.db"
runCatching {
val oldDB = databaseBuilder(context, EhDatabase::class.java, tmpDBName)
.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()
context.deleteDatabase(tmpDBName)
}.onFailure {
it.printStackTrace()
return context.getString(R.string.settings_advanced_import_data_cant_read)
}
return null
}
}
================================================
FILE: app/src/main/java/com/hippo/ehviewer/EhProxySelector.kt
================================================
/*
* Copyright 2019 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer
import android.text.TextUtils
import com.hippo.ehviewer.Settings.proxyIp
import com.hippo.ehviewer.Settings.proxyPort
import com.hippo.ehviewer.Settings.proxyType
import com.hippo.network.InetValidator.isValidInetPort
import com.hippo.util.ExceptionUtils
import java.io.IOException
import java.net.InetAddress
import java.net.InetSocketAddress
import java.net.Proxy
import java.net.ProxySelector
import java.net.SocketAddress
import java.net.URI
class EhProxySelector internal constructor() : ProxySelector() {
private var delegation: ProxySelector? = null
private var alternative: ProxySelector?
init {
alternative = getDefault() ?: NullProxySelector()
updateProxy()
}
fun updateProxy() {
delegation = when (proxyType) {
TYPE_DIRECT -> NullProxySelector()
TYPE_SYSTEM -> alternative
TYPE_HTTP, TYPE_SOCKS -> null
else -> alternative
}
}
override fun select(uri: URI): List {
val type = proxyType
if (type == TYPE_HTTP || type == TYPE_SOCKS) {
runCatching {
val ip = proxyIp
val port = proxyPort
if (!TextUtils.isEmpty(ip) && isValidInetPort(port)) {
val inetAddress = InetAddress.getByName(ip)
val socketAddress = InetSocketAddress(inetAddress, port)
return listOf(
Proxy(
if (type == TYPE_HTTP) Proxy.Type.HTTP else Proxy.Type.SOCKS,
socketAddress,
),
)
}
}.onFailure {
ExceptionUtils.throwIfFatal(it)
it.printStackTrace()
}
}
return delegation?.select(uri) ?: alternative!!.select(uri)
}
override fun connectFailed(uri: URI, sa: SocketAddress, ioe: IOException) {
delegation?.select(uri)
}
private class NullProxySelector : ProxySelector() {
override fun select(uri: URI): List = listOf(Proxy.NO_PROXY)
override fun connectFailed(uri: URI, sa: SocketAddress, ioe: IOException) {}
}
companion object {
const val TYPE_DIRECT = 0
const val TYPE_SYSTEM = 1
const val TYPE_HTTP = 2
const val TYPE_SOCKS = 3
}
}
================================================
FILE: app/src/main/java/com/hippo/ehviewer/FavouriteStatusRouter.java
================================================
/*
* 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.annotation.SuppressLint;
import com.hippo.ehviewer.client.data.GalleryInfo;
import com.hippo.yorozuya.IntIdGenerator;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class FavouriteStatusRouter {
private static final String KEY_DATA_MAP_NEXT_ID = "data_map_next_id";
private final IntIdGenerator idGenerator = new IntIdGenerator(Settings.getInt(KEY_DATA_MAP_NEXT_ID, 0));
@SuppressLint("UseSparseArrays")
private final HashMap> maps = new HashMap<>();
private final List listeners = new ArrayList<>();
public int saveDataMap(Map map) {
int id = idGenerator.nextId();
maps.put(id, map);
Settings.putInt(KEY_DATA_MAP_NEXT_ID, idGenerator.nextId());
return id;
}
public Map restoreDataMap(int id) {
return maps.remove(id);
}
public void modifyFavourites(long gid, int slot) {
for (Map map : maps.values()) {
GalleryInfo info = map.get(gid);
if (info != null) {
info.setFavoriteSlot(slot);
}
}
for (Listener listener : listeners) {
listener.onModifyFavourites(gid, slot);
}
}
public void addListener(Listener listener) {
listeners.add(listener);
}
public void removeListener(Listener listener) {
listeners.remove(listener);
}
public interface Listener {
void onModifyFavourites(long gid, int slot);
}
}
================================================
FILE: app/src/main/java/com/hippo/ehviewer/GetText.kt
================================================
/*
* 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 androidx.annotation.StringRes
object GetText {
fun getString(@StringRes id: Int): String = EhApplication.application.getString(id)
fun getString(@StringRes id: Int, vararg formatArgs: Any?): String = EhApplication.application.getString(id, *formatArgs)
}
================================================
FILE: app/src/main/java/com/hippo/ehviewer/Settings.kt
================================================
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer
import android.content.SharedPreferences
import android.net.Uri
import android.util.Log
import androidx.core.content.edit
import androidx.preference.PreferenceManager
import com.hippo.ehviewer.EhApplication.Companion.application
import com.hippo.ehviewer.client.data.FavListUrlBuilder
import com.hippo.ehviewer.ui.scene.GalleryListScene
import com.hippo.glgallery.GalleryView
import com.hippo.okhttp.CHROME_USER_AGENT
import com.hippo.unifile.UniFile
import com.hippo.yorozuya.NumberUtils
import java.util.Locale
@Suppress("SameParameterValue")
object Settings {
/********************
****** Eh
********************/
const val KEY_ACCOUNT = "account"
const val KEY_GALLERY_SITE = "gallery_site"
private const val DEFAULT_GALLERY_SITE = 0
private const val KEY_IMAGE_LIMITS = "image_limits"
private const val KEY_U_CONFIG = "uconfig"
private const val KEY_MY_TAGS = "mytags"
const val KEY_THEME = "theme"
private const val DEFAULT_THEME = -1
const val KEY_BLACK_DARK_THEME = "black_dark_theme"
private const val DEFAULT_BLACK_DARK_THEME = false
private const val KEY_LAUNCH_PAGE = "launch_page"
private const val DEFAULT_LAUNCH_PAGE = 0
const val KEY_LIST_MODE = "list_mode"
private const val DEFAULT_LIST_MODE = 0
const val KEY_DETAIL_SIZE = "detail_size_"
private const val DEFAULT_DETAIL_SIZE = 8
const val KEY_LIST_THUMB_SIZE = "list_cover_size"
private const val DEFAULT_LIST_THUMB_SIZE = 40
const val KEY_THUMB_SIZE = "cover_size"
private const val DEFAULT_THUMB_SIZE = 4
const val KEY_THUMB_SHOW_TITLE = "thumb_show_title"
private const val DEFAULT_THUMB_SHOW_TITLE = true
private const val KEY_SHOW_JPN_TITLE = "show_jpn_title"
private const val DEFAULT_SHOW_JPN_TITLE = false
private const val KEY_SHOW_GALLERY_PAGES = "show_gallery_pages"
private const val DEFAULT_SHOW_GALLERY_PAGES = true
private const val KEY_SHOW_COMMENTS = "show_gallery_comments"
private const val DEFAULT_SHOW_COMMENTS = true
private const val KEY_COMMENT_THRESHOLD = "comment_threshold"
private const val DEFAULT_COMMENT_THRESHOLD = -101
private const val KEY_PREVIEW_NUM = "preview_num"
private const val DEFAULT_PREVIEW_NUM = 60
private const val KEY_PREVIEW_SIZE = "preview_size"
private const val DEFAULT_PREVIEW_SIZE = 3
const val KEY_SHOW_TAG_TRANSLATIONS = "show_tag_translations"
private const val DEFAULT_SHOW_TAG_TRANSLATIONS = false
private const val KEY_TRANSLATIONS_LAST_UPDATE = "translations_last_update"
const val KEY_TAG_TRANSLATIONS_SOURCE = "tag_translations_source"
private const val KEY_METERED_NETWORK_WARNING = "cellular_network_warning"
private const val DEFAULT_METERED_NETWORK_WARNING = false
private const val KEY_REQUEST_NEWS = "request_news"
private const val DEFAULT_REQUEST_NEWS = false
private const val KEY_HIDE_HV_EVENTS = "hide_hv_events"
private const val DEFAULT_HIDE_HV_EVENTS = false
val SIGN_IN_REQUIRED = arrayOf(
KEY_GALLERY_SITE,
KEY_IMAGE_LIMITS,
KEY_U_CONFIG,
KEY_MY_TAGS,
KEY_SHOW_JPN_TITLE,
KEY_REQUEST_NEWS,
KEY_HIDE_HV_EVENTS,
)
/********************
****** Read
********************/
private const val KEY_SCREEN_ROTATION = "screen_rotation"
private const val DEFAULT_SCREEN_ROTATION = 0
private const val KEY_READING_DIRECTION = "reading_direction"
private const val DEFAULT_READING_DIRECTION = GalleryView.LAYOUT_RIGHT_TO_LEFT
private const val KEY_PAGE_SCALING = "page_scaling"
private const val DEFAULT_PAGE_SCALING = GalleryView.SCALE_FIT
private const val KEY_START_POSITION = "start_position"
private const val DEFAULT_START_POSITION = GalleryView.START_POSITION_TOP_RIGHT
private const val KEY_READ_THEME = "read_theme"
private const val DEFAULT_READ_THEME = 1
private const val KEY_KEEP_SCREEN_ON = "keep_screen_on"
private const val DEFAULT_KEEP_SCREEN_ON = false
private const val KEY_SHOW_CLOCK = "gallery_show_clock"
private const val DEFAULT_SHOW_CLOCK = true
private const val KEY_SHOW_PROGRESS = "gallery_show_progress"
private const val DEFAULT_SHOW_PROGRESS = true
private const val KEY_SHOW_BATTERY = "gallery_show_battery"
private const val DEFAULT_SHOW_BATTERY = true
private const val KEY_SHOW_PAGE_INTERVAL = "gallery_show_page_interval"
private const val DEFAULT_SHOW_PAGE_INTERVAL = false
private const val KEY_TURN_PAGE_INTERVAL = "turn_page_interval"
private const val DEFAULT_TURN_PAGE_INTERVAL = 5
private const val KEY_VOLUME_PAGE = "volume_page"
private const val DEFAULT_VOLUME_PAGE = false
private const val KEY_VOLUME_PAGE_INTERVAL = "volume_page_interval"
private const val DEFAULT_VOLUME_PAGE_INTERVAL = 1
private const val KEY_REVERSE_VOLUME_PAGE = "reserve_volume_page"
private const val DEFAULT_REVERSE_VOLUME_PAGE = false
private const val KEY_READING_FULLSCREEN = "reading_fullscreen"
private const val VALUE_READING_FULLSCREEN = true
private const val KEY_CUSTOM_SCREEN_LIGHTNESS = "custom_screen_lightness"
private const val DEFAULT_CUSTOM_SCREEN_LIGHTNESS = false
private const val KEY_SCREEN_LIGHTNESS = "screen_lightness"
private const val DEFAULT_SCREEN_LIGHTNESS = 50
/********************
****** Download
********************/
const val KEY_DOWNLOAD_LOCATION = "download_location"
private const val KEY_DOWNLOAD_SAVE_SCHEME = "image_scheme"
private const val KEY_DOWNLOAD_SAVE_AUTHORITY = "image_authority"
private const val KEY_DOWNLOAD_SAVE_PATH = "image_path"
private const val KEY_DOWNLOAD_SAVE_QUERY = "image_query"
private const val KEY_DOWNLOAD_SAVE_FRAGMENT = "image_fragment"
const val KEY_MEDIA_SCAN = "media_scan"
private const val DEFAULT_MEDIA_SCAN = false
const val KEY_MULTI_THREAD_DOWNLOAD = "download_thread"
private const val DEFAULT_MULTI_THREAD_DOWNLOAD = 3
const val KEY_DOWNLOAD_DELAY = "download_delay_2"
private const val DEFAULT_DOWNLOAD_DELAY = 1000
private const val KEY_DOWNLOAD_TIMEOUT = "download_timeout"
private const val DEFAULT_DOWNLOAD_TIMEOUT = 60
const val KEY_PRELOAD_IMAGE = "preload_image"
private const val DEFAULT_PRELOAD_IMAGE = 5
const val KEY_DOWNLOAD_ORIGIN_IMAGE = "download_origin_image_"
private const val DEFAULT_DOWNLOAD_ORIGIN_IMAGE = 0
/********************
****** Privacy and Security
********************/
private const val KEY_SECURITY = "security"
private const val DEFAULT_SECURITY = ""
private const val KEY_ENABLE_FINGERPRINT = "enable_fingerprint"
private const val DEFAULT_ENABLE_FINGERPRINT = false
private const val KEY_SEC_SECURITY = "enable_secure"
private const val DEFAULT_SEC_SECURITY = false
/********************
****** Advanced
********************/
private const val KEY_SAVE_PARSE_ERROR_BODY = "save_parse_error_body"
private const val DEFAULT_SAVE_PARSE_ERROR_BODY = true
private const val KEY_SAVE_CRASH_LOG = "save_crash_log"
private const val DEFAULT_SAVE_CRASH_LOG = true
private const val KEY_READ_CACHE_SIZE = "read_cache_size"
private const val DEFAULT_READ_CACHE_SIZE = 320
const val KEY_APP_LANGUAGE = "app_language"
private const val DEFAULT_APP_LANGUAGE = "system"
private const val KEY_PROXY_TYPE = "proxy_type"
private const val DEFAULT_PROXY_TYPE = EhProxySelector.TYPE_SYSTEM
private const val KEY_PROXY_IP = "proxy_ip"
private val DEFAULT_PROXY_IP: String? = null
private const val KEY_PROXY_PORT = "proxy_port"
private const val DEFAULT_PROXY_PORT = -1
private const val KEY_USER_AGENT = "user_agent"
private const val KEY_APP_LINK_VERIFY_TIP = "app_link_verify_tip"
private const val DEFAULT_APP_LINK_VERIFY_TIP = false
/********************
****** Favorites
********************/
private const val KEY_FAV_CAT_0 = "fav_cat_0"
private const val KEY_FAV_CAT_1 = "fav_cat_1"
private const val KEY_FAV_CAT_2 = "fav_cat_2"
private const val KEY_FAV_CAT_3 = "fav_cat_3"
private const val KEY_FAV_CAT_4 = "fav_cat_4"
private const val KEY_FAV_CAT_5 = "fav_cat_5"
private const val KEY_FAV_CAT_6 = "fav_cat_6"
private const val KEY_FAV_CAT_7 = "fav_cat_7"
private const val KEY_FAV_CAT_8 = "fav_cat_8"
private const val KEY_FAV_CAT_9 = "fav_cat_9"
private const val DEFAULT_FAV_CAT_0 = "Favorites 0"
private const val DEFAULT_FAV_CAT_1 = "Favorites 1"
private const val DEFAULT_FAV_CAT_2 = "Favorites 2"
private const val DEFAULT_FAV_CAT_3 = "Favorites 3"
private const val DEFAULT_FAV_CAT_4 = "Favorites 4"
private const val DEFAULT_FAV_CAT_5 = "Favorites 5"
private const val DEFAULT_FAV_CAT_6 = "Favorites 6"
private const val DEFAULT_FAV_CAT_7 = "Favorites 7"
private const val DEFAULT_FAV_CAT_8 = "Favorites 8"
private const val DEFAULT_FAV_CAT_9 = "Favorites 9"
private const val KEY_FAV_COUNT_0 = "fav_count_0"
private const val KEY_FAV_COUNT_1 = "fav_count_1"
private const val KEY_FAV_COUNT_2 = "fav_count_2"
private const val KEY_FAV_COUNT_3 = "fav_count_3"
private const val KEY_FAV_COUNT_4 = "fav_count_4"
private const val KEY_FAV_COUNT_5 = "fav_count_5"
private const val KEY_FAV_COUNT_6 = "fav_count_6"
private const val KEY_FAV_COUNT_7 = "fav_count_7"
private const val KEY_FAV_COUNT_8 = "fav_count_8"
private const val KEY_FAV_COUNT_9 = "fav_count_9"
private const val KEY_FAV_LOCAL = "fav_local"
private const val KEY_FAV_CLOUD = "fav_cloud"
private const val DEFAULT_FAV_COUNT = 0
private const val KEY_RECENT_FAV_CAT = "recent_fav_cat"
private const val DEFAULT_RECENT_FAV_CAT = FavListUrlBuilder.FAV_CAT_ALL
// -1 for local, 0 - 9 for cloud favorite, other for no default fav slot
const val INVALID_DEFAULT_FAV_SLOT = -2
private const val KEY_DEFAULT_FAV_SLOT = "default_favorite_2"
private const val DEFAULT_DEFAULT_FAV_SLOT = INVALID_DEFAULT_FAV_SLOT
private const val KEY_NEVER_ADD_FAV_NOTES = "never_add_favorite_notes"
private const val DEFAULT_NEVER_ADD_FAV_NOTES = false
/********************
****** Guide
********************/
private const val KEY_GUIDE_GALLERY = "guide_gallery"
private const val DEFAULT_GUIDE_GALLERY = true
/********************
****** Others
********************/
private val TAG = Settings::class.java.simpleName
private const val KEY_SELECT_SITE = "select_site"
private const val DEFAULT_SELECT_SITE = true
private const val KEY_NEED_SIGN_IN = "need_sign_in"
private const val DEFAULT_NEED_SIGN_IN = true
private const val KEY_DISPLAY_NAME = "display_name"
private val DEFAULT_DISPLAY_NAME: String? = null
private const val KEY_AVATAR = "avatar"
private val DEFAULT_AVATAR: String? = null
private const val KEY_QS_SAVE_PROGRESS = "qs_save_progress"
private const val DEFAULT_QS_SAVE_PROGRESS = false
private const val KEY_HAS_DEFAULT_DOWNLOAD_LABEL = "has_default_download_label"
private const val DEFAULT_HAS_DOWNLOAD_LABEL = false
private const val KEY_DEFAULT_DOWNLOAD_LABEL = "default_download_label"
private val DEFAULT_DOWNLOAD_LABEL: String? = null
private const val KEY_RECENT_DOWNLOAD_LABEL = "recent_download_label"
private val DEFAULT_RECENT_DOWNLOAD_LABEL: String? = null
private const val KEY_DEFAULT_SORTING_METHOD = "default_sorting_method"
private const val DEFAULT_SORTING_METHOD = 0
private const val KEY_DEFAULT_TOP_LIST = "default_top_list"
private const val DEFAULT_TOP_LIST = "15"
private const val KEY_REMOVE_IMAGE_FILES = "include_pic"
private const val DEFAULT_REMOVE_IMAGE_FILES = true
private const val KEY_CLIPBOARD_TEXT_HASH_CODE = "clipboard_text_hash_code"
private const val DEFAULT_CLIPBOARD_TEXT_HASH_CODE = 0
private const val KEY_ARCHIVE_PASSWDS = "archive_passwds"
private const val KEY_NOTIFICATION_REQUIRED = "notification_required"
private lateinit var sSettingsPre: SharedPreferences
fun initialize() {
sSettingsPre = PreferenceManager.getDefaultSharedPreferences(application)
fixDefaultValue()
}
private fun fixDefaultValue() {
if ("zh" == Locale.getDefault().language) {
// Enable show tag translations if the language is zh
if (!sSettingsPre.contains(KEY_SHOW_TAG_TRANSLATIONS)) {
putShowTagTranslations(true)
}
}
}
private fun getBoolean(key: String, defValue: Boolean): Boolean = try {
sSettingsPre.getBoolean(key, defValue)
} catch (e: ClassCastException) {
Log.d(TAG, "Get ClassCastException when get $key value", e)
defValue
}
private fun putBoolean(key: String, value: Boolean) {
sSettingsPre.edit { putBoolean(key, value) }
}
@JvmStatic
fun getInt(key: String, defValue: Int): Int = try {
sSettingsPre.getInt(key, defValue)
} catch (e: ClassCastException) {
Log.d(TAG, "Get ClassCastException when get $key value", e)
defValue
}
@JvmStatic
fun putInt(key: String, value: Int) {
sSettingsPre.edit { putInt(key, value) }
}
private fun getLong(key: String, defValue: Long): Long = try {
sSettingsPre.getLong(key, defValue)
} catch (e: ClassCastException) {
Log.d(TAG, "Get ClassCastException when get $key value", e)
defValue
}
private fun putLong(key: String, value: Long) {
sSettingsPre.edit { putLong(key, value) }
}
private fun getString(key: String, defValue: String?): String? = try {
sSettingsPre.getString(key, defValue)
} catch (e: ClassCastException) {
Log.d(TAG, "Get ClassCastException when get $key value", e)
defValue
}
private fun putString(key: String, value: String?) {
sSettingsPre.edit { putString(key, value) }
}
private fun getStringSet(key: String): MutableSet? = sSettingsPre.getStringSet(key, null)
private fun putStringToStringSet(key: String, value: String) {
var set = getStringSet(key)
if (set == null) {
set =
mutableSetOf(value)
} else if (set.contains(value)) {
return
} else {
set.add(value)
}
sSettingsPre.edit { putStringSet(key, set) }
}
private fun getIntFromStr(key: String, defValue: Int): Int = try {
NumberUtils.parseIntSafely(
sSettingsPre.getString(key, defValue.toString()),
defValue,
)
} catch (e: ClassCastException) {
Log.d(TAG, "Get ClassCastException when get $key value", e)
defValue
}
private fun putIntToStr(key: String, value: Int) {
sSettingsPre.edit { putString(key, value.toString()) }
}
private fun dip2px(dpValue: Int): Int {
val scale = application.resources.displayMetrics.density
return (dpValue * scale + 0.5f).toInt()
}
val locale: Locale
get() {
return if (appLanguage != null && appLanguage != "system") {
Locale.forLanguageTag(appLanguage!!)
} else {
Locale.getDefault()
}
}
val gallerySite: Int
get() = getIntFromStr(KEY_GALLERY_SITE, DEFAULT_GALLERY_SITE)
fun putGallerySite(value: Int) {
putIntToStr(KEY_GALLERY_SITE, value)
}
val theme: Int
get() = getIntFromStr(KEY_THEME, DEFAULT_THEME)
fun putTheme(theme: Int) {
putIntToStr(KEY_THEME, theme)
}
val blackDarkTheme
get() = getBoolean(KEY_BLACK_DARK_THEME, DEFAULT_BLACK_DARK_THEME)
val launchPageGalleryListSceneAction: String
get() {
return when (getIntFromStr(KEY_LAUNCH_PAGE, DEFAULT_LAUNCH_PAGE)) {
3 -> GalleryListScene.ACTION_TOP_LIST
2 -> GalleryListScene.ACTION_WHATS_HOT
1 -> GalleryListScene.ACTION_SUBSCRIPTION
else -> GalleryListScene.ACTION_HOMEPAGE
}
}
val listMode: Int
get() = getIntFromStr(KEY_LIST_MODE, DEFAULT_LIST_MODE)
val detailSize: Int
get() = dip2px(40 * getInt(KEY_DETAIL_SIZE, DEFAULT_DETAIL_SIZE))
val listThumbSize: Int
get() = dip2px(2 * getInt(KEY_LIST_THUMB_SIZE, DEFAULT_LIST_THUMB_SIZE))
val listTitleSingleLine: Boolean
get() = getInt(KEY_LIST_THUMB_SIZE, DEFAULT_LIST_THUMB_SIZE) < DEFAULT_LIST_THUMB_SIZE - 2
val thumbSize: Int
get() = dip2px(40 * getInt(KEY_THUMB_SIZE, DEFAULT_THUMB_SIZE))
val thumbShowTitle: Boolean
get() = getBoolean(KEY_THUMB_SHOW_TITLE, DEFAULT_THUMB_SHOW_TITLE)
val showJpnTitle: Boolean
get() = getBoolean(KEY_SHOW_JPN_TITLE, DEFAULT_SHOW_JPN_TITLE)
val showGalleryPages: Boolean
get() = getBoolean(KEY_SHOW_GALLERY_PAGES, DEFAULT_SHOW_GALLERY_PAGES)
val showComments: Boolean
get() = getBoolean(KEY_SHOW_COMMENTS, DEFAULT_SHOW_COMMENTS)
val commentThreshold: Int
get() = getInt(KEY_COMMENT_THRESHOLD, DEFAULT_COMMENT_THRESHOLD)
val previewNum: Int
get() = getInt(KEY_PREVIEW_NUM, DEFAULT_PREVIEW_NUM)
val previewSize: Int
get() = dip2px(40 * getInt(KEY_PREVIEW_SIZE, DEFAULT_PREVIEW_SIZE))
val showTagTranslations: Boolean
get() = getBoolean(KEY_SHOW_TAG_TRANSLATIONS, DEFAULT_SHOW_TAG_TRANSLATIONS)
private fun putShowTagTranslations(value: Boolean) {
putBoolean(KEY_SHOW_TAG_TRANSLATIONS, value)
}
val translationsLastUpdate: Long
get() = getLong(KEY_TRANSLATIONS_LAST_UPDATE, -1)
fun putTranslationsLastUpdate(value: Long) {
putLong(KEY_TRANSLATIONS_LAST_UPDATE, value)
}
val meteredNetworkWarning: Boolean
get() = getBoolean(KEY_METERED_NETWORK_WARNING, DEFAULT_METERED_NETWORK_WARNING)
val requestNews: Boolean
get() = getBoolean(KEY_REQUEST_NEWS, DEFAULT_REQUEST_NEWS)
val hideHvEvents: Boolean
get() = getBoolean(KEY_HIDE_HV_EVENTS, DEFAULT_HIDE_HV_EVENTS)
val screenRotation: Int
get() = getIntFromStr(KEY_SCREEN_ROTATION, DEFAULT_SCREEN_ROTATION)
fun putScreenRotation(value: Int) {
putIntToStr(KEY_SCREEN_ROTATION, value)
}
@GalleryView.LayoutMode
val readingDirection: Int
get() = GalleryView.sanitizeLayoutMode(getIntFromStr(KEY_READING_DIRECTION, DEFAULT_READING_DIRECTION))
fun putReadingDirection(value: Int) {
putIntToStr(KEY_READING_DIRECTION, value)
}
@GalleryView.ScaleMode
val pageScaling: Int
get() = GalleryView.sanitizeScaleMode(getIntFromStr(KEY_PAGE_SCALING, DEFAULT_PAGE_SCALING))
fun putPageScaling(value: Int) {
putIntToStr(KEY_PAGE_SCALING, value)
}
@GalleryView.StartPosition
val startPosition: Int
get() = GalleryView.sanitizeStartPosition(getIntFromStr(KEY_START_POSITION, DEFAULT_START_POSITION))
fun putStartPosition(value: Int) {
putIntToStr(KEY_START_POSITION, value)
}
val readTheme: Int
get() = getIntFromStr(KEY_READ_THEME, DEFAULT_READ_THEME)
fun putReadTheme(value: Int) {
putIntToStr(KEY_READ_THEME, value)
}
val keepScreenOn: Boolean
get() = getBoolean(KEY_KEEP_SCREEN_ON, DEFAULT_KEEP_SCREEN_ON)
fun putKeepScreenOn(value: Boolean) {
putBoolean(KEY_KEEP_SCREEN_ON, value)
}
val showClock: Boolean
get() = getBoolean(KEY_SHOW_CLOCK, DEFAULT_SHOW_CLOCK)
fun putShowClock(value: Boolean) {
putBoolean(KEY_SHOW_CLOCK, value)
}
val showProgress: Boolean
get() = getBoolean(KEY_SHOW_PROGRESS, DEFAULT_SHOW_PROGRESS)
fun putShowProgress(value: Boolean) {
putBoolean(KEY_SHOW_PROGRESS, value)
}
val showBattery: Boolean
get() = getBoolean(KEY_SHOW_BATTERY, DEFAULT_SHOW_BATTERY)
fun putShowBattery(value: Boolean) {
putBoolean(KEY_SHOW_BATTERY, value)
}
val showPageInterval: Boolean
get() = getBoolean(KEY_SHOW_PAGE_INTERVAL, DEFAULT_SHOW_PAGE_INTERVAL)
fun putShowPageInterval(value: Boolean) {
putBoolean(KEY_SHOW_PAGE_INTERVAL, value)
}
val turnPageInterval: Int
get() = getInt(KEY_TURN_PAGE_INTERVAL, DEFAULT_TURN_PAGE_INTERVAL)
fun putTurnPageInterval(value: Int) {
putInt(KEY_TURN_PAGE_INTERVAL, value)
}
val volumePage: Boolean
get() = getBoolean(KEY_VOLUME_PAGE, DEFAULT_VOLUME_PAGE)
fun putVolumePage(value: Boolean) {
putBoolean(KEY_VOLUME_PAGE, value)
}
val volumePageInterval: Int
get() = getInt(KEY_VOLUME_PAGE_INTERVAL, DEFAULT_VOLUME_PAGE_INTERVAL)
fun putVolumePageInterval(value: Int) {
putInt(KEY_VOLUME_PAGE_INTERVAL, value)
}
val reverseVolumePage: Boolean
get() = getBoolean(KEY_REVERSE_VOLUME_PAGE, DEFAULT_REVERSE_VOLUME_PAGE)
fun putReverseVolumePage(value: Boolean) {
putBoolean(KEY_REVERSE_VOLUME_PAGE, value)
}
val readingFullscreen: Boolean
get() = getBoolean(KEY_READING_FULLSCREEN, VALUE_READING_FULLSCREEN)
fun putReadingFullscreen(value: Boolean) {
putBoolean(KEY_READING_FULLSCREEN, value)
}
val customScreenLightness: Boolean
get() = getBoolean(KEY_CUSTOM_SCREEN_LIGHTNESS, DEFAULT_CUSTOM_SCREEN_LIGHTNESS)
fun putCustomScreenLightness(value: Boolean) {
putBoolean(KEY_CUSTOM_SCREEN_LIGHTNESS, value)
}
val screenLightness: Int
get() = getInt(KEY_SCREEN_LIGHTNESS, DEFAULT_SCREEN_LIGHTNESS)
fun putScreenLightness(value: Int) {
putInt(KEY_SCREEN_LIGHTNESS, value)
}
val downloadLocation: UniFile?
get() {
val dir: UniFile?
val builder = Uri.Builder()
builder.scheme(getString(KEY_DOWNLOAD_SAVE_SCHEME, null))
builder.encodedAuthority(getString(KEY_DOWNLOAD_SAVE_AUTHORITY, null))
builder.encodedPath(getString(KEY_DOWNLOAD_SAVE_PATH, null))
builder.encodedQuery(getString(KEY_DOWNLOAD_SAVE_QUERY, null))
builder.encodedFragment(getString(KEY_DOWNLOAD_SAVE_FRAGMENT, null))
dir = UniFile.fromUri(application, builder.build())
return dir ?: UniFile.fromFile(AppConfig.getDefaultDownloadDir())
}
fun putDownloadLocation(location: UniFile) {
val uri = location.uri
putString(KEY_DOWNLOAD_SAVE_SCHEME, uri.scheme)
putString(KEY_DOWNLOAD_SAVE_AUTHORITY, uri.encodedAuthority)
putString(KEY_DOWNLOAD_SAVE_PATH, uri.encodedPath)
putString(KEY_DOWNLOAD_SAVE_QUERY, uri.encodedQuery)
putString(KEY_DOWNLOAD_SAVE_FRAGMENT, uri.encodedFragment)
}
val mediaScan: Boolean
get() = getBoolean(KEY_MEDIA_SCAN, DEFAULT_MEDIA_SCAN)
val downloadThreadCount: Int
get() = getIntFromStr(KEY_MULTI_THREAD_DOWNLOAD, DEFAULT_MULTI_THREAD_DOWNLOAD)
val downloadDelay: Int
get() = getIntFromStr(KEY_DOWNLOAD_DELAY, DEFAULT_DOWNLOAD_DELAY)
val downloadTimeout: Int
get() = getInt(KEY_DOWNLOAD_TIMEOUT, DEFAULT_DOWNLOAD_TIMEOUT)
val preloadImage: Int
get() = getIntFromStr(KEY_PRELOAD_IMAGE, DEFAULT_PRELOAD_IMAGE)
fun getDownloadOriginImage(mode: Boolean): Boolean = when (getIntFromStr(KEY_DOWNLOAD_ORIGIN_IMAGE, DEFAULT_DOWNLOAD_ORIGIN_IMAGE)) {
2 -> mode
1 -> true
else -> false
}
val skipCopyImage: Boolean
get() = getIntFromStr(KEY_DOWNLOAD_ORIGIN_IMAGE, DEFAULT_DOWNLOAD_ORIGIN_IMAGE) == 2
val security: String?
get() = getString(KEY_SECURITY, DEFAULT_SECURITY)
fun putSecurity(value: String?) {
putString(KEY_SECURITY, value)
}
val enableFingerprint: Boolean
get() = getBoolean(KEY_ENABLE_FINGERPRINT, DEFAULT_ENABLE_FINGERPRINT)
fun putEnableFingerprint(value: Boolean) {
putBoolean(KEY_ENABLE_FINGERPRINT, value)
}
val enabledSecurity: Boolean
get() = getBoolean(KEY_SEC_SECURITY, DEFAULT_SEC_SECURITY)
val saveParseErrorBody: Boolean
get() = getBoolean(KEY_SAVE_PARSE_ERROR_BODY, DEFAULT_SAVE_PARSE_ERROR_BODY)
val saveCrashLog: Boolean
get() = getBoolean(KEY_SAVE_CRASH_LOG, DEFAULT_SAVE_CRASH_LOG)
val readCacheSize: Int
get() = getIntFromStr(KEY_READ_CACHE_SIZE, DEFAULT_READ_CACHE_SIZE)
val appLanguage: String?
get() = getString(KEY_APP_LANGUAGE, DEFAULT_APP_LANGUAGE)
val proxyType: Int
get() = getInt(KEY_PROXY_TYPE, DEFAULT_PROXY_TYPE)
fun putProxyType(value: Int) {
putInt(KEY_PROXY_TYPE, value)
}
val proxyIp: String?
get() = getString(KEY_PROXY_IP, DEFAULT_PROXY_IP)
fun putProxyIp(value: String?) {
putString(KEY_PROXY_IP, value)
}
val proxyPort: Int
get() = getInt(KEY_PROXY_PORT, DEFAULT_PROXY_PORT)
fun putProxyPort(value: Int) {
putInt(KEY_PROXY_PORT, value)
}
val userAgent: String?
get() = getString(KEY_USER_AGENT, CHROME_USER_AGENT)
fun putUserAgent(value: String?) {
putString(KEY_USER_AGENT, value)
}
val appLinkVerifyTip: Boolean
get() = getBoolean(KEY_APP_LINK_VERIFY_TIP, DEFAULT_APP_LINK_VERIFY_TIP)
fun putAppLinkVerifyTip(value: Boolean) {
putBoolean(KEY_APP_LINK_VERIFY_TIP, value)
}
var favCat: Array
get() = arrayOf(
sSettingsPre.getString(KEY_FAV_CAT_0, DEFAULT_FAV_CAT_0)!!,
sSettingsPre.getString(KEY_FAV_CAT_1, DEFAULT_FAV_CAT_1)!!,
sSettingsPre.getString(KEY_FAV_CAT_2, DEFAULT_FAV_CAT_2)!!,
sSettingsPre.getString(KEY_FAV_CAT_3, DEFAULT_FAV_CAT_3)!!,
sSettingsPre.getString(KEY_FAV_CAT_4, DEFAULT_FAV_CAT_4)!!,
sSettingsPre.getString(KEY_FAV_CAT_5, DEFAULT_FAV_CAT_5)!!,
sSettingsPre.getString(KEY_FAV_CAT_6, DEFAULT_FAV_CAT_6)!!,
sSettingsPre.getString(KEY_FAV_CAT_7, DEFAULT_FAV_CAT_7)!!,
sSettingsPre.getString(KEY_FAV_CAT_8, DEFAULT_FAV_CAT_8)!!,
sSettingsPre.getString(KEY_FAV_CAT_9, DEFAULT_FAV_CAT_9)!!,
)
set(value) {
check(value.size == 10)
sSettingsPre.edit {
putString(KEY_FAV_CAT_0, value[0])
.putString(KEY_FAV_CAT_1, value[1])
.putString(KEY_FAV_CAT_2, value[2])
.putString(KEY_FAV_CAT_3, value[3])
.putString(KEY_FAV_CAT_4, value[4])
.putString(KEY_FAV_CAT_5, value[5])
.putString(KEY_FAV_CAT_6, value[6])
.putString(KEY_FAV_CAT_7, value[7])
.putString(KEY_FAV_CAT_8, value[8])
.putString(KEY_FAV_CAT_9, value[9])
}
}
var favCount: IntArray
get() = intArrayOf(
sSettingsPre.getInt(KEY_FAV_COUNT_0, DEFAULT_FAV_COUNT),
sSettingsPre.getInt(KEY_FAV_COUNT_1, DEFAULT_FAV_COUNT),
sSettingsPre.getInt(KEY_FAV_COUNT_2, DEFAULT_FAV_COUNT),
sSettingsPre.getInt(KEY_FAV_COUNT_3, DEFAULT_FAV_COUNT),
sSettingsPre.getInt(KEY_FAV_COUNT_4, DEFAULT_FAV_COUNT),
sSettingsPre.getInt(KEY_FAV_COUNT_5, DEFAULT_FAV_COUNT),
sSettingsPre.getInt(KEY_FAV_COUNT_6, DEFAULT_FAV_COUNT),
sSettingsPre.getInt(KEY_FAV_COUNT_7, DEFAULT_FAV_COUNT),
sSettingsPre.getInt(KEY_FAV_COUNT_8, DEFAULT_FAV_COUNT),
sSettingsPre.getInt(KEY_FAV_COUNT_9, DEFAULT_FAV_COUNT),
)
set(count) {
check(count.size == 10)
sSettingsPre.edit {
putInt(KEY_FAV_COUNT_0, count[0])
.putInt(KEY_FAV_COUNT_1, count[1])
.putInt(KEY_FAV_COUNT_2, count[2])
.putInt(KEY_FAV_COUNT_3, count[3])
.putInt(KEY_FAV_COUNT_4, count[4])
.putInt(KEY_FAV_COUNT_5, count[5])
.putInt(KEY_FAV_COUNT_6, count[6])
.putInt(KEY_FAV_COUNT_7, count[7])
.putInt(KEY_FAV_COUNT_8, count[8])
.putInt(KEY_FAV_COUNT_9, count[9])
}
}
val favLocalCount: Int
get() = sSettingsPre.getInt(KEY_FAV_LOCAL, DEFAULT_FAV_COUNT)
fun putFavLocalCount(count: Int) {
sSettingsPre.edit { putInt(KEY_FAV_LOCAL, count) }
}
val favCloudCount: Int
get() = sSettingsPre.getInt(KEY_FAV_CLOUD, DEFAULT_FAV_COUNT)
fun putFavCloudCount(count: Int) {
sSettingsPre.edit { putInt(KEY_FAV_CLOUD, count) }
}
val recentFavCat: Int
get() = getInt(KEY_RECENT_FAV_CAT, DEFAULT_RECENT_FAV_CAT)
fun putRecentFavCat(value: Int) {
putInt(KEY_RECENT_FAV_CAT, value)
}
val defaultFavSlot: Int
get() = getInt(KEY_DEFAULT_FAV_SLOT, DEFAULT_DEFAULT_FAV_SLOT)
fun putDefaultFavSlot(value: Int) {
putInt(KEY_DEFAULT_FAV_SLOT, value)
}
val neverAddFavNotes: Boolean
get() = getBoolean(KEY_NEVER_ADD_FAV_NOTES, DEFAULT_NEVER_ADD_FAV_NOTES)
fun putNeverAddFavNotes(value: Boolean) {
putBoolean(KEY_NEVER_ADD_FAV_NOTES, value)
}
val guideGallery: Boolean
get() = getBoolean(KEY_GUIDE_GALLERY, DEFAULT_GUIDE_GALLERY)
fun putGuideGallery(value: Boolean) {
putBoolean(KEY_GUIDE_GALLERY, value)
}
val selectSite: Boolean
get() = getBoolean(KEY_SELECT_SITE, DEFAULT_SELECT_SITE)
fun putSelectSite(value: Boolean) {
putBoolean(KEY_SELECT_SITE, value)
}
val needSignIn: Boolean
get() = getBoolean(KEY_NEED_SIGN_IN, DEFAULT_NEED_SIGN_IN)
fun putNeedSignIn(value: Boolean) {
putBoolean(KEY_NEED_SIGN_IN, value)
}
val displayName: String?
get() = getString(KEY_DISPLAY_NAME, DEFAULT_DISPLAY_NAME)
fun putDisplayName(value: String?) {
putString(KEY_DISPLAY_NAME, value)
}
val avatar: String?
get() = getString(KEY_AVATAR, DEFAULT_AVATAR)
fun putAvatar(value: String?) {
putString(KEY_AVATAR, value)
}
val qSSaveProgress: Boolean
get() = getBoolean(KEY_QS_SAVE_PROGRESS, DEFAULT_QS_SAVE_PROGRESS)
fun putQSSaveProgress(value: Boolean) {
putBoolean(KEY_QS_SAVE_PROGRESS, value)
}
val hasDefaultDownloadLabel: Boolean
get() = getBoolean(KEY_HAS_DEFAULT_DOWNLOAD_LABEL, DEFAULT_HAS_DOWNLOAD_LABEL)
fun putHasDefaultDownloadLabel(has: Boolean) {
putBoolean(KEY_HAS_DEFAULT_DOWNLOAD_LABEL, has)
}
val defaultDownloadLabel: String?
get() = getString(KEY_DEFAULT_DOWNLOAD_LABEL, DEFAULT_DOWNLOAD_LABEL)
fun putDefaultDownloadLabel(value: String?) {
putString(KEY_DEFAULT_DOWNLOAD_LABEL, value)
}
val recentDownloadLabel: String?
get() = getString(KEY_RECENT_DOWNLOAD_LABEL, DEFAULT_RECENT_DOWNLOAD_LABEL)
fun putRecentDownloadLabel(value: String?) {
putString(KEY_RECENT_DOWNLOAD_LABEL, value)
}
val defaultSortingMethod: Int
get() = getInt(KEY_DEFAULT_SORTING_METHOD, DEFAULT_SORTING_METHOD)
fun putDefaultSortingMethod(value: Int) {
putInt(KEY_DEFAULT_SORTING_METHOD, value)
}
val defaultTopList: String?
get() = getString(KEY_DEFAULT_TOP_LIST, DEFAULT_TOP_LIST)
fun putDefaultTopList(value: String?) {
putString(KEY_DEFAULT_TOP_LIST, value)
}
val removeImageFiles: Boolean
get() = getBoolean(KEY_REMOVE_IMAGE_FILES, DEFAULT_REMOVE_IMAGE_FILES)
fun putRemoveImageFiles(value: Boolean) {
putBoolean(KEY_REMOVE_IMAGE_FILES, value)
}
val clipboardTextHashCode: Int
get() = getInt(KEY_CLIPBOARD_TEXT_HASH_CODE, DEFAULT_CLIPBOARD_TEXT_HASH_CODE)
fun putClipboardTextHashCode(value: Int) {
putInt(KEY_CLIPBOARD_TEXT_HASH_CODE, value)
}
val archivePasswds: Set?
get() = getStringSet(KEY_ARCHIVE_PASSWDS)
fun putPasswdToArchivePasswds(value: String) {
putStringToStringSet(KEY_ARCHIVE_PASSWDS, value)
}
val notificationRequired: Boolean
get() = getBoolean(KEY_NOTIFICATION_REQUIRED, false)
fun putNotificationRequired() {
putBoolean(KEY_NOTIFICATION_REQUIRED, true)
}
}
================================================
FILE: app/src/main/java/com/hippo/ehviewer/UrlOpener.kt
================================================
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.content.res.Configuration
import android.widget.Toast
import androidx.browser.customtabs.CustomTabColorSchemeParams
import androidx.browser.customtabs.CustomTabsIntent
import androidx.core.net.toUri
import com.hippo.ehviewer.client.EhUrlOpener.parseUrl
import com.hippo.ehviewer.client.data.GalleryDetail
import com.hippo.ehviewer.client.parser.GalleryPageUrlParser
import com.hippo.ehviewer.ui.GalleryActivity
import com.hippo.ehviewer.ui.MainActivity
import com.hippo.scene.StageActivity
import rikka.core.res.resolveColor
object UrlOpener {
fun openUrl(
context: Context,
url: String?,
ehUrl: Boolean,
galleryDetail: GalleryDetail? = null,
) {
if (url.isNullOrEmpty()) {
return
}
var intent: Intent
val uri = url.toUri()
if (ehUrl) {
galleryDetail?.let {
val result = GalleryPageUrlParser.parse(url)
if (result != null) {
if (result.gid == it.gid) {
intent = Intent(context, GalleryActivity::class.java)
intent.action = GalleryActivity.ACTION_EH
intent.putExtra(GalleryActivity.KEY_GALLERY_INFO, it)
intent.putExtra(GalleryActivity.KEY_PAGE, result.page)
context.startActivity(intent)
return
}
} else if (url.startsWith("#c")) {
try {
intent = Intent(context, GalleryActivity::class.java)
intent.action = GalleryActivity.ACTION_EH
intent.putExtra(GalleryActivity.KEY_GALLERY_INFO, it)
intent.putExtra(GalleryActivity.KEY_PAGE, url.replace("#c", "").toInt() - 1)
context.startActivity(intent)
return
} catch (_: NumberFormatException) {
}
}
}
parseUrl(url)?.let {
intent = Intent(context, MainActivity::class.java)
intent.action = StageActivity.ACTION_START_SCENE
intent.putExtra(StageActivity.KEY_SCENE_NAME, it.clazz.name)
intent.putExtra(StageActivity.KEY_SCENE_ARGS, it.args)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
context.startActivity(intent)
return
}
}
val isNight = context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_YES > 0
val customTabsIntent = CustomTabsIntent.Builder()
customTabsIntent.setShowTitle(true)
val params = CustomTabColorSchemeParams.Builder()
.setToolbarColor(context.theme.resolveColor(R.attr.toolbarColor))
.build()
customTabsIntent.setDefaultColorSchemeParams(params)
customTabsIntent.setColorScheme(if (isNight) CustomTabsIntent.COLOR_SCHEME_DARK else CustomTabsIntent.COLOR_SCHEME_LIGHT)
try {
customTabsIntent.build().launchUrl(context, uri)
} catch (_: ActivityNotFoundException) {
Toast.makeText(context, R.string.no_browser_installed, Toast.LENGTH_LONG).show()
}
}
}
================================================
FILE: app/src/main/java/com/hippo/ehviewer/WindowInsetsAnimationHelper.java
================================================
/*
* 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 .
*/
package com.hippo.ehviewer;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.core.view.WindowInsetsAnimationCompat;
import androidx.core.view.WindowInsetsCompat;
import com.hippo.yorozuya.MathUtils;
import java.util.HashMap;
import java.util.List;
public class WindowInsetsAnimationHelper extends WindowInsetsAnimationCompat.Callback {
private final View[] views;
private final HashMap startPaddings = new HashMap<>();
private final HashMap endPaddings = new HashMap<>();
WindowInsetsAnimationCompat animation;
public WindowInsetsAnimationHelper(int dispatchMode, View... views) {
super(dispatchMode);
this.views = views;
}
@Override
public void onPrepare(@NonNull WindowInsetsAnimationCompat animation) {
super.onPrepare(animation);
this.animation = animation;
for (View view : views) {
if (view == null) {
continue;
}
startPaddings.put(view, view.getPaddingBottom());
}
}
@NonNull
@Override
public WindowInsetsAnimationCompat.BoundsCompat onStart(@NonNull WindowInsetsAnimationCompat animation, @NonNull WindowInsetsAnimationCompat.BoundsCompat bounds) {
this.animation = animation;
for (View view : views) {
if (view == null) {
continue;
}
endPaddings.put(view, view.getPaddingBottom());
Integer padding = startPaddings.get(view);
int startPadding = (padding != null) ? padding : 0;
view.setTranslationY(-(startPadding - view.getPaddingBottom()));
}
return bounds;
}
@NonNull
@Override
public WindowInsetsCompat onProgress(@NonNull WindowInsetsCompat insets, @NonNull List runningAnimations) {
if (animation == null) {
return insets;
}
WindowInsetsAnimationCompat imeAnimation = null;
for (WindowInsetsAnimationCompat animation : runningAnimations) {
if ((animation.getTypeMask() & WindowInsetsCompat.Type.ime()) != 0) {
imeAnimation = animation;
break;
}
}
if (imeAnimation != null) {
for (View view : views) {
if (view == null) {
continue;
}
Integer padding1 = startPaddings.get(view);
int startPadding = (padding1 != null) ? padding1 : 0;
Integer padding2 = endPaddings.get(view);
int endPadding = (padding2 != null) ? padding2 : 0;
view.setTranslationY(MathUtils.lerp(endPadding - startPadding, 0, animation.getInterpolatedFraction()));
}
}
return insets;
}
@Override
public void onEnd(@NonNull WindowInsetsAnimationCompat animation) {
super.onEnd(animation);
startPaddings.clear();
endPaddings.clear();
this.animation = null;
for (View view : views) {
if (view == null) {
continue;
}
view.setTranslationY(0);
}
}
}
================================================
FILE: app/src/main/java/com/hippo/ehviewer/client/EhCacheKeyFactory.kt
================================================
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.client
import com.hippo.ehviewer.client.data.AbstractGalleryInfo
// E-Hentai Large Preview (v1 Cover): https://ehgt.org/**.jpg
// ExHentai Large Preview (v1 Cover): https://s.exhentai.org/t/**.jpg
// E-Hentai v2 Cover: https://ehgt.org/w/**.webp
// ExHentai v2 Cover: https://s.exhentai.org/w/**.webp
// Normal Preview (v1 v2): https://*.hath.network/c(m|1|2)/[timed token]/[gid]-[index].(jpg|webp)
// Large Preview (v2): https://*.hath.network/[timed token]/**.webp
const val URL_PREFIX_THUMB_E = "https://ehgt.org/"
const val URL_PREFIX_THUMB_EX = "https://s.exhentai.org/"
private const val URL_PREFIX_V1_THUMB_EX = URL_PREFIX_THUMB_EX + "t/"
private const val URL_PREFIX_V1_THUMB_EX_OLD = "https://exhentai.org/t/"
private val NormalPreviewKeyRegex = Regex("/(c[12m])/[^/]+/(\\d+-\\d+)")
fun getImageKey(gid: Long, index: Int) = "image:$gid:$index"
fun getThumbKey(gid: Long): String = "preview:large:$gid:0"
fun getLargePreviewKey(gid: Long, index: Int) = "preview:large:$gid:$index"
fun getNormalPreviewKey(url: String) = NormalPreviewKeyRegex.find(url)?.let { "preview:normal:${it.groupValues[1]}:${it.groupValues[2]}" } ?: url
val String.isNormalPreviewKey
get() = startsWith("preview:normal:")
val String.thumbUrl
get() = removePrefix(URL_PREFIX_THUMB_E)
.removePrefix(URL_PREFIX_V1_THUMB_EX_OLD)
.removePrefix(URL_PREFIX_V1_THUMB_EX)
.removePrefix(URL_PREFIX_THUMB_EX).let {
if (it.startsWith("https:")) {
it
} else {
if (EhUtils.isExHentai) {
if (it.endsWith("webp")) URL_PREFIX_THUMB_EX else URL_PREFIX_V1_THUMB_EX
} else {
URL_PREFIX_THUMB_E
} + it
}
}
val AbstractGalleryInfo.thumbUrl
get() = thumb?.thumbUrl
================================================
FILE: app/src/main/java/com/hippo/ehviewer/client/EhClient.kt
================================================
/*
* 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.util.launchIO
import com.hippo.util.withUIContext
import java.io.File
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
object EhClient {
internal fun enqueue(request: EhRequest, scope: CoroutineScope) {
check(!request.isActive) // Abort if attempt to execute an active request
request.job = scope.launchIO {
val callback: Callback? = request.callback
try {
val result: Any? = request.run {
if (args == null) {
execute(method)
} else {
execute(method, *args!!)
}
}
withUIContext { callback?.onSuccess(result) }
} catch (e: Exception) {
if (e is CancellationException) {
throw e // Don't catch coroutine CancellationException
}
e.printStackTrace()
withUIContext { callback?.onFailure(e) }
}
}
}
suspend fun execute(method: Int, vararg params: Any?): Any? = when (method) {
METHOD_GET_GALLERY_LIST -> EhEngine.getGalleryList(
params[0] as String,
)
METHOD_GET_GALLERY_DETAIL -> EhEngine.getGalleryDetail(
params[0] as String,
)
METHOD_GET_PREVIEW_SET -> EhEngine.getPreviewSet(
params[0] as String,
)
METHOD_GET_RATE_GALLERY -> EhEngine.rateGallery(
params[0] as Long,
params[1] as String?,
params[2] as Long,
params[3] as String?,
params[4] as Float,
)
METHOD_GET_COMMENT_GALLERY -> EhEngine.commentGallery(
params[0] as String?,
params[1] as String,
params[2] as String?,
)
METHOD_GET_GALLERY_TOKEN -> EhEngine.getGalleryToken(
params[0] as Long,
params[1] as String?,
params[2] as Int,
)
METHOD_GET_FAVORITES -> EhEngine.getFavorites(
params[0] as String,
)
METHOD_ADD_FAVORITES -> EhEngine.addFavorites(
params[0] as Long,
params[1] as String?,
params[2] as Int,
params[3] as String?,
)
METHOD_ADD_FAVORITES_RANGE ->
@Suppress("UNCHECKED_CAST")
EhEngine.addFavoritesRange(
params[0] as LongArray,
params[1] as Array,
params[2] as Int,
)
METHOD_MODIFY_FAVORITES -> EhEngine.modifyFavorites(
params[0] as String,
params[1] as LongArray,
params[2] as Int,
)
METHOD_GET_TORRENT_LIST -> EhEngine.getTorrentList(
params[0] as String,
params[1] as Long,
params[2] as String?,
)
METHOD_VOTE_COMMENT -> EhEngine.voteComment(
params[0] as Long,
params[1] as String?,
params[2] as Long,
params[3] as String?,
params[4] as Long,
params[5] as Int,
)
METHOD_IMAGE_SEARCH -> EhEngine.imageSearch(
params[0] as File,
params[1] as Boolean,
params[2] as Boolean,
params[3] as Boolean,
)
METHOD_ARCHIVE_LIST -> EhEngine.getArchiveList(
params[0] as String,
params[1] as Long,
params[2] as String?,
)
METHOD_DOWNLOAD_ARCHIVE -> EhEngine.downloadArchive(
params[0] as Long,
params[1] as String?,
params[2] as String?,
params[3] as Boolean,
)
METHOD_VOTE_TAG -> EhEngine.voteTag(
params[0] as Long,
params[1] as String?,
params[2] as Long,
params[3] as String?,
params[4] as String?,
params[5] as Int,
)
else -> throw IllegalStateException("Can't detect method $method")
}
interface Callback {
fun onSuccess(result: E)
fun onFailure(e: Exception)
fun onCancel()
}
const val METHOD_GET_GALLERY_LIST = 1
const val METHOD_GET_GALLERY_DETAIL = 3
const val METHOD_GET_PREVIEW_SET = 4
const val METHOD_GET_RATE_GALLERY = 5
const val METHOD_GET_COMMENT_GALLERY = 6
const val METHOD_GET_GALLERY_TOKEN = 7
const val METHOD_GET_FAVORITES = 8
const val METHOD_ADD_FAVORITES = 9
const val METHOD_ADD_FAVORITES_RANGE = 10
const val METHOD_MODIFY_FAVORITES = 11
const val METHOD_GET_TORRENT_LIST = 12
const val METHOD_VOTE_COMMENT = 15
const val METHOD_IMAGE_SEARCH = 16
const val METHOD_ARCHIVE_LIST = 17
const val METHOD_DOWNLOAD_ARCHIVE = 18
const val METHOD_VOTE_TAG = 19
}
================================================
FILE: app/src/main/java/com/hippo/ehviewer/client/EhCookieStore.kt
================================================
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.client
import android.webkit.CookieManager
import com.hippo.ehviewer.EhApplication
import com.hippo.network.CookieDatabase
import com.hippo.network.CookieSet
import com.hippo.util.launchIO
import java.util.Collections
import java.util.regex.Pattern
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import okhttp3.Cookie
import okhttp3.CookieJar
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl
@OptIn(DelicateCoroutinesApi::class)
object EhCookieStore : CookieJar {
private val cookieManager = CookieManager.getInstance()
private val db: CookieDatabase = CookieDatabase(EhApplication.application, "okhttp3-cookie.db")
private val map: MutableMap = db.allCookies
private val updateLock = Mutex()
const val KEY_CLOUDFLARE = "cf_clearance"
const val KEY_HATH_PERKS = "hath_perks"
const val KEY_IPB_MEMBER_ID = "ipb_member_id"
const val KEY_IPB_PASS_HASH = "ipb_pass_hash"
const val KEY_IGNEOUS = "igneous"
const val KEY_QUOTA = "iq"
const val KEY_SETTINGS_PROFILE = "sp"
const val KEY_STAR = "star"
private const val KEY_UTMP = "__utmp"
private const val KEY_CONTENT_WARNING = "nw"
private const val CONTENT_WARNING_NOT_SHOW = "1"
private val sTipsCookie: Cookie = Cookie.Builder()
.name(KEY_CONTENT_WARNING)
.value(CONTENT_WARNING_NOT_SHOW)
.domain(EhUrl.DOMAIN_E)
.path("/")
.expiresAt(Long.MAX_VALUE)
.build()
fun hasSignedIn(): Boolean {
val url = EhUrl.HOST_E.toHttpUrl()
return contains(url, KEY_IPB_MEMBER_ID) && contains(url, KEY_IPB_PASS_HASH)
}
suspend fun copyCookie(domain: String, newDomain: String, name: String, path: String = "/") {
val cookie = map[domain]?.get(name, domain, path)
cookie?.let { addCookie(newCookie(it, newDomain)) }
}
suspend fun deleteCookie(url: HttpUrl, name: String) {
val deletedCookie = Cookie.Builder()
.name(name)
.value("deleted")
.domain(url.host)
.expiresAt(0)
.build()
addCookie(deletedCookie)
}
suspend fun addCookie(cookie: Cookie) {
updateLock.withLock {
// For cookie database
var toAdd: Cookie? = null
var toUpdate: Cookie? = null
var toRemove: Cookie? = null
var set = map[cookie.domain]
if (set == null) {
set = CookieSet()
map[cookie.domain] = set
}
if (cookie.expiresAt <= System.currentTimeMillis()) {
toRemove = set.remove(cookie)
// If the cookie is not persistent, it's not in database
if (toRemove != null && !toRemove.persistent) {
toRemove = null
}
} else {
toAdd = cookie
toUpdate = set.add(cookie)
// If the cookie is not persistent, it's not in database
if (!toAdd.persistent) toAdd = null
if (toUpdate != null && !toUpdate.persistent) toUpdate = null
// Remove the cookie if it updates to null
if (toAdd == null && toUpdate != null) {
toRemove = toUpdate
toUpdate = null
}
}
if (toRemove != null) {
db.remove(toRemove)
}
if (toAdd != null) {
if (toUpdate != null) {
db.update(toUpdate, toAdd)
} else {
db.add(toAdd)
}
}
}
}
fun getCookieHeader(url: HttpUrl): String {
val cookies = getCookies(url)
val cookieHeader = StringBuilder()
for (i in cookies.indices) {
if (i > 0) {
cookieHeader.append("; ")
}
val cookie = cookies[i]
cookieHeader.append(cookie.name).append('=').append(cookie.value)
}
return cookieHeader.toString()
}
fun getCookieValue(url: HttpUrl, name: String): String? {
getCookies(url).forEach {
if (it.name == name) return it.value
}
return null
}
@Synchronized
fun getCookies(url: HttpUrl): List {
val accepted: MutableList = ArrayList()
val expired: MutableList = ArrayList()
for ((domain, cookieSet) in map) {
if (domainMatch(url, domain)) {
cookieSet[url, accepted, expired]
}
}
for (cookie in expired) {
if (cookie.persistent) {
launchIO {
db.remove(cookie)
}
}
}
// RFC 6265 Section-5.4 step 2, sort the cookie-list
// Cookies with longer paths are listed before cookies with shorter paths.
// Ignore creation-time, we don't store them.
accepted.sortWith { o1: Cookie, o2: Cookie -> o2.path.length - o1.path.length }
return accepted
}
/**
* Remove all cookies in this `CookieRepository`.
*/
suspend fun clear() {
updateLock.withLock {
map.clear()
db.clear()
}
}
fun newCookie(
cookie: Cookie,
newDomain: String,
forcePersistent: Boolean = false,
forceLongLive: Boolean = false,
forceNotHostOnly: Boolean = false,
): Cookie {
val builder = Cookie.Builder()
builder.name(cookie.name)
builder.value(cookie.value)
if (forceLongLive) {
builder.expiresAt(Long.MAX_VALUE)
} else if (cookie.persistent) {
builder.expiresAt(cookie.expiresAt)
} else if (forcePersistent) {
builder.expiresAt(Long.MAX_VALUE)
}
if (cookie.hostOnly && !forceNotHostOnly) {
builder.hostOnlyDomain(newDomain)
} else {
builder.domain(newDomain)
}
builder.path(cookie.path)
if (cookie.secure) {
builder.secure()
}
if (cookie.httpOnly) {
builder.httpOnly()
}
return builder.build()
}
/**
* Quick and dirty pattern to differentiate IP addresses from hostnames. This is an approximation
* of Android's private InetAddress#isNumeric API.
*
*
* This matches IPv6 addresses as a hex string containing at least one colon, and possibly
* including dots after the first colon. It matches IPv4 addresses as strings containing only
* decimal digits and dots. This pattern matches strings like "a:.23" and "54" that are neither IP
* addresses nor hostnames; they will be verified as IP addresses (which is a more strict
* verification).
*/
private val VERIFY_AS_IP_ADDRESS =
Pattern.compile("([0-9a-fA-F]*:[0-9a-fA-F:.]*)|([\\d.]+)")
/**
* Returns true if `host` is not a host name and might be an IP address.
*/
private fun verifyAsIpAddress(host: String): Boolean = VERIFY_AS_IP_ADDRESS.matcher(host).matches()
// okhttp3.Cookie.domainMatch(HttpUrl, String)
private fun domainMatch(url: HttpUrl, domain: String?): Boolean {
val urlHost = url.host
return if (urlHost == domain) {
true // As in 'example.com' matching 'example.com'.
} else {
urlHost.endsWith(domain!!) &&
urlHost[urlHost.length - domain.length - 1] == '.' &&
!verifyAsIpAddress(
urlHost,
)
}
// As in 'example.com' matching 'www.example.com'.
}
private fun contains(url: HttpUrl, name: String?): Boolean {
for (cookie in getCookies(url)) {
if (cookie.name == name) {
return true
}
}
return false
}
fun loadForWebView(url: String, filter: (Cookie) -> Boolean) {
cookieManager.removeAllCookies(null)
getCookies(url.toHttpUrl()).forEach {
if (filter(it)) {
cookieManager.setCookie(url, it.toString())
}
}
}
fun saveFromWebView(url: String, filter: (Cookie) -> Boolean): Boolean {
val cookies = cookieManager.getCookie(url) ?: return false
var saved = false
cookies.split(';').forEach { header ->
Cookie.parse(url.toHttpUrl(), header.trim())?.let {
if (filter(it)) {
val persistentCookie = Cookie.Builder()
.name(it.name)
.value(it.value)
.domain(it.domain)
.path(it.path)
.expiresAt(System.currentTimeMillis() + 365L * 24 * 60 * 60 * 1000)
.apply {
if (it.secure) secure()
if (it.httpOnly) httpOnly()
if (it.hostOnly) hostOnlyDomain(it.domain)
}
.build()
launchIO { addCookie(persistentCookie) }
saved = true
}
}
}
return saved
}
override fun loadForRequest(url: HttpUrl): List {
val cookies = getCookies(url)
val checkTips = domainMatch(url, EhUrl.DOMAIN_E)
return if (checkTips) {
val result: MutableList = ArrayList(cookies.size + 1)
// Add all but skip some
for (cookie in cookies) {
val name = cookie.name
if (KEY_CONTENT_WARNING == name) {
continue
}
result.add(cookie)
}
// Add some
result.add(sTipsCookie)
Collections.unmodifiableList(result)
} else {
cookies
}
}
override fun saveFromResponse(url: HttpUrl, cookies: List) {
for (cookie in cookies) {
// See https://github.com/Ehviewer-Overhauled/Ehviewer/issues/873
if (cookie.name != KEY_UTMP) {
launchIO { addCookie(cookie) }
}
}
}
}
================================================
FILE: app/src/main/java/com/hippo/ehviewer/client/EhEngine.kt
================================================
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.client
import android.util.Log
import com.hippo.ehviewer.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
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.PreviewSet
import com.hippo.ehviewer.client.exception.EhException
import com.hippo.ehviewer.client.exception.InsufficientFundsException
import com.hippo.ehviewer.client.exception.NotLoggedInException
import com.hippo.ehviewer.client.exception.ParseException
import com.hippo.ehviewer.client.parser.ArchiveParser
import com.hippo.ehviewer.client.parser.EventPaneParser
import com.hippo.ehviewer.client.parser.FavoritesParser
import com.hippo.ehviewer.client.parser.ForumsParser
import com.hippo.ehviewer.client.parser.GalleryApiParser
import com.hippo.ehviewer.client.parser.GalleryDetailParser
import com.hippo.ehviewer.client.parser.GalleryListParser
import com.hippo.ehviewer.client.parser.GalleryMultiPageViewerParser
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
import com.hippo.ehviewer.client.parser.TorrentParser
import com.hippo.ehviewer.client.parser.UserConfigParser
import com.hippo.ehviewer.client.parser.VoteCommentParser
import com.hippo.ehviewer.client.parser.VoteTagParser
import com.hippo.network.StatusCodeException
import java.io.File
import kotlin.math.ceil
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import okhttp3.FormBody
import okhttp3.Headers
import okhttp3.MediaType
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.MultipartBody
import okhttp3.Request
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.asRequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.coroutines.executeAsync
import org.json.JSONArray
import org.json.JSONObject
import org.jsoup.Jsoup
private val okHttpClient = EhApplication.okHttpClient
private val MEDIA_TYPE_JSON: MediaType = "application/json; charset=utf-8".toMediaType()
private const val TAG = "EhEngine"
private const val MAX_REQUEST_SIZE = 25
private val MEDIA_TYPE_JPEG: MediaType = "image/jpeg".toMediaType()
private var sEhFilter = EhFilter
private fun rethrowExactly(code: Int, body: String, e: Throwable) {
// Check sad panda (without panda)
if (body.isEmpty()) {
if (EhUtils.isExHentai) {
throw EhException("Sad Panda\n(without panda)")
} else {
throw EhException("IP banned")
}
}
// Check 503
if (body.contains("Backend fetch failed")) {
throw EhException("Error 503\nBackend fetch failed")
}
// Check Gallery Not Available
if (body.contains("Gallery Not Available - ")) {
val error = GalleryNotAvailableParser.parse(body)
if (!error.isNullOrBlank()) {
throw EhException(error)
}
}
// Check bad response code
if (code >= 400) {
if (Settings.saveParseErrorBody && e is ParseException && body.isNotEmpty()) {
AppConfig.saveParseErrorBody(e, body)
}
throw StatusCodeException(code)
}
if (e is ParseException) {
if (!body.contains("<")) {
throw EhException(body)
} else {
if (Settings.saveParseErrorBody) {
AppConfig.saveParseErrorBody(e, body)
}
throw EhException(GetText.getString(R.string.error_parse_error), e)
}
}
// We can't translate it, rethrow it anyway
throw e
}
private suspend inline fun Request.Builder.executeAndParsingWith(block: String.() -> T): T = okHttpClient.newCall(this.build()).executeAsync().use { response ->
val body = response.body.string()
runCatching {
block(body)
}.onFailure {
rethrowExactly(response.code, body, it)
}.getOrThrow()
}
object EhEngine {
suspend fun getOriginalImageUrl(url: String, referer: String?): String {
Log.d(TAG, url)
return EhRequestBuilder(url, referer).build().executeNoRedirect {
header("Location")?.apply {
if (contains("bounce_login")) {
throw NotLoggedInException()
}
} ?: throw InsufficientFundsException()
}
}
suspend fun signIn(username: String, password: String): String {
val referer = "https://forums.e-hentai.org/index.php?act=Login&CODE=00"
val builder = FormBody.Builder()
.add("referer", referer)
.add("b", "")
.add("bt", "")
.add("UserName", username)
.add("PassWord", password)
.add("CookieDate", "1")
val url = EhUrl.API_SIGN_IN
val origin = "https://forums.e-hentai.org"
Log.d(TAG, url)
return EhRequestBuilder(url, referer, origin)
.post(builder.build())
.executeAndParsingWith(SignInParser::parse)
}
private suspend fun fillGalleryList(
list: MutableList,
url: String,
filter: Boolean,
) {
// Filter title and uploader
if (filter) {
list.removeAll { !sEhFilter.filterTitle(it) || !sEhFilter.filterUploader(it) }
}
var hasTags = false
var hasPages = false
var hasRated = false
for (gi in list) {
if (gi.simpleTags != null) {
hasTags = true
}
if (gi.pages != 0) {
hasPages = true
}
if (gi.rated) {
hasRated = true
}
}
val needApi = filter && sEhFilter.needTags() && !hasTags || Settings.showGalleryPages && !hasPages || hasRated
if (needApi) {
fillGalleryListByApi(list, url)
}
// Filter tag
if (filter) {
// Thumbnail mode need filter uploader again
list.removeAll {
!sEhFilter.filterUploader(it) || !sEhFilter.filterTag(it) || !sEhFilter.filterTagNamespace(it)
}
}
}
suspend fun getGalleryList(url: String): GalleryListParser.Result {
val referer = EhUrl.referer
Log.d(TAG, url)
return EhRequestBuilder(url, referer).executeAndParsingWith(GalleryListParser::parse)
.apply { fillGalleryList(galleryInfoList, url, true) }
}
suspend fun fillGalleryListByApi(
galleryInfoList: List,
referer: String,
): List {
val requestItems: MutableList = ArrayList(MAX_REQUEST_SIZE)
var i = 0
val size = galleryInfoList.size
while (i < size) {
requestItems.add(galleryInfoList[i])
if (requestItems.size == MAX_REQUEST_SIZE || i == size - 1) {
doFillGalleryListByApi(requestItems, referer)
requestItems.clear()
}
i++
}
return galleryInfoList
}
private suspend fun doFillGalleryListByApi(
galleryInfoList: List,
referer: String,
) {
val json = JSONObject()
json.put("method", "gdata")
val ja = JSONArray()
var i = 0
val size = galleryInfoList.size
while (i < size) {
val gi = galleryInfoList[i]
val g = JSONArray()
g.put(gi.gid)
g.put(gi.token)
ja.put(g)
i++
}
json.put("gidlist", ja)
json.put("namespace", 1)
val url = EhUrl.apiUrl
val origin = EhUrl.origin
Log.d(TAG, url)
return EhRequestBuilder(url, referer, origin)
.post(json.toString().toRequestBody(MEDIA_TYPE_JSON))
.executeAndParsingWith {
GalleryApiParser.parse(this, galleryInfoList)
}
}
suspend fun getGalleryDetail(url: String): GalleryDetail {
val referer = EhUrl.referer
Log.d(TAG, url)
return EhRequestBuilder(url, referer).executeAndParsingWith {
EventPaneParser.parse(this)?.let {
application.showEventPane(it)
}
GalleryDetailParser.parse(this)
}
}
suspend fun getPreviewSet(url: String): Pair {
val referer = EhUrl.referer
Log.d(TAG, url)
return EhRequestBuilder(url, referer).executeAndParsingWith {
GalleryDetailParser.parsePreviewSet(this) to GalleryDetailParser.parsePreviewPages(this)
}
}
@Throws(Throwable::class)
suspend fun rateGallery(
apiUid: Long,
apiKey: String?,
gid: Long,
token: String?,
rating: Float,
): RateGalleryParser.Result {
val json = JSONObject()
json.put("method", "rategallery")
json.put("apiuid", apiUid)
json.put("apikey", apiKey)
json.put("gid", gid)
json.put("token", token)
json.put("rating", ceil((rating * 2).toDouble()).toInt())
val requestBody: RequestBody = json.toString().toRequestBody(MEDIA_TYPE_JSON)
val url = EhUrl.apiUrl
val referer = EhUrl.getGalleryDetailUrl(gid, token)
val origin = EhUrl.origin
Log.d(TAG, url)
return EhRequestBuilder(url, referer, origin)
.post(requestBody)
.executeAndParsingWith(RateGalleryParser::parse)
}
suspend fun commentGallery(
url: String?,
comment: String,
id: String?,
): GalleryCommentList {
val builder = FormBody.Builder()
if (id == null) {
builder.add("commenttext_new", comment)
} else {
builder.add("commenttext_edit", comment)
builder.add("edit_comment", id)
}
val origin = EhUrl.origin
Log.d(TAG, url!!)
return EhRequestBuilder(url, url, origin)
.post(builder.build())
.executeAndParsingWith {
val document = Jsoup.parse(this)
val elements = document.select("#chd + p")
if (elements.isNotEmpty()) {
throw EhException(elements[0].text())
}
GalleryDetailParser.parseComments(document)
}
}
suspend fun getGalleryToken(
gid: Long,
gtoken: String?,
page: Int,
): String {
val json = JSONObject()
.put("method", "gtoken")
.put(
"pagelist",
JSONArray().put(
JSONArray().put(gid).put(gtoken).put(page + 1),
),
)
val requestBody: RequestBody = json.toString().toRequestBody(MEDIA_TYPE_JSON)
val url = EhUrl.apiUrl
val referer = EhUrl.referer
val origin = EhUrl.origin
Log.d(TAG, url)
return EhRequestBuilder(url, referer, origin)
.post(requestBody)
.executeAndParsingWith(GalleryTokenApiParser::parse)
}
suspend fun getFavorites(
url: String,
): FavoritesParser.Result {
val referer = EhUrl.referer
Log.d(TAG, url)
return EhRequestBuilder(url, referer).executeAndParsingWith(FavoritesParser::parse)
.apply { fillGalleryList(galleryInfoList, url, false) }
}
/**
* @param dstCat -1 for delete, 0 - 9 for cloud favorite, others throw Exception
* @param note max 250 characters
*/
suspend fun addFavorites(
gid: Long,
token: String?,
dstCat: Int,
note: String?,
) {
val catStr: String = when (dstCat) {
-1 -> {
"favdel"
}
in 0..9 -> {
dstCat.toString()
}
else -> {
throw EhException("Invalid dstCat: $dstCat")
}
}
val builder = FormBody.Builder()
builder.add("favcat", catStr)
builder.add("favnote", note ?: "")
// submit=Add+to+Favorites is not necessary, just use submit=Apply+Changes all the time
builder.add("submit", "Apply Changes")
builder.add("update", "1")
val url = EhUrl.getAddFavorites(gid, token)
val origin = EhUrl.origin
Log.d(TAG, url)
return EhRequestBuilder(url, url, origin)
.post(builder.build())
.executeAndParsingWith { }
}
@Throws(Throwable::class)
suspend fun addFavoritesRange(
gidArray: LongArray,
tokenArray: Array,
dstCat: Int,
): Void? {
check(gidArray.size == tokenArray.size)
var i = 0
val n = gidArray.size
while (i < n) {
addFavorites(gidArray[i], tokenArray[i], dstCat, null)
i++
}
return null
}
suspend fun modifyFavorites(
url: String,
gidArray: LongArray,
dstCat: Int,
): FavoritesParser.Result {
val catStr: String = when (dstCat) {
-1 -> {
"delete"
}
in 0..9 -> {
"fav$dstCat"
}
else -> {
throw EhException("Invalid dstCat: $dstCat")
}
}
val builder = FormBody.Builder()
builder.add("ddact", catStr)
for (gid in gidArray) {
builder.add("modifygids[]", gid.toString())
}
builder.add("apply", "Apply")
val origin = EhUrl.origin
Log.d(TAG, url)
return EhRequestBuilder(url, url, origin)
.post(builder.build())
.executeAndParsingWith(FavoritesParser::parse)
.apply { fillGalleryList(galleryInfoList, url, false) }
}
suspend fun getTorrentList(
url: String,
gid: Long,
token: String?,
): List {
val referer = EhUrl.getGalleryDetailUrl(gid, token)
Log.d(TAG, url)
return EhRequestBuilder(url, referer).executeAndParsingWith(TorrentParser::parse)
}
suspend fun getArchiveList(
url: String,
gid: Long,
token: String?,
): ArchiveParser.Result {
val referer = EhUrl.getGalleryDetailUrl(gid, token)
Log.d(TAG, url)
return EhRequestBuilder(url, referer).executeAndParsingWith(ArchiveParser::parse)
.apply { funds = funds ?: getFunds() }
}
suspend fun downloadArchive(
gid: Long,
token: String?,
res: String?,
isHAtH: Boolean,
): String? {
if (res.isNullOrEmpty()) {
throw EhException("Invalid res: $res")
}
val builder = FormBody.Builder()
if (isHAtH) {
builder.add("hathdl_xres", res)
} else {
builder.add("dltype", res)
if (res == "org") {
builder.add("dlcheck", "Download Original Archive")
} else {
builder.add("dlcheck", "Download Resample Archive")
}
}
val url = EhUrl.getDownloadArchive(gid, token)
val referer = EhUrl.getGalleryDetailUrl(gid, token)
val origin = EhUrl.origin
Log.d(TAG, url)
val request = EhRequestBuilder(url, referer, origin)
.post(builder.build())
var result = request.executeAndParsingWith(ArchiveParser::parseArchiveUrl)
if (!isHAtH) {
if (result == null) {
// Wait for the server to prepare archives
delay(1000)
result = request.executeAndParsingWith(ArchiveParser::parseArchiveUrl)
if (result == null) {
throw EhException("Archive unavailable")
}
}
return result
}
return null
}
private suspend fun getFunds(): HomeParser.Funds {
val url = EhUrl.URL_FUNDS
Log.d(TAG, url)
return EhRequestBuilder(url).executeAndParsingWith(HomeParser::parseFunds)
}
private suspend fun getImageLimitsInternal(): HomeParser.Limits {
val url = EhUrl.URL_HOME
Log.d(TAG, url)
return EhRequestBuilder(url).executeAndParsingWith(HomeParser::parse)
}
suspend fun getImageLimits(): HomeParser.Result = coroutineScope {
val limitsDeferred = async {
getImageLimitsInternal()
}
val fundsDeferred = async {
getFunds()
}
HomeParser.Result(limitsDeferred.await(), fundsDeferred.await())
}
suspend fun resetImageLimits(unlock: Boolean = false): HomeParser.Limits {
val builder = FormBody.Builder()
.add("reset_imagelimit", if (unlock) "Unlock Quota" else "Reset Quota")
val url = EhUrl.URL_HOME
Log.d(TAG, url)
return EhRequestBuilder(url)
.post(builder.build())
.executeAndParsingWith(HomeParser::parseResetLimits)
}
suspend fun getNews(parse: Boolean): String? {
val url = EhUrl.URL_NEWS
val referer = EhUrl.REFERER_E
Log.d(TAG, url)
return EhRequestBuilder(url, referer).executeAndParsingWith {
if (parse) EventPaneParser.parse(this) else null
}
}
private suspend fun getProfileInternal(
url: String,
referer: String,
): ProfileParser.Result {
Log.d(TAG, url)
return EhRequestBuilder(url, referer).executeAndParsingWith(ProfileParser::parse)
}
suspend fun getProfile(): ProfileParser.Result {
val url = EhUrl.URL_FORUMS
Log.d(TAG, url)
return getProfileInternal(
EhRequestBuilder(url).executeAndParsingWith(ForumsParser::parse),
url,
)
}
private suspend fun getUConfigInternal(url: String) {
Log.d(TAG, url)
EhRequestBuilder(url).executeAndParsingWith(UserConfigParser::parse)
}
suspend fun getUConfig(url: String = EhUrl.uConfigUrl) {
runCatching {
getUConfigInternal(url)
}.onFailure {
// It may get redirected when accessing ex for the first time
if (url == EhUrl.URL_UCONFIG_EX) {
it.printStackTrace()
getUConfigInternal(url)
} else {
throw it
}
}
}
suspend fun voteComment(
apiUid: Long,
apiKey: String?,
gid: Long,
token: String?,
commentId: Long,
commentVote: Int,
): VoteCommentParser.Result {
val json = JSONObject()
json.put("method", "votecomment")
json.put("apiuid", apiUid)
json.put("apikey", apiKey)
json.put("gid", gid)
json.put("token", token)
json.put("comment_id", commentId)
json.put("comment_vote", commentVote)
val requestBody: RequestBody = json.toString().toRequestBody(MEDIA_TYPE_JSON)
val url = EhUrl.apiUrl
val referer = EhUrl.referer
val origin = EhUrl.origin
Log.d(TAG, url)
return EhRequestBuilder(url, referer, origin)
.post(requestBody)
.executeAndParsingWith {
VoteCommentParser.parse(this, commentVote)
}
}
suspend fun voteTag(
apiUid: Long,
apiKey: String?,
gid: Long,
token: String?,
tags: String?,
vote: Int,
): Pair?> {
val json = JSONObject()
json.put("method", "taggallery")
json.put("apiuid", apiUid)
json.put("apikey", apiKey)
json.put("gid", gid)
json.put("token", token)
json.put("tags", tags)
json.put("vote", vote)
val requestBody: RequestBody = json.toString().toRequestBody(MEDIA_TYPE_JSON)
val url = EhUrl.apiUrl
val referer = EhUrl.referer
val origin = EhUrl.origin
Log.d(TAG, url)
return EhRequestBuilder(url, referer, origin)
.post(requestBody)
.executeAndParsingWith(VoteTagParser::parse)
}
/**
* @param image Must be jpeg
*/
suspend fun imageSearch(
image: File,
uss: Boolean,
osc: Boolean,
se: Boolean,
): GalleryListParser.Result {
val builder = MultipartBody.Builder()
builder.setType(MultipartBody.FORM)
builder.addPart(
Headers.headersOf(
"Content-Disposition",
"form-data; name=\"sfile\"; filename=\"a.jpg\"",
),
image.asRequestBody(MEDIA_TYPE_JPEG),
)
if (uss) {
builder.addPart(
Headers.headersOf("Content-Disposition", "form-data; name=\"fs_similar\""),
"on".toRequestBody(),
)
}
if (osc) {
builder.addPart(
Headers.headersOf("Content-Disposition", "form-data; name=\"fs_covers\""),
"on".toRequestBody(),
)
}
if (se) {
builder.addPart(
Headers.headersOf("Content-Disposition", "form-data; name=\"fs_exp\""),
"on".toRequestBody(),
)
}
builder.addPart(
Headers.headersOf("Content-Disposition", "form-data; name=\"f_sfile\""),
"File Search".toRequestBody(),
)
val url = EhUrl.imageSearchUrl
val referer = EhUrl.referer
val origin = EhUrl.origin
Log.d(TAG, url)
return EhRequestBuilder(url, referer, origin)
.post(builder.build())
.executeAndParsingWith(GalleryListParser::parse)
.apply { fillGalleryList(galleryInfoList, url, true) }
}
suspend fun getGalleryPage(
url: String?,
gid: Long,
token: String?,
): GalleryPageParser.Result {
val referer = EhUrl.getGalleryDetailUrl(gid, token)
Log.d(TAG, url!!)
return EhRequestBuilder(url, referer).executeAndParsingWith(GalleryPageParser::parse)
}
suspend fun getGalleryPageApi(
gid: Long,
index: Int,
pToken: String?,
showKey: String?,
previousPToken: String?,
): GalleryPageApiParser.Result {
val json = JSONObject()
json.put("method", "showpage")
json.put("gid", gid)
json.put("page", index + 1)
json.put("imgkey", pToken)
json.put("showkey", showKey)
val requestBody: RequestBody = json.toString().toRequestBody(MEDIA_TYPE_JSON)
val url = EhUrl.apiUrl
var referer: String? = null
if (index > 0 && previousPToken != null) {
referer = EhUrl.getPageUrl(gid, index - 1, previousPToken)
}
val origin = EhUrl.origin
Log.d(TAG, url)
return EhRequestBuilder(url, referer, origin)
.post(requestBody)
.executeAndParsingWith(GalleryPageApiParser::parse)
}
suspend fun getPTokenFromMultiPageViewer(
gid: Long,
token: String?,
sha1: Boolean = false,
): List {
val url = EhUrl.getGalleryMultiPageViewerUrl(gid, token!!, sha1)
val referer = EhUrl.getGalleryDetailUrl(gid, token)
val parser = if (sha1) GalleryMultiPageViewerParser::parseSha1 else GalleryMultiPageViewerParser::parsePToken
Log.d(TAG, url)
return EhRequestBuilder(url, referer).executeAndParsingWith(parser)
}
}
================================================
FILE: app/src/main/java/com/hippo/ehviewer/client/EhFilter.kt
================================================
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.client
import android.util.Log
import com.hippo.ehviewer.EhDB
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.dao.Filter
import java.util.Locale
import java.util.regex.Pattern
object EhFilter {
private val mTitleFilterList: MutableList = ArrayList()
private val mUploaderFilterList: MutableList = ArrayList()
private val mTagFilterList: MutableList = ArrayList()
private val mTagNamespaceFilterList: MutableList = ArrayList()
private val mCommenterFilterList: MutableList = ArrayList()
private val mCommentFilterList: MutableList = ArrayList()
private const val MODE_TITLE = 0
const val MODE_UPLOADER = 1
const val MODE_TAG = 2
private const val MODE_TAG_NAMESPACE = 3
const val MODE_COMMENTER = 4
private const val MODE_COMMENT = 5
private val TAG = EhFilter::class.java.simpleName
init {
val list = EhDB.allFilter
var i = 0
val n = list.size
while (i < n) {
val filter = list[i]
when (filter.mode) {
MODE_TITLE -> {
filter.text = filter.text!!.lowercase(Locale.getDefault())
mTitleFilterList.add(filter)
}
MODE_TAG -> {
filter.text = filter.text!!.lowercase(Locale.getDefault())
mTagFilterList.add(filter)
}
MODE_TAG_NAMESPACE -> {
filter.text = filter.text!!.lowercase(Locale.getDefault())
mTagNamespaceFilterList.add(filter)
}
MODE_UPLOADER -> mUploaderFilterList.add(filter)
MODE_COMMENTER -> mCommenterFilterList.add(filter)
MODE_COMMENT -> mCommentFilterList.add(filter)
else -> Log.d(TAG, "Unknown mode: " + filter.mode)
}
i++
}
}
val titleFilterList: List
get() = mTitleFilterList
val uploaderFilterList: List
get() = mUploaderFilterList
val tagFilterList: List
get() = mTagFilterList
val tagNamespaceFilterList: List
get() = mTagNamespaceFilterList
val commenterFilterList: List
get() = mCommenterFilterList
val commentFilterList: List
get() = mCommentFilterList
fun applyTranslation(filter: Filter): String? {
var text = filter.text ?: return null
if (EhTagDatabase.isInitialized()) {
when (filter.mode) {
MODE_TAG_NAMESPACE -> {
EhTagDatabase.getTranslation(tag = text)?.let {
text = "$text ($it)"
}
}
MODE_TAG -> {
val index = text.indexOf(':')
if (index > 0) {
EhTagDatabase.getTranslation(
EhTagDatabase.namespaceToPrefix(text.substring(0, index)),
text.substring(index + 1),
)?.let {
text = "$text ($it)"
}
}
}
}
}
return text
}
@Synchronized
fun addFilter(filter: Filter): Boolean {
// enable filter by default before it is added to database
filter.enable = true
if (!EhDB.addFilter(filter)) return false
when (filter.mode) {
MODE_TITLE -> {
filter.text = filter.text!!.lowercase(Locale.getDefault())
mTitleFilterList.add(filter)
}
MODE_TAG -> {
filter.text = filter.text!!.lowercase(Locale.getDefault())
mTagFilterList.add(filter)
}
MODE_TAG_NAMESPACE -> {
filter.text = filter.text!!.lowercase(Locale.getDefault())
mTagNamespaceFilterList.add(filter)
}
MODE_UPLOADER -> mUploaderFilterList.add(filter)
MODE_COMMENTER -> mCommenterFilterList.add(filter)
MODE_COMMENT -> mCommentFilterList.add(filter)
else -> Log.d(TAG, "Unknown mode: " + filter.mode)
}
return true
}
@Synchronized
fun triggerFilter(filter: Filter) {
EhDB.triggerFilter(filter)
}
@Synchronized
fun deleteFilter(filter: Filter) {
EhDB.deleteFilter(filter)
when (filter.mode) {
MODE_TITLE -> mTitleFilterList.remove(filter)
MODE_TAG -> mTagFilterList.remove(filter)
MODE_TAG_NAMESPACE -> mTagNamespaceFilterList.remove(filter)
MODE_UPLOADER -> mUploaderFilterList.remove(filter)
MODE_COMMENTER -> mCommenterFilterList.remove(filter)
MODE_COMMENT -> mCommentFilterList.remove(filter)
else -> Log.d(TAG, "Unknown mode: " + filter.mode)
}
}
@Synchronized
fun needTags(): Boolean = mTagFilterList.isNotEmpty() || mTagNamespaceFilterList.isNotEmpty()
@Synchronized
fun filterTitle(info: GalleryInfo?): Boolean {
if (null == info) {
return false
}
// Title
val title = info.title
val filters: List = mTitleFilterList
if (null != title && filters.isNotEmpty()) {
var i = 0
val n = filters.size
while (i < n) {
if (filters[i].enable!! &&
title.lowercase(Locale.getDefault()).contains(
filters[i].text!!,
)
) {
return false
}
i++
}
}
return true
}
@Synchronized
fun filterUploader(info: GalleryInfo?): Boolean {
if (null == info) {
return false
}
// Uploader
val uploader = info.uploader
val filters: List = mUploaderFilterList
if (null != uploader && filters.isNotEmpty()) {
var i = 0
val n = filters.size
while (i < n) {
if (filters[i].enable!! && uploader == filters[i].text) {
return false
}
i++
}
}
return true
}
private fun matchTag(tag: String?, filter: String?): Boolean {
if (null == tag || null == filter) {
return false
}
val tagNamespace: String?
val tagName: String
val filterNamespace: String?
val filterName: String
var index = tag.indexOf(':')
if (index < 0) {
tagNamespace = null
tagName = tag
} else {
tagNamespace = tag.substring(0, index)
tagName = tag.substring(index + 1)
}
index = filter.indexOf(':')
if (index < 0) {
filterNamespace = null
filterName = filter
} else {
filterNamespace = filter.substring(0, index)
filterName = filter.substring(index + 1)
}
return if (null != tagNamespace &&
null != filterNamespace &&
tagNamespace != filterNamespace
) {
false
} else {
tagName == filterName
}
}
@Synchronized
fun filterTag(info: GalleryInfo?): Boolean {
if (null == info) {
return false
}
// Tag
val tags = info.simpleTags
val filters: List = mTagFilterList
if (null != tags && filters.isNotEmpty()) {
for (tag in tags) {
var i = 0
val n = filters.size
while (i < n) {
if (filters[i].enable!! && matchTag(tag, filters[i].text)) {
return false
}
i++
}
}
}
return true
}
private fun matchTagNamespace(tag: String?, filter: String?): Boolean {
if (null == tag || null == filter) {
return false
}
val tagNamespace: String
val index = tag.indexOf(':')
return if (index >= 0) {
tagNamespace = tag.substring(0, index)
tagNamespace == filter
} else {
false
}
}
@Synchronized
fun filterTagNamespace(info: GalleryInfo?): Boolean {
if (null == info) {
return false
}
val tags = info.simpleTags
val filters: List = mTagNamespaceFilterList
if (null != tags && filters.isNotEmpty()) {
for (tag in tags) {
var i = 0
val n = filters.size
while (i < n) {
if (filters[i].enable!! && matchTagNamespace(tag, filters[i].text)) {
return false
}
i++
}
}
}
return true
}
@Synchronized
fun filterCommenter(commenter: String?): Boolean {
if (null == commenter) {
return false
}
val filters: List = mCommenterFilterList
if (filters.isNotEmpty()) {
var i = 0
val n = filters.size
while (i < n) {
if (filters[i].enable!! && commenter == filters[i].text) {
return false
}
i++
}
}
return true
}
@Synchronized
fun filterComment(comment: String?): Boolean {
if (null == comment) {
return false
}
val filters: List = mCommentFilterList
if (filters.isNotEmpty()) {
var i = 0
val n = filters.size
while (i < n) {
if (filters[i].enable!!) {
val p = Pattern.compile(filters[i].text!!)
val m = p.matcher(comment)
if (m.find()) return false
}
i++
}
}
return true
}
}
================================================
FILE: app/src/main/java/com/hippo/ehviewer/client/EhRequest.kt
================================================
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.client
import android.app.Activity
import androidx.annotation.MainThread
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
class EhRequest {
internal var job: Job? = null
val isActive
get() = job?.isActive == true
var method = 0
private set
var args: Array? = null
private set
var callback: EhClient.Callback? = null
private set
fun setMethod(method: Int): EhRequest {
this.method = method
return this
}
fun setArgs(vararg args: Any?): EhRequest {
this.args = args
return this
}
@Suppress("UNCHECKED_CAST")
fun setCallback(callback: EhClient.Callback<*>?): EhRequest {
this.callback = callback as EhClient.Callback?
return this
}
fun enqueue(scope: CoroutineScope) {
EhClient.enqueue(this, scope)
}
@DelicateCoroutinesApi
fun enqueue() {
EhClient.enqueue(this, GlobalScope)
}
fun enqueue(fragment: Fragment) {
enqueue(fragment.viewLifecycleOwner.lifecycleScope)
}
fun enqueue(activity: Activity) {
check(activity is FragmentActivity)
enqueue(activity.lifecycleScope)
}
@MainThread
fun cancel() {
if (isActive) {
job?.cancel()
callback?.onCancel()
}
}
}
================================================
FILE: app/src/main/java/com/hippo/ehviewer/client/EhRequestBuilder.kt
================================================
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.client
import com.hippo.ehviewer.EhApplication.Companion.noRedirectOkHttpClient
import com.hippo.okhttp.ChromeRequestBuilder
import okhttp3.Request
import okhttp3.Response
import okhttp3.coroutines.executeAsync
class EhRequestBuilder(
url: String,
referer: String? = null,
origin: String? = null,
) : ChromeRequestBuilder(url) {
init {
referer?.let { addHeader("Referer", it) }
origin?.let { addHeader("Origin", it) }
}
}
suspend inline fun Request.executeNoRedirect(block: Response.() -> R) = noRedirectOkHttpClient.newCall(this).executeAsync().use(block)
================================================
FILE: app/src/main/java/com/hippo/ehviewer/client/EhTagDatabase.kt
================================================
/*
* Copyright 2019 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.client
import android.content.Context
import com.hippo.ehviewer.AppConfig
import com.hippo.ehviewer.EhApplication
import com.hippo.ehviewer.EhApplication.Companion.nonCacheOkHttpClient
import com.hippo.ehviewer.R
import com.hippo.ehviewer.Settings
import com.hippo.unifile.UniFile
import com.hippo.unifile.sha1
import com.hippo.yorozuya.FileUtils
import com.hippo.yorozuya.copyToFile
import java.io.File
import java.io.IOException
import java.nio.charset.StandardCharsets
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import okhttp3.Request
import okhttp3.coroutines.executeAsync
import okio.BufferedSource
import okio.buffer
import okio.source
import org.json.JSONException
import org.json.JSONObject
private typealias TagGroup = Map
private typealias TagGroups = Map
object EhTagDatabase {
private const val NAMESPACE_PREFIX = "n"
private const val UPDATE_INTERVAL = 3 * 24 * 3600 * 1000
const val TYPE_EQUAL = 0
const val TYPE_START = 1
const val TYPE_CONTAIN = 2
private lateinit var tagGroups: TagGroups
private val dir = AppConfig.getFilesDir("tag-translations")
private val urls = getMetadata(EhApplication.application)
private val sha1Name = urls?.get(0)!!
private val sha1Url = urls?.get(1)!!
private val dataName = urls?.get(2)!!
private val dataUrl = urls?.get(3)!!
private val updateLock = Mutex()
fun isInitialized(): Boolean = 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 {
tagGroups = JSONObject(source.readString(StandardCharsets.UTF_8)).toTagGroups()
} catch (e: JSONException) {
e.printStackTrace()
}
}
fun getTranslation(prefix: String? = NAMESPACE_PREFIX, tag: String?): String? = tagGroups[prefix]?.get(tag)?.trim()?.takeIf { it.isNotEmpty() }
private fun internalSuggestFlow(
tags: Map,
keyword: String,
translate: Boolean,
type: Int,
): Flow> = flow {
when (type) {
TYPE_EQUAL -> {
if (translate) {
tags.forEach { (tag, hint) ->
if (tag.equalsIgnoreSpace(keyword) || hint.equalsIgnoreSpace(keyword)) {
emit(Pair(hint, tag))
}
}
} else {
tags[keyword]?.let {
emit(Pair(null, keyword))
}
}
}
TYPE_START -> {
if (translate) {
tags.forEach { (tag, hint) ->
if (!tag.equalsIgnoreSpace(keyword) &&
!hint.equalsIgnoreSpace(keyword) &&
(tag.startsWithIgnoreSpace(keyword) || hint.startsWithIgnoreSpace(keyword))
) {
emit(Pair(hint, tag))
}
}
} else {
tags.keys.forEach { tag ->
if (!tag.equalsIgnoreSpace(keyword) && tag.startsWithIgnoreSpace(keyword)) {
emit(Pair(null, tag))
}
}
}
}
TYPE_CONTAIN -> {
if (translate) {
tags.forEach { (tag, hint) ->
if (!tag.equalsIgnoreSpace(keyword) &&
!hint.equalsIgnoreSpace(keyword) &&
!tag.startsWithIgnoreSpace(keyword) &&
!hint.startsWithIgnoreSpace(keyword) &&
(tag.containsIgnoreSpace(keyword) || hint.containsIgnoreSpace(keyword))
) {
emit(Pair(hint, tag))
}
}
} else {
tags.keys.forEach { tag ->
if (!tag.equalsIgnoreSpace(keyword) &&
!tag.startsWithIgnoreSpace(keyword) &&
tag.containsIgnoreSpace(keyword)
) {
emit(Pair(null, tag))
}
}
}
}
}
}
/* Construct a cold flow for tag database suggestions */
fun suggestFlow(
keyword: String,
translate: Boolean,
type: Int,
): Flow> = flow {
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, type).collect { (hint, tag) ->
emit(Pair(hint, "$prefix:$tag"))
}
} ?: tagGroups.forEach { (prefix, tags) ->
internalSuggestFlow(tags, keyword, translate, type).collect { (hint, tag) ->
emit(Pair(hint, if (prefix == NAMESPACE_PREFIX) "$tag:" else "$prefix:$tag"))
}
}
}
private fun String.removeSpace(): String = replace(" ", "")
private fun String.containsIgnoreSpace(other: String, ignoreCase: Boolean = true): Boolean = removeSpace().contains(other.removeSpace(), ignoreCase)
private fun String.equalsIgnoreSpace(other: String, ignoreCase: Boolean = true): Boolean = removeSpace().equals(other.removeSpace(), ignoreCase)
private fun String.startsWithIgnoreSpace(other: String, ignoreCase: Boolean = true): Boolean = removeSpace().startsWith(other.removeSpace(), ignoreCase)
private val NAMESPACE_TO_PREFIX = HashMap().also {
it["artist"] = "a"
it["character"] = "c"
it["cosplayer"] = "cos"
it["female"] = "f"
it["group"] = "g"
it["language"] = "l"
it["location"] = "loc"
it["male"] = "m"
it["mixed"] = "x"
it["other"] = "o"
it["parody"] = "p"
it["reclass"] = "r"
}
fun namespaceToPrefix(namespace: String): String? = NAMESPACE_TO_PREFIX[namespace]
private fun getMetadata(context: Context): Array? = context.resources.getStringArray(R.array.tag_translation_metadata)
.takeIf { it.size == 4 }
fun isTranslatable(context: Context): Boolean = context.resources.getBoolean(R.bool.tag_translatable)
private fun getFileContent(file: File): String? = runCatching {
file.source().buffer().use { it.readString(StandardCharsets.UTF_8) }
}.getOrNull()
private fun checkData(sha1: String?, data: File): Boolean = sha1 != null && sha1 == UniFile.fromFile(data)?.sha1()
private suspend fun save(url: String, file: File): Boolean {
val request: Request = Request.Builder().url(url).build()
val call = nonCacheOkHttpClient.newCall(request)
runCatching {
call.executeAsync().use { response ->
if (!response.isSuccessful) {
return false
}
response.body.use {
it.copyToFile(file)
}
return true
}
}.onFailure {
file.delete()
it.printStackTrace()
}
return false
}
suspend fun read() {
if (urls != null && !isInitialized()) {
runCatching {
checkNotNull(dir)
val sha1File = File(dir, sha1Name)
val dataFile = File(dir, dataName)
// Check current sha1 and current data
val sha1 = getFileContent(sha1File)
if (!checkData(sha1, dataFile)) {
FileUtils.delete(sha1File)
FileUtils.delete(dataFile)
Settings.putTranslationsLastUpdate(-1)
}
// Read current EhTagDatabase
if (dataFile.exists()) {
try {
dataFile.source().buffer().use { updateData(it) }
} catch (_: IOException) {
FileUtils.delete(sha1File)
FileUtils.delete(dataFile)
Settings.putTranslationsLastUpdate(-1)
}
}
}.onFailure {
it.printStackTrace()
}
update()
}
}
suspend fun update(force: Boolean = false) {
val time = System.currentTimeMillis()
if (urls != null && (force || time - Settings.translationsLastUpdate > UPDATE_INTERVAL)) {
updateLock.withLock {
runCatching {
checkNotNull(dir)
val sha1File = File(dir, sha1Name)
val dataFile = File(dir, dataName)
// Save new sha1
val tempSha1File = File(dir, "$sha1Name.tmp")
check(save(sha1Url, tempSha1File))
val tempSha1 = getFileContent(tempSha1File)
// Check new sha1 and current sha1
if (tempSha1 == getFileContent(sha1File)) {
// The data is the same
FileUtils.delete(tempSha1File)
return@runCatching
}
// Save new data
val tempDataFile = File(dir, "$dataName.tmp")
check(save(dataUrl, tempDataFile))
// Check new sha1 and new data
if (!checkData(tempSha1, tempDataFile)) {
FileUtils.delete(tempSha1File)
FileUtils.delete(tempDataFile)
return@runCatching
}
// Replace current sha1 and current data with new sha1 and new data
FileUtils.delete(sha1File)
FileUtils.delete(dataFile)
tempSha1File.renameTo(sha1File)
tempDataFile.renameTo(dataFile)
// Read new EhTagDatabase
try {
dataFile.source().buffer().use { updateData(it) }
Settings.putTranslationsLastUpdate(time)
} catch (_: IOException) {
}
}.onFailure {
it.printStackTrace()
}
}
}
}
}
================================================
FILE: app/src/main/java/com/hippo/ehviewer/client/EhUrl.kt
================================================
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.client
import com.hippo.ehviewer.Settings
import com.hippo.network.UrlBuilder
object EhUrl {
const val SITE_E = 0
const val SITE_EX = 1
const val DOMAIN_E = "e-hentai.org"
const val DOMAIN_EX = "exhentai.org"
const val DOMAIN_LOFI = "lofi.e-hentai.org"
const val HOST_E = "https://$DOMAIN_E/"
const val HOST_EX = "https://$DOMAIN_EX/"
private const val API_E = "https://api.e-hentai.org/api.php"
private const val API_EX = "https://s.exhentai.org/api.php"
private const val URL_FAVORITES_E = HOST_E + "favorites.php"
private const val URL_FAVORITES_EX = HOST_EX + "favorites.php"
private const val URL_IMAGE_SEARCH_E = "https://upload.e-hentai.org/image_lookup.php"
private const val URL_IMAGE_SEARCH_EX = "https://exhentai.org/upload/image_lookup.php"
private const val URL_MY_TAGS_E = HOST_E + "mytags"
private const val URL_MY_TAGS_EX = HOST_EX + "mytags"
private const val URL_POPULAR_E = HOST_E + "popular"
private const val URL_POPULAR_EX = HOST_EX + "popular"
private const val URL_WATCHED_E = HOST_E + "watched"
private const val URL_WATCHED_EX = HOST_EX + "watched"
const val URL_UCONFIG_E = HOST_E + "uconfig.php"
const val URL_UCONFIG_EX = HOST_EX + "uconfig.php"
const val URL_FORUMS = "https://forums.e-hentai.org/"
const val URL_SIGN_IN = URL_FORUMS + "index.php?act=Login"
const val URL_REGISTER = URL_FORUMS + "index.php?act=Reg&CODE=00"
const val API_SIGN_IN = "$URL_SIGN_IN&CODE=01"
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_E = "https://$DOMAIN_E"
private const val REFERER_EX = "https://$DOMAIN_EX"
private const val ORIGIN_E = REFERER_E
private const val ORIGIN_EX = REFERER_EX
val host: String
get() = when (Settings.gallerySite) {
SITE_E -> HOST_E
SITE_EX -> HOST_EX
else -> HOST_E
}
val favoritesUrl: String
get() = when (Settings.gallerySite) {
SITE_E -> URL_FAVORITES_E
SITE_EX -> URL_FAVORITES_EX
else -> URL_FAVORITES_E
}
val apiUrl: String
get() = when (Settings.gallerySite) {
SITE_E -> API_E
SITE_EX -> API_EX
else -> API_E
}
val referer: String
get() = when (Settings.gallerySite) {
SITE_E -> REFERER_E
SITE_EX -> REFERER_EX
else -> REFERER_E
}
val origin: String
get() = when (Settings.gallerySite) {
SITE_E -> ORIGIN_E
SITE_EX -> ORIGIN_EX
else -> ORIGIN_E
}
val uConfigUrl: String
get() = when (Settings.gallerySite) {
SITE_E -> URL_UCONFIG_E
SITE_EX -> URL_UCONFIG_EX
else -> URL_UCONFIG_E
}
val myTagsUrl: String
get() = when (Settings.gallerySite) {
SITE_E -> URL_MY_TAGS_E
SITE_EX -> URL_MY_TAGS_EX
else -> URL_MY_TAGS_E
}
val popularUrl: String
get() = when (Settings.gallerySite) {
SITE_E -> URL_POPULAR_E
SITE_EX -> URL_POPULAR_EX
else -> URL_POPULAR_E
}
val imageSearchUrl: String
get() = when (Settings.gallerySite) {
SITE_E -> URL_IMAGE_SEARCH_E
SITE_EX -> URL_IMAGE_SEARCH_EX
else -> URL_IMAGE_SEARCH_E
}
val watchedUrl: String
get() = when (Settings.gallerySite) {
SITE_E -> URL_WATCHED_E
SITE_EX -> URL_WATCHED_EX
else -> URL_WATCHED_E
}
fun getGalleryDetailUrl(
gid: Long,
token: String?,
index: Int = 0,
allComment: Boolean = false,
sha1: Boolean = false,
): String {
val builder = UrlBuilder(host + "g/" + gid + '/' + token + '/')
if (index > 0) builder.addQuery("p", index)
if (allComment) builder.addQuery("hc", 1)
if (sha1) builder.addQuery("datatags", 1)
return builder.build()
}
fun getGalleryMultiPageViewerUrl(gid: Long, token: String, sha1: Boolean = false): String {
val builder = UrlBuilder(host + "mpv/" + gid + '/' + token + '/')
if (sha1) builder.addQuery("datatags", 1)
return builder.build()
}
fun getPageUrl(gid: Long, index: Int, pToken: String): String = host + "s/" + pToken + '/' + gid + '-' + (index + 1)
fun getAddFavorites(gid: Long, token: String?): String = host + "gallerypopups.php?gid=" + gid + "&t=" + token + "&act=addfav"
fun getDownloadArchive(gid: Long, token: String?): String = host + "archiver.php?gid=" + gid + "&token=" + token
fun getTagDefinitionUrl(tag: String): String = "https://ehwiki.org/wiki/" + tag.replace(' ', '_')
}
================================================
FILE: app/src/main/java/com/hippo/ehviewer/client/EhUrlOpener.kt
================================================
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.client
import android.os.Bundle
import android.text.TextUtils
import android.util.Log
import com.hippo.ehviewer.client.parser.GalleryDetailUrlParser
import com.hippo.ehviewer.client.parser.GalleryListUrlParser
import com.hippo.ehviewer.client.parser.GalleryPageUrlParser
import com.hippo.ehviewer.ui.scene.GalleryDetailScene
import com.hippo.ehviewer.ui.scene.GalleryListScene
import com.hippo.ehviewer.ui.scene.ProgressScene
import com.hippo.scene.Announcer
object EhUrlOpener {
private val TAG = EhUrlOpener::class.java.simpleName
fun parseUrl(url: String): Announcer? {
if (TextUtils.isEmpty(url)) {
return null
}
val listUrlBuilder = GalleryListUrlParser.parse(url)
if (listUrlBuilder != null) {
val args = Bundle()
args.putString(GalleryListScene.KEY_ACTION, GalleryListScene.ACTION_LIST_URL_BUILDER)
args.putParcelable(GalleryListScene.KEY_LIST_URL_BUILDER, listUrlBuilder)
return Announcer(GalleryListScene::class.java).setArgs(args)
}
val result1 = GalleryDetailUrlParser.parse(url)
if (result1 != null) {
val args = Bundle()
args.putString(GalleryDetailScene.KEY_ACTION, GalleryDetailScene.ACTION_GID_TOKEN)
args.putLong(GalleryDetailScene.KEY_GID, result1.gid)
args.putString(GalleryDetailScene.KEY_TOKEN, result1.token)
return Announcer(GalleryDetailScene::class.java).setArgs(args)
}
val result2 = GalleryPageUrlParser.parse(url)
if (result2 != null) {
val args = Bundle()
args.putString(ProgressScene.KEY_ACTION, ProgressScene.ACTION_GALLERY_TOKEN)
args.putLong(ProgressScene.KEY_GID, result2.gid)
args.putString(ProgressScene.KEY_PTOKEN, result2.pToken)
args.putInt(ProgressScene.KEY_PAGE, result2.page)
return Announcer(ProgressScene::class.java).setArgs(args)
}
Log.i(TAG, "Can't parse url: $url")
return null
}
}
================================================
FILE: app/src/main/java/com/hippo/ehviewer/client/EhUtils.kt
================================================
/*
* 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.Settings
import com.hippo.ehviewer.client.EhUrl.host
import com.hippo.ehviewer.client.data.GalleryInfo
import java.util.regex.Pattern
import okhttp3.HttpUrl.Companion.toHttpUrl
object EhUtils {
const val NONE = -1 // Use it for homepage
const val MISC = 0x1
const val DOUJINSHI = 0x2
const val MANGA = 0x4
const val ARTIST_CG = 0x8
const val GAME_CG = 0x10
const val IMAGE_SET = 0x20
const val COSPLAY = 0x40
const val ASIAN_PORN = 0x80
const val NON_H = 0x100
const val WESTERN = 0x200
const val ALL_CATEGORY = 0x3ff
const val PRIVATE = 0x400
const val UNKNOWN = 0x800
// 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(
MISC to arrayOf("misc"),
DOUJINSHI to arrayOf("doujinshi"),
MANGA to arrayOf("manga"),
ARTIST_CG to arrayOf("artistcg", "Artist CG Sets", "Artist CG"),
GAME_CG to arrayOf("gamecg", "Game CG Sets", "Game CG"),
IMAGE_SET to arrayOf("imageset", "Image Sets", "Image Set"),
COSPLAY to arrayOf("cosplay"),
ASIAN_PORN to arrayOf("asianporn", "Asian Porn"),
NON_H to arrayOf("non-h"),
WESTERN to arrayOf("western"),
PRIVATE to arrayOf("private"),
UNKNOWN to arrayOf("unknown"),
)
private val CATEGORY_STRINGS = CATEGORY_VALUES.entries.map { (k, v) -> v to k }
val isExHentai: Boolean
get() = Settings.gallerySite == EhUrl.SITE_EX
val isMPVAvailable: Boolean
get() = EhCookieStore.getCookieValue(
host.toHttpUrl(),
EhCookieStore.KEY_HATH_PERKS,
)?.substringBefore("-")?.contains("q") == true
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 = CATEGORY_VALUES[type]?.let { it[0] } ?: CATEGORY_VALUES[UNKNOWN]!![0]
fun getCategoryColor(category: Int): Int = when (category) {
DOUJINSHI -> BG_COLOR_DOUJINSHI
MANGA -> BG_COLOR_MANGA
ARTIST_CG -> BG_COLOR_ARTIST_CG
GAME_CG -> BG_COLOR_GAME_CG
WESTERN -> BG_COLOR_WESTERN
NON_H -> BG_COLOR_NON_H
IMAGE_SET -> BG_COLOR_IMAGE_SET
COSPLAY -> BG_COLOR_COSPLAY
ASIAN_PORN -> BG_COLOR_ASIAN_PORN
MISC -> BG_COLOR_MISC
else -> BG_COLOR_UNKNOWN
}.toInt()
suspend fun signOut() {
EhCookieStore.clear()
Settings.putAvatar(null)
Settings.putDisplayName(null)
Settings.putGallerySite(EhUrl.SITE_E)
Settings.putNeedSignIn(true)
Settings.putSelectSite(true)
}
fun needSignedIn(): Boolean = Settings.needSignIn
fun getSuitableTitle(gi: GalleryInfo): String = if (Settings.showJpnTitle) {
if (TextUtils.isEmpty(gi.titleJpn)) gi.title else gi.titleJpn
} else {
if (TextUtils.isEmpty(gi.title)) gi.titleJpn else gi.title
}.orEmpty()
fun extractTitle(fullTitle: String?): String? {
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 }
}
}
================================================
FILE: app/src/main/java/com/hippo/ehviewer/client/data/AbstractGalleryInfo.kt
================================================
/*
* Copyright 2023 Moedog
*
* 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 .
*/
package com.hippo.ehviewer.client.data
interface AbstractGalleryInfo {
var gid: Long
var token: String?
var title: String?
var titleJpn: String?
var thumb: String?
var category: Int
var posted: String?
var uploader: String?
var disowned: Boolean
var rating: Float
var rated: Boolean
var simpleTags: Array?
var pages: Int
var thumbWidth: Int
var thumbHeight: Int
var spanSize: Int
var spanIndex: Int
var spanGroupIndex: Int
var simpleLanguage: String?
var favoriteSlot: Int
var favoriteName: String?
var favoriteNote: String?
}
================================================
FILE: app/src/main/java/com/hippo/ehviewer/client/data/BaseGalleryInfo.kt
================================================
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.client.data
import androidx.room.ColumnInfo
import androidx.room.Ignore
import androidx.room.PrimaryKey
import kotlinx.parcelize.Parcelize
@Parcelize
open class BaseGalleryInfo(
@PrimaryKey
@ColumnInfo(name = "GID")
override var gid: Long = 0,
@ColumnInfo(name = "TOKEN")
override var token: String? = null,
@ColumnInfo(name = "TITLE")
override var title: String? = null,
@ColumnInfo(name = "TITLE_JPN")
override var titleJpn: String? = null,
@ColumnInfo(name = "THUMB")
override var thumb: String? = null,
@ColumnInfo(name = "CATEGORY")
override var category: Int = 0,
@ColumnInfo(name = "POSTED")
override var posted: String? = null,
@ColumnInfo(name = "UPLOADER")
override var uploader: String? = null,
@Ignore
override var disowned: Boolean = false,
@ColumnInfo(name = "RATING")
override var rating: Float = 0f,
@Ignore
override var rated: Boolean = false,
@Ignore
override var simpleTags: Array? = null,
@Ignore
override var pages: Int = 0,
@Ignore
override var thumbWidth: Int = 0,
@Ignore
override var thumbHeight: Int = 0,
@Ignore
override var spanSize: Int = 0,
@Ignore
override var spanIndex: Int = 0,
@Ignore
override var spanGroupIndex: Int = 0,
@ColumnInfo(name = "SIMPLE_LANGUAGE")
override var simpleLanguage: String? = null,
@Ignore
override var favoriteSlot: Int = -2,
@Ignore
override var favoriteName: String? = null,
@Ignore
override var favoriteNote: String? = null,
) : GalleryInfo
================================================
FILE: app/src/main/java/com/hippo/ehviewer/client/data/FavListUrlBuilder.kt
================================================
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.client.data
import android.os.Parcelable
import com.hippo.ehviewer.client.EhUrl
import com.hippo.network.UrlBuilder
import com.hippo.util.encodeUTF8
import kotlinx.parcelize.Parcelize
@Parcelize
class FavListUrlBuilder(
private var mPrev: String? = null,
private var mNext: String? = null,
var jumpTo: String? = null,
var keyword: String? = null,
var favCat: Int = FAV_CAT_ALL,
) : Parcelable {
fun setIndex(index: String?, isNext: Boolean) {
mNext = index.takeIf { isNext }
mPrev = index.takeUnless { isNext }
}
fun build(): String {
val ub = UrlBuilder(EhUrl.favoritesUrl)
if (isValidFavCat(favCat)) {
ub.addQuery("favcat", favCat.toString())
} else if (favCat == FAV_CAT_ALL) {
ub.addQuery("favcat", "all")
}
keyword?.takeIf { it.isNotBlank() }?.let { ub.addQuery("f_search", encodeUTF8(it)) }
mPrev?.takeIf { it.isNotEmpty() }?.let { ub.addQuery("prev", it) }
mNext?.takeIf { it.isNotEmpty() }?.let { ub.addQuery("next", it) }
jumpTo?.takeIf { it.isNotEmpty() }?.let { ub.addQuery("seek", it) }
return ub.build()
}
companion object {
const val FAV_CAT_ALL = -1
const val FAV_CAT_LOCAL = -2
fun isValidFavCat(favCat: Int): Boolean = favCat in 0..9
}
}
================================================
FILE: app/src/main/java/com/hippo/ehviewer/client/data/GalleryComment.kt
================================================
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.client.data
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Parcelize
class GalleryComment(
// 0 for uploader comment. can't vote
var id: Long = 0,
var score: Int = 0,
var editable: Boolean = false,
var voteUpAble: Boolean = false,
var voteUpEd: Boolean = false,
var voteDownAble: Boolean = false,
var voteDownEd: Boolean = false,
var uploader: Boolean = false,
var voteState: String? = null,
var time: Long = 0,
var user: String? = null,
var comment: String? = null,
var lastEdited: Long = 0,
) : Parcelable
================================================
FILE: app/src/main/java/com/hippo/ehviewer/client/data/GalleryCommentList.kt
================================================
/*
* Copyright 2019 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.client.data
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Parcelize
class GalleryCommentList(
var comments: Array?,
var hasMore: Boolean,
) : Parcelable
================================================
FILE: app/src/main/java/com/hippo/ehviewer/client/data/GalleryDetail.kt
================================================
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.client.data
import com.hippo.ehviewer.client.data.GalleryInfo.Companion.S_LANGS
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
@Parcelize
class GalleryDetail(
val galleryInfo: GalleryInfo = BaseGalleryInfo(),
var apiUid: Long = -1L,
var apiKey: String? = null,
var torrentCount: Int = 0,
var torrentUrl: String? = null,
var archiveUrl: String? = null,
var parent: String? = null,
var newerVersions: ArrayList = arrayListOf(),
var visible: String? = null,
var language: String? = null,
var size: String? = null,
var favoriteCount: Int = 0,
var isFavorited: Boolean = false,
var ratingCount: Int = 0,
var tags: Array? = null,
var comments: GalleryCommentList? = null,
var previewPages: Int = 0,
@IgnoredOnParcel
var previewSet: PreviewSet? = null,
) : 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",
)
}
}
================================================
FILE: app/src/main/java/com/hippo/ehviewer/client/data/GalleryInfo.kt
================================================
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.client.data
import android.os.Parcelable
import java.util.regex.Pattern
interface GalleryInfo :
AbstractGalleryInfo,
Parcelable {
fun generateSLang() {
simpleLanguage = simpleTags?.let { generateSLangFromTags(it) }
?: title?.let { generateSLangFromTitle(it) }
}
private fun generateSLangFromTags(simpleTags: Array): String? {
for (tag in simpleTags) {
for (i in S_LANGS.indices) {
if (S_LANG_TAGS[i] == tag) {
return S_LANGS[i]
}
}
}
return null
}
private fun generateSLangFromTitle(title: String): String? {
for (i in S_LANGS.indices) {
if (S_LANG_PATTERNS[i].matcher(title).find()) {
return S_LANGS[i]
}
}
return null
}
companion object {
/**
* ISO 639-1
*/
private const val S_LANG_JA = "JA"
private const val S_LANG_EN = "EN"
private const val S_LANG_ZH = "ZH"
private const val S_LANG_NL = "NL"
private const val S_LANG_FR = "FR"
private const val S_LANG_DE = "DE"
private const val S_LANG_HU = "HU"
private const val S_LANG_IT = "IT"
private const val S_LANG_KO = "KO"
private const val S_LANG_PL = "PL"
private const val S_LANG_PT = "PT"
private const val S_LANG_RU = "RU"
private const val S_LANG_ES = "ES"
private const val S_LANG_TH = "TH"
private const val S_LANG_VI = "VI"
val S_LANGS = arrayOf(
S_LANG_EN,
S_LANG_ZH,
S_LANG_ES,
S_LANG_KO,
S_LANG_RU,
S_LANG_FR,
S_LANG_PT,
S_LANG_TH,
S_LANG_DE,
S_LANG_IT,
S_LANG_VI,
S_LANG_PL,
S_LANG_HU,
S_LANG_NL,
)
val S_LANG_PATTERNS = arrayOf(
Pattern.compile(
"[(\\[]eng(?:lish)?[)\\]]|英訳",
Pattern.CASE_INSENSITIVE,
),
// [((\[]ch(?:inese)?[))\]]|[汉漢]化|中[国國][语語]|中文|中国翻訳
Pattern.compile(
"[(\uFF08\\[]ch(?:inese)?[)\uFF09\\]]|[汉漢]化|中[国國][语語]|中文|中国翻訳",
Pattern.CASE_INSENSITIVE,
),
Pattern.compile(
"[(\\[]spanish[)\\]]|[(\\[]Español[)\\]]|スペイン翻訳",
Pattern.CASE_INSENSITIVE,
),
Pattern.compile("[(\\[]korean?[)\\]]|韓国翻訳", Pattern.CASE_INSENSITIVE),
Pattern.compile("[(\\[]rus(?:sian)?[)\\]]|ロシア翻訳", Pattern.CASE_INSENSITIVE),
Pattern.compile("[(\\[]fr(?:ench)?[)\\]]|フランス翻訳", Pattern.CASE_INSENSITIVE),
Pattern.compile("[(\\[]portuguese|ポルトガル翻訳", Pattern.CASE_INSENSITIVE),
Pattern.compile(
"[(\\[]thai(?: ภาษาไทย)?[)\\]]|แปลไทย|タイ翻訳",
Pattern.CASE_INSENSITIVE,
),
Pattern.compile("[(\\[]german[)\\]]|ドイツ翻訳", Pattern.CASE_INSENSITIVE),
Pattern.compile("[(\\[]italiano?[)\\]]|イタリア翻訳", Pattern.CASE_INSENSITIVE),
Pattern.compile(
"[(\\[]vietnamese(?: Tiếng Việt)?[)\\]]|ベトナム翻訳",
Pattern.CASE_INSENSITIVE,
),
Pattern.compile("[(\\[]polish[)\\]]|ポーランド翻訳", Pattern.CASE_INSENSITIVE),
Pattern.compile("[(\\[]hun(?:garian)?[)\\]]|ハンガリー翻訳", Pattern.CASE_INSENSITIVE),
Pattern.compile("[(\\[]dutch[)\\]]|オランダ翻訳", Pattern.CASE_INSENSITIVE),
)
val S_LANG_TAGS = arrayOf(
"language:english",
"language:chinese",
"language:spanish",
"language:korean",
"language:russian",
"language:french",
"language:portuguese",
"language:thai",
"language:german",
"language:italian",
"language:vietnamese",
"language:polish",
"language:hungarian",
"language:dutch",
)
}
}
================================================
FILE: app/src/main/java/com/hippo/ehviewer/client/data/GalleryPreview.kt
================================================
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.client.data
import android.os.Parcelable
import com.hippo.util.isAtLeastQ
import com.hippo.widget.LoadImageView
import kotlinx.parcelize.Parcelize
@Parcelize
class GalleryPreview(
var imageKey: String? = null,
var imageUrl: String? = null,
var pageUrl: String? = null,
var position: Int = 0,
var offsetX: Int = Int.MIN_VALUE,
var offsetY: Int = Int.MIN_VALUE,
var clipWidth: Int = Int.MIN_VALUE,
var clipHeight: Int = Int.MIN_VALUE,
) : Parcelable {
fun load(view: LoadImageView) {
view.setClip(offsetX, offsetY, clipWidth, clipHeight)
view.load(imageKey!!, imageUrl!!, offsetY == Int.MIN_VALUE || isAtLeastQ)
}
}
================================================
FILE: app/src/main/java/com/hippo/ehviewer/client/data/GalleryTagGroup.kt
================================================
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.client.data
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Suppress("JavaDefaultMethodsNotOverriddenByDelegation")
@Parcelize
class GalleryTagGroup(
private val mTagList: ArrayList = arrayListOf(),
var groupName: String? = null,
) : Parcelable, MutableList by mTagList
================================================
FILE: app/src/main/java/com/hippo/ehviewer/client/data/LargePreviewSet.kt
================================================
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.client.data
import com.hippo.ehviewer.client.getLargePreviewKey
import com.hippo.ehviewer.client.thumbUrl
import com.hippo.widget.LoadImageView
import com.hippo.yorozuya.collect.IntList
class LargePreviewSet(
private val mPositionList: IntList = IntList(),
private val mImageUrlList: ArrayList = arrayListOf(),
private val mPageUrlList: ArrayList = arrayListOf(),
private val mSha1List: ArrayList = arrayListOf(),
) : PreviewSet() {
fun addItem(index: Int, imageUrl: String, pageUrl: String, sha1: String) {
mPositionList.add(index)
mImageUrlList.add(imageUrl)
mPageUrlList.add(pageUrl)
mSha1List.add(sha1)
}
override fun size(): Int = mImageUrlList.size
override fun getPosition(index: Int): Int = mPositionList[index]
override fun getPageUrlAt(index: Int): String = mPageUrlList[index]
override fun getSha1At(index: Int): String = mSha1List[index]
override fun getGalleryPreview(gid: Long, index: Int): GalleryPreview {
val galleryPreview = GalleryPreview()
galleryPreview.position = mPositionList[index]
galleryPreview.imageKey = getLargePreviewKey(gid, galleryPreview.position)
galleryPreview.imageUrl = mImageUrlList[index]
galleryPreview.pageUrl = mPageUrlList[index]
return galleryPreview
}
override fun load(view: LoadImageView, gid: Long, index: Int) {
view.resetClip()
view.load(
getLargePreviewKey(gid, mPositionList[index]),
mImageUrlList[index].thumbUrl,
)
}
}
================================================
FILE: app/src/main/java/com/hippo/ehviewer/client/data/ListUrlBuilder.kt
================================================
/*
* Copyright (C) 2015 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.client.data
import android.os.Parcelable
import android.text.TextUtils
import androidx.annotation.IntDef
import com.hippo.ehviewer.client.EhUrl
import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.dao.QuickSearch
import com.hippo.ehviewer.widget.AdvanceSearchTable
import com.hippo.network.UrlBuilder
import com.hippo.util.encodeUTF8
import com.hippo.yorozuya.NumberUtils
import com.hippo.yorozuya.StringUtils
import java.io.UnsupportedEncodingException
import java.net.URLDecoder
import kotlinx.parcelize.Parcelize
@Parcelize
data class ListUrlBuilder(
@get:Mode @param:Mode
var mode: Int = MODE_NORMAL,
private var mPrev: String? = null,
private var mNext: String? = null,
private var mJumpTo: String? = null,
var category: Int = EhUtils.NONE,
private var mKeyword: String? = null,
var hash: String? = null,
var advanceSearch: Int = -1,
var minRating: Int = -1,
var pageFrom: Int = -1,
var pageTo: Int = -1,
var isShowExpunged: Boolean = false,
) : Parcelable {
fun reset() {
mode = MODE_NORMAL
mPrev = null
mNext = null
mJumpTo = null
this.category = EhUtils.NONE
mKeyword = null
advanceSearch = -1
minRating = -1
pageFrom = -1
pageTo = -1
isShowExpunged = false
hash = null
}
fun setIndex(index: String?, isNext: Boolean = true) {
mNext = index?.takeIf { isNext }
mPrev = index?.takeUnless { isNext }
}
fun setJumpTo(jumpTo: String?) {
mJumpTo = jumpTo
}
var keyword: String?
get() = if (MODE_UPLOADER == mode) "uploader:$mKeyword" else mKeyword
set(keyword) {
mKeyword = keyword
}
fun set(q: QuickSearch) {
mode = q.mode
this.category = q.category
mKeyword = q.keyword
advanceSearch = q.advanceSearch
minRating = q.minRating
pageFrom = q.pageFrom
pageTo = q.pageTo
isShowExpunged = false
}
fun toQuickSearch(): QuickSearch {
val q = QuickSearch()
q.mode = mode
q.category = this.category
q.keyword = mKeyword
q.advanceSearch = advanceSearch
q.minRating = minRating
q.pageFrom = pageFrom
q.pageTo = pageTo
return q
}
fun equalsQuickSearch(q: QuickSearch?): Boolean {
if (null == q) {
return false
}
if (q.mode != mode) {
return false
}
if (q.category != this.category) {
return false
}
if (!StringUtils.equals(q.keyword, mKeyword)) {
return false
}
if (q.advanceSearch != advanceSearch) {
return false
}
if (q.minRating != minRating) {
return false
}
return if (q.pageFrom != pageFrom) {
false
} else {
q.pageTo == pageTo
}
}
/**
* @param query xxx=yyy&mmm=nnn
*/
// TODO page
fun setQuery(query: String?) {
reset()
if (TextUtils.isEmpty(query)) {
return
}
val queries = StringUtils.split(query, '&')
var category = 0
var keyword: String? = null
var enableAdvanceSearch = false
var advanceSearch = 0
var enableMinRating = false
var minRating = -1
var enablePage = false
var pageFrom = -1
var pageTo = -1
for (str in queries) {
val index = str.indexOf('=')
if (index < 0) {
continue
}
val key = str.substring(0, index)
val value = str.substring(index + 1)
when (key) {
"f_cats" -> {
val cats = NumberUtils.parseIntSafely(value, EhUtils.ALL_CATEGORY)
category = category or (cats.inv() and EhUtils.ALL_CATEGORY)
}
"f_doujinshi" -> if ("1" == value) {
category = category or EhUtils.DOUJINSHI
}
"f_manga" -> if ("1" == value) {
category = category or EhUtils.MANGA
}
"f_artistcg" -> if ("1" == value) {
category = category or EhUtils.ARTIST_CG
}
"f_gamecg" -> if ("1" == value) {
category = category or EhUtils.GAME_CG
}
"f_western" -> if ("1" == value) {
category = category or EhUtils.WESTERN
}
"f_non-h" -> if ("1" == value) {
category = category or EhUtils.NON_H
}
"f_imageset" -> if ("1" == value) {
category = category or EhUtils.IMAGE_SET
}
"f_cosplay" -> if ("1" == value) {
category = category or EhUtils.COSPLAY
}
"f_asianporn" -> if ("1" == value) {
category = category or EhUtils.ASIAN_PORN
}
"f_misc" -> if ("1" == value) {
category = category or EhUtils.MISC
}
"f_search" -> try {
keyword = URLDecoder.decode(value, "utf-8")
} catch (_: UnsupportedEncodingException) {
// Ignore
} catch (_: IllegalArgumentException) {
}
"advsearch" -> if ("1" == value) {
enableAdvanceSearch = true
}
"f_sh" -> if ("on" == value) {
advanceSearch = advanceSearch or AdvanceSearchTable.SH
}
"f_sto" -> if ("on" == value) {
advanceSearch = advanceSearch or AdvanceSearchTable.STO
}
"f_sfl" -> if ("on" == value) {
advanceSearch = advanceSearch or AdvanceSearchTable.SFL
}
"f_sfu" -> if ("on" == value) {
advanceSearch = advanceSearch or AdvanceSearchTable.SFU
}
"f_sft" -> if ("on" == value) {
advanceSearch = advanceSearch or AdvanceSearchTable.SFT
}
"f_sr" -> if ("on" == value) {
enableMinRating = true
}
"f_srdd" -> minRating = NumberUtils.parseIntSafely(value, -1)
"f_sp" -> if ("on" == value) {
enablePage = true
}
"f_spf" -> pageFrom = NumberUtils.parseIntSafely(value, -1)
"f_spt" -> pageTo = NumberUtils.parseIntSafely(value, -1)
"f_shash" -> hash = value
}
}
this.category = category
mKeyword = keyword
if (enableAdvanceSearch) {
this.advanceSearch = advanceSearch
if (enableMinRating) {
this.minRating = minRating
} else {
this.minRating = -1
}
if (enablePage) {
this.pageFrom = pageFrom
this.pageTo = pageTo
} else {
this.pageFrom = -1
this.pageTo = -1
}
} else {
this.advanceSearch = -1
}
}
fun build(): String = when (mode) {
MODE_NORMAL, MODE_SUBSCRIPTION -> {
val url: String = if (mode == MODE_NORMAL) {
EhUrl.host
} else {
EhUrl.watchedUrl
}
val ub = UrlBuilder(url)
if (this.category != EhUtils.NONE) {
ub.addQuery("f_cats", category.inv() and EhUtils.ALL_CATEGORY)
}
// Search key
mKeyword?.run {
val keyword = trim { it <= ' ' }
if (keyword.isNotEmpty()) {
ub.addQuery("f_search", encodeUTF8(this))
}
}
hash?.let {
ub.addQuery("f_shash", it)
}
mJumpTo?.let {
ub.addQuery("seek", it)
}
mPrev?.let {
ub.addQuery("prev", it)
}
mNext?.let {
ub.addQuery("next", it)
}
// Advance search
if (advanceSearch != -1) {
ub.addQuery("advsearch", "1")
if (advanceSearch and AdvanceSearchTable.SH != 0) ub.addQuery("f_sh", "on")
if (advanceSearch and AdvanceSearchTable.STO != 0) ub.addQuery("f_sto", "on")
if (advanceSearch and AdvanceSearchTable.SFL != 0) ub.addQuery("f_sfl", "on")
if (advanceSearch and AdvanceSearchTable.SFU != 0) ub.addQuery("f_sfu", "on")
if (advanceSearch and AdvanceSearchTable.SFT != 0) ub.addQuery("f_sft", "on")
// Set min star
if (minRating != -1) {
ub.addQuery("f_sr", "on")
ub.addQuery("f_srdd", minRating)
}
// Pages
if (pageFrom != -1 || pageTo != -1) {
ub.addQuery("f_sp", "on")
ub.addQuery("f_spf", if (pageFrom != -1) pageFrom.toString() else "")
ub.addQuery("f_spt", if (pageTo != -1) pageTo.toString() else "")
}
}
ub.build()
}
MODE_UPLOADER -> {
val sb = StringBuilder(EhUrl.host)
mKeyword?.let {
sb.append("uploader/")
sb.append(encodeUTF8(it))
}
mPrev?.let {
sb.append("?prev=").append(it)
}
mNext?.let {
sb.append("?next=").append(it)
}
mJumpTo?.let {
sb.append("&seek=").append(it)
}
sb.toString()
}
MODE_TAG -> {
val sb = StringBuilder(EhUrl.host)
mKeyword?.let {
sb.append("tag/")
sb.append(encodeUTF8(it))
}
mPrev?.let {
sb.append("?prev=").append(it)
}
mNext?.let {
sb.append("?next=").append(it)
}
mJumpTo?.let {
sb.append("&seek=").append(it)
}
sb.toString()
}
MODE_WHATS_HOT -> EhUrl.popularUrl
MODE_IMAGE_SEARCH -> {
val ub = UrlBuilder(EhUrl.host)
hash?.let {
ub.addQuery("f_shash", it)
}
ub.build()
}
MODE_TOPLIST -> {
val sb = StringBuilder(EhUrl.HOST_E)
sb.append("toplist.php?tl=")
mKeyword.orEmpty().let {
sb.append(encodeUTF8(it))
}
mJumpTo?.let {
sb.append("&p=").append(it)
}
sb.toString()
}
else -> throw IllegalStateException("Unexpected value: $mode")
}
@IntDef(
MODE_NORMAL,
MODE_UPLOADER,
MODE_TAG,
MODE_WHATS_HOT,
MODE_IMAGE_SEARCH,
MODE_SUBSCRIPTION,
MODE_TOPLIST,
)
@Retention(AnnotationRetention.SOURCE)
private annotation class Mode
companion object {
const val MODE_NORMAL = 0x0
const val MODE_UPLOADER = 0x1
const val MODE_TAG = 0x2
const val MODE_WHATS_HOT = 0x3
const val MODE_IMAGE_SEARCH = 0x4
const val MODE_SUBSCRIPTION = 0x5
const val MODE_TOPLIST = 0x6
}
}
================================================
FILE: app/src/main/java/com/hippo/ehviewer/client/data/NormalPreviewSet.kt
================================================
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.client.data
import com.hippo.ehviewer.client.getNormalPreviewKey
import com.hippo.util.isAtLeastQ
import com.hippo.widget.LoadImageView
import com.hippo.yorozuya.collect.IntList
class NormalPreviewSet(
private var mPositionList: IntList = IntList(),
private var mImageKeyList: ArrayList = arrayListOf(),
private var mImageUrlList: ArrayList = arrayListOf(),
private var mOffsetXList: IntList = IntList(),
private var mOffsetYList: IntList = IntList(),
private var mClipWidthList: IntList = IntList(),
private var mClipHeightList: IntList = IntList(),
private var mPageUrlList: ArrayList = arrayListOf(),
private val mSha1List: ArrayList = arrayListOf(),
) : PreviewSet() {
fun addItem(
position: Int,
imageUrl: String,
xOffset: Int,
yOffset: Int,
width: Int,
height: Int,
pageUrl: String,
sha1: String,
) {
mPositionList.add(position)
mImageKeyList.add(getNormalPreviewKey(imageUrl))
mImageUrlList.add(imageUrl)
mOffsetXList.add(xOffset)
mOffsetYList.add(yOffset)
mClipWidthList.add(width)
mClipHeightList.add(height)
mPageUrlList.add(pageUrl)
mSha1List.add(sha1)
}
override fun size(): Int = mPositionList.size
override fun getPosition(index: Int): Int = mPositionList[index]
override fun getPageUrlAt(index: Int): String = mPageUrlList[index]
override fun getSha1At(index: Int): String = mSha1List[index]
override fun getGalleryPreview(gid: Long, index: Int): GalleryPreview {
val galleryPreview = GalleryPreview()
galleryPreview.position = mPositionList[index]
galleryPreview.imageKey = mImageKeyList[index]
galleryPreview.imageUrl = mImageUrlList[index]
galleryPreview.pageUrl = mPageUrlList[index]
galleryPreview.offsetX = mOffsetXList[index]
galleryPreview.offsetY = mOffsetYList[index]
galleryPreview.clipWidth = mClipWidthList[index]
galleryPreview.clipHeight = mClipHeightList[index]
return galleryPreview
}
override fun load(view: LoadImageView, gid: Long, index: Int) {
view.setClip(
mOffsetXList[index],
mOffsetYList[index],
mClipWidthList[index],
mClipHeightList[index],
)
view.load(mImageKeyList[index], mImageUrlList[index], isAtLeastQ)
}
}
================================================
FILE: app/src/main/java/com/hippo/ehviewer/client/data/PreviewSet.kt
================================================
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.client.data
import com.hippo.widget.LoadImageView
abstract class PreviewSet {
abstract fun size(): Int
abstract fun getPosition(index: Int): Int
abstract fun getPageUrlAt(index: Int): String
abstract fun getSha1At(index: Int): String
abstract fun getGalleryPreview(gid: Long, index: Int): GalleryPreview
abstract fun load(view: LoadImageView, gid: Long, index: Int)
}
================================================
FILE: app/src/main/java/com/hippo/ehviewer/client/exception/CloudflareBypassException.kt
================================================
package com.hippo.ehviewer.client.exception
import com.hippo.ehviewer.R
class CloudflareBypassException : EhException(R.string.cloudflare_bypass_failed)
================================================
FILE: app/src/main/java/com/hippo/ehviewer/client/exception/EhException.kt
================================================
/*
* 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.exception
import androidx.annotation.StringRes
import com.hippo.ehviewer.GetText
open class EhException : Exception {
constructor(detailMessage: String?) : super(detailMessage)
constructor(detailMessage: String?, cause: Throwable?) : super(detailMessage, cause)
constructor(@StringRes message: Int) : super(GetText.getString(message))
}
================================================
FILE: app/src/main/java/com/hippo/ehviewer/client/exception/InsufficientFundsException.kt
================================================
package com.hippo.ehviewer.client.exception
import com.hippo.ehviewer.R
class InsufficientFundsException : EhException(R.string.insufficient_funds)
================================================
FILE: app/src/main/java/com/hippo/ehviewer/client/exception/NoHAtHClientException.kt
================================================
/*
* 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.exception
import com.hippo.ehviewer.R
class NoHAtHClientException : EhException(R.string.download_archive_failure_no_hath)
================================================
FILE: app/src/main/java/com/hippo/ehviewer/client/exception/NotLoggedInException.kt
================================================
/*
* Copyright 2023 Moedog
*
* 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 .
*/
package com.hippo.ehviewer.client.exception
import com.hippo.ehviewer.R
class NotLoggedInException : EhException(R.string.need_sign_in)
================================================
FILE: app/src/main/java/com/hippo/ehviewer/client/exception/OffensiveException.kt
================================================
/*
* Copyright (C) 2015 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.client.exception
/**
* It is an exception for get offensive tip for g.e-hentai.org
*/
class OffensiveException : EhException("OFFENSIVE")
================================================
FILE: app/src/main/java/com/hippo/ehviewer/client/exception/ParseException.kt
================================================
/*
* 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.exception
class ParseException : EhException {
constructor(detailMessage: String?) : super(detailMessage)
constructor(detailMessage: String?, cause: Throwable?) : super(detailMessage, cause)
}
================================================
FILE: app/src/main/java/com/hippo/ehviewer/client/exception/PiningException.kt
================================================
/*
* Copyright (C) 2015 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.client.exception
class PiningException : EhException("pining for the fjords")
================================================
FILE: app/src/main/java/com/hippo/ehviewer/client/exception/QuotaExceededException.kt
================================================
/*
* Copyright 2023 Moedog
*
* 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 .
*/
package com.hippo.ehviewer.client.exception
import com.hippo.ehviewer.R
class QuotaExceededException : EhException(R.string.error_509)
================================================
FILE: app/src/main/java/com/hippo/ehviewer/client/parser/ArchiveParser.kt
================================================
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.client.parser
import com.hippo.ehviewer.client.exception.InsufficientFundsException
import com.hippo.ehviewer.client.exception.NoHAtHClientException
import org.jsoup.Jsoup
object ArchiveParser {
private val PATTERN_CURRENT_FUNDS =
Regex("([\\d,]+) GP \\[[^]]*] ([\\d,]+) Credits \\[[^]]*]
")
private val PATTERN_HATH_ARCHIVE =
Regex("([^<]+)
\\s*([\\w. ]+)
\\s*([\\w. ]+)
")
private const val ERROR_NEED_HATH_CLIENT =
"You must have a H@H client assigned to your account to use this feature."
private const val ERROR_INSUFFICIENT_FUNDS =
"You do not have enough funds to download this archive."
fun parse(body: String): Result {
val archiveList = ArrayList()
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()
}
}
}
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)
}
val result = Result(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
}
fun parseArchiveUrl(body: String): String? {
if (body.contains(ERROR_NEED_HATH_CLIENT)) {
throw NoHAtHClientException()
} else if (body.contains(ERROR_INSUFFICIENT_FUNDS)) {
throw InsufficientFundsException()
}
return Jsoup.parse(body).selectFirst("#continue>a[href]")
?.let { it.attr("href") + "?start=1" }
// TODO: Check more errors
}
class Archive(
val res: String,
val name: String,
val size: String,
val cost: String,
val isHAtH: Boolean,
)
class Result(val archiveList: List, var funds: HomeParser.Funds?)
}
================================================
FILE: app/src/main/java/com/hippo/ehviewer/client/parser/EventPaneParser.kt
================================================
/*
* 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 .
*/
package com.hippo.ehviewer.client.parser
import org.jsoup.Jsoup
object EventPaneParser {
fun parse(body: String): String? = Jsoup.parse(body).getElementById("eventpane")?.html()
}
================================================
FILE: app/src/main/java/com/hippo/ehviewer/client/parser/FavoritesParser.kt
================================================
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.client.parser
import com.hippo.ehviewer.client.exception.NotLoggedInException
import com.hippo.ehviewer.client.exception.ParseException
import com.hippo.util.ExceptionUtils
import com.hippo.util.JsoupUtils
import org.jsoup.Jsoup
object FavoritesParser {
fun parse(body: String): Result {
if (body.contains("This page requires you to log on.
")) {
throw NotLoggedInException()
}
val catArray = arrayOfNulls(10)
val countArray = IntArray(10)
val d = Jsoup.parse(body)
runCatching {
val ido = JsoupUtils.getElementByClass(d, "ido")
val fps = ido!!.getElementsByClass("fp")
// Last one is "fp fps"
check(fps.size == 11)
for (i in 0..9) {
val fp = fps[i]
countArray[i] = ParserUtils.parseInt(fp.child(0).text(), 0)
catArray[i] = ParserUtils.trim(fp.child(2).text())
}
}.onFailure {
ExceptionUtils.throwIfFatal(it)
it.printStackTrace()
throw ParseException("Parse favorites error")
}
val result = GalleryListParser.parse(d, body)
return Result(catArray.requireNoNulls(), countArray, result)
}
class Result(
val catArray: Array,
val countArray: IntArray,
galleryListResult: GalleryListParser.Result,
) {
val prev = galleryListResult.prev
val next = galleryListResult.next
val galleryInfoList = galleryListResult.galleryInfoList
}
}
================================================
FILE: app/src/main/java/com/hippo/ehviewer/client/parser/ForumsParser.kt
================================================
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.client.parser
import com.hippo.ehviewer.client.EhUrl
import com.hippo.ehviewer.client.exception.NotLoggedInException
import com.hippo.util.ExceptionUtils
import org.jsoup.Jsoup
object ForumsParser {
fun parse(body: String): String = runCatching {
val d = Jsoup.parse(body, EhUrl.URL_FORUMS)
val userlinks = d.getElementById("userlinks")
val child = userlinks!!.child(0).child(0).child(0)
child.attr("href")
}.getOrElse {
ExceptionUtils.throwIfFatal(it)
throw NotLoggedInException()
}
}
================================================
FILE: app/src/main/java/com/hippo/ehviewer/client/parser/GalleryApiParser.kt
================================================
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.client.parser
import com.hippo.ehviewer.client.EhUtils.getCategory
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.yorozuya.NumberUtils
import org.json.JSONObject
object GalleryApiParser {
fun parse(body: String, galleryInfoList: List) {
val jo = JSONObject(body)
val ja = jo.getJSONArray("gmetadata")
for (i in 0 until ja.length()) {
val g = ja.getJSONObject(i)
val gid = g.getLong("gid")
val gi = galleryInfoList.find { it.gid == gid } ?: continue
gi.title = ParserUtils.trim(g.getString("title"))
gi.titleJpn = ParserUtils.trim(g.getString("title_jpn"))
gi.category = getCategory(g.getString("category"))
gi.thumb = g.getString("thumb")
gi.uploader = g.getString("uploader")
gi.posted =
ParserUtils.formatDate(ParserUtils.parseLong(g.getString("posted"), 0) * 1000)
gi.rating = NumberUtils.parseFloatSafely(g.getString("rating"), 0.0f)
// tags
val tagJa = g.getJSONArray("tags")
gi.simpleTags = (0 until tagJa.length()).map { tagJa.getString(it) }.toTypedArray()
gi.pages = NumberUtils.parseIntSafely(g.getString("filecount"), 0)
gi.generateSLang()
}
}
}
================================================
FILE: app/src/main/java/com/hippo/ehviewer/client/parser/GalleryDetailParser.kt
================================================
/*
* 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.Settings
import com.hippo.ehviewer.client.EhFilter
import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.client.EhUtils.getCategory
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.util.toEpochMillis
import com.hippo.yorozuya.NumberUtils
import com.hippo.yorozuya.StringUtils
import com.hippo.yorozuya.trimAnd
import com.hippo.yorozuya.unescapeXml
import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.format.MonthNames
import kotlinx.datetime.format.char
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
object GalleryDetailParser {
private val PATTERN_ERROR = Regex("