├── .github ├── FUNDING.yml └── ISSUE_TEMPLATE │ └── bug_report.md ├── .gitignore ├── .project ├── .settings └── org.eclipse.buildship.core.prefs ├── .travis.yml ├── LICENSE.md ├── README.md ├── app ├── .gitignore ├── appcenter-pre-build.sh ├── build.gradle ├── debug │ └── output.json ├── myapps.jks.enc ├── proguard-rules.pro ├── schemas │ └── com.github.ttdyce.nhviewer.model.room.AppDatabase │ │ ├── 1.json │ │ └── 2.json └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── github │ │ └── ttdyce │ │ └── nhviewer │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── ic_launcher-playstore.png │ ├── ic_launcher_nhviewer-web.png │ ├── java │ │ └── com │ │ │ └── github │ │ │ └── ttdyce │ │ │ └── nhviewer │ │ │ ├── model │ │ │ ├── CookieStringRequest.java │ │ │ ├── CustomGlideModule.java │ │ │ ├── MyDistributeListener.java │ │ │ ├── api │ │ │ │ ├── NHAPI.java │ │ │ │ ├── PopularType.java │ │ │ │ └── ResponseCallback.java │ │ │ ├── comic │ │ │ │ ├── Comic.java │ │ │ │ ├── ComicCollection.java │ │ │ │ └── factory │ │ │ │ │ ├── ComicFactory.java │ │ │ │ │ ├── DBComicFactory.java │ │ │ │ │ └── NHApiComicFactory.java │ │ │ ├── proxy │ │ │ │ └── NHVProxyStack.java │ │ │ └── room │ │ │ │ ├── AppDatabase.java │ │ │ │ ├── ComicBookmarkDao.java │ │ │ │ ├── ComicBookmarkEntity.java │ │ │ │ ├── ComicCachedDao.java │ │ │ │ ├── ComicCachedEntity.java │ │ │ │ ├── ComicCollectionDao.java │ │ │ │ ├── ComicCollectionEntity.java │ │ │ │ └── DateConverter.java │ │ │ ├── presenter │ │ │ ├── ComicCollectionPresenter.java │ │ │ ├── ComicListPresenter.java │ │ │ └── ComicPresenter.java │ │ │ └── view │ │ │ ├── BackupActivity.java │ │ │ ├── ComicActivity.java │ │ │ ├── ComicCollectionFragment.java │ │ │ ├── ComicCollectionViewHolder.java │ │ │ ├── ComicListFragment.java │ │ │ ├── ComicListViewHolder.java │ │ │ ├── ComicViewHolder.java │ │ │ ├── MainActivity.java │ │ │ ├── ProxySettingsFragment.java │ │ │ ├── RefreshCookieActivity.java │ │ │ ├── SearchingFragment.java │ │ │ ├── SettingsFragment.java │ │ │ ├── SplashActivity.java │ │ │ └── component │ │ │ └── ZoomRecyclerView.java │ └── res │ │ ├── anim │ │ └── splash_loading.xml │ │ ├── drawable-v24 │ │ ├── ic_launcher_background.xml │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ ├── ic_add_to_photos_24dp.xml │ │ ├── ic_check_box_24dp.xml │ │ ├── ic_delete_24dp.xml │ │ ├── ic_done_24dp.xml │ │ ├── ic_favorite_black_24dp.xml │ │ ├── ic_favorite_border_24dp.xml │ │ ├── ic_folder_black_24dp.xml │ │ ├── ic_home_black_24dp.xml │ │ ├── ic_keyboard_arrow_right_24dp.xml │ │ ├── ic_launcher_background.xml │ │ ├── ic_launcher_nhviewer_background.xml │ │ ├── ic_nhlogo.xml │ │ ├── ic_search_black_24dp.xml │ │ ├── ic_settings_black_24dp.xml │ │ ├── ic_sort_24dp.xml │ │ └── rectangle.xml │ │ ├── layout │ │ ├── activity_backup.xml │ │ ├── activity_comic.xml │ │ ├── activity_main.xml │ │ ├── activity_refresh_cookie.xml │ │ ├── activity_splash.xml │ │ ├── fragment_comic_list.xml │ │ ├── item_comic.xml │ │ ├── item_comic_collection_list.xml │ │ └── item_comic_list.xml │ │ ├── menu │ │ ├── app_bar_items_main.xml │ │ ├── app_bar_items_searching.xml │ │ ├── app_bar_selection_mode.xml │ │ └── bottom_navigation_items.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ ├── ic_launcher_nhviewer.xml │ │ ├── ic_launcher_nhviewer_round.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ ├── ic_launcher_nhviewer.png │ │ ├── ic_launcher_nhviewer_foreground.png │ │ ├── ic_launcher_nhviewer_round.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ ├── ic_launcher_nhviewer.png │ │ ├── ic_launcher_nhviewer_foreground.png │ │ ├── ic_launcher_nhviewer_round.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ ├── ic_launcher_nhviewer.png │ │ ├── ic_launcher_nhviewer_foreground.png │ │ ├── ic_launcher_nhviewer_round.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ ├── ic_launcher_nhviewer.png │ │ ├── ic_launcher_nhviewer_foreground.png │ │ ├── ic_launcher_nhviewer_round.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ ├── ic_launcher_nhviewer.png │ │ ├── ic_launcher_nhviewer_foreground.png │ │ ├── ic_launcher_nhviewer_round.png │ │ └── ic_launcher_round.png │ │ ├── navigation │ │ └── nav_app.xml │ │ ├── values-zh-rCN │ │ └── strings.xml │ │ ├── values-zh-rTW │ │ └── strings.xml │ │ ├── values │ │ ├── arrays.xml │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── ic_launcher_nhviewer_background.xml │ │ ├── strings.xml │ │ └── styles.xml │ │ └── xml │ │ ├── firebase_default_config.xml │ │ ├── preferences.xml │ │ └── proxy_preferences.xml │ └── test │ └── java │ └── com │ └── github │ └── ttdyce │ └── nhviewer │ ├── APIUnitTest.java │ ├── ComicCollectionUnitTest.java │ ├── ComicUnitTest.java │ └── ExampleUnitTest.java ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── screenshots ├── collection.png ├── deprecated │ ├── V2 │ │ ├── collection.png │ │ ├── comic.png │ │ ├── favorite.png │ │ ├── index.png │ │ ├── search.png │ │ └── setting.png │ ├── collection_list.png │ ├── favorite_list.png │ └── navigation_view.png ├── search.png └── setting.png └── settings.gradle /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [ttdyce] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: ttdyce # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | name: '' 5 | labels: bug 6 | assignees: ttdyce 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Exception thrown** 24 | Copy the whole exception hints here, usually it is very helpful for debugging! 25 | 26 | **Screenshots** 27 | If applicable, add screenshots to help explain your problem. 28 | 29 | **Desktop (please complete the following information):** 30 | - OS: [e.g. iOS] 31 | - Version [e.g. 22] 32 | 33 | **Smartphone (please complete the following information):** 34 | - Device: [e.g. iPhone6] 35 | - OS: [e.g. iOS8.1] 36 | - Version [e.g. 22] 37 | 38 | **Additional context** 39 | Add any other context about the problem here. 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.ap_ 4 | *.aab 5 | 6 | # Files for the ART/Dalvik VM 7 | *.dex 8 | 9 | # Java class files 10 | *.class 11 | 12 | # Generated files 13 | bin/ 14 | gen/ 15 | out/ 16 | release/ 17 | 18 | # Gradle files 19 | .gradle/ 20 | build/ 21 | 22 | # Local configuration file (sdk path, etc) 23 | local.properties 24 | 25 | # Proguard folder generated by Eclipse 26 | proguard/ 27 | 28 | # Log Files 29 | *.log 30 | 31 | # Android Studio Navigation editor temp files 32 | .navigation/ 33 | 34 | # Android Studio captures folder 35 | captures/ 36 | 37 | # IntelliJ 38 | *.iml 39 | .idea/workspace.xml 40 | .idea/tasks.xml 41 | .idea/gradle.xml 42 | .idea/assetWizardSettings.xml 43 | .idea/dictionaries 44 | .idea/libraries 45 | # Android Studio 3 in .gitignore file. 46 | .idea/caches 47 | .idea/modules.xml 48 | # Comment next line if keeping position of elements in Navigation Editor is relevant for you 49 | .idea/navEditor.xml 50 | 51 | # Keystore files 52 | # Uncomment the following lines if you do not want to check your keystore files in. 53 | *.jks 54 | #*.keystore 55 | keystore.properties 56 | 57 | # External native build folder generated in Android Studio 2.2 and later 58 | .externalNativeBuild 59 | 60 | # Google Services (e.g. APIs or Firebase) 61 | # google-services.json 62 | 63 | # Freeline 64 | freeline.py 65 | freeline/ 66 | freeline_project_description.json 67 | 68 | # fastlane 69 | fastlane/report.xml 70 | fastlane/Preview.html 71 | fastlane/screenshots 72 | fastlane/test_output 73 | fastlane/readme.md 74 | 75 | # Version control 76 | vcs.xml 77 | 78 | # lint 79 | lint/intermediates/ 80 | lint/generated/ 81 | lint/outputs/ 82 | lint/tmp/ 83 | # lint/reports/ 84 | .idea 85 | -------------------------------------------------------------------------------- /.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | NHViewer 4 | Project NHentaiViewer created by Buildship. 5 | 6 | 7 | 8 | 9 | org.eclipse.buildship.core.gradleprojectbuilder 10 | 11 | 12 | 13 | 14 | 15 | org.eclipse.buildship.core.gradleprojectnature 16 | 17 | 18 | 19 | 1602326476666 20 | 21 | 30 22 | 23 | org.eclipse.core.resources.regexFilterMatcher 24 | node_modules|.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__ 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /.settings/org.eclipse.buildship.core.prefs: -------------------------------------------------------------------------------- 1 | arguments= 2 | auto.sync=false 3 | build.scans.enabled=false 4 | connection.gradle.distribution=GRADLE_DISTRIBUTION(WRAPPER) 5 | connection.project.dir= 6 | eclipse.preferences.version=1 7 | gradle.user.home= 8 | java.home=/Library/Java/JavaVirtualMachines/microsoft-17.jdk/Contents/Home 9 | jvm.arguments= 10 | offline.mode=false 11 | override.workspace.settings=true 12 | show.console.view=true 13 | show.executions.view=true 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: android 2 | dist: trusty 3 | os: linux 4 | env: 5 | global: 6 | - BUILD_TOOL_VERSION=29.0.2 7 | - ANDROID_API=29 8 | - secure: jqQJ3WXC94shPZSwQX6SnLMGoMGt7xZfUcSV2KgQ648R47kaSMnwt7kyh1X7wXX2ttL1BG4Mkii4JSsEzehzLfA0AJN9hRwdY7EctMPE3QTPfHXKL/CSQBc+2vh2OPpP0iw3FNHxL6uS/TT3zydy6m8aRxwvXPoZ4d7r0l6TnQG2MVzC4eufyZj7Ms2cXY57aZDDbramTFIIYzLGtCX+aGgCdFGQ/fWbNnVBprHdNbeRq0VqlhUl2Cc+GCZEVYzjbwIhRq3SEdmV5W0uHZR6+2kqf6pigyVdDvxcigKF/jzRzwkADzn/nTpBcBHcQ03sxtCBs2mBF0YDd0fNMzyX9i7SZliiGIzJo08zWqRoqipWd29fGHiJoM4VeSEparY/G+EPNzuA0IdnCgKzG8e6BLpo0cJ16CxVrj28lUel10VKihbJcvgyS0OaUpba57/5hplEw49Ldvj9jIf8nICSXLftrn25uDkhorOFnztAUo/R8F6QA5zzv+dska58y+jriAM/rhEA5xLzpLWmcJcph5abRvurhceAVI/s6gaj7MOJCjwTrJNyizQo3tof/wZlI4iXfBEbOuB3jo1rkxrrMHtRXLwewjpezm7GWiDcGBD49tgigV2B0VfCYjIM0MEm9oucLpB8Dp2e4VbFRxRuM6TkxfC5iZIVX1SL4jk2TlY= 9 | android: 10 | components: 11 | - build-tools-${BUILD_TOOL_VERSION} 12 | - android-${ANDROID_API} 13 | - tools 14 | - platform-tools 15 | before_cache: 16 | - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock 17 | - rm -fr $HOME/.gradle/caches/*/plugin-resolution/ 18 | cache: 19 | directories: 20 | - $HOME/.gradle/caches/ 21 | - $HOME/.gradle/wrapper/ 22 | - $HOME/.android/build-cache 23 | before_install: 24 | - openssl aes-256-cbc -k "$keystore_d" -in app/myapps.jks.enc -out app/myapps.jks -d 25 | - chmod +x gradlew 26 | script: 27 | - ./gradlew assembleDebug 28 | - ./gradlew assembleRelease 29 | - ls -l app/build/outputs/apk/debug 30 | - ls -l app/build/outputs/apk/release 31 | before_deploy: 32 | - mv app/build/outputs/apk/debug/app-debug.apk app/build/outputs/apk/debug/nhviewer-${TRAVIS_TAG}.apk 33 | - mv app/build/outputs/apk/release/app-release.apk app/build/outputs/apk/release/nhviewer-${TRAVIS_TAG}-signed.apk 34 | - ls -l app/build/outputs/apk/debug 35 | - ls -l app/build/outputs/apk/release 36 | deploy: 37 | provider: releases 38 | skip_cleanup: true 39 | overwrite: true 40 | draft: true 41 | api_key: 42 | secure: V4d6VAD4MBk3Uk9DlizxOCXEFgJGtsSMuTOddtJAT/ETByCfrk09u3FjRCPPzRipfnPwrtIbNtVOLI7/joR6amErxoyPg0kLeARFDIh8lcRhbELv+ybuDNRM/FvRlSc0+RtXZ5XYlU+cVAtUH3gnnv3smxFTZvLHM4ZD4f5ND6Q4AlemWqy7gpqYobxw8bFEhOTWKB+HUeBsNBdqPCSjWrmcvTWYdE0WNb8h5N03ncnJ1PySu4FM+UD1bOGvSgE/ACRcNG0ASCNwChrhokz/RnVfhujIhgL4G3bK5unJqTqgHeE76R9zV4pKdsHsiy0B1S/6IkAyiDFDgRtU9qfPWD1tc9d9E+xsoMiyX0h9B9EK+aoTu2wnZqwHVa5gk63eO0kPDjlU3SY/zeJ5DTMUTOdJVkvDPJrApsDs4H4yGQmCXuaG6i6kqICGjFLfB2bLIaKNbobIyxd5g2B9CqHn2eHNXT9LDUVUXDIT38LNvDf9a+2o9feRX5hwkj1FHEAXD/R67AljrLKsDWHgZJLi6xP9wjD2qFHsCZKIBrvw/+R3xoZkql37WS1QI6TjpcvaJ02dJJuOGoLs4L+SYb5tLS5eXGFb2L0ezzUOeutvzYtxT+rswmrY7rdqeJV1UZtLWvy6f+CiH4i1+YzT/qcDrSEM3Ly/AyzpT6oXLdpj7aw= 43 | file: 44 | - app/build/outputs/apk/debug/nhviewer-${TRAVIS_TAG}.apk 45 | - app/build/outputs/apk/release/nhviewer-${TRAVIS_TAG}-signed.apk 46 | on: 47 | repo: ttdyce/NHentai-NHViewer 48 | tags: true 49 | 50 | # jdk: oraclejdk8 51 | # android: 52 | # components: 53 | # - tools 54 | # - platform-tools 55 | # - build-tools-29.0.2 56 | # - android-${ANDROID_API} 57 | # - extra-android-m2repository 58 | # licenses: 59 | # - android-sdk-license-.+ 60 | 61 | # before_cache: "-rm -f $HOME/.gradle/caches/modules-2/modules-2.lock -rm -fr $HOME/.gradle/caches/*/plugin-resolution/" 62 | # cache: 63 | # directories: 64 | # - "$HOME/.gradle/caches/" 65 | # - "$HOME/.gradle/wrapper/" 66 | # - "$HOME/.android/build-cache" -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 ttdyce 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NHViewer 2 | 3 | [![GitHub license](https://img.shields.io/github/license/ttdyce/NHentaiViewer?color=brightgreen)](https://github.com/ttdyce/NHentaiViewer/blob/master/LICENSE.md) 4 | 5 | Simple third-party application for browsing nhentai.net. 6 | 7 | > Just a little app for Android 8 | 9 | ## Important Note: Visual Studio App Center is retiring soon! 10 | 11 | written on 2024-12-23 12 | 13 | Visual Studio App Center will be retired on March 31, 2025, which means this app won't receive auto update afterwards. 14 | To provide a wider support for both iOS and Android, I am working on a new solution on [ttdyce/nhviewer-universal](https://github.com/ttdyce/nhviewer-universal) as a replacement of this app. It's still under development but you can find the latest news there. 15 | 16 | --- 17 | 18 | ## Download 19 | 20 | [Download From VS App Center (retiring on March 31, 2025!)](https://install.appcenter.ms/users/ttdyce/apps/nhviewer-1/distribution_groups/public) 21 | 22 | ### Important note 23 | 24 | - Download from VS App Center is recommended for auto-update 25 | - To support this project, you can... 26 | 1. Sponsor this project on GitHub 27 | 28 | 3. Or... star the replacement app 🌟 [ttdyce/nhviewer-universal](https://github.com/ttdyce/nhviewer-universal) 29 | 30 | ## Look and Feel - in demo mode( •̀ ω •́ )y 31 | 32 | Search result display demoCollection display demoSetting demo 33 | 34 | ## Features 35 | 36 | - Collection system 37 | - Add / remove comic from Favorite / Read later / History 38 | - Backup collection content to desktop (by scanning QRCode with [NHV-Backup, Java program](https://github.com/ttdyce/NHV-Backup)) 39 | - General 40 | - Basic proxy 41 | - Vertical scrolling 42 | - Comic list sorting (by popularity / uploaded recently) 43 | - Search with specific language (Chinese / English / Japanese) 44 | 45 | ## V3 Overview 46 | 47 | - Dark theme (good for your eyes😉, or, at least for me...) 48 | 49 | - Better auto-update 50 | 51 | More new features (see #Roadmap) 52 | 53 | ### Version 2 overview 54 | 55 | - [M-V-P](https://stackoverflow.com/questions/2056/what-are-mvp-and-mvc-and-what-is-the-difference) 56 | - Retrieve data from [JSON API](https://github.com/NHMoeDev/NHentai-android/issues/27) (that page was removed, you may need to read the code/google it at this point...) 57 | - the closest document I could find is [this](https://hentaichan.pythonanywhere.com/projects/hentai/api-endpoints) by [@hentai-chan](https://github.com/hentai-chan) 58 | - [Android Jetpack Components](https://developer.android.com/jetpack) 59 | 60 | ### Icon (Version 2) & Splash screen 61 | 62 | I would like to include NHentai’s icon and slogan in this project since it is an application about their site. 63 | I have sent them an email to ask for permission but there is still no reply yet. 64 | Please contact me if there are any issue, thanks. 65 | 66 | --- 67 | 68 | ## Getting Started 69 | 70 | The application is using the [JSON API](https://github.com/NHMoeDev/NHentai-android/issues/27) (page was removed😥) and parse the response data into Java class from version 2. 71 | 72 | - the closest document I could find is [this](https://hentaichan.pythonanywhere.com/projects/hentai/api-endpoints) by [@hentai-chan](https://github.com/hentai-chan) 73 | 74 | 75 | 😣~~For more information about coding, see [the wiki](https://github.com/ttdyce/NHentaiViewer/wiki) (which is not yet ready '_' please come back later).~~ 76 | 77 | ## Deployment 78 | 79 | Build and run the project inside Android Studio. 80 | 81 | ## Built With 82 | 83 | - [Android Studio](https://developer.android.com/studio) 84 | - Any version after Android Studio 3.5 should be fine 85 | - Run on an Android device (tested on Android 8.0 Oreo) 86 | 87 | --- 88 | 89 | ## Versioning 90 | 91 | - [SemVer](http://semver.org/) 92 | 93 | For the versions available, see the [tags on this repository](https://github.com/ttdyce/nhviewer/tags) 94 | 95 | ## Authors 96 | 97 | - **ttdyce** - *Author and maintainer* - [github](https://github.com/ttdyce) 98 | 99 | ## Acknowledgments 100 | 101 | - Thanks for 102 | - Simplified Chinese translate from [@History-exe](https://github.com/History-exe) 103 | - Traditional Chinese translate from [@neslxzhen](https://github.com/neslxzhen) 104 | - Beautiful badges displaying GitHub data from [Shields.io](https://github.com/badges/shields) 105 | 106 | - Inspired by 107 | - [nhentai.net](https://nhentai.net) 108 | - [NHBooks](https://github.com/NHMoeDev/NHentai-android) 109 | - [EhViewer](https://github.com/seven332/EhViewer) 110 | 111 | - Dependencies 112 | - Image blur: [glide-transformations](https://github.com/wasabeef/glide-transformations) from [@wasabeef](https://github.com/wasabeef) 113 | - [QRCodeReaderView](https://github.com/dlazaro66/QRCodeReaderView) from [@dlazaro66](https://github.com/dlazaro66) 114 | - [Gson](https://github.com/google/gson) 115 | - [jsoup](https://jsoup.org/download) 116 | - [Glide](http://bumptech.github.io/glide/doc/download-setup.html) 117 | - [Volley](https://developer.android.com/training/volley) 118 | - Android's libraries 119 | 120 | ## License 121 | 122 | This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details 123 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/appcenter-pre-build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | openssl aes-256-cbc -d -a -in myapps.jks.enc -out myapps.jks -k "$keystore_d" -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: "org.ajoberstar.grgit" 3 | 4 | import org.ajoberstar.grgit.Grgit 5 | import org.ajoberstar.grgit.Tag 6 | 7 | ext { 8 | //Git tag as version name, using org.ajoberstar.grgit.Grgit 9 | git = Grgit.open(currentDir: project.rootDir) 10 | tags = git.tag.list() 11 | 12 | // sort tags in integer way, default in string way 13 | // e.g. after sort: (latest)2.10.0 > 2.9.0 > 2.8.0, default: (latest)2.9.0 > ... > 2.2.0 > 2.10.0 😑 14 | Collections.sort(tags, new Comparator() { 15 | @Override 16 | int compare(Tag a, Tag b) { 17 | ArrayList aNames = a.name.split("\\.") 18 | ArrayList bNames = b.name.split("\\.") 19 | 20 | int bigVersionDiff = Integer.parseInt(aNames[0]) - Integer.parseInt(bNames[0]) 21 | int featureVersionDiff = Integer.parseInt(aNames[1]) - Integer.parseInt(bNames[1]) 22 | int bugfixVersionDiff = Integer.parseInt(aNames[2]) - Integer.parseInt(bNames[2]) 23 | 24 | return bigVersionDiff > 0 || featureVersionDiff > 0 || bugfixVersionDiff > 0 ? 1 : 0 25 | } 26 | }) 27 | 28 | gitVersionCode = tags.size() 29 | gitVersionName = tags[tags.size()-1].getName() 30 | 31 | println 'gitVersionCode' 32 | println 'gitVersionName' 33 | println gitVersionCode 34 | println gitVersionName 35 | } 36 | 37 | android { 38 | compileSdkVersion 29 39 | buildToolsVersion "29.0.2" 40 | defaultConfig { 41 | applicationId "com.github.ttdyce.nhviewer" 42 | minSdkVersion 21 43 | targetSdkVersion 29 44 | versionCode gitVersionCode 45 | versionName gitVersionName 46 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 47 | 48 | //for blur 49 | renderscriptTargetApi 28 50 | renderscriptSupportModeEnabled true 51 | signingConfig signingConfigs.debug 52 | //export AppDatabase (Room database) to /schemas/*.json 53 | javaCompileOptions { 54 | annotationProcessorOptions { 55 | arguments = ["room.schemaLocation": "$projectDir/schemas".toString()] 56 | } 57 | } 58 | } 59 | signingConfigs { 60 | config 61 | release 62 | } 63 | buildTypes { 64 | debug { 65 | signingConfig signingConfigs.config 66 | applicationIdSuffix ".debug" 67 | } 68 | release { 69 | minifyEnabled false 70 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 71 | signingConfig signingConfigs.release 72 | } 73 | } 74 | 75 | //signing with travis config for release 76 | def isAppCenter = System.getenv("isAppCenter") == "true" 77 | if (isAppCenter) { 78 | // configure keystore 79 | signingConfigs.release.storeFile = signingConfigs.config.storeFile = file("myapps.jks") 80 | signingConfigs.release.storePassword = signingConfigs.config.storePassword = System.getenv("keystore_password") 81 | signingConfigs.release.keyAlias = signingConfigs.config.keyAlias = System.getenv("keystore_alias") 82 | signingConfigs.release.keyPassword = signingConfigs.config.keyPassword = System.getenv("keystore_alias_password") 83 | }else{ 84 | //signing with {project_root}/keystore.properties if exists 85 | def ksFile = rootProject.file('keystore.properties') 86 | def props = new Properties() 87 | 88 | if (ksFile.canRead()) { 89 | props.load(new FileInputStream(ksFile)) 90 | 91 | if (props != null) { 92 | android.signingConfigs.config.storeFile file(props['KEYSTORE_FILE']) 93 | android.signingConfigs.config.storePassword props['KEYSTORE_PASSWORD'] 94 | android.signingConfigs.config.keyAlias props['KEYSTORE_ALIAS'] 95 | android.signingConfigs.config.keyPassword props['KEYSTORE_ALIAS_PASSWORD'] 96 | } else { 97 | println 'some entries in \'keystore.properties\' not found!' 98 | } 99 | } else { 100 | println '\'keystore.properties\' not found! Using default signing config' 101 | android.signingConfigs.config.storeFile android.signingConfigs.debug.storeFile 102 | android.signingConfigs.config.storePassword android.signingConfigs.debug.storePassword 103 | android.signingConfigs.config.keyAlias android.signingConfigs.debug.keyAlias 104 | android.signingConfigs.config.keyPassword android.signingConfigs.debug.keyPassword 105 | } 106 | } 107 | 108 | 109 | compileOptions { 110 | sourceCompatibility JavaVersion.VERSION_1_8 111 | targetCompatibility JavaVersion.VERSION_1_8 112 | } 113 | } 114 | 115 | dependencies { 116 | def nav_version = '2.2.2' 117 | def room_version = '2.4.0' 118 | def preference_version = '1.1.1' 119 | def appCenterSdkVersion = '4.1.0' 120 | 121 | // Visual Studio App Center 122 | implementation "com.microsoft.appcenter:appcenter-analytics:${appCenterSdkVersion}" 123 | implementation "com.microsoft.appcenter:appcenter-crashes:${appCenterSdkVersion}" 124 | implementation "com.microsoft.appcenter:appcenter-distribute:${appCenterSdkVersion}" 125 | 126 | implementation "com.github.bumptech.glide:volley-integration:4.11.0" 127 | 128 | implementation 'com.google.android.material:material:1.1.0' 129 | //image transformations for Glide (for blur image) 130 | implementation 'jp.wasabeef:glide-transformations:4.3.0' 131 | //QRCode scanner 132 | implementation 'com.dlazaro66.qrcodereaderview:qrcodereaderview:2.0.2' 133 | //ROOM 134 | implementation "androidx.room:room-runtime:$room_version" 135 | annotationProcessor "androidx.room:room-compiler:$room_version" 136 | //Glide 137 | implementation 'com.github.bumptech.glide:glide:4.11.0' 138 | annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0' 139 | //preferenceFragment 140 | implementation "androidx.preference:preference:$preference_version" 141 | //for Jetpack navigation 142 | implementation "androidx.navigation:navigation-fragment:$nav_version" 143 | implementation "androidx.navigation:navigation-ui:$nav_version" 144 | implementation 'com.google.code.gson:gson:2.8.6' 145 | implementation 'com.android.volley:volley:1.2.1' 146 | implementation 'androidx.legacy:legacy-support-v4:1.0.0' 147 | testImplementation 'org.json:json:20200518'//for unit test using json 148 | implementation fileTree(dir: 'libs', include: ['*.jar']) 149 | implementation 'androidx.appcompat:appcompat:1.1.0' 150 | implementation 'androidx.constraintlayout:constraintlayout:1.1.3' 151 | testImplementation 'junit:junit:4.13' 152 | androidTestImplementation 'androidx.test:runner:1.2.0' 153 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' 154 | } 155 | 156 | //apply plugin: 'com.google.gms.google-services' // seems not using anymore 157 | -------------------------------------------------------------------------------- /app/debug/output.json: -------------------------------------------------------------------------------- 1 | [{"outputType":{"type":"APK"},"apkData":{"type":"MAIN","splits":[],"versionCode":1,"versionName":"1.0","enabled":true,"outputFile":"app-debug.apk","fullName":"debug","baseName":"debug"},"path":"app-debug.apk","properties":{}}] -------------------------------------------------------------------------------- /app/myapps.jks.enc: -------------------------------------------------------------------------------- 1 | U2FsdGVkX1+HbtGCWHdmojGSH6BZncaviW8e02gplw1v5OnwPs1MfPU71Qm+jgni 2 | XyB1pLRW1vcHOVgK3ycnAssVin38jISNM88JY0SS+qSka9R5nqAJyyUgl9LjYoJC 3 | CC7zM9nJ08Sa0w0rMY593NXr+O8xhAwEftBwWiyepVl0NYJJNG1bUETwfb3KE2VS 4 | jE3ILxiRgeLEKJvIOd8vwyEJw7cjDV1CTf7JeDmuzpnChMz5lp9yj1LbKO8+BF2L 5 | i9rDiJFE2xz3bSPJBrV8q1kWemQuJ9V4c5LVTevXDzazWuEJuYDSnfU/CzPF+h6Y 6 | iAeNNjW0K5S6EuxyL3ao9k3KrovIJl0K+CbWqqCpb5Mxq1B3BJzMW3n42HE2sdD/ 7 | YlrRDlSZq/uMJjkJNCRaEdGTU8CB29ayVhuOIz+r/wcJhAlOY+aKhBZElmk6sW+1 8 | 8nbwCi8uiTDv7dn0DYj/l4yv1r03AcgkmnkHHWgpmpnQfs4fM9qSbPDsEVEeJkZ8 9 | AgK8EwHNww98UCvweS/qO3tjkVBvpW8lm7U20D3GBDQ3vgGnG1YC+FQY1CAGlmja 10 | UX9jMbhL2KEEpozAJqGLvlpz8cBs6xkUGjoWbOUHcmH8O0HY8NOErTxB+SxuVnrz 11 | 7JlsbUJORWvvA0MGmqe18EazJm427+kmncZR7leOf7EIBZiAWxVqAfZXCny8h/cm 12 | 0+zXjgq8mVo0eTkyzOnee3xuKfyHTi7IsCzR4HksfwWRCq8iiASyfwn65yEJgQ9I 13 | aofPssTefYoYupkG/g4OS10GTr9dtUa41XJvYgTlN6DD9QIUqLSHFKQf91/d2w8z 14 | ioa749Ij8f0j/4Stkc8ixjr0WLl/tVQyNewc/HOLPf8L7ldIAhlpfnrnXxc0BKBu 15 | sxmy6V0L7+yfpAhGyqY9f6qXZQB4s2wu07Y+DFe3spEpL5kgMWo5EvSlzL/nIV+e 16 | GIXJ8GKSxUg620538Zk6ge5nuSggNHVVrJufCFKgzTFVrcCjxnz1sL3B5TD2Wqaq 17 | vdpeyyyoJ/fvh4AZu7rpkBgt8QRRk1plWCa3+6KPoylMaJ+hwzy0ZnwByvTcc+QU 18 | zQ0JIgn1hJRFARW3/1bXO2g8uFUryym7bT56tReQ57b/owiU5LDJm8nE8k8bsZx5 19 | ERpvgKNDY5HfkUBJpLnqGMX95mS620WQFtEyUzIyE2A9RcXm6mu98zAox0Y+EPEo 20 | D8ddr83jWqdF4W5mmTS9xLN8UfEmdzyjA5SFBTAvNRHpXCC+M8odS2eRDS/9y9CU 21 | s44PV750Jf4PI/zdXtqjq36eolIhL18gp1hPJAvq7gmz948WVEY3sjtoVHVikE+U 22 | /6rfoKaDgZ4vd5MAMlV/5On+Zo6kG7OzlcTkR3UGDXCkOU75g80DfPbf2OqaROdl 23 | 8Q2W6stYT0X8BFUzv/ZCkWh4QOC/J/vNb1r5igj369RKl3cw7gixEJbJW0coHVy3 24 | uAuuk44PBntQHoa/xTG5tSn5KJ7Xnqs5u91ddLxj53ehIt+PCKjgm6jBPDAoQcbL 25 | 8GYRDqJgeV1IUikFQWudzXb5IuuByKsVMhUuZjUgUcaUjwoTzkZ7MmGVU31daK6C 26 | kNplMxvBGnmGXSplS+RVC2uQi7ZEa+vrAReHG3vTpSaEaevZoN1B0P0X8aKg/mPz 27 | 2KiBmm+Sh3M9mdxhvbNPykkmq2xL+oOi7Qaogrj9/+Aok3o/GoPEU2UdzCeGQN2z 28 | QQ/u4kjfFs54A642gb+OyW99wA8ZObXW5MTVLRxGj3/3IAbBXqXKT635AMCRhIu8 29 | JTPR6FFCk5s1lomdanhOLqqm8Mhj6kncMEhSIW0oMKRgqOxJuTPdsm6vofHVCHNQ 30 | eKvKUP5kljiMW2LOWGLU7e/YwvjvMvJHFK8dbOvFKWz1rpOud9IaW9F2FpQdwWV1 31 | C8lRA03x4ZFcXaBEoxQL0ZArD3KCpwyyUXBb8zQxzbFdmOPeoV6AbcqLsjBt6X4E 32 | T0eDq1oJuMPFF33CnMnZb1jqcKf4vBttIEKnYe9XUmIRUMSfgSq7qM6be3aU/Fp2 33 | 5b8Ocy2Fc168tikP4L90X61l+W009OPuNpJcNe5IcipBl5Fzr8oGVaU9xGyVJx38 34 | v77PzwzyB+ElC1U4y7MdzRi+Vgwp04KefJw0mhbkZJLg+MbbRsQnqZ2VGMDv2F/Q 35 | nEjrtimvBEP22TtOjBxoxkjK8Sc3eLMR+5qAHgfzwffNEQbLPTC4XPiGMCXVGpXQ 36 | JGxkT6wyyTNm8qCStfuuhQ0SpVmvn6g7VrNH9kJkiSJD/DfLpcT/mA1fX/WCLtIB 37 | NNB42qWlPd0ym9U4Vo6G7SfbCdULvaDmAgLStEAgPY8v6u1aax/QYb8WrYIpV/2X 38 | xIFipm4Rt1i9eqqHKcD3LadGwAZ6sSPY2bGLiGwqao8OZe6mNCwOVGcpDAcfLm/C 39 | K/+2/mtaa7CyeWfWp05AxdzEhOFKuuu7rIr7e4xcgpWIZuK+MtbCfunaDFaIQ13L 40 | mZj6681AL1Ue7fAr6OVFCGi/rC0sP9J2m1d4PvmMiMsguqEYcw8cT//1ysSsFW7Z 41 | pP7ZgPzNdBcyEC0hCy1TCALyTTvtrt9tEMJpeHAIx10ydmA5BfkQ0C++0KHRbfdD 42 | IkAZgVrUuNd3NjIs/I1PeqLskYANOhjI4cobLnu/h35nTBLDF+4H+fsG3jfT43Ww 43 | C2CjKvU0pZIxO4F+60shVv5+w8f4Dt1cFosviyZ8zvAqduaafJUeuJ59ezeciwHS 44 | F3eMXeq/zS6g43asnXgrAyMGMAcIVzyy08wSGnKu1WR07N4emVj49RtoKeyPVglb 45 | /i+DiRClk4V9pgtPl4/MVv8f+HrnCq4VUtEArDXYf/kLxmwDU41TgvbhkwpJS1D8 46 | TE93MNqX0DYZW+goLAikQOxHdt0SiyOzXxH+d9CpH5PIdWIKWS5NdR5JzPFnmgsE 47 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /app/schemas/com.github.ttdyce.nhviewer.model.room.AppDatabase/1.json: -------------------------------------------------------------------------------- 1 | { 2 | "formatVersion": 1, 3 | "database": { 4 | "version": 1, 5 | "identityHash": "b0fd0de536c780c474fb1a1caaeb3786", 6 | "entities": [ 7 | { 8 | "tableName": "ComicCollection", 9 | "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `id` INTEGER NOT NULL, `dateCreated` TEXT NOT NULL, PRIMARY KEY(`name`, `id`))", 10 | "fields": [ 11 | { 12 | "fieldPath": "name", 13 | "columnName": "name", 14 | "affinity": "TEXT", 15 | "notNull": true 16 | }, 17 | { 18 | "fieldPath": "id", 19 | "columnName": "id", 20 | "affinity": "INTEGER", 21 | "notNull": true 22 | }, 23 | { 24 | "fieldPath": "dateCreated", 25 | "columnName": "dateCreated", 26 | "affinity": "TEXT", 27 | "notNull": true 28 | } 29 | ], 30 | "primaryKey": { 31 | "columnNames": [ 32 | "name", 33 | "id" 34 | ], 35 | "autoGenerate": false 36 | }, 37 | "indices": [], 38 | "foreignKeys": [] 39 | }, 40 | { 41 | "tableName": "ComicCached", 42 | "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `mid` TEXT NOT NULL, `title` TEXT NOT NULL, `pageTypes` TEXT NOT NULL, `numOfPages` INTEGER NOT NULL, PRIMARY KEY(`id`))", 43 | "fields": [ 44 | { 45 | "fieldPath": "id", 46 | "columnName": "id", 47 | "affinity": "INTEGER", 48 | "notNull": true 49 | }, 50 | { 51 | "fieldPath": "mid", 52 | "columnName": "mid", 53 | "affinity": "TEXT", 54 | "notNull": true 55 | }, 56 | { 57 | "fieldPath": "title", 58 | "columnName": "title", 59 | "affinity": "TEXT", 60 | "notNull": true 61 | }, 62 | { 63 | "fieldPath": "pageTypes", 64 | "columnName": "pageTypes", 65 | "affinity": "TEXT", 66 | "notNull": true 67 | }, 68 | { 69 | "fieldPath": "numOfPages", 70 | "columnName": "numOfPages", 71 | "affinity": "INTEGER", 72 | "notNull": true 73 | } 74 | ], 75 | "primaryKey": { 76 | "columnNames": [ 77 | "id" 78 | ], 79 | "autoGenerate": false 80 | }, 81 | "indices": [], 82 | "foreignKeys": [] 83 | }, 84 | { 85 | "tableName": "ComicBookmark", 86 | "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`page` INTEGER NOT NULL, `id` INTEGER NOT NULL, PRIMARY KEY(`id`, `page`))", 87 | "fields": [ 88 | { 89 | "fieldPath": "page", 90 | "columnName": "page", 91 | "affinity": "INTEGER", 92 | "notNull": true 93 | }, 94 | { 95 | "fieldPath": "id", 96 | "columnName": "id", 97 | "affinity": "INTEGER", 98 | "notNull": true 99 | } 100 | ], 101 | "primaryKey": { 102 | "columnNames": [ 103 | "id", 104 | "page" 105 | ], 106 | "autoGenerate": false 107 | }, 108 | "indices": [], 109 | "foreignKeys": [] 110 | } 111 | ], 112 | "views": [], 113 | "setupQueries": [ 114 | "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", 115 | "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'b0fd0de536c780c474fb1a1caaeb3786')" 116 | ] 117 | } 118 | } -------------------------------------------------------------------------------- /app/schemas/com.github.ttdyce.nhviewer.model.room.AppDatabase/2.json: -------------------------------------------------------------------------------- 1 | { 2 | "formatVersion": 1, 3 | "database": { 4 | "version": 2, 5 | "identityHash": "5522322ef61d43afac94f7fa9c9c083e", 6 | "entities": [ 7 | { 8 | "tableName": "ComicCollection", 9 | "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `id` INTEGER NOT NULL, `dateCreated` TEXT NOT NULL, PRIMARY KEY(`name`, `id`))", 10 | "fields": [ 11 | { 12 | "fieldPath": "name", 13 | "columnName": "name", 14 | "affinity": "TEXT", 15 | "notNull": true 16 | }, 17 | { 18 | "fieldPath": "id", 19 | "columnName": "id", 20 | "affinity": "INTEGER", 21 | "notNull": true 22 | }, 23 | { 24 | "fieldPath": "dateCreated", 25 | "columnName": "dateCreated", 26 | "affinity": "TEXT", 27 | "notNull": true 28 | } 29 | ], 30 | "primaryKey": { 31 | "columnNames": [ 32 | "name", 33 | "id" 34 | ], 35 | "autoGenerate": false 36 | }, 37 | "indices": [], 38 | "foreignKeys": [] 39 | }, 40 | { 41 | "tableName": "ComicCached", 42 | "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `mid` TEXT NOT NULL, `title` TEXT NOT NULL, `pageTypes` TEXT NOT NULL, `numOfPages` INTEGER NOT NULL, PRIMARY KEY(`id`))", 43 | "fields": [ 44 | { 45 | "fieldPath": "id", 46 | "columnName": "id", 47 | "affinity": "INTEGER", 48 | "notNull": true 49 | }, 50 | { 51 | "fieldPath": "mid", 52 | "columnName": "mid", 53 | "affinity": "TEXT", 54 | "notNull": true 55 | }, 56 | { 57 | "fieldPath": "title", 58 | "columnName": "title", 59 | "affinity": "TEXT", 60 | "notNull": true 61 | }, 62 | { 63 | "fieldPath": "pageTypes", 64 | "columnName": "pageTypes", 65 | "affinity": "TEXT", 66 | "notNull": true 67 | }, 68 | { 69 | "fieldPath": "numOfPages", 70 | "columnName": "numOfPages", 71 | "affinity": "INTEGER", 72 | "notNull": true 73 | } 74 | ], 75 | "primaryKey": { 76 | "columnNames": [ 77 | "id" 78 | ], 79 | "autoGenerate": false 80 | }, 81 | "indices": [], 82 | "foreignKeys": [] 83 | }, 84 | { 85 | "tableName": "ComicBookmark", 86 | "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`page` INTEGER NOT NULL, `id` INTEGER NOT NULL, `dateOfCreate` TEXT, PRIMARY KEY(`id`, `page`))", 87 | "fields": [ 88 | { 89 | "fieldPath": "page", 90 | "columnName": "page", 91 | "affinity": "INTEGER", 92 | "notNull": true 93 | }, 94 | { 95 | "fieldPath": "id", 96 | "columnName": "id", 97 | "affinity": "INTEGER", 98 | "notNull": true 99 | }, 100 | { 101 | "fieldPath": "dateOfCreate", 102 | "columnName": "dateOfCreate", 103 | "affinity": "TEXT", 104 | "notNull": false 105 | } 106 | ], 107 | "primaryKey": { 108 | "columnNames": [ 109 | "id", 110 | "page" 111 | ], 112 | "autoGenerate": false 113 | }, 114 | "indices": [], 115 | "foreignKeys": [] 116 | } 117 | ], 118 | "views": [], 119 | "setupQueries": [ 120 | "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", 121 | "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '5522322ef61d43afac94f7fa9c9c083e')" 122 | ] 123 | } 124 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/github/ttdyce/nhviewer/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.github.ttdyce.nhviewer; 2 | 3 | import android.content.Context; 4 | import androidx.test.platform.app.InstrumentationRegistry; 5 | import androidx.test.runner.AndroidJUnit4; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import static org.junit.Assert.*; 11 | 12 | /** 13 | * Instrumented test, which will execute on an Android device. 14 | * 15 | * @see Testing documentation 16 | */ 17 | @RunWith(AndroidJUnit4.class) 18 | public class ExampleInstrumentedTest { 19 | @Test 20 | public void useAppContext() { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); 23 | 24 | assertEquals("com.github.ttdyce.nhviewer", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 19 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 41 | 42 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ttdyce/NHentai-NHViewer/12d8416263166b4f15369b00a4255eb8039efe63/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /app/src/main/ic_launcher_nhviewer-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ttdyce/NHentai-NHViewer/12d8416263166b4f15369b00a4255eb8039efe63/app/src/main/ic_launcher_nhviewer-web.png -------------------------------------------------------------------------------- /app/src/main/java/com/github/ttdyce/nhviewer/model/CookieStringRequest.java: -------------------------------------------------------------------------------- 1 | package com.github.ttdyce.nhviewer.model; 2 | 3 | import androidx.annotation.Nullable; 4 | 5 | import com.android.volley.AuthFailureError; 6 | import com.android.volley.Response; 7 | import com.android.volley.toolbox.StringRequest; 8 | 9 | import java.util.HashMap; 10 | import java.util.Map; 11 | 12 | public class CookieStringRequest extends StringRequest { 13 | public static String challengeCookies; 14 | public static String userAgent; 15 | private Map headers = new HashMap<>(); 16 | 17 | public CookieStringRequest(int method, String url, Response.Listener listener, @Nullable Response.ErrorListener errorListener) { 18 | super(method, url, listener, errorListener); 19 | headers.put("cookie", challengeCookies); 20 | headers.put("user-agent", userAgent); 21 | } 22 | 23 | // not in use (as for 8/1/2022) 24 | public CookieStringRequest(String url, Response.Listener listener, @Nullable Response.ErrorListener errorListener) { 25 | super(url, listener, errorListener); 26 | headers.put("cookie", challengeCookies); 27 | headers.put("user-agent", userAgent); 28 | } 29 | 30 | @Override 31 | public Map getHeaders() throws AuthFailureError { 32 | return headers; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/ttdyce/nhviewer/model/CustomGlideModule.java: -------------------------------------------------------------------------------- 1 | package com.github.ttdyce.nhviewer.model; 2 | 3 | import android.content.Context; 4 | 5 | import androidx.annotation.NonNull; 6 | 7 | import com.android.volley.toolbox.Volley; 8 | import com.bumptech.glide.Glide; 9 | import com.bumptech.glide.GlideBuilder; 10 | import com.bumptech.glide.Registry; 11 | import com.bumptech.glide.annotation.GlideModule; 12 | import com.bumptech.glide.integration.volley.VolleyUrlLoader; 13 | import com.bumptech.glide.load.engine.DiskCacheStrategy; 14 | import com.bumptech.glide.load.model.GlideUrl; 15 | import com.bumptech.glide.module.AppGlideModule; 16 | import com.bumptech.glide.request.RequestOptions; 17 | import com.github.ttdyce.nhviewer.model.proxy.NHVProxyStack; 18 | import com.github.ttdyce.nhviewer.view.MainActivity; 19 | 20 | import java.io.InputStream; 21 | 22 | @GlideModule 23 | public class CustomGlideModule extends AppGlideModule { 24 | 25 | @Override 26 | public void registerComponents(Context context, Glide glide, Registry registry) { 27 | VolleyUrlLoader.Factory factory = new VolleyUrlLoader.Factory(MainActivity.isProxied() 28 | ? Volley.newRequestQueue(context, new NHVProxyStack(MainActivity.proxyHost, MainActivity.proxyPort)) 29 | : Volley.newRequestQueue(context) 30 | ); 31 | glide.getRegistry().replace(GlideUrl.class, InputStream.class, factory); 32 | } 33 | 34 | @Override 35 | public void applyOptions(@NonNull Context context, @NonNull GlideBuilder builder) { 36 | // disable glide/volley disk cache to avoid duplicate data...? 37 | builder.setDefaultRequestOptions( 38 | new RequestOptions() 39 | .diskCacheStrategy(DiskCacheStrategy.NONE) 40 | ); 41 | } 42 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/ttdyce/nhviewer/model/MyDistributeListener.java: -------------------------------------------------------------------------------- 1 | package com.github.ttdyce.nhviewer.model; 2 | 3 | import android.app.Activity; 4 | import android.content.DialogInterface; 5 | import android.widget.Toast; 6 | 7 | import com.github.ttdyce.nhviewer.R; 8 | import com.google.android.material.dialog.MaterialAlertDialogBuilder; 9 | import com.microsoft.appcenter.distribute.Distribute; 10 | import com.microsoft.appcenter.distribute.DistributeListener; 11 | import com.microsoft.appcenter.distribute.ReleaseDetails; 12 | import com.microsoft.appcenter.distribute.UpdateAction; 13 | 14 | public class MyDistributeListener implements DistributeListener { 15 | 16 | @Override 17 | public boolean onReleaseAvailable(Activity activity, ReleaseDetails releaseDetails) { 18 | 19 | // Look at releaseDetails public methods to get version information, release notes text or release notes URL 20 | String versionName = releaseDetails.getShortVersion(); 21 | // int versionCode = releaseDetails.getVersion(); 22 | String releaseNotes = releaseDetails.getReleaseNotes(); 23 | // Uri releaseNotesUrl = releaseDetails.getReleaseNotesUrl(); 24 | 25 | // Build our own dialog title and message 26 | new MaterialAlertDialogBuilder(activity, R.style.MaterialDialogTheme) 27 | .setTitle(activity.getString(R.string.new_version_available, versionName)) 28 | .setMessage(releaseNotes) 29 | .setPositiveButton(activity.getString(com.microsoft.appcenter.distribute.R.string.appcenter_distribute_update_dialog_download), 30 | new DialogInterface.OnClickListener() { 31 | @Override 32 | public void onClick(DialogInterface dialog, int which) { 33 | Distribute.notifyUpdateAction(UpdateAction.UPDATE); 34 | } 35 | }).setNegativeButton(activity.getString(com.microsoft.appcenter.distribute.R.string.appcenter_distribute_update_dialog_postpone), 36 | new DialogInterface.OnClickListener() { 37 | @Override 38 | public void onClick(DialogInterface dialog, int which) { 39 | Distribute.notifyUpdateAction(UpdateAction.POSTPONE); 40 | } 41 | }) 42 | .setOnCancelListener(new DialogInterface.OnCancelListener() { 43 | @Override 44 | public void onCancel(DialogInterface dialog) { 45 | Distribute.notifyUpdateAction(UpdateAction.POSTPONE); 46 | } 47 | }) 48 | .create().show(); 49 | 50 | // Return true if you're using your own dialog, false otherwise 51 | return true; 52 | 53 | 54 | 55 | } 56 | 57 | @Override 58 | public void onNoReleaseAvailable(Activity activity) { 59 | Toast.makeText(activity.getApplicationContext(), "no updates available", Toast.LENGTH_LONG).show(); 60 | } 61 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/ttdyce/nhviewer/model/api/NHAPI.java: -------------------------------------------------------------------------------- 1 | package com.github.ttdyce.nhviewer.model.api; 2 | 3 | import android.content.Context; 4 | import android.content.SharedPreferences; 5 | import android.util.Log; 6 | 7 | import com.android.volley.Request; 8 | import com.android.volley.RequestQueue; 9 | import com.android.volley.Response; 10 | import com.android.volley.VolleyError; 11 | import com.android.volley.toolbox.Volley; 12 | import com.github.ttdyce.nhviewer.R; 13 | import com.github.ttdyce.nhviewer.model.CookieStringRequest; 14 | import com.github.ttdyce.nhviewer.model.proxy.NHVProxyStack; 15 | import com.github.ttdyce.nhviewer.view.MainActivity; 16 | import com.github.ttdyce.nhviewer.view.SettingsFragment; 17 | import com.google.gson.JsonArray; 18 | import com.google.gson.JsonParser; 19 | 20 | import java.util.Locale; 21 | 22 | public class NHAPI { 23 | private static final String TAG = "NHAPI"; 24 | private Context context; 25 | private String proxyHost; 26 | private int proxyPort; 27 | private RequestQueue requestQueue; 28 | private RequestQueue requestQueueProxied; 29 | 30 | public NHAPI(Context context, String proxyHost, int proxyPort) { 31 | this.context = context; 32 | this.proxyHost = proxyHost; 33 | this.proxyPort = proxyPort; 34 | requestQueue = Volley.newRequestQueue(context); 35 | requestQueueProxied = Volley.newRequestQueue(context, new NHVProxyStack(proxyHost, proxyPort)); 36 | } 37 | 38 | /* 39 | * Return a JsonArray string containing 25 Comic object, as [ {"id": 284928,"media_id": "1483523",...}, ...] 40 | * */ 41 | public void getComicList(String query, int page, PopularType popularType, final ResponseCallback callback, SharedPreferences pref) { 42 | String languageId = pref.getString(MainActivity.KEY_PREF_DEFAULT_LANGUAGE, SettingsFragment.Language.notSet.toString()); 43 | int languageIdInt = Integer.parseInt(languageId); 44 | 45 | final String[] languageArray = context.getResources().getStringArray(R.array.key_languages); 46 | String language = languageArray[languageIdInt]; 47 | 48 | // choose the RequestQueue. 49 | RequestQueue queue = MainActivity.isProxied() ? requestQueueProxied : requestQueue; 50 | String url = URLs.search("language:" + language + " " + query, page, popularType); 51 | Log.d(TAG, "getComicList: loading from url " + url); 52 | Log.d(TAG, "getComicList: language id = " + languageId); 53 | if (languageIdInt == SettingsFragment.Language.all.getInt() || languageIdInt == SettingsFragment.Language.notSet.getInt())// TODO: 2019/10/1 Function is limited if language = all 54 | url = URLs.getIndex(page); 55 | 56 | while (CookieStringRequest.challengeCookies == null) { 57 | try { 58 | Thread.sleep(300); 59 | } catch (InterruptedException e) { 60 | e.printStackTrace(); 61 | Log.d(TAG, "getComicList: CookieStringRequest.challengeCookies is still null!"); 62 | } 63 | } 64 | 65 | Log.i(TAG, "getComicList: CookieStringRequest.challengeCookies is ready (SplashScreen/RefreshCookieScreen is ok) "); 66 | // Request a string response from the provided URL. 67 | CookieStringRequest stringRequest = new CookieStringRequest(Request.Method.GET, url, 68 | new Response.Listener() { 69 | @Override 70 | public void onResponse(String response) { 71 | JsonArray result = JsonParser.parseString(response).getAsJsonObject().get("result").getAsJsonArray(); 72 | callback.onReponse(result.toString()); 73 | } 74 | }, new Response.ErrorListener() { 75 | @Override 76 | public void onErrorResponse(VolleyError error) { 77 | callback.onErrorResponse(error); 78 | } 79 | }); 80 | 81 | // Add the request to the RequestQueue. 82 | queue.add(stringRequest); 83 | 84 | } 85 | 86 | public void getComic(int id, final ResponseCallback callback) { 87 | Log.d(TAG, "nhapi: getting comic"); 88 | // Get the RequestQueue. 89 | RequestQueue queue = MainActivity.isProxied() ? requestQueueProxied : requestQueue; 90 | String url = URLs.getComic(id); 91 | 92 | while (CookieStringRequest.challengeCookies == null) { 93 | try { 94 | Thread.sleep(300); 95 | } catch (InterruptedException e) { 96 | e.printStackTrace(); 97 | Log.d(TAG, "getComic: CookieStringRequest.challengeCookies is still null!"); 98 | } 99 | } 100 | 101 | Log.i(TAG, "getComic: CookieStringRequest.challengeCookies is ready (SplashScreen/RefreshCookieScreen is ok) "); 102 | 103 | // Request a string response from the provided URL. 104 | CookieStringRequest stringRequest = new CookieStringRequest(Request.Method.GET, url, 105 | new Response.Listener() { 106 | @Override 107 | public void onResponse(String response) { 108 | Log.d(TAG, "onResponse: got comic"); 109 | 110 | callback.onReponse(response); 111 | } 112 | }, new Response.ErrorListener() { 113 | @Override 114 | public void onErrorResponse(VolleyError error) { 115 | callback.onErrorResponse(error); 116 | } 117 | }); 118 | 119 | // Add the request to the RequestQueue. 120 | queue.add(stringRequest); 121 | } 122 | 123 | //https://nhentai.net/api/galleries/search?query=language:chinese&page=1&sort=popular 124 | //https://nhentai.net/api/gallery/284987 125 | public static class URLs { 126 | private static String searchPrefix = "https://nhentai.net/api/galleries/search?query="; 127 | private static String getComicPrefix = "https://nhentai.net/api/gallery/"; 128 | private static String[] types = {"jpg", "png", "gif", "webp"}; 129 | 130 | public static String search(String query, int page, PopularType popularType) { 131 | if (popularType == PopularType.none) 132 | return searchPrefix + query + "&page=" + page; 133 | if (popularType == PopularType.allTime) 134 | return searchPrefix + query + "&page=" + page + "&sort=popular"; 135 | if (popularType == PopularType.month) 136 | return searchPrefix + query + "&page=" + page + "&sort=popular-month"; 137 | if (popularType == PopularType.week) 138 | return searchPrefix + query + "&page=" + page + "&sort=popular-week"; 139 | if (popularType == PopularType.today) 140 | return searchPrefix + query + "&page=" + page + "&sort=popular-today"; 141 | 142 | Log.w(TAG, "search: popular-type not found"); 143 | return searchPrefix + query + "&page=" + page;// should be not needed 144 | } 145 | 146 | public static String getComic(int id) { 147 | return getComicPrefix + id; 148 | } 149 | 150 | public static String getThumbnail(String mid, String type) { 151 | for (String t : types) { 152 | if (t.charAt(0) == type.charAt(0)) 153 | return String.format(Locale.ENGLISH, "https://t1.nhentai.net/galleries/%s/thumb.%s", mid, t); 154 | } 155 | 156 | return "";//should be not needed 157 | } 158 | 159 | public static String getPage(String mid, int page, String type) { 160 | for (String t : types) { 161 | if (t.charAt(0) == type.charAt(0)) 162 | return String.format(Locale.ENGLISH, "https://i1.nhentai.net/galleries/%s/%d.%s", mid, page, t); 163 | } 164 | 165 | return "";//should be not needed 166 | } 167 | 168 | public static String getIndex(int page) { 169 | return "https://nhentai.net/api/galleries/all?page=" + page; 170 | } 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/ttdyce/nhviewer/model/api/PopularType.java: -------------------------------------------------------------------------------- 1 | package com.github.ttdyce.nhviewer.model.api; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | 6 | public enum PopularType { 7 | none, allTime, month, week, today; 8 | 9 | @SuppressWarnings("UnusedAssignment") 10 | public static PopularType get(int which) { 11 | Map typeOptions = new HashMap<>(); 12 | int index = 0; 13 | typeOptions.put(index++, none); 14 | typeOptions.put(index++, allTime); 15 | typeOptions.put(index++, month); 16 | typeOptions.put(index++, week); 17 | typeOptions.put(index++, today); 18 | 19 | if(typeOptions.containsKey(which)) 20 | return typeOptions.get(which); 21 | else 22 | return none; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/ttdyce/nhviewer/model/api/ResponseCallback.java: -------------------------------------------------------------------------------- 1 | package com.github.ttdyce.nhviewer.model.api; 2 | 3 | import android.util.Log; 4 | 5 | import com.android.volley.VolleyError; 6 | 7 | public abstract class ResponseCallback { 8 | public abstract void onReponse(String response); 9 | public void onErrorResponse(VolleyError error){ 10 | Log.e("NH ResponseCallback", "onErrorResponse: ", error); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/ttdyce/nhviewer/model/comic/Comic.java: -------------------------------------------------------------------------------- 1 | package com.github.ttdyce.nhviewer.model.comic; 2 | 3 | import androidx.annotation.NonNull; 4 | 5 | import com.google.gson.annotations.SerializedName; 6 | 7 | public class Comic { 8 | private int id; 9 | 10 | @SerializedName("num_pages") 11 | private int numOfPages; 12 | 13 | @SerializedName("num_favorites") 14 | private int numOfFavorites; 15 | 16 | @SerializedName("media_id") 17 | private String mid; 18 | 19 | //included 3 title: eng, jp, pretty 20 | private Title title; 21 | //included 3 images: pages, cover, thumbnail 22 | private Images images; 23 | 24 | @SerializedName("upload_date") 25 | private int uploadDate; 26 | private Tag[] tags; 27 | 28 | public Comic() { 29 | } 30 | 31 | public Comic(int id) { 32 | this.id = id; 33 | } 34 | 35 | public Comic(int id, String mid, Title title, int numOfPages, String[] pageTypes) { 36 | this.id = id; 37 | this.mid = mid; 38 | this.title = title; 39 | this.numOfPages = numOfPages; 40 | this.setPageTypes(pageTypes); 41 | } 42 | 43 | public int getId() { 44 | return id; 45 | } 46 | 47 | public int getNumOfPages() { 48 | return numOfPages; 49 | } 50 | 51 | public int getNumOfFavorites() { 52 | return numOfFavorites; 53 | } 54 | 55 | public String getMid() { 56 | return mid; 57 | } 58 | 59 | public Title getTitle() { 60 | return title; 61 | } 62 | 63 | public Images getImages() { 64 | return images; 65 | } 66 | 67 | public int getUploadDate() { 68 | return uploadDate; 69 | } 70 | 71 | public Tag[] getTags() { 72 | return tags; 73 | } 74 | 75 | public String[] getPageTypes() { 76 | String[] types = new String[numOfPages]; 77 | Image[] pages = getImages().getPages(); 78 | 79 | for (int i = 0; i < types.length; i++) { 80 | types[i] = pages[i].getType(); 81 | } 82 | 83 | return types; 84 | } 85 | 86 | public void setPageTypes(String [] pageTypes) { 87 | if(getImages() == null) 88 | images = new Images(); 89 | 90 | Images images = getImages(); 91 | Image[] pages = images.getPages(); 92 | Image thumbnail = images.getThumbnail(); 93 | 94 | for (int i = 0; i < pageTypes.length; i++) { 95 | pages[i].type = pageTypes[i]; 96 | } 97 | // TODO: 2019/9/28 Hardcoded thumbnail type, using first page of inner page type 98 | thumbnail.type = pageTypes[0]; 99 | } 100 | 101 | /*Inner classes*/ 102 | 103 | //Comic Title 104 | public static class Title { 105 | String english, japanese, pretty; 106 | 107 | public Title(String english) { 108 | this.english = english; 109 | } 110 | 111 | @NonNull 112 | @Override 113 | public String toString() { 114 | return english; 115 | } 116 | } 117 | 118 | public class Images { 119 | Image[] pages; 120 | Image cover; 121 | Image thumbnail; 122 | 123 | public Images() { 124 | pages = new Image[numOfPages]; 125 | for (int i = 0; i < pages.length; i++) 126 | pages[i] = new Image(); 127 | 128 | cover = new Image(); 129 | thumbnail = new Image(); 130 | } 131 | 132 | public Image[] getPages() { 133 | return pages; 134 | } 135 | 136 | public Image getCover() { 137 | return cover; 138 | } 139 | 140 | public Image getThumbnail() { 141 | return thumbnail; 142 | } 143 | } 144 | 145 | //Comic Image 146 | public class Image { 147 | @SerializedName("t") 148 | String type; 149 | @SerializedName("w") 150 | int width; 151 | @SerializedName("h") 152 | int height; 153 | 154 | public Image(String type, int width, int height) { 155 | this.type = type; 156 | this.width = width; 157 | this.height = height; 158 | } 159 | 160 | public Image() { 161 | 162 | } 163 | 164 | public String getType() { 165 | return type; 166 | } 167 | 168 | public int getWidth() { 169 | return width; 170 | } 171 | 172 | public int getHeight() { 173 | return height; 174 | } 175 | } 176 | 177 | //Comic Tag 178 | public class Tag { 179 | int id, count; 180 | String type, name, url; 181 | 182 | public Tag(int id, int count, String type, String name, String url) { 183 | this.id = id; 184 | this.count = count; 185 | this.type = type; 186 | this.name = name; 187 | this.url = url; 188 | } 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/ttdyce/nhviewer/model/comic/ComicCollection.java: -------------------------------------------------------------------------------- 1 | package com.github.ttdyce.nhviewer.model.comic; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | public class ComicCollection { 7 | private String name; 8 | private List comicList; 9 | 10 | public ComicCollection(String name, List comicList) { 11 | this.name = name; 12 | this.comicList = comicList; 13 | } 14 | 15 | public ComicCollection(List ids, String name) { 16 | this.name = name; 17 | this.comicList = new ArrayList<>(); 18 | for (int id :ids) { 19 | if(id != -1) 20 | //there is at least a comic with id -1 in a empty collection 21 | comicList.add(new Comic(id)); 22 | } 23 | } 24 | 25 | public String getName() { 26 | return name; 27 | } 28 | 29 | public void setName(String name) { 30 | this.name = name; 31 | } 32 | 33 | public List getComicList() { 34 | return comicList; 35 | } 36 | 37 | public void setComicList(List comicList) { 38 | this.comicList = comicList; 39 | } 40 | 41 | public int getComicCount(){ 42 | return comicList.size() ; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/ttdyce/nhviewer/model/comic/factory/ComicFactory.java: -------------------------------------------------------------------------------- 1 | package com.github.ttdyce.nhviewer.model.comic.factory; 2 | 3 | import com.github.ttdyce.nhviewer.model.api.PopularType; 4 | 5 | public interface ComicFactory { 6 | void requestComicList(); 7 | 8 | void setPage(int page); 9 | void setSortBy(PopularType sortBy); 10 | } 11 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/ttdyce/nhviewer/model/comic/factory/DBComicFactory.java: -------------------------------------------------------------------------------- 1 | package com.github.ttdyce.nhviewer.model.comic.factory; 2 | 3 | import android.os.AsyncTask; 4 | 5 | import com.github.ttdyce.nhviewer.model.api.PopularType; 6 | import com.github.ttdyce.nhviewer.model.api.ResponseCallback; 7 | import com.github.ttdyce.nhviewer.model.comic.Comic; 8 | import com.github.ttdyce.nhviewer.model.room.AppDatabase; 9 | import com.github.ttdyce.nhviewer.model.room.ComicCachedDao; 10 | import com.github.ttdyce.nhviewer.model.room.ComicCachedEntity; 11 | import com.github.ttdyce.nhviewer.model.room.ComicCollectionDao; 12 | import com.github.ttdyce.nhviewer.model.room.ComicCollectionEntity; 13 | import com.google.gson.Gson; 14 | 15 | import java.util.ArrayList; 16 | import java.util.List; 17 | 18 | public class DBComicFactory implements ComicFactory { 19 | public static final int SORT_BY_DEFAULT = 0; 20 | public static final int SORT_BY_COLLECTION_DATE = 1; 21 | public static final int SORT_BY_COLLECTION_DATE_DESC = -1; 22 | 23 | private String collectionName; 24 | private AppDatabase db; 25 | private int page; 26 | private boolean sortedPopular; 27 | private ResponseCallback callback; 28 | 29 | public DBComicFactory(String collectionName, AppDatabase db, int page, boolean sortedPopular, ResponseCallback callback) { 30 | this.collectionName = collectionName; 31 | this.db = db; 32 | this.page = page; 33 | this.sortedPopular = sortedPopular; 34 | this.callback = callback; 35 | } 36 | 37 | @Override 38 | public void requestComicList() { 39 | new DisplayCollectionTask(db, collectionName, callback).execute(); 40 | } 41 | 42 | @Override 43 | public void setPage(int page) { 44 | this.page = page; 45 | } 46 | 47 | @Override 48 | public void setSortBy(PopularType sortBy) { 49 | // TODO: 2019/9/28 Not yet supported sorting in Collection 50 | } 51 | 52 | private static class DisplayCollectionTask extends AsyncTask{ 53 | 54 | private AppDatabase db; 55 | private String collectionName; 56 | private ResponseCallback callback; 57 | 58 | public DisplayCollectionTask(AppDatabase db, String collectionName, ResponseCallback callback) { 59 | this.db = db; 60 | this.collectionName = collectionName; 61 | this.callback = callback; 62 | } 63 | 64 | @Override 65 | protected String doInBackground(Void... voids) { 66 | 67 | ComicCachedDao cachedDao = db.comicCachedDao(); 68 | ComicCollectionDao collectionDao = db.comicCollectionDao(); 69 | List ids = new ArrayList<>(); 70 | List comics = new ArrayList<>(); 71 | 72 | for (ComicCollectionEntity entity:collectionDao.getAllByName(collectionName)) 73 | ids.add(entity.getId()); 74 | 75 | for (int id : ids) { 76 | ComicCachedEntity entity = cachedDao.findById(id); 77 | if(entity == null) 78 | continue; 79 | int numOfPages = entity.getNumOfPages(); 80 | String mid = entity.getMid(); 81 | Comic.Title title = new Comic.Title(entity.getTitle()); 82 | String[] pageTypes = entity.getPageTypes().split("(?!^)"); 83 | comics.add(new Comic(id, mid, title, numOfPages, pageTypes)); 84 | } 85 | 86 | //create api-like json object 87 | Gson gson = new Gson(); 88 | 89 | return gson.toJson(comics); 90 | } 91 | 92 | @Override 93 | protected void onPostExecute(String result) { 94 | super.onPostExecute(result); 95 | 96 | callback.onReponse(result); 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/ttdyce/nhviewer/model/comic/factory/NHApiComicFactory.java: -------------------------------------------------------------------------------- 1 | package com.github.ttdyce.nhviewer.model.comic.factory; 2 | 3 | import android.content.SharedPreferences; 4 | 5 | import com.github.ttdyce.nhviewer.model.api.NHAPI; 6 | import com.github.ttdyce.nhviewer.model.api.PopularType; 7 | import com.github.ttdyce.nhviewer.model.api.ResponseCallback; 8 | 9 | public class NHApiComicFactory implements ComicFactory { 10 | private NHAPI nhapi; 11 | private String query; 12 | private int page; 13 | private PopularType popularType; 14 | private ResponseCallback callback; 15 | private final SharedPreferences pref; 16 | 17 | public NHApiComicFactory(NHAPI nhapi, String query, int page, PopularType popularType, ResponseCallback callback, SharedPreferences pref) { 18 | this.nhapi = nhapi; 19 | this.query = query; 20 | this.page = page; 21 | this.popularType = popularType; 22 | this.callback = callback; 23 | this.pref = pref; 24 | } 25 | 26 | @Override 27 | public void requestComicList() { 28 | nhapi.getComicList(query, page, popularType, callback, pref); 29 | 30 | } 31 | 32 | @Override 33 | public void setPage(int page) { 34 | this.page = page; 35 | } 36 | 37 | @Override 38 | public void setSortBy(PopularType popularType) { 39 | this.popularType = popularType; 40 | } 41 | 42 | public static void getComicById(NHAPI api, int id, ResponseCallback callback){ 43 | api.getComic(id, callback); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/ttdyce/nhviewer/model/proxy/NHVProxyStack.java: -------------------------------------------------------------------------------- 1 | package com.github.ttdyce.nhviewer.model.proxy; 2 | 3 | import com.android.volley.toolbox.HurlStack; 4 | 5 | import java.io.IOException; 6 | import java.net.HttpURLConnection; 7 | import java.net.InetSocketAddress; 8 | import java.net.Proxy; 9 | import java.net.URL; 10 | 11 | public class NHVProxyStack extends HurlStack { 12 | private String proxyHost; 13 | private int proxyPort; 14 | 15 | public NHVProxyStack(String proxyHost, int proxyPort) { 16 | super(); 17 | this.proxyHost = proxyHost; 18 | this.proxyPort = proxyPort; 19 | } 20 | 21 | @Override 22 | protected HttpURLConnection createConnection(URL url) throws IOException { 23 | // Start the connection by specifying a proxy server 24 | Proxy proxy = new Proxy(Proxy.Type.HTTP, 25 | InetSocketAddress.createUnresolved(proxyHost, proxyPort ));//proxy server 26 | return (HttpURLConnection) url 27 | .openConnection(proxy); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/ttdyce/nhviewer/model/room/AppDatabase.java: -------------------------------------------------------------------------------- 1 | package com.github.ttdyce.nhviewer.model.room; 2 | 3 | import androidx.room.Database; 4 | import androidx.room.RoomDatabase; 5 | import androidx.room.TypeConverters; 6 | import androidx.room.migration.Migration; 7 | import androidx.sqlite.db.SupportSQLiteDatabase; 8 | 9 | @Database(entities = {ComicCollectionEntity.class, ComicCachedEntity.class, ComicBookmarkEntity.class} 10 | , version = 2) 11 | @TypeConverters({DateConverter.class}) 12 | public abstract class AppDatabase extends RoomDatabase { 13 | public static final String COL_COLLECTION_HISTORY = "History"; 14 | public static final String COL_COLLECTION_FAVORITE = "Favorite"; 15 | public static final String COL_COLLECTION_NEXT = "Next"; 16 | public static final String DB_NAME = "Nhviewer"; 17 | 18 | public abstract ComicCollectionDao comicCollectionDao(); 19 | 20 | public abstract ComicCachedDao comicCachedDao(); 21 | 22 | public abstract ComicBookmarkDao comicBookmarkDao(); 23 | 24 | public static final Migration MIGRATION_1_2 = new Migration(1, 2) { 25 | @Override 26 | public void migrate(SupportSQLiteDatabase database) { 27 | // add comic bookmark table 28 | database.execSQL("CREATE TABLE IF NOT EXISTS `ComicBookmark` (`page` INTEGER NOT NULL, `id` INTEGER NOT NULL, `dateOfCreate` TEXT, PRIMARY KEY(`id`, `page`))"); 29 | } 30 | }; 31 | 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/ttdyce/nhviewer/model/room/ComicBookmarkDao.java: -------------------------------------------------------------------------------- 1 | package com.github.ttdyce.nhviewer.model.room; 2 | 3 | import androidx.room.Dao; 4 | import androidx.room.Delete; 5 | import androidx.room.Insert; 6 | import androidx.room.Query; 7 | import androidx.room.Update; 8 | 9 | import java.util.List; 10 | 11 | @Dao 12 | public interface ComicBookmarkDao { 13 | String ORDER_BY_DEFAULT = "dateOfCreate DESC"; 14 | 15 | @Query("SELECT * FROM ComicBookmark WHERE id = :id ORDER BY " + ORDER_BY_DEFAULT) 16 | List getById(int id); 17 | 18 | @Query("SELECT * FROM ComicBookmark ORDER BY " + ORDER_BY_DEFAULT) 19 | List getAll(); 20 | 21 | @Query("SELECT count(*) == 0 FROM ComicBookmark Where id = :id AND page = :page") 22 | boolean notExist(int id, int page); 23 | 24 | @Insert 25 | void insertAll(ComicBookmarkEntity... comicBookmark); 26 | 27 | @Insert 28 | void insert(ComicBookmarkEntity comicBookmark); 29 | 30 | @Delete 31 | void delete(ComicBookmarkEntity comicBookmark); 32 | 33 | @Update 34 | void update(ComicBookmarkEntity comicBookmark); 35 | } 36 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/ttdyce/nhviewer/model/room/ComicBookmarkEntity.java: -------------------------------------------------------------------------------- 1 | package com.github.ttdyce.nhviewer.model.room; 2 | 3 | import androidx.room.Entity; 4 | 5 | import java.util.Date; 6 | 7 | @Entity(tableName = "ComicBookmark", primaryKeys = {"id", "page"}) 8 | public class ComicBookmarkEntity { 9 | 10 | private int page; 11 | private int id; 12 | private Date dateOfCreate; 13 | 14 | public ComicBookmarkEntity(int page, int id) { 15 | this.page = page; 16 | this.id = id; 17 | this.dateOfCreate = new Date(); 18 | } 19 | 20 | // Room uses this factory method to create ComicCollectionEntity objects. 21 | public static ComicBookmarkEntity create(int page, int id) { 22 | return new ComicBookmarkEntity(page, id); 23 | } 24 | 25 | public int getPage() { 26 | return page; 27 | } 28 | 29 | public void setPage(int page) { 30 | this.page = page; 31 | } 32 | 33 | public int getId() { 34 | return id; 35 | } 36 | 37 | public void setId(int id) { 38 | this.id = id; 39 | } 40 | 41 | public Date getDateOfCreate() { 42 | return dateOfCreate; 43 | } 44 | 45 | public void setDateOfCreate(Date dateOfCreate) { 46 | this.dateOfCreate = dateOfCreate; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/ttdyce/nhviewer/model/room/ComicCachedDao.java: -------------------------------------------------------------------------------- 1 | package com.github.ttdyce.nhviewer.model.room; 2 | 3 | import androidx.room.Dao; 4 | import androidx.room.Delete; 5 | import androidx.room.Insert; 6 | import androidx.room.Query; 7 | 8 | import java.util.List; 9 | 10 | @Dao 11 | public interface ComicCachedDao { 12 | 13 | @Query("SELECT * FROM ComicCached") 14 | List getAll(); 15 | 16 | @Query("SELECT * FROM ComicCached WHERE id = :id") 17 | ComicCachedEntity findById(int id); 18 | 19 | @Query("SELECT * FROM ComicCached WHERE id In(:ids)") 20 | List findById(List ids); 21 | 22 | @Query("SELECT count(*) == 0 FROM ComicCached Where id = :id") 23 | boolean notExist(int id); 24 | 25 | @Insert 26 | void insertAll(ComicCachedEntity... comic); 27 | 28 | @Insert 29 | void insert(ComicCachedEntity comic); 30 | 31 | @Delete 32 | void delete(ComicCachedEntity comic); 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/ttdyce/nhviewer/model/room/ComicCachedEntity.java: -------------------------------------------------------------------------------- 1 | package com.github.ttdyce.nhviewer.model.room; 2 | 3 | import androidx.annotation.NonNull; 4 | import androidx.room.Entity; 5 | 6 | import com.google.gson.Gson; 7 | 8 | import java.io.Serializable; 9 | 10 | @Entity(tableName = "ComicCached", primaryKeys = {"id"}) 11 | public class ComicCachedEntity implements Serializable { 12 | private static final long serialVersionUID = 3001880502226771220L; 13 | @NonNull 14 | private int id; 15 | @NonNull 16 | private String mid; 17 | @NonNull 18 | private String title; 19 | @NonNull 20 | private String pageTypes; 21 | @NonNull 22 | private int numOfPages; 23 | 24 | public ComicCachedEntity(int id, @NonNull String mid, @NonNull String title, @NonNull String pageTypes, int numOfPages) { 25 | this.id = id; 26 | this.mid = mid; 27 | this.title = title; 28 | this.pageTypes = pageTypes; 29 | this.numOfPages = numOfPages; 30 | } 31 | 32 | // Room uses this factory method to create ComicCollectionEntity objects. 33 | public static ComicCachedEntity create(int id, String mid, String title, String pageTypes, int numOfPages) { 34 | return new ComicCachedEntity(id, mid, title, pageTypes, numOfPages); 35 | } 36 | 37 | public int getId() { 38 | return id; 39 | } 40 | 41 | public void setId(int id) { 42 | this.id = id; 43 | } 44 | 45 | @NonNull 46 | public String getMid() { 47 | return mid; 48 | } 49 | 50 | public void setMid(@NonNull String mid) { 51 | this.mid = mid; 52 | } 53 | 54 | @NonNull 55 | public String getTitle() { 56 | return title; 57 | } 58 | 59 | public void setTitle(@NonNull String title) { 60 | this.title = title; 61 | } 62 | 63 | @NonNull 64 | public String getPageTypes() { 65 | return pageTypes; 66 | } 67 | 68 | public void setPageTypes(@NonNull String pageTypes) { 69 | this.pageTypes = pageTypes; 70 | } 71 | 72 | public int getNumOfPages() { 73 | return numOfPages; 74 | } 75 | 76 | public void setNumOfPages(int numOfPages) { 77 | this.numOfPages = numOfPages; 78 | } 79 | 80 | public String toJson() { 81 | Gson gson = new Gson(); 82 | return gson.toJson(this); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/ttdyce/nhviewer/model/room/ComicCollectionDao.java: -------------------------------------------------------------------------------- 1 | package com.github.ttdyce.nhviewer.model.room; 2 | 3 | import androidx.room.Dao; 4 | import androidx.room.Delete; 5 | import androidx.room.Insert; 6 | import androidx.room.Query; 7 | import androidx.room.Update; 8 | 9 | import java.util.List; 10 | 11 | @Dao 12 | public interface ComicCollectionDao { 13 | String ORDER_BY_DEFAULT = "dateCreated DESC"; 14 | 15 | @Query("SELECT * FROM ComicCollection ORDER BY " + ORDER_BY_DEFAULT) 16 | List getAll(); 17 | 18 | @Query("SELECT * FROM ComicCollection WHERE name = :name ORDER BY " + ORDER_BY_DEFAULT) 19 | List getAllByName(String name); 20 | 21 | @Query("SELECT * FROM ComicCollection WHERE name = :name ORDER BY " + ORDER_BY_DEFAULT) 22 | ComicCollectionEntity findByName(String name); 23 | 24 | @Query("SELECT count(*) == 0 FROM ComicCollection Where id = :id AND name = :collectionName") 25 | boolean notExist(String collectionName, int id); 26 | 27 | @Insert 28 | void insertAll(ComicCollectionEntity... comicCollection); 29 | 30 | @Insert 31 | void insert(ComicCollectionEntity comicCollection); 32 | 33 | @Delete 34 | void delete(ComicCollectionEntity comicCollection); 35 | 36 | @Update 37 | void update(ComicCollectionEntity comicCollection); 38 | } 39 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/ttdyce/nhviewer/model/room/ComicCollectionEntity.java: -------------------------------------------------------------------------------- 1 | package com.github.ttdyce.nhviewer.model.room; 2 | 3 | import androidx.annotation.NonNull; 4 | import androidx.room.Entity; 5 | 6 | import com.google.gson.Gson; 7 | 8 | import java.io.Serializable; 9 | import java.util.Date; 10 | 11 | @Entity(tableName = "ComicCollection", primaryKeys = {"name", "id"}) 12 | public class ComicCollectionEntity implements Serializable { 13 | private static final long serialVersionUID = 3001880502226771220L; 14 | @NonNull 15 | private String name; 16 | 17 | @NonNull 18 | private int id; 19 | 20 | @NonNull 21 | private Date dateCreated; 22 | 23 | public ComicCollectionEntity(@NonNull String name, @NonNull int id, @NonNull Date dateCreated) { 24 | this.name = name; 25 | this.id = id; 26 | this.dateCreated = dateCreated; 27 | } 28 | 29 | // Room uses this factory method to create ComicCollectionEntity objects. 30 | public static ComicCollectionEntity create(String name, int id, Date dateCreated) { 31 | return new ComicCollectionEntity(name, id, dateCreated); 32 | } 33 | 34 | @NonNull 35 | public String getName() { 36 | return name; 37 | } 38 | 39 | public void setName(@NonNull String name) { 40 | this.name = name; 41 | } 42 | 43 | @NonNull 44 | public int getId() { 45 | return id; 46 | } 47 | 48 | public void setId(@NonNull int id) { 49 | this.id = id; 50 | } 51 | 52 | @NonNull 53 | public Date getDateCreated() { 54 | return dateCreated; 55 | } 56 | 57 | public void setDateCreated(@NonNull Date dateCreated) { 58 | this.dateCreated = dateCreated; 59 | } 60 | 61 | public String toJson(){ 62 | Gson gson = new Gson(); 63 | return gson.toJson(this); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/ttdyce/nhviewer/model/room/DateConverter.java: -------------------------------------------------------------------------------- 1 | package com.github.ttdyce.nhviewer.model.room; 2 | 3 | import androidx.room.TypeConverter; 4 | 5 | import java.text.DateFormat; 6 | import java.text.ParseException; 7 | import java.text.SimpleDateFormat; 8 | import java.util.Date; 9 | import java.util.Locale; 10 | 11 | class DateConverter { 12 | static DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US); 13 | 14 | @TypeConverter 15 | public static Date fromDate(String value) { 16 | if (value != null) { 17 | try { 18 | return df.parse(value); 19 | } catch (ParseException e) { 20 | e.printStackTrace(); 21 | } 22 | return null; 23 | } else { 24 | return null; 25 | } 26 | } 27 | 28 | @TypeConverter 29 | public static String dateToString(Date value) { 30 | if (value != null) { 31 | return df.format(value); 32 | } else { 33 | return null; 34 | } 35 | } 36 | 37 | 38 | } 39 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/ttdyce/nhviewer/presenter/ComicCollectionPresenter.java: -------------------------------------------------------------------------------- 1 | package com.github.ttdyce.nhviewer.presenter; 2 | 3 | import android.content.Context; 4 | import android.os.AsyncTask; 5 | import android.os.Bundle; 6 | import android.util.Log; 7 | import android.view.ViewGroup; 8 | 9 | import androidx.annotation.NonNull; 10 | import androidx.navigation.NavController; 11 | import androidx.recyclerview.widget.RecyclerView; 12 | 13 | import com.github.ttdyce.nhviewer.R; 14 | import com.github.ttdyce.nhviewer.model.api.NHAPI; 15 | import com.github.ttdyce.nhviewer.model.api.ResponseCallback; 16 | import com.github.ttdyce.nhviewer.model.comic.Comic; 17 | import com.github.ttdyce.nhviewer.model.comic.ComicCollection; 18 | import com.github.ttdyce.nhviewer.model.room.AppDatabase; 19 | import com.github.ttdyce.nhviewer.model.room.ComicCollectionDao; 20 | import com.github.ttdyce.nhviewer.model.room.ComicCollectionEntity; 21 | import com.github.ttdyce.nhviewer.view.ComicCollectionViewHolder; 22 | import com.github.ttdyce.nhviewer.view.MainActivity; 23 | import com.google.gson.Gson; 24 | import com.google.gson.JsonObject; 25 | import com.google.gson.JsonParser; 26 | 27 | import java.util.ArrayList; 28 | import java.util.Collections; 29 | import java.util.HashMap; 30 | import java.util.List; 31 | import java.util.Map; 32 | 33 | public class ComicCollectionPresenter { 34 | private ComicCollectionView comicCollectionView; 35 | private AppDatabase db; 36 | private ComicCollectionAdapter adapter; 37 | private NavController navController; 38 | 39 | public ComicCollectionPresenter(ComicCollectionView view, NavController navController) { 40 | this.comicCollectionView = view; 41 | this.db = MainActivity.getAppDatabase(); 42 | this.adapter = new ComicCollectionAdapter(); 43 | this.navController = navController; 44 | 45 | new LoadComicCollectionTask(db, view, adapter).execute(); 46 | 47 | } 48 | 49 | public ComicCollectionAdapter getAdapter() { 50 | return adapter; 51 | } 52 | 53 | public void onItemClick(int position) { 54 | String collectionName = adapter.get(position).getName(); 55 | 56 | Bundle bundle = new Bundle(); 57 | bundle.putString("collectionName", collectionName); 58 | 59 | navController.navigate(R.id.comicListFragment, bundle); 60 | } 61 | 62 | private class ComicCollectionAdapter extends RecyclerView.Adapter { 63 | private ArrayList comicCollections = new ArrayList<>(); 64 | 65 | @NonNull 66 | @Override 67 | public ComicCollectionViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { 68 | return comicCollectionView.onCreateViewHolder(parent, viewType); 69 | } 70 | 71 | @Override 72 | public void onBindViewHolder(@NonNull final ComicCollectionViewHolder holder, final int position) { 73 | ComicCollection cc = comicCollections.get(position); 74 | final String name = cc.getName(); 75 | final int numOfComics = cc.getComicCount(); 76 | 77 | if (numOfComics != 0) { 78 | Comic latestComic = cc.getComicList().get(0); 79 | NHAPI nhapi = new NHAPI(comicCollectionView.getContext(), MainActivity.proxyHost, MainActivity.proxyPort); 80 | ResponseCallback callback = new ResponseCallback() { 81 | @Override 82 | public void onReponse(String response) { 83 | JsonObject obj = JsonParser.parseString(response).getAsJsonObject(); 84 | Gson gson = new Gson(); 85 | Comic c = gson.fromJson(obj, Comic.class); 86 | final String thumbUrl = NHAPI.URLs.getThumbnail(c.getMid(), c.getImages().getThumbnail().getType()); 87 | 88 | if (c.getId() != -1)//id -1 is for empty comic collection 89 | comicCollectionView.onBindViewHolder(holder, position, name, thumbUrl, numOfComics); 90 | } 91 | }; 92 | 93 | nhapi.getComic(latestComic.getId(), callback); 94 | } 95 | 96 | comicCollectionView.onBindViewHolder(holder, position, name, "", numOfComics); 97 | } 98 | 99 | @Override 100 | public int getItemCount() { 101 | return comicCollections.size(); 102 | } 103 | 104 | public void add(ComicCollection cc) { 105 | comicCollections.add(cc); 106 | } 107 | 108 | public void clear() { 109 | comicCollections.clear(); 110 | } 111 | 112 | public ComicCollection get(int position) { 113 | return comicCollections.get(position); 114 | } 115 | } 116 | 117 | public interface ComicCollectionView { 118 | 119 | ComicCollectionViewHolder onCreateViewHolder(ViewGroup parent, int viewType); 120 | 121 | void onBindViewHolder(ComicCollectionViewHolder holder, int position, String name, String thumbUrl, int numOfPages); 122 | 123 | void updateList(); 124 | 125 | Context getContext(); 126 | 127 | } 128 | 129 | 130 | private static class LoadComicCollectionTask extends AsyncTask { 131 | private AppDatabase db; 132 | private ComicCollectionView view; 133 | private ComicCollectionAdapter adapter; 134 | 135 | public LoadComicCollectionTask(AppDatabase db, ComicCollectionView view, ComicCollectionAdapter adapter) { 136 | this.db = db; 137 | this.view = view; 138 | this.adapter = adapter; 139 | } 140 | 141 | protected Void doInBackground(Void... voids) { 142 | ComicCollectionDao dao = db.comicCollectionDao(); 143 | List entities = dao.getAll(); 144 | HashMap> comicCollections = new HashMap<>(); 145 | 146 | 147 | for (ComicCollectionEntity e : entities) { 148 | final String name = e.getName(); 149 | int id = e.getId(); 150 | 151 | if (comicCollections.get(name) == null) 152 | comicCollections.put(name, new ArrayList<>(Collections.singletonList(id))); 153 | else { 154 | List ids = comicCollections.get(name); 155 | ids.add(id); 156 | comicCollections.put(name, ids); 157 | } 158 | } 159 | 160 | for (Map.Entry> entry : comicCollections.entrySet()) { 161 | String name = entry.getKey(); 162 | List ids = entry.getValue(); 163 | 164 | adapter.add(new ComicCollection(ids, name)); 165 | } 166 | return null; 167 | } 168 | 169 | protected void onPostExecute(Void result) { 170 | Log.i("asyncTask", "onPostExecute: Finished task"); 171 | view.updateList(); 172 | } 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/ttdyce/nhviewer/view/ComicActivity.java: -------------------------------------------------------------------------------- 1 | package com.github.ttdyce.nhviewer.view; 2 | 3 | import android.content.SharedPreferences; 4 | import android.graphics.Color; 5 | import android.net.Uri; 6 | import android.os.Bundle; 7 | import android.preference.PreferenceManager; 8 | import android.view.LayoutInflater; 9 | import android.view.View; 10 | import android.view.ViewGroup; 11 | import android.widget.ProgressBar; 12 | 13 | import androidx.appcompat.app.AppCompatActivity; 14 | import androidx.appcompat.widget.Toolbar; 15 | import androidx.recyclerview.widget.LinearLayoutManager; 16 | import androidx.recyclerview.widget.RecyclerView; 17 | import androidx.swiperefreshlayout.widget.CircularProgressDrawable; 18 | 19 | import com.bumptech.glide.Glide; 20 | import com.bumptech.glide.request.RequestOptions; 21 | import com.github.ttdyce.nhviewer.R; 22 | import com.github.ttdyce.nhviewer.presenter.ComicPresenter; 23 | import com.github.ttdyce.nhviewer.view.component.ZoomRecyclerView; 24 | 25 | import jp.wasabeef.glide.transformations.BlurTransformation; 26 | 27 | public class ComicActivity extends AppCompatActivity implements ComicPresenter.ComicView { 28 | private int id; 29 | private String mid; 30 | private String title; 31 | private int numOfPages; 32 | private String[] pageTypes; 33 | private ComicPresenter presenter; 34 | 35 | private RecyclerView rvComic; 36 | private ProgressBar pbComic; 37 | private LinearLayoutManager layoutManager; 38 | private CircularProgressDrawable circularProgressDrawable; 39 | private int lastVisibleItemPosition; 40 | 41 | @Override 42 | protected void onCreate(Bundle savedInstanceState) { 43 | super.onCreate(savedInstanceState); 44 | setContentView(R.layout.activity_comic); 45 | init(); 46 | } 47 | 48 | @Override 49 | protected void onStop() { 50 | //save this comic to history 51 | presenter.onStop(); 52 | 53 | super.onStop(); 54 | } 55 | 56 | 57 | @Override 58 | public boolean onSupportNavigateUp() { 59 | onBackPressed(); 60 | return true; 61 | } 62 | 63 | private int getComicIdFromBrowser() { 64 | int comicid = -1; 65 | Uri browserData = getIntent().getData();//data from browser, contains only comicid (from url) 66 | 67 | if (browserData != null && browserData.isHierarchical()) {//using id from browser 68 | comicid = Integer.parseInt(browserData.getLastPathSegment()); 69 | } 70 | 71 | return comicid; 72 | } 73 | 74 | 75 | private void init() { 76 | if (getIntent().getExtras() == null) 77 | return; 78 | final Bundle extras = getIntent().getExtras(); 79 | final int idFromBrowser = getComicIdFromBrowser(); 80 | rvComic = findViewById(R.id.rvComic); 81 | pbComic = findViewById(R.id.pbComic); 82 | 83 | layoutManager = (LinearLayoutManager)rvComic.getLayoutManager(); 84 | final ZoomRecyclerView rvComic = findViewById(R.id.rvComic); 85 | rvComic.setEnableScale(true); 86 | rvComic.setHasFixedSize(true); 87 | initOnScrollListener(); 88 | 89 | presenter = ComicPresenter.factory(this, this, extras, idFromBrowser, rvComic); 90 | 91 | //set appbar 92 | Toolbar toolbar = findViewById(R.id.toolbar_comic); 93 | setSupportActionBar(toolbar); 94 | getSupportActionBar().setDisplayHomeAsUpEnabled(true); 95 | getSupportActionBar().setDisplayShowHomeEnabled(true); 96 | } 97 | 98 | private void initOnScrollListener() { 99 | //Remember Last page 100 | final LinearLayoutManager layoutManager = (LinearLayoutManager) rvComic.getLayoutManager(); 101 | rvComic.addOnScrollListener(new RecyclerView.OnScrollListener() { 102 | @Override 103 | public void onScrolled(RecyclerView recyclerView, int dx, int dy) {//dy > 0 = move down, dy < 0 = move up 104 | super.onScrolled(recyclerView, dx, dy); 105 | 106 | if (layoutManager.findFirstCompletelyVisibleItemPosition() == -1) 107 | return; 108 | 109 | lastVisibleItemPosition = layoutManager.findFirstCompletelyVisibleItemPosition() + 1; 110 | int itemCount = layoutManager.getItemCount() - 1; 111 | 112 | } 113 | }); 114 | } 115 | 116 | @Override 117 | public ComicViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 118 | View v = LayoutInflater.from(parent.getContext()) 119 | .inflate(R.layout.item_comic, parent, false); 120 | return new ComicViewHolder(v); 121 | } 122 | 123 | @Override 124 | public void onBindViewHolder(ComicViewHolder holder, int position, String url) { 125 | final SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); 126 | 127 | 128 | circularProgressDrawable = new CircularProgressDrawable(this);// TODO: 2019/10/27 many drawable object is created, may hurt performance & loading speed 129 | circularProgressDrawable.setStrokeWidth(10f); 130 | circularProgressDrawable.setCenterRadius(30f); 131 | circularProgressDrawable.setColorSchemeColors(Color.WHITE); // stroke color, found in CircularProgressDrawable comment 132 | circularProgressDrawable.start(); 133 | 134 | //determine blur image or not 135 | if (pref.getBoolean(MainActivity.KEY_PREF_DEMO_MODE, false)) 136 | Glide.with(this) 137 | .load(url) 138 | .placeholder(circularProgressDrawable) 139 | .apply(RequestOptions.bitmapTransform(new BlurTransformation(16, 5))) 140 | .into(holder.ivComicPage); 141 | else 142 | Glide.with(this) 143 | .load(url) 144 | .placeholder(circularProgressDrawable) 145 | .into(holder.ivComicPage); 146 | 147 | holder.tvComicPage.setText(String.valueOf(position + 1)); 148 | 149 | int pos = layoutManager.findLastVisibleItemPosition(); 150 | pbComic.setProgress(100 * pos / layoutManager.getItemCount()); 151 | } 152 | 153 | @Override 154 | public int getLastVisibleItemPosition() { 155 | return lastVisibleItemPosition; 156 | } 157 | 158 | @Override 159 | public View getRootView() { 160 | return rvComic.getRootView(); 161 | } 162 | 163 | @Override 164 | public RecyclerView getRVComic() { 165 | return rvComic; 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/ttdyce/nhviewer/view/ComicCollectionFragment.java: -------------------------------------------------------------------------------- 1 | package com.github.ttdyce.nhviewer.view; 2 | 3 | import android.content.SharedPreferences; 4 | import android.graphics.drawable.ColorDrawable; 5 | import android.os.Bundle; 6 | import android.preference.PreferenceManager; 7 | import android.view.LayoutInflater; 8 | import android.view.View; 9 | import android.view.ViewGroup; 10 | import android.widget.TextView; 11 | 12 | import androidx.annotation.NonNull; 13 | import androidx.annotation.Nullable; 14 | import androidx.core.content.ContextCompat; 15 | import androidx.core.widget.ContentLoadingProgressBar; 16 | import androidx.fragment.app.Fragment; 17 | import androidx.navigation.Navigation; 18 | import androidx.recyclerview.widget.GridLayoutManager; 19 | import androidx.recyclerview.widget.RecyclerView; 20 | 21 | import com.bumptech.glide.Glide; 22 | import com.bumptech.glide.request.RequestOptions; 23 | import com.github.ttdyce.nhviewer.R; 24 | import com.github.ttdyce.nhviewer.presenter.ComicCollectionPresenter; 25 | 26 | import java.util.Locale; 27 | 28 | import jp.wasabeef.glide.transformations.BlurTransformation; 29 | 30 | 31 | public class ComicCollectionFragment extends Fragment implements ComicCollectionPresenter.ComicCollectionView { 32 | private RecyclerView rvComicList; 33 | private ComicCollectionPresenter presenter; 34 | private ContentLoadingProgressBar pbComicList; 35 | private TextView tvComicListDesc; 36 | 37 | 38 | @Override 39 | public void onCreate(Bundle savedInstanceState) { 40 | super.onCreate(savedInstanceState); 41 | 42 | } 43 | 44 | @Override 45 | public View onCreateView(LayoutInflater inflater, ViewGroup container, 46 | Bundle savedInstanceState) { 47 | // Inflate the layout for this fragment 48 | return inflater.inflate(R.layout.fragment_comic_list, container, false); 49 | 50 | } 51 | 52 | @Override 53 | public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { 54 | super.onViewCreated(view, savedInstanceState); 55 | 56 | presenter = new ComicCollectionPresenter(this, Navigation.findNavController(view)); 57 | GridLayoutManager layoutManager = new GridLayoutManager(requireActivity(), 3); 58 | rvComicList = view.findViewById(R.id.rvComicList); 59 | pbComicList = view.findViewById(R.id.pbComicList); 60 | tvComicListDesc = view.findViewById(R.id.tvComicListDesc); 61 | 62 | tvComicListDesc.setText("Loading collection list..."); 63 | 64 | rvComicList.setHasFixedSize(true); 65 | rvComicList.setAdapter(presenter.getAdapter()); 66 | rvComicList.setLayoutManager(layoutManager); 67 | } 68 | 69 | @Override 70 | public ComicCollectionViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 71 | View v = LayoutInflater.from(parent.getContext()) 72 | .inflate(R.layout.item_comic_collection_list, parent, false); 73 | return new ComicCollectionViewHolder(v); 74 | } 75 | 76 | @Override 77 | public void onBindViewHolder(ComicCollectionViewHolder holder, final int position, String name, String thumbUrl, int numOfPages) { 78 | final SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(requireContext()); 79 | 80 | holder.tvNumOfComics.setText(String.format(Locale.ENGLISH, "%d collected", numOfPages)); 81 | holder.tvTitle.setText(name); 82 | 83 | //determine blur image or not 84 | if (pref.getBoolean(MainActivity.KEY_PREF_DEMO_MODE, false)) 85 | Glide.with(this) 86 | .load(thumbUrl) 87 | .placeholder(new ColorDrawable(ContextCompat.getColor(requireContext(), R.color.secondaryColor))) 88 | .apply(RequestOptions.bitmapTransform(new BlurTransformation(16, 5))) 89 | .into(holder.ivThumb); 90 | else 91 | Glide.with(this) 92 | .load(thumbUrl) 93 | .placeholder(new ColorDrawable(ContextCompat.getColor(requireContext(), R.color.secondaryColor))) 94 | .into(holder.ivThumb); 95 | 96 | holder.cvComicItem.setOnClickListener(new View.OnClickListener() { 97 | @Override 98 | public void onClick(View v) { 99 | presenter.onItemClick(position); 100 | } 101 | }); 102 | } 103 | 104 | @Override 105 | public void updateList() { 106 | RecyclerView.Adapter adapter = rvComicList.getAdapter(); 107 | if (adapter.getItemCount() != 0) 108 | adapter.notifyDataSetChanged(); 109 | 110 | toggleLoadingDesc(false); 111 | 112 | } 113 | 114 | private void toggleLoadingDesc(boolean loading) { 115 | if (loading) { 116 | pbComicList.show(); 117 | tvComicListDesc.setVisibility(View.VISIBLE); 118 | } else { 119 | pbComicList.hide(); 120 | tvComicListDesc.setVisibility(View.INVISIBLE); 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/ttdyce/nhviewer/view/ComicCollectionViewHolder.java: -------------------------------------------------------------------------------- 1 | package com.github.ttdyce.nhviewer.view; 2 | 3 | 4 | import android.view.View; 5 | import android.widget.ImageView; 6 | import android.widget.TextView; 7 | 8 | import androidx.cardview.widget.CardView; 9 | import androidx.recyclerview.widget.RecyclerView; 10 | 11 | import com.github.ttdyce.nhviewer.R; 12 | 13 | public class ComicCollectionViewHolder extends RecyclerView.ViewHolder { 14 | public CardView cvComicItem; 15 | public TextView tvTitle; 16 | public TextView tvNumOfComics; 17 | public ImageView ivThumb; 18 | 19 | public ComicCollectionViewHolder(View v) { 20 | super(v); 21 | tvTitle = v.findViewById(R.id.tvComicListItem); 22 | tvNumOfComics= v.findViewById(R.id.tvNumOfComics); 23 | ivThumb = v.findViewById(R.id.ivComicListItem); 24 | cvComicItem = v.findViewById(R.id.cvComicListItem); 25 | } 26 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/ttdyce/nhviewer/view/ComicListViewHolder.java: -------------------------------------------------------------------------------- 1 | package com.github.ttdyce.nhviewer.view; 2 | 3 | 4 | import android.view.View; 5 | import android.widget.ImageButton; 6 | import android.widget.ImageView; 7 | import android.widget.TextView; 8 | 9 | import androidx.cardview.widget.CardView; 10 | import androidx.recyclerview.widget.RecyclerView; 11 | 12 | import com.github.ttdyce.nhviewer.R; 13 | 14 | public class ComicListViewHolder extends RecyclerView.ViewHolder { 15 | public CardView cvComicItem; 16 | public TextView tvTitle; 17 | public TextView tvNumOfPages; 18 | public ImageView ivThumb; 19 | public ImageButton ibCollect; 20 | public ImageButton ibFavorite; 21 | 22 | public ComicListViewHolder(View v) { 23 | super(v); 24 | tvTitle = v.findViewById(R.id.tvComicListItem); 25 | tvNumOfPages= v.findViewById(R.id.tvNumOfPages); 26 | ivThumb = v.findViewById(R.id.ivComicListItem); 27 | cvComicItem = v.findViewById(R.id.cvComicListItem); 28 | ibCollect = v.findViewById(R.id.ibCollect); 29 | ibFavorite = v.findViewById(R.id.ibFavorite); 30 | } 31 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/ttdyce/nhviewer/view/ComicViewHolder.java: -------------------------------------------------------------------------------- 1 | package com.github.ttdyce.nhviewer.view; 2 | 3 | import android.view.View; 4 | import android.widget.ImageView; 5 | import android.widget.TextView; 6 | 7 | import androidx.recyclerview.widget.RecyclerView; 8 | 9 | import com.github.ttdyce.nhviewer.R; 10 | 11 | public class ComicViewHolder extends RecyclerView.ViewHolder { 12 | public ImageView ivComicPage; 13 | public TextView tvComicPage; 14 | 15 | public ComicViewHolder(View v) { 16 | super(v); 17 | ivComicPage = v.findViewById(R.id.ivComicPage); 18 | tvComicPage = v.findViewById(R.id.tvComicPage); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/ttdyce/nhviewer/view/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.github.ttdyce.nhviewer.view; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.content.DialogInterface; 5 | import android.content.Intent; 6 | import android.content.SharedPreferences; 7 | import android.os.Bundle; 8 | import android.preference.PreferenceManager; 9 | import android.util.Log; 10 | 11 | import androidx.appcompat.app.AlertDialog; 12 | import androidx.appcompat.app.AppCompatActivity; 13 | import androidx.appcompat.widget.Toolbar; 14 | import androidx.navigation.NavController; 15 | import androidx.navigation.Navigation; 16 | import androidx.navigation.ui.NavigationUI; 17 | import androidx.room.Room; 18 | 19 | import com.github.ttdyce.nhviewer.BuildConfig; 20 | import com.github.ttdyce.nhviewer.R; 21 | import com.github.ttdyce.nhviewer.model.MyDistributeListener; 22 | import com.github.ttdyce.nhviewer.model.room.AppDatabase; 23 | import com.github.ttdyce.nhviewer.model.room.ComicCollectionDao; 24 | import com.github.ttdyce.nhviewer.model.room.ComicCollectionEntity; 25 | import com.google.android.material.bottomnavigation.BottomNavigationView; 26 | import com.microsoft.appcenter.AppCenter; 27 | import com.microsoft.appcenter.analytics.Analytics; 28 | import com.microsoft.appcenter.crashes.Crashes; 29 | import com.microsoft.appcenter.distribute.Distribute; 30 | 31 | import java.util.Date; 32 | 33 | 34 | public class MainActivity extends AppCompatActivity { 35 | public static final String KEY_PREF_DEFAULT_LANGUAGE = "key_default_language"; 36 | public static final String KEY_PREF_DEMO_MODE = "key_demo_mode"; 37 | public static final String KEY_PREF_ENABLE_SPLASH = "key_enable_splash"; 38 | public static final String KEY_PREF_CHECK_UPDATE = "key_check_update"; 39 | public static final String KEY_PREF_LAST_VERSION_OPENED = "key_last_version_opened"; 40 | public static final CharSequence KEY_PREF_VERSION = "key_version"; 41 | public static final String KEY_PREF_PROXY = "key_proxy"; 42 | public static final String KEY_PREF_PROXY_HOST = "key_proxy_host"; 43 | public static final String KEY_PREF_PROXY_PORT = "key_proxy_port"; 44 | private static final String TAG = "MainActivity"; 45 | private static AppDatabase appDatabase; 46 | public static String proxyHost; 47 | public static int proxyPort; 48 | 49 | @Override 50 | protected void onCreate(Bundle savedInstanceState) { 51 | setTheme(R.style.AppTheme);//replacing the SplashTheme 52 | //Open SplashActivity 53 | final SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); 54 | boolean enabledSplash = pref.getBoolean(KEY_PREF_ENABLE_SPLASH, true); 55 | if (enabledSplash) 56 | startActivity(new Intent(this, SplashActivity.class));// TODO: 12/15/2019 Open SplashActivity from MainActivity 57 | 58 | super.onCreate(savedInstanceState); 59 | setContentView(R.layout.activity_main); 60 | 61 | checkMigration(); 62 | tryAskForLanguage(); 63 | init(); 64 | } 65 | 66 | private void checkMigration() { 67 | final SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); 68 | String currentVersion = BuildConfig.VERSION_NAME; 69 | String lastVersion = pref.getString(KEY_PREF_LAST_VERSION_OPENED, "0.0.0"); 70 | if (!currentVersion.equals(lastVersion)) { 71 | //it is first time open after update / simply first time open 72 | 73 | if (currentVersion.equals("2.6.0") || lastVersion.equals("0.0.0")) { 74 | //fix for 2.5.0 -> 2.6.0 75 | pref.edit().remove(KEY_PREF_DEFAULT_LANGUAGE).commit(); 76 | } 77 | 78 | } 79 | 80 | pref.edit().putString(KEY_PREF_LAST_VERSION_OPENED, currentVersion).apply(); 81 | } 82 | 83 | private void init() { 84 | // setup vs-app-center 85 | Distribute.setListener(new MyDistributeListener()); 86 | AppCenter.start(getApplication(), "3b65600f-dd4f-415c-8949-e32f594cba0d", 87 | Analytics.class, Crashes.class, Distribute.class); 88 | 89 | final SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); 90 | boolean enabledCheckUpdate = pref.getBoolean(KEY_PREF_CHECK_UPDATE, true); 91 | AppCenter.setEnabled(enabledCheckUpdate); // for all services 92 | 93 | appDatabase = Room.databaseBuilder(getApplicationContext(), 94 | AppDatabase.class, AppDatabase.DB_NAME) 95 | .addMigrations(AppDatabase.MIGRATION_1_2).build(); 96 | 97 | // deleteDatabase(AppDatabase.DB_NAME); 98 | //init Collections 99 | // todo replace with executor-things (java concurrent) 100 | new Thread( 101 | new Runnable() { 102 | @Override 103 | public void run() { 104 | ComicCollectionDao dao = appDatabase.comicCollectionDao(); 105 | if (dao.notExist(AppDatabase.COL_COLLECTION_HISTORY, -1)) { 106 | dao.insert(ComicCollectionEntity.create(AppDatabase.COL_COLLECTION_HISTORY, -1, new Date())); 107 | dao.insert(ComicCollectionEntity.create(AppDatabase.COL_COLLECTION_FAVORITE, -1, new Date())); 108 | dao.insert(ComicCollectionEntity.create(AppDatabase.COL_COLLECTION_NEXT, -1, new Date())); 109 | } 110 | } 111 | }).start(); 112 | //app bar 113 | Toolbar myToolbar = findViewById(R.id.toolbar_main); 114 | setSupportActionBar(myToolbar); 115 | 116 | //proxy 117 | proxyHost = pref.getString(KEY_PREF_PROXY_HOST, ""); 118 | try { 119 | proxyPort = Integer.parseInt(pref.getString(KEY_PREF_PROXY_PORT, "8080")); 120 | } catch (NumberFormatException e) { 121 | Log.e(TAG, "init ignorable error: setting proxyPort to default (8080)"); 122 | proxyPort = 8080; 123 | } 124 | 125 | Log.d(TAG, "init: proxyHost: " + proxyHost); 126 | Log.d(TAG, "init: proxyPort: " + proxyPort); 127 | 128 | } 129 | 130 | //Link bottom navigation view with jetpack navigation 131 | private void initNavigation() { 132 | NavController navController = Navigation.findNavController(this, R.id.fragmentNavHost); 133 | navController.setGraph(R.navigation.nav_app); 134 | BottomNavigationView bottomNavigation = findViewById(R.id.bottomNavigation); 135 | NavigationUI.setupWithNavController(bottomNavigation, navController); 136 | // Navigation.findNavController(this, R.id.fragmentNavHost) 137 | } 138 | 139 | @SuppressLint("ApplySharedPref") 140 | private void tryAskForLanguage() { 141 | String comicLanguage = SettingsFragment.Language.notSet.toString(); 142 | final SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); 143 | 144 | try { 145 | comicLanguage = pref.getString(KEY_PREF_DEFAULT_LANGUAGE, SettingsFragment.Language.notSet.toString()); 146 | } catch (ClassCastException e) { 147 | pref.edit().remove(KEY_PREF_DEFAULT_LANGUAGE).commit(); 148 | } 149 | 150 | if (!comicLanguage.equals(SettingsFragment.Language.notSet.toString())) { 151 | initNavigation(); 152 | return; 153 | } 154 | 155 | //pop up dialog for setting default language 156 | final String[] languageArray = getResources().getStringArray(R.array.languages); 157 | AlertDialog.Builder builder = new AlertDialog.Builder(this, R.style.DialogTheme); 158 | 159 | builder.setTitle(getString(R.string.set_default_language)); 160 | builder.setNegativeButton(getString(R.string.cancel), new DialogInterface.OnClickListener() { 161 | @Override 162 | public void onClick(DialogInterface dialog, int which) { 163 | SharedPreferences.Editor editor = pref.edit(); 164 | 165 | editor.putString(KEY_PREF_DEFAULT_LANGUAGE, SettingsFragment.Language.all.toString()); 166 | editor.apply(); 167 | 168 | } 169 | }); 170 | builder.setItems(languageArray, new DialogInterface.OnClickListener() { 171 | @Override 172 | public void onClick(DialogInterface dialog, int which) { 173 | Log.d(TAG, "onClick: init language clicked: " + which); 174 | SharedPreferences.Editor editor = pref.edit(); 175 | 176 | editor.putString(KEY_PREF_DEFAULT_LANGUAGE, String.valueOf(which)); 177 | editor.apply(); 178 | 179 | final SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); 180 | } 181 | }); 182 | builder.setOnDismissListener(new DialogInterface.OnDismissListener() { 183 | @Override 184 | public void onDismiss(DialogInterface dialog) { 185 | String language = pref.getString(KEY_PREF_DEFAULT_LANGUAGE, SettingsFragment.Language.notSet.toString()); 186 | if (SettingsFragment.Language.notSet.toString().equals(language)) { 187 | SharedPreferences.Editor editor = pref.edit(); 188 | editor.putString(KEY_PREF_DEFAULT_LANGUAGE, SettingsFragment.Language.all.toString()); 189 | editor.apply(); 190 | } 191 | 192 | Log.d(TAG, "onClick: after init language, before dismiss dialog: " + pref.getString(KEY_PREF_DEFAULT_LANGUAGE, "not set")); 193 | initNavigation(); 194 | } 195 | }); 196 | builder.show(); 197 | } 198 | 199 | //Singleton database 200 | public static AppDatabase getAppDatabase() { 201 | return appDatabase; 202 | } 203 | 204 | // TODO: 4/5/2023 seems not the best way to maintain proxy-related settings here, proxyHost sometimes becomes null 205 | public static boolean isProxied() { 206 | if (proxyHost == null || "".equals(proxyHost)) 207 | return false; 208 | 209 | return true; 210 | } 211 | 212 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/ttdyce/nhviewer/view/ProxySettingsFragment.java: -------------------------------------------------------------------------------- 1 | package com.github.ttdyce.nhviewer.view; 2 | 3 | import android.os.Bundle; 4 | 5 | import androidx.preference.PreferenceFragmentCompat; 6 | 7 | import com.github.ttdyce.nhviewer.R; 8 | 9 | public class ProxySettingsFragment extends PreferenceFragmentCompat { 10 | 11 | @Override 12 | public void onCreate(Bundle savedInstanceState) { 13 | super.onCreate(savedInstanceState); 14 | 15 | // Load the preferences from an XML resource 16 | addPreferencesFromResource(R.xml.proxy_preferences); 17 | } 18 | 19 | @Override 20 | public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { 21 | 22 | 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/ttdyce/nhviewer/view/RefreshCookieActivity.java: -------------------------------------------------------------------------------- 1 | package com.github.ttdyce.nhviewer.view; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.os.Bundle; 5 | import android.os.Handler; 6 | import android.util.Log; 7 | import android.webkit.CookieManager; 8 | import android.webkit.ValueCallback; 9 | import android.webkit.WebView; 10 | import android.widget.Toast; 11 | 12 | import androidx.appcompat.app.AppCompatActivity; 13 | 14 | import com.github.ttdyce.nhviewer.R; 15 | import com.github.ttdyce.nhviewer.model.CookieStringRequest; 16 | 17 | public class RefreshCookieActivity extends AppCompatActivity { 18 | String url = "https://nhentai.net"; 19 | 20 | @SuppressLint("SetJavaScriptEnabled") 21 | @Override 22 | protected void onCreate(Bundle savedInstanceState) { 23 | super.onCreate(savedInstanceState); 24 | setContentView(R.layout.activity_refresh_cookie); 25 | 26 | /* Use a WebView to bypass cloudflare challenge (Retrieve cookie) */ 27 | WebView wvRefreshCookie = findViewById(R.id.wvRefreshCookie); 28 | 29 | // clear cookie on create. on App open and Preference page item click enter this part 30 | CookieManager.getInstance().removeAllCookies(new ValueCallback() { 31 | @Override 32 | public void onReceiveValue(Boolean value) { 33 | // leave empty 34 | } 35 | }); 36 | 37 | wvRefreshCookie.getSettings().setJavaScriptEnabled(true); 38 | // wvInvisibleSplash.getSettings().setUserAgentString("Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/110.0"); 39 | wvRefreshCookie.loadUrl(url); 40 | 41 | String cookies = CookieManager.getInstance().getCookie(url); 42 | Log.d("SplashActivitiy", "url: " + url); 43 | Log.d("SplashActivitiy", "Got cookie: " + cookies); 44 | Log.d("SplashActivitiy", "User agent: " + wvRefreshCookie.getSettings().getUserAgentString()); 45 | checkCookie(wvRefreshCookie.getSettings().getUserAgentString()); 46 | } 47 | 48 | private void checkCookie(String userAgent) { 49 | Handler handler = new Handler(); 50 | handler.postDelayed(new Runnable() { 51 | public void run() { 52 | String cookies = CookieManager.getInstance().getCookie(url); 53 | if (cookies == null || !cookies.contains("cf_clearance=")) { 54 | Log.e("SplashActivitiy", "Not found required cookie: cf_clearance, try again soon..."); 55 | checkCookie(userAgent); 56 | // checkCookie(userAgent); 57 | finish(); 58 | } else { 59 | Log.d("SplashActivitiy", "url: " + url); 60 | Log.d("SplashActivitiy", "Got cookie: " + cookies); 61 | // Log.d("SplashActivitiy", "User agent: " + wvInvisibleSplash.getSettings().getUserAgentString()); 62 | CookieStringRequest.challengeCookies = cookies; 63 | CookieStringRequest.userAgent = userAgent; 64 | Toast.makeText(getApplicationContext(), "Saved cookie, page loading should be work now (" + cookies.substring(0, 20) + "...", Toast.LENGTH_LONG).show(); 65 | 66 | finish(); 67 | } 68 | } 69 | }, 500); 70 | 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/ttdyce/nhviewer/view/SearchingFragment.java: -------------------------------------------------------------------------------- 1 | package com.github.ttdyce.nhviewer.view; 2 | 3 | import android.app.SearchManager; 4 | import android.content.Context; 5 | import android.content.Intent; 6 | import android.os.Bundle; 7 | import android.util.Log; 8 | import android.view.LayoutInflater; 9 | import android.view.Menu; 10 | import android.view.MenuInflater; 11 | import android.view.View; 12 | import android.view.ViewGroup; 13 | import android.widget.TextView; 14 | 15 | import androidx.annotation.NonNull; 16 | import androidx.annotation.Nullable; 17 | import androidx.appcompat.widget.SearchView; 18 | import androidx.core.widget.ContentLoadingProgressBar; 19 | import androidx.fragment.app.Fragment; 20 | import androidx.navigation.Navigation; 21 | 22 | import com.github.ttdyce.nhviewer.R; 23 | import com.github.ttdyce.nhviewer.model.api.NHAPI; 24 | import com.github.ttdyce.nhviewer.model.api.ResponseCallback; 25 | import com.github.ttdyce.nhviewer.model.comic.Comic; 26 | import com.github.ttdyce.nhviewer.presenter.ComicPresenter; 27 | import com.google.gson.Gson; 28 | import com.google.gson.JsonObject; 29 | import com.google.gson.JsonParser; 30 | 31 | 32 | public class SearchingFragment extends Fragment { 33 | private static final String TAG = "SearchingFragment"; 34 | 35 | private ContentLoadingProgressBar pbComicList; 36 | private TextView tvComicListDesc; 37 | 38 | 39 | @Override 40 | public void onCreate(Bundle savedInstanceState) { 41 | super.onCreate(savedInstanceState); 42 | setHasOptionsMenu(true); 43 | 44 | } 45 | 46 | @Override 47 | public View onCreateView(LayoutInflater inflater, ViewGroup container, 48 | Bundle savedInstanceState) { 49 | // Inflate the layout for this fragment 50 | return inflater.inflate(R.layout.fragment_comic_list, container, false); 51 | 52 | } 53 | 54 | @Override 55 | public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { 56 | super.onViewCreated(view, savedInstanceState); 57 | 58 | pbComicList = view.findViewById(R.id.pbComicList); 59 | tvComicListDesc = view.findViewById(R.id.tvComicListDesc); 60 | 61 | pbComicList.setVisibility(View.INVISIBLE); 62 | tvComicListDesc.setText(getString(R.string.enter_search_query)); 63 | } 64 | 65 | 66 | @Override 67 | public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { 68 | inflater.inflate(R.menu.app_bar_items_searching, menu); 69 | 70 | // Get the SearchView and set the searchable configuration 71 | SearchManager searchManager = (SearchManager) requireActivity().getSystemService(Context.SEARCH_SERVICE); 72 | SearchView searchView = (SearchView) menu.findItem(R.id.action_searchview).getActionView(); 73 | // Assumes current activity is the searchable activity 74 | searchView.setSearchableInfo(searchManager.getSearchableInfo(requireActivity().getComponentName())); 75 | searchView.setIconified(false); 76 | searchView.requestFocusFromTouch(); 77 | 78 | searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { 79 | @Override 80 | public boolean onQueryTextSubmit(String query) { 81 | try { 82 | int id = Integer.parseInt(query); // treat number query as an id 83 | NHAPI nhapi = new NHAPI(requireContext(), MainActivity.proxyHost, MainActivity.proxyPort); 84 | 85 | // FIXME: 10/26/2020 activity is opened twice 86 | nhapi.getComic(id, new ResponseCallback() { 87 | @Override 88 | public void onReponse(String response) { 89 | JsonObject obj = JsonParser.parseString(response).getAsJsonObject(); 90 | Gson gson = new Gson(); 91 | Comic c = gson.fromJson(obj, Comic.class); 92 | //enter comic 93 | Context activity = getActivity(); 94 | Intent intent = new Intent(activity, ComicActivity.class); 95 | Bundle args = new Bundle(); 96 | 97 | intent.putExtra(ComicPresenter.ARG_ID, c.getId()); 98 | intent.putExtra(ComicPresenter.ARG_MID, c.getMid()); 99 | intent.putExtra(ComicPresenter.ARG_TITLE, c.getTitle().toString()); 100 | intent.putExtra(ComicPresenter.ARG_NUM_OF_PAGES, c.getNumOfPages()); 101 | intent.putExtra(ComicPresenter.ARG_PAGE_TYPES, c.getPageTypes()); 102 | 103 | activity.startActivity(intent, args); 104 | } 105 | }); 106 | 107 | } catch (NumberFormatException _ignored) { 108 | // do searching, not an id 109 | Log.d(TAG, String.format("onClick: searching %s", query)); 110 | Bundle bundle = new Bundle(); 111 | bundle.putString(ComicListFragment.ARG_COLLECTION_NAME, "result"); 112 | bundle.putString(ComicListFragment.ARG_QUERY, query); 113 | Navigation.findNavController(requireView()).navigate(R.id.comicListFragment, bundle); 114 | } 115 | 116 | return false; 117 | } 118 | 119 | @Override 120 | public boolean onQueryTextChange(String newText) { 121 | return false; 122 | } 123 | }); 124 | 125 | super.onCreateOptionsMenu(menu, inflater); 126 | } 127 | 128 | } 129 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/ttdyce/nhviewer/view/SettingsFragment.java: -------------------------------------------------------------------------------- 1 | package com.github.ttdyce.nhviewer.view; 2 | 3 | import android.os.Bundle; 4 | import android.util.Log; 5 | import android.widget.Toast; 6 | 7 | import androidx.appcompat.app.AlertDialog; 8 | import androidx.navigation.NavController; 9 | import androidx.navigation.Navigation; 10 | import androidx.preference.Preference; 11 | import androidx.preference.PreferenceFragmentCompat; 12 | import androidx.preference.PreferenceScreen; 13 | import androidx.preference.SwitchPreference; 14 | 15 | import com.github.ttdyce.nhviewer.BuildConfig; 16 | import com.github.ttdyce.nhviewer.R; 17 | import com.microsoft.appcenter.distribute.Distribute; 18 | 19 | public class SettingsFragment extends PreferenceFragmentCompat { 20 | 21 | @Override 22 | public void onCreate(Bundle savedInstanceState) { 23 | super.onCreate(savedInstanceState); 24 | 25 | // Load the preferences from an XML resource 26 | addPreferencesFromResource(R.xml.preferences); 27 | 28 | showVersionName(); 29 | setVersionOnClick(); 30 | 31 | PreferenceScreen proxyPreference = findPreference(MainActivity.KEY_PREF_PROXY); 32 | proxyPreference.setOnPreferenceClickListener(preference -> { 33 | NavController navController = Navigation.findNavController(getActivity(), R.id.fragmentNavHost); 34 | navController.navigate(R.id.proxySettingsFragment); 35 | return true; 36 | }); 37 | setCheckUpdateOnClick(); 38 | } 39 | 40 | private void setCheckUpdateOnClick() { 41 | 42 | SwitchPreference checkUpdatePreference = findPreference(MainActivity.KEY_PREF_CHECK_UPDATE); 43 | checkUpdatePreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { 44 | @Override 45 | public boolean onPreferenceClick(Preference preference) { 46 | Toast.makeText(requireActivity().getApplicationContext(), R.string.remind_restart_after_setting, Toast.LENGTH_SHORT).show(); 47 | return true; 48 | } 49 | }); 50 | } 51 | 52 | @Override 53 | public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { 54 | 55 | 56 | } 57 | 58 | private void setVersionOnClick() { 59 | PreferenceScreen versionPreference = findPreference(MainActivity.KEY_PREF_VERSION); 60 | versionPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { 61 | @Override 62 | public boolean onPreferenceClick(Preference preference) { 63 | Toast.makeText(requireActivity().getApplicationContext(), "Checking latest version...", Toast.LENGTH_SHORT).show(); 64 | Distribute.checkForUpdate(); 65 | 66 | return true; 67 | } 68 | }); 69 | } 70 | 71 | private void showVersionName() { 72 | int versionCode = BuildConfig.VERSION_CODE; 73 | String versionName = BuildConfig.VERSION_NAME; 74 | Log.i("SettingsFragment", "onCreate: version name=" + versionName); 75 | Log.i("SettingsFragment", "onCreate: version code=" + versionCode); 76 | 77 | PreferenceScreen editTextPreference = findPreference(MainActivity.KEY_PREF_VERSION); 78 | editTextPreference.setSummary(versionName); 79 | } 80 | 81 | 82 | 83 | 84 | public enum Language{ 85 | all(0), chinese(1), english(2), japanese(3), notSet(-1); 86 | 87 | int id; 88 | 89 | Language(int i) { 90 | id = i; 91 | } 92 | 93 | public int getInt() { 94 | return id; 95 | } 96 | 97 | public String toString(){ 98 | return String.valueOf(id); 99 | } 100 | 101 | } 102 | 103 | } 104 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/ttdyce/nhviewer/view/SplashActivity.java: -------------------------------------------------------------------------------- 1 | package com.github.ttdyce.nhviewer.view; 2 | 3 | import android.content.Intent; 4 | import android.os.Bundle; 5 | import android.os.Handler; 6 | import android.util.Log; 7 | import android.view.animation.Animation; 8 | import android.view.animation.AnimationUtils; 9 | import android.webkit.CookieManager; 10 | import android.webkit.WebView; 11 | import android.widget.TextView; 12 | import android.widget.Toast; 13 | 14 | import androidx.appcompat.app.AppCompatActivity; 15 | 16 | import com.github.ttdyce.nhviewer.R; 17 | import com.github.ttdyce.nhviewer.model.CookieStringRequest; 18 | 19 | public class SplashActivity extends AppCompatActivity { 20 | 21 | @Override 22 | protected void onCreate(Bundle savedInstanceState) { 23 | super.onCreate(savedInstanceState); 24 | setContentView(R.layout.activity_splash); 25 | 26 | /* Use a WebView to bypass cloudflare challenge (Retrieve cookie) */ 27 | // any api url is fine :| 28 | String url = "https://nhentai.net"; 29 | WebView wvInvisibleSplash = findViewById(R.id.wvInvisibleSplash); 30 | 31 | // uncomment this part to simulate no-cookie state, for debugging 32 | CookieManager.getInstance().removeAllCookies(null); 33 | CookieManager.getInstance().flush(); 34 | 35 | wvInvisibleSplash.getSettings().setJavaScriptEnabled(true); 36 | wvInvisibleSplash.getSettings().setUserAgentString("Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/112.0"); 37 | 38 | wvInvisibleSplash.loadUrl(url); 39 | 40 | /* Performs animation on a TextView. */ 41 | TextView splashLoading = findViewById(R.id.tvSplashLoading); 42 | Animation animation = AnimationUtils.loadAnimation(this, R.anim.splash_loading); 43 | splashLoading.startAnimation(animation); 44 | 45 | // TODO: 12/15/2019 hard coded SplashActivity wait for 1.5 second 46 | // TODO: 8/1/2022 hard coded 5 seconds for cloudflare challenge 47 | Handler handler = new Handler(); 48 | Intent refreshCookieIntent = new Intent(this, RefreshCookieActivity.class); 49 | handler.postDelayed(new Runnable() { 50 | public void run() { 51 | // handle Cloudflare challenge 52 | String cookies = CookieManager.getInstance().getCookie(url); 53 | Log.d("SplashActivitiy", "url: " + url); 54 | Log.d("SplashActivitiy", "Got cookie: " + cookies); 55 | Log.d("SplashActivitiy", "User agent: " + wvInvisibleSplash.getSettings().getUserAgentString()); 56 | if (cookies == null || !cookies.contains("cf_clearance=")) { 57 | Log.e("SplashActivitiy", "Not found required cookie: cf_clearance, try loading anyway as recently CF Cookie is not needed"); 58 | 59 | Toast.makeText(getApplicationContext(), "Failed to bypass human checking, maybe direct connect is allowed", Toast.LENGTH_LONG).show(); 60 | startActivity(refreshCookieIntent); 61 | CookieStringRequest.challengeCookies = "cf_clearance=N/A"; 62 | CookieStringRequest.userAgent = wvInvisibleSplash.getSettings().getUserAgentString(); 63 | } else { 64 | CookieStringRequest.challengeCookies = cookies; 65 | CookieStringRequest.userAgent = wvInvisibleSplash.getSettings().getUserAgentString(); 66 | } 67 | 68 | // return to MainActivity 69 | finish(); 70 | } 71 | }, 2 * 1000); // 2 seconds 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /app/src/main/res/anim/splash_loading.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 7 | 9 | 11 | 12 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 9 | 11 | 12 | 18 | 19 | 20 | 21 | 22 | 23 | 26 | 31 | 36 | 39 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_add_to_photos_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_check_box_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_delete_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_done_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_favorite_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_favorite_border_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_folder_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_home_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_keyboard_arrow_right_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 10 | 12 | 14 | 16 | 18 | 20 | 22 | 24 | 26 | 28 | 30 | 32 | 34 | 36 | 38 | 40 | 42 | 44 | 46 | 48 | 50 | 52 | 54 | 56 | 58 | 60 | 62 | 64 | 66 | 68 | 70 | 72 | 74 | 75 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_nhviewer_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 10 | 12 | 14 | 16 | 18 | 20 | 22 | 24 | 26 | 28 | 30 | 32 | 34 | 36 | 38 | 40 | 42 | 44 | 46 | 48 | 50 | 52 | 54 | 56 | 58 | 60 | 62 | 64 | 66 | 68 | 70 | 72 | 74 | 75 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_nhlogo.xml: -------------------------------------------------------------------------------- 1 | 6 | 11 | 16 | 19 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_search_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_settings_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_sort_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/rectangle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_backup.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 13 | 14 | 20 | 21 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_comic.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 13 | 14 | 20 | 21 | 22 | 23 | 24 | 25 | 32 | 33 | 34 | 39 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 11 | 20 | 21 | 25 | 26 | 34 | 35 | 36 | 37 | 40 | 41 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_refresh_cookie.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_splash.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 16 | 17 | 22 | 23 | 29 | 30 | 36 | 37 | 45 | 46 | 51 | 52 | 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_comic_list.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 12 | 13 | 18 | 19 | 26 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_comic.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 14 | 15 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_comic_collection_list.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 16 | 17 | 24 | 25 | 26 | 32 | 33 | 34 | 37 | 38 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_comic_list.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 20 | 21 | 24 | 25 | 30 | 31 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 53 | 54 | 55 | 58 | 59 | 68 | 69 | 75 | 76 | 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /app/src/main/res/menu/app_bar_items_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 12 | 17 | 22 | -------------------------------------------------------------------------------- /app/src/main/res/menu/app_bar_items_searching.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/menu/app_bar_selection_mode.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 10 | 11 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/menu/bottom_navigation_items.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 11 | 15 | 19 | 23 | 24 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_nhviewer.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_nhviewer_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ttdyce/NHentai-NHViewer/12d8416263166b4f15369b00a4255eb8039efe63/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ttdyce/NHentai-NHViewer/12d8416263166b4f15369b00a4255eb8039efe63/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_nhviewer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ttdyce/NHentai-NHViewer/12d8416263166b4f15369b00a4255eb8039efe63/app/src/main/res/mipmap-hdpi/ic_launcher_nhviewer.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_nhviewer_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ttdyce/NHentai-NHViewer/12d8416263166b4f15369b00a4255eb8039efe63/app/src/main/res/mipmap-hdpi/ic_launcher_nhviewer_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_nhviewer_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ttdyce/NHentai-NHViewer/12d8416263166b4f15369b00a4255eb8039efe63/app/src/main/res/mipmap-hdpi/ic_launcher_nhviewer_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ttdyce/NHentai-NHViewer/12d8416263166b4f15369b00a4255eb8039efe63/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ttdyce/NHentai-NHViewer/12d8416263166b4f15369b00a4255eb8039efe63/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ttdyce/NHentai-NHViewer/12d8416263166b4f15369b00a4255eb8039efe63/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_nhviewer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ttdyce/NHentai-NHViewer/12d8416263166b4f15369b00a4255eb8039efe63/app/src/main/res/mipmap-mdpi/ic_launcher_nhviewer.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_nhviewer_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ttdyce/NHentai-NHViewer/12d8416263166b4f15369b00a4255eb8039efe63/app/src/main/res/mipmap-mdpi/ic_launcher_nhviewer_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_nhviewer_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ttdyce/NHentai-NHViewer/12d8416263166b4f15369b00a4255eb8039efe63/app/src/main/res/mipmap-mdpi/ic_launcher_nhviewer_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ttdyce/NHentai-NHViewer/12d8416263166b4f15369b00a4255eb8039efe63/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ttdyce/NHentai-NHViewer/12d8416263166b4f15369b00a4255eb8039efe63/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ttdyce/NHentai-NHViewer/12d8416263166b4f15369b00a4255eb8039efe63/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_nhviewer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ttdyce/NHentai-NHViewer/12d8416263166b4f15369b00a4255eb8039efe63/app/src/main/res/mipmap-xhdpi/ic_launcher_nhviewer.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_nhviewer_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ttdyce/NHentai-NHViewer/12d8416263166b4f15369b00a4255eb8039efe63/app/src/main/res/mipmap-xhdpi/ic_launcher_nhviewer_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_nhviewer_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ttdyce/NHentai-NHViewer/12d8416263166b4f15369b00a4255eb8039efe63/app/src/main/res/mipmap-xhdpi/ic_launcher_nhviewer_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ttdyce/NHentai-NHViewer/12d8416263166b4f15369b00a4255eb8039efe63/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ttdyce/NHentai-NHViewer/12d8416263166b4f15369b00a4255eb8039efe63/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ttdyce/NHentai-NHViewer/12d8416263166b4f15369b00a4255eb8039efe63/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_nhviewer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ttdyce/NHentai-NHViewer/12d8416263166b4f15369b00a4255eb8039efe63/app/src/main/res/mipmap-xxhdpi/ic_launcher_nhviewer.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_nhviewer_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ttdyce/NHentai-NHViewer/12d8416263166b4f15369b00a4255eb8039efe63/app/src/main/res/mipmap-xxhdpi/ic_launcher_nhviewer_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_nhviewer_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ttdyce/NHentai-NHViewer/12d8416263166b4f15369b00a4255eb8039efe63/app/src/main/res/mipmap-xxhdpi/ic_launcher_nhviewer_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ttdyce/NHentai-NHViewer/12d8416263166b4f15369b00a4255eb8039efe63/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ttdyce/NHentai-NHViewer/12d8416263166b4f15369b00a4255eb8039efe63/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ttdyce/NHentai-NHViewer/12d8416263166b4f15369b00a4255eb8039efe63/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_nhviewer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ttdyce/NHentai-NHViewer/12d8416263166b4f15369b00a4255eb8039efe63/app/src/main/res/mipmap-xxxhdpi/ic_launcher_nhviewer.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_nhviewer_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ttdyce/NHentai-NHViewer/12d8416263166b4f15369b00a4255eb8039efe63/app/src/main/res/mipmap-xxxhdpi/ic_launcher_nhviewer_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_nhviewer_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ttdyce/NHentai-NHViewer/12d8416263166b4f15369b00a4255eb8039efe63/app/src/main/res/mipmap-xxxhdpi/ic_launcher_nhviewer_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ttdyce/NHentai-NHViewer/12d8416263166b4f15369b00a4255eb8039efe63/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/navigation/nav_app.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 12 | 16 | 20 | 21 | 26 | 30 | 34 | 35 | 40 | 44 | 48 | 49 | 54 | 58 | 59 | 64 | 67 | 70 | 71 | 76 | 79 | 80 | 85 | 86 | 88 | -------------------------------------------------------------------------------- /app/src/main/res/values-zh-rCN/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 首页加载中 5 | 6 | 7 | 8 | 所有 9 | 中文 10 | 英文 11 | 日文 12 | 13 | 14 | 15 | 有新版本了: %1$s 16 | 17 | 18 | 首页 19 | 收藏 20 | 搜索 21 | 最爱 22 | 设置 23 | 24 | 语言 25 | 启用开屏动画 26 | 检查更新 (VS App Center) 27 | 显示模式 28 | 通用 29 | 备份 30 | 扫描二维码 31 | 关于 32 | 作者 33 | 版本号 34 | 取消 35 | 载入 %s 中 36 | 设置漫画语言 37 | %s]]> 38 | 删除漫画时出错: %s 39 | %s 中删除: %s]]> 40 | %s]]> 41 | 搜索 42 | 设定会在重启后生效 43 | "需要从 GitHub 重新下载,以更新到3.0.0版本 (V3)。有关 V3 的更新内容亦放在了 GitHub 上 (如自动更新,外观,和更多的新功能!) " 44 | "更新到 3.0.0 " 45 | 46 | 47 | -------------------------------------------------------------------------------- /app/src/main/res/values-zh-rTW/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 載入首頁中 5 | 6 | 7 | 8 | 所有 9 | 中文 10 | 英文 11 | 日文 12 | 13 | 14 | 15 | 新版本檢查: %1$s 16 | 17 | 18 | 首頁 19 | 收藏 20 | 搜尋 21 | 最愛 22 | 設定 23 | 24 | 語言 25 | 開啟打開動畫 26 | 檢查更新 (VS App Center) 27 | 演示模式 28 | 通用 29 | 備份 30 | 掃瞄二維碼 31 | 關於 32 | 維護者 33 | 安裝版本 34 | 取消 35 | 載入 %s 中 36 | 設定漫畫語言 37 | %s]]> 38 | 刪除漫畫時出錯: %s 39 | %s 中刪除: %s]]> 40 | %s]]> 41 | 搜尋 42 | 設定會在重啟後生效 43 | 需要從 GitHub 重新下載,以更新到3.0.0版本 (V3)。有關 V3 的更新內容亦放在了 GitHub 上 (如自動更新,外觀,和更多的新功能!) 44 | "更新到 3.0.0 " 45 | 按受歡迎程度排列 46 | 47 | -------------------------------------------------------------------------------- /app/src/main/res/values/arrays.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #a00037 4 | #ff5c8d 5 | #8A1747 6 | #b0bec5 7 | #e2f1f8 8 | #808e95 9 | 10 | @color/secondaryTextColor 11 | #fff 12 | @color/secondaryColor 13 | #fff 14 | #202a34 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/values/ic_launcher_nhviewer_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #202A34 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | NHViewer 4 | github.com/ttdyce 5 | Abandon all hope, ye who enter here 6 | ここから入らんとする者は一切の希望を放棄せよ 7 | Comic inner page 8 | Comic thumbnail 9 | Comic selector 10 | Comic collect button 11 | Comic favorite button 12 | 13 | 14 | Loading from index 15 | 16 | 17 | 18 | All 19 | Chinese 20 | English 21 | Japanese 22 | 23 | 24 | All 25 | Chinese 26 | English 27 | Japanese 28 | 29 | 30 | 0 31 | 1 32 | 2 33 | 3 34 | 35 | 36 | 37 | 38 | None 39 | All-Time 40 | Month 41 | Week 42 | Day 43 | 44 | 45 | 46 | New version available %1$s 47 | 48 | Index 49 | Collection 50 | Search 51 | Favorite 52 | Setting 53 | Language 54 | Enable splash screen 55 | Check update (by VS App Center) 56 | Demo mode 57 | General 58 | Backup 59 | Scan QR code (Connect to server) 60 | About 61 | Maintained by 62 | Version 63 | Loading from %s … 64 | %s]]> 65 | %s]]> 66 | %s, named %s]]> 67 | Error when deleting comic named %s 68 | Set your default language 69 | Cancel 70 | Enter search query 71 | Settings would take effect after restart 72 | Re-downloading from GitHub is required for version 3.0.0 (V3). Please go to GitHub to see more details about V3 (e.g. changes on auto-update, the look and feel, and more features!) 73 | Version 3.0.0 update 74 | Popular type 75 | 76 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 20 | 21 | 24 | 25 | 29 | 30 | 31 | 37 | 38 | 42 | 43 | 50 | 51 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /app/src/main/res/xml/firebase_default_config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | updateRequired 6 | false 7 | 8 | 9 | currentVersion 10 | 2.0.0 11 | 12 | 13 | updateUrl 14 | https://github.com/ttdyce/NHentaiViewer/releases/download/2.0.0/nhviewer-2.0.0-signed.apk 15 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/xml/preferences.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 12 | > 16 | 19 | 20 | 21 | 26 | 31 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 48 | 49 | 53 | 54 | -------------------------------------------------------------------------------- /app/src/main/res/xml/proxy_preferences.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/test/java/com/github/ttdyce/nhviewer/APIUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.github.ttdyce.nhviewer; 2 | 3 | import com.github.ttdyce.nhviewer.model.api.NHAPI; 4 | 5 | import org.junit.Test; 6 | 7 | import static org.junit.Assert.assertEquals; 8 | 9 | public class APIUnitTest { 10 | private final String mid = "1438192"; 11 | 12 | @Test 13 | public void get_thumbnail_url() { 14 | String url = NHAPI.URLs.getThumbnail(mid, "j"); 15 | 16 | assertEquals(url, "https://t.nhentai.net/galleries/" + mid + "/thumb.jpg"); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /app/src/test/java/com/github/ttdyce/nhviewer/ComicCollectionUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.github.ttdyce.nhviewer; 2 | 3 | import com.github.ttdyce.nhviewer.model.comic.Comic; 4 | import com.github.ttdyce.nhviewer.model.room.ComicCollectionEntity; 5 | 6 | import org.junit.Test; 7 | 8 | import java.util.ArrayList; 9 | import java.util.Date; 10 | import java.util.List; 11 | 12 | public class ComicCollectionUnitTest { 13 | private static final String TAG = "ComicCollectionUnitTest"; 14 | 15 | @Test 16 | public void comicCollection_creation() { 17 | String name = "Demo collection"; 18 | List comics = new ArrayList<>(); 19 | comics.add(new Comic()); 20 | comics.add(new Comic()); 21 | comics.add(new Comic()); 22 | comics.add(new Comic()); 23 | 24 | // ComicCollectionEntity cc = new ComicCollectionEntity(name, comics); 25 | 26 | // assertNotNull(cc); 27 | 28 | } 29 | 30 | @Test 31 | public void comicCollectionEntity_toJson() { 32 | String name = "Demo collection"; 33 | ComicCollectionEntity cc = new ComicCollectionEntity(name, 333, new Date()); 34 | 35 | cc.toJson(); 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /app/src/test/java/com/github/ttdyce/nhviewer/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.github.ttdyce.nhviewer; 2 | 3 | /** 4 | * Example local unit test, which will execute on the development machine (host). 5 | * 6 | * @see Testing documentation 7 | */ 8 | public class ExampleUnitTest { 9 | 10 | 11 | } -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | repositories { 5 | google() 6 | jcenter() 7 | maven { 8 | url "https://plugins.gradle.org/m2/" 9 | } 10 | } 11 | dependencies { 12 | classpath 'com.android.tools.build:gradle:4.0.0' 13 | // NOTE: Do not place your application dependencies here; they belong 14 | // in the individual module build.gradle files 15 | //Firebase 16 | classpath 'com.google.gms:google-services:4.3.2' 17 | classpath "org.ajoberstar.grgit:grgit-gradle:4.1.1" 18 | } 19 | } 20 | 21 | 22 | allprojects { 23 | repositories { 24 | mavenCentral() 25 | google() 26 | jcenter() 27 | 28 | } 29 | } 30 | 31 | task clean(type: Delete) { 32 | delete rootProject.buildDir 33 | } 34 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | # fix gson >jdk8 exception 20220104 10 | org.gradle.jvmargs=-Xmx1536m --add-opens=java.base/java.io=ALL-UNNAMED 11 | # When configured, Gradle will run in incubating parallel mode. 12 | # This option should only be used with decoupled projects. More details, visit 13 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 14 | # org.gradle.parallel=true 15 | # AndroidX package structure to make it clearer which packages are bundled with the 16 | # Android operating system, and which are packaged with your app's APK 17 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 18 | android.useAndroidX=true 19 | # Automatically convert third-party libraries to use AndroidX 20 | android.enableJetifier=true 21 | 22 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ttdyce/NHentai-NHViewer/12d8416263166b4f15369b00a4255eb8039efe63/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Jun 09 04:04:02 HKT 2020 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /screenshots/collection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ttdyce/NHentai-NHViewer/12d8416263166b4f15369b00a4255eb8039efe63/screenshots/collection.png -------------------------------------------------------------------------------- /screenshots/deprecated/V2/collection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ttdyce/NHentai-NHViewer/12d8416263166b4f15369b00a4255eb8039efe63/screenshots/deprecated/V2/collection.png -------------------------------------------------------------------------------- /screenshots/deprecated/V2/comic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ttdyce/NHentai-NHViewer/12d8416263166b4f15369b00a4255eb8039efe63/screenshots/deprecated/V2/comic.png -------------------------------------------------------------------------------- /screenshots/deprecated/V2/favorite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ttdyce/NHentai-NHViewer/12d8416263166b4f15369b00a4255eb8039efe63/screenshots/deprecated/V2/favorite.png -------------------------------------------------------------------------------- /screenshots/deprecated/V2/index.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ttdyce/NHentai-NHViewer/12d8416263166b4f15369b00a4255eb8039efe63/screenshots/deprecated/V2/index.png -------------------------------------------------------------------------------- /screenshots/deprecated/V2/search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ttdyce/NHentai-NHViewer/12d8416263166b4f15369b00a4255eb8039efe63/screenshots/deprecated/V2/search.png -------------------------------------------------------------------------------- /screenshots/deprecated/V2/setting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ttdyce/NHentai-NHViewer/12d8416263166b4f15369b00a4255eb8039efe63/screenshots/deprecated/V2/setting.png -------------------------------------------------------------------------------- /screenshots/deprecated/collection_list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ttdyce/NHentai-NHViewer/12d8416263166b4f15369b00a4255eb8039efe63/screenshots/deprecated/collection_list.png -------------------------------------------------------------------------------- /screenshots/deprecated/favorite_list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ttdyce/NHentai-NHViewer/12d8416263166b4f15369b00a4255eb8039efe63/screenshots/deprecated/favorite_list.png -------------------------------------------------------------------------------- /screenshots/deprecated/navigation_view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ttdyce/NHentai-NHViewer/12d8416263166b4f15369b00a4255eb8039efe63/screenshots/deprecated/navigation_view.png -------------------------------------------------------------------------------- /screenshots/search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ttdyce/NHentai-NHViewer/12d8416263166b4f15369b00a4255eb8039efe63/screenshots/search.png -------------------------------------------------------------------------------- /screenshots/setting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ttdyce/NHentai-NHViewer/12d8416263166b4f15369b00a4255eb8039efe63/screenshots/setting.png -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | rootProject.name='NHViewer' 3 | --------------------------------------------------------------------------------