mirror of
https://github.com/mihonapp/mihon.git
synced 2025-10-09 12:59:34 +02:00
Compare commits
317 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
e4de208cf7 | ||
|
f1193866f4 | ||
|
3782e1b414 | ||
|
c4407eda0e | ||
|
33e0121a2a | ||
|
b93337cb3d | ||
|
358adb8cd1 | ||
|
22f851173b | ||
|
982ebcf777 | ||
|
e62cd0e816 | ||
|
1365b28106 | ||
|
7ec28eb3bd | ||
|
ff9dfe45ed | ||
|
967750ba58 | ||
|
269af7fe2b | ||
|
62eec15fe6 | ||
|
2ef8ae11c9 | ||
|
4f1faf49f3 | ||
|
6d717ea88b | ||
|
fbb5e6b92f | ||
|
8f5f29e737 | ||
|
a4b9c704b6 | ||
|
9352201b03 | ||
|
c715e981bf | ||
|
7f56555d63 | ||
|
8636b7a685 | ||
|
a49670bf0d | ||
|
61cee5c5e0 | ||
|
d805f0cd2a | ||
|
ce07259e8e | ||
|
2f10e7beaa | ||
|
4ef8fb9588 | ||
|
084e626669 | ||
|
f93ccaaaa4 | ||
|
5585388e2d | ||
|
d60241690b | ||
|
84aa07b7f0 | ||
|
0cc1224094 | ||
|
a5a0d83302 | ||
|
1fde0275e3 | ||
|
a992f2d467 | ||
|
d8dd170d1b | ||
|
6953090dab | ||
|
ab452a9945 | ||
|
7dd595f16e | ||
|
6eb2a022f1 | ||
|
d61c66c286 | ||
|
1e4ee14608 | ||
|
63943debc2 | ||
|
d2c1ff6adf | ||
|
b9e02e92be | ||
|
103218681a | ||
|
07136d3969 | ||
|
4962deeb0c | ||
|
cecf4596f9 | ||
|
0c77afbe03 | ||
|
d126b84f95 | ||
|
2df3382148 | ||
|
ee19050cc0 | ||
|
8de1fa854d | ||
|
019fc08da2 | ||
|
288f577a45 | ||
|
0290a2d815 | ||
|
a47d4ebbdd | ||
|
89954e68e3 | ||
|
c3b590cd3d | ||
|
cb3c5e9c9c | ||
|
7fa2834009 | ||
|
95e3c22429 | ||
|
8bd70342fc | ||
|
92ec6b17a3 | ||
|
591e9c1356 | ||
|
7fed9c2ccf | ||
|
ccb554c877 | ||
|
5235713d83 | ||
|
4692010400 | ||
|
be528ba12b | ||
|
405e536cbf | ||
|
8714653a2f | ||
|
2b126f1ff5 | ||
|
5919f34fc9 | ||
|
a4df33caf9 | ||
|
3580d2da6c | ||
|
77eb558742 | ||
|
e1f6d14393 | ||
|
2e180005a0 | ||
|
0f59fc1dd4 | ||
|
d1055475e2 | ||
|
32470657dd | ||
|
158896cfa9 | ||
|
92b376d9af | ||
|
1a2f09a622 | ||
|
209e982fe4 | ||
|
0109102901 | ||
|
4117a51674 | ||
|
4090a61d08 | ||
|
c406513557 | ||
|
625c85cbd6 | ||
|
737ceeea57 | ||
|
213b673b13 | ||
|
7933c9eeb7 | ||
|
f0de8f973b | ||
|
e8c6e3364d | ||
|
c12bdbae8e | ||
|
33d407ee9c | ||
|
ef8c3ca119 | ||
|
0cb3a4aeb6 | ||
|
8b45ef0e5d | ||
|
86ebf55815 | ||
|
ddf282b103 | ||
|
744b809d45 | ||
|
cd2ce44efa | ||
|
cae7c3dc58 | ||
|
c0074402e7 | ||
|
7deeabe844 | ||
|
536393a6d9 | ||
|
f8cb506137 | ||
|
98230ed30f | ||
|
d721a4321b | ||
|
1ac4b72cfe | ||
|
9331f2b93f | ||
|
99c2a99973 | ||
|
001716e34b | ||
|
37e19edf8a | ||
|
8b7f355988 | ||
|
0b77733673 | ||
|
9be558d6c0 | ||
|
0c8c5dbba6 | ||
|
1c982c2a01 | ||
|
eeab61fc94 | ||
|
a036407c75 | ||
|
615d93f780 | ||
|
9750c1e4bd | ||
|
e2915a1f69 | ||
|
f6617a7a22 | ||
|
ef37a4c80b | ||
|
df2b4c754b | ||
|
6632a12228 | ||
|
0cb1925cf1 | ||
|
a31b3b7bbf | ||
|
fea85241af | ||
|
e273a26c9b | ||
|
818e6931c6 | ||
|
ecc6ede081 | ||
|
fefa8f8498 | ||
|
f1e2efcb37 | ||
|
180318f57d | ||
|
ed749de806 | ||
|
bb33b0029e | ||
|
3a19e449b1 | ||
|
818edf2776 | ||
|
47d2646751 | ||
|
a1a7d67afb | ||
|
2090a380e0 | ||
|
8e5cfe9d0a | ||
|
3249228c49 | ||
|
d9c4b56336 | ||
|
5e029b1fe6 | ||
|
12abd9938b | ||
|
c1225a5ef9 | ||
|
a594ad392d | ||
|
2259164fde | ||
|
80de032819 | ||
|
0d35b6fdaf | ||
|
f81da3dcce | ||
|
2ce9fa0271 | ||
|
300bee865f | ||
|
542d30c14a | ||
|
5d2110f3fb | ||
|
4e68339783 | ||
|
c8ffabc84a | ||
|
77e79233ab | ||
|
8a21148578 | ||
|
e91db86fae | ||
|
556290f2d3 | ||
|
8b947919ac | ||
|
b62a9b40eb | ||
|
a6b532ee57 | ||
|
8fbe630308 | ||
|
132d77aa99 | ||
|
b00bbe91be | ||
|
3e5d3d099f | ||
|
941dde341e | ||
|
365d167eac | ||
|
d4aaf6521e | ||
|
f7046a503b | ||
|
953c4e7bc0 | ||
|
6294521460 | ||
|
1d6dc1e8b0 | ||
|
da4584e193 | ||
|
935f1fcf3f | ||
|
093e8e5c5a | ||
|
b372657238 | ||
|
dae7d17966 | ||
|
4c6bc8e830 | ||
|
8a3b610775 | ||
|
ac432e2e94 | ||
|
28093935d8 | ||
|
7028b8673a | ||
|
2dc8cf000b | ||
|
f76a3ad15a | ||
|
f33aa1ac92 | ||
|
05012de569 | ||
|
0d19308054 | ||
|
aa6a4953c5 | ||
|
046f09c4bd | ||
|
eddf07f9ac | ||
|
22b5fb58ff | ||
|
4f06c1cc09 | ||
|
7913679f9d | ||
|
0893609ad2 | ||
|
85d168ed5e | ||
|
d3691cc256 | ||
|
7f9406aec9 | ||
|
563bc02113 | ||
|
b702603965 | ||
|
2e2f1ed82d | ||
|
b2765a00d2 | ||
|
0e6d6c087e | ||
|
4f7122d6f0 | ||
|
b763d3e2c2 | ||
|
9957fff2fb | ||
|
debca74e0d | ||
|
1313ff7a16 | ||
|
793d7fbe40 | ||
|
b12ee027ea | ||
|
d7a1ae2734 | ||
|
7566918ee7 | ||
|
7b70b40d30 | ||
|
cd0481592c | ||
|
b93746b01e | ||
|
2b0c28938b | ||
|
4db3817782 | ||
|
ec07843f0c | ||
|
b08b22fdcc | ||
|
919607cd06 | ||
|
d91c7b6093 | ||
|
fab8b17d99 | ||
|
8b28a9bcee | ||
|
79e25451bd | ||
|
0dda64b9d8 | ||
|
181dbbb638 | ||
|
3ce013fa19 | ||
|
fe22f5aa37 | ||
|
1dd81ef1e1 | ||
|
2d0be5b0c9 | ||
|
4d7350e318 | ||
|
49b2b346b6 | ||
|
badc229a23 | ||
|
277d8bad8e | ||
|
8b48d1016b | ||
|
a96fbba3dc | ||
|
d8a530266f | ||
|
e1724d1aa0 | ||
|
1a5b4c2804 | ||
|
2cd52d5a1f | ||
|
ebfbbf0741 | ||
|
eeb683069a | ||
|
7e71a34256 | ||
|
29ee53f461 | ||
|
6a223f34a0 | ||
|
c335ea9103 | ||
|
c97fe71e29 | ||
|
8e81a5e68b | ||
|
b08270d523 | ||
|
34d1e6fa27 | ||
|
a80965f7f1 | ||
|
59ee61039b | ||
|
b7a96e6946 | ||
|
31a3f9e051 | ||
|
42e45e6020 | ||
|
e8c9cb2c2e | ||
|
d592ab2e87 | ||
|
9d6ed93daa | ||
|
34efa8d901 | ||
|
bfc8320aa4 | ||
|
29ec7c125a | ||
|
dce6aacf02 | ||
|
503d0be667 | ||
|
82fd89cee6 | ||
|
643f95f046 | ||
|
9c81f2486c | ||
|
e59d2d381d | ||
|
da90064c94 | ||
|
d53a3828b1 | ||
|
c283abefb0 | ||
|
4bc593861c | ||
|
5a9367603b | ||
|
c01e9f3e92 | ||
|
ae9753a1ea | ||
|
1fe4d6cbd4 | ||
|
2c5f28f277 | ||
|
3a3abc6854 | ||
|
d9a550b935 | ||
|
1617f8eb49 | ||
|
77faab14b1 | ||
|
d60802721b | ||
|
19af85ab61 | ||
|
4a7fe44e0e | ||
|
c5655e8803 | ||
|
d3973f4ad8 | ||
|
bb230fd6a7 | ||
|
e526fd44c6 | ||
|
f61f039a45 | ||
|
79eb02d8f0 | ||
|
814584d35b | ||
|
8751307301 | ||
|
bcff2262b3 | ||
|
04454ecdbe | ||
|
69320e4d09 | ||
|
e86aeee9c4 | ||
|
be37f214d8 | ||
|
1a833e88b1 | ||
|
4c84878adc | ||
|
054198e78f | ||
|
d522d81164 | ||
|
40fe5f8437 |
@@ -1,8 +1,31 @@
|
||||
[*.{kt,kts}]
|
||||
max_line_length = 120
|
||||
indent_size = 4
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.{xml,sq,sqm}]
|
||||
indent_size = 4
|
||||
|
||||
# noinspection EditorConfigKeyCorrectness
|
||||
[*.{kt,kts}]
|
||||
indent_size = 4
|
||||
max_line_length = 120
|
||||
|
||||
ij_kotlin_allow_trailing_comma = true
|
||||
ij_kotlin_allow_trailing_comma_on_call_site = true
|
||||
ij_kotlin_name_count_to_use_star_import = 2147483647
|
||||
ij_kotlin_name_count_to_use_star_import_for_members = 2147483647
|
||||
|
||||
ktlint_code_style = intellij_idea
|
||||
ktlint_function_naming_ignore_when_annotated_with = Composable
|
||||
ktlint_standard_class-signature = disabled
|
||||
ktlint_standard_comment-wrapping = disabled
|
||||
ktlint_standard_discouraged-comment-location = disabled
|
||||
ktlint_standard_function-expression-body = disabled
|
||||
ktlint_standard_function-signature = disabled
|
||||
ktlint_standard_type-argument-comment = disabled
|
||||
ktlint_standard_type-parameter-comment = disabled
|
||||
|
@@ -1,8 +1,7 @@
|
||||
name: ⭐ Feature request
|
||||
description: Suggest a feature to improve Mihon
|
||||
labels: [Feature request]
|
||||
labels: [feature request]
|
||||
body:
|
||||
|
||||
- type: textarea
|
||||
id: feature-description
|
||||
attributes:
|
||||
@@ -31,7 +30,7 @@ body:
|
||||
required: true
|
||||
- label: I have written a short but informative title.
|
||||
required: true
|
||||
- label: I have updated the app to version **[0.17.1](https://github.com/mihonapp/mihon/releases/latest)**.
|
||||
- label: I have updated the app to version **[0.19.0](https://github.com/mihonapp/mihon/releases/latest)**.
|
||||
required: true
|
||||
- label: I will fill out all of the requested information in this form.
|
||||
required: true
|
@@ -1,8 +1,7 @@
|
||||
name: 🐞 Issue report
|
||||
description: Report an issue in Mihon
|
||||
labels: [Bug]
|
||||
labels: [bug]
|
||||
body:
|
||||
|
||||
- type: textarea
|
||||
id: reproduce-steps
|
||||
attributes:
|
||||
@@ -43,9 +42,9 @@ body:
|
||||
attributes:
|
||||
label: Crash logs
|
||||
description: |
|
||||
If you're experiencing crashes, share the crash logs from **More → Settings → Advanced** then press **Dump crash logs**.
|
||||
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.
|
||||
placeholder: |
|
||||
You can paste the crash logs in plain text or upload it as an attachment.
|
||||
You can upload the crash log file as an attachment, or paste the crash logs in plain text if needed.
|
||||
|
||||
- type: input
|
||||
id: mihon-version
|
||||
@@ -53,7 +52,7 @@ body:
|
||||
label: Mihon version
|
||||
description: You can find your Mihon version in **More → About**.
|
||||
placeholder: |
|
||||
Example: "0.17.1"
|
||||
Example: "0.19.0"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -96,9 +95,9 @@ body:
|
||||
required: true
|
||||
- label: I have gone through the [FAQ](https://mihon.app/docs/faq/general) and [troubleshooting guide](https://mihon.app/docs/guides/troubleshooting/).
|
||||
required: true
|
||||
- label: I have updated the app to version **[0.17.1](https://github.com/mihonapp/mihon/releases/latest)**.
|
||||
- label: I have updated the app to version **[0.19.0](https://github.com/mihonapp/mihon/releases/latest)**.
|
||||
required: true
|
||||
- label: I have updated all installed extensions.
|
||||
- label: I have filled out all of the requested information in this form, including specific version numbers.
|
||||
required: true
|
||||
- label: I will fill out all of the requested information in this form.
|
||||
- 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.
|
||||
required: true
|
3
.github/ISSUE_TEMPLATE/config.yml
vendored
3
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,5 +1,8 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: ❌ Help with Extensions
|
||||
url: https://mihon.app/docs/faq/browse/extensions
|
||||
about: For extension-related questions/issues
|
||||
- name: 🖥️ Mihon website
|
||||
url: https://mihon.app/
|
||||
about: Guides, troubleshooting, and answers to common questions
|
||||
|
6
.github/renovate.json5
vendored
6
.github/renovate.json5
vendored
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": ["config:base"],
|
||||
"extends": ["config:recommended"],
|
||||
"labels": ["Dependencies"],
|
||||
"semanticCommits": "disabled",
|
||||
"packageRules": [
|
||||
@@ -8,6 +8,6 @@
|
||||
"groupName": "GitHub Actions",
|
||||
"matchManagers": ["github-actions"],
|
||||
"pinDigests": true,
|
||||
}
|
||||
]
|
||||
},
|
||||
],
|
||||
}
|
||||
|
31
.github/workflows/build_pull_request.yml
vendored
31
.github/workflows/build_pull_request.yml
vendored
@@ -19,38 +19,41 @@ permissions:
|
||||
jobs:
|
||||
build:
|
||||
name: Build app
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: 'ubuntu-24.04'
|
||||
|
||||
steps:
|
||||
- name: Clone repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Validate Gradle Wrapper
|
||||
uses: gradle/actions/wrapper-validation@cc4fc85e6b35bafd578d5ffbc76a5518407e1af0 # v4.2.1
|
||||
|
||||
- name: Dependency Review
|
||||
uses: actions/dependency-review-action@3b139cfc5fae8b618d3eae3675e383bb1769c019 # v4.5.0
|
||||
uses: actions/dependency-review-action@da24556b548a50705dd671f47852072ea4c105d9 # v4.7.1
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@8df1039502a15bceb9433410b1a100fbe190c53b # v4.5.0
|
||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
||||
with:
|
||||
java-version: 17
|
||||
distribution: adopt
|
||||
distribution: temurin
|
||||
|
||||
- name: Set up gradle
|
||||
uses: gradle/actions/setup-gradle@cc4fc85e6b35bafd578d5ffbc76a5518407e1af0 # v4.2.1
|
||||
uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1
|
||||
|
||||
- name: Build app and run unit tests
|
||||
run: ./gradlew spotlessCheck assembleStandardRelease testReleaseUnitTest testStandardReleaseUnitTest
|
||||
- name: Check code format
|
||||
run: ./gradlew spotlessCheck
|
||||
|
||||
- name: Build app
|
||||
run: ./gradlew assembleRelease
|
||||
|
||||
- name: Run unit tests
|
||||
run: ./gradlew testReleaseUnitTest
|
||||
|
||||
- name: Upload APK
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: arm64-v8a-${{ github.sha }}
|
||||
path: app/build/outputs/apk/standard/release/app-standard-arm64-v8a-release-unsigned.apk
|
||||
path: app/build/outputs/apk/release/app-arm64-v8a-release-unsigned.apk
|
||||
|
||||
- name: Upload mapping
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: mapping-${{ github.sha }}
|
||||
path: app/build/outputs/mapping/standardRelease
|
||||
path: app/build/outputs/mapping/release
|
||||
|
85
.github/workflows/build_push.yml
vendored
85
.github/workflows/build_push.yml
vendored
@@ -13,42 +13,41 @@ concurrency:
|
||||
jobs:
|
||||
build:
|
||||
name: Build app
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: 'ubuntu-24.04'
|
||||
|
||||
steps:
|
||||
- name: Clone repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Validate Gradle Wrapper
|
||||
uses: gradle/actions/wrapper-validation@cc4fc85e6b35bafd578d5ffbc76a5518407e1af0 # v4.2.1
|
||||
|
||||
- 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@8df1039502a15bceb9433410b1a100fbe190c53b # v4.5.0
|
||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
||||
with:
|
||||
java-version: 17
|
||||
distribution: adopt
|
||||
distribution: temurin
|
||||
|
||||
- name: Set up gradle
|
||||
uses: gradle/actions/setup-gradle@cc4fc85e6b35bafd578d5ffbc76a5518407e1af0 # v4.2.1
|
||||
uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1
|
||||
|
||||
- name: Build app and run unit tests
|
||||
run: ./gradlew spotlessCheck assembleStandardRelease testReleaseUnitTest testStandardReleaseUnitTest
|
||||
- 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@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: arm64-v8a-${{ github.sha }}
|
||||
path: app/build/outputs/apk/standard/release/app-standard-arm64-v8a-release-unsigned.apk
|
||||
path: app/build/outputs/apk/release/app-arm64-v8a-release-unsigned.apk
|
||||
|
||||
- name: Upload mapping
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: mapping-${{ github.sha }}
|
||||
path: app/build/outputs/mapping/standardRelease
|
||||
path: app/build/outputs/mapping/release
|
||||
|
||||
# Sign APK and create release for tags
|
||||
|
||||
@@ -60,59 +59,38 @@ jobs:
|
||||
|
||||
- name: Sign APK
|
||||
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'mihonapp/mihon'
|
||||
uses: r0adkll/sign-android-release@349ebdef58775b1e0d8099458af0816dc79b6407 # v1
|
||||
uses: r0adkll/sign-android-release@f30bdd30588842ac76044ecdbd4b6d0e3e813478
|
||||
with:
|
||||
releaseDirectory: app/build/outputs/apk/standard/release
|
||||
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: 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
|
||||
mv app/build/outputs/apk/release/app-universal-release-unsigned-signed.apk mihon-${{ env.VERSION_TAG }}.apk
|
||||
mv app/build/outputs/apk/release/app-arm64-v8a-release-unsigned-signed.apk mihon-arm64-v8a-${{ env.VERSION_TAG }}.apk
|
||||
mv app/build/outputs/apk/release/app-armeabi-v7a-release-unsigned-signed.apk mihon-armeabi-v7a-${{ env.VERSION_TAG }}.apk
|
||||
mv app/build/outputs/apk/release/app-x86-release-unsigned-signed.apk mihon-x86-${{ env.VERSION_TAG }}.apk
|
||||
mv app/build/outputs/apk/release/app-x86_64-release-unsigned-signed.apk mihon-x86_64-${{ env.VERSION_TAG }}.apk
|
||||
|
||||
- name: Create Release
|
||||
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'mihonapp/mihon'
|
||||
uses: softprops/action-gh-release@01570a1f39cb168c169c802c3bceb9e93fb10974 # v2.1.0
|
||||
uses: softprops/action-gh-release@72f2c25fcb47643c292f7107632f7a47c1df5cd8 # v2.3.2
|
||||
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
|
||||
<!-->
|
||||
> [!TIP]
|
||||
>
|
||||
> ### If you are unsure which version to download then go with `mihon-${{ env.VERSION_TAG }}.apk`
|
||||
files: |
|
||||
mihon-${{ env.VERSION_TAG }}.apk
|
||||
mihon-arm64-v8a-${{ env.VERSION_TAG }}.apk
|
||||
@@ -121,5 +99,4 @@ jobs:
|
||||
mihon-x86_64-${{ env.VERSION_TAG }}.apk
|
||||
draft: true
|
||||
prerelease: false
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.PAT }}
|
||||
token: ${{ secrets.MIHON_BOT_TOKEN }}
|
||||
|
19
.github/workflows/lock.yml
vendored
19
.github/workflows/lock.yml
vendored
@@ -1,19 +0,0 @@
|
||||
name: Lock threads
|
||||
|
||||
on:
|
||||
# Daily
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
# Manual trigger
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
|
||||
jobs:
|
||||
lock:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/lock-threads@1bf7ec25051fe7c00bdd17e6a7cf3d7bfb7dc771 # v5.0.1
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
issue-inactive-days: '2'
|
||||
pr-inactive-days: '2'
|
23
.github/workflows/update_website.yml
vendored
Normal file
23
.github/workflows/update_website.yml
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
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"}'
|
123
CHANGELOG.md
123
CHANGELOG.md
@@ -12,14 +12,116 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [v0.19.0] - 2025-08-04
|
||||
### Added
|
||||
- Add more Kaomoji for empty/error screens ([@ianfhunter](https://github.com/ianfhunter/)) ([#1909](https://github.com/mihonapp/mihon/pull/1909))
|
||||
- Add user manga notes ([@imkunet](https://github.com/imkunet), [@AntsyLich](https://github.com/AntsyLich)) ([#428](https://github.com/mihonapp/mihon/pull/428))
|
||||
- Fix user notes not restoring when manga doesn't exist in DB ([@AntsyLich](https://github.com/AntsyLich)) ([#1945](https://github.com/mihonapp/mihon/pull/1945))
|
||||
- Add markdown support for manga descriptions ([@Secozzi](https://github.com/Secozzi)) ([#1948](https://github.com/mihonapp/mihon/pull/1948))
|
||||
- Use simpler markdown flavour ([@Secozzi](https://github.com/Secozzi)) ([#2000](https://github.com/mihonapp/mihon/pull/2000))
|
||||
- Use Github markdown flavour for Github releases & fix bullet list alignment ([@Secozzi](https://github.com/Secozzi)) ([#2024](https://github.com/mihonapp/mihon/pull/2024))
|
||||
- Add option to toggle image loading ([@Secozzi](https://github.com/Secozzi)) ([#2076](https://github.com/mihonapp/mihon/pull/2076))
|
||||
- Add Nord Theme ([@Riztard](https://github.com/Riztard)) ([#1951](https://github.com/mihonapp/mihon/pull/1951))
|
||||
- Option to keep read manga when clearing database ([@AwkwardPeak7](https://github.com/AwkwardPeak7)) ([#1979](https://github.com/mihonapp/mihon/pull/1979))
|
||||
- Add advanced option to always update manga title from source ([@FlaminSarge](https://github.com/FlaminSarge)) ([#1182](https://github.com/mihonapp/mihon/pull/1182))
|
||||
- Full predictive back support ([@AntsyLich](https://github.com/AntsyLich)) ([#2085](https://github.com/mihonapp/mihon/pull/2085))
|
||||
- Add Catppuccin theme (mocha for dark and latte for light, mauve accent) ([@claymorwan](https://github.com/claymorwan/)) ([#2117](https://github.com/mihonapp/mihon/pull/2117))
|
||||
- Manga mass migration ([@AntsyLich](https://github.com/AntsyLich), [@jobobby04](https://github.com/jobobby04)) ([#2110](https://github.com/mihonapp/mihon/pull/2110), [#2336](https://github.com/mihonapp/mihon/pull/2336), [#2338](https://github.com/mihonapp/mihon/pull/2338), [`f119386`](https://github.com/mihonapp/mihon/commit/f119386))
|
||||
|
||||
### Improved
|
||||
- Significantly improve browsing speed (near instantaneous) ([@AntsyLich](https://github.com/AntsyLich)) ([#1946](https://github.com/mihonapp/mihon/pull/1946))
|
||||
- Deduplicate entries when browsing ([@AntsyLich](https://github.com/AntsyLich)) ([#1957](https://github.com/mihonapp/mihon/pull/1957))
|
||||
- Update non-library manga data when browsing ([@AntsyLich](https://github.com/AntsyLich)) ([#1967](https://github.com/mihonapp/mihon/pull/1967))
|
||||
- Surface image loading error in Reader ([@AwkwardPeak7](https://github.com/AwkwardPeak7)) ([#1981](https://github.com/mihonapp/mihon/pull/1981))
|
||||
- Include source headers when opening failed images from reader ([@AwkwardPeak7](https://github.com/AwkwardPeak7)) ([#2004](https://github.com/mihonapp/mihon/pull/2004))
|
||||
- Added autofill support to tracker login dialog ([@AntsyLich](https://github.com/AntsyLich)) ([#2069](https://github.com/mihonapp/mihon/pull/2069))
|
||||
- Added option to hide missing chapter count ([@User826](https://github.com/User826), [@AntsyLich](https://github.com/AntsyLich)) ([#2108](https://github.com/mihonapp/mihon/pull/2108))
|
||||
- Use median to determine smart update interval, making it more resilient to long hiatuses ([@Kladki](https://github.com/Kladki)) ([#2251](https://github.com/mihonapp/mihon/pull/2251))
|
||||
- Optimize library code to potentially better handle big user libraries ([@AntsyLich](https://github.com/AntsyLich)) ([#2329](https://github.com/mihonapp/mihon/pull/2329), [#2341](https://github.com/mihonapp/mihon/pull/2341))
|
||||
|
||||
### Changed
|
||||
- Display all similarly named duplicates in duplicate manga dialogue ([@NarwhalHorns](https://github.com/NarwhalHorns), [@AntsyLich](https://github.com/AntsyLich)) ([#1861](https://github.com/mihonapp/mihon/pull/1861))
|
||||
- Display chapter count on items in duplicate manga dialogue ([@NarwhalHorns](https://github.com/NarwhalHorns)) ([#1963](https://github.com/mihonapp/mihon/pull/1963))
|
||||
- Update Facebook and Reddit icons ([@Joehuu](https://github.com/Joehuu)) ([#1994](https://github.com/mihonapp/mihon/pull/1994))
|
||||
- Switch default user agent to Android Chrome ([@AntsyLich](https://github.com/AntsyLich)) ([#2048](https://github.com/mihonapp/mihon/pull/2048))
|
||||
- Changed log in button text when processing tracker login ([@AntsyLich](https://github.com/AntsyLich)) ([#2069](https://github.com/mihonapp/mihon/pull/2069))
|
||||
- Disable reader's 'Keep screen on' setting by default ([@AntsyLich](https://github.com/AntsyLich)) ([#2095](https://github.com/mihonapp/mihon/pull/2095))
|
||||
- Update manga without chapters even if restricted by source ([@AntsyLich](https://github.com/AntsyLich)) ([#2224](https://github.com/mihonapp/mihon/pull/224))
|
||||
- Make local source default chapter sorting match file explorer behavior ([@AntsyLich](https://github.com/AntsyLich)) ([#2224](https://github.com/mihonapp/mihon/pull/224))
|
||||
- Include Manga `initialized` status in backup ([@AwkwardPeak7](https://github.com/AwkwardPeak7)) ([#2285](https://github.com/mihonapp/mihon/pull/2285))
|
||||
|
||||
### Fixes
|
||||
- Fix Bangumi search results including novels ([@MajorTanya](https://github.com/MajorTanya)) ([#1885](https://github.com/mihonapp/mihon/pull/1885))
|
||||
- Fix next chapter button occasionally jumping to the last page of the current chapter ([@perokhe](https://github.com/perokhe)) ([#1920](https://github.com/mihonapp/mihon/pull/1920))
|
||||
- Fix page number not appearing when opening chapter ([@perokhe](https://github.com/perokhe)) ([#1936](https://github.com/mihonapp/mihon/pull/1936))
|
||||
- Fix backup sharing from notifications not working when app is in background ([@JaymanR](https://github.com/JaymanR))([#1929](https://github.com/mihonapp/mihon/pull/1929))
|
||||
- Fix mark existing duplicate read chapters as read option not working in some cases ([@AntsyLich](https://github.com/AntsyLich)) ([#1944](https://github.com/mihonapp/mihon/pull/1944))
|
||||
- Fix app bar action tooltips blocking clicks ([@Bartuzen](https://github.com/Bartuzen)) ([#1928](https://github.com/mihonapp/mihon/pull/1928))
|
||||
- Fix unintended app permissions due to Firebase misconfiguration ([@AntsyLich](https://github.com/AntsyLich)) ([#1960](https://github.com/mihonapp/mihon/pull/1960))
|
||||
- Fix navigation issue after migrating a duplicated entry from History tab ([@cuong-tran](https://github.com/cuong-tran)) ([#1980](https://github.com/mihonapp/mihon/pull/1980))
|
||||
- Fix duplicate requests in WebView due to empty reasonPhrase ([@AwkwardPeak7](https://github.com/AwkwardPeak7)) ([#2003](https://github.com/mihonapp/mihon/pull/2003))
|
||||
- Fix content under source browse screen top appbar is interactable ([@AntsyLich](https://github.com/AntsyLich)) ([#2026](https://github.com/mihonapp/mihon/pull/2026))
|
||||
- Fix crash when trying use source sort filter without a pre-selection ([@AntsyLich](https://github.com/AntsyLich)) ([#2036](https://github.com/mihonapp/mihon/pull/2036))
|
||||
- Fix empty layout not appearing in browse source screen in some cases ([@NarwhalHorns](https://github.com/NarwhalHorns)) ([#2043](https://github.com/mihonapp/mihon/pull/2043))
|
||||
- Fix Pill not following the local text style ([@AntsyLich](https://github.com/AntsyLich)) ([`f8cb506`](https://github.com/mihonapp/mihon/commit/f8cb506))
|
||||
- Fix downloader stopping after failing to create download directory of a manga ([@AntsyLich](https://github.com/AntsyLich)) ([#2068](https://github.com/mihonapp/mihon/pull/2068))
|
||||
- Fix pressing `Enter` while searching also triggering navigation back on physical keyboards ([@AwkwardPeak7](https://github.com/AwkwardPeak7)) ([#2077](https://github.com/mihonapp/mihon/pull/2077))
|
||||
- Ensure app waits for Cloudflare challenge to complete before continuing ([@AwkwardPeak7](https://github.com/AwkwardPeak7)) ([#2200](https://github.com/mihonapp/mihon/pull/2200))
|
||||
|
||||
### Removed
|
||||
- Remove Okhttp networking from WebView Screen ([@AwkwardPeak7](https://github.com/AwkwardPeak7)) ([#2020](https://github.com/mihonapp/mihon/pull/2020))
|
||||
|
||||
## [v0.18.0] - 2025-03-20
|
||||
### Added
|
||||
- Add option to always decode long strip images with SSIV ([@AntsyLich](https://github.com/AntsyLich)) ([`c5655e8`](https://github.com/mihonapp/mihon/commit/c5655e8803bc32d0931657f0b7bc6afeab70feaf))
|
||||
- Change option label ([@AntsyLich](https://github.com/AntsyLich)) ([#1835](https://github.com/mihonapp/mihon/pull/1835))
|
||||
- Added option to enable incognito per extension ([@sdaqo](https://github.com/sdaqo), [@AntsyLich](https://github.com/AntsyLich)) ([#157](https://github.com/mihonapp/mihon/pull/157))
|
||||
- Add button to favorite manga from history screen ([@Animeboynz](https://github.com/Animeboynz)) ([#1733](https://github.com/mihonapp/mihon/pull/1733))
|
||||
- Add Monochrome theme (made with e-ink displays in mind) ([@MajorTanya](https://github.com/MajorTanya)) ([#1752](https://github.com/mihonapp/mihon/pull/1752))
|
||||
- Support for private tracking with AniList and Bangumi ([@NarwhalHorns](https://github.com/NarwhalHorns)) ([#1736](https://github.com/mihonapp/mihon/pull/1736))
|
||||
- Add private tracking support for Kitsu ([@MajorTanya](https://github.com/MajorTanya)) ([#1774](https://github.com/mihonapp/mihon/pull/1774))
|
||||
- Add option to export minimal library information to a CSV file ([@Animeboynz](https://github.com/Animeboynz), [@AntsyLich](https://github.com/AntsyLich)) ([#1161](https://github.com/mihonapp/mihon/pull/1161))
|
||||
- Add back support for drag-and-drop category reordering ([@cuong-tran](https://github.com/cuong-tran)) ([#1427](https://github.com/mihonapp/mihon/pull/1427))
|
||||
- Add option to mark duplicate read chapters as read after library update or while reading ([@AntsyLich](https://github.com/AntsyLich)) ([#1785](https://github.com/mihonapp/mihon/pull/1785), [#1791](https://github.com/mihonapp/mihon/pull/1791), [#1870](https://github.com/mihonapp/mihon/pull/1870))
|
||||
- Display staff information on Anilist tracker search results ([@NarwhalHorns](https://github.com/NarwhalHorns)) ([#1810](https://github.com/mihonapp/mihon/pull/1810))
|
||||
- Add `id:` prefix search to library to search by internal DB ID ([@MajorTanya](https://github.com/MajorTanya)) ([#1856](https://github.com/mihonapp/mihon/pull/1856))
|
||||
- Add back option to disable unread chapter badge in library ([@AntsyLich](https://github.com/AntsyLich)) ([#1871](https://github.com/mihonapp/mihon/pull/1871))
|
||||
|
||||
### Changed
|
||||
- Sliders UI ([@AntsyLich](https://github.com/AntsyLich)) ([#1840](https://github.com/mihonapp/mihon/pull/1840))
|
||||
- Apply "Downloaded only" filter to all entries regardless of favourite status ([@NGB-Was-Taken](https://github.com/NGB-Was-Taken)) ([#1603](https://github.com/mihonapp/mihon/pull/1603))
|
||||
- Ignore hidden files/folders for Local Source chapter list ([@BrutuZ](https://github.com/BrutuZ)) ([#1763](https://github.com/mihonapp/mihon/pull/1763))
|
||||
- Migrate to newer Bangumi API ([@MajorTanya](https://github.com/MajorTanya)) ([#1748](https://github.com/mihonapp/mihon/pull/1748))
|
||||
- Now showing manga starting dates in search
|
||||
- Reduced request load by 2-4x in certain situations
|
||||
- Bump default user agent ([@AntsyLich](https://github.com/AntsyLich)) ([#1833](https://github.com/mihonapp/mihon/pull/1833))
|
||||
- Changed the label of chapter swipe settings and renamed the group to "Behavior" ([@AntsyLich](https://github.com/AntsyLich)) ([#1870](https://github.com/mihonapp/mihon/pull/1870))
|
||||
|
||||
### Fixed
|
||||
- Fix MAL `main_picture` nullability breaking search if a result doesn't have a cover set ([@MajorTanya](https://github.com/MajorTanya)) ([#1618](https://github.com/mihonapp/mihon/pull/1618))
|
||||
- Fix Bangumi and MAL tracking 401 errors due to Mihon sending expired credentials ([@MajorTanya](https://github.com/MajorTanya)) ([#1681](https://github.com/mihonapp/mihon/pull/1681), [#1682](https://github.com/mihonapp/mihon/pull/1682))
|
||||
- Fix certain Infinix, Xiaomi devices being unable to use any "Open link in browser" actions, including tracker setup ([@MajorTanya](https://github.com/MajorTanya)) ([#1684](https://github.com/mihonapp/mihon/pull/1684)) ([#1776](https://github.com/mihonapp/mihon/pull/1776))
|
||||
- Fix App's preferences referencing deleted categories ([@cuong-tran](https://github.com/cuong-tran)) ([#1734](https://github.com/mihonapp/mihon/pull/1734))
|
||||
- Fix backup/restore of category related preferences ([@cuong-tran](https://github.com/cuong-tran)) ([#1726](https://github.com/mihonapp/mihon/pull/1726))
|
||||
- Fix WebView sending app's package name in `X-Requested-With` header, which led to sources blocking access ([@AwkwardPeak7](https://github.com/AwkwardPeak7)) ([#1812](https://github.com/mihonapp/mihon/pull/1812))
|
||||
- Fix an issue where tracker reading progress is changed to a lower value ([@Animeboynz](https://github.com/Animeboynz)) ([#1795](https://github.com/mihonapp/mihon/pull/1795))
|
||||
- Attempt to fix crash when migrating or removing entries from library ([@FlaminSarge](https://github.com/FlaminSarge)) ([#1828](https://github.com/mihonapp/mihon/pull/1828))
|
||||
|
||||
### Removed
|
||||
- Remove alphabetical category sort option ([@AntsyLich](https://github.com/AntsyLich)) ([#1781](https://github.com/mihonapp/mihon/pull/1781))
|
||||
|
||||
### Other
|
||||
- Add zoned "Current time" to debug info and include year & timezone in logcat output ([@MajorTanya](https://github.com/MajorTanya)) ([#1672](https://github.com/mihonapp/mihon/pull/1672))
|
||||
- Add application package ID to debug info ([@MajorTanya](https://github.com/MajorTanya)) ([#1847](https://github.com/mihonapp/mihon/pull/1847))
|
||||
|
||||
## [v0.17.1] - 2024-12-06
|
||||
### Changed
|
||||
- Bump default user agent ([@AntsyLich](https://github.com/AntsyLich)) ([`76dcf90`](https://github.com/mihonapp/mihon/commit/76dcf903403d565056f44c66d965c1ea8affffc3))
|
||||
|
||||
### Improved
|
||||
- Bangumi search now shows the score and summary of a search result ([@MajorTanya](https://github.com/MajorTanya)) ([#1396](https://github.com/mihonapp/mihon/pull/1396))
|
||||
- Extension repo URLs are now auto-formatted ([@AntsyLich](https://github.com/AntsyLich), [@MajorTanya](https://github.com/MajorTanya))
|
||||
|
||||
- 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))
|
||||
@@ -60,7 +162,7 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co
|
||||
- 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))
|
||||
- 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))
|
||||
|
||||
@@ -78,7 +180,7 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co
|
||||
- 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))-
|
||||
- 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))
|
||||
@@ -149,7 +251,7 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co
|
||||
|
||||
### Other
|
||||
- Code cleanup
|
||||
- Minor refactor of theming when expressions ([@MajorTanya](https://github.com/MajorTanya)) ([#396](https://github.com/mihonapp/mihon/pull/396))
|
||||
- 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))
|
||||
@@ -178,7 +280,8 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co
|
||||
- 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))
|
||||
- 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))
|
||||
@@ -216,7 +319,7 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co
|
||||
- Fix extra date header introduced by parent PR ([@sirlag](https://github.com/sirlag)) ([#415](https://github.com/mihonapp/mihon/pull/415))
|
||||
- Fix build time in about screen displayed in UTC ([@AntsyLich](https://github.com/AntsyLich)) ([`aed53d3`](https://github.com/mihonapp/mihon/commit/aed53d3bdc85ce0e899fbb90b9f9cad0f1b86480))
|
||||
- App infinitely retries tracker update instead of failing after 3 tries ([@MajorTanya](https://github.com/MajorTanya)) ([#411](https://github.com/mihonapp/mihon/pull/411))
|
||||
- Crash on Pixel devices (was introduced due to compose update) ([`ab06720`](https://github.com/mihonapp/mihon/commit/ab067209661eceefc04c65f6bdbfcaa8a1264651))
|
||||
- Crash on Pixel devices (was introduced due to compose update) ([@AntsyLich](https://github.com/AntsyLich)) ([`ab06720`](https://github.com/mihonapp/mihon/commit/ab067209661eceefc04c65f6bdbfcaa8a1264651))
|
||||
- Crash when opening some heif/heic images ([@az4521](https://github.com/az4521)) ([#466](https://github.com/mihonapp/mihon/pull/466))
|
||||
- Crash when putting app in background while track date selection dialog is open ([@ivaniskandar](https://github.com/ivaniskandar)) ([`c348fac`](https://github.com/mihonapp/mihon/commit/c348fac78fac479fb123bd617c01c78b9ca851d5))
|
||||
- Dates for saved images not following the specification (fixes date issue mainly on Samsung devices) ([@MajorTanya](https://github.com/MajorTanya)) ([#552](https://github.com/mihonapp/mihon/pull/552))
|
||||
@@ -254,7 +357,7 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co
|
||||
|
||||
### Fixed
|
||||
- "Flash screen on page change" making the screen full black ([@AntsyLich](https://github.com/AntsyLich)) ([`38d6ab8`](https://github.com/mihonapp/mihon/commit/38d6ab80ce868707829dbc81de4170afe3c2f2a5))
|
||||
- Faulty MangaUpdates score in database ([@AntsyLich](https://github.com/AntsyLich) ([`a024218`](https://github.com/mihonapp/mihon/commit/a024218410953a389b8af4880fa7ae6cc30124a2)
|
||||
- Faulty MangaUpdates score in database ([@AntsyLich](https://github.com/AntsyLich)) ([`a024218`](https://github.com/mihonapp/mihon/commit/a024218410953a389b8af4880fa7ae6cc30124a2))
|
||||
- Updating extension not reflecting correctly ([@AntsyLich](https://github.com/AntsyLich)) ([`cb06898`](https://github.com/mihonapp/mihon/commit/cb068984303f811692531bf6f14902ae118d8ac7))
|
||||
- Inconsistent button height in "Data and storage" for some languages ([@theolm](https://github.com/theolm)) ([#202](https://github.com/mihonapp/mihon/pull/202))
|
||||
- Chapter not being marked as read locally when refreshing Enhanced Trackers ([@Secozzi](https://github.com/Secozzi)) ([#219](https://github.com/mihonapp/mihon/pull/219))
|
||||
@@ -279,7 +382,9 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co
|
||||
- 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.17.1...main
|
||||
[unreleased]: https://github.com/mihonapp/mihon/compare/v0.19.0...main
|
||||
[v0.18.0]: https://github.com/mihonapp/mihon/compare/v0.18.0...v0.19.0
|
||||
[v0.18.0]: https://github.com/mihonapp/mihon/compare/v0.17.1...v0.18.0
|
||||
[v0.17.1]: https://github.com/mihonapp/mihon/compare/v0.17.0...v0.17.1
|
||||
[v0.17.0]: https://github.com/mihonapp/mihon/compare/v0.16.5...v0.17.0
|
||||
[v0.16.5]: https://github.com/mihonapp/mihon/compare/v0.16.4...v0.16.5
|
||||
|
@@ -10,7 +10,7 @@
|
||||
Discover and read manga, webtoons, comics, and more – easier than ever on your Android device.
|
||||
|
||||
[](https://discord.gg/mihon)
|
||||
[](https://github.com/mihonapp/mihon/releases)
|
||||
[](https://mihon.app/download)
|
||||
|
||||
[](https://github.com/mihonapp/mihon/actions/workflows/build_push.yml)
|
||||
[](/LICENSE)
|
||||
@@ -18,8 +18,8 @@ Discover and read manga, webtoons, comics, and more – easier than ever on your
|
||||
|
||||
## Download
|
||||
|
||||
[](https://github.com/mihonapp/mihon/releases)
|
||||
[](https://github.com/mihonapp/mihon-preview/releases)
|
||||
[](https://mihon.app/download)
|
||||
[](https://mihon.app/download)
|
||||
|
||||
*Requires Android 8.0 or higher.*
|
||||
|
||||
|
@@ -1,5 +1,4 @@
|
||||
@file:Suppress("ChromeOsAbiSupport")
|
||||
|
||||
import mihon.buildlogic.Config
|
||||
import mihon.buildlogic.getBuildTime
|
||||
import mihon.buildlogic.getCommitCount
|
||||
import mihon.buildlogic.getGitSha
|
||||
@@ -12,7 +11,7 @@ plugins {
|
||||
alias(libs.plugins.aboutLibraries)
|
||||
}
|
||||
|
||||
if (gradle.startParameter.taskRequests.toString().contains("Standard")) {
|
||||
if (Config.includeTelemetry) {
|
||||
pluginManager.apply {
|
||||
apply(libs.plugins.google.services.get().pluginId)
|
||||
apply(libs.plugins.firebase.crashlytics.get().pluginId)
|
||||
@@ -21,69 +20,71 @@ if (gradle.startParameter.taskRequests.toString().contains("Standard")) {
|
||||
|
||||
shortcutHelper.setFilePath("./shortcuts.xml")
|
||||
|
||||
val supportedAbis = setOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
|
||||
|
||||
android {
|
||||
namespace = "eu.kanade.tachiyomi"
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "app.mihon"
|
||||
|
||||
versionCode = 9
|
||||
versionName = "0.17.1"
|
||||
versionCode = 12
|
||||
versionName = "0.19.0"
|
||||
|
||||
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
||||
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
|
||||
buildConfigField("String", "BUILD_TIME", "\"${getBuildTime()}\"")
|
||||
buildConfigField("boolean", "INCLUDE_UPDATER", "false")
|
||||
buildConfigField("boolean", "PREVIEW", "false")
|
||||
|
||||
ndk {
|
||||
abiFilters += supportedAbis
|
||||
}
|
||||
buildConfigField("String", "BUILD_TIME", "\"${getBuildTime(useLastCommitTime = false)}\"")
|
||||
buildConfigField("boolean", "TELEMETRY_INCLUDED", "${Config.includeTelemetry}")
|
||||
buildConfigField("boolean", "UPDATER_ENABLED", "${Config.enableUpdater}")
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
splits {
|
||||
abi {
|
||||
isEnable = true
|
||||
reset()
|
||||
include(*supportedAbis.toTypedArray())
|
||||
isUniversalApk = true
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
named("debug") {
|
||||
val debug by getting {
|
||||
applicationIdSuffix = ".dev"
|
||||
versionNameSuffix = "-${getCommitCount()}"
|
||||
applicationIdSuffix = ".debug"
|
||||
isPseudoLocalesEnabled = true
|
||||
}
|
||||
named("release") {
|
||||
isShrinkResources = true
|
||||
isMinifyEnabled = true
|
||||
val release by getting {
|
||||
isMinifyEnabled = Config.enableCodeShrink
|
||||
isShrinkResources = Config.enableCodeShrink
|
||||
|
||||
proguardFiles("proguard-android-optimize.txt", "proguard-rules.pro")
|
||||
|
||||
buildConfigField("String", "BUILD_TIME", "\"${getBuildTime(useLastCommitTime = true)}\"")
|
||||
}
|
||||
|
||||
val commonMatchingFallbacks = listOf(release.name)
|
||||
|
||||
create("foss") {
|
||||
initWith(release)
|
||||
|
||||
applicationIdSuffix = ".foss"
|
||||
|
||||
matchingFallbacks.addAll(commonMatchingFallbacks)
|
||||
}
|
||||
create("preview") {
|
||||
initWith(getByName("release"))
|
||||
buildConfigField("boolean", "PREVIEW", "true")
|
||||
initWith(release)
|
||||
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
matchingFallbacks.add("release")
|
||||
val debugType = getByName("debug")
|
||||
versionNameSuffix = debugType.versionNameSuffix
|
||||
applicationIdSuffix = debugType.applicationIdSuffix
|
||||
applicationIdSuffix = ".debug"
|
||||
|
||||
versionNameSuffix = debug.versionNameSuffix
|
||||
signingConfig = debug.signingConfig
|
||||
|
||||
matchingFallbacks.addAll(commonMatchingFallbacks)
|
||||
|
||||
buildConfigField("String", "BUILD_TIME", "\"${getBuildTime(useLastCommitTime = false)}\"")
|
||||
}
|
||||
create("benchmark") {
|
||||
initWith(getByName("release"))
|
||||
initWith(release)
|
||||
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
matchingFallbacks.add("release")
|
||||
isDebuggable = false
|
||||
isProfileable = true
|
||||
versionNameSuffix = "-benchmark"
|
||||
applicationIdSuffix = ".benchmark"
|
||||
|
||||
signingConfig = debug.signingConfig
|
||||
|
||||
matchingFallbacks.addAll(commonMatchingFallbacks)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,39 +93,46 @@ android {
|
||||
getByName("benchmark").res.srcDirs("src/debug/res")
|
||||
}
|
||||
|
||||
flavorDimensions.add("default")
|
||||
|
||||
productFlavors {
|
||||
create("standard") {
|
||||
buildConfigField("boolean", "INCLUDE_UPDATER", "true")
|
||||
dimension = "default"
|
||||
}
|
||||
create("dev") {
|
||||
// Include pseudolocales: https://developer.android.com/guide/topics/resources/pseudolocales
|
||||
resourceConfigurations.addAll(listOf("en", "en_XA", "ar_XB", "xxhdpi"))
|
||||
dimension = "default"
|
||||
splits {
|
||||
abi {
|
||||
isEnable = true
|
||||
isUniversalApk = true
|
||||
reset()
|
||||
include("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
|
||||
}
|
||||
}
|
||||
|
||||
packaging {
|
||||
resources.excludes.addAll(
|
||||
listOf(
|
||||
jniLibs {
|
||||
keepDebugSymbols += listOf(
|
||||
"libandroidx.graphics.path",
|
||||
"libarchive-jni",
|
||||
"libconscrypt_jni",
|
||||
"libimagedecoder",
|
||||
"libquickjs",
|
||||
"libsqlite3x",
|
||||
)
|
||||
.map { "**/$it.so" }
|
||||
}
|
||||
resources {
|
||||
excludes += setOf(
|
||||
"kotlin-tooling-metadata.json",
|
||||
"META-INF/DEPENDENCIES",
|
||||
"LICENSE.txt",
|
||||
"META-INF/LICENSE",
|
||||
"META-INF/**/*.properties",
|
||||
"META-INF/**/LICENSE.txt",
|
||||
"META-INF/*.properties",
|
||||
"META-INF/**/*.properties",
|
||||
"META-INF/README.md",
|
||||
"META-INF/NOTICE",
|
||||
"META-INF/*.version",
|
||||
),
|
||||
)
|
||||
"META-INF/DEPENDENCIES",
|
||||
"META-INF/LICENSE",
|
||||
"META-INF/NOTICE",
|
||||
"META-INF/README.md",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
dependenciesInfo {
|
||||
includeInApk = false
|
||||
includeInApk = Config.includeDependencyInfo
|
||||
includeInBundle = Config.includeDependencyInfo
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
@@ -172,6 +180,7 @@ dependencies {
|
||||
implementation(projects.domain)
|
||||
implementation(projects.presentationCore)
|
||||
implementation(projects.presentationWidget)
|
||||
implementation(projects.telemetry)
|
||||
|
||||
// Compose
|
||||
implementation(compose.activity)
|
||||
@@ -253,27 +262,28 @@ dependencies {
|
||||
exclude(group = "androidx.viewpager", module = "viewpager")
|
||||
}
|
||||
implementation(libs.insetter)
|
||||
implementation(libs.bundles.richtext)
|
||||
implementation(libs.richeditor.compose)
|
||||
implementation(libs.aboutLibraries.compose)
|
||||
implementation(libs.bundles.voyager)
|
||||
implementation(libs.compose.materialmotion)
|
||||
implementation(libs.swipe)
|
||||
implementation(libs.compose.webview)
|
||||
implementation(libs.compose.grid)
|
||||
implementation(libs.reorderable)
|
||||
implementation(libs.bundles.markdown)
|
||||
|
||||
// Logging
|
||||
implementation(libs.logcat)
|
||||
|
||||
// Crash reports/analytics
|
||||
"standardImplementation"(platform(libs.firebase.bom))
|
||||
"standardImplementation"(libs.firebase.analytics)
|
||||
"standardImplementation"(libs.firebase.crashlytics)
|
||||
|
||||
// Shizuku
|
||||
implementation(libs.bundles.shizuku)
|
||||
|
||||
// String similarity
|
||||
implementation(libs.stringSimilarity)
|
||||
|
||||
// Tests
|
||||
testImplementation(libs.bundles.test)
|
||||
testRuntimeOnly(libs.junit.platform.launcher)
|
||||
|
||||
// For detecting memory leaks; see https://square.github.io/leakcanary/
|
||||
// debugImplementation(libs.leakcanary.android)
|
||||
@@ -283,14 +293,6 @@ dependencies {
|
||||
}
|
||||
|
||||
androidComponents {
|
||||
beforeVariants { variantBuilder ->
|
||||
// Disables standardBenchmark
|
||||
if (variantBuilder.buildType == "benchmark") {
|
||||
variantBuilder.enable = variantBuilder.productFlavors.containsAll(
|
||||
listOf("default" to "dev"),
|
||||
)
|
||||
}
|
||||
}
|
||||
onVariants(selector().withFlavor("default" to "standard")) {
|
||||
// Only excluding in standard flavor because this breaks
|
||||
// Layout Inspector's Compose tree
|
||||
|
@@ -13,9 +13,11 @@ import eu.kanade.domain.manga.interactor.SetExcludedScanlators
|
||||
import eu.kanade.domain.manga.interactor.SetMangaViewerFlags
|
||||
import eu.kanade.domain.manga.interactor.UpdateManga
|
||||
import eu.kanade.domain.source.interactor.GetEnabledSources
|
||||
import eu.kanade.domain.source.interactor.GetIncognitoState
|
||||
import eu.kanade.domain.source.interactor.GetLanguagesWithSources
|
||||
import eu.kanade.domain.source.interactor.GetSourcesWithFavoriteCount
|
||||
import eu.kanade.domain.source.interactor.SetMigrateSorting
|
||||
import eu.kanade.domain.source.interactor.ToggleIncognito
|
||||
import eu.kanade.domain.source.interactor.ToggleLanguage
|
||||
import eu.kanade.domain.source.interactor.ToggleSource
|
||||
import eu.kanade.domain.source.interactor.ToggleSourcePin
|
||||
@@ -33,6 +35,7 @@ import mihon.domain.extensionrepo.interactor.ReplaceExtensionRepo
|
||||
import mihon.domain.extensionrepo.interactor.UpdateExtensionRepo
|
||||
import mihon.domain.extensionrepo.repository.ExtensionRepoRepository
|
||||
import mihon.domain.extensionrepo.service.ExtensionRepoService
|
||||
import mihon.domain.migration.usecases.MigrateMangaUseCase
|
||||
import mihon.domain.upcoming.interactor.GetUpcomingManga
|
||||
import tachiyomi.data.category.CategoryRepositoryImpl
|
||||
import tachiyomi.data.chapter.ChapterRepositoryImpl
|
||||
@@ -77,6 +80,7 @@ import tachiyomi.domain.manga.interactor.GetMangaWithChapters
|
||||
import tachiyomi.domain.manga.interactor.NetworkToLocalManga
|
||||
import tachiyomi.domain.manga.interactor.ResetViewerFlags
|
||||
import tachiyomi.domain.manga.interactor.SetMangaChapterFlags
|
||||
import tachiyomi.domain.manga.interactor.UpdateMangaNotes
|
||||
import tachiyomi.domain.manga.repository.MangaRepository
|
||||
import tachiyomi.domain.release.interactor.GetApplicationRelease
|
||||
import tachiyomi.domain.release.service.ReleaseService
|
||||
@@ -109,7 +113,7 @@ class DomainModule : InjektModule {
|
||||
addFactory { RenameCategory(get()) }
|
||||
addFactory { ReorderCategory(get()) }
|
||||
addFactory { UpdateCategory(get()) }
|
||||
addFactory { DeleteCategory(get()) }
|
||||
addFactory { DeleteCategory(get(), get(), get()) }
|
||||
|
||||
addSingletonFactory<MangaRepository> { MangaRepositoryImpl(get()) }
|
||||
addFactory { GetDuplicateLibraryManga(get()) }
|
||||
@@ -127,9 +131,15 @@ class DomainModule : InjektModule {
|
||||
addFactory { SetMangaViewerFlags(get()) }
|
||||
addFactory { NetworkToLocalManga(get()) }
|
||||
addFactory { UpdateManga(get(), get()) }
|
||||
addFactory { UpdateMangaNotes(get()) }
|
||||
addFactory { SetMangaCategories(get()) }
|
||||
addFactory { GetExcludedScanlators(get()) }
|
||||
addFactory { SetExcludedScanlators(get()) }
|
||||
addFactory {
|
||||
MigrateMangaUseCase(
|
||||
get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(),
|
||||
)
|
||||
}
|
||||
|
||||
addSingletonFactory<ReleaseService> { ReleaseServiceImpl(get(), get()) }
|
||||
addFactory { GetApplicationRelease(get(), get()) }
|
||||
@@ -151,7 +161,7 @@ class DomainModule : InjektModule {
|
||||
addFactory { UpdateChapter(get()) }
|
||||
addFactory { SetReadStatus(get(), get(), get(), get()) }
|
||||
addFactory { ShouldUpdateDbChapter() }
|
||||
addFactory { SyncChaptersWithSource(get(), get(), get(), get(), get(), get(), get(), get()) }
|
||||
addFactory { SyncChaptersWithSource(get(), get(), get(), get(), get(), get(), get(), get(), get()) }
|
||||
addFactory { GetAvailableScanlators(get()) }
|
||||
addFactory { FilterChaptersForDownload(get(), get(), get()) }
|
||||
|
||||
@@ -191,5 +201,7 @@ class DomainModule : InjektModule {
|
||||
addFactory { DeleteExtensionRepo(get()) }
|
||||
addFactory { ReplaceExtensionRepo(get()) }
|
||||
addFactory { UpdateExtensionRepo(get(), get()) }
|
||||
addFactory { ToggleIncognito(get()) }
|
||||
addFactory { GetIncognitoState(get(), get(), get()) }
|
||||
}
|
||||
}
|
||||
|
@@ -33,4 +33,6 @@ class BasePreferences(
|
||||
fun displayProfile() = preferenceStore.getString("pref_display_profile_key", "")
|
||||
|
||||
fun hardwareBitmapThreshold() = preferenceStore.getInt("pref_hardware_bitmap_threshold", GLUtil.SAFE_TEXTURE_LIMIT)
|
||||
|
||||
fun alwaysDecodeLongStripWithSSIV() = preferenceStore.getBoolean("pref_always_decode_long_strip_with_ssiv", false)
|
||||
}
|
||||
|
@@ -19,6 +19,7 @@ import tachiyomi.domain.chapter.model.NoChaptersException
|
||||
import tachiyomi.domain.chapter.model.toChapterUpdate
|
||||
import tachiyomi.domain.chapter.repository.ChapterRepository
|
||||
import tachiyomi.domain.chapter.service.ChapterRecognition
|
||||
import tachiyomi.domain.library.service.LibraryPreferences
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.source.local.isLocal
|
||||
import java.lang.Long.max
|
||||
@@ -34,6 +35,7 @@ class SyncChaptersWithSource(
|
||||
private val updateChapter: UpdateChapter,
|
||||
private val getChaptersByMangaId: GetChaptersByMangaId,
|
||||
private val getExcludedScanlators: GetExcludedScanlators,
|
||||
private val libraryPreferences: LibraryPreferences,
|
||||
) {
|
||||
|
||||
/**
|
||||
@@ -145,12 +147,18 @@ class SyncChaptersWithSource(
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
val reAdded = mutableListOf<Chapter>()
|
||||
val changedOrDuplicateReadUrls = mutableSetOf<String>()
|
||||
|
||||
val deletedChapterNumbers = TreeSet<Double>()
|
||||
val deletedReadChapterNumbers = TreeSet<Double>()
|
||||
val deletedBookmarkedChapterNumbers = TreeSet<Double>()
|
||||
|
||||
val readChapterNumbers = dbChapters
|
||||
.asSequence()
|
||||
.filter { it.read && it.isRecognizedNumber }
|
||||
.map { it.chapterNumber }
|
||||
.toSet()
|
||||
|
||||
removedChapters.forEach { chapter ->
|
||||
if (chapter.read) deletedReadChapterNumbers.add(chapter.chapterNumber)
|
||||
if (chapter.bookmark) deletedBookmarkedChapterNumbers.add(chapter.chapterNumber)
|
||||
@@ -160,12 +168,20 @@ class SyncChaptersWithSource(
|
||||
val deletedChapterNumberDateFetchMap = removedChapters.sortedByDescending { it.dateFetch }
|
||||
.associate { it.chapterNumber to it.dateFetch }
|
||||
|
||||
val markDuplicateAsRead = libraryPreferences.markDuplicateReadChapterAsRead().get()
|
||||
.contains(LibraryPreferences.MARK_DUPLICATE_CHAPTER_READ_NEW)
|
||||
|
||||
// Date fetch is set in such a way that the upper ones will have bigger value than the lower ones
|
||||
// Sources MUST return the chapters from most to less recent, which is common.
|
||||
var itemCount = newChapters.size
|
||||
var updatedToAdd = newChapters.map { toAddItem ->
|
||||
var chapter = toAddItem.copy(dateFetch = nowMillis + itemCount--)
|
||||
|
||||
if (chapter.chapterNumber in readChapterNumbers && markDuplicateAsRead) {
|
||||
changedOrDuplicateReadUrls.add(chapter.url)
|
||||
chapter = chapter.copy(read = true)
|
||||
}
|
||||
|
||||
if (!chapter.isRecognizedNumber || chapter.chapterNumber !in deletedChapterNumbers) return@map chapter
|
||||
|
||||
chapter = chapter.copy(
|
||||
@@ -178,7 +194,7 @@ class SyncChaptersWithSource(
|
||||
chapter = chapter.copy(dateFetch = it)
|
||||
}
|
||||
|
||||
reAdded.add(chapter)
|
||||
changedOrDuplicateReadUrls.add(chapter.url)
|
||||
|
||||
chapter
|
||||
}
|
||||
@@ -202,12 +218,8 @@ class SyncChaptersWithSource(
|
||||
// Note that last_update actually represents last time the chapter list changed at all
|
||||
updateManga.awaitUpdateLastUpdate(manga.id)
|
||||
|
||||
val reAddedUrls = reAdded.map { it.url }.toHashSet()
|
||||
|
||||
val excludedScanlators = getExcludedScanlators.await(manga.id).toHashSet()
|
||||
|
||||
return updatedToAdd.filterNot {
|
||||
it.url in reAddedUrls || it.scanlator in excludedScanlators
|
||||
}
|
||||
return updatedToAdd.filterNot { it.url in changedOrDuplicateReadUrls || it.scanlator in excludedScanlators }
|
||||
}
|
||||
}
|
||||
|
@@ -2,7 +2,9 @@ package eu.kanade.domain.manga.interactor
|
||||
|
||||
import eu.kanade.domain.manga.model.hasCustomCover
|
||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import tachiyomi.domain.library.service.LibraryPreferences
|
||||
import tachiyomi.domain.manga.interactor.FetchInterval
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.domain.manga.model.MangaUpdate
|
||||
@@ -31,6 +33,8 @@ class UpdateManga(
|
||||
remoteManga: SManga,
|
||||
manualFetch: Boolean,
|
||||
coverCache: CoverCache = Injekt.get(),
|
||||
libraryPreferences: LibraryPreferences = Injekt.get(),
|
||||
downloadManager: DownloadManager = Injekt.get(),
|
||||
): Boolean {
|
||||
val remoteTitle = try {
|
||||
remoteManga.title
|
||||
@@ -38,8 +42,13 @@ class UpdateManga(
|
||||
""
|
||||
}
|
||||
|
||||
// if the manga isn't a favorite, set its title from source and update in db
|
||||
val title = if (remoteTitle.isEmpty() || localManga.favorite) null else remoteTitle
|
||||
// if the manga isn't a favorite (or 'update titles' preference is enabled), set its title from source and update in db
|
||||
val title =
|
||||
if (remoteTitle.isNotEmpty() && (!localManga.favorite || libraryPreferences.updateMangaTitles().get())) {
|
||||
remoteTitle
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
val coverLastModified =
|
||||
when {
|
||||
@@ -59,7 +68,7 @@ class UpdateManga(
|
||||
|
||||
val thumbnailUrl = remoteManga.thumbnail_url?.takeIf { it.isNotEmpty() }
|
||||
|
||||
return mangaRepository.update(
|
||||
val success = mangaRepository.update(
|
||||
MangaUpdate(
|
||||
id = localManga.id,
|
||||
title = title,
|
||||
@@ -74,6 +83,10 @@ class UpdateManga(
|
||||
initialized = true,
|
||||
),
|
||||
)
|
||||
if (success && title != null) {
|
||||
downloadManager.renameManga(localManga, title)
|
||||
}
|
||||
return success
|
||||
}
|
||||
|
||||
suspend fun awaitUpdateFetchInterval(
|
||||
|
@@ -22,7 +22,7 @@ val Manga.readerOrientation: Long
|
||||
|
||||
val Manga.downloadedFilter: TriState
|
||||
get() {
|
||||
if (forceDownloaded()) return TriState.ENABLED_IS
|
||||
if (Injekt.get<BasePreferences>().downloadedOnly().get()) return TriState.ENABLED_IS
|
||||
return when (downloadedFilterRaw) {
|
||||
Manga.CHAPTER_SHOW_DOWNLOADED -> TriState.ENABLED_IS
|
||||
Manga.CHAPTER_SHOW_NOT_DOWNLOADED -> TriState.ENABLED_NOT
|
||||
@@ -34,9 +34,6 @@ fun Manga.chaptersFiltered(): Boolean {
|
||||
downloadedFilter != TriState.DISABLED ||
|
||||
bookmarkedFilter != TriState.DISABLED
|
||||
}
|
||||
fun Manga.forceDownloaded(): Boolean {
|
||||
return favorite && Injekt.get<BasePreferences>().downloadedOnly().get()
|
||||
}
|
||||
|
||||
fun Manga.toSManga(): SManga = SManga.create().also {
|
||||
it.url = url
|
||||
@@ -72,22 +69,6 @@ fun Manga.copyFrom(other: SManga): Manga {
|
||||
)
|
||||
}
|
||||
|
||||
fun SManga.toDomainManga(sourceId: Long): Manga {
|
||||
return Manga.create().copy(
|
||||
url = url,
|
||||
title = title,
|
||||
artist = artist,
|
||||
author = author,
|
||||
description = description,
|
||||
genre = getGenres(),
|
||||
status = status.toLong(),
|
||||
thumbnailUrl = thumbnail_url,
|
||||
updateStrategy = update_strategy,
|
||||
initialized = initialized,
|
||||
source = sourceId,
|
||||
)
|
||||
}
|
||||
|
||||
fun Manga.hasCustomCover(coverCache: CoverCache = Injekt.get()): Boolean {
|
||||
return coverCache.getCustomCoverFile(id).exists()
|
||||
}
|
||||
|
@@ -0,0 +1,35 @@
|
||||
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()
|
||||
}
|
||||
}
|
@@ -0,0 +1,14 @@
|
||||
package eu.kanade.domain.source.interactor
|
||||
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import tachiyomi.core.common.preference.getAndSet
|
||||
|
||||
class ToggleIncognito(
|
||||
private val preferences: SourcePreferences,
|
||||
) {
|
||||
fun await(extensions: String, enable: Boolean) {
|
||||
preferences.incognitoExtensions().getAndSet {
|
||||
if (enable) it.plus(extensions) else it.minus(extensions)
|
||||
}
|
||||
}
|
||||
}
|
@@ -2,16 +2,18 @@ package eu.kanade.domain.source.service
|
||||
|
||||
import eu.kanade.domain.source.interactor.SetMigrateSorting
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
import mihon.domain.migration.models.MigrationFlag
|
||||
import tachiyomi.core.common.preference.Preference
|
||||
import tachiyomi.core.common.preference.PreferenceStore
|
||||
import tachiyomi.core.common.preference.getEnum
|
||||
import tachiyomi.core.common.preference.getLongArray
|
||||
import tachiyomi.domain.library.model.LibraryDisplayMode
|
||||
|
||||
class SourcePreferences(
|
||||
private val preferenceStore: PreferenceStore,
|
||||
) {
|
||||
|
||||
fun sourceDisplayMode() = preferenceStore.getObject(
|
||||
fun sourceDisplayMode() = preferenceStore.getObjectFromString(
|
||||
"pref_display_mode_catalogue",
|
||||
LibraryDisplayMode.default,
|
||||
LibraryDisplayMode.Serializer::serialize,
|
||||
@@ -22,6 +24,8 @@ class SourcePreferences(
|
||||
|
||||
fun disabledSources() = preferenceStore.getStringSet("hidden_catalogues", emptySet())
|
||||
|
||||
fun incognitoExtensions() = preferenceStore.getStringSet("incognito_extensions", emptySet())
|
||||
|
||||
fun pinnedSources() = preferenceStore.getStringSet("pinned_catalogues", emptySet())
|
||||
|
||||
fun lastUsedSource() = preferenceStore.getLong(
|
||||
@@ -53,4 +57,21 @@ class SourcePreferences(
|
||||
Preference.appStateKey("has_filters_toggle_state"),
|
||||
false,
|
||||
)
|
||||
|
||||
fun migrationSources() = preferenceStore.getLongArray("migration_sources", emptyList())
|
||||
|
||||
fun migrationFlags() = preferenceStore.getObjectFromInt(
|
||||
key = "migration_flags",
|
||||
defaultValue = MigrationFlag.entries.toSet(),
|
||||
serializer = { MigrationFlag.toBit(it) },
|
||||
deserializer = { value: Int -> MigrationFlag.fromBit(value) },
|
||||
)
|
||||
|
||||
fun migrationDeepSearchMode() = preferenceStore.getBoolean("migration_deep_search", false)
|
||||
|
||||
fun migrationPrioritizeByChapters() = preferenceStore.getBoolean("migration_prioritize_by_chapters", false)
|
||||
|
||||
fun migrationHideUnmatched() = preferenceStore.getBoolean("migration_hide_unmatched", false)
|
||||
|
||||
fun migrationHideWithoutUpdates() = preferenceStore.getBoolean("migration_hide_without_updates", false)
|
||||
}
|
||||
|
@@ -10,6 +10,7 @@ fun Track.copyPersonalFrom(other: Track): Track {
|
||||
status = other.status,
|
||||
startDate = other.startDate,
|
||||
finishDate = other.finishDate,
|
||||
private = other.private,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -26,6 +27,7 @@ fun Track.toDbTrack(): DbTrack = DbTrack.create(trackerId).also {
|
||||
it.tracking_url = remoteUrl
|
||||
it.started_reading_date = startDate
|
||||
it.finished_reading_date = finishDate
|
||||
it.private = private
|
||||
}
|
||||
|
||||
fun DbTrack.toDomainTrack(idRequired: Boolean = true): Track? {
|
||||
@@ -44,5 +46,6 @@ fun DbTrack.toDomainTrack(idRequired: Boolean = true): Track? {
|
||||
remoteUrl = tracking_url,
|
||||
startDate = started_reading_date,
|
||||
finishDate = finished_reading_date,
|
||||
private = private,
|
||||
)
|
||||
}
|
||||
|
@@ -34,6 +34,8 @@ class UiPreferences(
|
||||
|
||||
fun tabletUiMode() = preferenceStore.getEnum("tablet_ui_mode", TabletUiMode.AUTOMATIC)
|
||||
|
||||
fun imagesInDescription() = preferenceStore.getBoolean("pref_render_images_description", true)
|
||||
|
||||
companion object {
|
||||
fun dateFormat(format: String): DateTimeFormatter = when (format) {
|
||||
"" -> DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT)
|
||||
|
@@ -1,25 +1,23 @@
|
||||
package eu.kanade.domain.ui.model
|
||||
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import eu.kanade.tachiyomi.util.system.isDevFlavor
|
||||
import eu.kanade.tachiyomi.util.system.isPreviewBuildType
|
||||
import tachiyomi.i18n.MR
|
||||
|
||||
enum class AppTheme(val titleRes: StringResource?) {
|
||||
DEFAULT(MR.strings.label_default),
|
||||
MONET(MR.strings.theme_monet),
|
||||
CATPPUCCIN(MR.strings.theme_catppuccin),
|
||||
GREEN_APPLE(MR.strings.theme_greenapple),
|
||||
LAVENDER(MR.strings.theme_lavender),
|
||||
MIDNIGHT_DUSK(MR.strings.theme_midnightdusk),
|
||||
|
||||
// TODO: re-enable for preview
|
||||
NORD(MR.strings.theme_nord.takeIf { isDevFlavor || isPreviewBuildType }),
|
||||
NORD(MR.strings.theme_nord),
|
||||
STRAWBERRY_DAIQUIRI(MR.strings.theme_strawberrydaiquiri),
|
||||
TAKO(MR.strings.theme_tako),
|
||||
TEALTURQUOISE(MR.strings.theme_tealturquoise),
|
||||
TIDAL_WAVE(MR.strings.theme_tidalwave),
|
||||
YINYANG(MR.strings.theme_yinyang),
|
||||
YOTSUBA(MR.strings.theme_yotsuba),
|
||||
MONOCHROME(MR.strings.theme_monochrome),
|
||||
|
||||
// Deprecated
|
||||
DARK_BLUE(null),
|
||||
|
@@ -73,10 +73,18 @@ fun BrowseSourceContent(
|
||||
}
|
||||
}
|
||||
|
||||
if (mangaList.itemCount <= 0 && errorState != null && errorState is LoadState.Error) {
|
||||
if (mangaList.itemCount == 0 && mangaList.loadState.refresh is LoadState.Loading) {
|
||||
LoadingScreen(Modifier.padding(contentPadding))
|
||||
return
|
||||
}
|
||||
|
||||
if (mangaList.itemCount == 0) {
|
||||
EmptyScreen(
|
||||
modifier = Modifier.padding(contentPadding),
|
||||
message = getErrorMessage(errorState),
|
||||
message = when (errorState) {
|
||||
is LoadState.Error -> getErrorMessage(errorState)
|
||||
else -> stringResource(MR.strings.no_results_found)
|
||||
},
|
||||
actions = if (source is LocalSource) {
|
||||
persistentListOf(
|
||||
EmptyScreenAction(
|
||||
@@ -109,13 +117,6 @@ fun BrowseSourceContent(
|
||||
return
|
||||
}
|
||||
|
||||
if (mangaList.itemCount == 0 && mangaList.loadState.refresh is LoadState.Loading) {
|
||||
LoadingScreen(
|
||||
modifier = Modifier.padding(contentPadding),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
when (displayMode) {
|
||||
LibraryDisplayMode.ComfortableGrid -> {
|
||||
BrowseSourceComfortableGrid(
|
||||
|
@@ -35,8 +35,10 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
@@ -48,6 +50,7 @@ import eu.kanade.presentation.components.AppBarActions
|
||||
import eu.kanade.presentation.components.WarningBanner
|
||||
import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget
|
||||
import eu.kanade.presentation.more.settings.widget.TrailingWidgetBuffer
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.extension.model.Extension
|
||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||
import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionDetailsScreenModel
|
||||
@@ -72,6 +75,7 @@ fun ExtensionDetailsScreen(
|
||||
onClickClearCookies: () -> Unit,
|
||||
onClickUninstall: () -> Unit,
|
||||
onClickSource: (sourceId: Long) -> Unit,
|
||||
onClickIncognito: (Boolean) -> Unit,
|
||||
) {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
val url = remember(state.extension) {
|
||||
@@ -140,9 +144,11 @@ fun ExtensionDetailsScreen(
|
||||
contentPadding = paddingValues,
|
||||
extension = state.extension,
|
||||
sources = state.sources,
|
||||
incognitoMode = state.isIncognito,
|
||||
onClickSourcePreferences = onClickSourcePreferences,
|
||||
onClickUninstall = onClickUninstall,
|
||||
onClickSource = onClickSource,
|
||||
onClickIncognito = onClickIncognito,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -152,9 +158,11 @@ private fun ExtensionDetails(
|
||||
contentPadding: PaddingValues,
|
||||
extension: Extension.Installed,
|
||||
sources: ImmutableList<ExtensionSourceItem>,
|
||||
incognitoMode: Boolean,
|
||||
onClickSourcePreferences: (sourceId: Long) -> Unit,
|
||||
onClickUninstall: () -> Unit,
|
||||
onClickSource: (sourceId: Long) -> Unit,
|
||||
onClickIncognito: (Boolean) -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
var showNsfwWarning by remember { mutableStateOf(false) }
|
||||
@@ -171,6 +179,7 @@ private fun ExtensionDetails(
|
||||
item {
|
||||
DetailsHeader(
|
||||
extension = extension,
|
||||
extIncognitoMode = incognitoMode,
|
||||
onClickUninstall = onClickUninstall,
|
||||
onClickAppInfo = {
|
||||
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
|
||||
@@ -182,6 +191,7 @@ private fun ExtensionDetails(
|
||||
onClickAgeRating = {
|
||||
showNsfwWarning = true
|
||||
},
|
||||
onExtIncognitoChange = onClickIncognito,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -209,9 +219,11 @@ private fun ExtensionDetails(
|
||||
@Composable
|
||||
private fun DetailsHeader(
|
||||
extension: Extension,
|
||||
extIncognitoMode: Boolean,
|
||||
onClickAgeRating: () -> Unit,
|
||||
onClickUninstall: () -> Unit,
|
||||
onClickAppInfo: (() -> Unit)?,
|
||||
onExtIncognitoChange: (Boolean) -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
@@ -219,9 +231,8 @@ private fun DetailsHeader(
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = MaterialTheme.padding.medium)
|
||||
.padding(
|
||||
start = MaterialTheme.padding.medium,
|
||||
end = MaterialTheme.padding.medium,
|
||||
top = MaterialTheme.padding.medium,
|
||||
bottom = MaterialTheme.padding.small,
|
||||
)
|
||||
@@ -313,12 +324,9 @@ private fun DetailsHeader(
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.padding(
|
||||
start = MaterialTheme.padding.medium,
|
||||
end = MaterialTheme.padding.medium,
|
||||
top = MaterialTheme.padding.small,
|
||||
bottom = MaterialTheme.padding.medium,
|
||||
),
|
||||
modifier = Modifier
|
||||
.padding(horizontal = MaterialTheme.padding.medium)
|
||||
.padding(top = MaterialTheme.padding.small),
|
||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.medium),
|
||||
) {
|
||||
OutlinedButton(
|
||||
@@ -341,6 +349,24 @@ 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()
|
||||
}
|
||||
}
|
||||
|
@@ -40,6 +40,7 @@ fun GlobalSearchScreen(
|
||||
navigateUp = navigateUp,
|
||||
onChangeSearchQuery = onChangeSearchQuery,
|
||||
onSearch = onSearch,
|
||||
hideSourceFilter = false,
|
||||
sourceFilter = state.sourceFilter,
|
||||
onChangeSearchFilter = onChangeSearchFilter,
|
||||
onlyShowHasResults = state.onlyShowHasResults,
|
||||
|
@@ -1,84 +0,0 @@
|
||||
package eu.kanade.presentation.browse
|
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import eu.kanade.presentation.components.AppBar
|
||||
import eu.kanade.presentation.manga.components.BaseMangaListItem
|
||||
import eu.kanade.tachiyomi.ui.browse.migration.manga.MigrateMangaScreenModel
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.FastScrollLazyColumn
|
||||
import tachiyomi.presentation.core.components.material.Scaffold
|
||||
import tachiyomi.presentation.core.screens.EmptyScreen
|
||||
|
||||
@Composable
|
||||
fun MigrateMangaScreen(
|
||||
navigateUp: () -> Unit,
|
||||
title: String?,
|
||||
state: MigrateMangaScreenModel.State,
|
||||
onClickItem: (Manga) -> Unit,
|
||||
onClickCover: (Manga) -> Unit,
|
||||
) {
|
||||
Scaffold(
|
||||
topBar = { scrollBehavior ->
|
||||
AppBar(
|
||||
title = title,
|
||||
navigateUp = navigateUp,
|
||||
scrollBehavior = scrollBehavior,
|
||||
)
|
||||
},
|
||||
) { contentPadding ->
|
||||
if (state.isEmpty) {
|
||||
EmptyScreen(
|
||||
stringRes = MR.strings.empty_screen,
|
||||
modifier = Modifier.padding(contentPadding),
|
||||
)
|
||||
return@Scaffold
|
||||
}
|
||||
|
||||
MigrateMangaContent(
|
||||
contentPadding = contentPadding,
|
||||
state = state,
|
||||
onClickItem = onClickItem,
|
||||
onClickCover = onClickCover,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MigrateMangaContent(
|
||||
contentPadding: PaddingValues,
|
||||
state: MigrateMangaScreenModel.State,
|
||||
onClickItem: (Manga) -> Unit,
|
||||
onClickCover: (Manga) -> Unit,
|
||||
) {
|
||||
FastScrollLazyColumn(
|
||||
contentPadding = contentPadding,
|
||||
) {
|
||||
items(state.titles) { manga ->
|
||||
MigrateMangaItem(
|
||||
manga = manga,
|
||||
onClickItem = onClickItem,
|
||||
onClickCover = onClickCover,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MigrateMangaItem(
|
||||
manga: Manga,
|
||||
onClickItem: (Manga) -> Unit,
|
||||
onClickCover: (Manga) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
BaseMangaListItem(
|
||||
modifier = modifier,
|
||||
manga = manga,
|
||||
onClickItem = { onClickItem(manga) },
|
||||
onClickCover = { onClickCover(manga) },
|
||||
)
|
||||
}
|
@@ -32,6 +32,7 @@ fun MigrateSearchScreen(
|
||||
navigateUp = navigateUp,
|
||||
onChangeSearchQuery = onChangeSearchQuery,
|
||||
onSearch = onSearch,
|
||||
hideSourceFilter = true,
|
||||
sourceFilter = state.sourceFilter,
|
||||
onChangeSearchFilter = onChangeSearchFilter,
|
||||
onlyShowHasResults = state.onlyShowHasResults,
|
||||
|
@@ -40,6 +40,7 @@ fun GlobalSearchToolbar(
|
||||
navigateUp: () -> Unit,
|
||||
onChangeSearchQuery: (String?) -> Unit,
|
||||
onSearch: (String) -> Unit,
|
||||
hideSourceFilter: Boolean,
|
||||
sourceFilter: SourceFilter,
|
||||
onChangeSearchFilter: (SourceFilter) -> Unit,
|
||||
onlyShowHasResults: Boolean,
|
||||
@@ -73,38 +74,40 @@ fun GlobalSearchToolbar(
|
||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
||||
) {
|
||||
// TODO: make this UX better; it only applies when triggering a new search
|
||||
FilterChip(
|
||||
selected = sourceFilter == SourceFilter.PinnedOnly,
|
||||
onClick = { onChangeSearchFilter(SourceFilter.PinnedOnly) },
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.PushPin,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.size(FilterChipDefaults.IconSize),
|
||||
)
|
||||
},
|
||||
label = {
|
||||
Text(text = stringResource(MR.strings.pinned_sources))
|
||||
},
|
||||
)
|
||||
FilterChip(
|
||||
selected = sourceFilter == SourceFilter.All,
|
||||
onClick = { onChangeSearchFilter(SourceFilter.All) },
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.DoneAll,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.size(FilterChipDefaults.IconSize),
|
||||
)
|
||||
},
|
||||
label = {
|
||||
Text(text = stringResource(MR.strings.all))
|
||||
},
|
||||
)
|
||||
if (!hideSourceFilter) {
|
||||
FilterChip(
|
||||
selected = sourceFilter == SourceFilter.PinnedOnly,
|
||||
onClick = { onChangeSearchFilter(SourceFilter.PinnedOnly) },
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.PushPin,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.size(FilterChipDefaults.IconSize),
|
||||
)
|
||||
},
|
||||
label = {
|
||||
Text(text = stringResource(MR.strings.pinned_sources))
|
||||
},
|
||||
)
|
||||
FilterChip(
|
||||
selected = sourceFilter == SourceFilter.All,
|
||||
onClick = { onChangeSearchFilter(SourceFilter.All) },
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.DoneAll,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.size(FilterChipDefaults.IconSize),
|
||||
)
|
||||
},
|
||||
label = {
|
||||
Text(text = stringResource(MR.strings.all))
|
||||
},
|
||||
)
|
||||
|
||||
VerticalDivider()
|
||||
VerticalDivider()
|
||||
}
|
||||
|
||||
FilterChip(
|
||||
selected = onlyShowHasResults,
|
||||
|
@@ -2,22 +2,24 @@ package eu.kanade.presentation.category
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.SortByAlpha
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.toMutableStateList
|
||||
import androidx.compose.ui.Modifier
|
||||
import eu.kanade.presentation.category.components.CategoryFloatingActionButton
|
||||
import eu.kanade.presentation.category.components.CategoryListItem
|
||||
import eu.kanade.presentation.components.AppBar
|
||||
import eu.kanade.presentation.components.AppBarActions
|
||||
import eu.kanade.tachiyomi.ui.category.CategoryScreenState
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import sh.calvin.reorderable.ReorderableItem
|
||||
import sh.calvin.reorderable.rememberReorderableLazyListState
|
||||
import tachiyomi.domain.category.model.Category
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.material.Scaffold
|
||||
@@ -31,11 +33,9 @@ import tachiyomi.presentation.core.util.plus
|
||||
fun CategoryScreen(
|
||||
state: CategoryScreenState.Success,
|
||||
onClickCreate: () -> Unit,
|
||||
onClickSortAlphabetically: () -> Unit,
|
||||
onClickRename: (Category) -> Unit,
|
||||
onClickDelete: (Category) -> Unit,
|
||||
onClickMoveUp: (Category) -> Unit,
|
||||
onClickMoveDown: (Category) -> Unit,
|
||||
onChangeOrder: (Category, Int) -> Unit,
|
||||
navigateUp: () -> Unit,
|
||||
) {
|
||||
val lazyListState = rememberLazyListState()
|
||||
@@ -44,17 +44,6 @@ fun CategoryScreen(
|
||||
AppBar(
|
||||
title = stringResource(MR.strings.action_edit_categories),
|
||||
navigateUp = navigateUp,
|
||||
actions = {
|
||||
AppBarActions(
|
||||
persistentListOf(
|
||||
AppBar.Action(
|
||||
title = stringResource(MR.strings.action_sort),
|
||||
icon = Icons.Outlined.SortByAlpha,
|
||||
onClick = onClickSortAlphabetically,
|
||||
),
|
||||
),
|
||||
)
|
||||
},
|
||||
scrollBehavior = scrollBehavior,
|
||||
)
|
||||
},
|
||||
@@ -76,13 +65,10 @@ fun CategoryScreen(
|
||||
CategoryContent(
|
||||
categories = state.categories,
|
||||
lazyListState = lazyListState,
|
||||
paddingValues = paddingValues +
|
||||
topSmallPaddingValues +
|
||||
PaddingValues(horizontal = MaterialTheme.padding.medium),
|
||||
paddingValues = paddingValues,
|
||||
onClickRename = onClickRename,
|
||||
onClickDelete = onClickDelete,
|
||||
onMoveUp = onClickMoveUp,
|
||||
onMoveDown = onClickMoveDown,
|
||||
onChangeOrder = onChangeOrder,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -94,28 +80,44 @@ private fun CategoryContent(
|
||||
paddingValues: PaddingValues,
|
||||
onClickRename: (Category) -> Unit,
|
||||
onClickDelete: (Category) -> Unit,
|
||||
onMoveUp: (Category) -> Unit,
|
||||
onMoveDown: (Category) -> Unit,
|
||||
onChangeOrder: (Category, Int) -> Unit,
|
||||
) {
|
||||
val categoriesState = remember { categories.toMutableStateList() }
|
||||
val reorderableState = rememberReorderableLazyListState(lazyListState, paddingValues) { from, to ->
|
||||
val item = categoriesState.removeAt(from.index)
|
||||
categoriesState.add(to.index, item)
|
||||
onChangeOrder(item, to.index)
|
||||
}
|
||||
|
||||
LaunchedEffect(categories) {
|
||||
if (!reorderableState.isAnyItemDragging) {
|
||||
categoriesState.clear()
|
||||
categoriesState.addAll(categories)
|
||||
}
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
state = lazyListState,
|
||||
contentPadding = paddingValues,
|
||||
contentPadding = paddingValues +
|
||||
topSmallPaddingValues +
|
||||
PaddingValues(horizontal = MaterialTheme.padding.medium),
|
||||
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
||||
) {
|
||||
itemsIndexed(
|
||||
items = categories,
|
||||
key = { _, category -> "category-${category.id}" },
|
||||
) { index, category ->
|
||||
CategoryListItem(
|
||||
modifier = Modifier.animateItem(),
|
||||
category = category,
|
||||
canMoveUp = index != 0,
|
||||
canMoveDown = index != categories.lastIndex,
|
||||
onMoveUp = onMoveUp,
|
||||
onMoveDown = onMoveDown,
|
||||
onRename = { onClickRename(category) },
|
||||
onDelete = { onClickDelete(category) },
|
||||
)
|
||||
items(
|
||||
items = categoriesState,
|
||||
key = { category -> category.key },
|
||||
) { category ->
|
||||
ReorderableItem(reorderableState, category.key) {
|
||||
CategoryListItem(
|
||||
modifier = Modifier.animateItem(),
|
||||
category = category,
|
||||
onRename = { onClickRename(category) },
|
||||
onDelete = { onClickDelete(category) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val Category.key inline get() = "category-$id"
|
||||
|
@@ -193,35 +193,6 @@ fun CategoryDeleteDialog(
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CategorySortAlphabeticallyDialog(
|
||||
onDismissRequest: () -> Unit,
|
||||
onSort: () -> Unit,
|
||||
) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
onSort()
|
||||
onDismissRequest()
|
||||
}) {
|
||||
Text(text = stringResource(MR.strings.action_ok))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismissRequest) {
|
||||
Text(text = stringResource(MR.strings.action_cancel))
|
||||
}
|
||||
},
|
||||
title = {
|
||||
Text(text = stringResource(MR.strings.action_sort_category))
|
||||
},
|
||||
text = {
|
||||
Text(text = stringResource(MR.strings.sort_category_confirmation))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChangeCategoryDialog(
|
||||
initialSelection: ImmutableList<CheckboxState<Category>>,
|
||||
|
@@ -2,14 +2,11 @@ package eu.kanade.presentation.category.components
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.Label
|
||||
import androidx.compose.material.icons.outlined.ArrowDropDown
|
||||
import androidx.compose.material.icons.outlined.ArrowDropUp
|
||||
import androidx.compose.material.icons.outlined.Delete
|
||||
import androidx.compose.material.icons.outlined.DragHandle
|
||||
import androidx.compose.material.icons.outlined.Edit
|
||||
import androidx.compose.material3.ElevatedCard
|
||||
import androidx.compose.material3.Icon
|
||||
@@ -19,57 +16,42 @@ import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import sh.calvin.reorderable.ReorderableCollectionItemScope
|
||||
import tachiyomi.domain.category.model.Category
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
|
||||
@Composable
|
||||
fun CategoryListItem(
|
||||
fun ReorderableCollectionItemScope.CategoryListItem(
|
||||
category: Category,
|
||||
canMoveUp: Boolean,
|
||||
canMoveDown: Boolean,
|
||||
onMoveUp: (Category) -> Unit,
|
||||
onMoveDown: (Category) -> Unit,
|
||||
onRename: () -> Unit,
|
||||
onDelete: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
ElevatedCard(
|
||||
modifier = modifier,
|
||||
) {
|
||||
ElevatedCard(modifier = modifier) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { onRename() }
|
||||
.clickable(onClick = onRename)
|
||||
.padding(vertical = MaterialTheme.padding.small)
|
||||
.padding(
|
||||
start = MaterialTheme.padding.medium,
|
||||
top = MaterialTheme.padding.medium,
|
||||
start = MaterialTheme.padding.small,
|
||||
end = MaterialTheme.padding.medium,
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(imageVector = Icons.AutoMirrored.Outlined.Label, contentDescription = null)
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.DragHandle,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.padding(MaterialTheme.padding.medium)
|
||||
.draggableHandle(),
|
||||
)
|
||||
Text(
|
||||
text = category.name,
|
||||
modifier = Modifier
|
||||
.padding(start = MaterialTheme.padding.medium),
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
}
|
||||
Row {
|
||||
IconButton(
|
||||
onClick = { onMoveUp(category) },
|
||||
enabled = canMoveUp,
|
||||
) {
|
||||
Icon(imageVector = Icons.Outlined.ArrowDropUp, contentDescription = null)
|
||||
}
|
||||
IconButton(
|
||||
onClick = { onMoveDown(category) },
|
||||
enabled = canMoveDown,
|
||||
) {
|
||||
Icon(imageVector = Icons.Outlined.ArrowDropDown, contentDescription = null)
|
||||
}
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
IconButton(onClick = onRename) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Edit,
|
||||
@@ -77,7 +59,10 @@ fun CategoryListItem(
|
||||
)
|
||||
}
|
||||
IconButton(onClick = onDelete) {
|
||||
Icon(imageVector = Icons.Outlined.Delete, contentDescription = stringResource(MR.strings.action_delete))
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Delete,
|
||||
contentDescription = stringResource(MR.strings.action_delete),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,10 +1,9 @@
|
||||
package eu.kanade.presentation.components
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.animation.SizeTransform
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.togetherWith
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.window.Dialog
|
||||
@@ -28,20 +27,14 @@ fun NavigatorAdaptiveSheet(
|
||||
screen = screen,
|
||||
content = { sheetNavigator ->
|
||||
AdaptiveSheet(
|
||||
enableSwipeDismiss = enableSwipeDismiss(sheetNavigator),
|
||||
onDismissRequest = onDismissRequest,
|
||||
enableSwipeDismiss = enableSwipeDismiss(sheetNavigator),
|
||||
) {
|
||||
ScreenTransition(
|
||||
navigator = sheetNavigator,
|
||||
transition = {
|
||||
fadeIn(animationSpec = tween(220, delayMillis = 90)) togetherWith
|
||||
fadeOut(animationSpec = tween(90))
|
||||
},
|
||||
)
|
||||
|
||||
BackHandler(
|
||||
enabled = sheetNavigator.size > 1,
|
||||
onBack = sheetNavigator::pop,
|
||||
enterTransition = { fadeIn(animationSpec = tween(220, delayMillis = 90)) },
|
||||
exitTransition = { fadeOut(animationSpec = tween(90)) },
|
||||
sizeTransform = { SizeTransform() },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -79,10 +72,10 @@ fun AdaptiveSheet(
|
||||
properties = dialogProperties,
|
||||
) {
|
||||
AdaptiveSheetImpl(
|
||||
modifier = modifier,
|
||||
isTabletUi = isTabletUi,
|
||||
enableSwipeDismiss = enableSwipeDismiss,
|
||||
onDismissRequest = onDismissRequest,
|
||||
modifier = modifier,
|
||||
) {
|
||||
content()
|
||||
}
|
||||
|
@@ -36,6 +36,7 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusDirection
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.graphics.Color
|
||||
@@ -201,6 +202,7 @@ fun AppBarActions(
|
||||
}
|
||||
},
|
||||
state = rememberTooltipState(),
|
||||
focusable = false,
|
||||
) {
|
||||
IconButton(
|
||||
onClick = it.onClick,
|
||||
@@ -225,6 +227,7 @@ fun AppBarActions(
|
||||
}
|
||||
},
|
||||
state = rememberTooltipState(),
|
||||
focusable = false,
|
||||
) {
|
||||
IconButton(
|
||||
onClick = { showMenu = !showMenu },
|
||||
@@ -289,6 +292,7 @@ fun SearchToolbar(
|
||||
onSearch(searchQuery)
|
||||
focusManager.clearFocus()
|
||||
keyboardController?.hide()
|
||||
focusManager.moveFocus(FocusDirection.Next)
|
||||
}
|
||||
|
||||
BasicTextField(
|
||||
@@ -352,6 +356,7 @@ fun SearchToolbar(
|
||||
}
|
||||
},
|
||||
state = rememberTooltipState(),
|
||||
focusable = false,
|
||||
) {
|
||||
IconButton(
|
||||
onClick = onClick,
|
||||
@@ -371,6 +376,7 @@ fun SearchToolbar(
|
||||
}
|
||||
},
|
||||
state = rememberTooltipState(),
|
||||
focusable = false,
|
||||
) {
|
||||
IconButton(
|
||||
onClick = {
|
||||
|
@@ -1,9 +1,11 @@
|
||||
package eu.kanade.presentation.components
|
||||
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.DpOffset
|
||||
import eu.kanade.presentation.manga.DownloadAction
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import tachiyomi.i18n.MR
|
||||
@@ -15,7 +17,41 @@ fun DownloadDropdownMenu(
|
||||
expanded: Boolean,
|
||||
onDismissRequest: () -> Unit,
|
||||
onDownloadClicked: (DownloadAction) -> Unit,
|
||||
offset: DpOffset? = null,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
if (offset != null) {
|
||||
DropdownMenu(
|
||||
expanded = expanded,
|
||||
onDismissRequest = onDismissRequest,
|
||||
modifier = modifier,
|
||||
offset = offset,
|
||||
content = {
|
||||
DownloadDropdownMenuItems(
|
||||
onDismissRequest = onDismissRequest,
|
||||
onDownloadClicked = onDownloadClicked,
|
||||
)
|
||||
},
|
||||
)
|
||||
} else {
|
||||
DropdownMenu(
|
||||
expanded = expanded,
|
||||
onDismissRequest = onDismissRequest,
|
||||
modifier = modifier,
|
||||
content = {
|
||||
DownloadDropdownMenuItems(
|
||||
onDismissRequest = onDismissRequest,
|
||||
onDownloadClicked = onDownloadClicked,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ColumnScope.DownloadDropdownMenuItems(
|
||||
onDismissRequest: () -> Unit,
|
||||
onDownloadClicked: (DownloadAction) -> Unit,
|
||||
) {
|
||||
val options = persistentListOf(
|
||||
DownloadAction.NEXT_1_CHAPTER to pluralStringResource(MR.plurals.download_amount, 1, 1),
|
||||
@@ -25,19 +61,13 @@ fun DownloadDropdownMenu(
|
||||
DownloadAction.UNREAD_CHAPTERS to stringResource(MR.strings.download_unread),
|
||||
)
|
||||
|
||||
DropdownMenu(
|
||||
expanded = expanded,
|
||||
onDismissRequest = onDismissRequest,
|
||||
modifier = modifier,
|
||||
) {
|
||||
options.map { (downloadAction, string) ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(text = string) },
|
||||
onClick = {
|
||||
onDownloadClicked(downloadAction)
|
||||
onDismissRequest()
|
||||
},
|
||||
)
|
||||
}
|
||||
options.map { (downloadAction, string) ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(text = string) },
|
||||
onClick = {
|
||||
onDownloadClicked(downloadAction)
|
||||
onDismissRequest()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@@ -38,6 +38,7 @@ fun HistoryScreen(
|
||||
onSearchQueryChange: (String?) -> Unit,
|
||||
onClickCover: (mangaId: Long) -> Unit,
|
||||
onClickResume: (mangaId: Long, chapterId: Long) -> Unit,
|
||||
onClickFavorite: (mangaId: Long) -> Unit,
|
||||
onDialogChange: (HistoryScreenModel.Dialog?) -> Unit,
|
||||
) {
|
||||
Scaffold(
|
||||
@@ -84,6 +85,7 @@ fun HistoryScreen(
|
||||
onClickCover = { history -> onClickCover(history.mangaId) },
|
||||
onClickResume = { history -> onClickResume(history.mangaId, history.chapterId) },
|
||||
onClickDelete = { item -> onDialogChange(HistoryScreenModel.Dialog.Delete(item)) },
|
||||
onClickFavorite = { history -> onClickFavorite(history.mangaId) },
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -97,6 +99,7 @@ private fun HistoryScreenContent(
|
||||
onClickCover: (HistoryWithRelations) -> Unit,
|
||||
onClickResume: (HistoryWithRelations) -> Unit,
|
||||
onClickDelete: (HistoryWithRelations) -> Unit,
|
||||
onClickFavorite: (HistoryWithRelations) -> Unit,
|
||||
) {
|
||||
FastScrollLazyColumn(
|
||||
contentPadding = contentPadding,
|
||||
@@ -126,6 +129,7 @@ private fun HistoryScreenContent(
|
||||
onClickCover = { onClickCover(value) },
|
||||
onClickResume = { onClickResume(value) },
|
||||
onClickDelete = { onClickDelete(value) },
|
||||
onClickFavorite = { onClickFavorite(value) },
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -152,6 +156,7 @@ internal fun HistoryScreenPreviews(
|
||||
onClickCover = {},
|
||||
onClickResume = { _, _ -> run {} },
|
||||
onDialogChange = {},
|
||||
onClickFavorite = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Delete
|
||||
import androidx.compose.material.icons.outlined.FavoriteBorder
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
@@ -39,6 +40,7 @@ fun HistoryItem(
|
||||
onClickCover: () -> Unit,
|
||||
onClickResume: () -> Unit,
|
||||
onClickDelete: () -> Unit,
|
||||
onClickFavorite: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Row(
|
||||
@@ -82,6 +84,16 @@ fun HistoryItem(
|
||||
)
|
||||
}
|
||||
|
||||
if (!history.coverData.isMangaFavorite) {
|
||||
IconButton(onClick = onClickFavorite) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.FavoriteBorder,
|
||||
contentDescription = stringResource(MR.strings.add_to_library),
|
||||
tint = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
IconButton(onClick = onClickDelete) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Delete,
|
||||
@@ -105,6 +117,7 @@ private fun HistoryItemPreviews(
|
||||
onClickCover = {},
|
||||
onClickResume = {},
|
||||
onClickDelete = {},
|
||||
onClickFavorite = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@@ -9,6 +9,7 @@ import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Refresh
|
||||
import androidx.compose.material3.FilterChip
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
@@ -19,8 +20,7 @@ import androidx.compose.ui.platform.LocalConfiguration
|
||||
import eu.kanade.presentation.components.TabbedDialog
|
||||
import eu.kanade.presentation.components.TabbedDialogPaddings
|
||||
import eu.kanade.tachiyomi.ui.library.LibrarySettingsScreenModel
|
||||
import eu.kanade.tachiyomi.util.system.isDevFlavor
|
||||
import eu.kanade.tachiyomi.util.system.isPreviewBuildType
|
||||
import eu.kanade.tachiyomi.util.system.isReleaseBuildType
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import tachiyomi.core.common.preference.TriState
|
||||
import tachiyomi.domain.category.model.Category
|
||||
@@ -117,10 +117,7 @@ private fun ColumnScope.FilterPage(
|
||||
onClick = { screenModel.toggleFilter(LibraryPreferences::filterCompleted) },
|
||||
)
|
||||
// TODO: re-enable when custom intervals are ready for stable
|
||||
if (
|
||||
(isDevFlavor || isPreviewBuildType) &&
|
||||
LibraryPreferences.MANGA_OUTSIDE_RELEASE_PERIOD in autoUpdateMangaRestrictions
|
||||
) {
|
||||
if ((!isReleaseBuildType) && LibraryPreferences.MANGA_OUTSIDE_RELEASE_PERIOD in autoUpdateMangaRestrictions) {
|
||||
val filterIntervalCustom by screenModel.libraryPreferences.filterIntervalCustom().collectAsState()
|
||||
TriStateItem(
|
||||
label = stringResource(MR.strings.action_filter_interval_custom),
|
||||
@@ -255,15 +252,16 @@ private fun ColumnScope.DisplayPage(
|
||||
|
||||
val columns by columnPreference.collectAsState()
|
||||
SliderItem(
|
||||
label = stringResource(MR.strings.pref_library_columns),
|
||||
max = 10,
|
||||
value = columns,
|
||||
valueRange = 0..10,
|
||||
label = stringResource(MR.strings.pref_library_columns),
|
||||
valueText = if (columns > 0) {
|
||||
stringResource(MR.strings.pref_library_columns_per_row, columns)
|
||||
columns.toString()
|
||||
} else {
|
||||
stringResource(MR.strings.label_default)
|
||||
stringResource(MR.strings.label_auto)
|
||||
},
|
||||
onChange = columnPreference::set,
|
||||
pillColor = MaterialTheme.colorScheme.surfaceContainerHighest,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -272,6 +270,10 @@ private fun ColumnScope.DisplayPage(
|
||||
label = stringResource(MR.strings.action_display_download_badge),
|
||||
pref = screenModel.libraryPreferences.downloadBadge(),
|
||||
)
|
||||
CheckboxItem(
|
||||
label = stringResource(MR.strings.action_display_unread_badge),
|
||||
pref = screenModel.libraryPreferences.unreadBadge(),
|
||||
)
|
||||
CheckboxItem(
|
||||
label = stringResource(MR.strings.action_display_local_badge),
|
||||
pref = screenModel.libraryPreferences.localBadge(),
|
||||
|
@@ -5,7 +5,6 @@ import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.lazy.grid.items
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.util.fastAny
|
||||
import eu.kanade.tachiyomi.ui.library.LibraryItem
|
||||
import tachiyomi.domain.library.model.LibraryManga
|
||||
import tachiyomi.domain.manga.model.MangaCover
|
||||
@@ -15,7 +14,7 @@ internal fun LibraryComfortableGrid(
|
||||
items: List<LibraryItem>,
|
||||
columns: Int,
|
||||
contentPadding: PaddingValues,
|
||||
selection: List<LibraryManga>,
|
||||
selection: Set<Long>,
|
||||
onClick: (LibraryManga) -> Unit,
|
||||
onLongClick: (LibraryManga) -> Unit,
|
||||
onClickContinueReading: ((LibraryManga) -> Unit)?,
|
||||
@@ -35,7 +34,7 @@ internal fun LibraryComfortableGrid(
|
||||
) { libraryItem ->
|
||||
val manga = libraryItem.libraryManga.manga
|
||||
MangaComfortableGridItem(
|
||||
isSelected = selection.fastAny { it.id == libraryItem.libraryManga.id },
|
||||
isSelected = manga.id in selection,
|
||||
title = manga.title,
|
||||
coverData = MangaCover(
|
||||
mangaId = manga.id,
|
||||
|
@@ -5,7 +5,6 @@ import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.lazy.grid.items
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.util.fastAny
|
||||
import eu.kanade.tachiyomi.ui.library.LibraryItem
|
||||
import tachiyomi.domain.library.model.LibraryManga
|
||||
import tachiyomi.domain.manga.model.MangaCover
|
||||
@@ -16,7 +15,7 @@ internal fun LibraryCompactGrid(
|
||||
showTitle: Boolean,
|
||||
columns: Int,
|
||||
contentPadding: PaddingValues,
|
||||
selection: List<LibraryManga>,
|
||||
selection: Set<Long>,
|
||||
onClick: (LibraryManga) -> Unit,
|
||||
onLongClick: (LibraryManga) -> Unit,
|
||||
onClickContinueReading: ((LibraryManga) -> Unit)?,
|
||||
@@ -36,7 +35,7 @@ internal fun LibraryCompactGrid(
|
||||
) { libraryItem ->
|
||||
val manga = libraryItem.libraryManga.manga
|
||||
MangaCompactGridItem(
|
||||
isSelected = selection.fastAny { it.id == libraryItem.libraryManga.id },
|
||||
isSelected = manga.id in selection,
|
||||
title = manga.title.takeIf { showTitle },
|
||||
coverData = MangaCover(
|
||||
mangaId = manga.id,
|
||||
|
@@ -29,22 +29,22 @@ import kotlin.time.Duration.Companion.seconds
|
||||
fun LibraryContent(
|
||||
categories: List<Category>,
|
||||
searchQuery: String?,
|
||||
selection: List<LibraryManga>,
|
||||
selection: Set<Long>,
|
||||
contentPadding: PaddingValues,
|
||||
currentPage: () -> Int,
|
||||
currentPage: Int,
|
||||
hasActiveFilters: Boolean,
|
||||
showPageTabs: Boolean,
|
||||
onChangeCurrentPage: (Int) -> Unit,
|
||||
onMangaClicked: (Long) -> Unit,
|
||||
onClickManga: (Long) -> Unit,
|
||||
onContinueReadingClicked: ((LibraryManga) -> Unit)?,
|
||||
onToggleSelection: (LibraryManga) -> Unit,
|
||||
onToggleRangeSelection: (LibraryManga) -> Unit,
|
||||
onRefresh: (Category?) -> Boolean,
|
||||
onToggleSelection: (Category, LibraryManga) -> Unit,
|
||||
onToggleRangeSelection: (Category, LibraryManga) -> Unit,
|
||||
onRefresh: () -> Boolean,
|
||||
onGlobalSearchClicked: () -> Unit,
|
||||
getNumberOfMangaForCategory: (Category) -> Int?,
|
||||
getItemCountForCategory: (Category) -> Int?,
|
||||
getDisplayMode: (Int) -> PreferenceMutableState<LibraryDisplayMode>,
|
||||
getColumnsForOrientation: (Boolean) -> PreferenceMutableState<Int>,
|
||||
getLibraryForPage: (Int) -> List<LibraryItem>,
|
||||
getItemsForCategory: (Category) -> List<LibraryItem>,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(
|
||||
@@ -53,13 +53,12 @@ fun LibraryContent(
|
||||
end = contentPadding.calculateEndPadding(LocalLayoutDirection.current),
|
||||
),
|
||||
) {
|
||||
val coercedCurrentPage = remember { currentPage().coerceAtMost(categories.lastIndex) }
|
||||
val pagerState = rememberPagerState(coercedCurrentPage) { categories.size }
|
||||
val pagerState = rememberPagerState(currentPage) { categories.size }
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
var isRefreshing by remember(pagerState.currentPage) { mutableStateOf(false) }
|
||||
|
||||
if (showPageTabs && categories.size > 1) {
|
||||
if (showPageTabs && categories.isNotEmpty()) {
|
||||
LaunchedEffect(categories) {
|
||||
if (categories.size <= pagerState.currentPage) {
|
||||
pagerState.scrollToPage(categories.size - 1)
|
||||
@@ -68,23 +67,20 @@ fun LibraryContent(
|
||||
LibraryTabs(
|
||||
categories = categories,
|
||||
pagerState = pagerState,
|
||||
getNumberOfMangaForCategory = getNumberOfMangaForCategory,
|
||||
) { scope.launch { pagerState.animateScrollToPage(it) } }
|
||||
}
|
||||
|
||||
val notSelectionMode = selection.isEmpty()
|
||||
val onClickManga = { manga: LibraryManga ->
|
||||
if (notSelectionMode) {
|
||||
onMangaClicked(manga.manga.id)
|
||||
} else {
|
||||
onToggleSelection(manga)
|
||||
}
|
||||
getItemCountForCategory = getItemCountForCategory,
|
||||
onTabItemClick = {
|
||||
scope.launch {
|
||||
pagerState.animateScrollToPage(it)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
PullRefresh(
|
||||
refreshing = isRefreshing,
|
||||
enabled = selection.isEmpty(),
|
||||
onRefresh = {
|
||||
val started = onRefresh(categories[currentPage()])
|
||||
val started = onRefresh()
|
||||
if (!started) return@PullRefresh
|
||||
scope.launch {
|
||||
// Fake refresh status but hide it after a second as it's a long running task
|
||||
@@ -93,19 +89,25 @@ fun LibraryContent(
|
||||
isRefreshing = false
|
||||
}
|
||||
},
|
||||
enabled = notSelectionMode,
|
||||
) {
|
||||
LibraryPager(
|
||||
state = pagerState,
|
||||
contentPadding = PaddingValues(bottom = contentPadding.calculateBottomPadding()),
|
||||
hasActiveFilters = hasActiveFilters,
|
||||
selectedManga = selection,
|
||||
selection = selection,
|
||||
searchQuery = searchQuery,
|
||||
onGlobalSearchClicked = onGlobalSearchClicked,
|
||||
getCategoryForPage = { page -> categories[page] },
|
||||
getDisplayMode = getDisplayMode,
|
||||
getColumnsForOrientation = getColumnsForOrientation,
|
||||
getLibraryForPage = getLibraryForPage,
|
||||
onClickManga = onClickManga,
|
||||
getItemsForCategory = getItemsForCategory,
|
||||
onClickManga = { category, manga ->
|
||||
if (selection.isNotEmpty()) {
|
||||
onToggleSelection(category, manga)
|
||||
} else {
|
||||
onClickManga(manga.manga.id)
|
||||
}
|
||||
},
|
||||
onLongClickManga = onToggleRangeSelection,
|
||||
onClickContinueReading = onContinueReadingClicked,
|
||||
)
|
||||
|
@@ -7,7 +7,6 @@ import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.fastAny
|
||||
import eu.kanade.tachiyomi.ui.library.LibraryItem
|
||||
import tachiyomi.domain.library.model.LibraryManga
|
||||
import tachiyomi.domain.manga.model.MangaCover
|
||||
@@ -18,7 +17,7 @@ import tachiyomi.presentation.core.util.plus
|
||||
internal fun LibraryList(
|
||||
items: List<LibraryItem>,
|
||||
contentPadding: PaddingValues,
|
||||
selection: List<LibraryManga>,
|
||||
selection: Set<Long>,
|
||||
onClick: (LibraryManga) -> Unit,
|
||||
onLongClick: (LibraryManga) -> Unit,
|
||||
onClickContinueReading: ((LibraryManga) -> Unit)?,
|
||||
@@ -45,7 +44,7 @@ internal fun LibraryList(
|
||||
) { libraryItem ->
|
||||
val manga = libraryItem.libraryManga.manga
|
||||
MangaListItem(
|
||||
isSelected = selection.fastAny { it.id == libraryItem.libraryManga.id },
|
||||
isSelected = manga.id in selection,
|
||||
title = manga.title,
|
||||
coverData = MangaCover(
|
||||
mangaId = manga.id,
|
||||
|
@@ -20,6 +20,7 @@ import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.core.preference.PreferenceMutableState
|
||||
import eu.kanade.tachiyomi.ui.library.LibraryItem
|
||||
import tachiyomi.domain.category.model.Category
|
||||
import tachiyomi.domain.library.model.LibraryDisplayMode
|
||||
import tachiyomi.domain.library.model.LibraryManga
|
||||
import tachiyomi.i18n.MR
|
||||
@@ -31,14 +32,15 @@ fun LibraryPager(
|
||||
state: PagerState,
|
||||
contentPadding: PaddingValues,
|
||||
hasActiveFilters: Boolean,
|
||||
selectedManga: List<LibraryManga>,
|
||||
selection: Set<Long>,
|
||||
searchQuery: String?,
|
||||
onGlobalSearchClicked: () -> Unit,
|
||||
getCategoryForPage: (Int) -> Category,
|
||||
getDisplayMode: (Int) -> PreferenceMutableState<LibraryDisplayMode>,
|
||||
getColumnsForOrientation: (Boolean) -> PreferenceMutableState<Int>,
|
||||
getLibraryForPage: (Int) -> List<LibraryItem>,
|
||||
onClickManga: (LibraryManga) -> Unit,
|
||||
onLongClickManga: (LibraryManga) -> Unit,
|
||||
getItemsForCategory: (Category) -> List<LibraryItem>,
|
||||
onClickManga: (Category, LibraryManga) -> Unit,
|
||||
onLongClickManga: (Category, LibraryManga) -> Unit,
|
||||
onClickContinueReading: ((LibraryManga) -> Unit)?,
|
||||
) {
|
||||
HorizontalPager(
|
||||
@@ -50,9 +52,10 @@ fun LibraryPager(
|
||||
// To make sure only one offscreen page is being composed
|
||||
return@HorizontalPager
|
||||
}
|
||||
val library = getLibraryForPage(page)
|
||||
val category = getCategoryForPage(page)
|
||||
val items = getItemsForCategory(category)
|
||||
|
||||
if (library.isEmpty()) {
|
||||
if (items.isEmpty()) {
|
||||
LibraryPagerEmptyScreen(
|
||||
searchQuery = searchQuery,
|
||||
hasActiveFilters = hasActiveFilters,
|
||||
@@ -72,12 +75,15 @@ fun LibraryPager(
|
||||
remember { mutableIntStateOf(0) }
|
||||
}
|
||||
|
||||
val onClickManga: (LibraryManga) -> Unit = { onClickManga(category, it) }
|
||||
val onLongClickManga: (LibraryManga) -> Unit = { onLongClickManga(category, it) }
|
||||
|
||||
when (displayMode) {
|
||||
LibraryDisplayMode.List -> {
|
||||
LibraryList(
|
||||
items = library,
|
||||
items = items,
|
||||
contentPadding = contentPadding,
|
||||
selection = selectedManga,
|
||||
selection = selection,
|
||||
onClick = onClickManga,
|
||||
onLongClick = onLongClickManga,
|
||||
onClickContinueReading = onClickContinueReading,
|
||||
@@ -87,11 +93,11 @@ fun LibraryPager(
|
||||
}
|
||||
LibraryDisplayMode.CompactGrid, LibraryDisplayMode.CoverOnlyGrid -> {
|
||||
LibraryCompactGrid(
|
||||
items = library,
|
||||
items = items,
|
||||
showTitle = displayMode is LibraryDisplayMode.CompactGrid,
|
||||
columns = columns,
|
||||
contentPadding = contentPadding,
|
||||
selection = selectedManga,
|
||||
selection = selection,
|
||||
onClick = onClickManga,
|
||||
onLongClick = onLongClickManga,
|
||||
onClickContinueReading = onClickContinueReading,
|
||||
@@ -101,10 +107,10 @@ fun LibraryPager(
|
||||
}
|
||||
LibraryDisplayMode.ComfortableGrid -> {
|
||||
LibraryComfortableGrid(
|
||||
items = library,
|
||||
items = items,
|
||||
columns = columns,
|
||||
contentPadding = contentPadding,
|
||||
selection = selectedManga,
|
||||
selection = selection,
|
||||
onClick = onClickManga,
|
||||
onLongClick = onLongClickManga,
|
||||
onClickContinueReading = onClickContinueReading,
|
||||
|
@@ -18,13 +18,11 @@ import tachiyomi.presentation.core.components.material.TabText
|
||||
internal fun LibraryTabs(
|
||||
categories: List<Category>,
|
||||
pagerState: PagerState,
|
||||
getNumberOfMangaForCategory: (Category) -> Int?,
|
||||
getItemCountForCategory: (Category) -> Int?,
|
||||
onTabItemClick: (Int) -> Unit,
|
||||
) {
|
||||
val currentPageIndex = pagerState.currentPage.coerceAtMost(categories.lastIndex)
|
||||
Column(
|
||||
modifier = Modifier.zIndex(1f),
|
||||
) {
|
||||
Column(modifier = Modifier.zIndex(2f)) {
|
||||
PrimaryScrollableTabRow(
|
||||
selectedTabIndex = currentPageIndex,
|
||||
edgePadding = 0.dp,
|
||||
@@ -39,7 +37,7 @@ internal fun LibraryTabs(
|
||||
text = {
|
||||
TabText(
|
||||
text = category.visualName,
|
||||
badgeCount = getNumberOfMangaForCategory(category),
|
||||
badgeCount = getItemCountForCategory(category),
|
||||
)
|
||||
},
|
||||
unselectedContentColor = MaterialTheme.colorScheme.onSurface,
|
||||
|
@@ -21,13 +21,14 @@ import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.domain.base.BasePreferences
|
||||
import eu.kanade.domain.manga.model.downloadedFilter
|
||||
import eu.kanade.domain.manga.model.forceDownloaded
|
||||
import eu.kanade.presentation.components.TabbedDialog
|
||||
import eu.kanade.presentation.components.TabbedDialogPaddings
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
@@ -40,6 +41,8 @@ import tachiyomi.presentation.core.components.SortItem
|
||||
import tachiyomi.presentation.core.components.TriStateItem
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import tachiyomi.presentation.core.theme.active
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
@Composable
|
||||
fun ChapterSettingsDialog(
|
||||
@@ -63,6 +66,8 @@ fun ChapterSettingsDialog(
|
||||
)
|
||||
}
|
||||
|
||||
val downloadedOnly = remember { Injekt.get<BasePreferences>().downloadedOnly().get() }
|
||||
|
||||
TabbedDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
tabTitles = persistentListOf(
|
||||
@@ -97,7 +102,7 @@ fun ChapterSettingsDialog(
|
||||
FilterPage(
|
||||
downloadFilter = manga?.downloadedFilter ?: TriState.DISABLED,
|
||||
onDownloadFilterChanged = onDownloadFilterChanged
|
||||
.takeUnless { manga?.forceDownloaded() == true },
|
||||
.takeUnless { downloadedOnly },
|
||||
unreadFilter = manga?.unreadFilter ?: TriState.DISABLED,
|
||||
onUnreadFilterChanged = onUnreadFilterChanged,
|
||||
bookmarkedFilter = manga?.bookmarkedFilter ?: TriState.DISABLED,
|
||||
|
@@ -1,44 +1,95 @@
|
||||
package eu.kanade.presentation.manga
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.sizeIn
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.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.Book
|
||||
import androidx.compose.material.icons.outlined.SwapVert
|
||||
import androidx.compose.material.icons.outlined.AttachMoney
|
||||
import androidx.compose.material.icons.outlined.Block
|
||||
import androidx.compose.material.icons.outlined.Close
|
||||
import androidx.compose.material.icons.outlined.Done
|
||||
import androidx.compose.material.icons.outlined.DoneAll
|
||||
import androidx.compose.material.icons.outlined.Pause
|
||||
import androidx.compose.material.icons.outlined.Schedule
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.Typography
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.text.TextMeasurer
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.rememberTextMeasurer
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.Constraints
|
||||
import androidx.compose.ui.unit.Density
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.util.fastMaxOfOrNull
|
||||
import coil3.request.ImageRequest
|
||||
import coil3.request.crossfade
|
||||
import eu.kanade.presentation.components.AdaptiveSheet
|
||||
import eu.kanade.presentation.components.TabbedDialogPaddings
|
||||
import eu.kanade.presentation.manga.components.MangaCover
|
||||
import eu.kanade.presentation.more.settings.LocalPreferenceMinHeight
|
||||
import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.domain.manga.model.MangaWithChapterCount
|
||||
import tachiyomi.domain.source.model.StubSource
|
||||
import tachiyomi.domain.source.service.SourceManager
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.Badge
|
||||
import tachiyomi.presentation.core.components.BadgeGroup
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
import tachiyomi.presentation.core.i18n.pluralStringResource
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import tachiyomi.presentation.core.util.secondaryItemAlpha
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
@Composable
|
||||
fun DuplicateMangaDialog(
|
||||
duplicates: List<MangaWithChapterCount>,
|
||||
onDismissRequest: () -> Unit,
|
||||
onConfirm: () -> Unit,
|
||||
onOpenManga: () -> Unit,
|
||||
onMigrate: () -> Unit,
|
||||
onOpenManga: (manga: Manga) -> Unit,
|
||||
onMigrate: (manga: Manga) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val sourceManager = remember { Injekt.get<SourceManager>() }
|
||||
val minHeight = LocalPreferenceMinHeight.current
|
||||
val horizontalPadding = PaddingValues(horizontal = TabbedDialogPaddings.Horizontal)
|
||||
val horizontalPaddingModifier = Modifier.padding(horizontalPadding)
|
||||
|
||||
AdaptiveSheet(
|
||||
modifier = modifier,
|
||||
@@ -46,81 +97,310 @@ fun DuplicateMangaDialog(
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
vertical = TabbedDialogPaddings.Vertical,
|
||||
horizontal = TabbedDialogPaddings.Horizontal,
|
||||
)
|
||||
.padding(vertical = TabbedDialogPaddings.Vertical)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.medium),
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.padding(TitlePadding),
|
||||
text = stringResource(MR.strings.are_you_sure),
|
||||
text = stringResource(MR.strings.possible_duplicates_title),
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
modifier = Modifier
|
||||
.then(horizontalPaddingModifier)
|
||||
.padding(top = MaterialTheme.padding.small),
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResource(MR.strings.confirm_add_duplicate_manga),
|
||||
text = stringResource(MR.strings.possible_duplicates_summary),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier.then(horizontalPaddingModifier),
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(PaddingSize))
|
||||
|
||||
TextPreferenceWidget(
|
||||
title = stringResource(MR.strings.action_show_manga),
|
||||
icon = Icons.Outlined.Book,
|
||||
onPreferenceClick = {
|
||||
onDismissRequest()
|
||||
onOpenManga()
|
||||
},
|
||||
)
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
TextPreferenceWidget(
|
||||
title = stringResource(MR.strings.action_migrate_duplicate),
|
||||
icon = Icons.Outlined.SwapVert,
|
||||
onPreferenceClick = {
|
||||
onDismissRequest()
|
||||
onMigrate()
|
||||
},
|
||||
)
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
TextPreferenceWidget(
|
||||
title = stringResource(MR.strings.action_add_anyway),
|
||||
icon = Icons.Outlined.Add,
|
||||
onPreferenceClick = {
|
||||
onDismissRequest()
|
||||
onConfirm()
|
||||
},
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.sizeIn(minHeight = minHeight)
|
||||
.clickable { onDismissRequest.invoke() }
|
||||
.padding(ButtonPadding)
|
||||
.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
LazyRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
||||
modifier = Modifier.height(getMaximumMangaCardHeight(duplicates)),
|
||||
contentPadding = horizontalPadding,
|
||||
) {
|
||||
OutlinedButton(onClick = onDismissRequest, modifier = Modifier.fillMaxWidth()) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.padding(vertical = 8.dp),
|
||||
text = stringResource(MR.strings.action_cancel),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontSize = 16.sp,
|
||||
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) {
|
||||
HorizontalDivider()
|
||||
|
||||
TextPreferenceWidget(
|
||||
title = stringResource(MR.strings.action_add_anyway),
|
||||
icon = Icons.Outlined.Add,
|
||||
onPreferenceClick = {
|
||||
onDismissRequest()
|
||||
onConfirm()
|
||||
},
|
||||
modifier = Modifier.clip(CircleShape),
|
||||
)
|
||||
}
|
||||
|
||||
OutlinedButton(
|
||||
onClick = onDismissRequest,
|
||||
modifier = Modifier
|
||||
.then(horizontalPaddingModifier)
|
||||
.padding(bottom = MaterialTheme.padding.medium)
|
||||
.heightIn(min = minHeight)
|
||||
.fillMaxWidth(),
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.padding(vertical = MaterialTheme.padding.extraSmall),
|
||||
text = stringResource(MR.strings.action_cancel),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val PaddingSize = 16.dp
|
||||
@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()
|
||||
onMigrate()
|
||||
},
|
||||
)
|
||||
.padding(MaterialTheme.padding.small),
|
||||
) {
|
||||
Box {
|
||||
MangaCover.Book(
|
||||
data = ImageRequest.Builder(LocalContext.current)
|
||||
.data(manga)
|
||||
.crossfade(true)
|
||||
.build(),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
BadgeGroup(
|
||||
modifier = Modifier
|
||||
.padding(4.dp)
|
||||
.align(Alignment.TopStart),
|
||||
) {
|
||||
Badge(
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
textColor = MaterialTheme.colorScheme.onSecondary,
|
||||
text = pluralStringResource(
|
||||
MR.plurals.manga_num_chapters,
|
||||
duplicate.chapterCount.toInt(),
|
||||
duplicate.chapterCount,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private val ButtonPadding = PaddingValues(top = 16.dp, bottom = 16.dp)
|
||||
private val TitlePadding = PaddingValues(bottom = 16.dp, top = 8.dp)
|
||||
Spacer(modifier = Modifier.height(MaterialTheme.padding.extraSmall))
|
||||
|
||||
Text(
|
||||
text = manga.title,
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 2,
|
||||
)
|
||||
|
||||
if (!manga.author.isNullOrBlank()) {
|
||||
MangaDetailRow(
|
||||
text = manga.author!!,
|
||||
iconImageVector = Icons.Filled.PersonOutline,
|
||||
maxLines = 2,
|
||||
)
|
||||
}
|
||||
|
||||
if (!manga.artist.isNullOrBlank() && manga.author != manga.artist) {
|
||||
MangaDetailRow(
|
||||
text = manga.artist!!,
|
||||
iconImageVector = Icons.Filled.Brush,
|
||||
maxLines = 2,
|
||||
)
|
||||
}
|
||||
|
||||
MangaDetailRow(
|
||||
text = when (manga.status) {
|
||||
SManga.ONGOING.toLong() -> stringResource(MR.strings.ongoing)
|
||||
SManga.COMPLETED.toLong() -> stringResource(MR.strings.completed)
|
||||
SManga.LICENSED.toLong() -> stringResource(MR.strings.licensed)
|
||||
SManga.PUBLISHING_FINISHED.toLong() -> stringResource(MR.strings.publishing_finished)
|
||||
SManga.CANCELLED.toLong() -> stringResource(MR.strings.cancelled)
|
||||
SManga.ON_HIATUS.toLong() -> stringResource(MR.strings.on_hiatus)
|
||||
else -> stringResource(MR.strings.unknown)
|
||||
},
|
||||
iconImageVector = when (manga.status) {
|
||||
SManga.ONGOING.toLong() -> Icons.Outlined.Schedule
|
||||
SManga.COMPLETED.toLong() -> Icons.Outlined.DoneAll
|
||||
SManga.LICENSED.toLong() -> Icons.Outlined.AttachMoney
|
||||
SManga.PUBLISHING_FINISHED.toLong() -> Icons.Outlined.Done
|
||||
SManga.CANCELLED.toLong() -> Icons.Outlined.Close
|
||||
SManga.ON_HIATUS.toLong() -> Icons.Outlined.Pause
|
||||
else -> Icons.Outlined.Block
|
||||
},
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
) {
|
||||
if (source is StubSource) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Warning,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(16.dp),
|
||||
tint = MaterialTheme.colorScheme.error,
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = source.name,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MangaDetailRow(
|
||||
text: String,
|
||||
iconImageVector: ImageVector,
|
||||
maxLines: Int = 1,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.secondaryItemAlpha()
|
||||
.padding(top = MaterialTheme.padding.extraSmall),
|
||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = iconImageVector,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(MangaDetailsIconWidth),
|
||||
)
|
||||
Text(
|
||||
text = text,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = maxLines,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getMaximumMangaCardHeight(duplicates: List<MangaWithChapterCount>): Dp {
|
||||
val density = LocalDensity.current
|
||||
val typography = MaterialTheme.typography
|
||||
val textMeasurer = rememberTextMeasurer()
|
||||
|
||||
val smallPadding = with(density) { MaterialTheme.padding.small.roundToPx() }
|
||||
val extraSmallPadding = with(density) { MaterialTheme.padding.extraSmall.roundToPx() }
|
||||
|
||||
val width = with(density) { MangaCardWidth.roundToPx() - (2 * smallPadding) }
|
||||
val iconWidth = with(density) { MangaDetailsIconWidth.roundToPx() }
|
||||
|
||||
val coverHeight = width / MangaCover.Book.ratio
|
||||
val constraints = Constraints(maxWidth = width)
|
||||
val detailsConstraints = Constraints(maxWidth = width - iconWidth - extraSmallPadding)
|
||||
|
||||
return remember(
|
||||
duplicates,
|
||||
density,
|
||||
typography,
|
||||
textMeasurer,
|
||||
smallPadding,
|
||||
extraSmallPadding,
|
||||
coverHeight,
|
||||
constraints,
|
||||
detailsConstraints,
|
||||
) {
|
||||
duplicates.fastMaxOfOrNull {
|
||||
calculateMangaCardHeight(
|
||||
manga = it.manga,
|
||||
density = density,
|
||||
typography = typography,
|
||||
textMeasurer = textMeasurer,
|
||||
smallPadding = smallPadding,
|
||||
extraSmallPadding = extraSmallPadding,
|
||||
coverHeight = coverHeight,
|
||||
constraints = constraints,
|
||||
detailsConstraints = detailsConstraints,
|
||||
)
|
||||
}
|
||||
?: 0.dp
|
||||
}
|
||||
}
|
||||
|
||||
private fun calculateMangaCardHeight(
|
||||
manga: Manga,
|
||||
density: Density,
|
||||
typography: Typography,
|
||||
textMeasurer: TextMeasurer,
|
||||
smallPadding: Int,
|
||||
extraSmallPadding: Int,
|
||||
coverHeight: Float,
|
||||
constraints: Constraints,
|
||||
detailsConstraints: Constraints,
|
||||
): Dp {
|
||||
val titleHeight = textMeasurer.measureHeight(manga.title, typography.titleSmall, 2, constraints)
|
||||
val authorHeight = if (!manga.author.isNullOrBlank()) {
|
||||
textMeasurer.measureHeight(manga.author!!, typography.bodySmall, 2, detailsConstraints)
|
||||
} else {
|
||||
0
|
||||
}
|
||||
val artistHeight = if (!manga.artist.isNullOrBlank() && manga.author != manga.artist) {
|
||||
textMeasurer.measureHeight(manga.artist!!, typography.bodySmall, 2, detailsConstraints)
|
||||
} else {
|
||||
0
|
||||
}
|
||||
val statusHeight = textMeasurer.measureHeight("", typography.bodySmall, 2, detailsConstraints)
|
||||
val sourceHeight = textMeasurer.measureHeight("", typography.labelSmall, 1, constraints)
|
||||
|
||||
val totalHeight = coverHeight + titleHeight + authorHeight + artistHeight + statusHeight + sourceHeight
|
||||
return with(density) { ((2 * smallPadding) + totalHeight + (5 * extraSmallPadding)).toDp() }
|
||||
}
|
||||
|
||||
private fun TextMeasurer.measureHeight(
|
||||
text: String,
|
||||
style: TextStyle,
|
||||
maxLines: Int,
|
||||
constraints: Constraints,
|
||||
): Int = measure(
|
||||
text = text,
|
||||
style = style,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = maxLines,
|
||||
constraints = constraints,
|
||||
)
|
||||
.size
|
||||
.height
|
||||
|
||||
private val MangaCardWidth = 150.dp
|
||||
private val MangaDetailsIconWidth = 16.dp
|
||||
|
@@ -0,0 +1,45 @@
|
||||
package eu.kanade.presentation.manga
|
||||
|
||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import eu.kanade.presentation.components.AppBar
|
||||
import eu.kanade.presentation.components.AppBarTitle
|
||||
import eu.kanade.presentation.manga.components.MangaNotesTextArea
|
||||
import eu.kanade.tachiyomi.ui.manga.notes.MangaNotesScreen
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.material.Scaffold
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
|
||||
@Composable
|
||||
fun MangaNotesScreen(
|
||||
state: MangaNotesScreen.State,
|
||||
navigateUp: () -> Unit,
|
||||
onUpdate: (String) -> Unit,
|
||||
) {
|
||||
Scaffold(
|
||||
topBar = { topBarScrollBehavior ->
|
||||
AppBar(
|
||||
titleContent = {
|
||||
AppBarTitle(
|
||||
title = stringResource(MR.strings.action_edit_notes),
|
||||
subtitle = state.manga.title,
|
||||
)
|
||||
},
|
||||
navigateUp = navigateUp,
|
||||
scrollBehavior = topBarScrollBehavior,
|
||||
)
|
||||
},
|
||||
) { contentPadding ->
|
||||
MangaNotesTextArea(
|
||||
state = state,
|
||||
onUpdate = onUpdate,
|
||||
modifier = Modifier
|
||||
.padding(contentPadding)
|
||||
.consumeWindowInsets(contentPadding)
|
||||
.imePadding(),
|
||||
)
|
||||
}
|
||||
}
|
@@ -87,7 +87,7 @@ fun MangaScreen(
|
||||
isTabletUi: Boolean,
|
||||
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
||||
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
|
||||
onBackClicked: () -> Unit,
|
||||
navigateUp: () -> Unit,
|
||||
onChapterClicked: (Chapter) -> Unit,
|
||||
onDownloadChapter: ((List<ChapterList.Item>, ChapterDownloadAction) -> Unit)?,
|
||||
onAddToLibraryClicked: () -> Unit,
|
||||
@@ -112,6 +112,7 @@ fun MangaScreen(
|
||||
onEditCategoryClicked: (() -> Unit)?,
|
||||
onEditFetchIntervalClicked: (() -> Unit)?,
|
||||
onMigrateClicked: (() -> Unit)?,
|
||||
onEditNotesClicked: () -> Unit,
|
||||
|
||||
// For bottom action menu
|
||||
onMultiBookmarkClicked: (List<Chapter>, bookmarked: Boolean) -> Unit,
|
||||
@@ -141,7 +142,7 @@ fun MangaScreen(
|
||||
nextUpdate = nextUpdate,
|
||||
chapterSwipeStartAction = chapterSwipeStartAction,
|
||||
chapterSwipeEndAction = chapterSwipeEndAction,
|
||||
onBackClicked = onBackClicked,
|
||||
navigateUp = navigateUp,
|
||||
onChapterClicked = onChapterClicked,
|
||||
onDownloadChapter = onDownloadChapter,
|
||||
onAddToLibraryClicked = onAddToLibraryClicked,
|
||||
@@ -160,6 +161,7 @@ fun MangaScreen(
|
||||
onEditCategoryClicked = onEditCategoryClicked,
|
||||
onEditIntervalClicked = onEditFetchIntervalClicked,
|
||||
onMigrateClicked = onMigrateClicked,
|
||||
onEditNotesClicked = onEditNotesClicked,
|
||||
onMultiBookmarkClicked = onMultiBookmarkClicked,
|
||||
onMultiMarkAsReadClicked = onMultiMarkAsReadClicked,
|
||||
onMarkPreviousAsReadClicked = onMarkPreviousAsReadClicked,
|
||||
@@ -176,7 +178,7 @@ fun MangaScreen(
|
||||
chapterSwipeStartAction = chapterSwipeStartAction,
|
||||
chapterSwipeEndAction = chapterSwipeEndAction,
|
||||
nextUpdate = nextUpdate,
|
||||
onBackClicked = onBackClicked,
|
||||
navigateUp = navigateUp,
|
||||
onChapterClicked = onChapterClicked,
|
||||
onDownloadChapter = onDownloadChapter,
|
||||
onAddToLibraryClicked = onAddToLibraryClicked,
|
||||
@@ -195,6 +197,7 @@ fun MangaScreen(
|
||||
onEditCategoryClicked = onEditCategoryClicked,
|
||||
onEditIntervalClicked = onEditFetchIntervalClicked,
|
||||
onMigrateClicked = onMigrateClicked,
|
||||
onEditNotesClicked = onEditNotesClicked,
|
||||
onMultiBookmarkClicked = onMultiBookmarkClicked,
|
||||
onMultiMarkAsReadClicked = onMultiMarkAsReadClicked,
|
||||
onMarkPreviousAsReadClicked = onMarkPreviousAsReadClicked,
|
||||
@@ -214,7 +217,7 @@ private fun MangaScreenSmallImpl(
|
||||
nextUpdate: Instant?,
|
||||
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
||||
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
|
||||
onBackClicked: () -> Unit,
|
||||
navigateUp: () -> Unit,
|
||||
onChapterClicked: (Chapter) -> Unit,
|
||||
onDownloadChapter: ((List<ChapterList.Item>, ChapterDownloadAction) -> Unit)?,
|
||||
onAddToLibraryClicked: () -> Unit,
|
||||
@@ -240,6 +243,7 @@ private fun MangaScreenSmallImpl(
|
||||
onEditCategoryClicked: (() -> Unit)?,
|
||||
onEditIntervalClicked: (() -> Unit)?,
|
||||
onMigrateClicked: (() -> Unit)?,
|
||||
onEditNotesClicked: () -> Unit,
|
||||
|
||||
// For bottom action menu
|
||||
onMultiBookmarkClicked: (List<Chapter>, bookmarked: Boolean) -> Unit,
|
||||
@@ -265,14 +269,9 @@ private fun MangaScreenSmallImpl(
|
||||
)
|
||||
}
|
||||
|
||||
val internalOnBackPressed = {
|
||||
if (isAnySelected) {
|
||||
onAllChapterSelected(false)
|
||||
} else {
|
||||
onBackClicked()
|
||||
}
|
||||
BackHandler(enabled = isAnySelected) {
|
||||
onAllChapterSelected(false)
|
||||
}
|
||||
BackHandler(onBack = internalOnBackPressed)
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
@@ -285,29 +284,31 @@ private fun MangaScreenSmallImpl(
|
||||
val isFirstItemScrolled by remember {
|
||||
derivedStateOf { chapterListState.firstVisibleItemScrollOffset > 0 }
|
||||
}
|
||||
val animatedTitleAlpha by animateFloatAsState(
|
||||
val titleAlpha by animateFloatAsState(
|
||||
if (!isFirstItemVisible) 1f else 0f,
|
||||
label = "Top Bar Title",
|
||||
)
|
||||
val animatedBgAlpha by animateFloatAsState(
|
||||
val backgroundAlpha by animateFloatAsState(
|
||||
if (!isFirstItemVisible || isFirstItemScrolled) 1f else 0f,
|
||||
label = "Top Bar Background",
|
||||
)
|
||||
MangaToolbar(
|
||||
title = state.manga.title,
|
||||
titleAlphaProvider = { animatedTitleAlpha },
|
||||
backgroundAlphaProvider = { animatedBgAlpha },
|
||||
hasFilters = state.filterActive,
|
||||
onBackClicked = internalOnBackPressed,
|
||||
navigateUp = navigateUp,
|
||||
onClickFilter = onFilterClicked,
|
||||
onClickShare = onShareClicked,
|
||||
onClickDownload = onDownloadActionClicked,
|
||||
onClickEditCategory = onEditCategoryClicked,
|
||||
onClickRefresh = onRefresh,
|
||||
onClickMigrate = onMigrateClicked,
|
||||
onClickEditNotes = onEditNotesClicked,
|
||||
actionModeCounter = selectedChapterCount,
|
||||
onCancelActionMode = { onAllChapterSelected(false) },
|
||||
onSelectAll = { onAllChapterSelected(true) },
|
||||
onInvertSelection = { onInvertSelection() },
|
||||
titleAlphaProvider = { titleAlpha },
|
||||
backgroundAlphaProvider = { backgroundAlpha },
|
||||
)
|
||||
},
|
||||
bottomBar = {
|
||||
@@ -414,8 +415,10 @@ private fun MangaScreenSmallImpl(
|
||||
defaultExpandState = state.isFromSource,
|
||||
description = state.manga.description,
|
||||
tagsProvider = { state.manga.genre },
|
||||
notes = state.manga.notes,
|
||||
onTagSearch = onTagSearch,
|
||||
onCopyTagToClipboard = onCopyTagToClipboard,
|
||||
onEditNotes = onEditNotesClicked,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -458,7 +461,7 @@ fun MangaScreenLargeImpl(
|
||||
nextUpdate: Instant?,
|
||||
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
||||
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
|
||||
onBackClicked: () -> Unit,
|
||||
navigateUp: () -> Unit,
|
||||
onChapterClicked: (Chapter) -> Unit,
|
||||
onDownloadChapter: ((List<ChapterList.Item>, ChapterDownloadAction) -> Unit)?,
|
||||
onAddToLibraryClicked: () -> Unit,
|
||||
@@ -484,6 +487,7 @@ fun MangaScreenLargeImpl(
|
||||
onEditCategoryClicked: (() -> Unit)?,
|
||||
onEditIntervalClicked: (() -> Unit)?,
|
||||
onMigrateClicked: (() -> Unit)?,
|
||||
onEditNotesClicked: () -> Unit,
|
||||
|
||||
// For bottom action menu
|
||||
onMultiBookmarkClicked: (List<Chapter>, bookmarked: Boolean) -> Unit,
|
||||
@@ -515,14 +519,9 @@ fun MangaScreenLargeImpl(
|
||||
|
||||
val chapterListState = rememberLazyListState()
|
||||
|
||||
val internalOnBackPressed = {
|
||||
if (isAnySelected) {
|
||||
onAllChapterSelected(false)
|
||||
} else {
|
||||
onBackClicked()
|
||||
}
|
||||
BackHandler(enabled = isAnySelected) {
|
||||
onAllChapterSelected(false)
|
||||
}
|
||||
BackHandler(onBack = internalOnBackPressed)
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
@@ -532,19 +531,21 @@ fun MangaScreenLargeImpl(
|
||||
MangaToolbar(
|
||||
modifier = Modifier.onSizeChanged { topBarHeight = it.height },
|
||||
title = state.manga.title,
|
||||
titleAlphaProvider = { if (isAnySelected) 1f else 0f },
|
||||
backgroundAlphaProvider = { 1f },
|
||||
hasFilters = state.filterActive,
|
||||
onBackClicked = internalOnBackPressed,
|
||||
navigateUp = navigateUp,
|
||||
onClickFilter = onFilterButtonClicked,
|
||||
onClickShare = onShareClicked,
|
||||
onClickDownload = onDownloadActionClicked,
|
||||
onClickEditCategory = onEditCategoryClicked,
|
||||
onClickRefresh = onRefresh,
|
||||
onClickMigrate = onMigrateClicked,
|
||||
onClickEditNotes = onEditNotesClicked,
|
||||
onCancelActionMode = { onAllChapterSelected(false) },
|
||||
actionModeCounter = selectedChapterCount,
|
||||
onSelectAll = { onAllChapterSelected(true) },
|
||||
onInvertSelection = { onInvertSelection() },
|
||||
titleAlphaProvider = { 1f },
|
||||
backgroundAlphaProvider = { 1f },
|
||||
)
|
||||
},
|
||||
bottomBar = {
|
||||
@@ -640,8 +641,10 @@ fun MangaScreenLargeImpl(
|
||||
defaultExpandState = true,
|
||||
description = state.manga.description,
|
||||
tagsProvider = { state.manga.genre },
|
||||
notes = state.manga.notes,
|
||||
onTagSearch = onTagSearch,
|
||||
onCopyTagToClipboard = onCopyTagToClipboard,
|
||||
onEditNotes = onEditNotesClicked,
|
||||
)
|
||||
}
|
||||
},
|
||||
|
@@ -9,6 +9,7 @@ import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.shrinkVertically
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
@@ -28,7 +29,10 @@ import androidx.compose.material.icons.outlined.BookmarkRemove
|
||||
import androidx.compose.material.icons.outlined.Delete
|
||||
import androidx.compose.material.icons.outlined.DoneAll
|
||||
import androidx.compose.material.icons.outlined.Download
|
||||
import androidx.compose.material.icons.outlined.MoreVert
|
||||
import androidx.compose.material.icons.outlined.RemoveDone
|
||||
import androidx.compose.material.icons.outlined.SwapCalls
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
@@ -48,8 +52,10 @@ import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.DpOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.presentation.components.DownloadDropdownMenu
|
||||
import eu.kanade.presentation.components.DropdownMenu
|
||||
import eu.kanade.presentation.manga.DownloadAction
|
||||
import eu.kanade.tachiyomi.R
|
||||
import kotlinx.coroutines.Job
|
||||
@@ -185,7 +191,7 @@ private fun RowScope.Button(
|
||||
targetValue = if (toConfirm) 2f else 1f,
|
||||
label = "weight",
|
||||
)
|
||||
Column(
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(48.dp)
|
||||
.weight(animatedWeight)
|
||||
@@ -195,24 +201,28 @@ private fun RowScope.Button(
|
||||
onLongClick = onLongClick,
|
||||
onClick = onClick,
|
||||
),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = title,
|
||||
)
|
||||
AnimatedVisibility(
|
||||
visible = toConfirm,
|
||||
enter = expandVertically(expandFrom = Alignment.Top) + fadeIn(),
|
||||
exit = shrinkVertically(shrinkTowards = Alignment.Top) + fadeOut(),
|
||||
Column(
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
overflow = TextOverflow.Visible,
|
||||
maxLines = 1,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = title,
|
||||
)
|
||||
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()
|
||||
}
|
||||
@@ -226,6 +236,7 @@ fun LibraryBottomActionMenu(
|
||||
onMarkAsUnreadClicked: () -> Unit,
|
||||
onDownloadClicked: ((DownloadAction) -> Unit)?,
|
||||
onDeleteClicked: () -> Unit,
|
||||
onMigrateClicked: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
@@ -240,17 +251,18 @@ fun LibraryBottomActionMenu(
|
||||
color = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||
) {
|
||||
val haptic = LocalHapticFeedback.current
|
||||
val confirm = remember { mutableStateListOf(false, false, false, false, false) }
|
||||
val confirm = remember { mutableStateListOf(false, false, false, false, false, false) }
|
||||
var resetJob: Job? = remember { null }
|
||||
val onLongClickItem: (Int) -> Unit = { toConfirmIndex ->
|
||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
(0..<5).forEach { i -> confirm[i] = i == toConfirmIndex }
|
||||
(0..5).forEach { i -> confirm[i] = i == toConfirmIndex }
|
||||
resetJob?.cancel()
|
||||
resetJob = scope.launch {
|
||||
delay(1.seconds)
|
||||
if (isActive) confirm[toConfirmIndex] = false
|
||||
}
|
||||
}
|
||||
val itemOverflow = onDownloadClicked != null
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.windowInsetsPadding(
|
||||
@@ -289,22 +301,57 @@ fun LibraryBottomActionMenu(
|
||||
onLongClick = { onLongClickItem(3) },
|
||||
onClick = { downloadExpanded = !downloadExpanded },
|
||||
) {
|
||||
val onDismissRequest = { downloadExpanded = false }
|
||||
DownloadDropdownMenu(
|
||||
expanded = downloadExpanded,
|
||||
onDismissRequest = onDismissRequest,
|
||||
onDismissRequest = { downloadExpanded = false },
|
||||
onDownloadClicked = onDownloadClicked,
|
||||
offset = BottomBarMenuDpOffset,
|
||||
)
|
||||
}
|
||||
}
|
||||
Button(
|
||||
title = stringResource(MR.strings.action_delete),
|
||||
icon = Icons.Outlined.Delete,
|
||||
toConfirm = confirm[4],
|
||||
onLongClick = { onLongClickItem(4) },
|
||||
onClick = onDeleteClicked,
|
||||
)
|
||||
if (!itemOverflow) {
|
||||
Button(
|
||||
title = stringResource(MR.strings.migrate),
|
||||
icon = Icons.Outlined.SwapCalls,
|
||||
toConfirm = confirm[4],
|
||||
onLongClick = { onLongClickItem(4) },
|
||||
onClick = onMigrateClicked,
|
||||
)
|
||||
Button(
|
||||
title = stringResource(MR.strings.action_delete),
|
||||
icon = Icons.Outlined.Delete,
|
||||
toConfirm = confirm[5],
|
||||
onLongClick = { onLongClickItem(5) },
|
||||
onClick = onDeleteClicked,
|
||||
)
|
||||
} else {
|
||||
var overflowMenuOpen by remember { mutableStateOf(false) }
|
||||
Button(
|
||||
title = stringResource(MR.strings.label_more),
|
||||
icon = Icons.Outlined.MoreVert,
|
||||
toConfirm = false,
|
||||
onLongClick = {},
|
||||
onClick = { overflowMenuOpen = true },
|
||||
) {
|
||||
DropdownMenu(
|
||||
expanded = overflowMenuOpen,
|
||||
onDismissRequest = { overflowMenuOpen = false },
|
||||
offset = BottomBarMenuDpOffset,
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(MR.strings.migrate)) },
|
||||
onClick = onMigrateClicked,
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(MR.strings.action_delete)) },
|
||||
onClick = onDeleteClicked,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val BottomBarMenuDpOffset = DpOffset(0.dp, 0.dp)
|
||||
|
@@ -2,6 +2,9 @@ package eu.kanade.presentation.manga.components
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import androidx.activity.compose.PredictiveBackHandler
|
||||
import androidx.compose.animation.core.animate
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
@@ -24,18 +27,22 @@ import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
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.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.DpOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.lerp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.core.graphics.drawable.toDrawable
|
||||
import androidx.core.view.updatePadding
|
||||
import coil3.asDrawable
|
||||
import coil3.imageLoader
|
||||
@@ -48,11 +55,14 @@ import eu.kanade.presentation.components.DropdownMenu
|
||||
import eu.kanade.presentation.manga.EditCoverAction
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageImageView
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import soup.compose.material.motion.MotionConstants
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.material.Scaffold
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import tachiyomi.presentation.core.util.PredictiveBack
|
||||
import tachiyomi.presentation.core.util.clickableNoIndication
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
|
||||
@Composable
|
||||
fun MangaCoverDialog(
|
||||
@@ -151,10 +161,32 @@ fun MangaCoverDialog(
|
||||
val statusBarPaddingPx = with(LocalDensity.current) { contentPadding.calculateTopPadding().roundToPx() }
|
||||
val bottomPaddingPx = with(LocalDensity.current) { contentPadding.calculateBottomPadding().roundToPx() }
|
||||
|
||||
var scale by remember { mutableFloatStateOf(1f) }
|
||||
PredictiveBackHandler { progress ->
|
||||
try {
|
||||
progress.collect { backEvent ->
|
||||
scale = lerp(1f, 0.8f, PredictiveBack.transform(backEvent.progress))
|
||||
}
|
||||
onDismissRequest()
|
||||
} catch (e: CancellationException) {
|
||||
animate(
|
||||
initialValue = scale,
|
||||
targetValue = 1f,
|
||||
animationSpec = tween(durationMillis = MotionConstants.DefaultMotionDuration),
|
||||
) { value, _ ->
|
||||
scale = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.clickableNoIndication(onClick = onDismissRequest),
|
||||
.clickableNoIndication(onClick = onDismissRequest)
|
||||
.graphicsLayer {
|
||||
scaleX = scale
|
||||
scaleY = scale
|
||||
},
|
||||
) {
|
||||
AndroidView(
|
||||
factory = {
|
||||
@@ -171,15 +203,13 @@ fun MangaCoverDialog(
|
||||
.memoryCachePolicy(CachePolicy.DISABLED)
|
||||
.target { image ->
|
||||
val drawable = image.asDrawable(view.context.resources)
|
||||
|
||||
// Copy bitmap in case it came from memory cache
|
||||
// Because SSIV needs to thoroughly read the image
|
||||
val copy = (drawable as? BitmapDrawable)?.let {
|
||||
BitmapDrawable(
|
||||
view.context.resources,
|
||||
it.bitmap.copy(Bitmap.Config.HARDWARE, false),
|
||||
)
|
||||
} ?: drawable
|
||||
val copy = (drawable as? BitmapDrawable)
|
||||
?.bitmap
|
||||
?.copy(Bitmap.Config.HARDWARE, false)
|
||||
?.toDrawable(view.context.resources)
|
||||
?: drawable
|
||||
view.setImage(copy, ReaderPageImageView.Config(zoomDuration = 500))
|
||||
}
|
||||
.build()
|
||||
|
@@ -19,8 +19,7 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.tachiyomi.util.system.isDevFlavor
|
||||
import eu.kanade.tachiyomi.util.system.isPreviewBuildType
|
||||
import eu.kanade.tachiyomi.util.system.isReleaseBuildType
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import tachiyomi.domain.manga.interactor.FetchInterval
|
||||
import tachiyomi.i18n.MR
|
||||
@@ -109,7 +108,7 @@ fun SetIntervalDialog(
|
||||
}
|
||||
Spacer(Modifier.height(MaterialTheme.padding.small))
|
||||
|
||||
if (onValueChanged != null && (isDevFlavor || isPreviewBuildType)) {
|
||||
if (onValueChanged != null && (!isReleaseBuildType)) {
|
||||
Text(stringResource(MR.strings.manga_interval_custom_amount))
|
||||
|
||||
BoxWithConstraints(
|
||||
|
@@ -23,6 +23,7 @@ import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.sizeIn
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.text.appendInlineContent
|
||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Brush
|
||||
@@ -68,8 +69,11 @@ import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.layout.Layout
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.LinkAnnotation
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.text.withLink
|
||||
import androidx.compose.ui.unit.Constraints
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -77,10 +81,17 @@ import androidx.compose.ui.unit.sp
|
||||
import coil3.compose.AsyncImage
|
||||
import coil3.request.ImageRequest
|
||||
import coil3.request.crossfade
|
||||
import com.mikepenz.markdown.model.markdownAnnotator
|
||||
import com.mikepenz.markdown.model.markdownAnnotatorConfig
|
||||
import com.mikepenz.markdown.utils.getUnescapedTextInNode
|
||||
import eu.kanade.domain.ui.UiPreferences
|
||||
import eu.kanade.presentation.components.DropdownMenu
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||
import org.intellij.markdown.MarkdownElementTypes
|
||||
import org.intellij.markdown.MarkdownTokenTypes
|
||||
import org.intellij.markdown.ast.findChildOfType
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.material.DISABLED_ALPHA
|
||||
@@ -90,12 +101,12 @@ import tachiyomi.presentation.core.i18n.pluralStringResource
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import tachiyomi.presentation.core.util.clickableNoIndication
|
||||
import tachiyomi.presentation.core.util.secondaryItemAlpha
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.time.Instant
|
||||
import java.time.temporal.ChronoUnit
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
private val whitespaceLineRegex = Regex("[\\r\\n]{2,}", setOf(RegexOption.MULTILINE))
|
||||
|
||||
@Composable
|
||||
fun MangaInfoBox(
|
||||
isTabletUi: Boolean,
|
||||
@@ -236,8 +247,10 @@ fun ExpandableMangaDescription(
|
||||
defaultExpandState: Boolean,
|
||||
description: String?,
|
||||
tagsProvider: () -> List<String>?,
|
||||
notes: String,
|
||||
onTagSearch: (String) -> Unit,
|
||||
onCopyTagToClipboard: (tag: String) -> Unit,
|
||||
onEditNotes: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(modifier = modifier) {
|
||||
@@ -246,15 +259,12 @@ fun ExpandableMangaDescription(
|
||||
}
|
||||
val desc =
|
||||
description.takeIf { !it.isNullOrBlank() } ?: stringResource(MR.strings.description_placeholder)
|
||||
val trimmedDescription = remember(desc) {
|
||||
desc
|
||||
.replace(whitespaceLineRegex, "\n")
|
||||
.trimEnd()
|
||||
}
|
||||
|
||||
MangaSummary(
|
||||
expandedDescription = desc,
|
||||
shrunkDescription = trimmedDescription,
|
||||
description = desc,
|
||||
expanded = expanded,
|
||||
notes = notes,
|
||||
onEditNotesClicked = onEditNotes,
|
||||
modifier = Modifier
|
||||
.padding(top = 8.dp)
|
||||
.padding(horizontal = 16.dp)
|
||||
@@ -555,13 +565,55 @@ private fun ColumnScope.MangaContentInfo(
|
||||
}
|
||||
}
|
||||
|
||||
private fun descriptionAnnotator(loadImages: Boolean, linkStyle: SpanStyle) = markdownAnnotator(
|
||||
annotate = { content, child ->
|
||||
if (!loadImages && child.type == MarkdownElementTypes.IMAGE) {
|
||||
val inlineLink = child.findChildOfType(MarkdownElementTypes.INLINE_LINK)
|
||||
|
||||
val url = inlineLink?.findChildOfType(MarkdownElementTypes.LINK_DESTINATION)
|
||||
?.getUnescapedTextInNode(content)
|
||||
?: inlineLink?.findChildOfType(MarkdownElementTypes.AUTOLINK)
|
||||
?.findChildOfType(MarkdownTokenTypes.AUTOLINK)
|
||||
?.getUnescapedTextInNode(content)
|
||||
?: return@markdownAnnotator false
|
||||
|
||||
val textNode = inlineLink?.findChildOfType(MarkdownElementTypes.LINK_TITLE)
|
||||
?: inlineLink?.findChildOfType(MarkdownElementTypes.LINK_TEXT)
|
||||
val altText = textNode?.findChildOfType(MarkdownTokenTypes.TEXT)
|
||||
?.getUnescapedTextInNode(content).orEmpty()
|
||||
|
||||
withLink(LinkAnnotation.Url(url = url)) {
|
||||
pushStyle(linkStyle)
|
||||
appendInlineContent(MARKDOWN_INLINE_IMAGE_TAG)
|
||||
append(altText)
|
||||
pop()
|
||||
}
|
||||
|
||||
return@markdownAnnotator true
|
||||
}
|
||||
|
||||
if (child.type in DISALLOWED_MARKDOWN_TYPES) {
|
||||
append(content.substring(child.startOffset, child.endOffset))
|
||||
return@markdownAnnotator true
|
||||
}
|
||||
|
||||
false
|
||||
},
|
||||
config = markdownAnnotatorConfig(
|
||||
eolAsNewLine = true,
|
||||
),
|
||||
)
|
||||
|
||||
@Composable
|
||||
private fun MangaSummary(
|
||||
expandedDescription: String,
|
||||
shrunkDescription: String,
|
||||
description: String,
|
||||
notes: String,
|
||||
expanded: Boolean,
|
||||
onEditNotesClicked: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val preferences = remember { Injekt.get<UiPreferences>() }
|
||||
val loadImages = remember { preferences.imagesInDescription().get() }
|
||||
val animProgress by animateFloatAsState(
|
||||
targetValue = if (expanded) 1f else 0f,
|
||||
label = "summary",
|
||||
@@ -571,25 +623,48 @@ private fun MangaSummary(
|
||||
contents = listOf(
|
||||
{
|
||||
Text(
|
||||
text = "\n\n", // Shows at least 3 lines
|
||||
// Shows at least 3 lines if no notes
|
||||
// when there are notes show 6
|
||||
text = if (notes.isBlank()) "\n\n" else "\n\n\n\n\n",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
},
|
||||
{
|
||||
Text(
|
||||
text = expandedDescription,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
},
|
||||
{
|
||||
SelectionContainer {
|
||||
Text(
|
||||
text = if (expanded) expandedDescription else shrunkDescription,
|
||||
maxLines = Int.MAX_VALUE,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
modifier = Modifier.secondaryItemAlpha(),
|
||||
Column {
|
||||
MangaNotesSection(
|
||||
content = notes,
|
||||
expanded = true,
|
||||
onEditNotes = onEditNotesClicked,
|
||||
)
|
||||
MarkdownRender(
|
||||
content = description,
|
||||
modifier = Modifier.secondaryItemAlpha(),
|
||||
annotator = descriptionAnnotator(
|
||||
loadImages = loadImages,
|
||||
linkStyle = getMarkdownLinkStyle().toSpanStyle(),
|
||||
),
|
||||
loadImages = loadImages,
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
Column {
|
||||
MangaNotesSection(
|
||||
content = notes,
|
||||
expanded = expanded,
|
||||
onEditNotes = onEditNotesClicked,
|
||||
)
|
||||
SelectionContainer {
|
||||
MarkdownRender(
|
||||
content = description,
|
||||
modifier = Modifier.secondaryItemAlpha(),
|
||||
annotator = descriptionAnnotator(
|
||||
loadImages = loadImages,
|
||||
linkStyle = getMarkdownLinkStyle().toSpanStyle(),
|
||||
),
|
||||
loadImages = loadImages,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@@ -0,0 +1,60 @@
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
@@ -0,0 +1,90 @@
|
||||
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!",
|
||||
)
|
||||
}
|
@@ -0,0 +1,224 @@
|
||||
package eu.kanade.presentation.manga.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.FormatListBulleted
|
||||
import androidx.compose.material.icons.outlined.FormatBold
|
||||
import androidx.compose.material.icons.outlined.FormatItalic
|
||||
import androidx.compose.material.icons.outlined.FormatListNumbered
|
||||
import androidx.compose.material.icons.outlined.FormatUnderlined
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.VerticalDivider
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.mohamedrejeb.richeditor.model.rememberRichTextState
|
||||
import com.mohamedrejeb.richeditor.ui.material3.RichTextEditor
|
||||
import com.mohamedrejeb.richeditor.ui.material3.RichTextEditorDefaults.richTextEditorColors
|
||||
import eu.kanade.tachiyomi.ui.manga.notes.MangaNotesScreen
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
private const val MAX_LENGTH = 250
|
||||
private const val MAX_LENGTH_WARN = MAX_LENGTH * 0.9
|
||||
|
||||
@Composable
|
||||
fun MangaNotesTextArea(
|
||||
state: MangaNotesScreen.State,
|
||||
onUpdate: (String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val richTextState = rememberRichTextState()
|
||||
val primaryColor = MaterialTheme.colorScheme.primary
|
||||
|
||||
DisposableEffect(scope, richTextState) {
|
||||
snapshotFlow { richTextState.annotatedString }
|
||||
.debounce(0.25.seconds)
|
||||
.distinctUntilChanged()
|
||||
.map { richTextState.toMarkdown() }
|
||||
.onEach { onUpdate(it) }
|
||||
.launchIn(scope)
|
||||
|
||||
onDispose {
|
||||
onUpdate(richTextState.toMarkdown())
|
||||
}
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
richTextState.setMarkdown(state.notes)
|
||||
richTextState.config.unorderedListIndent = 4
|
||||
richTextState.config.orderedListIndent = 20
|
||||
}
|
||||
LaunchedEffect(primaryColor) {
|
||||
richTextState.config.linkColor = primaryColor
|
||||
}
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
LaunchedEffect(focusRequester) {
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
val textLength = remember(richTextState.annotatedString) { richTextState.toText().length }
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.padding(horizontal = MaterialTheme.padding.small)
|
||||
.fillMaxSize(),
|
||||
) {
|
||||
RichTextEditor(
|
||||
state = richTextState,
|
||||
textStyle = MaterialTheme.typography.bodyLarge,
|
||||
maxLength = MAX_LENGTH,
|
||||
placeholder = {
|
||||
Text(text = stringResource(MR.strings.notes_placeholder))
|
||||
},
|
||||
colors = richTextEditorColors(
|
||||
containerColor = Color.Transparent,
|
||||
focusedIndicatorColor = Color.Transparent,
|
||||
unfocusedIndicatorColor = Color.Transparent,
|
||||
),
|
||||
contentPadding = PaddingValues(
|
||||
horizontal = MaterialTheme.padding.medium,
|
||||
),
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.fillMaxWidth()
|
||||
.focusRequester(focusRequester),
|
||||
)
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
modifier = Modifier
|
||||
.padding(vertical = MaterialTheme.padding.small)
|
||||
.fillMaxWidth(),
|
||||
) {
|
||||
LazyRow(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(2.dp),
|
||||
) {
|
||||
item {
|
||||
MangaNotesTextAreaButton(
|
||||
onClick = { richTextState.toggleSpanStyle(SpanStyle(fontWeight = FontWeight.Bold)) },
|
||||
isSelected = richTextState.currentSpanStyle.fontWeight == FontWeight.Bold,
|
||||
icon = Icons.Outlined.FormatBold,
|
||||
)
|
||||
}
|
||||
item {
|
||||
MangaNotesTextAreaButton(
|
||||
onClick = { richTextState.toggleSpanStyle(SpanStyle(fontStyle = FontStyle.Italic)) },
|
||||
isSelected = richTextState.currentSpanStyle.fontStyle == FontStyle.Italic,
|
||||
icon = Icons.Outlined.FormatItalic,
|
||||
)
|
||||
}
|
||||
item {
|
||||
MangaNotesTextAreaButton(
|
||||
onClick = {
|
||||
richTextState.toggleSpanStyle(SpanStyle(textDecoration = TextDecoration.Underline))
|
||||
},
|
||||
isSelected = richTextState.currentSpanStyle.textDecoration
|
||||
?.contains(TextDecoration.Underline)
|
||||
?: false,
|
||||
icon = Icons.Outlined.FormatUnderlined,
|
||||
)
|
||||
}
|
||||
item {
|
||||
VerticalDivider(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = MaterialTheme.padding.extraSmall)
|
||||
.height(MaterialTheme.padding.large),
|
||||
)
|
||||
}
|
||||
item {
|
||||
MangaNotesTextAreaButton(
|
||||
onClick = { richTextState.toggleUnorderedList() },
|
||||
isSelected = richTextState.isUnorderedList,
|
||||
icon = Icons.AutoMirrored.Outlined.FormatListBulleted,
|
||||
)
|
||||
}
|
||||
item {
|
||||
MangaNotesTextAreaButton(
|
||||
onClick = { richTextState.toggleOrderedList() },
|
||||
isSelected = richTextState.isOrderedList,
|
||||
icon = Icons.Outlined.FormatListNumbered,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(
|
||||
text = (MAX_LENGTH - textLength).toString(),
|
||||
color = if (textLength > MAX_LENGTH_WARN) {
|
||||
MaterialTheme.colorScheme.error
|
||||
} else {
|
||||
Color.Unspecified
|
||||
},
|
||||
modifier = Modifier.padding(MaterialTheme.padding.extraSmall),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MangaNotesTextAreaButton(
|
||||
onClick: () -> Unit,
|
||||
icon: ImageVector,
|
||||
isSelected: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.clip(MaterialTheme.shapes.small)
|
||||
.clickable(
|
||||
onClick = onClick,
|
||||
enabled = true,
|
||||
role = Role.Button,
|
||||
),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = icon.name,
|
||||
tint = if (isSelected) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier
|
||||
.background(color = if (isSelected) MaterialTheme.colorScheme.onBackground else Color.Transparent)
|
||||
.padding(MaterialTheme.padding.extraSmall),
|
||||
)
|
||||
}
|
||||
}
|
@@ -1,18 +1,12 @@
|
||||
package eu.kanade.presentation.manga.components
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Close
|
||||
import androidx.compose.material.icons.outlined.Download
|
||||
import androidx.compose.material.icons.outlined.FilterList
|
||||
import androidx.compose.material.icons.outlined.FlipToBack
|
||||
import androidx.compose.material.icons.outlined.SelectAll
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.surfaceColorAtElevation
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -20,12 +14,12 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.presentation.components.AppBar
|
||||
import eu.kanade.presentation.components.AppBarActions
|
||||
import eu.kanade.presentation.components.AppBarTitle
|
||||
import eu.kanade.presentation.components.DownloadDropdownMenu
|
||||
import eu.kanade.presentation.components.UpIcon
|
||||
import eu.kanade.presentation.manga.DownloadAction
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import tachiyomi.i18n.MR
|
||||
@@ -35,130 +29,129 @@ import tachiyomi.presentation.core.theme.active
|
||||
@Composable
|
||||
fun MangaToolbar(
|
||||
title: String,
|
||||
titleAlphaProvider: () -> Float,
|
||||
hasFilters: Boolean,
|
||||
onBackClicked: () -> Unit,
|
||||
navigateUp: () -> Unit,
|
||||
onClickFilter: () -> Unit,
|
||||
onClickShare: (() -> Unit)?,
|
||||
onClickDownload: ((DownloadAction) -> Unit)?,
|
||||
onClickEditCategory: (() -> Unit)?,
|
||||
onClickRefresh: () -> Unit,
|
||||
onClickMigrate: (() -> Unit)?,
|
||||
onClickEditNotes: () -> Unit,
|
||||
|
||||
// For action mode
|
||||
actionModeCounter: Int,
|
||||
onCancelActionMode: () -> Unit,
|
||||
onSelectAll: () -> Unit,
|
||||
onInvertSelection: () -> Unit,
|
||||
|
||||
titleAlphaProvider: () -> Float,
|
||||
backgroundAlphaProvider: () -> Float,
|
||||
modifier: Modifier = Modifier,
|
||||
backgroundAlphaProvider: () -> Float = titleAlphaProvider,
|
||||
) {
|
||||
Column(
|
||||
val isActionMode = actionModeCounter > 0
|
||||
AppBar(
|
||||
titleContent = {
|
||||
if (isActionMode) {
|
||||
AppBarTitle(actionModeCounter.toString())
|
||||
} else {
|
||||
AppBarTitle(title, modifier = Modifier.alpha(titleAlphaProvider()))
|
||||
}
|
||||
},
|
||||
modifier = modifier,
|
||||
) {
|
||||
val isActionMode = actionModeCounter > 0
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = if (isActionMode) actionModeCounter.toString() else title,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = LocalContentColor.current.copy(alpha = if (isActionMode) 1f else titleAlphaProvider()),
|
||||
backgroundColor = MaterialTheme.colorScheme
|
||||
.surfaceColorAtElevation(3.dp)
|
||||
.copy(alpha = if (isActionMode) 1f else backgroundAlphaProvider()),
|
||||
navigateUp = navigateUp,
|
||||
actions = {
|
||||
var downloadExpanded by remember { mutableStateOf(false) }
|
||||
if (onClickDownload != null) {
|
||||
val onDismissRequest = { downloadExpanded = false }
|
||||
DownloadDropdownMenu(
|
||||
expanded = downloadExpanded,
|
||||
onDismissRequest = onDismissRequest,
|
||||
onDownloadClicked = onClickDownload,
|
||||
)
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBackClicked) {
|
||||
UpIcon(navigationIcon = Icons.Outlined.Close.takeIf { isActionMode })
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
if (isActionMode) {
|
||||
AppBarActions(
|
||||
persistentListOf(
|
||||
}
|
||||
|
||||
val filterTint = if (hasFilters) MaterialTheme.colorScheme.active else LocalContentColor.current
|
||||
AppBarActions(
|
||||
actions = persistentListOf<AppBar.AppBarAction>().builder().apply {
|
||||
if (isActionMode) {
|
||||
add(
|
||||
AppBar.Action(
|
||||
title = stringResource(MR.strings.action_select_all),
|
||||
icon = Icons.Outlined.SelectAll,
|
||||
onClick = onSelectAll,
|
||||
),
|
||||
)
|
||||
add(
|
||||
AppBar.Action(
|
||||
title = stringResource(MR.strings.action_select_inverse),
|
||||
icon = Icons.Outlined.FlipToBack,
|
||||
onClick = onInvertSelection,
|
||||
),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
var downloadExpanded by remember { mutableStateOf(false) }
|
||||
)
|
||||
return@apply
|
||||
}
|
||||
if (onClickDownload != null) {
|
||||
val onDismissRequest = { downloadExpanded = false }
|
||||
DownloadDropdownMenu(
|
||||
expanded = downloadExpanded,
|
||||
onDismissRequest = onDismissRequest,
|
||||
onDownloadClicked = onClickDownload,
|
||||
add(
|
||||
AppBar.Action(
|
||||
title = stringResource(MR.strings.manga_download),
|
||||
icon = Icons.Outlined.Download,
|
||||
onClick = { downloadExpanded = !downloadExpanded },
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
val filterTint = if (hasFilters) MaterialTheme.colorScheme.active else LocalContentColor.current
|
||||
AppBarActions(
|
||||
actions = persistentListOf<AppBar.AppBarAction>().builder()
|
||||
.apply {
|
||||
if (onClickDownload != null) {
|
||||
add(
|
||||
AppBar.Action(
|
||||
title = stringResource(MR.strings.manga_download),
|
||||
icon = Icons.Outlined.Download,
|
||||
onClick = { downloadExpanded = !downloadExpanded },
|
||||
),
|
||||
)
|
||||
}
|
||||
add(
|
||||
AppBar.Action(
|
||||
title = stringResource(MR.strings.action_filter),
|
||||
icon = Icons.Outlined.FilterList,
|
||||
iconTint = filterTint,
|
||||
onClick = onClickFilter,
|
||||
),
|
||||
)
|
||||
add(
|
||||
AppBar.OverflowAction(
|
||||
title = stringResource(MR.strings.action_webview_refresh),
|
||||
onClick = onClickRefresh,
|
||||
),
|
||||
)
|
||||
if (onClickEditCategory != null) {
|
||||
add(
|
||||
AppBar.OverflowAction(
|
||||
title = stringResource(MR.strings.action_edit_categories),
|
||||
onClick = onClickEditCategory,
|
||||
),
|
||||
)
|
||||
}
|
||||
if (onClickMigrate != null) {
|
||||
add(
|
||||
AppBar.OverflowAction(
|
||||
title = stringResource(MR.strings.action_migrate),
|
||||
onClick = onClickMigrate,
|
||||
),
|
||||
)
|
||||
}
|
||||
if (onClickShare != null) {
|
||||
add(
|
||||
AppBar.OverflowAction(
|
||||
title = stringResource(MR.strings.action_share),
|
||||
onClick = onClickShare,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
.build(),
|
||||
add(
|
||||
AppBar.Action(
|
||||
title = stringResource(MR.strings.action_filter),
|
||||
icon = Icons.Outlined.FilterList,
|
||||
iconTint = filterTint,
|
||||
onClick = onClickFilter,
|
||||
),
|
||||
)
|
||||
add(
|
||||
AppBar.OverflowAction(
|
||||
title = stringResource(MR.strings.action_webview_refresh),
|
||||
onClick = onClickRefresh,
|
||||
),
|
||||
)
|
||||
if (onClickEditCategory != null) {
|
||||
add(
|
||||
AppBar.OverflowAction(
|
||||
title = stringResource(MR.strings.action_edit_categories),
|
||||
onClick = onClickEditCategory,
|
||||
),
|
||||
)
|
||||
}
|
||||
if (onClickMigrate != null) {
|
||||
add(
|
||||
AppBar.OverflowAction(
|
||||
title = stringResource(MR.strings.action_migrate),
|
||||
onClick = onClickMigrate,
|
||||
),
|
||||
)
|
||||
}
|
||||
if (onClickShare != null) {
|
||||
add(
|
||||
AppBar.OverflowAction(
|
||||
title = stringResource(MR.strings.action_share),
|
||||
onClick = onClickShare,
|
||||
),
|
||||
)
|
||||
}
|
||||
add(
|
||||
AppBar.OverflowAction(
|
||||
title = stringResource(MR.strings.action_notes),
|
||||
onClick = onClickEditNotes,
|
||||
),
|
||||
)
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme
|
||||
.surfaceColorAtElevation(3.dp)
|
||||
.copy(alpha = if (isActionMode) 1f else backgroundAlphaProvider()),
|
||||
),
|
||||
)
|
||||
}
|
||||
.build(),
|
||||
)
|
||||
},
|
||||
isActionMode = isActionMode,
|
||||
onCancelActionMode = onCancelActionMode,
|
||||
)
|
||||
}
|
||||
|
@@ -0,0 +1,292 @@
|
||||
package eu.kanade.presentation.manga.components
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.text.InlineTextContent
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Image
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.ReadOnlyComposable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.FirstBaseline
|
||||
import androidx.compose.ui.text.Placeholder
|
||||
import androidx.compose.ui.text.PlaceholderVerticalAlign
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.TextLinkStyles
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.mikepenz.markdown.coil3.Coil3ImageTransformerImpl
|
||||
import com.mikepenz.markdown.compose.LocalBulletListHandler
|
||||
import com.mikepenz.markdown.compose.Markdown
|
||||
import com.mikepenz.markdown.compose.components.markdownComponents
|
||||
import com.mikepenz.markdown.compose.elements.MarkdownBulletList
|
||||
import com.mikepenz.markdown.compose.elements.MarkdownDivider
|
||||
import com.mikepenz.markdown.compose.elements.MarkdownOrderedList
|
||||
import com.mikepenz.markdown.compose.elements.MarkdownTable
|
||||
import com.mikepenz.markdown.compose.elements.MarkdownTableHeader
|
||||
import com.mikepenz.markdown.compose.elements.MarkdownTableRow
|
||||
import com.mikepenz.markdown.compose.elements.MarkdownText
|
||||
import com.mikepenz.markdown.compose.elements.listDepth
|
||||
import com.mikepenz.markdown.model.DefaultMarkdownColors
|
||||
import com.mikepenz.markdown.model.DefaultMarkdownInlineContent
|
||||
import com.mikepenz.markdown.model.DefaultMarkdownTypography
|
||||
import com.mikepenz.markdown.model.MarkdownAnnotator
|
||||
import com.mikepenz.markdown.model.MarkdownColors
|
||||
import com.mikepenz.markdown.model.MarkdownPadding
|
||||
import com.mikepenz.markdown.model.MarkdownTypography
|
||||
import com.mikepenz.markdown.model.NoOpImageTransformerImpl
|
||||
import com.mikepenz.markdown.model.markdownAnnotator
|
||||
import com.mikepenz.markdown.model.rememberMarkdownState
|
||||
import org.intellij.markdown.MarkdownTokenTypes.Companion.HTML_TAG
|
||||
import org.intellij.markdown.flavours.MarkdownFlavourDescriptor
|
||||
import org.intellij.markdown.flavours.commonmark.CommonMarkFlavourDescriptor
|
||||
import org.intellij.markdown.flavours.commonmark.CommonMarkMarkerProcessor
|
||||
import org.intellij.markdown.flavours.gfm.table.GitHubTableMarkerProvider
|
||||
import org.intellij.markdown.parser.MarkerProcessor
|
||||
import org.intellij.markdown.parser.MarkerProcessorFactory
|
||||
import org.intellij.markdown.parser.ProductionHolder
|
||||
import org.intellij.markdown.parser.constraints.CommonMarkdownConstraints
|
||||
import org.intellij.markdown.parser.constraints.MarkdownConstraints
|
||||
import org.intellij.markdown.parser.markerblocks.MarkerBlockProvider
|
||||
import org.intellij.markdown.parser.markerblocks.providers.AtxHeaderProvider
|
||||
import org.intellij.markdown.parser.markerblocks.providers.BlockQuoteProvider
|
||||
import org.intellij.markdown.parser.markerblocks.providers.CodeBlockProvider
|
||||
import org.intellij.markdown.parser.markerblocks.providers.CodeFenceProvider
|
||||
import org.intellij.markdown.parser.markerblocks.providers.HorizontalRuleProvider
|
||||
import org.intellij.markdown.parser.markerblocks.providers.ListMarkerProvider
|
||||
import org.intellij.markdown.parser.markerblocks.providers.SetextHeaderProvider
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
|
||||
const val MARKDOWN_INLINE_IMAGE_TAG = "MARKDOWN_INLINE_IMAGE"
|
||||
|
||||
@Composable
|
||||
fun MarkdownRender(
|
||||
content: String,
|
||||
modifier: Modifier = Modifier,
|
||||
flavour: MarkdownFlavourDescriptor = SimpleMarkdownFlavourDescriptor,
|
||||
annotator: MarkdownAnnotator = remember { markdownAnnotator() },
|
||||
loadImages: Boolean = true,
|
||||
) {
|
||||
Markdown(
|
||||
markdownState = rememberMarkdownState(
|
||||
content = content,
|
||||
flavour = flavour,
|
||||
immediate = true,
|
||||
),
|
||||
annotator = annotator,
|
||||
colors = getMarkdownColors(),
|
||||
typography = getMarkdownTypography(),
|
||||
padding = markdownPadding,
|
||||
components = markdownComponents,
|
||||
imageTransformer = remember(loadImages) {
|
||||
if (loadImages) Coil3ImageTransformerImpl else NoOpImageTransformerImpl()
|
||||
},
|
||||
inlineContent = getMarkdownInlineContent(),
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@ReadOnlyComposable
|
||||
private fun getMarkdownColors(): MarkdownColors {
|
||||
val codeBackground = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f)
|
||||
return DefaultMarkdownColors(
|
||||
text = MaterialTheme.colorScheme.onSurface,
|
||||
codeText = Color.Unspecified,
|
||||
inlineCodeText = Color.Unspecified,
|
||||
linkText = Color.Unspecified,
|
||||
codeBackground = codeBackground,
|
||||
inlineCodeBackground = codeBackground,
|
||||
dividerColor = MaterialTheme.colorScheme.outlineVariant,
|
||||
tableText = Color.Unspecified,
|
||||
tableBackground = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.05f),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@ReadOnlyComposable
|
||||
fun getMarkdownLinkStyle() = MaterialTheme.typography.bodyMedium.copy(
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
fontWeight = FontWeight.Bold,
|
||||
)
|
||||
|
||||
@Composable
|
||||
@ReadOnlyComposable
|
||||
private fun getMarkdownTypography(): MarkdownTypography {
|
||||
val link = getMarkdownLinkStyle()
|
||||
return DefaultMarkdownTypography(
|
||||
h1 = MaterialTheme.typography.headlineMedium,
|
||||
h2 = MaterialTheme.typography.headlineSmall,
|
||||
h3 = MaterialTheme.typography.titleLarge,
|
||||
h4 = MaterialTheme.typography.titleMedium,
|
||||
h5 = MaterialTheme.typography.titleSmall,
|
||||
h6 = MaterialTheme.typography.bodyLarge,
|
||||
text = MaterialTheme.typography.bodyMedium,
|
||||
code = MaterialTheme.typography.bodyMedium.copy(fontFamily = FontFamily.Monospace),
|
||||
inlineCode = MaterialTheme.typography.bodyMedium.copy(fontFamily = FontFamily.Monospace),
|
||||
quote = MaterialTheme.typography.bodyMedium.plus(SpanStyle(fontStyle = FontStyle.Italic)),
|
||||
paragraph = MaterialTheme.typography.bodyMedium,
|
||||
ordered = MaterialTheme.typography.bodyMedium,
|
||||
bullet = MaterialTheme.typography.bodyMedium,
|
||||
list = MaterialTheme.typography.bodyMedium,
|
||||
link = link,
|
||||
textLink = TextLinkStyles(style = link.toSpanStyle()),
|
||||
table = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
}
|
||||
|
||||
private val markdownPadding = object : MarkdownPadding {
|
||||
override val block: Dp = 2.dp
|
||||
override val blockQuote: PaddingValues = PaddingValues(horizontal = 16.dp, vertical = 0.dp)
|
||||
override val blockQuoteBar: PaddingValues.Absolute = PaddingValues.Absolute(
|
||||
left = 4.dp,
|
||||
top = 2.dp,
|
||||
right = 4.dp,
|
||||
bottom = 2.dp,
|
||||
)
|
||||
override val blockQuoteText: PaddingValues = PaddingValues(vertical = 4.dp)
|
||||
override val codeBlock: PaddingValues = PaddingValues(8.dp)
|
||||
override val list: Dp = 0.dp
|
||||
override val listIndent: Dp = 8.dp
|
||||
override val listItemBottom: Dp = 0.dp
|
||||
override val listItemTop: Dp = 0.dp
|
||||
}
|
||||
|
||||
private val markdownComponents = markdownComponents(
|
||||
horizontalRule = {
|
||||
MarkdownDivider(
|
||||
modifier = Modifier
|
||||
.padding(vertical = MaterialTheme.padding.extraSmall)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
},
|
||||
orderedList = { ol ->
|
||||
Column(modifier = Modifier.padding(start = MaterialTheme.padding.small)) {
|
||||
MarkdownOrderedList(
|
||||
content = ol.content,
|
||||
node = ol.node,
|
||||
style = ol.typography.ordered,
|
||||
depth = ol.listDepth,
|
||||
markerModifier = { Modifier.alignBy(FirstBaseline) },
|
||||
listModifier = { Modifier.alignBy(FirstBaseline) },
|
||||
)
|
||||
}
|
||||
},
|
||||
unorderedList = { ul ->
|
||||
val markers = listOf("•", "◦", "▸", "▹")
|
||||
|
||||
CompositionLocalProvider(
|
||||
LocalBulletListHandler provides { _, _, _, _, _ -> "${markers[ul.listDepth % markers.size]} " },
|
||||
) {
|
||||
Column(modifier = Modifier.padding(start = MaterialTheme.padding.small)) {
|
||||
MarkdownBulletList(
|
||||
content = ul.content,
|
||||
node = ul.node,
|
||||
style = ul.typography.bullet,
|
||||
markerModifier = { Modifier.alignBy(FirstBaseline) },
|
||||
listModifier = { Modifier.alignBy(FirstBaseline) },
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
table = { t ->
|
||||
MarkdownTable(
|
||||
content = t.content,
|
||||
node = t.node,
|
||||
style = t.typography.text,
|
||||
headerBlock = { content, header, tableWidth, style ->
|
||||
MarkdownTableHeader(
|
||||
content = content,
|
||||
header = header,
|
||||
tableWidth = tableWidth,
|
||||
style = style,
|
||||
maxLines = Int.MAX_VALUE,
|
||||
)
|
||||
},
|
||||
rowBlock = { content, header, tableWidth, style ->
|
||||
MarkdownTableRow(
|
||||
content = content,
|
||||
header = header,
|
||||
tableWidth = tableWidth,
|
||||
style = style,
|
||||
maxLines = Int.MAX_VALUE,
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
custom = { type, model ->
|
||||
if (type in DISALLOWED_MARKDOWN_TYPES) {
|
||||
MarkdownText(
|
||||
content = model.content.substring(model.node.startOffset, model.node.endOffset),
|
||||
style = model.typography.text,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
@Composable
|
||||
@ReadOnlyComposable
|
||||
private fun getMarkdownInlineContent() = DefaultMarkdownInlineContent(
|
||||
inlineContent = mapOf(
|
||||
MARKDOWN_INLINE_IMAGE_TAG to InlineTextContent(
|
||||
placeholder = Placeholder(
|
||||
width = MaterialTheme.typography.bodyMedium.fontSize * 1.25,
|
||||
height = MaterialTheme.typography.bodyMedium.fontSize * 1.25,
|
||||
placeholderVerticalAlign = PlaceholderVerticalAlign.TextCenter,
|
||||
),
|
||||
children = {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Image,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
private object SimpleMarkdownFlavourDescriptor : CommonMarkFlavourDescriptor() {
|
||||
override val markerProcessorFactory: MarkerProcessorFactory = SimpleMarkdownProcessFactory
|
||||
}
|
||||
|
||||
private object SimpleMarkdownProcessFactory : MarkerProcessorFactory {
|
||||
override fun createMarkerProcessor(productionHolder: ProductionHolder): MarkerProcessor<*> {
|
||||
return SimpleMarkdownMarkerProcessor(productionHolder, CommonMarkdownConstraints.BASE)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Like `CommonMarkFlavour`, but with html blocks and reference links removed and
|
||||
* table support added
|
||||
*/
|
||||
private class SimpleMarkdownMarkerProcessor(
|
||||
productionHolder: ProductionHolder,
|
||||
constraints: MarkdownConstraints,
|
||||
) : CommonMarkMarkerProcessor(productionHolder, constraints) {
|
||||
private val markerBlockProviders = listOf(
|
||||
CodeBlockProvider(),
|
||||
HorizontalRuleProvider(),
|
||||
CodeFenceProvider(),
|
||||
SetextHeaderProvider(),
|
||||
BlockQuoteProvider(),
|
||||
ListMarkerProvider(),
|
||||
AtxHeaderProvider(),
|
||||
GitHubTableMarkerProvider(),
|
||||
)
|
||||
|
||||
override fun getMarkerBlockProviders(): List<MarkerBlockProvider<StateInfo>> {
|
||||
return markerBlockProviders
|
||||
}
|
||||
}
|
||||
|
||||
val DISALLOWED_MARKDOWN_TYPES = arrayOf(HTML_TAG)
|
@@ -1,15 +1,10 @@
|
||||
package eu.kanade.presentation.more
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||
import androidx.compose.foundation.layout.only
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.systemBars
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.HelpOutline
|
||||
import androidx.compose.material.icons.automirrored.outlined.Label
|
||||
import androidx.compose.material.icons.outlined.AttachMoney
|
||||
import androidx.compose.material.icons.outlined.CloudOff
|
||||
import androidx.compose.material.icons.outlined.GetApp
|
||||
import androidx.compose.material.icons.outlined.Info
|
||||
@@ -40,7 +35,6 @@ fun MoreScreen(
|
||||
onDownloadedOnlyChange: (Boolean) -> Unit,
|
||||
incognitoMode: Boolean,
|
||||
onIncognitoModeChange: (Boolean) -> Unit,
|
||||
isFDroid: Boolean,
|
||||
onClickDownloadQueue: () -> Unit,
|
||||
onClickCategories: () -> Unit,
|
||||
onClickStats: () -> Unit,
|
||||
@@ -50,19 +44,7 @@ fun MoreScreen(
|
||||
) {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
Column(
|
||||
modifier = Modifier.windowInsetsPadding(
|
||||
WindowInsets.systemBars.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
|
||||
),
|
||||
) {
|
||||
if (isFDroid) {
|
||||
// Don't really care about slow updaters now
|
||||
}
|
||||
}
|
||||
},
|
||||
) { contentPadding ->
|
||||
Scaffold { contentPadding ->
|
||||
ScrollbarLazyColumn(
|
||||
modifier = Modifier.padding(contentPadding),
|
||||
) {
|
||||
@@ -164,6 +146,13 @@ fun MoreScreen(
|
||||
onPreferenceClick = { uriHandler.openUri(Constants.URL_HELP) },
|
||||
)
|
||||
}
|
||||
item {
|
||||
TextPreferenceWidget(
|
||||
title = stringResource(MR.strings.label_donate),
|
||||
icon = Icons.Outlined.AttachMoney,
|
||||
onPreferenceClick = { uriHandler.openUri(Constants.URL_DONATE) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,5 +1,6 @@
|
||||
package eu.kanade.presentation.more
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
@@ -13,13 +14,10 @@ import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||
import 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.manga.components.MarkdownRender
|
||||
import eu.kanade.presentation.theme.TachiyomiPreviewTheme
|
||||
import org.intellij.markdown.flavours.gfm.GFMFlavourDescriptor
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
@@ -42,17 +40,15 @@ fun NewUpdateScreen(
|
||||
rejectText = stringResource(MR.strings.action_not_now),
|
||||
onRejectClick = onRejectUpdate,
|
||||
) {
|
||||
RichText(
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = MaterialTheme.padding.large),
|
||||
style = RichTextStyle(
|
||||
stringStyle = RichTextStringStyle(
|
||||
linkStyle = SpanStyle(color = MaterialTheme.colorScheme.primary),
|
||||
),
|
||||
),
|
||||
) {
|
||||
Markdown(content = changelogInfo)
|
||||
MarkdownRender(
|
||||
content = changelogInfo,
|
||||
flavour = GFMFlavourDescriptor(),
|
||||
)
|
||||
|
||||
TextButton(
|
||||
onClick = onOpenInBrowser,
|
||||
|
@@ -42,7 +42,9 @@ fun OnboardingScreen(
|
||||
}
|
||||
val isLastStep = currentStep == steps.lastIndex
|
||||
|
||||
BackHandler(enabled = currentStep != 0, onBack = { currentStep-- })
|
||||
BackHandler(enabled = currentStep != 0) {
|
||||
currentStep--
|
||||
}
|
||||
|
||||
InfoScreen(
|
||||
icon = Icons.Outlined.RocketLaunch,
|
||||
|
@@ -4,7 +4,6 @@ import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.PowerManager
|
||||
import android.provider.Settings
|
||||
@@ -32,12 +31,14 @@ import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.core.net.toUri
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||
import eu.kanade.presentation.util.rememberRequestPackageInstallsPermissionState
|
||||
import eu.kanade.tachiyomi.core.security.PrivacyPreferences
|
||||
import eu.kanade.tachiyomi.util.system.launchRequestPackageInstallsPermission
|
||||
import eu.kanade.tachiyomi.util.system.telemetryIncluded
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import tachiyomi.presentation.core.util.collectAsState
|
||||
@@ -111,12 +112,14 @@ internal class PermissionStep : OnboardingStep {
|
||||
onButtonClick = {
|
||||
@SuppressLint("BatteryLife")
|
||||
val intent = Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply {
|
||||
data = Uri.parse("package:${context.packageName}")
|
||||
data = "package:${context.packageName}".toUri()
|
||||
}
|
||||
context.startActivity(intent)
|
||||
},
|
||||
)
|
||||
|
||||
if (!telemetryIncluded) return@Column
|
||||
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(vertical = 8.dp, horizontal = 16.dp),
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||
|
@@ -1,5 +1,6 @@
|
||||
package eu.kanade.presentation.more.settings
|
||||
|
||||
import androidx.annotation.IntRange
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
@@ -17,7 +18,7 @@ sealed class Preference {
|
||||
sealed class PreferenceItem<T> : Preference() {
|
||||
abstract val subtitle: String?
|
||||
abstract val icon: ImageVector?
|
||||
abstract val onValueChanged: suspend (newValue: T) -> Boolean
|
||||
abstract val onValueChanged: suspend (value: T) -> Boolean
|
||||
|
||||
/**
|
||||
* A basic [PreferenceItem] that only displays texts.
|
||||
@@ -25,57 +26,58 @@ sealed class Preference {
|
||||
data class TextPreference(
|
||||
override val title: String,
|
||||
override val subtitle: String? = null,
|
||||
override val icon: ImageVector? = null,
|
||||
override val enabled: Boolean = true,
|
||||
override val onValueChanged: suspend (newValue: String) -> Boolean = { true },
|
||||
|
||||
val onClick: (() -> Unit)? = null,
|
||||
) : PreferenceItem<String>()
|
||||
) : PreferenceItem<String>() {
|
||||
override val icon: ImageVector? = null
|
||||
override val onValueChanged: suspend (value: String) -> Boolean = { true }
|
||||
}
|
||||
|
||||
/**
|
||||
* A [PreferenceItem] that provides a two-state toggleable option.
|
||||
*/
|
||||
data class SwitchPreference(
|
||||
val pref: PreferenceData<Boolean>,
|
||||
val preference: PreferenceData<Boolean>,
|
||||
override val title: String,
|
||||
override val subtitle: String? = null,
|
||||
override val icon: ImageVector? = null,
|
||||
override val enabled: Boolean = true,
|
||||
override val onValueChanged: suspend (newValue: Boolean) -> Boolean = { true },
|
||||
) : PreferenceItem<Boolean>()
|
||||
override val onValueChanged: suspend (value: Boolean) -> Boolean = { true },
|
||||
) : PreferenceItem<Boolean>() {
|
||||
override val icon: ImageVector? = null
|
||||
}
|
||||
|
||||
/**
|
||||
* A [PreferenceItem] that provides a slider to select an integer number.
|
||||
*/
|
||||
data class SliderPreference(
|
||||
val value: Int,
|
||||
val min: Int = 0,
|
||||
val max: Int,
|
||||
override val title: String = "",
|
||||
override val title: String,
|
||||
val valueRange: IntProgression = 0..1,
|
||||
@IntRange(from = 0) val steps: Int = with(valueRange) { (last - first) - 1 },
|
||||
override val subtitle: String? = null,
|
||||
override val icon: ImageVector? = null,
|
||||
override val enabled: Boolean = true,
|
||||
override val onValueChanged: suspend (newValue: Int) -> Boolean = { true },
|
||||
) : PreferenceItem<Int>()
|
||||
override val onValueChanged: suspend (value: Int) -> Boolean = { true },
|
||||
) : PreferenceItem<Int>() {
|
||||
override val icon: ImageVector? = null
|
||||
}
|
||||
|
||||
/**
|
||||
* A [PreferenceItem] that displays a list of entries as a dialog.
|
||||
*/
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
data class ListPreference<T>(
|
||||
val pref: PreferenceData<T>,
|
||||
val preference: PreferenceData<T>,
|
||||
val entries: ImmutableMap<T, String>,
|
||||
override val title: String,
|
||||
override val subtitle: String? = "%s",
|
||||
val subtitleProvider: @Composable (value: T, entries: ImmutableMap<T, String>) -> String? =
|
||||
{ v, e -> subtitle?.format(e[v]) },
|
||||
override val icon: ImageVector? = null,
|
||||
override val enabled: Boolean = true,
|
||||
override val onValueChanged: suspend (newValue: T) -> Boolean = { true },
|
||||
|
||||
val entries: ImmutableMap<T, String>,
|
||||
override val onValueChanged: suspend (value: T) -> Boolean = { true },
|
||||
) : PreferenceItem<T>() {
|
||||
internal fun internalSet(newValue: Any) = pref.set(newValue as T)
|
||||
internal suspend fun internalOnValueChanged(newValue: Any) = onValueChanged(newValue as T)
|
||||
internal fun internalSet(value: Any) = preference.set(value as T)
|
||||
internal suspend fun internalOnValueChanged(value: Any) = onValueChanged(value as T)
|
||||
|
||||
@Composable
|
||||
internal fun internalSubtitleProvider(value: Any?, entries: ImmutableMap<out Any?, String>) =
|
||||
@@ -87,15 +89,14 @@ sealed class Preference {
|
||||
*/
|
||||
data class BasicListPreference(
|
||||
val value: String,
|
||||
val entries: ImmutableMap<String, String>,
|
||||
override val title: String,
|
||||
override val subtitle: String? = "%s",
|
||||
val subtitleProvider: @Composable (value: String, entries: ImmutableMap<String, String>) -> String? =
|
||||
{ v, e -> subtitle?.format(e[v]) },
|
||||
override val icon: ImageVector? = null,
|
||||
override val enabled: Boolean = true,
|
||||
override val onValueChanged: suspend (newValue: String) -> Boolean = { true },
|
||||
|
||||
val entries: ImmutableMap<String, String>,
|
||||
override val onValueChanged: suspend (value: String) -> Boolean = { true },
|
||||
) : PreferenceItem<String>()
|
||||
|
||||
/**
|
||||
@@ -103,52 +104,51 @@ sealed class Preference {
|
||||
* Multiple entries can be selected at the same time.
|
||||
*/
|
||||
data class MultiSelectListPreference(
|
||||
val pref: PreferenceData<Set<String>>,
|
||||
val preference: PreferenceData<Set<String>>,
|
||||
val entries: ImmutableMap<String, String>,
|
||||
override val title: String,
|
||||
override val subtitle: String? = "%s",
|
||||
val subtitleProvider: @Composable (
|
||||
value: Set<String>,
|
||||
entries: ImmutableMap<String, String>,
|
||||
) -> String? = { v, e ->
|
||||
val combined = remember(v) {
|
||||
v.map { e[it] }
|
||||
.takeIf { it.isNotEmpty() }
|
||||
?.joinToString()
|
||||
} ?: stringResource(MR.strings.none)
|
||||
subtitle?.format(combined)
|
||||
},
|
||||
val subtitleProvider: @Composable (value: Set<String>, entries: ImmutableMap<String, String>) -> String? =
|
||||
{ v, e ->
|
||||
val combined = remember(v, e) {
|
||||
v.mapNotNull { e[it] }
|
||||
.joinToString()
|
||||
.takeUnless { it.isBlank() }
|
||||
}
|
||||
?: stringResource(MR.strings.none)
|
||||
subtitle?.format(combined)
|
||||
},
|
||||
override val icon: ImageVector? = null,
|
||||
override val enabled: Boolean = true,
|
||||
override val onValueChanged: suspend (newValue: Set<String>) -> Boolean = { true },
|
||||
|
||||
val entries: ImmutableMap<String, String>,
|
||||
override val onValueChanged: suspend (value: Set<String>) -> Boolean = { true },
|
||||
) : PreferenceItem<Set<String>>()
|
||||
|
||||
/**
|
||||
* A [PreferenceItem] that shows a EditText in the dialog.
|
||||
*/
|
||||
data class EditTextPreference(
|
||||
val pref: PreferenceData<String>,
|
||||
val preference: PreferenceData<String>,
|
||||
override val title: String,
|
||||
override val subtitle: String? = "%s",
|
||||
override val icon: ImageVector? = null,
|
||||
override val enabled: Boolean = true,
|
||||
override val onValueChanged: suspend (newValue: String) -> Boolean = { true },
|
||||
) : PreferenceItem<String>()
|
||||
override val onValueChanged: suspend (value: String) -> Boolean = { true },
|
||||
) : PreferenceItem<String>() {
|
||||
override val icon: ImageVector? = null
|
||||
}
|
||||
|
||||
/**
|
||||
* A [PreferenceItem] for individual tracker.
|
||||
*/
|
||||
data class TrackerPreference(
|
||||
val tracker: Tracker,
|
||||
override val title: String,
|
||||
val login: () -> Unit,
|
||||
val logout: () -> Unit,
|
||||
) : PreferenceItem<String>() {
|
||||
override val title: String = ""
|
||||
override val enabled: Boolean = true
|
||||
override val subtitle: String? = null
|
||||
override val icon: ImageVector? = null
|
||||
override val onValueChanged: suspend (newValue: String) -> Boolean = { true }
|
||||
override val onValueChanged: suspend (value: String) -> Boolean = { true }
|
||||
}
|
||||
|
||||
data class InfoPreference(
|
||||
@@ -157,7 +157,7 @@ sealed class Preference {
|
||||
override val enabled: Boolean = true
|
||||
override val subtitle: String? = null
|
||||
override val icon: ImageVector? = null
|
||||
override val onValueChanged: suspend (newValue: String) -> Boolean = { true }
|
||||
override val onValueChanged: suspend (value: String) -> Boolean = { true }
|
||||
}
|
||||
|
||||
data class CustomPreference(
|
||||
@@ -167,7 +167,7 @@ sealed class Preference {
|
||||
override val enabled: Boolean = true
|
||||
override val subtitle: String? = null
|
||||
override val icon: ImageVector? = null
|
||||
override val onValueChanged: suspend (newValue: Unit) -> Boolean = { true }
|
||||
override val onValueChanged: suspend (value: Unit) -> Boolean = { true }
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -5,6 +5,8 @@ import androidx.compose.animation.expandVertically
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.shrinkVertically
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.collectAsState
|
||||
@@ -12,16 +14,20 @@ import androidx.compose.runtime.compositionLocalOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.structuralEqualityPolicy
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.presentation.more.settings.widget.EditTextPreferenceWidget
|
||||
import eu.kanade.presentation.more.settings.widget.InfoWidget
|
||||
import eu.kanade.presentation.more.settings.widget.ListPreferenceWidget
|
||||
import eu.kanade.presentation.more.settings.widget.MultiSelectListPreferenceWidget
|
||||
import eu.kanade.presentation.more.settings.widget.PrefsHorizontalPadding
|
||||
import eu.kanade.presentation.more.settings.widget.PrefsVerticalPadding
|
||||
import eu.kanade.presentation.more.settings.widget.SwitchPreferenceWidget
|
||||
import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget
|
||||
import eu.kanade.presentation.more.settings.widget.TitleFontSize
|
||||
import eu.kanade.presentation.more.settings.widget.TrackingPreferenceWidget
|
||||
import kotlinx.coroutines.launch
|
||||
import tachiyomi.presentation.core.components.SliderItem
|
||||
import tachiyomi.presentation.core.components.BaseSliderItem
|
||||
import tachiyomi.presentation.core.util.collectAsState
|
||||
|
||||
val LocalPreferenceHighlighted = compositionLocalOf(structuralEqualityPolicy()) { false }
|
||||
@@ -60,7 +66,7 @@ internal fun PreferenceItem(
|
||||
) {
|
||||
when (item) {
|
||||
is Preference.PreferenceItem.SwitchPreference -> {
|
||||
val value by item.pref.collectAsState()
|
||||
val value by item.preference.collectAsState()
|
||||
SwitchPreferenceWidget(
|
||||
title = item.title,
|
||||
subtitle = item.subtitle,
|
||||
@@ -69,29 +75,33 @@ internal fun PreferenceItem(
|
||||
onCheckedChanged = { newValue ->
|
||||
scope.launch {
|
||||
if (item.onValueChanged(newValue)) {
|
||||
item.pref.set(newValue)
|
||||
item.preference.set(newValue)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
is Preference.PreferenceItem.SliderPreference -> {
|
||||
// TODO: use different composable?
|
||||
SliderItem(
|
||||
BaseSliderItem(
|
||||
label = item.title,
|
||||
min = item.min,
|
||||
max = item.max,
|
||||
value = item.value,
|
||||
valueRange = item.valueRange,
|
||||
valueText = item.subtitle.takeUnless { it.isNullOrEmpty() } ?: item.value.toString(),
|
||||
steps = item.steps,
|
||||
labelStyle = MaterialTheme.typography.titleLarge.copy(fontSize = TitleFontSize),
|
||||
onChange = {
|
||||
scope.launch {
|
||||
item.onValueChanged(it)
|
||||
}
|
||||
},
|
||||
modifier = Modifier.padding(
|
||||
horizontal = PrefsHorizontalPadding,
|
||||
vertical = PrefsVerticalPadding,
|
||||
),
|
||||
)
|
||||
}
|
||||
is Preference.PreferenceItem.ListPreference<*> -> {
|
||||
val value by item.pref.collectAsState()
|
||||
val value by item.preference.collectAsState()
|
||||
ListPreferenceWidget(
|
||||
value = value,
|
||||
title = item.title,
|
||||
@@ -118,14 +128,14 @@ internal fun PreferenceItem(
|
||||
)
|
||||
}
|
||||
is Preference.PreferenceItem.MultiSelectListPreference -> {
|
||||
val values by item.pref.collectAsState()
|
||||
val values by item.preference.collectAsState()
|
||||
MultiSelectListPreferenceWidget(
|
||||
preference = item,
|
||||
values = values,
|
||||
onValuesChange = { newValues ->
|
||||
scope.launch {
|
||||
if (item.onValueChanged(newValues)) {
|
||||
item.pref.set(newValues.toMutableSet())
|
||||
item.preference.set(newValues.toMutableSet())
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -140,7 +150,7 @@ internal fun PreferenceItem(
|
||||
)
|
||||
}
|
||||
is Preference.PreferenceItem.EditTextPreference -> {
|
||||
val values by item.pref.collectAsState()
|
||||
val values by item.preference.collectAsState()
|
||||
EditTextPreferenceWidget(
|
||||
title = item.title,
|
||||
subtitle = item.subtitle,
|
||||
@@ -148,7 +158,7 @@ internal fun PreferenceItem(
|
||||
value = values,
|
||||
onConfirm = {
|
||||
val accepted = item.onValueChanged(it)
|
||||
if (accepted) item.pref.set(it)
|
||||
if (accepted) item.preference.set(it)
|
||||
accepted
|
||||
},
|
||||
)
|
||||
|
@@ -48,8 +48,7 @@ import eu.kanade.tachiyomi.network.PREF_DOH_SHECAN
|
||||
import eu.kanade.tachiyomi.ui.more.OnboardingScreen
|
||||
import eu.kanade.tachiyomi.util.CrashLogUtil
|
||||
import eu.kanade.tachiyomi.util.system.GLUtil
|
||||
import eu.kanade.tachiyomi.util.system.isDevFlavor
|
||||
import eu.kanade.tachiyomi.util.system.isPreviewBuildType
|
||||
import eu.kanade.tachiyomi.util.system.isReleaseBuildType
|
||||
import eu.kanade.tachiyomi.util.system.isShizukuInstalled
|
||||
import eu.kanade.tachiyomi.util.system.powerManager
|
||||
import eu.kanade.tachiyomi.util.system.setDefaultSettings
|
||||
@@ -64,6 +63,7 @@ import tachiyomi.core.common.util.lang.launchNonCancellable
|
||||
import tachiyomi.core.common.util.lang.withUIContext
|
||||
import tachiyomi.core.common.util.system.ImageUtil
|
||||
import tachiyomi.core.common.util.system.logcat
|
||||
import tachiyomi.domain.library.service.LibraryPreferences
|
||||
import tachiyomi.domain.manga.interactor.ResetViewerFlags
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
@@ -86,6 +86,7 @@ object SettingsAdvancedScreen : SearchableSettings {
|
||||
|
||||
val basePreferences = remember { Injekt.get<BasePreferences>() }
|
||||
val networkPreferences = remember { Injekt.get<NetworkPreferences>() }
|
||||
val libraryPreferences = remember { Injekt.get<LibraryPreferences>() }
|
||||
|
||||
return listOf(
|
||||
Preference.PreferenceItem.TextPreference(
|
||||
@@ -98,7 +99,7 @@ object SettingsAdvancedScreen : SearchableSettings {
|
||||
},
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = networkPreferences.verboseLogging(),
|
||||
preference = networkPreferences.verboseLogging(),
|
||||
title = stringResource(MR.strings.pref_verbose_logging),
|
||||
subtitle = stringResource(MR.strings.pref_verbose_logging_summary),
|
||||
onValueChanged = {
|
||||
@@ -126,7 +127,7 @@ object SettingsAdvancedScreen : SearchableSettings {
|
||||
getBackgroundActivityGroup(),
|
||||
getDataGroup(),
|
||||
getNetworkGroup(networkPreferences = networkPreferences),
|
||||
getLibraryGroup(),
|
||||
getLibraryGroup(libraryPreferences = libraryPreferences),
|
||||
getReaderGroup(basePreferences = basePreferences),
|
||||
getExtensionsGroup(basePreferences = basePreferences),
|
||||
)
|
||||
@@ -237,8 +238,7 @@ object SettingsAdvancedScreen : SearchableSettings {
|
||||
},
|
||||
),
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = networkPreferences.dohProvider(),
|
||||
title = stringResource(MR.strings.pref_dns_over_https),
|
||||
preference = networkPreferences.dohProvider(),
|
||||
entries = persistentMapOf(
|
||||
-1 to stringResource(MR.strings.disabled),
|
||||
PREF_DOH_CLOUDFLARE to "Cloudflare",
|
||||
@@ -254,13 +254,14 @@ object SettingsAdvancedScreen : SearchableSettings {
|
||||
PREF_DOH_NJALLA to "Njalla",
|
||||
PREF_DOH_SHECAN to "Shecan",
|
||||
),
|
||||
title = stringResource(MR.strings.pref_dns_over_https),
|
||||
onValueChanged = {
|
||||
context.toast(MR.strings.requires_app_restart)
|
||||
true
|
||||
},
|
||||
),
|
||||
Preference.PreferenceItem.EditTextPreference(
|
||||
pref = userAgentPref,
|
||||
preference = userAgentPref,
|
||||
title = stringResource(MR.strings.pref_user_agent_string),
|
||||
onValueChanged = {
|
||||
try {
|
||||
@@ -287,7 +288,9 @@ object SettingsAdvancedScreen : SearchableSettings {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getLibraryGroup(): Preference.PreferenceGroup {
|
||||
private fun getLibraryGroup(
|
||||
libraryPreferences: LibraryPreferences,
|
||||
): Preference.PreferenceGroup {
|
||||
val scope = rememberCoroutineScope()
|
||||
val context = LocalContext.current
|
||||
|
||||
@@ -315,6 +318,11 @@ 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),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -337,13 +345,7 @@ object SettingsAdvancedScreen : SearchableSettings {
|
||||
title = stringResource(MR.strings.pref_category_reader),
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = basePreferences.hardwareBitmapThreshold(),
|
||||
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 = basePreferences.hardwareBitmapThreshold(),
|
||||
entries = GLUtil.CUSTOM_TEXTURE_LIMIT_OPTIONS
|
||||
.mapIndexed { index, option ->
|
||||
val display = if (index == 0) {
|
||||
@@ -355,6 +357,17 @@ object SettingsAdvancedScreen : SearchableSettings {
|
||||
}
|
||||
.toMap()
|
||||
.toImmutableMap(),
|
||||
title = stringResource(MR.strings.pref_hardware_bitmap_threshold),
|
||||
subtitleProvider = { value, options ->
|
||||
stringResource(MR.strings.pref_hardware_bitmap_threshold_summary, options[value].orEmpty())
|
||||
},
|
||||
enabled = !ImageUtil.HARDWARE_BITMAP_UNSUPPORTED &&
|
||||
GLUtil.DEVICE_TEXTURE_LIMIT > GLUtil.SAFE_TEXTURE_LIMIT,
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
preference = basePreferences.alwaysDecodeLongStripWithSSIV(),
|
||||
title = stringResource(MR.strings.pref_always_decode_long_strip_with_ssiv_2),
|
||||
subtitle = stringResource(MR.strings.pref_always_decode_long_strip_with_ssiv_summary),
|
||||
),
|
||||
Preference.PreferenceItem.TextPreference(
|
||||
title = stringResource(MR.strings.pref_display_profile),
|
||||
@@ -404,19 +417,19 @@ object SettingsAdvancedScreen : SearchableSettings {
|
||||
title = stringResource(MR.strings.label_extensions),
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = extensionInstallerPref,
|
||||
title = stringResource(MR.strings.ext_installer_pref),
|
||||
preference = extensionInstallerPref,
|
||||
entries = extensionInstallerPref.entries
|
||||
.filter {
|
||||
// TODO: allow private option in stable versions once URL handling is more fleshed out
|
||||
if (isPreviewBuildType || isDevFlavor) {
|
||||
true
|
||||
} else {
|
||||
if (isReleaseBuildType) {
|
||||
it != BasePreferences.ExtensionInstaller.PRIVATE
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
.associateWith { stringResource(it.titleRes) }
|
||||
.toImmutableMap(),
|
||||
title = stringResource(MR.strings.ext_installer_pref),
|
||||
onValueChanged = {
|
||||
if (it == BasePreferences.ExtensionInstaller.SHIZUKU &&
|
||||
!context.isShizukuInstalled
|
||||
|
@@ -82,7 +82,7 @@ object SettingsAppearanceScreen : SearchableSettings {
|
||||
}
|
||||
},
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = amoledPref,
|
||||
preference = amoledPref,
|
||||
title = stringResource(MR.strings.pref_dark_theme_pure_black),
|
||||
enabled = themeMode != ThemeMode.LIGHT,
|
||||
onValueChanged = {
|
||||
@@ -116,28 +116,28 @@ object SettingsAppearanceScreen : SearchableSettings {
|
||||
onClick = { navigator.push(AppLanguageScreen()) },
|
||||
),
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = uiPreferences.tabletUiMode(),
|
||||
title = stringResource(MR.strings.pref_tablet_ui_mode),
|
||||
preference = uiPreferences.tabletUiMode(),
|
||||
entries = TabletUiMode.entries
|
||||
.associateWith { stringResource(it.titleRes) }
|
||||
.toImmutableMap(),
|
||||
title = stringResource(MR.strings.pref_tablet_ui_mode),
|
||||
onValueChanged = {
|
||||
context.toast(MR.strings.requires_app_restart)
|
||||
true
|
||||
},
|
||||
),
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = uiPreferences.dateFormat(),
|
||||
title = stringResource(MR.strings.pref_date_format),
|
||||
preference = uiPreferences.dateFormat(),
|
||||
entries = DateFormats
|
||||
.associateWith {
|
||||
val formattedDate = UiPreferences.dateFormat(it).format(now)
|
||||
"${it.ifEmpty { stringResource(MR.strings.label_default) }} ($formattedDate)"
|
||||
}
|
||||
.toImmutableMap(),
|
||||
title = stringResource(MR.strings.pref_date_format),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = uiPreferences.relativeTime(),
|
||||
preference = uiPreferences.relativeTime(),
|
||||
title = stringResource(MR.strings.pref_relative_format),
|
||||
subtitle = stringResource(
|
||||
MR.strings.pref_relative_format_summary,
|
||||
@@ -145,6 +145,10 @@ object SettingsAppearanceScreen : SearchableSettings {
|
||||
formattedNow,
|
||||
),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
preference = uiPreferences.imagesInDescription(),
|
||||
title = stringResource(MR.strings.pref_display_images_description),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
@@ -43,7 +43,7 @@ object SettingsBrowseScreen : SearchableSettings {
|
||||
title = stringResource(MR.strings.label_sources),
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = sourcePreferences.hideInLibraryItems(),
|
||||
preference = sourcePreferences.hideInLibraryItems(),
|
||||
title = stringResource(MR.strings.pref_hide_in_library_items),
|
||||
),
|
||||
Preference.PreferenceItem.TextPreference(
|
||||
@@ -59,7 +59,7 @@ object SettingsBrowseScreen : SearchableSettings {
|
||||
title = stringResource(MR.strings.pref_category_nsfw_content),
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = sourcePreferences.showNsfwSource(),
|
||||
preference = sourcePreferences.showNsfwSource(),
|
||||
title = stringResource(MR.strings.pref_show_nsfw_source),
|
||||
subtitle = stringResource(MR.strings.requires_app_restart),
|
||||
onValueChanged = {
|
||||
|
@@ -7,7 +7,9 @@ import android.net.Uri
|
||||
import androidx.activity.compose.ManagedActivityResultLauncher
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
@@ -15,6 +17,8 @@ import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.HelpOutline
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MultiChoiceSegmentedButtonRow
|
||||
@@ -22,12 +26,15 @@ import androidx.compose.material3.SegmentedButton
|
||||
import androidx.compose.material3.SegmentedButtonDefaults
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.ReadOnlyComposable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
@@ -45,10 +52,14 @@ import eu.kanade.presentation.util.relativeTimeSpanString
|
||||
import eu.kanade.tachiyomi.data.backup.create.BackupCreateJob
|
||||
import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob
|
||||
import eu.kanade.tachiyomi.data.cache.ChapterCache
|
||||
import eu.kanade.tachiyomi.data.export.LibraryExporter
|
||||
import eu.kanade.tachiyomi.data.export.LibraryExporter.ExportOptions
|
||||
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.persistentMapOf
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import logcat.LogPriority
|
||||
import tachiyomi.core.common.i18n.stringResource
|
||||
import tachiyomi.core.common.storage.displayablePath
|
||||
@@ -57,8 +68,11 @@ import tachiyomi.core.common.util.lang.withUIContext
|
||||
import tachiyomi.core.common.util.system.logcat
|
||||
import tachiyomi.domain.backup.service.BackupPreferences
|
||||
import tachiyomi.domain.library.service.LibraryPreferences
|
||||
import tachiyomi.domain.manga.interactor.GetFavorites
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.domain.storage.service.StoragePreferences
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.material.TextButton
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import tachiyomi.presentation.core.util.collectAsState
|
||||
import uy.kohesive.injekt.Injekt
|
||||
@@ -95,6 +109,7 @@ object SettingsDataScreen : SearchableSettings {
|
||||
|
||||
getBackupAndRestoreGroup(backupPreferences = backupPreferences),
|
||||
getDataGroup(),
|
||||
getExportGroup(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -239,8 +254,7 @@ object SettingsDataScreen : SearchableSettings {
|
||||
|
||||
// Automatic backups
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = backupPreferences.backupInterval(),
|
||||
title = stringResource(MR.strings.pref_backup_interval),
|
||||
preference = backupPreferences.backupInterval(),
|
||||
entries = persistentMapOf(
|
||||
0 to stringResource(MR.strings.off),
|
||||
6 to stringResource(MR.strings.update_6hour),
|
||||
@@ -249,6 +263,7 @@ object SettingsDataScreen : SearchableSettings {
|
||||
48 to stringResource(MR.strings.update_48hour),
|
||||
168 to stringResource(MR.strings.update_weekly),
|
||||
),
|
||||
title = stringResource(MR.strings.pref_backup_interval),
|
||||
onValueChanged = {
|
||||
BackupCreateJob.setupTask(context, it)
|
||||
true
|
||||
@@ -306,10 +321,147 @@ object SettingsDataScreen : SearchableSettings {
|
||||
},
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = libraryPreferences.autoClearChapterCache(),
|
||||
preference = libraryPreferences.autoClearChapterCache(),
|
||||
title = stringResource(MR.strings.pref_auto_clear_chapter_cache),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getExportGroup(): Preference.PreferenceGroup {
|
||||
var showDialog by remember { mutableStateOf(false) }
|
||||
var exportOptions by remember {
|
||||
mutableStateOf(
|
||||
ExportOptions(
|
||||
includeTitle = true,
|
||||
includeAuthor = true,
|
||||
includeArtist = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
val getFavorites = remember { Injekt.get<GetFavorites>() }
|
||||
var favorites by remember { mutableStateOf<List<Manga>>(emptyList()) }
|
||||
LaunchedEffect(Unit) {
|
||||
favorites = getFavorites.await()
|
||||
}
|
||||
|
||||
val saveFileLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.CreateDocument("text/csv"),
|
||||
) { uri ->
|
||||
uri?.let {
|
||||
scope.launch {
|
||||
LibraryExporter.exportToCsv(
|
||||
context = context,
|
||||
uri = it,
|
||||
favorites = favorites,
|
||||
options = exportOptions,
|
||||
onExportComplete = {
|
||||
scope.launch(Dispatchers.Main) {
|
||||
context.toast(MR.strings.library_exported)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (showDialog) {
|
||||
ColumnSelectionDialog(
|
||||
options = exportOptions,
|
||||
onConfirm = { options ->
|
||||
exportOptions = options
|
||||
saveFileLauncher.launch("mihon_library.csv")
|
||||
},
|
||||
onDismissRequest = { showDialog = false },
|
||||
)
|
||||
}
|
||||
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(MR.strings.export),
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.TextPreference(
|
||||
title = stringResource(MR.strings.library_list),
|
||||
onClick = { showDialog = true },
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ColumnSelectionDialog(
|
||||
options: ExportOptions,
|
||||
onConfirm: (ExportOptions) -> Unit,
|
||||
onDismissRequest: () -> Unit,
|
||||
) {
|
||||
var titleSelected by remember { mutableStateOf(options.includeTitle) }
|
||||
var authorSelected by remember { mutableStateOf(options.includeAuthor) }
|
||||
var artistSelected by remember { mutableStateOf(options.includeArtist) }
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
title = {
|
||||
Text(text = stringResource(MR.strings.migration_dialog_what_to_include))
|
||||
},
|
||||
text = {
|
||||
Column {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Checkbox(
|
||||
checked = titleSelected,
|
||||
onCheckedChange = { checked ->
|
||||
titleSelected = checked
|
||||
if (!checked) {
|
||||
authorSelected = false
|
||||
artistSelected = false
|
||||
}
|
||||
},
|
||||
)
|
||||
Text(text = stringResource(MR.strings.title))
|
||||
}
|
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Checkbox(
|
||||
checked = authorSelected,
|
||||
onCheckedChange = { authorSelected = it },
|
||||
enabled = titleSelected,
|
||||
)
|
||||
Text(text = stringResource(MR.strings.author))
|
||||
}
|
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Checkbox(
|
||||
checked = artistSelected,
|
||||
onCheckedChange = { artistSelected = it },
|
||||
enabled = titleSelected,
|
||||
)
|
||||
Text(text = stringResource(MR.strings.artist))
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
onConfirm(
|
||||
ExportOptions(
|
||||
includeTitle = titleSelected,
|
||||
includeAuthor = authorSelected,
|
||||
includeArtist = artistSelected,
|
||||
),
|
||||
)
|
||||
onDismissRequest()
|
||||
},
|
||||
) {
|
||||
Text(text = stringResource(MR.strings.action_save))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismissRequest) {
|
||||
Text(text = stringResource(MR.strings.action_cancel))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@@ -39,15 +39,15 @@ object SettingsDownloadScreen : SearchableSettings {
|
||||
val downloadPreferences = remember { Injekt.get<DownloadPreferences>() }
|
||||
return listOf(
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = downloadPreferences.downloadOnlyOverWifi(),
|
||||
preference = downloadPreferences.downloadOnlyOverWifi(),
|
||||
title = stringResource(MR.strings.connected_to_wifi),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = downloadPreferences.saveChaptersAsCBZ(),
|
||||
preference = downloadPreferences.saveChaptersAsCBZ(),
|
||||
title = stringResource(MR.strings.save_chapter_as_cbz),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = downloadPreferences.splitTallImages(),
|
||||
preference = downloadPreferences.splitTallImages(),
|
||||
title = stringResource(MR.strings.split_tall_images),
|
||||
subtitle = stringResource(MR.strings.split_tall_images_summary),
|
||||
),
|
||||
@@ -72,12 +72,11 @@ object SettingsDownloadScreen : SearchableSettings {
|
||||
title = stringResource(MR.strings.pref_category_delete_chapters),
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = downloadPreferences.removeAfterMarkedAsRead(),
|
||||
preference = downloadPreferences.removeAfterMarkedAsRead(),
|
||||
title = stringResource(MR.strings.pref_remove_after_marked_as_read),
|
||||
),
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = downloadPreferences.removeAfterReadSlots(),
|
||||
title = stringResource(MR.strings.pref_remove_after_read),
|
||||
preference = downloadPreferences.removeAfterReadSlots(),
|
||||
entries = persistentMapOf(
|
||||
-1 to stringResource(MR.strings.disabled),
|
||||
0 to stringResource(MR.strings.last_read_chapter),
|
||||
@@ -86,9 +85,10 @@ object SettingsDownloadScreen : SearchableSettings {
|
||||
3 to stringResource(MR.strings.fourth_to_last),
|
||||
4 to stringResource(MR.strings.fifth_to_last),
|
||||
),
|
||||
title = stringResource(MR.strings.pref_remove_after_read),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = downloadPreferences.removeBookmarkedChapters(),
|
||||
preference = downloadPreferences.removeBookmarkedChapters(),
|
||||
title = stringResource(MR.strings.pref_remove_bookmarked_chapters),
|
||||
),
|
||||
getExcludedCategoriesPreference(
|
||||
@@ -105,11 +105,11 @@ object SettingsDownloadScreen : SearchableSettings {
|
||||
categories: () -> List<Category>,
|
||||
): Preference.PreferenceItem.MultiSelectListPreference {
|
||||
return Preference.PreferenceItem.MultiSelectListPreference(
|
||||
pref = downloadPreferences.removeExcludeCategories(),
|
||||
title = stringResource(MR.strings.pref_remove_exclude_categories),
|
||||
preference = downloadPreferences.removeExcludeCategories(),
|
||||
entries = categories()
|
||||
.associate { it.id.toString() to it.visualName }
|
||||
.toImmutableMap(),
|
||||
title = stringResource(MR.strings.pref_remove_exclude_categories),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -149,11 +149,11 @@ object SettingsDownloadScreen : SearchableSettings {
|
||||
title = stringResource(MR.strings.pref_category_auto_download),
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = downloadNewChaptersPref,
|
||||
preference = downloadNewChaptersPref,
|
||||
title = stringResource(MR.strings.pref_download_new),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = downloadNewUnreadChaptersOnlyPref,
|
||||
preference = downloadNewUnreadChaptersOnlyPref,
|
||||
title = stringResource(MR.strings.pref_download_new_unread_chapters_only),
|
||||
enabled = downloadNewChapters,
|
||||
),
|
||||
@@ -164,8 +164,8 @@ object SettingsDownloadScreen : SearchableSettings {
|
||||
included = included,
|
||||
excluded = excluded,
|
||||
),
|
||||
onClick = { showDialog = true },
|
||||
enabled = downloadNewChapters,
|
||||
onClick = { showDialog = true },
|
||||
),
|
||||
),
|
||||
)
|
||||
@@ -179,8 +179,7 @@ object SettingsDownloadScreen : SearchableSettings {
|
||||
title = stringResource(MR.strings.download_ahead),
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = downloadPreferences.autoDownloadWhileReading(),
|
||||
title = stringResource(MR.strings.auto_download_while_reading),
|
||||
preference = downloadPreferences.autoDownloadWhileReading(),
|
||||
entries = listOf(0, 2, 3, 5, 10)
|
||||
.associateWith {
|
||||
if (it == 0) {
|
||||
@@ -190,6 +189,7 @@ object SettingsDownloadScreen : SearchableSettings {
|
||||
}
|
||||
}
|
||||
.toImmutableMap(),
|
||||
title = stringResource(MR.strings.auto_download_while_reading),
|
||||
),
|
||||
Preference.PreferenceItem.InfoPreference(stringResource(MR.strings.download_ahead_info)),
|
||||
),
|
||||
|
@@ -35,6 +35,8 @@ import tachiyomi.domain.library.service.LibraryPreferences.Companion.MANGA_HAS_U
|
||||
import tachiyomi.domain.library.service.LibraryPreferences.Companion.MANGA_NON_COMPLETED
|
||||
import tachiyomi.domain.library.service.LibraryPreferences.Companion.MANGA_NON_READ
|
||||
import tachiyomi.domain.library.service.LibraryPreferences.Companion.MANGA_OUTSIDE_RELEASE_PERIOD
|
||||
import tachiyomi.domain.library.service.LibraryPreferences.Companion.MARK_DUPLICATE_CHAPTER_READ_EXISTING
|
||||
import tachiyomi.domain.library.service.LibraryPreferences.Companion.MARK_DUPLICATE_CHAPTER_READ_NEW
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.i18n.pluralStringResource
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
@@ -57,7 +59,7 @@ object SettingsLibraryScreen : SearchableSettings {
|
||||
return listOf(
|
||||
getCategoriesGroup(LocalNavigator.currentOrThrow, allCategories, libraryPreferences),
|
||||
getGlobalUpdateGroup(allCategories, libraryPreferences),
|
||||
getChapterSwipeActionsGroup(libraryPreferences),
|
||||
getBehaviorGroup(libraryPreferences),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -89,12 +91,12 @@ object SettingsLibraryScreen : SearchableSettings {
|
||||
onClick = { navigator.push(CategoryScreen()) },
|
||||
),
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = libraryPreferences.defaultCategory(),
|
||||
title = stringResource(MR.strings.default_category),
|
||||
preference = libraryPreferences.defaultCategory(),
|
||||
entries = ids.zip(labels).toMap().toImmutableMap(),
|
||||
title = stringResource(MR.strings.default_category),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = libraryPreferences.categorizedDisplaySettings(),
|
||||
preference = libraryPreferences.categorizedDisplaySettings(),
|
||||
title = stringResource(MR.strings.categorized_display_settings),
|
||||
onValueChanged = {
|
||||
if (!it) {
|
||||
@@ -146,8 +148,7 @@ object SettingsLibraryScreen : SearchableSettings {
|
||||
title = stringResource(MR.strings.pref_category_library_update),
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = autoUpdateIntervalPref,
|
||||
title = stringResource(MR.strings.pref_library_update_interval),
|
||||
preference = autoUpdateIntervalPref,
|
||||
entries = persistentMapOf(
|
||||
0 to stringResource(MR.strings.update_never),
|
||||
12 to stringResource(MR.strings.update_12hour),
|
||||
@@ -156,21 +157,22 @@ object SettingsLibraryScreen : SearchableSettings {
|
||||
72 to stringResource(MR.strings.update_72hour),
|
||||
168 to stringResource(MR.strings.update_weekly),
|
||||
),
|
||||
title = stringResource(MR.strings.pref_library_update_interval),
|
||||
onValueChanged = {
|
||||
LibraryUpdateJob.setupTask(context, it)
|
||||
true
|
||||
},
|
||||
),
|
||||
Preference.PreferenceItem.MultiSelectListPreference(
|
||||
pref = libraryPreferences.autoUpdateDeviceRestrictions(),
|
||||
enabled = autoUpdateInterval > 0,
|
||||
title = stringResource(MR.strings.pref_library_update_restriction),
|
||||
subtitle = stringResource(MR.strings.restrictions),
|
||||
preference = libraryPreferences.autoUpdateDeviceRestrictions(),
|
||||
entries = persistentMapOf(
|
||||
DEVICE_ONLY_ON_WIFI to stringResource(MR.strings.connected_to_wifi),
|
||||
DEVICE_NETWORK_NOT_METERED to stringResource(MR.strings.network_not_metered),
|
||||
DEVICE_CHARGING to stringResource(MR.strings.charging),
|
||||
),
|
||||
title = stringResource(MR.strings.pref_library_update_restriction),
|
||||
subtitle = stringResource(MR.strings.restrictions),
|
||||
enabled = autoUpdateInterval > 0,
|
||||
onValueChanged = {
|
||||
// Post to event looper to allow the preference to be updated.
|
||||
ContextCompat.getMainExecutor(context).execute { LibraryUpdateJob.setupTask(context) }
|
||||
@@ -187,22 +189,22 @@ object SettingsLibraryScreen : SearchableSettings {
|
||||
onClick = { showCategoriesDialog = true },
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = libraryPreferences.autoUpdateMetadata(),
|
||||
preference = libraryPreferences.autoUpdateMetadata(),
|
||||
title = stringResource(MR.strings.pref_library_update_refresh_metadata),
|
||||
subtitle = stringResource(MR.strings.pref_library_update_refresh_metadata_summary),
|
||||
),
|
||||
Preference.PreferenceItem.MultiSelectListPreference(
|
||||
pref = libraryPreferences.autoUpdateMangaRestrictions(),
|
||||
title = stringResource(MR.strings.pref_library_update_smart_update),
|
||||
preference = libraryPreferences.autoUpdateMangaRestrictions(),
|
||||
entries = persistentMapOf(
|
||||
MANGA_HAS_UNREAD to stringResource(MR.strings.pref_update_only_completely_read),
|
||||
MANGA_NON_READ to stringResource(MR.strings.pref_update_only_started),
|
||||
MANGA_NON_COMPLETED to stringResource(MR.strings.pref_update_only_non_completed),
|
||||
MANGA_OUTSIDE_RELEASE_PERIOD to stringResource(MR.strings.pref_update_only_in_release_period),
|
||||
),
|
||||
title = stringResource(MR.strings.pref_library_update_smart_update),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = libraryPreferences.newShowUpdatesCount(),
|
||||
preference = libraryPreferences.newShowUpdatesCount(),
|
||||
title = stringResource(MR.strings.pref_library_update_show_tab_badge),
|
||||
),
|
||||
),
|
||||
@@ -210,15 +212,14 @@ object SettingsLibraryScreen : SearchableSettings {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getChapterSwipeActionsGroup(
|
||||
private fun getBehaviorGroup(
|
||||
libraryPreferences: LibraryPreferences,
|
||||
): Preference.PreferenceGroup {
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(MR.strings.pref_chapter_swipe),
|
||||
title = stringResource(MR.strings.pref_behavior),
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = libraryPreferences.swipeToStartAction(),
|
||||
title = stringResource(MR.strings.pref_chapter_swipe_start),
|
||||
preference = libraryPreferences.swipeToStartAction(),
|
||||
entries = persistentMapOf(
|
||||
LibraryPreferences.ChapterSwipeAction.Disabled to
|
||||
stringResource(MR.strings.disabled),
|
||||
@@ -229,10 +230,10 @@ object SettingsLibraryScreen : SearchableSettings {
|
||||
LibraryPreferences.ChapterSwipeAction.Download to
|
||||
stringResource(MR.strings.action_download),
|
||||
),
|
||||
title = stringResource(MR.strings.pref_chapter_swipe_start),
|
||||
),
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = libraryPreferences.swipeToEndAction(),
|
||||
title = stringResource(MR.strings.pref_chapter_swipe_end),
|
||||
preference = libraryPreferences.swipeToEndAction(),
|
||||
entries = persistentMapOf(
|
||||
LibraryPreferences.ChapterSwipeAction.Disabled to
|
||||
stringResource(MR.strings.disabled),
|
||||
@@ -243,6 +244,21 @@ object SettingsLibraryScreen : SearchableSettings {
|
||||
LibraryPreferences.ChapterSwipeAction.Download to
|
||||
stringResource(MR.strings.action_download),
|
||||
),
|
||||
title = stringResource(MR.strings.pref_chapter_swipe_end),
|
||||
),
|
||||
Preference.PreferenceItem.MultiSelectListPreference(
|
||||
preference = libraryPreferences.markDuplicateReadChapterAsRead(),
|
||||
entries = persistentMapOf(
|
||||
MARK_DUPLICATE_CHAPTER_READ_EXISTING to
|
||||
stringResource(MR.strings.pref_mark_duplicate_read_chapter_read_existing),
|
||||
MARK_DUPLICATE_CHAPTER_READ_NEW to
|
||||
stringResource(MR.strings.pref_mark_duplicate_read_chapter_read_new),
|
||||
),
|
||||
title = stringResource(MR.strings.pref_mark_duplicate_read_chapter_read),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
preference = libraryPreferences.hideMissingChapters(),
|
||||
title = stringResource(MR.strings.pref_hide_missing_chapter_indicators),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
@@ -33,33 +33,33 @@ object SettingsReaderScreen : SearchableSettings {
|
||||
|
||||
return listOf(
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = readerPref.defaultReadingMode(),
|
||||
title = stringResource(MR.strings.pref_viewer_type),
|
||||
preference = readerPref.defaultReadingMode(),
|
||||
entries = ReadingMode.entries.drop(1)
|
||||
.associate { it.flagValue to stringResource(it.stringRes) }
|
||||
.toImmutableMap(),
|
||||
title = stringResource(MR.strings.pref_viewer_type),
|
||||
),
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = readerPref.doubleTapAnimSpeed(),
|
||||
title = stringResource(MR.strings.pref_double_tap_anim_speed),
|
||||
preference = readerPref.doubleTapAnimSpeed(),
|
||||
entries = persistentMapOf(
|
||||
1 to stringResource(MR.strings.double_tap_anim_speed_0),
|
||||
500 to stringResource(MR.strings.double_tap_anim_speed_normal),
|
||||
250 to stringResource(MR.strings.double_tap_anim_speed_fast),
|
||||
),
|
||||
title = stringResource(MR.strings.pref_double_tap_anim_speed),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readerPref.showReadingMode(),
|
||||
preference = readerPref.showReadingMode(),
|
||||
title = stringResource(MR.strings.pref_show_reading_mode),
|
||||
subtitle = stringResource(MR.strings.pref_show_reading_mode_summary),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readerPref.showNavigationOverlayOnStart(),
|
||||
preference = readerPref.showNavigationOverlayOnStart(),
|
||||
title = stringResource(MR.strings.pref_show_navigation_mode),
|
||||
subtitle = stringResource(MR.strings.pref_show_navigation_mode_summary),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readerPref.pageTransitions(),
|
||||
preference = readerPref.pageTransitions(),
|
||||
title = stringResource(MR.strings.pref_page_transitions),
|
||||
),
|
||||
getDisplayGroup(readerPreferences = readerPref),
|
||||
@@ -80,39 +80,39 @@ object SettingsReaderScreen : SearchableSettings {
|
||||
title = stringResource(MR.strings.pref_category_display),
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = readerPreferences.defaultOrientationType(),
|
||||
title = stringResource(MR.strings.pref_rotation_type),
|
||||
preference = readerPreferences.defaultOrientationType(),
|
||||
entries = ReaderOrientation.entries.drop(1)
|
||||
.associate { it.flagValue to stringResource(it.stringRes) }
|
||||
.toImmutableMap(),
|
||||
title = stringResource(MR.strings.pref_rotation_type),
|
||||
),
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = readerPreferences.readerTheme(),
|
||||
title = stringResource(MR.strings.pref_reader_theme),
|
||||
preference = readerPreferences.readerTheme(),
|
||||
entries = persistentMapOf(
|
||||
1 to stringResource(MR.strings.black_background),
|
||||
2 to stringResource(MR.strings.gray_background),
|
||||
0 to stringResource(MR.strings.white_background),
|
||||
3 to stringResource(MR.strings.automatic_background),
|
||||
),
|
||||
title = stringResource(MR.strings.pref_reader_theme),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = fullscreenPref,
|
||||
preference = fullscreenPref,
|
||||
title = stringResource(MR.strings.pref_fullscreen),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readerPreferences.cutoutShort(),
|
||||
preference = readerPreferences.cutoutShort(),
|
||||
title = stringResource(MR.strings.pref_cutout_short),
|
||||
enabled = fullscreen &&
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.P &&
|
||||
LocalView.current.rootWindowInsets?.displayCutout != null, // has cutout
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readerPreferences.keepScreenOn(),
|
||||
preference = readerPreferences.keepScreenOn(),
|
||||
title = stringResource(MR.strings.pref_keep_screen_on),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readerPreferences.showPageNumber(),
|
||||
preference = readerPreferences.showPageNumber(),
|
||||
title = stringResource(MR.strings.pref_show_page_number),
|
||||
),
|
||||
),
|
||||
@@ -135,43 +135,41 @@ object SettingsReaderScreen : SearchableSettings {
|
||||
title = "E-Ink",
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readerPreferences.flashOnPageChange(),
|
||||
preference = readerPreferences.flashOnPageChange(),
|
||||
title = stringResource(MR.strings.pref_flash_page),
|
||||
subtitle = stringResource(MR.strings.pref_flash_page_summ),
|
||||
),
|
||||
Preference.PreferenceItem.SliderPreference(
|
||||
value = flashMillis / ReaderPreferences.MILLI_CONVERSION,
|
||||
min = 1,
|
||||
max = 15,
|
||||
valueRange = 1..15,
|
||||
title = stringResource(MR.strings.pref_flash_duration),
|
||||
subtitle = stringResource(MR.strings.pref_flash_duration_summary, flashMillis),
|
||||
enabled = flashPageState,
|
||||
onValueChanged = {
|
||||
flashMillisPref.set(it * ReaderPreferences.MILLI_CONVERSION)
|
||||
true
|
||||
},
|
||||
enabled = flashPageState,
|
||||
),
|
||||
Preference.PreferenceItem.SliderPreference(
|
||||
value = flashInterval,
|
||||
min = 1,
|
||||
max = 10,
|
||||
valueRange = 1..10,
|
||||
title = stringResource(MR.strings.pref_flash_page_interval),
|
||||
subtitle = pluralStringResource(MR.plurals.pref_pages, flashInterval, flashInterval),
|
||||
enabled = flashPageState,
|
||||
onValueChanged = {
|
||||
flashIntervalPref.set(it)
|
||||
true
|
||||
},
|
||||
enabled = flashPageState,
|
||||
),
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = flashColorPref,
|
||||
title = stringResource(MR.strings.pref_flash_with),
|
||||
preference = flashColorPref,
|
||||
entries = persistentMapOf(
|
||||
ReaderPreferences.FlashColor.BLACK to stringResource(MR.strings.pref_flash_style_black),
|
||||
ReaderPreferences.FlashColor.WHITE to stringResource(MR.strings.pref_flash_style_white),
|
||||
ReaderPreferences.FlashColor.WHITE_BLACK
|
||||
to stringResource(MR.strings.pref_flash_style_white_black),
|
||||
),
|
||||
title = stringResource(MR.strings.pref_flash_with),
|
||||
enabled = flashPageState,
|
||||
),
|
||||
),
|
||||
@@ -184,19 +182,19 @@ object SettingsReaderScreen : SearchableSettings {
|
||||
title = stringResource(MR.strings.pref_category_reading),
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readerPreferences.skipRead(),
|
||||
preference = readerPreferences.skipRead(),
|
||||
title = stringResource(MR.strings.pref_skip_read_chapters),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readerPreferences.skipFiltered(),
|
||||
preference = readerPreferences.skipFiltered(),
|
||||
title = stringResource(MR.strings.pref_skip_filtered_chapters),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readerPreferences.skipDupe(),
|
||||
preference = readerPreferences.skipDupe(),
|
||||
title = stringResource(MR.strings.pref_skip_dupe_chapters),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readerPreferences.alwaysShowChapterTransition(),
|
||||
preference = readerPreferences.alwaysShowChapterTransition(),
|
||||
title = stringResource(MR.strings.pref_always_show_chapter_transition),
|
||||
),
|
||||
),
|
||||
@@ -219,16 +217,15 @@ object SettingsReaderScreen : SearchableSettings {
|
||||
title = stringResource(MR.strings.pager_viewer),
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = navModePref,
|
||||
title = stringResource(MR.strings.pref_viewer_nav),
|
||||
preference = navModePref,
|
||||
entries = ReaderPreferences.TapZones
|
||||
.mapIndexed { index, it -> index to stringResource(it) }
|
||||
.toMap()
|
||||
.toImmutableMap(),
|
||||
title = stringResource(MR.strings.pref_viewer_nav),
|
||||
),
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = readerPreferences.pagerNavInverted(),
|
||||
title = stringResource(MR.strings.pref_read_with_tapping_inverted),
|
||||
preference = readerPreferences.pagerNavInverted(),
|
||||
entries = persistentListOf(
|
||||
ReaderPreferences.TappingInvertMode.NONE,
|
||||
ReaderPreferences.TappingInvertMode.HORIZONTAL,
|
||||
@@ -237,40 +234,41 @@ object SettingsReaderScreen : SearchableSettings {
|
||||
)
|
||||
.associateWith { stringResource(it.titleRes) }
|
||||
.toImmutableMap(),
|
||||
title = stringResource(MR.strings.pref_read_with_tapping_inverted),
|
||||
enabled = navMode != 5,
|
||||
),
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = imageScaleTypePref,
|
||||
title = stringResource(MR.strings.pref_image_scale_type),
|
||||
preference = imageScaleTypePref,
|
||||
entries = ReaderPreferences.ImageScaleType
|
||||
.mapIndexed { index, it -> index + 1 to stringResource(it) }
|
||||
.toMap()
|
||||
.toImmutableMap(),
|
||||
title = stringResource(MR.strings.pref_image_scale_type),
|
||||
),
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = readerPreferences.zoomStart(),
|
||||
title = stringResource(MR.strings.pref_zoom_start),
|
||||
preference = readerPreferences.zoomStart(),
|
||||
entries = ReaderPreferences.ZoomStart
|
||||
.mapIndexed { index, it -> index + 1 to stringResource(it) }
|
||||
.toMap()
|
||||
.toImmutableMap(),
|
||||
title = stringResource(MR.strings.pref_zoom_start),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readerPreferences.cropBorders(),
|
||||
preference = readerPreferences.cropBorders(),
|
||||
title = stringResource(MR.strings.pref_crop_borders),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readerPreferences.landscapeZoom(),
|
||||
preference = readerPreferences.landscapeZoom(),
|
||||
title = stringResource(MR.strings.pref_landscape_zoom),
|
||||
enabled = imageScaleType == 1,
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readerPreferences.navigateToPan(),
|
||||
preference = readerPreferences.navigateToPan(),
|
||||
title = stringResource(MR.strings.pref_navigate_pan),
|
||||
enabled = navMode != 5,
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = dualPageSplitPref,
|
||||
preference = dualPageSplitPref,
|
||||
title = stringResource(MR.strings.pref_dual_page_split),
|
||||
onValueChanged = {
|
||||
rotateToFitPref.set(false)
|
||||
@@ -278,13 +276,13 @@ object SettingsReaderScreen : SearchableSettings {
|
||||
},
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readerPreferences.dualPageInvertPaged(),
|
||||
preference = readerPreferences.dualPageInvertPaged(),
|
||||
title = stringResource(MR.strings.pref_dual_page_invert),
|
||||
subtitle = stringResource(MR.strings.pref_dual_page_invert_summary),
|
||||
enabled = dualPageSplit,
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = rotateToFitPref,
|
||||
preference = rotateToFitPref,
|
||||
title = stringResource(MR.strings.pref_page_rotate),
|
||||
onValueChanged = {
|
||||
dualPageSplitPref.set(false)
|
||||
@@ -292,7 +290,7 @@ object SettingsReaderScreen : SearchableSettings {
|
||||
},
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readerPreferences.dualPageRotateToFitInvert(),
|
||||
preference = readerPreferences.dualPageRotateToFitInvert(),
|
||||
title = stringResource(MR.strings.pref_page_rotate_invert),
|
||||
enabled = rotateToFit,
|
||||
),
|
||||
@@ -318,16 +316,15 @@ object SettingsReaderScreen : SearchableSettings {
|
||||
title = stringResource(MR.strings.webtoon_viewer),
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = navModePref,
|
||||
title = stringResource(MR.strings.pref_viewer_nav),
|
||||
preference = navModePref,
|
||||
entries = ReaderPreferences.TapZones
|
||||
.mapIndexed { index, it -> index to stringResource(it) }
|
||||
.toMap()
|
||||
.toImmutableMap(),
|
||||
title = stringResource(MR.strings.pref_viewer_nav),
|
||||
),
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = readerPreferences.webtoonNavInverted(),
|
||||
title = stringResource(MR.strings.pref_read_with_tapping_inverted),
|
||||
preference = readerPreferences.webtoonNavInverted(),
|
||||
entries = persistentListOf(
|
||||
ReaderPreferences.TappingInvertMode.NONE,
|
||||
ReaderPreferences.TappingInvertMode.HORIZONTAL,
|
||||
@@ -336,35 +333,37 @@ object SettingsReaderScreen : SearchableSettings {
|
||||
)
|
||||
.associateWith { stringResource(it.titleRes) }
|
||||
.toImmutableMap(),
|
||||
title = stringResource(MR.strings.pref_read_with_tapping_inverted),
|
||||
enabled = navMode != 5,
|
||||
),
|
||||
Preference.PreferenceItem.SliderPreference(
|
||||
value = webtoonSidePadding,
|
||||
valueRange = ReaderPreferences.let {
|
||||
it.WEBTOON_PADDING_MIN..it.WEBTOON_PADDING_MAX
|
||||
},
|
||||
title = stringResource(MR.strings.pref_webtoon_side_padding),
|
||||
subtitle = numberFormat.format(webtoonSidePadding / 100f),
|
||||
min = ReaderPreferences.WEBTOON_PADDING_MIN,
|
||||
max = ReaderPreferences.WEBTOON_PADDING_MAX,
|
||||
onValueChanged = {
|
||||
webtoonSidePaddingPref.set(it)
|
||||
true
|
||||
},
|
||||
),
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = readerPreferences.readerHideThreshold(),
|
||||
title = stringResource(MR.strings.pref_hide_threshold),
|
||||
preference = readerPreferences.readerHideThreshold(),
|
||||
entries = persistentMapOf(
|
||||
ReaderPreferences.ReaderHideThreshold.HIGHEST to stringResource(MR.strings.pref_highest),
|
||||
ReaderPreferences.ReaderHideThreshold.HIGH to stringResource(MR.strings.pref_high),
|
||||
ReaderPreferences.ReaderHideThreshold.LOW to stringResource(MR.strings.pref_low),
|
||||
ReaderPreferences.ReaderHideThreshold.LOWEST to stringResource(MR.strings.pref_lowest),
|
||||
),
|
||||
title = stringResource(MR.strings.pref_hide_threshold),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readerPreferences.cropBordersWebtoon(),
|
||||
preference = readerPreferences.cropBordersWebtoon(),
|
||||
title = stringResource(MR.strings.pref_crop_borders),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = dualPageSplitPref,
|
||||
preference = dualPageSplitPref,
|
||||
title = stringResource(MR.strings.pref_dual_page_split),
|
||||
onValueChanged = {
|
||||
rotateToFitPref.set(false)
|
||||
@@ -372,13 +371,13 @@ object SettingsReaderScreen : SearchableSettings {
|
||||
},
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readerPreferences.dualPageInvertWebtoon(),
|
||||
preference = readerPreferences.dualPageInvertWebtoon(),
|
||||
title = stringResource(MR.strings.pref_dual_page_invert),
|
||||
subtitle = stringResource(MR.strings.pref_dual_page_invert_summary),
|
||||
enabled = dualPageSplit,
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = rotateToFitPref,
|
||||
preference = rotateToFitPref,
|
||||
title = stringResource(MR.strings.pref_page_rotate),
|
||||
onValueChanged = {
|
||||
dualPageSplitPref.set(false)
|
||||
@@ -386,16 +385,16 @@ object SettingsReaderScreen : SearchableSettings {
|
||||
},
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readerPreferences.dualPageRotateToFitInvertWebtoon(),
|
||||
preference = readerPreferences.dualPageRotateToFitInvertWebtoon(),
|
||||
title = stringResource(MR.strings.pref_page_rotate_invert),
|
||||
enabled = rotateToFit,
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readerPreferences.webtoonDoubleTapZoomEnabled(),
|
||||
preference = readerPreferences.webtoonDoubleTapZoomEnabled(),
|
||||
title = stringResource(MR.strings.pref_double_tap_zoom),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readerPreferences.webtoonDisableZoomOut(),
|
||||
preference = readerPreferences.webtoonDisableZoomOut(),
|
||||
title = stringResource(MR.strings.pref_webtoon_disable_zoom_out),
|
||||
),
|
||||
),
|
||||
@@ -410,11 +409,11 @@ object SettingsReaderScreen : SearchableSettings {
|
||||
title = stringResource(MR.strings.pref_reader_navigation),
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readWithVolumeKeysPref,
|
||||
preference = readWithVolumeKeysPref,
|
||||
title = stringResource(MR.strings.pref_read_with_volume_keys),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readerPreferences.readWithVolumeKeysInverted(),
|
||||
preference = readerPreferences.readWithVolumeKeysInverted(),
|
||||
title = stringResource(MR.strings.pref_read_with_volume_keys_inverted),
|
||||
enabled = readWithVolumeKeys,
|
||||
),
|
||||
@@ -428,11 +427,11 @@ object SettingsReaderScreen : SearchableSettings {
|
||||
title = stringResource(MR.strings.pref_reader_actions),
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readerPreferences.readWithLongTap(),
|
||||
preference = readerPreferences.readWithLongTap(),
|
||||
title = stringResource(MR.strings.pref_read_with_long_tap),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readerPreferences.folderPerManga(),
|
||||
preference = readerPreferences.folderPerManga(),
|
||||
title = stringResource(MR.strings.pref_create_folder_per_manga),
|
||||
subtitle = stringResource(MR.strings.pref_create_folder_per_manga_summary),
|
||||
),
|
||||
|
@@ -11,6 +11,7 @@ import eu.kanade.tachiyomi.core.security.PrivacyPreferences
|
||||
import eu.kanade.tachiyomi.core.security.SecurityPreferences
|
||||
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil.authenticate
|
||||
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil.isAuthenticationSupported
|
||||
import eu.kanade.tachiyomi.util.system.telemetryIncluded
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableMap
|
||||
import tachiyomi.core.common.i18n.stringResource
|
||||
@@ -31,10 +32,11 @@ object SettingsSecurityScreen : SearchableSettings {
|
||||
override fun getPreferences(): List<Preference> {
|
||||
val securityPreferences = remember { Injekt.get<SecurityPreferences>() }
|
||||
val privacyPreferences = remember { Injekt.get<PrivacyPreferences>() }
|
||||
return listOf(
|
||||
getSecurityGroup(securityPreferences),
|
||||
getFirebaseGroup(privacyPreferences),
|
||||
)
|
||||
return buildList(2) {
|
||||
add(getSecurityGroup(securityPreferences))
|
||||
if (!telemetryIncluded) return@buildList
|
||||
add(getFirebaseGroup(privacyPreferences))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -50,7 +52,7 @@ object SettingsSecurityScreen : SearchableSettings {
|
||||
title = stringResource(MR.strings.pref_security),
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = useAuthPref,
|
||||
preference = useAuthPref,
|
||||
title = stringResource(MR.strings.lock_with_biometrics),
|
||||
enabled = authSupported,
|
||||
onValueChanged = {
|
||||
@@ -60,9 +62,7 @@ object SettingsSecurityScreen : SearchableSettings {
|
||||
},
|
||||
),
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = securityPreferences.lockAppAfter(),
|
||||
title = stringResource(MR.strings.lock_when_idle),
|
||||
enabled = authSupported && useAuth,
|
||||
preference = securityPreferences.lockAppAfter(),
|
||||
entries = LockAfterValues
|
||||
.associateWith {
|
||||
when (it) {
|
||||
@@ -72,6 +72,8 @@ object SettingsSecurityScreen : SearchableSettings {
|
||||
}
|
||||
}
|
||||
.toImmutableMap(),
|
||||
title = stringResource(MR.strings.lock_when_idle),
|
||||
enabled = authSupported && useAuth,
|
||||
onValueChanged = {
|
||||
(context as FragmentActivity).authenticate(
|
||||
title = context.stringResource(MR.strings.lock_when_idle),
|
||||
@@ -80,15 +82,15 @@ object SettingsSecurityScreen : SearchableSettings {
|
||||
),
|
||||
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = securityPreferences.hideNotificationContent(),
|
||||
preference = securityPreferences.hideNotificationContent(),
|
||||
title = stringResource(MR.strings.hide_notification_content),
|
||||
),
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = securityPreferences.secureScreen(),
|
||||
title = stringResource(MR.strings.secure_screen),
|
||||
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)),
|
||||
),
|
||||
@@ -103,12 +105,12 @@ object SettingsSecurityScreen : SearchableSettings {
|
||||
title = stringResource(MR.strings.pref_firebase),
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = privacyPreferences.crashlytics(),
|
||||
preference = privacyPreferences.crashlytics(),
|
||||
title = stringResource(MR.strings.onboarding_permission_crashlytics),
|
||||
subtitle = stringResource(MR.strings.onboarding_permission_crashlytics_description),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = privacyPreferences.analytics(),
|
||||
preference = privacyPreferences.analytics(),
|
||||
title = stringResource(MR.strings.onboarding_permission_analytics),
|
||||
subtitle = stringResource(MR.strings.onboarding_permission_analytics_description),
|
||||
),
|
||||
|
@@ -30,8 +30,11 @@ import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.autofill.ContentType
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.semantics.contentType
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
@@ -125,51 +128,45 @@ object SettingsTrackingScreen : SearchableSettings {
|
||||
|
||||
return listOf(
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = trackPreferences.autoUpdateTrack(),
|
||||
preference = trackPreferences.autoUpdateTrack(),
|
||||
title = stringResource(MR.strings.pref_auto_update_manga_sync),
|
||||
),
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = trackPreferences.autoUpdateTrackOnMarkRead(),
|
||||
title = stringResource(MR.strings.pref_auto_update_manga_on_mark_read),
|
||||
preference = trackPreferences.autoUpdateTrackOnMarkRead(),
|
||||
entries = AutoTrackState.entries
|
||||
.associateWith { stringResource(it.titleRes) }
|
||||
.toPersistentMap(),
|
||||
title = stringResource(MR.strings.pref_auto_update_manga_on_mark_read),
|
||||
),
|
||||
Preference.PreferenceGroup(
|
||||
title = stringResource(MR.strings.services),
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.TrackerPreference(
|
||||
title = trackerManager.myAnimeList.name,
|
||||
tracker = trackerManager.myAnimeList,
|
||||
login = { context.openInBrowser(MyAnimeListApi.authUrl(), forceDefaultBrowser = true) },
|
||||
logout = { dialog = LogoutDialog(trackerManager.myAnimeList) },
|
||||
),
|
||||
Preference.PreferenceItem.TrackerPreference(
|
||||
title = trackerManager.aniList.name,
|
||||
tracker = trackerManager.aniList,
|
||||
login = { context.openInBrowser(AnilistApi.authUrl(), forceDefaultBrowser = true) },
|
||||
logout = { dialog = LogoutDialog(trackerManager.aniList) },
|
||||
),
|
||||
Preference.PreferenceItem.TrackerPreference(
|
||||
title = trackerManager.kitsu.name,
|
||||
tracker = trackerManager.kitsu,
|
||||
login = { dialog = LoginDialog(trackerManager.kitsu, MR.strings.email) },
|
||||
logout = { dialog = LogoutDialog(trackerManager.kitsu) },
|
||||
),
|
||||
Preference.PreferenceItem.TrackerPreference(
|
||||
title = trackerManager.mangaUpdates.name,
|
||||
tracker = trackerManager.mangaUpdates,
|
||||
login = { dialog = LoginDialog(trackerManager.mangaUpdates, MR.strings.username) },
|
||||
logout = { dialog = LogoutDialog(trackerManager.mangaUpdates) },
|
||||
),
|
||||
Preference.PreferenceItem.TrackerPreference(
|
||||
title = trackerManager.shikimori.name,
|
||||
tracker = trackerManager.shikimori,
|
||||
login = { context.openInBrowser(ShikimoriApi.authUrl(), forceDefaultBrowser = true) },
|
||||
logout = { dialog = LogoutDialog(trackerManager.shikimori) },
|
||||
),
|
||||
Preference.PreferenceItem.TrackerPreference(
|
||||
title = trackerManager.bangumi.name,
|
||||
tracker = trackerManager.bangumi,
|
||||
login = { context.openInBrowser(BangumiApi.authUrl(), forceDefaultBrowser = true) },
|
||||
logout = { dialog = LogoutDialog(trackerManager.bangumi) },
|
||||
@@ -183,7 +180,6 @@ object SettingsTrackingScreen : SearchableSettings {
|
||||
enhancedTrackers.first
|
||||
.map { service ->
|
||||
Preference.PreferenceItem.TrackerPreference(
|
||||
title = service.name,
|
||||
tracker = service,
|
||||
login = { (service as EnhancedTracker).loginNoop() },
|
||||
logout = service::logout,
|
||||
@@ -227,7 +223,9 @@ object SettingsTrackingScreen : SearchableSettings {
|
||||
text = {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
OutlinedTextField(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.semantics { contentType = ContentType.Username + ContentType.EmailAddress },
|
||||
value = username,
|
||||
onValueChange = { username = it },
|
||||
label = { Text(text = stringResource(uNameStringRes)) },
|
||||
@@ -238,7 +236,9 @@ object SettingsTrackingScreen : SearchableSettings {
|
||||
|
||||
var hidePassword by remember { mutableStateOf(true) }
|
||||
OutlinedTextField(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.semantics { contentType = ContentType.Password },
|
||||
value = password,
|
||||
onValueChange = { password = it },
|
||||
label = { Text(text = stringResource(MR.strings.password)) },
|
||||
@@ -287,7 +287,7 @@ object SettingsTrackingScreen : SearchableSettings {
|
||||
}
|
||||
},
|
||||
) {
|
||||
val id = if (processing) MR.strings.loading else MR.strings.login
|
||||
val id = if (processing) MR.strings.logging_in else MR.strings.login
|
||||
Text(text = stringResource(id))
|
||||
}
|
||||
},
|
||||
|
@@ -35,7 +35,9 @@ import eu.kanade.tachiyomi.ui.more.NewUpdateScreen
|
||||
import eu.kanade.tachiyomi.util.CrashLogUtil
|
||||
import eu.kanade.tachiyomi.util.lang.toDateTimestampString
|
||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||
import eu.kanade.tachiyomi.util.system.isPreviewBuildType
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import eu.kanade.tachiyomi.util.system.updaterEnabled
|
||||
import kotlinx.coroutines.launch
|
||||
import logcat.LogPriority
|
||||
import tachiyomi.core.common.util.lang.withIOContext
|
||||
@@ -97,7 +99,7 @@ object AboutScreen : Screen() {
|
||||
)
|
||||
}
|
||||
|
||||
if (BuildConfig.INCLUDE_UPDATER) {
|
||||
if (updaterEnabled) {
|
||||
item {
|
||||
TextPreferenceWidget(
|
||||
title = stringResource(MR.strings.check_for_updates),
|
||||
@@ -121,7 +123,7 @@ object AboutScreen : Screen() {
|
||||
versionName = result.release.version,
|
||||
changelogInfo = result.release.info,
|
||||
releaseLink = result.release.releaseLink,
|
||||
downloadLink = result.release.getDownloadLink(),
|
||||
downloadLink = result.release.downloadLink,
|
||||
)
|
||||
navigator.push(updateScreen)
|
||||
},
|
||||
@@ -245,7 +247,7 @@ object AboutScreen : Screen() {
|
||||
}
|
||||
}
|
||||
}
|
||||
BuildConfig.PREVIEW -> {
|
||||
isPreviewBuildType -> {
|
||||
"Beta r${BuildConfig.COMMIT_COUNT}".let {
|
||||
if (withBuildDate) {
|
||||
"$it (${BuildConfig.COMMIT_SHA}, ${getFormattedBuildTime()})"
|
||||
|
@@ -6,7 +6,7 @@ import androidx.compose.ui.Modifier
|
||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
import com.mikepenz.aboutlibraries.ui.compose.m3.LibrariesContainer
|
||||
import com.mikepenz.aboutlibraries.ui.compose.m3.util.htmlReadyLicenseContent
|
||||
import com.mikepenz.aboutlibraries.ui.compose.util.htmlReadyLicenseContent
|
||||
import eu.kanade.presentation.components.AppBar
|
||||
import eu.kanade.presentation.util.Screen
|
||||
import tachiyomi.i18n.MR
|
||||
|
@@ -1,8 +1,10 @@
|
||||
package eu.kanade.presentation.more.settings.screen.advanced
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.items
|
||||
@@ -12,13 +14,17 @@ import androidx.compose.material.icons.outlined.SelectAll
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
@@ -39,6 +45,7 @@ import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.update
|
||||
import tachiyomi.core.common.util.lang.launchIO
|
||||
import tachiyomi.core.common.util.lang.launchUI
|
||||
import tachiyomi.core.common.util.lang.toLong
|
||||
import tachiyomi.core.common.util.lang.withNonCancellableContext
|
||||
import tachiyomi.data.Database
|
||||
import tachiyomi.domain.source.interactor.GetSourcesWithNonLibraryManga
|
||||
@@ -47,6 +54,7 @@ import tachiyomi.domain.source.model.SourceWithCount
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.LazyColumnWithAction
|
||||
import tachiyomi.presentation.core.components.material.Scaffold
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import tachiyomi.presentation.core.screens.EmptyScreen
|
||||
import tachiyomi.presentation.core.screens.LoadingScreen
|
||||
@@ -68,13 +76,45 @@ class ClearDatabaseScreen : Screen() {
|
||||
is ClearDatabaseScreenModel.State.Loading -> LoadingScreen()
|
||||
is ClearDatabaseScreenModel.State.Ready -> {
|
||||
if (s.showConfirmation) {
|
||||
var keepReadManga by remember { mutableStateOf(true) }
|
||||
AlertDialog(
|
||||
title = {
|
||||
Text(text = stringResource(MR.strings.are_you_sure))
|
||||
},
|
||||
text = {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
||||
) {
|
||||
Text(text = stringResource(MR.strings.clear_database_text))
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(MR.strings.clear_db_exclude_read),
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
Switch(
|
||||
checked = keepReadManga,
|
||||
onCheckedChange = { keepReadManga = it },
|
||||
)
|
||||
}
|
||||
if (!keepReadManga) {
|
||||
Text(
|
||||
text = stringResource(MR.strings.clear_database_history_warning),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
onDismissRequest = model::hideConfirmation,
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
scope.launchUI {
|
||||
model.removeMangaBySourceId()
|
||||
model.removeMangaBySourceId(keepReadManga)
|
||||
model.clearSelection()
|
||||
model.hideConfirmation()
|
||||
context.toast(MR.strings.clear_database_completed)
|
||||
@@ -89,9 +129,6 @@ class ClearDatabaseScreen : Screen() {
|
||||
Text(text = stringResource(MR.strings.action_cancel))
|
||||
}
|
||||
},
|
||||
text = {
|
||||
Text(text = stringResource(MR.strings.clear_database_confirmation))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -203,9 +240,9 @@ private class ClearDatabaseScreenModel : StateScreenModel<ClearDatabaseScreenMod
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun removeMangaBySourceId() = withNonCancellableContext {
|
||||
suspend fun removeMangaBySourceId(keepReadManga: Boolean) = withNonCancellableContext {
|
||||
val state = state.value as? State.Ready ?: return@withNonCancellableContext
|
||||
database.mangasQueries.deleteMangasNotInLibraryBySourceIds(state.selection)
|
||||
database.mangasQueries.deleteNonLibraryManga(state.selection, keepReadManga.toLong())
|
||||
database.historyQueries.removeResettedHistory()
|
||||
}
|
||||
|
||||
|
@@ -25,7 +25,6 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.composed
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -86,7 +85,8 @@ internal fun BasePreferenceWidget(
|
||||
}
|
||||
}
|
||||
|
||||
internal fun Modifier.highlightBackground(highlighted: Boolean): Modifier = composed {
|
||||
@Composable
|
||||
internal fun Modifier.highlightBackground(highlighted: Boolean): Modifier {
|
||||
var highlightFlag by remember { mutableStateOf(false) }
|
||||
LaunchedEffect(Unit) {
|
||||
if (highlighted) {
|
||||
@@ -116,7 +116,7 @@ internal fun Modifier.highlightBackground(highlighted: Boolean): Modifier = comp
|
||||
},
|
||||
label = "highlight",
|
||||
)
|
||||
Modifier.background(color = highlight)
|
||||
return this.background(color = highlight)
|
||||
}
|
||||
|
||||
internal val TrailingWidgetBuffer = 16.dp
|
||||
|
@@ -2,6 +2,7 @@ package eu.kanade.presentation.reader.settings
|
||||
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.material3.FilterChip
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -36,12 +37,12 @@ internal fun ColumnScope.ColorFilterPage(screenModel: ReaderSettingsScreenModel)
|
||||
if (customBrightness) {
|
||||
val customBrightnessValue by screenModel.preferences.customBrightnessValue().collectAsState()
|
||||
SliderItem(
|
||||
label = stringResource(MR.strings.pref_custom_brightness),
|
||||
min = -75,
|
||||
max = 100,
|
||||
value = customBrightnessValue,
|
||||
valueText = customBrightnessValue.toString(),
|
||||
valueRange = -75..100,
|
||||
steps = 0,
|
||||
label = stringResource(MR.strings.pref_custom_brightness),
|
||||
onChange = { screenModel.preferences.customBrightnessValue().set(it) },
|
||||
pillColor = MaterialTheme.colorScheme.surfaceContainerHighest,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -53,48 +54,52 @@ internal fun ColumnScope.ColorFilterPage(screenModel: ReaderSettingsScreenModel)
|
||||
if (colorFilter) {
|
||||
val colorFilterValue by screenModel.preferences.colorFilterValue().collectAsState()
|
||||
SliderItem(
|
||||
label = stringResource(MR.strings.color_filter_r_value),
|
||||
max = 255,
|
||||
value = colorFilterValue.red,
|
||||
valueText = colorFilterValue.red.toString(),
|
||||
valueRange = 0..255,
|
||||
steps = 0,
|
||||
label = stringResource(MR.strings.color_filter_r_value),
|
||||
onChange = { newRValue ->
|
||||
screenModel.preferences.colorFilterValue().getAndSet {
|
||||
getColorValue(it, newRValue, RED_MASK, 16)
|
||||
}
|
||||
},
|
||||
pillColor = MaterialTheme.colorScheme.surfaceContainerHighest,
|
||||
)
|
||||
SliderItem(
|
||||
label = stringResource(MR.strings.color_filter_g_value),
|
||||
max = 255,
|
||||
value = colorFilterValue.green,
|
||||
valueText = colorFilterValue.green.toString(),
|
||||
valueRange = 0..255,
|
||||
steps = 0,
|
||||
label = stringResource(MR.strings.color_filter_g_value),
|
||||
onChange = { newGValue ->
|
||||
screenModel.preferences.colorFilterValue().getAndSet {
|
||||
getColorValue(it, newGValue, GREEN_MASK, 8)
|
||||
}
|
||||
},
|
||||
pillColor = MaterialTheme.colorScheme.surfaceContainerHighest,
|
||||
)
|
||||
SliderItem(
|
||||
label = stringResource(MR.strings.color_filter_b_value),
|
||||
max = 255,
|
||||
value = colorFilterValue.blue,
|
||||
valueText = colorFilterValue.blue.toString(),
|
||||
valueRange = 0..255,
|
||||
steps = 0,
|
||||
label = stringResource(MR.strings.color_filter_b_value),
|
||||
onChange = { newBValue ->
|
||||
screenModel.preferences.colorFilterValue().getAndSet {
|
||||
getColorValue(it, newBValue, BLUE_MASK, 0)
|
||||
}
|
||||
},
|
||||
pillColor = MaterialTheme.colorScheme.surfaceContainerHighest,
|
||||
)
|
||||
SliderItem(
|
||||
label = stringResource(MR.strings.color_filter_a_value),
|
||||
max = 255,
|
||||
value = colorFilterValue.alpha,
|
||||
valueText = colorFilterValue.alpha.toString(),
|
||||
valueRange = 0..255,
|
||||
steps = 0,
|
||||
label = stringResource(MR.strings.color_filter_a_value),
|
||||
onChange = { newAValue ->
|
||||
screenModel.preferences.colorFilterValue().getAndSet {
|
||||
getColorValue(it, newAValue, ALPHA_MASK, 24)
|
||||
}
|
||||
},
|
||||
pillColor = MaterialTheme.colorScheme.surfaceContainerHighest,
|
||||
)
|
||||
|
||||
val colorFilterMode by screenModel.preferences.colorFilterMode().collectAsState()
|
||||
|
@@ -2,6 +2,7 @@ package eu.kanade.presentation.reader.settings
|
||||
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.material3.FilterChip
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -97,21 +98,21 @@ internal fun ColumnScope.GeneralPage(screenModel: ReaderSettingsScreenModel) {
|
||||
if (flashPageState) {
|
||||
SliderItem(
|
||||
value = flashMillis / ReaderPreferences.MILLI_CONVERSION,
|
||||
valueRange = 1..15,
|
||||
label = stringResource(MR.strings.pref_flash_duration),
|
||||
valueText = stringResource(MR.strings.pref_flash_duration_summary, flashMillis),
|
||||
onChange = { flashMillisPref.set(it * ReaderPreferences.MILLI_CONVERSION) },
|
||||
min = 1,
|
||||
max = 15,
|
||||
pillColor = MaterialTheme.colorScheme.surfaceContainerHighest,
|
||||
)
|
||||
SliderItem(
|
||||
value = flashInterval,
|
||||
valueRange = 1..10,
|
||||
label = stringResource(MR.strings.pref_flash_page_interval),
|
||||
valueText = pluralStringResource(MR.plurals.pref_pages, flashInterval, flashInterval),
|
||||
onChange = {
|
||||
flashIntervalPref.set(it)
|
||||
},
|
||||
min = 1,
|
||||
max = 10,
|
||||
pillColor = MaterialTheme.colorScheme.surfaceContainerHighest,
|
||||
)
|
||||
SettingsChipRow(MR.strings.pref_flash_with) {
|
||||
flashColors.map { (labelRes, value) ->
|
||||
|
@@ -2,6 +2,7 @@ package eu.kanade.presentation.reader.settings
|
||||
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.material3.FilterChip
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
@@ -152,14 +153,14 @@ private fun ColumnScope.WebtoonViewerSettings(screenModel: ReaderSettingsScreenM
|
||||
|
||||
val webtoonSidePadding by screenModel.preferences.webtoonSidePadding().collectAsState()
|
||||
SliderItem(
|
||||
label = stringResource(MR.strings.pref_webtoon_side_padding),
|
||||
min = ReaderPreferences.WEBTOON_PADDING_MIN,
|
||||
max = ReaderPreferences.WEBTOON_PADDING_MAX,
|
||||
value = webtoonSidePadding,
|
||||
valueRange = ReaderPreferences.let { it.WEBTOON_PADDING_MIN..it.WEBTOON_PADDING_MAX },
|
||||
label = stringResource(MR.strings.pref_webtoon_side_padding),
|
||||
valueText = numberFormat.format(webtoonSidePadding / 100f),
|
||||
onChange = {
|
||||
screenModel.preferences.webtoonSidePadding().set(it)
|
||||
},
|
||||
pillColor = MaterialTheme.colorScheme.surfaceContainerHighest,
|
||||
)
|
||||
|
||||
CheckboxItem(
|
||||
|
@@ -9,10 +9,12 @@ import androidx.compose.ui.platform.LocalContext
|
||||
import eu.kanade.domain.ui.UiPreferences
|
||||
import eu.kanade.domain.ui.model.AppTheme
|
||||
import eu.kanade.presentation.theme.colorscheme.BaseColorScheme
|
||||
import eu.kanade.presentation.theme.colorscheme.CatppuccinColorScheme
|
||||
import eu.kanade.presentation.theme.colorscheme.GreenAppleColorScheme
|
||||
import eu.kanade.presentation.theme.colorscheme.LavenderColorScheme
|
||||
import eu.kanade.presentation.theme.colorscheme.MidnightDuskColorScheme
|
||||
import eu.kanade.presentation.theme.colorscheme.MonetColorScheme
|
||||
import eu.kanade.presentation.theme.colorscheme.MonochromeColorScheme
|
||||
import eu.kanade.presentation.theme.colorscheme.NordColorScheme
|
||||
import eu.kanade.presentation.theme.colorscheme.StrawberryColorScheme
|
||||
import eu.kanade.presentation.theme.colorscheme.TachiyomiColorScheme
|
||||
@@ -76,9 +78,11 @@ private fun getThemeColorScheme(
|
||||
|
||||
private val colorSchemes: Map<AppTheme, BaseColorScheme> = mapOf(
|
||||
AppTheme.DEFAULT to TachiyomiColorScheme,
|
||||
AppTheme.CATPPUCCIN to CatppuccinColorScheme,
|
||||
AppTheme.GREEN_APPLE to GreenAppleColorScheme,
|
||||
AppTheme.LAVENDER to LavenderColorScheme,
|
||||
AppTheme.MIDNIGHT_DUSK to MidnightDuskColorScheme,
|
||||
AppTheme.MONOCHROME to MonochromeColorScheme,
|
||||
AppTheme.NORD to NordColorScheme,
|
||||
AppTheme.STRAWBERRY_DAIQUIRI to StrawberryColorScheme,
|
||||
AppTheme.TAKO to TakoColorScheme,
|
||||
|
@@ -0,0 +1,103 @@
|
||||
package eu.kanade.presentation.theme.colorscheme
|
||||
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
/**
|
||||
* Colors for Catppuccin theme
|
||||
* MIT License
|
||||
* Copyright (c) 2021 Catppuccin
|
||||
* https://catppuccin.com
|
||||
* M3 colors generated by Material Theme Builder (https://goo.gle/material-theme-builder-web)
|
||||
*
|
||||
* Key colors (dark):
|
||||
* Primary #CBA6F4
|
||||
* Secondary #CBA6F4
|
||||
* Tertiary #CBA6F4
|
||||
* Neutral #181825
|
||||
|
||||
* Key colors (light):
|
||||
* Primary #8839EF
|
||||
* Secondary #8839EF
|
||||
* Tertiary #8839EF
|
||||
* Neutral #E6E9EF
|
||||
*/
|
||||
internal object CatppuccinColorScheme : BaseColorScheme() {
|
||||
|
||||
override val darkScheme = darkColorScheme(
|
||||
primary = Color(0xFFCBA6F7),
|
||||
onPrimary = Color(0xFF11111B),
|
||||
primaryContainer = Color(0xFFCBA6F7),
|
||||
onPrimaryContainer = Color(0xFF11111B),
|
||||
secondary = Color(0xFFCBA6F7), // Unread badge
|
||||
onSecondary = Color(0xFF11111B), // Unread badge text
|
||||
secondaryContainer = Color(0xFF313244), // Navigation bar selector pill & progress indicator (remaining)
|
||||
onSecondaryContainer = Color(0xFFCBA6F7), // Navigation bar selector icon
|
||||
tertiary = Color(0xFFCBA6F7), // Volume and brightness bars, Downloaded badge
|
||||
onTertiary = Color(0xFF11111B), // Downloaded badge text
|
||||
tertiaryContainer = Color(0xFF1E1E2E),
|
||||
onTertiaryContainer = Color(0xFFCDD6F4),
|
||||
error = Color(0xFFF38BA8),
|
||||
onError = Color(0xFF11111B),
|
||||
errorContainer = Color(0xFFFF0558),
|
||||
onErrorContainer = Color(0xFFEF9FB4),
|
||||
background = Color(0xFF181825),
|
||||
onBackground = Color(0xFFCDD6F4),
|
||||
surface = Color(0xFF181825),
|
||||
onSurface = Color(0xFFCDD6F4),
|
||||
surfaceVariant = Color(0xFF1E1E2E), // Navigation bar background (ThemePrefWidget)
|
||||
onSurfaceVariant = Color(0xFFCDD6F4), // Button (unselected)
|
||||
outline = Color(0xFFCBA6F7),
|
||||
outlineVariant = Color(0xFF585B70), // Outlines for buttons
|
||||
scrim = Color(0xFF11111B),
|
||||
inverseSurface = Color(0xFFEFF1F5), // Snackbar or whatever they called
|
||||
inverseOnSurface = Color(0xFF4C4F69), // Snackbar text
|
||||
inversePrimary = Color(0xFF8839EF), // Snackbar accent
|
||||
surfaceDim = Color(0xFF181825),
|
||||
surfaceBright = Color(0xFF313244),
|
||||
surfaceContainerLowest = Color(0xFF181825),
|
||||
surfaceContainerLow = Color(0xFF1E1E2E), // Repo cards
|
||||
surfaceContainer = Color(0xFF1E1E2E),
|
||||
surfaceContainerHigh = Color(0xFF1E1E2E), // Filter menu
|
||||
surfaceContainerHighest = Color(0xFF313244), // Untoggleg button bg
|
||||
)
|
||||
|
||||
override val lightScheme = lightColorScheme(
|
||||
primary = Color(0xFF8839EF),
|
||||
onPrimary = Color(0xFFDCE0E8),
|
||||
primaryContainer = Color(0xFF8839EF),
|
||||
onPrimaryContainer = Color(0xFFDCE0E8),
|
||||
secondary = Color(0xFF8839EF), // Unread badge
|
||||
onSecondary = Color(0xFFDCE0E8), // Unread badge text
|
||||
secondaryContainer = Color(0xFFCDD0DA), // Navigation bar selector pill & progress indicator (remaining)
|
||||
onSecondaryContainer = Color(0xFF8839EF), // Navigation bar selector icon
|
||||
tertiary = Color(0xFF8839EF), // Volume and brightness bars, Downloaded badge
|
||||
onTertiary = Color(0xFFDCE0E8), // Downloaded badge text
|
||||
tertiaryContainer = Color(0xFFEFF1F5),
|
||||
onTertiaryContainer = Color(0xFF4C4F69),
|
||||
error = Color(0xFFD20F39),
|
||||
onError = Color(0xFFDCE0E8),
|
||||
errorContainer = Color(0xFF68001C),
|
||||
onErrorContainer = Color(0xFFD61C41),
|
||||
background = Color(0xFFE6E9EF),
|
||||
onBackground = Color(0xFF4C4F69),
|
||||
surface = Color(0xFFE6E9EF),
|
||||
onSurface = Color(0xFF4C4F69),
|
||||
surfaceVariant = Color(0xFFEFF1F5), // Navigation bar background (ThemePrefWidget)
|
||||
onSurfaceVariant = Color(0xFF4C4F69), // Button (unselected)
|
||||
outline = Color(0xFF8839EF),
|
||||
outlineVariant = Color(0xFFACB0BE), // Outlines for buttons
|
||||
scrim = Color(0xFFDCE0E8),
|
||||
inverseSurface = Color(0xFF1E1E2E), // Snackbar
|
||||
inverseOnSurface = Color(0xFFCDD6F4), // Snackbar text
|
||||
inversePrimary = Color(0xFFCBA6F7), // Snackbar accent
|
||||
surfaceDim = Color(0xFFE6E9EF),
|
||||
surfaceBright = Color(0xFFCDD0DA),
|
||||
surfaceContainerLowest = Color(0xFFE6E9EF),
|
||||
surfaceContainerLow = Color(0xFFEFF1F5), // Repo cards
|
||||
surfaceContainer = Color(0xFFEFF1F5), // Navigation bar background
|
||||
surfaceContainerHigh = Color(0xFFEFF1F5), // Filter menu
|
||||
surfaceContainerHighest = Color(0xFFCDD0DA), // Untoggleg bg
|
||||
)
|
||||
}
|
@@ -0,0 +1,84 @@
|
||||
package eu.kanade.presentation.theme.colorscheme
|
||||
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
internal object MonochromeColorScheme : BaseColorScheme() {
|
||||
|
||||
override val darkScheme = darkColorScheme(
|
||||
primary = Color(0xFFFFFFFF),
|
||||
onPrimary = Color(0xFF000000),
|
||||
primaryContainer = Color(0xFFFFFFFF),
|
||||
onPrimaryContainer = Color(0xFF000000),
|
||||
secondary = Color(0xFFFFFFFF),
|
||||
onSecondary = Color(0xFF000000),
|
||||
secondaryContainer = Color(0xFF777777),
|
||||
onSecondaryContainer = Color(0xFF000000),
|
||||
tertiary = Color(0xFF777777),
|
||||
onTertiary = Color(0xFFFFFFFF),
|
||||
tertiaryContainer = Color(0xFFFFFFFF),
|
||||
onTertiaryContainer = Color(0xFF000000),
|
||||
error = Color(0xFFFFFFFF),
|
||||
onError = Color(0xFF000000),
|
||||
errorContainer = Color(0xFFFFFFFF),
|
||||
onErrorContainer = Color(0xFF000000),
|
||||
background = Color(0xFF000000),
|
||||
onBackground = Color(0xFFFFFFFF),
|
||||
surface = Color(0xFF000000),
|
||||
onSurface = Color(0xFFFFFFFF),
|
||||
surfaceVariant = Color(0xFF000000),
|
||||
onSurfaceVariant = Color(0xFFFFFFFF),
|
||||
outline = Color(0xFFFFFFFF),
|
||||
outlineVariant = Color(0xFFFFFFFF),
|
||||
scrim = Color(0xFF000000),
|
||||
inverseSurface = Color(0xFFFFFFFF),
|
||||
inverseOnSurface = Color(0xFF000000),
|
||||
inversePrimary = Color(0xFF000000),
|
||||
surfaceDim = Color(0xFF000000),
|
||||
surfaceBright = Color(0xFFFFFFFF),
|
||||
surfaceContainerLowest = Color(0xFF000000),
|
||||
surfaceContainerLow = Color(0xFF000000),
|
||||
surfaceContainer = Color(0xFF000000),
|
||||
surfaceContainerHigh = Color(0xFF000000),
|
||||
surfaceContainerHighest = Color(0xFF000000),
|
||||
)
|
||||
|
||||
override val lightScheme = lightColorScheme(
|
||||
primary = Color(0xFF000000),
|
||||
onPrimary = Color(0xFFFFFFFF),
|
||||
primaryContainer = Color(0xFF000000),
|
||||
onPrimaryContainer = Color(0xFFFFFFFF),
|
||||
secondary = Color(0xFF000000),
|
||||
onSecondary = Color(0xFFFFFFFF),
|
||||
secondaryContainer = Color(0xFF888888),
|
||||
onSecondaryContainer = Color(0xFFFFFFFF),
|
||||
tertiary = Color(0xFF888888),
|
||||
onTertiary = Color(0xFFFFFFFF),
|
||||
tertiaryContainer = Color(0xFF000000),
|
||||
onTertiaryContainer = Color(0xFFFFFFFF),
|
||||
error = Color(0xFF000000),
|
||||
onError = Color(0xFFFFFFFF),
|
||||
errorContainer = Color(0xFF000000),
|
||||
onErrorContainer = Color(0xFFFFFFFF),
|
||||
background = Color(0xFFFFFFFF),
|
||||
onBackground = Color(0xFF000000),
|
||||
surface = Color(0xFFFFFFFF),
|
||||
onSurface = Color(0xFF000000),
|
||||
surfaceVariant = Color(0xFFFFFFFF),
|
||||
onSurfaceVariant = Color(0xFF000000),
|
||||
outline = Color(0xFF000000),
|
||||
outlineVariant = Color(0xFF000000),
|
||||
scrim = Color(0xFF000000),
|
||||
inverseSurface = Color(0xFF000000),
|
||||
inverseOnSurface = Color(0xFFFFFFFF),
|
||||
inversePrimary = Color(0xFFFFFFFF),
|
||||
surfaceDim = Color(0xFFFFFFFF),
|
||||
surfaceBright = Color(0xFFFFFFFF),
|
||||
surfaceContainerLowest = Color(0xFFFFFFFF),
|
||||
surfaceContainerLow = Color(0xFFFFFFFF),
|
||||
surfaceContainer = Color(0xFFFFFFFF),
|
||||
surfaceContainerHigh = Color(0xFFFFFFFF),
|
||||
surfaceContainerHighest = Color(0xFFFFFFFF),
|
||||
)
|
||||
}
|
@@ -10,10 +10,12 @@ import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.absoluteOffset
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.systemBars
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.foundation.layout.wrapContentSize
|
||||
@@ -22,6 +24,9 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.MoreVert
|
||||
import androidx.compose.material.icons.filled.VisibilityOff
|
||||
import androidx.compose.material3.Badge
|
||||
import androidx.compose.material3.BadgedBox
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
@@ -70,6 +75,7 @@ fun TrackInfoDialogHome(
|
||||
onOpenInBrowser: (TrackItem) -> Unit,
|
||||
onRemoved: (TrackItem) -> Unit,
|
||||
onCopyLink: (TrackItem) -> Unit,
|
||||
onTogglePrivate: (TrackItem) -> Unit,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
@@ -84,6 +90,7 @@ fun TrackInfoDialogHome(
|
||||
if (item.track != null) {
|
||||
val supportsScoring = item.tracker.getScoreList().isNotEmpty()
|
||||
val supportsReadingDates = item.tracker.supportsReadingDates
|
||||
val supportsPrivate = item.tracker.supportsPrivateTracking
|
||||
TrackInfoItem(
|
||||
title = item.track.title,
|
||||
tracker = item.tracker,
|
||||
@@ -115,6 +122,9 @@ fun TrackInfoDialogHome(
|
||||
onOpenInBrowser = { onOpenInBrowser(item) },
|
||||
onRemoved = { onRemoved(item) },
|
||||
onCopyLink = { onCopyLink(item) },
|
||||
private = item.track.private,
|
||||
onTogglePrivate = { onTogglePrivate(item) }
|
||||
.takeIf { supportsPrivate },
|
||||
)
|
||||
} else {
|
||||
TrackInfoItemEmpty(
|
||||
@@ -144,17 +154,37 @@ private fun TrackInfoItem(
|
||||
onOpenInBrowser: () -> Unit,
|
||||
onRemoved: () -> Unit,
|
||||
onCopyLink: () -> Unit,
|
||||
private: Boolean,
|
||||
onTogglePrivate: (() -> Unit)?,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
Column {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
TrackLogoIcon(
|
||||
tracker = tracker,
|
||||
onClick = onOpenInBrowser,
|
||||
onLongClick = onCopyLink,
|
||||
)
|
||||
BadgedBox(
|
||||
badge = {
|
||||
if (private) {
|
||||
Badge(
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
contentColor = MaterialTheme.colorScheme.onPrimary,
|
||||
modifier = Modifier.absoluteOffset(x = (-5).dp),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.VisibilityOff,
|
||||
contentDescription = stringResource(MR.strings.tracked_privately),
|
||||
modifier = Modifier.size(14.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
) {
|
||||
TrackLogoIcon(
|
||||
tracker = tracker,
|
||||
onClick = onOpenInBrowser,
|
||||
onLongClick = onCopyLink,
|
||||
)
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.height(48.dp)
|
||||
@@ -181,6 +211,8 @@ private fun TrackInfoItem(
|
||||
onOpenInBrowser = onOpenInBrowser,
|
||||
onRemoved = onRemoved,
|
||||
onCopyLink = onCopyLink,
|
||||
private = private,
|
||||
onTogglePrivate = onTogglePrivate,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -291,6 +323,8 @@ private fun TrackInfoItemMenu(
|
||||
onOpenInBrowser: () -> Unit,
|
||||
onRemoved: () -> Unit,
|
||||
onCopyLink: () -> Unit,
|
||||
private: Boolean,
|
||||
onTogglePrivate: (() -> Unit)?,
|
||||
) {
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
Box(modifier = Modifier.wrapContentSize(Alignment.TopStart)) {
|
||||
@@ -318,6 +352,25 @@ private fun TrackInfoItemMenu(
|
||||
expanded = false
|
||||
},
|
||||
)
|
||||
if (onTogglePrivate != null) {
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(
|
||||
stringResource(
|
||||
if (private) {
|
||||
MR.strings.action_toggle_private_off
|
||||
} else {
|
||||
MR.strings.action_toggle_private_on
|
||||
},
|
||||
),
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
onTogglePrivate()
|
||||
expanded = false
|
||||
},
|
||||
)
|
||||
}
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(MR.strings.action_remove)) },
|
||||
onClick = {
|
||||
|
@@ -25,7 +25,9 @@ internal class TrackInfoDialogHomePreviewProvider :
|
||||
remoteUrl = "https://example.com",
|
||||
startDate = 0L,
|
||||
finishDate = 0L,
|
||||
private = false,
|
||||
)
|
||||
private val privateTrack = aTrack.copy(private = true)
|
||||
private val trackItemWithoutTrack = TrackItem(
|
||||
track = null,
|
||||
tracker = DummyTracker(
|
||||
@@ -40,6 +42,13 @@ internal class TrackInfoDialogHomePreviewProvider :
|
||||
name = "Example Tracker 2",
|
||||
),
|
||||
)
|
||||
private val trackItemWithPrivateTrack = TrackItem(
|
||||
track = privateTrack,
|
||||
tracker = DummyTracker(
|
||||
id = 2L,
|
||||
name = "Example Tracker 2",
|
||||
),
|
||||
)
|
||||
|
||||
private val trackersWithAndWithoutTrack = @Composable {
|
||||
TrackInfoDialogHome(
|
||||
@@ -57,6 +66,7 @@ internal class TrackInfoDialogHomePreviewProvider :
|
||||
onOpenInBrowser = {},
|
||||
onRemoved = {},
|
||||
onCopyLink = {},
|
||||
onTogglePrivate = {},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -73,6 +83,24 @@ internal class TrackInfoDialogHomePreviewProvider :
|
||||
onOpenInBrowser = {},
|
||||
onRemoved = {},
|
||||
onCopyLink = {},
|
||||
onTogglePrivate = {},
|
||||
)
|
||||
}
|
||||
|
||||
private val trackerWithPrivateTracking = @Composable {
|
||||
TrackInfoDialogHome(
|
||||
trackItems = listOf(trackItemWithPrivateTrack),
|
||||
dateFormat = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM),
|
||||
onStatusClick = {},
|
||||
onChapterClick = {},
|
||||
onScoreClick = {},
|
||||
onStartDateEdit = {},
|
||||
onEndDateEdit = {},
|
||||
onNewSearch = {},
|
||||
onOpenInBrowser = {},
|
||||
onRemoved = {},
|
||||
onCopyLink = {},
|
||||
onTogglePrivate = {},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -80,5 +108,6 @@ internal class TrackInfoDialogHomePreviewProvider :
|
||||
get() = sequenceOf(
|
||||
trackersWithAndWithoutTrack,
|
||||
noTrackers,
|
||||
trackerWithPrivateTracking,
|
||||
)
|
||||
}
|
||||
|
@@ -33,6 +33,7 @@ import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.ArrowBack
|
||||
import androidx.compose.material.icons.filled.CheckCircle
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.VisibilityOff
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
@@ -90,8 +91,9 @@ fun TrackerSearch(
|
||||
queryResult: Result<List<TrackSearch>>?,
|
||||
selected: TrackSearch?,
|
||||
onSelectedChange: (TrackSearch) -> Unit,
|
||||
onConfirmSelection: () -> Unit,
|
||||
onConfirmSelection: (private: Boolean) -> Unit,
|
||||
onDismissRequest: () -> Unit,
|
||||
supportsPrivateTracking: Boolean,
|
||||
) {
|
||||
val focusManager = LocalFocusManager.current
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
@@ -164,15 +166,31 @@ fun TrackerSearch(
|
||||
enter = fadeIn() + slideInVertically { it / 2 },
|
||||
exit = slideOutVertically { it / 2 } + fadeOut(),
|
||||
) {
|
||||
Button(
|
||||
onClick = { onConfirmSelection() },
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(12.dp)
|
||||
.padding(MaterialTheme.padding.small)
|
||||
.windowInsetsPadding(WindowInsets.navigationBars)
|
||||
.fillMaxWidth(),
|
||||
elevation = ButtonDefaults.elevatedButtonElevation(),
|
||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
||||
) {
|
||||
Text(text = stringResource(MR.strings.action_track))
|
||||
Button(
|
||||
onClick = { onConfirmSelection(false) },
|
||||
modifier = Modifier.weight(1f),
|
||||
elevation = ButtonDefaults.elevatedButtonElevation(),
|
||||
) {
|
||||
Text(text = stringResource(MR.strings.action_track))
|
||||
}
|
||||
if (supportsPrivateTracking) {
|
||||
Button(
|
||||
onClick = { onConfirmSelection(true) },
|
||||
elevation = ButtonDefaults.elevatedButtonElevation(),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.VisibilityOff,
|
||||
contentDescription = stringResource(MR.strings.action_toggle_private_on),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -286,6 +304,15 @@ private fun SearchResultItem(
|
||||
}
|
||||
},
|
||||
)
|
||||
if (trackSearch.authors.isNotEmpty() || trackSearch.artists.isNotEmpty()) {
|
||||
Text(
|
||||
text = (trackSearch.authors + trackSearch.artists).distinct().joinToString(),
|
||||
modifier = Modifier.secondaryItemAlpha(),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
}
|
||||
if (type.isNotBlank()) {
|
||||
SearchResultItemDetails(
|
||||
title = stringResource(MR.strings.track_type),
|
||||
|
@@ -5,8 +5,11 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import androidx.compose.ui.tooling.preview.datasource.LoremIpsum
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import java.text.SimpleDateFormat
|
||||
import java.time.Instant
|
||||
import java.time.temporal.ChronoUnit
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import kotlin.random.Random
|
||||
|
||||
internal class TrackerSearchPreviewProvider : PreviewParameterProvider<@Composable () -> Unit> {
|
||||
@@ -20,6 +23,7 @@ internal class TrackerSearchPreviewProvider : PreviewParameterProvider<@Composab
|
||||
onSelectedChange = {},
|
||||
onConfirmSelection = {},
|
||||
onDismissRequest = {},
|
||||
supportsPrivateTracking = false,
|
||||
)
|
||||
}
|
||||
private val fullPageWithoutSelected = @Composable {
|
||||
@@ -31,6 +35,7 @@ internal class TrackerSearchPreviewProvider : PreviewParameterProvider<@Composab
|
||||
onSelectedChange = {},
|
||||
onConfirmSelection = {},
|
||||
onDismissRequest = {},
|
||||
supportsPrivateTracking = false,
|
||||
)
|
||||
}
|
||||
private val loading = @Composable {
|
||||
@@ -42,12 +47,27 @@ internal class TrackerSearchPreviewProvider : PreviewParameterProvider<@Composab
|
||||
onSelectedChange = {},
|
||||
onConfirmSelection = {},
|
||||
onDismissRequest = {},
|
||||
supportsPrivateTracking = false,
|
||||
)
|
||||
}
|
||||
private val fullPageWithPrivateTracking = @Composable {
|
||||
val items = someTrackSearches().take(30).toList()
|
||||
TrackerSearch(
|
||||
state = TextFieldState(initialText = "search text"),
|
||||
onDispatchQuery = {},
|
||||
queryResult = Result.success(items),
|
||||
selected = items[1],
|
||||
onSelectedChange = {},
|
||||
onConfirmSelection = {},
|
||||
onDismissRequest = {},
|
||||
supportsPrivateTracking = true,
|
||||
)
|
||||
}
|
||||
override val values: Sequence<@Composable () -> Unit> = sequenceOf(
|
||||
fullPageWithSecondSelected,
|
||||
fullPageWithoutSelected,
|
||||
loading,
|
||||
fullPageWithPrivateTracking,
|
||||
)
|
||||
|
||||
private fun someTrackSearches(): Sequence<TrackSearch> = sequence {
|
||||
@@ -56,6 +76,8 @@ internal class TrackerSearchPreviewProvider : PreviewParameterProvider<@Composab
|
||||
}
|
||||
}
|
||||
|
||||
private val formatter: SimpleDateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
|
||||
|
||||
private fun randTrackSearch() = TrackSearch().let {
|
||||
it.id = Random.nextLong()
|
||||
it.manga_id = Random.nextLong()
|
||||
@@ -71,11 +93,17 @@ internal class TrackerSearchPreviewProvider : PreviewParameterProvider<@Composab
|
||||
it.finished_reading_date = 0L
|
||||
it.tracking_url = "https://example.com/tracker-example"
|
||||
it.cover_url = "https://example.com/cover.png"
|
||||
it.start_date = Instant.now().minus((1L..365).random(), ChronoUnit.DAYS).toString()
|
||||
it.start_date = formatter.format(Date.from(Instant.now().minus((1L..365).random(), ChronoUnit.DAYS)))
|
||||
it.summary = lorem((0..40).random()).joinToString()
|
||||
it.publishing_status = if (Random.nextBoolean()) "Finished" else ""
|
||||
it.publishing_type = if (Random.nextBoolean()) "Oneshot" else ""
|
||||
it.artists = randomNames()
|
||||
it.authors = randomNames()
|
||||
it
|
||||
}
|
||||
|
||||
private fun randomNames(): List<String> = (0..(0..3).random()).map { lorem((3..5).random()).joinToString() }
|
||||
|
||||
private fun lorem(words: Int): Sequence<String> =
|
||||
LoremIpsum(words).values
|
||||
}
|
||||
|
@@ -57,7 +57,9 @@ fun UpdateScreen(
|
||||
onUpdateSelected: (UpdatesItem, Boolean, Boolean, Boolean) -> Unit,
|
||||
onOpenChapter: (UpdatesItem) -> Unit,
|
||||
) {
|
||||
BackHandler(enabled = state.selectionMode, onBack = { onSelectAll(false) })
|
||||
BackHandler(enabled = state.selectionMode) {
|
||||
onSelectAll(false)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = { scrollBehavior ->
|
||||
|
@@ -1,12 +1,46 @@
|
||||
package eu.kanade.presentation.util
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.activity.BackEventCompat
|
||||
import androidx.activity.compose.PredictiveBackHandler
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.AnimatedContentTransitionScope
|
||||
import androidx.compose.animation.ContentTransform
|
||||
import androidx.compose.animation.EnterTransition
|
||||
import androidx.compose.animation.ExitTransition
|
||||
import androidx.compose.animation.SizeTransform
|
||||
import androidx.compose.animation.core.AnimationSpec
|
||||
import androidx.compose.animation.core.SeekableTransitionState
|
||||
import androidx.compose.animation.core.Spring
|
||||
import androidx.compose.animation.core.animate
|
||||
import androidx.compose.animation.core.rememberTransition
|
||||
import androidx.compose.animation.core.spring
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.ProvidableCompositionLocal
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.Saver
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.runtime.staticCompositionLocalOf
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.platform.LocalViewConfiguration
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.lerp
|
||||
import cafe.adriel.voyager.core.annotation.InternalVoyagerApi
|
||||
import cafe.adriel.voyager.core.model.ScreenModel
|
||||
import cafe.adriel.voyager.core.model.ScreenModelStore
|
||||
import cafe.adriel.voyager.core.screen.Screen
|
||||
@@ -15,18 +49,28 @@ import cafe.adriel.voyager.core.screen.uniqueScreenKey
|
||||
import cafe.adriel.voyager.core.stack.StackEvent
|
||||
import cafe.adriel.voyager.navigator.Navigator
|
||||
import cafe.adriel.voyager.transitions.ScreenTransitionContent
|
||||
import eu.kanade.tachiyomi.util.view.getWindowRadius
|
||||
import kotlinx.coroutines.CoroutineName
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.flow.dropWhile
|
||||
import kotlinx.coroutines.flow.onCompletion
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.plus
|
||||
import soup.compose.material.motion.animation.materialSharedAxisX
|
||||
import soup.compose.material.motion.animation.materialSharedAxisXIn
|
||||
import soup.compose.material.motion.animation.materialSharedAxisXOut
|
||||
import soup.compose.material.motion.animation.rememberSlideDistance
|
||||
import tachiyomi.presentation.core.util.PredictiveBack
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
import kotlin.math.absoluteValue
|
||||
|
||||
/**
|
||||
* For invoking back press to the parent activity
|
||||
*/
|
||||
@SuppressLint("ComposeCompositionLocalUsage")
|
||||
val LocalBackPress: ProvidableCompositionLocal<(() -> Unit)?> = staticCompositionLocalOf { null }
|
||||
|
||||
interface Tab : cafe.adriel.voyager.navigator.tab.Tab {
|
||||
@@ -54,39 +98,278 @@ interface AssistContentScreen {
|
||||
fun onProvideAssistUrl(): String?
|
||||
}
|
||||
|
||||
@OptIn(InternalVoyagerApi::class)
|
||||
@Composable
|
||||
fun DefaultNavigatorScreenTransition(
|
||||
navigator: Navigator,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val slideDistance = rememberSlideDistance()
|
||||
val screenCandidatesToDispose = rememberSaveable(saver = screenCandidatesToDisposeSaver()) {
|
||||
mutableStateOf(emptySet())
|
||||
}
|
||||
val currentScreens = navigator.items
|
||||
DisposableEffect(currentScreens) {
|
||||
onDispose {
|
||||
val newScreenKeys = navigator.items.map { it.key }
|
||||
screenCandidatesToDispose.value += currentScreens.filter { it.key !in newScreenKeys }
|
||||
}
|
||||
}
|
||||
|
||||
val slideDistance = rememberSlideDistance(slideDistance = 30.dp)
|
||||
ScreenTransition(
|
||||
navigator = navigator,
|
||||
transition = {
|
||||
materialSharedAxisX(
|
||||
forward = navigator.lastEvent != StackEvent.Pop,
|
||||
slideDistance = slideDistance,
|
||||
)
|
||||
},
|
||||
modifier = modifier,
|
||||
enterTransition = {
|
||||
if (it == SwipeEdge.Right) {
|
||||
materialSharedAxisXIn(forward = false, slideDistance = slideDistance)
|
||||
} else {
|
||||
materialSharedAxisXIn(forward = true, slideDistance = slideDistance)
|
||||
}
|
||||
},
|
||||
exitTransition = {
|
||||
if (it == SwipeEdge.Right) {
|
||||
materialSharedAxisXOut(forward = false, slideDistance = slideDistance)
|
||||
} else {
|
||||
materialSharedAxisXOut(forward = true, slideDistance = slideDistance)
|
||||
}
|
||||
},
|
||||
popEnterTransition = {
|
||||
if (it == SwipeEdge.Right) {
|
||||
materialSharedAxisXIn(forward = true, slideDistance = slideDistance)
|
||||
} else {
|
||||
materialSharedAxisXIn(forward = false, slideDistance = slideDistance)
|
||||
}
|
||||
},
|
||||
popExitTransition = {
|
||||
if (it == SwipeEdge.Right) {
|
||||
materialSharedAxisXOut(forward = true, slideDistance = slideDistance)
|
||||
} else {
|
||||
materialSharedAxisXOut(forward = false, slideDistance = slideDistance)
|
||||
}
|
||||
},
|
||||
content = { screen ->
|
||||
if (this.transition.targetState == this.transition.currentState) {
|
||||
LaunchedEffect(Unit) {
|
||||
val newScreens = navigator.items.map { it.key }
|
||||
val screensToDispose = screenCandidatesToDispose.value.filterNot { it.key in newScreens }
|
||||
if (screensToDispose.isNotEmpty()) {
|
||||
screensToDispose.forEach { navigator.dispose(it) }
|
||||
navigator.clearEvent()
|
||||
}
|
||||
screenCandidatesToDispose.value = emptySet()
|
||||
}
|
||||
}
|
||||
screen.Content()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
enum class SwipeEdge {
|
||||
Unknown,
|
||||
Left,
|
||||
Right,
|
||||
}
|
||||
|
||||
private enum class AnimationType {
|
||||
Pop,
|
||||
Cancel,
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ScreenTransition(
|
||||
navigator: Navigator,
|
||||
transition: AnimatedContentTransitionScope<Screen>.() -> ContentTransform,
|
||||
modifier: Modifier = Modifier,
|
||||
enterTransition: AnimatedContentTransitionScope<Screen>.(SwipeEdge) -> EnterTransition = { fadeIn() },
|
||||
exitTransition: AnimatedContentTransitionScope<Screen>.(SwipeEdge) -> ExitTransition = { fadeOut() },
|
||||
popEnterTransition: AnimatedContentTransitionScope<Screen>.(SwipeEdge) -> EnterTransition = enterTransition,
|
||||
popExitTransition: AnimatedContentTransitionScope<Screen>.(SwipeEdge) -> ExitTransition = exitTransition,
|
||||
sizeTransform: (AnimatedContentTransitionScope<Screen>.() -> SizeTransform?)? = null,
|
||||
flingAnimationSpec: () -> AnimationSpec<Float> = { spring(stiffness = Spring.StiffnessLow) },
|
||||
content: ScreenTransitionContent = { it.Content() },
|
||||
) {
|
||||
AnimatedContent(
|
||||
targetState = navigator.lastItem,
|
||||
transitionSpec = transition,
|
||||
val view = LocalView.current
|
||||
val viewConfig = LocalViewConfiguration.current
|
||||
val scope = rememberCoroutineScope()
|
||||
val state = remember {
|
||||
ScreenTransitionState(
|
||||
navigator = navigator,
|
||||
scope = scope,
|
||||
flingAnimationSpec = flingAnimationSpec(),
|
||||
windowCornerRadius = view.getWindowRadius().toFloat(),
|
||||
)
|
||||
}
|
||||
val transitionState = remember { SeekableTransitionState(navigator.lastItem) }
|
||||
val transition = rememberTransition(transitionState = transitionState)
|
||||
|
||||
if (state.isPredictiveBack || state.isAnimating) {
|
||||
LaunchedEffect(state.progress) {
|
||||
if (!state.isPredictiveBack) return@LaunchedEffect
|
||||
val previousEntry = navigator.items.getOrNull(navigator.size - 2)
|
||||
if (previousEntry != null) {
|
||||
transitionState.seekTo(fraction = state.progress, targetState = previousEntry)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LaunchedEffect(navigator) {
|
||||
snapshotFlow { navigator.lastItem }
|
||||
.collect {
|
||||
state.cancelCancelAnimation()
|
||||
if (it != transitionState.currentState) {
|
||||
transitionState.animateTo(it)
|
||||
} else {
|
||||
transitionState.snapTo(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PredictiveBackHandler(enabled = navigator.canPop) { backEvent ->
|
||||
state.cancelCancelAnimation()
|
||||
var startOffset: Offset? = null
|
||||
backEvent
|
||||
.dropWhile {
|
||||
if (startOffset == null) startOffset = Offset(it.touchX, it.touchY)
|
||||
if (state.isAnimating) return@dropWhile true
|
||||
// Touch slop check
|
||||
val diff = Offset(it.touchX, it.touchY) - startOffset!!
|
||||
diff.x.absoluteValue < viewConfig.touchSlop && diff.y.absoluteValue < viewConfig.touchSlop
|
||||
}
|
||||
.onCompletion {
|
||||
if (it == null) {
|
||||
state.finish()
|
||||
} else {
|
||||
state.cancel()
|
||||
}
|
||||
}
|
||||
.collect {
|
||||
state.setPredictiveBackProgress(
|
||||
progress = it.progress,
|
||||
swipeEdge = when (it.swipeEdge) {
|
||||
BackEventCompat.EDGE_LEFT -> SwipeEdge.Left
|
||||
BackEventCompat.EDGE_RIGHT -> SwipeEdge.Right
|
||||
else -> SwipeEdge.Unknown
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
transition.AnimatedContent(
|
||||
modifier = modifier,
|
||||
label = "transition",
|
||||
) { screen ->
|
||||
navigator.saveableState("transition", screen) {
|
||||
content(screen)
|
||||
transitionSpec = {
|
||||
val pop = navigator.lastEvent == StackEvent.Pop || state.isPredictiveBack
|
||||
ContentTransform(
|
||||
targetContentEnter = if (pop) {
|
||||
popEnterTransition(state.swipeEdge)
|
||||
} else {
|
||||
enterTransition(state.swipeEdge)
|
||||
},
|
||||
initialContentExit = if (pop) {
|
||||
popExitTransition(state.swipeEdge)
|
||||
} else {
|
||||
exitTransition(state.swipeEdge)
|
||||
},
|
||||
targetContentZIndex = if (pop) 0f else 1f,
|
||||
sizeTransform = sizeTransform?.invoke(this),
|
||||
)
|
||||
},
|
||||
contentKey = { it.key },
|
||||
) {
|
||||
navigator.saveableState("transition", it) {
|
||||
content(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Stable
|
||||
private class ScreenTransitionState(
|
||||
private val navigator: Navigator,
|
||||
private val scope: CoroutineScope,
|
||||
private val flingAnimationSpec: AnimationSpec<Float>,
|
||||
windowCornerRadius: Float,
|
||||
) {
|
||||
var isPredictiveBack: Boolean by mutableStateOf(false)
|
||||
private set
|
||||
|
||||
var progress: Float by mutableFloatStateOf(0f)
|
||||
private set
|
||||
|
||||
var swipeEdge: SwipeEdge by mutableStateOf(SwipeEdge.Unknown)
|
||||
private set
|
||||
|
||||
private var animationJob: Pair<Job, AnimationType>? by mutableStateOf(null)
|
||||
|
||||
val isAnimating: Boolean
|
||||
get() = animationJob?.first?.isActive == true
|
||||
|
||||
val windowCornerShape = RoundedCornerShape(windowCornerRadius)
|
||||
|
||||
private fun reset() {
|
||||
this.isPredictiveBack = false
|
||||
this.swipeEdge = SwipeEdge.Unknown
|
||||
this.animationJob = null
|
||||
}
|
||||
|
||||
fun setPredictiveBackProgress(progress: Float, swipeEdge: SwipeEdge) {
|
||||
this.progress = lerp(0f, 0.65f, PredictiveBack.transform(progress))
|
||||
this.swipeEdge = swipeEdge
|
||||
this.isPredictiveBack = true
|
||||
}
|
||||
|
||||
fun finish() {
|
||||
if (!isPredictiveBack) {
|
||||
navigator.pop()
|
||||
return
|
||||
}
|
||||
animationJob = scope.launch {
|
||||
try {
|
||||
animate(
|
||||
initialValue = progress,
|
||||
targetValue = 1f,
|
||||
animationSpec = flingAnimationSpec,
|
||||
block = { i, _ -> progress = i },
|
||||
)
|
||||
navigator.pop()
|
||||
} catch (e: CancellationException) {
|
||||
// Cancelled
|
||||
progress = 0f
|
||||
} finally {
|
||||
reset()
|
||||
}
|
||||
} to AnimationType.Pop
|
||||
}
|
||||
|
||||
fun cancel() {
|
||||
if (!isPredictiveBack) {
|
||||
return
|
||||
}
|
||||
animationJob = scope.launch {
|
||||
try {
|
||||
animate(
|
||||
initialValue = progress,
|
||||
targetValue = 0f,
|
||||
animationSpec = flingAnimationSpec,
|
||||
block = { i, _ -> progress = i },
|
||||
)
|
||||
} catch (e: CancellationException) {
|
||||
// Cancelled
|
||||
progress = 1f
|
||||
} finally {
|
||||
reset()
|
||||
}
|
||||
} to AnimationType.Cancel
|
||||
}
|
||||
|
||||
fun cancelCancelAnimation() {
|
||||
if (animationJob?.second == AnimationType.Cancel) {
|
||||
animationJob?.first?.cancel()
|
||||
animationJob = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun screenCandidatesToDisposeSaver(): Saver<MutableState<Set<Screen>>, List<Screen>> {
|
||||
return Saver(
|
||||
save = { it.value.toList() },
|
||||
restore = { mutableStateOf(it.toSet()) },
|
||||
)
|
||||
}
|
||||
|
@@ -44,7 +44,6 @@ import kotlinx.coroutines.launch
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.material.Scaffold
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
|
||||
@Composable
|
||||
fun WebViewScreenContent(
|
||||
onNavigateUp: () -> Unit,
|
||||
|
@@ -52,9 +52,9 @@ import kotlinx.coroutines.flow.onEach
|
||||
import logcat.AndroidLogcatLogger
|
||||
import logcat.LogPriority
|
||||
import logcat.LogcatLogger
|
||||
import mihon.core.firebase.FirebaseConfig
|
||||
import mihon.core.migration.Migrator
|
||||
import mihon.core.migration.migrations.migrations
|
||||
import mihon.telemetry.TelemetryConfig
|
||||
import org.conscrypt.Conscrypt
|
||||
import tachiyomi.core.common.i18n.stringResource
|
||||
import tachiyomi.core.common.preference.Preference
|
||||
@@ -80,7 +80,7 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
|
||||
override fun onCreate() {
|
||||
super<Application>.onCreate()
|
||||
patchInjekt()
|
||||
FirebaseConfig.init(applicationContext)
|
||||
TelemetryConfig.init(applicationContext)
|
||||
|
||||
GlobalExceptionHandler.initialize(applicationContext, CrashActivity::class.java)
|
||||
|
||||
@@ -136,12 +136,12 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
|
||||
|
||||
privacyPreferences.analytics()
|
||||
.changes()
|
||||
.onEach(FirebaseConfig::setAnalyticsEnabled)
|
||||
.onEach(TelemetryConfig::setAnalyticsEnabled)
|
||||
.launchIn(scope)
|
||||
|
||||
privacyPreferences.crashlytics()
|
||||
.changes()
|
||||
.onEach(FirebaseConfig::setCrashlyticsEnabled)
|
||||
.onEach(TelemetryConfig::setCrashlyticsEnabled)
|
||||
.launchIn(scope)
|
||||
|
||||
basePreferences.hardwareBitmapThreshold().let { preference ->
|
||||
@@ -219,17 +219,15 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
|
||||
try {
|
||||
// Override the value passed as X-Requested-With in WebView requests
|
||||
val stackTrace = Looper.getMainLooper().thread.stackTrace
|
||||
val chromiumElement = stackTrace.find {
|
||||
it.className.equals(
|
||||
"org.chromium.base.BuildInfo",
|
||||
ignoreCase = true,
|
||||
)
|
||||
}
|
||||
if (chromiumElement?.methodName.equals("getAll", ignoreCase = true)) {
|
||||
return WebViewUtil.SPOOF_PACKAGE_NAME
|
||||
val isChromiumCall = stackTrace.any { trace ->
|
||||
trace.className.equals("org.chromium.base.BuildInfo", ignoreCase = true) &&
|
||||
setOf("getAll", "getPackageName", "<init>").any { trace.methodName.equals(it, ignoreCase = true) }
|
||||
}
|
||||
|
||||
if (isChromiumCall) return WebViewUtil.spoofedPackageName(applicationContext)
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
|
||||
return super.getPackageName()
|
||||
}
|
||||
|
||||
|
@@ -80,7 +80,7 @@ class BackupNotifier(private val context: Context) {
|
||||
addAction(
|
||||
R.drawable.ic_share_24dp,
|
||||
context.stringResource(MR.strings.action_share),
|
||||
NotificationReceiver.shareBackupPendingBroadcast(context, file.uri),
|
||||
NotificationReceiver.shareBackupPendingActivity(context, file.uri),
|
||||
)
|
||||
|
||||
show(Notifications.ID_BACKUP_COMPLETE)
|
||||
|
@@ -99,4 +99,6 @@ private fun Manga.toBackupManga() =
|
||||
lastModifiedAt = this.lastModifiedAt,
|
||||
favoriteModifiedAt = this.favoriteModifiedAt,
|
||||
version = this.version,
|
||||
notes = this.notes,
|
||||
initialized = this.initialized,
|
||||
)
|
||||
|
@@ -8,6 +8,7 @@ import tachiyomi.domain.category.model.Category
|
||||
class BackupCategory(
|
||||
@ProtoNumber(1) var name: String,
|
||||
@ProtoNumber(2) var order: Long = 0,
|
||||
@ProtoNumber(3) var id: Long = 0,
|
||||
// @ProtoNumber(3) val updateInterval: Int = 0, 1.x value not used in 0.x
|
||||
@ProtoNumber(100) var flags: Long = 0,
|
||||
) {
|
||||
@@ -21,6 +22,7 @@ class BackupCategory(
|
||||
|
||||
val backupCategoryMapper = { category: Category ->
|
||||
BackupCategory(
|
||||
id = category.id,
|
||||
name = category.name,
|
||||
order = category.order,
|
||||
flags = category.flags,
|
||||
|
@@ -38,8 +38,11 @@ data class BackupManga(
|
||||
@ProtoNumber(105) var updateStrategy: UpdateStrategy = UpdateStrategy.ALWAYS_UPDATE,
|
||||
@ProtoNumber(106) var lastModifiedAt: Long = 0,
|
||||
@ProtoNumber(107) var favoriteModifiedAt: Long? = null,
|
||||
// Mihon values start here
|
||||
@ProtoNumber(108) var excludedScanlators: List<String> = emptyList(),
|
||||
@ProtoNumber(109) var version: Long = 0,
|
||||
@ProtoNumber(110) var notes: String = "",
|
||||
@ProtoNumber(111) var initialized: Boolean = false,
|
||||
) {
|
||||
fun getMangaImpl(): Manga {
|
||||
return Manga.create().copy(
|
||||
@@ -60,6 +63,8 @@ data class BackupManga(
|
||||
lastModifiedAt = this@BackupManga.lastModifiedAt,
|
||||
favoriteModifiedAt = this@BackupManga.favoriteModifiedAt,
|
||||
version = this@BackupManga.version,
|
||||
notes = this@BackupManga.notes,
|
||||
initialized = this@BackupManga.initialized,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@@ -25,6 +25,7 @@ data class BackupTracking(
|
||||
@ProtoNumber(10) var startedReadingDate: Long = 0,
|
||||
// finishedReadingDate is called endReadTime in 1.x
|
||||
@ProtoNumber(11) var finishedReadingDate: Long = 0,
|
||||
@ProtoNumber(12) var private: Boolean = false,
|
||||
@ProtoNumber(100) var mediaId: Long = 0,
|
||||
) {
|
||||
|
||||
@@ -48,6 +49,7 @@ data class BackupTracking(
|
||||
startDate = this@BackupTracking.startedReadingDate,
|
||||
finishDate = this@BackupTracking.finishedReadingDate,
|
||||
remoteUrl = this@BackupTracking.trackingUrl,
|
||||
private = this@BackupTracking.private,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -66,6 +68,7 @@ val backupTrackMapper = {
|
||||
remoteUrl: String,
|
||||
startDate: Long,
|
||||
finishDate: Long,
|
||||
private: Boolean,
|
||||
->
|
||||
BackupTracking(
|
||||
syncId = syncId.toInt(),
|
||||
@@ -80,5 +83,6 @@ val backupTrackMapper = {
|
||||
startedReadingDate = startDate,
|
||||
finishedReadingDate = finishDate,
|
||||
trackingUrl = remoteUrl,
|
||||
private = private,
|
||||
)
|
||||
}
|
||||
|
@@ -91,7 +91,7 @@ class BackupRestorer(
|
||||
restoreCategories(backup.backupCategories)
|
||||
}
|
||||
if (options.appSettings) {
|
||||
restoreAppPreferences(backup.backupPreferences)
|
||||
restoreAppPreferences(backup.backupPreferences, backup.backupCategories.takeIf { options.categories })
|
||||
}
|
||||
if (options.sourceSettings) {
|
||||
restoreSourcePreferences(backup.backupSourcePreferences)
|
||||
@@ -140,9 +140,15 @@ class BackupRestorer(
|
||||
}
|
||||
}
|
||||
|
||||
private fun CoroutineScope.restoreAppPreferences(preferences: List<BackupPreference>) = launch {
|
||||
private fun CoroutineScope.restoreAppPreferences(
|
||||
preferences: List<BackupPreference>,
|
||||
categories: List<BackupCategory>?,
|
||||
) = launch {
|
||||
ensureActive()
|
||||
preferenceRestorer.restoreApp(preferences)
|
||||
preferenceRestorer.restoreApp(
|
||||
preferences,
|
||||
categories,
|
||||
)
|
||||
|
||||
restoreProgress += 1
|
||||
notifier.showRestoreProgress(
|
||||
|
@@ -129,6 +129,7 @@ class MangaRestorer(
|
||||
updateStrategy = manga.updateStrategy.let(UpdateStrategyColumnAdapter::encode),
|
||||
version = manga.version,
|
||||
isSyncing = 1,
|
||||
notes = manga.notes,
|
||||
)
|
||||
}
|
||||
return manga
|
||||
@@ -138,9 +139,7 @@ class MangaRestorer(
|
||||
manga: Manga,
|
||||
): Manga {
|
||||
return manga.copy(
|
||||
initialized = manga.description != null,
|
||||
id = insertManga(manga),
|
||||
version = manga.version,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -261,6 +260,7 @@ class MangaRestorer(
|
||||
dateAdded = manga.dateAdded,
|
||||
updateStrategy = manga.updateStrategy,
|
||||
version = manga.version,
|
||||
notes = manga.notes,
|
||||
)
|
||||
mangasQueries.selectLastInsertedRowId()
|
||||
}
|
||||
@@ -404,6 +404,7 @@ class MangaRestorer(
|
||||
track.remoteUrl,
|
||||
track.startDate,
|
||||
track.finishDate,
|
||||
track.private,
|
||||
track.id,
|
||||
)
|
||||
}
|
||||
|
@@ -1,7 +1,9 @@
|
||||
package eu.kanade.tachiyomi.data.backup.restore.restorers
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import eu.kanade.tachiyomi.data.backup.create.BackupCreateJob
|
||||
import eu.kanade.tachiyomi.data.backup.models.BackupCategory
|
||||
import eu.kanade.tachiyomi.data.backup.models.BackupPreference
|
||||
import eu.kanade.tachiyomi.data.backup.models.BackupSourcePreferences
|
||||
import eu.kanade.tachiyomi.data.backup.models.BooleanPreferenceValue
|
||||
@@ -14,66 +16,122 @@ import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
|
||||
import eu.kanade.tachiyomi.source.sourcePreferences
|
||||
import tachiyomi.core.common.preference.AndroidPreferenceStore
|
||||
import tachiyomi.core.common.preference.PreferenceStore
|
||||
import tachiyomi.core.common.preference.plusAssign
|
||||
import tachiyomi.domain.category.interactor.GetCategories
|
||||
import tachiyomi.domain.category.model.Category
|
||||
import tachiyomi.domain.download.service.DownloadPreferences
|
||||
import tachiyomi.domain.library.service.LibraryPreferences
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class PreferenceRestorer(
|
||||
private val context: Context,
|
||||
private val getCategories: GetCategories = Injekt.get(),
|
||||
private val preferenceStore: PreferenceStore = Injekt.get(),
|
||||
) {
|
||||
|
||||
fun restoreApp(preferences: List<BackupPreference>) {
|
||||
restorePreferences(preferences, preferenceStore)
|
||||
suspend fun restoreApp(
|
||||
preferences: List<BackupPreference>,
|
||||
backupCategories: List<BackupCategory>?,
|
||||
) {
|
||||
restorePreferences(
|
||||
preferences,
|
||||
preferenceStore,
|
||||
backupCategories,
|
||||
)
|
||||
|
||||
LibraryUpdateJob.setupTask(context)
|
||||
BackupCreateJob.setupTask(context)
|
||||
}
|
||||
|
||||
fun restoreSource(preferences: List<BackupSourcePreferences>) {
|
||||
suspend fun restoreSource(preferences: List<BackupSourcePreferences>) {
|
||||
preferences.forEach {
|
||||
val sourcePrefs = AndroidPreferenceStore(context, sourcePreferences(it.sourceKey))
|
||||
restorePreferences(it.prefs, sourcePrefs)
|
||||
}
|
||||
}
|
||||
|
||||
private fun restorePreferences(
|
||||
private suspend fun restorePreferences(
|
||||
toRestore: List<BackupPreference>,
|
||||
preferenceStore: PreferenceStore,
|
||||
backupCategories: List<BackupCategory>? = null,
|
||||
) {
|
||||
val allCategories = if (backupCategories != null) getCategories.await() else emptyList()
|
||||
val categoriesByName = allCategories.associateBy { it.name }
|
||||
val backupCategoriesById = backupCategories?.associateBy { it.id.toString() }.orEmpty()
|
||||
val prefs = preferenceStore.getAll()
|
||||
toRestore.forEach { (key, value) ->
|
||||
when (value) {
|
||||
is IntPreferenceValue -> {
|
||||
if (prefs[key] is Int?) {
|
||||
preferenceStore.getInt(key).set(value.value)
|
||||
}
|
||||
}
|
||||
is LongPreferenceValue -> {
|
||||
if (prefs[key] is Long?) {
|
||||
preferenceStore.getLong(key).set(value.value)
|
||||
}
|
||||
}
|
||||
is FloatPreferenceValue -> {
|
||||
if (prefs[key] is Float?) {
|
||||
preferenceStore.getFloat(key).set(value.value)
|
||||
}
|
||||
}
|
||||
is StringPreferenceValue -> {
|
||||
if (prefs[key] is String?) {
|
||||
preferenceStore.getString(key).set(value.value)
|
||||
}
|
||||
}
|
||||
is BooleanPreferenceValue -> {
|
||||
if (prefs[key] is Boolean?) {
|
||||
preferenceStore.getBoolean(key).set(value.value)
|
||||
}
|
||||
}
|
||||
is StringSetPreferenceValue -> {
|
||||
if (prefs[key] is Set<*>?) {
|
||||
preferenceStore.getStringSet(key).set(value.value)
|
||||
try {
|
||||
when (value) {
|
||||
is IntPreferenceValue -> {
|
||||
if (prefs[key] is Int?) {
|
||||
val newValue = if (key == LibraryPreferences.DEFAULT_CATEGORY_PREF_KEY) {
|
||||
backupCategoriesById[value.value.toString()]
|
||||
?.let { categoriesByName[it.name]?.id?.toInt() }
|
||||
} else {
|
||||
value.value
|
||||
}
|
||||
|
||||
newValue?.let { preferenceStore.getInt(key).set(it) }
|
||||
}
|
||||
}
|
||||
is LongPreferenceValue -> {
|
||||
if (prefs[key] is Long?) {
|
||||
preferenceStore.getLong(key).set(value.value)
|
||||
}
|
||||
}
|
||||
is FloatPreferenceValue -> {
|
||||
if (prefs[key] is Float?) {
|
||||
preferenceStore.getFloat(key).set(value.value)
|
||||
}
|
||||
}
|
||||
is StringPreferenceValue -> {
|
||||
if (prefs[key] is String?) {
|
||||
preferenceStore.getString(key).set(value.value)
|
||||
}
|
||||
}
|
||||
is BooleanPreferenceValue -> {
|
||||
if (prefs[key] is Boolean?) {
|
||||
preferenceStore.getBoolean(key).set(value.value)
|
||||
}
|
||||
}
|
||||
is StringSetPreferenceValue -> {
|
||||
if (prefs[key] is Set<*>?) {
|
||||
val restored = restoreCategoriesPreference(
|
||||
key,
|
||||
value.value,
|
||||
preferenceStore,
|
||||
backupCategoriesById,
|
||||
categoriesByName,
|
||||
)
|
||||
if (!restored) preferenceStore.getStringSet(key).set(value.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("PreferenceRestorer", "Failed to restore preference <$key>", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun restoreCategoriesPreference(
|
||||
key: String,
|
||||
value: Set<String>,
|
||||
preferenceStore: PreferenceStore,
|
||||
backupCategoriesById: Map<String, BackupCategory>,
|
||||
categoriesByName: Map<String, Category>,
|
||||
): Boolean {
|
||||
val categoryPreferences = LibraryPreferences.categoryPreferenceKeys + DownloadPreferences.categoryPreferenceKeys
|
||||
if (key !in categoryPreferences) return false
|
||||
|
||||
val ids = value.mapNotNull {
|
||||
backupCategoriesById[it]?.name?.let { name ->
|
||||
categoriesByName[name]?.id?.toString()
|
||||
}
|
||||
}
|
||||
|
||||
if (ids.isNotEmpty()) {
|
||||
preferenceStore.getStringSet(key) += ids
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
@@ -27,6 +27,9 @@ interface Chapter : SChapter, Serializable {
|
||||
var version: Long
|
||||
}
|
||||
|
||||
val Chapter.isRecognizedNumber: Boolean
|
||||
get() = chapter_number >= 0f
|
||||
|
||||
fun Chapter.toDomainChapter(): DomainChapter? {
|
||||
if (id == null || manga_id == null) return null
|
||||
return DomainChapter(
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user