Compare commits

..

7 Commits

Author SHA1 Message Date
Jack Hamilton
ab0893b2d4 Changes according to feedback 2024-10-11 21:29:02 -05:00
Jack Hamilton
078758391e Update app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryScreenModel.kt
Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
2024-10-11 21:07:17 -05:00
Jack Hamilton
2eb1580788 Update app/src/main/java/eu/kanade/presentation/library/LibrarySettingsDialog.kt
Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
2024-10-11 21:07:08 -05:00
Jack Hamilton
d328ded17f nrevert collections import 2024-10-11 20:03:52 -05:00
Jack Hamilton
80f9dfb699 Spotless 2024-10-11 19:50:23 -05:00
Jack Hamilton
3d087f4428 Keyed random seed 2024-10-11 19:50:23 -05:00
Jack Hamilton
0ab795bfa3 Add random sort option 2024-10-11 19:49:11 -05:00
496 changed files with 6302 additions and 18989 deletions

View File

@@ -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,aidl}]
indent_size = 4
# noinspection EditorConfigKeyCorrectness
[*.{kt,kts}] [*.{kt,kts}]
indent_size = 4
max_line_length = 120 max_line_length = 120
indent_size = 4
insert_final_newline = true
ij_kotlin_allow_trailing_comma = true ij_kotlin_allow_trailing_comma = true
ij_kotlin_allow_trailing_comma_on_call_site = 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 = 2147483647
ij_kotlin_name_count_to_use_star_import_for_members = 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

View File

@@ -1,8 +1,5 @@
blank_issues_enabled: false blank_issues_enabled: false
contact_links: contact_links:
- name: ❌ Help with Extensions
url: https://mihon.app/docs/faq/browse/extensions
about: For extension-related questions/issues
- name: 🖥️ Mihon website - name: 🖥️ Mihon website
url: https://mihon.app/ url: https://mihon.app/
about: Guides, troubleshooting, and answers to common questions about: Guides, troubleshooting, and answers to common questions

View File

@@ -1,7 +1,8 @@
name: 🐞 Issue report name: 🐞 Issue report
description: Report an issue in Mihon description: Report an issue in Mihon
labels: [bug] labels: [Bug]
body: body:
- type: textarea - type: textarea
id: reproduce-steps id: reproduce-steps
attributes: attributes:
@@ -42,9 +43,9 @@ body:
attributes: attributes:
label: Crash logs label: Crash logs
description: | 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: | 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 - type: input
id: mihon-version id: mihon-version
@@ -52,7 +53,7 @@ body:
label: Mihon version label: Mihon version
description: You can find your Mihon version in **More → About**. description: You can find your Mihon version in **More → About**.
placeholder: | placeholder: |
Example: "0.19.2" Example: "0.16.5"
validations: validations:
required: true required: true
@@ -95,9 +96,9 @@ body:
required: true required: true
- label: I have gone through the [FAQ](https://mihon.app/docs/faq/general) and [troubleshooting guide](https://mihon.app/docs/guides/troubleshooting/). - label: I have gone through the [FAQ](https://mihon.app/docs/faq/general) and [troubleshooting guide](https://mihon.app/docs/guides/troubleshooting/).
required: true required: true
- label: I have updated the app to version **[0.19.2](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 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 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 required: true

View File

@@ -1,7 +1,8 @@
name: ⭐ Feature request name: ⭐ Feature request
description: Suggest a feature to improve Mihon description: Suggest a feature to improve Mihon
labels: [feature request] labels: [Feature request]
body: body:
- type: textarea - type: textarea
id: feature-description id: feature-description
attributes: attributes:
@@ -30,7 +31,7 @@ body:
required: true required: true
- label: I have written a short but informative title. - label: I have written a short but informative title.
required: true required: true
- label: I have updated the app to version **[0.19.2](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 required: true
- label: I will fill out all of the requested information in this form. - label: I will fill out all of the requested information in this form.
required: true required: true

View File

@@ -1,13 +1,6 @@
{ {
"$schema": "https://docs.renovatebot.com/renovate-schema.json", "$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:recommended"], "extends": ["config:base"],
"labels": ["Dependencies"], "labels": ["Dependencies"],
"semanticCommits": "disabled", "semanticCommits": "disabled"
"packageRules": [
{
"groupName": "GitHub Actions",
"matchManagers": ["github-actions"],
"pinDigests": true,
},
],
} }

View File

@@ -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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Dependency Review
if: github.event_name == 'pull_request'
uses: actions/dependency-review-action@40c09b7dc99638e5ddb0bfd91c1673effc064d8a # v4.8.1
- name: Set up JDK
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
with:
java-version: 17
distribution: temurin
- name: Set up Gradle
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
- 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@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
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@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: mapping-${{ github.sha }}
path: app/build/outputs/mapping/release

View 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
View 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
View 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'

View File

@@ -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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up JDK
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
with:
java-version: 17
distribution: temurin
- name: Set up Gradle
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
- 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@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up JDK
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
with:
java-version: 17
distribution: temurin
- name: Set up Gradle
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
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@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
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@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.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@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # v2.4.1
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 youre 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 }}

View File

@@ -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
View File

@@ -1,16 +1,18 @@
# Build files
.gradle .gradle
.kotlin .kotlin
build /local.properties
/.idea/workspace.xml
# IDE files
*.iml
.idea/*
!.idea/icon.svg
/captures
# Configuration files
local.properties
# macOS specific files
.DS_Store .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

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

6
.idea/icon.svg generated
View File

@@ -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

View File

@@ -2,447 +2,133 @@
All notable changes to this project will be documented in this file. 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). The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
- `Added` - for new features. and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- `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.
## [Unreleased] ## [Unreleased]
## [v0.19.2] - 2025-11-02
### Added
- Advanced setting to limit download filenames to ASCII characters. This is provided only as a workaround for OSes that do not properly handle standard Unicode filenames. This setting is generally not recommended and should only be used as a last resort ([@raxod502](https://github.com/radian-software)) ([#2305](https://github.com/mihonapp/mihon/pull/2305))
- Option to customize the number of concurrent source and page downloads ([@AntsyLich](https://github.com/AntsyLich)) ([#2637](https://github.com/mihonapp/mihon/pull/2637))
### Changed
- Increased default concurrent page downloads to 5 ([@AntsyLich](https://github.com/AntsyLich)) ([#2637](https://github.com/mihonapp/mihon/pull/2637))
### Improved
- Spoofing of `X-Requested-With` header to support newer WebView versions ([@Guzmazow](https://github.com/Guzmazow)) ([#2491](https://github.com/mihonapp/mihon/pull/2491))
- Download support for chapters with the same metadata. Now a hash based on chapter's url is appended to download filename to tell them apart, letting you download both. Existing downloaded chapters will continue to work normally ([@raxod502](https://github.com/radian-software)) ([#2305](https://github.com/mihonapp/mihon/pull/2305))
- Auto refresh extension list whenever a repository is added or removed ([@c2y5](https://github.com/c2y5)) ([#2483](https://github.com/mihonapp/mihon/pull/2483))
- Added proper multi window support in WebView instead of treating everything as a redirect ([@TheUnlocked](https://github.com/TheUnlocked)) ([#2584](https://github.com/mihonapp/mihon/pull/2584))
### Fixed
- Fix height of description not being calculated correctly if images are present ([@Secozzi](https://github.com/Secozzi)) ([#2382](https://github.com/mihonapp/mihon/pull/2382))
- Fix migration progress not updating after manual search ([@Secozzi](https://github.com/Secozzi)) ([#2484](https://github.com/mihonapp/mihon/pull/2484))
- Fix category migration flag being ignored due to incorrect check against chapter flag ([@Secozzi](https://github.com/Secozzi)) ([#2484](https://github.com/mihonapp/mihon/pull/2484))
- Fix disabling incognito mode from notification ([@NGB-Was-Taken](https://github.com/NGB-Was-Taken)) ([#2512](https://github.com/mihonapp/mihon/pull/2512))
- Fix mass migration advanced search query building ([@AntsyLich](https://github.com/AntsyLich)) ([#2629](https://github.com/mihonapp/mihon/pull/2629))
- Fix migration dialog migrating to wrong entry ([@AntsyLich](https://github.com/AntsyLich)) ([#2631](https://github.com/mihonapp/mihon/pull/2631))
- Fix migration "Attempt to invoke virtual method" crash ([@AntsyLich](https://github.com/AntsyLich)) ([#2632](https://github.com/mihonapp/mihon/pull/2632))
- Fix reader "Unable to edit key" error ([@AntsyLich](https://github.com/AntsyLich)) ([#2634](https://github.com/mihonapp/mihon/pull/2634))
- Fix extension download stuck in pending state in some cases ([@c2y5](https://github.com/c2y5)) ([#2483](https://github.com/mihonapp/mihon/pull/2483))
- Fix scrollbar not showing when animator duration scale animation is turned off ([@anirudhsnayak](https://github.com/anirudhsnayak)) ([#2398](https://github.com/mihonapp/mihon/pull/2398))
- Fix date picker not allowing the same start and finish date in negative time zones ([@AntsyLich](https://github.com/AntsyLich), [@kashish-aggarwal21](https://github.com/kashish-aggarwal21)) ([#2617](https://github.com/mihonapp/mihon/pull/2617))
- Fix reader tap zones triggering after scrolling was stopped by the user ([@Naputt1](https://github.com/Naputt1), [@AntsyLich](https://github.com/AntsyLich)) ([#2518](https://github.com/mihonapp/mihon/pull/2518))
- Fix reader page indicator being partially visible on some devices ([@AntsyLich](https://github.com/AntsyLich)) ([#1908](https://github.com/mihonapp/mihon/pull/1908))
- Fix inconsistent system bar and reader app bar background ([@AntsyLich](https://github.com/AntsyLich)) ([#1908](https://github.com/mihonapp/mihon/pull/1908))
- Fix transparent system bar background in reader on Android 15+ ([@AntsyLich](https://github.com/AntsyLich)) ([#1908](https://github.com/mihonapp/mihon/pull/1908))
### Other
- Delegate Suwayomi tracker authentication to extension ([@cpiber](https://github.com/cpiber)) ([#2476](https://github.com/mihonapp/mihon/pull/2476))
- Fix Kitsu tracker to conform to tracker data structure properly ([@cpiber](https://github.com/cpiber)) ([#2609](https://github.com/mihonapp/mihon/pull/2609))
- Update Suwayomi tracker to use GraphQL API instead of REST API ([@cpiber](https://github.com/cpiber)) ([#2585](https://github.com/mihonapp/mihon/pull/2585))
## [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))
### Fixed
- 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))
### Fixed
- 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 ### Added
- Option to disable reader zoom out ([@Splintorien](https://github.com/Splintorien)) ([#302](https://github.com/mihonapp/mihon/pull/302)) - 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)) - 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)) - 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)) - 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)) - 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)) - 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)) - 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)) - 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)) - 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)) - 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)) - 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)) - 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)) - 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 ### 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)) - 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))
- Fix dual page split ([@FooIbar](https://github.com/FooIbar)) ([#485](https://github.com/mihonapp/mihon/pull/485)) - 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))
- Bump default user agent ([@AntsyLich](https://github.com/AntsyLich)) ([`8160b47`](https://github.com/mihonapp/mihon/commit/8160b47ff5fbbd9b32caeb462b5be881fabd3449)) - 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)) - 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)) - Duplicate entry dialog UI ([@sirlag](https://github.com/sirlag)) ([#492](https://github.com/mihonapp/mihon/pull/492))
- Extension trust system - 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))
- 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))
- Make category backup/restore not dependant on library backup ([@AntsyLich](https://github.com/AntsyLich)) ([`56fb4f6`](https://github.com/mihonapp/mihon/commit/56fb4f62a152e87a71892aa68c78cac51a2c8596)) - 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)) - 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)) - 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 ### Improvement
- Reader performance - Long strip reader performance ([@FooIbar](https://github.com/FooIbar), [@wwww-wwww](https://github.com/wwww-wwww)) ([#687](https://github.com/mihonapp/mihon/pull/687))
- 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))
- Performance when looking up specific files ([@raxod502](https://github.com/raxod502)) ([#728](https://github.com/mihonapp/mihon/pull/728)) - 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)) - 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)) - 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 ### 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)) - 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)) - 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)) - Occasional black bar when scrolling in long strip reader ([@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))
- 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 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)) - 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)) - `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)) - 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)) - 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)) - 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)) - 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)) - 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)) - 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)) - 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)) - 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)) - 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)) - 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)) - 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)) - 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 ## [v0.16.5] - 2024-04-09
### Added ### Added
- Relative date for up to a week in the future ([@sirlag](https://github.com/sirlag)) ([#415](https://github.com/mihonapp/mihon/pull/415)) - 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))
- Advance setting to install custom color profiles ([@wwww-wwww](https://github.com/wwww-wwww)) ([#523](https://github.com/mihonapp/mihon/pull/523))
### Changed ### Changed
- Permanently enable 32-bit color mode ([@wwww-wwww](https://github.com/wwww-wwww)) ([#523](https://github.com/mihonapp/mihon/pull/523)) - Permanently enable 32-bit color mode ([@wwww-wwww](https://github.com/wwww-wwww)) ([#523](https://github.com/mihonapp/mihon/pull/523))
### Fixed ### 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 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 extra date header introduced by parent PR ([@sirlag](https://github.com/sirlag)) ([#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 build time in about screen displayed in UTC ([@AntsyLich](https://github.com/AntsyLich)) ([`aed53d3`](https://github.com/mihonapp/mihon/commit/aed53d3bdc85ce0e899fbb90b9f9cad0f1b86480)) - Fix crash on Pixel devices ([`ab06720`](https://github.com/mihonapp/mihon/commit/ab067209661eceefc04c65f6bdbfcaa8a1264651))
- 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 when opening some heif/heic images ([@az4521](https://github.com/az4521)) ([#466](https://github.com/mihonapp/mihon/pull/466))
- Crash on Pixel devices (was introduced due to compose update) ([@AntsyLich](https://github.com/AntsyLich)) ([`ab06720`](https://github.com/mihonapp/mihon/commit/ab067209661eceefc04c65f6bdbfcaa8a1264651)) - Fix crash in track date selection dialog ([@ivaniskandar](https://github.com/ivaniskandar)) ([`c348fac`](https://github.com/mihonapp/mihon/commit/c348fac78fac479fb123bd617c01c78b9ca851d5))
- Crash when opening some heif/heic images ([@az4521](https://github.com/az4521)) ([#466](https://github.com/mihonapp/mihon/pull/466)) - Fix dates for saved images on Samsung devices ([@MajorTanya](https://github.com/MajorTanya)) ([#552](https://github.com/mihonapp/mihon/pull/552))
- 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)) - Fix colors getting distorted when opening CMYK jpeg images ([@wwww-wwww](https://github.com/wwww-wwww)) ([#523](https://github.com/mihonapp/mihon/pull/523))
- 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))
## [v0.16.4] - 2024-02-27 ## [v0.16.4] - 2024-02-26
### Changed ### Fixed
- Don't include custom user agent for MAL (circumvents MAL block) ([@AntsyLich](https://github.com/AntsyLich)) ([`085ad8d`](https://github.com/mihonapp/mihon/commit/085ad8d44637c375a8ed24aba3a6f75f5b0cc9ee)) - Circumvent MAL block ([@AntsyLich](https://github.com/AntsyLich)) ([`085ad8d`](https://github.com/mihonapp/mihon/commit/085ad8d44637c375a8ed24aba3a6f75f5b0cc9ee))
## [v0.16.3] - 2024-01-30 ## [v0.16.3] - 2024-01-30
### Added ### 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)) - 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 ### 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)) - Rename extension update error file to `mihon_update_errors.txt` ([@mjishnu](https://github.com/mjishnu)) ([#253](https://github.com/mihonapp/mihon/pull/253))
- 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)) - 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 ### Fixed
- Bottom sheet UI issues on non-tablet devices ([@theolm](https://github.com/theolm)) ([#182](https://github.com/mihonapp/mihon/pull/182)) - Fix bottom sheet display issues on non-Tablet UI ([@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)) - Fix 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)) - Fix 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)) - Fix error handling when refreshing MAL OAuth token ([@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))
## [v0.16.2] - 2024-01-28 ## [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 ### Changed
- Backup now contains scanlator filter of a series ([@jobobby04](https://github.com/jobobby04)) ([#166](https://github.com/mihonapp/mihon/pull/166)) - Rename crash log filename to `mihon_crash_logs.txt` ([@MajorTanya](https://github.com/MajorTanya)) ([#234](https://github.com/mihonapp/mihon/pull/234))
- 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))
### Fixed ### Fixed
- "Flash screen on page change" making the screen full black ([@AntsyLich](https://github.com/AntsyLich)) ([`38d6ab8`](https://github.com/mihonapp/mihon/commit/38d6ab80ce868707829dbc81de4170afe3c2f2a5)) - "Flash screen on page change" Making the screen goes blank ([@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)) - 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)) - 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)) - Inconsistent button height with some languages in "Data and storage" ([@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)) - 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))
### 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))
## [v0.16.1] - 2024-01-18 ## [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 ### 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)) - 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 ## [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.2...main "The end of 立ち読み (Tachiyomi) is the beginning of みほん (Mihon)"
[v0.19.2]: https://github.com/mihonapp/mihon/compare/v0.19.1...v0.19.2 Credit to LinkCable, the icon designer, for this poetic quote.
[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 What's New?
[v0.18.0]: https://github.com/mihonapp/mihon/compare/v0.17.1...v0.18.0 Well, nothing, except you now you need Android 8+ to install the app.
[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 [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.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.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.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.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.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

View File

@@ -10,16 +10,16 @@
Discover and read manga, webtoons, comics, and more easier than ever on your Android device. Discover and read manga, webtoons, comics, and more easier than ever on your Android device.
[![Discord server](https://img.shields.io/discord/1195734228319617024.svg?label=&labelColor=6A7EC2&color=7389D8&logo=discord&logoColor=FFFFFF)](https://discord.gg/mihon) [![Discord server](https://img.shields.io/discord/1195734228319617024.svg?label=&labelColor=6A7EC2&color=7389D8&logo=discord&logoColor=FFFFFF)](https://discord.gg/mihon)
[![GitHub downloads](https://img.shields.io/github/downloads/mihonapp/mihon/total?label=downloads&labelColor=27303D&color=0D1117&logo=github&logoColor=FFFFFF&style=flat)](https://mihon.app/download) [![GitHub downloads](https://img.shields.io/github/downloads/mihonapp/mihon/total?label=downloads&labelColor=27303D&color=0D1117&logo=github&logoColor=FFFFFF&style=flat)](https://github.com/mihonapp/mihon/releases)
[![CI](https://img.shields.io/github/actions/workflow/status/mihonapp/mihon/build.yml?labelColor=27303D)](https://github.com/mihonapp/mihon/actions/workflows/build_push.yml) [![CI](https://img.shields.io/github/actions/workflow/status/mihonapp/mihon/build_push.yml?labelColor=27303D)](https://github.com/mihonapp/mihon/actions/workflows/build_push.yml)
[![License: Apache-2.0](https://img.shields.io/github/license/mihonapp/mihon?labelColor=27303D&color=0877d2)](/LICENSE) [![License: Apache-2.0](https://img.shields.io/github/license/mihonapp/mihon?labelColor=27303D&color=0877d2)](/LICENSE)
[![Translation status](https://img.shields.io/weblate/progress/mihon?labelColor=27303D&color=946300)](https://hosted.weblate.org/engage/mihon/) [![Translation status](https://img.shields.io/weblate/progress/mihon?labelColor=27303D&color=946300)](https://hosted.weblate.org/engage/mihon/)
## Download ## Download
[![Mihon Stable](https://img.shields.io/github/release/mihonapp/mihon.svg?maxAge=3600&label=Stable&labelColor=06599d&color=043b69)](https://mihon.app/download) [![Mihon Stable](https://img.shields.io/github/release/mihonapp/mihon.svg?maxAge=3600&label=Stable&labelColor=06599d&color=043b69)](https://github.com/mihonapp/mihon/releases)
[![Mihon Beta](https://img.shields.io/github/v/release/mihonapp/mihon-preview.svg?maxAge=3600&label=Beta&labelColor=2c2c47&color=1c1c39)](https://mihon.app/download) [![Mihon Beta](https://img.shields.io/github/v/release/mihonapp/mihon-preview.svg?maxAge=3600&label=Beta&labelColor=2c2c47&color=1c1c39)](https://github.com/mihonapp/mihon-preview/releases)
*Requires Android 8.0 or higher.* *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> <pre>
Copyright © 2015 Javier Tomás 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"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.

3
app/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
/build
*iml
*.iml

View File

@@ -1,7 +1,7 @@
import mihon.buildlogic.Config
import mihon.buildlogic.getBuildTime import mihon.buildlogic.getBuildTime
import mihon.buildlogic.getCommitCount import mihon.buildlogic.getCommitCount
import mihon.buildlogic.getGitSha import mihon.buildlogic.getGitSha
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins { plugins {
id("mihon.android.application") id("mihon.android.application")
@@ -11,7 +11,7 @@ plugins {
alias(libs.plugins.aboutLibraries) alias(libs.plugins.aboutLibraries)
} }
if (Config.includeTelemetry) { if (gradle.startParameter.taskRequests.toString().contains("Standard")) {
pluginManager.apply { pluginManager.apply {
apply(libs.plugins.google.services.get().pluginId) apply(libs.plugins.google.services.get().pluginId)
apply(libs.plugins.firebase.crashlytics.get().pluginId) apply(libs.plugins.firebase.crashlytics.get().pluginId)
@@ -20,71 +20,69 @@ if (Config.includeTelemetry) {
shortcutHelper.setFilePath("./shortcuts.xml") shortcutHelper.setFilePath("./shortcuts.xml")
val supportedAbis = setOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
android { android {
namespace = "eu.kanade.tachiyomi" namespace = "eu.kanade.tachiyomi"
defaultConfig { defaultConfig {
applicationId = "app.mihon" applicationId = "app.mihon"
versionCode = 15 versionCode = 7
versionName = "0.19.2" versionName = "0.16.5"
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"") buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"") buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
buildConfigField("String", "BUILD_TIME", "\"${getBuildTime(useLastCommitTime = false)}\"") buildConfigField("String", "BUILD_TIME", "\"${getBuildTime()}\"")
buildConfigField("boolean", "TELEMETRY_INCLUDED", "${Config.includeTelemetry}") buildConfigField("boolean", "INCLUDE_UPDATER", "false")
buildConfigField("boolean", "UPDATER_ENABLED", "${Config.enableUpdater}") buildConfigField("boolean", "PREVIEW", "false")
ndk {
abiFilters += supportedAbis
}
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
} }
splits {
abi {
isEnable = true
reset()
include(*supportedAbis.toTypedArray())
isUniversalApk = true
}
}
buildTypes { buildTypes {
val debug by getting { named("debug") {
applicationIdSuffix = ".dev"
versionNameSuffix = "-${getCommitCount()}" versionNameSuffix = "-${getCommitCount()}"
applicationIdSuffix = ".debug"
isPseudoLocalesEnabled = true isPseudoLocalesEnabled = true
} }
val release by getting { named("release") {
isMinifyEnabled = Config.enableCodeShrink isShrinkResources = true
isShrinkResources = Config.enableCodeShrink isMinifyEnabled = true
proguardFiles("proguard-android-optimize.txt", "proguard-rules.pro") 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") { create("preview") {
initWith(release) initWith(getByName("release"))
buildConfigField("boolean", "PREVIEW", "true")
applicationIdSuffix = ".debug" signingConfig = signingConfigs.getByName("debug")
matchingFallbacks.add("release")
versionNameSuffix = debug.versionNameSuffix val debugType = getByName("debug")
signingConfig = debug.signingConfig versionNameSuffix = debugType.versionNameSuffix
applicationIdSuffix = debugType.applicationIdSuffix
matchingFallbacks.addAll(commonMatchingFallbacks)
buildConfigField("String", "BUILD_TIME", "\"${getBuildTime(useLastCommitTime = false)}\"")
} }
create("benchmark") { create("benchmark") {
initWith(release) initWith(getByName("release"))
signingConfig = signingConfigs.getByName("debug")
matchingFallbacks.add("release")
isDebuggable = false isDebuggable = false
isProfileable = true isProfileable = true
versionNameSuffix = "-benchmark" versionNameSuffix = "-benchmark"
applicationIdSuffix = ".benchmark" applicationIdSuffix = ".benchmark"
signingConfig = debug.signingConfig
matchingFallbacks.addAll(commonMatchingFallbacks)
} }
} }
@@ -93,54 +91,47 @@ android {
getByName("benchmark").res.srcDirs("src/debug/res") getByName("benchmark").res.srcDirs("src/debug/res")
} }
splits { flavorDimensions.add("default")
abi {
isEnable = true productFlavors {
isUniversalApk = true create("standard") {
reset() buildConfigField("boolean", "INCLUDE_UPDATER", "true")
include("armeabi-v7a", "arm64-v8a", "x86", "x86_64") 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 { packaging {
jniLibs { resources.excludes.addAll(
keepDebugSymbols += listOf( listOf(
"libandroidx.graphics.path",
"libarchive-jni",
"libconscrypt_jni",
"libimagedecoder",
"libquickjs",
"libsqlite3x",
)
.map { "**/$it.so" }
}
resources {
excludes += setOf(
"kotlin-tooling-metadata.json", "kotlin-tooling-metadata.json",
"META-INF/DEPENDENCIES",
"LICENSE.txt", "LICENSE.txt",
"META-INF/**/*.properties", "META-INF/LICENSE",
"META-INF/**/LICENSE.txt", "META-INF/**/LICENSE.txt",
"META-INF/*.properties", "META-INF/*.properties",
"META-INF/*.version", "META-INF/**/*.properties",
"META-INF/DEPENDENCIES",
"META-INF/LICENSE",
"META-INF/NOTICE",
"META-INF/README.md", "META-INF/README.md",
) "META-INF/NOTICE",
} "META-INF/*.version",
),
)
} }
dependenciesInfo { dependenciesInfo {
includeInApk = Config.includeDependencyInfo includeInApk = false
includeInBundle = Config.includeDependencyInfo
} }
buildFeatures { buildFeatures {
viewBinding = true viewBinding = true
buildConfig = true buildConfig = true
aidl = true
// Disable some unused things // Disable some unused things
aidl = false
renderScript = false renderScript = false
shaders = false shaders = false
} }
@@ -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 { dependencies {
implementation(projects.i18n) implementation(projects.i18n)
implementation(projects.core.archive) implementation(projects.core.archive)
@@ -180,7 +153,6 @@ dependencies {
implementation(projects.domain) implementation(projects.domain)
implementation(projects.presentationCore) implementation(projects.presentationCore)
implementation(projects.presentationWidget) implementation(projects.presentationWidget)
implementation(projects.telemetry)
// Compose // Compose
implementation(compose.activity) implementation(compose.activity)
@@ -261,28 +233,28 @@ dependencies {
implementation(libs.directionalviewpager) { implementation(libs.directionalviewpager) {
exclude(group = "androidx.viewpager", module = "viewpager") exclude(group = "androidx.viewpager", module = "viewpager")
} }
implementation(libs.richeditor.compose) implementation(libs.insetter)
implementation(libs.bundles.richtext)
implementation(libs.aboutLibraries.compose) implementation(libs.aboutLibraries.compose)
implementation(libs.bundles.voyager) implementation(libs.bundles.voyager)
implementation(libs.compose.materialmotion) implementation(libs.compose.materialmotion)
implementation(libs.swipe) implementation(libs.swipe)
implementation(libs.compose.webview) implementation(libs.compose.webview)
implementation(libs.compose.grid) implementation(libs.compose.grid)
implementation(libs.reorderable)
implementation(libs.bundles.markdown)
// Logging // Logging
implementation(libs.logcat) implementation(libs.logcat)
// Crash reports/analytics
"standardImplementation"(platform(libs.firebase.bom))
"standardImplementation"(libs.firebase.analytics)
"standardImplementation"(libs.firebase.crashlytics)
// Shizuku // Shizuku
implementation(libs.bundles.shizuku) implementation(libs.bundles.shizuku)
// String similarity
implementation(libs.stringSimilarity)
// Tests // Tests
testImplementation(libs.bundles.test) testImplementation(libs.bundles.test)
testRuntimeOnly(libs.junit.platform.launcher)
// For detecting memory leaks; see https://square.github.io/leakcanary/ // For detecting memory leaks; see https://square.github.io/leakcanary/
// debugImplementation(libs.leakcanary.android) // debugImplementation(libs.leakcanary.android)
@@ -292,6 +264,14 @@ dependencies {
} }
androidComponents { androidComponents {
beforeVariants { variantBuilder ->
// Disables standardBenchmark
if (variantBuilder.buildType == "benchmark") {
variantBuilder.enable = variantBuilder.productFlavors.containsAll(
listOf("default" to "dev"),
)
}
}
onVariants(selector().withFlavor("default" to "standard")) { onVariants(selector().withFlavor("default" to "standard")) {
// Only excluding in standard flavor because this breaks // Only excluding in standard flavor because this breaks
// Layout Inspector's Compose tree // Layout Inspector's Compose tree
@@ -299,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 { buildscript {
dependencies { dependencies {
classpath(kotlinx.gradle) classpath(kotlinx.gradle)

View File

@@ -1,7 +0,0 @@
package mihon.app.shizuku;
interface IShellInterface {
void install(in AssetFileDescriptor apk) = 1;
void destroy() = 16777114;
}

View File

@@ -1,12 +1,11 @@
package eu.kanade.core.util package eu.kanade.core.util
import androidx.compose.ui.util.fastFilter
import androidx.compose.ui.util.fastForEach import androidx.compose.ui.util.fastForEach
import kotlin.contracts.ExperimentalContracts import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract import kotlin.contracts.contract
fun <T : R, R : Any> List<T>.insertSeparators( fun <T : R, R : Any> List<T>.insertSeparators(
generator: (before: T?, after: T?) -> R?, generator: (T?, T?) -> R?,
): List<R> { ): List<R> {
if (isEmpty()) return emptyList() if (isEmpty()) return emptyList()
val newList = mutableListOf<R>() val newList = mutableListOf<R>()
@@ -20,24 +19,6 @@ fun <T : R, R : Any> List<T>.insertSeparators(
return newList 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) { fun <E> HashSet<E>.addOrRemove(value: E, shouldAdd: Boolean) {
if (shouldAdd) { if (shouldAdd) {
add(value) 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]. * 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) @OptIn(ExperimentalContracts::class)
inline fun <T> List<T>.fastFilterNot(predicate: (T) -> Boolean): List<T> { inline fun <T> List<T>.fastFilterNot(predicate: (T) -> Boolean): List<T> {
contract { callsInPlace(predicate) } 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 } fastForEach { if (predicate(it)) --count }
return 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
}

View File

@@ -13,11 +13,9 @@ import eu.kanade.domain.manga.interactor.SetExcludedScanlators
import eu.kanade.domain.manga.interactor.SetMangaViewerFlags import eu.kanade.domain.manga.interactor.SetMangaViewerFlags
import eu.kanade.domain.manga.interactor.UpdateManga import eu.kanade.domain.manga.interactor.UpdateManga
import eu.kanade.domain.source.interactor.GetEnabledSources 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.GetLanguagesWithSources
import eu.kanade.domain.source.interactor.GetSourcesWithFavoriteCount import eu.kanade.domain.source.interactor.GetSourcesWithFavoriteCount
import eu.kanade.domain.source.interactor.SetMigrateSorting 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.ToggleLanguage
import eu.kanade.domain.source.interactor.ToggleSource import eu.kanade.domain.source.interactor.ToggleSource
import eu.kanade.domain.source.interactor.ToggleSourcePin 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.interactor.UpdateExtensionRepo
import mihon.domain.extensionrepo.repository.ExtensionRepoRepository import mihon.domain.extensionrepo.repository.ExtensionRepoRepository
import mihon.domain.extensionrepo.service.ExtensionRepoService import mihon.domain.extensionrepo.service.ExtensionRepoService
import mihon.domain.migration.usecases.MigrateMangaUseCase
import mihon.domain.upcoming.interactor.GetUpcomingManga import mihon.domain.upcoming.interactor.GetUpcomingManga
import tachiyomi.data.category.CategoryRepositoryImpl import tachiyomi.data.category.CategoryRepositoryImpl
import tachiyomi.data.chapter.ChapterRepositoryImpl 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.NetworkToLocalManga
import tachiyomi.domain.manga.interactor.ResetViewerFlags import tachiyomi.domain.manga.interactor.ResetViewerFlags
import tachiyomi.domain.manga.interactor.SetMangaChapterFlags import tachiyomi.domain.manga.interactor.SetMangaChapterFlags
import tachiyomi.domain.manga.interactor.UpdateMangaNotes
import tachiyomi.domain.manga.repository.MangaRepository import tachiyomi.domain.manga.repository.MangaRepository
import tachiyomi.domain.release.interactor.GetApplicationRelease import tachiyomi.domain.release.interactor.GetApplicationRelease
import tachiyomi.domain.release.service.ReleaseService import tachiyomi.domain.release.service.ReleaseService
@@ -113,7 +109,7 @@ class DomainModule : InjektModule {
addFactory { RenameCategory(get()) } addFactory { RenameCategory(get()) }
addFactory { ReorderCategory(get()) } addFactory { ReorderCategory(get()) }
addFactory { UpdateCategory(get()) } addFactory { UpdateCategory(get()) }
addFactory { DeleteCategory(get(), get(), get()) } addFactory { DeleteCategory(get()) }
addSingletonFactory<MangaRepository> { MangaRepositoryImpl(get()) } addSingletonFactory<MangaRepository> { MangaRepositoryImpl(get()) }
addFactory { GetDuplicateLibraryManga(get()) } addFactory { GetDuplicateLibraryManga(get()) }
@@ -131,15 +127,9 @@ class DomainModule : InjektModule {
addFactory { SetMangaViewerFlags(get()) } addFactory { SetMangaViewerFlags(get()) }
addFactory { NetworkToLocalManga(get()) } addFactory { NetworkToLocalManga(get()) }
addFactory { UpdateManga(get(), get()) } addFactory { UpdateManga(get(), get()) }
addFactory { UpdateMangaNotes(get()) }
addFactory { SetMangaCategories(get()) } addFactory { SetMangaCategories(get()) }
addFactory { GetExcludedScanlators(get()) } addFactory { GetExcludedScanlators(get()) }
addFactory { SetExcludedScanlators(get()) } addFactory { SetExcludedScanlators(get()) }
addFactory {
MigrateMangaUseCase(
get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(),
)
}
addSingletonFactory<ReleaseService> { ReleaseServiceImpl(get(), get()) } addSingletonFactory<ReleaseService> { ReleaseServiceImpl(get(), get()) }
addFactory { GetApplicationRelease(get(), get()) } addFactory { GetApplicationRelease(get(), get()) }
@@ -161,7 +151,7 @@ class DomainModule : InjektModule {
addFactory { UpdateChapter(get()) } addFactory { UpdateChapter(get()) }
addFactory { SetReadStatus(get(), get(), get(), get()) } addFactory { SetReadStatus(get(), get(), get(), get()) }
addFactory { ShouldUpdateDbChapter() } 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 { GetAvailableScanlators(get()) }
addFactory { FilterChaptersForDownload(get(), get(), get()) } addFactory { FilterChaptersForDownload(get(), get(), get()) }
@@ -201,7 +191,5 @@ class DomainModule : InjektModule {
addFactory { DeleteExtensionRepo(get()) } addFactory { DeleteExtensionRepo(get()) }
addFactory { ReplaceExtensionRepo(get()) } addFactory { ReplaceExtensionRepo(get()) }
addFactory { UpdateExtensionRepo(get(), get()) } addFactory { UpdateExtensionRepo(get(), get()) }
addFactory { ToggleIncognito(get()) }
addFactory { GetIncognitoState(get(), get(), get()) }
} }
} }

View File

@@ -2,7 +2,6 @@ package eu.kanade.domain.base
import android.content.Context import android.content.Context
import dev.icerock.moko.resources.StringResource import dev.icerock.moko.resources.StringResource
import eu.kanade.tachiyomi.util.system.GLUtil
import tachiyomi.core.common.preference.Preference import tachiyomi.core.common.preference.Preference
import tachiyomi.core.common.preference.PreferenceStore import tachiyomi.core.common.preference.PreferenceStore
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
@@ -31,8 +30,4 @@ class BasePreferences(
} }
fun displayProfile() = preferenceStore.getString("pref_display_profile_key", "") 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)
} }

View File

@@ -19,7 +19,6 @@ import tachiyomi.domain.chapter.model.NoChaptersException
import tachiyomi.domain.chapter.model.toChapterUpdate import tachiyomi.domain.chapter.model.toChapterUpdate
import tachiyomi.domain.chapter.repository.ChapterRepository import tachiyomi.domain.chapter.repository.ChapterRepository
import tachiyomi.domain.chapter.service.ChapterRecognition import tachiyomi.domain.chapter.service.ChapterRecognition
import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import tachiyomi.source.local.isLocal import tachiyomi.source.local.isLocal
import java.lang.Long.max import java.lang.Long.max
@@ -35,7 +34,6 @@ class SyncChaptersWithSource(
private val updateChapter: UpdateChapter, private val updateChapter: UpdateChapter,
private val getChaptersByMangaId: GetChaptersByMangaId, private val getChaptersByMangaId: GetChaptersByMangaId,
private val getExcludedScanlators: GetExcludedScanlators, private val getExcludedScanlators: GetExcludedScanlators,
private val libraryPreferences: LibraryPreferences,
) { ) {
/** /**
@@ -114,7 +112,6 @@ class SyncChaptersWithSource(
downloadManager.isChapterDownloaded( downloadManager.isChapterDownloaded(
dbChapter.name, dbChapter.name,
dbChapter.scanlator, dbChapter.scanlator,
dbChapter.url,
manga.title, manga.title,
manga.source, manga.source,
) )
@@ -122,14 +119,12 @@ class SyncChaptersWithSource(
if (shouldRenameChapter) { if (shouldRenameChapter) {
downloadManager.renameChapter(source, manga, dbChapter, chapter) downloadManager.renameChapter(source, manga, dbChapter, chapter)
} }
var toChangeChapter = dbChapter.copy( var toChangeChapter = dbChapter.copy(
name = chapter.name, name = chapter.name,
chapterNumber = chapter.chapterNumber, chapterNumber = chapter.chapterNumber,
scanlator = chapter.scanlator, scanlator = chapter.scanlator,
sourceOrder = chapter.sourceOrder, sourceOrder = chapter.sourceOrder,
) )
if (chapter.dateUpload != 0L) { if (chapter.dateUpload != 0L) {
toChangeChapter = toChangeChapter.copy(dateUpload = chapter.dateUpload) toChangeChapter = toChangeChapter.copy(dateUpload = chapter.dateUpload)
} }
@@ -150,18 +145,12 @@ class SyncChaptersWithSource(
return emptyList() return emptyList()
} }
val changedOrDuplicateReadUrls = mutableSetOf<String>() val reAdded = mutableListOf<Chapter>()
val deletedChapterNumbers = TreeSet<Double>() val deletedChapterNumbers = TreeSet<Double>()
val deletedReadChapterNumbers = TreeSet<Double>() val deletedReadChapterNumbers = TreeSet<Double>()
val deletedBookmarkedChapterNumbers = TreeSet<Double>() val deletedBookmarkedChapterNumbers = TreeSet<Double>()
val readChapterNumbers = dbChapters
.asSequence()
.filter { it.read && it.isRecognizedNumber }
.map { it.chapterNumber }
.toSet()
removedChapters.forEach { chapter -> removedChapters.forEach { chapter ->
if (chapter.read) deletedReadChapterNumbers.add(chapter.chapterNumber) if (chapter.read) deletedReadChapterNumbers.add(chapter.chapterNumber)
if (chapter.bookmark) deletedBookmarkedChapterNumbers.add(chapter.chapterNumber) if (chapter.bookmark) deletedBookmarkedChapterNumbers.add(chapter.chapterNumber)
@@ -171,20 +160,12 @@ class SyncChaptersWithSource(
val deletedChapterNumberDateFetchMap = removedChapters.sortedByDescending { it.dateFetch } val deletedChapterNumberDateFetchMap = removedChapters.sortedByDescending { it.dateFetch }
.associate { it.chapterNumber to 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 // 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. // Sources MUST return the chapters from most to less recent, which is common.
var itemCount = newChapters.size var itemCount = newChapters.size
var updatedToAdd = newChapters.map { toAddItem -> var updatedToAdd = newChapters.map { toAddItem ->
var chapter = toAddItem.copy(dateFetch = nowMillis + itemCount--) 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 if (!chapter.isRecognizedNumber || chapter.chapterNumber !in deletedChapterNumbers) return@map chapter
chapter = chapter.copy( chapter = chapter.copy(
@@ -197,7 +178,7 @@ class SyncChaptersWithSource(
chapter = chapter.copy(dateFetch = it) chapter = chapter.copy(dateFetch = it)
} }
changedOrDuplicateReadUrls.add(chapter.url) reAdded.add(chapter)
chapter chapter
} }
@@ -221,8 +202,12 @@ class SyncChaptersWithSource(
// Note that last_update actually represents last time the chapter list changed at all // Note that last_update actually represents last time the chapter list changed at all
updateManga.awaitUpdateLastUpdate(manga.id) updateManga.awaitUpdateLastUpdate(manga.id)
val reAddedUrls = reAdded.map { it.url }.toHashSet()
val excludedScanlators = getExcludedScanlators.await(manga.id).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
}
} }
} }

View File

@@ -26,7 +26,6 @@ fun List<Chapter>.applyFilters(manga: Manga, downloadManager: DownloadManager):
val downloaded = downloadManager.isChapterDownloaded( val downloaded = downloadManager.isChapterDownloaded(
chapter.name, chapter.name,
chapter.scanlator, chapter.scanlator,
chapter.url,
manga.title, manga.title,
manga.source, manga.source,
) )

View File

@@ -2,9 +2,7 @@ package eu.kanade.domain.manga.interactor
import eu.kanade.domain.manga.model.hasCustomCover import eu.kanade.domain.manga.model.hasCustomCover
import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.domain.manga.interactor.FetchInterval import tachiyomi.domain.manga.interactor.FetchInterval
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.manga.model.MangaUpdate import tachiyomi.domain.manga.model.MangaUpdate
@@ -33,8 +31,6 @@ class UpdateManga(
remoteManga: SManga, remoteManga: SManga,
manualFetch: Boolean, manualFetch: Boolean,
coverCache: CoverCache = Injekt.get(), coverCache: CoverCache = Injekt.get(),
libraryPreferences: LibraryPreferences = Injekt.get(),
downloadManager: DownloadManager = Injekt.get(),
): Boolean { ): Boolean {
val remoteTitle = try { val remoteTitle = try {
remoteManga.title 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 // if the manga isn't a favorite, set its title from source and update in db
val title = val title = if (remoteTitle.isEmpty() || localManga.favorite) null else remoteTitle
if (remoteTitle.isNotEmpty() && (!localManga.favorite || libraryPreferences.updateMangaTitles().get())) {
remoteTitle
} else {
null
}
val coverLastModified = val coverLastModified =
when { when {
@@ -68,7 +59,7 @@ class UpdateManga(
val thumbnailUrl = remoteManga.thumbnail_url?.takeIf { it.isNotEmpty() } val thumbnailUrl = remoteManga.thumbnail_url?.takeIf { it.isNotEmpty() }
val success = mangaRepository.update( return mangaRepository.update(
MangaUpdate( MangaUpdate(
id = localManga.id, id = localManga.id,
title = title, title = title,
@@ -83,10 +74,6 @@ class UpdateManga(
initialized = true, initialized = true,
), ),
) )
if (success && title != null) {
downloadManager.renameManga(localManga, title)
}
return success
} }
suspend fun awaitUpdateFetchInterval( suspend fun awaitUpdateFetchInterval(

View File

@@ -22,7 +22,7 @@ val Manga.readerOrientation: Long
val Manga.downloadedFilter: TriState val Manga.downloadedFilter: TriState
get() { get() {
if (Injekt.get<BasePreferences>().downloadedOnly().get()) return TriState.ENABLED_IS if (forceDownloaded()) return TriState.ENABLED_IS
return when (downloadedFilterRaw) { return when (downloadedFilterRaw) {
Manga.CHAPTER_SHOW_DOWNLOADED -> TriState.ENABLED_IS Manga.CHAPTER_SHOW_DOWNLOADED -> TriState.ENABLED_IS
Manga.CHAPTER_SHOW_NOT_DOWNLOADED -> TriState.ENABLED_NOT Manga.CHAPTER_SHOW_NOT_DOWNLOADED -> TriState.ENABLED_NOT
@@ -34,6 +34,9 @@ fun Manga.chaptersFiltered(): Boolean {
downloadedFilter != TriState.DISABLED || downloadedFilter != TriState.DISABLED ||
bookmarkedFilter != TriState.DISABLED bookmarkedFilter != TriState.DISABLED
} }
fun Manga.forceDownloaded(): Boolean {
return favorite && Injekt.get<BasePreferences>().downloadedOnly().get()
}
fun Manga.toSManga(): SManga = SManga.create().also { fun Manga.toSManga(): SManga = SManga.create().also {
it.url = url 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 { fun Manga.hasCustomCover(coverCache: CoverCache = Injekt.get()): Boolean {
return coverCache.getCustomCoverFile(id).exists() return coverCache.getCustomCoverFile(id).exists()
} }

View File

@@ -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()
}
}

View File

@@ -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)
}
}
}

View File

@@ -2,18 +2,16 @@ package eu.kanade.domain.source.service
import eu.kanade.domain.source.interactor.SetMigrateSorting import eu.kanade.domain.source.interactor.SetMigrateSorting
import eu.kanade.tachiyomi.util.system.LocaleHelper import eu.kanade.tachiyomi.util.system.LocaleHelper
import mihon.domain.migration.models.MigrationFlag
import tachiyomi.core.common.preference.Preference import tachiyomi.core.common.preference.Preference
import tachiyomi.core.common.preference.PreferenceStore import tachiyomi.core.common.preference.PreferenceStore
import tachiyomi.core.common.preference.getEnum import tachiyomi.core.common.preference.getEnum
import tachiyomi.core.common.preference.getLongArray
import tachiyomi.domain.library.model.LibraryDisplayMode import tachiyomi.domain.library.model.LibraryDisplayMode
class SourcePreferences( class SourcePreferences(
private val preferenceStore: PreferenceStore, private val preferenceStore: PreferenceStore,
) { ) {
fun sourceDisplayMode() = preferenceStore.getObjectFromString( fun sourceDisplayMode() = preferenceStore.getObject(
"pref_display_mode_catalogue", "pref_display_mode_catalogue",
LibraryDisplayMode.default, LibraryDisplayMode.default,
LibraryDisplayMode.Serializer::serialize, LibraryDisplayMode.Serializer::serialize,
@@ -24,8 +22,6 @@ class SourcePreferences(
fun disabledSources() = preferenceStore.getStringSet("hidden_catalogues", emptySet()) fun disabledSources() = preferenceStore.getStringSet("hidden_catalogues", emptySet())
fun incognitoExtensions() = preferenceStore.getStringSet("incognito_extensions", emptySet())
fun pinnedSources() = preferenceStore.getStringSet("pinned_catalogues", emptySet()) fun pinnedSources() = preferenceStore.getStringSet("pinned_catalogues", emptySet())
fun lastUsedSource() = preferenceStore.getLong( fun lastUsedSource() = preferenceStore.getLong(
@@ -57,21 +53,4 @@ class SourcePreferences(
Preference.appStateKey("has_filters_toggle_state"), Preference.appStateKey("has_filters_toggle_state"),
false, 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)
} }

View File

@@ -5,7 +5,6 @@ import eu.kanade.domain.track.model.toDomainTrack
import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.EnhancedTracker import eu.kanade.tachiyomi.data.track.EnhancedTracker
import eu.kanade.tachiyomi.data.track.Tracker import eu.kanade.tachiyomi.data.track.Tracker
import eu.kanade.tachiyomi.data.track.TrackerManager
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.util.lang.convertEpochMillisZone import eu.kanade.tachiyomi.util.lang.convertEpochMillisZone
import logcat.LogPriority import logcat.LogPriority
@@ -15,16 +14,17 @@ import tachiyomi.core.common.util.system.logcat
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
import tachiyomi.domain.history.interactor.GetHistory import tachiyomi.domain.history.interactor.GetHistory
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.track.interactor.GetTracks
import tachiyomi.domain.track.interactor.InsertTrack import tachiyomi.domain.track.interactor.InsertTrack
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.time.ZoneOffset import java.time.ZoneOffset
class AddTracks( class AddTracks(
private val getTracks: GetTracks,
private val insertTrack: InsertTrack, private val insertTrack: InsertTrack,
private val syncChapterProgressWithTrack: SyncChapterProgressWithTrack, private val syncChapterProgressWithTrack: SyncChapterProgressWithTrack,
private val getChaptersByMangaId: GetChaptersByMangaId, private val getChaptersByMangaId: GetChaptersByMangaId,
private val trackerManager: TrackerManager,
) { ) {
// TODO: update all trackers based on common data // TODO: update all trackers based on common data
@@ -79,7 +79,7 @@ class AddTracks(
suspend fun bindEnhancedTrackers(manga: Manga, source: Source) = withNonCancellableContext { suspend fun bindEnhancedTrackers(manga: Manga, source: Source) = withNonCancellableContext {
withIOContext { withIOContext {
trackerManager.loggedInTrackers() getTracks.await(manga.id)
.filterIsInstance<EnhancedTracker>() .filterIsInstance<EnhancedTracker>()
.filter { it.accept(source) } .filter { it.accept(source) }
.forEach { service -> .forEach { service ->
@@ -87,11 +87,11 @@ class AddTracks(
service.match(manga)?.let { track -> service.match(manga)?.let { track ->
track.manga_id = manga.id track.manga_id = manga.id
(service as Tracker).bind(track) (service as Tracker).bind(track)
insertTrack.await(track.toDomainTrack(idRequired = false)!!) insertTrack.await(track.toDomainTrack()!!)
syncChapterProgressWithTrack.await( syncChapterProgressWithTrack.await(
manga.id, manga.id,
track.toDomainTrack(idRequired = false)!!, track.toDomainTrack()!!,
service, service,
) )
} }

View File

@@ -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),
}

View File

@@ -10,7 +10,6 @@ fun Track.copyPersonalFrom(other: Track): Track {
status = other.status, status = other.status,
startDate = other.startDate, startDate = other.startDate,
finishDate = other.finishDate, finishDate = other.finishDate,
private = other.private,
) )
} }
@@ -27,7 +26,6 @@ fun Track.toDbTrack(): DbTrack = DbTrack.create(trackerId).also {
it.tracking_url = remoteUrl it.tracking_url = remoteUrl
it.started_reading_date = startDate it.started_reading_date = startDate
it.finished_reading_date = finishDate it.finished_reading_date = finishDate
it.private = private
} }
fun DbTrack.toDomainTrack(idRequired: Boolean = true): Track? { fun DbTrack.toDomainTrack(idRequired: Boolean = true): Track? {
@@ -46,6 +44,5 @@ fun DbTrack.toDomainTrack(idRequired: Boolean = true): Track? {
remoteUrl = tracking_url, remoteUrl = tracking_url,
startDate = started_reading_date, startDate = started_reading_date,
finishDate = finished_reading_date, finishDate = finished_reading_date,
private = private,
) )
} }

View File

@@ -1,11 +1,9 @@
package eu.kanade.domain.track.service 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.Tracker
import eu.kanade.tachiyomi.data.track.anilist.Anilist import eu.kanade.tachiyomi.data.track.anilist.Anilist
import tachiyomi.core.common.preference.Preference import tachiyomi.core.common.preference.Preference
import tachiyomi.core.common.preference.PreferenceStore import tachiyomi.core.common.preference.PreferenceStore
import tachiyomi.core.common.preference.getEnum
class TrackPreferences( class TrackPreferences(
private val preferenceStore: PreferenceStore, private val preferenceStore: PreferenceStore,
@@ -37,9 +35,4 @@ class TrackPreferences(
fun anilistScoreType() = preferenceStore.getString("anilist_score_type", Anilist.POINT_10) fun anilistScoreType() = preferenceStore.getString("anilist_score_type", Anilist.POINT_10)
fun autoUpdateTrack() = preferenceStore.getBoolean("pref_auto_update_manga_sync_key", true) fun autoUpdateTrack() = preferenceStore.getBoolean("pref_auto_update_manga_sync_key", true)
fun autoUpdateTrackOnMarkRead() = preferenceStore.getEnum(
"pref_auto_update_manga_on_mark_read",
AutoTrackState.ALWAYS,
)
} }

View File

@@ -34,8 +34,6 @@ class UiPreferences(
fun tabletUiMode() = preferenceStore.getEnum("tablet_ui_mode", TabletUiMode.AUTOMATIC) fun tabletUiMode() = preferenceStore.getEnum("tablet_ui_mode", TabletUiMode.AUTOMATIC)
fun imagesInDescription() = preferenceStore.getBoolean("pref_render_images_description", true)
companion object { companion object {
fun dateFormat(format: String): DateTimeFormatter = when (format) { fun dateFormat(format: String): DateTimeFormatter = when (format) {
"" -> DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT) "" -> DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT)

View File

@@ -1,23 +1,25 @@
package eu.kanade.domain.ui.model package eu.kanade.domain.ui.model
import dev.icerock.moko.resources.StringResource import dev.icerock.moko.resources.StringResource
import eu.kanade.tachiyomi.util.system.isDevFlavor
import eu.kanade.tachiyomi.util.system.isPreviewBuildType
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
enum class AppTheme(val titleRes: StringResource?) { enum class AppTheme(val titleRes: StringResource?) {
DEFAULT(MR.strings.label_default), DEFAULT(MR.strings.label_default),
MONET(MR.strings.theme_monet), MONET(MR.strings.theme_monet),
CATPPUCCIN(MR.strings.theme_catppuccin),
GREEN_APPLE(MR.strings.theme_greenapple), GREEN_APPLE(MR.strings.theme_greenapple),
LAVENDER(MR.strings.theme_lavender), LAVENDER(MR.strings.theme_lavender),
MIDNIGHT_DUSK(MR.strings.theme_midnightdusk), 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), STRAWBERRY_DAIQUIRI(MR.strings.theme_strawberrydaiquiri),
TAKO(MR.strings.theme_tako), TAKO(MR.strings.theme_tako),
TEALTURQUOISE(MR.strings.theme_tealturquoise), TEALTURQUOISE(MR.strings.theme_tealturquoise),
TIDAL_WAVE(MR.strings.theme_tidalwave), TIDAL_WAVE(MR.strings.theme_tidalwave),
YINYANG(MR.strings.theme_yinyang), YINYANG(MR.strings.theme_yinyang),
YOTSUBA(MR.strings.theme_yotsuba), YOTSUBA(MR.strings.theme_yotsuba),
MONOCHROME(MR.strings.theme_monochrome),
// Deprecated // Deprecated
DARK_BLUE(null), DARK_BLUE(null),

View File

@@ -73,18 +73,10 @@ fun BrowseSourceContent(
} }
} }
if (mangaList.itemCount == 0 && mangaList.loadState.refresh is LoadState.Loading) { if (mangaList.itemCount <= 0 && errorState != null && errorState is LoadState.Error) {
LoadingScreen(Modifier.padding(contentPadding))
return
}
if (mangaList.itemCount == 0) {
EmptyScreen( EmptyScreen(
modifier = Modifier.padding(contentPadding), modifier = Modifier.padding(contentPadding),
message = when (errorState) { message = getErrorMessage(errorState),
is LoadState.Error -> getErrorMessage(errorState)
else -> stringResource(MR.strings.no_results_found)
},
actions = if (source is LocalSource) { actions = if (source is LocalSource) {
persistentListOf( persistentListOf(
EmptyScreenAction( EmptyScreenAction(
@@ -117,6 +109,13 @@ fun BrowseSourceContent(
return return
} }
if (mangaList.itemCount == 0 && mangaList.loadState.refresh is LoadState.Loading) {
LoadingScreen(
modifier = Modifier.padding(contentPadding),
)
return
}
when (displayMode) { when (displayMode) {
LibraryDisplayMode.ComfortableGrid -> { LibraryDisplayMode.ComfortableGrid -> {
BrowseSourceComfortableGrid( BrowseSourceComfortableGrid(

View File

@@ -35,10 +35,8 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign 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.components.WarningBanner
import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget
import eu.kanade.presentation.more.settings.widget.TrailingWidgetBuffer import eu.kanade.presentation.more.settings.widget.TrailingWidgetBuffer
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.extension.model.Extension import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.source.ConfigurableSource import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionDetailsScreenModel import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionDetailsScreenModel
@@ -75,7 +72,6 @@ fun ExtensionDetailsScreen(
onClickClearCookies: () -> Unit, onClickClearCookies: () -> Unit,
onClickUninstall: () -> Unit, onClickUninstall: () -> Unit,
onClickSource: (sourceId: Long) -> Unit, onClickSource: (sourceId: Long) -> Unit,
onClickIncognito: (Boolean) -> Unit,
) { ) {
val uriHandler = LocalUriHandler.current val uriHandler = LocalUriHandler.current
val url = remember(state.extension) { val url = remember(state.extension) {
@@ -144,11 +140,9 @@ fun ExtensionDetailsScreen(
contentPadding = paddingValues, contentPadding = paddingValues,
extension = state.extension, extension = state.extension,
sources = state.sources, sources = state.sources,
incognitoMode = state.isIncognito,
onClickSourcePreferences = onClickSourcePreferences, onClickSourcePreferences = onClickSourcePreferences,
onClickUninstall = onClickUninstall, onClickUninstall = onClickUninstall,
onClickSource = onClickSource, onClickSource = onClickSource,
onClickIncognito = onClickIncognito,
) )
} }
} }
@@ -158,11 +152,9 @@ private fun ExtensionDetails(
contentPadding: PaddingValues, contentPadding: PaddingValues,
extension: Extension.Installed, extension: Extension.Installed,
sources: ImmutableList<ExtensionSourceItem>, sources: ImmutableList<ExtensionSourceItem>,
incognitoMode: Boolean,
onClickSourcePreferences: (sourceId: Long) -> Unit, onClickSourcePreferences: (sourceId: Long) -> Unit,
onClickUninstall: () -> Unit, onClickUninstall: () -> Unit,
onClickSource: (sourceId: Long) -> Unit, onClickSource: (sourceId: Long) -> Unit,
onClickIncognito: (Boolean) -> Unit,
) { ) {
val context = LocalContext.current val context = LocalContext.current
var showNsfwWarning by remember { mutableStateOf(false) } var showNsfwWarning by remember { mutableStateOf(false) }
@@ -179,7 +171,6 @@ private fun ExtensionDetails(
item { item {
DetailsHeader( DetailsHeader(
extension = extension, extension = extension,
extIncognitoMode = incognitoMode,
onClickUninstall = onClickUninstall, onClickUninstall = onClickUninstall,
onClickAppInfo = { onClickAppInfo = {
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
@@ -191,7 +182,6 @@ private fun ExtensionDetails(
onClickAgeRating = { onClickAgeRating = {
showNsfwWarning = true showNsfwWarning = true
}, },
onExtIncognitoChange = onClickIncognito,
) )
} }
@@ -219,11 +209,9 @@ private fun ExtensionDetails(
@Composable @Composable
private fun DetailsHeader( private fun DetailsHeader(
extension: Extension, extension: Extension,
extIncognitoMode: Boolean,
onClickAgeRating: () -> Unit, onClickAgeRating: () -> Unit,
onClickUninstall: () -> Unit, onClickUninstall: () -> Unit,
onClickAppInfo: (() -> Unit)?, onClickAppInfo: (() -> Unit)?,
onExtIncognitoChange: (Boolean) -> Unit,
) { ) {
val context = LocalContext.current val context = LocalContext.current
@@ -231,8 +219,9 @@ private fun DetailsHeader(
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = MaterialTheme.padding.medium)
.padding( .padding(
start = MaterialTheme.padding.medium,
end = MaterialTheme.padding.medium,
top = MaterialTheme.padding.medium, top = MaterialTheme.padding.medium,
bottom = MaterialTheme.padding.small, bottom = MaterialTheme.padding.small,
) )
@@ -324,9 +313,12 @@ private fun DetailsHeader(
} }
Row( Row(
modifier = Modifier modifier = Modifier.padding(
.padding(horizontal = MaterialTheme.padding.medium) start = MaterialTheme.padding.medium,
.padding(top = MaterialTheme.padding.small), end = MaterialTheme.padding.medium,
top = MaterialTheme.padding.small,
bottom = MaterialTheme.padding.medium,
),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.medium), horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.medium),
) { ) {
OutlinedButton( 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() HorizontalDivider()
} }
} }

View File

@@ -353,17 +353,13 @@ private fun ExtensionItemContent(
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall), horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
) { ) {
ProvideTextStyle(value = MaterialTheme.typography.bodySmall) { ProvideTextStyle(value = MaterialTheme.typography.bodySmall) {
var hasAlreadyShownAnElement by remember { mutableStateOf(false) }
if (extension is Extension.Installed && extension.lang.isNotEmpty()) { if (extension is Extension.Installed && extension.lang.isNotEmpty()) {
hasAlreadyShownAnElement = true
Text( Text(
text = LocaleHelper.getSourceDisplayName(extension.lang, LocalContext.current), text = LocaleHelper.getSourceDisplayName(extension.lang, LocalContext.current),
) )
} }
if (extension.versionName.isNotEmpty()) { if (extension.versionName.isNotEmpty()) {
if (hasAlreadyShownAnElement) DotSeparatorNoSpaceText()
hasAlreadyShownAnElement = true
Text( Text(
text = extension.versionName, text = extension.versionName,
) )
@@ -376,8 +372,6 @@ private fun ExtensionItemContent(
else -> null else -> null
} }
if (warning != null) { if (warning != null) {
if (hasAlreadyShownAnElement) DotSeparatorNoSpaceText()
hasAlreadyShownAnElement = true
Text( Text(
text = stringResource(warning).uppercase(), text = stringResource(warning).uppercase(),
color = MaterialTheme.colorScheme.error, color = MaterialTheme.colorScheme.error,
@@ -385,12 +379,6 @@ private fun ExtensionItemContent(
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
) )
} }
if (extension is Extension.Installed && !extension.isShared) {
if (hasAlreadyShownAnElement) DotSeparatorNoSpaceText()
Text(
text = stringResource(MR.strings.ext_installer_private),
)
}
if (!installStep.isCompleted()) { if (!installStep.isCompleted()) {
DotSeparatorNoSpaceText() DotSeparatorNoSpaceText()

View File

@@ -40,7 +40,6 @@ fun GlobalSearchScreen(
navigateUp = navigateUp, navigateUp = navigateUp,
onChangeSearchQuery = onChangeSearchQuery, onChangeSearchQuery = onChangeSearchQuery,
onSearch = onSearch, onSearch = onSearch,
hideSourceFilter = false,
sourceFilter = state.sourceFilter, sourceFilter = state.sourceFilter,
onChangeSearchFilter = onChangeSearchFilter, onChangeSearchFilter = onChangeSearchFilter,
onlyShowHasResults = state.onlyShowHasResults, onlyShowHasResults = state.onlyShowHasResults,

View File

@@ -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) },
)
}

View File

@@ -32,7 +32,6 @@ fun MigrateSearchScreen(
navigateUp = navigateUp, navigateUp = navigateUp,
onChangeSearchQuery = onChangeSearchQuery, onChangeSearchQuery = onChangeSearchQuery,
onSearch = onSearch, onSearch = onSearch,
hideSourceFilter = true,
sourceFilter = state.sourceFilter, sourceFilter = state.sourceFilter,
onChangeSearchFilter = onChangeSearchFilter, onChangeSearchFilter = onChangeSearchFilter,
onlyShowHasResults = state.onlyShowHasResults, onlyShowHasResults = state.onlyShowHasResults,

View File

@@ -40,7 +40,6 @@ fun GlobalSearchToolbar(
navigateUp: () -> Unit, navigateUp: () -> Unit,
onChangeSearchQuery: (String?) -> Unit, onChangeSearchQuery: (String?) -> Unit,
onSearch: (String) -> Unit, onSearch: (String) -> Unit,
hideSourceFilter: Boolean,
sourceFilter: SourceFilter, sourceFilter: SourceFilter,
onChangeSearchFilter: (SourceFilter) -> Unit, onChangeSearchFilter: (SourceFilter) -> Unit,
onlyShowHasResults: Boolean, onlyShowHasResults: Boolean,
@@ -74,40 +73,38 @@ fun GlobalSearchToolbar(
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small), horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
) { ) {
// TODO: make this UX better; it only applies when triggering a new search // TODO: make this UX better; it only applies when triggering a new search
if (!hideSourceFilter) { FilterChip(
FilterChip( selected = sourceFilter == SourceFilter.PinnedOnly,
selected = sourceFilter == SourceFilter.PinnedOnly, onClick = { onChangeSearchFilter(SourceFilter.PinnedOnly) },
onClick = { onChangeSearchFilter(SourceFilter.PinnedOnly) }, leadingIcon = {
leadingIcon = { Icon(
Icon( imageVector = Icons.Outlined.PushPin,
imageVector = Icons.Outlined.PushPin, contentDescription = null,
contentDescription = null, modifier = Modifier
modifier = Modifier .size(FilterChipDefaults.IconSize),
.size(FilterChipDefaults.IconSize), )
) },
}, label = {
label = { Text(text = stringResource(MR.strings.pinned_sources))
Text(text = stringResource(MR.strings.pinned_sources)) },
}, )
) FilterChip(
FilterChip( selected = sourceFilter == SourceFilter.All,
selected = sourceFilter == SourceFilter.All, onClick = { onChangeSearchFilter(SourceFilter.All) },
onClick = { onChangeSearchFilter(SourceFilter.All) }, leadingIcon = {
leadingIcon = { Icon(
Icon( imageVector = Icons.Outlined.DoneAll,
imageVector = Icons.Outlined.DoneAll, contentDescription = null,
contentDescription = null, modifier = Modifier
modifier = Modifier .size(FilterChipDefaults.IconSize),
.size(FilterChipDefaults.IconSize), )
) },
}, label = {
label = { Text(text = stringResource(MR.strings.all))
Text(text = stringResource(MR.strings.all)) },
}, )
)
VerticalDivider() VerticalDivider()
}
FilterChip( FilterChip(
selected = onlyShowHasResults, selected = onlyShowHasResults,

View File

@@ -2,24 +2,22 @@ package eu.kanade.presentation.category
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState 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.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.SortByAlpha
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable 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 androidx.compose.ui.Modifier
import eu.kanade.presentation.category.components.CategoryFloatingActionButton import eu.kanade.presentation.category.components.CategoryFloatingActionButton
import eu.kanade.presentation.category.components.CategoryListItem import eu.kanade.presentation.category.components.CategoryListItem
import eu.kanade.presentation.components.AppBar import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.AppBarActions
import eu.kanade.tachiyomi.ui.category.CategoryScreenState import eu.kanade.tachiyomi.ui.category.CategoryScreenState
import sh.calvin.reorderable.ReorderableItem import kotlinx.collections.immutable.persistentListOf
import sh.calvin.reorderable.rememberReorderableLazyListState
import tachiyomi.domain.category.model.Category import tachiyomi.domain.category.model.Category
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.components.material.Scaffold
@@ -33,9 +31,11 @@ import tachiyomi.presentation.core.util.plus
fun CategoryScreen( fun CategoryScreen(
state: CategoryScreenState.Success, state: CategoryScreenState.Success,
onClickCreate: () -> Unit, onClickCreate: () -> Unit,
onClickSortAlphabetically: () -> Unit,
onClickRename: (Category) -> Unit, onClickRename: (Category) -> Unit,
onClickDelete: (Category) -> Unit, onClickDelete: (Category) -> Unit,
onChangeOrder: (Category, Int) -> Unit, onClickMoveUp: (Category) -> Unit,
onClickMoveDown: (Category) -> Unit,
navigateUp: () -> Unit, navigateUp: () -> Unit,
) { ) {
val lazyListState = rememberLazyListState() val lazyListState = rememberLazyListState()
@@ -44,6 +44,17 @@ fun CategoryScreen(
AppBar( AppBar(
title = stringResource(MR.strings.action_edit_categories), title = stringResource(MR.strings.action_edit_categories),
navigateUp = navigateUp, navigateUp = navigateUp,
actions = {
AppBarActions(
persistentListOf(
AppBar.Action(
title = stringResource(MR.strings.action_sort),
icon = Icons.Outlined.SortByAlpha,
onClick = onClickSortAlphabetically,
),
),
)
},
scrollBehavior = scrollBehavior, scrollBehavior = scrollBehavior,
) )
}, },
@@ -65,10 +76,13 @@ fun CategoryScreen(
CategoryContent( CategoryContent(
categories = state.categories, categories = state.categories,
lazyListState = lazyListState, lazyListState = lazyListState,
paddingValues = paddingValues, paddingValues = paddingValues +
topSmallPaddingValues +
PaddingValues(horizontal = MaterialTheme.padding.medium),
onClickRename = onClickRename, onClickRename = onClickRename,
onClickDelete = onClickDelete, onClickDelete = onClickDelete,
onChangeOrder = onChangeOrder, onMoveUp = onClickMoveUp,
onMoveDown = onClickMoveDown,
) )
} }
} }
@@ -80,44 +94,28 @@ private fun CategoryContent(
paddingValues: PaddingValues, paddingValues: PaddingValues,
onClickRename: (Category) -> Unit, onClickRename: (Category) -> Unit,
onClickDelete: (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( LazyColumn(
modifier = Modifier.fillMaxSize(),
state = lazyListState, state = lazyListState,
contentPadding = paddingValues + contentPadding = paddingValues,
topSmallPaddingValues +
PaddingValues(horizontal = MaterialTheme.padding.medium),
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small), verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
) { ) {
items( itemsIndexed(
items = categoriesState, items = categories,
key = { category -> category.key }, key = { _, category -> "category-${category.id}" },
) { category -> ) { index, category ->
ReorderableItem(reorderableState, category.key) { CategoryListItem(
CategoryListItem( modifier = Modifier.animateItem(),
modifier = Modifier.animateItem(), category = category,
category = category, canMoveUp = index != 0,
onRename = { onClickRename(category) }, canMoveDown = index != categories.lastIndex,
onDelete = { onClickDelete(category) }, onMoveUp = onMoveUp,
) onMoveDown = onMoveDown,
} onRename = { onClickRename(category) },
onDelete = { onClickDelete(category) },
)
} }
} }
} }
private val Category.key inline get() = "category-$id"

View File

@@ -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 @Composable
fun ChangeCategoryDialog( fun ChangeCategoryDialog(
initialSelection: ImmutableList<CheckboxState<Category>>, initialSelection: ImmutableList<CheckboxState<Category>>,

View File

@@ -2,11 +2,14 @@ package eu.kanade.presentation.category.components
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons 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.Delete
import androidx.compose.material.icons.outlined.DragHandle
import androidx.compose.material.icons.outlined.Edit import androidx.compose.material.icons.outlined.Edit
import androidx.compose.material3.ElevatedCard import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
@@ -16,42 +19,57 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import sh.calvin.reorderable.ReorderableCollectionItemScope
import tachiyomi.domain.category.model.Category import tachiyomi.domain.category.model.Category
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
@Composable @Composable
fun ReorderableCollectionItemScope.CategoryListItem( fun CategoryListItem(
category: Category, category: Category,
canMoveUp: Boolean,
canMoveDown: Boolean,
onMoveUp: (Category) -> Unit,
onMoveDown: (Category) -> Unit,
onRename: () -> Unit, onRename: () -> Unit,
onDelete: () -> Unit, onDelete: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
ElevatedCard(modifier = modifier) { ElevatedCard(
modifier = modifier,
) {
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.clickable(onClick = onRename) .clickable { onRename() }
.padding(vertical = MaterialTheme.padding.small)
.padding( .padding(
start = MaterialTheme.padding.small, start = MaterialTheme.padding.medium,
top = MaterialTheme.padding.medium,
end = MaterialTheme.padding.medium, end = MaterialTheme.padding.medium,
), ),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
Icon( Icon(imageVector = Icons.AutoMirrored.Outlined.Label, contentDescription = null)
imageVector = Icons.Outlined.DragHandle,
contentDescription = null,
modifier = Modifier
.padding(MaterialTheme.padding.medium)
.draggableHandle(),
)
Text( Text(
text = category.name, 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) { IconButton(onClick = onRename) {
Icon( Icon(
imageVector = Icons.Outlined.Edit, imageVector = Icons.Outlined.Edit,
@@ -59,10 +77,7 @@ fun ReorderableCollectionItemScope.CategoryListItem(
) )
} }
IconButton(onClick = onDelete) { IconButton(onClick = onDelete) {
Icon( Icon(imageVector = Icons.Outlined.Delete, contentDescription = stringResource(MR.strings.action_delete))
imageVector = Icons.Outlined.Delete,
contentDescription = stringResource(MR.strings.action_delete),
)
} }
} }
} }

View File

@@ -1,5 +1,6 @@
package eu.kanade.presentation.components package eu.kanade.presentation.components
import androidx.activity.compose.BackHandler
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
@@ -27,8 +28,8 @@ fun NavigatorAdaptiveSheet(
screen = screen, screen = screen,
content = { sheetNavigator -> content = { sheetNavigator ->
AdaptiveSheet( AdaptiveSheet(
onDismissRequest = onDismissRequest,
enableSwipeDismiss = enableSwipeDismiss(sheetNavigator), enableSwipeDismiss = enableSwipeDismiss(sheetNavigator),
onDismissRequest = onDismissRequest,
) { ) {
ScreenTransition( ScreenTransition(
navigator = sheetNavigator, navigator = sheetNavigator,
@@ -37,6 +38,11 @@ fun NavigatorAdaptiveSheet(
fadeOut(animationSpec = tween(90)) fadeOut(animationSpec = tween(90))
}, },
) )
BackHandler(
enabled = sheetNavigator.size > 1,
onBack = sheetNavigator::pop,
)
} }
// Make sure screens are disposed no matter what // Make sure screens are disposed no matter what
@@ -73,10 +79,10 @@ fun AdaptiveSheet(
properties = dialogProperties, properties = dialogProperties,
) { ) {
AdaptiveSheetImpl( AdaptiveSheetImpl(
modifier = modifier,
isTabletUi = isTabletUi, isTabletUi = isTabletUi,
enableSwipeDismiss = enableSwipeDismiss, enableSwipeDismiss = enableSwipeDismiss,
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
modifier = modifier,
) { ) {
content() content()
} }

View File

@@ -36,7 +36,6 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier 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.focus.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
@@ -202,7 +201,6 @@ fun AppBarActions(
} }
}, },
state = rememberTooltipState(), state = rememberTooltipState(),
focusable = false,
) { ) {
IconButton( IconButton(
onClick = it.onClick, onClick = it.onClick,
@@ -227,7 +225,6 @@ fun AppBarActions(
} }
}, },
state = rememberTooltipState(), state = rememberTooltipState(),
focusable = false,
) { ) {
IconButton( IconButton(
onClick = { showMenu = !showMenu }, onClick = { showMenu = !showMenu },
@@ -292,7 +289,6 @@ fun SearchToolbar(
onSearch(searchQuery) onSearch(searchQuery)
focusManager.clearFocus() focusManager.clearFocus()
keyboardController?.hide() keyboardController?.hide()
focusManager.moveFocus(FocusDirection.Next)
} }
BasicTextField( BasicTextField(
@@ -356,7 +352,6 @@ fun SearchToolbar(
} }
}, },
state = rememberTooltipState(), state = rememberTooltipState(),
focusable = false,
) { ) {
IconButton( IconButton(
onClick = onClick, onClick = onClick,
@@ -376,7 +371,6 @@ fun SearchToolbar(
} }
}, },
state = rememberTooltipState(), state = rememberTooltipState(),
focusable = false,
) { ) {
IconButton( IconButton(
onClick = { onClick = {

View File

@@ -1,11 +1,9 @@
package eu.kanade.presentation.components package eu.kanade.presentation.components
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.DpOffset
import eu.kanade.presentation.manga.DownloadAction import eu.kanade.presentation.manga.DownloadAction
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
@@ -17,41 +15,7 @@ fun DownloadDropdownMenu(
expanded: Boolean, expanded: Boolean,
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
onDownloadClicked: (DownloadAction) -> Unit, onDownloadClicked: (DownloadAction) -> Unit,
offset: DpOffset? = null,
modifier: Modifier = Modifier, 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( val options = persistentListOf(
DownloadAction.NEXT_1_CHAPTER to pluralStringResource(MR.plurals.download_amount, 1, 1), 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), DownloadAction.UNREAD_CHAPTERS to stringResource(MR.strings.download_unread),
) )
options.map { (downloadAction, string) -> DropdownMenu(
DropdownMenuItem( expanded = expanded,
text = { Text(text = string) }, onDismissRequest = onDismissRequest,
onClick = { modifier = modifier,
onDownloadClicked(downloadAction) ) {
onDismissRequest() options.map { (downloadAction, string) ->
}, DropdownMenuItem(
) text = { Text(text = string) },
onClick = {
onDownloadClicked(downloadAction)
onDismissRequest()
},
)
}
} }
} }

View File

@@ -7,7 +7,6 @@ import androidx.compose.foundation.layout.calculateStartPadding
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.PrimaryTabRow import androidx.compose.material3.PrimaryTabRow
@@ -15,6 +14,7 @@ import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Tab import androidx.compose.material3.Tab
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@@ -33,13 +33,20 @@ import tachiyomi.presentation.core.i18n.stringResource
fun TabbedScreen( fun TabbedScreen(
titleRes: StringResource, titleRes: StringResource,
tabs: ImmutableList<TabContent>, tabs: ImmutableList<TabContent>,
state: PagerState = rememberPagerState { tabs.size }, startIndex: Int? = null,
searchQuery: String? = null, searchQuery: String? = null,
onChangeSearchQuery: (String?) -> Unit = {}, onChangeSearchQuery: (String?) -> Unit = {},
) { ) {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val state = rememberPagerState { tabs.size }
val snackbarHostState = remember { SnackbarHostState() } val snackbarHostState = remember { SnackbarHostState() }
LaunchedEffect(startIndex) {
if (startIndex != null) {
state.scrollToPage(startIndex)
}
}
Scaffold( Scaffold(
topBar = { topBar = {
val tab = tabs[state.currentPage] val tab = tabs[state.currentPage]

View File

@@ -38,7 +38,6 @@ fun HistoryScreen(
onSearchQueryChange: (String?) -> Unit, onSearchQueryChange: (String?) -> Unit,
onClickCover: (mangaId: Long) -> Unit, onClickCover: (mangaId: Long) -> Unit,
onClickResume: (mangaId: Long, chapterId: Long) -> Unit, onClickResume: (mangaId: Long, chapterId: Long) -> Unit,
onClickFavorite: (mangaId: Long) -> Unit,
onDialogChange: (HistoryScreenModel.Dialog?) -> Unit, onDialogChange: (HistoryScreenModel.Dialog?) -> Unit,
) { ) {
Scaffold( Scaffold(
@@ -85,7 +84,6 @@ fun HistoryScreen(
onClickCover = { history -> onClickCover(history.mangaId) }, onClickCover = { history -> onClickCover(history.mangaId) },
onClickResume = { history -> onClickResume(history.mangaId, history.chapterId) }, onClickResume = { history -> onClickResume(history.mangaId, history.chapterId) },
onClickDelete = { item -> onDialogChange(HistoryScreenModel.Dialog.Delete(item)) }, onClickDelete = { item -> onDialogChange(HistoryScreenModel.Dialog.Delete(item)) },
onClickFavorite = { history -> onClickFavorite(history.mangaId) },
) )
} }
} }
@@ -99,7 +97,6 @@ private fun HistoryScreenContent(
onClickCover: (HistoryWithRelations) -> Unit, onClickCover: (HistoryWithRelations) -> Unit,
onClickResume: (HistoryWithRelations) -> Unit, onClickResume: (HistoryWithRelations) -> Unit,
onClickDelete: (HistoryWithRelations) -> Unit, onClickDelete: (HistoryWithRelations) -> Unit,
onClickFavorite: (HistoryWithRelations) -> Unit,
) { ) {
FastScrollLazyColumn( FastScrollLazyColumn(
contentPadding = contentPadding, contentPadding = contentPadding,
@@ -129,7 +126,6 @@ private fun HistoryScreenContent(
onClickCover = { onClickCover(value) }, onClickCover = { onClickCover(value) },
onClickResume = { onClickResume(value) }, onClickResume = { onClickResume(value) },
onClickDelete = { onClickDelete(value) }, onClickDelete = { onClickDelete(value) },
onClickFavorite = { onClickFavorite(value) },
) )
} }
} }
@@ -156,7 +152,6 @@ internal fun HistoryScreenPreviews(
onClickCover = {}, onClickCover = {},
onClickResume = { _, _ -> run {} }, onClickResume = { _, _ -> run {} },
onDialogChange = {}, onDialogChange = {},
onClickFavorite = {},
) )
} }
} }

View File

@@ -8,7 +8,6 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Delete import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material.icons.outlined.FavoriteBorder
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@@ -40,7 +39,6 @@ fun HistoryItem(
onClickCover: () -> Unit, onClickCover: () -> Unit,
onClickResume: () -> Unit, onClickResume: () -> Unit,
onClickDelete: () -> Unit, onClickDelete: () -> Unit,
onClickFavorite: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
Row( 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) { IconButton(onClick = onClickDelete) {
Icon( Icon(
imageVector = Icons.Outlined.Delete, imageVector = Icons.Outlined.Delete,
@@ -117,7 +105,6 @@ private fun HistoryItemPreviews(
onClickCover = {}, onClickCover = {},
onClickResume = {}, onClickResume = {},
onClickDelete = {}, onClickDelete = {},
onClickFavorite = {},
) )
} }
} }

View File

@@ -9,7 +9,6 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Refresh import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material3.FilterChip import androidx.compose.material3.FilterChip
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState 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.TabbedDialog
import eu.kanade.presentation.components.TabbedDialogPaddings import eu.kanade.presentation.components.TabbedDialogPaddings
import eu.kanade.tachiyomi.ui.library.LibrarySettingsScreenModel 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 kotlinx.collections.immutable.persistentListOf
import tachiyomi.core.common.preference.TriState import tachiyomi.core.common.preference.TriState
import tachiyomi.domain.category.model.Category import tachiyomi.domain.category.model.Category
@@ -117,7 +117,10 @@ private fun ColumnScope.FilterPage(
onClick = { screenModel.toggleFilter(LibraryPreferences::filterCompleted) }, onClick = { screenModel.toggleFilter(LibraryPreferences::filterCompleted) },
) )
// TODO: re-enable when custom intervals are ready for stable // 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() val filterIntervalCustom by screenModel.libraryPreferences.filterIntervalCustom().collectAsState()
TriStateItem( TriStateItem(
label = stringResource(MR.strings.action_filter_interval_custom), label = stringResource(MR.strings.action_filter_interval_custom),
@@ -163,32 +166,32 @@ private fun ColumnScope.SortPage(
val sortingMode = category.sort.type val sortingMode = category.sort.type
val sortDescending = !category.sort.isAscending val sortDescending = !category.sort.isAscending
val options = remember(trackers.isEmpty()) { val trackerSortOption = if (trackers.isEmpty()) {
val trackerMeanPair = if (trackers.isNotEmpty()) { emptyList()
MR.strings.action_sort_tracker_score to LibrarySort.Type.TrackerMean } else {
} else { listOf(MR.strings.action_sort_tracker_score to LibrarySort.Type.TrackerMean)
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,
)
} }
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) { if (mode == LibrarySort.Type.Random) {
val enabledIcon = if (sortingMode == LibrarySort.Type.Random) {
Icons.Default.Refresh
} else {
null
}
BaseSortItem( BaseSortItem(
label = stringResource(titleRes), label = stringResource(titleRes),
icon = Icons.Default.Refresh icon = enabledIcon,
.takeIf { sortingMode == LibrarySort.Type.Random },
onClick = { onClick = {
screenModel.setSort(category, mode, LibrarySort.Direction.Ascending) screenModel.setSort(category, mode, LibrarySort.Direction.Ascending)
}, },
@@ -252,16 +255,15 @@ private fun ColumnScope.DisplayPage(
val columns by columnPreference.collectAsState() val columns by columnPreference.collectAsState()
SliderItem( SliderItem(
value = columns,
valueRange = 0..10,
label = stringResource(MR.strings.pref_library_columns), label = stringResource(MR.strings.pref_library_columns),
valueString = if (columns > 0) { max = 10,
columns.toString() value = columns,
valueText = if (columns > 0) {
stringResource(MR.strings.pref_library_columns_per_row, columns)
} else { } else {
stringResource(MR.strings.label_auto) stringResource(MR.strings.label_default)
}, },
onChange = columnPreference::set, onChange = columnPreference::set,
pillColor = MaterialTheme.colorScheme.surfaceContainerHighest,
) )
} }
@@ -270,10 +272,6 @@ private fun ColumnScope.DisplayPage(
label = stringResource(MR.strings.action_display_download_badge), label = stringResource(MR.strings.action_display_download_badge),
pref = screenModel.libraryPreferences.downloadBadge(), pref = screenModel.libraryPreferences.downloadBadge(),
) )
CheckboxItem(
label = stringResource(MR.strings.action_display_unread_badge),
pref = screenModel.libraryPreferences.unreadBadge(),
)
CheckboxItem( CheckboxItem(
label = stringResource(MR.strings.action_display_local_badge), label = stringResource(MR.strings.action_display_local_badge),
pref = screenModel.libraryPreferences.localBadge(), pref = screenModel.libraryPreferences.localBadge(),

View File

@@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.lazy.grid.items
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.util.fastAny
import eu.kanade.tachiyomi.ui.library.LibraryItem import eu.kanade.tachiyomi.ui.library.LibraryItem
import tachiyomi.domain.library.model.LibraryManga import tachiyomi.domain.library.model.LibraryManga
import tachiyomi.domain.manga.model.MangaCover import tachiyomi.domain.manga.model.MangaCover
@@ -14,7 +15,7 @@ internal fun LibraryComfortableGrid(
items: List<LibraryItem>, items: List<LibraryItem>,
columns: Int, columns: Int,
contentPadding: PaddingValues, contentPadding: PaddingValues,
selection: Set<Long>, selection: List<LibraryManga>,
onClick: (LibraryManga) -> Unit, onClick: (LibraryManga) -> Unit,
onLongClick: (LibraryManga) -> Unit, onLongClick: (LibraryManga) -> Unit,
onClickContinueReading: ((LibraryManga) -> Unit)?, onClickContinueReading: ((LibraryManga) -> Unit)?,
@@ -34,7 +35,7 @@ internal fun LibraryComfortableGrid(
) { libraryItem -> ) { libraryItem ->
val manga = libraryItem.libraryManga.manga val manga = libraryItem.libraryManga.manga
MangaComfortableGridItem( MangaComfortableGridItem(
isSelected = manga.id in selection, isSelected = selection.fastAny { it.id == libraryItem.libraryManga.id },
title = manga.title, title = manga.title,
coverData = MangaCover( coverData = MangaCover(
mangaId = manga.id, mangaId = manga.id,

View File

@@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.lazy.grid.items
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.util.fastAny
import eu.kanade.tachiyomi.ui.library.LibraryItem import eu.kanade.tachiyomi.ui.library.LibraryItem
import tachiyomi.domain.library.model.LibraryManga import tachiyomi.domain.library.model.LibraryManga
import tachiyomi.domain.manga.model.MangaCover import tachiyomi.domain.manga.model.MangaCover
@@ -15,7 +16,7 @@ internal fun LibraryCompactGrid(
showTitle: Boolean, showTitle: Boolean,
columns: Int, columns: Int,
contentPadding: PaddingValues, contentPadding: PaddingValues,
selection: Set<Long>, selection: List<LibraryManga>,
onClick: (LibraryManga) -> Unit, onClick: (LibraryManga) -> Unit,
onLongClick: (LibraryManga) -> Unit, onLongClick: (LibraryManga) -> Unit,
onClickContinueReading: ((LibraryManga) -> Unit)?, onClickContinueReading: ((LibraryManga) -> Unit)?,
@@ -35,7 +36,7 @@ internal fun LibraryCompactGrid(
) { libraryItem -> ) { libraryItem ->
val manga = libraryItem.libraryManga.manga val manga = libraryItem.libraryManga.manga
MangaCompactGridItem( MangaCompactGridItem(
isSelected = manga.id in selection, isSelected = selection.fastAny { it.id == libraryItem.libraryManga.id },
title = manga.title.takeIf { showTitle }, title = manga.title.takeIf { showTitle },
coverData = MangaCover( coverData = MangaCover(
mangaId = manga.id, mangaId = manga.id,

View File

@@ -29,22 +29,22 @@ import kotlin.time.Duration.Companion.seconds
fun LibraryContent( fun LibraryContent(
categories: List<Category>, categories: List<Category>,
searchQuery: String?, searchQuery: String?,
selection: Set<Long>, selection: List<LibraryManga>,
contentPadding: PaddingValues, contentPadding: PaddingValues,
currentPage: Int, currentPage: () -> Int,
hasActiveFilters: Boolean, hasActiveFilters: Boolean,
showPageTabs: Boolean, showPageTabs: Boolean,
onChangeCurrentPage: (Int) -> Unit, onChangeCurrentPage: (Int) -> Unit,
onClickManga: (Long) -> Unit, onMangaClicked: (Long) -> Unit,
onContinueReadingClicked: ((LibraryManga) -> Unit)?, onContinueReadingClicked: ((LibraryManga) -> Unit)?,
onToggleSelection: (Category, LibraryManga) -> Unit, onToggleSelection: (LibraryManga) -> Unit,
onToggleRangeSelection: (Category, LibraryManga) -> Unit, onToggleRangeSelection: (LibraryManga) -> Unit,
onRefresh: () -> Boolean, onRefresh: (Category?) -> Boolean,
onGlobalSearchClicked: () -> Unit, onGlobalSearchClicked: () -> Unit,
getItemCountForCategory: (Category) -> Int?, getNumberOfMangaForCategory: (Category) -> Int?,
getDisplayMode: (Int) -> PreferenceMutableState<LibraryDisplayMode>, getDisplayMode: (Int) -> PreferenceMutableState<LibraryDisplayMode>,
getColumnsForOrientation: (Boolean) -> PreferenceMutableState<Int>, getColumnsForOrientation: (Boolean) -> PreferenceMutableState<Int>,
getItemsForCategory: (Category) -> List<LibraryItem>, getLibraryForPage: (Int) -> List<LibraryItem>,
) { ) {
Column( Column(
modifier = Modifier.padding( modifier = Modifier.padding(
@@ -53,12 +53,13 @@ fun LibraryContent(
end = contentPadding.calculateEndPadding(LocalLayoutDirection.current), 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() val scope = rememberCoroutineScope()
var isRefreshing by remember(pagerState.currentPage) { mutableStateOf(false) } 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) { LaunchedEffect(categories) {
if (categories.size <= pagerState.currentPage) { if (categories.size <= pagerState.currentPage) {
pagerState.scrollToPage(categories.size - 1) pagerState.scrollToPage(categories.size - 1)
@@ -67,20 +68,23 @@ fun LibraryContent(
LibraryTabs( LibraryTabs(
categories = categories, categories = categories,
pagerState = pagerState, pagerState = pagerState,
getItemCountForCategory = getItemCountForCategory, getNumberOfMangaForCategory = getNumberOfMangaForCategory,
onTabItemClick = { ) { scope.launch { pagerState.animateScrollToPage(it) } }
scope.launch { }
pagerState.animateScrollToPage(it)
} val notSelectionMode = selection.isEmpty()
}, val onClickManga = { manga: LibraryManga ->
) if (notSelectionMode) {
onMangaClicked(manga.manga.id)
} else {
onToggleSelection(manga)
}
} }
PullRefresh( PullRefresh(
refreshing = isRefreshing, refreshing = isRefreshing,
enabled = selection.isEmpty(),
onRefresh = { onRefresh = {
val started = onRefresh() val started = onRefresh(categories[currentPage()])
if (!started) return@PullRefresh if (!started) return@PullRefresh
scope.launch { scope.launch {
// Fake refresh status but hide it after a second as it's a long running task // Fake refresh status but hide it after a second as it's a long running task
@@ -89,25 +93,19 @@ fun LibraryContent(
isRefreshing = false isRefreshing = false
} }
}, },
enabled = notSelectionMode,
) { ) {
LibraryPager( LibraryPager(
state = pagerState, state = pagerState,
contentPadding = PaddingValues(bottom = contentPadding.calculateBottomPadding()), contentPadding = PaddingValues(bottom = contentPadding.calculateBottomPadding()),
hasActiveFilters = hasActiveFilters, hasActiveFilters = hasActiveFilters,
selection = selection, selectedManga = selection,
searchQuery = searchQuery, searchQuery = searchQuery,
onGlobalSearchClicked = onGlobalSearchClicked, onGlobalSearchClicked = onGlobalSearchClicked,
getCategoryForPage = { page -> categories[page] },
getDisplayMode = getDisplayMode, getDisplayMode = getDisplayMode,
getColumnsForOrientation = getColumnsForOrientation, getColumnsForOrientation = getColumnsForOrientation,
getItemsForCategory = getItemsForCategory, getLibraryForPage = getLibraryForPage,
onClickManga = { category, manga -> onClickManga = onClickManga,
if (selection.isNotEmpty()) {
onToggleSelection(category, manga)
} else {
onClickManga(manga.manga.id)
}
},
onLongClickManga = onToggleRangeSelection, onLongClickManga = onToggleRangeSelection,
onClickContinueReading = onContinueReadingClicked, onClickContinueReading = onContinueReadingClicked,
) )

View File

@@ -7,6 +7,7 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastAny
import eu.kanade.tachiyomi.ui.library.LibraryItem import eu.kanade.tachiyomi.ui.library.LibraryItem
import tachiyomi.domain.library.model.LibraryManga import tachiyomi.domain.library.model.LibraryManga
import tachiyomi.domain.manga.model.MangaCover import tachiyomi.domain.manga.model.MangaCover
@@ -17,7 +18,7 @@ import tachiyomi.presentation.core.util.plus
internal fun LibraryList( internal fun LibraryList(
items: List<LibraryItem>, items: List<LibraryItem>,
contentPadding: PaddingValues, contentPadding: PaddingValues,
selection: Set<Long>, selection: List<LibraryManga>,
onClick: (LibraryManga) -> Unit, onClick: (LibraryManga) -> Unit,
onLongClick: (LibraryManga) -> Unit, onLongClick: (LibraryManga) -> Unit,
onClickContinueReading: ((LibraryManga) -> Unit)?, onClickContinueReading: ((LibraryManga) -> Unit)?,
@@ -44,7 +45,7 @@ internal fun LibraryList(
) { libraryItem -> ) { libraryItem ->
val manga = libraryItem.libraryManga.manga val manga = libraryItem.libraryManga.manga
MangaListItem( MangaListItem(
isSelected = manga.id in selection, isSelected = selection.fastAny { it.id == libraryItem.libraryManga.id },
title = manga.title, title = manga.title,
coverData = MangaCover( coverData = MangaCover(
mangaId = manga.id, mangaId = manga.id,

View File

@@ -20,7 +20,6 @@ import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import eu.kanade.core.preference.PreferenceMutableState import eu.kanade.core.preference.PreferenceMutableState
import eu.kanade.tachiyomi.ui.library.LibraryItem import eu.kanade.tachiyomi.ui.library.LibraryItem
import tachiyomi.domain.category.model.Category
import tachiyomi.domain.library.model.LibraryDisplayMode import tachiyomi.domain.library.model.LibraryDisplayMode
import tachiyomi.domain.library.model.LibraryManga import tachiyomi.domain.library.model.LibraryManga
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
@@ -32,15 +31,14 @@ fun LibraryPager(
state: PagerState, state: PagerState,
contentPadding: PaddingValues, contentPadding: PaddingValues,
hasActiveFilters: Boolean, hasActiveFilters: Boolean,
selection: Set<Long>, selectedManga: List<LibraryManga>,
searchQuery: String?, searchQuery: String?,
onGlobalSearchClicked: () -> Unit, onGlobalSearchClicked: () -> Unit,
getCategoryForPage: (Int) -> Category,
getDisplayMode: (Int) -> PreferenceMutableState<LibraryDisplayMode>, getDisplayMode: (Int) -> PreferenceMutableState<LibraryDisplayMode>,
getColumnsForOrientation: (Boolean) -> PreferenceMutableState<Int>, getColumnsForOrientation: (Boolean) -> PreferenceMutableState<Int>,
getItemsForCategory: (Category) -> List<LibraryItem>, getLibraryForPage: (Int) -> List<LibraryItem>,
onClickManga: (Category, LibraryManga) -> Unit, onClickManga: (LibraryManga) -> Unit,
onLongClickManga: (Category, LibraryManga) -> Unit, onLongClickManga: (LibraryManga) -> Unit,
onClickContinueReading: ((LibraryManga) -> Unit)?, onClickContinueReading: ((LibraryManga) -> Unit)?,
) { ) {
HorizontalPager( HorizontalPager(
@@ -52,10 +50,9 @@ fun LibraryPager(
// To make sure only one offscreen page is being composed // To make sure only one offscreen page is being composed
return@HorizontalPager return@HorizontalPager
} }
val category = getCategoryForPage(page) val library = getLibraryForPage(page)
val items = getItemsForCategory(category)
if (items.isEmpty()) { if (library.isEmpty()) {
LibraryPagerEmptyScreen( LibraryPagerEmptyScreen(
searchQuery = searchQuery, searchQuery = searchQuery,
hasActiveFilters = hasActiveFilters, hasActiveFilters = hasActiveFilters,
@@ -75,15 +72,12 @@ fun LibraryPager(
remember { mutableIntStateOf(0) } remember { mutableIntStateOf(0) }
} }
val onClickManga: (LibraryManga) -> Unit = { onClickManga(category, it) }
val onLongClickManga: (LibraryManga) -> Unit = { onLongClickManga(category, it) }
when (displayMode) { when (displayMode) {
LibraryDisplayMode.List -> { LibraryDisplayMode.List -> {
LibraryList( LibraryList(
items = items, items = library,
contentPadding = contentPadding, contentPadding = contentPadding,
selection = selection, selection = selectedManga,
onClick = onClickManga, onClick = onClickManga,
onLongClick = onLongClickManga, onLongClick = onLongClickManga,
onClickContinueReading = onClickContinueReading, onClickContinueReading = onClickContinueReading,
@@ -93,11 +87,11 @@ fun LibraryPager(
} }
LibraryDisplayMode.CompactGrid, LibraryDisplayMode.CoverOnlyGrid -> { LibraryDisplayMode.CompactGrid, LibraryDisplayMode.CoverOnlyGrid -> {
LibraryCompactGrid( LibraryCompactGrid(
items = items, items = library,
showTitle = displayMode is LibraryDisplayMode.CompactGrid, showTitle = displayMode is LibraryDisplayMode.CompactGrid,
columns = columns, columns = columns,
contentPadding = contentPadding, contentPadding = contentPadding,
selection = selection, selection = selectedManga,
onClick = onClickManga, onClick = onClickManga,
onLongClick = onLongClickManga, onLongClick = onLongClickManga,
onClickContinueReading = onClickContinueReading, onClickContinueReading = onClickContinueReading,
@@ -107,10 +101,10 @@ fun LibraryPager(
} }
LibraryDisplayMode.ComfortableGrid -> { LibraryDisplayMode.ComfortableGrid -> {
LibraryComfortableGrid( LibraryComfortableGrid(
items = items, items = library,
columns = columns, columns = columns,
contentPadding = contentPadding, contentPadding = contentPadding,
selection = selection, selection = selectedManga,
onClick = onClickManga, onClick = onClickManga,
onLongClick = onLongClickManga, onLongClick = onLongClickManga,
onClickContinueReading = onClickContinueReading, onClickContinueReading = onClickContinueReading,

View File

@@ -18,13 +18,14 @@ import tachiyomi.presentation.core.components.material.TabText
internal fun LibraryTabs( internal fun LibraryTabs(
categories: List<Category>, categories: List<Category>,
pagerState: PagerState, pagerState: PagerState,
getItemCountForCategory: (Category) -> Int?, getNumberOfMangaForCategory: (Category) -> Int?,
onTabItemClick: (Int) -> Unit, onTabItemClick: (Int) -> Unit,
) { ) {
val currentPageIndex = pagerState.currentPage.coerceAtMost(categories.lastIndex) Column(
Column(modifier = Modifier.zIndex(2f)) { modifier = Modifier.zIndex(1f),
) {
PrimaryScrollableTabRow( PrimaryScrollableTabRow(
selectedTabIndex = currentPageIndex, selectedTabIndex = pagerState.currentPage,
edgePadding = 0.dp, edgePadding = 0.dp,
// TODO: use default when width is fixed upstream // TODO: use default when width is fixed upstream
// https://issuetracker.google.com/issues/242879624 // https://issuetracker.google.com/issues/242879624
@@ -32,12 +33,12 @@ internal fun LibraryTabs(
) { ) {
categories.forEachIndexed { index, category -> categories.forEachIndexed { index, category ->
Tab( Tab(
selected = currentPageIndex == index, selected = pagerState.currentPage == index,
onClick = { onTabItemClick(index) }, onClick = { onTabItemClick(index) },
text = { text = {
TabText( TabText(
text = category.visualName, text = category.visualName,
badgeCount = getItemCountForCategory(category), badgeCount = getNumberOfMangaForCategory(category),
) )
}, },
unselectedContentColor = MaterialTheme.colorScheme.onSurface, unselectedContentColor = MaterialTheme.colorScheme.onSurface,

View File

@@ -21,14 +21,13 @@ import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp 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.downloadedFilter
import eu.kanade.domain.manga.model.forceDownloaded
import eu.kanade.presentation.components.TabbedDialog import eu.kanade.presentation.components.TabbedDialog
import eu.kanade.presentation.components.TabbedDialogPaddings import eu.kanade.presentation.components.TabbedDialogPaddings
import kotlinx.collections.immutable.persistentListOf 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.components.TriStateItem
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.theme.active import tachiyomi.presentation.core.theme.active
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@Composable @Composable
fun ChapterSettingsDialog( fun ChapterSettingsDialog(
@@ -66,8 +63,6 @@ fun ChapterSettingsDialog(
) )
} }
val downloadedOnly = remember { Injekt.get<BasePreferences>().downloadedOnly().get() }
TabbedDialog( TabbedDialog(
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
tabTitles = persistentListOf( tabTitles = persistentListOf(
@@ -102,7 +97,7 @@ fun ChapterSettingsDialog(
FilterPage( FilterPage(
downloadFilter = manga?.downloadedFilter ?: TriState.DISABLED, downloadFilter = manga?.downloadedFilter ?: TriState.DISABLED,
onDownloadFilterChanged = onDownloadFilterChanged onDownloadFilterChanged = onDownloadFilterChanged
.takeUnless { downloadedOnly }, .takeUnless { manga?.forceDownloaded() == true },
unreadFilter = manga?.unreadFilter ?: TriState.DISABLED, unreadFilter = manga?.unreadFilter ?: TriState.DISABLED,
onUnreadFilterChanged = onUnreadFilterChanged, onUnreadFilterChanged = onUnreadFilterChanged,
bookmarkedFilter = manga?.bookmarkedFilter ?: TriState.DISABLED, bookmarkedFilter = manga?.bookmarkedFilter ?: TriState.DISABLED,

View File

@@ -1,95 +1,44 @@
package eu.kanade.presentation.manga package eu.kanade.presentation.manga
import androidx.compose.foundation.background import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.sizeIn
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.material.icons.Icons 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.Add
import androidx.compose.material.icons.outlined.AttachMoney import androidx.compose.material.icons.outlined.Book
import androidx.compose.material.icons.outlined.Block import androidx.compose.material.icons.outlined.SwapVert
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.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.Typography
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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.unit.dp
import androidx.compose.ui.util.fastMaxOfOrNull import androidx.compose.ui.unit.sp
import coil3.request.ImageRequest
import coil3.request.crossfade
import eu.kanade.presentation.components.AdaptiveSheet import eu.kanade.presentation.components.AdaptiveSheet
import eu.kanade.presentation.components.TabbedDialogPaddings 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.LocalPreferenceMinHeight
import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget 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.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.i18n.stringResource
import tachiyomi.presentation.core.util.secondaryItemAlpha
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@Composable @Composable
fun DuplicateMangaDialog( fun DuplicateMangaDialog(
duplicates: List<MangaWithChapterCount>,
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
onConfirm: () -> Unit, onConfirm: () -> Unit,
onOpenManga: (manga: Manga) -> Unit, onOpenManga: () -> Unit,
onMigrate: (manga: Manga) -> Unit, onMigrate: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val sourceManager = remember { Injekt.get<SourceManager>() }
val minHeight = LocalPreferenceMinHeight.current val minHeight = LocalPreferenceMinHeight.current
val horizontalPadding = PaddingValues(horizontal = TabbedDialogPaddings.Horizontal)
val horizontalPaddingModifier = Modifier.padding(horizontalPadding)
AdaptiveSheet( AdaptiveSheet(
modifier = modifier, modifier = modifier,
@@ -97,310 +46,81 @@ fun DuplicateMangaDialog(
) { ) {
Column( Column(
modifier = Modifier modifier = Modifier
.padding(vertical = TabbedDialogPaddings.Vertical) .padding(
.verticalScroll(rememberScrollState()) vertical = TabbedDialogPaddings.Vertical,
horizontal = TabbedDialogPaddings.Horizontal,
)
.fillMaxWidth(), .fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.medium),
) { ) {
Text( Text(
text = stringResource(MR.strings.possible_duplicates_title), modifier = Modifier.padding(TitlePadding),
text = stringResource(MR.strings.are_you_sure),
style = MaterialTheme.typography.headlineMedium, style = MaterialTheme.typography.headlineMedium,
modifier = Modifier
.then(horizontalPaddingModifier)
.padding(top = MaterialTheme.padding.small),
) )
Text( Text(
text = stringResource(MR.strings.possible_duplicates_summary), text = stringResource(MR.strings.confirm_add_duplicate_manga),
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.then(horizontalPaddingModifier),
) )
LazyRow( Spacer(Modifier.height(PaddingSize))
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) },
)
}
}
Column(modifier = horizontalPaddingModifier) { TextPreferenceWidget(
HorizontalDivider() title = stringResource(MR.strings.action_show_manga),
icon = Icons.Outlined.Book,
onPreferenceClick = {
onDismissRequest()
onOpenManga()
},
)
TextPreferenceWidget( HorizontalDivider()
title = stringResource(MR.strings.action_add_anyway),
icon = Icons.Outlined.Add,
onPreferenceClick = {
onDismissRequest()
onConfirm()
},
modifier = Modifier.clip(CircleShape),
)
}
OutlinedButton( TextPreferenceWidget(
onClick = onDismissRequest, title = stringResource(MR.strings.action_migrate_duplicate),
modifier = Modifier icon = Icons.Outlined.SwapVert,
.then(horizontalPaddingModifier) onPreferenceClick = {
.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 = {
onDismissRequest() onDismissRequest()
onMigrate() onMigrate()
}, },
) )
.padding(MaterialTheme.padding.small),
) { HorizontalDivider()
Box {
MangaCover.Book( TextPreferenceWidget(
data = ImageRequest.Builder(LocalContext.current) title = stringResource(MR.strings.action_add_anyway),
.data(manga) icon = Icons.Outlined.Add,
.crossfade(true) onPreferenceClick = {
.build(), onDismissRequest()
modifier = Modifier.fillMaxWidth(), onConfirm()
},
) )
BadgeGroup(
Row(
modifier = Modifier modifier = Modifier
.padding(4.dp) .sizeIn(minHeight = minHeight)
.align(Alignment.TopStart), .clickable { onDismissRequest.invoke() }
.padding(ButtonPadding)
.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
) { ) {
Badge( OutlinedButton(onClick = onDismissRequest, modifier = Modifier.fillMaxWidth()) {
color = MaterialTheme.colorScheme.secondary, Text(
textColor = MaterialTheme.colorScheme.onSecondary, modifier = Modifier
text = pluralStringResource( .padding(vertical = 8.dp),
MR.plurals.manga_num_chapters, text = stringResource(MR.strings.action_cancel),
duplicate.chapterCount.toInt(), color = MaterialTheme.colorScheme.primary,
duplicate.chapterCount, 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 val PaddingSize = 16.dp
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,
)
}
}
@Composable private val ButtonPadding = PaddingValues(top = 16.dp, bottom = 16.dp)
private fun getMaximumMangaCardHeight(duplicates: List<MangaWithChapterCount>): Dp { private val TitlePadding = PaddingValues(bottom = 16.dp, top = 8.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

View File

@@ -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(),
)
}
}

View File

@@ -87,7 +87,7 @@ fun MangaScreen(
isTabletUi: Boolean, isTabletUi: Boolean,
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction, chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction, chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
navigateUp: () -> Unit, onBackClicked: () -> Unit,
onChapterClicked: (Chapter) -> Unit, onChapterClicked: (Chapter) -> Unit,
onDownloadChapter: ((List<ChapterList.Item>, ChapterDownloadAction) -> Unit)?, onDownloadChapter: ((List<ChapterList.Item>, ChapterDownloadAction) -> Unit)?,
onAddToLibraryClicked: () -> Unit, onAddToLibraryClicked: () -> Unit,
@@ -112,7 +112,6 @@ fun MangaScreen(
onEditCategoryClicked: (() -> Unit)?, onEditCategoryClicked: (() -> Unit)?,
onEditFetchIntervalClicked: (() -> Unit)?, onEditFetchIntervalClicked: (() -> Unit)?,
onMigrateClicked: (() -> Unit)?, onMigrateClicked: (() -> Unit)?,
onEditNotesClicked: () -> Unit,
// For bottom action menu // For bottom action menu
onMultiBookmarkClicked: (List<Chapter>, bookmarked: Boolean) -> Unit, onMultiBookmarkClicked: (List<Chapter>, bookmarked: Boolean) -> Unit,
@@ -142,7 +141,7 @@ fun MangaScreen(
nextUpdate = nextUpdate, nextUpdate = nextUpdate,
chapterSwipeStartAction = chapterSwipeStartAction, chapterSwipeStartAction = chapterSwipeStartAction,
chapterSwipeEndAction = chapterSwipeEndAction, chapterSwipeEndAction = chapterSwipeEndAction,
navigateUp = navigateUp, onBackClicked = onBackClicked,
onChapterClicked = onChapterClicked, onChapterClicked = onChapterClicked,
onDownloadChapter = onDownloadChapter, onDownloadChapter = onDownloadChapter,
onAddToLibraryClicked = onAddToLibraryClicked, onAddToLibraryClicked = onAddToLibraryClicked,
@@ -161,7 +160,6 @@ fun MangaScreen(
onEditCategoryClicked = onEditCategoryClicked, onEditCategoryClicked = onEditCategoryClicked,
onEditIntervalClicked = onEditFetchIntervalClicked, onEditIntervalClicked = onEditFetchIntervalClicked,
onMigrateClicked = onMigrateClicked, onMigrateClicked = onMigrateClicked,
onEditNotesClicked = onEditNotesClicked,
onMultiBookmarkClicked = onMultiBookmarkClicked, onMultiBookmarkClicked = onMultiBookmarkClicked,
onMultiMarkAsReadClicked = onMultiMarkAsReadClicked, onMultiMarkAsReadClicked = onMultiMarkAsReadClicked,
onMarkPreviousAsReadClicked = onMarkPreviousAsReadClicked, onMarkPreviousAsReadClicked = onMarkPreviousAsReadClicked,
@@ -178,7 +176,7 @@ fun MangaScreen(
chapterSwipeStartAction = chapterSwipeStartAction, chapterSwipeStartAction = chapterSwipeStartAction,
chapterSwipeEndAction = chapterSwipeEndAction, chapterSwipeEndAction = chapterSwipeEndAction,
nextUpdate = nextUpdate, nextUpdate = nextUpdate,
navigateUp = navigateUp, onBackClicked = onBackClicked,
onChapterClicked = onChapterClicked, onChapterClicked = onChapterClicked,
onDownloadChapter = onDownloadChapter, onDownloadChapter = onDownloadChapter,
onAddToLibraryClicked = onAddToLibraryClicked, onAddToLibraryClicked = onAddToLibraryClicked,
@@ -197,7 +195,6 @@ fun MangaScreen(
onEditCategoryClicked = onEditCategoryClicked, onEditCategoryClicked = onEditCategoryClicked,
onEditIntervalClicked = onEditFetchIntervalClicked, onEditIntervalClicked = onEditFetchIntervalClicked,
onMigrateClicked = onMigrateClicked, onMigrateClicked = onMigrateClicked,
onEditNotesClicked = onEditNotesClicked,
onMultiBookmarkClicked = onMultiBookmarkClicked, onMultiBookmarkClicked = onMultiBookmarkClicked,
onMultiMarkAsReadClicked = onMultiMarkAsReadClicked, onMultiMarkAsReadClicked = onMultiMarkAsReadClicked,
onMarkPreviousAsReadClicked = onMarkPreviousAsReadClicked, onMarkPreviousAsReadClicked = onMarkPreviousAsReadClicked,
@@ -217,7 +214,7 @@ private fun MangaScreenSmallImpl(
nextUpdate: Instant?, nextUpdate: Instant?,
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction, chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction, chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
navigateUp: () -> Unit, onBackClicked: () -> Unit,
onChapterClicked: (Chapter) -> Unit, onChapterClicked: (Chapter) -> Unit,
onDownloadChapter: ((List<ChapterList.Item>, ChapterDownloadAction) -> Unit)?, onDownloadChapter: ((List<ChapterList.Item>, ChapterDownloadAction) -> Unit)?,
onAddToLibraryClicked: () -> Unit, onAddToLibraryClicked: () -> Unit,
@@ -243,7 +240,6 @@ private fun MangaScreenSmallImpl(
onEditCategoryClicked: (() -> Unit)?, onEditCategoryClicked: (() -> Unit)?,
onEditIntervalClicked: (() -> Unit)?, onEditIntervalClicked: (() -> Unit)?,
onMigrateClicked: (() -> Unit)?, onMigrateClicked: (() -> Unit)?,
onEditNotesClicked: () -> Unit,
// For bottom action menu // For bottom action menu
onMultiBookmarkClicked: (List<Chapter>, bookmarked: Boolean) -> Unit, onMultiBookmarkClicked: (List<Chapter>, bookmarked: Boolean) -> Unit,
@@ -269,9 +265,14 @@ private fun MangaScreenSmallImpl(
) )
} }
BackHandler(enabled = isAnySelected) { val internalOnBackPressed = {
onAllChapterSelected(false) if (isAnySelected) {
onAllChapterSelected(false)
} else {
onBackClicked()
}
} }
BackHandler(onBack = internalOnBackPressed)
Scaffold( Scaffold(
topBar = { topBar = {
@@ -284,31 +285,29 @@ private fun MangaScreenSmallImpl(
val isFirstItemScrolled by remember { val isFirstItemScrolled by remember {
derivedStateOf { chapterListState.firstVisibleItemScrollOffset > 0 } derivedStateOf { chapterListState.firstVisibleItemScrollOffset > 0 }
} }
val titleAlpha by animateFloatAsState( val animatedTitleAlpha by animateFloatAsState(
if (!isFirstItemVisible) 1f else 0f, if (!isFirstItemVisible) 1f else 0f,
label = "Top Bar Title", label = "Top Bar Title",
) )
val backgroundAlpha by animateFloatAsState( val animatedBgAlpha by animateFloatAsState(
if (!isFirstItemVisible || isFirstItemScrolled) 1f else 0f, if (!isFirstItemVisible || isFirstItemScrolled) 1f else 0f,
label = "Top Bar Background", label = "Top Bar Background",
) )
MangaToolbar( MangaToolbar(
title = state.manga.title, title = state.manga.title,
titleAlphaProvider = { animatedTitleAlpha },
backgroundAlphaProvider = { animatedBgAlpha },
hasFilters = state.filterActive, hasFilters = state.filterActive,
navigateUp = navigateUp, onBackClicked = internalOnBackPressed,
onClickFilter = onFilterClicked, onClickFilter = onFilterClicked,
onClickShare = onShareClicked, onClickShare = onShareClicked,
onClickDownload = onDownloadActionClicked, onClickDownload = onDownloadActionClicked,
onClickEditCategory = onEditCategoryClicked, onClickEditCategory = onEditCategoryClicked,
onClickRefresh = onRefresh, onClickRefresh = onRefresh,
onClickMigrate = onMigrateClicked, onClickMigrate = onMigrateClicked,
onClickEditNotes = onEditNotesClicked,
actionModeCounter = selectedChapterCount, actionModeCounter = selectedChapterCount,
onCancelActionMode = { onAllChapterSelected(false) },
onSelectAll = { onAllChapterSelected(true) }, onSelectAll = { onAllChapterSelected(true) },
onInvertSelection = { onInvertSelection() }, onInvertSelection = { onInvertSelection() },
titleAlphaProvider = { titleAlpha },
backgroundAlphaProvider = { backgroundAlpha },
) )
}, },
bottomBar = { bottomBar = {
@@ -415,10 +414,8 @@ private fun MangaScreenSmallImpl(
defaultExpandState = state.isFromSource, defaultExpandState = state.isFromSource,
description = state.manga.description, description = state.manga.description,
tagsProvider = { state.manga.genre }, tagsProvider = { state.manga.genre },
notes = state.manga.notes,
onTagSearch = onTagSearch, onTagSearch = onTagSearch,
onCopyTagToClipboard = onCopyTagToClipboard, onCopyTagToClipboard = onCopyTagToClipboard,
onEditNotes = onEditNotesClicked,
) )
} }
@@ -461,7 +458,7 @@ fun MangaScreenLargeImpl(
nextUpdate: Instant?, nextUpdate: Instant?,
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction, chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction, chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
navigateUp: () -> Unit, onBackClicked: () -> Unit,
onChapterClicked: (Chapter) -> Unit, onChapterClicked: (Chapter) -> Unit,
onDownloadChapter: ((List<ChapterList.Item>, ChapterDownloadAction) -> Unit)?, onDownloadChapter: ((List<ChapterList.Item>, ChapterDownloadAction) -> Unit)?,
onAddToLibraryClicked: () -> Unit, onAddToLibraryClicked: () -> Unit,
@@ -487,7 +484,6 @@ fun MangaScreenLargeImpl(
onEditCategoryClicked: (() -> Unit)?, onEditCategoryClicked: (() -> Unit)?,
onEditIntervalClicked: (() -> Unit)?, onEditIntervalClicked: (() -> Unit)?,
onMigrateClicked: (() -> Unit)?, onMigrateClicked: (() -> Unit)?,
onEditNotesClicked: () -> Unit,
// For bottom action menu // For bottom action menu
onMultiBookmarkClicked: (List<Chapter>, bookmarked: Boolean) -> Unit, onMultiBookmarkClicked: (List<Chapter>, bookmarked: Boolean) -> Unit,
@@ -519,9 +515,14 @@ fun MangaScreenLargeImpl(
val chapterListState = rememberLazyListState() val chapterListState = rememberLazyListState()
BackHandler(enabled = isAnySelected) { val internalOnBackPressed = {
onAllChapterSelected(false) if (isAnySelected) {
onAllChapterSelected(false)
} else {
onBackClicked()
}
} }
BackHandler(onBack = internalOnBackPressed)
Scaffold( Scaffold(
topBar = { topBar = {
@@ -531,21 +532,19 @@ fun MangaScreenLargeImpl(
MangaToolbar( MangaToolbar(
modifier = Modifier.onSizeChanged { topBarHeight = it.height }, modifier = Modifier.onSizeChanged { topBarHeight = it.height },
title = state.manga.title, title = state.manga.title,
titleAlphaProvider = { if (isAnySelected) 1f else 0f },
backgroundAlphaProvider = { 1f },
hasFilters = state.filterActive, hasFilters = state.filterActive,
navigateUp = navigateUp, onBackClicked = internalOnBackPressed,
onClickFilter = onFilterButtonClicked, onClickFilter = onFilterButtonClicked,
onClickShare = onShareClicked, onClickShare = onShareClicked,
onClickDownload = onDownloadActionClicked, onClickDownload = onDownloadActionClicked,
onClickEditCategory = onEditCategoryClicked, onClickEditCategory = onEditCategoryClicked,
onClickRefresh = onRefresh, onClickRefresh = onRefresh,
onClickMigrate = onMigrateClicked, onClickMigrate = onMigrateClicked,
onClickEditNotes = onEditNotesClicked,
onCancelActionMode = { onAllChapterSelected(false) },
actionModeCounter = selectedChapterCount, actionModeCounter = selectedChapterCount,
onSelectAll = { onAllChapterSelected(true) }, onSelectAll = { onAllChapterSelected(true) },
onInvertSelection = { onInvertSelection() }, onInvertSelection = { onInvertSelection() },
titleAlphaProvider = { 1f },
backgroundAlphaProvider = { 1f },
) )
}, },
bottomBar = { bottomBar = {
@@ -641,10 +640,8 @@ fun MangaScreenLargeImpl(
defaultExpandState = true, defaultExpandState = true,
description = state.manga.description, description = state.manga.description,
tagsProvider = { state.manga.genre }, tagsProvider = { state.manga.genre },
notes = state.manga.notes,
onTagSearch = onTagSearch, onTagSearch = onTagSearch,
onCopyTagToClipboard = onCopyTagToClipboard, onCopyTagToClipboard = onCopyTagToClipboard,
onEditNotes = onEditNotesClicked,
) )
} }
}, },

View File

@@ -9,7 +9,6 @@ import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope 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.Delete
import androidx.compose.material.icons.outlined.DoneAll import androidx.compose.material.icons.outlined.DoneAll
import androidx.compose.material.icons.outlined.Download 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.RemoveDone
import androidx.compose.material.icons.outlined.SwapCalls
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface 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.platform.LocalHapticFeedback
import androidx.compose.ui.res.vectorResource import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import eu.kanade.presentation.components.DownloadDropdownMenu import eu.kanade.presentation.components.DownloadDropdownMenu
import eu.kanade.presentation.components.DropdownMenu
import eu.kanade.presentation.manga.DownloadAction import eu.kanade.presentation.manga.DownloadAction
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@@ -191,7 +185,7 @@ private fun RowScope.Button(
targetValue = if (toConfirm) 2f else 1f, targetValue = if (toConfirm) 2f else 1f,
label = "weight", label = "weight",
) )
Box( Column(
modifier = Modifier modifier = Modifier
.size(48.dp) .size(48.dp)
.weight(animatedWeight) .weight(animatedWeight)
@@ -201,28 +195,24 @@ private fun RowScope.Button(
onLongClick = onLongClick, onLongClick = onLongClick,
onClick = onClick, onClick = onClick,
), ),
contentAlignment = Alignment.Center, verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) { ) {
Column( Icon(
verticalArrangement = Arrangement.Center, imageVector = icon,
horizontalAlignment = Alignment.CenterHorizontally, contentDescription = title,
)
AnimatedVisibility(
visible = toConfirm,
enter = expandVertically(expandFrom = Alignment.Top) + fadeIn(),
exit = shrinkVertically(shrinkTowards = Alignment.Top) + fadeOut(),
) { ) {
Icon( Text(
imageVector = icon, text = title,
contentDescription = 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() content?.invoke()
} }
@@ -236,7 +226,6 @@ fun LibraryBottomActionMenu(
onMarkAsUnreadClicked: () -> Unit, onMarkAsUnreadClicked: () -> Unit,
onDownloadClicked: ((DownloadAction) -> Unit)?, onDownloadClicked: ((DownloadAction) -> Unit)?,
onDeleteClicked: () -> Unit, onDeleteClicked: () -> Unit,
onMigrateClicked: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
AnimatedVisibility( AnimatedVisibility(
@@ -251,18 +240,17 @@ fun LibraryBottomActionMenu(
color = MaterialTheme.colorScheme.surfaceContainerHigh, color = MaterialTheme.colorScheme.surfaceContainerHigh,
) { ) {
val haptic = LocalHapticFeedback.current 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 } var resetJob: Job? = remember { null }
val onLongClickItem: (Int) -> Unit = { toConfirmIndex -> val onLongClickItem: (Int) -> Unit = { toConfirmIndex ->
haptic.performHapticFeedback(HapticFeedbackType.LongPress) haptic.performHapticFeedback(HapticFeedbackType.LongPress)
(0..5).forEach { i -> confirm[i] = i == toConfirmIndex } (0..<5).forEach { i -> confirm[i] = i == toConfirmIndex }
resetJob?.cancel() resetJob?.cancel()
resetJob = scope.launch { resetJob = scope.launch {
delay(1.seconds) delay(1.seconds)
if (isActive) confirm[toConfirmIndex] = false if (isActive) confirm[toConfirmIndex] = false
} }
} }
val itemOverflow = onDownloadClicked != null
Row( Row(
modifier = Modifier modifier = Modifier
.windowInsetsPadding( .windowInsetsPadding(
@@ -301,57 +289,22 @@ fun LibraryBottomActionMenu(
onLongClick = { onLongClickItem(3) }, onLongClick = { onLongClickItem(3) },
onClick = { downloadExpanded = !downloadExpanded }, onClick = { downloadExpanded = !downloadExpanded },
) { ) {
val onDismissRequest = { downloadExpanded = false }
DownloadDropdownMenu( DownloadDropdownMenu(
expanded = downloadExpanded, expanded = downloadExpanded,
onDismissRequest = { downloadExpanded = false }, onDismissRequest = onDismissRequest,
onDownloadClicked = onDownloadClicked, onDownloadClicked = onDownloadClicked,
offset = BottomBarMenuDpOffset,
) )
} }
} }
if (!itemOverflow) { Button(
Button( title = stringResource(MR.strings.action_delete),
title = stringResource(MR.strings.migrate), icon = Icons.Outlined.Delete,
icon = Icons.Outlined.SwapCalls, toConfirm = confirm[4],
toConfirm = confirm[4], onLongClick = { onLongClickItem(4) },
onLongClick = { onLongClickItem(4) }, onClick = onDeleteClicked,
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,
)
}
}
}
} }
} }
} }
} }
private val BottomBarMenuDpOffset = DpOffset(0.dp, 0.dp)

View File

@@ -36,7 +36,6 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogProperties
import androidx.core.graphics.drawable.toDrawable
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import coil3.asDrawable import coil3.asDrawable
import coil3.imageLoader import coil3.imageLoader
@@ -172,13 +171,15 @@ fun MangaCoverDialog(
.memoryCachePolicy(CachePolicy.DISABLED) .memoryCachePolicy(CachePolicy.DISABLED)
.target { image -> .target { image ->
val drawable = image.asDrawable(view.context.resources) val drawable = image.asDrawable(view.context.resources)
// Copy bitmap in case it came from memory cache // Copy bitmap in case it came from memory cache
// Because SSIV needs to thoroughly read the image // Because SSIV needs to thoroughly read the image
val copy = (drawable as? BitmapDrawable) val copy = (drawable as? BitmapDrawable)?.let {
?.bitmap BitmapDrawable(
?.copy(Bitmap.Config.HARDWARE, false) view.context.resources,
?.toDrawable(view.context.resources) it.bitmap.copy(Bitmap.Config.HARDWARE, false),
?: drawable )
} ?: drawable
view.setImage(copy, ReaderPageImageView.Config(zoomDuration = 500)) view.setImage(copy, ReaderPageImageView.Config(zoomDuration = 500))
} }
.build() .build()

View File

@@ -19,7 +19,8 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp 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 kotlinx.collections.immutable.toImmutableList
import tachiyomi.domain.manga.interactor.FetchInterval import tachiyomi.domain.manga.interactor.FetchInterval
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
@@ -108,7 +109,7 @@ fun SetIntervalDialog(
} }
Spacer(Modifier.height(MaterialTheme.padding.small)) Spacer(Modifier.height(MaterialTheme.padding.small))
if (onValueChanged != null && (!isReleaseBuildType)) { if (onValueChanged != null && (isDevFlavor || isPreviewBuildType)) {
Text(stringResource(MR.strings.manga_interval_custom_amount)) Text(stringResource(MR.strings.manga_interval_custom_amount))
BoxWithConstraints( BoxWithConstraints(

View File

@@ -23,7 +23,6 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.sizeIn import androidx.compose.foundation.layout.sizeIn
import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.text.appendInlineContent
import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Brush import androidx.compose.material.icons.filled.Brush
@@ -53,7 +52,6 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
@@ -69,13 +67,9 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.Layout import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalContext 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.TextAlign
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.text.withLink
import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@@ -83,17 +77,10 @@ import androidx.compose.ui.unit.sp
import coil3.compose.AsyncImage import coil3.compose.AsyncImage
import coil3.request.ImageRequest import coil3.request.ImageRequest
import coil3.request.crossfade 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.presentation.components.DropdownMenu
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.util.system.copyToClipboard 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.domain.manga.model.Manga
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.DISABLED_ALPHA import tachiyomi.presentation.core.components.material.DISABLED_ALPHA
@@ -103,12 +90,12 @@ import tachiyomi.presentation.core.i18n.pluralStringResource
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.clickableNoIndication import tachiyomi.presentation.core.util.clickableNoIndication
import tachiyomi.presentation.core.util.secondaryItemAlpha import tachiyomi.presentation.core.util.secondaryItemAlpha
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.time.Instant import java.time.Instant
import java.time.temporal.ChronoUnit import java.time.temporal.ChronoUnit
import kotlin.math.roundToInt import kotlin.math.roundToInt
private val whitespaceLineRegex = Regex("[\\r\\n]{2,}", setOf(RegexOption.MULTILINE))
@Composable @Composable
fun MangaInfoBox( fun MangaInfoBox(
isTabletUi: Boolean, isTabletUi: Boolean,
@@ -249,10 +236,8 @@ fun ExpandableMangaDescription(
defaultExpandState: Boolean, defaultExpandState: Boolean,
description: String?, description: String?,
tagsProvider: () -> List<String>?, tagsProvider: () -> List<String>?,
notes: String,
onTagSearch: (String) -> Unit, onTagSearch: (String) -> Unit,
onCopyTagToClipboard: (tag: String) -> Unit, onCopyTagToClipboard: (tag: String) -> Unit,
onEditNotes: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
Column(modifier = modifier) { Column(modifier = modifier) {
@@ -261,12 +246,15 @@ fun ExpandableMangaDescription(
} }
val desc = val desc =
description.takeIf { !it.isNullOrBlank() } ?: stringResource(MR.strings.description_placeholder) description.takeIf { !it.isNullOrBlank() } ?: stringResource(MR.strings.description_placeholder)
val trimmedDescription = remember(desc) {
desc
.replace(whitespaceLineRegex, "\n")
.trimEnd()
}
MangaSummary( MangaSummary(
description = desc, expandedDescription = desc,
shrunkDescription = trimmedDescription,
expanded = expanded, expanded = expanded,
notes = notes,
onEditNotesClicked = onEditNotes,
modifier = Modifier modifier = Modifier
.padding(top = 8.dp) .padding(top = 8.dp)
.padding(horizontal = 16.dp) .padding(horizontal = 16.dp)
@@ -567,93 +555,41 @@ 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 @Composable
private fun MangaSummary( private fun MangaSummary(
description: String, expandedDescription: String,
notes: String, shrunkDescription: String,
expanded: Boolean, expanded: Boolean,
onEditNotesClicked: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val preferences = remember { Injekt.get<UiPreferences>() }
val loadImages = remember { preferences.imagesInDescription().get() }
val animProgress by animateFloatAsState( val animProgress by animateFloatAsState(
targetValue = if (expanded) 1f else 0f, targetValue = if (expanded) 1f else 0f,
label = "summary", label = "summary",
) )
var infoHeight by remember { mutableIntStateOf(0) }
Layout( Layout(
modifier = modifier.clipToBounds(), modifier = modifier.clipToBounds(),
contents = listOf( contents = listOf(
{ {
Text( Text(
// Shows at least 3 lines if no notes text = "\n\n", // Shows at least 3 lines
// when there are notes show 6
text = if (notes.isBlank()) "\n\n" else "\n\n\n\n\n",
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
) )
}, },
{ {
Column( Text(
modifier = Modifier.onSizeChanged { size -> text = expandedDescription,
infoHeight = size.height style = MaterialTheme.typography.bodyMedium,
}, )
) { },
MangaNotesSection( {
content = notes, SelectionContainer {
expanded = expanded, Text(
onEditNotes = onEditNotesClicked, 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,
)
}
} }
}, },
{ {
@@ -674,11 +610,14 @@ private fun MangaSummary(
} }
}, },
), ),
) { (shrunk, actual, scrim), constraints -> ) { (shrunk, expanded, actual, scrim), constraints ->
val shrunkHeight = shrunk.single() val shrunkHeight = shrunk.single()
.measure(constraints) .measure(constraints)
.height .height
val heightDelta = infoHeight - shrunkHeight val expandedHeight = expanded.single()
.measure(constraints)
.height
val heightDelta = expandedHeight - shrunkHeight
val scrimHeight = 24.dp.roundToPx() val scrimHeight = 24.dp.roundToPx()
val actualPlaceable = actual.single() val actualPlaceable = actual.single()

View File

@@ -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,
)
}
}

View File

@@ -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!",
)
}

View File

@@ -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),
)
}
}

View File

@@ -1,12 +1,18 @@
package eu.kanade.presentation.manga.components package eu.kanade.presentation.manga.components
import androidx.compose.foundation.layout.Column
import androidx.compose.material.icons.Icons 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.Download
import androidx.compose.material.icons.outlined.FilterList import androidx.compose.material.icons.outlined.FilterList
import androidx.compose.material.icons.outlined.FlipToBack import androidx.compose.material.icons.outlined.FlipToBack
import androidx.compose.material.icons.outlined.SelectAll import androidx.compose.material.icons.outlined.SelectAll
import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalContentColor import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme 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.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -14,12 +20,12 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier 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 androidx.compose.ui.unit.dp
import eu.kanade.presentation.components.AppBar import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.AppBarActions import eu.kanade.presentation.components.AppBarActions
import eu.kanade.presentation.components.AppBarTitle
import eu.kanade.presentation.components.DownloadDropdownMenu import eu.kanade.presentation.components.DownloadDropdownMenu
import eu.kanade.presentation.components.UpIcon
import eu.kanade.presentation.manga.DownloadAction import eu.kanade.presentation.manga.DownloadAction
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
@@ -29,129 +35,130 @@ import tachiyomi.presentation.core.theme.active
@Composable @Composable
fun MangaToolbar( fun MangaToolbar(
title: String, title: String,
titleAlphaProvider: () -> Float,
hasFilters: Boolean, hasFilters: Boolean,
navigateUp: () -> Unit, onBackClicked: () -> Unit,
onClickFilter: () -> Unit, onClickFilter: () -> Unit,
onClickShare: (() -> Unit)?, onClickShare: (() -> Unit)?,
onClickDownload: ((DownloadAction) -> Unit)?, onClickDownload: ((DownloadAction) -> Unit)?,
onClickEditCategory: (() -> Unit)?, onClickEditCategory: (() -> Unit)?,
onClickRefresh: () -> Unit, onClickRefresh: () -> Unit,
onClickMigrate: (() -> Unit)?, onClickMigrate: (() -> Unit)?,
onClickEditNotes: () -> Unit,
// For action mode // For action mode
actionModeCounter: Int, actionModeCounter: Int,
onCancelActionMode: () -> Unit,
onSelectAll: () -> Unit, onSelectAll: () -> Unit,
onInvertSelection: () -> Unit, onInvertSelection: () -> Unit,
titleAlphaProvider: () -> Float,
backgroundAlphaProvider: () -> Float,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
backgroundAlphaProvider: () -> Float = titleAlphaProvider,
) { ) {
val isActionMode = actionModeCounter > 0 Column(
AppBar(
titleContent = {
if (isActionMode) {
AppBarTitle(actionModeCounter.toString())
} else {
AppBarTitle(title, modifier = Modifier.alpha(titleAlphaProvider()))
}
},
modifier = modifier, modifier = modifier,
backgroundColor = MaterialTheme.colorScheme ) {
.surfaceColorAtElevation(3.dp) val isActionMode = actionModeCounter > 0
.copy(alpha = if (isActionMode) 1f else backgroundAlphaProvider()), TopAppBar(
navigateUp = navigateUp, title = {
actions = { Text(
var downloadExpanded by remember { mutableStateOf(false) } text = if (isActionMode) actionModeCounter.toString() else title,
if (onClickDownload != null) { maxLines = 1,
val onDismissRequest = { downloadExpanded = false } overflow = TextOverflow.Ellipsis,
DownloadDropdownMenu( color = LocalContentColor.current.copy(alpha = if (isActionMode) 1f else titleAlphaProvider()),
expanded = downloadExpanded,
onDismissRequest = onDismissRequest,
onDownloadClicked = onClickDownload,
) )
} },
navigationIcon = {
val filterTint = if (hasFilters) MaterialTheme.colorScheme.active else LocalContentColor.current IconButton(onClick = onBackClicked) {
AppBarActions( UpIcon(navigationIcon = Icons.Outlined.Close.takeIf { isActionMode })
actions = persistentListOf<AppBar.AppBarAction>().builder().apply { }
if (isActionMode) { },
add( actions = {
if (isActionMode) {
AppBarActions(
persistentListOf(
AppBar.Action( AppBar.Action(
title = stringResource(MR.strings.action_select_all), title = stringResource(MR.strings.action_select_all),
icon = Icons.Outlined.SelectAll, icon = Icons.Outlined.SelectAll,
onClick = onSelectAll, onClick = onSelectAll,
), ),
)
add(
AppBar.Action( AppBar.Action(
title = stringResource(MR.strings.action_select_inverse), title = stringResource(MR.strings.action_select_inverse),
icon = Icons.Outlined.FlipToBack, icon = Icons.Outlined.FlipToBack,
onClick = onInvertSelection, onClick = onInvertSelection,
), ),
) ),
return@apply )
} } else {
var downloadExpanded by remember { mutableStateOf(false) }
if (onClickDownload != null) { if (onClickDownload != null) {
add( val onDismissRequest = { downloadExpanded = false }
AppBar.Action( DownloadDropdownMenu(
title = stringResource(MR.strings.manga_download), expanded = downloadExpanded,
icon = Icons.Outlined.Download, onDismissRequest = onDismissRequest,
onClick = { downloadExpanded = !downloadExpanded }, onDownloadClicked = onClickDownload,
),
) )
} }
add(
AppBar.Action( val filterTint = if (hasFilters) MaterialTheme.colorScheme.active else LocalContentColor.current
title = stringResource(MR.strings.action_filter), AppBarActions(
icon = Icons.Outlined.FilterList, actions = persistentListOf<AppBar.AppBarAction>().builder()
iconTint = filterTint, .apply {
onClick = onClickFilter, if (onClickDownload != null) {
), add(
) AppBar.Action(
add( title = stringResource(MR.strings.manga_download),
AppBar.OverflowAction( icon = Icons.Outlined.Download,
title = stringResource(MR.strings.action_webview_refresh), onClick = { downloadExpanded = !downloadExpanded },
onClick = onClickRefresh, ),
), )
) }
if (onClickEditCategory != null) { add(
add( AppBar.Action(
AppBar.OverflowAction( title = stringResource(MR.strings.action_filter),
title = stringResource(MR.strings.action_edit_categories), icon = Icons.Outlined.FilterList,
onClick = onClickEditCategory, iconTint = filterTint,
), onClick = onClickFilter,
) ),
} )
if (onClickMigrate != null) { add(
add( AppBar.OverflowAction(
AppBar.OverflowAction( title = stringResource(MR.strings.action_webview_refresh),
title = stringResource(MR.strings.action_migrate), onClick = onClickRefresh,
onClick = onClickMigrate, ),
), )
) if (onClickEditCategory != null) {
} add(
if (onClickShare != null) { AppBar.OverflowAction(
add( title = stringResource(MR.strings.action_edit_categories),
AppBar.OverflowAction( onClick = onClickEditCategory,
title = stringResource(MR.strings.action_share), ),
onClick = onClickShare, )
), }
) if (onClickMigrate != null) {
} add(
add( AppBar.OverflowAction(
AppBar.OverflowAction( title = stringResource(MR.strings.action_migrate),
title = stringResource(MR.strings.action_notes), onClick = onClickMigrate,
onClick = onClickEditNotes, ),
), )
}
if (onClickShare != null) {
add(
AppBar.OverflowAction(
title = stringResource(MR.strings.action_share),
onClick = onClickShare,
),
)
}
}
.build(),
) )
} }
.build(), },
) colors = TopAppBarDefaults.topAppBarColors(
}, containerColor = MaterialTheme.colorScheme
isActionMode = isActionMode, .surfaceColorAtElevation(3.dp)
onCancelActionMode = onCancelActionMode, .copy(alpha = if (isActionMode) 1f else backgroundAlphaProvider()),
) ),
)
}
} }

View File

@@ -1,287 +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,
codeBackground = codeBackground,
inlineCodeBackground = codeBackground,
dividerColor = MaterialTheme.colorScheme.outlineVariant,
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,
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)

View File

@@ -1,10 +1,15 @@
package eu.kanade.presentation.more 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.padding
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.HelpOutline import androidx.compose.material.icons.automirrored.outlined.HelpOutline
import androidx.compose.material.icons.automirrored.outlined.Label 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.CloudOff
import androidx.compose.material.icons.outlined.GetApp import androidx.compose.material.icons.outlined.GetApp
import androidx.compose.material.icons.outlined.Info import androidx.compose.material.icons.outlined.Info
@@ -35,6 +40,7 @@ fun MoreScreen(
onDownloadedOnlyChange: (Boolean) -> Unit, onDownloadedOnlyChange: (Boolean) -> Unit,
incognitoMode: Boolean, incognitoMode: Boolean,
onIncognitoModeChange: (Boolean) -> Unit, onIncognitoModeChange: (Boolean) -> Unit,
isFDroid: Boolean,
onClickDownloadQueue: () -> Unit, onClickDownloadQueue: () -> Unit,
onClickCategories: () -> Unit, onClickCategories: () -> Unit,
onClickStats: () -> Unit, onClickStats: () -> Unit,
@@ -44,7 +50,19 @@ fun MoreScreen(
) { ) {
val uriHandler = LocalUriHandler.current 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( ScrollbarLazyColumn(
modifier = Modifier.padding(contentPadding), modifier = Modifier.padding(contentPadding),
) { ) {
@@ -146,13 +164,6 @@ fun MoreScreen(
onPreferenceClick = { uriHandler.openUri(Constants.URL_HELP) }, onPreferenceClick = { uriHandler.openUri(Constants.URL_HELP) },
) )
} }
item {
TextPreferenceWidget(
title = stringResource(MR.strings.label_donate),
icon = Icons.Outlined.AttachMoney,
onPreferenceClick = { uriHandler.openUri(Constants.URL_DONATE) },
)
}
} }
} }
} }

View File

@@ -1,6 +1,5 @@
package eu.kanade.presentation.more package eu.kanade.presentation.more
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
@@ -14,10 +13,13 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.tooling.preview.PreviewLightDark 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 eu.kanade.presentation.theme.TachiyomiPreviewTheme
import org.intellij.markdown.flavours.gfm.GFMFlavourDescriptor
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
@@ -40,15 +42,17 @@ fun NewUpdateScreen(
rejectText = stringResource(MR.strings.action_not_now), rejectText = stringResource(MR.strings.action_not_now),
onRejectClick = onRejectUpdate, onRejectClick = onRejectUpdate,
) { ) {
Column( RichText(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(vertical = MaterialTheme.padding.large), .padding(vertical = MaterialTheme.padding.large),
style = RichTextStyle(
stringStyle = RichTextStringStyle(
linkStyle = SpanStyle(color = MaterialTheme.colorScheme.primary),
),
),
) { ) {
MarkdownRender( Markdown(content = changelogInfo)
content = changelogInfo,
flavour = GFMFlavourDescriptor(),
)
TextButton( TextButton(
onClick = onOpenInBrowser, onClick = onOpenInBrowser,

View File

@@ -42,9 +42,7 @@ fun OnboardingScreen(
} }
val isLastStep = currentStep == steps.lastIndex val isLastStep = currentStep == steps.lastIndex
BackHandler(enabled = currentStep != 0) { BackHandler(enabled = currentStep != 0, onBack = { currentStep-- })
currentStep--
}
InfoScreen( InfoScreen(
icon = Icons.Outlined.RocketLaunch, icon = Icons.Outlined.RocketLaunch,

View File

@@ -4,6 +4,7 @@ import android.Manifest
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build import android.os.Build
import android.os.PowerManager import android.os.PowerManager
import android.provider.Settings import android.provider.Settings
@@ -13,13 +14,11 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.Check
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem import androidx.compose.material3.ListItem
import androidx.compose.material3.ListItemDefaults import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Switch
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect 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.platform.LocalContext
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import androidx.core.net.toUri
import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.LocalLifecycleOwner
import eu.kanade.presentation.util.rememberRequestPackageInstallsPermissionState 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.launchRequestPackageInstallsPermission
import eu.kanade.tachiyomi.util.system.telemetryIncluded
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.collectAsState
import tachiyomi.presentation.core.util.secondaryItemAlpha import tachiyomi.presentation.core.util.secondaryItemAlpha
import uy.kohesive.injekt.injectLazy
internal class PermissionStep : OnboardingStep { internal class PermissionStep : OnboardingStep {
private val privacyPreferences: PrivacyPreferences by injectLazy()
private var notificationGranted by mutableStateOf(false) private var notificationGranted by mutableStateOf(false)
private var batteryGranted by mutableStateOf(false) private var batteryGranted by mutableStateOf(false)
@@ -81,7 +73,7 @@ internal class PermissionStep : OnboardingStep {
} }
Column { Column {
PermissionCheckbox( PermissionItem(
title = stringResource(MR.strings.onboarding_permission_install_apps), title = stringResource(MR.strings.onboarding_permission_install_apps),
subtitle = stringResource(MR.strings.onboarding_permission_install_apps_description), subtitle = stringResource(MR.strings.onboarding_permission_install_apps_description),
granted = installGranted, granted = installGranted,
@@ -97,7 +89,7 @@ internal class PermissionStep : OnboardingStep {
// no-op. resulting checks is being done on resume // no-op. resulting checks is being done on resume
}, },
) )
PermissionCheckbox( PermissionItem(
title = stringResource(MR.strings.onboarding_permission_notifications), title = stringResource(MR.strings.onboarding_permission_notifications),
subtitle = stringResource(MR.strings.onboarding_permission_notifications_description), subtitle = stringResource(MR.strings.onboarding_permission_notifications_description),
granted = notificationGranted, granted = notificationGranted,
@@ -105,43 +97,18 @@ internal class PermissionStep : OnboardingStep {
) )
} }
PermissionCheckbox( PermissionItem(
title = stringResource(MR.strings.onboarding_permission_ignore_battery_opts), title = stringResource(MR.strings.onboarding_permission_ignore_battery_opts),
subtitle = stringResource(MR.strings.onboarding_permission_ignore_battery_opts_description), subtitle = stringResource(MR.strings.onboarding_permission_ignore_battery_opts_description),
granted = batteryGranted, granted = batteryGranted,
onButtonClick = { onButtonClick = {
@SuppressLint("BatteryLife") @SuppressLint("BatteryLife")
val intent = Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply { val intent = Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply {
data = "package:${context.packageName}".toUri() data = Uri.parse("package:${context.packageName}")
} }
context.startActivity(intent) 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 @Composable
private fun PermissionCheckbox( private fun PermissionItem(
title: String, title: String,
subtitle: String, subtitle: String,
granted: Boolean, granted: Boolean,
@@ -190,26 +157,4 @@ internal class PermissionStep : OnboardingStep {
colors = ListItemDefaults.colors(containerColor = Color.Transparent), 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),
)
}
} }

View File

@@ -1,6 +1,5 @@
package eu.kanade.presentation.more.settings package eu.kanade.presentation.more.settings
import androidx.annotation.IntRange
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
@@ -15,10 +14,10 @@ sealed class Preference {
abstract val title: String abstract val title: String
abstract val enabled: Boolean abstract val enabled: Boolean
sealed class PreferenceItem<T, R> : Preference() { sealed class PreferenceItem<T> : Preference() {
abstract val subtitle: String? abstract val subtitle: String?
abstract val icon: ImageVector? abstract val icon: ImageVector?
abstract val onValueChanged: suspend (value: T) -> R abstract val onValueChanged: suspend (newValue: T) -> Boolean
/** /**
* A basic [PreferenceItem] that only displays texts. * A basic [PreferenceItem] that only displays texts.
@@ -26,59 +25,57 @@ sealed class Preference {
data class TextPreference( data class TextPreference(
override val title: String, override val title: String,
override val subtitle: String? = null, override val subtitle: String? = null,
override val icon: ImageVector? = null,
override val enabled: Boolean = true, override val enabled: Boolean = true,
override val onValueChanged: suspend (newValue: String) -> Boolean = { true },
val onClick: (() -> Unit)? = null, val onClick: (() -> Unit)? = null,
) : PreferenceItem<String, Unit>() { ) : PreferenceItem<String>()
override val icon: ImageVector? = null
override val onValueChanged: suspend (value: String) -> Unit = {}
}
/** /**
* A [PreferenceItem] that provides a two-state toggleable option. * A [PreferenceItem] that provides a two-state toggleable option.
*/ */
data class SwitchPreference( data class SwitchPreference(
val preference: PreferenceData<Boolean>, val pref: PreferenceData<Boolean>,
override val title: String, override val title: String,
override val subtitle: String? = null, override val subtitle: String? = null,
override val icon: ImageVector? = null,
override val enabled: Boolean = true, override val enabled: Boolean = true,
override val onValueChanged: suspend (value: Boolean) -> Boolean = { true }, override val onValueChanged: suspend (newValue: Boolean) -> Boolean = { true },
) : PreferenceItem<Boolean, Boolean>() { ) : PreferenceItem<Boolean>()
override val icon: ImageVector? = null
}
/** /**
* A [PreferenceItem] that provides a slider to select an integer number. * A [PreferenceItem] that provides a slider to select an integer number.
*/ */
data class SliderPreference( data class SliderPreference(
val value: Int, val value: Int,
override val title: String, val min: Int = 0,
val max: Int,
override val title: String = "",
override val subtitle: String? = null, override val subtitle: String? = null,
val valueString: String? = null, override val icon: ImageVector? = null,
val valueRange: IntProgression = 0..1,
@IntRange(from = 0) val steps: Int = with(valueRange) { (last - first) - 1 },
override val enabled: Boolean = true, override val enabled: Boolean = true,
override val onValueChanged: suspend (value: Int) -> Unit = {}, override val onValueChanged: suspend (newValue: Int) -> Boolean = { true },
) : PreferenceItem<Int, Unit>() { ) : PreferenceItem<Int>()
override val icon: ImageVector? = null
}
/** /**
* A [PreferenceItem] that displays a list of entries as a dialog. * A [PreferenceItem] that displays a list of entries as a dialog.
*/ */
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
data class ListPreference<T>( data class ListPreference<T>(
val preference: PreferenceData<T>, val pref: PreferenceData<T>,
val entries: ImmutableMap<T, String>,
override val title: String, override val title: String,
override val subtitle: String? = "%s", override val subtitle: String? = "%s",
val subtitleProvider: @Composable (value: T, entries: ImmutableMap<T, String>) -> String? = val subtitleProvider: @Composable (value: T, entries: ImmutableMap<T, String>) -> String? =
{ v, e -> subtitle?.format(e[v]) }, { v, e -> subtitle?.format(e[v]) },
override val icon: ImageVector? = null, override val icon: ImageVector? = null,
override val enabled: Boolean = true, override val enabled: Boolean = true,
override val onValueChanged: suspend (value: T) -> Boolean = { true }, override val onValueChanged: suspend (newValue: T) -> Boolean = { true },
) : PreferenceItem<T, Boolean>() {
internal fun internalSet(value: Any) = preference.set(value as T) val entries: ImmutableMap<T, String>,
internal suspend fun internalOnValueChanged(value: Any) = onValueChanged(value as T) ) : PreferenceItem<T>() {
internal fun internalSet(newValue: Any) = pref.set(newValue as T)
internal suspend fun internalOnValueChanged(newValue: Any) = onValueChanged(newValue as T)
@Composable @Composable
internal fun internalSubtitleProvider(value: Any?, entries: ImmutableMap<out Any?, String>) = internal fun internalSubtitleProvider(value: Any?, entries: ImmutableMap<out Any?, String>) =
@@ -90,85 +87,87 @@ sealed class Preference {
*/ */
data class BasicListPreference( data class BasicListPreference(
val value: String, val value: String,
val entries: ImmutableMap<String, String>,
override val title: String, override val title: String,
override val subtitle: String? = "%s", override val subtitle: String? = "%s",
val subtitleProvider: @Composable (value: String, entries: ImmutableMap<String, String>) -> String? = val subtitleProvider: @Composable (value: String, entries: ImmutableMap<String, String>) -> String? =
{ v, e -> subtitle?.format(e[v]) }, { v, e -> subtitle?.format(e[v]) },
override val icon: ImageVector? = null, override val icon: ImageVector? = null,
override val enabled: Boolean = true, override val enabled: Boolean = true,
override val onValueChanged: suspend (value: String) -> Unit = {}, override val onValueChanged: suspend (newValue: String) -> Boolean = { true },
) : PreferenceItem<String, Unit>()
val entries: ImmutableMap<String, String>,
) : PreferenceItem<String>()
/** /**
* A [PreferenceItem] that displays a list of entries as a dialog. * A [PreferenceItem] that displays a list of entries as a dialog.
* Multiple entries can be selected at the same time. * Multiple entries can be selected at the same time.
*/ */
data class MultiSelectListPreference( data class MultiSelectListPreference(
val preference: PreferenceData<Set<String>>, val pref: PreferenceData<Set<String>>,
val entries: ImmutableMap<String, String>,
override val title: String, override val title: String,
override val subtitle: String? = "%s", override val subtitle: String? = "%s",
val subtitleProvider: @Composable (value: Set<String>, entries: ImmutableMap<String, String>) -> String? = val subtitleProvider: @Composable (
{ v, e -> value: Set<String>,
val combined = remember(v, e) { entries: ImmutableMap<String, String>,
v.mapNotNull { e[it] } ) -> String? = { v, e ->
.joinToString() val combined = remember(v) {
.takeUnless { it.isBlank() } v.map { e[it] }
} .takeIf { it.isNotEmpty() }
?: stringResource(MR.strings.none) ?.joinToString()
subtitle?.format(combined) } ?: stringResource(MR.strings.none)
}, subtitle?.format(combined)
},
override val icon: ImageVector? = null, override val icon: ImageVector? = null,
override val enabled: Boolean = true, override val enabled: Boolean = true,
override val onValueChanged: suspend (value: Set<String>) -> Boolean = { true }, override val onValueChanged: suspend (newValue: Set<String>) -> Boolean = { true },
) : PreferenceItem<Set<String>, Boolean>()
val entries: ImmutableMap<String, String>,
) : PreferenceItem<Set<String>>()
/** /**
* A [PreferenceItem] that shows a EditText in the dialog. * A [PreferenceItem] that shows a EditText in the dialog.
*/ */
data class EditTextPreference( data class EditTextPreference(
val preference: PreferenceData<String>, val pref: PreferenceData<String>,
override val title: String, override val title: String,
override val subtitle: String? = "%s", override val subtitle: String? = "%s",
override val icon: ImageVector? = null,
override val enabled: Boolean = true, override val enabled: Boolean = true,
override val onValueChanged: suspend (value: String) -> Boolean = { true }, override val onValueChanged: suspend (newValue: String) -> Boolean = { true },
) : PreferenceItem<String, Boolean>() { ) : PreferenceItem<String>()
override val icon: ImageVector? = null
}
/** /**
* A [PreferenceItem] for individual tracker. * A [PreferenceItem] for individual tracker.
*/ */
data class TrackerPreference( data class TrackerPreference(
val tracker: Tracker, val tracker: Tracker,
override val title: String,
val login: () -> Unit, val login: () -> Unit,
val logout: () -> Unit, val logout: () -> Unit,
) : PreferenceItem<String, Unit>() { ) : PreferenceItem<String>() {
override val title: String = ""
override val enabled: Boolean = true override val enabled: Boolean = true
override val subtitle: String? = null override val subtitle: String? = null
override val icon: ImageVector? = null override val icon: ImageVector? = null
override val onValueChanged: suspend (value: String) -> Unit = {} override val onValueChanged: suspend (newValue: String) -> Boolean = { true }
} }
data class InfoPreference( data class InfoPreference(
override val title: String, override val title: String,
) : PreferenceItem<String, Unit>() { ) : PreferenceItem<String>() {
override val enabled: Boolean = true override val enabled: Boolean = true
override val subtitle: String? = null override val subtitle: String? = null
override val icon: ImageVector? = null override val icon: ImageVector? = null
override val onValueChanged: suspend (value: String) -> Unit = {} override val onValueChanged: suspend (newValue: String) -> Boolean = { true }
} }
data class CustomPreference( data class CustomPreference(
override val title: String, override val title: String,
val content: @Composable () -> Unit, val content: @Composable (PreferenceItem<String>) -> Unit,
) : PreferenceItem<Unit, Unit>() { ) : PreferenceItem<String>() {
override val enabled: Boolean = true override val enabled: Boolean = true
override val subtitle: String? = null override val subtitle: String? = null
override val icon: ImageVector? = null override val icon: ImageVector? = null
override val onValueChanged: suspend (value: Unit) -> Unit = {} override val onValueChanged: suspend (newValue: String) -> Boolean = { true }
} }
} }
@@ -176,6 +175,6 @@ sealed class Preference {
override val title: String, override val title: String,
override val enabled: Boolean = true, override val enabled: Boolean = true,
val preferenceItems: ImmutableList<PreferenceItem<out Any, out Any>>, val preferenceItems: ImmutableList<PreferenceItem<out Any>>,
) : Preference() ) : Preference()
} }

View File

@@ -5,8 +5,6 @@ import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically 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.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
@@ -14,20 +12,16 @@ import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.structuralEqualityPolicy import androidx.compose.runtime.structuralEqualityPolicy
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import eu.kanade.presentation.more.settings.widget.EditTextPreferenceWidget import eu.kanade.presentation.more.settings.widget.EditTextPreferenceWidget
import eu.kanade.presentation.more.settings.widget.InfoWidget import eu.kanade.presentation.more.settings.widget.InfoWidget
import eu.kanade.presentation.more.settings.widget.ListPreferenceWidget import eu.kanade.presentation.more.settings.widget.ListPreferenceWidget
import eu.kanade.presentation.more.settings.widget.MultiSelectListPreferenceWidget 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.SwitchPreferenceWidget
import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget 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 eu.kanade.presentation.more.settings.widget.TrackingPreferenceWidget
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import tachiyomi.presentation.core.components.BaseSliderItem import tachiyomi.presentation.core.components.SliderItem
import tachiyomi.presentation.core.util.collectAsState import tachiyomi.presentation.core.util.collectAsState
val LocalPreferenceHighlighted = compositionLocalOf(structuralEqualityPolicy()) { false } val LocalPreferenceHighlighted = compositionLocalOf(structuralEqualityPolicy()) { false }
@@ -35,7 +29,7 @@ val LocalPreferenceMinHeight = compositionLocalOf(structuralEqualityPolicy()) {
@Composable @Composable
fun StatusWrapper( fun StatusWrapper(
item: Preference.PreferenceItem<*, *>, item: Preference.PreferenceItem<*>,
highlightKey: String?, highlightKey: String?,
content: @Composable () -> Unit, content: @Composable () -> Unit,
) { ) {
@@ -56,7 +50,7 @@ fun StatusWrapper(
@Composable @Composable
internal fun PreferenceItem( internal fun PreferenceItem(
item: Preference.PreferenceItem<*, *>, item: Preference.PreferenceItem<*>,
highlightKey: String?, highlightKey: String?,
) { ) {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
@@ -66,7 +60,7 @@ internal fun PreferenceItem(
) { ) {
when (item) { when (item) {
is Preference.PreferenceItem.SwitchPreference -> { is Preference.PreferenceItem.SwitchPreference -> {
val value by item.preference.collectAsState() val value by item.pref.collectAsState()
SwitchPreferenceWidget( SwitchPreferenceWidget(
title = item.title, title = item.title,
subtitle = item.subtitle, subtitle = item.subtitle,
@@ -75,34 +69,29 @@ internal fun PreferenceItem(
onCheckedChanged = { newValue -> onCheckedChanged = { newValue ->
scope.launch { scope.launch {
if (item.onValueChanged(newValue)) { if (item.onValueChanged(newValue)) {
item.preference.set(newValue) item.pref.set(newValue)
} }
} }
}, },
) )
} }
is Preference.PreferenceItem.SliderPreference -> { is Preference.PreferenceItem.SliderPreference -> {
BaseSliderItem( // TODO: use different composable?
SliderItem(
label = item.title,
min = item.min,
max = item.max,
value = item.value, value = item.value,
valueRange = item.valueRange, valueText = item.subtitle.takeUnless { it.isNullOrEmpty() } ?: item.value.toString(),
steps = item.steps,
title = item.title,
subtitle = item.subtitle,
valueString = item.valueString.takeUnless { it.isNullOrEmpty() } ?: item.value.toString(),
onChange = { onChange = {
scope.launch { scope.launch {
item.onValueChanged(it) item.onValueChanged(it)
} }
}, },
titleStyle = MaterialTheme.typography.titleLarge.copy(fontSize = TitleFontSize),
modifier = Modifier.padding(
horizontal = PrefsHorizontalPadding,
vertical = PrefsVerticalPadding,
),
) )
} }
is Preference.PreferenceItem.ListPreference<*> -> { is Preference.PreferenceItem.ListPreference<*> -> {
val value by item.preference.collectAsState() val value by item.pref.collectAsState()
ListPreferenceWidget( ListPreferenceWidget(
value = value, value = value,
title = item.title, title = item.title,
@@ -129,14 +118,14 @@ internal fun PreferenceItem(
) )
} }
is Preference.PreferenceItem.MultiSelectListPreference -> { is Preference.PreferenceItem.MultiSelectListPreference -> {
val values by item.preference.collectAsState() val values by item.pref.collectAsState()
MultiSelectListPreferenceWidget( MultiSelectListPreferenceWidget(
preference = item, preference = item,
values = values, values = values,
onValuesChange = { newValues -> onValuesChange = { newValues ->
scope.launch { scope.launch {
if (item.onValueChanged(newValues)) { if (item.onValueChanged(newValues)) {
item.preference.set(newValues.toMutableSet()) item.pref.set(newValues.toMutableSet())
} }
} }
}, },
@@ -151,7 +140,7 @@ internal fun PreferenceItem(
) )
} }
is Preference.PreferenceItem.EditTextPreference -> { is Preference.PreferenceItem.EditTextPreference -> {
val values by item.preference.collectAsState() val values by item.pref.collectAsState()
EditTextPreferenceWidget( EditTextPreferenceWidget(
title = item.title, title = item.title,
subtitle = item.subtitle, subtitle = item.subtitle,
@@ -159,7 +148,7 @@ internal fun PreferenceItem(
value = values, value = values,
onConfirm = { onConfirm = {
val accepted = item.onValueChanged(it) val accepted = item.onValueChanged(it)
if (accepted) item.preference.set(it) if (accepted) item.pref.set(it)
accepted accepted
}, },
) )
@@ -178,7 +167,7 @@ internal fun PreferenceItem(
InfoWidget(text = item.title) InfoWidget(text = item.title)
} }
is Preference.PreferenceItem.CustomPreference -> { is Preference.PreferenceItem.CustomPreference -> {
item.content() item.content(item)
} }
} }
} }

View File

@@ -71,7 +71,7 @@ fun PreferenceScreen(
} }
// Create Preference Item // Create Preference Item
is Preference.PreferenceItem<*, *> -> item { is Preference.PreferenceItem<*> -> item {
PreferenceItem( PreferenceItem(
item = preference, item = preference,
highlightKey = highlightKey, highlightKey = highlightKey,

View File

@@ -47,8 +47,8 @@ import eu.kanade.tachiyomi.network.PREF_DOH_QUAD9
import eu.kanade.tachiyomi.network.PREF_DOH_SHECAN import eu.kanade.tachiyomi.network.PREF_DOH_SHECAN
import eu.kanade.tachiyomi.ui.more.OnboardingScreen import eu.kanade.tachiyomi.ui.more.OnboardingScreen
import eu.kanade.tachiyomi.util.CrashLogUtil import eu.kanade.tachiyomi.util.CrashLogUtil
import eu.kanade.tachiyomi.util.system.GLUtil import eu.kanade.tachiyomi.util.system.isDevFlavor
import eu.kanade.tachiyomi.util.system.isReleaseBuildType import eu.kanade.tachiyomi.util.system.isPreviewBuildType
import eu.kanade.tachiyomi.util.system.isShizukuInstalled import eu.kanade.tachiyomi.util.system.isShizukuInstalled
import eu.kanade.tachiyomi.util.system.powerManager import eu.kanade.tachiyomi.util.system.powerManager
import eu.kanade.tachiyomi.util.system.setDefaultSettings import eu.kanade.tachiyomi.util.system.setDefaultSettings
@@ -61,9 +61,7 @@ import logcat.LogPriority
import okhttp3.Headers import okhttp3.Headers
import tachiyomi.core.common.util.lang.launchNonCancellable import tachiyomi.core.common.util.lang.launchNonCancellable
import tachiyomi.core.common.util.lang.withUIContext import tachiyomi.core.common.util.lang.withUIContext
import tachiyomi.core.common.util.system.ImageUtil
import tachiyomi.core.common.util.system.logcat import tachiyomi.core.common.util.system.logcat
import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.domain.manga.interactor.ResetViewerFlags import tachiyomi.domain.manga.interactor.ResetViewerFlags
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
@@ -86,7 +84,6 @@ object SettingsAdvancedScreen : SearchableSettings {
val basePreferences = remember { Injekt.get<BasePreferences>() } val basePreferences = remember { Injekt.get<BasePreferences>() }
val networkPreferences = remember { Injekt.get<NetworkPreferences>() } val networkPreferences = remember { Injekt.get<NetworkPreferences>() }
val libraryPreferences = remember { Injekt.get<LibraryPreferences>() }
return listOf( return listOf(
Preference.PreferenceItem.TextPreference( Preference.PreferenceItem.TextPreference(
@@ -99,7 +96,7 @@ object SettingsAdvancedScreen : SearchableSettings {
}, },
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = networkPreferences.verboseLogging(), pref = networkPreferences.verboseLogging(),
title = stringResource(MR.strings.pref_verbose_logging), title = stringResource(MR.strings.pref_verbose_logging),
subtitle = stringResource(MR.strings.pref_verbose_logging_summary), subtitle = stringResource(MR.strings.pref_verbose_logging_summary),
onValueChanged = { onValueChanged = {
@@ -127,7 +124,7 @@ object SettingsAdvancedScreen : SearchableSettings {
getBackgroundActivityGroup(), getBackgroundActivityGroup(),
getDataGroup(), getDataGroup(),
getNetworkGroup(networkPreferences = networkPreferences), getNetworkGroup(networkPreferences = networkPreferences),
getLibraryGroup(libraryPreferences = libraryPreferences), getLibraryGroup(),
getReaderGroup(basePreferences = basePreferences), getReaderGroup(basePreferences = basePreferences),
getExtensionsGroup(basePreferences = basePreferences), getExtensionsGroup(basePreferences = basePreferences),
) )
@@ -238,7 +235,8 @@ object SettingsAdvancedScreen : SearchableSettings {
}, },
), ),
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
preference = networkPreferences.dohProvider(), pref = networkPreferences.dohProvider(),
title = stringResource(MR.strings.pref_dns_over_https),
entries = persistentMapOf( entries = persistentMapOf(
-1 to stringResource(MR.strings.disabled), -1 to stringResource(MR.strings.disabled),
PREF_DOH_CLOUDFLARE to "Cloudflare", PREF_DOH_CLOUDFLARE to "Cloudflare",
@@ -254,14 +252,13 @@ object SettingsAdvancedScreen : SearchableSettings {
PREF_DOH_NJALLA to "Njalla", PREF_DOH_NJALLA to "Njalla",
PREF_DOH_SHECAN to "Shecan", PREF_DOH_SHECAN to "Shecan",
), ),
title = stringResource(MR.strings.pref_dns_over_https),
onValueChanged = { onValueChanged = {
context.toast(MR.strings.requires_app_restart) context.toast(MR.strings.requires_app_restart)
true true
}, },
), ),
Preference.PreferenceItem.EditTextPreference( Preference.PreferenceItem.EditTextPreference(
preference = userAgentPref, pref = userAgentPref,
title = stringResource(MR.strings.pref_user_agent_string), title = stringResource(MR.strings.pref_user_agent_string),
onValueChanged = { onValueChanged = {
try { try {
@@ -288,9 +285,7 @@ object SettingsAdvancedScreen : SearchableSettings {
} }
@Composable @Composable
private fun getLibraryGroup( private fun getLibraryGroup(): Preference.PreferenceGroup {
libraryPreferences: LibraryPreferences,
): Preference.PreferenceGroup {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val context = LocalContext.current val context = LocalContext.current
@@ -318,16 +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),
),
Preference.PreferenceItem.SwitchPreference(
preference = libraryPreferences.disallowNonAsciiFilenames(),
title = stringResource(MR.strings.pref_disallow_non_ascii_filenames),
subtitle = stringResource(MR.strings.pref_disallow_non_ascii_filenames_details),
),
), ),
) )
} }
@@ -349,31 +334,6 @@ object SettingsAdvancedScreen : SearchableSettings {
return Preference.PreferenceGroup( return Preference.PreferenceGroup(
title = stringResource(MR.strings.pref_category_reader), title = stringResource(MR.strings.pref_category_reader),
preferenceItems = persistentListOf( 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( Preference.PreferenceItem.TextPreference(
title = stringResource(MR.strings.pref_display_profile), title = stringResource(MR.strings.pref_display_profile),
subtitle = basePreferences.displayProfile().get(), subtitle = basePreferences.displayProfile().get(),
@@ -422,19 +382,19 @@ object SettingsAdvancedScreen : SearchableSettings {
title = stringResource(MR.strings.label_extensions), title = stringResource(MR.strings.label_extensions),
preferenceItems = persistentListOf( preferenceItems = persistentListOf(
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
preference = extensionInstallerPref, pref = extensionInstallerPref,
title = stringResource(MR.strings.ext_installer_pref),
entries = extensionInstallerPref.entries entries = extensionInstallerPref.entries
.filter { .filter {
// TODO: allow private option in stable versions once URL handling is more fleshed out // TODO: allow private option in stable versions once URL handling is more fleshed out
if (isReleaseBuildType) { if (isPreviewBuildType || isDevFlavor) {
it != BasePreferences.ExtensionInstaller.PRIVATE
} else {
true true
} else {
it != BasePreferences.ExtensionInstaller.PRIVATE
} }
} }
.associateWith { stringResource(it.titleRes) } .associateWith { stringResource(it.titleRes) }
.toImmutableMap(), .toImmutableMap(),
title = stringResource(MR.strings.ext_installer_pref),
onValueChanged = { onValueChanged = {
if (it == BasePreferences.ExtensionInstaller.SHIZUKU && if (it == BasePreferences.ExtensionInstaller.SHIZUKU &&
!context.isShizukuInstalled !context.isShizukuInstalled

View File

@@ -82,7 +82,7 @@ object SettingsAppearanceScreen : SearchableSettings {
} }
}, },
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = amoledPref, pref = amoledPref,
title = stringResource(MR.strings.pref_dark_theme_pure_black), title = stringResource(MR.strings.pref_dark_theme_pure_black),
enabled = themeMode != ThemeMode.LIGHT, enabled = themeMode != ThemeMode.LIGHT,
onValueChanged = { onValueChanged = {
@@ -116,28 +116,28 @@ object SettingsAppearanceScreen : SearchableSettings {
onClick = { navigator.push(AppLanguageScreen()) }, onClick = { navigator.push(AppLanguageScreen()) },
), ),
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
preference = uiPreferences.tabletUiMode(), pref = uiPreferences.tabletUiMode(),
title = stringResource(MR.strings.pref_tablet_ui_mode),
entries = TabletUiMode.entries entries = TabletUiMode.entries
.associateWith { stringResource(it.titleRes) } .associateWith { stringResource(it.titleRes) }
.toImmutableMap(), .toImmutableMap(),
title = stringResource(MR.strings.pref_tablet_ui_mode),
onValueChanged = { onValueChanged = {
context.toast(MR.strings.requires_app_restart) context.toast(MR.strings.requires_app_restart)
true true
}, },
), ),
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
preference = uiPreferences.dateFormat(), pref = uiPreferences.dateFormat(),
title = stringResource(MR.strings.pref_date_format),
entries = DateFormats entries = DateFormats
.associateWith { .associateWith {
val formattedDate = UiPreferences.dateFormat(it).format(now) val formattedDate = UiPreferences.dateFormat(it).format(now)
"${it.ifEmpty { stringResource(MR.strings.label_default) }} ($formattedDate)" "${it.ifEmpty { stringResource(MR.strings.label_default) }} ($formattedDate)"
} }
.toImmutableMap(), .toImmutableMap(),
title = stringResource(MR.strings.pref_date_format),
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = uiPreferences.relativeTime(), pref = uiPreferences.relativeTime(),
title = stringResource(MR.strings.pref_relative_format), title = stringResource(MR.strings.pref_relative_format),
subtitle = stringResource( subtitle = stringResource(
MR.strings.pref_relative_format_summary, MR.strings.pref_relative_format_summary,
@@ -145,10 +145,6 @@ object SettingsAppearanceScreen : SearchableSettings {
formattedNow, formattedNow,
), ),
), ),
Preference.PreferenceItem.SwitchPreference(
preference = uiPreferences.imagesInDescription(),
title = stringResource(MR.strings.pref_display_images_description),
),
), ),
) )
} }

View File

@@ -43,7 +43,7 @@ object SettingsBrowseScreen : SearchableSettings {
title = stringResource(MR.strings.label_sources), title = stringResource(MR.strings.label_sources),
preferenceItems = persistentListOf( preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = sourcePreferences.hideInLibraryItems(), pref = sourcePreferences.hideInLibraryItems(),
title = stringResource(MR.strings.pref_hide_in_library_items), title = stringResource(MR.strings.pref_hide_in_library_items),
), ),
Preference.PreferenceItem.TextPreference( Preference.PreferenceItem.TextPreference(
@@ -59,7 +59,7 @@ object SettingsBrowseScreen : SearchableSettings {
title = stringResource(MR.strings.pref_category_nsfw_content), title = stringResource(MR.strings.pref_category_nsfw_content),
preferenceItems = persistentListOf( preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = sourcePreferences.showNsfwSource(), pref = sourcePreferences.showNsfwSource(),
title = stringResource(MR.strings.pref_show_nsfw_source), title = stringResource(MR.strings.pref_show_nsfw_source),
subtitle = stringResource(MR.strings.requires_app_restart), subtitle = stringResource(MR.strings.requires_app_restart),
onValueChanged = { onValueChanged = {

View File

@@ -7,9 +7,7 @@ import android.net.Uri
import androidx.activity.compose.ManagedActivityResultLauncher import androidx.activity.compose.ManagedActivityResultLauncher
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth 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.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.HelpOutline 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.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MultiChoiceSegmentedButtonRow import androidx.compose.material3.MultiChoiceSegmentedButtonRow
@@ -26,15 +22,12 @@ import androidx.compose.material3.SegmentedButton
import androidx.compose.material3.SegmentedButtonDefaults import androidx.compose.material3.SegmentedButtonDefaults
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler 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.create.BackupCreateJob
import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob
import eu.kanade.tachiyomi.data.cache.ChapterCache 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.DeviceUtil
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.persistentMapOf import kotlinx.collections.immutable.persistentMapOf
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import logcat.LogPriority import logcat.LogPriority
import tachiyomi.core.common.i18n.stringResource import tachiyomi.core.common.i18n.stringResource
import tachiyomi.core.common.storage.displayablePath 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.core.common.util.system.logcat
import tachiyomi.domain.backup.service.BackupPreferences import tachiyomi.domain.backup.service.BackupPreferences
import tachiyomi.domain.library.service.LibraryPreferences 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.domain.storage.service.StoragePreferences
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.TextButton
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.collectAsState import tachiyomi.presentation.core.util.collectAsState
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
@@ -109,7 +95,6 @@ object SettingsDataScreen : SearchableSettings {
getBackupAndRestoreGroup(backupPreferences = backupPreferences), getBackupAndRestoreGroup(backupPreferences = backupPreferences),
getDataGroup(), getDataGroup(),
getExportGroup(),
) )
} }
@@ -254,7 +239,8 @@ object SettingsDataScreen : SearchableSettings {
// Automatic backups // Automatic backups
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
preference = backupPreferences.backupInterval(), pref = backupPreferences.backupInterval(),
title = stringResource(MR.strings.pref_backup_interval),
entries = persistentMapOf( entries = persistentMapOf(
0 to stringResource(MR.strings.off), 0 to stringResource(MR.strings.off),
6 to stringResource(MR.strings.update_6hour), 6 to stringResource(MR.strings.update_6hour),
@@ -263,7 +249,6 @@ object SettingsDataScreen : SearchableSettings {
48 to stringResource(MR.strings.update_48hour), 48 to stringResource(MR.strings.update_48hour),
168 to stringResource(MR.strings.update_weekly), 168 to stringResource(MR.strings.update_weekly),
), ),
title = stringResource(MR.strings.pref_backup_interval),
onValueChanged = { onValueChanged = {
BackupCreateJob.setupTask(context, it) BackupCreateJob.setupTask(context, it)
true true
@@ -321,147 +306,10 @@ object SettingsDataScreen : SearchableSettings {
}, },
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = libraryPreferences.autoClearChapterCache(), pref = libraryPreferences.autoClearChapterCache(),
title = stringResource(MR.strings.pref_auto_clear_chapter_cache), 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))
}
},
)
}
} }

View File

@@ -15,6 +15,7 @@ import eu.kanade.presentation.more.settings.widget.TriStateListDialog
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.persistentMapOf import kotlinx.collections.immutable.persistentMapOf
import kotlinx.collections.immutable.toImmutableMap import kotlinx.collections.immutable.toImmutableMap
import kotlinx.coroutines.runBlocking
import tachiyomi.domain.category.interactor.GetCategories import tachiyomi.domain.category.interactor.GetCategories
import tachiyomi.domain.category.model.Category import tachiyomi.domain.category.model.Category
import tachiyomi.domain.download.service.DownloadPreferences import tachiyomi.domain.download.service.DownloadPreferences
@@ -34,38 +35,23 @@ object SettingsDownloadScreen : SearchableSettings {
@Composable @Composable
override fun getPreferences(): List<Preference> { override fun getPreferences(): List<Preference> {
val getCategories = remember { Injekt.get<GetCategories>() } 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>() } val downloadPreferences = remember { Injekt.get<DownloadPreferences>() }
val parallelSourceLimit by downloadPreferences.parallelSourceLimit().collectAsState()
val parallelPageLimit by downloadPreferences.parallelPageLimit().collectAsState()
return listOf( return listOf(
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = downloadPreferences.downloadOnlyOverWifi(), pref = downloadPreferences.downloadOnlyOverWifi(),
title = stringResource(MR.strings.connected_to_wifi), title = stringResource(MR.strings.connected_to_wifi),
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = downloadPreferences.saveChaptersAsCBZ(), pref = downloadPreferences.saveChaptersAsCBZ(),
title = stringResource(MR.strings.save_chapter_as_cbz), title = stringResource(MR.strings.save_chapter_as_cbz),
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = downloadPreferences.splitTallImages(), pref = downloadPreferences.splitTallImages(),
title = stringResource(MR.strings.split_tall_images), title = stringResource(MR.strings.split_tall_images),
subtitle = stringResource(MR.strings.split_tall_images_summary), subtitle = stringResource(MR.strings.split_tall_images_summary),
), ),
Preference.PreferenceItem.SliderPreference(
value = parallelSourceLimit,
valueRange = 1..10,
title = stringResource(MR.strings.pref_download_concurrent_sources),
onValueChanged = { downloadPreferences.parallelSourceLimit().set(it) },
),
Preference.PreferenceItem.SliderPreference(
value = parallelPageLimit,
valueRange = 1..15,
title = stringResource(MR.strings.pref_download_concurrent_pages),
subtitle = stringResource(MR.strings.pref_download_concurrent_pages_summary),
onValueChanged = { downloadPreferences.parallelPageLimit().set(it) },
),
getDeleteChaptersGroup( getDeleteChaptersGroup(
downloadPreferences = downloadPreferences, downloadPreferences = downloadPreferences,
categories = allCategories, categories = allCategories,
@@ -87,11 +73,12 @@ object SettingsDownloadScreen : SearchableSettings {
title = stringResource(MR.strings.pref_category_delete_chapters), title = stringResource(MR.strings.pref_category_delete_chapters),
preferenceItems = persistentListOf( preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = downloadPreferences.removeAfterMarkedAsRead(), pref = downloadPreferences.removeAfterMarkedAsRead(),
title = stringResource(MR.strings.pref_remove_after_marked_as_read), title = stringResource(MR.strings.pref_remove_after_marked_as_read),
), ),
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
preference = downloadPreferences.removeAfterReadSlots(), pref = downloadPreferences.removeAfterReadSlots(),
title = stringResource(MR.strings.pref_remove_after_read),
entries = persistentMapOf( entries = persistentMapOf(
-1 to stringResource(MR.strings.disabled), -1 to stringResource(MR.strings.disabled),
0 to stringResource(MR.strings.last_read_chapter), 0 to stringResource(MR.strings.last_read_chapter),
@@ -100,10 +87,9 @@ object SettingsDownloadScreen : SearchableSettings {
3 to stringResource(MR.strings.fourth_to_last), 3 to stringResource(MR.strings.fourth_to_last),
4 to stringResource(MR.strings.fifth_to_last), 4 to stringResource(MR.strings.fifth_to_last),
), ),
title = stringResource(MR.strings.pref_remove_after_read),
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = downloadPreferences.removeBookmarkedChapters(), pref = downloadPreferences.removeBookmarkedChapters(),
title = stringResource(MR.strings.pref_remove_bookmarked_chapters), title = stringResource(MR.strings.pref_remove_bookmarked_chapters),
), ),
getExcludedCategoriesPreference( getExcludedCategoriesPreference(
@@ -120,11 +106,11 @@ object SettingsDownloadScreen : SearchableSettings {
categories: () -> List<Category>, categories: () -> List<Category>,
): Preference.PreferenceItem.MultiSelectListPreference { ): Preference.PreferenceItem.MultiSelectListPreference {
return Preference.PreferenceItem.MultiSelectListPreference( return Preference.PreferenceItem.MultiSelectListPreference(
preference = downloadPreferences.removeExcludeCategories(), pref = downloadPreferences.removeExcludeCategories(),
title = stringResource(MR.strings.pref_remove_exclude_categories),
entries = categories() entries = categories()
.associate { it.id.toString() to it.visualName } .associate { it.id.toString() to it.visualName }
.toImmutableMap(), .toImmutableMap(),
title = stringResource(MR.strings.pref_remove_exclude_categories),
) )
} }
@@ -164,11 +150,11 @@ object SettingsDownloadScreen : SearchableSettings {
title = stringResource(MR.strings.pref_category_auto_download), title = stringResource(MR.strings.pref_category_auto_download),
preferenceItems = persistentListOf( preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = downloadNewChaptersPref, pref = downloadNewChaptersPref,
title = stringResource(MR.strings.pref_download_new), title = stringResource(MR.strings.pref_download_new),
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = downloadNewUnreadChaptersOnlyPref, pref = downloadNewUnreadChaptersOnlyPref,
title = stringResource(MR.strings.pref_download_new_unread_chapters_only), title = stringResource(MR.strings.pref_download_new_unread_chapters_only),
enabled = downloadNewChapters, enabled = downloadNewChapters,
), ),
@@ -179,8 +165,8 @@ object SettingsDownloadScreen : SearchableSettings {
included = included, included = included,
excluded = excluded, excluded = excluded,
), ),
enabled = downloadNewChapters,
onClick = { showDialog = true }, onClick = { showDialog = true },
enabled = downloadNewChapters,
), ),
), ),
) )
@@ -194,7 +180,8 @@ object SettingsDownloadScreen : SearchableSettings {
title = stringResource(MR.strings.download_ahead), title = stringResource(MR.strings.download_ahead),
preferenceItems = persistentListOf( preferenceItems = persistentListOf(
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
preference = downloadPreferences.autoDownloadWhileReading(), pref = downloadPreferences.autoDownloadWhileReading(),
title = stringResource(MR.strings.auto_download_while_reading),
entries = listOf(0, 2, 3, 5, 10) entries = listOf(0, 2, 3, 5, 10)
.associateWith { .associateWith {
if (it == 0) { if (it == 0) {
@@ -204,7 +191,6 @@ object SettingsDownloadScreen : SearchableSettings {
} }
} }
.toImmutableMap(), .toImmutableMap(),
title = stringResource(MR.strings.auto_download_while_reading),
), ),
Preference.PreferenceItem.InfoPreference(stringResource(MR.strings.download_ahead_info)), Preference.PreferenceItem.InfoPreference(stringResource(MR.strings.download_ahead_info)),
), ),

View File

@@ -24,6 +24,7 @@ import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.persistentMapOf import kotlinx.collections.immutable.persistentMapOf
import kotlinx.collections.immutable.toImmutableMap import kotlinx.collections.immutable.toImmutableMap
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import tachiyomi.domain.category.interactor.GetCategories import tachiyomi.domain.category.interactor.GetCategories
import tachiyomi.domain.category.interactor.ResetCategoryFlags import tachiyomi.domain.category.interactor.ResetCategoryFlags
import tachiyomi.domain.category.model.Category 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_COMPLETED
import tachiyomi.domain.library.service.LibraryPreferences.Companion.MANGA_NON_READ 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.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.i18n.MR
import tachiyomi.presentation.core.i18n.pluralStringResource import tachiyomi.presentation.core.i18n.pluralStringResource
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
@@ -54,12 +53,12 @@ object SettingsLibraryScreen : SearchableSettings {
override fun getPreferences(): List<Preference> { override fun getPreferences(): List<Preference> {
val getCategories = remember { Injekt.get<GetCategories>() } val getCategories = remember { Injekt.get<GetCategories>() }
val libraryPreferences = remember { Injekt.get<LibraryPreferences>() } 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( return listOf(
getCategoriesGroup(LocalNavigator.currentOrThrow, allCategories, libraryPreferences), getCategoriesGroup(LocalNavigator.currentOrThrow, allCategories, libraryPreferences),
getGlobalUpdateGroup(allCategories, libraryPreferences), getGlobalUpdateGroup(allCategories, libraryPreferences),
getBehaviorGroup(libraryPreferences), getChapterSwipeActionsGroup(libraryPreferences),
) )
} }
@@ -91,12 +90,12 @@ object SettingsLibraryScreen : SearchableSettings {
onClick = { navigator.push(CategoryScreen()) }, onClick = { navigator.push(CategoryScreen()) },
), ),
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
preference = libraryPreferences.defaultCategory(), pref = libraryPreferences.defaultCategory(),
entries = ids.zip(labels).toMap().toImmutableMap(),
title = stringResource(MR.strings.default_category), title = stringResource(MR.strings.default_category),
entries = ids.zip(labels).toMap().toImmutableMap(),
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = libraryPreferences.categorizedDisplaySettings(), pref = libraryPreferences.categorizedDisplaySettings(),
title = stringResource(MR.strings.categorized_display_settings), title = stringResource(MR.strings.categorized_display_settings),
onValueChanged = { onValueChanged = {
if (!it) { if (!it) {
@@ -148,7 +147,8 @@ object SettingsLibraryScreen : SearchableSettings {
title = stringResource(MR.strings.pref_category_library_update), title = stringResource(MR.strings.pref_category_library_update),
preferenceItems = persistentListOf( preferenceItems = persistentListOf(
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
preference = autoUpdateIntervalPref, pref = autoUpdateIntervalPref,
title = stringResource(MR.strings.pref_library_update_interval),
entries = persistentMapOf( entries = persistentMapOf(
0 to stringResource(MR.strings.update_never), 0 to stringResource(MR.strings.update_never),
12 to stringResource(MR.strings.update_12hour), 12 to stringResource(MR.strings.update_12hour),
@@ -157,22 +157,21 @@ object SettingsLibraryScreen : SearchableSettings {
72 to stringResource(MR.strings.update_72hour), 72 to stringResource(MR.strings.update_72hour),
168 to stringResource(MR.strings.update_weekly), 168 to stringResource(MR.strings.update_weekly),
), ),
title = stringResource(MR.strings.pref_library_update_interval),
onValueChanged = { onValueChanged = {
LibraryUpdateJob.setupTask(context, it) LibraryUpdateJob.setupTask(context, it)
true true
}, },
), ),
Preference.PreferenceItem.MultiSelectListPreference( 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( entries = persistentMapOf(
DEVICE_ONLY_ON_WIFI to stringResource(MR.strings.connected_to_wifi), DEVICE_ONLY_ON_WIFI to stringResource(MR.strings.connected_to_wifi),
DEVICE_NETWORK_NOT_METERED to stringResource(MR.strings.network_not_metered), DEVICE_NETWORK_NOT_METERED to stringResource(MR.strings.network_not_metered),
DEVICE_CHARGING to stringResource(MR.strings.charging), DEVICE_CHARGING to stringResource(MR.strings.charging),
), ),
title = stringResource(MR.strings.pref_library_update_restriction),
subtitle = stringResource(MR.strings.restrictions),
enabled = autoUpdateInterval > 0,
onValueChanged = { onValueChanged = {
// Post to event looper to allow the preference to be updated. // Post to event looper to allow the preference to be updated.
ContextCompat.getMainExecutor(context).execute { LibraryUpdateJob.setupTask(context) } ContextCompat.getMainExecutor(context).execute { LibraryUpdateJob.setupTask(context) }
@@ -189,22 +188,22 @@ object SettingsLibraryScreen : SearchableSettings {
onClick = { showCategoriesDialog = true }, onClick = { showCategoriesDialog = true },
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = libraryPreferences.autoUpdateMetadata(), pref = libraryPreferences.autoUpdateMetadata(),
title = stringResource(MR.strings.pref_library_update_refresh_metadata), title = stringResource(MR.strings.pref_library_update_refresh_metadata),
subtitle = stringResource(MR.strings.pref_library_update_refresh_metadata_summary), subtitle = stringResource(MR.strings.pref_library_update_refresh_metadata_summary),
), ),
Preference.PreferenceItem.MultiSelectListPreference( Preference.PreferenceItem.MultiSelectListPreference(
preference = libraryPreferences.autoUpdateMangaRestrictions(), pref = libraryPreferences.autoUpdateMangaRestrictions(),
title = stringResource(MR.strings.pref_library_update_smart_update),
entries = persistentMapOf( entries = persistentMapOf(
MANGA_HAS_UNREAD to stringResource(MR.strings.pref_update_only_completely_read), 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_READ to stringResource(MR.strings.pref_update_only_started),
MANGA_NON_COMPLETED to stringResource(MR.strings.pref_update_only_non_completed), 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), 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.PreferenceItem.SwitchPreference(
preference = libraryPreferences.newShowUpdatesCount(), pref = libraryPreferences.newShowUpdatesCount(),
title = stringResource(MR.strings.pref_library_update_show_tab_badge), title = stringResource(MR.strings.pref_library_update_show_tab_badge),
), ),
), ),
@@ -212,14 +211,15 @@ object SettingsLibraryScreen : SearchableSettings {
} }
@Composable @Composable
private fun getBehaviorGroup( private fun getChapterSwipeActionsGroup(
libraryPreferences: LibraryPreferences, libraryPreferences: LibraryPreferences,
): Preference.PreferenceGroup { ): Preference.PreferenceGroup {
return Preference.PreferenceGroup( return Preference.PreferenceGroup(
title = stringResource(MR.strings.pref_behavior), title = stringResource(MR.strings.pref_chapter_swipe),
preferenceItems = persistentListOf( preferenceItems = persistentListOf(
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
preference = libraryPreferences.swipeToStartAction(), pref = libraryPreferences.swipeToStartAction(),
title = stringResource(MR.strings.pref_chapter_swipe_start),
entries = persistentMapOf( entries = persistentMapOf(
LibraryPreferences.ChapterSwipeAction.Disabled to LibraryPreferences.ChapterSwipeAction.Disabled to
stringResource(MR.strings.disabled), stringResource(MR.strings.disabled),
@@ -230,10 +230,10 @@ object SettingsLibraryScreen : SearchableSettings {
LibraryPreferences.ChapterSwipeAction.Download to LibraryPreferences.ChapterSwipeAction.Download to
stringResource(MR.strings.action_download), stringResource(MR.strings.action_download),
), ),
title = stringResource(MR.strings.pref_chapter_swipe_start),
), ),
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
preference = libraryPreferences.swipeToEndAction(), pref = libraryPreferences.swipeToEndAction(),
title = stringResource(MR.strings.pref_chapter_swipe_end),
entries = persistentMapOf( entries = persistentMapOf(
LibraryPreferences.ChapterSwipeAction.Disabled to LibraryPreferences.ChapterSwipeAction.Disabled to
stringResource(MR.strings.disabled), stringResource(MR.strings.disabled),
@@ -244,21 +244,6 @@ object SettingsLibraryScreen : SearchableSettings {
LibraryPreferences.ChapterSwipeAction.Download to LibraryPreferences.ChapterSwipeAction.Download to
stringResource(MR.strings.action_download), 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),
), ),
), ),
) )

View File

@@ -1,5 +1,6 @@
package eu.kanade.presentation.more.settings.screen package eu.kanade.presentation.more.settings.screen
import android.os.Build
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -9,7 +10,6 @@ import eu.kanade.presentation.more.settings.Preference
import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode
import eu.kanade.tachiyomi.util.system.hasDisplayCutout
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.persistentMapOf import kotlinx.collections.immutable.persistentMapOf
import kotlinx.collections.immutable.toImmutableMap import kotlinx.collections.immutable.toImmutableMap
@@ -33,33 +33,33 @@ object SettingsReaderScreen : SearchableSettings {
return listOf( return listOf(
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
preference = readerPref.defaultReadingMode(), pref = readerPref.defaultReadingMode(),
title = stringResource(MR.strings.pref_viewer_type),
entries = ReadingMode.entries.drop(1) entries = ReadingMode.entries.drop(1)
.associate { it.flagValue to stringResource(it.stringRes) } .associate { it.flagValue to stringResource(it.stringRes) }
.toImmutableMap(), .toImmutableMap(),
title = stringResource(MR.strings.pref_viewer_type),
), ),
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
preference = readerPref.doubleTapAnimSpeed(), pref = readerPref.doubleTapAnimSpeed(),
title = stringResource(MR.strings.pref_double_tap_anim_speed),
entries = persistentMapOf( entries = persistentMapOf(
1 to stringResource(MR.strings.double_tap_anim_speed_0), 1 to stringResource(MR.strings.double_tap_anim_speed_0),
500 to stringResource(MR.strings.double_tap_anim_speed_normal), 500 to stringResource(MR.strings.double_tap_anim_speed_normal),
250 to stringResource(MR.strings.double_tap_anim_speed_fast), 250 to stringResource(MR.strings.double_tap_anim_speed_fast),
), ),
title = stringResource(MR.strings.pref_double_tap_anim_speed),
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = readerPref.showReadingMode(), pref = readerPref.showReadingMode(),
title = stringResource(MR.strings.pref_show_reading_mode), title = stringResource(MR.strings.pref_show_reading_mode),
subtitle = stringResource(MR.strings.pref_show_reading_mode_summary), subtitle = stringResource(MR.strings.pref_show_reading_mode_summary),
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = readerPref.showNavigationOverlayOnStart(), pref = readerPref.showNavigationOverlayOnStart(),
title = stringResource(MR.strings.pref_show_navigation_mode), title = stringResource(MR.strings.pref_show_navigation_mode),
subtitle = stringResource(MR.strings.pref_show_navigation_mode_summary), subtitle = stringResource(MR.strings.pref_show_navigation_mode_summary),
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = readerPref.pageTransitions(), pref = readerPref.pageTransitions(),
title = stringResource(MR.strings.pref_page_transitions), title = stringResource(MR.strings.pref_page_transitions),
), ),
getDisplayGroup(readerPreferences = readerPref), getDisplayGroup(readerPreferences = readerPref),
@@ -80,37 +80,39 @@ object SettingsReaderScreen : SearchableSettings {
title = stringResource(MR.strings.pref_category_display), title = stringResource(MR.strings.pref_category_display),
preferenceItems = persistentListOf( preferenceItems = persistentListOf(
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
preference = readerPreferences.defaultOrientationType(), pref = readerPreferences.defaultOrientationType(),
title = stringResource(MR.strings.pref_rotation_type),
entries = ReaderOrientation.entries.drop(1) entries = ReaderOrientation.entries.drop(1)
.associate { it.flagValue to stringResource(it.stringRes) } .associate { it.flagValue to stringResource(it.stringRes) }
.toImmutableMap(), .toImmutableMap(),
title = stringResource(MR.strings.pref_rotation_type),
), ),
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
preference = readerPreferences.readerTheme(), pref = readerPreferences.readerTheme(),
title = stringResource(MR.strings.pref_reader_theme),
entries = persistentMapOf( entries = persistentMapOf(
1 to stringResource(MR.strings.black_background), 1 to stringResource(MR.strings.black_background),
2 to stringResource(MR.strings.gray_background), 2 to stringResource(MR.strings.gray_background),
0 to stringResource(MR.strings.white_background), 0 to stringResource(MR.strings.white_background),
3 to stringResource(MR.strings.automatic_background), 3 to stringResource(MR.strings.automatic_background),
), ),
title = stringResource(MR.strings.pref_reader_theme),
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = fullscreenPref, pref = fullscreenPref,
title = stringResource(MR.strings.pref_fullscreen), title = stringResource(MR.strings.pref_fullscreen),
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = readerPreferences.drawUnderCutout(), pref = readerPreferences.cutoutShort(),
title = stringResource(MR.strings.pref_cutout_short), title = stringResource(MR.strings.pref_cutout_short),
enabled = LocalView.current.hasDisplayCutout() && fullscreen, enabled = fullscreen &&
Build.VERSION.SDK_INT >= Build.VERSION_CODES.P &&
LocalView.current.rootWindowInsets?.displayCutout != null, // has cutout
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = readerPreferences.keepScreenOn(), pref = readerPreferences.keepScreenOn(),
title = stringResource(MR.strings.pref_keep_screen_on), title = stringResource(MR.strings.pref_keep_screen_on),
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = readerPreferences.showPageNumber(), pref = readerPreferences.showPageNumber(),
title = stringResource(MR.strings.pref_show_page_number), title = stringResource(MR.strings.pref_show_page_number),
), ),
), ),
@@ -133,35 +135,43 @@ object SettingsReaderScreen : SearchableSettings {
title = "E-Ink", title = "E-Ink",
preferenceItems = persistentListOf( preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = readerPreferences.flashOnPageChange(), pref = readerPreferences.flashOnPageChange(),
title = stringResource(MR.strings.pref_flash_page), title = stringResource(MR.strings.pref_flash_page),
subtitle = stringResource(MR.strings.pref_flash_page_summ), subtitle = stringResource(MR.strings.pref_flash_page_summ),
), ),
Preference.PreferenceItem.SliderPreference( Preference.PreferenceItem.SliderPreference(
value = flashMillis / ReaderPreferences.MILLI_CONVERSION, value = flashMillis / ReaderPreferences.MILLI_CONVERSION,
valueRange = 1..15, min = 1,
max = 15,
title = stringResource(MR.strings.pref_flash_duration), title = stringResource(MR.strings.pref_flash_duration),
valueString = stringResource(MR.strings.pref_flash_duration_summary, flashMillis), subtitle = stringResource(MR.strings.pref_flash_duration_summary, flashMillis),
onValueChanged = {
flashMillisPref.set(it * ReaderPreferences.MILLI_CONVERSION)
true
},
enabled = flashPageState, enabled = flashPageState,
onValueChanged = { flashMillisPref.set(it * ReaderPreferences.MILLI_CONVERSION) },
), ),
Preference.PreferenceItem.SliderPreference( Preference.PreferenceItem.SliderPreference(
value = flashInterval, value = flashInterval,
valueRange = 1..10, min = 1,
max = 10,
title = stringResource(MR.strings.pref_flash_page_interval), title = stringResource(MR.strings.pref_flash_page_interval),
valueString = pluralStringResource(MR.plurals.pref_pages, flashInterval, flashInterval), subtitle = pluralStringResource(MR.plurals.pref_pages, flashInterval, flashInterval),
onValueChanged = {
flashIntervalPref.set(it)
true
},
enabled = flashPageState, enabled = flashPageState,
onValueChanged = { flashIntervalPref.set(it) },
), ),
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
preference = flashColorPref, pref = flashColorPref,
title = stringResource(MR.strings.pref_flash_with),
entries = persistentMapOf( entries = persistentMapOf(
ReaderPreferences.FlashColor.BLACK to stringResource(MR.strings.pref_flash_style_black), 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 to stringResource(MR.strings.pref_flash_style_white),
ReaderPreferences.FlashColor.WHITE_BLACK ReaderPreferences.FlashColor.WHITE_BLACK
to stringResource(MR.strings.pref_flash_style_white_black), to stringResource(MR.strings.pref_flash_style_white_black),
), ),
title = stringResource(MR.strings.pref_flash_with),
enabled = flashPageState, enabled = flashPageState,
), ),
), ),
@@ -174,19 +184,19 @@ object SettingsReaderScreen : SearchableSettings {
title = stringResource(MR.strings.pref_category_reading), title = stringResource(MR.strings.pref_category_reading),
preferenceItems = persistentListOf( preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = readerPreferences.skipRead(), pref = readerPreferences.skipRead(),
title = stringResource(MR.strings.pref_skip_read_chapters), title = stringResource(MR.strings.pref_skip_read_chapters),
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = readerPreferences.skipFiltered(), pref = readerPreferences.skipFiltered(),
title = stringResource(MR.strings.pref_skip_filtered_chapters), title = stringResource(MR.strings.pref_skip_filtered_chapters),
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = readerPreferences.skipDupe(), pref = readerPreferences.skipDupe(),
title = stringResource(MR.strings.pref_skip_dupe_chapters), title = stringResource(MR.strings.pref_skip_dupe_chapters),
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = readerPreferences.alwaysShowChapterTransition(), pref = readerPreferences.alwaysShowChapterTransition(),
title = stringResource(MR.strings.pref_always_show_chapter_transition), title = stringResource(MR.strings.pref_always_show_chapter_transition),
), ),
), ),
@@ -209,15 +219,16 @@ object SettingsReaderScreen : SearchableSettings {
title = stringResource(MR.strings.pager_viewer), title = stringResource(MR.strings.pager_viewer),
preferenceItems = persistentListOf( preferenceItems = persistentListOf(
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
preference = navModePref, pref = navModePref,
title = stringResource(MR.strings.pref_viewer_nav),
entries = ReaderPreferences.TapZones entries = ReaderPreferences.TapZones
.mapIndexed { index, it -> index to stringResource(it) } .mapIndexed { index, it -> index to stringResource(it) }
.toMap() .toMap()
.toImmutableMap(), .toImmutableMap(),
title = stringResource(MR.strings.pref_viewer_nav),
), ),
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
preference = readerPreferences.pagerNavInverted(), pref = readerPreferences.pagerNavInverted(),
title = stringResource(MR.strings.pref_read_with_tapping_inverted),
entries = persistentListOf( entries = persistentListOf(
ReaderPreferences.TappingInvertMode.NONE, ReaderPreferences.TappingInvertMode.NONE,
ReaderPreferences.TappingInvertMode.HORIZONTAL, ReaderPreferences.TappingInvertMode.HORIZONTAL,
@@ -226,41 +237,40 @@ object SettingsReaderScreen : SearchableSettings {
) )
.associateWith { stringResource(it.titleRes) } .associateWith { stringResource(it.titleRes) }
.toImmutableMap(), .toImmutableMap(),
title = stringResource(MR.strings.pref_read_with_tapping_inverted),
enabled = navMode != 5, enabled = navMode != 5,
), ),
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
preference = imageScaleTypePref, pref = imageScaleTypePref,
title = stringResource(MR.strings.pref_image_scale_type),
entries = ReaderPreferences.ImageScaleType entries = ReaderPreferences.ImageScaleType
.mapIndexed { index, it -> index + 1 to stringResource(it) } .mapIndexed { index, it -> index + 1 to stringResource(it) }
.toMap() .toMap()
.toImmutableMap(), .toImmutableMap(),
title = stringResource(MR.strings.pref_image_scale_type),
), ),
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
preference = readerPreferences.zoomStart(), pref = readerPreferences.zoomStart(),
title = stringResource(MR.strings.pref_zoom_start),
entries = ReaderPreferences.ZoomStart entries = ReaderPreferences.ZoomStart
.mapIndexed { index, it -> index + 1 to stringResource(it) } .mapIndexed { index, it -> index + 1 to stringResource(it) }
.toMap() .toMap()
.toImmutableMap(), .toImmutableMap(),
title = stringResource(MR.strings.pref_zoom_start),
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = readerPreferences.cropBorders(), pref = readerPreferences.cropBorders(),
title = stringResource(MR.strings.pref_crop_borders), title = stringResource(MR.strings.pref_crop_borders),
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = readerPreferences.landscapeZoom(), pref = readerPreferences.landscapeZoom(),
title = stringResource(MR.strings.pref_landscape_zoom), title = stringResource(MR.strings.pref_landscape_zoom),
enabled = imageScaleType == 1, enabled = imageScaleType == 1,
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = readerPreferences.navigateToPan(), pref = readerPreferences.navigateToPan(),
title = stringResource(MR.strings.pref_navigate_pan), title = stringResource(MR.strings.pref_navigate_pan),
enabled = navMode != 5, enabled = navMode != 5,
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = dualPageSplitPref, pref = dualPageSplitPref,
title = stringResource(MR.strings.pref_dual_page_split), title = stringResource(MR.strings.pref_dual_page_split),
onValueChanged = { onValueChanged = {
rotateToFitPref.set(false) rotateToFitPref.set(false)
@@ -268,13 +278,13 @@ object SettingsReaderScreen : SearchableSettings {
}, },
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = readerPreferences.dualPageInvertPaged(), pref = readerPreferences.dualPageInvertPaged(),
title = stringResource(MR.strings.pref_dual_page_invert), title = stringResource(MR.strings.pref_dual_page_invert),
subtitle = stringResource(MR.strings.pref_dual_page_invert_summary), subtitle = stringResource(MR.strings.pref_dual_page_invert_summary),
enabled = dualPageSplit, enabled = dualPageSplit,
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = rotateToFitPref, pref = rotateToFitPref,
title = stringResource(MR.strings.pref_page_rotate), title = stringResource(MR.strings.pref_page_rotate),
onValueChanged = { onValueChanged = {
dualPageSplitPref.set(false) dualPageSplitPref.set(false)
@@ -282,7 +292,7 @@ object SettingsReaderScreen : SearchableSettings {
}, },
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = readerPreferences.dualPageRotateToFitInvert(), pref = readerPreferences.dualPageRotateToFitInvert(),
title = stringResource(MR.strings.pref_page_rotate_invert), title = stringResource(MR.strings.pref_page_rotate_invert),
enabled = rotateToFit, enabled = rotateToFit,
), ),
@@ -308,15 +318,16 @@ object SettingsReaderScreen : SearchableSettings {
title = stringResource(MR.strings.webtoon_viewer), title = stringResource(MR.strings.webtoon_viewer),
preferenceItems = persistentListOf( preferenceItems = persistentListOf(
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
preference = navModePref, pref = navModePref,
title = stringResource(MR.strings.pref_viewer_nav),
entries = ReaderPreferences.TapZones entries = ReaderPreferences.TapZones
.mapIndexed { index, it -> index to stringResource(it) } .mapIndexed { index, it -> index to stringResource(it) }
.toMap() .toMap()
.toImmutableMap(), .toImmutableMap(),
title = stringResource(MR.strings.pref_viewer_nav),
), ),
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
preference = readerPreferences.webtoonNavInverted(), pref = readerPreferences.webtoonNavInverted(),
title = stringResource(MR.strings.pref_read_with_tapping_inverted),
entries = persistentListOf( entries = persistentListOf(
ReaderPreferences.TappingInvertMode.NONE, ReaderPreferences.TappingInvertMode.NONE,
ReaderPreferences.TappingInvertMode.HORIZONTAL, ReaderPreferences.TappingInvertMode.HORIZONTAL,
@@ -325,34 +336,35 @@ object SettingsReaderScreen : SearchableSettings {
) )
.associateWith { stringResource(it.titleRes) } .associateWith { stringResource(it.titleRes) }
.toImmutableMap(), .toImmutableMap(),
title = stringResource(MR.strings.pref_read_with_tapping_inverted),
enabled = navMode != 5, enabled = navMode != 5,
), ),
Preference.PreferenceItem.SliderPreference( Preference.PreferenceItem.SliderPreference(
value = webtoonSidePadding, value = webtoonSidePadding,
valueRange = ReaderPreferences.let {
it.WEBTOON_PADDING_MIN..it.WEBTOON_PADDING_MAX
},
title = stringResource(MR.strings.pref_webtoon_side_padding), title = stringResource(MR.strings.pref_webtoon_side_padding),
valueString = numberFormat.format(webtoonSidePadding / 100f), subtitle = numberFormat.format(webtoonSidePadding / 100f),
onValueChanged = { webtoonSidePaddingPref.set(it) }, min = ReaderPreferences.WEBTOON_PADDING_MIN,
max = ReaderPreferences.WEBTOON_PADDING_MAX,
onValueChanged = {
webtoonSidePaddingPref.set(it)
true
},
), ),
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
preference = readerPreferences.readerHideThreshold(), pref = readerPreferences.readerHideThreshold(),
title = stringResource(MR.strings.pref_hide_threshold),
entries = persistentMapOf( entries = persistentMapOf(
ReaderPreferences.ReaderHideThreshold.HIGHEST to stringResource(MR.strings.pref_highest), ReaderPreferences.ReaderHideThreshold.HIGHEST to stringResource(MR.strings.pref_highest),
ReaderPreferences.ReaderHideThreshold.HIGH to stringResource(MR.strings.pref_high), ReaderPreferences.ReaderHideThreshold.HIGH to stringResource(MR.strings.pref_high),
ReaderPreferences.ReaderHideThreshold.LOW to stringResource(MR.strings.pref_low), ReaderPreferences.ReaderHideThreshold.LOW to stringResource(MR.strings.pref_low),
ReaderPreferences.ReaderHideThreshold.LOWEST to stringResource(MR.strings.pref_lowest), ReaderPreferences.ReaderHideThreshold.LOWEST to stringResource(MR.strings.pref_lowest),
), ),
title = stringResource(MR.strings.pref_hide_threshold),
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = readerPreferences.cropBordersWebtoon(), pref = readerPreferences.cropBordersWebtoon(),
title = stringResource(MR.strings.pref_crop_borders), title = stringResource(MR.strings.pref_crop_borders),
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = dualPageSplitPref, pref = dualPageSplitPref,
title = stringResource(MR.strings.pref_dual_page_split), title = stringResource(MR.strings.pref_dual_page_split),
onValueChanged = { onValueChanged = {
rotateToFitPref.set(false) rotateToFitPref.set(false)
@@ -360,13 +372,13 @@ object SettingsReaderScreen : SearchableSettings {
}, },
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = readerPreferences.dualPageInvertWebtoon(), pref = readerPreferences.dualPageInvertWebtoon(),
title = stringResource(MR.strings.pref_dual_page_invert), title = stringResource(MR.strings.pref_dual_page_invert),
subtitle = stringResource(MR.strings.pref_dual_page_invert_summary), subtitle = stringResource(MR.strings.pref_dual_page_invert_summary),
enabled = dualPageSplit, enabled = dualPageSplit,
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = rotateToFitPref, pref = rotateToFitPref,
title = stringResource(MR.strings.pref_page_rotate), title = stringResource(MR.strings.pref_page_rotate),
onValueChanged = { onValueChanged = {
dualPageSplitPref.set(false) dualPageSplitPref.set(false)
@@ -374,16 +386,16 @@ object SettingsReaderScreen : SearchableSettings {
}, },
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = readerPreferences.dualPageRotateToFitInvertWebtoon(), pref = readerPreferences.dualPageRotateToFitInvertWebtoon(),
title = stringResource(MR.strings.pref_page_rotate_invert), title = stringResource(MR.strings.pref_page_rotate_invert),
enabled = rotateToFit, enabled = rotateToFit,
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = readerPreferences.webtoonDoubleTapZoomEnabled(), pref = readerPreferences.webtoonDoubleTapZoomEnabled(),
title = stringResource(MR.strings.pref_double_tap_zoom), title = stringResource(MR.strings.pref_double_tap_zoom),
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = readerPreferences.webtoonDisableZoomOut(), pref = readerPreferences.webtoonDisableZoomOut(),
title = stringResource(MR.strings.pref_webtoon_disable_zoom_out), title = stringResource(MR.strings.pref_webtoon_disable_zoom_out),
), ),
), ),
@@ -398,11 +410,11 @@ object SettingsReaderScreen : SearchableSettings {
title = stringResource(MR.strings.pref_reader_navigation), title = stringResource(MR.strings.pref_reader_navigation),
preferenceItems = persistentListOf( preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = readWithVolumeKeysPref, pref = readWithVolumeKeysPref,
title = stringResource(MR.strings.pref_read_with_volume_keys), title = stringResource(MR.strings.pref_read_with_volume_keys),
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = readerPreferences.readWithVolumeKeysInverted(), pref = readerPreferences.readWithVolumeKeysInverted(),
title = stringResource(MR.strings.pref_read_with_volume_keys_inverted), title = stringResource(MR.strings.pref_read_with_volume_keys_inverted),
enabled = readWithVolumeKeys, enabled = readWithVolumeKeys,
), ),
@@ -416,11 +428,11 @@ object SettingsReaderScreen : SearchableSettings {
title = stringResource(MR.strings.pref_reader_actions), title = stringResource(MR.strings.pref_reader_actions),
preferenceItems = persistentListOf( preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = readerPreferences.readWithLongTap(), pref = readerPreferences.readWithLongTap(),
title = stringResource(MR.strings.pref_read_with_long_tap), title = stringResource(MR.strings.pref_read_with_long_tap),
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = readerPreferences.folderPerManga(), pref = readerPreferences.folderPerManga(),
title = stringResource(MR.strings.pref_create_folder_per_manga), title = stringResource(MR.strings.pref_create_folder_per_manga),
subtitle = stringResource(MR.strings.pref_create_folder_per_manga_summary), subtitle = stringResource(MR.strings.pref_create_folder_per_manga_summary),
), ),

View File

@@ -183,7 +183,7 @@ private fun SearchResult(
emptySequence() emptySequence()
} }
} }
is Preference.PreferenceItem<*, *> -> sequenceOf(null to p) is Preference.PreferenceItem<*> -> sequenceOf(null to p)
} }
} }
// Don't show info preference // Don't show info preference

View File

@@ -7,11 +7,9 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import eu.kanade.presentation.more.settings.Preference 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.core.security.SecurityPreferences
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil.authenticate import eu.kanade.tachiyomi.util.system.AuthenticatorUtil.authenticate
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil.isAuthenticationSupported import eu.kanade.tachiyomi.util.system.AuthenticatorUtil.isAuthenticationSupported
import eu.kanade.tachiyomi.util.system.telemetryIncluded
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableMap import kotlinx.collections.immutable.toImmutableMap
import tachiyomi.core.common.i18n.stringResource import tachiyomi.core.common.i18n.stringResource
@@ -30,92 +28,55 @@ object SettingsSecurityScreen : SearchableSettings {
@Composable @Composable
override fun getPreferences(): List<Preference> { 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 context = LocalContext.current
val securityPreferences = remember { Injekt.get<SecurityPreferences>() }
val authSupported = remember { context.isAuthenticationSupported() } val authSupported = remember { context.isAuthenticationSupported() }
val useAuthPref = securityPreferences.useAuthenticator() val useAuthPref = securityPreferences.useAuthenticator()
val useAuth by useAuthPref.collectAsState() val useAuth by useAuthPref.collectAsState()
return Preference.PreferenceGroup( return listOf(
title = stringResource(MR.strings.pref_security), Preference.PreferenceItem.SwitchPreference(
preferenceItems = persistentListOf( pref = useAuthPref,
Preference.PreferenceItem.SwitchPreference( title = stringResource(MR.strings.lock_with_biometrics),
preference = useAuthPref, enabled = authSupported,
title = stringResource(MR.strings.lock_with_biometrics), onValueChanged = {
enabled = authSupported, (context as FragmentActivity).authenticate(
onValueChanged = { title = context.stringResource(MR.strings.lock_with_biometrics),
(context as FragmentActivity).authenticate( )
title = context.stringResource(MR.strings.lock_with_biometrics), },
) ),
}, Preference.PreferenceItem.ListPreference(
), pref = securityPreferences.lockAppAfter(),
Preference.PreferenceItem.ListPreference( title = stringResource(MR.strings.lock_when_idle),
preference = securityPreferences.lockAppAfter(), enabled = authSupported && useAuth,
entries = LockAfterValues entries = LockAfterValues
.associateWith { .associateWith {
when (it) { when (it) {
-1 -> stringResource(MR.strings.lock_never) -1 -> stringResource(MR.strings.lock_never)
0 -> stringResource(MR.strings.lock_always) 0 -> stringResource(MR.strings.lock_always)
else -> pluralStringResource(MR.plurals.lock_after_mins, count = it, it) else -> pluralStringResource(MR.plurals.lock_after_mins, count = it, it)
}
} }
.toImmutableMap(), }
title = stringResource(MR.strings.lock_when_idle), .toImmutableMap(),
enabled = authSupported && useAuth, onValueChanged = {
onValueChanged = { (context as FragmentActivity).authenticate(
(context as FragmentActivity).authenticate( title = context.stringResource(MR.strings.lock_when_idle),
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)),
), ),
) Preference.PreferenceItem.SwitchPreference(
} pref = securityPreferences.hideNotificationContent(),
title = stringResource(MR.strings.hide_notification_content),
@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.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)),
) )
} }
} }

View File

@@ -30,11 +30,8 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.autofill.ContentType
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler 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.ImeAction
import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation 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.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import dev.icerock.moko.resources.StringResource import dev.icerock.moko.resources.StringResource
import eu.kanade.domain.track.model.AutoTrackState
import eu.kanade.domain.track.service.TrackPreferences import eu.kanade.domain.track.service.TrackPreferences
import eu.kanade.presentation.more.settings.Preference import eu.kanade.presentation.more.settings.Preference
import eu.kanade.tachiyomi.data.track.EnhancedTracker 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 eu.kanade.tachiyomi.util.system.toast
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toPersistentMap
import tachiyomi.core.common.util.lang.launchIO import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.core.common.util.lang.withUIContext import tachiyomi.core.common.util.lang.withUIContext
import tachiyomi.domain.source.service.SourceManager import tachiyomi.domain.source.service.SourceManager
@@ -90,7 +85,6 @@ object SettingsTrackingScreen : SearchableSettings {
val trackPreferences = remember { Injekt.get<TrackPreferences>() } val trackPreferences = remember { Injekt.get<TrackPreferences>() }
val trackerManager = remember { Injekt.get<TrackerManager>() } val trackerManager = remember { Injekt.get<TrackerManager>() }
val sourceManager = remember { Injekt.get<SourceManager>() } val sourceManager = remember { Injekt.get<SourceManager>() }
val autoTrackStatePref = trackPreferences.autoUpdateTrackOnMarkRead()
var dialog by remember { mutableStateOf<Any?>(null) } var dialog by remember { mutableStateOf<Any?>(null) }
dialog?.run { dialog?.run {
@@ -128,45 +122,44 @@ object SettingsTrackingScreen : SearchableSettings {
return listOf( return listOf(
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = trackPreferences.autoUpdateTrack(), pref = trackPreferences.autoUpdateTrack(),
title = stringResource(MR.strings.pref_auto_update_manga_sync), 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( Preference.PreferenceGroup(
title = stringResource(MR.strings.services), title = stringResource(MR.strings.services),
preferenceItems = persistentListOf( preferenceItems = persistentListOf(
Preference.PreferenceItem.TrackerPreference( Preference.PreferenceItem.TrackerPreference(
title = trackerManager.myAnimeList.name,
tracker = trackerManager.myAnimeList, tracker = trackerManager.myAnimeList,
login = { context.openInBrowser(MyAnimeListApi.authUrl(), forceDefaultBrowser = true) }, login = { context.openInBrowser(MyAnimeListApi.authUrl(), forceDefaultBrowser = true) },
logout = { dialog = LogoutDialog(trackerManager.myAnimeList) }, logout = { dialog = LogoutDialog(trackerManager.myAnimeList) },
), ),
Preference.PreferenceItem.TrackerPreference( Preference.PreferenceItem.TrackerPreference(
title = trackerManager.aniList.name,
tracker = trackerManager.aniList, tracker = trackerManager.aniList,
login = { context.openInBrowser(AnilistApi.authUrl(), forceDefaultBrowser = true) }, login = { context.openInBrowser(AnilistApi.authUrl(), forceDefaultBrowser = true) },
logout = { dialog = LogoutDialog(trackerManager.aniList) }, logout = { dialog = LogoutDialog(trackerManager.aniList) },
), ),
Preference.PreferenceItem.TrackerPreference( Preference.PreferenceItem.TrackerPreference(
title = trackerManager.kitsu.name,
tracker = trackerManager.kitsu, tracker = trackerManager.kitsu,
login = { dialog = LoginDialog(trackerManager.kitsu, MR.strings.email) }, login = { dialog = LoginDialog(trackerManager.kitsu, MR.strings.email) },
logout = { dialog = LogoutDialog(trackerManager.kitsu) }, logout = { dialog = LogoutDialog(trackerManager.kitsu) },
), ),
Preference.PreferenceItem.TrackerPreference( Preference.PreferenceItem.TrackerPreference(
title = trackerManager.mangaUpdates.name,
tracker = trackerManager.mangaUpdates, tracker = trackerManager.mangaUpdates,
login = { dialog = LoginDialog(trackerManager.mangaUpdates, MR.strings.username) }, login = { dialog = LoginDialog(trackerManager.mangaUpdates, MR.strings.username) },
logout = { dialog = LogoutDialog(trackerManager.mangaUpdates) }, logout = { dialog = LogoutDialog(trackerManager.mangaUpdates) },
), ),
Preference.PreferenceItem.TrackerPreference( Preference.PreferenceItem.TrackerPreference(
title = trackerManager.shikimori.name,
tracker = trackerManager.shikimori, tracker = trackerManager.shikimori,
login = { context.openInBrowser(ShikimoriApi.authUrl(), forceDefaultBrowser = true) }, login = { context.openInBrowser(ShikimoriApi.authUrl(), forceDefaultBrowser = true) },
logout = { dialog = LogoutDialog(trackerManager.shikimori) }, logout = { dialog = LogoutDialog(trackerManager.shikimori) },
), ),
Preference.PreferenceItem.TrackerPreference( Preference.PreferenceItem.TrackerPreference(
title = trackerManager.bangumi.name,
tracker = trackerManager.bangumi, tracker = trackerManager.bangumi,
login = { context.openInBrowser(BangumiApi.authUrl(), forceDefaultBrowser = true) }, login = { context.openInBrowser(BangumiApi.authUrl(), forceDefaultBrowser = true) },
logout = { dialog = LogoutDialog(trackerManager.bangumi) }, logout = { dialog = LogoutDialog(trackerManager.bangumi) },
@@ -180,6 +173,7 @@ object SettingsTrackingScreen : SearchableSettings {
enhancedTrackers.first enhancedTrackers.first
.map { service -> .map { service ->
Preference.PreferenceItem.TrackerPreference( Preference.PreferenceItem.TrackerPreference(
title = service.name,
tracker = service, tracker = service,
login = { (service as EnhancedTracker).loginNoop() }, login = { (service as EnhancedTracker).loginNoop() },
logout = service::logout, logout = service::logout,
@@ -223,9 +217,7 @@ object SettingsTrackingScreen : SearchableSettings {
text = { text = {
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
OutlinedTextField( OutlinedTextField(
modifier = Modifier modifier = Modifier.fillMaxWidth(),
.fillMaxWidth()
.semantics { contentType = ContentType.Username + ContentType.EmailAddress },
value = username, value = username,
onValueChange = { username = it }, onValueChange = { username = it },
label = { Text(text = stringResource(uNameStringRes)) }, label = { Text(text = stringResource(uNameStringRes)) },
@@ -236,9 +228,7 @@ object SettingsTrackingScreen : SearchableSettings {
var hidePassword by remember { mutableStateOf(true) } var hidePassword by remember { mutableStateOf(true) }
OutlinedTextField( OutlinedTextField(
modifier = Modifier modifier = Modifier.fillMaxWidth(),
.fillMaxWidth()
.semantics { contentType = ContentType.Password },
value = password, value = password,
onValueChange = { password = it }, onValueChange = { password = it },
label = { Text(text = stringResource(MR.strings.password)) }, 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)) Text(text = stringResource(id))
} }
}, },

View File

@@ -35,9 +35,7 @@ import eu.kanade.tachiyomi.ui.more.NewUpdateScreen
import eu.kanade.tachiyomi.util.CrashLogUtil import eu.kanade.tachiyomi.util.CrashLogUtil
import eu.kanade.tachiyomi.util.lang.toDateTimestampString import eu.kanade.tachiyomi.util.lang.toDateTimestampString
import eu.kanade.tachiyomi.util.system.copyToClipboard 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.toast
import eu.kanade.tachiyomi.util.system.updaterEnabled
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import logcat.LogPriority import logcat.LogPriority
import tachiyomi.core.common.util.lang.withIOContext import tachiyomi.core.common.util.lang.withIOContext
@@ -99,7 +97,7 @@ object AboutScreen : Screen() {
) )
} }
if (updaterEnabled) { if (BuildConfig.INCLUDE_UPDATER) {
item { item {
TextPreferenceWidget( TextPreferenceWidget(
title = stringResource(MR.strings.check_for_updates), title = stringResource(MR.strings.check_for_updates),
@@ -123,7 +121,7 @@ object AboutScreen : Screen() {
versionName = result.release.version, versionName = result.release.version,
changelogInfo = result.release.info, changelogInfo = result.release.info,
releaseLink = result.release.releaseLink, releaseLink = result.release.releaseLink,
downloadLink = result.release.downloadLink, downloadLink = result.release.getDownloadLink(),
) )
navigator.push(updateScreen) navigator.push(updateScreen)
}, },
@@ -247,7 +245,7 @@ object AboutScreen : Screen() {
} }
} }
} }
isPreviewBuildType -> { BuildConfig.PREVIEW -> {
"Beta r${BuildConfig.COMMIT_COUNT}".let { "Beta r${BuildConfig.COMMIT_COUNT}".let {
if (withBuildDate) { if (withBuildDate) {
"$it (${BuildConfig.COMMIT_SHA}, ${getFormattedBuildTime()})" "$it (${BuildConfig.COMMIT_SHA}, ${getFormattedBuildTime()})"

View File

@@ -6,7 +6,7 @@ import androidx.compose.ui.Modifier
import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.currentOrThrow
import com.mikepenz.aboutlibraries.ui.compose.m3.LibrariesContainer 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.components.AppBar
import eu.kanade.presentation.util.Screen import eu.kanade.presentation.util.Screen
import tachiyomi.i18n.MR import tachiyomi.i18n.MR

View File

@@ -1,10 +1,8 @@
package eu.kanade.presentation.more.settings.screen.advanced package eu.kanade.presentation.more.settings.screen.advanced
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.items 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.AlertDialog
import androidx.compose.material3.Checkbox import androidx.compose.material3.Checkbox
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Switch
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
@@ -45,7 +39,6 @@ import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import tachiyomi.core.common.util.lang.launchIO import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.core.common.util.lang.launchUI import tachiyomi.core.common.util.lang.launchUI
import tachiyomi.core.common.util.lang.toLong
import tachiyomi.core.common.util.lang.withNonCancellableContext import tachiyomi.core.common.util.lang.withNonCancellableContext
import tachiyomi.data.Database import tachiyomi.data.Database
import tachiyomi.domain.source.interactor.GetSourcesWithNonLibraryManga import tachiyomi.domain.source.interactor.GetSourcesWithNonLibraryManga
@@ -54,7 +47,6 @@ import tachiyomi.domain.source.model.SourceWithCount
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.LazyColumnWithAction import tachiyomi.presentation.core.components.LazyColumnWithAction
import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.screens.EmptyScreen import tachiyomi.presentation.core.screens.EmptyScreen
import tachiyomi.presentation.core.screens.LoadingScreen import tachiyomi.presentation.core.screens.LoadingScreen
@@ -76,45 +68,13 @@ class ClearDatabaseScreen : Screen() {
is ClearDatabaseScreenModel.State.Loading -> LoadingScreen() is ClearDatabaseScreenModel.State.Loading -> LoadingScreen()
is ClearDatabaseScreenModel.State.Ready -> { is ClearDatabaseScreenModel.State.Ready -> {
if (s.showConfirmation) { if (s.showConfirmation) {
var keepReadManga by remember { mutableStateOf(true) }
AlertDialog( 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, onDismissRequest = model::hideConfirmation,
confirmButton = { confirmButton = {
TextButton( TextButton(
onClick = { onClick = {
scope.launchUI { scope.launchUI {
model.removeMangaBySourceId(keepReadManga) model.removeMangaBySourceId()
model.clearSelection() model.clearSelection()
model.hideConfirmation() model.hideConfirmation()
context.toast(MR.strings.clear_database_completed) context.toast(MR.strings.clear_database_completed)
@@ -129,6 +89,9 @@ class ClearDatabaseScreen : Screen() {
Text(text = stringResource(MR.strings.action_cancel)) 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 val state = state.value as? State.Ready ?: return@withNonCancellableContext
database.mangasQueries.deleteNonLibraryManga(state.selection, keepReadManga.toLong()) database.mangasQueries.deleteMangasNotInLibraryBySourceIds(state.selection)
database.historyQueries.removeResettedHistory() database.historyQueries.removeResettedHistory()
} }

View File

@@ -4,7 +4,6 @@ import androidx.compose.runtime.Immutable
import cafe.adriel.voyager.core.model.StateScreenModel import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.screenModelScope import cafe.adriel.voyager.core.model.screenModelScope
import dev.icerock.moko.resources.StringResource import dev.icerock.moko.resources.StringResource
import eu.kanade.tachiyomi.extension.ExtensionManager
import kotlinx.collections.immutable.ImmutableSet import kotlinx.collections.immutable.ImmutableSet
import kotlinx.collections.immutable.toImmutableSet import kotlinx.collections.immutable.toImmutableSet
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
@@ -28,7 +27,6 @@ class ExtensionReposScreenModel(
private val deleteExtensionRepo: DeleteExtensionRepo = Injekt.get(), private val deleteExtensionRepo: DeleteExtensionRepo = Injekt.get(),
private val replaceExtensionRepo: ReplaceExtensionRepo = Injekt.get(), private val replaceExtensionRepo: ReplaceExtensionRepo = Injekt.get(),
private val updateExtensionRepo: UpdateExtensionRepo = Injekt.get(), private val updateExtensionRepo: UpdateExtensionRepo = Injekt.get(),
private val extensionManager: ExtensionManager = Injekt.get(),
) : StateScreenModel<RepoScreenState>(RepoScreenState.Loading) { ) : StateScreenModel<RepoScreenState>(RepoScreenState.Loading) {
private val _events: Channel<RepoEvent> = Channel(Int.MAX_VALUE) private val _events: Channel<RepoEvent> = Channel(Int.MAX_VALUE)
@@ -55,7 +53,6 @@ class ExtensionReposScreenModel(
fun createRepo(baseUrl: String) { fun createRepo(baseUrl: String) {
screenModelScope.launchIO { screenModelScope.launchIO {
when (val result = createExtensionRepo.await(baseUrl)) { when (val result = createExtensionRepo.await(baseUrl)) {
CreateExtensionRepo.Result.Success -> extensionManager.findAvailableExtensions()
CreateExtensionRepo.Result.InvalidUrl -> _events.send(RepoEvent.InvalidUrl) CreateExtensionRepo.Result.InvalidUrl -> _events.send(RepoEvent.InvalidUrl)
CreateExtensionRepo.Result.RepoAlreadyExists -> _events.send(RepoEvent.RepoAlreadyExists) CreateExtensionRepo.Result.RepoAlreadyExists -> _events.send(RepoEvent.RepoAlreadyExists)
is CreateExtensionRepo.Result.DuplicateFingerprint -> { is CreateExtensionRepo.Result.DuplicateFingerprint -> {
@@ -96,7 +93,6 @@ class ExtensionReposScreenModel(
fun deleteRepo(baseUrl: String) { fun deleteRepo(baseUrl: String) {
screenModelScope.launchIO { screenModelScope.launchIO {
deleteExtensionRepo.await(baseUrl) deleteExtensionRepo.await(baseUrl)
extensionManager.findAvailableExtensions()
} }
} }

View File

@@ -57,7 +57,7 @@ fun ExtensionRepoCreateDialog(
}, },
text = { text = {
Column { Column {
Text(text = stringResource(MR.strings.action_add_repo_message, stringResource(MR.strings.app_name))) Text(text = stringResource(MR.strings.action_add_repo_message))
OutlinedTextField( OutlinedTextField(
modifier = Modifier modifier = Modifier

View File

@@ -99,7 +99,7 @@ class DebugInfoScreen : Screen() {
} }
private fun getDeviceInfoGroup(): Preference.PreferenceGroup { private fun getDeviceInfoGroup(): Preference.PreferenceGroup {
val items = persistentListOf<Preference.PreferenceItem<out Any, out Any>>().mutate { val items = persistentListOf<Preference.PreferenceItem<out Any>>().mutate {
it.add( it.add(
Preference.PreferenceItem.TextPreference( Preference.PreferenceItem.TextPreference(
title = "Model", title = "Model",

View File

@@ -25,6 +25,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@@ -85,8 +86,7 @@ internal fun BasePreferenceWidget(
} }
} }
@Composable internal fun Modifier.highlightBackground(highlighted: Boolean): Modifier = composed {
internal fun Modifier.highlightBackground(highlighted: Boolean): Modifier {
var highlightFlag by remember { mutableStateOf(false) } var highlightFlag by remember { mutableStateOf(false) }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
if (highlighted) { if (highlighted) {
@@ -116,7 +116,7 @@ internal fun Modifier.highlightBackground(highlighted: Boolean): Modifier {
}, },
label = "highlight", label = "highlight",
) )
return this.background(color = highlight) Modifier.background(color = highlight)
} }
internal val TrailingWidgetBuffer = 16.dp internal val TrailingWidgetBuffer = 16.dp

View File

@@ -6,7 +6,6 @@ import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
@@ -16,10 +15,9 @@ import androidx.compose.ui.unit.sp
import eu.kanade.presentation.theme.TachiyomiPreviewTheme import eu.kanade.presentation.theme.TachiyomiPreviewTheme
@Composable @Composable
fun ReaderPageIndicator( fun PageIndicatorText(
currentPage: Int, currentPage: Int,
totalPages: Int, totalPages: Int,
modifier: Modifier = Modifier,
) { ) {
if (currentPage <= 0 || totalPages <= 0) return if (currentPage <= 0 || totalPages <= 0) return
@@ -38,7 +36,6 @@ fun ReaderPageIndicator(
Box( Box(
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,
modifier = modifier,
) { ) {
Text( Text(
text = text, text = text,
@@ -53,10 +50,10 @@ fun ReaderPageIndicator(
@PreviewLightDark @PreviewLightDark
@Composable @Composable
private fun ReaderPageIndicatorPreview() { private fun PageIndicatorTextPreview() {
TachiyomiPreviewTheme { TachiyomiPreviewTheme {
Surface { Surface {
ReaderPageIndicator(currentPage = 10, totalPages = 69) PageIndicatorText(currentPage = 10, totalPages = 69)
} }
} }
} }

View File

@@ -1,7 +1,10 @@
package eu.kanade.presentation.reader.appbars package eu.kanade.presentation.reader.appbars
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Settings import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
@@ -9,8 +12,9 @@ import androidx.compose.material3.IconButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation
import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode
@@ -18,7 +22,8 @@ import tachiyomi.i18n.MR
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
@Composable @Composable
fun ReaderBottomBar( fun BottomReaderBar(
backgroundColor: Color,
readingMode: ReadingMode, readingMode: ReadingMode,
onClickReadingMode: () -> Unit, onClickReadingMode: () -> Unit,
orientation: ReaderOrientation, orientation: ReaderOrientation,
@@ -26,11 +31,12 @@ fun ReaderBottomBar(
cropEnabled: Boolean, cropEnabled: Boolean,
onClickCropBorder: () -> Unit, onClickCropBorder: () -> Unit,
onClickSettings: () -> Unit, onClickSettings: () -> Unit,
modifier: Modifier = Modifier,
) { ) {
Row( Row(
modifier = modifier modifier = Modifier
.pointerInput(Unit) {}, .fillMaxWidth()
.background(backgroundColor)
.padding(8.dp),
horizontalArrangement = Arrangement.SpaceEvenly, horizontalArrangement = Arrangement.SpaceEvenly,
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {

View File

@@ -2,41 +2,42 @@ package eu.kanade.presentation.reader.appbars
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.foundation.layout.navigationBars import androidx.compose.material.icons.Icons
import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.outlined.Bookmark
import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.material.icons.outlined.BookmarkBorder
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.AppBarActions
import eu.kanade.presentation.reader.components.ChapterNavigator import eu.kanade.presentation.reader.components.ChapterNavigator
import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation
import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode
import eu.kanade.tachiyomi.ui.reader.viewer.Viewer import eu.kanade.tachiyomi.ui.reader.viewer.Viewer
import eu.kanade.tachiyomi.ui.reader.viewer.pager.R2LPagerViewer import eu.kanade.tachiyomi.ui.reader.viewer.pager.R2LPagerViewer
import kotlinx.collections.immutable.persistentListOf
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource
private val readerBarsSlideAnimationSpec = tween<IntOffset>(200) private val animationSpec = tween<IntOffset>(200)
private val readerBarsFadeAnimationSpec = tween<Float>(150)
@Composable @Composable
fun ReaderAppBars( fun ReaderAppBars(
visible: Boolean, visible: Boolean,
fullscreen: Boolean,
mangaTitle: String?, mangaTitle: String?,
chapterTitle: String?, chapterTitle: String?,
@@ -44,8 +45,8 @@ fun ReaderAppBars(
onClickTopAppBar: () -> Unit, onClickTopAppBar: () -> Unit,
bookmarked: Boolean, bookmarked: Boolean,
onToggleBookmarked: () -> Unit, onToggleBookmarked: () -> Unit,
onOpenInWebView: (() -> Unit)?,
onOpenInBrowser: (() -> Unit)?, onOpenInBrowser: (() -> Unit)?,
onOpenInWebView: (() -> Unit)?,
onShare: (() -> Unit)?, onShare: (() -> Unit)?,
viewer: Viewer?, viewer: Viewer?,
@@ -55,7 +56,7 @@ fun ReaderAppBars(
enabledPrevious: Boolean, enabledPrevious: Boolean,
currentPage: Int, currentPage: Int,
totalPages: Int, totalPages: Int,
onPageIndexChange: (Int) -> Unit, onSliderValueChange: (Int) -> Unit,
readingMode: ReadingMode, readingMode: ReadingMode,
onClickReadingMode: () -> Unit, onClickReadingMode: () -> Unit,
@@ -70,26 +71,83 @@ fun ReaderAppBars(
.surfaceColorAtElevation(3.dp) .surfaceColorAtElevation(3.dp)
.copy(alpha = if (isSystemInDarkTheme()) 0.9f else 0.95f) .copy(alpha = if (isSystemInDarkTheme()) 0.9f else 0.95f)
Column(modifier = Modifier.fillMaxHeight()) { val modifierWithInsetsPadding = if (fullscreen) {
Modifier.systemBarsPadding()
} else {
Modifier
}
Column(
modifier = Modifier.fillMaxHeight(),
verticalArrangement = Arrangement.SpaceBetween,
) {
AnimatedVisibility( AnimatedVisibility(
visible = visible, visible = visible,
enter = slideInVertically(initialOffsetY = { -it }, animationSpec = readerBarsSlideAnimationSpec) + enter = slideInVertically(
fadeIn(animationSpec = readerBarsFadeAnimationSpec), initialOffsetY = { -it },
exit = slideOutVertically(targetOffsetY = { -it }, animationSpec = readerBarsSlideAnimationSpec) + animationSpec = animationSpec,
fadeOut(animationSpec = readerBarsFadeAnimationSpec), ),
exit = slideOutVertically(
targetOffsetY = { -it },
animationSpec = animationSpec,
),
) { ) {
ReaderTopBar( AppBar(
modifier = Modifier modifier = modifierWithInsetsPadding
.background(backgroundColor)
.clickable(onClick = onClickTopAppBar), .clickable(onClick = onClickTopAppBar),
mangaTitle = mangaTitle, backgroundColor = backgroundColor,
chapterTitle = chapterTitle, title = mangaTitle,
subtitle = chapterTitle,
navigateUp = navigateUp, navigateUp = navigateUp,
bookmarked = bookmarked, actions = {
onToggleBookmarked = onToggleBookmarked, AppBarActions(
onOpenInWebView = onOpenInWebView, actions = persistentListOf<AppBar.AppBarAction>().builder()
onOpenInBrowser = onOpenInBrowser, .apply {
onShare = onShare, add(
AppBar.Action(
title = stringResource(
if (bookmarked) {
MR.strings.action_remove_bookmark
} else {
MR.strings.action_bookmark
},
),
icon = if (bookmarked) {
Icons.Outlined.Bookmark
} else {
Icons.Outlined.BookmarkBorder
},
onClick = onToggleBookmarked,
),
)
onOpenInBrowser?.let {
add(
AppBar.OverflowAction(
title = stringResource(MR.strings.action_open_in_browser),
onClick = it,
),
)
}
onOpenInWebView?.let {
add(
AppBar.OverflowAction(
title = stringResource(MR.strings.action_open_in_web_view),
onClick = it,
),
)
}
onShare?.let {
add(
AppBar.OverflowAction(
title = stringResource(MR.strings.action_share),
onClick = it,
),
)
}
}
.build(),
)
},
) )
} }
@@ -97,12 +155,19 @@ fun ReaderAppBars(
AnimatedVisibility( AnimatedVisibility(
visible = visible, visible = visible,
enter = slideInVertically(initialOffsetY = { it }, animationSpec = readerBarsSlideAnimationSpec) + enter = slideInVertically(
fadeIn(animationSpec = readerBarsFadeAnimationSpec), initialOffsetY = { it },
exit = slideOutVertically(targetOffsetY = { it }, animationSpec = readerBarsSlideAnimationSpec) + animationSpec = animationSpec,
fadeOut(animationSpec = readerBarsFadeAnimationSpec), ),
exit = slideOutVertically(
targetOffsetY = { it },
animationSpec = animationSpec,
),
) { ) {
Column(verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small)) { Column(
modifier = modifierWithInsetsPadding,
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
) {
ChapterNavigator( ChapterNavigator(
isRtl = isRtl, isRtl = isRtl,
onNextChapter = onNextChapter, onNextChapter = onNextChapter,
@@ -111,14 +176,11 @@ fun ReaderAppBars(
enabledPrevious = enabledPrevious, enabledPrevious = enabledPrevious,
currentPage = currentPage, currentPage = currentPage,
totalPages = totalPages, totalPages = totalPages,
onPageIndexChange = onPageIndexChange, onSliderValueChange = onSliderValueChange,
) )
ReaderBottomBar(
modifier = Modifier BottomReaderBar(
.fillMaxWidth() backgroundColor = backgroundColor,
.background(backgroundColor)
.padding(horizontal = MaterialTheme.padding.small)
.windowInsetsPadding(WindowInsets.navigationBars),
readingMode = readingMode, readingMode = readingMode,
onClickReadingMode = onClickReadingMode, onClickReadingMode = onClickReadingMode,
orientation = orientation, orientation = orientation,

View File

@@ -1,83 +0,0 @@
package eu.kanade.presentation.reader.appbars
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Bookmark
import androidx.compose.material.icons.outlined.BookmarkBorder
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.AppBarActions
import kotlinx.collections.immutable.persistentListOf
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.i18n.stringResource
@Composable
fun ReaderTopBar(
mangaTitle: String?,
chapterTitle: String?,
navigateUp: () -> Unit,
bookmarked: Boolean,
onToggleBookmarked: () -> Unit,
onOpenInWebView: (() -> Unit)?,
onOpenInBrowser: (() -> Unit)?,
onShare: (() -> Unit)?,
modifier: Modifier = Modifier,
) {
AppBar(
modifier = modifier,
backgroundColor = Color.Transparent,
title = mangaTitle,
subtitle = chapterTitle,
navigateUp = navigateUp,
actions = {
AppBarActions(
actions = persistentListOf<AppBar.AppBarAction>().builder()
.apply {
add(
AppBar.Action(
title = stringResource(
if (bookmarked) {
MR.strings.action_remove_bookmark
} else {
MR.strings.action_bookmark
},
),
icon = if (bookmarked) {
Icons.Outlined.Bookmark
} else {
Icons.Outlined.BookmarkBorder
},
onClick = onToggleBookmarked,
),
)
onOpenInWebView?.let {
add(
AppBar.OverflowAction(
title = stringResource(MR.strings.action_open_in_web_view),
onClick = it,
),
)
}
onOpenInBrowser?.let {
add(
AppBar.OverflowAction(
title = stringResource(MR.strings.action_open_in_browser),
onClick = it,
),
)
}
onShare?.let {
add(
AppBar.OverflowAction(
title = stringResource(MR.strings.action_share),
onClick = it,
),
)
}
}
.build(),
)
},
)
}

View File

@@ -4,7 +4,6 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsDraggedAsState import androidx.compose.foundation.interaction.collectIsDraggedAsState
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
@@ -17,6 +16,7 @@ import androidx.compose.material3.FilledIconButton
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButtonDefaults import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Slider
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -29,7 +29,6 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.LocalLayoutDirection 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.theme.TachiyomiPreviewTheme
import eu.kanade.presentation.util.isTabletUi import eu.kanade.presentation.util.isTabletUi
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.Slider
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
import kotlin.math.roundToInt
@Composable @Composable
fun ChapterNavigator( fun ChapterNavigator(
@@ -51,7 +50,7 @@ fun ChapterNavigator(
enabledPrevious: Boolean, enabledPrevious: Boolean,
currentPage: Int, currentPage: Int,
totalPages: Int, totalPages: Int,
onPageIndexChange: (Int) -> Unit, onSliderValueChange: (Int) -> Unit,
) { ) {
val isTabletUi = isTabletUi() val isTabletUi = isTabletUi()
val horizontalPadding = if (isTabletUi) 24.dp else 8.dp val horizontalPadding = if (isTabletUi) 24.dp else 8.dp
@@ -98,11 +97,7 @@ fun ChapterNavigator(
.padding(horizontal = 16.dp), .padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
Box(contentAlignment = Alignment.CenterEnd) { Text(text = currentPage.toString())
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)
}
val interactionSource = remember { MutableInteractionSource() } val interactionSource = remember { MutableInteractionSource() }
val sliderDragged by interactionSource.collectIsDraggedAsState() val sliderDragged by interactionSource.collectIsDraggedAsState()
@@ -115,11 +110,14 @@ fun ChapterNavigator(
modifier = Modifier modifier = Modifier
.weight(1f) .weight(1f)
.padding(horizontal = 8.dp), .padding(horizontal = 8.dp),
value = currentPage, value = currentPage.toFloat(),
valueRange = 1..totalPages, valueRange = 1f..totalPages.toFloat(),
onValueChange = f@{ steps = totalPages - 2,
if (it == currentPage) return@f onValueChange = {
onPageIndexChange(it - 1) val new = it.roundToInt() - 1
if (new != currentPage) {
onSliderValueChange(new)
}
}, },
interactionSource = interactionSource, interactionSource = interactionSource,
) )
@@ -160,7 +158,7 @@ private fun ChapterNavigatorPreview() {
enabledPrevious = true, enabledPrevious = true,
currentPage = currentPage, currentPage = currentPage,
totalPages = 10, totalPages = 10,
onPageIndexChange = { currentPage = (it + 1) }, onSliderValueChange = { currentPage = it },
) )
} }
} }

Some files were not shown because too many files have changed in this diff Show More