mirror of
https://github.com/null2264/yokai.git
synced 2025-06-21 02:34:39 +00:00
Compare commits
310 commits
Author | SHA1 | Date | |
---|---|---|---|
17879ddc5a | |||
7f83c117be | |||
abbe606473 | |||
7ac42d5545 | |||
|
f90e2a1425 | ||
f604e4e256 | |||
a04ea9f5ea | |||
43d4d5404d | |||
|
54df9436b8 | ||
|
cd5cdbe746 | ||
|
41662979fe | ||
|
f68e9df74d | ||
|
a77d315922 | ||
|
4e2c4aef8a | ||
|
d80b53ba78 | ||
|
a1f6eb6524 | ||
|
9f256bb8c6 | ||
|
1d49d65961 | ||
|
9ccdd36c46 | ||
|
48938f02dd | ||
|
63435b933a | ||
|
ef49bf3321 | ||
|
c05ba1a8fb | ||
|
764d52a729 | ||
|
af2be0d2d0 | ||
|
733fcbba4a | ||
|
b6e1cabc59 | ||
|
3867aabff1 | ||
|
ca6bb95b84 | ||
|
4b7564e410 | ||
|
8d3cfffa66 | ||
|
97339689c6 | ||
|
9453c3e808 | ||
|
6f03935c17 | ||
|
f1597bd95c | ||
|
903a37e390 | ||
|
1655540a16 | ||
|
6a7b386127 | ||
|
75191dde05 | ||
|
db0af71901 | ||
|
89c5e997cc | ||
e22559b2df | |||
|
370bb62ef9 | ||
|
18528fbd92 | ||
7964ac87c6 | |||
850151720b | |||
d3050d5799 | |||
c7d2ff0970 | |||
524c00fd44 | |||
93f819c236 | |||
1c73b925b1 | |||
|
ea179979b1 | ||
71c26b77fc | |||
43a5e8edd8 | |||
|
33d7c3cd2b | ||
0e0e865cb9 | |||
|
2b2e0491e8 | ||
f035454150 | |||
96f88d5e90 | |||
f362b0bda0 | |||
f74662c0f3 | |||
65639391b7 | |||
|
94c314559b | ||
66241774dc | |||
ea634a5ce3 | |||
271e440014 | |||
|
8be33e0f81 | ||
f13f98f19a | |||
7a08ca294a | |||
4faa641739 | |||
|
ebd891fa75 | ||
|
d46f5fb73e | ||
|
d6c5a9a7c2 | ||
|
2208a81013 | ||
0bf55a8ca0 | |||
4dd8aece0c | |||
ece849b008 | |||
d2ddf7dfb0 | |||
8b53e5ad10 | |||
63cdf247b4 | |||
9ed12ef07c | |||
86b01a297f | |||
d6ffbe15ee | |||
915ce20904 | |||
9e5d13f261 | |||
33fa77d527 | |||
baaa841278 | |||
453ea32bc9 | |||
f37e657a9b | |||
258708b038 | |||
c6da3325b3 | |||
c9a90f6847 | |||
9cf1fbb118 | |||
f01ace94be | |||
ad22250265 | |||
48c2ad9b33 | |||
7d9c0faf86 | |||
6614bd3ed8 | |||
c6c40ffb71 | |||
d0d322fd67 | |||
d655c3e09a | |||
6a680facd5 | |||
0565fc2665 | |||
568859891a | |||
7fc95e3029 | |||
eebc3dc822 | |||
968639a59b | |||
|
cae0332ef9 | ||
e415fd4ef2 | |||
a3672be728 | |||
e06b28a60e | |||
eba5aa1d2e | |||
a554c079fb | |||
49b10c1b4f | |||
1a16d84e61 | |||
fc87410d46 | |||
84d2924a82 | |||
03e1953c9f | |||
|
02c2b370b4 | ||
fa84ce8fe8 | |||
42dd857d94 | |||
2cf2fcfc4f | |||
b4377a4609 | |||
54a3059730 | |||
672d364f43 | |||
b19480de1a | |||
3c00a249c3 | |||
|
012407aede | ||
1461f048dd | |||
85af94d810 | |||
d02f1bdd11 | |||
1b92ae2e5f | |||
ac0d2e9fc0 | |||
8a28d1d484 | |||
3399d6a326 | |||
6747795690 | |||
e554513392 | |||
f7e5abba59 | |||
a7874f2f29 | |||
2f2ccac8e7 | |||
dd6a2f377a | |||
f8807f81b1 | |||
7ee9c7a746 | |||
67c4500cce | |||
cd4079aa4b | |||
c6a86d773b | |||
cab40214d2 | |||
37f1f0e330 | |||
310e90beb5 | |||
9bb869111d | |||
8a9d8166af | |||
71dcb2ab85 | |||
60ef9482c8 | |||
1cc8abb599 | |||
96348dbf7d | |||
23d4fb1fdd | |||
f78d4e9e6a | |||
120d2cfb96 | |||
dec1a70091 | |||
448c93365a | |||
55fad67223 | |||
c09c4045e2 | |||
b201e410a3 | |||
915debd41b | |||
6d2ae386a2 | |||
a853deae4a | |||
f9bb2b96cb | |||
031e30e227 | |||
0049653355 | |||
677d96eed5 | |||
640feb69ac | |||
f3cac7cac8 | |||
99bec41056 | |||
afb7e79ea4 | |||
76af51a319 | |||
5824ac81c2 | |||
b1665eaedf | |||
2b953a53d8 | |||
37f47ae2f5 | |||
3fa475c1cf | |||
|
bcf21858ee | ||
90f5dfc55a | |||
|
365d259e94 | ||
09bd1a9f78 | |||
f8e7002a9b | |||
4caefff11d | |||
07f9e921f1 | |||
|
4256e86e97 | ||
fdb69a1c7d | |||
e65497baef | |||
acafe931f1 | |||
bafd9e54aa | |||
a6e5b41d7d | |||
629f1891f6 | |||
bfbbd1b4f3 | |||
33a84f7e39 | |||
fe666b614f | |||
f240fe0dd4 | |||
3787845893 | |||
3606f67dba | |||
9e5262140e | |||
c73d0f843f | |||
b974eff320 | |||
8ac8187977 | |||
778bd05e21 | |||
3d2e2b2774 | |||
c1cb7a2066 | |||
2299aaac63 | |||
f985ad6daa | |||
12002f62cd | |||
eea87eede4 | |||
2466d3a493 | |||
bf8eccc186 | |||
1571678ddb | |||
cd8ff6f898 | |||
985ac6d7a8 | |||
17eec5f6aa | |||
33332110f1 | |||
659ba89218 | |||
02296985d6 | |||
b3e69bdb28 | |||
6c40fe92be | |||
18c5a68981 | |||
cba97eb94d | |||
0d205f4a6d | |||
fcaff92db1 | |||
5fad2c0154 | |||
bbfa8c8bc4 | |||
8020f6591c | |||
a404eb2c83 | |||
8c9208c3b6 | |||
2dfc1e3451 | |||
4ffb0ad8ee | |||
4a0f578211 | |||
60fe907cc0 | |||
f81be429df | |||
f8d74a6b2f | |||
2ef1195a90 | |||
50cad86c0c | |||
8ec29bf755 | |||
3651c2a853 | |||
2461b93af5 | |||
301acb9f4d | |||
4fc18b4913 | |||
e5b8ed9e9d | |||
263603616e | |||
6c1d8d5011 | |||
247ed3bca7 | |||
c7b6e8ee00 | |||
d99476f9bf | |||
834958a819 | |||
1bbfd97b30 | |||
59a8bfc7aa | |||
93e5effaaf | |||
0f54d5e59d | |||
316bc87a5c | |||
82b73bce76 | |||
71a9e2493b | |||
abcf06b921 | |||
e604c951ed | |||
62e26d1f38 | |||
ee2acf8b98 | |||
8c5b54df5f | |||
|
00aa93d189 | ||
bfee1de3b1 | |||
|
fbe9760616 | ||
|
86f5e743e1 | ||
8f45148a9e | |||
|
09621111bf | ||
5fe72d4cb5 | |||
5b637fae8f | |||
336579bd35 | |||
d61052485f | |||
|
16316d810b | ||
37535d3bcf | |||
39775ea308 | |||
1c996f9a59 | |||
ea04968581 | |||
365875590f | |||
1bc107f26b | |||
eeb572740a | |||
dbf5a7efcd | |||
b4e3dcfdda | |||
8e7f5d8897 | |||
87c13b44ab | |||
8c8b2f9634 | |||
88959d956f | |||
53f8a37c8e | |||
05b20e00e0 | |||
158049d4f4 | |||
119b1c64b2 | |||
1cb635999e | |||
160a7109da | |||
618109c80e | |||
|
9918c407c8 | ||
|
d780d6ceb1 | ||
|
ee52e6ecf7 | ||
|
4cdcf62351 | ||
|
1504d94f52 | ||
|
e1bf13f1d9 | ||
|
c6f6718d30 | ||
|
41319660f6 | ||
6935ae545c | |||
fe59d7f4ec | |||
e45baf6ab4 | |||
d3c98fb897 | |||
22978ab8bf | |||
07ed81454f | |||
6c111a1247 | |||
c357f6f658 |
408 changed files with 9505 additions and 5783 deletions
|
@ -1,13 +1,28 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_style=space
|
||||
insert_final_newline=true
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.{json,json5}]
|
||||
indent_size=2
|
||||
[*.xml]
|
||||
indent_size = 4
|
||||
|
||||
# noinspection EditorConfigKeyCorrectness
|
||||
[*.{kt,kts}]
|
||||
indent_size=4
|
||||
ij_kotlin_allow_trailing_comma=true
|
||||
ij_kotlin_allow_trailing_comma_on_call_site=true
|
||||
indent_size = 4
|
||||
max_line_length = 120
|
||||
|
||||
ij_kotlin_allow_trailing_comma = true
|
||||
ij_kotlin_allow_trailing_comma_on_call_site = true
|
||||
ij_kotlin_name_count_to_use_star_import = 2147483647
|
||||
ij_kotlin_name_count_to_use_star_import_for_members = 2147483647
|
||||
|
||||
ktlint_code_style = intellij_idea
|
||||
ktlint_function_naming_ignore_when_annotated_with = Composable
|
||||
ktlint_standard_class-signature = disabled
|
||||
ktlint_standard_discouraged-comment-location = disabled
|
||||
ktlint_standard_function-expression-body = disabled
|
||||
ktlint_standard_function-signature = disabled
|
||||
|
|
17
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
17
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
|
@ -35,9 +35,24 @@ body:
|
|||
required: true
|
||||
- label: If this is an issue with an extension, or a request for an extension, I should be contacting the extensions repository's maintainer/support for help.
|
||||
required: true
|
||||
- label: I have updated the app to version **[1.8.5.13](https://github.com/null2264/yokai/releases/latest)**.
|
||||
- label: I have updated the app to version **[1.9.7.3](https://github.com/null2264/yokai/releases/latest)**.
|
||||
required: true
|
||||
- label: I have checked through the app settings for my feature.
|
||||
required: true
|
||||
- label: I will fill out all of the requested information in this form.
|
||||
required: true
|
||||
|
||||
- type: "textarea"
|
||||
id: "prioritisation"
|
||||
attributes:
|
||||
label: "Is this issue important to you?"
|
||||
description: |
|
||||
**Please do not modify this text area!**
|
||||
|
||||
This template let users to vote with a :+1: reaction if they find it important.
|
||||
This is not a guarantee that highly-requested issues will be fixed first, but it helps us to figure out what's important to users. Please react on other users' issues if you find them important.
|
||||
value: |
|
||||
Add a :+1: [reaction] to [issues you find important].
|
||||
|
||||
[reaction]: https://github.blog/2016-03-10-add-reactions-to-pull-requests-issues-and-comments/
|
||||
[issues you find important]: https://github.com/null2264/yokai/issues?q=is%3Aissue+is%3Aopen+sort%3Areactions-%2B1-desc
|
||||
|
|
23
.github/ISSUE_TEMPLATE/issue_report.yml
vendored
23
.github/ISSUE_TEMPLATE/issue_report.yml
vendored
|
@ -94,15 +94,30 @@ body:
|
|||
required: true
|
||||
- label: I have written a short but informative title.
|
||||
required: true
|
||||
- label: If this is an issue with an extension, or a request for an extension, I should be contacting the extensions repository's maintainer/support for help.
|
||||
- label: If this is an issue with an extension, or a request for an extension, I should be contacting the extensions repository's maintainer/support for help ([Browse → Extensions → Find the extension → Settings → Tap the `[<>]` icon](https://cdn.aap.my.id/extension-repo-link.png), it *should* redirect you to the maintainer).
|
||||
required: true
|
||||
- label: I am reporting an issue exclusive to this fork. I have also checked that is not an issue on the [main version of Mihon](https://github.com/mihonapp/mihon)
|
||||
- label: I am reporting an issue exclusive to this fork. I have also checked that is not an issue on the [main version of Mihon](https://github.com/mihonapp/mihon).
|
||||
required: true
|
||||
- label: I have tried the [troubleshooting guide](https://mihon.app/help/).
|
||||
- label: I have tried the [troubleshooting guide](https://mihon.app/docs/guides/troubleshooting/).
|
||||
required: true
|
||||
- label: I have updated the app to version **[1.8.5.13](https://github.com/null2264/yokai/releases/latest)**.
|
||||
- label: I have updated the app to version **[1.9.7.3](https://github.com/null2264/yokai/releases/latest)**.
|
||||
required: true
|
||||
- label: I have updated all installed extensions.
|
||||
required: true
|
||||
- label: I have filled out all of the requested information in this form.
|
||||
required: true
|
||||
|
||||
- type: "textarea"
|
||||
id: "prioritisation"
|
||||
attributes:
|
||||
label: "Is this issue important to you?"
|
||||
description: |
|
||||
**Please do not modify this text area!**
|
||||
|
||||
This template let users to vote with a :+1: reaction if they find it important.
|
||||
This is not a guarantee that highly-requested issues will be fixed first, but it helps us to figure out what's important to users. Please react on other users' issues if you find them important.
|
||||
value: |
|
||||
Add a :+1: [reaction] to [issues you find important].
|
||||
|
||||
[reaction]: https://github.blog/2016-03-10-add-reactions-to-pull-requests-issues-and-comments/
|
||||
[issues you find important]: https://github.com/null2264/yokai/issues?q=is%3Aissue+is%3Aopen+sort%3Areactions-%2B1-desc
|
||||
|
|
11
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
11
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
|
@ -0,0 +1,11 @@
|
|||
|
||||
<!--
|
||||
^ Please summarise the changes you have made here ^
|
||||
-->
|
||||
|
||||
---
|
||||
|
||||
Add a :+1: [reaction] to [pull requests you find important].
|
||||
|
||||
[reaction]: https://github.blog/2016-03-10-add-reactions-to-pull-requests-issues-and-comments/
|
||||
[pull requests you find important]: https://github.com/null2264/yokai/pulls?q=is%3Aopen+sort%3Areactions-%2B1-desc
|
23
.github/workflows/build_check.yml
vendored
23
.github/workflows/build_check.yml
vendored
|
@ -4,7 +4,15 @@ concurrency:
|
|||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
on: [pull_request]
|
||||
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'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
@ -19,21 +27,24 @@ jobs:
|
|||
|
||||
- name: Setup Android SDK
|
||||
run: |
|
||||
${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager "build-tools;29.0.3"
|
||||
${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager "build-tools;35.0.0"
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: null2264/actions/gradle-setup@a4d662095a2f2af1ed24f1228eb6e55b0f9f1f29
|
||||
uses: null2264/actions/gradle-java-setup@363cb9cf3d66bd9c72ed6860142c6b2c121d7e94
|
||||
with:
|
||||
java: 17
|
||||
distro: adopt
|
||||
distro: temurin
|
||||
|
||||
- name: Copy CI gradle.properties
|
||||
run: |
|
||||
mkdir -p ~/.gradle
|
||||
cp .github/runner-files/ci-gradle.properties ~/.gradle/gradle.properties
|
||||
|
||||
- name: Build and run tests
|
||||
run: ./gradlew assembleStandardRelease testReleaseUnitTest testStandardReleaseUnitTest
|
||||
- name: Build the app
|
||||
run: ./gradlew assembleStandardRelease
|
||||
|
||||
- name: Run unit tests
|
||||
run: ./gradlew testReleaseUnitTest testStandardReleaseUnitTest
|
||||
|
||||
- name: Publish test report
|
||||
uses: mikepenz/action-junit-report@v5
|
||||
|
|
38
.github/workflows/build_push.yml
vendored
38
.github/workflows/build_push.yml
vendored
|
@ -40,13 +40,13 @@ jobs:
|
|||
|
||||
- name: Setup Android SDK
|
||||
run: |
|
||||
${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager "build-tools;34.0.0"
|
||||
${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager "build-tools;35.0.0"
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: null2264/actions/gradle-setup@a4d662095a2f2af1ed24f1228eb6e55b0f9f1f29
|
||||
uses: null2264/actions/gradle-java-setup@363cb9cf3d66bd9c72ed6860142c6b2c121d7e94
|
||||
with:
|
||||
java: 17
|
||||
distro: adopt
|
||||
distro: temurin
|
||||
|
||||
- name: Setup CHANGELOG parser
|
||||
uses: taiki-e/install-action@parse-changelog
|
||||
|
@ -69,7 +69,7 @@ jobs:
|
|||
VERSION_FORMAT='^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(\.(0|[1-9][0-9]*))?(-[0-9A-Za-z\.-]+)?(\+[0-9A-Za-z\.-]+)?$|^Unreleased$'
|
||||
{
|
||||
echo "CHANGELOG<<END_OF_FILE"
|
||||
parse-changelog CHANGELOG.md ${{ github.event.inputs.version == '' && 'Unreleased' || github.event.inputs.version }} --version-format $VERSION_FORMAT || parse-changelog CHANGELOG.md Unreleased --version-format $VERSION_FORMAT || echo ""
|
||||
parse-changelog CHANGELOG.md ${{ github.event.inputs.version == '' && 'Unreleased' || github.event.inputs.version }} --version-format $VERSION_FORMAT || parse-changelog CHANGELOG.md Unreleased --version-format $VERSION_FORMAT || echo "No documented changes so far..."
|
||||
echo ""
|
||||
echo "END_OF_FILE"
|
||||
} >> "$GITHUB_OUTPUT" 2> /dev/null
|
||||
|
@ -80,6 +80,7 @@ jobs:
|
|||
run: |
|
||||
set -x
|
||||
echo "VERSION_TAG=v${{github.event.inputs.version}}" >> $GITHUB_ENV
|
||||
echo "BUILD_TYPE=StandardRelease" >> $GITHUB_ENV
|
||||
|
||||
# BETA
|
||||
- name: Prepare beta build
|
||||
|
@ -88,9 +89,14 @@ jobs:
|
|||
set -x
|
||||
|
||||
BETA_COUNT=$(git tag -l --sort=refname "v${{github.event.inputs.version}}-b*" | tail -n1 | sed "s/^\S*-b//g")
|
||||
[ "$BETA_COUNT" = "" ] && BETA_COUNT="1" || BETA_COUNT=$((BETA_COUNT+1))
|
||||
if [ -z "$BETA_COUNT" ]; then
|
||||
BETA_COUNT="1"
|
||||
else
|
||||
BETA_COUNT=$((BETA_COUNT+1))
|
||||
fi
|
||||
|
||||
echo "VERSION_TAG=v${{github.event.inputs.version}}-b${BETA_COUNT}" >> $GITHUB_ENV
|
||||
echo "BUILD_TYPE=StandardBeta" >> $GITHUB_ENV
|
||||
|
||||
# NIGHTLY
|
||||
- name: Prepare nightly build
|
||||
|
@ -98,23 +104,17 @@ jobs:
|
|||
run: |
|
||||
set -x
|
||||
echo "VERSION_TAG=r$(git rev-list --count HEAD)" >> $GITHUB_ENV
|
||||
echo "BUILD_TYPE=StandardNightly" >> $GITHUB_ENV
|
||||
echo "COMMIT_HASH=$(git rev-parse HEAD)" >> $GITHUB_ENV
|
||||
echo "COMMIT_SHORT_HASH=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
|
||||
|
||||
# PROD
|
||||
- name: Build release build and run tests
|
||||
if: startsWith(env.VERSION_TAG, 'v') && github.event.inputs.beta != 'true'
|
||||
run: ./gradlew assembleStandardRelease testReleaseUnitTest testStandardReleaseUnitTest
|
||||
- name: Build the app
|
||||
if: startsWith(env.BUILD_TYPE, 'Standard')
|
||||
run: ./gradlew assemble${{ env.BUILD_TYPE }}
|
||||
|
||||
# BETA
|
||||
- name: Build beta build and run tests
|
||||
if: startsWith(env.VERSION_TAG, 'v') && github.event.inputs.beta == 'true'
|
||||
run: ./gradlew assembleStandardBeta testReleaseUnitTest testStandardBetaUnitTest
|
||||
|
||||
# NIGHTLY
|
||||
- name: Build nightly build and run tests
|
||||
if: startsWith(env.VERSION_TAG, 'r')
|
||||
run: ./gradlew assembleStandardNightly testReleaseUnitTest testStandardNightlyUnitTest
|
||||
- name: Run unit tests
|
||||
if: startsWith(env.BUILD_TYPE, 'Standard')
|
||||
run: ./gradlew testReleaseUnitTest test${{ env.BUILD_TYPE }}UnitTest
|
||||
|
||||
- name: Upload R8 APK to artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
|
@ -154,7 +154,7 @@ jobs:
|
|||
echo "STAGE=${stage}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Sign APK
|
||||
uses: null2264/actions/android-signer@a4d662095a2f2af1ed24f1228eb6e55b0f9f1f29
|
||||
uses: null2264/actions/android-signer@363cb9cf3d66bd9c72ed6860142c6b2c121d7e94
|
||||
if: env.VERSION_TAG != ''
|
||||
with:
|
||||
releaseDir: app/build/outputs/apk/standard/${{ steps.version_stage.outputs.STAGE }}
|
||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -10,3 +10,4 @@
|
|||
*/*/build
|
||||
.kotlin/
|
||||
kls_database.db
|
||||
weblate.conf
|
||||
|
|
|
@ -24,6 +24,8 @@
|
|||
'com.github.tachiyomiorg:image-decoder',
|
||||
'com.github.tachiyomiorg:unifile',
|
||||
'com.github.tachiyomiorg:conductor-support-preference',
|
||||
'com.github.chrisbanes:PhotoView',
|
||||
'com.github.PhilJay:MPAndroidChart',
|
||||
],
|
||||
enabled: false,
|
||||
},
|
||||
|
|
175
CHANGELOG.md
175
CHANGELOG.md
|
@ -6,11 +6,182 @@ The format is simplified version of [Keep a Changelog](https://keepachangelog.co
|
|||
- `Additions` - New features
|
||||
- `Changes` - Behaviour/visual changes
|
||||
- `Fixes` - Bugfixes
|
||||
- `Translation` - Translation changes/updates
|
||||
- `Other` - Technical changes/updates
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Additions
|
||||
- Add random library sort
|
||||
- Add the ability to save search queries
|
||||
- Add toggle to enable/disable hide source on swipe (@Hiirbaf)
|
||||
- Add the ability to mark duplicate read chapters as read (@AntsyLich)
|
||||
|
||||
### Changes
|
||||
- Temporarily disable log file
|
||||
- Categories' header now show filtered count when you search the library when you have "Show number of items" enabled (@LeeSF03)
|
||||
- Chapter progress now saved everything the page is changed
|
||||
|
||||
### Fixes
|
||||
- Allow users to bypass onboarding's permission step if Shizuku is installed
|
||||
- Fix Recents page shows "No recent chapters" instead of a loading screen
|
||||
- Fix not fully loaded entries can't be selected on Library page
|
||||
- Fix certain Infinix devices being unable to use any "Open link in browser" actions, including tracker setup (@MajorTanya)
|
||||
- Fix source filter bottom sheet unable to be fully scrolled to the bottom
|
||||
- Prevent potential "Comparison method violates its general contract!" crash
|
||||
- Fix staggered grid cover being squashed for local source (@AwkwardPeak7)
|
||||
|
||||
### Translation
|
||||
- Update translations from Weblate
|
||||
|
||||
### Other
|
||||
- Refactor Library to utilize Flow even more
|
||||
- Refactor EmptyView to use Compose
|
||||
- Refactor Reader ChapterTransition to use Compose (@arkon)
|
||||
- [Experimental] Add modified version of LargeTopAppBar that mimic J2K's ExpandedAppBarLayout
|
||||
- Refactor About page to use Compose
|
||||
- Adjust Compose-based pages' transition to match J2K's Conductor transition
|
||||
- Resolve deprecation warnings
|
||||
- Kotlin's context-receiver, schedule for removal on Kotlin v2.1.x and planned to be replaced by context-parameters on Kotlin v2.2
|
||||
- Project.exec -> Providers.exec
|
||||
- Remove internal API usage to retrieve Kotlin version for kotlin-stdlib
|
||||
- Move :core module to :core:main
|
||||
- Move archive related code to :core:archive (@AntsyLich)
|
||||
- Refactor Library to store LibraryMap instead of flatten list of LibraryItem
|
||||
- LibraryItem abstraction to make it easier to manage
|
||||
- LibraryManga no longer extend MangaImpl
|
||||
- Update dependency gradle to v8.12
|
||||
- Update user agent (@Hiirbaf)
|
||||
- Update serialization to v1.8.1
|
||||
- Update dependency io.github.fornewid:material-motion-compose-core to v1.2.1
|
||||
- Update lifecycle to v2.9.0
|
||||
- Update dependency org.jsoup:jsoup to v1.20.1
|
||||
- Update dependency org.jetbrains.kotlinx:kotlinx-collections-immutable to v0.4.0
|
||||
- Update dependency io.mockk:mockk to v1.14.2
|
||||
- Update dependency io.coil-kt.coil3:coil-bom to v3.2.0
|
||||
- Update dependency com.squareup.okio:okio to v3.12.0
|
||||
- Update dependency com.google.firebase:firebase-bom to v33.14.0
|
||||
- Update dependency com.google.accompanist:accompanist-themeadapter-material3 to v0.36.0
|
||||
- Update dependency com.github.requery:sqlite-android to v3.49.0
|
||||
- Update dependency com.getkeepsafe.taptargetview:taptargetview to v1.15.0
|
||||
- Update dependency androidx.window:window to v1.4.0
|
||||
- Update dependency androidx.webkit:webkit to v1.13.0
|
||||
- Update dependency androidx.sqlite:sqlite-ktx to v2.5.1
|
||||
- Update dependency androidx.sqlite:sqlite to v2.5.1
|
||||
- Update dependency androidx.recyclerview:recyclerview to v1.4.0
|
||||
- Update dependency androidx.core:core-ktx to v1.16.0
|
||||
- Update dependency androidx.compose:compose-bom to v2025.05.01
|
||||
- Update aboutlibraries to v11.6.3
|
||||
- Update plugin kotlinter to v5.1.0
|
||||
- Update plugin gradle-versions to v0.52.0
|
||||
- Update okhttp monorepo to v5.0.0-alpha.16
|
||||
- Update moko to v0.24.5
|
||||
- Update kotlin monorepo to v2.1.21
|
||||
- Update dependency org.jetbrains.kotlinx:kotlinx-coroutines-bom to v1.10.2
|
||||
- Update dependency me.zhanghai.android.libarchive:library to v1.1.5
|
||||
- Update dependency io.insert-koin:koin-bom to v4.0.4
|
||||
- Update dependency com.android.tools:desugar_jdk_libs to v2.1.5
|
||||
- Update dependency androidx.work:work-runtime-ktx to v2.10.1
|
||||
- Update dependency androidx.constraintlayout:constraintlayout to v2.2.1
|
||||
- Update plugin firebase-crashlytics to v3.0.3
|
||||
- Update null2264/actions digest to 363cb9c
|
||||
- Update dependency io.github.pdvrieze.xmlutil:core-android to v0.91.1
|
||||
|
||||
## [1.9.7.3]
|
||||
|
||||
### Fixes
|
||||
- More `Comparison method violates its general contract!` crash prevention
|
||||
|
||||
## [1.9.7.2]
|
||||
|
||||
### Fixes
|
||||
- Fix MyAnimeList timeout issue
|
||||
|
||||
## [1.9.7.1]
|
||||
|
||||
### Fixes
|
||||
- Prevent `Comparison method violates its general contract!` crashes
|
||||
|
||||
## [1.9.7]
|
||||
|
||||
### Changes
|
||||
- Adjust log file to only log important information by default
|
||||
|
||||
### Fixes
|
||||
- Fix sorting by latest chapter is not working properly
|
||||
- Prevent some NPE crashes
|
||||
- Fix some flickering issues when browsing sources
|
||||
- Fix download count is not updating
|
||||
|
||||
### Translation
|
||||
- Update Korean translation (@Meokjeng)
|
||||
|
||||
### Other
|
||||
- Update NDK to v27.2.12479018
|
||||
|
||||
## [1.9.6]
|
||||
|
||||
### Fixes
|
||||
- Fix some crashes
|
||||
|
||||
## [1.9.5]
|
||||
|
||||
### Changes
|
||||
- Entries from local source now behaves similar to entries from online sources
|
||||
|
||||
### Fixes
|
||||
- Fix new chapters not showing up in `Recents > Grouped`
|
||||
- Add potential workarounds for duplicate chapter bug
|
||||
- Fix favorite state is not being updated when browsing source
|
||||
|
||||
### Other
|
||||
- Update dependency androidx.compose:compose-bom to v2024.12.01
|
||||
- Update plugin kotlinter to v5
|
||||
- Update plugin gradle-versions to v0.51.0
|
||||
- Update kotlin monorepo to v2.1.0
|
||||
|
||||
## [1.9.4]
|
||||
|
||||
### Fixes
|
||||
- Fix chapter date fetch always null causing it to not appear on Updates tab
|
||||
|
||||
## [1.9.3]
|
||||
|
||||
### Fixes
|
||||
- Fix slow chapter load
|
||||
- Fix chapter bookmark state is not persistent
|
||||
|
||||
### Other
|
||||
- Refactor downloader
|
||||
- Replace RxJava usage with Kotlin coroutines
|
||||
- Replace DownloadQueue with Flow to hopefully fix ConcurrentModificationException entirely
|
||||
|
||||
## [1.9.2]
|
||||
|
||||
### Changes
|
||||
- Adjust chapter title-details contrast
|
||||
- Make app updater notification consistent with other notifications
|
||||
|
||||
### Fixes
|
||||
- Fix "Remove from read" not working properly
|
||||
|
||||
## [1.9.1]
|
||||
|
||||
### Fixes
|
||||
- Fix chapters cannot be opened from `Recents > Grouped` and `Recents > All`
|
||||
- Fix crashes caused by malformed XML
|
||||
- Fix potential memory leak
|
||||
|
||||
### Other
|
||||
- Update dependency io.github.kevinnzou:compose-webview to v0.33.6
|
||||
- Update dependency org.jsoup:jsoup to v1.18.3
|
||||
- Update voyager to v1.1.0-beta03
|
||||
- Update dependency androidx.annotation:annotation to v1.9.1
|
||||
- Update dependency androidx.constraintlayout:constraintlayout to v2.2.0
|
||||
- Update dependency androidx.glance:glance-appwidget to v1.1.1
|
||||
- Update dependency com.google.firebase:firebase-bom to v33.7.0
|
||||
- Update fast.adapter to v5.7.0
|
||||
- Downgrade dependency org.conscrypt:conscrypt-android to v2.5.2
|
||||
|
||||
## [1.9.0]
|
||||
|
||||
### Additions
|
||||
|
@ -76,7 +247,7 @@ The format is simplified version of [Keep a Changelog](https://keepachangelog.co
|
|||
- Update dependency io.mockk:mockk to v1.13.13
|
||||
- Update shizuku to v13.1.5
|
||||
- Use reflection to fix shizuku breaking changes (@Jobobby04)
|
||||
- Bump comple sdk to 35
|
||||
- Bump compile sdk to 35
|
||||
- Handle Android SDK 35 API collision (@AntsyLich)
|
||||
- Update kotlin monorepo to v2.0.21
|
||||
- Update dependency androidx.work:work-runtime-ktx to v2.10.0
|
||||
|
|
|
@ -12,10 +12,13 @@
|
|||
|
||||
A free and open source manga reader
|
||||
|
||||
[](https://github.com/null2264/yokai/actions/workflows/build_push.yml)
|
||||
[](/LICENSE)
|
||||
[](https://discord.gg/mihon)
|
||||
[](https://gitlab.com/null2264/yokai)
|
||||
[](https://gitlab.com/null2264/yokai)
|
||||
[](https://git.aap.my.id/null2264/yokai)
|
||||
|
||||
[](https://github.com/null2264/yokai/actions/workflows/build_push.yml)
|
||||
[](/LICENSE)
|
||||
[](https://hosted.weblate.org/engage/yokai/)
|
||||
|
||||
<img src="./.github/readme-images/screens.gif" alt="Yokai screenshots" />
|
||||
|
||||
|
|
|
@ -1,15 +1,13 @@
|
|||
import com.google.firebase.crashlytics.buildtools.gradle.CrashlyticsPlugin
|
||||
import com.google.gms.googleservices.GoogleServicesPlugin
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneOffset
|
||||
import java.time.format.DateTimeFormatter
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
|
||||
plugins {
|
||||
alias(androidx.plugins.application)
|
||||
alias(kotlinx.plugins.android)
|
||||
alias(kotlinx.plugins.compose.compiler)
|
||||
id("yokai.android.application")
|
||||
id("yokai.android.application.compose")
|
||||
alias(kotlinx.plugins.serialization)
|
||||
alias(kotlinx.plugins.parcelize)
|
||||
alias(libs.plugins.aboutlibraries)
|
||||
|
@ -23,15 +21,12 @@ if (gradle.startParameter.taskRequests.toString().contains("standard", true)) {
|
|||
}
|
||||
|
||||
fun runCommand(command: String): String {
|
||||
val byteOut = ByteArrayOutputStream()
|
||||
project.exec {
|
||||
commandLine = command.split(" ")
|
||||
standardOutput = byteOut
|
||||
}
|
||||
return String(byteOut.toByteArray()).trim()
|
||||
val result = providers.exec { commandLine(command.split(" ")) }
|
||||
return result.standardOutput.asText.get().trim()
|
||||
}
|
||||
|
||||
val _versionName = "1.9.0"
|
||||
@Suppress("PropertyName")
|
||||
val _versionName = "1.10.0"
|
||||
val betaCount by lazy {
|
||||
val betaTags = runCommand("git tag -l --sort=refname v${_versionName}-b*")
|
||||
|
||||
|
@ -54,7 +49,7 @@ val supportedAbis = setOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
|
|||
android {
|
||||
defaultConfig {
|
||||
applicationId = "eu.kanade.tachiyomi"
|
||||
versionCode = 150
|
||||
versionCode = 158
|
||||
versionName = _versionName
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
multiDexEnabled = true
|
||||
|
@ -72,11 +67,6 @@ android {
|
|||
//noinspection ChromeOsAbiSupport
|
||||
abiFilters += supportedAbis
|
||||
}
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
this.arguments("-DHAVE_LIBJXL=FALSE")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
splits {
|
||||
|
@ -122,7 +112,6 @@ android {
|
|||
|
||||
buildFeatures {
|
||||
viewBinding = true
|
||||
compose = true
|
||||
// If you're here because there's not BuildConfig, build the app first, it'll generate it for you
|
||||
buildConfig = true
|
||||
|
||||
|
@ -156,7 +145,8 @@ android {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.core)
|
||||
implementation(projects.core.archive)
|
||||
implementation(projects.core.main)
|
||||
implementation(projects.data)
|
||||
implementation(projects.domain)
|
||||
implementation(projects.i18n)
|
||||
|
@ -251,8 +241,6 @@ dependencies {
|
|||
implementation(libs.shizuku.api)
|
||||
implementation(libs.shizuku.provider)
|
||||
|
||||
implementation(kotlin("stdlib", org.jetbrains.kotlin.config.KotlinCompilerVersion.VERSION))
|
||||
|
||||
implementation(platform(kotlinx.coroutines.bom))
|
||||
implementation(kotlinx.bundles.coroutines)
|
||||
|
||||
|
@ -269,23 +257,26 @@ dependencies {
|
|||
testRuntimeOnly(libs.bundles.test.runtime)
|
||||
androidTestImplementation(libs.bundles.test.android)
|
||||
testImplementation(kotlinx.coroutines.test)
|
||||
|
||||
// For detecting memory leaks
|
||||
// REF: https://square.github.io/leakcanary/
|
||||
debugImplementation(libs.leakcanary.android)
|
||||
implementation(libs.leakcanary.plumber)
|
||||
}
|
||||
|
||||
tasks {
|
||||
// See https://kotlinlang.org/docs/reference/experimental.html#experimental-status-of-experimental-api(-markers)
|
||||
withType<KotlinCompile> {
|
||||
compilerOptions.freeCompilerArgs.addAll(
|
||||
"-Xcontext-receivers",
|
||||
// "-opt-in=kotlin.Experimental",
|
||||
"-opt-in=kotlin.RequiresOptIn",
|
||||
"-opt-in=kotlin.ExperimentalStdlibApi",
|
||||
"-opt-in=coil3.annotation.ExperimentalCoilApi",
|
||||
"-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi",
|
||||
"-opt-in=androidx.compose.material3.ExperimentalMaterial3Api",
|
||||
"-opt-in=androidx.compose.ui.ExperimentalComposeUiApi",
|
||||
"-opt-in=androidx.compose.foundation.ExperimentalFoundationApi",
|
||||
"-opt-in=androidx.compose.animation.ExperimentalAnimationApi",
|
||||
// "-opt-in=androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi",
|
||||
"-opt-in=coil3.annotation.ExperimentalCoilApi",
|
||||
// "-opt-in=com.google.accompanist.permissions.ExperimentalPermissionsApi",
|
||||
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
||||
"-opt-in=kotlinx.coroutines.FlowPreview",
|
||||
|
@ -293,19 +284,6 @@ tasks {
|
|||
"-opt-in=kotlinx.coroutines.InternalCoroutinesApi",
|
||||
"-opt-in=kotlinx.serialization.ExperimentalSerializationApi",
|
||||
)
|
||||
|
||||
if (project.findProperty("tachiyomi.enableComposeCompilerMetrics") == "true") {
|
||||
compilerOptions.freeCompilerArgs.addAll(
|
||||
"-P",
|
||||
"plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=" +
|
||||
(project.layout.buildDirectory.asFile.orNull?.absolutePath ?: "/tmp/yokai") + "/compose_metrics",
|
||||
)
|
||||
compilerOptions.freeCompilerArgs.addAll(
|
||||
"-P",
|
||||
"plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=" +
|
||||
(project.layout.buildDirectory.asFile.orNull?.absolutePath ?: "/tmp/yokai") + "/compose_metrics",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Duplicating Hebrew string assets due to some locale code issues on different devices
|
||||
|
|
|
@ -22,6 +22,7 @@ import androidx.lifecycle.LifecycleOwner
|
|||
import androidx.lifecycle.ProcessLifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.multidex.MultiDex
|
||||
import co.touchlab.kermit.LogWriter
|
||||
import co.touchlab.kermit.Logger
|
||||
import coil3.ImageLoader
|
||||
import coil3.PlatformContext
|
||||
|
@ -113,7 +114,12 @@ open class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.F
|
|||
|
||||
val scope = ProcessLifecycleOwner.get().lifecycleScope
|
||||
|
||||
Logger.setToDefault(buildLogWritersToAdd(storageManager.getLogsDirectory()))
|
||||
networkPreferences.verboseLogging().changes()
|
||||
.onEach { enabled ->
|
||||
// FlexibleAdapter.enableLogs(if (enabled) Level.VERBOSE else Level.SUPPRESS)
|
||||
Logger.setToDefault(buildLogWritersToAdd(storageManager.getLogsDirectory(), enabled))
|
||||
}
|
||||
.launchIn(scope)
|
||||
|
||||
basePreferences.crashReport().changes()
|
||||
.onEach {
|
||||
|
@ -283,12 +289,18 @@ open class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.F
|
|||
}
|
||||
}
|
||||
|
||||
fun buildLogWritersToAdd(logPath: UniFile?): List<LogWriter> {
|
||||
val networkPreferences: NetworkPreferences = Injekt.get()
|
||||
return buildLogWritersToAdd(logPath, networkPreferences.verboseLogging().get())
|
||||
}
|
||||
|
||||
fun buildLogWritersToAdd(
|
||||
logPath: UniFile?,
|
||||
isVerbose: Boolean,
|
||||
) = buildList {
|
||||
if (!BuildConfig.DEBUG) add(CrashlyticsLogWriter())
|
||||
|
||||
if (logPath != null) add(RollingUniFileLogWriter(logPath))
|
||||
// if (logPath != null && !BuildConfig.DEBUG) add(RollingUniFileLogWriter(logPath = logPath, isVerbose = isVerbose))
|
||||
}
|
||||
|
||||
private const val ACTION_DISABLE_INCOGNITO_MODE = "tachi.action.DISABLE_INCOGNITO_MODE"
|
||||
|
|
|
@ -40,9 +40,13 @@ import eu.kanade.tachiyomi.util.system.launchIO
|
|||
import java.util.Calendar
|
||||
import java.util.Date
|
||||
import kotlin.math.min
|
||||
import kotlin.math.roundToLong
|
||||
import kotlinx.coroutines.MainScope
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import yokai.domain.manga.models.cover
|
||||
import yokai.domain.recents.interactor.GetRecents
|
||||
|
||||
class UpdatesGridGlanceWidget : GlanceAppWidget() {
|
||||
private val app: Application by injectLazy()
|
||||
|
@ -64,6 +68,33 @@ class UpdatesGridGlanceWidget : GlanceAppWidget() {
|
|||
}
|
||||
}
|
||||
|
||||
// FIXME: Don't depends on RecentsPresenter
|
||||
private suspend fun getUpdates(customAmount: Int = 0, getRecents: GetRecents = Injekt.get()): List<Pair<Manga, Long>> {
|
||||
return getRecents
|
||||
.awaitUpdates(
|
||||
limit = when {
|
||||
customAmount > 0 -> (customAmount * 1.5).roundToLong()
|
||||
else -> 25L
|
||||
}
|
||||
)
|
||||
.mapNotNull {
|
||||
when {
|
||||
it.chapter.read || it.chapter.id == null -> RecentsPresenter.getNextChapter(it.manga)
|
||||
it.history.id == null -> RecentsPresenter.getFirstUpdatedChapter(it.manga, it.chapter)
|
||||
else -> it.chapter
|
||||
} ?: return@mapNotNull null
|
||||
it
|
||||
}
|
||||
.asSequence()
|
||||
.distinctBy { it.manga.id }
|
||||
.sortedByDescending { it.history.last_read }
|
||||
// nChapterItems + nAdditionalItems + cReadingItems
|
||||
.take((RecentsPresenter.UPDATES_CHAPTER_LIMIT * 2) + RecentsPresenter.UPDATES_READING_LIMIT_LOWER)
|
||||
.filter { it.manga.id != null }
|
||||
.map { it.manga to it.history.last_read }
|
||||
.toList()
|
||||
}
|
||||
|
||||
fun loadData(list: List<Pair<Manga, Long>>? = null) {
|
||||
coroutineScope.launchIO {
|
||||
// Don't show anything when lock is active
|
||||
|
@ -80,7 +111,7 @@ class UpdatesGridGlanceWidget : GlanceAppWidget() {
|
|||
.flatMap { manager.getAppWidgetSizes(it) }
|
||||
.maxBy { it.height.value * it.width.value }
|
||||
.calculateRowAndColumnCount()
|
||||
val processList = list ?: RecentsPresenter.getRecentManga(customAmount = min(50, rowCount * columnCount))
|
||||
val processList = list ?: getUpdates(customAmount = min(50, rowCount * columnCount))
|
||||
|
||||
data = prepareList(processList, rowCount * columnCount)
|
||||
ids.forEach { update(app, it) }
|
||||
|
|
|
@ -17,15 +17,15 @@ import androidx.glance.text.TextAlign
|
|||
import androidx.glance.text.TextStyle
|
||||
import androidx.glance.unit.ColorProvider
|
||||
import eu.kanade.tachiyomi.R
|
||||
import yokai.i18n.MR
|
||||
import eu.kanade.tachiyomi.appwidget.ContainerModifier
|
||||
import eu.kanade.tachiyomi.appwidget.util.stringResource
|
||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||
import yokai.i18n.MR
|
||||
import yokai.presentation.core.Constants
|
||||
|
||||
@Composable
|
||||
fun LockedWidget() {
|
||||
val context = LocalContext.current
|
||||
val intent = Intent(LocalContext.current, Class.forName(MainActivity.MAIN_ACTIVITY)).apply {
|
||||
val intent = Intent(LocalContext.current, Class.forName(Constants.MAIN_ACTIVITY)).apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
Box(
|
||||
|
|
|
@ -17,19 +17,18 @@ import androidx.glance.layout.Row
|
|||
import androidx.glance.layout.fillMaxWidth
|
||||
import androidx.glance.layout.padding
|
||||
import androidx.glance.text.Text
|
||||
import eu.kanade.tachiyomi.R
|
||||
import yokai.i18n.MR
|
||||
import yokai.util.lang.getString
|
||||
import eu.kanade.tachiyomi.appwidget.ContainerModifier
|
||||
import eu.kanade.tachiyomi.appwidget.util.calculateRowAndColumnCount
|
||||
import eu.kanade.tachiyomi.appwidget.util.stringResource
|
||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||
import eu.kanade.tachiyomi.ui.main.SearchActivity
|
||||
import yokai.i18n.MR
|
||||
import yokai.presentation.core.Constants
|
||||
|
||||
@Composable
|
||||
fun UpdatesWidget(data: List<Pair<Long, Bitmap?>>?) {
|
||||
val (rowCount, columnCount) = LocalSize.current.calculateRowAndColumnCount()
|
||||
val mainIntent = Intent(LocalContext.current, MainActivity::class.java).setAction(MainActivity.SHORTCUT_RECENTS)
|
||||
val mainIntent = Intent(LocalContext.current, MainActivity::class.java).setAction(Constants.SHORTCUT_RECENTS)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
||||
Column(
|
||||
modifier = ContainerModifier.clickable(actionStartActivity(mainIntent)),
|
||||
|
|
|
@ -5,9 +5,17 @@ import androidx.compose.runtime.State
|
|||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.remember
|
||||
import eu.kanade.tachiyomi.core.preference.Preference
|
||||
import java.text.DateFormat
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
@Composable
|
||||
fun <T> Preference<T>.collectAsState(): State<T> {
|
||||
val flow = remember(this) { changes() }
|
||||
return flow.collectAsState(initial = get())
|
||||
}
|
||||
|
||||
fun String.asDateFormat(): DateFormat = when (this) {
|
||||
"" -> DateFormat.getDateInstance(DateFormat.SHORT)
|
||||
else -> SimpleDateFormat(this, Locale.getDefault())
|
||||
}
|
||||
|
|
|
@ -57,8 +57,10 @@ data class BackupManga(
|
|||
@ProtoNumber(805) var customGenre: List<String>? = null,
|
||||
) {
|
||||
fun getMangaImpl(): MangaImpl {
|
||||
return MangaImpl().apply {
|
||||
url = this@BackupManga.url
|
||||
return MangaImpl(
|
||||
source = this.source,
|
||||
url = this.url,
|
||||
).apply {
|
||||
title = this@BackupManga.title
|
||||
artist = this@BackupManga.artist
|
||||
author = this@BackupManga.author
|
||||
|
@ -67,7 +69,6 @@ data class BackupManga(
|
|||
status = this@BackupManga.status
|
||||
thumbnail_url = this@BackupManga.thumbnailUrl
|
||||
favorite = this@BackupManga.favorite
|
||||
source = this@BackupManga.source
|
||||
date_added = this@BackupManga.dateAdded
|
||||
viewer_flags = (
|
||||
this@BackupManga.viewer_flags
|
||||
|
|
|
@ -205,6 +205,7 @@ class CoverCache(val context: Context) {
|
|||
100,
|
||||
it
|
||||
)
|
||||
bitmap.recycle()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -3,9 +3,9 @@ package eu.kanade.tachiyomi.data.database.models
|
|||
import android.content.Context
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import eu.kanade.tachiyomi.ui.library.LibrarySort
|
||||
import java.io.Serializable
|
||||
import yokai.i18n.MR
|
||||
import yokai.util.lang.getString
|
||||
import java.io.Serializable
|
||||
|
||||
interface Category : Serializable {
|
||||
|
||||
|
@ -37,8 +37,7 @@ interface Category : Serializable {
|
|||
return ((mangaSort?.minus('a') ?: 0) % 2) != 1
|
||||
}
|
||||
|
||||
fun sortingMode(nullAsDND: Boolean = false): LibrarySort? = LibrarySort.valueOf(mangaSort)
|
||||
?: if (nullAsDND && !isDynamic) LibrarySort.DragAndDrop else null
|
||||
fun sortingMode(): LibrarySort? = LibrarySort.valueOf(mangaSort)
|
||||
|
||||
val isDragAndDrop
|
||||
get() = (
|
||||
|
@ -56,7 +55,21 @@ interface Category : Serializable {
|
|||
fun mangaOrderToString(): String =
|
||||
if (mangaSort != null) mangaSort.toString() else mangaOrder.joinToString("/")
|
||||
|
||||
// For dynamic categories
|
||||
fun dynamicHeaderKey(): String {
|
||||
if (!isDynamic) throw IllegalStateException("This category is not a dynamic category")
|
||||
|
||||
return when {
|
||||
sourceId != null -> "${name}$sourceSplitter${sourceId}"
|
||||
langId != null -> "${langId}$langSplitter${name}"
|
||||
else -> name
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val sourceSplitter = "◘•◘"
|
||||
const val langSplitter = "⨼⨦⨠"
|
||||
|
||||
var lastCategoriesAddedTo = emptySet<Int>()
|
||||
|
||||
fun create(name: String): Category = CategoryImpl().apply {
|
||||
|
|
|
@ -32,10 +32,14 @@ class CategoryImpl : Category {
|
|||
|
||||
val category = other as Category
|
||||
|
||||
if (isDynamic && category.isDynamic) return dynamicHeaderKey() == category.dynamicHeaderKey()
|
||||
|
||||
return name == category.name
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
if (isDynamic) return dynamicHeaderKey().hashCode()
|
||||
|
||||
return name.hashCode()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -90,4 +90,8 @@ interface Chapter : SChapter, Serializable {
|
|||
source_order = other.source_order
|
||||
copyFrom(other as SChapter)
|
||||
}
|
||||
|
||||
fun copy() = ChapterImpl().apply {
|
||||
copyFrom(this@Chapter)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,11 +4,11 @@ import eu.kanade.tachiyomi.source.model.SChapter
|
|||
|
||||
fun SChapter.toChapter(): ChapterImpl {
|
||||
return ChapterImpl().apply {
|
||||
name = this@SChapter.name
|
||||
url = this@SChapter.url
|
||||
date_upload = this@SChapter.date_upload
|
||||
chapter_number = this@SChapter.chapter_number
|
||||
scanlator = this@SChapter.scanlator
|
||||
name = this@toChapter.name
|
||||
url = this@toChapter.url
|
||||
date_upload = this@toChapter.date_upload
|
||||
chapter_number = this@toChapter.chapter_number
|
||||
scanlator = this@toChapter.scanlator
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -49,8 +49,8 @@ interface History : Serializable {
|
|||
): History = HistoryImpl().apply {
|
||||
this.id = id
|
||||
this.chapter_id = chapterId
|
||||
this.last_read = lastRead ?: 0L
|
||||
this.time_read = timeRead ?: 0L
|
||||
lastRead?.let { this.last_read = it }
|
||||
timeRead?.let { this.time_read = it }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
package eu.kanade.tachiyomi.data.database.models
|
||||
|
||||
import eu.kanade.tachiyomi.ui.library.LibraryItem
|
||||
import eu.kanade.tachiyomi.domain.manga.models.Manga
|
||||
import kotlin.math.roundToInt
|
||||
import yokai.data.updateStrategyAdapter
|
||||
|
||||
data class LibraryManga(
|
||||
val manga: Manga,
|
||||
var unread: Int = 0,
|
||||
var read: Int = 0,
|
||||
var category: Int = 0,
|
||||
|
@ -13,41 +13,11 @@ data class LibraryManga(
|
|||
var latestUpdate: Long = 0,
|
||||
var lastRead: Long = 0,
|
||||
var lastFetch: Long = 0,
|
||||
) : MangaImpl() {
|
||||
|
||||
var realMangaCount = 0
|
||||
get() = if (isBlank()) field else throw IllegalStateException("realMangaCount is only accessible by placeholders")
|
||||
set(value) {
|
||||
if (!isBlank()) throw IllegalStateException("realMangaCount can only be set by placeholders")
|
||||
field = value
|
||||
}
|
||||
|
||||
) {
|
||||
val hasRead
|
||||
get() = read > 0
|
||||
|
||||
@Transient
|
||||
var items: List<LibraryItem>? = null
|
||||
get() = if (isHidden()) field else throw IllegalStateException("items only accessible by placeholders")
|
||||
set(value) {
|
||||
if (!isHidden()) throw IllegalStateException("items can only be set by placeholders")
|
||||
field = value
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun createBlank(categoryId: Int): LibraryManga = LibraryManga().apply {
|
||||
title = ""
|
||||
id = Long.MIN_VALUE
|
||||
category = categoryId
|
||||
}
|
||||
|
||||
fun createHide(categoryId: Int, title: String, hiddenItems: List<LibraryItem>): LibraryManga =
|
||||
createBlank(categoryId).apply {
|
||||
this.title = title
|
||||
this.status = -1
|
||||
this.read = hiddenItems.size
|
||||
this.items = hiddenItems
|
||||
}
|
||||
|
||||
fun mapper(
|
||||
// manga
|
||||
id: Long,
|
||||
|
@ -78,34 +48,37 @@ data class LibraryManga(
|
|||
latestUpdate: Long,
|
||||
lastRead: Long,
|
||||
lastFetch: Long,
|
||||
): LibraryManga = createBlank(categoryId.toInt()).apply {
|
||||
this.id = id
|
||||
this.source = source
|
||||
this.url = url
|
||||
this.artist = artist
|
||||
this.author = author
|
||||
this.description = description
|
||||
this.genre = genre
|
||||
this.title = title
|
||||
this.status = status.toInt()
|
||||
this.thumbnail_url = thumbnailUrl
|
||||
this.favorite = favorite
|
||||
this.last_update = lastUpdate ?: 0L
|
||||
this.initialized = initialized
|
||||
this.viewer_flags = viewerFlags.toInt()
|
||||
this.hide_title = hideTitle
|
||||
this.chapter_flags = chapterFlags.toInt()
|
||||
this.date_added = dateAdded ?: 0L
|
||||
this.filtered_scanlators = filteredScanlators
|
||||
this.update_strategy = updateStrategy.let(updateStrategyAdapter::decode)
|
||||
this.cover_last_modified = coverLastModified
|
||||
this.read = readCount.roundToInt()
|
||||
this.unread = maxOf((total - readCount).roundToInt(), 0)
|
||||
this.totalChapters = total.toInt()
|
||||
this.bookmarkCount = bookmarkCount.roundToInt()
|
||||
this.latestUpdate = latestUpdate
|
||||
this.lastRead = lastRead
|
||||
this.lastFetch = lastFetch
|
||||
}
|
||||
): LibraryManga = LibraryManga(
|
||||
manga = Manga.mapper(
|
||||
id = id,
|
||||
source = source,
|
||||
url = url,
|
||||
artist = artist,
|
||||
author = author,
|
||||
description = description,
|
||||
genre = genre,
|
||||
title = title,
|
||||
status = status,
|
||||
thumbnailUrl = thumbnailUrl,
|
||||
favorite = favorite,
|
||||
lastUpdate = lastUpdate,
|
||||
initialized = initialized,
|
||||
viewerFlags = viewerFlags,
|
||||
hideTitle = hideTitle,
|
||||
chapterFlags = chapterFlags,
|
||||
dateAdded = dateAdded,
|
||||
filteredScanlators = filteredScanlators,
|
||||
updateStrategy = updateStrategy,
|
||||
coverLastModified = coverLastModified,
|
||||
),
|
||||
read = readCount.roundToInt(),
|
||||
unread = maxOf((total - readCount).roundToInt(), 0),
|
||||
totalChapters = total.toInt(),
|
||||
bookmarkCount = bookmarkCount.roundToInt(),
|
||||
category = categoryId.toInt(),
|
||||
latestUpdate = latestUpdate,
|
||||
lastRead = lastRead,
|
||||
lastFetch = lastFetch,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -182,15 +182,13 @@ var Manga.vibrantCoverColor: Int?
|
|||
id?.let { MangaCoverMetadata.setVibrantColor(it, value) }
|
||||
}
|
||||
|
||||
fun Manga.Companion.create(source: Long) = MangaImpl().apply {
|
||||
this.source = source
|
||||
}
|
||||
|
||||
fun Manga.Companion.create(pathUrl: String, title: String, source: Long = 0) = MangaImpl().apply {
|
||||
url = pathUrl
|
||||
this.title = title
|
||||
this.source = source
|
||||
}
|
||||
fun Manga.Companion.create(url: String, title: String, source: Long = 0) =
|
||||
MangaImpl(
|
||||
source = source,
|
||||
url = url,
|
||||
).apply {
|
||||
this.title = title
|
||||
}
|
||||
|
||||
fun Manga.Companion.mapper(
|
||||
id: Long,
|
||||
|
@ -213,14 +211,12 @@ fun Manga.Companion.mapper(
|
|||
filteredScanlators: String?,
|
||||
updateStrategy: Long,
|
||||
coverLastModified: Long,
|
||||
) = create(source).apply {
|
||||
) = create(url, title, source).apply {
|
||||
this.id = id
|
||||
this.url = url
|
||||
this.artist = artist
|
||||
this.author = author
|
||||
this.description = description
|
||||
this.genre = genre
|
||||
this.title = title
|
||||
this.status = status.toInt()
|
||||
this.thumbnail_url = thumbnailUrl
|
||||
this.favorite = favorite
|
||||
|
|
|
@ -12,7 +12,11 @@ import eu.kanade.tachiyomi.domain.manga.models.Manga
|
|||
data class MangaChapterHistory(val manga: Manga, val chapter: Chapter, val history: History, var extraChapters: List<ChapterHistory> = emptyList()) {
|
||||
|
||||
companion object {
|
||||
fun createBlank() = MangaChapterHistory(MangaImpl(), ChapterImpl(), HistoryImpl())
|
||||
fun createBlank() = MangaChapterHistory(
|
||||
MangaImpl(null, -1, ""),
|
||||
ChapterImpl(),
|
||||
HistoryImpl(),
|
||||
)
|
||||
|
||||
fun mapper(
|
||||
// manga
|
||||
|
@ -38,7 +42,7 @@ data class MangaChapterHistory(val manga: Manga, val chapter: Chapter, val histo
|
|||
coverLastModified: Long,
|
||||
// chapter
|
||||
chapterId: Long?,
|
||||
_mangaId: Long?,
|
||||
chapterMangaId: Long?,
|
||||
chapterUrl: String?,
|
||||
name: String?,
|
||||
scanlator: String?,
|
||||
|
@ -80,36 +84,38 @@ data class MangaChapterHistory(val manga: Manga, val chapter: Chapter, val histo
|
|||
)
|
||||
|
||||
val chapter = try {
|
||||
chapterId?.let {
|
||||
Chapter.mapper(
|
||||
id = chapterId,
|
||||
mangaId = _mangaId ?: mangaId,
|
||||
url = chapterUrl!!,
|
||||
name = name!!,
|
||||
scanlator = scanlator,
|
||||
read = read!!,
|
||||
bookmark = bookmark!!,
|
||||
lastPageRead = lastPageRead!!,
|
||||
pagesLeft = pagesLeft!!,
|
||||
chapterNumber = chapterNumber!!,
|
||||
sourceOrder = sourceOrder!!,
|
||||
dateFetch = dateFetch!!,
|
||||
dateUpload = dateUpload!!,
|
||||
)
|
||||
}
|
||||
} catch (_: NullPointerException) { null } ?: Chapter.create()
|
||||
Chapter.mapper(
|
||||
id = chapterId!!,
|
||||
mangaId = chapterMangaId!!,
|
||||
url = chapterUrl!!,
|
||||
name = name!!,
|
||||
scanlator = scanlator,
|
||||
read = read!!,
|
||||
bookmark = bookmark!!,
|
||||
lastPageRead = lastPageRead!!,
|
||||
pagesLeft = pagesLeft!!,
|
||||
chapterNumber = chapterNumber!!,
|
||||
sourceOrder = sourceOrder!!,
|
||||
dateFetch = dateFetch!!,
|
||||
dateUpload = dateUpload!!,
|
||||
)
|
||||
} catch (_: NullPointerException) {
|
||||
ChapterImpl()
|
||||
}
|
||||
|
||||
val history = try {
|
||||
historyId?.let {
|
||||
History.mapper(
|
||||
id = historyId,
|
||||
chapterId = historyChapterId ?: chapterId ?: 0L,
|
||||
lastRead = historyLastRead,
|
||||
timeRead = historyTimeRead,
|
||||
)
|
||||
History.mapper(
|
||||
id = historyId!!,
|
||||
chapterId = historyChapterId!!,
|
||||
lastRead = historyLastRead,
|
||||
timeRead = historyTimeRead,
|
||||
)
|
||||
} catch (_: NullPointerException) {
|
||||
HistoryImpl().apply {
|
||||
historyChapterId?.let { chapter_id = it }
|
||||
historyLastRead?.let { last_read = it }
|
||||
historyTimeRead?.let { time_read = it }
|
||||
}
|
||||
} catch (_: NullPointerException) { null } ?: History.create().apply {
|
||||
historyLastRead?.let { last_read = it }
|
||||
}
|
||||
|
||||
return MangaChapterHistory(manga, chapter, history)
|
||||
|
|
|
@ -8,13 +8,11 @@ import eu.kanade.tachiyomi.source.model.SManga
|
|||
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
open class MangaImpl : Manga {
|
||||
|
||||
override var id: Long? = null
|
||||
|
||||
override var source: Long = -1
|
||||
|
||||
override lateinit var url: String
|
||||
open class MangaImpl(
|
||||
override var id: Long? = null,
|
||||
override var source: Long = -1,
|
||||
override var url: String = "",
|
||||
) : Manga {
|
||||
|
||||
private val customMangaManager: CustomMangaManager by injectLazy()
|
||||
|
||||
|
@ -107,7 +105,7 @@ open class MangaImpl : Manga {
|
|||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return if (::url.isInitialized) {
|
||||
return if (url.isNotBlank()) {
|
||||
url.hashCode()
|
||||
} else {
|
||||
(id ?: 0L).hashCode()
|
||||
|
|
|
@ -62,8 +62,8 @@ import yokai.domain.storage.StorageManager
|
|||
*/
|
||||
class DownloadCache(
|
||||
private val context: Context,
|
||||
private val provider: DownloadProvider,
|
||||
private val sourceManager: SourceManager,
|
||||
private val provider: DownloadProvider = Injekt.get(),
|
||||
private val sourceManager: SourceManager = Injekt.get(),
|
||||
private val storageManager: StorageManager = Injekt.get(),
|
||||
) {
|
||||
|
||||
|
|
|
@ -22,8 +22,9 @@ import eu.kanade.tachiyomi.util.system.workManager
|
|||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import yokai.i18n.MR
|
||||
|
@ -39,7 +40,7 @@ class DownloadJob(val context: Context, workerParams: WorkerParameters) : Corout
|
|||
private val preferences: PreferencesHelper = Injekt.get()
|
||||
|
||||
override suspend fun getForegroundInfo(): ForegroundInfo {
|
||||
val firstDL = downloadManager.queue.firstOrNull()
|
||||
val firstDL = downloadManager.queueState.value.firstOrNull()
|
||||
val notification = DownloadNotifier(context).setPlaceholder(firstDL).build()
|
||||
val id = Notifications.ID_DOWNLOAD_CHAPTER
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
|
@ -70,7 +71,6 @@ class DownloadJob(val context: Context, workerParams: WorkerParameters) : Corout
|
|||
} catch (_: CancellationException) {
|
||||
Result.success()
|
||||
} finally {
|
||||
callListeners(false, downloadManager)
|
||||
if (runExtJobAfter) {
|
||||
ExtensionUpdateJob.runJobAgain(applicationContext, NetworkType.CONNECTED)
|
||||
}
|
||||
|
@ -96,12 +96,6 @@ class DownloadJob(val context: Context, workerParams: WorkerParameters) : Corout
|
|||
private const val TAG = "Downloader"
|
||||
private const val START_EXT_JOB_AFTER = "StartExtJobAfter"
|
||||
|
||||
private val downloadChannel = MutableSharedFlow<Boolean>(
|
||||
extraBufferCapacity = 1,
|
||||
onBufferOverflow = BufferOverflow.DROP_OLDEST,
|
||||
)
|
||||
val downloadFlow = downloadChannel.asSharedFlow()
|
||||
|
||||
fun start(context: Context, alsoStartExtJob: Boolean = false) {
|
||||
val request = OneTimeWorkRequestBuilder<DownloadJob>()
|
||||
.addTag(TAG)
|
||||
|
@ -118,16 +112,17 @@ class DownloadJob(val context: Context, workerParams: WorkerParameters) : Corout
|
|||
context.workManager.cancelUniqueWork(TAG)
|
||||
}
|
||||
|
||||
fun callListeners(downloading: Boolean? = null, downloadManager: DownloadManager? = null) {
|
||||
val dManager by lazy { downloadManager ?: Injekt.get() }
|
||||
downloadChannel.tryEmit(downloading ?: !dManager.isPaused())
|
||||
}
|
||||
|
||||
fun isRunning(context: Context): Boolean {
|
||||
return context.workManager
|
||||
.getWorkInfosForUniqueWork(TAG)
|
||||
.get()
|
||||
.let { list -> list.count { it.state == WorkInfo.State.RUNNING } == 1 }
|
||||
}
|
||||
|
||||
fun isRunningFlow(context: Context): Flow<Boolean> {
|
||||
return context.workManager
|
||||
.getWorkInfosForUniqueWorkFlow(TAG)
|
||||
.map { list -> list.count { it.state == WorkInfo.State.RUNNING } == 1 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,6 @@ import co.touchlab.kermit.Logger
|
|||
import com.hippo.unifile.UniFile
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.download.model.Download
|
||||
import eu.kanade.tachiyomi.data.download.model.DownloadQueue
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.domain.manga.models.Manga
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
|
@ -13,10 +12,16 @@ import eu.kanade.tachiyomi.source.SourceManager
|
|||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.util.system.ImageUtil
|
||||
import eu.kanade.tachiyomi.util.system.launchIO
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.asFlow
|
||||
import kotlinx.coroutines.flow.drop
|
||||
import kotlinx.coroutines.flow.emitAll
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.merge
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import yokai.domain.download.DownloadPreferences
|
||||
import yokai.i18n.MR
|
||||
|
@ -29,31 +34,21 @@ import yokai.util.lang.getString
|
|||
*
|
||||
* @param context the application context.
|
||||
*/
|
||||
class DownloadManager(val context: Context) {
|
||||
|
||||
/**
|
||||
* The sources manager.
|
||||
*/
|
||||
private val sourceManager by injectLazy<SourceManager>()
|
||||
class DownloadManager(
|
||||
val context: Context,
|
||||
private val sourceManager: SourceManager = Injekt.get(),
|
||||
private val provider: DownloadProvider = Injekt.get(),
|
||||
private val cache: DownloadCache = Injekt.get(),
|
||||
) {
|
||||
|
||||
private val preferences by injectLazy<PreferencesHelper>()
|
||||
|
||||
private val downloadPreferences by injectLazy<DownloadPreferences>()
|
||||
|
||||
/**
|
||||
* Downloads provider, used to retrieve the folders where the chapters are or should be stored.
|
||||
*/
|
||||
private val provider = DownloadProvider(context)
|
||||
|
||||
/**
|
||||
* Cache of downloaded chapters.
|
||||
*/
|
||||
private val cache = DownloadCache(context, provider, sourceManager)
|
||||
|
||||
/**
|
||||
* Downloader whose only task is to download chapters.
|
||||
*/
|
||||
private val downloader = Downloader(context, provider, cache, sourceManager)
|
||||
private val downloader = Downloader(context)
|
||||
|
||||
val isRunning: Boolean get() = downloader.isRunning
|
||||
|
||||
|
@ -65,8 +60,11 @@ class DownloadManager(val context: Context) {
|
|||
/**
|
||||
* Downloads queue, where the pending chapters are stored.
|
||||
*/
|
||||
val queue: DownloadQueue
|
||||
get() = downloader.queue
|
||||
val queueState
|
||||
get() = downloader.queueState
|
||||
|
||||
val isDownloaderRunning
|
||||
get() = DownloadJob.isRunningFlow(context)
|
||||
|
||||
/**
|
||||
* Tells the downloader to begin downloads.
|
||||
|
@ -75,7 +73,6 @@ class DownloadManager(val context: Context) {
|
|||
*/
|
||||
fun startDownloads(): Boolean {
|
||||
val hasStarted = downloader.start()
|
||||
DownloadJob.callListeners(downloadManager = this)
|
||||
return hasStarted
|
||||
}
|
||||
|
||||
|
@ -99,22 +96,21 @@ class DownloadManager(val context: Context) {
|
|||
*
|
||||
* @param isNotification value that determines if status is set (needed for view updates)
|
||||
*/
|
||||
fun clearQueue(isNotification: Boolean = false) {
|
||||
deletePendingDownloads(*downloader.queue.toTypedArray())
|
||||
downloader.removeFromQueue(isNotification)
|
||||
DownloadJob.callListeners(false, this)
|
||||
fun clearQueue() {
|
||||
deletePendingDownloads(*queueState.value.toTypedArray())
|
||||
downloader.clearQueue()
|
||||
downloader.stop()
|
||||
}
|
||||
|
||||
fun startDownloadNow(chapter: Chapter) {
|
||||
val download = downloader.queue.find { it.chapter.id == chapter.id } ?: return
|
||||
val queue = downloader.queue.toMutableList()
|
||||
val download = queueState.value.find { it.chapter.id == chapter.id } ?: return
|
||||
val queue = queueState.value.toMutableList()
|
||||
queue.remove(download)
|
||||
queue.add(0, download)
|
||||
reorderQueue(queue)
|
||||
if (isPaused()) {
|
||||
if (DownloadJob.isRunning(context)) {
|
||||
downloader.start()
|
||||
DownloadJob.callListeners(true, this)
|
||||
} else {
|
||||
DownloadJob.start(context)
|
||||
}
|
||||
|
@ -127,24 +123,12 @@ class DownloadManager(val context: Context) {
|
|||
* @param downloads value to set the download queue to
|
||||
*/
|
||||
fun reorderQueue(downloads: List<Download>) {
|
||||
val wasPaused = isPaused()
|
||||
if (downloads.isEmpty()) {
|
||||
DownloadJob.stop(context)
|
||||
downloader.queue.clear()
|
||||
return
|
||||
}
|
||||
downloader.pause()
|
||||
downloader.queue.clear()
|
||||
downloader.queue.addAll(downloads)
|
||||
if (!wasPaused) {
|
||||
downloader.start()
|
||||
DownloadJob.callListeners(true, this)
|
||||
}
|
||||
downloader.updateQueue(downloads)
|
||||
}
|
||||
|
||||
fun isPaused() = !downloader.isRunning
|
||||
|
||||
fun hasQueue() = downloader.queue.isNotEmpty()
|
||||
fun hasQueue() = queueState.value.isNotEmpty()
|
||||
|
||||
/**
|
||||
* Tells the downloader to enqueue the given list of chapters.
|
||||
|
@ -164,10 +148,7 @@ class DownloadManager(val context: Context) {
|
|||
*/
|
||||
fun addDownloadsToStartOfQueue(downloads: List<Download>) {
|
||||
if (downloads.isEmpty()) return
|
||||
queue.toMutableList().apply {
|
||||
addAll(0, downloads)
|
||||
reorderQueue(this)
|
||||
}
|
||||
reorderQueue(downloads + queueState.value)
|
||||
if (!DownloadJob.isRunning(context)) DownloadJob.start(context)
|
||||
}
|
||||
|
||||
|
@ -190,7 +171,7 @@ class DownloadManager(val context: Context) {
|
|||
|
||||
return files.sortedBy { it.name }
|
||||
.mapIndexed { i, file ->
|
||||
Page(i, uri = file.uri).apply { status = Page.State.READY }
|
||||
Page(i, uri = file.uri).apply { status = Page.State.Ready }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -212,7 +193,7 @@ class DownloadManager(val context: Context) {
|
|||
* @param chapter the chapter to check.
|
||||
*/
|
||||
fun getChapterDownloadOrNull(chapter: Chapter): Download? {
|
||||
return downloader.queue
|
||||
return queueState.value
|
||||
.firstOrNull { it.chapter.id == chapter.id && it.chapter.manga_id == chapter.manga_id }
|
||||
}
|
||||
|
||||
|
@ -249,27 +230,15 @@ class DownloadManager(val context: Context) {
|
|||
* @param manga the manga of the chapters.
|
||||
* @param source the source of the chapters.
|
||||
*/
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
fun deleteChapters(chapters: List<Chapter>, manga: Manga, source: Source, force: Boolean = false) {
|
||||
val filteredChapters = if (force) chapters else getChaptersToDelete(chapters, manga)
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
val wasPaused = isPaused()
|
||||
launchIO {
|
||||
val filteredChapters = if (force) chapters else getChaptersToDelete(chapters, manga)
|
||||
if (filteredChapters.isEmpty()) {
|
||||
return@launch
|
||||
return@launchIO
|
||||
}
|
||||
downloader.pause()
|
||||
downloader.queue.remove(filteredChapters)
|
||||
if (!wasPaused && downloader.queue.isNotEmpty()) {
|
||||
downloader.start()
|
||||
DownloadJob.callListeners(true)
|
||||
} else if (downloader.queue.isEmpty() && DownloadJob.isRunning(context)) {
|
||||
DownloadJob.callListeners(false)
|
||||
DownloadJob.stop(context)
|
||||
} else if (downloader.queue.isEmpty()) {
|
||||
DownloadJob.callListeners(false)
|
||||
downloader.stop()
|
||||
}
|
||||
queue.remove(filteredChapters)
|
||||
|
||||
removeFromDownloadQueue(filteredChapters)
|
||||
|
||||
val chapterDirs =
|
||||
provider.findChapterDirs(filteredChapters, manga, source) + provider.findTempChapterDirs(
|
||||
filteredChapters,
|
||||
|
@ -278,10 +247,27 @@ class DownloadManager(val context: Context) {
|
|||
)
|
||||
chapterDirs.forEach { it.delete() }
|
||||
cache.removeChapters(filteredChapters, manga)
|
||||
|
||||
if (cache.getDownloadCount(manga, true) == 0) { // Delete manga directory if empty
|
||||
chapterDirs.firstOrNull()?.parentFile?.delete()
|
||||
}
|
||||
queue.updateListeners()
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeFromDownloadQueue(chapters: List<Chapter>) {
|
||||
val wasRunning = downloader.isRunning
|
||||
if (wasRunning) {
|
||||
downloader.pause()
|
||||
}
|
||||
|
||||
downloader.removeFromQueue(chapters)
|
||||
|
||||
if (wasRunning) {
|
||||
if (queueState.value.isEmpty()) {
|
||||
downloader.stop()
|
||||
} else if (queueState.value.isNotEmpty()) {
|
||||
downloader.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -345,9 +331,7 @@ class DownloadManager(val context: Context) {
|
|||
fun deleteManga(manga: Manga, source: Source, removeQueued: Boolean = true) {
|
||||
launchIO {
|
||||
if (removeQueued) {
|
||||
downloader.removeFromQueue(manga, true)
|
||||
queue.remove(manga)
|
||||
queue.updateListeners()
|
||||
downloader.removeFromQueue(manga)
|
||||
}
|
||||
provider.findMangaDir(manga, source)?.delete()
|
||||
cache.removeManga(manga)
|
||||
|
@ -418,9 +402,6 @@ class DownloadManager(val context: Context) {
|
|||
cache.forceRenewCache()
|
||||
}
|
||||
|
||||
fun addListener(listener: DownloadQueue.DownloadListener) = queue.addListener(listener)
|
||||
fun removeListener(listener: DownloadQueue.DownloadListener) = queue.removeListener(listener)
|
||||
|
||||
private fun getChaptersToDelete(chapters: List<Chapter>, manga: Manga): List<Chapter> {
|
||||
// Retrieve the categories that are set to exclude from being deleted on read
|
||||
return if (!preferences.removeBookmarkedChapters().get()) {
|
||||
|
@ -429,4 +410,33 @@ class DownloadManager(val context: Context) {
|
|||
chapters
|
||||
}
|
||||
}
|
||||
|
||||
fun statusFlow(): Flow<Download> = queueState
|
||||
.flatMapLatest { downloads ->
|
||||
downloads
|
||||
.map { download ->
|
||||
download.statusFlow.drop(1).map { download }
|
||||
}
|
||||
.merge()
|
||||
}
|
||||
.onStart {
|
||||
emitAll(
|
||||
queueState.value.filter { download -> download.status == Download.State.DOWNLOADING }.asFlow(),
|
||||
)
|
||||
}
|
||||
|
||||
fun progressFlow(): Flow<Download> = queueState
|
||||
.flatMapLatest { downloads ->
|
||||
downloads
|
||||
.map { download ->
|
||||
download.progressFlow.drop(1).map { download }
|
||||
}
|
||||
.merge()
|
||||
}
|
||||
.onStart {
|
||||
emitAll(
|
||||
queueState.value.filter { download -> download.status == Download.State.DOWNLOADING }
|
||||
.asFlow(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -155,9 +155,10 @@ internal class DownloadNotifier(private val context: Context) {
|
|||
}
|
||||
setStyle(null)
|
||||
setProgress(download.pages!!.size, download.downloadedImages, false)
|
||||
|
||||
// Displays the progress bar on notification
|
||||
show()
|
||||
}
|
||||
// Displays the progress bar on notification
|
||||
notification.show()
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -212,8 +213,9 @@ internal class DownloadNotifier(private val context: Context) {
|
|||
clearActions()
|
||||
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
||||
setProgress(0, 0, false)
|
||||
|
||||
show(Notifications.ID_DOWNLOAD_CHAPTER_ERROR)
|
||||
}
|
||||
notification.show(Notifications.ID_DOWNLOAD_CHAPTER_ERROR)
|
||||
|
||||
// Reset download information
|
||||
isDownloading = false
|
||||
|
@ -291,8 +293,9 @@ internal class DownloadNotifier(private val context: Context) {
|
|||
}
|
||||
color = ContextCompat.getColor(context, R.color.secondaryTachiyomi)
|
||||
setProgress(0, 0, false)
|
||||
|
||||
show(Notifications.ID_DOWNLOAD_CHAPTER_ERROR)
|
||||
}
|
||||
notification.show(Notifications.ID_DOWNLOAD_CHAPTER_ERROR)
|
||||
|
||||
// Reset download information
|
||||
errorThrown = true
|
||||
|
|
|
@ -59,6 +59,12 @@ class DownloadStore(
|
|||
}
|
||||
}
|
||||
|
||||
fun removeAll(downloads: List<Download>) {
|
||||
preferences.edit {
|
||||
downloads.forEach { remove(getKey(it)) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all the downloads from the store.
|
||||
*/
|
||||
|
|
|
@ -1,15 +1,11 @@
|
|||
package eu.kanade.tachiyomi.data.download
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import co.touchlab.kermit.Logger
|
||||
import com.hippo.unifile.UniFile
|
||||
import com.jakewharton.rxrelay.PublishRelay
|
||||
import eu.kanade.tachiyomi.data.cache.ChapterCache
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.download.model.Download
|
||||
import eu.kanade.tachiyomi.data.download.model.DownloadQueue
|
||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.domain.manga.models.Manga
|
||||
|
@ -32,22 +28,33 @@ import java.io.File
|
|||
import java.util.*
|
||||
import java.util.zip.*
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flatMapMerge
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.retryWhen
|
||||
import kotlinx.coroutines.flow.transformLatest
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.supervisorScope
|
||||
import nl.adaptivity.xmlutil.serialization.XML
|
||||
import okhttp3.Response
|
||||
import rx.Observable
|
||||
import rx.Subscription
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.schedulers.Schedulers
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import yokai.core.archive.ZipWriter
|
||||
import yokai.core.metadata.COMIC_INFO_FILE
|
||||
|
@ -61,22 +68,13 @@ import yokai.util.lang.getString
|
|||
/**
|
||||
* This class is the one in charge of downloading chapters.
|
||||
*
|
||||
* Its [queue] contains the list of chapters to download. In order to download them, the downloader
|
||||
* subscriptions must be running and the list of chapters must be sent to them by [downloadsRelay].
|
||||
*
|
||||
* The queue manipulation must be done in one thread (currently the main thread) to avoid unexpected
|
||||
* behavior, but it's safe to read it from multiple threads.
|
||||
*
|
||||
* @param context the application context.
|
||||
* @param provider the downloads directory provider.
|
||||
* @param cache the downloads cache, used to add the downloads to the cache after their completion.
|
||||
* @param sourceManager the source manager.
|
||||
* Its queue contains the list of chapters to download.
|
||||
*/
|
||||
class Downloader(
|
||||
private val context: Context,
|
||||
private val provider: DownloadProvider,
|
||||
private val cache: DownloadCache,
|
||||
private val sourceManager: SourceManager,
|
||||
private val provider: DownloadProvider = Injekt.get(),
|
||||
private val cache: DownloadCache = Injekt.get(),
|
||||
private val sourceManager: SourceManager = Injekt.get(),
|
||||
) {
|
||||
private val preferences: PreferencesHelper by injectLazy()
|
||||
private val downloadPreferences: DownloadPreferences by injectLazy()
|
||||
|
@ -92,30 +90,22 @@ class Downloader(
|
|||
/**
|
||||
* Queue where active downloads are kept.
|
||||
*/
|
||||
val queue = DownloadQueue(store)
|
||||
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
private val _queueState = MutableStateFlow<List<Download>>(emptyList())
|
||||
val queueState = _queueState.asStateFlow()
|
||||
|
||||
/**
|
||||
* Notifier for the downloader state and progress.
|
||||
*/
|
||||
private val notifier by lazy { DownloadNotifier(context) }
|
||||
|
||||
/**
|
||||
* Downloader subscription.
|
||||
*/
|
||||
private var subscription: Subscription? = null
|
||||
|
||||
/**
|
||||
* Relay to send a list of downloads to the downloader.
|
||||
*/
|
||||
private val downloadsRelay = PublishRelay.create<List<Download>>()
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
private var downloaderJob: Job? = null
|
||||
|
||||
/**
|
||||
* Whether the downloader is running.
|
||||
*/
|
||||
val isRunning: Boolean
|
||||
get() = subscription != null
|
||||
get() = downloaderJob?.isActive ?: false
|
||||
|
||||
/**
|
||||
* Whether the downloader is paused
|
||||
|
@ -126,8 +116,7 @@ class Downloader(
|
|||
init {
|
||||
launchNow {
|
||||
val chapters = async { store.restore() }
|
||||
queue.addAll(chapters.await())
|
||||
DownloadJob.callListeners()
|
||||
addAllToQueue(chapters.await())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -138,17 +127,17 @@ class Downloader(
|
|||
* @return true if the downloader is started, false otherwise.
|
||||
*/
|
||||
fun start(): Boolean {
|
||||
if (subscription != null || queue.isEmpty()) {
|
||||
if (isRunning || queueState.value.isEmpty()) {
|
||||
return false
|
||||
}
|
||||
initializeSubscription()
|
||||
|
||||
val pending = queue.filter { it.status != Download.State.DOWNLOADED }
|
||||
val pending = queueState.value.filter { it.status != Download.State.DOWNLOADED }
|
||||
pending.forEach { if (it.status != Download.State.QUEUE) it.status = Download.State.QUEUE }
|
||||
|
||||
isPaused = false
|
||||
|
||||
downloadsRelay.call(pending)
|
||||
launchDownloaderJob()
|
||||
|
||||
return pending.isNotEmpty()
|
||||
}
|
||||
|
||||
|
@ -156,8 +145,8 @@ class Downloader(
|
|||
* Stops the downloader.
|
||||
*/
|
||||
fun stop(reason: String? = null) {
|
||||
destroySubscription()
|
||||
queue
|
||||
cancelDownloaderJob()
|
||||
queueState.value
|
||||
.filter { it.status == Download.State.DOWNLOADING }
|
||||
.forEach { it.status = Download.State.ERROR }
|
||||
|
||||
|
@ -166,104 +155,109 @@ class Downloader(
|
|||
return
|
||||
}
|
||||
|
||||
DownloadJob.stop(context)
|
||||
if (isPaused && queue.isNotEmpty()) {
|
||||
handler.postDelayed({ notifier.onDownloadPaused() }, 150)
|
||||
if (isPaused && queueState.value.isNotEmpty()) {
|
||||
notifier.onDownloadPaused()
|
||||
} else {
|
||||
notifier.dismiss()
|
||||
}
|
||||
DownloadJob.callListeners(false)
|
||||
|
||||
isPaused = false
|
||||
|
||||
DownloadJob.stop(context)
|
||||
}
|
||||
|
||||
/**
|
||||
* Pauses the downloader
|
||||
*/
|
||||
fun pause() {
|
||||
destroySubscription()
|
||||
queue
|
||||
cancelDownloaderJob()
|
||||
queueState.value
|
||||
.filter { it.status == Download.State.DOWNLOADING }
|
||||
.forEach { it.status = Download.State.QUEUE }
|
||||
isPaused = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes everything from the queue.
|
||||
*
|
||||
* @param isNotification value that determines if status is set (needed for view updates)
|
||||
*/
|
||||
fun removeFromQueue(isNotification: Boolean = false) {
|
||||
destroySubscription()
|
||||
fun clearQueue() {
|
||||
cancelDownloaderJob()
|
||||
|
||||
// Needed to update the chapter view
|
||||
if (isNotification) {
|
||||
queue
|
||||
.filter { it.status == Download.State.QUEUE }
|
||||
.forEach { it.status = Download.State.NOT_DOWNLOADED }
|
||||
}
|
||||
queue.clear()
|
||||
internalClearQueue()
|
||||
notifier.dismiss()
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes everything from the queue for a certain manga
|
||||
*
|
||||
* @param isNotification value that determines if status is set (needed for view updates)
|
||||
*/
|
||||
fun removeFromQueue(manga: Manga, isNotification: Boolean = false) {
|
||||
// Needed to update the chapter view
|
||||
if (isNotification) {
|
||||
queue.filter { it.status == Download.State.QUEUE && it.manga.id == manga.id }
|
||||
.forEach { it.status = Download.State.NOT_DOWNLOADED }
|
||||
}
|
||||
queue.remove(manga)
|
||||
if (queue.isEmpty()) {
|
||||
if (DownloadJob.isRunning(context)) DownloadJob.stop(context)
|
||||
stop()
|
||||
}
|
||||
notifier.dismiss()
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the subscriptions to start downloading.
|
||||
*/
|
||||
private fun initializeSubscription() {
|
||||
private fun launchDownloaderJob() {
|
||||
if (isRunning) return
|
||||
|
||||
subscription = downloadsRelay.concatMapIterable { it }
|
||||
// Concurrently download from 5 different sources
|
||||
.groupBy { it.source }
|
||||
.flatMap(
|
||||
{ bySource ->
|
||||
bySource.concatMap { download ->
|
||||
Observable.fromCallable {
|
||||
runBlocking { downloadChapter(download) }
|
||||
download
|
||||
}.subscribeOn(Schedulers.io())
|
||||
downloaderJob = scope.launch {
|
||||
val activeDownloadsFlow = queueState.transformLatest { queue ->
|
||||
while (true) {
|
||||
val activeDownloads = queue.asSequence()
|
||||
// Ignore completed downloads, leave them in the queue
|
||||
.filter {
|
||||
val statusValue = it.status.value
|
||||
Download.State.NOT_DOWNLOADED.value <= statusValue && statusValue <= Download.State.DOWNLOADING.value
|
||||
}
|
||||
.groupBy { it.source }
|
||||
.toList()
|
||||
// Concurrently download from 5 different sources
|
||||
.take(5)
|
||||
.map { (_, downloads) -> downloads.first() }
|
||||
emit(activeDownloads)
|
||||
|
||||
if (activeDownloads.isEmpty()) break
|
||||
// Suspend until a download enters the ERROR state
|
||||
val activeDownloadsErroredFlow =
|
||||
combine(activeDownloads.map(Download::statusFlow)) { states ->
|
||||
states.contains(Download.State.ERROR)
|
||||
}.filter { it }
|
||||
activeDownloadsErroredFlow.first()
|
||||
}
|
||||
}.distinctUntilChanged()
|
||||
|
||||
// Use supervisorScope to cancel child jobs when the downloader job is cancelled
|
||||
supervisorScope {
|
||||
val downloadJobs = mutableMapOf<Download, Job>()
|
||||
|
||||
activeDownloadsFlow.collectLatest { activeDownloads ->
|
||||
val downloadJobsToStop = downloadJobs.filter { it.key !in activeDownloads }
|
||||
downloadJobsToStop.forEach { (download, job) ->
|
||||
job.cancel()
|
||||
downloadJobs.remove(download)
|
||||
}
|
||||
},
|
||||
5,
|
||||
)
|
||||
.onBackpressureLatest()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{
|
||||
completeDownload(it)
|
||||
},
|
||||
{ error ->
|
||||
Logger.e(error)
|
||||
notifier.onError(error.message)
|
||||
stop()
|
||||
},
|
||||
)
|
||||
|
||||
val downloadsToStart = activeDownloads.filter { it !in downloadJobs }
|
||||
downloadsToStart.forEach { download ->
|
||||
downloadJobs[download] = launchDownloadJob(download)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun CoroutineScope.launchDownloadJob(download: Download) = launchIO {
|
||||
try {
|
||||
downloadChapter(download)
|
||||
|
||||
// Remove successful download from queue
|
||||
if (download.status == Download.State.DOWNLOADED) {
|
||||
removeFromQueue(download)
|
||||
}
|
||||
if (areAllDownloadsFinished()) {
|
||||
stop()
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
if (e is CancellationException) throw e
|
||||
Logger.e(e)
|
||||
notifier.onError(e.message)
|
||||
stop()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroys the downloader subscriptions.
|
||||
*/
|
||||
private fun destroySubscription() {
|
||||
subscription?.unsubscribe()
|
||||
subscription = null
|
||||
private fun cancelDownloaderJob() {
|
||||
downloaderJob?.cancel()
|
||||
downloaderJob = null
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -279,7 +273,7 @@ class Downloader(
|
|||
}
|
||||
|
||||
val source = sourceManager.get(manga.source) as? HttpSource ?: return@launchIO
|
||||
val wasEmpty = queue.isEmpty()
|
||||
val wasEmpty = queueState.value.isEmpty()
|
||||
// Called in background thread, the operation can be slow with SAF.
|
||||
val chaptersWithoutDir = async {
|
||||
chapters
|
||||
|
@ -292,22 +286,17 @@ class Downloader(
|
|||
// Runs in main thread (synchronization needed).
|
||||
val chaptersToQueue = chaptersWithoutDir.await()
|
||||
// Filter out those already enqueued.
|
||||
.filter { chapter -> queue.none { it.chapter.id == chapter.id } }
|
||||
.filter { chapter -> queueState.value.none { it.chapter.id == chapter.id } }
|
||||
// Create a download for each one.
|
||||
.map { Download(source, manga, it) }
|
||||
|
||||
if (chaptersToQueue.isNotEmpty()) {
|
||||
queue.addAll(chaptersToQueue)
|
||||
|
||||
if (isRunning) {
|
||||
// Send the list of downloads to the downloader.
|
||||
downloadsRelay.call(chaptersToQueue)
|
||||
}
|
||||
addAllToQueue(chaptersToQueue)
|
||||
|
||||
// Start downloader if needed
|
||||
if (autoStart && wasEmpty) {
|
||||
val queuedDownloads = queue.count { it.source !is UnmeteredSource }
|
||||
val maxDownloadsFromSource = queue
|
||||
val queuedDownloads = queueState.value.count { it.source !is UnmeteredSource }
|
||||
val maxDownloadsFromSource = queueState.value
|
||||
.groupBy { it.source }
|
||||
.filterKeys { it !is UnmeteredSource }
|
||||
.maxOfOrNull { it.value.size } ?: 0
|
||||
|
@ -375,11 +364,11 @@ class Downloader(
|
|||
|
||||
// Get all the URLs to the source images, fetch pages if necessary
|
||||
pageList.filter { it.imageUrl.isNullOrEmpty() }.forEach { page ->
|
||||
page.status = Page.State.LOAD_PAGE
|
||||
page.status = Page.State.LoadPage
|
||||
try {
|
||||
page.imageUrl = download.source.getImageUrl(page)
|
||||
} catch (e: Throwable) {
|
||||
page.status = Page.State.ERROR
|
||||
page.status = Page.State.Error
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -398,7 +387,30 @@ class Downloader(
|
|||
}
|
||||
|
||||
// Do after download completes
|
||||
ensureSuccessfulDownload(download, mangaDir, tmpDir, chapterDirname)
|
||||
|
||||
if (!isDownloadSuccessful(download, tmpDir)) {
|
||||
download.status = Download.State.ERROR
|
||||
return
|
||||
}
|
||||
|
||||
createComicInfoFile(
|
||||
tmpDir,
|
||||
download.manga,
|
||||
download.chapter,
|
||||
download.source,
|
||||
)
|
||||
|
||||
// Only rename the directory if it's downloaded
|
||||
if (preferences.saveChaptersAsCBZ().get()) {
|
||||
archiveChapter(mangaDir, chapterDirname, tmpDir)
|
||||
} else {
|
||||
tmpDir.renameTo(chapterDirname)
|
||||
}
|
||||
cache.addChapter(chapterDirname, mangaDir, download.manga)
|
||||
|
||||
DiskUtil.createNoMediaFile(tmpDir, context)
|
||||
|
||||
download.status = Download.State.DOWNLOADED
|
||||
} catch (error: Throwable) {
|
||||
if (error is CancellationException) throw error
|
||||
// If the page list threw, it will resume here
|
||||
|
@ -408,6 +420,31 @@ class Downloader(
|
|||
}
|
||||
}
|
||||
|
||||
private fun isDownloadSuccessful(
|
||||
download: Download,
|
||||
tmpDir: UniFile,
|
||||
): Boolean {
|
||||
// Page list hasn't been initialized
|
||||
val downloadPageCount = download.pages?.size ?: return false
|
||||
|
||||
// Ensure that all pages has been downloaded
|
||||
if (download.downloadedImages != downloadPageCount) return false
|
||||
|
||||
// Ensure that the chapter folder has all the pages
|
||||
val downloadedImagesCount = tmpDir.listFiles().orEmpty().count {
|
||||
val fileName = it.name.orEmpty()
|
||||
when {
|
||||
fileName in listOf(COMIC_INFO_FILE, NOMEDIA_FILE) -> false
|
||||
fileName.endsWith(".tmp") -> false
|
||||
// Only count the first split page and not the others
|
||||
fileName.contains("__") && !fileName.endsWith("__001.jpg") -> false
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
|
||||
return downloadedImagesCount == downloadPageCount
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the observable which gets the image from the filesystem if it exists or downloads it
|
||||
* otherwise.
|
||||
|
@ -456,12 +493,12 @@ class Downloader(
|
|||
|
||||
page.uri = file.uri
|
||||
page.progress = 100
|
||||
page.status = Page.State.READY
|
||||
page.status = Page.State.Ready
|
||||
} catch (e: Throwable) {
|
||||
if (e is CancellationException) throw e
|
||||
// Mark this page as error and allow to download the remaining
|
||||
page.progress = 0
|
||||
page.status = Page.State.ERROR
|
||||
page.status = Page.State.Error
|
||||
notifier.onError(e.message, chapName, download.manga.title)
|
||||
}
|
||||
}
|
||||
|
@ -480,7 +517,7 @@ class Downloader(
|
|||
tmpDir: UniFile,
|
||||
filename: String,
|
||||
): UniFile {
|
||||
page.status = Page.State.DOWNLOAD_IMAGE
|
||||
page.status = Page.State.DownloadImage
|
||||
page.progress = 0
|
||||
return flow {
|
||||
val response = source.getImage(page)
|
||||
|
@ -558,60 +595,6 @@ class Downloader(
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the download was successful.
|
||||
*
|
||||
* @param download the download to check.
|
||||
* @param mangaDir the manga directory of the download.
|
||||
* @param tmpDir the directory where the download is currently stored.
|
||||
* @param dirname the real (non temporary) directory name of the download.
|
||||
*/
|
||||
private suspend fun ensureSuccessfulDownload(
|
||||
download: Download,
|
||||
mangaDir: UniFile,
|
||||
tmpDir: UniFile,
|
||||
dirname: String,
|
||||
) {
|
||||
// Page list hasn't been initialized
|
||||
val downloadPageCount = download.pages?.size ?: return
|
||||
// Ensure that all pages has been downloaded
|
||||
if (download.downloadedImages < downloadPageCount) return
|
||||
// Ensure that the chapter folder has all the pages
|
||||
val downloadedImagesCount = tmpDir.listFiles().orEmpty().count {
|
||||
val fileName = it.name.orEmpty()
|
||||
when {
|
||||
fileName in listOf(COMIC_INFO_FILE, NOMEDIA_FILE) -> false
|
||||
fileName.endsWith(".tmp") -> false
|
||||
// Only count the first split page and not the others
|
||||
fileName.contains("__") && !fileName.endsWith("__001.jpg") -> false
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
|
||||
download.status = if (downloadedImagesCount == downloadPageCount) {
|
||||
createComicInfoFile(
|
||||
tmpDir,
|
||||
download.manga,
|
||||
download.chapter,
|
||||
download.source,
|
||||
)
|
||||
|
||||
// Only rename the directory if it's downloaded
|
||||
if (preferences.saveChaptersAsCBZ().get()) {
|
||||
archiveChapter(mangaDir, dirname, tmpDir)
|
||||
} else {
|
||||
tmpDir.renameTo(dirname)
|
||||
}
|
||||
cache.addChapter(dirname, mangaDir, download.manga)
|
||||
|
||||
DiskUtil.createNoMediaFile(tmpDir, context)
|
||||
|
||||
Download.State.DOWNLOADED
|
||||
} else {
|
||||
Download.State.ERROR
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Archive the chapter pages as a CBZ.
|
||||
*/
|
||||
|
@ -620,8 +603,9 @@ class Downloader(
|
|||
dirname: String,
|
||||
tmpDir: UniFile,
|
||||
) {
|
||||
val zip = mangaDir.createFile("$dirname.cbz$TMP_DIR_SUFFIX") ?: return
|
||||
ZipWriter(context, zip).use { writer ->
|
||||
val zip = mangaDir.createFile("$dirname.cbz$TMP_DIR_SUFFIX")
|
||||
if (zip?.isFile != true) throw Exception("Failed to create CBZ file for downloaded chapter")
|
||||
ZipWriter(context, zip!!).use { writer ->
|
||||
tmpDir.listFiles()?.forEach { file ->
|
||||
writer.write(file)
|
||||
}
|
||||
|
@ -670,25 +654,86 @@ class Downloader(
|
|||
dir.createFile(COMIC_INFO_FILE)?.writeText(xml.encodeToString(ComicInfo.serializer(), comicInfo))
|
||||
}
|
||||
|
||||
/**
|
||||
* Completes a download. This method is called in the main thread.
|
||||
*/
|
||||
private fun completeDownload(download: Download) {
|
||||
// Delete successful downloads from queue
|
||||
if (download.status == Download.State.DOWNLOADED) {
|
||||
// Remove downloaded chapter from queue
|
||||
queue.remove(download)
|
||||
}
|
||||
if (areAllDownloadsFinished()) {
|
||||
stop()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if all the queued downloads are in DOWNLOADED or ERROR state.
|
||||
*/
|
||||
private fun areAllDownloadsFinished(): Boolean {
|
||||
return queue.none { it.status <= Download.State.DOWNLOADING }
|
||||
return queueState.value.none { it.status <= Download.State.DOWNLOADING }
|
||||
}
|
||||
|
||||
private fun addAllToQueue(downloads: List<Download>) {
|
||||
_queueState.update {
|
||||
downloads.forEach { download ->
|
||||
download.status = Download.State.QUEUE
|
||||
}
|
||||
store.addAll(downloads)
|
||||
it + downloads
|
||||
}
|
||||
}
|
||||
|
||||
fun removeFromQueue(download: Download) {
|
||||
_queueState.update {
|
||||
store.remove(download)
|
||||
if (download.status == Download.State.DOWNLOADING || download.status == Download.State.QUEUE) {
|
||||
download.status = Download.State.NOT_DOWNLOADED
|
||||
}
|
||||
it - download
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun removeFromQueueIf(predicate: (Download) -> Boolean) {
|
||||
_queueState.update { queue ->
|
||||
val downloads = queue.filter { predicate(it) }
|
||||
store.removeAll(downloads)
|
||||
downloads.forEach { download ->
|
||||
if (download.status == Download.State.DOWNLOADING || download.status == Download.State.QUEUE) {
|
||||
download.status = Download.State.NOT_DOWNLOADED
|
||||
}
|
||||
}
|
||||
queue - downloads
|
||||
}
|
||||
}
|
||||
|
||||
fun removeFromQueue(chapter: Chapter) {
|
||||
removeFromQueueIf { it.chapter.id == chapter.id }
|
||||
}
|
||||
|
||||
fun removeFromQueue(chapters: List<Chapter>) {
|
||||
removeFromQueueIf { it.chapter.id in chapters.map { it.id } }
|
||||
}
|
||||
|
||||
fun removeFromQueue(manga: Manga) {
|
||||
removeFromQueueIf { it.manga.id == manga.id }
|
||||
}
|
||||
|
||||
private fun internalClearQueue() {
|
||||
_queueState.update {
|
||||
it.forEach { download ->
|
||||
if (download.status == Download.State.DOWNLOADING || download.status == Download.State.QUEUE) {
|
||||
download.status = Download.State.NOT_DOWNLOADED
|
||||
}
|
||||
}
|
||||
store.clear()
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
fun updateQueue(downloads: List<Download>) {
|
||||
val wasRunning = isRunning
|
||||
|
||||
if (downloads.isEmpty()) {
|
||||
clearQueue()
|
||||
DownloadJob.stop(context)
|
||||
return
|
||||
}
|
||||
|
||||
pause()
|
||||
internalClearQueue()
|
||||
addAllToQueue(downloads)
|
||||
|
||||
if (wasRunning) {
|
||||
start()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
|
|
@ -4,8 +4,15 @@ import eu.kanade.tachiyomi.data.database.models.Chapter
|
|||
import eu.kanade.tachiyomi.domain.manga.models.Manga
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import rx.subjects.PublishSubject
|
||||
import kotlin.math.roundToInt
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.emitAll
|
||||
import kotlinx.coroutines.flow.flow
|
||||
|
||||
class Download(val source: HttpSource, val manga: Manga, val chapter: Chapter) {
|
||||
|
||||
|
@ -15,19 +22,33 @@ class Download(val source: HttpSource, val manga: Manga, val chapter: Chapter) {
|
|||
get() = pages?.sumOf(Page::progress) ?: 0
|
||||
|
||||
val downloadedImages: Int
|
||||
get() = pages?.count { it.status == Page.State.READY } ?: 0
|
||||
get() = pages?.count { it.status is Page.State.Ready } ?: 0
|
||||
|
||||
@Volatile @Transient
|
||||
var status: State = State.default
|
||||
@Transient
|
||||
private val _statusFlow = MutableStateFlow(State.NOT_DOWNLOADED)
|
||||
|
||||
@Transient
|
||||
val statusFlow = _statusFlow.asStateFlow()
|
||||
var status: State
|
||||
get() = _statusFlow.value
|
||||
set(status) {
|
||||
field = status
|
||||
statusSubject?.onNext(this)
|
||||
statusCallback?.invoke(this)
|
||||
_statusFlow.value = status
|
||||
}
|
||||
|
||||
@Transient private var statusSubject: PublishSubject<Download>? = null
|
||||
@Transient
|
||||
val progressFlow = flow {
|
||||
if (pages == null) {
|
||||
emit(0)
|
||||
while (pages == null) {
|
||||
delay(50)
|
||||
}
|
||||
}
|
||||
|
||||
@Transient private var statusCallback: ((Download) -> Unit)? = null
|
||||
val progressFlows = pages!!.map(Page::progressFlow)
|
||||
emitAll(combine(progressFlows) { it.average().roundToInt() })
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.debounce(50)
|
||||
|
||||
val pageProgress: Int
|
||||
get() {
|
||||
|
@ -41,21 +62,13 @@ class Download(val source: HttpSource, val manga: Manga, val chapter: Chapter) {
|
|||
return pages.map(Page::progress).average().roundToInt()
|
||||
}
|
||||
|
||||
fun setStatusSubject(subject: PublishSubject<Download>?) {
|
||||
statusSubject = subject
|
||||
}
|
||||
|
||||
fun setStatusCallback(f: ((Download) -> Unit)?) {
|
||||
statusCallback = f
|
||||
}
|
||||
|
||||
enum class State {
|
||||
CHECKED,
|
||||
NOT_DOWNLOADED,
|
||||
QUEUE,
|
||||
DOWNLOADING,
|
||||
DOWNLOADED,
|
||||
ERROR,
|
||||
enum class State(val value: Int) {
|
||||
CHECKED(-1),
|
||||
NOT_DOWNLOADED(0),
|
||||
QUEUE(1),
|
||||
DOWNLOADING(2),
|
||||
DOWNLOADED(3),
|
||||
ERROR(4),
|
||||
;
|
||||
|
||||
companion object {
|
||||
|
|
|
@ -1,131 +1,84 @@
|
|||
package eu.kanade.tachiyomi.data.download.model
|
||||
|
||||
import com.jakewharton.rxrelay.PublishRelay
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.download.DownloadStore
|
||||
import eu.kanade.tachiyomi.domain.manga.models.Manga
|
||||
import kotlinx.coroutines.MainScope
|
||||
import androidx.annotation.CallSuper
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.util.system.launchUI
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import rx.subjects.PublishSubject
|
||||
import java.util.concurrent.*
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
|
||||
class DownloadQueue(
|
||||
private val store: DownloadStore,
|
||||
private val queue: MutableList<Download> = CopyOnWriteArrayList<Download>(),
|
||||
) :
|
||||
List<Download> by queue {
|
||||
sealed class DownloadQueue {
|
||||
interface Listener {
|
||||
val progressJobs: MutableMap<Download, Job>
|
||||
|
||||
private val statusSubject = PublishSubject.create<Download>()
|
||||
// Override with presenterScope or viewScope
|
||||
val queueListenerScope: CoroutineScope
|
||||
|
||||
private val updatedRelay = PublishRelay.create<Unit>()
|
||||
|
||||
private val downloadListeners: MutableList<DownloadListener> = CopyOnWriteArrayList<DownloadListener>()
|
||||
|
||||
private var scope = MainScope()
|
||||
|
||||
fun addAll(downloads: List<Download>) {
|
||||
downloads.forEach { download ->
|
||||
download.setStatusSubject(statusSubject)
|
||||
download.setStatusCallback(::setPagesFor)
|
||||
download.status = Download.State.QUEUE
|
||||
fun onPageProgressUpdate(download: Download) {
|
||||
onProgressUpdate(download)
|
||||
}
|
||||
queue.addAll(downloads)
|
||||
store.addAll(downloads)
|
||||
updatedRelay.call(Unit)
|
||||
}
|
||||
fun onProgressUpdate(download: Download)
|
||||
fun onQueueUpdate(download: Download)
|
||||
|
||||
fun remove(download: Download) {
|
||||
val removed = queue.remove(download)
|
||||
store.remove(download)
|
||||
download.setStatusSubject(null)
|
||||
download.setStatusCallback(null)
|
||||
if (download.status == Download.State.DOWNLOADING || download.status == Download.State.QUEUE) {
|
||||
download.status = Download.State.NOT_DOWNLOADED
|
||||
}
|
||||
callListeners(download)
|
||||
if (removed) {
|
||||
updatedRelay.call(Unit)
|
||||
}
|
||||
}
|
||||
// Subscribe on presenter/controller creation on UI thread
|
||||
@CallSuper
|
||||
fun onStatusChange(download: Download) {
|
||||
when (download.status) {
|
||||
Download.State.DOWNLOADING -> {
|
||||
launchProgressJob(download)
|
||||
// Initial update of the downloaded pages
|
||||
onQueueUpdate(download)
|
||||
}
|
||||
Download.State.DOWNLOADED -> {
|
||||
cancelProgressJob(download)
|
||||
|
||||
fun updateListeners() {
|
||||
val listeners = downloadListeners.toList()
|
||||
listeners.forEach { it.updateDownloads() }
|
||||
}
|
||||
|
||||
fun remove(chapter: Chapter) {
|
||||
find { it.chapter.id == chapter.id }?.let { remove(it) }
|
||||
}
|
||||
|
||||
fun remove(chapters: List<Chapter>) {
|
||||
for (chapter in chapters) { remove(chapter) }
|
||||
}
|
||||
|
||||
fun remove(manga: Manga) {
|
||||
filter { it.manga.id == manga.id }.forEach { remove(it) }
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
queue.forEach { download ->
|
||||
download.setStatusSubject(null)
|
||||
download.setStatusCallback(null)
|
||||
if (download.status == Download.State.DOWNLOADING || download.status == Download.State.QUEUE) {
|
||||
download.status = Download.State.NOT_DOWNLOADED
|
||||
onProgressUpdate(download)
|
||||
onQueueUpdate(download)
|
||||
}
|
||||
Download.State.ERROR -> cancelProgressJob(download)
|
||||
else -> {
|
||||
/* unused */
|
||||
}
|
||||
}
|
||||
callListeners(download)
|
||||
}
|
||||
queue.clear()
|
||||
store.clear()
|
||||
updatedRelay.call(Unit)
|
||||
}
|
||||
|
||||
private fun setPagesFor(download: Download) {
|
||||
if (download.status == Download.State.DOWNLOADING) {
|
||||
if (download.pages != null) {
|
||||
for (page in download.pages!!)
|
||||
scope.launch {
|
||||
page.statusFlow.collectLatest {
|
||||
callListeners(download)
|
||||
}
|
||||
/**
|
||||
* Observe the progress of a download and notify the view.
|
||||
*
|
||||
* @param download the download to observe its progress.
|
||||
*/
|
||||
private fun launchProgressJob(download: Download) {
|
||||
val job = queueListenerScope.launchUI {
|
||||
while (download.pages == null) {
|
||||
delay(50)
|
||||
}
|
||||
|
||||
val progressFlows = download.pages!!.map(Page::progressFlow)
|
||||
combine(progressFlows, Array<Int>::sum)
|
||||
.distinctUntilChanged()
|
||||
.debounce(50)
|
||||
.collectLatest {
|
||||
onPageProgressUpdate(download)
|
||||
}
|
||||
}
|
||||
callListeners(download)
|
||||
} else if (download.status == Download.State.DOWNLOADED || download.status == Download.State.ERROR) {
|
||||
// setPagesSubject(download.pages, null)
|
||||
if (download.status == Download.State.ERROR) {
|
||||
callListeners(download)
|
||||
}
|
||||
} else {
|
||||
callListeners(download)
|
||||
|
||||
// Avoid leaking jobs
|
||||
progressJobs.remove(download)?.cancel()
|
||||
|
||||
progressJobs[download] = job
|
||||
}
|
||||
}
|
||||
|
||||
private fun callListeners(download: Download) {
|
||||
val iterator = downloadListeners.iterator()
|
||||
while (iterator.hasNext()) {
|
||||
iterator.next().updateDownload(download)
|
||||
/**
|
||||
* Unsubscribes the given download from the progress subscriptions.
|
||||
*
|
||||
* @param download the download to unsubscribe.
|
||||
*/
|
||||
private fun cancelProgressJob(download: Download) {
|
||||
progressJobs.remove(download)?.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
// private fun setPagesSubject(pages: List<Page>?, subject: PublishSubject<Int>?) {
|
||||
// if (pages != null) {
|
||||
// for (page in pages) {
|
||||
// page.setStatusSubject(subject)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
fun addListener(listener: DownloadListener) {
|
||||
downloadListeners.add(listener)
|
||||
}
|
||||
|
||||
fun removeListener(listener: DownloadListener) {
|
||||
downloadListeners.remove(listener)
|
||||
}
|
||||
|
||||
interface DownloadListener {
|
||||
fun updateDownload(download: Download)
|
||||
fun updateDownloads()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import android.content.Context
|
|||
import com.hippo.unifile.UniFile
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaImpl
|
||||
import eu.kanade.tachiyomi.domain.manga.models.Manga
|
||||
import java.nio.charset.StandardCharsets
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
|
@ -26,7 +27,6 @@ import yokai.domain.library.custom.interactor.GetCustomManga
|
|||
import yokai.domain.library.custom.interactor.RelinkCustomManga
|
||||
import yokai.domain.library.custom.model.CustomMangaInfo
|
||||
import yokai.domain.library.custom.model.CustomMangaInfo.Companion.getMangaInfo
|
||||
import java.nio.charset.StandardCharsets
|
||||
|
||||
class CustomMangaManager(val context: Context) {
|
||||
private val scope = CoroutineScope(Dispatchers.IO)
|
||||
|
@ -176,8 +176,7 @@ class CustomMangaManager(val context: Context) {
|
|||
val status: Int? = null,
|
||||
) {
|
||||
|
||||
fun toManga() = MangaImpl().apply {
|
||||
id = this@MangaJson.id
|
||||
fun toManga() = MangaImpl(id = this.id).apply {
|
||||
title = this@MangaJson.title ?: ""
|
||||
author = this@MangaJson.author
|
||||
artist = this@MangaJson.artist
|
||||
|
@ -272,9 +271,6 @@ class CustomMangaManager(val context: Context) {
|
|||
}
|
||||
}
|
||||
|
||||
private fun mangaFromComicInfoObject(id: Long, comicInfo: ComicInfo) = MangaImpl().apply {
|
||||
this.id = id
|
||||
this.copyFromComicInfo(comicInfo)
|
||||
this.title = comicInfo.series?.value ?: ""
|
||||
}
|
||||
private fun mangaFromComicInfoObject(id: Long, comicInfo: ComicInfo) =
|
||||
MangaImpl(id = id).apply { this.copyFromComicInfo(comicInfo) }
|
||||
}
|
||||
|
|
|
@ -173,16 +173,17 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
|||
|
||||
val mangaList = (
|
||||
if (savedMangasList != null) {
|
||||
val mangas = getLibraryManga.await().filter {
|
||||
it.id in savedMangasList
|
||||
}.distinctBy { it.id }
|
||||
val mangas =
|
||||
getLibraryManga.await()
|
||||
.filter { it.manga.id in savedMangasList }
|
||||
.distinctBy { it.manga.id }
|
||||
val categoryId = inputData.getInt(KEY_CATEGORY, -1)
|
||||
if (categoryId > -1) categoryIds.add(categoryId)
|
||||
mangas
|
||||
} else {
|
||||
getMangaToUpdate()
|
||||
}
|
||||
).sortedBy { it.title }
|
||||
).sortedBy { it.manga.title }
|
||||
|
||||
return withIOContext {
|
||||
try {
|
||||
|
@ -227,7 +228,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
|||
private suspend fun updateChaptersJob(mangaToAdd: List<LibraryManga>) {
|
||||
// Initialize the variables holding the progress of the updates.
|
||||
mangaToUpdate.addAll(mangaToAdd)
|
||||
mangaToUpdateMap.putAll(mangaToAdd.groupBy { it.source })
|
||||
mangaToUpdateMap.putAll(mangaToAdd.groupBy { it.manga.source })
|
||||
checkIfMassiveUpdate()
|
||||
coroutineScope {
|
||||
val list = mangaToUpdateMap.keys.map { source ->
|
||||
|
@ -257,42 +258,42 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
|||
private suspend fun updateDetails(mangaToUpdate: List<LibraryManga>) = coroutineScope {
|
||||
// Initialize the variables holding the progress of the updates.
|
||||
val count = AtomicInteger(0)
|
||||
val asyncList = mangaToUpdate.groupBy { it.source }.values.map { list ->
|
||||
val asyncList = mangaToUpdate.groupBy { it.manga.source }.values.map { list ->
|
||||
async {
|
||||
requestSemaphore.withPermit {
|
||||
list.forEach { manga ->
|
||||
ensureActive()
|
||||
val source = sourceManager.get(manga.source) as? HttpSource ?: return@async
|
||||
val source = sourceManager.get(manga.manga.source) as? HttpSource ?: return@async
|
||||
notifier.showProgressNotification(
|
||||
manga,
|
||||
manga.manga,
|
||||
count.andIncrement,
|
||||
mangaToUpdate.size,
|
||||
)
|
||||
ensureActive()
|
||||
val networkManga = try {
|
||||
source.getMangaDetails(manga.copy())
|
||||
source.getMangaDetails(manga.manga.copy())
|
||||
} catch (e: java.lang.Exception) {
|
||||
Logger.e(e)
|
||||
null
|
||||
}
|
||||
if (networkManga != null) {
|
||||
manga.prepareCoverUpdate(coverCache, networkManga, false)
|
||||
val thumbnailUrl = manga.thumbnail_url
|
||||
manga.copyFrom(networkManga)
|
||||
manga.initialized = true
|
||||
manga.manga.prepareCoverUpdate(coverCache, networkManga, false)
|
||||
val thumbnailUrl = manga.manga.thumbnail_url
|
||||
manga.manga.copyFrom(networkManga)
|
||||
manga.manga.initialized = true
|
||||
val request: ImageRequest =
|
||||
if (thumbnailUrl != manga.thumbnail_url) {
|
||||
if (thumbnailUrl != manga.manga.thumbnail_url) {
|
||||
// load new covers in background
|
||||
ImageRequest.Builder(context).data(manga.cover())
|
||||
ImageRequest.Builder(context).data(manga.manga.cover())
|
||||
.memoryCachePolicy(CachePolicy.DISABLED).build()
|
||||
} else {
|
||||
ImageRequest.Builder(context).data(manga.cover())
|
||||
ImageRequest.Builder(context).data(manga.manga.cover())
|
||||
.memoryCachePolicy(CachePolicy.DISABLED)
|
||||
.diskCachePolicy(CachePolicy.WRITE_ONLY)
|
||||
.build()
|
||||
}
|
||||
context.imageLoader.execute(request)
|
||||
updateManga.await(manga.toMangaUpdate())
|
||||
updateManga.await(manga.manga.toMangaUpdate())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -313,9 +314,9 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
|||
val loggedServices = trackManager.services.filter { it.isLogged }
|
||||
|
||||
mangaToUpdate.forEach { manga ->
|
||||
notifier.showProgressNotification(manga, count++, mangaToUpdate.size)
|
||||
notifier.showProgressNotification(manga.manga, count++, mangaToUpdate.size)
|
||||
|
||||
val tracks = getTrack.awaitAllByMangaId(manga.id!!)
|
||||
val tracks = getTrack.awaitAllByMangaId(manga.manga.id!!)
|
||||
|
||||
tracks.forEach { track ->
|
||||
val service = trackManager.getService(track.sync_id)
|
||||
|
@ -324,7 +325,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
|||
val newTrack = service.refresh(track)
|
||||
insertTrack.await(newTrack)
|
||||
|
||||
syncChaptersWithTrackServiceTwoWay(getChapter.awaitAll(manga.id!!, false), track, service)
|
||||
syncChaptersWithTrackServiceTwoWay(getChapter.awaitAll(manga.manga.id!!, false), track, service)
|
||||
} catch (e: Exception) {
|
||||
Logger.e(e)
|
||||
}
|
||||
|
@ -376,7 +377,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
|||
|
||||
private fun checkIfMassiveUpdate() {
|
||||
val largestSourceSize = mangaToUpdate
|
||||
.groupBy { it.source }
|
||||
.groupBy { it.manga.source }
|
||||
.filterKeys { sourceManager.get(it) !is UnmeteredSource }
|
||||
.maxOfOrNull { it.value.size } ?: 0
|
||||
if (largestSourceSize > MANGA_PER_SOURCE_QUEUE_WARNING_THRESHOLD) {
|
||||
|
@ -391,7 +392,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
|||
val httpSource = sourceManager.get(source) as? HttpSource ?: return false
|
||||
while (count < mangaToUpdateMap[source]!!.size) {
|
||||
val manga = mangaToUpdateMap[source]!![count]
|
||||
val shouldDownload = manga.shouldDownloadNewChapters(preferences)
|
||||
val shouldDownload = manga.manga.shouldDownloadNewChapters(preferences)
|
||||
if (updateMangaChapters(manga, this.count.andIncrement, httpSource, shouldDownload)) {
|
||||
hasDownloads = true
|
||||
}
|
||||
|
@ -410,15 +411,15 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
|||
try {
|
||||
var hasDownloads = false
|
||||
ensureActive()
|
||||
notifier.showProgressNotification(manga, progress, mangaToUpdate.size)
|
||||
val fetchedChapters = source.getChapterList(manga.copy())
|
||||
notifier.showProgressNotification(manga.manga, progress, mangaToUpdate.size)
|
||||
val fetchedChapters = source.getChapterList(manga.manga.copy())
|
||||
|
||||
if (fetchedChapters.isNotEmpty()) {
|
||||
val newChapters = syncChaptersWithSource(fetchedChapters, manga, source)
|
||||
val newChapters = syncChaptersWithSource(fetchedChapters, manga.manga, source)
|
||||
if (newChapters.first.isNotEmpty()) {
|
||||
if (shouldDownload) {
|
||||
downloadChapters(
|
||||
manga,
|
||||
manga.manga,
|
||||
newChapters.first.sortedBy { it.chapter_number },
|
||||
)
|
||||
hasDownloads = true
|
||||
|
@ -428,24 +429,24 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
|||
}
|
||||
if (deleteRemoved && newChapters.second.isNotEmpty()) {
|
||||
val removedChapters = newChapters.second.filter {
|
||||
downloadManager.isChapterDownloaded(it, manga) &&
|
||||
downloadManager.isChapterDownloaded(it, manga.manga) &&
|
||||
newChapters.first.none { newChapter ->
|
||||
newChapter.chapter_number == it.chapter_number && it.scanlator.isNullOrBlank()
|
||||
}
|
||||
}
|
||||
if (removedChapters.isNotEmpty()) {
|
||||
downloadManager.deleteChapters(removedChapters, manga, source)
|
||||
downloadManager.deleteChapters(removedChapters, manga.manga, source)
|
||||
}
|
||||
}
|
||||
if (newChapters.first.size + newChapters.second.size > 0) {
|
||||
sendUpdate(manga.id)
|
||||
sendUpdate(manga.manga.id)
|
||||
}
|
||||
}
|
||||
return@coroutineScope hasDownloads
|
||||
} catch (e: Exception) {
|
||||
if (e !is CancellationException) {
|
||||
failedUpdates[manga] = e.message
|
||||
Logger.e { "Failed updating: ${manga.title}: $e" }
|
||||
failedUpdates[manga.manga] = e.message
|
||||
Logger.e { "Failed updating: ${manga.manga.title}: $e" }
|
||||
}
|
||||
return@coroutineScope false
|
||||
}
|
||||
|
@ -461,17 +462,17 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
|||
val restrictions = preferences.libraryUpdateMangaRestriction().get()
|
||||
return mangaToAdd.filter { manga ->
|
||||
when {
|
||||
MANGA_NON_COMPLETED in restrictions && manga.status == SManga.COMPLETED -> {
|
||||
skippedUpdates[manga] = context.getString(MR.strings.skipped_reason_completed)
|
||||
MANGA_NON_COMPLETED in restrictions && manga.manga.status == SManga.COMPLETED -> {
|
||||
skippedUpdates[manga.manga] = context.getString(MR.strings.skipped_reason_completed)
|
||||
}
|
||||
MANGA_HAS_UNREAD in restrictions && manga.unread != 0 -> {
|
||||
skippedUpdates[manga] = context.getString(MR.strings.skipped_reason_not_caught_up)
|
||||
skippedUpdates[manga.manga] = context.getString(MR.strings.skipped_reason_not_caught_up)
|
||||
}
|
||||
MANGA_NON_READ in restrictions && manga.totalChapters > 0 && !manga.hasRead -> {
|
||||
skippedUpdates[manga] = context.getString(MR.strings.skipped_reason_not_started)
|
||||
skippedUpdates[manga.manga] = context.getString(MR.strings.skipped_reason_not_started)
|
||||
}
|
||||
manga.update_strategy != UpdateStrategy.ALWAYS_UPDATE -> {
|
||||
skippedUpdates[manga] = context.getString(MR.strings.skipped_reason_not_always_update)
|
||||
manga.manga.update_strategy != UpdateStrategy.ALWAYS_UPDATE -> {
|
||||
skippedUpdates[manga.manga] = context.getString(MR.strings.skipped_reason_not_always_update)
|
||||
}
|
||||
else -> {
|
||||
return@filter true
|
||||
|
@ -503,10 +504,10 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
|||
preferences.libraryUpdateCategories().get().map(String::toInt)
|
||||
if (categoriesToUpdate.isNotEmpty()) {
|
||||
categoryIds.addAll(categoriesToUpdate)
|
||||
libraryManga.filter { it.category in categoriesToUpdate }.distinctBy { it.id }
|
||||
libraryManga.filter { it.category in categoriesToUpdate }.distinctBy { it.manga.id }
|
||||
} else {
|
||||
categoryIds.addAll(getCategories.await().mapNotNull { it.id } + 0)
|
||||
libraryManga.distinctBy { it.id }
|
||||
libraryManga.distinctBy { it.manga.id }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -564,13 +565,13 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
|||
}
|
||||
|
||||
private fun addMangaToQueue(categoryId: Int, manga: List<LibraryManga>) {
|
||||
val mangas = filterMangaToUpdate(manga).sortedBy { it.title }
|
||||
val mangas = filterMangaToUpdate(manga).sortedBy { it.manga.title }
|
||||
categoryIds.add(categoryId)
|
||||
addManga(mangas)
|
||||
}
|
||||
|
||||
private fun addCategory(categoryId: Int) {
|
||||
val mangas = filterMangaToUpdate(runBlocking { getMangaToUpdate(categoryId) }).sortedBy { it.title }
|
||||
val mangas = filterMangaToUpdate(runBlocking { getMangaToUpdate(categoryId) }).sortedBy { it.manga.title }
|
||||
categoryIds.add(categoryId)
|
||||
addManga(mangas)
|
||||
}
|
||||
|
@ -579,7 +580,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
|||
val distinctManga = mangaToAdd.filter { it !in mangaToUpdate }
|
||||
mangaToUpdate.addAll(distinctManga)
|
||||
checkIfMassiveUpdate()
|
||||
distinctManga.groupBy { it.source }.forEach {
|
||||
distinctManga.groupBy { it.manga.source }.forEach {
|
||||
// if added queue items is a new source not in the async list or an async list has
|
||||
// finished running
|
||||
if (mangaToUpdateMap[it.key].isNullOrEmpty()) {
|
||||
|
@ -727,9 +728,9 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
|||
if (mangaToUse != null) {
|
||||
builder.putLongArray(
|
||||
KEY_MANGAS,
|
||||
mangaToUse.firstOrNull()?.id?.let { longArrayOf(it) } ?: longArrayOf(),
|
||||
mangaToUse.firstOrNull()?.manga?.id?.let { longArrayOf(it) } ?: longArrayOf(),
|
||||
)
|
||||
extraManga = mangaToUse.subList(1, mangaToUse.size).mapNotNull { it.id }
|
||||
extraManga = mangaToUse.subList(1, mangaToUse.size).mapNotNull { it.manga.id }
|
||||
}
|
||||
}
|
||||
val inputData = builder.build()
|
||||
|
|
|
@ -185,14 +185,14 @@ class LibraryUpdateNotifier(private val context: Context) {
|
|||
val manga = it.key
|
||||
val chapters = it.value
|
||||
val chapterNames = chapters.map { chapter ->
|
||||
chapter.preferredChapterName(context, manga, preferences)
|
||||
chapter.preferredChapterName(context, manga.manga, preferences)
|
||||
}
|
||||
notifications.add(
|
||||
Pair(
|
||||
context.notification(Notifications.CHANNEL_NEW_CHAPTERS) {
|
||||
setSmallIcon(R.drawable.ic_yokai)
|
||||
try {
|
||||
val request = ImageRequest.Builder(context).data(manga.cover())
|
||||
val request = ImageRequest.Builder(context).data(manga.manga.cover())
|
||||
.networkCachePolicy(CachePolicy.DISABLED)
|
||||
.diskCachePolicy(CachePolicy.ENABLED)
|
||||
.transformations(CircleCropTransformation())
|
||||
|
@ -205,7 +205,7 @@ class LibraryUpdateNotifier(private val context: Context) {
|
|||
} catch (_: Exception) {
|
||||
}
|
||||
setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY)
|
||||
setContentTitle(manga.title)
|
||||
setContentTitle(manga.manga.title)
|
||||
color = ContextCompat.getColor(context, R.color.secondaryTachiyomi)
|
||||
val chaptersNames = if (chapterNames.size > MAX_CHAPTERS) {
|
||||
"${chapterNames.take(MAX_CHAPTERS - 1).joinToString(", ")}, " +
|
||||
|
@ -224,7 +224,7 @@ class LibraryUpdateNotifier(private val context: Context) {
|
|||
setContentIntent(
|
||||
NotificationReceiver.openChapterPendingActivity(
|
||||
context,
|
||||
manga,
|
||||
manga.manga,
|
||||
chapters.first(),
|
||||
),
|
||||
)
|
||||
|
@ -233,7 +233,7 @@ class LibraryUpdateNotifier(private val context: Context) {
|
|||
context.getString(MR.strings.mark_as_read),
|
||||
NotificationReceiver.markAsReadPendingBroadcast(
|
||||
context,
|
||||
manga,
|
||||
manga.manga,
|
||||
chapters,
|
||||
Notifications.ID_NEW_CHAPTERS,
|
||||
),
|
||||
|
@ -243,13 +243,13 @@ class LibraryUpdateNotifier(private val context: Context) {
|
|||
context.getString(MR.strings.view_chapters),
|
||||
NotificationReceiver.openChapterPendingActivity(
|
||||
context,
|
||||
manga,
|
||||
manga.manga,
|
||||
Notifications.ID_NEW_CHAPTERS,
|
||||
),
|
||||
)
|
||||
setAutoCancel(true)
|
||||
},
|
||||
manga.id.hashCode(),
|
||||
manga.manga.id.hashCode(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
@ -281,13 +281,13 @@ class LibraryUpdateNotifier(private val context: Context) {
|
|||
NotificationCompat.BigTextStyle()
|
||||
.bigText(
|
||||
updates.keys.joinToString("\n") {
|
||||
it.title.chop(45)
|
||||
it.manga.title.chop(45)
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
} else if (!preferences.hideNotificationContent().get()) {
|
||||
setContentText(updates.keys.first().title.chop(45))
|
||||
setContentText(updates.keys.first().manga.title.chop(45))
|
||||
}
|
||||
priority = NotificationCompat.PRIORITY_HIGH
|
||||
setGroup(Notifications.GROUP_NEW_CHAPTERS)
|
||||
|
|
|
@ -64,7 +64,7 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||
downloadManager.pauseDownloads()
|
||||
}
|
||||
// Clear the download queue
|
||||
ACTION_CLEAR_DOWNLOADS -> downloadManager.clearQueue(true)
|
||||
ACTION_CLEAR_DOWNLOADS -> downloadManager.clearQueue()
|
||||
// Delete image from path and dismiss notification
|
||||
ACTION_DELETE_IMAGE -> deleteImage(
|
||||
context,
|
||||
|
@ -487,7 +487,7 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||
*/
|
||||
internal fun openChapterPendingActivity(context: Context, manga: Manga, groupId: Int): PendingIntent {
|
||||
val newIntent =
|
||||
Intent(context, MainActivity::class.java).setAction(MainActivity.SHORTCUT_MANGA)
|
||||
Intent(context, MainActivity::class.java).setAction(Constants.SHORTCUT_MANGA)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
||||
.putExtra(Constants.MANGA_EXTRA, manga.id)
|
||||
.putExtra("notificationId", manga.id.hashCode())
|
||||
|
@ -610,6 +610,11 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||
)
|
||||
}
|
||||
|
||||
internal fun dismissFailThenStartAppUpdatePendingJob(context: Context, url: String, notifyOnInstall: Boolean = false): PendingIntent {
|
||||
dismissNotification(context, Notifications.ID_UPDATER_FAILED)
|
||||
return startAppUpdatePendingJob(context, url, notifyOnInstall)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns [PendingIntent] that cancels the download for a Tachiyomi update
|
||||
*
|
||||
|
|
|
@ -7,6 +7,7 @@ import eu.kanade.tachiyomi.BuildConfig
|
|||
import eu.kanade.tachiyomi.core.preference.Preference
|
||||
import eu.kanade.tachiyomi.core.preference.PreferenceStore
|
||||
import eu.kanade.tachiyomi.core.preference.getEnum
|
||||
import eu.kanade.tachiyomi.core.storage.preference.asDateFormat
|
||||
import eu.kanade.tachiyomi.data.updater.AppDownloadInstallJob
|
||||
import eu.kanade.tachiyomi.domain.manga.models.Manga
|
||||
import eu.kanade.tachiyomi.extension.model.InstalledExtensionsOrder
|
||||
|
@ -19,13 +20,12 @@ import eu.kanade.tachiyomi.ui.reader.settings.ReadingModeType
|
|||
import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation
|
||||
import eu.kanade.tachiyomi.ui.recents.RecentsPresenter
|
||||
import eu.kanade.tachiyomi.util.system.Themes
|
||||
import java.text.DateFormat
|
||||
import java.util.Locale
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import java.text.DateFormat
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
|
||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues as Values
|
||||
|
||||
|
@ -187,10 +187,10 @@ class PreferencesHelper(val context: Context, val preferenceStore: PreferenceSto
|
|||
|
||||
fun anilistScoreType() = preferenceStore.getString("anilist_score_type", "POINT_10")
|
||||
|
||||
fun dateFormat(format: String = preferenceStore.getString(Keys.dateFormat, "").get()): DateFormat = when (format) {
|
||||
"" -> DateFormat.getDateInstance(DateFormat.SHORT)
|
||||
else -> SimpleDateFormat(format, Locale.getDefault())
|
||||
}
|
||||
fun dateFormatRaw() = preferenceStore.getString(Keys.dateFormat, "")
|
||||
|
||||
@Deprecated("Use dateFormatRaw().get().asDateFormat() instead")
|
||||
fun dateFormat(format: String = dateFormatRaw().get()): DateFormat = format.asDateFormat()
|
||||
|
||||
fun appLanguage() = preferenceStore.getString("app_language", "")
|
||||
|
||||
|
|
|
@ -49,15 +49,13 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
|||
put("completedAt", createDate(track.finished_reading_date))
|
||||
}
|
||||
}
|
||||
with(json) {
|
||||
authClient.newCall(POST(API_URL, body = payload.toString().toRequestBody(jsonMime)))
|
||||
.awaitSuccess()
|
||||
.parseAs<ALAddMangaResult>()
|
||||
.let {
|
||||
track.library_id = it.data.entry.id
|
||||
track
|
||||
}
|
||||
}
|
||||
authClient.newCall(POST(API_URL, body = payload.toString().toRequestBody(jsonMime)))
|
||||
.awaitSuccess()
|
||||
.parseAs<ALAddMangaResult>()
|
||||
.let {
|
||||
track.library_id = it.data.entry.id
|
||||
track
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -74,11 +72,9 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
|||
put("completedAt", createDate(track.finished_reading_date))
|
||||
}
|
||||
}
|
||||
with(json) {
|
||||
authClient.newCall(POST(API_URL, body = payload.toString().toRequestBody(jsonMime)))
|
||||
.awaitSuccess()
|
||||
track
|
||||
}
|
||||
authClient.newCall(POST(API_URL, body = payload.toString().toRequestBody(jsonMime)))
|
||||
.awaitSuccess()
|
||||
track
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -90,13 +86,11 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
|||
put("query", search)
|
||||
}
|
||||
}
|
||||
with(json) {
|
||||
authClient.newCall(POST(API_URL, body = payload.toString().toRequestBody(jsonMime)))
|
||||
.awaitSuccess()
|
||||
.parseAs<ALSearchResult>()
|
||||
.data.page.media
|
||||
.map { it.toALManga().toTrack() }
|
||||
}
|
||||
authClient.newCall(POST(API_URL, body = payload.toString().toRequestBody(jsonMime)))
|
||||
.awaitSuccess()
|
||||
.parseAs<ALSearchResult>()
|
||||
.data.page.media
|
||||
.map { it.toALManga().toTrack() }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -109,15 +103,13 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
|||
put("manga_id", track.media_id)
|
||||
}
|
||||
}
|
||||
with(json) {
|
||||
authClient.newCall(POST(API_URL, body = payload.toString().toRequestBody(jsonMime)))
|
||||
.awaitSuccess()
|
||||
.parseAs<ALUserListMangaQueryResult>()
|
||||
.data.page.mediaList
|
||||
.map { it.toALUserManga() }
|
||||
.firstOrNull()
|
||||
?.toTrack()
|
||||
}
|
||||
authClient.newCall(POST(API_URL, body = payload.toString().toRequestBody(jsonMime)))
|
||||
.awaitSuccess()
|
||||
.parseAs<ALUserListMangaQueryResult>()
|
||||
.data.page.mediaList
|
||||
.map { it.toALUserManga() }
|
||||
.firstOrNull()
|
||||
?.toTrack()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -75,29 +75,25 @@ class BangumiApi(
|
|||
.appendQueryParameter("responseGroup", "large")
|
||||
.appendQueryParameter("max_results", "20")
|
||||
.build()
|
||||
with(json) {
|
||||
authClient.newCall(GET(url.toString()))
|
||||
.awaitSuccess()
|
||||
.parseAs<BGMSearchResult>()
|
||||
.let { result ->
|
||||
if (result.code == 404) emptyList<TrackSearch>()
|
||||
authClient.newCall(GET(url.toString()))
|
||||
.awaitSuccess()
|
||||
.parseAs<BGMSearchResult>()
|
||||
.let { result ->
|
||||
if (result.code == 404) emptyList<TrackSearch>()
|
||||
|
||||
result.list
|
||||
?.map { it.toTrackSearch(trackId) }
|
||||
.orEmpty()
|
||||
}
|
||||
}
|
||||
result.list
|
||||
?.map { it.toTrackSearch(trackId) }
|
||||
.orEmpty()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun findLibManga(track: Track): Track? {
|
||||
return withIOContext {
|
||||
with(json) {
|
||||
authClient.newCall(GET("$API_URL/subject/${track.media_id}"))
|
||||
.awaitSuccess()
|
||||
.parseAs<BGMSearchItem>()
|
||||
.toTrackSearch(trackId)
|
||||
}
|
||||
authClient.newCall(GET("$API_URL/subject/${track.media_id}"))
|
||||
.awaitSuccess()
|
||||
.parseAs<BGMSearchItem>()
|
||||
.toTrackSearch(trackId)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -111,29 +107,25 @@ class BangumiApi(
|
|||
.build()
|
||||
|
||||
// TODO: get user readed chapter here
|
||||
with(json) {
|
||||
authClient.newCall(requestUserRead)
|
||||
.awaitSuccess()
|
||||
.parseAs<BGMCollectionResponse>()
|
||||
.let {
|
||||
if (it.code == 400) return@let null
|
||||
authClient.newCall(requestUserRead)
|
||||
.awaitSuccess()
|
||||
.parseAs<BGMCollectionResponse>()
|
||||
.let {
|
||||
if (it.code == 400) return@let null
|
||||
|
||||
track.status = it.status?.id?.toInt() ?: Bangumi.DEFAULT_STATUS
|
||||
track.last_chapter_read = it.epStatus!!.toFloat()
|
||||
track.score = it.rating!!.toFloat()
|
||||
track
|
||||
}
|
||||
}
|
||||
track.status = it.status?.id?.toInt() ?: Bangumi.DEFAULT_STATUS
|
||||
track.last_chapter_read = it.epStatus!!.toFloat()
|
||||
track.score = it.rating!!.toFloat()
|
||||
track
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun accessToken(code: String): BGMOAuth {
|
||||
return withIOContext {
|
||||
with(json) {
|
||||
client.newCall(accessTokenRequest(code))
|
||||
.awaitSuccess()
|
||||
.parseAs()
|
||||
}
|
||||
client.newCall(accessTokenRequest(code))
|
||||
.awaitSuccess()
|
||||
.parseAs()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -44,7 +44,7 @@ class KavitaApi(private val client: OkHttpClient, interceptor: KavitaInterceptor
|
|||
try {
|
||||
client.newCall(request).execute().use {
|
||||
when (it.code) {
|
||||
200 -> return with(json) { it.parseAs<AuthenticationDto>().token }
|
||||
200 -> return it.parseAs<AuthenticationDto>().token
|
||||
401 -> {
|
||||
Logger.w { "Unauthorized / api key not valid: Cleaned api URL: $apiUrl, Api key is empty: ${apiKey.isEmpty()}" }
|
||||
throw IOException("Unauthorized / api key not valid")
|
||||
|
@ -89,11 +89,10 @@ class KavitaApi(private val client: OkHttpClient, interceptor: KavitaInterceptor
|
|||
private fun getTotalChapters(url: String): Long {
|
||||
val requestUrl = getApiVolumesUrl(url)
|
||||
try {
|
||||
val listVolumeDto = with(json) {
|
||||
val listVolumeDto =
|
||||
authClient.newCall(GET(requestUrl))
|
||||
.execute()
|
||||
.parseAs<List<VolumeDto>>()
|
||||
}
|
||||
var volumeNumber = 0L
|
||||
var maxChapterNumber = 0L
|
||||
for (volume in listVolumeDto) {
|
||||
|
@ -117,9 +116,7 @@ class KavitaApi(private val client: OkHttpClient, interceptor: KavitaInterceptor
|
|||
try {
|
||||
authClient.newCall(GET(requestUrl)).execute().use {
|
||||
if (it.code == 200) {
|
||||
return with(json) {
|
||||
it.parseAs<ChapterDto>().number!!.replace(",", ".").toFloat()
|
||||
}
|
||||
return it.parseAs<ChapterDto>().number!!.replace(",", ".").toFloat()
|
||||
}
|
||||
if (it.code == 204) {
|
||||
return 0F
|
||||
|
@ -134,11 +131,10 @@ class KavitaApi(private val client: OkHttpClient, interceptor: KavitaInterceptor
|
|||
|
||||
suspend fun getTrackSearch(url: String): TrackSearch = withIOContext {
|
||||
try {
|
||||
val serieDto: SeriesDto = with(json) {
|
||||
val serieDto: SeriesDto =
|
||||
authClient.newCall(GET(url))
|
||||
.awaitSuccess()
|
||||
.parseAs()
|
||||
}
|
||||
|
||||
val track = serieDto.toTrack()
|
||||
track.apply {
|
||||
|
|
|
@ -131,14 +131,12 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
|
|||
|
||||
suspend fun search(query: String): List<TrackSearch> {
|
||||
return withIOContext {
|
||||
with(json) {
|
||||
authClient.newCall(GET(ALGOLIA_KEY_URL))
|
||||
.awaitSuccess()
|
||||
.parseAs<KitsuSearchResult>()
|
||||
.let {
|
||||
algoliaSearch(it.media.key, query)
|
||||
}
|
||||
}
|
||||
authClient.newCall(GET(ALGOLIA_KEY_URL))
|
||||
.awaitSuccess()
|
||||
.parseAs<KitsuSearchResult>()
|
||||
.let {
|
||||
algoliaSearch(it.media.key, query)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -147,25 +145,23 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
|
|||
val jsonObject = buildJsonObject {
|
||||
put("params", "query=$query$ALGOLIA_FILTER")
|
||||
}
|
||||
with(json) {
|
||||
client.newCall(
|
||||
POST(
|
||||
ALGOLIA_URL,
|
||||
headers = headersOf(
|
||||
"X-Algolia-Application-Id",
|
||||
ALGOLIA_APP_ID,
|
||||
"X-Algolia-API-Key",
|
||||
key,
|
||||
),
|
||||
body = jsonObject.toString().toRequestBody(jsonMime),
|
||||
client.newCall(
|
||||
POST(
|
||||
ALGOLIA_URL,
|
||||
headers = headersOf(
|
||||
"X-Algolia-Application-Id",
|
||||
ALGOLIA_APP_ID,
|
||||
"X-Algolia-API-Key",
|
||||
key,
|
||||
),
|
||||
)
|
||||
.awaitSuccess()
|
||||
.parseAs<KitsuAlgoliaSearchResult>()
|
||||
.hits
|
||||
.filter { it.subtype != "novel" }
|
||||
.map { it.toTrack() }
|
||||
}
|
||||
body = jsonObject.toString().toRequestBody(jsonMime),
|
||||
),
|
||||
)
|
||||
.awaitSuccess()
|
||||
.parseAs<KitsuAlgoliaSearchResult>()
|
||||
.hits
|
||||
.filter { it.subtype != "novel" }
|
||||
.map { it.toTrack() }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -175,18 +171,16 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
|
|||
.encodedQuery("filter[manga_id]=${track.media_id}&filter[user_id]=$userId")
|
||||
.appendQueryParameter("include", "manga")
|
||||
.build()
|
||||
with(json) {
|
||||
authClient.newCall(GET(url.toString()))
|
||||
.awaitSuccess()
|
||||
.parseAs<KitsuListSearchResult>()
|
||||
.let {
|
||||
if (it.data.isNotEmpty() && it.included.isNotEmpty()) {
|
||||
it.firstToTrack()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
authClient.newCall(GET(url.toString()))
|
||||
.awaitSuccess()
|
||||
.parseAs<KitsuListSearchResult>()
|
||||
.let {
|
||||
if (it.data.isNotEmpty() && it.included.isNotEmpty()) {
|
||||
it.firstToTrack()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -196,18 +190,16 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
|
|||
.encodedQuery("filter[id]=${track.media_id}")
|
||||
.appendQueryParameter("include", "manga")
|
||||
.build()
|
||||
with(json) {
|
||||
authClient.newCall(GET(url.toString()))
|
||||
.awaitSuccess()
|
||||
.parseAs<KitsuListSearchResult>()
|
||||
.let {
|
||||
if (it.data.isNotEmpty() && it.included.isNotEmpty()) {
|
||||
it.firstToTrack()
|
||||
} else {
|
||||
throw Exception("Could not find manga")
|
||||
}
|
||||
authClient.newCall(GET(url.toString()))
|
||||
.awaitSuccess()
|
||||
.parseAs<KitsuListSearchResult>()
|
||||
.let {
|
||||
if (it.data.isNotEmpty() && it.included.isNotEmpty()) {
|
||||
it.firstToTrack()
|
||||
} else {
|
||||
throw Exception("Could not find manga")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -220,11 +212,9 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
|
|||
.add("client_id", CLIENT_ID)
|
||||
.add("client_secret", CLIENT_SECRET)
|
||||
.build()
|
||||
with(json) {
|
||||
client.newCall(POST(LOGIN_URL, body = formBody))
|
||||
.awaitSuccess()
|
||||
.parseAs()
|
||||
}
|
||||
client.newCall(POST(LOGIN_URL, body = formBody))
|
||||
.awaitSuccess()
|
||||
.parseAs()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -233,13 +223,11 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
|
|||
val url = "${BASE_URL}users".toUri().buildUpon()
|
||||
.encodedQuery("filter[self]=true")
|
||||
.build()
|
||||
with(json) {
|
||||
authClient.newCall(GET(url.toString()))
|
||||
.awaitSuccess()
|
||||
.parseAs<KitsuCurrentUserResult>()
|
||||
.data[0]
|
||||
.id
|
||||
}
|
||||
authClient.newCall(GET(url.toString()))
|
||||
.awaitSuccess()
|
||||
.parseAs<KitsuCurrentUserResult>()
|
||||
.data[0]
|
||||
.id
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -26,21 +26,19 @@ class KomgaApi(private val client: OkHttpClient) {
|
|||
withIOContext {
|
||||
try {
|
||||
val track =
|
||||
with(json) {
|
||||
if (url.contains(READLIST_API)) {
|
||||
client.newCall(GET(url))
|
||||
.awaitSuccess()
|
||||
.parseAs<ReadListDto>()
|
||||
.toTrack()
|
||||
} else {
|
||||
client.newCall(GET(url))
|
||||
.awaitSuccess()
|
||||
.parseAs<SeriesDto>()
|
||||
.toTrack()
|
||||
}
|
||||
if (url.contains(READLIST_API)) {
|
||||
client.newCall(GET(url))
|
||||
.awaitSuccess()
|
||||
.parseAs<ReadListDto>()
|
||||
.toTrack()
|
||||
} else {
|
||||
client.newCall(GET(url))
|
||||
.awaitSuccess()
|
||||
.parseAs<SeriesDto>()
|
||||
.toTrack()
|
||||
}
|
||||
|
||||
val progress = with(json) {
|
||||
val progress =
|
||||
client
|
||||
.newCall(
|
||||
GET(
|
||||
|
@ -59,7 +57,6 @@ class KomgaApi(private val client: OkHttpClient) {
|
|||
it.parseAs<ReadProgressDto>().toV2()
|
||||
}
|
||||
}
|
||||
}
|
||||
track.apply {
|
||||
cover_url = "$url/thumbnail"
|
||||
tracking_url = url
|
||||
|
|
|
@ -37,15 +37,13 @@ class MangaUpdatesApi(
|
|||
|
||||
suspend fun getSeriesListItem(track: Track): Pair<MUListItem, MURating?> {
|
||||
val listItem =
|
||||
with(json) {
|
||||
authClient.newCall(
|
||||
GET(
|
||||
url = "$BASE_URL/v1/lists/series/${track.media_id}",
|
||||
),
|
||||
)
|
||||
.awaitSuccess()
|
||||
.parseAs<MUListItem>()
|
||||
}
|
||||
authClient.newCall(
|
||||
GET(
|
||||
url = "$BASE_URL/v1/lists/series/${track.media_id}",
|
||||
),
|
||||
)
|
||||
.awaitSuccess()
|
||||
.parseAs<MUListItem>()
|
||||
|
||||
val rating = getSeriesRating(track)
|
||||
|
||||
|
@ -104,15 +102,13 @@ class MangaUpdatesApi(
|
|||
|
||||
private suspend fun getSeriesRating(track: Track): MURating? {
|
||||
return try {
|
||||
with(json) {
|
||||
authClient.newCall(
|
||||
GET(
|
||||
url = "$BASE_URL/v1/series/${track.media_id}/rating",
|
||||
),
|
||||
)
|
||||
.awaitSuccess()
|
||||
.parseAs<MURating>()
|
||||
}
|
||||
authClient.newCall(
|
||||
GET(
|
||||
url = "$BASE_URL/v1/series/${track.media_id}/rating",
|
||||
),
|
||||
)
|
||||
.awaitSuccess()
|
||||
.parseAs<MURating>()
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
|
@ -151,18 +147,16 @@ class MangaUpdatesApi(
|
|||
},
|
||||
)
|
||||
}
|
||||
return with(json) {
|
||||
client.newCall(
|
||||
POST(
|
||||
url = "$BASE_URL/v1/series/search",
|
||||
body = body.toString().toRequestBody(CONTENT_TYPE),
|
||||
),
|
||||
)
|
||||
.awaitSuccess()
|
||||
.parseAs<MUSearchResult>()
|
||||
.results
|
||||
.map { it.record }
|
||||
}
|
||||
return client.newCall(
|
||||
POST(
|
||||
url = "$BASE_URL/v1/series/search",
|
||||
body = body.toString().toRequestBody(CONTENT_TYPE),
|
||||
),
|
||||
)
|
||||
.awaitSuccess()
|
||||
.parseAs<MUSearchResult>()
|
||||
.results
|
||||
.map { it.record }
|
||||
}
|
||||
|
||||
suspend fun authenticate(username: String, password: String): MUContext? {
|
||||
|
@ -170,17 +164,15 @@ class MangaUpdatesApi(
|
|||
put("username", username)
|
||||
put("password", password)
|
||||
}
|
||||
return with(json) {
|
||||
client.newCall(
|
||||
PUT(
|
||||
url = "$BASE_URL/v1/account/login",
|
||||
body = body.toString().toRequestBody(CONTENT_TYPE),
|
||||
),
|
||||
)
|
||||
.awaitSuccess()
|
||||
.parseAs<MULoginResponse>()
|
||||
.context
|
||||
}
|
||||
return client.newCall(
|
||||
PUT(
|
||||
url = "$BASE_URL/v1/account/login",
|
||||
body = body.toString().toRequestBody(CONTENT_TYPE),
|
||||
),
|
||||
)
|
||||
.awaitSuccess()
|
||||
.parseAs<MULoginResponse>()
|
||||
.context
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
|
|
@ -45,11 +45,9 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
|
|||
.add("code_verifier", codeVerifier)
|
||||
.add("grant_type", "authorization_code")
|
||||
.build()
|
||||
with(json) {
|
||||
client.newCall(POST("$BASE_OAUTH_URL/token", body = formBody))
|
||||
.awaitSuccess()
|
||||
.parseAs()
|
||||
}
|
||||
client.newCall(POST("$BASE_OAUTH_URL/token", body = formBody))
|
||||
.awaitSuccess()
|
||||
.parseAs()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -59,12 +57,10 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
|
|||
.url("$BASE_API_URL/users/@me")
|
||||
.get()
|
||||
.build()
|
||||
with(json) {
|
||||
authClient.newCall(request)
|
||||
.awaitSuccess()
|
||||
.parseAs<MALUser>()
|
||||
.name
|
||||
}
|
||||
authClient.newCall(request)
|
||||
.awaitSuccess()
|
||||
.parseAs<MALUser>()
|
||||
.name
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -75,15 +71,13 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
|
|||
.appendQueryParameter("q", query.take(64))
|
||||
.appendQueryParameter("nsfw", "true")
|
||||
.build()
|
||||
with(json) {
|
||||
authClient.newCall(GET(url.toString()))
|
||||
.awaitSuccess()
|
||||
.parseAs<MALSearchResult>()
|
||||
.data
|
||||
.map { async { getMangaDetails(it.node.id) } }
|
||||
.awaitAll()
|
||||
.filter { !it.publishing_type.contains("novel") }
|
||||
}
|
||||
authClient.newCall(GET(url.toString()))
|
||||
.awaitSuccess()
|
||||
.parseAs<MALSearchResult>()
|
||||
.data
|
||||
.map { async { getMangaDetails(it.node.id) } }
|
||||
.awaitAll()
|
||||
.filter { !it.publishing_type.contains("novel") }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -93,24 +87,22 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
|
|||
.appendPath(id.toString())
|
||||
.appendQueryParameter("fields", "id,title,synopsis,num_chapters,main_picture,status,media_type,start_date")
|
||||
.build()
|
||||
with(json) {
|
||||
authClient.newCall(GET(url.toString()))
|
||||
.awaitSuccess()
|
||||
.parseAs<MALManga>()
|
||||
.let {
|
||||
TrackSearch.create(TrackManager.MYANIMELIST).apply {
|
||||
media_id = it.id
|
||||
title = it.title
|
||||
summary = it.synopsis
|
||||
total_chapters = it.numChapters
|
||||
cover_url = it.covers.large
|
||||
tracking_url = "https://myanimelist.net/manga/$media_id"
|
||||
publishing_status = it.status.replace("_", " ")
|
||||
publishing_type = it.mediaType.replace("_", " ")
|
||||
start_date = it.startDate ?: ""
|
||||
}
|
||||
authClient.newCall(GET(url.toString()))
|
||||
.awaitSuccess()
|
||||
.parseAs<MALManga>()
|
||||
.let {
|
||||
TrackSearch.create(TrackManager.MYANIMELIST).apply {
|
||||
media_id = it.id
|
||||
title = it.title
|
||||
summary = it.synopsis
|
||||
total_chapters = it.numChapters
|
||||
cover_url = (it.covers?.large ?: it.covers?.medium).orEmpty()
|
||||
tracking_url = "https://myanimelist.net/manga/$media_id"
|
||||
publishing_status = it.status.replace("_", " ")
|
||||
publishing_type = it.mediaType.replace("_", " ")
|
||||
start_date = it.startDate ?: ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -132,12 +124,10 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
|
|||
.url(mangaUrl(track.media_id).toString())
|
||||
.put(formBodyBuilder.build())
|
||||
.build()
|
||||
with(json) {
|
||||
authClient.newCall(request)
|
||||
.awaitSuccess()
|
||||
.parseAs<MALListItemStatus>()
|
||||
.let { parseMangaItem(it, track) }
|
||||
}
|
||||
authClient.newCall(request)
|
||||
.awaitSuccess()
|
||||
.parseAs<MALListItemStatus>()
|
||||
.let { parseMangaItem(it, track) }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -147,15 +137,13 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
|
|||
.appendPath(track.media_id.toString())
|
||||
.appendQueryParameter("fields", "num_chapters,my_list_status{start_date,finish_date}")
|
||||
.build()
|
||||
with(json) {
|
||||
authClient.newCall(GET(uri.toString()))
|
||||
.awaitSuccess()
|
||||
.parseAs<MALListItem>()
|
||||
.let { item ->
|
||||
track.total_chapters = item.numChapters
|
||||
item.myListStatus?.let { parseMangaItem(it, track) }
|
||||
}
|
||||
}
|
||||
authClient.newCall(GET(uri.toString()))
|
||||
.awaitSuccess()
|
||||
.parseAs<MALListItem>()
|
||||
.let { item ->
|
||||
track.total_chapters = item.numChapters
|
||||
item.myListStatus?.let { parseMangaItem(it, track) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -190,11 +178,9 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
|
|||
.url(urlBuilder.build().toString())
|
||||
.get()
|
||||
.build()
|
||||
with(json) {
|
||||
authClient.newCall(request)
|
||||
.awaitSuccess()
|
||||
.parseAs()
|
||||
}
|
||||
authClient.newCall(request)
|
||||
.awaitSuccess()
|
||||
.parseAs()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -34,7 +34,7 @@ class MyAnimeListInterceptor(private val myanimelist: MyAnimeList) : Interceptor
|
|||
// Add the authorization header to the original request
|
||||
val authRequest = originalRequest.newBuilder()
|
||||
.addHeader("Authorization", "Bearer ${oauth!!.accessToken}")
|
||||
.header("User-Agent", "null2264/yokai/${BuildConfig.VERSION_NAME} (${BuildConfig.APPLICATION_ID})")
|
||||
// .header("User-Agent", "null2264/yokai/${BuildConfig.VERSION_NAME} (${BuildConfig.APPLICATION_ID})")
|
||||
.build()
|
||||
|
||||
return chain.proceed(authRequest)
|
||||
|
@ -66,7 +66,7 @@ class MyAnimeListInterceptor(private val myanimelist: MyAnimeList) : Interceptor
|
|||
|
||||
return runCatching {
|
||||
if (response.isSuccessful) {
|
||||
with(json) { response.parseAs<MALOAuth>() }
|
||||
response.parseAs<MALOAuth>()
|
||||
} else {
|
||||
response.close()
|
||||
null
|
||||
|
|
|
@ -12,7 +12,7 @@ data class MALManga(
|
|||
val numChapters: Long,
|
||||
val mean: Double = -1.0,
|
||||
@SerialName("main_picture")
|
||||
val covers: MALMangaCovers,
|
||||
val covers: MALMangaCovers?,
|
||||
val status: String,
|
||||
@SerialName("media_type")
|
||||
val mediaType: String,
|
||||
|
@ -22,5 +22,6 @@ data class MALManga(
|
|||
|
||||
@Serializable
|
||||
data class MALMangaCovers(
|
||||
val large: String = "",
|
||||
val large: String?,
|
||||
val medium: String,
|
||||
)
|
||||
|
|
|
@ -74,12 +74,10 @@ class ShikimoriApi(
|
|||
.appendQueryParameter("search", search)
|
||||
.appendQueryParameter("limit", "20")
|
||||
.build()
|
||||
with(json) {
|
||||
authClient.newCall(GET(url.toString()))
|
||||
.awaitSuccess()
|
||||
.parseAs<List<SMManga>>()
|
||||
.map { it.toTrack(trackId) }
|
||||
}
|
||||
authClient.newCall(GET(url.toString()))
|
||||
.awaitSuccess()
|
||||
.parseAs<List<SMManga>>()
|
||||
.map { it.toTrack(trackId) }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -102,51 +100,44 @@ class ShikimoriApi(
|
|||
val urlMangas = "$API_URL/mangas".toUri().buildUpon()
|
||||
.appendPath(track.media_id.toString())
|
||||
.build()
|
||||
val manga = with(json) {
|
||||
val manga =
|
||||
authClient.newCall(GET(urlMangas.toString()))
|
||||
.awaitSuccess()
|
||||
.parseAs<SMManga>()
|
||||
}
|
||||
|
||||
val url = "$API_URL/v2/user_rates".toUri().buildUpon()
|
||||
.appendQueryParameter("user_id", user_id)
|
||||
.appendQueryParameter("target_id", track.media_id.toString())
|
||||
.appendQueryParameter("target_type", "Manga")
|
||||
.build()
|
||||
with(json) {
|
||||
authClient.newCall(GET(url.toString()))
|
||||
.execute()
|
||||
.parseAs<List<SMUserListEntry>>()
|
||||
.let { entries ->
|
||||
if (entries.size > 1) {
|
||||
throw Exception("Too manga manga in response")
|
||||
}
|
||||
entries
|
||||
.map { it.toTrack(trackId, manga) }
|
||||
.firstOrNull()
|
||||
authClient.newCall(GET(url.toString()))
|
||||
.execute()
|
||||
.parseAs<List<SMUserListEntry>>()
|
||||
.let { entries ->
|
||||
if (entries.size > 1) {
|
||||
throw Exception("Too manga manga in response")
|
||||
}
|
||||
}
|
||||
entries
|
||||
.map { it.toTrack(trackId, manga) }
|
||||
.firstOrNull()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getCurrentUser(): Int {
|
||||
return withIOContext {
|
||||
with(json) {
|
||||
authClient.newCall(GET("$API_URL/users/whoami"))
|
||||
.awaitSuccess()
|
||||
.parseAs<SMUser>()
|
||||
.id
|
||||
}
|
||||
authClient.newCall(GET("$API_URL/users/whoami"))
|
||||
.awaitSuccess()
|
||||
.parseAs<SMUser>()
|
||||
.id
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun accessToken(code: String): SMOAuth {
|
||||
return withIOContext {
|
||||
with(json) {
|
||||
client.newCall(accessTokenRequest(code))
|
||||
.awaitSuccess()
|
||||
.parseAs()
|
||||
}
|
||||
client.newCall(accessTokenRequest(code))
|
||||
.awaitSuccess()
|
||||
.parseAs()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -52,9 +52,7 @@ class TachideskApi {
|
|||
trackUrl
|
||||
}
|
||||
|
||||
val manga = with(json) {
|
||||
client.newCall(GET("$url/full", headers)).awaitSuccess().parseAs<MangaDataClass>()
|
||||
}
|
||||
val manga = client.newCall(GET("$url/full", headers)).awaitSuccess().parseAs<MangaDataClass>()
|
||||
|
||||
TrackSearch.create(TrackManager.SUWAYOMI).apply {
|
||||
title = manga.title
|
||||
|
@ -74,9 +72,7 @@ class TachideskApi {
|
|||
|
||||
suspend fun updateProgress(track: Track): Track {
|
||||
val url = track.tracking_url
|
||||
val chapters = with(json) {
|
||||
client.newCall(GET("$url/chapters", headers)).awaitSuccess().parseAs<List<ChapterDataClass>>()
|
||||
}
|
||||
val chapters = client.newCall(GET("$url/chapters", headers)).awaitSuccess().parseAs<List<ChapterDataClass>>()
|
||||
val lastChapterIndex = chapters.first { it.chapterNumber == track.last_chapter_read }.index
|
||||
|
||||
client.newCall(
|
||||
|
|
|
@ -11,12 +11,12 @@ import eu.kanade.tachiyomi.network.await
|
|||
import eu.kanade.tachiyomi.network.parseAs
|
||||
import eu.kanade.tachiyomi.util.system.localeContext
|
||||
import eu.kanade.tachiyomi.util.system.withIOContext
|
||||
import java.util.Date
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlinx.serialization.json.Json
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import yokai.domain.base.models.Version
|
||||
import java.util.*
|
||||
import java.util.concurrent.*
|
||||
|
||||
class AppUpdateChecker(
|
||||
private val json: Json = Injekt.get(),
|
||||
|
@ -31,48 +31,46 @@ class AppUpdateChecker(
|
|||
}
|
||||
|
||||
return withIOContext {
|
||||
val result = with(json) {
|
||||
if (preferences.checkForBetas().get()) {
|
||||
networkService.client
|
||||
.newCall(GET("https://api.github.com/repos/$GITHUB_REPO/releases"))
|
||||
.await()
|
||||
.parseAs<List<GithubRelease>>()
|
||||
.let { githubReleases ->
|
||||
val releases =
|
||||
githubReleases.take(10).filter { isNewVersion(it.version) }
|
||||
// Check if any of the latest versions are newer than the current version
|
||||
val release = releases
|
||||
.maxWithOrNull { r1, r2 ->
|
||||
when {
|
||||
r1.version == r2.version -> 0
|
||||
isNewVersion(r2.version, r1.version) -> -1
|
||||
else -> 1
|
||||
}
|
||||
val result = if (preferences.checkForBetas().get()) {
|
||||
networkService.client
|
||||
.newCall(GET("https://api.github.com/repos/$GITHUB_REPO/releases"))
|
||||
.await()
|
||||
.parseAs<List<GithubRelease>>()
|
||||
.let { githubReleases ->
|
||||
val releases =
|
||||
githubReleases.take(10).filter { isNewVersion(it.version) }
|
||||
// Check if any of the latest versions are newer than the current version
|
||||
val release = releases
|
||||
.maxWithOrNull { r1, r2 ->
|
||||
when {
|
||||
r1.version == r2.version -> 0
|
||||
isNewVersion(r2.version, r1.version) -> -1
|
||||
else -> 1
|
||||
}
|
||||
preferences.lastAppCheck().set(Date().time)
|
||||
|
||||
if (release != null) {
|
||||
AppUpdateResult.NewUpdate(release)
|
||||
} else {
|
||||
AppUpdateResult.NoNewUpdate
|
||||
}
|
||||
}
|
||||
} else {
|
||||
networkService.client
|
||||
.newCall(GET("https://api.github.com/repos/$GITHUB_REPO/releases/latest"))
|
||||
.await()
|
||||
.parseAs<GithubRelease>()
|
||||
.let {
|
||||
preferences.lastAppCheck().set(Date().time)
|
||||
preferences.lastAppCheck().set(Date().time)
|
||||
|
||||
// Check if latest version is newer than the current version
|
||||
if (isNewVersion(it.version)) {
|
||||
AppUpdateResult.NewUpdate(it)
|
||||
} else {
|
||||
AppUpdateResult.NoNewUpdate
|
||||
}
|
||||
if (release != null) {
|
||||
AppUpdateResult.NewUpdate(release)
|
||||
} else {
|
||||
AppUpdateResult.NoNewUpdate
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
networkService.client
|
||||
.newCall(GET("https://api.github.com/repos/$GITHUB_REPO/releases/latest"))
|
||||
.await()
|
||||
.parseAs<GithubRelease>()
|
||||
.let {
|
||||
preferences.lastAppCheck().set(Date().time)
|
||||
|
||||
// Check if latest version is newer than the current version
|
||||
if (isNewVersion(it.version)) {
|
||||
AppUpdateResult.NewUpdate(it)
|
||||
} else {
|
||||
AppUpdateResult.NoNewUpdate
|
||||
}
|
||||
}
|
||||
}
|
||||
if (doExtrasAfterNewUpdate && result is AppUpdateResult.NewUpdate) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S &&
|
||||
|
|
|
@ -14,6 +14,7 @@ import eu.kanade.tachiyomi.data.notification.NotificationHandler
|
|||
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import eu.kanade.tachiyomi.util.system.getResourceColor
|
||||
import eu.kanade.tachiyomi.util.system.notificationBuilder
|
||||
import eu.kanade.tachiyomi.util.system.notificationManager
|
||||
import yokai.i18n.MR
|
||||
import yokai.util.lang.getString
|
||||
|
@ -30,7 +31,7 @@ internal class AppUpdateNotifier(private val context: Context) {
|
|||
* Builder to manage notifications.
|
||||
*/
|
||||
val notificationBuilder by lazy {
|
||||
NotificationCompat.Builder(context, Notifications.CHANNEL_COMMON).apply {
|
||||
context.notificationBuilder(Notifications.CHANNEL_COMMON).apply {
|
||||
setSmallIcon(AR.drawable.stat_sys_download)
|
||||
setContentTitle(context.getString(MR.strings.app_name))
|
||||
}
|
||||
|
@ -232,7 +233,7 @@ internal class AppUpdateNotifier(private val context: Context) {
|
|||
addAction(
|
||||
R.drawable.ic_refresh_24dp,
|
||||
context.getString(MR.strings.retry),
|
||||
NotificationReceiver.startAppUpdatePendingJob(context, url),
|
||||
NotificationReceiver.dismissFailThenStartAppUpdatePendingJob(context, url),
|
||||
)
|
||||
// Cancel action
|
||||
addAction(
|
||||
|
|
|
@ -7,6 +7,7 @@ import android.os.Parcelable
|
|||
import co.touchlab.kermit.Logger
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.extension.api.ExtensionApi
|
||||
import eu.kanade.tachiyomi.extension.installer.ShizukuInstaller
|
||||
import eu.kanade.tachiyomi.extension.model.Extension
|
||||
import eu.kanade.tachiyomi.extension.model.InstallStep
|
||||
import eu.kanade.tachiyomi.extension.model.LoadResult
|
||||
|
@ -17,6 +18,9 @@ import eu.kanade.tachiyomi.source.Source
|
|||
import eu.kanade.tachiyomi.ui.extension.ExtensionIntallInfo
|
||||
import eu.kanade.tachiyomi.util.system.launchNow
|
||||
import eu.kanade.tachiyomi.util.system.withIOContext
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
@ -27,8 +31,6 @@ import uy.kohesive.injekt.Injekt
|
|||
import uy.kohesive.injekt.api.get
|
||||
import yokai.domain.base.BasePreferences
|
||||
import yokai.domain.extension.interactor.TrustExtension
|
||||
import java.util.*
|
||||
import java.util.concurrent.*
|
||||
|
||||
/**
|
||||
* The manager of extensions installed as another apk which extend the available sources. It handles
|
||||
|
|
|
@ -14,7 +14,6 @@ import eu.kanade.tachiyomi.util.system.withIOContext
|
|||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
@ -24,7 +23,6 @@ import yokai.domain.extension.repo.model.ExtensionRepo
|
|||
|
||||
internal class ExtensionApi {
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
private val networkService: NetworkHelper by injectLazy()
|
||||
private val getExtensionRepo: GetExtensionRepo by injectLazy()
|
||||
private val updateExtensionRepo: UpdateExtensionRepo by injectLazy()
|
||||
|
@ -47,11 +45,9 @@ internal class ExtensionApi {
|
|||
.newCall(GET("$repoBaseUrl/index.min.json"))
|
||||
.awaitSuccess()
|
||||
|
||||
with(json) {
|
||||
response
|
||||
.parseAs<List<ExtensionJsonObject>>()
|
||||
.toExtensions(repoBaseUrl)
|
||||
}
|
||||
response
|
||||
.parseAs<List<ExtensionJsonObject>>()
|
||||
.toExtensions(repoBaseUrl)
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(e) { "Failed to get extensions from $repoBaseUrl" }
|
||||
emptyList()
|
||||
|
|
|
@ -0,0 +1,122 @@
|
|||
package eu.kanade.tachiyomi.extension.installer
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.annotation.CallSuper
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||
import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||
import eu.kanade.tachiyomi.extension.util.ExtensionInstaller.Companion.EXTRA_DOWNLOAD_ID
|
||||
import java.util.Collections
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
abstract class Installer(
|
||||
internal val context: Context,
|
||||
// TODO: Remove finishedQueue
|
||||
internal val finishedQueue: (Installer) -> Unit,
|
||||
) {
|
||||
|
||||
private val extensionManager: ExtensionManager by injectLazy()
|
||||
|
||||
private var waitingInstall = AtomicReference<Entry>(null)
|
||||
private val queue = Collections.synchronizedList(mutableListOf<Entry>())
|
||||
|
||||
private val cancelReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
val downloadId = intent.getLongExtra(EXTRA_DOWNLOAD_ID, -1).takeIf { it >= 0 } ?: return
|
||||
cancelQueue(downloadId)
|
||||
}
|
||||
}
|
||||
|
||||
abstract var ready: Boolean
|
||||
|
||||
fun isInQueue(pkgName: String) = queue.any { it.pkgName == pkgName }
|
||||
|
||||
/**
|
||||
* Add an item to install queue.
|
||||
*
|
||||
* @param downloadId Download ID as known by [ExtensionManager]
|
||||
* @param uri Uri of APK to install
|
||||
*/
|
||||
fun addToQueue(downloadId: Long, pkgName: String, uri: Uri) {
|
||||
queue.add(Entry(downloadId, pkgName, uri))
|
||||
checkQueue()
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
open fun processEntry(entry: Entry) {
|
||||
extensionManager.setInstalling(entry.downloadId, entry.uri.hashCode())
|
||||
}
|
||||
|
||||
open fun cancelEntry(entry: Entry): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells the queue to continue processing the next entry and updates the install step
|
||||
* of the completed entry ([waitingInstall]) to [ExtensionManager].
|
||||
*
|
||||
* @param resultStep new install step for the processed entry.
|
||||
* @see waitingInstall
|
||||
*/
|
||||
fun continueQueue(succeeded: Boolean) {
|
||||
val completedEntry = waitingInstall.getAndSet(null)
|
||||
if (completedEntry != null) {
|
||||
extensionManager.setInstallationResult(completedEntry.downloadId, succeeded)
|
||||
checkQueue()
|
||||
}
|
||||
}
|
||||
|
||||
fun checkQueue() {
|
||||
if (!ready) {
|
||||
return
|
||||
}
|
||||
if (queue.isEmpty()) {
|
||||
finishedQueue(this)
|
||||
return
|
||||
}
|
||||
val nextEntry = queue.first()
|
||||
if (waitingInstall.compareAndSet(null, nextEntry)) {
|
||||
queue.removeAt(0)
|
||||
processEntry(nextEntry)
|
||||
}
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
open fun onDestroy() {
|
||||
LocalBroadcastManager.getInstance(context).unregisterReceiver(cancelReceiver)
|
||||
queue.forEach { extensionManager.setInstallationResult(it.pkgName, false) }
|
||||
queue.clear()
|
||||
waitingInstall.set(null)
|
||||
}
|
||||
|
||||
protected fun getActiveEntry(): Entry? = waitingInstall.get()
|
||||
|
||||
/**
|
||||
* Cancels queue for the provided download ID if exists.
|
||||
*
|
||||
* @param downloadId Download ID as known by [ExtensionManager]
|
||||
*/
|
||||
fun cancelQueue(downloadId: Long) {
|
||||
val waitingInstall = this.waitingInstall.get()
|
||||
val toCancel = queue.find { it.downloadId == downloadId } ?: waitingInstall ?: return
|
||||
if (cancelEntry(toCancel)) {
|
||||
queue.remove(toCancel)
|
||||
if (waitingInstall == toCancel) {
|
||||
// Currently processing removed entry, continue queue
|
||||
this.waitingInstall.set(null)
|
||||
checkQueue()
|
||||
}
|
||||
queue.forEach { extensionManager.setInstallationResult(it.downloadId, false) }
|
||||
// extensionManager.up(downloadId, InstallStep.Idle)
|
||||
}
|
||||
}
|
||||
|
||||
data class Entry(
|
||||
val downloadId: Long,
|
||||
val pkgName: String,
|
||||
val uri: Uri,
|
||||
)
|
||||
}
|
|
@ -1,21 +1,16 @@
|
|||
package eu.kanade.tachiyomi.extension
|
||||
package eu.kanade.tachiyomi.extension.installer
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Process
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||
import co.touchlab.kermit.Logger
|
||||
import eu.kanade.tachiyomi.R
|
||||
import yokai.i18n.MR
|
||||
import yokai.util.lang.getString
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import eu.kanade.tachiyomi.extension.util.ExtensionInstaller.Companion.EXTRA_DOWNLOAD_ID
|
||||
import eu.kanade.tachiyomi.util.system.getUriSize
|
||||
import eu.kanade.tachiyomi.util.system.isPackageInstalled
|
||||
import eu.kanade.tachiyomi.util.system.isShizukuInstalled
|
||||
import java.io.BufferedReader
|
||||
import java.io.InputStream
|
||||
import java.lang.reflect.Method
|
||||
import java.util.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
|
@ -23,38 +18,21 @@ import kotlinx.coroutines.cancel
|
|||
import kotlinx.coroutines.launch
|
||||
import rikka.shizuku.Shizuku
|
||||
import rikka.shizuku.ShizukuRemoteProcess
|
||||
import rikka.sui.Sui
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.BufferedReader
|
||||
import java.io.InputStream
|
||||
import java.lang.reflect.Method
|
||||
import java.util.*
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
import yokai.i18n.MR
|
||||
import yokai.util.lang.getString
|
||||
|
||||
class ShizukuInstaller(private val context: Context, val finishedQueue: (ShizukuInstaller) -> Unit) {
|
||||
class ShizukuInstaller(
|
||||
context: Context,
|
||||
finishedQueue: (Installer) -> Unit,
|
||||
) : Installer(context, finishedQueue) {
|
||||
|
||||
private val extensionManager: ExtensionManager by injectLazy()
|
||||
|
||||
private var waitingInstall = AtomicReference<Entry>(null)
|
||||
private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
|
||||
private val cancelReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
val downloadId = intent.getLongExtra(EXTRA_DOWNLOAD_ID, -1).takeIf { it >= 0 } ?: return
|
||||
cancelQueue(downloadId)
|
||||
}
|
||||
}
|
||||
|
||||
data class Entry(val downloadId: Long, val pkgName: String, val uri: Uri)
|
||||
private val queue = Collections.synchronizedList(mutableListOf<Entry>())
|
||||
|
||||
private val shizukuDeadListener = Shizuku.OnBinderDeadListener {
|
||||
Logger.d { "Shizuku was killed prematurely" }
|
||||
finishedQueue(this)
|
||||
}
|
||||
|
||||
fun isInQueue(pkgName: String) = queue.any { it.pkgName == pkgName }
|
||||
|
||||
private val shizukuPermissionListener = object : Shizuku.OnRequestPermissionResultListener {
|
||||
override fun onRequestPermissionResult(requestCode: Int, grantResult: Int) {
|
||||
if (requestCode == SHIZUKU_PERMISSION_REQUEST_CODE) {
|
||||
|
@ -69,13 +47,13 @@ class ShizukuInstaller(private val context: Context, val finishedQueue: (Shizuku
|
|||
}
|
||||
}
|
||||
|
||||
var ready = false
|
||||
override var ready = false
|
||||
|
||||
private val newProcess: Method
|
||||
|
||||
init {
|
||||
Shizuku.addBinderDeadListener(shizukuDeadListener)
|
||||
require(Shizuku.pingBinder() && (context.isPackageInstalled(shizukuPkgName) || Sui.isSui())) {
|
||||
require(Shizuku.pingBinder() && context.isShizukuInstalled) {
|
||||
finishedQueue(this)
|
||||
context.getString(MR.strings.ext_installer_shizuku_stopped)
|
||||
}
|
||||
|
@ -91,9 +69,8 @@ class ShizukuInstaller(private val context: Context, val finishedQueue: (Shizuku
|
|||
newProcess.isAccessible = true
|
||||
}
|
||||
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
fun processEntry(entry: Entry) {
|
||||
extensionManager.setInstalling(entry.downloadId, entry.uri.hashCode())
|
||||
override fun processEntry(entry: Entry) {
|
||||
super.processEntry(entry)
|
||||
ioScope.launch {
|
||||
var sessionId: String? = null
|
||||
try {
|
||||
|
@ -131,85 +108,14 @@ class ShizukuInstaller(private val context: Context, val finishedQueue: (Shizuku
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the queue. The provided service will be stopped if the queue is empty.
|
||||
* Will not be run when not ready.
|
||||
*
|
||||
* @see ready
|
||||
*/
|
||||
fun checkQueue() {
|
||||
if (!ready) {
|
||||
return
|
||||
}
|
||||
if (queue.isEmpty()) {
|
||||
finishedQueue(this)
|
||||
return
|
||||
}
|
||||
val nextEntry = queue.first()
|
||||
if (waitingInstall.compareAndSet(null, nextEntry)) {
|
||||
queue.removeAt(0)
|
||||
processEntry(nextEntry)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells the queue to continue processing the next entry and updates the install step
|
||||
* of the completed entry ([waitingInstall]) to [ExtensionManager].
|
||||
*
|
||||
* @param resultStep new install step for the processed entry.
|
||||
* @see waitingInstall
|
||||
*/
|
||||
fun continueQueue(succeeded: Boolean) {
|
||||
val completedEntry = waitingInstall.getAndSet(null)
|
||||
if (completedEntry != null) {
|
||||
extensionManager.setInstallationResult(completedEntry.downloadId, succeeded)
|
||||
checkQueue()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an item to install queue.
|
||||
*
|
||||
* @param downloadId Download ID as known by [ExtensionManager]
|
||||
* @param uri Uri of APK to install
|
||||
*/
|
||||
fun addToQueue(downloadId: Long, pkgName: String, uri: Uri) {
|
||||
queue.add(Entry(downloadId, pkgName, uri))
|
||||
checkQueue()
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels queue for the provided download ID if exists.
|
||||
*
|
||||
* @param downloadId Download ID as known by [ExtensionManager]
|
||||
*/
|
||||
private fun cancelQueue(downloadId: Long) {
|
||||
val waitingInstall = this.waitingInstall.get()
|
||||
val toCancel = queue.find { it.downloadId == downloadId } ?: waitingInstall ?: return
|
||||
if (cancelEntry(toCancel)) {
|
||||
queue.remove(toCancel)
|
||||
if (waitingInstall == toCancel) {
|
||||
// Currently processing removed entry, continue queue
|
||||
this.waitingInstall.set(null)
|
||||
checkQueue()
|
||||
}
|
||||
queue.forEach { extensionManager.setInstallationResult(it.downloadId, false) }
|
||||
// extensionManager.up(downloadId, InstallStep.Idle)
|
||||
}
|
||||
}
|
||||
|
||||
// Don't cancel if entry is already started installing
|
||||
fun cancelEntry(entry: Entry): Boolean = getActiveEntry() != entry
|
||||
fun getActiveEntry(): Entry? = waitingInstall.get()
|
||||
override fun cancelEntry(entry: Entry): Boolean = getActiveEntry() != entry
|
||||
|
||||
fun onDestroy() {
|
||||
override fun onDestroy() {
|
||||
Shizuku.removeBinderDeadListener(shizukuDeadListener)
|
||||
Shizuku.removeRequestPermissionResultListener(shizukuPermissionListener)
|
||||
ioScope.cancel()
|
||||
LocalBroadcastManager.getInstance(context).unregisterReceiver(cancelReceiver)
|
||||
queue.forEach { extensionManager.setInstallationResult(it.pkgName, false) }
|
||||
queue.clear()
|
||||
waitingInstall.set(null)
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun exec(command: String, stdin: InputStream? = null): ShellResult {
|
|
@ -14,7 +14,7 @@ import androidx.core.net.toUri
|
|||
import co.touchlab.kermit.Logger
|
||||
import eu.kanade.tachiyomi.extension.ExtensionInstallerJob
|
||||
import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||
import eu.kanade.tachiyomi.extension.ShizukuInstaller
|
||||
import eu.kanade.tachiyomi.extension.installer.ShizukuInstaller
|
||||
import eu.kanade.tachiyomi.extension.model.InstallStep
|
||||
import eu.kanade.tachiyomi.ui.extension.ExtensionIntallInfo
|
||||
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
||||
|
@ -22,6 +22,7 @@ import eu.kanade.tachiyomi.util.system.e
|
|||
import eu.kanade.tachiyomi.util.system.isPackageInstalled
|
||||
import eu.kanade.tachiyomi.util.system.launchUI
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import java.io.File
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
|
@ -47,7 +48,6 @@ import uy.kohesive.injekt.Injekt
|
|||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import yokai.domain.base.BasePreferences
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* The installer which installs, updates and uninstalls the extensions.
|
||||
|
|
|
@ -12,8 +12,6 @@ import eu.kanade.tachiyomi.source.model.SChapter
|
|||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
|
||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||
import eu.kanade.tachiyomi.util.storage.EpubFile
|
||||
import eu.kanade.tachiyomi.util.storage.fillMetadata
|
||||
import eu.kanade.tachiyomi.util.system.ImageUtil
|
||||
import eu.kanade.tachiyomi.util.system.extension
|
||||
import eu.kanade.tachiyomi.util.system.nameWithoutExtension
|
||||
|
@ -33,7 +31,8 @@ import nl.adaptivity.xmlutil.serialization.XML
|
|||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import yokai.core.archive.archiveReader
|
||||
import yokai.core.archive.util.archiveReader
|
||||
import yokai.core.archive.util.epubReader
|
||||
import yokai.core.metadata.COMIC_INFO_FILE
|
||||
import yokai.core.metadata.ComicInfo
|
||||
import yokai.core.metadata.copyFromComicInfo
|
||||
|
@ -42,6 +41,7 @@ import yokai.domain.chapter.services.ChapterRecognition
|
|||
import yokai.domain.source.SourcePreferences
|
||||
import yokai.domain.storage.StorageManager
|
||||
import yokai.i18n.MR
|
||||
import yokai.util.fillMetadata
|
||||
import yokai.util.lang.getString
|
||||
|
||||
class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSource {
|
||||
|
@ -69,11 +69,18 @@ class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSour
|
|||
.filter { !it.isDirectory }
|
||||
.firstOrNull { it.name == COMIC_INFO_FILE }
|
||||
|
||||
return if (localDetails != null) {
|
||||
decodeComicInfo(localDetails.openInputStream()).language?.value ?: "other"
|
||||
val lang = if (localDetails != null) {
|
||||
try {
|
||||
decodeComicInfo(localDetails.openInputStream()).language?.value
|
||||
} catch (e: Exception) {
|
||||
Logger.e(e) { "Unable to retrieve manga language" }
|
||||
null
|
||||
}
|
||||
} else {
|
||||
"other"
|
||||
null
|
||||
}
|
||||
|
||||
return lang ?: "other"
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -279,8 +286,9 @@ class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSour
|
|||
if (!directory.exists()) return
|
||||
|
||||
lang?.let { langMap[manga.url] = it }
|
||||
val file = directory.createFile(COMIC_INFO_FILE)!!
|
||||
file.writeText(xml.encodeToString(ComicInfo.serializer(), manga.toComicInfo(lang = lang)))
|
||||
directory.createFile(COMIC_INFO_FILE)?.let { file ->
|
||||
file.writeText(xml.encodeToString(ComicInfo.serializer(), manga.toComicInfo(lang = lang)))
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
|
@ -402,7 +410,7 @@ class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSour
|
|||
}
|
||||
}
|
||||
is Format.Epub -> {
|
||||
EpubFile(format.file.archiveReader(context)).use { epub ->
|
||||
format.file.epubReader(context).use { epub ->
|
||||
val entry = epub.getImagesFromPages().firstOrNull()
|
||||
|
||||
entry?.let { updateCover(manga, epub.getInputStream(it)!!, context) }
|
||||
|
@ -425,7 +433,7 @@ class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSour
|
|||
true
|
||||
}
|
||||
is Format.Epub -> {
|
||||
EpubFile(format.file.archiveReader(context)).use { epub ->
|
||||
format.file.epubReader(context).use { epub ->
|
||||
epub.fillMetadata(chapter, manga)
|
||||
}
|
||||
true
|
||||
|
|
|
@ -1,20 +1,13 @@
|
|||
package eu.kanade.tachiyomi.source
|
||||
|
||||
import android.content.Context
|
||||
import eu.kanade.tachiyomi.R
|
||||
import yokai.i18n.MR
|
||||
import yokai.util.lang.getString
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.DelegatedHttpSource
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.source.online.all.Cubari
|
||||
import eu.kanade.tachiyomi.source.online.all.MangaDex
|
||||
import eu.kanade.tachiyomi.source.online.english.KireiCake
|
||||
import eu.kanade.tachiyomi.source.online.english.MangaPlus
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
|
@ -24,7 +17,8 @@ import kotlinx.coroutines.flow.collectLatest
|
|||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import yokai.i18n.MR
|
||||
import yokai.util.lang.getString
|
||||
|
||||
class SourceManager(
|
||||
private val context: Context,
|
||||
|
@ -40,28 +34,8 @@ class SourceManager(
|
|||
val catalogueSources: Flow<List<CatalogueSource>> = sourcesMapFlow.map { it.values.filterIsInstance<CatalogueSource>() }
|
||||
val onlineSources: Flow<List<HttpSource>> = catalogueSources.map { it.filterIsInstance<HttpSource>() }
|
||||
|
||||
private val delegatedSources = listOf(
|
||||
DelegatedSource(
|
||||
"reader.kireicake.com",
|
||||
5509224355268673176,
|
||||
KireiCake(),
|
||||
),
|
||||
DelegatedSource(
|
||||
"mangadex.org",
|
||||
2499283573021220255,
|
||||
MangaDex(),
|
||||
),
|
||||
DelegatedSource(
|
||||
"mangaplus.shueisha.co.jp",
|
||||
1998944621602463790,
|
||||
MangaPlus(),
|
||||
),
|
||||
DelegatedSource(
|
||||
"cubari.moe",
|
||||
6338219619148105941,
|
||||
Cubari(),
|
||||
),
|
||||
).associateBy { it.sourceId }
|
||||
// FIXME: Delegated source, unused at the moment, J2K only delegate deep links
|
||||
private val delegatedSources = emptyList<DelegatedSource>().associateBy { it.sourceId }
|
||||
|
||||
init {
|
||||
scope.launch {
|
||||
|
@ -71,8 +45,8 @@ class SourceManager(
|
|||
extensions.forEach { extension ->
|
||||
extension.sources.forEach {
|
||||
mutableMap[it.id] = it
|
||||
delegatedSources[it.id]?.delegatedHttpSource?.delegate = it as? HttpSource
|
||||
// registerStubSource(it)
|
||||
//delegatedSources[it.id]?.delegatedHttpSource?.delegate = it as? HttpSource
|
||||
//registerStubSource(it)
|
||||
}
|
||||
}
|
||||
sourcesMapFlow.value = mutableMap
|
||||
|
|
|
@ -1,47 +0,0 @@
|
|||
package eu.kanade.tachiyomi.source.online
|
||||
|
||||
import android.net.Uri
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaImpl
|
||||
import eu.kanade.tachiyomi.data.database.models.create
|
||||
import eu.kanade.tachiyomi.domain.manga.models.Manga
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import yokai.domain.chapter.interactor.GetChapter
|
||||
import yokai.domain.manga.interactor.GetManga
|
||||
|
||||
abstract class DelegatedHttpSource {
|
||||
|
||||
var delegate: HttpSource? = null
|
||||
abstract val domainName: String
|
||||
|
||||
protected val getChapter: GetChapter by injectLazy()
|
||||
protected val getManga: GetManga by injectLazy()
|
||||
|
||||
protected val network: NetworkHelper by injectLazy()
|
||||
|
||||
abstract fun canOpenUrl(uri: Uri): Boolean
|
||||
abstract fun chapterUrl(uri: Uri): String?
|
||||
open fun pageNumber(uri: Uri): Int? = uri.pathSegments.lastOrNull()?.toIntOrNull()
|
||||
abstract suspend fun fetchMangaFromChapterUrl(uri: Uri): Triple<Chapter, Manga, List<SChapter>>?
|
||||
|
||||
protected open suspend fun getMangaInfo(url: String): Manga? {
|
||||
val id = delegate?.id ?: return null
|
||||
val manga = Manga.create(url, "", id)
|
||||
val networkManga = delegate?.getMangaDetails(manga.copy()) ?: return null
|
||||
val newManga = MangaImpl().apply {
|
||||
this.url = url
|
||||
title = try { networkManga.title } catch (e: Exception) { "" }
|
||||
source = id
|
||||
}
|
||||
newManga.copyFrom(networkManga)
|
||||
return newManga
|
||||
}
|
||||
|
||||
suspend fun getChapters(url: String): List<SChapter>? {
|
||||
val id = delegate?.id ?: return null
|
||||
val manga = Manga.create(url, "", id)
|
||||
return delegate?.getChapterList(manga)
|
||||
}
|
||||
}
|
|
@ -1,22 +1,31 @@
|
|||
package eu.kanade.tachiyomi.source.online.all
|
||||
|
||||
import android.net.Uri
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.toChapter
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.domain.manga.models.Manga
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.DelegatedHttpSource
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import java.util.Locale
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.withContext
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import yokai.domain.chapter.interactor.GetChapter
|
||||
import yokai.domain.manga.interactor.GetManga
|
||||
import yokai.i18n.MR
|
||||
import yokai.util.lang.getString
|
||||
|
||||
class Cubari : DelegatedHttpSource() {
|
||||
class Cubari(delegate: HttpSource) :
|
||||
DelegatedHttpSource(delegate) {
|
||||
|
||||
private val getManga: GetManga = Injekt.get()
|
||||
private val getChapter: GetChapter = Injekt.get()
|
||||
|
||||
override val lang = "all"
|
||||
|
||||
override val domainName: String = "cubari"
|
||||
override fun canOpenUrl(uri: Uri): Boolean = true
|
||||
|
||||
|
@ -24,24 +33,24 @@ class Cubari : DelegatedHttpSource() {
|
|||
|
||||
override fun pageNumber(uri: Uri): Int? = uri.pathSegments.getOrNull(4)?.toIntOrNull()
|
||||
|
||||
override suspend fun fetchMangaFromChapterUrl(uri: Uri): Triple<Chapter, Manga, List<SChapter>>? {
|
||||
override suspend fun fetchMangaFromChapterUrl(uri: Uri): Triple<SChapter, SManga, List<SChapter>>? {
|
||||
val cubariType = uri.pathSegments.getOrNull(1)?.lowercase(Locale.ROOT) ?: return null
|
||||
val cubariPath = uri.pathSegments.getOrNull(2) ?: return null
|
||||
val chapterNumber = uri.pathSegments.getOrNull(3)?.replace("-", ".")?.toFloatOrNull() ?: return null
|
||||
val mangaUrl = "/read/$cubariType/$cubariPath"
|
||||
return withContext(Dispatchers.IO) {
|
||||
val deferredManga = async {
|
||||
getManga.awaitByUrlAndSource(mangaUrl, delegate?.id!!) ?: getMangaInfo(mangaUrl)
|
||||
getManga.awaitByUrlAndSource(mangaUrl, delegate.id) ?: getMangaDetailsByUrl(mangaUrl)
|
||||
}
|
||||
val deferredChapters = async {
|
||||
getManga.awaitByUrlAndSource(mangaUrl, delegate?.id!!)?.let { manga ->
|
||||
getManga.awaitByUrlAndSource(mangaUrl, delegate.id)?.let { manga ->
|
||||
val chapters = getChapter.awaitAll(manga, false)
|
||||
val chapter = findChapter(chapters, cubariType, chapterNumber)
|
||||
if (chapter != null) {
|
||||
return@async chapters
|
||||
}
|
||||
}
|
||||
getChapters(mangaUrl)
|
||||
getChapterListByUrl(mangaUrl)
|
||||
}
|
||||
val manga = deferredManga.await()
|
||||
val chapters = deferredChapters.await()
|
||||
|
@ -50,11 +59,7 @@ class Cubari : DelegatedHttpSource() {
|
|||
?: error(
|
||||
context.getString(MR.strings.chapter_not_found),
|
||||
)
|
||||
if (manga != null) {
|
||||
Triple(trueChapter, manga, chapters.orEmpty())
|
||||
} else {
|
||||
null
|
||||
}
|
||||
Triple(trueChapter, manga, chapters)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
package eu.kanade.tachiyomi.source.online.all
|
||||
|
||||
import android.net.Uri
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.toChapter
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.domain.manga.models.Manga
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.await
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.DelegatedHttpSource
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import java.util.Locale
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
|
@ -20,10 +20,15 @@ import okhttp3.CacheControl
|
|||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import yokai.domain.manga.interactor.GetManga
|
||||
import yokai.i18n.MR
|
||||
import yokai.util.lang.getString
|
||||
|
||||
class MangaDex : DelegatedHttpSource() {
|
||||
class MangaDex(delegate: HttpSource) : DelegatedHttpSource(delegate) {
|
||||
|
||||
private val getManga: GetManga = Injekt.get()
|
||||
|
||||
override val lang: String = "all"
|
||||
|
||||
override val domainName: String = "mangadex"
|
||||
|
||||
|
@ -42,13 +47,13 @@ class MangaDex : DelegatedHttpSource() {
|
|||
return uri.pathSegments.getOrNull(2)?.toIntOrNull()
|
||||
}
|
||||
|
||||
override suspend fun fetchMangaFromChapterUrl(uri: Uri): Triple<Chapter, Manga, List<SChapter>>? {
|
||||
override suspend fun fetchMangaFromChapterUrl(uri: Uri): Triple<SChapter, SManga, List<SChapter>>? {
|
||||
val url = chapterUrl(uri) ?: return null
|
||||
val request =
|
||||
GET("https:///api.mangadex.org/v2$url", delegate!!.headers, CacheControl.FORCE_NETWORK)
|
||||
val response = network.client.newCall(request).await()
|
||||
if (response.code != 200) throw Exception("HTTP error ${response.code}")
|
||||
val body = response.body.string().orEmpty()
|
||||
val body = response.body.string()
|
||||
if (body.isEmpty()) {
|
||||
throw Exception("Null Response")
|
||||
}
|
||||
|
@ -56,28 +61,19 @@ class MangaDex : DelegatedHttpSource() {
|
|||
val jsonObject = Json.decodeFromString<MangaDexChapterData>(body)
|
||||
val dataObject = jsonObject.data ?: throw Exception("Chapter not found")
|
||||
val mangaId = dataObject.mangaId ?: throw Exception("No manga associated with chapter")
|
||||
val langCode = getRealLangCode(dataObject.language ?: "en").uppercase(Locale.getDefault())
|
||||
// Use the correct MangaDex source based on the language code, or the api will not return
|
||||
// the correct chapter list
|
||||
delegate = sourceManager.getOnlineSources().find { it.toString() == "MangaDex ($langCode)" }
|
||||
?: error("Source not found")
|
||||
val mangaUrl = "/manga/$mangaId/"
|
||||
return withContext(Dispatchers.IO) {
|
||||
val deferredManga = async {
|
||||
getManga.awaitByUrlAndSource(mangaUrl, delegate?.id!!) ?: getMangaInfo(mangaUrl)
|
||||
getManga.awaitByUrlAndSource(mangaUrl, delegate.id) ?: getMangaDetailsByUrl(mangaUrl)
|
||||
}
|
||||
val deferredChapters = async { getChapters(mangaUrl) }
|
||||
val deferredChapters = async { getChapterListByUrl(mangaUrl) }
|
||||
val manga = deferredManga.await()
|
||||
val chapters = deferredChapters.await()
|
||||
val context = Injekt.get<PreferencesHelper>().context
|
||||
val trueChapter = chapters?.find { it.url == "/api$url" }?.toChapter() ?: error(
|
||||
val trueChapter = chapters.find { it.url == "/api$url" }?.toChapter() ?: error(
|
||||
context.getString(MR.strings.chapter_not_found),
|
||||
)
|
||||
if (manga != null) {
|
||||
Triple(trueChapter, manga, chapters.orEmpty())
|
||||
} else {
|
||||
null
|
||||
}
|
||||
Triple(trueChapter, manga, chapters)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,110 +0,0 @@
|
|||
package eu.kanade.tachiyomi.source.online.english
|
||||
|
||||
import android.net.Uri
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaImpl
|
||||
import eu.kanade.tachiyomi.data.database.models.toChapter
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.domain.manga.models.Manga
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.network.await
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.online.DelegatedHttpSource
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.Request
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import yokai.i18n.MR
|
||||
import yokai.util.lang.getString
|
||||
|
||||
open class FoolSlide(override val domainName: String, private val urlModifier: String = "") :
|
||||
DelegatedHttpSource
|
||||
() {
|
||||
|
||||
override fun canOpenUrl(uri: Uri): Boolean = true
|
||||
|
||||
override fun chapterUrl(uri: Uri): String? {
|
||||
val offset = if (urlModifier.isEmpty()) 0 else 1
|
||||
val mangaName = uri.pathSegments.getOrNull(1 + offset) ?: return null
|
||||
val lang = uri.pathSegments.getOrNull(2 + offset) ?: return null
|
||||
val volume = uri.pathSegments.getOrNull(3 + offset) ?: return null
|
||||
val chapterNumber = uri.pathSegments.getOrNull(4 + offset) ?: return null
|
||||
val subChapterNumber = uri.pathSegments.getOrNull(5 + offset)?.toIntOrNull()?.toString()
|
||||
return "$urlModifier/read/" + listOfNotNull(
|
||||
mangaName,
|
||||
lang,
|
||||
volume,
|
||||
chapterNumber,
|
||||
subChapterNumber,
|
||||
).joinToString("/") + "/"
|
||||
}
|
||||
|
||||
override fun pageNumber(uri: Uri): Int? {
|
||||
val count = uri.pathSegments.count()
|
||||
if (count > 2 && uri.pathSegments[count - 2] == "page") {
|
||||
return super.pageNumber(uri)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
override suspend fun fetchMangaFromChapterUrl(uri: Uri): Triple<Chapter, Manga, List<SChapter>>? {
|
||||
val offset = if (urlModifier.isEmpty()) 0 else 1
|
||||
val mangaName = uri.pathSegments.getOrNull(1 + offset) ?: return null
|
||||
var chapterNumber = uri.pathSegments.getOrNull(4 + offset) ?: return null
|
||||
val subChapterNumber = uri.pathSegments.getOrNull(5 + offset)?.toIntOrNull()
|
||||
if (subChapterNumber != null) {
|
||||
chapterNumber += ".$subChapterNumber"
|
||||
}
|
||||
return withContext(Dispatchers.IO) {
|
||||
val mangaUrl = "$urlModifier/series/$mangaName/"
|
||||
val sourceId = delegate?.id ?: return@withContext null
|
||||
val deferredManga = async {
|
||||
getManga.awaitByUrlAndSource(mangaUrl, sourceId) ?: getManga(mangaUrl)
|
||||
}
|
||||
val chapterUrl = chapterUrl(uri)
|
||||
val deferredChapters = async { getChapters(mangaUrl) }
|
||||
val manga = deferredManga.await()
|
||||
val chapters = deferredChapters.await()
|
||||
val context = Injekt.get<PreferencesHelper>().context
|
||||
val trueChapter = chapters?.find { it.url == chapterUrl }?.toChapter() ?: error(
|
||||
context.getString(MR.strings.chapter_not_found),
|
||||
)
|
||||
if (manga != null) Triple(trueChapter, manga, chapters) else null
|
||||
}
|
||||
}
|
||||
|
||||
open suspend fun getManga(url: String): Manga? {
|
||||
val request = GET("${delegate!!.baseUrl}$url")
|
||||
val document = network.client.newCall(allowAdult(request)).await().asJsoup()
|
||||
val mangaDetailsInfoSelector = "div.info"
|
||||
val infoElement = document.select(mangaDetailsInfoSelector).first()?.text() ?: return null
|
||||
return MangaImpl().apply {
|
||||
this.url = url
|
||||
source = delegate?.id ?: -1
|
||||
title = infoElement.substringAfter("Title:").substringBefore("Author:").trim()
|
||||
author = infoElement.substringAfter("Author:").substringBefore("Artist:").trim()
|
||||
artist = infoElement.substringAfter("Artist:").substringBefore("Synopsis:").trim()
|
||||
description = infoElement.substringAfter("Synopsis:").trim()
|
||||
thumbnail_url = document.select("div.thumbnail img").firstOrNull()?.attr("abs:src")?.trim()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform a GET request into a POST request that automatically authorizes all adult content
|
||||
*/
|
||||
private fun allowAdult(request: Request) = allowAdult(request.url.toString())
|
||||
|
||||
private fun allowAdult(url: String): Request {
|
||||
return POST(
|
||||
url,
|
||||
body = FormBody.Builder()
|
||||
.add("adult", "true")
|
||||
.build(),
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
package eu.kanade.tachiyomi.source.online.english
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaImpl
|
||||
import eu.kanade.tachiyomi.domain.manga.models.Manga
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.await
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import eu.kanade.tachiyomi.util.lang.capitalizeWords
|
||||
|
||||
class KireiCake : FoolSlide("kireicake") {
|
||||
|
||||
override suspend fun getManga(url: String): Manga? {
|
||||
val request = GET("${delegate!!.baseUrl}$url")
|
||||
val document = network.client.newCall(request).await().asJsoup()
|
||||
val mangaDetailsInfoSelector = "div.info"
|
||||
return MangaImpl().apply {
|
||||
this.url = url
|
||||
source = delegate?.id ?: -1
|
||||
title = document.select("$mangaDetailsInfoSelector li:has(b:contains(title))").first()
|
||||
?.ownText()?.substringAfter(":")?.trim()
|
||||
?: url.split("/").last().replace("_", " " + "").capitalizeWords()
|
||||
description =
|
||||
document.select("$mangaDetailsInfoSelector li:has(b:contains(description))").first()
|
||||
?.ownText()?.substringAfter(":")
|
||||
thumbnail_url = document.select("div.thumbnail img").firstOrNull()?.attr("abs:src")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,24 +1,31 @@
|
|||
package eu.kanade.tachiyomi.source.online.english
|
||||
|
||||
import android.net.Uri
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.toChapter
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.domain.manga.models.Manga
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.await
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.DelegatedHttpSource
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.CacheControl
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import yokai.domain.manga.interactor.GetManga
|
||||
import yokai.i18n.MR
|
||||
import yokai.util.lang.getString
|
||||
|
||||
class MangaPlus : DelegatedHttpSource() {
|
||||
class MangaPlus(delegate: HttpSource) :
|
||||
DelegatedHttpSource(delegate) {
|
||||
|
||||
private val getManga: GetManga = Injekt.get()
|
||||
|
||||
override val lang: String get() = delegate.lang
|
||||
|
||||
override val domainName: String = "jumpg-webapi.tokyo-cdn"
|
||||
|
||||
private val titleIdRegex =
|
||||
|
@ -34,11 +41,11 @@ class MangaPlus : DelegatedHttpSource() {
|
|||
|
||||
override fun pageNumber(uri: Uri): Int? = null
|
||||
|
||||
override suspend fun fetchMangaFromChapterUrl(uri: Uri): Triple<Chapter, Manga, List<SChapter>>? {
|
||||
override suspend fun fetchMangaFromChapterUrl(uri: Uri): Triple<SChapter, SManga, List<SChapter>>? {
|
||||
val url = chapterUrl(uri) ?: return null
|
||||
val request = GET(
|
||||
chapterUrlTemplate.replace("##", uri.pathSegments[1]),
|
||||
delegate!!.headers,
|
||||
delegate.headers,
|
||||
CacheControl.FORCE_NETWORK,
|
||||
)
|
||||
return withContext(Dispatchers.IO) {
|
||||
|
@ -53,26 +60,22 @@ class MangaPlus : DelegatedHttpSource() {
|
|||
val trimmedTitle = title.substring(0, title.length - 1)
|
||||
val mangaUrl = "#/titles/$titleId"
|
||||
val deferredManga = async {
|
||||
getManga.awaitByUrlAndSource(mangaUrl, delegate?.id!!) ?: getMangaInfo(mangaUrl)
|
||||
getManga.awaitByUrlAndSource(mangaUrl, delegate.id) ?: getMangaDetailsByUrl(mangaUrl)
|
||||
}
|
||||
val deferredChapters = async { getChapters(mangaUrl) }
|
||||
val deferredChapters = async { getChapterListByUrl(mangaUrl) }
|
||||
val manga = deferredManga.await()
|
||||
val chapters = deferredChapters.await()
|
||||
val context = Injekt.get<PreferencesHelper>().context
|
||||
val trueChapter = chapters?.find { it.url == url }?.toChapter() ?: error(
|
||||
val trueChapter = chapters.find { it.url == url }?.toChapter() ?: error(
|
||||
context.getString(MR.strings.chapter_not_found),
|
||||
)
|
||||
if (manga != null) {
|
||||
Triple(
|
||||
trueChapter,
|
||||
manga.apply {
|
||||
this.title = trimmedTitle
|
||||
},
|
||||
chapters,
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
Triple(
|
||||
trueChapter,
|
||||
manga.apply {
|
||||
this.title = trimmedTitle
|
||||
},
|
||||
chapters,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,19 +5,26 @@ import android.view.LayoutInflater
|
|||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.compose.ui.platform.ViewCompositionStrategy
|
||||
import eu.kanade.tachiyomi.util.compose.LocalBackPress
|
||||
import eu.kanade.tachiyomi.util.compose.LocalDialogHostState
|
||||
import yokai.domain.DialogHostState
|
||||
import yokai.presentation.theme.YokaiTheme
|
||||
|
||||
abstract class BaseComposeController(bundle: Bundle? = null) :
|
||||
BaseController(bundle) {
|
||||
|
||||
override val shouldHideLegacyAppBar = true
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup,
|
||||
savedViewState: Bundle?
|
||||
): View {
|
||||
hideLegacyAppBar()
|
||||
setAppBarVisibility()
|
||||
return ComposeView(container.context).apply {
|
||||
layoutParams = ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
|
@ -25,8 +32,14 @@ abstract class BaseComposeController(bundle: Bundle? = null) :
|
|||
)
|
||||
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
|
||||
setContent {
|
||||
val dialogHostState = remember { DialogHostState() }
|
||||
YokaiTheme {
|
||||
ScreenContent()
|
||||
CompositionLocalProvider(
|
||||
LocalDialogHostState provides dialogHostState,
|
||||
LocalBackPress provides router::handleBack,
|
||||
) {
|
||||
ScreenContent()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,6 +25,8 @@ import kotlinx.coroutines.cancel
|
|||
abstract class BaseController(bundle: Bundle? = null) :
|
||||
Controller(bundle), BackHandlerControllerInterface, BaseControllerPreferenceControllerCommonInterface {
|
||||
|
||||
abstract val shouldHideLegacyAppBar: Boolean
|
||||
|
||||
lateinit var viewScope: CoroutineScope
|
||||
var isDragging = false
|
||||
|
||||
|
@ -37,20 +39,20 @@ abstract class BaseController(bundle: Bundle? = null) :
|
|||
|
||||
override fun preCreateView(controller: Controller) {
|
||||
viewScope = MainScope()
|
||||
Logger.d { "Create view for ${controller.instance()}" }
|
||||
Logger.v { "Create view for ${controller.instance()}" }
|
||||
}
|
||||
|
||||
override fun preAttach(controller: Controller, view: View) {
|
||||
Logger.d { "Attach view for ${controller.instance()}" }
|
||||
Logger.v { "Attach view for ${controller.instance()}" }
|
||||
}
|
||||
|
||||
override fun preDetach(controller: Controller, view: View) {
|
||||
Logger.d { "Detach view for ${controller.instance()}" }
|
||||
Logger.v { "Detach view for ${controller.instance()}" }
|
||||
}
|
||||
|
||||
override fun preDestroyView(controller: Controller, view: View) {
|
||||
viewScope.cancel()
|
||||
Logger.d { "Destroy view for ${controller.instance()}" }
|
||||
Logger.v { "Destroy view for ${controller.instance()}" }
|
||||
}
|
||||
},
|
||||
)
|
||||
|
@ -58,6 +60,10 @@ abstract class BaseController(bundle: Bundle? = null) :
|
|||
|
||||
open fun onViewCreated(view: View) { }
|
||||
|
||||
internal fun setAppBarVisibility() {
|
||||
if (shouldHideLegacyAppBar) hideLegacyAppBar() else showLegacyAppBar()
|
||||
}
|
||||
|
||||
override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
|
||||
if (type.isEnter && !isControllerVisible) {
|
||||
view?.alpha = 0f
|
||||
|
|
|
@ -20,11 +20,13 @@ import eu.kanade.tachiyomi.util.view.isControllerVisible
|
|||
abstract class BaseLegacyController<VB : ViewBinding>(bundle: Bundle? = null) :
|
||||
BaseController(bundle) {
|
||||
|
||||
override val shouldHideLegacyAppBar = false
|
||||
|
||||
lateinit var binding: VB
|
||||
val isBindingInitialized get() = this::binding.isInitialized
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup, savedViewState: Bundle?): View {
|
||||
showLegacyAppBar()
|
||||
setAppBarVisibility()
|
||||
binding = createBinding(inflater)
|
||||
binding.root.backgroundColor = binding.root.context.getResourceColor(R.attr.background)
|
||||
return binding.root
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
package eu.kanade.tachiyomi.ui.base.presenter
|
||||
|
||||
import androidx.annotation.CallSuper
|
||||
import java.lang.ref.WeakReference
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import java.lang.ref.WeakReference
|
||||
|
||||
open class BaseCoroutinePresenter<T> {
|
||||
var presenterScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||
|
@ -24,6 +25,7 @@ open class BaseCoroutinePresenter<T> {
|
|||
open fun onCreate() {
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
open fun onDestroy() {
|
||||
presenterScope.cancel()
|
||||
weakView = null
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
package eu.kanade.tachiyomi.ui.base.presenter
|
||||
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
|
||||
/**
|
||||
* Presenter that mimic [cafe.adriel.voyager.core.model.StateScreenModel] for easier migration.
|
||||
* Temporary class while we're migrating to Compose.
|
||||
*/
|
||||
abstract class StateCoroutinePresenter<S, C>(initialState: S) : BaseCoroutinePresenter<C>() {
|
||||
|
||||
protected val mutableState: MutableStateFlow<S> = MutableStateFlow(initialState)
|
||||
val state: StateFlow<S> = mutableState.asStateFlow()
|
||||
}
|
|
@ -4,7 +4,9 @@ import eu.kanade.tachiyomi.data.download.DownloadManager
|
|||
import eu.kanade.tachiyomi.data.download.model.Download
|
||||
import eu.kanade.tachiyomi.data.download.model.DownloadQueue
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BaseCoroutinePresenter
|
||||
import eu.kanade.tachiyomi.util.system.launchUI
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
@ -12,7 +14,8 @@ import uy.kohesive.injekt.injectLazy
|
|||
/**
|
||||
* Presenter of [DownloadBottomSheet].
|
||||
*/
|
||||
class DownloadBottomPresenter : BaseCoroutinePresenter<DownloadBottomSheet>() {
|
||||
class DownloadBottomPresenter : BaseCoroutinePresenter<DownloadBottomSheet>(),
|
||||
DownloadQueue.Listener {
|
||||
|
||||
/**
|
||||
* Download manager.
|
||||
|
@ -20,15 +23,27 @@ class DownloadBottomPresenter : BaseCoroutinePresenter<DownloadBottomSheet>() {
|
|||
val downloadManager: DownloadManager by injectLazy()
|
||||
var items = listOf<DownloadHeaderItem>()
|
||||
|
||||
override val progressJobs = mutableMapOf<Download, Job>()
|
||||
override val queueListenerScope get() = presenterScope
|
||||
|
||||
/**
|
||||
* Property to get the queue from the download manager.
|
||||
*/
|
||||
val downloadQueue: DownloadQueue
|
||||
get() = downloadManager.queue
|
||||
val downloadQueueState
|
||||
get() = downloadManager.queueState
|
||||
|
||||
override fun onCreate() {
|
||||
presenterScope.launchUI {
|
||||
downloadManager.statusFlow().collect(::onStatusChange)
|
||||
}
|
||||
presenterScope.launchUI {
|
||||
downloadManager.progressFlow().collect(::onPageProgressUpdate)
|
||||
}
|
||||
}
|
||||
|
||||
fun getItems() {
|
||||
presenterScope.launch {
|
||||
val items = downloadQueue
|
||||
val items = downloadQueueState.value
|
||||
.groupBy { it.source }
|
||||
.map { entry ->
|
||||
DownloadHeaderItem(entry.key.id, entry.key.name, entry.value.size).apply {
|
||||
|
@ -85,4 +100,22 @@ class DownloadBottomPresenter : BaseCoroutinePresenter<DownloadBottomSheet>() {
|
|||
fun cancelDownloads(downloads: List<Download>) {
|
||||
downloadManager.deletePendingDownloads(*downloads.toTypedArray())
|
||||
}
|
||||
|
||||
override fun onStatusChange(download: Download) {
|
||||
super.onStatusChange(download)
|
||||
view?.update(downloadManager.isRunning)
|
||||
}
|
||||
|
||||
override fun onQueueUpdate(download: Download) {
|
||||
view?.onUpdateDownloadedPages(download)
|
||||
}
|
||||
|
||||
override fun onProgressUpdate(download: Download) {
|
||||
view?.onUpdateProgress(download)
|
||||
}
|
||||
|
||||
override fun onPageProgressUpdate(download: Download) {
|
||||
super.onPageProgressUpdate(download)
|
||||
view?.onUpdateDownloadedPages(download)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,8 @@ import android.content.Context
|
|||
import android.util.AttributeSet
|
||||
import android.view.MenuItem
|
||||
import android.widget.LinearLayout
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.FileDownloadOff
|
||||
import androidx.core.view.WindowInsetsCompat.Type.systemBars
|
||||
import androidx.core.view.isInvisible
|
||||
import androidx.core.view.updateLayoutParams
|
||||
|
@ -117,14 +119,14 @@ class DownloadBottomSheet @JvmOverloads constructor(
|
|||
fun update(isRunning: Boolean) {
|
||||
presenter.getItems()
|
||||
onQueueStatusChange(isRunning)
|
||||
if (binding.downloadFab.isInvisible != presenter.downloadQueue.isEmpty()) {
|
||||
binding.downloadFab.isInvisible = presenter.downloadQueue.isEmpty()
|
||||
if (binding.downloadFab.isInvisible != presenter.downloadQueueState.value.isEmpty()) {
|
||||
binding.downloadFab.isInvisible = presenter.downloadQueueState.value.isEmpty()
|
||||
}
|
||||
prepareMenu()
|
||||
}
|
||||
|
||||
private fun updateDLTitle() {
|
||||
val extCount = presenter.downloadQueue.firstOrNull()
|
||||
val extCount = presenter.downloadQueueState.value.firstOrNull()
|
||||
binding.titleText.text = if (extCount != null) {
|
||||
context.getString(
|
||||
MR.strings.downloading_,
|
||||
|
@ -143,8 +145,8 @@ class DownloadBottomSheet @JvmOverloads constructor(
|
|||
private fun onQueueStatusChange(running: Boolean) {
|
||||
val oldRunning = isRunning
|
||||
isRunning = running
|
||||
if (binding.downloadFab.isInvisible != presenter.downloadQueue.isEmpty()) {
|
||||
binding.downloadFab.isInvisible = presenter.downloadQueue.isEmpty()
|
||||
if (binding.downloadFab.isInvisible != presenter.downloadQueueState.value.isEmpty()) {
|
||||
binding.downloadFab.isInvisible = presenter.downloadQueueState.value.isEmpty()
|
||||
}
|
||||
updateFab()
|
||||
if (oldRunning != running) {
|
||||
|
@ -210,9 +212,9 @@ class DownloadBottomSheet @JvmOverloads constructor(
|
|||
private fun setInformationView() {
|
||||
updateDLTitle()
|
||||
setBottomSheet()
|
||||
if (presenter.downloadQueue.isEmpty()) {
|
||||
if (presenter.downloadQueueState.value.isEmpty()) {
|
||||
binding.emptyView.show(
|
||||
R.drawable.ic_download_off_24dp,
|
||||
Icons.Filled.FileDownloadOff,
|
||||
MR.strings.nothing_is_downloading,
|
||||
)
|
||||
} else {
|
||||
|
@ -224,10 +226,10 @@ class DownloadBottomSheet @JvmOverloads constructor(
|
|||
val menu = binding.sheetToolbar.menu
|
||||
updateFab()
|
||||
// Set clear button visibility.
|
||||
menu.findItem(R.id.clear_queue)?.isVisible = !presenter.downloadQueue.isEmpty()
|
||||
menu.findItem(R.id.clear_queue)?.isVisible = presenter.downloadQueueState.value.isNotEmpty()
|
||||
|
||||
// Set reorder button visibility.
|
||||
menu.findItem(R.id.reorder)?.isVisible = !presenter.downloadQueue.isEmpty()
|
||||
menu.findItem(R.id.reorder)?.isVisible = presenter.downloadQueueState.value.isNotEmpty()
|
||||
}
|
||||
|
||||
private fun updateFab() {
|
||||
|
@ -274,7 +276,7 @@ class DownloadBottomSheet @JvmOverloads constructor(
|
|||
}
|
||||
|
||||
private fun setBottomSheet() {
|
||||
val hasQueue = presenter.downloadQueue.isNotEmpty()
|
||||
val hasQueue = presenter.downloadQueueState.value.isNotEmpty()
|
||||
if (hasQueue) {
|
||||
sheetBehavior?.skipCollapsed = !hasQueue
|
||||
if (sheetBehavior.isHidden()) sheetBehavior?.collapse()
|
||||
|
@ -320,7 +322,6 @@ class DownloadBottomSheet @JvmOverloads constructor(
|
|||
}
|
||||
}
|
||||
presenter.reorder(downloads)
|
||||
controller?.updateChapterDownload(download, false)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -68,7 +68,7 @@ class DownloadHolder(private val view: View, val adapter: DownloadAdapter) :
|
|||
if (binding.downloadProgress.max == 1) {
|
||||
binding.downloadProgress.max = pages.size * 100
|
||||
}
|
||||
binding.downloadProgress.progress = download.pageProgress
|
||||
binding.downloadProgress.setProgressCompat(download.pageProgress, true)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -2,8 +2,6 @@ package eu.kanade.tachiyomi.ui.extension
|
|||
|
||||
import android.content.pm.PackageInstaller
|
||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||
import eu.kanade.tachiyomi.data.download.model.Download
|
||||
import eu.kanade.tachiyomi.data.download.model.DownloadQueue
|
||||
import eu.kanade.tachiyomi.extension.ExtensionInstallerJob
|
||||
import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||
import eu.kanade.tachiyomi.extension.model.Extension
|
||||
|
@ -12,7 +10,6 @@ import eu.kanade.tachiyomi.extension.model.InstalledExtensionsOrder
|
|||
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
|
||||
import eu.kanade.tachiyomi.ui.migration.BaseMigrationPresenter
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
import eu.kanade.tachiyomi.util.system.launchUI
|
||||
import eu.kanade.tachiyomi.util.system.withUIContext
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
|
@ -31,7 +28,7 @@ typealias ExtensionIntallInfo = Pair<InstallStep, PackageInstaller.SessionInfo?>
|
|||
/**
|
||||
* Presenter of [ExtensionBottomSheet].
|
||||
*/
|
||||
class ExtensionBottomPresenter : BaseMigrationPresenter<ExtensionBottomSheet>(), DownloadQueue.DownloadListener {
|
||||
class ExtensionBottomPresenter : BaseMigrationPresenter<ExtensionBottomSheet>() {
|
||||
|
||||
private var extensions = emptyList<ExtensionItem>()
|
||||
|
||||
|
@ -43,7 +40,7 @@ class ExtensionBottomPresenter : BaseMigrationPresenter<ExtensionBottomSheet>(),
|
|||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
downloadManager.addListener(this)
|
||||
|
||||
presenterScope.launch {
|
||||
val extensionJob = async {
|
||||
extensionManager.findAvailableExtensions()
|
||||
|
@ -289,11 +286,4 @@ class ExtensionBottomPresenter : BaseMigrationPresenter<ExtensionBottomSheet>(),
|
|||
extensionManager.trust(pkgName, versionCode, signatureHash)
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateDownload(download: Download) = updateDownloads()
|
||||
override fun updateDownloads() {
|
||||
presenterScope.launchUI {
|
||||
view?.updateDownloadStatus(!downloadManager.isPaused())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -521,8 +521,4 @@ class ExtensionBottomSheet @JvmOverloads constructor(context: Context, attrs: At
|
|||
return if (index == -1) POSITION_NONE else index
|
||||
}
|
||||
}
|
||||
|
||||
fun updateDownloadStatus(isRunning: Boolean) {
|
||||
(controller.activity as? MainActivity)?.downloadStatusChanged(isRunning)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,15 +13,15 @@ import eu.kanade.tachiyomi.util.lang.removeArticles
|
|||
import eu.kanade.tachiyomi.util.system.isLTR
|
||||
import eu.kanade.tachiyomi.util.system.timeSpanFromNow
|
||||
import eu.kanade.tachiyomi.util.system.withDefContext
|
||||
import java.util.*
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import yokai.domain.ui.UiPreferences
|
||||
import yokai.i18n.MR
|
||||
import yokai.util.lang.getString
|
||||
import java.util.*
|
||||
import yokai.domain.category.interactor.GetCategories
|
||||
import yokai.domain.chapter.interactor.GetChapter
|
||||
import yokai.domain.history.interactor.GetHistory
|
||||
import yokai.domain.ui.UiPreferences
|
||||
import yokai.i18n.MR
|
||||
import yokai.util.lang.getString
|
||||
|
||||
/**
|
||||
* Adapter storing a list of manga in a certain category.
|
||||
|
@ -117,8 +117,8 @@ class LibraryCategoryAdapter(val controller: LibraryController?) :
|
|||
*/
|
||||
fun indexOf(manga: Manga): Int {
|
||||
return currentItems.indexOfFirst {
|
||||
if (it is LibraryItem) {
|
||||
it.manga.id == manga.id
|
||||
if (it is LibraryMangaItem) {
|
||||
it.manga.manga.id == manga.id
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
@ -142,7 +142,7 @@ class LibraryCategoryAdapter(val controller: LibraryController?) :
|
|||
*/
|
||||
fun allIndexOf(manga: Manga): List<Int> {
|
||||
return currentItems.mapIndexedNotNull { index, it ->
|
||||
if (it is LibraryItem && it.manga.id == manga.id) {
|
||||
if (it is LibraryMangaItem && it.manga.manga.id == manga.id) {
|
||||
index
|
||||
} else {
|
||||
null
|
||||
|
@ -164,7 +164,7 @@ class LibraryCategoryAdapter(val controller: LibraryController?) :
|
|||
} else {
|
||||
val filteredManga = withDefContext { mangas.filter { it.filter(s) } }
|
||||
if (filteredManga.isEmpty() && controller?.presenter?.showAllCategories == false) {
|
||||
val catId = mangas.firstOrNull()?.let { it.header?.catId ?: it.manga.category }
|
||||
val catId = (mangas.firstOrNull() as? LibraryMangaItem)?.let { it.header?.catId ?: it.manga.category }
|
||||
val blankItem = catId?.let { controller.presenter.blankItem(it) }
|
||||
updateDataSet(blankItem ?: emptyList())
|
||||
} else {
|
||||
|
@ -173,6 +173,7 @@ class LibraryCategoryAdapter(val controller: LibraryController?) :
|
|||
}
|
||||
isLongPressDragEnabled = libraryListener?.canDrag() == true && s.isNullOrBlank()
|
||||
setItemsPerCategoryMap()
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
private fun getFirstLetter(name: String): String {
|
||||
|
@ -202,18 +203,19 @@ class LibraryCategoryAdapter(val controller: LibraryController?) :
|
|||
vibrateOnCategoryChange(item.category.name)
|
||||
item.category.name
|
||||
}
|
||||
is LibraryItem -> {
|
||||
val text = if (item.manga.isBlank()) {
|
||||
return item.header?.category?.name.orEmpty()
|
||||
} else {
|
||||
is LibraryPlaceholderItem -> {
|
||||
item.header?.category?.name.orEmpty()
|
||||
}
|
||||
is LibraryMangaItem -> {
|
||||
val text =
|
||||
when (getSort(position)) {
|
||||
LibrarySort.DragAndDrop -> {
|
||||
if (item.header.category.isDynamic && item.manga.id != null) {
|
||||
if (item.header.category.isDynamic && item.manga.manga.id != null) {
|
||||
// FIXME: Don't do blocking
|
||||
val category = runBlocking { getCategories.awaitByMangaId(item.manga.id!!) }.firstOrNull()?.name
|
||||
val category = runBlocking { getCategories.awaitByMangaId(item.manga.manga.id!!) }.firstOrNull()?.name
|
||||
category ?: context.getString(MR.strings.default_value)
|
||||
} else {
|
||||
val title = item.manga.title
|
||||
val title = item.manga.manga.title
|
||||
if (preferences.removeArticles().get()) {
|
||||
title.removeArticles().chop(15)
|
||||
} else {
|
||||
|
@ -222,14 +224,14 @@ class LibraryCategoryAdapter(val controller: LibraryController?) :
|
|||
}
|
||||
}
|
||||
LibrarySort.DateFetched -> {
|
||||
val id = item.manga.id ?: return ""
|
||||
val id = item.manga.manga.id ?: return ""
|
||||
// FIXME: Don't do blocking
|
||||
val history = runBlocking { getChapter.awaitAll(id, false) }
|
||||
val last = history.maxOfOrNull { it.date_fetch }
|
||||
context.timeSpanFromNow(MR.strings.fetched_, last ?: 0)
|
||||
}
|
||||
LibrarySort.LastRead -> {
|
||||
val id = item.manga.id ?: return ""
|
||||
val id = item.manga.manga.id ?: return ""
|
||||
// FIXME: Don't do blocking
|
||||
val history = runBlocking { getHistory.awaitAllByMangaId(id) }
|
||||
val last = history.maxOfOrNull { it.last_read }
|
||||
|
@ -256,21 +258,23 @@ class LibraryCategoryAdapter(val controller: LibraryController?) :
|
|||
}
|
||||
}
|
||||
LibrarySort.LatestChapter -> {
|
||||
context.timeSpanFromNow(MR.strings.updated_, item.manga.last_update)
|
||||
context.timeSpanFromNow(MR.strings.updated_, item.manga.manga.last_update)
|
||||
}
|
||||
LibrarySort.DateAdded -> {
|
||||
context.timeSpanFromNow(MR.strings.added_, item.manga.date_added)
|
||||
context.timeSpanFromNow(MR.strings.added_, item.manga.manga.date_added)
|
||||
}
|
||||
LibrarySort.Title -> {
|
||||
val title = if (preferences.removeArticles().get()) {
|
||||
item.manga.title.removeArticles()
|
||||
item.manga.manga.title.removeArticles()
|
||||
} else {
|
||||
item.manga.title
|
||||
item.manga.manga.title
|
||||
}
|
||||
getFirstLetter(title)
|
||||
}
|
||||
LibrarySort.Random -> {
|
||||
context.getString(MR.strings.random)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!isSingleCategory) {
|
||||
vibrateOnCategoryChange(item.header?.category?.name.orEmpty())
|
||||
}
|
||||
|
|
|
@ -28,6 +28,8 @@ import android.widget.ImageView
|
|||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.view.ActionMode
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.HeartBroken
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.core.animation.doOnEnd
|
||||
import androidx.core.view.ViewCompat
|
||||
|
@ -61,7 +63,6 @@ import eu.kanade.tachiyomi.R
|
|||
import eu.kanade.tachiyomi.core.preference.Preference
|
||||
import eu.kanade.tachiyomi.data.database.models.Category
|
||||
import eu.kanade.tachiyomi.data.database.models.LibraryManga
|
||||
import eu.kanade.tachiyomi.data.download.DownloadJob
|
||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
|
||||
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
|
@ -136,7 +137,6 @@ import kotlin.math.roundToInt
|
|||
import kotlin.random.Random
|
||||
import kotlin.random.nextInt
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.drop
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
|
@ -451,7 +451,7 @@ open class LibraryController(
|
|||
|
||||
private fun setActiveCategory() {
|
||||
val currentCategory = presenter.categories.indexOfFirst {
|
||||
if (presenter.showAllCategories) it.order == activeCategory else presenter.currentCategory == it.id
|
||||
if (presenter.showAllCategories) it.order == activeCategory else presenter.currentCategoryId == it.id
|
||||
}
|
||||
if (currentCategory > -1) {
|
||||
binding.categoryRecycler.setCategories(currentCategory)
|
||||
|
@ -521,14 +521,13 @@ open class LibraryController(
|
|||
}
|
||||
|
||||
private fun openRandomManga(global: Boolean) {
|
||||
val items = if (global) {
|
||||
presenter.allLibraryItems
|
||||
} else {
|
||||
adapter.currentItems
|
||||
}.filter { (it is LibraryItem && !it.manga.isBlank() && !it.manga.isHidden() && (!it.manga.initialized || it.manga.unread > 0)) }
|
||||
val items =
|
||||
if (global) { presenter.currentLibraryItems } else { adapter.currentItems }
|
||||
.filterIsInstance<LibraryMangaItem>()
|
||||
.filter { !it.manga.manga.initialized || it.manga.unread > 0 }
|
||||
if (items.isNotEmpty()) {
|
||||
val item = items.random() as LibraryItem
|
||||
openManga(item.manga)
|
||||
val item = items.random() as LibraryMangaItem
|
||||
openManga(item.manga.manga)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -559,7 +558,7 @@ open class LibraryController(
|
|||
}
|
||||
presenter.groupType = item
|
||||
shouldScrollToTop = true
|
||||
presenter.getLibrary()
|
||||
presenter.updateLibrary()
|
||||
true
|
||||
}.show()
|
||||
}
|
||||
|
@ -617,10 +616,12 @@ open class LibraryController(
|
|||
setPreferenceFlows()
|
||||
LibraryUpdateJob.updateFlow.onEach(::onUpdateManga).launchIn(viewScope)
|
||||
viewScope.launchUI {
|
||||
LibraryUpdateJob.isRunningFlow(view.context).collectLatest {
|
||||
val holder = if (mAdapter != null) visibleHeaderHolder() else null
|
||||
val category = holder?.category ?: return@collectLatest
|
||||
holder.notifyStatus(LibraryUpdateJob.categoryInQueue(category.id), category)
|
||||
LibraryUpdateJob.isRunningFlow(view.context).collect {
|
||||
adapter.getHeaderPositions().forEach {
|
||||
val holder = (binding.libraryGridRecycler.recycler.findViewHolderForAdapterPosition(it) as? LibraryHeaderHolder) ?: return@forEach
|
||||
val category = holder.category ?: return@forEach
|
||||
holder.notifyStatus(LibraryUpdateJob.categoryInQueue(category.id), category)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -660,7 +661,7 @@ open class LibraryController(
|
|||
createActionModeIfNeeded()
|
||||
}
|
||||
|
||||
if (presenter.libraryItems.isNotEmpty() && !isSubClass) {
|
||||
if (presenter.libraryItemsToDisplay.isNotEmpty() && !isSubClass) {
|
||||
presenter.restoreLibrary()
|
||||
if (justStarted) {
|
||||
val activityBinding = activityBinding ?: return
|
||||
|
@ -704,7 +705,7 @@ open class LibraryController(
|
|||
if (!LibraryUpdateJob.isRunning(context)) {
|
||||
when {
|
||||
!presenter.showAllCategories && presenter.groupType == BY_DEFAULT -> {
|
||||
presenter.findCurrentCategory()?.let {
|
||||
presenter.currentCategory?.let {
|
||||
updateLibrary(it)
|
||||
}
|
||||
}
|
||||
|
@ -902,7 +903,7 @@ open class LibraryController(
|
|||
}
|
||||
} else {
|
||||
val newOffset =
|
||||
presenter.categories.indexOfFirst { presenter.currentCategory == it.id } +
|
||||
presenter.categories.indexOfFirst { presenter.currentCategoryId == it.id } +
|
||||
(if (next) 1 else -1)
|
||||
if (if (!next) {
|
||||
newOffset > -1
|
||||
|
@ -1011,7 +1012,7 @@ open class LibraryController(
|
|||
override fun getSpanSize(position: Int): Int {
|
||||
if (libraryLayout == LibraryItem.LAYOUT_LIST) return managerSpanCount
|
||||
val item = this@LibraryController.mAdapter?.getItem(position)
|
||||
return if (item is LibraryHeaderItem || item is SearchGlobalItem || (item is LibraryItem && item.manga.isBlank())) {
|
||||
return if (item is LibraryHeaderItem || item is SearchGlobalItem || item is LibraryPlaceholderItem) {
|
||||
managerSpanCount
|
||||
} else {
|
||||
1
|
||||
|
@ -1056,10 +1057,9 @@ open class LibraryController(
|
|||
if (type.isEnter) {
|
||||
binding.filterBottomSheet.filterBottomSheet.isVisible = true
|
||||
if (type == ControllerChangeType.POP_ENTER) {
|
||||
presenter.getLibrary()
|
||||
presenter.updateLibrary()
|
||||
isPoppingIn = true
|
||||
}
|
||||
DownloadJob.callListeners()
|
||||
binding.recyclerCover.isClickable = false
|
||||
binding.recyclerCover.isFocusable = false
|
||||
singleCategory = presenter.categories.size <= 1
|
||||
|
@ -1096,7 +1096,7 @@ open class LibraryController(
|
|||
if (!isBindingInitialized) return
|
||||
updateFilterSheetY()
|
||||
if (observeLater) {
|
||||
presenter.getLibrary()
|
||||
presenter.updateLibrary()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1136,7 +1136,7 @@ open class LibraryController(
|
|||
binding.emptyView.hide()
|
||||
} else {
|
||||
binding.emptyView.show(
|
||||
R.drawable.ic_heart_off_24dp,
|
||||
Icons.Filled.HeartBroken,
|
||||
if (hasActiveFilters) {
|
||||
MR.strings.no_matches_for_filters
|
||||
} else {
|
||||
|
@ -1375,7 +1375,7 @@ open class LibraryController(
|
|||
setActiveCategory()
|
||||
return
|
||||
}
|
||||
val headerPosition = adapter.indexOf(pos)
|
||||
val headerPosition = mAdapter?.indexOf(pos) ?: return
|
||||
if (headerPosition > -1) {
|
||||
val activityBinding = activityBinding ?: return
|
||||
val index = adapter.headerItems.indexOf(adapter.getItem(headerPosition))
|
||||
|
@ -1409,7 +1409,7 @@ open class LibraryController(
|
|||
|
||||
private fun onRefresh() {
|
||||
showCategories(false)
|
||||
presenter.getLibrary()
|
||||
presenter.updateLibrary()
|
||||
destroyActionModeIfNeeded()
|
||||
}
|
||||
|
||||
|
@ -1433,14 +1433,14 @@ open class LibraryController(
|
|||
val isShowAllCategoriesSet = preferences.showAllCategories().get()
|
||||
if (!query.isNullOrBlank() && this.query.isBlank() && !isShowAllCategoriesSet) {
|
||||
presenter.forceShowAllCategories = preferences.showAllCategoriesWhenSearchingSingleCategory().get()
|
||||
presenter.getLibrary()
|
||||
presenter.updateLibrary()
|
||||
} else if (query.isNullOrBlank() && this.query.isNotBlank() && !isShowAllCategoriesSet) {
|
||||
if (!isSubClass) {
|
||||
preferences.showAllCategoriesWhenSearchingSingleCategory()
|
||||
.set(presenter.forceShowAllCategories)
|
||||
}
|
||||
presenter.forceShowAllCategories = false
|
||||
presenter.getLibrary()
|
||||
presenter.updateLibrary()
|
||||
}
|
||||
|
||||
if (query != this.query && !query.isNullOrBlank()) {
|
||||
|
@ -1458,7 +1458,7 @@ open class LibraryController(
|
|||
adapter.removeAllScrollableHeaders()
|
||||
}
|
||||
adapter.setFilter(query)
|
||||
if (presenter.allLibraryItems.isEmpty()) return true
|
||||
if (presenter.currentLibraryItems.isEmpty()) return true
|
||||
viewScope.launchUI {
|
||||
adapter.performFilterAsync()
|
||||
}
|
||||
|
@ -1477,7 +1477,6 @@ open class LibraryController(
|
|||
}
|
||||
|
||||
private fun setSelection(manga: Manga, selected: Boolean) {
|
||||
if (manga.isBlank()) return
|
||||
val currentMode = adapter.mode
|
||||
if (selected) {
|
||||
if (selectedMangas.add(manga)) {
|
||||
|
@ -1527,7 +1526,7 @@ open class LibraryController(
|
|||
toggleSelection(position)
|
||||
return
|
||||
}
|
||||
val manga = (adapter.getItem(position) as? LibraryItem)?.manga ?: return
|
||||
val manga = (adapter.getItem(position) as? LibraryMangaItem)?.manga?.manga ?: return
|
||||
val activity = activity ?: return
|
||||
val chapter = presenter.getFirstUnread(manga) ?: return
|
||||
activity.apply {
|
||||
|
@ -1543,9 +1542,8 @@ open class LibraryController(
|
|||
}
|
||||
|
||||
private fun toggleSelection(position: Int) {
|
||||
val item = adapter.getItem(position) as? LibraryItem ?: return
|
||||
if (item.manga.isBlank()) return
|
||||
setSelection(item.manga, !adapter.isSelected(position))
|
||||
val item = adapter.getItem(position) as? LibraryMangaItem ?: return
|
||||
setSelection(item.manga.manga, !adapter.isSelected(position))
|
||||
invalidateActionMode()
|
||||
}
|
||||
|
||||
|
@ -1561,14 +1559,14 @@ open class LibraryController(
|
|||
* @return true if the item should be selected, false otherwise.
|
||||
*/
|
||||
override fun onItemClick(view: View?, position: Int): Boolean {
|
||||
val item = adapter.getItem(position) as? LibraryItem ?: return false
|
||||
val item = adapter.getItem(position) as? LibraryMangaItem ?: return false
|
||||
return if (adapter.mode == SelectableAdapter.Mode.MULTI) {
|
||||
snack?.dismiss()
|
||||
lastClickPosition = position
|
||||
toggleSelection(position)
|
||||
false
|
||||
} else {
|
||||
openManga(item.manga)
|
||||
openManga(item.manga.manga)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
@ -1590,10 +1588,10 @@ open class LibraryController(
|
|||
*/
|
||||
override fun onItemLongClick(position: Int) {
|
||||
val item = adapter.getItem(position)
|
||||
if (item !is LibraryItem) return
|
||||
if (item !is LibraryMangaItem) return
|
||||
snack?.dismiss()
|
||||
if (libraryLayout == LibraryItem.LAYOUT_COVER_ONLY_GRID && actionMode == null) {
|
||||
snack = view?.snack(item.manga.title) {
|
||||
snack = view?.snack(item.manga.manga.title) {
|
||||
anchorView = activityBinding?.bottomNav
|
||||
view.elevation = 15f.dpToPx
|
||||
}
|
||||
|
@ -1642,14 +1640,14 @@ open class LibraryController(
|
|||
if (mangaId == null) {
|
||||
adapter.getHeaderPositions().forEach { adapter.notifyItemChanged(it) }
|
||||
} else {
|
||||
presenter.updateManga()
|
||||
presenter.updateLibrary()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setSelection(position: Int, selected: Boolean = true) {
|
||||
val item = adapter.getItem(position) as? LibraryItem ?: return
|
||||
val item = adapter.getItem(position) as? LibraryMangaItem ?: return
|
||||
|
||||
setSelection(item.manga, selected)
|
||||
setSelection(item.manga.manga, selected)
|
||||
invalidateActionMode()
|
||||
}
|
||||
|
||||
|
@ -1673,7 +1671,7 @@ open class LibraryController(
|
|||
|
||||
override fun shouldMoveItem(fromPosition: Int, toPosition: Int): Boolean {
|
||||
if (adapter.isSelected(fromPosition)) toggleSelection(fromPosition)
|
||||
val item = adapter.getItem(fromPosition) as? LibraryItem ?: return false
|
||||
val item = adapter.getItem(fromPosition) as? LibraryMangaItem ?: return false
|
||||
val newHeader = adapter.getSectionHeader(toPosition) as? LibraryHeaderItem
|
||||
if (toPosition < 1) return false
|
||||
return (adapter.getItem(toPosition) !is LibraryHeaderItem) && (
|
||||
|
@ -1688,18 +1686,18 @@ open class LibraryController(
|
|||
lastItem = null
|
||||
isDragging = false
|
||||
binding.swipeRefresh.isEnabled = true
|
||||
if (adapter.selectedItemCount > 0) {
|
||||
if (mAdapter == null || adapter.selectedItemCount > 0) {
|
||||
lastItemPosition = null
|
||||
return
|
||||
}
|
||||
destroyActionModeIfNeeded()
|
||||
// if nothing moved
|
||||
if (lastItemPosition == null) return
|
||||
val item = adapter.getItem(position) as? LibraryItem ?: return
|
||||
val item = adapter.getItem(position) as? LibraryMangaItem ?: return
|
||||
val newHeader = adapter.getSectionHeader(position) as? LibraryHeaderItem
|
||||
val libraryItems = getSectionItems(adapter.getSectionHeader(position), item)
|
||||
.filterIsInstance<LibraryItem>()
|
||||
val mangaIds = libraryItems.mapNotNull { (it as? LibraryItem)?.manga?.id }
|
||||
.filterIsInstance<LibraryMangaItem>()
|
||||
val mangaIds = libraryItems.mapNotNull { (it as? LibraryMangaItem)?.manga?.manga?.id }
|
||||
if (newHeader?.category?.id == item.manga.category) {
|
||||
presenter.rearrangeCategory(item.manga.category, mangaIds)
|
||||
} else {
|
||||
|
@ -1821,7 +1819,7 @@ open class LibraryController(
|
|||
val category = (adapter.getItem(position) as? LibraryHeaderItem)?.category ?: return
|
||||
if (!category.isDynamic) {
|
||||
ManageCategoryDialog(category) {
|
||||
presenter.getLibrary()
|
||||
presenter.updateLibrary()
|
||||
}.showDialog(router)
|
||||
}
|
||||
}
|
||||
|
@ -1831,8 +1829,8 @@ open class LibraryController(
|
|||
if (category?.isDynamic == false && sortBy == LibrarySort.DragAndDrop.categoryValue) {
|
||||
val item = adapter.findCategoryHeader(catId) ?: return
|
||||
val libraryItems = adapter.getSectionItems(item)
|
||||
.filterIsInstance<LibraryItem>()
|
||||
val mangaIds = libraryItems.mapNotNull { (it as? LibraryItem)?.manga?.id }
|
||||
.filterIsInstance<LibraryMangaItem>()
|
||||
val mangaIds = libraryItems.mapNotNull { (it as? LibraryMangaItem)?.manga?.manga?.id }
|
||||
presenter.rearrangeCategory(catId, mangaIds)
|
||||
} else {
|
||||
presenter.sortCategory(catId, sortBy)
|
||||
|
@ -1914,7 +1912,7 @@ open class LibraryController(
|
|||
isGone = true
|
||||
setOnClickListener {
|
||||
presenter.forceShowAllCategories = !presenter.forceShowAllCategories
|
||||
presenter.getLibrary()
|
||||
presenter.updateLibrary()
|
||||
isSelected = presenter.forceShowAllCategories
|
||||
}
|
||||
val pad = 12.dpToPx
|
||||
|
@ -2194,13 +2192,9 @@ open class LibraryController(
|
|||
val activity = activity ?: return
|
||||
viewScope.launchIO {
|
||||
selectedMangas.toList().moveCategories(activity) {
|
||||
presenter.getLibrary()
|
||||
presenter.updateLibrary()
|
||||
destroyActionModeIfNeeded()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateDownloadStatus(isRunning: Boolean) {
|
||||
(activity as? MainActivity)?.downloadStatusChanged(isRunning)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@ import eu.kanade.tachiyomi.util.system.getResourceColor
|
|||
import eu.kanade.tachiyomi.util.view.backgroundColor
|
||||
import eu.kanade.tachiyomi.util.view.setCards
|
||||
import eu.kanade.tachiyomi.widget.AutofitRecyclerView
|
||||
import yokai.presentation.core.util.coil.loadManga
|
||||
import yokai.util.coil.loadManga
|
||||
|
||||
/**
|
||||
* Class used to hold the displayed data of a manga in the library, like the cover or the title.
|
||||
|
@ -64,23 +64,24 @@ class LibraryGridHolder(
|
|||
* @param item the manga item to bind.
|
||||
*/
|
||||
override fun onSetValues(item: LibraryItem) {
|
||||
if (item !is LibraryMangaItem) throw IllegalStateException("Only LibraryMangaItem can use grid holder")
|
||||
// Update the title and subtitle of the manga.
|
||||
setCards(adapter.showOutline, binding.card, binding.unreadDownloadBadge.root)
|
||||
binding.playButton.transitionName = "library chapter $bindingAdapterPosition transition"
|
||||
binding.constraintLayout.isVisible = !item.manga.isBlank()
|
||||
binding.title.text = item.manga.title.highlightText(item.filter, color)
|
||||
binding.behindTitle.text = item.manga.title
|
||||
val mangaColor = item.manga.dominantCoverColors
|
||||
binding.constraintLayout.isVisible = item.manga.manga.id != Long.MIN_VALUE
|
||||
binding.title.text = item.manga.manga.title.highlightText(item.filter, color)
|
||||
binding.behindTitle.text = item.manga.manga.title
|
||||
val mangaColor = item.manga.manga.dominantCoverColors
|
||||
binding.coverConstraint.backgroundColor = mangaColor?.first ?: itemView.context.getResourceColor(R.attr.background)
|
||||
binding.behindTitle.setTextColor(
|
||||
mangaColor?.second ?: itemView.context.getResourceColor(R.attr.colorOnBackground),
|
||||
)
|
||||
val authorArtist = if (item.manga.author == item.manga.artist || item.manga.artist.isNullOrBlank()) {
|
||||
item.manga.author?.trim() ?: ""
|
||||
val authorArtist = if (item.manga.manga.author == item.manga.manga.artist || item.manga.manga.artist.isNullOrBlank()) {
|
||||
item.manga.manga.author?.trim() ?: ""
|
||||
} else {
|
||||
listOfNotNull(
|
||||
item.manga.author?.trim()?.takeIf { it.isNotBlank() },
|
||||
item.manga.artist?.trim()?.takeIf { it.isNotBlank() },
|
||||
item.manga.manga.author?.trim()?.takeIf { it.isNotBlank() },
|
||||
item.manga.manga.artist?.trim()?.takeIf { it.isNotBlank() },
|
||||
).joinToString(", ")
|
||||
}
|
||||
binding.subtitle.text = authorArtist.highlightText(item.filter, color)
|
||||
|
@ -101,7 +102,7 @@ class LibraryGridHolder(
|
|||
|
||||
// Update the cover.
|
||||
binding.coverThumbnail.dispose()
|
||||
setCover(item.manga)
|
||||
setCover(item.manga.manga)
|
||||
}
|
||||
|
||||
override fun toggleActivation() {
|
||||
|
|
|
@ -32,14 +32,17 @@ import eu.kanade.tachiyomi.util.system.getResourceColor
|
|||
import eu.kanade.tachiyomi.util.view.compatToolTipText
|
||||
import eu.kanade.tachiyomi.util.view.setText
|
||||
import eu.kanade.tachiyomi.util.view.text
|
||||
import kotlin.random.Random
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import yokai.domain.library.LibraryPreferences
|
||||
import yokai.i18n.MR
|
||||
import yokai.util.lang.getString
|
||||
|
||||
class LibraryHeaderHolder(val view: View, val adapter: LibraryCategoryAdapter) :
|
||||
BaseFlexibleViewHolder(view, adapter, true) {
|
||||
|
||||
private val libraryPreferences: LibraryPreferences = Injekt.get()
|
||||
private val binding = LibraryCategoryHeaderItemBinding.bind(view)
|
||||
val progressDrawableStart = CircularProgressDrawable(itemView.context)
|
||||
val progressDrawableEnd = CircularProgressDrawable(itemView.context)
|
||||
|
@ -183,7 +186,17 @@ class LibraryHeaderHolder(val view: View, val adapter: LibraryCategoryAdapter) :
|
|||
|
||||
binding.categoryTitle.text = categoryName +
|
||||
if (adapter.showNumber) {
|
||||
" (${adapter.itemsPerCategory[item.catId]})"
|
||||
val filteredCount = adapter.currentItems.count {
|
||||
it is LibraryMangaItem && it.header?.catId == item.catId
|
||||
}
|
||||
val totalCount = adapter.itemsPerCategory[item.catId] ?: 0
|
||||
val searchText = adapter.getFilter(String::class.java)
|
||||
var countText = if (searchText.isNullOrBlank()) {
|
||||
" ($totalCount)"
|
||||
} else {
|
||||
" ($filteredCount/$totalCount)"
|
||||
}
|
||||
countText
|
||||
} else { "" }
|
||||
if (category.sourceId != null) {
|
||||
val icon = adapter.sourceManager.get(category.sourceId!!)?.icon()
|
||||
|
@ -198,7 +211,7 @@ class LibraryHeaderHolder(val view: View, val adapter: LibraryCategoryAdapter) :
|
|||
|
||||
val isAscending = category.isAscending()
|
||||
val sortingMode = category.sortingMode()
|
||||
val sortDrawable = getSortRes(sortingMode, isAscending, R.drawable.ic_sort_24dp)
|
||||
val sortDrawable = getSortRes(sortingMode, isAscending, category.isDynamic, false)
|
||||
|
||||
binding.categorySort.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, sortDrawable, 0)
|
||||
binding.categorySort.setText(category.sortRes())
|
||||
|
@ -260,81 +273,80 @@ class LibraryHeaderHolder(val view: View, val adapter: LibraryCategoryAdapter) :
|
|||
}
|
||||
|
||||
private fun showCatSortOptions() {
|
||||
if (category == null) return
|
||||
val cat = category ?: return
|
||||
adapter.controller?.activity?.let { activity ->
|
||||
val items = LibrarySort.entries.map { it.menuSheetItem(category!!.isDynamic) }
|
||||
val sortingMode = category!!.sortingMode(true)
|
||||
val items = LibrarySort.entries.map { it.menuSheetItem(cat.isDynamic) }
|
||||
val sortingMode = cat.sortingMode() ?: if (!cat.isDynamic) LibrarySort.DragAndDrop else null
|
||||
val sheet = MaterialMenuSheet(
|
||||
activity,
|
||||
items,
|
||||
activity.getString(MR.strings.sort_by),
|
||||
sortingMode?.mainValue,
|
||||
) { sheet, item ->
|
||||
onCatSortClicked(category!!, item)
|
||||
onCatSortClicked(cat, item)
|
||||
val nCategory = (adapter.getItem(flexibleAdapterPosition) as? LibraryHeaderItem)?.category
|
||||
val isAscending = nCategory?.isAscending() ?: false
|
||||
val drawableRes = getSortRes(item, isAscending)
|
||||
sheet.setDrawable(item, drawableRes)
|
||||
sheet.updateSortIcon(nCategory, LibrarySort.valueOf(item))
|
||||
false
|
||||
}
|
||||
val isAscending = category!!.isAscending()
|
||||
val drawableRes = getSortRes(sortingMode, isAscending)
|
||||
sheet.setDrawable(sortingMode?.mainValue ?: -1, drawableRes)
|
||||
sheet.updateSortIcon(cat, sortingMode)
|
||||
sheet.show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun MaterialMenuSheet.updateSortIcon(category: Category?, sortingMode: LibrarySort?) {
|
||||
val isAscending = category?.isAscending() ?: false
|
||||
val drawableRes = getSortRes(sortingMode, isAscending, category?.isDynamic ?: false, true)
|
||||
this.setDrawable(sortingMode?.mainValue ?: -1, drawableRes)
|
||||
}
|
||||
|
||||
private fun getSortRes(
|
||||
sortMode: LibrarySort?,
|
||||
isAscending: Boolean,
|
||||
@DrawableRes defaultDrawableRes: Int = R.drawable.ic_check_24dp,
|
||||
isDynamic: Boolean,
|
||||
onSelection: Boolean,
|
||||
@DrawableRes defaultDrawableRes: Int = R.drawable.ic_sort_24dp,
|
||||
@DrawableRes defaultSelectedDrawableRes: Int = R.drawable.ic_check_24dp,
|
||||
): Int {
|
||||
sortMode ?: return defaultDrawableRes
|
||||
return when (sortMode) {
|
||||
LibrarySort.DragAndDrop -> defaultDrawableRes
|
||||
else -> {
|
||||
if (if (sortMode.hasInvertedSort) !isAscending else isAscending) {
|
||||
R.drawable.ic_arrow_downward_24dp
|
||||
} else {
|
||||
R.drawable.ic_arrow_upward_24dp
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
sortMode ?: return if (onSelection) defaultSelectedDrawableRes else defaultDrawableRes
|
||||
|
||||
private fun getSortRes(
|
||||
sortingMode: Int?,
|
||||
isAscending: Boolean,
|
||||
@DrawableRes defaultDrawableRes: Int = R.drawable.ic_check_24dp,
|
||||
): Int {
|
||||
sortingMode ?: return defaultDrawableRes
|
||||
return when (val sortMode = LibrarySort.valueOf(sortingMode)) {
|
||||
LibrarySort.DragAndDrop -> defaultDrawableRes
|
||||
else -> {
|
||||
if (if (sortMode?.hasInvertedSort == true) !isAscending else isAscending) {
|
||||
R.drawable.ic_arrow_downward_24dp
|
||||
} else {
|
||||
R.drawable.ic_arrow_upward_24dp
|
||||
}
|
||||
if (sortMode.isDirectional) {
|
||||
return if (if (sortMode.hasInvertedSort) !isAscending else isAscending) {
|
||||
R.drawable.ic_arrow_downward_24dp
|
||||
} else {
|
||||
R.drawable.ic_arrow_upward_24dp
|
||||
}
|
||||
}
|
||||
|
||||
if (onSelection) {
|
||||
return when(sortMode) {
|
||||
LibrarySort.DragAndDrop -> R.drawable.ic_check_24dp
|
||||
LibrarySort.Random -> R.drawable.ic_refresh_24dp
|
||||
else -> defaultSelectedDrawableRes
|
||||
}
|
||||
}
|
||||
|
||||
return sortMode.iconRes(isDynamic)
|
||||
}
|
||||
|
||||
private fun onCatSortClicked(category: Category, menuId: Int?) {
|
||||
val modType = if (menuId == null) {
|
||||
val (mode, modType) = if (menuId == null) {
|
||||
val sortingMode = category.sortingMode() ?: LibrarySort.Title
|
||||
if (category.isAscending()) {
|
||||
sortingMode.categoryValueDescending
|
||||
} else {
|
||||
sortingMode.categoryValue
|
||||
}
|
||||
sortingMode to
|
||||
if (sortingMode != LibrarySort.Random && category.isAscending()) {
|
||||
sortingMode.categoryValueDescending
|
||||
} else {
|
||||
sortingMode.categoryValue
|
||||
}
|
||||
} else {
|
||||
val sortingMode = LibrarySort.valueOf(menuId) ?: LibrarySort.Title
|
||||
if (sortingMode != LibrarySort.DragAndDrop && sortingMode == category.sortingMode()) {
|
||||
onCatSortClicked(category, null)
|
||||
return
|
||||
}
|
||||
sortingMode.categoryValue
|
||||
sortingMode to sortingMode.categoryValue
|
||||
}
|
||||
if (mode == LibrarySort.Random) {
|
||||
libraryPreferences.randomSortSeed().set(Random.nextInt())
|
||||
}
|
||||
adapter.libraryListener?.sortCategory(category.id!!, modType)
|
||||
}
|
||||
|
|
|
@ -5,9 +5,6 @@ import androidx.core.graphics.ColorUtils
|
|||
import androidx.core.view.isVisible
|
||||
import com.google.android.material.card.MaterialCardView
|
||||
import eu.kanade.tachiyomi.R
|
||||
import yokai.i18n.MR
|
||||
import yokai.util.lang.getString
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
|
||||
import eu.kanade.tachiyomi.util.isLocal
|
||||
import eu.kanade.tachiyomi.util.system.getResourceColor
|
||||
|
@ -17,9 +14,7 @@ import eu.kanade.tachiyomi.util.view.setCards
|
|||
* Generic class used to hold the displayed data of a manga in the library.
|
||||
* @param view the inflated view for this holder.
|
||||
* @param adapter the adapter handling this holder.
|
||||
* @param listener a listener to react to the single tap and long tap events.
|
||||
*/
|
||||
|
||||
abstract class LibraryHolder(
|
||||
view: View,
|
||||
val adapter: LibraryCategoryAdapter,
|
||||
|
@ -43,7 +38,7 @@ abstract class LibraryHolder(
|
|||
*/
|
||||
abstract fun onSetValues(item: LibraryItem)
|
||||
|
||||
fun setUnreadBadge(badge: LibraryBadge, item: LibraryItem) {
|
||||
fun setUnreadBadge(badge: LibraryBadge, item: LibraryMangaItem) {
|
||||
val showTotal = item.header.category.sortingMode() == LibrarySort.TotalChapters
|
||||
badge.setUnreadDownload(
|
||||
when {
|
||||
|
@ -54,7 +49,7 @@ abstract class LibraryHolder(
|
|||
},
|
||||
when {
|
||||
item.downloadCount == -1 -> -1
|
||||
item.manga.isLocal() -> -2
|
||||
item.manga.manga.isLocal() -> -2
|
||||
else -> item.downloadCount
|
||||
},
|
||||
showTotal,
|
||||
|
@ -63,7 +58,7 @@ abstract class LibraryHolder(
|
|||
)
|
||||
}
|
||||
|
||||
fun setReadingButton(item: LibraryItem) {
|
||||
fun setReadingButton(item: LibraryMangaItem) {
|
||||
itemView.findViewById<View>(R.id.play_layout)?.isVisible =
|
||||
item.manga.unread > 0 && !item.hideReadingButton
|
||||
}
|
||||
|
@ -80,8 +75,8 @@ abstract class LibraryHolder(
|
|||
|
||||
override fun onLongClick(view: View?): Boolean {
|
||||
return if (adapter.isLongPressDragEnabled) {
|
||||
val manga = (adapter.getItem(flexibleAdapterPosition) as? LibraryItem)?.manga
|
||||
if (manga != null && !isDraggable && !manga.isBlank() && !manga.isHidden()) {
|
||||
val manga = (adapter.getItem(flexibleAdapterPosition) as? LibraryMangaItem)?.manga
|
||||
if (manga != null && !isDraggable) {
|
||||
adapter.mItemLongClickListener.onItemLongClick(flexibleAdapterPosition)
|
||||
toggleActivation()
|
||||
true
|
||||
|
|
|
@ -1,205 +1,48 @@
|
|||
package eu.kanade.tachiyomi.ui.library
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageView
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.annotation.CallSuper
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.StaggeredGridLayoutManager
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.AbstractSectionableItem
|
||||
import eu.davidea.flexibleadapter.items.IFilterable
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.LibraryManga
|
||||
import eu.kanade.tachiyomi.data.database.models.seriesType
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.databinding.MangaGridItemBinding
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.util.system.dpToPx
|
||||
import eu.kanade.tachiyomi.util.view.compatToolTipText
|
||||
import eu.kanade.tachiyomi.widget.AutofitRecyclerView
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import yokai.domain.ui.UiPreferences
|
||||
|
||||
class LibraryItem(
|
||||
val manga: LibraryManga,
|
||||
abstract class LibraryItem(
|
||||
header: LibraryHeaderItem,
|
||||
private val context: Context?,
|
||||
internal val context: Context?,
|
||||
) : AbstractSectionableItem<LibraryHolder, LibraryHeaderItem>(header), IFilterable<String> {
|
||||
|
||||
var downloadCount = -1
|
||||
var unreadType = 2
|
||||
var sourceLanguage: String? = null
|
||||
var filter = ""
|
||||
|
||||
private val sourceManager: SourceManager by injectLazy()
|
||||
internal val sourceManager: SourceManager by injectLazy()
|
||||
private val uiPreferences: UiPreferences by injectLazy()
|
||||
private val preferences: PreferencesHelper by injectLazy()
|
||||
|
||||
private val uniformSize: Boolean
|
||||
internal val uniformSize: Boolean
|
||||
get() = uiPreferences.uniformGrid().get()
|
||||
|
||||
private val libraryLayout: Int
|
||||
internal val libraryLayout: Int
|
||||
get() = preferences.libraryLayout().get()
|
||||
|
||||
val hideReadingButton: Boolean
|
||||
get() = preferences.hideStartReadingButton().get()
|
||||
|
||||
override fun getLayoutRes(): Int {
|
||||
return if (libraryLayout == LAYOUT_LIST || manga.isBlank()) {
|
||||
R.layout.manga_list_item
|
||||
} else {
|
||||
R.layout.manga_grid_item
|
||||
}
|
||||
}
|
||||
|
||||
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): LibraryHolder {
|
||||
val parent = adapter.recyclerView
|
||||
return if (parent is AutofitRecyclerView) {
|
||||
val libraryLayout = libraryLayout
|
||||
val isFixedSize = uniformSize
|
||||
if (libraryLayout == LAYOUT_LIST || manga.isBlank()) {
|
||||
LibraryListHolder(view, adapter as LibraryCategoryAdapter)
|
||||
} else {
|
||||
view.apply {
|
||||
val isStaggered = parent.layoutManager is StaggeredGridLayoutManager
|
||||
val binding = MangaGridItemBinding.bind(this)
|
||||
binding.behindTitle.isVisible = libraryLayout == LAYOUT_COVER_ONLY_GRID
|
||||
if (libraryLayout >= LAYOUT_COMFORTABLE_GRID) {
|
||||
binding.textLayout.isVisible = libraryLayout == LAYOUT_COMFORTABLE_GRID
|
||||
binding.card.setCardForegroundColor(
|
||||
ContextCompat.getColorStateList(
|
||||
context,
|
||||
R.color.library_comfortable_grid_foreground,
|
||||
),
|
||||
)
|
||||
}
|
||||
if (isFixedSize) {
|
||||
binding.constraintLayout.layoutParams = FrameLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
)
|
||||
binding.coverThumbnail.maxHeight = Int.MAX_VALUE
|
||||
binding.coverThumbnail.minimumHeight = 0
|
||||
binding.constraintLayout.minHeight = 0
|
||||
binding.coverThumbnail.scaleType = ImageView.ScaleType.CENTER_CROP
|
||||
binding.coverThumbnail.adjustViewBounds = false
|
||||
binding.coverThumbnail.updateLayoutParams<ConstraintLayout.LayoutParams> {
|
||||
height = ConstraintLayout.LayoutParams.MATCH_CONSTRAINT
|
||||
dimensionRatio = "15:22"
|
||||
}
|
||||
}
|
||||
if (libraryLayout != LAYOUT_COMFORTABLE_GRID) {
|
||||
binding.card.updateLayoutParams<ConstraintLayout.LayoutParams> {
|
||||
bottomMargin = (if (isStaggered) 2 else 6).dpToPx
|
||||
}
|
||||
}
|
||||
binding.setBGAndFG(libraryLayout)
|
||||
}
|
||||
val gridHolder = LibraryGridHolder(
|
||||
view,
|
||||
adapter as LibraryCategoryAdapter,
|
||||
libraryLayout == LAYOUT_COMPACT_GRID,
|
||||
isFixedSize,
|
||||
)
|
||||
if (!isFixedSize) {
|
||||
gridHolder.setFreeformCoverRatio(manga, parent)
|
||||
}
|
||||
gridHolder
|
||||
}
|
||||
} else {
|
||||
LibraryListHolder(view, adapter as LibraryCategoryAdapter)
|
||||
}
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
override fun bindViewHolder(
|
||||
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
|
||||
holder: LibraryHolder,
|
||||
position: Int,
|
||||
payloads: MutableList<Any?>?,
|
||||
) {
|
||||
if (holder is LibraryGridHolder && !holder.fixedSize) {
|
||||
holder.setFreeformCoverRatio(manga, adapter.recyclerView as? AutofitRecyclerView)
|
||||
}
|
||||
holder.onSetValues(this)
|
||||
(holder as? LibraryGridHolder)?.setSelected(adapter.isSelected(position))
|
||||
val layoutParams = holder.itemView.layoutParams as? StaggeredGridLayoutManager.LayoutParams
|
||||
layoutParams?.isFullSpan = manga.isBlank()
|
||||
if (libraryLayout == LAYOUT_COVER_ONLY_GRID) {
|
||||
holder.itemView.compatToolTipText = manga.title
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this item is draggable.
|
||||
*/
|
||||
override fun isDraggable(): Boolean {
|
||||
return !manga.isBlank() && header.category.isDragAndDrop
|
||||
}
|
||||
|
||||
override fun isEnabled(): Boolean {
|
||||
return !manga.isBlank()
|
||||
}
|
||||
|
||||
override fun isSelectable(): Boolean {
|
||||
return !manga.isBlank()
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters a manga depending on a query.
|
||||
*
|
||||
* @param constraint the query to apply.
|
||||
* @return true if the manga should be included, false otherwise.
|
||||
*/
|
||||
override fun filter(constraint: String): Boolean {
|
||||
filter = constraint
|
||||
if (manga.isBlank() && manga.title.isBlank()) {
|
||||
return constraint.isEmpty()
|
||||
}
|
||||
val sourceName by lazy { sourceManager.getOrStub(manga.source).name }
|
||||
return manga.title.contains(constraint, true) ||
|
||||
(manga.author?.contains(constraint, true) ?: false) ||
|
||||
(manga.artist?.contains(constraint, true) ?: false) ||
|
||||
sourceName.contains(constraint, true) ||
|
||||
if (constraint.contains(",")) {
|
||||
val genres = manga.genre?.split(", ")
|
||||
constraint.split(",").all { containsGenre(it.trim(), genres) }
|
||||
} else {
|
||||
containsGenre(constraint, manga.genre?.split(", "))
|
||||
}
|
||||
}
|
||||
|
||||
private fun containsGenre(tag: String, genres: List<String>?): Boolean {
|
||||
if (tag.trim().isEmpty()) return true
|
||||
context ?: return false
|
||||
val seriesType by lazy { manga.seriesType(context, sourceManager) }
|
||||
return if (tag.startsWith("-")) {
|
||||
val realTag = tag.substringAfter("-")
|
||||
genres?.find {
|
||||
it.trim().equals(realTag, ignoreCase = true) || seriesType.equals(realTag, true)
|
||||
} == null
|
||||
} else {
|
||||
genres?.find {
|
||||
it.trim().equals(tag, ignoreCase = true) || seriesType.equals(tag, true)
|
||||
} != null
|
||||
}
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other is LibraryItem) {
|
||||
return manga.id == other.manga.id && manga.category == other.manga.category
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return 31 * manga.id!!.hashCode() + header!!.hashCode()
|
||||
(holder.itemView.layoutParams as? StaggeredGridLayoutManager.LayoutParams)?.isFullSpan = this is LibraryPlaceholderItem
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
|
|
@ -10,7 +10,7 @@ import eu.kanade.tachiyomi.util.lang.highlightText
|
|||
import eu.kanade.tachiyomi.util.system.dpToPx
|
||||
import eu.kanade.tachiyomi.util.view.setCards
|
||||
import yokai.i18n.MR
|
||||
import yokai.presentation.core.util.coil.loadManga
|
||||
import yokai.util.coil.loadManga
|
||||
import yokai.util.lang.getString
|
||||
|
||||
/**
|
||||
|
@ -39,22 +39,25 @@ class LibraryListHolder(
|
|||
setCards(adapter.showOutline, binding.card, binding.unreadDownloadBadge.root)
|
||||
binding.title.isVisible = true
|
||||
binding.constraintLayout.minHeight = 56.dpToPx
|
||||
if (item.manga.isBlank()) {
|
||||
if (item is LibraryPlaceholderItem) {
|
||||
binding.constraintLayout.minHeight = 0
|
||||
binding.constraintLayout.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
height = ViewGroup.MarginLayoutParams.WRAP_CONTENT
|
||||
}
|
||||
if (item.manga.status == -1) {
|
||||
binding.title.text = null
|
||||
binding.title.isVisible = false
|
||||
} else {
|
||||
binding.title.text = itemView.context.getString(
|
||||
if (adapter.hasActiveFilters && item.manga.realMangaCount >= 1) {
|
||||
MR.strings.no_matches_for_filters_short
|
||||
} else {
|
||||
MR.strings.category_is_empty
|
||||
},
|
||||
)
|
||||
when (item.type) {
|
||||
is LibraryPlaceholderItem.Type.Blank -> {
|
||||
binding.title.text = itemView.context.getString(
|
||||
if (adapter.hasActiveFilters && item.type.mangaCount >= 1) {
|
||||
MR.strings.no_matches_for_filters_short
|
||||
} else {
|
||||
MR.strings.category_is_empty
|
||||
},
|
||||
)
|
||||
}
|
||||
is LibraryPlaceholderItem.Type.Hidden -> {
|
||||
binding.title.text = null
|
||||
binding.title.isVisible = false
|
||||
}
|
||||
}
|
||||
binding.title.textAlignment = View.TEXT_ALIGNMENT_CENTER
|
||||
binding.card.isVisible = false
|
||||
|
@ -63,6 +66,9 @@ class LibraryListHolder(
|
|||
binding.subtitle.isVisible = false
|
||||
return
|
||||
}
|
||||
|
||||
if (item !is LibraryMangaItem) error("${item::class.qualifiedName} is not a valid item")
|
||||
|
||||
binding.constraintLayout.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
height = 52.dpToPx
|
||||
}
|
||||
|
@ -71,16 +77,16 @@ class LibraryListHolder(
|
|||
binding.title.textAlignment = View.TEXT_ALIGNMENT_TEXT_START
|
||||
|
||||
// Update the binding.title of the manga.
|
||||
binding.title.text = item.manga.title.highlightText(item.filter, color)
|
||||
binding.title.text = item.manga.manga.title.highlightText(item.filter, color)
|
||||
setUnreadBadge(binding.unreadDownloadBadge.badgeView, item)
|
||||
|
||||
val authorArtist =
|
||||
if (item.manga.author == item.manga.artist || item.manga.artist.isNullOrBlank()) {
|
||||
item.manga.author?.trim() ?: ""
|
||||
if (item.manga.manga.author == item.manga.manga.artist || item.manga.manga.artist.isNullOrBlank()) {
|
||||
item.manga.manga.author?.trim() ?: ""
|
||||
} else {
|
||||
listOfNotNull(
|
||||
item.manga.author?.trim()?.takeIf { it.isNotBlank() },
|
||||
item.manga.artist?.trim()?.takeIf { it.isNotBlank() },
|
||||
item.manga.manga.author?.trim()?.takeIf { it.isNotBlank() },
|
||||
item.manga.manga.artist?.trim()?.takeIf { it.isNotBlank() },
|
||||
).joinToString(", ")
|
||||
}
|
||||
|
||||
|
@ -95,7 +101,7 @@ class LibraryListHolder(
|
|||
|
||||
// Update the cover.
|
||||
binding.coverThumbnail.dispose()
|
||||
binding.coverThumbnail.loadManga(item.manga)
|
||||
binding.coverThumbnail.loadManga(item.manga.manga)
|
||||
}
|
||||
|
||||
override fun onActionStateChanged(position: Int, actionState: Int) {
|
||||
|
|
|
@ -0,0 +1,180 @@
|
|||
package eu.kanade.tachiyomi.ui.library
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageView
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.StaggeredGridLayoutManager
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.LibraryManga
|
||||
import eu.kanade.tachiyomi.data.database.models.seriesType
|
||||
import eu.kanade.tachiyomi.databinding.MangaGridItemBinding
|
||||
import eu.kanade.tachiyomi.util.system.dpToPx
|
||||
import eu.kanade.tachiyomi.util.view.compatToolTipText
|
||||
import eu.kanade.tachiyomi.widget.AutofitRecyclerView
|
||||
|
||||
class LibraryMangaItem(
|
||||
val manga: LibraryManga,
|
||||
header: LibraryHeaderItem,
|
||||
context: Context?,
|
||||
) : LibraryItem(header, context) {
|
||||
|
||||
var downloadCount = -1
|
||||
var unreadType = 2
|
||||
var sourceLanguage: String? = null
|
||||
|
||||
override fun getLayoutRes(): Int {
|
||||
return if (libraryLayout == LAYOUT_LIST) {
|
||||
R.layout.manga_list_item
|
||||
} else {
|
||||
R.layout.manga_grid_item
|
||||
}
|
||||
}
|
||||
|
||||
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): LibraryHolder {
|
||||
val listHolder by lazy { LibraryListHolder(view, adapter as LibraryCategoryAdapter) }
|
||||
val parent = adapter.recyclerView
|
||||
if (parent !is AutofitRecyclerView) return listHolder
|
||||
|
||||
val libraryLayout = libraryLayout
|
||||
val isFixedSize = uniformSize
|
||||
|
||||
if (libraryLayout == LAYOUT_LIST) { return listHolder }
|
||||
|
||||
view.apply {
|
||||
val isStaggered = parent.layoutManager is StaggeredGridLayoutManager
|
||||
val binding = MangaGridItemBinding.bind(this)
|
||||
binding.behindTitle.isVisible = libraryLayout == LAYOUT_COVER_ONLY_GRID
|
||||
if (libraryLayout >= LAYOUT_COMFORTABLE_GRID) {
|
||||
binding.textLayout.isVisible = libraryLayout == LAYOUT_COMFORTABLE_GRID
|
||||
binding.card.setCardForegroundColor(
|
||||
ContextCompat.getColorStateList(
|
||||
context,
|
||||
R.color.library_comfortable_grid_foreground,
|
||||
),
|
||||
)
|
||||
}
|
||||
if (isFixedSize) {
|
||||
binding.constraintLayout.layoutParams = FrameLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
)
|
||||
binding.coverThumbnail.maxHeight = Int.MAX_VALUE
|
||||
binding.coverThumbnail.minimumHeight = 0
|
||||
binding.constraintLayout.minHeight = 0
|
||||
binding.coverThumbnail.scaleType = ImageView.ScaleType.CENTER_CROP
|
||||
binding.coverThumbnail.adjustViewBounds = false
|
||||
binding.coverThumbnail.updateLayoutParams<ConstraintLayout.LayoutParams> {
|
||||
height = ConstraintLayout.LayoutParams.MATCH_CONSTRAINT
|
||||
dimensionRatio = "2:3"
|
||||
}
|
||||
}
|
||||
if (libraryLayout != LAYOUT_COMFORTABLE_GRID) {
|
||||
binding.card.updateLayoutParams<ConstraintLayout.LayoutParams> {
|
||||
bottomMargin = (if (isStaggered) 2 else 6).dpToPx
|
||||
}
|
||||
}
|
||||
binding.setBGAndFG(libraryLayout)
|
||||
}
|
||||
val gridHolder = LibraryGridHolder(
|
||||
view,
|
||||
adapter as LibraryCategoryAdapter,
|
||||
libraryLayout == LAYOUT_COMPACT_GRID,
|
||||
isFixedSize,
|
||||
)
|
||||
if (!isFixedSize) {
|
||||
gridHolder.setFreeformCoverRatio(manga.manga, parent)
|
||||
}
|
||||
return gridHolder
|
||||
}
|
||||
|
||||
override fun bindViewHolder(
|
||||
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
|
||||
holder: LibraryHolder,
|
||||
position: Int,
|
||||
payloads: MutableList<Any?>?,
|
||||
) {
|
||||
if (holder is LibraryGridHolder && !holder.fixedSize) {
|
||||
holder.setFreeformCoverRatio(manga.manga, adapter.recyclerView as? AutofitRecyclerView)
|
||||
}
|
||||
super.bindViewHolder(adapter, holder, position, payloads)
|
||||
if (libraryLayout == LAYOUT_COVER_ONLY_GRID) {
|
||||
holder.itemView.compatToolTipText = manga.manga.title
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this item is draggable.
|
||||
*/
|
||||
override fun isDraggable(): Boolean {
|
||||
return header.category.isDragAndDrop
|
||||
}
|
||||
|
||||
override fun isEnabled(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun isSelectable(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters a manga depending on a query.
|
||||
*
|
||||
* @param constraint the query to apply.
|
||||
* @return true if the manga should be included, false otherwise.
|
||||
*/
|
||||
override fun filter(constraint: String): Boolean {
|
||||
filter = constraint
|
||||
if (manga.manga.title.isBlank()) {
|
||||
return constraint.isEmpty()
|
||||
}
|
||||
val sourceName by lazy { sourceManager.getOrStub(manga.manga.source).name }
|
||||
return manga.manga.title.contains(constraint, true) ||
|
||||
(manga.manga.author?.contains(constraint, true) ?: false) ||
|
||||
(manga.manga.artist?.contains(constraint, true) ?: false) ||
|
||||
sourceName.contains(constraint, true) ||
|
||||
if (constraint.contains(",")) {
|
||||
val genres = manga.manga.genre?.split(", ")
|
||||
constraint.split(",").all { containsGenre(it.trim(), genres) }
|
||||
} else {
|
||||
containsGenre(constraint, manga.manga.genre?.split(", "))
|
||||
}
|
||||
}
|
||||
|
||||
private fun containsGenre(tag: String, genres: List<String>?): Boolean {
|
||||
if (tag.trim().isEmpty()) return true
|
||||
context ?: return false
|
||||
|
||||
val seriesType by lazy { manga.manga.seriesType(context, sourceManager) }
|
||||
return if (tag.startsWith("-")) {
|
||||
val realTag = tag.substringAfter("-")
|
||||
genres?.find {
|
||||
it.trim().equals(realTag, ignoreCase = true) || seriesType.equals(realTag, true)
|
||||
} == null
|
||||
} else {
|
||||
genres?.find {
|
||||
it.trim().equals(tag, ignoreCase = true) || seriesType.equals(tag, true)
|
||||
} != null
|
||||
}
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other is LibraryMangaItem) {
|
||||
return manga.manga.id == other.manga.manga.id && manga.category == other.manga.category
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return 31 * manga.manga.id.hashCode() + header!!.hashCode()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
package eu.kanade.tachiyomi.ui.library
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.kanade.tachiyomi.R
|
||||
|
||||
/**
|
||||
* Placeholder item to indicate if the category is hidden or empty/filtered out.
|
||||
*/
|
||||
class LibraryPlaceholderItem (
|
||||
val category: Int,
|
||||
val type: Type,
|
||||
header: LibraryHeaderItem,
|
||||
context: Context?,
|
||||
) : LibraryItem(header, context) {
|
||||
|
||||
override fun getLayoutRes(): Int = R.layout.manga_list_item
|
||||
|
||||
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): LibraryHolder {
|
||||
return LibraryListHolder(view, adapter as LibraryCategoryAdapter)
|
||||
}
|
||||
|
||||
override fun filter(constraint: String): Boolean {
|
||||
filter = constraint
|
||||
|
||||
if (type !is Type.Hidden || type.title.isBlank()) return constraint.isEmpty()
|
||||
|
||||
return type.title.contains(constraint, true)
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other is LibraryPlaceholderItem) {
|
||||
return category == other.category
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return 31 * Long.MIN_VALUE.hashCode() + header!!.hashCode()
|
||||
}
|
||||
|
||||
sealed class Type {
|
||||
data class Hidden(val title: String, val hiddenItems: List<LibraryMangaItem>) : Type()
|
||||
data class Blank(val mangaCount: Int) : Type()
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun hidden(category: Int, header: LibraryHeaderItem, context: Context?, title: String, hiddenItems: List<LibraryMangaItem>) =
|
||||
LibraryPlaceholderItem(category, Type.Hidden(title, hiddenItems), header, context)
|
||||
|
||||
fun blank(category: Int, header: LibraryHeaderItem, context: Context?, mangaCount: Int = 0) =
|
||||
LibraryPlaceholderItem(category, Type.Blank(mangaCount), header, context)
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load diff
|
@ -1,13 +1,10 @@
|
|||
package eu.kanade.tachiyomi.ui.library
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import eu.kanade.tachiyomi.R
|
||||
import yokai.i18n.MR
|
||||
import yokai.util.lang.getString
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import eu.kanade.tachiyomi.ui.base.MaterialMenuSheet
|
||||
import yokai.i18n.MR
|
||||
|
||||
enum class LibrarySort(
|
||||
val mainValue: Int,
|
||||
|
@ -33,7 +30,11 @@ enum class LibrarySort(
|
|||
MR.strings.category,
|
||||
R.drawable.ic_label_outline_24dp,
|
||||
),
|
||||
|
||||
Random(
|
||||
8,
|
||||
MR.strings.random,
|
||||
R.drawable.ic_shuffle_24dp,
|
||||
),
|
||||
;
|
||||
|
||||
val categoryValue: Char
|
||||
|
@ -50,6 +51,7 @@ enum class LibrarySort(
|
|||
LatestChapter -> "LATEST_CHAPTER"
|
||||
DateFetched -> "CHAPTER_FETCH_DATE"
|
||||
DateAdded -> "DATE_ADDED"
|
||||
Random -> "RANDOM"
|
||||
else -> "ALPHABETICAL"
|
||||
}
|
||||
return "$type,ASCENDING"
|
||||
|
@ -63,6 +65,9 @@ enum class LibrarySort(
|
|||
val hasInvertedSort: Boolean
|
||||
get() = this in listOf(LastRead, DateAdded, LatestChapter, DateFetched)
|
||||
|
||||
val isDirectional: Boolean
|
||||
get() = this !in listOf(DragAndDrop, Random)
|
||||
|
||||
fun menuSheetItem(isDynamic: Boolean): MaterialMenuSheet.MenuSheetItem {
|
||||
return MaterialMenuSheet.MenuSheetItem(
|
||||
mainValue,
|
||||
|
@ -85,6 +90,7 @@ enum class LibrarySort(
|
|||
"LATEST_CHAPTER" -> LatestChapter
|
||||
"CHAPTER_FETCH_DATE" -> DateFetched
|
||||
"DATE_ADDED" -> DateAdded
|
||||
"RANDOM" -> Random
|
||||
else -> Title
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
|
|
|
@ -0,0 +1,93 @@
|
|||
package eu.kanade.tachiyomi.ui.library.compose
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.databinding.LibraryControllerBinding
|
||||
import eu.kanade.tachiyomi.ui.base.controller.BaseCoroutineController
|
||||
import eu.kanade.tachiyomi.ui.library.models.LibraryItem
|
||||
import eu.kanade.tachiyomi.ui.main.BottomSheetController
|
||||
import eu.kanade.tachiyomi.ui.main.FloatingSearchInterface
|
||||
import eu.kanade.tachiyomi.ui.main.RootSearchInterface
|
||||
import java.util.Locale
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import yokai.domain.ui.UiPreferences
|
||||
import yokai.i18n.MR
|
||||
import yokai.presentation.library.LibraryContent
|
||||
import yokai.presentation.theme.YokaiTheme
|
||||
import yokai.util.lang.getString
|
||||
|
||||
class LibraryComposeController(
|
||||
bundle: Bundle? = null,
|
||||
val uiPreferences: UiPreferences = Injekt.get(),
|
||||
val preferences: PreferencesHelper = Injekt.get(),
|
||||
) : BaseCoroutineController<LibraryControllerBinding, LibraryComposePresenter>(bundle) ,
|
||||
BottomSheetController,
|
||||
RootSearchInterface,
|
||||
FloatingSearchInterface {
|
||||
|
||||
override fun getTitle(): String? {
|
||||
return view?.context?.getString(MR.strings.library)
|
||||
}
|
||||
|
||||
override fun getSearchTitle(): String? {
|
||||
val searchSuggestion by lazy { preferences.librarySearchSuggestion().get() }
|
||||
|
||||
return searchTitle(
|
||||
if (preferences.showLibrarySearchSuggestions().get() && searchSuggestion.isNotBlank()) {
|
||||
"\"$searchSuggestion\""
|
||||
} else {
|
||||
view?.context?.getString(MR.strings.your_library)?.lowercase(Locale.ROOT)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
override val presenter = LibraryComposePresenter()
|
||||
|
||||
override fun createBinding(inflater: LayoutInflater) = LibraryControllerBinding.inflate(inflater)
|
||||
|
||||
override fun onViewCreated(view: View) {
|
||||
super.onViewCreated(view)
|
||||
|
||||
binding.composeView.isVisible = true
|
||||
binding.swipeRefresh.isGone = true
|
||||
binding.fastScroller.isGone = true
|
||||
|
||||
binding.composeView.setContent {
|
||||
YokaiTheme {
|
||||
ScreenContent()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ScreenContent() {
|
||||
val nestedScrollInterop = rememberNestedScrollInteropConnection()
|
||||
|
||||
val state by presenter.state.collectAsState()
|
||||
LibraryContent(
|
||||
modifier = Modifier.nestedScroll(nestedScrollInterop),
|
||||
items = (0..50).map { LibraryItem.Blank(it) },
|
||||
columns = 3,
|
||||
)
|
||||
}
|
||||
|
||||
override fun showSheet() {
|
||||
}
|
||||
|
||||
override fun hideSheet() {
|
||||
}
|
||||
|
||||
override fun toggleSheet() {
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
package eu.kanade.tachiyomi.ui.library.compose
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.models.Category
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.StateCoroutinePresenter
|
||||
import eu.kanade.tachiyomi.ui.library.models.LibraryItem
|
||||
|
||||
typealias LibraryMap = Map<Category, List<LibraryItem>>
|
||||
|
||||
class LibraryComposePresenter :
|
||||
StateCoroutinePresenter<LibraryComposePresenter.State, LibraryComposeController>(State()) {
|
||||
|
||||
data class State(
|
||||
var isLoading: Boolean = true,
|
||||
var library: LibraryMap = emptyMap()
|
||||
)
|
||||
}
|
|
@ -2,16 +2,14 @@ package eu.kanade.tachiyomi.ui.library.display
|
|||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import eu.kanade.tachiyomi.R
|
||||
import yokai.i18n.MR
|
||||
import yokai.util.lang.getString
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import eu.kanade.tachiyomi.databinding.LibraryCategoryLayoutBinding
|
||||
import eu.kanade.tachiyomi.util.bindToPreference
|
||||
import eu.kanade.tachiyomi.util.lang.withSubtitle
|
||||
import eu.kanade.tachiyomi.util.system.toInt
|
||||
import eu.kanade.tachiyomi.widget.BaseLibraryDisplayView
|
||||
import kotlin.math.min
|
||||
import yokai.i18n.MR
|
||||
import yokai.util.lang.getString
|
||||
|
||||
class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
|
||||
BaseLibraryDisplayView<LibraryCategoryLayoutBinding>(context, attrs) {
|
||||
|
@ -20,7 +18,7 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
|
|||
override fun initGeneralPreferences() {
|
||||
with(binding) {
|
||||
showAll.bindToPreference(preferences.showAllCategories()) {
|
||||
controller?.presenter?.getLibrary()
|
||||
controller?.presenter?.updateLibrary()
|
||||
binding.categoryShow.isEnabled = it
|
||||
}
|
||||
categoryShow.isEnabled = showAll.isChecked
|
||||
|
@ -30,7 +28,7 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
|
|||
dynamicToBottom.text = context.getString(MR.strings.move_dynamic_to_bottom)
|
||||
.withSubtitle(context, MR.strings.when_grouping_by_sources_tags)
|
||||
dynamicToBottom.bindToPreference(preferences.collapsedDynamicAtBottom()) {
|
||||
controller?.presenter?.getLibrary()
|
||||
controller?.presenter?.updateLibrary()
|
||||
}
|
||||
showEmptyCatsFiltering.bindToPreference(preferences.showEmptyCategoriesWhileFiltering()) {
|
||||
controller?.presenter?.requestFilterUpdate()
|
||||
|
|
|
@ -22,6 +22,7 @@ import eu.kanade.tachiyomi.domain.manga.models.Manga
|
|||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.ui.library.LibraryController
|
||||
import eu.kanade.tachiyomi.ui.library.LibraryGroup
|
||||
import eu.kanade.tachiyomi.ui.library.LibraryMangaItem
|
||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||
import eu.kanade.tachiyomi.util.system.dpToPx
|
||||
import eu.kanade.tachiyomi.util.system.launchUI
|
||||
|
@ -36,6 +37,8 @@ import eu.kanade.tachiyomi.util.view.isCollapsed
|
|||
import eu.kanade.tachiyomi.util.view.isExpanded
|
||||
import eu.kanade.tachiyomi.util.view.isHidden
|
||||
import eu.kanade.tachiyomi.util.view.setText
|
||||
import kotlin.math.max
|
||||
import kotlin.math.roundToInt
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.drop
|
||||
|
@ -48,8 +51,6 @@ import uy.kohesive.injekt.api.get
|
|||
import uy.kohesive.injekt.injectLazy
|
||||
import yokai.i18n.MR
|
||||
import yokai.util.lang.getString
|
||||
import kotlin.math.max
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class FilterBottomSheet @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
|
||||
LinearLayout(context, attrs),
|
||||
|
@ -368,11 +369,12 @@ class FilterBottomSheet @JvmOverloads constructor(context: Context, attrs: Attri
|
|||
suspend fun checkForManhwa(sourceManager: SourceManager) {
|
||||
if (checked) return
|
||||
withIOContext {
|
||||
val libraryManga = controller?.presenter?.allLibraryItems ?: return@withIOContext
|
||||
val libraryManga = controller?.presenter?.currentLibraryItems ?: return@withIOContext
|
||||
checked = true
|
||||
var types = mutableSetOf<StringResource>()
|
||||
libraryManga.forEach {
|
||||
when (it.manga.seriesType(sourceManager = sourceManager)) {
|
||||
if (it !is LibraryMangaItem) return@forEach
|
||||
when (it.manga.manga.seriesType(sourceManager = sourceManager)) {
|
||||
Manga.TYPE_MANHWA, Manga.TYPE_WEBTOON -> types.add(MR.strings.manhwa)
|
||||
Manga.TYPE_MANHUA -> types.add(MR.strings.manhua)
|
||||
Manga.TYPE_COMIC -> types.add(MR.strings.comic)
|
||||
|
@ -637,7 +639,7 @@ class FilterBottomSheet @JvmOverloads constructor(context: Context, attrs: Attri
|
|||
SeriesType('m', MR.strings.series_type),
|
||||
Bookmarked('b', MR.strings.bookmarked),
|
||||
Tracked('t', MR.strings.tracking),
|
||||
ContentType('s', MR.strings.content_type);
|
||||
ContentType('s', MR.strings.content_type)
|
||||
;
|
||||
|
||||
companion object {
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
package eu.kanade.tachiyomi.ui.library.models
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.models.LibraryManga
|
||||
|
||||
sealed interface LibraryItem {
|
||||
data class Blank(val mangaCount: Int = 0) : LibraryItem
|
||||
data class Hidden(val title: String, val hiddenItems: List<LibraryItem>) : LibraryItem
|
||||
data class Manga(
|
||||
val libraryManga: LibraryManga,
|
||||
val isLocal: Boolean = false,
|
||||
val downloadCount: Long = -1,
|
||||
val unreadCount: Long = -1,
|
||||
val language: String = "",
|
||||
) : LibraryItem
|
||||
}
|
|
@ -62,12 +62,12 @@ import com.bluelinelabs.conductor.ControllerChangeHandler
|
|||
import com.bluelinelabs.conductor.Router
|
||||
import com.getkeepsafe.taptargetview.TapTarget
|
||||
import com.getkeepsafe.taptargetview.TapTargetView
|
||||
import com.google.android.material.badge.BadgeDrawable
|
||||
import com.google.android.material.navigation.NavigationBarView
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.google.android.material.transition.platform.MaterialContainerTransformSharedElementCallback
|
||||
import eu.kanade.tachiyomi.BuildConfig
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.download.DownloadJob
|
||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
|
||||
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
||||
|
@ -86,6 +86,7 @@ import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
|
|||
import eu.kanade.tachiyomi.ui.base.controller.BaseLegacyController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||
import eu.kanade.tachiyomi.ui.library.LibraryController
|
||||
import eu.kanade.tachiyomi.ui.library.compose.LibraryComposeController
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaDetailsController
|
||||
import eu.kanade.tachiyomi.ui.more.AboutController
|
||||
import eu.kanade.tachiyomi.ui.more.OverflowDialog
|
||||
|
@ -117,6 +118,7 @@ import eu.kanade.tachiyomi.util.system.prepareSideNavContext
|
|||
import eu.kanade.tachiyomi.util.system.rootWindowInsetsCompat
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import eu.kanade.tachiyomi.util.system.tryTakePersistableUriPermission
|
||||
import eu.kanade.tachiyomi.util.system.withUIContext
|
||||
import eu.kanade.tachiyomi.util.view.BackHandlerControllerInterface
|
||||
import eu.kanade.tachiyomi.util.view.backgroundColor
|
||||
import eu.kanade.tachiyomi.util.view.blurBehindWindow
|
||||
|
@ -136,11 +138,10 @@ import kotlin.collections.set
|
|||
import kotlin.math.abs
|
||||
import kotlin.math.min
|
||||
import kotlin.math.roundToLong
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.withContext
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import yokai.core.migration.Migrator
|
||||
import yokai.domain.base.BasePreferences
|
||||
|
@ -197,6 +198,7 @@ open class MainActivity : BaseActivity<MainActivityBinding>() {
|
|||
dimenW to dimenH
|
||||
}
|
||||
|
||||
@Deprecated("Create contract directly from Composable")
|
||||
private val requestNotificationPermissionLauncher =
|
||||
registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
|
||||
if (!isGranted) {
|
||||
|
@ -458,8 +460,12 @@ open class MainActivity : BaseActivity<MainActivityBinding>() {
|
|||
}
|
||||
}
|
||||
|
||||
DownloadJob.downloadFlow.onEach(::downloadStatusChanged).launchIn(lifecycleScope)
|
||||
lifecycleScope
|
||||
combine(
|
||||
downloadManager.isDownloaderRunning,
|
||||
downloadManager.queueState,
|
||||
) { isDownloading, queueState -> isDownloading to queueState.size }
|
||||
.onEach { downloadStatusChanged(it.first, it.second) }
|
||||
.launchIn(lifecycleScope)
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
setSupportActionBar(binding.toolbar)
|
||||
supportActionBar?.setDisplayShowCustomEnabled(true)
|
||||
|
@ -533,7 +539,7 @@ open class MainActivity : BaseActivity<MainActivityBinding>() {
|
|||
if (currentRoot?.tag()?.toIntOrNull() != id) {
|
||||
setRoot(
|
||||
when (id) {
|
||||
R.id.nav_library -> LibraryController()
|
||||
R.id.nav_library -> if (basePreferences.composeLibrary().get()) LibraryComposeController() else LibraryController()
|
||||
R.id.nav_recents -> RecentsController()
|
||||
else -> BrowseController()
|
||||
},
|
||||
|
@ -947,7 +953,6 @@ open class MainActivity : BaseActivity<MainActivityBinding>() {
|
|||
extensionManager.getExtensionUpdates(false)
|
||||
}
|
||||
setExtensionsBadge()
|
||||
DownloadJob.callListeners(downloadManager = downloadManager)
|
||||
showDLQueueTutorial()
|
||||
reEnableBackPressedCallBack()
|
||||
}
|
||||
|
@ -997,7 +1002,7 @@ open class MainActivity : BaseActivity<MainActivityBinding>() {
|
|||
}
|
||||
|
||||
private fun checkForAppUpdates() {
|
||||
if (isUpdaterEnabled) {
|
||||
if (isUpdaterEnabled && router.backstack.lastOrNull()?.controller !is AboutController) {
|
||||
lifecycleScope.launchIO {
|
||||
try {
|
||||
val result = updateChecker.checkForUpdate(this@MainActivity)
|
||||
|
@ -1007,7 +1012,7 @@ open class MainActivity : BaseActivity<MainActivityBinding>() {
|
|||
val isBeta = result.release.preRelease == true
|
||||
|
||||
// Create confirmation window
|
||||
withContext(Dispatchers.Main) {
|
||||
withUIContext {
|
||||
showNotificationPermissionPrompt()
|
||||
AppUpdateNotifier.releasePageUrl = result.release.releaseLink
|
||||
AboutController.NewUpdateDialogController(body, url, isBeta).showDialog(router)
|
||||
|
@ -1033,6 +1038,7 @@ open class MainActivity : BaseActivity<MainActivityBinding>() {
|
|||
|
||||
@SuppressLint("MissingSuperCall")
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
splashState.ready = true
|
||||
if (!handleIntentAction(intent)) {
|
||||
super.onNewIntent(intent)
|
||||
}
|
||||
|
@ -1049,13 +1055,13 @@ open class MainActivity : BaseActivity<MainActivityBinding>() {
|
|||
}
|
||||
when (intent.action) {
|
||||
SHORTCUT_LIBRARY -> nav.selectedItemId = R.id.nav_library
|
||||
SHORTCUT_RECENTLY_UPDATED, SHORTCUT_RECENTLY_READ, SHORTCUT_RECENTS -> {
|
||||
SHORTCUT_RECENTLY_UPDATED, SHORTCUT_RECENTLY_READ, Constants.SHORTCUT_RECENTS -> {
|
||||
if (nav.selectedItemId != R.id.nav_recents) {
|
||||
nav.selectedItemId = R.id.nav_recents
|
||||
} else {
|
||||
router.popToRoot()
|
||||
}
|
||||
if (intent.action == SHORTCUT_RECENTS) return true
|
||||
if (intent.action == Constants.SHORTCUT_RECENTS) return true
|
||||
nav.post {
|
||||
val controller =
|
||||
router.backstack.firstOrNull()?.controller as? RecentsController
|
||||
|
@ -1088,7 +1094,11 @@ open class MainActivity : BaseActivity<MainActivityBinding>() {
|
|||
SHORTCUT_UPDATE_NOTES -> {
|
||||
val extras = intent.extras ?: return false
|
||||
if (router.backstack.isEmpty()) nav.selectedItemId = R.id.nav_library
|
||||
if (router.backstack.lastOrNull()?.controller !is AboutController.NewUpdateDialogController) {
|
||||
if (
|
||||
router.backstack.lastOrNull()?.controller !is AboutController.NewUpdateDialogController &&
|
||||
// FIXME: Show Compose version of NewUpdateDialog for AboutController
|
||||
router.backstack.lastOrNull()?.controller !is AboutController
|
||||
) {
|
||||
AboutController.NewUpdateDialogController(extras).showDialog(router)
|
||||
}
|
||||
}
|
||||
|
@ -1118,7 +1128,6 @@ open class MainActivity : BaseActivity<MainActivityBinding>() {
|
|||
else -> return false
|
||||
}
|
||||
|
||||
splashState.ready = true
|
||||
return true
|
||||
}
|
||||
|
||||
|
@ -1504,13 +1513,17 @@ open class MainActivity : BaseActivity<MainActivityBinding>() {
|
|||
}
|
||||
}
|
||||
|
||||
fun downloadStatusChanged(downloading: Boolean) {
|
||||
private fun BadgeDrawable.updateQueueSize(queueSize: Int) {
|
||||
number = queueSize
|
||||
}
|
||||
|
||||
private fun downloadStatusChanged(downloading: Boolean, queueSize: Int) {
|
||||
lifecycleScope.launchUI {
|
||||
val hasQueue = downloading || downloadManager.hasQueue()
|
||||
val hasQueue = downloading || queueSize > 0
|
||||
if (hasQueue) {
|
||||
val badge = nav.getOrCreateBadge(R.id.nav_recents)
|
||||
badge.number = downloadManager.queue.size
|
||||
if (downloading) badge.backgroundColor = -870219 else badge.backgroundColor = Color.GRAY
|
||||
badge.updateQueueSize(queueSize)
|
||||
badge.backgroundColor = if (downloading) getResourceColor(R.attr.colorError) else Color.GRAY
|
||||
showDLQueueTutorial()
|
||||
} else {
|
||||
nav.removeBadge(R.id.nav_recents)
|
||||
|
@ -1601,20 +1614,12 @@ open class MainActivity : BaseActivity<MainActivityBinding>() {
|
|||
private const val SWIPE_THRESHOLD = 100
|
||||
private const val SWIPE_VELOCITY_THRESHOLD = 100
|
||||
|
||||
const val MAIN_ACTIVITY = Constants.MAIN_ACTIVITY
|
||||
|
||||
// Shortcut actions
|
||||
const val SHORTCUT_LIBRARY = "eu.kanade.tachiyomi.SHOW_LIBRARY"
|
||||
@Deprecated("Use the one from Constants object instead")
|
||||
const val SHORTCUT_RECENTS = Constants.SHORTCUT_RECENTS
|
||||
const val SHORTCUT_RECENTLY_UPDATED = "eu.kanade.tachiyomi.SHOW_RECENTLY_UPDATED"
|
||||
const val SHORTCUT_RECENTLY_READ = "eu.kanade.tachiyomi.SHOW_RECENTLY_READ"
|
||||
const val SHORTCUT_BROWSE = "eu.kanade.tachiyomi.SHOW_BROWSE"
|
||||
const val SHORTCUT_DOWNLOADS = "eu.kanade.tachiyomi.SHOW_DOWNLOADS"
|
||||
@Deprecated("Use the one from Constants object instead")
|
||||
const val SHORTCUT_MANGA = Constants.SHORTCUT_MANGA
|
||||
@Deprecated("Use the one from Constants object instead")
|
||||
const val SHORTCUT_MANGA_BACK = Constants.SHORTCUT_MANGA_BACK
|
||||
const val SHORTCUT_UPDATE_NOTES = "eu.kanade.tachiyomi.SHOW_UPDATE_NOTES"
|
||||
const val SHORTCUT_SOURCE = "eu.kanade.tachiyomi.SHOW_SOURCE"
|
||||
const val SHORTCUT_READER_SETTINGS = "eu.kanade.tachiyomi.READER_SETTINGS"
|
||||
|
|
|
@ -97,7 +97,7 @@ class SearchActivity : MainActivity() {
|
|||
}
|
||||
|
||||
private fun intentShouldGoBack() =
|
||||
intent.action in listOf(SHORTCUT_MANGA, SHORTCUT_READER_SETTINGS, SHORTCUT_BROWSE)
|
||||
intent.action in listOf(Constants.SHORTCUT_MANGA, SHORTCUT_READER_SETTINGS, SHORTCUT_BROWSE)
|
||||
|
||||
override fun syncActivityViewWithController(
|
||||
to: Controller?,
|
||||
|
|
|
@ -43,8 +43,8 @@ import uy.kohesive.injekt.injectLazy
|
|||
import yokai.domain.manga.interactor.GetManga
|
||||
import yokai.domain.manga.models.cover
|
||||
import yokai.i18n.MR
|
||||
import yokai.presentation.core.util.coil.asTarget
|
||||
import yokai.presentation.core.util.coil.loadManga
|
||||
import yokai.util.coil.asTarget
|
||||
import yokai.util.coil.loadManga
|
||||
import yokai.util.lang.getString
|
||||
import android.R as AR
|
||||
|
||||
|
|
|
@ -792,7 +792,6 @@ class MangaDetailsController :
|
|||
binding.swipeRefresh.isRefreshing = enabled
|
||||
}
|
||||
|
||||
//region Recycler methods
|
||||
fun updateChapterDownload(download: Download) {
|
||||
getHolder(download.chapter)?.notifyStatus(
|
||||
download.status,
|
||||
|
@ -807,6 +806,8 @@ class MangaDetailsController :
|
|||
}
|
||||
|
||||
private fun getHeader(): MangaHeaderHolder? {
|
||||
if (!isBindingInitialized) return null
|
||||
|
||||
return if (isTablet) {
|
||||
binding.tabletRecycler.findViewHolderForAdapterPosition(0) as? MangaHeaderHolder
|
||||
} else {
|
||||
|
@ -822,27 +823,24 @@ class MangaDetailsController :
|
|||
updateMenuVisibility(activityBinding?.toolbar?.menu)
|
||||
}
|
||||
|
||||
fun updateChapters(chapters: List<ChapterItem>) {
|
||||
fun updateChapters() {
|
||||
view ?: return
|
||||
binding.swipeRefresh.isRefreshing = presenter.isLoading
|
||||
if (presenter.chapters.isEmpty() && fromCatalogue && !presenter.hasRequested) {
|
||||
launchUI { binding.swipeRefresh.isRefreshing = true }
|
||||
presenter.fetchChaptersFromSource()
|
||||
}
|
||||
tabletAdapter?.notifyItemChanged(0)
|
||||
adapter?.setChapters(chapters)
|
||||
adapter?.setChapters(presenter.chapters)
|
||||
addMangaHeader()
|
||||
colorToolbar(binding.recycler.canScrollVertically(-1))
|
||||
updateMenuVisibility(activityBinding?.toolbar?.menu)
|
||||
}
|
||||
|
||||
private fun addMangaHeader() {
|
||||
if (tabletAdapter?.scrollableHeaders?.isEmpty() == true) {
|
||||
val tabletHeader = presenter.tabletChapterHeaderItem
|
||||
if (tabletHeader != null && tabletAdapter?.scrollableHeaders?.isEmpty() == true) {
|
||||
tabletAdapter?.removeAllScrollableHeaders()
|
||||
tabletAdapter?.addScrollableHeader(presenter.headerItem)
|
||||
adapter?.removeAllScrollableHeaders()
|
||||
adapter?.addScrollableHeader(presenter.tabletChapterHeaderItem!!)
|
||||
} else if (!isTablet && adapter?.scrollableHeaders?.isEmpty() == true) {
|
||||
adapter?.addScrollableHeader(tabletHeader)
|
||||
} else if (adapter?.scrollableHeaders?.isEmpty() == true) {
|
||||
adapter?.removeAllScrollableHeaders()
|
||||
adapter?.addScrollableHeader(presenter.headerItem)
|
||||
}
|
||||
|
@ -1802,7 +1800,7 @@ class MangaDetailsController :
|
|||
override fun onDestroyActionMode(mode: ActionMode?) {
|
||||
actionMode = null
|
||||
setStatusBarAndToolbar()
|
||||
if (startingRangeChapterPos != null && rangeMode == RangeMode.Download) {
|
||||
if (startingRangeChapterPos != null && rangeMode in setOf(RangeMode.Download, RangeMode.RemoveDownload)) {
|
||||
val item = adapter?.getItem(startingRangeChapterPos!!) as? ChapterItem
|
||||
(binding.recycler.findViewHolderForAdapterPosition(startingRangeChapterPos!!) as? ChapterHolder)?.notifyStatus(
|
||||
item?.status ?: Download.State.NOT_DOWNLOADED,
|
||||
|
|
|
@ -34,6 +34,7 @@ import eu.kanade.tachiyomi.data.track.EnhancedTrackService
|
|||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.data.track.TrackService
|
||||
import eu.kanade.tachiyomi.domain.manga.models.Manga
|
||||
import eu.kanade.tachiyomi.network.HttpException
|
||||
import eu.kanade.tachiyomi.network.NetworkPreferences
|
||||
import eu.kanade.tachiyomi.source.LocalSource
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
|
@ -60,6 +61,7 @@ import eu.kanade.tachiyomi.util.manga.MangaUtil
|
|||
import eu.kanade.tachiyomi.util.shouldDownloadNewChapters
|
||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||
import eu.kanade.tachiyomi.util.system.ImageUtil
|
||||
import eu.kanade.tachiyomi.util.system.e
|
||||
import eu.kanade.tachiyomi.util.system.launchIO
|
||||
import eu.kanade.tachiyomi.util.system.launchNonCancellableIO
|
||||
import eu.kanade.tachiyomi.util.system.launchNow
|
||||
|
@ -72,8 +74,11 @@ import java.io.FileOutputStream
|
|||
import java.io.OutputStream
|
||||
import java.util.Locale
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
|
@ -108,7 +113,8 @@ class MangaDetailsPresenter(
|
|||
private val downloadManager: DownloadManager = Injekt.get(),
|
||||
private val chapterFilter: ChapterFilter = Injekt.get(),
|
||||
private val storageManager: StorageManager = Injekt.get(),
|
||||
) : BaseCoroutinePresenter<MangaDetailsController>(), DownloadQueue.DownloadListener {
|
||||
) : BaseCoroutinePresenter<MangaDetailsController>(),
|
||||
DownloadQueue.Listener {
|
||||
private val getAvailableScanlators: GetAvailableScanlators by injectLazy()
|
||||
private val getCategories: GetCategories by injectLazy()
|
||||
private val getChapter: GetChapter by injectLazy()
|
||||
|
@ -174,6 +180,9 @@ class MangaDetailsPresenter(
|
|||
|
||||
var allChapterScanlators: Set<String> = emptySet()
|
||||
|
||||
override val progressJobs: MutableMap<Download, Job> = mutableMapOf()
|
||||
override val queueListenerScope get() = presenterScope
|
||||
|
||||
override fun onCreate() {
|
||||
val controller = view ?: return
|
||||
|
||||
|
@ -181,10 +190,24 @@ class MangaDetailsPresenter(
|
|||
if (!::manga.isInitialized) runBlocking { refreshMangaFromDb() }
|
||||
syncData()
|
||||
|
||||
downloadManager.addListener(this)
|
||||
presenterScope.launchUI {
|
||||
downloadManager.statusFlow()
|
||||
.filter { it.manga.id == mangaId }
|
||||
.catch { error -> Logger.e(error) }
|
||||
.collect(::onStatusChange)
|
||||
}
|
||||
presenterScope.launchUI {
|
||||
downloadManager.progressFlow()
|
||||
.filter { it.manga.id == mangaId }
|
||||
.catch { error -> Logger.e(error) }
|
||||
.collect(::onQueueUpdate)
|
||||
}
|
||||
presenterScope.launchIO {
|
||||
downloadManager.queueState.collectLatest(::onQueueUpdate)
|
||||
}
|
||||
|
||||
runBlocking {
|
||||
tracks = getTrack.awaitAllByMangaId(manga.id!!)
|
||||
tracks = getTrack.awaitAllByMangaId(mangaId)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -199,36 +222,35 @@ class MangaDetailsPresenter(
|
|||
.onEach { onUpdateManga() }
|
||||
.launchIn(presenterScope)
|
||||
|
||||
if (manga.isLocal()) {
|
||||
refreshAll()
|
||||
} else if (!manga.initialized) {
|
||||
isLoading = true
|
||||
controller.setRefresh(true)
|
||||
controller.updateHeader()
|
||||
refreshAll()
|
||||
} else {
|
||||
runBlocking { getChapters() }
|
||||
controller.updateChapters(this.chapters)
|
||||
getHistory()
|
||||
}
|
||||
val fetchMangaNeeded = !manga.initialized || manga.isLocal()
|
||||
val fetchChaptersNeeded = runBlocking { getChaptersNow() }.isEmpty() || manga.isLocal()
|
||||
|
||||
presenterScope.launch {
|
||||
isLoading = true
|
||||
withUIContext {
|
||||
controller.updateHeader()
|
||||
}
|
||||
val tasks = listOf(
|
||||
async { if (fetchMangaNeeded) fetchMangaFromSource() },
|
||||
async { if (fetchChaptersNeeded) fetchChaptersFromSource(false) },
|
||||
)
|
||||
tasks.awaitAll()
|
||||
isLoading = false
|
||||
withUIContext {
|
||||
controller.updateChapters()
|
||||
}
|
||||
|
||||
setTrackItems()
|
||||
}
|
||||
|
||||
refreshTracking(false)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
downloadManager.removeListener(this)
|
||||
}
|
||||
|
||||
fun fetchChapters(andTracking: Boolean = true) {
|
||||
presenterScope.launch {
|
||||
getChapters()
|
||||
if (andTracking) fetchTracks()
|
||||
withContext(Dispatchers.Main) { view?.updateChapters(chapters) }
|
||||
withUIContext { view?.updateChapters() }
|
||||
getHistory()
|
||||
}
|
||||
}
|
||||
|
@ -252,21 +274,19 @@ class MangaDetailsPresenter(
|
|||
return chapters
|
||||
}
|
||||
|
||||
private suspend fun getChapters() {
|
||||
private suspend fun getChapters(queue: List<Download> = downloadManager.queueState.value) {
|
||||
val chapters = getChapter.awaitAll(mangaId, isScanlatorFiltered()).map { it.toModel() }
|
||||
allChapters = if (!isScanlatorFiltered()) chapters else getChapter.awaitAll(mangaId, false).map { it.toModel() }
|
||||
|
||||
// Find downloaded chapters
|
||||
setDownloadedChapters(chapters)
|
||||
setDownloadedChapters(chapters, queue)
|
||||
allChapterScanlators = allChapters.mapNotNull { it.chapter.scanlator }.toSet()
|
||||
|
||||
this.chapters = applyChapterFilters(chapters)
|
||||
}
|
||||
|
||||
private fun getHistory() {
|
||||
presenterScope.launchIO {
|
||||
allHistory = getHistory.awaitAllByMangaId(mangaId)
|
||||
}
|
||||
private suspend fun getHistory() {
|
||||
allHistory = getHistory.awaitAllByMangaId(mangaId)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -274,33 +294,17 @@ class MangaDetailsPresenter(
|
|||
*
|
||||
* @param chapters the list of chapter from the database.
|
||||
*/
|
||||
private fun setDownloadedChapters(chapters: List<ChapterItem>) {
|
||||
private fun setDownloadedChapters(chapters: List<ChapterItem>, queue: List<Download>) {
|
||||
for (chapter in chapters) {
|
||||
if (downloadManager.isChapterDownloaded(chapter, manga)) {
|
||||
chapter.status = Download.State.DOWNLOADED
|
||||
} else if (downloadManager.hasQueue()) {
|
||||
chapter.status = downloadManager.queue.find { it.chapter.id == chapter.id }
|
||||
} else if (queue.isNotEmpty()) {
|
||||
chapter.status = queue.find { it.chapter.id == chapter.id }
|
||||
?.status ?: Download.State.default
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateDownload(download: Download) {
|
||||
chapters.find { it.id == download.chapter.id }?.download = download
|
||||
presenterScope.launchUI {
|
||||
view?.updateChapterDownload(download)
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateDownloads() {
|
||||
presenterScope.launch(Dispatchers.Default) {
|
||||
getChapters()
|
||||
withContext(Dispatchers.Main) {
|
||||
view?.updateChapters(chapters)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a chapter from the database to an extended model, allowing to store new fields.
|
||||
*/
|
||||
|
@ -310,7 +314,7 @@ class MangaDetailsPresenter(
|
|||
model.isLocked = isLockedFromSearch
|
||||
|
||||
// Find an active download for this chapter.
|
||||
val download = downloadManager.queue.find { it.chapter.id == id }
|
||||
val download = downloadManager.queueState.value.find { it.chapter.id == id }
|
||||
|
||||
if (download != null) {
|
||||
// If there's an active download, assign it.
|
||||
|
@ -387,14 +391,15 @@ class MangaDetailsPresenter(
|
|||
* @param chapter the chapter to delete.
|
||||
*/
|
||||
fun deleteChapter(chapter: ChapterItem) {
|
||||
downloadManager.deleteChapters(listOf(chapter), manga, source, true)
|
||||
this.chapters.find { it.id == chapter.id }?.apply {
|
||||
if (chapter.chapter.bookmark && !preferences.removeBookmarkedChapters().get()) return@apply
|
||||
status = Download.State.QUEUE
|
||||
status = Download.State.NOT_DOWNLOADED
|
||||
download = null
|
||||
}
|
||||
|
||||
view?.updateChapters(this.chapters)
|
||||
view?.updateChapters()
|
||||
|
||||
downloadManager.deleteChapters(listOf(chapter), manga, source, true)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -402,22 +407,21 @@ class MangaDetailsPresenter(
|
|||
* @param chapters the list of chapters to delete.
|
||||
*/
|
||||
fun deleteChapters(chapters: List<ChapterItem>, update: Boolean = true, isEverything: Boolean = false) {
|
||||
launchIO {
|
||||
if (isEverything) {
|
||||
downloadManager.deleteManga(manga, source)
|
||||
} else {
|
||||
downloadManager.deleteChapters(chapters, manga, source)
|
||||
}
|
||||
}
|
||||
chapters.forEach { chapter ->
|
||||
this.chapters.find { it.id == chapter.id }?.apply {
|
||||
if (chapter.chapter.bookmark && !preferences.removeBookmarkedChapters().get() && !isEverything) return@apply
|
||||
status = Download.State.QUEUE
|
||||
status = Download.State.NOT_DOWNLOADED
|
||||
download = null
|
||||
}
|
||||
}
|
||||
|
||||
if (update) view?.updateChapters(this.chapters)
|
||||
if (update) view?.updateChapters()
|
||||
|
||||
if (isEverything) {
|
||||
downloadManager.deleteManga(manga, source)
|
||||
} else {
|
||||
downloadManager.deleteChapters(chapters, manga, source)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun refreshMangaFromDb(): Manga {
|
||||
|
@ -426,39 +430,17 @@ class MangaDetailsPresenter(
|
|||
return dbManga
|
||||
}
|
||||
|
||||
/** Refresh Manga Info and Chapter List (not tracking) */
|
||||
fun refreshAll() {
|
||||
if (view?.isNotOnline() == true && !manga.isLocal()) return
|
||||
presenterScope.launch {
|
||||
isLoading = true
|
||||
var mangaError: java.lang.Exception? = null
|
||||
var chapterError: java.lang.Exception? = null
|
||||
val chapters = async(Dispatchers.IO) {
|
||||
try {
|
||||
source.getChapterList(manga.copy())
|
||||
} catch (e: Exception) {
|
||||
chapterError = e
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
val nManga = async(Dispatchers.IO) {
|
||||
try {
|
||||
source.getMangaDetails(manga.copy())
|
||||
} catch (e: java.lang.Exception) {
|
||||
mangaError = e
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
val networkManga = nManga.await()
|
||||
if (networkManga != null) {
|
||||
private suspend fun fetchMangaFromSource() {
|
||||
try {
|
||||
withIOContext {
|
||||
val networkManga = source.getMangaDetails(manga.copy())
|
||||
manga.prepareCoverUpdate(coverCache, networkManga, false)
|
||||
manga.copyFrom(networkManga)
|
||||
manga.initialized = true
|
||||
|
||||
updateManga.await(manga.toMangaUpdate())
|
||||
|
||||
launchIO {
|
||||
presenterScope.launchNonCancellableIO {
|
||||
val request =
|
||||
ImageRequest.Builder(preferences.context).data(manga.cover())
|
||||
.memoryCachePolicy(CachePolicy.DISABLED)
|
||||
|
@ -466,90 +448,68 @@ class MangaDetailsPresenter(
|
|||
.build()
|
||||
|
||||
if (preferences.context.imageLoader.execute(request) is SuccessResult) {
|
||||
withContext(Dispatchers.Main) {
|
||||
withUIContext {
|
||||
view?.setPaletteColor()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
val finChapters = chapters.await()
|
||||
if (finChapters.isNotEmpty()) {
|
||||
val newChapters = withIOContext { syncChaptersWithSource(finChapters, manga, source) }
|
||||
if (newChapters.first.isNotEmpty()) {
|
||||
if (manga.shouldDownloadNewChapters(preferences)) {
|
||||
} catch (e: Exception) {
|
||||
if (e is HttpException && e.code == 103) return
|
||||
withUIContext {
|
||||
view?.showError(trimException(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchChaptersFromSource(manualFetch: Boolean = true) {
|
||||
try {
|
||||
withIOContext {
|
||||
val chapters = source.getChapterList(manga.copy())
|
||||
val (added, removed) = withIOContext { syncChaptersWithSource(chapters, manga, source) }
|
||||
if (added.isNotEmpty()) {
|
||||
if (manga.shouldDownloadNewChapters(preferences) && manualFetch) {
|
||||
downloadChapters(
|
||||
newChapters.first.sortedBy { it.chapter_number }
|
||||
added.sortedBy { it.chapter_number }
|
||||
.map { it.toModel() },
|
||||
)
|
||||
}
|
||||
view?.view?.context?.let { mangaShortcutManager.updateShortcuts(it) }
|
||||
}
|
||||
if (newChapters.second.isNotEmpty()) {
|
||||
val removedChaptersId = newChapters.second.map { it.id }
|
||||
if (removed.isNotEmpty()) {
|
||||
val removedChaptersId = removed.map { it.id }
|
||||
val removedChapters = this@MangaDetailsPresenter.chapters.filter {
|
||||
it.id in removedChaptersId && it.isDownloaded
|
||||
}
|
||||
if (removedChapters.isNotEmpty()) {
|
||||
withContext(Dispatchers.Main) {
|
||||
view?.showChaptersRemovedPopup(
|
||||
removedChapters,
|
||||
)
|
||||
withUIContext {
|
||||
view?.showChaptersRemovedPopup(removedChapters)
|
||||
}
|
||||
}
|
||||
}
|
||||
getChapters()
|
||||
getHistory()
|
||||
}
|
||||
isLoading = false
|
||||
if (chapterError == null) {
|
||||
withContext(Dispatchers.Main) {
|
||||
view?.updateChapters(this@MangaDetailsPresenter.chapters)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
withUIContext {
|
||||
view?.showError(trimException(e))
|
||||
}
|
||||
if (chapterError != null) {
|
||||
withContext(Dispatchers.Main) {
|
||||
view?.showError(
|
||||
trimException(chapterError!!),
|
||||
)
|
||||
}
|
||||
return@launch
|
||||
} else if (mangaError != null) {
|
||||
withContext(Dispatchers.Main) {
|
||||
view?.showError(
|
||||
trimException(mangaError!!),
|
||||
)
|
||||
}
|
||||
}
|
||||
getHistory()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests an updated list of chapters from the source.
|
||||
*/
|
||||
fun fetchChaptersFromSource() {
|
||||
hasRequested = true
|
||||
isLoading = true
|
||||
|
||||
presenterScope.launch(Dispatchers.IO) {
|
||||
val chapters = try {
|
||||
source.getChapterList(manga.copy())
|
||||
} catch (e: Exception) {
|
||||
withContext(Dispatchers.Main) { view?.showError(trimException(e)) }
|
||||
return@launch
|
||||
}
|
||||
/** Refresh Manga Info and Chapter List (not tracking) */
|
||||
fun refreshAll() {
|
||||
if (view?.isNotOnline() == true && !manga.isLocal()) return
|
||||
presenterScope.launch {
|
||||
isLoading = true
|
||||
val tasks = listOf(
|
||||
async { fetchMangaFromSource() },
|
||||
async { fetchChaptersFromSource() },
|
||||
)
|
||||
tasks.awaitAll()
|
||||
isLoading = false
|
||||
try {
|
||||
syncChaptersWithSource(chapters, manga, source)
|
||||
|
||||
getChapters()
|
||||
withContext(Dispatchers.Main) {
|
||||
view?.updateChapters(this@MangaDetailsPresenter.chapters)
|
||||
}
|
||||
getHistory()
|
||||
} catch (e: java.lang.Exception) {
|
||||
withContext(Dispatchers.Main) {
|
||||
view?.showError(trimException(e))
|
||||
}
|
||||
withUIContext {
|
||||
view?.updateChapters()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -579,7 +539,7 @@ class MangaDetailsPresenter(
|
|||
}
|
||||
updateChapter.awaitAll(updates)
|
||||
getChapters()
|
||||
withContext(Dispatchers.Main) { view?.updateChapters(chapters) }
|
||||
withUIContext { view?.updateChapters() }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -609,7 +569,7 @@ class MangaDetailsPresenter(
|
|||
deleteChapters(selectedChapters, false)
|
||||
}
|
||||
getChapters()
|
||||
withContext(Dispatchers.Main) { view?.updateChapters(chapters) }
|
||||
withUIContext { view?.updateChapters() }
|
||||
if (read && deleteNow) {
|
||||
val latestReadChapter = selectedChapters.maxByOrNull { it.chapter_number.toInt() }?.chapter
|
||||
updateTrackChapterMarkedAsRead(preferences, latestReadChapter, manga.id) {
|
||||
|
@ -741,7 +701,7 @@ class MangaDetailsPresenter(
|
|||
private suspend fun asyncUpdateMangaAndChapters(justChapters: Boolean = false) {
|
||||
if (!justChapters) updateManga.await(MangaUpdate(manga.id!!, chapterFlags = manga.chapter_flags))
|
||||
getChapters()
|
||||
withUIContext { view?.updateChapters(chapters) }
|
||||
withUIContext { view?.updateChapters() }
|
||||
}
|
||||
|
||||
private fun isScanlatorFiltered() = manga.filtered_scanlators?.isNotEmpty() == true
|
||||
|
@ -1150,6 +1110,32 @@ class MangaDetailsPresenter(
|
|||
return if (date <= 0L) null else date
|
||||
}
|
||||
|
||||
override fun onStatusChange(download: Download) {
|
||||
super.onStatusChange(download)
|
||||
chapters.find { it.id == download.chapter.id }?.status = download.status
|
||||
onPageProgressUpdate(download)
|
||||
}
|
||||
|
||||
private suspend fun onQueueUpdate(queue: List<Download>) = withIOContext {
|
||||
getChapters(queue)
|
||||
withUIContext {
|
||||
view?.updateChapters()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onQueueUpdate(download: Download) {
|
||||
// already handled by onStatusChange
|
||||
}
|
||||
|
||||
override fun onProgressUpdate(download: Download) {
|
||||
// already handled by onStatusChange
|
||||
}
|
||||
|
||||
override fun onPageProgressUpdate(download: Download) {
|
||||
chapters.find { it.id == download.chapter.id }?.download = download
|
||||
view?.updateChapterDownload(download)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val MULTIPLE_VOLUMES = 1
|
||||
const val TENS_OF_CHAPTERS = 2
|
||||
|
|
|
@ -47,7 +47,7 @@ import eu.kanade.tachiyomi.util.system.isInNightMode
|
|||
import eu.kanade.tachiyomi.util.system.isLTR
|
||||
import eu.kanade.tachiyomi.util.view.resetStrokeColor
|
||||
import yokai.i18n.MR
|
||||
import yokai.presentation.core.util.coil.loadManga
|
||||
import yokai.util.coil.loadManga
|
||||
import yokai.util.lang.getString
|
||||
import android.R as AR
|
||||
|
||||
|
|
|
@ -18,6 +18,8 @@ import android.widget.TextView
|
|||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.widget.PopupMenu
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.SearchOff
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.core.net.toUri
|
||||
import androidx.core.view.WindowInsetsCompat.Type.systemBars
|
||||
|
@ -31,7 +33,6 @@ import com.google.android.material.datepicker.MaterialDatePicker
|
|||
import com.mikepenz.fastadapter.FastAdapter
|
||||
import com.mikepenz.fastadapter.adapters.ItemAdapter
|
||||
import com.mikepenz.fastadapter.listeners.addClickListener
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.track.EnhancedTrackService
|
||||
import eu.kanade.tachiyomi.data.track.TrackService
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
|
@ -317,7 +318,7 @@ class TrackingBottomSheet(private val controller: MangaDetailsController) :
|
|||
if (results.isEmpty()) {
|
||||
setMiddleTrackView(binding.searchEmptyView.id)
|
||||
binding.searchEmptyView.show(
|
||||
R.drawable.ic_search_off_24dp,
|
||||
Icons.Filled.SearchOff,
|
||||
MR.strings.no_results_found,
|
||||
)
|
||||
} else {
|
||||
|
@ -338,7 +339,7 @@ class TrackingBottomSheet(private val controller: MangaDetailsController) :
|
|||
binding.trackSearchRecycler.isVisible = false
|
||||
searchItemAdapter.clear()
|
||||
binding.searchEmptyView.show(
|
||||
R.drawable.ic_search_off_24dp,
|
||||
Icons.Filled.SearchOff,
|
||||
error.message ?: "",
|
||||
)
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ import eu.davidea.flexibleadapter.items.IFlexible
|
|||
import eu.kanade.tachiyomi.databinding.MangaListItemBinding
|
||||
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
|
||||
import eu.kanade.tachiyomi.util.view.setCards
|
||||
import yokai.presentation.core.util.coil.loadManga
|
||||
import yokai.util.coil.loadManga
|
||||
|
||||
class MangaHolder(
|
||||
view: View,
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue