Repository: maxwai/NClientV3 Branch: main Commit: da29ac75c9a1 Files: 313 Total size: 9.1 MB Directory structure: gitextract_atw2n2is/ ├── .editorconfig ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.yaml │ │ ├── config.yml │ │ └── feature_request.yaml │ ├── PULL_REQUEST_TEMPLATE.md │ ├── dependabot.yml │ └── workflows/ │ ├── android.yml │ ├── dependencies.yml │ └── update_data.yml ├── .gitignore ├── DCO ├── LICENSE ├── README.md ├── SECURITY.md ├── app/ │ ├── .gitignore │ ├── build.gradle.kts │ ├── proguard-rules.pro │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ ├── java/ │ │ └── com/ │ │ └── maxwai/ │ │ └── nclientv3/ │ │ ├── ApiKeyActivity.java │ │ ├── BookmarkActivity.java │ │ ├── CommentActivity.java │ │ ├── CopyToClipboardActivity.java │ │ ├── FavoriteActivity.java │ │ ├── GalleryActivity.java │ │ ├── HistoryActivity.java │ │ ├── LocalActivity.java │ │ ├── MainActivity.java │ │ ├── PINActivity.java │ │ ├── RandomActivity.java │ │ ├── SearchActivity.java │ │ ├── SettingsActivity.java │ │ ├── StatusManagerActivity.java │ │ ├── StatusViewerActivity.java │ │ ├── TagFilterActivity.java │ │ ├── ZoomActivity.java │ │ ├── adapters/ │ │ │ ├── BookmarkAdapter.java │ │ │ ├── CommentAdapter.java │ │ │ ├── FavoriteAdapter.java │ │ │ ├── GalleryAdapter.java │ │ │ ├── GenericAdapter.java │ │ │ ├── HistoryAdapter.java │ │ │ ├── ListAdapter.java │ │ │ ├── LocalAdapter.java │ │ │ ├── StatusManagerAdapter.java │ │ │ ├── StatusViewerAdapter.java │ │ │ └── TagsAdapter.java │ │ ├── api/ │ │ │ ├── InspectorV3.java │ │ │ ├── RandomLoader.java │ │ │ ├── SimpleGallery.java │ │ │ ├── comments/ │ │ │ │ ├── Comment.java │ │ │ │ ├── CommentsFetcher.java │ │ │ │ └── User.java │ │ │ ├── components/ │ │ │ │ ├── Gallery.java │ │ │ │ ├── GalleryData.java │ │ │ │ ├── GenericGallery.java │ │ │ │ ├── Page.java │ │ │ │ ├── Ranges.java │ │ │ │ ├── Tag.java │ │ │ │ └── TagList.java │ │ │ ├── enums/ │ │ │ │ ├── ApiRequestType.java │ │ │ │ ├── ImageType.java │ │ │ │ ├── Language.java │ │ │ │ ├── SortType.java │ │ │ │ ├── SpecialTagIds.java │ │ │ │ ├── TagStatus.java │ │ │ │ ├── TagType.java │ │ │ │ └── TitleType.java │ │ │ └── local/ │ │ │ ├── FakeInspector.java │ │ │ ├── LocalGallery.java │ │ │ └── LocalSortType.java │ │ ├── async/ │ │ │ ├── MetadataFetcher.java │ │ │ ├── ScrapeTags.java │ │ │ ├── VersionChecker.java │ │ │ ├── converters/ │ │ │ │ └── CreatePdfOrZip.java │ │ │ ├── database/ │ │ │ │ ├── DatabaseHelper.java │ │ │ │ ├── Queries.java │ │ │ │ └── export/ │ │ │ │ ├── Exporter.java │ │ │ │ ├── Importer.java │ │ │ │ └── Manager.java │ │ │ └── downloader/ │ │ │ ├── DownloadGalleryV2.java │ │ │ ├── DownloadObserver.java │ │ │ ├── DownloadQueue.java │ │ │ ├── GalleryDownloaderManager.java │ │ │ ├── GalleryDownloaderV2.java │ │ │ └── PageChecker.java │ │ ├── components/ │ │ │ ├── CookieInterceptor.java │ │ │ ├── CustomCookieJar.java │ │ │ ├── GlideX.java │ │ │ ├── ThreadAsyncTask.java │ │ │ ├── activities/ │ │ │ │ ├── BaseActivity.java │ │ │ │ ├── CrashApplication.java │ │ │ │ └── GeneralActivity.java │ │ │ ├── classes/ │ │ │ │ ├── Bookmark.java │ │ │ │ ├── History.java │ │ │ │ ├── MultichoiceAdapter.java │ │ │ │ └── Size.java │ │ │ ├── launcher/ │ │ │ │ ├── LauncherCalculator.java │ │ │ │ └── LauncherReal.java │ │ │ ├── status/ │ │ │ │ ├── Status.java │ │ │ │ └── StatusManager.java │ │ │ ├── views/ │ │ │ │ ├── CFTokenView.java │ │ │ │ ├── GeneralPreferenceFragment.java │ │ │ │ ├── PageSwitcher.java │ │ │ │ ├── RangeSelector.java │ │ │ │ └── ZoomFragment.java │ │ │ └── widgets/ │ │ │ ├── ChipTag.java │ │ │ ├── CustomGridLayoutManager.java │ │ │ ├── CustomImageView.java │ │ │ ├── CustomLinearLayoutManager.java │ │ │ ├── CustomSearchView.java │ │ │ ├── CustomSwipe.java │ │ │ └── TagTypePage.java │ │ ├── files/ │ │ │ ├── GalleryFolder.java │ │ │ └── PageFile.java │ │ ├── github/ │ │ │ └── chrisbanes/ │ │ │ └── photoview/ │ │ │ ├── Compat.java │ │ │ ├── CustomGestureDetector.java │ │ │ ├── OnGestureListener.java │ │ │ ├── OnMatrixChangedListener.java │ │ │ ├── OnOutsidePhotoTapListener.java │ │ │ ├── OnPhotoTapListener.java │ │ │ ├── OnScaleChangedListener.java │ │ │ ├── OnSingleFlingListener.java │ │ │ ├── OnViewDragListener.java │ │ │ ├── OnViewTapListener.java │ │ │ ├── PhotoView.java │ │ │ ├── PhotoViewAttacher.java │ │ │ └── Util.java │ │ ├── loginapi/ │ │ │ ├── LoadTags.java │ │ │ └── User.java │ │ ├── settings/ │ │ │ ├── ApiAuthInterceptor.java │ │ │ ├── AuthCredentials.java │ │ │ ├── AuthStore.java │ │ │ ├── Database.java │ │ │ ├── DefaultDialogs.java │ │ │ ├── Favorites.java │ │ │ ├── Global.java │ │ │ ├── Login.java │ │ │ ├── NotificationSettings.java │ │ │ └── TagV2.java │ │ ├── ui/ │ │ │ └── main/ │ │ │ ├── PlaceholderFragment.java │ │ │ └── SectionsPagerAdapter.java │ │ └── utility/ │ │ ├── CSRFGet.java │ │ ├── ImageDownloadUtility.java │ │ ├── IntentUtility.java │ │ ├── LogUtility.java │ │ ├── Utility.java │ │ └── network/ │ │ └── NetworkUtil.java │ └── res/ │ ├── drawable/ │ │ ├── ic_access_time.xml │ │ ├── ic_add.xml │ │ ├── ic_archive.xml │ │ ├── ic_arrow_back.xml │ │ ├── ic_arrow_forward.xml │ │ ├── ic_backspace.xml │ │ ├── ic_bookmark.xml │ │ ├── ic_bookmark_border.xml │ │ ├── ic_burst_mode.xml │ │ ├── ic_chat_bubble.xml │ │ ├── ic_check.xml │ │ ├── ic_check_circle.xml │ │ ├── ic_close.xml │ │ ├── ic_cnbw.xml │ │ ├── ic_content_copy.xml │ │ ├── ic_delete.xml │ │ ├── ic_exit_to_app.xml │ │ ├── ic_favorite.xml │ │ ├── ic_favorite_border.xml │ │ ├── ic_filter_list.xml │ │ ├── ic_find_in_page.xml │ │ ├── ic_folder.xml │ │ ├── ic_gbbw.xml │ │ ├── ic_hashtag.xml │ │ ├── ic_help.xml │ │ ├── ic_jpbw.xml │ │ ├── ic_keyboard_arrow_left.xml │ │ ├── ic_keyboard_arrow_right.xml │ │ ├── ic_launcher_calculator_foreground.xml │ │ ├── ic_launcher_foreground.xml │ │ ├── ic_logo.xml │ │ ├── ic_mode_edit.xml │ │ ├── ic_pause.xml │ │ ├── ic_pdf.xml │ │ ├── ic_person.xml │ │ ├── ic_play.xml │ │ ├── ic_refresh.xml │ │ ├── ic_rotate_90_degrees.xml │ │ ├── ic_save.xml │ │ ├── ic_search.xml │ │ ├── ic_select_all.xml │ │ ├── ic_settings.xml │ │ ├── ic_share.xml │ │ ├── ic_shuffle.xml │ │ ├── ic_sort.xml │ │ ├── ic_sort_by_alpha.xml │ │ ├── ic_star.xml │ │ ├── ic_star_border.xml │ │ ├── ic_view_1.xml │ │ ├── ic_view_2.xml │ │ ├── ic_view_3.xml │ │ ├── ic_view_4.xml │ │ ├── ic_void.xml │ │ ├── ic_world.xml │ │ ├── side_nav_bar.xml │ │ └── thumb.xml │ ├── drawable-anydpi/ │ │ ├── ic_archive.xml │ │ ├── ic_check.xml │ │ ├── ic_close.xml │ │ ├── ic_file.xml │ │ ├── ic_pause.xml │ │ └── ic_play.xml │ ├── drawable-night/ │ │ ├── ic_logo.xml │ │ └── side_nav_bar.xml │ ├── layout/ │ │ ├── activity_api_key.xml │ │ ├── activity_bookmark.xml │ │ ├── activity_comment.xml │ │ ├── activity_gallery.xml │ │ ├── activity_main.xml │ │ ├── activity_pin.xml │ │ ├── activity_random.xml │ │ ├── activity_search.xml │ │ ├── activity_settings.xml │ │ ├── activity_status_viewer.xml │ │ ├── activity_tag_filter.xml │ │ ├── activity_zoom.xml │ │ ├── app_bar_gallery.xml │ │ ├── app_bar_main.xml │ │ ├── autocomplete_entry.xml │ │ ├── bookmark_layout.xml │ │ ├── cftoken_layout.xml │ │ ├── chip_layout.xml │ │ ├── chip_layout_entry.xml │ │ ├── comment_layout.xml │ │ ├── content_gallery.xml │ │ ├── content_main.xml │ │ ├── dialog_add_status.xml │ │ ├── entry_download_layout.xml │ │ ├── entry_download_layout_compact.xml │ │ ├── entry_history.xml │ │ ├── entry_layout.xml │ │ ├── entry_layout_single.xml │ │ ├── entry_status.xml │ │ ├── entry_tag_layout.xml │ │ ├── fragment_status_viewer.xml │ │ ├── fragment_tag_filter.xml │ │ ├── fragment_zoom.xml │ │ ├── image_void_full.xml │ │ ├── image_void_static.xml │ │ ├── info_layout.xml │ │ ├── local_sort_type.xml │ │ ├── multichoice_adapter.xml │ │ ├── nav_header_main.xml │ │ ├── page_changer.xml │ │ ├── page_switcher.xml │ │ ├── range_selector.xml │ │ ├── related_recycler.xml │ │ ├── search_options.xml │ │ ├── search_range.xml │ │ ├── sub_tag_layout.xml │ │ ├── tags_layout.xml │ │ └── zoom_manager.xml │ ├── layout-land/ │ │ └── activity_random.xml │ ├── menu/ │ │ ├── activity_main_drawer.xml │ │ ├── download.xml │ │ ├── gallery.xml │ │ ├── history.xml │ │ ├── local_multichoice.xml │ │ ├── main.xml │ │ ├── menu_tag_filter.xml │ │ ├── menu_zoom.xml │ │ ├── search.xml │ │ └── status_viewer.xml │ ├── mipmap-anydpi/ │ │ ├── ic_launcher.xml │ │ ├── ic_launcher_calculator.xml │ │ └── ic_launcher_calculator_round.xml │ ├── resources.properties │ ├── values/ │ │ ├── attrs.xml │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── ic_launcher_background.xml │ │ ├── ic_launcher_calculator_background.xml │ │ ├── strings.xml │ │ └── styles.xml │ ├── values-ar-rSA/ │ │ └── strings.xml │ ├── values-de-rDE/ │ │ └── strings.xml │ ├── values-es-rES/ │ │ └── strings.xml │ ├── values-fr-rFR/ │ │ └── strings.xml │ ├── values-it-rIT/ │ │ └── strings.xml │ ├── values-ja-rJP/ │ │ └── strings.xml │ ├── values-night/ │ │ └── colors.xml │ ├── values-pt-rBR/ │ │ └── strings.xml │ ├── values-ru-rRU/ │ │ └── strings.xml │ ├── values-tr-rTR/ │ │ └── strings.xml │ ├── values-uk-rUA/ │ │ └── strings.xml │ ├── values-w820dp/ │ │ └── dimens.xml │ ├── values-zh-rCN/ │ │ └── strings.xml │ ├── values-zh-rTW/ │ │ └── strings.xml │ └── xml/ │ ├── backup_content.xml │ ├── provider_paths.xml │ ├── settings.xml │ ├── settings_column.xml │ └── settings_data.xml ├── build.gradle.kts ├── crowdin.yml ├── data/ │ ├── tags.json │ ├── tagsPretty.json │ └── tagsVersion ├── gradle/ │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat ├── scripts/ │ ├── requirements.txt │ └── update_tags.py └── settings.gradle.kts ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ # editorconfig.org root = true [*] charset = utf-8 indent_style = space indent_size = 4 end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.yaml ================================================ name: Bug Report description: Create a report to help us improve labels: [ "bug" ] body: - type: checkboxes attributes: label: Checklist description: Please confirm that you have done following steps. options: - label: I have looked at the common issues and ways to fix them in the [wiki](https://github.com/maxwai/NClientV3/wiki) required: true - label: I have searched the existing issues for the same bug required: true - label: I have attached the log file as an attachment to this issue required: true - type: textarea attributes: label: Describe the bug description: A clear and concise description of what the bug is. validations: required: true - type: textarea attributes: label: Steps To Reproduce description: Steps to reproduce the behavior. placeholder: | 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error validations: required: true - type: textarea attributes: label: Expected behavior description: A clear and concise description of what you expected to happen. validations: required: true - type: textarea attributes: label: Screenshots description: If applicable, add screenshots to help explain your problem. validations: required: false - type: input attributes: label: Android Version description: Android Version of the phone where the App is installed placeholder: "14" validations: required: true - type: input attributes: label: App Version description: App Version where the bug happens placeholder: "4.0.9" validations: required: true - type: textarea attributes: label: Additional context description: Add any other context about the problem here. validations: required: false ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: false ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.yaml ================================================ name: Feature request description: Suggest an idea for this project labels: [ "enhancement" ] body: - type: checkboxes attributes: label: Checklist description: Please confirm that you have done following steps. options: - label: I have searched the existing issues for the same feature required: true - type: textarea attributes: label: Is your feature request related to a problem? Please describe description: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] validations: required: false - type: textarea attributes: label: Describe the solution you'd like description: A clear and concise description of what you want to happen. validations: required: false - type: textarea attributes: label: Describe alternatives you've considered description: A clear and concise description of any alternative solutions or features you've considered. validations: required: false - type: textarea attributes: label: Additional context description: Add any other context about the problem here. validations: required: false ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ A similar PR may already be submitted! Please search among the [Pull request](../) before creating one. Thanks for submitting a pull request! Please provide enough information so that others can review your pull request: For more information, see the `CONTRIBUTING` guide. **Summary** This PR fixes/implements the following **bugs/features** * [ ] Bug 1 * [ ] Bug 2 * [ ] Feature 1 * [ ] Feature 2 * [ ] Breaking changes Explain the **motivation** for making this change. What existing problem does the pull request solve? **Test plan (required)** Demonstrate the code is solid. Example: The exact commands you ran and their output, screenshots / videos if the pull request changes UI. **Code formatting** **Closing issues** Fixes # ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: # Enable version updates for Gradle - package-ecosystem: "gradle" # Look for a `build.gradle.kts` in the `root` directory directory: "/" # Check for updates once a week schedule: interval: "weekly" # Enable version updates for npm - package-ecosystem: "pip" # Look for `package.json` and `lock` files in the `root` directory directory: "/scripts" # Check the npm registry for updates every day (weekdays) schedule: interval: "weekly" # Enable version updates for GitHub Actions - package-ecosystem: "github-actions" # Workflow files stored in the default location of `.github/workflows` # You don't need to specify `/.github/workflows` for `directory`. You can use `directory: "/"`. directory: "/" schedule: interval: "weekly" ================================================ FILE: .github/workflows/android.yml ================================================ name: Android CI on: push: branches: - 'main' pull_request: branches: - '*' jobs: build: runs-on: ubuntu-latest steps: - name: Checkout sources uses: actions/checkout@v6 - name: set up JDK 17 uses: actions/setup-java@v5 with: java-version: '17' distribution: 'temurin' cache: gradle - name: Grant execute permission for gradlew run: chmod +x gradlew - name: Add empty keystore.properties run: | echo "storePassword=dummy" > keystore.properties echo "keyPassword=dummy" >> keystore.properties echo "keyAlias=dummy" >> keystore.properties echo "storeFile=dummy.jks" >> keystore.properties - name: Assemble Debug with Gradle run: ./gradlew assembleDebug - name: Bundle Debug with Gradle run: ./gradlew bundleDebug - name: Upload Artifact uses: actions/upload-artifact@v7 with: name: Debug APKs path: app/build/outputs/apk/ retention-days: 30 ================================================ FILE: .github/workflows/dependencies.yml ================================================ name: Dependency Submission on: push: branches: - 'main' permissions: contents: write jobs: dependency-submission: runs-on: ubuntu-latest steps: - name: Checkout sources uses: actions/checkout@v6 - name: Setup JDK 17 uses: actions/setup-java@v5 with: java-version: '17' distribution: 'temurin' - name: Add empty keystore.properties run: | echo "storePassword=dummy" > keystore.properties echo "keyPassword=dummy" >> keystore.properties echo "keyAlias=dummy" >> keystore.properties echo "storeFile=dummy.jks" >> keystore.properties - name: Generate and submit dependency graph uses: gradle/actions/dependency-submission@v6 with: # Exclude dependencies that are only resolved in test classpaths dependency-graph-exclude-configurations: '.*([aA]ndroidTest|android-test-plugin|unified-test-platform|lint|UnitTest|^test[A-Z]).*' ================================================ FILE: .github/workflows/update_data.yml ================================================ name: Update Data on: schedule: - cron: "42 1 * * 0" workflow_dispatch: jobs: update: runs-on: ubuntu-latest permissions: contents: write pull-requests: write steps: - name: Checkout sources uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v6 with: python-version: "3.13" - name: Install dependencies run: | pip install -r scripts/requirements.txt - name: Run update script run: python scripts/update_tags.py - name: Create Pull Request uses: peter-evans/create-pull-request@v8 with: commit-message: Update tags.json branch: ci/update-tags title: 'Update tags' body: 'Changed files in data directory' reviewers: maxwai ================================================ FILE: .gitignore ================================================ *.iml *.apk .gradle /local.properties .idea/ output.json .DS_Store /build /captures /svgs /app/release /app/debug /app/schemas .externalNativeBuild /crowdin.properties /keystore.properties *.jks ================================================ FILE: DCO ================================================ Developer Certificate of Origin Version 1.1 Copyright (C) 2004, 2006 The Linux Foundation and its contributors. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Developer's Certificate of Origin 1.1 By making a contribution to this project, I certify that: (a) The contribution was created in whole or in part by me and I have the right to submit it under the open source license indicated in the file; or (b) The contribution is based upon previous work that, to the best of my knowledge, is covered under an appropriate open source license and I have the right under that license to submit that work with modifications, whether created in whole or in part by me, under the same open source license (unless I am permitted to submit under a different license), as indicated in the file; or (c) The contribution was provided directly to me by some other person who certified (a), (b) or (c) and I have not modified it. (d) I understand and agree that this project and the contribution are public and that a record of the contribution (including all personal information I submit with it, including my sign-off) is maintained indefinitely and may be redistributed consistent with this project or the open source license(s) involved. ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright 2024 Maxwai Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: README.md ================================================ # NClientV3 [![Github](https://img.shields.io/github/v/release/maxwai/NClientV3.svg?logo=github)](https://github.com/maxwai/NClientV3/releases/latest) An unofficial NHentai Android Client. This is a fork of the original Project by [@Dar9586](https://github.com/Dar9586) found [here](https://github.com/Dar9586/NClientV2) This app works for devices from API 26 (Android 8) and above. For Devices Running Android 8 (SDK 26 and 27) there is a separate APK with `pre28` in the name. Support for this Version of the app will be reduced. Releases: ## Migrate from original NClientV2 to NClientV3 Info can be found in the [wiki](https://github.com/maxwai/NClientV3/wiki/Migrate-from-NClientV2-to-NClientV3) ## Translate App You can help translate the Project by going to the Crowdin Project [here](https://crowdin.com/project/nclientv3/invite?h=33e3f83681ebaea1bf037ed157d2ea272410538). If your desired language is missing, write an issue, the language will be added. ## API Features - Browse main page - Search by query or tags - Include or exclude tags - Blur or hide excluded tags - Download manga - Favorite galleries - Enable PIN to access the app ## Custom feature - Share galleries - Open in browser - Bookmark ## App Screen | Main page | Lateral menu | |:-------------------------------------------------------------------------------------------------------------------------------------------------:|:-------------------------------------------------------------------------------------------------------------------------------------------:| | ![Main page](https://raw.githubusercontent.com/maxwai/NClientV3/refs/heads/main/docs/images/phoneScreenshots/img1.png) | ![Lateral menu](https://raw.githubusercontent.com/maxwai/NClientV3/main/docs/images/phoneScreenshots/img2.png) | | Search | Random manga | | ![Search](https://raw.githubusercontent.com/maxwai/NClientV3/main/docs/images/phoneScreenshots/img3.png) | ![Random manga](https://raw.githubusercontent.com/maxwai/NClientV3/main/docs/images/phoneScreenshots/img4.jpg) | ## Contributors - [shirokun20](https://github.com/shirokun20) for the initial Bug fixes - [w0x8m](https://github.com/w0x8m) for the new language picker & Chinese Simplified & Traditional translation - [ananas7](https://github.com/ananas7) for Arabic translation - [raymi7066](https://github.com/raymi7066) for Chinese Simplified translation - Худі Таджик (tadzikhudi) for Ukrainian translation - [PegadaDLancha](https://github.com/PegadaDLancha) for Brazilian Portuguese translation - Ivan Lost (dovakin1886) for Ukrainian translation - crorcetn for Japanese translation - [Inori333](https://github.com/inori-3333) for the initial implementation of the new api - [Kronos2308](https://github.com/kronos2308) for Spanish translation - [Locked_Fog](https://github.com/locked-fog) for Chinese Simplified translation ## Contributors of original Project - [Still34](https://github.com/Still34) for code cleanup & Traditional Chinese translation - [TacoTheDank](https://github.com/TacoTheDank) for XML and gradle cleanup - [hmaltr](https://github.com/hmaltr) for Turkish translation and issue moderation - [ZerOri](https://github.com/ZerOri) and [linsui](https://github.com/linsui) for Chinese translation - [herrsunchess](https://github.com/herrsunchess) for German translation - [eme22](https://github.com/herrsunchess) for Spanish translation - [velosipedistufa](https://github.com/velosipedistufa) for Russian translation - [bottomtextboy](https://github.com/bottomtextboy) for Arabic translation - [MaticBabnik](https://github.com/MaticBabnik) for bug fixes - [DontPayAttention](https://github.com/DontPayAttention) for French translation - [kuragehimekurara1](https://github.com/kuragehimekurara1) for Japanese translation - [chayleaf](https://github.com/chayleaf) for Cloudflare bypass - [Atmosphelen](https://github.com/Atmosphelen) for Ukrainian translation ## Star History Star History Chart ## Libraries - OKHttp ([License](https://github.com/square/okhttp/blob/master/LICENSE.txt)) - PersistentCookieJar ([License](https://github.com/franmontiel/PersistentCookieJar/blob/master/LICENSE.txt)) - JSoup ([License](https://github.com/jhy/jsoup/blob/master/LICENSE)) - Glide ([License](https://github.com/bumptech/glide/blob/master/LICENSE)) - AmbilWarna ([License](https://github.com/yukuku/ambilwarna/blob/master/LICENSE)) - AndroidFastScroll ([License](https://github.com/zhanghai/AndroidFastScroll/blob/master/LICENSE)) ## License ```text Copyright 2024 maxwai Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ``` ================================================ FILE: SECURITY.md ================================================ # Security Policy ## Supported Versions Only the latest version is supported. The `pre28` versions are also not supported. ## Reporting a Vulnerability If you discover a security vulnerability, please open a private security report in the [Security tab](https://github.com/maxwai/NClientV3/security) of the repo or send an email to [security.penny394@passmail.net](mailto:security.penny394@passmail.net) Please do not create a public issue for the vulnerability. Your email should include the following information: - A description of the vulnerability - Steps to reproduce the vulnerability - Possible impact of the vulnerability - Any suggested mitigation or remediation steps We will respond to your email as soon as possible and work with you to address any security issues. If there isn't a response within 5 working days (considering public holidays in Germany), please send a follow-up post or email, depending on how you reported the vulnerability. ## Language All reports must be done in English ================================================ FILE: app/.gitignore ================================================ /build ================================================ FILE: app/build.gradle.kts ================================================ import com.android.build.api.artifact.SingleArtifact import java.io.FileInputStream import java.util.Properties import com.android.build.api.variant.BuiltArtifactsLoader import java.io.File import org.gradle.api.DefaultTask import org.gradle.api.file.DirectoryProperty import org.gradle.api.provider.Property import org.gradle.api.tasks.Input import org.gradle.api.tasks.InputFiles import org.gradle.api.tasks.Internal import org.gradle.api.tasks.TaskAction import org.gradle.internal.extensions.stdlib.capitalized plugins { id("com.android.application") } val keystorePropertiesFile: File = rootProject.file("keystore.properties") val keystoreProperties = Properties().apply { load(FileInputStream(keystorePropertiesFile)) } android { signingConfigs { create("release") { keyAlias = keystoreProperties["keyAlias"] as String keyPassword = keystoreProperties["keyPassword"] as String storeFile = file(keystoreProperties["storeFile"] as String) storePassword = keystoreProperties["storePassword"] as String } } compileSdk = 36 defaultConfig { applicationId = "com.maxwai.nclientv3" // Format: MmPPbb // M: Major, m: minor, P: Patch, b: build versionCode = 420500 multiDexEnabled = true versionName = "4.2.5" vectorDrawables.useSupportLibrary = true proguardFiles("proguard-rules.pro") } flavorDimensions += "sdk" productFlavors { create("post28") { dimension = "sdk" targetSdk = 36 minSdk = 28 } create("pre28") { dimension = "sdk" targetSdk = 28 minSdk = 26 } } buildTypes { getByName("release") { isMinifyEnabled = true isShrinkResources = true proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt")) versionNameSuffix = "-release" resValue("string", "app_name", "NClientV3") signingConfig = signingConfigs.getByName("release") } getByName("debug") { applicationIdSuffix = ".debug" versionNameSuffix = "-debug" resValue("string", "app_name", "NClientV3 Debug") } } compileOptions { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 } lint { abortOnError = false checkReleaseBuilds = false disable += "RestrictedApi" } bundle { language { // Specifies that the app bundle should not support // configuration APKs for language resources. These // resources are instead packaged with each base and // dynamic feature APK. @Suppress("UnstableApiUsage") enableSplit = false } } namespace = "com.maxwai.nclientv3" buildFeatures { buildConfig = true } androidResources { @Suppress("UnstableApiUsage") generateLocaleConfig = true } } androidComponents { onVariants() { variant -> var renameTask = tasks.register("createRenamedApk${variant.flavorName?.capitalized()}${variant.buildType?.capitalized()}") { this.apkFolder.set(variant.artifacts.get(SingleArtifact.APK)) this.builtArtifactsLoader.set(variant.artifacts.getBuiltArtifactsLoader()) this.versionName.set(variant.outputs.single().versionName.get().substringBeforeLast("-")) this.suffix.set((if (variant.flavorName == "pre28") "_pre28" else "") + (if (variant.buildType == "debug") "_debug" else "")) }.get() tasks.whenTaskAdded { if (name == "assemble${variant.flavorName?.capitalized()}${variant.buildType?.capitalized()}") { finalizedBy(renameTask) } } } } dependencies { // Android implementation("androidx.appcompat:appcompat:1.7.1") implementation("androidx.cardview:cardview:1.0.0") implementation("androidx.constraintlayout:constraintlayout:2.2.1") implementation("androidx.fragment:fragment:1.8.9") implementation("androidx.preference:preference:1.2.1") implementation("androidx.viewpager2:viewpager2:1.1.0") implementation("androidx.recyclerview:recyclerview:1.4.0") implementation("androidx.work:work-runtime:2.11.2") implementation("androidx.core:core-splashscreen:1.2.0") implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.2.0") implementation("androidx.biometric:biometric:1.1.0") implementation("com.google.android.material:material:1.13.0") // Other // image loading and caching implementation("com.github.bumptech.glide:glide:5.0.5") { exclude(group = "com.android.support") } implementation("com.github.bumptech.glide:okhttp3-integration:5.0.5@aar") annotationProcessor("com.github.bumptech.glide:compiler:5.0.5") // For Http Connection implementation("com.squareup.okhttp3:okhttp-urlconnection:5.3.2") // Used to store the cookies between runs implementation("com.github.franmontiel:PersistentCookieJar:v1.0.1") // To parse HTML implementation("org.jsoup:jsoup:1.22.1") // color picker implementation("com.github.yukuku:ambilwarna:2.0.1") // fast scroll implementation("me.zhanghai.android.fastscroll:library:1.3.0") } abstract class CreateRenamedApk : DefaultTask() { @get:InputFiles abstract val apkFolder: DirectoryProperty @get:Internal abstract val builtArtifactsLoader: Property @get:Input abstract val versionName: Property @get:Input abstract val suffix: Property @TaskAction fun taskAction() { val builtArtifacts = builtArtifactsLoader.get().load(apkFolder.get()) ?: throw RuntimeException("Cannot load APKs") File(builtArtifacts.elements.single().outputFile).renameTo( File(apkFolder.asFile.get(), "NClientV3_${versionName.get()}${suffix.get()}.apk") ) } } ================================================ FILE: app/proguard-rules.pro ================================================ # Add project specific ProGuard rules here. # You can control the set of applied configuration files using the # proguardFiles setting in build.gradle.kts. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} # Uncomment this to preserve the line number information for # debugging stack traces. #-keepattributes SourceFile,LineNumberTable # If you keep the line number information, uncomment this to # hide the original source file name. #-renamesourcefileattribute SourceFile -ignorewarnings -keep class * { public private *; } #glide proguard -keep public class * implements com.bumptech.glide.module.GlideModule -keep public class * extends com.bumptech.glide.module.AppGlideModule -keep public enum com.bumptech.glide.load.ImageHeaderParser$** { **[] $VALUES; public *; } -assumenosideeffects class com.maxwai.nclientv3.utility.LogUtility { public static void d(...); public static void i(...); public static void e(...); } -keep public class * implements com.bumptech.glide.module.GlideModule -dontwarn com.bumptech.glide.load.resource.bitmap.VideoDecoder ================================================ FILE: app/src/main/AndroidManifest.xml ================================================ ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/ApiKeyActivity.java ================================================ package com.maxwai.nclientv3; import android.content.Intent; import android.net.Uri; import android.os.Bundle; import android.view.MenuItem; import android.view.View; import android.widget.EditText; import android.widget.LinearLayout; import android.widget.ProgressBar; import android.widget.TextView; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.widget.Toolbar; import com.google.android.material.button.MaterialButton; import com.maxwai.nclientv3.components.activities.GeneralActivity; import com.maxwai.nclientv3.settings.AuthStore; import com.maxwai.nclientv3.settings.Global; import com.maxwai.nclientv3.settings.Login; import com.maxwai.nclientv3.utility.LogUtility; import com.maxwai.nclientv3.utility.Utility; import java.io.IOException; import okhttp3.Call; import okhttp3.Callback; import okhttp3.Request; import okhttp3.Response; public class ApiKeyActivity extends GeneralActivity { private TextView statusText; private LinearLayout inputGroup; private EditText apiKeyInput; private ProgressBar progressBar; private MaterialButton openApiKeyPage; private MaterialButton clearApiKey; private MaterialButton validateApiKey; private boolean validationInFlight = false; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_api_key); Toolbar toolbar = findViewById(R.id.toolbar); setSupportActionBar(toolbar); toolbar.setTitle(R.string.title_activity_api_key); assert getSupportActionBar() != null; getSupportActionBar().setDisplayHomeAsUpEnabled(true); getSupportActionBar().setDisplayShowTitleEnabled(true); statusText = findViewById(R.id.api_key_status_text); inputGroup = findViewById(R.id.api_key_input_group); apiKeyInput = findViewById(R.id.api_key_input); progressBar = findViewById(R.id.login_progress); openApiKeyPage = findViewById(R.id.open_api_key_page); clearApiKey = findViewById(R.id.clear_api_key); validateApiKey = findViewById(R.id.validate_api_key); setInputVisible(true); openApiKeyPage.setOnClickListener(v -> openApiKeyPage()); clearApiKey.setOnClickListener(v -> clearSavedApiKey()); validateApiKey.setOnClickListener(v -> validateAndSaveApiKey()); loadSavedApiKey(); } @Override protected void onResume() { super.onResume(); loadSavedApiKey(); } private void openApiKeyPage() { startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(Utility.getBaseUrl() + "user/settings#apikeys"))); } private void updateStatusMessage(@NonNull String message) { statusText.setText(message); } private void setInputVisible(boolean visible) { inputGroup.setVisibility(visible ? View.VISIBLE : View.GONE); } private void setLoading(boolean loading) { validationInFlight = loading; progressBar.setVisibility(loading ? View.VISIBLE : View.GONE); apiKeyInput.setEnabled(!loading); openApiKeyPage.setEnabled(!loading); clearApiKey.setEnabled(!loading && AuthStore.hasApiKey(this)); validateApiKey.setEnabled(!loading); updateStatusMessage(getStatusMessage()); } private void loadSavedApiKey() { String apiKey = AuthStore.getApiKey(this); if (apiKey != null && !validationInFlight) { apiKeyInput.setText(apiKey); } clearApiKey.setEnabled(!validationInFlight && AuthStore.hasApiKey(this)); updateStatusMessage(getStatusMessage()); } @NonNull private String getStatusMessage() { if (AuthStore.hasValidApiKey(this)) return getString(R.string.login_status_api_key_saved); if (AuthStore.hasApiKey(this)) return getString(R.string.login_status_api_key_invalid); return getString(R.string.login_api_key_intro_message); } private void clearSavedApiKey() { AuthStore.clear(this); Login.updateUser(null); apiKeyInput.setText(""); clearApiKey.setEnabled(false); updateStatusMessage(getStatusMessage()); Toast.makeText(this, R.string.login_api_key_removed, Toast.LENGTH_SHORT).show(); } private void validateAndSaveApiKey() { String apiKey = apiKeyInput.getText().toString().trim(); if (apiKey.isEmpty()) { Toast.makeText(this, R.string.login_api_key_empty, Toast.LENGTH_SHORT).show(); return; } setLoading(true); Request request = new Request.Builder() .url(Utility.getApiBaseUrl() + "favorites") .header("Authorization", "Key " + apiKey) .build(); Global.getClient(this).newCall(request).enqueue(new Callback() { @Override public void onFailure(@NonNull Call call, @NonNull IOException e) { LogUtility.e("API key validation failed", e); runOnUiThread(() -> { setLoading(false); Toast.makeText(ApiKeyActivity.this, R.string.unable_to_connect_to_the_site, Toast.LENGTH_SHORT).show(); }); } @Override public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException { try (response) { if (response.isSuccessful()) { AuthStore.saveApiKey(ApiKeyActivity.this, apiKey, true); Login.updateUser(null); runOnUiThread(() -> { setLoading(false); Toast.makeText(ApiKeyActivity.this, R.string.login_api_key_saved, Toast.LENGTH_SHORT).show(); finish(); }); return; } LogUtility.w("API key validation rejected: " + response.code()); runOnUiThread(() -> { setLoading(false); Toast.makeText(ApiKeyActivity.this, R.string.login_api_key_invalid, Toast.LENGTH_SHORT).show(); }); } } }); } @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { if (item.getItemId() == android.R.id.home) finish(); return super.onOptionsItemSelected(item); } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/BookmarkActivity.java ================================================ package com.maxwai.nclientv3; import android.os.Bundle; import android.view.MenuItem; import androidx.appcompat.app.ActionBar; import androidx.appcompat.widget.Toolbar; import androidx.recyclerview.widget.RecyclerView; import com.maxwai.nclientv3.adapters.BookmarkAdapter; import com.maxwai.nclientv3.components.activities.GeneralActivity; import com.maxwai.nclientv3.components.widgets.CustomLinearLayoutManager; import java.util.Objects; public class BookmarkActivity extends GeneralActivity { BookmarkAdapter adapter; RecyclerView recycler; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); //Global.initActivity(this); setContentView(R.layout.activity_bookmark); Toolbar toolbar = findViewById(R.id.toolbar); setSupportActionBar(toolbar); ActionBar actionBar = Objects.requireNonNull(getSupportActionBar()); actionBar.setDisplayHomeAsUpEnabled(true); actionBar.setDisplayShowTitleEnabled(true); actionBar.setTitle(R.string.manage_bookmarks); recycler = findViewById(R.id.recycler); adapter = new BookmarkAdapter(this); recycler.setLayoutManager(new CustomLinearLayoutManager(this)); recycler.setAdapter(adapter); } @Override public boolean onOptionsItemSelected(MenuItem item) { if (item.getItemId() == android.R.id.home) { finish(); return true; } return super.onOptionsItemSelected(item); } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/CommentActivity.java ================================================ package com.maxwai.nclientv3; import android.content.res.Configuration; import android.os.Bundle; import android.util.JsonReader; import android.util.JsonToken; import android.util.JsonWriter; import android.view.MenuItem; import android.view.View; import android.widget.EditText; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.appcompat.app.ActionBar; import androidx.appcompat.widget.Toolbar; import androidx.recyclerview.widget.DividerItemDecoration; import com.maxwai.nclientv3.adapters.CommentAdapter; import com.maxwai.nclientv3.api.comments.Comment; import com.maxwai.nclientv3.api.comments.CommentsFetcher; import com.maxwai.nclientv3.components.activities.BaseActivity; import com.maxwai.nclientv3.settings.Global; import com.maxwai.nclientv3.settings.Login; import com.maxwai.nclientv3.utility.Utility; import java.io.IOException; import java.io.StringWriter; import java.util.Locale; import java.util.Objects; import okhttp3.Call; import okhttp3.Callback; import okhttp3.MediaType; import okhttp3.Request; import okhttp3.RequestBody; import okhttp3.Response; public class CommentActivity extends BaseActivity { private static final int MINIUM_MESSAGE_LENGHT = 10; private CommentAdapter adapter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); //Global.initActivity(this); setContentView(R.layout.activity_comment); Toolbar toolbar = findViewById(R.id.toolbar); setSupportActionBar(toolbar); ActionBar actionBar = Objects.requireNonNull(getSupportActionBar()); actionBar.setDisplayHomeAsUpEnabled(true); actionBar.setDisplayShowTitleEnabled(true); actionBar.setTitle(R.string.comments); findViewById(R.id.page_switcher).setVisibility(View.GONE); int id = getIntent().getIntExtra(getPackageName() + ".GALLERYID", -1); if (id == -1) { finish(); return; } recycler = findViewById(R.id.recycler); refresher = findViewById(R.id.refresher); refresher.setOnRefreshListener(() -> new CommentsFetcher(CommentActivity.this, id).start()); EditText commentText = findViewById(R.id.commentText); findViewById(R.id.card).setVisibility(Login.isLogged() ? View.VISIBLE : View.GONE); findViewById(R.id.sendButton).setOnClickListener(v -> { if (commentText.getText().toString().length() < MINIUM_MESSAGE_LENGHT) { Toast.makeText(this, getString(R.string.minimum_comment_length, MINIUM_MESSAGE_LENGHT), Toast.LENGTH_SHORT).show(); return; } String submitUrl = String.format(Locale.US, Utility.getApiBaseUrl() + "galleries/%d/comments", id); String requestString = createRequestString(commentText.getText().toString()); commentText.setText(""); RequestBody body = RequestBody.create(requestString, MediaType.get("application/json")); Global.getClient(this) .newCall(new Request.Builder().url(submitUrl).post(body).build()) .enqueue(new Callback() { @Override public void onFailure(@NonNull Call call, @NonNull IOException e) { } @Override public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException { try (JsonReader reader = new JsonReader(response.body().charStream())) { Comment comment = new Comment(reader); if (adapter != null) adapter.addComment(comment); } } }); }); // TODO: deactivated feature until fixed findViewById(R.id.sendButton).setClickable(false); findViewById(R.id.sendButton).setEnabled(false); commentText.setEnabled(false); commentText.setHint("Deactivated feature"); // TODO: end changeLayout(getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE); recycler.addItemDecoration(new DividerItemDecoration(this, DividerItemDecoration.VERTICAL)); refresher.setRefreshing(true); new CommentsFetcher(CommentActivity.this, id).start(); } public void setAdapter(CommentAdapter adapter) { this.adapter = adapter; } private String createRequestString(String text) { try (StringWriter writer = new StringWriter(); JsonWriter json = new JsonWriter(writer)) { json.beginObject(); json.name("body").value(text); // TODO: this now needs pow and captcha json.endObject(); return writer.toString(); } catch (IOException ignore) { } return ""; } @Override protected int getPortraitColumnCount() { return 1; } @Override protected int getLandscapeColumnCount() { return 2; } @Override public boolean onOptionsItemSelected(MenuItem item) { if (item.getItemId() == android.R.id.home) { getOnBackPressedDispatcher().onBackPressed(); return true; } return super.onOptionsItemSelected(item); } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/CopyToClipboardActivity.java ================================================ package com.maxwai.nclientv3; import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; import android.net.Uri; import android.os.Bundle; import android.widget.Toast; import com.maxwai.nclientv3.components.activities.GeneralActivity; public class CopyToClipboardActivity extends GeneralActivity { public static void copyTextToClipboard(Context context, String text) { ClipboardManager clipboard = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); ClipData clip = ClipData.newPlainText("text", text); if (clipboard != null) clipboard.setPrimaryClip(clip); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Uri uri = getIntent().getData(); if (uri != null) { copyTextToClipboard(this, uri.toString()); Toast.makeText(this, R.string.link_copied_to_clipboard, Toast.LENGTH_SHORT).show(); } finish(); } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/FavoriteActivity.java ================================================ package com.maxwai.nclientv3; import android.content.Intent; import android.content.res.Configuration; import android.net.Uri; import android.os.Bundle; import android.view.Menu; import android.view.MenuItem; import androidx.annotation.Nullable; import androidx.appcompat.app.ActionBar; import androidx.appcompat.widget.SearchView; import androidx.appcompat.widget.Toolbar; import com.maxwai.nclientv3.adapters.FavoriteAdapter; import com.maxwai.nclientv3.api.components.Gallery; import com.maxwai.nclientv3.async.database.Queries; import com.maxwai.nclientv3.async.downloader.DownloadGalleryV2; import com.maxwai.nclientv3.components.activities.BaseActivity; import com.maxwai.nclientv3.components.views.PageSwitcher; import com.maxwai.nclientv3.settings.Global; import com.maxwai.nclientv3.utility.Utility; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import java.util.Objects; public class FavoriteActivity extends BaseActivity { private static final int ENTRY_PER_PAGE = 24; private FavoriteAdapter adapter = null; private boolean sortByTitle = false; private PageSwitcher pageSwitcher; private SearchView searchView; public static int getEntryPerPage() { return Global.isInfiniteScrollFavorite() ? Integer.MAX_VALUE : ENTRY_PER_PAGE; } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); //Global.initActivity(this); setContentView(R.layout.app_bar_main); Toolbar toolbar = findViewById(R.id.toolbar); setSupportActionBar(toolbar); ActionBar actionBar = Objects.requireNonNull(getSupportActionBar()); actionBar.setDisplayHomeAsUpEnabled(true); actionBar.setDisplayShowTitleEnabled(true); actionBar.setTitle(R.string.favorite_manga); pageSwitcher = findViewById(R.id.page_switcher); recycler = findViewById(R.id.recycler); refresher = findViewById(R.id.refresher); refresher.setRefreshing(true); adapter = new FavoriteAdapter(this); refresher.setOnRefreshListener(adapter::forceReload); changeLayout(getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE); recycler.setAdapter(adapter); pageSwitcher.setPages(1, 1); pageSwitcher.setChanger(new PageSwitcher.DefaultPageChanger() { @Override public void pageChanged() { if (adapter != null) adapter.changePage(); } }); } public int getActualPage() { return pageSwitcher.getActualPage(); } @Override protected int getLandscapeColumnCount() { return Global.getColLandFavorite(); } @Override protected int getPortraitColumnCount() { return Global.getColPortFavorite(); } private int calculatePages(@Nullable String text) { int perPage = getEntryPerPage(); int totalEntries = Queries.FavoriteTable.countFavorite(text); int div = totalEntries / perPage; int mod = totalEntries % perPage; return div + (mod == 0 ? 0 : 1); } @Override protected void onResume() { refresher.setEnabled(true); refresher.setRefreshing(true); String query = searchView == null ? null : searchView.getQuery().toString(); pageSwitcher.setTotalPage(calculatePages(query)); adapter.forceReload(); super.onResume(); } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.main, menu); menu.findItem(R.id.download_page).setVisible(true); menu.findItem(R.id.sort_by_name).setVisible(true); menu.findItem(R.id.by_popular).setVisible(false); menu.findItem(R.id.only_language).setVisible(false); menu.findItem(R.id.add_bookmark).setVisible(false); searchView = (androidx.appcompat.widget.SearchView) menu.findItem(R.id.search).getActionView(); Objects.requireNonNull(searchView).setOnQueryTextListener(new androidx.appcompat.widget.SearchView.OnQueryTextListener() { @Override public boolean onQueryTextSubmit(String query) { return true; } @Override public boolean onQueryTextChange(String newText) { pageSwitcher.setTotalPage(calculatePages(newText)); if (adapter != null) adapter.getFilter().filter(newText); return true; } }); Utility.tintMenu(this, menu); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { Intent i; if (item.getItemId() == R.id.open_browser) { i = new Intent(Intent.ACTION_VIEW, Uri.parse(Utility.getBaseUrl() + "favorites/")); startActivity(i); } else if (item.getItemId() == R.id.download_page) { if (adapter != null) showDialogDownloadAll(); } else if (item.getItemId() == R.id.sort_by_name) { sortByTitle = !sortByTitle; adapter.setSortByTitle(sortByTitle); item.setTitle(sortByTitle ? R.string.sort_by_latest : R.string.sort_by_title); } else if (item.getItemId() == R.id.random_favorite) { adapter.randomGallery(); } return super.onOptionsItemSelected(item); } private void showDialogDownloadAll() { MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this); builder .setTitle(R.string.download_all_galleries_in_this_page) .setIcon(R.drawable.ic_file) .setNegativeButton(R.string.cancel, null) .setPositiveButton(R.string.ok, (dialog, which) -> { for (Gallery g : adapter.getAllGalleries()) DownloadGalleryV2.downloadGallery(this, g); }); builder.show(); } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/GalleryActivity.java ================================================ package com.maxwai.nclientv3; import android.Manifest; import android.content.Intent; import android.content.pm.PackageManager; import android.graphics.Color; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.view.WindowManager; import android.widget.ArrayAdapter; import android.widget.Button; import android.widget.CheckedTextView; import android.widget.EditText; import android.widget.LinearLayout; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.ActionBar; import androidx.appcompat.widget.Toolbar; import com.google.android.material.appbar.CollapsingToolbarLayout; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.snackbar.Snackbar; import com.maxwai.nclientv3.adapters.GalleryAdapter; import com.maxwai.nclientv3.api.InspectorV3; import com.maxwai.nclientv3.api.components.Gallery; import com.maxwai.nclientv3.api.components.GenericGallery; import com.maxwai.nclientv3.async.database.Queries; import com.maxwai.nclientv3.components.activities.BaseActivity; import com.maxwai.nclientv3.components.status.Status; import com.maxwai.nclientv3.components.status.StatusManager; import com.maxwai.nclientv3.components.views.RangeSelector; import com.maxwai.nclientv3.components.widgets.CustomGridLayoutManager; import com.maxwai.nclientv3.settings.AuthStore; import com.maxwai.nclientv3.settings.Favorites; import com.maxwai.nclientv3.settings.Global; import com.maxwai.nclientv3.utility.LogUtility; import com.maxwai.nclientv3.utility.Utility; import java.io.IOException; import java.util.List; import java.util.Locale; import java.util.Objects; import okhttp3.Call; import okhttp3.Callback; import okhttp3.Request; import okhttp3.RequestBody; import okhttp3.Response; import yuku.ambilwarna.AmbilWarnaDialog; public class GalleryActivity extends BaseActivity { @NonNull private GenericGallery gallery = Gallery.emptyGallery(); private boolean isLocal; private GalleryAdapter adapter; private int zoom; private boolean isLocalFavorite; private Toolbar toolbar; private MenuItem onlineFavoriteItem; private String statusString; private int newStatusColor; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); //Global.initActivity(this); setContentView(R.layout.activity_gallery); if (Global.isLockScreen()) getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); toolbar = findViewById(R.id.toolbar); setSupportActionBar(toolbar); ActionBar actionBar = Objects.requireNonNull(getSupportActionBar()); actionBar.setDisplayHomeAsUpEnabled(true); actionBar.setDisplayShowTitleEnabled(true); recycler = findViewById(R.id.recycler); refresher = findViewById(R.id.refresher); masterLayout = findViewById(R.id.master_layout); GenericGallery gal; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { gal = getIntent().getParcelableExtra(getPackageName() + ".GALLERY", GenericGallery.class); } else { gal = getIntent().getParcelableExtra(getPackageName() + ".GALLERY"); } if (gal == null && !tryLoadFromURL()) { finish(); return; } if (gal != null) this.gallery = gal; if (gallery.getType() != GenericGallery.Type.LOCAL) { Queries.HistoryTable.addGallery(((Gallery) gallery).toSimpleGallery()); } LogUtility.d("" + gallery); if (Global.useRtl()) recycler.setRotationY(180); isLocal = getIntent().getBooleanExtra(getPackageName() + ".ISLOCAL", false); zoom = getIntent().getIntExtra(getPackageName() + ".ZOOM", 0); refresher.setEnabled(false); recycler.setLayoutManager(new CustomGridLayoutManager(this, Global.getColumnCount())); loadGallery(gallery, zoom);//if already has gallery } private boolean tryLoadFromURL() { Uri data = getIntent().getData(); if (data != null && data.getPathSegments().size() >= 2) {//if using an URL List params = data.getPathSegments(); LogUtility.d(params.size() + ": " + params); int id; try {//if not an id return id = Integer.parseInt(params.get(1)); } catch (NumberFormatException ignore) { return false; } if (params.size() > 2) {//check if it has a specific page try { zoom = Integer.parseInt(params.get(2)); } catch (NumberFormatException e) { LogUtility.e(e.getLocalizedMessage(), e); zoom = 0; } } InspectorV3.galleryInspector(this, id, new InspectorV3.DefaultInspectorResponse() { @Override public void onSuccess(List galleries) { if (!galleries.isEmpty()) { Intent intent = new Intent(GalleryActivity.this, GalleryActivity.class); intent.putExtra(getPackageName() + ".GALLERY", galleries.get(0)); intent.putExtra(getPackageName() + ".ZOOM", zoom); startActivity(intent); } finish(); } }).start(); return true; } return false; } private void lookup() { CustomGridLayoutManager manager = (CustomGridLayoutManager) recycler.getLayoutManager(); GalleryAdapter adapter = (GalleryAdapter) recycler.getAdapter(); Objects.requireNonNull(manager).setSpanSizeLookup(new CustomGridLayoutManager.SpanSizeLookup() { @Override public int getSpanSize(int position) { return adapter == null ? 0 : (adapter.positionToType(position) == GalleryAdapter.Type.PAGE ? 1 : manager.getSpanCount()); } }); } private void loadGallery(GenericGallery gall, int zoom) { this.gallery = gall; if (getSupportActionBar() != null) { applyTitle(); } adapter = new GalleryAdapter(this, gallery, Global.getColumnCount()); recycler.setAdapter(adapter); lookup(); if (zoom > 0 && Global.getDownloadPolicy() != Global.DataUsageType.NONE) { Intent intent = new Intent(this, ZoomActivity.class); intent.putExtra(getPackageName() + ".GALLERY", this.gallery); intent.putExtra(getPackageName() + ".DIRECTORY", adapter.getDirectory()); intent.putExtra(getPackageName() + ".PAGE", zoom); startActivity(intent); } checkBookmark(); } private void checkBookmark() { int page = Queries.ResumeTable.pageFromId(gallery.getId()); if (page < 0) return; Snackbar snack = Snackbar.make(toolbar, getString(R.string.resume_from_page, page), Snackbar.LENGTH_LONG); //Should be already compensated snack.setAction(R.string.resume, v -> new Thread(() -> { runOnUiThread(() -> recycler.scrollToPosition(page)); if (Global.getColumnCount() != 1) return; Utility.threadSleep(500); runOnUiThread(() -> recycler.scrollToPosition(page)); }).start()); snack.show(); } private void applyTitle() { CollapsingToolbarLayout collapsing = findViewById(R.id.collapsing); ActionBar actionBar = getSupportActionBar(); final String title = gallery.getTitle(); if (collapsing == null || actionBar == null) return; View.OnLongClickListener listener = v -> { CopyToClipboardActivity.copyTextToClipboard(GalleryActivity.this, title); GalleryActivity.this.runOnUiThread( () -> Toast.makeText(GalleryActivity.this, R.string.title_copied_to_clipboard, Toast.LENGTH_SHORT).show() ); return true; }; collapsing.setOnLongClickListener(listener); findViewById(R.id.toolbar).setOnLongClickListener(listener); if (title.length() > 100) { collapsing.setExpandedTitleTextAppearance(android.R.style.TextAppearance_DeviceDefault_Medium); collapsing.setMaxLines(5); } else { collapsing.setExpandedTitleTextAppearance(android.R.style.TextAppearance_DeviceDefault_Large); collapsing.setMaxLines(4); } actionBar.setTitle(title); } @Override protected int getPortraitColumnCount() { return 0; } @Override protected int getLandscapeColumnCount() { return 0; } public void initFavoriteIcon(Menu menu) { boolean onlineFavorite = !isLocal && ((Gallery) gallery).isOnlineFavorite(); boolean unknown = getIntent().getBooleanExtra(getPackageName() + ".UNKNOWN", false); MenuItem item = menu.findItem(R.id.add_online_gallery); item.setIcon(onlineFavorite ? R.drawable.ic_star : R.drawable.ic_star_border); if (unknown) item.setTitle(R.string.toggle_online_favorite); else if (onlineFavorite) item.setTitle(R.string.remove_from_online_favorites); else item.setTitle(R.string.add_to_online_favorite); } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.gallery, menu); isLocalFavorite = Favorites.isFavorite(gallery); menu.findItem(R.id.favorite_manager).setIcon(isLocalFavorite ? R.drawable.ic_favorite : R.drawable.ic_favorite_border); menuItemsVisible(menu); initFavoriteIcon(menu); Utility.tintMenu(this, menu); updateColumnCount(false); return true; } private void menuItemsVisible(Menu menu) { boolean hasValidApiKey = AuthStore.hasValidApiKey(this); boolean isValidOnline = gallery.isValid() && !isLocal; onlineFavoriteItem = menu.findItem(R.id.add_online_gallery); onlineFavoriteItem.setVisible(isValidOnline && hasValidApiKey); menu.findItem(R.id.favorite_manager).setVisible(isValidOnline); menu.findItem(R.id.download_gallery).setVisible(isValidOnline); menu.findItem(R.id.related).setVisible(isValidOnline); menu.findItem(R.id.comments).setVisible(isValidOnline); menu.findItem(R.id.share).setVisible(gallery.isValid()); menu.findItem(R.id.load_internet).setVisible(isLocal && gallery.isValid()); } @Override protected void onResume() { super.onResume(); updateColumnCount(false); supportInvalidateOptionsMenu(); } @Override public boolean onOptionsItemSelected(final MenuItem item) { int id = item.getItemId(); if (id == R.id.download_gallery) { if (Global.hasStoragePermission(this)) new RangeSelector(this, (Gallery) gallery).show(); else requestStorage(); } else if (id == R.id.add_online_gallery) addToFavorite(); else if (id == R.id.change_view) updateColumnCount(true); else if (id == R.id.load_internet) toInternet(); else if (id == R.id.manage_status) updateStatus(); else if (id == R.id.share) Global.shareGallery(this, gallery); else if (id == R.id.comments) { Intent i = new Intent(this, CommentActivity.class); i.putExtra(getPackageName() + ".GALLERYID", gallery.getId()); startActivity(i); } else if (id == R.id.related) { if (recycler.getAdapter() != null) recycler.smoothScrollToPosition(recycler.getAdapter().getItemCount()); } else if (id == R.id.favorite_manager) { if (isLocalFavorite) { Favorites.removeFavorite(gallery); } else { Favorites.addFavorite((Gallery) gallery); } isLocalFavorite = !isLocalFavorite; item.setIcon(isLocalFavorite ? R.drawable.ic_favorite : R.drawable.ic_favorite_border); Global.setTint(this, item.getIcon()); } else if (id == android.R.id.home) { getOnBackPressedDispatcher().onBackPressed(); return true; } return super.onOptionsItemSelected(item); } private void updateStatus() { List statuses = StatusManager.getNames(); MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this); statusString = Queries.StatusMangaTable.getStatus(gallery.getId()).name; ArrayAdapter adapter = new ArrayAdapter<>(this, android.R.layout.select_dialog_singlechoice, statuses) { @NonNull @Override public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { CheckedTextView textView = (CheckedTextView) super.getView(position, convertView, parent); textView.setTextColor(StatusManager.getByName(statuses.get(position)).opaqueColor()); return textView; } }; builder.setSingleChoiceItems(adapter, statuses.indexOf(statusString), (dialog, which) -> statusString = statuses.get(which)); builder .setNeutralButton(R.string.add, (dialog, which) -> createNewStatusDialog()) .setNegativeButton(R.string.remove_status, (dialog, which) -> Queries.StatusMangaTable.remove(gallery.getId())) .setPositiveButton(R.string.ok, (dialog, which) -> Queries.StatusMangaTable.insert(gallery, statusString)) .setTitle(R.string.change_status_title) .show(); } private void createNewStatusDialog() { MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this); LinearLayout layout = (LinearLayout) View.inflate(this, R.layout.dialog_add_status, null); EditText name = layout.findViewById(R.id.name); Button btnColor = layout.findViewById(R.id.color); do { newStatusColor = Utility.RANDOM.nextInt() | 0xff000000; } while (newStatusColor == Color.BLACK || newStatusColor == Color.WHITE); btnColor.setBackgroundColor(newStatusColor); btnColor.setOnClickListener(v -> new AmbilWarnaDialog(GalleryActivity.this, newStatusColor, false, new AmbilWarnaDialog.OnAmbilWarnaListener() { @Override public void onCancel(AmbilWarnaDialog dialog) { } @Override public void onOk(AmbilWarnaDialog dialog, int color) { if (color == Color.WHITE || color == Color.BLACK) { Toast.makeText(GalleryActivity.this, R.string.invalid_color_selected, Toast.LENGTH_SHORT).show(); return; } newStatusColor = color; btnColor.setBackgroundColor(color); } }).show()); builder.setView(layout); builder.setTitle(R.string.create_new_status); builder.setPositiveButton(R.string.ok, (dialog, which) -> { String newName = name.getText().toString(); if (newName.length() < 2) { Toast.makeText(this, R.string.name_too_short, Toast.LENGTH_SHORT).show(); return; } if (StatusManager.getByName(newName) != null) { Toast.makeText(this, R.string.duplicated_name, Toast.LENGTH_SHORT).show(); return; } Status status = StatusManager.add(name.getText().toString(), newStatusColor); Queries.StatusMangaTable.insert(gallery, status); }); builder.setNegativeButton(R.string.cancel, (dialog, which) -> updateStatus()); builder.setOnCancelListener(dialog -> updateStatus()); builder.show(); } private void updateIcon(boolean nowIsFavorite) { GalleryActivity.this.runOnUiThread(() -> { onlineFavoriteItem.setIcon(!nowIsFavorite ? R.drawable.ic_star_border : R.drawable.ic_star); onlineFavoriteItem.setTitle(!nowIsFavorite ? R.string.add_to_online_favorite : R.string.remove_from_online_favorites); }); } private void addToFavorite() { boolean wasFavorite = Objects.equals(onlineFavoriteItem.getTitle(), getString(R.string.remove_from_online_favorites)); String url = String.format(Locale.US, Utility.getApiBaseUrl() + "galleries/%d/favorite", gallery.getId()); LogUtility.d("Calling: " + url); Global.getClient(this) .newCall(new Request.Builder().url(url).method(wasFavorite ? "DELETE" : "POST", RequestBody.EMPTY).build()) .enqueue(new Callback() { @Override public void onFailure(@NonNull Call call, @NonNull IOException e) { } @Override public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException { String responseString = response.body().string(); boolean nowIsFavorite = responseString.contains("true"); updateIcon(nowIsFavorite); } }); } private void updateColumnCount(boolean increase) { int x = Global.getColumnCount(); CustomGridLayoutManager manager = (CustomGridLayoutManager) recycler.getLayoutManager(); if (manager == null) return; MenuItem item = ((Toolbar) findViewById(R.id.toolbar)).getMenu().findItem(R.id.change_view); if (increase || manager.getSpanCount() != x) { if (increase) x = x % 4 + 1; int pos = manager.findFirstVisibleItemPosition(); Global.updateColumnCount(this, x); recycler.setLayoutManager(new CustomGridLayoutManager(this, x)); LogUtility.d("Span count: " + manager.getSpanCount()); if (adapter != null) { adapter.setColCount(Global.getColumnCount()); recycler.setAdapter(adapter); lookup(); recycler.scrollToPosition(pos); } } if (item != null) { switch (x) { case 1: item.setIcon(R.drawable.ic_view_1); break; case 2: item.setIcon(R.drawable.ic_view_2); break; case 3: item.setIcon(R.drawable.ic_view_3); break; case 4: item.setIcon(R.drawable.ic_view_4); break; } Global.setTint(this, item.getIcon()); } } private void toInternet() { refresher.setEnabled(true); InspectorV3.galleryInspector(this, gallery.getId(), new InspectorV3.DefaultInspectorResponse() { @Override public void onSuccess(List galleries) { if (galleries.isEmpty()) return; Intent intent = new Intent(GalleryActivity.this, GalleryActivity.class); LogUtility.d(galleries.get(0).toString()); intent.putExtra(getPackageName() + ".GALLERY", galleries.get(0)); runOnUiThread(() -> startActivity(intent)); } }).start(); } private void requestStorage() { requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE}, 1); } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); Global.initStorage(this); if (requestCode == 1 && grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) new RangeSelector(this, (Gallery) gallery).show(); } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/HistoryActivity.java ================================================ package com.maxwai.nclientv3; import android.content.res.Configuration; import android.os.Bundle; import android.view.Menu; import android.view.MenuItem; import androidx.appcompat.app.ActionBar; import androidx.appcompat.widget.Toolbar; import com.maxwai.nclientv3.adapters.ListAdapter; import com.maxwai.nclientv3.async.database.Queries; import com.maxwai.nclientv3.components.activities.BaseActivity; import com.maxwai.nclientv3.settings.Global; import com.maxwai.nclientv3.utility.Utility; import java.util.ArrayList; import java.util.Objects; public class HistoryActivity extends BaseActivity { ListAdapter adapter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); //Global.initActivity(this); setContentView(R.layout.activity_bookmark); Toolbar toolbar = findViewById(R.id.toolbar); setSupportActionBar(toolbar); ActionBar actionBar = Objects.requireNonNull(getSupportActionBar()); actionBar.setDisplayHomeAsUpEnabled(true); actionBar.setDisplayShowTitleEnabled(true); actionBar.setTitle(R.string.history); recycler = findViewById(R.id.recycler); masterLayout = findViewById(R.id.master_layout); adapter = new ListAdapter(this); adapter.addGalleries(new ArrayList<>(Queries.HistoryTable.getHistory())); changeLayout(getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE); recycler.setAdapter(adapter); } @Override public boolean onOptionsItemSelected(MenuItem item) { if (item.getItemId() == android.R.id.home) { finish(); return true; } else if (item.getItemId() == R.id.cancelAll) { Queries.HistoryTable.emptyHistory(); adapter.restartDataset(new ArrayList<>(1)); return true; } return super.onOptionsItemSelected(item); } @Override protected int getPortraitColumnCount() { return Global.getColPortHistory(); } @Override protected int getLandscapeColumnCount() { return Global.getColLandHistory(); } @Override public boolean onCreateOptionsMenu(Menu menu) { super.onCreateOptionsMenu(menu); getMenuInflater().inflate(R.menu.history, menu); Utility.tintMenu(this, menu); return true; } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/LocalActivity.java ================================================ package com.maxwai.nclientv3; import android.content.res.Configuration; import android.os.Bundle; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.widget.ArrayAdapter; import android.widget.LinearLayout; import androidx.activity.OnBackPressedCallback; import androidx.appcompat.app.ActionBar; import androidx.appcompat.widget.Toolbar; import com.maxwai.nclientv3.adapters.LocalAdapter; import com.maxwai.nclientv3.api.local.FakeInspector; import com.maxwai.nclientv3.api.local.LocalGallery; import com.maxwai.nclientv3.api.local.LocalSortType; import com.maxwai.nclientv3.async.converters.CreatePdfOrZip; import com.maxwai.nclientv3.async.downloader.GalleryDownloaderV2; import com.maxwai.nclientv3.components.activities.BaseActivity; import com.maxwai.nclientv3.components.classes.MultichoiceAdapter; import com.maxwai.nclientv3.settings.Global; import com.maxwai.nclientv3.utility.Utility; import com.google.android.material.chip.ChipGroup; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.switchmaterial.SwitchMaterial; import java.io.File; import java.util.List; import java.util.Objects; public class LocalActivity extends BaseActivity { private Menu optionMenu; private LocalAdapter adapter; private final MultichoiceAdapter.MultichoiceListener listener = new MultichoiceAdapter.DefaultMultichoiceListener() { @Override public void choiceChanged() { setMenuVisibility(optionMenu); } }; private Toolbar toolbar; private int colCount; private int idGalleryPosition = -1; private File folder = Global.MAINFOLDER; private androidx.appcompat.widget.SearchView searchView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); //Global.initActivity(this); setContentView(R.layout.app_bar_main); toolbar = findViewById(R.id.toolbar); setSupportActionBar(toolbar); ActionBar actionBar = Objects.requireNonNull(getSupportActionBar()); actionBar.setDisplayHomeAsUpEnabled(true); actionBar.setDisplayShowTitleEnabled(true); actionBar.setTitle(R.string.downloaded_manga); findViewById(R.id.page_switcher).setVisibility(View.GONE); recycler = findViewById(R.id.recycler); refresher = findViewById(R.id.refresher); refresher.setOnRefreshListener(() -> new FakeInspector(this, folder).execute(this)); changeLayout(getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE); new FakeInspector(this, folder).execute(this); getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) { @Override public void handleOnBackPressed() { if (adapter != null && adapter.getMode() == MultichoiceAdapter.Mode.SELECTING) adapter.deselectAll(); else { setEnabled(false); getOnBackPressedDispatcher().onBackPressed(); setEnabled(true); } } }); } public void setAdapter(LocalAdapter adapter) { this.adapter = adapter; this.adapter.addListener(listener); runOnUiThread(() -> recycler.setAdapter(adapter)); } public void setIdGalleryPosition(int idGalleryPosition) { this.idGalleryPosition = idGalleryPosition; } @Override public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu; this adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.download, menu); getMenuInflater().inflate(R.menu.local_multichoice, menu); this.optionMenu = menu; setMenuVisibility(menu); searchView = (androidx.appcompat.widget.SearchView) menu.findItem(R.id.search).getActionView(); Objects.requireNonNull(searchView).setOnQueryTextListener(new androidx.appcompat.widget.SearchView.OnQueryTextListener() { @Override public boolean onQueryTextSubmit(String query) { return true; } @Override public boolean onQueryTextChange(String newText) { if (recycler.getAdapter() != null) ((LocalAdapter) recycler.getAdapter()).getFilter().filter(newText); return true; } }); Utility.tintMenu(this, menu); return true; } private void setMenuVisibility(Menu menu) { if (menu == null) return; MultichoiceAdapter.Mode mode = adapter == null ? MultichoiceAdapter.Mode.NORMAL : adapter.getMode(); boolean hasGallery = false; boolean hasDownloads = false; if (mode == MultichoiceAdapter.Mode.SELECTING) { hasGallery = adapter.hasSelectedClass(LocalGallery.class); hasDownloads = adapter.hasSelectedClass(GalleryDownloaderV2.class); } menu.findItem(R.id.search).setVisible(mode == MultichoiceAdapter.Mode.NORMAL); menu.findItem(R.id.sort_by_name).setVisible(mode == MultichoiceAdapter.Mode.NORMAL); menu.findItem(R.id.folder_choose).setVisible(mode == MultichoiceAdapter.Mode.NORMAL && Global.getUsableFolders(this).size() > 1); menu.findItem(R.id.random_favorite).setVisible(mode == MultichoiceAdapter.Mode.NORMAL); menu.findItem(R.id.delete_all).setVisible(mode == MultichoiceAdapter.Mode.SELECTING); menu.findItem(R.id.select_all).setVisible(mode == MultichoiceAdapter.Mode.SELECTING); menu.findItem(R.id.pause_all).setVisible(mode == MultichoiceAdapter.Mode.SELECTING && !hasGallery && hasDownloads); menu.findItem(R.id.start_all).setVisible(mode == MultichoiceAdapter.Mode.SELECTING && !hasGallery && hasDownloads); menu.findItem(R.id.pdf_all).setVisible(mode == MultichoiceAdapter.Mode.SELECTING && hasGallery && !hasDownloads && CreatePdfOrZip.hasPDFCapabilities()); menu.findItem(R.id.zip_all).setVisible(mode == MultichoiceAdapter.Mode.SELECTING && hasGallery && !hasDownloads); } @Override protected void onDestroy() { if (adapter != null) adapter.removeObserver(); super.onDestroy(); } @Override protected void changeLayout(boolean landscape) { colCount = (landscape ? getLandscapeColumnCount() : getPortraitColumnCount()); if (adapter != null) adapter.setColCount(colCount); super.changeLayout(landscape); } public int getColCount() { return colCount; } @Override protected void onResume() { super.onResume(); if (idGalleryPosition != -1) { adapter.updateColor(idGalleryPosition); idGalleryPosition = -1; } } public boolean onOptionsItemSelected(MenuItem item) { if (item.getItemId() == android.R.id.home) { getOnBackPressedDispatcher().onBackPressed(); return true; } else if (item.getItemId() == R.id.pause_all) { adapter.pauseSelected(); } else if (item.getItemId() == R.id.start_all) { adapter.startSelected(); } else if (item.getItemId() == R.id.delete_all) { adapter.deleteSelected(); } else if (item.getItemId() == R.id.pdf_all) { adapter.pdfSelected(); } else if (item.getItemId() == R.id.zip_all) { adapter.zipSelected(); } else if (item.getItemId() == R.id.select_all) { adapter.selectAll(); } else if (item.getItemId() == R.id.folder_choose) { showDialogFolderChoose(); } else if (item.getItemId() == R.id.random_favorite) { if (adapter != null) adapter.viewRandom(); } else if (item.getItemId() == R.id.sort_by_name) { dialogSortType(); } return super.onOptionsItemSelected(item); } private void showDialogFolderChoose() { List strings = Global.getUsableFolders(this); ArrayAdapter adapter = new ArrayAdapter<>(this, android.R.layout.select_dialog_singlechoice, strings); MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this); builder.setTitle(R.string.choose_directory).setIcon(R.drawable.ic_folder); builder.setAdapter(adapter, (dialog, which) -> { folder = new File(strings.get(which), "NClientV3"); new FakeInspector(this, folder).execute(this); }).setNegativeButton(R.string.cancel, null).show(); } private void dialogSortType() { LocalSortType sortType = Global.getLocalSortType(); MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this); LinearLayout view = (LinearLayout) LayoutInflater.from(this).inflate(R.layout.local_sort_type, toolbar, false); ChipGroup group = view.findViewById(R.id.chip_group); SwitchMaterial switchMaterial = view.findViewById(R.id.ascending); group.check(group.getChildAt(sortType.type.ordinal()).getId()); switchMaterial.setChecked(sortType.descending); builder.setView(view); builder.setPositiveButton(R.string.ok, (dialog, which) -> { int typeSelectedIndex = group.indexOfChild(group.findViewById(group.getCheckedChipId())); LocalSortType.Type typeSelected = LocalSortType.Type.values()[typeSelectedIndex]; boolean descending = switchMaterial.isChecked(); LocalSortType newSortType = new LocalSortType(typeSelected, descending); if (sortType.equals(newSortType)) return; Global.setLocalSortType(LocalActivity.this, newSortType); if (adapter != null) adapter.sortChanged(); }) .setNeutralButton(R.string.cancel, null) .setTitle(R.string.sort_select_type) .show(); /* boolean sortByName=Global.isLocalSortByName(); item.setIcon(sortByName?R.drawable.ic_sort_by_alpha:R.drawable.ic_access_time); item.setTitle(sortByName?R.string.sort_by_title:R.string.sort_by_latest); Global.setTint(item.getIcon());*/ } @Override protected int getPortraitColumnCount() { return Global.getColPortDownload(); } @Override protected int getLandscapeColumnCount() { return Global.getColLandDownload(); } public String getQuery() { if (searchView == null) return ""; CharSequence query = searchView.getQuery(); return query == null ? "" : query.toString(); } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/MainActivity.java ================================================ package com.maxwai.nclientv3; import android.Manifest; import android.annotation.SuppressLint; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.content.res.Configuration; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.widget.AdapterView; import android.widget.ArrayAdapter; import androidx.activity.OnBackPressedCallback; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.StringRes; import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.ActionBarDrawerToggle; import androidx.appcompat.app.AppCompatDelegate; import androidx.appcompat.widget.SearchView; import androidx.appcompat.widget.Toolbar; import androidx.core.os.LocaleListCompat; import androidx.core.view.GravityCompat; import androidx.drawerlayout.widget.DrawerLayout; import androidx.recyclerview.widget.RecyclerView; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.navigation.NavigationView; import com.google.android.material.snackbar.Snackbar; import com.maxwai.nclientv3.adapters.ListAdapter; import com.maxwai.nclientv3.api.InspectorV3; import com.maxwai.nclientv3.api.components.Gallery; import com.maxwai.nclientv3.api.components.GenericGallery; import com.maxwai.nclientv3.api.components.Ranges; import com.maxwai.nclientv3.api.components.Tag; import com.maxwai.nclientv3.api.enums.ApiRequestType; import com.maxwai.nclientv3.api.enums.Language; import com.maxwai.nclientv3.api.enums.SortType; import com.maxwai.nclientv3.api.enums.SpecialTagIds; import com.maxwai.nclientv3.api.enums.TagStatus; import com.maxwai.nclientv3.api.enums.TagType; import com.maxwai.nclientv3.async.ScrapeTags; import com.maxwai.nclientv3.async.VersionChecker; import com.maxwai.nclientv3.async.database.Queries; import com.maxwai.nclientv3.async.downloader.DownloadGalleryV2; import com.maxwai.nclientv3.components.activities.BaseActivity; import com.maxwai.nclientv3.components.views.PageSwitcher; import com.maxwai.nclientv3.components.widgets.CustomGridLayoutManager; import com.maxwai.nclientv3.settings.AuthStore; import com.maxwai.nclientv3.settings.Global; import com.maxwai.nclientv3.settings.Login; import com.maxwai.nclientv3.settings.TagV2; import com.maxwai.nclientv3.utility.LogUtility; import com.maxwai.nclientv3.utility.Utility; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Objects; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; public class MainActivity extends BaseActivity implements NavigationView.OnNavigationItemSelectedListener { private static final int CHANGE_LANGUAGE_DELAY = 1000; private static boolean firstTime = true;//true only when app starting private final InspectorV3.InspectorResponse startGallery = new MainInspectorResponse() { @Override public void onSuccess(List galleries) { Gallery g = galleries.size() == 1 ? (Gallery) galleries.get(0) : Gallery.emptyGallery(); Intent intent = new Intent(MainActivity.this, GalleryActivity.class); LogUtility.d(g.toString()); intent.putExtra(getPackageName() + ".GALLERY", g); runOnUiThread(() -> { startActivity(intent); finish(); }); LogUtility.d("STARTED"); } }; private final Handler changeLanguageTimeHandler = new Handler(Objects.requireNonNull(Looper.myLooper())); public ListAdapter adapter; private final InspectorV3.InspectorResponse addDataset = new MainInspectorResponse() { @Override public void onSuccess(List galleries) { adapter.addGalleries(galleries); } }; //views public MenuItem onlineFavoriteManager; private InspectorV3 inspector = null; private NavigationView navigationView; private ModeType modeType = ModeType.UNKNOWN; private int idOpenedGallery = -1;//Position in the recycler of the opened gallery private boolean inspecting = false, filteringTag = false; private SortType temporaryType; private Snackbar snackbar = null; private PageSwitcher pageSwitcher; private final InspectorV3.InspectorResponse resetDataset = new MainInspectorResponse() { @Override public void onSuccess(List galleries) { super.onSuccess(galleries); adapter.restartDataset(galleries); showPageSwitcher(inspector.getPage(), inspector.getPageCount()); runOnUiThread(() -> recycler.smoothScrollToPosition(0)); } }; final Runnable changeLanguageRunnable = () -> { useNormalMode(); inspector.start(); }; private DrawerLayout drawerLayout; private Toolbar toolbar; private boolean setting = false; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); //load inspector selectStartMode(getIntent(), getPackageName()); LogUtility.d("Main started with mode " + modeType); //init views and actions findUsefulViews(); initializeToolbar(); initializeNavigationView(); initializeRecyclerView(); initializePageSwitcherActions(); refresher.setOnRefreshListener(() -> { inspector = inspector.cloneInspector(MainActivity.this, resetDataset); if (Global.isInfiniteScrollMain()) inspector.setPage(1); inspector.start(); }); manageDrawer(); setActivityTitle(); if (firstTime) checkUpdate(); if (inspector != null) { inspector.start(); } else { LogUtility.e(getIntent().getExtras()); } getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) { @Override public void handleOnBackPressed() { if (drawerLayout.isDrawerOpen(GravityCompat.START)) drawerLayout.closeDrawer(GravityCompat.START); else { setEnabled(false); getOnBackPressedDispatcher().onBackPressed(); setEnabled(true); } } }); } private void manageDrawer() { if (modeType != ModeType.NORMAL) { drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED); } else { ActionBarDrawerToggle toggle = new ActionBarDrawerToggle(this, drawerLayout, toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close); drawerLayout.addDrawerListener(toggle); toggle.syncState(); } } private void setActivityTitle() { ActionBar actionBar = Objects.requireNonNull(getSupportActionBar()); switch (modeType) { case FAVORITE: actionBar.setTitle(R.string.favorite_online_manga); break; case SEARCH: actionBar.setTitle(inspector.getSearchTitle()); break; case TAG: actionBar.setTitle(inspector.getTag().getName()); break; case NORMAL: actionBar.setTitle(R.string.app_name); break; default: actionBar.setTitle("WTF"); break; } } private void initializeToolbar() { setSupportActionBar(toolbar); ActionBar bar = getSupportActionBar(); assert bar != null; bar.setDisplayShowTitleEnabled(true); bar.setTitle(R.string.app_name); } private void initializePageSwitcherActions() { pageSwitcher.setChanger(new PageSwitcher.DefaultPageChanger() { @Override public void pageChanged() { inspector = inspector.cloneInspector(MainActivity.this, resetDataset); inspector.setPage(pageSwitcher.getActualPage()); inspector.start(); } }); } private void initializeRecyclerView() { adapter = new ListAdapter(this); recycler.setAdapter(adapter); recycler.setHasFixedSize(true); //recycler.setItemViewCacheSize(24); recycler.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { if (inspecting) return; if (!Global.isInfiniteScrollMain()) return; if (refresher.isRefreshing()) return; CustomGridLayoutManager manager = (CustomGridLayoutManager) recycler.getLayoutManager(); assert manager != null; if (!pageSwitcher.lastPageReached() && lastGalleryReached(manager)) { inspecting = true; inspector = inspector.cloneInspector(MainActivity.this, addDataset); inspector.setPage(inspector.getPage() + 1); inspector.start(); } } }); changeLayout(getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE); } /** * Check if the last gallery has been shown **/ private boolean lastGalleryReached(CustomGridLayoutManager manager) { return recycler.getAdapter() != null && manager.findLastVisibleItemPosition() >= (recycler.getAdapter().getItemCount() - 1 - manager.getSpanCount()); } private void initializeNavigationView() { toolbar.setNavigationIcon(R.drawable.ic_arrow_back); toolbar.setNavigationOnClickListener(v -> finish()); navigationView.setNavigationItemSelectedListener(this); onlineFavoriteManager.setVisible(AuthStore.hasValidApiKey(this)); } public void setIdOpenedGallery(int idOpenedGallery) { this.idOpenedGallery = idOpenedGallery; } private void findUsefulViews() { masterLayout = findViewById(R.id.master_layout); toolbar = findViewById(R.id.toolbar); navigationView = findViewById(R.id.nav_view); recycler = findViewById(R.id.recycler); refresher = findViewById(R.id.refresher); pageSwitcher = findViewById(R.id.page_switcher); drawerLayout = findViewById(R.id.drawer_layout); onlineFavoriteManager = navigationView.getMenu().findItem(R.id.online_favorite_manager); } private void hideError() { //errorText.setVisibility(View.GONE); runOnUiThread(() -> { if (snackbar != null && snackbar.isShown()) { snackbar.dismiss(); snackbar = null; } }); } private void showError(@Nullable String text, @Nullable View.OnClickListener listener) { if (text == null) { hideError(); return; } if (listener == null) { snackbar = Snackbar.make(masterLayout, text, Snackbar.LENGTH_SHORT); } else { snackbar = Snackbar.make(masterLayout, text, Snackbar.LENGTH_INDEFINITE); snackbar.setAction(R.string.retry, listener); } snackbar.show(); } private void showError(@StringRes int text, View.OnClickListener listener) { showError(getString(text), listener); } private void checkUpdate() { if (Global.shouldCheckForUpdates(this)) new VersionChecker(this, true); ScrapeTags.startWork(this); firstTime = false; } private void selectStartMode(Intent intent, String packageName) { Uri data = intent.getData(); if (intent.getBooleanExtra(packageName + ".ISBYTAG", false)) useTagMode(intent, packageName); else if (intent.getBooleanExtra(packageName + ".SEARCHMODE", false)) useSearchMode(intent, packageName); else if (intent.getBooleanExtra(packageName + ".FAVORITE", false)) useFavoriteMode(1); else if (intent.getBooleanExtra(packageName + ".BYBOOKMARK", false)) useBookmarkMode(intent, packageName); else if (data != null) manageDataStart(data); else useNormalMode(); } private void useNormalMode() { inspector = InspectorV3.basicInspector(this, 1, resetDataset); modeType = ModeType.NORMAL; } private void useBookmarkMode(Intent intent, String packageName) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { inspector = intent.getParcelableExtra(packageName + ".INSPECTOR", InspectorV3.class); } else { inspector = intent.getParcelableExtra(packageName + ".INSPECTOR"); } assert inspector != null; inspector.initialize(this, resetDataset); modeType = ModeType.BOOKMARK; ApiRequestType type = inspector.getRequestType(); if (type == ApiRequestType.BYTAG) modeType = ModeType.TAG; else if (type == ApiRequestType.BYALL) modeType = ModeType.NORMAL; else if (type == ApiRequestType.BYSEARCH) modeType = ModeType.SEARCH; else if (type == ApiRequestType.FAVORITE) modeType = ModeType.FAVORITE; } private void useFavoriteMode(int page) { //instantiateWebView(); inspector = InspectorV3.favoriteInspector(this, null, page, resetDataset); modeType = ModeType.FAVORITE; } private void useSearchMode(Intent intent, String packageName) { String query = intent.getStringExtra(packageName + ".QUERY"); boolean ok = tryOpenId(query); if (!ok) createSearchInspector(intent, packageName, query); } private void createSearchInspector(Intent intent, String packageName, String query) { boolean advanced = intent.getBooleanExtra(packageName + ".ADVANCED", false); ArrayList tagArrayList = intent.getParcelableArrayListExtra(packageName + ".TAGS"); Ranges ranges; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { ranges = intent.getParcelableExtra(getPackageName() + ".RANGES", Ranges.class); } else { ranges = intent.getParcelableExtra(getPackageName() + ".RANGES"); } HashSet tags = null; query = query.trim(); if (advanced) { assert tagArrayList != null;//tags is always not null when advanced is set tags = new HashSet<>(tagArrayList); } inspector = InspectorV3.searchInspector(this, query, tags, 1, Global.getSortType(), ranges, resetDataset); modeType = ModeType.SEARCH; } private boolean tryOpenId(String query) { try { int id = Integer.parseInt(query); inspector = InspectorV3.galleryInspector(this, id, startGallery); modeType = ModeType.ID; return true; } catch (NumberFormatException ignore) { } return false; } private void useTagMode(Intent intent, String packageName) { Tag t; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { t = intent.getParcelableExtra(packageName + ".TAG", Tag.class); } else { t = intent.getParcelableExtra(packageName + ".TAG"); } inspector = InspectorV3.tagInspector(this, t, 1, Global.getSortType(), resetDataset); modeType = ModeType.TAG; } /** * Load inspector from an URL, it can be either a tag or a search */ private void manageDataStart(Uri data) { List datas = data.getPathSegments(); TagType dataType; LogUtility.d("Datas: " + datas); if (datas.isEmpty()) { useNormalMode(); return; } dataType = TagType.typeByName(datas.get(0)); if (dataType != TagType.UNKNOWN) useDataTagMode(datas, dataType); else useDataSearchMode(data, datas); } private void useDataSearchMode(Uri data, List datas) { String query = data.getQueryParameter("q"); String pageParam = data.getQueryParameter("page"); boolean favorite = "favorites".equals(datas.get(0)); SortType type = SortType.findFromAddition(data.getQueryParameter("sort")); int page = 1; if (pageParam != null) page = Integer.parseInt(pageParam); if (favorite) { if (AuthStore.hasValidApiKey(this)) useFavoriteMode(page); else { Intent intent = new Intent(this, FavoriteActivity.class); startActivity(intent); finish(); } return; } inspector = InspectorV3.searchInspector(this, query, null, page, type, null, resetDataset); modeType = ModeType.SEARCH; } private void useDataTagMode(List datas, TagType type) { String query = datas.get(1); Tag tag = Queries.TagTable.getTagFromTagName(query); if (tag == null) tag = new Tag(query, -1, SpecialTagIds.INVALID_ID, type, TagStatus.DEFAULT); SortType sortType = SortType.RECENT_ALL_TIME; if (datas.size() == 3) { sortType = SortType.findFromAddition(datas.get(2)); } inspector = InspectorV3.tagInspector(this, tag, 1, sortType, resetDataset); modeType = ModeType.TAG; } public void hidePageSwitcher() { runOnUiThread(() -> pageSwitcher.setVisibility(View.GONE)); } public void showPageSwitcher(final int actualPage, final int totalPage) { pageSwitcher.setPages(totalPage, actualPage); if (Global.isInfiniteScrollMain()) { hidePageSwitcher(); } } @SuppressLint("NotifyDataSetChanged") @Override protected void onResume() { super.onResume(); Login.initLogin(this); if (idOpenedGallery != -1) { adapter.updateColor(idOpenedGallery); idOpenedGallery = -1; } onlineFavoriteManager.setVisible(AuthStore.hasValidApiKey(this)); SharedPreferences settings = getSharedPreferences("Settings", 0); LocaleListCompat setLocaleList = AppCompatDelegate.getApplicationLocales(); settings.edit().putString(getString(R.string.preference_key_language), setLocaleList.isEmpty() ? getString(R.string.key_default_value) : Objects.requireNonNull(setLocaleList.get(0)).toLanguageTag()).apply(); if (setting) { Global.initFromShared(this);//restart all settings inspector = inspector.cloneInspector(this, resetDataset); inspector.start();//restart inspector adapter.notifyDataSetChanged();//restart adapter adapter.resetStatuses(); showPageSwitcher(inspector.getPage(), inspector.getPageCount());//restart page switcher changeLayout(getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE); setting = false; } else if (filteringTag) { inspector = InspectorV3.basicInspector(this, 1, resetDataset); inspector.start(); filteringTag = false; } invalidateOptionsMenu(); } @Override public void onConfigurationChanged(@NonNull Configuration newConfig) { super.onConfigurationChanged(newConfig); } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.main, menu); popularItemDispay(menu.findItem(R.id.by_popular)); showLanguageIcon(menu.findItem(R.id.only_language)); menu.findItem(R.id.only_language).setVisible(modeType == ModeType.NORMAL); menu.findItem(R.id.random_favorite).setVisible(modeType == ModeType.FAVORITE); initializeSearchItem(menu.findItem(R.id.search)); if (modeType == ModeType.TAG) { MenuItem item = menu.findItem(R.id.tag_manager); item.setVisible(inspector.getTag().getId() > 0); TagStatus ts = inspector.getTag().getStatus(); updateTagStatus(item, ts); } Utility.tintMenu(this, menu); return true; } private void initializeSearchItem(MenuItem item) { if (modeType != ModeType.FAVORITE) item.setActionView(null); else { ((SearchView) Objects.requireNonNull(item.getActionView())).setOnQueryTextListener(new SearchView.OnQueryTextListener() { @Override public boolean onQueryTextSubmit(String query) { inspector = InspectorV3.favoriteInspector(MainActivity.this, query, 1, resetDataset); inspector.start(); Objects.requireNonNull(getSupportActionBar()).setTitle(query); return true; } @Override public boolean onQueryTextChange(String newText) { return false; } }); } } private void popularItemDispay(MenuItem item) { item.setTitle(getString(R.string.sort_type_title_format, getString(Global.getSortType().getNameId()))); Global.setTint(this, item.getIcon()); } private void showLanguageIcon(MenuItem item) { switch (Global.getOnlyLanguage()) { case JAPANESE: item.setTitle(R.string.only_japanese); item.setIcon(R.drawable.ic_jpbw); break; case CHINESE: item.setTitle(R.string.only_chinese); item.setIcon(R.drawable.ic_cnbw); break; case ENGLISH: item.setTitle(R.string.only_english); item.setIcon(R.drawable.ic_gbbw); break; case ALL: item.setTitle(R.string.all_languages); item.setIcon(R.drawable.ic_world); break; } Global.setTint(this, item.getIcon()); } @Override protected int getPortraitColumnCount() { return Global.getColPortMain(); } @Override protected int getLandscapeColumnCount() { return Global.getColLandMain(); } @Override public boolean onOptionsItemSelected(MenuItem item) { Intent i; LogUtility.d("Pressed item: " + item.getItemId()); if (item.getItemId() == R.id.by_popular) { updateSortType(item); } else if (item.getItemId() == R.id.only_language) { updateLanguageAndIcon(item); } else if (item.getItemId() == R.id.search) { if (modeType != ModeType.FAVORITE) {//show textbox or start search activity i = new Intent(this, SearchActivity.class); startActivity(i); } } else if (item.getItemId() == R.id.open_browser) { if (inspector != null) { i = new Intent(Intent.ACTION_VIEW, Uri.parse(inspector.getUrl())); startActivity(i); } } else if (item.getItemId() == R.id.random_favorite) { inspector = InspectorV3.randomInspector(this, startGallery, true); inspector.start(); } else if (item.getItemId() == R.id.download_page) { if (inspector.getGalleries() != null) showDialogDownloadAll(); } else if (item.getItemId() == R.id.add_bookmark) { Queries.BookmarkTable.addBookmark(inspector); } else if (item.getItemId() == R.id.tag_manager) { TagStatus ts = TagV2.updateStatus(inspector.getTag()); updateTagStatus(item, ts); } else if (item.getItemId() == android.R.id.home) { finish(); } return super.onOptionsItemSelected(item); } private void updateSortType(MenuItem item) { ArrayAdapter adapter = new ArrayAdapter<>(this, android.R.layout.select_dialog_singlechoice); MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this); for (SortType type : SortType.values()) adapter.add(getString(type.getNameId())); temporaryType = Global.getSortType(); builder.setIcon(R.drawable.ic_sort).setTitle(R.string.sort_select_type); builder.setSingleChoiceItems(adapter, temporaryType.ordinal(), (dialog, which) -> temporaryType = SortType.values()[which]); builder.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { @Override public void onItemSelected(AdapterView parent, View view, int position, long id) { temporaryType = SortType.values()[position]; parent.setSelection(position); } @Override public void onNothingSelected(AdapterView parent) { } }); builder.setPositiveButton(R.string.ok, (dialog, which) -> { Global.updateSortType(MainActivity.this, temporaryType); popularItemDispay(item); inspector = inspector.cloneInspector(MainActivity.this, resetDataset); inspector.setSortType(temporaryType); inspector.start(); }); builder.setNegativeButton(R.string.cancel, null); builder.show(); } private void updateLanguageAndIcon(MenuItem item) { ArrayAdapter adapter = new ArrayAdapter<>(this, android.R.layout.select_dialog_singlechoice); MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this); adapter.addAll(Arrays.stream(Language.getFilteredValuesArray()) .map(lang -> getString(lang.getNameResId())) .collect(Collectors.toList()) ); AtomicReference selectedLanguage = new AtomicReference<>(Global.getOnlyLanguage()); builder.setIcon(R.drawable.ic_world) .setTitle(R.string.change_language) .setSingleChoiceItems(adapter, selectedLanguage.get().ordinal(), (dialog, which) -> selectedLanguage.set(Language.getFilteredValuesArray()[which])) .setPositiveButton(R.string.ok, (dialog, which) -> { Global.updateOnlyLanguage(MainActivity.this, Language.valueOf(selectedLanguage.get().name())); // wait 250ms to reduce the requests changeLanguageTimeHandler.removeCallbacks(changeLanguageRunnable); changeLanguageTimeHandler.postDelayed(changeLanguageRunnable, CHANGE_LANGUAGE_DELAY); showLanguageIcon(item); }) .setNegativeButton(R.string.cancel, null) .show(); } private void showDialogDownloadAll() { MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this); builder .setTitle(R.string.download_all_galleries_in_this_page) .setIcon(R.drawable.ic_file) .setNegativeButton(R.string.cancel, null) .setPositiveButton(R.string.ok, (dialog, which) -> { for (GenericGallery g : inspector.getGalleries()) DownloadGalleryV2.downloadGallery(MainActivity.this, g); }); builder.show(); } private void updateTagStatus(MenuItem item, TagStatus ts) { switch (ts) { case DEFAULT: item.setIcon(R.drawable.ic_help); break; case AVOIDED: item.setIcon(R.drawable.ic_close); break; case ACCEPTED: item.setIcon(R.drawable.ic_check); break; } Global.setTint(this, item.getIcon()); } @Override public boolean onNavigationItemSelected(@NonNull MenuItem item) { Intent intent; if (item.getItemId() == R.id.downloaded) { if (Global.hasStoragePermission(this)) startLocalActivity(); else requestStorage(); } else if (item.getItemId() == R.id.bookmarks) { intent = new Intent(this, BookmarkActivity.class); startActivity(intent); } else if (item.getItemId() == R.id.history) { intent = new Intent(this, HistoryActivity.class); startActivity(intent); } else if (item.getItemId() == R.id.favorite_manager) { intent = new Intent(this, FavoriteActivity.class); startActivity(intent); } else if (item.getItemId() == R.id.action_settings) { setting = true; intent = new Intent(this, SettingsActivity.class); startActivity(intent); } else if (item.getItemId() == R.id.online_favorite_manager) { intent = new Intent(this, MainActivity.class); intent.putExtra(getPackageName() + ".FAVORITE", true); startActivity(intent); } else if (item.getItemId() == R.id.random) { intent = new Intent(this, RandomActivity.class); startActivity(intent); } else if (item.getItemId() == R.id.tag_manager) { intent = new Intent(this, TagFilterActivity.class); filteringTag = true; startActivity(intent); } else if (item.getItemId() == R.id.status_manager) { intent = new Intent(this, StatusViewerActivity.class); startActivity(intent); } //drawerLayout.closeDrawer(GravityCompat.START); return true; } private void requestStorage() { requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE}, 1); } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); Global.initStorage(this); if (requestCode == 1 && grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) startLocalActivity(); if (requestCode == 2 && grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) new VersionChecker(this, true); } private void startLocalActivity() { Intent i = new Intent(this, LocalActivity.class); startActivity(i); } /** * UNKNOWN in case of error * NORMAL when in main page * TAG when searching for a specific tag * FAVORITE when using online favorite button * SEARCH when used SearchActivity * BOOKMARK when loaded a bookmark * ID when searched for an ID */ private enum ModeType {UNKNOWN, NORMAL, TAG, FAVORITE, SEARCH, BOOKMARK, ID} abstract class MainInspectorResponse extends InspectorV3.DefaultInspectorResponse { @Override public void onSuccess(List galleries) { super.onSuccess(galleries); if (adapter != null) adapter.resetStatuses(); if (galleries.isEmpty()) showError(R.string.no_entry_found, null); } @Override public void onStart() { runOnUiThread(() -> refresher.setRefreshing(true)); hideError(); } @Override public void onEnd() { runOnUiThread(() -> refresher.setRefreshing(false)); inspecting = false; } @Override public void onFailure(Exception e) { super.onFailure(e); showError(R.string.unable_to_connect_to_the_site, v -> { inspector = inspector.cloneInspector(MainActivity.this, inspector.getResponse()); inspector.start(); }); } @Override public boolean shouldStart(InspectorV3 inspector) { return true; //loadWebVewUrl(inspector.getUrl()); //return inspector.canParseDocument(); } } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/PINActivity.java ================================================ package com.maxwai.nclientv3; import static androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_WEAK; import static androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL; import android.content.Intent; import android.content.SharedPreferences; import android.os.Bundle; import android.widget.Button; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.biometric.BiometricPrompt; import androidx.core.content.ContextCompat; import com.maxwai.nclientv3.components.activities.GeneralActivity; import java.util.concurrent.Executor; public class PINActivity extends GeneralActivity { private SharedPreferences preferences; private boolean authSuccess = false; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); //Global.initActivity(this); setContentView(R.layout.activity_pin); preferences = getSharedPreferences("Settings", 0); if (!hasPin()) { finish(); return; } Executor executor = ContextCompat.getMainExecutor(this); BiometricPrompt biometricPrompt = new BiometricPrompt(this, executor, new BiometricPrompt.AuthenticationCallback() { @Override public void onAuthenticationError(int errorCode, @NonNull CharSequence errString) { super.onAuthenticationError(errorCode, errString); Toast.makeText(getApplicationContext(), getString(R.string.auth_error), Toast.LENGTH_SHORT) .show(); } @Override public void onAuthenticationSucceeded( @NonNull BiometricPrompt.AuthenticationResult result) { super.onAuthenticationSucceeded(result); authSuccess = true; finish(); } @Override public void onAuthenticationFailed() { super.onAuthenticationFailed(); Toast.makeText(getApplicationContext(), getString(R.string.auth_error), Toast.LENGTH_SHORT) .show(); } }); BiometricPrompt.PromptInfo promptInfo = new BiometricPrompt.PromptInfo.Builder() .setTitle(getString(R.string.auth_title)) .setSubtitle(getString(R.string.auth_subtitle)) .setAllowedAuthenticators(BIOMETRIC_WEAK | DEVICE_CREDENTIAL) .build(); Button unlockButton = findViewById(R.id.unlockButton); unlockButton.setOnClickListener(view -> biometricPrompt.authenticate(promptInfo)); unlockButton.performClick(); } @SuppressWarnings("BooleanMethodIsAlwaysInverted") private boolean hasPin() { return preferences.getBoolean(getString(R.string.preference_key_has_credentials), false); } @Override public void finish() { Intent i = new Intent(this, MainActivity.class); if (!hasPin() || authSuccess) startActivity(i); authSuccess = false; super.finish(); } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/RandomActivity.java ================================================ package com.maxwai.nclientv3; import android.content.Intent; import android.content.res.ColorStateList; import android.os.Bundle; import android.view.MenuItem; import android.view.View; import android.widget.ImageButton; import android.widget.TextView; import androidx.activity.OnBackPressedCallback; import androidx.appcompat.app.ActionBar; import androidx.appcompat.widget.Toolbar; import androidx.core.widget.ImageViewCompat; import com.maxwai.nclientv3.api.RandomLoader; import com.maxwai.nclientv3.api.components.Gallery; import com.maxwai.nclientv3.components.activities.GeneralActivity; import com.maxwai.nclientv3.settings.Favorites; import com.maxwai.nclientv3.settings.Global; import com.maxwai.nclientv3.utility.ImageDownloadUtility; import com.google.android.material.floatingactionbutton.FloatingActionButton; import java.util.Objects; public class RandomActivity extends GeneralActivity { public static Gallery loadedGallery = null; private TextView language; private ImageButton thumbnail; private ImageButton favorite; private TextView title; private TextView page; private View censor; private RandomLoader loader = null; private boolean isFavorite; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); //Global.initActivity(this); setContentView(R.layout.activity_random); loader = new RandomLoader(this); //init components id Toolbar toolbar = findViewById(R.id.toolbar); FloatingActionButton shuffle = findViewById(R.id.shuffle); ImageButton share = findViewById(R.id.share); censor = findViewById(R.id.censor); language = findViewById(R.id.language); thumbnail = findViewById(R.id.thumbnail); favorite = findViewById(R.id.favorite); title = findViewById(R.id.title); page = findViewById(R.id.pages); //init toolbar setSupportActionBar(toolbar); ActionBar actionBar = Objects.requireNonNull(getSupportActionBar()); actionBar.setDisplayHomeAsUpEnabled(true); actionBar.setDisplayShowTitleEnabled(true); actionBar.setTitle(R.string.random_manga); if (loadedGallery != null) loadGallery(loadedGallery); shuffle.setOnClickListener(v -> loader.requestGallery()); thumbnail.setOnClickListener(v -> { if (loadedGallery != null) { Intent intent = new Intent(RandomActivity.this, GalleryActivity.class); intent.putExtra(RandomActivity.this.getPackageName() + ".GALLERY", loadedGallery); RandomActivity.this.startActivity(intent); } }); share.setOnClickListener(v -> { if (loadedGallery != null) Global.shareGallery(RandomActivity.this, loadedGallery); }); censor.setOnClickListener(v -> censor.setVisibility(View.GONE)); favorite.setOnClickListener(v -> { if (loadedGallery != null) { if (isFavorite) { Favorites.removeFavorite(loadedGallery); isFavorite = false; } else { Favorites.addFavorite(loadedGallery); isFavorite = true; } } favoriteUpdateButton(); }); ColorStateList colorStateList = ColorStateList.valueOf(getColor(R.color.tint_light)); ImageViewCompat.setImageTintList(shuffle, colorStateList); ImageViewCompat.setImageTintList(share, colorStateList); ImageViewCompat.setImageTintList(favorite, colorStateList); Global.setTint(this, shuffle.getContentBackground()); Global.setTint(this, share.getDrawable()); Global.setTint(this, favorite.getDrawable()); getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) { @Override public void handleOnBackPressed() { loadedGallery = null; setEnabled(false); getOnBackPressedDispatcher().onBackPressed(); setEnabled(true); } }); } @Override public boolean onOptionsItemSelected(MenuItem item) { if (item.getItemId() == android.R.id.home) { finish(); return true; } return super.onOptionsItemSelected(item); } public void loadGallery(Gallery gallery) { loadedGallery = gallery; if (Global.isDestroyed(this)) return; ImageDownloadUtility.loadImage(this, gallery.getCover(), thumbnail); language.setText(Global.getLanguageFlag(gallery.getLanguage())); isFavorite = Favorites.isFavorite(loadedGallery); favoriteUpdateButton(); title.setText(gallery.getTitle()); page.setText(getString(R.string.page_count_format, gallery.getPageCount())); censor.setVisibility(gallery.hasIgnoredTags() ? View.VISIBLE : View.GONE); } private void favoriteUpdateButton() { runOnUiThread(() -> { ImageDownloadUtility.loadImage(isFavorite ? R.drawable.ic_favorite : R.drawable.ic_favorite_border, favorite); Global.setTint(this, favorite.getDrawable()); }); } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/SearchActivity.java ================================================ package com.maxwai.nclientv3; import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodManager; import android.widget.ArrayAdapter; import android.widget.Button; import android.widget.LinearLayout; import android.widget.TextView; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.widget.AppCompatAutoCompleteTextView; import androidx.appcompat.widget.SearchView; import androidx.appcompat.widget.Toolbar; import androidx.recyclerview.widget.DividerItemDecoration; import androidx.recyclerview.widget.RecyclerView; import com.maxwai.nclientv3.adapters.HistoryAdapter; import com.maxwai.nclientv3.api.components.Ranges; import com.maxwai.nclientv3.api.components.Tag; import com.maxwai.nclientv3.api.enums.Language; import com.maxwai.nclientv3.api.enums.SpecialTagIds; import com.maxwai.nclientv3.api.enums.TagStatus; import com.maxwai.nclientv3.api.enums.TagType; import com.maxwai.nclientv3.async.database.Queries; import com.maxwai.nclientv3.components.activities.GeneralActivity; import com.maxwai.nclientv3.components.widgets.ChipTag; import com.maxwai.nclientv3.components.widgets.CustomLinearLayoutManager; import com.maxwai.nclientv3.settings.DefaultDialogs; import com.maxwai.nclientv3.settings.Global; import com.maxwai.nclientv3.settings.Login; import com.maxwai.nclientv3.utility.LogUtility; import com.maxwai.nclientv3.utility.Utility; import com.google.android.material.chip.Chip; import com.google.android.material.chip.ChipGroup; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import java.util.ArrayList; import java.util.List; import java.util.Locale; public class SearchActivity extends GeneralActivity { public static final int CUSTOM_ID_START = 100000000; private static int customId = CUSTOM_ID_START; private final Ranges ranges = new Ranges(); private final ArrayList tags = new ArrayList<>(); private final Chip[] addChip = new Chip[TagType.values.length]; private ChipGroup[] groups; private SearchView searchView; private AppCompatAutoCompleteTextView autoComplete; private TagType loadedTag = null; private HistoryAdapter adapter; private boolean advanced = false; private Ranges.TimeUnit temporaryUnit; private InputMethodManager inputMethodManager; private AlertDialog alertDialog; public void setQuery(String str, boolean submit) { runOnUiThread(() -> searchView.setQuery(str, submit)); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); //Global.initActivity(this); setContentView(R.layout.activity_search); //init toolbar Toolbar toolbar = findViewById(R.id.toolbar); setSupportActionBar(toolbar); assert getSupportActionBar() != null; getSupportActionBar().setDisplayHomeAsUpEnabled(true); getSupportActionBar().setDisplayShowTitleEnabled(false); inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); //find IDs searchView = findViewById(R.id.search); RecyclerView recyclerView = findViewById(R.id.recycler); groups = new ChipGroup[]{ null, findViewById(R.id.parody_group), findViewById(R.id.character_group), findViewById(R.id.tag_group), findViewById(R.id.artist_group), findViewById(R.id.group_group), findViewById(R.id.language_group), findViewById(R.id.category_group), }; initRanges(); adapter = new HistoryAdapter(this); autoComplete = (AppCompatAutoCompleteTextView) getLayoutInflater().inflate(R.layout.autocomplete_entry, findViewById(R.id.appbar), false); autoComplete.setOnEditorActionListener((v, actionId, event) -> { if (actionId == EditorInfo.IME_ACTION_SEND) { alertDialog.dismiss(); createChip(); return true; } return false; }); //init recyclerview recyclerView.setLayoutManager(new CustomLinearLayoutManager(this)); recyclerView.setAdapter(adapter); recyclerView.addItemDecoration(new DividerItemDecoration(this, DividerItemDecoration.VERTICAL)); searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { @Override public boolean onQueryTextSubmit(String query) { query = query.trim(); if (query.isEmpty() && !advanced) return true; if (!query.isEmpty()) adapter.addHistory(query); final Intent i = new Intent(SearchActivity.this, MainActivity.class); i.putExtra(getPackageName() + ".SEARCHMODE", true); i.putExtra(getPackageName() + ".QUERY", query); i.putExtra(getPackageName() + ".ADVANCED", advanced); if (advanced) { ArrayList tt = new ArrayList<>(tags.size()); for (ChipTag t : tags) if (t.getTag().getStatus() == TagStatus.ACCEPTED) tt.add(t.getTag()); i.putParcelableArrayListExtra(getPackageName() + ".TAGS", tt); i.putExtra(getPackageName() + ".RANGES", ranges); } SearchActivity.this.runOnUiThread(() -> { startActivity(i); finish(); }); return true; } @Override public boolean onQueryTextChange(String newText) { return false; } }); populateGroup(); searchView.requestFocus(); } private void createPageBuilder(int title, int min, int max, int actual, DefaultDialogs.DialogResults results) { min = Math.max(1, min); actual = Math.max(actual, min); DefaultDialogs.pageChangerDialog(new DefaultDialogs.Builder(this) .setTitle(title) .setMax(max) .setMin(min) .setDrawable(R.drawable.ic_search) .setActual(actual) .setYesbtn(R.string.ok) .setNobtn(R.string.cancel) .setMaybebtn(R.string.reset) .setDialogs(results)); } private void initRanges() { LinearLayout pageRangeLayout = findViewById(R.id.page_range); LinearLayout uploadRangeLayout = findViewById(R.id.upload_range); ((TextView) pageRangeLayout.findViewById(R.id.title)).setText(R.string.page_range); ((TextView) uploadRangeLayout.findViewById(R.id.title)).setText(R.string.upload_time); Button fromPage = pageRangeLayout.findViewById(R.id.fromButton); Button toPage = pageRangeLayout.findViewById(R.id.toButton); Button fromDate = uploadRangeLayout.findViewById(R.id.fromButton); Button toDate = uploadRangeLayout.findViewById(R.id.toButton); fromPage.setOnClickListener(v -> createPageBuilder(R.string.from_page, 0, 2000, ranges.getFromPage(), new DefaultDialogs.CustomDialogResults() { @Override public void positive(int actual) { ranges.setFromPage(actual); fromPage.setText(String.format(Locale.US, "%d", actual)); if (ranges.getFromPage() > ranges.getToPage()) { ranges.setToPage(Ranges.UNDEFINED); toPage.setText(""); } advanced = true; } @Override public void neutral() { ranges.setFromPage(Ranges.UNDEFINED); fromPage.setText(""); } })); toPage.setOnClickListener(v -> createPageBuilder(R.string.to_page, ranges.getFromPage(), 2000, ranges.getToPage(), new DefaultDialogs.CustomDialogResults() { @Override public void positive(int actual) { ranges.setToPage(actual); toPage.setText(String.format(Locale.US, "%d", actual)); advanced = true; } @Override public void neutral() { ranges.setToPage(Ranges.UNDEFINED); toPage.setText(""); } })); fromDate.setOnClickListener(v -> showUnitDialog(fromDate, true)); toDate.setOnClickListener(v -> showUnitDialog(toDate, false)); } private void showUnitDialog(Button button, boolean from) { int i = 0; String[] strings = new String[Ranges.TimeUnit.values().length]; for (Ranges.TimeUnit unit : Ranges.TimeUnit.values()) strings[i++] = getString(unit.getString()); MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this); builder.setTitle(R.string.choose_unit); builder.setIcon(R.drawable.ic_search); builder.setAdapter(new ArrayAdapter<>(this, android.R.layout.simple_dropdown_item_1line, strings), (dialog, which) -> { temporaryUnit = Ranges.TimeUnit.values()[which]; createPageBuilder(from ? R.string.from_time : R.string.to_time, 1, temporaryUnit == Ranges.TimeUnit.YEAR ? 10 : 100, 1, new DefaultDialogs.CustomDialogResults() { @Override public void positive(int actual) { if (from) { ranges.setFromDateUnit(temporaryUnit); ranges.setFromDate(actual); } else { ranges.setToDateUnit(temporaryUnit); ranges.setToDate(actual); } button.setText(String.format(Locale.US, "%d %c", actual, Character.toUpperCase(temporaryUnit.getVal()))); advanced = true; } @Override public void neutral() { if (from) { ranges.setFromDateUnit(Ranges.UNDEFINED_DATE); ranges.setFromDate(Ranges.UNDEFINED); } else { ranges.setToDateUnit(Ranges.UNDEFINED_DATE); ranges.setToDate(Ranges.UNDEFINED); } button.setText(""); } }); }); builder.setNeutralButton(R.string.reset, (dialog, which) -> { if (from) { ranges.setFromDateUnit(Ranges.UNDEFINED_DATE); ranges.setFromDate(Ranges.UNDEFINED); } else { ranges.setToDateUnit(Ranges.UNDEFINED_DATE); ranges.setToDate(Ranges.UNDEFINED); } button.setText(""); }); builder.show(); } private void populateGroup() { //add top tags for (TagType type : new TagType[]{TagType.TAG, TagType.PARODY, TagType.CHARACTER, TagType.ARTIST, TagType.GROUP}) { for (Tag t : Queries.TagTable.getTopTags(type, Global.getFavoriteLimit(this))) addChipTag(t, true, true); } //add already filtered tags for (Tag t : Queries.TagTable.getAllFiltered()) if (!tagAlreadyExist(t)) addChipTag(t, true, true); //add categories for (Tag t : Queries.TagTable.getTrueAllType(TagType.CATEGORY)) addChipTag(t, false, false); //add languages for (Tag t : Queries.TagTable.getTrueAllType(TagType.LANGUAGE)) { if (t.getId() == SpecialTagIds.LANGUAGE_ENGLISH && Global.getOnlyLanguage() == Language.ENGLISH) t.setStatus(TagStatus.ACCEPTED); else if (t.getId() == SpecialTagIds.LANGUAGE_JAPANESE && Global.getOnlyLanguage() == Language.JAPANESE) t.setStatus(TagStatus.ACCEPTED); else if (t.getId() == SpecialTagIds.LANGUAGE_CHINESE && Global.getOnlyLanguage() == Language.CHINESE) t.setStatus(TagStatus.ACCEPTED); addChipTag(t, false, false); } //add online tags if (Login.useAccountTag()) for (Tag t : Queries.TagTable.getAllOnlineBlacklisted()) if (!tagAlreadyExist(t)) addChipTag(t, true, true); //add + button for (TagType type : TagType.values) { //ignore these tags if (type == TagType.UNKNOWN || type == TagType.LANGUAGE || type == TagType.CATEGORY) { addChip[type.getId()] = null; continue; } ChipGroup cg = getGroup(type); Chip add = createAddChip(type, cg); addChip[type.getId()] = add; cg.addView(add); } } private Chip createAddChip(TagType type, ChipGroup group) { Chip c = (Chip) getLayoutInflater().inflate(R.layout.chip_layout, group, false); c.setCloseIconVisible(false); c.setChipIconResource(R.drawable.ic_add); c.setText(getString(R.string.add)); c.setOnClickListener(v -> loadTag(type)); Global.setTint(this, c.getChipIcon()); return c; } private boolean tagAlreadyExist(Tag tag) { for (ChipTag t : tags) { if (t.getTag().getName().equals(tag.getName())) return true; } return false; } private void addChipTag(Tag t, boolean close, boolean canBeAvoided) { ChipGroup cg = getGroup(t.getType()); ChipTag c = (ChipTag) getLayoutInflater().inflate(R.layout.chip_layout_entry, cg, false); c.init(t, close, canBeAvoided); c.setOnCloseIconClickListener(v -> { cg.removeView(c); tags.remove(c); advanced = true; }); c.setOnClickListener(v -> { c.updateStatus(); advanced = true; }); cg.addView(c); tags.add(c); } private void loadDropdown(TagType type) { List allTags = Queries.TagTable.getAllTagOfType(type); String[] tagNames = new String[allTags.size()]; int i = 0; for (Tag t : allTags) tagNames[i++] = t.getName(); autoComplete.setAdapter(new ArrayAdapter<>(SearchActivity.this, android.R.layout.simple_dropdown_item_1line, tagNames)); loadedTag = type; } private void loadTag(TagType type) { if (type != loadedTag) loadDropdown(type); addDialog(); autoComplete.requestFocus(); inputMethodManager.showSoftInput(autoComplete, InputMethodManager.SHOW_IMPLICIT); } private ChipGroup getGroup(TagType type) { return groups[type.getId()]; } private void addDialog() { MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this); builder.setView(autoComplete); autoComplete.setText(""); builder.setPositiveButton(R.string.ok, (dialog, which) -> createChip()); builder.setCancelable(true).setNegativeButton(R.string.cancel, null); builder.setTitle(R.string.insert_tag_name); try { alertDialog = builder.show(); } catch (IllegalStateException e) {//the autoComplete is still attached to another View ((ViewGroup) autoComplete.getParent()).removeView(autoComplete); alertDialog = builder.show(); } } private void createChip() { String name = autoComplete.getText().toString().toLowerCase(Locale.US); Tag tag = Queries.TagTable.searchTag(name, loadedTag); if (tag == null) tag = new Tag(name, 0, customId++, loadedTag, TagStatus.ACCEPTED); LogUtility.d("CREATED WITH ID: " + tag.getId()); if (tagAlreadyExist(tag)) return; //remove add, insert new tag, reinsert add if (getGroup(loadedTag) != null) getGroup(loadedTag).removeView(addChip[loadedTag.getId()]); addChipTag(tag, true, true); getGroup(loadedTag).addView(addChip[loadedTag.getId()]); inputMethodManager.hideSoftInputFromWindow(searchView.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS); autoComplete.setText(""); advanced = true; if (autoComplete.getParent() != null) ((ViewGroup) autoComplete.getParent()).removeView(autoComplete); } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.search, menu); Utility.tintMenu(this, menu); return super.onCreateOptionsMenu(menu); } @Override public boolean onOptionsItemSelected(MenuItem item) { if (item.getItemId() == android.R.id.home) { finish(); return true; } else if (item.getItemId() == R.id.view_groups) { View v = findViewById(R.id.groups); boolean isVisible = v.getVisibility() == View.VISIBLE; v.setVisibility(isVisible ? View.GONE : View.VISIBLE); item.setIcon(isVisible ? R.drawable.ic_add : R.drawable.ic_close); Global.setTint(this, item.getIcon()); } return super.onOptionsItemSelected(item); } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/SettingsActivity.java ================================================ package com.maxwai.nclientv3; import android.content.Context; import android.content.Intent; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.provider.Settings; import android.view.MenuItem; import android.widget.Toast; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContract; import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.appcompat.app.ActionBar; import androidx.appcompat.widget.Toolbar; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.maxwai.nclientv3.async.database.export.Exporter; import com.maxwai.nclientv3.async.database.export.Manager; import com.maxwai.nclientv3.components.activities.GeneralActivity; import com.maxwai.nclientv3.components.views.GeneralPreferenceFragment; import com.maxwai.nclientv3.settings.Global; import com.maxwai.nclientv3.utility.LogUtility; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.Writer; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; import java.util.Objects; import java.util.stream.Collectors; public class SettingsActivity extends GeneralActivity { GeneralPreferenceFragment fragment; private ActivityResultLauncher IMPORT_ZIP; private ActivityResultLauncher SAVE_SETTINGS; private ActivityResultLauncher REQUEST_STORAGE_MANAGER; private ActivityResultLauncher COPY_LOGS; private int selectedItem; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); registerActivities(); //Global.initActivity(this); setContentView(R.layout.activity_settings); Toolbar toolbar = findViewById(R.id.toolbar); setSupportActionBar(toolbar); ActionBar actionBar = Objects.requireNonNull(getSupportActionBar()); actionBar.setTitle(R.string.settings); actionBar.setDisplayHomeAsUpEnabled(true); actionBar.setDisplayShowTitleEnabled(true); fragment = Objects.requireNonNull((GeneralPreferenceFragment) getSupportFragmentManager().findFragmentById(R.id.fragment)); fragment.setAct(this); fragment.setType(SettingsActivity.Type.values()[getIntent().getIntExtra(getPackageName() + ".TYPE", SettingsActivity.Type.MAIN.ordinal())]); } private void registerActivities() { IMPORT_ZIP = registerForActivityResult(new ActivityResultContracts.GetContent(), selectedFile -> { if (selectedFile == null) return; importSettings(selectedFile); }); SAVE_SETTINGS = registerForActivityResult(new ActivityResultContracts.CreateDocument("application/zip") { @NonNull @Override public Intent createIntent(@NonNull Context context, @NonNull String input) { Intent i = super.createIntent(context, input); i.setType("application/zip"); return i; } }, selectedFile -> { if (selectedFile == null) return; exportSettings(selectedFile); }); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { REQUEST_STORAGE_MANAGER = registerForActivityResult(new ActivityResultContract<>() { @RequiresApi(api = Build.VERSION_CODES.R) @NonNull @Override public Intent createIntent(@NonNull Context context, Object input) { Intent i = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION); i.setData(Uri.parse("package:" + getPackageName())); return i; } @Override public Object parseResult(int resultCode, @Nullable Intent intent) { return null; } }, result -> { if (Global.isExternalStorageManager()) { fragment.manageCustomPath(); } }); } COPY_LOGS = registerForActivityResult(new ActivityResultContracts.CreateDocument("text/log") { @NonNull @Override public Intent createIntent(@NonNull Context context, @NonNull String input) { Intent i = super.createIntent(context, input); i.setType("text/log"); return i; } }, selectedFile -> { if (selectedFile == null) return; try { Process process = Runtime.getRuntime().exec(new String[]{"logcat", "-d"}); try (OutputStream outputStream = getContentResolver().openOutputStream(selectedFile); Writer writer = new BufferedWriter(new OutputStreamWriter(outputStream)); BufferedReader in = new BufferedReader(new InputStreamReader(process.getInputStream()))) { String output = in.lines().collect(Collectors.joining("\n")); writer.write(output); } Toast.makeText(this, getString(process.exitValue() != 0 ? R.string.copy_logs_fail : R.string.export_finished), Toast.LENGTH_SHORT).show(); } catch (IOException e) { LogUtility.e("Error getting logcat", e); Toast.makeText(this, getString(R.string.copy_logs_fail), Toast.LENGTH_SHORT).show(); } }); } private void importSettings(Uri selectedFile) { new Manager(selectedFile, this, false, () -> { Toast.makeText(this, R.string.import_finished, Toast.LENGTH_SHORT).show(); finish(); }).start(); } private void exportSettings(Uri selectedFile) { new Manager(selectedFile, this, true, () -> Toast.makeText(this, R.string.export_finished, Toast.LENGTH_SHORT).show()).start(); } public void importSettings() { if (IMPORT_ZIP != null) { IMPORT_ZIP.launch("application/zip"); } else { importOldVersion(); } } private void importOldVersion() { String[] files = Global.BACKUPFOLDER.list(); if (files == null || files.length == 0) return; selectedItem = 0; MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this); builder.setSingleChoiceItems(files, 0, (dialog, which) -> { LogUtility.d(which); selectedItem = which; }); builder.setPositiveButton(R.string.ok, (dialog, which) -> importSettings(Uri.fromFile(new File(Global.BACKUPFOLDER, files[selectedItem])))).setNegativeButton(R.string.cancel, null); builder.show(); } public void exportSettings() { String name = Exporter.defaultExportName(this); if (SAVE_SETTINGS != null) SAVE_SETTINGS.launch(name); else { File f = new File(Global.BACKUPFOLDER, name); exportSettings(Uri.fromFile(f)); } } public void exportLogs() { if (COPY_LOGS == null) { Toast.makeText(this, R.string.failed, Toast.LENGTH_SHORT).show(); return; } Date actualTime = new Date(); COPY_LOGS.launch(String.format("NClientv3_Log_%s.log", new SimpleDateFormat("yyMMdd_HHmmss", Locale.getDefault()).format(actualTime))); } @Override public boolean onOptionsItemSelected(MenuItem item) { if (item.getItemId() == android.R.id.home) { finish(); return true; } return super.onOptionsItemSelected(item); } @RequiresApi(Build.VERSION_CODES.R) public void requestStorageManager() { if (REQUEST_STORAGE_MANAGER == null) { Toast.makeText(this, R.string.failed, Toast.LENGTH_SHORT).show(); return; } MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this); builder.setIcon(R.drawable.ic_file); builder.setTitle(R.string.requesting_storage_access); builder.setMessage(R.string.request_storage_manager_summary); builder.setPositiveButton(R.string.ok, (dialog, which) -> REQUEST_STORAGE_MANAGER.launch(null)).setNegativeButton(R.string.cancel, null).show(); } public enum Type {MAIN, COLUMN, DATA} } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/StatusManagerActivity.java ================================================ package com.maxwai.nclientv3; import android.os.Bundle; import android.view.MenuItem; import androidx.appcompat.app.ActionBar; import androidx.appcompat.widget.Toolbar; import androidx.recyclerview.widget.RecyclerView; import com.maxwai.nclientv3.adapters.StatusManagerAdapter; import com.maxwai.nclientv3.components.activities.GeneralActivity; import com.maxwai.nclientv3.components.widgets.CustomLinearLayoutManager; import java.util.Objects; public class StatusManagerActivity extends GeneralActivity { StatusManagerAdapter adapter; RecyclerView recycler; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); //Global.initActivity(this); setContentView(R.layout.activity_bookmark); Toolbar toolbar = findViewById(R.id.toolbar); setSupportActionBar(toolbar); ActionBar actionBar = Objects.requireNonNull(getSupportActionBar()); actionBar.setDisplayHomeAsUpEnabled(true); actionBar.setDisplayShowTitleEnabled(true); actionBar.setTitle(R.string.manage_statuses); recycler = findViewById(R.id.recycler); adapter = new StatusManagerAdapter(this); recycler.setLayoutManager(new CustomLinearLayoutManager(this)); recycler.setAdapter(adapter); } @Override public boolean onOptionsItemSelected(MenuItem item) { if (item.getItemId() == android.R.id.home) { finish(); return true; } return super.onOptionsItemSelected(item); } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/StatusViewerActivity.java ================================================ package com.maxwai.nclientv3; import android.os.Bundle; import android.view.Menu; import android.view.MenuItem; import androidx.annotation.Nullable; import androidx.appcompat.app.ActionBar; import androidx.appcompat.widget.SearchView; import androidx.appcompat.widget.Toolbar; import androidx.viewpager2.widget.ViewPager2; import com.maxwai.nclientv3.components.activities.GeneralActivity; import com.maxwai.nclientv3.settings.Global; import com.maxwai.nclientv3.ui.main.PlaceholderFragment; import com.maxwai.nclientv3.ui.main.SectionsPagerAdapter; import com.maxwai.nclientv3.utility.LogUtility; import com.maxwai.nclientv3.utility.Utility; import com.google.android.material.tabs.TabLayout; import com.google.android.material.tabs.TabLayoutMediator; import java.util.Objects; public class StatusViewerActivity extends GeneralActivity { private boolean sortByTitle = false; private String query; private ViewPager2 viewPager; private SectionsPagerAdapter sectionsPagerAdapter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_status_viewer); Toolbar toolbar = findViewById(R.id.toolbar); setSupportActionBar(toolbar); ActionBar actionBar = Objects.requireNonNull(getSupportActionBar()); actionBar.setDisplayHomeAsUpEnabled(true); actionBar.setDisplayShowTitleEnabled(true); actionBar.setTitle(R.string.manage_statuses); viewPager = findViewById(R.id.view_pager); sectionsPagerAdapter = new SectionsPagerAdapter(this); viewPager.setAdapter(sectionsPagerAdapter); viewPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() { @Override public void onPageSelected(int position) { PlaceholderFragment fragment = getPositionFragment(position); if (fragment != null) fragment.reload(query, sortByTitle); } }); TabLayout tabs = findViewById(R.id.tabs); new TabLayoutMediator(tabs, viewPager, true, (tab, position) -> tab.setText(sectionsPagerAdapter.getPageTitle(position))).attach(); } @Override protected void onResume() { super.onResume(); TabLayout tabs = findViewById(R.id.tabs); for (int i = 0; i < tabs.getTabCount(); i++) { Objects.requireNonNull(tabs.getTabAt(i)).setText(sectionsPagerAdapter.getPageTitle(i)); } PlaceholderFragment fragment = getActualFragment(); if (fragment != null) fragment.reload(query, sortByTitle); } @Override public boolean onOptionsItemSelected(MenuItem item) { if (item.getItemId() == android.R.id.home) { finish(); return true; } else if (item.getItemId() == R.id.sort_by_name) { sortByTitle = !sortByTitle; PlaceholderFragment fragment = getActualFragment(); if (fragment != null) fragment.changeSort(sortByTitle); item.setTitle(sortByTitle ? R.string.sort_by_latest : R.string.sort_by_title); item.setIcon(sortByTitle ? R.drawable.ic_sort_by_alpha : R.drawable.ic_access_time); Global.setTint(this, item.getIcon()); } return super.onOptionsItemSelected(item); } @Nullable private PlaceholderFragment getActualFragment() { return getPositionFragment(viewPager.getCurrentItem()); } @Nullable private PlaceholderFragment getPositionFragment(int position) { PlaceholderFragment f = (PlaceholderFragment) getSupportFragmentManager().findFragmentByTag("f" + position); LogUtility.d(f); return f; } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.status_viewer, menu); final SearchView searchView = (androidx.appcompat.widget.SearchView) menu.findItem(R.id.search).getActionView(); Objects.requireNonNull(searchView).setOnQueryTextListener(new androidx.appcompat.widget.SearchView.OnQueryTextListener() { @Override public boolean onQueryTextSubmit(String query) { return false; } @Override public boolean onQueryTextChange(String newText) { query = newText; PlaceholderFragment fragment = getActualFragment(); if (fragment != null) fragment.changeQuery(query); return true; } }); Utility.tintMenu(this, menu); return super.onCreateOptionsMenu(menu); } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/TagFilterActivity.java ================================================ package com.maxwai.nclientv3; import android.content.res.Configuration; import android.net.Uri; import android.os.Bundle; import android.view.Menu; import android.view.MenuItem; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AppCompatDelegate; import androidx.appcompat.widget.SearchView; import androidx.appcompat.widget.Toolbar; import androidx.fragment.app.Fragment; import androidx.recyclerview.widget.RecyclerView; import androidx.viewpager2.adapter.FragmentStateAdapter; import androidx.viewpager2.widget.ViewPager2; import com.maxwai.nclientv3.adapters.TagsAdapter; import com.maxwai.nclientv3.components.activities.GeneralActivity; import com.maxwai.nclientv3.components.widgets.CustomGridLayoutManager; import com.maxwai.nclientv3.components.widgets.TagTypePage; import com.maxwai.nclientv3.settings.DefaultDialogs; import com.maxwai.nclientv3.settings.Global; import com.maxwai.nclientv3.settings.Login; import com.maxwai.nclientv3.settings.TagV2; import com.maxwai.nclientv3.utility.LogUtility; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.tabs.TabLayout; import com.google.android.material.tabs.TabLayoutMediator; import java.util.List; import java.util.Objects; public class TagFilterActivity extends GeneralActivity { static { AppCompatDelegate.setCompatVectorFromResourcesEnabled(true); } private SearchView searchView; private ViewPager2 mViewPager; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); //Global.initActivity(this); setContentView(R.layout.activity_tag_filter); //init toolbar Toolbar toolbar = findViewById(R.id.toolbar); setSupportActionBar(toolbar); ActionBar actionBar = Objects.requireNonNull(getSupportActionBar()); actionBar.setDisplayHomeAsUpEnabled(true); // Create the adapter that will return a fragment for each of the three // primary sections of the activity. TagTypePageAdapter mTagTypePageAdapter = new TagTypePageAdapter(this); mViewPager = findViewById(R.id.container); mViewPager.setAdapter(mTagTypePageAdapter); mViewPager.setOffscreenPageLimit(1); TabLayout tabLayout = findViewById(R.id.tabs); mViewPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() { @Override public void onPageSelected(int position) { TagTypePage page = getFragment(position); if (page != null) { ((TagsAdapter) Objects.requireNonNull(page.getRecyclerView().getAdapter())).addItem(); } } }); new TabLayoutMediator(tabLayout, mViewPager, (tab, position) -> { int id = 0; switch (position) { case 0: id = R.string.applied_filters; break; case 1: id = R.string.tags; break; case 2: id = R.string.artists; break; case 3: id = R.string.characters; break; case 4: id = R.string.parodies; break; case 5: id = R.string.groups; break; case 6: id = R.string.online_tags; break; } tab.setText(id); }).attach(); mViewPager.setCurrentItem(getPage()); } @Nullable private TagTypePage getActualFragment() { return getFragment(mViewPager.getCurrentItem()); } @Nullable private TagTypePage getFragment(int position) { return (TagTypePage) getSupportFragmentManager().findFragmentByTag("f" + position); } private int getPage() { Uri data = getIntent().getData(); if (data != null) { List params = data.getPathSegments(); for (String x : params) LogUtility.i(x); if (!params.isEmpty()) { switch (params.get(0)) { case "tags": return 1; case "artists": return 2; case "characters": return 3; case "parodies": return 4; case "groups": return 5; } } } return 0; } private void updateSortItem(MenuItem item) { item.setIcon(TagV2.isSortedByName() ? R.drawable.ic_sort_by_alpha : R.drawable.ic_sort); item.setTitle(TagV2.isSortedByName() ? R.string.sort_by_title : R.string.sort_by_popular); Global.setTint(this, item.getIcon()); } @Override public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu; this adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.menu_tag_filter, menu); updateSortItem(menu.findItem(R.id.sort_by_name)); searchView = (androidx.appcompat.widget.SearchView) menu.findItem(R.id.search).getActionView(); Objects.requireNonNull(searchView).setOnQueryTextListener(new androidx.appcompat.widget.SearchView.OnQueryTextListener() { @Override public boolean onQueryTextSubmit(String query) { return true; } @Override public boolean onQueryTextChange(String newText) { TagTypePage page = getActualFragment(); if (page != null) { page.refilter(newText); } return true; } }); return true; } private void createDialog() { MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this); builder.setTitle(R.string.are_you_sure).setMessage(getString(R.string.clear_this_list)).setIcon(R.drawable.ic_help); builder.setPositiveButton(R.string.yes, (dialog, which) -> { TagTypePage page = getActualFragment(); if (page != null) { page.reset(); } }).setNegativeButton(R.string.no, null).setCancelable(true); builder.show(); } @Override public boolean onOptionsItemSelected(MenuItem item) { // Handle action bar item clicks here. The action bar will // automatically handle clicks on the Home/Up button, so long // as you specify a parent activity in AndroidManifest.xml. int id = item.getItemId(); TagTypePage page = getActualFragment(); if (id == R.id.reset_tags) createDialog(); else if (id == R.id.set_min_count) minCountBuild(); else if (id == R.id.sort_by_name) { TagV2.updateSortByName(this); updateSortItem(item); if (page != null) page.refilter(searchView.getQuery().toString()); } return super.onOptionsItemSelected(item); } private void minCountBuild() { int min = TagV2.getMinCount(); DefaultDialogs.Builder builder = new DefaultDialogs.Builder(this); builder.setActual(min).setMax(100).setMin(2); builder.setYesbtn(R.string.ok).setNobtn(R.string.cancel); builder.setTitle(R.string.set_minimum_count).setDialogs(new DefaultDialogs.CustomDialogResults() { @Override public void positive(int actual) { LogUtility.d("ACTUAL: " + actual); TagV2.updateMinCount(TagFilterActivity.this, actual); TagTypePage page = getActualFragment(); if (page != null) { page.changeSize(); } } }); DefaultDialogs.pageChangerDialog(builder); } @Override public void onConfigurationChanged(@NonNull Configuration newConfig) { super.onConfigurationChanged(newConfig); if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) { changeLayout(true); } else if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) { changeLayout(false); } } private void changeLayout(boolean landscape) { final int count = landscape ? 4 : 2; TagTypePage page = getActualFragment(); if (page != null) { RecyclerView recycler = page.getRecyclerView(); if (recycler != null) { RecyclerView.Adapter adapter = recycler.getAdapter(); CustomGridLayoutManager gridLayoutManager = new CustomGridLayoutManager(this, count); recycler.setLayoutManager(gridLayoutManager); recycler.setAdapter(adapter); } } } static class TagTypePageAdapter extends FragmentStateAdapter { TagTypePageAdapter(TagFilterActivity activity) { super(activity.getSupportFragmentManager(), activity.getLifecycle()); } @NonNull @Override public Fragment createFragment(int position) { return TagTypePage.newInstance(position); } @Override public int getItemCount() { return Login.isLogged() ? 7 : 6; } } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/ZoomActivity.java ================================================ package com.maxwai.nclientv3; import android.Manifest; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.content.res.Configuration; import android.os.Build; import android.os.Bundle; import android.view.KeyEvent; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.ViewConfiguration; import android.view.WindowManager; import android.widget.SeekBar; import android.widget.TextView; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.appcompat.app.ActionBar; import androidx.appcompat.widget.Toolbar; import androidx.constraintlayout.widget.ConstraintLayout; import androidx.fragment.app.Fragment; import androidx.viewpager2.adapter.FragmentStateAdapter; import androidx.viewpager2.widget.ViewPager2; import com.bumptech.glide.Priority; import com.maxwai.nclientv3.api.components.GenericGallery; import com.maxwai.nclientv3.async.database.Queries; import com.maxwai.nclientv3.components.activities.GeneralActivity; import com.maxwai.nclientv3.components.views.ZoomFragment; import com.maxwai.nclientv3.files.GalleryFolder; import com.maxwai.nclientv3.settings.DefaultDialogs; import com.maxwai.nclientv3.settings.Global; import com.maxwai.nclientv3.utility.LogUtility; import com.maxwai.nclientv3.utility.Utility; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import java.io.File; import java.util.Objects; public class ZoomActivity extends GeneralActivity { private final static int hideFlags = View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY; private final static int showFlags = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_STABLE; private static final String VOLUME_SIDE_KEY = "volumeSide"; private static final String SCROLL_TYPE_KEY = "zoomScrollType"; private GenericGallery gallery; private int actualPage = 0; private boolean isHidden = false; private ViewPager2 mViewPager; private TextView pageManagerLabel, cornerPageViewer; private View pageSwitcher; private SeekBar seekBar; private Toolbar toolbar; private View view; private GalleryFolder directory; @ViewPager2.Orientation private int tmpScrollType; private boolean up = false, down = false, side; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); //Global.initActivity(this); SharedPreferences preferences = getSharedPreferences("Settings", 0); side = preferences.getBoolean(VOLUME_SIDE_KEY, true); setContentView(R.layout.activity_zoom); //read arguments if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { gallery = getIntent().getParcelableExtra(getPackageName() + ".GALLERY", GenericGallery.class); } else { gallery = getIntent().getParcelableExtra(getPackageName() + ".GALLERY"); } final int page = Objects.requireNonNull(getIntent().getExtras()).getInt(getPackageName() + ".PAGE", 1) - 1; directory = gallery.getGalleryFolder(); //toolbar setup toolbar = findViewById(R.id.toolbar); setSupportActionBar(toolbar); ActionBar actionBar = Objects.requireNonNull(getSupportActionBar()); actionBar.setDisplayHomeAsUpEnabled(true); actionBar.setDisplayShowTitleEnabled(true); setTitle(gallery.getTitle()); getWindow().setFlags( WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS); if (Global.isLockScreen()) getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); //find views SectionsPagerAdapter mSectionsPagerAdapter = new SectionsPagerAdapter(this); mViewPager = findViewById(R.id.container); mViewPager.setAdapter(mSectionsPagerAdapter); mViewPager.setOrientation(preferences.getInt(SCROLL_TYPE_KEY, ScrollType.HORIZONTAL.ordinal())); mViewPager.setOffscreenPageLimit(Global.getOffscreenLimit()); pageSwitcher = findViewById(R.id.page_switcher); pageManagerLabel = findViewById(R.id.pages); cornerPageViewer = findViewById(R.id.page_text); seekBar = findViewById(R.id.seekBar); view = findViewById(R.id.view); //initial setup for views changeLayout(getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE); mViewPager.setKeepScreenOn(Global.isLockScreen()); findViewById(R.id.prev).setOnClickListener(v -> changeClosePage(false)); findViewById(R.id.next).setOnClickListener(v -> changeClosePage(true)); seekBar.setMax(gallery.getPageCount() - 1); if (Global.useRtl()) { seekBar.setRotationY(180); mViewPager.setLayoutDirection(ViewPager2.LAYOUT_DIRECTION_RTL); } //Adding listeners mViewPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() { @Override public void onPageSelected(int newPage) { int oldPage = actualPage; actualPage = newPage; LogUtility.d("Page selected: " + newPage + " from page " + oldPage); setPageText(newPage + 1); seekBar.setProgress(newPage); clearFarRequests(oldPage, newPage); makeNearRequests(newPage); } }); pageManagerLabel.setOnClickListener(v -> DefaultDialogs.pageChangerDialog( new DefaultDialogs.Builder(this) .setActual(actualPage + 1) .setMin(1) .setMax(gallery.getPageCount()) .setTitle(R.string.change_page) .setDrawable(R.drawable.ic_find_in_page) .setDialogs(new DefaultDialogs.CustomDialogResults() { @Override public void positive(int actual) { changePage(actual - 1); } }) )); seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { if (fromUser) { setPageText(progress + 1); } } @Override public void onStartTrackingTouch(SeekBar seekBar) { } @Override public void onStopTrackingTouch(SeekBar seekBar) { changePage(seekBar.getProgress()); } }); changePage(page); setPageText(page + 1); seekBar.setProgress(page); } private void setUserInput(boolean enabled) { mViewPager.setUserInputEnabled(enabled); } private void setPageText(int page) { pageManagerLabel.setText(getString(R.string.page_format, page, gallery.getPageCount())); cornerPageViewer.setText(getString(R.string.page_format, page, gallery.getPageCount())); } @Override public boolean onKeyUp(int keyCode, KeyEvent event) { if (Global.volumeOverride()) { switch (keyCode) { case KeyEvent.KEYCODE_VOLUME_UP: up = false; return true; case KeyEvent.KEYCODE_VOLUME_DOWN: down = false; return true; } } return super.onKeyUp(keyCode, event); } @Override public boolean onKeyDown(int keyCode, KeyEvent event) { if (Global.volumeOverride()) { switch (keyCode) { case KeyEvent.KEYCODE_VOLUME_UP: up = true; changeClosePage(side); if (up && down) changeSide(); return true; case KeyEvent.KEYCODE_VOLUME_DOWN: down = true; changeClosePage(!side); if (up && down) changeSide(); return true; } } return super.onKeyDown(keyCode, event); } private void changeSide() { getSharedPreferences("Settings", 0).edit().putBoolean(VOLUME_SIDE_KEY, side = !side).apply(); Toast.makeText(this, side ? R.string.next_page_volume_up : R.string.next_page_volume_down, Toast.LENGTH_SHORT).show(); } public void changeClosePage(boolean next) { if (Global.useRtl()) next = !next; if (next && mViewPager.getCurrentItem() < (Objects.requireNonNull(mViewPager.getAdapter()).getItemCount() - 1)) changePage(mViewPager.getCurrentItem() + 1); if (!next && mViewPager.getCurrentItem() > 0) changePage(mViewPager.getCurrentItem() - 1); } @Override public void onConfigurationChanged(@NonNull Configuration newConfig) { super.onConfigurationChanged(newConfig); changeLayout(newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE); } private boolean hardwareKeys() { return ViewConfiguration.get(this).hasPermanentMenuKey(); } private void applyMargin(boolean landscape, View view) { ConstraintLayout.LayoutParams lp = (ConstraintLayout.LayoutParams) view.getLayoutParams(); lp.setMargins(0, 0, landscape && !hardwareKeys() ? Global.getNavigationBarHeight(this) : 0, 0); view.setLayoutParams(lp); } private void changeLayout(boolean landscape) { int statusBarHeight = Global.getStatusBarHeight(this); applyMargin(landscape, findViewById(R.id.master_layout)); applyMargin(landscape, toolbar); pageSwitcher.setPadding(0, 0, 0, landscape ? 0 : statusBarHeight); } private void changePage(int newPage) { mViewPager.setCurrentItem(newPage); } private void changeScrollTypeDialog() { MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this); int scrollType = mViewPager.getOrientation(); tmpScrollType = mViewPager.getOrientation(); builder.setTitle(getString(R.string.change_scroll_type) + ":"); builder.setSingleChoiceItems(R.array.scroll_type, scrollType, (dialog, which) -> tmpScrollType = which); builder.setPositiveButton(R.string.ok, (dialog, which) -> { if (tmpScrollType != scrollType) { mViewPager.setOrientation(tmpScrollType); getSharedPreferences("Settings", 0).edit().putInt(SCROLL_TYPE_KEY, tmpScrollType).apply(); int page = actualPage; changePage(page + 1); changePage(page); } }).setNegativeButton(R.string.cancel, null); builder.show(); } @Override public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu; this adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.menu_zoom, menu); Utility.tintMenu(this, menu); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { // Handle action bar item clicks here. The action bar will // automatically handle clicks on the Home/Up button, so long // as you specify a parent activity in AndroidManifest.xml. int id = item.getItemId(); if (id == R.id.rotate) { getActualFragment().rotate(); } else if (id == R.id.save_page) { if (Global.hasStoragePermission(this)) { downloadPage(); } else requestStorage(); } else if (id == R.id.share) { if (gallery.getId() <= 0) sendImage(false); else openSendImageDialog(); } else if (id == android.R.id.home) { finish(); return true; } else if (id == R.id.bookmark) { Queries.ResumeTable.insert(gallery.getId(), actualPage + 1); } else if (id == R.id.scrollType) { changeScrollTypeDialog(); } return super.onOptionsItemSelected(item); } private void openSendImageDialog() { MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this); builder.setPositiveButton(R.string.yes, (dialog, which) -> sendImage(true)) .setNegativeButton(R.string.no, (dialog, which) -> sendImage(false)) .setCancelable(true).setTitle(R.string.send_with_title) .setMessage(R.string.caption_send_with_title) .show(); } private void requestStorage() { requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE}, 1); } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); Global.initStorage(this); if (requestCode == 1 && grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) downloadPage(); } private ZoomFragment getActualFragment() { return getActualFragment(mViewPager.getCurrentItem()); } private void makeNearRequests(int newPage) { ZoomFragment fragment; int offScreenLimit = Global.getOffscreenLimit(); for (int i = newPage - offScreenLimit; i <= newPage + offScreenLimit; i++) { fragment = getActualFragment(i); if (fragment == null) continue; if (i == newPage) fragment.loadImage(Priority.IMMEDIATE); else fragment.loadImage(); } } private void clearFarRequests(int oldPage, int newPage) { ZoomFragment fragment; int offScreenLimit = Global.getOffscreenLimit(); for (int i = oldPage - offScreenLimit; i <= oldPage + offScreenLimit; i++) { if (i >= newPage - offScreenLimit && i <= newPage + offScreenLimit) continue; fragment = getActualFragment(i); if (fragment == null) continue; fragment.cancelRequest(); } } private ZoomFragment getActualFragment(int position) { return (ZoomFragment) getSupportFragmentManager().findFragmentByTag("f" + position); } private void sendImage(boolean withText) { int pageNum = mViewPager.getCurrentItem(); Utility.sendImage(this, getActualFragment().getDrawable(), withText ? gallery.sharePageUrl(pageNum) : null); } private void downloadPage() { final File output = new File(Global.SCREENFOLDER, gallery.getId() + "-" + (mViewPager.getCurrentItem() + 1) + ".jpg"); Utility.saveImage(getActualFragment().getDrawable(), output); } private void animateLayout() { AnimatorListenerAdapter adapter = new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { if (isHidden) { pageSwitcher.setVisibility(View.GONE); toolbar.setVisibility(View.GONE); view.setVisibility(View.GONE); cornerPageViewer.setVisibility(View.VISIBLE); } } }; pageSwitcher.setVisibility(View.VISIBLE); toolbar.setVisibility(View.VISIBLE); view.setVisibility(View.VISIBLE); cornerPageViewer.setVisibility(View.GONE); pageSwitcher.animate().alpha(isHidden ? 0f : 0.75f).setDuration(150).setListener(adapter).start(); view.animate().alpha(isHidden ? 0f : 0.75f).setDuration(150).setListener(adapter).start(); toolbar.animate().alpha(isHidden ? 0f : 0.75f).setDuration(150).setListener(adapter).start(); } private void applyVisibilityFlag() { getWindow().getDecorView().setSystemUiVisibility(isHidden ? hideFlags : showFlags); } private enum ScrollType {HORIZONTAL} public class SectionsPagerAdapter extends FragmentStateAdapter { public SectionsPagerAdapter(ZoomActivity activity) { super(activity.getSupportFragmentManager(), activity.getLifecycle()); } private boolean allowScroll = true; @NonNull @Override public Fragment createFragment(int position) { ZoomFragment f = ZoomFragment.newInstance(gallery, position, directory); f.setZoomChangeListener((v, zoomLevel) -> { try { boolean _allowScroll = zoomLevel < 1.1f; if (_allowScroll != allowScroll) { setUserInput(!allowScroll); allowScroll = _allowScroll; } } catch (Exception ignored) { } }); f.setClickListener(v -> { isHidden = !isHidden; LogUtility.d("Clicked " + isHidden); applyVisibilityFlag(); animateLayout(); }); return f; } @Override public int getItemCount() { return gallery.getPageCount(); } } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/adapters/BookmarkAdapter.java ================================================ package com.maxwai.nclientv3.adapters; import android.content.Intent; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.appcompat.widget.AppCompatImageButton; import androidx.constraintlayout.widget.ConstraintLayout; import androidx.recyclerview.widget.RecyclerView; import com.maxwai.nclientv3.BookmarkActivity; import com.maxwai.nclientv3.MainActivity; import com.maxwai.nclientv3.R; import com.maxwai.nclientv3.async.database.Queries; import com.maxwai.nclientv3.components.classes.Bookmark; import com.maxwai.nclientv3.utility.IntentUtility; import java.util.List; public class BookmarkAdapter extends RecyclerView.Adapter { private static final int LAYOUT = R.layout.bookmark_layout; private final List bookmarks; private final BookmarkActivity bookmarkActivity; public BookmarkAdapter(BookmarkActivity bookmarkActivity) { this.bookmarkActivity = bookmarkActivity; this.bookmarks = Queries.BookmarkTable.getBookmarks(); } @NonNull @Override public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { return new ViewHolder(LayoutInflater.from(parent.getContext()).inflate(LAYOUT, parent, false)); } @Override public void onBindViewHolder(@NonNull ViewHolder holder, int pos) { final int position = holder.getBindingAdapterPosition(); Bookmark bookmark = bookmarks.get(position); holder.queryText.setText(bookmark.toString()); holder.pageLabel.setText(bookmarkActivity.getString(R.string.bookmark_page_format, bookmark.page)); holder.deleteButton.setOnClickListener(v -> removeBookmarkAtPosition(position)); holder.rootLayout.setOnClickListener(v -> loadBookmark(bookmark)); } /** * Start an {@link MainActivity} with bookmark as query and page * * @param bookmark bookmark to load */ private void loadBookmark(Bookmark bookmark) { Intent i = new Intent(bookmarkActivity, MainActivity.class); i.putExtra(bookmarkActivity.getPackageName() + ".BYBOOKMARK", true); i.putExtra(bookmarkActivity.getPackageName() + ".INSPECTOR", bookmark.createInspector(bookmarkActivity, null)); IntentUtility.startAnotherActivity(bookmarkActivity, i); } /** * remove bookmark from the adapter at position * * @param position index to delete */ private void removeBookmarkAtPosition(int position) { if (position >= bookmarks.size()) return; Bookmark bookmark = bookmarks.get(position); bookmark.deleteBookmark(); bookmarks.remove(bookmark); bookmarkActivity.runOnUiThread(() -> notifyItemRemoved(position)); } @Override public int getItemCount() { return bookmarks.size(); } public static class ViewHolder extends RecyclerView.ViewHolder { final AppCompatImageButton deleteButton; final TextView queryText; final TextView pageLabel; final ConstraintLayout rootLayout; ViewHolder(@NonNull View itemView) { super(itemView); deleteButton = itemView.findViewById(R.id.remove_button); pageLabel = itemView.findViewById(R.id.page); queryText = itemView.findViewById(R.id.title); rootLayout = itemView.findViewById(R.id.master_layout); } } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/adapters/CommentAdapter.java ================================================ package com.maxwai.nclientv3.adapters; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ImageButton; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatActivity; import androidx.constraintlayout.widget.ConstraintLayout; import androidx.recyclerview.widget.RecyclerView; import com.maxwai.nclientv3.R; import com.maxwai.nclientv3.api.comments.Comment; import com.maxwai.nclientv3.settings.Global; import com.maxwai.nclientv3.settings.Login; import com.maxwai.nclientv3.utility.ImageDownloadUtility; import com.maxwai.nclientv3.utility.Utility; import java.io.IOException; import java.text.DateFormat; import java.util.ArrayList; import java.util.List; import java.util.Locale; import okhttp3.Call; import okhttp3.Callback; import okhttp3.Request; import okhttp3.Response; public class CommentAdapter extends RecyclerView.Adapter { private final List comments; private final DateFormat format; private final int userId; private final AppCompatActivity context; public CommentAdapter(AppCompatActivity context, List comments) { this.context = context; format = android.text.format.DateFormat.getDateFormat(context); this.comments = comments == null ? new ArrayList<>() : comments; if (Login.isLogged() && Login.getUser() != null) { userId = Login.getUser().getId(); } else userId = -1; } @NonNull @Override public CommentAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { return new ViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.comment_layout, parent, false)); } @Override public void onBindViewHolder(@NonNull CommentAdapter.ViewHolder holder, int pos) { int position = holder.getBindingAdapterPosition(); Comment c = comments.get(position); holder.layout.setOnClickListener(v1 -> context.runOnUiThread(() -> holder.body.setMaxLines(holder.body.getMaxLines() == 7 ? 999 : 7))); holder.close.setVisibility(c.getPosterId() != userId ? View.GONE : View.VISIBLE); holder.user.setText(c.getUsername()); holder.body.setText(c.getComment()); holder.date.setText(format.format(c.getPostDate())); holder.close.setOnClickListener(v -> { String submitUrl = String.format(Locale.US, Utility.getApiBaseUrl() + "comments/%d", c.getId()); Global.getClient(context) .newCall(new Request.Builder().url(submitUrl).delete().build()) .enqueue(new Callback() { @Override public void onFailure(@NonNull Call call, @NonNull IOException e) { } @Override public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException { if (response.body().string().contains("true")) { comments.remove(position); context.runOnUiThread(() -> notifyItemRemoved(position)); } } }); }); if (c.getAvatarUrl() == null || Global.getDownloadPolicy() != Global.DataUsageType.FULL) ImageDownloadUtility.loadImage(R.drawable.ic_person, holder.userImage); else ImageDownloadUtility.loadImage(context, c.getAvatarUrl(), holder.userImage); } @Override public int getItemCount() { return comments.size(); } public void addComment(Comment c) { comments.add(0, c); context.runOnUiThread(() -> notifyItemInserted(0)); } public static class ViewHolder extends RecyclerView.ViewHolder { final ImageButton userImage; final ImageButton close; final TextView user; final TextView body; final TextView date; final ConstraintLayout layout; public ViewHolder(@NonNull View v) { super(v); layout = v.findViewById(R.id.master_layout); userImage = v.findViewById(R.id.propic); close = v.findViewById(R.id.close); user = v.findViewById(R.id.username); body = v.findViewById(R.id.body); date = v.findViewById(R.id.date); } } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/adapters/FavoriteAdapter.java ================================================ package com.maxwai.nclientv3.adapters; import android.annotation.SuppressLint; import android.content.Intent; import android.database.Cursor; import android.text.Layout; import android.util.SparseIntArray; import android.view.LayoutInflater; import android.view.ViewGroup; import android.widget.Filter; import android.widget.Filterable; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.recyclerview.widget.RecyclerView; import com.maxwai.nclientv3.FavoriteActivity; import com.maxwai.nclientv3.GalleryActivity; import com.maxwai.nclientv3.R; import com.maxwai.nclientv3.api.components.Gallery; import com.maxwai.nclientv3.async.database.Queries; import com.maxwai.nclientv3.settings.Global; import com.maxwai.nclientv3.utility.ImageDownloadUtility; import com.maxwai.nclientv3.utility.LogUtility; import com.maxwai.nclientv3.utility.Utility; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Locale; public class FavoriteAdapter extends RecyclerView.Adapter implements Filterable { private final int perPage = FavoriteActivity.getEntryPerPage(); private final SparseIntArray statuses = new SparseIntArray(); private final FavoriteActivity activity; private Gallery[] galleries; private CharSequence lastQuery; private Cursor cursor; private boolean force = false; private boolean sortByTitle = false; public FavoriteAdapter(FavoriteActivity activity) { this.activity = activity; this.lastQuery = ""; setHasStableIds(true); } @SuppressLint("Range") @Override public long getItemId(int position) { cursor.moveToPosition(position); return cursor.getInt(cursor.getColumnIndex(Queries.GalleryTable.IDGALLERY)); } @NonNull @Override public GenericAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { return new GenericAdapter.ViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.entry_layout, parent, false)); } @Nullable private Gallery galleryFromPosition(int position) { if (galleries[position] != null) return galleries[position]; cursor.moveToPosition(position); Gallery g = Queries.GalleryTable.cursorToGallery(activity, cursor); galleries[position] = g; if (g.getGalleryData().hasUpdatedInfo()) { // TODO: to be removed in next major version if (g.getGalleryData().isDeleted()) { LogUtility.w("Deleting Gallery " + g.getTitle() + " with id " + g.getId() + " since not available anymore"); Queries.GalleryTable.delete(g.getId()); } else { Queries.GalleryTable.insert(g); } } return g; } @Override public void onBindViewHolder(@NonNull final GenericAdapter.ViewHolder holder, int position) { final Gallery ent = galleryFromPosition(holder.getBindingAdapterPosition()); if (ent == null) return; ImageDownloadUtility.loadImage(activity, ent.getThumbnail(), holder.imgView); holder.pages.setText(String.format(Locale.US, "%d", ent.getPageCount())); holder.title.setText(ent.getTitle()); holder.flag.setText(Global.getLanguageFlag(ent.getLanguage())); holder.title.setOnClickListener(v -> { Layout layout = holder.title.getLayout(); if (layout.getEllipsisCount(layout.getLineCount() - 1) > 0) holder.title.setMaxLines(7); else if (holder.title.getMaxLines() == 7) holder.title.setMaxLines(3); else holder.layout.performClick(); }); holder.layout.setOnClickListener(v -> { if (ent.getGalleryData().isValid()) startGallery(ent); }); holder.layout.setOnLongClickListener(v -> { holder.title.animate().alpha(holder.title.getAlpha() == 0f ? 1f : 0f).setDuration(100).start(); holder.flag.animate().alpha(holder.flag.getAlpha() == 0f ? 1f : 0f).setDuration(100).start(); holder.pages.animate().alpha(holder.pages.getAlpha() == 0f ? 1f : 0f).setDuration(100).start(); return true; }); int statusColor = statuses.get(ent.getId(), 0); if (statusColor == 0) { statusColor = Queries.StatusMangaTable.getStatus(ent.getId()).color; statuses.put(ent.getId(), statusColor); } holder.title.setBackgroundColor(statusColor); } private void startGallery(Gallery ent) { Intent intent = new Intent(activity, GalleryActivity.class); LogUtility.d(ent + ""); intent.putExtra(activity.getPackageName() + ".GALLERY", ent); intent.putExtra(activity.getPackageName() + ".UNKNOWN", true); activity.startActivity(intent); } public void changePage() { forceReload(); } @Override public int getItemCount() { return cursor == null ? 0 : cursor.getCount(); } @Override public Filter getFilter() { return new Filter() { @Override protected FilterResults performFiltering(CharSequence constraint) { constraint = constraint.toString().toLowerCase(Locale.US); if ((!force && lastQuery.equals(constraint))) return null; LogUtility.d("FILTERING"); setRefresh(true); FilterResults results = new FilterResults(); lastQuery = constraint.toString(); LogUtility.d(lastQuery + "LASTQERY"); force = false; Cursor c = Queries.FavoriteTable.getAllFavoriteGalleriesCursor(lastQuery, sortByTitle, perPage, (activity.getActualPage() - 1) * perPage); results.count = c.getCount(); results.values = c; LogUtility.d("FILTERING3"); LogUtility.d(results.count + ";" + results.values); setRefresh(false); return results; } @Override protected void publishResults(CharSequence constraint, FilterResults results) { if (results == null) return; setRefresh(true); final int oldSize = getItemCount(), newSize = results.count; updateCursor((Cursor) results.values); //not in runOnUIThread because is always executed on UI if (oldSize > newSize) notifyItemRangeRemoved(newSize, oldSize - newSize); else notifyItemRangeInserted(oldSize, newSize - oldSize); notifyItemRangeChanged(0, Math.min(newSize, oldSize)); setRefresh(false); } }; } public void setSortByTitle(boolean sortByTitle) { this.sortByTitle = sortByTitle; forceReload(); } public void forceReload() { force = true; activity.runOnUiThread(() -> getFilter().filter(lastQuery)); } public void setRefresh(boolean refresh) { activity.runOnUiThread(() -> activity.getRefresher().setRefreshing(refresh)); } private void updateCursor(@Nullable Cursor c) { if (cursor != null) cursor.close(); galleries = new Gallery[c == null ? 0 : c.getCount()]; cursor = c; statuses.clear(); } public Collection getAllGalleries() { if (cursor == null) return Collections.emptyList(); int count = cursor.getCount(); ArrayList galleries = new ArrayList<>(count); for (int i = 0; i < count; i++) galleries.add(galleryFromPosition(i)); return galleries; } public void randomGallery() { if (cursor == null || cursor.getCount() < 1) return; startGallery(galleryFromPosition(Utility.RANDOM.nextInt(cursor.getCount()))); } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/adapters/GalleryAdapter.java ================================================ package com.maxwai.nclientv3.adapters; import android.content.Intent; import android.util.SparseIntArray; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ArrayAdapter; import android.widget.ImageView; import android.widget.TextView; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import com.maxwai.nclientv3.CopyToClipboardActivity; import com.maxwai.nclientv3.GalleryActivity; import com.maxwai.nclientv3.MainActivity; import com.maxwai.nclientv3.R; import com.maxwai.nclientv3.ZoomActivity; import com.maxwai.nclientv3.api.components.Gallery; import com.maxwai.nclientv3.api.components.GalleryData; import com.maxwai.nclientv3.api.components.GenericGallery; import com.maxwai.nclientv3.api.components.Tag; import com.maxwai.nclientv3.api.components.TagList; import com.maxwai.nclientv3.api.enums.SpecialTagIds; import com.maxwai.nclientv3.api.enums.TagType; import com.maxwai.nclientv3.api.local.LocalGallery; import com.maxwai.nclientv3.async.database.Queries; import com.maxwai.nclientv3.components.widgets.CustomGridLayoutManager; import com.maxwai.nclientv3.files.GalleryFolder; import com.maxwai.nclientv3.github.chrisbanes.photoview.PhotoView; import com.maxwai.nclientv3.settings.Global; import com.maxwai.nclientv3.utility.ImageDownloadUtility; import com.maxwai.nclientv3.utility.LogUtility; import com.maxwai.nclientv3.utility.Utility; import com.google.android.material.chip.Chip; import com.google.android.material.chip.ChipGroup; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import java.io.File; import java.util.ArrayList; import java.util.Locale; public class GalleryAdapter extends RecyclerView.Adapter { private static final int[] TAG_NAMES = { R.string.unknown, R.string.tag_parody_gallery, R.string.tag_character_gallery, R.string.tag_tag_gallery, R.string.tag_artist_gallery, R.string.tag_group_gallery, R.string.tag_language_gallery, R.string.tag_category_gallery, }; private final SparseIntArray angles = new SparseIntArray(); private final GalleryActivity context; private final GenericGallery gallery; private GalleryFolder directory = null; private Policy policy; private int colCount; public GalleryAdapter(GalleryActivity cont, GenericGallery gallery, int colCount) { this.context = cont; this.gallery = gallery; setColCount(colCount); try { if (gallery instanceof LocalGallery) { directory = gallery.getGalleryFolder(); } else if (Global.hasStoragePermission(cont)) { if (gallery.getId() != -1) { File f = Global.findGalleryFolder(context, gallery.getId()); if (f != null) directory = new GalleryFolder(f); } else { directory = new GalleryFolder(gallery.getTitle()); } } } catch (IllegalArgumentException ignore) { directory = null; } LogUtility.d("Max maxSize: " + gallery.getMaxSize() + ", min maxSize: " + gallery.getMinSize()); } public Type positionToType(int pos) { if (pos == 0) return Type.TAG; if (pos > gallery.getPageCount()) return Type.RELATED; return Type.PAGE; } public void setColCount(int colCount) { this.colCount = colCount; applyProportionPolicy(); } private void applyProportionPolicy() { if (colCount == 1) policy = Policy.FULL; else policy = Policy.PROPORTION; LogUtility.d("NEW POLICY: " + policy); } public GalleryFolder getDirectory() { return directory; } @NonNull @Override public GalleryAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { int id = 0; switch (Type.values()[viewType]) { case TAG: id = R.layout.tags_layout; break; case PAGE: switch (policy) { case FULL: id = R.layout.image_void_full; break; case PROPORTION: id = R.layout.image_void_static; break; } break; case RELATED: id = R.layout.related_recycler; break; } return new GalleryAdapter.ViewHolder(LayoutInflater.from(parent.getContext()).inflate(id, parent, false), Type.values()[viewType]); } @Override public void onBindViewHolder(@NonNull final GalleryAdapter.ViewHolder holder, int position) { switch (positionToType(holder.getBindingAdapterPosition())) { case TAG: loadTagLayout(holder); break; case PAGE: loadPageLayout(holder); break; case RELATED: loadRelatedLayout(holder); break; } } private void loadRelatedLayout(ViewHolder holder) { LogUtility.d("Called RElated"); final RecyclerView recyclerView = holder.master.findViewById(R.id.recycler); if (gallery.isLocal()) { holder.master.setVisibility(View.GONE); return; } final Gallery gallery = (Gallery) this.gallery; if (!gallery.isRelatedLoaded() || gallery.getRelated().isEmpty()) { holder.master.setVisibility(View.GONE); return; } else holder.master.setVisibility(View.VISIBLE); recyclerView.setLayoutManager(new CustomGridLayoutManager(context, 1, RecyclerView.HORIZONTAL, false)); if (gallery.isRelatedLoaded()) { ListAdapter adapter = new ListAdapter(context); adapter.addGalleries(new ArrayList<>(gallery.getRelated())); recyclerView.setAdapter(adapter); } } private void loadTagLayout(ViewHolder holder) { final ViewGroup vg = holder.master.findViewById(R.id.tag_master); final TextView idContainer = holder.master.findViewById(R.id.id_num); initializeIdContainer(idContainer); if (!hasTags()) { ViewGroup.LayoutParams layoutParams = vg.getLayoutParams(); layoutParams.height = 0; vg.setLayoutParams(layoutParams); return; } final LayoutInflater inflater = context.getLayoutInflater(); int tagCount, idStringTagName; ViewGroup lay; ChipGroup cg; TagList tagList = this.gallery.getGalleryData().getTags(); for (TagType type : TagType.values) { idStringTagName = TAG_NAMES[type.getId()]; tagCount = tagList.getCount(type); lay = (ViewGroup) vg.getChildAt(type.getId()); cg = lay.findViewById(R.id.chip_group); if (cg.getChildCount() != 0) continue; lay.setVisibility(tagCount == 0 ? View.GONE : View.VISIBLE); ((TextView) lay.findViewById(R.id.title)).setText(idStringTagName); for (int a = 0; a < tagCount; a++) { final Tag tag = tagList.getTag(type, a); Chip c = (Chip) inflater.inflate(R.layout.chip_layout, cg, false); c.setText(tag.getName()); c.setOnClickListener(v -> { Intent intent = new Intent(context, MainActivity.class); intent.putExtra(context.getPackageName() + ".TAG", tag); intent.putExtra(context.getPackageName() + ".ISBYTAG", true); context.startActivity(intent); }); c.setOnLongClickListener(v -> { CopyToClipboardActivity.copyTextToClipboard(context, tag.getName()); Toast.makeText(context, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show(); return true; }); cg.addView(c); } addInfoLayout(holder, gallery.getGalleryData()); } } private void initializeIdContainer(TextView idContainer) { if (gallery.getId() <= 0) { idContainer.setVisibility(View.GONE); return; } String id = Integer.toString(gallery.getId()); idContainer.setText(id); idContainer.setVisibility(gallery.getId() != SpecialTagIds.INVALID_ID ? View.VISIBLE : View.GONE); idContainer.setOnClickListener(v -> { CopyToClipboardActivity.copyTextToClipboard(context, id); context.runOnUiThread(() -> Toast.makeText(context, R.string.id_copied_to_clipboard, Toast.LENGTH_SHORT).show() ); }); } private void addInfoLayout(ViewHolder holder, GalleryData gallery) { TextView text = holder.master.findViewById(R.id.page_count); text.setText(context.getString(R.string.page_count_format, gallery.getPageCount())); text = holder.master.findViewById(R.id.upload_date); text.setText( context.getString(R.string.upload_date_format, android.text.format.DateFormat.getDateFormat(context).format(gallery.getUploadDate()), android.text.format.DateFormat.getTimeFormat(context).format(gallery.getUploadDate()) )); text = holder.master.findViewById(R.id.favorite_count); text.setText(context.getString(R.string.favorite_count_format, gallery.getFavoriteCount())); } private void loadPageLayout(ViewHolder holder) { final int pos = holder.getBindingAdapterPosition(); final ImageView imgView = holder.master.findViewById(R.id.image); imgView.setOnClickListener(v -> startGallery(holder.getBindingAdapterPosition())); imgView.setOnLongClickListener(null); holder.master.setOnClickListener(v -> startGallery(holder.getBindingAdapterPosition())); holder.master.setOnLongClickListener(null); holder.pageNumber.setText(String.format(Locale.US, "%d", pos)); if (policy == Policy.FULL) { PhotoView photoView = (PhotoView) imgView; photoView.setZoomable(Global.isZoomOneColumn()); photoView.setOnMatrixChangeListener(rect -> photoView.setAllowParentInterceptOnEdge(photoView.getScale() <= 1f)); photoView.setOnClickListener(v -> { if (photoView.getScale() <= 1f) startGallery(holder.getBindingAdapterPosition()); }); View.OnLongClickListener listener = v -> { optionDialog(imgView, pos); return true; }; imgView.setOnLongClickListener(listener); holder.master.setOnLongClickListener(listener); } loadImageOnPolicy(imgView, pos); } private void optionDialog(ImageView imgView, final int pos) { ArrayAdapter adapter = new ArrayAdapter<>(context, android.R.layout.select_dialog_item); adapter.add(context.getString(R.string.share)); adapter.add(context.getString(R.string.rotate_image)); adapter.add(context.getString(R.string.bookmark_here)); if (Global.hasStoragePermission(context)) adapter.add(context.getString(R.string.save_page)); MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(context); builder.setTitle(R.string.settings).setIcon(R.drawable.ic_share); builder.setAdapter(adapter, (dialog, which) -> { switch (which) { case 0: openSendImageDialog(imgView, pos); break; case 1: rotate(pos); break; case 2: Queries.ResumeTable.insert(gallery.getId(), pos); break; case 3: String name = String.format(Locale.US, "%d-%d.jpg", gallery.getId(), pos); Utility.saveImage(imgView.getDrawable(), new File(Global.SCREENFOLDER, name)); break; } }).show(); } private void openSendImageDialog(ImageView img, int pos) { MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(context); builder.setPositiveButton(R.string.yes, (dialog, which) -> sendImage(img, pos, true)) .setNegativeButton(R.string.no, (dialog, which) -> sendImage(img, pos, false)) .setCancelable(true).setTitle(R.string.send_with_title) .setMessage(R.string.caption_send_with_title) .show(); } private void sendImage(ImageView img, int pos, boolean text) { Utility.sendImage(context, img.getDrawable(), text ? gallery.sharePageUrl(pos - 1) : null); } private void rotate(int pos) { angles.append(pos, (angles.get(pos) + 270) % 360); context.runOnUiThread(() -> notifyItemChanged(pos)); } private void startGallery(int page) { if (!gallery.isLocal() && Global.getDownloadPolicy() == Global.DataUsageType.NONE) { context.runOnUiThread(() -> Toast.makeText(context, R.string.enable_network_to_continue, Toast.LENGTH_SHORT).show() ); return; } Intent intent = new Intent(context, ZoomActivity.class); intent.putExtra(context.getPackageName() + ".GALLERY", gallery); intent.putExtra(context.getPackageName() + ".DIRECTORY", directory); intent.putExtra(context.getPackageName() + ".PAGE", page); context.startActivity(intent); } private void loadImageOnPolicy(ImageView imgView, int pos) { final File file = directory == null ? null : directory.getPage(pos); int angle = angles.get(pos); if (policy == Policy.FULL) { if (file != null && file.exists()) ImageDownloadUtility.loadImageOp(context, imgView, file, angle); else if (!gallery.isLocal()) { Gallery ent = (Gallery) gallery; ImageDownloadUtility.loadImageOp(context, imgView, ent, pos - 1, angle); } else ImageDownloadUtility.loadImage(R.mipmap.ic_launcher, imgView); } else { if (file != null && file.exists()) ImageDownloadUtility.loadImage(context, file, imgView); else if (!gallery.isLocal()) { Gallery ent = (Gallery) gallery; ImageDownloadUtility.downloadPage(context, imgView, ent, pos - 1, false); } else ImageDownloadUtility.loadImage(R.mipmap.ic_launcher, imgView); } } private boolean hasTags() { return gallery.hasGalleryData(); } @Override public int getItemViewType(int position) { return positionToType(position).ordinal(); } @Override public int getItemCount() { return gallery.getPageCount() + 2; } public enum Type {TAG, PAGE, RELATED} public enum Policy {PROPORTION, FULL} public static class ViewHolder extends RecyclerView.ViewHolder { final View master; final TextView pageNumber; ViewHolder(View v, Type type) { super(v); master = v.findViewById(R.id.master); pageNumber = v.findViewById(R.id.page_number); if (Global.useRtl()) v.setRotationY(180); if (type == Type.RELATED) Global.applyFastScroller(master.findViewById(R.id.recycler)); } } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/adapters/GenericAdapter.java ================================================ package com.maxwai.nclientv3.adapters; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.Filter; import android.widget.Filterable; import android.widget.ImageView; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import com.maxwai.nclientv3.R; import com.maxwai.nclientv3.api.components.GenericGallery; import java.util.ArrayList; import java.util.Comparator; import java.util.List; import java.util.Locale; public abstract class GenericAdapter extends RecyclerView.Adapter implements Filterable { final List dataset; List filter; String lastQuery = ""; GenericAdapter(List dataset) { this.dataset = dataset; dataset.sort(Comparator.comparing(GenericGallery::getTitle)); filter = new ArrayList<>(dataset); } @NonNull @Override public GenericAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { return new GenericAdapter.ViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.entry_layout, parent, false)); } @Override public int getItemCount() { return filter.size(); } @Override public Filter getFilter() { return new Filter() { @Override protected FilterResults performFiltering(CharSequence constraint) { String query = constraint.toString().toLowerCase(Locale.US); if (lastQuery.equals(query)) return null; FilterResults results = new FilterResults(); results.count = filter.size(); lastQuery = query; List filter = new ArrayList<>(); for (T gallery : dataset) if (gallery.getTitle().toLowerCase(Locale.US).contains(query)) filter.add(gallery); results.values = filter; return results; } @SuppressWarnings("unchecked") @Override protected void publishResults(CharSequence constraint, FilterResults results) { if (results != null && results.values instanceof List) { for (Object tmp : (List) results.values) { try { //noinspection unused T _tmp = (T) tmp; } catch (ClassCastException ignored) { return; } } filter = (List) results.values; if (filter.size() > results.count) notifyItemRangeInserted(results.count, filter.size() - results.count); else if (filter.size() < results.count) notifyItemRangeRemoved(filter.size(), results.count - filter.size()); notifyItemRangeRemoved(filter.size(), results.count); notifyItemRangeChanged(0, filter.size() - 1); } } }; } public static class ViewHolder extends RecyclerView.ViewHolder { final ImageView imgView; final View overlay; final TextView title, pages, flag; final View layout; ViewHolder(View v) { super(v); imgView = v.findViewById(R.id.image); title = v.findViewById(R.id.title); pages = v.findViewById(R.id.pages); layout = v.findViewById(R.id.master_layout); flag = v.findViewById(R.id.flag); overlay = v.findViewById(R.id.overlay); } } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/adapters/HistoryAdapter.java ================================================ package com.maxwai.nclientv3.adapters; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ImageButton; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.constraintlayout.widget.ConstraintLayout; import androidx.recyclerview.widget.RecyclerView; import com.maxwai.nclientv3.R; import com.maxwai.nclientv3.SearchActivity; import com.maxwai.nclientv3.components.classes.History; import com.maxwai.nclientv3.settings.Global; import com.maxwai.nclientv3.utility.ImageDownloadUtility; import java.util.HashSet; import java.util.List; public class HistoryAdapter extends RecyclerView.Adapter { private final List history; private final SearchActivity context; private int remove = -1; public HistoryAdapter(SearchActivity context) { this.context = context; if (!Global.isKeepHistory()) context.getSharedPreferences("History", 0).edit().clear().apply(); history = Global.isKeepHistory() ? History.setToList(context.getSharedPreferences("History", 0).getStringSet("history", new HashSet<>())) : null; } @NonNull @Override public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { return new ViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.entry_history, parent, false)); } @Override public void onBindViewHolder(@NonNull ViewHolder holder, int position) { ImageDownloadUtility.loadImage(remove == holder.getBindingAdapterPosition() ? R.drawable.ic_close : R.drawable.ic_mode_edit, holder.imageButton); String entry = history.get(holder.getBindingAdapterPosition()).getValue(); holder.text.setText(entry); holder.master.setOnClickListener(v -> context.setQuery(entry, true)); holder.imageButton.setOnLongClickListener(v -> { context.runOnUiThread(() -> { if (remove == holder.getBindingAdapterPosition()) { remove = -1; notifyItemChanged(holder.getBindingAdapterPosition()); } else { if (remove != -1) { int l = remove; remove = -1; notifyItemChanged(l); } remove = holder.getBindingAdapterPosition(); notifyItemChanged(holder.getBindingAdapterPosition()); } }); return true; }); holder.imageButton.setOnClickListener(v -> { if (remove == holder.getBindingAdapterPosition()) { removeHistory(remove); remove = -1; } else { context.setQuery(entry, false); } }); } @Override public int getItemCount() { return history == null ? 0 : history.size(); } public void addHistory(String value) { if (!Global.isKeepHistory()) return; History history = new History(value, false); int pos = this.history.indexOf(history); if (pos >= 0) this.history.set(pos, history); else this.history.add(history); context.getSharedPreferences("History", 0).edit().putStringSet("history", History.listToSet(this.history)).apply(); } public void removeHistory(int pos) { if (pos < 0 || pos >= history.size()) return; history.remove(pos); context.getSharedPreferences("History", 0).edit().putStringSet("history", History.listToSet(this.history)).apply(); context.runOnUiThread(() -> notifyItemRemoved(pos)); } public static class ViewHolder extends RecyclerView.ViewHolder { final ConstraintLayout master; final TextView text; final ImageButton imageButton; public ViewHolder(@NonNull View itemView) { super(itemView); this.master = itemView.findViewById(R.id.master_layout); this.text = itemView.findViewById(R.id.text); this.imageButton = itemView.findViewById(R.id.edit); } } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/adapters/ListAdapter.java ================================================ package com.maxwai.nclientv3.adapters; import android.content.Intent; import android.text.Layout; import android.util.SparseIntArray; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.cardview.widget.CardView; import androidx.recyclerview.widget.RecyclerView; import com.maxwai.nclientv3.GalleryActivity; import com.maxwai.nclientv3.MainActivity; import com.maxwai.nclientv3.R; import com.maxwai.nclientv3.api.InspectorV3; import com.maxwai.nclientv3.api.SimpleGallery; import com.maxwai.nclientv3.api.components.GenericGallery; import com.maxwai.nclientv3.api.enums.Language; import com.maxwai.nclientv3.async.database.Queries; import com.maxwai.nclientv3.components.activities.BaseActivity; import com.maxwai.nclientv3.settings.Global; import com.maxwai.nclientv3.settings.TagV2; import com.maxwai.nclientv3.utility.ImageDownloadUtility; import com.maxwai.nclientv3.utility.LogUtility; import com.google.android.material.snackbar.Snackbar; import java.io.File; import java.util.ArrayList; import java.util.List; import java.util.Locale; public class ListAdapter extends RecyclerView.Adapter { private final SparseIntArray statuses = new SparseIntArray(); private final List mDataset; private final BaseActivity context; private final String queryString; public ListAdapter(BaseActivity cont) { this.context = cont; this.mDataset = new ArrayList<>() { @Override public SimpleGallery get(int index) { try { return super.get(index); } catch (ArrayIndexOutOfBoundsException ignore) { return null; } } }; queryString = TagV2.getAvoidedTags(); } @NonNull @Override public GenericAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { return new GenericAdapter.ViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.entry_layout, parent, false)); } private void loadGallery(final GenericAdapter.ViewHolder holder, SimpleGallery ent) { if (context.isFinishing()) return; try { if (Global.isDestroyed(context)) return; ImageDownloadUtility.loadImage(context, ent.getThumbnail(), holder.imgView); } catch (VerifyError ignore) { } } @Override public void onBindViewHolder(@NonNull final GenericAdapter.ViewHolder holder, int position) { int holderPos = holder.getBindingAdapterPosition(); if (holderPos >= mDataset.size()) return; final SimpleGallery ent = mDataset.get(holderPos); if (ent == null) return; if (!Global.showTitles()) { holder.title.setAlpha(0f); holder.flag.setAlpha(0f); } else { holder.title.setAlpha(1f); holder.flag.setAlpha(1f); } if (context instanceof GalleryActivity) { CardView card = (CardView) holder.layout.getParent(); ViewGroup.LayoutParams params = card.getLayoutParams(); params.width = Global.getGalleryWidth(); params.height = Global.getGalleryHeight(); card.setLayoutParams(params); } holder.overlay.setVisibility((queryString != null && ent.hasIgnoredTags(queryString)) ? View.VISIBLE : View.GONE); loadGallery(holder, ent); holder.pages.setVisibility(View.GONE); holder.title.setText(ent.getTitle()); holder.flag.setVisibility(View.VISIBLE); if (Global.getOnlyLanguage() == Language.ALL || context instanceof GalleryActivity) { holder.flag.setText(Global.getLanguageFlag(ent.getLanguage())); } else holder.flag.setVisibility(View.GONE); holder.title.setOnClickListener(v -> { Layout layout = holder.title.getLayout(); if (layout.getEllipsisCount(layout.getLineCount() - 1) > 0) holder.title.setMaxLines(7); else if (holder.title.getMaxLines() == 7) holder.title.setMaxLines(3); else holder.layout.performClick(); }); holder.layout.setOnClickListener(v -> { /*Intent intent = new Intent(context, GalleryActivity.class); intent.putExtra(context.getPackageName() + ".ID", ent.getId()); context.startActivity(intent);*/ if (context instanceof MainActivity) ((MainActivity) context).setIdOpenedGallery(ent.getId()); downloadGallery(ent); holder.overlay.setVisibility((queryString != null && ent.hasIgnoredTags(queryString)) ? View.VISIBLE : View.GONE); }); holder.overlay.setOnClickListener(v -> holder.overlay.setVisibility(View.GONE)); holder.layout.setOnLongClickListener(v -> { holder.title.animate().alpha(holder.title.getAlpha() == 0f ? 1f : 0f).setDuration(100).start(); holder.flag.animate().alpha(holder.flag.getAlpha() == 0f ? 1f : 0f).setDuration(100).start(); holder.pages.animate().alpha(holder.pages.getAlpha() == 0f ? 1f : 0f).setDuration(100).start(); return true; }); int statusColor = statuses.get(ent.getId(), 0); if (statusColor == 0) { statusColor = Queries.StatusMangaTable.getStatus(ent.getId()).color; statuses.put(ent.getId(), statusColor); } holder.title.setBackgroundColor(statusColor); } public void updateColor(int id) { if (id < 0) return; int position = -1; statuses.put(id, Queries.StatusMangaTable.getStatus(id).color); for (int i = 0; i < mDataset.size(); i++) { SimpleGallery gallery= mDataset.get(i); if (gallery != null && gallery.getId() == id) { position = id; break; } } if (position >= 0) notifyItemChanged(position); } private void downloadGallery(final SimpleGallery ent) { InspectorV3.galleryInspector(context, ent.getId(), new InspectorV3.DefaultInspectorResponse() { @Override public void onFailure(Exception e) { super.onFailure(e); File file = Global.findGalleryFolder(context, ent.getId()); if (file != null) { LocalAdapter.startGallery(context, file); } else if (context.getMasterLayout() != null) { context.runOnUiThread(() -> { Snackbar snackbar = Snackbar.make(context.getMasterLayout(), R.string.unable_to_connect_to_the_site, Snackbar.LENGTH_SHORT); snackbar.setAction(R.string.retry, v -> downloadGallery(ent)); snackbar.show(); } ); } } @Override public void onSuccess(List galleries) { if (galleries.size() != 1) { if (context.getMasterLayout() != null) { context.runOnUiThread(() -> Snackbar.make(context.getMasterLayout(), R.string.no_entry_found, Snackbar.LENGTH_SHORT).show() ); } return; } Intent intent = new Intent(context, GalleryActivity.class); LogUtility.d(galleries.get(0).toString()); intent.putExtra(context.getPackageName() + ".GALLERY", galleries.get(0)); context.runOnUiThread(() -> context.startActivity(intent)); } }).start(); } @Override public int getItemCount() { return mDataset == null ? 0 : mDataset.size(); } public void addGalleries(List galleries) { int c = mDataset.size(); for (GenericGallery g : galleries) { mDataset.add((SimpleGallery) g); LogUtility.d("Simple: " + g); } LogUtility.d(String.format(Locale.US, "%s,old:%d,new:%d,len%d", this, c, mDataset.size(), galleries.size())); context.runOnUiThread(() -> notifyItemRangeInserted(c, galleries.size())); } public void restartDataset(List galleries) { /*int c=mDataset.size(); if(c>0) { mDataset.clear(); context.runOnUiThread(() -> notifyItemRangeRemoved(0, c)); } mDataset.addAll(galleries); context.runOnUiThread(()->notifyItemRangeInserted(0,galleries.size()));*/ mDataset.clear(); for (GenericGallery g : galleries) if (g instanceof SimpleGallery) mDataset.add((SimpleGallery) g); context.runOnUiThread(this::notifyDataSetChanged); } public void resetStatuses() { statuses.clear(); } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/adapters/LocalAdapter.java ================================================ package com.maxwai.nclientv3.adapters; import android.annotation.SuppressLint; import android.app.Activity; import android.content.Intent; import android.text.Layout; import android.util.SparseIntArray; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.Filter; import android.widget.Filterable; import android.widget.ImageButton; import android.widget.ImageView; import android.widget.ProgressBar; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.recyclerview.widget.RecyclerView; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.maxwai.nclientv3.GalleryActivity; import com.maxwai.nclientv3.LocalActivity; import com.maxwai.nclientv3.R; import com.maxwai.nclientv3.api.enums.TagType; import com.maxwai.nclientv3.api.local.LocalGallery; import com.maxwai.nclientv3.api.local.LocalSortType; import com.maxwai.nclientv3.async.converters.CreatePdfOrZip; import com.maxwai.nclientv3.async.database.Queries; import com.maxwai.nclientv3.async.downloader.DownloadGalleryV2; import com.maxwai.nclientv3.async.downloader.DownloadObserver; import com.maxwai.nclientv3.async.downloader.DownloadQueue; import com.maxwai.nclientv3.async.downloader.GalleryDownloaderV2; import com.maxwai.nclientv3.components.classes.MultichoiceAdapter; import com.maxwai.nclientv3.settings.Global; import com.maxwai.nclientv3.utility.ImageDownloadUtility; import com.maxwai.nclientv3.utility.LogUtility; import com.maxwai.nclientv3.utility.Utility; import java.io.File; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Objects; import java.util.concurrent.CopyOnWriteArrayList; public class LocalAdapter extends MultichoiceAdapter implements Filterable { private final SparseIntArray statuses = new SparseIntArray(); private final LocalActivity context; private final List dataset; private final List galleryDownloaders; private final Comparator comparatorByName = (o1, o2) -> { if (o1 == o2) return 0; boolean b1 = o1 instanceof LocalGallery; boolean b2 = o2 instanceof LocalGallery; String s1 = b1 ? ((LocalGallery) o1).getTitle() : ((GalleryDownloaderV2) o1).getPathTitle(); String s2 = b2 ? ((LocalGallery) o2).getTitle() : ((GalleryDownloaderV2) o2).getPathTitle(); return s1.compareTo(s2); }; private final Comparator comparatorBySize = (o1, o2) -> { if (o1 == o2) return 0; long page1 = o1 instanceof LocalGallery ? Global.recursiveSize(((LocalGallery) o1).getDirectory()) : 0; long page2 = o2 instanceof LocalGallery ? Global.recursiveSize(((LocalGallery) o2).getDirectory()) : 0; return Long.compare(page1, page2); }; private final Comparator comparatorByPageCount = (o1, o2) -> { if (o1 == o2) return 0; int page1 = o1 instanceof LocalGallery ? ((LocalGallery) o1).getPageCount() : 0; int page2 = o2 instanceof LocalGallery ? ((LocalGallery) o2).getPageCount() : 0; return page1 - page2; }; private final Comparator comparatorByDate = (o1, o2) -> { if (o1 == o2) return 0; boolean b1 = o1 instanceof LocalGallery; boolean b2 = o2 instanceof LocalGallery; //downloading manga are newer long d1 = b1 ? ((LocalGallery) o1).getDirectory().lastModified() : Long.MAX_VALUE; long d2 = b2 ? ((LocalGallery) o2).getDirectory().lastModified() : Long.MAX_VALUE; if (d1 != d2) return Long.compare(d1, d2); return comparatorByName.compare(o1, o2); }; private final Comparator comparatorByArtist = (o1, o2) -> { if (o1 == o2) return 0; boolean b1 = o1 instanceof LocalGallery; boolean b2 = o2 instanceof LocalGallery; String s1 = (b1 && ((LocalGallery) o1).getGalleryData().getTags().getCount(TagType.ARTIST) > 0) ? ((LocalGallery) o1).getGalleryData().getTags().getTag(TagType.ARTIST, 0).getName() : ""; String s2 = (b2 && ((LocalGallery) o2).getGalleryData().getTags().getCount(TagType.ARTIST) > 0) ? ((LocalGallery) o2).getGalleryData().getTags().getTag(TagType.ARTIST, 0).getName() : ""; if (s1.isEmpty() && !s2.isEmpty()) return 1; if (!s1.isEmpty() && s2.isEmpty()) return -1; return s1.compareTo(s2); }; private final Comparator comparatorByGroup = (o1, o2) -> { if (o1 == o2) return 0; boolean b1 = o1 instanceof LocalGallery; boolean b2 = o2 instanceof LocalGallery; String s1 = (b1 && ((LocalGallery) o1).getGalleryData().getTags().getCount(TagType.GROUP) > 0) ? ((LocalGallery) o1).getGalleryData().getTags().getTag(TagType.GROUP, 0).getName() : ""; String s2 = (b2 && ((LocalGallery) o2).getGalleryData().getTags().getCount(TagType.GROUP) > 0) ? ((LocalGallery) o2).getGalleryData().getTags().getTag(TagType.GROUP, 0).getName() : ""; if (s1.isEmpty() && !s2.isEmpty()) return 1; if (!s1.isEmpty() && s2.isEmpty()) return -1; return s1.compareTo(s2); }; private List filter; @NonNull private String lastQuery; private final DownloadObserver observer = new DownloadObserver() { private void updatePosition(GalleryDownloaderV2 downloader) { final int id = filter.indexOf(downloader); if (id >= 0) context.runOnUiThread(() -> notifyItemChanged(id)); } @Override public void triggerStartDownload(GalleryDownloaderV2 downloader) { updatePosition(downloader); } @Override public void triggerUpdateProgress(GalleryDownloaderV2 downloader, int reach, int total) { updatePosition(downloader); } @Override public void triggerEndDownload(GalleryDownloaderV2 downloader) { LocalGallery l = downloader.localGallery(); galleryDownloaders.remove(downloader); if (l != null) { dataset.remove(l); dataset.add(l); LogUtility.d(l); sortElements(); } context.runOnUiThread(() -> notifyItemRangeChanged(0, getItemCount())); } @Override public void triggerCancelDownload(GalleryDownloaderV2 downloader) { removeDownloader(downloader); } @Override public void triggerPauseDownload(GalleryDownloaderV2 downloader) { context.runOnUiThread(() -> notifyItemChanged(filter.indexOf(downloader))); } }; private int colCount; public LocalAdapter(LocalActivity cont, ArrayList myDataset) { this.context = cont; dataset = new CopyOnWriteArrayList<>(myDataset); colCount = cont.getColCount(); galleryDownloaders = DownloadQueue.getDownloaders(); lastQuery = cont.getQuery(); filter = new ArrayList<>(myDataset); filter.addAll(galleryDownloaders); DownloadQueue.addObserver(observer); sortElements(); } static void startGallery(Activity context, File directory) { if (!directory.isDirectory()) return; LocalGallery ent = new LocalGallery(directory); ent.calculateSizes(); new Thread(() -> { Intent intent = new Intent(context, GalleryActivity.class); intent.putExtra(context.getPackageName() + ".GALLERY", ent); intent.putExtra(context.getPackageName() + ".ISLOCAL", true); context.runOnUiThread(() -> context.startActivity(intent)); }).start(); } public void addGalleries(@Nullable List gallery) { if (gallery != null && !gallery.isEmpty()) { dataset.removeAll(gallery); dataset.addAll(gallery); sortElements(); context.runOnUiThread(() -> notifyItemRangeChanged(0, getItemCount())); } } @Override protected ViewGroup getMaster(ViewHolder holder) { return holder.layout; } @Override protected Object getItemAt(int position) { return filter.get(position); } private CopyOnWriteArrayList createHash(List galleryDownloaders, List dataset) { HashMap hashMap = new HashMap<>(dataset.size() + galleryDownloaders.size()); for (LocalGallery gall : dataset) { if (gall != null && gall.getTitle().toLowerCase(Locale.US).contains(lastQuery)) hashMap.put(gall.getTrueTitle(), gall); } for (GalleryDownloaderV2 gall : galleryDownloaders) { if (gall != null && gall.getPathTitle().toLowerCase(Locale.US).contains(lastQuery)) hashMap.put(gall.getTruePathTitle(), gall); } ArrayList arr = new ArrayList<>(hashMap.values()); sortItems(arr); return new CopyOnWriteArrayList<>(arr); } private void sortItems(ArrayList arr) { LocalSortType type = Global.getLocalSortType(); if (type.type == LocalSortType.Type.RANDOM) { Collections.shuffle(arr, Utility.RANDOM); } else { try { arr.sort(getComparator(type.type)); } catch (IllegalArgumentException ignore) { } if (type.descending) Collections.reverse(arr); } } private Comparator getComparator(LocalSortType.Type type) { switch (type) { case DATE: return comparatorByDate; case TITLE: return comparatorByName; case PAGE_COUNT: return comparatorByPageCount; case ARTIST: return comparatorByArtist; case GROUP: return comparatorByGroup; //case SIZE:return comparatorBySize; } return comparatorByName; } public void setColCount(int colCount) { this.colCount = colCount; } private void sortElements() { filter = createHash(galleryDownloaders, dataset); } @NonNull @Override protected ViewHolder onCreateMultichoiceViewHolder(@NonNull ViewGroup parent, int viewType) { int id = 0; switch (viewType) { case 0: id = colCount == 1 ? R.layout.entry_layout_single : R.layout.entry_layout; break; case 1: id = colCount == 1 ? R.layout.entry_download_layout : R.layout.entry_download_layout_compact; break; } return new ViewHolder(LayoutInflater.from(parent.getContext()).inflate(id, parent, false)); } @Override public int getItemViewType(int position) { return filter.get(position) instanceof LocalGallery ? 0 : 1; } private void bindGallery(@NonNull final ViewHolder holder, LocalGallery ent) { if (holder.flag != null) holder.flag.setVisibility(View.GONE); ImageDownloadUtility.loadImage(context, ent.getPage(ent.getMin()), holder.imgView); holder.title.setText(ent.getTitle()); if (colCount == 1) holder.pages.setText(context.getString(R.string.page_count_format, ent.getPageCount())); else holder.pages.setText(String.format(Locale.US, "%d", ent.getPageCount())); holder.title.setOnClickListener(v -> { Layout layout = holder.title.getLayout(); if (layout.getEllipsisCount(layout.getLineCount() - 1) > 0) holder.title.setMaxLines(7); else if (holder.title.getMaxLines() == 7) holder.title.setMaxLines(3); else holder.layout.performClick(); }); /*holder.layout.setOnLongClickListener(v -> { createContextualMenu(position); return true; });*/ int statusColor = statuses.get(ent.getId(), 0); if (statusColor == 0) { statusColor = Queries.StatusMangaTable.getStatus(ent.getId()).color; statuses.put(ent.getId(), statusColor); } holder.title.setBackgroundColor(statusColor); } public void updateColor(int id) { if (id < 0) return; statuses.put(id, Queries.StatusMangaTable.getStatus(id).color); for (int i = 0; i < filter.size(); i++) { Object o = filter.get(i); if (!(o instanceof LocalGallery)) continue; LocalGallery lg = (LocalGallery) o; if (lg.getId() == id) notifyItemChanged(i); } } @Override protected void defaultMasterAction(int position) { if (position < 0 || filter.size() <= position) return; if (!(filter.get(position) instanceof LocalGallery)) return; LocalGallery lg = (LocalGallery) filter.get(position); startGallery(context, lg.getDirectory()); context.setIdGalleryPosition(lg.getId()); } private void bindDownload(@NonNull final ViewHolder holder, int position, GalleryDownloaderV2 downloader) { int percentage = downloader.getPercentage(); //if (!downloader.hasData())return; ImageDownloadUtility.loadImage(context, downloader.getThumbnail(), holder.imgView); holder.title.setText(downloader.getPathTitle()); holder.cancelButton.setOnClickListener(v -> removeDownloader(downloader)); switch (downloader.getStatus()) { case PAUSED: holder.playButton.setImageResource(R.drawable.ic_play); holder.playButton.setOnClickListener(v -> { downloader.setStatus(GalleryDownloaderV2.Status.NOT_STARTED); DownloadGalleryV2.startWork(context); notifyItemChanged(position); }); break; case DOWNLOADING: holder.playButton.setImageResource(R.drawable.ic_pause); holder.playButton.setOnClickListener(v -> { downloader.setStatus(GalleryDownloaderV2.Status.PAUSED); notifyItemChanged(position); }); break; case NOT_STARTED: holder.playButton.setImageResource(R.drawable.ic_play); holder.playButton.setOnClickListener(v -> DownloadQueue.givePriority(downloader)); break; } holder.progress.setText(context.getString(R.string.percentage_format, percentage)); holder.progress.setVisibility(downloader.getStatus() == GalleryDownloaderV2.Status.NOT_STARTED ? View.GONE : View.VISIBLE); holder.progressBar.setProgress(percentage); holder.progressBar.setIndeterminate(downloader.getStatus() == GalleryDownloaderV2.Status.NOT_STARTED); Global.setTint(context, holder.playButton.getDrawable()); Global.setTint(context, holder.cancelButton.getDrawable()); } private void removeDownloader(GalleryDownloaderV2 downloader) { int position = filter.indexOf(downloader); if (position < 0) return; filter.remove(position); DownloadQueue.remove(downloader, true); galleryDownloaders.remove(downloader); context.runOnUiThread(() -> notifyItemRemoved(position)); } @Override public long getItemId(int position) { if (position == -1) return -1; return Objects.hash(filter.get(position).hashCode(), position); } @Override public void onBindMultichoiceViewHolder(@NonNull ViewHolder holder, int position) { if (filter.get(position) instanceof LocalGallery) bindGallery(holder, (LocalGallery) filter.get(position)); else bindDownload(holder, position, (GalleryDownloaderV2) filter.get(position)); } private void showDialogDelete() { MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(context); builder.setTitle(R.string.delete_galleries).setMessage(getAllGalleries()); builder.setPositiveButton(R.string.yes, (dialog, which) -> { ArrayList coll = new ArrayList<>(getSelected()); for (Object o : coll) { filter.remove(o); if (o instanceof LocalGallery) { dataset.remove(o); Global.recursiveDelete(((LocalGallery) o).getDirectory()); } else if (o instanceof GalleryDownloaderV2) { DownloadQueue.remove((GalleryDownloaderV2) o, true); } } context.runOnUiThread(this::notifyDataSetChanged); }).setNegativeButton(R.string.no, null).setCancelable(true); builder.show(); } private String getAllGalleries() { StringBuilder builder = new StringBuilder(); for (Object o : getSelected()) { if (o instanceof LocalGallery) builder.append(((LocalGallery) o).getTitle()); else builder.append(((GalleryDownloaderV2) o).getTitle()); builder.append('\n'); } return builder.toString(); } private void showDialogPDF() { MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(context); builder.setTitle(R.string.create_pdf).setMessage(getAllGalleries()); builder.setPositiveButton(R.string.yes, (dialog, which) -> { for (Object o : getSelected()) { if (!(o instanceof LocalGallery)) continue; CreatePdfOrZip.startWork(context, (LocalGallery) o, true); } }).setNegativeButton(R.string.no, null).setCancelable(true); builder.show(); } private void showDialogZip() { MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(context); builder.setTitle(R.string.create_zip).setMessage(getAllGalleries()); builder.setPositiveButton(R.string.yes, (dialog, which) -> { for (Object o : getSelected()) { if (!(o instanceof LocalGallery)) continue; CreatePdfOrZip.startWork(context, (LocalGallery) o, false); } }).setNegativeButton(R.string.no, null).setCancelable(true); builder.show(); } public boolean hasSelectedClass(Class c) { for (Object x : getSelected()) if (x.getClass() == c) return true; return false; } @Override public int getItemCount() { return filter.size(); } @Override public Filter getFilter() { return new Filter() { @Override protected FilterResults performFiltering(CharSequence constraint) { String query = constraint.toString().toLowerCase(Locale.US); if (lastQuery.equals(query)) return null; FilterResults results = new FilterResults(); lastQuery = query; results.values = createHash(galleryDownloaders, dataset); return results; } @SuppressLint("NotifyDataSetChanged") @SuppressWarnings("unchecked") @Override protected void publishResults(CharSequence constraint, FilterResults results) { if (results != null && results.values instanceof CopyOnWriteArrayList) { filter = (CopyOnWriteArrayList) results.values; context.runOnUiThread(() -> notifyDataSetChanged()); } } }; } public void removeObserver() { DownloadQueue.removeObserver(observer); } public void viewRandom() { if (dataset.isEmpty()) return; int x = Utility.RANDOM.nextInt(dataset.size()); startGallery(context, dataset.get(x).getDirectory()); } public void sortChanged() { sortElements(); context.runOnUiThread(() -> notifyItemRangeChanged(0, getItemCount())); } public void startSelected() { for (Object o : getSelected()) { if (!(o instanceof GalleryDownloaderV2)) continue; GalleryDownloaderV2 d = (GalleryDownloaderV2) o; if (d.getStatus() == GalleryDownloaderV2.Status.PAUSED) d.setStatus(GalleryDownloaderV2.Status.NOT_STARTED); DownloadGalleryV2.startWork(context); } context.runOnUiThread(this::notifyDataSetChanged); } public void pauseSelected() { for (Object o : getSelected()) { if (!(o instanceof GalleryDownloaderV2)) continue; GalleryDownloaderV2 d = (GalleryDownloaderV2) o; d.setStatus(GalleryDownloaderV2.Status.PAUSED); } context.runOnUiThread(this::notifyDataSetChanged); } public void deleteSelected() { showDialogDelete(); } public void zipSelected() { showDialogZip(); } public void pdfSelected() { showDialogPDF(); } public static class ViewHolder extends RecyclerView.ViewHolder { final ImageView imgView; final View overlay; final TextView title, pages, flag, progress; final ViewGroup layout; final ImageButton playButton, cancelButton; final ProgressBar progressBar; ViewHolder(View v) { super(v); //Both imgView = v.findViewById(R.id.image); title = v.findViewById(R.id.title); //Local pages = v.findViewById(R.id.pages); layout = v.findViewById(R.id.master_layout); flag = v.findViewById(R.id.flag); overlay = v.findViewById(R.id.overlay); //Downloader progress = itemView.findViewById(R.id.progress); progressBar = itemView.findViewById(R.id.progressBar); playButton = itemView.findViewById(R.id.playButton); cancelButton = itemView.findViewById(R.id.cancelButton); } } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/adapters/StatusManagerAdapter.java ================================================ package com.maxwai.nclientv3.adapters; import android.app.Activity; import android.graphics.Color; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.Button; import android.widget.EditText; import android.widget.ImageButton; import android.widget.LinearLayout; import android.widget.TextView; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.recyclerview.widget.RecyclerView; import com.maxwai.nclientv3.R; import com.maxwai.nclientv3.components.status.Status; import com.maxwai.nclientv3.components.status.StatusManager; import com.maxwai.nclientv3.settings.Global; import com.maxwai.nclientv3.utility.Utility; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import java.util.List; import yuku.ambilwarna.AmbilWarnaDialog; public class StatusManagerAdapter extends RecyclerView.Adapter { private final List statusList; private final Activity activity; private int newColor; public StatusManagerAdapter(Activity activity) { statusList = StatusManager.toList(); this.activity = activity; } @NonNull @Override public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { int resId = R.layout.entry_status; View view = LayoutInflater.from(activity).inflate(resId, parent, false); return new ViewHolder(view); } @Override public void onBindViewHolder(@NonNull ViewHolder holder, int position) { if (holder.getBindingAdapterPosition() == statusList.size()) { holder.name.setText(R.string.add); holder.color.setVisibility(View.INVISIBLE); holder.color.setBackgroundColor(Color.TRANSPARENT); holder.cancel.setImageResource(R.drawable.ic_add); Global.setTint(activity, holder.cancel.getDrawable()); holder.cancel.setOnClickListener(null); holder.master.setOnClickListener(v -> updateStatus(null)); return; } Status status = statusList.get(holder.getBindingAdapterPosition()); holder.name.setText(status.name); holder.color.setVisibility(View.VISIBLE); holder.color.setBackgroundColor(status.opaqueColor()); holder.cancel.setImageResource(R.drawable.ic_close); holder.master.setOnClickListener(v -> updateStatus(status)); holder.cancel.setOnClickListener(v -> { StatusManager.remove(status); notifyItemRemoved(statusList.indexOf(status)); statusList.remove(status); }); } @Override public int getItemCount() { return statusList.size() + 1; } @Override public int getItemViewType(int position) { return position == statusList.size() ? 1 : 0; } private void updateStatus(@Nullable Status status) { MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(activity); LinearLayout layout = (LinearLayout) View.inflate(activity, R.layout.dialog_add_status, null); EditText name = layout.findViewById(R.id.name); Button btnColor = layout.findViewById(R.id.color); int color = status == null ? Utility.RANDOM.nextInt() | 0xff000000 : status.opaqueColor(); newColor = color; btnColor.setBackgroundColor(color); name.setText(status == null ? "" : status.name); btnColor.setOnClickListener(v -> new AmbilWarnaDialog(activity, color, false, new AmbilWarnaDialog.OnAmbilWarnaListener() { @Override public void onCancel(AmbilWarnaDialog dialog) { } @Override public void onOk(AmbilWarnaDialog dialog, int color) { if (color == Color.WHITE || color == Color.BLACK) { Toast.makeText(activity, R.string.invalid_color_selected, Toast.LENGTH_SHORT).show(); return; } newColor = color; btnColor.setBackgroundColor(color); } }).show()); builder.setView(layout); builder.setTitle(status == null ? R.string.create_new_status : R.string.update_status); builder.setPositiveButton(R.string.ok, (dialog, which) -> { String newName = name.getText().toString(); if (newName.length() < 2) { Toast.makeText(activity, R.string.name_too_short, Toast.LENGTH_SHORT).show(); return; } if (StatusManager.getByName(newName) != null && status != null && !newName.equals(status.name)) { Toast.makeText(activity, R.string.duplicated_name, Toast.LENGTH_SHORT).show(); return; } Status newStatus = StatusManager.updateStatus(status, name.getText().toString(), newColor); if (status == null) { statusList.add(newStatus); statusList.sort((o1, o2) -> o1.name.compareToIgnoreCase(o2.name)); int index = statusList.indexOf(newStatus); notifyItemInserted(index); } else { int oldIndex = statusList.indexOf(status); statusList.set(oldIndex, newStatus); statusList.sort((o1, o2) -> o1.name.compareToIgnoreCase(o2.name)); int newIndex = statusList.indexOf(newStatus); notifyItemMoved(oldIndex, newIndex); notifyItemChanged(newIndex); } }); builder.setNegativeButton(R.string.cancel, null); builder.show(); } public static class ViewHolder extends RecyclerView.ViewHolder { final LinearLayout master; final Button color; final ImageButton cancel; final TextView name; public ViewHolder(@NonNull View itemView) { super(itemView); name = itemView.findViewById(R.id.name); cancel = itemView.findViewById(R.id.cancelButton); color = itemView.findViewById(R.id.color); master = itemView.findViewById(R.id.master_layout); } } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/adapters/StatusViewerAdapter.java ================================================ package com.maxwai.nclientv3.adapters; import android.app.Activity; import android.content.Intent; import android.database.Cursor; import android.text.Layout; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.recyclerview.widget.RecyclerView; import com.maxwai.nclientv3.GalleryActivity; import com.maxwai.nclientv3.R; import com.maxwai.nclientv3.api.components.Gallery; import com.maxwai.nclientv3.async.database.Queries; import com.maxwai.nclientv3.settings.Global; import com.maxwai.nclientv3.utility.ImageDownloadUtility; import com.maxwai.nclientv3.utility.LogUtility; import org.json.JSONException; import java.io.IOException; import java.util.Locale; public class StatusViewerAdapter extends RecyclerView.Adapter { private final String statusName; private final Activity context; @NonNull private String query = ""; private boolean sortByTitle = false; @Nullable private Cursor galleries = null; public StatusViewerAdapter(Activity context, String statusName) { this.statusName = statusName; this.context = context; reloadGalleries(); } @NonNull @Override public GenericAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { View view = LayoutInflater.from(context).inflate(R.layout.entry_layout, parent, false); return new GenericAdapter.ViewHolder(view); } @Override public void onBindViewHolder(@NonNull GenericAdapter.ViewHolder holder, int position) { Gallery ent = positionToGallery(holder.getBindingAdapterPosition()); if (ent == null) return; ImageDownloadUtility.loadImage(context, ent.getThumbnail(), holder.imgView); holder.pages.setText(String.format(Locale.US, "%d", ent.getPageCount())); holder.title.setText(ent.getTitle()); holder.flag.setText(Global.getLanguageFlag(ent.getLanguage())); holder.title.setOnClickListener(v -> { Layout layout = holder.title.getLayout(); if (layout.getEllipsisCount(layout.getLineCount() - 1) > 0) holder.title.setMaxLines(7); else if (holder.title.getMaxLines() == 7) holder.title.setMaxLines(3); else holder.layout.performClick(); }); holder.layout.setOnClickListener(v -> { //Global.setLoadedGallery(ent); Intent intent = new Intent(context, GalleryActivity.class); LogUtility.d(ent + ""); intent.putExtra(context.getPackageName() + ".GALLERY", ent); context.startActivity(intent); }); holder.layout.setOnLongClickListener(v -> { holder.title.animate().alpha(holder.title.getAlpha() == 0f ? 1f : 0f).setDuration(100).start(); holder.flag.animate().alpha(holder.flag.getAlpha() == 0f ? 1f : 0f).setDuration(100).start(); holder.pages.animate().alpha(holder.pages.getAlpha() == 0f ? 1f : 0f).setDuration(100).start(); return true; }); } @Nullable private Gallery positionToGallery(int position) { if (galleries != null && galleries.moveToPosition(position)) { return Queries.GalleryTable.cursorToGallery(context, galleries); } return null; } @Override public int getItemCount() { return galleries != null ? galleries.getCount() : 0; } public void setGalleries(@Nullable Cursor galleries) { if (this.galleries != null) this.galleries.close(); this.galleries = galleries; context.runOnUiThread(this::notifyDataSetChanged); } public void reloadGalleries() { setGalleries(Queries.StatusMangaTable.getGalleryOfStatus(statusName, query, sortByTitle)); } public void setQuery(@Nullable String newQuery) { query = newQuery == null ? "" : newQuery; reloadGalleries(); } public void updateSort(boolean byTitle) { sortByTitle = byTitle; reloadGalleries(); } public void update(String newQuery, boolean byTitle) { if (query.equals(newQuery) && byTitle == sortByTitle) return; query = newQuery == null ? "" : newQuery; sortByTitle = byTitle; reloadGalleries(); } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/adapters/TagsAdapter.java ================================================ package com.maxwai.nclientv3.adapters; import android.database.Cursor; import android.util.JsonWriter; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.Filter; import android.widget.Filterable; import android.widget.ImageView; import android.widget.TextView; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.constraintlayout.widget.ConstraintLayout; import androidx.recyclerview.widget.RecyclerView; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.maxwai.nclientv3.R; import com.maxwai.nclientv3.TagFilterActivity; import com.maxwai.nclientv3.api.components.Tag; import com.maxwai.nclientv3.api.enums.TagStatus; import com.maxwai.nclientv3.api.enums.TagType; import com.maxwai.nclientv3.async.database.Queries; import com.maxwai.nclientv3.settings.Global; import com.maxwai.nclientv3.settings.Login; import com.maxwai.nclientv3.settings.TagV2; import com.maxwai.nclientv3.utility.ImageDownloadUtility; import com.maxwai.nclientv3.utility.LogUtility; import com.maxwai.nclientv3.utility.Utility; import java.io.IOException; import java.io.StringWriter; import java.util.Locale; import okhttp3.Call; import okhttp3.Callback; import okhttp3.MediaType; import okhttp3.Request; import okhttp3.RequestBody; import okhttp3.Response; public class TagsAdapter extends RecyclerView.Adapter implements Filterable { @NonNull private final TagFilterActivity context; private final boolean logged = Login.isLogged(); private final TagType type; private final TagMode tagMode; private String lastQuery = null; private Cursor cursor = null; public TagsAdapter(@NonNull TagFilterActivity cont, String query, boolean online) { this.context = cont; this.type = null; this.tagMode = online ? TagMode.ONLINE : TagMode.OFFLINE; getFilter().filter(query); } public TagsAdapter(@NonNull TagFilterActivity cont, String query, TagType type) { this.context = cont; this.type = type; this.tagMode = TagMode.TYPE; getFilter().filter(query); } @Override public Filter getFilter() { return new Filter() { @Override protected FilterResults performFiltering(CharSequence constraint) { FilterResults results = new FilterResults(); if (constraint == null) constraint = ""; lastQuery = constraint.toString(); Cursor tags = Queries.TagTable.getFilterCursor(lastQuery, type, tagMode == TagMode.ONLINE, TagV2.isSortedByName()); results.count = tags.getCount(); results.values = tags; LogUtility.d(results.count + "," + results.values); return results; } @Override protected void publishResults(CharSequence constraint, FilterResults results) { if (results.count == -1) return; Cursor newCursor = (Cursor) results.values; int oldCount = getItemCount(), newCount = results.count; if (cursor != null) cursor.close(); cursor = newCursor; if (newCount > oldCount) notifyItemRangeInserted(oldCount, newCount - oldCount); else notifyItemRangeRemoved(newCount, oldCount - newCount); notifyItemRangeChanged(0, Math.min(newCount, oldCount)); } }; } @NonNull @Override public TagsAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { return new TagsAdapter.ViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.entry_tag_layout, parent, false)); } @Override public void onBindViewHolder(@NonNull final TagsAdapter.ViewHolder holder, int position) { cursor.moveToPosition(position); final Tag ent = Queries.TagTable.cursorToTag(cursor); holder.title.setText(ent.getName()); holder.count.setText(String.format(Locale.US, "%d", ent.getCount())); holder.master.setOnClickListener(v -> { switch (tagMode) { case OFFLINE: case TYPE: if (TagV2.maxTagReached() && ent.getStatus() == TagStatus.DEFAULT) { context.runOnUiThread(() -> Toast.makeText(context, context.getString(R.string.tags_max_reached, TagV2.MAXTAGS), Toast.LENGTH_LONG).show()); } else { TagV2.updateStatus(ent); updateLogo(holder.imgView, ent.getStatus()); } break; case ONLINE: try { onlineTagUpdate(ent, !Login.isOnlineTags(ent), holder.imgView); } catch (IOException e) { LogUtility.e("Error setting tag", e); } break; } }); if (tagMode != TagMode.ONLINE && logged) holder.master.setOnLongClickListener(view -> { if (!Login.isOnlineTags(ent)) showBlacklistDialog(ent, holder.imgView); else Toast.makeText(context, R.string.tag_already_in_blacklist, Toast.LENGTH_SHORT).show(); return true; }); updateLogo(holder.imgView, tagMode == TagMode.ONLINE ? TagStatus.AVOIDED : ent.getStatus()); } @Override public int getItemCount() { return cursor == null ? 0 : cursor.getCount(); } private void showBlacklistDialog(final Tag tag, final ImageView imgView) { MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(context); builder.setIcon(R.drawable.ic_star_border).setTitle(R.string.add_to_online_blacklist).setMessage(R.string.are_you_sure); builder.setPositiveButton(R.string.yes, (dialogInterface, i) -> { try { onlineTagUpdate(tag, true, imgView); } catch (IOException e) { LogUtility.w("Error adding to online blacklist", e); Toast.makeText(context, R.string.online_blacklist_add_error, Toast.LENGTH_SHORT).show(); } }).setNegativeButton(R.string.no, null).show(); } private void onlineTagUpdate(final Tag tag, final boolean add, final ImageView imgView) throws IOException { if (!Login.isLogged()) return; StringWriter sw = new StringWriter(); JsonWriter jw = new JsonWriter(sw); jw.beginObject().name("added").beginArray(); if (add) jw.value(tag.getId());; jw.endArray().name("removed").beginArray(); if (!add) jw.value(tag.getId());; jw.endArray().endObject(); final String url = Utility.getApiBaseUrl() + "blacklist"; final RequestBody ss = RequestBody.create(sw.toString(), MediaType.get("application/json")); Global.getClient(context) .newCall(new Request.Builder().url(url).post(ss).build()) .enqueue(new Callback() { @Override public void onFailure(@NonNull Call call, @NonNull IOException e) { } @Override public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException { if (response.body().string().contains("true")) { if (add) Login.addOnlineTag(tag); else Login.removeOnlineTag(tag); if (tagMode == TagMode.ONLINE) updateLogo(imgView, add ? TagStatus.AVOIDED : TagStatus.DEFAULT); } } }); } private void updateLogo(ImageView img, TagStatus s) { context.runOnUiThread(() -> { switch (s) { case DEFAULT: img.setImageDrawable(null); break;//ImageDownloadUtility.loadImage(R.drawable.ic_void,img); break; case ACCEPTED: ImageDownloadUtility.loadImage(R.drawable.ic_check, img); Global.setTint(context, img.getDrawable()); break; case AVOIDED: ImageDownloadUtility.loadImage(R.drawable.ic_close, img); Global.setTint(context, img.getDrawable()); break; } }); } public void addItem() { getFilter().filter(lastQuery); } private enum TagMode {ONLINE, OFFLINE, TYPE} public static class ViewHolder extends RecyclerView.ViewHolder { final ImageView imgView; final TextView title, count; final ConstraintLayout master; ViewHolder(View v) { super(v); imgView = v.findViewById(R.id.image); title = v.findViewById(R.id.title); count = v.findViewById(R.id.count); master = v.findViewById(R.id.master_layout); } } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/api/InspectorV3.java ================================================ package com.maxwai.nclientv3.api; import android.content.Context; import android.os.Build; import android.os.Parcel; import android.os.Parcelable; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.maxwai.nclientv3.api.components.Gallery; import com.maxwai.nclientv3.api.components.GenericGallery; import com.maxwai.nclientv3.api.components.Ranges; import com.maxwai.nclientv3.api.components.Tag; import com.maxwai.nclientv3.api.enums.ApiRequestType; import com.maxwai.nclientv3.api.enums.Language; import com.maxwai.nclientv3.api.enums.SortType; import com.maxwai.nclientv3.api.enums.SpecialTagIds; import com.maxwai.nclientv3.api.enums.TagStatus; import com.maxwai.nclientv3.api.enums.TagType; import com.maxwai.nclientv3.api.local.LocalGallery; import com.maxwai.nclientv3.async.database.Queries; import com.maxwai.nclientv3.settings.Global; import com.maxwai.nclientv3.utility.LogUtility; import com.maxwai.nclientv3.utility.Utility; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.lang.ref.WeakReference; import java.net.HttpURLConnection; import java.net.URLEncoder; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Objects; import java.util.Set; import okhttp3.Request; import okhttp3.Response; public class InspectorV3 extends Thread implements Parcelable { public static final Creator CREATOR = new Creator<>() { @Override public InspectorV3 createFromParcel(Parcel in) { return new InspectorV3(in); } @Override public InspectorV3[] newArray(int size) { return new InspectorV3[size]; } }; private SortType sortType; private boolean custom; private int page, pageCount = -1, id; private String query, url; private ApiRequestType requestType; private Set tags; private ArrayList galleries = null; private Ranges ranges = null; private InspectorResponse response; private WeakReference context; private String jsonResponse; protected InspectorV3(Parcel in) { sortType = SortType.values()[in.readByte()]; custom = in.readByte() != 0; page = in.readInt(); pageCount = in.readInt(); id = in.readInt(); query = in.readString(); url = in.readString(); requestType = ApiRequestType.values[in.readByte()]; List tmpList = null; switch (GenericGallery.Type.values()[in.readByte()]) { case LOCAL: tmpList = in.createTypedArrayList(LocalGallery.CREATOR); break; case SIMPLE: tmpList = in.createTypedArrayList(SimpleGallery.CREATOR); break; case COMPLETE: tmpList = in.createTypedArrayList(Gallery.CREATOR); break; } if (tmpList != null) { galleries = new ArrayList<>(); galleries.addAll(tmpList); } tags = new HashSet<>(Objects.requireNonNull(in.createTypedArrayList(Tag.CREATOR))); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { ranges = in.readParcelable(Ranges.class.getClassLoader(), Ranges.class); } else { ranges = in.readParcelable(Ranges.class.getClassLoader()); } } private InspectorV3(Context context, InspectorResponse response) { initialize(context, response); } /** * This method will not run, but a WebView inside MainActivity will do it in its place */ public static InspectorV3 favoriteInspector(Context context, String query, int page, InspectorResponse response) { InspectorV3 inspector = new InspectorV3(context, response); inspector.page = page; inspector.pageCount = 0; inspector.query = query == null ? "" : query; inspector.requestType = ApiRequestType.FAVORITE; inspector.tags = new HashSet<>(1); inspector.createUrl(); return inspector; } /** * @param favorite true if random online favorite, false for general random manga */ public static InspectorV3 randomInspector(Context context, InspectorResponse response, boolean favorite) { InspectorV3 inspector = new InspectorV3(context, response); inspector.requestType = favorite ? ApiRequestType.RANDOM_FAVORITE : ApiRequestType.RANDOM; inspector.createUrl(); return inspector; } public static InspectorV3 galleryInspector(Context context, int id, InspectorResponse response) { InspectorV3 inspector = new InspectorV3(context, response); inspector.id = id; inspector.requestType = ApiRequestType.BYSINGLE; inspector.createUrl(); return inspector; } public static InspectorV3 basicInspector(Context context, int page, InspectorResponse response) { return searchInspector(context, null, null, page, Global.getSortType(), null, response); } public static InspectorV3 tagInspector(Context context, Tag tag, int page, SortType sortType, InspectorResponse response) { Collection tags; if (!Global.isOnlyTag()) { tags = getDefaultTags(); tags.add(tag); } else { tags = Collections.singleton(tag); } return searchInspector(context, null, tags, page, sortType, null, response); } public static InspectorV3 searchInspector(Context context, String query, Collection tags, int page, SortType sortType, @Nullable Ranges ranges, InspectorResponse response) { InspectorV3 inspector = new InspectorV3(context, response); inspector.custom = tags != null; inspector.tags = inspector.custom ? new HashSet<>(tags) : getDefaultTags(); inspector.tags.addAll(getLanguageTags(Global.getOnlyLanguage())); inspector.page = page; inspector.pageCount = 0; inspector.ranges = ranges; inspector.query = query == null ? "" : query; inspector.sortType = sortType; if (inspector.query.isEmpty() && (ranges == null || ranges.isDefault())) { switch (inspector.tags.size()) { case 0: inspector.requestType = ApiRequestType.BYALL; inspector.tryByAllPopular(); break; case 1: inspector.requestType = ApiRequestType.BYTAG; //else by search for the negative tag if (inspector.getTag().getStatus() != TagStatus.AVOIDED) break; default: inspector.requestType = ApiRequestType.BYSEARCH; break; } } else inspector.requestType = ApiRequestType.BYSEARCH; inspector.createUrl(); return inspector; } @NonNull private static HashSet getDefaultTags() { HashSet tags = new HashSet<>(Queries.TagTable.getAllStatus(TagStatus.ACCEPTED)); tags.addAll(getLanguageTags(Global.getOnlyLanguage())); if (Global.removeAvoidedGalleries()) tags.addAll(Queries.TagTable.getAllStatus(TagStatus.AVOIDED)); tags.addAll(Queries.TagTable.getAllOnlineBlacklisted()); return tags; } private static Set getLanguageTags(Language onlyLanguage) { Set tags = new HashSet<>(); if (onlyLanguage == null) return tags; switch (onlyLanguage) { case ENGLISH: tags.add(Queries.TagTable.getTagById(SpecialTagIds.LANGUAGE_ENGLISH)); break; case JAPANESE: tags.add(Queries.TagTable.getTagById(SpecialTagIds.LANGUAGE_JAPANESE)); break; case CHINESE: tags.add(Queries.TagTable.getTagById(SpecialTagIds.LANGUAGE_CHINESE)); break; } return tags; } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeByte((byte) (Objects.requireNonNullElse(sortType, SortType.RECENT_ALL_TIME).ordinal())); dest.writeByte((byte) (custom ? 1 : 0)); dest.writeInt(page); dest.writeInt(pageCount); dest.writeInt(id); dest.writeString(query); dest.writeString(url); dest.writeByte(requestType.ordinal()); if (galleries == null || galleries.isEmpty()) dest.writeByte((byte) GenericGallery.Type.SIMPLE.ordinal()); else dest.writeByte((byte) galleries.get(0).getType().ordinal()); dest.writeTypedList(galleries); dest.writeTypedList(new ArrayList<>(tags)); dest.writeParcelable(ranges, flags); } public String getSearchTitle() { //triggered only when in searchMode if (!query.isEmpty()) return query; return url.replace(Utility.getBaseUrl() + "api/v2/search?query=", "").replace('+', ' '); } public void initialize(Context context, InspectorResponse response) { this.response = response; this.context = new WeakReference<>(context); } public InspectorResponse getResponse() { return response; } public InspectorV3 cloneInspector(Context context, InspectorResponse response) { InspectorV3 inspectorV3 = new InspectorV3(context, response); inspectorV3.query = query; inspectorV3.url = url; inspectorV3.tags = tags; inspectorV3.requestType = requestType; inspectorV3.sortType = sortType; inspectorV3.pageCount = pageCount; inspectorV3.page = page; inspectorV3.id = id; inspectorV3.custom = custom; inspectorV3.ranges = ranges; return inspectorV3; } private void tryByAllPopular() { if (sortType != SortType.RECENT_ALL_TIME) { requestType = ApiRequestType.BYSEARCH; query = "-nclientv3"; } } private void createUrl() { String query; try { query = this.query == null ? null : URLEncoder.encode(this.query, Charset.defaultCharset().name()); } catch (UnsupportedEncodingException ignore) { query = this.query; } StringBuilder builder = new StringBuilder(Utility.getBaseUrl()).append("api/v2/"); if (requestType == ApiRequestType.BYALL) { builder.append("galleries?page=").append(page); } else if (requestType == ApiRequestType.RANDOM) { builder.append("galleries/random"); } else if (requestType == ApiRequestType.RANDOM_FAVORITE) { builder.append("favorites/random"); } else if (requestType == ApiRequestType.BYSINGLE) { builder.append("galleries/").append(id).append("?include=related,favorite"); } else if (requestType == ApiRequestType.FAVORITE) { builder.append("favorites?page=").append(page); if (query != null && !query.isEmpty()) builder.append("&q=").append(query); } else if (requestType == ApiRequestType.BYSEARCH || requestType == ApiRequestType.BYTAG) { builder.append("search?query=").append(query); for (Tag tt : tags) { if (builder.toString().contains(tt.toQueryTag(TagStatus.ACCEPTED))) continue; builder.append('+'); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { builder.append(URLEncoder.encode(tt.toQueryTag(), Charset.defaultCharset())); } else { try { //noinspection CharsetObjectCanBeUsed builder.append(URLEncoder.encode(tt.toQueryTag(), Charset.defaultCharset().name())); } catch (UnsupportedEncodingException e) { LogUtility.wtf("This should not happen since we used the default charset", e); return; } } } if (ranges != null) builder.append('+').append(ranges.toQuery()); builder.append("&page=").append(page); if (sortType != null && sortType.getUrlAddition() != null) { builder.append("&sort=").append(sortType.getUrlAddition()); } } url = builder.toString().replace(' ', '+'); LogUtility.d("WWW: " + getBookmarkURL()); } private String getBookmarkURL() { if (page < 2) return url; else return url.substring(0, url.lastIndexOf('=') + 1); } public boolean createDocument() throws IOException { if (jsonResponse != null) return true; try (Response response = Global.getClient(context.get()).newCall(new Request.Builder().url(url).build()).execute()) { jsonResponse = response.body().string(); return response.code() == HttpURLConnection.HTTP_OK; } } public void parseDocument() throws IOException, InvalidResponseException { try { if (requestType.isSingle()) doSingleV2(); else doSearchV2(); } catch (JSONException e) { LogUtility.e("JSON parse error: " + e.getMessage(), e); throw new InvalidResponseException(); } jsonResponse = null; } @Override public synchronized void start() { if (getState() != State.NEW) return; if (response.shouldStart(this)) super.start(); } @Override public void run() { LogUtility.d("Starting download: " + url); if (response != null) response.onStart(); try { createDocument(); parseDocument(); if (response != null) { response.onSuccess(galleries); } } catch (Exception e) { if (response != null) response.onFailure(e); } if (response != null) response.onEnd(); LogUtility.d("Finished download: " + url); } private void filterDocumentTags() { if (galleries == null || tags == null) return; ArrayList galleryTag = new ArrayList<>(galleries.size()); for (GenericGallery gal : galleries) { assert gal instanceof SimpleGallery; SimpleGallery gallery = (SimpleGallery) gal; if (gallery.hasTags(tags)) { galleryTag.add(gallery); } } galleries.clear(); galleries.addAll(galleryTag); } /** * Parse single gallery from API v2 response. * For RANDOM, the response is just {"id": N}, so we fetch the full detail. */ private void doSingleV2() throws IOException, JSONException { galleries = new ArrayList<>(1); JSONObject v2 = new JSONObject(jsonResponse); if (v2.has("error")) return; // Random endpoint returns only {"id": N} — fetch full gallery detail if (!v2.has("title") && v2.has("id")) { int galleryId = v2.getInt("id"); String detailUrl = Utility.getBaseUrl() + "api/v2/galleries/" + galleryId + "?include=related,favorite"; try (Response resp = Global.getClient(context.get()).newCall(new Request.Builder().url(detailUrl).build()).execute()) { String body = resp.body().string(); if (resp.code() != HttpURLConnection.HTTP_OK) return; v2 = new JSONObject(body); } } // Handle related galleries List relatedList = new ArrayList<>(); JSONArray relatedArr = v2.optJSONArray("related"); if (relatedArr != null) { for (int i = 0; i < relatedArr.length(); i++) { relatedList.add(SimpleGallery.fromV2ListItem(context.get(), relatedArr.getJSONObject(i))); } } boolean isFavorite = v2.optBoolean("is_favorited", false); Gallery gallery = new Gallery(context.get(), v2.toString(), relatedList, isFavorite); galleries.add(gallery); } /** * Parse search/list results from API v2 response. * v2 list items have: id, media_id, thumbnail, english_title, japanese_title, tag_ids, num_pages */ private void doSearchV2() throws InvalidResponseException, JSONException { JSONObject json = new JSONObject(jsonResponse); if (!json.has("result")) throw new InvalidResponseException(); JSONArray results = json.getJSONArray("result"); galleries = new ArrayList<>(results.length()); for (int i = 0; i < results.length(); i++) { galleries.add(SimpleGallery.fromV2ListItem(context.get(), results.getJSONObject(i))); } pageCount = json.optInt("num_pages", Math.max(1, page)); if (Global.isExactTagMatch()) filterDocumentTags(); } public void setSortType(SortType sortType) { this.sortType = sortType; if (this.requestType == ApiRequestType.BYALL) tryByAllPopular(); createUrl(); } public int getPage() { return page; } public void setPage(int page) { this.page = page; createUrl(); } public List getGalleries() { return galleries; } public String getUrl() { return url; } public ApiRequestType getRequestType() { return requestType; } public int getPageCount() { return pageCount; } public Tag getTag() { Tag t = null; if (tags == null) return null; for (Tag tt : tags) { if (tt.getType() != TagType.LANGUAGE) return tt; t = tt; } return t; } public static class InvalidResponseException extends Exception { public InvalidResponseException() { super(); } } public interface InspectorResponse { boolean shouldStart(InspectorV3 inspector); void onSuccess(List galleries); void onFailure(Exception e); void onStart(); void onEnd(); } public static abstract class DefaultInspectorResponse implements InspectorResponse { @Override public boolean shouldStart(InspectorV3 inspector) { return true; } @Override public void onStart() { } @Override public void onEnd() { } @Override public void onSuccess(List galleries) { } @Override public void onFailure(Exception e) { LogUtility.e(e.getLocalizedMessage(), e); } } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/api/RandomLoader.java ================================================ package com.maxwai.nclientv3.api; import com.maxwai.nclientv3.RandomActivity; import com.maxwai.nclientv3.api.components.Gallery; import com.maxwai.nclientv3.api.components.GenericGallery; import com.maxwai.nclientv3.utility.ImageDownloadUtility; import java.util.ArrayList; import java.util.List; public class RandomLoader { private static final int MAXLOADED = 5; private final List galleries; private final RandomActivity activity; private boolean galleryHasBeenRequested; private final InspectorV3.InspectorResponse response = new InspectorV3.DefaultInspectorResponse() { @Override public void onFailure(Exception e) { loadRandomGallery(); } @Override public void onSuccess(List galleryList) { if (galleryList.isEmpty() || !galleryList.get(0).isValid()) { loadRandomGallery(); return; } Gallery gallery = (Gallery) galleryList.get(0); galleries.add(gallery); ImageDownloadUtility.preloadImage(activity, gallery.getCover()); if (galleryHasBeenRequested) requestGallery();//requestGallery will call loadRandomGallery else if (galleries.size() < MAXLOADED) loadRandomGallery(); } }; public RandomLoader(RandomActivity activity) { this.activity = activity; galleries = new ArrayList<>(MAXLOADED); galleryHasBeenRequested = RandomActivity.loadedGallery == null; loadRandomGallery(); } private void loadRandomGallery() { if (galleries.size() >= MAXLOADED) return; InspectorV3.randomInspector(activity, response, false).start(); } public void requestGallery() { galleryHasBeenRequested = true; for (int i = 0; i < galleries.size(); i++) { if (galleries.get(i) == null) galleries.remove(i--); } if (!galleries.isEmpty()) { Gallery gallery = galleries.remove(0); activity.runOnUiThread(() -> activity.loadGallery(gallery)); galleryHasBeenRequested = false; } loadRandomGallery(); } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/api/SimpleGallery.java ================================================ package com.maxwai.nclientv3.api; import android.annotation.SuppressLint; import android.content.Context; import android.database.Cursor; import android.net.Uri; import android.os.Parcel; import androidx.annotation.NonNull; import com.maxwai.nclientv3.api.components.Gallery; import com.maxwai.nclientv3.api.components.GalleryData; import com.maxwai.nclientv3.api.components.GenericGallery; import com.maxwai.nclientv3.api.components.Tag; import com.maxwai.nclientv3.api.components.TagList; import com.maxwai.nclientv3.api.enums.Language; import com.maxwai.nclientv3.api.enums.TagStatus; import com.maxwai.nclientv3.api.enums.TagType; import com.maxwai.nclientv3.async.database.Queries; import com.maxwai.nclientv3.components.classes.Size; import com.maxwai.nclientv3.files.GalleryFolder; import com.maxwai.nclientv3.settings.Global; import com.maxwai.nclientv3.utility.LogUtility; import com.maxwai.nclientv3.utility.Utility; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.util.Collection; public class SimpleGallery extends GenericGallery { public static final Creator CREATOR = new Creator<>() { @Override public SimpleGallery createFromParcel(Parcel in) { return new SimpleGallery(in); } @Override public SimpleGallery[] newArray(int size) { return new SimpleGallery[size]; } }; private final String title; private final Uri thumbnail; private final int id, mediaId; private Language language = Language.UNKNOWN; private TagList tags; public SimpleGallery(Parcel in) { title = in.readString(); id = in.readInt(); mediaId = in.readInt(); thumbnail = Uri.parse(in.readString()); language = Language.values()[in.readByte()]; } @SuppressLint("Range") public SimpleGallery(Cursor c) { title = c.getString(c.getColumnIndex(Queries.HistoryTable.TITLE)); id = c.getInt(c.getColumnIndex(Queries.HistoryTable.ID)); mediaId = c.getInt(c.getColumnIndex(Queries.HistoryTable.MEDIAID)); thumbnail = Uri.parse(c.getString(c.getColumnIndex(Queries.HistoryTable.THUMB))); } private SimpleGallery(String title, int id, int mediaId, Uri thumbnail, Language language, TagList tags) { this.title = title; this.id = id; this.mediaId = mediaId; this.thumbnail = thumbnail; this.language = language; this.tags = tags; } public SimpleGallery(Gallery gallery) { title = gallery.getTitle(); mediaId = gallery.getMediaId(); id = gallery.getId(); thumbnail = gallery.getThumbnail(); } /** * Create a SimpleGallery from API v2 GalleryListItem JSON. * v2 list items have: id, media_id(string), thumbnail(path string), * english_title, japanese_title, tag_ids(int array), num_pages */ public static SimpleGallery fromV2ListItem(Context context, JSONObject json) throws JSONException { int id = json.getInt("id"); int mediaId; try { mediaId = Integer.parseInt(json.getString("media_id")); } catch (NumberFormatException e) { mediaId = 0; } // Title: v2 list items use english_title/japanese_title directly String englishTitle = json.optString("english_title", ""); String japaneseTitle = json.optString("japanese_title", ""); // But v2 detail/related items may use a title object JSONObject titleObj = json.optJSONObject("title"); String title; if (titleObj != null) { String pretty = titleObj.optString("pretty", ""); String english = titleObj.optString("english", ""); String japanese = titleObj.optString("japanese", ""); title = !pretty.isEmpty() ? pretty : (!english.isEmpty() ? english : japanese); } else { title = !englishTitle.isEmpty() ? englishTitle : japaneseTitle; } String thumbPath = json.optString("thumbnail", ""); // Tags: v2 list items only have tag_ids, look them up from local DB JSONArray tagIdsArr = json.optJSONArray("tag_ids"); TagList tags; Language language = Language.UNKNOWN; if (tagIdsArr != null && tagIdsArr.length() > 0) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < tagIdsArr.length(); i++) { if (i > 0) sb.append(','); sb.append(tagIdsArr.getInt(i)); } tags = Queries.TagTable.getTagsFromListOfInt(sb.toString()); language = Gallery.loadLanguage(tags); } else { // v2 detail related items may have full tag objects JSONArray tagsArray = json.optJSONArray("tags"); if (tagsArray != null && tagsArray.length() > 0) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < tagsArray.length(); i++) { JSONObject tagObj = tagsArray.getJSONObject(i); Tag tag = new Tag( tagObj.getString("name"), tagObj.optInt("count", 0), tagObj.getInt("id"), TagType.typeByName(tagObj.getString("type")), TagStatus.DEFAULT ); Queries.TagTable.insert(tag); if (i > 0) sb.append(','); sb.append(tag.getId()); } tags = Queries.TagTable.getTagsFromListOfInt(sb.toString()); language = Gallery.loadLanguage(tags); } else { tags = new TagList(); } } if (context != null && id > Global.getMaxId()) Global.updateMaxId(context, id); return new SimpleGallery(title, id, mediaId, Uri.parse("https://t1." + Utility.getHost() + "/" + thumbPath), language, tags); } public boolean hasTags(Collection tags) { return this.tags.hasTags(tags); } public Language getLanguage() { return language; } public boolean hasIgnoredTags(String s) { if (tags == null) return false; for (Tag t : tags.getAllTagsList()) if (s.contains(t.toQueryTag(TagStatus.AVOIDED))) { LogUtility.d("Found: " + s + ",," + t.toQueryTag()); return true; } return false; } @Override public int getId() { return id; } @Override public Type getType() { return Type.SIMPLE; } @Override public int getPageCount() { return 0; } @Override public boolean isValid() { return id > 0; } @Override @NonNull public String getTitle() { return title; } @Override public Size getMaxSize() { return null; } @Override public Size getMinSize() { return null; } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeString(title); dest.writeInt(id); dest.writeInt(mediaId); dest.writeString(thumbnail.toString()); dest.writeByte((byte) language.ordinal()); //TAGS AREN'T WRITTEN } public Uri getThumbnail() { return thumbnail; } public int getMediaId() { return mediaId; } @Override public GalleryFolder getGalleryFolder() { return null; } @NonNull @Override public String toString() { return "SimpleGallery{" + "language=" + language + ", title='" + title + '\'' + ", thumbnail=" + thumbnail + ", id=" + id + ", mediaId=" + mediaId + '}'; } @Override public boolean hasGalleryData() { return false; } @Override public GalleryData getGalleryData() { return null; } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/api/comments/Comment.java ================================================ package com.maxwai.nclientv3.api.comments; import android.net.Uri; import android.os.Build; import android.os.Parcel; import android.os.Parcelable; import android.util.JsonReader; import android.util.JsonToken; import java.io.IOException; import java.util.Date; public class Comment implements Parcelable { public static final Creator CREATOR = new Creator<>() { @Override public Comment createFromParcel(Parcel in) { return new Comment(in); } @Override public Comment[] newArray(int size) { return new Comment[size]; } }; private int id; private User poster; private Date postDate; private String body; public Comment(JsonReader reader) throws IOException { reader.beginObject(); while (reader.peek() != JsonToken.END_OBJECT) { switch (reader.nextName()) { case "id": id = reader.nextInt(); break; case "post_date": postDate = new Date(reader.nextLong() * 1000); break; case "body": body = reader.nextString(); break; case "poster": poster = new User(reader); break; default: reader.skipValue(); break; } } reader.endObject(); } protected Comment(Parcel in) { id = in.readInt(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { poster = in.readParcelable(User.class.getClassLoader(), User.class); } else { poster = in.readParcelable(User.class.getClassLoader()); } body = in.readString(); postDate = new Date(in.readLong()); } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeInt(id); dest.writeParcelable(poster, flags); dest.writeString(body); dest.writeLong(postDate.getTime()); } public int getId() { return id; } public Date getPostDate() { return postDate; } public String getComment() { return body; } public int getPosterId() { return poster.getId(); } public String getUsername() { return poster.getUsername(); } public Uri getAvatarUrl() { return poster.getAvatarUrl(); } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/api/comments/CommentsFetcher.java ================================================ package com.maxwai.nclientv3.api.comments; import android.util.JsonReader; import android.util.JsonToken; import com.maxwai.nclientv3.CommentActivity; import com.maxwai.nclientv3.adapters.CommentAdapter; import com.maxwai.nclientv3.settings.Global; import com.maxwai.nclientv3.utility.LogUtility; import com.maxwai.nclientv3.utility.Utility; import java.io.IOException; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.Objects; import okhttp3.Request; import okhttp3.Response; import okhttp3.ResponseBody; public class CommentsFetcher extends Thread { private static final String COMMENT_API_URL = Utility.getApiBaseUrl() + "galleries/%d/comments"; private final int id; private final CommentActivity commentActivity; private final List comments = new ArrayList<>(); public CommentsFetcher(CommentActivity commentActivity, int id) { this.id = id; this.commentActivity = commentActivity; } @Override public void run() { populateComments(); postResult(); } private void postResult() { CommentAdapter commentAdapter = new CommentAdapter(commentActivity, comments); commentActivity.setAdapter(commentAdapter); commentActivity.runOnUiThread(() -> { commentActivity.getRecycler().setAdapter(commentAdapter); commentActivity.getRefresher().setRefreshing(false); }); } private void populateComments() { String url = String.format(Locale.US, COMMENT_API_URL, id); try (Response response = Objects.requireNonNull(Global.getClient()).newCall(new Request.Builder().url(url).build()).execute()) { ResponseBody body = response.body(); try (JsonReader reader = new JsonReader(new InputStreamReader(body.byteStream()))) { if(reader.peek() == JsonToken.BEGIN_ARRAY) { reader.beginArray(); while (reader.hasNext()) comments.add(new Comment(reader)); } } } catch (NullPointerException | IOException e) { LogUtility.w("Error getting comments", e); } } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/api/comments/User.java ================================================ package com.maxwai.nclientv3.api.comments; import android.net.Uri; import android.os.Parcel; import android.os.Parcelable; import android.util.JsonReader; import android.util.JsonToken; import com.maxwai.nclientv3.utility.Utility; import java.io.IOException; import java.util.Locale; public class User implements Parcelable { public static final Creator CREATOR = new Creator<>() { @Override public User createFromParcel(Parcel in) { return new User(in); } @Override public User[] newArray(int size) { return new User[size]; } }; private int id; private String username, avatarUrl; public User(JsonReader reader) throws IOException { reader.beginObject(); while (reader.peek() != JsonToken.END_OBJECT) { switch (reader.nextName()) { case "id": id = reader.nextInt(); break; case "post_date": username = reader.nextString(); break; case "avatar_url": avatarUrl = reader.nextString(); break; default: reader.skipValue(); break; } } reader.endObject(); } protected User(Parcel in) { id = in.readInt(); username = in.readString(); avatarUrl = in.readString(); } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeInt(id); dest.writeString(username); dest.writeString(avatarUrl); } public int getId() { return id; } public Uri getAvatarUrl() { return Uri.parse(String.format(Locale.US, "https://i.%s/%s", Utility.getHost(), avatarUrl)); } public String getUsername() { return username; } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/api/components/Gallery.java ================================================ package com.maxwai.nclientv3.api.components; import android.content.Context; import android.database.Cursor; import android.net.Uri; import android.os.Build; import android.os.Parcel; import android.util.JsonReader; import android.util.JsonWriter; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.maxwai.nclientv3.api.SimpleGallery; import com.maxwai.nclientv3.api.enums.Language; import com.maxwai.nclientv3.api.enums.SpecialTagIds; import com.maxwai.nclientv3.api.enums.TagStatus; import com.maxwai.nclientv3.api.enums.TagType; import com.maxwai.nclientv3.api.enums.TitleType; import com.maxwai.nclientv3.async.database.Queries; import com.maxwai.nclientv3.components.classes.Size; import com.maxwai.nclientv3.files.GalleryFolder; import com.maxwai.nclientv3.files.PageFile; import com.maxwai.nclientv3.settings.Global; import com.maxwai.nclientv3.utility.LogUtility; import java.io.IOException; import java.io.StringReader; import java.io.Writer; import java.util.ArrayList; import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Objects; import java.util.Set; public class Gallery extends GenericGallery { public static final Creator CREATOR = new Creator<>() { @Override public Gallery createFromParcel(Parcel in) { LogUtility.d("Reading to parcel"); return new Gallery(in); } @Override public Gallery[] newArray(int size) { return new Gallery[size]; } }; @NonNull private final GalleryData galleryData; @Nullable private final GalleryFolder folder; private final boolean onlineFavorite; private List related = new ArrayList<>(); private Language language = Language.UNKNOWN; private Size maxSize = new Size(0, 0), minSize = new Size(Integer.MAX_VALUE, Integer.MAX_VALUE); public Gallery(Context context, String json, List relatedList, boolean isFavorite) throws IOException { LogUtility.d("Found JSON: " + json); JsonReader reader = new JsonReader(new StringReader(json)); this.related = relatedList != null ? relatedList : new ArrayList<>(); galleryData = new GalleryData(reader); folder = GalleryFolder.fromId(context, galleryData.getId()); calculateSizes(galleryData); language = loadLanguage(getTags()); onlineFavorite = isFavorite; } public Gallery(Context context, Cursor cursor, TagList tags) { maxSize.setWidth(cursor.getInt(Queries.getColumnFromName(cursor, Queries.GalleryTable.MAX_WIDTH))); maxSize.setHeight(cursor.getInt(Queries.getColumnFromName(cursor, Queries.GalleryTable.MAX_HEIGHT))); minSize.setWidth(cursor.getInt(Queries.getColumnFromName(cursor, Queries.GalleryTable.MIN_WIDTH))); minSize.setHeight(cursor.getInt(Queries.getColumnFromName(cursor, Queries.GalleryTable.MIN_HEIGHT))); galleryData = new GalleryData(context, cursor, tags); folder = GalleryFolder.fromId(null, galleryData.getId()); this.language = loadLanguage(tags); onlineFavorite = false; LogUtility.d(toString()); } private Gallery() { onlineFavorite = false; galleryData = GalleryData.fakeData(); folder = null; } public Gallery(Parcel in) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { maxSize = in.readParcelable(Size.class.getClassLoader(), Size.class); minSize = in.readParcelable(Size.class.getClassLoader(), Size.class); galleryData = Objects.requireNonNull(in.readParcelable(GalleryData.class.getClassLoader(), GalleryData.class)); folder = in.readParcelable(GalleryFolder.class.getClassLoader(), GalleryFolder.class); } else { maxSize = in.readParcelable(Size.class.getClassLoader()); minSize = in.readParcelable(Size.class.getClassLoader()); galleryData = Objects.requireNonNull(in.readParcelable(GalleryData.class.getClassLoader())); folder = in.readParcelable(GalleryFolder.class.getClassLoader()); } in.readTypedList(related, SimpleGallery.CREATOR); onlineFavorite = in.readByte() == 1; language = loadLanguage(getTags()); } public static String getPathTitle(@Nullable String title, @NonNull String defaultValue) { if (title == null) return defaultValue; String pathTitle = title.replace('/', ' ').replaceAll("[/|\\\\*\"'?:<>]", " "); while (pathTitle.contains(" ")) pathTitle = pathTitle.replace(" ", " "); return pathTitle.trim(); } public static String getPathTitle(@Nullable String title) { return getPathTitle(title, ""); } public static Language loadLanguage(TagList tags) { for (Tag tag : tags.retrieveForType(TagType.LANGUAGE)) { switch (tag.getId()) { case SpecialTagIds.LANGUAGE_JAPANESE: return Language.JAPANESE; case SpecialTagIds.LANGUAGE_ENGLISH: return Language.ENGLISH; case SpecialTagIds.LANGUAGE_CHINESE: return Language.CHINESE; } } return Language.UNKNOWN; } public static Gallery emptyGallery() { return new Gallery(); } private void calculateSizes(GalleryData galleryData) { Size actualSize; for (Page page : galleryData.getPages()) { actualSize = page.getSize(); if (actualSize.getWidth() > maxSize.getWidth()) maxSize.setWidth(actualSize.getWidth()); if (actualSize.getHeight() > maxSize.getHeight()) maxSize.setHeight(actualSize.getHeight()); if (actualSize.getWidth() < minSize.getWidth()) minSize.setWidth(actualSize.getWidth()); if (actualSize.getHeight() < minSize.getHeight()) minSize.setHeight(actualSize.getHeight()); } } public boolean isOnlineFavorite() { return onlineFavorite; } @NonNull public String getPathTitle() { return getPathTitle(getTitle()); } public Uri getCover() { if (Global.getDownloadPolicy() == Global.DataUsageType.THUMBNAIL) return getThumbnail(); return galleryData.getCover().getThumbnailPath(); } public Uri getThumbnail() { return galleryData.getThumbnail().getThumbnailPath(); } private @Nullable Uri getFileUri(int page) { if (folder == null) return null; PageFile f = folder.getPage(page + 1); if (f == null) return null; return f.toUri(); } public Uri getPageUrl(int page) { if (Global.getDownloadPolicy() == Global.DataUsageType.THUMBNAIL) return getLowPage(page); Uri uri = getFileUri(page); if (uri != null) return uri; return getHighPage(page); } public Uri getHighPage(int page) { return getPage(page).getImagePath(); } public Uri getLowPage(int page) { Uri uri = getFileUri(page); if (uri != null) return uri; return getPage(page).getThumbnailPath(); } private Page getPage(int index) { return galleryData.getPage(index); } public SimpleGallery toSimpleGallery() { return new SimpleGallery(this); } public boolean isRelatedLoaded() { return related != null; } public List getRelated() { return related; } @Override public boolean isValid() { return galleryData.isValid(); } @Override public Size getMaxSize() { return maxSize; } @Override public Size getMinSize() { return minSize; } @Override public GalleryFolder getGalleryFolder() { return folder; } @NonNull @Override public String getTitle() { String x = getTitle(Global.getTitleType()); if (x.length() > 2) return x; if ((x = getTitle(TitleType.PRETTY)).length() > 2) return x; if ((x = getTitle(TitleType.ENGLISH)).length() > 2) return x; if ((x = getTitle(TitleType.JAPANESE)).length() > 2) return x; return "Unnamed"; } public String getTitle(TitleType x) { return galleryData.getTitle(x); } public Language getLanguage() { return language; } public Date getUploadDate() { return galleryData.getUploadDate(); } public int getFavoriteCount() { return galleryData.getFavoriteCount(); } @Override public int getId() { return galleryData.getId(); } public TagList getTags() { return galleryData.getTags(); } @Override public int getPageCount() { return galleryData.getPageCount(); } @Override public Type getType() { return Type.COMPLETE; } public int getMediaId() { return galleryData.getMediaId(); } public boolean hasIgnoredTags(Set s) { for (Tag t : getTags().getAllTagsSet()) if (s.contains(t)) { LogUtility.d("Found: " + s + ",," + t.toQueryTag()); return true; } return false; } public boolean hasIgnoredTags() { Set tags = new HashSet<>(Queries.TagTable.getAllStatus(TagStatus.AVOIDED)); if (Global.removeAvoidedGalleries()) tags.addAll(Queries.TagTable.getAllOnlineBlacklisted()); return hasIgnoredTags(tags); } @Override public boolean hasGalleryData() { return true; } @NonNull @Override public GalleryData getGalleryData() { return galleryData; } public void jsonWrite(Writer ww) throws IOException { //images aren't saved JsonWriter writer = new JsonWriter(ww); writer.beginObject(); writer.name("id").value(getId()); writer.name("media_id").value(getMediaId()); writer.name("upload_date").value(getUploadDate().getTime() / 1000); writer.name("num_favorites").value(getFavoriteCount()); toJsonTitle(writer); toJsonTags(writer); writer.endObject(); writer.flush(); } private void toJsonTags(JsonWriter writer) throws IOException { writer.name("tags"); writer.beginArray(); for (Tag t : getTags().getAllTagsSet()) t.writeJson(writer); writer.endArray(); } private void toJsonTitle(JsonWriter writer) throws IOException { String title; writer.name("title"); writer.beginObject(); if ((title = getTitle(TitleType.JAPANESE)) != null) writer.name("japanese").value(title); if ((title = getTitle(TitleType.PRETTY)) != null) writer.name("pretty").value(title); if ((title = getTitle(TitleType.ENGLISH)) != null) writer.name("english").value(title); writer.endObject(); } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeParcelable(maxSize, flags); dest.writeParcelable(minSize, flags); dest.writeParcelable(galleryData, flags); dest.writeParcelable(folder, flags); dest.writeTypedList(related); dest.writeByte((byte) (onlineFavorite ? 1 : 0)); } @NonNull @Override public String toString() { return "Gallery{" + "galleryData=" + galleryData + ", language=" + language + ", maxSize=" + maxSize + ", minSize=" + minSize + ", onlineFavorite=" + onlineFavorite + '}'; } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/api/components/GalleryData.java ================================================ package com.maxwai.nclientv3.api.components; import android.content.Context; import android.database.Cursor; import android.net.Uri; import android.os.Build; import android.os.Parcel; import android.os.Parcelable; import android.util.JsonReader; import android.util.JsonToken; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.maxwai.nclientv3.api.enums.ImageType; import com.maxwai.nclientv3.api.enums.SpecialTagIds; import com.maxwai.nclientv3.api.enums.TitleType; import com.maxwai.nclientv3.async.database.Queries; import com.maxwai.nclientv3.files.GalleryFolder; import com.maxwai.nclientv3.settings.Global; import com.maxwai.nclientv3.utility.LogUtility; import com.maxwai.nclientv3.utility.Utility; import org.json.JSONException; import org.json.JSONObject; import java.io.IOException; import java.io.StringWriter; import java.net.HttpURLConnection; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.Objects; import okhttp3.Request; import okhttp3.Response; public class GalleryData implements Parcelable { public static final Creator CREATOR = new Creator<>() { @Override public GalleryData createFromParcel(Parcel in) { return new GalleryData(in); } @Override public GalleryData[] newArray(int size) { return new GalleryData[size]; } }; @NonNull private Date uploadDate = new Date(0); private int favoriteCount, id, pageCount, mediaId; @NonNull private String[] titles = new String[]{"", "", ""}; @NonNull private TagList tags = new TagList(); @NonNull private Page cover = new Page(), thumbnail = new Page(); @NonNull private ArrayList pages = new ArrayList<>(); private boolean valid = true; private boolean checkedExt = false; @Nullable private final Context context; private boolean changedInfo = false; private boolean isDeleted = false; private GalleryData(@Nullable Context context) { this.context = context; } public GalleryData(JsonReader jr) throws IOException { this((Context) null); parseJSON(jr); } public GalleryData(@NonNull Context context, Cursor cursor, @NonNull TagList tagList) { this(context); id = cursor.getInt(Queries.getColumnFromName(cursor, Queries.GalleryTable.IDGALLERY)); mediaId = cursor.getInt(Queries.getColumnFromName(cursor, Queries.GalleryTable.MEDIAID)); favoriteCount = cursor.getInt(Queries.getColumnFromName(cursor, Queries.GalleryTable.FAVORITE_COUNT)); titles[TitleType.JAPANESE.ordinal()] = cursor.getString(Queries.getColumnFromName(cursor, Queries.GalleryTable.TITLE_JP)); titles[TitleType.PRETTY.ordinal()] = cursor.getString(Queries.getColumnFromName(cursor, Queries.GalleryTable.TITLE_PRETTY)); titles[TitleType.ENGLISH.ordinal()] = cursor.getString(Queries.getColumnFromName(cursor, Queries.GalleryTable.TITLE_ENG)); uploadDate = new Date(cursor.getLong(Queries.getColumnFromName(cursor, Queries.GalleryTable.UPLOAD))); readPagePath(cursor.getString(Queries.getColumnFromName(cursor, Queries.GalleryTable.PAGES))); pageCount = pages.size(); this.tags = tagList; } protected GalleryData(Parcel in) { uploadDate = new Date(in.readLong()); favoriteCount = in.readInt(); id = in.readInt(); pageCount = in.readInt(); mediaId = in.readInt(); titles = Objects.requireNonNull(in.createStringArray()); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { context = in.readParcelable(Context.class.getClassLoader(), Context.class); tags = Objects.requireNonNull(in.readParcelable(TagList.class.getClassLoader(), TagList.class)); cover = Objects.requireNonNull(in.readParcelable(Page.class.getClassLoader(), Page.class)); thumbnail = Objects.requireNonNull(in.readParcelable(Page.class.getClassLoader(), Page.class)); } else { context = in.readParcelable(Context.class.getClassLoader()); tags = Objects.requireNonNull(in.readParcelable(TagList.class.getClassLoader())); cover = Objects.requireNonNull(in.readParcelable(Page.class.getClassLoader())); thumbnail = Objects.requireNonNull(in.readParcelable(Page.class.getClassLoader())); } pages = Objects.requireNonNull(in.createTypedArrayList(Page.CREATOR)); valid = in.readByte() != 0; } public static GalleryData fakeData() { GalleryData galleryData = new GalleryData((Context) null); galleryData.id = SpecialTagIds.INVALID_ID; galleryData.favoriteCount = -1; galleryData.pageCount = -1; galleryData.mediaId = SpecialTagIds.INVALID_ID; galleryData.pages.trimToSize(); galleryData.valid = false; return galleryData; } public boolean hasUpdatedInfo() { return changedInfo; } public boolean isDeleted() { return isDeleted; } private void parseJSON(JsonReader jr) throws IOException { jr.beginObject(); while (jr.peek() != JsonToken.END_OBJECT) { switch (jr.nextName()) { case "upload_date": uploadDate = new Date(jr.nextLong() * 1000); break; case "num_favorites": favoriteCount = jr.nextInt(); break; case "num_pages": pageCount = jr.nextInt(); break; case "media_id": if (jr.peek() == JsonToken.STRING) { mediaId = Integer.parseInt(jr.nextString()); } else { mediaId = jr.nextInt(); } break; case "id": id = jr.nextInt(); break; case "cover": cover = new Page(ImageType.COVER, jr); break; case "thumbnail": thumbnail = new Page(ImageType.THUMBNAIL, jr); break; case "pages": int actualPage = 0; jr.beginArray(); while (jr.hasNext()) pages.add(new Page(ImageType.PAGE, jr, actualPage++)); jr.endArray(); pages.trimToSize(); break; case "title": readTitles(jr); break; case "tags": readTags(jr); break; case "error": jr.skipValue(); valid = false; break; default: jr.skipValue(); break; } } jr.endObject(); } private void setTitle(TitleType type, String title) { titles[type.ordinal()] = Utility.unescapeUnicodeString(title); } private void readTitles(JsonReader jr) throws IOException { jr.beginObject(); while (jr.peek() != JsonToken.END_OBJECT) { switch (jr.nextName()) { case "japanese": setTitle(TitleType.JAPANESE, jr.peek() != JsonToken.NULL ? jr.nextString() : ""); break; case "english": setTitle(TitleType.ENGLISH, jr.peek() != JsonToken.NULL ? jr.nextString() : ""); break; case "pretty": setTitle(TitleType.PRETTY, jr.peek() != JsonToken.NULL ? jr.nextString() : ""); break; default: jr.skipValue(); break; } if (jr.peek() == JsonToken.NULL) jr.skipValue(); } jr.endObject(); } private void readTags(JsonReader jr) throws IOException { jr.beginArray(); while (jr.hasNext()) { Tag createdTag = new Tag(jr); Queries.TagTable.insert(createdTag); tags.addTag(createdTag); } jr.endArray(); tags.sort((o1, o2) -> o2.getCount() - o1.getCount()); } @NonNull public Date getUploadDate() { return uploadDate; } public int getFavoriteCount() { return favoriteCount; } public int getId() { return id; } public void setId(int id) { this.id = id; } public int getPageCount() { return pageCount; } public void setPageInfo(GalleryFolder folder) { this.pageCount = folder.getPageCount(); if (pageCount > 0) { Uri firstPage = folder.getFirstPage().toUri(); this.cover.setImagePath(firstPage); this.thumbnail.setImagePath(firstPage); } } public void setCheckedExt() { checkedExt = true; } public int getMediaId() { return mediaId; } public String getTitle(TitleType type) { return titles[type.ordinal()]; } @NonNull public TagList getTags() { return tags; } @NonNull public Page getCover() { return cover; } @NonNull public Page getThumbnail() { return thumbnail; } public Page getPage(int index) { return pages.get(index); } @NonNull public ArrayList getPages() { return pages; } public boolean isValid() { return valid; } @Override public int describeContents() { return 0; } public boolean getCheckedExt() { return checkedExt; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeLong(uploadDate.getTime()); dest.writeInt(favoriteCount); dest.writeInt(id); dest.writeInt(pageCount); dest.writeInt(mediaId); dest.writeStringArray(titles); if (context instanceof Parcelable) dest.writeParcelable((Parcelable) context, flags); else dest.writeParcelable(null, flags); dest.writeParcelable(tags, flags); dest.writeParcelable(cover, flags); dest.writeParcelable(thumbnail, flags); dest.writeTypedList(pages); dest.writeByte((byte) (valid ? 1 : 0)); } private String truncateUrl(Uri uri) { String output = uri.toString(); return output.substring(output.lastIndexOf('/')); } public String createPagePath() { StringWriter writer = new StringWriter(); writer.write(Integer.toString(pages.size())); writer.write(";"); writer.write(truncateUrl(cover.getThumbnailPath())); writer.write(";"); writer.write(truncateUrl(thumbnail.getThumbnailPath())); writer.write(";"); if (pages.isEmpty()) return writer.toString(); for (Page page : pages) { writer.write(truncateUrl(page.getImagePath())); writer.write(";"); } return writer.toString(); } private void readPagePathNew(String path) { LogUtility.d(path); String[] parts = path.split(";"); if (parts[1].startsWith("http")) { changedInfo = true; cover = new Page(ImageType.COVER, Uri.parse(parts[1])); thumbnail = new Page(ImageType.THUMBNAIL, Uri.parse(parts[2])); int absolutePage = 0; for (int i = 3; i < parts.length; i++) { pages.add(new Page(ImageType.PAGE, Uri.parse(parts[i]), null, absolutePage++)); } return; } cover = new Page(ImageType.COVER, Uri.parse("https://t1." + Utility.getHost() + "/galleries/" + mediaId + parts[1])); thumbnail = new Page(ImageType.THUMBNAIL, Uri.parse("https://t1." + Utility.getHost() + "/galleries/" + mediaId + parts[2])); int absolutePage = 0; for (int i = 3; i < parts.length; i++) { pages.add(new Page(ImageType.PAGE, Uri.parse("https://i1." + Utility.getHost() + "/galleries/" + mediaId + parts[i]), null, absolutePage++)); } } private void readPagePath(String path) { if (path.contains(";") && path.contains("/")) { readPagePathNew(path); return; } Thread updateThread = new Thread(() -> { // Old entry, needs to be updated String detailUrl = Utility.getBaseUrl() + "api/v2/galleries/" + id; try (Response resp = Global.getClient(Objects.requireNonNull(context)).newCall(new Request.Builder().url(detailUrl).build()).execute()) { String body = resp.body().string(); if (resp.code() == HttpURLConnection.HTTP_OK) { JSONObject v2 = new JSONObject(body); Gallery gallery = new Gallery(context, v2.toString(), null, false); cover = new Page(ImageType.COVER, gallery.getCover()); thumbnail = new Page(ImageType.THUMBNAIL, gallery.getThumbnail()); for (int i = 0; i < gallery.getPageCount(); i++) { pages.add(new Page(ImageType.PAGE, gallery.getHighPage(i), null, i)); } valid = true; } else if (resp.code() == HttpURLConnection.HTTP_NOT_FOUND) { isDeleted = true; } else { LogUtility.w("Got rate limit while updating favorites"); changedInfo = false; valid = false; } } catch (IOException | JSONException e) { LogUtility.e(e); } }); updateThread.start(); try { changedInfo = true; updateThread.join(); } catch (InterruptedException e) { LogUtility.w(e); } } @NonNull @Override public String toString() { return "GalleryData{" + "uploadDate=" + uploadDate + ", favoriteCount=" + favoriteCount + ", id=" + id + ", pageCount=" + pageCount + ", mediaId=" + mediaId + ", titles=" + Arrays.toString(titles) + ", tags=" + tags + ", cover=" + cover + ", thumbnail=" + thumbnail + ", pages=" + pages + ", valid=" + valid + '}'; } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/api/components/GenericGallery.java ================================================ package com.maxwai.nclientv3.api.components; import android.os.Parcelable; import androidx.annotation.NonNull; import com.maxwai.nclientv3.components.classes.Size; import com.maxwai.nclientv3.files.GalleryFolder; import com.maxwai.nclientv3.utility.Utility; import java.util.Locale; public abstract class GenericGallery implements Parcelable { public abstract int getId(); public abstract Type getType(); public abstract int getPageCount(); public abstract boolean isValid(); @NonNull public abstract String getTitle(); public abstract Size getMaxSize(); public abstract Size getMinSize(); public abstract GalleryFolder getGalleryFolder(); public String sharePageUrl(int i) { return String.format(Locale.US, "https://" + Utility.getHost() + "/g/%d/%d/", getId(), i + 1); } public boolean isLocal() { return getType() == Type.LOCAL; } public abstract boolean hasGalleryData(); public abstract GalleryData getGalleryData(); public enum Type {COMPLETE, LOCAL, SIMPLE} } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/api/components/Page.java ================================================ package com.maxwai.nclientv3.api.components; import android.net.Uri; import android.os.Build; import android.os.Parcel; import android.os.Parcelable; import android.util.JsonReader; import android.util.JsonToken; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.maxwai.nclientv3.api.enums.ImageType; import com.maxwai.nclientv3.components.classes.Size; import com.maxwai.nclientv3.utility.Utility; import java.io.IOException; import java.util.Objects; public class Page implements Parcelable { public static final Creator CREATOR = new Creator<>() { @Override public Page createFromParcel(Parcel in) { return new Page(in); } @Override public Page[] newArray(int size) { return new Page[size]; } }; private final int page; private final ImageType imageType; private Uri path; @Nullable private Uri thumbPath; private Size size = new Size(0, 0); Page() { this.imageType = ImageType.PAGE; this.page = 0; } public Page(ImageType type, JsonReader reader) throws IOException { this(type, reader, 0); } public Page(ImageType type, Uri path) { this(type, path, null, 0); } public Page(ImageType type, Uri path, @Nullable Uri thumbPath, int page) { this.imageType = type; this.path = path; this.thumbPath = thumbPath; this.page = page; } public Page(ImageType type, JsonReader reader, int page) throws IOException { this.imageType = type; this.page = page; reader.beginObject(); while (reader.peek() != JsonToken.END_OBJECT) { switch (reader.nextName()) { case "path": String prefix = imageType == ImageType.PAGE ? "i1" : "t1"; path = Uri.parse("https://" + prefix + "." + Utility.getHost() + "/" + reader.nextString()); break; case "thumbnail": thumbPath = Uri.parse("https://t1." + Utility.getHost() + "/" + reader.nextString()); break; case "width": size.setWidth(reader.nextInt()); break; case "height": size.setHeight(reader.nextInt()); break; default: reader.skipValue(); break; } } reader.endObject(); } protected Page(Parcel in) { page = in.readInt(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { size = in.readParcelable(Size.class.getClassLoader(), Size.class); } else { size = in.readParcelable(Size.class.getClassLoader()); } path = Uri.parse(in.readString()); String thumbString = in.readString(); thumbPath = Objects.requireNonNull(thumbString).isEmpty() ? null : Uri.parse(thumbString); imageType = ImageType.values()[in.readByte()]; } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeInt(page); dest.writeParcelable(size, flags); dest.writeString(path.toString()); dest.writeString(thumbPath == null ? "" : thumbPath.toString()); dest.writeByte((byte) (imageType == null ? ImageType.PAGE.ordinal() : imageType.ordinal())); } public Uri getImagePath() { return path; } public void setImagePath(Uri path) { this.path = path; } @NonNull public Uri getThumbnailPath() { return thumbPath == null ? path : thumbPath; } public Size getSize() { return size; } @NonNull @Override public String toString() { return "Page{" + "page=" + page + ", path=" + path + ", thumbPath=" + thumbPath + ", imageType=" + imageType + ", size=" + size + '}'; } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/api/components/Ranges.java ================================================ package com.maxwai.nclientv3.api.components; import android.os.Parcel; import android.os.Parcelable; import androidx.annotation.StringRes; import com.maxwai.nclientv3.R; public class Ranges implements Parcelable { public static final int UNDEFINED = -1; public static final TimeUnit UNDEFINED_DATE = null; public static final Creator CREATOR = new Creator<>() { @Override public Ranges createFromParcel(Parcel in) { return new Ranges(in); } @Override public Ranges[] newArray(int size) { return new Ranges[size]; } }; private int fromPage = UNDEFINED, toPage = UNDEFINED; private int fromDate = UNDEFINED, toDate = UNDEFINED; private TimeUnit fromDateUnit = UNDEFINED_DATE, toDateUnit = UNDEFINED_DATE; public Ranges() { } protected Ranges(Parcel in) { int date; fromPage = in.readInt(); toPage = in.readInt(); fromDate = in.readInt(); toDate = in.readInt(); date = in.readInt(); fromDateUnit = date == -1 ? UNDEFINED_DATE : TimeUnit.values()[date]; date = in.readInt(); toDateUnit = date == -1 ? UNDEFINED_DATE : TimeUnit.values()[date]; } public boolean isDefault() { return fromDate == UNDEFINED && toDate == UNDEFINED && toPage == UNDEFINED && fromPage == UNDEFINED; } public int getFromPage() { return fromPage; } public void setFromPage(int fromPage) { this.fromPage = fromPage; } public int getToPage() { return toPage; } public void setToPage(int toPage) { this.toPage = toPage; } public void setFromDate(int fromDate) { this.fromDate = fromDate; } public void setToDate(int toDate) { this.toDate = toDate; } public void setFromDateUnit(TimeUnit fromDateUnit) { this.fromDateUnit = fromDateUnit; } public void setToDateUnit(TimeUnit toDateUnit) { this.toDateUnit = toDateUnit; } public String toQuery() { StringBuilder builder = new StringBuilder(); if (fromPage != UNDEFINED && toPage != UNDEFINED && fromPage == toPage) { builder.append("pages:").append(fromPage).append(' '); } else { if (fromPage != UNDEFINED) builder.append("pages:>=").append(fromPage).append(' '); if (toPage != UNDEFINED) builder.append("pages:<=").append(toPage).append(' '); } if (fromDate != UNDEFINED && toDate != UNDEFINED && fromDate == toDate) { builder.append("uploaded:").append(fromDate).append(fromDateUnit.val); } else { if (fromDate != UNDEFINED) builder.append("uploaded:>=").append(fromDate).append(fromDateUnit.val).append(' '); if (toDate != UNDEFINED) builder.append("uploaded:<=").append(toDate).append(toDateUnit.val); } return builder.toString().trim(); } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeInt(fromPage); dest.writeInt(toPage); dest.writeInt(fromDate); dest.writeInt(toDate); dest.writeInt(fromDateUnit == UNDEFINED_DATE ? -1 : fromDateUnit.ordinal()); dest.writeInt(toDateUnit == UNDEFINED_DATE ? -1 : toDateUnit.ordinal()); } @Override public int describeContents() { return 0; } public enum TimeUnit { HOUR(R.string.hours, 'h'), DAY(R.string.days, 'd'), WEEK(R.string.weeks, 'w'), MONTH(R.string.months, 'm'), YEAR(R.string.years, 'y'); @StringRes final int string; final char val; TimeUnit(int string, char val) { this.string = string; this.val = val; } public int getString() { return string; } public char getVal() { return val; } } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/api/components/Tag.java ================================================ package com.maxwai.nclientv3.api.components; import android.os.Build; import android.os.Parcel; import android.os.Parcelable; import android.util.JsonReader; import android.util.JsonToken; import android.util.JsonWriter; import androidx.annotation.NonNull; import com.maxwai.nclientv3.api.enums.TagStatus; import com.maxwai.nclientv3.api.enums.TagType; import com.maxwai.nclientv3.utility.LogUtility; import java.io.IOException; import java.util.Locale; @SuppressWarnings("unused") public class Tag implements Parcelable { public static final Creator CREATOR = new Creator<>() { @Override public Tag createFromParcel(Parcel in) { return new Tag(in); } @Override public Tag[] newArray(int size) { return new Tag[size]; } }; private String name; private int count, id; private TagType type; private TagStatus status = TagStatus.DEFAULT; public Tag(String text) { this.count = Integer.parseInt(text.substring(0, text.indexOf(','))); text = text.substring(text.indexOf(',') + 1); this.id = Integer.parseInt(text.substring(0, text.indexOf(','))); text = text.substring(text.indexOf(',') + 1); this.type = TagType.values[Integer.parseInt(text.substring(0, text.indexOf(',')))]; this.name = text.substring(text.indexOf(',') + 1); } public Tag(String name, int count, int id, TagType type, TagStatus status) { this.name = name; this.count = count; this.id = id; this.type = type; this.status = status; } public Tag(JsonReader jr) throws IOException { jr.beginObject(); while (jr.peek() != JsonToken.END_OBJECT) { switch (jr.nextName()) { case "count": count = jr.nextInt(); break; case "type": type = TagType.typeByName(jr.nextString()); break; case "id": id = jr.nextInt(); break; case "name": name = jr.nextString(); break; case "slug": LogUtility.d("Tag slug: " + jr.nextString()); break; default: jr.skipValue(); break; } } jr.endObject(); } private Tag(Parcel in) { name = in.readString(); count = in.readInt(); id = in.readInt(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { type = in.readParcelable(TagType.class.getClassLoader(), TagType.class); } else { type = in.readParcelable(TagType.class.getClassLoader()); } status = TagStatus.values()[in.readByte()]; } public String toQueryTag(TagStatus status) { StringBuilder builder = new StringBuilder(); if (status == TagStatus.AVOIDED) builder.append('-'); builder .append(type.getSingle()) .append(':') .append('"') .append(name) .append('"'); return builder.toString(); } public String toQueryTag() { return toQueryTag(status); } public String getName() { return name; } public int getCount() { return count; } public TagStatus getStatus() { return status; } public void setStatus(TagStatus status) { this.status = status; } public int getId() { return id; } public TagStatus updateStatus() { switch (status) { case AVOIDED: return status = TagStatus.DEFAULT; case DEFAULT: return status = TagStatus.ACCEPTED; case ACCEPTED: return status = TagStatus.AVOIDED; } return null; } void writeJson(JsonWriter writer) throws IOException { writer.beginObject(); writer.name("count").value(count); writer.name("type").value(getTypeSingleName()); writer.name("id").value(id); writer.name("name").value(name); writer.endObject(); } public TagType getType() { return type; } public String getTypeSingleName() { return type.getSingle(); } @NonNull @Override public String toString() { return "Tag{" + "name='" + name + '\'' + ", count=" + count + ", id=" + id + ", type=" + type + ", status=" + status + '}'; } public String toScrapedString() { return String.format(Locale.US, "%d,%d,%d,%s", count, id, type.getId(), name); } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Tag tag = (Tag) o; return id == tag.id; } @Override public int hashCode() { return id; } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel parcel, int flags) { parcel.writeString(name); parcel.writeInt(count); parcel.writeInt(id); parcel.writeParcelable(type, flags); parcel.writeByte((byte) status.ordinal()); } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/api/components/TagList.java ================================================ package com.maxwai.nclientv3.api.components; import android.os.Parcel; import android.os.Parcelable; import com.maxwai.nclientv3.api.enums.TagType; import java.util.ArrayList; import java.util.Collection; import java.util.Comparator; import java.util.HashSet; import java.util.List; import java.util.Set; public class TagList implements Parcelable { public static final Creator CREATOR = new Creator<>() { @Override public TagList createFromParcel(Parcel in) { return new TagList(in); } @Override public TagList[] newArray(int size) { return new TagList[size]; } }; private final Tags[] tagList = new Tags[TagType.values.length]; protected TagList(Parcel in) { this(); ArrayList list = new ArrayList<>(); in.readTypedList(list, Tag.CREATOR); addTags(list); } public TagList() { for (TagType type : TagType.values) tagList[type.getId()] = new Tags(); } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeTypedList(getAllTagsList()); } public Set getAllTagsSet() { HashSet tags = new HashSet<>(); for (Tags t : tagList) tags.addAll(t); return tags; } public List getAllTagsList() { List tags = new ArrayList<>(); for (Tags t : tagList) tags.addAll(t); return tags; } public int getCount(TagType type) { return tagList[type.getId()].size(); } public Tag getTag(TagType type, int index) { return tagList[type.getId()].get(index); } public void addTag(Tag tag) { tagList[tag.getType().getId()].add(tag); } public void addTags(Collection tags) { for (Tag t : tags) addTag(t); } public List retrieveForType(TagType type) { return tagList[type.getId()]; } public void sort(Comparator comparator) { for (Tags t : tagList) t.sort(comparator); } public boolean hasTag(Tag tag) { return tagList[tag.getType().getId()].contains(tag); } public boolean hasTags(Collection tags) { for (Tag tag : tags) { if (!hasTag(tag)) { return false; } } return true; } public static class Tags extends ArrayList { public Tags() { } } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/api/enums/ApiRequestType.java ================================================ package com.maxwai.nclientv3.api.enums; public class ApiRequestType { public static final ApiRequestType BYALL = new ApiRequestType(0, false); public static final ApiRequestType BYTAG = new ApiRequestType(1, false); public static final ApiRequestType BYSEARCH = new ApiRequestType(2, false); public static final ApiRequestType BYSINGLE = new ApiRequestType(3, true); public static final ApiRequestType RELATED = new ApiRequestType(4, false); public static final ApiRequestType FAVORITE = new ApiRequestType(5, false); public static final ApiRequestType RANDOM = new ApiRequestType(6, true); public static final ApiRequestType RANDOM_FAVORITE = new ApiRequestType(7, true); public static final ApiRequestType[] values = { BYALL, BYTAG, BYSEARCH, BYSINGLE, RELATED, FAVORITE, RANDOM, RANDOM_FAVORITE }; private final byte id; private final boolean single; private ApiRequestType(int id, boolean single) { this.id = (byte) id; this.single = single; } public byte ordinal() { return id; } public boolean isSingle() { return single; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; ApiRequestType that = (ApiRequestType) o; return id == that.id; } @Override public int hashCode() { return id; } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/api/enums/ImageType.java ================================================ package com.maxwai.nclientv3.api.enums; public enum ImageType { PAGE, COVER, THUMBNAIL } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/api/enums/Language.java ================================================ package com.maxwai.nclientv3.api.enums; import com.maxwai.nclientv3.R; import java.util.Arrays; public enum Language { ENGLISH(R.string.only_english), CHINESE(R.string.only_chinese), JAPANESE(R.string.only_japanese), ALL(R.string.all_languages), UNKNOWN(R.string.unknown_language); private final int nameResId; Language(int nameResId) { this.nameResId = nameResId; } public int getNameResId() { return nameResId; } /** * @return Array without the UNKNOWN value */ public static Language[] getFilteredValuesArray() { return Arrays.stream(Language.values()) .filter(lang -> lang != Language.UNKNOWN) .toArray(Language[]::new); } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/api/enums/SortType.java ================================================ package com.maxwai.nclientv3.api.enums; import androidx.annotation.Nullable; import com.maxwai.nclientv3.R; public enum SortType { RECENT_ALL_TIME(R.string.sort_recent, null), POPULAR_ALL_TIME(R.string.sort_popular_all_time, "popular"), POPULAR_WEEKLY(R.string.sort_popular_week, "popular-week"), POPULAR_DAILY(R.string.sort_popular_day, "popular-today"), POPULAR_MONTH(R.string.sort_popoular_month, "popular-month"); private final int nameId; @Nullable private final String urlAddition; SortType(int nameId, @Nullable String urlAddition) { this.nameId = nameId; this.urlAddition = urlAddition; } public static SortType findFromAddition(@Nullable String addition) { if (addition == null) return SortType.RECENT_ALL_TIME; for (SortType t : SortType.values()) { String url = t.getUrlAddition(); if (url != null && addition.contains(url)) { return t; } } return SortType.RECENT_ALL_TIME; } public int getNameId() { return nameId; } @Nullable public String getUrlAddition() { return urlAddition; } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/api/enums/SpecialTagIds.java ================================================ package com.maxwai.nclientv3.api.enums; public class SpecialTagIds { public static final short LANGUAGE_JAPANESE = 6346; public static final short LANGUAGE_ENGLISH = 12227; public static final short LANGUAGE_CHINESE = 29963; public static final short INVALID_ID = -1; } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/api/enums/TagStatus.java ================================================ package com.maxwai.nclientv3.api.enums; public enum TagStatus { DEFAULT, AVOIDED, ACCEPTED } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/api/enums/TagType.java ================================================ package com.maxwai.nclientv3.api.enums; import android.os.Parcel; import android.os.Parcelable; import androidx.annotation.NonNull; public class TagType implements Parcelable { public static final TagType UNKNOWN = new TagType(0, "", null); public static final TagType PARODY = new TagType(1, "parody", "parodies"); public static final TagType CHARACTER = new TagType(2, "character", "characters"); public static final TagType TAG = new TagType(3, "tag", "tags"); public static final TagType ARTIST = new TagType(4, "artist", "artists"); public static final TagType GROUP = new TagType(5, "group", "groups"); public static final TagType LANGUAGE = new TagType(6, "language", null); public static final TagType CATEGORY = new TagType(7, "category", null); public static final TagType[] values = new TagType[]{UNKNOWN, PARODY, CHARACTER, TAG, ARTIST, GROUP, LANGUAGE, CATEGORY}; public static final Creator CREATOR = new Creator<>() { @Override public TagType createFromParcel(Parcel in) { return new TagType(in); } @Override public TagType[] newArray(int size) { return new TagType[size]; } }; private final byte id; private final String single, plural; private TagType(int id, String single, String plural) { this.id = (byte) id; this.single = single; this.plural = plural; } protected TagType(Parcel in) { id = in.readByte(); single = in.readString(); plural = in.readString(); } public static TagType typeByName(String name) { for (TagType t : values) if (t.getSingle().equals(name)) return t; return UNKNOWN; } public byte getId() { return id; } public String getSingle() { return single; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; TagType type = (TagType) o; return id == type.id; } @Override public int hashCode() { return id; } @NonNull @Override public String toString() { return this.single; } //start parcelable implementation @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeByte(id); dest.writeString(single); dest.writeString(plural); } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/api/enums/TitleType.java ================================================ package com.maxwai.nclientv3.api.enums; public enum TitleType { JAPANESE, PRETTY, ENGLISH } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/api/local/FakeInspector.java ================================================ package com.maxwai.nclientv3.api.local; import com.maxwai.nclientv3.LocalActivity; import com.maxwai.nclientv3.adapters.LocalAdapter; import com.maxwai.nclientv3.components.ThreadAsyncTask; import com.maxwai.nclientv3.utility.LogUtility; import java.io.File; import java.util.ArrayList; public class FakeInspector extends ThreadAsyncTask { private final ArrayList galleries; private final ArrayList invalidPaths; private LocalAdapter localAdapter; private final File folder; public FakeInspector(LocalActivity activity, File folder) { super(activity); this.folder = new File(folder, "Download"); galleries = new ArrayList<>(); invalidPaths = new ArrayList<>(); } @Override protected LocalActivity doInBackground(LocalActivity activity) { localAdapter = new LocalAdapter(activity, new ArrayList<>()); activity.setAdapter(localAdapter); if (!this.folder.exists()) return activity; publishProgress(activity); File parent = this.folder; //noinspection ResultOfMethodCallIgnored parent.mkdirs(); File[] files = parent.listFiles(); if (files == null) return activity; for (File f : files) if (f.isDirectory()) createGallery(f); for (String x : invalidPaths) LogUtility.d("Invalid path: " + x); localAdapter.addGalleries(galleries); galleries.clear(); return activity; } @Override protected void onProgressUpdate(LocalActivity values) { values.getRefresher().setRefreshing(true); } @Override protected void onPostExecute(LocalActivity activity) { activity.getRefresher().setRefreshing(false); } private void createGallery(final File file) { LocalGallery lg = new LocalGallery(file, false); if (lg.isValid()) { galleries.add(lg); if (galleries.size() == 50){ localAdapter.addGalleries(galleries); galleries.clear(); } } else { LogUtility.e(lg); invalidPaths.add(file.getAbsolutePath()); } } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/api/local/LocalGallery.java ================================================ package com.maxwai.nclientv3.api.local; import android.graphics.BitmapFactory; import android.os.Build; import android.os.Parcel; import android.util.JsonReader; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.maxwai.nclientv3.api.components.GalleryData; import com.maxwai.nclientv3.api.components.GenericGallery; import com.maxwai.nclientv3.api.enums.SpecialTagIds; import com.maxwai.nclientv3.components.classes.Size; import com.maxwai.nclientv3.files.GalleryFolder; import com.maxwai.nclientv3.files.PageFile; import com.maxwai.nclientv3.utility.LogUtility; import java.io.File; import java.io.FileReader; import java.util.Objects; import java.util.regex.Matcher; import java.util.regex.Pattern; public class LocalGallery extends GenericGallery { public static final Creator CREATOR = new Creator<>() { @Override public LocalGallery createFromParcel(Parcel in) { return new LocalGallery(in); } @Override public LocalGallery[] newArray(int size) { return new LocalGallery[size]; } }; private static final Pattern DUP_PATTERN = Pattern.compile("^(.*)\\.DUP\\d+$"); private final GalleryFolder folder; @NonNull private final GalleryData galleryData; private final String title, trueTitle; private final boolean valid; private boolean hasAdvancedData = true; @NonNull private Size maxSize = new Size(0, 0), minSize = new Size(Integer.MAX_VALUE, Integer.MAX_VALUE); public LocalGallery(@NonNull File file, boolean jumpDataRetrieve) { GalleryFolder folder1; try { folder1 = new GalleryFolder(file); } catch (IllegalArgumentException ignore) { folder1 = null; } folder = folder1; trueTitle = file.getName(); title = createTitle(file); if (jumpDataRetrieve) { galleryData = GalleryData.fakeData(); } else { galleryData = readGalleryData(); if (galleryData.getId() == SpecialTagIds.INVALID_ID) galleryData.setId(getId()); } //Start search pages //Find page with max number if (folder != null) galleryData.setPageInfo(folder); valid = folder != null && folder.getPageCount() > 0; } public LocalGallery(@NonNull File file) { this(file, false); } private LocalGallery(Parcel in) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { galleryData = Objects.requireNonNull(in.readParcelable(GalleryData.class.getClassLoader(), GalleryData.class)); maxSize = Objects.requireNonNull(in.readParcelable(Size.class.getClassLoader(), Size.class)); minSize = Objects.requireNonNull(in.readParcelable(Size.class.getClassLoader(), Size.class)); } else { galleryData = Objects.requireNonNull(in.readParcelable(GalleryData.class.getClassLoader())); maxSize = Objects.requireNonNull(in.readParcelable(Size.class.getClassLoader())); minSize = Objects.requireNonNull(in.readParcelable(Size.class.getClassLoader())); } trueTitle = in.readString(); title = in.readString(); hasAdvancedData = in.readByte() == 1; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { folder = in.readParcelable(GalleryFolder.class.getClassLoader(), GalleryFolder.class); } else { folder = in.readParcelable(GalleryFolder.class.getClassLoader()); } valid = true; } private static String createTitle(File file) { String name = file.getName(); Matcher matcher = DUP_PATTERN.matcher(name); if (!matcher.matches()) return name; String title = matcher.group(1); return title == null ? name : title; } @Override public GalleryFolder getGalleryFolder() { return folder; } @NonNull private GalleryData readGalleryData() { if (folder == null) return GalleryData.fakeData(); File nomedia = folder.getGalleryDataFile(); try (JsonReader reader = new JsonReader(new FileReader(nomedia))) { return new GalleryData(reader); } catch (Exception ignore) { } hasAdvancedData = false; return GalleryData.fakeData(); } public void calculateSizes() { for (PageFile f : folder) checkSize(f); } private void checkSize(File f) { LogUtility.d("Decoding: " + f); BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeFile(f.getAbsolutePath(), options); if (options.outWidth > maxSize.getWidth()) maxSize.setWidth(options.outWidth); if (options.outWidth < minSize.getWidth()) minSize.setWidth(options.outWidth); if (options.outHeight > maxSize.getHeight()) maxSize.setHeight(options.outHeight); if (options.outHeight < minSize.getHeight()) minSize.setHeight(options.outHeight); } @NonNull @Override public Size getMaxSize() { return maxSize; } @NonNull @Override public Size getMinSize() { return minSize; } public String getTrueTitle() { return trueTitle; } @Override public boolean hasGalleryData() { return hasAdvancedData; } @Override @NonNull public GalleryData getGalleryData() { return galleryData; } @Override public Type getType() { return Type.LOCAL; } @Override public boolean isValid() { return valid; } @Override public int getId() { return folder == null ? SpecialTagIds.INVALID_ID : folder.getId(); } @Override public int getPageCount() { return galleryData.getPageCount(); } @Override @NonNull public String getTitle() { return title; } public int getMin() { return folder.getMin(); } @NonNull public File getDirectory() { return folder.getFolder(); } @Nullable public File getPage(int index) { return folder.getPage(index); } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeParcelable(galleryData, flags); dest.writeParcelable(maxSize, flags); dest.writeParcelable(minSize, flags); dest.writeString(trueTitle); dest.writeString(title); dest.writeByte((byte) (hasAdvancedData ? 1 : 0)); dest.writeParcelable(folder, flags); } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; LocalGallery gallery = (LocalGallery) o; return folder.equals(gallery.folder); } @Override public int hashCode() { return folder.hashCode(); } @NonNull @Override public String toString() { return "LocalGallery{" + "galleryData=" + galleryData + ", title='" + title + '\'' + ", folder=" + folder + ", valid=" + valid + ", maxSize=" + maxSize + ", minSize=" + minSize + '}'; } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/api/local/LocalSortType.java ================================================ package com.maxwai.nclientv3.api.local; import androidx.annotation.NonNull; public class LocalSortType { public static final byte MASK_DESCENDING = (byte) (1 << 7); //10000000 private static final byte MASK_TYPE = (byte) (MASK_DESCENDING - 1); //01111111 @NonNull public final Type type; public final boolean descending; public LocalSortType(@NonNull Type type, boolean ascending) { this.type = type; this.descending = ascending; } public LocalSortType(int hash) { this.type = Type.values()[(hash & MASK_TYPE) % Type.values().length]; this.descending = (hash & MASK_DESCENDING) != 0; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; LocalSortType that = (LocalSortType) o; return this.type == that.type && this.descending == that.descending; } @Override public int hashCode() { int hash = type.ordinal(); if (descending) hash |= MASK_DESCENDING; return hash; } @NonNull @Override public String toString() { return "LocalSortType{" + "type=" + type + ", descending=" + descending + ", hash=" + hashCode() + '}'; } public enum Type {TITLE, DATE, PAGE_COUNT, ARTIST, GROUP, RANDOM} } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/async/MetadataFetcher.java ================================================ package com.maxwai.nclientv3.async; import android.content.Context; import com.maxwai.nclientv3.api.InspectorV3; import com.maxwai.nclientv3.api.components.Gallery; import com.maxwai.nclientv3.api.enums.SpecialTagIds; import com.maxwai.nclientv3.api.local.LocalGallery; import com.maxwai.nclientv3.settings.Global; import com.maxwai.nclientv3.utility.LogUtility; import java.io.File; import java.io.FileWriter; import java.io.IOException; public class MetadataFetcher implements Runnable { private final Context context; public MetadataFetcher(Context context) { this.context = context; } @Override public void run() { File[] files = Global.DOWNLOADFOLDER.listFiles(); if (files == null) return; for (File f : files) { if (!f.isDirectory()) continue; LocalGallery lg = new LocalGallery(f, false); if (lg.getId() == SpecialTagIds.INVALID_ID || lg.hasGalleryData()) continue; InspectorV3 inspector = InspectorV3.galleryInspector(context, lg.getId(), null); //noinspection CallToThreadRun inspector.run();//it is run, not start if (inspector.getGalleries() == null || inspector.getGalleries().isEmpty()) continue; Gallery g = (Gallery) inspector.getGalleries().get(0); try (FileWriter writer = new FileWriter(new File(lg.getDirectory(), ".nomedia"))) { g.jsonWrite(writer); } catch (IOException e) { LogUtility.e("Error saving metadata", e); } } } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/async/ScrapeTags.java ================================================ package com.maxwai.nclientv3.async; import android.content.Context; import android.content.SharedPreferences; import androidx.annotation.NonNull; import androidx.work.OneTimeWorkRequest; import androidx.work.WorkManager; import androidx.work.WorkRequest; import androidx.work.Worker; import androidx.work.WorkerParameters; import com.maxwai.nclientv3.api.components.Tag; import com.maxwai.nclientv3.api.enums.TagStatus; import com.maxwai.nclientv3.api.enums.TagType; import com.maxwai.nclientv3.async.database.Queries; import com.maxwai.nclientv3.settings.Global; import com.maxwai.nclientv3.utility.LogUtility; import org.json.JSONArray; import org.json.JSONException; import java.io.IOException; import java.util.ArrayList; import java.util.Calendar; import java.util.Date; import java.util.List; import okhttp3.Request; import okhttp3.Response; import okhttp3.ResponseBody; public class ScrapeTags extends Worker { private static final int DAYS_UNTIL_SCRAPE = 7; private static final String DATA_FOLDER = "https://raw.githubusercontent.com/maxwai/NClientV3/main/data/"; private static final String TAGS = DATA_FOLDER + "tags.json"; private static final String VERSION = DATA_FOLDER + "tagsVersion"; public ScrapeTags(@NonNull Context context, @NonNull WorkerParameters params) { super(context, params); } public static void startWork(Context context) { WorkRequest scrapeTagsWorkRequest = new OneTimeWorkRequest.Builder(ScrapeTags.class).build(); WorkManager.getInstance(context).enqueue(scrapeTagsWorkRequest); } private int getNewVersionCode() throws IOException { try (Response x = Global.getClient(getApplicationContext()).newCall(new Request.Builder().url(VERSION).build()).execute()) { ResponseBody body = x.body(); try { int k = Integer.parseInt(body.string().trim()); LogUtility.d("Found version: " + k); return k; } catch (NumberFormatException e) { LogUtility.e("Unable to convert", e); } } return -1; } @NonNull @Override public Result doWork() { SharedPreferences preferences = getApplicationContext().getSharedPreferences("Settings", 0); Date nowTime = new Date(); Date lastTime = new Date(preferences.getLong("lastSync", nowTime.getTime())); int lastVersion = preferences.getInt("lastTagsVersion", -1), newVersion; if (!enoughDayPassed(nowTime, lastTime)) return Result.retry(); LogUtility.d("Scraping tags"); try { newVersion = getNewVersionCode(); if (lastVersion > -1 && lastVersion >= newVersion) return Result.success(); List tags = Queries.TagTable.getAllFiltered(); fetchTags(); for (Tag t : tags) Queries.TagTable.updateStatus(t.getId(), t.getStatus()); } catch (IOException | JSONException e) { LogUtility.w("Error updating Tags", e); return Result.failure(); } LogUtility.d("End scraping"); preferences.edit() .putLong("lastSync", nowTime.getTime()) .putInt("lastTagsVersion", newVersion) .apply(); return Result.success(); } private void fetchTags() throws IOException, JSONException { try (Response x = Global.getClient(getApplicationContext()) .newCall(new Request.Builder().url(TAGS).build()) .execute()) { ResponseBody body = x.body(); JSONArray rootArray = new JSONArray(body.string()); int size = rootArray.length(); int batchSize = 5000; try { List tags = new ArrayList<>(batchSize); for (int i = 0; i <= size / batchSize; i++) { tags.clear(); for (int j = i * batchSize; j < i * batchSize + batchSize && j < size; j++) { JSONArray entry = rootArray.getJSONArray(j); tags.add(readTag(entry)); } Queries.TagTable.insertScrape(tags, true); } } catch (JSONException ignored) { throw new JSONException("Something went wrong parsing json"); } } } private Tag readTag(JSONArray reader) throws JSONException { int id = reader.getInt(0); String name = reader.getString(1); int count = reader.getInt(2); TagType type = TagType.values[reader.getInt(3)]; return new Tag(name, count, id, type, TagStatus.DEFAULT); } private boolean enoughDayPassed(Date nowTime, Date lastTime) { //first start or never completed if (nowTime.getTime() == lastTime.getTime()) return true; int daysBetween = 0; Calendar now = Calendar.getInstance(), last = Calendar.getInstance(); now.setTime(nowTime); last.setTime(lastTime); while (last.before(now)) { last.add(Calendar.DAY_OF_MONTH, 1); daysBetween++; if (daysBetween > DAYS_UNTIL_SCRAPE) return true; } LogUtility.d("Passed " + daysBetween + " days since last scrape"); return false; } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/async/VersionChecker.java ================================================ package com.maxwai.nclientv3.async; import android.Manifest; import android.annotation.SuppressLint; import android.content.Intent; import android.net.Uri; import android.util.JsonReader; import android.util.JsonToken; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatActivity; import androidx.core.content.FileProvider; import com.maxwai.nclientv3.BuildConfig; import com.maxwai.nclientv3.R; import com.maxwai.nclientv3.settings.Global; import com.maxwai.nclientv3.utility.LogUtility; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.Objects; import java.util.regex.Matcher; import java.util.regex.Pattern; import okhttp3.Call; import okhttp3.Callback; import okhttp3.Request; import okhttp3.Response; public class VersionChecker { private static final String RELEASE_API_URL = "https://api.github.com/repos/maxwai/NClientV3/releases"; private static final String LATEST_RELEASE_URL = "https://github.com/maxwai/NClientV3/releases/latest"; private static String latest = null; private final AppCompatActivity context; private String downloadUrl; public VersionChecker(AppCompatActivity context, final boolean silent) { boolean withPrerelease = Global.isEnableBeta(); this.context = context; if (latest != null && Global.hasStoragePermission(context)) { downloadVersion(latest); latest = null; return; } String actualVersionName = Global.getVersionName(context); LogUtility.d("ACTUAL VERSION: " + actualVersionName); Global.getClient(context).newCall(new Request.Builder().url(RELEASE_API_URL).build()).enqueue(new Callback() { @Override public void onFailure(@NonNull Call call, @NonNull IOException e) { context.runOnUiThread(() -> { LogUtility.e(e.getLocalizedMessage(), e); if (!silent) Toast.makeText(context, R.string.error_retrieving, Toast.LENGTH_SHORT).show(); }); } @Override public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException { GitHubRelease release; try (JsonReader jr = new JsonReader(response.body().charStream())) { release = parseVersionJson(jr, withPrerelease); } if (release == null) { release = new GitHubRelease(); release.versionCode = actualVersionName; } downloadUrl = release.downloadUrl; GitHubRelease finalRelease = release; context.runOnUiThread(() -> { boolean newer; try { newer = finalRelease.isNewerThenVersion(actualVersionName); } catch (IllegalStateException ignored) { newer = false; } if (downloadUrl == null || !newer) { if (!silent) Toast.makeText(context, R.string.no_updates_found, Toast.LENGTH_SHORT).show(); } else { LogUtility.d("Executing false"); createDialog(actualVersionName, finalRelease); } }); } }); } private static GitHubRelease parseVersionJson(JsonReader jr, boolean withPrerelease) throws IOException { try { GitHubRelease release; jr.beginArray(); while (jr.hasNext()) { release = parseVersion(jr, withPrerelease); if (release != null) return release; } } catch (IllegalStateException ignore) { } return null; } private static GitHubRelease parseVersion(JsonReader jr, boolean withPrerelease) throws IOException { GitHubRelease release = new GitHubRelease(); boolean invalid = false; jr.beginObject(); while (jr.peek() != JsonToken.END_OBJECT) { switch (jr.nextName()) { case "tag_name": release.versionCode = jr.nextString(); break; case "body": release.body = jr.nextString(); break; case "prerelease": release.beta = jr.nextBoolean(); if (release.beta && !withPrerelease) invalid = true; break; case "assets": jr.beginArray(); while (jr.hasNext()) { if (release.downloadUrl != null) { jr.skipValue(); continue; } release.downloadUrl = getDownloadUrl(jr); //noinspection ConstantValue if ((BuildConfig.FLAVOR.equals("pre28") && !release.downloadUrl.contains("pre28")) || (BuildConfig.FLAVOR.equals("post28") && release.downloadUrl.contains("pre28"))) release.downloadUrl = null; } jr.endArray(); break; default: jr.skipValue(); break; } } jr.endObject(); return invalid ? null : release; } private static String getDownloadUrl(JsonReader jr) throws IOException { String url = null; jr.beginObject(); while (jr.peek() != JsonToken.END_OBJECT) { if ("browser_download_url".equals(jr.nextName())) url = jr.nextString(); else jr.skipValue(); } jr.endObject(); return url; } private void createDialog(String versionName, GitHubRelease release) { String finalBody = release.body; String latestVersion = release.versionCode; boolean beta = release.beta; if (finalBody == null) return; finalBody = finalBody .replace("\r\n", "\n")//Remove ugly newline .replace("NClientV3 " + latestVersion, "")//remove version header .replaceAll("(\\s*\n\\s*)+", "\n")//remove multiple newline .replaceAll("\\(.*\\)", "").trim();//remove things between () LogUtility.d("Evaluated: " + finalBody); LogUtility.d("Creating dialog"); MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(context); LogUtility.d("" + context); builder.setTitle(beta ? R.string.new_beta_version_found : R.string.new_version_found); builder.setIcon(R.drawable.ic_file); builder.setMessage(context.getString(R.string.update_version_format, versionName, latestVersion, finalBody)); builder.setPositiveButton(R.string.install, (dialog, which) -> { if (Global.hasStoragePermission(context)) downloadVersion(latestVersion); else { latest = latestVersion; context.runOnUiThread(() -> context.requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE}, 2)); } }).setNegativeButton(R.string.cancel, null) .setNeutralButton(R.string.github, (dialog, which) -> { Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(LATEST_RELEASE_URL)); context.startActivity(browserIntent); }); if (!context.isFinishing()) builder.show(); } private void downloadVersion(String latestVersion) { final File f = new File(Global.UPDATEFOLDER, "NClientV3_" + latestVersion + ".apk"); if (f.exists()) { if (context.getSharedPreferences("Settings", 0).getBoolean("downloaded", false)) { installApp(f); return; } //noinspection ResultOfMethodCallIgnored f.delete(); } if (downloadUrl == null) return; LogUtility.d(f.getAbsolutePath()); Global.getClient(context).newCall(new Request.Builder().url(downloadUrl).build()).enqueue(new Callback() { @Override public void onFailure(@NonNull Call call, @NonNull IOException e) { context.runOnUiThread(() -> Toast.makeText(context, R.string.download_update_failed, Toast.LENGTH_LONG).show()); } @Override public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException { context.getSharedPreferences("Settings", 0).edit().putBoolean("downloaded", false).apply(); if (Global.UPDATEFOLDER == null) { Global.initStorage(context); } //noinspection ResultOfMethodCallIgnored Global.UPDATEFOLDER.mkdirs(); //noinspection ResultOfMethodCallIgnored f.createNewFile(); try (FileOutputStream stream = new FileOutputStream(f); InputStream stream1 = response.body().byteStream()) { int read; byte[] bytes = new byte[1024]; while ((read = stream1.read(bytes)) != -1) { stream.write(bytes, 0, read); } stream.flush(); } context.getSharedPreferences("Settings", 0).edit().putBoolean("downloaded", true).apply(); installApp(f); } }); } private void installApp(File f) { try { Uri apkUri = FileProvider.getUriForFile(context, context.getPackageName() + ".provider", f); @SuppressLint("RequestInstallPackagesPolicy") Intent intent = new Intent(Intent.ACTION_INSTALL_PACKAGE); intent.setData(apkUri); intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); context.startActivity(intent); } catch (IllegalArgumentException ignore) { context.runOnUiThread(() -> Toast.makeText(context, context.getString(R.string.downloaded_update_at, f.getAbsolutePath()), Toast.LENGTH_SHORT).show()); } } public static class GitHubRelease { // regex to get version in the form: MM.mm.pp(-suffix) private final static String regex = "(\\d+)\\.(\\d+)\\.(\\d+)(?:-((?:pre|rc)\\d))?"; String versionCode, body, downloadUrl; boolean beta; public boolean isNewerThenVersion(String currentVersion) throws IllegalArgumentException { Matcher matcher = Pattern.compile(regex).matcher(currentVersion); if (!matcher.find()) { throw new IllegalArgumentException("Current Version not in format"); } int[] curNumbers = new int[3]; String curSuffix; try { curNumbers[0] = Integer.parseInt(Objects.requireNonNull(matcher.group(1))); curNumbers[1] = Integer.parseInt(Objects.requireNonNull(matcher.group(2))); curNumbers[2] = Integer.parseInt(Objects.requireNonNull(matcher.group(3))); curSuffix = matcher.group(4); } catch (NumberFormatException ignored) { throw new IllegalStateException("Current Version not in format"); } matcher = Pattern.compile(regex).matcher(versionCode); if (!matcher.find()) { throw new IllegalArgumentException("New Version not in format"); } int[] newNumbers = new int[3]; String newSuffix; try { newNumbers[0] = Integer.parseInt(Objects.requireNonNull(matcher.group(1))); newNumbers[1] = Integer.parseInt(Objects.requireNonNull(matcher.group(2))); newNumbers[2] = Integer.parseInt(Objects.requireNonNull(matcher.group(3))); newSuffix = matcher.group(4); } catch (NumberFormatException ignored) { throw new IllegalStateException("New Version not in format"); } for (int i = 0; i < curNumbers.length; i++) { if (newNumbers[i] < curNumbers[i]) { return false; } else if (newNumbers[i] > curNumbers[i]) { return true; } } // At this point only the suffix may be different if (curSuffix == null && newSuffix == null) { return false; } else if (curSuffix == null) { // newSuffix != null // our current version doesn't have a suffix but the newest has one. // as suffixes are only for pre release, we have a newer version return false; } else if (newSuffix == null) { // curSuffix != null // our current version has a suffix but the newest has none. // as suffixes are only for pre release, there is a newer version return true; } // Hierarchy of pre release: preX -> rcX if (curSuffix.startsWith("pre") && newSuffix.startsWith("rc")) { return true; } else if (curSuffix.startsWith("rc") && newSuffix.startsWith("pre")) { return false; } if (curSuffix.startsWith("pre") && newSuffix.startsWith("pre")) { int curPreRelease, newPreRelease; try { curPreRelease = Integer.parseInt(curSuffix.substring(3)); newPreRelease = Integer.parseInt(newSuffix.substring(3)); } catch (NumberFormatException ignored) { throw new IllegalStateException("New Version not in format"); } return newPreRelease > curPreRelease; } if (curSuffix.startsWith("rc") && newSuffix.startsWith("rc")) { int curPreRelease, newPreRelease; try { curPreRelease = Integer.parseInt(curSuffix.substring(2)); newPreRelease = Integer.parseInt(newSuffix.substring(2)); } catch (NumberFormatException ignored) { throw new IllegalStateException("New Version not in format"); } return newPreRelease > curPreRelease; } throw new IllegalStateException("Suffix was not formated as expected"); } } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/async/converters/CreatePdfOrZip.java ================================================ package com.maxwai.nclientv3.async.converters; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.pdf.PdfDocument; import android.net.Uri; import androidx.annotation.NonNull; import androidx.core.app.NotificationCompat; import androidx.core.content.FileProvider; import androidx.work.Data; import androidx.work.OneTimeWorkRequest; import androidx.work.WorkManager; import androidx.work.WorkRequest; import androidx.work.Worker; import androidx.work.WorkerParameters; import com.maxwai.nclientv3.R; import com.maxwai.nclientv3.api.local.LocalGallery; import com.maxwai.nclientv3.settings.Global; import com.maxwai.nclientv3.settings.NotificationSettings; import com.maxwai.nclientv3.utility.LogUtility; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Random; import java.util.zip.Deflater; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; public class CreatePdfOrZip extends Worker { private static final String GALLERY_ID_KEY = "GALLERY_ID"; private static final String PDF_OR_ZIP_KEY = "PDF_OR_ZIP"; private static final Map galleryMap = Collections.synchronizedMap(new HashMap<>()); private int notId; private int totalPage; private NotificationCompat.Builder notification; public CreatePdfOrZip(@NonNull Context context, @NonNull WorkerParameters params) { super(context, params); } public static void startWork(Context context, LocalGallery gallery, boolean pdf) { int id; do { id = new Random().nextInt(Integer.MAX_VALUE); } while (galleryMap.containsKey(id)); galleryMap.put(id, gallery); WorkRequest createPdfOrZipWorkRequest = new OneTimeWorkRequest.Builder(CreatePdfOrZip.class) .setInputData(new Data.Builder() .putInt(GALLERY_ID_KEY, id) .putBoolean(PDF_OR_ZIP_KEY, pdf) .build()) .build(); WorkManager.getInstance(context).enqueue(createPdfOrZipWorkRequest); } public static boolean hasPDFCapabilities() { try { Class.forName("android.graphics.pdf.PdfDocument"); return true; } catch (ClassNotFoundException e) { return false; } } @NonNull @Override public Result doWork() { notId = NotificationSettings.getNotificationId(); System.gc(); if (!getInputData().hasKeyWithValueOfType(PDF_OR_ZIP_KEY, Boolean.class)) { return Result.failure(); } boolean pdf = getInputData().getBoolean(PDF_OR_ZIP_KEY, false); if (pdf && !hasPDFCapabilities()) { return Result.failure(); } int galleryId = getInputData().getInt(GALLERY_ID_KEY, -1); if (galleryId == -1) { return Result.failure(); } LocalGallery gallery = galleryMap.remove(galleryId); if (gallery == null) { return Result.failure(); } totalPage = gallery.getPageCount(); preExecute(gallery.getDirectory(), pdf); if (pdf) { PdfDocument document = new PdfDocument(); try { File page; for (int a = 1; a <= gallery.getPageCount(); a++) { page = gallery.getPage(a); if (page == null) continue; Bitmap bitmap = BitmapFactory.decodeFile(page.getAbsolutePath()); if (bitmap != null) { PdfDocument.PageInfo info = new PdfDocument.PageInfo.Builder(bitmap.getWidth(), bitmap.getHeight(), a).create(); PdfDocument.Page p = document.startPage(info); p.getCanvas().drawBitmap(bitmap, 0f, 0f, null); document.finishPage(p); bitmap.recycle(); } notification.setProgress(totalPage - 1, a + 1, false); NotificationSettings.notify(getApplicationContext(), notId, notification.build()); } notification.setContentText(getApplicationContext().getString(R.string.writing_pdf)); notification.setProgress(totalPage, 0, true); NotificationSettings.notify(getApplicationContext(), notId, notification.build()); try { File finalPath = Global.PDFFOLDER; //noinspection ResultOfMethodCallIgnored finalPath.mkdirs(); finalPath = new File(finalPath, gallery.getTitle() + ".pdf"); //noinspection ResultOfMethodCallIgnored finalPath.createNewFile(); LogUtility.d("Generating PDF at: " + finalPath); try (FileOutputStream out = new FileOutputStream(finalPath)) { document.writeTo(out); } notification.setProgress(0, 0, false); notification.setContentTitle(getApplicationContext().getString(R.string.created_pdf)); notification.setContentText(gallery.getTitle()); createIntentOpen(finalPath, true); NotificationSettings.notify(getApplicationContext(), notId, notification.build()); LogUtility.d(finalPath.getAbsolutePath()); } catch (IOException e) { notification.setContentTitle(getApplicationContext().getString(R.string.error_pdf)); notification.setContentText(getApplicationContext().getString(R.string.failed)); notification.setProgress(0, 0, false); NotificationSettings.notify(getApplicationContext(), notId, notification.build()); LogUtility.e(new RuntimeException("Error generating file", e)); return Result.failure(); } } finally { document.close(); } } else { try { File file = new File(Global.ZIPFOLDER, gallery.getTitle() + ".zip"); FileOutputStream o = new FileOutputStream(file); try (ZipOutputStream out = new ZipOutputStream(o)) { out.setLevel(Deflater.BEST_COMPRESSION); File actual; int read; byte[] buffer = new byte[1024]; for (int i = 1; i <= gallery.getPageCount(); i++) { actual = gallery.getPage(i); if (actual == null) continue; ZipEntry entry = new ZipEntry(actual.getName()); try (FileInputStream in = new FileInputStream(actual)) { out.putNextEntry(entry); while ((read = in.read(buffer)) != -1) { out.write(buffer, 0, read); } } out.closeEntry(); notification.setProgress(gallery.getPageCount(), i, false); NotificationSettings.notify(getApplicationContext(), notId, notification.build()); } out.flush(); } postExecutePdf(true, gallery, null, file); } catch (IOException e) { LogUtility.e(e.getLocalizedMessage(), e); postExecutePdf(false, gallery, e.getLocalizedMessage(), null); return Result.failure(); } } return Result.success(); } private void postExecutePdf(boolean success, LocalGallery gallery, String localizedMessage, File file) { notification.setProgress(0, 0, false) .setContentTitle(success ? getApplicationContext().getString(R.string.created_zip) : getApplicationContext().getString(R.string.failed_zip)); if (!success) { notification.setStyle(new NotificationCompat.BigTextStyle() .bigText(gallery.getTitle()) .setSummaryText(localizedMessage)); } else { createIntentOpen(file, false); } NotificationSettings.notify(getApplicationContext(), notId, notification.build()); } private void createIntentOpen(File finalPath, boolean pdf) { try { Intent i = new Intent(Intent.ACTION_VIEW); Uri apkURI = FileProvider.getUriForFile( getApplicationContext(), getApplicationContext().getPackageName() + ".provider", finalPath); i.setDataAndType(apkURI, pdf ? "application/pdf" : "application/zip"); i.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); i.setFlags(Intent.FLAG_ACTIVITY_NO_HISTORY); List resInfoList = getApplicationContext().getPackageManager().queryIntentActivities(i, PackageManager.MATCH_DEFAULT_ONLY); for (ResolveInfo resolveInfo : resInfoList) { String packageName = resolveInfo.activityInfo.packageName; getApplicationContext().grantUriPermission(packageName, apkURI, Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION); } notification.setContentIntent(PendingIntent.getActivity(getApplicationContext(), 0, i, PendingIntent.FLAG_MUTABLE)); LogUtility.d(apkURI.toString()); } catch (IllegalArgumentException ignore) {//sometimes the uri isn't available } } private void preExecute(File file, boolean pdf) { notification = new NotificationCompat.Builder(getApplicationContext(), pdf ? Global.CHANNEL_ID2 : Global.CHANNEL_ID3); notification.setSmallIcon(pdf ? R.drawable.ic_pdf : R.drawable.ic_archive) .setOnlyAlertOnce(true) .setContentText(pdf ? getApplicationContext().getString(R.string.parsing_pages) : file.getName()) .setContentTitle(getApplicationContext().getString(pdf ? R.string.channel2_title : R.string.channel3_title)) .setProgress(pdf ? (totalPage - 1) : 1, 0, false) .setPriority(NotificationCompat.PRIORITY_DEFAULT) .setCategory(NotificationCompat.CATEGORY_STATUS); if (pdf) { notification.setStyle(new NotificationCompat.BigTextStyle().bigText(file.getName())); } NotificationSettings.notify(getApplicationContext(), notId, notification.build()); } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/async/database/DatabaseHelper.java ================================================ package com.maxwai.nclientv3.async.database; import android.annotation.SuppressLint; import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.graphics.Color; import com.maxwai.nclientv3.R; import com.maxwai.nclientv3.api.components.Gallery; import com.maxwai.nclientv3.api.components.Tag; import com.maxwai.nclientv3.api.enums.SpecialTagIds; import com.maxwai.nclientv3.api.enums.TagStatus; import com.maxwai.nclientv3.api.enums.TagType; import com.maxwai.nclientv3.components.status.StatusManager; import com.maxwai.nclientv3.settings.Database; import com.maxwai.nclientv3.utility.LogUtility; import org.json.JSONException; import java.io.IOException; import java.util.Date; import java.util.List; public class DatabaseHelper extends SQLiteOpenHelper { static final String DATABASE_NAME = "Entries.db"; private static final int DATABASE_VERSION = 13; private final Context context; public DatabaseHelper(Context context1) { super(context1, DATABASE_NAME, null, DATABASE_VERSION); this.context = context1; } @Override public void onCreate(SQLiteDatabase db) { createAllTables(db); Database.setDatabase(db); insertLanguageTags(); insertCategoryTags(); insertDefaultStatus(); //Queries.DebugDatabase.dumpDatabase(db); } private void createAllTables(SQLiteDatabase db) { db.execSQL(Queries.GalleryTable.CREATE_TABLE); db.execSQL(Queries.TagTable.CREATE_TABLE); db.execSQL(Queries.GalleryBridgeTable.CREATE_TABLE); db.execSQL(Queries.BookmarkTable.CREATE_TABLE); db.execSQL(Queries.DownloadTable.CREATE_TABLE); db.execSQL(Queries.HistoryTable.CREATE_TABLE); db.execSQL(Queries.FavoriteTable.CREATE_TABLE); db.execSQL(Queries.ResumeTable.CREATE_TABLE); db.execSQL(Queries.StatusTable.CREATE_TABLE); db.execSQL(Queries.StatusMangaTable.CREATE_TABLE); } // TODO: 28/10/20 Add search history to DB instead of shared private void insertCategoryTags() { Tag[] types = { new Tag("doujinshi", 0, 33172, TagType.CATEGORY, TagStatus.DEFAULT), new Tag("manga", 0, 33173, TagType.CATEGORY, TagStatus.DEFAULT), new Tag("misc", 0, 97152, TagType.CATEGORY, TagStatus.DEFAULT), new Tag("western", 0, 34125, TagType.CATEGORY, TagStatus.DEFAULT), new Tag("non-h", 0, 34065, TagType.CATEGORY, TagStatus.DEFAULT), new Tag("artistcg", 0, 36320, TagType.CATEGORY, TagStatus.DEFAULT), }; for (Tag t : types) Queries.TagTable.insert(t); } private void insertLanguageTags() { Tag[] languages = { new Tag("english", 0, SpecialTagIds.LANGUAGE_ENGLISH, TagType.LANGUAGE, TagStatus.DEFAULT), new Tag("japanese", 0, SpecialTagIds.LANGUAGE_JAPANESE, TagType.LANGUAGE, TagStatus.DEFAULT), new Tag("chinese", 0, SpecialTagIds.LANGUAGE_CHINESE, TagType.LANGUAGE, TagStatus.DEFAULT), }; for (Tag t : languages) Queries.TagTable.insert(t); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { Database.setDatabase(db); if (oldVersion == 2) insertLanguageTags(); if (oldVersion <= 3) insertCategoryTags(); if (oldVersion <= 4) db.execSQL(Queries.BookmarkTable.CREATE_TABLE); if (oldVersion <= 5) updateGalleryWithSizes(db); if (oldVersion <= 6) db.execSQL(Queries.DownloadTable.CREATE_TABLE); if (oldVersion <= 7) db.execSQL(Queries.HistoryTable.CREATE_TABLE); if (oldVersion <= 8) insertFavorite(context, db); if (oldVersion <= 9) addRangeColumn(db); if (oldVersion <= 10) db.execSQL(Queries.ResumeTable.CREATE_TABLE); if (oldVersion <= 11) updateFavoriteTable(db); if (oldVersion <= 12) addStatusTables(db); } private void addStatusTables(SQLiteDatabase db) { db.execSQL(Queries.StatusTable.CREATE_TABLE); db.execSQL(Queries.StatusMangaTable.CREATE_TABLE); insertDefaultStatus(); } private void insertDefaultStatus() { StatusManager.add(context.getString(R.string.default_status_1), Color.BLUE); StatusManager.add(context.getString(R.string.default_status_2), Color.GREEN); StatusManager.add(context.getString(R.string.default_status_3), Color.YELLOW); StatusManager.add(context.getString(R.string.default_status_4), Color.RED); StatusManager.add(context.getString(R.string.default_status_5), Color.GRAY); StatusManager.add(StatusManager.DEFAULT_STATUS, Color.BLACK); } private void updateFavoriteTable(SQLiteDatabase db) { db.execSQL("ALTER TABLE Favorite ADD COLUMN `time` INT NOT NULL DEFAULT " + new Date().getTime()); } private void addRangeColumn(SQLiteDatabase db) { db.execSQL("ALTER TABLE Downloads ADD COLUMN `range_start` INT NOT NULL DEFAULT -1"); db.execSQL("ALTER TABLE Downloads ADD COLUMN `range_end` INT NOT NULL DEFAULT -1"); } /** * Add all item which are favorite into the favorite table */ @SuppressLint("Range") private int[] getAllFavoriteIndex() { //noinspection deprecation try (Cursor c = Queries.GalleryTable.getAllFavoriteCursorDeprecated()) { int[] favorites = new int[c.getCount()]; int i = 0; if (c.moveToFirst()) { do { favorites[i++] = c.getInt(c.getColumnIndex(Queries.GalleryTable.IDGALLERY)); } while (c.moveToNext()); } return favorites; } } /** * Create favorite table * Get all id of favorite gallery * save all galleries * delete and recreate table without favorite column * insert all galleries again * populate favorite */ private void insertFavorite(Context context, SQLiteDatabase db) { Database.setDatabase(db); db.execSQL(Queries.FavoriteTable.CREATE_TABLE); int[] favorites = getAllFavoriteIndex(); List allGalleries = Queries.GalleryTable.getAllGalleries(context); db.execSQL(Queries.GalleryTable.DROP_TABLE); db.execSQL(Queries.GalleryTable.CREATE_TABLE); for (Gallery g : allGalleries) Queries.GalleryTable.insert(g); for (int i : favorites) Queries.FavoriteTable.insert(i); } /** * Add the columns which contains the sizes of the images */ private void updateGalleryWithSizes(SQLiteDatabase db) { db.execSQL("ALTER TABLE Gallery ADD COLUMN `maxW` INT NOT NULL DEFAULT 0"); db.execSQL("ALTER TABLE Gallery ADD COLUMN `maxH` INT NOT NULL DEFAULT 0"); db.execSQL("ALTER TABLE Gallery ADD COLUMN `minW` INT NOT NULL DEFAULT 0"); db.execSQL("ALTER TABLE Gallery ADD COLUMN `minH` INT NOT NULL DEFAULT 0"); } @Override public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) { LogUtility.d("Downgrading database from " + oldVersion + " to " + newVersion); onCreate(db); } @Override public void onOpen(SQLiteDatabase db) { super.onOpen(db); Database.setDatabase(db); Queries.GalleryTable.clearGalleries(); } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/async/database/Queries.java ================================================ package com.maxwai.nclientv3.async.database; import android.annotation.SuppressLint; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.database.DatabaseUtils; import android.database.sqlite.SQLiteDatabase; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.maxwai.nclientv3.api.InspectorV3; import com.maxwai.nclientv3.api.SimpleGallery; import com.maxwai.nclientv3.api.components.Gallery; import com.maxwai.nclientv3.api.components.GalleryData; import com.maxwai.nclientv3.api.components.GenericGallery; import com.maxwai.nclientv3.api.components.Tag; import com.maxwai.nclientv3.api.components.TagList; import com.maxwai.nclientv3.api.enums.ApiRequestType; import com.maxwai.nclientv3.api.enums.TagStatus; import com.maxwai.nclientv3.api.enums.TagType; import com.maxwai.nclientv3.api.enums.TitleType; import com.maxwai.nclientv3.async.downloader.GalleryDownloaderManager; import com.maxwai.nclientv3.async.downloader.GalleryDownloaderV2; import com.maxwai.nclientv3.components.classes.Bookmark; import com.maxwai.nclientv3.components.status.Status; import com.maxwai.nclientv3.components.status.StatusManager; import com.maxwai.nclientv3.settings.Global; import com.maxwai.nclientv3.settings.TagV2; import com.maxwai.nclientv3.utility.LogUtility; import org.json.JSONException; import java.io.FileWriter; import java.io.IOException; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Locale; @SuppressLint("Range") public class Queries { static SQLiteDatabase db; public static void setDb(SQLiteDatabase database) { db = database; } public static int getColumnFromName(Cursor cursor, String name) { return cursor.getColumnIndex(name); } /** * @noinspection unused */ public static class DebugDatabase { private static void dumpTable(String name, FileWriter sb) throws IOException { String query = "SELECT * FROM " + name; sb.write("DUMPING: "); sb.write(name); try (Cursor c = db.rawQuery(query, null)) { sb.write(" count: "); sb.write("" + c.getCount()); sb.write(": "); if (c.moveToFirst()) { do { sb.write(DatabaseUtils.dumpCurrentRowToString(c)); } while (c.moveToNext()); } } sb.append("END DUMPING\n"); } } /** * Table with information about the galleries */ public static class GalleryTable { public static final String TABLE_NAME = "Gallery"; public static final String DROP_TABLE = "DROP TABLE IF EXISTS " + TABLE_NAME; public static final String IDGALLERY = "idGallery"; public static final String TITLE_ENG = "title_eng"; public static final String TITLE_JP = "title_jp"; public static final String TITLE_PRETTY = "title_pretty"; public static final String FAVORITE_COUNT = "favorite_count"; public static final String MEDIAID = "mediaId"; public static final String FAVORITE = "favorite"; public static final String PAGES = "pages"; public static final String UPLOAD = "upload"; public static final String MAX_WIDTH = "maxW"; public static final String MAX_HEIGHT = "maxH"; public static final String MIN_WIDTH = "minW"; public static final String MIN_HEIGHT = "minH"; static final String CREATE_TABLE = "CREATE TABLE IF NOT EXISTS `Gallery` ( " + "`idGallery` INT NOT NULL PRIMARY KEY , " + "`title_eng` TINYTEXT NOT NULL, " + "`title_jp` TINYTEXT NOT NULL, " + "`title_pretty` TINYTEXT NOT NULL, " + "`favorite_count` INT NOT NULL, " + "`mediaId` INT NOT NULL, " + "`pages` TEXT NOT NULL," + "`upload` UNSIGNED BIG INT NOT NULL," +//Date "`maxW` INT NOT NULL," + "`maxH` INT NOT NULL," + "`minW` INT NOT NULL," + "`minH` INT NOT NULL" + ");"; static void clearGalleries() { db.delete(GalleryTable.TABLE_NAME, String.format(Locale.US, "%s NOT IN (SELECT %s FROM %s) AND " + "%s NOT IN (SELECT %s FROM %s) AND " + "%s NOT IN (SELECT %s FROM %s)", GalleryTable.IDGALLERY, DownloadTable.ID_GALLERY, DownloadTable.TABLE_NAME, GalleryTable.IDGALLERY, FavoriteTable.ID_GALLERY, FavoriteTable.TABLE_NAME, GalleryTable.IDGALLERY, StatusMangaTable.GALLERY, StatusMangaTable.TABLE_NAME) , null); db.delete(GalleryBridgeTable.TABLE_NAME, String.format(Locale.US, "%s NOT IN (SELECT %s FROM %s)", GalleryBridgeTable.ID_GALLERY, GalleryTable.IDGALLERY, GalleryTable.TABLE_NAME) , null); db.delete(FavoriteTable.TABLE_NAME, String.format(Locale.US, "%s NOT IN (SELECT %s FROM %s)", FavoriteTable.ID_GALLERY, GalleryTable.IDGALLERY, GalleryTable.TABLE_NAME) , null); db.delete(DownloadTable.TABLE_NAME, String.format(Locale.US, "%s NOT IN (SELECT %s FROM %s)", DownloadTable.ID_GALLERY, GalleryTable.IDGALLERY, GalleryTable.TABLE_NAME) , null); } /** * Retrieve gallery using the id * * @param id id of the gallery to retrieve */ public static Gallery galleryFromId(Context context, int id) { try (Cursor cursor = db.query(true, TABLE_NAME, null, IDGALLERY + "=?", new String[]{"" + id}, null, null, null, null)) { Gallery g = null; if (cursor.moveToFirst()) { g = cursorToGallery(context, cursor); } return g; } } /** * @return Cursor to favorite Table * @noinspection DeprecatedIsStillUsed * @deprecated This is only used to update an old database format to the newest one */ @Deprecated @NonNull public static Cursor getAllFavoriteCursorDeprecated() { LogUtility.i("FILTER IN: %;;false"); String sql = "SELECT * FROM " + TABLE_NAME + " WHERE (" + FAVORITE + " =? OR " + FAVORITE + "=3)"; sql += " AND (" + TITLE_ENG + " LIKE ? OR " + TITLE_JP + " LIKE ? OR " + TITLE_PRETTY + " LIKE ? )"; String q = "%%%"; Cursor cursor = db.rawQuery(sql, new String[]{"1", q, q, q}); LogUtility.d(sql); LogUtility.d("AFTER FILTERING: " + cursor.getCount()); LogUtility.i("END FILTER IN: %;;false"); return cursor; } /** * Retrieve all galleries inside the DB */ public static List getAllGalleries(Context context) { String query = "SELECT * FROM " + TABLE_NAME; try (Cursor cursor = db.rawQuery(query, null)) { List galleries = new ArrayList<>(cursor.getCount()); if (cursor.moveToFirst()) { do { galleries.add(cursorToGallery(context, cursor)); } while (cursor.moveToNext()); } return galleries; } } public static void insert(GenericGallery gallery) { ContentValues values = new ContentValues(12); GalleryData data = gallery.getGalleryData(); values.put(IDGALLERY, gallery.getId()); values.put(TITLE_ENG, data.getTitle(TitleType.ENGLISH)); values.put(TITLE_JP, data.getTitle(TitleType.JAPANESE)); values.put(TITLE_PRETTY, data.getTitle(TitleType.PRETTY)); values.put(FAVORITE_COUNT, data.getFavoriteCount()); values.put(MEDIAID, data.getMediaId()); values.put(PAGES, data.createPagePath()); values.put(UPLOAD, data.getUploadDate().getTime()); values.put(MAX_WIDTH, gallery.getMaxSize().getWidth()); values.put(MAX_HEIGHT, gallery.getMaxSize().getHeight()); values.put(MIN_WIDTH, gallery.getMinSize().getWidth()); values.put(MIN_HEIGHT, gallery.getMinSize().getHeight()); //Insert gallery db.insertWithOnConflict(TABLE_NAME, null, values, gallery instanceof Gallery ? SQLiteDatabase.CONFLICT_REPLACE : SQLiteDatabase.CONFLICT_IGNORE); TagTable.insertTagsForGallery(data); } /** * Convert a cursor pointing to galleries to a list of galleries, cursor not closed * * @param cursor Cursor to scroll * @return ArrayList of galleries */ static List cursorToList(Context context, Cursor cursor) { List galleries = new ArrayList<>(cursor.getCount()); if (cursor.moveToFirst()) { do { galleries.add(GalleryTable.cursorToGallery(context, cursor)); } while (cursor.moveToNext()); } return galleries; } public static void delete(int id) { db.delete(TABLE_NAME, IDGALLERY + "=?", new String[]{"" + id}); GalleryBridgeTable.deleteGallery(id); } /** * Convert a row of a cursor to a {@link Gallery} */ public static Gallery cursorToGallery(Context context, Cursor cursor) { return new Gallery(context, cursor, GalleryBridgeTable.getTagsForGallery(cursor.getInt(getColumnFromName(cursor, IDGALLERY)))); } /** * Insert max and min size of a certain {@link Gallery} */ public static void updateSizes(@Nullable Gallery gallery) { if (gallery == null) return; ContentValues values = new ContentValues(4); values.put(MAX_WIDTH, gallery.getMaxSize().getWidth()); values.put(MAX_HEIGHT, gallery.getMaxSize().getHeight()); values.put(MIN_WIDTH, gallery.getMinSize().getWidth()); values.put(MIN_HEIGHT, gallery.getMinSize().getHeight()); db.updateWithOnConflict("Gallery", values, IDGALLERY + "=?", new String[]{"" + gallery.getId()}, SQLiteDatabase.CONFLICT_IGNORE); } } public static class TagTable { public static final String TABLE_NAME = "Tags"; /** * @noinspection unused */ public static final String DROP_TABLE = "DROP TABLE IF EXISTS " + TABLE_NAME; static final String CREATE_TABLE = "CREATE TABLE IF NOT EXISTS `Tags` (" + " `idTag` INT NOT NULL PRIMARY KEY," + " `name` TEXT NOT NULL , " + "`type` TINYINT(1) NOT NULL , " + "`count` INT NOT NULL," + "`status` TINYINT(1) NOT NULL," + "`online` TINYINT(1) NOT NULL DEFAULT 0);"; static final String IDTAG = "idTag"; static final String NAME = "name"; static final String TYPE = "type"; static final String COUNT = "count"; static final String STATUS = "status"; static final String ONLINE = "online"; /** * Convert a {@link Cursor} row to a {@link Tag} */ public static Tag cursorToTag(Cursor cursor) { return new Tag( cursor.getString(cursor.getColumnIndex(NAME)), cursor.getInt(cursor.getColumnIndex(COUNT)), cursor.getInt(cursor.getColumnIndex(IDTAG)), TagType.values[cursor.getInt(cursor.getColumnIndex(TYPE))], TagStatus.values()[cursor.getInt(cursor.getColumnIndex(STATUS))] ); } /** * Fetch all rows inside a {@link Cursor} and convert them into a {@link Tag} * The {@link Cursor} passed as parameter is closed */ private static List getTagsFromCursor(Cursor cursor) { try (cursor) { List tags = new ArrayList<>(cursor.getCount()); int i = 0; if (cursor.moveToFirst()) { do { tags.add(cursorToTag(cursor)); } while (cursor.moveToNext()); } return tags; } } /** * Return a cursor which points to a list of {@link Tag} which have certain properties * * @param query Retrieve only tags which contains a certain string * @param type If not null only tags which are of a specific {@link TagType} * @param online Retrieve only tags which have been blacklisted from the main site * @param sortByName sort by name or by count */ public static Cursor getFilterCursor(@NonNull String query, TagType type, boolean online, boolean sortByName) { //create query StringBuilder sql = new StringBuilder("SELECT * FROM ").append(TABLE_NAME); sql.append(" WHERE "); sql.append(COUNT).append(">=? "); //min tag count if (!query.isEmpty()) sql.append("AND ").append(NAME).append(" LIKE ?"); //query if is used if (type != null) sql.append("AND ").append(TYPE).append("=? "); //type if is used if (online) sql.append("AND ").append(ONLINE).append("=1 "); //retrieve only online tags if (!online && type == null) sql.append("AND ").append(STATUS).append("!=0 ");//retrieve only used tags sql.append("ORDER BY "); //sort first by name if provided, the for count if (!sortByName) sql.append(COUNT).append(" DESC,"); sql.append(NAME).append(" ASC"); //create parameter list ArrayList list = new ArrayList<>(); list.add("" + TagV2.getMinCount()); //minium tags (always provided) if (!query.isEmpty()) list.add('%' + query + '%'); //query if (type != null) list.add("" + type.getId()); //type of the tag LogUtility.d("FILTER URL: " + sql + ", ARGS: " + list); return db.rawQuery(sql.toString(), list.toArray(new String[0])); } /** * Returns a List of all tags of a specific type and which have a min count * * @param type The type to fetch */ public static List getAllTagOfType(TagType type) { String query = "SELECT * FROM " + TABLE_NAME + " WHERE " + TYPE + " = ? AND " + COUNT + " >= ?"; return getTagsFromCursor(db.rawQuery(query, new String[]{"" + type.getId(), "" + TagV2.getMinCount()})); } /** * Returns a List of all tags of a specific type * * @param type The type to fetch */ public static List getTrueAllType(TagType type) { String query = "SELECT * FROM " + TABLE_NAME + " WHERE " + TYPE + " = ?"; return getTagsFromCursor(db.rawQuery(query, new String[]{"" + type.getId()})); } /** * Returns a List of all tags of a specific status * * @param status The status to fetch */ public static List getAllStatus(TagStatus status) { String query = "SELECT * FROM " + TABLE_NAME + " WHERE " + STATUS + " = ?"; return getTagsFromCursor(db.rawQuery(query, new String[]{"" + status.ordinal()})); } /** * Returns a List of all tags which are AVOIDED or ACCEPTED */ public static List getAllFiltered() { String query = "SELECT * FROM " + TABLE_NAME + " WHERE " + STATUS + " != ?"; return getTagsFromCursor(db.rawQuery(query, new String[]{"" + TagStatus.DEFAULT.ordinal()})); } /** * Returns a List of all tags which are AVOIDED or ACCEPTED of a specific type */ public static List getAllFilteredByType(TagType type) { String query = "SELECT * FROM " + TABLE_NAME + " WHERE " + STATUS + " != ?"; return getTagsFromCursor(db.rawQuery(query, new String[]{"" + TagStatus.DEFAULT.ordinal()})); } /** * Returns a List of all tags which have been blacklisted from the site */ public static List getAllOnlineBlacklisted() { String query = "SELECT * FROM " + TABLE_NAME + " WHERE " + ONLINE + " = 1"; List t = getTagsFromCursor(db.rawQuery(query, null)); for (Tag t1 : t) t1.setStatus(TagStatus.AVOIDED); return t; } /** * Returns true if the tag has been blacklisted form the main site */ public static boolean isBlackListed(Tag tag) { String query = "SELECT " + IDTAG + " FROM " + TABLE_NAME + " WHERE " + IDTAG + "=? AND " + ONLINE + "=1"; try (Cursor c = db.rawQuery(query, new String[]{"" + tag.getId()})) { return c.moveToFirst(); } } /** * Returns the tag which has a specific if, null if it does not exists */ @Nullable public static Tag getTagById(int id) { String query = "SELECT * FROM " + TABLE_NAME + " WHERE " + IDTAG + " = ?"; try (Cursor c = db.rawQuery(query, new String[]{"" + id})) { Tag t = null; if (c.moveToFirst()) t = cursorToTag(c); return t; } } public static void updateStatus(int id, TagStatus status) { ContentValues values = new ContentValues(1); values.put(STATUS, status.ordinal()); db.updateWithOnConflict(TABLE_NAME, values, IDTAG + "=?", new String[]{"" + id}, SQLiteDatabase.CONFLICT_IGNORE); } /** * Update status and count of a specific tag */ public static int updateTag(Tag tag) { insert(tag); ContentValues values = new ContentValues(2); values.put(STATUS, tag.getStatus().ordinal()); values.put(COUNT, tag.getCount()); return db.updateWithOnConflict(TABLE_NAME, values, IDTAG + "=?", new String[]{"" + tag.getId()}, SQLiteDatabase.CONFLICT_IGNORE); } public static void insert(Tag tag, boolean replace) { ContentValues values = new ContentValues(5); values.put(IDTAG, tag.getId()); values.put(NAME, tag.getName()); values.put(TYPE, tag.getType().getId()); values.put(COUNT, tag.getCount()); values.put(STATUS, tag.getStatus().ordinal()); db.insertWithOnConflict(TABLE_NAME, null, values, replace ? SQLiteDatabase.CONFLICT_REPLACE : SQLiteDatabase.CONFLICT_IGNORE); } public static void insert(Tag tag) { insert(tag, false); } public static void updateBlacklistedTag(Tag tag, boolean online) { ContentValues values = new ContentValues(1); values.put(ONLINE, online ? 1 : 0); db.updateWithOnConflict(TABLE_NAME, values, IDTAG + "=?", new String[]{"" + tag.getId()}, SQLiteDatabase.CONFLICT_IGNORE); } public static void removeAllBlacklisted() { ContentValues values = new ContentValues(1); values.put(ONLINE, 0); db.updateWithOnConflict(TABLE_NAME, values, null, null, SQLiteDatabase.CONFLICT_IGNORE); } public static void resetAllStatus() { ContentValues values = new ContentValues(1); values.put(STATUS, TagStatus.DEFAULT.ordinal()); db.updateWithOnConflict(TABLE_NAME, values, null, null, SQLiteDatabase.CONFLICT_IGNORE); } /** * Get the first count tags of type, ordered by tag count */ public static List getTopTags(TagType type, int count) { String query = "SELECT * FROM " + TABLE_NAME + " WHERE " + TYPE + "=? ORDER BY " + COUNT + " DESC LIMIT ?;"; Cursor cursor = db.rawQuery(query, new String[]{"" + type.getId(), "" + count}); return TagTable.getTagsFromCursor(cursor); } /** * Retrieve the status of a tag from the DB and set it * * @return the status if the tag exists, null otherwise */ @Nullable public static TagStatus getStatus(Tag tag) { String query = "SELECT " + STATUS + " FROM " + TABLE_NAME + " WHERE " + IDTAG + " =?"; try (Cursor c = db.rawQuery(query, new String[]{"" + tag.getId()})) { TagStatus status = null; if (c.moveToFirst()) { status = TagTable.cursorToTag(c).getStatus(); tag.setStatus(status); } return status; } } public static Tag getTagFromTagName(String name) { Tag tag = null; try (Cursor cursor = db.query(TABLE_NAME, null, NAME + "=?", new String[]{name}, null, null, null)) { if (cursor.moveToFirst()) tag = cursorToTag(cursor); return tag; } } /** * @param tagString a comma-separated list of integers (maybe vulnerable) * @return the tags with id contained inside the list */ public static TagList getTagsFromListOfInt(String tagString) { TagList tags = new TagList(); String query = "SELECT * FROM " + TABLE_NAME + " WHERE " + IDTAG + " IN (" + tagString + ")"; try (Cursor cursor = db.rawQuery(query, null)) { if (cursor.moveToFirst()) { do { tags.addTag(cursorToTag(cursor)); } while (cursor.moveToNext()); } } return tags; } /** * Return a list of tags which contain name and are of a certain type */ public static List search(String name, TagType type) { String query = "SELECT * FROM " + TABLE_NAME + " WHERE " + NAME + " LIKE ? AND " + TYPE + "=?"; LogUtility.d(query); Cursor c = db.rawQuery(query, new String[]{'%' + name + '%', "" + type.getId()}); return getTagsFromCursor(c); } /** * Search a tag by name and type * * @return The Tag if found, null otehrwise */ public static Tag searchTag(String name, TagType type) { Tag tag = null; String query = "SELECT * FROM " + TABLE_NAME + " WHERE " + NAME + " = ? AND " + TYPE + "=?"; LogUtility.d(query); try (Cursor c = db.rawQuery(query, new String[]{name, "" + type.getId()})) { if (c.moveToFirst()) tag = cursorToTag(c); return tag; } } /** * Insert all tags owned by a gallery and link it using {@link GalleryBridgeTable} */ public static void insertTagsForGallery(GalleryData gallery) { TagList tags = gallery.getTags(); int len; Tag tag; for (TagType t : TagType.values) { len = tags.getCount(t); for (int i = 0; i < len; i++) { tag = tags.getTag(t, i); TagTable.insert(tag);//Insert tag GalleryBridgeTable.insert(gallery.getId(), tag.getId());//Insert link } } } /*To avoid conflict between the import process and the ScrapeTags*/ public static void insertScrape(List tags, boolean b) { if (db.isOpen()){ db.beginTransaction(); try { for (Tag tag : tags) { insert(tag, b); } db.setTransactionSuccessful(); } finally { db.endTransaction(); } } } } public static class DownloadTable { public static final String ID_GALLERY = "id_gallery"; public static final String RANGE_START = "range_start"; public static final String RANGE_END = "range_end"; public static final String TABLE_NAME = "Downloads"; /** * @noinspection unused */ public static final String DROP_TABLE = "DROP TABLE IF EXISTS " + TABLE_NAME; static final String CREATE_TABLE = "CREATE TABLE IF NOT EXISTS `Downloads` (" + "`id_gallery` INT NOT NULL PRIMARY KEY , " + "`range_start` INT NOT NULL," + "`range_end` INT NOT NULL," + "FOREIGN KEY(`id_gallery`) REFERENCES `Gallery`(`idGallery`) ON UPDATE CASCADE ON DELETE CASCADE" + "); "; public static void addGallery(GalleryDownloaderV2 downloader) { Gallery gallery = downloader.getGallery(); Queries.GalleryTable.insert(gallery); ContentValues values = new ContentValues(3); values.put(ID_GALLERY, gallery.getId()); values.put(RANGE_START, downloader.getStart()); values.put(RANGE_END, downloader.getEnd()); db.insertWithOnConflict(TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_IGNORE); } public static void removeGallery(int id) { boolean favorite = Queries.FavoriteTable.isFavorite(id); if (!favorite) Queries.GalleryTable.delete(id); db.delete(TABLE_NAME, ID_GALLERY + "=?", new String[]{"" + id}); } public static List getAllDownloads(Context context) { String q = "SELECT * FROM %s INNER JOIN %s ON %s=%s"; String query = String.format(Locale.US, q, GalleryTable.TABLE_NAME, DownloadTable.TABLE_NAME, GalleryTable.IDGALLERY, DownloadTable.ID_GALLERY); try (Cursor c = db.rawQuery(query, null)) { List managers = new ArrayList<>(); Gallery x; GalleryDownloaderManager m; if (c.moveToFirst()) { do { x = GalleryTable.cursorToGallery(context, c); m = new GalleryDownloaderManager(context, x, c.getInt(c.getColumnIndex(RANGE_START)), c.getInt(c.getColumnIndex(RANGE_END))); managers.add(m); } while (c.moveToNext()); } return managers; } } } public static class HistoryTable { public static final String ID = "id"; public static final String MEDIAID = "mediaId"; public static final String TITLE = "title"; public static final String THUMB = "thumbType"; public static final String TIME = "time"; public static final String TABLE_NAME = "History"; /** * @noinspection unused */ public static final String DROP_TABLE = "DROP TABLE IF EXISTS " + TABLE_NAME; static final String CREATE_TABLE = "CREATE TABLE IF NOT EXISTS `History`(" + "`id` INT NOT NULL PRIMARY KEY," + "`mediaId` INT NOT NULL," + "`title` TEXT NOT NULL," + "`thumbType` TINYINT(1) NOT NULL," + "`time` INT NOT NULL" + ");"; public static void addGallery(SimpleGallery gallery) { if (gallery.getId() <= 0) return; ContentValues values = new ContentValues(5); values.put(ID, gallery.getId()); values.put(MEDIAID, gallery.getMediaId()); values.put(TITLE, gallery.getTitle()); values.put(THUMB, gallery.getThumbnail().toString()); values.put(TIME, new Date().getTime()); db.insertWithOnConflict(TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_REPLACE); cleanHistory(); } public static List getHistory() { ArrayList galleries = new ArrayList<>(); try (Cursor c = db.query(TABLE_NAME, null, null, null, null, null, TIME + " DESC", "" + Global.getMaxHistory())) { if (c.moveToFirst()) { do { galleries.add(new SimpleGallery(c)); } while (c.moveToNext()); } galleries.trimToSize(); } return galleries; } public static void emptyHistory() { db.delete(TABLE_NAME, null, null); } private static void cleanHistory() { //noinspection StatementWithEmptyBody while (db.delete(TABLE_NAME, "(SELECT COUNT(*) FROM " + TABLE_NAME + ")>? AND " + TIME + "=(SELECT MIN(" + TIME + ") FROM " + TABLE_NAME + ")", new String[]{"" + Global.getMaxHistory()}) == 1) ; } } public static class BookmarkTable { public static final String TABLE_NAME = "Bookmark"; /** * @noinspection unused */ public static final String DROP_TABLE = "DROP TABLE IF EXISTS " + TABLE_NAME; static final String URL = "url"; static final String PAGE = "page"; static final String TYPE = "type"; static final String TAG_ID = "tagId"; static final String CREATE_TABLE = "CREATE TABLE IF NOT EXISTS `Bookmark`(" + "`url` TEXT NOT NULL UNIQUE," + "`page` INT NOT NULL," + "`type` INT NOT NULL," + "`tagId` INT NOT NULL" + ");"; public static void deleteBookmark(String url) { LogUtility.d("Deleted: " + db.delete(TABLE_NAME, URL + "=?", new String[]{url})); } public static void addBookmark(InspectorV3 inspector) { Tag tag = inspector.getTag(); ContentValues values = new ContentValues(4); values.put(URL, inspector.getUrl()); values.put(PAGE, inspector.getPage()); values.put(TYPE, inspector.getRequestType().ordinal()); values.put(TAG_ID, tag == null ? 0 : tag.getId()); LogUtility.d("ADDED: " + inspector.getUrl()); db.insertWithOnConflict(TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_IGNORE); } public static List getBookmarks() { String query = "SELECT * FROM " + TABLE_NAME; try (Cursor cursor = db.rawQuery(query, null)) { List bookmarks = new ArrayList<>(cursor.getCount()); Bookmark b; LogUtility.d("This url has " + cursor.getCount()); if (cursor.moveToFirst()) { do { b = new Bookmark( cursor.getString(cursor.getColumnIndex(URL)), cursor.getInt(cursor.getColumnIndex(PAGE)), ApiRequestType.values[cursor.getInt(cursor.getColumnIndex(TYPE))], cursor.getInt(cursor.getColumnIndex(TAG_ID)) ); bookmarks.add(b); } while (cursor.moveToNext()); } return bookmarks; } } } public static class GalleryBridgeTable { public static final String TABLE_NAME = "GalleryTags"; /** * @noinspection unused */ public static final String DROP_TABLE = "DROP TABLE IF EXISTS " + TABLE_NAME; static final String CREATE_TABLE = "CREATE TABLE IF NOT EXISTS `GalleryTags` (" + "`id_gallery` INT NOT NULL , " + "`id_tag` INT NOT NULL ," + "PRIMARY KEY (`id_gallery`, `id_tag`), " + "FOREIGN KEY(`id_gallery`) REFERENCES `Gallery`(`idGallery`) ON UPDATE CASCADE ON DELETE CASCADE , " + "FOREIGN KEY(`id_tag`) REFERENCES `Tags`(`idTag`) ON UPDATE CASCADE ON DELETE RESTRICT );"; static final String ID_GALLERY = "id_gallery"; static final String ID_TAG = "id_tag"; static void insert(int galleryId, int tagId) { ContentValues values = new ContentValues(2); values.put(ID_GALLERY, galleryId); values.put(ID_TAG, tagId); db.insertWithOnConflict(TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_IGNORE); } public static void deleteGallery(int id) { db.delete(TABLE_NAME, ID_GALLERY + "=?", new String[]{"" + id}); } static Cursor getTagCursorForGallery(int id) { String query = String.format(Locale.US, "SELECT * FROM %s WHERE %s IN (SELECT %s FROM %s WHERE %s=%d)", TagTable.TABLE_NAME, TagTable.IDTAG, GalleryBridgeTable.ID_TAG, GalleryBridgeTable.TABLE_NAME, GalleryBridgeTable.ID_GALLERY, id ); return db.rawQuery(query, null); } public static TagList getTagsForGallery(int id) { Cursor c = getTagCursorForGallery(id); TagList tagList = new TagList(); List tags = TagTable.getTagsFromCursor(c); tagList.addTags(tags); return tagList; } } public static class FavoriteTable { public static final String TABLE_NAME = "Favorite"; /** * @noinspection unused */ public static final String DROP_TABLE = "DROP TABLE IF EXISTS " + TABLE_NAME; static final String CREATE_TABLE = "CREATE TABLE IF NOT EXISTS `Favorite` (" + "`id_gallery` INT NOT NULL PRIMARY KEY , " + "`time` INT NOT NULL," + "FOREIGN KEY(`id_gallery`) REFERENCES `Gallery`(`idGallery`) ON UPDATE CASCADE ON DELETE CASCADE);"; static final String ID_GALLERY = "id_gallery"; static final String TIME = "time"; private static final String TITLE_CLAUSE = String.format(Locale.US, "%s LIKE ? OR %s LIKE ? OR %s LIKE ?", GalleryTable.TITLE_ENG, GalleryTable.TITLE_JP, GalleryTable.TITLE_PRETTY ); private static final String FAVORITE_JOIN_GALLERY = String.format(Locale.US, "%s INNER JOIN %s ON %s=%s", FavoriteTable.TABLE_NAME, GalleryTable.TABLE_NAME, FavoriteTable.ID_GALLERY, GalleryTable.IDGALLERY ); public static void addFavorite(Gallery gallery) { GalleryTable.insert(gallery); FavoriteTable.insert(gallery.getId()); } static String titleTypeToColumn(TitleType type) { switch (type) { case PRETTY: return GalleryTable.TITLE_PRETTY; case ENGLISH: return GalleryTable.TITLE_ENG; case JAPANESE: return GalleryTable.TITLE_JP; } return ""; } /** * Get all favorites galleries which title contains query * * @param orderByTitle true if order by title, false order by latest * @return cursor which points to the galleries */ public static Cursor getAllFavoriteGalleriesCursor(CharSequence query, boolean orderByTitle, int limit, int offset) { String order = orderByTitle ? titleTypeToColumn(Global.getTitleType()) : FavoriteTable.TIME + " DESC"; String param = "%" + query + "%"; String limitString = String.format(Locale.US, " %d, %d ", offset, limit); return db.query(FAVORITE_JOIN_GALLERY, null, TITLE_CLAUSE, new String[]{param, param, param}, null, null, order, limitString); } /** * Get all favorites galleries * * @return cursor which points to the galleries */ public static Cursor getAllFavoriteGalleriesCursor() { String query = String.format(Locale.US, "SELECT * FROM %s WHERE %s IN (SELECT %s FROM %s)", GalleryTable.TABLE_NAME, GalleryTable.IDGALLERY, FavoriteTable.ID_GALLERY, FavoriteTable.TABLE_NAME ); return db.rawQuery(query, null); } /** * Retrieve all favorite galleries */ static List getAllFavoriteGalleries(Context context) { try (Cursor c = getAllFavoriteGalleriesCursor()) { return GalleryTable.cursorToList(context, c); } } static void insert(int galleryId) { ContentValues values = new ContentValues(2); values.put(ID_GALLERY, galleryId); values.put(TIME, new Date().getTime()); db.insertWithOnConflict(TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_IGNORE); } public static void removeFavorite(int id) { db.delete(TABLE_NAME, ID_GALLERY + "=?", new String[]{"" + id}); } public static int countFavorite(@Nullable String text) { if (text == null || text.trim().isEmpty()) return countFavorite(); int totalFavorite = 0; String param = "%" + text + "%"; try (Cursor c = db.query(FAVORITE_JOIN_GALLERY, new String[]{"COUNT(*)"}, TITLE_CLAUSE, new String[]{param, param, param}, null, null, null)) { if (c.moveToFirst()) { totalFavorite = c.getInt(0); } } return totalFavorite; } public static int countFavorite() { int totalFavorite = 0; String query = "SELECT COUNT(*) FROM " + TABLE_NAME; try (Cursor c = db.rawQuery(query, null)) { if (c.moveToFirst()) { totalFavorite = c.getInt(0); } return totalFavorite; } } public static boolean isFavorite(int id) { String query = "SELECT * FROM " + TABLE_NAME + " WHERE " + ID_GALLERY + "=?"; try (Cursor c = db.rawQuery(query, new String[]{"" + id})) { return c.moveToFirst(); } } public static void removeAllFavorite() { db.delete(TABLE_NAME, null, null); } } public static class ResumeTable { public static final String TABLE_NAME = "Resume"; /** * @noinspection unused */ public static final String DROP_TABLE = "DROP TABLE IF EXISTS " + TABLE_NAME; static final String CREATE_TABLE = "CREATE TABLE IF NOT EXISTS `Resume` (" + "`id_gallery` INT NOT NULL PRIMARY KEY , " + "`page` INT NOT NULL" + ");"; static final String ID_GALLERY = "id_gallery"; static final String PAGE = "page"; public static void insert(int id, int page) { if (id < 0) return; ContentValues values = new ContentValues(2); values.put(ID_GALLERY, id); values.put(PAGE, page); db.insertWithOnConflict(TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_REPLACE); LogUtility.d("Added bookmark to page " + page + " of id " + id); } public static int pageFromId(int id) { if (id < 0) return -1; int val = -1; try (Cursor c = db.query(TABLE_NAME, new String[]{PAGE}, ID_GALLERY + "= ?", new String[]{"" + id}, null, null, null)) { if (c.moveToFirst()) val = c.getInt(c.getColumnIndex(PAGE)); return val; } } public static void remove(int id) { db.delete(TABLE_NAME, ID_GALLERY + "= ?", new String[]{"" + id}); } } public static class StatusMangaTable { public static final String TABLE_NAME = "StatusManga"; /** * @noinspection unused */ public static final String DROP_TABLE = "DROP TABLE IF EXISTS " + TABLE_NAME; static final String CREATE_TABLE = "CREATE TABLE IF NOT EXISTS `StatusManga` (" + "`gallery` INT NOT NULL PRIMARY KEY, " + "`name` TINYTEXT NOT NULL, " + "`time` INT NOT NULL," + "FOREIGN KEY(`gallery`) REFERENCES `" + GalleryTable.TABLE_NAME + "`(`" + GalleryTable.IDGALLERY + "`) ON UPDATE CASCADE ON DELETE CASCADE," + "FOREIGN KEY(`name`) REFERENCES `" + StatusTable.TABLE_NAME + "`(`" + StatusTable.NAME + "`) ON UPDATE CASCADE ON DELETE CASCADE" + ");"; static final String NAME = "name"; static final String GALLERY = "gallery"; static final String TIME = "time"; public static void insert(GenericGallery gallery, Status status) { ContentValues values = new ContentValues(3); GalleryTable.insert(gallery); StatusTable.insert(status); values.put(NAME, status.name); values.put(GALLERY, gallery.getId()); values.put(TIME, new Date().getTime()); db.insertWithOnConflict(TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_REPLACE); } public static void remove(int id) { db.delete(TABLE_NAME, GALLERY + "=?", new String[]{"" + id}); } @NonNull public static Status getStatus(int id) { try (Cursor cursor = db.query(TABLE_NAME, new String[]{NAME}, GALLERY + "=?", new String[]{"" + id}, null, null, null)) { Status status; if (cursor.moveToFirst()) status = StatusManager.getByName(cursor.getString(cursor.getColumnIndex(NAME))); else status = StatusManager.getByName(StatusManager.DEFAULT_STATUS); return status; } } public static void insert(GenericGallery gallery, String s) { insert(gallery, StatusManager.getByName(s)); } public static void update(Status oldStatus, Status newStatus) { ContentValues values = new ContentValues(1); values.put(NAME, newStatus.name); values.put(TIME, new Date().getTime()); db.update(TABLE_NAME, values, NAME + "=?", new String[]{oldStatus.name}); } public static Cursor getGalleryOfStatus(String name, String filter, boolean sortByTitle) { String query = String.format("SELECT * FROM %s INNER JOIN %s ON %s=%s WHERE %s=? AND (%s LIKE ? OR %s LIKE ? OR %s LIKE ?) ORDER BY %s", GalleryTable.TABLE_NAME, StatusMangaTable.TABLE_NAME, GalleryTable.IDGALLERY, StatusMangaTable.GALLERY, StatusMangaTable.NAME, GalleryTable.TITLE_ENG, GalleryTable.TITLE_JP, GalleryTable.TITLE_PRETTY, sortByTitle ? FavoriteTable.titleTypeToColumn(Global.getTitleType()) : TIME + " DESC" ); String likeFilter = '%' + filter + '%'; LogUtility.d(query); return db.rawQuery(query, new String[]{name, likeFilter, likeFilter, likeFilter}); } public static int getCountPerStatus(String name) { String query = String.format("SELECT COUNT(*) FROM %s WHERE %s = ?", StatusMangaTable.TABLE_NAME, StatusMangaTable.NAME); LogUtility.d(query); int value = -1; try (Cursor cursor = db.rawQuery(query, new String[]{name})) { if (cursor.moveToFirst()) { value = cursor.getInt(0); } return value; } } public static void removeStatus(String name) { db.delete(TABLE_NAME, NAME + "=?", new String[]{name}); } } public static class StatusTable { public static final String TABLE_NAME = "Status"; /** * @noinspection unused */ public static final String DROP_TABLE = "DROP TABLE IF EXISTS " + TABLE_NAME; static final String CREATE_TABLE = "CREATE TABLE IF NOT EXISTS `Status` (" + "`name` TINYTEXT NOT NULL PRIMARY KEY, " + "`color` INT NOT NULL " + ");"; static final String NAME = "name"; static final String COLOR = "color"; public static void insert(Status status) { ContentValues values = new ContentValues(2); values.put(NAME, status.name); values.put(COLOR, status.color); db.insertWithOnConflict(TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_IGNORE); } public static void remove(String name) { db.delete(TABLE_NAME, NAME + "= ?", new String[]{name}); StatusMangaTable.removeStatus(name); } public static void initStatuses() { try (Cursor cursor = db.rawQuery("SELECT * FROM " + TABLE_NAME, null)) { if (cursor.moveToFirst()) { do { StatusManager.add( cursor.getString(cursor.getColumnIndex(NAME)), cursor.getInt(cursor.getColumnIndex(COLOR)) ); } while (cursor.moveToNext()); } } } public static void update(Status oldStatus, Status newStatus) { ContentValues values = new ContentValues(2); values.put(NAME, newStatus.name); values.put(COLOR, newStatus.color); db.update(TABLE_NAME, values, NAME + "=?", new String[]{oldStatus.name}); StatusMangaTable.update(oldStatus, newStatus); } } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/async/database/export/Exporter.java ================================================ package com.maxwai.nclientv3.async.database.export; import android.content.Context; import android.content.SharedPreferences; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.net.Uri; import android.text.format.DateFormat; import android.util.JsonWriter; import com.maxwai.nclientv3.SettingsActivity; import com.maxwai.nclientv3.async.database.Queries; import com.maxwai.nclientv3.settings.Database; import com.maxwai.nclientv3.utility.LogUtility; import java.io.IOException; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.util.Date; import java.util.Map; import java.util.Set; import java.util.zip.Deflater; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; public class Exporter { static final String DB_ZIP_FILE = "Database.json"; private static final String[] SHARED_FILES = new String[]{ "Settings", "ScrapedTags", }; private static final String[] SCHEMAS = new String[]{ Queries.GalleryTable.TABLE_NAME, Queries.TagTable.TABLE_NAME, Queries.GalleryBridgeTable.TABLE_NAME, Queries.BookmarkTable.TABLE_NAME, Queries.DownloadTable.TABLE_NAME, Queries.HistoryTable.TABLE_NAME, Queries.FavoriteTable.TABLE_NAME, Queries.ResumeTable.TABLE_NAME, Queries.StatusTable.TABLE_NAME, Queries.StatusMangaTable.TABLE_NAME, }; private static void dumpDB(OutputStream stream) throws IOException { SQLiteDatabase db = Database.getDatabase(); if (db == null) throw new IOException("Can't export Database, don't have database connection yet"); try (JsonWriter writer = new JsonWriter(new OutputStreamWriter(stream))) { writer.beginObject(); for (String s : SCHEMAS) { try (Cursor cur = db.query(s, null, null, null, null, null, null)) { writer.name(s).beginArray(); if (cur.moveToFirst()) { do { writer.beginObject(); for (int i = 0; i < cur.getColumnCount(); i++) { writer.name(cur.getColumnName(i)); if (cur.isNull(i)) { writer.nullValue(); } else { switch (cur.getType(i)) { case Cursor.FIELD_TYPE_INTEGER: writer.value(cur.getLong(i)); break; case Cursor.FIELD_TYPE_FLOAT: writer.value(cur.getDouble(i)); break; case Cursor.FIELD_TYPE_STRING: writer.value(cur.getString(i)); break; case Cursor.FIELD_TYPE_BLOB: case Cursor.FIELD_TYPE_NULL: break; } } } writer.endObject(); } while (cur.moveToNext()); } writer.endArray(); } } writer.endObject(); writer.flush(); } } public static String defaultExportName(SettingsActivity context) { Date actualTime = new Date(); String date = DateFormat.getDateFormat(context).format(actualTime).replaceAll("[^0-9]*", ""); String time = DateFormat.getTimeFormat(context).format(actualTime).replaceAll("[^0-9]*", ""); return String.format("Backup_%s_%s.zip", date, time); } public static void exportData(SettingsActivity context, Uri selectedFile) throws IOException { OutputStream outputStream = context.getContentResolver().openOutputStream(selectedFile); try (ZipOutputStream zip = new ZipOutputStream(outputStream)) { zip.setLevel(Deflater.BEST_COMPRESSION); zip.putNextEntry(new ZipEntry(DB_ZIP_FILE)); dumpDB(zip); zip.closeEntry(); for (String shared : SHARED_FILES) { zip.putNextEntry(new ZipEntry(shared + ".json")); exportSharedPreferences(context, shared, zip); zip.closeEntry(); } } } private static void exportSharedPreferences(Context context, String sharedName, OutputStream stream) throws IOException { JsonWriter writer = new JsonWriter(new OutputStreamWriter(stream)); SharedPreferences pref = context.getSharedPreferences(sharedName, 0); Map map = pref.getAll(); writer.beginObject(); for (Map.Entry o : map.entrySet()) { Object val = o.getValue(); writer.name(o.getKey()); if (val instanceof String) { writer.beginObject().name(SharedType.STRING.name()).value((String) val).endObject(); } else if (val instanceof Boolean) { writer.beginObject().name(SharedType.BOOLEAN.name()).value((Boolean) val).endObject(); } else if (val instanceof Integer) { writer.beginObject().name(SharedType.INT.name()).value((Integer) val).endObject(); } else if (val instanceof Float) { writer.beginObject().name(SharedType.FLOAT.name()).value((Float) val).endObject(); } else if (val instanceof Set) { writer.beginObject().name(SharedType.STRING_SET.name()); writer.beginArray(); Set val2 = (Set) val; if (!val2.isEmpty()) { for (Object s : (Set) val) { if (s instanceof String) { writer.value((String) s); } else { LogUtility.e("Missing export class: " + val.getClass().getName()); } } } writer.endArray(); writer.endObject(); } else if (val instanceof Long) { writer.beginObject().name(SharedType.LONG.name()).value((Long) val).endObject(); } else { LogUtility.e("Missing export class: " + val.getClass().getName()); } } writer.endObject(); writer.flush(); } enum SharedType { FLOAT, INT, LONG, STRING_SET, STRING, BOOLEAN } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/async/database/export/Importer.java ================================================ package com.maxwai.nclientv3.async.database.export; import android.content.ContentValues; import android.content.Context; import android.content.SharedPreferences; import android.database.sqlite.SQLiteDatabase; import android.net.Uri; import android.util.JsonReader; import androidx.annotation.NonNull; import com.maxwai.nclientv3.settings.Database; import com.maxwai.nclientv3.utility.LogUtility; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.util.HashSet; import java.util.Set; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; class Importer { private static void importSharedPreferences(Context context, String sharedName, InputStream stream) throws IOException { JsonReader reader = new JsonReader(new InputStreamReader(stream)); if (sharedName.contains("/")) { String[] names = sharedName.split("/"); sharedName = names[names.length - 1]; } SharedPreferences.Editor editor = context.getSharedPreferences(sharedName, 0).edit(); reader.beginObject(); while (reader.hasNext()) { String name = reader.nextName(); reader.beginObject(); Exporter.SharedType type = Exporter.SharedType.valueOf(reader.nextName()); switch (type) { case STRING: editor.putString(name, reader.nextString()); break; case INT: editor.putInt(name, reader.nextInt()); break; case FLOAT: editor.putFloat(name, (float) reader.nextDouble()); break; case LONG: editor.putLong(name, reader.nextLong()); break; case BOOLEAN: editor.putBoolean(name, reader.nextBoolean()); break; case STRING_SET: Set strings = new HashSet<>(); reader.beginArray(); while (reader.hasNext()) strings.add(reader.nextString()); reader.endArray(); editor.putStringSet(name, strings); break; } reader.endObject(); } editor.apply(); } private static void importDB(InputStream stream) throws IOException { SQLiteDatabase db = Database.getDatabase(); if (db == null) throw new IOException("Can't import Database, don't have database connection yet"); db.beginTransaction(); JsonReader reader = new JsonReader(new InputStreamReader(stream)); reader.beginObject(); while (reader.hasNext()) { String tableName = reader.nextName(); db.delete(tableName, null, null); reader.beginArray(); while (reader.hasNext()) { reader.beginObject(); ContentValues values = new ContentValues(); while (reader.hasNext()) { String fieldName = reader.nextName(); switch (reader.peek()) { case NULL: values.putNull(fieldName); reader.nextNull(); break; case NUMBER: //there are no doubles in the DB values.put(fieldName, reader.nextLong()); break; case STRING: values.put(fieldName, reader.nextString()); break; } } db.insertWithOnConflict(tableName, null, values, SQLiteDatabase.CONFLICT_REPLACE); reader.endObject(); } reader.endArray(); } reader.endObject(); db.setTransactionSuccessful(); db.endTransaction(); } public static void importData(@NonNull Context context, Uri selectedFile) throws IOException { InputStream stream = context.getContentResolver().openInputStream(selectedFile); try (ZipInputStream inputStream = new ZipInputStream(stream)) { ZipEntry entry; while ((entry = inputStream.getNextEntry()) != null) { String name = entry.getName(); LogUtility.d("Importing: " + name); if (Exporter.DB_ZIP_FILE.equals(name)) { importDB(inputStream); } else { String shared = name.substring(0, name.length() - 5); importSharedPreferences(context, shared, inputStream); } inputStream.closeEntry(); } } } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/async/database/export/Manager.java ================================================ package com.maxwai.nclientv3.async.database.export; import android.net.Uri; import androidx.annotation.NonNull; import com.maxwai.nclientv3.SettingsActivity; import com.maxwai.nclientv3.utility.LogUtility; import java.io.IOException; public class Manager extends Thread { @NonNull private final Uri file; @NonNull private final SettingsActivity context; private final boolean export; private final Runnable end; public Manager(@NonNull Uri file, @NonNull SettingsActivity context, boolean export, Runnable end) { this.file = file; this.context = context; this.export = export; this.end = end; } @Override public void run() { try { if (export) Exporter.exportData(context, file); else Importer.importData(context, file); context.runOnUiThread(end); } catch (IOException e) { LogUtility.e(e, e); } } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/async/downloader/DownloadGalleryV2.java ================================================ package com.maxwai.nclientv3.async.downloader; import android.content.Context; import android.net.Uri; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.work.OneTimeWorkRequest; import androidx.work.WorkManager; import androidx.work.WorkRequest; import androidx.work.Worker; import androidx.work.WorkerParameters; import com.maxwai.nclientv3.api.SimpleGallery; import com.maxwai.nclientv3.api.components.Gallery; import com.maxwai.nclientv3.api.components.GenericGallery; import com.maxwai.nclientv3.async.database.Queries; import com.maxwai.nclientv3.utility.LogUtility; import com.maxwai.nclientv3.utility.Utility; import org.json.JSONException; import java.io.IOException; import java.util.List; import java.util.concurrent.locks.ReentrantLock; public class DownloadGalleryV2 extends Worker { private static final ReentrantLock lock = new ReentrantLock(); public DownloadGalleryV2(@NonNull Context context, @NonNull WorkerParameters params) { super(context, params); } public static void downloadGallery(Context context, GenericGallery gallery) { if (gallery.isValid() && gallery instanceof Gallery) downloadGallery(context, (Gallery) gallery); if (gallery.getId() > 0) { if (gallery instanceof SimpleGallery) { SimpleGallery simple = (SimpleGallery) gallery; downloadGallery(context, gallery.getTitle(), simple.getThumbnail(), simple.getId()); } else downloadGallery(context, null, null, gallery.getId()); } } private static void downloadGallery(Context context, String title, Uri thumbnail, int id) { if (id < 1) return; DownloadQueue.add(new GalleryDownloaderManager(context, title, thumbnail, id)); startWork(context); } private static void downloadGallery(Context context, Gallery gallery) { downloadGallery(context, gallery, 0, gallery.getPageCount() - 1); } private static void downloadGallery(Context context, Gallery gallery, int start, int end) { DownloadQueue.add(new GalleryDownloaderManager(context, gallery, start, end)); startWork(context); } public static void loadDownloads(Context context) { List g = Queries.DownloadTable.getAllDownloads(context); for (GalleryDownloaderManager gg : g) { gg.downloader().setStatus(GalleryDownloaderV2.Status.PAUSED); DownloadQueue.add(gg); } new PageChecker().start(); startWork(context); } public static void downloadRange(Context context, Gallery gallery, int start, int end) { downloadGallery(context, gallery, start, end); } public static void startWork(@Nullable Context context) { if (context != null) { WorkRequest DownloadGalleryWorkRequest = new OneTimeWorkRequest.Builder(DownloadGalleryV2.class).build(); WorkManager.getInstance(context).enqueue(DownloadGalleryWorkRequest); } } @NonNull @Override public Result doWork() { lock.lock(); try { obtainData(); GalleryDownloaderManager entry = DownloadQueue.fetch(); if (entry != null) { LogUtility.d("Downloading: " + entry.downloader().getId()); if (entry.downloader().downloadGalleryData()) { entry.downloader().download(); } } } finally { lock.unlock(); } return Result.success(); } private void obtainData() { GalleryDownloaderV2 downloader = DownloadQueue.fetchForData(); while (downloader != null) { downloader.downloadGalleryData(); Utility.threadSleep(100); downloader = DownloadQueue.fetchForData(); } } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/async/downloader/DownloadObserver.java ================================================ package com.maxwai.nclientv3.async.downloader; public interface DownloadObserver { void triggerStartDownload(GalleryDownloaderV2 downloader); void triggerUpdateProgress(GalleryDownloaderV2 downloader, int reach, int total); void triggerEndDownload(GalleryDownloaderV2 downloader); void triggerCancelDownload(GalleryDownloaderV2 downloader); void triggerPauseDownload(GalleryDownloaderV2 downloader); } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/async/downloader/DownloadQueue.java ================================================ package com.maxwai.nclientv3.async.downloader; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; public class DownloadQueue { private static final List downloadQueue = new CopyOnWriteArrayList<>(); public static void add(GalleryDownloaderManager x) { for (GalleryDownloaderManager manager : downloadQueue) if (x.downloader().getId() == manager.downloader().getId()) { manager.downloader().setStatus(GalleryDownloaderV2.Status.NOT_STARTED); givePriority(manager.downloader()); return; } downloadQueue.add(x); } public static GalleryDownloaderV2 fetchForData() { for (GalleryDownloaderManager x : downloadQueue) if (!x.downloader().hasData()) return x.downloader(); return null; } public static GalleryDownloaderManager fetch() { for (GalleryDownloaderManager x : downloadQueue) if (x.downloader().canBeFetched()) return x; return null; } public static CopyOnWriteArrayList getDownloaders() { CopyOnWriteArrayList downloaders = new CopyOnWriteArrayList<>(); for (GalleryDownloaderManager manager : downloadQueue) downloaders.add(manager.downloader()); return downloaders; } public static void addObserver(DownloadObserver observer) { for (GalleryDownloaderManager manager : downloadQueue) manager.downloader().addObserver(observer); } public static void removeObserver(DownloadObserver observer) { for (GalleryDownloaderManager manager : downloadQueue) manager.downloader().removeObserver(observer); } private static GalleryDownloaderManager findManagerFromDownloader(GalleryDownloaderV2 downloader) { for (GalleryDownloaderManager manager : downloadQueue) if (manager.downloader() == downloader) return manager; return null; } public static void remove(GalleryDownloaderV2 downloader, boolean cancel) { GalleryDownloaderManager manager = findManagerFromDownloader(downloader); if (manager == null) return; if (cancel) downloader.setStatus(GalleryDownloaderV2.Status.CANCELED); downloadQueue.remove(manager); } public static void givePriority(GalleryDownloaderV2 downloader) { GalleryDownloaderManager manager = findManagerFromDownloader(downloader); if (manager == null) return; downloadQueue.remove(manager); downloadQueue.add(0, manager); } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/async/downloader/GalleryDownloaderManager.java ================================================ package com.maxwai.nclientv3.async.downloader; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.net.Uri; import androidx.core.app.NotificationCompat; import com.maxwai.nclientv3.GalleryActivity; import com.maxwai.nclientv3.R; import com.maxwai.nclientv3.api.components.Gallery; import com.maxwai.nclientv3.settings.Global; import com.maxwai.nclientv3.settings.NotificationSettings; import java.util.ConcurrentModificationException; import java.util.Locale; public class GalleryDownloaderManager { private final int notificationId = NotificationSettings.getNotificationId(); private final GalleryDownloaderV2 downloaderV2; private final Context context; private NotificationCompat.Builder notification; private Gallery gallery; private final DownloadObserver observer = new DownloadObserver() { @Override public void triggerStartDownload(GalleryDownloaderV2 downloader) { gallery = downloader.getGallery(); prepareNotification(); notificationUpdate(); } @Override public void triggerUpdateProgress(GalleryDownloaderV2 downloader, int reach, int total) { setPercentage(reach, total); notificationUpdate(); } @Override public void triggerEndDownload(GalleryDownloaderV2 downloader) { endNotification(); addClickListener(); notificationUpdate(); DownloadQueue.remove(downloader, false); } @Override public void triggerCancelDownload(GalleryDownloaderV2 downloader) { cancelNotification(); Global.recursiveDelete(downloader.getFolder()); } @Override public void triggerPauseDownload(GalleryDownloaderV2 downloader) { notificationUpdate(); } }; public GalleryDownloaderManager(Context context, Gallery gallery, int start, int end) { this.context = context; this.gallery = gallery; this.downloaderV2 = new GalleryDownloaderV2(context, gallery, start, end); this.downloaderV2.addObserver(observer); } public GalleryDownloaderManager(Context context, String title, Uri thumbnail, int id) { this.context = context; this.downloaderV2 = new GalleryDownloaderV2(context, title, thumbnail, id); this.downloaderV2.addObserver(observer); } private void cancelNotification() { NotificationSettings.cancel(notificationId); } private void addClickListener() { Intent notifyIntent = new Intent(context, GalleryActivity.class); notifyIntent.putExtra(context.getPackageName() + ".GALLERY", downloaderV2.localGallery()); notifyIntent.putExtra(context.getPackageName() + ".ISLOCAL", true); // Create the PendingIntent PendingIntent notifyPendingIntent; if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) { notifyPendingIntent = PendingIntent.getActivity( context, 0, notifyIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE ); } else { notifyPendingIntent = PendingIntent.getActivity( context, 0, notifyIntent, PendingIntent.FLAG_UPDATE_CURRENT ); } notification.setContentIntent(notifyPendingIntent); } public GalleryDownloaderV2 downloader() { return downloaderV2; } private void endNotification() { //notification=new NotificationCompat.Builder(context.getApplicationContext(), Global.CHANNEL_ID1); //notification.setOnlyAlertOnce(true).setSmallIcon(R.drawable.ic_check).setAutoCancel(true); hidePercentage(); if (downloaderV2.getStatus() != GalleryDownloaderV2.Status.CANCELED) { notification.setSmallIcon(R.drawable.ic_check); notification.setContentTitle(String.format(Locale.US, context.getString(R.string.completed_format), gallery.getTitle())); } else { notification.setSmallIcon(R.drawable.ic_close); notification.setContentTitle(String.format(Locale.US, context.getString(R.string.cancelled_format), gallery.getTitle())); } } private void hidePercentage() { setPercentage(0, 0); } private void setPercentage(int reach, int total) { notification.setProgress(total, reach, false); } private void prepareNotification() { notification = new NotificationCompat.Builder(context.getApplicationContext(), Global.CHANNEL_ID1); notification.setOnlyAlertOnce(true) .setContentTitle(String.format(Locale.US, context.getString(R.string.downloading_format), gallery.getTitle())) .setProgress(gallery.getPageCount(), 0, false) .setSmallIcon(R.drawable.ic_file); setPercentage(0, 1); } private synchronized void notificationUpdate() { try { NotificationSettings.notify(context, notificationId, notification.build()); } catch (NullPointerException | ConcurrentModificationException ignore) { } } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/async/downloader/GalleryDownloaderV2.java ================================================ package com.maxwai.nclientv3.async.downloader; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.net.Uri; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.maxwai.nclientv3.R; import com.maxwai.nclientv3.api.InspectorV3; import com.maxwai.nclientv3.api.components.Gallery; import com.maxwai.nclientv3.api.local.LocalGallery; import com.maxwai.nclientv3.async.database.Queries; import com.maxwai.nclientv3.settings.Global; import com.maxwai.nclientv3.utility.LogUtility; import com.maxwai.nclientv3.utility.Utility; import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.Objects; import java.util.concurrent.CopyOnWriteArraySet; import java.util.regex.Pattern; import okhttp3.Request; import okhttp3.Response; public class GalleryDownloaderV2 { public static final String DUPLICATE_EXTENSION = ".DUP"; public static final Pattern ID_FILE = Pattern.compile("^\\.\\d{1,6}$"); private final Context context; private final int id; private final CopyOnWriteArraySet observers = new CopyOnWriteArraySet<>(); private final List urls = new ArrayList<>(); private Status status = Status.NOT_STARTED; private String title; private Uri thumbnail; private int start = -1, end = -1; private Gallery gallery; private File folder; private boolean initialized = false; public GalleryDownloaderV2(Context context, @Nullable String title, @Nullable Uri thumbnail, int id) { this.context = context; this.id = id; this.thumbnail = thumbnail; this.title = Gallery.getPathTitle(title, context.getString(R.string.download_gallery)); } public GalleryDownloaderV2(Context context, Gallery gallery, int start, int end) { this(context, gallery.getTitle(), gallery.getCover(), gallery.getId()); this.start = start; this.end = end; setGallery(gallery); } private static File findFolder(File downloadfolder, String pathTitle, int id) { File folder = new File(downloadfolder, pathTitle); if (usableFolder(folder, id)) return folder; int i = 1; do { folder = new File(downloadfolder, pathTitle + DUPLICATE_EXTENSION + (i++)); } while (!usableFolder(folder, id)); return folder; } private static boolean usableFolder(File file, int id) { if (!file.exists()) return true;//folder not exists if (new File(file, "." + id).exists()) return true;//same id File[] files = file.listFiles((dir, name) -> ID_FILE.matcher(name).matches()); if (files != null && files.length > 0) return false;//has id but not equal LocalGallery localGallery = new LocalGallery(file);//read id from metadata return localGallery.getId() == id; } public boolean hasData() { return gallery != null; } public void removeObserver(DownloadObserver observer) { observers.remove(observer); } public File getFolder() { return folder; } public Gallery getGallery() { return gallery; } private void setGallery(Gallery gallery) { this.gallery = gallery; title = gallery.getPathTitle(); thumbnail = gallery.getThumbnail(); Queries.DownloadTable.addGallery(this); if (start == -1) start = 0; if (end == -1) end = gallery.getPageCount() - 1; } private int getTotalPage() { return Math.max(1, end - start + 1); } public int getPercentage() { if (gallery == null || urls.isEmpty()) return 0; return ((getTotalPage() - urls.size()) * 100) / getTotalPage(); } private void onStart() { setStatus(Status.DOWNLOADING); for (DownloadObserver observer : observers) observer.triggerStartDownload(this); } private void onEnd() { setStatus(Status.FINISHED); for (DownloadObserver observer : observers) observer.triggerEndDownload(this); LogUtility.d("Delete 75: " + id); Queries.DownloadTable.removeGallery(id); } private void onUpdate() { int total = getTotalPage(); int reach = total - urls.size(); for (DownloadObserver observer : observers) observer.triggerUpdateProgress(this, reach, total); } private void onCancel() { for (DownloadObserver observer : observers) observer.triggerCancelDownload(this); } private void onPause() { for (DownloadObserver observer : observers) observer.triggerPauseDownload(this); } public LocalGallery localGallery() { if (status != Status.FINISHED || folder == null) return null; return new LocalGallery(folder); } public String getTitle() { return title; } public void addObserver(DownloadObserver observer) { if (observer == null) return; observers.add(observer); } public int getId() { return id; } public int getStart() { return start; } public int getEnd() { return end; } @NonNull public String getPathTitle() { return title; } @NonNull public String getTruePathTitle() { return title; } /** * @return true if the download has been completed, false otherwise */ public boolean downloadGalleryData() { if (this.gallery != null) return true; InspectorV3 inspector = InspectorV3.galleryInspector(context, id, null); try { if (!inspector.createDocument()) return false; inspector.parseDocument(); if (inspector.getGalleries() == null || inspector.getGalleries().isEmpty()) return false; Gallery g = (Gallery) inspector.getGalleries().get(0); if (g.isValid()) setGallery(g); return g.isValid(); } catch (Exception e) { LogUtility.e("Error while downloading", e); return false; } } public Uri getThumbnail() { return thumbnail; } public boolean canBeFetched() { return status != Status.FINISHED && status != Status.PAUSED; } public Status getStatus() { return status; } public void setStatus(Status status) { if (this.status == status) return; this.status = status; if (status == Status.CANCELED) { LogUtility.d("Delete 95: " + id); onCancel(); Global.recursiveDelete(folder); Queries.DownloadTable.removeGallery(id); } } public void download() { initDownload(); onStart(); while (!urls.isEmpty()) { downloadPage(urls.get(0)); Utility.threadSleep(50); if (status == Status.PAUSED) { onPause(); return; } if (status == Status.CANCELED) { onCancel(); return; } } onEnd(); } private void downloadPage(PageContainer page) { if (savePage(page)) { urls.remove(page); onUpdate(); } } private boolean isCorrupted(File file) { String path = file.getAbsolutePath(); if (path.endsWith(".jpg") || path.endsWith(".jpeg")) { return Global.isJPEGCorrupted(path); } BitmapFactory.Options options = new BitmapFactory.Options(); options.inSampleSize = 256; Bitmap bitmap = BitmapFactory.decodeFile(path, options); boolean x = bitmap == null; if (!x) bitmap.recycle(); return x; } private boolean savePage(PageContainer page) { if (page == null) return true; File filePath = new File(folder, page.getPageName()); LogUtility.d("Saving into: " + filePath + "," + page.url); if (filePath.exists() && !isCorrupted(filePath)) return true; try (Response r = Global.getClient(context).newCall(new Request.Builder().url(page.url.toString()).build()).execute()) { if (r.code() != 200) { return false; } //noinspection DataFlowIssue long expectedSize = Integer.parseInt(r.header("Content-Length", "-1")); long len = r.body().contentLength(); if (len < 0 || expectedSize != len) { return false; } long written = Utility.writeStreamToFile(r.body().byteStream(), filePath); if (written != len) { //noinspection ResultOfMethodCallIgnored filePath.delete(); return false; } return true; } catch (IOException | NumberFormatException e) { LogUtility.e(e, e); } return false; } public void initDownload() { if (initialized) return; initialized = true; createFolder(); createPages(); checkPages(); } private void checkPages() { File filePath; for (int i = 0; i < urls.size(); i++) { if (urls.get(i) == null) { urls.remove(i--); continue; } filePath = new File(folder, urls.get(i).getPageName()); if (filePath.exists() && !isCorrupted(filePath)) urls.remove(i--); } } private void createPages() { for (int i = start; i <= end && i < gallery.getPageCount(); i++) urls.add(new PageContainer(i + 1, gallery.getHighPage(i))); } private void createFolder() { folder = findFolder(Global.DOWNLOADFOLDER, title, id); if (!folder.mkdirs()) { return; } try { writeNoMedia(); createIdFile(); } catch (IOException e) { LogUtility.e("Error creating base files", e); } } private void createIdFile() throws IOException { File idFile = new File(folder, "." + id); //noinspection ResultOfMethodCallIgnored idFile.createNewFile(); } private void writeNoMedia() throws IOException { File nomedia = new File(folder, ".nomedia"); LogUtility.d("NOMEDIA: " + nomedia + " for id " + id); try (FileWriter writer = new FileWriter(nomedia)) { gallery.jsonWrite(writer); } } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; GalleryDownloaderV2 that = (GalleryDownloaderV2) o; if (id != that.id) return false; return Objects.equals(folder, that.folder); } @Override public int hashCode() { int result = id; result = 31 * result + (folder != null ? folder.hashCode() : 0); return result; } public enum Status {NOT_STARTED, DOWNLOADING, PAUSED, FINISHED, CANCELED} public static class PageContainer { public final int page; public final Uri url; public PageContainer(int page, Uri url) { this.page = page; this.url = url; } public String getPageName() { String fileName = Objects.requireNonNull(url.getLastPathSegment()); return String.format(Locale.US, "%03d.%s", page, fileName.substring(fileName.indexOf('.') + 1)); } } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/async/downloader/PageChecker.java ================================================ package com.maxwai.nclientv3.async.downloader; public class PageChecker extends Thread { @Override public void run() { for (GalleryDownloaderV2 g : DownloadQueue.getDownloaders()) if (g.hasData()) g.initDownload(); } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/components/CookieInterceptor.java ================================================ package com.maxwai.nclientv3.components; import android.view.View; import android.webkit.CookieManager; import androidx.annotation.NonNull; import com.maxwai.nclientv3.components.activities.GeneralActivity; import com.maxwai.nclientv3.components.views.CFTokenView; import com.maxwai.nclientv3.utility.LogUtility; import com.maxwai.nclientv3.utility.Utility; import java.util.HashMap; public class CookieInterceptor { private static volatile boolean intercepting = false; private static volatile boolean webViewHidden = false; public static void hideWebView() { webViewHidden = true; CFTokenView tokenView = GeneralActivity.getLastCFView(); if (tokenView != null) { tokenView.post(() -> tokenView.setVisibility(View.GONE)); } } @NonNull private final Manager manager; String cookies = null; private CFTokenView web = null; public CookieInterceptor(@NonNull Manager manager) { this.manager = manager; } private CFTokenView setupWebView() { CFTokenView tokenView = GeneralActivity.getLastCFView(); if (tokenView == null) return null; tokenView.post(() -> { CFTokenView.CFTokenWebView webView = tokenView.getWebView(); webView.loadUrl(Utility.getBaseUrl()); }); return tokenView; } @NonNull private CFTokenView getWebView() { while (web == null) { Utility.threadSleep(100); web = setupWebView(); } return web; } private void interceptInternal() { CFTokenView web = getWebView(); if(!webViewHidden) web.post(() -> web.setVisibility(View.VISIBLE)); CookieManager manager = CookieManager.getInstance(); HashMap cookiesMap = new HashMap<>(); do { Utility.threadSleep(100); cookies = manager.getCookie(Utility.getBaseUrl()); if (cookies == null) return; String[] splitCookies = cookies.split("; "); for (String splitCookie : splitCookies) { String[] kv = splitCookie.split("=", 2); if (kv.length == 2) { if (!kv[1].equals(cookiesMap.put(kv[0], kv[1]))) { LogUtility.d("Processing cookie: " + kv[0] + "=" + kv[1]); CookieInterceptor.this.manager.applyCookie(kv[0], kv[1]); } } } } while (!this.manager.endInterceptor()); web.post(() -> web.setVisibility(View.GONE)); } public void intercept() { while(!manager.endInterceptor()){ while (intercepting) { Utility.threadSleep(100); } intercepting = true; synchronized (CookieInterceptor.class) { if (!manager.endInterceptor()) { interceptInternal(); } } intercepting = false; } this.manager.onFinish(); } public interface Manager { void applyCookie(String key, String value); boolean endInterceptor(); void onFinish(); } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/components/CustomCookieJar.java ================================================ package com.maxwai.nclientv3.components; import androidx.annotation.NonNull; import com.franmontiel.persistentcookiejar.ClearableCookieJar; import com.franmontiel.persistentcookiejar.cache.CookieCache; import com.franmontiel.persistentcookiejar.persistence.CookiePersistor; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.List; import okhttp3.Cookie; import okhttp3.HttpUrl; public class CustomCookieJar implements ClearableCookieJar { private final CookieCache cache; private final CookiePersistor persistor; public CustomCookieJar(CookieCache cache, CookiePersistor persistor) { this.cache = cache; this.persistor = persistor; this.cache.addAll(persistor.loadAll()); } private static List filterPersistentCookies(List cookies) { List persistentCookies = new ArrayList<>(); for (Cookie cookie : cookies) { if (cookie.persistent()) { persistentCookies.add(cookie); } } return persistentCookies; } private static boolean isCookieExpired(Cookie cookie) { return cookie.expiresAt() < System.currentTimeMillis(); } @Override synchronized public void saveFromResponse(@NonNull HttpUrl url, @NonNull List cookies) { cache.addAll(cookies); persistor.saveAll(filterPersistentCookies(cookies)); } @NonNull @Override synchronized public List loadForRequest(@NonNull HttpUrl url) { List cookiesToRemove = new ArrayList<>(); List validCookies = new ArrayList<>(); for (Iterator it = cache.iterator(); it.hasNext(); ) { Cookie currentCookie = it.next(); if (isCookieExpired(currentCookie)) { cookiesToRemove.add(currentCookie); it.remove(); } else { validCookies.add(currentCookie); } } persistor.removeAll(cookiesToRemove); return validCookies; } @Override synchronized public void clearSession() { cache.clear(); cache.addAll(persistor.loadAll()); } @Override synchronized public void clear() { cache.clear(); persistor.clear(); } public void removeCookie(String name) { List cookies = persistor.loadAll(); for (Cookie cookie : cookies) { if (cookie.name().equals(name)) { cache.clear(); persistor.removeAll(Collections.singletonList(cookie)); } } } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/components/GlideX.java ================================================ package com.maxwai.nclientv3.components; import android.content.Context; import android.view.View; import androidx.annotation.Nullable; import com.bumptech.glide.Glide; import com.bumptech.glide.RequestManager; public class GlideX { @Nullable public static RequestManager with(View view) { try { return Glide.with(view); } catch (VerifyError | IllegalStateException ignore) { return null; } } @Nullable public static RequestManager with(Context context) { try { return Glide.with(context); } catch (VerifyError | IllegalStateException ignore) { return null; } } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/components/ThreadAsyncTask.java ================================================ package com.maxwai.nclientv3.components; import androidx.appcompat.app.AppCompatActivity; import com.maxwai.nclientv3.settings.Global; public abstract class ThreadAsyncTask { private final AppCompatActivity activity; /** @noinspection FieldCanBeLocal*/ private Thread thread; public ThreadAsyncTask(AppCompatActivity activity) { this.activity = activity; } public final void execute(Param params) { thread = new AsyncThread(params); thread.start(); } protected void onPreExecute() { } protected void onPostExecute(Result result) { } protected void onProgressUpdate(Progress value) { } protected abstract Result doInBackground(Param param); protected final void publishProgress(Progress value) { if (!Global.isDestroyed(activity)) activity.runOnUiThread(() -> onProgressUpdate(value)); } class AsyncThread extends Thread { final Param param; AsyncThread(Param param) { this.param = param; } @Override public void run() { if (!Global.isDestroyed(activity)) activity.runOnUiThread(ThreadAsyncTask.this::onPreExecute); Result result = doInBackground(param); if (!Global.isDestroyed(activity)) activity.runOnUiThread(() -> onPostExecute(result)); } } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/components/activities/BaseActivity.java ================================================ package com.maxwai.nclientv3.components.activities; import android.content.res.Configuration; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import com.maxwai.nclientv3.components.widgets.CustomGridLayoutManager; public abstract class BaseActivity extends GeneralActivity { protected RecyclerView recycler; protected SwipeRefreshLayout refresher; protected ViewGroup masterLayout; protected abstract int getPortraitColumnCount(); protected abstract int getLandscapeColumnCount(); public SwipeRefreshLayout getRefresher() { return refresher; } public RecyclerView getRecycler() { return recycler; } public ViewGroup getMasterLayout() { return masterLayout; } @Override public void onConfigurationChanged(@NonNull Configuration newConfig) { super.onConfigurationChanged(newConfig); if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) { changeLayout(true); } else if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) { changeLayout(false); } } protected void changeLayout(boolean landscape) { CustomGridLayoutManager manager = (CustomGridLayoutManager) recycler.getLayoutManager(); RecyclerView.Adapter adapter = recycler.getAdapter(); int count = landscape ? getLandscapeColumnCount() : getPortraitColumnCount(); int position = 0; if (manager != null) position = manager.findFirstCompletelyVisibleItemPosition(); CustomGridLayoutManager gridLayoutManager = new CustomGridLayoutManager(this, count); recycler.setLayoutManager(gridLayoutManager); recycler.setAdapter(adapter); recycler.scrollToPosition(position); } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/components/activities/CrashApplication.java ================================================ package com.maxwai.nclientv3.components.activities; import android.app.Activity; import android.app.Application; import android.content.Context; import android.content.SharedPreferences; import android.os.Build; import android.os.Bundle; import android.view.View; import androidx.annotation.DeprecatedSinceApi; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatDelegate; import androidx.core.content.ContextCompat; import androidx.core.graphics.Insets; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; import com.maxwai.nclientv3.R; import com.maxwai.nclientv3.async.database.DatabaseHelper; import com.maxwai.nclientv3.async.downloader.DownloadGalleryV2; import com.maxwai.nclientv3.settings.Database; import com.maxwai.nclientv3.settings.Global; import com.maxwai.nclientv3.settings.TagV2; import com.maxwai.nclientv3.utility.network.NetworkUtil; public class CrashApplication extends Application { /** * Don't use on API >= 31 (S) */ @DeprecatedSinceApi(api = Build.VERSION_CODES.S) public static void setDarkLightTheme(String theme, Context ctx) { String[] availableThemes = ctx.getResources().getStringArray(R.array.theme_data); ContextCompat.getMainExecutor(ctx).execute(() -> AppCompatDelegate.setDefaultNightMode(theme.equals(availableThemes[0]) ? // light AppCompatDelegate.MODE_NIGHT_NO : AppCompatDelegate.MODE_NIGHT_YES)); } @Override public void onCreate() { super.onCreate(); AppCompatDelegate.setCompatVectorFromResourcesEnabled(true); Global.initStorage(this); //noinspection resource Database.setDatabase(new DatabaseHelper(getApplicationContext()).getWritableDatabase()); String version = Global.getLastVersion(this); String actualVersion = Global.getVersionName(this); SharedPreferences preferences = getSharedPreferences("Settings", 0); if (!actualVersion.equals(version)) afterUpdateChecks(preferences, version); Global.initFromShared(this); NetworkUtil.initConnectivity(this); TagV2.initMinCount(this); TagV2.initSortByName(this); DownloadGalleryV2.loadDownloads(this); registerActivityLifecycleCallbacks(new CustomActivityLifecycleCallback()); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { String theme = preferences.getString(getString(R.string.preference_key_theme_select), ""); setDarkLightTheme(theme, this); } } private void afterUpdateChecks(SharedPreferences preferences, String oldVersion) { SharedPreferences.Editor editor = preferences.edit(); removeOldUpdates(); if ("0.0.0".equals(oldVersion)) editor.putBoolean(getString(R.string.preference_key_check_update), true); editor.apply(); Global.setLastVersion(this); } private void removeOldUpdates() { if (!Global.hasStoragePermission(this)) return; Global.recursiveDelete(Global.UPDATEFOLDER); //noinspection ResultOfMethodCallIgnored Global.UPDATEFOLDER.mkdir(); } private static class CustomActivityLifecycleCallback implements ActivityLifecycleCallbacks { @Override public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState) { View rootView = activity.getWindow().getDecorView().getRootView(); ViewCompat.setOnApplyWindowInsetsListener(rootView, (v, insets) -> { Insets barsInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars()); v.setPadding( barsInsets.left, barsInsets.top, barsInsets.right, barsInsets.bottom ); return WindowInsetsCompat.CONSUMED; }); } @Override public void onActivityDestroyed(@NonNull Activity activity) { } @Override public void onActivityPaused(@NonNull Activity activity) { } @Override public void onActivityResumed(@NonNull Activity activity) { } @Override public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState) { } @Override public void onActivityStarted(@NonNull Activity activity) { } @Override public void onActivityStopped(@NonNull Activity activity) { } } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/components/activities/GeneralActivity.java ================================================ package com.maxwai.nclientv3.components.activities; import android.content.SharedPreferences; import android.content.res.Resources; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.WindowManager; import android.widget.Toast; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; import com.maxwai.nclientv3.R; import com.maxwai.nclientv3.components.views.CFTokenView; import com.maxwai.nclientv3.settings.Global; import java.lang.ref.WeakReference; public abstract class GeneralActivity extends AppCompatActivity { private static WeakReference lastActivity; private boolean isFastScrollerApplied = false; private CFTokenView tokenView = null; public static @Nullable CFTokenView getLastCFView() { if (lastActivity == null) return null; GeneralActivity activity = lastActivity.get(); if (activity != null) { activity.runOnUiThread(activity::inflateWebView); return activity.tokenView; } return null; } private void inflateWebView() { if (tokenView == null) { Toast.makeText(this, R.string.fetching_cloudflare_token, Toast.LENGTH_SHORT).show(); ViewGroup rootView = (ViewGroup) findViewById(android.R.id.content).getRootView(); ViewGroup v = (ViewGroup) LayoutInflater.from(this).inflate(R.layout.cftoken_layout, rootView, false); tokenView = new CFTokenView(v); ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); tokenView.setVisibility(View.GONE); this.addContentView(v, params); } } @Override protected void onPause() { super.onPause(); } @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (Global.hideMultitask()) getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE); Global.initActivity(this); } @Override protected void onResume() { super.onResume(); lastActivity = new WeakReference<>(this); if (!isFastScrollerApplied) { isFastScrollerApplied = true; Global.applyFastScroller(findViewById(R.id.recycler)); } } @Override public Resources.Theme getTheme() { Resources.Theme theme = super.getTheme(); SharedPreferences preferences = getSharedPreferences("Settings", 0); if (preferences.getBoolean(getString(R.string.preference_key_black_theme), false)) { theme.applyStyle(R.style.AppTheme_Black, true); } return theme; } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/components/classes/Bookmark.java ================================================ package com.maxwai.nclientv3.components.classes; import android.content.Context; import android.net.Uri; import androidx.annotation.NonNull; import com.maxwai.nclientv3.api.InspectorV3; import com.maxwai.nclientv3.api.components.Tag; import com.maxwai.nclientv3.api.enums.ApiRequestType; import com.maxwai.nclientv3.api.enums.SortType; import com.maxwai.nclientv3.api.enums.SpecialTagIds; import com.maxwai.nclientv3.api.enums.TagStatus; import com.maxwai.nclientv3.api.enums.TagType; import com.maxwai.nclientv3.async.database.Queries; import java.util.Collections; public class Bookmark { public final String url; public final int page, tag; private final ApiRequestType requestType; private final Tag tagVal; private final Uri uri; public Bookmark(String url, int page, ApiRequestType requestType, int tag) { Tag tagVal1; this.url = url; this.page = page; this.requestType = requestType; this.tag = tag; tagVal1 = Queries.TagTable.getTagById(this.tag); if (tagVal1 == null) tagVal1 = new Tag("english", 0, SpecialTagIds.LANGUAGE_ENGLISH, TagType.LANGUAGE, TagStatus.DEFAULT); this.tagVal = tagVal1; this.uri = Uri.parse(url); } public InspectorV3 createInspector(Context context, InspectorV3.InspectorResponse response) { String query = uri.getQueryParameter("q"); SortType popular = SortType.findFromAddition(uri.getQueryParameter("sort")); if (requestType == ApiRequestType.FAVORITE) return InspectorV3.favoriteInspector(context, query, page, response); if (requestType == ApiRequestType.BYSEARCH) return InspectorV3.searchInspector(context, query, null, page, popular, null, response); if (requestType == ApiRequestType.BYALL) return InspectorV3.searchInspector(context, "", null, page, SortType.RECENT_ALL_TIME, null, response); if (requestType == ApiRequestType.BYTAG) return InspectorV3.searchInspector(context, "", Collections.singleton(tagVal), page, SortType.findFromAddition(this.url), null, response); return null; } public void deleteBookmark() { Queries.BookmarkTable.deleteBookmark(url); } @NonNull @Override public String toString() { if (requestType == ApiRequestType.BYTAG) return tagVal.getType().getSingle() + ": " + tagVal.getName(); if (requestType == ApiRequestType.FAVORITE) return "Favorite"; if (requestType == ApiRequestType.BYSEARCH) //noinspection ConcatenationWithEmptyString return "" + uri.getQueryParameter("q"); if (requestType == ApiRequestType.BYALL) return "Main page"; return "WTF"; } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/components/classes/History.java ================================================ package com.maxwai.nclientv3.components.classes; import java.util.ArrayList; import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Set; public class History { private final String value; private final Date date; public History(String value, boolean set) { if (set) { int p = value.indexOf('|'); date = new Date(Long.parseLong(value.substring(0, p))); this.value = value.substring(p + 1); } else { this.value = value; this.date = new Date(); } } public static List setToList(Set set) { List h = new ArrayList<>(set.size()); for (String s : set) h.add(new History(s, true)); h.sort((o2, o1) -> { int o = o1.date.compareTo(o2.date); if (o == 0) o = o1.value.compareTo(o2.value); return o; }); return h; } public static Set listToSet(List list) { HashSet s = new HashSet<>(list.size()); for (History h : list) s.add(h.date.getTime() + "|" + h.value); return s; } public String getValue() { return value; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; History history = (History) o; return value.equals(history.value); } @Override public int hashCode() { return value.hashCode(); } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/components/classes/MultichoiceAdapter.java ================================================ package com.maxwai.nclientv3.components.classes; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.constraintlayout.widget.ConstraintLayout; import androidx.recyclerview.widget.RecyclerView; import com.maxwai.nclientv3.R; import com.maxwai.nclientv3.utility.LogUtility; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; public abstract class MultichoiceAdapter extends RecyclerView.Adapter> { private final List listeners = new ArrayList<>(3); private Mode mode = Mode.NORMAL; private final HashMap map = new HashMap<>() { @Nullable @Override public D put(Long key, D value) { D res = super.put(key, value); if (size() == 1) startSelecting(); changeSelecting(); return res; } @Nullable @Override public D remove(@Nullable Object key) { D res = super.remove(key); if (isEmpty()) endSelecting(); changeSelecting(); return res; } @Override public void clear() { super.clear(); endSelecting(); changeSelecting(); } }; public MultichoiceAdapter() { setHasStableIds(true); } private void changeSelecting() { for (MultichoiceListener listener : listeners) listener.choiceChanged(); } /** * Used only to do a put */ protected abstract D getItemAt(int position); protected abstract ViewGroup getMaster(T holder); protected abstract void defaultMasterAction(int position); protected abstract void onBindMultichoiceViewHolder(T holder, int position); @NonNull protected abstract T onCreateMultichoiceViewHolder(@NonNull ViewGroup parent, int viewType); @Override public abstract long getItemId(int position); private void startSelecting() { setMode(Mode.SELECTING); for (MultichoiceListener listener : listeners) listener.firstChoice(); } private void endSelecting() { setMode(Mode.NORMAL); for (MultichoiceListener listener : listeners) listener.noMoreChoices(); } public void addListener(MultichoiceListener listener) { this.listeners.add(listener); } @NonNull @Override public final MultichoiceViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { T innerLayout = onCreateMultichoiceViewHolder(parent, viewType); ViewGroup master = getMaster(innerLayout); ConstraintLayout multiLayout = (ConstraintLayout) LayoutInflater.from(parent.getContext()).inflate(R.layout.multichoice_adapter, master, true); return new MultichoiceViewHolder<>(multiLayout, innerLayout); } @Override public final void onBindViewHolder(@NonNull MultichoiceViewHolder holder, final int position) { boolean isSelected = map.containsKey(getItemId(holder.getBindingAdapterPosition())); View master = getMaster(holder.innerHolder); updateLayoutParams(master, holder.censor, isSelected); if (master != null) { master.setOnClickListener(v -> { switch (mode) { case SELECTING: toggleSelection(holder.getBindingAdapterPosition()); break; case NORMAL: defaultMasterAction(holder.getBindingAdapterPosition()); break; } }); master.setOnLongClickListener(v -> { map.put(getItemId(holder.getBindingAdapterPosition()), getItemAt(holder.getBindingAdapterPosition())); notifyItemChanged(holder.getBindingAdapterPosition()); return true; }); } holder.censor.setVisibility(isSelected ? View.VISIBLE : View.GONE); holder.checkmark.setVisibility(isSelected ? View.VISIBLE : View.GONE); holder.censor.setOnClickListener(v -> toggleSelection(holder.getBindingAdapterPosition())); onBindMultichoiceViewHolder(holder.innerHolder, holder.getBindingAdapterPosition()); } private void updateLayoutParams(View master, View multichoiceHolder, boolean isSelected) { if (master == null) return; int margin = isSelected ? 8 : 0; ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) master.getLayoutParams(); params.setMargins(margin, margin, margin, margin); master.setLayoutParams(params); if (isSelected && multichoiceHolder != null) { master.post(() -> { ViewGroup.LayoutParams multiParam = multichoiceHolder.getLayoutParams(); multiParam.width = master.getWidth(); multiParam.height = master.getHeight(); LogUtility.d("Multiparam: " + multiParam.width + ", " + multiParam.height); multichoiceHolder.setLayoutParams(multiParam); }); } } private void toggleSelection(int position) { long id = getItemId(position); if (map.containsKey(id)) map.remove(id); else map.put(id, getItemAt(position)); notifyItemChanged(position); } public Mode getMode() { return mode; } private void setMode(Mode mode) { this.mode = mode; } public void selectAll() { final int count = getItemCount(); for (int i = 0; i < count; i++) map.put(getItemId(i), getItemAt(i)); notifyItemRangeChanged(0, count); } public Collection getSelected() { return map.values(); } public void deselectAll() { map.clear(); notifyItemRangeChanged(0, getItemCount()); } public enum Mode {NORMAL, SELECTING} public interface MultichoiceListener { void firstChoice(); void noMoreChoices(); void choiceChanged(); } public static class DefaultMultichoiceListener implements MultichoiceListener { @Override public void firstChoice() { } @Override public void noMoreChoices() { } @Override public void choiceChanged() { } } public static class MultichoiceViewHolder extends RecyclerView.ViewHolder { final T innerHolder; final View censor; final ImageView checkmark; public MultichoiceViewHolder(@NonNull ConstraintLayout multichoiceHolder, T holder) { super(holder.itemView); this.innerHolder = holder; this.censor = multichoiceHolder.findViewById(R.id.censor); this.checkmark = multichoiceHolder.findViewById(R.id.checkmark); } } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/components/classes/Size.java ================================================ package com.maxwai.nclientv3.components.classes; import android.os.Parcel; import android.os.Parcelable; import androidx.annotation.NonNull; public class Size implements Parcelable { public static final Creator CREATOR = new Creator<>() { @Override public Size createFromParcel(Parcel in) { return new Size(in); } @Override public Size[] newArray(int size) { return new Size[size]; } }; private int width, height; public Size(int width, int height) { this.width = width; this.height = height; } protected Size(Parcel in) { width = in.readInt(); height = in.readInt(); } public int getWidth() { return width; } public void setWidth(int width) { this.width = width; } public int getHeight() { return height; } public void setHeight(int height) { this.height = height; } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeInt(width); dest.writeInt(height); } @NonNull @Override public String toString() { return "Size{" + "width=" + width + ", height=" + height + '}'; } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/components/launcher/LauncherCalculator.java ================================================ package com.maxwai.nclientv3.components.launcher; public class LauncherCalculator { } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/components/launcher/LauncherReal.java ================================================ package com.maxwai.nclientv3.components.launcher; public class LauncherReal { } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/components/status/Status.java ================================================ package com.maxwai.nclientv3.components.status; import android.graphics.Color; public class Status { public final int color; public final String name; Status(int color, String name) { this.color = Color.argb(0x7f, Color.red(color), Color.green(color), Color.blue(color)); this.name = name; } public int opaqueColor() { return color | 0xff000000; } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/components/status/StatusManager.java ================================================ package com.maxwai.nclientv3.components.status; import androidx.annotation.Nullable; import com.maxwai.nclientv3.async.database.Queries; import java.util.ArrayList; import java.util.HashMap; import java.util.List; public class StatusManager { public static final String DEFAULT_STATUS = "None"; private static final HashMap statusMap = new HashMap<>(); public static Status getByName(String name) { return statusMap.get(name); } public static Status add(String name, int color) { return add(new Status(color, name)); } static Status add(Status status) { Queries.StatusTable.insert(status); statusMap.put(status.name, status); return status; } public static void remove(Status status) { Queries.StatusTable.remove(status.name); statusMap.remove(status.name); } public static List getNames() { List st = new ArrayList<>(statusMap.keySet()); st.sort(String::compareToIgnoreCase); st.remove(DEFAULT_STATUS); //st.add(0,DEFAULT_STATUS); return st; } public static List toList() { ArrayList statuses = new ArrayList<>(statusMap.values()); statuses.sort((o1, o2) -> o1.name.compareToIgnoreCase(o2.name)); statuses.remove(getByName(DEFAULT_STATUS)); return statuses; } public static Status updateStatus(@Nullable Status oldStatus, String newName, int newColor) { if (oldStatus == null) return add(newName, newColor); Status newStatus = new Status(newColor, newName); Queries.StatusTable.update(oldStatus, newStatus); statusMap.remove(oldStatus.name); statusMap.put(newStatus.name, newStatus); return newStatus; } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/components/views/CFTokenView.java ================================================ package com.maxwai.nclientv3.components.views; import android.annotation.SuppressLint; import android.content.Context; import android.util.AttributeSet; import android.view.ViewGroup; import android.webkit.CookieManager; import android.webkit.WebChromeClient; import android.webkit.WebSettings; import android.webkit.WebView; import android.webkit.WebViewClient; import android.widget.Button; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.maxwai.nclientv3.R; import com.maxwai.nclientv3.components.CookieInterceptor; public class CFTokenView { private final ViewGroup masterLayout; private final CFTokenWebView webView; public CFTokenView(ViewGroup masterLayout) { this.masterLayout = masterLayout; webView=masterLayout.findViewById(R.id.webView); Button button = masterLayout.findViewById(R.id.hideWebView); button.setOnClickListener(v -> CookieInterceptor.hideWebView()); } public CFTokenWebView getWebView() { return webView; } public void setVisibility(int visible) { masterLayout.setVisibility(visible); } public void post(Runnable o) { masterLayout.post(o); } public static class CFTokenWebView extends WebView{ public CFTokenWebView(@NonNull Context context) { super(context); init(); } public CFTokenWebView(@NonNull Context context, @Nullable @org.jetbrains.annotations.Nullable AttributeSet attrs) { super(context, attrs); init(); } public CFTokenWebView(@NonNull Context context, @Nullable @org.jetbrains.annotations.Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } private void init() { forceAcceptCookies(); applyWebViewSettings(); } private void forceAcceptCookies() { CookieManager.getInstance().setAcceptCookie(true); CookieManager.getInstance().setAcceptThirdPartyCookies(this, true); } @SuppressLint("SetJavaScriptEnabled") private void applyWebViewSettings() { setWebChromeClient(new WebChromeClient()); setWebViewClient(new WebViewClient()); WebSettings webSettings = getSettings(); webSettings.setJavaScriptEnabled(true); webSettings.setDomStorageEnabled(true); webSettings.setUseWideViewPort(true); webSettings.setLoadWithOverviewMode(true); webSettings.setCacheMode(WebSettings.LOAD_DEFAULT); webSettings.setSupportZoom(true); webSettings.setBuiltInZoomControls(true); webSettings.setDisplayZoomControls(false); webSettings.setAllowContentAccess(true); } } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/components/views/GeneralPreferenceFragment.java ================================================ package com.maxwai.nclientv3.components.views; import static androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_WEAK; import static androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL; import android.annotation.SuppressLint; import android.app.UiModeManager; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.provider.Settings; import android.util.JsonWriter; import android.view.View; import android.webkit.CookieManager; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatDelegate; import androidx.appcompat.widget.AppCompatAutoCompleteTextView; import androidx.biometric.BiometricManager; import androidx.core.content.ContextCompat; import androidx.core.os.LocaleListCompat; import androidx.preference.ListPreference; import androidx.preference.Preference; import androidx.preference.PreferenceFragmentCompat; import androidx.preference.SeekBarPreference; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.maxwai.nclientv3.CopyToClipboardActivity; import com.maxwai.nclientv3.ApiKeyActivity; import com.maxwai.nclientv3.R; import com.maxwai.nclientv3.SettingsActivity; import com.maxwai.nclientv3.StatusManagerActivity; import com.maxwai.nclientv3.async.MetadataFetcher; import com.maxwai.nclientv3.async.VersionChecker; import com.maxwai.nclientv3.components.activities.CrashApplication; import com.maxwai.nclientv3.components.launcher.LauncherCalculator; import com.maxwai.nclientv3.components.launcher.LauncherReal; import com.maxwai.nclientv3.settings.AuthStore; import com.maxwai.nclientv3.settings.Global; import com.maxwai.nclientv3.settings.Login; import com.maxwai.nclientv3.utility.LogUtility; import com.maxwai.nclientv3.utility.Utility; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import java.io.File; import java.io.IOException; import java.io.StringWriter; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Objects; public class GeneralPreferenceFragment extends PreferenceFragmentCompat { private SettingsActivity act; public void setAct(SettingsActivity act) { this.act = act; } public void setType(SettingsActivity.Type type) { switch (type) { case MAIN: mainMenu(); break; case COLUMN: columnMenu(); break; case DATA: dataMenu(); break; } } private void dataMenu() { addPreferencesFromResource(R.xml.settings_data); SeekBarPreference mobile = Objects.requireNonNull(findPreference(getString(R.string.key_mobile_usage))); SeekBarPreference wifi = Objects.requireNonNull(findPreference(getString(R.string.key_wifi_usage))); mobile.setOnPreferenceChangeListener((preference, newValue) -> { mobile.setTitle(getDataUsageString((Integer) newValue)); return true; }); wifi.setOnPreferenceChangeListener((preference, newValue) -> { wifi.setTitle(getDataUsageString((Integer) newValue)); return true; }); mobile.setTitle(getDataUsageString(mobile.getValue())); wifi.setTitle(getDataUsageString(wifi.getValue())); mobile.setUpdatesContinuously(true); wifi.setUpdatesContinuously(true); } private int getDataUsageString(int val) { switch (val) { case 0: return R.string.data_usage_no; case 1: return R.string.data_usage_thumb; case 2: return R.string.data_usage_full; } return R.string.data_usage_full; } private LocaleListCompat getLocaleListFromXml() { List tagsList = new ArrayList<>(); try { @SuppressLint("DiscouragedApi") int id = getResources().getIdentifier( "_generated_res_locale_config", "xml", requireContext().getPackageName() ); XmlPullParser xpp = getResources().getXml(id); while (xpp.getEventType() != XmlPullParser.END_DOCUMENT) { if (xpp.getEventType() == XmlPullParser.START_TAG) { if (Objects.equals(xpp.getName(), "locale")) { tagsList.add(xpp.getAttributeValue(0)); } } xpp.next(); } } catch (XmlPullParserException | IOException e) { LogUtility.w("Problem parsing locales xml", e); } return LocaleListCompat.forLanguageTags(tagsList.stream() .reduce((a, b) -> a + "," + b) .orElse("") .toString()); } private void fillRoba() { LocaleListCompat setLocaleList = AppCompatDelegate.getApplicationLocales(); Locale actualLocale = setLocaleList.isEmpty() ? Locale.ENGLISH : Objects.requireNonNull(setLocaleList.get(0)); ListPreference preference = Objects.requireNonNull(findPreference(getString(R.string.preference_key_language))); LocaleListCompat localeList = getLocaleListFromXml(); String[] languagesEntry = new String[localeList.size() + 1]; String[] languagesNames = new String[localeList.size() + 1]; // System language languagesEntry[0] = getString(R.string.key_default_value); languagesNames[0] = Character.toUpperCase(getString(R.string.system_default).charAt(0)) + getString(R.string.system_default).substring(1); // Other languages for (int i = 0; i < localeList.size(); i++) { Locale locale = Objects.requireNonNull(localeList.get(i)); languagesEntry[i + 1] = locale.toLanguageTag(); languagesNames[i + 1] = Character.toUpperCase(locale.getDisplayName(actualLocale).charAt(0)) + locale.getDisplayName(actualLocale).substring(1); } preference.setEntryValues(languagesEntry); preference.setEntries(languagesNames); } @SuppressLint("ApplySharedPref") private void mainMenu() { addPreferencesFromResource(R.xml.settings); fillRoba(); { Preference apiKey = Objects.requireNonNull(findPreference(getString(R.string.preference_key_api_key))); updateApiKeySummary(apiKey); apiKey.setOnPreferenceClickListener(preference -> { Intent i = new Intent(act, ApiKeyActivity.class); act.runOnUiThread(() -> act.startActivity(i)); return true; }); } { Preference statusScreen = Objects.requireNonNull(findPreference(getString(R.string.preference_key_status_screen))); statusScreen.setOnPreferenceClickListener(preference -> { Intent i = new Intent(act, StatusManagerActivity.class); act.runOnUiThread(() -> act.startActivity(i)); return false; }); } { Preference colScreen = Objects.requireNonNull(findPreference(getString(R.string.preference_key_col_screen))); colScreen.setOnPreferenceClickListener(preference -> { Intent i = new Intent(act, SettingsActivity.class); i.putExtra(act.getPackageName() + ".TYPE", SettingsActivity.Type.COLUMN.ordinal()); act.runOnUiThread(() -> act.startActivity(i)); return false; }); } { Preference dataScreen = Objects.requireNonNull(findPreference(getString(R.string.preference_key_data_screen))); dataScreen.setOnPreferenceClickListener(preference -> { Intent i = new Intent(act, SettingsActivity.class); i.putExtra(act.getPackageName() + ".TYPE", SettingsActivity.Type.DATA.ordinal()); act.runOnUiThread(() -> act.startActivity(i)); return false; }); } { Preference fetchMetadata = Objects.requireNonNull(findPreference(getString(R.string.preference_key_fetch_metadata))); fetchMetadata.setVisible(Global.hasStoragePermission(act)); fetchMetadata.setOnPreferenceClickListener(preference -> { new Thread(new MetadataFetcher(act)).start(); return true; }); } { Preference fakeIcon = Objects.requireNonNull(findPreference(getString(R.string.preference_key_fake_icon))); fakeIcon.setOnPreferenceChangeListener((preference, newValue) -> { PackageManager pm = act.getPackageManager(); ComponentName name1 = new ComponentName(act, LauncherReal.class); ComponentName name2 = new ComponentName(act, LauncherCalculator.class); if ((boolean) newValue) { changeLauncher(pm, name1, false); changeLauncher(pm, name2, true); } else { changeLauncher(pm, name1, true); changeLauncher(pm, name2, false); } return true; }); } { Preference useAccountTag = Objects.requireNonNull(findPreference(getString(R.string.preference_key_use_account_tag))); useAccountTag.setEnabled(Login.isLogged()); } { Preference themeSelect = Objects.requireNonNull(findPreference(getString(R.string.preference_key_theme_select))); themeSelect.setOnPreferenceChangeListener((preference, newValue) -> { String newTheme = (String) newValue; String[] availableThemes = getResources().getStringArray(R.array.theme_data); assert availableThemes.length == 3; if (Arrays.stream(availableThemes).noneMatch(newTheme::equals)) { return false; } act.getSharedPreferences("Settings", 0) .edit() .putBoolean(getString(R.string.preference_key_black_theme), newTheme.equals(availableThemes[2])) // black .apply(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { int theme; if (newTheme.equals(availableThemes[0])) { // light theme = UiModeManager.MODE_NIGHT_NO; } else if (newTheme.equals(availableThemes[1]) || newTheme.equals(availableThemes[2])) { // dark / black theme = UiModeManager.MODE_NIGHT_YES; } else { return false; } UiModeManager uim = (UiModeManager) act.getSystemService(Context.UI_MODE_SERVICE); uim.setApplicationNightMode(theme); } else { CrashApplication.setDarkLightTheme(newTheme, act); } return true; }); } { Preference keyLanguage = Objects.requireNonNull(findPreference(getString(R.string.preference_key_language))); keyLanguage.setOnPreferenceChangeListener((preference, newValue) -> { LocaleListCompat newLocale; if (newValue.equals(getString(R.string.key_default_value))) { newLocale = LocaleListCompat.getEmptyLocaleList(); } else { newLocale = LocaleListCompat.forLanguageTags((String) newValue); } ContextCompat.getMainExecutor(act).execute(() -> AppCompatDelegate.setApplicationLocales(newLocale)); return true; }); } { Preference enableBeta = Objects.requireNonNull(findPreference(getString(R.string.preference_key_enable_beta))); enableBeta.setOnPreferenceChangeListener((preference, newValue) -> { //Instant update to allow search for updates Global.setEnableBeta((Boolean) newValue); return true; }); } { Preference hasCredentials = Objects.requireNonNull(findPreference(getString(R.string.preference_key_has_credentials))); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { hasCredentials.setOnPreferenceChangeListener((preference, newValue) -> { if (newValue.equals(Boolean.TRUE)) { BiometricManager biometricManager = BiometricManager.from(act); switch (biometricManager.canAuthenticate(BIOMETRIC_WEAK | DEVICE_CREDENTIAL)) { case BiometricManager.BIOMETRIC_SUCCESS: break; case BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED: // Prompts the user to create credentials that your app accepts. final Intent enrollIntent = new Intent(Settings.ACTION_BIOMETRIC_ENROLL); enrollIntent.putExtra(Settings.EXTRA_BIOMETRIC_AUTHENTICATORS_ALLOWED, BIOMETRIC_WEAK | DEVICE_CREDENTIAL); startActivity(enrollIntent); case BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE: case BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE: case BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED: case BiometricManager.BIOMETRIC_ERROR_UNSUPPORTED: case BiometricManager.BIOMETRIC_STATUS_UNKNOWN: return false; } } return true; }); } else { hasCredentials.setEnabled(false); hasCredentials.setSummary(R.string.setting_device_credentials_low_sdk); } } { Preference keyVersion = Objects.requireNonNull(findPreference(getString(R.string.preference_key_version))); keyVersion.setTitle(getString(R.string.app_version_format, Global.getVersionName(act))); } { ListPreference savePath = Objects.requireNonNull(findPreference(getString(R.string.preference_key_save_path))); initStoragePaths(savePath); savePath.setOnPreferenceChangeListener((preference, newValue) -> { if (!newValue.equals(getString(R.string.custom_path))) return true; manageCustomPath(); return false; }); } { //clear cache if pressed double cacheSize = Global.recursiveSize(act.getCacheDir()) / ((double) (1 << 20)); Preference cache = Objects.requireNonNull(findPreference(getString(R.string.preference_key_cache))); cache.setSummary(getString(R.string.cache_size_formatted, cacheSize)); cache.setOnPreferenceClickListener(preference -> { MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(act); builder.setTitle(R.string.clear_cache); builder.setPositiveButton(R.string.yes, (dialog, which) -> { Global.recursiveDelete(act.getCacheDir()); act.runOnUiThread(() -> { Toast.makeText(act, act.getString(R.string.cache_cleared), Toast.LENGTH_SHORT).show(); double cSize = Global.recursiveSize(act.getCacheDir()) / ((double) (2 << 20)); cache.setSummary(getString(R.string.cache_size_formatted, cSize)); }); }).setNegativeButton(R.string.no, null).setCancelable(true); builder.show(); return true; }); } { Preference cookie = Objects.requireNonNull(findPreference(getString(R.string.preference_key_cookie))); cookie.setOnPreferenceClickListener(preference -> { CookieManager.getInstance().removeAllCookies(null); return true; }); } { Preference update = Objects.requireNonNull(findPreference(getString(R.string.preference_key_update))); update.setOnPreferenceClickListener(preference -> { new VersionChecker(act, false); return true; }); } { Preference bug = Objects.requireNonNull(findPreference(getString(R.string.preference_key_bug))); bug.setOnPreferenceClickListener(preference -> { Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse("https://github.com/maxwai/NClientV3/issues/new")); startActivity(i); return true; }); } { Preference bug = Objects.requireNonNull(findPreference(getString(R.string.preference_key_copy_logs))); bug.setOnPreferenceClickListener(preference -> { act.exportLogs(); return true; }); } { Preference copySettings = Objects.requireNonNull(findPreference(getString(R.string.preference_key_copy_settings))); copySettings.setOnPreferenceClickListener(preference -> { try { CopyToClipboardActivity.copyTextToClipboard(act, getDataSettings(act)); } catch (IOException e) { LogUtility.e("Error copying settings into clipboard", e); Toast.makeText(act, R.string.clipboard_settings_error, Toast.LENGTH_SHORT).show(); } return true; }); } { Preference export = Objects.requireNonNull(findPreference(getString(R.string.preference_key_export))); export.setOnPreferenceClickListener(preference -> { act.exportSettings(); return true; }); } { Preference _import = Objects.requireNonNull(findPreference(getString(R.string.preference_key_import))); _import.setOnPreferenceClickListener(preference -> { act.importSettings(); return true; }); } { ListPreference mirror = Objects.requireNonNull(findPreference(getString(R.string.preference_key_site_mirror))); mirror.setSummary( act.getSharedPreferences("Settings", Context.MODE_PRIVATE) .getString(getString(R.string.preference_key_site_mirror), Utility.ORIGINAL_URL) ); mirror.setOnPreferenceChangeListener((preference, newValue) -> { preference.setSummary(newValue.toString()); return true; }); } } @Override public void onResume() { super.onResume(); if (act == null) return; Preference apiKey = findPreference(getString(R.string.preference_key_api_key)); if (apiKey != null) updateApiKeySummary(apiKey); } private void updateApiKeySummary(@NonNull Preference preference) { if (act == null) return; if (AuthStore.hasValidApiKey(act)) { preference.setSummary(R.string.setting_api_key_summary_valid); } else if (AuthStore.hasApiKey(act)) { preference.setSummary(R.string.setting_api_key_summary_invalid); } else { preference.setSummary(R.string.setting_api_key_summary_missing); } } public void manageCustomPath() { if (!Global.isExternalStorageManager()) { act.requestStorageManager(); return; } final String key = getString(R.string.preference_key_save_path); Preference savePathPreference = Objects.requireNonNull(findPreference(key)); MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(act); AppCompatAutoCompleteTextView edit = (AppCompatAutoCompleteTextView) View.inflate(act, R.layout.autocomplete_entry, null); edit.setHint(R.string.insert_path); builder.setView(edit); builder.setTitle(R.string.insert_path); builder.setPositiveButton(R.string.ok, (dialog, which) -> { act.getSharedPreferences("Settings", Context.MODE_PRIVATE).edit().putString(key, edit.getText().toString()).apply(); savePathPreference.setSummary(edit.getText().toString()); }).setNegativeButton(R.string.cancel, null).show(); } private void changeLauncher(PackageManager pm, ComponentName name, boolean enabled) { int enableState = enabled ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED : PackageManager.COMPONENT_ENABLED_STATE_DISABLED; pm.setComponentEnabledSetting(name, enableState, PackageManager.DONT_KILL_APP); } private void initStoragePaths(ListPreference storagePreference) { if (!Global.hasStoragePermission(act)) { storagePreference.setVisible(false); return; } List files = Global.getUsableFolders(act); List strings = new ArrayList<>(files.size() + 1); for (File f : files) { if (f != null) strings.add(f.getAbsolutePath()); } strings.add(getString(R.string.custom_path)); storagePreference.setEntries(strings.toArray(new CharSequence[0])); storagePreference.setEntryValues(strings.toArray(new CharSequence[0])); storagePreference.setSummary( act.getSharedPreferences("Settings", Context.MODE_PRIVATE) .getString(getString(R.string.preference_key_save_path), Global.MAINFOLDER.getParent()) ); storagePreference.setOnPreferenceChangeListener((preference, newValue) -> { preference.setSummary(newValue.toString()); return true; }); } private String getDataSettings(Context context) throws IOException { String[] names = new String[]{"Settings", "ScrapedTags"}; try (StringWriter sw = new StringWriter(); JsonWriter writer = new JsonWriter(sw)) { writer.setIndent("\t"); writer.beginObject(); for (String name : names) processSharedFromName(writer, context, name); writer.endObject(); writer.flush(); String settings = sw.toString(); LogUtility.d(settings); return settings; } } private void processSharedFromName(JsonWriter writer, Context context, String name) throws IOException { writer.name(name); writer.beginObject(); SharedPreferences preferences = context.getSharedPreferences(name, 0); for (Map.Entry entry : preferences.getAll().entrySet()) { writeEntry(writer, entry); } writer.endObject(); } private void writeEntry(JsonWriter writer, Map.Entry entry) throws IOException { writer.name(entry.getKey()); if (entry.getValue() instanceof Integer) writer.value((Integer) entry.getValue()); else if (entry.getValue() instanceof Boolean) writer.value((Boolean) entry.getValue()); else if (entry.getValue() instanceof String) writer.value((String) entry.getValue()); else if (entry.getValue() instanceof Long) writer.value((Long) entry.getValue()); } @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { getPreferenceManager().setSharedPreferencesName("Settings"); } private void columnMenu() { addPreferencesFromResource(R.xml.settings_column); } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/components/views/PageSwitcher.java ================================================ package com.maxwai.nclientv3.components.views; import android.app.Activity; import android.content.Context; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.View; import android.widget.LinearLayout; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.widget.AppCompatEditText; import androidx.appcompat.widget.AppCompatImageButton; import androidx.cardview.widget.CardView; import com.maxwai.nclientv3.R; import com.maxwai.nclientv3.settings.DefaultDialogs; import java.util.Locale; public class PageSwitcher extends CardView { private AppCompatImageButton prev, next; private AppCompatEditText text; @Nullable private PageChanger changer; private int totalPage; private int actualPage; public PageSwitcher(@NonNull Context context) { super(context); init(context); } public PageSwitcher(@NonNull Context context, @Nullable AttributeSet attrs) { super(context, attrs); init(context); setPages(0, 0); } public PageSwitcher(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context); } public void setChanger(@Nullable PageChanger changer) { this.changer = changer; } public void setPages(int totalPage, int actualPage) { actualPage = Math.min(totalPage, Math.max(actualPage, 1)); boolean pageChanged = this.actualPage != actualPage; if (this.totalPage == totalPage && !pageChanged) return; this.totalPage = totalPage; this.actualPage = actualPage; if (pageChanged && changer != null) changer.pageChanged(); updateViews(); } public void setTotalPage(int totalPage) { setPages(totalPage, actualPage); } private void updateViews() { ((Activity) getContext()).runOnUiThread(() -> { setVisibility(totalPage <= 1 ? View.GONE : View.VISIBLE); prev.setAlpha(actualPage > 1 ? 1f : .5f); prev.setEnabled(actualPage > 1); next.setAlpha(actualPage < totalPage ? 1f : .5f); next.setEnabled(actualPage < totalPage); text.setText(String.format(Locale.US, "%d / %d", actualPage, totalPage)); }); } private void init(Context context) { LinearLayout master = LayoutInflater.from(context).inflate(R.layout.page_switcher, this, true).findViewById(R.id.master_layout); prev = master.findViewById(R.id.prev); next = master.findViewById(R.id.next); text = master.findViewById(R.id.page_index); addViewListeners(); } private void addViewListeners() { next.setOnClickListener(v -> { if (changer != null) changer.onNextClicked(this); }); prev.setOnClickListener(v -> { if (changer != null) changer.onPrevClicked(this); }); text.setOnClickListener(v -> loadDialog()); } public int getActualPage() { return actualPage; } public void setActualPage(int actualPage) { setPages(totalPage, actualPage); } public boolean lastPageReached() { return actualPage == totalPage; } private void loadDialog() { DefaultDialogs.pageChangerDialog( new DefaultDialogs.Builder(getContext()) .setActual(actualPage) .setMin(1) .setMax(totalPage) .setTitle(R.string.change_page) .setDrawable(R.drawable.ic_find_in_page) .setDialogs(new DefaultDialogs.CustomDialogResults() { @Override public void positive(int actual) { setActualPage(actual); } }) ); } public interface PageChanger { void pageChanged(); void onPrevClicked(PageSwitcher switcher); void onNextClicked(PageSwitcher switcher); } public static class DefaultPageChanger implements PageChanger { @Override public void pageChanged() { } @Override public void onPrevClicked(PageSwitcher switcher) { switcher.setActualPage(switcher.getActualPage() - 1); } @Override public void onNextClicked(PageSwitcher switcher) { switcher.setActualPage(switcher.getActualPage() + 1); } } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/components/views/RangeSelector.java ================================================ package com.maxwai.nclientv3.components.views; import android.content.Context; import android.view.View; import android.widget.ImageButton; import android.widget.LinearLayout; import android.widget.SeekBar; import android.widget.TextView; import android.widget.Toast; import androidx.annotation.NonNull; import com.maxwai.nclientv3.R; import com.maxwai.nclientv3.api.components.Gallery; import com.maxwai.nclientv3.async.downloader.DownloadGalleryV2; import com.google.android.material.dialog.MaterialAlertDialogBuilder; public class RangeSelector extends MaterialAlertDialogBuilder { private final Gallery gallery; private SeekBar s1, s2; public RangeSelector(@NonNull Context context, Gallery gallery) { super(context); this.gallery = gallery; View v = View.inflate(context, R.layout.range_selector, null); setView(v); LinearLayout l1 = v.findViewById(R.id.layout1); LinearLayout l2 = v.findViewById(R.id.layout2); applyLogic(l1, true); applyLogic(l2, false); setPositiveButton(R.string.ok, (dialog, which) -> { if (s1.getProgress() <= s2.getProgress()) DownloadGalleryV2.downloadRange(context, gallery, s1.getProgress(), s2.getProgress()); else Toast.makeText(context, R.string.invalid_range_selected, Toast.LENGTH_SHORT).show(); }).setNegativeButton(R.string.cancel, null); setCancelable(true); } private View.OnClickListener getPrevListener(SeekBar s) { return v -> s.setProgress(s.getProgress() - 1); } private View.OnClickListener getNextListener(SeekBar s) { return v -> s.setProgress(s.getProgress() + 1); } private SeekBar.OnSeekBarChangeListener getSeekBarListener(TextView t) { return new SeekBar.OnSeekBarChangeListener() { @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { t.setText(getContext().getString(R.string.page_format, progress + 1, gallery.getPageCount())); } @Override public void onStartTrackingTouch(SeekBar seekBar) { } @Override public void onStopTrackingTouch(SeekBar seekBar) { } }; } private void applyLogic(LinearLayout layout, boolean start) { ImageButton prev = layout.findViewById(R.id.prev); ImageButton next = layout.findViewById(R.id.next); TextView pages = layout.findViewById(R.id.pages); SeekBar seekBar = layout.findViewById(R.id.seekBar); prev.setOnClickListener(getPrevListener(seekBar)); next.setOnClickListener(getNextListener(seekBar)); seekBar.setMax(gallery.getPageCount() - 1); seekBar.setOnSeekBarChangeListener(getSeekBarListener(pages)); seekBar.setProgress(1); seekBar.setProgress(start ? 0 : gallery.getPageCount()); if (start) s1 = seekBar; else s2 = seekBar; } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/components/views/ZoomFragment.java ================================================ package com.maxwai.nclientv3.components.views; import static com.bumptech.glide.request.target.Target.SIZE_ORIGINAL; import android.graphics.drawable.Animatable; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ImageButton; import android.widget.ImageView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentActivity; import com.bumptech.glide.Priority; import com.bumptech.glide.RequestBuilder; import com.bumptech.glide.RequestManager; import com.bumptech.glide.load.DataSource; import com.bumptech.glide.load.engine.GlideException; import com.bumptech.glide.load.resource.bitmap.Rotate; import com.bumptech.glide.load.resource.gif.GifDrawable; import com.bumptech.glide.request.RequestListener; import com.bumptech.glide.request.RequestOptions; import com.bumptech.glide.request.target.ImageViewTarget; import com.bumptech.glide.request.target.Target; import com.bumptech.glide.request.transition.Transition; import com.maxwai.nclientv3.R; import com.maxwai.nclientv3.ZoomActivity; import com.maxwai.nclientv3.api.components.Gallery; import com.maxwai.nclientv3.api.components.GenericGallery; import com.maxwai.nclientv3.components.GlideX; import com.maxwai.nclientv3.files.GalleryFolder; import com.maxwai.nclientv3.files.PageFile; import com.maxwai.nclientv3.github.chrisbanes.photoview.PhotoView; import com.maxwai.nclientv3.settings.Global; import com.maxwai.nclientv3.utility.LogUtility; public class ZoomFragment extends Fragment { public interface OnZoomChangeListener { void onZoomChange(View v, float zoomLevel); } private static final float MAX_SCALE = 8f; private static final float CHANGE_PAGE_THRESHOLD = .2f; private PhotoView photoView = null; private ImageButton retryButton; private PageFile pageFile = null; private Uri url; private int degree = 0; private boolean completedDownload = false; private View.OnClickListener clickListener; private OnZoomChangeListener zoomChangeListener; private ImageViewTarget target = null; public ZoomFragment() { } public static ZoomFragment newInstance(GenericGallery gallery, int page, @Nullable GalleryFolder directory) { Bundle args = new Bundle(); args.putString("URL", gallery.isLocal() ? null : ((Gallery) gallery).getPageUrl(page).toString()); args.putParcelable("FOLDER", directory == null ? null : directory.getPage(page + 1)); ZoomFragment fragment = new ZoomFragment(); fragment.setArguments(args); return fragment; } public void setClickListener(View.OnClickListener clickListener) { this.clickListener = clickListener; } public void setZoomChangeListener(OnZoomChangeListener zoomChangeListener) { this.zoomChangeListener = zoomChangeListener; } private float calculateScaleFactor(int width, int height) { FragmentActivity activity = getActivity(); if (height < width * 2) return Global.getDefaultZoom(); float finalSize = ((float) Global.getDeviceWidth(activity) * height) / ((float) Global.getDeviceHeight(activity) * width); finalSize = Math.max(finalSize, Global.getDefaultZoom()); finalSize = Math.min(finalSize, MAX_SCALE); LogUtility.d("Final scale: " + finalSize); return (float) Math.floor(finalSize); } @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { View rootView = inflater.inflate(R.layout.fragment_zoom, container, false); ZoomActivity activity = (ZoomActivity) getActivity(); assert getArguments() != null; assert activity != null; //find views photoView = rootView.findViewById(R.id.image); retryButton = rootView.findViewById(R.id.imageView); //read arguments String str = getArguments().getString("URL"); url = str == null ? null : Uri.parse(str); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { pageFile = getArguments().getParcelable("FOLDER", PageFile.class); } else { pageFile = getArguments().getParcelable("FOLDER"); } photoView.setAllowParentInterceptOnEdge(true); photoView.setOnPhotoTapListener((view, x, y) -> { boolean prev = x < CHANGE_PAGE_THRESHOLD; boolean next = x > 1f - CHANGE_PAGE_THRESHOLD; if ((prev || next) && Global.isButtonChangePage()) { activity.changeClosePage(next); } else if (clickListener != null) { clickListener.onClick(view); } LogUtility.d(view, x, y, prev, next); }); photoView.setOnScaleChangeListener((float scaleFactor, float focusX, float focusY)->{ if(this.zoomChangeListener!=null) { this.zoomChangeListener.onZoomChange(rootView, photoView.getScale()); } }); photoView.setMaximumScale(MAX_SCALE); retryButton.setOnClickListener(v -> loadImage()); createTarget(); loadImage(); return rootView; } private void createTarget() { target = new ImageViewTarget(photoView) { @Override protected void setResource(@Nullable Drawable resource) { photoView.setImageDrawable(resource); } void applyDrawable(ImageView toShow, ImageView toHide, Drawable drawable) { toShow.setVisibility(View.VISIBLE); toHide.setVisibility(View.GONE); toShow.setImageDrawable(drawable); if (toShow instanceof PhotoView) scalePhoto(drawable); } @Override public void onLoadStarted(@Nullable Drawable placeholder) { super.onLoadStarted(placeholder); applyDrawable(photoView, retryButton, placeholder); } @Override public void onLoadFailed(@Nullable Drawable errorDrawable) { super.onLoadFailed(errorDrawable); applyDrawable(retryButton, photoView, errorDrawable); } @Override public void onResourceReady(@NonNull Drawable resource, @Nullable Transition transition) { applyDrawable(photoView, retryButton, resource); if (resource instanceof Animatable) ((GifDrawable) resource).start(); } @Override public void onLoadCleared(@Nullable Drawable placeholder) { super.onLoadCleared(placeholder); applyDrawable(photoView, retryButton, placeholder); } }; } private void scalePhoto(Drawable drawable) { photoView.setScale(calculateScaleFactor( drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight() ), 0, 0, false); } public void loadImage() { loadImage(Priority.NORMAL); } public void loadImage(Priority priority) { if (completedDownload) return; cancelRequest(); RequestBuilder dra = loadPage(); if (dra == null) return; dra .transform(new Rotate(degree)) .apply(new RequestOptions().fitCenter()) .placeholder(R.drawable.ic_launcher_foreground) .error(R.drawable.ic_refresh) .priority(priority) .addListener(new RequestListener<>() { @Override public boolean onLoadFailed(@Nullable GlideException e, Object model, @NonNull Target target, boolean isFirstResource) { return false; } @Override public boolean onResourceReady(@NonNull Drawable resource, @NonNull Object model, Target target, @NonNull DataSource dataSource, boolean isFirstResource) { completedDownload = true; return false; } }) .into(target); } @Nullable private RequestBuilder loadPage() { RequestBuilder request; RequestManager glide = GlideX.with(photoView); if (glide == null) return null; if (pageFile != null) { request = glide.load(pageFile); LogUtility.d("Requested file glide: " + pageFile); } else { if (url == null) request = glide.load(R.mipmap.ic_launcher); else { LogUtility.d("Requested url glide: " + url); request = glide.load(url); } } return request.override(SIZE_ORIGINAL); } public Drawable getDrawable() { return photoView.getDrawable(); } public void cancelRequest() { if (completedDownload) return; if (photoView != null && target != null) { RequestManager manager = GlideX.with(photoView); if (manager != null) manager.clear(target); } } private void updateDegree() { degree = (degree + 270) % 360; loadImage(); } public void rotate() { updateDegree(); } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/components/widgets/ChipTag.java ================================================ package com.maxwai.nclientv3.components.widgets; import android.content.Context; import android.graphics.drawable.Drawable; import android.util.AttributeSet; import androidx.core.content.ContextCompat; import com.maxwai.nclientv3.R; import com.maxwai.nclientv3.api.components.Tag; import com.maxwai.nclientv3.api.enums.TagStatus; import com.maxwai.nclientv3.settings.Global; import com.google.android.material.chip.Chip; public class ChipTag extends Chip { private Tag tag; private boolean canBeAvoided = true; public ChipTag(Context context) { super(context); } public ChipTag(Context context, AttributeSet attrs) { super(context, attrs); } public ChipTag(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } public void init(Tag t, boolean close, boolean canBeAvoided) { setTag(t); setCloseIconVisible(close); setCanBeAvoided(canBeAvoided); } private void setCanBeAvoided(boolean canBeAvoided) { this.canBeAvoided = canBeAvoided; } @Override public Tag getTag() { return tag; } private void setTag(Tag tag) { this.tag = tag; setText(tag.getName()); loadStatusIcon(); } public void changeStatus(TagStatus status) { tag.setStatus(status); loadStatusIcon(); } public void updateStatus() { switch (tag.getStatus()) { case DEFAULT: changeStatus(TagStatus.ACCEPTED); break; case ACCEPTED: changeStatus(canBeAvoided ? TagStatus.AVOIDED : TagStatus.DEFAULT); break; case AVOIDED: changeStatus(TagStatus.DEFAULT); break; } } private void loadStatusIcon() { Drawable drawable = ContextCompat.getDrawable(getContext(), tag.getStatus() == TagStatus.ACCEPTED ? R.drawable.ic_check : tag.getStatus() == TagStatus.AVOIDED ? R.drawable.ic_close : R.drawable.ic_void); if (drawable == null) { setChipIconResource(tag.getStatus() == TagStatus.ACCEPTED ? R.drawable.ic_check : tag.getStatus() == TagStatus.AVOIDED ? R.drawable.ic_close : R.drawable.ic_void); return; } setChipIcon(drawable); Global.setTint(getContext(), getChipIcon()); } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/components/widgets/CustomGridLayoutManager.java ================================================ package com.maxwai.nclientv3.components.widgets; import android.content.Context; import androidx.recyclerview.widget.GridLayoutManager; public class CustomGridLayoutManager extends GridLayoutManager { public CustomGridLayoutManager(Context context, int spanCount) { super(context, Math.max(1, spanCount)); } public CustomGridLayoutManager(Context context, int spanCount, int orientation, boolean reverseLayout) { super(context, Math.max(1, spanCount), orientation, reverseLayout); } @Override public boolean supportsPredictiveItemAnimations() { return false; } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/components/widgets/CustomImageView.java ================================================ package com.maxwai.nclientv3.components.widgets; import android.content.Context; import android.graphics.Matrix; import android.graphics.drawable.Drawable; import android.util.AttributeSet; import androidx.annotation.Nullable; import androidx.appcompat.widget.AppCompatImageView; public class CustomImageView extends AppCompatImageView { public CustomImageView(Context context) { super(context); } public CustomImageView(Context context, AttributeSet attrs) { super(context, attrs); } public CustomImageView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } @Override public void setImageDrawable(@Nullable Drawable drawable) { super.setImageDrawable(drawable); invalidate(); } @Override protected boolean setFrame(int l, int t, int r, int b) { final Matrix matrix = getImageMatrix(); float scale; final int viewWidth = getWidth() - getPaddingLeft() - getPaddingRight(); final int viewHeight = getHeight() - getPaddingTop() - getPaddingBottom(); final int drawableWidth = getDrawable().getIntrinsicWidth(); final int drawableHeight = getDrawable().getIntrinsicHeight(); if (drawableWidth * viewHeight > drawableHeight * viewWidth) { scale = (float) viewHeight / (float) drawableHeight; } else { scale = (float) viewWidth / (float) drawableWidth; } matrix.setScale(scale, scale); setImageMatrix(matrix); return super.setFrame(l, t, r, b); } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/components/widgets/CustomLinearLayoutManager.java ================================================ package com.maxwai.nclientv3.components.widgets; import android.content.Context; import androidx.recyclerview.widget.LinearLayoutManager; public class CustomLinearLayoutManager extends LinearLayoutManager { public CustomLinearLayoutManager(Context context) { super(context); } @Override public boolean supportsPredictiveItemAnimations() { return false; } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/components/widgets/CustomSearchView.java ================================================ package com.maxwai.nclientv3.components.widgets; import android.content.Context; import android.util.AttributeSet; import androidx.appcompat.widget.SearchView; public class CustomSearchView extends SearchView { public CustomSearchView(Context context) { super(context); } public CustomSearchView(Context context, AttributeSet attrs) { super(context, attrs); } public CustomSearchView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } @Override public void setOnQueryTextListener(OnQueryTextListener listener) { super.setOnQueryTextListener(listener); SearchAutoComplete mSearchSrcTextView = this.findViewById(androidx.appcompat.R.id.search_src_text); mSearchSrcTextView.setOnEditorActionListener((textView, i, keyEvent) -> { if (listener != null) { listener.onQueryTextSubmit(getQuery().toString()); } return true; }); } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/components/widgets/CustomSwipe.java ================================================ package com.maxwai.nclientv3.components.widgets; import android.content.Context; import android.util.AttributeSet; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import com.maxwai.nclientv3.utility.LogUtility; public class CustomSwipe extends SwipeRefreshLayout { public CustomSwipe(@NonNull Context context) { super(context); } public CustomSwipe(@NonNull Context context, @Nullable AttributeSet attrs) { super(context, attrs); } @Override public void setEnabled(boolean refreshing) { try { throw new Exception(); } catch (Exception e) { LogUtility.e("NEW VALUE: " + refreshing + ",," + e.getLocalizedMessage(), e); } super.setRefreshing(refreshing); } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/components/widgets/TagTypePage.java ================================================ package com.maxwai.nclientv3.components.widgets; import android.app.Activity; import android.content.res.Configuration; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.fragment.app.Fragment; import androidx.recyclerview.widget.RecyclerView; import com.maxwai.nclientv3.R; import com.maxwai.nclientv3.TagFilterActivity; import com.maxwai.nclientv3.adapters.TagsAdapter; import com.maxwai.nclientv3.api.enums.TagType; import com.maxwai.nclientv3.async.ScrapeTags; import com.maxwai.nclientv3.settings.Global; import com.maxwai.nclientv3.settings.TagV2; public class TagTypePage extends Fragment { private TagType type; private RecyclerView recyclerView; private TagFilterActivity activity; private String query; private TagsAdapter adapter; public TagTypePage() { } private static int getTag(int page) { switch (page) { case 0: return TagType.UNKNOWN.getId();//tags with status case 1: return TagType.TAG.getId(); case 2: return TagType.ARTIST.getId(); case 3: return TagType.CHARACTER.getId(); case 4: return TagType.PARODY.getId(); case 5: return TagType.GROUP.getId(); case 6: return TagType.CATEGORY.getId();//online blacklisted tags } return -1; } public static TagTypePage newInstance(int page) { TagTypePage fragment = new TagTypePage(); Bundle args = new Bundle(); args.putInt("TAGTYPE", getTag(page)); fragment.setArguments(args); return fragment; } public RecyclerView getRecyclerView() { return recyclerView; } @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { activity = (TagFilterActivity) getActivity(); type = TagType.values[requireArguments().getInt("TAGTYPE")]; View rootView = inflater.inflate(R.layout.fragment_tag_filter, container, false); recyclerView = rootView.findViewById(R.id.recycler); Global.applyFastScroller(recyclerView); loadTags(); return rootView; } public void loadTags() { recyclerView.setLayoutManager(new CustomGridLayoutManager(activity, getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE ? 4 : 2)); if (type.equals(TagType.UNKNOWN)) adapter = new TagsAdapter(activity, query, false); else if (type.equals(TagType.CATEGORY)) adapter = new TagsAdapter(activity, query, true); else adapter = new TagsAdapter(activity, query, type); recyclerView.setAdapter(adapter); } public void refilter(String newText) { if (activity != null) activity.runOnUiThread(() -> adapter.getFilter().filter(newText)); } public void reset() { if (type.equals(TagType.UNKNOWN)) TagV2.resetAllStatus(); else if (!type.equals(TagType.CATEGORY)) { ScrapeTags.startWork(activity); } Activity activity = getActivity(); if (activity == null || adapter == null) return; activity.runOnUiThread(adapter::notifyDataSetChanged); } public void changeSize() { refilter(query); } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/files/GalleryFolder.java ================================================ package com.maxwai.nclientv3.files; import android.content.Context; import android.os.Build; import android.os.Parcel; import android.os.Parcelable; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.collection.SparseArrayCompat; import com.maxwai.nclientv3.api.enums.SpecialTagIds; import com.maxwai.nclientv3.settings.Global; import java.io.File; import java.util.Iterator; import java.util.Objects; import java.util.regex.Matcher; import java.util.regex.Pattern; public class GalleryFolder implements Parcelable, Iterable { public static final Creator CREATOR = new Creator<>() { @Override public GalleryFolder createFromParcel(Parcel in) { return new GalleryFolder(in); } @Override public GalleryFolder[] newArray(int size) { return new GalleryFolder[size]; } }; private static final Pattern FILE_PATTERN = Pattern.compile("^0*(\\d{1,9})\\.(gif|png|jpg|webp)$", Pattern.CASE_INSENSITIVE); private static final Pattern IDFILE_PATTERN = Pattern.compile("^\\.(\\d{1,6})$"); private static final String NOMEDIA_FILE = ".nomedia"; private final SparseArrayCompat pageArray = new SparseArrayCompat<>(); private final File folder; private int id = SpecialTagIds.INVALID_ID; private int max = -1; private int min = Integer.MAX_VALUE; private File nomedia; public GalleryFolder(@NonNull String child) { this(Global.DOWNLOADFOLDER, child); } public GalleryFolder(@Nullable File parent, @NonNull String child) { this(new File(parent, child)); } public GalleryFolder(File file) { folder = file; if (!folder.isDirectory()) throw new IllegalArgumentException("File is not a folder"); parseFiles(); } protected GalleryFolder(Parcel in) { folder = new File(Objects.requireNonNull(in.readString())); id = in.readInt(); min = in.readInt(); max = in.readInt(); int pageCount = in.readInt(); for (int i = 0; i < pageCount; i++) { int k = in.readInt(); PageFile f; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { f = in.readParcelable(PageFile.class.getClassLoader(), PageFile.class); } else { f = in.readParcelable(PageFile.class.getClassLoader()); } pageArray.put(k, f); } } public static @Nullable GalleryFolder fromId(@Nullable Context context, int id) { File f = Global.findGalleryFolder(context, id); if (f == null) return null; return new GalleryFolder(f); } private void parseFiles() { File[] files = folder.listFiles(); if (files == null) return; for (File f : files) { elaborateFile(f); } } private void elaborateFile(File f) { String name = f.getName(); Matcher matcher = FILE_PATTERN.matcher(name); if (matcher.matches()) elaboratePage(f, matcher); if (id == SpecialTagIds.INVALID_ID) { matcher = IDFILE_PATTERN.matcher(name); if (matcher.matches()) id = elaborateId(matcher); } if (nomedia == null && name.equals(NOMEDIA_FILE)) nomedia = f; } private int elaborateId(Matcher matcher) { return Integer.parseInt(Objects.requireNonNull(matcher.group(1))); } public int getPageCount() { return pageArray.size(); } public File getGalleryDataFile() { return nomedia; } public File getFolder() { return folder; } public int getMax() { return max; } public int getMin() { return min; } private void elaboratePage(File f, Matcher matcher) { int page = Integer.parseInt(Objects.requireNonNull(matcher.group(1))); pageArray.append(page, new PageFile(f, page)); if (page > max) max = page; if (page < min) min = page; } public PageFile getPage(int page) { return pageArray.get(page); } public PageFile getFirstPage() { int minPage = Integer.MAX_VALUE; for(int i = 0; i < pageArray.size(); i++) { if (minPage > pageArray.keyAt(i)) { minPage = pageArray.keyAt(i); } } return pageArray.get(minPage); } public int getId() { return id; } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeString(folder.getAbsolutePath()); dest.writeInt(id); dest.writeInt(min); dest.writeInt(max); dest.writeInt(pageArray.size()); for (int i = 0; i < pageArray.size(); i++) { dest.writeInt(pageArray.keyAt(i)); dest.writeParcelable(pageArray.valueAt(i), flags); } } @NonNull @Override public Iterator iterator() { return new PageFileIterator(pageArray); } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; GalleryFolder pageFiles = (GalleryFolder) o; return folder.equals(pageFiles.folder); } @Override public int hashCode() { return folder.hashCode(); } @NonNull @Override public String toString() { return "GalleryFolder{" + "pageArray=" + pageArray + ", folder=" + folder + ", id=" + id + ", max=" + max + ", min=" + min + ", nomedia=" + nomedia + '}'; } public static class PageFileIterator implements Iterator { private final SparseArrayCompat files; private int reach = 0; public PageFileIterator(SparseArrayCompat files) { this.files = files; } @Override public boolean hasNext() { return reach < files.size(); } @Override public PageFile next() { PageFile f = files.valueAt(reach); reach++; return f; } } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/files/PageFile.java ================================================ package com.maxwai.nclientv3.files; import android.net.Uri; import android.os.Parcel; import android.os.Parcelable; import java.io.File; import java.util.Objects; public class PageFile extends File implements Parcelable { public static final Creator CREATOR = new Creator<>() { @Override public PageFile createFromParcel(Parcel in) { return new PageFile(in); } @Override public PageFile[] newArray(int size) { return new PageFile[size]; } }; private final int page; public PageFile(File file, int page) { super(file.getAbsolutePath()); this.page = page; } protected PageFile(Parcel in) { super(Objects.requireNonNull(in.readString())); page = in.readInt(); } public Uri toUri() { return Uri.fromFile(this); } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeString(this.getAbsolutePath()); dest.writeInt(page); } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/github/chrisbanes/photoview/Compat.java ================================================ /* Copyright 2011, 2012 Chris Banes. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package com.maxwai.nclientv3.github.chrisbanes.photoview; import android.view.View; class Compat { public static void postOnAnimation(View view, Runnable runnable) { postOnAnimationJellyBean(view, runnable); } private static void postOnAnimationJellyBean(View view, Runnable runnable) { view.postOnAnimation(runnable); } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/github/chrisbanes/photoview/CustomGestureDetector.java ================================================ /* Copyright 2011, 2012 Chris Banes.

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package com.maxwai.nclientv3.github.chrisbanes.photoview; import android.content.Context; import android.view.MotionEvent; import android.view.ScaleGestureDetector; import android.view.VelocityTracker; import android.view.ViewConfiguration; import androidx.annotation.NonNull; /** * Does a whole lot of gesture detecting. */ class CustomGestureDetector { private static final int INVALID_POINTER_ID = -1; private final ScaleGestureDetector mDetector; private final float mTouchSlop; private final float mMinimumVelocity; private final OnGestureListener mListener; private int mActivePointerId = INVALID_POINTER_ID; private int mActivePointerIndex = 0; private VelocityTracker mVelocityTracker; private boolean mIsDragging; private float mLastTouchX; private float mLastTouchY; CustomGestureDetector(Context context, OnGestureListener listener) { final ViewConfiguration configuration = ViewConfiguration .get(context); mMinimumVelocity = configuration.getScaledMinimumFlingVelocity(); mTouchSlop = configuration.getScaledTouchSlop(); mListener = listener; ScaleGestureDetector.OnScaleGestureListener mScaleListener = new ScaleGestureDetector.OnScaleGestureListener() { private float lastFocusX, lastFocusY = 0; @Override public boolean onScale(ScaleGestureDetector detector) { float scaleFactor = detector.getScaleFactor(); if (Float.isNaN(scaleFactor) || Float.isInfinite(scaleFactor)) return false; if (scaleFactor >= 0) { mListener.onScale(scaleFactor, detector.getFocusX(), detector.getFocusY(), detector.getFocusX() - lastFocusX, detector.getFocusY() - lastFocusY ); lastFocusX = detector.getFocusX(); lastFocusY = detector.getFocusY(); } return true; } @Override public boolean onScaleBegin(ScaleGestureDetector detector) { lastFocusX = detector.getFocusX(); lastFocusY = detector.getFocusY(); return true; } @Override public void onScaleEnd(@NonNull ScaleGestureDetector detector) { // NO-OP } }; mDetector = new ScaleGestureDetector(context, mScaleListener); } private float getActiveX(MotionEvent ev) { try { return ev.getX(mActivePointerIndex); } catch (Exception e) { return ev.getX(); } } private float getActiveY(MotionEvent ev) { try { return ev.getY(mActivePointerIndex); } catch (Exception e) { return ev.getY(); } } public boolean isScaling() { return mDetector.isInProgress(); } public boolean isDragging() { return mIsDragging; } public boolean onTouchEvent(MotionEvent ev) { try { mDetector.onTouchEvent(ev); return processTouchEvent(ev); } catch (IllegalArgumentException e) { // Fix for support lib bug, happening when onDestroy is called return true; } } private boolean processTouchEvent(MotionEvent ev) { final int action = ev.getAction(); switch (action & MotionEvent.ACTION_MASK) { case MotionEvent.ACTION_DOWN: mActivePointerId = ev.getPointerId(0); mVelocityTracker = VelocityTracker.obtain(); if (null != mVelocityTracker) { mVelocityTracker.addMovement(ev); } mLastTouchX = getActiveX(ev); mLastTouchY = getActiveY(ev); mIsDragging = false; break; case MotionEvent.ACTION_MOVE: final float x = getActiveX(ev); final float y = getActiveY(ev); final float dx = x - mLastTouchX, dy = y - mLastTouchY; if (!mIsDragging) { // Use Pythagoras to see if drag length is larger than // touch slop mIsDragging = Math.sqrt((dx * dx) + (dy * dy)) >= mTouchSlop; } if (mIsDragging) { mListener.onDrag(dx, dy); mLastTouchX = x; mLastTouchY = y; if (null != mVelocityTracker) { mVelocityTracker.addMovement(ev); } } break; case MotionEvent.ACTION_CANCEL: mActivePointerId = INVALID_POINTER_ID; // Recycle Velocity Tracker if (null != mVelocityTracker) { mVelocityTracker.recycle(); mVelocityTracker = null; } break; case MotionEvent.ACTION_UP: mActivePointerId = INVALID_POINTER_ID; if (mIsDragging) { if (null != mVelocityTracker) { mLastTouchX = getActiveX(ev); mLastTouchY = getActiveY(ev); // Compute velocity within the last 1000ms mVelocityTracker.addMovement(ev); mVelocityTracker.computeCurrentVelocity(1000); final float vX = mVelocityTracker.getXVelocity(), vY = mVelocityTracker .getYVelocity(); // If the velocity is greater than minVelocity, call // listener if (Math.max(Math.abs(vX), Math.abs(vY)) >= mMinimumVelocity) { mListener.onFling(mLastTouchX, mLastTouchY, -vX, -vY); } } } // Recycle Velocity Tracker if (null != mVelocityTracker) { mVelocityTracker.recycle(); mVelocityTracker = null; } break; case MotionEvent.ACTION_POINTER_UP: final int pointerIndex = Util.getPointerIndex(ev.getAction()); final int pointerId = ev.getPointerId(pointerIndex); if (pointerId == mActivePointerId) { // This was our active pointer going up. Choose a new // active pointer and adjust accordingly. final int newPointerIndex = pointerIndex == 0 ? 1 : 0; mActivePointerId = ev.getPointerId(newPointerIndex); mLastTouchX = ev.getX(newPointerIndex); mLastTouchY = ev.getY(newPointerIndex); } break; } mActivePointerIndex = ev .findPointerIndex(mActivePointerId != INVALID_POINTER_ID ? mActivePointerId : 0); return true; } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/github/chrisbanes/photoview/OnGestureListener.java ================================================ /* Copyright 2011, 2012 Chris Banes. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package com.maxwai.nclientv3.github.chrisbanes.photoview; interface OnGestureListener { void onDrag(float dx, float dy); void onFling(float startX, float startY, float velocityX, float velocityY); void onScale(float scaleFactor, float focusX, float focusY); void onScale(float scaleFactor, float focusX, float focusY, float dx, float dy); } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/github/chrisbanes/photoview/OnMatrixChangedListener.java ================================================ package com.maxwai.nclientv3.github.chrisbanes.photoview; import android.graphics.RectF; /** * Interface definition for a callback to be invoked when the internal Matrix has changed for * this View. */ public interface OnMatrixChangedListener { /** * Callback for when the Matrix displaying the Drawable has changed. This could be because * the View's bounds have changed, or the user has zoomed. * * @param rect - Rectangle displaying the Drawable's new bounds. */ void onMatrixChanged(RectF rect); } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/github/chrisbanes/photoview/OnOutsidePhotoTapListener.java ================================================ package com.maxwai.nclientv3.github.chrisbanes.photoview; import android.widget.ImageView; /** * Callback when the user tapped outside of the photo */ public interface OnOutsidePhotoTapListener { /** * The outside of the photo has been tapped */ void onOutsidePhotoTap(ImageView imageView); } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/github/chrisbanes/photoview/OnPhotoTapListener.java ================================================ package com.maxwai.nclientv3.github.chrisbanes.photoview; import android.widget.ImageView; /** * A callback to be invoked when the Photo is tapped with a single * tap. */ public interface OnPhotoTapListener { /** * A callback to receive where the user taps on a photo. You will only receive a callback if * the user taps on the actual photo, tapping on 'whitespace' will be ignored. * * @param view ImageView the user tapped. * @param x where the user tapped from the of the Drawable, as percentage of the * Drawable width. * @param y where the user tapped from the top of the Drawable, as percentage of the * Drawable height. */ void onPhotoTap(ImageView view, float x, float y); } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/github/chrisbanes/photoview/OnScaleChangedListener.java ================================================ package com.maxwai.nclientv3.github.chrisbanes.photoview; /** * Interface definition for callback to be invoked when attached ImageView scale changes */ public interface OnScaleChangedListener { /** * Callback for when the scale changes * * @param scaleFactor the scale factor (less than 1 for zoom out, greater than 1 for zoom in) * @param focusX focal point X position * @param focusY focal point Y position */ void onScaleChange(float scaleFactor, float focusX, float focusY); } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/github/chrisbanes/photoview/OnSingleFlingListener.java ================================================ package com.maxwai.nclientv3.github.chrisbanes.photoview; import android.view.MotionEvent; /** * A callback to be invoked when the ImageView is flung with a single * touch */ public interface OnSingleFlingListener { /** * A callback to receive where the user flings on a ImageView. You will receive a callback if * the user flings anywhere on the view. * * @param e1 MotionEvent the user first touch. * @param e2 MotionEvent the user last touch. * @param velocityX distance of user's horizontal fling. * @param velocityY distance of user's vertical fling. */ boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY); } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/github/chrisbanes/photoview/OnViewDragListener.java ================================================ package com.maxwai.nclientv3.github.chrisbanes.photoview; /** * Interface definition for a callback to be invoked when the photo is experiencing a drag event */ public interface OnViewDragListener { /** * Callback for when the photo is experiencing a drag event. This cannot be invoked when the * user is scaling. * * @param dx The change of the coordinates in the x-direction * @param dy The change of the coordinates in the y-direction */ void onDrag(float dx, float dy); } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/github/chrisbanes/photoview/OnViewTapListener.java ================================================ package com.maxwai.nclientv3.github.chrisbanes.photoview; import android.view.View; public interface OnViewTapListener { /** * A callback to receive where the user taps on a ImageView. You will receive a callback if * the user taps anywhere on the view, tapping on 'whitespace' will not be ignored. * * @param view - View the user tapped. * @param x - where the user tapped from the left of the View. * @param y - where the user tapped from the top of the View. */ void onViewTap(View view, float x, float y); } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/github/chrisbanes/photoview/PhotoView.java ================================================ /* Copyright 2011, 2012 Chris Banes.

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package com.maxwai.nclientv3.github.chrisbanes.photoview; import android.content.Context; import android.graphics.Matrix; import android.graphics.RectF; import android.graphics.drawable.Drawable; import android.net.Uri; import android.util.AttributeSet; import android.view.GestureDetector; import androidx.appcompat.widget.AppCompatImageView; /** * A zoomable ImageView. See {@link PhotoViewAttacher} for most of the details on how the zooming * is accomplished */ @SuppressWarnings("unused") public class PhotoView extends AppCompatImageView { private PhotoViewAttacher attacher; private ScaleType pendingScaleType; public PhotoView(Context context) { this(context, null); } public PhotoView(Context context, AttributeSet attr) { this(context, attr, 0); } public PhotoView(Context context, AttributeSet attr, int defStyle) { super(context, attr, defStyle); init(); } private void init() { attacher = new PhotoViewAttacher(this); //We always pose as a Matrix scale type, though we can change to another scale type //via the attacher super.setScaleType(ScaleType.MATRIX); //apply the previously applied scale type if (pendingScaleType != null) { setScaleType(pendingScaleType); pendingScaleType = null; } } /** * Get the current {@link PhotoViewAttacher} for this view. Be wary of holding on to references * to this attacher, as it has a reference to this view, which, if a reference is held in the * wrong place, can cause memory leaks. * * @return the attacher. */ public PhotoViewAttacher getAttacher() { return attacher; } @Override public ScaleType getScaleType() { return attacher.getScaleType(); } @Override public void setScaleType(ScaleType scaleType) { if (attacher == null) { pendingScaleType = scaleType; } else { attacher.setScaleType(scaleType); } } @Override public Matrix getImageMatrix() { return attacher.getImageMatrix(); } @Override public void setOnLongClickListener(OnLongClickListener l) { attacher.setOnLongClickListener(l); } @Override public void setOnClickListener(OnClickListener l) { attacher.setOnClickListener(l); } @Override public void setImageDrawable(Drawable drawable) { super.setImageDrawable(drawable); // setImageBitmap calls through to this method if (attacher != null) { attacher.update(); } } @Override public void setImageResource(int resId) { super.setImageResource(resId); if (attacher != null) { attacher.update(); } } @Override public void setImageURI(Uri uri) { super.setImageURI(uri); if (attacher != null) { attacher.update(); } } @Override protected boolean setFrame(int l, int t, int r, int b) { boolean changed = super.setFrame(l, t, r, b); if (changed) { attacher.update(); } return changed; } public void setRotationTo(float rotationDegree) { attacher.setRotationTo(rotationDegree); } public void setRotationBy(float rotationDegree) { attacher.setRotationBy(rotationDegree); } public boolean isZoomable() { return attacher.isZoomable(); } public void setZoomable(boolean zoomable) { attacher.setZoomable(zoomable); } public RectF getDisplayRect() { return attacher.getDisplayRect(); } public void getDisplayMatrix(Matrix matrix) { attacher.getDisplayMatrix(matrix); } @SuppressWarnings("UnusedReturnValue") public boolean setDisplayMatrix(Matrix finalRectangle) { return attacher.setDisplayMatrix(finalRectangle); } public void getSuppMatrix(Matrix matrix) { attacher.getSuppMatrix(matrix); } public boolean setSuppMatrix(Matrix matrix) { return attacher.setDisplayMatrix(matrix); } public float getMinimumScale() { return attacher.getMinimumScale(); } public void setMinimumScale(float minimumScale) { attacher.setMinimumScale(minimumScale); } public float getMediumScale() { return attacher.getMediumScale(); } public void setMediumScale(float mediumScale) { attacher.setMediumScale(mediumScale); } public float getMaximumScale() { return attacher.getMaximumScale(); } public void setMaximumScale(float maximumScale) { attacher.setMaximumScale(maximumScale); } public float getScale() { return attacher.getScale(); } public void setScale(float scale) { attacher.setScale(scale); } public void setAllowParentInterceptOnEdge(boolean allow) { attacher.setAllowParentInterceptOnEdge(allow); } public void setScaleLevels(float minimumScale, float mediumScale, float maximumScale) { attacher.setScaleLevels(minimumScale, mediumScale, maximumScale); } public void setOnMatrixChangeListener(OnMatrixChangedListener listener) { attacher.setOnMatrixChangeListener(listener); } public void setOnPhotoTapListener(OnPhotoTapListener listener) { attacher.setOnPhotoTapListener(listener); } public void setOnOutsidePhotoTapListener(OnOutsidePhotoTapListener listener) { attacher.setOnOutsidePhotoTapListener(listener); } public void setOnViewTapListener(OnViewTapListener listener) { attacher.setOnViewTapListener(listener); } public void setOnViewDragListener(OnViewDragListener listener) { attacher.setOnViewDragListener(listener); } public void setScale(float scale, boolean animate) { attacher.setScale(scale, animate); } public void setScale(float scale, float focalX, float focalY, boolean animate) { attacher.setScale(scale, focalX, focalY, animate); } public void setZoomTransitionDuration(int milliseconds) { attacher.setZoomTransitionDuration(milliseconds); } public void setOnDoubleTapListener(GestureDetector.OnDoubleTapListener onDoubleTapListener) { attacher.setOnDoubleTapListener(onDoubleTapListener); } public void setOnScaleChangeListener(OnScaleChangedListener onScaleChangedListener) { attacher.setOnScaleChangeListener(onScaleChangedListener); } public void setOnSingleFlingListener(OnSingleFlingListener onSingleFlingListener) { attacher.setOnSingleFlingListener(onSingleFlingListener); } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/github/chrisbanes/photoview/PhotoViewAttacher.java ================================================ /* Copyright 2011, 2012 Chris Banes.

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package com.maxwai.nclientv3.github.chrisbanes.photoview; import android.content.Context; import android.graphics.Matrix; import android.graphics.Matrix.ScaleToFit; import android.graphics.RectF; import android.graphics.drawable.Drawable; import android.view.GestureDetector; import android.view.MotionEvent; import android.view.View; import android.view.View.OnLongClickListener; import android.view.ViewParent; import android.view.animation.AccelerateDecelerateInterpolator; import android.view.animation.Interpolator; import android.widget.ImageView; import android.widget.ImageView.ScaleType; import android.widget.OverScroller; import androidx.annotation.NonNull; /** * The component of {@link PhotoView} which does the work allowing for zooming, scaling, panning, etc. * It is made public in case you need to subclass something other than AppCompatImageView and still * gain the functionality that {@link PhotoView} offers */ public class PhotoViewAttacher implements View.OnTouchListener, View.OnLayoutChangeListener { private static final float DEFAULT_MAX_SCALE = 3.0f; private static final float DEFAULT_MID_SCALE = 1.75f; private static final float DEFAULT_MIN_SCALE = 1.0f; private static final int DEFAULT_ZOOM_DURATION = 200; private static final int HORIZONTAL_EDGE_NONE = -1; private static final int HORIZONTAL_EDGE_LEFT = 0; private static final int HORIZONTAL_EDGE_RIGHT = 1; private static final int HORIZONTAL_EDGE_BOTH = 2; private static final int VERTICAL_EDGE_NONE = -1; private static final int VERTICAL_EDGE_TOP = 0; private static final int VERTICAL_EDGE_BOTTOM = 1; private static final int VERTICAL_EDGE_BOTH = 2; private static final int SINGLE_TOUCH = 1; private final ImageView mImageView; // These are set so we don't keep allocating them on the heap private final Matrix mBaseMatrix = new Matrix(); private final Matrix mDrawMatrix = new Matrix(); private final Matrix mSuppMatrix = new Matrix(); private final RectF mDisplayRect = new RectF(); private final float[] mMatrixValues = new float[9]; private final Interpolator mInterpolator = new AccelerateDecelerateInterpolator(); private int mZoomDuration = DEFAULT_ZOOM_DURATION; private float mMinScale = DEFAULT_MIN_SCALE; private float mMidScale = DEFAULT_MID_SCALE; private float mMaxScale = DEFAULT_MAX_SCALE; private boolean mAllowParentInterceptOnEdge = true; private boolean mBlockParentIntercept = false; // Gesture Detectors private GestureDetector mGestureDetector; private CustomGestureDetector mScaleDragDetector; // Listeners private OnMatrixChangedListener mMatrixChangeListener; private OnPhotoTapListener mPhotoTapListener; private OnOutsidePhotoTapListener mOutsidePhotoTapListener; private OnViewTapListener mViewTapListener; private View.OnClickListener mOnClickListener; private OnLongClickListener mLongClickListener; private OnScaleChangedListener mScaleChangeListener; private OnSingleFlingListener mSingleFlingListener; private OnViewDragListener mOnViewDragListener; private FlingRunnable mCurrentFlingRunnable; private int mHorizontalScrollEdge = HORIZONTAL_EDGE_BOTH; private int mVerticalScrollEdge = VERTICAL_EDGE_BOTH; private float mBaseRotation; private boolean mZoomEnabled = true; private ScaleType mScaleType = ScaleType.FIT_CENTER; private final OnGestureListener onGestureListener = new OnGestureListener() { @Override public void onDrag(float dx, float dy) { if (mScaleDragDetector.isScaling()) { return; // Do not drag if we are already scaling } if (mOnViewDragListener != null) { mOnViewDragListener.onDrag(dx, dy); } mSuppMatrix.postTranslate(dx, dy); checkAndDisplayMatrix(); /* * Here we decide whether to let the ImageView's parent to start taking * over the touch event. * * First we check whether this function is enabled. We never want the * parent to take over if we're scaling. We then check the edge we're * on, and the direction of the scroll (i.e. if we're pulling against * the edge, aka 'overscrolling', let the parent take over). */ ViewParent parent = mImageView.getParent(); if (mAllowParentInterceptOnEdge && !mScaleDragDetector.isScaling() && !mBlockParentIntercept) { if (mHorizontalScrollEdge == HORIZONTAL_EDGE_BOTH || (mHorizontalScrollEdge == HORIZONTAL_EDGE_LEFT && dx >= 1f) || (mHorizontalScrollEdge == HORIZONTAL_EDGE_RIGHT && dx <= -1f) || (mVerticalScrollEdge == VERTICAL_EDGE_TOP && dy >= 1f) || (mVerticalScrollEdge == VERTICAL_EDGE_BOTTOM && dy <= -1f)) { if (parent != null) { parent.requestDisallowInterceptTouchEvent(false); } } } else { if (parent != null) { parent.requestDisallowInterceptTouchEvent(true); } } } @Override public void onFling(float startX, float startY, float velocityX, float velocityY) { mCurrentFlingRunnable = new FlingRunnable(mImageView.getContext()); mCurrentFlingRunnable.fling(getImageViewWidth(mImageView), getImageViewHeight(mImageView), (int) velocityX, (int) velocityY); mImageView.post(mCurrentFlingRunnable); } @Override public void onScale(float scaleFactor, float focusX, float focusY) { onScale(scaleFactor, focusX, focusY, 0, 0); } @Override public void onScale(float scaleFactor, float focusX, float focusY, float dx, float dy) { if (getScale() < mMaxScale || scaleFactor < 1f) { if (mScaleChangeListener != null) { mScaleChangeListener.onScaleChange(scaleFactor, focusX, focusY); } mSuppMatrix.postScale(scaleFactor, scaleFactor, focusX, focusY); mSuppMatrix.postTranslate(dx, dy); checkAndDisplayMatrix(); } } }; public PhotoViewAttacher(ImageView imageView) { mImageView = imageView; imageView.setOnTouchListener(this); imageView.addOnLayoutChangeListener(this); if (imageView.isInEditMode()) { return; } mBaseRotation = 0.0f; // Create Gesture Detectors... mScaleDragDetector = new CustomGestureDetector(imageView.getContext(), onGestureListener); mGestureDetector = new GestureDetector(imageView.getContext(), new GestureDetector.SimpleOnGestureListener() { // forward long click listener @Override public void onLongPress(@NonNull MotionEvent e) { if (mLongClickListener != null) { mLongClickListener.onLongClick(mImageView); } } @Override public boolean onFling(MotionEvent e1, @NonNull MotionEvent e2, float velocityX, float velocityY) { if (mSingleFlingListener != null) { if (getScale() > DEFAULT_MIN_SCALE) { return false; } if (e1.getPointerCount() > SINGLE_TOUCH || e2.getPointerCount() > SINGLE_TOUCH) { return false; } return mSingleFlingListener.onFling(e1, e2, velocityX, velocityY); } return false; } }); mGestureDetector.setOnDoubleTapListener(new GestureDetector.OnDoubleTapListener() { @Override public boolean onSingleTapConfirmed(@NonNull MotionEvent e) { if (mOnClickListener != null) { mOnClickListener.onClick(mImageView); } final RectF displayRect = getDisplayRect(); final float x = e.getX(), y = e.getY(); if (mViewTapListener != null) { mViewTapListener.onViewTap(mImageView, x, y); } if (displayRect != null) { // Check to see if the user tapped on the photo if (displayRect.contains(x, y)) { float xResult = (x - displayRect.left) / displayRect.width(); float yResult = (y - displayRect.top) / displayRect.height(); if (mPhotoTapListener != null) { mPhotoTapListener.onPhotoTap(mImageView, xResult, yResult); } return true; } else { if (mOutsidePhotoTapListener != null) { mOutsidePhotoTapListener.onOutsidePhotoTap(mImageView); } } } return false; } @Override public boolean onDoubleTap(@NonNull MotionEvent ev) { try { float scale = getScale(); float x = ev.getX(); float y = ev.getY(); if (scale < getMediumScale()) { setScale(getMediumScale(), x, y, true); } else if (scale >= getMediumScale() && scale < getMaximumScale()) { setScale(getMaximumScale(), x, y, true); } else { setScale(getMinimumScale(), x, y, true); } } catch (ArrayIndexOutOfBoundsException e) { // Can sometimes happen when getX() and getY() is called } return true; } @Override public boolean onDoubleTapEvent(@NonNull MotionEvent e) { // Wait for the confirmed onDoubleTap() instead return false; } }); } public void setOnDoubleTapListener(GestureDetector.OnDoubleTapListener newOnDoubleTapListener) { this.mGestureDetector.setOnDoubleTapListener(newOnDoubleTapListener); } public void setOnScaleChangeListener(OnScaleChangedListener onScaleChangeListener) { this.mScaleChangeListener = onScaleChangeListener; } public void setOnSingleFlingListener(OnSingleFlingListener onSingleFlingListener) { this.mSingleFlingListener = onSingleFlingListener; } @Deprecated public boolean isZoomEnabled() { return mZoomEnabled; } public RectF getDisplayRect() { checkMatrixBounds(); return getDisplayRect(getDrawMatrix()); } public boolean setDisplayMatrix(Matrix finalMatrix) { if (finalMatrix == null) { throw new IllegalArgumentException("Matrix cannot be null"); } if (mImageView.getDrawable() == null) { return false; } mSuppMatrix.set(finalMatrix); checkAndDisplayMatrix(); return true; } public void setRotationTo(float degrees) { mSuppMatrix.setRotate(degrees % 360); checkAndDisplayMatrix(); } public void setRotationBy(float degrees) { mSuppMatrix.postRotate(degrees % 360); checkAndDisplayMatrix(); } public float getMinimumScale() { return mMinScale; } public void setMinimumScale(float minimumScale) { Util.checkZoomLevels(minimumScale, mMidScale, mMaxScale); mMinScale = minimumScale; } public float getMediumScale() { return mMidScale; } public void setMediumScale(float mediumScale) { Util.checkZoomLevels(mMinScale, mediumScale, mMaxScale); mMidScale = mediumScale; } public float getMaximumScale() { return mMaxScale; } public void setMaximumScale(float maximumScale) { Util.checkZoomLevels(mMinScale, mMidScale, maximumScale); mMaxScale = maximumScale; } public float getScale() { return (float) Math.sqrt((float) Math.pow(getValue(mSuppMatrix, Matrix.MSCALE_X), 2) + (float) Math.pow (getValue(mSuppMatrix, Matrix.MSKEW_Y), 2)); } public void setScale(float scale) { setScale(scale, false); } public ScaleType getScaleType() { return mScaleType; } public void setScaleType(ScaleType scaleType) { if (Util.isSupportedScaleType(scaleType) && scaleType != mScaleType) { mScaleType = scaleType; update(); } } @Override public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) { // Update our base matrix, as the bounds have changed if (left != oldLeft || top != oldTop || right != oldRight || bottom != oldBottom) { updateBaseMatrix(mImageView.getDrawable()); } } @Override public boolean onTouch(View v, MotionEvent ev) { boolean handled = false; if (mZoomEnabled && Util.hasDrawable((ImageView) v)) { switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: ViewParent parent = v.getParent(); // First, disable the Parent from intercepting the touch // event if (parent != null) { parent.requestDisallowInterceptTouchEvent(true); } // If we're flinging, and the user presses down, cancel // fling cancelFling(); break; case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: // If the user has zoomed less than min scale, zoom back // to min scale if (getScale() < mMinScale) { RectF rect = getDisplayRect(); if (rect != null) { v.post(new AnimatedZoomRunnable(getScale(), mMinScale, rect.centerX(), rect.centerY())); handled = true; } } else if (getScale() > mMaxScale) { RectF rect = getDisplayRect(); if (rect != null) { v.post(new AnimatedZoomRunnable(getScale(), mMaxScale, rect.centerX(), rect.centerY())); handled = true; } } break; } // Try the Scale/Drag detector if (mScaleDragDetector != null) { boolean wasScaling = mScaleDragDetector.isScaling(); boolean wasDragging = mScaleDragDetector.isDragging(); handled = mScaleDragDetector.onTouchEvent(ev); boolean didntScale = !wasScaling && !mScaleDragDetector.isScaling(); boolean didntDrag = !wasDragging && !mScaleDragDetector.isDragging(); mBlockParentIntercept = didntScale && didntDrag; } // Check to see if the user double tapped if (mGestureDetector != null && mGestureDetector.onTouchEvent(ev)) { handled = true; } } return handled; } public void setAllowParentInterceptOnEdge(boolean allow) { mAllowParentInterceptOnEdge = allow; } public void setScaleLevels(float minimumScale, float mediumScale, float maximumScale) { Util.checkZoomLevels(minimumScale, mediumScale, maximumScale); mMinScale = minimumScale; mMidScale = mediumScale; mMaxScale = maximumScale; } public void setOnLongClickListener(OnLongClickListener listener) { mLongClickListener = listener; } public void setOnClickListener(View.OnClickListener listener) { mOnClickListener = listener; } public void setOnMatrixChangeListener(OnMatrixChangedListener listener) { mMatrixChangeListener = listener; } public void setOnPhotoTapListener(OnPhotoTapListener listener) { mPhotoTapListener = listener; } public void setOnOutsidePhotoTapListener(OnOutsidePhotoTapListener mOutsidePhotoTapListener) { this.mOutsidePhotoTapListener = mOutsidePhotoTapListener; } public void setOnViewTapListener(OnViewTapListener listener) { mViewTapListener = listener; } public void setOnViewDragListener(OnViewDragListener listener) { mOnViewDragListener = listener; } public void setScale(float scale, boolean animate) { setScale(scale, (mImageView.getRight()) / 2f, (mImageView.getBottom()) / 2f, animate); } public void setScale(float scale, float focalX, float focalY, boolean animate) { // Check to see if the scale is within bounds scale = Math.min(scale, mMaxScale); scale = Math.max(scale, mMinScale); if (animate) { mImageView.post(new AnimatedZoomRunnable(getScale(), scale, focalX, focalY)); } else { mSuppMatrix.setScale(scale, scale, focalX, focalY); checkAndDisplayMatrix(); } } public boolean isZoomable() { return mZoomEnabled; } public void setZoomable(boolean zoomable) { mZoomEnabled = zoomable; update(); } public void update() { if (mZoomEnabled) { // Update the base matrix using the current drawable updateBaseMatrix(mImageView.getDrawable()); } else { // Reset the Matrix... resetMatrix(); } } /** * Get the display matrix * * @param matrix target matrix to copy to */ public void getDisplayMatrix(Matrix matrix) { matrix.set(getDrawMatrix()); } /** * Get the current support matrix */ public void getSuppMatrix(Matrix matrix) { matrix.set(mSuppMatrix); } private Matrix getDrawMatrix() { mDrawMatrix.set(mBaseMatrix); mDrawMatrix.postConcat(mSuppMatrix); return mDrawMatrix; } public Matrix getImageMatrix() { return mDrawMatrix; } public void setZoomTransitionDuration(int milliseconds) { this.mZoomDuration = milliseconds; } /** * Helper method that 'unpacks' a Matrix and returns the required value * * @param matrix Matrix to unpack * @param whichValue Which value from Matrix.M* to return * @return returned value */ private float getValue(Matrix matrix, int whichValue) { matrix.getValues(mMatrixValues); return mMatrixValues[whichValue]; } /** * Resets the Matrix back to FIT_CENTER, and then displays its contents */ private void resetMatrix() { mSuppMatrix.reset(); setRotationBy(mBaseRotation); setImageViewMatrix(getDrawMatrix()); checkMatrixBounds(); } private void setImageViewMatrix(Matrix matrix) { mImageView.setImageMatrix(matrix); // Call MatrixChangedListener if needed if (mMatrixChangeListener != null) { RectF displayRect = getDisplayRect(matrix); if (displayRect != null) { mMatrixChangeListener.onMatrixChanged(displayRect); } } } /** * Helper method that simply checks the Matrix, and then displays the result */ private void checkAndDisplayMatrix() { if (checkMatrixBounds()) { setImageViewMatrix(getDrawMatrix()); } } /** * Helper method that maps the supplied Matrix to the current Drawable * * @param matrix - Matrix to map Drawable against * @return RectF - Displayed Rectangle */ private RectF getDisplayRect(Matrix matrix) { Drawable d = mImageView.getDrawable(); if (d != null) { mDisplayRect.set(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight()); matrix.mapRect(mDisplayRect); return mDisplayRect; } return null; } /** * Calculate Matrix for FIT_CENTER * * @param drawable - Drawable being displayed */ private void updateBaseMatrix(Drawable drawable) { if (drawable == null) { return; } final float viewWidth = getImageViewWidth(mImageView); final float viewHeight = getImageViewHeight(mImageView); final int drawableWidth = drawable.getIntrinsicWidth(); final int drawableHeight = drawable.getIntrinsicHeight(); mBaseMatrix.reset(); final float widthScale = viewWidth / drawableWidth; final float heightScale = viewHeight / drawableHeight; if (mScaleType == ScaleType.CENTER) { mBaseMatrix.postTranslate((viewWidth - drawableWidth) / 2F, (viewHeight - drawableHeight) / 2F); } else if (mScaleType == ScaleType.CENTER_CROP) { float scale = Math.max(widthScale, heightScale); mBaseMatrix.postScale(scale, scale); mBaseMatrix.postTranslate((viewWidth - drawableWidth * scale) / 2F, (viewHeight - drawableHeight * scale) / 2F); } else if (mScaleType == ScaleType.CENTER_INSIDE) { float scale = Math.min(1.0f, Math.min(widthScale, heightScale)); mBaseMatrix.postScale(scale, scale); mBaseMatrix.postTranslate((viewWidth - drawableWidth * scale) / 2F, (viewHeight - drawableHeight * scale) / 2F); } else { RectF mTempSrc = new RectF(0, 0, drawableWidth, drawableHeight); RectF mTempDst = new RectF(0, 0, viewWidth, viewHeight); if ((int) mBaseRotation % 180 != 0) { //noinspection SuspiciousNameCombination mTempSrc = new RectF(0, 0, drawableHeight, drawableWidth); } switch (mScaleType) { case FIT_CENTER: mBaseMatrix.setRectToRect(mTempSrc, mTempDst, ScaleToFit.CENTER); break; case FIT_START: mBaseMatrix.setRectToRect(mTempSrc, mTempDst, ScaleToFit.START); break; case FIT_END: mBaseMatrix.setRectToRect(mTempSrc, mTempDst, ScaleToFit.END); break; case FIT_XY: mBaseMatrix.setRectToRect(mTempSrc, mTempDst, ScaleToFit.FILL); break; default: break; } } resetMatrix(); } private boolean checkMatrixBounds() { final RectF rect = getDisplayRect(getDrawMatrix()); if (rect == null) { return false; } final float height = rect.height(), width = rect.width(); float deltaX = 0, deltaY = 0; final int viewHeight = getImageViewHeight(mImageView); if (height <= viewHeight) { switch (mScaleType) { case FIT_START: deltaY = -rect.top; break; case FIT_END: deltaY = viewHeight - height - rect.top; break; default: deltaY = (viewHeight - height) / 2 - rect.top; break; } mVerticalScrollEdge = VERTICAL_EDGE_BOTH; } else if (rect.top > 0) { mVerticalScrollEdge = VERTICAL_EDGE_TOP; deltaY = -rect.top; } else if (rect.bottom < viewHeight) { mVerticalScrollEdge = VERTICAL_EDGE_BOTTOM; deltaY = viewHeight - rect.bottom; } else { mVerticalScrollEdge = VERTICAL_EDGE_NONE; } final int viewWidth = getImageViewWidth(mImageView); if (width <= viewWidth) { switch (mScaleType) { case FIT_START: deltaX = -rect.left; break; case FIT_END: deltaX = viewWidth - width - rect.left; break; default: deltaX = (viewWidth - width) / 2 - rect.left; break; } mHorizontalScrollEdge = HORIZONTAL_EDGE_BOTH; } else if (rect.left > 0) { mHorizontalScrollEdge = HORIZONTAL_EDGE_LEFT; deltaX = -rect.left; } else if (rect.right < viewWidth) { deltaX = viewWidth - rect.right; mHorizontalScrollEdge = HORIZONTAL_EDGE_RIGHT; } else { mHorizontalScrollEdge = HORIZONTAL_EDGE_NONE; } // Finally actually translate the matrix mSuppMatrix.postTranslate(deltaX, deltaY); return true; } private int getImageViewWidth(ImageView imageView) { return imageView.getWidth() - imageView.getPaddingLeft() - imageView.getPaddingRight(); } private int getImageViewHeight(ImageView imageView) { return imageView.getHeight() - imageView.getPaddingTop() - imageView.getPaddingBottom(); } private void cancelFling() { if (mCurrentFlingRunnable != null) { mCurrentFlingRunnable.cancelFling(); mCurrentFlingRunnable = null; } } private class AnimatedZoomRunnable implements Runnable { private final float mFocalX, mFocalY; private final long mStartTime; private final float mZoomStart, mZoomEnd; public AnimatedZoomRunnable(final float currentZoom, final float targetZoom, final float focalX, final float focalY) { mFocalX = focalX; mFocalY = focalY; mStartTime = System.currentTimeMillis(); mZoomStart = currentZoom; mZoomEnd = targetZoom; } @Override public void run() { float t = interpolate(); float scale = mZoomStart + t * (mZoomEnd - mZoomStart); float deltaScale = scale / getScale(); onGestureListener.onScale(deltaScale, mFocalX, mFocalY); // We haven't hit our target scale yet, so post ourselves again if (t < 1f) { Compat.postOnAnimation(mImageView, this); } } private float interpolate() { float t = 1f * (System.currentTimeMillis() - mStartTime) / mZoomDuration; t = Math.min(1f, t); t = mInterpolator.getInterpolation(t); return t; } } private class FlingRunnable implements Runnable { private final OverScroller mScroller; private int mCurrentX, mCurrentY; public FlingRunnable(Context context) { mScroller = new OverScroller(context); } public void cancelFling() { mScroller.forceFinished(true); } public void fling(int viewWidth, int viewHeight, int velocityX, int velocityY) { final RectF rect = getDisplayRect(); if (rect == null) { return; } final int startX = Math.round(-rect.left); final int minX, maxX, minY, maxY; if (viewWidth < rect.width()) { minX = 0; maxX = Math.round(rect.width() - viewWidth); } else { minX = maxX = startX; } final int startY = Math.round(-rect.top); if (viewHeight < rect.height()) { minY = 0; maxY = Math.round(rect.height() - viewHeight); } else { minY = maxY = startY; } mCurrentX = startX; mCurrentY = startY; // If we actually can move, fling the scroller if (startX != maxX || startY != maxY) { mScroller.fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY, 0, 0); } } @Override public void run() { if (mScroller.isFinished()) { return; // remaining post that should not be handled } if (mScroller.computeScrollOffset()) { final int newX = mScroller.getCurrX(); final int newY = mScroller.getCurrY(); mSuppMatrix.postTranslate(mCurrentX - newX, mCurrentY - newY); checkAndDisplayMatrix(); mCurrentX = newX; mCurrentY = newY; // Post On animation Compat.postOnAnimation(mImageView, this); } } } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/github/chrisbanes/photoview/Util.java ================================================ package com.maxwai.nclientv3.github.chrisbanes.photoview; import android.view.MotionEvent; import android.widget.ImageView; class Util { static void checkZoomLevels(float minZoom, float midZoom, float maxZoom) { if (minZoom >= midZoom) { throw new IllegalArgumentException( "Minimum zoom has to be less than Medium zoom. Call setMinimumZoom() with a more appropriate value"); } else if (midZoom >= maxZoom) { throw new IllegalArgumentException( "Medium zoom has to be less than Maximum zoom. Call setMaximumZoom() with a more appropriate value"); } } static boolean hasDrawable(ImageView imageView) { return imageView.getDrawable() != null; } static boolean isSupportedScaleType(final ImageView.ScaleType scaleType) { if (scaleType == null) { return false; } if (scaleType == ImageView.ScaleType.MATRIX) { throw new IllegalStateException("Matrix scale type is not supported"); } return true; } static int getPointerIndex(int action) { return (action & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT; } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/loginapi/LoadTags.java ================================================ package com.maxwai.nclientv3.loginapi; import android.content.Context; import android.util.JsonReader; import android.util.JsonToken; import androidx.annotation.NonNull; import com.maxwai.nclientv3.api.components.Tag; import com.maxwai.nclientv3.api.enums.TagType; import com.maxwai.nclientv3.settings.Global; import com.maxwai.nclientv3.settings.Login; import com.maxwai.nclientv3.utility.LogUtility; import com.maxwai.nclientv3.utility.Utility; import java.io.IOException; import okhttp3.Request; import okhttp3.Response; public class LoadTags extends Thread { @NonNull private final Context context; public LoadTags(@NonNull Context context) { this.context = context; } private void readTags(JsonReader jr) throws IOException { jr.beginObject(); while (jr.peek() != JsonToken.END_OBJECT) { if (jr.nextName().equals("tags")) { jr.beginArray(); while (jr.peek() != JsonToken.END_ARRAY) { Tag tt = new Tag(jr); if (tt.getType() != TagType.LANGUAGE && tt.getType() != TagType.CATEGORY) { Login.addOnlineTag(tt); } } jr.endArray(); } else { jr.skipValue(); } } jr.endObject(); } @Override public void run() { super.run(); if (Login.getUser() == null) return; String url = Utility.getApiBaseUrl() + "blacklist"; LogUtility.d(url); try (Response response = Global.getClient(context).newCall(new Request.Builder().url(url).build()).execute()) { JsonReader json = new JsonReader(response.body().charStream()); Login.clearOnlineTags(); readTags(json); } catch (IOException | StringIndexOutOfBoundsException e) { LogUtility.e("Error getting blacklisted Tags from website", e); } } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/loginapi/User.java ================================================ package com.maxwai.nclientv3.loginapi; import android.content.Context; import android.util.JsonReader; import android.util.JsonToken; import androidx.annotation.NonNull; import com.maxwai.nclientv3.settings.Global; import com.maxwai.nclientv3.settings.Login; import com.maxwai.nclientv3.utility.Utility; import java.io.IOException; import okhttp3.Call; import okhttp3.Callback; import okhttp3.Request; import okhttp3.Response; public class User { private final String username; private final int id; private User(JsonReader jr) throws IOException { int id = -1; String username = null; jr.beginObject(); while (jr.peek() != JsonToken.END_OBJECT) { switch (jr.nextName()) { case "id": id = jr.nextInt(); break; case "username": username = jr.nextString(); break; default: jr.skipValue(); break; } } jr.endObject(); if (id == -1 || username == null) { throw new RuntimeException("No user information found"); } this.username = username; this.id = id; } public static void createUser(@NonNull Context context, final CreateUser createUser) { Global.getClient(context) .newCall(new Request.Builder().url(Utility.getApiBaseUrl() + "user").build()) .enqueue(new Callback() { @Override public void onFailure(@NonNull Call call, @NonNull IOException e) { } @Override public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException { JsonReader json = new JsonReader(response.body().charStream()); User user = new User(json); Login.updateUser(user); if (createUser != null) createUser.onCreateUser(Login.getUser()); } }); } @NonNull @Override public String toString() { return username + '(' + id + ')'; } public String getUsername() { return username; } public int getId() { return id; } public interface CreateUser { void onCreateUser(User user); } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/settings/ApiAuthInterceptor.java ================================================ package com.maxwai.nclientv3.settings; import android.content.Context; import android.webkit.CookieManager; import androidx.annotation.NonNull; import com.maxwai.nclientv3.R; import com.maxwai.nclientv3.components.CookieInterceptor; import com.maxwai.nclientv3.utility.LogUtility; import java.io.IOException; import java.net.HttpURLConnection; import okhttp3.Interceptor; import okhttp3.Request; import okhttp3.Response; public class ApiAuthInterceptor implements Interceptor { private final boolean logRequests; @NonNull private final Context context; public ApiAuthInterceptor(@NonNull Context context, boolean logRequests) { this.context = context.getApplicationContext(); this.logRequests = logRequests; } @NonNull @Override public Response intercept(@NonNull Chain chain) throws IOException { Request request = chain.request(); if (logRequests) LogUtility.d("Requested url: " + request.url()); if (request.header("Authorization") != null || !request.url().encodedPath().startsWith("/api/v2/")) { return chain.proceed(request); } Request.Builder r = request.newBuilder(); r.addHeader("User-Agent", "NClient/" + Global.getVersionName(context) + " (https://github.com/maxwai/NClientV3)"); if (!AuthStore.hasValidApiKey(context)) return chain.proceed(r.build()); String authorization = AuthStore.getAuthorizationHeader(context); if (authorization == null) return chain.proceed(r.build()); Request authenticated =r.header("Authorization", authorization) .build(); Response response = chain.proceed(authenticated); if (response.code() == 401 || response.code() == 403) { AuthStore.setApiKeyValidation(context, false); } else if (response.isSuccessful()) { AuthStore.setApiKeyValidation(context, true); } return response; } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/settings/AuthCredentials.java ================================================ package com.maxwai.nclientv3.settings; import androidx.annotation.NonNull; public final class AuthCredentials { public enum Type { API_KEY } @NonNull private final Type type; @NonNull private final String secret; public AuthCredentials(@NonNull Type type, @NonNull String secret) { this.type = type; this.secret = secret; } @NonNull public Type getType() { return type; } @NonNull public String getSecret() { return secret; } @NonNull public String toAuthorizationHeader() { return "Key " + secret; } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/settings/AuthStore.java ================================================ package com.maxwai.nclientv3.settings; import android.content.Context; import android.content.SharedPreferences; import androidx.annotation.NonNull; import androidx.annotation.Nullable; public final class AuthStore { private static final String AUTH_PREFERENCES = "Auth"; private static final String KEY_TYPE = "type"; private static final String KEY_SECRET = "secret"; private static final String KEY_VALID = "valid"; private AuthStore() { } @NonNull private static SharedPreferences getPreferences(@NonNull Context context) { return context.getSharedPreferences(AUTH_PREFERENCES, Context.MODE_PRIVATE); } public static void saveApiKey(@NonNull Context context, @NonNull String apiKey, boolean valid) { String normalized = apiKey.trim(); getPreferences(context).edit() .putString(KEY_TYPE, AuthCredentials.Type.API_KEY.name()) .putString(KEY_SECRET, normalized) .putBoolean(KEY_VALID, valid) .apply(); } public static void clear(@NonNull Context context) { getPreferences(context).edit().clear().apply(); } @Nullable public static AuthCredentials getCredentials(@NonNull Context context) { SharedPreferences preferences = getPreferences(context); String typeName = preferences.getString(KEY_TYPE, null); String secret = preferences.getString(KEY_SECRET, null); if (typeName == null || secret == null || secret.trim().isEmpty()) return null; try { return new AuthCredentials(AuthCredentials.Type.valueOf(typeName), secret.trim()); } catch (IllegalArgumentException ignore) { clear(context); return null; } } public static boolean hasCredentials(@NonNull Context context) { return getCredentials(context) != null; } public static boolean hasApiKey(@NonNull Context context) { AuthCredentials credentials = getCredentials(context); return credentials != null && credentials.getType() == AuthCredentials.Type.API_KEY; } public static boolean hasValidApiKey(@NonNull Context context) { AuthCredentials credentials = getCredentials(context); if (credentials == null || credentials.getType() != AuthCredentials.Type.API_KEY) return false; SharedPreferences preferences = getPreferences(context); if (!preferences.contains(KEY_VALID)) return true; return preferences.getBoolean(KEY_VALID, false); } public static void setApiKeyValidation(@NonNull Context context, boolean valid) { if (!hasApiKey(context)) return; getPreferences(context).edit().putBoolean(KEY_VALID, valid).apply(); } @Nullable public static String getApiKey(@NonNull Context context) { AuthCredentials credentials = getCredentials(context); if (credentials == null || credentials.getType() != AuthCredentials.Type.API_KEY) return null; return credentials.getSecret(); } @Nullable public static String getAuthorizationHeader(@NonNull Context context) { AuthCredentials credentials = getCredentials(context); return credentials == null ? null : credentials.toAuthorizationHeader(); } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/settings/Database.java ================================================ package com.maxwai.nclientv3.settings; import android.database.sqlite.SQLiteDatabase; import androidx.annotation.Nullable; import com.maxwai.nclientv3.async.database.Queries; import com.maxwai.nclientv3.utility.LogUtility; public class Database { private static SQLiteDatabase database; @Nullable public static SQLiteDatabase getDatabase() { return database; } public static void setDatabase(SQLiteDatabase database) { Database.database = database; LogUtility.d("SETTED database" + database); setDBForTables(database); Queries.StatusTable.initStatuses(); } private static void setDBForTables(SQLiteDatabase database) { Queries.setDb(database); } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/settings/DefaultDialogs.java ================================================ package com.maxwai.nclientv3.settings; import android.content.Context; import android.text.Editable; import android.text.InputFilter; import android.text.TextWatcher; import android.view.View; import android.widget.EditText; import android.widget.SeekBar; import android.widget.TextView; import androidx.annotation.DrawableRes; import androidx.annotation.StringRes; import com.maxwai.nclientv3.R; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import java.util.Locale; public class DefaultDialogs { public static void pageChangerDialog(final Builder builder) { MaterialAlertDialogBuilder build = new MaterialAlertDialogBuilder(builder.context); if (builder.title != 0) build.setTitle(builder.context.getString(builder.title)); if (builder.drawable != 0) build.setIcon(builder.drawable); View v = View.inflate(builder.context, R.layout.page_changer, null); build.setView(v); final SeekBar seekBar = v.findViewById(R.id.seekBar); if (Global.useRtl()) seekBar.setRotationY(180); final TextView totalPage = v.findViewById(R.id.page); final EditText actualPage = v.findViewById(R.id.edit_page); v.findViewById(R.id.prev).setOnClickListener(v12 -> { seekBar.setProgress(seekBar.getProgress() - 1); actualPage.setText(String.format(Locale.US, "%d", seekBar.getProgress() + builder.min)); }); v.findViewById(R.id.next).setOnClickListener(v1 -> { seekBar.setProgress(seekBar.getProgress() + 1); actualPage.setText(String.format(Locale.US, "%d", seekBar.getProgress() + builder.min)); }); seekBar.setMax(builder.max - builder.min); seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { if (fromUser) actualPage.setText(String.format(Locale.US, "%d", progress + builder.min)); } @Override public void onStartTrackingTouch(SeekBar seekBar) { } @Override public void onStopTrackingTouch(SeekBar seekBar) { } }); actualPage.setText(String.format(Locale.US, "%d", builder.actual)); seekBar.setProgress(builder.actual - builder.min); totalPage.setText(String.format(Locale.US, "%d", builder.max)); InputFilter[] filterArray = new InputFilter[1]; filterArray[0] = new InputFilter.LengthFilter(Integer.toString(builder.max).length()); actualPage.setFilters(filterArray); actualPage.addTextChangedListener(new TextWatcher() { @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { } @Override public void onTextChanged(CharSequence s, int start, int before, int count) { } @Override public void afterTextChanged(Editable s) { int x; try { x = Integer.parseInt(s.toString()); } catch (NumberFormatException e) { x = -1; } if (x < builder.min) seekBar.setProgress(0); else seekBar.setProgress(x - builder.min); } }); if (builder.dialogs != null) build .setPositiveButton(builder.context.getString(builder.yesbtn), (dialog, id) -> builder.dialogs.positive(seekBar.getProgress() + builder.min)) .setNegativeButton(builder.context.getString(builder.nobtn), (dialog, which) -> builder.dialogs.negative()); if (builder.maybebtn != 0) build.setNeutralButton(builder.context.getString(builder.maybebtn), (dialog, which) -> builder.dialogs.neutral()); build.setCancelable(true); build.show(); } public interface DialogResults { void positive(int actual); void negative(); void neutral(); } public static class CustomDialogResults implements DialogResults { @Override public void positive(int actual) { } @Override public void negative() { } @Override public void neutral() { } } @SuppressWarnings("UnusedReturnValue") public static class Builder { private final Context context; DialogResults dialogs; private @StringRes int title, yesbtn, nobtn, maybebtn; private @DrawableRes int drawable; private int max, actual, min; public Builder(Context context) { this.context = context; title = drawable = 0; yesbtn = R.string.ok; nobtn = R.string.cancel; maybebtn = 0; max = actual = 1; min = 0; dialogs = null; } public Builder setMin(int min) { this.min = min; return this; } public Builder setTitle(int title) { this.title = title; return this; } public Builder setYesbtn(int yesbtn) { this.yesbtn = yesbtn; return this; } public Builder setNobtn(int nobtn) { this.nobtn = nobtn; return this; } public Builder setDrawable(int drawable) { this.drawable = drawable; return this; } public Builder setMax(int max) { this.max = max; return this; } public Builder setMaybebtn(int maybebtn) { this.maybebtn = maybebtn; return this; } public Builder setActual(int actual) { this.actual = actual; return this; } public Builder setDialogs(DialogResults dialogs) { this.dialogs = dialogs; return this; } } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/settings/Favorites.java ================================================ package com.maxwai.nclientv3.settings; import com.maxwai.nclientv3.api.components.Gallery; import com.maxwai.nclientv3.api.components.GenericGallery; import com.maxwai.nclientv3.async.database.Queries; public class Favorites { public static void addFavorite(Gallery gallery) { Queries.FavoriteTable.addFavorite(gallery); } public static void removeFavorite(GenericGallery gallery) { Queries.FavoriteTable.removeFavorite(gallery.getId()); } public static boolean isFavorite(GenericGallery gallery) { if (gallery == null || !gallery.isValid()) return false; return Queries.FavoriteTable.isFavorite(gallery.getId()); } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/settings/Global.java ================================================ package com.maxwai.nclientv3.settings; import android.Manifest; import android.app.Activity; import android.app.NotificationChannel; import android.app.NotificationManager; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.res.Resources; import android.graphics.Point; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Build; import android.os.Environment; import android.util.DisplayMetrics; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; import androidx.core.content.ContextCompat; import androidx.core.content.res.ResourcesCompat; import androidx.core.graphics.drawable.DrawableCompat; import androidx.recyclerview.widget.RecyclerView; import com.maxwai.nclientv3.CopyToClipboardActivity; import com.maxwai.nclientv3.R; import com.maxwai.nclientv3.api.components.GenericGallery; import com.maxwai.nclientv3.api.enums.Language; import com.maxwai.nclientv3.api.enums.SortType; import com.maxwai.nclientv3.api.enums.TitleType; import com.maxwai.nclientv3.api.local.LocalSortType; import com.maxwai.nclientv3.components.CustomCookieJar; import com.maxwai.nclientv3.utility.LogUtility; import com.maxwai.nclientv3.utility.Utility; import com.maxwai.nclientv3.utility.network.NetworkUtil; import com.franmontiel.persistentcookiejar.cache.SetCookieCache; import com.franmontiel.persistentcookiejar.persistence.SharedPrefsCookiePersistor; import java.io.File; import java.io.IOException; import java.io.RandomAccessFile; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Objects; import me.zhanghai.android.fastscroll.FastScrollerBuilder; import okhttp3.Cookie; import okhttp3.OkHttpClient; public class Global { public static final String CHANNEL_ID1 = "download_gallery", CHANNEL_ID2 = "create_pdf", CHANNEL_ID3 = "create_zip"; private static final String MAINFOLDER_NAME = "NClientV3"; private static final String DOWNLOADFOLDER_NAME = "Download"; private static final String SCREENFOLDER_NAME = "Screen"; private static final String PDFFOLDER_NAME = "PDF"; private static final String UPDATEFOLDER_NAME = "Update"; private static final String ZIPFOLDER_NAME = "ZIP"; private static final String BACKUPFOLDER_NAME = "Backup"; private static final DisplayMetrics lastDisplay = new DisplayMetrics(); public static OkHttpClient client = null; public static File MAINFOLDER; public static File DOWNLOADFOLDER; public static File SCREENFOLDER; public static File PDFFOLDER; public static File UPDATEFOLDER; public static File ZIPFOLDER; public static File BACKUPFOLDER; private static Language onlyLanguage; private static TitleType titleType; private static SortType sortType; private static LocalSortType localSortType; private static boolean buttonChangePage, hideMultitask, enableBeta, volumeOverride, zoomOneColumn, keepHistory, lockScreen, onlyTag, showTitles, removeAvoidedGalleries, useRtl; private static DataUsageType usageMobile, usageWifi; private static String lastVersion, mirror; private static int maxHistory, columnCount, maxId, galleryWidth = -1, galleryHeight = -1; private static int colPortStat, colLandStat, colPortHist, colLandHist, colPortMain, colLandMain, colPortDownload, colLandDownload, colLandFavorite, colPortFavorite; private static boolean infiniteScrollMain, infiniteScrollFavorite, exactTagMatch; private static int defaultZoom, offscreenLimit; private static Point screenSize; public static long recursiveSize(File path) { if (path.isFile()) return path.length(); long size = 0; File[] files = path.listFiles(); if (files == null) return size; for (File f : files) size += f.isFile() ? f.length() : recursiveSize(f); return size; } public static boolean isExactTagMatch() { return exactTagMatch; } public static int getFavoriteLimit(Context context) { return context.getSharedPreferences("Settings", 0).getInt(context.getString(R.string.preference_key_favorite_limit), 10); } public static String getLastVersion(Context context) { if (context != null) lastVersion = context.getSharedPreferences("Settings", 0).getString("last_version", "0.0.0"); return lastVersion; } public static boolean isEnableBeta() { return enableBeta; } public static void setEnableBeta(boolean enableBeta) { Global.enableBeta = enableBeta; } public static void setLastVersion(Context context) { lastVersion = getVersionName(context); context.getSharedPreferences("Settings", 0).edit().putString("last_version", lastVersion).apply(); } public static int getColLandHistory() { return colLandHist; } public static int getColPortHistory() { return colPortHist; } public static int getColLandStatus() { return colLandStat; } public static int getColPortStatus() { return colPortStat; } public static boolean isDestroyed(Activity activity) { return activity.isDestroyed(); } @Nullable public static String getDefaultFileParent(Context context) { File f; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { f = context.getExternalFilesDir(null); } else { f = Environment.getExternalStorageDirectory(); } return f == null ? null : f.getAbsolutePath(); } private static void initFilesTree(@NonNull Context context) { List files = getUsableFolders(context); String path = context.getSharedPreferences("Settings", Context.MODE_PRIVATE).getString(context.getString(R.string.preference_key_save_path), Objects.requireNonNull(getDefaultFileParent(context))); File ROOTFOLDER = new File(path); //in case the permission is removed if (!files.contains(ROOTFOLDER) && !isExternalStorageManager()) ROOTFOLDER = new File(Objects.requireNonNull(getDefaultFileParent(context))); MAINFOLDER = new File(ROOTFOLDER, MAINFOLDER_NAME); LogUtility.d(MAINFOLDER); DOWNLOADFOLDER = new File(MAINFOLDER, DOWNLOADFOLDER_NAME); SCREENFOLDER = new File(MAINFOLDER, SCREENFOLDER_NAME); PDFFOLDER = new File(MAINFOLDER, PDFFOLDER_NAME); UPDATEFOLDER = new File(MAINFOLDER, UPDATEFOLDER_NAME); ZIPFOLDER = new File(MAINFOLDER, ZIPFOLDER_NAME); BACKUPFOLDER = new File(MAINFOLDER, BACKUPFOLDER_NAME); } @Nullable public static OkHttpClient getClient() { return client; } @NonNull public static OkHttpClient getClient(@NonNull Context context) { if (client == null) initHttpClient(context); return client; } public static int getGalleryWidth() { return galleryWidth; } public static void initScreenSize(AppCompatActivity activity) { if (screenSize == null) { screenSize = new Point(); activity.getWindowManager().getDefaultDisplay().getSize(screenSize); } } private static void initGallerySize() { galleryHeight = screenSize.y / 2; galleryWidth = (galleryHeight * 3) / 4;//the ratio is 3:4 } public static int getGalleryHeight() { return galleryHeight; } public static int getMaxHistory() { return maxHistory; } public static boolean isInfiniteScrollMain() { return infiniteScrollMain; } public static boolean isInfiniteScrollFavorite() { return infiniteScrollFavorite; } private static void initTitleType(@NonNull Context context) { String s = context.getSharedPreferences("Settings", 0).getString(context.getString(R.string.preference_key_title_type), "pretty"); switch (s) { case "pretty": titleType = TitleType.PRETTY; break; case "english": titleType = TitleType.ENGLISH; break; case "japanese": titleType = TitleType.JAPANESE; break; } } public static int getDeviceWidth(@Nullable Activity activity) { getDeviceMetrics(activity); return lastDisplay.widthPixels; } public static int getDeviceHeight(@Nullable Activity activity) { getDeviceMetrics(activity); return lastDisplay.heightPixels; } private static void getDeviceMetrics(Activity activity) { if (activity != null) activity.getWindowManager().getDefaultDisplay().getMetrics(lastDisplay); } public static void initFromShared(@NonNull Context context) { SharedPreferences shared = context.getSharedPreferences("Settings", 0); shared.edit().remove("local_sort").apply(); localSortType = new LocalSortType(shared.getInt(context.getString(R.string.key_local_sort), 0)); useRtl = shared.getBoolean(context.getString(R.string.preference_key_use_rtl), false); mirror = shared.getString(context.getString(R.string.preference_key_site_mirror), Utility.ORIGINAL_URL); keepHistory = shared.getBoolean(context.getString(R.string.preference_key_keep_history), true); removeAvoidedGalleries = shared.getBoolean(context.getString(R.string.preference_key_remove_ignored), true); onlyTag = shared.getBoolean(context.getString(R.string.key_ignore_tags), true); volumeOverride = shared.getBoolean(context.getString(R.string.preference_key_override_volume), true); enableBeta = shared.getBoolean(context.getString(R.string.preference_key_enable_beta), true); columnCount = shared.getInt(context.getString(R.string.key_column_count), 2); showTitles = shared.getBoolean(context.getString(R.string.preference_key_show_titles), true); exactTagMatch = shared.getBoolean(context.getString(R.string.preference_key_exact_title_match), false); buttonChangePage = shared.getBoolean(context.getString(R.string.preference_key_change_page_buttons), true); lockScreen = shared.getBoolean(context.getString(R.string.preference_key_disable_lock), false); hideMultitask = shared.getBoolean(context.getString(R.string.preference_key_hide_multitasking), true); infiniteScrollFavorite = shared.getBoolean(context.getString(R.string.key_infinite_scroll_favo), false); infiniteScrollMain = shared.getBoolean(context.getString(R.string.key_infinite_scroll_main), false); maxId = shared.getInt(context.getString(R.string.key_max_id), 300000); offscreenLimit = Math.max(1, shared.getInt(context.getString(R.string.preference_key_offscreen_limit), 5)); maxHistory = shared.getInt(context.getString(R.string.preference_key_max_history_size), 2); defaultZoom = shared.getInt(context.getString(R.string.preference_key_default_zoom), 100); colPortMain = shared.getInt(context.getString(R.string.key_column_port_main), 2); colLandMain = shared.getInt(context.getString(R.string.key_column_land_main), 4); colPortDownload = shared.getInt(context.getString(R.string.key_column_port_down), 2); colLandDownload = shared.getInt(context.getString(R.string.key_column_land_down), 4); colPortFavorite = shared.getInt(context.getString(R.string.key_column_port_favo), 2); colLandFavorite = shared.getInt(context.getString(R.string.key_column_land_favo), 4); colPortHist = shared.getInt(context.getString(R.string.key_column_port_hist), 2); colLandHist = shared.getInt(context.getString(R.string.key_column_land_hist), 4); colPortStat = shared.getInt(context.getString(R.string.key_column_port_stat), 2); colLandStat = shared.getInt(context.getString(R.string.key_column_land_stat), 4); zoomOneColumn = shared.getBoolean(context.getString(R.string.preference_key_zoom_one_column), false); int x = Math.max(0, shared.getInt(context.getString(R.string.key_only_language), Language.ALL.ordinal())); sortType = SortType.values()[shared.getInt(context.getString(R.string.key_by_popular), SortType.RECENT_ALL_TIME.ordinal())]; usageMobile = DataUsageType.values()[shared.getInt(context.getString(R.string.key_mobile_usage), DataUsageType.FULL.ordinal())]; usageWifi = DataUsageType.values()[shared.getInt(context.getString(R.string.key_wifi_usage), DataUsageType.FULL.ordinal())]; if (Language.values()[x] == Language.UNKNOWN) { updateOnlyLanguage(context, Language.ALL); x = Language.ALL.ordinal(); } onlyLanguage = Language.values()[x]; Login.initLogin(context); initHttpClient(context); initTitleType(context); loadNotificationChannel(context); NotificationSettings.initializeNotificationManager(context); Global.initStorage(context); } public static boolean isButtonChangePage() { return buttonChangePage; } public static boolean hideMultitask() { return hideMultitask; } public static LocalSortType getLocalSortType() { return localSortType; } public static void setLocalSortType(Context context, LocalSortType localSortType) { context.getSharedPreferences("Settings", 0).edit().putInt(context.getString(R.string.key_local_sort), localSortType.hashCode()).apply(); Global.localSortType = localSortType; LogUtility.d("Assigning: " + localSortType); } public static String getMirror() { return mirror; } public static DataUsageType getDownloadPolicy() { switch (NetworkUtil.getType()) { case WIFI: return usageWifi; case CELLULAR: return usageMobile; } return usageWifi; } public static boolean volumeOverride() { return volumeOverride; } public static boolean isZoomOneColumn() { return zoomOneColumn; } public static void reloadHttpClient(@NonNull Context context) { SharedPreferences preferences = context.getSharedPreferences("Login", 0); OkHttpClient.Builder builder = new OkHttpClient.Builder() .cookieJar( new CustomCookieJar( new SetCookieCache(), new SharedPrefsCookiePersistor(preferences) ) ); builder.addInterceptor(new ApiAuthInterceptor(context.getApplicationContext(), true)); client = builder.build(); client.dispatcher().setMaxRequests(25); client.dispatcher().setMaxRequestsPerHost(25); for (Cookie cookie : client.cookieJar().loadForRequest(Login.BASE_HTTP_URL)) { LogUtility.d("Cookie: " + cookie); } Login.isLogged(context); } private static void initHttpClient(@NonNull Context context) { if (client != null) return; reloadHttpClient(context); } public static int getOffscreenLimit() { return offscreenLimit; } public static boolean shouldCheckForUpdates(Context context) { return context.getSharedPreferences("Settings", 0).getBoolean(context.getString(R.string.preference_key_check_update), true); } public static Drawable getLogo(Resources resources) { return ResourcesCompat.getDrawable(resources, R.drawable.ic_logo, null); } public static float getDefaultZoom() { return ((float) defaultZoom) / 100f; } public static TitleType getTitleType() { return titleType; } public static boolean removeAvoidedGalleries() { return removeAvoidedGalleries; } @NonNull public static Language getOnlyLanguage() { return onlyLanguage; } public static boolean isOnlyTag() { return onlyTag; } public static boolean isLockScreen() { return lockScreen; } public static int getColLandDownload() { return colLandDownload; } public static int getColPortMain() { return colPortMain; } public static int getColLandMain() { return colLandMain; } public static int getColPortDownload() { return colPortDownload; } public static int getColLandFavorite() { return colLandFavorite; } public static int getColPortFavorite() { return colPortFavorite; } public static boolean isKeepHistory() { return keepHistory; } public static boolean useRtl() { return useRtl; } public static boolean showTitles() { return showTitles; } public static SortType getSortType() { return sortType; } public static int getColumnCount() { return columnCount; } public static int getMaxId() { return maxId; } public static void initStorage(Context context) { if (!Global.hasStoragePermission(context)) return; Global.initFilesTree(context); boolean[] bools = new boolean[]{ Global.MAINFOLDER.mkdirs(), Global.DOWNLOADFOLDER.mkdir(), Global.PDFFOLDER.mkdir(), Global.UPDATEFOLDER.mkdir(), Global.SCREENFOLDER.mkdir(), Global.ZIPFOLDER.mkdir(), Global.BACKUPFOLDER.mkdir(), }; LogUtility.d( "0:" + context.getFilesDir() + '\n' + "1:" + Global.MAINFOLDER + bools[0] + '\n' + "2:" + Global.DOWNLOADFOLDER + bools[1] + '\n' + "3:" + Global.PDFFOLDER + bools[2] + '\n' + "4:" + Global.UPDATEFOLDER + bools[3] + '\n' + "5:" + Global.SCREENFOLDER + bools[4] + '\n' + "5:" + Global.ZIPFOLDER + bools[5] + '\n' + "6:" + Global.BACKUPFOLDER + bools[6] + '\n' ); try { //noinspection ResultOfMethodCallIgnored new File(Global.MAINFOLDER, ".nomedia").createNewFile(); } catch (IOException e) { LogUtility.e("Couldn't create nomedia file", e); } } public static void updateOnlyLanguage(@NonNull Context context, @NonNull Language type) { context.getSharedPreferences("Settings", 0).edit().putInt(context.getString((R.string.key_only_language)), type.ordinal()).apply(); onlyLanguage = type; } public static void updateSortType(@NonNull Context context, @NonNull SortType sortType) { context.getSharedPreferences("Settings", 0).edit().putInt(context.getString((R.string.key_by_popular)), sortType.ordinal()).apply(); Global.sortType = sortType; } public static void updateColumnCount(@NonNull Context context, int count) { context.getSharedPreferences("Settings", 0).edit().putInt(context.getString((R.string.key_column_count)), count).apply(); columnCount = count; } public static void updateMaxId(@NonNull Context context, int id) { context.getSharedPreferences("Settings", 0).edit().putInt(context.getString((R.string.key_max_id)), id).apply(); maxId = id; } public static int getStatusBarHeight(Context context) { return context.getResources().getDimensionPixelSize(R.dimen.status_bar_height); } public static int getNavigationBarHeight(Context context) { return context.getResources().getDimensionPixelSize(R.dimen.nav_header_height); } public static void shareURL(Context context, String title, String url) { Intent sendIntent = new Intent(); sendIntent.setAction(Intent.ACTION_SEND); sendIntent.putExtra(Intent.EXTRA_TEXT, title + ": " + url); sendIntent.setType("text/plain"); Intent clipboardIntent = new Intent(context, CopyToClipboardActivity.class); clipboardIntent.setData(Uri.parse(url)); Intent chooserIntent = Intent.createChooser(sendIntent, context.getString(R.string.share_with)); chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, new Intent[]{clipboardIntent}); context.startActivity(chooserIntent); } public static void shareGallery(Context context, GenericGallery gallery) { shareURL(context, gallery.getTitle(), Utility.getBaseUrl() + "g/" + gallery.getId()); } public static void setTint(Context context, Drawable drawable) { if (drawable == null) return; DrawableCompat.setTint(drawable, context.getColor(R.color.tint_dark)); } private static void loadNotificationChannel(@NonNull Context context) { NotificationChannel channel1 = new NotificationChannel(CHANNEL_ID1, context.getString(R.string.channel1_name), NotificationManager.IMPORTANCE_DEFAULT); NotificationChannel channel2 = new NotificationChannel(CHANNEL_ID2, context.getString(R.string.channel2_name), NotificationManager.IMPORTANCE_DEFAULT); NotificationChannel channel3 = new NotificationChannel(CHANNEL_ID3, context.getString(R.string.channel3_name), NotificationManager.IMPORTANCE_DEFAULT); channel1.setDescription(context.getString(R.string.channel1_description)); channel2.setDescription(context.getString(R.string.channel2_description)); channel3.setDescription(context.getString(R.string.channel3_description)); NotificationManager notificationManager = context.getSystemService(NotificationManager.class); if (notificationManager != null) { notificationManager.createNotificationChannel(channel1); notificationManager.createNotificationChannel(channel2); notificationManager.createNotificationChannel(channel3); } } public static List getUsableFolders(Context context) { List strings = new ArrayList<>(3); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) strings.add(Environment.getExternalStorageDirectory()); File[] files = context.getExternalFilesDirs(null); strings.addAll(Arrays.asList(files)); return strings; } public static boolean hasStoragePermission(Context context) { if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { return true; //We don't check permission on Android 13 } else { return ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED; } } public static boolean isJPEGCorrupted(String path) { if (!new File(path).exists()) return true; try (RandomAccessFile fh = new RandomAccessFile(path, "r")) { long length = fh.length(); if (length < 10L) { return true; } fh.seek(length - 2); byte[] eoi = new byte[2]; fh.read(eoi); return eoi[0] != (byte) 0xFF || eoi[1] != (byte) 0xD9; // FF D9 } catch (IOException e) { LogUtility.e(e.getMessage(), e); } return true; } private static File findGalleryFolder(File directory, int id) { if (directory == null || !directory.exists() || !directory.isDirectory()) return null; String fileName = "." + id; File[] tmp = directory.listFiles(); if (tmp == null) return null; for (File tmp2 : tmp) { if (tmp2.isDirectory() && new File(tmp2, fileName).exists()) { return tmp2; } } return null; } @Nullable private static File findGalleryFolder(int id) { return findGalleryFolder(Global.DOWNLOADFOLDER, id); } @Nullable public static File findGalleryFolder(Context context, int id) { if (id < 1) return null; if (context == null) return findGalleryFolder(id); for (File dir : getUsableFolders(context)) { dir = new File(dir, MAINFOLDER_NAME); dir = new File(dir, DOWNLOADFOLDER_NAME); File f = findGalleryFolder(dir, id); if (f != null) return f; } return null; } public static void initActivity(AppCompatActivity context) { initScreenSize(context); initGallerySize(); } public static void recursiveDelete(File file) { if (file == null || !file.exists()) return; if (file.isDirectory()) { File[] files = file.listFiles(); if (files == null) return; for (File x : files) recursiveDelete(x); } //noinspection ResultOfMethodCallIgnored file.delete(); } @NonNull public static String getVersionName(Context context) { try { PackageInfo pInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0); return Objects.requireNonNull(pInfo.versionName); } catch (PackageManager.NameNotFoundException e) { LogUtility.w("Couldn't get Package Info", e); } return "0.0.0"; } public static boolean isExternalStorageManager() { return Build.VERSION.SDK_INT < Build.VERSION_CODES.R || Environment.isExternalStorageManager(); } public static void applyFastScroller(RecyclerView recycler) { if (recycler == null) return; Drawable drawable = ContextCompat.getDrawable(recycler.getContext(), R.drawable.thumb); if (drawable == null) return; new FastScrollerBuilder(recycler).setThumbDrawable(drawable).build(); } @NonNull public static String getLanguageFlag(Language language) { switch (language) { case CHINESE: return "\uD83C\uDDE8\uD83C\uDDF3"; case ENGLISH: return "\uD83C\uDDEC\uD83C\uDDE7"; case JAPANESE: return "\uD83C\uDDEF\uD83C\uDDF5"; case UNKNOWN: return "\uD83C\uDFF3"; } return ""; } public enum DataUsageType {NONE, THUMBNAIL, FULL} } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/settings/Login.java ================================================ package com.maxwai.nclientv3.settings; import android.content.Context; import android.content.SharedPreferences; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.maxwai.nclientv3.R; import com.maxwai.nclientv3.api.components.Tag; import com.maxwai.nclientv3.async.database.Queries; import com.maxwai.nclientv3.loginapi.LoadTags; import com.maxwai.nclientv3.loginapi.User; import com.maxwai.nclientv3.utility.Utility; import okhttp3.HttpUrl; public class Login { public static HttpUrl BASE_HTTP_URL; private static User user; private static boolean accountTag; private static Context appContext; public static void initLogin(@NonNull Context context) { appContext = context.getApplicationContext(); SharedPreferences preferences = context.getSharedPreferences("Settings", 0); accountTag = preferences.getBoolean(context.getString(R.string.preference_key_use_account_tag), false); BASE_HTTP_URL = HttpUrl.get(Utility.getBaseUrl()); } public static boolean useAccountTag() { return accountTag; } public static void clearOnlineTags() { Queries.TagTable.removeAllBlacklisted(); } public static void addOnlineTag(Tag tag) { Queries.TagTable.insert(tag); Queries.TagTable.updateBlacklistedTag(tag, true); } public static void removeOnlineTag(Tag tag) { Queries.TagTable.updateBlacklistedTag(tag, false); } public static boolean isLogged(@Nullable Context context) { Context authContext = context != null ? context.getApplicationContext() : appContext; if (authContext != null && AuthStore.hasCredentials(authContext)) { if (context != null && user == null) { User.createUser(context, user -> { if (user != null) { new LoadTags(context).start(); } }); } return true; } return false; } public static boolean isLogged() { return isLogged(null); } public static User getUser() { return user; } public static void updateUser(User user) { Login.user = user; } public static boolean isOnlineTags(Tag tag) { return Queries.TagTable.isBlackListed(tag); } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/settings/NotificationSettings.java ================================================ package com.maxwai.nclientv3.settings; import android.Manifest; import android.app.Notification; import android.content.Context; import android.content.pm.PackageManager; import androidx.core.app.ActivityCompat; import androidx.core.app.NotificationManagerCompat; import com.maxwai.nclientv3.R; import com.maxwai.nclientv3.utility.LogUtility; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; public class NotificationSettings { private static final List notificationArray = new CopyOnWriteArrayList<>(); private static NotificationSettings notificationSettings; private static int notificationId = 999, maximumNotification; private final NotificationManagerCompat notificationManager; private NotificationSettings(NotificationManagerCompat notificationManager) { this.notificationManager = notificationManager; } public static int getNotificationId() { return notificationId++; } public static void initializeNotificationManager(Context context) { notificationSettings = new NotificationSettings(NotificationManagerCompat.from(context.getApplicationContext())); maximumNotification = context.getSharedPreferences("Settings", 0).getInt(context.getString(R.string.preference_key_maximum_notification), 25); trimArray(); } public static void notify(Context context, int notificationId, Notification notification) { if (maximumNotification == 0) return; notificationArray.remove(Integer.valueOf(notificationId)); notificationArray.add(notificationId); trimArray(); LogUtility.d("Notification count: " + notificationArray.size()); if (ActivityCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { // TODO: Consider calling // ActivityCompat#requestPermissions // here to request the missing permissions, and then overriding // public void onRequestPermissionsResult(int requestCode, String[] permissions, // int[] grantResults) // to handle the case where the user grants the permission. See the documentation // for ActivityCompat#requestPermissions for more details. return; } notificationSettings.notificationManager.notify(notificationId, notification); } public static void cancel(int notificationId) { notificationSettings.notificationManager.cancel(notificationId); notificationArray.remove(Integer.valueOf(notificationId)); } private static void trimArray() { while (notificationArray.size() > maximumNotification) { int first = notificationArray.remove(0); notificationSettings.notificationManager.cancel(first); } } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/settings/TagV2.java ================================================ package com.maxwai.nclientv3.settings; import android.content.Context; import androidx.annotation.NonNull; import com.maxwai.nclientv3.api.components.Tag; import com.maxwai.nclientv3.api.enums.TagStatus; import com.maxwai.nclientv3.api.enums.TagType; import com.maxwai.nclientv3.async.database.Queries; import java.util.List; import java.util.Set; @SuppressWarnings({"unused", "UnusedReturnValue"}) public class TagV2 { public static final int MAXTAGS = 100; private static int minCount; private static boolean sortByName; public static List getTagSet(TagType type) { return Queries.TagTable.getAllTagOfType(type); } public static List getTagStatus(TagStatus status) { return Queries.TagTable.getAllStatus(status); } public static String getQueryString(String query, @NonNull Set all) { StringBuilder builder = new StringBuilder(); for (Tag t : all) if (!query.contains(t.getName())) builder.append('+').append(t.toQueryTag()); return builder.toString(); } public static List getListPrefer(boolean removeIgnoredGalleries) { return removeIgnoredGalleries ? Queries.TagTable.getAllFiltered() : Queries.TagTable.getAllStatus(TagStatus.ACCEPTED); } public static TagStatus updateStatus(Tag t) { TagStatus old = t.getStatus(); switch (t.getStatus()) { case ACCEPTED: t.setStatus(TagStatus.AVOIDED); break; case AVOIDED: t.setStatus(TagStatus.DEFAULT); break; case DEFAULT: t.setStatus(TagStatus.ACCEPTED); break; } if (Queries.TagTable.updateTag(t) == 1) return t.getStatus(); throw new RuntimeException("Unable to update: " + t); } public static void resetAllStatus() { Queries.TagTable.resetAllStatus(); } public static boolean containTag(Tag[] tags, Tag t) { for (Tag t1 : tags) if (t.equals(t1)) return true; return false; } public static TagStatus getStatus(Tag tag) { return Queries.TagTable.getStatus(tag); } public static boolean maxTagReached() { return getListPrefer(Global.removeAvoidedGalleries()).size() >= MAXTAGS; } public static void updateMinCount(Context context, int min) { context.getSharedPreferences("ScrapedTags", 0).edit().putInt("min_count", minCount = min).apply(); } public static void initMinCount(Context context) { minCount = context.getSharedPreferences("ScrapedTags", 0).getInt("min_count", 25); } public static void initSortByName(Context context) { sortByName = context.getSharedPreferences("ScrapedTags", 0).getBoolean("sort_by_name", false); } public static boolean updateSortByName(Context context) { context.getSharedPreferences("ScrapedTags", 0).edit().putBoolean("sort_by_name", sortByName = !sortByName).apply(); return sortByName; } public static boolean isSortedByName() { return sortByName; } public static int getMinCount() { return minCount; } public static String getAvoidedTags() { StringBuilder builder = new StringBuilder(); List tags = Queries.TagTable.getAllStatus(TagStatus.AVOIDED); for (Tag t : tags) builder.append('+').append(t.toQueryTag(TagStatus.AVOIDED)); return builder.toString(); } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/ui/main/PlaceholderFragment.java ================================================ package com.maxwai.nclientv3.ui.main; import android.content.res.Configuration; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.fragment.app.Fragment; import androidx.recyclerview.widget.RecyclerView; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import com.maxwai.nclientv3.R; import com.maxwai.nclientv3.adapters.StatusViewerAdapter; import com.maxwai.nclientv3.components.widgets.CustomGridLayoutManager; import com.maxwai.nclientv3.settings.Global; /** * A placeholder fragment containing a simple view. */ public class PlaceholderFragment extends Fragment { private StatusViewerAdapter adapter = null; private RecyclerView recycler; private SwipeRefreshLayout refresher; public static PlaceholderFragment newInstance(String statusName) { PlaceholderFragment fragment = new PlaceholderFragment(); Bundle bundle = new Bundle(); bundle.putString("STATUS_NAME", statusName); fragment.setArguments(bundle); return fragment; } private void updateColumnCount(boolean landscape) { recycler.setLayoutManager(new CustomGridLayoutManager(getContext(), getColumnCount(landscape))); recycler.setAdapter(adapter); } private int getColumnCount(boolean landscape) { return landscape ? Global.getColLandStatus() : Global.getColPortStatus(); } @Override public void onConfigurationChanged(@NonNull Configuration newConfig) { super.onConfigurationChanged(newConfig); if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) { updateColumnCount(true); } else if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) { updateColumnCount(false); } } @Override public View onCreateView( @NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View root = inflater.inflate(R.layout.fragment_status_viewer, container, false); recycler = root.findViewById(R.id.recycler); refresher = root.findViewById(R.id.refresher); adapter = new StatusViewerAdapter(getActivity(), requireArguments().getString("STATUS_NAME")); refresher.setOnRefreshListener(() -> { adapter.reloadGalleries(); refresher.setRefreshing(false); }); Global.applyFastScroller(recycler); updateColumnCount(getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE); return root; } public void changeQuery(String newQuery) { if (adapter != null) adapter.setQuery(newQuery); } public void changeSort(boolean byTitle) { if (adapter != null) adapter.updateSort(byTitle); } public void reload(String query, boolean sortByTitle) { if (adapter != null) adapter.update(query, sortByTitle); } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/ui/main/SectionsPagerAdapter.java ================================================ package com.maxwai.nclientv3.ui.main; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.viewpager2.adapter.FragmentStateAdapter; import com.maxwai.nclientv3.StatusViewerActivity; import com.maxwai.nclientv3.async.database.Queries; import com.maxwai.nclientv3.components.status.StatusManager; import java.util.List; import java.util.Locale; /** * A [FragmentPagerAdapter] that returns a fragment corresponding to * one of the sections/tabs/pages. */ public class SectionsPagerAdapter extends FragmentStateAdapter { final List statuses; public SectionsPagerAdapter(StatusViewerActivity context) { super(context.getSupportFragmentManager(), context.getLifecycle()); statuses = StatusManager.getNames(); } @Nullable public CharSequence getPageTitle(int position) { String status = statuses.get(position); int count = Queries.StatusMangaTable.getCountPerStatus(status); return String.format(Locale.US, "%s - %d", status, count); } @NonNull @Override public Fragment createFragment(int position) { return PlaceholderFragment.newInstance(statuses.get(position)); } @Override public int getItemCount() { return statuses.size(); } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/utility/CSRFGet.java ================================================ package com.maxwai.nclientv3.utility; import androidx.annotation.Nullable; import com.maxwai.nclientv3.settings.Global; import java.io.IOException; import okhttp3.Request; public class CSRFGet extends Thread { @Nullable private final Response response; private final String url; public CSRFGet(@Nullable Response response, String url) { this.response = response; this.url = url; } @Override public void run() { try { assert Global.getClient() != null; try (okhttp3.Response response = Global.getClient().newCall(new Request.Builder().url(url).build()).execute()) { String token = response.body().string(); token = token.substring(token.lastIndexOf("csrf_token")); token = token.substring(token.indexOf('"') + 1); token = token.substring(0, token.indexOf('"')); if (this.response != null) this.response.onResponse(token); } } catch (Exception e) { if (response != null) response.onError(e); } } public interface Response { void onResponse(String token) throws IOException; default void onError(Exception e) { LogUtility.e("Error in response", e); } } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/utility/ImageDownloadUtility.java ================================================ package com.maxwai.nclientv3.utility; import android.app.Activity; import android.content.Context; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Handler; import android.widget.ImageView; import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.bumptech.glide.RequestBuilder; import com.bumptech.glide.RequestManager; import com.bumptech.glide.load.DataSource; import com.bumptech.glide.load.engine.GlideException; import com.bumptech.glide.load.resource.bitmap.Rotate; import com.bumptech.glide.request.RequestListener; import com.bumptech.glide.request.target.ImageViewTarget; import com.bumptech.glide.request.target.Target; import com.maxwai.nclientv3.api.components.Gallery; import com.maxwai.nclientv3.components.GlideX; import com.maxwai.nclientv3.settings.Global; import java.io.File; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.function.Supplier; public class ImageDownloadUtility { private static final Map> imageDownloadQueue = new HashMap<>(); public static void preloadImage(Context context, Uri url) { if (Global.getDownloadPolicy() == Global.DataUsageType.NONE) return; RequestManager manager = GlideX.with(context); LogUtility.d("Requested url glide: " + url); if (manager != null) manager.load(url).preload(); } public static void loadImageOp(Context context, ImageView view, File file, int angle) { RequestManager glide = GlideX.with(context); if (glide == null) return; Drawable logo = Global.getLogo(context.getResources()); glide.load(file).transform(new Rotate(angle)).error(logo).placeholder(logo).into(view); LogUtility.d("Requested file glide: " + file); } public static void loadImageOp(Context context, ImageView view, Gallery gallery, int page, int angle) { loadImageOp(context, view, gallery, page, angle, true); } public static void downloadPage(Activity activity, ImageView imageView, Gallery gallery, int page, boolean shouldFull) { shouldFull = gallery.getHighPage(page).toString().endsWith("gif") || shouldFull; loadImageOp(activity, imageView, gallery, page, 0, shouldFull); } public static void loadImageOp(Context context, ImageView imageView, Gallery gallery, int page, int angle, boolean shouldFull) { loadImageOp(context, imageView, gallery, () -> getUrlForGallery(gallery, page, shouldFull), angle, false); } private static void loadImageOp(Context context, ImageView view, @Nullable Gallery gallery, Supplier url, int angle, boolean priority) { if (Global.getDownloadPolicy() == Global.DataUsageType.NONE) { loadLogo(view); return; } boolean newGallery = false; if (!imageDownloadQueue.containsKey(gallery)) { imageDownloadQueue.put(gallery, new LinkedList<>()); newGallery = true; } //noinspection DataFlowIssue imageDownloadQueue.get(gallery).add(() -> { LogUtility.d("Requested url glide: " + url.get()); RequestManager glide = GlideX.with(context); if (glide == null) return; Drawable logo = Global.getLogo(context.getResources()); RequestBuilder dra = glide.load(url.get()); if (angle != 0) dra = dra.transform(new Rotate(angle)); dra.error(logo) .addListener(new RequestListener<>() { @Override public boolean onLoadFailed(@Nullable GlideException e, @Nullable Object model, @NonNull Target target, boolean isFirstResource) { return false; } @Override public boolean onResourceReady(@NonNull Drawable resource, @NonNull Object model, Target target, @NonNull DataSource dataSource, boolean isFirstResource) { if (gallery != null && !gallery.getGalleryData().getCheckedExt()) gallery.getGalleryData().setCheckedExt(); new Handler(context.getMainLooper()).post(() -> { //noinspection DataFlowIssue while (imageDownloadQueue.containsKey(gallery) && !imageDownloadQueue.get(gallery).isEmpty()) { //noinspection DataFlowIssue imageDownloadQueue.get(gallery).remove(0).run(); } }); return false; } }) .placeholder(logo) .into(new ImageViewTarget(view) { @Override protected void setResource(@Nullable Drawable resource) { new Handler(context.getMainLooper()).post(() -> this.view.setImageDrawable(resource)); } }); }); if (newGallery) { //noinspection DataFlowIssue imageDownloadQueue.get(gallery).remove(0).run(); } else if (priority) { //noinspection DataFlowIssue imageDownloadQueue.get(gallery).remove(imageDownloadQueue.get(gallery).size() - 1).run(); } else if (gallery == null || gallery.getGalleryData().getCheckedExt()) { //noinspection DataFlowIssue while (!imageDownloadQueue.get(gallery).isEmpty()) //noinspection DataFlowIssue imageDownloadQueue.get(gallery).remove(0).run(); } } private static Uri getUrlForGallery(Gallery gallery, int page, boolean shouldFull) { return shouldFull ? gallery.getPageUrl(page) : gallery.getLowPage(page); } private static void loadLogo(ImageView imageView) { imageView.setImageDrawable(Global.getLogo(imageView.getResources())); } public static void loadImage(Activity activity, Uri url, ImageView imageView) { loadImageOp(activity, imageView, null, () -> url, 0, false); } public static void loadImage(Activity activity, File file, ImageView imageView) { loadImage(activity, file == null ? null : Uri.fromFile(file), imageView); } /** * Load Resource using id */ public static void loadImage(@DrawableRes int resource, ImageView imageView) { imageView.setImageResource(resource); } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/utility/IntentUtility.java ================================================ package com.maxwai.nclientv3.utility; import android.app.Activity; import android.content.Intent; public class IntentUtility extends Intent { public static void startAnotherActivity(Activity activity, Intent intent) { activity.runOnUiThread(() -> activity.startActivity(intent)); } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/utility/LogUtility.java ================================================ package com.maxwai.nclientv3.utility; import android.util.Log; import java.util.Arrays; import java.util.function.BiConsumer; public class LogUtility { public static final String LOGTAG = "NCLIENTLOG"; public static void d(Object... message) { common(Log::d, message); } public static void d(Object message, Throwable throwable) { common(Log::d, message, throwable); } public static void i(Object... message) { common(Log::i, message); } public static void i(Object message, Throwable throwable) { common(Log::i, message, throwable); } public static void w(Object... message) { common(Log::w, message); } public static void w(Object message, Throwable throwable) { common(Log::w, message, throwable); } public static void e(Object... message) { common(Log::e, message); } public static void e(Object message, Throwable throwable) { common(Log::e, message, throwable); } public static void wtf(Object... message) { common(Log::wtf, message); } public static void wtf(Object message, Throwable throwable) { common(Log::wtf, message, throwable); } private static void common(BiConsumer logCall, Object... message) { if (message == null) return; if (message.length == 1) logCall.accept(LogUtility.LOGTAG, "" + message[0]); else logCall.accept(LogUtility.LOGTAG, Arrays.toString(message)); } private static void common(TriConsumer logCall, Object message, Throwable throwable) { if (message == null) message = ""; logCall.accept(LogUtility.LOGTAG, message.toString(), throwable); } @FunctionalInterface private interface TriConsumer { void accept(String tag, String msg, Throwable tr); } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/utility/Utility.java ================================================ package com.maxwai.nclientv3.utility; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.graphics.Bitmap; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.net.Uri; import android.view.Menu; import android.view.MenuItem; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.content.FileProvider; import com.maxwai.nclientv3.R; import com.maxwai.nclientv3.settings.Global; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.Reader; import java.io.StringReader; import java.io.StringWriter; import java.io.Writer; import java.util.List; import java.util.Random; public class Utility { public static final Random RANDOM = new Random(System.nanoTime()); public static final String ORIGINAL_URL = "nhentai.net"; public static String getBaseUrl() { return "https://" + Utility.getHost() + "/"; } public static String getApiBaseUrl() { return getBaseUrl() + "api/v2/"; } public static String getHost() { return Global.getMirror(); } private static void parseEscapedCharacter(Reader reader, Writer writer) throws IOException { int toCreate, read; switch (read = reader.read()) { case 'u': toCreate = 0; for (int i = 0; i < 4; i++) { toCreate *= 16; toCreate += Character.digit(reader.read(), 16); } writer.write(toCreate); break; case 'n': writer.write('\n'); break; case 't': writer.write('\t'); break; default: writer.write('\\'); writer.write(read); break; } } @NonNull public static String unescapeUnicodeString(@Nullable String scriptHtml) { if (scriptHtml == null) return ""; StringReader reader = new StringReader(scriptHtml); StringWriter writer = new StringWriter(); int actualChar; try { while ((actualChar = reader.read()) != -1) { if (actualChar != '\\') writer.write(actualChar); else parseEscapedCharacter(reader, writer); } } catch (IOException ignore) { return ""; } return writer.toString(); } public static void threadSleep(long millis) { try { Thread.sleep(millis); } catch (InterruptedException e) { LogUtility.d("Unimportant sleep interrupted", e); } } public static void tintMenu(Context context, Menu menu) { int x = menu.size(); for (int i = 0; i < x; i++) { MenuItem item = menu.getItem(i); Global.setTint(context, item.getIcon()); } } @Nullable private static Bitmap drawableToBitmap(Drawable dra) { if (!(dra instanceof BitmapDrawable)) return null; return ((BitmapDrawable) dra).getBitmap(); } public static void saveImage(Drawable drawable, File output) { Bitmap b = drawableToBitmap(drawable); if (b != null) saveImage(b, output); } private static void saveImage(@NonNull Bitmap bitmap, @NonNull File output) { try { if (!output.exists()) //noinspection ResultOfMethodCallIgnored output.createNewFile(); try (FileOutputStream ostream = new FileOutputStream(output)) { bitmap.compress(Bitmap.CompressFormat.JPEG, 95, ostream); ostream.flush(); } } catch (IOException e) { LogUtility.e(e.getLocalizedMessage(), e); } } public static long writeStreamToFile(InputStream inputStream, File filePath) throws IOException { try (inputStream; FileOutputStream outputStream = new FileOutputStream(filePath)) { int read; long totalByte = 0; byte[] bytes = new byte[1024]; while ((read = inputStream.read(bytes)) != -1) { outputStream.write(bytes, 0, read); totalByte += read; } outputStream.flush(); return totalByte; } } public static void sendImage(Context context, Drawable drawable, String text) { context = context.getApplicationContext(); try { File tempFile = File.createTempFile("toSend", ".jpg"); tempFile.deleteOnExit(); Bitmap image = drawableToBitmap(drawable); if (image == null) return; saveImage(image, tempFile); Intent shareIntent = new Intent(Intent.ACTION_SEND); if (text != null) shareIntent.putExtra(Intent.EXTRA_TEXT, text); Uri x = FileProvider.getUriForFile(context, context.getPackageName() + ".provider", tempFile); shareIntent.putExtra(Intent.EXTRA_STREAM, x); shareIntent.setType("image/jpeg"); shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); List resInfoList = context.getPackageManager().queryIntentActivities(shareIntent, PackageManager.MATCH_DEFAULT_ONLY); for (ResolveInfo resolveInfo : resInfoList) { String packageName = resolveInfo.activityInfo.packageName; context.grantUriPermission(packageName, x, Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION); } shareIntent = Intent.createChooser(shareIntent, context.getString(R.string.share_with)); shareIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(shareIntent); } catch (IOException e) { LogUtility.e("Error creating temp file", e); Toast.makeText(context, R.string.send_image_error, Toast.LENGTH_SHORT).show(); } } } ================================================ FILE: app/src/main/java/com/maxwai/nclientv3/utility/network/NetworkUtil.java ================================================ package com.maxwai.nclientv3.utility.network; import android.content.Context; import android.net.ConnectivityManager; import android.net.Network; import android.net.NetworkCapabilities; import android.net.NetworkRequest; import androidx.annotation.NonNull; import com.maxwai.nclientv3.utility.LogUtility; public class NetworkUtil { private volatile static ConnectionType type = ConnectionType.WIFI; public static ConnectionType getType() { return type; } public static void setType(ConnectionType x) { LogUtility.d("new Status: " + x); type = x; } private static ConnectionType getConnectivityPostLollipop(ConnectivityManager cm, Network network) { NetworkCapabilities capabilities = cm.getNetworkCapabilities(network); if (capabilities == null) { return ConnectionType.WIFI; } if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) return ConnectionType.CELLULAR; return ConnectionType.WIFI; } public static void initConnectivity(@NonNull Context context) { context = context.getApplicationContext(); ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); assert cm != null; NetworkRequest.Builder builder = new NetworkRequest.Builder(); builder.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET); builder.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR); builder.addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET); builder.addTransportType(NetworkCapabilities.TRANSPORT_WIFI); ConnectivityManager.NetworkCallback callback = new ConnectivityManager.NetworkCallback() { @Override public void onAvailable(@NonNull Network network) { super.onAvailable(network); setType(getConnectivityPostLollipop(cm, network)); } }; try { cm.registerNetworkCallback(builder.build(), callback); Network[] networks = cm.getAllNetworks(); if (networks.length > 0) setType(getConnectivityPostLollipop(cm, networks[0])); } catch (SecurityException e) { LogUtility.e("Problem initializing connectivity", e); } } public enum ConnectionType {WIFI, CELLULAR} } ================================================ FILE: app/src/main/res/drawable/ic_access_time.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_add.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_archive.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_arrow_back.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_arrow_forward.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_backspace.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_bookmark.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_bookmark_border.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_burst_mode.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_chat_bubble.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_check.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_check_circle.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_close.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_cnbw.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_content_copy.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_delete.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_exit_to_app.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_favorite.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_favorite_border.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_filter_list.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_find_in_page.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_folder.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_gbbw.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_hashtag.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_help.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_jpbw.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_keyboard_arrow_left.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_keyboard_arrow_right.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_launcher_calculator_foreground.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_launcher_foreground.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_logo.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_mode_edit.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_pause.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_pdf.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_person.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_play.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_refresh.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_rotate_90_degrees.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_save.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_search.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_select_all.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_settings.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_share.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_shuffle.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_sort.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_sort_by_alpha.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_star.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_star_border.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_view_1.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_view_2.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_view_3.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_view_4.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_void.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_world.xml ================================================ ================================================ FILE: app/src/main/res/drawable/side_nav_bar.xml ================================================ ================================================ FILE: app/src/main/res/drawable/thumb.xml ================================================ ================================================ FILE: app/src/main/res/drawable-anydpi/ic_archive.xml ================================================ ================================================ FILE: app/src/main/res/drawable-anydpi/ic_check.xml ================================================ ================================================ FILE: app/src/main/res/drawable-anydpi/ic_close.xml ================================================ ================================================ FILE: app/src/main/res/drawable-anydpi/ic_file.xml ================================================ ================================================ FILE: app/src/main/res/drawable-anydpi/ic_pause.xml ================================================ ================================================ FILE: app/src/main/res/drawable-anydpi/ic_play.xml ================================================ ================================================ FILE: app/src/main/res/drawable-night/ic_logo.xml ================================================ ================================================ FILE: app/src/main/res/drawable-night/side_nav_bar.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_api_key.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_bookmark.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_comment.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_gallery.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_main.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_pin.xml ================================================