mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-10-31 06:17:57 +01:00 
			
		
		
		
	Compare commits
	
		
			7 Commits
		
	
	
		
			v0.19.1
			...
			ab0893b2d4
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | ab0893b2d4 | ||
|  | 078758391e | ||
|  | 2eb1580788 | ||
|  | d328ded17f | ||
|  | 80f9dfb699 | ||
|  | 3d087f4428 | ||
|  | 0ab795bfa3 | 
| @@ -1,31 +1,8 @@ | ||||
| root = true | ||||
|  | ||||
| [*] | ||||
| charset = utf-8 | ||||
| indent_size = 2 | ||||
| indent_style = space | ||||
| insert_final_newline = true | ||||
| trim_trailing_whitespace = true | ||||
|  | ||||
| [*.{xml,sq,sqm}] | ||||
| indent_size = 4 | ||||
|  | ||||
| # noinspection EditorConfigKeyCorrectness | ||||
| [*.{kt,kts}] | ||||
| indent_size = 4 | ||||
| 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 | ||||
|  | ||||
| ktlint_code_style = intellij_idea | ||||
| ktlint_function_naming_ignore_when_annotated_with = Composable | ||||
| ktlint_standard_class-signature = disabled | ||||
| ktlint_standard_comment-wrapping = disabled | ||||
| ktlint_standard_discouraged-comment-location = disabled | ||||
| ktlint_standard_function-expression-body = disabled | ||||
| ktlint_standard_function-signature = disabled | ||||
| ktlint_standard_type-argument-comment = disabled | ||||
| ktlint_standard_type-parameter-comment = disabled | ||||
|   | ||||
							
								
								
									
										3
									
								
								.github/ISSUE_TEMPLATE/config.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.github/ISSUE_TEMPLATE/config.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,8 +1,5 @@ | ||||
| blank_issues_enabled: false | ||||
| contact_links: | ||||
|   - name: ❌ Help with Extensions | ||||
|     url: https://mihon.app/docs/faq/browse/extensions | ||||
|     about: For extension-related questions/issues | ||||
|   - name: 🖥️ Mihon website | ||||
|     url: https://mihon.app/ | ||||
|     about: Guides, troubleshooting, and answers to common questions | ||||
|   | ||||
| @@ -1,7 +1,8 @@ | ||||
| name: 🐞 Issue report | ||||
| description: Report an issue in Mihon | ||||
| labels: [bug] | ||||
| labels: [Bug] | ||||
| body: | ||||
| 
 | ||||
|   - type: textarea | ||||
|     id: reproduce-steps | ||||
|     attributes: | ||||
| @@ -42,9 +43,9 @@ body: | ||||
|     attributes: | ||||
|       label: Crash logs | ||||
|       description: | | ||||
|         If you're experiencing crashes, if possible, go to the app's **More → Settings → Advanced** page, press **Dump crash logs** and share the crash logs here. | ||||
|         If you're experiencing crashes, share the crash logs from **More → Settings → Advanced** then press **Dump crash logs**. | ||||
|       placeholder: | | ||||
|         You can upload the crash log file as an attachment, or paste the crash logs in plain text if needed. | ||||
|         You can paste the crash logs in plain text or upload it as an attachment. | ||||
| 
 | ||||
|   - type: input | ||||
|     id: mihon-version | ||||
| @@ -52,7 +53,7 @@ body: | ||||
|       label: Mihon version | ||||
|       description: You can find your Mihon version in **More → About**. | ||||
|       placeholder: | | ||||
|         Example: "0.19.1" | ||||
|         Example: "0.16.5" | ||||
|     validations: | ||||
|       required: true | ||||
| 
 | ||||
| @@ -95,9 +96,9 @@ body: | ||||
|           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.19.1](https://github.com/mihonapp/mihon/releases/latest)**. | ||||
|         - label: I have updated the app to version **[0.16.5](https://github.com/mihonapp/mihon/releases/latest)**. | ||||
|           required: true | ||||
|         - label: I have filled out all of the requested information in this form, including specific version numbers. | ||||
|         - label: I have updated all installed extensions. | ||||
|           required: true | ||||
|         - label: I understand that **Mihon does not have or fix any extensions**, and I **will not receive help** for any issues related to sources or extensions. | ||||
|         - label: I will fill out all of the requested information in this form. | ||||
|           required: true | ||||
| @@ -1,7 +1,8 @@ | ||||
| name: ⭐ Feature request | ||||
| description: Suggest a feature to improve Mihon | ||||
| labels: [feature request] | ||||
| labels: [Feature request] | ||||
| body: | ||||
| 
 | ||||
|   - type: textarea | ||||
|     id: feature-description | ||||
|     attributes: | ||||
| @@ -30,7 +31,7 @@ body: | ||||
|           required: true | ||||
|         - label: I have written a short but informative title. | ||||
|           required: true | ||||
|         - label: I have updated the app to version **[0.19.1](https://github.com/mihonapp/mihon/releases/latest)**. | ||||
|         - 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 | ||||
							
								
								
									
										11
									
								
								.github/renovate.json5
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										11
									
								
								.github/renovate.json5
									
									
									
									
										vendored
									
									
								
							| @@ -1,13 +1,6 @@ | ||||
| { | ||||
|   "$schema": "https://docs.renovatebot.com/renovate-schema.json", | ||||
|   "extends": ["config:recommended"], | ||||
|   "extends": ["config:base"], | ||||
|   "labels": ["Dependencies"], | ||||
|   "semanticCommits": "disabled", | ||||
|   "packageRules": [ | ||||
|     { | ||||
|       "groupName": "GitHub Actions", | ||||
|       "matchManagers": ["github-actions"], | ||||
|       "pinDigests": true, | ||||
|     }, | ||||
|   ], | ||||
|   "semanticCommits": "disabled" | ||||
| } | ||||
|   | ||||
							
								
								
									
										63
									
								
								.github/workflows/build.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										63
									
								
								.github/workflows/build.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,63 +0,0 @@ | ||||
| name: Build & Test | ||||
| on: | ||||
|   pull_request: | ||||
|     paths: | ||||
|       - '**' | ||||
|       - '!**.md' | ||||
|       - '!i18n/src/commonMain/moko-resources/**/strings.xml' | ||||
|       - '!i18n/src/commonMain/moko-resources/**/plurals.xml' | ||||
|       - 'i18n/src/commonMain/moko-resources/base/strings.xml' | ||||
|       - 'i18n/src/commonMain/moko-resources/base/plurals.xml' | ||||
|   push: | ||||
|     branches: | ||||
|       - main | ||||
|  | ||||
| concurrency: | ||||
|   group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} | ||||
|   cancel-in-progress: true | ||||
|  | ||||
| permissions: | ||||
|   contents: read | ||||
|  | ||||
| jobs: | ||||
|   build: | ||||
|     name: Build & Test App | ||||
|     runs-on: 'ubuntu-24.04' | ||||
|  | ||||
|     steps: | ||||
|       - name: Checkout | ||||
|         uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 | ||||
|  | ||||
|       - name: Dependency Review | ||||
|         if: github.event_name == 'pull_request' | ||||
|         uses: actions/dependency-review-action@da24556b548a50705dd671f47852072ea4c105d9 # v4.7.1 | ||||
|  | ||||
|       - name: Set up JDK | ||||
|         uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 | ||||
|         with: | ||||
|           java-version: 17 | ||||
|           distribution: temurin | ||||
|  | ||||
|       - name: Set up Gradle | ||||
|         uses: gradle/actions/setup-gradle@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2 | ||||
|  | ||||
|       - name: Check code format | ||||
|         run: ./gradlew spotlessCheck | ||||
|  | ||||
|       - name: Build app | ||||
|         run: ./gradlew assembleRelease -Pinclude-telemetry -Penable-updater | ||||
|  | ||||
|       - name: Run unit tests | ||||
|         run: ./gradlew testReleaseUnitTest | ||||
|  | ||||
|       - name: Upload APK | ||||
|         uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 | ||||
|         with: | ||||
|           name: arm64-v8a-${{ github.sha }} | ||||
|           path: app/build/outputs/apk/release/app-arm64-v8a-release-unsigned.apk | ||||
|  | ||||
|       - name: Upload mapping | ||||
|         uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 | ||||
|         with: | ||||
|           name: mapping-${{ github.sha }} | ||||
|           path: app/build/outputs/mapping/release | ||||
							
								
								
									
										53
									
								
								.github/workflows/build_pull_request.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								.github/workflows/build_pull_request.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,53 @@ | ||||
| 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
									
									
										Normal file
									
								
							
							
						
						
									
										125
									
								
								.github/workflows/build_push.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,125 @@ | ||||
| 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
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								.github/workflows/lock.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | ||||
| 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' | ||||
							
								
								
									
										171
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										171
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,171 +0,0 @@ | ||||
| name: Release | ||||
| on: | ||||
|   push: | ||||
|     tags: | ||||
|       - v* | ||||
|  | ||||
| concurrency: | ||||
|   group: ${{ github.workflow }}-${{ github.ref }} | ||||
|   cancel-in-progress: true | ||||
|  | ||||
| jobs: | ||||
|   get_tag: | ||||
|     if: github.repository == 'mihonapp/mihon' | ||||
|     name: Extract tag name | ||||
|     runs-on: 'ubuntu-24.04' | ||||
|     outputs: | ||||
|       tag: ${{ steps.extract.outputs.tag }} | ||||
|  | ||||
|     steps: | ||||
|       - name: Get tag name | ||||
|         id: extract | ||||
|         run: echo "tag=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_OUTPUT | ||||
|  | ||||
|   build: | ||||
|     if: github.repository == 'mihonapp/mihon' | ||||
|     name: Build | ||||
|     runs-on: 'ubuntu-24.04' | ||||
|     needs: get_tag | ||||
|  | ||||
|     steps: | ||||
|       - name: Checkout | ||||
|         uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 | ||||
|  | ||||
|       - name: Set up JDK | ||||
|         uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 | ||||
|         with: | ||||
|           java-version: 17 | ||||
|           distribution: temurin | ||||
|  | ||||
|       - name: Set up Gradle | ||||
|         uses: gradle/actions/setup-gradle@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2 | ||||
|  | ||||
|       - name: Build | ||||
|         run: ./gradlew assembleRelease -Pinclude-telemetry -Penable-updater | ||||
|  | ||||
|       - name: Sign APK | ||||
|         uses: r0adkll/sign-android-release@f30bdd30588842ac76044ecdbd4b6d0e3e813478 | ||||
|         with: | ||||
|           releaseDirectory: app/build/outputs/apk/release | ||||
|           signingKeyBase64: ${{ secrets.SIGNING_KEY }} | ||||
|           alias: ${{ secrets.ALIAS }} | ||||
|           keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }} | ||||
|           keyPassword: ${{ secrets.KEY_PASSWORD }} | ||||
|         env: | ||||
|           BUILD_TOOLS_VERSION: '35.0.1' | ||||
|  | ||||
|       - name: Rename APK | ||||
|         run: | | ||||
|           set -e | ||||
|  | ||||
|           mv app/build/outputs/apk/release/app-universal-release-unsigned-signed.apk mihon-${{ needs.get_tag.outputs.tag }}.apk | ||||
|           mv app/build/outputs/apk/release/app-arm64-v8a-release-unsigned-signed.apk mihon-arm64-v8a-${{ needs.get_tag.outputs.tag }}.apk | ||||
|           mv app/build/outputs/apk/release/app-armeabi-v7a-release-unsigned-signed.apk mihon-armeabi-v7a-${{ needs.get_tag.outputs.tag }}.apk | ||||
|           mv app/build/outputs/apk/release/app-x86-release-unsigned-signed.apk mihon-x86-${{ needs.get_tag.outputs.tag }}.apk | ||||
|           mv app/build/outputs/apk/release/app-x86_64-release-unsigned-signed.apk mihon-x86_64-${{ needs.get_tag.outputs.tag }}.apk | ||||
|  | ||||
|       - name: Upload APK | ||||
|         uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 | ||||
|         with: | ||||
|           name: mihon | ||||
|           path: | | ||||
|             mihon-${{ needs.get_tag.outputs.tag }}.apk | ||||
|             mihon-arm64-v8a-${{ needs.get_tag.outputs.tag }}.apk | ||||
|             mihon-armeabi-v7a-${{ needs.get_tag.outputs.tag }}.apk | ||||
|             mihon-x86-${{ needs.get_tag.outputs.tag }}.apk | ||||
|             mihon-x86_64-${{ needs.get_tag.outputs.tag }}.apk | ||||
|  | ||||
|   build_foss: | ||||
|     if: github.repository == 'mihonapp/mihon' | ||||
|     name: Build (FOSS) | ||||
|     runs-on: ubuntu-24.04 | ||||
|     needs: get_tag | ||||
|  | ||||
|     steps: | ||||
|       - name: Checkout | ||||
|         uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 | ||||
|  | ||||
|       - name: Set up JDK | ||||
|         uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 | ||||
|         with: | ||||
|           java-version: 17 | ||||
|           distribution: temurin | ||||
|  | ||||
|       - name: Set up Gradle | ||||
|         uses: gradle/actions/setup-gradle@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2 | ||||
|         with: | ||||
|           cache-disabled: true | ||||
|  | ||||
|       - name: Build | ||||
|         run: ./gradlew assembleFoss -Penable-updater | ||||
|  | ||||
|       - name: Sign APK | ||||
|         uses: r0adkll/sign-android-release@f30bdd30588842ac76044ecdbd4b6d0e3e813478 | ||||
|         with: | ||||
|           releaseDirectory: app/build/outputs/apk/foss | ||||
|           signingKeyBase64: ${{ secrets.SIGNING_KEY }} | ||||
|           alias: ${{ secrets.ALIAS }} | ||||
|           keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }} | ||||
|           keyPassword: ${{ secrets.KEY_PASSWORD }} | ||||
|         env: | ||||
|           BUILD_TOOLS_VERSION: '35.0.1' | ||||
|  | ||||
|       - name: Rename APK | ||||
|         run: | | ||||
|           set -e | ||||
|  | ||||
|           mv app/build/outputs/apk/foss/app-universal-foss-unsigned-signed.apk mihon-${{ needs.get_tag.outputs.tag }}-foss.apk | ||||
|  | ||||
|       - name: Upload APK | ||||
|         uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 | ||||
|         with: | ||||
|           name: mihon-foss | ||||
|           path: mihon-${{ needs.get_tag.outputs.tag }}-foss.apk | ||||
|  | ||||
|   release: | ||||
|     if: github.repository == 'mihonapp/mihon' | ||||
|     name: Create GitHub Release | ||||
|     runs-on: ubuntu-24.04 | ||||
|     needs: [get_tag, build, build_foss] | ||||
|  | ||||
|     steps: | ||||
|       - name: Download all artifacts | ||||
|         uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 | ||||
|         with: | ||||
|           merge-multiple: true | ||||
|  | ||||
|       - name: Delete all artifacts | ||||
|         uses: geekyeggo/delete-artifact@f275313e70c08f6120db482d7a6b98377786765b # v5.1.0 | ||||
|         with: | ||||
|           failOnError: false | ||||
|           name: | | ||||
|             mihon | ||||
|             mihon-foss | ||||
|  | ||||
|       - name: Create GitHub Release | ||||
|         uses: softprops/action-gh-release@72f2c25fcb47643c292f7107632f7a47c1df5cd8 # v2.3.2 | ||||
|         with: | ||||
|           tag_name: ${{ needs.get_tag.outputs.tag }} | ||||
|           name: Mihon ${{ needs.get_tag.outputs.tag }} | ||||
|           body: | | ||||
|             Check out the [past release notes](https://github.com/mihonapp/mihon/releases) if you’re upgrading from an earlier version. Consider [donating via Open Collective](https://opencollective.com/mihon/contribute) to help keep Mihon improving! | ||||
|  | ||||
|             <!--> | ||||
|  | ||||
|  | ||||
|  | ||||
|             <!--> | ||||
|  | ||||
|             > [!TIP] | ||||
|             > | ||||
|             > ### If you are unsure which version to download then go with `mihon-${{ needs.get_tag.outputs.tag }}.apk` | ||||
|           files: | | ||||
|             mihon-${{ needs.get_tag.outputs.tag }}.apk | ||||
|             mihon-${{ needs.get_tag.outputs.tag }}-foss.apk | ||||
|             mihon-arm64-v8a-${{ needs.get_tag.outputs.tag }}.apk | ||||
|             mihon-armeabi-v7a-${{ needs.get_tag.outputs.tag }}.apk | ||||
|             mihon-x86-${{ needs.get_tag.outputs.tag }}.apk | ||||
|             mihon-x86_64-${{ needs.get_tag.outputs.tag }}.apk | ||||
|           draft: true | ||||
|           prerelease: false | ||||
|           token: ${{ secrets.MIHON_BOT_TOKEN }} | ||||
							
								
								
									
										23
									
								
								.github/workflows/update_website.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										23
									
								
								.github/workflows/update_website.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,23 +0,0 @@ | ||||
| name: Update website | ||||
|  | ||||
| on: | ||||
|   release: | ||||
|     types:  | ||||
|       - published | ||||
|       - deleted | ||||
|       - edited | ||||
|  | ||||
| jobs: | ||||
|   update_website: | ||||
|     runs-on: 'ubuntu-24.04' | ||||
|  | ||||
|     steps: | ||||
|       - name: Update website | ||||
|         run: | | ||||
|           curl --fail-with-body -L \ | ||||
|           -X POST \ | ||||
|           -H "Accept: application/vnd.github+json" \ | ||||
|           -H "Authorization: Bearer ${{ secrets.MIHON_BOT_TOKEN }}" \ | ||||
|           -H "X-GitHub-Api-Version: 2022-11-28" \ | ||||
|           https://api.github.com/repos/mihonapp/website/dispatches \ | ||||
|           -d '{"event_type":"app_release"}' | ||||
							
								
								
									
										28
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										28
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -1,16 +1,18 @@ | ||||
| # Build files | ||||
| .gradle | ||||
| .kotlin | ||||
| build | ||||
|  | ||||
| # IDE files | ||||
| *.iml | ||||
| .idea/* | ||||
| !.idea/icon.svg | ||||
| /captures | ||||
|  | ||||
| # Configuration files | ||||
| local.properties | ||||
|  | ||||
| # macOS specific files | ||||
| /local.properties | ||||
| /.idea/workspace.xml | ||||
| .DS_Store | ||||
| .idea/* | ||||
| !.idea/icon.png | ||||
| *iml | ||||
| *.iml | ||||
|  | ||||
| # Built files | ||||
| */build | ||||
| /build | ||||
| *.apk | ||||
| app/**/output.json | ||||
|  | ||||
| # Unnecessary file | ||||
| *.swp | ||||
							
								
								
									
										
											BIN
										
									
								
								.idea/icon.png
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								.idea/icon.png
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 62 KiB | 
							
								
								
									
										6
									
								
								.idea/icon.svg
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										6
									
								
								.idea/icon.svg
									
									
									
										generated
									
									
									
								
							| @@ -1,6 +0,0 @@ | ||||
| <svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" fill="none" viewBox="0 0 432 432"> | ||||
|   <circle cx="216" cy="216" r="216" fill="#2e3943"/> | ||||
|   <path fill="#f2faff" d="M398.073 216c0 97.433-81.517 176.419-182.073 176.419-100.556 0-182.073-78.986-182.073-176.419S115.444 39.581 216 39.581c100.556 0 182.073 78.986 182.073 176.419Z"/> | ||||
|   <path fill="#7ebbed" fill-rule="evenodd" d="M216 359.34c81.702 0 147.934-64.175 147.934-143.34S297.702 72.66 216 72.66 68.065 136.835 68.065 216 134.298 359.34 216 359.34zm0 33.079c100.556 0 182.073-78.986 182.073-176.419S316.556 39.581 216 39.581C115.444 39.581 33.927 118.567 33.927 216S115.444 392.419 216 392.419z" clip-rule="evenodd"/> | ||||
|   <path fill="#031019" d="m155.273 168.033-1.227-28.215c3.68.7 8.063.875 18.052.875 12.092 0 28.041-.7 36.279-1.752 3.504-.35 4.907-.876 7.185-2.103l18.928 16.124c-1.753 2.453-2.279 3.505-4.207 8.412-1.576 3.856-8.762 26.113-11.567 35.577 12.97 2.63 20.155 4.557 29.97 8.588 1.226-8.588 1.401-13.144 1.401-28.742 0-4.03-.175-6.31-.7-9.99l30.495 1.051c-.877 4.207-1.052 5.959-1.227 12.794-.701 16.475-1.403 24.361-3.154 36.279 12.092 6.134 12.092 6.134 18.226 9.464 3.154 1.752 3.855 2.102 5.959 2.804l-10.165 32.773c-4.908-4.381-11.743-9.113-21.732-14.721-8.763 20.855-23.31 36.103-45.392 48.195-7.36-9.814-12.97-15.772-21.907-22.783 12.969-6.134 18.928-9.99 25.763-16.475 6.66-6.484 11.04-12.793 15.247-22.258-11.217-5.082-18.403-7.36-30.846-9.989-7.185 21.382-12.969 35.052-18.051 43.29-6.835 11.04-16.124 16.824-26.815 16.824-8.237 0-16.65-3.68-22.784-9.99-7.01-7.185-10.69-17.175-10.69-28.742 0-17.176 8.238-32.072 22.609-41.361 9.288-5.959 19.103-8.588 34.7-9.465 3.155-10.34 5.785-19.278 8.238-29.267-7.712.701-17.35 1.227-29.093 1.752-6.309.175-8.412.35-13.495 1.051zm26.64 53.279c-8.238 1.402-13.145 4.031-17.527 9.64-3.33 3.855-4.907 8.412-4.907 13.32 0 5.432 2.63 9.464 5.959 9.464 4.03 0 8.588-9.114 16.475-32.424z"/> | ||||
| </svg> | ||||
| Before Width: | Height: | Size: 1.9 KiB | 
							
								
								
									
										375
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										375
									
								
								CHANGELOG.md
									
									
									
									
									
								
							| @@ -2,410 +2,133 @@ | ||||
|  | ||||
| All notable changes to this project will be documented in this file. | ||||
|  | ||||
| The format is a modified version of [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). | ||||
| - `Added` - for new features. | ||||
| - `Changed ` - for changes in existing functionality. | ||||
| - `Improved` - for enhancement or optimization in existing functionality. | ||||
| - `Removed` - for now removed features. | ||||
| - `Fixed` - for any bug fixes. | ||||
| - `Other` - for technical stuff. | ||||
| 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] | ||||
|  | ||||
| ## [v0.19.1] - 2025-08-07 | ||||
| ### Changed | ||||
| - LocalSource now reads ComicInfo.xml file for chapter (if available) to display chapter title, number and scanlator ([@raxod502](https://github.com/radian-software)) ([#2332](https://github.com/mihonapp/mihon/pull/2332)) | ||||
|  | ||||
| ### Removed | ||||
| - Predictive back support ([@AntsyLich](https://github.com/AntsyLich)) ([#2362](https://github.com/mihonapp/mihon/pull/2362)) | ||||
|  | ||||
| ### Fixes | ||||
| - Fix scrollbar sometimes not showing during scroll or not reaching the bottom with few items ([@anirudhsnayak](https://github.com/anirudhsnayak)) ([#2304](https://github.com/mihonapp/mihon/pull/2304)) | ||||
| - Fix local source EPUB files not loading ([@AntsyLich](https://github.com/AntsyLich)) ([#2369](https://github.com/mihonapp/mihon/pull/2369)) | ||||
| - Fix title text color in light mode on mass migration list ([@AntsyLich](https://github.com/AntsyLich)) ([#2370](https://github.com/mihonapp/mihon/pull/2370)) | ||||
| - Fix 'Default' category showing in library with no user-added categories ([@AntsyLich](https://github.com/AntsyLich)) ([#2371](https://github.com/mihonapp/mihon/pull/2371)) | ||||
| - Fix crash when opening filter sheet with an empty library ([@krysanify](https://github.com/krysanify/)) ([#2355](https://github.com/mihonapp/mihon/pull/2355)) | ||||
| - Fix mark as read/unread not working for selected library items ([@krysanify](https://github.com/krysanify/)) ([#2355](https://github.com/mihonapp/mihon/pull/2355)) | ||||
|  | ||||
| ## [v0.19.0] - 2025-08-04 | ||||
| ### Added | ||||
| - Add more Kaomoji for empty/error screens ([@ianfhunter](https://github.com/ianfhunter/)) ([#1909](https://github.com/mihonapp/mihon/pull/1909)) | ||||
| - Add user manga notes ([@imkunet](https://github.com/imkunet), [@AntsyLich](https://github.com/AntsyLich)) ([#428](https://github.com/mihonapp/mihon/pull/428)) | ||||
|   - Fix user notes not restoring when manga doesn't exist in DB ([@AntsyLich](https://github.com/AntsyLich)) ([#1945](https://github.com/mihonapp/mihon/pull/1945)) | ||||
| - Add markdown support for manga descriptions ([@Secozzi](https://github.com/Secozzi)) ([#1948](https://github.com/mihonapp/mihon/pull/1948)) | ||||
|   - Use simpler markdown flavour ([@Secozzi](https://github.com/Secozzi)) ([#2000](https://github.com/mihonapp/mihon/pull/2000)) | ||||
|   - Use Github markdown flavour for Github releases & fix bullet list alignment ([@Secozzi](https://github.com/Secozzi)) ([#2024](https://github.com/mihonapp/mihon/pull/2024)) | ||||
|   - Add option to toggle image loading ([@Secozzi](https://github.com/Secozzi)) ([#2076](https://github.com/mihonapp/mihon/pull/2076)) | ||||
| - Add Nord Theme ([@Riztard](https://github.com/Riztard)) ([#1951](https://github.com/mihonapp/mihon/pull/1951)) | ||||
| - Option to keep read manga when clearing database ([@AwkwardPeak7](https://github.com/AwkwardPeak7)) ([#1979](https://github.com/mihonapp/mihon/pull/1979)) | ||||
| - Add advanced option to always update manga title from source ([@FlaminSarge](https://github.com/FlaminSarge)) ([#1182](https://github.com/mihonapp/mihon/pull/1182)) | ||||
| - Full predictive back support ([@AntsyLich](https://github.com/AntsyLich)) ([#2085](https://github.com/mihonapp/mihon/pull/2085)) | ||||
| - Add Catppuccin theme (mocha for dark and latte for light, mauve accent) ([@claymorwan](https://github.com/claymorwan/)) ([#2117](https://github.com/mihonapp/mihon/pull/2117)) | ||||
| - Manga mass migration ([@AntsyLich](https://github.com/AntsyLich), [@jobobby04](https://github.com/jobobby04)) ([#2110](https://github.com/mihonapp/mihon/pull/2110), [#2336](https://github.com/mihonapp/mihon/pull/2336), [#2338](https://github.com/mihonapp/mihon/pull/2338), [`f119386`](https://github.com/mihonapp/mihon/commit/f119386)) | ||||
|  | ||||
| ### Improved | ||||
| - Significantly improve browsing speed (near instantaneous) ([@AntsyLich](https://github.com/AntsyLich)) ([#1946](https://github.com/mihonapp/mihon/pull/1946)) | ||||
| - Deduplicate entries when browsing ([@AntsyLich](https://github.com/AntsyLich)) ([#1957](https://github.com/mihonapp/mihon/pull/1957)) | ||||
| - Update non-library manga data when browsing ([@AntsyLich](https://github.com/AntsyLich)) ([#1967](https://github.com/mihonapp/mihon/pull/1967)) | ||||
| - Surface image loading error in Reader ([@AwkwardPeak7](https://github.com/AwkwardPeak7)) ([#1981](https://github.com/mihonapp/mihon/pull/1981)) | ||||
| - Include source headers when opening failed images from reader ([@AwkwardPeak7](https://github.com/AwkwardPeak7)) ([#2004](https://github.com/mihonapp/mihon/pull/2004)) | ||||
| - Added autofill support to tracker login dialog ([@AntsyLich](https://github.com/AntsyLich)) ([#2069](https://github.com/mihonapp/mihon/pull/2069)) | ||||
| - Added option to hide missing chapter count ([@User826](https://github.com/User826), [@AntsyLich](https://github.com/AntsyLich)) ([#2108](https://github.com/mihonapp/mihon/pull/2108)) | ||||
| - Use median to determine smart update interval, making it more resilient to long hiatuses ([@Kladki](https://github.com/Kladki)) ([#2251](https://github.com/mihonapp/mihon/pull/2251)) | ||||
| - Optimize library code to potentially better handle big user libraries ([@AntsyLich](https://github.com/AntsyLich)) ([#2329](https://github.com/mihonapp/mihon/pull/2329), [#2341](https://github.com/mihonapp/mihon/pull/2341)) | ||||
|  | ||||
| ### Changed | ||||
| - Display all similarly named duplicates in duplicate manga dialogue ([@NarwhalHorns](https://github.com/NarwhalHorns), [@AntsyLich](https://github.com/AntsyLich)) ([#1861](https://github.com/mihonapp/mihon/pull/1861)) | ||||
|   - Display chapter count on items in duplicate manga dialogue ([@NarwhalHorns](https://github.com/NarwhalHorns)) ([#1963](https://github.com/mihonapp/mihon/pull/1963)) | ||||
| - Update Facebook and Reddit icons ([@Joehuu](https://github.com/Joehuu)) ([#1994](https://github.com/mihonapp/mihon/pull/1994)) | ||||
| - Switch default user agent to Android Chrome ([@AntsyLich](https://github.com/AntsyLich)) ([#2048](https://github.com/mihonapp/mihon/pull/2048)) | ||||
| - Changed log in button text when processing tracker login ([@AntsyLich](https://github.com/AntsyLich)) ([#2069](https://github.com/mihonapp/mihon/pull/2069)) | ||||
| - Disable reader's 'Keep screen on' setting by default ([@AntsyLich](https://github.com/AntsyLich)) ([#2095](https://github.com/mihonapp/mihon/pull/2095)) | ||||
| - Update manga without chapters even if restricted by source ([@AntsyLich](https://github.com/AntsyLich)) ([#2224](https://github.com/mihonapp/mihon/pull/224)) | ||||
| - Make local source default chapter sorting match file explorer behavior ([@AntsyLich](https://github.com/AntsyLich)) ([#2224](https://github.com/mihonapp/mihon/pull/224)) | ||||
| - Include Manga `initialized` status in backup ([@AwkwardPeak7](https://github.com/AwkwardPeak7)) ([#2285](https://github.com/mihonapp/mihon/pull/2285)) | ||||
|  | ||||
| ### Fixes | ||||
| - Fix Bangumi search results including novels ([@MajorTanya](https://github.com/MajorTanya)) ([#1885](https://github.com/mihonapp/mihon/pull/1885)) | ||||
| - Fix next chapter button occasionally jumping to the last page of the current chapter ([@perokhe](https://github.com/perokhe)) ([#1920](https://github.com/mihonapp/mihon/pull/1920)) | ||||
|   - Fix page number not appearing when opening chapter ([@perokhe](https://github.com/perokhe)) ([#1936](https://github.com/mihonapp/mihon/pull/1936)) | ||||
| - Fix backup sharing from notifications not working when app is in background ([@JaymanR](https://github.com/JaymanR))([#1929](https://github.com/mihonapp/mihon/pull/1929)) | ||||
| - Fix mark existing duplicate read chapters as read option not working in some cases ([@AntsyLich](https://github.com/AntsyLich)) ([#1944](https://github.com/mihonapp/mihon/pull/1944)) | ||||
| - Fix app bar action tooltips blocking clicks ([@Bartuzen](https://github.com/Bartuzen)) ([#1928](https://github.com/mihonapp/mihon/pull/1928)) | ||||
| - Fix unintended app permissions due to Firebase misconfiguration ([@AntsyLich](https://github.com/AntsyLich)) ([#1960](https://github.com/mihonapp/mihon/pull/1960)) | ||||
| - Fix navigation issue after migrating a duplicated entry from History tab ([@cuong-tran](https://github.com/cuong-tran)) ([#1980](https://github.com/mihonapp/mihon/pull/1980)) | ||||
| - Fix duplicate requests in WebView due to empty reasonPhrase ([@AwkwardPeak7](https://github.com/AwkwardPeak7)) ([#2003](https://github.com/mihonapp/mihon/pull/2003)) | ||||
| - Fix content under source browse screen top appbar is interactable ([@AntsyLich](https://github.com/AntsyLich)) ([#2026](https://github.com/mihonapp/mihon/pull/2026)) | ||||
| - Fix crash when trying use source sort filter without a pre-selection ([@AntsyLich](https://github.com/AntsyLich)) ([#2036](https://github.com/mihonapp/mihon/pull/2036)) | ||||
| - Fix empty layout not appearing in browse source screen in some cases ([@NarwhalHorns](https://github.com/NarwhalHorns)) ([#2043](https://github.com/mihonapp/mihon/pull/2043)) | ||||
| - Fix Pill not following the local text style ([@AntsyLich](https://github.com/AntsyLich)) ([`f8cb506`](https://github.com/mihonapp/mihon/commit/f8cb506)) | ||||
| - Fix downloader stopping after failing to create download directory of a manga ([@AntsyLich](https://github.com/AntsyLich)) ([#2068](https://github.com/mihonapp/mihon/pull/2068)) | ||||
| - Fix pressing `Enter` while searching also triggering navigation back on physical keyboards ([@AwkwardPeak7](https://github.com/AwkwardPeak7)) ([#2077](https://github.com/mihonapp/mihon/pull/2077)) | ||||
| - Ensure app waits for Cloudflare challenge to complete before continuing ([@AwkwardPeak7](https://github.com/AwkwardPeak7)) ([#2200](https://github.com/mihonapp/mihon/pull/2200)) | ||||
|  | ||||
| ### Removed | ||||
| - Remove Okhttp networking from WebView Screen ([@AwkwardPeak7](https://github.com/AwkwardPeak7)) ([#2020](https://github.com/mihonapp/mihon/pull/2020)) | ||||
|  | ||||
| ## [v0.18.0] - 2025-03-20 | ||||
| ### Added | ||||
| - Add option to always decode long strip images with SSIV ([@AntsyLich](https://github.com/AntsyLich)) ([`c5655e8`](https://github.com/mihonapp/mihon/commit/c5655e8803bc32d0931657f0b7bc6afeab70feaf)) | ||||
|   - Change option label ([@AntsyLich](https://github.com/AntsyLich)) ([#1835](https://github.com/mihonapp/mihon/pull/1835)) | ||||
| - Added option to enable incognito per extension ([@sdaqo](https://github.com/sdaqo), [@AntsyLich](https://github.com/AntsyLich)) ([#157](https://github.com/mihonapp/mihon/pull/157)) | ||||
| - Add button to favorite manga from history screen ([@Animeboynz](https://github.com/Animeboynz)) ([#1733](https://github.com/mihonapp/mihon/pull/1733)) | ||||
| - Add Monochrome theme (made with e-ink displays in mind) ([@MajorTanya](https://github.com/MajorTanya)) ([#1752](https://github.com/mihonapp/mihon/pull/1752)) | ||||
| - Support for private tracking with AniList and Bangumi ([@NarwhalHorns](https://github.com/NarwhalHorns)) ([#1736](https://github.com/mihonapp/mihon/pull/1736)) | ||||
|   - Add private tracking support for Kitsu ([@MajorTanya](https://github.com/MajorTanya)) ([#1774](https://github.com/mihonapp/mihon/pull/1774)) | ||||
| - Add option to export minimal library information to a CSV file ([@Animeboynz](https://github.com/Animeboynz), [@AntsyLich](https://github.com/AntsyLich)) ([#1161](https://github.com/mihonapp/mihon/pull/1161)) | ||||
| - Add back support for drag-and-drop category reordering ([@cuong-tran](https://github.com/cuong-tran)) ([#1427](https://github.com/mihonapp/mihon/pull/1427)) | ||||
| - Add option to mark duplicate read chapters as read after library update or while reading ([@AntsyLich](https://github.com/AntsyLich)) ([#1785](https://github.com/mihonapp/mihon/pull/1785), [#1791](https://github.com/mihonapp/mihon/pull/1791), [#1870](https://github.com/mihonapp/mihon/pull/1870)) | ||||
| - Display staff information on Anilist tracker search results ([@NarwhalHorns](https://github.com/NarwhalHorns)) ([#1810](https://github.com/mihonapp/mihon/pull/1810)) | ||||
| - Add `id:` prefix search to library to search by internal DB ID ([@MajorTanya](https://github.com/MajorTanya)) ([#1856](https://github.com/mihonapp/mihon/pull/1856)) | ||||
| - Add back option to disable unread chapter badge in library ([@AntsyLich](https://github.com/AntsyLich)) ([#1871](https://github.com/mihonapp/mihon/pull/1871)) | ||||
|  | ||||
| ### Changed | ||||
| - Sliders UI ([@AntsyLich](https://github.com/AntsyLich)) ([#1840](https://github.com/mihonapp/mihon/pull/1840)) | ||||
| - Apply "Downloaded only" filter to all entries regardless of favourite status ([@NGB-Was-Taken](https://github.com/NGB-Was-Taken)) ([#1603](https://github.com/mihonapp/mihon/pull/1603)) | ||||
| - Ignore hidden files/folders for Local Source chapter list ([@BrutuZ](https://github.com/BrutuZ)) ([#1763](https://github.com/mihonapp/mihon/pull/1763)) | ||||
| - Migrate to newer Bangumi API ([@MajorTanya](https://github.com/MajorTanya)) ([#1748](https://github.com/mihonapp/mihon/pull/1748)) | ||||
|   - Now showing manga starting dates in search | ||||
|   - Reduced request load by 2-4x in certain situations | ||||
| - Bump default user agent ([@AntsyLich](https://github.com/AntsyLich)) ([#1833](https://github.com/mihonapp/mihon/pull/1833)) | ||||
| - Changed the label of chapter swipe settings and renamed the group to "Behavior" ([@AntsyLich](https://github.com/AntsyLich)) ([#1870](https://github.com/mihonapp/mihon/pull/1870)) | ||||
|  | ||||
| ### Fixed | ||||
| - Fix MAL `main_picture` nullability breaking search if a result doesn't have a cover set ([@MajorTanya](https://github.com/MajorTanya)) ([#1618](https://github.com/mihonapp/mihon/pull/1618)) | ||||
| - Fix Bangumi and MAL tracking 401 errors due to Mihon sending expired credentials ([@MajorTanya](https://github.com/MajorTanya)) ([#1681](https://github.com/mihonapp/mihon/pull/1681), [#1682](https://github.com/mihonapp/mihon/pull/1682)) | ||||
| - Fix certain Infinix, Xiaomi devices being unable to use any "Open link in browser" actions, including tracker setup ([@MajorTanya](https://github.com/MajorTanya)) ([#1684](https://github.com/mihonapp/mihon/pull/1684)) ([#1776](https://github.com/mihonapp/mihon/pull/1776)) | ||||
| - Fix App's preferences referencing deleted categories ([@cuong-tran](https://github.com/cuong-tran)) ([#1734](https://github.com/mihonapp/mihon/pull/1734)) | ||||
| - Fix backup/restore of category related preferences ([@cuong-tran](https://github.com/cuong-tran)) ([#1726](https://github.com/mihonapp/mihon/pull/1726)) | ||||
| - Fix WebView sending app's package name in `X-Requested-With` header, which led to sources blocking access ([@AwkwardPeak7](https://github.com/AwkwardPeak7)) ([#1812](https://github.com/mihonapp/mihon/pull/1812)) | ||||
| - Fix an issue where tracker reading progress is changed to a lower value ([@Animeboynz](https://github.com/Animeboynz)) ([#1795](https://github.com/mihonapp/mihon/pull/1795)) | ||||
| - Attempt to fix crash when migrating or removing entries from library ([@FlaminSarge](https://github.com/FlaminSarge)) ([#1828](https://github.com/mihonapp/mihon/pull/1828)) | ||||
|  | ||||
| ### Removed | ||||
| - Remove alphabetical category sort option ([@AntsyLich](https://github.com/AntsyLich)) ([#1781](https://github.com/mihonapp/mihon/pull/1781)) | ||||
|  | ||||
| ### Other | ||||
| - Add zoned "Current time" to debug info and include year & timezone in logcat output ([@MajorTanya](https://github.com/MajorTanya)) ([#1672](https://github.com/mihonapp/mihon/pull/1672)) | ||||
| - Add application package ID to debug info ([@MajorTanya](https://github.com/MajorTanya)) ([#1847](https://github.com/mihonapp/mihon/pull/1847)) | ||||
|  | ||||
| ## [v0.17.1] - 2024-12-06 | ||||
| ### Changed | ||||
| - Bump default user agent ([@AntsyLich](https://github.com/AntsyLich)) ([`76dcf90`](https://github.com/mihonapp/mihon/commit/76dcf903403d565056f44c66d965c1ea8affffc3)) | ||||
|  | ||||
| ### Improved | ||||
| - Bangumi search now shows the score and summary of a search result ([@MajorTanya](https://github.com/MajorTanya)) ([#1396](https://github.com/mihonapp/mihon/pull/1396)) | ||||
| - Extension repo URLs are now auto-formatted ([@AntsyLich](https://github.com/AntsyLich), [@MajorTanya](https://github.com/MajorTanya)) ([`22d8aad`](https://github.com/mihonapp/mihon/commit/22d8aad598bea8f00f2831779e45a6645392ca0f)) | ||||
|  | ||||
| ### Fixed | ||||
| - Fix "currentTab was used multiple times" ([@AntsyLich](https://github.com/AntsyLich)) ([`371c143`](https://github.com/mihonapp/mihon/commit/371c1432e218f6dcf129f05405dceb2cd351c647)) | ||||
| - Fix a rare crash when invoking "Mark previous as read" action ([@AntsyLich](https://github.com/AntsyLich)) ([`f508d10`](https://github.com/mihonapp/mihon/commit/f508d10ad13560d7316df8642bc93fe66c05b9a8)) | ||||
| - Fix long strip images not loading in some old devices ([@AntsyLich](https://github.com/AntsyLich)) ([`06efc3b`](https://github.com/mihonapp/mihon/commit/06efc3b25c5af51f42448af27a269ee459d9093d)) | ||||
|   - Switch to hardware bitmap in reader only if device can handle it ([@AntsyLich](https://github.com/AntsyLich)) ([`e6d96bd`](https://github.com/mihonapp/mihon/commit/e6d96bd348ea5d18a005d6465222ad5f5123103e)) | ||||
|   - Add option to lower the threshold for hardware bitmaps ([@AntsyLich](https://github.com/AntsyLich)) ([`dcddac5`](https://github.com/mihonapp/mihon/commit/dcddac5daaff3ec89c8507c35dc13d345ffdb6d7)) | ||||
|     - Improve hardware bitmap threshold option ([@AntsyLich](https://github.com/AntsyLich)) ([`d6dfd24`](https://github.com/mihonapp/mihon/commit/d6dfd24548eaa05a8c3e478068fe2e08f2ee4473)) | ||||
|   - Always use software bitmap on certain devices ([@MajorTanya](https://github.com/MajorTanya)) ([#1543](https://github.com/mihonapp/mihon/pull/1543)) | ||||
| - Fix crash after removing last category while it's active in library ([@cuong-tran](https://github.com/cuong-tran)) ([#1450](https://github.com/mihonapp/mihon/pull/1450)) | ||||
| - Fix reader transition color scheme in auto background mode ([@cuong-tran](https://github.com/cuong-tran)) ([#1487](https://github.com/mihonapp/mihon/pull/1487)) | ||||
| - Fix app update error notification disappearing ([@cuong-tran](https://github.com/cuong-tran)) ([#1476](https://github.com/mihonapp/mihon/pull/1476)) | ||||
| - Fix browser not opening in some cases in Honor devices ([@AntsyLich](https://github.com/AntsyLich), [@MajorTanya](https://github.com/MajorTanya)) ([#1520](https://github.com/mihonapp/mihon/pull/1520)) | ||||
|  | ||||
| ## [v0.17.0] - 2024-10-26 | ||||
| ### 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)) | ||||
|   - Only show upcoming updates in the future ([@sirlag](https://github.com/sirlag)) ([#606](https://github.com/mihonapp/mihon/pull/606)) | ||||
|   - Add Quantity Badge to Upcoming Screen ([@Animeboynz](https://github.com/Animeboynz), [@AntsyLich](https://github.com/AntsyLich)) ([#1250](https://github.com/mihonapp/mihon/pull/1250)) | ||||
| - 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)) ([#949](https://github.com/mihonapp/mihon/pull/949)) | ||||
| - 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)) | ||||
|   - Reorder reader menu overflow items ([@AntsyLich](https://github.com/AntsyLich)) ([`788235f`](https://github.com/mihonapp/mihon/commit/788235feeca241228eac0561339dd07b5ea0b77d)) | ||||
| - Option to skip downloading duplicate read chapters ([@shabnix](https://github.com/shabnix)) ([#1125](https://github.com/mihonapp/mihon/pull/1125)) | ||||
| - Add confirmation dialog when adding repo via URI ([@Animeboynz](https://github.com/Animeboynz)) ([#1158](https://github.com/mihonapp/mihon/pull/1158)) | ||||
| - Add "show entry" action to download notifications ([@mm12](https://github.com/mm12), [@AntsyLich](https://github.com/AntsyLich)) ([#1159](https://github.com/mihonapp/mihon/pull/1159)) | ||||
| - Option to update trackers when chapter marked as read ([@Animeboynz](https://github.com/Animeboynz), [@AntsyLich](https://github.com/AntsyLich)) ([#1177](https://github.com/mihonapp/mihon/pull/1177), [#1365](https://github.com/mihonapp/mihon/pull/1365), [#1374](https://github.com/mihonapp/mihon/pull/1374)) | ||||
| - Toast to restart app when User-Agent is changed ([@NGB-Was-Taken](https://github.com/NGB-Was-Taken)) ([#1204](https://github.com/mihonapp/mihon/pull/1204)) | ||||
| - Added more profile compilation status (p) ([`c8bb78d`](https://github.com/mihonapp/mihon/commit/c8bb78d91afc2824baaca999f0095559c49d1306)) | ||||
| - Add option to opt out of Analytics and Crashlytics ([@Animeboynz](https://github.com/Animeboynz)) ([#1237](https://github.com/mihonapp/mihon/pull/1237)) | ||||
|   - Rework Firebase setup ([@AntsyLich](https://github.com/AntsyLich)) ([`15e3f28`](https://github.com/mihonapp/mihon/commit/15e3f28aa36bec3c31f212c572ab57ce960cc862)) | ||||
| - Added random library sort ([@jackhamilton](https://github.com/jackhamilton)) ([#1317](https://github.com/mihonapp/mihon/pull/1317)) | ||||
|   - Make sure random library sort is at the bottom ([@AntsyLich](https://github.com/AntsyLich)) ([`2e2c8d3`](https://github.com/mihonapp/mihon/commit/2e2c8d36c1e23bf274c7c19f1242e14b0c7afbc1)) | ||||
| - Confirmation dialog when removing privately installed extensions ([@Animeboynz](https://github.com/Animeboynz), [@AntsyLich](https://github.com/AntsyLich)) ([#1320](https://github.com/mihonapp/mihon/pull/1320)) | ||||
| - Option to backup non-library read entries ([@Animeboynz](https://github.com/Animeboynz), [@jobobby04](https://github.com/jobobby04), [@AntsyLich](https://github.com/AntsyLich)) ([#1324](https://github.com/mihonapp/mihon/pull/1324)) | ||||
|  | ||||
| ### Changed | ||||
| - Read archive files from memory instead of temporarily extracting to internal storage ([@FooIbar](https://github.com/FooIbar)) ([#326](https://github.com/mihonapp/mihon/pull/326)) | ||||
|   - Fix dual page split ([@FooIbar](https://github.com/FooIbar)) ([#485](https://github.com/mihonapp/mihon/pull/485)) | ||||
| - Bump default user agent ([@AntsyLich](https://github.com/AntsyLich)) ([`8160b47`](https://github.com/mihonapp/mihon/commit/8160b47ff5fbbd9b32caeb462b5be881fabd3449)) | ||||
| - 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 | ||||
|   - Store extension repo details from `repo.json` in database ([@sirlag](https://github.com/sirlag)) ([#506](https://github.com/mihonapp/mihon/pull/506)) | ||||
|     - Fix extension repo migration not triggering ([@AntsyLich](https://github.com/AntsyLich)) ([`9672ea8`](https://github.com/mihonapp/mihon/commit/9672ea8b1b06f464800e310c96e060ead182f7ca)) | ||||
|     - Refactor the ExtensionRepoService to use DTOs ([@MajorTanya](https://github.com/MajorTanya)) ([#573](https://github.com/mihonapp/mihon/pull/573)) | ||||
|     - Fix extension repo name is used to construct URL instead of baseUrl ([@MajorTanya](https://github.com/MajorTanya)) ([#572](https://github.com/mihonapp/mihon/pull/572)) | ||||
|     - Fix crash with `TypeReference` issue when creating extension repo ([@AntsyLich](https://github.com/AntsyLich)) ([#574](https://github.com/mihonapp/mihon/pull/574), [`e020ae5`](https://github.com/mihonapp/mihon/commit/e020ae5ed558e80742ef0ad8bfa0f69af0959d5a)) | ||||
|       - Fix mishap in [`e020ae5`](https://github.com/mihonapp/mihon/commit/e020ae5ed558e80742ef0ad8bfa0f69af0959d5a) ([@AntsyLich](https://github.com/AntsyLich)) ([`6965e59`](https://github.com/mihonapp/mihon/commit/6965e59a643c67a2bf81b3c69ec70268e5da5797)) | ||||
|     - Backup and Restore ([@Animeboynz](https://github.com/Animeboynz)) ([#1057](https://github.com/mihonapp/mihon/pull/1057)) | ||||
|   - Trust extension by repo ([@AntsyLich](https://github.com/AntsyLich)) ([#570](https://github.com/mihonapp/mihon/pull/570)) | ||||
| - From M2 ripple to M3 ([@FooIbar](https://github.com/FooIbar)) ([#675](https://github.com/mihonapp/mihon/pull/675)) | ||||
| - Increased continue reading button size ([@AntsyLich](https://github.com/AntsyLich), [@Animeboynz](https://github.com/Animeboynz)) ([`e17f70f`](https://github.com/mihonapp/mihon/commit/e17f70f7226ea031fc1f962c9dfea3e404ba53ad)) | ||||
| - Global search "Has result" choice is now sticky ([@AntsyLich](https://github.com/AntsyLich)) ([`5a61ca5`](https://github.com/mihonapp/mihon/commit/5a61ca5535fe0d9e8e7bcb9e665ba2f9cb0cf649)) | ||||
| - 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)) | ||||
| - Rename backup restore error log file ([@AntsyLich](https://github.com/AntsyLich)) ([`2858ef8`](https://github.com/mihonapp/mihon/commit/2858ef835fec8d7278b1d0cad1b5664104d1e4b0)) | ||||
| - Keyboard type in add extension repo dialog ([@xbjfk](https://github.com/xbjfk)) ([#764](https://github.com/mihonapp/mihon/pull/764)) | ||||
| - Adjust collapse/open animation on manga description ([@AntsyLich](https://github.com/AntsyLich), [@ivaniskandar](https://github.com/ivaniskandar)) ([`1c16fc7`](https://github.com/mihonapp/mihon/commit/1c16fc79c2ac4c4be30308fed84ffb371dab5902)) | ||||
| - 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)) | ||||
| - Hide keyboard when a Tracker SearchResultItem is clicked ([@Animeboynz](https://github.com/Animeboynz)) ([#1168](https://github.com/mihonapp/mihon/pull/1168)) | ||||
| - Enable 'Split Tall Images' by default ([@Smol-Ame](https://github.com/Smol-Ame)) ([#1185](https://github.com/mihonapp/mihon/pull/1185)) | ||||
| - Ignore "intent://" urls on webview ([@bapeey](https://github.com/bapeey)) ([#1193](https://github.com/mihonapp/mihon/pull/1193)) | ||||
| - Make reader chapter navigator slightly wider on small screens (p) ([#1202](https://github.com/mihonapp/mihon/pull/1202)) | ||||
| - Re-enable fetching chapters list for entries with licenced status ([@Animeboynz](https://github.com/Animeboynz)) ([#1230](https://github.com/mihonapp/mihon/pull/1230)) | ||||
| - Change casing for Extention Repos String ([@Animeboynz](https://github.com/Animeboynz)) ([#1248](https://github.com/mihonapp/mihon/pull/1248)) | ||||
| - Retain remote last chapter read if it's higher than the local one for EnhancedTracker ([@brewkunz](https://github.com/brewkunz)) ([#1301](https://github.com/mihonapp/mihon/pull/1301)) | ||||
| - Adjust expandable fab animation (p) ([`eb6092b`](https://github.com/mihonapp/mihon/commit/eb6092bd0cfa09694985a8bafdd8bbf2815190a1)) | ||||
| - "Invalidate downloads index" to "Reindex downloads" ([@AntsyLich](https://github.com/AntsyLich)) ([`d2afbfe`](https://github.com/mihonapp/mihon/commit/d2afbfe4ede283076aae40633c79c3f90b4390e7)) | ||||
|  | ||||
| ### Improved | ||||
| - Reader performance | ||||
|   - Avoid unnecessary copying when processing reader image ([@FooIbar](https://github.com/FooIbar)) ([#691](https://github.com/mihonapp/mihon/pull/691)) | ||||
|   - Significantly improve performance when loading extremely long images in long strip mode ([@FooIbar](https://github.com/FooIbar)) ([#692](https://github.com/mihonapp/mihon/pull/692)) | ||||
|   - Use `Bitmap.Config.HARDWARE` if possible to improve image loading speed ([@wwww-wwww](https://github.com/wwww-wwww)) ([#687](https://github.com/mihonapp/mihon/pull/687)) | ||||
|   - Improve preloading in long strip mode ([@FooIbar](https://github.com/FooIbar)) ([#1076](https://github.com/mihonapp/mihon/pull/1076)) | ||||
| ### 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)) | ||||
|  | ||||
| ### Removed | ||||
| - Legacy download folder names no longer supported ([@AntsyLich](https://github.com/AntsyLich)) ([`e55e5f6`](https://github.com/mihonapp/mihon/commit/e55e5f6f64f872475d370d6ce0c186e2601776e4)) | ||||
| - Remove legacy broken source and history backup ([@AntsyLich](https://github.com/AntsyLich)) ([`518abf0`](https://github.com/mihonapp/mihon/commit/518abf032ccb9bb45d197927be2a5faca4167d29)) | ||||
| - Remove more unnecessary permissions from Firebase dependency ([@AntsyLich](https://github.com/AntsyLich)) ([`02af9b1`](https://github.com/mihonapp/mihon/commit/02af9b1acf9f590d29560bc3fc90d206e8e6e1af)) | ||||
|   - Fix mishap in `02af9b1` ([@AntsyLich](https://github.com/AntsyLich)) ([`f22767d`](https://github.com/mihonapp/mihon/commit/f22767d863a0fa001f93f24092cd5ade87350502)) | ||||
|  | ||||
| ### Fixed | ||||
| - Extracting `ComicInfo.xml` from local source archives ([@FooIbar](https://github.com/FooIbar)) ([#325](https://github.com/mihonapp/mihon/pull/325)) | ||||
| - 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)) | ||||
| - Fix reader page image not being decoded until it's visible ([@FooIbar](https://github.com/FooIbar)) ([#563](https://github.com/mihonapp/mihon/pull/563)) | ||||
| - Reader chapter progress slider visuals ([@FooIbar](https://github.com/FooIbar)) ([#674](https://github.com/mihonapp/mihon/pull/674)) | ||||
| - 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), [9a34ace](https://github.com/mihonapp/mihon/commit/9a34ace09c66274e6c2b3f9446058a0fa99d4bd0)) | ||||
| - 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)) | ||||
| - Fix unexpected skips in strong skipping mode ([@FooIbar](https://github.com/FooIbar)) ([#940](https://github.com/mihonapp/mihon/pull/940)) | ||||
| - 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)) | ||||
|   - Fix login prompts despite being logged in to trackers in Manga screen ([@AntsyLich](https://github.com/AntsyLich)) ([`cbcd8bd`](https://github.com/mihonapp/mihon/commit/cbcd8bd6682023f728568f2b44da26124618aed7)) | ||||
| - 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)) | ||||
| - Fix item disappearing when fast scrolling ([@cuong-tran](https://github.com/cuong-tran)) ([#1035](https://github.com/mihonapp/mihon/pull/1035)) | ||||
| - Library is backed up while being disabled ([@AntsyLich](https://github.com/AntsyLich)) ([`56fb4f6`](https://github.com/mihonapp/mihon/commit/56fb4f62a152e87a71892aa68c78cac51a2c8596)) | ||||
| - Crash on list with only sticky header ([@cuong-tran](https://github.com/cuong-tran)) ([#1083](https://github.com/mihonapp/mihon/pull/1083)) | ||||
| - 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)) | ||||
| - Manga next update calculation when considering custom fetch interval ([@cuong-tran](https://github.com/cuong-tran)) ([#1206](https://github.com/mihonapp/mihon/pull/1206)) | ||||
| - WheelPicker Manual Input ([@Animeboynz](https://github.com/Animeboynz)) ([#1209](https://github.com/mihonapp/mihon/pull/1209)) | ||||
| - EnhancedTracker not auto binding when adding manga to library ([@brewkunz](https://github.com/brewkunz)) ([#1298](https://github.com/mihonapp/mihon/pull/1298)) | ||||
| - Step count in settings slider ([@abdurisaq](https://github.com/abdurisaq)) ([#1356](https://github.com/mihonapp/mihon/pull/1356)) | ||||
| - Freezing in some screens due to blocking call ([@cuong-tran](https://github.com/cuong-tran)) ([#1364](https://github.com/mihonapp/mihon/pull/1364)) | ||||
| - Crash when removing non-existent tracked entry from tracker ([@cuong-tran](https://github.com/cuong-tran)) ([#1380](https://github.com/mihonapp/mihon/pull/1380)) | ||||
|  | ||||
| ### Other | ||||
| - Code cleanup | ||||
|   - Minor refactor of theming when expressions ([@MajorTanya](https://github.com/MajorTanya)) ([#396](https://github.com/mihonapp/mihon/pull/396)) | ||||
|   - Inside `WorkerInfoScreen` ([@AntsyLich](https://github.com/AntsyLich)) ([`5aec8f8`](https://github.com/mihonapp/mihon/commit/5aec8f8018236a38106483da08f9cbc28261ac9b)) | ||||
|   - Inside `ChapterDownloadIndicator`, `MangaChapterListItem` ([@AntsyLich](https://github.com/AntsyLich)) ([`b7e091d`](https://github.com/mihonapp/mihon/commit/b7e091d5d039e00cababc7daf555280df6cf9c03)) | ||||
|   - MangaCoverFetcher ([@ivaniskandar](https://github.com/ivaniskandar)) ([`1365695`](https://github.com/mihonapp/mihon/commit/13656959ae0606736f6ca9eb62699dc23e467c2f)) | ||||
| - Cleanup `LibraryScreenModel` `LibraryMap.applySort` and some more ([@AntsyLich](https://github.com/AntsyLich)) ([`2beb89d`](https://github.com/mihonapp/mihon/commit/2beb89d53163a6d288f8acdebe0f5d26fea8ab3e)) | ||||
| - Address `overridePendingTransition` deprecation ([@MajorTanya](https://github.com/MajorTanya)) ([#410](https://github.com/mihonapp/mihon/pull/410)) | ||||
| - Prioritize extension classes and files over app ([@beer-psi](https://github.com/beer-psi)) ([#433](https://github.com/mihonapp/mihon/pull/433)) | ||||
| - Use compose pager implementation ([@ivaniskandar](https://github.com/ivaniskandar)) ([`84984ef`](https://github.com/mihonapp/mihon/commit/84984ef7e1d7242924120cd2f171cb9dd75bc916)) | ||||
| - Switch to coil3 from coil2 ([@ivaniskandar](https://github.com/ivaniskandar)) ([`f72b6e4`](https://github.com/mihonapp/mihon/commit/f72b6e4d7c1f2f93d705402e4d80c94160bef54d)) | ||||
|   - Fix GIF not playing ([@jobobby04](https://github.com/jobobby04)) ([`59bedb3`](https://github.com/mihonapp/mihon/commit/59bedb33ff59ad5db1df2e93567a2266fb63eacc)) | ||||
| - Accommodate db for sync support ([@kaiserbh](https://github.com/kaiserbh)) ([#450](https://github.com/mihonapp/mihon/pull/450)) | ||||
| - Fix webtoon last visible item position calculation ([@FooIbar](https://github.com/FooIbar)) ([#562](https://github.com/mihonapp/mihon/pull/562)) | ||||
| - Migrate from `com.google.accompanist:accompanist-webview` to `io.github.kevinnzou:compose-webview` ([@sirlag](https://github.com/sirlag)) ([#569](https://github.com/mihonapp/mihon/pull/569)) | ||||
| - Rewrite migrations ([@ghostbear](https://github.com/ghostbear)) ([#577](https://github.com/mihonapp/mihon/pull/577)) | ||||
|   - Further improve migration ([@ghostbear](https://github.com/ghostbear)) ([#588](https://github.com/mihonapp/mihon/pull/588)) | ||||
|   - Fix migrations not running ([@ghostbear](https://github.com/ghostbear)) ([#604](https://github.com/mihonapp/mihon/pull/604)) | ||||
|   - Fix MigratorTest after updating to Kotlin 2 ([@cuong-tran](https://github.com/cuong-tran)) ([#896](https://github.com/mihonapp/mihon/pull/896)) | ||||
|   - Add MigratorTest to build script ([@cuong-tran](https://github.com/cuong-tran)) ([#896](https://github.com/mihonapp/mihon/pull/896)) | ||||
|   - Fix UI freeze after migration ([@AntsyLich](https://github.com/AntsyLich)) ([`3f1d28c`](https://github.com/mihonapp/mihon/commit/3f1d28c3833e6b868152149ed02b3fb8c54eccef)) | ||||
|   - Fix some migrations never running ([@MajorTanya](https://github.com/MajorTanya), [@AntsyLich](https://github.com/AntsyLich)) ([#1030](https://github.com/mihonapp/mihon/pull/1030)) | ||||
| - Add ProGuard rule to keep `mihon` namespace classes ([@MajorTanya](https://github.com/MajorTanya)) ([#605](https://github.com/mihonapp/mihon/pull/605)) | ||||
| - Use gradle plugins to share build configuration instead of subprojects ([@AntsyLich](https://github.com/AntsyLich)) ([`e448e40`](https://github.com/mihonapp/mihon/commit/e448e40406e8d9916120a278e42829a6f1b25a7a)) | ||||
| - Remove dependency on compose material 2 components ([@AntsyLich](https://github.com/AntsyLich)) ([`fb94230`](https://github.com/mihonapp/mihon/commit/fb9423028eb017c110cb805f2d0601e5b02e50f9)) | ||||
| - Upload PR build artifacts to GitHub ([@FooIbar](https://github.com/FooIbar)) ([#941](https://github.com/mihonapp/mihon/pull/941)) | ||||
| - Refactor archive support with libarchive ([@FooIbar](https://github.com/FooIbar)) ([#949](https://github.com/mihonapp/mihon/pull/949)) | ||||
|   - Add safeguard to prevent ArchiveInputStream from being closed twice ([@null2264](https://github.com/null2264)) ([#967](https://github.com/mihonapp/mihon/pull/967)) | ||||
|   - Move archive related code to :core:archive ([@AntsyLich](https://github.com/AntsyLich)) ([`bd7b354`](https://github.com/mihonapp/mihon/commit/bd7b35419861df6d426d6ec0a188391910d0f615)) | ||||
| - Replace detekt with ktlint via spotless ([@AntsyLich](https://github.com/AntsyLich)) ([#1130](https://github.com/mihonapp/mihon/pull/1130), [#1136](https://github.com/mihonapp/mihon/pull/1136), [#1138](https://github.com/mihonapp/mihon/pull/1138)) | ||||
|   - Refrain from running spotless on weblate files ([@AntsyLich](https://github.com/AntsyLich)) ([`32d2c2a`](https://github.com/mihonapp/mihon/commit/32d2c2ac1bc224cbda2f09a4023d7d120ea0e954)) | ||||
| - Use feature flags in compose compiler plugin ([@AntsyLich](https://github.com/AntsyLich)) ([`8f9a325`](https://github.com/mihonapp/mihon/commit/8f9a325895bb7b94c2ec92dd969094fc30b3b5e2)) | ||||
| - PagerPageHolder: lazy init loading indicator ([@AntsyLich](https://github.com/AntsyLich), [@ivaniskandar](https://github.com/ivaniskandar)) ([`a45eb5e`](https://github.com/mihonapp/mihon/commit/a45eb5e5288159dbbbbb5f92140ce0dd32a8f3ab)) | ||||
| - Collect MangaScreen state with lifecycle ([@AntsyLich](https://github.com/AntsyLich), [@ivaniskandar](https://github.com/ivaniskandar)) ([`03eb756`](https://github.com/mihonapp/mihon/commit/03eb756ecba0692d88d3a76254afc4c157fa225b)) | ||||
| - Add stable marker to Manga data class ([@AntsyLich](https://github.com/AntsyLich), [@ivaniskandar](https://github.com/ivaniskandar)) ([`03eb756`](https://github.com/mihonapp/mihon/commit/03eb756ecba0692d88d3a76254afc4c157fa225b)) | ||||
| - Use DTOs to parse tracking API responses ([@MajorTanya](https://github.com/MajorTanya)) ([#1103](https://github.com/mihonapp/mihon/pull/1103)) | ||||
|   - Fix Kitsu ratingTwenty being typed as String ([@MajorTanya](https://github.com/MajorTanya)) ([#1191](https://github.com/mihonapp/mihon/pull/1191)) | ||||
|   - Fix Kitsu `synopsis` nullability ([@MajorTanya](https://github.com/MajorTanya)) ([#1233](https://github.com/mihonapp/mihon/pull/1233)) | ||||
|   - Fix AniList `ALSearchItem.status` nullibility ([@Secozzi](https://github.com/Secozzi)) ([#1297](https://github.com/mihonapp/mihon/pull/1297)) | ||||
| - Migrate some classpaths to gradle plugins ([@AntsyLich](https://github.com/AntsyLich)) ([`fc1c804`](https://github.com/mihonapp/mihon/commit/fc1c804bfda1d76c0399bbb6214e75b3def951cc)) | ||||
| - Add crashlytics to standard builds ([@AntsyLich](https://github.com/AntsyLich)) ([`3c611b9`](https://github.com/mihonapp/mihon/commit/3c611b95fb79e5ac972019b76c7b24f46a3087fd)) | ||||
| - Switch to stable compose ([@AntsyLich](https://github.com/AntsyLich)) ([`2baffa6`](https://github.com/mihonapp/mihon/commit/2baffa62cade1abd978d5fd03151b47fc87fd31e)) | ||||
| - Switch from inorichi injekt to kohesive Injekt ([@AntsyLich](https://github.com/AntsyLich)) ([#1205](https://github.com/mihonapp/mihon/pull/1205)) | ||||
|   - Use custom injekt register with inorichi patch ([@AntsyLich](https://github.com/AntsyLich)) ([`83fd474`](https://github.com/mihonapp/mihon/commit/83fd4746eda1b99f35292b0c2211e606a421b3eb)) | ||||
| - Use TextFieldState in BasicTextField where applicable (p) ([#1201](https://github.com/mihonapp/mihon/pull/1201)) | ||||
| - Bump NDK version ([@AntsyLich](https://github.com/AntsyLich)) ([#1203](https://github.com/mihonapp/mihon/pull/1203)) | ||||
| - Move firebase permission removal to standard flavor ([@AntsyLich](https://github.com/AntsyLich)) ([`be671b4`](https://github.com/mihonapp/mihon/commit/be671b42cefd70180644e01bb065a18cb7701bf9)) | ||||
| - Adjust distinct checker in WidgetManager and run on default dispatcher (p) ([`9b8ab6a`](https://github.com/mihonapp/mihon/commit/9b8ab6acc25a5f99c9c5eebf9cc250975931c57c)) | ||||
| - Update resources exclusion rules (p) ([`481cfed`](https://github.com/mihonapp/mihon/commit/481cfedf08576cecfbb35616837bd8f627d8f959)) | ||||
| - Bump compile sdk to 35 (p) ([`37419cd`](https://github.com/mihonapp/mihon/commit/37419cdc26c2b5c4f8583fc2ba439b08fab42856)) | ||||
| - ChapterNavigator: dispatch page change only when needed (p) ([`f84d9a0`](https://github.com/mihonapp/mihon/commit/f84d9a08b4af768b1e9920c43cc445c86f5427fc)) | ||||
| - Remove usage of deprecated accompanist SystemUiController ([@AntsyLich](https://github.com/AntsyLich)) ([`2ba3f06`](https://github.com/mihonapp/mihon/commit/2ba3f0612c08c7021fed2f6d96cd538da2f34a13)) | ||||
| - Run PR check when base strings are changed ([@AntsyLich](https://github.com/AntsyLich)) ([`4051f18`](https://github.com/mihonapp/mihon/commit/4051f180a2e36e8a2cde6c55f0bea7952fdc4704)) | ||||
|   - Fix PR build check ([@AntsyLich](https://github.com/AntsyLich)) ([`9503082`](https://github.com/mihonapp/mihon/commit/9503082d44b5bd868ee1bfc42741dc978d1d9047)) | ||||
| - Cleanup .gitignore files ([@AntsyLich](https://github.com/AntsyLich)) ([`afa5002`](https://github.com/mihonapp/mihon/commit/afa50029882655af8d5eea40aed7644fce4564d8)) | ||||
| - Pass uncaught exception to default handler in GlobalExceptionHandler (so it's reported to crashlytics) ([@AntsyLich](https://github.com/AntsyLich)) ([`f3a2f56`](https://github.com/mihonapp/mihon/commit/f3a2f566c8a09ab862758ae69b43da2a2cd8f1db)) | ||||
|  | ||||
| ## [v0.16.5] - 2024-04-09 | ||||
| ### Added | ||||
| - Relative date for up to a week in the future ([@sirlag](https://github.com/sirlag)) ([#415](https://github.com/mihonapp/mihon/pull/415)) | ||||
| - Advance setting to install custom color profiles ([@wwww-wwww](https://github.com/wwww-wwww)) ([#523](https://github.com/mihonapp/mihon/pull/523)) | ||||
| - 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 | ||||
| - 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)) | ||||
|   - Fix extra date header introduced by parent PR ([@sirlag](https://github.com/sirlag)) ([#415](https://github.com/mihonapp/mihon/pull/415)) | ||||
|   - Fix build time in about screen displayed in UTC ([@AntsyLich](https://github.com/AntsyLich)) ([`aed53d3`](https://github.com/mihonapp/mihon/commit/aed53d3bdc85ce0e899fbb90b9f9cad0f1b86480)) | ||||
| - App infinitely retries tracker update instead of failing after 3 tries ([@MajorTanya](https://github.com/MajorTanya)) ([#411](https://github.com/mihonapp/mihon/pull/411)) | ||||
| - Crash on Pixel devices (was introduced due to compose update) ([@AntsyLich](https://github.com/AntsyLich)) ([`ab06720`](https://github.com/mihonapp/mihon/commit/ab067209661eceefc04c65f6bdbfcaa8a1264651)) | ||||
| - Crash when opening some heif/heic images ([@az4521](https://github.com/az4521)) ([#466](https://github.com/mihonapp/mihon/pull/466)) | ||||
| - Crash when putting app in background while track date selection dialog is open ([@ivaniskandar](https://github.com/ivaniskandar)) ([`c348fac`](https://github.com/mihonapp/mihon/commit/c348fac78fac479fb123bd617c01c78b9ca851d5)) | ||||
| - Dates for saved images not following the specification (fixes date issue mainly on Samsung devices) ([@MajorTanya](https://github.com/MajorTanya)) ([#552](https://github.com/mihonapp/mihon/pull/552)) | ||||
| - Colors getting distorted when opening CMYK jpeg images ([@wwww-wwww](https://github.com/wwww-wwww)) ([#523](https://github.com/mihonapp/mihon/pull/523)) | ||||
| - 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-27 | ||||
| ### Changed | ||||
| - Don't include custom user agent for MAL (circumvents MAL block) ([@AntsyLich](https://github.com/AntsyLich)) ([`085ad8d`](https://github.com/mihonapp/mihon/commit/085ad8d44637c375a8ed24aba3a6f75f5b0cc9ee)) | ||||
| ## [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 | ||||
| - Hide display cutoff setting in reader settings sheet if fullscreen is disabled ([@Riztard](https://github.com/Riztard)) ([#241](https://github.com/mihonapp/mihon/pull/241)) | ||||
| - Library update error filename to `mihon_update_errors.txt` from `tachiyomi_update_errors.txt` ([@mjishnu](https://github.com/mjishnu)) ([#253](https://github.com/mihonapp/mihon/pull/253)) | ||||
| - 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 | ||||
| - Bottom sheet UI issues on non-tablet devices ([@theolm](https://github.com/theolm)) ([#182](https://github.com/mihonapp/mihon/pull/182)) | ||||
| - Crash when switching screen while a list is scrolling ([@theolm](https://github.com/theolm)) ([#272](https://github.com/mihonapp/mihon/pull/272)) | ||||
| - Newly installed extensions not being recognized by Mihon ([@AwkwardPeak7](https://github.com/AwkwardPeak7)) ([#275](https://github.com/mihonapp/mihon/pull/275)) | ||||
| - Failing to refresh MAL token being inferred as token expiration ([@AntsyLich](https://github.com/AntsyLich)) ([`0f4de03`](https://github.com/mihonapp/mihon/commit/0f4de03d7a77b52490dc9a95e96a308b93b26e4f)) | ||||
|  | ||||
| ### Other | ||||
| - Add `detekt` (kotlin code analyzer) to the project ([@theolm](https://github.com/theolm)) ([#216](https://github.com/mihonapp/mihon/pull/216)) | ||||
| - 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 | ||||
| - Backup now contains scanlator filter of a series ([@jobobby04](https://github.com/jobobby04)) ([#166](https://github.com/mihonapp/mihon/pull/166)) | ||||
| - App icon scaling ([@AntsyLich](https://github.com/AntsyLich)) ([`26815c7`](https://github.com/mihonapp/mihon/commit/26815c7356111394665467c1e81255ac9ee33c1a)) | ||||
| - Tracker OAuth client to Mihon's (fixes login issue for Shikimori tracker) ([@AntsyLich](https://github.com/AntsyLich)) ([`e3f33e2`](https://github.com/mihonapp/mihon/commit/e3f33e24f5e928ac8a85d1f500fd42d4715fc6b5)) | ||||
| - Tracker user agents ([@AntsyLich](https://github.com/AntsyLich), [@kitsumed](https://github.com/kitsumed)) ([`e3f33e2`](https://github.com/mihonapp/mihon/commit/e3f33e24f5e928ac8a85d1f500fd42d4715fc6b5)) | ||||
| - Crash log filename to `mihon_crash_logs.txt` from `tachiyomi_crash_logs.txt` ([@MajorTanya](https://github.com/MajorTanya)) ([#234](https://github.com/mihonapp/mihon/pull/234)) | ||||
| - Don't try to refresh MAL token after refresh token expires ([@AntsyLich](https://github.com/AntsyLich)) ([`32188f9`](https://github.com/mihonapp/mihon/commit/32188f9f65009a18250674ef1bd6e57d351c1fba)) | ||||
| - 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 full black ([@AntsyLich](https://github.com/AntsyLich)) ([`38d6ab8`](https://github.com/mihonapp/mihon/commit/38d6ab80ce868707829dbc81de4170afe3c2f2a5)) | ||||
| - Faulty MangaUpdates score in database ([@AntsyLich](https://github.com/AntsyLich)) ([`a024218`](https://github.com/mihonapp/mihon/commit/a024218410953a389b8af4880fa7ae6cc30124a2)) | ||||
| - "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 in "Data and storage" for some languages ([@theolm](https://github.com/theolm)) ([#202](https://github.com/mihonapp/mihon/pull/202)) | ||||
| - Chapter not being marked as read locally when refreshing Enhanced Trackers ([@Secozzi](https://github.com/Secozzi)) ([#219](https://github.com/mihonapp/mihon/pull/219)) | ||||
|  | ||||
| ### Other | ||||
| - Make `last_modified_at` field in database be `0` on insert ([@kaiserbh](https://github.com/kaiserbh)) ([#113](https://github.com/mihonapp/mihon/pull/113)) | ||||
| - Remove usage of `.not()` where possible in code ([@AntsyLich](https://github.com/AntsyLich)) ([`3940740`](https://github.com/mihonapp/mihon/commit/39407407f282dbb7fa972b12053c26b3e3bd66d8)) | ||||
| - Use type-safe project accessors ([@theolm](https://github.com/theolm)) ([#194](https://github.com/mihonapp/mihon/pull/194)) | ||||
| - Legacy tracker model properties now has the same type as the domain ones ([@AntsyLich](https://github.com/AntsyLich)) ([#245](https://github.com/mihonapp/mihon/pull/245)) | ||||
| - 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 | ||||
| ### Changed | ||||
| - Branding to Mihon (for references we missed) ([@AntsyLich](https://github.com/AntsyLich)) ([`6539406`](https://github.com/mihonapp/mihon/commit/653940613d661eb371aab3b3c3a8181e4e308c43)) | ||||
| - Preview builds are now called Beta builds ([@AntsyLich](https://github.com/AntsyLich)) ([`3c3a1cd`](https://github.com/mihonapp/mihon/commit/3c3a1cd448ab1f653ddd12b2afe0cba38968d1b9)) | ||||
|  | ||||
| ### Fixed | ||||
| - App icon not following the [specification](https://developer.android.com/develop/ui/views/launch/icon_design_adaptive) ([@AntsyLich](https://github.com/AntsyLich)) ([`1849715`](https://github.com/mihonapp/mihon/commit/18497154183356bb0d469b27827f9f7d6b7a3130)) | ||||
| - 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 | ||||
| ### Changed | ||||
| - Branding to Mihon ([@AntsyLich](https://github.com/AntsyLich)) | ||||
| - Minimum supported Android version to 8 ([@AntsyLich](https://github.com/AntsyLich)) ([`dfb3091`](https://github.com/mihonapp/mihon/commit/dfb3091e380dda3e9bfb64bf5c9a685cf3a03d0e)) | ||||
|  | ||||
| [unreleased]: https://github.com/mihonapp/mihon/compare/v0.19.1...main | ||||
| [v0.19.1]: https://github.com/mihonapp/mihon/compare/v0.19.0...v0.19.1 | ||||
| [v0.19.0]: https://github.com/mihonapp/mihon/compare/v0.18.0...v0.19.0 | ||||
| [v0.18.0]: https://github.com/mihonapp/mihon/compare/v0.17.1...v0.18.0 | ||||
| [v0.17.1]: https://github.com/mihonapp/mihon/compare/v0.17.0...v0.17.1 | ||||
| [v0.17.0]: https://github.com/mihonapp/mihon/compare/v0.16.5...v0.17.0 | ||||
| "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/compare/a9c7cbf...v0.16.0 | ||||
| [v0.16.0]: https://github.com/mihonapp/mihon/releases/tag/v0.16.0 | ||||
|   | ||||
| @@ -10,7 +10,7 @@ | ||||
| Discover and read manga, webtoons, comics, and more – easier than ever on your Android device. | ||||
|  | ||||
| [](https://discord.gg/mihon) | ||||
| [](https://mihon.app/download) | ||||
| [](https://github.com/mihonapp/mihon/releases) | ||||
|  | ||||
| [](https://github.com/mihonapp/mihon/actions/workflows/build_push.yml) | ||||
| [](/LICENSE) | ||||
| @@ -18,8 +18,8 @@ Discover and read manga, webtoons, comics, and more – easier than ever on your | ||||
|  | ||||
| ## Download | ||||
|  | ||||
| [](https://mihon.app/download) | ||||
| [](https://mihon.app/download) | ||||
| [](https://github.com/mihonapp/mihon/releases) | ||||
| [](https://github.com/mihonapp/mihon-preview/releases) | ||||
|  | ||||
| *Requires Android 8.0 or higher.* | ||||
|  | ||||
| @@ -68,7 +68,7 @@ The developer(s) of this application does not have any affiliation with the cont | ||||
|  | ||||
| <pre> | ||||
| Copyright © 2015 Javier Tomás | ||||
| Copyright © 2024 Mihon Open Source Project | ||||
| 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. | ||||
|   | ||||
							
								
								
									
										3
									
								
								app/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								app/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| /build | ||||
| *iml | ||||
| *.iml | ||||
| @@ -1,7 +1,7 @@ | ||||
| import mihon.buildlogic.Config | ||||
| import mihon.buildlogic.getBuildTime | ||||
| import mihon.buildlogic.getCommitCount | ||||
| import mihon.buildlogic.getGitSha | ||||
| import org.jetbrains.kotlin.gradle.tasks.KotlinCompile | ||||
|  | ||||
| plugins { | ||||
|     id("mihon.android.application") | ||||
| @@ -11,7 +11,7 @@ plugins { | ||||
|     alias(libs.plugins.aboutLibraries) | ||||
| } | ||||
|  | ||||
| if (Config.includeTelemetry) { | ||||
| if (gradle.startParameter.taskRequests.toString().contains("Standard")) { | ||||
|     pluginManager.apply { | ||||
|         apply(libs.plugins.google.services.get().pluginId) | ||||
|         apply(libs.plugins.firebase.crashlytics.get().pluginId) | ||||
| @@ -20,71 +20,69 @@ if (Config.includeTelemetry) { | ||||
|  | ||||
| shortcutHelper.setFilePath("./shortcuts.xml") | ||||
|  | ||||
| val supportedAbis = setOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64") | ||||
|  | ||||
| android { | ||||
|     namespace = "eu.kanade.tachiyomi" | ||||
|  | ||||
|     defaultConfig { | ||||
|         applicationId = "app.mihon" | ||||
|  | ||||
|         versionCode = 13 | ||||
|         versionName = "0.19.1" | ||||
|         versionCode = 7 | ||||
|         versionName = "0.16.5" | ||||
|  | ||||
|         buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"") | ||||
|         buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"") | ||||
|         buildConfigField("String", "BUILD_TIME", "\"${getBuildTime(useLastCommitTime = false)}\"") | ||||
|         buildConfigField("boolean", "TELEMETRY_INCLUDED", "${Config.includeTelemetry}") | ||||
|         buildConfigField("boolean", "UPDATER_ENABLED", "${Config.enableUpdater}") | ||||
|         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 { | ||||
|         val debug by getting { | ||||
|             applicationIdSuffix = ".dev" | ||||
|         named("debug") { | ||||
|             versionNameSuffix = "-${getCommitCount()}" | ||||
|             applicationIdSuffix = ".debug" | ||||
|             isPseudoLocalesEnabled = true | ||||
|         } | ||||
|         val release by getting { | ||||
|             isMinifyEnabled = Config.enableCodeShrink | ||||
|             isShrinkResources = Config.enableCodeShrink | ||||
|  | ||||
|         named("release") { | ||||
|             isShrinkResources = true | ||||
|             isMinifyEnabled = true | ||||
|             proguardFiles("proguard-android-optimize.txt", "proguard-rules.pro") | ||||
|  | ||||
|             buildConfigField("String", "BUILD_TIME", "\"${getBuildTime(useLastCommitTime = true)}\"") | ||||
|         } | ||||
|  | ||||
|         val commonMatchingFallbacks = listOf(release.name) | ||||
|  | ||||
|         create("foss") { | ||||
|             initWith(release) | ||||
|  | ||||
|             applicationIdSuffix = ".foss" | ||||
|  | ||||
|             matchingFallbacks.addAll(commonMatchingFallbacks) | ||||
|         } | ||||
|         create("preview") { | ||||
|             initWith(release) | ||||
|             initWith(getByName("release")) | ||||
|             buildConfigField("boolean", "PREVIEW", "true") | ||||
|  | ||||
|             applicationIdSuffix = ".debug" | ||||
|  | ||||
|             versionNameSuffix = debug.versionNameSuffix | ||||
|             signingConfig = debug.signingConfig | ||||
|  | ||||
|             matchingFallbacks.addAll(commonMatchingFallbacks) | ||||
|  | ||||
|             buildConfigField("String", "BUILD_TIME", "\"${getBuildTime(useLastCommitTime = false)}\"") | ||||
|             signingConfig = signingConfigs.getByName("debug") | ||||
|             matchingFallbacks.add("release") | ||||
|             val debugType = getByName("debug") | ||||
|             versionNameSuffix = debugType.versionNameSuffix | ||||
|             applicationIdSuffix = debugType.applicationIdSuffix | ||||
|         } | ||||
|         create("benchmark") { | ||||
|             initWith(release) | ||||
|             initWith(getByName("release")) | ||||
|  | ||||
|             signingConfig = signingConfigs.getByName("debug") | ||||
|             matchingFallbacks.add("release") | ||||
|             isDebuggable = false | ||||
|             isProfileable = true | ||||
|             versionNameSuffix = "-benchmark" | ||||
|             applicationIdSuffix = ".benchmark" | ||||
|  | ||||
|             signingConfig = debug.signingConfig | ||||
|  | ||||
|             matchingFallbacks.addAll(commonMatchingFallbacks) | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -93,46 +91,39 @@ android { | ||||
|         getByName("benchmark").res.srcDirs("src/debug/res") | ||||
|     } | ||||
|  | ||||
|     splits { | ||||
|         abi { | ||||
|             isEnable = true | ||||
|             isUniversalApk = true | ||||
|             reset() | ||||
|             include("armeabi-v7a", "arm64-v8a", "x86", "x86_64") | ||||
|     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 { | ||||
|         jniLibs { | ||||
|             keepDebugSymbols += listOf( | ||||
|                 "libandroidx.graphics.path", | ||||
|                 "libarchive-jni", | ||||
|                 "libconscrypt_jni", | ||||
|                 "libimagedecoder", | ||||
|                 "libquickjs", | ||||
|                 "libsqlite3x", | ||||
|             ) | ||||
|                 .map { "**/$it.so" } | ||||
|         } | ||||
|         resources { | ||||
|             excludes += setOf( | ||||
|         resources.excludes.addAll( | ||||
|             listOf( | ||||
|                 "kotlin-tooling-metadata.json", | ||||
|                 "META-INF/DEPENDENCIES", | ||||
|                 "LICENSE.txt", | ||||
|                 "META-INF/**/*.properties", | ||||
|                 "META-INF/LICENSE", | ||||
|                 "META-INF/**/LICENSE.txt", | ||||
|                 "META-INF/*.properties", | ||||
|                 "META-INF/*.version", | ||||
|                 "META-INF/DEPENDENCIES", | ||||
|                 "META-INF/LICENSE", | ||||
|                 "META-INF/NOTICE", | ||||
|                 "META-INF/**/*.properties", | ||||
|                 "META-INF/README.md", | ||||
|             ) | ||||
|         } | ||||
|                 "META-INF/NOTICE", | ||||
|                 "META-INF/*.version", | ||||
|             ), | ||||
|         ) | ||||
|     } | ||||
|  | ||||
|     dependenciesInfo { | ||||
|         includeInApk = Config.includeDependencyInfo | ||||
|         includeInBundle = Config.includeDependencyInfo | ||||
|         includeInApk = false | ||||
|     } | ||||
|  | ||||
|     buildFeatures { | ||||
| @@ -151,24 +142,6 @@ android { | ||||
|     } | ||||
| } | ||||
|  | ||||
| kotlin { | ||||
|     compilerOptions { | ||||
|         freeCompilerArgs.addAll( | ||||
|             "-opt-in=androidx.compose.animation.ExperimentalAnimationApi", | ||||
|             "-opt-in=androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi", | ||||
|             "-opt-in=androidx.compose.foundation.ExperimentalFoundationApi", | ||||
|             "-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi", | ||||
|             "-opt-in=androidx.compose.material3.ExperimentalMaterial3Api", | ||||
|             "-opt-in=androidx.compose.ui.ExperimentalComposeUiApi", | ||||
|             "-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", | ||||
|         ) | ||||
|     } | ||||
| } | ||||
|  | ||||
| dependencies { | ||||
|     implementation(projects.i18n) | ||||
|     implementation(projects.core.archive) | ||||
| @@ -180,7 +153,6 @@ dependencies { | ||||
|     implementation(projects.domain) | ||||
|     implementation(projects.presentationCore) | ||||
|     implementation(projects.presentationWidget) | ||||
|     implementation(projects.telemetry) | ||||
|  | ||||
|     // Compose | ||||
|     implementation(compose.activity) | ||||
| @@ -262,28 +234,27 @@ dependencies { | ||||
|         exclude(group = "androidx.viewpager", module = "viewpager") | ||||
|     } | ||||
|     implementation(libs.insetter) | ||||
|     implementation(libs.richeditor.compose) | ||||
|     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) | ||||
|     implementation(libs.reorderable) | ||||
|     implementation(libs.bundles.markdown) | ||||
|  | ||||
|     // 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) | ||||
|  | ||||
|     // String similarity | ||||
|     implementation(libs.stringSimilarity) | ||||
|  | ||||
|     // Tests | ||||
|     testImplementation(libs.bundles.test) | ||||
|     testRuntimeOnly(libs.junit.platform.launcher) | ||||
|  | ||||
|     // For detecting memory leaks; see https://square.github.io/leakcanary/ | ||||
|     // debugImplementation(libs.leakcanary.android) | ||||
| @@ -293,6 +264,14 @@ dependencies { | ||||
| } | ||||
|  | ||||
| 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 | ||||
| @@ -300,6 +279,28 @@ androidComponents { | ||||
|     } | ||||
| } | ||||
|  | ||||
| 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,12 +1,11 @@ | ||||
| package eu.kanade.core.util | ||||
|  | ||||
| import androidx.compose.ui.util.fastFilter | ||||
| import androidx.compose.ui.util.fastForEach | ||||
| import kotlin.contracts.ExperimentalContracts | ||||
| import kotlin.contracts.contract | ||||
|  | ||||
| fun <T : R, R : Any> List<T>.insertSeparators( | ||||
|     generator: (before: T?, after: T?) -> R?, | ||||
|     generator: (T?, T?) -> R?, | ||||
| ): List<R> { | ||||
|     if (isEmpty()) return emptyList() | ||||
|     val newList = mutableListOf<R>() | ||||
| @@ -20,24 +19,6 @@ fun <T : R, R : Any> List<T>.insertSeparators( | ||||
|     return newList | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Similar to [eu.kanade.core.util.insertSeparators] but iterates from last to first element | ||||
|  */ | ||||
| fun <T : R, R : Any> List<T>.insertSeparatorsReversed( | ||||
|     generator: (before: T?, after: T?) -> R?, | ||||
| ): List<R> { | ||||
|     if (isEmpty()) return emptyList() | ||||
|     val newList = mutableListOf<R>() | ||||
|     for (i in size downTo 0) { | ||||
|         val after = getOrNull(i) | ||||
|         after?.let(newList::add) | ||||
|         val before = getOrNull(i - 1) | ||||
|         val separator = generator.invoke(before, after) | ||||
|         separator?.let(newList::add) | ||||
|     } | ||||
|     return newList.asReversed() | ||||
| } | ||||
|  | ||||
| fun <E> HashSet<E>.addOrRemove(value: E, shouldAdd: Boolean) { | ||||
|     if (shouldAdd) { | ||||
|         add(value) | ||||
| @@ -46,6 +27,21 @@ fun <E> HashSet<E>.addOrRemove(value: E, shouldAdd: Boolean) { | ||||
|     } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * 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]. | ||||
|  * | ||||
| @@ -56,7 +52,27 @@ fun <E> HashSet<E>.addOrRemove(value: E, shouldAdd: Boolean) { | ||||
| @OptIn(ExperimentalContracts::class) | ||||
| inline fun <T> List<T>.fastFilterNot(predicate: (T) -> Boolean): List<T> { | ||||
|     contract { callsInPlace(predicate) } | ||||
|     return fastFilter { !predicate(it) } | ||||
|     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 | ||||
| } | ||||
|  | ||||
| /** | ||||
| @@ -97,3 +113,26 @@ inline fun <T> List<T>.fastCountNot(predicate: (T) -> Boolean): Int { | ||||
|     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 | ||||
| } | ||||
|   | ||||
| @@ -13,11 +13,9 @@ 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.GetIncognitoState | ||||
| 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.ToggleIncognito | ||||
| import eu.kanade.domain.source.interactor.ToggleLanguage | ||||
| import eu.kanade.domain.source.interactor.ToggleSource | ||||
| import eu.kanade.domain.source.interactor.ToggleSourcePin | ||||
| @@ -35,7 +33,6 @@ 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.migration.usecases.MigrateMangaUseCase | ||||
| import mihon.domain.upcoming.interactor.GetUpcomingManga | ||||
| import tachiyomi.data.category.CategoryRepositoryImpl | ||||
| import tachiyomi.data.chapter.ChapterRepositoryImpl | ||||
| @@ -80,7 +77,6 @@ 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.interactor.UpdateMangaNotes | ||||
| import tachiyomi.domain.manga.repository.MangaRepository | ||||
| import tachiyomi.domain.release.interactor.GetApplicationRelease | ||||
| import tachiyomi.domain.release.service.ReleaseService | ||||
| @@ -113,7 +109,7 @@ class DomainModule : InjektModule { | ||||
|         addFactory { RenameCategory(get()) } | ||||
|         addFactory { ReorderCategory(get()) } | ||||
|         addFactory { UpdateCategory(get()) } | ||||
|         addFactory { DeleteCategory(get(), get(), get()) } | ||||
|         addFactory { DeleteCategory(get()) } | ||||
|  | ||||
|         addSingletonFactory<MangaRepository> { MangaRepositoryImpl(get()) } | ||||
|         addFactory { GetDuplicateLibraryManga(get()) } | ||||
| @@ -131,15 +127,9 @@ class DomainModule : InjektModule { | ||||
|         addFactory { SetMangaViewerFlags(get()) } | ||||
|         addFactory { NetworkToLocalManga(get()) } | ||||
|         addFactory { UpdateManga(get(), get()) } | ||||
|         addFactory { UpdateMangaNotes(get()) } | ||||
|         addFactory { SetMangaCategories(get()) } | ||||
|         addFactory { GetExcludedScanlators(get()) } | ||||
|         addFactory { SetExcludedScanlators(get()) } | ||||
|         addFactory { | ||||
|             MigrateMangaUseCase( | ||||
|                 get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), | ||||
|             ) | ||||
|         } | ||||
|  | ||||
|         addSingletonFactory<ReleaseService> { ReleaseServiceImpl(get(), get()) } | ||||
|         addFactory { GetApplicationRelease(get(), get()) } | ||||
| @@ -161,7 +151,7 @@ class DomainModule : InjektModule { | ||||
|         addFactory { UpdateChapter(get()) } | ||||
|         addFactory { SetReadStatus(get(), get(), get(), get()) } | ||||
|         addFactory { ShouldUpdateDbChapter() } | ||||
|         addFactory { SyncChaptersWithSource(get(), get(), get(), get(), get(), get(), get(), get(), get()) } | ||||
|         addFactory { SyncChaptersWithSource(get(), get(), get(), get(), get(), get(), get(), get()) } | ||||
|         addFactory { GetAvailableScanlators(get()) } | ||||
|         addFactory { FilterChaptersForDownload(get(), get(), get()) } | ||||
|  | ||||
| @@ -201,7 +191,5 @@ class DomainModule : InjektModule { | ||||
|         addFactory { DeleteExtensionRepo(get()) } | ||||
|         addFactory { ReplaceExtensionRepo(get()) } | ||||
|         addFactory { UpdateExtensionRepo(get(), get()) } | ||||
|         addFactory { ToggleIncognito(get()) } | ||||
|         addFactory { GetIncognitoState(get(), get(), get()) } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -2,7 +2,6 @@ package eu.kanade.domain.base | ||||
|  | ||||
| import android.content.Context | ||||
| import dev.icerock.moko.resources.StringResource | ||||
| import eu.kanade.tachiyomi.util.system.GLUtil | ||||
| import tachiyomi.core.common.preference.Preference | ||||
| import tachiyomi.core.common.preference.PreferenceStore | ||||
| import tachiyomi.i18n.MR | ||||
| @@ -31,8 +30,4 @@ class BasePreferences( | ||||
|     } | ||||
|  | ||||
|     fun displayProfile() = preferenceStore.getString("pref_display_profile_key", "") | ||||
|  | ||||
|     fun hardwareBitmapThreshold() = preferenceStore.getInt("pref_hardware_bitmap_threshold", GLUtil.SAFE_TEXTURE_LIMIT) | ||||
|  | ||||
|     fun alwaysDecodeLongStripWithSSIV() = preferenceStore.getBoolean("pref_always_decode_long_strip_with_ssiv", false) | ||||
| } | ||||
|   | ||||
| @@ -19,7 +19,6 @@ 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.library.service.LibraryPreferences | ||||
| import tachiyomi.domain.manga.model.Manga | ||||
| import tachiyomi.source.local.isLocal | ||||
| import java.lang.Long.max | ||||
| @@ -35,7 +34,6 @@ class SyncChaptersWithSource( | ||||
|     private val updateChapter: UpdateChapter, | ||||
|     private val getChaptersByMangaId: GetChaptersByMangaId, | ||||
|     private val getExcludedScanlators: GetExcludedScanlators, | ||||
|     private val libraryPreferences: LibraryPreferences, | ||||
| ) { | ||||
|  | ||||
|     /** | ||||
| @@ -147,18 +145,12 @@ class SyncChaptersWithSource( | ||||
|             return emptyList() | ||||
|         } | ||||
|  | ||||
|         val changedOrDuplicateReadUrls = mutableSetOf<String>() | ||||
|         val reAdded = mutableListOf<Chapter>() | ||||
|  | ||||
|         val deletedChapterNumbers = TreeSet<Double>() | ||||
|         val deletedReadChapterNumbers = TreeSet<Double>() | ||||
|         val deletedBookmarkedChapterNumbers = TreeSet<Double>() | ||||
|  | ||||
|         val readChapterNumbers = dbChapters | ||||
|             .asSequence() | ||||
|             .filter { it.read && it.isRecognizedNumber } | ||||
|             .map { it.chapterNumber } | ||||
|             .toSet() | ||||
|  | ||||
|         removedChapters.forEach { chapter -> | ||||
|             if (chapter.read) deletedReadChapterNumbers.add(chapter.chapterNumber) | ||||
|             if (chapter.bookmark) deletedBookmarkedChapterNumbers.add(chapter.chapterNumber) | ||||
| @@ -168,20 +160,12 @@ class SyncChaptersWithSource( | ||||
|         val deletedChapterNumberDateFetchMap = removedChapters.sortedByDescending { it.dateFetch } | ||||
|             .associate { it.chapterNumber to it.dateFetch } | ||||
|  | ||||
|         val markDuplicateAsRead = libraryPreferences.markDuplicateReadChapterAsRead().get() | ||||
|             .contains(LibraryPreferences.MARK_DUPLICATE_CHAPTER_READ_NEW) | ||||
|  | ||||
|         // 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.chapterNumber in readChapterNumbers && markDuplicateAsRead) { | ||||
|                 changedOrDuplicateReadUrls.add(chapter.url) | ||||
|                 chapter = chapter.copy(read = true) | ||||
|             } | ||||
|  | ||||
|             if (!chapter.isRecognizedNumber || chapter.chapterNumber !in deletedChapterNumbers) return@map chapter | ||||
|  | ||||
|             chapter = chapter.copy( | ||||
| @@ -194,7 +178,7 @@ class SyncChaptersWithSource( | ||||
|                 chapter = chapter.copy(dateFetch = it) | ||||
|             } | ||||
|  | ||||
|             changedOrDuplicateReadUrls.add(chapter.url) | ||||
|             reAdded.add(chapter) | ||||
|  | ||||
|             chapter | ||||
|         } | ||||
| @@ -218,8 +202,12 @@ class SyncChaptersWithSource( | ||||
|         // 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 changedOrDuplicateReadUrls || it.scanlator in excludedScanlators } | ||||
|         return updatedToAdd.filterNot { | ||||
|             it.url in reAddedUrls || it.scanlator in excludedScanlators | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -2,9 +2,7 @@ package eu.kanade.domain.manga.interactor | ||||
|  | ||||
| import eu.kanade.domain.manga.model.hasCustomCover | ||||
| import eu.kanade.tachiyomi.data.cache.CoverCache | ||||
| import eu.kanade.tachiyomi.data.download.DownloadManager | ||||
| import eu.kanade.tachiyomi.source.model.SManga | ||||
| import tachiyomi.domain.library.service.LibraryPreferences | ||||
| import tachiyomi.domain.manga.interactor.FetchInterval | ||||
| import tachiyomi.domain.manga.model.Manga | ||||
| import tachiyomi.domain.manga.model.MangaUpdate | ||||
| @@ -33,8 +31,6 @@ class UpdateManga( | ||||
|         remoteManga: SManga, | ||||
|         manualFetch: Boolean, | ||||
|         coverCache: CoverCache = Injekt.get(), | ||||
|         libraryPreferences: LibraryPreferences = Injekt.get(), | ||||
|         downloadManager: DownloadManager = Injekt.get(), | ||||
|     ): Boolean { | ||||
|         val remoteTitle = try { | ||||
|             remoteManga.title | ||||
| @@ -42,13 +38,8 @@ class UpdateManga( | ||||
|             "" | ||||
|         } | ||||
|  | ||||
|         // if the manga isn't a favorite (or 'update titles' preference is enabled), set its title from source and update in db | ||||
|         val title = | ||||
|             if (remoteTitle.isNotEmpty() && (!localManga.favorite || libraryPreferences.updateMangaTitles().get())) { | ||||
|                 remoteTitle | ||||
|             } else { | ||||
|                 null | ||||
|             } | ||||
|         // 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 { | ||||
| @@ -68,7 +59,7 @@ class UpdateManga( | ||||
|  | ||||
|         val thumbnailUrl = remoteManga.thumbnail_url?.takeIf { it.isNotEmpty() } | ||||
|  | ||||
|         val success = mangaRepository.update( | ||||
|         return mangaRepository.update( | ||||
|             MangaUpdate( | ||||
|                 id = localManga.id, | ||||
|                 title = title, | ||||
| @@ -83,10 +74,6 @@ class UpdateManga( | ||||
|                 initialized = true, | ||||
|             ), | ||||
|         ) | ||||
|         if (success && title != null) { | ||||
|             downloadManager.renameManga(localManga, title) | ||||
|         } | ||||
|         return success | ||||
|     } | ||||
|  | ||||
|     suspend fun awaitUpdateFetchInterval( | ||||
|   | ||||
| @@ -22,7 +22,7 @@ val Manga.readerOrientation: Long | ||||
|  | ||||
| val Manga.downloadedFilter: TriState | ||||
|     get() { | ||||
|         if (Injekt.get<BasePreferences>().downloadedOnly().get()) return TriState.ENABLED_IS | ||||
|         if (forceDownloaded()) return TriState.ENABLED_IS | ||||
|         return when (downloadedFilterRaw) { | ||||
|             Manga.CHAPTER_SHOW_DOWNLOADED -> TriState.ENABLED_IS | ||||
|             Manga.CHAPTER_SHOW_NOT_DOWNLOADED -> TriState.ENABLED_NOT | ||||
| @@ -34,6 +34,9 @@ fun Manga.chaptersFiltered(): Boolean { | ||||
|         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 | ||||
| @@ -69,6 +72,22 @@ fun Manga.copyFrom(other: SManga): Manga { | ||||
|     ) | ||||
| } | ||||
|  | ||||
| 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() | ||||
| } | ||||
|   | ||||
| @@ -1,35 +0,0 @@ | ||||
| package eu.kanade.domain.source.interactor | ||||
|  | ||||
| import eu.kanade.domain.base.BasePreferences | ||||
| import eu.kanade.domain.source.service.SourcePreferences | ||||
| import eu.kanade.tachiyomi.extension.ExtensionManager | ||||
| import kotlinx.coroutines.flow.Flow | ||||
| import kotlinx.coroutines.flow.combine | ||||
| import kotlinx.coroutines.flow.distinctUntilChanged | ||||
|  | ||||
| class GetIncognitoState( | ||||
|     private val basePreferences: BasePreferences, | ||||
|     private val sourcePreferences: SourcePreferences, | ||||
|     private val extensionManager: ExtensionManager, | ||||
| ) { | ||||
|     fun await(sourceId: Long?): Boolean { | ||||
|         if (basePreferences.incognitoMode().get()) return true | ||||
|         if (sourceId == null) return false | ||||
|         val extensionPackage = extensionManager.getExtensionPackage(sourceId) ?: return false | ||||
|  | ||||
|         return extensionPackage in sourcePreferences.incognitoExtensions().get() | ||||
|     } | ||||
|  | ||||
|     fun subscribe(sourceId: Long?): Flow<Boolean> { | ||||
|         if (sourceId == null) return basePreferences.incognitoMode().changes() | ||||
|  | ||||
|         return combine( | ||||
|             basePreferences.incognitoMode().changes(), | ||||
|             sourcePreferences.incognitoExtensions().changes(), | ||||
|             extensionManager.getExtensionPackageAsFlow(sourceId), | ||||
|         ) { incognito, incognitoExtensions, extensionPackage -> | ||||
|             incognito || (extensionPackage in incognitoExtensions) | ||||
|         } | ||||
|             .distinctUntilChanged() | ||||
|     } | ||||
| } | ||||
| @@ -1,14 +0,0 @@ | ||||
| package eu.kanade.domain.source.interactor | ||||
|  | ||||
| import eu.kanade.domain.source.service.SourcePreferences | ||||
| import tachiyomi.core.common.preference.getAndSet | ||||
|  | ||||
| class ToggleIncognito( | ||||
|     private val preferences: SourcePreferences, | ||||
| ) { | ||||
|     fun await(extensions: String, enable: Boolean) { | ||||
|         preferences.incognitoExtensions().getAndSet { | ||||
|             if (enable) it.plus(extensions) else it.minus(extensions) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -2,18 +2,16 @@ package eu.kanade.domain.source.service | ||||
|  | ||||
| import eu.kanade.domain.source.interactor.SetMigrateSorting | ||||
| import eu.kanade.tachiyomi.util.system.LocaleHelper | ||||
| import mihon.domain.migration.models.MigrationFlag | ||||
| import tachiyomi.core.common.preference.Preference | ||||
| import tachiyomi.core.common.preference.PreferenceStore | ||||
| import tachiyomi.core.common.preference.getEnum | ||||
| import tachiyomi.core.common.preference.getLongArray | ||||
| import tachiyomi.domain.library.model.LibraryDisplayMode | ||||
|  | ||||
| class SourcePreferences( | ||||
|     private val preferenceStore: PreferenceStore, | ||||
| ) { | ||||
|  | ||||
|     fun sourceDisplayMode() = preferenceStore.getObjectFromString( | ||||
|     fun sourceDisplayMode() = preferenceStore.getObject( | ||||
|         "pref_display_mode_catalogue", | ||||
|         LibraryDisplayMode.default, | ||||
|         LibraryDisplayMode.Serializer::serialize, | ||||
| @@ -24,8 +22,6 @@ class SourcePreferences( | ||||
|  | ||||
|     fun disabledSources() = preferenceStore.getStringSet("hidden_catalogues", emptySet()) | ||||
|  | ||||
|     fun incognitoExtensions() = preferenceStore.getStringSet("incognito_extensions", emptySet()) | ||||
|  | ||||
|     fun pinnedSources() = preferenceStore.getStringSet("pinned_catalogues", emptySet()) | ||||
|  | ||||
|     fun lastUsedSource() = preferenceStore.getLong( | ||||
| @@ -57,21 +53,4 @@ class SourcePreferences( | ||||
|         Preference.appStateKey("has_filters_toggle_state"), | ||||
|         false, | ||||
|     ) | ||||
|  | ||||
|     fun migrationSources() = preferenceStore.getLongArray("migration_sources", emptyList()) | ||||
|  | ||||
|     fun migrationFlags() = preferenceStore.getObjectFromInt( | ||||
|         key = "migration_flags", | ||||
|         defaultValue = MigrationFlag.entries.toSet(), | ||||
|         serializer = { MigrationFlag.toBit(it) }, | ||||
|         deserializer = { value: Int -> MigrationFlag.fromBit(value) }, | ||||
|     ) | ||||
|  | ||||
|     fun migrationDeepSearchMode() = preferenceStore.getBoolean("migration_deep_search", false) | ||||
|  | ||||
|     fun migrationPrioritizeByChapters() = preferenceStore.getBoolean("migration_prioritize_by_chapters", false) | ||||
|  | ||||
|     fun migrationHideUnmatched() = preferenceStore.getBoolean("migration_hide_unmatched", false) | ||||
|  | ||||
|     fun migrationHideWithoutUpdates() = preferenceStore.getBoolean("migration_hide_without_updates", false) | ||||
| } | ||||
|   | ||||
| @@ -5,7 +5,6 @@ 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.data.track.TrackerManager | ||||
| import eu.kanade.tachiyomi.source.Source | ||||
| import eu.kanade.tachiyomi.util.lang.convertEpochMillisZone | ||||
| import logcat.LogPriority | ||||
| @@ -15,16 +14,17 @@ 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, | ||||
|     private val trackerManager: TrackerManager, | ||||
| ) { | ||||
|  | ||||
|     // TODO: update all trackers based on common data | ||||
| @@ -79,7 +79,7 @@ class AddTracks( | ||||
|  | ||||
|     suspend fun bindEnhancedTrackers(manga: Manga, source: Source) = withNonCancellableContext { | ||||
|         withIOContext { | ||||
|             trackerManager.loggedInTrackers() | ||||
|             getTracks.await(manga.id) | ||||
|                 .filterIsInstance<EnhancedTracker>() | ||||
|                 .filter { it.accept(source) } | ||||
|                 .forEach { service -> | ||||
| @@ -87,11 +87,11 @@ class AddTracks( | ||||
|                         service.match(manga)?.let { track -> | ||||
|                             track.manga_id = manga.id | ||||
|                             (service as Tracker).bind(track) | ||||
|                             insertTrack.await(track.toDomainTrack(idRequired = false)!!) | ||||
|                             insertTrack.await(track.toDomainTrack()!!) | ||||
|  | ||||
|                             syncChapterProgressWithTrack.await( | ||||
|                                 manga.id, | ||||
|                                 track.toDomainTrack(idRequired = false)!!, | ||||
|                                 track.toDomainTrack()!!, | ||||
|                                 service, | ||||
|                             ) | ||||
|                         } | ||||
|   | ||||
| @@ -1,10 +0,0 @@ | ||||
| package eu.kanade.domain.track.model | ||||
|  | ||||
| import dev.icerock.moko.resources.StringResource | ||||
| import tachiyomi.i18n.MR | ||||
|  | ||||
| enum class AutoTrackState(val titleRes: StringResource) { | ||||
|     ALWAYS(MR.strings.lock_always), | ||||
|     ASK(MR.strings.default_category_summary), | ||||
|     NEVER(MR.strings.lock_never), | ||||
| } | ||||
| @@ -10,7 +10,6 @@ fun Track.copyPersonalFrom(other: Track): Track { | ||||
|         status = other.status, | ||||
|         startDate = other.startDate, | ||||
|         finishDate = other.finishDate, | ||||
|         private = other.private, | ||||
|     ) | ||||
| } | ||||
|  | ||||
| @@ -27,7 +26,6 @@ fun Track.toDbTrack(): DbTrack = DbTrack.create(trackerId).also { | ||||
|     it.tracking_url = remoteUrl | ||||
|     it.started_reading_date = startDate | ||||
|     it.finished_reading_date = finishDate | ||||
|     it.private = private | ||||
| } | ||||
|  | ||||
| fun DbTrack.toDomainTrack(idRequired: Boolean = true): Track? { | ||||
| @@ -46,6 +44,5 @@ fun DbTrack.toDomainTrack(idRequired: Boolean = true): Track? { | ||||
|         remoteUrl = tracking_url, | ||||
|         startDate = started_reading_date, | ||||
|         finishDate = finished_reading_date, | ||||
|         private = private, | ||||
|     ) | ||||
| } | ||||
|   | ||||
| @@ -1,11 +1,9 @@ | ||||
| package eu.kanade.domain.track.service | ||||
|  | ||||
| import eu.kanade.domain.track.model.AutoTrackState | ||||
| 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 | ||||
| import tachiyomi.core.common.preference.getEnum | ||||
|  | ||||
| class TrackPreferences( | ||||
|     private val preferenceStore: PreferenceStore, | ||||
| @@ -37,9 +35,4 @@ class TrackPreferences( | ||||
|     fun anilistScoreType() = preferenceStore.getString("anilist_score_type", Anilist.POINT_10) | ||||
|  | ||||
|     fun autoUpdateTrack() = preferenceStore.getBoolean("pref_auto_update_manga_sync_key", true) | ||||
|  | ||||
|     fun autoUpdateTrackOnMarkRead() = preferenceStore.getEnum( | ||||
|         "pref_auto_update_manga_on_mark_read", | ||||
|         AutoTrackState.ALWAYS, | ||||
|     ) | ||||
| } | ||||
|   | ||||
| @@ -34,8 +34,6 @@ class UiPreferences( | ||||
|  | ||||
|     fun tabletUiMode() = preferenceStore.getEnum("tablet_ui_mode", TabletUiMode.AUTOMATIC) | ||||
|  | ||||
|     fun imagesInDescription() = preferenceStore.getBoolean("pref_render_images_description", true) | ||||
|  | ||||
|     companion object { | ||||
|         fun dateFormat(format: String): DateTimeFormatter = when (format) { | ||||
|             "" -> DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT) | ||||
|   | ||||
| @@ -1,23 +1,25 @@ | ||||
| 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), | ||||
|     CATPPUCCIN(MR.strings.theme_catppuccin), | ||||
|     GREEN_APPLE(MR.strings.theme_greenapple), | ||||
|     LAVENDER(MR.strings.theme_lavender), | ||||
|     MIDNIGHT_DUSK(MR.strings.theme_midnightdusk), | ||||
|     NORD(MR.strings.theme_nord), | ||||
|  | ||||
|     // 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), | ||||
|     MONOCHROME(MR.strings.theme_monochrome), | ||||
|  | ||||
|     // Deprecated | ||||
|     DARK_BLUE(null), | ||||
|   | ||||
| @@ -73,18 +73,10 @@ fun BrowseSourceContent( | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     if (mangaList.itemCount == 0 && mangaList.loadState.refresh is LoadState.Loading) { | ||||
|         LoadingScreen(Modifier.padding(contentPadding)) | ||||
|         return | ||||
|     } | ||||
|  | ||||
|     if (mangaList.itemCount == 0) { | ||||
|     if (mangaList.itemCount <= 0 && errorState != null && errorState is LoadState.Error) { | ||||
|         EmptyScreen( | ||||
|             modifier = Modifier.padding(contentPadding), | ||||
|             message = when (errorState) { | ||||
|                 is LoadState.Error -> getErrorMessage(errorState) | ||||
|                 else -> stringResource(MR.strings.no_results_found) | ||||
|             }, | ||||
|             message = getErrorMessage(errorState), | ||||
|             actions = if (source is LocalSource) { | ||||
|                 persistentListOf( | ||||
|                     EmptyScreenAction( | ||||
| @@ -117,6 +109,13 @@ fun BrowseSourceContent( | ||||
|         return | ||||
|     } | ||||
|  | ||||
|     if (mangaList.itemCount == 0 && mangaList.loadState.refresh is LoadState.Loading) { | ||||
|         LoadingScreen( | ||||
|             modifier = Modifier.padding(contentPadding), | ||||
|         ) | ||||
|         return | ||||
|     } | ||||
|  | ||||
|     when (displayMode) { | ||||
|         LibraryDisplayMode.ComfortableGrid -> { | ||||
|             BrowseSourceComfortableGrid( | ||||
|   | ||||
| @@ -35,10 +35,8 @@ import androidx.compose.runtime.remember | ||||
| import androidx.compose.runtime.setValue | ||||
| import androidx.compose.ui.Alignment | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.graphics.vector.ImageVector | ||||
| import androidx.compose.ui.platform.LocalContext | ||||
| import androidx.compose.ui.platform.LocalUriHandler | ||||
| import androidx.compose.ui.res.vectorResource | ||||
| import androidx.compose.ui.text.TextStyle | ||||
| import androidx.compose.ui.text.font.FontWeight | ||||
| import androidx.compose.ui.text.style.TextAlign | ||||
| @@ -50,7 +48,6 @@ 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.R | ||||
| import eu.kanade.tachiyomi.extension.model.Extension | ||||
| import eu.kanade.tachiyomi.source.ConfigurableSource | ||||
| import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionDetailsScreenModel | ||||
| @@ -75,7 +72,6 @@ fun ExtensionDetailsScreen( | ||||
|     onClickClearCookies: () -> Unit, | ||||
|     onClickUninstall: () -> Unit, | ||||
|     onClickSource: (sourceId: Long) -> Unit, | ||||
|     onClickIncognito: (Boolean) -> Unit, | ||||
| ) { | ||||
|     val uriHandler = LocalUriHandler.current | ||||
|     val url = remember(state.extension) { | ||||
| @@ -144,11 +140,9 @@ fun ExtensionDetailsScreen( | ||||
|             contentPadding = paddingValues, | ||||
|             extension = state.extension, | ||||
|             sources = state.sources, | ||||
|             incognitoMode = state.isIncognito, | ||||
|             onClickSourcePreferences = onClickSourcePreferences, | ||||
|             onClickUninstall = onClickUninstall, | ||||
|             onClickSource = onClickSource, | ||||
|             onClickIncognito = onClickIncognito, | ||||
|         ) | ||||
|     } | ||||
| } | ||||
| @@ -158,11 +152,9 @@ private fun ExtensionDetails( | ||||
|     contentPadding: PaddingValues, | ||||
|     extension: Extension.Installed, | ||||
|     sources: ImmutableList<ExtensionSourceItem>, | ||||
|     incognitoMode: Boolean, | ||||
|     onClickSourcePreferences: (sourceId: Long) -> Unit, | ||||
|     onClickUninstall: () -> Unit, | ||||
|     onClickSource: (sourceId: Long) -> Unit, | ||||
|     onClickIncognito: (Boolean) -> Unit, | ||||
| ) { | ||||
|     val context = LocalContext.current | ||||
|     var showNsfwWarning by remember { mutableStateOf(false) } | ||||
| @@ -179,7 +171,6 @@ private fun ExtensionDetails( | ||||
|         item { | ||||
|             DetailsHeader( | ||||
|                 extension = extension, | ||||
|                 extIncognitoMode = incognitoMode, | ||||
|                 onClickUninstall = onClickUninstall, | ||||
|                 onClickAppInfo = { | ||||
|                     Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { | ||||
| @@ -191,7 +182,6 @@ private fun ExtensionDetails( | ||||
|                 onClickAgeRating = { | ||||
|                     showNsfwWarning = true | ||||
|                 }, | ||||
|                 onExtIncognitoChange = onClickIncognito, | ||||
|             ) | ||||
|         } | ||||
|  | ||||
| @@ -219,11 +209,9 @@ private fun ExtensionDetails( | ||||
| @Composable | ||||
| private fun DetailsHeader( | ||||
|     extension: Extension, | ||||
|     extIncognitoMode: Boolean, | ||||
|     onClickAgeRating: () -> Unit, | ||||
|     onClickUninstall: () -> Unit, | ||||
|     onClickAppInfo: (() -> Unit)?, | ||||
|     onExtIncognitoChange: (Boolean) -> Unit, | ||||
| ) { | ||||
|     val context = LocalContext.current | ||||
|  | ||||
| @@ -231,8 +219,9 @@ private fun DetailsHeader( | ||||
|         Column( | ||||
|             modifier = Modifier | ||||
|                 .fillMaxWidth() | ||||
|                 .padding(horizontal = MaterialTheme.padding.medium) | ||||
|                 .padding( | ||||
|                     start = MaterialTheme.padding.medium, | ||||
|                     end = MaterialTheme.padding.medium, | ||||
|                     top = MaterialTheme.padding.medium, | ||||
|                     bottom = MaterialTheme.padding.small, | ||||
|                 ) | ||||
| @@ -324,9 +313,12 @@ private fun DetailsHeader( | ||||
|         } | ||||
|  | ||||
|         Row( | ||||
|             modifier = Modifier | ||||
|                 .padding(horizontal = MaterialTheme.padding.medium) | ||||
|                 .padding(top = MaterialTheme.padding.small), | ||||
|             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( | ||||
| @@ -349,24 +341,6 @@ private fun DetailsHeader( | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         TextPreferenceWidget( | ||||
|             modifier = Modifier.padding(horizontal = MaterialTheme.padding.small), | ||||
|             title = stringResource(MR.strings.pref_incognito_mode), | ||||
|             subtitle = stringResource(MR.strings.pref_incognito_mode_extension_summary), | ||||
|             icon = ImageVector.vectorResource(R.drawable.ic_glasses_24dp), | ||||
|             widget = { | ||||
|                 Row( | ||||
|                     verticalAlignment = Alignment.CenterVertically, | ||||
|                 ) { | ||||
|                     Switch( | ||||
|                         checked = extIncognitoMode, | ||||
|                         onCheckedChange = onExtIncognitoChange, | ||||
|                         modifier = Modifier.padding(start = TrailingWidgetBuffer), | ||||
|                     ) | ||||
|                 } | ||||
|             }, | ||||
|         ) | ||||
|  | ||||
|         HorizontalDivider() | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -353,17 +353,13 @@ private fun ExtensionItemContent( | ||||
|             horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall), | ||||
|         ) { | ||||
|             ProvideTextStyle(value = MaterialTheme.typography.bodySmall) { | ||||
|                 var hasAlreadyShownAnElement by remember { mutableStateOf(false) } | ||||
|                 if (extension is Extension.Installed && extension.lang.isNotEmpty()) { | ||||
|                     hasAlreadyShownAnElement = true | ||||
|                     Text( | ||||
|                         text = LocaleHelper.getSourceDisplayName(extension.lang, LocalContext.current), | ||||
|                     ) | ||||
|                 } | ||||
|  | ||||
|                 if (extension.versionName.isNotEmpty()) { | ||||
|                     if (hasAlreadyShownAnElement) DotSeparatorNoSpaceText() | ||||
|                     hasAlreadyShownAnElement = true | ||||
|                     Text( | ||||
|                         text = extension.versionName, | ||||
|                     ) | ||||
| @@ -376,8 +372,6 @@ private fun ExtensionItemContent( | ||||
|                     else -> null | ||||
|                 } | ||||
|                 if (warning != null) { | ||||
|                     if (hasAlreadyShownAnElement) DotSeparatorNoSpaceText() | ||||
|                     hasAlreadyShownAnElement = true | ||||
|                     Text( | ||||
|                         text = stringResource(warning).uppercase(), | ||||
|                         color = MaterialTheme.colorScheme.error, | ||||
| @@ -385,12 +379,6 @@ private fun ExtensionItemContent( | ||||
|                         overflow = TextOverflow.Ellipsis, | ||||
|                     ) | ||||
|                 } | ||||
|                 if (extension is Extension.Installed && !extension.isShared) { | ||||
|                     if (hasAlreadyShownAnElement) DotSeparatorNoSpaceText() | ||||
|                     Text( | ||||
|                         text = stringResource(MR.strings.ext_installer_private), | ||||
|                     ) | ||||
|                 } | ||||
|  | ||||
|                 if (!installStep.isCompleted()) { | ||||
|                     DotSeparatorNoSpaceText() | ||||
|   | ||||
| @@ -40,7 +40,6 @@ fun GlobalSearchScreen( | ||||
|                 navigateUp = navigateUp, | ||||
|                 onChangeSearchQuery = onChangeSearchQuery, | ||||
|                 onSearch = onSearch, | ||||
|                 hideSourceFilter = false, | ||||
|                 sourceFilter = state.sourceFilter, | ||||
|                 onChangeSearchFilter = onChangeSearchFilter, | ||||
|                 onlyShowHasResults = state.onlyShowHasResults, | ||||
|   | ||||
| @@ -0,0 +1,84 @@ | ||||
| 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) }, | ||||
|     ) | ||||
| } | ||||
| @@ -32,7 +32,6 @@ fun MigrateSearchScreen( | ||||
|                 navigateUp = navigateUp, | ||||
|                 onChangeSearchQuery = onChangeSearchQuery, | ||||
|                 onSearch = onSearch, | ||||
|                 hideSourceFilter = true, | ||||
|                 sourceFilter = state.sourceFilter, | ||||
|                 onChangeSearchFilter = onChangeSearchFilter, | ||||
|                 onlyShowHasResults = state.onlyShowHasResults, | ||||
|   | ||||
| @@ -40,7 +40,6 @@ fun GlobalSearchToolbar( | ||||
|     navigateUp: () -> Unit, | ||||
|     onChangeSearchQuery: (String?) -> Unit, | ||||
|     onSearch: (String) -> Unit, | ||||
|     hideSourceFilter: Boolean, | ||||
|     sourceFilter: SourceFilter, | ||||
|     onChangeSearchFilter: (SourceFilter) -> Unit, | ||||
|     onlyShowHasResults: Boolean, | ||||
| @@ -74,40 +73,38 @@ fun GlobalSearchToolbar( | ||||
|             horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small), | ||||
|         ) { | ||||
|             // TODO: make this UX better; it only applies when triggering a new search | ||||
|             if (!hideSourceFilter) { | ||||
|                 FilterChip( | ||||
|                     selected = sourceFilter == SourceFilter.PinnedOnly, | ||||
|                     onClick = { onChangeSearchFilter(SourceFilter.PinnedOnly) }, | ||||
|                     leadingIcon = { | ||||
|                         Icon( | ||||
|                             imageVector = Icons.Outlined.PushPin, | ||||
|                             contentDescription = null, | ||||
|                             modifier = Modifier | ||||
|                                 .size(FilterChipDefaults.IconSize), | ||||
|                         ) | ||||
|                     }, | ||||
|                     label = { | ||||
|                         Text(text = stringResource(MR.strings.pinned_sources)) | ||||
|                     }, | ||||
|                 ) | ||||
|                 FilterChip( | ||||
|                     selected = sourceFilter == SourceFilter.All, | ||||
|                     onClick = { onChangeSearchFilter(SourceFilter.All) }, | ||||
|                     leadingIcon = { | ||||
|                         Icon( | ||||
|                             imageVector = Icons.Outlined.DoneAll, | ||||
|                             contentDescription = null, | ||||
|                             modifier = Modifier | ||||
|                                 .size(FilterChipDefaults.IconSize), | ||||
|                         ) | ||||
|                     }, | ||||
|                     label = { | ||||
|                         Text(text = stringResource(MR.strings.all)) | ||||
|                     }, | ||||
|                 ) | ||||
|             FilterChip( | ||||
|                 selected = sourceFilter == SourceFilter.PinnedOnly, | ||||
|                 onClick = { onChangeSearchFilter(SourceFilter.PinnedOnly) }, | ||||
|                 leadingIcon = { | ||||
|                     Icon( | ||||
|                         imageVector = Icons.Outlined.PushPin, | ||||
|                         contentDescription = null, | ||||
|                         modifier = Modifier | ||||
|                             .size(FilterChipDefaults.IconSize), | ||||
|                     ) | ||||
|                 }, | ||||
|                 label = { | ||||
|                     Text(text = stringResource(MR.strings.pinned_sources)) | ||||
|                 }, | ||||
|             ) | ||||
|             FilterChip( | ||||
|                 selected = sourceFilter == SourceFilter.All, | ||||
|                 onClick = { onChangeSearchFilter(SourceFilter.All) }, | ||||
|                 leadingIcon = { | ||||
|                     Icon( | ||||
|                         imageVector = Icons.Outlined.DoneAll, | ||||
|                         contentDescription = null, | ||||
|                         modifier = Modifier | ||||
|                             .size(FilterChipDefaults.IconSize), | ||||
|                     ) | ||||
|                 }, | ||||
|                 label = { | ||||
|                     Text(text = stringResource(MR.strings.all)) | ||||
|                 }, | ||||
|             ) | ||||
|  | ||||
|                 VerticalDivider() | ||||
|             } | ||||
|             VerticalDivider() | ||||
|  | ||||
|             FilterChip( | ||||
|                 selected = onlyShowHasResults, | ||||
|   | ||||
| @@ -2,24 +2,22 @@ package eu.kanade.presentation.category | ||||
|  | ||||
| import androidx.compose.foundation.layout.Arrangement | ||||
| import androidx.compose.foundation.layout.PaddingValues | ||||
| import androidx.compose.foundation.layout.fillMaxSize | ||||
| import androidx.compose.foundation.layout.padding | ||||
| import androidx.compose.foundation.lazy.LazyColumn | ||||
| import androidx.compose.foundation.lazy.LazyListState | ||||
| import androidx.compose.foundation.lazy.items | ||||
| import androidx.compose.foundation.lazy.itemsIndexed | ||||
| import androidx.compose.foundation.lazy.rememberLazyListState | ||||
| import androidx.compose.material.icons.Icons | ||||
| import androidx.compose.material.icons.outlined.SortByAlpha | ||||
| import androidx.compose.material3.MaterialTheme | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.runtime.LaunchedEffect | ||||
| import androidx.compose.runtime.remember | ||||
| import androidx.compose.runtime.toMutableStateList | ||||
| import androidx.compose.ui.Modifier | ||||
| import eu.kanade.presentation.category.components.CategoryFloatingActionButton | ||||
| import eu.kanade.presentation.category.components.CategoryListItem | ||||
| import eu.kanade.presentation.components.AppBar | ||||
| import eu.kanade.presentation.components.AppBarActions | ||||
| import eu.kanade.tachiyomi.ui.category.CategoryScreenState | ||||
| import sh.calvin.reorderable.ReorderableItem | ||||
| import sh.calvin.reorderable.rememberReorderableLazyListState | ||||
| import kotlinx.collections.immutable.persistentListOf | ||||
| import tachiyomi.domain.category.model.Category | ||||
| import tachiyomi.i18n.MR | ||||
| import tachiyomi.presentation.core.components.material.Scaffold | ||||
| @@ -33,9 +31,11 @@ import tachiyomi.presentation.core.util.plus | ||||
| fun CategoryScreen( | ||||
|     state: CategoryScreenState.Success, | ||||
|     onClickCreate: () -> Unit, | ||||
|     onClickSortAlphabetically: () -> Unit, | ||||
|     onClickRename: (Category) -> Unit, | ||||
|     onClickDelete: (Category) -> Unit, | ||||
|     onChangeOrder: (Category, Int) -> Unit, | ||||
|     onClickMoveUp: (Category) -> Unit, | ||||
|     onClickMoveDown: (Category) -> Unit, | ||||
|     navigateUp: () -> Unit, | ||||
| ) { | ||||
|     val lazyListState = rememberLazyListState() | ||||
| @@ -44,6 +44,17 @@ fun CategoryScreen( | ||||
|             AppBar( | ||||
|                 title = stringResource(MR.strings.action_edit_categories), | ||||
|                 navigateUp = navigateUp, | ||||
|                 actions = { | ||||
|                     AppBarActions( | ||||
|                         persistentListOf( | ||||
|                             AppBar.Action( | ||||
|                                 title = stringResource(MR.strings.action_sort), | ||||
|                                 icon = Icons.Outlined.SortByAlpha, | ||||
|                                 onClick = onClickSortAlphabetically, | ||||
|                             ), | ||||
|                         ), | ||||
|                     ) | ||||
|                 }, | ||||
|                 scrollBehavior = scrollBehavior, | ||||
|             ) | ||||
|         }, | ||||
| @@ -65,10 +76,13 @@ fun CategoryScreen( | ||||
|         CategoryContent( | ||||
|             categories = state.categories, | ||||
|             lazyListState = lazyListState, | ||||
|             paddingValues = paddingValues, | ||||
|             paddingValues = paddingValues + | ||||
|                 topSmallPaddingValues + | ||||
|                 PaddingValues(horizontal = MaterialTheme.padding.medium), | ||||
|             onClickRename = onClickRename, | ||||
|             onClickDelete = onClickDelete, | ||||
|             onChangeOrder = onChangeOrder, | ||||
|             onMoveUp = onClickMoveUp, | ||||
|             onMoveDown = onClickMoveDown, | ||||
|         ) | ||||
|     } | ||||
| } | ||||
| @@ -80,44 +94,28 @@ private fun CategoryContent( | ||||
|     paddingValues: PaddingValues, | ||||
|     onClickRename: (Category) -> Unit, | ||||
|     onClickDelete: (Category) -> Unit, | ||||
|     onChangeOrder: (Category, Int) -> Unit, | ||||
|     onMoveUp: (Category) -> Unit, | ||||
|     onMoveDown: (Category) -> Unit, | ||||
| ) { | ||||
|     val categoriesState = remember { categories.toMutableStateList() } | ||||
|     val reorderableState = rememberReorderableLazyListState(lazyListState, paddingValues) { from, to -> | ||||
|         val item = categoriesState.removeAt(from.index) | ||||
|         categoriesState.add(to.index, item) | ||||
|         onChangeOrder(item, to.index) | ||||
|     } | ||||
|  | ||||
|     LaunchedEffect(categories) { | ||||
|         if (!reorderableState.isAnyItemDragging) { | ||||
|             categoriesState.clear() | ||||
|             categoriesState.addAll(categories) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     LazyColumn( | ||||
|         modifier = Modifier.fillMaxSize(), | ||||
|         state = lazyListState, | ||||
|         contentPadding = paddingValues + | ||||
|             topSmallPaddingValues + | ||||
|             PaddingValues(horizontal = MaterialTheme.padding.medium), | ||||
|         contentPadding = paddingValues, | ||||
|         verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small), | ||||
|     ) { | ||||
|         items( | ||||
|             items = categoriesState, | ||||
|             key = { category -> category.key }, | ||||
|         ) { category -> | ||||
|             ReorderableItem(reorderableState, category.key) { | ||||
|                 CategoryListItem( | ||||
|                     modifier = Modifier.animateItem(), | ||||
|                     category = category, | ||||
|                     onRename = { onClickRename(category) }, | ||||
|                     onDelete = { onClickDelete(category) }, | ||||
|                 ) | ||||
|             } | ||||
|         itemsIndexed( | ||||
|             items = categories, | ||||
|             key = { _, category -> "category-${category.id}" }, | ||||
|         ) { index, category -> | ||||
|             CategoryListItem( | ||||
|                 modifier = Modifier.animateItem(), | ||||
|                 category = category, | ||||
|                 canMoveUp = index != 0, | ||||
|                 canMoveDown = index != categories.lastIndex, | ||||
|                 onMoveUp = onMoveUp, | ||||
|                 onMoveDown = onMoveDown, | ||||
|                 onRename = { onClickRename(category) }, | ||||
|                 onDelete = { onClickDelete(category) }, | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| private val Category.key inline get() = "category-$id" | ||||
|   | ||||
| @@ -193,6 +193,35 @@ fun CategoryDeleteDialog( | ||||
|     ) | ||||
| } | ||||
|  | ||||
| @Composable | ||||
| fun CategorySortAlphabeticallyDialog( | ||||
|     onDismissRequest: () -> Unit, | ||||
|     onSort: () -> Unit, | ||||
| ) { | ||||
|     AlertDialog( | ||||
|         onDismissRequest = onDismissRequest, | ||||
|         confirmButton = { | ||||
|             TextButton(onClick = { | ||||
|                 onSort() | ||||
|                 onDismissRequest() | ||||
|             }) { | ||||
|                 Text(text = stringResource(MR.strings.action_ok)) | ||||
|             } | ||||
|         }, | ||||
|         dismissButton = { | ||||
|             TextButton(onClick = onDismissRequest) { | ||||
|                 Text(text = stringResource(MR.strings.action_cancel)) | ||||
|             } | ||||
|         }, | ||||
|         title = { | ||||
|             Text(text = stringResource(MR.strings.action_sort_category)) | ||||
|         }, | ||||
|         text = { | ||||
|             Text(text = stringResource(MR.strings.sort_category_confirmation)) | ||||
|         }, | ||||
|     ) | ||||
| } | ||||
|  | ||||
| @Composable | ||||
| fun ChangeCategoryDialog( | ||||
|     initialSelection: ImmutableList<CheckboxState<Category>>, | ||||
|   | ||||
| @@ -2,11 +2,14 @@ package eu.kanade.presentation.category.components | ||||
|  | ||||
| import androidx.compose.foundation.clickable | ||||
| import androidx.compose.foundation.layout.Row | ||||
| import androidx.compose.foundation.layout.Spacer | ||||
| import androidx.compose.foundation.layout.fillMaxWidth | ||||
| import androidx.compose.foundation.layout.padding | ||||
| import androidx.compose.material.icons.Icons | ||||
| import androidx.compose.material.icons.automirrored.outlined.Label | ||||
| import androidx.compose.material.icons.outlined.ArrowDropDown | ||||
| import androidx.compose.material.icons.outlined.ArrowDropUp | ||||
| import androidx.compose.material.icons.outlined.Delete | ||||
| import androidx.compose.material.icons.outlined.DragHandle | ||||
| import androidx.compose.material.icons.outlined.Edit | ||||
| import androidx.compose.material3.ElevatedCard | ||||
| import androidx.compose.material3.Icon | ||||
| @@ -16,42 +19,57 @@ import androidx.compose.material3.Text | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.ui.Alignment | ||||
| import androidx.compose.ui.Modifier | ||||
| import sh.calvin.reorderable.ReorderableCollectionItemScope | ||||
| import tachiyomi.domain.category.model.Category | ||||
| import tachiyomi.i18n.MR | ||||
| import tachiyomi.presentation.core.components.material.padding | ||||
| import tachiyomi.presentation.core.i18n.stringResource | ||||
|  | ||||
| @Composable | ||||
| fun ReorderableCollectionItemScope.CategoryListItem( | ||||
| fun CategoryListItem( | ||||
|     category: Category, | ||||
|     canMoveUp: Boolean, | ||||
|     canMoveDown: Boolean, | ||||
|     onMoveUp: (Category) -> Unit, | ||||
|     onMoveDown: (Category) -> Unit, | ||||
|     onRename: () -> Unit, | ||||
|     onDelete: () -> Unit, | ||||
|     modifier: Modifier = Modifier, | ||||
| ) { | ||||
|     ElevatedCard(modifier = modifier) { | ||||
|     ElevatedCard( | ||||
|         modifier = modifier, | ||||
|     ) { | ||||
|         Row( | ||||
|             modifier = Modifier | ||||
|                 .fillMaxWidth() | ||||
|                 .clickable(onClick = onRename) | ||||
|                 .padding(vertical = MaterialTheme.padding.small) | ||||
|                 .clickable { onRename() } | ||||
|                 .padding( | ||||
|                     start = MaterialTheme.padding.small, | ||||
|                     start = MaterialTheme.padding.medium, | ||||
|                     top = MaterialTheme.padding.medium, | ||||
|                     end = MaterialTheme.padding.medium, | ||||
|                 ), | ||||
|             verticalAlignment = Alignment.CenterVertically, | ||||
|         ) { | ||||
|             Icon( | ||||
|                 imageVector = Icons.Outlined.DragHandle, | ||||
|                 contentDescription = null, | ||||
|                 modifier = Modifier | ||||
|                     .padding(MaterialTheme.padding.medium) | ||||
|                     .draggableHandle(), | ||||
|             ) | ||||
|             Icon(imageVector = Icons.AutoMirrored.Outlined.Label, contentDescription = null) | ||||
|             Text( | ||||
|                 text = category.name, | ||||
|                 modifier = Modifier.weight(1f), | ||||
|                 modifier = Modifier | ||||
|                     .padding(start = MaterialTheme.padding.medium), | ||||
|             ) | ||||
|         } | ||||
|         Row { | ||||
|             IconButton( | ||||
|                 onClick = { onMoveUp(category) }, | ||||
|                 enabled = canMoveUp, | ||||
|             ) { | ||||
|                 Icon(imageVector = Icons.Outlined.ArrowDropUp, contentDescription = null) | ||||
|             } | ||||
|             IconButton( | ||||
|                 onClick = { onMoveDown(category) }, | ||||
|                 enabled = canMoveDown, | ||||
|             ) { | ||||
|                 Icon(imageVector = Icons.Outlined.ArrowDropDown, contentDescription = null) | ||||
|             } | ||||
|             Spacer(modifier = Modifier.weight(1f)) | ||||
|             IconButton(onClick = onRename) { | ||||
|                 Icon( | ||||
|                     imageVector = Icons.Outlined.Edit, | ||||
| @@ -59,10 +77,7 @@ fun ReorderableCollectionItemScope.CategoryListItem( | ||||
|                 ) | ||||
|             } | ||||
|             IconButton(onClick = onDelete) { | ||||
|                 Icon( | ||||
|                     imageVector = Icons.Outlined.Delete, | ||||
|                     contentDescription = stringResource(MR.strings.action_delete), | ||||
|                 ) | ||||
|                 Icon(imageVector = Icons.Outlined.Delete, contentDescription = stringResource(MR.strings.action_delete)) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| package eu.kanade.presentation.components | ||||
|  | ||||
| import androidx.activity.compose.BackHandler | ||||
| import androidx.compose.animation.core.tween | ||||
| import androidx.compose.animation.fadeIn | ||||
| import androidx.compose.animation.fadeOut | ||||
| @@ -27,8 +28,8 @@ fun NavigatorAdaptiveSheet( | ||||
|         screen = screen, | ||||
|         content = { sheetNavigator -> | ||||
|             AdaptiveSheet( | ||||
|                 onDismissRequest = onDismissRequest, | ||||
|                 enableSwipeDismiss = enableSwipeDismiss(sheetNavigator), | ||||
|                 onDismissRequest = onDismissRequest, | ||||
|             ) { | ||||
|                 ScreenTransition( | ||||
|                     navigator = sheetNavigator, | ||||
| @@ -37,6 +38,11 @@ fun NavigatorAdaptiveSheet( | ||||
|                             fadeOut(animationSpec = tween(90)) | ||||
|                     }, | ||||
|                 ) | ||||
|  | ||||
|                 BackHandler( | ||||
|                     enabled = sheetNavigator.size > 1, | ||||
|                     onBack = sheetNavigator::pop, | ||||
|                 ) | ||||
|             } | ||||
|  | ||||
|             // Make sure screens are disposed no matter what | ||||
| @@ -73,10 +79,10 @@ fun AdaptiveSheet( | ||||
|         properties = dialogProperties, | ||||
|     ) { | ||||
|         AdaptiveSheetImpl( | ||||
|             modifier = modifier, | ||||
|             isTabletUi = isTabletUi, | ||||
|             enableSwipeDismiss = enableSwipeDismiss, | ||||
|             onDismissRequest = onDismissRequest, | ||||
|             modifier = modifier, | ||||
|         ) { | ||||
|             content() | ||||
|         } | ||||
|   | ||||
| @@ -36,7 +36,6 @@ import androidx.compose.runtime.mutableStateOf | ||||
| import androidx.compose.runtime.remember | ||||
| import androidx.compose.runtime.setValue | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.focus.FocusDirection | ||||
| import androidx.compose.ui.focus.FocusRequester | ||||
| import androidx.compose.ui.focus.focusRequester | ||||
| import androidx.compose.ui.graphics.Color | ||||
| @@ -202,7 +201,6 @@ fun AppBarActions( | ||||
|                 } | ||||
|             }, | ||||
|             state = rememberTooltipState(), | ||||
|             focusable = false, | ||||
|         ) { | ||||
|             IconButton( | ||||
|                 onClick = it.onClick, | ||||
| @@ -227,7 +225,6 @@ fun AppBarActions( | ||||
|                 } | ||||
|             }, | ||||
|             state = rememberTooltipState(), | ||||
|             focusable = false, | ||||
|         ) { | ||||
|             IconButton( | ||||
|                 onClick = { showMenu = !showMenu }, | ||||
| @@ -292,7 +289,6 @@ fun SearchToolbar( | ||||
|                 onSearch(searchQuery) | ||||
|                 focusManager.clearFocus() | ||||
|                 keyboardController?.hide() | ||||
|                 focusManager.moveFocus(FocusDirection.Next) | ||||
|             } | ||||
|  | ||||
|             BasicTextField( | ||||
| @@ -356,7 +352,6 @@ fun SearchToolbar( | ||||
|                             } | ||||
|                         }, | ||||
|                         state = rememberTooltipState(), | ||||
|                         focusable = false, | ||||
|                     ) { | ||||
|                         IconButton( | ||||
|                             onClick = onClick, | ||||
| @@ -376,7 +371,6 @@ fun SearchToolbar( | ||||
|                             } | ||||
|                         }, | ||||
|                         state = rememberTooltipState(), | ||||
|                         focusable = false, | ||||
|                     ) { | ||||
|                         IconButton( | ||||
|                             onClick = { | ||||
|   | ||||
| @@ -1,11 +1,9 @@ | ||||
| package eu.kanade.presentation.components | ||||
|  | ||||
| import androidx.compose.foundation.layout.ColumnScope | ||||
| import androidx.compose.material3.DropdownMenuItem | ||||
| import androidx.compose.material3.Text | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.unit.DpOffset | ||||
| import eu.kanade.presentation.manga.DownloadAction | ||||
| import kotlinx.collections.immutable.persistentListOf | ||||
| import tachiyomi.i18n.MR | ||||
| @@ -17,41 +15,7 @@ fun DownloadDropdownMenu( | ||||
|     expanded: Boolean, | ||||
|     onDismissRequest: () -> Unit, | ||||
|     onDownloadClicked: (DownloadAction) -> Unit, | ||||
|     offset: DpOffset? = null, | ||||
|     modifier: Modifier = Modifier, | ||||
| ) { | ||||
|     if (offset != null) { | ||||
|         DropdownMenu( | ||||
|             expanded = expanded, | ||||
|             onDismissRequest = onDismissRequest, | ||||
|             modifier = modifier, | ||||
|             offset = offset, | ||||
|             content = { | ||||
|                 DownloadDropdownMenuItems( | ||||
|                     onDismissRequest = onDismissRequest, | ||||
|                     onDownloadClicked = onDownloadClicked, | ||||
|                 ) | ||||
|             }, | ||||
|         ) | ||||
|     } else { | ||||
|         DropdownMenu( | ||||
|             expanded = expanded, | ||||
|             onDismissRequest = onDismissRequest, | ||||
|             modifier = modifier, | ||||
|             content = { | ||||
|                 DownloadDropdownMenuItems( | ||||
|                     onDismissRequest = onDismissRequest, | ||||
|                     onDownloadClicked = onDownloadClicked, | ||||
|                 ) | ||||
|             }, | ||||
|         ) | ||||
|     } | ||||
| } | ||||
|  | ||||
| @Composable | ||||
| private fun ColumnScope.DownloadDropdownMenuItems( | ||||
|     onDismissRequest: () -> Unit, | ||||
|     onDownloadClicked: (DownloadAction) -> Unit, | ||||
| ) { | ||||
|     val options = persistentListOf( | ||||
|         DownloadAction.NEXT_1_CHAPTER to pluralStringResource(MR.plurals.download_amount, 1, 1), | ||||
| @@ -61,13 +25,19 @@ private fun ColumnScope.DownloadDropdownMenuItems( | ||||
|         DownloadAction.UNREAD_CHAPTERS to stringResource(MR.strings.download_unread), | ||||
|     ) | ||||
|  | ||||
|     options.map { (downloadAction, string) -> | ||||
|         DropdownMenuItem( | ||||
|             text = { Text(text = string) }, | ||||
|             onClick = { | ||||
|                 onDownloadClicked(downloadAction) | ||||
|                 onDismissRequest() | ||||
|             }, | ||||
|         ) | ||||
|     DropdownMenu( | ||||
|         expanded = expanded, | ||||
|         onDismissRequest = onDismissRequest, | ||||
|         modifier = modifier, | ||||
|     ) { | ||||
|         options.map { (downloadAction, string) -> | ||||
|             DropdownMenuItem( | ||||
|                 text = { Text(text = string) }, | ||||
|                 onClick = { | ||||
|                     onDownloadClicked(downloadAction) | ||||
|                     onDismissRequest() | ||||
|                 }, | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -7,7 +7,6 @@ import androidx.compose.foundation.layout.calculateStartPadding | ||||
| import androidx.compose.foundation.layout.fillMaxSize | ||||
| import androidx.compose.foundation.layout.padding | ||||
| import androidx.compose.foundation.pager.HorizontalPager | ||||
| import androidx.compose.foundation.pager.PagerState | ||||
| import androidx.compose.foundation.pager.rememberPagerState | ||||
| import androidx.compose.material3.MaterialTheme | ||||
| import androidx.compose.material3.PrimaryTabRow | ||||
| @@ -15,6 +14,7 @@ import androidx.compose.material3.SnackbarHost | ||||
| import androidx.compose.material3.SnackbarHostState | ||||
| import androidx.compose.material3.Tab | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.runtime.LaunchedEffect | ||||
| import androidx.compose.runtime.remember | ||||
| import androidx.compose.runtime.rememberCoroutineScope | ||||
| import androidx.compose.ui.Alignment | ||||
| @@ -33,13 +33,20 @@ import tachiyomi.presentation.core.i18n.stringResource | ||||
| fun TabbedScreen( | ||||
|     titleRes: StringResource, | ||||
|     tabs: ImmutableList<TabContent>, | ||||
|     state: PagerState = rememberPagerState { tabs.size }, | ||||
|     startIndex: Int? = null, | ||||
|     searchQuery: String? = null, | ||||
|     onChangeSearchQuery: (String?) -> Unit = {}, | ||||
| ) { | ||||
|     val scope = rememberCoroutineScope() | ||||
|     val state = rememberPagerState { tabs.size } | ||||
|     val snackbarHostState = remember { SnackbarHostState() } | ||||
|  | ||||
|     LaunchedEffect(startIndex) { | ||||
|         if (startIndex != null) { | ||||
|             state.scrollToPage(startIndex) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     Scaffold( | ||||
|         topBar = { | ||||
|             val tab = tabs[state.currentPage] | ||||
|   | ||||
| @@ -38,7 +38,6 @@ fun HistoryScreen( | ||||
|     onSearchQueryChange: (String?) -> Unit, | ||||
|     onClickCover: (mangaId: Long) -> Unit, | ||||
|     onClickResume: (mangaId: Long, chapterId: Long) -> Unit, | ||||
|     onClickFavorite: (mangaId: Long) -> Unit, | ||||
|     onDialogChange: (HistoryScreenModel.Dialog?) -> Unit, | ||||
| ) { | ||||
|     Scaffold( | ||||
| @@ -85,7 +84,6 @@ fun HistoryScreen( | ||||
|                     onClickCover = { history -> onClickCover(history.mangaId) }, | ||||
|                     onClickResume = { history -> onClickResume(history.mangaId, history.chapterId) }, | ||||
|                     onClickDelete = { item -> onDialogChange(HistoryScreenModel.Dialog.Delete(item)) }, | ||||
|                     onClickFavorite = { history -> onClickFavorite(history.mangaId) }, | ||||
|                 ) | ||||
|             } | ||||
|         } | ||||
| @@ -99,7 +97,6 @@ private fun HistoryScreenContent( | ||||
|     onClickCover: (HistoryWithRelations) -> Unit, | ||||
|     onClickResume: (HistoryWithRelations) -> Unit, | ||||
|     onClickDelete: (HistoryWithRelations) -> Unit, | ||||
|     onClickFavorite: (HistoryWithRelations) -> Unit, | ||||
| ) { | ||||
|     FastScrollLazyColumn( | ||||
|         contentPadding = contentPadding, | ||||
| @@ -129,7 +126,6 @@ private fun HistoryScreenContent( | ||||
|                         onClickCover = { onClickCover(value) }, | ||||
|                         onClickResume = { onClickResume(value) }, | ||||
|                         onClickDelete = { onClickDelete(value) }, | ||||
|                         onClickFavorite = { onClickFavorite(value) }, | ||||
|                     ) | ||||
|                 } | ||||
|             } | ||||
| @@ -156,7 +152,6 @@ internal fun HistoryScreenPreviews( | ||||
|             onClickCover = {}, | ||||
|             onClickResume = { _, _ -> run {} }, | ||||
|             onDialogChange = {}, | ||||
|             onClickFavorite = {}, | ||||
|         ) | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -8,7 +8,6 @@ import androidx.compose.foundation.layout.height | ||||
| import androidx.compose.foundation.layout.padding | ||||
| import androidx.compose.material.icons.Icons | ||||
| import androidx.compose.material.icons.outlined.Delete | ||||
| import androidx.compose.material.icons.outlined.FavoriteBorder | ||||
| import androidx.compose.material3.Icon | ||||
| import androidx.compose.material3.IconButton | ||||
| import androidx.compose.material3.MaterialTheme | ||||
| @@ -40,7 +39,6 @@ fun HistoryItem( | ||||
|     onClickCover: () -> Unit, | ||||
|     onClickResume: () -> Unit, | ||||
|     onClickDelete: () -> Unit, | ||||
|     onClickFavorite: () -> Unit, | ||||
|     modifier: Modifier = Modifier, | ||||
| ) { | ||||
|     Row( | ||||
| @@ -84,16 +82,6 @@ fun HistoryItem( | ||||
|             ) | ||||
|         } | ||||
|  | ||||
|         if (!history.coverData.isMangaFavorite) { | ||||
|             IconButton(onClick = onClickFavorite) { | ||||
|                 Icon( | ||||
|                     imageVector = Icons.Outlined.FavoriteBorder, | ||||
|                     contentDescription = stringResource(MR.strings.add_to_library), | ||||
|                     tint = MaterialTheme.colorScheme.onSurface, | ||||
|                 ) | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         IconButton(onClick = onClickDelete) { | ||||
|             Icon( | ||||
|                 imageVector = Icons.Outlined.Delete, | ||||
| @@ -117,7 +105,6 @@ private fun HistoryItemPreviews( | ||||
|                 onClickCover = {}, | ||||
|                 onClickResume = {}, | ||||
|                 onClickDelete = {}, | ||||
|                 onClickFavorite = {}, | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
|   | ||||
| @@ -9,7 +9,6 @@ import androidx.compose.foundation.verticalScroll | ||||
| import androidx.compose.material.icons.Icons | ||||
| import androidx.compose.material.icons.filled.Refresh | ||||
| import androidx.compose.material3.FilterChip | ||||
| import androidx.compose.material3.MaterialTheme | ||||
| import androidx.compose.material3.Text | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.runtime.collectAsState | ||||
| @@ -20,7 +19,8 @@ import androidx.compose.ui.platform.LocalConfiguration | ||||
| import eu.kanade.presentation.components.TabbedDialog | ||||
| import eu.kanade.presentation.components.TabbedDialogPaddings | ||||
| import eu.kanade.tachiyomi.ui.library.LibrarySettingsScreenModel | ||||
| import eu.kanade.tachiyomi.util.system.isReleaseBuildType | ||||
| import eu.kanade.tachiyomi.util.system.isDevFlavor | ||||
| import eu.kanade.tachiyomi.util.system.isPreviewBuildType | ||||
| import kotlinx.collections.immutable.persistentListOf | ||||
| import tachiyomi.core.common.preference.TriState | ||||
| import tachiyomi.domain.category.model.Category | ||||
| @@ -117,7 +117,10 @@ private fun ColumnScope.FilterPage( | ||||
|         onClick = { screenModel.toggleFilter(LibraryPreferences::filterCompleted) }, | ||||
|     ) | ||||
|     // TODO: re-enable when custom intervals are ready for stable | ||||
|     if ((!isReleaseBuildType) && LibraryPreferences.MANGA_OUTSIDE_RELEASE_PERIOD in autoUpdateMangaRestrictions) { | ||||
|     if ( | ||||
|         (isDevFlavor || isPreviewBuildType) && | ||||
|         LibraryPreferences.MANGA_OUTSIDE_RELEASE_PERIOD in autoUpdateMangaRestrictions | ||||
|     ) { | ||||
|         val filterIntervalCustom by screenModel.libraryPreferences.filterIntervalCustom().collectAsState() | ||||
|         TriStateItem( | ||||
|             label = stringResource(MR.strings.action_filter_interval_custom), | ||||
| @@ -163,32 +166,32 @@ private fun ColumnScope.SortPage( | ||||
|     val sortingMode = category.sort.type | ||||
|     val sortDescending = !category.sort.isAscending | ||||
|  | ||||
|     val options = remember(trackers.isEmpty()) { | ||||
|         val trackerMeanPair = if (trackers.isNotEmpty()) { | ||||
|             MR.strings.action_sort_tracker_score to LibrarySort.Type.TrackerMean | ||||
|         } else { | ||||
|             null | ||||
|         } | ||||
|         listOfNotNull( | ||||
|             MR.strings.action_sort_alpha to LibrarySort.Type.Alphabetical, | ||||
|             MR.strings.action_sort_total to LibrarySort.Type.TotalChapters, | ||||
|             MR.strings.action_sort_last_read to LibrarySort.Type.LastRead, | ||||
|             MR.strings.action_sort_last_manga_update to LibrarySort.Type.LastUpdate, | ||||
|             MR.strings.action_sort_unread_count to LibrarySort.Type.UnreadCount, | ||||
|             MR.strings.action_sort_latest_chapter to LibrarySort.Type.LatestChapter, | ||||
|             MR.strings.action_sort_chapter_fetch_date to LibrarySort.Type.ChapterFetchDate, | ||||
|             MR.strings.action_sort_date_added to LibrarySort.Type.DateAdded, | ||||
|             trackerMeanPair, | ||||
|             MR.strings.action_sort_random to LibrarySort.Type.Random, | ||||
|         ) | ||||
|     val trackerSortOption = if (trackers.isEmpty()) { | ||||
|         emptyList() | ||||
|     } else { | ||||
|         listOf(MR.strings.action_sort_tracker_score to LibrarySort.Type.TrackerMean) | ||||
|     } | ||||
|  | ||||
|     options.map { (titleRes, mode) -> | ||||
|     listOf( | ||||
|         MR.strings.action_sort_alpha to LibrarySort.Type.Alphabetical, | ||||
|         MR.strings.action_sort_total to LibrarySort.Type.TotalChapters, | ||||
|         MR.strings.action_sort_last_read to LibrarySort.Type.LastRead, | ||||
|         MR.strings.action_sort_last_manga_update to LibrarySort.Type.LastUpdate, | ||||
|         MR.strings.action_sort_unread_count to LibrarySort.Type.UnreadCount, | ||||
|         MR.strings.action_sort_latest_chapter to LibrarySort.Type.LatestChapter, | ||||
|         MR.strings.action_sort_chapter_fetch_date to LibrarySort.Type.ChapterFetchDate, | ||||
|         MR.strings.action_sort_date_added to LibrarySort.Type.DateAdded, | ||||
|         MR.strings.action_sort_random to LibrarySort.Type.Random, | ||||
|     ).plus(trackerSortOption).map { (titleRes, mode) -> | ||||
|         if (mode == LibrarySort.Type.Random) { | ||||
|             val enabledIcon = if (sortingMode == LibrarySort.Type.Random) { | ||||
|                 Icons.Default.Refresh | ||||
|             } else { | ||||
|                 null | ||||
|             } | ||||
|             BaseSortItem( | ||||
|                 label = stringResource(titleRes), | ||||
|                 icon = Icons.Default.Refresh | ||||
|                     .takeIf { sortingMode == LibrarySort.Type.Random }, | ||||
|                 icon = enabledIcon, | ||||
|                 onClick = { | ||||
|                     screenModel.setSort(category, mode, LibrarySort.Direction.Ascending) | ||||
|                 }, | ||||
| @@ -252,16 +255,15 @@ private fun ColumnScope.DisplayPage( | ||||
|  | ||||
|         val columns by columnPreference.collectAsState() | ||||
|         SliderItem( | ||||
|             value = columns, | ||||
|             valueRange = 0..10, | ||||
|             label = stringResource(MR.strings.pref_library_columns), | ||||
|             max = 10, | ||||
|             value = columns, | ||||
|             valueText = if (columns > 0) { | ||||
|                 columns.toString() | ||||
|                 stringResource(MR.strings.pref_library_columns_per_row, columns) | ||||
|             } else { | ||||
|                 stringResource(MR.strings.label_auto) | ||||
|                 stringResource(MR.strings.label_default) | ||||
|             }, | ||||
|             onChange = columnPreference::set, | ||||
|             pillColor = MaterialTheme.colorScheme.surfaceContainerHighest, | ||||
|         ) | ||||
|     } | ||||
|  | ||||
| @@ -270,10 +272,6 @@ private fun ColumnScope.DisplayPage( | ||||
|         label = stringResource(MR.strings.action_display_download_badge), | ||||
|         pref = screenModel.libraryPreferences.downloadBadge(), | ||||
|     ) | ||||
|     CheckboxItem( | ||||
|         label = stringResource(MR.strings.action_display_unread_badge), | ||||
|         pref = screenModel.libraryPreferences.unreadBadge(), | ||||
|     ) | ||||
|     CheckboxItem( | ||||
|         label = stringResource(MR.strings.action_display_local_badge), | ||||
|         pref = screenModel.libraryPreferences.localBadge(), | ||||
|   | ||||
| @@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.fillMaxSize | ||||
| import androidx.compose.foundation.lazy.grid.items | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.util.fastAny | ||||
| import eu.kanade.tachiyomi.ui.library.LibraryItem | ||||
| import tachiyomi.domain.library.model.LibraryManga | ||||
| import tachiyomi.domain.manga.model.MangaCover | ||||
| @@ -14,7 +15,7 @@ internal fun LibraryComfortableGrid( | ||||
|     items: List<LibraryItem>, | ||||
|     columns: Int, | ||||
|     contentPadding: PaddingValues, | ||||
|     selection: Set<Long>, | ||||
|     selection: List<LibraryManga>, | ||||
|     onClick: (LibraryManga) -> Unit, | ||||
|     onLongClick: (LibraryManga) -> Unit, | ||||
|     onClickContinueReading: ((LibraryManga) -> Unit)?, | ||||
| @@ -34,7 +35,7 @@ internal fun LibraryComfortableGrid( | ||||
|         ) { libraryItem -> | ||||
|             val manga = libraryItem.libraryManga.manga | ||||
|             MangaComfortableGridItem( | ||||
|                 isSelected = manga.id in selection, | ||||
|                 isSelected = selection.fastAny { it.id == libraryItem.libraryManga.id }, | ||||
|                 title = manga.title, | ||||
|                 coverData = MangaCover( | ||||
|                     mangaId = manga.id, | ||||
|   | ||||
| @@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.fillMaxSize | ||||
| import androidx.compose.foundation.lazy.grid.items | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.util.fastAny | ||||
| import eu.kanade.tachiyomi.ui.library.LibraryItem | ||||
| import tachiyomi.domain.library.model.LibraryManga | ||||
| import tachiyomi.domain.manga.model.MangaCover | ||||
| @@ -15,7 +16,7 @@ internal fun LibraryCompactGrid( | ||||
|     showTitle: Boolean, | ||||
|     columns: Int, | ||||
|     contentPadding: PaddingValues, | ||||
|     selection: Set<Long>, | ||||
|     selection: List<LibraryManga>, | ||||
|     onClick: (LibraryManga) -> Unit, | ||||
|     onLongClick: (LibraryManga) -> Unit, | ||||
|     onClickContinueReading: ((LibraryManga) -> Unit)?, | ||||
| @@ -35,7 +36,7 @@ internal fun LibraryCompactGrid( | ||||
|         ) { libraryItem -> | ||||
|             val manga = libraryItem.libraryManga.manga | ||||
|             MangaCompactGridItem( | ||||
|                 isSelected = manga.id in selection, | ||||
|                 isSelected = selection.fastAny { it.id == libraryItem.libraryManga.id }, | ||||
|                 title = manga.title.takeIf { showTitle }, | ||||
|                 coverData = MangaCover( | ||||
|                     mangaId = manga.id, | ||||
|   | ||||
| @@ -29,22 +29,22 @@ import kotlin.time.Duration.Companion.seconds | ||||
| fun LibraryContent( | ||||
|     categories: List<Category>, | ||||
|     searchQuery: String?, | ||||
|     selection: Set<Long>, | ||||
|     selection: List<LibraryManga>, | ||||
|     contentPadding: PaddingValues, | ||||
|     currentPage: Int, | ||||
|     currentPage: () -> Int, | ||||
|     hasActiveFilters: Boolean, | ||||
|     showPageTabs: Boolean, | ||||
|     onChangeCurrentPage: (Int) -> Unit, | ||||
|     onClickManga: (Long) -> Unit, | ||||
|     onMangaClicked: (Long) -> Unit, | ||||
|     onContinueReadingClicked: ((LibraryManga) -> Unit)?, | ||||
|     onToggleSelection: (Category, LibraryManga) -> Unit, | ||||
|     onToggleRangeSelection: (Category, LibraryManga) -> Unit, | ||||
|     onRefresh: () -> Boolean, | ||||
|     onToggleSelection: (LibraryManga) -> Unit, | ||||
|     onToggleRangeSelection: (LibraryManga) -> Unit, | ||||
|     onRefresh: (Category?) -> Boolean, | ||||
|     onGlobalSearchClicked: () -> Unit, | ||||
|     getItemCountForCategory: (Category) -> Int?, | ||||
|     getNumberOfMangaForCategory: (Category) -> Int?, | ||||
|     getDisplayMode: (Int) -> PreferenceMutableState<LibraryDisplayMode>, | ||||
|     getColumnsForOrientation: (Boolean) -> PreferenceMutableState<Int>, | ||||
|     getItemsForCategory: (Category) -> List<LibraryItem>, | ||||
|     getLibraryForPage: (Int) -> List<LibraryItem>, | ||||
| ) { | ||||
|     Column( | ||||
|         modifier = Modifier.padding( | ||||
| @@ -53,12 +53,13 @@ fun LibraryContent( | ||||
|             end = contentPadding.calculateEndPadding(LocalLayoutDirection.current), | ||||
|         ), | ||||
|     ) { | ||||
|         val pagerState = rememberPagerState(currentPage) { categories.size } | ||||
|         val coercedCurrentPage = remember { currentPage().coerceAtMost(categories.lastIndex) } | ||||
|         val pagerState = rememberPagerState(coercedCurrentPage) { categories.size } | ||||
|  | ||||
|         val scope = rememberCoroutineScope() | ||||
|         var isRefreshing by remember(pagerState.currentPage) { mutableStateOf(false) } | ||||
|  | ||||
|         if (showPageTabs && categories.isNotEmpty() && (categories.size > 1 || !categories.first().isSystemCategory)) { | ||||
|         if (showPageTabs && categories.size > 1) { | ||||
|             LaunchedEffect(categories) { | ||||
|                 if (categories.size <= pagerState.currentPage) { | ||||
|                     pagerState.scrollToPage(categories.size - 1) | ||||
| @@ -67,20 +68,23 @@ fun LibraryContent( | ||||
|             LibraryTabs( | ||||
|                 categories = categories, | ||||
|                 pagerState = pagerState, | ||||
|                 getItemCountForCategory = getItemCountForCategory, | ||||
|                 onTabItemClick = { | ||||
|                     scope.launch { | ||||
|                         pagerState.animateScrollToPage(it) | ||||
|                     } | ||||
|                 }, | ||||
|             ) | ||||
|                 getNumberOfMangaForCategory = getNumberOfMangaForCategory, | ||||
|             ) { scope.launch { pagerState.animateScrollToPage(it) } } | ||||
|         } | ||||
|  | ||||
|         val notSelectionMode = selection.isEmpty() | ||||
|         val onClickManga = { manga: LibraryManga -> | ||||
|             if (notSelectionMode) { | ||||
|                 onMangaClicked(manga.manga.id) | ||||
|             } else { | ||||
|                 onToggleSelection(manga) | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         PullRefresh( | ||||
|             refreshing = isRefreshing, | ||||
|             enabled = selection.isEmpty(), | ||||
|             onRefresh = { | ||||
|                 val started = onRefresh() | ||||
|                 val started = onRefresh(categories[currentPage()]) | ||||
|                 if (!started) return@PullRefresh | ||||
|                 scope.launch { | ||||
|                     // Fake refresh status but hide it after a second as it's a long running task | ||||
| @@ -89,25 +93,19 @@ fun LibraryContent( | ||||
|                     isRefreshing = false | ||||
|                 } | ||||
|             }, | ||||
|             enabled = notSelectionMode, | ||||
|         ) { | ||||
|             LibraryPager( | ||||
|                 state = pagerState, | ||||
|                 contentPadding = PaddingValues(bottom = contentPadding.calculateBottomPadding()), | ||||
|                 hasActiveFilters = hasActiveFilters, | ||||
|                 selection = selection, | ||||
|                 selectedManga = selection, | ||||
|                 searchQuery = searchQuery, | ||||
|                 onGlobalSearchClicked = onGlobalSearchClicked, | ||||
|                 getCategoryForPage = { page -> categories[page] }, | ||||
|                 getDisplayMode = getDisplayMode, | ||||
|                 getColumnsForOrientation = getColumnsForOrientation, | ||||
|                 getItemsForCategory = getItemsForCategory, | ||||
|                 onClickManga = { category, manga -> | ||||
|                     if (selection.isNotEmpty()) { | ||||
|                         onToggleSelection(category, manga) | ||||
|                     } else { | ||||
|                         onClickManga(manga.manga.id) | ||||
|                     } | ||||
|                 }, | ||||
|                 getLibraryForPage = getLibraryForPage, | ||||
|                 onClickManga = onClickManga, | ||||
|                 onLongClickManga = onToggleRangeSelection, | ||||
|                 onClickContinueReading = onContinueReadingClicked, | ||||
|             ) | ||||
|   | ||||
| @@ -7,6 +7,7 @@ import androidx.compose.foundation.lazy.items | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.unit.dp | ||||
| import androidx.compose.ui.util.fastAny | ||||
| import eu.kanade.tachiyomi.ui.library.LibraryItem | ||||
| import tachiyomi.domain.library.model.LibraryManga | ||||
| import tachiyomi.domain.manga.model.MangaCover | ||||
| @@ -17,7 +18,7 @@ import tachiyomi.presentation.core.util.plus | ||||
| internal fun LibraryList( | ||||
|     items: List<LibraryItem>, | ||||
|     contentPadding: PaddingValues, | ||||
|     selection: Set<Long>, | ||||
|     selection: List<LibraryManga>, | ||||
|     onClick: (LibraryManga) -> Unit, | ||||
|     onLongClick: (LibraryManga) -> Unit, | ||||
|     onClickContinueReading: ((LibraryManga) -> Unit)?, | ||||
| @@ -44,7 +45,7 @@ internal fun LibraryList( | ||||
|         ) { libraryItem -> | ||||
|             val manga = libraryItem.libraryManga.manga | ||||
|             MangaListItem( | ||||
|                 isSelected = manga.id in selection, | ||||
|                 isSelected = selection.fastAny { it.id == libraryItem.libraryManga.id }, | ||||
|                 title = manga.title, | ||||
|                 coverData = MangaCover( | ||||
|                     mangaId = manga.id, | ||||
|   | ||||
| @@ -20,7 +20,6 @@ import androidx.compose.ui.platform.LocalConfiguration | ||||
| import androidx.compose.ui.unit.dp | ||||
| import eu.kanade.core.preference.PreferenceMutableState | ||||
| import eu.kanade.tachiyomi.ui.library.LibraryItem | ||||
| import tachiyomi.domain.category.model.Category | ||||
| import tachiyomi.domain.library.model.LibraryDisplayMode | ||||
| import tachiyomi.domain.library.model.LibraryManga | ||||
| import tachiyomi.i18n.MR | ||||
| @@ -32,15 +31,14 @@ fun LibraryPager( | ||||
|     state: PagerState, | ||||
|     contentPadding: PaddingValues, | ||||
|     hasActiveFilters: Boolean, | ||||
|     selection: Set<Long>, | ||||
|     selectedManga: List<LibraryManga>, | ||||
|     searchQuery: String?, | ||||
|     onGlobalSearchClicked: () -> Unit, | ||||
|     getCategoryForPage: (Int) -> Category, | ||||
|     getDisplayMode: (Int) -> PreferenceMutableState<LibraryDisplayMode>, | ||||
|     getColumnsForOrientation: (Boolean) -> PreferenceMutableState<Int>, | ||||
|     getItemsForCategory: (Category) -> List<LibraryItem>, | ||||
|     onClickManga: (Category, LibraryManga) -> Unit, | ||||
|     onLongClickManga: (Category, LibraryManga) -> Unit, | ||||
|     getLibraryForPage: (Int) -> List<LibraryItem>, | ||||
|     onClickManga: (LibraryManga) -> Unit, | ||||
|     onLongClickManga: (LibraryManga) -> Unit, | ||||
|     onClickContinueReading: ((LibraryManga) -> Unit)?, | ||||
| ) { | ||||
|     HorizontalPager( | ||||
| @@ -52,10 +50,9 @@ fun LibraryPager( | ||||
|             // To make sure only one offscreen page is being composed | ||||
|             return@HorizontalPager | ||||
|         } | ||||
|         val category = getCategoryForPage(page) | ||||
|         val items = getItemsForCategory(category) | ||||
|         val library = getLibraryForPage(page) | ||||
|  | ||||
|         if (items.isEmpty()) { | ||||
|         if (library.isEmpty()) { | ||||
|             LibraryPagerEmptyScreen( | ||||
|                 searchQuery = searchQuery, | ||||
|                 hasActiveFilters = hasActiveFilters, | ||||
| @@ -75,15 +72,12 @@ fun LibraryPager( | ||||
|             remember { mutableIntStateOf(0) } | ||||
|         } | ||||
|  | ||||
|         val onClickManga: (LibraryManga) -> Unit = { onClickManga(category, it) } | ||||
|         val onLongClickManga: (LibraryManga) -> Unit = { onLongClickManga(category, it) } | ||||
|  | ||||
|         when (displayMode) { | ||||
|             LibraryDisplayMode.List -> { | ||||
|                 LibraryList( | ||||
|                     items = items, | ||||
|                     items = library, | ||||
|                     contentPadding = contentPadding, | ||||
|                     selection = selection, | ||||
|                     selection = selectedManga, | ||||
|                     onClick = onClickManga, | ||||
|                     onLongClick = onLongClickManga, | ||||
|                     onClickContinueReading = onClickContinueReading, | ||||
| @@ -93,11 +87,11 @@ fun LibraryPager( | ||||
|             } | ||||
|             LibraryDisplayMode.CompactGrid, LibraryDisplayMode.CoverOnlyGrid -> { | ||||
|                 LibraryCompactGrid( | ||||
|                     items = items, | ||||
|                     items = library, | ||||
|                     showTitle = displayMode is LibraryDisplayMode.CompactGrid, | ||||
|                     columns = columns, | ||||
|                     contentPadding = contentPadding, | ||||
|                     selection = selection, | ||||
|                     selection = selectedManga, | ||||
|                     onClick = onClickManga, | ||||
|                     onLongClick = onLongClickManga, | ||||
|                     onClickContinueReading = onClickContinueReading, | ||||
| @@ -107,10 +101,10 @@ fun LibraryPager( | ||||
|             } | ||||
|             LibraryDisplayMode.ComfortableGrid -> { | ||||
|                 LibraryComfortableGrid( | ||||
|                     items = items, | ||||
|                     items = library, | ||||
|                     columns = columns, | ||||
|                     contentPadding = contentPadding, | ||||
|                     selection = selection, | ||||
|                     selection = selectedManga, | ||||
|                     onClick = onClickManga, | ||||
|                     onLongClick = onLongClickManga, | ||||
|                     onClickContinueReading = onClickContinueReading, | ||||
|   | ||||
| @@ -18,13 +18,14 @@ import tachiyomi.presentation.core.components.material.TabText | ||||
| internal fun LibraryTabs( | ||||
|     categories: List<Category>, | ||||
|     pagerState: PagerState, | ||||
|     getItemCountForCategory: (Category) -> Int?, | ||||
|     getNumberOfMangaForCategory: (Category) -> Int?, | ||||
|     onTabItemClick: (Int) -> Unit, | ||||
| ) { | ||||
|     val currentPageIndex = pagerState.currentPage.coerceAtMost(categories.lastIndex) | ||||
|     Column(modifier = Modifier.zIndex(2f)) { | ||||
|     Column( | ||||
|         modifier = Modifier.zIndex(1f), | ||||
|     ) { | ||||
|         PrimaryScrollableTabRow( | ||||
|             selectedTabIndex = currentPageIndex, | ||||
|             selectedTabIndex = pagerState.currentPage, | ||||
|             edgePadding = 0.dp, | ||||
|             // TODO: use default when width is fixed upstream | ||||
|             // https://issuetracker.google.com/issues/242879624 | ||||
| @@ -32,12 +33,12 @@ internal fun LibraryTabs( | ||||
|         ) { | ||||
|             categories.forEachIndexed { index, category -> | ||||
|                 Tab( | ||||
|                     selected = currentPageIndex == index, | ||||
|                     selected = pagerState.currentPage == index, | ||||
|                     onClick = { onTabItemClick(index) }, | ||||
|                     text = { | ||||
|                         TabText( | ||||
|                             text = category.visualName, | ||||
|                             badgeCount = getItemCountForCategory(category), | ||||
|                             badgeCount = getNumberOfMangaForCategory(category), | ||||
|                         ) | ||||
|                     }, | ||||
|                     unselectedContentColor = MaterialTheme.colorScheme.onSurface, | ||||
|   | ||||
| @@ -21,14 +21,13 @@ 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.saveable.rememberSaveable | ||||
| import androidx.compose.runtime.setValue | ||||
| import androidx.compose.ui.Alignment | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.unit.dp | ||||
| import eu.kanade.domain.base.BasePreferences | ||||
| import eu.kanade.domain.manga.model.downloadedFilter | ||||
| import eu.kanade.domain.manga.model.forceDownloaded | ||||
| import eu.kanade.presentation.components.TabbedDialog | ||||
| import eu.kanade.presentation.components.TabbedDialogPaddings | ||||
| import kotlinx.collections.immutable.persistentListOf | ||||
| @@ -41,8 +40,6 @@ import tachiyomi.presentation.core.components.SortItem | ||||
| import tachiyomi.presentation.core.components.TriStateItem | ||||
| import tachiyomi.presentation.core.i18n.stringResource | ||||
| import tachiyomi.presentation.core.theme.active | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
|  | ||||
| @Composable | ||||
| fun ChapterSettingsDialog( | ||||
| @@ -66,8 +63,6 @@ fun ChapterSettingsDialog( | ||||
|         ) | ||||
|     } | ||||
|  | ||||
|     val downloadedOnly = remember { Injekt.get<BasePreferences>().downloadedOnly().get() } | ||||
|  | ||||
|     TabbedDialog( | ||||
|         onDismissRequest = onDismissRequest, | ||||
|         tabTitles = persistentListOf( | ||||
| @@ -102,7 +97,7 @@ fun ChapterSettingsDialog( | ||||
|                     FilterPage( | ||||
|                         downloadFilter = manga?.downloadedFilter ?: TriState.DISABLED, | ||||
|                         onDownloadFilterChanged = onDownloadFilterChanged | ||||
|                             .takeUnless { downloadedOnly }, | ||||
|                             .takeUnless { manga?.forceDownloaded() == true }, | ||||
|                         unreadFilter = manga?.unreadFilter ?: TriState.DISABLED, | ||||
|                         onUnreadFilterChanged = onUnreadFilterChanged, | ||||
|                         bookmarkedFilter = manga?.bookmarkedFilter ?: TriState.DISABLED, | ||||
|   | ||||
| @@ -1,95 +1,44 @@ | ||||
| package eu.kanade.presentation.manga | ||||
|  | ||||
| import androidx.compose.foundation.background | ||||
| import androidx.compose.foundation.combinedClickable | ||||
| 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.PaddingValues | ||||
| 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.heightIn | ||||
| import androidx.compose.foundation.layout.padding | ||||
| import androidx.compose.foundation.layout.size | ||||
| import androidx.compose.foundation.layout.width | ||||
| import androidx.compose.foundation.lazy.LazyRow | ||||
| import androidx.compose.foundation.lazy.items | ||||
| import androidx.compose.foundation.rememberScrollState | ||||
| import androidx.compose.foundation.shape.CircleShape | ||||
| import androidx.compose.foundation.verticalScroll | ||||
| import androidx.compose.foundation.layout.sizeIn | ||||
| import androidx.compose.material.icons.Icons | ||||
| import androidx.compose.material.icons.filled.Brush | ||||
| import androidx.compose.material.icons.filled.PersonOutline | ||||
| import androidx.compose.material.icons.filled.Warning | ||||
| import androidx.compose.material.icons.outlined.Add | ||||
| import androidx.compose.material.icons.outlined.AttachMoney | ||||
| import androidx.compose.material.icons.outlined.Block | ||||
| import androidx.compose.material.icons.outlined.Close | ||||
| import androidx.compose.material.icons.outlined.Done | ||||
| import androidx.compose.material.icons.outlined.DoneAll | ||||
| import androidx.compose.material.icons.outlined.Pause | ||||
| import androidx.compose.material.icons.outlined.Schedule | ||||
| import androidx.compose.material.icons.outlined.Book | ||||
| import androidx.compose.material.icons.outlined.SwapVert | ||||
| import androidx.compose.material3.HorizontalDivider | ||||
| import androidx.compose.material3.Icon | ||||
| import androidx.compose.material3.MaterialTheme | ||||
| import androidx.compose.material3.OutlinedButton | ||||
| import androidx.compose.material3.Text | ||||
| import androidx.compose.material3.Typography | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.runtime.remember | ||||
| import androidx.compose.ui.Alignment | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.draw.clip | ||||
| import androidx.compose.ui.graphics.vector.ImageVector | ||||
| import androidx.compose.ui.platform.LocalContext | ||||
| import androidx.compose.ui.platform.LocalDensity | ||||
| import androidx.compose.ui.text.TextMeasurer | ||||
| import androidx.compose.ui.text.TextStyle | ||||
| import androidx.compose.ui.text.rememberTextMeasurer | ||||
| import androidx.compose.ui.text.style.TextOverflow | ||||
| import androidx.compose.ui.unit.Constraints | ||||
| import androidx.compose.ui.unit.Density | ||||
| import androidx.compose.ui.unit.Dp | ||||
| import androidx.compose.ui.unit.dp | ||||
| import androidx.compose.ui.util.fastMaxOfOrNull | ||||
| import coil3.request.ImageRequest | ||||
| import coil3.request.crossfade | ||||
| import androidx.compose.ui.unit.sp | ||||
| import eu.kanade.presentation.components.AdaptiveSheet | ||||
| import eu.kanade.presentation.components.TabbedDialogPaddings | ||||
| import eu.kanade.presentation.manga.components.MangaCover | ||||
| import eu.kanade.presentation.more.settings.LocalPreferenceMinHeight | ||||
| import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget | ||||
| import eu.kanade.tachiyomi.source.Source | ||||
| import eu.kanade.tachiyomi.source.model.SManga | ||||
| import tachiyomi.domain.manga.model.Manga | ||||
| import tachiyomi.domain.manga.model.MangaWithChapterCount | ||||
| import tachiyomi.domain.source.model.StubSource | ||||
| import tachiyomi.domain.source.service.SourceManager | ||||
| import tachiyomi.i18n.MR | ||||
| import tachiyomi.presentation.core.components.Badge | ||||
| import tachiyomi.presentation.core.components.BadgeGroup | ||||
| import tachiyomi.presentation.core.components.material.padding | ||||
| import tachiyomi.presentation.core.i18n.pluralStringResource | ||||
| import tachiyomi.presentation.core.i18n.stringResource | ||||
| import tachiyomi.presentation.core.util.secondaryItemAlpha | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
|  | ||||
| @Composable | ||||
| fun DuplicateMangaDialog( | ||||
|     duplicates: List<MangaWithChapterCount>, | ||||
|     onDismissRequest: () -> Unit, | ||||
|     onConfirm: () -> Unit, | ||||
|     onOpenManga: (manga: Manga) -> Unit, | ||||
|     onMigrate: (manga: Manga) -> Unit, | ||||
|     onOpenManga: () -> Unit, | ||||
|     onMigrate: () -> Unit, | ||||
|     modifier: Modifier = Modifier, | ||||
| ) { | ||||
|     val sourceManager = remember { Injekt.get<SourceManager>() } | ||||
|     val minHeight = LocalPreferenceMinHeight.current | ||||
|     val horizontalPadding = PaddingValues(horizontal = TabbedDialogPaddings.Horizontal) | ||||
|     val horizontalPaddingModifier = Modifier.padding(horizontalPadding) | ||||
|  | ||||
|     AdaptiveSheet( | ||||
|         modifier = modifier, | ||||
| @@ -97,310 +46,81 @@ fun DuplicateMangaDialog( | ||||
|     ) { | ||||
|         Column( | ||||
|             modifier = Modifier | ||||
|                 .padding(vertical = TabbedDialogPaddings.Vertical) | ||||
|                 .verticalScroll(rememberScrollState()) | ||||
|                 .padding( | ||||
|                     vertical = TabbedDialogPaddings.Vertical, | ||||
|                     horizontal = TabbedDialogPaddings.Horizontal, | ||||
|                 ) | ||||
|                 .fillMaxWidth(), | ||||
|             verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.medium), | ||||
|         ) { | ||||
|             Text( | ||||
|                 text = stringResource(MR.strings.possible_duplicates_title), | ||||
|                 modifier = Modifier.padding(TitlePadding), | ||||
|                 text = stringResource(MR.strings.are_you_sure), | ||||
|                 style = MaterialTheme.typography.headlineMedium, | ||||
|                 modifier = Modifier | ||||
|                     .then(horizontalPaddingModifier) | ||||
|                     .padding(top = MaterialTheme.padding.small), | ||||
|             ) | ||||
|  | ||||
|             Text( | ||||
|                 text = stringResource(MR.strings.possible_duplicates_summary), | ||||
|                 text = stringResource(MR.strings.confirm_add_duplicate_manga), | ||||
|                 style = MaterialTheme.typography.bodyMedium, | ||||
|                 modifier = Modifier.then(horizontalPaddingModifier), | ||||
|             ) | ||||
|  | ||||
|             LazyRow( | ||||
|                 horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small), | ||||
|                 modifier = Modifier.height(getMaximumMangaCardHeight(duplicates)), | ||||
|                 contentPadding = horizontalPadding, | ||||
|             ) { | ||||
|                 items( | ||||
|                     items = duplicates, | ||||
|                     key = { it.manga.id }, | ||||
|                 ) { | ||||
|                     DuplicateMangaListItem( | ||||
|                         duplicate = it, | ||||
|                         getSource = { sourceManager.getOrStub(it.manga.source) }, | ||||
|                         onMigrate = { onMigrate(it.manga) }, | ||||
|                         onDismissRequest = onDismissRequest, | ||||
|                         onOpenManga = { onOpenManga(it.manga) }, | ||||
|                     ) | ||||
|                 } | ||||
|             } | ||||
|             Spacer(Modifier.height(PaddingSize)) | ||||
|  | ||||
|             Column(modifier = horizontalPaddingModifier) { | ||||
|                 HorizontalDivider() | ||||
|             TextPreferenceWidget( | ||||
|                 title = stringResource(MR.strings.action_show_manga), | ||||
|                 icon = Icons.Outlined.Book, | ||||
|                 onPreferenceClick = { | ||||
|                     onDismissRequest() | ||||
|                     onOpenManga() | ||||
|                 }, | ||||
|             ) | ||||
|  | ||||
|                 TextPreferenceWidget( | ||||
|                     title = stringResource(MR.strings.action_add_anyway), | ||||
|                     icon = Icons.Outlined.Add, | ||||
|                     onPreferenceClick = { | ||||
|                         onDismissRequest() | ||||
|                         onConfirm() | ||||
|                     }, | ||||
|                     modifier = Modifier.clip(CircleShape), | ||||
|                 ) | ||||
|             } | ||||
|             HorizontalDivider() | ||||
|  | ||||
|             OutlinedButton( | ||||
|                 onClick = onDismissRequest, | ||||
|                 modifier = Modifier | ||||
|                     .then(horizontalPaddingModifier) | ||||
|                     .padding(bottom = MaterialTheme.padding.medium) | ||||
|                     .heightIn(min = minHeight) | ||||
|                     .fillMaxWidth(), | ||||
|             ) { | ||||
|                 Text( | ||||
|                     modifier = Modifier.padding(vertical = MaterialTheme.padding.extraSmall), | ||||
|                     text = stringResource(MR.strings.action_cancel), | ||||
|                     color = MaterialTheme.colorScheme.primary, | ||||
|                     style = MaterialTheme.typography.bodyLarge, | ||||
|                 ) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @Composable | ||||
| private fun DuplicateMangaListItem( | ||||
|     duplicate: MangaWithChapterCount, | ||||
|     getSource: () -> Source, | ||||
|     onDismissRequest: () -> Unit, | ||||
|     onOpenManga: () -> Unit, | ||||
|     onMigrate: () -> Unit, | ||||
| ) { | ||||
|     val source = getSource() | ||||
|     val manga = duplicate.manga | ||||
|     Column( | ||||
|         modifier = Modifier | ||||
|             .width(MangaCardWidth) | ||||
|             .clip(MaterialTheme.shapes.medium) | ||||
|             .background(MaterialTheme.colorScheme.surface) | ||||
|             .combinedClickable( | ||||
|                 onLongClick = { onOpenManga() }, | ||||
|                 onClick = { | ||||
|             TextPreferenceWidget( | ||||
|                 title = stringResource(MR.strings.action_migrate_duplicate), | ||||
|                 icon = Icons.Outlined.SwapVert, | ||||
|                 onPreferenceClick = { | ||||
|                     onDismissRequest() | ||||
|                     onMigrate() | ||||
|                 }, | ||||
|             ) | ||||
|             .padding(MaterialTheme.padding.small), | ||||
|     ) { | ||||
|         Box { | ||||
|             MangaCover.Book( | ||||
|                 data = ImageRequest.Builder(LocalContext.current) | ||||
|                     .data(manga) | ||||
|                     .crossfade(true) | ||||
|                     .build(), | ||||
|                 modifier = Modifier.fillMaxWidth(), | ||||
|  | ||||
|             HorizontalDivider() | ||||
|  | ||||
|             TextPreferenceWidget( | ||||
|                 title = stringResource(MR.strings.action_add_anyway), | ||||
|                 icon = Icons.Outlined.Add, | ||||
|                 onPreferenceClick = { | ||||
|                     onDismissRequest() | ||||
|                     onConfirm() | ||||
|                 }, | ||||
|             ) | ||||
|             BadgeGroup( | ||||
|  | ||||
|             Row( | ||||
|                 modifier = Modifier | ||||
|                     .padding(4.dp) | ||||
|                     .align(Alignment.TopStart), | ||||
|                     .sizeIn(minHeight = minHeight) | ||||
|                     .clickable { onDismissRequest.invoke() } | ||||
|                     .padding(ButtonPadding) | ||||
|                     .fillMaxWidth(), | ||||
|                 verticalAlignment = Alignment.CenterVertically, | ||||
|                 horizontalArrangement = Arrangement.Center, | ||||
|             ) { | ||||
|                 Badge( | ||||
|                     color = MaterialTheme.colorScheme.secondary, | ||||
|                     textColor = MaterialTheme.colorScheme.onSecondary, | ||||
|                     text = pluralStringResource( | ||||
|                         MR.plurals.manga_num_chapters, | ||||
|                         duplicate.chapterCount.toInt(), | ||||
|                         duplicate.chapterCount, | ||||
|                     ), | ||||
|                 ) | ||||
|                 OutlinedButton(onClick = onDismissRequest, modifier = Modifier.fillMaxWidth()) { | ||||
|                     Text( | ||||
|                         modifier = Modifier | ||||
|                             .padding(vertical = 8.dp), | ||||
|                         text = stringResource(MR.strings.action_cancel), | ||||
|                         color = MaterialTheme.colorScheme.primary, | ||||
|                         style = MaterialTheme.typography.titleLarge, | ||||
|                         fontSize = 16.sp, | ||||
|                     ) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         Spacer(modifier = Modifier.height(MaterialTheme.padding.extraSmall)) | ||||
|  | ||||
|         Text( | ||||
|             text = manga.title, | ||||
|             style = MaterialTheme.typography.titleSmall, | ||||
|             overflow = TextOverflow.Ellipsis, | ||||
|             maxLines = 2, | ||||
|         ) | ||||
|  | ||||
|         if (!manga.author.isNullOrBlank()) { | ||||
|             MangaDetailRow( | ||||
|                 text = manga.author!!, | ||||
|                 iconImageVector = Icons.Filled.PersonOutline, | ||||
|                 maxLines = 2, | ||||
|             ) | ||||
|         } | ||||
|  | ||||
|         if (!manga.artist.isNullOrBlank() && manga.author != manga.artist) { | ||||
|             MangaDetailRow( | ||||
|                 text = manga.artist!!, | ||||
|                 iconImageVector = Icons.Filled.Brush, | ||||
|                 maxLines = 2, | ||||
|             ) | ||||
|         } | ||||
|  | ||||
|         MangaDetailRow( | ||||
|             text = when (manga.status) { | ||||
|                 SManga.ONGOING.toLong() -> stringResource(MR.strings.ongoing) | ||||
|                 SManga.COMPLETED.toLong() -> stringResource(MR.strings.completed) | ||||
|                 SManga.LICENSED.toLong() -> stringResource(MR.strings.licensed) | ||||
|                 SManga.PUBLISHING_FINISHED.toLong() -> stringResource(MR.strings.publishing_finished) | ||||
|                 SManga.CANCELLED.toLong() -> stringResource(MR.strings.cancelled) | ||||
|                 SManga.ON_HIATUS.toLong() -> stringResource(MR.strings.on_hiatus) | ||||
|                 else -> stringResource(MR.strings.unknown) | ||||
|             }, | ||||
|             iconImageVector = when (manga.status) { | ||||
|                 SManga.ONGOING.toLong() -> Icons.Outlined.Schedule | ||||
|                 SManga.COMPLETED.toLong() -> Icons.Outlined.DoneAll | ||||
|                 SManga.LICENSED.toLong() -> Icons.Outlined.AttachMoney | ||||
|                 SManga.PUBLISHING_FINISHED.toLong() -> Icons.Outlined.Done | ||||
|                 SManga.CANCELLED.toLong() -> Icons.Outlined.Close | ||||
|                 SManga.ON_HIATUS.toLong() -> Icons.Outlined.Pause | ||||
|                 else -> Icons.Outlined.Block | ||||
|             }, | ||||
|         ) | ||||
|  | ||||
|         Spacer(modifier = Modifier.weight(1f)) | ||||
|  | ||||
|         Row( | ||||
|             modifier = Modifier.fillMaxWidth(), | ||||
|             horizontalArrangement = Arrangement.Center, | ||||
|         ) { | ||||
|             if (source is StubSource) { | ||||
|                 Icon( | ||||
|                     imageVector = Icons.Filled.Warning, | ||||
|                     contentDescription = null, | ||||
|                     modifier = Modifier.size(16.dp), | ||||
|                     tint = MaterialTheme.colorScheme.error, | ||||
|                 ) | ||||
|             } | ||||
|             Text( | ||||
|                 text = source.name, | ||||
|                 style = MaterialTheme.typography.labelSmall, | ||||
|                 overflow = TextOverflow.Ellipsis, | ||||
|                 maxLines = 1, | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @Composable | ||||
| private fun MangaDetailRow( | ||||
|     text: String, | ||||
|     iconImageVector: ImageVector, | ||||
|     maxLines: Int = 1, | ||||
| ) { | ||||
|     Row( | ||||
|         modifier = Modifier | ||||
|             .secondaryItemAlpha() | ||||
|             .padding(top = MaterialTheme.padding.extraSmall), | ||||
|         horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall), | ||||
|         verticalAlignment = Alignment.CenterVertically, | ||||
|     ) { | ||||
|         Icon( | ||||
|             imageVector = iconImageVector, | ||||
|             contentDescription = null, | ||||
|             modifier = Modifier.size(MangaDetailsIconWidth), | ||||
|         ) | ||||
|         Text( | ||||
|             text = text, | ||||
|             style = MaterialTheme.typography.bodySmall, | ||||
|             overflow = TextOverflow.Ellipsis, | ||||
|             maxLines = maxLines, | ||||
|         ) | ||||
|     } | ||||
| } | ||||
| private val PaddingSize = 16.dp | ||||
|  | ||||
| @Composable | ||||
| private fun getMaximumMangaCardHeight(duplicates: List<MangaWithChapterCount>): Dp { | ||||
|     val density = LocalDensity.current | ||||
|     val typography = MaterialTheme.typography | ||||
|     val textMeasurer = rememberTextMeasurer() | ||||
|  | ||||
|     val smallPadding = with(density) { MaterialTheme.padding.small.roundToPx() } | ||||
|     val extraSmallPadding = with(density) { MaterialTheme.padding.extraSmall.roundToPx() } | ||||
|  | ||||
|     val width = with(density) { MangaCardWidth.roundToPx() - (2 * smallPadding) } | ||||
|     val iconWidth = with(density) { MangaDetailsIconWidth.roundToPx() } | ||||
|  | ||||
|     val coverHeight = width / MangaCover.Book.ratio | ||||
|     val constraints = Constraints(maxWidth = width) | ||||
|     val detailsConstraints = Constraints(maxWidth = width - iconWidth - extraSmallPadding) | ||||
|  | ||||
|     return remember( | ||||
|         duplicates, | ||||
|         density, | ||||
|         typography, | ||||
|         textMeasurer, | ||||
|         smallPadding, | ||||
|         extraSmallPadding, | ||||
|         coverHeight, | ||||
|         constraints, | ||||
|         detailsConstraints, | ||||
|     ) { | ||||
|         duplicates.fastMaxOfOrNull { | ||||
|             calculateMangaCardHeight( | ||||
|                 manga = it.manga, | ||||
|                 density = density, | ||||
|                 typography = typography, | ||||
|                 textMeasurer = textMeasurer, | ||||
|                 smallPadding = smallPadding, | ||||
|                 extraSmallPadding = extraSmallPadding, | ||||
|                 coverHeight = coverHeight, | ||||
|                 constraints = constraints, | ||||
|                 detailsConstraints = detailsConstraints, | ||||
|             ) | ||||
|         } | ||||
|             ?: 0.dp | ||||
|     } | ||||
| } | ||||
|  | ||||
| private fun calculateMangaCardHeight( | ||||
|     manga: Manga, | ||||
|     density: Density, | ||||
|     typography: Typography, | ||||
|     textMeasurer: TextMeasurer, | ||||
|     smallPadding: Int, | ||||
|     extraSmallPadding: Int, | ||||
|     coverHeight: Float, | ||||
|     constraints: Constraints, | ||||
|     detailsConstraints: Constraints, | ||||
| ): Dp { | ||||
|     val titleHeight = textMeasurer.measureHeight(manga.title, typography.titleSmall, 2, constraints) | ||||
|     val authorHeight = if (!manga.author.isNullOrBlank()) { | ||||
|         textMeasurer.measureHeight(manga.author!!, typography.bodySmall, 2, detailsConstraints) | ||||
|     } else { | ||||
|         0 | ||||
|     } | ||||
|     val artistHeight = if (!manga.artist.isNullOrBlank() && manga.author != manga.artist) { | ||||
|         textMeasurer.measureHeight(manga.artist!!, typography.bodySmall, 2, detailsConstraints) | ||||
|     } else { | ||||
|         0 | ||||
|     } | ||||
|     val statusHeight = textMeasurer.measureHeight("", typography.bodySmall, 2, detailsConstraints) | ||||
|     val sourceHeight = textMeasurer.measureHeight("", typography.labelSmall, 1, constraints) | ||||
|  | ||||
|     val totalHeight = coverHeight + titleHeight + authorHeight + artistHeight + statusHeight + sourceHeight | ||||
|     return with(density) { ((2 * smallPadding) + totalHeight + (5 * extraSmallPadding)).toDp() } | ||||
| } | ||||
|  | ||||
| private fun TextMeasurer.measureHeight( | ||||
|     text: String, | ||||
|     style: TextStyle, | ||||
|     maxLines: Int, | ||||
|     constraints: Constraints, | ||||
| ): Int = measure( | ||||
|     text = text, | ||||
|     style = style, | ||||
|     overflow = TextOverflow.Ellipsis, | ||||
|     maxLines = maxLines, | ||||
|     constraints = constraints, | ||||
| ) | ||||
|     .size | ||||
|     .height | ||||
|  | ||||
| private val MangaCardWidth = 150.dp | ||||
| private val MangaDetailsIconWidth = 16.dp | ||||
| private val ButtonPadding = PaddingValues(top = 16.dp, bottom = 16.dp) | ||||
| private val TitlePadding = PaddingValues(bottom = 16.dp, top = 8.dp) | ||||
|   | ||||
| @@ -1,45 +0,0 @@ | ||||
| package eu.kanade.presentation.manga | ||||
|  | ||||
| import androidx.compose.foundation.layout.consumeWindowInsets | ||||
| import androidx.compose.foundation.layout.imePadding | ||||
| import androidx.compose.foundation.layout.padding | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.ui.Modifier | ||||
| import eu.kanade.presentation.components.AppBar | ||||
| import eu.kanade.presentation.components.AppBarTitle | ||||
| import eu.kanade.presentation.manga.components.MangaNotesTextArea | ||||
| import eu.kanade.tachiyomi.ui.manga.notes.MangaNotesScreen | ||||
| import tachiyomi.i18n.MR | ||||
| import tachiyomi.presentation.core.components.material.Scaffold | ||||
| import tachiyomi.presentation.core.i18n.stringResource | ||||
|  | ||||
| @Composable | ||||
| fun MangaNotesScreen( | ||||
|     state: MangaNotesScreen.State, | ||||
|     navigateUp: () -> Unit, | ||||
|     onUpdate: (String) -> Unit, | ||||
| ) { | ||||
|     Scaffold( | ||||
|         topBar = { topBarScrollBehavior -> | ||||
|             AppBar( | ||||
|                 titleContent = { | ||||
|                     AppBarTitle( | ||||
|                         title = stringResource(MR.strings.action_edit_notes), | ||||
|                         subtitle = state.manga.title, | ||||
|                     ) | ||||
|                 }, | ||||
|                 navigateUp = navigateUp, | ||||
|                 scrollBehavior = topBarScrollBehavior, | ||||
|             ) | ||||
|         }, | ||||
|     ) { contentPadding -> | ||||
|         MangaNotesTextArea( | ||||
|             state = state, | ||||
|             onUpdate = onUpdate, | ||||
|             modifier = Modifier | ||||
|                 .padding(contentPadding) | ||||
|                 .consumeWindowInsets(contentPadding) | ||||
|                 .imePadding(), | ||||
|         ) | ||||
|     } | ||||
| } | ||||
| @@ -87,7 +87,7 @@ fun MangaScreen( | ||||
|     isTabletUi: Boolean, | ||||
|     chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction, | ||||
|     chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction, | ||||
|     navigateUp: () -> Unit, | ||||
|     onBackClicked: () -> Unit, | ||||
|     onChapterClicked: (Chapter) -> Unit, | ||||
|     onDownloadChapter: ((List<ChapterList.Item>, ChapterDownloadAction) -> Unit)?, | ||||
|     onAddToLibraryClicked: () -> Unit, | ||||
| @@ -112,7 +112,6 @@ fun MangaScreen( | ||||
|     onEditCategoryClicked: (() -> Unit)?, | ||||
|     onEditFetchIntervalClicked: (() -> Unit)?, | ||||
|     onMigrateClicked: (() -> Unit)?, | ||||
|     onEditNotesClicked: () -> Unit, | ||||
|  | ||||
|     // For bottom action menu | ||||
|     onMultiBookmarkClicked: (List<Chapter>, bookmarked: Boolean) -> Unit, | ||||
| @@ -142,7 +141,7 @@ fun MangaScreen( | ||||
|             nextUpdate = nextUpdate, | ||||
|             chapterSwipeStartAction = chapterSwipeStartAction, | ||||
|             chapterSwipeEndAction = chapterSwipeEndAction, | ||||
|             navigateUp = navigateUp, | ||||
|             onBackClicked = onBackClicked, | ||||
|             onChapterClicked = onChapterClicked, | ||||
|             onDownloadChapter = onDownloadChapter, | ||||
|             onAddToLibraryClicked = onAddToLibraryClicked, | ||||
| @@ -161,7 +160,6 @@ fun MangaScreen( | ||||
|             onEditCategoryClicked = onEditCategoryClicked, | ||||
|             onEditIntervalClicked = onEditFetchIntervalClicked, | ||||
|             onMigrateClicked = onMigrateClicked, | ||||
|             onEditNotesClicked = onEditNotesClicked, | ||||
|             onMultiBookmarkClicked = onMultiBookmarkClicked, | ||||
|             onMultiMarkAsReadClicked = onMultiMarkAsReadClicked, | ||||
|             onMarkPreviousAsReadClicked = onMarkPreviousAsReadClicked, | ||||
| @@ -178,7 +176,7 @@ fun MangaScreen( | ||||
|             chapterSwipeStartAction = chapterSwipeStartAction, | ||||
|             chapterSwipeEndAction = chapterSwipeEndAction, | ||||
|             nextUpdate = nextUpdate, | ||||
|             navigateUp = navigateUp, | ||||
|             onBackClicked = onBackClicked, | ||||
|             onChapterClicked = onChapterClicked, | ||||
|             onDownloadChapter = onDownloadChapter, | ||||
|             onAddToLibraryClicked = onAddToLibraryClicked, | ||||
| @@ -197,7 +195,6 @@ fun MangaScreen( | ||||
|             onEditCategoryClicked = onEditCategoryClicked, | ||||
|             onEditIntervalClicked = onEditFetchIntervalClicked, | ||||
|             onMigrateClicked = onMigrateClicked, | ||||
|             onEditNotesClicked = onEditNotesClicked, | ||||
|             onMultiBookmarkClicked = onMultiBookmarkClicked, | ||||
|             onMultiMarkAsReadClicked = onMultiMarkAsReadClicked, | ||||
|             onMarkPreviousAsReadClicked = onMarkPreviousAsReadClicked, | ||||
| @@ -217,7 +214,7 @@ private fun MangaScreenSmallImpl( | ||||
|     nextUpdate: Instant?, | ||||
|     chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction, | ||||
|     chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction, | ||||
|     navigateUp: () -> Unit, | ||||
|     onBackClicked: () -> Unit, | ||||
|     onChapterClicked: (Chapter) -> Unit, | ||||
|     onDownloadChapter: ((List<ChapterList.Item>, ChapterDownloadAction) -> Unit)?, | ||||
|     onAddToLibraryClicked: () -> Unit, | ||||
| @@ -243,7 +240,6 @@ private fun MangaScreenSmallImpl( | ||||
|     onEditCategoryClicked: (() -> Unit)?, | ||||
|     onEditIntervalClicked: (() -> Unit)?, | ||||
|     onMigrateClicked: (() -> Unit)?, | ||||
|     onEditNotesClicked: () -> Unit, | ||||
|  | ||||
|     // For bottom action menu | ||||
|     onMultiBookmarkClicked: (List<Chapter>, bookmarked: Boolean) -> Unit, | ||||
| @@ -269,9 +265,14 @@ private fun MangaScreenSmallImpl( | ||||
|         ) | ||||
|     } | ||||
|  | ||||
|     BackHandler(enabled = isAnySelected) { | ||||
|         onAllChapterSelected(false) | ||||
|     val internalOnBackPressed = { | ||||
|         if (isAnySelected) { | ||||
|             onAllChapterSelected(false) | ||||
|         } else { | ||||
|             onBackClicked() | ||||
|         } | ||||
|     } | ||||
|     BackHandler(onBack = internalOnBackPressed) | ||||
|  | ||||
|     Scaffold( | ||||
|         topBar = { | ||||
| @@ -284,31 +285,29 @@ private fun MangaScreenSmallImpl( | ||||
|             val isFirstItemScrolled by remember { | ||||
|                 derivedStateOf { chapterListState.firstVisibleItemScrollOffset > 0 } | ||||
|             } | ||||
|             val titleAlpha by animateFloatAsState( | ||||
|             val animatedTitleAlpha by animateFloatAsState( | ||||
|                 if (!isFirstItemVisible) 1f else 0f, | ||||
|                 label = "Top Bar Title", | ||||
|             ) | ||||
|             val backgroundAlpha by animateFloatAsState( | ||||
|             val animatedBgAlpha by animateFloatAsState( | ||||
|                 if (!isFirstItemVisible || isFirstItemScrolled) 1f else 0f, | ||||
|                 label = "Top Bar Background", | ||||
|             ) | ||||
|             MangaToolbar( | ||||
|                 title = state.manga.title, | ||||
|                 titleAlphaProvider = { animatedTitleAlpha }, | ||||
|                 backgroundAlphaProvider = { animatedBgAlpha }, | ||||
|                 hasFilters = state.filterActive, | ||||
|                 navigateUp = navigateUp, | ||||
|                 onBackClicked = internalOnBackPressed, | ||||
|                 onClickFilter = onFilterClicked, | ||||
|                 onClickShare = onShareClicked, | ||||
|                 onClickDownload = onDownloadActionClicked, | ||||
|                 onClickEditCategory = onEditCategoryClicked, | ||||
|                 onClickRefresh = onRefresh, | ||||
|                 onClickMigrate = onMigrateClicked, | ||||
|                 onClickEditNotes = onEditNotesClicked, | ||||
|                 actionModeCounter = selectedChapterCount, | ||||
|                 onCancelActionMode = { onAllChapterSelected(false) }, | ||||
|                 onSelectAll = { onAllChapterSelected(true) }, | ||||
|                 onInvertSelection = { onInvertSelection() }, | ||||
|                 titleAlphaProvider = { titleAlpha }, | ||||
|                 backgroundAlphaProvider = { backgroundAlpha }, | ||||
|             ) | ||||
|         }, | ||||
|         bottomBar = { | ||||
| @@ -415,10 +414,8 @@ private fun MangaScreenSmallImpl( | ||||
|                             defaultExpandState = state.isFromSource, | ||||
|                             description = state.manga.description, | ||||
|                             tagsProvider = { state.manga.genre }, | ||||
|                             notes = state.manga.notes, | ||||
|                             onTagSearch = onTagSearch, | ||||
|                             onCopyTagToClipboard = onCopyTagToClipboard, | ||||
|                             onEditNotes = onEditNotesClicked, | ||||
|                         ) | ||||
|                     } | ||||
|  | ||||
| @@ -461,7 +458,7 @@ fun MangaScreenLargeImpl( | ||||
|     nextUpdate: Instant?, | ||||
|     chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction, | ||||
|     chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction, | ||||
|     navigateUp: () -> Unit, | ||||
|     onBackClicked: () -> Unit, | ||||
|     onChapterClicked: (Chapter) -> Unit, | ||||
|     onDownloadChapter: ((List<ChapterList.Item>, ChapterDownloadAction) -> Unit)?, | ||||
|     onAddToLibraryClicked: () -> Unit, | ||||
| @@ -487,7 +484,6 @@ fun MangaScreenLargeImpl( | ||||
|     onEditCategoryClicked: (() -> Unit)?, | ||||
|     onEditIntervalClicked: (() -> Unit)?, | ||||
|     onMigrateClicked: (() -> Unit)?, | ||||
|     onEditNotesClicked: () -> Unit, | ||||
|  | ||||
|     // For bottom action menu | ||||
|     onMultiBookmarkClicked: (List<Chapter>, bookmarked: Boolean) -> Unit, | ||||
| @@ -519,9 +515,14 @@ fun MangaScreenLargeImpl( | ||||
|  | ||||
|     val chapterListState = rememberLazyListState() | ||||
|  | ||||
|     BackHandler(enabled = isAnySelected) { | ||||
|         onAllChapterSelected(false) | ||||
|     val internalOnBackPressed = { | ||||
|         if (isAnySelected) { | ||||
|             onAllChapterSelected(false) | ||||
|         } else { | ||||
|             onBackClicked() | ||||
|         } | ||||
|     } | ||||
|     BackHandler(onBack = internalOnBackPressed) | ||||
|  | ||||
|     Scaffold( | ||||
|         topBar = { | ||||
| @@ -531,21 +532,19 @@ fun MangaScreenLargeImpl( | ||||
|             MangaToolbar( | ||||
|                 modifier = Modifier.onSizeChanged { topBarHeight = it.height }, | ||||
|                 title = state.manga.title, | ||||
|                 titleAlphaProvider = { if (isAnySelected) 1f else 0f }, | ||||
|                 backgroundAlphaProvider = { 1f }, | ||||
|                 hasFilters = state.filterActive, | ||||
|                 navigateUp = navigateUp, | ||||
|                 onBackClicked = internalOnBackPressed, | ||||
|                 onClickFilter = onFilterButtonClicked, | ||||
|                 onClickShare = onShareClicked, | ||||
|                 onClickDownload = onDownloadActionClicked, | ||||
|                 onClickEditCategory = onEditCategoryClicked, | ||||
|                 onClickRefresh = onRefresh, | ||||
|                 onClickMigrate = onMigrateClicked, | ||||
|                 onClickEditNotes = onEditNotesClicked, | ||||
|                 onCancelActionMode = { onAllChapterSelected(false) }, | ||||
|                 actionModeCounter = selectedChapterCount, | ||||
|                 onSelectAll = { onAllChapterSelected(true) }, | ||||
|                 onInvertSelection = { onInvertSelection() }, | ||||
|                 titleAlphaProvider = { 1f }, | ||||
|                 backgroundAlphaProvider = { 1f }, | ||||
|             ) | ||||
|         }, | ||||
|         bottomBar = { | ||||
| @@ -641,10 +640,8 @@ fun MangaScreenLargeImpl( | ||||
|                             defaultExpandState = true, | ||||
|                             description = state.manga.description, | ||||
|                             tagsProvider = { state.manga.genre }, | ||||
|                             notes = state.manga.notes, | ||||
|                             onTagSearch = onTagSearch, | ||||
|                             onCopyTagToClipboard = onCopyTagToClipboard, | ||||
|                             onEditNotes = onEditNotesClicked, | ||||
|                         ) | ||||
|                     } | ||||
|                 }, | ||||
|   | ||||
| @@ -9,7 +9,6 @@ import androidx.compose.animation.fadeOut | ||||
| import androidx.compose.animation.shrinkVertically | ||||
| 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.Row | ||||
| import androidx.compose.foundation.layout.RowScope | ||||
| @@ -29,10 +28,7 @@ import androidx.compose.material.icons.outlined.BookmarkRemove | ||||
| import androidx.compose.material.icons.outlined.Delete | ||||
| import androidx.compose.material.icons.outlined.DoneAll | ||||
| import androidx.compose.material.icons.outlined.Download | ||||
| import androidx.compose.material.icons.outlined.MoreVert | ||||
| import androidx.compose.material.icons.outlined.RemoveDone | ||||
| import androidx.compose.material.icons.outlined.SwapCalls | ||||
| import androidx.compose.material3.DropdownMenuItem | ||||
| import androidx.compose.material3.Icon | ||||
| import androidx.compose.material3.MaterialTheme | ||||
| import androidx.compose.material3.Surface | ||||
| @@ -52,10 +48,8 @@ import androidx.compose.ui.hapticfeedback.HapticFeedbackType | ||||
| import androidx.compose.ui.platform.LocalHapticFeedback | ||||
| import androidx.compose.ui.res.vectorResource | ||||
| import androidx.compose.ui.text.style.TextOverflow | ||||
| import androidx.compose.ui.unit.DpOffset | ||||
| import androidx.compose.ui.unit.dp | ||||
| import eu.kanade.presentation.components.DownloadDropdownMenu | ||||
| import eu.kanade.presentation.components.DropdownMenu | ||||
| import eu.kanade.presentation.manga.DownloadAction | ||||
| import eu.kanade.tachiyomi.R | ||||
| import kotlinx.coroutines.Job | ||||
| @@ -191,7 +185,7 @@ private fun RowScope.Button( | ||||
|         targetValue = if (toConfirm) 2f else 1f, | ||||
|         label = "weight", | ||||
|     ) | ||||
|     Box( | ||||
|     Column( | ||||
|         modifier = Modifier | ||||
|             .size(48.dp) | ||||
|             .weight(animatedWeight) | ||||
| @@ -201,28 +195,24 @@ private fun RowScope.Button( | ||||
|                 onLongClick = onLongClick, | ||||
|                 onClick = onClick, | ||||
|             ), | ||||
|         contentAlignment = Alignment.Center, | ||||
|         verticalArrangement = Arrangement.Center, | ||||
|         horizontalAlignment = Alignment.CenterHorizontally, | ||||
|     ) { | ||||
|         Column( | ||||
|             verticalArrangement = Arrangement.Center, | ||||
|             horizontalAlignment = Alignment.CenterHorizontally, | ||||
|         Icon( | ||||
|             imageVector = icon, | ||||
|             contentDescription = title, | ||||
|         ) | ||||
|         AnimatedVisibility( | ||||
|             visible = toConfirm, | ||||
|             enter = expandVertically(expandFrom = Alignment.Top) + fadeIn(), | ||||
|             exit = shrinkVertically(shrinkTowards = Alignment.Top) + fadeOut(), | ||||
|         ) { | ||||
|             Icon( | ||||
|                 imageVector = icon, | ||||
|                 contentDescription = title, | ||||
|             Text( | ||||
|                 text = title, | ||||
|                 overflow = TextOverflow.Visible, | ||||
|                 maxLines = 1, | ||||
|                 style = MaterialTheme.typography.labelSmall, | ||||
|             ) | ||||
|             AnimatedVisibility( | ||||
|                 visible = toConfirm, | ||||
|                 enter = expandVertically(expandFrom = Alignment.Top) + fadeIn(), | ||||
|                 exit = shrinkVertically(shrinkTowards = Alignment.Top) + fadeOut(), | ||||
|             ) { | ||||
|                 Text( | ||||
|                     text = title, | ||||
|                     overflow = TextOverflow.Visible, | ||||
|                     maxLines = 1, | ||||
|                     style = MaterialTheme.typography.labelSmall, | ||||
|                 ) | ||||
|             } | ||||
|         } | ||||
|         content?.invoke() | ||||
|     } | ||||
| @@ -236,7 +226,6 @@ fun LibraryBottomActionMenu( | ||||
|     onMarkAsUnreadClicked: () -> Unit, | ||||
|     onDownloadClicked: ((DownloadAction) -> Unit)?, | ||||
|     onDeleteClicked: () -> Unit, | ||||
|     onMigrateClicked: () -> Unit, | ||||
|     modifier: Modifier = Modifier, | ||||
| ) { | ||||
|     AnimatedVisibility( | ||||
| @@ -251,18 +240,17 @@ fun LibraryBottomActionMenu( | ||||
|             color = MaterialTheme.colorScheme.surfaceContainerHigh, | ||||
|         ) { | ||||
|             val haptic = LocalHapticFeedback.current | ||||
|             val confirm = remember { mutableStateListOf(false, false, false, false, false, false) } | ||||
|             val confirm = remember { mutableStateListOf(false, false, false, false, false) } | ||||
|             var resetJob: Job? = remember { null } | ||||
|             val onLongClickItem: (Int) -> Unit = { toConfirmIndex -> | ||||
|                 haptic.performHapticFeedback(HapticFeedbackType.LongPress) | ||||
|                 (0..5).forEach { i -> confirm[i] = i == toConfirmIndex } | ||||
|                 (0..<5).forEach { i -> confirm[i] = i == toConfirmIndex } | ||||
|                 resetJob?.cancel() | ||||
|                 resetJob = scope.launch { | ||||
|                     delay(1.seconds) | ||||
|                     if (isActive) confirm[toConfirmIndex] = false | ||||
|                 } | ||||
|             } | ||||
|             val itemOverflow = onDownloadClicked != null | ||||
|             Row( | ||||
|                 modifier = Modifier | ||||
|                     .windowInsetsPadding( | ||||
| @@ -301,57 +289,22 @@ fun LibraryBottomActionMenu( | ||||
|                         onLongClick = { onLongClickItem(3) }, | ||||
|                         onClick = { downloadExpanded = !downloadExpanded }, | ||||
|                     ) { | ||||
|                         val onDismissRequest = { downloadExpanded = false } | ||||
|                         DownloadDropdownMenu( | ||||
|                             expanded = downloadExpanded, | ||||
|                             onDismissRequest = { downloadExpanded = false }, | ||||
|                             onDismissRequest = onDismissRequest, | ||||
|                             onDownloadClicked = onDownloadClicked, | ||||
|                             offset = BottomBarMenuDpOffset, | ||||
|                         ) | ||||
|                     } | ||||
|                 } | ||||
|                 if (!itemOverflow) { | ||||
|                     Button( | ||||
|                         title = stringResource(MR.strings.migrate), | ||||
|                         icon = Icons.Outlined.SwapCalls, | ||||
|                         toConfirm = confirm[4], | ||||
|                         onLongClick = { onLongClickItem(4) }, | ||||
|                         onClick = onMigrateClicked, | ||||
|                     ) | ||||
|                     Button( | ||||
|                         title = stringResource(MR.strings.action_delete), | ||||
|                         icon = Icons.Outlined.Delete, | ||||
|                         toConfirm = confirm[5], | ||||
|                         onLongClick = { onLongClickItem(5) }, | ||||
|                         onClick = onDeleteClicked, | ||||
|                     ) | ||||
|                 } else { | ||||
|                     var overflowMenuOpen by remember { mutableStateOf(false) } | ||||
|                     Button( | ||||
|                         title = stringResource(MR.strings.label_more), | ||||
|                         icon = Icons.Outlined.MoreVert, | ||||
|                         toConfirm = false, | ||||
|                         onLongClick = {}, | ||||
|                         onClick = { overflowMenuOpen = true }, | ||||
|                     ) { | ||||
|                         DropdownMenu( | ||||
|                             expanded = overflowMenuOpen, | ||||
|                             onDismissRequest = { overflowMenuOpen = false }, | ||||
|                             offset = BottomBarMenuDpOffset, | ||||
|                         ) { | ||||
|                             DropdownMenuItem( | ||||
|                                 text = { Text(stringResource(MR.strings.migrate)) }, | ||||
|                                 onClick = onMigrateClicked, | ||||
|                             ) | ||||
|                             DropdownMenuItem( | ||||
|                                 text = { Text(stringResource(MR.strings.action_delete)) }, | ||||
|                                 onClick = onDeleteClicked, | ||||
|                             ) | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|                 Button( | ||||
|                     title = stringResource(MR.strings.action_delete), | ||||
|                     icon = Icons.Outlined.Delete, | ||||
|                     toConfirm = confirm[4], | ||||
|                     onLongClick = { onLongClickItem(4) }, | ||||
|                     onClick = onDeleteClicked, | ||||
|                 ) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| private val BottomBarMenuDpOffset = DpOffset(0.dp, 0.dp) | ||||
|   | ||||
| @@ -36,7 +36,6 @@ import androidx.compose.ui.unit.dp | ||||
| import androidx.compose.ui.viewinterop.AndroidView | ||||
| import androidx.compose.ui.window.Dialog | ||||
| import androidx.compose.ui.window.DialogProperties | ||||
| import androidx.core.graphics.drawable.toDrawable | ||||
| import androidx.core.view.updatePadding | ||||
| import coil3.asDrawable | ||||
| import coil3.imageLoader | ||||
| @@ -172,13 +171,15 @@ fun MangaCoverDialog( | ||||
|                             .memoryCachePolicy(CachePolicy.DISABLED) | ||||
|                             .target { image -> | ||||
|                                 val drawable = image.asDrawable(view.context.resources) | ||||
|  | ||||
|                                 // Copy bitmap in case it came from memory cache | ||||
|                                 // Because SSIV needs to thoroughly read the image | ||||
|                                 val copy = (drawable as? BitmapDrawable) | ||||
|                                     ?.bitmap | ||||
|                                     ?.copy(Bitmap.Config.HARDWARE, false) | ||||
|                                     ?.toDrawable(view.context.resources) | ||||
|                                     ?: drawable | ||||
|                                 val copy = (drawable as? BitmapDrawable)?.let { | ||||
|                                     BitmapDrawable( | ||||
|                                         view.context.resources, | ||||
|                                         it.bitmap.copy(Bitmap.Config.HARDWARE, false), | ||||
|                                     ) | ||||
|                                 } ?: drawable | ||||
|                                 view.setImage(copy, ReaderPageImageView.Config(zoomDuration = 500)) | ||||
|                             } | ||||
|                             .build() | ||||
|   | ||||
| @@ -19,7 +19,8 @@ import androidx.compose.ui.Alignment | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.unit.DpSize | ||||
| import androidx.compose.ui.unit.dp | ||||
| import eu.kanade.tachiyomi.util.system.isReleaseBuildType | ||||
| import eu.kanade.tachiyomi.util.system.isDevFlavor | ||||
| import eu.kanade.tachiyomi.util.system.isPreviewBuildType | ||||
| import kotlinx.collections.immutable.toImmutableList | ||||
| import tachiyomi.domain.manga.interactor.FetchInterval | ||||
| import tachiyomi.i18n.MR | ||||
| @@ -108,7 +109,7 @@ fun SetIntervalDialog( | ||||
|                 } | ||||
|                 Spacer(Modifier.height(MaterialTheme.padding.small)) | ||||
|  | ||||
|                 if (onValueChanged != null && (!isReleaseBuildType)) { | ||||
|                 if (onValueChanged != null && (isDevFlavor || isPreviewBuildType)) { | ||||
|                     Text(stringResource(MR.strings.manga_interval_custom_amount)) | ||||
|  | ||||
|                     BoxWithConstraints( | ||||
|   | ||||
| @@ -23,7 +23,6 @@ import androidx.compose.foundation.layout.size | ||||
| import androidx.compose.foundation.layout.sizeIn | ||||
| import androidx.compose.foundation.lazy.LazyRow | ||||
| import androidx.compose.foundation.lazy.items | ||||
| import androidx.compose.foundation.text.appendInlineContent | ||||
| import androidx.compose.foundation.text.selection.SelectionContainer | ||||
| import androidx.compose.material.icons.Icons | ||||
| import androidx.compose.material.icons.filled.Brush | ||||
| @@ -69,11 +68,8 @@ import androidx.compose.ui.graphics.vector.ImageVector | ||||
| import androidx.compose.ui.layout.ContentScale | ||||
| import androidx.compose.ui.layout.Layout | ||||
| import androidx.compose.ui.platform.LocalContext | ||||
| import androidx.compose.ui.text.LinkAnnotation | ||||
| import androidx.compose.ui.text.SpanStyle | ||||
| import androidx.compose.ui.text.style.TextAlign | ||||
| import androidx.compose.ui.text.style.TextOverflow | ||||
| import androidx.compose.ui.text.withLink | ||||
| import androidx.compose.ui.unit.Constraints | ||||
| import androidx.compose.ui.unit.Dp | ||||
| import androidx.compose.ui.unit.dp | ||||
| @@ -81,17 +77,10 @@ import androidx.compose.ui.unit.sp | ||||
| import coil3.compose.AsyncImage | ||||
| import coil3.request.ImageRequest | ||||
| import coil3.request.crossfade | ||||
| import com.mikepenz.markdown.model.markdownAnnotator | ||||
| import com.mikepenz.markdown.model.markdownAnnotatorConfig | ||||
| import com.mikepenz.markdown.utils.getUnescapedTextInNode | ||||
| import eu.kanade.domain.ui.UiPreferences | ||||
| import eu.kanade.presentation.components.DropdownMenu | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.source.model.SManga | ||||
| import eu.kanade.tachiyomi.util.system.copyToClipboard | ||||
| import org.intellij.markdown.MarkdownElementTypes | ||||
| import org.intellij.markdown.MarkdownTokenTypes | ||||
| import org.intellij.markdown.ast.findChildOfType | ||||
| import tachiyomi.domain.manga.model.Manga | ||||
| import tachiyomi.i18n.MR | ||||
| import tachiyomi.presentation.core.components.material.DISABLED_ALPHA | ||||
| @@ -101,12 +90,12 @@ import tachiyomi.presentation.core.i18n.pluralStringResource | ||||
| import tachiyomi.presentation.core.i18n.stringResource | ||||
| import tachiyomi.presentation.core.util.clickableNoIndication | ||||
| import tachiyomi.presentation.core.util.secondaryItemAlpha | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
| import java.time.Instant | ||||
| import java.time.temporal.ChronoUnit | ||||
| import kotlin.math.roundToInt | ||||
|  | ||||
| private val whitespaceLineRegex = Regex("[\\r\\n]{2,}", setOf(RegexOption.MULTILINE)) | ||||
|  | ||||
| @Composable | ||||
| fun MangaInfoBox( | ||||
|     isTabletUi: Boolean, | ||||
| @@ -247,10 +236,8 @@ fun ExpandableMangaDescription( | ||||
|     defaultExpandState: Boolean, | ||||
|     description: String?, | ||||
|     tagsProvider: () -> List<String>?, | ||||
|     notes: String, | ||||
|     onTagSearch: (String) -> Unit, | ||||
|     onCopyTagToClipboard: (tag: String) -> Unit, | ||||
|     onEditNotes: () -> Unit, | ||||
|     modifier: Modifier = Modifier, | ||||
| ) { | ||||
|     Column(modifier = modifier) { | ||||
| @@ -259,12 +246,15 @@ fun ExpandableMangaDescription( | ||||
|         } | ||||
|         val desc = | ||||
|             description.takeIf { !it.isNullOrBlank() } ?: stringResource(MR.strings.description_placeholder) | ||||
|  | ||||
|         val trimmedDescription = remember(desc) { | ||||
|             desc | ||||
|                 .replace(whitespaceLineRegex, "\n") | ||||
|                 .trimEnd() | ||||
|         } | ||||
|         MangaSummary( | ||||
|             description = desc, | ||||
|             expandedDescription = desc, | ||||
|             shrunkDescription = trimmedDescription, | ||||
|             expanded = expanded, | ||||
|             notes = notes, | ||||
|             onEditNotesClicked = onEditNotes, | ||||
|             modifier = Modifier | ||||
|                 .padding(top = 8.dp) | ||||
|                 .padding(horizontal = 16.dp) | ||||
| @@ -565,55 +555,13 @@ private fun ColumnScope.MangaContentInfo( | ||||
|     } | ||||
| } | ||||
|  | ||||
| private fun descriptionAnnotator(loadImages: Boolean, linkStyle: SpanStyle) = markdownAnnotator( | ||||
|     annotate = { content, child -> | ||||
|         if (!loadImages && child.type == MarkdownElementTypes.IMAGE) { | ||||
|             val inlineLink = child.findChildOfType(MarkdownElementTypes.INLINE_LINK) | ||||
|  | ||||
|             val url = inlineLink?.findChildOfType(MarkdownElementTypes.LINK_DESTINATION) | ||||
|                 ?.getUnescapedTextInNode(content) | ||||
|                 ?: inlineLink?.findChildOfType(MarkdownElementTypes.AUTOLINK) | ||||
|                     ?.findChildOfType(MarkdownTokenTypes.AUTOLINK) | ||||
|                     ?.getUnescapedTextInNode(content) | ||||
|                 ?: return@markdownAnnotator false | ||||
|  | ||||
|             val textNode = inlineLink?.findChildOfType(MarkdownElementTypes.LINK_TITLE) | ||||
|                 ?: inlineLink?.findChildOfType(MarkdownElementTypes.LINK_TEXT) | ||||
|             val altText = textNode?.findChildOfType(MarkdownTokenTypes.TEXT) | ||||
|                 ?.getUnescapedTextInNode(content).orEmpty() | ||||
|  | ||||
|             withLink(LinkAnnotation.Url(url = url)) { | ||||
|                 pushStyle(linkStyle) | ||||
|                 appendInlineContent(MARKDOWN_INLINE_IMAGE_TAG) | ||||
|                 append(altText) | ||||
|                 pop() | ||||
|             } | ||||
|  | ||||
|             return@markdownAnnotator true | ||||
|         } | ||||
|  | ||||
|         if (child.type in DISALLOWED_MARKDOWN_TYPES) { | ||||
|             append(content.substring(child.startOffset, child.endOffset)) | ||||
|             return@markdownAnnotator true | ||||
|         } | ||||
|  | ||||
|         false | ||||
|     }, | ||||
|     config = markdownAnnotatorConfig( | ||||
|         eolAsNewLine = true, | ||||
|     ), | ||||
| ) | ||||
|  | ||||
| @Composable | ||||
| private fun MangaSummary( | ||||
|     description: String, | ||||
|     notes: String, | ||||
|     expandedDescription: String, | ||||
|     shrunkDescription: String, | ||||
|     expanded: Boolean, | ||||
|     onEditNotesClicked: () -> Unit, | ||||
|     modifier: Modifier = Modifier, | ||||
| ) { | ||||
|     val preferences = remember { Injekt.get<UiPreferences>() } | ||||
|     val loadImages = remember { preferences.imagesInDescription().get() } | ||||
|     val animProgress by animateFloatAsState( | ||||
|         targetValue = if (expanded) 1f else 0f, | ||||
|         label = "summary", | ||||
| @@ -623,48 +571,25 @@ private fun MangaSummary( | ||||
|         contents = listOf( | ||||
|             { | ||||
|                 Text( | ||||
|                     // Shows at least 3 lines if no notes | ||||
|                     // when there are notes show 6 | ||||
|                     text = if (notes.isBlank()) "\n\n" else "\n\n\n\n\n", | ||||
|                     text = "\n\n", // Shows at least 3 lines | ||||
|                     style = MaterialTheme.typography.bodyMedium, | ||||
|                 ) | ||||
|             }, | ||||
|             { | ||||
|                 Column { | ||||
|                     MangaNotesSection( | ||||
|                         content = notes, | ||||
|                         expanded = true, | ||||
|                         onEditNotes = onEditNotesClicked, | ||||
|                     ) | ||||
|                     MarkdownRender( | ||||
|                         content = description, | ||||
|                         modifier = Modifier.secondaryItemAlpha(), | ||||
|                         annotator = descriptionAnnotator( | ||||
|                             loadImages = loadImages, | ||||
|                             linkStyle = getMarkdownLinkStyle().toSpanStyle(), | ||||
|                         ), | ||||
|                         loadImages = loadImages, | ||||
|                     ) | ||||
|                 } | ||||
|                 Text( | ||||
|                     text = expandedDescription, | ||||
|                     style = MaterialTheme.typography.bodyMedium, | ||||
|                 ) | ||||
|             }, | ||||
|             { | ||||
|                 Column { | ||||
|                     MangaNotesSection( | ||||
|                         content = notes, | ||||
|                         expanded = expanded, | ||||
|                         onEditNotes = onEditNotesClicked, | ||||
|                 SelectionContainer { | ||||
|                     Text( | ||||
|                         text = if (expanded) expandedDescription else shrunkDescription, | ||||
|                         maxLines = Int.MAX_VALUE, | ||||
|                         style = MaterialTheme.typography.bodyMedium, | ||||
|                         color = MaterialTheme.colorScheme.onBackground, | ||||
|                         modifier = Modifier.secondaryItemAlpha(), | ||||
|                     ) | ||||
|                     SelectionContainer { | ||||
|                         MarkdownRender( | ||||
|                             content = description, | ||||
|                             modifier = Modifier.secondaryItemAlpha(), | ||||
|                             annotator = descriptionAnnotator( | ||||
|                                 loadImages = loadImages, | ||||
|                                 linkStyle = getMarkdownLinkStyle().toSpanStyle(), | ||||
|                             ), | ||||
|                             loadImages = loadImages, | ||||
|                         ) | ||||
|                     } | ||||
|                 } | ||||
|             }, | ||||
|             { | ||||
|   | ||||
| @@ -1,60 +0,0 @@ | ||||
| package eu.kanade.presentation.manga.components | ||||
|  | ||||
| import androidx.compose.animation.animateContentSize | ||||
| import androidx.compose.animation.core.Animatable | ||||
| import androidx.compose.animation.core.tween | ||||
| import androidx.compose.foundation.text.selection.SelectionContainer | ||||
| import androidx.compose.material3.MaterialTheme | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.runtime.LaunchedEffect | ||||
| import androidx.compose.runtime.getValue | ||||
| import androidx.compose.runtime.mutableStateOf | ||||
| import androidx.compose.runtime.remember | ||||
| import androidx.compose.runtime.setValue | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.draw.alpha | ||||
| import com.mohamedrejeb.richeditor.model.rememberRichTextState | ||||
| import com.mohamedrejeb.richeditor.ui.material3.RichText | ||||
|  | ||||
| private val FADE_TIME = tween<Float>(500) | ||||
|  | ||||
| @Composable | ||||
| fun MangaNotesDisplay( | ||||
|     content: String, | ||||
|     modifier: Modifier, | ||||
| ) { | ||||
|     val alpha = remember { Animatable(1f) } | ||||
|     var contentUpdatedOnce by remember { mutableStateOf(false) } | ||||
|  | ||||
|     val richTextState = rememberRichTextState() | ||||
|     val primaryColor = MaterialTheme.colorScheme.primary | ||||
|     LaunchedEffect(content) { | ||||
|         richTextState.setMarkdown(content) | ||||
|  | ||||
|         if (!contentUpdatedOnce) { | ||||
|             contentUpdatedOnce = true | ||||
|             return@LaunchedEffect | ||||
|         } | ||||
|  | ||||
|         alpha.snapTo(targetValue = 0f) | ||||
|         alpha.animateTo(targetValue = 1f, animationSpec = FADE_TIME) | ||||
|     } | ||||
|     LaunchedEffect(Unit) { | ||||
|         richTextState.config.unorderedListIndent = 4 | ||||
|         richTextState.config.orderedListIndent = 20 | ||||
|     } | ||||
|     LaunchedEffect(primaryColor) { | ||||
|         richTextState.config.linkColor = primaryColor | ||||
|     } | ||||
|  | ||||
|     SelectionContainer { | ||||
|         RichText( | ||||
|             modifier = modifier | ||||
|                 // Only animate size if the notes changes | ||||
|                 .then(if (contentUpdatedOnce) Modifier.animateContentSize() else Modifier) | ||||
|                 .alpha(alpha.value), | ||||
|             style = MaterialTheme.typography.bodyMedium, | ||||
|             state = richTextState, | ||||
|         ) | ||||
|     } | ||||
| } | ||||
| @@ -1,90 +0,0 @@ | ||||
| package eu.kanade.presentation.manga.components | ||||
|  | ||||
| import androidx.compose.foundation.layout.Arrangement | ||||
| import androidx.compose.foundation.layout.Column | ||||
| import androidx.compose.foundation.layout.Row | ||||
| import androidx.compose.foundation.layout.fillMaxWidth | ||||
| import androidx.compose.foundation.layout.padding | ||||
| import androidx.compose.foundation.layout.size | ||||
| import androidx.compose.foundation.shape.RoundedCornerShape | ||||
| import androidx.compose.material.icons.Icons | ||||
| import androidx.compose.material.icons.filled.EditNote | ||||
| import androidx.compose.material3.HorizontalDivider | ||||
| import androidx.compose.material3.Icon | ||||
| 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.graphics.Color | ||||
| import androidx.compose.ui.tooling.preview.PreviewLightDark | ||||
| import androidx.compose.ui.unit.dp | ||||
| import tachiyomi.i18n.MR | ||||
| import tachiyomi.presentation.core.components.material.Button | ||||
| import tachiyomi.presentation.core.components.material.ButtonDefaults | ||||
| import tachiyomi.presentation.core.components.material.padding | ||||
| import tachiyomi.presentation.core.i18n.stringResource | ||||
|  | ||||
| @Composable | ||||
| fun MangaNotesSection( | ||||
|     content: String, | ||||
|     expanded: Boolean, | ||||
|     onEditNotes: () -> Unit, | ||||
|     modifier: Modifier = Modifier, | ||||
| ) { | ||||
|     if (content.isBlank()) return | ||||
|     Column( | ||||
|         modifier = modifier.fillMaxWidth(), | ||||
|         horizontalAlignment = Alignment.CenterHorizontally, | ||||
|     ) { | ||||
|         MangaNotesDisplay( | ||||
|             content = content, | ||||
|             modifier = modifier.fillMaxWidth(), | ||||
|         ) | ||||
|         if (expanded) { | ||||
|             Button( | ||||
|                 onClick = onEditNotes, | ||||
|                 colors = ButtonDefaults.buttonColors( | ||||
|                     containerColor = Color.Transparent, | ||||
|                     contentColor = MaterialTheme.colorScheme.primary, | ||||
|                 ), | ||||
|                 shape = RoundedCornerShape(8.dp), | ||||
|                 modifier = Modifier | ||||
|                     .padding(horizontal = 16.dp, vertical = 4.dp), | ||||
|             ) { | ||||
|                 Row( | ||||
|                     horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall), | ||||
|                     verticalAlignment = Alignment.CenterVertically, | ||||
|                 ) { | ||||
|                     Icon( | ||||
|                         imageVector = Icons.Filled.EditNote, | ||||
|                         contentDescription = null, | ||||
|                         modifier = Modifier | ||||
|                             .size(16.dp), | ||||
|                     ) | ||||
|                     Text( | ||||
|                         stringResource(MR.strings.action_edit_notes), | ||||
|                     ) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         HorizontalDivider( | ||||
|             modifier = Modifier | ||||
|                 .padding( | ||||
|                     top = if (expanded) 0.dp else 12.dp, | ||||
|                     bottom = if (expanded) 16.dp else 12.dp, | ||||
|                 ), | ||||
|         ) | ||||
|     } | ||||
| } | ||||
|  | ||||
| @PreviewLightDark | ||||
| @Composable | ||||
| private fun MangaNotesSectionPreview() { | ||||
|     MangaNotesSection( | ||||
|         onEditNotes = {}, | ||||
|         expanded = true, | ||||
|         content = "# Hello world\ntest1234 hi there!", | ||||
|     ) | ||||
| } | ||||
| @@ -1,224 +0,0 @@ | ||||
| package eu.kanade.presentation.manga.components | ||||
|  | ||||
| import androidx.compose.foundation.background | ||||
| 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.PaddingValues | ||||
| import androidx.compose.foundation.layout.Row | ||||
| import androidx.compose.foundation.layout.fillMaxSize | ||||
| import androidx.compose.foundation.layout.fillMaxWidth | ||||
| import androidx.compose.foundation.layout.height | ||||
| import androidx.compose.foundation.layout.padding | ||||
| import androidx.compose.foundation.lazy.LazyRow | ||||
| import androidx.compose.material.icons.Icons | ||||
| import androidx.compose.material.icons.automirrored.outlined.FormatListBulleted | ||||
| import androidx.compose.material.icons.outlined.FormatBold | ||||
| import androidx.compose.material.icons.outlined.FormatItalic | ||||
| import androidx.compose.material.icons.outlined.FormatListNumbered | ||||
| import androidx.compose.material.icons.outlined.FormatUnderlined | ||||
| import androidx.compose.material3.Icon | ||||
| import androidx.compose.material3.MaterialTheme | ||||
| import androidx.compose.material3.Text | ||||
| import androidx.compose.material3.VerticalDivider | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.runtime.DisposableEffect | ||||
| import androidx.compose.runtime.LaunchedEffect | ||||
| import androidx.compose.runtime.remember | ||||
| import androidx.compose.runtime.rememberCoroutineScope | ||||
| import androidx.compose.runtime.snapshotFlow | ||||
| import androidx.compose.ui.Alignment | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.draw.clip | ||||
| import androidx.compose.ui.focus.FocusRequester | ||||
| import androidx.compose.ui.focus.focusRequester | ||||
| import androidx.compose.ui.graphics.Color | ||||
| import androidx.compose.ui.graphics.vector.ImageVector | ||||
| import androidx.compose.ui.semantics.Role | ||||
| import androidx.compose.ui.text.SpanStyle | ||||
| import androidx.compose.ui.text.font.FontStyle | ||||
| import androidx.compose.ui.text.font.FontWeight | ||||
| import androidx.compose.ui.text.style.TextDecoration | ||||
| import androidx.compose.ui.unit.dp | ||||
| import com.mohamedrejeb.richeditor.model.rememberRichTextState | ||||
| import com.mohamedrejeb.richeditor.ui.material3.RichTextEditor | ||||
| import com.mohamedrejeb.richeditor.ui.material3.RichTextEditorDefaults.richTextEditorColors | ||||
| import eu.kanade.tachiyomi.ui.manga.notes.MangaNotesScreen | ||||
| import kotlinx.coroutines.flow.debounce | ||||
| import kotlinx.coroutines.flow.distinctUntilChanged | ||||
| import kotlinx.coroutines.flow.launchIn | ||||
| import kotlinx.coroutines.flow.map | ||||
| import kotlinx.coroutines.flow.onEach | ||||
| import tachiyomi.i18n.MR | ||||
| import tachiyomi.presentation.core.components.material.padding | ||||
| import tachiyomi.presentation.core.i18n.stringResource | ||||
| import kotlin.time.Duration.Companion.seconds | ||||
|  | ||||
| private const val MAX_LENGTH = 250 | ||||
| private const val MAX_LENGTH_WARN = MAX_LENGTH * 0.9 | ||||
|  | ||||
| @Composable | ||||
| fun MangaNotesTextArea( | ||||
|     state: MangaNotesScreen.State, | ||||
|     onUpdate: (String) -> Unit, | ||||
|     modifier: Modifier = Modifier, | ||||
| ) { | ||||
|     val scope = rememberCoroutineScope() | ||||
|     val richTextState = rememberRichTextState() | ||||
|     val primaryColor = MaterialTheme.colorScheme.primary | ||||
|  | ||||
|     DisposableEffect(scope, richTextState) { | ||||
|         snapshotFlow { richTextState.annotatedString } | ||||
|             .debounce(0.25.seconds) | ||||
|             .distinctUntilChanged() | ||||
|             .map { richTextState.toMarkdown() } | ||||
|             .onEach { onUpdate(it) } | ||||
|             .launchIn(scope) | ||||
|  | ||||
|         onDispose { | ||||
|             onUpdate(richTextState.toMarkdown()) | ||||
|         } | ||||
|     } | ||||
|     LaunchedEffect(Unit) { | ||||
|         richTextState.setMarkdown(state.notes) | ||||
|         richTextState.config.unorderedListIndent = 4 | ||||
|         richTextState.config.orderedListIndent = 20 | ||||
|     } | ||||
|     LaunchedEffect(primaryColor) { | ||||
|         richTextState.config.linkColor = primaryColor | ||||
|     } | ||||
|     val focusRequester = remember { FocusRequester() } | ||||
|     LaunchedEffect(focusRequester) { | ||||
|         focusRequester.requestFocus() | ||||
|     } | ||||
|     val textLength = remember(richTextState.annotatedString) { richTextState.toText().length } | ||||
|  | ||||
|     Column( | ||||
|         modifier = modifier | ||||
|             .padding(horizontal = MaterialTheme.padding.small) | ||||
|             .fillMaxSize(), | ||||
|     ) { | ||||
|         RichTextEditor( | ||||
|             state = richTextState, | ||||
|             textStyle = MaterialTheme.typography.bodyLarge, | ||||
|             maxLength = MAX_LENGTH, | ||||
|             placeholder = { | ||||
|                 Text(text = stringResource(MR.strings.notes_placeholder)) | ||||
|             }, | ||||
|             colors = richTextEditorColors( | ||||
|                 containerColor = Color.Transparent, | ||||
|                 focusedIndicatorColor = Color.Transparent, | ||||
|                 unfocusedIndicatorColor = Color.Transparent, | ||||
|             ), | ||||
|             contentPadding = PaddingValues( | ||||
|                 horizontal = MaterialTheme.padding.medium, | ||||
|             ), | ||||
|             modifier = Modifier | ||||
|                 .weight(1f) | ||||
|                 .fillMaxWidth() | ||||
|                 .focusRequester(focusRequester), | ||||
|         ) | ||||
|         Row( | ||||
|             verticalAlignment = Alignment.CenterVertically, | ||||
|             horizontalArrangement = Arrangement.SpaceBetween, | ||||
|             modifier = Modifier | ||||
|                 .padding(vertical = MaterialTheme.padding.small) | ||||
|                 .fillMaxWidth(), | ||||
|         ) { | ||||
|             LazyRow( | ||||
|                 verticalAlignment = Alignment.CenterVertically, | ||||
|                 horizontalArrangement = Arrangement.spacedBy(2.dp), | ||||
|             ) { | ||||
|                 item { | ||||
|                     MangaNotesTextAreaButton( | ||||
|                         onClick = { richTextState.toggleSpanStyle(SpanStyle(fontWeight = FontWeight.Bold)) }, | ||||
|                         isSelected = richTextState.currentSpanStyle.fontWeight == FontWeight.Bold, | ||||
|                         icon = Icons.Outlined.FormatBold, | ||||
|                     ) | ||||
|                 } | ||||
|                 item { | ||||
|                     MangaNotesTextAreaButton( | ||||
|                         onClick = { richTextState.toggleSpanStyle(SpanStyle(fontStyle = FontStyle.Italic)) }, | ||||
|                         isSelected = richTextState.currentSpanStyle.fontStyle == FontStyle.Italic, | ||||
|                         icon = Icons.Outlined.FormatItalic, | ||||
|                     ) | ||||
|                 } | ||||
|                 item { | ||||
|                     MangaNotesTextAreaButton( | ||||
|                         onClick = { | ||||
|                             richTextState.toggleSpanStyle(SpanStyle(textDecoration = TextDecoration.Underline)) | ||||
|                         }, | ||||
|                         isSelected = richTextState.currentSpanStyle.textDecoration | ||||
|                             ?.contains(TextDecoration.Underline) | ||||
|                             ?: false, | ||||
|                         icon = Icons.Outlined.FormatUnderlined, | ||||
|                     ) | ||||
|                 } | ||||
|                 item { | ||||
|                     VerticalDivider( | ||||
|                         modifier = Modifier | ||||
|                             .padding(horizontal = MaterialTheme.padding.extraSmall) | ||||
|                             .height(MaterialTheme.padding.large), | ||||
|                     ) | ||||
|                 } | ||||
|                 item { | ||||
|                     MangaNotesTextAreaButton( | ||||
|                         onClick = { richTextState.toggleUnorderedList() }, | ||||
|                         isSelected = richTextState.isUnorderedList, | ||||
|                         icon = Icons.AutoMirrored.Outlined.FormatListBulleted, | ||||
|                     ) | ||||
|                 } | ||||
|                 item { | ||||
|                     MangaNotesTextAreaButton( | ||||
|                         onClick = { richTextState.toggleOrderedList() }, | ||||
|                         isSelected = richTextState.isOrderedList, | ||||
|                         icon = Icons.Outlined.FormatListNumbered, | ||||
|                     ) | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             Box( | ||||
|                 contentAlignment = Alignment.Center, | ||||
|             ) { | ||||
|                 Text( | ||||
|                     text = (MAX_LENGTH - textLength).toString(), | ||||
|                     color = if (textLength > MAX_LENGTH_WARN) { | ||||
|                         MaterialTheme.colorScheme.error | ||||
|                     } else { | ||||
|                         Color.Unspecified | ||||
|                     }, | ||||
|                     modifier = Modifier.padding(MaterialTheme.padding.extraSmall), | ||||
|                 ) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @Composable | ||||
| fun MangaNotesTextAreaButton( | ||||
|     onClick: () -> Unit, | ||||
|     icon: ImageVector, | ||||
|     isSelected: Boolean, | ||||
|     modifier: Modifier = Modifier, | ||||
| ) { | ||||
|     Box( | ||||
|         modifier = modifier | ||||
|             .clip(MaterialTheme.shapes.small) | ||||
|             .clickable( | ||||
|                 onClick = onClick, | ||||
|                 enabled = true, | ||||
|                 role = Role.Button, | ||||
|             ), | ||||
|         contentAlignment = Alignment.Center, | ||||
|     ) { | ||||
|         Icon( | ||||
|             imageVector = icon, | ||||
|             contentDescription = icon.name, | ||||
|             tint = if (isSelected) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.primary, | ||||
|             modifier = Modifier | ||||
|                 .background(color = if (isSelected) MaterialTheme.colorScheme.onBackground else Color.Transparent) | ||||
|                 .padding(MaterialTheme.padding.extraSmall), | ||||
|         ) | ||||
|     } | ||||
| } | ||||
| @@ -1,12 +1,18 @@ | ||||
| package eu.kanade.presentation.manga.components | ||||
|  | ||||
| import androidx.compose.foundation.layout.Column | ||||
| import androidx.compose.material.icons.Icons | ||||
| import androidx.compose.material.icons.outlined.Close | ||||
| import androidx.compose.material.icons.outlined.Download | ||||
| import androidx.compose.material.icons.outlined.FilterList | ||||
| import androidx.compose.material.icons.outlined.FlipToBack | ||||
| import androidx.compose.material.icons.outlined.SelectAll | ||||
| import androidx.compose.material3.IconButton | ||||
| import androidx.compose.material3.LocalContentColor | ||||
| import androidx.compose.material3.MaterialTheme | ||||
| import androidx.compose.material3.Text | ||||
| import androidx.compose.material3.TopAppBar | ||||
| import androidx.compose.material3.TopAppBarDefaults | ||||
| import androidx.compose.material3.surfaceColorAtElevation | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.runtime.getValue | ||||
| @@ -14,12 +20,12 @@ import androidx.compose.runtime.mutableStateOf | ||||
| import androidx.compose.runtime.remember | ||||
| import androidx.compose.runtime.setValue | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.draw.alpha | ||||
| import androidx.compose.ui.text.style.TextOverflow | ||||
| import androidx.compose.ui.unit.dp | ||||
| import eu.kanade.presentation.components.AppBar | ||||
| import eu.kanade.presentation.components.AppBarActions | ||||
| import eu.kanade.presentation.components.AppBarTitle | ||||
| import eu.kanade.presentation.components.DownloadDropdownMenu | ||||
| import eu.kanade.presentation.components.UpIcon | ||||
| import eu.kanade.presentation.manga.DownloadAction | ||||
| import kotlinx.collections.immutable.persistentListOf | ||||
| import tachiyomi.i18n.MR | ||||
| @@ -29,129 +35,130 @@ import tachiyomi.presentation.core.theme.active | ||||
| @Composable | ||||
| fun MangaToolbar( | ||||
|     title: String, | ||||
|     titleAlphaProvider: () -> Float, | ||||
|     hasFilters: Boolean, | ||||
|     navigateUp: () -> Unit, | ||||
|     onBackClicked: () -> Unit, | ||||
|     onClickFilter: () -> Unit, | ||||
|     onClickShare: (() -> Unit)?, | ||||
|     onClickDownload: ((DownloadAction) -> Unit)?, | ||||
|     onClickEditCategory: (() -> Unit)?, | ||||
|     onClickRefresh: () -> Unit, | ||||
|     onClickMigrate: (() -> Unit)?, | ||||
|     onClickEditNotes: () -> Unit, | ||||
|  | ||||
|     // For action mode | ||||
|     actionModeCounter: Int, | ||||
|     onCancelActionMode: () -> Unit, | ||||
|     onSelectAll: () -> Unit, | ||||
|     onInvertSelection: () -> Unit, | ||||
|  | ||||
|     titleAlphaProvider: () -> Float, | ||||
|     backgroundAlphaProvider: () -> Float, | ||||
|     modifier: Modifier = Modifier, | ||||
|     backgroundAlphaProvider: () -> Float = titleAlphaProvider, | ||||
| ) { | ||||
|     val isActionMode = actionModeCounter > 0 | ||||
|     AppBar( | ||||
|         titleContent = { | ||||
|             if (isActionMode) { | ||||
|                 AppBarTitle(actionModeCounter.toString()) | ||||
|             } else { | ||||
|                 AppBarTitle(title, modifier = Modifier.alpha(titleAlphaProvider())) | ||||
|             } | ||||
|         }, | ||||
|     Column( | ||||
|         modifier = modifier, | ||||
|         backgroundColor = MaterialTheme.colorScheme | ||||
|             .surfaceColorAtElevation(3.dp) | ||||
|             .copy(alpha = if (isActionMode) 1f else backgroundAlphaProvider()), | ||||
|         navigateUp = navigateUp, | ||||
|         actions = { | ||||
|             var downloadExpanded by remember { mutableStateOf(false) } | ||||
|             if (onClickDownload != null) { | ||||
|                 val onDismissRequest = { downloadExpanded = false } | ||||
|                 DownloadDropdownMenu( | ||||
|                     expanded = downloadExpanded, | ||||
|                     onDismissRequest = onDismissRequest, | ||||
|                     onDownloadClicked = onClickDownload, | ||||
|     ) { | ||||
|         val isActionMode = actionModeCounter > 0 | ||||
|         TopAppBar( | ||||
|             title = { | ||||
|                 Text( | ||||
|                     text = if (isActionMode) actionModeCounter.toString() else title, | ||||
|                     maxLines = 1, | ||||
|                     overflow = TextOverflow.Ellipsis, | ||||
|                     color = LocalContentColor.current.copy(alpha = if (isActionMode) 1f else titleAlphaProvider()), | ||||
|                 ) | ||||
|             } | ||||
|  | ||||
|             val filterTint = if (hasFilters) MaterialTheme.colorScheme.active else LocalContentColor.current | ||||
|             AppBarActions( | ||||
|                 actions = persistentListOf<AppBar.AppBarAction>().builder().apply { | ||||
|                     if (isActionMode) { | ||||
|                         add( | ||||
|             }, | ||||
|             navigationIcon = { | ||||
|                 IconButton(onClick = onBackClicked) { | ||||
|                     UpIcon(navigationIcon = Icons.Outlined.Close.takeIf { isActionMode }) | ||||
|                 } | ||||
|             }, | ||||
|             actions = { | ||||
|                 if (isActionMode) { | ||||
|                     AppBarActions( | ||||
|                         persistentListOf( | ||||
|                             AppBar.Action( | ||||
|                                 title = stringResource(MR.strings.action_select_all), | ||||
|                                 icon = Icons.Outlined.SelectAll, | ||||
|                                 onClick = onSelectAll, | ||||
|                             ), | ||||
|                         ) | ||||
|                         add( | ||||
|                             AppBar.Action( | ||||
|                                 title = stringResource(MR.strings.action_select_inverse), | ||||
|                                 icon = Icons.Outlined.FlipToBack, | ||||
|                                 onClick = onInvertSelection, | ||||
|                             ), | ||||
|                         ) | ||||
|                         return@apply | ||||
|                     } | ||||
|                         ), | ||||
|                     ) | ||||
|                 } else { | ||||
|                     var downloadExpanded by remember { mutableStateOf(false) } | ||||
|                     if (onClickDownload != null) { | ||||
|                         add( | ||||
|                             AppBar.Action( | ||||
|                                 title = stringResource(MR.strings.manga_download), | ||||
|                                 icon = Icons.Outlined.Download, | ||||
|                                 onClick = { downloadExpanded = !downloadExpanded }, | ||||
|                             ), | ||||
|                         val onDismissRequest = { downloadExpanded = false } | ||||
|                         DownloadDropdownMenu( | ||||
|                             expanded = downloadExpanded, | ||||
|                             onDismissRequest = onDismissRequest, | ||||
|                             onDownloadClicked = onClickDownload, | ||||
|                         ) | ||||
|                     } | ||||
|                     add( | ||||
|                         AppBar.Action( | ||||
|                             title = stringResource(MR.strings.action_filter), | ||||
|                             icon = Icons.Outlined.FilterList, | ||||
|                             iconTint = filterTint, | ||||
|                             onClick = onClickFilter, | ||||
|                         ), | ||||
|                     ) | ||||
|                     add( | ||||
|                         AppBar.OverflowAction( | ||||
|                             title = stringResource(MR.strings.action_webview_refresh), | ||||
|                             onClick = onClickRefresh, | ||||
|                         ), | ||||
|                     ) | ||||
|                     if (onClickEditCategory != null) { | ||||
|                         add( | ||||
|                             AppBar.OverflowAction( | ||||
|                                 title = stringResource(MR.strings.action_edit_categories), | ||||
|                                 onClick = onClickEditCategory, | ||||
|                             ), | ||||
|                         ) | ||||
|                     } | ||||
|                     if (onClickMigrate != null) { | ||||
|                         add( | ||||
|                             AppBar.OverflowAction( | ||||
|                                 title = stringResource(MR.strings.action_migrate), | ||||
|                                 onClick = onClickMigrate, | ||||
|                             ), | ||||
|                         ) | ||||
|                     } | ||||
|                     if (onClickShare != null) { | ||||
|                         add( | ||||
|                             AppBar.OverflowAction( | ||||
|                                 title = stringResource(MR.strings.action_share), | ||||
|                                 onClick = onClickShare, | ||||
|                             ), | ||||
|                         ) | ||||
|                     } | ||||
|                     add( | ||||
|                         AppBar.OverflowAction( | ||||
|                             title = stringResource(MR.strings.action_notes), | ||||
|                             onClick = onClickEditNotes, | ||||
|                         ), | ||||
|  | ||||
|                     val filterTint = if (hasFilters) MaterialTheme.colorScheme.active else LocalContentColor.current | ||||
|                     AppBarActions( | ||||
|                         actions = persistentListOf<AppBar.AppBarAction>().builder() | ||||
|                             .apply { | ||||
|                                 if (onClickDownload != null) { | ||||
|                                     add( | ||||
|                                         AppBar.Action( | ||||
|                                             title = stringResource(MR.strings.manga_download), | ||||
|                                             icon = Icons.Outlined.Download, | ||||
|                                             onClick = { downloadExpanded = !downloadExpanded }, | ||||
|                                         ), | ||||
|                                     ) | ||||
|                                 } | ||||
|                                 add( | ||||
|                                     AppBar.Action( | ||||
|                                         title = stringResource(MR.strings.action_filter), | ||||
|                                         icon = Icons.Outlined.FilterList, | ||||
|                                         iconTint = filterTint, | ||||
|                                         onClick = onClickFilter, | ||||
|                                     ), | ||||
|                                 ) | ||||
|                                 add( | ||||
|                                     AppBar.OverflowAction( | ||||
|                                         title = stringResource(MR.strings.action_webview_refresh), | ||||
|                                         onClick = onClickRefresh, | ||||
|                                     ), | ||||
|                                 ) | ||||
|                                 if (onClickEditCategory != null) { | ||||
|                                     add( | ||||
|                                         AppBar.OverflowAction( | ||||
|                                             title = stringResource(MR.strings.action_edit_categories), | ||||
|                                             onClick = onClickEditCategory, | ||||
|                                         ), | ||||
|                                     ) | ||||
|                                 } | ||||
|                                 if (onClickMigrate != null) { | ||||
|                                     add( | ||||
|                                         AppBar.OverflowAction( | ||||
|                                             title = stringResource(MR.strings.action_migrate), | ||||
|                                             onClick = onClickMigrate, | ||||
|                                         ), | ||||
|                                     ) | ||||
|                                 } | ||||
|                                 if (onClickShare != null) { | ||||
|                                     add( | ||||
|                                         AppBar.OverflowAction( | ||||
|                                             title = stringResource(MR.strings.action_share), | ||||
|                                             onClick = onClickShare, | ||||
|                                         ), | ||||
|                                     ) | ||||
|                                 } | ||||
|                             } | ||||
|                             .build(), | ||||
|                     ) | ||||
|                 } | ||||
|                     .build(), | ||||
|             ) | ||||
|         }, | ||||
|         isActionMode = isActionMode, | ||||
|         onCancelActionMode = onCancelActionMode, | ||||
|     ) | ||||
|             }, | ||||
|             colors = TopAppBarDefaults.topAppBarColors( | ||||
|                 containerColor = MaterialTheme.colorScheme | ||||
|                     .surfaceColorAtElevation(3.dp) | ||||
|                     .copy(alpha = if (isActionMode) 1f else backgroundAlphaProvider()), | ||||
|             ), | ||||
|         ) | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -1,292 +0,0 @@ | ||||
| package eu.kanade.presentation.manga.components | ||||
|  | ||||
| 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.text.InlineTextContent | ||||
| import androidx.compose.material.icons.Icons | ||||
| import androidx.compose.material.icons.outlined.Image | ||||
| import androidx.compose.material3.Icon | ||||
| import androidx.compose.material3.MaterialTheme | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.runtime.CompositionLocalProvider | ||||
| import androidx.compose.runtime.ReadOnlyComposable | ||||
| import androidx.compose.runtime.remember | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.graphics.Color | ||||
| import androidx.compose.ui.layout.FirstBaseline | ||||
| import androidx.compose.ui.text.Placeholder | ||||
| import androidx.compose.ui.text.PlaceholderVerticalAlign | ||||
| import androidx.compose.ui.text.SpanStyle | ||||
| import androidx.compose.ui.text.TextLinkStyles | ||||
| import androidx.compose.ui.text.font.FontFamily | ||||
| import androidx.compose.ui.text.font.FontStyle | ||||
| import androidx.compose.ui.text.font.FontWeight | ||||
| import androidx.compose.ui.unit.Dp | ||||
| import androidx.compose.ui.unit.dp | ||||
| import com.mikepenz.markdown.coil3.Coil3ImageTransformerImpl | ||||
| import com.mikepenz.markdown.compose.LocalBulletListHandler | ||||
| import com.mikepenz.markdown.compose.Markdown | ||||
| import com.mikepenz.markdown.compose.components.markdownComponents | ||||
| import com.mikepenz.markdown.compose.elements.MarkdownBulletList | ||||
| import com.mikepenz.markdown.compose.elements.MarkdownDivider | ||||
| import com.mikepenz.markdown.compose.elements.MarkdownOrderedList | ||||
| import com.mikepenz.markdown.compose.elements.MarkdownTable | ||||
| import com.mikepenz.markdown.compose.elements.MarkdownTableHeader | ||||
| import com.mikepenz.markdown.compose.elements.MarkdownTableRow | ||||
| import com.mikepenz.markdown.compose.elements.MarkdownText | ||||
| import com.mikepenz.markdown.compose.elements.listDepth | ||||
| import com.mikepenz.markdown.model.DefaultMarkdownColors | ||||
| import com.mikepenz.markdown.model.DefaultMarkdownInlineContent | ||||
| import com.mikepenz.markdown.model.DefaultMarkdownTypography | ||||
| import com.mikepenz.markdown.model.MarkdownAnnotator | ||||
| import com.mikepenz.markdown.model.MarkdownColors | ||||
| import com.mikepenz.markdown.model.MarkdownPadding | ||||
| import com.mikepenz.markdown.model.MarkdownTypography | ||||
| import com.mikepenz.markdown.model.NoOpImageTransformerImpl | ||||
| import com.mikepenz.markdown.model.markdownAnnotator | ||||
| import com.mikepenz.markdown.model.rememberMarkdownState | ||||
| import org.intellij.markdown.MarkdownTokenTypes.Companion.HTML_TAG | ||||
| import org.intellij.markdown.flavours.MarkdownFlavourDescriptor | ||||
| import org.intellij.markdown.flavours.commonmark.CommonMarkFlavourDescriptor | ||||
| import org.intellij.markdown.flavours.commonmark.CommonMarkMarkerProcessor | ||||
| import org.intellij.markdown.flavours.gfm.table.GitHubTableMarkerProvider | ||||
| import org.intellij.markdown.parser.MarkerProcessor | ||||
| import org.intellij.markdown.parser.MarkerProcessorFactory | ||||
| import org.intellij.markdown.parser.ProductionHolder | ||||
| import org.intellij.markdown.parser.constraints.CommonMarkdownConstraints | ||||
| import org.intellij.markdown.parser.constraints.MarkdownConstraints | ||||
| import org.intellij.markdown.parser.markerblocks.MarkerBlockProvider | ||||
| import org.intellij.markdown.parser.markerblocks.providers.AtxHeaderProvider | ||||
| import org.intellij.markdown.parser.markerblocks.providers.BlockQuoteProvider | ||||
| import org.intellij.markdown.parser.markerblocks.providers.CodeBlockProvider | ||||
| import org.intellij.markdown.parser.markerblocks.providers.CodeFenceProvider | ||||
| import org.intellij.markdown.parser.markerblocks.providers.HorizontalRuleProvider | ||||
| import org.intellij.markdown.parser.markerblocks.providers.ListMarkerProvider | ||||
| import org.intellij.markdown.parser.markerblocks.providers.SetextHeaderProvider | ||||
| import tachiyomi.presentation.core.components.material.padding | ||||
|  | ||||
| const val MARKDOWN_INLINE_IMAGE_TAG = "MARKDOWN_INLINE_IMAGE" | ||||
|  | ||||
| @Composable | ||||
| fun MarkdownRender( | ||||
|     content: String, | ||||
|     modifier: Modifier = Modifier, | ||||
|     flavour: MarkdownFlavourDescriptor = SimpleMarkdownFlavourDescriptor, | ||||
|     annotator: MarkdownAnnotator = remember { markdownAnnotator() }, | ||||
|     loadImages: Boolean = true, | ||||
| ) { | ||||
|     Markdown( | ||||
|         markdownState = rememberMarkdownState( | ||||
|             content = content, | ||||
|             flavour = flavour, | ||||
|             immediate = true, | ||||
|         ), | ||||
|         annotator = annotator, | ||||
|         colors = getMarkdownColors(), | ||||
|         typography = getMarkdownTypography(), | ||||
|         padding = markdownPadding, | ||||
|         components = markdownComponents, | ||||
|         imageTransformer = remember(loadImages) { | ||||
|             if (loadImages) Coil3ImageTransformerImpl else NoOpImageTransformerImpl() | ||||
|         }, | ||||
|         inlineContent = getMarkdownInlineContent(), | ||||
|         modifier = modifier, | ||||
|     ) | ||||
| } | ||||
|  | ||||
| @Composable | ||||
| @ReadOnlyComposable | ||||
| private fun getMarkdownColors(): MarkdownColors { | ||||
|     val codeBackground = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f) | ||||
|     return DefaultMarkdownColors( | ||||
|         text = MaterialTheme.colorScheme.onSurface, | ||||
|         codeText = Color.Unspecified, | ||||
|         inlineCodeText = Color.Unspecified, | ||||
|         linkText = Color.Unspecified, | ||||
|         codeBackground = codeBackground, | ||||
|         inlineCodeBackground = codeBackground, | ||||
|         dividerColor = MaterialTheme.colorScheme.outlineVariant, | ||||
|         tableText = Color.Unspecified, | ||||
|         tableBackground = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.05f), | ||||
|     ) | ||||
| } | ||||
|  | ||||
| @Composable | ||||
| @ReadOnlyComposable | ||||
| fun getMarkdownLinkStyle() = MaterialTheme.typography.bodyMedium.copy( | ||||
|     color = MaterialTheme.colorScheme.primary, | ||||
|     fontWeight = FontWeight.Bold, | ||||
| ) | ||||
|  | ||||
| @Composable | ||||
| @ReadOnlyComposable | ||||
| private fun getMarkdownTypography(): MarkdownTypography { | ||||
|     val link = getMarkdownLinkStyle() | ||||
|     return DefaultMarkdownTypography( | ||||
|         h1 = MaterialTheme.typography.headlineMedium, | ||||
|         h2 = MaterialTheme.typography.headlineSmall, | ||||
|         h3 = MaterialTheme.typography.titleLarge, | ||||
|         h4 = MaterialTheme.typography.titleMedium, | ||||
|         h5 = MaterialTheme.typography.titleSmall, | ||||
|         h6 = MaterialTheme.typography.bodyLarge, | ||||
|         text = MaterialTheme.typography.bodyMedium, | ||||
|         code = MaterialTheme.typography.bodyMedium.copy(fontFamily = FontFamily.Monospace), | ||||
|         inlineCode = MaterialTheme.typography.bodyMedium.copy(fontFamily = FontFamily.Monospace), | ||||
|         quote = MaterialTheme.typography.bodyMedium.plus(SpanStyle(fontStyle = FontStyle.Italic)), | ||||
|         paragraph = MaterialTheme.typography.bodyMedium, | ||||
|         ordered = MaterialTheme.typography.bodyMedium, | ||||
|         bullet = MaterialTheme.typography.bodyMedium, | ||||
|         list = MaterialTheme.typography.bodyMedium, | ||||
|         link = link, | ||||
|         textLink = TextLinkStyles(style = link.toSpanStyle()), | ||||
|         table = MaterialTheme.typography.bodyMedium, | ||||
|     ) | ||||
| } | ||||
|  | ||||
| private val markdownPadding = object : MarkdownPadding { | ||||
|     override val block: Dp = 2.dp | ||||
|     override val blockQuote: PaddingValues = PaddingValues(horizontal = 16.dp, vertical = 0.dp) | ||||
|     override val blockQuoteBar: PaddingValues.Absolute = PaddingValues.Absolute( | ||||
|         left = 4.dp, | ||||
|         top = 2.dp, | ||||
|         right = 4.dp, | ||||
|         bottom = 2.dp, | ||||
|     ) | ||||
|     override val blockQuoteText: PaddingValues = PaddingValues(vertical = 4.dp) | ||||
|     override val codeBlock: PaddingValues = PaddingValues(8.dp) | ||||
|     override val list: Dp = 0.dp | ||||
|     override val listIndent: Dp = 8.dp | ||||
|     override val listItemBottom: Dp = 0.dp | ||||
|     override val listItemTop: Dp = 0.dp | ||||
| } | ||||
|  | ||||
| private val markdownComponents = markdownComponents( | ||||
|     horizontalRule = { | ||||
|         MarkdownDivider( | ||||
|             modifier = Modifier | ||||
|                 .padding(vertical = MaterialTheme.padding.extraSmall) | ||||
|                 .fillMaxWidth(), | ||||
|         ) | ||||
|     }, | ||||
|     orderedList = { ol -> | ||||
|         Column(modifier = Modifier.padding(start = MaterialTheme.padding.small)) { | ||||
|             MarkdownOrderedList( | ||||
|                 content = ol.content, | ||||
|                 node = ol.node, | ||||
|                 style = ol.typography.ordered, | ||||
|                 depth = ol.listDepth, | ||||
|                 markerModifier = { Modifier.alignBy(FirstBaseline) }, | ||||
|                 listModifier = { Modifier.alignBy(FirstBaseline) }, | ||||
|             ) | ||||
|         } | ||||
|     }, | ||||
|     unorderedList = { ul -> | ||||
|         val markers = listOf("•", "◦", "▸", "▹") | ||||
|  | ||||
|         CompositionLocalProvider( | ||||
|             LocalBulletListHandler provides { _, _, _, _, _ -> "${markers[ul.listDepth % markers.size]} " }, | ||||
|         ) { | ||||
|             Column(modifier = Modifier.padding(start = MaterialTheme.padding.small)) { | ||||
|                 MarkdownBulletList( | ||||
|                     content = ul.content, | ||||
|                     node = ul.node, | ||||
|                     style = ul.typography.bullet, | ||||
|                     markerModifier = { Modifier.alignBy(FirstBaseline) }, | ||||
|                     listModifier = { Modifier.alignBy(FirstBaseline) }, | ||||
|                 ) | ||||
|             } | ||||
|         } | ||||
|     }, | ||||
|     table = { t -> | ||||
|         MarkdownTable( | ||||
|             content = t.content, | ||||
|             node = t.node, | ||||
|             style = t.typography.text, | ||||
|             headerBlock = { content, header, tableWidth, style -> | ||||
|                 MarkdownTableHeader( | ||||
|                     content = content, | ||||
|                     header = header, | ||||
|                     tableWidth = tableWidth, | ||||
|                     style = style, | ||||
|                     maxLines = Int.MAX_VALUE, | ||||
|                 ) | ||||
|             }, | ||||
|             rowBlock = { content, header, tableWidth, style -> | ||||
|                 MarkdownTableRow( | ||||
|                     content = content, | ||||
|                     header = header, | ||||
|                     tableWidth = tableWidth, | ||||
|                     style = style, | ||||
|                     maxLines = Int.MAX_VALUE, | ||||
|                 ) | ||||
|             }, | ||||
|         ) | ||||
|     }, | ||||
|     custom = { type, model -> | ||||
|         if (type in DISALLOWED_MARKDOWN_TYPES) { | ||||
|             MarkdownText( | ||||
|                 content = model.content.substring(model.node.startOffset, model.node.endOffset), | ||||
|                 style = model.typography.text, | ||||
|             ) | ||||
|         } | ||||
|     }, | ||||
| ) | ||||
|  | ||||
| @Composable | ||||
| @ReadOnlyComposable | ||||
| private fun getMarkdownInlineContent() = DefaultMarkdownInlineContent( | ||||
|     inlineContent = mapOf( | ||||
|         MARKDOWN_INLINE_IMAGE_TAG to InlineTextContent( | ||||
|             placeholder = Placeholder( | ||||
|                 width = MaterialTheme.typography.bodyMedium.fontSize * 1.25, | ||||
|                 height = MaterialTheme.typography.bodyMedium.fontSize * 1.25, | ||||
|                 placeholderVerticalAlign = PlaceholderVerticalAlign.TextCenter, | ||||
|             ), | ||||
|             children = { | ||||
|                 Icon( | ||||
|                     imageVector = Icons.Outlined.Image, | ||||
|                     contentDescription = null, | ||||
|                     tint = MaterialTheme.colorScheme.primary, | ||||
|                 ) | ||||
|             }, | ||||
|         ), | ||||
|     ), | ||||
| ) | ||||
|  | ||||
| private object SimpleMarkdownFlavourDescriptor : CommonMarkFlavourDescriptor() { | ||||
|     override val markerProcessorFactory: MarkerProcessorFactory = SimpleMarkdownProcessFactory | ||||
| } | ||||
|  | ||||
| private object SimpleMarkdownProcessFactory : MarkerProcessorFactory { | ||||
|     override fun createMarkerProcessor(productionHolder: ProductionHolder): MarkerProcessor<*> { | ||||
|         return SimpleMarkdownMarkerProcessor(productionHolder, CommonMarkdownConstraints.BASE) | ||||
|     } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Like `CommonMarkFlavour`, but with html blocks and reference links removed and | ||||
|  * table support added | ||||
|  */ | ||||
| private class SimpleMarkdownMarkerProcessor( | ||||
|     productionHolder: ProductionHolder, | ||||
|     constraints: MarkdownConstraints, | ||||
| ) : CommonMarkMarkerProcessor(productionHolder, constraints) { | ||||
|     private val markerBlockProviders = listOf( | ||||
|         CodeBlockProvider(), | ||||
|         HorizontalRuleProvider(), | ||||
|         CodeFenceProvider(), | ||||
|         SetextHeaderProvider(), | ||||
|         BlockQuoteProvider(), | ||||
|         ListMarkerProvider(), | ||||
|         AtxHeaderProvider(), | ||||
|         GitHubTableMarkerProvider(), | ||||
|     ) | ||||
|  | ||||
|     override fun getMarkerBlockProviders(): List<MarkerBlockProvider<StateInfo>> { | ||||
|         return markerBlockProviders | ||||
|     } | ||||
| } | ||||
|  | ||||
| val DISALLOWED_MARKDOWN_TYPES = arrayOf(HTML_TAG) | ||||
| @@ -1,10 +1,15 @@ | ||||
| package eu.kanade.presentation.more | ||||
|  | ||||
| import androidx.compose.foundation.layout.Column | ||||
| import androidx.compose.foundation.layout.WindowInsets | ||||
| import androidx.compose.foundation.layout.WindowInsetsSides | ||||
| import androidx.compose.foundation.layout.only | ||||
| import androidx.compose.foundation.layout.padding | ||||
| import androidx.compose.foundation.layout.systemBars | ||||
| import androidx.compose.foundation.layout.windowInsetsPadding | ||||
| import androidx.compose.material.icons.Icons | ||||
| import androidx.compose.material.icons.automirrored.outlined.HelpOutline | ||||
| import androidx.compose.material.icons.automirrored.outlined.Label | ||||
| import androidx.compose.material.icons.outlined.AttachMoney | ||||
| import androidx.compose.material.icons.outlined.CloudOff | ||||
| import androidx.compose.material.icons.outlined.GetApp | ||||
| import androidx.compose.material.icons.outlined.Info | ||||
| @@ -35,6 +40,7 @@ fun MoreScreen( | ||||
|     onDownloadedOnlyChange: (Boolean) -> Unit, | ||||
|     incognitoMode: Boolean, | ||||
|     onIncognitoModeChange: (Boolean) -> Unit, | ||||
|     isFDroid: Boolean, | ||||
|     onClickDownloadQueue: () -> Unit, | ||||
|     onClickCategories: () -> Unit, | ||||
|     onClickStats: () -> Unit, | ||||
| @@ -44,7 +50,19 @@ fun MoreScreen( | ||||
| ) { | ||||
|     val uriHandler = LocalUriHandler.current | ||||
|  | ||||
|     Scaffold { contentPadding -> | ||||
|     Scaffold( | ||||
|         topBar = { | ||||
|             Column( | ||||
|                 modifier = Modifier.windowInsetsPadding( | ||||
|                     WindowInsets.systemBars.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal), | ||||
|                 ), | ||||
|             ) { | ||||
|                 if (isFDroid) { | ||||
|                     // Don't really care about slow updaters now | ||||
|                 } | ||||
|             } | ||||
|         }, | ||||
|     ) { contentPadding -> | ||||
|         ScrollbarLazyColumn( | ||||
|             modifier = Modifier.padding(contentPadding), | ||||
|         ) { | ||||
| @@ -146,13 +164,6 @@ fun MoreScreen( | ||||
|                     onPreferenceClick = { uriHandler.openUri(Constants.URL_HELP) }, | ||||
|                 ) | ||||
|             } | ||||
|             item { | ||||
|                 TextPreferenceWidget( | ||||
|                     title = stringResource(MR.strings.label_donate), | ||||
|                     icon = Icons.Outlined.AttachMoney, | ||||
|                     onPreferenceClick = { uriHandler.openUri(Constants.URL_DONATE) }, | ||||
|                 ) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -1,6 +1,5 @@ | ||||
| package eu.kanade.presentation.more | ||||
|  | ||||
| import androidx.compose.foundation.layout.Column | ||||
| import androidx.compose.foundation.layout.Spacer | ||||
| import androidx.compose.foundation.layout.fillMaxWidth | ||||
| import androidx.compose.foundation.layout.padding | ||||
| @@ -14,10 +13,13 @@ import androidx.compose.material3.Text | ||||
| import androidx.compose.material3.TextButton | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.text.SpanStyle | ||||
| import androidx.compose.ui.tooling.preview.PreviewLightDark | ||||
| import eu.kanade.presentation.manga.components.MarkdownRender | ||||
| import com.halilibo.richtext.markdown.Markdown | ||||
| import com.halilibo.richtext.ui.RichTextStyle | ||||
| import com.halilibo.richtext.ui.material3.RichText | ||||
| import com.halilibo.richtext.ui.string.RichTextStringStyle | ||||
| import eu.kanade.presentation.theme.TachiyomiPreviewTheme | ||||
| import org.intellij.markdown.flavours.gfm.GFMFlavourDescriptor | ||||
| import tachiyomi.i18n.MR | ||||
| import tachiyomi.presentation.core.components.material.padding | ||||
| import tachiyomi.presentation.core.i18n.stringResource | ||||
| @@ -40,15 +42,17 @@ fun NewUpdateScreen( | ||||
|         rejectText = stringResource(MR.strings.action_not_now), | ||||
|         onRejectClick = onRejectUpdate, | ||||
|     ) { | ||||
|         Column( | ||||
|         RichText( | ||||
|             modifier = Modifier | ||||
|                 .fillMaxWidth() | ||||
|                 .padding(vertical = MaterialTheme.padding.large), | ||||
|             style = RichTextStyle( | ||||
|                 stringStyle = RichTextStringStyle( | ||||
|                     linkStyle = SpanStyle(color = MaterialTheme.colorScheme.primary), | ||||
|                 ), | ||||
|             ), | ||||
|         ) { | ||||
|             MarkdownRender( | ||||
|                 content = changelogInfo, | ||||
|                 flavour = GFMFlavourDescriptor(), | ||||
|             ) | ||||
|             Markdown(content = changelogInfo) | ||||
|  | ||||
|             TextButton( | ||||
|                 onClick = onOpenInBrowser, | ||||
|   | ||||
| @@ -42,9 +42,7 @@ fun OnboardingScreen( | ||||
|     } | ||||
|     val isLastStep = currentStep == steps.lastIndex | ||||
|  | ||||
|     BackHandler(enabled = currentStep != 0) { | ||||
|         currentStep-- | ||||
|     } | ||||
|     BackHandler(enabled = currentStep != 0, onBack = { currentStep-- }) | ||||
|  | ||||
|     InfoScreen( | ||||
|         icon = Icons.Outlined.RocketLaunch, | ||||
|   | ||||
| @@ -4,6 +4,7 @@ import android.Manifest | ||||
| import android.annotation.SuppressLint | ||||
| import android.content.Intent | ||||
| import android.content.pm.PackageManager | ||||
| import android.net.Uri | ||||
| import android.os.Build | ||||
| import android.os.PowerManager | ||||
| import android.provider.Settings | ||||
| @@ -13,13 +14,11 @@ import androidx.compose.foundation.layout.Column | ||||
| import androidx.compose.foundation.layout.padding | ||||
| import androidx.compose.material.icons.Icons | ||||
| import androidx.compose.material.icons.filled.Check | ||||
| import androidx.compose.material3.HorizontalDivider | ||||
| import androidx.compose.material3.Icon | ||||
| import androidx.compose.material3.ListItem | ||||
| import androidx.compose.material3.ListItemDefaults | ||||
| import androidx.compose.material3.MaterialTheme | ||||
| import androidx.compose.material3.OutlinedButton | ||||
| import androidx.compose.material3.Switch | ||||
| import androidx.compose.material3.Text | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.runtime.DisposableEffect | ||||
| @@ -31,24 +30,17 @@ import androidx.compose.ui.graphics.Color | ||||
| import androidx.compose.ui.platform.LocalContext | ||||
| import androidx.compose.ui.unit.dp | ||||
| import androidx.core.content.getSystemService | ||||
| import androidx.core.net.toUri | ||||
| import androidx.lifecycle.DefaultLifecycleObserver | ||||
| import androidx.lifecycle.LifecycleOwner | ||||
| import androidx.lifecycle.compose.LocalLifecycleOwner | ||||
| import eu.kanade.presentation.util.rememberRequestPackageInstallsPermissionState | ||||
| import eu.kanade.tachiyomi.core.security.PrivacyPreferences | ||||
| import eu.kanade.tachiyomi.util.system.launchRequestPackageInstallsPermission | ||||
| import eu.kanade.tachiyomi.util.system.telemetryIncluded | ||||
| import tachiyomi.i18n.MR | ||||
| import tachiyomi.presentation.core.i18n.stringResource | ||||
| import tachiyomi.presentation.core.util.collectAsState | ||||
| import tachiyomi.presentation.core.util.secondaryItemAlpha | ||||
| import uy.kohesive.injekt.injectLazy | ||||
|  | ||||
| internal class PermissionStep : OnboardingStep { | ||||
|  | ||||
|     private val privacyPreferences: PrivacyPreferences by injectLazy() | ||||
|  | ||||
|     private var notificationGranted by mutableStateOf(false) | ||||
|     private var batteryGranted by mutableStateOf(false) | ||||
|  | ||||
| @@ -81,7 +73,7 @@ internal class PermissionStep : OnboardingStep { | ||||
|         } | ||||
|  | ||||
|         Column { | ||||
|             PermissionCheckbox( | ||||
|             PermissionItem( | ||||
|                 title = stringResource(MR.strings.onboarding_permission_install_apps), | ||||
|                 subtitle = stringResource(MR.strings.onboarding_permission_install_apps_description), | ||||
|                 granted = installGranted, | ||||
| @@ -97,7 +89,7 @@ internal class PermissionStep : OnboardingStep { | ||||
|                         // no-op. resulting checks is being done on resume | ||||
|                     }, | ||||
|                 ) | ||||
|                 PermissionCheckbox( | ||||
|                 PermissionItem( | ||||
|                     title = stringResource(MR.strings.onboarding_permission_notifications), | ||||
|                     subtitle = stringResource(MR.strings.onboarding_permission_notifications_description), | ||||
|                     granted = notificationGranted, | ||||
| @@ -105,43 +97,18 @@ internal class PermissionStep : OnboardingStep { | ||||
|                 ) | ||||
|             } | ||||
|  | ||||
|             PermissionCheckbox( | ||||
|             PermissionItem( | ||||
|                 title = stringResource(MR.strings.onboarding_permission_ignore_battery_opts), | ||||
|                 subtitle = stringResource(MR.strings.onboarding_permission_ignore_battery_opts_description), | ||||
|                 granted = batteryGranted, | ||||
|                 onButtonClick = { | ||||
|                     @SuppressLint("BatteryLife") | ||||
|                     val intent = Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply { | ||||
|                         data = "package:${context.packageName}".toUri() | ||||
|                         data = Uri.parse("package:${context.packageName}") | ||||
|                     } | ||||
|                     context.startActivity(intent) | ||||
|                 }, | ||||
|             ) | ||||
|  | ||||
|             if (!telemetryIncluded) return@Column | ||||
|  | ||||
|             HorizontalDivider( | ||||
|                 modifier = Modifier.padding(vertical = 8.dp, horizontal = 16.dp), | ||||
|                 color = MaterialTheme.colorScheme.onPrimaryContainer, | ||||
|             ) | ||||
|  | ||||
|             val crashlyticsPref = privacyPreferences.crashlytics() | ||||
|             val crashlytics by crashlyticsPref.collectAsState() | ||||
|             PermissionSwitch( | ||||
|                 title = stringResource(MR.strings.onboarding_permission_crashlytics), | ||||
|                 subtitle = stringResource(MR.strings.onboarding_permission_crashlytics_description), | ||||
|                 granted = crashlytics, | ||||
|                 onToggleChange = crashlyticsPref::set, | ||||
|             ) | ||||
|  | ||||
|             val analyticsPref = privacyPreferences.analytics() | ||||
|             val analytics by analyticsPref.collectAsState() | ||||
|             PermissionSwitch( | ||||
|                 title = stringResource(MR.strings.onboarding_permission_analytics), | ||||
|                 subtitle = stringResource(MR.strings.onboarding_permission_analytics_description), | ||||
|                 granted = analytics, | ||||
|                 onToggleChange = analyticsPref::set, | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -160,7 +127,7 @@ internal class PermissionStep : OnboardingStep { | ||||
|     } | ||||
|  | ||||
|     @Composable | ||||
|     private fun PermissionCheckbox( | ||||
|     private fun PermissionItem( | ||||
|         title: String, | ||||
|         subtitle: String, | ||||
|         granted: Boolean, | ||||
| @@ -190,26 +157,4 @@ internal class PermissionStep : OnboardingStep { | ||||
|             colors = ListItemDefaults.colors(containerColor = Color.Transparent), | ||||
|         ) | ||||
|     } | ||||
|  | ||||
|     @Composable | ||||
|     private fun PermissionSwitch( | ||||
|         title: String, | ||||
|         subtitle: String, | ||||
|         granted: Boolean, | ||||
|         modifier: Modifier = Modifier, | ||||
|         onToggleChange: (Boolean) -> Unit, | ||||
|     ) { | ||||
|         ListItem( | ||||
|             modifier = modifier, | ||||
|             headlineContent = { Text(text = title) }, | ||||
|             supportingContent = { Text(text = subtitle) }, | ||||
|             trailingContent = { | ||||
|                 Switch( | ||||
|                     checked = granted, | ||||
|                     onCheckedChange = onToggleChange, | ||||
|                 ) | ||||
|             }, | ||||
|             colors = ListItemDefaults.colors(containerColor = Color.Transparent), | ||||
|         ) | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -1,6 +1,5 @@ | ||||
| package eu.kanade.presentation.more.settings | ||||
|  | ||||
| import androidx.annotation.IntRange | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.runtime.remember | ||||
| import androidx.compose.ui.graphics.vector.ImageVector | ||||
| @@ -18,7 +17,7 @@ sealed class Preference { | ||||
|     sealed class PreferenceItem<T> : Preference() { | ||||
|         abstract val subtitle: String? | ||||
|         abstract val icon: ImageVector? | ||||
|         abstract val onValueChanged: suspend (value: T) -> Boolean | ||||
|         abstract val onValueChanged: suspend (newValue: T) -> Boolean | ||||
|  | ||||
|         /** | ||||
|          * A basic [PreferenceItem] that only displays texts. | ||||
| @@ -26,58 +25,57 @@ sealed class Preference { | ||||
|         data class TextPreference( | ||||
|             override val title: String, | ||||
|             override val subtitle: String? = null, | ||||
|             override val icon: ImageVector? = null, | ||||
|             override val enabled: Boolean = true, | ||||
|             override val onValueChanged: suspend (newValue: String) -> Boolean = { true }, | ||||
|  | ||||
|             val onClick: (() -> Unit)? = null, | ||||
|         ) : PreferenceItem<String>() { | ||||
|             override val icon: ImageVector? = null | ||||
|             override val onValueChanged: suspend (value: String) -> Boolean = { true } | ||||
|         } | ||||
|         ) : PreferenceItem<String>() | ||||
|  | ||||
|         /** | ||||
|          * A [PreferenceItem] that provides a two-state toggleable option. | ||||
|          */ | ||||
|         data class SwitchPreference( | ||||
|             val preference: PreferenceData<Boolean>, | ||||
|             val pref: PreferenceData<Boolean>, | ||||
|             override val title: String, | ||||
|             override val subtitle: String? = null, | ||||
|             override val icon: ImageVector? = null, | ||||
|             override val enabled: Boolean = true, | ||||
|             override val onValueChanged: suspend (value: Boolean) -> Boolean = { true }, | ||||
|         ) : PreferenceItem<Boolean>() { | ||||
|             override val icon: ImageVector? = null | ||||
|         } | ||||
|             override val onValueChanged: suspend (newValue: Boolean) -> Boolean = { true }, | ||||
|         ) : PreferenceItem<Boolean>() | ||||
|  | ||||
|         /** | ||||
|          * A [PreferenceItem] that provides a slider to select an integer number. | ||||
|          */ | ||||
|         data class SliderPreference( | ||||
|             val value: Int, | ||||
|             override val title: String, | ||||
|             val valueRange: IntProgression = 0..1, | ||||
|             @IntRange(from = 0) val steps: Int = with(valueRange) { (last - first) - 1 }, | ||||
|             val min: Int = 0, | ||||
|             val max: Int, | ||||
|             override val title: String = "", | ||||
|             override val subtitle: String? = null, | ||||
|             override val icon: ImageVector? = null, | ||||
|             override val enabled: Boolean = true, | ||||
|             override val onValueChanged: suspend (value: Int) -> Boolean = { true }, | ||||
|         ) : PreferenceItem<Int>() { | ||||
|             override val icon: ImageVector? = null | ||||
|         } | ||||
|             override val onValueChanged: suspend (newValue: Int) -> Boolean = { true }, | ||||
|         ) : PreferenceItem<Int>() | ||||
|  | ||||
|         /** | ||||
|          * A [PreferenceItem] that displays a list of entries as a dialog. | ||||
|          */ | ||||
|         @Suppress("UNCHECKED_CAST") | ||||
|         data class ListPreference<T>( | ||||
|             val preference: PreferenceData<T>, | ||||
|             val entries: ImmutableMap<T, String>, | ||||
|             val pref: PreferenceData<T>, | ||||
|             override val title: String, | ||||
|             override val subtitle: String? = "%s", | ||||
|             val subtitleProvider: @Composable (value: T, entries: ImmutableMap<T, String>) -> String? = | ||||
|                 { v, e -> subtitle?.format(e[v]) }, | ||||
|             override val icon: ImageVector? = null, | ||||
|             override val enabled: Boolean = true, | ||||
|             override val onValueChanged: suspend (value: T) -> Boolean = { true }, | ||||
|             override val onValueChanged: suspend (newValue: T) -> Boolean = { true }, | ||||
|  | ||||
|             val entries: ImmutableMap<T, String>, | ||||
|         ) : PreferenceItem<T>() { | ||||
|             internal fun internalSet(value: Any) = preference.set(value as T) | ||||
|             internal suspend fun internalOnValueChanged(value: Any) = onValueChanged(value as T) | ||||
|             internal fun internalSet(newValue: Any) = pref.set(newValue as T) | ||||
|             internal suspend fun internalOnValueChanged(newValue: Any) = onValueChanged(newValue as T) | ||||
|  | ||||
|             @Composable | ||||
|             internal fun internalSubtitleProvider(value: Any?, entries: ImmutableMap<out Any?, String>) = | ||||
| @@ -89,14 +87,15 @@ sealed class Preference { | ||||
|          */ | ||||
|         data class BasicListPreference( | ||||
|             val value: String, | ||||
|             val entries: ImmutableMap<String, String>, | ||||
|             override val title: String, | ||||
|             override val subtitle: String? = "%s", | ||||
|             val subtitleProvider: @Composable (value: String, entries: ImmutableMap<String, String>) -> String? = | ||||
|                 { v, e -> subtitle?.format(e[v]) }, | ||||
|             override val icon: ImageVector? = null, | ||||
|             override val enabled: Boolean = true, | ||||
|             override val onValueChanged: suspend (value: String) -> Boolean = { true }, | ||||
|             override val onValueChanged: suspend (newValue: String) -> Boolean = { true }, | ||||
|  | ||||
|             val entries: ImmutableMap<String, String>, | ||||
|         ) : PreferenceItem<String>() | ||||
|  | ||||
|         /** | ||||
| @@ -104,51 +103,52 @@ sealed class Preference { | ||||
|          * Multiple entries can be selected at the same time. | ||||
|          */ | ||||
|         data class MultiSelectListPreference( | ||||
|             val preference: PreferenceData<Set<String>>, | ||||
|             val entries: ImmutableMap<String, String>, | ||||
|             val pref: PreferenceData<Set<String>>, | ||||
|             override val title: String, | ||||
|             override val subtitle: String? = "%s", | ||||
|             val subtitleProvider: @Composable (value: Set<String>, entries: ImmutableMap<String, String>) -> String? = | ||||
|                 { v, e -> | ||||
|                     val combined = remember(v, e) { | ||||
|                         v.mapNotNull { e[it] } | ||||
|                             .joinToString() | ||||
|                             .takeUnless { it.isBlank() } | ||||
|                     } | ||||
|                         ?: stringResource(MR.strings.none) | ||||
|                     subtitle?.format(combined) | ||||
|                 }, | ||||
|             val subtitleProvider: @Composable ( | ||||
|                 value: Set<String>, | ||||
|                 entries: ImmutableMap<String, String>, | ||||
|             ) -> String? = { v, e -> | ||||
|                 val combined = remember(v) { | ||||
|                     v.map { e[it] } | ||||
|                         .takeIf { it.isNotEmpty() } | ||||
|                         ?.joinToString() | ||||
|                 } ?: stringResource(MR.strings.none) | ||||
|                 subtitle?.format(combined) | ||||
|             }, | ||||
|             override val icon: ImageVector? = null, | ||||
|             override val enabled: Boolean = true, | ||||
|             override val onValueChanged: suspend (value: Set<String>) -> Boolean = { true }, | ||||
|             override val onValueChanged: suspend (newValue: Set<String>) -> Boolean = { true }, | ||||
|  | ||||
|             val entries: ImmutableMap<String, String>, | ||||
|         ) : PreferenceItem<Set<String>>() | ||||
|  | ||||
|         /** | ||||
|          * A [PreferenceItem] that shows a EditText in the dialog. | ||||
|          */ | ||||
|         data class EditTextPreference( | ||||
|             val preference: PreferenceData<String>, | ||||
|             val pref: PreferenceData<String>, | ||||
|             override val title: String, | ||||
|             override val subtitle: String? = "%s", | ||||
|             override val icon: ImageVector? = null, | ||||
|             override val enabled: Boolean = true, | ||||
|             override val onValueChanged: suspend (value: String) -> Boolean = { true }, | ||||
|         ) : PreferenceItem<String>() { | ||||
|             override val icon: ImageVector? = null | ||||
|         } | ||||
|             override val onValueChanged: suspend (newValue: String) -> Boolean = { true }, | ||||
|         ) : PreferenceItem<String>() | ||||
|  | ||||
|         /** | ||||
|          * A [PreferenceItem] for individual tracker. | ||||
|          */ | ||||
|         data class TrackerPreference( | ||||
|             val tracker: Tracker, | ||||
|             override val title: String, | ||||
|             val login: () -> Unit, | ||||
|             val logout: () -> Unit, | ||||
|         ) : PreferenceItem<String>() { | ||||
|             override val title: String = "" | ||||
|             override val enabled: Boolean = true | ||||
|             override val subtitle: String? = null | ||||
|             override val icon: ImageVector? = null | ||||
|             override val onValueChanged: suspend (value: String) -> Boolean = { true } | ||||
|             override val onValueChanged: suspend (newValue: String) -> Boolean = { true } | ||||
|         } | ||||
|  | ||||
|         data class InfoPreference( | ||||
| @@ -157,17 +157,17 @@ sealed class Preference { | ||||
|             override val enabled: Boolean = true | ||||
|             override val subtitle: String? = null | ||||
|             override val icon: ImageVector? = null | ||||
|             override val onValueChanged: suspend (value: String) -> Boolean = { true } | ||||
|             override val onValueChanged: suspend (newValue: String) -> Boolean = { true } | ||||
|         } | ||||
|  | ||||
|         data class CustomPreference( | ||||
|             override val title: String, | ||||
|             val content: @Composable () -> Unit, | ||||
|         ) : PreferenceItem<Unit>() { | ||||
|             val content: @Composable (PreferenceItem<String>) -> Unit, | ||||
|         ) : PreferenceItem<String>() { | ||||
|             override val enabled: Boolean = true | ||||
|             override val subtitle: String? = null | ||||
|             override val icon: ImageVector? = null | ||||
|             override val onValueChanged: suspend (value: Unit) -> Boolean = { true } | ||||
|             override val onValueChanged: suspend (newValue: String) -> Boolean = { true } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -5,8 +5,6 @@ import androidx.compose.animation.expandVertically | ||||
| import androidx.compose.animation.fadeIn | ||||
| import androidx.compose.animation.fadeOut | ||||
| import androidx.compose.animation.shrinkVertically | ||||
| import androidx.compose.foundation.layout.padding | ||||
| import androidx.compose.material3.MaterialTheme | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.runtime.CompositionLocalProvider | ||||
| import androidx.compose.runtime.collectAsState | ||||
| @@ -14,20 +12,16 @@ import androidx.compose.runtime.compositionLocalOf | ||||
| import androidx.compose.runtime.getValue | ||||
| import androidx.compose.runtime.rememberCoroutineScope | ||||
| import androidx.compose.runtime.structuralEqualityPolicy | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.unit.dp | ||||
| import eu.kanade.presentation.more.settings.widget.EditTextPreferenceWidget | ||||
| import eu.kanade.presentation.more.settings.widget.InfoWidget | ||||
| import eu.kanade.presentation.more.settings.widget.ListPreferenceWidget | ||||
| import eu.kanade.presentation.more.settings.widget.MultiSelectListPreferenceWidget | ||||
| import eu.kanade.presentation.more.settings.widget.PrefsHorizontalPadding | ||||
| import eu.kanade.presentation.more.settings.widget.PrefsVerticalPadding | ||||
| import eu.kanade.presentation.more.settings.widget.SwitchPreferenceWidget | ||||
| import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget | ||||
| import eu.kanade.presentation.more.settings.widget.TitleFontSize | ||||
| import eu.kanade.presentation.more.settings.widget.TrackingPreferenceWidget | ||||
| import kotlinx.coroutines.launch | ||||
| import tachiyomi.presentation.core.components.BaseSliderItem | ||||
| import tachiyomi.presentation.core.components.SliderItem | ||||
| import tachiyomi.presentation.core.util.collectAsState | ||||
|  | ||||
| val LocalPreferenceHighlighted = compositionLocalOf(structuralEqualityPolicy()) { false } | ||||
| @@ -66,7 +60,7 @@ internal fun PreferenceItem( | ||||
|     ) { | ||||
|         when (item) { | ||||
|             is Preference.PreferenceItem.SwitchPreference -> { | ||||
|                 val value by item.preference.collectAsState() | ||||
|                 val value by item.pref.collectAsState() | ||||
|                 SwitchPreferenceWidget( | ||||
|                     title = item.title, | ||||
|                     subtitle = item.subtitle, | ||||
| @@ -75,33 +69,29 @@ internal fun PreferenceItem( | ||||
|                     onCheckedChanged = { newValue -> | ||||
|                         scope.launch { | ||||
|                             if (item.onValueChanged(newValue)) { | ||||
|                                 item.preference.set(newValue) | ||||
|                                 item.pref.set(newValue) | ||||
|                             } | ||||
|                         } | ||||
|                     }, | ||||
|                 ) | ||||
|             } | ||||
|             is Preference.PreferenceItem.SliderPreference -> { | ||||
|                 BaseSliderItem( | ||||
|                 // TODO: use different composable? | ||||
|                 SliderItem( | ||||
|                     label = item.title, | ||||
|                     min = item.min, | ||||
|                     max = item.max, | ||||
|                     value = item.value, | ||||
|                     valueRange = item.valueRange, | ||||
|                     valueText = item.subtitle.takeUnless { it.isNullOrEmpty() } ?: item.value.toString(), | ||||
|                     steps = item.steps, | ||||
|                     labelStyle = MaterialTheme.typography.titleLarge.copy(fontSize = TitleFontSize), | ||||
|                     onChange = { | ||||
|                         scope.launch { | ||||
|                             item.onValueChanged(it) | ||||
|                         } | ||||
|                     }, | ||||
|                     modifier = Modifier.padding( | ||||
|                         horizontal = PrefsHorizontalPadding, | ||||
|                         vertical = PrefsVerticalPadding, | ||||
|                     ), | ||||
|                 ) | ||||
|             } | ||||
|             is Preference.PreferenceItem.ListPreference<*> -> { | ||||
|                 val value by item.preference.collectAsState() | ||||
|                 val value by item.pref.collectAsState() | ||||
|                 ListPreferenceWidget( | ||||
|                     value = value, | ||||
|                     title = item.title, | ||||
| @@ -128,14 +118,14 @@ internal fun PreferenceItem( | ||||
|                 ) | ||||
|             } | ||||
|             is Preference.PreferenceItem.MultiSelectListPreference -> { | ||||
|                 val values by item.preference.collectAsState() | ||||
|                 val values by item.pref.collectAsState() | ||||
|                 MultiSelectListPreferenceWidget( | ||||
|                     preference = item, | ||||
|                     values = values, | ||||
|                     onValuesChange = { newValues -> | ||||
|                         scope.launch { | ||||
|                             if (item.onValueChanged(newValues)) { | ||||
|                                 item.preference.set(newValues.toMutableSet()) | ||||
|                                 item.pref.set(newValues.toMutableSet()) | ||||
|                             } | ||||
|                         } | ||||
|                     }, | ||||
| @@ -150,7 +140,7 @@ internal fun PreferenceItem( | ||||
|                 ) | ||||
|             } | ||||
|             is Preference.PreferenceItem.EditTextPreference -> { | ||||
|                 val values by item.preference.collectAsState() | ||||
|                 val values by item.pref.collectAsState() | ||||
|                 EditTextPreferenceWidget( | ||||
|                     title = item.title, | ||||
|                     subtitle = item.subtitle, | ||||
| @@ -158,7 +148,7 @@ internal fun PreferenceItem( | ||||
|                     value = values, | ||||
|                     onConfirm = { | ||||
|                         val accepted = item.onValueChanged(it) | ||||
|                         if (accepted) item.preference.set(it) | ||||
|                         if (accepted) item.pref.set(it) | ||||
|                         accepted | ||||
|                     }, | ||||
|                 ) | ||||
| @@ -177,7 +167,7 @@ internal fun PreferenceItem( | ||||
|                 InfoWidget(text = item.title) | ||||
|             } | ||||
|             is Preference.PreferenceItem.CustomPreference -> { | ||||
|                 item.content() | ||||
|                 item.content(item) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|   | ||||
| @@ -47,8 +47,8 @@ import eu.kanade.tachiyomi.network.PREF_DOH_QUAD9 | ||||
| import eu.kanade.tachiyomi.network.PREF_DOH_SHECAN | ||||
| import eu.kanade.tachiyomi.ui.more.OnboardingScreen | ||||
| import eu.kanade.tachiyomi.util.CrashLogUtil | ||||
| import eu.kanade.tachiyomi.util.system.GLUtil | ||||
| import eu.kanade.tachiyomi.util.system.isReleaseBuildType | ||||
| import eu.kanade.tachiyomi.util.system.isDevFlavor | ||||
| import eu.kanade.tachiyomi.util.system.isPreviewBuildType | ||||
| import eu.kanade.tachiyomi.util.system.isShizukuInstalled | ||||
| import eu.kanade.tachiyomi.util.system.powerManager | ||||
| import eu.kanade.tachiyomi.util.system.setDefaultSettings | ||||
| @@ -61,9 +61,7 @@ import logcat.LogPriority | ||||
| import okhttp3.Headers | ||||
| import tachiyomi.core.common.util.lang.launchNonCancellable | ||||
| import tachiyomi.core.common.util.lang.withUIContext | ||||
| import tachiyomi.core.common.util.system.ImageUtil | ||||
| import tachiyomi.core.common.util.system.logcat | ||||
| import tachiyomi.domain.library.service.LibraryPreferences | ||||
| import tachiyomi.domain.manga.interactor.ResetViewerFlags | ||||
| import tachiyomi.i18n.MR | ||||
| import tachiyomi.presentation.core.i18n.stringResource | ||||
| @@ -86,7 +84,6 @@ object SettingsAdvancedScreen : SearchableSettings { | ||||
|  | ||||
|         val basePreferences = remember { Injekt.get<BasePreferences>() } | ||||
|         val networkPreferences = remember { Injekt.get<NetworkPreferences>() } | ||||
|         val libraryPreferences = remember { Injekt.get<LibraryPreferences>() } | ||||
|  | ||||
|         return listOf( | ||||
|             Preference.PreferenceItem.TextPreference( | ||||
| @@ -99,7 +96,7 @@ object SettingsAdvancedScreen : SearchableSettings { | ||||
|                 }, | ||||
|             ), | ||||
|             Preference.PreferenceItem.SwitchPreference( | ||||
|                 preference = networkPreferences.verboseLogging(), | ||||
|                 pref = networkPreferences.verboseLogging(), | ||||
|                 title = stringResource(MR.strings.pref_verbose_logging), | ||||
|                 subtitle = stringResource(MR.strings.pref_verbose_logging_summary), | ||||
|                 onValueChanged = { | ||||
| @@ -127,7 +124,7 @@ object SettingsAdvancedScreen : SearchableSettings { | ||||
|             getBackgroundActivityGroup(), | ||||
|             getDataGroup(), | ||||
|             getNetworkGroup(networkPreferences = networkPreferences), | ||||
|             getLibraryGroup(libraryPreferences = libraryPreferences), | ||||
|             getLibraryGroup(), | ||||
|             getReaderGroup(basePreferences = basePreferences), | ||||
|             getExtensionsGroup(basePreferences = basePreferences), | ||||
|         ) | ||||
| @@ -238,7 +235,8 @@ object SettingsAdvancedScreen : SearchableSettings { | ||||
|                     }, | ||||
|                 ), | ||||
|                 Preference.PreferenceItem.ListPreference( | ||||
|                     preference = networkPreferences.dohProvider(), | ||||
|                     pref = networkPreferences.dohProvider(), | ||||
|                     title = stringResource(MR.strings.pref_dns_over_https), | ||||
|                     entries = persistentMapOf( | ||||
|                         -1 to stringResource(MR.strings.disabled), | ||||
|                         PREF_DOH_CLOUDFLARE to "Cloudflare", | ||||
| @@ -254,14 +252,13 @@ object SettingsAdvancedScreen : SearchableSettings { | ||||
|                         PREF_DOH_NJALLA to "Njalla", | ||||
|                         PREF_DOH_SHECAN to "Shecan", | ||||
|                     ), | ||||
|                     title = stringResource(MR.strings.pref_dns_over_https), | ||||
|                     onValueChanged = { | ||||
|                         context.toast(MR.strings.requires_app_restart) | ||||
|                         true | ||||
|                     }, | ||||
|                 ), | ||||
|                 Preference.PreferenceItem.EditTextPreference( | ||||
|                     preference = userAgentPref, | ||||
|                     pref = userAgentPref, | ||||
|                     title = stringResource(MR.strings.pref_user_agent_string), | ||||
|                     onValueChanged = { | ||||
|                         try { | ||||
| @@ -288,9 +285,7 @@ object SettingsAdvancedScreen : SearchableSettings { | ||||
|     } | ||||
|  | ||||
|     @Composable | ||||
|     private fun getLibraryGroup( | ||||
|         libraryPreferences: LibraryPreferences, | ||||
|     ): Preference.PreferenceGroup { | ||||
|     private fun getLibraryGroup(): Preference.PreferenceGroup { | ||||
|         val scope = rememberCoroutineScope() | ||||
|         val context = LocalContext.current | ||||
|  | ||||
| @@ -318,11 +313,6 @@ object SettingsAdvancedScreen : SearchableSettings { | ||||
|                         } | ||||
|                     }, | ||||
|                 ), | ||||
|                 Preference.PreferenceItem.SwitchPreference( | ||||
|                     preference = libraryPreferences.updateMangaTitles(), | ||||
|                     title = stringResource(MR.strings.pref_update_library_manga_titles), | ||||
|                     subtitle = stringResource(MR.strings.pref_update_library_manga_titles_summary), | ||||
|                 ), | ||||
|             ), | ||||
|         ) | ||||
|     } | ||||
| @@ -344,31 +334,6 @@ object SettingsAdvancedScreen : SearchableSettings { | ||||
|         return Preference.PreferenceGroup( | ||||
|             title = stringResource(MR.strings.pref_category_reader), | ||||
|             preferenceItems = persistentListOf( | ||||
|                 Preference.PreferenceItem.ListPreference( | ||||
|                     preference = basePreferences.hardwareBitmapThreshold(), | ||||
|                     entries = GLUtil.CUSTOM_TEXTURE_LIMIT_OPTIONS | ||||
|                         .mapIndexed { index, option -> | ||||
|                             val display = if (index == 0) { | ||||
|                                 stringResource(MR.strings.pref_hardware_bitmap_threshold_default, option) | ||||
|                             } else { | ||||
|                                 option.toString() | ||||
|                             } | ||||
|                             option to display | ||||
|                         } | ||||
|                         .toMap() | ||||
|                         .toImmutableMap(), | ||||
|                     title = stringResource(MR.strings.pref_hardware_bitmap_threshold), | ||||
|                     subtitleProvider = { value, options -> | ||||
|                         stringResource(MR.strings.pref_hardware_bitmap_threshold_summary, options[value].orEmpty()) | ||||
|                     }, | ||||
|                     enabled = !ImageUtil.HARDWARE_BITMAP_UNSUPPORTED && | ||||
|                         GLUtil.DEVICE_TEXTURE_LIMIT > GLUtil.SAFE_TEXTURE_LIMIT, | ||||
|                 ), | ||||
|                 Preference.PreferenceItem.SwitchPreference( | ||||
|                     preference = basePreferences.alwaysDecodeLongStripWithSSIV(), | ||||
|                     title = stringResource(MR.strings.pref_always_decode_long_strip_with_ssiv_2), | ||||
|                     subtitle = stringResource(MR.strings.pref_always_decode_long_strip_with_ssiv_summary), | ||||
|                 ), | ||||
|                 Preference.PreferenceItem.TextPreference( | ||||
|                     title = stringResource(MR.strings.pref_display_profile), | ||||
|                     subtitle = basePreferences.displayProfile().get(), | ||||
| @@ -417,19 +382,19 @@ object SettingsAdvancedScreen : SearchableSettings { | ||||
|             title = stringResource(MR.strings.label_extensions), | ||||
|             preferenceItems = persistentListOf( | ||||
|                 Preference.PreferenceItem.ListPreference( | ||||
|                     preference = extensionInstallerPref, | ||||
|                     pref = extensionInstallerPref, | ||||
|                     title = stringResource(MR.strings.ext_installer_pref), | ||||
|                     entries = extensionInstallerPref.entries | ||||
|                         .filter { | ||||
|                             // TODO: allow private option in stable versions once URL handling is more fleshed out | ||||
|                             if (isReleaseBuildType) { | ||||
|                                 it != BasePreferences.ExtensionInstaller.PRIVATE | ||||
|                             } else { | ||||
|                             if (isPreviewBuildType || isDevFlavor) { | ||||
|                                 true | ||||
|                             } else { | ||||
|                                 it != BasePreferences.ExtensionInstaller.PRIVATE | ||||
|                             } | ||||
|                         } | ||||
|                         .associateWith { stringResource(it.titleRes) } | ||||
|                         .toImmutableMap(), | ||||
|                     title = stringResource(MR.strings.ext_installer_pref), | ||||
|                     onValueChanged = { | ||||
|                         if (it == BasePreferences.ExtensionInstaller.SHIZUKU && | ||||
|                             !context.isShizukuInstalled | ||||
|   | ||||
| @@ -82,7 +82,7 @@ object SettingsAppearanceScreen : SearchableSettings { | ||||
|                     } | ||||
|                 }, | ||||
|                 Preference.PreferenceItem.SwitchPreference( | ||||
|                     preference = amoledPref, | ||||
|                     pref = amoledPref, | ||||
|                     title = stringResource(MR.strings.pref_dark_theme_pure_black), | ||||
|                     enabled = themeMode != ThemeMode.LIGHT, | ||||
|                     onValueChanged = { | ||||
| @@ -116,28 +116,28 @@ object SettingsAppearanceScreen : SearchableSettings { | ||||
|                     onClick = { navigator.push(AppLanguageScreen()) }, | ||||
|                 ), | ||||
|                 Preference.PreferenceItem.ListPreference( | ||||
|                     preference = uiPreferences.tabletUiMode(), | ||||
|                     pref = uiPreferences.tabletUiMode(), | ||||
|                     title = stringResource(MR.strings.pref_tablet_ui_mode), | ||||
|                     entries = TabletUiMode.entries | ||||
|                         .associateWith { stringResource(it.titleRes) } | ||||
|                         .toImmutableMap(), | ||||
|                     title = stringResource(MR.strings.pref_tablet_ui_mode), | ||||
|                     onValueChanged = { | ||||
|                         context.toast(MR.strings.requires_app_restart) | ||||
|                         true | ||||
|                     }, | ||||
|                 ), | ||||
|                 Preference.PreferenceItem.ListPreference( | ||||
|                     preference = uiPreferences.dateFormat(), | ||||
|                     pref = uiPreferences.dateFormat(), | ||||
|                     title = stringResource(MR.strings.pref_date_format), | ||||
|                     entries = DateFormats | ||||
|                         .associateWith { | ||||
|                             val formattedDate = UiPreferences.dateFormat(it).format(now) | ||||
|                             "${it.ifEmpty { stringResource(MR.strings.label_default) }} ($formattedDate)" | ||||
|                         } | ||||
|                         .toImmutableMap(), | ||||
|                     title = stringResource(MR.strings.pref_date_format), | ||||
|                 ), | ||||
|                 Preference.PreferenceItem.SwitchPreference( | ||||
|                     preference = uiPreferences.relativeTime(), | ||||
|                     pref = uiPreferences.relativeTime(), | ||||
|                     title = stringResource(MR.strings.pref_relative_format), | ||||
|                     subtitle = stringResource( | ||||
|                         MR.strings.pref_relative_format_summary, | ||||
| @@ -145,10 +145,6 @@ object SettingsAppearanceScreen : SearchableSettings { | ||||
|                         formattedNow, | ||||
|                     ), | ||||
|                 ), | ||||
|                 Preference.PreferenceItem.SwitchPreference( | ||||
|                     preference = uiPreferences.imagesInDescription(), | ||||
|                     title = stringResource(MR.strings.pref_display_images_description), | ||||
|                 ), | ||||
|             ), | ||||
|         ) | ||||
|     } | ||||
|   | ||||
| @@ -43,7 +43,7 @@ object SettingsBrowseScreen : SearchableSettings { | ||||
|                 title = stringResource(MR.strings.label_sources), | ||||
|                 preferenceItems = persistentListOf( | ||||
|                     Preference.PreferenceItem.SwitchPreference( | ||||
|                         preference = sourcePreferences.hideInLibraryItems(), | ||||
|                         pref = sourcePreferences.hideInLibraryItems(), | ||||
|                         title = stringResource(MR.strings.pref_hide_in_library_items), | ||||
|                     ), | ||||
|                     Preference.PreferenceItem.TextPreference( | ||||
| @@ -59,7 +59,7 @@ object SettingsBrowseScreen : SearchableSettings { | ||||
|                 title = stringResource(MR.strings.pref_category_nsfw_content), | ||||
|                 preferenceItems = persistentListOf( | ||||
|                     Preference.PreferenceItem.SwitchPreference( | ||||
|                         preference = sourcePreferences.showNsfwSource(), | ||||
|                         pref = sourcePreferences.showNsfwSource(), | ||||
|                         title = stringResource(MR.strings.pref_show_nsfw_source), | ||||
|                         subtitle = stringResource(MR.strings.requires_app_restart), | ||||
|                         onValueChanged = { | ||||
|   | ||||
| @@ -7,9 +7,7 @@ import android.net.Uri | ||||
| import androidx.activity.compose.ManagedActivityResultLauncher | ||||
| import androidx.activity.compose.rememberLauncherForActivityResult | ||||
| import androidx.activity.result.contract.ActivityResultContracts | ||||
| import androidx.compose.foundation.layout.Column | ||||
| import androidx.compose.foundation.layout.IntrinsicSize | ||||
| import androidx.compose.foundation.layout.Row | ||||
| import androidx.compose.foundation.layout.RowScope | ||||
| import androidx.compose.foundation.layout.fillMaxHeight | ||||
| import androidx.compose.foundation.layout.fillMaxWidth | ||||
| @@ -17,8 +15,6 @@ import androidx.compose.foundation.layout.height | ||||
| import androidx.compose.foundation.layout.padding | ||||
| import androidx.compose.material.icons.Icons | ||||
| import androidx.compose.material.icons.automirrored.outlined.HelpOutline | ||||
| import androidx.compose.material3.AlertDialog | ||||
| import androidx.compose.material3.Checkbox | ||||
| import androidx.compose.material3.Icon | ||||
| import androidx.compose.material3.IconButton | ||||
| import androidx.compose.material3.MultiChoiceSegmentedButtonRow | ||||
| @@ -26,15 +22,12 @@ import androidx.compose.material3.SegmentedButton | ||||
| import androidx.compose.material3.SegmentedButtonDefaults | ||||
| import androidx.compose.material3.Text | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.runtime.LaunchedEffect | ||||
| import androidx.compose.runtime.ReadOnlyComposable | ||||
| import androidx.compose.runtime.getValue | ||||
| import androidx.compose.runtime.mutableIntStateOf | ||||
| import androidx.compose.runtime.mutableStateOf | ||||
| import androidx.compose.runtime.remember | ||||
| import androidx.compose.runtime.rememberCoroutineScope | ||||
| 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 | ||||
| @@ -52,14 +45,10 @@ import eu.kanade.presentation.util.relativeTimeSpanString | ||||
| import eu.kanade.tachiyomi.data.backup.create.BackupCreateJob | ||||
| import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob | ||||
| import eu.kanade.tachiyomi.data.cache.ChapterCache | ||||
| import eu.kanade.tachiyomi.data.export.LibraryExporter | ||||
| import eu.kanade.tachiyomi.data.export.LibraryExporter.ExportOptions | ||||
| import eu.kanade.tachiyomi.util.system.DeviceUtil | ||||
| import eu.kanade.tachiyomi.util.system.toast | ||||
| import kotlinx.collections.immutable.persistentListOf | ||||
| import kotlinx.collections.immutable.persistentMapOf | ||||
| import kotlinx.coroutines.Dispatchers | ||||
| import kotlinx.coroutines.launch | ||||
| import logcat.LogPriority | ||||
| import tachiyomi.core.common.i18n.stringResource | ||||
| import tachiyomi.core.common.storage.displayablePath | ||||
| @@ -68,11 +57,8 @@ import tachiyomi.core.common.util.lang.withUIContext | ||||
| import tachiyomi.core.common.util.system.logcat | ||||
| import tachiyomi.domain.backup.service.BackupPreferences | ||||
| import tachiyomi.domain.library.service.LibraryPreferences | ||||
| import tachiyomi.domain.manga.interactor.GetFavorites | ||||
| import tachiyomi.domain.manga.model.Manga | ||||
| import tachiyomi.domain.storage.service.StoragePreferences | ||||
| import tachiyomi.i18n.MR | ||||
| import tachiyomi.presentation.core.components.material.TextButton | ||||
| import tachiyomi.presentation.core.i18n.stringResource | ||||
| import tachiyomi.presentation.core.util.collectAsState | ||||
| import uy.kohesive.injekt.Injekt | ||||
| @@ -109,7 +95,6 @@ object SettingsDataScreen : SearchableSettings { | ||||
|  | ||||
|             getBackupAndRestoreGroup(backupPreferences = backupPreferences), | ||||
|             getDataGroup(), | ||||
|             getExportGroup(), | ||||
|         ) | ||||
|     } | ||||
|  | ||||
| @@ -254,7 +239,8 @@ object SettingsDataScreen : SearchableSettings { | ||||
|  | ||||
|                 // Automatic backups | ||||
|                 Preference.PreferenceItem.ListPreference( | ||||
|                     preference = backupPreferences.backupInterval(), | ||||
|                     pref = backupPreferences.backupInterval(), | ||||
|                     title = stringResource(MR.strings.pref_backup_interval), | ||||
|                     entries = persistentMapOf( | ||||
|                         0 to stringResource(MR.strings.off), | ||||
|                         6 to stringResource(MR.strings.update_6hour), | ||||
| @@ -263,7 +249,6 @@ object SettingsDataScreen : SearchableSettings { | ||||
|                         48 to stringResource(MR.strings.update_48hour), | ||||
|                         168 to stringResource(MR.strings.update_weekly), | ||||
|                     ), | ||||
|                     title = stringResource(MR.strings.pref_backup_interval), | ||||
|                     onValueChanged = { | ||||
|                         BackupCreateJob.setupTask(context, it) | ||||
|                         true | ||||
| @@ -321,147 +306,10 @@ object SettingsDataScreen : SearchableSettings { | ||||
|                     }, | ||||
|                 ), | ||||
|                 Preference.PreferenceItem.SwitchPreference( | ||||
|                     preference = libraryPreferences.autoClearChapterCache(), | ||||
|                     pref = libraryPreferences.autoClearChapterCache(), | ||||
|                     title = stringResource(MR.strings.pref_auto_clear_chapter_cache), | ||||
|                 ), | ||||
|             ), | ||||
|         ) | ||||
|     } | ||||
|  | ||||
|     @Composable | ||||
|     private fun getExportGroup(): Preference.PreferenceGroup { | ||||
|         var showDialog by remember { mutableStateOf(false) } | ||||
|         var exportOptions by remember { | ||||
|             mutableStateOf( | ||||
|                 ExportOptions( | ||||
|                     includeTitle = true, | ||||
|                     includeAuthor = true, | ||||
|                     includeArtist = true, | ||||
|                 ), | ||||
|             ) | ||||
|         } | ||||
|  | ||||
|         val context = LocalContext.current | ||||
|         val scope = rememberCoroutineScope() | ||||
|         val getFavorites = remember { Injekt.get<GetFavorites>() } | ||||
|         var favorites by remember { mutableStateOf<List<Manga>>(emptyList()) } | ||||
|         LaunchedEffect(Unit) { | ||||
|             favorites = getFavorites.await() | ||||
|         } | ||||
|  | ||||
|         val saveFileLauncher = rememberLauncherForActivityResult( | ||||
|             contract = ActivityResultContracts.CreateDocument("text/csv"), | ||||
|         ) { uri -> | ||||
|             uri?.let { | ||||
|                 scope.launch { | ||||
|                     LibraryExporter.exportToCsv( | ||||
|                         context = context, | ||||
|                         uri = it, | ||||
|                         favorites = favorites, | ||||
|                         options = exportOptions, | ||||
|                         onExportComplete = { | ||||
|                             scope.launch(Dispatchers.Main) { | ||||
|                                 context.toast(MR.strings.library_exported) | ||||
|                             } | ||||
|                         }, | ||||
|                     ) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         if (showDialog) { | ||||
|             ColumnSelectionDialog( | ||||
|                 options = exportOptions, | ||||
|                 onConfirm = { options -> | ||||
|                     exportOptions = options | ||||
|                     saveFileLauncher.launch("mihon_library.csv") | ||||
|                 }, | ||||
|                 onDismissRequest = { showDialog = false }, | ||||
|             ) | ||||
|         } | ||||
|  | ||||
|         return Preference.PreferenceGroup( | ||||
|             title = stringResource(MR.strings.export), | ||||
|             preferenceItems = persistentListOf( | ||||
|                 Preference.PreferenceItem.TextPreference( | ||||
|                     title = stringResource(MR.strings.library_list), | ||||
|                     onClick = { showDialog = true }, | ||||
|                 ), | ||||
|             ), | ||||
|         ) | ||||
|     } | ||||
|  | ||||
|     @Composable | ||||
|     private fun ColumnSelectionDialog( | ||||
|         options: ExportOptions, | ||||
|         onConfirm: (ExportOptions) -> Unit, | ||||
|         onDismissRequest: () -> Unit, | ||||
|     ) { | ||||
|         var titleSelected by remember { mutableStateOf(options.includeTitle) } | ||||
|         var authorSelected by remember { mutableStateOf(options.includeAuthor) } | ||||
|         var artistSelected by remember { mutableStateOf(options.includeArtist) } | ||||
|  | ||||
|         AlertDialog( | ||||
|             onDismissRequest = onDismissRequest, | ||||
|             title = { | ||||
|                 Text(text = stringResource(MR.strings.migration_dialog_what_to_include)) | ||||
|             }, | ||||
|             text = { | ||||
|                 Column { | ||||
|                     Row(verticalAlignment = Alignment.CenterVertically) { | ||||
|                         Checkbox( | ||||
|                             checked = titleSelected, | ||||
|                             onCheckedChange = { checked -> | ||||
|                                 titleSelected = checked | ||||
|                                 if (!checked) { | ||||
|                                     authorSelected = false | ||||
|                                     artistSelected = false | ||||
|                                 } | ||||
|                             }, | ||||
|                         ) | ||||
|                         Text(text = stringResource(MR.strings.title)) | ||||
|                     } | ||||
|  | ||||
|                     Row(verticalAlignment = Alignment.CenterVertically) { | ||||
|                         Checkbox( | ||||
|                             checked = authorSelected, | ||||
|                             onCheckedChange = { authorSelected = it }, | ||||
|                             enabled = titleSelected, | ||||
|                         ) | ||||
|                         Text(text = stringResource(MR.strings.author)) | ||||
|                     } | ||||
|  | ||||
|                     Row(verticalAlignment = Alignment.CenterVertically) { | ||||
|                         Checkbox( | ||||
|                             checked = artistSelected, | ||||
|                             onCheckedChange = { artistSelected = it }, | ||||
|                             enabled = titleSelected, | ||||
|                         ) | ||||
|                         Text(text = stringResource(MR.strings.artist)) | ||||
|                     } | ||||
|                 } | ||||
|             }, | ||||
|             confirmButton = { | ||||
|                 TextButton( | ||||
|                     onClick = { | ||||
|                         onConfirm( | ||||
|                             ExportOptions( | ||||
|                                 includeTitle = titleSelected, | ||||
|                                 includeAuthor = authorSelected, | ||||
|                                 includeArtist = artistSelected, | ||||
|                             ), | ||||
|                         ) | ||||
|                         onDismissRequest() | ||||
|                     }, | ||||
|                 ) { | ||||
|                     Text(text = stringResource(MR.strings.action_save)) | ||||
|                 } | ||||
|             }, | ||||
|             dismissButton = { | ||||
|                 TextButton(onClick = onDismissRequest) { | ||||
|                     Text(text = stringResource(MR.strings.action_cancel)) | ||||
|                 } | ||||
|             }, | ||||
|         ) | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -15,6 +15,7 @@ import eu.kanade.presentation.more.settings.widget.TriStateListDialog | ||||
| import kotlinx.collections.immutable.persistentListOf | ||||
| import kotlinx.collections.immutable.persistentMapOf | ||||
| import kotlinx.collections.immutable.toImmutableMap | ||||
| import kotlinx.coroutines.runBlocking | ||||
| import tachiyomi.domain.category.interactor.GetCategories | ||||
| import tachiyomi.domain.category.model.Category | ||||
| import tachiyomi.domain.download.service.DownloadPreferences | ||||
| @@ -34,20 +35,20 @@ object SettingsDownloadScreen : SearchableSettings { | ||||
|     @Composable | ||||
|     override fun getPreferences(): List<Preference> { | ||||
|         val getCategories = remember { Injekt.get<GetCategories>() } | ||||
|         val allCategories by getCategories.subscribe().collectAsState(initial = emptyList()) | ||||
|         val allCategories by getCategories.subscribe().collectAsState(initial = runBlocking { getCategories.await() }) | ||||
|  | ||||
|         val downloadPreferences = remember { Injekt.get<DownloadPreferences>() } | ||||
|         return listOf( | ||||
|             Preference.PreferenceItem.SwitchPreference( | ||||
|                 preference = downloadPreferences.downloadOnlyOverWifi(), | ||||
|                 pref = downloadPreferences.downloadOnlyOverWifi(), | ||||
|                 title = stringResource(MR.strings.connected_to_wifi), | ||||
|             ), | ||||
|             Preference.PreferenceItem.SwitchPreference( | ||||
|                 preference = downloadPreferences.saveChaptersAsCBZ(), | ||||
|                 pref = downloadPreferences.saveChaptersAsCBZ(), | ||||
|                 title = stringResource(MR.strings.save_chapter_as_cbz), | ||||
|             ), | ||||
|             Preference.PreferenceItem.SwitchPreference( | ||||
|                 preference = downloadPreferences.splitTallImages(), | ||||
|                 pref = downloadPreferences.splitTallImages(), | ||||
|                 title = stringResource(MR.strings.split_tall_images), | ||||
|                 subtitle = stringResource(MR.strings.split_tall_images_summary), | ||||
|             ), | ||||
| @@ -72,11 +73,12 @@ object SettingsDownloadScreen : SearchableSettings { | ||||
|             title = stringResource(MR.strings.pref_category_delete_chapters), | ||||
|             preferenceItems = persistentListOf( | ||||
|                 Preference.PreferenceItem.SwitchPreference( | ||||
|                     preference = downloadPreferences.removeAfterMarkedAsRead(), | ||||
|                     pref = downloadPreferences.removeAfterMarkedAsRead(), | ||||
|                     title = stringResource(MR.strings.pref_remove_after_marked_as_read), | ||||
|                 ), | ||||
|                 Preference.PreferenceItem.ListPreference( | ||||
|                     preference = downloadPreferences.removeAfterReadSlots(), | ||||
|                     pref = downloadPreferences.removeAfterReadSlots(), | ||||
|                     title = stringResource(MR.strings.pref_remove_after_read), | ||||
|                     entries = persistentMapOf( | ||||
|                         -1 to stringResource(MR.strings.disabled), | ||||
|                         0 to stringResource(MR.strings.last_read_chapter), | ||||
| @@ -85,10 +87,9 @@ object SettingsDownloadScreen : SearchableSettings { | ||||
|                         3 to stringResource(MR.strings.fourth_to_last), | ||||
|                         4 to stringResource(MR.strings.fifth_to_last), | ||||
|                     ), | ||||
|                     title = stringResource(MR.strings.pref_remove_after_read), | ||||
|                 ), | ||||
|                 Preference.PreferenceItem.SwitchPreference( | ||||
|                     preference = downloadPreferences.removeBookmarkedChapters(), | ||||
|                     pref = downloadPreferences.removeBookmarkedChapters(), | ||||
|                     title = stringResource(MR.strings.pref_remove_bookmarked_chapters), | ||||
|                 ), | ||||
|                 getExcludedCategoriesPreference( | ||||
| @@ -105,11 +106,11 @@ object SettingsDownloadScreen : SearchableSettings { | ||||
|         categories: () -> List<Category>, | ||||
|     ): Preference.PreferenceItem.MultiSelectListPreference { | ||||
|         return Preference.PreferenceItem.MultiSelectListPreference( | ||||
|             preference = downloadPreferences.removeExcludeCategories(), | ||||
|             pref = downloadPreferences.removeExcludeCategories(), | ||||
|             title = stringResource(MR.strings.pref_remove_exclude_categories), | ||||
|             entries = categories() | ||||
|                 .associate { it.id.toString() to it.visualName } | ||||
|                 .toImmutableMap(), | ||||
|             title = stringResource(MR.strings.pref_remove_exclude_categories), | ||||
|         ) | ||||
|     } | ||||
|  | ||||
| @@ -149,11 +150,11 @@ object SettingsDownloadScreen : SearchableSettings { | ||||
|             title = stringResource(MR.strings.pref_category_auto_download), | ||||
|             preferenceItems = persistentListOf( | ||||
|                 Preference.PreferenceItem.SwitchPreference( | ||||
|                     preference = downloadNewChaptersPref, | ||||
|                     pref = downloadNewChaptersPref, | ||||
|                     title = stringResource(MR.strings.pref_download_new), | ||||
|                 ), | ||||
|                 Preference.PreferenceItem.SwitchPreference( | ||||
|                     preference = downloadNewUnreadChaptersOnlyPref, | ||||
|                     pref = downloadNewUnreadChaptersOnlyPref, | ||||
|                     title = stringResource(MR.strings.pref_download_new_unread_chapters_only), | ||||
|                     enabled = downloadNewChapters, | ||||
|                 ), | ||||
| @@ -164,8 +165,8 @@ object SettingsDownloadScreen : SearchableSettings { | ||||
|                         included = included, | ||||
|                         excluded = excluded, | ||||
|                     ), | ||||
|                     enabled = downloadNewChapters, | ||||
|                     onClick = { showDialog = true }, | ||||
|                     enabled = downloadNewChapters, | ||||
|                 ), | ||||
|             ), | ||||
|         ) | ||||
| @@ -179,7 +180,8 @@ object SettingsDownloadScreen : SearchableSettings { | ||||
|             title = stringResource(MR.strings.download_ahead), | ||||
|             preferenceItems = persistentListOf( | ||||
|                 Preference.PreferenceItem.ListPreference( | ||||
|                     preference = downloadPreferences.autoDownloadWhileReading(), | ||||
|                     pref = downloadPreferences.autoDownloadWhileReading(), | ||||
|                     title = stringResource(MR.strings.auto_download_while_reading), | ||||
|                     entries = listOf(0, 2, 3, 5, 10) | ||||
|                         .associateWith { | ||||
|                             if (it == 0) { | ||||
| @@ -189,7 +191,6 @@ object SettingsDownloadScreen : SearchableSettings { | ||||
|                             } | ||||
|                         } | ||||
|                         .toImmutableMap(), | ||||
|                     title = stringResource(MR.strings.auto_download_while_reading), | ||||
|                 ), | ||||
|                 Preference.PreferenceItem.InfoPreference(stringResource(MR.strings.download_ahead_info)), | ||||
|             ), | ||||
|   | ||||
| @@ -24,6 +24,7 @@ import kotlinx.collections.immutable.persistentListOf | ||||
| import kotlinx.collections.immutable.persistentMapOf | ||||
| import kotlinx.collections.immutable.toImmutableMap | ||||
| import kotlinx.coroutines.launch | ||||
| import kotlinx.coroutines.runBlocking | ||||
| import tachiyomi.domain.category.interactor.GetCategories | ||||
| import tachiyomi.domain.category.interactor.ResetCategoryFlags | ||||
| import tachiyomi.domain.category.model.Category | ||||
| @@ -35,8 +36,6 @@ import tachiyomi.domain.library.service.LibraryPreferences.Companion.MANGA_HAS_U | ||||
| import tachiyomi.domain.library.service.LibraryPreferences.Companion.MANGA_NON_COMPLETED | ||||
| import tachiyomi.domain.library.service.LibraryPreferences.Companion.MANGA_NON_READ | ||||
| import tachiyomi.domain.library.service.LibraryPreferences.Companion.MANGA_OUTSIDE_RELEASE_PERIOD | ||||
| import tachiyomi.domain.library.service.LibraryPreferences.Companion.MARK_DUPLICATE_CHAPTER_READ_EXISTING | ||||
| import tachiyomi.domain.library.service.LibraryPreferences.Companion.MARK_DUPLICATE_CHAPTER_READ_NEW | ||||
| import tachiyomi.i18n.MR | ||||
| import tachiyomi.presentation.core.i18n.pluralStringResource | ||||
| import tachiyomi.presentation.core.i18n.stringResource | ||||
| @@ -54,12 +53,12 @@ object SettingsLibraryScreen : SearchableSettings { | ||||
|     override fun getPreferences(): List<Preference> { | ||||
|         val getCategories = remember { Injekt.get<GetCategories>() } | ||||
|         val libraryPreferences = remember { Injekt.get<LibraryPreferences>() } | ||||
|         val allCategories by getCategories.subscribe().collectAsState(initial = emptyList()) | ||||
|         val allCategories by getCategories.subscribe().collectAsState(initial = runBlocking { getCategories.await() }) | ||||
|  | ||||
|         return listOf( | ||||
|             getCategoriesGroup(LocalNavigator.currentOrThrow, allCategories, libraryPreferences), | ||||
|             getGlobalUpdateGroup(allCategories, libraryPreferences), | ||||
|             getBehaviorGroup(libraryPreferences), | ||||
|             getChapterSwipeActionsGroup(libraryPreferences), | ||||
|         ) | ||||
|     } | ||||
|  | ||||
| @@ -91,12 +90,12 @@ object SettingsLibraryScreen : SearchableSettings { | ||||
|                     onClick = { navigator.push(CategoryScreen()) }, | ||||
|                 ), | ||||
|                 Preference.PreferenceItem.ListPreference( | ||||
|                     preference = libraryPreferences.defaultCategory(), | ||||
|                     entries = ids.zip(labels).toMap().toImmutableMap(), | ||||
|                     pref = libraryPreferences.defaultCategory(), | ||||
|                     title = stringResource(MR.strings.default_category), | ||||
|                     entries = ids.zip(labels).toMap().toImmutableMap(), | ||||
|                 ), | ||||
|                 Preference.PreferenceItem.SwitchPreference( | ||||
|                     preference = libraryPreferences.categorizedDisplaySettings(), | ||||
|                     pref = libraryPreferences.categorizedDisplaySettings(), | ||||
|                     title = stringResource(MR.strings.categorized_display_settings), | ||||
|                     onValueChanged = { | ||||
|                         if (!it) { | ||||
| @@ -148,7 +147,8 @@ object SettingsLibraryScreen : SearchableSettings { | ||||
|             title = stringResource(MR.strings.pref_category_library_update), | ||||
|             preferenceItems = persistentListOf( | ||||
|                 Preference.PreferenceItem.ListPreference( | ||||
|                     preference = autoUpdateIntervalPref, | ||||
|                     pref = autoUpdateIntervalPref, | ||||
|                     title = stringResource(MR.strings.pref_library_update_interval), | ||||
|                     entries = persistentMapOf( | ||||
|                         0 to stringResource(MR.strings.update_never), | ||||
|                         12 to stringResource(MR.strings.update_12hour), | ||||
| @@ -157,22 +157,21 @@ object SettingsLibraryScreen : SearchableSettings { | ||||
|                         72 to stringResource(MR.strings.update_72hour), | ||||
|                         168 to stringResource(MR.strings.update_weekly), | ||||
|                     ), | ||||
|                     title = stringResource(MR.strings.pref_library_update_interval), | ||||
|                     onValueChanged = { | ||||
|                         LibraryUpdateJob.setupTask(context, it) | ||||
|                         true | ||||
|                     }, | ||||
|                 ), | ||||
|                 Preference.PreferenceItem.MultiSelectListPreference( | ||||
|                     preference = libraryPreferences.autoUpdateDeviceRestrictions(), | ||||
|                     pref = libraryPreferences.autoUpdateDeviceRestrictions(), | ||||
|                     enabled = autoUpdateInterval > 0, | ||||
|                     title = stringResource(MR.strings.pref_library_update_restriction), | ||||
|                     subtitle = stringResource(MR.strings.restrictions), | ||||
|                     entries = persistentMapOf( | ||||
|                         DEVICE_ONLY_ON_WIFI to stringResource(MR.strings.connected_to_wifi), | ||||
|                         DEVICE_NETWORK_NOT_METERED to stringResource(MR.strings.network_not_metered), | ||||
|                         DEVICE_CHARGING to stringResource(MR.strings.charging), | ||||
|                     ), | ||||
|                     title = stringResource(MR.strings.pref_library_update_restriction), | ||||
|                     subtitle = stringResource(MR.strings.restrictions), | ||||
|                     enabled = autoUpdateInterval > 0, | ||||
|                     onValueChanged = { | ||||
|                         // Post to event looper to allow the preference to be updated. | ||||
|                         ContextCompat.getMainExecutor(context).execute { LibraryUpdateJob.setupTask(context) } | ||||
| @@ -189,22 +188,22 @@ object SettingsLibraryScreen : SearchableSettings { | ||||
|                     onClick = { showCategoriesDialog = true }, | ||||
|                 ), | ||||
|                 Preference.PreferenceItem.SwitchPreference( | ||||
|                     preference = libraryPreferences.autoUpdateMetadata(), | ||||
|                     pref = libraryPreferences.autoUpdateMetadata(), | ||||
|                     title = stringResource(MR.strings.pref_library_update_refresh_metadata), | ||||
|                     subtitle = stringResource(MR.strings.pref_library_update_refresh_metadata_summary), | ||||
|                 ), | ||||
|                 Preference.PreferenceItem.MultiSelectListPreference( | ||||
|                     preference = libraryPreferences.autoUpdateMangaRestrictions(), | ||||
|                     pref = libraryPreferences.autoUpdateMangaRestrictions(), | ||||
|                     title = stringResource(MR.strings.pref_library_update_smart_update), | ||||
|                     entries = persistentMapOf( | ||||
|                         MANGA_HAS_UNREAD to stringResource(MR.strings.pref_update_only_completely_read), | ||||
|                         MANGA_NON_READ to stringResource(MR.strings.pref_update_only_started), | ||||
|                         MANGA_NON_COMPLETED to stringResource(MR.strings.pref_update_only_non_completed), | ||||
|                         MANGA_OUTSIDE_RELEASE_PERIOD to stringResource(MR.strings.pref_update_only_in_release_period), | ||||
|                     ), | ||||
|                     title = stringResource(MR.strings.pref_library_update_smart_update), | ||||
|                 ), | ||||
|                 Preference.PreferenceItem.SwitchPreference( | ||||
|                     preference = libraryPreferences.newShowUpdatesCount(), | ||||
|                     pref = libraryPreferences.newShowUpdatesCount(), | ||||
|                     title = stringResource(MR.strings.pref_library_update_show_tab_badge), | ||||
|                 ), | ||||
|             ), | ||||
| @@ -212,14 +211,15 @@ object SettingsLibraryScreen : SearchableSettings { | ||||
|     } | ||||
|  | ||||
|     @Composable | ||||
|     private fun getBehaviorGroup( | ||||
|     private fun getChapterSwipeActionsGroup( | ||||
|         libraryPreferences: LibraryPreferences, | ||||
|     ): Preference.PreferenceGroup { | ||||
|         return Preference.PreferenceGroup( | ||||
|             title = stringResource(MR.strings.pref_behavior), | ||||
|             title = stringResource(MR.strings.pref_chapter_swipe), | ||||
|             preferenceItems = persistentListOf( | ||||
|                 Preference.PreferenceItem.ListPreference( | ||||
|                     preference = libraryPreferences.swipeToStartAction(), | ||||
|                     pref = libraryPreferences.swipeToStartAction(), | ||||
|                     title = stringResource(MR.strings.pref_chapter_swipe_start), | ||||
|                     entries = persistentMapOf( | ||||
|                         LibraryPreferences.ChapterSwipeAction.Disabled to | ||||
|                             stringResource(MR.strings.disabled), | ||||
| @@ -230,10 +230,10 @@ object SettingsLibraryScreen : SearchableSettings { | ||||
|                         LibraryPreferences.ChapterSwipeAction.Download to | ||||
|                             stringResource(MR.strings.action_download), | ||||
|                     ), | ||||
|                     title = stringResource(MR.strings.pref_chapter_swipe_start), | ||||
|                 ), | ||||
|                 Preference.PreferenceItem.ListPreference( | ||||
|                     preference = libraryPreferences.swipeToEndAction(), | ||||
|                     pref = libraryPreferences.swipeToEndAction(), | ||||
|                     title = stringResource(MR.strings.pref_chapter_swipe_end), | ||||
|                     entries = persistentMapOf( | ||||
|                         LibraryPreferences.ChapterSwipeAction.Disabled to | ||||
|                             stringResource(MR.strings.disabled), | ||||
| @@ -244,21 +244,6 @@ object SettingsLibraryScreen : SearchableSettings { | ||||
|                         LibraryPreferences.ChapterSwipeAction.Download to | ||||
|                             stringResource(MR.strings.action_download), | ||||
|                     ), | ||||
|                     title = stringResource(MR.strings.pref_chapter_swipe_end), | ||||
|                 ), | ||||
|                 Preference.PreferenceItem.MultiSelectListPreference( | ||||
|                     preference = libraryPreferences.markDuplicateReadChapterAsRead(), | ||||
|                     entries = persistentMapOf( | ||||
|                         MARK_DUPLICATE_CHAPTER_READ_EXISTING to | ||||
|                             stringResource(MR.strings.pref_mark_duplicate_read_chapter_read_existing), | ||||
|                         MARK_DUPLICATE_CHAPTER_READ_NEW to | ||||
|                             stringResource(MR.strings.pref_mark_duplicate_read_chapter_read_new), | ||||
|                     ), | ||||
|                     title = stringResource(MR.strings.pref_mark_duplicate_read_chapter_read), | ||||
|                 ), | ||||
|                 Preference.PreferenceItem.SwitchPreference( | ||||
|                     preference = libraryPreferences.hideMissingChapters(), | ||||
|                     title = stringResource(MR.strings.pref_hide_missing_chapter_indicators), | ||||
|                 ), | ||||
|             ), | ||||
|         ) | ||||
|   | ||||
| @@ -33,33 +33,33 @@ object SettingsReaderScreen : SearchableSettings { | ||||
|  | ||||
|         return listOf( | ||||
|             Preference.PreferenceItem.ListPreference( | ||||
|                 preference = readerPref.defaultReadingMode(), | ||||
|                 pref = readerPref.defaultReadingMode(), | ||||
|                 title = stringResource(MR.strings.pref_viewer_type), | ||||
|                 entries = ReadingMode.entries.drop(1) | ||||
|                     .associate { it.flagValue to stringResource(it.stringRes) } | ||||
|                     .toImmutableMap(), | ||||
|                 title = stringResource(MR.strings.pref_viewer_type), | ||||
|             ), | ||||
|             Preference.PreferenceItem.ListPreference( | ||||
|                 preference = readerPref.doubleTapAnimSpeed(), | ||||
|                 pref = readerPref.doubleTapAnimSpeed(), | ||||
|                 title = stringResource(MR.strings.pref_double_tap_anim_speed), | ||||
|                 entries = persistentMapOf( | ||||
|                     1 to stringResource(MR.strings.double_tap_anim_speed_0), | ||||
|                     500 to stringResource(MR.strings.double_tap_anim_speed_normal), | ||||
|                     250 to stringResource(MR.strings.double_tap_anim_speed_fast), | ||||
|                 ), | ||||
|                 title = stringResource(MR.strings.pref_double_tap_anim_speed), | ||||
|             ), | ||||
|             Preference.PreferenceItem.SwitchPreference( | ||||
|                 preference = readerPref.showReadingMode(), | ||||
|                 pref = readerPref.showReadingMode(), | ||||
|                 title = stringResource(MR.strings.pref_show_reading_mode), | ||||
|                 subtitle = stringResource(MR.strings.pref_show_reading_mode_summary), | ||||
|             ), | ||||
|             Preference.PreferenceItem.SwitchPreference( | ||||
|                 preference = readerPref.showNavigationOverlayOnStart(), | ||||
|                 pref = readerPref.showNavigationOverlayOnStart(), | ||||
|                 title = stringResource(MR.strings.pref_show_navigation_mode), | ||||
|                 subtitle = stringResource(MR.strings.pref_show_navigation_mode_summary), | ||||
|             ), | ||||
|             Preference.PreferenceItem.SwitchPreference( | ||||
|                 preference = readerPref.pageTransitions(), | ||||
|                 pref = readerPref.pageTransitions(), | ||||
|                 title = stringResource(MR.strings.pref_page_transitions), | ||||
|             ), | ||||
|             getDisplayGroup(readerPreferences = readerPref), | ||||
| @@ -80,39 +80,39 @@ object SettingsReaderScreen : SearchableSettings { | ||||
|             title = stringResource(MR.strings.pref_category_display), | ||||
|             preferenceItems = persistentListOf( | ||||
|                 Preference.PreferenceItem.ListPreference( | ||||
|                     preference = readerPreferences.defaultOrientationType(), | ||||
|                     pref = readerPreferences.defaultOrientationType(), | ||||
|                     title = stringResource(MR.strings.pref_rotation_type), | ||||
|                     entries = ReaderOrientation.entries.drop(1) | ||||
|                         .associate { it.flagValue to stringResource(it.stringRes) } | ||||
|                         .toImmutableMap(), | ||||
|                     title = stringResource(MR.strings.pref_rotation_type), | ||||
|                 ), | ||||
|                 Preference.PreferenceItem.ListPreference( | ||||
|                     preference = readerPreferences.readerTheme(), | ||||
|                     pref = readerPreferences.readerTheme(), | ||||
|                     title = stringResource(MR.strings.pref_reader_theme), | ||||
|                     entries = persistentMapOf( | ||||
|                         1 to stringResource(MR.strings.black_background), | ||||
|                         2 to stringResource(MR.strings.gray_background), | ||||
|                         0 to stringResource(MR.strings.white_background), | ||||
|                         3 to stringResource(MR.strings.automatic_background), | ||||
|                     ), | ||||
|                     title = stringResource(MR.strings.pref_reader_theme), | ||||
|                 ), | ||||
|                 Preference.PreferenceItem.SwitchPreference( | ||||
|                     preference = fullscreenPref, | ||||
|                     pref = fullscreenPref, | ||||
|                     title = stringResource(MR.strings.pref_fullscreen), | ||||
|                 ), | ||||
|                 Preference.PreferenceItem.SwitchPreference( | ||||
|                     preference = readerPreferences.cutoutShort(), | ||||
|                     pref = readerPreferences.cutoutShort(), | ||||
|                     title = stringResource(MR.strings.pref_cutout_short), | ||||
|                     enabled = fullscreen && | ||||
|                         Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && | ||||
|                         LocalView.current.rootWindowInsets?.displayCutout != null, // has cutout | ||||
|                 ), | ||||
|                 Preference.PreferenceItem.SwitchPreference( | ||||
|                     preference = readerPreferences.keepScreenOn(), | ||||
|                     pref = readerPreferences.keepScreenOn(), | ||||
|                     title = stringResource(MR.strings.pref_keep_screen_on), | ||||
|                 ), | ||||
|                 Preference.PreferenceItem.SwitchPreference( | ||||
|                     preference = readerPreferences.showPageNumber(), | ||||
|                     pref = readerPreferences.showPageNumber(), | ||||
|                     title = stringResource(MR.strings.pref_show_page_number), | ||||
|                 ), | ||||
|             ), | ||||
| @@ -135,41 +135,43 @@ object SettingsReaderScreen : SearchableSettings { | ||||
|             title = "E-Ink", | ||||
|             preferenceItems = persistentListOf( | ||||
|                 Preference.PreferenceItem.SwitchPreference( | ||||
|                     preference = readerPreferences.flashOnPageChange(), | ||||
|                     pref = readerPreferences.flashOnPageChange(), | ||||
|                     title = stringResource(MR.strings.pref_flash_page), | ||||
|                     subtitle = stringResource(MR.strings.pref_flash_page_summ), | ||||
|                 ), | ||||
|                 Preference.PreferenceItem.SliderPreference( | ||||
|                     value = flashMillis / ReaderPreferences.MILLI_CONVERSION, | ||||
|                     valueRange = 1..15, | ||||
|                     min = 1, | ||||
|                     max = 15, | ||||
|                     title = stringResource(MR.strings.pref_flash_duration), | ||||
|                     subtitle = stringResource(MR.strings.pref_flash_duration_summary, flashMillis), | ||||
|                     enabled = flashPageState, | ||||
|                     onValueChanged = { | ||||
|                         flashMillisPref.set(it * ReaderPreferences.MILLI_CONVERSION) | ||||
|                         true | ||||
|                     }, | ||||
|                     enabled = flashPageState, | ||||
|                 ), | ||||
|                 Preference.PreferenceItem.SliderPreference( | ||||
|                     value = flashInterval, | ||||
|                     valueRange = 1..10, | ||||
|                     min = 1, | ||||
|                     max = 10, | ||||
|                     title = stringResource(MR.strings.pref_flash_page_interval), | ||||
|                     subtitle = pluralStringResource(MR.plurals.pref_pages, flashInterval, flashInterval), | ||||
|                     enabled = flashPageState, | ||||
|                     onValueChanged = { | ||||
|                         flashIntervalPref.set(it) | ||||
|                         true | ||||
|                     }, | ||||
|                     enabled = flashPageState, | ||||
|                 ), | ||||
|                 Preference.PreferenceItem.ListPreference( | ||||
|                     preference = flashColorPref, | ||||
|                     pref = flashColorPref, | ||||
|                     title = stringResource(MR.strings.pref_flash_with), | ||||
|                     entries = persistentMapOf( | ||||
|                         ReaderPreferences.FlashColor.BLACK to stringResource(MR.strings.pref_flash_style_black), | ||||
|                         ReaderPreferences.FlashColor.WHITE to stringResource(MR.strings.pref_flash_style_white), | ||||
|                         ReaderPreferences.FlashColor.WHITE_BLACK | ||||
|                             to stringResource(MR.strings.pref_flash_style_white_black), | ||||
|                     ), | ||||
|                     title = stringResource(MR.strings.pref_flash_with), | ||||
|                     enabled = flashPageState, | ||||
|                 ), | ||||
|             ), | ||||
| @@ -182,19 +184,19 @@ object SettingsReaderScreen : SearchableSettings { | ||||
|             title = stringResource(MR.strings.pref_category_reading), | ||||
|             preferenceItems = persistentListOf( | ||||
|                 Preference.PreferenceItem.SwitchPreference( | ||||
|                     preference = readerPreferences.skipRead(), | ||||
|                     pref = readerPreferences.skipRead(), | ||||
|                     title = stringResource(MR.strings.pref_skip_read_chapters), | ||||
|                 ), | ||||
|                 Preference.PreferenceItem.SwitchPreference( | ||||
|                     preference = readerPreferences.skipFiltered(), | ||||
|                     pref = readerPreferences.skipFiltered(), | ||||
|                     title = stringResource(MR.strings.pref_skip_filtered_chapters), | ||||
|                 ), | ||||
|                 Preference.PreferenceItem.SwitchPreference( | ||||
|                     preference = readerPreferences.skipDupe(), | ||||
|                     pref = readerPreferences.skipDupe(), | ||||
|                     title = stringResource(MR.strings.pref_skip_dupe_chapters), | ||||
|                 ), | ||||
|                 Preference.PreferenceItem.SwitchPreference( | ||||
|                     preference = readerPreferences.alwaysShowChapterTransition(), | ||||
|                     pref = readerPreferences.alwaysShowChapterTransition(), | ||||
|                     title = stringResource(MR.strings.pref_always_show_chapter_transition), | ||||
|                 ), | ||||
|             ), | ||||
| @@ -217,15 +219,16 @@ object SettingsReaderScreen : SearchableSettings { | ||||
|             title = stringResource(MR.strings.pager_viewer), | ||||
|             preferenceItems = persistentListOf( | ||||
|                 Preference.PreferenceItem.ListPreference( | ||||
|                     preference = navModePref, | ||||
|                     pref = navModePref, | ||||
|                     title = stringResource(MR.strings.pref_viewer_nav), | ||||
|                     entries = ReaderPreferences.TapZones | ||||
|                         .mapIndexed { index, it -> index to stringResource(it) } | ||||
|                         .toMap() | ||||
|                         .toImmutableMap(), | ||||
|                     title = stringResource(MR.strings.pref_viewer_nav), | ||||
|                 ), | ||||
|                 Preference.PreferenceItem.ListPreference( | ||||
|                     preference = readerPreferences.pagerNavInverted(), | ||||
|                     pref = readerPreferences.pagerNavInverted(), | ||||
|                     title = stringResource(MR.strings.pref_read_with_tapping_inverted), | ||||
|                     entries = persistentListOf( | ||||
|                         ReaderPreferences.TappingInvertMode.NONE, | ||||
|                         ReaderPreferences.TappingInvertMode.HORIZONTAL, | ||||
| @@ -234,41 +237,40 @@ object SettingsReaderScreen : SearchableSettings { | ||||
|                     ) | ||||
|                         .associateWith { stringResource(it.titleRes) } | ||||
|                         .toImmutableMap(), | ||||
|                     title = stringResource(MR.strings.pref_read_with_tapping_inverted), | ||||
|                     enabled = navMode != 5, | ||||
|                 ), | ||||
|                 Preference.PreferenceItem.ListPreference( | ||||
|                     preference = imageScaleTypePref, | ||||
|                     pref = imageScaleTypePref, | ||||
|                     title = stringResource(MR.strings.pref_image_scale_type), | ||||
|                     entries = ReaderPreferences.ImageScaleType | ||||
|                         .mapIndexed { index, it -> index + 1 to stringResource(it) } | ||||
|                         .toMap() | ||||
|                         .toImmutableMap(), | ||||
|                     title = stringResource(MR.strings.pref_image_scale_type), | ||||
|                 ), | ||||
|                 Preference.PreferenceItem.ListPreference( | ||||
|                     preference = readerPreferences.zoomStart(), | ||||
|                     pref = readerPreferences.zoomStart(), | ||||
|                     title = stringResource(MR.strings.pref_zoom_start), | ||||
|                     entries = ReaderPreferences.ZoomStart | ||||
|                         .mapIndexed { index, it -> index + 1 to stringResource(it) } | ||||
|                         .toMap() | ||||
|                         .toImmutableMap(), | ||||
|                     title = stringResource(MR.strings.pref_zoom_start), | ||||
|                 ), | ||||
|                 Preference.PreferenceItem.SwitchPreference( | ||||
|                     preference = readerPreferences.cropBorders(), | ||||
|                     pref = readerPreferences.cropBorders(), | ||||
|                     title = stringResource(MR.strings.pref_crop_borders), | ||||
|                 ), | ||||
|                 Preference.PreferenceItem.SwitchPreference( | ||||
|                     preference = readerPreferences.landscapeZoom(), | ||||
|                     pref = readerPreferences.landscapeZoom(), | ||||
|                     title = stringResource(MR.strings.pref_landscape_zoom), | ||||
|                     enabled = imageScaleType == 1, | ||||
|                 ), | ||||
|                 Preference.PreferenceItem.SwitchPreference( | ||||
|                     preference = readerPreferences.navigateToPan(), | ||||
|                     pref = readerPreferences.navigateToPan(), | ||||
|                     title = stringResource(MR.strings.pref_navigate_pan), | ||||
|                     enabled = navMode != 5, | ||||
|                 ), | ||||
|                 Preference.PreferenceItem.SwitchPreference( | ||||
|                     preference = dualPageSplitPref, | ||||
|                     pref = dualPageSplitPref, | ||||
|                     title = stringResource(MR.strings.pref_dual_page_split), | ||||
|                     onValueChanged = { | ||||
|                         rotateToFitPref.set(false) | ||||
| @@ -276,13 +278,13 @@ object SettingsReaderScreen : SearchableSettings { | ||||
|                     }, | ||||
|                 ), | ||||
|                 Preference.PreferenceItem.SwitchPreference( | ||||
|                     preference = readerPreferences.dualPageInvertPaged(), | ||||
|                     pref = readerPreferences.dualPageInvertPaged(), | ||||
|                     title = stringResource(MR.strings.pref_dual_page_invert), | ||||
|                     subtitle = stringResource(MR.strings.pref_dual_page_invert_summary), | ||||
|                     enabled = dualPageSplit, | ||||
|                 ), | ||||
|                 Preference.PreferenceItem.SwitchPreference( | ||||
|                     preference = rotateToFitPref, | ||||
|                     pref = rotateToFitPref, | ||||
|                     title = stringResource(MR.strings.pref_page_rotate), | ||||
|                     onValueChanged = { | ||||
|                         dualPageSplitPref.set(false) | ||||
| @@ -290,7 +292,7 @@ object SettingsReaderScreen : SearchableSettings { | ||||
|                     }, | ||||
|                 ), | ||||
|                 Preference.PreferenceItem.SwitchPreference( | ||||
|                     preference = readerPreferences.dualPageRotateToFitInvert(), | ||||
|                     pref = readerPreferences.dualPageRotateToFitInvert(), | ||||
|                     title = stringResource(MR.strings.pref_page_rotate_invert), | ||||
|                     enabled = rotateToFit, | ||||
|                 ), | ||||
| @@ -316,15 +318,16 @@ object SettingsReaderScreen : SearchableSettings { | ||||
|             title = stringResource(MR.strings.webtoon_viewer), | ||||
|             preferenceItems = persistentListOf( | ||||
|                 Preference.PreferenceItem.ListPreference( | ||||
|                     preference = navModePref, | ||||
|                     pref = navModePref, | ||||
|                     title = stringResource(MR.strings.pref_viewer_nav), | ||||
|                     entries = ReaderPreferences.TapZones | ||||
|                         .mapIndexed { index, it -> index to stringResource(it) } | ||||
|                         .toMap() | ||||
|                         .toImmutableMap(), | ||||
|                     title = stringResource(MR.strings.pref_viewer_nav), | ||||
|                 ), | ||||
|                 Preference.PreferenceItem.ListPreference( | ||||
|                     preference = readerPreferences.webtoonNavInverted(), | ||||
|                     pref = readerPreferences.webtoonNavInverted(), | ||||
|                     title = stringResource(MR.strings.pref_read_with_tapping_inverted), | ||||
|                     entries = persistentListOf( | ||||
|                         ReaderPreferences.TappingInvertMode.NONE, | ||||
|                         ReaderPreferences.TappingInvertMode.HORIZONTAL, | ||||
| @@ -333,37 +336,35 @@ object SettingsReaderScreen : SearchableSettings { | ||||
|                     ) | ||||
|                         .associateWith { stringResource(it.titleRes) } | ||||
|                         .toImmutableMap(), | ||||
|                     title = stringResource(MR.strings.pref_read_with_tapping_inverted), | ||||
|                     enabled = navMode != 5, | ||||
|                 ), | ||||
|                 Preference.PreferenceItem.SliderPreference( | ||||
|                     value = webtoonSidePadding, | ||||
|                     valueRange = ReaderPreferences.let { | ||||
|                         it.WEBTOON_PADDING_MIN..it.WEBTOON_PADDING_MAX | ||||
|                     }, | ||||
|                     title = stringResource(MR.strings.pref_webtoon_side_padding), | ||||
|                     subtitle = numberFormat.format(webtoonSidePadding / 100f), | ||||
|                     min = ReaderPreferences.WEBTOON_PADDING_MIN, | ||||
|                     max = ReaderPreferences.WEBTOON_PADDING_MAX, | ||||
|                     onValueChanged = { | ||||
|                         webtoonSidePaddingPref.set(it) | ||||
|                         true | ||||
|                     }, | ||||
|                 ), | ||||
|                 Preference.PreferenceItem.ListPreference( | ||||
|                     preference = readerPreferences.readerHideThreshold(), | ||||
|                     pref = readerPreferences.readerHideThreshold(), | ||||
|                     title = stringResource(MR.strings.pref_hide_threshold), | ||||
|                     entries = persistentMapOf( | ||||
|                         ReaderPreferences.ReaderHideThreshold.HIGHEST to stringResource(MR.strings.pref_highest), | ||||
|                         ReaderPreferences.ReaderHideThreshold.HIGH to stringResource(MR.strings.pref_high), | ||||
|                         ReaderPreferences.ReaderHideThreshold.LOW to stringResource(MR.strings.pref_low), | ||||
|                         ReaderPreferences.ReaderHideThreshold.LOWEST to stringResource(MR.strings.pref_lowest), | ||||
|                     ), | ||||
|                     title = stringResource(MR.strings.pref_hide_threshold), | ||||
|                 ), | ||||
|                 Preference.PreferenceItem.SwitchPreference( | ||||
|                     preference = readerPreferences.cropBordersWebtoon(), | ||||
|                     pref = readerPreferences.cropBordersWebtoon(), | ||||
|                     title = stringResource(MR.strings.pref_crop_borders), | ||||
|                 ), | ||||
|                 Preference.PreferenceItem.SwitchPreference( | ||||
|                     preference = dualPageSplitPref, | ||||
|                     pref = dualPageSplitPref, | ||||
|                     title = stringResource(MR.strings.pref_dual_page_split), | ||||
|                     onValueChanged = { | ||||
|                         rotateToFitPref.set(false) | ||||
| @@ -371,13 +372,13 @@ object SettingsReaderScreen : SearchableSettings { | ||||
|                     }, | ||||
|                 ), | ||||
|                 Preference.PreferenceItem.SwitchPreference( | ||||
|                     preference = readerPreferences.dualPageInvertWebtoon(), | ||||
|                     pref = readerPreferences.dualPageInvertWebtoon(), | ||||
|                     title = stringResource(MR.strings.pref_dual_page_invert), | ||||
|                     subtitle = stringResource(MR.strings.pref_dual_page_invert_summary), | ||||
|                     enabled = dualPageSplit, | ||||
|                 ), | ||||
|                 Preference.PreferenceItem.SwitchPreference( | ||||
|                     preference = rotateToFitPref, | ||||
|                     pref = rotateToFitPref, | ||||
|                     title = stringResource(MR.strings.pref_page_rotate), | ||||
|                     onValueChanged = { | ||||
|                         dualPageSplitPref.set(false) | ||||
| @@ -385,16 +386,16 @@ object SettingsReaderScreen : SearchableSettings { | ||||
|                     }, | ||||
|                 ), | ||||
|                 Preference.PreferenceItem.SwitchPreference( | ||||
|                     preference = readerPreferences.dualPageRotateToFitInvertWebtoon(), | ||||
|                     pref = readerPreferences.dualPageRotateToFitInvertWebtoon(), | ||||
|                     title = stringResource(MR.strings.pref_page_rotate_invert), | ||||
|                     enabled = rotateToFit, | ||||
|                 ), | ||||
|                 Preference.PreferenceItem.SwitchPreference( | ||||
|                     preference = readerPreferences.webtoonDoubleTapZoomEnabled(), | ||||
|                     pref = readerPreferences.webtoonDoubleTapZoomEnabled(), | ||||
|                     title = stringResource(MR.strings.pref_double_tap_zoom), | ||||
|                 ), | ||||
|                 Preference.PreferenceItem.SwitchPreference( | ||||
|                     preference = readerPreferences.webtoonDisableZoomOut(), | ||||
|                     pref = readerPreferences.webtoonDisableZoomOut(), | ||||
|                     title = stringResource(MR.strings.pref_webtoon_disable_zoom_out), | ||||
|                 ), | ||||
|             ), | ||||
| @@ -409,11 +410,11 @@ object SettingsReaderScreen : SearchableSettings { | ||||
|             title = stringResource(MR.strings.pref_reader_navigation), | ||||
|             preferenceItems = persistentListOf( | ||||
|                 Preference.PreferenceItem.SwitchPreference( | ||||
|                     preference = readWithVolumeKeysPref, | ||||
|                     pref = readWithVolumeKeysPref, | ||||
|                     title = stringResource(MR.strings.pref_read_with_volume_keys), | ||||
|                 ), | ||||
|                 Preference.PreferenceItem.SwitchPreference( | ||||
|                     preference = readerPreferences.readWithVolumeKeysInverted(), | ||||
|                     pref = readerPreferences.readWithVolumeKeysInverted(), | ||||
|                     title = stringResource(MR.strings.pref_read_with_volume_keys_inverted), | ||||
|                     enabled = readWithVolumeKeys, | ||||
|                 ), | ||||
| @@ -427,11 +428,11 @@ object SettingsReaderScreen : SearchableSettings { | ||||
|             title = stringResource(MR.strings.pref_reader_actions), | ||||
|             preferenceItems = persistentListOf( | ||||
|                 Preference.PreferenceItem.SwitchPreference( | ||||
|                     preference = readerPreferences.readWithLongTap(), | ||||
|                     pref = readerPreferences.readWithLongTap(), | ||||
|                     title = stringResource(MR.strings.pref_read_with_long_tap), | ||||
|                 ), | ||||
|                 Preference.PreferenceItem.SwitchPreference( | ||||
|                     preference = readerPreferences.folderPerManga(), | ||||
|                     pref = readerPreferences.folderPerManga(), | ||||
|                     title = stringResource(MR.strings.pref_create_folder_per_manga), | ||||
|                     subtitle = stringResource(MR.strings.pref_create_folder_per_manga_summary), | ||||
|                 ), | ||||
|   | ||||
| @@ -7,11 +7,9 @@ import androidx.compose.runtime.remember | ||||
| import androidx.compose.ui.platform.LocalContext | ||||
| import androidx.fragment.app.FragmentActivity | ||||
| import eu.kanade.presentation.more.settings.Preference | ||||
| import eu.kanade.tachiyomi.core.security.PrivacyPreferences | ||||
| import eu.kanade.tachiyomi.core.security.SecurityPreferences | ||||
| import eu.kanade.tachiyomi.util.system.AuthenticatorUtil.authenticate | ||||
| import eu.kanade.tachiyomi.util.system.AuthenticatorUtil.isAuthenticationSupported | ||||
| import eu.kanade.tachiyomi.util.system.telemetryIncluded | ||||
| import kotlinx.collections.immutable.persistentListOf | ||||
| import kotlinx.collections.immutable.toImmutableMap | ||||
| import tachiyomi.core.common.i18n.stringResource | ||||
| @@ -30,92 +28,55 @@ object SettingsSecurityScreen : SearchableSettings { | ||||
|  | ||||
|     @Composable | ||||
|     override fun getPreferences(): List<Preference> { | ||||
|         val securityPreferences = remember { Injekt.get<SecurityPreferences>() } | ||||
|         val privacyPreferences = remember { Injekt.get<PrivacyPreferences>() } | ||||
|         return buildList(2) { | ||||
|             add(getSecurityGroup(securityPreferences)) | ||||
|             if (!telemetryIncluded) return@buildList | ||||
|             add(getFirebaseGroup(privacyPreferences)) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Composable | ||||
|     private fun getSecurityGroup( | ||||
|         securityPreferences: SecurityPreferences, | ||||
|     ): Preference.PreferenceGroup { | ||||
|         val context = LocalContext.current | ||||
|         val securityPreferences = remember { Injekt.get<SecurityPreferences>() } | ||||
|         val authSupported = remember { context.isAuthenticationSupported() } | ||||
|  | ||||
|         val useAuthPref = securityPreferences.useAuthenticator() | ||||
|         val useAuth by useAuthPref.collectAsState() | ||||
|  | ||||
|         return Preference.PreferenceGroup( | ||||
|             title = stringResource(MR.strings.pref_security), | ||||
|             preferenceItems = persistentListOf( | ||||
|                 Preference.PreferenceItem.SwitchPreference( | ||||
|                     preference = useAuthPref, | ||||
|                     title = stringResource(MR.strings.lock_with_biometrics), | ||||
|                     enabled = authSupported, | ||||
|                     onValueChanged = { | ||||
|                         (context as FragmentActivity).authenticate( | ||||
|                             title = context.stringResource(MR.strings.lock_with_biometrics), | ||||
|                         ) | ||||
|                     }, | ||||
|                 ), | ||||
|                 Preference.PreferenceItem.ListPreference( | ||||
|                     preference = securityPreferences.lockAppAfter(), | ||||
|                     entries = LockAfterValues | ||||
|                         .associateWith { | ||||
|                             when (it) { | ||||
|                                 -1 -> stringResource(MR.strings.lock_never) | ||||
|                                 0 -> stringResource(MR.strings.lock_always) | ||||
|                                 else -> pluralStringResource(MR.plurals.lock_after_mins, count = it, it) | ||||
|                             } | ||||
|         return listOf( | ||||
|             Preference.PreferenceItem.SwitchPreference( | ||||
|                 pref = useAuthPref, | ||||
|                 title = stringResource(MR.strings.lock_with_biometrics), | ||||
|                 enabled = authSupported, | ||||
|                 onValueChanged = { | ||||
|                     (context as FragmentActivity).authenticate( | ||||
|                         title = context.stringResource(MR.strings.lock_with_biometrics), | ||||
|                     ) | ||||
|                 }, | ||||
|             ), | ||||
|             Preference.PreferenceItem.ListPreference( | ||||
|                 pref = securityPreferences.lockAppAfter(), | ||||
|                 title = stringResource(MR.strings.lock_when_idle), | ||||
|                 enabled = authSupported && useAuth, | ||||
|                 entries = LockAfterValues | ||||
|                     .associateWith { | ||||
|                         when (it) { | ||||
|                             -1 -> stringResource(MR.strings.lock_never) | ||||
|                             0 -> stringResource(MR.strings.lock_always) | ||||
|                             else -> pluralStringResource(MR.plurals.lock_after_mins, count = it, it) | ||||
|                         } | ||||
|                         .toImmutableMap(), | ||||
|                     title = stringResource(MR.strings.lock_when_idle), | ||||
|                     enabled = authSupported && useAuth, | ||||
|                     onValueChanged = { | ||||
|                         (context as FragmentActivity).authenticate( | ||||
|                             title = context.stringResource(MR.strings.lock_when_idle), | ||||
|                         ) | ||||
|                     }, | ||||
|                 ), | ||||
|  | ||||
|                 Preference.PreferenceItem.SwitchPreference( | ||||
|                     preference = securityPreferences.hideNotificationContent(), | ||||
|                     title = stringResource(MR.strings.hide_notification_content), | ||||
|                 ), | ||||
|                 Preference.PreferenceItem.ListPreference( | ||||
|                     preference = securityPreferences.secureScreen(), | ||||
|                     entries = SecurityPreferences.SecureScreenMode.entries | ||||
|                         .associateWith { stringResource(it.titleRes) } | ||||
|                         .toImmutableMap(), | ||||
|                     title = stringResource(MR.strings.secure_screen), | ||||
|                 ), | ||||
|                 Preference.PreferenceItem.InfoPreference(stringResource(MR.strings.secure_screen_summary)), | ||||
|                     } | ||||
|                     .toImmutableMap(), | ||||
|                 onValueChanged = { | ||||
|                     (context as FragmentActivity).authenticate( | ||||
|                         title = context.stringResource(MR.strings.lock_when_idle), | ||||
|                     ) | ||||
|                 }, | ||||
|             ), | ||||
|         ) | ||||
|     } | ||||
|  | ||||
|     @Composable | ||||
|     private fun getFirebaseGroup( | ||||
|         privacyPreferences: PrivacyPreferences, | ||||
|     ): Preference.PreferenceGroup { | ||||
|         return Preference.PreferenceGroup( | ||||
|             title = stringResource(MR.strings.pref_firebase), | ||||
|             preferenceItems = persistentListOf( | ||||
|                 Preference.PreferenceItem.SwitchPreference( | ||||
|                     preference = privacyPreferences.crashlytics(), | ||||
|                     title = stringResource(MR.strings.onboarding_permission_crashlytics), | ||||
|                     subtitle = stringResource(MR.strings.onboarding_permission_crashlytics_description), | ||||
|                 ), | ||||
|                 Preference.PreferenceItem.SwitchPreference( | ||||
|                     preference = privacyPreferences.analytics(), | ||||
|                     title = stringResource(MR.strings.onboarding_permission_analytics), | ||||
|                     subtitle = stringResource(MR.strings.onboarding_permission_analytics_description), | ||||
|                 ), | ||||
|                 Preference.PreferenceItem.InfoPreference(stringResource(MR.strings.firebase_summary)), | ||||
|             Preference.PreferenceItem.SwitchPreference( | ||||
|                 pref = securityPreferences.hideNotificationContent(), | ||||
|                 title = stringResource(MR.strings.hide_notification_content), | ||||
|             ), | ||||
|             Preference.PreferenceItem.ListPreference( | ||||
|                 pref = securityPreferences.secureScreen(), | ||||
|                 title = stringResource(MR.strings.secure_screen), | ||||
|                 entries = SecurityPreferences.SecureScreenMode.entries | ||||
|                     .associateWith { stringResource(it.titleRes) } | ||||
|                     .toImmutableMap(), | ||||
|             ), | ||||
|             Preference.PreferenceItem.InfoPreference(stringResource(MR.strings.secure_screen_summary)), | ||||
|         ) | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -30,11 +30,8 @@ import androidx.compose.runtime.rememberCoroutineScope | ||||
| import androidx.compose.runtime.setValue | ||||
| import androidx.compose.ui.Alignment | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.autofill.ContentType | ||||
| import androidx.compose.ui.platform.LocalContext | ||||
| import androidx.compose.ui.platform.LocalUriHandler | ||||
| import androidx.compose.ui.semantics.contentType | ||||
| import androidx.compose.ui.semantics.semantics | ||||
| import androidx.compose.ui.text.input.ImeAction | ||||
| import androidx.compose.ui.text.input.KeyboardType | ||||
| import androidx.compose.ui.text.input.PasswordVisualTransformation | ||||
| @@ -43,7 +40,6 @@ import androidx.compose.ui.text.input.VisualTransformation | ||||
| import androidx.compose.ui.text.style.TextAlign | ||||
| import androidx.compose.ui.unit.dp | ||||
| import dev.icerock.moko.resources.StringResource | ||||
| import eu.kanade.domain.track.model.AutoTrackState | ||||
| import eu.kanade.domain.track.service.TrackPreferences | ||||
| import eu.kanade.presentation.more.settings.Preference | ||||
| import eu.kanade.tachiyomi.data.track.EnhancedTracker | ||||
| @@ -57,7 +53,6 @@ import eu.kanade.tachiyomi.util.system.openInBrowser | ||||
| import eu.kanade.tachiyomi.util.system.toast | ||||
| import kotlinx.collections.immutable.persistentListOf | ||||
| import kotlinx.collections.immutable.toImmutableList | ||||
| import kotlinx.collections.immutable.toPersistentMap | ||||
| import tachiyomi.core.common.util.lang.launchIO | ||||
| import tachiyomi.core.common.util.lang.withUIContext | ||||
| import tachiyomi.domain.source.service.SourceManager | ||||
| @@ -90,7 +85,6 @@ object SettingsTrackingScreen : SearchableSettings { | ||||
|         val trackPreferences = remember { Injekt.get<TrackPreferences>() } | ||||
|         val trackerManager = remember { Injekt.get<TrackerManager>() } | ||||
|         val sourceManager = remember { Injekt.get<SourceManager>() } | ||||
|         val autoTrackStatePref = trackPreferences.autoUpdateTrackOnMarkRead() | ||||
|  | ||||
|         var dialog by remember { mutableStateOf<Any?>(null) } | ||||
|         dialog?.run { | ||||
| @@ -128,45 +122,44 @@ object SettingsTrackingScreen : SearchableSettings { | ||||
|  | ||||
|         return listOf( | ||||
|             Preference.PreferenceItem.SwitchPreference( | ||||
|                 preference = trackPreferences.autoUpdateTrack(), | ||||
|                 pref = trackPreferences.autoUpdateTrack(), | ||||
|                 title = stringResource(MR.strings.pref_auto_update_manga_sync), | ||||
|             ), | ||||
|             Preference.PreferenceItem.ListPreference( | ||||
|                 preference = trackPreferences.autoUpdateTrackOnMarkRead(), | ||||
|                 entries = AutoTrackState.entries | ||||
|                     .associateWith { stringResource(it.titleRes) } | ||||
|                     .toPersistentMap(), | ||||
|                 title = stringResource(MR.strings.pref_auto_update_manga_on_mark_read), | ||||
|             ), | ||||
|             Preference.PreferenceGroup( | ||||
|                 title = stringResource(MR.strings.services), | ||||
|                 preferenceItems = persistentListOf( | ||||
|                     Preference.PreferenceItem.TrackerPreference( | ||||
|                         title = trackerManager.myAnimeList.name, | ||||
|                         tracker = trackerManager.myAnimeList, | ||||
|                         login = { context.openInBrowser(MyAnimeListApi.authUrl(), forceDefaultBrowser = true) }, | ||||
|                         logout = { dialog = LogoutDialog(trackerManager.myAnimeList) }, | ||||
|                     ), | ||||
|                     Preference.PreferenceItem.TrackerPreference( | ||||
|                         title = trackerManager.aniList.name, | ||||
|                         tracker = trackerManager.aniList, | ||||
|                         login = { context.openInBrowser(AnilistApi.authUrl(), forceDefaultBrowser = true) }, | ||||
|                         logout = { dialog = LogoutDialog(trackerManager.aniList) }, | ||||
|                     ), | ||||
|                     Preference.PreferenceItem.TrackerPreference( | ||||
|                         title = trackerManager.kitsu.name, | ||||
|                         tracker = trackerManager.kitsu, | ||||
|                         login = { dialog = LoginDialog(trackerManager.kitsu, MR.strings.email) }, | ||||
|                         logout = { dialog = LogoutDialog(trackerManager.kitsu) }, | ||||
|                     ), | ||||
|                     Preference.PreferenceItem.TrackerPreference( | ||||
|                         title = trackerManager.mangaUpdates.name, | ||||
|                         tracker = trackerManager.mangaUpdates, | ||||
|                         login = { dialog = LoginDialog(trackerManager.mangaUpdates, MR.strings.username) }, | ||||
|                         logout = { dialog = LogoutDialog(trackerManager.mangaUpdates) }, | ||||
|                     ), | ||||
|                     Preference.PreferenceItem.TrackerPreference( | ||||
|                         title = trackerManager.shikimori.name, | ||||
|                         tracker = trackerManager.shikimori, | ||||
|                         login = { context.openInBrowser(ShikimoriApi.authUrl(), forceDefaultBrowser = true) }, | ||||
|                         logout = { dialog = LogoutDialog(trackerManager.shikimori) }, | ||||
|                     ), | ||||
|                     Preference.PreferenceItem.TrackerPreference( | ||||
|                         title = trackerManager.bangumi.name, | ||||
|                         tracker = trackerManager.bangumi, | ||||
|                         login = { context.openInBrowser(BangumiApi.authUrl(), forceDefaultBrowser = true) }, | ||||
|                         logout = { dialog = LogoutDialog(trackerManager.bangumi) }, | ||||
| @@ -180,6 +173,7 @@ object SettingsTrackingScreen : SearchableSettings { | ||||
|                     enhancedTrackers.first | ||||
|                         .map { service -> | ||||
|                             Preference.PreferenceItem.TrackerPreference( | ||||
|                                 title = service.name, | ||||
|                                 tracker = service, | ||||
|                                 login = { (service as EnhancedTracker).loginNoop() }, | ||||
|                                 logout = service::logout, | ||||
| @@ -223,9 +217,7 @@ object SettingsTrackingScreen : SearchableSettings { | ||||
|             text = { | ||||
|                 Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { | ||||
|                     OutlinedTextField( | ||||
|                         modifier = Modifier | ||||
|                             .fillMaxWidth() | ||||
|                             .semantics { contentType = ContentType.Username + ContentType.EmailAddress }, | ||||
|                         modifier = Modifier.fillMaxWidth(), | ||||
|                         value = username, | ||||
|                         onValueChange = { username = it }, | ||||
|                         label = { Text(text = stringResource(uNameStringRes)) }, | ||||
| @@ -236,9 +228,7 @@ object SettingsTrackingScreen : SearchableSettings { | ||||
|  | ||||
|                     var hidePassword by remember { mutableStateOf(true) } | ||||
|                     OutlinedTextField( | ||||
|                         modifier = Modifier | ||||
|                             .fillMaxWidth() | ||||
|                             .semantics { contentType = ContentType.Password }, | ||||
|                         modifier = Modifier.fillMaxWidth(), | ||||
|                         value = password, | ||||
|                         onValueChange = { password = it }, | ||||
|                         label = { Text(text = stringResource(MR.strings.password)) }, | ||||
| @@ -287,7 +277,7 @@ object SettingsTrackingScreen : SearchableSettings { | ||||
|                         } | ||||
|                     }, | ||||
|                 ) { | ||||
|                     val id = if (processing) MR.strings.logging_in else MR.strings.login | ||||
|                     val id = if (processing) MR.strings.loading else MR.strings.login | ||||
|                     Text(text = stringResource(id)) | ||||
|                 } | ||||
|             }, | ||||
|   | ||||
| @@ -35,9 +35,7 @@ import eu.kanade.tachiyomi.ui.more.NewUpdateScreen | ||||
| import eu.kanade.tachiyomi.util.CrashLogUtil | ||||
| import eu.kanade.tachiyomi.util.lang.toDateTimestampString | ||||
| import eu.kanade.tachiyomi.util.system.copyToClipboard | ||||
| import eu.kanade.tachiyomi.util.system.isPreviewBuildType | ||||
| import eu.kanade.tachiyomi.util.system.toast | ||||
| import eu.kanade.tachiyomi.util.system.updaterEnabled | ||||
| import kotlinx.coroutines.launch | ||||
| import logcat.LogPriority | ||||
| import tachiyomi.core.common.util.lang.withIOContext | ||||
| @@ -99,7 +97,7 @@ object AboutScreen : Screen() { | ||||
|                     ) | ||||
|                 } | ||||
|  | ||||
|                 if (updaterEnabled) { | ||||
|                 if (BuildConfig.INCLUDE_UPDATER) { | ||||
|                     item { | ||||
|                         TextPreferenceWidget( | ||||
|                             title = stringResource(MR.strings.check_for_updates), | ||||
| @@ -123,7 +121,7 @@ object AboutScreen : Screen() { | ||||
|                                                     versionName = result.release.version, | ||||
|                                                     changelogInfo = result.release.info, | ||||
|                                                     releaseLink = result.release.releaseLink, | ||||
|                                                     downloadLink = result.release.downloadLink, | ||||
|                                                     downloadLink = result.release.getDownloadLink(), | ||||
|                                                 ) | ||||
|                                                 navigator.push(updateScreen) | ||||
|                                             }, | ||||
| @@ -247,7 +245,7 @@ object AboutScreen : Screen() { | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             isPreviewBuildType -> { | ||||
|             BuildConfig.PREVIEW -> { | ||||
|                 "Beta r${BuildConfig.COMMIT_COUNT}".let { | ||||
|                     if (withBuildDate) { | ||||
|                         "$it (${BuildConfig.COMMIT_SHA}, ${getFormattedBuildTime()})" | ||||
|   | ||||
| @@ -6,7 +6,7 @@ import androidx.compose.ui.Modifier | ||||
| import cafe.adriel.voyager.navigator.LocalNavigator | ||||
| import cafe.adriel.voyager.navigator.currentOrThrow | ||||
| import com.mikepenz.aboutlibraries.ui.compose.m3.LibrariesContainer | ||||
| import com.mikepenz.aboutlibraries.ui.compose.util.htmlReadyLicenseContent | ||||
| import com.mikepenz.aboutlibraries.ui.compose.m3.util.htmlReadyLicenseContent | ||||
| import eu.kanade.presentation.components.AppBar | ||||
| import eu.kanade.presentation.util.Screen | ||||
| import tachiyomi.i18n.MR | ||||
|   | ||||
| @@ -1,10 +1,8 @@ | ||||
| package eu.kanade.presentation.more.settings.screen.advanced | ||||
|  | ||||
| import androidx.compose.foundation.clickable | ||||
| import androidx.compose.foundation.layout.Arrangement | ||||
| import androidx.compose.foundation.layout.Column | ||||
| 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.lazy.items | ||||
| @@ -14,17 +12,13 @@ import androidx.compose.material.icons.outlined.SelectAll | ||||
| import androidx.compose.material3.AlertDialog | ||||
| import androidx.compose.material3.Checkbox | ||||
| import androidx.compose.material3.MaterialTheme | ||||
| import androidx.compose.material3.Switch | ||||
| import androidx.compose.material3.Text | ||||
| import androidx.compose.material3.TextButton | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.runtime.Immutable | ||||
| import androidx.compose.runtime.collectAsState | ||||
| import androidx.compose.runtime.getValue | ||||
| import androidx.compose.runtime.mutableStateOf | ||||
| import androidx.compose.runtime.remember | ||||
| import androidx.compose.runtime.rememberCoroutineScope | ||||
| import androidx.compose.runtime.setValue | ||||
| import androidx.compose.ui.Alignment | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.platform.LocalContext | ||||
| @@ -45,7 +39,6 @@ import kotlinx.coroutines.flow.collectLatest | ||||
| import kotlinx.coroutines.flow.update | ||||
| import tachiyomi.core.common.util.lang.launchIO | ||||
| import tachiyomi.core.common.util.lang.launchUI | ||||
| import tachiyomi.core.common.util.lang.toLong | ||||
| import tachiyomi.core.common.util.lang.withNonCancellableContext | ||||
| import tachiyomi.data.Database | ||||
| import tachiyomi.domain.source.interactor.GetSourcesWithNonLibraryManga | ||||
| @@ -54,7 +47,6 @@ import tachiyomi.domain.source.model.SourceWithCount | ||||
| import tachiyomi.i18n.MR | ||||
| import tachiyomi.presentation.core.components.LazyColumnWithAction | ||||
| 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 | ||||
| import tachiyomi.presentation.core.screens.LoadingScreen | ||||
| @@ -76,45 +68,13 @@ class ClearDatabaseScreen : Screen() { | ||||
|             is ClearDatabaseScreenModel.State.Loading -> LoadingScreen() | ||||
|             is ClearDatabaseScreenModel.State.Ready -> { | ||||
|                 if (s.showConfirmation) { | ||||
|                     var keepReadManga by remember { mutableStateOf(true) } | ||||
|                     AlertDialog( | ||||
|                         title = { | ||||
|                             Text(text = stringResource(MR.strings.are_you_sure)) | ||||
|                         }, | ||||
|                         text = { | ||||
|                             Column( | ||||
|                                 verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small), | ||||
|                             ) { | ||||
|                                 Text(text = stringResource(MR.strings.clear_database_text)) | ||||
|                                 Row( | ||||
|                                     modifier = Modifier.fillMaxWidth(), | ||||
|                                     horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall), | ||||
|                                     verticalAlignment = Alignment.CenterVertically, | ||||
|                                 ) { | ||||
|                                     Text( | ||||
|                                         text = stringResource(MR.strings.clear_db_exclude_read), | ||||
|                                         modifier = Modifier.weight(1f), | ||||
|                                     ) | ||||
|                                     Switch( | ||||
|                                         checked = keepReadManga, | ||||
|                                         onCheckedChange = { keepReadManga = it }, | ||||
|                                     ) | ||||
|                                 } | ||||
|                                 if (!keepReadManga) { | ||||
|                                     Text( | ||||
|                                         text = stringResource(MR.strings.clear_database_history_warning), | ||||
|                                         style = MaterialTheme.typography.bodySmall, | ||||
|                                         color = MaterialTheme.colorScheme.error, | ||||
|                                     ) | ||||
|                                 } | ||||
|                             } | ||||
|                         }, | ||||
|                         onDismissRequest = model::hideConfirmation, | ||||
|                         confirmButton = { | ||||
|                             TextButton( | ||||
|                                 onClick = { | ||||
|                                     scope.launchUI { | ||||
|                                         model.removeMangaBySourceId(keepReadManga) | ||||
|                                         model.removeMangaBySourceId() | ||||
|                                         model.clearSelection() | ||||
|                                         model.hideConfirmation() | ||||
|                                         context.toast(MR.strings.clear_database_completed) | ||||
| @@ -129,6 +89,9 @@ class ClearDatabaseScreen : Screen() { | ||||
|                                 Text(text = stringResource(MR.strings.action_cancel)) | ||||
|                             } | ||||
|                         }, | ||||
|                         text = { | ||||
|                             Text(text = stringResource(MR.strings.clear_database_confirmation)) | ||||
|                         }, | ||||
|                     ) | ||||
|                 } | ||||
|  | ||||
| @@ -240,9 +203,9 @@ private class ClearDatabaseScreenModel : StateScreenModel<ClearDatabaseScreenMod | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     suspend fun removeMangaBySourceId(keepReadManga: Boolean) = withNonCancellableContext { | ||||
|     suspend fun removeMangaBySourceId() = withNonCancellableContext { | ||||
|         val state = state.value as? State.Ready ?: return@withNonCancellableContext | ||||
|         database.mangasQueries.deleteNonLibraryManga(state.selection, keepReadManga.toLong()) | ||||
|         database.mangasQueries.deleteMangasNotInLibraryBySourceIds(state.selection) | ||||
|         database.historyQueries.removeResettedHistory() | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -25,6 +25,7 @@ import androidx.compose.runtime.remember | ||||
| import androidx.compose.runtime.setValue | ||||
| import androidx.compose.ui.Alignment | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.composed | ||||
| import androidx.compose.ui.graphics.Color | ||||
| import androidx.compose.ui.text.style.TextOverflow | ||||
| import androidx.compose.ui.unit.dp | ||||
| @@ -85,8 +86,7 @@ internal fun BasePreferenceWidget( | ||||
|     } | ||||
| } | ||||
|  | ||||
| @Composable | ||||
| internal fun Modifier.highlightBackground(highlighted: Boolean): Modifier { | ||||
| internal fun Modifier.highlightBackground(highlighted: Boolean): Modifier = composed { | ||||
|     var highlightFlag by remember { mutableStateOf(false) } | ||||
|     LaunchedEffect(Unit) { | ||||
|         if (highlighted) { | ||||
| @@ -116,7 +116,7 @@ internal fun Modifier.highlightBackground(highlighted: Boolean): Modifier { | ||||
|         }, | ||||
|         label = "highlight", | ||||
|     ) | ||||
|     return this.background(color = highlight) | ||||
|     Modifier.background(color = highlight) | ||||
| } | ||||
|  | ||||
| internal val TrailingWidgetBuffer = 16.dp | ||||
|   | ||||
| @@ -45,8 +45,8 @@ fun ReaderAppBars( | ||||
|     onClickTopAppBar: () -> Unit, | ||||
|     bookmarked: Boolean, | ||||
|     onToggleBookmarked: () -> Unit, | ||||
|     onOpenInWebView: (() -> Unit)?, | ||||
|     onOpenInBrowser: (() -> Unit)?, | ||||
|     onOpenInWebView: (() -> Unit)?, | ||||
|     onShare: (() -> Unit)?, | ||||
|  | ||||
|     viewer: Viewer?, | ||||
| @@ -56,7 +56,7 @@ fun ReaderAppBars( | ||||
|     enabledPrevious: Boolean, | ||||
|     currentPage: Int, | ||||
|     totalPages: Int, | ||||
|     onPageIndexChange: (Int) -> Unit, | ||||
|     onSliderValueChange: (Int) -> Unit, | ||||
|  | ||||
|     readingMode: ReadingMode, | ||||
|     onClickReadingMode: () -> Unit, | ||||
| @@ -120,14 +120,6 @@ fun ReaderAppBars( | ||||
|                                         onClick = onToggleBookmarked, | ||||
|                                     ), | ||||
|                                 ) | ||||
|                                 onOpenInWebView?.let { | ||||
|                                     add( | ||||
|                                         AppBar.OverflowAction( | ||||
|                                             title = stringResource(MR.strings.action_open_in_web_view), | ||||
|                                             onClick = it, | ||||
|                                         ), | ||||
|                                     ) | ||||
|                                 } | ||||
|                                 onOpenInBrowser?.let { | ||||
|                                     add( | ||||
|                                         AppBar.OverflowAction( | ||||
| @@ -136,6 +128,14 @@ fun ReaderAppBars( | ||||
|                                         ), | ||||
|                                     ) | ||||
|                                 } | ||||
|                                 onOpenInWebView?.let { | ||||
|                                     add( | ||||
|                                         AppBar.OverflowAction( | ||||
|                                             title = stringResource(MR.strings.action_open_in_web_view), | ||||
|                                             onClick = it, | ||||
|                                         ), | ||||
|                                     ) | ||||
|                                 } | ||||
|                                 onShare?.let { | ||||
|                                     add( | ||||
|                                         AppBar.OverflowAction( | ||||
| @@ -176,8 +176,9 @@ fun ReaderAppBars( | ||||
|                     enabledPrevious = enabledPrevious, | ||||
|                     currentPage = currentPage, | ||||
|                     totalPages = totalPages, | ||||
|                     onPageIndexChange = onPageIndexChange, | ||||
|                     onSliderValueChange = onSliderValueChange, | ||||
|                 ) | ||||
|  | ||||
|                 BottomReaderBar( | ||||
|                     backgroundColor = backgroundColor, | ||||
|                     readingMode = readingMode, | ||||
|   | ||||
| @@ -4,7 +4,6 @@ import androidx.compose.foundation.background | ||||
| import androidx.compose.foundation.interaction.MutableInteractionSource | ||||
| import androidx.compose.foundation.interaction.collectIsDraggedAsState | ||||
| import androidx.compose.foundation.isSystemInDarkTheme | ||||
| import androidx.compose.foundation.layout.Box | ||||
| import androidx.compose.foundation.layout.Row | ||||
| import androidx.compose.foundation.layout.Spacer | ||||
| import androidx.compose.foundation.layout.fillMaxWidth | ||||
| @@ -17,6 +16,7 @@ import androidx.compose.material3.FilledIconButton | ||||
| import androidx.compose.material3.Icon | ||||
| import androidx.compose.material3.IconButtonDefaults | ||||
| import androidx.compose.material3.MaterialTheme | ||||
| import androidx.compose.material3.Slider | ||||
| import androidx.compose.material3.Text | ||||
| import androidx.compose.material3.surfaceColorAtElevation | ||||
| import androidx.compose.runtime.Composable | ||||
| @@ -29,7 +29,6 @@ import androidx.compose.runtime.setValue | ||||
| import androidx.compose.ui.Alignment | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.draw.clip | ||||
| import androidx.compose.ui.graphics.Color | ||||
| import androidx.compose.ui.hapticfeedback.HapticFeedbackType | ||||
| import androidx.compose.ui.platform.LocalHapticFeedback | ||||
| import androidx.compose.ui.platform.LocalLayoutDirection | ||||
| @@ -39,8 +38,8 @@ import androidx.compose.ui.unit.dp | ||||
| import eu.kanade.presentation.theme.TachiyomiPreviewTheme | ||||
| import eu.kanade.presentation.util.isTabletUi | ||||
| import tachiyomi.i18n.MR | ||||
| import tachiyomi.presentation.core.components.material.Slider | ||||
| import tachiyomi.presentation.core.i18n.stringResource | ||||
| import kotlin.math.roundToInt | ||||
|  | ||||
| @Composable | ||||
| fun ChapterNavigator( | ||||
| @@ -51,7 +50,7 @@ fun ChapterNavigator( | ||||
|     enabledPrevious: Boolean, | ||||
|     currentPage: Int, | ||||
|     totalPages: Int, | ||||
|     onPageIndexChange: (Int) -> Unit, | ||||
|     onSliderValueChange: (Int) -> Unit, | ||||
| ) { | ||||
|     val isTabletUi = isTabletUi() | ||||
|     val horizontalPadding = if (isTabletUi) 24.dp else 8.dp | ||||
| @@ -98,11 +97,7 @@ fun ChapterNavigator( | ||||
|                             .padding(horizontal = 16.dp), | ||||
|                         verticalAlignment = Alignment.CenterVertically, | ||||
|                     ) { | ||||
|                         Box(contentAlignment = Alignment.CenterEnd) { | ||||
|                             Text(text = currentPage.toString()) | ||||
|                             // Taking up full length so the slider doesn't shift when 'currentPage' length changes | ||||
|                             Text(text = totalPages.toString(), color = Color.Transparent) | ||||
|                         } | ||||
|                         Text(text = currentPage.toString()) | ||||
|  | ||||
|                         val interactionSource = remember { MutableInteractionSource() } | ||||
|                         val sliderDragged by interactionSource.collectIsDraggedAsState() | ||||
| @@ -115,11 +110,14 @@ fun ChapterNavigator( | ||||
|                             modifier = Modifier | ||||
|                                 .weight(1f) | ||||
|                                 .padding(horizontal = 8.dp), | ||||
|                             value = currentPage, | ||||
|                             valueRange = 1..totalPages, | ||||
|                             onValueChange = f@{ | ||||
|                                 if (it == currentPage) return@f | ||||
|                                 onPageIndexChange(it - 1) | ||||
|                             value = currentPage.toFloat(), | ||||
|                             valueRange = 1f..totalPages.toFloat(), | ||||
|                             steps = totalPages - 2, | ||||
|                             onValueChange = { | ||||
|                                 val new = it.roundToInt() - 1 | ||||
|                                 if (new != currentPage) { | ||||
|                                     onSliderValueChange(new) | ||||
|                                 } | ||||
|                             }, | ||||
|                             interactionSource = interactionSource, | ||||
|                         ) | ||||
| @@ -160,7 +158,7 @@ private fun ChapterNavigatorPreview() { | ||||
|             enabledPrevious = true, | ||||
|             currentPage = currentPage, | ||||
|             totalPages = 10, | ||||
|             onPageIndexChange = { currentPage = (it + 1) }, | ||||
|             onSliderValueChange = { currentPage = it }, | ||||
|         ) | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -2,7 +2,6 @@ package eu.kanade.presentation.reader.settings | ||||
|  | ||||
| import androidx.compose.foundation.layout.ColumnScope | ||||
| import androidx.compose.material3.FilterChip | ||||
| import androidx.compose.material3.MaterialTheme | ||||
| import androidx.compose.material3.Text | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.runtime.getValue | ||||
| @@ -37,12 +36,12 @@ internal fun ColumnScope.ColorFilterPage(screenModel: ReaderSettingsScreenModel) | ||||
|     if (customBrightness) { | ||||
|         val customBrightnessValue by screenModel.preferences.customBrightnessValue().collectAsState() | ||||
|         SliderItem( | ||||
|             value = customBrightnessValue, | ||||
|             valueRange = -75..100, | ||||
|             steps = 0, | ||||
|             label = stringResource(MR.strings.pref_custom_brightness), | ||||
|             min = -75, | ||||
|             max = 100, | ||||
|             value = customBrightnessValue, | ||||
|             valueText = customBrightnessValue.toString(), | ||||
|             onChange = { screenModel.preferences.customBrightnessValue().set(it) }, | ||||
|             pillColor = MaterialTheme.colorScheme.surfaceContainerHighest, | ||||
|         ) | ||||
|     } | ||||
|  | ||||
| @@ -54,52 +53,48 @@ internal fun ColumnScope.ColorFilterPage(screenModel: ReaderSettingsScreenModel) | ||||
|     if (colorFilter) { | ||||
|         val colorFilterValue by screenModel.preferences.colorFilterValue().collectAsState() | ||||
|         SliderItem( | ||||
|             value = colorFilterValue.red, | ||||
|             valueRange = 0..255, | ||||
|             steps = 0, | ||||
|             label = stringResource(MR.strings.color_filter_r_value), | ||||
|             max = 255, | ||||
|             value = colorFilterValue.red, | ||||
|             valueText = colorFilterValue.red.toString(), | ||||
|             onChange = { newRValue -> | ||||
|                 screenModel.preferences.colorFilterValue().getAndSet { | ||||
|                     getColorValue(it, newRValue, RED_MASK, 16) | ||||
|                 } | ||||
|             }, | ||||
|             pillColor = MaterialTheme.colorScheme.surfaceContainerHighest, | ||||
|         ) | ||||
|         SliderItem( | ||||
|             value = colorFilterValue.green, | ||||
|             valueRange = 0..255, | ||||
|             steps = 0, | ||||
|             label = stringResource(MR.strings.color_filter_g_value), | ||||
|             max = 255, | ||||
|             value = colorFilterValue.green, | ||||
|             valueText = colorFilterValue.green.toString(), | ||||
|             onChange = { newGValue -> | ||||
|                 screenModel.preferences.colorFilterValue().getAndSet { | ||||
|                     getColorValue(it, newGValue, GREEN_MASK, 8) | ||||
|                 } | ||||
|             }, | ||||
|             pillColor = MaterialTheme.colorScheme.surfaceContainerHighest, | ||||
|         ) | ||||
|         SliderItem( | ||||
|             value = colorFilterValue.blue, | ||||
|             valueRange = 0..255, | ||||
|             steps = 0, | ||||
|             label = stringResource(MR.strings.color_filter_b_value), | ||||
|             max = 255, | ||||
|             value = colorFilterValue.blue, | ||||
|             valueText = colorFilterValue.blue.toString(), | ||||
|             onChange = { newBValue -> | ||||
|                 screenModel.preferences.colorFilterValue().getAndSet { | ||||
|                     getColorValue(it, newBValue, BLUE_MASK, 0) | ||||
|                 } | ||||
|             }, | ||||
|             pillColor = MaterialTheme.colorScheme.surfaceContainerHighest, | ||||
|         ) | ||||
|         SliderItem( | ||||
|             value = colorFilterValue.alpha, | ||||
|             valueRange = 0..255, | ||||
|             steps = 0, | ||||
|             label = stringResource(MR.strings.color_filter_a_value), | ||||
|             max = 255, | ||||
|             value = colorFilterValue.alpha, | ||||
|             valueText = colorFilterValue.alpha.toString(), | ||||
|             onChange = { newAValue -> | ||||
|                 screenModel.preferences.colorFilterValue().getAndSet { | ||||
|                     getColorValue(it, newAValue, ALPHA_MASK, 24) | ||||
|                 } | ||||
|             }, | ||||
|             pillColor = MaterialTheme.colorScheme.surfaceContainerHighest, | ||||
|         ) | ||||
|  | ||||
|         val colorFilterMode by screenModel.preferences.colorFilterMode().collectAsState() | ||||
|   | ||||
| @@ -2,7 +2,6 @@ package eu.kanade.presentation.reader.settings | ||||
|  | ||||
| import androidx.compose.foundation.layout.ColumnScope | ||||
| import androidx.compose.material3.FilterChip | ||||
| import androidx.compose.material3.MaterialTheme | ||||
| import androidx.compose.material3.Text | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.runtime.getValue | ||||
| @@ -98,21 +97,21 @@ internal fun ColumnScope.GeneralPage(screenModel: ReaderSettingsScreenModel) { | ||||
|     if (flashPageState) { | ||||
|         SliderItem( | ||||
|             value = flashMillis / ReaderPreferences.MILLI_CONVERSION, | ||||
|             valueRange = 1..15, | ||||
|             label = stringResource(MR.strings.pref_flash_duration), | ||||
|             valueText = stringResource(MR.strings.pref_flash_duration_summary, flashMillis), | ||||
|             onChange = { flashMillisPref.set(it * ReaderPreferences.MILLI_CONVERSION) }, | ||||
|             pillColor = MaterialTheme.colorScheme.surfaceContainerHighest, | ||||
|             min = 1, | ||||
|             max = 15, | ||||
|         ) | ||||
|         SliderItem( | ||||
|             value = flashInterval, | ||||
|             valueRange = 1..10, | ||||
|             label = stringResource(MR.strings.pref_flash_page_interval), | ||||
|             valueText = pluralStringResource(MR.plurals.pref_pages, flashInterval, flashInterval), | ||||
|             onChange = { | ||||
|                 flashIntervalPref.set(it) | ||||
|             }, | ||||
|             pillColor = MaterialTheme.colorScheme.surfaceContainerHighest, | ||||
|             min = 1, | ||||
|             max = 10, | ||||
|         ) | ||||
|         SettingsChipRow(MR.strings.pref_flash_with) { | ||||
|             flashColors.map { (labelRes, value) -> | ||||
|   | ||||
| @@ -2,7 +2,6 @@ package eu.kanade.presentation.reader.settings | ||||
|  | ||||
| import androidx.compose.foundation.layout.ColumnScope | ||||
| import androidx.compose.material3.FilterChip | ||||
| import androidx.compose.material3.MaterialTheme | ||||
| import androidx.compose.material3.Text | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.runtime.collectAsState | ||||
| @@ -153,14 +152,14 @@ private fun ColumnScope.WebtoonViewerSettings(screenModel: ReaderSettingsScreenM | ||||
|  | ||||
|     val webtoonSidePadding by screenModel.preferences.webtoonSidePadding().collectAsState() | ||||
|     SliderItem( | ||||
|         value = webtoonSidePadding, | ||||
|         valueRange = ReaderPreferences.let { it.WEBTOON_PADDING_MIN..it.WEBTOON_PADDING_MAX }, | ||||
|         label = stringResource(MR.strings.pref_webtoon_side_padding), | ||||
|         min = ReaderPreferences.WEBTOON_PADDING_MIN, | ||||
|         max = ReaderPreferences.WEBTOON_PADDING_MAX, | ||||
|         value = webtoonSidePadding, | ||||
|         valueText = numberFormat.format(webtoonSidePadding / 100f), | ||||
|         onChange = { | ||||
|             screenModel.preferences.webtoonSidePadding().set(it) | ||||
|         }, | ||||
|         pillColor = MaterialTheme.colorScheme.surfaceContainerHighest, | ||||
|     ) | ||||
|  | ||||
|     CheckboxItem( | ||||
|   | ||||
| @@ -9,12 +9,10 @@ import androidx.compose.ui.platform.LocalContext | ||||
| import eu.kanade.domain.ui.UiPreferences | ||||
| import eu.kanade.domain.ui.model.AppTheme | ||||
| import eu.kanade.presentation.theme.colorscheme.BaseColorScheme | ||||
| import eu.kanade.presentation.theme.colorscheme.CatppuccinColorScheme | ||||
| import eu.kanade.presentation.theme.colorscheme.GreenAppleColorScheme | ||||
| import eu.kanade.presentation.theme.colorscheme.LavenderColorScheme | ||||
| import eu.kanade.presentation.theme.colorscheme.MidnightDuskColorScheme | ||||
| import eu.kanade.presentation.theme.colorscheme.MonetColorScheme | ||||
| import eu.kanade.presentation.theme.colorscheme.MonochromeColorScheme | ||||
| import eu.kanade.presentation.theme.colorscheme.NordColorScheme | ||||
| import eu.kanade.presentation.theme.colorscheme.StrawberryColorScheme | ||||
| import eu.kanade.presentation.theme.colorscheme.TachiyomiColorScheme | ||||
| @@ -78,11 +76,9 @@ private fun getThemeColorScheme( | ||||
|  | ||||
| private val colorSchemes: Map<AppTheme, BaseColorScheme> = mapOf( | ||||
|     AppTheme.DEFAULT to TachiyomiColorScheme, | ||||
|     AppTheme.CATPPUCCIN to CatppuccinColorScheme, | ||||
|     AppTheme.GREEN_APPLE to GreenAppleColorScheme, | ||||
|     AppTheme.LAVENDER to LavenderColorScheme, | ||||
|     AppTheme.MIDNIGHT_DUSK to MidnightDuskColorScheme, | ||||
|     AppTheme.MONOCHROME to MonochromeColorScheme, | ||||
|     AppTheme.NORD to NordColorScheme, | ||||
|     AppTheme.STRAWBERRY_DAIQUIRI to StrawberryColorScheme, | ||||
|     AppTheme.TAKO to TakoColorScheme, | ||||
|   | ||||
| @@ -1,103 +0,0 @@ | ||||
| package eu.kanade.presentation.theme.colorscheme | ||||
|  | ||||
| import androidx.compose.material3.darkColorScheme | ||||
| import androidx.compose.material3.lightColorScheme | ||||
| import androidx.compose.ui.graphics.Color | ||||
|  | ||||
| /** | ||||
|  * Colors for Catppuccin theme | ||||
|  * MIT License | ||||
|  * Copyright (c) 2021 Catppuccin | ||||
|  * https://catppuccin.com | ||||
|  * M3 colors generated by Material Theme Builder (https://goo.gle/material-theme-builder-web) | ||||
|  * | ||||
|  * Key colors (dark): | ||||
|  * Primary #CBA6F4 | ||||
|  * Secondary #CBA6F4 | ||||
|  * Tertiary #CBA6F4 | ||||
|  * Neutral #181825 | ||||
|  | ||||
|  * Key colors (light): | ||||
|  * Primary #8839EF | ||||
|  * Secondary #8839EF | ||||
|  * Tertiary #8839EF | ||||
|  * Neutral #E6E9EF | ||||
|  */ | ||||
| internal object CatppuccinColorScheme : BaseColorScheme() { | ||||
|  | ||||
|     override val darkScheme = darkColorScheme( | ||||
|         primary = Color(0xFFCBA6F7), | ||||
|         onPrimary = Color(0xFF11111B), | ||||
|         primaryContainer = Color(0xFFCBA6F7), | ||||
|         onPrimaryContainer = Color(0xFF11111B), | ||||
|         secondary = Color(0xFFCBA6F7), // Unread badge | ||||
|         onSecondary = Color(0xFF11111B), // Unread badge text | ||||
|         secondaryContainer = Color(0xFF313244), // Navigation bar selector pill & progress indicator (remaining) | ||||
|         onSecondaryContainer = Color(0xFFCBA6F7), // Navigation bar selector icon | ||||
|         tertiary = Color(0xFFCBA6F7), // Volume and brightness bars, Downloaded badge | ||||
|         onTertiary = Color(0xFF11111B), // Downloaded badge text | ||||
|         tertiaryContainer = Color(0xFF1E1E2E), | ||||
|         onTertiaryContainer = Color(0xFFCDD6F4), | ||||
|         error = Color(0xFFF38BA8), | ||||
|         onError = Color(0xFF11111B), | ||||
|         errorContainer = Color(0xFFFF0558), | ||||
|         onErrorContainer = Color(0xFFEF9FB4), | ||||
|         background = Color(0xFF181825), | ||||
|         onBackground = Color(0xFFCDD6F4), | ||||
|         surface = Color(0xFF181825), | ||||
|         onSurface = Color(0xFFCDD6F4), | ||||
|         surfaceVariant = Color(0xFF1E1E2E), // Navigation bar background (ThemePrefWidget) | ||||
|         onSurfaceVariant = Color(0xFFCDD6F4), // Button (unselected) | ||||
|         outline = Color(0xFFCBA6F7), | ||||
|         outlineVariant = Color(0xFF585B70), // Outlines for buttons | ||||
|         scrim = Color(0xFF11111B), | ||||
|         inverseSurface = Color(0xFFEFF1F5), // Snackbar or whatever they called | ||||
|         inverseOnSurface = Color(0xFF4C4F69), // Snackbar text | ||||
|         inversePrimary = Color(0xFF8839EF), // Snackbar accent | ||||
|         surfaceDim = Color(0xFF181825), | ||||
|         surfaceBright = Color(0xFF313244), | ||||
|         surfaceContainerLowest = Color(0xFF181825), | ||||
|         surfaceContainerLow = Color(0xFF1E1E2E), // Repo cards | ||||
|         surfaceContainer = Color(0xFF1E1E2E), | ||||
|         surfaceContainerHigh = Color(0xFF1E1E2E), // Filter menu | ||||
|         surfaceContainerHighest = Color(0xFF313244), // Untoggleg button bg | ||||
|     ) | ||||
|  | ||||
|     override val lightScheme = lightColorScheme( | ||||
|         primary = Color(0xFF8839EF), | ||||
|         onPrimary = Color(0xFFDCE0E8), | ||||
|         primaryContainer = Color(0xFF8839EF), | ||||
|         onPrimaryContainer = Color(0xFFDCE0E8), | ||||
|         secondary = Color(0xFF8839EF), // Unread badge | ||||
|         onSecondary = Color(0xFFDCE0E8), // Unread badge text | ||||
|         secondaryContainer = Color(0xFFCDD0DA), // Navigation bar selector pill & progress indicator (remaining) | ||||
|         onSecondaryContainer = Color(0xFF8839EF), // Navigation bar selector icon | ||||
|         tertiary = Color(0xFF8839EF), // Volume and brightness bars, Downloaded badge | ||||
|         onTertiary = Color(0xFFDCE0E8), // Downloaded badge text | ||||
|         tertiaryContainer = Color(0xFFEFF1F5), | ||||
|         onTertiaryContainer = Color(0xFF4C4F69), | ||||
|         error = Color(0xFFD20F39), | ||||
|         onError = Color(0xFFDCE0E8), | ||||
|         errorContainer = Color(0xFF68001C), | ||||
|         onErrorContainer = Color(0xFFD61C41), | ||||
|         background = Color(0xFFE6E9EF), | ||||
|         onBackground = Color(0xFF4C4F69), | ||||
|         surface = Color(0xFFE6E9EF), | ||||
|         onSurface = Color(0xFF4C4F69), | ||||
|         surfaceVariant = Color(0xFFEFF1F5), // Navigation bar background (ThemePrefWidget) | ||||
|         onSurfaceVariant = Color(0xFF4C4F69), // Button (unselected) | ||||
|         outline = Color(0xFF8839EF), | ||||
|         outlineVariant = Color(0xFFACB0BE), // Outlines for buttons | ||||
|         scrim = Color(0xFFDCE0E8), | ||||
|         inverseSurface = Color(0xFF1E1E2E), // Snackbar | ||||
|         inverseOnSurface = Color(0xFFCDD6F4), // Snackbar text | ||||
|         inversePrimary = Color(0xFFCBA6F7), // Snackbar accent | ||||
|         surfaceDim = Color(0xFFE6E9EF), | ||||
|         surfaceBright = Color(0xFFCDD0DA), | ||||
|         surfaceContainerLowest = Color(0xFFE6E9EF), | ||||
|         surfaceContainerLow = Color(0xFFEFF1F5), // Repo cards | ||||
|         surfaceContainer = Color(0xFFEFF1F5), // Navigation bar background | ||||
|         surfaceContainerHigh = Color(0xFFEFF1F5), // Filter menu | ||||
|         surfaceContainerHighest = Color(0xFFCDD0DA), // Untoggleg bg | ||||
|     ) | ||||
| } | ||||
| @@ -1,84 +0,0 @@ | ||||
| package eu.kanade.presentation.theme.colorscheme | ||||
|  | ||||
| import androidx.compose.material3.darkColorScheme | ||||
| import androidx.compose.material3.lightColorScheme | ||||
| import androidx.compose.ui.graphics.Color | ||||
|  | ||||
| internal object MonochromeColorScheme : BaseColorScheme() { | ||||
|  | ||||
|     override val darkScheme = darkColorScheme( | ||||
|         primary = Color(0xFFFFFFFF), | ||||
|         onPrimary = Color(0xFF000000), | ||||
|         primaryContainer = Color(0xFFFFFFFF), | ||||
|         onPrimaryContainer = Color(0xFF000000), | ||||
|         secondary = Color(0xFFFFFFFF), | ||||
|         onSecondary = Color(0xFF000000), | ||||
|         secondaryContainer = Color(0xFF777777), | ||||
|         onSecondaryContainer = Color(0xFF000000), | ||||
|         tertiary = Color(0xFF777777), | ||||
|         onTertiary = Color(0xFFFFFFFF), | ||||
|         tertiaryContainer = Color(0xFFFFFFFF), | ||||
|         onTertiaryContainer = Color(0xFF000000), | ||||
|         error = Color(0xFFFFFFFF), | ||||
|         onError = Color(0xFF000000), | ||||
|         errorContainer = Color(0xFFFFFFFF), | ||||
|         onErrorContainer = Color(0xFF000000), | ||||
|         background = Color(0xFF000000), | ||||
|         onBackground = Color(0xFFFFFFFF), | ||||
|         surface = Color(0xFF000000), | ||||
|         onSurface = Color(0xFFFFFFFF), | ||||
|         surfaceVariant = Color(0xFF000000), | ||||
|         onSurfaceVariant = Color(0xFFFFFFFF), | ||||
|         outline = Color(0xFFFFFFFF), | ||||
|         outlineVariant = Color(0xFFFFFFFF), | ||||
|         scrim = Color(0xFF000000), | ||||
|         inverseSurface = Color(0xFFFFFFFF), | ||||
|         inverseOnSurface = Color(0xFF000000), | ||||
|         inversePrimary = Color(0xFF000000), | ||||
|         surfaceDim = Color(0xFF000000), | ||||
|         surfaceBright = Color(0xFFFFFFFF), | ||||
|         surfaceContainerLowest = Color(0xFF000000), | ||||
|         surfaceContainerLow = Color(0xFF000000), | ||||
|         surfaceContainer = Color(0xFF000000), | ||||
|         surfaceContainerHigh = Color(0xFF000000), | ||||
|         surfaceContainerHighest = Color(0xFF000000), | ||||
|     ) | ||||
|  | ||||
|     override val lightScheme = lightColorScheme( | ||||
|         primary = Color(0xFF000000), | ||||
|         onPrimary = Color(0xFFFFFFFF), | ||||
|         primaryContainer = Color(0xFF000000), | ||||
|         onPrimaryContainer = Color(0xFFFFFFFF), | ||||
|         secondary = Color(0xFF000000), | ||||
|         onSecondary = Color(0xFFFFFFFF), | ||||
|         secondaryContainer = Color(0xFF888888), | ||||
|         onSecondaryContainer = Color(0xFFFFFFFF), | ||||
|         tertiary = Color(0xFF888888), | ||||
|         onTertiary = Color(0xFFFFFFFF), | ||||
|         tertiaryContainer = Color(0xFF000000), | ||||
|         onTertiaryContainer = Color(0xFFFFFFFF), | ||||
|         error = Color(0xFF000000), | ||||
|         onError = Color(0xFFFFFFFF), | ||||
|         errorContainer = Color(0xFF000000), | ||||
|         onErrorContainer = Color(0xFFFFFFFF), | ||||
|         background = Color(0xFFFFFFFF), | ||||
|         onBackground = Color(0xFF000000), | ||||
|         surface = Color(0xFFFFFFFF), | ||||
|         onSurface = Color(0xFF000000), | ||||
|         surfaceVariant = Color(0xFFFFFFFF), | ||||
|         onSurfaceVariant = Color(0xFF000000), | ||||
|         outline = Color(0xFF000000), | ||||
|         outlineVariant = Color(0xFF000000), | ||||
|         scrim = Color(0xFF000000), | ||||
|         inverseSurface = Color(0xFF000000), | ||||
|         inverseOnSurface = Color(0xFFFFFFFF), | ||||
|         inversePrimary = Color(0xFFFFFFFF), | ||||
|         surfaceDim = Color(0xFFFFFFFF), | ||||
|         surfaceBright = Color(0xFFFFFFFF), | ||||
|         surfaceContainerLowest = Color(0xFFFFFFFF), | ||||
|         surfaceContainerLow = Color(0xFFFFFFFF), | ||||
|         surfaceContainer = Color(0xFFFFFFFF), | ||||
|         surfaceContainerHigh = Color(0xFFFFFFFF), | ||||
|         surfaceContainerHighest = Color(0xFFFFFFFF), | ||||
|     ) | ||||
| } | ||||
| @@ -10,12 +10,10 @@ import androidx.compose.foundation.layout.Column | ||||
| import androidx.compose.foundation.layout.IntrinsicSize | ||||
| import androidx.compose.foundation.layout.Row | ||||
| import androidx.compose.foundation.layout.WindowInsets | ||||
| import androidx.compose.foundation.layout.absoluteOffset | ||||
| import androidx.compose.foundation.layout.fillMaxHeight | ||||
| 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.layout.systemBars | ||||
| import androidx.compose.foundation.layout.windowInsetsPadding | ||||
| import androidx.compose.foundation.layout.wrapContentSize | ||||
| @@ -24,9 +22,6 @@ import androidx.compose.foundation.shape.RoundedCornerShape | ||||
| import androidx.compose.foundation.verticalScroll | ||||
| import androidx.compose.material.icons.Icons | ||||
| import androidx.compose.material.icons.filled.MoreVert | ||||
| import androidx.compose.material.icons.filled.VisibilityOff | ||||
| import androidx.compose.material3.Badge | ||||
| import androidx.compose.material3.BadgedBox | ||||
| import androidx.compose.material3.DropdownMenuItem | ||||
| import androidx.compose.material3.HorizontalDivider | ||||
| import androidx.compose.material3.Icon | ||||
| @@ -75,7 +70,6 @@ fun TrackInfoDialogHome( | ||||
|     onOpenInBrowser: (TrackItem) -> Unit, | ||||
|     onRemoved: (TrackItem) -> Unit, | ||||
|     onCopyLink: (TrackItem) -> Unit, | ||||
|     onTogglePrivate: (TrackItem) -> Unit, | ||||
| ) { | ||||
|     Column( | ||||
|         modifier = Modifier | ||||
| @@ -90,7 +84,6 @@ fun TrackInfoDialogHome( | ||||
|             if (item.track != null) { | ||||
|                 val supportsScoring = item.tracker.getScoreList().isNotEmpty() | ||||
|                 val supportsReadingDates = item.tracker.supportsReadingDates | ||||
|                 val supportsPrivate = item.tracker.supportsPrivateTracking | ||||
|                 TrackInfoItem( | ||||
|                     title = item.track.title, | ||||
|                     tracker = item.tracker, | ||||
| @@ -122,9 +115,6 @@ fun TrackInfoDialogHome( | ||||
|                     onOpenInBrowser = { onOpenInBrowser(item) }, | ||||
|                     onRemoved = { onRemoved(item) }, | ||||
|                     onCopyLink = { onCopyLink(item) }, | ||||
|                     private = item.track.private, | ||||
|                     onTogglePrivate = { onTogglePrivate(item) } | ||||
|                         .takeIf { supportsPrivate }, | ||||
|                 ) | ||||
|             } else { | ||||
|                 TrackInfoItemEmpty( | ||||
| @@ -154,37 +144,17 @@ private fun TrackInfoItem( | ||||
|     onOpenInBrowser: () -> Unit, | ||||
|     onRemoved: () -> Unit, | ||||
|     onCopyLink: () -> Unit, | ||||
|     private: Boolean, | ||||
|     onTogglePrivate: (() -> Unit)?, | ||||
| ) { | ||||
|     val context = LocalContext.current | ||||
|     Column { | ||||
|         Row( | ||||
|             verticalAlignment = Alignment.CenterVertically, | ||||
|         ) { | ||||
|             BadgedBox( | ||||
|                 badge = { | ||||
|                     if (private) { | ||||
|                         Badge( | ||||
|                             containerColor = MaterialTheme.colorScheme.primary, | ||||
|                             contentColor = MaterialTheme.colorScheme.onPrimary, | ||||
|                             modifier = Modifier.absoluteOffset(x = (-5).dp), | ||||
|                         ) { | ||||
|                             Icon( | ||||
|                                 imageVector = Icons.Filled.VisibilityOff, | ||||
|                                 contentDescription = stringResource(MR.strings.tracked_privately), | ||||
|                                 modifier = Modifier.size(14.dp), | ||||
|                             ) | ||||
|                         } | ||||
|                     } | ||||
|                 }, | ||||
|             ) { | ||||
|                 TrackLogoIcon( | ||||
|                     tracker = tracker, | ||||
|                     onClick = onOpenInBrowser, | ||||
|                     onLongClick = onCopyLink, | ||||
|                 ) | ||||
|             } | ||||
|             TrackLogoIcon( | ||||
|                 tracker = tracker, | ||||
|                 onClick = onOpenInBrowser, | ||||
|                 onLongClick = onCopyLink, | ||||
|             ) | ||||
|             Box( | ||||
|                 modifier = Modifier | ||||
|                     .height(48.dp) | ||||
| @@ -211,8 +181,6 @@ private fun TrackInfoItem( | ||||
|                 onOpenInBrowser = onOpenInBrowser, | ||||
|                 onRemoved = onRemoved, | ||||
|                 onCopyLink = onCopyLink, | ||||
|                 private = private, | ||||
|                 onTogglePrivate = onTogglePrivate, | ||||
|             ) | ||||
|         } | ||||
|  | ||||
| @@ -323,8 +291,6 @@ private fun TrackInfoItemMenu( | ||||
|     onOpenInBrowser: () -> Unit, | ||||
|     onRemoved: () -> Unit, | ||||
|     onCopyLink: () -> Unit, | ||||
|     private: Boolean, | ||||
|     onTogglePrivate: (() -> Unit)?, | ||||
| ) { | ||||
|     var expanded by remember { mutableStateOf(false) } | ||||
|     Box(modifier = Modifier.wrapContentSize(Alignment.TopStart)) { | ||||
| @@ -352,25 +318,6 @@ private fun TrackInfoItemMenu( | ||||
|                     expanded = false | ||||
|                 }, | ||||
|             ) | ||||
|             if (onTogglePrivate != null) { | ||||
|                 DropdownMenuItem( | ||||
|                     text = { | ||||
|                         Text( | ||||
|                             stringResource( | ||||
|                                 if (private) { | ||||
|                                     MR.strings.action_toggle_private_off | ||||
|                                 } else { | ||||
|                                     MR.strings.action_toggle_private_on | ||||
|                                 }, | ||||
|                             ), | ||||
|                         ) | ||||
|                     }, | ||||
|                     onClick = { | ||||
|                         onTogglePrivate() | ||||
|                         expanded = false | ||||
|                     }, | ||||
|                 ) | ||||
|             } | ||||
|             DropdownMenuItem( | ||||
|                 text = { Text(stringResource(MR.strings.action_remove)) }, | ||||
|                 onClick = { | ||||
|   | ||||
| @@ -25,9 +25,7 @@ internal class TrackInfoDialogHomePreviewProvider : | ||||
|         remoteUrl = "https://example.com", | ||||
|         startDate = 0L, | ||||
|         finishDate = 0L, | ||||
|         private = false, | ||||
|     ) | ||||
|     private val privateTrack = aTrack.copy(private = true) | ||||
|     private val trackItemWithoutTrack = TrackItem( | ||||
|         track = null, | ||||
|         tracker = DummyTracker( | ||||
| @@ -42,13 +40,6 @@ internal class TrackInfoDialogHomePreviewProvider : | ||||
|             name = "Example Tracker 2", | ||||
|         ), | ||||
|     ) | ||||
|     private val trackItemWithPrivateTrack = TrackItem( | ||||
|         track = privateTrack, | ||||
|         tracker = DummyTracker( | ||||
|             id = 2L, | ||||
|             name = "Example Tracker 2", | ||||
|         ), | ||||
|     ) | ||||
|  | ||||
|     private val trackersWithAndWithoutTrack = @Composable { | ||||
|         TrackInfoDialogHome( | ||||
| @@ -66,7 +57,6 @@ internal class TrackInfoDialogHomePreviewProvider : | ||||
|             onOpenInBrowser = {}, | ||||
|             onRemoved = {}, | ||||
|             onCopyLink = {}, | ||||
|             onTogglePrivate = {}, | ||||
|         ) | ||||
|     } | ||||
|  | ||||
| @@ -83,24 +73,6 @@ internal class TrackInfoDialogHomePreviewProvider : | ||||
|             onOpenInBrowser = {}, | ||||
|             onRemoved = {}, | ||||
|             onCopyLink = {}, | ||||
|             onTogglePrivate = {}, | ||||
|         ) | ||||
|     } | ||||
|  | ||||
|     private val trackerWithPrivateTracking = @Composable { | ||||
|         TrackInfoDialogHome( | ||||
|             trackItems = listOf(trackItemWithPrivateTrack), | ||||
|             dateFormat = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM), | ||||
|             onStatusClick = {}, | ||||
|             onChapterClick = {}, | ||||
|             onScoreClick = {}, | ||||
|             onStartDateEdit = {}, | ||||
|             onEndDateEdit = {}, | ||||
|             onNewSearch = {}, | ||||
|             onOpenInBrowser = {}, | ||||
|             onRemoved = {}, | ||||
|             onCopyLink = {}, | ||||
|             onTogglePrivate = {}, | ||||
|         ) | ||||
|     } | ||||
|  | ||||
| @@ -108,6 +80,5 @@ internal class TrackInfoDialogHomePreviewProvider : | ||||
|         get() = sequenceOf( | ||||
|             trackersWithAndWithoutTrack, | ||||
|             noTrackers, | ||||
|             trackerWithPrivateTracking, | ||||
|         ) | ||||
| } | ||||
|   | ||||
| @@ -33,7 +33,6 @@ import androidx.compose.material.icons.Icons | ||||
| import androidx.compose.material.icons.automirrored.outlined.ArrowBack | ||||
| import androidx.compose.material.icons.filled.CheckCircle | ||||
| import androidx.compose.material.icons.filled.Close | ||||
| import androidx.compose.material.icons.filled.VisibilityOff | ||||
| import androidx.compose.material3.Button | ||||
| import androidx.compose.material3.ButtonDefaults | ||||
| import androidx.compose.material3.DropdownMenuItem | ||||
| @@ -91,9 +90,8 @@ fun TrackerSearch( | ||||
|     queryResult: Result<List<TrackSearch>>?, | ||||
|     selected: TrackSearch?, | ||||
|     onSelectedChange: (TrackSearch) -> Unit, | ||||
|     onConfirmSelection: (private: Boolean) -> Unit, | ||||
|     onConfirmSelection: () -> Unit, | ||||
|     onDismissRequest: () -> Unit, | ||||
|     supportsPrivateTracking: Boolean, | ||||
| ) { | ||||
|     val focusManager = LocalFocusManager.current | ||||
|     val focusRequester = remember { FocusRequester() } | ||||
| @@ -166,31 +164,15 @@ fun TrackerSearch( | ||||
|                 enter = fadeIn() + slideInVertically { it / 2 }, | ||||
|                 exit = slideOutVertically { it / 2 } + fadeOut(), | ||||
|             ) { | ||||
|                 Row( | ||||
|                 Button( | ||||
|                     onClick = { onConfirmSelection() }, | ||||
|                     modifier = Modifier | ||||
|                         .padding(MaterialTheme.padding.small) | ||||
|                         .padding(12.dp) | ||||
|                         .windowInsetsPadding(WindowInsets.navigationBars) | ||||
|                         .fillMaxWidth(), | ||||
|                     horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small), | ||||
|                     elevation = ButtonDefaults.elevatedButtonElevation(), | ||||
|                 ) { | ||||
|                     Button( | ||||
|                         onClick = { onConfirmSelection(false) }, | ||||
|                         modifier = Modifier.weight(1f), | ||||
|                         elevation = ButtonDefaults.elevatedButtonElevation(), | ||||
|                     ) { | ||||
|                         Text(text = stringResource(MR.strings.action_track)) | ||||
|                     } | ||||
|                     if (supportsPrivateTracking) { | ||||
|                         Button( | ||||
|                             onClick = { onConfirmSelection(true) }, | ||||
|                             elevation = ButtonDefaults.elevatedButtonElevation(), | ||||
|                         ) { | ||||
|                             Icon( | ||||
|                                 imageVector = Icons.Filled.VisibilityOff, | ||||
|                                 contentDescription = stringResource(MR.strings.action_toggle_private_on), | ||||
|                             ) | ||||
|                         } | ||||
|                     } | ||||
|                     Text(text = stringResource(MR.strings.action_track)) | ||||
|                 } | ||||
|             } | ||||
|         }, | ||||
| @@ -304,15 +286,6 @@ private fun SearchResultItem( | ||||
|                             } | ||||
|                         }, | ||||
|                     ) | ||||
|                     if (trackSearch.authors.isNotEmpty() || trackSearch.artists.isNotEmpty()) { | ||||
|                         Text( | ||||
|                             text = (trackSearch.authors + trackSearch.artists).distinct().joinToString(), | ||||
|                             modifier = Modifier.secondaryItemAlpha(), | ||||
|                             maxLines = 1, | ||||
|                             overflow = TextOverflow.Ellipsis, | ||||
|                             style = MaterialTheme.typography.bodySmall, | ||||
|                         ) | ||||
|                     } | ||||
|                     if (type.isNotBlank()) { | ||||
|                         SearchResultItemDetails( | ||||
|                             title = stringResource(MR.strings.track_type), | ||||
|   | ||||
| @@ -5,11 +5,8 @@ import androidx.compose.runtime.Composable | ||||
| import androidx.compose.ui.tooling.preview.PreviewParameterProvider | ||||
| import androidx.compose.ui.tooling.preview.datasource.LoremIpsum | ||||
| import eu.kanade.tachiyomi.data.track.model.TrackSearch | ||||
| import java.text.SimpleDateFormat | ||||
| import java.time.Instant | ||||
| import java.time.temporal.ChronoUnit | ||||
| import java.util.Date | ||||
| import java.util.Locale | ||||
| import kotlin.random.Random | ||||
|  | ||||
| internal class TrackerSearchPreviewProvider : PreviewParameterProvider<@Composable () -> Unit> { | ||||
| @@ -23,7 +20,6 @@ internal class TrackerSearchPreviewProvider : PreviewParameterProvider<@Composab | ||||
|             onSelectedChange = {}, | ||||
|             onConfirmSelection = {}, | ||||
|             onDismissRequest = {}, | ||||
|             supportsPrivateTracking = false, | ||||
|         ) | ||||
|     } | ||||
|     private val fullPageWithoutSelected = @Composable { | ||||
| @@ -35,7 +31,6 @@ internal class TrackerSearchPreviewProvider : PreviewParameterProvider<@Composab | ||||
|             onSelectedChange = {}, | ||||
|             onConfirmSelection = {}, | ||||
|             onDismissRequest = {}, | ||||
|             supportsPrivateTracking = false, | ||||
|         ) | ||||
|     } | ||||
|     private val loading = @Composable { | ||||
| @@ -47,27 +42,12 @@ internal class TrackerSearchPreviewProvider : PreviewParameterProvider<@Composab | ||||
|             onSelectedChange = {}, | ||||
|             onConfirmSelection = {}, | ||||
|             onDismissRequest = {}, | ||||
|             supportsPrivateTracking = false, | ||||
|         ) | ||||
|     } | ||||
|     private val fullPageWithPrivateTracking = @Composable { | ||||
|         val items = someTrackSearches().take(30).toList() | ||||
|         TrackerSearch( | ||||
|             state = TextFieldState(initialText = "search text"), | ||||
|             onDispatchQuery = {}, | ||||
|             queryResult = Result.success(items), | ||||
|             selected = items[1], | ||||
|             onSelectedChange = {}, | ||||
|             onConfirmSelection = {}, | ||||
|             onDismissRequest = {}, | ||||
|             supportsPrivateTracking = true, | ||||
|         ) | ||||
|     } | ||||
|     override val values: Sequence<@Composable () -> Unit> = sequenceOf( | ||||
|         fullPageWithSecondSelected, | ||||
|         fullPageWithoutSelected, | ||||
|         loading, | ||||
|         fullPageWithPrivateTracking, | ||||
|     ) | ||||
|  | ||||
|     private fun someTrackSearches(): Sequence<TrackSearch> = sequence { | ||||
| @@ -76,8 +56,6 @@ internal class TrackerSearchPreviewProvider : PreviewParameterProvider<@Composab | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private val formatter: SimpleDateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()) | ||||
|  | ||||
|     private fun randTrackSearch() = TrackSearch().let { | ||||
|         it.id = Random.nextLong() | ||||
|         it.manga_id = Random.nextLong() | ||||
| @@ -93,17 +71,11 @@ internal class TrackerSearchPreviewProvider : PreviewParameterProvider<@Composab | ||||
|         it.finished_reading_date = 0L | ||||
|         it.tracking_url = "https://example.com/tracker-example" | ||||
|         it.cover_url = "https://example.com/cover.png" | ||||
|         it.start_date = formatter.format(Date.from(Instant.now().minus((1L..365).random(), ChronoUnit.DAYS))) | ||||
|         it.start_date = Instant.now().minus((1L..365).random(), ChronoUnit.DAYS).toString() | ||||
|         it.summary = lorem((0..40).random()).joinToString() | ||||
|         it.publishing_status = if (Random.nextBoolean()) "Finished" else "" | ||||
|         it.publishing_type = if (Random.nextBoolean()) "Oneshot" else "" | ||||
|         it.artists = randomNames() | ||||
|         it.authors = randomNames() | ||||
|         it | ||||
|     } | ||||
|  | ||||
|     private fun randomNames(): List<String> = (0..(0..3).random()).map { lorem((3..5).random()).joinToString() } | ||||
|  | ||||
|     private fun lorem(words: Int): Sequence<String> = | ||||
|         LoremIpsum(words).values | ||||
| } | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user