mirror of
https://github.com/mihonapp/mihon.git
synced 2025-11-21 00:11:13 +01:00
Compare commits
298 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
412815af06 | ||
|
|
f7fb68692a | ||
|
|
aa300cb53e | ||
|
|
855eea2ada | ||
|
|
f4703ed83a | ||
|
|
506d51a007 | ||
|
|
9f9155121c | ||
|
|
282110ef21 | ||
|
|
ace387f8bf | ||
|
|
5e428071c9 | ||
|
|
0acd80dd95 | ||
|
|
bdb0ce4779 | ||
|
|
e8bdf58530 | ||
|
|
e36b4ce60b | ||
|
|
6d543024a3 | ||
|
|
0e0b6d9283 | ||
|
|
38b1bd7383 | ||
|
|
8609553896 | ||
|
|
5f0c460668 | ||
|
|
ac28b6c80c | ||
|
|
cc28776735 | ||
|
|
6ab87c7931 | ||
|
|
8662f80fbf | ||
|
|
6508766ccd | ||
|
|
09ec9fc8c5 | ||
|
|
87c6f34a55 | ||
|
|
643762f913 | ||
|
|
7e880014b0 | ||
|
|
f36c259c1f | ||
|
|
aef3beb15f | ||
|
|
e9469451ac | ||
|
|
f9793d3323 | ||
|
|
93ba6acea5 | ||
|
|
5e7fecc2c1 | ||
|
|
343074da5f | ||
|
|
7c08b75555 | ||
|
|
cbf72f4c60 | ||
|
|
0b6de39f2f | ||
|
|
72c4d1fdee | ||
|
|
fa96366b55 | ||
|
|
3ff25bc984 | ||
|
|
e9224bc2ba | ||
|
|
3c731c2cf5 | ||
|
|
5ac58d01b8 | ||
|
|
eefaf028ce | ||
|
|
582ccca1ab | ||
|
|
8f972115a8 | ||
|
|
6f6c033811 | ||
|
|
57a0ab6711 | ||
|
|
58b25d697f | ||
|
|
1a31c7c7ee | ||
|
|
ad6b651b37 | ||
|
|
96e5131358 | ||
|
|
1d5bc8d2c2 | ||
|
|
6cee911239 | ||
|
|
96347e3f76 | ||
|
|
9a45d248b1 | ||
|
|
04168ecec8 | ||
|
|
607f0ea9cd | ||
|
|
27a4f6f45c | ||
|
|
5236d003d2 | ||
|
|
d61a41e819 | ||
|
|
5637860dd2 | ||
|
|
d4d18d0898 | ||
|
|
065147472e | ||
|
|
6f635782c2 | ||
|
|
86d85f74c0 | ||
|
|
29e6a2c4a6 | ||
|
|
60c66bbd3a | ||
|
|
060e5b2e2e | ||
|
|
4ac9fcd4d3 | ||
|
|
d3b7f7e55f | ||
|
|
0d926626a1 | ||
|
|
6495a2ea43 | ||
|
|
94f711ba2a | ||
|
|
9f5c4e03b2 | ||
|
|
49562e1915 | ||
|
|
57c82b30ba | ||
|
|
e573f72cfd | ||
|
|
95357a8625 | ||
|
|
bd90307df9 | ||
|
|
16b5317b90 | ||
|
|
83f4b48629 | ||
|
|
4665dc50f6 | ||
|
|
85f5e5019e | ||
|
|
4bc3b9f3b6 | ||
|
|
2c0d3678d9 | ||
|
|
feda410152 | ||
|
|
200c2df5ba | ||
|
|
be09cddde2 | ||
|
|
498317de52 | ||
|
|
33b876edc6 | ||
|
|
e7251f2034 | ||
|
|
3d3c36078a | ||
|
|
c6a96b3970 | ||
|
|
fb3dc1c984 | ||
|
|
029e36bfb4 | ||
|
|
d88dbe6409 | ||
|
|
d0bad9f0bd | ||
|
|
5c88f3860d | ||
|
|
7d717ee7fd | ||
|
|
a93f71b82b | ||
|
|
a8b6629b08 | ||
|
|
9bf3f15fff | ||
|
|
1c3e96bf7f | ||
|
|
45c1a31488 | ||
|
|
32257e438e | ||
|
|
49a84c8914 | ||
|
|
095ef8e74b | ||
|
|
4de3bf574a | ||
|
|
549d74a2c9 | ||
|
|
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 |
@@ -7,7 +7,7 @@ indent_style = space
|
|||||||
insert_final_newline = true
|
insert_final_newline = true
|
||||||
trim_trailing_whitespace = true
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
[*.xml]
|
[*.{xml,sq,sqm,aidl}]
|
||||||
indent_size = 4
|
indent_size = 4
|
||||||
|
|
||||||
# noinspection EditorConfigKeyCorrectness
|
# noinspection EditorConfigKeyCorrectness
|
||||||
@@ -23,6 +23,9 @@ ij_kotlin_name_count_to_use_star_import_for_members = 2147483647
|
|||||||
ktlint_code_style = intellij_idea
|
ktlint_code_style = intellij_idea
|
||||||
ktlint_function_naming_ignore_when_annotated_with = Composable
|
ktlint_function_naming_ignore_when_annotated_with = Composable
|
||||||
ktlint_standard_class-signature = disabled
|
ktlint_standard_class-signature = disabled
|
||||||
|
ktlint_standard_comment-wrapping = disabled
|
||||||
ktlint_standard_discouraged-comment-location = disabled
|
ktlint_standard_discouraged-comment-location = disabled
|
||||||
ktlint_standard_function-expression-body = disabled
|
ktlint_standard_function-expression-body = disabled
|
||||||
ktlint_standard_function-signature = 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
|
name: ⭐ Feature request
|
||||||
description: Suggest a feature to improve Mihon
|
description: Suggest a feature to improve Mihon
|
||||||
labels: [Feature request]
|
labels: [feature request]
|
||||||
body:
|
body:
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: feature-description
|
id: feature-description
|
||||||
attributes:
|
attributes:
|
||||||
@@ -31,7 +30,7 @@ body:
|
|||||||
required: true
|
required: true
|
||||||
- label: I have written a short but informative title.
|
- label: I have written a short but informative title.
|
||||||
required: true
|
required: true
|
||||||
- label: I have updated the app to version **[0.18.0](https://github.com/mihonapp/mihon/releases/latest)**.
|
- label: I have updated the app to version **[0.19.3](https://github.com/mihonapp/mihon/releases/latest)**.
|
||||||
required: true
|
required: true
|
||||||
- label: I will fill out all of the requested information in this form.
|
- label: I will fill out all of the requested information in this form.
|
||||||
required: true
|
required: true
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
name: 🐞 Issue report
|
name: 🐞 Issue report
|
||||||
description: Report an issue in Mihon
|
description: Report an issue in Mihon
|
||||||
labels: [Bug]
|
labels: [bug]
|
||||||
body:
|
body:
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: reproduce-steps
|
id: reproduce-steps
|
||||||
attributes:
|
attributes:
|
||||||
@@ -53,7 +52,7 @@ body:
|
|||||||
label: Mihon version
|
label: Mihon version
|
||||||
description: You can find your Mihon version in **More → About**.
|
description: You can find your Mihon version in **More → About**.
|
||||||
placeholder: |
|
placeholder: |
|
||||||
Example: "0.18.0"
|
Example: "0.19.3"
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
@@ -96,7 +95,7 @@ body:
|
|||||||
required: true
|
required: true
|
||||||
- label: I have gone through the [FAQ](https://mihon.app/docs/faq/general) and [troubleshooting guide](https://mihon.app/docs/guides/troubleshooting/).
|
- label: I have gone through the [FAQ](https://mihon.app/docs/faq/general) and [troubleshooting guide](https://mihon.app/docs/guides/troubleshooting/).
|
||||||
required: true
|
required: true
|
||||||
- label: I have updated the app to version **[0.18.0](https://github.com/mihonapp/mihon/releases/latest)**.
|
- label: I have updated the app to version **[0.19.3](https://github.com/mihonapp/mihon/releases/latest)**.
|
||||||
required: true
|
required: true
|
||||||
- label: I have filled out all of the requested information in this form, including specific version numbers.
|
- label: I have filled out all of the requested information in this form, including specific version numbers.
|
||||||
required: true
|
required: true
|
||||||
71
.github/workflows/build.yml
vendored
Normal file
71
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
name: Build & Test
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- '**'
|
||||||
|
- '!**.md'
|
||||||
|
- '!i18n/src/commonMain/moko-resources/**/strings.xml'
|
||||||
|
- '!i18n/src/commonMain/moko-resources/**/plurals.xml'
|
||||||
|
- 'i18n/src/commonMain/moko-resources/base/strings.xml'
|
||||||
|
- 'i18n/src/commonMain/moko-resources/base/plurals.xml'
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build & Test App
|
||||||
|
runs-on: 'ubuntu-24.04'
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
|
- name: Dependency Review
|
||||||
|
if: github.event_name == 'pull_request'
|
||||||
|
uses: actions/dependency-review-action@40c09b7dc99638e5ddb0bfd91c1673effc064d8a # v4.8.1
|
||||||
|
|
||||||
|
- name: Set up JDK
|
||||||
|
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||||
|
with:
|
||||||
|
java-version: 17
|
||||||
|
distribution: temurin
|
||||||
|
|
||||||
|
- name: Set up Gradle
|
||||||
|
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
|
||||||
|
|
||||||
|
- name: Check code format
|
||||||
|
run: ./gradlew spotlessCheck
|
||||||
|
|
||||||
|
- name: Build app
|
||||||
|
run: ./gradlew assembleRelease -Pinclude-telemetry -Penable-updater
|
||||||
|
|
||||||
|
- name: Run unit tests
|
||||||
|
id: unit_tests
|
||||||
|
run: ./gradlew testReleaseUnitTest
|
||||||
|
|
||||||
|
- name: Upload test report
|
||||||
|
if: steps.unit_tests.outcome == 'failure'
|
||||||
|
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||||
|
with:
|
||||||
|
name: test-report-${{ github.sha }}
|
||||||
|
path: app/build/reports/tests/testReleaseUnitTest
|
||||||
|
|
||||||
|
- name: Upload APK
|
||||||
|
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||||
|
with:
|
||||||
|
name: arm64-v8a-${{ github.sha }}
|
||||||
|
path: app/build/outputs/apk/release/app-arm64-v8a-release-unsigned.apk
|
||||||
|
|
||||||
|
- name: Upload mapping
|
||||||
|
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||||
|
with:
|
||||||
|
name: mapping-${{ github.sha }}
|
||||||
|
path: app/build/outputs/mapping/release
|
||||||
59
.github/workflows/build_pull_request.yml
vendored
59
.github/workflows/build_pull_request.yml
vendored
@@ -1,59 +0,0 @@
|
|||||||
name: PR build check
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
paths:
|
|
||||||
- '**'
|
|
||||||
- '!**.md'
|
|
||||||
- '!i18n/src/commonMain/moko-resources/**/strings.xml'
|
|
||||||
- '!i18n/src/commonMain/moko-resources/**/plurals.xml'
|
|
||||||
- 'i18n/src/commonMain/moko-resources/base/strings.xml'
|
|
||||||
- 'i18n/src/commonMain/moko-resources/base/plurals.xml'
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
name: Build app
|
|
||||||
runs-on: 'ubuntu-24.04'
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Clone repo
|
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
|
||||||
|
|
||||||
- name: Dependency Review
|
|
||||||
uses: actions/dependency-review-action@3b139cfc5fae8b618d3eae3675e383bb1769c019 # v4.5.0
|
|
||||||
|
|
||||||
- name: Set up JDK
|
|
||||||
uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0
|
|
||||||
with:
|
|
||||||
java-version: 17
|
|
||||||
distribution: temurin
|
|
||||||
|
|
||||||
- name: Set up gradle
|
|
||||||
uses: gradle/actions/setup-gradle@94baf225fe0a508e581a564467443d0e2379123b # v4.3.0
|
|
||||||
|
|
||||||
- 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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
|
||||||
with:
|
|
||||||
name: arm64-v8a-${{ github.sha }}
|
|
||||||
path: app/build/outputs/apk/release/app-arm64-v8a-release-unsigned.apk
|
|
||||||
|
|
||||||
- name: Upload mapping
|
|
||||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
|
||||||
with:
|
|
||||||
name: mapping-${{ github.sha }}
|
|
||||||
path: app/build/outputs/mapping/release
|
|
||||||
125
.github/workflows/build_push.yml
vendored
125
.github/workflows/build_push.yml
vendored
@@ -1,125 +0,0 @@
|
|||||||
name: CI
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
tags:
|
|
||||||
- v*
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
name: Build app
|
|
||||||
runs-on: 'ubuntu-24.04'
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Clone repo
|
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
|
||||||
|
|
||||||
- name: Set up JDK
|
|
||||||
uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0
|
|
||||||
with:
|
|
||||||
java-version: 17
|
|
||||||
distribution: temurin
|
|
||||||
|
|
||||||
- name: Set up gradle
|
|
||||||
uses: gradle/actions/setup-gradle@94baf225fe0a508e581a564467443d0e2379123b # v4.3.0
|
|
||||||
|
|
||||||
- name: Check code format
|
|
||||||
run: ./gradlew spotlessCheck
|
|
||||||
|
|
||||||
- name: Build app
|
|
||||||
run: ./gradlew assembleRelease -Pinclude-telemetry -Penable-updater
|
|
||||||
|
|
||||||
- name: Run unit tests
|
|
||||||
run: ./gradlew testReleaseUnitTest
|
|
||||||
|
|
||||||
- name: Upload APK
|
|
||||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
|
||||||
with:
|
|
||||||
name: arm64-v8a-${{ github.sha }}
|
|
||||||
path: app/build/outputs/apk/release/app-arm64-v8a-release-unsigned.apk
|
|
||||||
|
|
||||||
- name: Upload mapping
|
|
||||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
|
||||||
with:
|
|
||||||
name: mapping-${{ github.sha }}
|
|
||||||
path: app/build/outputs/mapping/release
|
|
||||||
|
|
||||||
# Sign APK and create release for tags
|
|
||||||
|
|
||||||
- name: Get tag name
|
|
||||||
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'mihonapp/mihon'
|
|
||||||
run: |
|
|
||||||
set -x
|
|
||||||
echo "VERSION_TAG=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Sign APK
|
|
||||||
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'mihonapp/mihon'
|
|
||||||
uses: r0adkll/sign-android-release@f30bdd30588842ac76044ecdbd4b6d0e3e813478
|
|
||||||
with:
|
|
||||||
releaseDirectory: app/build/outputs/apk/release
|
|
||||||
signingKeyBase64: ${{ secrets.SIGNING_KEY }}
|
|
||||||
alias: ${{ secrets.ALIAS }}
|
|
||||||
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
|
|
||||||
keyPassword: ${{ secrets.KEY_PASSWORD }}
|
|
||||||
env:
|
|
||||||
BUILD_TOOLS_VERSION: '35.0.1'
|
|
||||||
|
|
||||||
- name: Clean up build artifacts
|
|
||||||
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'mihonapp/mihon'
|
|
||||||
run: |
|
|
||||||
set -e
|
|
||||||
|
|
||||||
mv app/build/outputs/apk/release/app-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
|
|
||||||
|
|
||||||
mv app/build/outputs/apk/release/app-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
|
|
||||||
|
|
||||||
mv app/build/outputs/apk/release/app-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
|
|
||||||
|
|
||||||
mv app/build/outputs/apk/release/app-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
|
|
||||||
|
|
||||||
mv app/build/outputs/apk/release/app-x86_64-release-unsigned-signed.apk mihon-x86_64-${{ env.VERSION_TAG }}.apk
|
|
||||||
sha=`sha256sum mihon-x86_64-${{ env.VERSION_TAG }}.apk | awk '{ print $1 }'`
|
|
||||||
echo "APK_X86_64_SHA=$sha" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Create Release
|
|
||||||
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'mihonapp/mihon'
|
|
||||||
uses: softprops/action-gh-release@c95fe1489396fe8a9eb87c0abf8aa5b2ef267fda # v2.2.1
|
|
||||||
with:
|
|
||||||
tag_name: ${{ env.VERSION_TAG }}
|
|
||||||
name: Mihon ${{ env.VERSION_TAG }}
|
|
||||||
body: |
|
|
||||||
---
|
|
||||||
|
|
||||||
### Checksums
|
|
||||||
|
|
||||||
| Variant | SHA-256 |
|
|
||||||
| ------- | ------- |
|
|
||||||
| Universal | ${{ env.APK_UNIVERSAL_SHA }}
|
|
||||||
| arm64-v8a | ${{ env.APK_ARM64_V8A_SHA }}
|
|
||||||
| armeabi-v7a | ${{ env.APK_ARMEABI_V7A_SHA }}
|
|
||||||
| x86 | ${{ env.APK_X86_SHA }} |
|
|
||||||
| x86_64 | ${{ env.APK_X86_64_SHA }} |
|
|
||||||
|
|
||||||
## If you are unsure which version to choose then go with mihon-${{ env.VERSION_TAG }}.apk
|
|
||||||
files: |
|
|
||||||
mihon-${{ env.VERSION_TAG }}.apk
|
|
||||||
mihon-arm64-v8a-${{ env.VERSION_TAG }}.apk
|
|
||||||
mihon-armeabi-v7a-${{ env.VERSION_TAG }}.apk
|
|
||||||
mihon-x86-${{ env.VERSION_TAG }}.apk
|
|
||||||
mihon-x86_64-${{ env.VERSION_TAG }}.apk
|
|
||||||
draft: true
|
|
||||||
prerelease: false
|
|
||||||
token: ${{ secrets.MIHON_BOT_TOKEN }}
|
|
||||||
171
.github/workflows/release.yml
vendored
Normal file
171
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
name: Release
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- v*
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
get_tag:
|
||||||
|
if: github.repository == 'mihonapp/mihon'
|
||||||
|
name: Extract tag name
|
||||||
|
runs-on: 'ubuntu-24.04'
|
||||||
|
outputs:
|
||||||
|
tag: ${{ steps.extract.outputs.tag }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Get tag name
|
||||||
|
id: extract
|
||||||
|
run: echo "tag=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
build:
|
||||||
|
if: github.repository == 'mihonapp/mihon'
|
||||||
|
name: Build
|
||||||
|
runs-on: 'ubuntu-24.04'
|
||||||
|
needs: get_tag
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
|
- name: Set up JDK
|
||||||
|
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||||
|
with:
|
||||||
|
java-version: 17
|
||||||
|
distribution: temurin
|
||||||
|
|
||||||
|
- name: Set up Gradle
|
||||||
|
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: ./gradlew assembleRelease -Pinclude-telemetry -Penable-updater
|
||||||
|
|
||||||
|
- name: Sign APK
|
||||||
|
uses: r0adkll/sign-android-release@f30bdd30588842ac76044ecdbd4b6d0e3e813478
|
||||||
|
with:
|
||||||
|
releaseDirectory: app/build/outputs/apk/release
|
||||||
|
signingKeyBase64: ${{ secrets.SIGNING_KEY }}
|
||||||
|
alias: ${{ secrets.ALIAS }}
|
||||||
|
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
|
||||||
|
keyPassword: ${{ secrets.KEY_PASSWORD }}
|
||||||
|
env:
|
||||||
|
BUILD_TOOLS_VERSION: '35.0.1'
|
||||||
|
|
||||||
|
- name: Rename APK
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
|
||||||
|
mv app/build/outputs/apk/release/app-universal-release-unsigned-signed.apk mihon-${{ needs.get_tag.outputs.tag }}.apk
|
||||||
|
mv app/build/outputs/apk/release/app-arm64-v8a-release-unsigned-signed.apk mihon-arm64-v8a-${{ needs.get_tag.outputs.tag }}.apk
|
||||||
|
mv app/build/outputs/apk/release/app-armeabi-v7a-release-unsigned-signed.apk mihon-armeabi-v7a-${{ needs.get_tag.outputs.tag }}.apk
|
||||||
|
mv app/build/outputs/apk/release/app-x86-release-unsigned-signed.apk mihon-x86-${{ needs.get_tag.outputs.tag }}.apk
|
||||||
|
mv app/build/outputs/apk/release/app-x86_64-release-unsigned-signed.apk mihon-x86_64-${{ needs.get_tag.outputs.tag }}.apk
|
||||||
|
|
||||||
|
- name: Upload APK
|
||||||
|
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||||
|
with:
|
||||||
|
name: mihon
|
||||||
|
path: |
|
||||||
|
mihon-${{ needs.get_tag.outputs.tag }}.apk
|
||||||
|
mihon-arm64-v8a-${{ needs.get_tag.outputs.tag }}.apk
|
||||||
|
mihon-armeabi-v7a-${{ needs.get_tag.outputs.tag }}.apk
|
||||||
|
mihon-x86-${{ needs.get_tag.outputs.tag }}.apk
|
||||||
|
mihon-x86_64-${{ needs.get_tag.outputs.tag }}.apk
|
||||||
|
|
||||||
|
build_foss:
|
||||||
|
if: github.repository == 'mihonapp/mihon'
|
||||||
|
name: Build (FOSS)
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
needs: get_tag
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
|
- name: Set up JDK
|
||||||
|
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||||
|
with:
|
||||||
|
java-version: 17
|
||||||
|
distribution: temurin
|
||||||
|
|
||||||
|
- name: Set up Gradle
|
||||||
|
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
|
||||||
|
with:
|
||||||
|
cache-disabled: true
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: ./gradlew assembleFoss -Penable-updater
|
||||||
|
|
||||||
|
- name: Sign APK
|
||||||
|
uses: r0adkll/sign-android-release@f30bdd30588842ac76044ecdbd4b6d0e3e813478
|
||||||
|
with:
|
||||||
|
releaseDirectory: app/build/outputs/apk/foss
|
||||||
|
signingKeyBase64: ${{ secrets.SIGNING_KEY }}
|
||||||
|
alias: ${{ secrets.ALIAS }}
|
||||||
|
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
|
||||||
|
keyPassword: ${{ secrets.KEY_PASSWORD }}
|
||||||
|
env:
|
||||||
|
BUILD_TOOLS_VERSION: '35.0.1'
|
||||||
|
|
||||||
|
- name: Rename APK
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
|
||||||
|
mv app/build/outputs/apk/foss/app-universal-foss-unsigned-signed.apk mihon-${{ needs.get_tag.outputs.tag }}-foss.apk
|
||||||
|
|
||||||
|
- name: Upload APK
|
||||||
|
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||||
|
with:
|
||||||
|
name: mihon-foss
|
||||||
|
path: mihon-${{ needs.get_tag.outputs.tag }}-foss.apk
|
||||||
|
|
||||||
|
release:
|
||||||
|
if: github.repository == 'mihonapp/mihon'
|
||||||
|
name: Create GitHub Release
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
needs: [get_tag, build, build_foss]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Download all artifacts
|
||||||
|
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||||
|
with:
|
||||||
|
merge-multiple: true
|
||||||
|
|
||||||
|
- name: Delete all artifacts
|
||||||
|
uses: geekyeggo/delete-artifact@f275313e70c08f6120db482d7a6b98377786765b # v5.1.0
|
||||||
|
with:
|
||||||
|
failOnError: false
|
||||||
|
name: |
|
||||||
|
mihon
|
||||||
|
mihon-foss
|
||||||
|
|
||||||
|
- name: Create GitHub Release
|
||||||
|
uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # v2.4.1
|
||||||
|
with:
|
||||||
|
tag_name: ${{ needs.get_tag.outputs.tag }}
|
||||||
|
name: Mihon ${{ needs.get_tag.outputs.tag }}
|
||||||
|
body: |
|
||||||
|
Check out the [past release notes](https://github.com/mihonapp/mihon/releases) if you’re upgrading from an earlier version. Consider [donating via Open Collective](https://opencollective.com/mihon/contribute) to help keep Mihon improving!
|
||||||
|
|
||||||
|
<!-->
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!-->
|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
|
>
|
||||||
|
> ### If you are unsure which version to download then go with `mihon-${{ needs.get_tag.outputs.tag }}.apk`
|
||||||
|
files: |
|
||||||
|
mihon-${{ needs.get_tag.outputs.tag }}.apk
|
||||||
|
mihon-${{ needs.get_tag.outputs.tag }}-foss.apk
|
||||||
|
mihon-arm64-v8a-${{ needs.get_tag.outputs.tag }}.apk
|
||||||
|
mihon-armeabi-v7a-${{ needs.get_tag.outputs.tag }}.apk
|
||||||
|
mihon-x86-${{ needs.get_tag.outputs.tag }}.apk
|
||||||
|
mihon-x86_64-${{ needs.get_tag.outputs.tag }}.apk
|
||||||
|
draft: true
|
||||||
|
prerelease: false
|
||||||
|
token: ${{ secrets.MIHON_BOT_TOKEN }}
|
||||||
128
CHANGELOG.md
128
CHANGELOG.md
@@ -10,7 +10,127 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co
|
|||||||
- `Fixed` - for any bug fixes.
|
- `Fixed` - for any bug fixes.
|
||||||
- `Other` - for technical stuff.
|
- `Other` - for technical stuff.
|
||||||
|
|
||||||
## [Unreleased]
|
## [v0.19.3] - 2025-11-07
|
||||||
|
### Improved
|
||||||
|
- Improved various aspects of the WebView multi window support ([@TheUnlocked](https://github.com/TheUnlocked)) ([#2662](https://github.com/mihonapp/mihon/pull/2662))
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
- Revert "Fix reader tap zones triggering after scrolling was stopped by the user" due to introduction of regression ([@AntsyLich](https://github.com/AntsyLich)) ([#2670](https://github.com/mihonapp/mihon/pull/2670))
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Fix WebView crash introduced in 0.19.2 ([@bapeey](https://github.com/bapeey)) ([#2649](https://github.com/mihonapp/mihon/pull/2649))
|
||||||
|
- Fix extra padding appearing in reader after user interactions ([@AntsyLich](https://github.com/AntsyLich)) ([#2669](https://github.com/mihonapp/mihon/pull/2669))
|
||||||
|
- Fix long strip reader not scrolling on consecutive taps ([@AntsyLich](https://github.com/AntsyLich)) ([#2670](https://github.com/mihonapp/mihon/pull/2670))
|
||||||
|
|
||||||
|
## [v0.19.2] - 2025-11-02
|
||||||
|
### Added
|
||||||
|
- Advanced setting to limit download filenames to ASCII characters. This is provided only as a workaround for OSes that do not properly handle standard Unicode filenames. This setting is generally not recommended and should only be used as a last resort ([@raxod502](https://github.com/radian-software)) ([#2305](https://github.com/mihonapp/mihon/pull/2305))
|
||||||
|
- Option to customize the number of concurrent source and page downloads ([@AntsyLich](https://github.com/AntsyLich)) ([#2637](https://github.com/mihonapp/mihon/pull/2637))
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Increased default concurrent page downloads to 5 ([@AntsyLich](https://github.com/AntsyLich)) ([#2637](https://github.com/mihonapp/mihon/pull/2637))
|
||||||
|
|
||||||
|
### Improved
|
||||||
|
- Spoofing of `X-Requested-With` header to support newer WebView versions ([@Guzmazow](https://github.com/Guzmazow)) ([#2491](https://github.com/mihonapp/mihon/pull/2491))
|
||||||
|
- Download support for chapters with the same metadata. Now a hash based on chapter's url is appended to download filename to tell them apart, letting you download both. Existing downloaded chapters will continue to work normally ([@raxod502](https://github.com/radian-software)) ([#2305](https://github.com/mihonapp/mihon/pull/2305))
|
||||||
|
- Auto refresh extension list whenever a repository is added or removed ([@c2y5](https://github.com/c2y5)) ([#2483](https://github.com/mihonapp/mihon/pull/2483))
|
||||||
|
- Added proper multi window support in WebView instead of treating everything as a redirect ([@TheUnlocked](https://github.com/TheUnlocked)) ([#2584](https://github.com/mihonapp/mihon/pull/2584))
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Fix height of description not being calculated correctly if images are present ([@Secozzi](https://github.com/Secozzi)) ([#2382](https://github.com/mihonapp/mihon/pull/2382))
|
||||||
|
- Fix migration progress not updating after manual search ([@Secozzi](https://github.com/Secozzi)) ([#2484](https://github.com/mihonapp/mihon/pull/2484))
|
||||||
|
- Fix category migration flag being ignored due to incorrect check against chapter flag ([@Secozzi](https://github.com/Secozzi)) ([#2484](https://github.com/mihonapp/mihon/pull/2484))
|
||||||
|
- Fix disabling incognito mode from notification ([@NGB-Was-Taken](https://github.com/NGB-Was-Taken)) ([#2512](https://github.com/mihonapp/mihon/pull/2512))
|
||||||
|
- Fix mass migration advanced search query building ([@AntsyLich](https://github.com/AntsyLich)) ([#2629](https://github.com/mihonapp/mihon/pull/2629))
|
||||||
|
- Fix migration dialog migrating to wrong entry ([@AntsyLich](https://github.com/AntsyLich)) ([#2631](https://github.com/mihonapp/mihon/pull/2631))
|
||||||
|
- Fix migration "Attempt to invoke virtual method" crash ([@AntsyLich](https://github.com/AntsyLich)) ([#2632](https://github.com/mihonapp/mihon/pull/2632))
|
||||||
|
- Fix reader "Unable to edit key" error ([@AntsyLich](https://github.com/AntsyLich)) ([#2634](https://github.com/mihonapp/mihon/pull/2634))
|
||||||
|
- Fix extension download stuck in pending state in some cases ([@c2y5](https://github.com/c2y5)) ([#2483](https://github.com/mihonapp/mihon/pull/2483))
|
||||||
|
- Fix scrollbar not showing when animator duration scale animation is turned off ([@anirudhsnayak](https://github.com/anirudhsnayak)) ([#2398](https://github.com/mihonapp/mihon/pull/2398))
|
||||||
|
- Fix date picker not allowing the same start and finish date in negative time zones ([@AntsyLich](https://github.com/AntsyLich), [@kashish-aggarwal21](https://github.com/kashish-aggarwal21)) ([#2617](https://github.com/mihonapp/mihon/pull/2617))
|
||||||
|
- Fix reader tap zones triggering after scrolling was stopped by the user ([@Naputt1](https://github.com/Naputt1), [@AntsyLich](https://github.com/AntsyLich)) ([#2518](https://github.com/mihonapp/mihon/pull/2518))
|
||||||
|
- Fix reader page indicator being partially visible on some devices ([@AntsyLich](https://github.com/AntsyLich)) ([#1908](https://github.com/mihonapp/mihon/pull/1908))
|
||||||
|
- Fix inconsistent system bar and reader app bar background ([@AntsyLich](https://github.com/AntsyLich)) ([#1908](https://github.com/mihonapp/mihon/pull/1908))
|
||||||
|
- Fix transparent system bar background in reader on Android 15+ ([@AntsyLich](https://github.com/AntsyLich)) ([#1908](https://github.com/mihonapp/mihon/pull/1908))
|
||||||
|
|
||||||
|
### Other
|
||||||
|
- Delegate Suwayomi tracker authentication to extension ([@cpiber](https://github.com/cpiber)) ([#2476](https://github.com/mihonapp/mihon/pull/2476))
|
||||||
|
- Fix Kitsu tracker to conform to tracker data structure properly ([@cpiber](https://github.com/cpiber)) ([#2609](https://github.com/mihonapp/mihon/pull/2609))
|
||||||
|
- Update Suwayomi tracker to use GraphQL API instead of REST API ([@cpiber](https://github.com/cpiber)) ([#2585](https://github.com/mihonapp/mihon/pull/2585))
|
||||||
|
|
||||||
|
## [v0.19.1] - 2025-08-07
|
||||||
|
### Changed
|
||||||
|
- LocalSource now reads ComicInfo.xml file for chapter (if available) to display chapter title, number and scanlator ([@raxod502](https://github.com/radian-software)) ([#2332](https://github.com/mihonapp/mihon/pull/2332))
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
- Predictive back support ([@AntsyLich](https://github.com/AntsyLich)) ([#2362](https://github.com/mihonapp/mihon/pull/2362))
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Fix scrollbar sometimes not showing during scroll or not reaching the bottom with few items ([@anirudhsnayak](https://github.com/anirudhsnayak)) ([#2304](https://github.com/mihonapp/mihon/pull/2304))
|
||||||
|
- Fix local source EPUB files not loading ([@AntsyLich](https://github.com/AntsyLich)) ([#2369](https://github.com/mihonapp/mihon/pull/2369))
|
||||||
|
- Fix title text color in light mode on mass migration list ([@AntsyLich](https://github.com/AntsyLich)) ([#2370](https://github.com/mihonapp/mihon/pull/2370))
|
||||||
|
- Fix 'Default' category showing in library with no user-added categories ([@AntsyLich](https://github.com/AntsyLich)) ([#2371](https://github.com/mihonapp/mihon/pull/2371))
|
||||||
|
- Fix crash when opening filter sheet with an empty library ([@krysanify](https://github.com/krysanify/)) ([#2355](https://github.com/mihonapp/mihon/pull/2355))
|
||||||
|
- Fix mark as read/unread not working for selected library items ([@krysanify](https://github.com/krysanify/)) ([#2355](https://github.com/mihonapp/mihon/pull/2355))
|
||||||
|
|
||||||
|
## [v0.19.0] - 2025-08-04
|
||||||
|
### Added
|
||||||
|
- Add more Kaomoji for empty/error screens ([@ianfhunter](https://github.com/ianfhunter/)) ([#1909](https://github.com/mihonapp/mihon/pull/1909))
|
||||||
|
- Add user manga notes ([@imkunet](https://github.com/imkunet), [@AntsyLich](https://github.com/AntsyLich)) ([#428](https://github.com/mihonapp/mihon/pull/428))
|
||||||
|
- Fix user notes not restoring when manga doesn't exist in DB ([@AntsyLich](https://github.com/AntsyLich)) ([#1945](https://github.com/mihonapp/mihon/pull/1945))
|
||||||
|
- Add markdown support for manga descriptions ([@Secozzi](https://github.com/Secozzi)) ([#1948](https://github.com/mihonapp/mihon/pull/1948))
|
||||||
|
- Use simpler markdown flavour ([@Secozzi](https://github.com/Secozzi)) ([#2000](https://github.com/mihonapp/mihon/pull/2000))
|
||||||
|
- Use Github markdown flavour for Github releases & fix bullet list alignment ([@Secozzi](https://github.com/Secozzi)) ([#2024](https://github.com/mihonapp/mihon/pull/2024))
|
||||||
|
- Add option to toggle image loading ([@Secozzi](https://github.com/Secozzi)) ([#2076](https://github.com/mihonapp/mihon/pull/2076))
|
||||||
|
- Add Nord Theme ([@Riztard](https://github.com/Riztard)) ([#1951](https://github.com/mihonapp/mihon/pull/1951))
|
||||||
|
- Option to keep read manga when clearing database ([@AwkwardPeak7](https://github.com/AwkwardPeak7)) ([#1979](https://github.com/mihonapp/mihon/pull/1979))
|
||||||
|
- Add advanced option to always update manga title from source ([@FlaminSarge](https://github.com/FlaminSarge)) ([#1182](https://github.com/mihonapp/mihon/pull/1182))
|
||||||
|
- Full predictive back support ([@AntsyLich](https://github.com/AntsyLich)) ([#2085](https://github.com/mihonapp/mihon/pull/2085))
|
||||||
|
- Add Catppuccin theme (mocha for dark and latte for light, mauve accent) ([@claymorwan](https://github.com/claymorwan/)) ([#2117](https://github.com/mihonapp/mihon/pull/2117))
|
||||||
|
- Manga mass migration ([@AntsyLich](https://github.com/AntsyLich), [@jobobby04](https://github.com/jobobby04)) ([#2110](https://github.com/mihonapp/mihon/pull/2110), [#2336](https://github.com/mihonapp/mihon/pull/2336), [#2338](https://github.com/mihonapp/mihon/pull/2338), [`f119386`](https://github.com/mihonapp/mihon/commit/f119386))
|
||||||
|
|
||||||
|
### Improved
|
||||||
|
- Significantly improve browsing speed (near instantaneous) ([@AntsyLich](https://github.com/AntsyLich)) ([#1946](https://github.com/mihonapp/mihon/pull/1946))
|
||||||
|
- Deduplicate entries when browsing ([@AntsyLich](https://github.com/AntsyLich)) ([#1957](https://github.com/mihonapp/mihon/pull/1957))
|
||||||
|
- Update non-library manga data when browsing ([@AntsyLich](https://github.com/AntsyLich)) ([#1967](https://github.com/mihonapp/mihon/pull/1967))
|
||||||
|
- Surface image loading error in Reader ([@AwkwardPeak7](https://github.com/AwkwardPeak7)) ([#1981](https://github.com/mihonapp/mihon/pull/1981))
|
||||||
|
- Include source headers when opening failed images from reader ([@AwkwardPeak7](https://github.com/AwkwardPeak7)) ([#2004](https://github.com/mihonapp/mihon/pull/2004))
|
||||||
|
- Added autofill support to tracker login dialog ([@AntsyLich](https://github.com/AntsyLich)) ([#2069](https://github.com/mihonapp/mihon/pull/2069))
|
||||||
|
- Added option to hide missing chapter count ([@User826](https://github.com/User826), [@AntsyLich](https://github.com/AntsyLich)) ([#2108](https://github.com/mihonapp/mihon/pull/2108))
|
||||||
|
- Use median to determine smart update interval, making it more resilient to long hiatuses ([@Kladki](https://github.com/Kladki)) ([#2251](https://github.com/mihonapp/mihon/pull/2251))
|
||||||
|
- Optimize library code to potentially better handle big user libraries ([@AntsyLich](https://github.com/AntsyLich)) ([#2329](https://github.com/mihonapp/mihon/pull/2329), [#2341](https://github.com/mihonapp/mihon/pull/2341))
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Display all similarly named duplicates in duplicate manga dialogue ([@NarwhalHorns](https://github.com/NarwhalHorns), [@AntsyLich](https://github.com/AntsyLich)) ([#1861](https://github.com/mihonapp/mihon/pull/1861))
|
||||||
|
- Display chapter count on items in duplicate manga dialogue ([@NarwhalHorns](https://github.com/NarwhalHorns)) ([#1963](https://github.com/mihonapp/mihon/pull/1963))
|
||||||
|
- Update Facebook and Reddit icons ([@Joehuu](https://github.com/Joehuu)) ([#1994](https://github.com/mihonapp/mihon/pull/1994))
|
||||||
|
- Switch default user agent to Android Chrome ([@AntsyLich](https://github.com/AntsyLich)) ([#2048](https://github.com/mihonapp/mihon/pull/2048))
|
||||||
|
- Changed log in button text when processing tracker login ([@AntsyLich](https://github.com/AntsyLich)) ([#2069](https://github.com/mihonapp/mihon/pull/2069))
|
||||||
|
- Disable reader's 'Keep screen on' setting by default ([@AntsyLich](https://github.com/AntsyLich)) ([#2095](https://github.com/mihonapp/mihon/pull/2095))
|
||||||
|
- Update manga without chapters even if restricted by source ([@AntsyLich](https://github.com/AntsyLich)) ([#2224](https://github.com/mihonapp/mihon/pull/224))
|
||||||
|
- Make local source default chapter sorting match file explorer behavior ([@AntsyLich](https://github.com/AntsyLich)) ([#2224](https://github.com/mihonapp/mihon/pull/224))
|
||||||
|
- Include Manga `initialized` status in backup ([@AwkwardPeak7](https://github.com/AwkwardPeak7)) ([#2285](https://github.com/mihonapp/mihon/pull/2285))
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Fix Bangumi search results including novels ([@MajorTanya](https://github.com/MajorTanya)) ([#1885](https://github.com/mihonapp/mihon/pull/1885))
|
||||||
|
- Fix next chapter button occasionally jumping to the last page of the current chapter ([@perokhe](https://github.com/perokhe)) ([#1920](https://github.com/mihonapp/mihon/pull/1920))
|
||||||
|
- Fix page number not appearing when opening chapter ([@perokhe](https://github.com/perokhe)) ([#1936](https://github.com/mihonapp/mihon/pull/1936))
|
||||||
|
- Fix backup sharing from notifications not working when app is in background ([@JaymanR](https://github.com/JaymanR))([#1929](https://github.com/mihonapp/mihon/pull/1929))
|
||||||
|
- Fix mark existing duplicate read chapters as read option not working in some cases ([@AntsyLich](https://github.com/AntsyLich)) ([#1944](https://github.com/mihonapp/mihon/pull/1944))
|
||||||
|
- Fix app bar action tooltips blocking clicks ([@Bartuzen](https://github.com/Bartuzen)) ([#1928](https://github.com/mihonapp/mihon/pull/1928))
|
||||||
|
- Fix unintended app permissions due to Firebase misconfiguration ([@AntsyLich](https://github.com/AntsyLich)) ([#1960](https://github.com/mihonapp/mihon/pull/1960))
|
||||||
|
- Fix navigation issue after migrating a duplicated entry from History tab ([@cuong-tran](https://github.com/cuong-tran)) ([#1980](https://github.com/mihonapp/mihon/pull/1980))
|
||||||
|
- Fix duplicate requests in WebView due to empty reasonPhrase ([@AwkwardPeak7](https://github.com/AwkwardPeak7)) ([#2003](https://github.com/mihonapp/mihon/pull/2003))
|
||||||
|
- Fix content under source browse screen top appbar is interactable ([@AntsyLich](https://github.com/AntsyLich)) ([#2026](https://github.com/mihonapp/mihon/pull/2026))
|
||||||
|
- Fix crash when trying use source sort filter without a pre-selection ([@AntsyLich](https://github.com/AntsyLich)) ([#2036](https://github.com/mihonapp/mihon/pull/2036))
|
||||||
|
- Fix empty layout not appearing in browse source screen in some cases ([@NarwhalHorns](https://github.com/NarwhalHorns)) ([#2043](https://github.com/mihonapp/mihon/pull/2043))
|
||||||
|
- Fix Pill not following the local text style ([@AntsyLich](https://github.com/AntsyLich)) ([`f8cb506`](https://github.com/mihonapp/mihon/commit/f8cb506))
|
||||||
|
- Fix downloader stopping after failing to create download directory of a manga ([@AntsyLich](https://github.com/AntsyLich)) ([#2068](https://github.com/mihonapp/mihon/pull/2068))
|
||||||
|
- Fix pressing `Enter` while searching also triggering navigation back on physical keyboards ([@AwkwardPeak7](https://github.com/AwkwardPeak7)) ([#2077](https://github.com/mihonapp/mihon/pull/2077))
|
||||||
|
- Ensure app waits for Cloudflare challenge to complete before continuing ([@AwkwardPeak7](https://github.com/AwkwardPeak7)) ([#2200](https://github.com/mihonapp/mihon/pull/2200))
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
- Remove Okhttp networking from WebView Screen ([@AwkwardPeak7](https://github.com/AwkwardPeak7)) ([#2020](https://github.com/mihonapp/mihon/pull/2020))
|
||||||
|
|
||||||
## [v0.18.0] - 2025-03-20
|
## [v0.18.0] - 2025-03-20
|
||||||
### Added
|
### Added
|
||||||
@@ -323,7 +443,11 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co
|
|||||||
- Branding to Mihon ([@AntsyLich](https://github.com/AntsyLich))
|
- 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))
|
- 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.18.0...main
|
[unreleased]: https://github.com/mihonapp/mihon/compare/v0.19.3...main
|
||||||
|
[v0.19.3]: https://github.com/mihonapp/mihon/compare/v0.19.2...v0.19.3
|
||||||
|
[v0.19.2]: https://github.com/mihonapp/mihon/compare/v0.19.1...v0.19.2
|
||||||
|
[v0.19.1]: https://github.com/mihonapp/mihon/compare/v0.19.0...v0.19.1
|
||||||
|
[v0.19.0]: https://github.com/mihonapp/mihon/compare/v0.18.0...v0.19.0
|
||||||
[v0.18.0]: https://github.com/mihonapp/mihon/compare/v0.17.1...v0.18.0
|
[v0.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.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.17.0]: https://github.com/mihonapp/mihon/compare/v0.16.5...v0.17.0
|
||||||
|
|||||||
@@ -10,16 +10,16 @@
|
|||||||
Discover and read manga, webtoons, comics, and more – easier than ever on your Android device.
|
Discover and read manga, webtoons, comics, and more – easier than ever on your Android device.
|
||||||
|
|
||||||
[](https://discord.gg/mihon)
|
[](https://discord.gg/mihon)
|
||||||
[](https://github.com/mihonapp/mihon/releases)
|
[](https://mihon.app/download)
|
||||||
|
|
||||||
[](https://github.com/mihonapp/mihon/actions/workflows/build_push.yml)
|
[](https://github.com/mihonapp/mihon/actions/workflows/build_push.yml)
|
||||||
[](/LICENSE)
|
[](/LICENSE)
|
||||||
[](https://hosted.weblate.org/engage/mihon/)
|
[](https://hosted.weblate.org/engage/mihon/)
|
||||||
|
|
||||||
## Download
|
## Download
|
||||||
|
|
||||||
[](https://github.com/mihonapp/mihon/releases)
|
[](https://mihon.app/download)
|
||||||
[](https://github.com/mihonapp/mihon-preview/releases)
|
[](https://mihon.app/download)
|
||||||
|
|
||||||
*Requires Android 8.0 or higher.*
|
*Requires Android 8.0 or higher.*
|
||||||
|
|
||||||
|
|||||||
@@ -26,8 +26,8 @@ android {
|
|||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "app.mihon"
|
applicationId = "app.mihon"
|
||||||
|
|
||||||
versionCode = 11
|
versionCode = 16
|
||||||
versionName = "0.18.0"
|
versionName = "0.19.3"
|
||||||
|
|
||||||
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
||||||
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
|
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
|
||||||
@@ -138,9 +138,9 @@ android {
|
|||||||
buildFeatures {
|
buildFeatures {
|
||||||
viewBinding = true
|
viewBinding = true
|
||||||
buildConfig = true
|
buildConfig = true
|
||||||
|
aidl = true
|
||||||
|
|
||||||
// Disable some unused things
|
// Disable some unused things
|
||||||
aidl = false
|
|
||||||
renderScript = false
|
renderScript = false
|
||||||
shaders = false
|
shaders = false
|
||||||
}
|
}
|
||||||
@@ -261,8 +261,7 @@ dependencies {
|
|||||||
implementation(libs.directionalviewpager) {
|
implementation(libs.directionalviewpager) {
|
||||||
exclude(group = "androidx.viewpager", module = "viewpager")
|
exclude(group = "androidx.viewpager", module = "viewpager")
|
||||||
}
|
}
|
||||||
implementation(libs.insetter)
|
implementation(libs.richeditor.compose)
|
||||||
implementation(libs.bundles.richtext)
|
|
||||||
implementation(libs.aboutLibraries.compose)
|
implementation(libs.aboutLibraries.compose)
|
||||||
implementation(libs.bundles.voyager)
|
implementation(libs.bundles.voyager)
|
||||||
implementation(libs.compose.materialmotion)
|
implementation(libs.compose.materialmotion)
|
||||||
@@ -270,6 +269,7 @@ dependencies {
|
|||||||
implementation(libs.compose.webview)
|
implementation(libs.compose.webview)
|
||||||
implementation(libs.compose.grid)
|
implementation(libs.compose.grid)
|
||||||
implementation(libs.reorderable)
|
implementation(libs.reorderable)
|
||||||
|
implementation(libs.bundles.markdown)
|
||||||
|
|
||||||
// Logging
|
// Logging
|
||||||
implementation(libs.logcat)
|
implementation(libs.logcat)
|
||||||
@@ -277,8 +277,12 @@ dependencies {
|
|||||||
// Shizuku
|
// Shizuku
|
||||||
implementation(libs.bundles.shizuku)
|
implementation(libs.bundles.shizuku)
|
||||||
|
|
||||||
|
// String similarity
|
||||||
|
implementation(libs.stringSimilarity)
|
||||||
|
|
||||||
// Tests
|
// Tests
|
||||||
testImplementation(libs.bundles.test)
|
testImplementation(libs.bundles.test)
|
||||||
|
testRuntimeOnly(libs.junit.platform.launcher)
|
||||||
|
|
||||||
// For detecting memory leaks; see https://square.github.io/leakcanary/
|
// For detecting memory leaks; see https://square.github.io/leakcanary/
|
||||||
// debugImplementation(libs.leakcanary.android)
|
// debugImplementation(libs.leakcanary.android)
|
||||||
@@ -288,14 +292,6 @@ dependencies {
|
|||||||
}
|
}
|
||||||
|
|
||||||
androidComponents {
|
androidComponents {
|
||||||
beforeVariants { variantBuilder ->
|
|
||||||
// Disables standardBenchmark
|
|
||||||
if (variantBuilder.buildType == "benchmark") {
|
|
||||||
variantBuilder.enable = variantBuilder.productFlavors.containsAll(
|
|
||||||
listOf("default" to "dev"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onVariants(selector().withFlavor("default" to "standard")) {
|
onVariants(selector().withFlavor("default" to "standard")) {
|
||||||
// Only excluding in standard flavor because this breaks
|
// Only excluding in standard flavor because this breaks
|
||||||
// Layout Inspector's Compose tree
|
// Layout Inspector's Compose tree
|
||||||
|
|||||||
7
app/src/main/aidl/mihon/app/shizuku/IShellInterface.aidl
Normal file
7
app/src/main/aidl/mihon/app/shizuku/IShellInterface.aidl
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
package mihon.app.shizuku;
|
||||||
|
|
||||||
|
interface IShellInterface {
|
||||||
|
void install(in AssetFileDescriptor apk) = 1;
|
||||||
|
|
||||||
|
void destroy() = 16777114;
|
||||||
|
}
|
||||||
@@ -35,6 +35,7 @@ import mihon.domain.extensionrepo.interactor.ReplaceExtensionRepo
|
|||||||
import mihon.domain.extensionrepo.interactor.UpdateExtensionRepo
|
import mihon.domain.extensionrepo.interactor.UpdateExtensionRepo
|
||||||
import mihon.domain.extensionrepo.repository.ExtensionRepoRepository
|
import mihon.domain.extensionrepo.repository.ExtensionRepoRepository
|
||||||
import mihon.domain.extensionrepo.service.ExtensionRepoService
|
import mihon.domain.extensionrepo.service.ExtensionRepoService
|
||||||
|
import mihon.domain.migration.usecases.MigrateMangaUseCase
|
||||||
import mihon.domain.upcoming.interactor.GetUpcomingManga
|
import mihon.domain.upcoming.interactor.GetUpcomingManga
|
||||||
import tachiyomi.data.category.CategoryRepositoryImpl
|
import tachiyomi.data.category.CategoryRepositoryImpl
|
||||||
import tachiyomi.data.chapter.ChapterRepositoryImpl
|
import tachiyomi.data.chapter.ChapterRepositoryImpl
|
||||||
@@ -79,6 +80,7 @@ import tachiyomi.domain.manga.interactor.GetMangaWithChapters
|
|||||||
import tachiyomi.domain.manga.interactor.NetworkToLocalManga
|
import tachiyomi.domain.manga.interactor.NetworkToLocalManga
|
||||||
import tachiyomi.domain.manga.interactor.ResetViewerFlags
|
import tachiyomi.domain.manga.interactor.ResetViewerFlags
|
||||||
import tachiyomi.domain.manga.interactor.SetMangaChapterFlags
|
import tachiyomi.domain.manga.interactor.SetMangaChapterFlags
|
||||||
|
import tachiyomi.domain.manga.interactor.UpdateMangaNotes
|
||||||
import tachiyomi.domain.manga.repository.MangaRepository
|
import tachiyomi.domain.manga.repository.MangaRepository
|
||||||
import tachiyomi.domain.release.interactor.GetApplicationRelease
|
import tachiyomi.domain.release.interactor.GetApplicationRelease
|
||||||
import tachiyomi.domain.release.service.ReleaseService
|
import tachiyomi.domain.release.service.ReleaseService
|
||||||
@@ -129,9 +131,15 @@ class DomainModule : InjektModule {
|
|||||||
addFactory { SetMangaViewerFlags(get()) }
|
addFactory { SetMangaViewerFlags(get()) }
|
||||||
addFactory { NetworkToLocalManga(get()) }
|
addFactory { NetworkToLocalManga(get()) }
|
||||||
addFactory { UpdateManga(get(), get()) }
|
addFactory { UpdateManga(get(), get()) }
|
||||||
|
addFactory { UpdateMangaNotes(get()) }
|
||||||
addFactory { SetMangaCategories(get()) }
|
addFactory { SetMangaCategories(get()) }
|
||||||
addFactory { GetExcludedScanlators(get()) }
|
addFactory { GetExcludedScanlators(get()) }
|
||||||
addFactory { SetExcludedScanlators(get()) }
|
addFactory { SetExcludedScanlators(get()) }
|
||||||
|
addFactory {
|
||||||
|
MigrateMangaUseCase(
|
||||||
|
get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
addSingletonFactory<ReleaseService> { ReleaseServiceImpl(get(), get()) }
|
addSingletonFactory<ReleaseService> { ReleaseServiceImpl(get(), get()) }
|
||||||
addFactory { GetApplicationRelease(get(), get()) }
|
addFactory { GetApplicationRelease(get(), get()) }
|
||||||
|
|||||||
@@ -114,6 +114,7 @@ class SyncChaptersWithSource(
|
|||||||
downloadManager.isChapterDownloaded(
|
downloadManager.isChapterDownloaded(
|
||||||
dbChapter.name,
|
dbChapter.name,
|
||||||
dbChapter.scanlator,
|
dbChapter.scanlator,
|
||||||
|
dbChapter.url,
|
||||||
manga.title,
|
manga.title,
|
||||||
manga.source,
|
manga.source,
|
||||||
)
|
)
|
||||||
@@ -121,12 +122,14 @@ class SyncChaptersWithSource(
|
|||||||
if (shouldRenameChapter) {
|
if (shouldRenameChapter) {
|
||||||
downloadManager.renameChapter(source, manga, dbChapter, chapter)
|
downloadManager.renameChapter(source, manga, dbChapter, chapter)
|
||||||
}
|
}
|
||||||
|
|
||||||
var toChangeChapter = dbChapter.copy(
|
var toChangeChapter = dbChapter.copy(
|
||||||
name = chapter.name,
|
name = chapter.name,
|
||||||
chapterNumber = chapter.chapterNumber,
|
chapterNumber = chapter.chapterNumber,
|
||||||
scanlator = chapter.scanlator,
|
scanlator = chapter.scanlator,
|
||||||
sourceOrder = chapter.sourceOrder,
|
sourceOrder = chapter.sourceOrder,
|
||||||
)
|
)
|
||||||
|
|
||||||
if (chapter.dateUpload != 0L) {
|
if (chapter.dateUpload != 0L) {
|
||||||
toChangeChapter = toChangeChapter.copy(dateUpload = chapter.dateUpload)
|
toChangeChapter = toChangeChapter.copy(dateUpload = chapter.dateUpload)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ fun List<Chapter>.applyFilters(manga: Manga, downloadManager: DownloadManager):
|
|||||||
val downloaded = downloadManager.isChapterDownloaded(
|
val downloaded = downloadManager.isChapterDownloaded(
|
||||||
chapter.name,
|
chapter.name,
|
||||||
chapter.scanlator,
|
chapter.scanlator,
|
||||||
|
chapter.url,
|
||||||
manga.title,
|
manga.title,
|
||||||
manga.source,
|
manga.source,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ package eu.kanade.domain.manga.interactor
|
|||||||
|
|
||||||
import eu.kanade.domain.manga.model.hasCustomCover
|
import eu.kanade.domain.manga.model.hasCustomCover
|
||||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||||
|
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
import tachiyomi.domain.library.service.LibraryPreferences
|
||||||
import tachiyomi.domain.manga.interactor.FetchInterval
|
import tachiyomi.domain.manga.interactor.FetchInterval
|
||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
import tachiyomi.domain.manga.model.MangaUpdate
|
import tachiyomi.domain.manga.model.MangaUpdate
|
||||||
@@ -31,6 +33,8 @@ class UpdateManga(
|
|||||||
remoteManga: SManga,
|
remoteManga: SManga,
|
||||||
manualFetch: Boolean,
|
manualFetch: Boolean,
|
||||||
coverCache: CoverCache = Injekt.get(),
|
coverCache: CoverCache = Injekt.get(),
|
||||||
|
libraryPreferences: LibraryPreferences = Injekt.get(),
|
||||||
|
downloadManager: DownloadManager = Injekt.get(),
|
||||||
): Boolean {
|
): Boolean {
|
||||||
val remoteTitle = try {
|
val remoteTitle = try {
|
||||||
remoteManga.title
|
remoteManga.title
|
||||||
@@ -38,8 +42,13 @@ class UpdateManga(
|
|||||||
""
|
""
|
||||||
}
|
}
|
||||||
|
|
||||||
// if the manga isn't a favorite, set its title from source and update in db
|
// 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.isEmpty() || localManga.favorite) null else remoteTitle
|
val title =
|
||||||
|
if (remoteTitle.isNotEmpty() && (!localManga.favorite || libraryPreferences.updateMangaTitles().get())) {
|
||||||
|
remoteTitle
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
val coverLastModified =
|
val coverLastModified =
|
||||||
when {
|
when {
|
||||||
@@ -59,7 +68,7 @@ class UpdateManga(
|
|||||||
|
|
||||||
val thumbnailUrl = remoteManga.thumbnail_url?.takeIf { it.isNotEmpty() }
|
val thumbnailUrl = remoteManga.thumbnail_url?.takeIf { it.isNotEmpty() }
|
||||||
|
|
||||||
return mangaRepository.update(
|
val success = mangaRepository.update(
|
||||||
MangaUpdate(
|
MangaUpdate(
|
||||||
id = localManga.id,
|
id = localManga.id,
|
||||||
title = title,
|
title = title,
|
||||||
@@ -74,6 +83,10 @@ class UpdateManga(
|
|||||||
initialized = true,
|
initialized = true,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
if (success && title != null) {
|
||||||
|
downloadManager.renameManga(localManga, title)
|
||||||
|
}
|
||||||
|
return success
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun awaitUpdateFetchInterval(
|
suspend fun awaitUpdateFetchInterval(
|
||||||
|
|||||||
@@ -69,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 {
|
fun Manga.hasCustomCover(coverCache: CoverCache = Injekt.get()): Boolean {
|
||||||
return coverCache.getCustomCoverFile(id).exists()
|
return coverCache.getCustomCoverFile(id).exists()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,16 +2,18 @@ package eu.kanade.domain.source.service
|
|||||||
|
|
||||||
import eu.kanade.domain.source.interactor.SetMigrateSorting
|
import eu.kanade.domain.source.interactor.SetMigrateSorting
|
||||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||||
|
import mihon.domain.migration.models.MigrationFlag
|
||||||
import tachiyomi.core.common.preference.Preference
|
import tachiyomi.core.common.preference.Preference
|
||||||
import tachiyomi.core.common.preference.PreferenceStore
|
import tachiyomi.core.common.preference.PreferenceStore
|
||||||
import tachiyomi.core.common.preference.getEnum
|
import tachiyomi.core.common.preference.getEnum
|
||||||
|
import tachiyomi.core.common.preference.getLongArray
|
||||||
import tachiyomi.domain.library.model.LibraryDisplayMode
|
import tachiyomi.domain.library.model.LibraryDisplayMode
|
||||||
|
|
||||||
class SourcePreferences(
|
class SourcePreferences(
|
||||||
private val preferenceStore: PreferenceStore,
|
private val preferenceStore: PreferenceStore,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun sourceDisplayMode() = preferenceStore.getObject(
|
fun sourceDisplayMode() = preferenceStore.getObjectFromString(
|
||||||
"pref_display_mode_catalogue",
|
"pref_display_mode_catalogue",
|
||||||
LibraryDisplayMode.default,
|
LibraryDisplayMode.default,
|
||||||
LibraryDisplayMode.Serializer::serialize,
|
LibraryDisplayMode.Serializer::serialize,
|
||||||
@@ -55,4 +57,21 @@ class SourcePreferences(
|
|||||||
Preference.appStateKey("has_filters_toggle_state"),
|
Preference.appStateKey("has_filters_toggle_state"),
|
||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
fun migrationSources() = preferenceStore.getLongArray("migration_sources", emptyList())
|
||||||
|
|
||||||
|
fun migrationFlags() = preferenceStore.getObjectFromInt(
|
||||||
|
key = "migration_flags",
|
||||||
|
defaultValue = MigrationFlag.entries.toSet(),
|
||||||
|
serializer = { MigrationFlag.toBit(it) },
|
||||||
|
deserializer = { value: Int -> MigrationFlag.fromBit(value) },
|
||||||
|
)
|
||||||
|
|
||||||
|
fun migrationDeepSearchMode() = preferenceStore.getBoolean("migration_deep_search", false)
|
||||||
|
|
||||||
|
fun migrationPrioritizeByChapters() = preferenceStore.getBoolean("migration_prioritize_by_chapters", false)
|
||||||
|
|
||||||
|
fun migrationHideUnmatched() = preferenceStore.getBoolean("migration_hide_unmatched", false)
|
||||||
|
|
||||||
|
fun migrationHideWithoutUpdates() = preferenceStore.getBoolean("migration_hide_without_updates", false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ class UiPreferences(
|
|||||||
|
|
||||||
fun tabletUiMode() = preferenceStore.getEnum("tablet_ui_mode", TabletUiMode.AUTOMATIC)
|
fun tabletUiMode() = preferenceStore.getEnum("tablet_ui_mode", TabletUiMode.AUTOMATIC)
|
||||||
|
|
||||||
|
fun imagesInDescription() = preferenceStore.getBoolean("pref_render_images_description", true)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun dateFormat(format: String): DateTimeFormatter = when (format) {
|
fun dateFormat(format: String): DateTimeFormatter = when (format) {
|
||||||
"" -> DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT)
|
"" -> DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT)
|
||||||
|
|||||||
@@ -1,18 +1,16 @@
|
|||||||
package eu.kanade.domain.ui.model
|
package eu.kanade.domain.ui.model
|
||||||
|
|
||||||
import dev.icerock.moko.resources.StringResource
|
import dev.icerock.moko.resources.StringResource
|
||||||
import eu.kanade.tachiyomi.util.system.isReleaseBuildType
|
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
|
|
||||||
enum class AppTheme(val titleRes: StringResource?) {
|
enum class AppTheme(val titleRes: StringResource?) {
|
||||||
DEFAULT(MR.strings.label_default),
|
DEFAULT(MR.strings.label_default),
|
||||||
MONET(MR.strings.theme_monet),
|
MONET(MR.strings.theme_monet),
|
||||||
|
CATPPUCCIN(MR.strings.theme_catppuccin),
|
||||||
GREEN_APPLE(MR.strings.theme_greenapple),
|
GREEN_APPLE(MR.strings.theme_greenapple),
|
||||||
LAVENDER(MR.strings.theme_lavender),
|
LAVENDER(MR.strings.theme_lavender),
|
||||||
MIDNIGHT_DUSK(MR.strings.theme_midnightdusk),
|
MIDNIGHT_DUSK(MR.strings.theme_midnightdusk),
|
||||||
|
NORD(MR.strings.theme_nord),
|
||||||
// TODO: re-enable for preview
|
|
||||||
NORD(MR.strings.theme_nord.takeUnless { isReleaseBuildType }),
|
|
||||||
STRAWBERRY_DAIQUIRI(MR.strings.theme_strawberrydaiquiri),
|
STRAWBERRY_DAIQUIRI(MR.strings.theme_strawberrydaiquiri),
|
||||||
TAKO(MR.strings.theme_tako),
|
TAKO(MR.strings.theme_tako),
|
||||||
TEALTURQUOISE(MR.strings.theme_tealturquoise),
|
TEALTURQUOISE(MR.strings.theme_tealturquoise),
|
||||||
|
|||||||
@@ -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(
|
EmptyScreen(
|
||||||
modifier = Modifier.padding(contentPadding),
|
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) {
|
actions = if (source is LocalSource) {
|
||||||
persistentListOf(
|
persistentListOf(
|
||||||
EmptyScreenAction(
|
EmptyScreenAction(
|
||||||
@@ -109,13 +117,6 @@ fun BrowseSourceContent(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mangaList.itemCount == 0 && mangaList.loadState.refresh is LoadState.Loading) {
|
|
||||||
LoadingScreen(
|
|
||||||
modifier = Modifier.padding(contentPadding),
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
when (displayMode) {
|
when (displayMode) {
|
||||||
LibraryDisplayMode.ComfortableGrid -> {
|
LibraryDisplayMode.ComfortableGrid -> {
|
||||||
BrowseSourceComfortableGrid(
|
BrowseSourceComfortableGrid(
|
||||||
|
|||||||
@@ -353,13 +353,17 @@ private fun ExtensionItemContent(
|
|||||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
|
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
|
||||||
) {
|
) {
|
||||||
ProvideTextStyle(value = MaterialTheme.typography.bodySmall) {
|
ProvideTextStyle(value = MaterialTheme.typography.bodySmall) {
|
||||||
|
var hasAlreadyShownAnElement by remember { mutableStateOf(false) }
|
||||||
if (extension is Extension.Installed && extension.lang.isNotEmpty()) {
|
if (extension is Extension.Installed && extension.lang.isNotEmpty()) {
|
||||||
|
hasAlreadyShownAnElement = true
|
||||||
Text(
|
Text(
|
||||||
text = LocaleHelper.getSourceDisplayName(extension.lang, LocalContext.current),
|
text = LocaleHelper.getSourceDisplayName(extension.lang, LocalContext.current),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (extension.versionName.isNotEmpty()) {
|
if (extension.versionName.isNotEmpty()) {
|
||||||
|
if (hasAlreadyShownAnElement) DotSeparatorNoSpaceText()
|
||||||
|
hasAlreadyShownAnElement = true
|
||||||
Text(
|
Text(
|
||||||
text = extension.versionName,
|
text = extension.versionName,
|
||||||
)
|
)
|
||||||
@@ -372,6 +376,8 @@ private fun ExtensionItemContent(
|
|||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
if (warning != null) {
|
if (warning != null) {
|
||||||
|
if (hasAlreadyShownAnElement) DotSeparatorNoSpaceText()
|
||||||
|
hasAlreadyShownAnElement = true
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(warning).uppercase(),
|
text = stringResource(warning).uppercase(),
|
||||||
color = MaterialTheme.colorScheme.error,
|
color = MaterialTheme.colorScheme.error,
|
||||||
@@ -379,6 +385,12 @@ private fun ExtensionItemContent(
|
|||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if (extension is Extension.Installed && !extension.isShared) {
|
||||||
|
if (hasAlreadyShownAnElement) DotSeparatorNoSpaceText()
|
||||||
|
Text(
|
||||||
|
text = stringResource(MR.strings.ext_installer_private),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (!installStep.isCompleted()) {
|
if (!installStep.isCompleted()) {
|
||||||
DotSeparatorNoSpaceText()
|
DotSeparatorNoSpaceText()
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ fun GlobalSearchScreen(
|
|||||||
navigateUp = navigateUp,
|
navigateUp = navigateUp,
|
||||||
onChangeSearchQuery = onChangeSearchQuery,
|
onChangeSearchQuery = onChangeSearchQuery,
|
||||||
onSearch = onSearch,
|
onSearch = onSearch,
|
||||||
|
hideSourceFilter = false,
|
||||||
sourceFilter = state.sourceFilter,
|
sourceFilter = state.sourceFilter,
|
||||||
onChangeSearchFilter = onChangeSearchFilter,
|
onChangeSearchFilter = onChangeSearchFilter,
|
||||||
onlyShowHasResults = state.onlyShowHasResults,
|
onlyShowHasResults = state.onlyShowHasResults,
|
||||||
|
|||||||
@@ -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,
|
navigateUp = navigateUp,
|
||||||
onChangeSearchQuery = onChangeSearchQuery,
|
onChangeSearchQuery = onChangeSearchQuery,
|
||||||
onSearch = onSearch,
|
onSearch = onSearch,
|
||||||
|
hideSourceFilter = true,
|
||||||
sourceFilter = state.sourceFilter,
|
sourceFilter = state.sourceFilter,
|
||||||
onChangeSearchFilter = onChangeSearchFilter,
|
onChangeSearchFilter = onChangeSearchFilter,
|
||||||
onlyShowHasResults = state.onlyShowHasResults,
|
onlyShowHasResults = state.onlyShowHasResults,
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ fun GlobalSearchToolbar(
|
|||||||
navigateUp: () -> Unit,
|
navigateUp: () -> Unit,
|
||||||
onChangeSearchQuery: (String?) -> Unit,
|
onChangeSearchQuery: (String?) -> Unit,
|
||||||
onSearch: (String) -> Unit,
|
onSearch: (String) -> Unit,
|
||||||
|
hideSourceFilter: Boolean,
|
||||||
sourceFilter: SourceFilter,
|
sourceFilter: SourceFilter,
|
||||||
onChangeSearchFilter: (SourceFilter) -> Unit,
|
onChangeSearchFilter: (SourceFilter) -> Unit,
|
||||||
onlyShowHasResults: Boolean,
|
onlyShowHasResults: Boolean,
|
||||||
@@ -73,6 +74,7 @@ fun GlobalSearchToolbar(
|
|||||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
||||||
) {
|
) {
|
||||||
// TODO: make this UX better; it only applies when triggering a new search
|
// TODO: make this UX better; it only applies when triggering a new search
|
||||||
|
if (!hideSourceFilter) {
|
||||||
FilterChip(
|
FilterChip(
|
||||||
selected = sourceFilter == SourceFilter.PinnedOnly,
|
selected = sourceFilter == SourceFilter.PinnedOnly,
|
||||||
onClick = { onChangeSearchFilter(SourceFilter.PinnedOnly) },
|
onClick = { onChangeSearchFilter(SourceFilter.PinnedOnly) },
|
||||||
@@ -105,6 +107,7 @@ fun GlobalSearchToolbar(
|
|||||||
)
|
)
|
||||||
|
|
||||||
VerticalDivider()
|
VerticalDivider()
|
||||||
|
}
|
||||||
|
|
||||||
FilterChip(
|
FilterChip(
|
||||||
selected = onlyShowHasResults,
|
selected = onlyShowHasResults,
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package eu.kanade.presentation.components
|
package eu.kanade.presentation.components
|
||||||
|
|
||||||
import androidx.activity.compose.BackHandler
|
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
@@ -28,8 +27,8 @@ fun NavigatorAdaptiveSheet(
|
|||||||
screen = screen,
|
screen = screen,
|
||||||
content = { sheetNavigator ->
|
content = { sheetNavigator ->
|
||||||
AdaptiveSheet(
|
AdaptiveSheet(
|
||||||
enableSwipeDismiss = enableSwipeDismiss(sheetNavigator),
|
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
|
enableSwipeDismiss = enableSwipeDismiss(sheetNavigator),
|
||||||
) {
|
) {
|
||||||
ScreenTransition(
|
ScreenTransition(
|
||||||
navigator = sheetNavigator,
|
navigator = sheetNavigator,
|
||||||
@@ -38,11 +37,6 @@ fun NavigatorAdaptiveSheet(
|
|||||||
fadeOut(animationSpec = tween(90))
|
fadeOut(animationSpec = tween(90))
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
BackHandler(
|
|
||||||
enabled = sheetNavigator.size > 1,
|
|
||||||
onBack = sheetNavigator::pop,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure screens are disposed no matter what
|
// Make sure screens are disposed no matter what
|
||||||
@@ -79,10 +73,10 @@ fun AdaptiveSheet(
|
|||||||
properties = dialogProperties,
|
properties = dialogProperties,
|
||||||
) {
|
) {
|
||||||
AdaptiveSheetImpl(
|
AdaptiveSheetImpl(
|
||||||
modifier = modifier,
|
|
||||||
isTabletUi = isTabletUi,
|
isTabletUi = isTabletUi,
|
||||||
enableSwipeDismiss = enableSwipeDismiss,
|
enableSwipeDismiss = enableSwipeDismiss,
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
|
modifier = modifier,
|
||||||
) {
|
) {
|
||||||
content()
|
content()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import androidx.compose.runtime.mutableStateOf
|
|||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.focus.FocusDirection
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
@@ -201,6 +202,7 @@ fun AppBarActions(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
state = rememberTooltipState(),
|
state = rememberTooltipState(),
|
||||||
|
focusable = false,
|
||||||
) {
|
) {
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = it.onClick,
|
onClick = it.onClick,
|
||||||
@@ -225,6 +227,7 @@ fun AppBarActions(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
state = rememberTooltipState(),
|
state = rememberTooltipState(),
|
||||||
|
focusable = false,
|
||||||
) {
|
) {
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = { showMenu = !showMenu },
|
onClick = { showMenu = !showMenu },
|
||||||
@@ -289,6 +292,7 @@ fun SearchToolbar(
|
|||||||
onSearch(searchQuery)
|
onSearch(searchQuery)
|
||||||
focusManager.clearFocus()
|
focusManager.clearFocus()
|
||||||
keyboardController?.hide()
|
keyboardController?.hide()
|
||||||
|
focusManager.moveFocus(FocusDirection.Next)
|
||||||
}
|
}
|
||||||
|
|
||||||
BasicTextField(
|
BasicTextField(
|
||||||
@@ -352,6 +356,7 @@ fun SearchToolbar(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
state = rememberTooltipState(),
|
state = rememberTooltipState(),
|
||||||
|
focusable = false,
|
||||||
) {
|
) {
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
@@ -371,6 +376,7 @@ fun SearchToolbar(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
state = rememberTooltipState(),
|
state = rememberTooltipState(),
|
||||||
|
focusable = false,
|
||||||
) {
|
) {
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
package eu.kanade.presentation.components
|
package eu.kanade.presentation.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.DpOffset
|
||||||
import eu.kanade.presentation.manga.DownloadAction
|
import eu.kanade.presentation.manga.DownloadAction
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
@@ -15,7 +17,41 @@ fun DownloadDropdownMenu(
|
|||||||
expanded: Boolean,
|
expanded: Boolean,
|
||||||
onDismissRequest: () -> Unit,
|
onDismissRequest: () -> Unit,
|
||||||
onDownloadClicked: (DownloadAction) -> Unit,
|
onDownloadClicked: (DownloadAction) -> Unit,
|
||||||
|
offset: DpOffset? = null,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
if (offset != null) {
|
||||||
|
DropdownMenu(
|
||||||
|
expanded = expanded,
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
modifier = modifier,
|
||||||
|
offset = offset,
|
||||||
|
content = {
|
||||||
|
DownloadDropdownMenuItems(
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
onDownloadClicked = onDownloadClicked,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
DropdownMenu(
|
||||||
|
expanded = expanded,
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
modifier = modifier,
|
||||||
|
content = {
|
||||||
|
DownloadDropdownMenuItems(
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
onDownloadClicked = onDownloadClicked,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ColumnScope.DownloadDropdownMenuItems(
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
onDownloadClicked: (DownloadAction) -> Unit,
|
||||||
) {
|
) {
|
||||||
val options = persistentListOf(
|
val options = persistentListOf(
|
||||||
DownloadAction.NEXT_1_CHAPTER to pluralStringResource(MR.plurals.download_amount, 1, 1),
|
DownloadAction.NEXT_1_CHAPTER to pluralStringResource(MR.plurals.download_amount, 1, 1),
|
||||||
@@ -25,11 +61,6 @@ fun DownloadDropdownMenu(
|
|||||||
DownloadAction.UNREAD_CHAPTERS to stringResource(MR.strings.download_unread),
|
DownloadAction.UNREAD_CHAPTERS to stringResource(MR.strings.download_unread),
|
||||||
)
|
)
|
||||||
|
|
||||||
DropdownMenu(
|
|
||||||
expanded = expanded,
|
|
||||||
onDismissRequest = onDismissRequest,
|
|
||||||
modifier = modifier,
|
|
||||||
) {
|
|
||||||
options.map { (downloadAction, string) ->
|
options.map { (downloadAction, string) ->
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = { Text(text = string) },
|
text = { Text(text = string) },
|
||||||
@@ -39,5 +70,4 @@ fun DownloadDropdownMenu(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -255,7 +255,7 @@ private fun ColumnScope.DisplayPage(
|
|||||||
value = columns,
|
value = columns,
|
||||||
valueRange = 0..10,
|
valueRange = 0..10,
|
||||||
label = stringResource(MR.strings.pref_library_columns),
|
label = stringResource(MR.strings.pref_library_columns),
|
||||||
valueText = if (columns > 0) {
|
valueString = if (columns > 0) {
|
||||||
columns.toString()
|
columns.toString()
|
||||||
} else {
|
} else {
|
||||||
stringResource(MR.strings.label_auto)
|
stringResource(MR.strings.label_auto)
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import androidx.compose.foundation.layout.fillMaxSize
|
|||||||
import androidx.compose.foundation.lazy.grid.items
|
import androidx.compose.foundation.lazy.grid.items
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.util.fastAny
|
|
||||||
import eu.kanade.tachiyomi.ui.library.LibraryItem
|
import eu.kanade.tachiyomi.ui.library.LibraryItem
|
||||||
import tachiyomi.domain.library.model.LibraryManga
|
import tachiyomi.domain.library.model.LibraryManga
|
||||||
import tachiyomi.domain.manga.model.MangaCover
|
import tachiyomi.domain.manga.model.MangaCover
|
||||||
@@ -15,7 +14,7 @@ internal fun LibraryComfortableGrid(
|
|||||||
items: List<LibraryItem>,
|
items: List<LibraryItem>,
|
||||||
columns: Int,
|
columns: Int,
|
||||||
contentPadding: PaddingValues,
|
contentPadding: PaddingValues,
|
||||||
selection: List<LibraryManga>,
|
selection: Set<Long>,
|
||||||
onClick: (LibraryManga) -> Unit,
|
onClick: (LibraryManga) -> Unit,
|
||||||
onLongClick: (LibraryManga) -> Unit,
|
onLongClick: (LibraryManga) -> Unit,
|
||||||
onClickContinueReading: ((LibraryManga) -> Unit)?,
|
onClickContinueReading: ((LibraryManga) -> Unit)?,
|
||||||
@@ -35,7 +34,7 @@ internal fun LibraryComfortableGrid(
|
|||||||
) { libraryItem ->
|
) { libraryItem ->
|
||||||
val manga = libraryItem.libraryManga.manga
|
val manga = libraryItem.libraryManga.manga
|
||||||
MangaComfortableGridItem(
|
MangaComfortableGridItem(
|
||||||
isSelected = selection.fastAny { it.id == libraryItem.libraryManga.id },
|
isSelected = manga.id in selection,
|
||||||
title = manga.title,
|
title = manga.title,
|
||||||
coverData = MangaCover(
|
coverData = MangaCover(
|
||||||
mangaId = manga.id,
|
mangaId = manga.id,
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import androidx.compose.foundation.layout.fillMaxSize
|
|||||||
import androidx.compose.foundation.lazy.grid.items
|
import androidx.compose.foundation.lazy.grid.items
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.util.fastAny
|
|
||||||
import eu.kanade.tachiyomi.ui.library.LibraryItem
|
import eu.kanade.tachiyomi.ui.library.LibraryItem
|
||||||
import tachiyomi.domain.library.model.LibraryManga
|
import tachiyomi.domain.library.model.LibraryManga
|
||||||
import tachiyomi.domain.manga.model.MangaCover
|
import tachiyomi.domain.manga.model.MangaCover
|
||||||
@@ -16,7 +15,7 @@ internal fun LibraryCompactGrid(
|
|||||||
showTitle: Boolean,
|
showTitle: Boolean,
|
||||||
columns: Int,
|
columns: Int,
|
||||||
contentPadding: PaddingValues,
|
contentPadding: PaddingValues,
|
||||||
selection: List<LibraryManga>,
|
selection: Set<Long>,
|
||||||
onClick: (LibraryManga) -> Unit,
|
onClick: (LibraryManga) -> Unit,
|
||||||
onLongClick: (LibraryManga) -> Unit,
|
onLongClick: (LibraryManga) -> Unit,
|
||||||
onClickContinueReading: ((LibraryManga) -> Unit)?,
|
onClickContinueReading: ((LibraryManga) -> Unit)?,
|
||||||
@@ -36,7 +35,7 @@ internal fun LibraryCompactGrid(
|
|||||||
) { libraryItem ->
|
) { libraryItem ->
|
||||||
val manga = libraryItem.libraryManga.manga
|
val manga = libraryItem.libraryManga.manga
|
||||||
MangaCompactGridItem(
|
MangaCompactGridItem(
|
||||||
isSelected = selection.fastAny { it.id == libraryItem.libraryManga.id },
|
isSelected = manga.id in selection,
|
||||||
title = manga.title.takeIf { showTitle },
|
title = manga.title.takeIf { showTitle },
|
||||||
coverData = MangaCover(
|
coverData = MangaCover(
|
||||||
mangaId = manga.id,
|
mangaId = manga.id,
|
||||||
|
|||||||
@@ -29,22 +29,22 @@ import kotlin.time.Duration.Companion.seconds
|
|||||||
fun LibraryContent(
|
fun LibraryContent(
|
||||||
categories: List<Category>,
|
categories: List<Category>,
|
||||||
searchQuery: String?,
|
searchQuery: String?,
|
||||||
selection: List<LibraryManga>,
|
selection: Set<Long>,
|
||||||
contentPadding: PaddingValues,
|
contentPadding: PaddingValues,
|
||||||
currentPage: () -> Int,
|
currentPage: Int,
|
||||||
hasActiveFilters: Boolean,
|
hasActiveFilters: Boolean,
|
||||||
showPageTabs: Boolean,
|
showPageTabs: Boolean,
|
||||||
onChangeCurrentPage: (Int) -> Unit,
|
onChangeCurrentPage: (Int) -> Unit,
|
||||||
onMangaClicked: (Long) -> Unit,
|
onClickManga: (Long) -> Unit,
|
||||||
onContinueReadingClicked: ((LibraryManga) -> Unit)?,
|
onContinueReadingClicked: ((LibraryManga) -> Unit)?,
|
||||||
onToggleSelection: (LibraryManga) -> Unit,
|
onToggleSelection: (Category, LibraryManga) -> Unit,
|
||||||
onToggleRangeSelection: (LibraryManga) -> Unit,
|
onToggleRangeSelection: (Category, LibraryManga) -> Unit,
|
||||||
onRefresh: (Category?) -> Boolean,
|
onRefresh: () -> Boolean,
|
||||||
onGlobalSearchClicked: () -> Unit,
|
onGlobalSearchClicked: () -> Unit,
|
||||||
getNumberOfMangaForCategory: (Category) -> Int?,
|
getItemCountForCategory: (Category) -> Int?,
|
||||||
getDisplayMode: (Int) -> PreferenceMutableState<LibraryDisplayMode>,
|
getDisplayMode: (Int) -> PreferenceMutableState<LibraryDisplayMode>,
|
||||||
getColumnsForOrientation: (Boolean) -> PreferenceMutableState<Int>,
|
getColumnsForOrientation: (Boolean) -> PreferenceMutableState<Int>,
|
||||||
getLibraryForPage: (Int) -> List<LibraryItem>,
|
getItemsForCategory: (Category) -> List<LibraryItem>,
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.padding(
|
modifier = Modifier.padding(
|
||||||
@@ -53,13 +53,12 @@ fun LibraryContent(
|
|||||||
end = contentPadding.calculateEndPadding(LocalLayoutDirection.current),
|
end = contentPadding.calculateEndPadding(LocalLayoutDirection.current),
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
val coercedCurrentPage = remember { currentPage().coerceAtMost(categories.lastIndex) }
|
val pagerState = rememberPagerState(currentPage) { categories.size }
|
||||||
val pagerState = rememberPagerState(coercedCurrentPage) { categories.size }
|
|
||||||
|
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
var isRefreshing by remember(pagerState.currentPage) { mutableStateOf(false) }
|
var isRefreshing by remember(pagerState.currentPage) { mutableStateOf(false) }
|
||||||
|
|
||||||
if (showPageTabs && categories.size > 1) {
|
if (showPageTabs && categories.isNotEmpty() && (categories.size > 1 || !categories.first().isSystemCategory)) {
|
||||||
LaunchedEffect(categories) {
|
LaunchedEffect(categories) {
|
||||||
if (categories.size <= pagerState.currentPage) {
|
if (categories.size <= pagerState.currentPage) {
|
||||||
pagerState.scrollToPage(categories.size - 1)
|
pagerState.scrollToPage(categories.size - 1)
|
||||||
@@ -68,23 +67,20 @@ fun LibraryContent(
|
|||||||
LibraryTabs(
|
LibraryTabs(
|
||||||
categories = categories,
|
categories = categories,
|
||||||
pagerState = pagerState,
|
pagerState = pagerState,
|
||||||
getNumberOfMangaForCategory = getNumberOfMangaForCategory,
|
getItemCountForCategory = getItemCountForCategory,
|
||||||
) { scope.launch { pagerState.animateScrollToPage(it) } }
|
onTabItemClick = {
|
||||||
}
|
scope.launch {
|
||||||
|
pagerState.animateScrollToPage(it)
|
||||||
val notSelectionMode = selection.isEmpty()
|
|
||||||
val onClickManga = { manga: LibraryManga ->
|
|
||||||
if (notSelectionMode) {
|
|
||||||
onMangaClicked(manga.manga.id)
|
|
||||||
} else {
|
|
||||||
onToggleSelection(manga)
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
PullRefresh(
|
PullRefresh(
|
||||||
refreshing = isRefreshing,
|
refreshing = isRefreshing,
|
||||||
|
enabled = selection.isEmpty(),
|
||||||
onRefresh = {
|
onRefresh = {
|
||||||
val started = onRefresh(categories[currentPage()])
|
val started = onRefresh()
|
||||||
if (!started) return@PullRefresh
|
if (!started) return@PullRefresh
|
||||||
scope.launch {
|
scope.launch {
|
||||||
// Fake refresh status but hide it after a second as it's a long running task
|
// Fake refresh status but hide it after a second as it's a long running task
|
||||||
@@ -93,19 +89,25 @@ fun LibraryContent(
|
|||||||
isRefreshing = false
|
isRefreshing = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
enabled = notSelectionMode,
|
|
||||||
) {
|
) {
|
||||||
LibraryPager(
|
LibraryPager(
|
||||||
state = pagerState,
|
state = pagerState,
|
||||||
contentPadding = PaddingValues(bottom = contentPadding.calculateBottomPadding()),
|
contentPadding = PaddingValues(bottom = contentPadding.calculateBottomPadding()),
|
||||||
hasActiveFilters = hasActiveFilters,
|
hasActiveFilters = hasActiveFilters,
|
||||||
selectedManga = selection,
|
selection = selection,
|
||||||
searchQuery = searchQuery,
|
searchQuery = searchQuery,
|
||||||
onGlobalSearchClicked = onGlobalSearchClicked,
|
onGlobalSearchClicked = onGlobalSearchClicked,
|
||||||
|
getCategoryForPage = { page -> categories[page] },
|
||||||
getDisplayMode = getDisplayMode,
|
getDisplayMode = getDisplayMode,
|
||||||
getColumnsForOrientation = getColumnsForOrientation,
|
getColumnsForOrientation = getColumnsForOrientation,
|
||||||
getLibraryForPage = getLibraryForPage,
|
getItemsForCategory = getItemsForCategory,
|
||||||
onClickManga = onClickManga,
|
onClickManga = { category, manga ->
|
||||||
|
if (selection.isNotEmpty()) {
|
||||||
|
onToggleSelection(category, manga)
|
||||||
|
} else {
|
||||||
|
onClickManga(manga.manga.id)
|
||||||
|
}
|
||||||
|
},
|
||||||
onLongClickManga = onToggleRangeSelection,
|
onLongClickManga = onToggleRangeSelection,
|
||||||
onClickContinueReading = onContinueReadingClicked,
|
onClickContinueReading = onContinueReadingClicked,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import androidx.compose.foundation.lazy.items
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.util.fastAny
|
|
||||||
import eu.kanade.tachiyomi.ui.library.LibraryItem
|
import eu.kanade.tachiyomi.ui.library.LibraryItem
|
||||||
import tachiyomi.domain.library.model.LibraryManga
|
import tachiyomi.domain.library.model.LibraryManga
|
||||||
import tachiyomi.domain.manga.model.MangaCover
|
import tachiyomi.domain.manga.model.MangaCover
|
||||||
@@ -18,7 +17,7 @@ import tachiyomi.presentation.core.util.plus
|
|||||||
internal fun LibraryList(
|
internal fun LibraryList(
|
||||||
items: List<LibraryItem>,
|
items: List<LibraryItem>,
|
||||||
contentPadding: PaddingValues,
|
contentPadding: PaddingValues,
|
||||||
selection: List<LibraryManga>,
|
selection: Set<Long>,
|
||||||
onClick: (LibraryManga) -> Unit,
|
onClick: (LibraryManga) -> Unit,
|
||||||
onLongClick: (LibraryManga) -> Unit,
|
onLongClick: (LibraryManga) -> Unit,
|
||||||
onClickContinueReading: ((LibraryManga) -> Unit)?,
|
onClickContinueReading: ((LibraryManga) -> Unit)?,
|
||||||
@@ -45,7 +44,7 @@ internal fun LibraryList(
|
|||||||
) { libraryItem ->
|
) { libraryItem ->
|
||||||
val manga = libraryItem.libraryManga.manga
|
val manga = libraryItem.libraryManga.manga
|
||||||
MangaListItem(
|
MangaListItem(
|
||||||
isSelected = selection.fastAny { it.id == libraryItem.libraryManga.id },
|
isSelected = manga.id in selection,
|
||||||
title = manga.title,
|
title = manga.title,
|
||||||
coverData = MangaCover(
|
coverData = MangaCover(
|
||||||
mangaId = manga.id,
|
mangaId = manga.id,
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import androidx.compose.ui.platform.LocalConfiguration
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import eu.kanade.core.preference.PreferenceMutableState
|
import eu.kanade.core.preference.PreferenceMutableState
|
||||||
import eu.kanade.tachiyomi.ui.library.LibraryItem
|
import eu.kanade.tachiyomi.ui.library.LibraryItem
|
||||||
|
import tachiyomi.domain.category.model.Category
|
||||||
import tachiyomi.domain.library.model.LibraryDisplayMode
|
import tachiyomi.domain.library.model.LibraryDisplayMode
|
||||||
import tachiyomi.domain.library.model.LibraryManga
|
import tachiyomi.domain.library.model.LibraryManga
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
@@ -31,14 +32,15 @@ fun LibraryPager(
|
|||||||
state: PagerState,
|
state: PagerState,
|
||||||
contentPadding: PaddingValues,
|
contentPadding: PaddingValues,
|
||||||
hasActiveFilters: Boolean,
|
hasActiveFilters: Boolean,
|
||||||
selectedManga: List<LibraryManga>,
|
selection: Set<Long>,
|
||||||
searchQuery: String?,
|
searchQuery: String?,
|
||||||
onGlobalSearchClicked: () -> Unit,
|
onGlobalSearchClicked: () -> Unit,
|
||||||
|
getCategoryForPage: (Int) -> Category,
|
||||||
getDisplayMode: (Int) -> PreferenceMutableState<LibraryDisplayMode>,
|
getDisplayMode: (Int) -> PreferenceMutableState<LibraryDisplayMode>,
|
||||||
getColumnsForOrientation: (Boolean) -> PreferenceMutableState<Int>,
|
getColumnsForOrientation: (Boolean) -> PreferenceMutableState<Int>,
|
||||||
getLibraryForPage: (Int) -> List<LibraryItem>,
|
getItemsForCategory: (Category) -> List<LibraryItem>,
|
||||||
onClickManga: (LibraryManga) -> Unit,
|
onClickManga: (Category, LibraryManga) -> Unit,
|
||||||
onLongClickManga: (LibraryManga) -> Unit,
|
onLongClickManga: (Category, LibraryManga) -> Unit,
|
||||||
onClickContinueReading: ((LibraryManga) -> Unit)?,
|
onClickContinueReading: ((LibraryManga) -> Unit)?,
|
||||||
) {
|
) {
|
||||||
HorizontalPager(
|
HorizontalPager(
|
||||||
@@ -50,9 +52,10 @@ fun LibraryPager(
|
|||||||
// To make sure only one offscreen page is being composed
|
// To make sure only one offscreen page is being composed
|
||||||
return@HorizontalPager
|
return@HorizontalPager
|
||||||
}
|
}
|
||||||
val library = getLibraryForPage(page)
|
val category = getCategoryForPage(page)
|
||||||
|
val items = getItemsForCategory(category)
|
||||||
|
|
||||||
if (library.isEmpty()) {
|
if (items.isEmpty()) {
|
||||||
LibraryPagerEmptyScreen(
|
LibraryPagerEmptyScreen(
|
||||||
searchQuery = searchQuery,
|
searchQuery = searchQuery,
|
||||||
hasActiveFilters = hasActiveFilters,
|
hasActiveFilters = hasActiveFilters,
|
||||||
@@ -72,12 +75,15 @@ fun LibraryPager(
|
|||||||
remember { mutableIntStateOf(0) }
|
remember { mutableIntStateOf(0) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val onClickManga: (LibraryManga) -> Unit = { onClickManga(category, it) }
|
||||||
|
val onLongClickManga: (LibraryManga) -> Unit = { onLongClickManga(category, it) }
|
||||||
|
|
||||||
when (displayMode) {
|
when (displayMode) {
|
||||||
LibraryDisplayMode.List -> {
|
LibraryDisplayMode.List -> {
|
||||||
LibraryList(
|
LibraryList(
|
||||||
items = library,
|
items = items,
|
||||||
contentPadding = contentPadding,
|
contentPadding = contentPadding,
|
||||||
selection = selectedManga,
|
selection = selection,
|
||||||
onClick = onClickManga,
|
onClick = onClickManga,
|
||||||
onLongClick = onLongClickManga,
|
onLongClick = onLongClickManga,
|
||||||
onClickContinueReading = onClickContinueReading,
|
onClickContinueReading = onClickContinueReading,
|
||||||
@@ -87,11 +93,11 @@ fun LibraryPager(
|
|||||||
}
|
}
|
||||||
LibraryDisplayMode.CompactGrid, LibraryDisplayMode.CoverOnlyGrid -> {
|
LibraryDisplayMode.CompactGrid, LibraryDisplayMode.CoverOnlyGrid -> {
|
||||||
LibraryCompactGrid(
|
LibraryCompactGrid(
|
||||||
items = library,
|
items = items,
|
||||||
showTitle = displayMode is LibraryDisplayMode.CompactGrid,
|
showTitle = displayMode is LibraryDisplayMode.CompactGrid,
|
||||||
columns = columns,
|
columns = columns,
|
||||||
contentPadding = contentPadding,
|
contentPadding = contentPadding,
|
||||||
selection = selectedManga,
|
selection = selection,
|
||||||
onClick = onClickManga,
|
onClick = onClickManga,
|
||||||
onLongClick = onLongClickManga,
|
onLongClick = onLongClickManga,
|
||||||
onClickContinueReading = onClickContinueReading,
|
onClickContinueReading = onClickContinueReading,
|
||||||
@@ -101,10 +107,10 @@ fun LibraryPager(
|
|||||||
}
|
}
|
||||||
LibraryDisplayMode.ComfortableGrid -> {
|
LibraryDisplayMode.ComfortableGrid -> {
|
||||||
LibraryComfortableGrid(
|
LibraryComfortableGrid(
|
||||||
items = library,
|
items = items,
|
||||||
columns = columns,
|
columns = columns,
|
||||||
contentPadding = contentPadding,
|
contentPadding = contentPadding,
|
||||||
selection = selectedManga,
|
selection = selection,
|
||||||
onClick = onClickManga,
|
onClick = onClickManga,
|
||||||
onLongClick = onLongClickManga,
|
onLongClick = onLongClickManga,
|
||||||
onClickContinueReading = onClickContinueReading,
|
onClickContinueReading = onClickContinueReading,
|
||||||
|
|||||||
@@ -18,13 +18,11 @@ import tachiyomi.presentation.core.components.material.TabText
|
|||||||
internal fun LibraryTabs(
|
internal fun LibraryTabs(
|
||||||
categories: List<Category>,
|
categories: List<Category>,
|
||||||
pagerState: PagerState,
|
pagerState: PagerState,
|
||||||
getNumberOfMangaForCategory: (Category) -> Int?,
|
getItemCountForCategory: (Category) -> Int?,
|
||||||
onTabItemClick: (Int) -> Unit,
|
onTabItemClick: (Int) -> Unit,
|
||||||
) {
|
) {
|
||||||
val currentPageIndex = pagerState.currentPage.coerceAtMost(categories.lastIndex)
|
val currentPageIndex = pagerState.currentPage.coerceAtMost(categories.lastIndex)
|
||||||
Column(
|
Column(modifier = Modifier.zIndex(2f)) {
|
||||||
modifier = Modifier.zIndex(1f),
|
|
||||||
) {
|
|
||||||
PrimaryScrollableTabRow(
|
PrimaryScrollableTabRow(
|
||||||
selectedTabIndex = currentPageIndex,
|
selectedTabIndex = currentPageIndex,
|
||||||
edgePadding = 0.dp,
|
edgePadding = 0.dp,
|
||||||
@@ -39,7 +37,7 @@ internal fun LibraryTabs(
|
|||||||
text = {
|
text = {
|
||||||
TabText(
|
TabText(
|
||||||
text = category.visualName,
|
text = category.visualName,
|
||||||
badgeCount = getNumberOfMangaForCategory(category),
|
badgeCount = getItemCountForCategory(category),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
unselectedContentColor = MaterialTheme.colorScheme.onSurface,
|
unselectedContentColor = MaterialTheme.colorScheme.onSurface,
|
||||||
|
|||||||
@@ -1,44 +1,95 @@
|
|||||||
package eu.kanade.presentation.manga
|
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.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.heightIn
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.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.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Brush
|
||||||
|
import androidx.compose.material.icons.filled.PersonOutline
|
||||||
|
import androidx.compose.material.icons.filled.Warning
|
||||||
import androidx.compose.material.icons.outlined.Add
|
import androidx.compose.material.icons.outlined.Add
|
||||||
import androidx.compose.material.icons.outlined.Book
|
import androidx.compose.material.icons.outlined.AttachMoney
|
||||||
import androidx.compose.material.icons.outlined.SwapVert
|
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.HorizontalDivider
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedButton
|
import androidx.compose.material3.OutlinedButton
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.Typography
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.text.TextMeasurer
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.rememberTextMeasurer
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.Constraints
|
||||||
|
import androidx.compose.ui.unit.Density
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.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.AdaptiveSheet
|
||||||
import eu.kanade.presentation.components.TabbedDialogPaddings
|
import eu.kanade.presentation.components.TabbedDialogPaddings
|
||||||
|
import eu.kanade.presentation.manga.components.MangaCover
|
||||||
import eu.kanade.presentation.more.settings.LocalPreferenceMinHeight
|
import eu.kanade.presentation.more.settings.LocalPreferenceMinHeight
|
||||||
import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget
|
import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget
|
||||||
|
import eu.kanade.tachiyomi.source.Source
|
||||||
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
import tachiyomi.domain.manga.model.Manga
|
||||||
|
import tachiyomi.domain.manga.model.MangaWithChapterCount
|
||||||
|
import tachiyomi.domain.source.model.StubSource
|
||||||
|
import tachiyomi.domain.source.service.SourceManager
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
|
import tachiyomi.presentation.core.components.Badge
|
||||||
|
import tachiyomi.presentation.core.components.BadgeGroup
|
||||||
|
import tachiyomi.presentation.core.components.material.padding
|
||||||
|
import tachiyomi.presentation.core.i18n.pluralStringResource
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
|
import tachiyomi.presentation.core.util.secondaryItemAlpha
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DuplicateMangaDialog(
|
fun DuplicateMangaDialog(
|
||||||
|
duplicates: List<MangaWithChapterCount>,
|
||||||
onDismissRequest: () -> Unit,
|
onDismissRequest: () -> Unit,
|
||||||
onConfirm: () -> Unit,
|
onConfirm: () -> Unit,
|
||||||
onOpenManga: () -> Unit,
|
onOpenManga: (manga: Manga) -> Unit,
|
||||||
onMigrate: () -> Unit,
|
onMigrate: (manga: Manga) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
val sourceManager = remember { Injekt.get<SourceManager>() }
|
||||||
val minHeight = LocalPreferenceMinHeight.current
|
val minHeight = LocalPreferenceMinHeight.current
|
||||||
|
val horizontalPadding = PaddingValues(horizontal = TabbedDialogPaddings.Horizontal)
|
||||||
|
val horizontalPaddingModifier = Modifier.padding(horizontalPadding)
|
||||||
|
|
||||||
AdaptiveSheet(
|
AdaptiveSheet(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
@@ -46,45 +97,45 @@ fun DuplicateMangaDialog(
|
|||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(
|
.padding(vertical = TabbedDialogPaddings.Vertical)
|
||||||
vertical = TabbedDialogPaddings.Vertical,
|
.verticalScroll(rememberScrollState())
|
||||||
horizontal = TabbedDialogPaddings.Horizontal,
|
|
||||||
)
|
|
||||||
.fillMaxWidth(),
|
.fillMaxWidth(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.medium),
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
modifier = Modifier.padding(TitlePadding),
|
text = stringResource(MR.strings.possible_duplicates_title),
|
||||||
text = stringResource(MR.strings.are_you_sure),
|
|
||||||
style = MaterialTheme.typography.headlineMedium,
|
style = MaterialTheme.typography.headlineMedium,
|
||||||
|
modifier = Modifier
|
||||||
|
.then(horizontalPaddingModifier)
|
||||||
|
.padding(top = MaterialTheme.padding.small),
|
||||||
)
|
)
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(MR.strings.confirm_add_duplicate_manga),
|
text = stringResource(MR.strings.possible_duplicates_summary),
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
modifier = Modifier.then(horizontalPaddingModifier),
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(Modifier.height(PaddingSize))
|
LazyRow(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
||||||
TextPreferenceWidget(
|
modifier = Modifier.height(getMaximumMangaCardHeight(duplicates)),
|
||||||
title = stringResource(MR.strings.action_show_manga),
|
contentPadding = horizontalPadding,
|
||||||
icon = Icons.Outlined.Book,
|
) {
|
||||||
onPreferenceClick = {
|
items(
|
||||||
onDismissRequest()
|
items = duplicates,
|
||||||
onOpenManga()
|
key = { it.manga.id },
|
||||||
},
|
) {
|
||||||
)
|
DuplicateMangaListItem(
|
||||||
|
duplicate = it,
|
||||||
HorizontalDivider()
|
getSource = { sourceManager.getOrStub(it.manga.source) },
|
||||||
|
onMigrate = { onMigrate(it.manga) },
|
||||||
TextPreferenceWidget(
|
onDismissRequest = onDismissRequest,
|
||||||
title = stringResource(MR.strings.action_migrate_duplicate),
|
onOpenManga = { onOpenManga(it.manga) },
|
||||||
icon = Icons.Outlined.SwapVert,
|
|
||||||
onPreferenceClick = {
|
|
||||||
onDismissRequest()
|
|
||||||
onMigrate()
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(modifier = horizontalPaddingModifier) {
|
||||||
HorizontalDivider()
|
HorizontalDivider()
|
||||||
|
|
||||||
TextPreferenceWidget(
|
TextPreferenceWidget(
|
||||||
@@ -94,33 +145,262 @@ fun DuplicateMangaDialog(
|
|||||||
onDismissRequest()
|
onDismissRequest()
|
||||||
onConfirm()
|
onConfirm()
|
||||||
},
|
},
|
||||||
)
|
modifier = Modifier.clip(CircleShape),
|
||||||
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.sizeIn(minHeight = minHeight)
|
|
||||||
.clickable { onDismissRequest.invoke() }
|
|
||||||
.padding(ButtonPadding)
|
|
||||||
.fillMaxWidth(),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.Center,
|
|
||||||
) {
|
|
||||||
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,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
Spacer(modifier = Modifier.height(MaterialTheme.padding.extraSmall))
|
||||||
private val TitlePadding = PaddingValues(bottom = 16.dp, top = 8.dp)
|
|
||||||
|
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(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -112,6 +112,7 @@ fun MangaScreen(
|
|||||||
onEditCategoryClicked: (() -> Unit)?,
|
onEditCategoryClicked: (() -> Unit)?,
|
||||||
onEditFetchIntervalClicked: (() -> Unit)?,
|
onEditFetchIntervalClicked: (() -> Unit)?,
|
||||||
onMigrateClicked: (() -> Unit)?,
|
onMigrateClicked: (() -> Unit)?,
|
||||||
|
onEditNotesClicked: () -> Unit,
|
||||||
|
|
||||||
// For bottom action menu
|
// For bottom action menu
|
||||||
onMultiBookmarkClicked: (List<Chapter>, bookmarked: Boolean) -> Unit,
|
onMultiBookmarkClicked: (List<Chapter>, bookmarked: Boolean) -> Unit,
|
||||||
@@ -160,6 +161,7 @@ fun MangaScreen(
|
|||||||
onEditCategoryClicked = onEditCategoryClicked,
|
onEditCategoryClicked = onEditCategoryClicked,
|
||||||
onEditIntervalClicked = onEditFetchIntervalClicked,
|
onEditIntervalClicked = onEditFetchIntervalClicked,
|
||||||
onMigrateClicked = onMigrateClicked,
|
onMigrateClicked = onMigrateClicked,
|
||||||
|
onEditNotesClicked = onEditNotesClicked,
|
||||||
onMultiBookmarkClicked = onMultiBookmarkClicked,
|
onMultiBookmarkClicked = onMultiBookmarkClicked,
|
||||||
onMultiMarkAsReadClicked = onMultiMarkAsReadClicked,
|
onMultiMarkAsReadClicked = onMultiMarkAsReadClicked,
|
||||||
onMarkPreviousAsReadClicked = onMarkPreviousAsReadClicked,
|
onMarkPreviousAsReadClicked = onMarkPreviousAsReadClicked,
|
||||||
@@ -195,6 +197,7 @@ fun MangaScreen(
|
|||||||
onEditCategoryClicked = onEditCategoryClicked,
|
onEditCategoryClicked = onEditCategoryClicked,
|
||||||
onEditIntervalClicked = onEditFetchIntervalClicked,
|
onEditIntervalClicked = onEditFetchIntervalClicked,
|
||||||
onMigrateClicked = onMigrateClicked,
|
onMigrateClicked = onMigrateClicked,
|
||||||
|
onEditNotesClicked = onEditNotesClicked,
|
||||||
onMultiBookmarkClicked = onMultiBookmarkClicked,
|
onMultiBookmarkClicked = onMultiBookmarkClicked,
|
||||||
onMultiMarkAsReadClicked = onMultiMarkAsReadClicked,
|
onMultiMarkAsReadClicked = onMultiMarkAsReadClicked,
|
||||||
onMarkPreviousAsReadClicked = onMarkPreviousAsReadClicked,
|
onMarkPreviousAsReadClicked = onMarkPreviousAsReadClicked,
|
||||||
@@ -240,6 +243,7 @@ private fun MangaScreenSmallImpl(
|
|||||||
onEditCategoryClicked: (() -> Unit)?,
|
onEditCategoryClicked: (() -> Unit)?,
|
||||||
onEditIntervalClicked: (() -> Unit)?,
|
onEditIntervalClicked: (() -> Unit)?,
|
||||||
onMigrateClicked: (() -> Unit)?,
|
onMigrateClicked: (() -> Unit)?,
|
||||||
|
onEditNotesClicked: () -> Unit,
|
||||||
|
|
||||||
// For bottom action menu
|
// For bottom action menu
|
||||||
onMultiBookmarkClicked: (List<Chapter>, bookmarked: Boolean) -> Unit,
|
onMultiBookmarkClicked: (List<Chapter>, bookmarked: Boolean) -> Unit,
|
||||||
@@ -265,13 +269,9 @@ private fun MangaScreenSmallImpl(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
BackHandler(onBack = {
|
BackHandler(enabled = isAnySelected) {
|
||||||
if (isAnySelected) {
|
|
||||||
onAllChapterSelected(false)
|
onAllChapterSelected(false)
|
||||||
} else {
|
|
||||||
navigateUp()
|
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
@@ -302,6 +302,7 @@ private fun MangaScreenSmallImpl(
|
|||||||
onClickEditCategory = onEditCategoryClicked,
|
onClickEditCategory = onEditCategoryClicked,
|
||||||
onClickRefresh = onRefresh,
|
onClickRefresh = onRefresh,
|
||||||
onClickMigrate = onMigrateClicked,
|
onClickMigrate = onMigrateClicked,
|
||||||
|
onClickEditNotes = onEditNotesClicked,
|
||||||
actionModeCounter = selectedChapterCount,
|
actionModeCounter = selectedChapterCount,
|
||||||
onCancelActionMode = { onAllChapterSelected(false) },
|
onCancelActionMode = { onAllChapterSelected(false) },
|
||||||
onSelectAll = { onAllChapterSelected(true) },
|
onSelectAll = { onAllChapterSelected(true) },
|
||||||
@@ -414,8 +415,10 @@ private fun MangaScreenSmallImpl(
|
|||||||
defaultExpandState = state.isFromSource,
|
defaultExpandState = state.isFromSource,
|
||||||
description = state.manga.description,
|
description = state.manga.description,
|
||||||
tagsProvider = { state.manga.genre },
|
tagsProvider = { state.manga.genre },
|
||||||
|
notes = state.manga.notes,
|
||||||
onTagSearch = onTagSearch,
|
onTagSearch = onTagSearch,
|
||||||
onCopyTagToClipboard = onCopyTagToClipboard,
|
onCopyTagToClipboard = onCopyTagToClipboard,
|
||||||
|
onEditNotes = onEditNotesClicked,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -484,6 +487,7 @@ fun MangaScreenLargeImpl(
|
|||||||
onEditCategoryClicked: (() -> Unit)?,
|
onEditCategoryClicked: (() -> Unit)?,
|
||||||
onEditIntervalClicked: (() -> Unit)?,
|
onEditIntervalClicked: (() -> Unit)?,
|
||||||
onMigrateClicked: (() -> Unit)?,
|
onMigrateClicked: (() -> Unit)?,
|
||||||
|
onEditNotesClicked: () -> Unit,
|
||||||
|
|
||||||
// For bottom action menu
|
// For bottom action menu
|
||||||
onMultiBookmarkClicked: (List<Chapter>, bookmarked: Boolean) -> Unit,
|
onMultiBookmarkClicked: (List<Chapter>, bookmarked: Boolean) -> Unit,
|
||||||
@@ -515,13 +519,9 @@ fun MangaScreenLargeImpl(
|
|||||||
|
|
||||||
val chapterListState = rememberLazyListState()
|
val chapterListState = rememberLazyListState()
|
||||||
|
|
||||||
BackHandler(onBack = {
|
BackHandler(enabled = isAnySelected) {
|
||||||
if (isAnySelected) {
|
|
||||||
onAllChapterSelected(false)
|
onAllChapterSelected(false)
|
||||||
} else {
|
|
||||||
navigateUp()
|
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
@@ -539,6 +539,7 @@ fun MangaScreenLargeImpl(
|
|||||||
onClickEditCategory = onEditCategoryClicked,
|
onClickEditCategory = onEditCategoryClicked,
|
||||||
onClickRefresh = onRefresh,
|
onClickRefresh = onRefresh,
|
||||||
onClickMigrate = onMigrateClicked,
|
onClickMigrate = onMigrateClicked,
|
||||||
|
onClickEditNotes = onEditNotesClicked,
|
||||||
onCancelActionMode = { onAllChapterSelected(false) },
|
onCancelActionMode = { onAllChapterSelected(false) },
|
||||||
actionModeCounter = selectedChapterCount,
|
actionModeCounter = selectedChapterCount,
|
||||||
onSelectAll = { onAllChapterSelected(true) },
|
onSelectAll = { onAllChapterSelected(true) },
|
||||||
@@ -640,8 +641,10 @@ fun MangaScreenLargeImpl(
|
|||||||
defaultExpandState = true,
|
defaultExpandState = true,
|
||||||
description = state.manga.description,
|
description = state.manga.description,
|
||||||
tagsProvider = { state.manga.genre },
|
tagsProvider = { state.manga.genre },
|
||||||
|
notes = state.manga.notes,
|
||||||
onTagSearch = onTagSearch,
|
onTagSearch = onTagSearch,
|
||||||
onCopyTagToClipboard = onCopyTagToClipboard,
|
onCopyTagToClipboard = onCopyTagToClipboard,
|
||||||
|
onEditNotes = onEditNotesClicked,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import androidx.compose.animation.fadeOut
|
|||||||
import androidx.compose.animation.shrinkVertically
|
import androidx.compose.animation.shrinkVertically
|
||||||
import androidx.compose.foundation.combinedClickable
|
import androidx.compose.foundation.combinedClickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.RowScope
|
import androidx.compose.foundation.layout.RowScope
|
||||||
@@ -28,7 +29,10 @@ import androidx.compose.material.icons.outlined.BookmarkRemove
|
|||||||
import androidx.compose.material.icons.outlined.Delete
|
import androidx.compose.material.icons.outlined.Delete
|
||||||
import androidx.compose.material.icons.outlined.DoneAll
|
import androidx.compose.material.icons.outlined.DoneAll
|
||||||
import androidx.compose.material.icons.outlined.Download
|
import androidx.compose.material.icons.outlined.Download
|
||||||
|
import androidx.compose.material.icons.outlined.MoreVert
|
||||||
import androidx.compose.material.icons.outlined.RemoveDone
|
import androidx.compose.material.icons.outlined.RemoveDone
|
||||||
|
import androidx.compose.material.icons.outlined.SwapCalls
|
||||||
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
@@ -48,8 +52,10 @@ import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
|||||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||||
import androidx.compose.ui.res.vectorResource
|
import androidx.compose.ui.res.vectorResource
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.DpOffset
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import eu.kanade.presentation.components.DownloadDropdownMenu
|
import eu.kanade.presentation.components.DownloadDropdownMenu
|
||||||
|
import eu.kanade.presentation.components.DropdownMenu
|
||||||
import eu.kanade.presentation.manga.DownloadAction
|
import eu.kanade.presentation.manga.DownloadAction
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
@@ -185,7 +191,7 @@ private fun RowScope.Button(
|
|||||||
targetValue = if (toConfirm) 2f else 1f,
|
targetValue = if (toConfirm) 2f else 1f,
|
||||||
label = "weight",
|
label = "weight",
|
||||||
)
|
)
|
||||||
Column(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(48.dp)
|
.size(48.dp)
|
||||||
.weight(animatedWeight)
|
.weight(animatedWeight)
|
||||||
@@ -195,6 +201,9 @@ private fun RowScope.Button(
|
|||||||
onLongClick = onLongClick,
|
onLongClick = onLongClick,
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
),
|
),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
verticalArrangement = Arrangement.Center,
|
verticalArrangement = Arrangement.Center,
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
) {
|
) {
|
||||||
@@ -214,6 +223,7 @@ private fun RowScope.Button(
|
|||||||
style = MaterialTheme.typography.labelSmall,
|
style = MaterialTheme.typography.labelSmall,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
content?.invoke()
|
content?.invoke()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -226,6 +236,7 @@ fun LibraryBottomActionMenu(
|
|||||||
onMarkAsUnreadClicked: () -> Unit,
|
onMarkAsUnreadClicked: () -> Unit,
|
||||||
onDownloadClicked: ((DownloadAction) -> Unit)?,
|
onDownloadClicked: ((DownloadAction) -> Unit)?,
|
||||||
onDeleteClicked: () -> Unit,
|
onDeleteClicked: () -> Unit,
|
||||||
|
onMigrateClicked: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
@@ -240,17 +251,18 @@ fun LibraryBottomActionMenu(
|
|||||||
color = MaterialTheme.colorScheme.surfaceContainerHigh,
|
color = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||||
) {
|
) {
|
||||||
val haptic = LocalHapticFeedback.current
|
val haptic = LocalHapticFeedback.current
|
||||||
val confirm = remember { mutableStateListOf(false, false, false, false, false) }
|
val confirm = remember { mutableStateListOf(false, false, false, false, false, false) }
|
||||||
var resetJob: Job? = remember { null }
|
var resetJob: Job? = remember { null }
|
||||||
val onLongClickItem: (Int) -> Unit = { toConfirmIndex ->
|
val onLongClickItem: (Int) -> Unit = { toConfirmIndex ->
|
||||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||||
(0..<5).forEach { i -> confirm[i] = i == toConfirmIndex }
|
(0..5).forEach { i -> confirm[i] = i == toConfirmIndex }
|
||||||
resetJob?.cancel()
|
resetJob?.cancel()
|
||||||
resetJob = scope.launch {
|
resetJob = scope.launch {
|
||||||
delay(1.seconds)
|
delay(1.seconds)
|
||||||
if (isActive) confirm[toConfirmIndex] = false
|
if (isActive) confirm[toConfirmIndex] = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
val itemOverflow = onDownloadClicked != null
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.windowInsetsPadding(
|
.windowInsetsPadding(
|
||||||
@@ -289,22 +301,57 @@ fun LibraryBottomActionMenu(
|
|||||||
onLongClick = { onLongClickItem(3) },
|
onLongClick = { onLongClickItem(3) },
|
||||||
onClick = { downloadExpanded = !downloadExpanded },
|
onClick = { downloadExpanded = !downloadExpanded },
|
||||||
) {
|
) {
|
||||||
val onDismissRequest = { downloadExpanded = false }
|
|
||||||
DownloadDropdownMenu(
|
DownloadDropdownMenu(
|
||||||
expanded = downloadExpanded,
|
expanded = downloadExpanded,
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = { downloadExpanded = false },
|
||||||
onDownloadClicked = onDownloadClicked,
|
onDownloadClicked = onDownloadClicked,
|
||||||
|
offset = BottomBarMenuDpOffset,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (!itemOverflow) {
|
||||||
|
Button(
|
||||||
|
title = stringResource(MR.strings.migrate),
|
||||||
|
icon = Icons.Outlined.SwapCalls,
|
||||||
|
toConfirm = confirm[4],
|
||||||
|
onLongClick = { onLongClickItem(4) },
|
||||||
|
onClick = onMigrateClicked,
|
||||||
|
)
|
||||||
Button(
|
Button(
|
||||||
title = stringResource(MR.strings.action_delete),
|
title = stringResource(MR.strings.action_delete),
|
||||||
icon = Icons.Outlined.Delete,
|
icon = Icons.Outlined.Delete,
|
||||||
toConfirm = confirm[4],
|
toConfirm = confirm[5],
|
||||||
onLongClick = { onLongClickItem(4) },
|
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,
|
onClick = onDeleteClicked,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val BottomBarMenuDpOffset = DpOffset(0.dp, 0.dp)
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.compose.ui.viewinterop.AndroidView
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
import androidx.compose.ui.window.Dialog
|
import androidx.compose.ui.window.Dialog
|
||||||
import androidx.compose.ui.window.DialogProperties
|
import androidx.compose.ui.window.DialogProperties
|
||||||
|
import androidx.core.graphics.drawable.toDrawable
|
||||||
import androidx.core.view.updatePadding
|
import androidx.core.view.updatePadding
|
||||||
import coil3.asDrawable
|
import coil3.asDrawable
|
||||||
import coil3.imageLoader
|
import coil3.imageLoader
|
||||||
@@ -171,15 +172,13 @@ fun MangaCoverDialog(
|
|||||||
.memoryCachePolicy(CachePolicy.DISABLED)
|
.memoryCachePolicy(CachePolicy.DISABLED)
|
||||||
.target { image ->
|
.target { image ->
|
||||||
val drawable = image.asDrawable(view.context.resources)
|
val drawable = image.asDrawable(view.context.resources)
|
||||||
|
|
||||||
// Copy bitmap in case it came from memory cache
|
// Copy bitmap in case it came from memory cache
|
||||||
// Because SSIV needs to thoroughly read the image
|
// Because SSIV needs to thoroughly read the image
|
||||||
val copy = (drawable as? BitmapDrawable)?.let {
|
val copy = (drawable as? BitmapDrawable)
|
||||||
BitmapDrawable(
|
?.bitmap
|
||||||
view.context.resources,
|
?.copy(Bitmap.Config.HARDWARE, false)
|
||||||
it.bitmap.copy(Bitmap.Config.HARDWARE, false),
|
?.toDrawable(view.context.resources)
|
||||||
)
|
?: drawable
|
||||||
} ?: drawable
|
|
||||||
view.setImage(copy, ReaderPageImageView.Config(zoomDuration = 500))
|
view.setImage(copy, ReaderPageImageView.Config(zoomDuration = 500))
|
||||||
}
|
}
|
||||||
.build()
|
.build()
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import androidx.compose.foundation.layout.size
|
|||||||
import androidx.compose.foundation.layout.sizeIn
|
import androidx.compose.foundation.layout.sizeIn
|
||||||
import androidx.compose.foundation.lazy.LazyRow
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.text.appendInlineContent
|
||||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Brush
|
import androidx.compose.material.icons.filled.Brush
|
||||||
@@ -52,6 +53,7 @@ import androidx.compose.material3.Text
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
@@ -67,9 +69,13 @@ import androidx.compose.ui.graphics.Color
|
|||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.layout.Layout
|
import androidx.compose.ui.layout.Layout
|
||||||
|
import androidx.compose.ui.layout.onSizeChanged
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.text.LinkAnnotation
|
||||||
|
import androidx.compose.ui.text.SpanStyle
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.text.withLink
|
||||||
import androidx.compose.ui.unit.Constraints
|
import androidx.compose.ui.unit.Constraints
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@@ -77,10 +83,17 @@ import androidx.compose.ui.unit.sp
|
|||||||
import coil3.compose.AsyncImage
|
import coil3.compose.AsyncImage
|
||||||
import coil3.request.ImageRequest
|
import coil3.request.ImageRequest
|
||||||
import coil3.request.crossfade
|
import coil3.request.crossfade
|
||||||
|
import com.mikepenz.markdown.model.markdownAnnotator
|
||||||
|
import com.mikepenz.markdown.model.markdownAnnotatorConfig
|
||||||
|
import com.mikepenz.markdown.utils.getUnescapedTextInNode
|
||||||
|
import eu.kanade.domain.ui.UiPreferences
|
||||||
import eu.kanade.presentation.components.DropdownMenu
|
import eu.kanade.presentation.components.DropdownMenu
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||||
|
import org.intellij.markdown.MarkdownElementTypes
|
||||||
|
import org.intellij.markdown.MarkdownTokenTypes
|
||||||
|
import org.intellij.markdown.ast.findChildOfType
|
||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.components.material.DISABLED_ALPHA
|
import tachiyomi.presentation.core.components.material.DISABLED_ALPHA
|
||||||
@@ -90,12 +103,12 @@ import tachiyomi.presentation.core.i18n.pluralStringResource
|
|||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
import tachiyomi.presentation.core.util.clickableNoIndication
|
import tachiyomi.presentation.core.util.clickableNoIndication
|
||||||
import tachiyomi.presentation.core.util.secondaryItemAlpha
|
import tachiyomi.presentation.core.util.secondaryItemAlpha
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.time.temporal.ChronoUnit
|
import java.time.temporal.ChronoUnit
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
private val whitespaceLineRegex = Regex("[\\r\\n]{2,}", setOf(RegexOption.MULTILINE))
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MangaInfoBox(
|
fun MangaInfoBox(
|
||||||
isTabletUi: Boolean,
|
isTabletUi: Boolean,
|
||||||
@@ -236,8 +249,10 @@ fun ExpandableMangaDescription(
|
|||||||
defaultExpandState: Boolean,
|
defaultExpandState: Boolean,
|
||||||
description: String?,
|
description: String?,
|
||||||
tagsProvider: () -> List<String>?,
|
tagsProvider: () -> List<String>?,
|
||||||
|
notes: String,
|
||||||
onTagSearch: (String) -> Unit,
|
onTagSearch: (String) -> Unit,
|
||||||
onCopyTagToClipboard: (tag: String) -> Unit,
|
onCopyTagToClipboard: (tag: String) -> Unit,
|
||||||
|
onEditNotes: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Column(modifier = modifier) {
|
Column(modifier = modifier) {
|
||||||
@@ -246,15 +261,12 @@ fun ExpandableMangaDescription(
|
|||||||
}
|
}
|
||||||
val desc =
|
val desc =
|
||||||
description.takeIf { !it.isNullOrBlank() } ?: stringResource(MR.strings.description_placeholder)
|
description.takeIf { !it.isNullOrBlank() } ?: stringResource(MR.strings.description_placeholder)
|
||||||
val trimmedDescription = remember(desc) {
|
|
||||||
desc
|
|
||||||
.replace(whitespaceLineRegex, "\n")
|
|
||||||
.trimEnd()
|
|
||||||
}
|
|
||||||
MangaSummary(
|
MangaSummary(
|
||||||
expandedDescription = desc,
|
description = desc,
|
||||||
shrunkDescription = trimmedDescription,
|
|
||||||
expanded = expanded,
|
expanded = expanded,
|
||||||
|
notes = notes,
|
||||||
|
onEditNotesClicked = onEditNotes,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(top = 8.dp)
|
.padding(top = 8.dp)
|
||||||
.padding(horizontal = 16.dp)
|
.padding(horizontal = 16.dp)
|
||||||
@@ -555,42 +567,94 @@ private fun ColumnScope.MangaContentInfo(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun descriptionAnnotator(loadImages: Boolean, linkStyle: SpanStyle) = markdownAnnotator(
|
||||||
|
annotate = { content, child ->
|
||||||
|
if (!loadImages && child.type == MarkdownElementTypes.IMAGE) {
|
||||||
|
val inlineLink = child.findChildOfType(MarkdownElementTypes.INLINE_LINK)
|
||||||
|
|
||||||
|
val url = inlineLink?.findChildOfType(MarkdownElementTypes.LINK_DESTINATION)
|
||||||
|
?.getUnescapedTextInNode(content)
|
||||||
|
?: inlineLink?.findChildOfType(MarkdownElementTypes.AUTOLINK)
|
||||||
|
?.findChildOfType(MarkdownTokenTypes.AUTOLINK)
|
||||||
|
?.getUnescapedTextInNode(content)
|
||||||
|
?: return@markdownAnnotator false
|
||||||
|
|
||||||
|
val textNode = inlineLink?.findChildOfType(MarkdownElementTypes.LINK_TITLE)
|
||||||
|
?: inlineLink?.findChildOfType(MarkdownElementTypes.LINK_TEXT)
|
||||||
|
val altText = textNode?.findChildOfType(MarkdownTokenTypes.TEXT)
|
||||||
|
?.getUnescapedTextInNode(content).orEmpty()
|
||||||
|
|
||||||
|
withLink(LinkAnnotation.Url(url = url)) {
|
||||||
|
pushStyle(linkStyle)
|
||||||
|
appendInlineContent(MARKDOWN_INLINE_IMAGE_TAG)
|
||||||
|
append(altText)
|
||||||
|
pop()
|
||||||
|
}
|
||||||
|
|
||||||
|
return@markdownAnnotator true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (child.type in DISALLOWED_MARKDOWN_TYPES) {
|
||||||
|
append(content.substring(child.startOffset, child.endOffset))
|
||||||
|
return@markdownAnnotator true
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
},
|
||||||
|
config = markdownAnnotatorConfig(
|
||||||
|
eolAsNewLine = true,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun MangaSummary(
|
private fun MangaSummary(
|
||||||
expandedDescription: String,
|
description: String,
|
||||||
shrunkDescription: String,
|
notes: String,
|
||||||
expanded: Boolean,
|
expanded: Boolean,
|
||||||
|
onEditNotesClicked: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
val preferences = remember { Injekt.get<UiPreferences>() }
|
||||||
|
val loadImages = remember { preferences.imagesInDescription().get() }
|
||||||
val animProgress by animateFloatAsState(
|
val animProgress by animateFloatAsState(
|
||||||
targetValue = if (expanded) 1f else 0f,
|
targetValue = if (expanded) 1f else 0f,
|
||||||
label = "summary",
|
label = "summary",
|
||||||
)
|
)
|
||||||
|
var infoHeight by remember { mutableIntStateOf(0) }
|
||||||
Layout(
|
Layout(
|
||||||
modifier = modifier.clipToBounds(),
|
modifier = modifier.clipToBounds(),
|
||||||
contents = listOf(
|
contents = listOf(
|
||||||
{
|
{
|
||||||
Text(
|
Text(
|
||||||
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,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Text(
|
Column(
|
||||||
text = expandedDescription,
|
modifier = Modifier.onSizeChanged { size ->
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
infoHeight = size.height
|
||||||
)
|
|
||||||
},
|
},
|
||||||
{
|
) {
|
||||||
|
MangaNotesSection(
|
||||||
|
content = notes,
|
||||||
|
expanded = expanded,
|
||||||
|
onEditNotes = onEditNotesClicked,
|
||||||
|
)
|
||||||
SelectionContainer {
|
SelectionContainer {
|
||||||
Text(
|
MarkdownRender(
|
||||||
text = if (expanded) expandedDescription else shrunkDescription,
|
content = description,
|
||||||
maxLines = Int.MAX_VALUE,
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
color = MaterialTheme.colorScheme.onBackground,
|
|
||||||
modifier = Modifier.secondaryItemAlpha(),
|
modifier = Modifier.secondaryItemAlpha(),
|
||||||
|
annotator = descriptionAnnotator(
|
||||||
|
loadImages = loadImages,
|
||||||
|
linkStyle = getMarkdownLinkStyle().toSpanStyle(),
|
||||||
|
),
|
||||||
|
loadImages = loadImages,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
val colors = listOf(Color.Transparent, MaterialTheme.colorScheme.background)
|
val colors = listOf(Color.Transparent, MaterialTheme.colorScheme.background)
|
||||||
@@ -610,14 +674,11 @@ private fun MangaSummary(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
) { (shrunk, expanded, actual, scrim), constraints ->
|
) { (shrunk, actual, scrim), constraints ->
|
||||||
val shrunkHeight = shrunk.single()
|
val shrunkHeight = shrunk.single()
|
||||||
.measure(constraints)
|
.measure(constraints)
|
||||||
.height
|
.height
|
||||||
val expandedHeight = expanded.single()
|
val heightDelta = infoHeight - shrunkHeight
|
||||||
.measure(constraints)
|
|
||||||
.height
|
|
||||||
val heightDelta = expandedHeight - shrunkHeight
|
|
||||||
val scrimHeight = 24.dp.roundToPx()
|
val scrimHeight = 24.dp.roundToPx()
|
||||||
|
|
||||||
val actualPlaceable = actual.single()
|
val actualPlaceable = actual.single()
|
||||||
|
|||||||
@@ -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),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -37,6 +37,7 @@ fun MangaToolbar(
|
|||||||
onClickEditCategory: (() -> Unit)?,
|
onClickEditCategory: (() -> Unit)?,
|
||||||
onClickRefresh: () -> Unit,
|
onClickRefresh: () -> Unit,
|
||||||
onClickMigrate: (() -> Unit)?,
|
onClickMigrate: (() -> Unit)?,
|
||||||
|
onClickEditNotes: () -> Unit,
|
||||||
|
|
||||||
// For action mode
|
// For action mode
|
||||||
actionModeCounter: Int,
|
actionModeCounter: Int,
|
||||||
@@ -140,6 +141,12 @@ fun MangaToolbar(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
add(
|
||||||
|
AppBar.OverflowAction(
|
||||||
|
title = stringResource(MR.strings.action_notes),
|
||||||
|
onClick = onClickEditNotes,
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
.build(),
|
.build(),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,287 @@
|
|||||||
|
package eu.kanade.presentation.manga.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.text.InlineTextContent
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.Image
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.runtime.ReadOnlyComposable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.layout.FirstBaseline
|
||||||
|
import androidx.compose.ui.text.Placeholder
|
||||||
|
import androidx.compose.ui.text.PlaceholderVerticalAlign
|
||||||
|
import androidx.compose.ui.text.SpanStyle
|
||||||
|
import androidx.compose.ui.text.TextLinkStyles
|
||||||
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
|
import androidx.compose.ui.text.font.FontStyle
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.mikepenz.markdown.coil3.Coil3ImageTransformerImpl
|
||||||
|
import com.mikepenz.markdown.compose.LocalBulletListHandler
|
||||||
|
import com.mikepenz.markdown.compose.Markdown
|
||||||
|
import com.mikepenz.markdown.compose.components.markdownComponents
|
||||||
|
import com.mikepenz.markdown.compose.elements.MarkdownBulletList
|
||||||
|
import com.mikepenz.markdown.compose.elements.MarkdownDivider
|
||||||
|
import com.mikepenz.markdown.compose.elements.MarkdownOrderedList
|
||||||
|
import com.mikepenz.markdown.compose.elements.MarkdownTable
|
||||||
|
import com.mikepenz.markdown.compose.elements.MarkdownTableHeader
|
||||||
|
import com.mikepenz.markdown.compose.elements.MarkdownTableRow
|
||||||
|
import com.mikepenz.markdown.compose.elements.MarkdownText
|
||||||
|
import com.mikepenz.markdown.compose.elements.listDepth
|
||||||
|
import com.mikepenz.markdown.model.DefaultMarkdownColors
|
||||||
|
import com.mikepenz.markdown.model.DefaultMarkdownInlineContent
|
||||||
|
import com.mikepenz.markdown.model.DefaultMarkdownTypography
|
||||||
|
import com.mikepenz.markdown.model.MarkdownAnnotator
|
||||||
|
import com.mikepenz.markdown.model.MarkdownColors
|
||||||
|
import com.mikepenz.markdown.model.MarkdownPadding
|
||||||
|
import com.mikepenz.markdown.model.MarkdownTypography
|
||||||
|
import com.mikepenz.markdown.model.NoOpImageTransformerImpl
|
||||||
|
import com.mikepenz.markdown.model.markdownAnnotator
|
||||||
|
import com.mikepenz.markdown.model.rememberMarkdownState
|
||||||
|
import org.intellij.markdown.MarkdownTokenTypes.Companion.HTML_TAG
|
||||||
|
import org.intellij.markdown.flavours.MarkdownFlavourDescriptor
|
||||||
|
import org.intellij.markdown.flavours.commonmark.CommonMarkFlavourDescriptor
|
||||||
|
import org.intellij.markdown.flavours.commonmark.CommonMarkMarkerProcessor
|
||||||
|
import org.intellij.markdown.flavours.gfm.table.GitHubTableMarkerProvider
|
||||||
|
import org.intellij.markdown.parser.MarkerProcessor
|
||||||
|
import org.intellij.markdown.parser.MarkerProcessorFactory
|
||||||
|
import org.intellij.markdown.parser.ProductionHolder
|
||||||
|
import org.intellij.markdown.parser.constraints.CommonMarkdownConstraints
|
||||||
|
import org.intellij.markdown.parser.constraints.MarkdownConstraints
|
||||||
|
import org.intellij.markdown.parser.markerblocks.MarkerBlockProvider
|
||||||
|
import org.intellij.markdown.parser.markerblocks.providers.AtxHeaderProvider
|
||||||
|
import org.intellij.markdown.parser.markerblocks.providers.BlockQuoteProvider
|
||||||
|
import org.intellij.markdown.parser.markerblocks.providers.CodeBlockProvider
|
||||||
|
import org.intellij.markdown.parser.markerblocks.providers.CodeFenceProvider
|
||||||
|
import org.intellij.markdown.parser.markerblocks.providers.HorizontalRuleProvider
|
||||||
|
import org.intellij.markdown.parser.markerblocks.providers.ListMarkerProvider
|
||||||
|
import org.intellij.markdown.parser.markerblocks.providers.SetextHeaderProvider
|
||||||
|
import tachiyomi.presentation.core.components.material.padding
|
||||||
|
|
||||||
|
const val MARKDOWN_INLINE_IMAGE_TAG = "MARKDOWN_INLINE_IMAGE"
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MarkdownRender(
|
||||||
|
content: String,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
flavour: MarkdownFlavourDescriptor = SimpleMarkdownFlavourDescriptor,
|
||||||
|
annotator: MarkdownAnnotator = remember { markdownAnnotator() },
|
||||||
|
loadImages: Boolean = true,
|
||||||
|
) {
|
||||||
|
Markdown(
|
||||||
|
markdownState = rememberMarkdownState(
|
||||||
|
content = content,
|
||||||
|
flavour = flavour,
|
||||||
|
immediate = true,
|
||||||
|
),
|
||||||
|
annotator = annotator,
|
||||||
|
colors = getMarkdownColors(),
|
||||||
|
typography = getMarkdownTypography(),
|
||||||
|
padding = markdownPadding,
|
||||||
|
components = markdownComponents,
|
||||||
|
imageTransformer = remember(loadImages) {
|
||||||
|
if (loadImages) Coil3ImageTransformerImpl else NoOpImageTransformerImpl()
|
||||||
|
},
|
||||||
|
inlineContent = getMarkdownInlineContent(),
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@ReadOnlyComposable
|
||||||
|
private fun getMarkdownColors(): MarkdownColors {
|
||||||
|
val codeBackground = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f)
|
||||||
|
return DefaultMarkdownColors(
|
||||||
|
text = MaterialTheme.colorScheme.onSurface,
|
||||||
|
codeBackground = codeBackground,
|
||||||
|
inlineCodeBackground = codeBackground,
|
||||||
|
dividerColor = MaterialTheme.colorScheme.outlineVariant,
|
||||||
|
tableBackground = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.05f),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@ReadOnlyComposable
|
||||||
|
fun getMarkdownLinkStyle() = MaterialTheme.typography.bodyMedium.copy(
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@ReadOnlyComposable
|
||||||
|
private fun getMarkdownTypography(): MarkdownTypography {
|
||||||
|
val link = getMarkdownLinkStyle()
|
||||||
|
return DefaultMarkdownTypography(
|
||||||
|
h1 = MaterialTheme.typography.headlineMedium,
|
||||||
|
h2 = MaterialTheme.typography.headlineSmall,
|
||||||
|
h3 = MaterialTheme.typography.titleLarge,
|
||||||
|
h4 = MaterialTheme.typography.titleMedium,
|
||||||
|
h5 = MaterialTheme.typography.titleSmall,
|
||||||
|
h6 = MaterialTheme.typography.bodyLarge,
|
||||||
|
text = MaterialTheme.typography.bodyMedium,
|
||||||
|
code = MaterialTheme.typography.bodyMedium.copy(fontFamily = FontFamily.Monospace),
|
||||||
|
inlineCode = MaterialTheme.typography.bodyMedium.copy(fontFamily = FontFamily.Monospace),
|
||||||
|
quote = MaterialTheme.typography.bodyMedium.plus(SpanStyle(fontStyle = FontStyle.Italic)),
|
||||||
|
paragraph = MaterialTheme.typography.bodyMedium,
|
||||||
|
ordered = MaterialTheme.typography.bodyMedium,
|
||||||
|
bullet = MaterialTheme.typography.bodyMedium,
|
||||||
|
list = MaterialTheme.typography.bodyMedium,
|
||||||
|
textLink = TextLinkStyles(style = link.toSpanStyle()),
|
||||||
|
table = MaterialTheme.typography.bodyMedium,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val markdownPadding = object : MarkdownPadding {
|
||||||
|
override val block: Dp = 2.dp
|
||||||
|
override val blockQuote: PaddingValues = PaddingValues(horizontal = 16.dp, vertical = 0.dp)
|
||||||
|
override val blockQuoteBar: PaddingValues.Absolute = PaddingValues.Absolute(
|
||||||
|
left = 4.dp,
|
||||||
|
top = 2.dp,
|
||||||
|
right = 4.dp,
|
||||||
|
bottom = 2.dp,
|
||||||
|
)
|
||||||
|
override val blockQuoteText: PaddingValues = PaddingValues(vertical = 4.dp)
|
||||||
|
override val codeBlock: PaddingValues = PaddingValues(8.dp)
|
||||||
|
override val list: Dp = 0.dp
|
||||||
|
override val listIndent: Dp = 8.dp
|
||||||
|
override val listItemBottom: Dp = 0.dp
|
||||||
|
override val listItemTop: Dp = 0.dp
|
||||||
|
}
|
||||||
|
|
||||||
|
private val markdownComponents = markdownComponents(
|
||||||
|
horizontalRule = {
|
||||||
|
MarkdownDivider(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(vertical = MaterialTheme.padding.extraSmall)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
orderedList = { ol ->
|
||||||
|
Column(modifier = Modifier.padding(start = MaterialTheme.padding.small)) {
|
||||||
|
MarkdownOrderedList(
|
||||||
|
content = ol.content,
|
||||||
|
node = ol.node,
|
||||||
|
style = ol.typography.ordered,
|
||||||
|
depth = ol.listDepth,
|
||||||
|
markerModifier = { Modifier.alignBy(FirstBaseline) },
|
||||||
|
listModifier = { Modifier.alignBy(FirstBaseline) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
unorderedList = { ul ->
|
||||||
|
val markers = listOf("•", "◦", "▸", "▹")
|
||||||
|
|
||||||
|
CompositionLocalProvider(
|
||||||
|
LocalBulletListHandler provides { _, _, _, _, _ -> "${markers[ul.listDepth % markers.size]} " },
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.padding(start = MaterialTheme.padding.small)) {
|
||||||
|
MarkdownBulletList(
|
||||||
|
content = ul.content,
|
||||||
|
node = ul.node,
|
||||||
|
style = ul.typography.bullet,
|
||||||
|
markerModifier = { Modifier.alignBy(FirstBaseline) },
|
||||||
|
listModifier = { Modifier.alignBy(FirstBaseline) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
table = { t ->
|
||||||
|
MarkdownTable(
|
||||||
|
content = t.content,
|
||||||
|
node = t.node,
|
||||||
|
style = t.typography.text,
|
||||||
|
headerBlock = { content, header, tableWidth, style ->
|
||||||
|
MarkdownTableHeader(
|
||||||
|
content = content,
|
||||||
|
header = header,
|
||||||
|
tableWidth = tableWidth,
|
||||||
|
style = style,
|
||||||
|
maxLines = Int.MAX_VALUE,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
rowBlock = { content, header, tableWidth, style ->
|
||||||
|
MarkdownTableRow(
|
||||||
|
content = content,
|
||||||
|
header = header,
|
||||||
|
tableWidth = tableWidth,
|
||||||
|
style = style,
|
||||||
|
maxLines = Int.MAX_VALUE,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
custom = { type, model ->
|
||||||
|
if (type in DISALLOWED_MARKDOWN_TYPES) {
|
||||||
|
MarkdownText(
|
||||||
|
content = model.content.substring(model.node.startOffset, model.node.endOffset),
|
||||||
|
style = model.typography.text,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@ReadOnlyComposable
|
||||||
|
private fun getMarkdownInlineContent() = DefaultMarkdownInlineContent(
|
||||||
|
inlineContent = mapOf(
|
||||||
|
MARKDOWN_INLINE_IMAGE_TAG to InlineTextContent(
|
||||||
|
placeholder = Placeholder(
|
||||||
|
width = MaterialTheme.typography.bodyMedium.fontSize * 1.25,
|
||||||
|
height = MaterialTheme.typography.bodyMedium.fontSize * 1.25,
|
||||||
|
placeholderVerticalAlign = PlaceholderVerticalAlign.TextCenter,
|
||||||
|
),
|
||||||
|
children = {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.Image,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
private object SimpleMarkdownFlavourDescriptor : CommonMarkFlavourDescriptor() {
|
||||||
|
override val markerProcessorFactory: MarkerProcessorFactory = SimpleMarkdownProcessFactory
|
||||||
|
}
|
||||||
|
|
||||||
|
private object SimpleMarkdownProcessFactory : MarkerProcessorFactory {
|
||||||
|
override fun createMarkerProcessor(productionHolder: ProductionHolder): MarkerProcessor<*> {
|
||||||
|
return SimpleMarkdownMarkerProcessor(productionHolder, CommonMarkdownConstraints.BASE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Like `CommonMarkFlavour`, but with html blocks and reference links removed and
|
||||||
|
* table support added
|
||||||
|
*/
|
||||||
|
private class SimpleMarkdownMarkerProcessor(
|
||||||
|
productionHolder: ProductionHolder,
|
||||||
|
constraints: MarkdownConstraints,
|
||||||
|
) : CommonMarkMarkerProcessor(productionHolder, constraints) {
|
||||||
|
private val markerBlockProviders = listOf(
|
||||||
|
CodeBlockProvider(),
|
||||||
|
HorizontalRuleProvider(),
|
||||||
|
CodeFenceProvider(),
|
||||||
|
SetextHeaderProvider(),
|
||||||
|
BlockQuoteProvider(),
|
||||||
|
ListMarkerProvider(),
|
||||||
|
AtxHeaderProvider(),
|
||||||
|
GitHubTableMarkerProvider(),
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun getMarkerBlockProviders(): List<MarkerBlockProvider<StateInfo>> {
|
||||||
|
return markerBlockProviders
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val DISALLOWED_MARKDOWN_TYPES = arrayOf(HTML_TAG)
|
||||||
@@ -4,6 +4,7 @@ import androidx.compose.foundation.layout.padding
|
|||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.outlined.HelpOutline
|
import androidx.compose.material.icons.automirrored.outlined.HelpOutline
|
||||||
import androidx.compose.material.icons.automirrored.outlined.Label
|
import androidx.compose.material.icons.automirrored.outlined.Label
|
||||||
|
import androidx.compose.material.icons.outlined.AttachMoney
|
||||||
import androidx.compose.material.icons.outlined.CloudOff
|
import androidx.compose.material.icons.outlined.CloudOff
|
||||||
import androidx.compose.material.icons.outlined.GetApp
|
import androidx.compose.material.icons.outlined.GetApp
|
||||||
import androidx.compose.material.icons.outlined.Info
|
import androidx.compose.material.icons.outlined.Info
|
||||||
@@ -145,6 +146,13 @@ fun MoreScreen(
|
|||||||
onPreferenceClick = { uriHandler.openUri(Constants.URL_HELP) },
|
onPreferenceClick = { uriHandler.openUri(Constants.URL_HELP) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
item {
|
||||||
|
TextPreferenceWidget(
|
||||||
|
title = stringResource(MR.strings.label_donate),
|
||||||
|
icon = Icons.Outlined.AttachMoney,
|
||||||
|
onPreferenceClick = { uriHandler.openUri(Constants.URL_DONATE) },
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package eu.kanade.presentation.more
|
package eu.kanade.presentation.more
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
@@ -13,13 +14,10 @@ import androidx.compose.material3.Text
|
|||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.text.SpanStyle
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||||
import com.halilibo.richtext.markdown.Markdown
|
import eu.kanade.presentation.manga.components.MarkdownRender
|
||||||
import com.halilibo.richtext.ui.RichTextStyle
|
|
||||||
import com.halilibo.richtext.ui.material3.RichText
|
|
||||||
import com.halilibo.richtext.ui.string.RichTextStringStyle
|
|
||||||
import eu.kanade.presentation.theme.TachiyomiPreviewTheme
|
import eu.kanade.presentation.theme.TachiyomiPreviewTheme
|
||||||
|
import org.intellij.markdown.flavours.gfm.GFMFlavourDescriptor
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.components.material.padding
|
import tachiyomi.presentation.core.components.material.padding
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
@@ -42,17 +40,15 @@ fun NewUpdateScreen(
|
|||||||
rejectText = stringResource(MR.strings.action_not_now),
|
rejectText = stringResource(MR.strings.action_not_now),
|
||||||
onRejectClick = onRejectUpdate,
|
onRejectClick = onRejectUpdate,
|
||||||
) {
|
) {
|
||||||
RichText(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(vertical = MaterialTheme.padding.large),
|
.padding(vertical = MaterialTheme.padding.large),
|
||||||
style = RichTextStyle(
|
|
||||||
stringStyle = RichTextStringStyle(
|
|
||||||
linkStyle = SpanStyle(color = MaterialTheme.colorScheme.primary),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
) {
|
) {
|
||||||
Markdown(content = changelogInfo)
|
MarkdownRender(
|
||||||
|
content = changelogInfo,
|
||||||
|
flavour = GFMFlavourDescriptor(),
|
||||||
|
)
|
||||||
|
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = onOpenInBrowser,
|
onClick = onOpenInBrowser,
|
||||||
|
|||||||
@@ -42,7 +42,9 @@ fun OnboardingScreen(
|
|||||||
}
|
}
|
||||||
val isLastStep = currentStep == steps.lastIndex
|
val isLastStep = currentStep == steps.lastIndex
|
||||||
|
|
||||||
BackHandler(enabled = currentStep != 0, onBack = { currentStep-- })
|
BackHandler(enabled = currentStep != 0) {
|
||||||
|
currentStep--
|
||||||
|
}
|
||||||
|
|
||||||
InfoScreen(
|
InfoScreen(
|
||||||
icon = Icons.Outlined.RocketLaunch,
|
icon = Icons.Outlined.RocketLaunch,
|
||||||
|
|||||||
@@ -15,10 +15,10 @@ sealed class Preference {
|
|||||||
abstract val title: String
|
abstract val title: String
|
||||||
abstract val enabled: Boolean
|
abstract val enabled: Boolean
|
||||||
|
|
||||||
sealed class PreferenceItem<T> : Preference() {
|
sealed class PreferenceItem<T, R> : Preference() {
|
||||||
abstract val subtitle: String?
|
abstract val subtitle: String?
|
||||||
abstract val icon: ImageVector?
|
abstract val icon: ImageVector?
|
||||||
abstract val onValueChanged: suspend (value: T) -> Boolean
|
abstract val onValueChanged: suspend (value: T) -> R
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A basic [PreferenceItem] that only displays texts.
|
* A basic [PreferenceItem] that only displays texts.
|
||||||
@@ -28,9 +28,9 @@ sealed class Preference {
|
|||||||
override val subtitle: String? = null,
|
override val subtitle: String? = null,
|
||||||
override val enabled: Boolean = true,
|
override val enabled: Boolean = true,
|
||||||
val onClick: (() -> Unit)? = null,
|
val onClick: (() -> Unit)? = null,
|
||||||
) : PreferenceItem<String>() {
|
) : PreferenceItem<String, Unit>() {
|
||||||
override val icon: ImageVector? = null
|
override val icon: ImageVector? = null
|
||||||
override val onValueChanged: suspend (value: String) -> Boolean = { true }
|
override val onValueChanged: suspend (value: String) -> Unit = {}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -42,7 +42,7 @@ sealed class Preference {
|
|||||||
override val subtitle: String? = null,
|
override val subtitle: String? = null,
|
||||||
override val enabled: Boolean = true,
|
override val enabled: Boolean = true,
|
||||||
override val onValueChanged: suspend (value: Boolean) -> Boolean = { true },
|
override val onValueChanged: suspend (value: Boolean) -> Boolean = { true },
|
||||||
) : PreferenceItem<Boolean>() {
|
) : PreferenceItem<Boolean, Boolean>() {
|
||||||
override val icon: ImageVector? = null
|
override val icon: ImageVector? = null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,12 +52,13 @@ sealed class Preference {
|
|||||||
data class SliderPreference(
|
data class SliderPreference(
|
||||||
val value: Int,
|
val value: Int,
|
||||||
override val title: String,
|
override val title: String,
|
||||||
|
override val subtitle: String? = null,
|
||||||
|
val valueString: String? = null,
|
||||||
val valueRange: IntProgression = 0..1,
|
val valueRange: IntProgression = 0..1,
|
||||||
@IntRange(from = 0) val steps: Int = with(valueRange) { (last - first) - 1 },
|
@IntRange(from = 0) val steps: Int = with(valueRange) { (last - first) - 1 },
|
||||||
override val subtitle: String? = null,
|
|
||||||
override val enabled: Boolean = true,
|
override val enabled: Boolean = true,
|
||||||
override val onValueChanged: suspend (value: Int) -> Boolean = { true },
|
override val onValueChanged: suspend (value: Int) -> Unit = {},
|
||||||
) : PreferenceItem<Int>() {
|
) : PreferenceItem<Int, Unit>() {
|
||||||
override val icon: ImageVector? = null
|
override val icon: ImageVector? = null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,7 +76,7 @@ sealed class Preference {
|
|||||||
override val icon: ImageVector? = null,
|
override val icon: ImageVector? = null,
|
||||||
override val enabled: Boolean = true,
|
override val enabled: Boolean = true,
|
||||||
override val onValueChanged: suspend (value: T) -> Boolean = { true },
|
override val onValueChanged: suspend (value: T) -> Boolean = { true },
|
||||||
) : PreferenceItem<T>() {
|
) : PreferenceItem<T, Boolean>() {
|
||||||
internal fun internalSet(value: Any) = preference.set(value as T)
|
internal fun internalSet(value: Any) = preference.set(value as T)
|
||||||
internal suspend fun internalOnValueChanged(value: Any) = onValueChanged(value as T)
|
internal suspend fun internalOnValueChanged(value: Any) = onValueChanged(value as T)
|
||||||
|
|
||||||
@@ -96,8 +97,8 @@ sealed class Preference {
|
|||||||
{ v, e -> subtitle?.format(e[v]) },
|
{ v, e -> subtitle?.format(e[v]) },
|
||||||
override val icon: ImageVector? = null,
|
override val icon: ImageVector? = null,
|
||||||
override val enabled: Boolean = true,
|
override val enabled: Boolean = true,
|
||||||
override val onValueChanged: suspend (value: String) -> Boolean = { true },
|
override val onValueChanged: suspend (value: String) -> Unit = {},
|
||||||
) : PreferenceItem<String>()
|
) : PreferenceItem<String, Unit>()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A [PreferenceItem] that displays a list of entries as a dialog.
|
* A [PreferenceItem] that displays a list of entries as a dialog.
|
||||||
@@ -121,7 +122,7 @@ sealed class Preference {
|
|||||||
override val icon: ImageVector? = null,
|
override val icon: ImageVector? = null,
|
||||||
override val enabled: Boolean = true,
|
override val enabled: Boolean = true,
|
||||||
override val onValueChanged: suspend (value: Set<String>) -> Boolean = { true },
|
override val onValueChanged: suspend (value: Set<String>) -> Boolean = { true },
|
||||||
) : PreferenceItem<Set<String>>()
|
) : PreferenceItem<Set<String>, Boolean>()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A [PreferenceItem] that shows a EditText in the dialog.
|
* A [PreferenceItem] that shows a EditText in the dialog.
|
||||||
@@ -132,7 +133,7 @@ sealed class Preference {
|
|||||||
override val subtitle: String? = "%s",
|
override val subtitle: String? = "%s",
|
||||||
override val enabled: Boolean = true,
|
override val enabled: Boolean = true,
|
||||||
override val onValueChanged: suspend (value: String) -> Boolean = { true },
|
override val onValueChanged: suspend (value: String) -> Boolean = { true },
|
||||||
) : PreferenceItem<String>() {
|
) : PreferenceItem<String, Boolean>() {
|
||||||
override val icon: ImageVector? = null
|
override val icon: ImageVector? = null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,31 +144,31 @@ sealed class Preference {
|
|||||||
val tracker: Tracker,
|
val tracker: Tracker,
|
||||||
val login: () -> Unit,
|
val login: () -> Unit,
|
||||||
val logout: () -> Unit,
|
val logout: () -> Unit,
|
||||||
) : PreferenceItem<String>() {
|
) : PreferenceItem<String, Unit>() {
|
||||||
override val title: String = ""
|
override val title: String = ""
|
||||||
override val enabled: Boolean = true
|
override val enabled: Boolean = true
|
||||||
override val subtitle: String? = null
|
override val subtitle: String? = null
|
||||||
override val icon: ImageVector? = null
|
override val icon: ImageVector? = null
|
||||||
override val onValueChanged: suspend (value: String) -> Boolean = { true }
|
override val onValueChanged: suspend (value: String) -> Unit = {}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class InfoPreference(
|
data class InfoPreference(
|
||||||
override val title: String,
|
override val title: String,
|
||||||
) : PreferenceItem<String>() {
|
) : PreferenceItem<String, Unit>() {
|
||||||
override val enabled: Boolean = true
|
override val enabled: Boolean = true
|
||||||
override val subtitle: String? = null
|
override val subtitle: String? = null
|
||||||
override val icon: ImageVector? = null
|
override val icon: ImageVector? = null
|
||||||
override val onValueChanged: suspend (value: String) -> Boolean = { true }
|
override val onValueChanged: suspend (value: String) -> Unit = {}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class CustomPreference(
|
data class CustomPreference(
|
||||||
override val title: String,
|
override val title: String,
|
||||||
val content: @Composable () -> Unit,
|
val content: @Composable () -> Unit,
|
||||||
) : PreferenceItem<Unit>() {
|
) : PreferenceItem<Unit, Unit>() {
|
||||||
override val enabled: Boolean = true
|
override val enabled: Boolean = true
|
||||||
override val subtitle: String? = null
|
override val subtitle: String? = null
|
||||||
override val icon: ImageVector? = null
|
override val icon: ImageVector? = null
|
||||||
override val onValueChanged: suspend (value: Unit) -> Boolean = { true }
|
override val onValueChanged: suspend (value: Unit) -> Unit = {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,6 +176,6 @@ sealed class Preference {
|
|||||||
override val title: String,
|
override val title: String,
|
||||||
override val enabled: Boolean = true,
|
override val enabled: Boolean = true,
|
||||||
|
|
||||||
val preferenceItems: ImmutableList<PreferenceItem<out Any>>,
|
val preferenceItems: ImmutableList<PreferenceItem<out Any, out Any>>,
|
||||||
) : Preference()
|
) : Preference()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ val LocalPreferenceMinHeight = compositionLocalOf(structuralEqualityPolicy()) {
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun StatusWrapper(
|
fun StatusWrapper(
|
||||||
item: Preference.PreferenceItem<*>,
|
item: Preference.PreferenceItem<*, *>,
|
||||||
highlightKey: String?,
|
highlightKey: String?,
|
||||||
content: @Composable () -> Unit,
|
content: @Composable () -> Unit,
|
||||||
) {
|
) {
|
||||||
@@ -56,7 +56,7 @@ fun StatusWrapper(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun PreferenceItem(
|
internal fun PreferenceItem(
|
||||||
item: Preference.PreferenceItem<*>,
|
item: Preference.PreferenceItem<*, *>,
|
||||||
highlightKey: String?,
|
highlightKey: String?,
|
||||||
) {
|
) {
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
@@ -83,17 +83,18 @@ internal fun PreferenceItem(
|
|||||||
}
|
}
|
||||||
is Preference.PreferenceItem.SliderPreference -> {
|
is Preference.PreferenceItem.SliderPreference -> {
|
||||||
BaseSliderItem(
|
BaseSliderItem(
|
||||||
label = item.title,
|
|
||||||
value = item.value,
|
value = item.value,
|
||||||
valueRange = item.valueRange,
|
valueRange = item.valueRange,
|
||||||
valueText = item.subtitle.takeUnless { it.isNullOrEmpty() } ?: item.value.toString(),
|
|
||||||
steps = item.steps,
|
steps = item.steps,
|
||||||
labelStyle = MaterialTheme.typography.titleLarge.copy(fontSize = TitleFontSize),
|
title = item.title,
|
||||||
|
subtitle = item.subtitle,
|
||||||
|
valueString = item.valueString.takeUnless { it.isNullOrEmpty() } ?: item.value.toString(),
|
||||||
onChange = {
|
onChange = {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
item.onValueChanged(it)
|
item.onValueChanged(it)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
titleStyle = MaterialTheme.typography.titleLarge.copy(fontSize = TitleFontSize),
|
||||||
modifier = Modifier.padding(
|
modifier = Modifier.padding(
|
||||||
horizontal = PrefsHorizontalPadding,
|
horizontal = PrefsHorizontalPadding,
|
||||||
vertical = PrefsVerticalPadding,
|
vertical = PrefsVerticalPadding,
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ fun PreferenceScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create Preference Item
|
// Create Preference Item
|
||||||
is Preference.PreferenceItem<*> -> item {
|
is Preference.PreferenceItem<*, *> -> item {
|
||||||
PreferenceItem(
|
PreferenceItem(
|
||||||
item = preference,
|
item = preference,
|
||||||
highlightKey = highlightKey,
|
highlightKey = highlightKey,
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ import tachiyomi.core.common.util.lang.launchNonCancellable
|
|||||||
import tachiyomi.core.common.util.lang.withUIContext
|
import tachiyomi.core.common.util.lang.withUIContext
|
||||||
import tachiyomi.core.common.util.system.ImageUtil
|
import tachiyomi.core.common.util.system.ImageUtil
|
||||||
import tachiyomi.core.common.util.system.logcat
|
import tachiyomi.core.common.util.system.logcat
|
||||||
|
import tachiyomi.domain.library.service.LibraryPreferences
|
||||||
import tachiyomi.domain.manga.interactor.ResetViewerFlags
|
import tachiyomi.domain.manga.interactor.ResetViewerFlags
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
@@ -85,6 +86,7 @@ object SettingsAdvancedScreen : SearchableSettings {
|
|||||||
|
|
||||||
val basePreferences = remember { Injekt.get<BasePreferences>() }
|
val basePreferences = remember { Injekt.get<BasePreferences>() }
|
||||||
val networkPreferences = remember { Injekt.get<NetworkPreferences>() }
|
val networkPreferences = remember { Injekt.get<NetworkPreferences>() }
|
||||||
|
val libraryPreferences = remember { Injekt.get<LibraryPreferences>() }
|
||||||
|
|
||||||
return listOf(
|
return listOf(
|
||||||
Preference.PreferenceItem.TextPreference(
|
Preference.PreferenceItem.TextPreference(
|
||||||
@@ -125,7 +127,7 @@ object SettingsAdvancedScreen : SearchableSettings {
|
|||||||
getBackgroundActivityGroup(),
|
getBackgroundActivityGroup(),
|
||||||
getDataGroup(),
|
getDataGroup(),
|
||||||
getNetworkGroup(networkPreferences = networkPreferences),
|
getNetworkGroup(networkPreferences = networkPreferences),
|
||||||
getLibraryGroup(),
|
getLibraryGroup(libraryPreferences = libraryPreferences),
|
||||||
getReaderGroup(basePreferences = basePreferences),
|
getReaderGroup(basePreferences = basePreferences),
|
||||||
getExtensionsGroup(basePreferences = basePreferences),
|
getExtensionsGroup(basePreferences = basePreferences),
|
||||||
)
|
)
|
||||||
@@ -286,7 +288,9 @@ object SettingsAdvancedScreen : SearchableSettings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun getLibraryGroup(): Preference.PreferenceGroup {
|
private fun getLibraryGroup(
|
||||||
|
libraryPreferences: LibraryPreferences,
|
||||||
|
): Preference.PreferenceGroup {
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
@@ -314,6 +318,16 @@ object SettingsAdvancedScreen : SearchableSettings {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
Preference.PreferenceItem.SwitchPreference(
|
||||||
|
preference = libraryPreferences.updateMangaTitles(),
|
||||||
|
title = stringResource(MR.strings.pref_update_library_manga_titles),
|
||||||
|
subtitle = stringResource(MR.strings.pref_update_library_manga_titles_summary),
|
||||||
|
),
|
||||||
|
Preference.PreferenceItem.SwitchPreference(
|
||||||
|
preference = libraryPreferences.disallowNonAsciiFilenames(),
|
||||||
|
title = stringResource(MR.strings.pref_disallow_non_ascii_filenames),
|
||||||
|
subtitle = stringResource(MR.strings.pref_disallow_non_ascii_filenames_details),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -145,6 +145,10 @@ object SettingsAppearanceScreen : SearchableSettings {
|
|||||||
formattedNow,
|
formattedNow,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
Preference.PreferenceItem.SwitchPreference(
|
||||||
|
preference = uiPreferences.imagesInDescription(),
|
||||||
|
title = stringResource(MR.strings.pref_display_images_description),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ object SettingsDownloadScreen : SearchableSettings {
|
|||||||
val allCategories by getCategories.subscribe().collectAsState(initial = emptyList())
|
val allCategories by getCategories.subscribe().collectAsState(initial = emptyList())
|
||||||
|
|
||||||
val downloadPreferences = remember { Injekt.get<DownloadPreferences>() }
|
val downloadPreferences = remember { Injekt.get<DownloadPreferences>() }
|
||||||
|
val parallelSourceLimit by downloadPreferences.parallelSourceLimit().collectAsState()
|
||||||
|
val parallelPageLimit by downloadPreferences.parallelPageLimit().collectAsState()
|
||||||
return listOf(
|
return listOf(
|
||||||
Preference.PreferenceItem.SwitchPreference(
|
Preference.PreferenceItem.SwitchPreference(
|
||||||
preference = downloadPreferences.downloadOnlyOverWifi(),
|
preference = downloadPreferences.downloadOnlyOverWifi(),
|
||||||
@@ -51,6 +53,19 @@ object SettingsDownloadScreen : SearchableSettings {
|
|||||||
title = stringResource(MR.strings.split_tall_images),
|
title = stringResource(MR.strings.split_tall_images),
|
||||||
subtitle = stringResource(MR.strings.split_tall_images_summary),
|
subtitle = stringResource(MR.strings.split_tall_images_summary),
|
||||||
),
|
),
|
||||||
|
Preference.PreferenceItem.SliderPreference(
|
||||||
|
value = parallelSourceLimit,
|
||||||
|
valueRange = 1..10,
|
||||||
|
title = stringResource(MR.strings.pref_download_concurrent_sources),
|
||||||
|
onValueChanged = { downloadPreferences.parallelSourceLimit().set(it) },
|
||||||
|
),
|
||||||
|
Preference.PreferenceItem.SliderPreference(
|
||||||
|
value = parallelPageLimit,
|
||||||
|
valueRange = 1..15,
|
||||||
|
title = stringResource(MR.strings.pref_download_concurrent_pages),
|
||||||
|
subtitle = stringResource(MR.strings.pref_download_concurrent_pages_summary),
|
||||||
|
onValueChanged = { downloadPreferences.parallelPageLimit().set(it) },
|
||||||
|
),
|
||||||
getDeleteChaptersGroup(
|
getDeleteChaptersGroup(
|
||||||
downloadPreferences = downloadPreferences,
|
downloadPreferences = downloadPreferences,
|
||||||
categories = allCategories,
|
categories = allCategories,
|
||||||
|
|||||||
@@ -256,6 +256,10 @@ object SettingsLibraryScreen : SearchableSettings {
|
|||||||
),
|
),
|
||||||
title = stringResource(MR.strings.pref_mark_duplicate_read_chapter_read),
|
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),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package eu.kanade.presentation.more.settings.screen
|
package eu.kanade.presentation.more.settings.screen
|
||||||
|
|
||||||
import android.os.Build
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.ReadOnlyComposable
|
import androidx.compose.runtime.ReadOnlyComposable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
@@ -10,6 +9,7 @@ import eu.kanade.presentation.more.settings.Preference
|
|||||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation
|
import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
|
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode
|
import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode
|
||||||
|
import eu.kanade.tachiyomi.util.system.hasDisplayCutout
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.collections.immutable.persistentMapOf
|
import kotlinx.collections.immutable.persistentMapOf
|
||||||
import kotlinx.collections.immutable.toImmutableMap
|
import kotlinx.collections.immutable.toImmutableMap
|
||||||
@@ -101,11 +101,9 @@ object SettingsReaderScreen : SearchableSettings {
|
|||||||
title = stringResource(MR.strings.pref_fullscreen),
|
title = stringResource(MR.strings.pref_fullscreen),
|
||||||
),
|
),
|
||||||
Preference.PreferenceItem.SwitchPreference(
|
Preference.PreferenceItem.SwitchPreference(
|
||||||
preference = readerPreferences.cutoutShort(),
|
preference = readerPreferences.drawUnderCutout(),
|
||||||
title = stringResource(MR.strings.pref_cutout_short),
|
title = stringResource(MR.strings.pref_cutout_short),
|
||||||
enabled = fullscreen &&
|
enabled = LocalView.current.hasDisplayCutout() && fullscreen,
|
||||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.P &&
|
|
||||||
LocalView.current.rootWindowInsets?.displayCutout != null, // has cutout
|
|
||||||
),
|
),
|
||||||
Preference.PreferenceItem.SwitchPreference(
|
Preference.PreferenceItem.SwitchPreference(
|
||||||
preference = readerPreferences.keepScreenOn(),
|
preference = readerPreferences.keepScreenOn(),
|
||||||
@@ -143,23 +141,17 @@ object SettingsReaderScreen : SearchableSettings {
|
|||||||
value = flashMillis / ReaderPreferences.MILLI_CONVERSION,
|
value = flashMillis / ReaderPreferences.MILLI_CONVERSION,
|
||||||
valueRange = 1..15,
|
valueRange = 1..15,
|
||||||
title = stringResource(MR.strings.pref_flash_duration),
|
title = stringResource(MR.strings.pref_flash_duration),
|
||||||
subtitle = stringResource(MR.strings.pref_flash_duration_summary, flashMillis),
|
valueString = stringResource(MR.strings.pref_flash_duration_summary, flashMillis),
|
||||||
enabled = flashPageState,
|
enabled = flashPageState,
|
||||||
onValueChanged = {
|
onValueChanged = { flashMillisPref.set(it * ReaderPreferences.MILLI_CONVERSION) },
|
||||||
flashMillisPref.set(it * ReaderPreferences.MILLI_CONVERSION)
|
|
||||||
true
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
Preference.PreferenceItem.SliderPreference(
|
Preference.PreferenceItem.SliderPreference(
|
||||||
value = flashInterval,
|
value = flashInterval,
|
||||||
valueRange = 1..10,
|
valueRange = 1..10,
|
||||||
title = stringResource(MR.strings.pref_flash_page_interval),
|
title = stringResource(MR.strings.pref_flash_page_interval),
|
||||||
subtitle = pluralStringResource(MR.plurals.pref_pages, flashInterval, flashInterval),
|
valueString = pluralStringResource(MR.plurals.pref_pages, flashInterval, flashInterval),
|
||||||
enabled = flashPageState,
|
enabled = flashPageState,
|
||||||
onValueChanged = {
|
onValueChanged = { flashIntervalPref.set(it) },
|
||||||
flashIntervalPref.set(it)
|
|
||||||
true
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
Preference.PreferenceItem.ListPreference(
|
Preference.PreferenceItem.ListPreference(
|
||||||
preference = flashColorPref,
|
preference = flashColorPref,
|
||||||
@@ -342,11 +334,8 @@ object SettingsReaderScreen : SearchableSettings {
|
|||||||
it.WEBTOON_PADDING_MIN..it.WEBTOON_PADDING_MAX
|
it.WEBTOON_PADDING_MIN..it.WEBTOON_PADDING_MAX
|
||||||
},
|
},
|
||||||
title = stringResource(MR.strings.pref_webtoon_side_padding),
|
title = stringResource(MR.strings.pref_webtoon_side_padding),
|
||||||
subtitle = numberFormat.format(webtoonSidePadding / 100f),
|
valueString = numberFormat.format(webtoonSidePadding / 100f),
|
||||||
onValueChanged = {
|
onValueChanged = { webtoonSidePaddingPref.set(it) },
|
||||||
webtoonSidePaddingPref.set(it)
|
|
||||||
true
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
Preference.PreferenceItem.ListPreference(
|
Preference.PreferenceItem.ListPreference(
|
||||||
preference = readerPreferences.readerHideThreshold(),
|
preference = readerPreferences.readerHideThreshold(),
|
||||||
|
|||||||
@@ -183,7 +183,7 @@ private fun SearchResult(
|
|||||||
emptySequence()
|
emptySequence()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is Preference.PreferenceItem<*> -> sequenceOf(null to p)
|
is Preference.PreferenceItem<*, *> -> sequenceOf(null to p)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Don't show info preference
|
// Don't show info preference
|
||||||
|
|||||||
@@ -30,8 +30,11 @@ import androidx.compose.runtime.rememberCoroutineScope
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.autofill.ContentType
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalUriHandler
|
import androidx.compose.ui.platform.LocalUriHandler
|
||||||
|
import androidx.compose.ui.semantics.contentType
|
||||||
|
import androidx.compose.ui.semantics.semantics
|
||||||
import androidx.compose.ui.text.input.ImeAction
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
import androidx.compose.ui.text.input.KeyboardType
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||||
@@ -220,7 +223,9 @@ object SettingsTrackingScreen : SearchableSettings {
|
|||||||
text = {
|
text = {
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.semantics { contentType = ContentType.Username + ContentType.EmailAddress },
|
||||||
value = username,
|
value = username,
|
||||||
onValueChange = { username = it },
|
onValueChange = { username = it },
|
||||||
label = { Text(text = stringResource(uNameStringRes)) },
|
label = { Text(text = stringResource(uNameStringRes)) },
|
||||||
@@ -231,7 +236,9 @@ object SettingsTrackingScreen : SearchableSettings {
|
|||||||
|
|
||||||
var hidePassword by remember { mutableStateOf(true) }
|
var hidePassword by remember { mutableStateOf(true) }
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.semantics { contentType = ContentType.Password },
|
||||||
value = password,
|
value = password,
|
||||||
onValueChange = { password = it },
|
onValueChange = { password = it },
|
||||||
label = { Text(text = stringResource(MR.strings.password)) },
|
label = { Text(text = stringResource(MR.strings.password)) },
|
||||||
@@ -280,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))
|
Text(text = stringResource(id))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import androidx.compose.ui.Modifier
|
|||||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||||
import com.mikepenz.aboutlibraries.ui.compose.m3.LibrariesContainer
|
import com.mikepenz.aboutlibraries.ui.compose.m3.LibrariesContainer
|
||||||
import com.mikepenz.aboutlibraries.ui.compose.m3.util.htmlReadyLicenseContent
|
import com.mikepenz.aboutlibraries.ui.compose.util.htmlReadyLicenseContent
|
||||||
import eu.kanade.presentation.components.AppBar
|
import eu.kanade.presentation.components.AppBar
|
||||||
import eu.kanade.presentation.util.Screen
|
import eu.kanade.presentation.util.Screen
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
package eu.kanade.presentation.more.settings.screen.advanced
|
package eu.kanade.presentation.more.settings.screen.advanced
|
||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
@@ -12,13 +14,17 @@ import androidx.compose.material.icons.outlined.SelectAll
|
|||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.Checkbox
|
import androidx.compose.material3.Checkbox
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Switch
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
@@ -39,6 +45,7 @@ import kotlinx.coroutines.flow.collectLatest
|
|||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import tachiyomi.core.common.util.lang.launchIO
|
import tachiyomi.core.common.util.lang.launchIO
|
||||||
import tachiyomi.core.common.util.lang.launchUI
|
import tachiyomi.core.common.util.lang.launchUI
|
||||||
|
import tachiyomi.core.common.util.lang.toLong
|
||||||
import tachiyomi.core.common.util.lang.withNonCancellableContext
|
import tachiyomi.core.common.util.lang.withNonCancellableContext
|
||||||
import tachiyomi.data.Database
|
import tachiyomi.data.Database
|
||||||
import tachiyomi.domain.source.interactor.GetSourcesWithNonLibraryManga
|
import tachiyomi.domain.source.interactor.GetSourcesWithNonLibraryManga
|
||||||
@@ -47,6 +54,7 @@ import tachiyomi.domain.source.model.SourceWithCount
|
|||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.components.LazyColumnWithAction
|
import tachiyomi.presentation.core.components.LazyColumnWithAction
|
||||||
import tachiyomi.presentation.core.components.material.Scaffold
|
import tachiyomi.presentation.core.components.material.Scaffold
|
||||||
|
import tachiyomi.presentation.core.components.material.padding
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
import tachiyomi.presentation.core.screens.EmptyScreen
|
import tachiyomi.presentation.core.screens.EmptyScreen
|
||||||
import tachiyomi.presentation.core.screens.LoadingScreen
|
import tachiyomi.presentation.core.screens.LoadingScreen
|
||||||
@@ -68,13 +76,45 @@ class ClearDatabaseScreen : Screen() {
|
|||||||
is ClearDatabaseScreenModel.State.Loading -> LoadingScreen()
|
is ClearDatabaseScreenModel.State.Loading -> LoadingScreen()
|
||||||
is ClearDatabaseScreenModel.State.Ready -> {
|
is ClearDatabaseScreenModel.State.Ready -> {
|
||||||
if (s.showConfirmation) {
|
if (s.showConfirmation) {
|
||||||
|
var keepReadManga by remember { mutableStateOf(true) }
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
|
title = {
|
||||||
|
Text(text = stringResource(MR.strings.are_you_sure))
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(MR.strings.clear_database_text))
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(MR.strings.clear_db_exclude_read),
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
Switch(
|
||||||
|
checked = keepReadManga,
|
||||||
|
onCheckedChange = { keepReadManga = it },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (!keepReadManga) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(MR.strings.clear_database_history_warning),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.error,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
onDismissRequest = model::hideConfirmation,
|
onDismissRequest = model::hideConfirmation,
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
scope.launchUI {
|
scope.launchUI {
|
||||||
model.removeMangaBySourceId()
|
model.removeMangaBySourceId(keepReadManga)
|
||||||
model.clearSelection()
|
model.clearSelection()
|
||||||
model.hideConfirmation()
|
model.hideConfirmation()
|
||||||
context.toast(MR.strings.clear_database_completed)
|
context.toast(MR.strings.clear_database_completed)
|
||||||
@@ -89,9 +129,6 @@ class ClearDatabaseScreen : Screen() {
|
|||||||
Text(text = stringResource(MR.strings.action_cancel))
|
Text(text = stringResource(MR.strings.action_cancel))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
text = {
|
|
||||||
Text(text = stringResource(MR.strings.clear_database_confirmation))
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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
|
val state = state.value as? State.Ready ?: return@withNonCancellableContext
|
||||||
database.mangasQueries.deleteMangasNotInLibraryBySourceIds(state.selection)
|
database.mangasQueries.deleteNonLibraryManga(state.selection, keepReadManga.toLong())
|
||||||
database.historyQueries.removeResettedHistory()
|
database.historyQueries.removeResettedHistory()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import androidx.compose.runtime.Immutable
|
|||||||
import cafe.adriel.voyager.core.model.StateScreenModel
|
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||||
import cafe.adriel.voyager.core.model.screenModelScope
|
import cafe.adriel.voyager.core.model.screenModelScope
|
||||||
import dev.icerock.moko.resources.StringResource
|
import dev.icerock.moko.resources.StringResource
|
||||||
|
import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||||
import kotlinx.collections.immutable.ImmutableSet
|
import kotlinx.collections.immutable.ImmutableSet
|
||||||
import kotlinx.collections.immutable.toImmutableSet
|
import kotlinx.collections.immutable.toImmutableSet
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
@@ -27,6 +28,7 @@ class ExtensionReposScreenModel(
|
|||||||
private val deleteExtensionRepo: DeleteExtensionRepo = Injekt.get(),
|
private val deleteExtensionRepo: DeleteExtensionRepo = Injekt.get(),
|
||||||
private val replaceExtensionRepo: ReplaceExtensionRepo = Injekt.get(),
|
private val replaceExtensionRepo: ReplaceExtensionRepo = Injekt.get(),
|
||||||
private val updateExtensionRepo: UpdateExtensionRepo = Injekt.get(),
|
private val updateExtensionRepo: UpdateExtensionRepo = Injekt.get(),
|
||||||
|
private val extensionManager: ExtensionManager = Injekt.get(),
|
||||||
) : StateScreenModel<RepoScreenState>(RepoScreenState.Loading) {
|
) : StateScreenModel<RepoScreenState>(RepoScreenState.Loading) {
|
||||||
|
|
||||||
private val _events: Channel<RepoEvent> = Channel(Int.MAX_VALUE)
|
private val _events: Channel<RepoEvent> = Channel(Int.MAX_VALUE)
|
||||||
@@ -53,6 +55,7 @@ class ExtensionReposScreenModel(
|
|||||||
fun createRepo(baseUrl: String) {
|
fun createRepo(baseUrl: String) {
|
||||||
screenModelScope.launchIO {
|
screenModelScope.launchIO {
|
||||||
when (val result = createExtensionRepo.await(baseUrl)) {
|
when (val result = createExtensionRepo.await(baseUrl)) {
|
||||||
|
CreateExtensionRepo.Result.Success -> extensionManager.findAvailableExtensions()
|
||||||
CreateExtensionRepo.Result.InvalidUrl -> _events.send(RepoEvent.InvalidUrl)
|
CreateExtensionRepo.Result.InvalidUrl -> _events.send(RepoEvent.InvalidUrl)
|
||||||
CreateExtensionRepo.Result.RepoAlreadyExists -> _events.send(RepoEvent.RepoAlreadyExists)
|
CreateExtensionRepo.Result.RepoAlreadyExists -> _events.send(RepoEvent.RepoAlreadyExists)
|
||||||
is CreateExtensionRepo.Result.DuplicateFingerprint -> {
|
is CreateExtensionRepo.Result.DuplicateFingerprint -> {
|
||||||
@@ -93,6 +96,7 @@ class ExtensionReposScreenModel(
|
|||||||
fun deleteRepo(baseUrl: String) {
|
fun deleteRepo(baseUrl: String) {
|
||||||
screenModelScope.launchIO {
|
screenModelScope.launchIO {
|
||||||
deleteExtensionRepo.await(baseUrl)
|
deleteExtensionRepo.await(baseUrl)
|
||||||
|
extensionManager.findAvailableExtensions()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ fun ExtensionRepoCreateDialog(
|
|||||||
},
|
},
|
||||||
text = {
|
text = {
|
||||||
Column {
|
Column {
|
||||||
Text(text = stringResource(MR.strings.action_add_repo_message))
|
Text(text = stringResource(MR.strings.action_add_repo_message, stringResource(MR.strings.app_name)))
|
||||||
|
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ class DebugInfoScreen : Screen() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun getDeviceInfoGroup(): Preference.PreferenceGroup {
|
private fun getDeviceInfoGroup(): Preference.PreferenceGroup {
|
||||||
val items = persistentListOf<Preference.PreferenceItem<out Any>>().mutate {
|
val items = persistentListOf<Preference.PreferenceItem<out Any, out Any>>().mutate {
|
||||||
it.add(
|
it.add(
|
||||||
Preference.PreferenceItem.TextPreference(
|
Preference.PreferenceItem.TextPreference(
|
||||||
title = "Model",
|
title = "Model",
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.composed
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@@ -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) }
|
var highlightFlag by remember { mutableStateOf(false) }
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
if (highlighted) {
|
if (highlighted) {
|
||||||
@@ -116,7 +116,7 @@ internal fun Modifier.highlightBackground(highlighted: Boolean): Modifier = comp
|
|||||||
},
|
},
|
||||||
label = "highlight",
|
label = "highlight",
|
||||||
)
|
)
|
||||||
Modifier.background(color = highlight)
|
return this.background(color = highlight)
|
||||||
}
|
}
|
||||||
|
|
||||||
internal val TrailingWidgetBuffer = 16.dp
|
internal val TrailingWidgetBuffer = 16.dp
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import androidx.compose.material3.Surface
|
|||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
@@ -15,9 +16,10 @@ import androidx.compose.ui.unit.sp
|
|||||||
import eu.kanade.presentation.theme.TachiyomiPreviewTheme
|
import eu.kanade.presentation.theme.TachiyomiPreviewTheme
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PageIndicatorText(
|
fun ReaderPageIndicator(
|
||||||
currentPage: Int,
|
currentPage: Int,
|
||||||
totalPages: Int,
|
totalPages: Int,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
if (currentPage <= 0 || totalPages <= 0) return
|
if (currentPage <= 0 || totalPages <= 0) return
|
||||||
|
|
||||||
@@ -36,6 +38,7 @@ fun PageIndicatorText(
|
|||||||
|
|
||||||
Box(
|
Box(
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
|
modifier = modifier,
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = text,
|
text = text,
|
||||||
@@ -50,10 +53,10 @@ fun PageIndicatorText(
|
|||||||
|
|
||||||
@PreviewLightDark
|
@PreviewLightDark
|
||||||
@Composable
|
@Composable
|
||||||
private fun PageIndicatorTextPreview() {
|
private fun ReaderPageIndicatorPreview() {
|
||||||
TachiyomiPreviewTheme {
|
TachiyomiPreviewTheme {
|
||||||
Surface {
|
Surface {
|
||||||
PageIndicatorText(currentPage = 10, totalPages = 69)
|
ReaderPageIndicator(currentPage = 10, totalPages = 69)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,42 +2,41 @@ package eu.kanade.presentation.reader.appbars
|
|||||||
|
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.fadeOut
|
||||||
import androidx.compose.animation.slideInVertically
|
import androidx.compose.animation.slideInVertically
|
||||||
import androidx.compose.animation.slideOutVertically
|
import androidx.compose.animation.slideOutVertically
|
||||||
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.systemBarsPadding
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.foundation.layout.navigationBars
|
||||||
import androidx.compose.material.icons.outlined.Bookmark
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material.icons.outlined.BookmarkBorder
|
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.surfaceColorAtElevation
|
import androidx.compose.material3.surfaceColorAtElevation
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.IntOffset
|
import androidx.compose.ui.unit.IntOffset
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import eu.kanade.presentation.components.AppBar
|
|
||||||
import eu.kanade.presentation.components.AppBarActions
|
|
||||||
import eu.kanade.presentation.reader.components.ChapterNavigator
|
import eu.kanade.presentation.reader.components.ChapterNavigator
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation
|
import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode
|
import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode
|
||||||
import eu.kanade.tachiyomi.ui.reader.viewer.Viewer
|
import eu.kanade.tachiyomi.ui.reader.viewer.Viewer
|
||||||
import eu.kanade.tachiyomi.ui.reader.viewer.pager.R2LPagerViewer
|
import eu.kanade.tachiyomi.ui.reader.viewer.pager.R2LPagerViewer
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
|
||||||
import tachiyomi.i18n.MR
|
|
||||||
import tachiyomi.presentation.core.components.material.padding
|
import tachiyomi.presentation.core.components.material.padding
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
|
||||||
|
|
||||||
private val animationSpec = tween<IntOffset>(200)
|
private val readerBarsSlideAnimationSpec = tween<IntOffset>(200)
|
||||||
|
private val readerBarsFadeAnimationSpec = tween<Float>(150)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ReaderAppBars(
|
fun ReaderAppBars(
|
||||||
visible: Boolean,
|
visible: Boolean,
|
||||||
fullscreen: Boolean,
|
|
||||||
|
|
||||||
mangaTitle: String?,
|
mangaTitle: String?,
|
||||||
chapterTitle: String?,
|
chapterTitle: String?,
|
||||||
@@ -71,83 +70,26 @@ fun ReaderAppBars(
|
|||||||
.surfaceColorAtElevation(3.dp)
|
.surfaceColorAtElevation(3.dp)
|
||||||
.copy(alpha = if (isSystemInDarkTheme()) 0.9f else 0.95f)
|
.copy(alpha = if (isSystemInDarkTheme()) 0.9f else 0.95f)
|
||||||
|
|
||||||
val modifierWithInsetsPadding = if (fullscreen) {
|
Column(modifier = Modifier.fillMaxHeight()) {
|
||||||
Modifier.systemBarsPadding()
|
|
||||||
} else {
|
|
||||||
Modifier
|
|
||||||
}
|
|
||||||
|
|
||||||
Column(
|
|
||||||
modifier = Modifier.fillMaxHeight(),
|
|
||||||
verticalArrangement = Arrangement.SpaceBetween,
|
|
||||||
) {
|
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = visible,
|
visible = visible,
|
||||||
enter = slideInVertically(
|
enter = slideInVertically(initialOffsetY = { -it }, animationSpec = readerBarsSlideAnimationSpec) +
|
||||||
initialOffsetY = { -it },
|
fadeIn(animationSpec = readerBarsFadeAnimationSpec),
|
||||||
animationSpec = animationSpec,
|
exit = slideOutVertically(targetOffsetY = { -it }, animationSpec = readerBarsSlideAnimationSpec) +
|
||||||
),
|
fadeOut(animationSpec = readerBarsFadeAnimationSpec),
|
||||||
exit = slideOutVertically(
|
|
||||||
targetOffsetY = { -it },
|
|
||||||
animationSpec = animationSpec,
|
|
||||||
),
|
|
||||||
) {
|
) {
|
||||||
AppBar(
|
ReaderTopBar(
|
||||||
modifier = modifierWithInsetsPadding
|
modifier = Modifier
|
||||||
|
.background(backgroundColor)
|
||||||
.clickable(onClick = onClickTopAppBar),
|
.clickable(onClick = onClickTopAppBar),
|
||||||
backgroundColor = backgroundColor,
|
mangaTitle = mangaTitle,
|
||||||
title = mangaTitle,
|
chapterTitle = chapterTitle,
|
||||||
subtitle = chapterTitle,
|
|
||||||
navigateUp = navigateUp,
|
navigateUp = navigateUp,
|
||||||
actions = {
|
bookmarked = bookmarked,
|
||||||
AppBarActions(
|
onToggleBookmarked = onToggleBookmarked,
|
||||||
actions = persistentListOf<AppBar.AppBarAction>().builder()
|
onOpenInWebView = onOpenInWebView,
|
||||||
.apply {
|
onOpenInBrowser = onOpenInBrowser,
|
||||||
add(
|
onShare = onShare,
|
||||||
AppBar.Action(
|
|
||||||
title = stringResource(
|
|
||||||
if (bookmarked) {
|
|
||||||
MR.strings.action_remove_bookmark
|
|
||||||
} else {
|
|
||||||
MR.strings.action_bookmark
|
|
||||||
},
|
|
||||||
),
|
|
||||||
icon = if (bookmarked) {
|
|
||||||
Icons.Outlined.Bookmark
|
|
||||||
} else {
|
|
||||||
Icons.Outlined.BookmarkBorder
|
|
||||||
},
|
|
||||||
onClick = onToggleBookmarked,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
onOpenInWebView?.let {
|
|
||||||
add(
|
|
||||||
AppBar.OverflowAction(
|
|
||||||
title = stringResource(MR.strings.action_open_in_web_view),
|
|
||||||
onClick = it,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
onOpenInBrowser?.let {
|
|
||||||
add(
|
|
||||||
AppBar.OverflowAction(
|
|
||||||
title = stringResource(MR.strings.action_open_in_browser),
|
|
||||||
onClick = it,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
onShare?.let {
|
|
||||||
add(
|
|
||||||
AppBar.OverflowAction(
|
|
||||||
title = stringResource(MR.strings.action_share),
|
|
||||||
onClick = it,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.build(),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,19 +97,12 @@ fun ReaderAppBars(
|
|||||||
|
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = visible,
|
visible = visible,
|
||||||
enter = slideInVertically(
|
enter = slideInVertically(initialOffsetY = { it }, animationSpec = readerBarsSlideAnimationSpec) +
|
||||||
initialOffsetY = { it },
|
fadeIn(animationSpec = readerBarsFadeAnimationSpec),
|
||||||
animationSpec = animationSpec,
|
exit = slideOutVertically(targetOffsetY = { it }, animationSpec = readerBarsSlideAnimationSpec) +
|
||||||
),
|
fadeOut(animationSpec = readerBarsFadeAnimationSpec),
|
||||||
exit = slideOutVertically(
|
|
||||||
targetOffsetY = { it },
|
|
||||||
animationSpec = animationSpec,
|
|
||||||
),
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = modifierWithInsetsPadding,
|
|
||||||
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
|
||||||
) {
|
) {
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small)) {
|
||||||
ChapterNavigator(
|
ChapterNavigator(
|
||||||
isRtl = isRtl,
|
isRtl = isRtl,
|
||||||
onNextChapter = onNextChapter,
|
onNextChapter = onNextChapter,
|
||||||
@@ -178,8 +113,12 @@ fun ReaderAppBars(
|
|||||||
totalPages = totalPages,
|
totalPages = totalPages,
|
||||||
onPageIndexChange = onPageIndexChange,
|
onPageIndexChange = onPageIndexChange,
|
||||||
)
|
)
|
||||||
BottomReaderBar(
|
ReaderBottomBar(
|
||||||
backgroundColor = backgroundColor,
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(backgroundColor)
|
||||||
|
.padding(horizontal = MaterialTheme.padding.small)
|
||||||
|
.windowInsetsPadding(WindowInsets.navigationBars),
|
||||||
readingMode = readingMode,
|
readingMode = readingMode,
|
||||||
onClickReadingMode = onClickReadingMode,
|
onClickReadingMode = onClickReadingMode,
|
||||||
orientation = orientation,
|
orientation = orientation,
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
package eu.kanade.presentation.reader.appbars
|
package eu.kanade.presentation.reader.appbars
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.outlined.Settings
|
import androidx.compose.material.icons.outlined.Settings
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
@@ -12,9 +9,8 @@ import androidx.compose.material3.IconButton
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation
|
import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode
|
import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode
|
||||||
@@ -22,8 +18,7 @@ import tachiyomi.i18n.MR
|
|||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun BottomReaderBar(
|
fun ReaderBottomBar(
|
||||||
backgroundColor: Color,
|
|
||||||
readingMode: ReadingMode,
|
readingMode: ReadingMode,
|
||||||
onClickReadingMode: () -> Unit,
|
onClickReadingMode: () -> Unit,
|
||||||
orientation: ReaderOrientation,
|
orientation: ReaderOrientation,
|
||||||
@@ -31,12 +26,11 @@ fun BottomReaderBar(
|
|||||||
cropEnabled: Boolean,
|
cropEnabled: Boolean,
|
||||||
onClickCropBorder: () -> Unit,
|
onClickCropBorder: () -> Unit,
|
||||||
onClickSettings: () -> Unit,
|
onClickSettings: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.pointerInput(Unit) {},
|
||||||
.background(backgroundColor)
|
|
||||||
.padding(8.dp),
|
|
||||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
package eu.kanade.presentation.reader.appbars
|
||||||
|
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.Bookmark
|
||||||
|
import androidx.compose.material.icons.outlined.BookmarkBorder
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import eu.kanade.presentation.components.AppBar
|
||||||
|
import eu.kanade.presentation.components.AppBarActions
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
import tachiyomi.i18n.MR
|
||||||
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ReaderTopBar(
|
||||||
|
mangaTitle: String?,
|
||||||
|
chapterTitle: String?,
|
||||||
|
navigateUp: () -> Unit,
|
||||||
|
bookmarked: Boolean,
|
||||||
|
onToggleBookmarked: () -> Unit,
|
||||||
|
onOpenInWebView: (() -> Unit)?,
|
||||||
|
onOpenInBrowser: (() -> Unit)?,
|
||||||
|
onShare: (() -> Unit)?,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
AppBar(
|
||||||
|
modifier = modifier,
|
||||||
|
backgroundColor = Color.Transparent,
|
||||||
|
title = mangaTitle,
|
||||||
|
subtitle = chapterTitle,
|
||||||
|
navigateUp = navigateUp,
|
||||||
|
actions = {
|
||||||
|
AppBarActions(
|
||||||
|
actions = persistentListOf<AppBar.AppBarAction>().builder()
|
||||||
|
.apply {
|
||||||
|
add(
|
||||||
|
AppBar.Action(
|
||||||
|
title = stringResource(
|
||||||
|
if (bookmarked) {
|
||||||
|
MR.strings.action_remove_bookmark
|
||||||
|
} else {
|
||||||
|
MR.strings.action_bookmark
|
||||||
|
},
|
||||||
|
),
|
||||||
|
icon = if (bookmarked) {
|
||||||
|
Icons.Outlined.Bookmark
|
||||||
|
} else {
|
||||||
|
Icons.Outlined.BookmarkBorder
|
||||||
|
},
|
||||||
|
onClick = onToggleBookmarked,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
onOpenInWebView?.let {
|
||||||
|
add(
|
||||||
|
AppBar.OverflowAction(
|
||||||
|
title = stringResource(MR.strings.action_open_in_web_view),
|
||||||
|
onClick = it,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
onOpenInBrowser?.let {
|
||||||
|
add(
|
||||||
|
AppBar.OverflowAction(
|
||||||
|
title = stringResource(MR.strings.action_open_in_browser),
|
||||||
|
onClick = it,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
onShare?.let {
|
||||||
|
add(
|
||||||
|
AppBar.OverflowAction(
|
||||||
|
title = stringResource(MR.strings.action_share),
|
||||||
|
onClick = it,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package eu.kanade.presentation.reader.settings
|
package eu.kanade.presentation.reader.settings
|
||||||
|
|
||||||
|
import androidx.activity.compose.LocalActivity
|
||||||
import androidx.compose.foundation.layout.ColumnScope
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
import androidx.compose.material3.FilterChip
|
import androidx.compose.material3.FilterChip
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
@@ -8,6 +9,7 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
|
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel
|
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel
|
||||||
|
import eu.kanade.tachiyomi.util.system.hasDisplayCutout
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.components.CheckboxItem
|
import tachiyomi.presentation.core.components.CheckboxItem
|
||||||
import tachiyomi.presentation.core.components.SettingsChipRow
|
import tachiyomi.presentation.core.components.SettingsChipRow
|
||||||
@@ -64,10 +66,11 @@ internal fun ColumnScope.GeneralPage(screenModel: ReaderSettingsScreenModel) {
|
|||||||
pref = screenModel.preferences.fullscreen(),
|
pref = screenModel.preferences.fullscreen(),
|
||||||
)
|
)
|
||||||
|
|
||||||
if (screenModel.hasDisplayCutout && screenModel.preferences.fullscreen().get()) {
|
val isFullscreen by screenModel.preferences.fullscreen().collectAsState()
|
||||||
|
if (LocalActivity.current?.hasDisplayCutout() == true && isFullscreen) {
|
||||||
CheckboxItem(
|
CheckboxItem(
|
||||||
label = stringResource(MR.strings.pref_cutout_short),
|
label = stringResource(MR.strings.pref_cutout_short),
|
||||||
pref = screenModel.preferences.cutoutShort(),
|
pref = screenModel.preferences.drawUnderCutout(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,7 +103,7 @@ internal fun ColumnScope.GeneralPage(screenModel: ReaderSettingsScreenModel) {
|
|||||||
value = flashMillis / ReaderPreferences.MILLI_CONVERSION,
|
value = flashMillis / ReaderPreferences.MILLI_CONVERSION,
|
||||||
valueRange = 1..15,
|
valueRange = 1..15,
|
||||||
label = stringResource(MR.strings.pref_flash_duration),
|
label = stringResource(MR.strings.pref_flash_duration),
|
||||||
valueText = stringResource(MR.strings.pref_flash_duration_summary, flashMillis),
|
valueString = stringResource(MR.strings.pref_flash_duration_summary, flashMillis),
|
||||||
onChange = { flashMillisPref.set(it * ReaderPreferences.MILLI_CONVERSION) },
|
onChange = { flashMillisPref.set(it * ReaderPreferences.MILLI_CONVERSION) },
|
||||||
pillColor = MaterialTheme.colorScheme.surfaceContainerHighest,
|
pillColor = MaterialTheme.colorScheme.surfaceContainerHighest,
|
||||||
)
|
)
|
||||||
@@ -108,7 +111,7 @@ internal fun ColumnScope.GeneralPage(screenModel: ReaderSettingsScreenModel) {
|
|||||||
value = flashInterval,
|
value = flashInterval,
|
||||||
valueRange = 1..10,
|
valueRange = 1..10,
|
||||||
label = stringResource(MR.strings.pref_flash_page_interval),
|
label = stringResource(MR.strings.pref_flash_page_interval),
|
||||||
valueText = pluralStringResource(MR.plurals.pref_pages, flashInterval, flashInterval),
|
valueString = pluralStringResource(MR.plurals.pref_pages, flashInterval, flashInterval),
|
||||||
onChange = {
|
onChange = {
|
||||||
flashIntervalPref.set(it)
|
flashIntervalPref.set(it)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -156,7 +156,7 @@ private fun ColumnScope.WebtoonViewerSettings(screenModel: ReaderSettingsScreenM
|
|||||||
value = webtoonSidePadding,
|
value = webtoonSidePadding,
|
||||||
valueRange = ReaderPreferences.let { it.WEBTOON_PADDING_MIN..it.WEBTOON_PADDING_MAX },
|
valueRange = ReaderPreferences.let { it.WEBTOON_PADDING_MIN..it.WEBTOON_PADDING_MAX },
|
||||||
label = stringResource(MR.strings.pref_webtoon_side_padding),
|
label = stringResource(MR.strings.pref_webtoon_side_padding),
|
||||||
valueText = numberFormat.format(webtoonSidePadding / 100f),
|
valueString = numberFormat.format(webtoonSidePadding / 100f),
|
||||||
onChange = {
|
onChange = {
|
||||||
screenModel.preferences.webtoonSidePadding().set(it)
|
screenModel.preferences.webtoonSidePadding().set(it)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import androidx.compose.ui.platform.LocalContext
|
|||||||
import eu.kanade.domain.ui.UiPreferences
|
import eu.kanade.domain.ui.UiPreferences
|
||||||
import eu.kanade.domain.ui.model.AppTheme
|
import eu.kanade.domain.ui.model.AppTheme
|
||||||
import eu.kanade.presentation.theme.colorscheme.BaseColorScheme
|
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.GreenAppleColorScheme
|
||||||
import eu.kanade.presentation.theme.colorscheme.LavenderColorScheme
|
import eu.kanade.presentation.theme.colorscheme.LavenderColorScheme
|
||||||
import eu.kanade.presentation.theme.colorscheme.MidnightDuskColorScheme
|
import eu.kanade.presentation.theme.colorscheme.MidnightDuskColorScheme
|
||||||
@@ -77,6 +78,7 @@ private fun getThemeColorScheme(
|
|||||||
|
|
||||||
private val colorSchemes: Map<AppTheme, BaseColorScheme> = mapOf(
|
private val colorSchemes: Map<AppTheme, BaseColorScheme> = mapOf(
|
||||||
AppTheme.DEFAULT to TachiyomiColorScheme,
|
AppTheme.DEFAULT to TachiyomiColorScheme,
|
||||||
|
AppTheme.CATPPUCCIN to CatppuccinColorScheme,
|
||||||
AppTheme.GREEN_APPLE to GreenAppleColorScheme,
|
AppTheme.GREEN_APPLE to GreenAppleColorScheme,
|
||||||
AppTheme.LAVENDER to LavenderColorScheme,
|
AppTheme.LAVENDER to LavenderColorScheme,
|
||||||
AppTheme.MIDNIGHT_DUSK to MidnightDuskColorScheme,
|
AppTheme.MIDNIGHT_DUSK to MidnightDuskColorScheme,
|
||||||
|
|||||||
@@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -57,7 +57,9 @@ fun UpdateScreen(
|
|||||||
onUpdateSelected: (UpdatesItem, Boolean, Boolean, Boolean) -> Unit,
|
onUpdateSelected: (UpdatesItem, Boolean, Boolean, Boolean) -> Unit,
|
||||||
onOpenChapter: (UpdatesItem) -> Unit,
|
onOpenChapter: (UpdatesItem) -> Unit,
|
||||||
) {
|
) {
|
||||||
BackHandler(enabled = state.selectionMode, onBack = { onSelectAll(false) })
|
BackHandler(enabled = state.selectionMode) {
|
||||||
|
onSelectAll(false)
|
||||||
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = { scrollBehavior ->
|
topBar = { scrollBehavior ->
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package eu.kanade.presentation.util
|
package eu.kanade.presentation.util
|
||||||
|
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.compose.animation.AnimatedContent
|
import androidx.compose.animation.AnimatedContent
|
||||||
import androidx.compose.animation.AnimatedContentTransitionScope
|
import androidx.compose.animation.AnimatedContentTransitionScope
|
||||||
import androidx.compose.animation.ContentTransform
|
import androidx.compose.animation.ContentTransform
|
||||||
@@ -89,4 +90,6 @@ fun ScreenTransition(
|
|||||||
content(screen)
|
content(screen)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BackHandler(enabled = navigator.canPop, onBack = navigator::pop)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ package eu.kanade.presentation.webview
|
|||||||
|
|
||||||
import android.content.pm.ApplicationInfo
|
import android.content.pm.ApplicationInfo
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
|
import android.os.Message
|
||||||
import android.webkit.WebResourceRequest
|
import android.webkit.WebResourceRequest
|
||||||
import android.webkit.WebResourceResponse
|
|
||||||
import android.webkit.WebView
|
import android.webkit.WebView
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -20,6 +21,7 @@ import androidx.compose.material3.MaterialTheme
|
|||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.key
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
@@ -27,30 +29,41 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.platform.LocalUriHandler
|
import androidx.compose.ui.platform.LocalUriHandler
|
||||||
|
import androidx.compose.ui.res.vectorResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import cafe.adriel.voyager.core.stack.mutableStateStackOf
|
||||||
|
import com.kevinnzou.web.AccompanistWebChromeClient
|
||||||
import com.kevinnzou.web.AccompanistWebViewClient
|
import com.kevinnzou.web.AccompanistWebViewClient
|
||||||
import com.kevinnzou.web.LoadingState
|
import com.kevinnzou.web.LoadingState
|
||||||
|
import com.kevinnzou.web.WebContent
|
||||||
import com.kevinnzou.web.WebView
|
import com.kevinnzou.web.WebView
|
||||||
import com.kevinnzou.web.rememberWebViewNavigator
|
import com.kevinnzou.web.WebViewNavigator
|
||||||
import com.kevinnzou.web.rememberWebViewState
|
import com.kevinnzou.web.WebViewState
|
||||||
import eu.kanade.presentation.components.AppBar
|
import eu.kanade.presentation.components.AppBar
|
||||||
import eu.kanade.presentation.components.AppBarActions
|
import eu.kanade.presentation.components.AppBarActions
|
||||||
import eu.kanade.presentation.components.WarningBanner
|
import eu.kanade.presentation.components.WarningBanner
|
||||||
import eu.kanade.tachiyomi.BuildConfig
|
import eu.kanade.tachiyomi.BuildConfig
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.util.system.WebViewUtil
|
|
||||||
import eu.kanade.tachiyomi.util.system.getHtml
|
import eu.kanade.tachiyomi.util.system.getHtml
|
||||||
import eu.kanade.tachiyomi.util.system.setDefaultSettings
|
import eu.kanade.tachiyomi.util.system.setDefaultSettings
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import okhttp3.Request
|
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.components.material.Scaffold
|
import tachiyomi.presentation.core.components.material.Scaffold
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
class WebViewWindow(webContent: WebContent, val navigator: WebViewNavigator) {
|
||||||
|
var state by mutableStateOf(WebViewState(webContent))
|
||||||
|
var popupMessage: Message? = null
|
||||||
|
private set
|
||||||
|
var webView: WebView? = null
|
||||||
|
|
||||||
|
constructor(popupMessage: Message, navigator: WebViewNavigator) : this(WebContent.NavigatorOnly, navigator) {
|
||||||
|
this.popupMessage = popupMessage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun WebViewScreenContent(
|
fun WebViewScreenContent(
|
||||||
@@ -63,13 +76,22 @@ fun WebViewScreenContent(
|
|||||||
headers: Map<String, String> = emptyMap(),
|
headers: Map<String, String> = emptyMap(),
|
||||||
onUrlChange: (String) -> Unit = {},
|
onUrlChange: (String) -> Unit = {},
|
||||||
) {
|
) {
|
||||||
val state = rememberWebViewState(url = url, additionalHttpHeaders = headers)
|
val coroutineScope = rememberCoroutineScope()
|
||||||
val navigator = rememberWebViewNavigator()
|
|
||||||
val context = LocalContext.current
|
val windowStack = remember {
|
||||||
|
mutableStateStackOf(
|
||||||
|
WebViewWindow(
|
||||||
|
WebContent.Url(url = url, additionalHttpHeaders = headers),
|
||||||
|
WebViewNavigator(coroutineScope),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val currentWindow = windowStack.lastItemOrNull!!
|
||||||
|
val navigator = currentWindow.navigator
|
||||||
|
|
||||||
val uriHandler = LocalUriHandler.current
|
val uriHandler = LocalUriHandler.current
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val network = remember { Injekt.get<NetworkHelper>() }
|
|
||||||
val spoofedPackageName = remember { WebViewUtil.spoofedPackageName(context) }
|
|
||||||
|
|
||||||
var currentUrl by remember { mutableStateOf(url) }
|
var currentUrl by remember { mutableStateOf(url) }
|
||||||
var showCloudflareHelp by remember { mutableStateOf(false) }
|
var showCloudflareHelp by remember { mutableStateOf(false) }
|
||||||
@@ -108,65 +130,67 @@ fun WebViewScreenContent(
|
|||||||
view: WebView?,
|
view: WebView?,
|
||||||
request: WebResourceRequest?,
|
request: WebResourceRequest?,
|
||||||
): Boolean {
|
): Boolean {
|
||||||
request?.let {
|
val url = request?.url?.toString() ?: return false
|
||||||
// Don't attempt to open blobs as webpages
|
|
||||||
if (it.url.toString().startsWith("blob:http")) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ignore intents urls
|
// Ignore intents urls
|
||||||
if (it.url.toString().startsWith("intent://")) {
|
if (url.startsWith("intent://")) return true
|
||||||
|
|
||||||
|
// Only open valid web urls
|
||||||
|
if (url.startsWith("http") || url.startsWith("https")) {
|
||||||
|
if (url != view?.url) {
|
||||||
|
view?.loadUrl(url, headers)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Continue with request, but with custom headers
|
|
||||||
view?.loadUrl(it.url.toString(), headers)
|
|
||||||
}
|
|
||||||
return super.shouldOverrideUrlLoading(view, request)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun shouldInterceptRequest(
|
return false
|
||||||
view: WebView?,
|
}
|
||||||
request: WebResourceRequest?,
|
|
||||||
): WebResourceResponse? {
|
|
||||||
return try {
|
|
||||||
val internalRequest = Request.Builder().apply {
|
|
||||||
url(request!!.url.toString())
|
|
||||||
request.requestHeaders.forEach { (key, value) ->
|
|
||||||
if (key == "X-Requested-With" && value in setOf(context.packageName, spoofedPackageName)) {
|
|
||||||
return@forEach
|
|
||||||
}
|
}
|
||||||
addHeader(key, value)
|
|
||||||
}
|
}
|
||||||
method(request.method, null)
|
|
||||||
}.build()
|
|
||||||
|
|
||||||
val response = network.nonCloudflareClient.newCall(internalRequest).execute()
|
val webChromeClient = remember {
|
||||||
|
object : AccompanistWebChromeClient() {
|
||||||
|
override fun onCreateWindow(
|
||||||
|
view: WebView,
|
||||||
|
isDialog: Boolean,
|
||||||
|
isUserGesture: Boolean,
|
||||||
|
resultMsg: Message,
|
||||||
|
): Boolean {
|
||||||
|
// if it wasn't initiated by a user gesture, we should ignore it like a normal browser would
|
||||||
|
if (isUserGesture) {
|
||||||
|
windowStack.push(WebViewWindow(resultMsg, WebViewNavigator(coroutineScope)))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val contentType = response.body.contentType()?.let { "${it.type}/${it.subtype}" } ?: "text/html"
|
fun initializePopup(webView: WebView, message: Message): WebView {
|
||||||
val contentEncoding = response.body.contentType()?.charset()?.name() ?: "utf-8"
|
val transport = message.obj as WebView.WebViewTransport
|
||||||
|
transport.webView = webView
|
||||||
|
message.sendToTarget()
|
||||||
|
return webView
|
||||||
|
}
|
||||||
|
|
||||||
WebResourceResponse(
|
val popState = remember<() -> Unit> {
|
||||||
contentType,
|
{
|
||||||
contentEncoding,
|
if (windowStack.size == 1) {
|
||||||
response.code,
|
onNavigateUp()
|
||||||
response.message,
|
} else {
|
||||||
response.headers.associate { it.first to it.second },
|
windowStack.pop()
|
||||||
response.body.byteStream(),
|
|
||||||
)
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
super.shouldInterceptRequest(view, request)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BackHandler(windowStack.size > 1, popState)
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
Box {
|
Box {
|
||||||
Column {
|
Column {
|
||||||
AppBar(
|
AppBar(
|
||||||
title = state.pageTitle ?: initialTitle,
|
title = currentWindow.state.pageTitle ?: initialTitle,
|
||||||
subtitle = currentUrl,
|
subtitle = currentUrl,
|
||||||
navigateUp = onNavigateUp,
|
navigateUp = onNavigateUp,
|
||||||
navigationIcon = Icons.Outlined.Close,
|
navigationIcon = Icons.Outlined.Close,
|
||||||
@@ -209,8 +233,19 @@ fun WebViewScreenContent(
|
|||||||
title = stringResource(MR.strings.pref_clear_cookies),
|
title = stringResource(MR.strings.pref_clear_cookies),
|
||||||
onClick = { onClearCookies(currentUrl) },
|
onClick = { onClearCookies(currentUrl) },
|
||||||
),
|
),
|
||||||
|
).builder().apply {
|
||||||
|
if (windowStack.size > 1) {
|
||||||
|
add(
|
||||||
|
0,
|
||||||
|
AppBar.Action(
|
||||||
|
title = stringResource(MR.strings.action_webview_close_tab),
|
||||||
|
icon = ImageVector.vectorResource(R.drawable.ic_tab_close_24px),
|
||||||
|
onClick = popState,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
}.build(),
|
||||||
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -231,7 +266,7 @@ fun WebViewScreenContent(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
when (val loadingState = state.loadingState) {
|
when (val loadingState = currentWindow.state.loadingState) {
|
||||||
is LoadingState.Initializing -> LinearProgressIndicator(
|
is LoadingState.Initializing -> LinearProgressIndicator(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -248,8 +283,12 @@ fun WebViewScreenContent(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
) { contentPadding ->
|
) { contentPadding ->
|
||||||
|
// We need to key the WebView composable to the window object since simply updating the WebView composable will
|
||||||
|
// not cause it to re-invoke the WebView factory and render the new current window's WebView. This lets us
|
||||||
|
// completely reset the WebView composable when the current window switches.
|
||||||
|
key(currentWindow) {
|
||||||
WebView(
|
WebView(
|
||||||
state = state,
|
state = currentWindow.state,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(contentPadding),
|
.padding(contentPadding),
|
||||||
@@ -268,7 +307,31 @@ fun WebViewScreenContent(
|
|||||||
webView.settings.userAgentString = it
|
webView.settings.userAgentString = it
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
onDispose = { webView ->
|
||||||
|
val window = windowStack.items.find { it.webView == webView }
|
||||||
|
if (window == null) {
|
||||||
|
// If we couldn't find any window on the stack that owns this WebView, it means that we can
|
||||||
|
// safely dispose of it because the window containing it has been closed.
|
||||||
|
webView.destroy()
|
||||||
|
} else {
|
||||||
|
// The composable is being disposed but the WebView object is not.
|
||||||
|
// When the WebView element is recomposed, we will want the WebView to resume from its state
|
||||||
|
// before it was unmounted, we won't want it to reset back to its original target.
|
||||||
|
window.state.content = WebContent.NavigatorOnly
|
||||||
|
}
|
||||||
|
},
|
||||||
client = webClient,
|
client = webClient,
|
||||||
|
chromeClient = webChromeClient,
|
||||||
|
factory = { context ->
|
||||||
|
currentWindow.webView
|
||||||
|
?: WebView(context).also { webView ->
|
||||||
|
currentWindow.webView = webView
|
||||||
|
currentWindow.popupMessage?.let {
|
||||||
|
initializePopup(webView, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
|
|||||||
val pendingIntent = PendingIntent.getBroadcast(
|
val pendingIntent = PendingIntent.getBroadcast(
|
||||||
this@App,
|
this@App,
|
||||||
0,
|
0,
|
||||||
Intent(ACTION_DISABLE_INCOGNITO_MODE),
|
Intent(ACTION_DISABLE_INCOGNITO_MODE).setPackage(BuildConfig.APPLICATION_ID),
|
||||||
PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE,
|
PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE,
|
||||||
)
|
)
|
||||||
setContentIntent(pendingIntent)
|
setContentIntent(pendingIntent)
|
||||||
@@ -220,8 +220,8 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
|
|||||||
// Override the value passed as X-Requested-With in WebView requests
|
// Override the value passed as X-Requested-With in WebView requests
|
||||||
val stackTrace = Looper.getMainLooper().thread.stackTrace
|
val stackTrace = Looper.getMainLooper().thread.stackTrace
|
||||||
val isChromiumCall = stackTrace.any { trace ->
|
val isChromiumCall = stackTrace.any { trace ->
|
||||||
trace.className.equals("org.chromium.base.BuildInfo", ignoreCase = true) &&
|
trace.className.lowercase() in setOf("org.chromium.base.buildinfo", "org.chromium.base.apkinfo") &&
|
||||||
setOf("getAll", "getPackageName", "<init>").any { trace.methodName.equals(it, ignoreCase = true) }
|
trace.methodName.lowercase() in setOf("getall", "getpackagename", "<init>")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isChromiumCall) return WebViewUtil.spoofedPackageName(applicationContext)
|
if (isChromiumCall) return WebViewUtil.spoofedPackageName(applicationContext)
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ class BackupNotifier(private val context: Context) {
|
|||||||
addAction(
|
addAction(
|
||||||
R.drawable.ic_share_24dp,
|
R.drawable.ic_share_24dp,
|
||||||
context.stringResource(MR.strings.action_share),
|
context.stringResource(MR.strings.action_share),
|
||||||
NotificationReceiver.shareBackupPendingBroadcast(context, file.uri),
|
NotificationReceiver.shareBackupPendingActivity(context, file.uri),
|
||||||
)
|
)
|
||||||
|
|
||||||
show(Notifications.ID_BACKUP_COMPLETE)
|
show(Notifications.ID_BACKUP_COMPLETE)
|
||||||
|
|||||||
@@ -99,4 +99,6 @@ private fun Manga.toBackupManga() =
|
|||||||
lastModifiedAt = this.lastModifiedAt,
|
lastModifiedAt = this.lastModifiedAt,
|
||||||
favoriteModifiedAt = this.favoriteModifiedAt,
|
favoriteModifiedAt = this.favoriteModifiedAt,
|
||||||
version = this.version,
|
version = this.version,
|
||||||
|
notes = this.notes,
|
||||||
|
initialized = this.initialized,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -38,8 +38,11 @@ data class BackupManga(
|
|||||||
@ProtoNumber(105) var updateStrategy: UpdateStrategy = UpdateStrategy.ALWAYS_UPDATE,
|
@ProtoNumber(105) var updateStrategy: UpdateStrategy = UpdateStrategy.ALWAYS_UPDATE,
|
||||||
@ProtoNumber(106) var lastModifiedAt: Long = 0,
|
@ProtoNumber(106) var lastModifiedAt: Long = 0,
|
||||||
@ProtoNumber(107) var favoriteModifiedAt: Long? = null,
|
@ProtoNumber(107) var favoriteModifiedAt: Long? = null,
|
||||||
|
// Mihon values start here
|
||||||
@ProtoNumber(108) var excludedScanlators: List<String> = emptyList(),
|
@ProtoNumber(108) var excludedScanlators: List<String> = emptyList(),
|
||||||
@ProtoNumber(109) var version: Long = 0,
|
@ProtoNumber(109) var version: Long = 0,
|
||||||
|
@ProtoNumber(110) var notes: String = "",
|
||||||
|
@ProtoNumber(111) var initialized: Boolean = false,
|
||||||
) {
|
) {
|
||||||
fun getMangaImpl(): Manga {
|
fun getMangaImpl(): Manga {
|
||||||
return Manga.create().copy(
|
return Manga.create().copy(
|
||||||
@@ -60,6 +63,8 @@ data class BackupManga(
|
|||||||
lastModifiedAt = this@BackupManga.lastModifiedAt,
|
lastModifiedAt = this@BackupManga.lastModifiedAt,
|
||||||
favoriteModifiedAt = this@BackupManga.favoriteModifiedAt,
|
favoriteModifiedAt = this@BackupManga.favoriteModifiedAt,
|
||||||
version = this@BackupManga.version,
|
version = this@BackupManga.version,
|
||||||
|
notes = this@BackupManga.notes,
|
||||||
|
initialized = this@BackupManga.initialized,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -129,6 +129,7 @@ class MangaRestorer(
|
|||||||
updateStrategy = manga.updateStrategy.let(UpdateStrategyColumnAdapter::encode),
|
updateStrategy = manga.updateStrategy.let(UpdateStrategyColumnAdapter::encode),
|
||||||
version = manga.version,
|
version = manga.version,
|
||||||
isSyncing = 1,
|
isSyncing = 1,
|
||||||
|
notes = manga.notes,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return manga
|
return manga
|
||||||
@@ -138,9 +139,7 @@ class MangaRestorer(
|
|||||||
manga: Manga,
|
manga: Manga,
|
||||||
): Manga {
|
): Manga {
|
||||||
return manga.copy(
|
return manga.copy(
|
||||||
initialized = manga.description != null,
|
|
||||||
id = insertManga(manga),
|
id = insertManga(manga),
|
||||||
version = manga.version,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -261,6 +260,7 @@ class MangaRestorer(
|
|||||||
dateAdded = manga.dateAdded,
|
dateAdded = manga.dateAdded,
|
||||||
updateStrategy = manga.updateStrategy,
|
updateStrategy = manga.updateStrategy,
|
||||||
version = manga.version,
|
version = manga.version,
|
||||||
|
notes = manga.notes,
|
||||||
)
|
)
|
||||||
mangasQueries.selectLastInsertedRowId()
|
mangasQueries.selectLastInsertedRowId()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import com.jakewharton.disklrucache.DiskLruCache
|
|||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||||
import eu.kanade.tachiyomi.util.storage.saveTo
|
import eu.kanade.tachiyomi.util.storage.saveTo
|
||||||
import kotlinx.serialization.encodeToString
|
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
@@ -115,7 +114,7 @@ class ChapterCache(
|
|||||||
fun isImageInCache(imageUrl: String): Boolean {
|
fun isImageInCache(imageUrl: String): Boolean {
|
||||||
return try {
|
return try {
|
||||||
diskCache.get(DiskUtil.hashKeyForDisk(imageUrl)).use { it != null }
|
diskCache.get(DiskUtil.hashKeyForDisk(imageUrl)).use { it != null }
|
||||||
} catch (e: IOException) {
|
} catch (_: IOException) {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -147,7 +146,7 @@ class ChapterCache(
|
|||||||
try {
|
try {
|
||||||
// Get editor from md5 key.
|
// Get editor from md5 key.
|
||||||
val key = DiskUtil.hashKeyForDisk(imageUrl)
|
val key = DiskUtil.hashKeyForDisk(imageUrl)
|
||||||
editor = diskCache.edit(key) ?: throw IOException("Unable to edit key")
|
editor = diskCache.edit(key) ?: return
|
||||||
|
|
||||||
// Get OutputStream and write image with Okio.
|
// Get OutputStream and write image with Okio.
|
||||||
response.body.source().saveTo(editor.newOutputStream(0))
|
response.body.source().saveTo(editor.newOutputStream(0))
|
||||||
|
|||||||
@@ -128,6 +128,7 @@ class DownloadCache(
|
|||||||
*
|
*
|
||||||
* @param chapterName the name of the chapter to query.
|
* @param chapterName the name of the chapter to query.
|
||||||
* @param chapterScanlator scanlator of the chapter to query
|
* @param chapterScanlator scanlator of the chapter to query
|
||||||
|
* @param chapterUrl the url of the chapter to query
|
||||||
* @param mangaTitle the title of the manga to query.
|
* @param mangaTitle the title of the manga to query.
|
||||||
* @param sourceId the id of the source of the chapter.
|
* @param sourceId the id of the source of the chapter.
|
||||||
* @param skipCache whether to skip the directory cache and check in the filesystem.
|
* @param skipCache whether to skip the directory cache and check in the filesystem.
|
||||||
@@ -135,13 +136,14 @@ class DownloadCache(
|
|||||||
fun isChapterDownloaded(
|
fun isChapterDownloaded(
|
||||||
chapterName: String,
|
chapterName: String,
|
||||||
chapterScanlator: String?,
|
chapterScanlator: String?,
|
||||||
|
chapterUrl: String,
|
||||||
mangaTitle: String,
|
mangaTitle: String,
|
||||||
sourceId: Long,
|
sourceId: Long,
|
||||||
skipCache: Boolean,
|
skipCache: Boolean,
|
||||||
): Boolean {
|
): Boolean {
|
||||||
if (skipCache) {
|
if (skipCache) {
|
||||||
val source = sourceManager.getOrStub(sourceId)
|
val source = sourceManager.getOrStub(sourceId)
|
||||||
return provider.findChapterDir(chapterName, chapterScanlator, mangaTitle, source) != null
|
return provider.findChapterDir(chapterName, chapterScanlator, chapterUrl, mangaTitle, source) != null
|
||||||
}
|
}
|
||||||
|
|
||||||
renewCache()
|
renewCache()
|
||||||
@@ -153,6 +155,7 @@ class DownloadCache(
|
|||||||
return provider.getValidChapterDirNames(
|
return provider.getValidChapterDirNames(
|
||||||
chapterName,
|
chapterName,
|
||||||
chapterScanlator,
|
chapterScanlator,
|
||||||
|
chapterUrl,
|
||||||
).any { it in mangaDir.chapterDirs }
|
).any { it in mangaDir.chapterDirs }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -233,7 +236,7 @@ class DownloadCache(
|
|||||||
rootDownloadsDirMutex.withLock {
|
rootDownloadsDirMutex.withLock {
|
||||||
val sourceDir = rootDownloadsDir.sourceDirs[manga.source] ?: return
|
val sourceDir = rootDownloadsDir.sourceDirs[manga.source] ?: return
|
||||||
val mangaDir = sourceDir.mangaDirs[provider.getMangaDirName(manga.title)] ?: return
|
val mangaDir = sourceDir.mangaDirs[provider.getMangaDirName(manga.title)] ?: return
|
||||||
provider.getValidChapterDirNames(chapter.name, chapter.scanlator).forEach {
|
provider.getValidChapterDirNames(chapter.name, chapter.scanlator, chapter.url).forEach {
|
||||||
if (it in mangaDir.chapterDirs) {
|
if (it in mangaDir.chapterDirs) {
|
||||||
mangaDir.chapterDirs -= it
|
mangaDir.chapterDirs -= it
|
||||||
}
|
}
|
||||||
@@ -254,7 +257,7 @@ class DownloadCache(
|
|||||||
val sourceDir = rootDownloadsDir.sourceDirs[manga.source] ?: return
|
val sourceDir = rootDownloadsDir.sourceDirs[manga.source] ?: return
|
||||||
val mangaDir = sourceDir.mangaDirs[provider.getMangaDirName(manga.title)] ?: return
|
val mangaDir = sourceDir.mangaDirs[provider.getMangaDirName(manga.title)] ?: return
|
||||||
chapters.forEach { chapter ->
|
chapters.forEach { chapter ->
|
||||||
provider.getValidChapterDirNames(chapter.name, chapter.scanlator).forEach {
|
provider.getValidChapterDirNames(chapter.name, chapter.scanlator, chapter.url).forEach {
|
||||||
if (it in mangaDir.chapterDirs) {
|
if (it in mangaDir.chapterDirs) {
|
||||||
mangaDir.chapterDirs -= it
|
mangaDir.chapterDirs -= it
|
||||||
}
|
}
|
||||||
@@ -282,6 +285,41 @@ class DownloadCache(
|
|||||||
notifyChanges()
|
notifyChanges()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renames a manga in this cache.
|
||||||
|
*
|
||||||
|
* @param manga the manga being renamed.
|
||||||
|
* @param mangaUniFile the manga's new directory.
|
||||||
|
* @param newTitle the manga's new title.
|
||||||
|
*/
|
||||||
|
suspend fun renameManga(manga: Manga, mangaUniFile: UniFile, newTitle: String) {
|
||||||
|
rootDownloadsDirMutex.withLock {
|
||||||
|
val sourceDir = rootDownloadsDir.sourceDirs[manga.source] ?: return
|
||||||
|
val oldMangaDirName = provider.getMangaDirName(manga.title)
|
||||||
|
var oldChapterDirs: MutableSet<String>? = null
|
||||||
|
// Save the old name's cached chapter dirs
|
||||||
|
if (sourceDir.mangaDirs.containsKey(oldMangaDirName)) {
|
||||||
|
oldChapterDirs = sourceDir.mangaDirs[oldMangaDirName]?.chapterDirs
|
||||||
|
sourceDir.mangaDirs -= oldMangaDirName
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve/create the cached manga directory for new name
|
||||||
|
val newMangaDirName = provider.getMangaDirName(newTitle)
|
||||||
|
var mangaDir = sourceDir.mangaDirs[newMangaDirName]
|
||||||
|
if (mangaDir == null) {
|
||||||
|
mangaDir = MangaDirectory(mangaUniFile)
|
||||||
|
sourceDir.mangaDirs += newMangaDirName to mangaDir
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the old chapters to new name's cache
|
||||||
|
if (!oldChapterDirs.isNullOrEmpty()) {
|
||||||
|
mangaDir.chapterDirs += oldChapterDirs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
notifyChanges()
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun removeSource(source: Source) {
|
suspend fun removeSource(source: Source) {
|
||||||
rootDownloadsDirMutex.withLock {
|
rootDownloadsDirMutex.withLock {
|
||||||
rootDownloadsDir.sourceDirs -= source.id
|
rootDownloadsDir.sourceDirs -= source.id
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ class DownloadManager(
|
|||||||
* @return the list of pages from the chapter.
|
* @return the list of pages from the chapter.
|
||||||
*/
|
*/
|
||||||
fun buildPageList(source: Source, manga: Manga, chapter: Chapter): List<Page> {
|
fun buildPageList(source: Source, manga: Manga, chapter: Chapter): List<Page> {
|
||||||
val chapterDir = provider.findChapterDir(chapter.name, chapter.scanlator, manga.title, source)
|
val chapterDir = provider.findChapterDir(chapter.name, chapter.scanlator, chapter.url, manga.title, source)
|
||||||
val files = chapterDir?.listFiles().orEmpty()
|
val files = chapterDir?.listFiles().orEmpty()
|
||||||
.filter { it.isFile && ImageUtil.isImage(it.name) { it.openInputStream() } }
|
.filter { it.isFile && ImageUtil.isImage(it.name) { it.openInputStream() } }
|
||||||
|
|
||||||
@@ -169,7 +169,7 @@ class DownloadManager(
|
|||||||
|
|
||||||
return files.sortedBy { it.name }
|
return files.sortedBy { it.name }
|
||||||
.mapIndexed { i, file ->
|
.mapIndexed { i, file ->
|
||||||
Page(i, uri = file.uri).apply { status = Page.State.READY }
|
Page(i, uri = file.uri).apply { status = Page.State.Ready }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,11 +185,12 @@ class DownloadManager(
|
|||||||
fun isChapterDownloaded(
|
fun isChapterDownloaded(
|
||||||
chapterName: String,
|
chapterName: String,
|
||||||
chapterScanlator: String?,
|
chapterScanlator: String?,
|
||||||
|
chapterUrl: String,
|
||||||
mangaTitle: String,
|
mangaTitle: String,
|
||||||
sourceId: Long,
|
sourceId: Long,
|
||||||
skipCache: Boolean = false,
|
skipCache: Boolean = false,
|
||||||
): Boolean {
|
): Boolean {
|
||||||
return cache.isChapterDownloaded(chapterName, chapterScanlator, mangaTitle, sourceId, skipCache)
|
return cache.isChapterDownloaded(chapterName, chapterScanlator, chapterUrl, mangaTitle, sourceId, skipCache)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -327,6 +328,38 @@ class DownloadManager(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renames manga download folder
|
||||||
|
*
|
||||||
|
* @param manga the manga
|
||||||
|
* @param newTitle the new manga title.
|
||||||
|
*/
|
||||||
|
suspend fun renameManga(manga: Manga, newTitle: String) {
|
||||||
|
val source = sourceManager.getOrStub(manga.source)
|
||||||
|
val oldFolder = provider.findMangaDir(manga.title, source) ?: return
|
||||||
|
val newName = provider.getMangaDirName(newTitle)
|
||||||
|
|
||||||
|
if (oldFolder.name == newName) return
|
||||||
|
|
||||||
|
// just to be safe, don't allow downloads for this manga while renaming it
|
||||||
|
downloader.removeFromQueue(manga)
|
||||||
|
|
||||||
|
val capitalizationChanged = oldFolder.name.equals(newName, ignoreCase = true)
|
||||||
|
if (capitalizationChanged) {
|
||||||
|
val tempName = newName + Downloader.TMP_DIR_SUFFIX
|
||||||
|
if (!oldFolder.renameTo(tempName)) {
|
||||||
|
logcat(LogPriority.ERROR) { "Failed to rename manga download folder: ${oldFolder.name}" }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldFolder.renameTo(newName)) {
|
||||||
|
cache.renameManga(manga, oldFolder, newTitle)
|
||||||
|
} else {
|
||||||
|
logcat(LogPriority.ERROR) { "Failed to rename manga download folder: ${oldFolder.name}" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renames an already downloaded chapter
|
* Renames an already downloaded chapter
|
||||||
*
|
*
|
||||||
@@ -336,15 +369,18 @@ class DownloadManager(
|
|||||||
* @param newChapter the target chapter with the new name.
|
* @param newChapter the target chapter with the new name.
|
||||||
*/
|
*/
|
||||||
suspend fun renameChapter(source: Source, manga: Manga, oldChapter: Chapter, newChapter: Chapter) {
|
suspend fun renameChapter(source: Source, manga: Manga, oldChapter: Chapter, newChapter: Chapter) {
|
||||||
val oldNames = provider.getValidChapterDirNames(oldChapter.name, oldChapter.scanlator)
|
val oldNames = provider.getValidChapterDirNames(oldChapter.name, oldChapter.scanlator, oldChapter.url)
|
||||||
val mangaDir = provider.getMangaDir(manga.title, source)
|
val mangaDir = provider.getMangaDir(manga.title, source).getOrElse { e ->
|
||||||
|
logcat(LogPriority.ERROR, e) { "Manga download folder doesn't exist. Skipping renaming after source sync" }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Assume there's only 1 version of the chapter name formats present
|
// Assume there's only 1 version of the chapter name formats present
|
||||||
val oldDownload = oldNames.asSequence()
|
val oldDownload = oldNames.asSequence()
|
||||||
.mapNotNull { mangaDir.findFile(it) }
|
.mapNotNull { mangaDir.findFile(it) }
|
||||||
.firstOrNull() ?: return
|
.firstOrNull() ?: return
|
||||||
|
|
||||||
var newName = provider.getChapterDirName(newChapter.name, newChapter.scanlator)
|
var newName = provider.getChapterDirName(newChapter.name, newChapter.scanlator, newChapter.url)
|
||||||
if (oldDownload.isFile && oldDownload.extension == "cbz") {
|
if (oldDownload.isFile && oldDownload.extension == "cbz") {
|
||||||
newName += ".cbz"
|
newName += ".cbz"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,17 +3,20 @@ package eu.kanade.tachiyomi.data.download
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.hippo.unifile.UniFile
|
import com.hippo.unifile.UniFile
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
|
import eu.kanade.tachiyomi.util.lang.Hash.md5
|
||||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import tachiyomi.core.common.i18n.stringResource
|
import tachiyomi.core.common.i18n.stringResource
|
||||||
import tachiyomi.core.common.storage.displayablePath
|
import tachiyomi.core.common.storage.displayablePath
|
||||||
import tachiyomi.core.common.util.system.logcat
|
import tachiyomi.core.common.util.system.logcat
|
||||||
import tachiyomi.domain.chapter.model.Chapter
|
import tachiyomi.domain.chapter.model.Chapter
|
||||||
|
import tachiyomi.domain.library.service.LibraryPreferences
|
||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
import tachiyomi.domain.storage.service.StorageManager
|
import tachiyomi.domain.storage.service.StorageManager
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class is used to provide the directories where the downloads should be saved.
|
* This class is used to provide the directories where the downloads should be saved.
|
||||||
@@ -24,6 +27,7 @@ import uy.kohesive.injekt.api.get
|
|||||||
class DownloadProvider(
|
class DownloadProvider(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val storageManager: StorageManager = Injekt.get(),
|
private val storageManager: StorageManager = Injekt.get(),
|
||||||
|
private val libraryPreferences: LibraryPreferences = Injekt.get(),
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private val downloadsDir: UniFile?
|
private val downloadsDir: UniFile?
|
||||||
@@ -35,20 +39,36 @@ class DownloadProvider(
|
|||||||
* @param mangaTitle the title of the manga to query.
|
* @param mangaTitle the title of the manga to query.
|
||||||
* @param source the source of the manga.
|
* @param source the source of the manga.
|
||||||
*/
|
*/
|
||||||
internal fun getMangaDir(mangaTitle: String, source: Source): UniFile {
|
internal fun getMangaDir(mangaTitle: String, source: Source): Result<UniFile> {
|
||||||
try {
|
val downloadsDir = downloadsDir
|
||||||
return downloadsDir!!
|
if (downloadsDir == null) {
|
||||||
.createDirectory(getSourceDirName(source))!!
|
logcat(LogPriority.ERROR) { "Failed to create download directory" }
|
||||||
.createDirectory(getMangaDirName(mangaTitle))!!
|
return Result.failure(
|
||||||
} catch (e: Throwable) {
|
IOException(context.stringResource(MR.strings.storage_failed_to_create_download_directory)),
|
||||||
logcat(LogPriority.ERROR, e) { "Invalid download directory" }
|
|
||||||
throw Exception(
|
|
||||||
context.stringResource(
|
|
||||||
MR.strings.invalid_location,
|
|
||||||
downloadsDir?.displayablePath ?: "",
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val sourceDirName = getSourceDirName(source)
|
||||||
|
val sourceDir = downloadsDir.createDirectory(sourceDirName)
|
||||||
|
if (sourceDir == null) {
|
||||||
|
val displayablePath = downloadsDir.displayablePath + "/$sourceDirName"
|
||||||
|
logcat(LogPriority.ERROR) { "Failed to create source download directory: $displayablePath" }
|
||||||
|
return Result.failure(
|
||||||
|
IOException(context.stringResource(MR.strings.storage_failed_to_create_directory, displayablePath)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val mangaDirName = getMangaDirName(mangaTitle)
|
||||||
|
val mangaDir = sourceDir.createDirectory(mangaDirName)
|
||||||
|
if (mangaDir == null) {
|
||||||
|
val displayablePath = sourceDir.displayablePath + "/$mangaDirName"
|
||||||
|
logcat(LogPriority.ERROR) { "Failed to create manga download directory: $displayablePath" }
|
||||||
|
return Result.failure(
|
||||||
|
IOException(context.stringResource(MR.strings.storage_failed_to_create_directory, displayablePath)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result.success(mangaDir)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -79,9 +99,15 @@ class DownloadProvider(
|
|||||||
* @param mangaTitle the title of the manga to query.
|
* @param mangaTitle the title of the manga to query.
|
||||||
* @param source the source of the chapter.
|
* @param source the source of the chapter.
|
||||||
*/
|
*/
|
||||||
fun findChapterDir(chapterName: String, chapterScanlator: String?, mangaTitle: String, source: Source): UniFile? {
|
fun findChapterDir(
|
||||||
|
chapterName: String,
|
||||||
|
chapterScanlator: String?,
|
||||||
|
chapterUrl: String,
|
||||||
|
mangaTitle: String,
|
||||||
|
source: Source,
|
||||||
|
): UniFile? {
|
||||||
val mangaDir = findMangaDir(mangaTitle, source)
|
val mangaDir = findMangaDir(mangaTitle, source)
|
||||||
return getValidChapterDirNames(chapterName, chapterScanlator).asSequence()
|
return getValidChapterDirNames(chapterName, chapterScanlator, chapterUrl).asSequence()
|
||||||
.mapNotNull { mangaDir?.findFile(it) }
|
.mapNotNull { mangaDir?.findFile(it) }
|
||||||
.firstOrNull()
|
.firstOrNull()
|
||||||
}
|
}
|
||||||
@@ -96,7 +122,7 @@ class DownloadProvider(
|
|||||||
fun findChapterDirs(chapters: List<Chapter>, manga: Manga, source: Source): Pair<UniFile?, List<UniFile>> {
|
fun findChapterDirs(chapters: List<Chapter>, manga: Manga, source: Source): Pair<UniFile?, List<UniFile>> {
|
||||||
val mangaDir = findMangaDir(manga.title, source) ?: return null to emptyList()
|
val mangaDir = findMangaDir(manga.title, source) ?: return null to emptyList()
|
||||||
return mangaDir to chapters.mapNotNull { chapter ->
|
return mangaDir to chapters.mapNotNull { chapter ->
|
||||||
getValidChapterDirNames(chapter.name, chapter.scanlator).asSequence()
|
getValidChapterDirNames(chapter.name, chapter.scanlator, chapter.url).asSequence()
|
||||||
.mapNotNull { mangaDir.findFile(it) }
|
.mapNotNull { mangaDir.findFile(it) }
|
||||||
.firstOrNull()
|
.firstOrNull()
|
||||||
}
|
}
|
||||||
@@ -108,7 +134,10 @@ class DownloadProvider(
|
|||||||
* @param source the source to query.
|
* @param source the source to query.
|
||||||
*/
|
*/
|
||||||
fun getSourceDirName(source: Source): String {
|
fun getSourceDirName(source: Source): String {
|
||||||
return DiskUtil.buildValidFilename(source.toString())
|
return DiskUtil.buildValidFilename(
|
||||||
|
source.toString(),
|
||||||
|
disallowNonAscii = libraryPreferences.disallowNonAsciiFilenames().get(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -117,23 +146,75 @@ class DownloadProvider(
|
|||||||
* @param mangaTitle the title of the manga to query.
|
* @param mangaTitle the title of the manga to query.
|
||||||
*/
|
*/
|
||||||
fun getMangaDirName(mangaTitle: String): String {
|
fun getMangaDirName(mangaTitle: String): String {
|
||||||
return DiskUtil.buildValidFilename(mangaTitle)
|
return DiskUtil.buildValidFilename(
|
||||||
|
mangaTitle,
|
||||||
|
disallowNonAscii = libraryPreferences.disallowNonAsciiFilenames().get(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the chapter directory name for a chapter.
|
* Returns the chapter directory name for a chapter.
|
||||||
*
|
*
|
||||||
* @param chapterName the name of the chapter to query.
|
* @param chapterName the name of the chapter to query.
|
||||||
* @param chapterScanlator scanlator of the chapter to query
|
* @param chapterScanlator scanlator of the chapter to query.
|
||||||
|
* @param chapterUrl url of the chapter to query.
|
||||||
*/
|
*/
|
||||||
fun getChapterDirName(chapterName: String, chapterScanlator: String?): String {
|
fun getChapterDirName(
|
||||||
val newChapterName = sanitizeChapterName(chapterName)
|
chapterName: String,
|
||||||
return DiskUtil.buildValidFilename(
|
chapterScanlator: String?,
|
||||||
|
chapterUrl: String,
|
||||||
|
disallowNonAsciiFilenames: Boolean = libraryPreferences.disallowNonAsciiFilenames().get(),
|
||||||
|
): String {
|
||||||
|
var dirName = sanitizeChapterName(chapterName)
|
||||||
|
if (!chapterScanlator.isNullOrBlank()) {
|
||||||
|
dirName = chapterScanlator + "_" + dirName
|
||||||
|
}
|
||||||
|
// Subtract 7 bytes for hash and underscore, 4 bytes for .cbz
|
||||||
|
dirName = DiskUtil.buildValidFilename(dirName, DiskUtil.MAX_FILE_NAME_BYTES - 11, disallowNonAsciiFilenames)
|
||||||
|
dirName += "_" + md5(chapterUrl).take(6)
|
||||||
|
return dirName
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns list of names that might have been previously used as
|
||||||
|
* the directory name for a chapter.
|
||||||
|
* Add to this list if naming pattern ever changes.
|
||||||
|
*
|
||||||
|
* @param chapterName the name of the chapter to query.
|
||||||
|
* @param chapterScanlator scanlator of the chapter to query.
|
||||||
|
* @param chapterUrl url of the chapter to query.
|
||||||
|
*/
|
||||||
|
private fun getLegacyChapterDirNames(
|
||||||
|
chapterName: String,
|
||||||
|
chapterScanlator: String?,
|
||||||
|
chapterUrl: String,
|
||||||
|
): List<String> {
|
||||||
|
val sanitizedChapterName = sanitizeChapterName(chapterName)
|
||||||
|
val chapterNameV1 = DiskUtil.buildValidFilename(
|
||||||
when {
|
when {
|
||||||
!chapterScanlator.isNullOrBlank() -> "${chapterScanlator}_$newChapterName"
|
!chapterScanlator.isNullOrBlank() -> "${chapterScanlator}_$sanitizedChapterName"
|
||||||
else -> newChapterName
|
else -> sanitizedChapterName
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Get the filename that would be generated if the user were
|
||||||
|
// using the other value for the disallow non-ASCII
|
||||||
|
// filenames setting. This ensures that chapters downloaded
|
||||||
|
// before the user changed the setting can still be found.
|
||||||
|
val otherChapterDirName =
|
||||||
|
getChapterDirName(
|
||||||
|
chapterName,
|
||||||
|
chapterScanlator,
|
||||||
|
chapterUrl,
|
||||||
|
!libraryPreferences.disallowNonAsciiFilenames().get(),
|
||||||
|
)
|
||||||
|
|
||||||
|
return buildList(2) {
|
||||||
|
// Chapter name without hash (unable to handle duplicate
|
||||||
|
// chapter names)
|
||||||
|
add(chapterNameV1)
|
||||||
|
add(otherChapterDirName)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -148,24 +229,30 @@ class DownloadProvider(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun isChapterDirNameChanged(oldChapter: Chapter, newChapter: Chapter): Boolean {
|
fun isChapterDirNameChanged(oldChapter: Chapter, newChapter: Chapter): Boolean {
|
||||||
return oldChapter.name != newChapter.name ||
|
return getChapterDirName(oldChapter.name, oldChapter.scanlator, oldChapter.url) !=
|
||||||
oldChapter.scanlator?.takeIf { it.isNotBlank() } != newChapter.scanlator?.takeIf { it.isNotBlank() }
|
getChapterDirName(newChapter.name, newChapter.scanlator, newChapter.url)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns valid downloaded chapter directory names.
|
* Returns valid downloaded chapter directory names.
|
||||||
*
|
*
|
||||||
* @param chapterName the name of the chapter to query.
|
* @param chapter the domain chapter object.
|
||||||
* @param chapterScanlator scanlator of the chapter to query
|
|
||||||
*/
|
*/
|
||||||
fun getValidChapterDirNames(chapterName: String, chapterScanlator: String?): List<String> {
|
fun getValidChapterDirNames(chapterName: String, chapterScanlator: String?, chapterUrl: String): List<String> {
|
||||||
val chapterDirName = getChapterDirName(chapterName, chapterScanlator)
|
val chapterDirName = getChapterDirName(chapterName, chapterScanlator, chapterUrl)
|
||||||
return buildList(2) {
|
val legacyChapterDirNames = getLegacyChapterDirNames(chapterName, chapterScanlator, chapterUrl)
|
||||||
|
|
||||||
|
return buildList {
|
||||||
// Folder of images
|
// Folder of images
|
||||||
add(chapterDirName)
|
add(chapterDirName)
|
||||||
|
|
||||||
// Archived chapters
|
// Archived chapters
|
||||||
add("$chapterDirName.cbz")
|
add("$chapterDirName.cbz")
|
||||||
|
|
||||||
|
// any legacy names
|
||||||
|
legacyChapterDirNames.forEach {
|
||||||
|
add(it)
|
||||||
|
add("$it.cbz")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -191,15 +191,17 @@ class Downloader(
|
|||||||
if (isRunning) return
|
if (isRunning) return
|
||||||
|
|
||||||
downloaderJob = scope.launch {
|
downloaderJob = scope.launch {
|
||||||
val activeDownloadsFlow = queueState.transformLatest { queue ->
|
val activeDownloadsFlow = combine(
|
||||||
|
queueState,
|
||||||
|
downloadPreferences.parallelSourceLimit().changes(),
|
||||||
|
) { a, b -> a to b }.transformLatest { (queue, parallelCount) ->
|
||||||
while (true) {
|
while (true) {
|
||||||
val activeDownloads = queue.asSequence()
|
val activeDownloads = queue.asSequence()
|
||||||
// Ignore completed downloads, leave them in the queue
|
// Ignore completed downloads, leave them in the queue
|
||||||
.filter { it.status.value <= Download.State.DOWNLOADING.value }
|
.filter { it.status.value <= Download.State.DOWNLOADING.value }
|
||||||
.groupBy { it.source }
|
.groupBy { it.source }
|
||||||
.toList()
|
.toList()
|
||||||
// Concurrently download from 5 different sources
|
.take(parallelCount)
|
||||||
.take(5)
|
|
||||||
.map { (_, downloads) -> downloads.first() }
|
.map { (_, downloads) -> downloads.first() }
|
||||||
emit(activeDownloads)
|
emit(activeDownloads)
|
||||||
|
|
||||||
@@ -211,7 +213,8 @@ class Downloader(
|
|||||||
}.filter { it }
|
}.filter { it }
|
||||||
activeDownloadsErroredFlow.first()
|
activeDownloadsErroredFlow.first()
|
||||||
}
|
}
|
||||||
}.distinctUntilChanged()
|
}
|
||||||
|
.distinctUntilChanged()
|
||||||
|
|
||||||
// Use supervisorScope to cancel child jobs when the downloader job is cancelled
|
// Use supervisorScope to cancel child jobs when the downloader job is cancelled
|
||||||
supervisorScope {
|
supervisorScope {
|
||||||
@@ -274,7 +277,7 @@ class Downloader(
|
|||||||
val wasEmpty = queueState.value.isEmpty()
|
val wasEmpty = queueState.value.isEmpty()
|
||||||
val chaptersToQueue = chapters.asSequence()
|
val chaptersToQueue = chapters.asSequence()
|
||||||
// Filter out those already downloaded.
|
// Filter out those already downloaded.
|
||||||
.filter { provider.findChapterDir(it.name, it.scanlator, manga.title, source) == null }
|
.filter { provider.findChapterDir(it.name, it.scanlator, it.url, manga.title, source) == null }
|
||||||
// Add chapters to queue from the start.
|
// Add chapters to queue from the start.
|
||||||
.sortedByDescending { it.sourceOrder }
|
.sortedByDescending { it.sourceOrder }
|
||||||
// Filter out those already enqueued.
|
// Filter out those already enqueued.
|
||||||
@@ -299,7 +302,10 @@ class Downloader(
|
|||||||
maxDownloadsFromSource > CHAPTERS_PER_SOURCE_QUEUE_WARNING_THRESHOLD
|
maxDownloadsFromSource > CHAPTERS_PER_SOURCE_QUEUE_WARNING_THRESHOLD
|
||||||
) {
|
) {
|
||||||
notifier.onWarning(
|
notifier.onWarning(
|
||||||
context.stringResource(MR.strings.download_queue_size_warning),
|
context.stringResource(
|
||||||
|
MR.strings.download_queue_size_warning,
|
||||||
|
context.stringResource(MR.strings.app_name),
|
||||||
|
),
|
||||||
WARNING_NOTIF_TIMEOUT_MS,
|
WARNING_NOTIF_TIMEOUT_MS,
|
||||||
NotificationHandler.openUrl(context, LibraryUpdateNotifier.HELP_WARNING_URL),
|
NotificationHandler.openUrl(context, LibraryUpdateNotifier.HELP_WARNING_URL),
|
||||||
)
|
)
|
||||||
@@ -315,7 +321,11 @@ class Downloader(
|
|||||||
* @param download the chapter to be downloaded.
|
* @param download the chapter to be downloaded.
|
||||||
*/
|
*/
|
||||||
private suspend fun downloadChapter(download: Download) {
|
private suspend fun downloadChapter(download: Download) {
|
||||||
val mangaDir = provider.getMangaDir(download.manga.title, download.source)
|
val mangaDir = provider.getMangaDir(download.manga.title, download.source).getOrElse { e ->
|
||||||
|
download.status = Download.State.ERROR
|
||||||
|
notifier.onError(e.message, download.chapter.name, download.manga.title, download.manga.id)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
val availSpace = DiskUtil.getAvailableStorageSpace(mangaDir)
|
val availSpace = DiskUtil.getAvailableStorageSpace(mangaDir)
|
||||||
if (availSpace != -1L && availSpace < MIN_DISK_SPACE) {
|
if (availSpace != -1L && availSpace < MIN_DISK_SPACE) {
|
||||||
@@ -329,7 +339,11 @@ class Downloader(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val chapterDirname = provider.getChapterDirName(download.chapter.name, download.chapter.scanlator)
|
val chapterDirname = provider.getChapterDirName(
|
||||||
|
download.chapter.name,
|
||||||
|
download.chapter.scanlator,
|
||||||
|
download.chapter.url,
|
||||||
|
)
|
||||||
val tmpDir = mangaDir.createDirectory(chapterDirname + TMP_DIR_SUFFIX)!!
|
val tmpDir = mangaDir.createDirectory(chapterDirname + TMP_DIR_SUFFIX)!!
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -355,23 +369,22 @@ class Downloader(
|
|||||||
download.status = Download.State.DOWNLOADING
|
download.status = Download.State.DOWNLOADING
|
||||||
|
|
||||||
// Start downloading images, consider we can have downloaded images already
|
// Start downloading images, consider we can have downloaded images already
|
||||||
// Concurrently do 2 pages at a time
|
pageList.asFlow().flatMapMerge(concurrency = downloadPreferences.parallelPageLimit().get()) { page ->
|
||||||
pageList.asFlow()
|
|
||||||
.flatMapMerge(concurrency = 2) { page ->
|
|
||||||
flow {
|
flow {
|
||||||
// Fetch image URL if necessary
|
// Fetch image URL if necessary
|
||||||
if (page.imageUrl.isNullOrEmpty()) {
|
if (page.imageUrl.isNullOrEmpty()) {
|
||||||
page.status = Page.State.LOAD_PAGE
|
page.status = Page.State.LoadPage
|
||||||
try {
|
try {
|
||||||
page.imageUrl = download.source.getImageUrl(page)
|
page.imageUrl = download.source.getImageUrl(page)
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
page.status = Page.State.ERROR
|
page.status = Page.State.Error(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
withIOContext { getOrDownloadImage(page, download, tmpDir) }
|
withIOContext { getOrDownloadImage(page, download, tmpDir) }
|
||||||
emit(page)
|
emit(page)
|
||||||
}.flowOn(Dispatchers.IO)
|
}
|
||||||
|
.flowOn(Dispatchers.IO)
|
||||||
}
|
}
|
||||||
.collect {
|
.collect {
|
||||||
// Do when page is downloaded.
|
// Do when page is downloaded.
|
||||||
@@ -452,12 +465,12 @@ class Downloader(
|
|||||||
|
|
||||||
page.uri = file.uri
|
page.uri = file.uri
|
||||||
page.progress = 100
|
page.progress = 100
|
||||||
page.status = Page.State.READY
|
page.status = Page.State.Ready
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
if (e is CancellationException) throw e
|
if (e is CancellationException) throw e
|
||||||
// Mark this page as error and allow to download the remaining
|
// Mark this page as error and allow to download the remaining
|
||||||
page.progress = 0
|
page.progress = 0
|
||||||
page.status = Page.State.ERROR
|
page.status = Page.State.Error(e)
|
||||||
notifier.onError(e.message, download.chapter.name, download.manga.title, download.manga.id)
|
notifier.onError(e.message, download.chapter.name, download.manga.title, download.manga.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -471,7 +484,7 @@ class Downloader(
|
|||||||
* @param filename the filename of the image.
|
* @param filename the filename of the image.
|
||||||
*/
|
*/
|
||||||
private suspend fun downloadImage(page: Page, source: HttpSource, tmpDir: UniFile, filename: String): UniFile {
|
private suspend fun downloadImage(page: Page, source: HttpSource, tmpDir: UniFile, filename: String): UniFile {
|
||||||
page.status = Page.State.DOWNLOAD_IMAGE
|
page.status = Page.State.DownloadImage
|
||||||
page.progress = 0
|
page.progress = 0
|
||||||
return flow {
|
return flow {
|
||||||
val response = source.getImage(page)
|
val response = source.getImage(page)
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ data class Download(
|
|||||||
get() = pages?.sumOf(Page::progress) ?: 0
|
get() = pages?.sumOf(Page::progress) ?: 0
|
||||||
|
|
||||||
val downloadedImages: Int
|
val downloadedImages: Int
|
||||||
get() = pages?.count { it.status == Page.State.READY } ?: 0
|
get() = pages?.count { it.status == Page.State.Ready } ?: 0
|
||||||
|
|
||||||
@Transient
|
@Transient
|
||||||
private val _statusFlow = MutableStateFlow(State.NOT_DOWNLOADED)
|
private val _statusFlow = MutableStateFlow(State.NOT_DOWNLOADED)
|
||||||
|
|||||||
@@ -71,9 +71,12 @@ import java.time.Instant
|
|||||||
import java.time.ZonedDateTime
|
import java.time.ZonedDateTime
|
||||||
import java.util.concurrent.CopyOnWriteArrayList
|
import java.util.concurrent.CopyOnWriteArrayList
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import kotlin.concurrent.atomics.AtomicBoolean
|
||||||
import java.util.concurrent.atomic.AtomicInteger
|
import kotlin.concurrent.atomics.AtomicInt
|
||||||
|
import kotlin.concurrent.atomics.ExperimentalAtomicApi
|
||||||
|
import kotlin.concurrent.atomics.incrementAndFetch
|
||||||
|
|
||||||
|
@OptIn(ExperimentalAtomicApi::class)
|
||||||
class LibraryUpdateJob(private val context: Context, workerParams: WorkerParameters) :
|
class LibraryUpdateJob(private val context: Context, workerParams: WorkerParameters) :
|
||||||
CoroutineWorker(context, workerParams) {
|
CoroutineWorker(context, workerParams) {
|
||||||
|
|
||||||
@@ -155,25 +158,16 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
|||||||
val libraryManga = getLibraryManga.await()
|
val libraryManga = getLibraryManga.await()
|
||||||
|
|
||||||
val listToUpdate = if (categoryId != -1L) {
|
val listToUpdate = if (categoryId != -1L) {
|
||||||
libraryManga.filter { it.category == categoryId }
|
libraryManga.filter { categoryId in it.categories }
|
||||||
} else {
|
} else {
|
||||||
val categoriesToUpdate = libraryPreferences.updateCategories().get().map { it.toLong() }
|
val includedCategories = libraryPreferences.updateCategories().get().map { it.toLong() }
|
||||||
val includedManga = if (categoriesToUpdate.isNotEmpty()) {
|
val excludedCategories = libraryPreferences.updateCategoriesExclude().get().map { it.toLong() }
|
||||||
libraryManga.filter { it.category in categoriesToUpdate }
|
|
||||||
} else {
|
|
||||||
libraryManga
|
|
||||||
}
|
|
||||||
|
|
||||||
val categoriesToExclude = libraryPreferences.updateCategoriesExclude().get().map { it.toLong() }
|
libraryManga.filter {
|
||||||
val excludedMangaIds = if (categoriesToExclude.isNotEmpty()) {
|
val included = includedCategories.isEmpty() || it.categories.intersect(includedCategories).isNotEmpty()
|
||||||
libraryManga.filter { it.category in categoriesToExclude }.map { it.manga.id }
|
val excluded = it.categories.intersect(excludedCategories).isNotEmpty()
|
||||||
} else {
|
included && !excluded
|
||||||
emptyList()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
includedManga
|
|
||||||
.filterNot { it.manga.id in excludedMangaIds }
|
|
||||||
.distinctBy { it.manga.id }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val restrictions = libraryPreferences.autoUpdateMangaRestrictions().get()
|
val restrictions = libraryPreferences.autoUpdateMangaRestrictions().get()
|
||||||
@@ -183,7 +177,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
|||||||
mangaToUpdate = listToUpdate
|
mangaToUpdate = listToUpdate
|
||||||
.filter {
|
.filter {
|
||||||
when {
|
when {
|
||||||
it.manga.updateStrategy != UpdateStrategy.ALWAYS_UPDATE -> {
|
it.manga.updateStrategy == UpdateStrategy.ONLY_FETCH_ONCE && it.totalChapters > 0L -> {
|
||||||
skippedUpdates.add(
|
skippedUpdates.add(
|
||||||
it.manga to context.stringResource(MR.strings.skipped_reason_not_always_update),
|
it.manga to context.stringResource(MR.strings.skipped_reason_not_always_update),
|
||||||
)
|
)
|
||||||
@@ -240,7 +234,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
|||||||
*/
|
*/
|
||||||
private suspend fun updateChapterList() {
|
private suspend fun updateChapterList() {
|
||||||
val semaphore = Semaphore(5)
|
val semaphore = Semaphore(5)
|
||||||
val progressCount = AtomicInteger(0)
|
val progressCount = AtomicInt(0)
|
||||||
val currentlyUpdatingManga = CopyOnWriteArrayList<Manga>()
|
val currentlyUpdatingManga = CopyOnWriteArrayList<Manga>()
|
||||||
val newUpdates = CopyOnWriteArrayList<Pair<Manga, Array<Chapter>>>()
|
val newUpdates = CopyOnWriteArrayList<Pair<Manga, Array<Chapter>>>()
|
||||||
val failedUpdates = CopyOnWriteArrayList<Pair<Manga, String?>>()
|
val failedUpdates = CopyOnWriteArrayList<Pair<Manga, String?>>()
|
||||||
@@ -275,7 +269,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
|||||||
|
|
||||||
if (chaptersToDownload.isNotEmpty()) {
|
if (chaptersToDownload.isNotEmpty()) {
|
||||||
downloadChapters(manga, chaptersToDownload)
|
downloadChapters(manga, chaptersToDownload)
|
||||||
hasDownloads.set(true)
|
hasDownloads.store(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
libraryPreferences.newUpdatesCount().getAndSet { it + newChapters.size }
|
libraryPreferences.newUpdatesCount().getAndSet { it + newChapters.size }
|
||||||
@@ -308,7 +302,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
|||||||
|
|
||||||
if (newUpdates.isNotEmpty()) {
|
if (newUpdates.isNotEmpty()) {
|
||||||
notifier.showUpdateNotifications(newUpdates)
|
notifier.showUpdateNotifications(newUpdates)
|
||||||
if (hasDownloads.get()) {
|
if (hasDownloads.load()) {
|
||||||
downloadManager.startDownloads()
|
downloadManager.startDownloads()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -354,7 +348,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
|||||||
|
|
||||||
private suspend fun withUpdateNotification(
|
private suspend fun withUpdateNotification(
|
||||||
updatingManga: CopyOnWriteArrayList<Manga>,
|
updatingManga: CopyOnWriteArrayList<Manga>,
|
||||||
completed: AtomicInteger,
|
completed: AtomicInt,
|
||||||
manga: Manga,
|
manga: Manga,
|
||||||
block: suspend () -> Unit,
|
block: suspend () -> Unit,
|
||||||
) = coroutineScope {
|
) = coroutineScope {
|
||||||
@@ -363,7 +357,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
|||||||
updatingManga.add(manga)
|
updatingManga.add(manga)
|
||||||
notifier.showProgressNotification(
|
notifier.showProgressNotification(
|
||||||
updatingManga,
|
updatingManga,
|
||||||
completed.get(),
|
completed.load(),
|
||||||
mangaToUpdate.size,
|
mangaToUpdate.size,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -372,10 +366,10 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
|||||||
ensureActive()
|
ensureActive()
|
||||||
|
|
||||||
updatingManga.remove(manga)
|
updatingManga.remove(manga)
|
||||||
completed.getAndIncrement()
|
completed.incrementAndFetch()
|
||||||
notifier.showProgressNotification(
|
notifier.showProgressNotification(
|
||||||
updatingManga,
|
updatingManga,
|
||||||
completed.get(),
|
completed.load(),
|
||||||
mangaToUpdate.size,
|
mangaToUpdate.size,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,8 +37,11 @@ import tachiyomi.domain.source.service.SourceManager
|
|||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.util.concurrent.CopyOnWriteArrayList
|
import java.util.concurrent.CopyOnWriteArrayList
|
||||||
import java.util.concurrent.atomic.AtomicInteger
|
import kotlin.concurrent.atomics.AtomicInt
|
||||||
|
import kotlin.concurrent.atomics.ExperimentalAtomicApi
|
||||||
|
import kotlin.concurrent.atomics.fetchAndIncrement
|
||||||
|
|
||||||
|
@OptIn(ExperimentalAtomicApi::class)
|
||||||
class MetadataUpdateJob(private val context: Context, workerParams: WorkerParameters) :
|
class MetadataUpdateJob(private val context: Context, workerParams: WorkerParameters) :
|
||||||
CoroutineWorker(context, workerParams) {
|
CoroutineWorker(context, workerParams) {
|
||||||
|
|
||||||
@@ -97,7 +100,7 @@ class MetadataUpdateJob(private val context: Context, workerParams: WorkerParame
|
|||||||
|
|
||||||
private suspend fun updateMetadata() {
|
private suspend fun updateMetadata() {
|
||||||
val semaphore = Semaphore(5)
|
val semaphore = Semaphore(5)
|
||||||
val progressCount = AtomicInteger(0)
|
val progressCount = AtomicInt(0)
|
||||||
val currentlyUpdatingManga = CopyOnWriteArrayList<Manga>()
|
val currentlyUpdatingManga = CopyOnWriteArrayList<Manga>()
|
||||||
|
|
||||||
coroutineScope {
|
coroutineScope {
|
||||||
@@ -142,7 +145,7 @@ class MetadataUpdateJob(private val context: Context, workerParams: WorkerParame
|
|||||||
|
|
||||||
private suspend fun withUpdateNotification(
|
private suspend fun withUpdateNotification(
|
||||||
updatingManga: CopyOnWriteArrayList<Manga>,
|
updatingManga: CopyOnWriteArrayList<Manga>,
|
||||||
completed: AtomicInteger,
|
completed: AtomicInt,
|
||||||
manga: Manga,
|
manga: Manga,
|
||||||
block: suspend () -> Unit,
|
block: suspend () -> Unit,
|
||||||
) = coroutineScope {
|
) = coroutineScope {
|
||||||
@@ -151,7 +154,7 @@ class MetadataUpdateJob(private val context: Context, workerParams: WorkerParame
|
|||||||
updatingManga.add(manga)
|
updatingManga.add(manga)
|
||||||
notifier.showProgressNotification(
|
notifier.showProgressNotification(
|
||||||
updatingManga,
|
updatingManga,
|
||||||
completed.get(),
|
completed.load(),
|
||||||
mangaToUpdate.size,
|
mangaToUpdate.size,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -160,10 +163,10 @@ class MetadataUpdateJob(private val context: Context, workerParams: WorkerParame
|
|||||||
ensureActive()
|
ensureActive()
|
||||||
|
|
||||||
updatingManga.remove(manga)
|
updatingManga.remove(manga)
|
||||||
completed.getAndIncrement()
|
completed.fetchAndIncrement()
|
||||||
notifier.showProgressNotification(
|
notifier.showProgressNotification(
|
||||||
updatingManga,
|
updatingManga,
|
||||||
completed.get(),
|
completed.load(),
|
||||||
mangaToUpdate.size,
|
mangaToUpdate.size,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -583,18 +583,17 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns [PendingIntent] that starts a share activity for a backup file.
|
* Returns [PendingIntent] that directly launches a share activity for a backup file.
|
||||||
*
|
*
|
||||||
* @param context context of application
|
* @param context context of application
|
||||||
* @param uri uri of backup file
|
* @param uri uri of backup file
|
||||||
* @return [PendingIntent]
|
* @return [PendingIntent]
|
||||||
*/
|
*/
|
||||||
internal fun shareBackupPendingBroadcast(context: Context, uri: Uri): PendingIntent {
|
internal fun shareBackupPendingActivity(context: Context, uri: Uri): PendingIntent {
|
||||||
val intent = Intent(context, NotificationReceiver::class.java).apply {
|
val intent = uri.toShareIntent(context, "application/x-protobuf+gzip").apply {
|
||||||
action = ACTION_SHARE_BACKUP
|
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
putExtra(EXTRA_URI, uri)
|
|
||||||
}
|
}
|
||||||
return PendingIntent.getBroadcast(
|
return PendingIntent.getActivity(
|
||||||
context,
|
context,
|
||||||
0,
|
0,
|
||||||
intent,
|
intent,
|
||||||
|
|||||||
@@ -104,6 +104,7 @@ class BangumiApi(
|
|||||||
.awaitSuccess()
|
.awaitSuccess()
|
||||||
.parseAs<BGMSearchResult>()
|
.parseAs<BGMSearchResult>()
|
||||||
.data
|
.data
|
||||||
|
.filter { it.platform == null || it.platform == "漫画" }
|
||||||
.map { it.toTrackSearch(trackId) }
|
.map { it.toTrackSearch(trackId) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ data class BGMSubject(
|
|||||||
val volumes: Long = 0,
|
val volumes: Long = 0,
|
||||||
val eps: Long = 0,
|
val eps: Long = 0,
|
||||||
val rating: BGMSubjectRating?,
|
val rating: BGMSubjectRating?,
|
||||||
|
val platform: String?,
|
||||||
) {
|
) {
|
||||||
fun toTrackSearch(trackId: Long): TrackSearch = TrackSearch.create(trackId).apply {
|
fun toTrackSearch(trackId: Long): TrackSearch = TrackSearch.create(trackId).apply {
|
||||||
remote_id = this@BGMSubject.id
|
remote_id = this@BGMSubject.id
|
||||||
|
|||||||
@@ -104,6 +104,7 @@ class Kitsu(id: Long) : BaseTracker(id, "Kitsu"), DeletableTracker {
|
|||||||
return if (remoteTrack != null) {
|
return if (remoteTrack != null) {
|
||||||
track.copyPersonalFrom(remoteTrack, copyRemotePrivate = false)
|
track.copyPersonalFrom(remoteTrack, copyRemotePrivate = false)
|
||||||
track.remote_id = remoteTrack.remote_id
|
track.remote_id = remoteTrack.remote_id
|
||||||
|
track.library_id = remoteTrack.library_id
|
||||||
|
|
||||||
if (track.status != COMPLETED) {
|
if (track.status != COMPLETED) {
|
||||||
track.status = if (hasReadChapters) READING else track.status
|
track.status = if (hasReadChapters) READING else track.status
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
|
|||||||
.awaitSuccess()
|
.awaitSuccess()
|
||||||
.parseAs<KitsuAddMangaResult>()
|
.parseAs<KitsuAddMangaResult>()
|
||||||
.let {
|
.let {
|
||||||
track.remote_id = it.data.id
|
track.library_id = it.data.id
|
||||||
track
|
track
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -88,7 +88,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
|
|||||||
val data = buildJsonObject {
|
val data = buildJsonObject {
|
||||||
putJsonObject("data") {
|
putJsonObject("data") {
|
||||||
put("type", "libraryEntries")
|
put("type", "libraryEntries")
|
||||||
put("id", track.remote_id)
|
put("id", track.library_id)
|
||||||
putJsonObject("attributes") {
|
putJsonObject("attributes") {
|
||||||
put("status", track.toApiStatus())
|
put("status", track.toApiStatus())
|
||||||
put("progress", track.last_chapter_read.toInt())
|
put("progress", track.last_chapter_read.toInt())
|
||||||
@@ -102,7 +102,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
|
|||||||
|
|
||||||
authClient.newCall(
|
authClient.newCall(
|
||||||
Request.Builder()
|
Request.Builder()
|
||||||
.url("${BASE_URL}library-entries/${track.remote_id}")
|
.url("${BASE_URL}library-entries/${track.library_id}")
|
||||||
.headers(
|
.headers(
|
||||||
headersOf("Content-Type", VND_API_JSON),
|
headersOf("Content-Type", VND_API_JSON),
|
||||||
)
|
)
|
||||||
@@ -119,7 +119,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
|
|||||||
withIOContext {
|
withIOContext {
|
||||||
authClient.newCall(
|
authClient.newCall(
|
||||||
DELETE(
|
DELETE(
|
||||||
"${BASE_URL}library-entries/${track.remoteId}",
|
"${BASE_URL}library-entries/${track.libraryId}",
|
||||||
headers = headersOf("Content-Type", VND_API_JSON),
|
headers = headersOf("Content-Type", VND_API_JSON),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -192,7 +192,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
|
|||||||
suspend fun getLibManga(track: Track): Track {
|
suspend fun getLibManga(track: Track): Track {
|
||||||
return withIOContext {
|
return withIOContext {
|
||||||
val url = "${BASE_URL}library-entries".toUri().buildUpon()
|
val url = "${BASE_URL}library-entries".toUri().buildUpon()
|
||||||
.encodedQuery("filter[id]=${track.remote_id}")
|
.encodedQuery("filter[id]=${track.library_id}")
|
||||||
.appendQueryParameter("include", "manga")
|
.appendQueryParameter("include", "manga")
|
||||||
.build()
|
.build()
|
||||||
with(json) {
|
with(json) {
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ data class KitsuListSearchResult(
|
|||||||
val manga = included[0].attributes
|
val manga = included[0].attributes
|
||||||
|
|
||||||
return TrackSearch.create(TrackerManager.KITSU).apply {
|
return TrackSearch.create(TrackerManager.KITSU).apply {
|
||||||
remote_id = userData.id
|
remote_id = included[0].id
|
||||||
|
library_id = userData.id
|
||||||
title = manga.canonicalTitle
|
title = manga.canonicalTitle
|
||||||
total_chapters = manga.chapterCount ?: 0
|
total_chapters = manga.chapterCount ?: 0
|
||||||
cover_url = manga.posterImage?.original ?: ""
|
cover_url = manga.posterImage?.original ?: ""
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ class Suwayomi(id: Long) : BaseTracker(id, "Suwayomi"), EnhancedTracker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun refresh(track: Track): Track {
|
override suspend fun refresh(track: Track): Track {
|
||||||
val remoteTrack = api.getTrackSearch(track.tracking_url)
|
val remoteTrack = api.getTrackSearch(track.remote_id)
|
||||||
track.copyPersonalFrom(remoteTrack)
|
track.copyPersonalFrom(remoteTrack)
|
||||||
track.total_chapters = remoteTrack.total_chapters
|
track.total_chapters = remoteTrack.total_chapters
|
||||||
return track
|
return track
|
||||||
@@ -88,14 +88,13 @@ class Suwayomi(id: Long) : BaseTracker(id, "Suwayomi"), EnhancedTracker {
|
|||||||
|
|
||||||
override suspend fun match(manga: DomainManga): TrackSearch? =
|
override suspend fun match(manga: DomainManga): TrackSearch? =
|
||||||
try {
|
try {
|
||||||
api.getTrackSearch(manga.url)
|
api.getTrackSearch(manga.url.getMangaId())
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun isTrackFrom(track: DomainTrack, manga: DomainManga, source: Source?): Boolean = source?.let {
|
override fun isTrackFrom(track: DomainTrack, manga: DomainManga, source: Source?): Boolean =
|
||||||
accept(it)
|
track.remoteUrl == manga.url && source?.let { accept(it) } == true
|
||||||
} == true
|
|
||||||
|
|
||||||
override fun migrateTrack(track: DomainTrack, manga: DomainManga, newSource: Source): DomainTrack? =
|
override fun migrateTrack(track: DomainTrack, manga: DomainManga, newSource: Source): DomainTrack? =
|
||||||
if (accept(newSource)) {
|
if (accept(newSource)) {
|
||||||
@@ -103,4 +102,7 @@ class Suwayomi(id: Long) : BaseTracker(id, "Suwayomi"), EnhancedTracker {
|
|||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun String.getMangaId(): Long =
|
||||||
|
this.substringAfterLast('/').toLong()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,22 @@
|
|||||||
package eu.kanade.tachiyomi.data.track.suwayomi
|
package eu.kanade.tachiyomi.data.track.suwayomi
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.SharedPreferences
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.POST
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
|
||||||
import eu.kanade.tachiyomi.network.PUT
|
|
||||||
import eu.kanade.tachiyomi.network.awaitSuccess
|
import eu.kanade.tachiyomi.network.awaitSuccess
|
||||||
|
import eu.kanade.tachiyomi.network.jsonMime
|
||||||
import eu.kanade.tachiyomi.network.parseAs
|
import eu.kanade.tachiyomi.network.parseAs
|
||||||
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import okhttp3.Credentials
|
import kotlinx.serialization.json.addAll
|
||||||
import okhttp3.Dns
|
import kotlinx.serialization.json.buildJsonObject
|
||||||
import okhttp3.FormBody
|
import kotlinx.serialization.json.put
|
||||||
import okhttp3.Headers
|
import kotlinx.serialization.json.putJsonArray
|
||||||
|
import kotlinx.serialization.json.putJsonObject
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
import tachiyomi.core.common.util.lang.withIOContext
|
import tachiyomi.core.common.util.lang.withIOContext
|
||||||
|
import tachiyomi.domain.source.service.SourceManager
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
@@ -25,79 +25,147 @@ import java.security.MessageDigest
|
|||||||
|
|
||||||
class SuwayomiApi(private val trackId: Long) {
|
class SuwayomiApi(private val trackId: Long) {
|
||||||
|
|
||||||
private val network: NetworkHelper by injectLazy()
|
|
||||||
private val json: Json by injectLazy()
|
private val json: Json by injectLazy()
|
||||||
|
|
||||||
private val client: OkHttpClient =
|
private val sourceManager: SourceManager by injectLazy()
|
||||||
network.client.newBuilder()
|
private val source: HttpSource by lazy { (sourceManager.get(sourceId) as HttpSource) }
|
||||||
.dns(Dns.SYSTEM) // don't use DNS over HTTPS as it breaks IP addressing
|
private val client: OkHttpClient by lazy { source.client }
|
||||||
.build()
|
private val baseUrl: String by lazy { source.baseUrl.trimEnd('/') }
|
||||||
|
private val apiUrl: String by lazy { "$baseUrl/api/graphql" }
|
||||||
|
|
||||||
private fun headersBuilder(): Headers.Builder = Headers.Builder().apply {
|
suspend fun getTrackSearch(mangaId: Long): TrackSearch = withIOContext {
|
||||||
if (basePassword.isNotEmpty() && baseLogin.isNotEmpty()) {
|
val query = """
|
||||||
val credentials = Credentials.basic(baseLogin, basePassword)
|
|query GetManga(${'$'}mangaId: Int!) {
|
||||||
add("Authorization", credentials)
|
| manga(id: ${'$'}mangaId) {
|
||||||
|
| ...MangaFragment
|
||||||
|
| }
|
||||||
|
|}
|
||||||
|
|
|
||||||
|
|$MangaFragment
|
||||||
|
""".trimMargin()
|
||||||
|
val payload = buildJsonObject {
|
||||||
|
put("query", query)
|
||||||
|
putJsonObject("variables") {
|
||||||
|
put("mangaId", mangaId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val headers: Headers by lazy { headersBuilder().build() }
|
|
||||||
|
|
||||||
private val baseUrl by lazy { getPrefBaseUrl() }
|
|
||||||
private val baseLogin by lazy { getPrefBaseLogin() }
|
|
||||||
private val basePassword by lazy { getPrefBasePassword() }
|
|
||||||
|
|
||||||
suspend fun getTrackSearch(trackUrl: String): TrackSearch = withIOContext {
|
|
||||||
val url = try {
|
|
||||||
// test if getting api url or manga id
|
|
||||||
val mangaId = trackUrl.toLong()
|
|
||||||
"$baseUrl/api/v1/manga/$mangaId"
|
|
||||||
} catch (e: NumberFormatException) {
|
|
||||||
trackUrl
|
|
||||||
}
|
|
||||||
|
|
||||||
val manga = with(json) {
|
val manga = with(json) {
|
||||||
client.newCall(GET("$url/full", headers))
|
client.newCall(
|
||||||
|
POST(
|
||||||
|
apiUrl,
|
||||||
|
body = payload.toString().toRequestBody(jsonMime),
|
||||||
|
),
|
||||||
|
)
|
||||||
.awaitSuccess()
|
.awaitSuccess()
|
||||||
.parseAs<MangaDataClass>()
|
.parseAs<GetMangaResult>()
|
||||||
|
.data
|
||||||
|
.entry
|
||||||
}
|
}
|
||||||
|
|
||||||
TrackSearch.create(trackId).apply {
|
TrackSearch.create(trackId).apply {
|
||||||
|
remote_id = mangaId
|
||||||
title = manga.title
|
title = manga.title
|
||||||
cover_url = "$url/thumbnail"
|
cover_url = "$baseUrl/${manga.thumbnailUrl}"
|
||||||
summary = manga.description.orEmpty()
|
summary = manga.description.orEmpty()
|
||||||
tracking_url = url
|
tracking_url = "$baseUrl/manga/$mangaId"
|
||||||
total_chapters = manga.chapterCount
|
total_chapters = manga.chapters.totalCount.toLong()
|
||||||
publishing_status = manga.status
|
publishing_status = manga.status.name
|
||||||
last_chapter_read = manga.lastChapterRead?.chapterNumber ?: 0.0
|
last_chapter_read = manga.latestReadChapter?.chapterNumber ?: 0.0
|
||||||
status = when (manga.unreadCount) {
|
status = when (manga.unreadCount) {
|
||||||
manga.chapterCount -> Suwayomi.UNREAD
|
manga.chapters.totalCount -> Suwayomi.UNREAD
|
||||||
0L -> Suwayomi.COMPLETED
|
0 -> Suwayomi.COMPLETED
|
||||||
else -> Suwayomi.READING
|
else -> Suwayomi.READING
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun updateProgress(track: Track): Track {
|
suspend fun updateProgress(track: Track): Track {
|
||||||
val url = track.tracking_url
|
val mangaId = track.remote_id
|
||||||
val chapters = with(json) {
|
|
||||||
client.newCall(GET("$url/chapters", headers))
|
val chaptersQuery = """
|
||||||
.awaitSuccess()
|
|query GetMangaUnreadChapters(${'$'}mangaId: Int!) {
|
||||||
.parseAs<List<ChapterDataClass>>()
|
| chapters(condition: {mangaId: ${'$'}mangaId, isRead: false}) {
|
||||||
|
| nodes {
|
||||||
|
| id
|
||||||
|
| chapterNumber
|
||||||
|
| }
|
||||||
|
| }
|
||||||
|
|}
|
||||||
|
""".trimMargin()
|
||||||
|
val chaptersPayload = buildJsonObject {
|
||||||
|
put("query", chaptersQuery)
|
||||||
|
putJsonObject("variables") {
|
||||||
|
put("mangaId", mangaId)
|
||||||
}
|
}
|
||||||
val lastChapterIndex = chapters.first { it.chapterNumber == track.last_chapter_read }.index
|
}
|
||||||
|
val chaptersToMark = with(json) {
|
||||||
client.newCall(
|
client.newCall(
|
||||||
PUT(
|
POST(
|
||||||
"$url/chapter/$lastChapterIndex",
|
apiUrl,
|
||||||
headers,
|
body = chaptersPayload.toString().toRequestBody(jsonMime),
|
||||||
FormBody.Builder(Charset.forName("utf8"))
|
|
||||||
.add("markPrevRead", "true")
|
|
||||||
.add("read", "true")
|
|
||||||
.build(),
|
|
||||||
),
|
),
|
||||||
).awaitSuccess()
|
)
|
||||||
|
.awaitSuccess()
|
||||||
|
.parseAs<GetMangaUnreadChaptersResult>()
|
||||||
|
.data
|
||||||
|
.entry
|
||||||
|
.nodes
|
||||||
|
.mapNotNull { n -> n.id.takeIf { n.chapterNumber <= track.last_chapter_read } }
|
||||||
|
}
|
||||||
|
|
||||||
return getTrackSearch(track.tracking_url)
|
val markQuery = """
|
||||||
|
|mutation MarkChaptersRead(${'$'}chapters: [Int!]!) {
|
||||||
|
| updateChapters(input: {ids: ${'$'}chapters, patch: {isRead: true}}) {
|
||||||
|
| chapters {
|
||||||
|
| id
|
||||||
|
| }
|
||||||
|
| }
|
||||||
|
|}
|
||||||
|
""".trimMargin()
|
||||||
|
val markPayload = buildJsonObject {
|
||||||
|
put("query", markQuery)
|
||||||
|
putJsonObject("variables") {
|
||||||
|
putJsonArray("chapters") {
|
||||||
|
addAll(chaptersToMark)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
with(json) {
|
||||||
|
client.newCall(
|
||||||
|
POST(
|
||||||
|
apiUrl,
|
||||||
|
body = markPayload.toString().toRequestBody(jsonMime),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.awaitSuccess()
|
||||||
|
}
|
||||||
|
|
||||||
|
val trackQuery = """
|
||||||
|
|mutation TrackManga(${'$'}mangaId: Int!) {
|
||||||
|
| trackProgress(input: {mangaId: ${'$'}mangaId}) {
|
||||||
|
| trackRecords {
|
||||||
|
| lastChapterRead
|
||||||
|
| }
|
||||||
|
| }
|
||||||
|
|}
|
||||||
|
""".trimMargin()
|
||||||
|
val trackPayload = buildJsonObject {
|
||||||
|
put("query", trackQuery)
|
||||||
|
putJsonObject("variables") {
|
||||||
|
put("mangaId", mangaId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
with(json) {
|
||||||
|
client.newCall(
|
||||||
|
POST(
|
||||||
|
apiUrl,
|
||||||
|
body = trackPayload.toString().toRequestBody(jsonMime),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.awaitSuccess()
|
||||||
|
}
|
||||||
|
|
||||||
|
return getTrackSearch(track.remote_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val sourceId by lazy {
|
private val sourceId by lazy {
|
||||||
@@ -106,18 +174,35 @@ class SuwayomiApi(private val trackId: Long) {
|
|||||||
(0..7).map { bytes[it].toLong() and 0xff shl 8 * (7 - it) }.reduce(Long::or) and Long.MAX_VALUE
|
(0..7).map { bytes[it].toLong() and 0xff shl 8 * (7 - it) }.reduce(Long::or) and Long.MAX_VALUE
|
||||||
}
|
}
|
||||||
|
|
||||||
private val preferences: SharedPreferences by lazy {
|
companion object {
|
||||||
Injekt.get<Application>().getSharedPreferences("source_$sourceId", Context.MODE_PRIVATE)
|
private val MangaFragment = """
|
||||||
|
|fragment MangaFragment on MangaType {
|
||||||
|
| artist
|
||||||
|
| author
|
||||||
|
| description
|
||||||
|
| id
|
||||||
|
| status
|
||||||
|
| thumbnailUrl
|
||||||
|
| title
|
||||||
|
| url
|
||||||
|
| genre
|
||||||
|
| inLibraryAt
|
||||||
|
| chapters {
|
||||||
|
| totalCount
|
||||||
|
| }
|
||||||
|
| latestUploadedChapter {
|
||||||
|
| uploadDate
|
||||||
|
| }
|
||||||
|
| latestFetchedChapter {
|
||||||
|
| fetchedAt
|
||||||
|
| }
|
||||||
|
| latestReadChapter {
|
||||||
|
| lastReadAt
|
||||||
|
| chapterNumber
|
||||||
|
| }
|
||||||
|
| unreadCount
|
||||||
|
| downloadCount
|
||||||
|
|}
|
||||||
|
""".trimMargin()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getPrefBaseUrl(): String = preferences.getString(ADDRESS_TITLE, ADDRESS_DEFAULT)!!
|
|
||||||
private fun getPrefBaseLogin(): String = preferences.getString(LOGIN_TITLE, LOGIN_DEFAULT)!!
|
|
||||||
private fun getPrefBasePassword(): String = preferences.getString(PASSWORD_TITLE, PASSWORD_DEFAULT)!!
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private const val ADDRESS_TITLE = "Server URL Address"
|
|
||||||
private const val ADDRESS_DEFAULT = ""
|
|
||||||
private const val LOGIN_TITLE = "Login (Basic Auth)"
|
|
||||||
private const val LOGIN_DEFAULT = ""
|
|
||||||
private const val PASSWORD_TITLE = "Password (Basic Auth)"
|
|
||||||
private const val PASSWORD_DEFAULT = ""
|
|
||||||
|
|||||||
@@ -1,100 +1,90 @@
|
|||||||
package eu.kanade.tachiyomi.data.track.suwayomi
|
package eu.kanade.tachiyomi.data.track.suwayomi
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
public enum class MangaStatus(
|
||||||
|
public val rawValue: String,
|
||||||
|
) {
|
||||||
|
UNKNOWN("UNKNOWN"),
|
||||||
|
ONGOING("ONGOING"),
|
||||||
|
COMPLETED("COMPLETED"),
|
||||||
|
LICENSED("LICENSED"),
|
||||||
|
PUBLISHING_FINISHED("PUBLISHING_FINISHED"),
|
||||||
|
CANCELLED("CANCELLED"),
|
||||||
|
ON_HIATUS("ON_HIATUS"),
|
||||||
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class SourceDataClass(
|
public data class MangaFragment(
|
||||||
val id: String,
|
public val artist: String?,
|
||||||
val name: String,
|
public val author: String?,
|
||||||
val lang: String,
|
public val description: String?,
|
||||||
val iconUrl: String,
|
public val id: Int,
|
||||||
|
public val status: MangaStatus,
|
||||||
|
public val thumbnailUrl: String?,
|
||||||
|
public val title: String,
|
||||||
|
public val url: String,
|
||||||
|
public val genre: List<String>,
|
||||||
|
public val inLibraryAt: Long,
|
||||||
|
public val chapters: Chapters,
|
||||||
|
public val latestUploadedChapter: LatestUploadedChapter?,
|
||||||
|
public val latestFetchedChapter: LatestFetchedChapter?,
|
||||||
|
public val latestReadChapter: LatestReadChapter?,
|
||||||
|
public val unreadCount: Int,
|
||||||
|
public val downloadCount: Int,
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
public data class Chapters(
|
||||||
|
public val totalCount: Int,
|
||||||
|
)
|
||||||
|
|
||||||
/** The Source provides a latest listing */
|
@Serializable
|
||||||
val supportsLatest: Boolean,
|
public data class LatestUploadedChapter(
|
||||||
|
public val uploadDate: Long,
|
||||||
|
)
|
||||||
|
|
||||||
/** The Source implements [ConfigurableSource] */
|
@Serializable
|
||||||
val isConfigurable: Boolean,
|
public data class LatestFetchedChapter(
|
||||||
|
public val fetchedAt: Long,
|
||||||
|
)
|
||||||
|
|
||||||
/** The Source class has a @Nsfw annotation */
|
@Serializable
|
||||||
val isNsfw: Boolean,
|
public data class LatestReadChapter(
|
||||||
|
public val lastReadAt: Long,
|
||||||
|
public val chapterNumber: Double,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/** A nicer version of [name] */
|
@Serializable
|
||||||
val displayName: String,
|
public data class GetMangaResult(
|
||||||
|
public val data: GetMangaData,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class MangaDataClass(
|
public data class GetMangaData(
|
||||||
val id: Int,
|
@SerialName("manga")
|
||||||
val sourceId: String,
|
public val entry: MangaFragment,
|
||||||
|
|
||||||
val url: String,
|
|
||||||
val title: String,
|
|
||||||
val thumbnailUrl: String?,
|
|
||||||
|
|
||||||
val initialized: Boolean,
|
|
||||||
|
|
||||||
val artist: String?,
|
|
||||||
val author: String?,
|
|
||||||
val description: String?,
|
|
||||||
val genre: List<String>,
|
|
||||||
val status: String,
|
|
||||||
val inLibrary: Boolean,
|
|
||||||
val inLibraryAt: Long,
|
|
||||||
val source: SourceDataClass?,
|
|
||||||
|
|
||||||
val meta: Map<String, String>,
|
|
||||||
|
|
||||||
val realUrl: String?,
|
|
||||||
val lastFetchedAt: Long?,
|
|
||||||
val chaptersLastFetchedAt: Long?,
|
|
||||||
|
|
||||||
val freshData: Boolean,
|
|
||||||
val unreadCount: Long?,
|
|
||||||
val downloadCount: Long?,
|
|
||||||
val chapterCount: Long, // actually is nullable server side, but should be set at this time
|
|
||||||
val lastChapterRead: ChapterDataClass?,
|
|
||||||
|
|
||||||
val age: Long?,
|
|
||||||
val chaptersAge: Long?,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class ChapterDataClass(
|
public data class GetMangaUnreadChaptersEntry(
|
||||||
val id: Int,
|
public val nodes: List<GetMangaUnreadChaptersNode>,
|
||||||
val url: String,
|
)
|
||||||
val name: String,
|
|
||||||
val uploadDate: Long,
|
@Serializable
|
||||||
val chapterNumber: Double,
|
public data class GetMangaUnreadChaptersNode(
|
||||||
val scanlator: String?,
|
public val id: Int,
|
||||||
val mangaId: Int,
|
public val chapterNumber: Double,
|
||||||
|
)
|
||||||
/** chapter is read */
|
|
||||||
val read: Boolean,
|
@Serializable
|
||||||
|
public data class GetMangaUnreadChaptersResult(
|
||||||
/** chapter is bookmarked */
|
public val data: GetMangaUnreadChaptersData,
|
||||||
val bookmarked: Boolean,
|
)
|
||||||
|
|
||||||
/** last read page, zero means not read/no data */
|
@Serializable
|
||||||
val lastPageRead: Int,
|
public data class GetMangaUnreadChaptersData(
|
||||||
|
@SerialName("chapters")
|
||||||
/** last read page, zero means not read/no data */
|
public val entry: GetMangaUnreadChaptersEntry,
|
||||||
val lastReadAt: Long,
|
|
||||||
|
|
||||||
/** this chapter's index, starts with 1 */
|
|
||||||
val index: Int,
|
|
||||||
|
|
||||||
/** the date we fist saw this chapter*/
|
|
||||||
val fetchedAt: Long,
|
|
||||||
|
|
||||||
/** is chapter downloaded */
|
|
||||||
val downloaded: Boolean,
|
|
||||||
|
|
||||||
/** used to construct pages in the front-end */
|
|
||||||
val pageCount: Int,
|
|
||||||
|
|
||||||
/** total chapter count, used to calculate if there's a next and prev chapter */
|
|
||||||
val chapterCount: Int?,
|
|
||||||
|
|
||||||
/** used to store client specific values */
|
|
||||||
val meta: Map<String, String>,
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ class ExtensionManager(
|
|||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logcat(LogPriority.ERROR, e)
|
logcat(LogPriority.ERROR, e)
|
||||||
withUIContext { context.toast(MR.strings.extension_api_error) }
|
withUIContext { context.toast(MR.strings.extension_api_error) }
|
||||||
emptyList()
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
enableAdditionalSubLanguages(extensions)
|
enableAdditionalSubLanguages(extensions)
|
||||||
|
|||||||
@@ -12,16 +12,18 @@ import eu.kanade.tachiyomi.extension.ExtensionManager
|
|||||||
import eu.kanade.tachiyomi.extension.model.InstallStep
|
import eu.kanade.tachiyomi.extension.model.InstallStep
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.util.Collections
|
import java.util.Collections
|
||||||
import java.util.concurrent.atomic.AtomicReference
|
import kotlin.concurrent.atomics.AtomicReference
|
||||||
|
import kotlin.concurrent.atomics.ExperimentalAtomicApi
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base implementation class for extension installer. To be used inside a foreground [Service].
|
* Base implementation class for extension installer. To be used inside a foreground [Service].
|
||||||
*/
|
*/
|
||||||
|
@OptIn(ExperimentalAtomicApi::class)
|
||||||
abstract class Installer(private val service: Service) {
|
abstract class Installer(private val service: Service) {
|
||||||
|
|
||||||
private val extensionManager: ExtensionManager by injectLazy()
|
private val extensionManager: ExtensionManager by injectLazy()
|
||||||
|
|
||||||
private var waitingInstall = AtomicReference<Entry>(null)
|
private var waitingInstall = AtomicReference<Entry?>(null)
|
||||||
private val queue = Collections.synchronizedList(mutableListOf<Entry>())
|
private val queue = Collections.synchronizedList(mutableListOf<Entry>())
|
||||||
|
|
||||||
private val cancelReceiver = object : BroadcastReceiver() {
|
private val cancelReceiver = object : BroadcastReceiver() {
|
||||||
@@ -79,7 +81,7 @@ abstract class Installer(private val service: Service) {
|
|||||||
* @see waitingInstall
|
* @see waitingInstall
|
||||||
*/
|
*/
|
||||||
fun continueQueue(resultStep: InstallStep) {
|
fun continueQueue(resultStep: InstallStep) {
|
||||||
val completedEntry = waitingInstall.getAndSet(null)
|
val completedEntry = waitingInstall.exchange(null)
|
||||||
if (completedEntry != null) {
|
if (completedEntry != null) {
|
||||||
extensionManager.updateInstallStep(completedEntry.downloadId, resultStep)
|
extensionManager.updateInstallStep(completedEntry.downloadId, resultStep)
|
||||||
checkQueue()
|
checkQueue()
|
||||||
@@ -115,10 +117,10 @@ abstract class Installer(private val service: Service) {
|
|||||||
LocalBroadcastManager.getInstance(service).unregisterReceiver(cancelReceiver)
|
LocalBroadcastManager.getInstance(service).unregisterReceiver(cancelReceiver)
|
||||||
queue.forEach { extensionManager.updateInstallStep(it.downloadId, InstallStep.Error) }
|
queue.forEach { extensionManager.updateInstallStep(it.downloadId, InstallStep.Error) }
|
||||||
queue.clear()
|
queue.clear()
|
||||||
waitingInstall.set(null)
|
waitingInstall.store(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun getActiveEntry(): Entry? = waitingInstall.get()
|
protected fun getActiveEntry(): Entry? = waitingInstall.load()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cancels queue for the provided download ID if exists.
|
* Cancels queue for the provided download ID if exists.
|
||||||
@@ -126,13 +128,13 @@ abstract class Installer(private val service: Service) {
|
|||||||
* @param downloadId Download ID as known by [ExtensionManager]
|
* @param downloadId Download ID as known by [ExtensionManager]
|
||||||
*/
|
*/
|
||||||
private fun cancelQueue(downloadId: Long) {
|
private fun cancelQueue(downloadId: Long) {
|
||||||
val waitingInstall = this.waitingInstall.get()
|
val waitingInstall = this.waitingInstall.load()
|
||||||
val toCancel = queue.find { it.downloadId == downloadId } ?: waitingInstall ?: return
|
val toCancel = queue.find { it.downloadId == downloadId } ?: waitingInstall ?: return
|
||||||
if (cancelEntry(toCancel)) {
|
if (cancelEntry(toCancel)) {
|
||||||
queue.remove(toCancel)
|
queue.remove(toCancel)
|
||||||
if (waitingInstall == toCancel) {
|
if (waitingInstall == toCancel) {
|
||||||
// Currently processing removed entry, continue queue
|
// Currently processing removed entry, continue queue
|
||||||
this.waitingInstall.set(null)
|
this.waitingInstall.store(null)
|
||||||
checkQueue()
|
checkQueue()
|
||||||
}
|
}
|
||||||
extensionManager.updateInstallStep(downloadId, InstallStep.Idle)
|
extensionManager.updateInstallStep(downloadId, InstallStep.Idle)
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user