mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-10-31 06:17:57 +01:00 
			
		
		
		
	Compare commits
	
		
			1 Commits
		
	
	
		
			ab0893b2d4
			...
			v0.2.1
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 0cffb9e503 | 
| @@ -1,8 +0,0 @@ | ||||
| [*.{kt,kts}] | ||||
| max_line_length = 120 | ||||
| indent_size = 4 | ||||
| insert_final_newline = true | ||||
| ij_kotlin_allow_trailing_comma = true | ||||
| ij_kotlin_allow_trailing_comma_on_call_site = true | ||||
| ij_kotlin_name_count_to_use_star_import = 2147483647 | ||||
| ij_kotlin_name_count_to_use_star_import_for_members = 2147483647 | ||||
							
								
								
									
										24
									
								
								.gitattributes
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										24
									
								
								.gitattributes
									
									
									
									
										vendored
									
									
								
							| @@ -1,24 +0,0 @@ | ||||
| * text=auto | ||||
| * text eol=lf | ||||
|  | ||||
| # Windows forced line-endings | ||||
| /.idea/* text eol=crlf | ||||
|  | ||||
| # Gradle wrapper | ||||
| *.jar binary | ||||
|  | ||||
| # Images | ||||
| *.webp binary | ||||
| *.png binary | ||||
| *.jpg binary | ||||
| *.jpeg binary | ||||
| *.gif binary | ||||
| *.ico binary | ||||
| *.gz binary | ||||
| *.zip binary | ||||
| *.7z binary | ||||
| *.ttf binary | ||||
| *.eot binary | ||||
| *.woff binary | ||||
| *.pyc binary | ||||
| *.swp binary | ||||
							
								
								
									
										30
									
								
								.github/CONTRIBUTING.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								.github/CONTRIBUTING.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | ||||
| # Bugs | ||||
| * Include version (Setting > About > Version) | ||||
|  * If not latest, try updating, it may have already been solved | ||||
|  * Debug version is equal to the number of commits as seen in the main page | ||||
| * Include steps to reproduce (if not obvious from description) | ||||
| * Include screenshot (if needed) | ||||
| * If it could be device-dependent, try reproducing on another device (if possible),  include results and device names, OS, modifications (root, Xposed) | ||||
| * **Before reporting a new issue, take a look at the [FAQ](https://github.com/inorichi/tachiyomi/wiki/FAQ), the [changelog](https://github.com/inorichi/tachiyomi/releases) and the already opened [issues](https://github.com/inorichi/tachiyomi/issues).** | ||||
| * For large logs use http://pastebin.com/ (or similar) | ||||
| * For multipart issues use list like this: | ||||
|  * [x] Done | ||||
|  * [ ] Not done | ||||
| ``` | ||||
| * [x] Done | ||||
| * [ ] Not done | ||||
| ``` | ||||
|  | ||||
| DO: https://github.com/inorichi/tachiyomi/issues/24 https://github.com/inorichi/tachiyomi/issues/71 | ||||
|  | ||||
| DON'T: https://github.com/inorichi/tachiyomi/issues/75 | ||||
|  | ||||
| # Feature requests | ||||
|  | ||||
| * Write a detailed issue, explaning what it should do or how. Avoid writing just "like X app does" | ||||
| * Include screenshot (if needed) | ||||
|  | ||||
| # Translations | ||||
|  | ||||
| File `app/src/main/res/values/strings.xml` should be copied over to appropriate directories and then translated. | ||||
| Consult [Android.com](http://developer.android.com/training/basics/supporting-devices/languages.html#CreateDirs) | ||||
							
								
								
									
										7
									
								
								.github/ISSUE_TEMPLATE.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								.github/ISSUE_TEMPLATE.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | ||||
| **Please read https://github.com/inorichi/tachiyomi/blob/master/.github/CONTRIBUTING.md before posting** | ||||
|  | ||||
| Remove line above and describe your issue here. Fill out version below. Use Preview. | ||||
|  | ||||
|  | ||||
| Version: r000 or v0.0.0 | ||||
| (other relevant info like OS) | ||||
							
								
								
									
										5
									
								
								.github/ISSUE_TEMPLATE/config.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										5
									
								
								.github/ISSUE_TEMPLATE/config.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,5 +0,0 @@ | ||||
| blank_issues_enabled: false | ||||
| contact_links: | ||||
|   - name: 🖥️ Mihon website | ||||
|     url: https://mihon.app/ | ||||
|     about: Guides, troubleshooting, and answers to common questions | ||||
							
								
								
									
										104
									
								
								.github/ISSUE_TEMPLATE/report_issue.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										104
									
								
								.github/ISSUE_TEMPLATE/report_issue.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,104 +0,0 @@ | ||||
| name: 🐞 Issue report | ||||
| description: Report an issue in Mihon | ||||
| labels: [Bug] | ||||
| body: | ||||
|  | ||||
|   - type: textarea | ||||
|     id: reproduce-steps | ||||
|     attributes: | ||||
|       label: Steps to reproduce | ||||
|       description: Provide an example of the issue. | ||||
|       placeholder: | | ||||
|         Example: | ||||
|           1. First step | ||||
|           2. Second step | ||||
|           3. Issue here | ||||
|     validations: | ||||
|       required: true | ||||
|  | ||||
|   - type: textarea | ||||
|     id: expected-behavior | ||||
|     attributes: | ||||
|       label: Expected behavior | ||||
|       description: Explain what you should expect to happen. | ||||
|       placeholder: | | ||||
|         Example: | ||||
|           "This should happen..." | ||||
|     validations: | ||||
|       required: true | ||||
|  | ||||
|   - type: textarea | ||||
|     id: actual-behavior | ||||
|     attributes: | ||||
|       label: Actual behavior | ||||
|       description: Explain what actually happens. | ||||
|       placeholder: | | ||||
|         Example: | ||||
|           "This happened instead..." | ||||
|     validations: | ||||
|       required: true | ||||
|  | ||||
|   - type: textarea | ||||
|     id: crash-logs | ||||
|     attributes: | ||||
|       label: Crash logs | ||||
|       description: | | ||||
|         If you're experiencing crashes, share the crash logs from **More → Settings → Advanced** then press **Dump crash logs**. | ||||
|       placeholder: | | ||||
|         You can paste the crash logs in plain text or upload it as an attachment. | ||||
|  | ||||
|   - type: input | ||||
|     id: mihon-version | ||||
|     attributes: | ||||
|       label: Mihon version | ||||
|       description: You can find your Mihon version in **More → About**. | ||||
|       placeholder: | | ||||
|         Example: "0.16.5" | ||||
|     validations: | ||||
|       required: true | ||||
|  | ||||
|   - type: input | ||||
|     id: android-version | ||||
|     attributes: | ||||
|       label: Android version | ||||
|       description: You can find this somewhere in your Android settings. | ||||
|       placeholder: | | ||||
|         Example: "Android 11" | ||||
|     validations: | ||||
|       required: true | ||||
|  | ||||
|   - type: input | ||||
|     id: device | ||||
|     attributes: | ||||
|       label: Device | ||||
|       description: List your device and model. | ||||
|       placeholder: | | ||||
|         Example: "Google Pixel 5" | ||||
|     validations: | ||||
|       required: true | ||||
|  | ||||
|   - type: textarea | ||||
|     id: other-details | ||||
|     attributes: | ||||
|       label: Other details | ||||
|       placeholder: | | ||||
|         Additional details and attachments. | ||||
|  | ||||
|   - type: checkboxes | ||||
|     id: acknowledgements | ||||
|     attributes: | ||||
|       label: Acknowledgements | ||||
|       description: Read this carefully, we will close and ignore your issue if you skimmed through this. | ||||
|       options: | ||||
|         - label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open or closed issue. | ||||
|           required: true | ||||
|         - label: I have written a short but informative title. | ||||
|           required: true | ||||
|         - label: I have gone through the [FAQ](https://mihon.app/docs/faq/general) and [troubleshooting guide](https://mihon.app/docs/guides/troubleshooting/). | ||||
|           required: true | ||||
|         - label: I have updated the app to version **[0.16.5](https://github.com/mihonapp/mihon/releases/latest)**. | ||||
|           required: true | ||||
|         - label: I have updated all installed extensions. | ||||
|           required: true | ||||
|         - label: I will fill out all of the requested information in this form. | ||||
|           required: true | ||||
							
								
								
									
										37
									
								
								.github/ISSUE_TEMPLATE/request_feature.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										37
									
								
								.github/ISSUE_TEMPLATE/request_feature.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,37 +0,0 @@ | ||||
| name: ⭐ Feature request | ||||
| description: Suggest a feature to improve Mihon | ||||
| labels: [Feature request] | ||||
| body: | ||||
|  | ||||
|   - type: textarea | ||||
|     id: feature-description | ||||
|     attributes: | ||||
|       label: Describe your suggested feature | ||||
|       description: How can Mihon be improved? | ||||
|       placeholder: | | ||||
|         Example: | ||||
|           "It should work like this..." | ||||
|     validations: | ||||
|       required: true | ||||
|  | ||||
|   - type: textarea | ||||
|     id: other-details | ||||
|     attributes: | ||||
|       label: Other details | ||||
|       placeholder: | | ||||
|         Additional details and attachments. | ||||
|  | ||||
|   - type: checkboxes | ||||
|     id: acknowledgements | ||||
|     attributes: | ||||
|       label: Acknowledgements | ||||
|       description: Read this carefully, we will close and ignore your issue if you skimmed through this. | ||||
|       options: | ||||
|         - label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open or closed issue. | ||||
|           required: true | ||||
|         - label: I have written a short but informative title. | ||||
|           required: true | ||||
|         - label: I have updated the app to version **[0.16.5](https://github.com/mihonapp/mihon/releases/latest)**. | ||||
|           required: true | ||||
|         - label: I will fill out all of the requested information in this form. | ||||
|           required: true | ||||
							
								
								
									
										
											BIN
										
									
								
								.github/assets/logo.png
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										
											BIN
										
									
								
								.github/assets/logo.png
									
									
									
									
										vendored
									
									
								
							
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 7.5 KiB | 
							
								
								
									
										12
									
								
								.github/pull_request_template.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										12
									
								
								.github/pull_request_template.md
									
									
									
									
										vendored
									
									
								
							| @@ -1,12 +0,0 @@ | ||||
| <!-- | ||||
|   Please include a summary of the change and which issue is fixed. | ||||
|   Also make sure you've tested your code and also done a self-review of it. | ||||
|   Don't forget to check all base themes and tablet mode for relevant changes. | ||||
|    | ||||
|   If your changes are visual, please provide images below: | ||||
|  | ||||
| ### Images | ||||
| | Image 1 | Image 2 | | ||||
| | ------- | ------- | | ||||
| |  |  | | ||||
| --> | ||||
							
								
								
									
										6
									
								
								.github/renovate.json5
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								.github/renovate.json5
									
									
									
									
										vendored
									
									
								
							| @@ -1,6 +0,0 @@ | ||||
| { | ||||
|   "$schema": "https://docs.renovatebot.com/renovate-schema.json", | ||||
|   "extends": ["config:base"], | ||||
|   "labels": ["Dependencies"], | ||||
|   "semanticCommits": "disabled" | ||||
| } | ||||
							
								
								
									
										53
									
								
								.github/workflows/build_pull_request.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										53
									
								
								.github/workflows/build_pull_request.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,53 +0,0 @@ | ||||
| name: PR build check | ||||
| on: | ||||
|   pull_request: | ||||
|     paths-ignore: | ||||
|       - '**.md' | ||||
|       - 'i18n/src/commonMain/moko-resources/**/strings.xml' | ||||
|       - 'i18n/src/commonMain/moko-resources/**/plurals.xml' | ||||
|  | ||||
| concurrency: | ||||
|   group: ${{ github.workflow }}-${{ github.event.pull_request.number }} | ||||
|   cancel-in-progress: true | ||||
|  | ||||
| permissions: | ||||
|   contents: read | ||||
|  | ||||
| jobs: | ||||
|   build: | ||||
|     name: Build app | ||||
|     runs-on: ubuntu-latest | ||||
|  | ||||
|     steps: | ||||
|       - name: Clone repo | ||||
|         uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 | ||||
|  | ||||
|       - name: Validate Gradle Wrapper | ||||
|         uses: gradle/actions/wrapper-validation@d156388eb19639ec20ade50009f3d199ce1e2808 # v4.1.0 | ||||
|  | ||||
|       - name: Dependency Review | ||||
|         uses: actions/dependency-review-action@5a2ce3f5b92ee19cbb1541a4984c76d921601d7c # v4.3.4 | ||||
|  | ||||
|       - name: Set up JDK | ||||
|         uses: actions/setup-java@b36c23c0d998641eff861008f374ee103c25ac73 # v4.4.0 | ||||
|         with: | ||||
|           java-version: 17 | ||||
|           distribution: adopt | ||||
|  | ||||
|       - name: Set up gradle | ||||
|         uses: gradle/actions/setup-gradle@d156388eb19639ec20ade50009f3d199ce1e2808 # v4.1.0 | ||||
|  | ||||
|       - name: Build app and run unit tests | ||||
|         run: ./gradlew spotlessCheck assembleStandardRelease testReleaseUnitTest testStandardReleaseUnitTest | ||||
|  | ||||
|       - name: Upload APK | ||||
|         uses: actions/upload-artifact@v4 | ||||
|         with: | ||||
|           name: arm64-v8a-${{ github.sha }} | ||||
|           path: app/build/outputs/apk/standard/release/app-standard-arm64-v8a-release-unsigned.apk | ||||
|  | ||||
|       - name: Upload mapping | ||||
|         uses: actions/upload-artifact@v4 | ||||
|         with: | ||||
|           name: mapping-${{ github.sha }} | ||||
|           path: app/build/outputs/mapping/standardRelease | ||||
							
								
								
									
										125
									
								
								.github/workflows/build_push.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										125
									
								
								.github/workflows/build_push.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,125 +0,0 @@ | ||||
| name: CI | ||||
| on: | ||||
|   push: | ||||
|     branches: | ||||
|       - main | ||||
|     tags: | ||||
|       - v* | ||||
|  | ||||
| concurrency: | ||||
|   group: ${{ github.workflow }}-${{ github.ref }} | ||||
|   cancel-in-progress: true | ||||
|  | ||||
| jobs: | ||||
|   build: | ||||
|     name: Build app | ||||
|     runs-on: ubuntu-latest | ||||
|  | ||||
|     steps: | ||||
|       - name: Clone repo | ||||
|         uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 | ||||
|  | ||||
|       - name: Validate Gradle Wrapper | ||||
|         uses: gradle/actions/wrapper-validation@d156388eb19639ec20ade50009f3d199ce1e2808 # v4.1.0 | ||||
|  | ||||
|       - name: Setup Android SDK | ||||
|         run: | | ||||
|           ${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager "build-tools;29.0.3" | ||||
|  | ||||
|       - name: Set up JDK | ||||
|         uses: actions/setup-java@b36c23c0d998641eff861008f374ee103c25ac73 # v4.4.0 | ||||
|         with: | ||||
|           java-version: 17 | ||||
|           distribution: adopt | ||||
|  | ||||
|       - name: Set up gradle | ||||
|         uses: gradle/actions/setup-gradle@d156388eb19639ec20ade50009f3d199ce1e2808 # v4.1.0 | ||||
|  | ||||
|       - name: Build app and run unit tests | ||||
|         run: ./gradlew spotlessCheck assembleStandardRelease testReleaseUnitTest testStandardReleaseUnitTest | ||||
|  | ||||
|       - name: Upload APK | ||||
|         uses: actions/upload-artifact@v4 | ||||
|         with: | ||||
|           name: arm64-v8a-${{ github.sha }} | ||||
|           path: app/build/outputs/apk/standard/release/app-standard-arm64-v8a-release-unsigned.apk | ||||
|  | ||||
|       - name: Upload mapping | ||||
|         uses: actions/upload-artifact@v4 | ||||
|         with: | ||||
|           name: mapping-${{ github.sha }} | ||||
|           path: app/build/outputs/mapping/standardRelease | ||||
|  | ||||
|       # Sign APK and create release for tags | ||||
|  | ||||
|       - name: Get tag name | ||||
|         if: startsWith(github.ref, 'refs/tags/') && github.repository == 'mihonapp/mihon' | ||||
|         run: | | ||||
|           set -x | ||||
|           echo "VERSION_TAG=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_ENV | ||||
|  | ||||
|       - name: Sign APK | ||||
|         if: startsWith(github.ref, 'refs/tags/') && github.repository == 'mihonapp/mihon' | ||||
|         uses: r0adkll/sign-android-release@349ebdef58775b1e0d8099458af0816dc79b6407 # v1 | ||||
|         with: | ||||
|           releaseDirectory: app/build/outputs/apk/standard/release | ||||
|           signingKeyBase64: ${{ secrets.SIGNING_KEY }} | ||||
|           alias: ${{ secrets.ALIAS }} | ||||
|           keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }} | ||||
|           keyPassword: ${{ secrets.KEY_PASSWORD }} | ||||
|  | ||||
|       - name: Clean up build artifacts | ||||
|         if: startsWith(github.ref, 'refs/tags/') && github.repository == 'mihonapp/mihon' | ||||
|         run: | | ||||
|           set -e | ||||
|  | ||||
|           mv app/build/outputs/apk/standard/release/app-standard-universal-release-unsigned-signed.apk mihon-${{ env.VERSION_TAG }}.apk | ||||
|           sha=`sha256sum mihon-${{ env.VERSION_TAG }}.apk | awk '{ print $1 }'` | ||||
|           echo "APK_UNIVERSAL_SHA=$sha" >> $GITHUB_ENV | ||||
|  | ||||
|           cp app/build/outputs/apk/standard/release/app-standard-arm64-v8a-release-unsigned-signed.apk mihon-arm64-v8a-${{ env.VERSION_TAG }}.apk | ||||
|           sha=`sha256sum mihon-arm64-v8a-${{ env.VERSION_TAG }}.apk | awk '{ print $1 }'` | ||||
|           echo "APK_ARM64_V8A_SHA=$sha" >> $GITHUB_ENV | ||||
|  | ||||
|           cp app/build/outputs/apk/standard/release/app-standard-armeabi-v7a-release-unsigned-signed.apk mihon-armeabi-v7a-${{ env.VERSION_TAG }}.apk | ||||
|           sha=`sha256sum mihon-armeabi-v7a-${{ env.VERSION_TAG }}.apk | awk '{ print $1 }'` | ||||
|           echo "APK_ARMEABI_V7A_SHA=$sha" >> $GITHUB_ENV | ||||
|  | ||||
|           cp app/build/outputs/apk/standard/release/app-standard-x86-release-unsigned-signed.apk mihon-x86-${{ env.VERSION_TAG }}.apk | ||||
|           sha=`sha256sum mihon-x86-${{ env.VERSION_TAG }}.apk | awk '{ print $1 }'` | ||||
|           echo "APK_X86_SHA=$sha" >> $GITHUB_ENV | ||||
|            | ||||
|           cp app/build/outputs/apk/standard/release/app-standard-x86_64-release-unsigned-signed.apk mihon-x86_64-${{ env.VERSION_TAG }}.apk | ||||
|           sha=`sha256sum mihon-x86_64-${{ env.VERSION_TAG }}.apk | awk '{ print $1 }'` | ||||
|           echo "APK_X86_64_SHA=$sha" >> $GITHUB_ENV | ||||
|  | ||||
|       - name: Create Release | ||||
|         if: startsWith(github.ref, 'refs/tags/') && github.repository == 'mihonapp/mihon' | ||||
|         uses: softprops/action-gh-release@c062e08bd532815e2082a85e87e3ef29c3e6d191 # v2.0.8 | ||||
|         with: | ||||
|           tag_name: ${{ env.VERSION_TAG }} | ||||
|           name: Mihon ${{ env.VERSION_TAG }} | ||||
|           body: | | ||||
|             --- | ||||
|  | ||||
|             ### Checksums | ||||
|  | ||||
|             | Variant | SHA-256 | | ||||
|             | ------- | ------- | | ||||
|             | Universal | ${{ env.APK_UNIVERSAL_SHA }} | ||||
|             | arm64-v8a | ${{ env.APK_ARM64_V8A_SHA }} | ||||
|             | armeabi-v7a | ${{ env.APK_ARMEABI_V7A_SHA }} | ||||
|             | x86 | ${{ env.APK_X86_SHA }} | | ||||
|             | x86_64 | ${{ env.APK_X86_64_SHA }} | | ||||
|              | ||||
|             ## If you are unsure which version to choose then go with mihon-${{ env.VERSION_TAG }}.apk | ||||
|           files: | | ||||
|             mihon-${{ env.VERSION_TAG }}.apk | ||||
|             mihon-arm64-v8a-${{ env.VERSION_TAG }}.apk | ||||
|             mihon-armeabi-v7a-${{ env.VERSION_TAG }}.apk | ||||
|             mihon-x86-${{ env.VERSION_TAG }}.apk | ||||
|             mihon-x86_64-${{ env.VERSION_TAG }}.apk | ||||
|           draft: true | ||||
|           prerelease: false | ||||
|         env: | ||||
|           GITHUB_TOKEN: ${{ secrets.PAT }} | ||||
							
								
								
									
										19
									
								
								.github/workflows/lock.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										19
									
								
								.github/workflows/lock.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,19 +0,0 @@ | ||||
| name: Lock threads | ||||
|  | ||||
| on: | ||||
|   # Daily | ||||
|   schedule: | ||||
|     - cron: '0 0 * * *' | ||||
|   # Manual trigger | ||||
|   workflow_dispatch: | ||||
|     inputs: | ||||
|  | ||||
| jobs: | ||||
|   lock: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - uses: dessant/lock-threads@1bf7ec25051fe7c00bdd17e6a7cf3d7bfb7dc771 # v5.0.1 | ||||
|         with: | ||||
|           github-token: ${{ github.token }} | ||||
|           issue-inactive-days: '2' | ||||
|           pr-inactive-days: '2' | ||||
							
								
								
									
										15
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										15
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -1,18 +1,9 @@ | ||||
| .gradle | ||||
| .kotlin | ||||
| /local.properties | ||||
| /.idea/workspace.xml | ||||
| .DS_Store | ||||
| .idea/* | ||||
| !.idea/icon.png | ||||
| /build | ||||
| .idea/ | ||||
| *iml | ||||
| *.iml | ||||
|  | ||||
| # Built files | ||||
| */build | ||||
| /build | ||||
| *.apk | ||||
| app/**/output.json | ||||
|  | ||||
| # Unnecessary file | ||||
| *.swp | ||||
| */build | ||||
							
								
								
									
										
											BIN
										
									
								
								.idea/icon.png
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										
											BIN
										
									
								
								.idea/icon.png
									
									
									
										generated
									
									
									
								
							
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 62 KiB | 
							
								
								
									
										28
									
								
								.travis.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								.travis.yml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,28 @@ | ||||
| language: android | ||||
| android: | ||||
|   components: | ||||
|     - platform-tools | ||||
|     - tools | ||||
|  | ||||
|     # The BuildTools version used by your project | ||||
|     - build-tools-23.0.3 | ||||
|     - android-23 | ||||
|     - extra-android-m2repository | ||||
|     - extra-google-m2repository | ||||
|     - extra-android-support | ||||
|     - extra-google-google_play_services | ||||
|  | ||||
| before_script: | ||||
|     - chmod +x gradlew | ||||
| #Build, and run tests | ||||
| script: "./gradlew clean assembleDebug testDebugUnitTest" | ||||
| sudo: false | ||||
|  | ||||
| before_cache: | ||||
|   - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock | ||||
| cache: | ||||
|   directories: | ||||
|     - $HOME/.gradle/caches/ | ||||
|     - $HOME/.gradle/wrapper/ | ||||
| env: | ||||
|   - GRADLE_OPTS="-XX:MaxPermSize=1024m -XX:+CMSClassUnloadingEnabled -XX:+HeapDumpOnOutOfMemoryError -Xmx2048m" | ||||
							
								
								
									
										134
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										134
									
								
								CHANGELOG.md
									
									
									
									
									
								
							| @@ -1,134 +0,0 @@ | ||||
| # Changelog | ||||
|  | ||||
| All notable changes to this project will be documented in this file. | ||||
|  | ||||
| The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), | ||||
| and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). | ||||
|  | ||||
| ## [Unreleased] | ||||
| ### Added | ||||
| - Option to disable reader zoom out ([@Splintorien](https://github.com/Splintorien)) ([#302](https://github.com/mihonapp/mihon/pull/302)) | ||||
| - Source name and tracker urls to app generated `ComicInfo.xml` file ([@Shamicen](https://github.com/Shamicen)) ([#459](https://github.com/mihonapp/mihon/pull/459)) | ||||
| - Option to migrate in Duplicate entry dialog ([@sirlag](https://github.com/sirlag)) ([#492](https://github.com/mihonapp/mihon/pull/492)) | ||||
| - Upcoming screen to visualize expected update dates ([@sirlag](https://github.com/sirlag)) ([#420](https://github.com/mihonapp/mihon/pull/420)) | ||||
| - Crash screen error message to the top of the crash log generated from that screen ([@FooIbar](https://github.com/FooIbar)) ([#742](https://github.com/mihonapp/mihon/pull/742)) | ||||
| - Support for 7Zip and RAR5 archives ([@FooIbar](https://github.com/FooIbar), [@null2264](https://github.com/null2264)) ([#949](https://github.com/mihonapp/mihon/pull/949), [#967](https://github.com/mihonapp/mihon/pull/967)) | ||||
| - Extra configuration options to e-ink page flashes ([@sirlag](https://github.com/sirlag)) ([#625](https://github.com/mihonapp/mihon/pull/625)) | ||||
| - 8-bit+ AVIF image support ([@WerctFourth](https://github.com/WerctFourth)) ([#971](https://github.com/mihonapp/mihon/pull/971)) | ||||
| - Smart update dialog message when no predicted released date exists ([@Animeboynz](https://github.com/Animeboynz)) ([#977](https://github.com/mihonapp/mihon/pull/977)) | ||||
| - Save global search "Has result" choice ([@AntsyLich](https://github.com/AntsyLich)) ([`5a61ca5`](https://github.com/mihonapp/mihon/commit/5a61ca5535fe0d9e8e7bcb9e665ba2f9cb0cf649)) | ||||
| - Option to copy reader panel to clipboard ([@Animeboynz](https://github.com/Animeboynz)) ([#1003](https://github.com/mihonapp/mihon/pull/1003)) | ||||
| - Copy Tracker URL option to tracker sheet ([@mm12](https://github.com/mm12)) ([#1101](https://github.com/mihonapp/mihon/pull/1101)) | ||||
| - A button to exclude all scanlators in exclude scanlators dialog ([@AntsyLich](https://github.com/AntsyLich)) ([`84b2164`](https://github.com/mihonapp/mihon/commit/84b2164787a795f3fd757c325cbfb6ef660ac3a3)) | ||||
| - Open in browser option to reader menu ([@mm12](https://github.com/mm12)) ([#1110](https://github.com/mihonapp/mihon/pull/1110)) | ||||
| - Option to skip downloading duplicate read chapters ([@shabnix](https://github.com/shabnix)) ([#1125](https://github.com/mihonapp/mihon/pull/1125)) | ||||
|  | ||||
| ### Changed | ||||
| - Read archive files from memory instead of extracting files to internal storage ([@FooIbar](https://github.com/FooIbar)) ([#326](https://github.com/mihonapp/mihon/pull/326)) | ||||
| - Try to get resource from Extension before checking in the app ([@beer-psi](https://github.com/beer-psi)) ([#433](https://github.com/mihonapp/mihon/pull/433)) | ||||
| - Default user agent ([@AntsyLich](https://github.com/AntsyLich)) ([`8160b47`](https://github.com/mihonapp/mihon/commit/8160b47ff5fbbd9b32caeb462b5be881fabd3449)) | ||||
| - Wait for sources to be initialized before performing source related tasks ([@jobobby04](https://github.com/jobobby04)) ([`a08e03f`](https://github.com/mihonapp/mihon/commit/a08e03f5cbf3f4e6be1de35f97ef8ebb26a1210e)) | ||||
| - Duplicate entry dialog UI ([@sirlag](https://github.com/sirlag)) ([#492](https://github.com/mihonapp/mihon/pull/492)) | ||||
| - Extension trust system ([@AntsyLich](https://github.com/AntsyLich), [@Animeboynz](https://github.com/Animeboynz) ([#570](https://github.com/mihonapp/mihon/pull/570), [#1057](https://github.com/mihonapp/mihon/pull/1057)) | ||||
| - Make category backup/restore not dependant on library backup ([@AntsyLich](https://github.com/AntsyLich)) ([`56fb4f6`](https://github.com/mihonapp/mihon/commit/56fb4f62a152e87a71892aa68c78cac51a2c8596)) | ||||
| - Kitsu domain to `kitsu.app` ([@MajorTanya](https://github.com/MajorTanya)) ([#1106](https://github.com/mihonapp/mihon/pull/1106)) | ||||
| - Respect privacy settings in extension update notification ([@Animeboynz](https://github.com/Animeboynz)) ([#1156](https://github.com/mihonapp/mihon/pull/1156)) | ||||
|  | ||||
| ### Improvement | ||||
| - Long strip reader performance ([@FooIbar](https://github.com/FooIbar), [@wwww-wwww](https://github.com/wwww-wwww)) ([#687](https://github.com/mihonapp/mihon/pull/687)) | ||||
| - Performance when looking up specific files ([@raxod502](https://github.com/raxod502)) ([#728](https://github.com/mihonapp/mihon/pull/728)) | ||||
| - Chapter number parsing ([@Naputt1](https://github.com/Naputt1)) ([`6a80305`](https://github.com/mihonapp/mihon/commit/6a80305d6c572da6c08c0c69f5c25ff26ecf7383)) | ||||
| - Error message on restoring if backup decoding fails ([@vetleledaal](https://github.com/vetleledaal)) ([#1056](https://github.com/mihonapp/mihon/pull/1056)) | ||||
|  | ||||
| ### Fixed | ||||
| - Creating `ComicInfo.xml` file for local source ([@FooIbar](https://github.com/FooIbar)) ([#325](https://github.com/mihonapp/mihon/pull/325)) | ||||
| - Chapter download indicator ([@ivaniskandar](https://github.com/ivaniskandar)) ([`d8b9a9f`](https://github.com/mihonapp/mihon/commit/d8b9a9f593911569ff2bceb49b4f020978d0d2e1)) | ||||
| - Issues with shizuku in a multi user setup ([@Redjard](https://github.com/Redjard)) ([#494](https://github.com/mihonapp/mihon/pull/494)) | ||||
| - Occasional black bar when scrolling in long strip reader ([@FooIbar](https://github.com/FooIbar)) ([#563](https://github.com/mihonapp/mihon/pull/563)) | ||||
| - Extension being marked as not installed instead of untrusted after updating with private installer ([@AntsyLich](https://github.com/AntsyLich)) ([`2114514`](https://github.com/mihonapp/mihon/commit/21145144cdf550aa775047603e06e261951ebc42)) | ||||
| - Extension update counter not updating due to extension being marked as untrusted ([@AntsyLich](https://github.com/AntsyLich)) ([`2114514`](https://github.com/mihonapp/mihon/commit/21145144cdf550aa775047603e06e261951ebc42)) | ||||
| - `Key "extension-XXX-YYY" was already used` crash ([@AntsyLich](https://github.com/AntsyLich)) ([`2114514`](https://github.com/mihonapp/mihon/commit/21145144cdf550aa775047603e06e261951ebc42)) | ||||
| - Navigation layout tap zones shifting after zooming out in webtoon readers ([@FooIbar](https://github.com/FooIbar)) ([#767](https://github.com/mihonapp/mihon/pull/767)) | ||||
| - Some extension not loading due to missing classes ([@AwkwardPeak7](https://github.com/AwkwardPeak7)) ([#783](https://github.com/mihonapp/mihon/pull/783)) | ||||
| - Theme colors in accordance to upstream changes ([@CrepeTF](https://github.com/CrepeTF), [@AntsyLich](https://github.com/AntsyLich)) ([#766](https://github.com/mihonapp/mihon/pull/766), [#963](https://github.com/mihonapp/mihon/pull/963), [#976](https://github.com/mihonapp/mihon/pull/976)) | ||||
| - Crash when requesting folder access on non-conforming devices ([@mainrs](https://github.com/mainrs)) ([#726](https://github.com/mihonapp/mihon/pull/726)) | ||||
| - Bugged color for Date/Scanlator in chapter list for read chapters ([@ivaniskandar](https://github.com/ivaniskandar)) ([`15d9992`](https://github.com/mihonapp/mihon/commit/15d999229fcce865001d5fa77d0163e6e80e38db)) | ||||
| - Categories having same `order` after restoring backup ([@Cologler](https://github.com/Cologler)) ([`119bcbf`](https://github.com/mihonapp/mihon/commit/119bcbf8ed2415664922ea77fadf0da1165d1732)) | ||||
| - Filter by "Tracking" temporarily stuck after signing out of tracker ([@AntsyLich](https://github.com/AntsyLich)) ([#987](https://github.com/mihonapp/mihon/pull/987)) | ||||
| - JXL image downloading and loading ([@FooIbar](https://github.com/FooIbar)) ([#993](https://github.com/mihonapp/mihon/pull/993)) | ||||
| - Crash when using `%` in category name ([@Animeboynz](https://github.com/Animeboynz), [@FooIbar](https://github.com/FooIbar)) ([#1030](https://github.com/mihonapp/mihon/pull/1030)) | ||||
| - Library is backed up while being disabled ([@AntsyLich](https://github.com/AntsyLich)) ([`56fb4f6`](https://github.com/mihonapp/mihon/commit/56fb4f62a152e87a71892aa68c78cac51a2c8596)) | ||||
| - Crash on list with 0 item but only sticky header ([@cuong-tran](https://github.com/cuong-tran)) ([#1083](https://github.com/mihonapp/mihon/pull/1083)) | ||||
| - Crash when trying to clear cookies of some source ([@FooIbar](https://github.com/FooIbar)) ([#1084](https://github.com/mihonapp/mihon/pull/1084)) | ||||
| - MAL search results not showing start dates ([@MajorTanya](https://github.com/MajorTanya)) ([#1098](https://github.com/mihonapp/mihon/pull/1098)) | ||||
| - Android SDK 35 API collision ([@AntsyLich](https://github.com/AntsyLich)) ([`fdb9617`](https://github.com/mihonapp/mihon/commit/fdb96179c6373eb0a8e7d6daea671a315d5ce5f0)) | ||||
|  | ||||
| ## [v0.16.5] - 2024-04-09 | ||||
| ### Added | ||||
| - Setting to install custom color profiles to get true colors ([@wwww-wwww](https://github.com/wwww-wwww)) ([#523](https://github.com/mihonapp/mihon/pull/523)) | ||||
|  | ||||
| ### Changed | ||||
| - Permanently enable 32-bit color mode ([@wwww-wwww](https://github.com/wwww-wwww)) ([#523](https://github.com/mihonapp/mihon/pull/523)) | ||||
|  | ||||
| ### Fixed | ||||
| - Fix wrong dates in Updates and History tab due to time zone issues ([@sirlag](https://github.com/sirlag)) ([#402](https://github.com/mihonapp/mihon/pull/402), [#415](https://github.com/mihonapp/mihon/pull/415)) | ||||
| - Fix app infinitely retries tracker update instead of failing after 3 tries ([@MajorTanya](https://github.com/MajorTanya)) ([#411](https://github.com/mihonapp/mihon/pull/411)) | ||||
| - Fix crash on Pixel devices ([`ab06720`](https://github.com/mihonapp/mihon/commit/ab067209661eceefc04c65f6bdbfcaa8a1264651)) | ||||
| - Fix crash when opening some heif/heic images ([@az4521](https://github.com/az4521)) ([#466](https://github.com/mihonapp/mihon/pull/466)) | ||||
| - Fix crash in track date selection dialog ([@ivaniskandar](https://github.com/ivaniskandar)) ([`c348fac`](https://github.com/mihonapp/mihon/commit/c348fac78fac479fb123bd617c01c78b9ca851d5)) | ||||
| - Fix dates for saved images on Samsung devices ([@MajorTanya](https://github.com/MajorTanya)) ([#552](https://github.com/mihonapp/mihon/pull/552)) | ||||
| - Fix colors getting distorted when opening CMYK jpeg images ([@wwww-wwww](https://github.com/wwww-wwww)) ([#523](https://github.com/mihonapp/mihon/pull/523)) | ||||
|  | ||||
| ## [v0.16.4] - 2024-02-26 | ||||
| ### Fixed | ||||
| - Circumvent MAL block ([@AntsyLich](https://github.com/AntsyLich)) ([`085ad8d`](https://github.com/mihonapp/mihon/commit/085ad8d44637c375a8ed24aba3a6f75f5b0cc9ee)) | ||||
|  | ||||
| ## [v0.16.3] - 2024-01-30 | ||||
| ### Added | ||||
| - Copy extension debug info when clicking logo or name in the extension details screen ([@MajorTanya](https://github.com/MajorTanya)) ([#271](https://github.com/mihonapp/mihon/pull/271)) | ||||
|  | ||||
| ### Changed | ||||
| - Rename extension update error file to `mihon_update_errors.txt` ([@mjishnu](https://github.com/mjishnu)) ([#253](https://github.com/mihonapp/mihon/pull/253)) | ||||
| - Hide display cutoff setting in reader settings sheet if fullscreen is off ([@Riztard](https://github.com/Riztard)) ([#241](https://github.com/mihonapp/mihon/pull/241)) | ||||
|  | ||||
| ### Fixed | ||||
| - Fix bottom sheet display issues on non-Tablet UI ([@theolm](https://github.com/theolm)) ([#182](https://github.com/mihonapp/mihon/pull/182)) | ||||
| - Fix crash when switching screen while a list is scrolling ([@theolm](https://github.com/theolm)) ([#272](https://github.com/mihonapp/mihon/pull/272)) | ||||
| - Fix newly installed extensions not being recognized by Mihon ([@AwkwardPeak7](https://github.com/AwkwardPeak7)) ([#275](https://github.com/mihonapp/mihon/pull/275)) | ||||
| - Fix error handling when refreshing MAL OAuth token ([@AntsyLich](https://github.com/AntsyLich)) ([`0f4de03`](https://github.com/mihonapp/mihon/commit/0f4de03d7a77b52490dc9a95e96a308b93b26e4f)) | ||||
|  | ||||
| ## [v0.16.2] - 2024-01-28 | ||||
| ### Added | ||||
| - Scanlator filter is now part of Backup ([@jobobby04](https://github.com/jobobby04)) ([#166](https://github.com/mihonapp/mihon/pull/166)) | ||||
|  | ||||
| ### Changed | ||||
| - Rename crash log filename to `mihon_crash_logs.txt` ([@MajorTanya](https://github.com/MajorTanya)) ([#234](https://github.com/mihonapp/mihon/pull/234)) | ||||
|  | ||||
| ### Fixed | ||||
| - "Flash screen on page change" Making the screen goes blank ([@AntsyLich](https://github.com/AntsyLich)) ([`38d6ab8`](https://github.com/mihonapp/mihon/commit/38d6ab80ce868707829dbc81de4170afe3c2f2a5)) | ||||
| - App icon scaling ([@AntsyLich](https://github.com/AntsyLich)) ([`26815c7`](https://github.com/mihonapp/mihon/commit/26815c7356111394665467c1e81255ac9ee33c1a)) | ||||
| - Updating extension not reflecting correctly ([@AntsyLich](https://github.com/AntsyLich)) ([`cb06898`](https://github.com/mihonapp/mihon/commit/cb068984303f811692531bf6f14902ae118d8ac7)) | ||||
| - Inconsistent button height with some languages in "Data and storage" ([@theolm](https://github.com/theolm)) ([#202](https://github.com/mihonapp/mihon/pull/202)) | ||||
| - Fix chapter not being marked as read in some cases with Enhanced Trackers ([@Secozzi](https://github.com/Secozzi)) ([#219](https://github.com/mihonapp/mihon/pull/219))  | ||||
| - And various tracker related fixes ([@AntsyLich](https://github.com/AntsyLich), [@kitsumed](https://github.com/kitsumed), [@Secozzi](https://github.com/Secozzi)) ([`a024218`](https://github.com/mihonapp/mihon/commit/a024218410953a389b8af4880fa7ae6cc30124a2), [`e3f33e2`](https://github.com/mihonapp/mihon/commit/e3f33e24f5e928ac8a85d1f500fd42d4715fc6b5), [`32188f9`](https://github.com/mihonapp/mihon/commit/32188f9f65009a18250674ef1bd6e57d351c1fba)) | ||||
|  | ||||
| ## [v0.16.1] - 2024-01-18 | ||||
| ### Fixed | ||||
| - App Icon not filled ([@AntsyLich](https://github.com/AntsyLich)) ([`1849715`](https://github.com/mihonapp/mihon/commit/18497154183356bb0d469b27827f9f7d6b7a3130)) | ||||
| - MangaUpdates default score being set to -1.0 ([@AntsyLich](https://github.com/AntsyLich)) ([`99fd273`](https://github.com/mihonapp/mihon/commit/99fd2731f5d9d374700e89fa67d4d5bf611bbafa)) | ||||
|  | ||||
| ## [v0.16.0] - 2024-01-16 | ||||
|  | ||||
| "The end of 立ち読み (Tachiyomi) is the beginning of みほん (Mihon)" | ||||
| Credit to LinkCable, the icon designer, for this poetic quote. | ||||
|  | ||||
| What's New? | ||||
| Well, nothing, except you now you need Android 8+ to install the app. | ||||
|  | ||||
| [unreleased]: https://github.com/mihonapp/mihon/compare/v0.16.5...HEAD | ||||
| [v0.16.5]: https://github.com/mihonapp/mihon/compare/v0.16.4...v0.16.5 | ||||
| [v0.16.4]: https://github.com/mihonapp/mihon/compare/v0.16.3...v0.16.4 | ||||
| [v0.16.3]: https://github.com/mihonapp/mihon/compare/v0.16.2...v0.16.3 | ||||
| [v0.16.2]: https://github.com/mihonapp/mihon/compare/v0.16.1...v0.16.2 | ||||
| [v0.16.1]: https://github.com/mihonapp/mihon/compare/v0.16.0...v0.16.1 | ||||
| [v0.16.0]: https://github.com/mihonapp/mihon/releases/tag/v0.16.0 | ||||
| @@ -1,126 +0,0 @@ | ||||
| # Contributor Covenant Code of Conduct | ||||
|  | ||||
| ## Our Pledge | ||||
|  | ||||
| We as members, contributors, and leaders pledge to make participation in our | ||||
| community a harassment-free experience for everyone, regardless of age, body | ||||
| size, visible or invisible disability, ethnicity, sex characteristics, gender | ||||
| identity and expression, level of experience, education, socio-economic status, | ||||
| nationality, personal appearance, race, caste, color, religion, or sexual identity | ||||
| and orientation. | ||||
|  | ||||
| We pledge to act and interact in ways that contribute to an open, welcoming, | ||||
| diverse, inclusive, and healthy community. | ||||
|  | ||||
| ## Our Standards | ||||
|  | ||||
| Examples of behavior that contributes to a positive environment for our | ||||
| community include: | ||||
|  | ||||
| * Demonstrating empathy and kindness toward other people | ||||
| * Being respectful of differing opinions, viewpoints, and experiences | ||||
| * Giving and gracefully accepting constructive feedback | ||||
| * Accepting responsibility and apologizing to those affected by our mistakes, | ||||
|   and learning from the experience | ||||
| * Focusing on what is best not just for us as individuals, but for the | ||||
|   overall community | ||||
|  | ||||
| Examples of unacceptable behavior include: | ||||
|  | ||||
| * The use of sexualized language or imagery, and sexual attention or | ||||
|   advances of any kind | ||||
| * Trolling, insulting or derogatory comments, and personal or political attacks | ||||
| * Public or private harassment | ||||
| * Publishing others' private information, such as a physical or email | ||||
|   address, without their explicit permission | ||||
| * Other conduct which could reasonably be considered inappropriate in a | ||||
|   professional setting | ||||
|  | ||||
| ## Enforcement Responsibilities | ||||
|  | ||||
| Community moderators are responsible for clarifying and enforcing our standards of | ||||
| acceptable behavior and will take appropriate and fair corrective action in | ||||
| response to any behavior that they deem inappropriate, threatening, offensive, | ||||
| or harmful. | ||||
|  | ||||
| Community moderators have the right and responsibility to remove, edit, or reject | ||||
| comments, commits, code, wiki edits, issues, and other contributions that are | ||||
| not aligned to this Code of Conduct, and will communicate reasons for moderation | ||||
| decisions when appropriate. | ||||
|  | ||||
| ## Scope | ||||
|  | ||||
| This Code of Conduct applies within all community spaces, and also applies when | ||||
| an individual is officially representing the community in public spaces. | ||||
| Examples of representing our community include using an official e-mail address, | ||||
| posting via an official social media account, or acting as an appointed | ||||
| representative at an online or offline event. | ||||
|  | ||||
| ## Enforcement | ||||
|  | ||||
| Instances of abusive, harassing, or otherwise unacceptable behavior may be | ||||
| reported to the community moderators responsible for enforcement at | ||||
| the [Mihon Discord server](https://discord.gg/mihon). | ||||
| All complaints will be reviewed and investigated promptly and fairly. | ||||
|  | ||||
| All community moderators are obligated to respect the privacy and security of the | ||||
| reporter of any incident. | ||||
|  | ||||
| ## Enforcement Guidelines | ||||
|  | ||||
| Community moderators will follow these Community Impact Guidelines in determining | ||||
| the consequences for any action they deem in violation of this Code of Conduct: | ||||
|  | ||||
| ### 1. Correction | ||||
|  | ||||
| **Community Impact**: Use of inappropriate language or other behavior deemed | ||||
| unprofessional or unwelcome in the community. | ||||
|  | ||||
| **Consequence**: A private, written warning from community moderators, providing | ||||
| clarity around the nature of the violation and an explanation of why the | ||||
| behavior was inappropriate. A public apology may be requested. | ||||
|  | ||||
| ### 2. Warning | ||||
|  | ||||
| **Community Impact**: A violation through a single incident or series | ||||
| of actions. | ||||
|  | ||||
| **Consequence**: A warning with consequences for continued behavior. No | ||||
| interaction with the people involved, including unsolicited interaction with | ||||
| those enforcing the Code of Conduct, for a specified period of time. This | ||||
| includes avoiding interactions in community spaces as well as external channels | ||||
| like social media. Violating these terms may lead to a temporary or | ||||
| permanent ban. | ||||
|  | ||||
| ### 3. Temporary Ban | ||||
|  | ||||
| **Community Impact**: A serious violation of community standards, including | ||||
| sustained inappropriate behavior. | ||||
|  | ||||
| **Consequence**: A temporary ban from any sort of interaction or public | ||||
| communication with the community for a specified period of time. No public or | ||||
| private interaction with the people involved, including unsolicited interaction | ||||
| with those enforcing the Code of Conduct, is allowed during this period. | ||||
| Violating these terms may lead to a permanent ban. | ||||
|  | ||||
| ### 4. Permanent Ban | ||||
|  | ||||
| **Community Impact**: Demonstrating a pattern of violation of community | ||||
| standards, including sustained inappropriate behavior,  harassment of an | ||||
| individual, or aggression toward or disparagement of classes of individuals. | ||||
|  | ||||
| **Consequence**: A permanent ban from any sort of public interaction within | ||||
| the community. | ||||
|  | ||||
| ## Attribution | ||||
|  | ||||
| This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org/), | ||||
| version 2.1, available at | ||||
| [v2.1](https://www.contributor-covenant.org/version/2/1/code_of_conduct.html). | ||||
|  | ||||
| Community Impact Guidelines were inspired by | ||||
| [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). | ||||
|  | ||||
| For answers to common questions about this code of conduct, see the FAQ at | ||||
| [FAQ](https://www.contributor-covenant.org/faq). Translations are available | ||||
| at [translations](https://www.contributor-covenant.org/translations). | ||||
| @@ -1,49 +0,0 @@ | ||||
| Looking to report an issue/bug or make a feature request? Please refer to the [README file](https://github.com/mihonapp/mihon#issues-feature-requests-and-contributing). | ||||
|  | ||||
| --- | ||||
|  | ||||
| Thanks for your interest in contributing to Mihon! | ||||
|  | ||||
|  | ||||
| # Code contributions | ||||
|  | ||||
| Pull requests are welcome! | ||||
|  | ||||
| If you're interested in taking on [an open issue](https://github.com/mihonapp/mihon/issues), please comment on it so others are aware. | ||||
| You do not need to ask for permission nor an assignment. | ||||
|  | ||||
| ## Prerequisites | ||||
|  | ||||
| Before you start, please note that the ability to use following technologies is **required** and that existing contributors will not actively teach them to you. | ||||
|  | ||||
| - Basic [Android development](https://developer.android.com/) | ||||
| - [Kotlin](https://kotlinlang.org/) | ||||
|  | ||||
| ### Tools | ||||
|  | ||||
| - [Android Studio](https://developer.android.com/studio) | ||||
| - Emulator or phone with developer options enabled to test changes. | ||||
|  | ||||
| ## Getting help | ||||
|  | ||||
| - Join [the Discord server](https://discord.gg/mihon) for online help and to ask questions while developing. | ||||
|  | ||||
| # Translations | ||||
|  | ||||
| Translations are done externally via Weblate. See [our website](https://mihon.app/docs/contribute#translation) for more details. | ||||
|  | ||||
|  | ||||
| # Forks | ||||
|  | ||||
| Forks are allowed so long as they abide by [the project's LICENSE](https://github.com/mihonapp/mihon/blob/main/LICENSE). | ||||
|  | ||||
| When creating a fork, remember to: | ||||
|  | ||||
| - To avoid confusion with the main app: | ||||
|     - Change the app name | ||||
|     - Change the app icon | ||||
|     - Change or disable the [app update checker](https://github.com/mihonapp/mihon/blob/main/app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateChecker.kt) | ||||
| - To avoid installation conflicts: | ||||
|     - Change the `applicationId` in [`build.gradle.kts`](https://github.com/mihonapp/mihon/blob/main/app/build.gradle.kts) | ||||
| - To avoid having your data polluting the main app's analytics and crash report services: | ||||
|     - If you want to use Firebase analytics, replace [`google-services.json`](https://github.com/mihonapp/mihon/blob/main/app/src/standard/google-services.json) with your own | ||||
							
								
								
									
										26
									
								
								LICENSE
									
									
									
									
									
								
							
							
						
						
									
										26
									
								
								LICENSE
									
									
									
									
									
								
							| @@ -174,3 +174,29 @@ | ||||
|       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 {yyyy} {name of copyright owner} | ||||
|  | ||||
|    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. | ||||
|  | ||||
|   | ||||
							
								
								
									
										101
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										101
									
								
								README.md
									
									
									
									
									
								
							| @@ -1,86 +1,41 @@ | ||||
| <div align="center"> | ||||
| | Build | Download | Auto Update | | ||||
| |-------|----------|-------------| | ||||
| | [](https://teamcity.kanade.eu/project.html?projectId=tachiyomi) [](https://travis-ci.org/inorichi/tachiyomi) | [](https://github.com/inorichi/tachiyomi/releases) [](http://tachiyomi.kanade.eu/latest/app-debug.apk) | [](https://f-droid.org/repository/browse/?fdid=eu.kanade.tachiyomi) [](//github.com/inorichi/tachiyomi/wiki/FDroid-for-debug-versions) | | ||||
|  | ||||
| <a href="https://mihon.app"> | ||||
|     <img src="./.github/assets/logo.png" alt="Mihon logo" title="Mihon logo" width="80"/> | ||||
| </a> | ||||
| ## [Report an issue](https://github.com/inorichi/tachiyomi/blob/master/.github/CONTRIBUTING.md) | ||||
|  | ||||
| # Mihon [App](#) | ||||
| **Before reporting a new issue, take a look at the [FAQ](https://github.com/inorichi/tachiyomi/wiki/FAQ), the [changelog](https://github.com/inorichi/tachiyomi/releases) and the already opened issues.** | ||||
|  | ||||
| ### Full-featured reader | ||||
| Discover and read manga, webtoons, comics, and more – easier than ever on your Android device. | ||||
| Tachiyomi is a free and open source manga reader for Android. | ||||
|  | ||||
| [](https://discord.gg/mihon) | ||||
| [](https://github.com/mihonapp/mihon/releases) | ||||
| Keep in mind it's still a beta, so expect it to crash sometimes. | ||||
|  | ||||
| [](https://github.com/mihonapp/mihon/actions/workflows/build_push.yml) | ||||
| [](/LICENSE) | ||||
| [](https://hosted.weblate.org/engage/mihon/) | ||||
| # Features | ||||
|  | ||||
| ## Download | ||||
| * Online and offline reading | ||||
| * Configurable reader with multiple viewers and settings | ||||
| * MyAnimeList support | ||||
| * Resume from the next unread chapter | ||||
| * Chapter filtering | ||||
| * Schedule searching for updates | ||||
| * Categories to organize your library | ||||
|  | ||||
| [](https://github.com/mihonapp/mihon/releases) | ||||
| [](https://github.com/mihonapp/mihon-preview/releases) | ||||
| ## License | ||||
|  | ||||
| *Requires Android 8.0 or higher.* | ||||
|     Copyright 2015 Javier Tomás | ||||
|  | ||||
| ## Features | ||||
|     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 | ||||
|  | ||||
| <div align="left"> | ||||
|     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
| * Local reading of content. | ||||
| * A configurable reader with multiple viewers, reading directions and other settings. | ||||
| * Tracker support: [MyAnimeList](https://myanimelist.net/), [AniList](https://anilist.co/), [Kitsu](https://kitsu.app/), [MangaUpdates](https://mangaupdates.com), [Shikimori](https://shikimori.one), and [Bangumi](https://bgm.tv/) support. | ||||
| * Categories to organize your library. | ||||
| * Light and dark themes. | ||||
| * Schedule updating your library for new chapters. | ||||
| * Create backups locally to read offline or to your desired cloud service. | ||||
| * Plus much more... | ||||
|     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. | ||||
|  | ||||
| </div> | ||||
| ## Disclaimer | ||||
|  | ||||
| ## Contributing | ||||
|  | ||||
| [Code of conduct](./CODE_OF_CONDUCT.md) · [Contributing guide](./CONTRIBUTING.md) | ||||
|  | ||||
| Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. | ||||
|  | ||||
| Before reporting a new issue, take a look at the [FAQ](https://mihon.app/docs/faq/general), the [changelog](https://mihon.app/changelogs/) and the already opened [issues](https://github.com/mihonapp/mihon/issues); if you got any questions, join our [Discord server](https://discord.gg/mihon). | ||||
|  | ||||
|  | ||||
| ### Repositories | ||||
|  | ||||
| [](https://github.com/mihonapp/website/) | ||||
| [](https://github.com/mihonapp/bitmap.kt/) | ||||
|  | ||||
| ### Credits | ||||
|  | ||||
| Thank you to all the people who have contributed! | ||||
|  | ||||
| <a href="https://github.com/mihonapp/mihon/graphs/contributors"> | ||||
|     <img src="https://contrib.rocks/image?repo=mihonapp/mihon" alt="Mihon app contributors" title="Mihon app contributors" width="800"/> | ||||
| </a> | ||||
|  | ||||
| ### Disclaimer | ||||
|  | ||||
| The developer(s) of this application does not have any affiliation with the content providers available, and this application hosts zero content. | ||||
|  | ||||
| ### License | ||||
|  | ||||
| <pre> | ||||
| Copyright © 2015 Javier Tomás | ||||
| Copyright © 2024 The Mihon Open Source Project | ||||
|  | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
|  | ||||
| http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| </pre> | ||||
|  | ||||
| </div> | ||||
| The developer of this application does not have any affiliation with the content providers available. | ||||
|   | ||||
							
								
								
									
										1
									
								
								app/.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								app/.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -1,3 +1,4 @@ | ||||
| /build | ||||
| *iml | ||||
| *.iml | ||||
| .idea | ||||
							
								
								
									
										195
									
								
								app/build.gradle
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										195
									
								
								app/build.gradle
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,195 @@ | ||||
| import java.text.SimpleDateFormat | ||||
|  | ||||
| apply plugin: 'com.android.application' | ||||
| apply plugin: 'kotlin-android' | ||||
| apply plugin: 'kotlin-android-extensions' | ||||
|  | ||||
| ext { | ||||
|     // Git is needed in your system PATH for these commands to work. | ||||
|     // If it's not installed, you can return a random value as a workaround | ||||
|     getCommitCount = { | ||||
|         return 'git rev-list --count origin/master'.execute().text.trim() | ||||
|         // return "1" | ||||
|     } | ||||
|  | ||||
|     getGitSha = { | ||||
|         return 'git rev-parse --short HEAD'.execute().text.trim() | ||||
|         // return "1" | ||||
|     } | ||||
|  | ||||
|     getBuildTime = { | ||||
|         def df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'") | ||||
|         df.setTimeZone(TimeZone.getTimeZone("UTC")) | ||||
|         return df.format(new Date()) | ||||
|     } | ||||
| } | ||||
|  | ||||
| def includeUpdater() { | ||||
|     return hasProperty("include_updater") | ||||
| } | ||||
|  | ||||
| android { | ||||
|     compileSdkVersion 23 | ||||
|     buildToolsVersion "23.0.3" | ||||
|     publishNonDefault true | ||||
|  | ||||
|     defaultConfig { | ||||
|         applicationId "eu.kanade.tachiyomi" | ||||
|         minSdkVersion 16 | ||||
|         targetSdkVersion 23 | ||||
|         testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" | ||||
|         versionCode 7 | ||||
|         versionName "0.2.1" | ||||
|  | ||||
|         buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\"" | ||||
|         buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\"" | ||||
|         buildConfigField "String", "BUILD_TIME", "\"${getBuildTime()}\"" | ||||
|         buildConfigField "boolean", "INCLUDE_UPDATER", "${includeUpdater()}" | ||||
|  | ||||
|         vectorDrawables.useSupportLibrary = true | ||||
|     } | ||||
|  | ||||
|     buildTypes { | ||||
|         debug { | ||||
|             versionNameSuffix ".${getCommitCount()}" | ||||
|             applicationIdSuffix ".debug" | ||||
|         } | ||||
|         release { | ||||
|             minifyEnabled true | ||||
|             shrinkResources true | ||||
|             proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     packagingOptions { | ||||
|         exclude 'META-INF/DEPENDENCIES' | ||||
|         exclude 'LICENSE.txt' | ||||
|         exclude 'META-INF/LICENSE' | ||||
|         exclude 'META-INF/LICENSE.txt' | ||||
|         exclude 'META-INF/NOTICE' | ||||
|     } | ||||
|  | ||||
|     lintOptions { | ||||
|         abortOnError false | ||||
|         checkReleaseBuilds false | ||||
|     } | ||||
|  | ||||
|     sourceSets { | ||||
|         main.java.srcDirs += 'src/main/kotlin' | ||||
|     } | ||||
|  | ||||
|     // http://stackoverflow.com/questions/32759529/androidhttpclient-not-found-when-running-robolectric | ||||
|     useLibrary 'org.apache.http.legacy' | ||||
|  | ||||
| } | ||||
|  | ||||
| kapt { | ||||
|     generateStubs = true | ||||
| } | ||||
|  | ||||
| dependencies { | ||||
|     final SUPPORT_LIBRARY_VERSION = '23.3.0' | ||||
|     final DAGGER_VERSION = '2.2' | ||||
|     final OKHTTP_VERSION = '3.2.0' | ||||
|     final RETROFIT_VERSION = '2.0.1' | ||||
|     final STORIO_VERSION = '1.8.0' | ||||
|     final MOCKITO_VERSION = '1.10.19' | ||||
|  | ||||
|     // Modified dependencies | ||||
|     compile 'com.github.inorichi:subsampling-scale-image-view:421fb81' | ||||
|     compile 'com.github.inorichi:ReactiveNetwork:69092ed' | ||||
|  | ||||
|     // Android support library | ||||
|     compile "com.android.support:support-v4:$SUPPORT_LIBRARY_VERSION" | ||||
|     compile "com.android.support:appcompat-v7:$SUPPORT_LIBRARY_VERSION" | ||||
|     compile "com.android.support:cardview-v7:$SUPPORT_LIBRARY_VERSION" | ||||
|     compile "com.android.support:design:$SUPPORT_LIBRARY_VERSION" | ||||
|     compile "com.android.support:recyclerview-v7:$SUPPORT_LIBRARY_VERSION" | ||||
|     compile "com.android.support:support-annotations:$SUPPORT_LIBRARY_VERSION" | ||||
|     compile "com.android.support:percent:$SUPPORT_LIBRARY_VERSION" | ||||
|     compile "com.android.support:preference-v7:$SUPPORT_LIBRARY_VERSION" | ||||
|     compile "com.android.support:preference-v14:$SUPPORT_LIBRARY_VERSION" | ||||
|  | ||||
|     // ReactiveX | ||||
|     compile 'io.reactivex:rxandroid:1.1.0' | ||||
|     compile 'io.reactivex:rxjava:1.1.1' | ||||
|     compile 'com.f2prateek.rx.preferences:rx-preferences:1.0.1' | ||||
|  | ||||
|     // Network client | ||||
|     compile "com.squareup.okhttp3:okhttp:$OKHTTP_VERSION" | ||||
|     compile "com.squareup.okhttp3:okhttp-urlconnection:$OKHTTP_VERSION" | ||||
|  | ||||
|     // REST | ||||
|     compile "com.squareup.retrofit2:retrofit:$RETROFIT_VERSION" | ||||
|     compile "com.squareup.retrofit2:converter-gson:$RETROFIT_VERSION" | ||||
|     compile "com.squareup.retrofit2:adapter-rxjava:$RETROFIT_VERSION" | ||||
|  | ||||
|     // IO | ||||
|     compile 'com.squareup.okio:okio:1.7.0' | ||||
|  | ||||
|     // JSON | ||||
|     compile 'com.google.code.gson:gson:2.6.2' | ||||
|  | ||||
|     // Disk cache | ||||
|     compile 'com.jakewharton:disklrucache:2.0.2' | ||||
|  | ||||
|     // Parse HTML | ||||
|     compile 'org.jsoup:jsoup:1.8.3' | ||||
|  | ||||
|     // Database | ||||
|     compile "com.pushtorefresh.storio:sqlite:$STORIO_VERSION" | ||||
|     compile "com.pushtorefresh.storio:sqlite-annotations:$STORIO_VERSION" | ||||
|     kapt "com.pushtorefresh.storio:sqlite-annotations-processor:$STORIO_VERSION" | ||||
|  | ||||
|     // Model View Presenter | ||||
|     compile 'info.android15.nucleus:nucleus:3.0.0-beta' | ||||
|  | ||||
|     // Dependency injection | ||||
|     compile "com.google.dagger:dagger:$DAGGER_VERSION" | ||||
|     kapt "com.google.dagger:dagger-compiler:$DAGGER_VERSION" | ||||
|     provided 'org.glassfish:javax.annotation:10.0-b28' | ||||
|  | ||||
|     // Image library | ||||
|     compile 'com.github.bumptech.glide:glide:3.7.0' | ||||
|  | ||||
|     // Logging | ||||
|     compile 'com.jakewharton.timber:timber:4.1.2' | ||||
|  | ||||
|     // Crash reports | ||||
|     compile 'ch.acra:acra:4.8.5' | ||||
|  | ||||
|     // UI | ||||
|     compile 'com.github.dmytrodanylyk.android-process-button:library:1.0.4' | ||||
|     compile 'eu.davidea:flexible-adapter:4.2.0' | ||||
|     compile 'com.nononsenseapps:filepicker:2.5.2' | ||||
|     compile 'com.github.amulyakhare:TextDrawable:558677e' | ||||
|     compile('com.github.afollestad.material-dialogs:core:0.8.5.5@aar') { | ||||
|         transitive = true | ||||
|     } | ||||
|  | ||||
|     // Tests | ||||
|     testCompile 'junit:junit:4.12' | ||||
|     testCompile 'org.assertj:assertj-core:1.7.1' | ||||
|     testCompile "org.mockito:mockito-core:$MOCKITO_VERSION" | ||||
|     testCompile('org.robolectric:robolectric:3.0') { | ||||
|         exclude group: 'commons-logging', module: 'commons-logging' | ||||
|         exclude group: 'org.apache.httpcomponents', module: 'httpclient' | ||||
|     } | ||||
|  | ||||
|     kaptTest "com.google.dagger:dagger-compiler:$DAGGER_VERSION" | ||||
|     compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" | ||||
| } | ||||
|  | ||||
| buildscript { | ||||
|     ext.kotlin_version = '1.0.1' | ||||
|     repositories { | ||||
|         mavenCentral() | ||||
|     } | ||||
|     dependencies { | ||||
|         classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" | ||||
|     } | ||||
| } | ||||
|  | ||||
| repositories { | ||||
|     mavenCentral() | ||||
| } | ||||
| @@ -1,308 +0,0 @@ | ||||
| import mihon.buildlogic.getBuildTime | ||||
| import mihon.buildlogic.getCommitCount | ||||
| import mihon.buildlogic.getGitSha | ||||
| import org.jetbrains.kotlin.gradle.tasks.KotlinCompile | ||||
|  | ||||
| plugins { | ||||
|     id("mihon.android.application") | ||||
|     id("mihon.android.application.compose") | ||||
|     id("com.github.zellius.shortcut-helper") | ||||
|     kotlin("plugin.serialization") | ||||
|     alias(libs.plugins.aboutLibraries) | ||||
| } | ||||
|  | ||||
| if (gradle.startParameter.taskRequests.toString().contains("Standard")) { | ||||
|     pluginManager.apply { | ||||
|         apply(libs.plugins.google.services.get().pluginId) | ||||
|         apply(libs.plugins.firebase.crashlytics.get().pluginId) | ||||
|     } | ||||
| } | ||||
|  | ||||
| shortcutHelper.setFilePath("./shortcuts.xml") | ||||
|  | ||||
| val supportedAbis = setOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64") | ||||
|  | ||||
| android { | ||||
|     namespace = "eu.kanade.tachiyomi" | ||||
|  | ||||
|     defaultConfig { | ||||
|         applicationId = "app.mihon" | ||||
|  | ||||
|         versionCode = 7 | ||||
|         versionName = "0.16.5" | ||||
|  | ||||
|         buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"") | ||||
|         buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"") | ||||
|         buildConfigField("String", "BUILD_TIME", "\"${getBuildTime()}\"") | ||||
|         buildConfigField("boolean", "INCLUDE_UPDATER", "false") | ||||
|         buildConfigField("boolean", "PREVIEW", "false") | ||||
|  | ||||
|         ndk { | ||||
|             abiFilters += supportedAbis | ||||
|         } | ||||
|  | ||||
|         testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" | ||||
|     } | ||||
|  | ||||
|     splits { | ||||
|         abi { | ||||
|             isEnable = true | ||||
|             reset() | ||||
|             include(*supportedAbis.toTypedArray()) | ||||
|             isUniversalApk = true | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     buildTypes { | ||||
|         named("debug") { | ||||
|             versionNameSuffix = "-${getCommitCount()}" | ||||
|             applicationIdSuffix = ".debug" | ||||
|             isPseudoLocalesEnabled = true | ||||
|         } | ||||
|         named("release") { | ||||
|             isShrinkResources = true | ||||
|             isMinifyEnabled = true | ||||
|             proguardFiles("proguard-android-optimize.txt", "proguard-rules.pro") | ||||
|         } | ||||
|         create("preview") { | ||||
|             initWith(getByName("release")) | ||||
|             buildConfigField("boolean", "PREVIEW", "true") | ||||
|  | ||||
|             signingConfig = signingConfigs.getByName("debug") | ||||
|             matchingFallbacks.add("release") | ||||
|             val debugType = getByName("debug") | ||||
|             versionNameSuffix = debugType.versionNameSuffix | ||||
|             applicationIdSuffix = debugType.applicationIdSuffix | ||||
|         } | ||||
|         create("benchmark") { | ||||
|             initWith(getByName("release")) | ||||
|  | ||||
|             signingConfig = signingConfigs.getByName("debug") | ||||
|             matchingFallbacks.add("release") | ||||
|             isDebuggable = false | ||||
|             isProfileable = true | ||||
|             versionNameSuffix = "-benchmark" | ||||
|             applicationIdSuffix = ".benchmark" | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     sourceSets { | ||||
|         getByName("preview").res.srcDirs("src/debug/res") | ||||
|         getByName("benchmark").res.srcDirs("src/debug/res") | ||||
|     } | ||||
|  | ||||
|     flavorDimensions.add("default") | ||||
|  | ||||
|     productFlavors { | ||||
|         create("standard") { | ||||
|             buildConfigField("boolean", "INCLUDE_UPDATER", "true") | ||||
|             dimension = "default" | ||||
|         } | ||||
|         create("dev") { | ||||
|             // Include pseudolocales: https://developer.android.com/guide/topics/resources/pseudolocales | ||||
|             resourceConfigurations.addAll(listOf("en", "en_XA", "ar_XB", "xxhdpi")) | ||||
|             dimension = "default" | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     packaging { | ||||
|         resources.excludes.addAll( | ||||
|             listOf( | ||||
|                 "kotlin-tooling-metadata.json", | ||||
|                 "META-INF/DEPENDENCIES", | ||||
|                 "LICENSE.txt", | ||||
|                 "META-INF/LICENSE", | ||||
|                 "META-INF/**/LICENSE.txt", | ||||
|                 "META-INF/*.properties", | ||||
|                 "META-INF/**/*.properties", | ||||
|                 "META-INF/README.md", | ||||
|                 "META-INF/NOTICE", | ||||
|                 "META-INF/*.version", | ||||
|             ), | ||||
|         ) | ||||
|     } | ||||
|  | ||||
|     dependenciesInfo { | ||||
|         includeInApk = false | ||||
|     } | ||||
|  | ||||
|     buildFeatures { | ||||
|         viewBinding = true | ||||
|         buildConfig = true | ||||
|  | ||||
|         // Disable some unused things | ||||
|         aidl = false | ||||
|         renderScript = false | ||||
|         shaders = false | ||||
|     } | ||||
|  | ||||
|     lint { | ||||
|         abortOnError = false | ||||
|         checkReleaseBuilds = false | ||||
|     } | ||||
| } | ||||
|  | ||||
| dependencies { | ||||
|     implementation(projects.i18n) | ||||
|     implementation(projects.core.archive) | ||||
|     implementation(projects.core.common) | ||||
|     implementation(projects.coreMetadata) | ||||
|     implementation(projects.sourceApi) | ||||
|     implementation(projects.sourceLocal) | ||||
|     implementation(projects.data) | ||||
|     implementation(projects.domain) | ||||
|     implementation(projects.presentationCore) | ||||
|     implementation(projects.presentationWidget) | ||||
|  | ||||
|     // Compose | ||||
|     implementation(compose.activity) | ||||
|     implementation(compose.foundation) | ||||
|     implementation(compose.material3.core) | ||||
|     implementation(compose.material.icons) | ||||
|     implementation(compose.animation) | ||||
|     implementation(compose.animation.graphics) | ||||
|     debugImplementation(compose.ui.tooling) | ||||
|     implementation(compose.ui.tooling.preview) | ||||
|     implementation(compose.ui.util) | ||||
|  | ||||
|     implementation(androidx.interpolator) | ||||
|  | ||||
|     implementation(androidx.paging.runtime) | ||||
|     implementation(androidx.paging.compose) | ||||
|  | ||||
|     implementation(libs.bundles.sqlite) | ||||
|  | ||||
|     implementation(kotlinx.reflect) | ||||
|     implementation(kotlinx.immutables) | ||||
|  | ||||
|     implementation(platform(kotlinx.coroutines.bom)) | ||||
|     implementation(kotlinx.bundles.coroutines) | ||||
|  | ||||
|     // AndroidX libraries | ||||
|     implementation(androidx.annotation) | ||||
|     implementation(androidx.appcompat) | ||||
|     implementation(androidx.biometricktx) | ||||
|     implementation(androidx.constraintlayout) | ||||
|     implementation(androidx.corektx) | ||||
|     implementation(androidx.splashscreen) | ||||
|     implementation(androidx.recyclerview) | ||||
|     implementation(androidx.viewpager) | ||||
|     implementation(androidx.profileinstaller) | ||||
|  | ||||
|     implementation(androidx.bundles.lifecycle) | ||||
|  | ||||
|     // Job scheduling | ||||
|     implementation(androidx.workmanager) | ||||
|  | ||||
|     // RxJava | ||||
|     implementation(libs.rxjava) | ||||
|  | ||||
|     // Networking | ||||
|     implementation(libs.bundles.okhttp) | ||||
|     implementation(libs.okio) | ||||
|     implementation(libs.conscrypt.android) // TLS 1.3 support for Android < 10 | ||||
|  | ||||
|     // Data serialization (JSON, protobuf, xml) | ||||
|     implementation(kotlinx.bundles.serialization) | ||||
|  | ||||
|     // HTML parser | ||||
|     implementation(libs.jsoup) | ||||
|  | ||||
|     // Disk | ||||
|     implementation(libs.disklrucache) | ||||
|     implementation(libs.unifile) | ||||
|  | ||||
|     // Preferences | ||||
|     implementation(libs.preferencektx) | ||||
|  | ||||
|     // Dependency injection | ||||
|     implementation(libs.injekt) | ||||
|  | ||||
|     // Image loading | ||||
|     implementation(platform(libs.coil.bom)) | ||||
|     implementation(libs.bundles.coil) | ||||
|     implementation(libs.subsamplingscaleimageview) { | ||||
|         exclude(module = "image-decoder") | ||||
|     } | ||||
|     implementation(libs.image.decoder) | ||||
|  | ||||
|     // UI libraries | ||||
|     implementation(libs.material) | ||||
|     implementation(libs.flexible.adapter.core) | ||||
|     implementation(libs.photoview) | ||||
|     implementation(libs.directionalviewpager) { | ||||
|         exclude(group = "androidx.viewpager", module = "viewpager") | ||||
|     } | ||||
|     implementation(libs.insetter) | ||||
|     implementation(libs.bundles.richtext) | ||||
|     implementation(libs.aboutLibraries.compose) | ||||
|     implementation(libs.bundles.voyager) | ||||
|     implementation(libs.compose.materialmotion) | ||||
|     implementation(libs.swipe) | ||||
|     implementation(libs.compose.webview) | ||||
|     implementation(libs.compose.grid) | ||||
|  | ||||
|     // Logging | ||||
|     implementation(libs.logcat) | ||||
|  | ||||
|     // Crash reports/analytics | ||||
|     "standardImplementation"(platform(libs.firebase.bom)) | ||||
|     "standardImplementation"(libs.firebase.analytics) | ||||
|     "standardImplementation"(libs.firebase.crashlytics) | ||||
|  | ||||
|     // Shizuku | ||||
|     implementation(libs.bundles.shizuku) | ||||
|  | ||||
|     // Tests | ||||
|     testImplementation(libs.bundles.test) | ||||
|  | ||||
|     // For detecting memory leaks; see https://square.github.io/leakcanary/ | ||||
|     // debugImplementation(libs.leakcanary.android) | ||||
|     implementation(libs.leakcanary.plumber) | ||||
|  | ||||
|     testImplementation(kotlinx.coroutines.test) | ||||
| } | ||||
|  | ||||
| androidComponents { | ||||
|     beforeVariants { variantBuilder -> | ||||
|         // Disables standardBenchmark | ||||
|         if (variantBuilder.buildType == "benchmark") { | ||||
|             variantBuilder.enable = variantBuilder.productFlavors.containsAll( | ||||
|                 listOf("default" to "dev"), | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
|     onVariants(selector().withFlavor("default" to "standard")) { | ||||
|         // Only excluding in standard flavor because this breaks | ||||
|         // Layout Inspector's Compose tree | ||||
|         it.packaging.resources.excludes.add("META-INF/*.version") | ||||
|     } | ||||
| } | ||||
|  | ||||
| tasks { | ||||
|     // See https://kotlinlang.org/docs/reference/experimental.html#experimental-status-of-experimental-api(-markers) | ||||
|     withType<KotlinCompile> { | ||||
|         compilerOptions.freeCompilerArgs.addAll( | ||||
|             "-Xcontext-receivers", | ||||
|             "-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi", | ||||
|             "-opt-in=androidx.compose.material.ExperimentalMaterialApi", | ||||
|             "-opt-in=androidx.compose.material3.ExperimentalMaterial3Api", | ||||
|             "-opt-in=androidx.compose.material.ExperimentalMaterialApi", | ||||
|             "-opt-in=androidx.compose.ui.ExperimentalComposeUiApi", | ||||
|             "-opt-in=androidx.compose.foundation.ExperimentalFoundationApi", | ||||
|             "-opt-in=androidx.compose.animation.ExperimentalAnimationApi", | ||||
|             "-opt-in=androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi", | ||||
|             "-opt-in=coil3.annotation.ExperimentalCoilApi", | ||||
|             "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi", | ||||
|             "-opt-in=kotlinx.coroutines.FlowPreview", | ||||
|             "-opt-in=kotlinx.coroutines.InternalCoroutinesApi", | ||||
|             "-opt-in=kotlinx.serialization.ExperimentalSerializationApi", | ||||
|         ) | ||||
|     } | ||||
| } | ||||
|  | ||||
| buildscript { | ||||
|     dependencies { | ||||
|         classpath(kotlinx.gradle) | ||||
|     } | ||||
| } | ||||
| @@ -1,34 +0,0 @@ | ||||
| -dontusemixedcaseclassnames | ||||
| -ignorewarnings | ||||
| -verbose | ||||
|  | ||||
| -keepattributes *Annotation* | ||||
|  | ||||
| -keepclasseswithmembernames,includedescriptorclasses class * { | ||||
|     native <methods>; | ||||
| } | ||||
|  | ||||
| -keepclassmembers enum * { | ||||
|     public static **[] values(); | ||||
|     public static ** valueOf(java.lang.String); | ||||
| } | ||||
|  | ||||
| -keepclassmembers class * implements android.os.Parcelable { | ||||
|     public static final ** CREATOR; | ||||
| } | ||||
|  | ||||
| -keep class androidx.annotation.Keep | ||||
|  | ||||
| -keep @androidx.annotation.Keep class * {*;} | ||||
|  | ||||
| -keepclasseswithmembers class * { | ||||
|     @androidx.annotation.Keep <methods>; | ||||
| } | ||||
|  | ||||
| -keepclasseswithmembers class * { | ||||
|     @androidx.annotation.Keep <fields>; | ||||
| } | ||||
|  | ||||
| -keepclasseswithmembers class * { | ||||
|     @androidx.annotation.Keep <init>(...); | ||||
| } | ||||
							
								
								
									
										145
									
								
								app/proguard-rules.pro
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										145
									
								
								app/proguard-rules.pro
									
									
									
									
										vendored
									
									
								
							| @@ -1,31 +1,30 @@ | ||||
| -dontobfuscate | ||||
|  | ||||
| -keep,allowoptimization class eu.kanade.** | ||||
| -keep,allowoptimization class tachiyomi.** | ||||
| -keep,allowoptimization class mihon.** | ||||
| -keep class eu.kanade.tachiyomi.injection.** { *; } | ||||
|  | ||||
| # Keep common dependencies used in extensions | ||||
| -keep,allowoptimization class androidx.preference.** { public protected *; } | ||||
| -keep,allowoptimization class kotlin.** { public protected *; } | ||||
| -keep,allowoptimization class kotlinx.coroutines.** { public protected *; } | ||||
| -keep,allowoptimization class kotlinx.serialization.** { public protected *; } | ||||
| -keep,allowoptimization class kotlin.time.** { public protected *; } | ||||
| -keep,allowoptimization class okhttp3.** { public protected *; } | ||||
| -keep,allowoptimization class okio.** { public protected *; } | ||||
| -keep,allowoptimization class org.jsoup.** { public protected *; } | ||||
| -keep,allowoptimization class rx.** { public protected *; } | ||||
| -keep,allowoptimization class app.cash.quickjs.** { public protected *; } | ||||
| -keep,allowoptimization class uy.kohesive.injekt.** { public protected *; } | ||||
| # OkHttp | ||||
| -keepattributes Signature | ||||
| -keepattributes *Annotation* | ||||
| -keep class okhttp3.** { *; } | ||||
| -keep interface okhttp3.** { *; } | ||||
| -dontwarn okhttp3.** | ||||
| -dontwarn okio.** | ||||
|  | ||||
| # From extensions-lib | ||||
| -keep,allowoptimization class eu.kanade.tachiyomi.network.interceptor.RateLimitInterceptorKt { public protected *; } | ||||
| -keep,allowoptimization class eu.kanade.tachiyomi.network.interceptor.SpecificHostRateLimitInterceptorKt { public protected *; } | ||||
| -keep,allowoptimization class eu.kanade.tachiyomi.network.NetworkHelper { public protected *; } | ||||
| -keep,allowoptimization class eu.kanade.tachiyomi.network.OkHttpExtensionsKt { public protected *; } | ||||
| -keep,allowoptimization class eu.kanade.tachiyomi.network.RequestsKt { public protected *; } | ||||
| -keep,allowoptimization class eu.kanade.tachiyomi.AppInfo { public protected *; } | ||||
| # Okio | ||||
| -keep class sun.misc.Unsafe { *; } | ||||
| -dontwarn java.nio.file.* | ||||
| -dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement | ||||
| -dontwarn okio.** | ||||
|  | ||||
| ##---------------Begin: proguard configuration for RxJava 1.x  ---------- | ||||
| # Glide specific rules # | ||||
| # https://github.com/bumptech/glide | ||||
| -keep public class * implements com.bumptech.glide.module.GlideModule | ||||
| -keep public enum com.bumptech.glide.load.resource.bitmap.ImageHeaderParser$** { | ||||
|     **[] $VALUES; | ||||
|     public *; | ||||
| } | ||||
|  | ||||
| # RxJava 1.1.0 | ||||
| -dontwarn sun.misc.** | ||||
|  | ||||
| -keepclassmembers class rx.internal.util.unsafe.*ArrayQueue*Field* { | ||||
| @@ -41,42 +40,82 @@ | ||||
|     rx.internal.util.atomic.LinkedQueueNode consumerNode; | ||||
| } | ||||
|  | ||||
| -dontnote rx.internal.util.PlatformDependent | ||||
| ##---------------End: proguard configuration for RxJava 1.x  ---------- | ||||
| # Retrofit 2.X | ||||
| ## https://square.github.io/retrofit/ ## | ||||
|  | ||||
| ##---------------Begin: proguard configuration for okhttp  ---------- | ||||
| -keepclasseswithmembers class okhttp3.MultipartBody$Builder { *; } | ||||
| ##---------------End: proguard configuration for okhttp  ---------- | ||||
| -dontwarn retrofit2.** | ||||
| -keep class retrofit2.** { *; } | ||||
| -keepattributes Signature | ||||
| -keepattributes Exceptions | ||||
|  | ||||
| ##---------------Begin: proguard configuration for kotlinx.serialization  ---------- | ||||
| -keepattributes *Annotation*, InnerClasses | ||||
| -dontnote kotlinx.serialization.** # core serialization annotations | ||||
|  | ||||
| # kotlinx-serialization-json specific. Add this if you have java.lang.NoClassDefFoundError kotlinx.serialization.json.JsonObjectSerializer | ||||
| -keepclassmembers class kotlinx.serialization.json.** { | ||||
|     *** Companion; | ||||
| } | ||||
| -keepclasseswithmembers class kotlinx.serialization.json.** { | ||||
|     kotlinx.serialization.KSerializer serializer(...); | ||||
| -keepclasseswithmembers class * { | ||||
|     @retrofit2.http.* <methods>; | ||||
| } | ||||
|  | ||||
| -keep,includedescriptorclasses class eu.kanade.**$$serializer { *; } | ||||
| -keepclassmembers class eu.kanade.** { | ||||
|     *** Companion; | ||||
| } | ||||
| -keepclasseswithmembers class eu.kanade.** { | ||||
|     kotlinx.serialization.KSerializer serializer(...); | ||||
| # AppCombat | ||||
| -keep public class android.support.v7.widget.** { *; } | ||||
| -keep public class android.support.v7.internal.widget.** { *; } | ||||
| -keep public class android.support.v7.internal.view.menu.** { *; } | ||||
|  | ||||
| -keep public class * extends android.support.v4.view.ActionProvider { | ||||
|     public <init>(android.content.Context); | ||||
| } | ||||
|  | ||||
| -keep class kotlinx.serialization.** | ||||
| -keepclassmembers class kotlinx.serialization.** { | ||||
|     <methods>; | ||||
| ## GSON 2.2.4 specific rules ## | ||||
|  | ||||
| # Gson uses generic type information stored in a class file when working with fields. Proguard | ||||
| # removes such information by default, so configure it to keep all of it. | ||||
| -keepattributes Signature | ||||
|  | ||||
| # For using GSON @Expose annotation | ||||
| -keepattributes *Annotation* | ||||
|  | ||||
| -keepattributes EnclosingMethod | ||||
|  | ||||
| # Gson specific classes | ||||
| -keep class sun.misc.Unsafe { *; } | ||||
| -keep class com.google.gson.stream.** { *; } | ||||
|  | ||||
| ## ACRA 4.5.0 specific rules ## | ||||
|  | ||||
| # we need line numbers in our stack traces otherwise they are pretty useless | ||||
| -renamesourcefileattribute SourceFile | ||||
| -keepattributes SourceFile,LineNumberTable | ||||
|  | ||||
| # ACRA needs "annotations" so add this... | ||||
| -keepattributes *Annotation* | ||||
|  | ||||
| # keep this class so that logging will show 'ACRA' and not a obfuscated name like 'a'. | ||||
| # Note: if you are removing log messages elsewhere in this file then this isn't necessary | ||||
| -keep class org.acra.ACRA { | ||||
| 	*; | ||||
| } | ||||
| ##---------------End: proguard configuration for kotlinx.serialization  ---------- | ||||
|  | ||||
| # XmlUtil | ||||
| -keep public enum nl.adaptivity.xmlutil.EventType { *; } | ||||
| # keep this around for some enums that ACRA needs | ||||
| -keep class org.acra.ReportingInteractionMode { | ||||
|     *; | ||||
| } | ||||
|  | ||||
| # Firebase | ||||
| -keep class com.google.firebase.installations.** { *; } | ||||
| -keep interface com.google.firebase.installations.** { *; } | ||||
| -keepnames class org.acra.sender.HttpSender$** { | ||||
|     *; | ||||
| } | ||||
|  | ||||
| -keepnames class org.acra.ReportField { | ||||
|     *; | ||||
| } | ||||
|  | ||||
| # keep this otherwise it is removed by ProGuard | ||||
| -keep public class org.acra.ErrorReporter { | ||||
|     public void addCustomData(java.lang.String,java.lang.String); | ||||
|     public void putCustomData(java.lang.String,java.lang.String); | ||||
|     public void removeCustomData(java.lang.String); | ||||
| } | ||||
|  | ||||
| # keep this otherwise it is removed by ProGuard | ||||
| -keep public class org.acra.ErrorReporter { | ||||
|     public void handleSilentException(java.lang.Throwable); | ||||
| } | ||||
|  | ||||
| # Keep the support library | ||||
| -keep class org.acra.** { *; } | ||||
| -keep interface org.acra.** { *; } | ||||
| @@ -1,46 +0,0 @@ | ||||
| <shortcuts xmlns:android="http://schemas.android.com/apk/res/android"> | ||||
|     <shortcut | ||||
|         android:enabled="true" | ||||
|         android:icon="@drawable/sc_collections_bookmark_48dp" | ||||
|         android:shortcutDisabledMessage="@string/app_not_available" | ||||
|         android:shortcutId="show_library" | ||||
|         android:shortcutLongLabel="@string/label_library" | ||||
|         android:shortcutShortLabel="@string/label_library"> | ||||
|         <intent | ||||
|             android:action="eu.kanade.tachiyomi.SHOW_LIBRARY" | ||||
|             android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity" /> | ||||
|     </shortcut> | ||||
|     <shortcut | ||||
|         android:enabled="true" | ||||
|         android:icon="@drawable/sc_new_releases_48dp" | ||||
|         android:shortcutDisabledMessage="@string/app_not_available" | ||||
|         android:shortcutId="show_recently_updated" | ||||
|         android:shortcutLongLabel="@string/label_recent_updates" | ||||
|         android:shortcutShortLabel="@string/label_recent_updates"> | ||||
|         <intent | ||||
|             android:action="eu.kanade.tachiyomi.SHOW_RECENTLY_UPDATED" | ||||
|             android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity" /> | ||||
|     </shortcut> | ||||
|     <shortcut | ||||
|         android:enabled="true" | ||||
|         android:icon="@drawable/sc_history_48dp" | ||||
|         android:shortcutDisabledMessage="@string/app_not_available" | ||||
|         android:shortcutId="show_recently_read" | ||||
|         android:shortcutLongLabel="@string/label_recent_manga" | ||||
|         android:shortcutShortLabel="@string/label_recent_manga"> | ||||
|         <intent | ||||
|             android:action="eu.kanade.tachiyomi.SHOW_RECENTLY_READ" | ||||
|             android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity" /> | ||||
|     </shortcut> | ||||
|     <shortcut | ||||
|         android:enabled="true" | ||||
|         android:icon="@drawable/sc_explore_48dp" | ||||
|         android:shortcutDisabledMessage="@string/app_not_available" | ||||
|         android:shortcutId="show_catalogues" | ||||
|         android:shortcutLongLabel="@string/browse" | ||||
|         android:shortcutShortLabel="@string/browse"> | ||||
|         <intent | ||||
|             android:action="eu.kanade.tachiyomi.SHOW_CATALOGUES" | ||||
|             android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity" /> | ||||
|     </shortcut> | ||||
| </shortcuts> | ||||
| @@ -1,23 +0,0 @@ | ||||
| <vector xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     android:width="108dp" | ||||
|     android:height="108dp" | ||||
|     android:viewportWidth="432" | ||||
|     android:viewportHeight="432"> | ||||
|   <group> | ||||
|     <clip-path | ||||
|         android:pathData="M0,0h432v432h-432z"/> | ||||
|     <path | ||||
|         android:pathData="M0,0h432v432h-432z" | ||||
|         android:fillColor="#FAFAFA"/> | ||||
|     <path | ||||
|         android:pathData="M0,0h432v432h-432z" | ||||
|         android:fillColor="#2E3943"/> | ||||
|     <path | ||||
|         android:pathData="M322.13,215.5C322.13,272.66 274.64,319 216.07,319C157.49,319 110,272.66 110,215.5C110,158.34 157.49,112 216.07,112C274.64,112 322.13,158.34 322.13,215.5Z" | ||||
|         android:fillColor="#F2FAFF"/> | ||||
|     <path | ||||
|         android:pathData="M216.07,299.59C263.66,299.59 302.24,261.94 302.24,215.5C302.24,169.06 263.66,131.41 216.07,131.41C168.47,131.41 129.89,169.06 129.89,215.5C129.89,261.94 168.47,299.59 216.07,299.59ZM216.07,319C274.64,319 322.13,272.66 322.13,215.5C322.13,158.34 274.64,112 216.07,112C157.49,112 110,158.34 110,215.5C110,272.66 157.49,319 216.07,319Z" | ||||
|         android:fillColor="#7EBBED" | ||||
|         android:fillType="evenOdd"/> | ||||
|   </group> | ||||
| </vector> | ||||
| @@ -1,9 +0,0 @@ | ||||
| <vector xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     android:width="108dp" | ||||
|     android:height="108dp" | ||||
|     android:viewportWidth="432" | ||||
|     android:viewportHeight="432"> | ||||
|   <path | ||||
|       android:pathData="M182.03,188.7L181.33,172.69C183.42,173.09 185.91,173.19 191.57,173.19C198.44,173.19 207.49,172.79 212.16,172.19C214.15,171.99 214.95,171.7 216.24,171L226.98,180.15C225.98,181.54 225.68,182.14 224.59,184.92C223.7,187.11 219.62,199.74 218.03,205.11C225.39,206.6 229.46,207.7 235.03,209.98C235.73,205.11 235.83,202.52 235.83,193.67C235.83,191.39 235.73,190.09 235.43,188.01L252.74,188.6C252.24,190.99 252.14,191.98 252.04,195.86C251.64,205.21 251.24,209.68 250.25,216.45C257.11,219.93 257.11,219.93 260.59,221.82C262.38,222.81 262.78,223.01 263.97,223.41L258.2,242.01C255.42,239.52 251.54,236.83 245.87,233.65C240.9,245.49 232.65,254.14 220.12,261C215.94,255.43 212.76,252.05 207.68,248.07C215.04,244.59 218.43,242.4 222.3,238.72C226.08,235.04 228.57,231.46 230.96,226.09C224.59,223.21 220.51,221.92 213.45,220.43C209.38,232.56 206.09,240.32 203.21,244.99C199.33,251.25 194.06,254.54 187.99,254.54C183.32,254.54 178.55,252.45 175.07,248.87C171.09,244.79 169,239.12 169,232.56C169,222.81 173.67,214.36 181.83,209.09C187.1,205.71 192.67,204.21 201.52,203.72C203.31,197.85 204.8,192.78 206.19,187.11C201.82,187.51 196.35,187.81 189.68,188.1C186.1,188.2 184.91,188.3 182.03,188.7ZM197.14,218.93C192.47,219.73 189.68,221.22 187.2,224.4C185.31,226.59 184.41,229.18 184.41,231.96C184.41,235.04 185.91,237.33 187.8,237.33C190.08,237.33 192.67,232.16 197.14,218.93Z" | ||||
|       android:fillColor="#031019"/> | ||||
| </vector> | ||||
| @@ -1,235 +1,108 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <manifest xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     xmlns:tools="http://schemas.android.com/tools"> | ||||
|           package="eu.kanade.tachiyomi"> | ||||
|  | ||||
|     <!-- Internet --> | ||||
|     <uses-permission android:name="android.permission.INTERNET" /> | ||||
|     <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> | ||||
|     <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> | ||||
|  | ||||
|     <!-- Storage --> | ||||
|     <uses-permission | ||||
|         android:name="android.permission.WRITE_EXTERNAL_STORAGE" | ||||
|         tools:ignore="ScopedStorage" /> | ||||
|  | ||||
|     <!-- For background jobs --> | ||||
|     <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> | ||||
|     <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/> | ||||
|     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> | ||||
|     <uses-permission android:name="android.permission.WAKE_LOCK" /> | ||||
|     <uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" /> | ||||
|  | ||||
|     <!-- For managing extensions --> | ||||
|     <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" /> | ||||
|     <uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" /> | ||||
|     <uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" /> | ||||
|     <!-- To view extension packages in API 30+ --> | ||||
|     <uses-permission | ||||
|         android:name="android.permission.QUERY_ALL_PACKAGES" | ||||
|         tools:ignore="QueryAllPackagesPermission" /> | ||||
|  | ||||
|     <uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> | ||||
|     <uses-permission | ||||
|         android:name="android.permission.READ_APP_SPECIFIC_LOCALES" | ||||
|         tools:ignore="ProtectedPermissions" /> | ||||
|  | ||||
|     <uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" /> | ||||
|     <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/> | ||||
|  | ||||
|     <application | ||||
|         android:name=".App" | ||||
|         android:allowBackup="false" | ||||
|         android:enableOnBackInvokedCallback="true" | ||||
|         android:allowBackup="true" | ||||
|         android:hardwareAccelerated="true" | ||||
|         android:icon="@mipmap/ic_launcher" | ||||
|         android:label="@string/app_name" | ||||
|         android:largeHeap="true" | ||||
|         android:localeConfig="@xml/locales_config" | ||||
|         android:networkSecurityConfig="@xml/network_security_config" | ||||
|         android:preserveLegacyExternalStorage="true" | ||||
|         android:requestLegacyExternalStorage="true" | ||||
|         android:roundIcon="@mipmap/ic_launcher" | ||||
|         android:supportsRtl="true" | ||||
|         android:theme="@style/Theme.Tachiyomi"> | ||||
|  | ||||
|         android:theme="@style/Theme.Tachiyomi" > | ||||
|         <activity | ||||
|             android:name=".ui.main.MainActivity" | ||||
|             android:exported="true" | ||||
|             android:launchMode="singleTop" | ||||
|             android:theme="@style/Theme.Tachiyomi.SplashScreen"> | ||||
|             android:theme="@style/Theme.BrandedLaunch"> | ||||
|             <intent-filter> | ||||
|                 <action android:name="android.intent.action.MAIN" /> | ||||
|  | ||||
|                 <category android:name="android.intent.category.LAUNCHER" /> | ||||
|             </intent-filter> | ||||
|  | ||||
|             <!-- Deep link to add repos --> | ||||
|             <intent-filter android:label="@string/action_add_repo"> | ||||
|                 <action android:name="android.intent.action.VIEW" /> | ||||
|  | ||||
|                 <category android:name="android.intent.category.DEFAULT" /> | ||||
|                 <category android:name="android.intent.category.BROWSABLE" /> | ||||
|  | ||||
|                 <data android:scheme="tachiyomi" /> | ||||
|                 <data android:host="add-repo" /> | ||||
|             </intent-filter> | ||||
|  | ||||
|             <!-- Open backup files --> | ||||
|             <intent-filter android:label="@string/pref_restore_backup"> | ||||
|                 <action android:name="android.intent.action.VIEW" /> | ||||
|  | ||||
|                 <category android:name="android.intent.category.DEFAULT" /> | ||||
|                 <category android:name="android.intent.category.BROWSABLE" /> | ||||
|  | ||||
|                 <data android:scheme="file" /> | ||||
|                 <data android:scheme="content" /> | ||||
|                 <data android:host="*" /> | ||||
|                 <data android:mimeType="*/*" /> | ||||
|                 <!-- | ||||
|                 Work around Android's ugly primitive PatternMatcher | ||||
|                 implementation that can't cope with finding a . early in | ||||
|                 the path unless it's explicitly matched. | ||||
|  | ||||
|                 See https://stackoverflow.com/a/31028507 | ||||
|                 --> | ||||
|                 <data android:pathPattern=".*\\.tachibk" /> | ||||
|                 <data android:pathPattern=".*\\..*\\.tachibk" /> | ||||
|                 <data android:pathPattern=".*\\..*\\..*\\.tachibk" /> | ||||
|                 <data android:pathPattern=".*\\..*\\..*\\..*\\.tachibk" /> | ||||
|                 <data android:pathPattern=".*\\..*\\..*\\..*\\..*\\.tachibk" /> | ||||
|                 <data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\.tachibk" /> | ||||
|                 <data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\.tachibk" /> | ||||
|             </intent-filter> | ||||
|  | ||||
|             <!--suppress AndroidDomInspection --> | ||||
|             <meta-data | ||||
|                 android:name="android.app.shortcuts" | ||||
|                 android:resource="@xml/shortcuts" /> | ||||
|         </activity> | ||||
|  | ||||
|         <activity | ||||
|             android:name=".crash.CrashActivity" | ||||
|             android:exported="false" | ||||
|             android:process=":error_handler" /> | ||||
|  | ||||
|         <activity | ||||
|             android:name=".ui.deeplink.DeepLinkActivity" | ||||
|             android:exported="true" | ||||
|             android:label="@string/action_search" | ||||
|             android:launchMode="singleTask" | ||||
|             android:theme="@android:style/Theme.NoDisplay"> | ||||
|             <intent-filter> | ||||
|                 <action android:name="android.intent.action.SEARCH" /> | ||||
|                 <action android:name="com.google.android.gms.actions.SEARCH_ACTION" /> | ||||
|  | ||||
|                 <category android:name="android.intent.category.DEFAULT" /> | ||||
|             </intent-filter> | ||||
|             <intent-filter> | ||||
|                 <action android:name="eu.kanade.tachiyomi.SEARCH" /> | ||||
|                 <category android:name="android.intent.category.DEFAULT" /> | ||||
|             </intent-filter> | ||||
|             <intent-filter> | ||||
|                 <action android:name="android.intent.action.SEND" /> | ||||
|                 <category android:name="android.intent.category.DEFAULT" /> | ||||
|                 <data android:mimeType="text/plain" /> | ||||
|             </intent-filter> | ||||
|  | ||||
|             <meta-data | ||||
|                 android:name="android.app.searchable" | ||||
|                 android:resource="@xml/searchable" /> | ||||
|             android:name=".ui.manga.MangaActivity" | ||||
|             android:parentActivityName=".ui.main.MainActivity" > | ||||
|         </activity> | ||||
|  | ||||
|         <activity | ||||
|             android:name=".ui.reader.ReaderActivity" | ||||
|             android:exported="false" | ||||
|             android:launchMode="singleTask"> | ||||
|             <intent-filter> | ||||
|                 <action android:name="com.samsung.android.support.REMOTE_ACTION" /> | ||||
|             </intent-filter> | ||||
|  | ||||
|             <meta-data | ||||
|                 android:name="com.samsung.android.support.REMOTE_ACTION" | ||||
|                 android:resource="@xml/s_pen_actions" /> | ||||
|             android:theme="@style/Theme.Reader"> | ||||
|         </activity> | ||||
|         <activity | ||||
|             android:name=".ui.setting.SettingsActivity" | ||||
|             android:label="@string/label_settings" | ||||
|             android:parentActivityName=".ui.main.MainActivity" > | ||||
|         </activity> | ||||
|         <activity | ||||
|             android:name=".ui.category.CategoryActivity" | ||||
|             android:label="@string/label_categories" | ||||
|             android:parentActivityName=".ui.main.MainActivity"> | ||||
|         </activity> | ||||
|         <activity | ||||
|             android:name=".ui.setting.SettingsDownloadsFragment$CustomLayoutPickerActivity" | ||||
|             android:label="@string/app_name" | ||||
|             android:theme="@style/FilePickerTheme"> | ||||
|         </activity> | ||||
|  | ||||
|         <activity | ||||
|             android:name=".ui.security.UnlockActivity" | ||||
|             android:exported="false" | ||||
|             android:theme="@style/Theme.Tachiyomi" /> | ||||
|         <service android:name=".data.library.LibraryUpdateService" | ||||
|             android:exported="false"/> | ||||
|  | ||||
|         <activity | ||||
|             android:name=".ui.webview.WebViewActivity" | ||||
|             android:configChanges="uiMode|orientation|screenSize" | ||||
|             android:exported="false" /> | ||||
|         <service android:name=".data.download.DownloadService" | ||||
|             android:exported="false"/> | ||||
|  | ||||
|         <activity | ||||
|             android:name=".extension.util.ExtensionInstallActivity" | ||||
|             android:exported="false" | ||||
|             android:theme="@android:style/Theme.Translucent.NoTitleBar" /> | ||||
|  | ||||
|         <activity | ||||
|             android:name=".ui.setting.track.TrackLoginActivity" | ||||
|             android:exported="true" | ||||
|             android:label="@string/track_activity_name"> | ||||
|             <intent-filter> | ||||
|                 <action android:name="android.intent.action.VIEW" /> | ||||
|  | ||||
|                 <category android:name="android.intent.category.DEFAULT" /> | ||||
|                 <category android:name="android.intent.category.BROWSABLE" /> | ||||
|  | ||||
|                 <data android:scheme="mihon" /> | ||||
|  | ||||
|                 <data android:host="anilist-auth" /> | ||||
|                 <data android:host="bangumi-auth" /> | ||||
|                 <data android:host="myanimelist-auth" /> | ||||
|                 <data android:host="shikimori-auth" /> | ||||
|             </intent-filter> | ||||
|         </activity> | ||||
|         <service android:name=".data.mangasync.UpdateMangaSyncService" | ||||
|             android:exported="false"/> | ||||
|  | ||||
|         <receiver | ||||
|             android:name=".data.notification.NotificationReceiver" | ||||
|             android:exported="false" /> | ||||
|             android:name=".data.library.LibraryUpdateService$SyncOnConnectionAvailable" | ||||
|             android:enabled="false"> | ||||
|             <intent-filter> | ||||
|                 <action android:name="android.net.conn.CONNECTIVITY_CHANGE" /> | ||||
|             </intent-filter> | ||||
|         </receiver> | ||||
|  | ||||
|         <service | ||||
|             android:name=".extension.util.ExtensionInstallService" | ||||
|             android:exported="false" | ||||
|             android:foregroundServiceType="shortService" /> | ||||
|         <receiver | ||||
|             android:name=".data.library.LibraryUpdateService$SyncOnPowerConnected" | ||||
|             android:enabled="false"> | ||||
|             <intent-filter> | ||||
|                 <action android:name="android.intent.action.ACTION_POWER_CONNECTED" /> | ||||
|             </intent-filter> | ||||
|         </receiver> | ||||
|  | ||||
|         <service | ||||
|             android:name="androidx.appcompat.app.AppLocalesMetadataHolderService" | ||||
|             android:enabled="false" | ||||
|             android:exported="false"> | ||||
|             <meta-data | ||||
|                 android:name="autoStoreLocales" | ||||
|                 android:value="true" /> | ||||
|         </service> | ||||
|         <receiver | ||||
|             android:name=".data.library.LibraryUpdateService$CancelUpdateReceiver"> | ||||
|         </receiver> | ||||
|  | ||||
|         <service | ||||
|             android:name="androidx.work.impl.foreground.SystemForegroundService" | ||||
|             android:foregroundServiceType="dataSync" | ||||
|             tools:node="merge" /> | ||||
|         <receiver | ||||
|             android:name=".data.updater.UpdateDownloader$InstallOnReceived"> | ||||
|         </receiver> | ||||
|  | ||||
|         <provider | ||||
|             android:name="androidx.core.content.FileProvider" | ||||
|             android:authorities="${applicationId}.provider" | ||||
|             android:exported="false" | ||||
|             android:grantUriPermissions="true"> | ||||
|             <meta-data | ||||
|                 android:name="android.support.FILE_PROVIDER_PATHS" | ||||
|                 android:resource="@xml/provider_paths" /> | ||||
|         </provider> | ||||
|         <receiver | ||||
|             android:name=".data.library.LibraryUpdateAlarm"> | ||||
|             <intent-filter> | ||||
|                 <action android:name="android.intent.action.BOOT_COMPLETED"/> | ||||
|                 <action android:name="eu.kanade.UPDATE_LIBRARY" /> | ||||
|             </intent-filter> | ||||
|         </receiver> | ||||
|  | ||||
|         <receiver | ||||
|             android:name=".data.updater.UpdateDownloaderAlarm"> | ||||
|             <intent-filter> | ||||
|                 <action android:name="android.intent.action.BOOT_COMPLETED"/> | ||||
|                 <action android:name="eu.kanade.CHECK_UPDATE"/> | ||||
|             </intent-filter> | ||||
|         </receiver> | ||||
|  | ||||
|         <provider | ||||
|             android:name="rikka.shizuku.ShizukuProvider" | ||||
|             android:authorities="${applicationId}.shizuku" | ||||
|             android:enabled="true" | ||||
|             android:exported="true" | ||||
|             android:multiprocess="false" | ||||
|             android:permission="android.permission.INTERACT_ACROSS_USERS_FULL" /> | ||||
|  | ||||
|         <meta-data | ||||
|             android:name="android.webkit.WebView.EnableSafeBrowsing" | ||||
|             android:value="false" /> | ||||
|         <meta-data | ||||
|             android:name="android.webkit.WebView.MetricsOptOut" | ||||
|             android:value="true" /> | ||||
|             android:name="eu.kanade.tachiyomi.data.cache.CoverGlideModule" | ||||
|             android:value="GlideModule" /> | ||||
|  | ||||
|     </application> | ||||
|  | ||||
|   | ||||
							
								
								
									
										
											BIN
										
									
								
								app/src/main/assets/fonts/PTSans-Narrow.ttf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								app/src/main/assets/fonts/PTSans-Narrow.ttf
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								app/src/main/assets/fonts/PTSans-NarrowBold.ttf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								app/src/main/assets/fonts/PTSans-NarrowBold.ttf
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								app/src/main/ic_launcher-web.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								app/src/main/ic_launcher-web.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 25 KiB | 
| @@ -1,10 +0,0 @@ | ||||
| package eu.kanade.core.preference | ||||
|  | ||||
| import androidx.compose.ui.state.ToggleableState | ||||
| import tachiyomi.core.common.preference.CheckboxState | ||||
|  | ||||
| fun <T> CheckboxState.TriState<T>.asToggleableState() = when (this) { | ||||
|     is CheckboxState.TriState.Exclude -> ToggleableState.Indeterminate | ||||
|     is CheckboxState.TriState.Include -> ToggleableState.On | ||||
|     is CheckboxState.TriState.None -> ToggleableState.Off | ||||
| } | ||||
| @@ -1,38 +0,0 @@ | ||||
| package eu.kanade.core.preference | ||||
|  | ||||
| import androidx.compose.runtime.MutableState | ||||
| import androidx.compose.runtime.mutableStateOf | ||||
| import kotlinx.coroutines.CoroutineScope | ||||
| import kotlinx.coroutines.flow.launchIn | ||||
| import kotlinx.coroutines.flow.onEach | ||||
| import tachiyomi.core.common.preference.Preference | ||||
|  | ||||
| class PreferenceMutableState<T>( | ||||
|     private val preference: Preference<T>, | ||||
|     scope: CoroutineScope, | ||||
| ) : MutableState<T> { | ||||
|  | ||||
|     private val state = mutableStateOf(preference.get()) | ||||
|  | ||||
|     init { | ||||
|         preference.changes() | ||||
|             .onEach { state.value = it } | ||||
|             .launchIn(scope) | ||||
|     } | ||||
|  | ||||
|     override var value: T | ||||
|         get() = state.value | ||||
|         set(value) { | ||||
|             preference.set(value) | ||||
|         } | ||||
|  | ||||
|     override fun component1(): T { | ||||
|         return state.value | ||||
|     } | ||||
|  | ||||
|     override fun component2(): (T) -> Unit { | ||||
|         return preference::set | ||||
|     } | ||||
| } | ||||
|  | ||||
| fun <T> Preference<T>.asState(scope: CoroutineScope) = PreferenceMutableState(this, scope) | ||||
| @@ -1,138 +0,0 @@ | ||||
| package eu.kanade.core.util | ||||
|  | ||||
| import androidx.compose.ui.util.fastForEach | ||||
| import kotlin.contracts.ExperimentalContracts | ||||
| import kotlin.contracts.contract | ||||
|  | ||||
| fun <T : R, R : Any> List<T>.insertSeparators( | ||||
|     generator: (T?, T?) -> R?, | ||||
| ): List<R> { | ||||
|     if (isEmpty()) return emptyList() | ||||
|     val newList = mutableListOf<R>() | ||||
|     for (i in -1..lastIndex) { | ||||
|         val before = getOrNull(i) | ||||
|         before?.let(newList::add) | ||||
|         val after = getOrNull(i + 1) | ||||
|         val separator = generator.invoke(before, after) | ||||
|         separator?.let(newList::add) | ||||
|     } | ||||
|     return newList | ||||
| } | ||||
|  | ||||
| fun <E> HashSet<E>.addOrRemove(value: E, shouldAdd: Boolean) { | ||||
|     if (shouldAdd) { | ||||
|         add(value) | ||||
|     } else { | ||||
|         remove(value) | ||||
|     } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Returns a list containing only elements matching the given [predicate]. | ||||
|  * | ||||
|  * **Do not use for collections that come from public APIs**, since they may not support random | ||||
|  * access in an efficient way, and this method may actually be a lot slower. Only use for | ||||
|  * collections that are created by code we control and are known to support random access. | ||||
|  */ | ||||
| @OptIn(ExperimentalContracts::class) | ||||
| inline fun <T> List<T>.fastFilter(predicate: (T) -> Boolean): List<T> { | ||||
|     contract { callsInPlace(predicate) } | ||||
|     val destination = ArrayList<T>() | ||||
|     fastForEach { if (predicate(it)) destination.add(it) } | ||||
|     return destination | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Returns a list containing all elements not matching the given [predicate]. | ||||
|  * | ||||
|  * **Do not use for collections that come from public APIs**, since they may not support random | ||||
|  * access in an efficient way, and this method may actually be a lot slower. Only use for | ||||
|  * collections that are created by code we control and are known to support random access. | ||||
|  */ | ||||
| @OptIn(ExperimentalContracts::class) | ||||
| inline fun <T> List<T>.fastFilterNot(predicate: (T) -> Boolean): List<T> { | ||||
|     contract { callsInPlace(predicate) } | ||||
|     val destination = ArrayList<T>() | ||||
|     fastForEach { if (!predicate(it)) destination.add(it) } | ||||
|     return destination | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Returns a list containing only the non-null results of applying the | ||||
|  * given [transform] function to each element in the original collection. | ||||
|  * | ||||
|  * **Do not use for collections that come from public APIs**, since they may not support random | ||||
|  * access in an efficient way, and this method may actually be a lot slower. Only use for | ||||
|  * collections that are created by code we control and are known to support random access. | ||||
|  */ | ||||
| @OptIn(ExperimentalContracts::class) | ||||
| inline fun <T, R> List<T>.fastMapNotNull(transform: (T) -> R?): List<R> { | ||||
|     contract { callsInPlace(transform) } | ||||
|     val destination = ArrayList<R>() | ||||
|     fastForEach { element -> | ||||
|         transform(element)?.let(destination::add) | ||||
|     } | ||||
|     return destination | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Splits the original collection into pair of lists, | ||||
|  * where *first* list contains elements for which [predicate] yielded `true`, | ||||
|  * while *second* list contains elements for which [predicate] yielded `false`. | ||||
|  * | ||||
|  * **Do not use for collections that come from public APIs**, since they may not support random | ||||
|  * access in an efficient way, and this method may actually be a lot slower. Only use for | ||||
|  * collections that are created by code we control and are known to support random access. | ||||
|  */ | ||||
| @OptIn(ExperimentalContracts::class) | ||||
| inline fun <T> List<T>.fastPartition(predicate: (T) -> Boolean): Pair<List<T>, List<T>> { | ||||
|     contract { callsInPlace(predicate) } | ||||
|     val first = ArrayList<T>() | ||||
|     val second = ArrayList<T>() | ||||
|     fastForEach { | ||||
|         if (predicate(it)) { | ||||
|             first.add(it) | ||||
|         } else { | ||||
|             second.add(it) | ||||
|         } | ||||
|     } | ||||
|     return Pair(first, second) | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Returns the number of entries not matching the given [predicate]. | ||||
|  * | ||||
|  * **Do not use for collections that come from public APIs**, since they may not support random | ||||
|  * access in an efficient way, and this method may actually be a lot slower. Only use for | ||||
|  * collections that are created by code we control and are known to support random access. | ||||
|  */ | ||||
| @OptIn(ExperimentalContracts::class) | ||||
| inline fun <T> List<T>.fastCountNot(predicate: (T) -> Boolean): Int { | ||||
|     contract { callsInPlace(predicate) } | ||||
|     var count = size | ||||
|     fastForEach { if (predicate(it)) --count } | ||||
|     return count | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Returns a list containing only elements from the given collection | ||||
|  * having distinct keys returned by the given [selector] function. | ||||
|  * | ||||
|  * Among elements of the given collection with equal keys, only the first one will be present in the resulting list. | ||||
|  * The elements in the resulting list are in the same order as they were in the source collection. | ||||
|  * | ||||
|  * **Do not use for collections that come from public APIs**, since they may not support random | ||||
|  * access in an efficient way, and this method may actually be a lot slower. Only use for | ||||
|  * collections that are created by code we control and are known to support random access. | ||||
|  */ | ||||
| @OptIn(ExperimentalContracts::class) | ||||
| inline fun <T, K> List<T>.fastDistinctBy(selector: (T) -> K): List<T> { | ||||
|     contract { callsInPlace(selector) } | ||||
|     val set = HashSet<K>() | ||||
|     val list = ArrayList<T>() | ||||
|     fastForEach { | ||||
|         val key = selector(it) | ||||
|         if (set.add(key)) list.add(it) | ||||
|     } | ||||
|     return list | ||||
| } | ||||
| @@ -1,13 +0,0 @@ | ||||
| package eu.kanade.core.util | ||||
|  | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.runtime.collectAsState | ||||
| import androidx.compose.runtime.remember | ||||
| import tachiyomi.domain.source.service.SourceManager | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
|  | ||||
| @Composable | ||||
| fun ifSourcesLoaded(): Boolean { | ||||
|     return remember { Injekt.get<SourceManager>().isInitialized }.collectAsState().value | ||||
| } | ||||
| @@ -1,195 +0,0 @@ | ||||
| package eu.kanade.domain | ||||
|  | ||||
| import eu.kanade.domain.chapter.interactor.GetAvailableScanlators | ||||
| import eu.kanade.domain.chapter.interactor.SetReadStatus | ||||
| import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource | ||||
| import eu.kanade.domain.download.interactor.DeleteDownload | ||||
| import eu.kanade.domain.extension.interactor.GetExtensionLanguages | ||||
| import eu.kanade.domain.extension.interactor.GetExtensionSources | ||||
| import eu.kanade.domain.extension.interactor.GetExtensionsByType | ||||
| import eu.kanade.domain.extension.interactor.TrustExtension | ||||
| import eu.kanade.domain.manga.interactor.GetExcludedScanlators | ||||
| import eu.kanade.domain.manga.interactor.SetExcludedScanlators | ||||
| import eu.kanade.domain.manga.interactor.SetMangaViewerFlags | ||||
| import eu.kanade.domain.manga.interactor.UpdateManga | ||||
| import eu.kanade.domain.source.interactor.GetEnabledSources | ||||
| import eu.kanade.domain.source.interactor.GetLanguagesWithSources | ||||
| import eu.kanade.domain.source.interactor.GetSourcesWithFavoriteCount | ||||
| import eu.kanade.domain.source.interactor.SetMigrateSorting | ||||
| import eu.kanade.domain.source.interactor.ToggleLanguage | ||||
| import eu.kanade.domain.source.interactor.ToggleSource | ||||
| import eu.kanade.domain.source.interactor.ToggleSourcePin | ||||
| import eu.kanade.domain.track.interactor.AddTracks | ||||
| import eu.kanade.domain.track.interactor.RefreshTracks | ||||
| import eu.kanade.domain.track.interactor.SyncChapterProgressWithTrack | ||||
| import eu.kanade.domain.track.interactor.TrackChapter | ||||
| import mihon.data.repository.ExtensionRepoRepositoryImpl | ||||
| import mihon.domain.chapter.interactor.FilterChaptersForDownload | ||||
| import mihon.domain.extensionrepo.interactor.CreateExtensionRepo | ||||
| import mihon.domain.extensionrepo.interactor.DeleteExtensionRepo | ||||
| import mihon.domain.extensionrepo.interactor.GetExtensionRepo | ||||
| import mihon.domain.extensionrepo.interactor.GetExtensionRepoCount | ||||
| import mihon.domain.extensionrepo.interactor.ReplaceExtensionRepo | ||||
| import mihon.domain.extensionrepo.interactor.UpdateExtensionRepo | ||||
| import mihon.domain.extensionrepo.repository.ExtensionRepoRepository | ||||
| import mihon.domain.extensionrepo.service.ExtensionRepoService | ||||
| import mihon.domain.upcoming.interactor.GetUpcomingManga | ||||
| import tachiyomi.data.category.CategoryRepositoryImpl | ||||
| import tachiyomi.data.chapter.ChapterRepositoryImpl | ||||
| import tachiyomi.data.history.HistoryRepositoryImpl | ||||
| import tachiyomi.data.manga.MangaRepositoryImpl | ||||
| import tachiyomi.data.release.ReleaseServiceImpl | ||||
| import tachiyomi.data.source.SourceRepositoryImpl | ||||
| import tachiyomi.data.source.StubSourceRepositoryImpl | ||||
| import tachiyomi.data.track.TrackRepositoryImpl | ||||
| import tachiyomi.data.updates.UpdatesRepositoryImpl | ||||
| import tachiyomi.domain.category.interactor.CreateCategoryWithName | ||||
| import tachiyomi.domain.category.interactor.DeleteCategory | ||||
| import tachiyomi.domain.category.interactor.GetCategories | ||||
| import tachiyomi.domain.category.interactor.RenameCategory | ||||
| import tachiyomi.domain.category.interactor.ReorderCategory | ||||
| import tachiyomi.domain.category.interactor.ResetCategoryFlags | ||||
| import tachiyomi.domain.category.interactor.SetDisplayMode | ||||
| import tachiyomi.domain.category.interactor.SetMangaCategories | ||||
| import tachiyomi.domain.category.interactor.SetSortModeForCategory | ||||
| import tachiyomi.domain.category.interactor.UpdateCategory | ||||
| import tachiyomi.domain.category.repository.CategoryRepository | ||||
| import tachiyomi.domain.chapter.interactor.GetChapter | ||||
| import tachiyomi.domain.chapter.interactor.GetChapterByUrlAndMangaId | ||||
| import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId | ||||
| import tachiyomi.domain.chapter.interactor.SetMangaDefaultChapterFlags | ||||
| import tachiyomi.domain.chapter.interactor.ShouldUpdateDbChapter | ||||
| import tachiyomi.domain.chapter.interactor.UpdateChapter | ||||
| import tachiyomi.domain.chapter.repository.ChapterRepository | ||||
| import tachiyomi.domain.history.interactor.GetHistory | ||||
| import tachiyomi.domain.history.interactor.GetNextChapters | ||||
| import tachiyomi.domain.history.interactor.GetTotalReadDuration | ||||
| import tachiyomi.domain.history.interactor.RemoveHistory | ||||
| import tachiyomi.domain.history.interactor.UpsertHistory | ||||
| import tachiyomi.domain.history.repository.HistoryRepository | ||||
| import tachiyomi.domain.manga.interactor.FetchInterval | ||||
| import tachiyomi.domain.manga.interactor.GetDuplicateLibraryManga | ||||
| import tachiyomi.domain.manga.interactor.GetFavorites | ||||
| import tachiyomi.domain.manga.interactor.GetLibraryManga | ||||
| import tachiyomi.domain.manga.interactor.GetManga | ||||
| import tachiyomi.domain.manga.interactor.GetMangaByUrlAndSourceId | ||||
| import tachiyomi.domain.manga.interactor.GetMangaWithChapters | ||||
| import tachiyomi.domain.manga.interactor.NetworkToLocalManga | ||||
| import tachiyomi.domain.manga.interactor.ResetViewerFlags | ||||
| import tachiyomi.domain.manga.interactor.SetMangaChapterFlags | ||||
| import tachiyomi.domain.manga.repository.MangaRepository | ||||
| import tachiyomi.domain.release.interactor.GetApplicationRelease | ||||
| import tachiyomi.domain.release.service.ReleaseService | ||||
| import tachiyomi.domain.source.interactor.GetRemoteManga | ||||
| import tachiyomi.domain.source.interactor.GetSourcesWithNonLibraryManga | ||||
| import tachiyomi.domain.source.repository.SourceRepository | ||||
| import tachiyomi.domain.source.repository.StubSourceRepository | ||||
| import tachiyomi.domain.track.interactor.DeleteTrack | ||||
| import tachiyomi.domain.track.interactor.GetTracks | ||||
| import tachiyomi.domain.track.interactor.GetTracksPerManga | ||||
| import tachiyomi.domain.track.interactor.InsertTrack | ||||
| import tachiyomi.domain.track.repository.TrackRepository | ||||
| import tachiyomi.domain.updates.interactor.GetUpdates | ||||
| import tachiyomi.domain.updates.repository.UpdatesRepository | ||||
| import uy.kohesive.injekt.api.InjektModule | ||||
| import uy.kohesive.injekt.api.InjektRegistrar | ||||
| import uy.kohesive.injekt.api.addFactory | ||||
| import uy.kohesive.injekt.api.addSingletonFactory | ||||
| import uy.kohesive.injekt.api.get | ||||
|  | ||||
| class DomainModule : InjektModule { | ||||
|  | ||||
|     override fun InjektRegistrar.registerInjectables() { | ||||
|         addSingletonFactory<CategoryRepository> { CategoryRepositoryImpl(get()) } | ||||
|         addFactory { GetCategories(get()) } | ||||
|         addFactory { ResetCategoryFlags(get(), get()) } | ||||
|         addFactory { SetDisplayMode(get()) } | ||||
|         addFactory { SetSortModeForCategory(get(), get()) } | ||||
|         addFactory { CreateCategoryWithName(get(), get()) } | ||||
|         addFactory { RenameCategory(get()) } | ||||
|         addFactory { ReorderCategory(get()) } | ||||
|         addFactory { UpdateCategory(get()) } | ||||
|         addFactory { DeleteCategory(get()) } | ||||
|  | ||||
|         addSingletonFactory<MangaRepository> { MangaRepositoryImpl(get()) } | ||||
|         addFactory { GetDuplicateLibraryManga(get()) } | ||||
|         addFactory { GetFavorites(get()) } | ||||
|         addFactory { GetLibraryManga(get()) } | ||||
|         addFactory { GetMangaWithChapters(get(), get()) } | ||||
|         addFactory { GetMangaByUrlAndSourceId(get()) } | ||||
|         addFactory { GetManga(get()) } | ||||
|         addFactory { GetNextChapters(get(), get(), get()) } | ||||
|         addFactory { GetUpcomingManga(get()) } | ||||
|         addFactory { ResetViewerFlags(get()) } | ||||
|         addFactory { SetMangaChapterFlags(get()) } | ||||
|         addFactory { FetchInterval(get()) } | ||||
|         addFactory { SetMangaDefaultChapterFlags(get(), get(), get()) } | ||||
|         addFactory { SetMangaViewerFlags(get()) } | ||||
|         addFactory { NetworkToLocalManga(get()) } | ||||
|         addFactory { UpdateManga(get(), get()) } | ||||
|         addFactory { SetMangaCategories(get()) } | ||||
|         addFactory { GetExcludedScanlators(get()) } | ||||
|         addFactory { SetExcludedScanlators(get()) } | ||||
|  | ||||
|         addSingletonFactory<ReleaseService> { ReleaseServiceImpl(get(), get()) } | ||||
|         addFactory { GetApplicationRelease(get(), get()) } | ||||
|  | ||||
|         addSingletonFactory<TrackRepository> { TrackRepositoryImpl(get()) } | ||||
|         addFactory { TrackChapter(get(), get(), get(), get()) } | ||||
|         addFactory { AddTracks(get(), get(), get(), get()) } | ||||
|         addFactory { RefreshTracks(get(), get(), get(), get()) } | ||||
|         addFactory { DeleteTrack(get()) } | ||||
|         addFactory { GetTracksPerManga(get()) } | ||||
|         addFactory { GetTracks(get()) } | ||||
|         addFactory { InsertTrack(get()) } | ||||
|         addFactory { SyncChapterProgressWithTrack(get(), get(), get()) } | ||||
|  | ||||
|         addSingletonFactory<ChapterRepository> { ChapterRepositoryImpl(get()) } | ||||
|         addFactory { GetChapter(get()) } | ||||
|         addFactory { GetChaptersByMangaId(get()) } | ||||
|         addFactory { GetChapterByUrlAndMangaId(get()) } | ||||
|         addFactory { UpdateChapter(get()) } | ||||
|         addFactory { SetReadStatus(get(), get(), get(), get()) } | ||||
|         addFactory { ShouldUpdateDbChapter() } | ||||
|         addFactory { SyncChaptersWithSource(get(), get(), get(), get(), get(), get(), get(), get()) } | ||||
|         addFactory { GetAvailableScanlators(get()) } | ||||
|         addFactory { FilterChaptersForDownload(get(), get(), get()) } | ||||
|  | ||||
|         addSingletonFactory<HistoryRepository> { HistoryRepositoryImpl(get()) } | ||||
|         addFactory { GetHistory(get()) } | ||||
|         addFactory { UpsertHistory(get()) } | ||||
|         addFactory { RemoveHistory(get()) } | ||||
|         addFactory { GetTotalReadDuration(get()) } | ||||
|  | ||||
|         addFactory { DeleteDownload(get(), get()) } | ||||
|  | ||||
|         addFactory { GetExtensionsByType(get(), get()) } | ||||
|         addFactory { GetExtensionSources(get()) } | ||||
|         addFactory { GetExtensionLanguages(get(), get()) } | ||||
|  | ||||
|         addSingletonFactory<UpdatesRepository> { UpdatesRepositoryImpl(get()) } | ||||
|         addFactory { GetUpdates(get()) } | ||||
|  | ||||
|         addSingletonFactory<SourceRepository> { SourceRepositoryImpl(get(), get()) } | ||||
|         addSingletonFactory<StubSourceRepository> { StubSourceRepositoryImpl(get()) } | ||||
|         addFactory { GetEnabledSources(get(), get()) } | ||||
|         addFactory { GetLanguagesWithSources(get(), get()) } | ||||
|         addFactory { GetRemoteManga(get()) } | ||||
|         addFactory { GetSourcesWithFavoriteCount(get(), get()) } | ||||
|         addFactory { GetSourcesWithNonLibraryManga(get()) } | ||||
|         addFactory { SetMigrateSorting(get()) } | ||||
|         addFactory { ToggleLanguage(get()) } | ||||
|         addFactory { ToggleSource(get()) } | ||||
|         addFactory { ToggleSourcePin(get()) } | ||||
|         addFactory { TrustExtension(get(), get()) } | ||||
|  | ||||
|         addSingletonFactory<ExtensionRepoRepository> { ExtensionRepoRepositoryImpl(get()) } | ||||
|         addFactory { ExtensionRepoService(get(), get()) } | ||||
|         addFactory { GetExtensionRepo(get()) } | ||||
|         addFactory { GetExtensionRepoCount(get()) } | ||||
|         addFactory { CreateExtensionRepo(get(), get()) } | ||||
|         addFactory { DeleteExtensionRepo(get()) } | ||||
|         addFactory { ReplaceExtensionRepo(get()) } | ||||
|         addFactory { UpdateExtensionRepo(get(), get()) } | ||||
|     } | ||||
| } | ||||
| @@ -1,33 +0,0 @@ | ||||
| package eu.kanade.domain.base | ||||
|  | ||||
| import android.content.Context | ||||
| import dev.icerock.moko.resources.StringResource | ||||
| import tachiyomi.core.common.preference.Preference | ||||
| import tachiyomi.core.common.preference.PreferenceStore | ||||
| import tachiyomi.i18n.MR | ||||
|  | ||||
| class BasePreferences( | ||||
|     val context: Context, | ||||
|     private val preferenceStore: PreferenceStore, | ||||
| ) { | ||||
|  | ||||
|     fun downloadedOnly() = preferenceStore.getBoolean( | ||||
|         Preference.appStateKey("pref_downloaded_only"), | ||||
|         false, | ||||
|     ) | ||||
|  | ||||
|     fun incognitoMode() = preferenceStore.getBoolean(Preference.appStateKey("incognito_mode"), false) | ||||
|  | ||||
|     fun extensionInstaller() = ExtensionInstallerPreference(context, preferenceStore) | ||||
|  | ||||
|     fun shownOnboardingFlow() = preferenceStore.getBoolean(Preference.appStateKey("onboarding_complete"), false) | ||||
|  | ||||
|     enum class ExtensionInstaller(val titleRes: StringResource, val requiresSystemPermission: Boolean) { | ||||
|         LEGACY(MR.strings.ext_installer_legacy, true), | ||||
|         PACKAGEINSTALLER(MR.strings.ext_installer_packageinstaller, true), | ||||
|         SHIZUKU(MR.strings.ext_installer_shizuku, false), | ||||
|         PRIVATE(MR.strings.ext_installer_private, false), | ||||
|     } | ||||
|  | ||||
|     fun displayProfile() = preferenceStore.getString("pref_display_profile_key", "") | ||||
| } | ||||
| @@ -1,68 +0,0 @@ | ||||
| package eu.kanade.domain.base | ||||
|  | ||||
| import android.content.Context | ||||
| import eu.kanade.domain.base.BasePreferences.ExtensionInstaller | ||||
| import eu.kanade.tachiyomi.util.system.hasMiuiPackageInstaller | ||||
| import eu.kanade.tachiyomi.util.system.isShizukuInstalled | ||||
| import kotlinx.coroutines.CoroutineScope | ||||
| import tachiyomi.core.common.preference.Preference | ||||
| import tachiyomi.core.common.preference.PreferenceStore | ||||
| import tachiyomi.core.common.preference.getEnum | ||||
|  | ||||
| class ExtensionInstallerPreference( | ||||
|     private val context: Context, | ||||
|     preferenceStore: PreferenceStore, | ||||
| ) : Preference<ExtensionInstaller> { | ||||
|  | ||||
|     private val basePref = preferenceStore.getEnum(key(), defaultValue()) | ||||
|  | ||||
|     override fun key() = "extension_installer" | ||||
|  | ||||
|     val entries get() = ExtensionInstaller.entries.run { | ||||
|         if (context.hasMiuiPackageInstaller) { | ||||
|             filter { it != ExtensionInstaller.PACKAGEINSTALLER } | ||||
|         } else { | ||||
|             toList() | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     override fun defaultValue() = if (context.hasMiuiPackageInstaller) { | ||||
|         ExtensionInstaller.LEGACY | ||||
|     } else { | ||||
|         ExtensionInstaller.PACKAGEINSTALLER | ||||
|     } | ||||
|  | ||||
|     private fun check(value: ExtensionInstaller): ExtensionInstaller { | ||||
|         when (value) { | ||||
|             ExtensionInstaller.PACKAGEINSTALLER -> { | ||||
|                 if (context.hasMiuiPackageInstaller) return ExtensionInstaller.LEGACY | ||||
|             } | ||||
|             ExtensionInstaller.SHIZUKU -> { | ||||
|                 if (!context.isShizukuInstalled) return defaultValue() | ||||
|             } | ||||
|             else -> {} | ||||
|         } | ||||
|         return value | ||||
|     } | ||||
|  | ||||
|     override fun get(): ExtensionInstaller { | ||||
|         val value = basePref.get() | ||||
|         val checkedValue = check(value) | ||||
|         if (value != checkedValue) { | ||||
|             basePref.set(checkedValue) | ||||
|         } | ||||
|         return checkedValue | ||||
|     } | ||||
|  | ||||
|     override fun set(value: ExtensionInstaller) { | ||||
|         basePref.set(check(value)) | ||||
|     } | ||||
|  | ||||
|     override fun isSet() = basePref.isSet() | ||||
|  | ||||
|     override fun delete() = basePref.delete() | ||||
|  | ||||
|     override fun changes() = basePref.changes() | ||||
|  | ||||
|     override fun stateIn(scope: CoroutineScope) = basePref.stateIn(scope) | ||||
| } | ||||
| @@ -1,24 +0,0 @@ | ||||
| package eu.kanade.domain.chapter.interactor | ||||
|  | ||||
| import kotlinx.coroutines.flow.Flow | ||||
| import kotlinx.coroutines.flow.map | ||||
| import tachiyomi.domain.chapter.repository.ChapterRepository | ||||
|  | ||||
| class GetAvailableScanlators( | ||||
|     private val repository: ChapterRepository, | ||||
| ) { | ||||
|  | ||||
|     private fun List<String>.cleanupAvailableScanlators(): Set<String> { | ||||
|         return mapNotNull { it.ifBlank { null } }.toSet() | ||||
|     } | ||||
|  | ||||
|     suspend fun await(mangaId: Long): Set<String> { | ||||
|         return repository.getScanlatorsByMangaId(mangaId) | ||||
|             .cleanupAvailableScanlators() | ||||
|     } | ||||
|  | ||||
|     fun subscribe(mangaId: Long): Flow<Set<String>> { | ||||
|         return repository.getScanlatorsByMangaIdAsFlow(mangaId) | ||||
|             .map { it.cleanupAvailableScanlators() } | ||||
|     } | ||||
| } | ||||
| @@ -1,80 +0,0 @@ | ||||
| package eu.kanade.domain.chapter.interactor | ||||
|  | ||||
| import eu.kanade.domain.download.interactor.DeleteDownload | ||||
| import logcat.LogPriority | ||||
| import tachiyomi.core.common.util.lang.withNonCancellableContext | ||||
| import tachiyomi.core.common.util.system.logcat | ||||
| import tachiyomi.domain.chapter.model.Chapter | ||||
| import tachiyomi.domain.chapter.model.ChapterUpdate | ||||
| import tachiyomi.domain.chapter.repository.ChapterRepository | ||||
| import tachiyomi.domain.download.service.DownloadPreferences | ||||
| import tachiyomi.domain.manga.model.Manga | ||||
| import tachiyomi.domain.manga.repository.MangaRepository | ||||
|  | ||||
| class SetReadStatus( | ||||
|     private val downloadPreferences: DownloadPreferences, | ||||
|     private val deleteDownload: DeleteDownload, | ||||
|     private val mangaRepository: MangaRepository, | ||||
|     private val chapterRepository: ChapterRepository, | ||||
| ) { | ||||
|  | ||||
|     private val mapper = { chapter: Chapter, read: Boolean -> | ||||
|         ChapterUpdate( | ||||
|             read = read, | ||||
|             lastPageRead = if (!read) 0 else null, | ||||
|             id = chapter.id, | ||||
|         ) | ||||
|     } | ||||
|  | ||||
|     suspend fun await(read: Boolean, vararg chapters: Chapter): Result = withNonCancellableContext { | ||||
|         val chaptersToUpdate = chapters.filter { | ||||
|             when (read) { | ||||
|                 true -> !it.read | ||||
|                 false -> it.read || it.lastPageRead > 0 | ||||
|             } | ||||
|         } | ||||
|         if (chaptersToUpdate.isEmpty()) { | ||||
|             return@withNonCancellableContext Result.NoChapters | ||||
|         } | ||||
|  | ||||
|         try { | ||||
|             chapterRepository.updateAll( | ||||
|                 chaptersToUpdate.map { mapper(it, read) }, | ||||
|             ) | ||||
|         } catch (e: Exception) { | ||||
|             logcat(LogPriority.ERROR, e) | ||||
|             return@withNonCancellableContext Result.InternalError(e) | ||||
|         } | ||||
|  | ||||
|         if (read && downloadPreferences.removeAfterMarkedAsRead().get()) { | ||||
|             chaptersToUpdate | ||||
|                 .groupBy { it.mangaId } | ||||
|                 .forEach { (mangaId, chapters) -> | ||||
|                     deleteDownload.awaitAll( | ||||
|                         manga = mangaRepository.getMangaById(mangaId), | ||||
|                         chapters = chapters.toTypedArray(), | ||||
|                     ) | ||||
|                 } | ||||
|         } | ||||
|  | ||||
|         Result.Success | ||||
|     } | ||||
|  | ||||
|     suspend fun await(mangaId: Long, read: Boolean): Result = withNonCancellableContext { | ||||
|         await( | ||||
|             read = read, | ||||
|             chapters = chapterRepository | ||||
|                 .getChapterByMangaId(mangaId) | ||||
|                 .toTypedArray(), | ||||
|         ) | ||||
|     } | ||||
|  | ||||
|     suspend fun await(manga: Manga, read: Boolean) = | ||||
|         await(manga.id, read) | ||||
|  | ||||
|     sealed interface Result { | ||||
|         data object Success : Result | ||||
|         data object NoChapters : Result | ||||
|         data class InternalError(val error: Throwable) : Result | ||||
|     } | ||||
| } | ||||
| @@ -1,213 +0,0 @@ | ||||
| package eu.kanade.domain.chapter.interactor | ||||
|  | ||||
| import eu.kanade.domain.chapter.model.copyFromSChapter | ||||
| import eu.kanade.domain.chapter.model.toSChapter | ||||
| import eu.kanade.domain.manga.interactor.GetExcludedScanlators | ||||
| import eu.kanade.domain.manga.interactor.UpdateManga | ||||
| import eu.kanade.domain.manga.model.toSManga | ||||
| import eu.kanade.tachiyomi.data.download.DownloadManager | ||||
| import eu.kanade.tachiyomi.data.download.DownloadProvider | ||||
| import eu.kanade.tachiyomi.source.Source | ||||
| import eu.kanade.tachiyomi.source.model.SChapter | ||||
| import eu.kanade.tachiyomi.source.online.HttpSource | ||||
| import tachiyomi.data.chapter.ChapterSanitizer | ||||
| import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId | ||||
| import tachiyomi.domain.chapter.interactor.ShouldUpdateDbChapter | ||||
| import tachiyomi.domain.chapter.interactor.UpdateChapter | ||||
| import tachiyomi.domain.chapter.model.Chapter | ||||
| import tachiyomi.domain.chapter.model.NoChaptersException | ||||
| import tachiyomi.domain.chapter.model.toChapterUpdate | ||||
| import tachiyomi.domain.chapter.repository.ChapterRepository | ||||
| import tachiyomi.domain.chapter.service.ChapterRecognition | ||||
| import tachiyomi.domain.manga.model.Manga | ||||
| import tachiyomi.source.local.isLocal | ||||
| import java.lang.Long.max | ||||
| import java.time.ZonedDateTime | ||||
| import java.util.TreeSet | ||||
|  | ||||
| class SyncChaptersWithSource( | ||||
|     private val downloadManager: DownloadManager, | ||||
|     private val downloadProvider: DownloadProvider, | ||||
|     private val chapterRepository: ChapterRepository, | ||||
|     private val shouldUpdateDbChapter: ShouldUpdateDbChapter, | ||||
|     private val updateManga: UpdateManga, | ||||
|     private val updateChapter: UpdateChapter, | ||||
|     private val getChaptersByMangaId: GetChaptersByMangaId, | ||||
|     private val getExcludedScanlators: GetExcludedScanlators, | ||||
| ) { | ||||
|  | ||||
|     /** | ||||
|      * Method to synchronize db chapters with source ones | ||||
|      * | ||||
|      * @param rawSourceChapters the chapters from the source. | ||||
|      * @param manga the manga the chapters belong to. | ||||
|      * @param source the source the manga belongs to. | ||||
|      * @return Newly added chapters | ||||
|      */ | ||||
|     suspend fun await( | ||||
|         rawSourceChapters: List<SChapter>, | ||||
|         manga: Manga, | ||||
|         source: Source, | ||||
|         manualFetch: Boolean = false, | ||||
|         fetchWindow: Pair<Long, Long> = Pair(0, 0), | ||||
|     ): List<Chapter> { | ||||
|         if (rawSourceChapters.isEmpty() && !source.isLocal()) { | ||||
|             throw NoChaptersException() | ||||
|         } | ||||
|  | ||||
|         val now = ZonedDateTime.now() | ||||
|         val nowMillis = now.toInstant().toEpochMilli() | ||||
|  | ||||
|         val sourceChapters = rawSourceChapters | ||||
|             .distinctBy { it.url } | ||||
|             .mapIndexed { i, sChapter -> | ||||
|                 Chapter.create() | ||||
|                     .copyFromSChapter(sChapter) | ||||
|                     .copy(name = with(ChapterSanitizer) { sChapter.name.sanitize(manga.title) }) | ||||
|                     .copy(mangaId = manga.id, sourceOrder = i.toLong()) | ||||
|             } | ||||
|  | ||||
|         val dbChapters = getChaptersByMangaId.await(manga.id) | ||||
|  | ||||
|         val newChapters = mutableListOf<Chapter>() | ||||
|         val updatedChapters = mutableListOf<Chapter>() | ||||
|         val removedChapters = dbChapters.filterNot { dbChapter -> | ||||
|             sourceChapters.any { sourceChapter -> | ||||
|                 dbChapter.url == sourceChapter.url | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // Used to not set upload date of older chapters | ||||
|         // to a higher value than newer chapters | ||||
|         var maxSeenUploadDate = 0L | ||||
|  | ||||
|         for (sourceChapter in sourceChapters) { | ||||
|             var chapter = sourceChapter | ||||
|  | ||||
|             // Update metadata from source if necessary. | ||||
|             if (source is HttpSource) { | ||||
|                 val sChapter = chapter.toSChapter() | ||||
|                 source.prepareNewChapter(sChapter, manga.toSManga()) | ||||
|                 chapter = chapter.copyFromSChapter(sChapter) | ||||
|             } | ||||
|  | ||||
|             // Recognize chapter number for the chapter. | ||||
|             val chapterNumber = ChapterRecognition.parseChapterNumber(manga.title, chapter.name, chapter.chapterNumber) | ||||
|             chapter = chapter.copy(chapterNumber = chapterNumber) | ||||
|  | ||||
|             val dbChapter = dbChapters.find { it.url == chapter.url } | ||||
|  | ||||
|             if (dbChapter == null) { | ||||
|                 val toAddChapter = if (chapter.dateUpload == 0L) { | ||||
|                     val altDateUpload = if (maxSeenUploadDate == 0L) nowMillis else maxSeenUploadDate | ||||
|                     chapter.copy(dateUpload = altDateUpload) | ||||
|                 } else { | ||||
|                     maxSeenUploadDate = max(maxSeenUploadDate, sourceChapter.dateUpload) | ||||
|                     chapter | ||||
|                 } | ||||
|                 newChapters.add(toAddChapter) | ||||
|             } else { | ||||
|                 if (shouldUpdateDbChapter.await(dbChapter, chapter)) { | ||||
|                     val shouldRenameChapter = downloadProvider.isChapterDirNameChanged(dbChapter, chapter) && | ||||
|                         downloadManager.isChapterDownloaded( | ||||
|                             dbChapter.name, | ||||
|                             dbChapter.scanlator, | ||||
|                             manga.title, | ||||
|                             manga.source, | ||||
|                         ) | ||||
|  | ||||
|                     if (shouldRenameChapter) { | ||||
|                         downloadManager.renameChapter(source, manga, dbChapter, chapter) | ||||
|                     } | ||||
|                     var toChangeChapter = dbChapter.copy( | ||||
|                         name = chapter.name, | ||||
|                         chapterNumber = chapter.chapterNumber, | ||||
|                         scanlator = chapter.scanlator, | ||||
|                         sourceOrder = chapter.sourceOrder, | ||||
|                     ) | ||||
|                     if (chapter.dateUpload != 0L) { | ||||
|                         toChangeChapter = toChangeChapter.copy(dateUpload = chapter.dateUpload) | ||||
|                     } | ||||
|                     updatedChapters.add(toChangeChapter) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // Return if there's nothing to add, delete, or update to avoid unnecessary db transactions. | ||||
|         if (newChapters.isEmpty() && removedChapters.isEmpty() && updatedChapters.isEmpty()) { | ||||
|             if (manualFetch || manga.fetchInterval == 0 || manga.nextUpdate < fetchWindow.first) { | ||||
|                 updateManga.awaitUpdateFetchInterval( | ||||
|                     manga, | ||||
|                     now, | ||||
|                     fetchWindow, | ||||
|                 ) | ||||
|             } | ||||
|             return emptyList() | ||||
|         } | ||||
|  | ||||
|         val reAdded = mutableListOf<Chapter>() | ||||
|  | ||||
|         val deletedChapterNumbers = TreeSet<Double>() | ||||
|         val deletedReadChapterNumbers = TreeSet<Double>() | ||||
|         val deletedBookmarkedChapterNumbers = TreeSet<Double>() | ||||
|  | ||||
|         removedChapters.forEach { chapter -> | ||||
|             if (chapter.read) deletedReadChapterNumbers.add(chapter.chapterNumber) | ||||
|             if (chapter.bookmark) deletedBookmarkedChapterNumbers.add(chapter.chapterNumber) | ||||
|             deletedChapterNumbers.add(chapter.chapterNumber) | ||||
|         } | ||||
|  | ||||
|         val deletedChapterNumberDateFetchMap = removedChapters.sortedByDescending { it.dateFetch } | ||||
|             .associate { it.chapterNumber to it.dateFetch } | ||||
|  | ||||
|         // Date fetch is set in such a way that the upper ones will have bigger value than the lower ones | ||||
|         // Sources MUST return the chapters from most to less recent, which is common. | ||||
|         var itemCount = newChapters.size | ||||
|         var updatedToAdd = newChapters.map { toAddItem -> | ||||
|             var chapter = toAddItem.copy(dateFetch = nowMillis + itemCount--) | ||||
|  | ||||
|             if (!chapter.isRecognizedNumber || chapter.chapterNumber !in deletedChapterNumbers) return@map chapter | ||||
|  | ||||
|             chapter = chapter.copy( | ||||
|                 read = chapter.chapterNumber in deletedReadChapterNumbers, | ||||
|                 bookmark = chapter.chapterNumber in deletedBookmarkedChapterNumbers, | ||||
|             ) | ||||
|  | ||||
|             // Try to to use the fetch date of the original entry to not pollute 'Updates' tab | ||||
|             deletedChapterNumberDateFetchMap[chapter.chapterNumber]?.let { | ||||
|                 chapter = chapter.copy(dateFetch = it) | ||||
|             } | ||||
|  | ||||
|             reAdded.add(chapter) | ||||
|  | ||||
|             chapter | ||||
|         } | ||||
|  | ||||
|         if (removedChapters.isNotEmpty()) { | ||||
|             val toDeleteIds = removedChapters.map { it.id } | ||||
|             chapterRepository.removeChaptersWithIds(toDeleteIds) | ||||
|         } | ||||
|  | ||||
|         if (updatedToAdd.isNotEmpty()) { | ||||
|             updatedToAdd = chapterRepository.addAll(updatedToAdd) | ||||
|         } | ||||
|  | ||||
|         if (updatedChapters.isNotEmpty()) { | ||||
|             val chapterUpdates = updatedChapters.map { it.toChapterUpdate() } | ||||
|             updateChapter.awaitAll(chapterUpdates) | ||||
|         } | ||||
|         updateManga.awaitUpdateFetchInterval(manga, now, fetchWindow) | ||||
|  | ||||
|         // Set this manga as updated since chapters were changed | ||||
|         // Note that last_update actually represents last time the chapter list changed at all | ||||
|         updateManga.awaitUpdateLastUpdate(manga.id) | ||||
|  | ||||
|         val reAddedUrls = reAdded.map { it.url }.toHashSet() | ||||
|  | ||||
|         val excludedScanlators = getExcludedScanlators.await(manga.id).toHashSet() | ||||
|  | ||||
|         return updatedToAdd.filterNot { | ||||
|             it.url in reAddedUrls || it.scanlator in excludedScanlators | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,42 +0,0 @@ | ||||
| package eu.kanade.domain.chapter.model | ||||
|  | ||||
| import eu.kanade.tachiyomi.data.database.models.ChapterImpl | ||||
| import eu.kanade.tachiyomi.source.model.SChapter | ||||
| import tachiyomi.domain.chapter.model.Chapter | ||||
| import eu.kanade.tachiyomi.data.database.models.Chapter as DbChapter | ||||
|  | ||||
| // TODO: Remove when all deps are migrated | ||||
| fun Chapter.toSChapter(): SChapter { | ||||
|     return SChapter.create().also { | ||||
|         it.url = url | ||||
|         it.name = name | ||||
|         it.date_upload = dateUpload | ||||
|         it.chapter_number = chapterNumber.toFloat() | ||||
|         it.scanlator = scanlator | ||||
|     } | ||||
| } | ||||
|  | ||||
| fun Chapter.copyFromSChapter(sChapter: SChapter): Chapter { | ||||
|     return this.copy( | ||||
|         name = sChapter.name, | ||||
|         url = sChapter.url, | ||||
|         dateUpload = sChapter.date_upload, | ||||
|         chapterNumber = sChapter.chapter_number.toDouble(), | ||||
|         scanlator = sChapter.scanlator?.ifBlank { null }?.trim(), | ||||
|     ) | ||||
| } | ||||
|  | ||||
| fun Chapter.toDbChapter(): DbChapter = ChapterImpl().also { | ||||
|     it.id = id | ||||
|     it.manga_id = mangaId | ||||
|     it.url = url | ||||
|     it.name = name | ||||
|     it.scanlator = scanlator | ||||
|     it.read = read | ||||
|     it.bookmark = bookmark | ||||
|     it.last_page_read = lastPageRead.toInt() | ||||
|     it.date_fetch = dateFetch | ||||
|     it.date_upload = dateUpload | ||||
|     it.chapter_number = chapterNumber.toFloat() | ||||
|     it.source_order = sourceOrder.toInt() | ||||
| } | ||||
| @@ -1,52 +0,0 @@ | ||||
| package eu.kanade.domain.chapter.model | ||||
|  | ||||
| import eu.kanade.domain.manga.model.downloadedFilter | ||||
| import eu.kanade.tachiyomi.data.download.DownloadManager | ||||
| import eu.kanade.tachiyomi.ui.manga.ChapterList | ||||
| import tachiyomi.domain.chapter.model.Chapter | ||||
| import tachiyomi.domain.chapter.service.getChapterSort | ||||
| import tachiyomi.domain.manga.model.Manga | ||||
| import tachiyomi.domain.manga.model.applyFilter | ||||
| import tachiyomi.source.local.isLocal | ||||
|  | ||||
| /** | ||||
|  * Applies the view filters to the list of chapters obtained from the database. | ||||
|  * @return an observable of the list of chapters filtered and sorted. | ||||
|  */ | ||||
| fun List<Chapter>.applyFilters(manga: Manga, downloadManager: DownloadManager): List<Chapter> { | ||||
|     val isLocalManga = manga.isLocal() | ||||
|     val unreadFilter = manga.unreadFilter | ||||
|     val downloadedFilter = manga.downloadedFilter | ||||
|     val bookmarkedFilter = manga.bookmarkedFilter | ||||
|  | ||||
|     return filter { chapter -> applyFilter(unreadFilter) { !chapter.read } } | ||||
|         .filter { chapter -> applyFilter(bookmarkedFilter) { chapter.bookmark } } | ||||
|         .filter { chapter -> | ||||
|             applyFilter(downloadedFilter) { | ||||
|                 val downloaded = downloadManager.isChapterDownloaded( | ||||
|                     chapter.name, | ||||
|                     chapter.scanlator, | ||||
|                     manga.title, | ||||
|                     manga.source, | ||||
|                 ) | ||||
|                 downloaded || isLocalManga | ||||
|             } | ||||
|         } | ||||
|         .sortedWith(getChapterSort(manga)) | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Applies the view filters to the list of chapters obtained from the database. | ||||
|  * @return an observable of the list of chapters filtered and sorted. | ||||
|  */ | ||||
| fun List<ChapterList.Item>.applyFilters(manga: Manga): Sequence<ChapterList.Item> { | ||||
|     val isLocalManga = manga.isLocal() | ||||
|     val unreadFilter = manga.unreadFilter | ||||
|     val downloadedFilter = manga.downloadedFilter | ||||
|     val bookmarkedFilter = manga.bookmarkedFilter | ||||
|     return asSequence() | ||||
|         .filter { (chapter) -> applyFilter(unreadFilter) { !chapter.read } } | ||||
|         .filter { (chapter) -> applyFilter(bookmarkedFilter) { chapter.bookmark } } | ||||
|         .filter { applyFilter(downloadedFilter) { it.isDownloaded || isLocalManga } } | ||||
|         .sortedWith { (chapter1), (chapter2) -> getChapterSort(manga).invoke(chapter1, chapter2) } | ||||
| } | ||||
| @@ -1,19 +0,0 @@ | ||||
| package eu.kanade.domain.download.interactor | ||||
|  | ||||
| import eu.kanade.tachiyomi.data.download.DownloadManager | ||||
| import tachiyomi.core.common.util.lang.withNonCancellableContext | ||||
| import tachiyomi.domain.chapter.model.Chapter | ||||
| import tachiyomi.domain.manga.model.Manga | ||||
| import tachiyomi.domain.source.service.SourceManager | ||||
|  | ||||
| class DeleteDownload( | ||||
|     private val sourceManager: SourceManager, | ||||
|     private val downloadManager: DownloadManager, | ||||
| ) { | ||||
|  | ||||
|     suspend fun awaitAll(manga: Manga, vararg chapters: Chapter) = withNonCancellableContext { | ||||
|         sourceManager.get(manga.source)?.let { source -> | ||||
|             downloadManager.deleteChapters(chapters.toList(), manga, source) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,32 +0,0 @@ | ||||
| package eu.kanade.domain.extension.interactor | ||||
|  | ||||
| import eu.kanade.domain.source.service.SourcePreferences | ||||
| import eu.kanade.tachiyomi.extension.ExtensionManager | ||||
| import eu.kanade.tachiyomi.util.system.LocaleHelper | ||||
| import kotlinx.coroutines.flow.Flow | ||||
| import kotlinx.coroutines.flow.combine | ||||
|  | ||||
| class GetExtensionLanguages( | ||||
|     private val preferences: SourcePreferences, | ||||
|     private val extensionManager: ExtensionManager, | ||||
| ) { | ||||
|     fun subscribe(): Flow<List<String>> { | ||||
|         return combine( | ||||
|             preferences.enabledLanguages().changes(), | ||||
|             extensionManager.availableExtensionsFlow, | ||||
|         ) { enabledLanguage, availableExtensions -> | ||||
|             availableExtensions | ||||
|                 .flatMap { ext -> | ||||
|                     if (ext.sources.isEmpty()) { | ||||
|                         listOf(ext.lang) | ||||
|                     } else { | ||||
|                         ext.sources.map { it.lang } | ||||
|                     } | ||||
|                 } | ||||
|                 .distinct() | ||||
|                 .sortedWith( | ||||
|                     compareBy<String> { it !in enabledLanguage }.then(LocaleHelper.comparator), | ||||
|                 ) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,37 +0,0 @@ | ||||
| package eu.kanade.domain.extension.interactor | ||||
|  | ||||
| import eu.kanade.domain.source.service.SourcePreferences | ||||
| import eu.kanade.tachiyomi.extension.model.Extension | ||||
| import eu.kanade.tachiyomi.source.Source | ||||
| import kotlinx.coroutines.flow.Flow | ||||
| import kotlinx.coroutines.flow.map | ||||
|  | ||||
| class GetExtensionSources( | ||||
|     private val preferences: SourcePreferences, | ||||
| ) { | ||||
|  | ||||
|     fun subscribe(extension: Extension.Installed): Flow<List<ExtensionSourceItem>> { | ||||
|         val isMultiSource = extension.sources.size > 1 | ||||
|         val isMultiLangSingleSource = | ||||
|             isMultiSource && extension.sources.map { it.name }.distinct().size == 1 | ||||
|  | ||||
|         return preferences.disabledSources().changes().map { disabledSources -> | ||||
|             fun Source.isEnabled() = id.toString() !in disabledSources | ||||
|  | ||||
|             extension.sources | ||||
|                 .map { source -> | ||||
|                     ExtensionSourceItem( | ||||
|                         source = source, | ||||
|                         enabled = source.isEnabled(), | ||||
|                         labelAsName = isMultiSource && !isMultiLangSingleSource, | ||||
|                     ) | ||||
|                 } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| data class ExtensionSourceItem( | ||||
|     val source: Source, | ||||
|     val enabled: Boolean, | ||||
|     val labelAsName: Boolean, | ||||
| ) | ||||
| @@ -1,60 +0,0 @@ | ||||
| package eu.kanade.domain.extension.interactor | ||||
|  | ||||
| import eu.kanade.domain.extension.model.Extensions | ||||
| import eu.kanade.domain.source.service.SourcePreferences | ||||
| import eu.kanade.tachiyomi.extension.ExtensionManager | ||||
| import eu.kanade.tachiyomi.extension.model.Extension | ||||
| import kotlinx.coroutines.flow.Flow | ||||
| import kotlinx.coroutines.flow.combine | ||||
|  | ||||
| class GetExtensionsByType( | ||||
|     private val preferences: SourcePreferences, | ||||
|     private val extensionManager: ExtensionManager, | ||||
| ) { | ||||
|  | ||||
|     fun subscribe(): Flow<Extensions> { | ||||
|         val showNsfwSources = preferences.showNsfwSource().get() | ||||
|  | ||||
|         return combine( | ||||
|             preferences.enabledLanguages().changes(), | ||||
|             extensionManager.installedExtensionsFlow, | ||||
|             extensionManager.untrustedExtensionsFlow, | ||||
|             extensionManager.availableExtensionsFlow, | ||||
|         ) { enabledLanguages, _installed, _untrusted, _available -> | ||||
|             val (updates, installed) = _installed | ||||
|                 .filter { (showNsfwSources || !it.isNsfw) } | ||||
|                 .sortedWith( | ||||
|                     compareBy<Extension.Installed> { !it.isObsolete } | ||||
|                         .thenBy(String.CASE_INSENSITIVE_ORDER) { it.name }, | ||||
|                 ) | ||||
|                 .partition { it.hasUpdate } | ||||
|  | ||||
|             val untrusted = _untrusted | ||||
|                 .sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name }) | ||||
|  | ||||
|             val available = _available | ||||
|                 .filter { extension -> | ||||
|                     _installed.none { it.pkgName == extension.pkgName } && | ||||
|                         _untrusted.none { it.pkgName == extension.pkgName } && | ||||
|                         (showNsfwSources || !extension.isNsfw) | ||||
|                 } | ||||
|                 .flatMap { ext -> | ||||
|                     if (ext.sources.isEmpty()) { | ||||
|                         return@flatMap if (ext.lang in enabledLanguages) listOf(ext) else emptyList() | ||||
|                     } | ||||
|                     ext.sources.filter { it.lang in enabledLanguages } | ||||
|                         .map { | ||||
|                             ext.copy( | ||||
|                                 name = it.name, | ||||
|                                 lang = it.lang, | ||||
|                                 pkgName = "${ext.pkgName}-${it.id}", | ||||
|                                 sources = listOf(it), | ||||
|                             ) | ||||
|                         } | ||||
|                 } | ||||
|                 .sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name }) | ||||
|  | ||||
|             Extensions(updates, installed, available, untrusted) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,32 +0,0 @@ | ||||
| package eu.kanade.domain.extension.interactor | ||||
|  | ||||
| import android.content.pm.PackageInfo | ||||
| import androidx.core.content.pm.PackageInfoCompat | ||||
| import eu.kanade.domain.source.service.SourcePreferences | ||||
| import mihon.domain.extensionrepo.repository.ExtensionRepoRepository | ||||
| import tachiyomi.core.common.preference.getAndSet | ||||
|  | ||||
| class TrustExtension( | ||||
|     private val extensionRepoRepository: ExtensionRepoRepository, | ||||
|     private val preferences: SourcePreferences, | ||||
| ) { | ||||
|  | ||||
|     suspend fun isTrusted(pkgInfo: PackageInfo, fingerprints: List<String>): Boolean { | ||||
|         val trustedFingerprints = extensionRepoRepository.getAll().map { it.signingKeyFingerprint }.toHashSet() | ||||
|         val key = "${pkgInfo.packageName}:${PackageInfoCompat.getLongVersionCode(pkgInfo)}:${fingerprints.last()}" | ||||
|         return trustedFingerprints.any { fingerprints.contains(it) } || key in preferences.trustedExtensions().get() | ||||
|     } | ||||
|  | ||||
|     fun trust(pkgName: String, versionCode: Long, signatureHash: String) { | ||||
|         preferences.trustedExtensions().getAndSet { exts -> | ||||
|             // Remove previously trusted versions | ||||
|             val removed = exts.filterNot { it.startsWith("$pkgName:") }.toMutableSet() | ||||
|  | ||||
|             removed.also { it += "$pkgName:$versionCode:$signatureHash" } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fun revokeAll() { | ||||
|         preferences.trustedExtensions().delete() | ||||
|     } | ||||
| } | ||||
| @@ -1,10 +0,0 @@ | ||||
| package eu.kanade.domain.extension.model | ||||
|  | ||||
| import eu.kanade.tachiyomi.extension.model.Extension | ||||
|  | ||||
| data class Extensions( | ||||
|     val updates: List<Extension.Installed>, | ||||
|     val installed: List<Extension.Installed>, | ||||
|     val available: List<Extension.Available>, | ||||
|     val untrusted: List<Extension.Untrusted>, | ||||
| ) | ||||
| @@ -1,24 +0,0 @@ | ||||
| package eu.kanade.domain.manga.interactor | ||||
|  | ||||
| import kotlinx.coroutines.flow.Flow | ||||
| import kotlinx.coroutines.flow.map | ||||
| import tachiyomi.data.DatabaseHandler | ||||
|  | ||||
| class GetExcludedScanlators( | ||||
|     private val handler: DatabaseHandler, | ||||
| ) { | ||||
|  | ||||
|     suspend fun await(mangaId: Long): Set<String> { | ||||
|         return handler.awaitList { | ||||
|             excluded_scanlatorsQueries.getExcludedScanlatorsByMangaId(mangaId) | ||||
|         } | ||||
|             .toSet() | ||||
|     } | ||||
|  | ||||
|     fun subscribe(mangaId: Long): Flow<Set<String>> { | ||||
|         return handler.subscribeToList { | ||||
|             excluded_scanlatorsQueries.getExcludedScanlatorsByMangaId(mangaId) | ||||
|         } | ||||
|             .map { it.toSet() } | ||||
|     } | ||||
| } | ||||
| @@ -1,22 +0,0 @@ | ||||
| package eu.kanade.domain.manga.interactor | ||||
|  | ||||
| import tachiyomi.data.DatabaseHandler | ||||
|  | ||||
| class SetExcludedScanlators( | ||||
|     private val handler: DatabaseHandler, | ||||
| ) { | ||||
|  | ||||
|     suspend fun await(mangaId: Long, excludedScanlators: Set<String>) { | ||||
|         handler.await(inTransaction = true) { | ||||
|             val currentExcluded = handler.awaitList { | ||||
|                 excluded_scanlatorsQueries.getExcludedScanlatorsByMangaId(mangaId) | ||||
|             }.toSet() | ||||
|             val toAdd = excludedScanlators.minus(currentExcluded) | ||||
|             for (scanlator in toAdd) { | ||||
|                 excluded_scanlatorsQueries.insert(mangaId, scanlator) | ||||
|             } | ||||
|             val toRemove = currentExcluded.minus(excludedScanlators) | ||||
|             excluded_scanlatorsQueries.remove(mangaId, toRemove) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,35 +0,0 @@ | ||||
| package eu.kanade.domain.manga.interactor | ||||
|  | ||||
| import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation | ||||
| import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode | ||||
| import tachiyomi.domain.manga.model.MangaUpdate | ||||
| import tachiyomi.domain.manga.repository.MangaRepository | ||||
|  | ||||
| class SetMangaViewerFlags( | ||||
|     private val mangaRepository: MangaRepository, | ||||
| ) { | ||||
|  | ||||
|     suspend fun awaitSetReadingMode(id: Long, flag: Long) { | ||||
|         val manga = mangaRepository.getMangaById(id) | ||||
|         mangaRepository.update( | ||||
|             MangaUpdate( | ||||
|                 id = id, | ||||
|                 viewerFlags = manga.viewerFlags.setFlag(flag, ReadingMode.MASK.toLong()), | ||||
|             ), | ||||
|         ) | ||||
|     } | ||||
|  | ||||
|     suspend fun awaitSetOrientation(id: Long, flag: Long) { | ||||
|         val manga = mangaRepository.getMangaById(id) | ||||
|         mangaRepository.update( | ||||
|             MangaUpdate( | ||||
|                 id = id, | ||||
|                 viewerFlags = manga.viewerFlags.setFlag(flag, ReaderOrientation.MASK.toLong()), | ||||
|             ), | ||||
|         ) | ||||
|     } | ||||
|  | ||||
|     private fun Long.setFlag(flag: Long, mask: Long): Long { | ||||
|         return this and mask.inv() or (flag and mask) | ||||
|     } | ||||
| } | ||||
| @@ -1,106 +0,0 @@ | ||||
| package eu.kanade.domain.manga.interactor | ||||
|  | ||||
| import eu.kanade.domain.manga.model.hasCustomCover | ||||
| import eu.kanade.tachiyomi.data.cache.CoverCache | ||||
| import eu.kanade.tachiyomi.source.model.SManga | ||||
| import tachiyomi.domain.manga.interactor.FetchInterval | ||||
| import tachiyomi.domain.manga.model.Manga | ||||
| import tachiyomi.domain.manga.model.MangaUpdate | ||||
| import tachiyomi.domain.manga.repository.MangaRepository | ||||
| import tachiyomi.source.local.isLocal | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
| import java.time.Instant | ||||
| import java.time.ZonedDateTime | ||||
|  | ||||
| class UpdateManga( | ||||
|     private val mangaRepository: MangaRepository, | ||||
|     private val fetchInterval: FetchInterval, | ||||
| ) { | ||||
|  | ||||
|     suspend fun await(mangaUpdate: MangaUpdate): Boolean { | ||||
|         return mangaRepository.update(mangaUpdate) | ||||
|     } | ||||
|  | ||||
|     suspend fun awaitAll(mangaUpdates: List<MangaUpdate>): Boolean { | ||||
|         return mangaRepository.updateAll(mangaUpdates) | ||||
|     } | ||||
|  | ||||
|     suspend fun awaitUpdateFromSource( | ||||
|         localManga: Manga, | ||||
|         remoteManga: SManga, | ||||
|         manualFetch: Boolean, | ||||
|         coverCache: CoverCache = Injekt.get(), | ||||
|     ): Boolean { | ||||
|         val remoteTitle = try { | ||||
|             remoteManga.title | ||||
|         } catch (_: UninitializedPropertyAccessException) { | ||||
|             "" | ||||
|         } | ||||
|  | ||||
|         // if the manga isn't a favorite, set its title from source and update in db | ||||
|         val title = if (remoteTitle.isEmpty() || localManga.favorite) null else remoteTitle | ||||
|  | ||||
|         val coverLastModified = | ||||
|             when { | ||||
|                 // Never refresh covers if the url is empty to avoid "losing" existing covers | ||||
|                 remoteManga.thumbnail_url.isNullOrEmpty() -> null | ||||
|                 !manualFetch && localManga.thumbnailUrl == remoteManga.thumbnail_url -> null | ||||
|                 localManga.isLocal() -> Instant.now().toEpochMilli() | ||||
|                 localManga.hasCustomCover(coverCache) -> { | ||||
|                     coverCache.deleteFromCache(localManga, false) | ||||
|                     null | ||||
|                 } | ||||
|                 else -> { | ||||
|                     coverCache.deleteFromCache(localManga, false) | ||||
|                     Instant.now().toEpochMilli() | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|         val thumbnailUrl = remoteManga.thumbnail_url?.takeIf { it.isNotEmpty() } | ||||
|  | ||||
|         return mangaRepository.update( | ||||
|             MangaUpdate( | ||||
|                 id = localManga.id, | ||||
|                 title = title, | ||||
|                 coverLastModified = coverLastModified, | ||||
|                 author = remoteManga.author, | ||||
|                 artist = remoteManga.artist, | ||||
|                 description = remoteManga.description, | ||||
|                 genre = remoteManga.getGenres(), | ||||
|                 thumbnailUrl = thumbnailUrl, | ||||
|                 status = remoteManga.status.toLong(), | ||||
|                 updateStrategy = remoteManga.update_strategy, | ||||
|                 initialized = true, | ||||
|             ), | ||||
|         ) | ||||
|     } | ||||
|  | ||||
|     suspend fun awaitUpdateFetchInterval( | ||||
|         manga: Manga, | ||||
|         dateTime: ZonedDateTime = ZonedDateTime.now(), | ||||
|         window: Pair<Long, Long> = fetchInterval.getWindow(dateTime), | ||||
|     ): Boolean { | ||||
|         return mangaRepository.update( | ||||
|             fetchInterval.toMangaUpdate(manga, dateTime, window), | ||||
|         ) | ||||
|     } | ||||
|  | ||||
|     suspend fun awaitUpdateLastUpdate(mangaId: Long): Boolean { | ||||
|         return mangaRepository.update(MangaUpdate(id = mangaId, lastUpdate = Instant.now().toEpochMilli())) | ||||
|     } | ||||
|  | ||||
|     suspend fun awaitUpdateCoverLastModified(mangaId: Long): Boolean { | ||||
|         return mangaRepository.update(MangaUpdate(id = mangaId, coverLastModified = Instant.now().toEpochMilli())) | ||||
|     } | ||||
|  | ||||
|     suspend fun awaitUpdateFavorite(mangaId: Long, favorite: Boolean): Boolean { | ||||
|         val dateAdded = when (favorite) { | ||||
|             true -> Instant.now().toEpochMilli() | ||||
|             false -> 0 | ||||
|         } | ||||
|         return mangaRepository.update( | ||||
|             MangaUpdate(id = mangaId, favorite = favorite, dateAdded = dateAdded), | ||||
|         ) | ||||
|     } | ||||
| } | ||||
| @@ -1,130 +0,0 @@ | ||||
| package eu.kanade.domain.manga.model | ||||
|  | ||||
| import eu.kanade.domain.base.BasePreferences | ||||
| import eu.kanade.tachiyomi.data.cache.CoverCache | ||||
| import eu.kanade.tachiyomi.source.model.SManga | ||||
| import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation | ||||
| import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode | ||||
| import tachiyomi.core.common.preference.TriState | ||||
| import tachiyomi.core.metadata.comicinfo.ComicInfo | ||||
| import tachiyomi.core.metadata.comicinfo.ComicInfoPublishingStatus | ||||
| import tachiyomi.domain.chapter.model.Chapter | ||||
| import tachiyomi.domain.manga.model.Manga | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
|  | ||||
| // TODO: move these into the domain model | ||||
| val Manga.readingMode: Long | ||||
|     get() = viewerFlags and ReadingMode.MASK.toLong() | ||||
|  | ||||
| val Manga.readerOrientation: Long | ||||
|     get() = viewerFlags and ReaderOrientation.MASK.toLong() | ||||
|  | ||||
| val Manga.downloadedFilter: TriState | ||||
|     get() { | ||||
|         if (forceDownloaded()) return TriState.ENABLED_IS | ||||
|         return when (downloadedFilterRaw) { | ||||
|             Manga.CHAPTER_SHOW_DOWNLOADED -> TriState.ENABLED_IS | ||||
|             Manga.CHAPTER_SHOW_NOT_DOWNLOADED -> TriState.ENABLED_NOT | ||||
|             else -> TriState.DISABLED | ||||
|         } | ||||
|     } | ||||
| fun Manga.chaptersFiltered(): Boolean { | ||||
|     return unreadFilter != TriState.DISABLED || | ||||
|         downloadedFilter != TriState.DISABLED || | ||||
|         bookmarkedFilter != TriState.DISABLED | ||||
| } | ||||
| fun Manga.forceDownloaded(): Boolean { | ||||
|     return favorite && Injekt.get<BasePreferences>().downloadedOnly().get() | ||||
| } | ||||
|  | ||||
| fun Manga.toSManga(): SManga = SManga.create().also { | ||||
|     it.url = url | ||||
|     it.title = title | ||||
|     it.artist = artist | ||||
|     it.author = author | ||||
|     it.description = description | ||||
|     it.genre = genre.orEmpty().joinToString() | ||||
|     it.status = status.toInt() | ||||
|     it.thumbnail_url = thumbnailUrl | ||||
|     it.initialized = initialized | ||||
| } | ||||
|  | ||||
| fun Manga.copyFrom(other: SManga): Manga { | ||||
|     val author = other.author ?: author | ||||
|     val artist = other.artist ?: artist | ||||
|     val description = other.description ?: description | ||||
|     val genres = if (other.genre != null) { | ||||
|         other.getGenres() | ||||
|     } else { | ||||
|         genre | ||||
|     } | ||||
|     val thumbnailUrl = other.thumbnail_url ?: thumbnailUrl | ||||
|     return this.copy( | ||||
|         author = author, | ||||
|         artist = artist, | ||||
|         description = description, | ||||
|         genre = genres, | ||||
|         thumbnailUrl = thumbnailUrl, | ||||
|         status = other.status.toLong(), | ||||
|         updateStrategy = other.update_strategy, | ||||
|         initialized = other.initialized && initialized, | ||||
|     ) | ||||
| } | ||||
|  | ||||
| fun SManga.toDomainManga(sourceId: Long): Manga { | ||||
|     return Manga.create().copy( | ||||
|         url = url, | ||||
|         title = title, | ||||
|         artist = artist, | ||||
|         author = author, | ||||
|         description = description, | ||||
|         genre = getGenres(), | ||||
|         status = status.toLong(), | ||||
|         thumbnailUrl = thumbnail_url, | ||||
|         updateStrategy = update_strategy, | ||||
|         initialized = initialized, | ||||
|         source = sourceId, | ||||
|     ) | ||||
| } | ||||
|  | ||||
| fun Manga.hasCustomCover(coverCache: CoverCache = Injekt.get()): Boolean { | ||||
|     return coverCache.getCustomCoverFile(id).exists() | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Creates a ComicInfo instance based on the manga and chapter metadata. | ||||
|  */ | ||||
| fun getComicInfo( | ||||
|     manga: Manga, | ||||
|     chapter: Chapter, | ||||
|     urls: List<String>, | ||||
|     categories: List<String>?, | ||||
|     sourceName: String, | ||||
| ) = ComicInfo( | ||||
|     title = ComicInfo.Title(chapter.name), | ||||
|     series = ComicInfo.Series(manga.title), | ||||
|     number = chapter.chapterNumber.takeIf { it >= 0 }?.let { | ||||
|         if ((it.rem(1) == 0.0)) { | ||||
|             ComicInfo.Number(it.toInt().toString()) | ||||
|         } else { | ||||
|             ComicInfo.Number(it.toString()) | ||||
|         } | ||||
|     }, | ||||
|     web = ComicInfo.Web(urls.joinToString(" ")), | ||||
|     summary = manga.description?.let { ComicInfo.Summary(it) }, | ||||
|     writer = manga.author?.let { ComicInfo.Writer(it) }, | ||||
|     penciller = manga.artist?.let { ComicInfo.Penciller(it) }, | ||||
|     translator = chapter.scanlator?.let { ComicInfo.Translator(it) }, | ||||
|     genre = manga.genre?.let { ComicInfo.Genre(it.joinToString()) }, | ||||
|     publishingStatus = ComicInfo.PublishingStatusTachiyomi( | ||||
|         ComicInfoPublishingStatus.toComicInfoValue(manga.status), | ||||
|     ), | ||||
|     categories = categories?.let { ComicInfo.CategoriesTachiyomi(it.joinToString()) }, | ||||
|     source = ComicInfo.SourceMihon(sourceName), | ||||
|     inker = null, | ||||
|     colorist = null, | ||||
|     letterer = null, | ||||
|     coverArtist = null, | ||||
|     tags = null, | ||||
| ) | ||||
| @@ -1,42 +0,0 @@ | ||||
| package eu.kanade.domain.source.interactor | ||||
|  | ||||
| import eu.kanade.domain.source.service.SourcePreferences | ||||
| import kotlinx.coroutines.flow.Flow | ||||
| import kotlinx.coroutines.flow.combine | ||||
| import kotlinx.coroutines.flow.distinctUntilChanged | ||||
| import tachiyomi.domain.source.model.Pin | ||||
| import tachiyomi.domain.source.model.Pins | ||||
| import tachiyomi.domain.source.model.Source | ||||
| import tachiyomi.domain.source.repository.SourceRepository | ||||
| import tachiyomi.source.local.isLocal | ||||
|  | ||||
| class GetEnabledSources( | ||||
|     private val repository: SourceRepository, | ||||
|     private val preferences: SourcePreferences, | ||||
| ) { | ||||
|  | ||||
|     fun subscribe(): Flow<List<Source>> { | ||||
|         return combine( | ||||
|             preferences.pinnedSources().changes(), | ||||
|             preferences.enabledLanguages().changes(), | ||||
|             preferences.disabledSources().changes(), | ||||
|             preferences.lastUsedSource().changes(), | ||||
|             repository.getSources(), | ||||
|         ) { pinnedSourceIds, enabledLanguages, disabledSources, lastUsedSource, sources -> | ||||
|             sources | ||||
|                 .filter { it.lang in enabledLanguages || it.isLocal() } | ||||
|                 .filterNot { it.id.toString() in disabledSources } | ||||
|                 .sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name }) | ||||
|                 .flatMap { | ||||
|                     val flag = if ("${it.id}" in pinnedSourceIds) Pins.pinned else Pins.unpinned | ||||
|                     val source = it.copy(pin = flag) | ||||
|                     val toFlatten = mutableListOf(source) | ||||
|                     if (source.id == lastUsedSource) { | ||||
|                         toFlatten.add(source.copy(isUsedLast = true, pin = source.pin - Pin.Actual)) | ||||
|                     } | ||||
|                     toFlatten | ||||
|                 } | ||||
|         } | ||||
|             .distinctUntilChanged() | ||||
|     } | ||||
| } | ||||
| @@ -1,34 +0,0 @@ | ||||
| package eu.kanade.domain.source.interactor | ||||
|  | ||||
| import eu.kanade.domain.source.service.SourcePreferences | ||||
| import eu.kanade.tachiyomi.util.system.LocaleHelper | ||||
| import kotlinx.coroutines.flow.Flow | ||||
| import kotlinx.coroutines.flow.combine | ||||
| import tachiyomi.domain.source.model.Source | ||||
| import tachiyomi.domain.source.repository.SourceRepository | ||||
| import java.util.SortedMap | ||||
|  | ||||
| class GetLanguagesWithSources( | ||||
|     private val repository: SourceRepository, | ||||
|     private val preferences: SourcePreferences, | ||||
| ) { | ||||
|  | ||||
|     fun subscribe(): Flow<SortedMap<String, List<Source>>> { | ||||
|         return combine( | ||||
|             preferences.enabledLanguages().changes(), | ||||
|             preferences.disabledSources().changes(), | ||||
|             repository.getOnlineSources(), | ||||
|         ) { enabledLanguage, disabledSource, onlineSources -> | ||||
|             val sortedSources = onlineSources.sortedWith( | ||||
|                 compareBy<Source> { it.id.toString() in disabledSource } | ||||
|                     .thenBy(String.CASE_INSENSITIVE_ORDER) { it.name }, | ||||
|             ) | ||||
|  | ||||
|             sortedSources | ||||
|                 .groupBy { it.lang } | ||||
|                 .toSortedMap( | ||||
|                     compareBy<String> { it !in enabledLanguage }.then(LocaleHelper.comparator), | ||||
|                 ) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,57 +0,0 @@ | ||||
| package eu.kanade.domain.source.interactor | ||||
|  | ||||
| import eu.kanade.domain.source.service.SourcePreferences | ||||
| import kotlinx.coroutines.flow.Flow | ||||
| import kotlinx.coroutines.flow.combine | ||||
| import tachiyomi.core.common.util.lang.compareToWithCollator | ||||
| import tachiyomi.domain.source.model.Source | ||||
| import tachiyomi.domain.source.repository.SourceRepository | ||||
| import tachiyomi.source.local.isLocal | ||||
| import java.util.Collections | ||||
|  | ||||
| class GetSourcesWithFavoriteCount( | ||||
|     private val repository: SourceRepository, | ||||
|     private val preferences: SourcePreferences, | ||||
| ) { | ||||
|  | ||||
|     fun subscribe(): Flow<List<Pair<Source, Long>>> { | ||||
|         return combine( | ||||
|             preferences.migrationSortingDirection().changes(), | ||||
|             preferences.migrationSortingMode().changes(), | ||||
|             repository.getSourcesWithFavoriteCount(), | ||||
|         ) { direction, mode, list -> | ||||
|             list | ||||
|                 .filterNot { it.first.isLocal() } | ||||
|                 .sortedWith(sortFn(direction, mode)) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private fun sortFn( | ||||
|         direction: SetMigrateSorting.Direction, | ||||
|         sorting: SetMigrateSorting.Mode, | ||||
|     ): java.util.Comparator<Pair<Source, Long>> { | ||||
|         val sortFn: (Pair<Source, Long>, Pair<Source, Long>) -> Int = { a, b -> | ||||
|             when (sorting) { | ||||
|                 SetMigrateSorting.Mode.ALPHABETICAL -> { | ||||
|                     when { | ||||
|                         a.first.isStub && !b.first.isStub -> -1 | ||||
|                         b.first.isStub && !a.first.isStub -> 1 | ||||
|                         else -> a.first.name.lowercase().compareToWithCollator(b.first.name.lowercase()) | ||||
|                     } | ||||
|                 } | ||||
|                 SetMigrateSorting.Mode.TOTAL -> { | ||||
|                     when { | ||||
|                         a.first.isStub && !b.first.isStub -> -1 | ||||
|                         b.first.isStub && !a.first.isStub -> 1 | ||||
|                         else -> a.second.compareTo(b.second) | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return when (direction) { | ||||
|             SetMigrateSorting.Direction.ASCENDING -> Comparator(sortFn) | ||||
|             SetMigrateSorting.Direction.DESCENDING -> Collections.reverseOrder(sortFn) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,23 +0,0 @@ | ||||
| package eu.kanade.domain.source.interactor | ||||
|  | ||||
| import eu.kanade.domain.source.service.SourcePreferences | ||||
|  | ||||
| class SetMigrateSorting( | ||||
|     private val preferences: SourcePreferences, | ||||
| ) { | ||||
|  | ||||
|     fun await(mode: Mode, direction: Direction) { | ||||
|         preferences.migrationSortingMode().set(mode) | ||||
|         preferences.migrationSortingDirection().set(direction) | ||||
|     } | ||||
|  | ||||
|     enum class Mode { | ||||
|         ALPHABETICAL, | ||||
|         TOTAL, | ||||
|     } | ||||
|  | ||||
|     enum class Direction { | ||||
|         ASCENDING, | ||||
|         DESCENDING, | ||||
|     } | ||||
| } | ||||
| @@ -1,16 +0,0 @@ | ||||
| package eu.kanade.domain.source.interactor | ||||
|  | ||||
| import eu.kanade.domain.source.service.SourcePreferences | ||||
| import tachiyomi.core.common.preference.getAndSet | ||||
|  | ||||
| class ToggleLanguage( | ||||
|     val preferences: SourcePreferences, | ||||
| ) { | ||||
|  | ||||
|     fun await(language: String) { | ||||
|         val isEnabled = language in preferences.enabledLanguages().get() | ||||
|         preferences.enabledLanguages().getAndSet { enabled -> | ||||
|             if (isEnabled) enabled.minus(language) else enabled.plus(language) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,31 +0,0 @@ | ||||
| package eu.kanade.domain.source.interactor | ||||
|  | ||||
| import eu.kanade.domain.source.service.SourcePreferences | ||||
| import tachiyomi.core.common.preference.getAndSet | ||||
| import tachiyomi.domain.source.model.Source | ||||
|  | ||||
| class ToggleSource( | ||||
|     private val preferences: SourcePreferences, | ||||
| ) { | ||||
|  | ||||
|     fun await(source: Source, enable: Boolean = isEnabled(source.id)) { | ||||
|         await(source.id, enable) | ||||
|     } | ||||
|  | ||||
|     fun await(sourceId: Long, enable: Boolean = isEnabled(sourceId)) { | ||||
|         preferences.disabledSources().getAndSet { disabled -> | ||||
|             if (enable) disabled.minus("$sourceId") else disabled.plus("$sourceId") | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fun await(sourceIds: List<Long>, enable: Boolean) { | ||||
|         val transformedSourceIds = sourceIds.map { it.toString() } | ||||
|         preferences.disabledSources().getAndSet { disabled -> | ||||
|             if (enable) disabled.minus(transformedSourceIds) else disabled.plus(transformedSourceIds) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private fun isEnabled(sourceId: Long): Boolean { | ||||
|         return sourceId.toString() in preferences.disabledSources().get() | ||||
|     } | ||||
| } | ||||
| @@ -1,17 +0,0 @@ | ||||
| package eu.kanade.domain.source.interactor | ||||
|  | ||||
| import eu.kanade.domain.source.service.SourcePreferences | ||||
| import tachiyomi.core.common.preference.getAndSet | ||||
| import tachiyomi.domain.source.model.Source | ||||
|  | ||||
| class ToggleSourcePin( | ||||
|     private val preferences: SourcePreferences, | ||||
| ) { | ||||
|  | ||||
|     fun await(source: Source) { | ||||
|         val isPinned = source.id.toString() in preferences.pinnedSources().get() | ||||
|         preferences.pinnedSources().getAndSet { pinned -> | ||||
|             if (isPinned) pinned.minus("${source.id}") else pinned.plus("${source.id}") | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,16 +0,0 @@ | ||||
| package eu.kanade.domain.source.model | ||||
|  | ||||
| import androidx.compose.ui.graphics.ImageBitmap | ||||
| import androidx.compose.ui.graphics.asImageBitmap | ||||
| import androidx.core.graphics.drawable.toBitmap | ||||
| import eu.kanade.tachiyomi.extension.ExtensionManager | ||||
| import tachiyomi.domain.source.model.Source | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
|  | ||||
| val Source.icon: ImageBitmap? | ||||
|     get() { | ||||
|         return Injekt.get<ExtensionManager>().getAppIconForSource(id) | ||||
|             ?.toBitmap() | ||||
|             ?.asImageBitmap() | ||||
|     } | ||||
| @@ -1,56 +0,0 @@ | ||||
| package eu.kanade.domain.source.service | ||||
|  | ||||
| import eu.kanade.domain.source.interactor.SetMigrateSorting | ||||
| import eu.kanade.tachiyomi.util.system.LocaleHelper | ||||
| import tachiyomi.core.common.preference.Preference | ||||
| import tachiyomi.core.common.preference.PreferenceStore | ||||
| import tachiyomi.core.common.preference.getEnum | ||||
| import tachiyomi.domain.library.model.LibraryDisplayMode | ||||
|  | ||||
| class SourcePreferences( | ||||
|     private val preferenceStore: PreferenceStore, | ||||
| ) { | ||||
|  | ||||
|     fun sourceDisplayMode() = preferenceStore.getObject( | ||||
|         "pref_display_mode_catalogue", | ||||
|         LibraryDisplayMode.default, | ||||
|         LibraryDisplayMode.Serializer::serialize, | ||||
|         LibraryDisplayMode.Serializer::deserialize, | ||||
|     ) | ||||
|  | ||||
|     fun enabledLanguages() = preferenceStore.getStringSet("source_languages", LocaleHelper.getDefaultEnabledLanguages()) | ||||
|  | ||||
|     fun disabledSources() = preferenceStore.getStringSet("hidden_catalogues", emptySet()) | ||||
|  | ||||
|     fun pinnedSources() = preferenceStore.getStringSet("pinned_catalogues", emptySet()) | ||||
|  | ||||
|     fun lastUsedSource() = preferenceStore.getLong( | ||||
|         Preference.appStateKey("last_catalogue_source"), | ||||
|         -1, | ||||
|     ) | ||||
|  | ||||
|     fun showNsfwSource() = preferenceStore.getBoolean("show_nsfw_source", true) | ||||
|  | ||||
|     fun migrationSortingMode() = preferenceStore.getEnum("pref_migration_sorting", SetMigrateSorting.Mode.ALPHABETICAL) | ||||
|  | ||||
|     fun migrationSortingDirection() = preferenceStore.getEnum( | ||||
|         "pref_migration_direction", | ||||
|         SetMigrateSorting.Direction.ASCENDING, | ||||
|     ) | ||||
|  | ||||
|     fun hideInLibraryItems() = preferenceStore.getBoolean("browse_hide_in_library_items", false) | ||||
|  | ||||
|     fun extensionRepos() = preferenceStore.getStringSet("extension_repos", emptySet()) | ||||
|  | ||||
|     fun extensionUpdatesCount() = preferenceStore.getInt("ext_updates_count", 0) | ||||
|  | ||||
|     fun trustedExtensions() = preferenceStore.getStringSet( | ||||
|         Preference.appStateKey("trusted_extensions"), | ||||
|         emptySet(), | ||||
|     ) | ||||
|  | ||||
|     fun globalSearchFilterState() = preferenceStore.getBoolean( | ||||
|         Preference.appStateKey("has_filters_toggle_state"), | ||||
|         false, | ||||
|     ) | ||||
| } | ||||
| @@ -1,107 +0,0 @@ | ||||
| package eu.kanade.domain.track.interactor | ||||
|  | ||||
| import eu.kanade.domain.track.model.toDbTrack | ||||
| import eu.kanade.domain.track.model.toDomainTrack | ||||
| import eu.kanade.tachiyomi.data.database.models.Track | ||||
| import eu.kanade.tachiyomi.data.track.EnhancedTracker | ||||
| import eu.kanade.tachiyomi.data.track.Tracker | ||||
| import eu.kanade.tachiyomi.source.Source | ||||
| import eu.kanade.tachiyomi.util.lang.convertEpochMillisZone | ||||
| import logcat.LogPriority | ||||
| import tachiyomi.core.common.util.lang.withIOContext | ||||
| import tachiyomi.core.common.util.lang.withNonCancellableContext | ||||
| import tachiyomi.core.common.util.system.logcat | ||||
| import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId | ||||
| import tachiyomi.domain.history.interactor.GetHistory | ||||
| import tachiyomi.domain.manga.model.Manga | ||||
| import tachiyomi.domain.track.interactor.GetTracks | ||||
| import tachiyomi.domain.track.interactor.InsertTrack | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
| import java.time.ZoneOffset | ||||
|  | ||||
| class AddTracks( | ||||
|     private val getTracks: GetTracks, | ||||
|     private val insertTrack: InsertTrack, | ||||
|     private val syncChapterProgressWithTrack: SyncChapterProgressWithTrack, | ||||
|     private val getChaptersByMangaId: GetChaptersByMangaId, | ||||
| ) { | ||||
|  | ||||
|     // TODO: update all trackers based on common data | ||||
|     suspend fun bind(tracker: Tracker, item: Track, mangaId: Long) = withNonCancellableContext { | ||||
|         withIOContext { | ||||
|             val allChapters = getChaptersByMangaId.await(mangaId) | ||||
|             val hasReadChapters = allChapters.any { it.read } | ||||
|             tracker.bind(item, hasReadChapters) | ||||
|  | ||||
|             var track = item.toDomainTrack(idRequired = false) ?: return@withIOContext | ||||
|  | ||||
|             insertTrack.await(track) | ||||
|  | ||||
|             // TODO: merge into [SyncChapterProgressWithTrack]? | ||||
|             // Update chapter progress if newer chapters marked read locally | ||||
|             if (hasReadChapters) { | ||||
|                 val latestLocalReadChapterNumber = allChapters | ||||
|                     .sortedBy { it.chapterNumber } | ||||
|                     .takeWhile { it.read } | ||||
|                     .lastOrNull() | ||||
|                     ?.chapterNumber ?: -1.0 | ||||
|  | ||||
|                 if (latestLocalReadChapterNumber > track.lastChapterRead) { | ||||
|                     track = track.copy( | ||||
|                         lastChapterRead = latestLocalReadChapterNumber, | ||||
|                     ) | ||||
|                     tracker.setRemoteLastChapterRead(track.toDbTrack(), latestLocalReadChapterNumber.toInt()) | ||||
|                 } | ||||
|  | ||||
|                 if (track.startDate <= 0) { | ||||
|                     val firstReadChapterDate = Injekt.get<GetHistory>().await(mangaId) | ||||
|                         .sortedBy { it.readAt } | ||||
|                         .firstOrNull() | ||||
|                         ?.readAt | ||||
|  | ||||
|                     firstReadChapterDate?.let { | ||||
|                         val startDate = firstReadChapterDate.time.convertEpochMillisZone( | ||||
|                             ZoneOffset.systemDefault(), | ||||
|                             ZoneOffset.UTC, | ||||
|                         ) | ||||
|                         track = track.copy( | ||||
|                             startDate = startDate, | ||||
|                         ) | ||||
|                         tracker.setRemoteStartDate(track.toDbTrack(), startDate) | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             syncChapterProgressWithTrack.await(mangaId, track, tracker) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     suspend fun bindEnhancedTrackers(manga: Manga, source: Source) = withNonCancellableContext { | ||||
|         withIOContext { | ||||
|             getTracks.await(manga.id) | ||||
|                 .filterIsInstance<EnhancedTracker>() | ||||
|                 .filter { it.accept(source) } | ||||
|                 .forEach { service -> | ||||
|                     try { | ||||
|                         service.match(manga)?.let { track -> | ||||
|                             track.manga_id = manga.id | ||||
|                             (service as Tracker).bind(track) | ||||
|                             insertTrack.await(track.toDomainTrack()!!) | ||||
|  | ||||
|                             syncChapterProgressWithTrack.await( | ||||
|                                 manga.id, | ||||
|                                 track.toDomainTrack()!!, | ||||
|                                 service, | ||||
|                             ) | ||||
|                         } | ||||
|                     } catch (e: Exception) { | ||||
|                         logcat( | ||||
|                             LogPriority.WARN, | ||||
|                             e, | ||||
|                         ) { "Could not match manga: ${manga.title} with service $service" } | ||||
|                     } | ||||
|                 } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,46 +0,0 @@ | ||||
| package eu.kanade.domain.track.interactor | ||||
|  | ||||
| import eu.kanade.domain.track.model.toDbTrack | ||||
| import eu.kanade.domain.track.model.toDomainTrack | ||||
| import eu.kanade.tachiyomi.data.track.Tracker | ||||
| import eu.kanade.tachiyomi.data.track.TrackerManager | ||||
| import kotlinx.coroutines.async | ||||
| import kotlinx.coroutines.awaitAll | ||||
| import kotlinx.coroutines.supervisorScope | ||||
| import tachiyomi.domain.track.interactor.GetTracks | ||||
| import tachiyomi.domain.track.interactor.InsertTrack | ||||
|  | ||||
| class RefreshTracks( | ||||
|     private val getTracks: GetTracks, | ||||
|     private val trackerManager: TrackerManager, | ||||
|     private val insertTrack: InsertTrack, | ||||
|     private val syncChapterProgressWithTrack: SyncChapterProgressWithTrack, | ||||
| ) { | ||||
|  | ||||
|     /** | ||||
|      * Fetches updated tracking data from all logged in trackers. | ||||
|      * | ||||
|      * @return Failed updates. | ||||
|      */ | ||||
|     suspend fun await(mangaId: Long): List<Pair<Tracker?, Throwable>> { | ||||
|         return supervisorScope { | ||||
|             return@supervisorScope getTracks.await(mangaId) | ||||
|                 .map { it to trackerManager.get(it.trackerId) } | ||||
|                 .filter { (_, service) -> service?.isLoggedIn == true } | ||||
|                 .map { (track, service) -> | ||||
|                     async { | ||||
|                         return@async try { | ||||
|                             val updatedTrack = service!!.refresh(track.toDbTrack()).toDomainTrack()!! | ||||
|                             insertTrack.await(updatedTrack) | ||||
|                             syncChapterProgressWithTrack.await(mangaId, updatedTrack, service) | ||||
|                             null | ||||
|                         } catch (e: Throwable) { | ||||
|                             service to e | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|                 .awaitAll() | ||||
|                 .filterNotNull() | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,51 +0,0 @@ | ||||
| package eu.kanade.domain.track.interactor | ||||
|  | ||||
| import eu.kanade.domain.track.model.toDbTrack | ||||
| import eu.kanade.tachiyomi.data.track.EnhancedTracker | ||||
| import eu.kanade.tachiyomi.data.track.Tracker | ||||
| import logcat.LogPriority | ||||
| import tachiyomi.core.common.util.system.logcat | ||||
| import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId | ||||
| import tachiyomi.domain.chapter.interactor.UpdateChapter | ||||
| import tachiyomi.domain.chapter.model.toChapterUpdate | ||||
| import tachiyomi.domain.track.interactor.InsertTrack | ||||
| import tachiyomi.domain.track.model.Track | ||||
| import kotlin.math.max | ||||
|  | ||||
| class SyncChapterProgressWithTrack( | ||||
|     private val updateChapter: UpdateChapter, | ||||
|     private val insertTrack: InsertTrack, | ||||
|     private val getChaptersByMangaId: GetChaptersByMangaId, | ||||
| ) { | ||||
|  | ||||
|     suspend fun await( | ||||
|         mangaId: Long, | ||||
|         remoteTrack: Track, | ||||
|         tracker: Tracker, | ||||
|     ) { | ||||
|         if (tracker !is EnhancedTracker) { | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         val sortedChapters = getChaptersByMangaId.await(mangaId) | ||||
|             .sortedBy { it.chapterNumber } | ||||
|             .filter { it.isRecognizedNumber } | ||||
|  | ||||
|         val chapterUpdates = sortedChapters | ||||
|             .filter { chapter -> chapter.chapterNumber <= remoteTrack.lastChapterRead && !chapter.read } | ||||
|             .map { it.copy(read = true).toChapterUpdate() } | ||||
|  | ||||
|         // only take into account continuous reading | ||||
|         val localLastRead = sortedChapters.takeWhile { it.read }.lastOrNull()?.chapterNumber ?: 0F | ||||
|         val lastRead = max(remoteTrack.lastChapterRead, localLastRead.toDouble()) | ||||
|         val updatedTrack = remoteTrack.copy(lastChapterRead = lastRead) | ||||
|  | ||||
|         try { | ||||
|             tracker.update(updatedTrack.toDbTrack()) | ||||
|             updateChapter.awaitAll(chapterUpdates) | ||||
|             insertTrack.await(updatedTrack) | ||||
|         } catch (e: Throwable) { | ||||
|             logcat(LogPriority.WARN, e) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,59 +0,0 @@ | ||||
| package eu.kanade.domain.track.interactor | ||||
|  | ||||
| import android.content.Context | ||||
| import eu.kanade.domain.track.model.toDbTrack | ||||
| import eu.kanade.domain.track.model.toDomainTrack | ||||
| import eu.kanade.domain.track.service.DelayedTrackingUpdateJob | ||||
| import eu.kanade.domain.track.store.DelayedTrackingStore | ||||
| import eu.kanade.tachiyomi.data.track.TrackerManager | ||||
| import kotlinx.coroutines.async | ||||
| import kotlinx.coroutines.awaitAll | ||||
| import logcat.LogPriority | ||||
| import tachiyomi.core.common.util.lang.withNonCancellableContext | ||||
| import tachiyomi.core.common.util.system.logcat | ||||
| import tachiyomi.domain.track.interactor.GetTracks | ||||
| import tachiyomi.domain.track.interactor.InsertTrack | ||||
|  | ||||
| class TrackChapter( | ||||
|     private val getTracks: GetTracks, | ||||
|     private val trackerManager: TrackerManager, | ||||
|     private val insertTrack: InsertTrack, | ||||
|     private val delayedTrackingStore: DelayedTrackingStore, | ||||
| ) { | ||||
|  | ||||
|     suspend fun await(context: Context, mangaId: Long, chapterNumber: Double, setupJobOnFailure: Boolean = true) { | ||||
|         withNonCancellableContext { | ||||
|             val tracks = getTracks.await(mangaId) | ||||
|             if (tracks.isEmpty()) return@withNonCancellableContext | ||||
|  | ||||
|             tracks.mapNotNull { track -> | ||||
|                 val service = trackerManager.get(track.trackerId) | ||||
|                 if (service == null || !service.isLoggedIn || chapterNumber <= track.lastChapterRead) { | ||||
|                     return@mapNotNull null | ||||
|                 } | ||||
|  | ||||
|                 async { | ||||
|                     runCatching { | ||||
|                         try { | ||||
|                             val updatedTrack = service.refresh(track.toDbTrack()) | ||||
|                                 .toDomainTrack(idRequired = true)!! | ||||
|                                 .copy(lastChapterRead = chapterNumber) | ||||
|                             service.update(updatedTrack.toDbTrack(), true) | ||||
|                             insertTrack.await(updatedTrack) | ||||
|                             delayedTrackingStore.remove(track.id) | ||||
|                         } catch (e: Exception) { | ||||
|                             delayedTrackingStore.add(track.id, chapterNumber) | ||||
|                             if (setupJobOnFailure) { | ||||
|                                 DelayedTrackingUpdateJob.setupTask(context) | ||||
|                             } | ||||
|                             throw e | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|                 .awaitAll() | ||||
|                 .mapNotNull { it.exceptionOrNull() } | ||||
|                 .forEach { logcat(LogPriority.WARN, it) } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,48 +0,0 @@ | ||||
| package eu.kanade.domain.track.model | ||||
|  | ||||
| import tachiyomi.domain.track.model.Track | ||||
| import eu.kanade.tachiyomi.data.database.models.Track as DbTrack | ||||
|  | ||||
| fun Track.copyPersonalFrom(other: Track): Track { | ||||
|     return this.copy( | ||||
|         lastChapterRead = other.lastChapterRead, | ||||
|         score = other.score, | ||||
|         status = other.status, | ||||
|         startDate = other.startDate, | ||||
|         finishDate = other.finishDate, | ||||
|     ) | ||||
| } | ||||
|  | ||||
| fun Track.toDbTrack(): DbTrack = DbTrack.create(trackerId).also { | ||||
|     it.id = id | ||||
|     it.manga_id = mangaId | ||||
|     it.remote_id = remoteId | ||||
|     it.library_id = libraryId | ||||
|     it.title = title | ||||
|     it.last_chapter_read = lastChapterRead | ||||
|     it.total_chapters = totalChapters | ||||
|     it.status = status | ||||
|     it.score = score | ||||
|     it.tracking_url = remoteUrl | ||||
|     it.started_reading_date = startDate | ||||
|     it.finished_reading_date = finishDate | ||||
| } | ||||
|  | ||||
| fun DbTrack.toDomainTrack(idRequired: Boolean = true): Track? { | ||||
|     val trackId = id ?: if (!idRequired) -1 else return null | ||||
|     return Track( | ||||
|         id = trackId, | ||||
|         mangaId = manga_id, | ||||
|         trackerId = tracker_id, | ||||
|         remoteId = remote_id, | ||||
|         libraryId = library_id, | ||||
|         title = title, | ||||
|         lastChapterRead = last_chapter_read, | ||||
|         totalChapters = total_chapters, | ||||
|         status = status, | ||||
|         score = score, | ||||
|         remoteUrl = tracking_url, | ||||
|         startDate = started_reading_date, | ||||
|         finishDate = finished_reading_date, | ||||
|     ) | ||||
| } | ||||
| @@ -1,72 +0,0 @@ | ||||
| package eu.kanade.domain.track.service | ||||
|  | ||||
| import android.content.Context | ||||
| import androidx.work.BackoffPolicy | ||||
| import androidx.work.Constraints | ||||
| import androidx.work.CoroutineWorker | ||||
| import androidx.work.ExistingWorkPolicy | ||||
| import androidx.work.NetworkType | ||||
| import androidx.work.OneTimeWorkRequestBuilder | ||||
| import androidx.work.WorkerParameters | ||||
| import eu.kanade.domain.track.interactor.TrackChapter | ||||
| import eu.kanade.domain.track.store.DelayedTrackingStore | ||||
| import eu.kanade.tachiyomi.util.system.workManager | ||||
| import logcat.LogPriority | ||||
| import tachiyomi.core.common.util.lang.withIOContext | ||||
| import tachiyomi.core.common.util.system.logcat | ||||
| import tachiyomi.domain.track.interactor.GetTracks | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
| import java.util.concurrent.TimeUnit | ||||
|  | ||||
| class DelayedTrackingUpdateJob(private val context: Context, workerParams: WorkerParameters) : | ||||
|     CoroutineWorker(context, workerParams) { | ||||
|  | ||||
|     override suspend fun doWork(): Result { | ||||
|         if (runAttemptCount > 3) { | ||||
|             return Result.failure() | ||||
|         } | ||||
|  | ||||
|         val getTracks = Injekt.get<GetTracks>() | ||||
|         val trackChapter = Injekt.get<TrackChapter>() | ||||
|  | ||||
|         val delayedTrackingStore = Injekt.get<DelayedTrackingStore>() | ||||
|  | ||||
|         withIOContext { | ||||
|             delayedTrackingStore.getItems() | ||||
|                 .mapNotNull { | ||||
|                     val track = getTracks.awaitOne(it.trackId) | ||||
|                     if (track == null) { | ||||
|                         delayedTrackingStore.remove(it.trackId) | ||||
|                     } | ||||
|                     track?.copy(lastChapterRead = it.lastChapterRead.toDouble()) | ||||
|                 } | ||||
|                 .forEach { track -> | ||||
|                     logcat(LogPriority.DEBUG) { | ||||
|                         "Updating delayed track item: ${track.mangaId}, last chapter read: ${track.lastChapterRead}" | ||||
|                     } | ||||
|                     trackChapter.await(context, track.mangaId, track.lastChapterRead, setupJobOnFailure = false) | ||||
|                 } | ||||
|         } | ||||
|  | ||||
|         return if (delayedTrackingStore.getItems().isEmpty()) Result.success() else Result.retry() | ||||
|     } | ||||
|  | ||||
|     companion object { | ||||
|         private const val TAG = "DelayedTrackingUpdate" | ||||
|  | ||||
|         fun setupTask(context: Context) { | ||||
|             val constraints = Constraints( | ||||
|                 requiredNetworkType = NetworkType.CONNECTED, | ||||
|             ) | ||||
|  | ||||
|             val request = OneTimeWorkRequestBuilder<DelayedTrackingUpdateJob>() | ||||
|                 .setConstraints(constraints) | ||||
|                 .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 5, TimeUnit.MINUTES) | ||||
|                 .addTag(TAG) | ||||
|                 .build() | ||||
|  | ||||
|             context.workManager.enqueueUniqueWork(TAG, ExistingWorkPolicy.REPLACE, request) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,38 +0,0 @@ | ||||
| package eu.kanade.domain.track.service | ||||
|  | ||||
| import eu.kanade.tachiyomi.data.track.Tracker | ||||
| import eu.kanade.tachiyomi.data.track.anilist.Anilist | ||||
| import tachiyomi.core.common.preference.Preference | ||||
| import tachiyomi.core.common.preference.PreferenceStore | ||||
|  | ||||
| class TrackPreferences( | ||||
|     private val preferenceStore: PreferenceStore, | ||||
| ) { | ||||
|  | ||||
|     fun trackUsername(tracker: Tracker) = preferenceStore.getString( | ||||
|         Preference.privateKey("pref_mangasync_username_${tracker.id}"), | ||||
|         "", | ||||
|     ) | ||||
|  | ||||
|     fun trackPassword(tracker: Tracker) = preferenceStore.getString( | ||||
|         Preference.privateKey("pref_mangasync_password_${tracker.id}"), | ||||
|         "", | ||||
|     ) | ||||
|  | ||||
|     fun trackAuthExpired(tracker: Tracker) = preferenceStore.getBoolean( | ||||
|         Preference.privateKey("pref_tracker_auth_expired_${tracker.id}"), | ||||
|         false, | ||||
|     ) | ||||
|  | ||||
|     fun setCredentials(tracker: Tracker, username: String, password: String) { | ||||
|         trackUsername(tracker).set(username) | ||||
|         trackPassword(tracker).set(password) | ||||
|         trackAuthExpired(tracker).set(false) | ||||
|     } | ||||
|  | ||||
|     fun trackToken(tracker: Tracker) = preferenceStore.getString(Preference.privateKey("track_token_${tracker.id}"), "") | ||||
|  | ||||
|     fun anilistScoreType() = preferenceStore.getString("anilist_score_type", Anilist.POINT_10) | ||||
|  | ||||
|     fun autoUpdateTrack() = preferenceStore.getBoolean("pref_auto_update_manga_sync_key", true) | ||||
| } | ||||
| @@ -1,44 +0,0 @@ | ||||
| package eu.kanade.domain.track.store | ||||
|  | ||||
| import android.content.Context | ||||
| import androidx.core.content.edit | ||||
| import logcat.LogPriority | ||||
| import tachiyomi.core.common.util.system.logcat | ||||
|  | ||||
| class DelayedTrackingStore(context: Context) { | ||||
|  | ||||
|     /** | ||||
|      * Preference file where queued tracking updates are stored. | ||||
|      */ | ||||
|     private val preferences = context.getSharedPreferences("tracking_queue", Context.MODE_PRIVATE) | ||||
|  | ||||
|     fun add(trackId: Long, lastChapterRead: Double) { | ||||
|         val previousLastChapterRead = preferences.getFloat(trackId.toString(), 0f) | ||||
|         if (lastChapterRead > previousLastChapterRead) { | ||||
|             logcat(LogPriority.DEBUG) { "Queuing track item: $trackId, last chapter read: $lastChapterRead" } | ||||
|             preferences.edit { | ||||
|                 putFloat(trackId.toString(), lastChapterRead.toFloat()) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fun remove(trackId: Long) { | ||||
|         preferences.edit { | ||||
|             remove(trackId.toString()) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fun getItems(): List<DelayedTrackingItem> { | ||||
|         return preferences.all.mapNotNull { | ||||
|             DelayedTrackingItem( | ||||
|                 trackId = it.key.toLong(), | ||||
|                 lastChapterRead = it.value.toString().toFloat(), | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     data class DelayedTrackingItem( | ||||
|         val trackId: Long, | ||||
|         val lastChapterRead: Float, | ||||
|     ) | ||||
| } | ||||
| @@ -1,43 +0,0 @@ | ||||
| package eu.kanade.domain.ui | ||||
|  | ||||
| import eu.kanade.domain.ui.model.AppTheme | ||||
| import eu.kanade.domain.ui.model.TabletUiMode | ||||
| import eu.kanade.domain.ui.model.ThemeMode | ||||
| import eu.kanade.tachiyomi.util.system.DeviceUtil | ||||
| import eu.kanade.tachiyomi.util.system.isDynamicColorAvailable | ||||
| import tachiyomi.core.common.preference.PreferenceStore | ||||
| import tachiyomi.core.common.preference.getEnum | ||||
| import java.time.format.DateTimeFormatter | ||||
| import java.time.format.FormatStyle | ||||
| import java.util.Locale | ||||
|  | ||||
| class UiPreferences( | ||||
|     private val preferenceStore: PreferenceStore, | ||||
| ) { | ||||
|  | ||||
|     fun themeMode() = preferenceStore.getEnum("pref_theme_mode_key", ThemeMode.SYSTEM) | ||||
|  | ||||
|     fun appTheme() = preferenceStore.getEnum( | ||||
|         "pref_app_theme", | ||||
|         if (DeviceUtil.isDynamicColorAvailable) { | ||||
|             AppTheme.MONET | ||||
|         } else { | ||||
|             AppTheme.DEFAULT | ||||
|         }, | ||||
|     ) | ||||
|  | ||||
|     fun themeDarkAmoled() = preferenceStore.getBoolean("pref_theme_dark_amoled_key", false) | ||||
|  | ||||
|     fun relativeTime() = preferenceStore.getBoolean("relative_time_v2", true) | ||||
|  | ||||
|     fun dateFormat() = preferenceStore.getString("app_date_format", "") | ||||
|  | ||||
|     fun tabletUiMode() = preferenceStore.getEnum("tablet_ui_mode", TabletUiMode.AUTOMATIC) | ||||
|  | ||||
|     companion object { | ||||
|         fun dateFormat(format: String): DateTimeFormatter = when (format) { | ||||
|             "" -> DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT) | ||||
|             else -> DateTimeFormatter.ofPattern(format, Locale.getDefault()) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,28 +0,0 @@ | ||||
| package eu.kanade.domain.ui.model | ||||
|  | ||||
| import dev.icerock.moko.resources.StringResource | ||||
| import eu.kanade.tachiyomi.util.system.isDevFlavor | ||||
| import eu.kanade.tachiyomi.util.system.isPreviewBuildType | ||||
| import tachiyomi.i18n.MR | ||||
|  | ||||
| enum class AppTheme(val titleRes: StringResource?) { | ||||
|     DEFAULT(MR.strings.label_default), | ||||
|     MONET(MR.strings.theme_monet), | ||||
|     GREEN_APPLE(MR.strings.theme_greenapple), | ||||
|     LAVENDER(MR.strings.theme_lavender), | ||||
|     MIDNIGHT_DUSK(MR.strings.theme_midnightdusk), | ||||
|  | ||||
|     // TODO: re-enable for preview | ||||
|     NORD(MR.strings.theme_nord.takeIf { isDevFlavor || isPreviewBuildType }), | ||||
|     STRAWBERRY_DAIQUIRI(MR.strings.theme_strawberrydaiquiri), | ||||
|     TAKO(MR.strings.theme_tako), | ||||
|     TEALTURQUOISE(MR.strings.theme_tealturquoise), | ||||
|     TIDAL_WAVE(MR.strings.theme_tidalwave), | ||||
|     YINYANG(MR.strings.theme_yinyang), | ||||
|     YOTSUBA(MR.strings.theme_yotsuba), | ||||
|  | ||||
|     // Deprecated | ||||
|     DARK_BLUE(null), | ||||
|     HOT_PINK(null), | ||||
|     BLUE(null), | ||||
| } | ||||
| @@ -1,11 +0,0 @@ | ||||
| package eu.kanade.domain.ui.model | ||||
|  | ||||
| import dev.icerock.moko.resources.StringResource | ||||
| import tachiyomi.i18n.MR | ||||
|  | ||||
| enum class TabletUiMode(val titleRes: StringResource) { | ||||
|     AUTOMATIC(MR.strings.automatic_background), | ||||
|     ALWAYS(MR.strings.lock_always), | ||||
|     LANDSCAPE(MR.strings.landscape), | ||||
|     NEVER(MR.strings.lock_never), | ||||
| } | ||||
| @@ -1,19 +0,0 @@ | ||||
| package eu.kanade.domain.ui.model | ||||
|  | ||||
| import androidx.appcompat.app.AppCompatDelegate | ||||
|  | ||||
| enum class ThemeMode { | ||||
|     LIGHT, | ||||
|     DARK, | ||||
|     SYSTEM, | ||||
| } | ||||
|  | ||||
| fun setAppCompatDelegateThemeMode(themeMode: ThemeMode) { | ||||
|     AppCompatDelegate.setDefaultNightMode( | ||||
|         when (themeMode) { | ||||
|             ThemeMode.LIGHT -> AppCompatDelegate.MODE_NIGHT_NO | ||||
|             ThemeMode.DARK -> AppCompatDelegate.MODE_NIGHT_YES | ||||
|             ThemeMode.SYSTEM -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM | ||||
|         }, | ||||
|     ) | ||||
| } | ||||
| @@ -1,168 +0,0 @@ | ||||
| package eu.kanade.presentation.browse | ||||
|  | ||||
| import androidx.compose.foundation.layout.PaddingValues | ||||
| import androidx.compose.foundation.layout.padding | ||||
| import androidx.compose.foundation.lazy.grid.GridCells | ||||
| import androidx.compose.material.icons.Icons | ||||
| import androidx.compose.material.icons.automirrored.outlined.HelpOutline | ||||
| import androidx.compose.material.icons.outlined.Public | ||||
| import androidx.compose.material.icons.outlined.Refresh | ||||
| import androidx.compose.material3.SnackbarDuration | ||||
| import androidx.compose.material3.SnackbarHostState | ||||
| import androidx.compose.material3.SnackbarResult | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.runtime.LaunchedEffect | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.platform.LocalContext | ||||
| import androidx.paging.LoadState | ||||
| import androidx.paging.compose.LazyPagingItems | ||||
| import eu.kanade.presentation.browse.components.BrowseSourceComfortableGrid | ||||
| import eu.kanade.presentation.browse.components.BrowseSourceCompactGrid | ||||
| import eu.kanade.presentation.browse.components.BrowseSourceList | ||||
| import eu.kanade.presentation.components.AppBar | ||||
| import eu.kanade.presentation.util.formattedMessage | ||||
| import eu.kanade.tachiyomi.source.Source | ||||
| import kotlinx.collections.immutable.persistentListOf | ||||
| import kotlinx.coroutines.flow.StateFlow | ||||
| import tachiyomi.core.common.i18n.stringResource | ||||
| import tachiyomi.domain.library.model.LibraryDisplayMode | ||||
| import tachiyomi.domain.manga.model.Manga | ||||
| import tachiyomi.domain.source.model.StubSource | ||||
| import tachiyomi.i18n.MR | ||||
| import tachiyomi.presentation.core.components.material.Scaffold | ||||
| import tachiyomi.presentation.core.i18n.stringResource | ||||
| import tachiyomi.presentation.core.screens.EmptyScreen | ||||
| import tachiyomi.presentation.core.screens.EmptyScreenAction | ||||
| import tachiyomi.presentation.core.screens.LoadingScreen | ||||
| import tachiyomi.source.local.LocalSource | ||||
|  | ||||
| @Composable | ||||
| fun BrowseSourceContent( | ||||
|     source: Source?, | ||||
|     mangaList: LazyPagingItems<StateFlow<Manga>>, | ||||
|     columns: GridCells, | ||||
|     displayMode: LibraryDisplayMode, | ||||
|     snackbarHostState: SnackbarHostState, | ||||
|     contentPadding: PaddingValues, | ||||
|     onWebViewClick: () -> Unit, | ||||
|     onHelpClick: () -> Unit, | ||||
|     onLocalSourceHelpClick: () -> Unit, | ||||
|     onMangaClick: (Manga) -> Unit, | ||||
|     onMangaLongClick: (Manga) -> Unit, | ||||
| ) { | ||||
|     val context = LocalContext.current | ||||
|  | ||||
|     val errorState = mangaList.loadState.refresh.takeIf { it is LoadState.Error } | ||||
|         ?: mangaList.loadState.append.takeIf { it is LoadState.Error } | ||||
|  | ||||
|     val getErrorMessage: (LoadState.Error) -> String = { state -> | ||||
|         with(context) { state.error.formattedMessage } | ||||
|     } | ||||
|  | ||||
|     LaunchedEffect(errorState) { | ||||
|         if (mangaList.itemCount > 0 && errorState != null && errorState is LoadState.Error) { | ||||
|             val result = snackbarHostState.showSnackbar( | ||||
|                 message = getErrorMessage(errorState), | ||||
|                 actionLabel = context.stringResource(MR.strings.action_retry), | ||||
|                 duration = SnackbarDuration.Indefinite, | ||||
|             ) | ||||
|             when (result) { | ||||
|                 SnackbarResult.Dismissed -> snackbarHostState.currentSnackbarData?.dismiss() | ||||
|                 SnackbarResult.ActionPerformed -> mangaList.retry() | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     if (mangaList.itemCount <= 0 && errorState != null && errorState is LoadState.Error) { | ||||
|         EmptyScreen( | ||||
|             modifier = Modifier.padding(contentPadding), | ||||
|             message = getErrorMessage(errorState), | ||||
|             actions = if (source is LocalSource) { | ||||
|                 persistentListOf( | ||||
|                     EmptyScreenAction( | ||||
|                         stringRes = MR.strings.local_source_help_guide, | ||||
|                         icon = Icons.AutoMirrored.Outlined.HelpOutline, | ||||
|                         onClick = onLocalSourceHelpClick, | ||||
|                     ), | ||||
|                 ) | ||||
|             } else { | ||||
|                 persistentListOf( | ||||
|                     EmptyScreenAction( | ||||
|                         stringRes = MR.strings.action_retry, | ||||
|                         icon = Icons.Outlined.Refresh, | ||||
|                         onClick = mangaList::refresh, | ||||
|                     ), | ||||
|                     EmptyScreenAction( | ||||
|                         stringRes = MR.strings.action_open_in_web_view, | ||||
|                         icon = Icons.Outlined.Public, | ||||
|                         onClick = onWebViewClick, | ||||
|                     ), | ||||
|                     EmptyScreenAction( | ||||
|                         stringRes = MR.strings.label_help, | ||||
|                         icon = Icons.AutoMirrored.Outlined.HelpOutline, | ||||
|                         onClick = onHelpClick, | ||||
|                     ), | ||||
|                 ) | ||||
|             }, | ||||
|         ) | ||||
|  | ||||
|         return | ||||
|     } | ||||
|  | ||||
|     if (mangaList.itemCount == 0 && mangaList.loadState.refresh is LoadState.Loading) { | ||||
|         LoadingScreen( | ||||
|             modifier = Modifier.padding(contentPadding), | ||||
|         ) | ||||
|         return | ||||
|     } | ||||
|  | ||||
|     when (displayMode) { | ||||
|         LibraryDisplayMode.ComfortableGrid -> { | ||||
|             BrowseSourceComfortableGrid( | ||||
|                 mangaList = mangaList, | ||||
|                 columns = columns, | ||||
|                 contentPadding = contentPadding, | ||||
|                 onMangaClick = onMangaClick, | ||||
|                 onMangaLongClick = onMangaLongClick, | ||||
|             ) | ||||
|         } | ||||
|         LibraryDisplayMode.List -> { | ||||
|             BrowseSourceList( | ||||
|                 mangaList = mangaList, | ||||
|                 contentPadding = contentPadding, | ||||
|                 onMangaClick = onMangaClick, | ||||
|                 onMangaLongClick = onMangaLongClick, | ||||
|             ) | ||||
|         } | ||||
|         LibraryDisplayMode.CompactGrid, LibraryDisplayMode.CoverOnlyGrid -> { | ||||
|             BrowseSourceCompactGrid( | ||||
|                 mangaList = mangaList, | ||||
|                 columns = columns, | ||||
|                 contentPadding = contentPadding, | ||||
|                 onMangaClick = onMangaClick, | ||||
|                 onMangaLongClick = onMangaLongClick, | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @Composable | ||||
| internal fun MissingSourceScreen( | ||||
|     source: StubSource, | ||||
|     navigateUp: () -> Unit, | ||||
| ) { | ||||
|     Scaffold( | ||||
|         topBar = { scrollBehavior -> | ||||
|             AppBar( | ||||
|                 title = source.name, | ||||
|                 navigateUp = navigateUp, | ||||
|                 scrollBehavior = scrollBehavior, | ||||
|             ) | ||||
|         }, | ||||
|     ) { paddingValues -> | ||||
|         EmptyScreen( | ||||
|             message = stringResource(MR.strings.source_not_installed, source.toString()), | ||||
|             modifier = Modifier.padding(paddingValues), | ||||
|         ) | ||||
|     } | ||||
| } | ||||
| @@ -1,445 +0,0 @@ | ||||
| package eu.kanade.presentation.browse | ||||
|  | ||||
| import android.content.Intent | ||||
| import android.net.Uri | ||||
| import android.provider.Settings | ||||
| import android.util.DisplayMetrics | ||||
| import androidx.compose.foundation.clickable | ||||
| import androidx.compose.foundation.layout.Arrangement | ||||
| import androidx.compose.foundation.layout.Column | ||||
| import androidx.compose.foundation.layout.PaddingValues | ||||
| import androidx.compose.foundation.layout.Row | ||||
| import androidx.compose.foundation.layout.fillMaxWidth | ||||
| import androidx.compose.foundation.layout.height | ||||
| import androidx.compose.foundation.layout.padding | ||||
| import androidx.compose.foundation.layout.size | ||||
| import androidx.compose.foundation.lazy.items | ||||
| import androidx.compose.material.icons.Icons | ||||
| import androidx.compose.material.icons.automirrored.outlined.Launch | ||||
| import androidx.compose.material.icons.outlined.Settings | ||||
| import androidx.compose.material3.AlertDialog | ||||
| import androidx.compose.material3.Button | ||||
| import androidx.compose.material3.HorizontalDivider | ||||
| import androidx.compose.material3.Icon | ||||
| import androidx.compose.material3.IconButton | ||||
| import androidx.compose.material3.MaterialTheme | ||||
| import androidx.compose.material3.OutlinedButton | ||||
| import androidx.compose.material3.Switch | ||||
| import androidx.compose.material3.Text | ||||
| import androidx.compose.material3.TextButton | ||||
| import androidx.compose.material3.VerticalDivider | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.runtime.getValue | ||||
| import androidx.compose.runtime.mutableStateOf | ||||
| import androidx.compose.runtime.remember | ||||
| import androidx.compose.runtime.setValue | ||||
| import androidx.compose.ui.Alignment | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.platform.LocalContext | ||||
| import androidx.compose.ui.platform.LocalUriHandler | ||||
| import androidx.compose.ui.text.TextStyle | ||||
| import androidx.compose.ui.text.font.FontWeight | ||||
| import androidx.compose.ui.text.style.TextAlign | ||||
| import androidx.compose.ui.unit.dp | ||||
| import eu.kanade.domain.extension.interactor.ExtensionSourceItem | ||||
| import eu.kanade.presentation.browse.components.ExtensionIcon | ||||
| import eu.kanade.presentation.components.AppBar | ||||
| import eu.kanade.presentation.components.AppBarActions | ||||
| import eu.kanade.presentation.components.WarningBanner | ||||
| import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget | ||||
| import eu.kanade.presentation.more.settings.widget.TrailingWidgetBuffer | ||||
| import eu.kanade.tachiyomi.extension.model.Extension | ||||
| import eu.kanade.tachiyomi.source.ConfigurableSource | ||||
| import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionDetailsScreenModel | ||||
| import eu.kanade.tachiyomi.util.system.LocaleHelper | ||||
| import eu.kanade.tachiyomi.util.system.copyToClipboard | ||||
| import kotlinx.collections.immutable.ImmutableList | ||||
| import kotlinx.collections.immutable.persistentListOf | ||||
| import tachiyomi.i18n.MR | ||||
| import tachiyomi.presentation.core.components.ScrollbarLazyColumn | ||||
| import tachiyomi.presentation.core.components.material.Scaffold | ||||
| import tachiyomi.presentation.core.components.material.padding | ||||
| import tachiyomi.presentation.core.i18n.stringResource | ||||
| import tachiyomi.presentation.core.screens.EmptyScreen | ||||
|  | ||||
| @Composable | ||||
| fun ExtensionDetailsScreen( | ||||
|     navigateUp: () -> Unit, | ||||
|     state: ExtensionDetailsScreenModel.State, | ||||
|     onClickSourcePreferences: (sourceId: Long) -> Unit, | ||||
|     onClickEnableAll: () -> Unit, | ||||
|     onClickDisableAll: () -> Unit, | ||||
|     onClickClearCookies: () -> Unit, | ||||
|     onClickUninstall: () -> Unit, | ||||
|     onClickSource: (sourceId: Long) -> Unit, | ||||
| ) { | ||||
|     val uriHandler = LocalUriHandler.current | ||||
|     val url = remember(state.extension) { | ||||
|         val regex = """https://raw.githubusercontent.com/(.+?)/(.+?)/.+""".toRegex() | ||||
|         regex.find(state.extension?.repoUrl.orEmpty()) | ||||
|             ?.let { | ||||
|                 val (user, repo) = it.destructured | ||||
|                 "https://github.com/$user/$repo" | ||||
|             } | ||||
|             ?: state.extension?.repoUrl | ||||
|     } | ||||
|  | ||||
|     Scaffold( | ||||
|         topBar = { scrollBehavior -> | ||||
|             AppBar( | ||||
|                 title = stringResource(MR.strings.label_extension_info), | ||||
|                 navigateUp = navigateUp, | ||||
|                 actions = { | ||||
|                     AppBarActions( | ||||
|                         actions = persistentListOf<AppBar.AppBarAction>().builder() | ||||
|                             .apply { | ||||
|                                 if (url != null) { | ||||
|                                     add( | ||||
|                                         AppBar.Action( | ||||
|                                             title = stringResource(MR.strings.action_open_repo), | ||||
|                                             icon = Icons.AutoMirrored.Outlined.Launch, | ||||
|                                             onClick = { | ||||
|                                                 uriHandler.openUri(url) | ||||
|                                             }, | ||||
|                                         ), | ||||
|                                     ) | ||||
|                                 } | ||||
|                                 addAll( | ||||
|                                     listOf( | ||||
|                                         AppBar.OverflowAction( | ||||
|                                             title = stringResource(MR.strings.action_enable_all), | ||||
|                                             onClick = onClickEnableAll, | ||||
|                                         ), | ||||
|                                         AppBar.OverflowAction( | ||||
|                                             title = stringResource(MR.strings.action_disable_all), | ||||
|                                             onClick = onClickDisableAll, | ||||
|                                         ), | ||||
|                                         AppBar.OverflowAction( | ||||
|                                             title = stringResource(MR.strings.pref_clear_cookies), | ||||
|                                             onClick = onClickClearCookies, | ||||
|                                         ), | ||||
|                                     ), | ||||
|                                 ) | ||||
|                             } | ||||
|                             .build(), | ||||
|                     ) | ||||
|                 }, | ||||
|                 scrollBehavior = scrollBehavior, | ||||
|             ) | ||||
|         }, | ||||
|     ) { paddingValues -> | ||||
|         if (state.extension == null) { | ||||
|             EmptyScreen( | ||||
|                 MR.strings.empty_screen, | ||||
|                 modifier = Modifier.padding(paddingValues), | ||||
|             ) | ||||
|             return@Scaffold | ||||
|         } | ||||
|  | ||||
|         ExtensionDetails( | ||||
|             contentPadding = paddingValues, | ||||
|             extension = state.extension, | ||||
|             sources = state.sources, | ||||
|             onClickSourcePreferences = onClickSourcePreferences, | ||||
|             onClickUninstall = onClickUninstall, | ||||
|             onClickSource = onClickSource, | ||||
|         ) | ||||
|     } | ||||
| } | ||||
|  | ||||
| @Composable | ||||
| private fun ExtensionDetails( | ||||
|     contentPadding: PaddingValues, | ||||
|     extension: Extension.Installed, | ||||
|     sources: ImmutableList<ExtensionSourceItem>, | ||||
|     onClickSourcePreferences: (sourceId: Long) -> Unit, | ||||
|     onClickUninstall: () -> Unit, | ||||
|     onClickSource: (sourceId: Long) -> Unit, | ||||
| ) { | ||||
|     val context = LocalContext.current | ||||
|     var showNsfwWarning by remember { mutableStateOf(false) } | ||||
|  | ||||
|     ScrollbarLazyColumn( | ||||
|         contentPadding = contentPadding, | ||||
|     ) { | ||||
|         if (extension.isObsolete) { | ||||
|             item { | ||||
|                 WarningBanner(MR.strings.obsolete_extension_message) | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         item { | ||||
|             DetailsHeader( | ||||
|                 extension = extension, | ||||
|                 onClickUninstall = onClickUninstall, | ||||
|                 onClickAppInfo = { | ||||
|                     Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { | ||||
|                         data = Uri.fromParts("package", extension.pkgName, null) | ||||
|                         context.startActivity(this) | ||||
|                     } | ||||
|                     Unit | ||||
|                 }.takeIf { extension.isShared }, | ||||
|                 onClickAgeRating = { | ||||
|                     showNsfwWarning = true | ||||
|                 }, | ||||
|             ) | ||||
|         } | ||||
|  | ||||
|         items( | ||||
|             items = sources, | ||||
|             key = { it.source.id }, | ||||
|         ) { source -> | ||||
|             SourceSwitchPreference( | ||||
|                 modifier = Modifier.animateItem(), | ||||
|                 source = source, | ||||
|                 onClickSourcePreferences = onClickSourcePreferences, | ||||
|                 onClickSource = onClickSource, | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
|     if (showNsfwWarning) { | ||||
|         NsfwWarningDialog( | ||||
|             onClickConfirm = { | ||||
|                 showNsfwWarning = false | ||||
|             }, | ||||
|         ) | ||||
|     } | ||||
| } | ||||
|  | ||||
| @Composable | ||||
| private fun DetailsHeader( | ||||
|     extension: Extension, | ||||
|     onClickAgeRating: () -> Unit, | ||||
|     onClickUninstall: () -> Unit, | ||||
|     onClickAppInfo: (() -> Unit)?, | ||||
| ) { | ||||
|     val context = LocalContext.current | ||||
|  | ||||
|     Column { | ||||
|         Column( | ||||
|             modifier = Modifier | ||||
|                 .fillMaxWidth() | ||||
|                 .padding( | ||||
|                     start = MaterialTheme.padding.medium, | ||||
|                     end = MaterialTheme.padding.medium, | ||||
|                     top = MaterialTheme.padding.medium, | ||||
|                     bottom = MaterialTheme.padding.small, | ||||
|                 ) | ||||
|                 .clickable { | ||||
|                     val extDebugInfo = buildString { | ||||
|                         append( | ||||
|                             """ | ||||
|                             Extension name: ${extension.name} (lang: ${extension.lang}; package: ${extension.pkgName}) | ||||
|                             Extension version: ${extension.versionName} (lib: ${extension.libVersion}; version code: ${extension.versionCode}) | ||||
|                             NSFW: ${extension.isNsfw} | ||||
|                             """.trimIndent(), | ||||
|                         ) | ||||
|  | ||||
|                         if (extension is Extension.Installed) { | ||||
|                             append("\n\n") | ||||
|                             append( | ||||
|                                 """ | ||||
|                                 Update available: ${extension.hasUpdate} | ||||
|                                 Obsolete: ${extension.isObsolete} | ||||
|                                 Shared: ${extension.isShared} | ||||
|                                 Repository: ${extension.repoUrl} | ||||
|                                 """.trimIndent(), | ||||
|                             ) | ||||
|                         } | ||||
|                     } | ||||
|                     context.copyToClipboard("Extension Debug information", extDebugInfo) | ||||
|                 }, | ||||
|             horizontalAlignment = Alignment.CenterHorizontally, | ||||
|         ) { | ||||
|             ExtensionIcon( | ||||
|                 modifier = Modifier | ||||
|                     .size(112.dp), | ||||
|                 extension = extension, | ||||
|                 density = DisplayMetrics.DENSITY_XXXHIGH, | ||||
|             ) | ||||
|  | ||||
|             Text( | ||||
|                 text = extension.name, | ||||
|                 style = MaterialTheme.typography.headlineSmall, | ||||
|                 textAlign = TextAlign.Center, | ||||
|             ) | ||||
|  | ||||
|             val strippedPkgName = extension.pkgName.substringAfter("eu.kanade.tachiyomi.extension.") | ||||
|  | ||||
|             Text( | ||||
|                 text = strippedPkgName, | ||||
|                 style = MaterialTheme.typography.bodySmall, | ||||
|             ) | ||||
|         } | ||||
|  | ||||
|         Row( | ||||
|             modifier = Modifier | ||||
|                 .fillMaxWidth() | ||||
|                 .padding( | ||||
|                     horizontal = MaterialTheme.padding.extraLarge, | ||||
|                     vertical = MaterialTheme.padding.small, | ||||
|                 ), | ||||
|             horizontalArrangement = Arrangement.SpaceEvenly, | ||||
|             verticalAlignment = Alignment.CenterVertically, | ||||
|         ) { | ||||
|             InfoText( | ||||
|                 modifier = Modifier.weight(1f), | ||||
|                 primaryText = extension.versionName, | ||||
|                 secondaryText = stringResource(MR.strings.ext_info_version), | ||||
|             ) | ||||
|  | ||||
|             InfoDivider() | ||||
|  | ||||
|             InfoText( | ||||
|                 modifier = Modifier.weight(if (extension.isNsfw) 1.5f else 1f), | ||||
|                 primaryText = LocaleHelper.getSourceDisplayName(extension.lang, context), | ||||
|                 secondaryText = stringResource(MR.strings.ext_info_language), | ||||
|             ) | ||||
|  | ||||
|             if (extension.isNsfw) { | ||||
|                 InfoDivider() | ||||
|  | ||||
|                 InfoText( | ||||
|                     modifier = Modifier.weight(1f), | ||||
|                     primaryText = stringResource(MR.strings.ext_nsfw_short), | ||||
|                     primaryTextStyle = MaterialTheme.typography.bodyLarge.copy( | ||||
|                         color = MaterialTheme.colorScheme.error, | ||||
|                         fontWeight = FontWeight.Medium, | ||||
|                     ), | ||||
|                     secondaryText = stringResource(MR.strings.ext_info_age_rating), | ||||
|                     onClick = onClickAgeRating, | ||||
|                 ) | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         Row( | ||||
|             modifier = Modifier.padding( | ||||
|                 start = MaterialTheme.padding.medium, | ||||
|                 end = MaterialTheme.padding.medium, | ||||
|                 top = MaterialTheme.padding.small, | ||||
|                 bottom = MaterialTheme.padding.medium, | ||||
|             ), | ||||
|             horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.medium), | ||||
|         ) { | ||||
|             OutlinedButton( | ||||
|                 modifier = Modifier.weight(1f), | ||||
|                 onClick = onClickUninstall, | ||||
|             ) { | ||||
|                 Text(stringResource(MR.strings.ext_uninstall)) | ||||
|             } | ||||
|  | ||||
|             if (onClickAppInfo != null) { | ||||
|                 Button( | ||||
|                     modifier = Modifier.weight(1f), | ||||
|                     onClick = onClickAppInfo, | ||||
|                 ) { | ||||
|                     Text( | ||||
|                         text = stringResource(MR.strings.ext_app_info), | ||||
|                         color = MaterialTheme.colorScheme.onPrimary, | ||||
|                     ) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         HorizontalDivider() | ||||
|     } | ||||
| } | ||||
|  | ||||
| @Composable | ||||
| private fun InfoText( | ||||
|     primaryText: String, | ||||
|     secondaryText: String, | ||||
|     modifier: Modifier = Modifier, | ||||
|     primaryTextStyle: TextStyle = MaterialTheme.typography.bodyLarge, | ||||
|     onClick: (() -> Unit)? = null, | ||||
| ) { | ||||
|     val clickableModifier = if (onClick != null) { | ||||
|         Modifier.clickable(interactionSource = null, indication = null, onClick = onClick) | ||||
|     } else { | ||||
|         Modifier | ||||
|     } | ||||
|  | ||||
|     Column( | ||||
|         modifier = modifier.then(clickableModifier), | ||||
|         horizontalAlignment = Alignment.CenterHorizontally, | ||||
|         verticalArrangement = Arrangement.Center, | ||||
|     ) { | ||||
|         Text( | ||||
|             text = primaryText, | ||||
|             textAlign = TextAlign.Center, | ||||
|             style = primaryTextStyle, | ||||
|         ) | ||||
|  | ||||
|         Text( | ||||
|             text = secondaryText + if (onClick != null) " ⓘ" else "", | ||||
|             textAlign = TextAlign.Center, | ||||
|             style = MaterialTheme.typography.bodyMedium, | ||||
|             color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f), | ||||
|         ) | ||||
|     } | ||||
| } | ||||
|  | ||||
| @Composable | ||||
| private fun InfoDivider() { | ||||
|     VerticalDivider( | ||||
|         modifier = Modifier.height(20.dp), | ||||
|     ) | ||||
| } | ||||
|  | ||||
| @Composable | ||||
| private fun SourceSwitchPreference( | ||||
|     source: ExtensionSourceItem, | ||||
|     onClickSourcePreferences: (sourceId: Long) -> Unit, | ||||
|     onClickSource: (sourceId: Long) -> Unit, | ||||
|     modifier: Modifier = Modifier, | ||||
| ) { | ||||
|     val context = LocalContext.current | ||||
|  | ||||
|     TextPreferenceWidget( | ||||
|         modifier = modifier, | ||||
|         title = if (source.labelAsName) { | ||||
|             source.source.toString() | ||||
|         } else { | ||||
|             LocaleHelper.getSourceDisplayName(source.source.lang, context) | ||||
|         }, | ||||
|         widget = { | ||||
|             Row( | ||||
|                 verticalAlignment = Alignment.CenterVertically, | ||||
|             ) { | ||||
|                 if (source.source is ConfigurableSource) { | ||||
|                     IconButton(onClick = { onClickSourcePreferences(source.source.id) }) { | ||||
|                         Icon( | ||||
|                             imageVector = Icons.Outlined.Settings, | ||||
|                             contentDescription = stringResource(MR.strings.label_settings), | ||||
|                             tint = MaterialTheme.colorScheme.onSurface, | ||||
|                         ) | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 Switch( | ||||
|                     checked = source.enabled, | ||||
|                     onCheckedChange = null, | ||||
|                     modifier = Modifier.padding(start = TrailingWidgetBuffer), | ||||
|                 ) | ||||
|             } | ||||
|         }, | ||||
|         onPreferenceClick = { onClickSource(source.source.id) }, | ||||
|     ) | ||||
| } | ||||
|  | ||||
| @Composable | ||||
| private fun NsfwWarningDialog( | ||||
|     onClickConfirm: () -> Unit, | ||||
| ) { | ||||
|     AlertDialog( | ||||
|         text = { | ||||
|             Text(text = stringResource(MR.strings.ext_nsfw_warning)) | ||||
|         }, | ||||
|         confirmButton = { | ||||
|             TextButton(onClick = onClickConfirm) { | ||||
|                 Text(text = stringResource(MR.strings.action_ok)) | ||||
|             } | ||||
|         }, | ||||
|         onDismissRequest = onClickConfirm, | ||||
|     ) | ||||
| } | ||||
| @@ -1,68 +0,0 @@ | ||||
| package eu.kanade.presentation.browse | ||||
|  | ||||
| import androidx.compose.foundation.layout.PaddingValues | ||||
| import androidx.compose.foundation.layout.padding | ||||
| import androidx.compose.foundation.lazy.LazyColumn | ||||
| import androidx.compose.foundation.lazy.items | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.platform.LocalContext | ||||
| import eu.kanade.presentation.components.AppBar | ||||
| import eu.kanade.presentation.more.settings.widget.SwitchPreferenceWidget | ||||
| import eu.kanade.tachiyomi.ui.browse.extension.ExtensionFilterState | ||||
| import eu.kanade.tachiyomi.util.system.LocaleHelper | ||||
| import tachiyomi.i18n.MR | ||||
| import tachiyomi.presentation.core.components.material.Scaffold | ||||
| import tachiyomi.presentation.core.i18n.stringResource | ||||
| import tachiyomi.presentation.core.screens.EmptyScreen | ||||
|  | ||||
| @Composable | ||||
| fun ExtensionFilterScreen( | ||||
|     navigateUp: () -> Unit, | ||||
|     state: ExtensionFilterState.Success, | ||||
|     onClickToggle: (String) -> Unit, | ||||
| ) { | ||||
|     Scaffold( | ||||
|         topBar = { scrollBehavior -> | ||||
|             AppBar( | ||||
|                 title = stringResource(MR.strings.label_extensions), | ||||
|                 navigateUp = navigateUp, | ||||
|                 scrollBehavior = scrollBehavior, | ||||
|             ) | ||||
|         }, | ||||
|     ) { contentPadding -> | ||||
|         if (state.isEmpty) { | ||||
|             EmptyScreen( | ||||
|                 stringRes = MR.strings.empty_screen, | ||||
|                 modifier = Modifier.padding(contentPadding), | ||||
|             ) | ||||
|             return@Scaffold | ||||
|         } | ||||
|         ExtensionFilterContent( | ||||
|             contentPadding = contentPadding, | ||||
|             state = state, | ||||
|             onClickLang = onClickToggle, | ||||
|         ) | ||||
|     } | ||||
| } | ||||
|  | ||||
| @Composable | ||||
| private fun ExtensionFilterContent( | ||||
|     contentPadding: PaddingValues, | ||||
|     state: ExtensionFilterState.Success, | ||||
|     onClickLang: (String) -> Unit, | ||||
| ) { | ||||
|     val context = LocalContext.current | ||||
|     LazyColumn( | ||||
|         contentPadding = contentPadding, | ||||
|     ) { | ||||
|         items(state.languages) { language -> | ||||
|             SwitchPreferenceWidget( | ||||
|                 modifier = Modifier.animateItem(), | ||||
|                 title = LocaleHelper.getSourceDisplayName(language, context), | ||||
|                 checked = language in state.enabledLanguages, | ||||
|                 onCheckedChanged = { onClickLang(language) }, | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,542 +0,0 @@ | ||||
| package eu.kanade.presentation.browse | ||||
|  | ||||
| import androidx.compose.animation.core.animateDpAsState | ||||
| import androidx.compose.foundation.clickable | ||||
| import androidx.compose.foundation.combinedClickable | ||||
| import androidx.compose.foundation.layout.Arrangement | ||||
| import androidx.compose.foundation.layout.Box | ||||
| import androidx.compose.foundation.layout.Column | ||||
| import androidx.compose.foundation.layout.FlowRow | ||||
| import androidx.compose.foundation.layout.PaddingValues | ||||
| import androidx.compose.foundation.layout.Row | ||||
| import androidx.compose.foundation.layout.RowScope | ||||
| import androidx.compose.foundation.layout.padding | ||||
| import androidx.compose.foundation.layout.size | ||||
| import androidx.compose.foundation.lazy.items | ||||
| import androidx.compose.material.icons.Icons | ||||
| import androidx.compose.material.icons.outlined.Close | ||||
| import androidx.compose.material.icons.outlined.GetApp | ||||
| import androidx.compose.material.icons.outlined.Public | ||||
| import androidx.compose.material.icons.outlined.Refresh | ||||
| import androidx.compose.material.icons.outlined.Settings | ||||
| import androidx.compose.material.icons.outlined.VerifiedUser | ||||
| import androidx.compose.material3.AlertDialog | ||||
| import androidx.compose.material3.Button | ||||
| import androidx.compose.material3.CircularProgressIndicator | ||||
| import androidx.compose.material3.Icon | ||||
| import androidx.compose.material3.IconButton | ||||
| import androidx.compose.material3.LocalTextStyle | ||||
| import androidx.compose.material3.MaterialTheme | ||||
| import androidx.compose.material3.ProvideTextStyle | ||||
| import androidx.compose.material3.Text | ||||
| import androidx.compose.material3.TextButton | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.runtime.getValue | ||||
| import androidx.compose.runtime.mutableStateOf | ||||
| import androidx.compose.runtime.remember | ||||
| import androidx.compose.runtime.setValue | ||||
| import androidx.compose.ui.Alignment | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.platform.LocalContext | ||||
| import androidx.compose.ui.text.style.TextOverflow | ||||
| import androidx.compose.ui.unit.dp | ||||
| import cafe.adriel.voyager.navigator.LocalNavigator | ||||
| import cafe.adriel.voyager.navigator.currentOrThrow | ||||
| import dev.icerock.moko.resources.StringResource | ||||
| import eu.kanade.presentation.browse.components.BaseBrowseItem | ||||
| import eu.kanade.presentation.browse.components.ExtensionIcon | ||||
| import eu.kanade.presentation.components.WarningBanner | ||||
| import eu.kanade.presentation.manga.components.DotSeparatorNoSpaceText | ||||
| import eu.kanade.presentation.more.settings.screen.browse.ExtensionReposScreen | ||||
| import eu.kanade.presentation.util.animateItemFastScroll | ||||
| import eu.kanade.presentation.util.rememberRequestPackageInstallsPermissionState | ||||
| import eu.kanade.tachiyomi.extension.model.Extension | ||||
| import eu.kanade.tachiyomi.extension.model.InstallStep | ||||
| import eu.kanade.tachiyomi.ui.browse.extension.ExtensionUiModel | ||||
| import eu.kanade.tachiyomi.ui.browse.extension.ExtensionsScreenModel | ||||
| import eu.kanade.tachiyomi.util.system.LocaleHelper | ||||
| import eu.kanade.tachiyomi.util.system.launchRequestPackageInstallsPermission | ||||
| import kotlinx.collections.immutable.persistentListOf | ||||
| import tachiyomi.i18n.MR | ||||
| import tachiyomi.presentation.core.components.FastScrollLazyColumn | ||||
| import tachiyomi.presentation.core.components.material.PullRefresh | ||||
| import tachiyomi.presentation.core.components.material.padding | ||||
| import tachiyomi.presentation.core.components.material.topSmallPaddingValues | ||||
| import tachiyomi.presentation.core.i18n.stringResource | ||||
| import tachiyomi.presentation.core.screens.EmptyScreen | ||||
| import tachiyomi.presentation.core.screens.EmptyScreenAction | ||||
| import tachiyomi.presentation.core.screens.LoadingScreen | ||||
| import tachiyomi.presentation.core.theme.header | ||||
| import tachiyomi.presentation.core.util.plus | ||||
| import tachiyomi.presentation.core.util.secondaryItemAlpha | ||||
|  | ||||
| @Composable | ||||
| fun ExtensionScreen( | ||||
|     state: ExtensionsScreenModel.State, | ||||
|     contentPadding: PaddingValues, | ||||
|     searchQuery: String?, | ||||
|     onLongClickItem: (Extension) -> Unit, | ||||
|     onClickItemCancel: (Extension) -> Unit, | ||||
|     onOpenWebView: (Extension.Available) -> Unit, | ||||
|     onInstallExtension: (Extension.Available) -> Unit, | ||||
|     onUninstallExtension: (Extension) -> Unit, | ||||
|     onUpdateExtension: (Extension.Installed) -> Unit, | ||||
|     onTrustExtension: (Extension.Untrusted) -> Unit, | ||||
|     onOpenExtension: (Extension.Installed) -> Unit, | ||||
|     onClickUpdateAll: () -> Unit, | ||||
|     onRefresh: () -> Unit, | ||||
| ) { | ||||
|     val navigator = LocalNavigator.currentOrThrow | ||||
|  | ||||
|     PullRefresh( | ||||
|         refreshing = state.isRefreshing, | ||||
|         onRefresh = onRefresh, | ||||
|         enabled = !state.isLoading, | ||||
|     ) { | ||||
|         when { | ||||
|             state.isLoading -> LoadingScreen(Modifier.padding(contentPadding)) | ||||
|             state.isEmpty -> { | ||||
|                 val msg = if (!searchQuery.isNullOrEmpty()) { | ||||
|                     MR.strings.no_results_found | ||||
|                 } else { | ||||
|                     MR.strings.empty_screen | ||||
|                 } | ||||
|                 EmptyScreen( | ||||
|                     stringRes = msg, | ||||
|                     modifier = Modifier.padding(contentPadding), | ||||
|                     actions = persistentListOf( | ||||
|                         EmptyScreenAction( | ||||
|                             stringRes = MR.strings.label_extension_repos, | ||||
|                             icon = Icons.Outlined.Settings, | ||||
|                             onClick = { navigator.push(ExtensionReposScreen()) }, | ||||
|                         ), | ||||
|                     ), | ||||
|                 ) | ||||
|             } | ||||
|             else -> { | ||||
|                 ExtensionContent( | ||||
|                     state = state, | ||||
|                     contentPadding = contentPadding, | ||||
|                     onLongClickItem = onLongClickItem, | ||||
|                     onClickItemCancel = onClickItemCancel, | ||||
|                     onOpenWebView = onOpenWebView, | ||||
|                     onInstallExtension = onInstallExtension, | ||||
|                     onUninstallExtension = onUninstallExtension, | ||||
|                     onUpdateExtension = onUpdateExtension, | ||||
|                     onTrustExtension = onTrustExtension, | ||||
|                     onOpenExtension = onOpenExtension, | ||||
|                     onClickUpdateAll = onClickUpdateAll, | ||||
|                 ) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @Composable | ||||
| private fun ExtensionContent( | ||||
|     state: ExtensionsScreenModel.State, | ||||
|     contentPadding: PaddingValues, | ||||
|     onLongClickItem: (Extension) -> Unit, | ||||
|     onClickItemCancel: (Extension) -> Unit, | ||||
|     onOpenWebView: (Extension.Available) -> Unit, | ||||
|     onInstallExtension: (Extension.Available) -> Unit, | ||||
|     onUninstallExtension: (Extension) -> Unit, | ||||
|     onUpdateExtension: (Extension.Installed) -> Unit, | ||||
|     onTrustExtension: (Extension.Untrusted) -> Unit, | ||||
|     onOpenExtension: (Extension.Installed) -> Unit, | ||||
|     onClickUpdateAll: () -> Unit, | ||||
| ) { | ||||
|     val context = LocalContext.current | ||||
|     var trustState by remember { mutableStateOf<Extension.Untrusted?>(null) } | ||||
|     val installGranted = rememberRequestPackageInstallsPermissionState(initialValue = true) | ||||
|  | ||||
|     FastScrollLazyColumn( | ||||
|         contentPadding = contentPadding + topSmallPaddingValues, | ||||
|     ) { | ||||
|         if (!installGranted && state.installer?.requiresSystemPermission == true) { | ||||
|             item(key = "extension-permissions-warning") { | ||||
|                 WarningBanner( | ||||
|                     textRes = MR.strings.ext_permission_install_apps_warning, | ||||
|                     modifier = Modifier.clickable { | ||||
|                         context.launchRequestPackageInstallsPermission() | ||||
|                     }, | ||||
|                 ) | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         state.items.forEach { (header, items) -> | ||||
|             item( | ||||
|                 contentType = "header", | ||||
|                 key = "extensionHeader-${header.hashCode()}", | ||||
|             ) { | ||||
|                 when (header) { | ||||
|                     is ExtensionUiModel.Header.Resource -> { | ||||
|                         val action: @Composable RowScope.() -> Unit = | ||||
|                             if (header.textRes == MR.strings.ext_updates_pending) { | ||||
|                                 { | ||||
|                                     Button(onClick = { onClickUpdateAll() }) { | ||||
|                                         Text( | ||||
|                                             text = stringResource(MR.strings.ext_update_all), | ||||
|                                             style = LocalTextStyle.current.copy( | ||||
|                                                 color = MaterialTheme.colorScheme.onPrimary, | ||||
|                                             ), | ||||
|                                         ) | ||||
|                                     } | ||||
|                                 } | ||||
|                             } else { | ||||
|                                 {} | ||||
|                             } | ||||
|                         ExtensionHeader( | ||||
|                             textRes = header.textRes, | ||||
|                             modifier = Modifier.animateItemFastScroll(), | ||||
|                             action = action, | ||||
|                         ) | ||||
|                     } | ||||
|                     is ExtensionUiModel.Header.Text -> { | ||||
|                         ExtensionHeader( | ||||
|                             text = header.text, | ||||
|                             modifier = Modifier.animateItemFastScroll(), | ||||
|                         ) | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             items( | ||||
|                 items = items, | ||||
|                 contentType = { "item" }, | ||||
|                 key = { item -> | ||||
|                     when (item.extension) { | ||||
|                         is Extension.Untrusted -> "extension-untrusted-${item.hashCode()}" | ||||
|                         is Extension.Installed -> "extension-installed-${item.hashCode()}" | ||||
|                         is Extension.Available -> "extension-available-${item.hashCode()}" | ||||
|                     } | ||||
|                 }, | ||||
|             ) { item -> | ||||
|                 ExtensionItem( | ||||
|                     modifier = Modifier.animateItemFastScroll(), | ||||
|                     item = item, | ||||
|                     onClickItem = { | ||||
|                         when (it) { | ||||
|                             is Extension.Available -> onInstallExtension(it) | ||||
|                             is Extension.Installed -> onOpenExtension(it) | ||||
|                             is Extension.Untrusted -> { | ||||
|                                 trustState = it | ||||
|                             } | ||||
|                         } | ||||
|                     }, | ||||
|                     onLongClickItem = onLongClickItem, | ||||
|                     onClickItemSecondaryAction = { | ||||
|                         when (it) { | ||||
|                             is Extension.Available -> onOpenWebView(it) | ||||
|                             is Extension.Installed -> onOpenExtension(it) | ||||
|                             else -> {} | ||||
|                         } | ||||
|                     }, | ||||
|                     onClickItemCancel = onClickItemCancel, | ||||
|                     onClickItemAction = { | ||||
|                         when (it) { | ||||
|                             is Extension.Available -> onInstallExtension(it) | ||||
|                             is Extension.Installed -> { | ||||
|                                 if (it.hasUpdate) { | ||||
|                                     onUpdateExtension(it) | ||||
|                                 } else { | ||||
|                                     onOpenExtension(it) | ||||
|                                 } | ||||
|                             } | ||||
|                             is Extension.Untrusted -> { | ||||
|                                 trustState = it | ||||
|                             } | ||||
|                         } | ||||
|                     }, | ||||
|                 ) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|     if (trustState != null) { | ||||
|         ExtensionTrustDialog( | ||||
|             onClickConfirm = { | ||||
|                 onTrustExtension(trustState!!) | ||||
|                 trustState = null | ||||
|             }, | ||||
|             onClickDismiss = { | ||||
|                 onUninstallExtension(trustState!!) | ||||
|                 trustState = null | ||||
|             }, | ||||
|             onDismissRequest = { | ||||
|                 trustState = null | ||||
|             }, | ||||
|         ) | ||||
|     } | ||||
| } | ||||
|  | ||||
| @Composable | ||||
| private fun ExtensionItem( | ||||
|     item: ExtensionUiModel.Item, | ||||
|     onClickItem: (Extension) -> Unit, | ||||
|     onLongClickItem: (Extension) -> Unit, | ||||
|     onClickItemCancel: (Extension) -> Unit, | ||||
|     onClickItemAction: (Extension) -> Unit, | ||||
|     onClickItemSecondaryAction: (Extension) -> Unit, | ||||
|     modifier: Modifier = Modifier, | ||||
| ) { | ||||
|     val (extension, installStep) = item | ||||
|     BaseBrowseItem( | ||||
|         modifier = modifier | ||||
|             .combinedClickable( | ||||
|                 onClick = { onClickItem(extension) }, | ||||
|                 onLongClick = { onLongClickItem(extension) }, | ||||
|             ), | ||||
|         onClickItem = { onClickItem(extension) }, | ||||
|         onLongClickItem = { onLongClickItem(extension) }, | ||||
|         icon = { | ||||
|             Box( | ||||
|                 modifier = Modifier | ||||
|                     .size(40.dp), | ||||
|                 contentAlignment = Alignment.Center, | ||||
|             ) { | ||||
|                 val idle = installStep.isCompleted() | ||||
|                 if (!idle) { | ||||
|                     CircularProgressIndicator( | ||||
|                         modifier = Modifier.size(40.dp), | ||||
|                         strokeWidth = 2.dp, | ||||
|                     ) | ||||
|                 } | ||||
|  | ||||
|                 val padding by animateDpAsState( | ||||
|                     targetValue = if (idle) 0.dp else 8.dp, | ||||
|                     label = "iconPadding", | ||||
|                 ) | ||||
|                 ExtensionIcon( | ||||
|                     extension = extension, | ||||
|                     modifier = Modifier | ||||
|                         .matchParentSize() | ||||
|                         .padding(padding), | ||||
|                 ) | ||||
|             } | ||||
|         }, | ||||
|         action = { | ||||
|             ExtensionItemActions( | ||||
|                 extension = extension, | ||||
|                 installStep = installStep, | ||||
|                 onClickItemCancel = onClickItemCancel, | ||||
|                 onClickItemAction = onClickItemAction, | ||||
|                 onClickItemSecondaryAction = onClickItemSecondaryAction, | ||||
|             ) | ||||
|         }, | ||||
|     ) { | ||||
|         ExtensionItemContent( | ||||
|             extension = extension, | ||||
|             installStep = installStep, | ||||
|             modifier = Modifier.weight(1f), | ||||
|         ) | ||||
|     } | ||||
| } | ||||
|  | ||||
| @Composable | ||||
| private fun ExtensionItemContent( | ||||
|     extension: Extension, | ||||
|     installStep: InstallStep, | ||||
|     modifier: Modifier = Modifier, | ||||
| ) { | ||||
|     Column( | ||||
|         modifier = modifier.padding(start = MaterialTheme.padding.medium), | ||||
|     ) { | ||||
|         Text( | ||||
|             text = extension.name, | ||||
|             maxLines = 1, | ||||
|             overflow = TextOverflow.Ellipsis, | ||||
|             style = MaterialTheme.typography.bodyMedium, | ||||
|         ) | ||||
|         // Won't look good but it's not like we can ellipsize overflowing content | ||||
|         FlowRow( | ||||
|             modifier = Modifier.secondaryItemAlpha(), | ||||
|             horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall), | ||||
|         ) { | ||||
|             ProvideTextStyle(value = MaterialTheme.typography.bodySmall) { | ||||
|                 if (extension is Extension.Installed && extension.lang.isNotEmpty()) { | ||||
|                     Text( | ||||
|                         text = LocaleHelper.getSourceDisplayName(extension.lang, LocalContext.current), | ||||
|                     ) | ||||
|                 } | ||||
|  | ||||
|                 if (extension.versionName.isNotEmpty()) { | ||||
|                     Text( | ||||
|                         text = extension.versionName, | ||||
|                     ) | ||||
|                 } | ||||
|  | ||||
|                 val warning = when { | ||||
|                     extension is Extension.Untrusted -> MR.strings.ext_untrusted | ||||
|                     extension is Extension.Installed && extension.isObsolete -> MR.strings.ext_obsolete | ||||
|                     extension.isNsfw -> MR.strings.ext_nsfw_short | ||||
|                     else -> null | ||||
|                 } | ||||
|                 if (warning != null) { | ||||
|                     Text( | ||||
|                         text = stringResource(warning).uppercase(), | ||||
|                         color = MaterialTheme.colorScheme.error, | ||||
|                         maxLines = 1, | ||||
|                         overflow = TextOverflow.Ellipsis, | ||||
|                     ) | ||||
|                 } | ||||
|  | ||||
|                 if (!installStep.isCompleted()) { | ||||
|                     DotSeparatorNoSpaceText() | ||||
|                     Text( | ||||
|                         text = when (installStep) { | ||||
|                             InstallStep.Pending -> stringResource(MR.strings.ext_pending) | ||||
|                             InstallStep.Downloading -> stringResource(MR.strings.ext_downloading) | ||||
|                             InstallStep.Installing -> stringResource(MR.strings.ext_installing) | ||||
|                             else -> error("Must not show non-install process text") | ||||
|                         }, | ||||
|                     ) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @Composable | ||||
| private fun ExtensionItemActions( | ||||
|     extension: Extension, | ||||
|     installStep: InstallStep, | ||||
|     modifier: Modifier = Modifier, | ||||
|     onClickItemCancel: (Extension) -> Unit = {}, | ||||
|     onClickItemAction: (Extension) -> Unit = {}, | ||||
|     onClickItemSecondaryAction: (Extension) -> Unit = {}, | ||||
| ) { | ||||
|     val isIdle = installStep.isCompleted() | ||||
|  | ||||
|     Row( | ||||
|         modifier = modifier, | ||||
|         horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small), | ||||
|     ) { | ||||
|         when { | ||||
|             !isIdle -> { | ||||
|                 IconButton(onClick = { onClickItemCancel(extension) }) { | ||||
|                     Icon( | ||||
|                         imageVector = Icons.Outlined.Close, | ||||
|                         contentDescription = stringResource(MR.strings.action_cancel), | ||||
|                     ) | ||||
|                 } | ||||
|             } | ||||
|             installStep == InstallStep.Error -> { | ||||
|                 IconButton(onClick = { onClickItemAction(extension) }) { | ||||
|                     Icon( | ||||
|                         imageVector = Icons.Outlined.Refresh, | ||||
|                         contentDescription = stringResource(MR.strings.action_retry), | ||||
|                     ) | ||||
|                 } | ||||
|             } | ||||
|             installStep == InstallStep.Idle -> { | ||||
|                 when (extension) { | ||||
|                     is Extension.Installed -> { | ||||
|                         IconButton(onClick = { onClickItemSecondaryAction(extension) }) { | ||||
|                             Icon( | ||||
|                                 imageVector = Icons.Outlined.Settings, | ||||
|                                 contentDescription = stringResource(MR.strings.action_settings), | ||||
|                             ) | ||||
|                         } | ||||
|  | ||||
|                         if (extension.hasUpdate) { | ||||
|                             IconButton(onClick = { onClickItemAction(extension) }) { | ||||
|                                 Icon( | ||||
|                                     imageVector = Icons.Outlined.GetApp, | ||||
|                                     contentDescription = stringResource(MR.strings.ext_update), | ||||
|                                 ) | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                     is Extension.Untrusted -> { | ||||
|                         IconButton(onClick = { onClickItemAction(extension) }) { | ||||
|                             Icon( | ||||
|                                 imageVector = Icons.Outlined.VerifiedUser, | ||||
|                                 contentDescription = stringResource(MR.strings.ext_trust), | ||||
|                             ) | ||||
|                         } | ||||
|                     } | ||||
|                     is Extension.Available -> { | ||||
|                         if (extension.sources.isNotEmpty()) { | ||||
|                             IconButton( | ||||
|                                 onClick = { onClickItemSecondaryAction(extension) }, | ||||
|                             ) { | ||||
|                                 Icon( | ||||
|                                     imageVector = Icons.Outlined.Public, | ||||
|                                     contentDescription = stringResource(MR.strings.action_open_in_web_view), | ||||
|                                 ) | ||||
|                             } | ||||
|                         } | ||||
|  | ||||
|                         IconButton(onClick = { onClickItemAction(extension) }) { | ||||
|                             Icon( | ||||
|                                 imageVector = Icons.Outlined.GetApp, | ||||
|                                 contentDescription = stringResource(MR.strings.ext_install), | ||||
|                             ) | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @Composable | ||||
| private fun ExtensionHeader( | ||||
|     textRes: StringResource, | ||||
|     modifier: Modifier = Modifier, | ||||
|     action: @Composable RowScope.() -> Unit = {}, | ||||
| ) { | ||||
|     ExtensionHeader( | ||||
|         text = stringResource(textRes), | ||||
|         modifier = modifier, | ||||
|         action = action, | ||||
|     ) | ||||
| } | ||||
|  | ||||
| @Composable | ||||
| private fun ExtensionHeader( | ||||
|     text: String, | ||||
|     modifier: Modifier = Modifier, | ||||
|     action: @Composable RowScope.() -> Unit = {}, | ||||
| ) { | ||||
|     Row( | ||||
|         modifier = modifier.padding(horizontal = MaterialTheme.padding.medium), | ||||
|         verticalAlignment = Alignment.CenterVertically, | ||||
|     ) { | ||||
|         Text( | ||||
|             text = text, | ||||
|             modifier = Modifier | ||||
|                 .padding(vertical = 8.dp) | ||||
|                 .weight(1f), | ||||
|             style = MaterialTheme.typography.header, | ||||
|         ) | ||||
|         action() | ||||
|     } | ||||
| } | ||||
|  | ||||
| @Composable | ||||
| private fun ExtensionTrustDialog( | ||||
|     onClickConfirm: () -> Unit, | ||||
|     onClickDismiss: () -> Unit, | ||||
|     onDismissRequest: () -> Unit, | ||||
| ) { | ||||
|     AlertDialog( | ||||
|         title = { | ||||
|             Text(text = stringResource(MR.strings.untrusted_extension)) | ||||
|         }, | ||||
|         text = { | ||||
|             Text(text = stringResource(MR.strings.untrusted_extension_message)) | ||||
|         }, | ||||
|         confirmButton = { | ||||
|             TextButton(onClick = onClickConfirm) { | ||||
|                 Text(text = stringResource(MR.strings.ext_trust)) | ||||
|             } | ||||
|         }, | ||||
|         dismissButton = { | ||||
|             TextButton(onClick = onClickDismiss) { | ||||
|                 Text(text = stringResource(MR.strings.ext_uninstall)) | ||||
|             } | ||||
|         }, | ||||
|         onDismissRequest = onDismissRequest, | ||||
|     ) | ||||
| } | ||||
| @@ -1,105 +0,0 @@ | ||||
| package eu.kanade.presentation.browse | ||||
|  | ||||
| import androidx.compose.foundation.layout.PaddingValues | ||||
| import androidx.compose.foundation.lazy.LazyColumn | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.runtime.State | ||||
| import androidx.compose.ui.Modifier | ||||
| import eu.kanade.presentation.browse.components.GlobalSearchCardRow | ||||
| import eu.kanade.presentation.browse.components.GlobalSearchErrorResultItem | ||||
| import eu.kanade.presentation.browse.components.GlobalSearchLoadingResultItem | ||||
| import eu.kanade.presentation.browse.components.GlobalSearchResultItem | ||||
| import eu.kanade.presentation.browse.components.GlobalSearchToolbar | ||||
| import eu.kanade.tachiyomi.source.CatalogueSource | ||||
| import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchItemResult | ||||
| import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchScreenModel | ||||
| import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SourceFilter | ||||
| import eu.kanade.tachiyomi.util.system.LocaleHelper | ||||
| import tachiyomi.domain.manga.model.Manga | ||||
| import tachiyomi.presentation.core.components.material.Scaffold | ||||
|  | ||||
| @Composable | ||||
| fun GlobalSearchScreen( | ||||
|     state: SearchScreenModel.State, | ||||
|     navigateUp: () -> Unit, | ||||
|     onChangeSearchQuery: (String?) -> Unit, | ||||
|     onSearch: (String) -> Unit, | ||||
|     onChangeSearchFilter: (SourceFilter) -> Unit, | ||||
|     onToggleResults: () -> Unit, | ||||
|     getManga: @Composable (Manga) -> State<Manga>, | ||||
|     onClickSource: (CatalogueSource) -> Unit, | ||||
|     onClickItem: (Manga) -> Unit, | ||||
|     onLongClickItem: (Manga) -> Unit, | ||||
| ) { | ||||
|     Scaffold( | ||||
|         topBar = { scrollBehavior -> | ||||
|             GlobalSearchToolbar( | ||||
|                 searchQuery = state.searchQuery, | ||||
|                 progress = state.progress, | ||||
|                 total = state.total, | ||||
|                 navigateUp = navigateUp, | ||||
|                 onChangeSearchQuery = onChangeSearchQuery, | ||||
|                 onSearch = onSearch, | ||||
|                 sourceFilter = state.sourceFilter, | ||||
|                 onChangeSearchFilter = onChangeSearchFilter, | ||||
|                 onlyShowHasResults = state.onlyShowHasResults, | ||||
|                 onToggleResults = onToggleResults, | ||||
|                 scrollBehavior = scrollBehavior, | ||||
|             ) | ||||
|         }, | ||||
|     ) { paddingValues -> | ||||
|         GlobalSearchContent( | ||||
|             items = state.filteredItems, | ||||
|             contentPadding = paddingValues, | ||||
|             getManga = getManga, | ||||
|             onClickSource = onClickSource, | ||||
|             onClickItem = onClickItem, | ||||
|             onLongClickItem = onLongClickItem, | ||||
|         ) | ||||
|     } | ||||
| } | ||||
|  | ||||
| @Composable | ||||
| internal fun GlobalSearchContent( | ||||
|     items: Map<CatalogueSource, SearchItemResult>, | ||||
|     contentPadding: PaddingValues, | ||||
|     getManga: @Composable (Manga) -> State<Manga>, | ||||
|     onClickSource: (CatalogueSource) -> Unit, | ||||
|     onClickItem: (Manga) -> Unit, | ||||
|     onLongClickItem: (Manga) -> Unit, | ||||
|     fromSourceId: Long? = null, | ||||
| ) { | ||||
|     LazyColumn( | ||||
|         contentPadding = contentPadding, | ||||
|     ) { | ||||
|         items.forEach { (source, result) -> | ||||
|             item(key = source.id) { | ||||
|                 GlobalSearchResultItem( | ||||
|                     title = fromSourceId?.let { | ||||
|                         "▶ ${source.name}".takeIf { source.id == fromSourceId } | ||||
|                     } ?: source.name, | ||||
|                     subtitle = LocaleHelper.getLocalizedDisplayName(source.lang), | ||||
|                     onClick = { onClickSource(source) }, | ||||
|                     modifier = Modifier.animateItem(), | ||||
|                 ) { | ||||
|                     when (result) { | ||||
|                         SearchItemResult.Loading -> { | ||||
|                             GlobalSearchLoadingResultItem() | ||||
|                         } | ||||
|                         is SearchItemResult.Success -> { | ||||
|                             GlobalSearchCardRow( | ||||
|                                 titles = result.result, | ||||
|                                 getManga = getManga, | ||||
|                                 onClick = onClickItem, | ||||
|                                 onLongClick = onLongClickItem, | ||||
|                             ) | ||||
|                         } | ||||
|                         is SearchItemResult.Error -> { | ||||
|                             GlobalSearchErrorResultItem(message = result.throwable.message) | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,84 +0,0 @@ | ||||
| package eu.kanade.presentation.browse | ||||
|  | ||||
| import androidx.compose.foundation.layout.PaddingValues | ||||
| import androidx.compose.foundation.layout.padding | ||||
| import androidx.compose.foundation.lazy.items | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.ui.Modifier | ||||
| import eu.kanade.presentation.components.AppBar | ||||
| import eu.kanade.presentation.manga.components.BaseMangaListItem | ||||
| import eu.kanade.tachiyomi.ui.browse.migration.manga.MigrateMangaScreenModel | ||||
| import tachiyomi.domain.manga.model.Manga | ||||
| import tachiyomi.i18n.MR | ||||
| import tachiyomi.presentation.core.components.FastScrollLazyColumn | ||||
| import tachiyomi.presentation.core.components.material.Scaffold | ||||
| import tachiyomi.presentation.core.screens.EmptyScreen | ||||
|  | ||||
| @Composable | ||||
| fun MigrateMangaScreen( | ||||
|     navigateUp: () -> Unit, | ||||
|     title: String?, | ||||
|     state: MigrateMangaScreenModel.State, | ||||
|     onClickItem: (Manga) -> Unit, | ||||
|     onClickCover: (Manga) -> Unit, | ||||
| ) { | ||||
|     Scaffold( | ||||
|         topBar = { scrollBehavior -> | ||||
|             AppBar( | ||||
|                 title = title, | ||||
|                 navigateUp = navigateUp, | ||||
|                 scrollBehavior = scrollBehavior, | ||||
|             ) | ||||
|         }, | ||||
|     ) { contentPadding -> | ||||
|         if (state.isEmpty) { | ||||
|             EmptyScreen( | ||||
|                 stringRes = MR.strings.empty_screen, | ||||
|                 modifier = Modifier.padding(contentPadding), | ||||
|             ) | ||||
|             return@Scaffold | ||||
|         } | ||||
|  | ||||
|         MigrateMangaContent( | ||||
|             contentPadding = contentPadding, | ||||
|             state = state, | ||||
|             onClickItem = onClickItem, | ||||
|             onClickCover = onClickCover, | ||||
|         ) | ||||
|     } | ||||
| } | ||||
|  | ||||
| @Composable | ||||
| private fun MigrateMangaContent( | ||||
|     contentPadding: PaddingValues, | ||||
|     state: MigrateMangaScreenModel.State, | ||||
|     onClickItem: (Manga) -> Unit, | ||||
|     onClickCover: (Manga) -> Unit, | ||||
| ) { | ||||
|     FastScrollLazyColumn( | ||||
|         contentPadding = contentPadding, | ||||
|     ) { | ||||
|         items(state.titles) { manga -> | ||||
|             MigrateMangaItem( | ||||
|                 manga = manga, | ||||
|                 onClickItem = onClickItem, | ||||
|                 onClickCover = onClickCover, | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @Composable | ||||
| private fun MigrateMangaItem( | ||||
|     manga: Manga, | ||||
|     onClickItem: (Manga) -> Unit, | ||||
|     onClickCover: (Manga) -> Unit, | ||||
|     modifier: Modifier = Modifier, | ||||
| ) { | ||||
|     BaseMangaListItem( | ||||
|         modifier = modifier, | ||||
|         manga = manga, | ||||
|         onClickItem = { onClickItem(manga) }, | ||||
|         onClickCover = { onClickCover(manga) }, | ||||
|     ) | ||||
| } | ||||
| @@ -1,53 +0,0 @@ | ||||
| package eu.kanade.presentation.browse | ||||
|  | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.runtime.State | ||||
| import eu.kanade.presentation.browse.components.GlobalSearchToolbar | ||||
| import eu.kanade.tachiyomi.source.CatalogueSource | ||||
| import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchScreenModel | ||||
| import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SourceFilter | ||||
| import tachiyomi.domain.manga.model.Manga | ||||
| import tachiyomi.presentation.core.components.material.Scaffold | ||||
|  | ||||
| @Composable | ||||
| fun MigrateSearchScreen( | ||||
|     state: SearchScreenModel.State, | ||||
|     fromSourceId: Long?, | ||||
|     navigateUp: () -> Unit, | ||||
|     onChangeSearchQuery: (String?) -> Unit, | ||||
|     onSearch: (String) -> Unit, | ||||
|     onChangeSearchFilter: (SourceFilter) -> Unit, | ||||
|     onToggleResults: () -> Unit, | ||||
|     getManga: @Composable (Manga) -> State<Manga>, | ||||
|     onClickSource: (CatalogueSource) -> Unit, | ||||
|     onClickItem: (Manga) -> Unit, | ||||
|     onLongClickItem: (Manga) -> Unit, | ||||
| ) { | ||||
|     Scaffold( | ||||
|         topBar = { scrollBehavior -> | ||||
|             GlobalSearchToolbar( | ||||
|                 searchQuery = state.searchQuery, | ||||
|                 progress = state.progress, | ||||
|                 total = state.total, | ||||
|                 navigateUp = navigateUp, | ||||
|                 onChangeSearchQuery = onChangeSearchQuery, | ||||
|                 onSearch = onSearch, | ||||
|                 sourceFilter = state.sourceFilter, | ||||
|                 onChangeSearchFilter = onChangeSearchFilter, | ||||
|                 onlyShowHasResults = state.onlyShowHasResults, | ||||
|                 onToggleResults = onToggleResults, | ||||
|                 scrollBehavior = scrollBehavior, | ||||
|             ) | ||||
|         }, | ||||
|     ) { paddingValues -> | ||||
|         GlobalSearchContent( | ||||
|             fromSourceId = fromSourceId, | ||||
|             items = state.filteredItems, | ||||
|             contentPadding = paddingValues, | ||||
|             getManga = getManga, | ||||
|             onClickSource = onClickSource, | ||||
|             onClickItem = onClickItem, | ||||
|             onLongClickItem = onLongClickItem, | ||||
|         ) | ||||
|     } | ||||
| } | ||||
| @@ -1,205 +0,0 @@ | ||||
| package eu.kanade.presentation.browse | ||||
|  | ||||
| import androidx.compose.foundation.background | ||||
| import androidx.compose.foundation.layout.Arrangement | ||||
| import androidx.compose.foundation.layout.Column | ||||
| import androidx.compose.foundation.layout.PaddingValues | ||||
| import androidx.compose.foundation.layout.Row | ||||
| import androidx.compose.foundation.layout.padding | ||||
| import androidx.compose.foundation.lazy.items | ||||
| import androidx.compose.material.icons.Icons | ||||
| import androidx.compose.material.icons.outlined.ArrowDownward | ||||
| import androidx.compose.material.icons.outlined.ArrowUpward | ||||
| import androidx.compose.material.icons.outlined.Numbers | ||||
| import androidx.compose.material.icons.outlined.SortByAlpha | ||||
| import androidx.compose.material3.Icon | ||||
| import androidx.compose.material3.IconButton | ||||
| import androidx.compose.material3.MaterialTheme | ||||
| import androidx.compose.material3.Text | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.ui.Alignment | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.platform.LocalContext | ||||
| import androidx.compose.ui.text.style.TextOverflow | ||||
| import eu.kanade.domain.source.interactor.SetMigrateSorting | ||||
| import eu.kanade.presentation.browse.components.BaseSourceItem | ||||
| import eu.kanade.presentation.browse.components.SourceIcon | ||||
| import eu.kanade.tachiyomi.ui.browse.migration.sources.MigrateSourceScreenModel | ||||
| import eu.kanade.tachiyomi.util.system.copyToClipboard | ||||
| import kotlinx.collections.immutable.ImmutableList | ||||
| import tachiyomi.domain.source.model.Source | ||||
| import tachiyomi.i18n.MR | ||||
| import tachiyomi.presentation.core.components.Badge | ||||
| import tachiyomi.presentation.core.components.BadgeGroup | ||||
| import tachiyomi.presentation.core.components.ScrollbarLazyColumn | ||||
| import tachiyomi.presentation.core.components.Scroller.STICKY_HEADER_KEY_PREFIX | ||||
| import tachiyomi.presentation.core.components.material.padding | ||||
| import tachiyomi.presentation.core.components.material.topSmallPaddingValues | ||||
| import tachiyomi.presentation.core.i18n.stringResource | ||||
| import tachiyomi.presentation.core.screens.EmptyScreen | ||||
| import tachiyomi.presentation.core.screens.LoadingScreen | ||||
| import tachiyomi.presentation.core.theme.header | ||||
| import tachiyomi.presentation.core.util.plus | ||||
| import tachiyomi.presentation.core.util.secondaryItemAlpha | ||||
|  | ||||
| @Composable | ||||
| fun MigrateSourceScreen( | ||||
|     state: MigrateSourceScreenModel.State, | ||||
|     contentPadding: PaddingValues, | ||||
|     onClickItem: (Source) -> Unit, | ||||
|     onToggleSortingDirection: () -> Unit, | ||||
|     onToggleSortingMode: () -> Unit, | ||||
| ) { | ||||
|     val context = LocalContext.current | ||||
|     when { | ||||
|         state.isLoading -> LoadingScreen(Modifier.padding(contentPadding)) | ||||
|         state.isEmpty -> EmptyScreen( | ||||
|             stringRes = MR.strings.information_empty_library, | ||||
|             modifier = Modifier.padding(contentPadding), | ||||
|         ) | ||||
|         else -> | ||||
|             MigrateSourceList( | ||||
|                 list = state.items, | ||||
|                 contentPadding = contentPadding, | ||||
|                 onClickItem = onClickItem, | ||||
|                 onLongClickItem = { source -> | ||||
|                     val sourceId = source.id.toString() | ||||
|                     context.copyToClipboard(sourceId, sourceId) | ||||
|                 }, | ||||
|                 sortingMode = state.sortingMode, | ||||
|                 onToggleSortingMode = onToggleSortingMode, | ||||
|                 sortingDirection = state.sortingDirection, | ||||
|                 onToggleSortingDirection = onToggleSortingDirection, | ||||
|             ) | ||||
|     } | ||||
| } | ||||
|  | ||||
| @Composable | ||||
| private fun MigrateSourceList( | ||||
|     list: ImmutableList<Pair<Source, Long>>, | ||||
|     contentPadding: PaddingValues, | ||||
|     onClickItem: (Source) -> Unit, | ||||
|     onLongClickItem: (Source) -> Unit, | ||||
|     sortingMode: SetMigrateSorting.Mode, | ||||
|     onToggleSortingMode: () -> Unit, | ||||
|     sortingDirection: SetMigrateSorting.Direction, | ||||
|     onToggleSortingDirection: () -> Unit, | ||||
| ) { | ||||
|     ScrollbarLazyColumn( | ||||
|         contentPadding = contentPadding + topSmallPaddingValues, | ||||
|     ) { | ||||
|         stickyHeader(key = STICKY_HEADER_KEY_PREFIX) { | ||||
|             Row( | ||||
|                 modifier = Modifier | ||||
|                     .background(MaterialTheme.colorScheme.background) | ||||
|                     .padding(start = MaterialTheme.padding.medium), | ||||
|                 verticalAlignment = Alignment.CenterVertically, | ||||
|             ) { | ||||
|                 Text( | ||||
|                     text = stringResource(MR.strings.migration_selection_prompt), | ||||
|                     modifier = Modifier.weight(1f), | ||||
|                     style = MaterialTheme.typography.header, | ||||
|                 ) | ||||
|  | ||||
|                 IconButton(onClick = onToggleSortingMode) { | ||||
|                     when (sortingMode) { | ||||
|                         SetMigrateSorting.Mode.ALPHABETICAL -> Icon( | ||||
|                             Icons.Outlined.SortByAlpha, | ||||
|                             contentDescription = stringResource(MR.strings.action_sort_alpha), | ||||
|                         ) | ||||
|                         SetMigrateSorting.Mode.TOTAL -> Icon( | ||||
|                             Icons.Outlined.Numbers, | ||||
|                             contentDescription = stringResource(MR.strings.action_sort_count), | ||||
|                         ) | ||||
|                     } | ||||
|                 } | ||||
|                 IconButton(onClick = onToggleSortingDirection) { | ||||
|                     when (sortingDirection) { | ||||
|                         SetMigrateSorting.Direction.ASCENDING -> Icon( | ||||
|                             Icons.Outlined.ArrowUpward, | ||||
|                             contentDescription = stringResource(MR.strings.action_asc), | ||||
|                         ) | ||||
|                         SetMigrateSorting.Direction.DESCENDING -> Icon( | ||||
|                             Icons.Outlined.ArrowDownward, | ||||
|                             contentDescription = stringResource(MR.strings.action_desc), | ||||
|                         ) | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         items( | ||||
|             items = list, | ||||
|             key = { (source, _) -> "migrate-${source.id}" }, | ||||
|         ) { (source, count) -> | ||||
|             MigrateSourceItem( | ||||
|                 modifier = Modifier.animateItem(), | ||||
|                 source = source, | ||||
|                 count = count, | ||||
|                 onClickItem = { onClickItem(source) }, | ||||
|                 onLongClickItem = { onLongClickItem(source) }, | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @Composable | ||||
| private fun MigrateSourceItem( | ||||
|     source: Source, | ||||
|     count: Long, | ||||
|     onClickItem: () -> Unit, | ||||
|     onLongClickItem: () -> Unit, | ||||
|     modifier: Modifier = Modifier, | ||||
| ) { | ||||
|     BaseSourceItem( | ||||
|         modifier = modifier, | ||||
|         source = source, | ||||
|         showLanguageInContent = source.lang != "", | ||||
|         onClickItem = onClickItem, | ||||
|         onLongClickItem = onLongClickItem, | ||||
|         icon = { SourceIcon(source = source) }, | ||||
|         action = { | ||||
|             BadgeGroup { | ||||
|                 Badge(text = "$count") | ||||
|             } | ||||
|         }, | ||||
|         content = { _, sourceLangString -> | ||||
|             Column( | ||||
|                 modifier = Modifier | ||||
|                     .padding(horizontal = MaterialTheme.padding.medium) | ||||
|                     .weight(1f), | ||||
|             ) { | ||||
|                 Text( | ||||
|                     text = source.name.ifBlank { source.id.toString() }, | ||||
|                     maxLines = 1, | ||||
|                     overflow = TextOverflow.Ellipsis, | ||||
|                     style = MaterialTheme.typography.bodyMedium, | ||||
|                 ) | ||||
|                 Row( | ||||
|                     horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small), | ||||
|                     verticalAlignment = Alignment.CenterVertically, | ||||
|                 ) { | ||||
|                     if (sourceLangString != null) { | ||||
|                         Text( | ||||
|                             modifier = Modifier.secondaryItemAlpha(), | ||||
|                             text = sourceLangString, | ||||
|                             maxLines = 1, | ||||
|                             overflow = TextOverflow.Ellipsis, | ||||
|                             style = MaterialTheme.typography.bodySmall, | ||||
|                         ) | ||||
|                     } | ||||
|                     if (source.isStub) { | ||||
|                         Text( | ||||
|                             modifier = Modifier.secondaryItemAlpha(), | ||||
|                             text = stringResource(MR.strings.not_installed), | ||||
|                             maxLines = 1, | ||||
|                             overflow = TextOverflow.Ellipsis, | ||||
|                             style = MaterialTheme.typography.bodySmall, | ||||
|                             color = MaterialTheme.colorScheme.error, | ||||
|                         ) | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         }, | ||||
|     ) | ||||
| } | ||||
| @@ -1,127 +0,0 @@ | ||||
| package eu.kanade.presentation.browse | ||||
|  | ||||
| import androidx.compose.foundation.layout.PaddingValues | ||||
| import androidx.compose.foundation.layout.padding | ||||
| import androidx.compose.foundation.lazy.items | ||||
| import androidx.compose.material3.Checkbox | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.platform.LocalContext | ||||
| import eu.kanade.presentation.browse.components.BaseSourceItem | ||||
| import eu.kanade.presentation.components.AppBar | ||||
| import eu.kanade.presentation.more.settings.widget.SwitchPreferenceWidget | ||||
| import eu.kanade.presentation.util.animateItemFastScroll | ||||
| import eu.kanade.tachiyomi.ui.browse.source.SourcesFilterScreenModel | ||||
| import eu.kanade.tachiyomi.util.system.LocaleHelper | ||||
| import tachiyomi.domain.source.model.Source | ||||
| import tachiyomi.i18n.MR | ||||
| import tachiyomi.presentation.core.components.FastScrollLazyColumn | ||||
| import tachiyomi.presentation.core.components.material.Scaffold | ||||
| import tachiyomi.presentation.core.i18n.stringResource | ||||
| import tachiyomi.presentation.core.screens.EmptyScreen | ||||
|  | ||||
| @Composable | ||||
| fun SourcesFilterScreen( | ||||
|     navigateUp: () -> Unit, | ||||
|     state: SourcesFilterScreenModel.State.Success, | ||||
|     onClickLanguage: (String) -> Unit, | ||||
|     onClickSource: (Source) -> Unit, | ||||
| ) { | ||||
|     Scaffold( | ||||
|         topBar = { scrollBehavior -> | ||||
|             AppBar( | ||||
|                 title = stringResource(MR.strings.label_sources), | ||||
|                 navigateUp = navigateUp, | ||||
|                 scrollBehavior = scrollBehavior, | ||||
|             ) | ||||
|         }, | ||||
|     ) { contentPadding -> | ||||
|         if (state.isEmpty) { | ||||
|             EmptyScreen( | ||||
|                 stringRes = MR.strings.source_filter_empty_screen, | ||||
|                 modifier = Modifier.padding(contentPadding), | ||||
|             ) | ||||
|             return@Scaffold | ||||
|         } | ||||
|         SourcesFilterContent( | ||||
|             contentPadding = contentPadding, | ||||
|             state = state, | ||||
|             onClickLanguage = onClickLanguage, | ||||
|             onClickSource = onClickSource, | ||||
|         ) | ||||
|     } | ||||
| } | ||||
|  | ||||
| @Composable | ||||
| private fun SourcesFilterContent( | ||||
|     contentPadding: PaddingValues, | ||||
|     state: SourcesFilterScreenModel.State.Success, | ||||
|     onClickLanguage: (String) -> Unit, | ||||
|     onClickSource: (Source) -> Unit, | ||||
| ) { | ||||
|     FastScrollLazyColumn( | ||||
|         contentPadding = contentPadding, | ||||
|     ) { | ||||
|         state.items.forEach { (language, sources) -> | ||||
|             val enabled = language in state.enabledLanguages | ||||
|             item( | ||||
|                 key = language, | ||||
|                 contentType = "source-filter-header", | ||||
|             ) { | ||||
|                 SourcesFilterHeader( | ||||
|                     modifier = Modifier.animateItemFastScroll(), | ||||
|                     language = language, | ||||
|                     enabled = enabled, | ||||
|                     onClickItem = onClickLanguage, | ||||
|                 ) | ||||
|             } | ||||
|             if (enabled) { | ||||
|                 items( | ||||
|                     items = sources, | ||||
|                     key = { "source-filter-${it.key()}" }, | ||||
|                     contentType = { "source-filter-item" }, | ||||
|                 ) { source -> | ||||
|                     SourcesFilterItem( | ||||
|                         modifier = Modifier.animateItemFastScroll(), | ||||
|                         source = source, | ||||
|                         enabled = "${source.id}" !in state.disabledSources, | ||||
|                         onClickItem = onClickSource, | ||||
|                     ) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @Composable | ||||
| private fun SourcesFilterHeader( | ||||
|     language: String, | ||||
|     enabled: Boolean, | ||||
|     onClickItem: (String) -> Unit, | ||||
|     modifier: Modifier = Modifier, | ||||
| ) { | ||||
|     SwitchPreferenceWidget( | ||||
|         modifier = modifier, | ||||
|         title = LocaleHelper.getSourceDisplayName(language, LocalContext.current), | ||||
|         checked = enabled, | ||||
|         onCheckedChanged = { onClickItem(language) }, | ||||
|     ) | ||||
| } | ||||
|  | ||||
| @Composable | ||||
| private fun SourcesFilterItem( | ||||
|     source: Source, | ||||
|     enabled: Boolean, | ||||
|     onClickItem: (Source) -> Unit, | ||||
|     modifier: Modifier = Modifier, | ||||
| ) { | ||||
|     BaseSourceItem( | ||||
|         modifier = modifier, | ||||
|         source = source, | ||||
|         showLanguageInContent = false, | ||||
|         onClickItem = { onClickItem(source) }, | ||||
|         action = { | ||||
|             Checkbox(checked = enabled, onCheckedChange = null) | ||||
|         }, | ||||
|     ) | ||||
| } | ||||
| @@ -1,204 +0,0 @@ | ||||
| package eu.kanade.presentation.browse | ||||
|  | ||||
| import androidx.compose.foundation.clickable | ||||
| import androidx.compose.foundation.layout.Column | ||||
| import androidx.compose.foundation.layout.PaddingValues | ||||
| import androidx.compose.foundation.layout.fillMaxWidth | ||||
| import androidx.compose.foundation.layout.padding | ||||
| import androidx.compose.foundation.lazy.items | ||||
| import androidx.compose.material.icons.Icons | ||||
| import androidx.compose.material.icons.filled.PushPin | ||||
| import androidx.compose.material.icons.outlined.PushPin | ||||
| import androidx.compose.material3.AlertDialog | ||||
| import androidx.compose.material3.Icon | ||||
| import androidx.compose.material3.IconButton | ||||
| import androidx.compose.material3.LocalTextStyle | ||||
| import androidx.compose.material3.MaterialTheme | ||||
| import androidx.compose.material3.Text | ||||
| import androidx.compose.material3.TextButton | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.platform.LocalContext | ||||
| import androidx.compose.ui.unit.dp | ||||
| import eu.kanade.presentation.browse.components.BaseSourceItem | ||||
| import eu.kanade.tachiyomi.ui.browse.source.SourcesScreenModel | ||||
| import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreenModel.Listing | ||||
| import eu.kanade.tachiyomi.util.system.LocaleHelper | ||||
| import tachiyomi.domain.source.model.Pin | ||||
| import tachiyomi.domain.source.model.Source | ||||
| import tachiyomi.i18n.MR | ||||
| import tachiyomi.presentation.core.components.ScrollbarLazyColumn | ||||
| import tachiyomi.presentation.core.components.material.SECONDARY_ALPHA | ||||
| import tachiyomi.presentation.core.components.material.padding | ||||
| import tachiyomi.presentation.core.components.material.topSmallPaddingValues | ||||
| import tachiyomi.presentation.core.i18n.stringResource | ||||
| import tachiyomi.presentation.core.screens.EmptyScreen | ||||
| import tachiyomi.presentation.core.screens.LoadingScreen | ||||
| import tachiyomi.presentation.core.theme.header | ||||
| import tachiyomi.presentation.core.util.plus | ||||
| import tachiyomi.source.local.isLocal | ||||
|  | ||||
| @Composable | ||||
| fun SourcesScreen( | ||||
|     state: SourcesScreenModel.State, | ||||
|     contentPadding: PaddingValues, | ||||
|     onClickItem: (Source, Listing) -> Unit, | ||||
|     onClickPin: (Source) -> Unit, | ||||
|     onLongClickItem: (Source) -> Unit, | ||||
| ) { | ||||
|     when { | ||||
|         state.isLoading -> LoadingScreen(Modifier.padding(contentPadding)) | ||||
|         state.isEmpty -> EmptyScreen( | ||||
|             stringRes = MR.strings.source_empty_screen, | ||||
|             modifier = Modifier.padding(contentPadding), | ||||
|         ) | ||||
|         else -> { | ||||
|             ScrollbarLazyColumn( | ||||
|                 contentPadding = contentPadding + topSmallPaddingValues, | ||||
|             ) { | ||||
|                 items( | ||||
|                     items = state.items, | ||||
|                     contentType = { | ||||
|                         when (it) { | ||||
|                             is SourceUiModel.Header -> "header" | ||||
|                             is SourceUiModel.Item -> "item" | ||||
|                         } | ||||
|                     }, | ||||
|                     key = { | ||||
|                         when (it) { | ||||
|                             is SourceUiModel.Header -> it.hashCode() | ||||
|                             is SourceUiModel.Item -> "source-${it.source.key()}" | ||||
|                         } | ||||
|                     }, | ||||
|                 ) { model -> | ||||
|                     when (model) { | ||||
|                         is SourceUiModel.Header -> { | ||||
|                             SourceHeader( | ||||
|                                 modifier = Modifier.animateItem(), | ||||
|                                 language = model.language, | ||||
|                             ) | ||||
|                         } | ||||
|                         is SourceUiModel.Item -> SourceItem( | ||||
|                             modifier = Modifier.animateItem(), | ||||
|                             source = model.source, | ||||
|                             onClickItem = onClickItem, | ||||
|                             onLongClickItem = onLongClickItem, | ||||
|                             onClickPin = onClickPin, | ||||
|                         ) | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @Composable | ||||
| private fun SourceHeader( | ||||
|     language: String, | ||||
|     modifier: Modifier = Modifier, | ||||
| ) { | ||||
|     val context = LocalContext.current | ||||
|     Text( | ||||
|         text = LocaleHelper.getSourceDisplayName(language, context), | ||||
|         modifier = modifier | ||||
|             .padding(horizontal = MaterialTheme.padding.medium, vertical = MaterialTheme.padding.small), | ||||
|         style = MaterialTheme.typography.header, | ||||
|     ) | ||||
| } | ||||
|  | ||||
| @Composable | ||||
| private fun SourceItem( | ||||
|     source: Source, | ||||
|     onClickItem: (Source, Listing) -> Unit, | ||||
|     onLongClickItem: (Source) -> Unit, | ||||
|     onClickPin: (Source) -> Unit, | ||||
|     modifier: Modifier = Modifier, | ||||
| ) { | ||||
|     BaseSourceItem( | ||||
|         modifier = modifier, | ||||
|         source = source, | ||||
|         onClickItem = { onClickItem(source, Listing.Popular) }, | ||||
|         onLongClickItem = { onLongClickItem(source) }, | ||||
|         action = { | ||||
|             if (source.supportsLatest) { | ||||
|                 TextButton(onClick = { onClickItem(source, Listing.Latest) }) { | ||||
|                     Text( | ||||
|                         text = stringResource(MR.strings.latest), | ||||
|                         style = LocalTextStyle.current.copy( | ||||
|                             color = MaterialTheme.colorScheme.primary, | ||||
|                         ), | ||||
|                     ) | ||||
|                 } | ||||
|             } | ||||
|             SourcePinButton( | ||||
|                 isPinned = Pin.Pinned in source.pin, | ||||
|                 onClick = { onClickPin(source) }, | ||||
|             ) | ||||
|         }, | ||||
|     ) | ||||
| } | ||||
|  | ||||
| @Composable | ||||
| private fun SourcePinButton( | ||||
|     isPinned: Boolean, | ||||
|     onClick: () -> Unit, | ||||
| ) { | ||||
|     val icon = if (isPinned) Icons.Filled.PushPin else Icons.Outlined.PushPin | ||||
|     val tint = if (isPinned) { | ||||
|         MaterialTheme.colorScheme.primary | ||||
|     } else { | ||||
|         MaterialTheme.colorScheme.onBackground.copy( | ||||
|             alpha = SECONDARY_ALPHA, | ||||
|         ) | ||||
|     } | ||||
|     val description = if (isPinned) MR.strings.action_unpin else MR.strings.action_pin | ||||
|     IconButton(onClick = onClick) { | ||||
|         Icon( | ||||
|             imageVector = icon, | ||||
|             tint = tint, | ||||
|             contentDescription = stringResource(description), | ||||
|         ) | ||||
|     } | ||||
| } | ||||
|  | ||||
| @Composable | ||||
| fun SourceOptionsDialog( | ||||
|     source: Source, | ||||
|     onClickPin: () -> Unit, | ||||
|     onClickDisable: () -> Unit, | ||||
|     onDismiss: () -> Unit, | ||||
| ) { | ||||
|     AlertDialog( | ||||
|         title = { | ||||
|             Text(text = source.visualName) | ||||
|         }, | ||||
|         text = { | ||||
|             Column { | ||||
|                 val textId = if (Pin.Pinned in source.pin) MR.strings.action_unpin else MR.strings.action_pin | ||||
|                 Text( | ||||
|                     text = stringResource(textId), | ||||
|                     modifier = Modifier | ||||
|                         .clickable(onClick = onClickPin) | ||||
|                         .fillMaxWidth() | ||||
|                         .padding(vertical = 16.dp), | ||||
|                 ) | ||||
|                 if (!source.isLocal()) { | ||||
|                     Text( | ||||
|                         text = stringResource(MR.strings.action_disable), | ||||
|                         modifier = Modifier | ||||
|                             .clickable(onClick = onClickDisable) | ||||
|                             .fillMaxWidth() | ||||
|                             .padding(vertical = 16.dp), | ||||
|                     ) | ||||
|                 } | ||||
|             } | ||||
|         }, | ||||
|         onDismissRequest = onDismiss, | ||||
|         confirmButton = {}, | ||||
|     ) | ||||
| } | ||||
|  | ||||
| sealed interface SourceUiModel { | ||||
|     data class Item(val source: Source) : SourceUiModel | ||||
|     data class Header(val language: String) : SourceUiModel | ||||
| } | ||||
| @@ -1,35 +0,0 @@ | ||||
| package eu.kanade.presentation.browse.components | ||||
|  | ||||
| import androidx.compose.foundation.combinedClickable | ||||
| import androidx.compose.foundation.layout.Row | ||||
| import androidx.compose.foundation.layout.RowScope | ||||
| import androidx.compose.foundation.layout.padding | ||||
| import androidx.compose.material3.MaterialTheme | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.ui.Alignment | ||||
| import androidx.compose.ui.Modifier | ||||
| import tachiyomi.presentation.core.components.material.padding | ||||
|  | ||||
| @Composable | ||||
| fun BaseBrowseItem( | ||||
|     modifier: Modifier = Modifier, | ||||
|     onClickItem: () -> Unit = {}, | ||||
|     onLongClickItem: () -> Unit = {}, | ||||
|     icon: @Composable RowScope.() -> Unit = {}, | ||||
|     action: @Composable RowScope.() -> Unit = {}, | ||||
|     content: @Composable RowScope.() -> Unit = {}, | ||||
| ) { | ||||
|     Row( | ||||
|         modifier = modifier | ||||
|             .combinedClickable( | ||||
|                 onClick = onClickItem, | ||||
|                 onLongClick = onLongClickItem, | ||||
|             ) | ||||
|             .padding(horizontal = MaterialTheme.padding.medium, vertical = MaterialTheme.padding.small), | ||||
|         verticalAlignment = Alignment.CenterVertically, | ||||
|     ) { | ||||
|         icon() | ||||
|         content() | ||||
|         action() | ||||
|     } | ||||
| } | ||||
| @@ -1,67 +0,0 @@ | ||||
| package eu.kanade.presentation.browse.components | ||||
|  | ||||
| import androidx.compose.foundation.layout.Column | ||||
| import androidx.compose.foundation.layout.RowScope | ||||
| import androidx.compose.foundation.layout.padding | ||||
| import androidx.compose.material3.MaterialTheme | ||||
| import androidx.compose.material3.Text | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.platform.LocalContext | ||||
| import androidx.compose.ui.text.style.TextOverflow | ||||
| import eu.kanade.tachiyomi.util.system.LocaleHelper | ||||
| import tachiyomi.domain.source.model.Source | ||||
| import tachiyomi.presentation.core.components.material.padding | ||||
| import tachiyomi.presentation.core.util.secondaryItemAlpha | ||||
|  | ||||
| @Composable | ||||
| fun BaseSourceItem( | ||||
|     source: Source, | ||||
|     modifier: Modifier = Modifier, | ||||
|     showLanguageInContent: Boolean = true, | ||||
|     onClickItem: () -> Unit = {}, | ||||
|     onLongClickItem: () -> Unit = {}, | ||||
|     icon: @Composable RowScope.(Source) -> Unit = defaultIcon, | ||||
|     action: @Composable RowScope.(Source) -> Unit = {}, | ||||
|     content: @Composable RowScope.(Source, String?) -> Unit = defaultContent, | ||||
| ) { | ||||
|     val sourceLangString = LocaleHelper.getSourceDisplayName(source.lang, LocalContext.current).takeIf { | ||||
|         showLanguageInContent | ||||
|     } | ||||
|     BaseBrowseItem( | ||||
|         modifier = modifier, | ||||
|         onClickItem = onClickItem, | ||||
|         onLongClickItem = onLongClickItem, | ||||
|         icon = { icon.invoke(this, source) }, | ||||
|         action = { action.invoke(this, source) }, | ||||
|         content = { content.invoke(this, source, sourceLangString) }, | ||||
|     ) | ||||
| } | ||||
|  | ||||
| private val defaultIcon: @Composable RowScope.(Source) -> Unit = { source -> | ||||
|     SourceIcon(source = source) | ||||
| } | ||||
|  | ||||
| private val defaultContent: @Composable RowScope.(Source, String?) -> Unit = { source, sourceLangString -> | ||||
|     Column( | ||||
|         modifier = Modifier | ||||
|             .padding(horizontal = MaterialTheme.padding.medium) | ||||
|             .weight(1f), | ||||
|     ) { | ||||
|         Text( | ||||
|             text = source.name, | ||||
|             maxLines = 1, | ||||
|             overflow = TextOverflow.Ellipsis, | ||||
|             style = MaterialTheme.typography.bodyMedium, | ||||
|         ) | ||||
|         if (sourceLangString != null) { | ||||
|             Text( | ||||
|                 modifier = Modifier.secondaryItemAlpha(), | ||||
|                 text = sourceLangString, | ||||
|                 maxLines = 1, | ||||
|                 overflow = TextOverflow.Ellipsis, | ||||
|                 style = MaterialTheme.typography.bodySmall, | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,15 +0,0 @@ | ||||
| package eu.kanade.presentation.browse.components | ||||
|  | ||||
| import androidx.compose.material.icons.Icons | ||||
| import androidx.compose.material.icons.outlined.CollectionsBookmark | ||||
| import androidx.compose.runtime.Composable | ||||
| import tachiyomi.presentation.core.components.Badge | ||||
|  | ||||
| @Composable | ||||
| internal fun InLibraryBadge(enabled: Boolean) { | ||||
|     if (enabled) { | ||||
|         Badge( | ||||
|             imageVector = Icons.Outlined.CollectionsBookmark, | ||||
|         ) | ||||
|     } | ||||
| } | ||||
| @@ -1,148 +0,0 @@ | ||||
| package eu.kanade.presentation.browse.components | ||||
|  | ||||
| import android.util.DisplayMetrics | ||||
| import androidx.compose.foundation.Image | ||||
| import androidx.compose.foundation.layout.Box | ||||
| import androidx.compose.foundation.layout.aspectRatio | ||||
| import androidx.compose.foundation.layout.height | ||||
| import androidx.compose.material.icons.Icons | ||||
| import androidx.compose.material.icons.filled.Dangerous | ||||
| import androidx.compose.material.icons.filled.Warning | ||||
| import androidx.compose.material3.MaterialTheme | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.runtime.State | ||||
| import androidx.compose.runtime.getValue | ||||
| import androidx.compose.runtime.produceState | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.draw.clip | ||||
| import androidx.compose.ui.graphics.Color | ||||
| import androidx.compose.ui.graphics.ColorFilter | ||||
| import androidx.compose.ui.graphics.ImageBitmap | ||||
| import androidx.compose.ui.graphics.asImageBitmap | ||||
| import androidx.compose.ui.graphics.painter.ColorPainter | ||||
| import androidx.compose.ui.platform.LocalContext | ||||
| import androidx.compose.ui.res.imageResource | ||||
| import androidx.compose.ui.res.painterResource | ||||
| import androidx.compose.ui.unit.dp | ||||
| import androidx.core.graphics.drawable.toBitmap | ||||
| import coil3.compose.AsyncImage | ||||
| import eu.kanade.domain.source.model.icon | ||||
| import eu.kanade.presentation.util.rememberResourceBitmapPainter | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.extension.model.Extension | ||||
| import eu.kanade.tachiyomi.extension.util.ExtensionLoader | ||||
| import tachiyomi.core.common.util.lang.withIOContext | ||||
| import tachiyomi.domain.source.model.Source | ||||
| import tachiyomi.source.local.isLocal | ||||
|  | ||||
| private val defaultModifier = Modifier | ||||
|     .height(40.dp) | ||||
|     .aspectRatio(1f) | ||||
|  | ||||
| @Composable | ||||
| fun SourceIcon( | ||||
|     source: Source, | ||||
|     modifier: Modifier = Modifier, | ||||
| ) { | ||||
|     val icon = source.icon | ||||
|  | ||||
|     when { | ||||
|         source.isStub && icon == null -> { | ||||
|             Image( | ||||
|                 imageVector = Icons.Filled.Warning, | ||||
|                 contentDescription = null, | ||||
|                 colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.error), | ||||
|                 modifier = modifier.then(defaultModifier), | ||||
|             ) | ||||
|         } | ||||
|         icon != null -> { | ||||
|             Image( | ||||
|                 bitmap = icon, | ||||
|                 contentDescription = null, | ||||
|                 modifier = modifier.then(defaultModifier), | ||||
|             ) | ||||
|         } | ||||
|         source.isLocal() -> { | ||||
|             Image( | ||||
|                 painter = painterResource(R.mipmap.ic_local_source), | ||||
|                 contentDescription = null, | ||||
|                 modifier = modifier.then(defaultModifier), | ||||
|             ) | ||||
|         } | ||||
|         else -> { | ||||
|             Image( | ||||
|                 painter = painterResource(R.mipmap.ic_default_source), | ||||
|                 contentDescription = null, | ||||
|                 modifier = modifier.then(defaultModifier), | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @Composable | ||||
| fun ExtensionIcon( | ||||
|     extension: Extension, | ||||
|     modifier: Modifier = Modifier, | ||||
|     density: Int = DisplayMetrics.DENSITY_DEFAULT, | ||||
| ) { | ||||
|     when (extension) { | ||||
|         is Extension.Available -> { | ||||
|             AsyncImage( | ||||
|                 model = extension.iconUrl, | ||||
|                 contentDescription = null, | ||||
|                 placeholder = ColorPainter(Color(0x1F888888)), | ||||
|                 error = rememberResourceBitmapPainter(id = R.drawable.cover_error), | ||||
|                 modifier = modifier | ||||
|                     .clip(MaterialTheme.shapes.extraSmall), | ||||
|             ) | ||||
|         } | ||||
|         is Extension.Installed -> { | ||||
|             val icon by extension.getIcon(density) | ||||
|             when (icon) { | ||||
|                 Result.Loading -> Box(modifier = modifier) | ||||
|                 is Result.Success -> Image( | ||||
|                     bitmap = (icon as Result.Success<ImageBitmap>).value, | ||||
|                     contentDescription = null, | ||||
|                     modifier = modifier, | ||||
|                 ) | ||||
|                 Result.Error -> Image( | ||||
|                     bitmap = ImageBitmap.imageResource(id = R.mipmap.ic_default_source), | ||||
|                     contentDescription = null, | ||||
|                     modifier = modifier, | ||||
|                 ) | ||||
|             } | ||||
|         } | ||||
|         is Extension.Untrusted -> Image( | ||||
|             imageVector = Icons.Filled.Dangerous, | ||||
|             contentDescription = null, | ||||
|             colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.error), | ||||
|             modifier = modifier.then(defaultModifier), | ||||
|         ) | ||||
|     } | ||||
| } | ||||
|  | ||||
| @Composable | ||||
| private fun Extension.getIcon(density: Int = DisplayMetrics.DENSITY_DEFAULT): State<Result<ImageBitmap>> { | ||||
|     val context = LocalContext.current | ||||
|     return produceState<Result<ImageBitmap>>(initialValue = Result.Loading, this) { | ||||
|         withIOContext { | ||||
|             value = try { | ||||
|                 val appInfo = ExtensionLoader.getExtensionPackageInfoFromPkgName(context, pkgName)!!.applicationInfo!! | ||||
|                 val appResources = context.packageManager.getResourcesForApplication(appInfo) | ||||
|                 Result.Success( | ||||
|                     appResources.getDrawableForDensity(appInfo.icon, density, null)!! | ||||
|                         .toBitmap() | ||||
|                         .asImageBitmap(), | ||||
|                 ) | ||||
|             } catch (e: Exception) { | ||||
|                 Result.Error | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| sealed class Result<out T> { | ||||
|     data object Loading : Result<Nothing>() | ||||
|     data object Error : Result<Nothing>() | ||||
|     data class Success<out T>(val value: T) : Result<T>() | ||||
| } | ||||
| @@ -1,80 +0,0 @@ | ||||
| package eu.kanade.presentation.browse.components | ||||
|  | ||||
| import androidx.compose.foundation.layout.Arrangement | ||||
| import androidx.compose.foundation.layout.PaddingValues | ||||
| import androidx.compose.foundation.lazy.grid.GridCells | ||||
| import androidx.compose.foundation.lazy.grid.GridItemSpan | ||||
| import androidx.compose.foundation.lazy.grid.LazyVerticalGrid | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.runtime.collectAsState | ||||
| import androidx.compose.runtime.getValue | ||||
| import androidx.compose.ui.unit.dp | ||||
| import androidx.paging.LoadState | ||||
| import androidx.paging.compose.LazyPagingItems | ||||
| import eu.kanade.presentation.library.components.CommonMangaItemDefaults | ||||
| import eu.kanade.presentation.library.components.MangaComfortableGridItem | ||||
| import kotlinx.coroutines.flow.StateFlow | ||||
| import tachiyomi.domain.manga.model.Manga | ||||
| import tachiyomi.domain.manga.model.MangaCover | ||||
| import tachiyomi.presentation.core.util.plus | ||||
|  | ||||
| @Composable | ||||
| fun BrowseSourceComfortableGrid( | ||||
|     mangaList: LazyPagingItems<StateFlow<Manga>>, | ||||
|     columns: GridCells, | ||||
|     contentPadding: PaddingValues, | ||||
|     onMangaClick: (Manga) -> Unit, | ||||
|     onMangaLongClick: (Manga) -> Unit, | ||||
| ) { | ||||
|     LazyVerticalGrid( | ||||
|         columns = columns, | ||||
|         contentPadding = contentPadding + PaddingValues(8.dp), | ||||
|         verticalArrangement = Arrangement.spacedBy(CommonMangaItemDefaults.GridVerticalSpacer), | ||||
|         horizontalArrangement = Arrangement.spacedBy(CommonMangaItemDefaults.GridHorizontalSpacer), | ||||
|     ) { | ||||
|         if (mangaList.loadState.prepend is LoadState.Loading) { | ||||
|             item(span = { GridItemSpan(maxLineSpan) }) { | ||||
|                 BrowseSourceLoadingItem() | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         items(count = mangaList.itemCount) { index -> | ||||
|             val manga by mangaList[index]?.collectAsState() ?: return@items | ||||
|             BrowseSourceComfortableGridItem( | ||||
|                 manga = manga, | ||||
|                 onClick = { onMangaClick(manga) }, | ||||
|                 onLongClick = { onMangaLongClick(manga) }, | ||||
|             ) | ||||
|         } | ||||
|  | ||||
|         if (mangaList.loadState.refresh is LoadState.Loading || mangaList.loadState.append is LoadState.Loading) { | ||||
|             item(span = { GridItemSpan(maxLineSpan) }) { | ||||
|                 BrowseSourceLoadingItem() | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @Composable | ||||
| private fun BrowseSourceComfortableGridItem( | ||||
|     manga: Manga, | ||||
|     onClick: () -> Unit = {}, | ||||
|     onLongClick: () -> Unit = onClick, | ||||
| ) { | ||||
|     MangaComfortableGridItem( | ||||
|         title = manga.title, | ||||
|         coverData = MangaCover( | ||||
|             mangaId = manga.id, | ||||
|             sourceId = manga.source, | ||||
|             isMangaFavorite = manga.favorite, | ||||
|             url = manga.thumbnailUrl, | ||||
|             lastModified = manga.coverLastModified, | ||||
|         ), | ||||
|         coverAlpha = if (manga.favorite) CommonMangaItemDefaults.BrowseFavoriteCoverAlpha else 1f, | ||||
|         coverBadgeStart = { | ||||
|             InLibraryBadge(enabled = manga.favorite) | ||||
|         }, | ||||
|         onLongClick = onLongClick, | ||||
|         onClick = onClick, | ||||
|     ) | ||||
| } | ||||
| @@ -1,80 +0,0 @@ | ||||
| package eu.kanade.presentation.browse.components | ||||
|  | ||||
| import androidx.compose.foundation.layout.Arrangement | ||||
| import androidx.compose.foundation.layout.PaddingValues | ||||
| import androidx.compose.foundation.lazy.grid.GridCells | ||||
| import androidx.compose.foundation.lazy.grid.GridItemSpan | ||||
| import androidx.compose.foundation.lazy.grid.LazyVerticalGrid | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.runtime.collectAsState | ||||
| import androidx.compose.runtime.getValue | ||||
| import androidx.compose.ui.unit.dp | ||||
| import androidx.paging.LoadState | ||||
| import androidx.paging.compose.LazyPagingItems | ||||
| import eu.kanade.presentation.library.components.CommonMangaItemDefaults | ||||
| import eu.kanade.presentation.library.components.MangaCompactGridItem | ||||
| import kotlinx.coroutines.flow.StateFlow | ||||
| import tachiyomi.domain.manga.model.Manga | ||||
| import tachiyomi.domain.manga.model.MangaCover | ||||
| import tachiyomi.presentation.core.util.plus | ||||
|  | ||||
| @Composable | ||||
| fun BrowseSourceCompactGrid( | ||||
|     mangaList: LazyPagingItems<StateFlow<Manga>>, | ||||
|     columns: GridCells, | ||||
|     contentPadding: PaddingValues, | ||||
|     onMangaClick: (Manga) -> Unit, | ||||
|     onMangaLongClick: (Manga) -> Unit, | ||||
| ) { | ||||
|     LazyVerticalGrid( | ||||
|         columns = columns, | ||||
|         contentPadding = contentPadding + PaddingValues(8.dp), | ||||
|         verticalArrangement = Arrangement.spacedBy(CommonMangaItemDefaults.GridVerticalSpacer), | ||||
|         horizontalArrangement = Arrangement.spacedBy(CommonMangaItemDefaults.GridHorizontalSpacer), | ||||
|     ) { | ||||
|         if (mangaList.loadState.prepend is LoadState.Loading) { | ||||
|             item(span = { GridItemSpan(maxLineSpan) }) { | ||||
|                 BrowseSourceLoadingItem() | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         items(count = mangaList.itemCount) { index -> | ||||
|             val manga by mangaList[index]?.collectAsState() ?: return@items | ||||
|             BrowseSourceCompactGridItem( | ||||
|                 manga = manga, | ||||
|                 onClick = { onMangaClick(manga) }, | ||||
|                 onLongClick = { onMangaLongClick(manga) }, | ||||
|             ) | ||||
|         } | ||||
|  | ||||
|         if (mangaList.loadState.refresh is LoadState.Loading || mangaList.loadState.append is LoadState.Loading) { | ||||
|             item(span = { GridItemSpan(maxLineSpan) }) { | ||||
|                 BrowseSourceLoadingItem() | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @Composable | ||||
| private fun BrowseSourceCompactGridItem( | ||||
|     manga: Manga, | ||||
|     onClick: () -> Unit = {}, | ||||
|     onLongClick: () -> Unit = onClick, | ||||
| ) { | ||||
|     MangaCompactGridItem( | ||||
|         title = manga.title, | ||||
|         coverData = MangaCover( | ||||
|             mangaId = manga.id, | ||||
|             sourceId = manga.source, | ||||
|             isMangaFavorite = manga.favorite, | ||||
|             url = manga.thumbnailUrl, | ||||
|             lastModified = manga.coverLastModified, | ||||
|         ), | ||||
|         coverAlpha = if (manga.favorite) CommonMangaItemDefaults.BrowseFavoriteCoverAlpha else 1f, | ||||
|         coverBadgeStart = { | ||||
|             InLibraryBadge(enabled = manga.favorite) | ||||
|         }, | ||||
|         onLongClick = onLongClick, | ||||
|         onClick = onClick, | ||||
|     ) | ||||
| } | ||||
| @@ -1,41 +0,0 @@ | ||||
| package eu.kanade.presentation.browse.components | ||||
|  | ||||
| import androidx.compose.material3.AlertDialog | ||||
| import androidx.compose.material3.Text | ||||
| import androidx.compose.material3.TextButton | ||||
| import androidx.compose.runtime.Composable | ||||
| import tachiyomi.domain.manga.model.Manga | ||||
| import tachiyomi.i18n.MR | ||||
| import tachiyomi.presentation.core.i18n.stringResource | ||||
|  | ||||
| @Composable | ||||
| fun RemoveMangaDialog( | ||||
|     onDismissRequest: () -> Unit, | ||||
|     onConfirm: () -> Unit, | ||||
|     mangaToRemove: Manga, | ||||
| ) { | ||||
|     AlertDialog( | ||||
|         onDismissRequest = onDismissRequest, | ||||
|         dismissButton = { | ||||
|             TextButton(onClick = onDismissRequest) { | ||||
|                 Text(text = stringResource(MR.strings.action_cancel)) | ||||
|             } | ||||
|         }, | ||||
|         confirmButton = { | ||||
|             TextButton( | ||||
|                 onClick = { | ||||
|                     onDismissRequest() | ||||
|                     onConfirm() | ||||
|                 }, | ||||
|             ) { | ||||
|                 Text(text = stringResource(MR.strings.action_remove)) | ||||
|             } | ||||
|         }, | ||||
|         title = { | ||||
|             Text(text = stringResource(MR.strings.are_you_sure)) | ||||
|         }, | ||||
|         text = { | ||||
|             Text(text = stringResource(MR.strings.remove_manga, mangaToRemove.title)) | ||||
|         }, | ||||
|     ) | ||||
| } | ||||
| @@ -1,73 +0,0 @@ | ||||
| package eu.kanade.presentation.browse.components | ||||
|  | ||||
| import androidx.compose.foundation.layout.PaddingValues | ||||
| import androidx.compose.foundation.lazy.LazyColumn | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.runtime.collectAsState | ||||
| import androidx.compose.runtime.getValue | ||||
| import androidx.compose.ui.unit.dp | ||||
| import androidx.paging.LoadState | ||||
| import androidx.paging.compose.LazyPagingItems | ||||
| import eu.kanade.presentation.library.components.CommonMangaItemDefaults | ||||
| import eu.kanade.presentation.library.components.MangaListItem | ||||
| import kotlinx.coroutines.flow.StateFlow | ||||
| import tachiyomi.domain.manga.model.Manga | ||||
| import tachiyomi.domain.manga.model.MangaCover | ||||
| import tachiyomi.presentation.core.util.plus | ||||
|  | ||||
| @Composable | ||||
| fun BrowseSourceList( | ||||
|     mangaList: LazyPagingItems<StateFlow<Manga>>, | ||||
|     contentPadding: PaddingValues, | ||||
|     onMangaClick: (Manga) -> Unit, | ||||
|     onMangaLongClick: (Manga) -> Unit, | ||||
| ) { | ||||
|     LazyColumn( | ||||
|         contentPadding = contentPadding + PaddingValues(vertical = 8.dp), | ||||
|     ) { | ||||
|         item { | ||||
|             if (mangaList.loadState.prepend is LoadState.Loading) { | ||||
|                 BrowseSourceLoadingItem() | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         items(count = mangaList.itemCount) { index -> | ||||
|             val manga by mangaList[index]?.collectAsState() ?: return@items | ||||
|             BrowseSourceListItem( | ||||
|                 manga = manga, | ||||
|                 onClick = { onMangaClick(manga) }, | ||||
|                 onLongClick = { onMangaLongClick(manga) }, | ||||
|             ) | ||||
|         } | ||||
|  | ||||
|         item { | ||||
|             if (mangaList.loadState.refresh is LoadState.Loading || mangaList.loadState.append is LoadState.Loading) { | ||||
|                 BrowseSourceLoadingItem() | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @Composable | ||||
| private fun BrowseSourceListItem( | ||||
|     manga: Manga, | ||||
|     onClick: () -> Unit = {}, | ||||
|     onLongClick: () -> Unit = onClick, | ||||
| ) { | ||||
|     MangaListItem( | ||||
|         title = manga.title, | ||||
|         coverData = MangaCover( | ||||
|             mangaId = manga.id, | ||||
|             sourceId = manga.source, | ||||
|             isMangaFavorite = manga.favorite, | ||||
|             url = manga.thumbnailUrl, | ||||
|             lastModified = manga.coverLastModified, | ||||
|         ), | ||||
|         coverAlpha = if (manga.favorite) CommonMangaItemDefaults.BrowseFavoriteCoverAlpha else 1f, | ||||
|         badge = { | ||||
|             InLibraryBadge(enabled = manga.favorite) | ||||
|         }, | ||||
|         onLongClick = onLongClick, | ||||
|         onClick = onClick, | ||||
|     ) | ||||
| } | ||||
| @@ -1,22 +0,0 @@ | ||||
| package eu.kanade.presentation.browse.components | ||||
|  | ||||
| import androidx.compose.foundation.layout.Arrangement | ||||
| import androidx.compose.foundation.layout.Row | ||||
| import androidx.compose.foundation.layout.fillMaxWidth | ||||
| import androidx.compose.foundation.layout.padding | ||||
| import androidx.compose.material3.CircularProgressIndicator | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.unit.dp | ||||
|  | ||||
| @Composable | ||||
| internal fun BrowseSourceLoadingItem() { | ||||
|     Row( | ||||
|         modifier = Modifier | ||||
|             .fillMaxWidth() | ||||
|             .padding(vertical = 16.dp), | ||||
|         horizontalArrangement = Arrangement.Center, | ||||
|     ) { | ||||
|         CircularProgressIndicator() | ||||
|     } | ||||
| } | ||||
| @@ -1,126 +0,0 @@ | ||||
| package eu.kanade.presentation.browse.components | ||||
|  | ||||
| import androidx.compose.material.icons.Icons | ||||
| import androidx.compose.material.icons.automirrored.filled.ViewList | ||||
| import androidx.compose.material.icons.filled.ViewModule | ||||
| import androidx.compose.material3.Text | ||||
| import androidx.compose.material3.TopAppBarScrollBehavior | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.runtime.getValue | ||||
| import androidx.compose.runtime.mutableStateOf | ||||
| import androidx.compose.runtime.remember | ||||
| import androidx.compose.runtime.setValue | ||||
| import eu.kanade.presentation.components.AppBar | ||||
| import eu.kanade.presentation.components.AppBarActions | ||||
| import eu.kanade.presentation.components.AppBarTitle | ||||
| import eu.kanade.presentation.components.DropdownMenu | ||||
| import eu.kanade.presentation.components.RadioMenuItem | ||||
| import eu.kanade.presentation.components.SearchToolbar | ||||
| import eu.kanade.tachiyomi.source.ConfigurableSource | ||||
| import eu.kanade.tachiyomi.source.Source | ||||
| import kotlinx.collections.immutable.persistentListOf | ||||
| import tachiyomi.domain.library.model.LibraryDisplayMode | ||||
| import tachiyomi.i18n.MR | ||||
| import tachiyomi.presentation.core.i18n.stringResource | ||||
| import tachiyomi.source.local.LocalSource | ||||
|  | ||||
| @Composable | ||||
| fun BrowseSourceToolbar( | ||||
|     searchQuery: String?, | ||||
|     onSearchQueryChange: (String?) -> Unit, | ||||
|     source: Source?, | ||||
|     displayMode: LibraryDisplayMode, | ||||
|     onDisplayModeChange: (LibraryDisplayMode) -> Unit, | ||||
|     navigateUp: () -> Unit, | ||||
|     onWebViewClick: () -> Unit, | ||||
|     onHelpClick: () -> Unit, | ||||
|     onSettingsClick: () -> Unit, | ||||
|     onSearch: (String) -> Unit, | ||||
|     scrollBehavior: TopAppBarScrollBehavior? = null, | ||||
| ) { | ||||
|     // Avoid capturing unstable source in actions lambda | ||||
|     val title = source?.name | ||||
|     val isLocalSource = source is LocalSource | ||||
|     val isConfigurableSource = source is ConfigurableSource | ||||
|  | ||||
|     var selectingDisplayMode by remember { mutableStateOf(false) } | ||||
|  | ||||
|     SearchToolbar( | ||||
|         navigateUp = navigateUp, | ||||
|         titleContent = { AppBarTitle(title) }, | ||||
|         searchQuery = searchQuery, | ||||
|         onChangeSearchQuery = onSearchQueryChange, | ||||
|         onSearch = onSearch, | ||||
|         onClickCloseSearch = navigateUp, | ||||
|         actions = { | ||||
|             AppBarActions( | ||||
|                 actions = persistentListOf<AppBar.AppBarAction>().builder() | ||||
|                     .apply { | ||||
|                         add( | ||||
|                             AppBar.Action( | ||||
|                                 title = stringResource(MR.strings.action_display_mode), | ||||
|                                 icon = if (displayMode == LibraryDisplayMode.List) { | ||||
|                                     Icons.AutoMirrored.Filled.ViewList | ||||
|                                 } else { | ||||
|                                     Icons.Filled.ViewModule | ||||
|                                 }, | ||||
|                                 onClick = { selectingDisplayMode = true }, | ||||
|                             ), | ||||
|                         ) | ||||
|                         if (isLocalSource) { | ||||
|                             add( | ||||
|                                 AppBar.OverflowAction( | ||||
|                                     title = stringResource(MR.strings.label_help), | ||||
|                                     onClick = onHelpClick, | ||||
|                                 ), | ||||
|                             ) | ||||
|                         } else { | ||||
|                             add( | ||||
|                                 AppBar.OverflowAction( | ||||
|                                     title = stringResource(MR.strings.action_open_in_web_view), | ||||
|                                     onClick = onWebViewClick, | ||||
|                                 ), | ||||
|                             ) | ||||
|                         } | ||||
|                         if (isConfigurableSource) { | ||||
|                             add( | ||||
|                                 AppBar.OverflowAction( | ||||
|                                     title = stringResource(MR.strings.action_settings), | ||||
|                                     onClick = onSettingsClick, | ||||
|                                 ), | ||||
|                             ) | ||||
|                         } | ||||
|                     } | ||||
|                     .build(), | ||||
|             ) | ||||
|  | ||||
|             DropdownMenu( | ||||
|                 expanded = selectingDisplayMode, | ||||
|                 onDismissRequest = { selectingDisplayMode = false }, | ||||
|             ) { | ||||
|                 RadioMenuItem( | ||||
|                     text = { Text(text = stringResource(MR.strings.action_display_comfortable_grid)) }, | ||||
|                     isChecked = displayMode == LibraryDisplayMode.ComfortableGrid, | ||||
|                 ) { | ||||
|                     selectingDisplayMode = false | ||||
|                     onDisplayModeChange(LibraryDisplayMode.ComfortableGrid) | ||||
|                 } | ||||
|                 RadioMenuItem( | ||||
|                     text = { Text(text = stringResource(MR.strings.action_display_grid)) }, | ||||
|                     isChecked = displayMode == LibraryDisplayMode.CompactGrid, | ||||
|                 ) { | ||||
|                     selectingDisplayMode = false | ||||
|                     onDisplayModeChange(LibraryDisplayMode.CompactGrid) | ||||
|                 } | ||||
|                 RadioMenuItem( | ||||
|                     text = { Text(text = stringResource(MR.strings.action_display_list)) }, | ||||
|                     isChecked = displayMode == LibraryDisplayMode.List, | ||||
|                 ) { | ||||
|                     selectingDisplayMode = false | ||||
|                     onDisplayModeChange(LibraryDisplayMode.List) | ||||
|                 } | ||||
|             } | ||||
|         }, | ||||
|         scrollBehavior = scrollBehavior, | ||||
|     ) | ||||
| } | ||||
| @@ -1,88 +0,0 @@ | ||||
| package eu.kanade.presentation.browse.components | ||||
|  | ||||
| import androidx.compose.foundation.layout.Arrangement | ||||
| import androidx.compose.foundation.layout.Box | ||||
| import androidx.compose.foundation.layout.PaddingValues | ||||
| import androidx.compose.foundation.layout.padding | ||||
| import androidx.compose.foundation.layout.width | ||||
| import androidx.compose.foundation.lazy.LazyRow | ||||
| import androidx.compose.foundation.lazy.items | ||||
| import androidx.compose.material3.MaterialTheme | ||||
| import androidx.compose.material3.Text | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.runtime.State | ||||
| import androidx.compose.runtime.getValue | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.unit.dp | ||||
| import eu.kanade.presentation.library.components.CommonMangaItemDefaults | ||||
| import eu.kanade.presentation.library.components.MangaComfortableGridItem | ||||
| import tachiyomi.domain.manga.model.Manga | ||||
| import tachiyomi.domain.manga.model.MangaCover | ||||
| import tachiyomi.domain.manga.model.asMangaCover | ||||
| import tachiyomi.i18n.MR | ||||
| import tachiyomi.presentation.core.components.material.padding | ||||
| import tachiyomi.presentation.core.i18n.stringResource | ||||
|  | ||||
| @Composable | ||||
| fun GlobalSearchCardRow( | ||||
|     titles: List<Manga>, | ||||
|     getManga: @Composable (Manga) -> State<Manga>, | ||||
|     onClick: (Manga) -> Unit, | ||||
|     onLongClick: (Manga) -> Unit, | ||||
| ) { | ||||
|     if (titles.isEmpty()) { | ||||
|         EmptyResultItem() | ||||
|         return | ||||
|     } | ||||
|  | ||||
|     LazyRow( | ||||
|         contentPadding = PaddingValues(MaterialTheme.padding.small), | ||||
|         horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall), | ||||
|     ) { | ||||
|         items(titles) { | ||||
|             val title by getManga(it) | ||||
|             MangaItem( | ||||
|                 title = title.title, | ||||
|                 cover = title.asMangaCover(), | ||||
|                 isFavorite = title.favorite, | ||||
|                 onClick = { onClick(title) }, | ||||
|                 onLongClick = { onLongClick(title) }, | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @Composable | ||||
| private fun MangaItem( | ||||
|     title: String, | ||||
|     cover: MangaCover, | ||||
|     isFavorite: Boolean, | ||||
|     onClick: () -> Unit, | ||||
|     onLongClick: () -> Unit, | ||||
| ) { | ||||
|     Box(modifier = Modifier.width(96.dp)) { | ||||
|         MangaComfortableGridItem( | ||||
|             title = title, | ||||
|             titleMaxLines = 3, | ||||
|             coverData = cover, | ||||
|             coverBadgeStart = { | ||||
|                 InLibraryBadge(enabled = isFavorite) | ||||
|             }, | ||||
|             coverAlpha = if (isFavorite) CommonMangaItemDefaults.BrowseFavoriteCoverAlpha else 1f, | ||||
|             onClick = onClick, | ||||
|             onLongClick = onLongClick, | ||||
|         ) | ||||
|     } | ||||
| } | ||||
|  | ||||
| @Composable | ||||
| private fun EmptyResultItem() { | ||||
|     Text( | ||||
|         text = stringResource(MR.strings.no_results_found), | ||||
|         modifier = Modifier | ||||
|             .padding( | ||||
|                 horizontal = MaterialTheme.padding.medium, | ||||
|                 vertical = MaterialTheme.padding.small, | ||||
|             ), | ||||
|     ) | ||||
| } | ||||
| @@ -1,100 +0,0 @@ | ||||
| package eu.kanade.presentation.browse.components | ||||
|  | ||||
| import androidx.compose.foundation.clickable | ||||
| import androidx.compose.foundation.layout.Arrangement | ||||
| import androidx.compose.foundation.layout.Box | ||||
| import androidx.compose.foundation.layout.Column | ||||
| import androidx.compose.foundation.layout.Row | ||||
| import androidx.compose.foundation.layout.Spacer | ||||
| import androidx.compose.foundation.layout.fillMaxWidth | ||||
| import androidx.compose.foundation.layout.height | ||||
| import androidx.compose.foundation.layout.padding | ||||
| import androidx.compose.foundation.layout.size | ||||
| import androidx.compose.material.icons.Icons | ||||
| import androidx.compose.material.icons.automirrored.outlined.ArrowForward | ||||
| import androidx.compose.material.icons.outlined.Error | ||||
| import androidx.compose.material3.CircularProgressIndicator | ||||
| import androidx.compose.material3.Icon | ||||
| import androidx.compose.material3.IconButton | ||||
| import androidx.compose.material3.MaterialTheme | ||||
| import androidx.compose.material3.Text | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.ui.Alignment | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.text.style.TextAlign | ||||
| import androidx.compose.ui.unit.dp | ||||
| import tachiyomi.i18n.MR | ||||
| import tachiyomi.presentation.core.components.material.padding | ||||
| import tachiyomi.presentation.core.i18n.stringResource | ||||
|  | ||||
| @Composable | ||||
| fun GlobalSearchResultItem( | ||||
|     title: String, | ||||
|     subtitle: String, | ||||
|     onClick: () -> Unit, | ||||
|     modifier: Modifier = Modifier, | ||||
|     content: @Composable () -> Unit, | ||||
| ) { | ||||
|     Column(modifier = modifier) { | ||||
|         Row( | ||||
|             modifier = Modifier | ||||
|                 .padding( | ||||
|                     start = MaterialTheme.padding.medium, | ||||
|                     end = MaterialTheme.padding.extraSmall, | ||||
|                 ) | ||||
|                 .fillMaxWidth() | ||||
|                 .clickable(onClick = onClick), | ||||
|             horizontalArrangement = Arrangement.SpaceBetween, | ||||
|             verticalAlignment = Alignment.CenterVertically, | ||||
|         ) { | ||||
|             Column { | ||||
|                 Text( | ||||
|                     text = title, | ||||
|                     style = MaterialTheme.typography.titleMedium, | ||||
|                 ) | ||||
|                 Text(text = subtitle) | ||||
|             } | ||||
|             IconButton(onClick = onClick) { | ||||
|                 Icon(imageVector = Icons.AutoMirrored.Outlined.ArrowForward, contentDescription = null) | ||||
|             } | ||||
|         } | ||||
|         content() | ||||
|     } | ||||
| } | ||||
|  | ||||
| @Composable | ||||
| fun GlobalSearchLoadingResultItem() { | ||||
|     Box( | ||||
|         modifier = Modifier | ||||
|             .fillMaxWidth() | ||||
|             .padding(vertical = MaterialTheme.padding.medium), | ||||
|     ) { | ||||
|         CircularProgressIndicator( | ||||
|             modifier = Modifier | ||||
|                 .size(16.dp) | ||||
|                 .align(Alignment.Center), | ||||
|             strokeWidth = 2.dp, | ||||
|         ) | ||||
|     } | ||||
| } | ||||
|  | ||||
| @Composable | ||||
| fun GlobalSearchErrorResultItem(message: String?) { | ||||
|     Column( | ||||
|         modifier = Modifier | ||||
|             .padding( | ||||
|                 horizontal = MaterialTheme.padding.medium, | ||||
|                 vertical = MaterialTheme.padding.small, | ||||
|             ) | ||||
|             .fillMaxWidth(), | ||||
|         horizontalAlignment = Alignment.CenterHorizontally, | ||||
|         verticalArrangement = Arrangement.Center, | ||||
|     ) { | ||||
|         Icon(imageVector = Icons.Outlined.Error, contentDescription = null) | ||||
|         Spacer(Modifier.height(4.dp)) | ||||
|         Text( | ||||
|             text = message ?: stringResource(MR.strings.unknown_error), | ||||
|             textAlign = TextAlign.Center, | ||||
|         ) | ||||
|     } | ||||
| } | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user