mirror of
https://github.com/mihonapp/mihon.git
synced 2025-06-28 12:07:52 +02:00
Compare commits
192 Commits
Author | SHA1 | Date | |
---|---|---|---|
85791a9336 | |||
a4eba50cfd | |||
f48b2681e3 | |||
ab46bd56b0 | |||
c23506e887 | |||
9ad67a7b7d | |||
7a1b6142df | |||
478256d766 | |||
4d92caacef | |||
fd45de5c58 | |||
bcaa9674fe | |||
40aa3b7e18 | |||
5aea21a194 | |||
b5e118e2b4 | |||
dfec0e45ed | |||
ff2a4e6952 | |||
7660751f7f | |||
78b9ac4766 | |||
d5c75571dc | |||
16b9c459ab | |||
41c060e28b | |||
a3090e62f5 | |||
39b7024be0 | |||
d019c5999b | |||
20264eecb9 | |||
cc55453076 | |||
6cab2427f5 | |||
511bcc9197 | |||
00ac632d8f | |||
649209890d | |||
f2fca0f13d | |||
4084d5e69a | |||
e8beb7103c | |||
0e4ce0f1ae | |||
c42d517f6b | |||
356cd4ef52 | |||
88619145d8 | |||
6ba779fb7a | |||
8bd965267c | |||
7f76ffa5cb | |||
4acc7cee3d | |||
be28e0b559 | |||
116fec208b | |||
fece92e15a | |||
dce3049446 | |||
fcd6fe5d8a | |||
a69a833716 | |||
697b082591 | |||
b2d58e04d2 | |||
8bfc5f0450 | |||
a252a8acee | |||
447ee4bd09 | |||
3cd6382795 | |||
5d1134dfa8 | |||
05e7b0dc22 | |||
c0647c3110 | |||
ef84ed4982 | |||
a1e83b9f19 | |||
4ce4ee3c00 | |||
0d62aedfbb | |||
b7c2890250 | |||
ae97bb0445 | |||
117fd4bd0f | |||
bd424ce460 | |||
1dddba7f25 | |||
7fd75b7501 | |||
423f07033e | |||
ef9c457681 | |||
a6d4a3b785 | |||
2e487f8a3f | |||
2423a70abd | |||
13d39fc942 | |||
b7547a8458 | |||
8931dbb657 | |||
52416ff3a8 | |||
3dbfee91f6 | |||
09d4901781 | |||
62955e7385 | |||
1ef7722504 | |||
24bb2f02dc | |||
627698d81f | |||
d4c8480dee | |||
015e8deb79 | |||
714aa4b4ba | |||
8d5f798591 | |||
e65f59b3df | |||
341c3d179e | |||
67128937ca | |||
d9ea621e54 | |||
fb35d7af59 | |||
c254aa6fcc | |||
37d30eb887 | |||
49cdcc644c | |||
07e5525c74 | |||
776194f5b2 | |||
ed80ee98a7 | |||
040bac3da2 | |||
9df721d158 | |||
c50ede8b2c | |||
ba0907ae59 | |||
e9dce32a98 | |||
535cc0d81e | |||
5801297d78 | |||
51a33a47cd | |||
01a1a9ebab | |||
438bad9649 | |||
fe3b36caeb | |||
83588e14d9 | |||
64b1c9636b | |||
db0c1b2634 | |||
568c4d8c8e | |||
d645507eeb | |||
3548112ab2 | |||
0cb042cd93 | |||
0eadc028b6 | |||
82f3677168 | |||
70ed49e478 | |||
3c67a36b60 | |||
e5621246ec | |||
cb71d44024 | |||
7e3ea9074c | |||
e2cf157857 | |||
60890147c3 | |||
64c95305b9 | |||
feddd9285d | |||
d1b393965f | |||
e31a39b9d5 | |||
98fc028d39 | |||
88fd799a30 | |||
ef937f277e | |||
c3fb5af3fc | |||
859e8deb02 | |||
932c92412c | |||
05771ddf6d | |||
848d387ec4 | |||
ac6b4235b9 | |||
ab73e98075 | |||
aecdd04e04 | |||
e5cdf74587 | |||
8d25ce7323 | |||
8deca3b63a | |||
9b967177c5 | |||
4dfb3cc972 | |||
73e5e9ecd9 | |||
653b7ffcd0 | |||
8791b72cb1 | |||
d961492380 | |||
07de367476 | |||
31d96c2bf0 | |||
fb8aafb69f | |||
3d58b78062 | |||
ec5e6958ef | |||
71bd5fe367 | |||
6385c71c72 | |||
d43255e688 | |||
3527dedc99 | |||
de50f53be4 | |||
f2e4b2fc99 | |||
e6f3cd03bb | |||
a1e31549a2 | |||
71d225c562 | |||
7c23212850 | |||
fdf178d4df | |||
04ebca8413 | |||
edeee54fb2 | |||
a906e9b302 | |||
fff72b61df | |||
74381ef59e | |||
64f95af3e5 | |||
85a1eb75c9 | |||
597cec3064 | |||
b03ebc1fa4 | |||
6c53bb4d51 | |||
fb7a458747 | |||
db25a9ae4f | |||
c69420373a | |||
2b8347f899 | |||
281a3911f6 | |||
9b77dd9a2b | |||
cb8cff3179 | |||
3db85c7274 | |||
b41ac355a0 | |||
88d9ffe92e | |||
5113c78ab6 | |||
3854995ef2 | |||
36e14b951a | |||
9299a4beff | |||
d681bea395 | |||
0f3f1e9226 | |||
79ab492a5b | |||
62db4bb09d | |||
7be2cbb75b |
2
.github/ISSUE_TEMPLATE.md
vendored
2
.github/ISSUE_TEMPLATE.md
vendored
@ -3,7 +3,7 @@
|
|||||||
I acknowledge that:
|
I acknowledge that:
|
||||||
|
|
||||||
- I have updated:
|
- I have updated:
|
||||||
- To the latest version of the app (stable is v0.11.0)
|
- To the latest version of the app (stable is v0.12.1)
|
||||||
- All extensions
|
- All extensions
|
||||||
- I have tried the troubleshooting guide: https://tachiyomi.org/help/guides/troubleshooting-problems/
|
- I have tried the troubleshooting guide: https://tachiyomi.org/help/guides/troubleshooting-problems/
|
||||||
- If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions
|
- If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions
|
||||||
|
44
.github/ISSUE_TEMPLATE/bug_report.md
vendored
44
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -1,44 +0,0 @@
|
|||||||
---
|
|
||||||
name: "🐞 Bug report"
|
|
||||||
about: Report a bug
|
|
||||||
title: "[Bug] <Write short description here>"
|
|
||||||
labels: "bug"
|
|
||||||
---
|
|
||||||
|
|
||||||
**PLEASE READ THIS**
|
|
||||||
|
|
||||||
I acknowledge that:
|
|
||||||
|
|
||||||
- I have updated:
|
|
||||||
- To the latest version of the app (stable is v0.11.0)
|
|
||||||
- All extensions
|
|
||||||
- I have tried the troubleshooting guide: https://tachiyomi.org/help/guides/troubleshooting-problems/
|
|
||||||
- If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions
|
|
||||||
- I have searched the existing issues and this is new ticket **NOT** a duplicate or related to another open issue
|
|
||||||
- I will fill out the title and the information in this template
|
|
||||||
|
|
||||||
Note that the issue will be automatically closed if you do not fill out the title or requested information.
|
|
||||||
|
|
||||||
**DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Device information
|
|
||||||
* Tachiyomi version: ?
|
|
||||||
* Android version: ?
|
|
||||||
* Device: ?
|
|
||||||
|
|
||||||
## Steps to reproduce
|
|
||||||
1. First step
|
|
||||||
2. Second step
|
|
||||||
|
|
||||||
### Expected behavior
|
|
||||||
This should happen.
|
|
||||||
|
|
||||||
### Actual behavior
|
|
||||||
This happened instead.
|
|
||||||
|
|
||||||
## Other details
|
|
||||||
Additional details and attachments.
|
|
||||||
|
|
||||||
If you're experiencing crashes, share the crash logs from More → Settings → Advanced → Dump crash logs.
|
|
13
.github/ISSUE_TEMPLATE/config.yml
vendored
13
.github/ISSUE_TEMPLATE/config.yml
vendored
@ -1,8 +1,11 @@
|
|||||||
blank_issues_enabled: false
|
blank_issues_enabled: false
|
||||||
contact_links:
|
contact_links:
|
||||||
- name: Tachiyomi help website
|
- name: ⚠️ Extension/source issue
|
||||||
|
url: https://github.com/tachiyomiorg/tachiyomi-extensions/issues/new/choose
|
||||||
|
about: Issues and requests for extensions and sources should be opened in the tachiyomi-extensions repository instead
|
||||||
|
- name: 📦 Tachiyomi extensions
|
||||||
|
url: https://tachiyomi.org/extensions
|
||||||
|
about: List of all available extensions with download links
|
||||||
|
- name: 🖥️ Tachiyomi website
|
||||||
url: https://tachiyomi.org/help/
|
url: https://tachiyomi.org/help/
|
||||||
about: Common questions are answered here.
|
about: Guides, troubleshooting, and answers to common questions
|
||||||
- name: Tachiyomi extensions GitHub repository
|
|
||||||
url: https://github.com/tachiyomiorg/tachiyomi-extensions
|
|
||||||
about: Issues about an extension/source/catalogue should be opened here instead.
|
|
||||||
|
29
.github/ISSUE_TEMPLATE/feature_request.md
vendored
29
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@ -1,29 +0,0 @@
|
|||||||
---
|
|
||||||
name: "🌟 Feature request"
|
|
||||||
about: Suggest a feature to improve Tachiyomi
|
|
||||||
title: "[Feature Request] <Write short description here>"
|
|
||||||
labels: "feature"
|
|
||||||
---
|
|
||||||
|
|
||||||
**PLEASE READ THIS**
|
|
||||||
|
|
||||||
I acknowledge that:
|
|
||||||
|
|
||||||
- I have updated:
|
|
||||||
- To the latest version of the app (stable is v0.11.0)
|
|
||||||
- All extensions
|
|
||||||
- If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions
|
|
||||||
- I have searched the existing issues and this is new ticket **NOT** a duplicate or related to another open issue
|
|
||||||
- I will fill out the title and the information in this template
|
|
||||||
|
|
||||||
Note that the issue will be automatically closed if you do not fill out the title or requested information.
|
|
||||||
|
|
||||||
**DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Why/User Benefit/User Problem
|
|
||||||
(explain why this feature should be added)
|
|
||||||
|
|
||||||
## What/Requirements
|
|
||||||
(explain how this feature would behave)
|
|
106
.github/ISSUE_TEMPLATE/report_issue.yml
vendored
Normal file
106
.github/ISSUE_TEMPLATE/report_issue.yml
vendored
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
name: 🐞 Issue report
|
||||||
|
description: Report an issue in Tachiyomi
|
||||||
|
labels: [Bug]
|
||||||
|
body:
|
||||||
|
|
||||||
|
- type: checkboxes
|
||||||
|
id: acknowledgements
|
||||||
|
attributes:
|
||||||
|
label: Acknowledgements
|
||||||
|
description: Read this carefully, we will close and ignore your issue if you skimmed through this.
|
||||||
|
options:
|
||||||
|
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open issue.
|
||||||
|
required: true
|
||||||
|
- label: I have written a short but informative title.
|
||||||
|
required: true
|
||||||
|
- label: If this is an issue with an extension, I should be opening an issue in the [extensions repository](https://github.com/tachiyomiorg/tachiyomi-extensions/issues/new/choose).
|
||||||
|
required: true
|
||||||
|
- label: I have tried the [troubleshooting guide](https://tachiyomi.org/help/guides/troubleshooting/).
|
||||||
|
required: true
|
||||||
|
- label: I have updated the app to version **[0.12.1](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**.
|
||||||
|
required: true
|
||||||
|
- label: I have updated all installed extensions.
|
||||||
|
required: true
|
||||||
|
- label: I will fill out all of the requested information in this form.
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: tachiyomi-version
|
||||||
|
attributes:
|
||||||
|
label: Tachiyomi version
|
||||||
|
description: You can find your Tachiyomi version in **More → About**.
|
||||||
|
placeholder: |
|
||||||
|
Example: "0.11.1"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: android-version
|
||||||
|
attributes:
|
||||||
|
label: Android version
|
||||||
|
description: You can find this somewhere in your Android settings.
|
||||||
|
placeholder: |
|
||||||
|
Example: "Android 11"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: device
|
||||||
|
attributes:
|
||||||
|
label: Device
|
||||||
|
description: List your device and model.
|
||||||
|
placeholder: |
|
||||||
|
Example: "Google Pixel 5"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: reproduce-steps
|
||||||
|
attributes:
|
||||||
|
label: Steps to reproduce
|
||||||
|
description: Provide an example of the issue.
|
||||||
|
placeholder: |
|
||||||
|
Example:
|
||||||
|
1. First step
|
||||||
|
2. Second step
|
||||||
|
3. Issue here
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: expected-behavior
|
||||||
|
attributes:
|
||||||
|
label: Expected behavior
|
||||||
|
description: Explain what you should expect to happen.
|
||||||
|
placeholder: |
|
||||||
|
Example:
|
||||||
|
"This should happen..."
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: actual-behavior
|
||||||
|
attributes:
|
||||||
|
label: Actual behavior
|
||||||
|
description: Explain what actually happens.
|
||||||
|
placeholder: |
|
||||||
|
Example:
|
||||||
|
"This happened instead..."
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: crash-logs
|
||||||
|
attributes:
|
||||||
|
label: Crash logs
|
||||||
|
description: |
|
||||||
|
If you're experiencing crashes, share the crash logs from **More → Settings → Advanced** then press **Dump crash logs**.
|
||||||
|
placeholder: |
|
||||||
|
You can paste the crash logs in pure text or upload it as an attachment.
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: other-details
|
||||||
|
attributes:
|
||||||
|
label: Other details
|
||||||
|
placeholder: |
|
||||||
|
Additional details and attachments.
|
39
.github/ISSUE_TEMPLATE/request_feature.yml
vendored
Normal file
39
.github/ISSUE_TEMPLATE/request_feature.yml
vendored
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
name: ⭐ Feature request
|
||||||
|
description: Suggest a feature to improve Tachiyomi
|
||||||
|
labels: [Feature request]
|
||||||
|
body:
|
||||||
|
|
||||||
|
- type: checkboxes
|
||||||
|
id: acknowledgements
|
||||||
|
attributes:
|
||||||
|
label: Acknowledgements
|
||||||
|
description: Read this carefully, we will close and ignore your issue if you skimmed through this.
|
||||||
|
options:
|
||||||
|
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open issue.
|
||||||
|
required: true
|
||||||
|
- label: I have written a short but informative title.
|
||||||
|
required: true
|
||||||
|
- label: If this is an issue with an extension, I should be opening an issue in the [extensions repository](https://github.com/tachiyomiorg/tachiyomi-extensions/issues/new/choose).
|
||||||
|
required: true
|
||||||
|
- label: I have updated the app to version **[0.12.1](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**.
|
||||||
|
required: true
|
||||||
|
- label: I will fill out all of the requested information in this form.
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: feature-description
|
||||||
|
attributes:
|
||||||
|
label: Describe your suggested feature
|
||||||
|
description: How can Tachiyomi be improved?
|
||||||
|
placeholder: |
|
||||||
|
Example:
|
||||||
|
"It should work like this..."
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: other-details
|
||||||
|
attributes:
|
||||||
|
label: Other details
|
||||||
|
placeholder: |
|
||||||
|
Additional details and attachments.
|
8
.github/ISSUE_TEMPLATE/source_issue.md
vendored
8
.github/ISSUE_TEMPLATE/source_issue.md
vendored
@ -1,8 +0,0 @@
|
|||||||
---
|
|
||||||
name: "Extension/source/catalogue issue"
|
|
||||||
about: "Do not open an issue here. See https://github.com/tachiyomiorg/tachiyomi-extensions"
|
|
||||||
title: "THIS ISSUE IS IN THE WRONG REPO; SEE https://github.com/tachiyomiorg/tachiyomi-extensions"
|
|
||||||
labels: "catalog, invalid"
|
|
||||||
---
|
|
||||||
|
|
||||||
DO NOT OPEN AN ISSUE IN THIS REPO. SEE https://github.com/tachiyomiorg/tachiyomi-extensions
|
|
3
.github/workflows/build.yml
vendored
3
.github/workflows/build.yml
vendored
@ -22,7 +22,6 @@ jobs:
|
|||||||
build:
|
build:
|
||||||
name: Build app
|
name: Build app
|
||||||
needs: check_wrapper
|
needs: check_wrapper
|
||||||
if: "!startsWith(github.event.head_commit.message, '[SKIP CI]')"
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@ -61,6 +60,8 @@ jobs:
|
|||||||
set -x
|
set -x
|
||||||
echo "VERSION_TAG=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_ENV
|
echo "VERSION_TAG=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
# TODO: need to support multiple APKs
|
||||||
|
|
||||||
- name: Sign APK
|
- name: Sign APK
|
||||||
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'tachiyomiorg/tachiyomi'
|
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'tachiyomiorg/tachiyomi'
|
||||||
uses: r0adkll/sign-android-release@v1
|
uses: r0adkll/sign-android-release@v1
|
||||||
|
16
.github/workflows/issue_closer.yml
vendored
16
.github/workflows/issue_closer.yml
vendored
@ -13,16 +13,6 @@ jobs:
|
|||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
rules: |
|
rules: |
|
||||||
[
|
[
|
||||||
{
|
|
||||||
"type": "title",
|
|
||||||
"regex": ".*THIS ISSUE IS IN THE WRONG REPO.*",
|
|
||||||
"message": "It was not opened in the correct repo, as the template mentioned."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "title",
|
|
||||||
"regex": ".*<Write short description here>*",
|
|
||||||
"message": "The description in the title was not filled out."
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"type": "body",
|
"type": "body",
|
||||||
"regex": ".*DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT.*",
|
"regex": ".*DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT.*",
|
||||||
@ -32,5 +22,11 @@ jobs:
|
|||||||
"type": "body",
|
"type": "body",
|
||||||
"regex": ".*\\* (Tachiyomi version|Android version|Device): \\?.*",
|
"regex": ".*\\* (Tachiyomi version|Android version|Device): \\?.*",
|
||||||
"message": "Requested information in the template was not filled out."
|
"message": "Requested information in the template was not filled out."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "both",
|
||||||
|
"regex": ".*(aniyomi|anime).*",
|
||||||
|
"ignoreCase": true,
|
||||||
|
"message": "Tachiyomi does not support anime, and has no plans to support anime. In addition Tachiyomi is not affiliated with Aniyomi https://github.com/jmir1/aniyomi"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
2
.github/workflows/issue_moderator.yml
vendored
2
.github/workflows/issue_moderator.yml
vendored
@ -9,6 +9,6 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Moderate issues
|
- name: Moderate issues
|
||||||
uses: tachiyomiorg/issue-moderator-action@v1.0
|
uses: tachiyomiorg/issue-moderator-action@v1.1
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
@ -26,7 +26,7 @@ When creating a fork, remember to:
|
|||||||
- To avoid confusion with the main app:
|
- To avoid confusion with the main app:
|
||||||
- Change the app name
|
- Change the app name
|
||||||
- Change the app icon
|
- Change the app icon
|
||||||
- Change or disable the [app update checker](https://github.com/tachiyomiorg/tachiyomi/blob/master/app/src/main/java/eu/kanade/tachiyomi/data/updater/github/GithubUpdateChecker.kt)
|
- Change or disable the [app update checker](https://github.com/tachiyomiorg/tachiyomi/blob/master/app/src/main/java/eu/kanade/tachiyomi/data/updater/GithubUpdateChecker.kt)
|
||||||
- To avoid installation conflicts:
|
- To avoid installation conflicts:
|
||||||
- Change the `applicationId` in [`build.gradle.kts`](https://github.com/tachiyomiorg/tachiyomi/blob/master/app/build.gradle.kts)
|
- Change the `applicationId` in [`build.gradle.kts`](https://github.com/tachiyomiorg/tachiyomi/blob/master/app/build.gradle.kts)
|
||||||
- To avoid having your data polluting the main app's analytics and crash report services:
|
- To avoid having your data polluting the main app's analytics and crash report services:
|
||||||
|
11
README.md
11
README.md
@ -1,6 +1,6 @@
|
|||||||
| Build | Stable | Weekly Preview | Contribute | Support Server |
|
| Build | Stable | Weekly Preview | Contribute | Support Server |
|
||||||
|-------|----------|---------|------------|---------|
|
|-------|----------|---------|------------|---------|
|
||||||
|  | [](https://github.com/tachiyomiorg/tachiyomi/releases) | [](https://github.com/tachiyomiorg/tachiyomi-preview/releases) | [](https://hosted.weblate.org/engage/tachiyomi/?utm_source=widget) | [](https://discord.gg/tachiyomi) |
|
|  | [](https://github.com/tachiyomiorg/tachiyomi/releases) | [](https://github.com/tachiyomiorg/tachiyomi-preview/releases) | [](https://hosted.weblate.org/engage/tachiyomi/?utm_source=widget) | [](https://discord.gg/tachiyomi) |
|
||||||
|
|
||||||
|
|
||||||
# Tachiyomi
|
# Tachiyomi
|
||||||
@ -11,10 +11,10 @@ Tachiyomi is a free and open source manga reader for Android 6.0 and above.
|
|||||||
## Features
|
## Features
|
||||||
|
|
||||||
Features include:
|
Features include:
|
||||||
* Online reading from [a variety of sources](https://github.com/tachiyomiorg/tachiyomi-extensions)
|
* Online reading from a variety of sources
|
||||||
* Local reading of downloaded manga
|
* Local reading of downloaded content
|
||||||
* A configurable reader with multiple viewers, reading directions and other settings.
|
* A configurable reader with multiple viewers, reading directions and other settings.
|
||||||
* [MyAnimeList](https://myanimelist.net/), [AniList](https://anilist.co/), [Kitsu](https://kitsu.io/), [Shikimori](https://shikimori.one), and [Bangumi](https://bgm.tv/) support
|
* Tracker support: [MyAnimeList](https://myanimelist.net/), [AniList](https://anilist.co/), [Kitsu](https://kitsu.io/), [Shikimori](https://shikimori.one), and [Bangumi](https://bgm.tv/)
|
||||||
* Categories to organize your library
|
* Categories to organize your library
|
||||||
* Light and dark themes
|
* Light and dark themes
|
||||||
* Schedule updating your library for new chapters
|
* Schedule updating your library for new chapters
|
||||||
@ -23,7 +23,7 @@ Features include:
|
|||||||
## Download
|
## Download
|
||||||
Get the app from our [releases page](https://github.com/tachiyomiorg/tachiyomi/releases).
|
Get the app from our [releases page](https://github.com/tachiyomiorg/tachiyomi/releases).
|
||||||
|
|
||||||
If you want to try new features before they get to the stable release, you can download the preview version [here](https://github.com/tachiyomiorg/android-app-preview/releases).
|
If you want to try new features before they get to the stable release, you can download the preview version [here](https://github.com/tachiyomiorg/tachiyomi-preview/releases).
|
||||||
|
|
||||||
## Issues, Feature Requests and Contributing
|
## Issues, Feature Requests and Contributing
|
||||||
|
|
||||||
@ -44,7 +44,6 @@ Please make sure to read the full guidelines. Your issue may be closed without w
|
|||||||
* Include steps to reproduce (if not obvious from description)
|
* Include steps to reproduce (if not obvious from description)
|
||||||
* Include screenshot (if needed)
|
* Include screenshot (if needed)
|
||||||
* If it could be device-dependent, try reproducing on another device (if possible)
|
* If it could be device-dependent, try reproducing on another device (if possible)
|
||||||
* For large logs use http://pastebin.com/ (or similar)
|
|
||||||
* Don't group unrelated requests into one issue
|
* Don't group unrelated requests into one issue
|
||||||
|
|
||||||
DO: https://github.com/tachiyomiorg/tachiyomi/issues/24 https://github.com/tachiyomiorg/tachiyomi/issues/71
|
DO: https://github.com/tachiyomiorg/tachiyomi/issues/24 https://github.com/tachiyomiorg/tachiyomi/issues/71
|
||||||
|
@ -18,18 +18,19 @@ if (gradle.startParameter.taskRequests.toString().contains("Standard")) {
|
|||||||
|
|
||||||
shortcutHelper.setFilePath("./shortcuts.xml")
|
shortcutHelper.setFilePath("./shortcuts.xml")
|
||||||
|
|
||||||
|
val SUPPORTED_ABIS = setOf("armeabi-v7a", "arm64-v8a", "x86")
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion(AndroidConfig.compileSdk)
|
compileSdk = AndroidConfig.compileSdk
|
||||||
buildToolsVersion(AndroidConfig.buildTools)
|
|
||||||
ndkVersion = AndroidConfig.ndk
|
ndkVersion = AndroidConfig.ndk
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "eu.kanade.tachiyomi"
|
applicationId = "eu.kanade.tachiyomi"
|
||||||
minSdkVersion(AndroidConfig.minSdk)
|
minSdk = AndroidConfig.minSdk
|
||||||
targetSdkVersion(AndroidConfig.targetSdk)
|
targetSdk = AndroidConfig.targetSdk
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
versionCode = 62
|
versionCode = 67
|
||||||
versionName = "0.11.0"
|
versionName = "0.12.1"
|
||||||
|
|
||||||
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
||||||
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
|
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
|
||||||
@ -39,15 +40,18 @@ android {
|
|||||||
// Please disable ACRA or use your own instance in forked versions of the project
|
// Please disable ACRA or use your own instance in forked versions of the project
|
||||||
buildConfigField("String", "ACRA_URI", "\"https://tachiyomi.kanade.eu/crash_report\"")
|
buildConfigField("String", "ACRA_URI", "\"https://tachiyomi.kanade.eu/crash_report\"")
|
||||||
|
|
||||||
multiDexEnabled = true
|
|
||||||
|
|
||||||
ndk {
|
ndk {
|
||||||
abiFilters += setOf("armeabi-v7a", "arm64-v8a", "x86")
|
abiFilters += SUPPORTED_ABIS
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
buildFeatures {
|
splits {
|
||||||
viewBinding = true
|
abi {
|
||||||
|
isEnable = false
|
||||||
|
reset()
|
||||||
|
include(*SUPPORTED_ABIS.toTypedArray())
|
||||||
|
isUniversalApk = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
@ -101,7 +105,11 @@ android {
|
|||||||
includeInApk = false
|
includeInApk = false
|
||||||
}
|
}
|
||||||
|
|
||||||
lintOptions {
|
buildFeatures {
|
||||||
|
viewBinding = true
|
||||||
|
}
|
||||||
|
|
||||||
|
lint {
|
||||||
disable("MissingTranslation", "ExtraTranslation")
|
disable("MissingTranslation", "ExtraTranslation")
|
||||||
isAbortOnError = false
|
isAbortOnError = false
|
||||||
isCheckReleaseBuilds = false
|
isCheckReleaseBuilds = false
|
||||||
@ -119,21 +127,27 @@ android {
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|
||||||
|
implementation(kotlin("reflect", version = BuildPluginsVersion.KOTLIN))
|
||||||
|
|
||||||
|
val coroutinesVersion = "1.5.1"
|
||||||
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion")
|
||||||
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion")
|
||||||
|
|
||||||
// Source models and interfaces from Tachiyomi 1.x
|
// Source models and interfaces from Tachiyomi 1.x
|
||||||
implementation("org.tachiyomi:source-api:1.1")
|
implementation("org.tachiyomi:source-api:1.1")
|
||||||
|
|
||||||
// AndroidX libraries
|
// AndroidX libraries
|
||||||
implementation("androidx.annotation:annotation:1.3.0-alpha01")
|
implementation("androidx.annotation:annotation:1.3.0-alpha01")
|
||||||
implementation("androidx.appcompat:appcompat:1.4.0-alpha01")
|
implementation("androidx.appcompat:appcompat:1.4.0-alpha03")
|
||||||
implementation("androidx.biometric:biometric-ktx:1.2.0-alpha03")
|
implementation("androidx.biometric:biometric-ktx:1.2.0-alpha03")
|
||||||
implementation("androidx.browser:browser:1.3.0")
|
implementation("androidx.browser:browser:1.3.0")
|
||||||
implementation("androidx.cardview:cardview:1.0.0")
|
implementation("androidx.cardview:cardview:1.0.0")
|
||||||
implementation("androidx.constraintlayout:constraintlayout:2.1.0-beta02")
|
implementation("androidx.constraintlayout:constraintlayout:2.1.0")
|
||||||
implementation("androidx.coordinatorlayout:coordinatorlayout:1.1.0")
|
implementation("androidx.coordinatorlayout:coordinatorlayout:1.1.0")
|
||||||
implementation("androidx.core:core-ktx:1.6.0-beta01")
|
implementation("androidx.core:core-ktx:1.7.0-alpha01")
|
||||||
implementation("androidx.multidex:multidex:2.0.1")
|
implementation("androidx.core:core-splashscreen:1.0.0-alpha01")
|
||||||
implementation("androidx.preference:preference-ktx:1.1.1")
|
implementation("androidx.preference:preference-ktx:1.1.1")
|
||||||
implementation("androidx.recyclerview:recyclerview:1.2.0")
|
implementation("androidx.recyclerview:recyclerview:1.2.1")
|
||||||
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01")
|
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01")
|
||||||
|
|
||||||
val lifecycleVersion = "2.4.0-alpha01"
|
val lifecycleVersion = "2.4.0-alpha01"
|
||||||
@ -142,10 +156,10 @@ dependencies {
|
|||||||
implementation("androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion")
|
implementation("androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion")
|
||||||
|
|
||||||
// Job scheduling
|
// Job scheduling
|
||||||
implementation("androidx.work:work-runtime-ktx:2.7.0-alpha03")
|
implementation("androidx.work:work-runtime-ktx:2.6.0-beta01")
|
||||||
|
|
||||||
// UI library
|
// UI library
|
||||||
implementation("com.google.android.material:material:1.4.0-beta01")
|
implementation("com.google.android.material:material:1.5.0-alpha01")
|
||||||
|
|
||||||
"standardImplementation"("com.google.firebase:firebase-core:19.0.0")
|
"standardImplementation"("com.google.firebase:firebase-core:19.0.0")
|
||||||
|
|
||||||
@ -163,13 +177,13 @@ dependencies {
|
|||||||
implementation("com.squareup.okio:okio:2.10.0")
|
implementation("com.squareup.okio:okio:2.10.0")
|
||||||
|
|
||||||
// TLS 1.3 support for Android < 10
|
// TLS 1.3 support for Android < 10
|
||||||
implementation("org.conscrypt:conscrypt-android:2.5.1")
|
implementation("org.conscrypt:conscrypt-android:2.5.2")
|
||||||
|
|
||||||
// JSON
|
// JSON
|
||||||
val kotlinSerializationVersion = "1.2.0"
|
val kotlinSerializationVersion = "1.2.2"
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinSerializationVersion")
|
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinSerializationVersion")
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-protobuf:$kotlinSerializationVersion")
|
implementation("org.jetbrains.kotlinx:kotlinx-serialization-protobuf:$kotlinSerializationVersion")
|
||||||
implementation("com.google.code.gson:gson:2.8.6")
|
implementation("com.google.code.gson:gson:2.8.7")
|
||||||
implementation("com.github.salomonbrys.kotson:kotson:2.5.0")
|
implementation("com.github.salomonbrys.kotson:kotson:2.5.0")
|
||||||
|
|
||||||
// JavaScript engine
|
// JavaScript engine
|
||||||
@ -181,13 +195,13 @@ dependencies {
|
|||||||
implementation("com.github.junrar:junrar:7.4.0")
|
implementation("com.github.junrar:junrar:7.4.0")
|
||||||
|
|
||||||
// HTML parser
|
// HTML parser
|
||||||
implementation("org.jsoup:jsoup:1.13.1")
|
implementation("org.jsoup:jsoup:1.14.1")
|
||||||
|
|
||||||
// Database
|
// Database
|
||||||
implementation("androidx.sqlite:sqlite-ktx:2.1.0")
|
implementation("androidx.sqlite:sqlite-ktx:2.1.0")
|
||||||
implementation("com.github.inorichi.storio:storio-common:8be19de@aar")
|
implementation("com.github.inorichi.storio:storio-common:8be19de@aar")
|
||||||
implementation("com.github.inorichi.storio:storio-sqlite:8be19de@aar")
|
implementation("com.github.inorichi.storio:storio-sqlite:8be19de@aar")
|
||||||
implementation("com.github.requery:sqlite-android:3.35.5")
|
implementation("com.github.requery:sqlite-android:3.36.0")
|
||||||
|
|
||||||
// Preferences
|
// Preferences
|
||||||
implementation("com.github.tfcporciuncula.flow-preferences:flow-preferences:1.4.0")
|
implementation("com.github.tfcporciuncula.flow-preferences:flow-preferences:1.4.0")
|
||||||
@ -201,14 +215,14 @@ dependencies {
|
|||||||
implementation("com.github.inorichi.injekt:injekt-core:65b0440")
|
implementation("com.github.inorichi.injekt:injekt-core:65b0440")
|
||||||
|
|
||||||
// Image library
|
// Image library
|
||||||
val coilVersion = "1.2.1"
|
val coilVersion = "1.3.2"
|
||||||
implementation("io.coil-kt:coil:$coilVersion")
|
implementation("io.coil-kt:coil:$coilVersion")
|
||||||
implementation("io.coil-kt:coil-gif:$coilVersion")
|
implementation("io.coil-kt:coil-gif:$coilVersion")
|
||||||
|
|
||||||
implementation("com.github.tachiyomiorg:subsampling-scale-image-view:846abe0") {
|
implementation("com.github.tachiyomiorg:subsampling-scale-image-view:846abe0") {
|
||||||
exclude(module = "image-decoder")
|
exclude(module = "image-decoder")
|
||||||
}
|
}
|
||||||
implementation("com.github.tachiyomiorg:image-decoder:7a44c9b")
|
implementation("com.github.tachiyomiorg:image-decoder:7481a4a")
|
||||||
|
|
||||||
// Logging
|
// Logging
|
||||||
implementation("com.jakewharton.timber:timber:4.7.1")
|
implementation("com.jakewharton.timber:timber:4.7.1")
|
||||||
@ -228,21 +242,14 @@ dependencies {
|
|||||||
implementation("com.github.tachiyomiorg:DirectionalViewPager:1.0.0")
|
implementation("com.github.tachiyomiorg:DirectionalViewPager:1.0.0")
|
||||||
implementation("dev.chrisbanes.insetter:insetter:0.6.0")
|
implementation("dev.chrisbanes.insetter:insetter:0.6.0")
|
||||||
|
|
||||||
// 3.2.0+ introduces weird UI blinking or cut off issues on some devices
|
|
||||||
val materialDialogsVersion = "3.1.1"
|
|
||||||
implementation("com.afollestad.material-dialogs:core:$materialDialogsVersion")
|
|
||||||
implementation("com.afollestad.material-dialogs:input:$materialDialogsVersion")
|
|
||||||
implementation("com.afollestad.material-dialogs:datetime:$materialDialogsVersion")
|
|
||||||
|
|
||||||
// Conductor
|
// Conductor
|
||||||
implementation("com.bluelinelabs:conductor:2.1.5")
|
val conductorVersion = "3.0.0"
|
||||||
implementation("com.bluelinelabs:conductor-support:2.1.5") {
|
implementation("com.bluelinelabs:conductor:$conductorVersion")
|
||||||
exclude(group = "com.android.support")
|
implementation("com.bluelinelabs:conductor-viewpager:$conductorVersion")
|
||||||
}
|
implementation("com.github.tachiyomiorg:conductor-support-preference:$conductorVersion")
|
||||||
implementation("com.github.tachiyomiorg:conductor-support-preference:2.0.1")
|
|
||||||
|
|
||||||
// FlowBinding
|
// FlowBinding
|
||||||
val flowbindingVersion = "1.0.0"
|
val flowbindingVersion = "1.2.0"
|
||||||
implementation("io.github.reactivecircus.flowbinding:flowbinding-android:$flowbindingVersion")
|
implementation("io.github.reactivecircus.flowbinding:flowbinding-android:$flowbindingVersion")
|
||||||
implementation("io.github.reactivecircus.flowbinding:flowbinding-appcompat:$flowbindingVersion")
|
implementation("io.github.reactivecircus.flowbinding:flowbinding-appcompat:$flowbindingVersion")
|
||||||
implementation("io.github.reactivecircus.flowbinding:flowbinding-recyclerview:$flowbindingVersion")
|
implementation("io.github.reactivecircus.flowbinding:flowbinding-recyclerview:$flowbindingVersion")
|
||||||
@ -259,15 +266,8 @@ dependencies {
|
|||||||
|
|
||||||
val robolectricVersion = "3.1.4"
|
val robolectricVersion = "3.1.4"
|
||||||
testImplementation("org.robolectric:robolectric:$robolectricVersion")
|
testImplementation("org.robolectric:robolectric:$robolectricVersion")
|
||||||
testImplementation("org.robolectric:shadows-multidex:$robolectricVersion")
|
|
||||||
testImplementation("org.robolectric:shadows-play-services:$robolectricVersion")
|
testImplementation("org.robolectric:shadows-play-services:$robolectricVersion")
|
||||||
|
|
||||||
implementation(kotlin("reflect", version = BuildPluginsVersion.KOTLIN))
|
|
||||||
|
|
||||||
val coroutinesVersion = "1.4.3"
|
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion")
|
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion")
|
|
||||||
|
|
||||||
// For detecting memory leaks; see https://square.github.io/leakcanary/
|
// For detecting memory leaks; see https://square.github.io/leakcanary/
|
||||||
// debugImplementation("com.squareup.leakcanary:leakcanary-android:2.7")
|
// debugImplementation("com.squareup.leakcanary:leakcanary-android:2.7")
|
||||||
}
|
}
|
||||||
|
2
app/proguard-rules.pro
vendored
2
app/proguard-rules.pro
vendored
@ -4,6 +4,7 @@
|
|||||||
-keep,allowoptimization class eu.kanade.tachiyomi.** { public protected *; }
|
-keep,allowoptimization class eu.kanade.tachiyomi.** { public protected *; }
|
||||||
-keep,allowoptimization class androidx.preference.** { *; }
|
-keep,allowoptimization class androidx.preference.** { *; }
|
||||||
-keep,allowoptimization class kotlin.** { public protected *; }
|
-keep,allowoptimization class kotlin.** { public protected *; }
|
||||||
|
-keep,allowoptimization class kotlinx.coroutines.** { public protected *; }
|
||||||
-keep,allowoptimization class okhttp3.** { public protected *; }
|
-keep,allowoptimization class okhttp3.** { public protected *; }
|
||||||
-keep,allowoptimization class okio.** { public protected *; }
|
-keep,allowoptimization class okio.** { public protected *; }
|
||||||
-keep,allowoptimization class rx.** { public protected *; }
|
-keep,allowoptimization class rx.** { public protected *; }
|
||||||
@ -11,6 +12,7 @@
|
|||||||
-keep,allowoptimization class com.google.gson.** { public protected *; }
|
-keep,allowoptimization class com.google.gson.** { public protected *; }
|
||||||
-keep,allowoptimization class com.github.salomonbrys.kotson.** { public protected *; }
|
-keep,allowoptimization class com.github.salomonbrys.kotson.** { public protected *; }
|
||||||
-keep,allowoptimization class com.squareup.duktape.** { public protected *; }
|
-keep,allowoptimization class com.squareup.duktape.** { public protected *; }
|
||||||
|
-keep,allowoptimization class uy.kohesive.injekt.** { public protected *; }
|
||||||
|
|
||||||
##---------------Begin: proguard configuration for RxJava 1.x ----------
|
##---------------Begin: proguard configuration for RxJava 1.x ----------
|
||||||
-dontwarn sun.misc.**
|
-dontwarn sun.misc.**
|
||||||
|
@ -23,8 +23,7 @@
|
|||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".App"
|
android:name=".App"
|
||||||
android:allowBackup="true"
|
android:allowBackup="false"
|
||||||
android:fullBackupContent="@xml/backup_rules"
|
|
||||||
android:hardwareAccelerated="true"
|
android:hardwareAccelerated="true"
|
||||||
android:hasFragileUserData="true"
|
android:hasFragileUserData="true"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
@ -32,12 +31,15 @@
|
|||||||
android:largeHeap="true"
|
android:largeHeap="true"
|
||||||
android:requestLegacyExternalStorage="true"
|
android:requestLegacyExternalStorage="true"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:theme="@style/Theme.Base"
|
android:theme="@style/Theme.Tachiyomi"
|
||||||
|
android:supportsRtl="true"
|
||||||
android:networkSecurityConfig="@xml/network_security_config">
|
android:networkSecurityConfig="@xml/network_security_config">
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.main.MainActivity"
|
android:name=".ui.main.MainActivity"
|
||||||
android:launchMode="singleTop"
|
android:launchMode="singleTop"
|
||||||
android:theme="@style/Theme.Splash">
|
android:theme="@style/Theme.Tachiyomi.SplashScreen"
|
||||||
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
@ -51,7 +53,8 @@
|
|||||||
android:name=".ui.main.DeepLinkActivity"
|
android:name=".ui.main.DeepLinkActivity"
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask"
|
||||||
android:theme="@android:style/Theme.NoDisplay"
|
android:theme="@android:style/Theme.NoDisplay"
|
||||||
android:label="@string/action_global_search">
|
android:label="@string/action_global_search"
|
||||||
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.SEARCH" />
|
<action android:name="android.intent.action.SEARCH" />
|
||||||
<action android:name="com.google.android.gms.actions.SEARCH_ACTION" />
|
<action android:name="com.google.android.gms.actions.SEARCH_ACTION" />
|
||||||
@ -72,9 +75,11 @@
|
|||||||
android:name="android.app.searchable"
|
android:name="android.app.searchable"
|
||||||
android:resource="@xml/searchable" />
|
android:resource="@xml/searchable" />
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.reader.ReaderActivity"
|
android:name=".ui.reader.ReaderActivity"
|
||||||
android:launchMode="singleTask">
|
android:launchMode="singleTask"
|
||||||
|
android:exported="false">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="com.samsung.android.support.REMOTE_ACTION" />
|
<action android:name="com.samsung.android.support.REMOTE_ACTION" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
@ -82,15 +87,26 @@
|
|||||||
<meta-data android:name="com.samsung.android.support.REMOTE_ACTION"
|
<meta-data android:name="com.samsung.android.support.REMOTE_ACTION"
|
||||||
android:resource="@xml/s_pen_actions"/>
|
android:resource="@xml/s_pen_actions"/>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.security.UnlockActivity"
|
android:name=".ui.security.UnlockActivity"
|
||||||
android:theme="@style/Theme.Base" />
|
android:theme="@style/Theme.Tachiyomi"
|
||||||
|
android:exported="false" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.webview.WebViewActivity"
|
android:name=".ui.webview.WebViewActivity"
|
||||||
android:configChanges="uiMode|orientation|screenSize" />
|
android:configChanges="uiMode|orientation|screenSize"
|
||||||
|
android:exported="false" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".extension.util.ExtensionInstallActivity"
|
||||||
|
android:theme="@android:style/Theme.Translucent.NoTitleBar"
|
||||||
|
android:exported="false" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.setting.track.AnilistLoginActivity"
|
android:name=".ui.setting.track.AnilistLoginActivity"
|
||||||
android:label="Anilist">
|
android:label="Anilist"
|
||||||
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
@ -104,7 +120,8 @@
|
|||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.setting.track.MyAnimeListLoginActivity"
|
android:name=".ui.setting.track.MyAnimeListLoginActivity"
|
||||||
android:label="MyAnimeList">
|
android:label="MyAnimeList"
|
||||||
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
@ -118,7 +135,8 @@
|
|||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.setting.track.ShikimoriLoginActivity"
|
android:name=".ui.setting.track.ShikimoriLoginActivity"
|
||||||
android:label="Shikimori">
|
android:label="Shikimori"
|
||||||
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
@ -132,7 +150,8 @@
|
|||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.setting.track.BangumiLoginActivity"
|
android:name=".ui.setting.track.BangumiLoginActivity"
|
||||||
android:label="Bangumi">
|
android:label="Bangumi"
|
||||||
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
@ -145,20 +164,6 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<activity
|
|
||||||
android:name=".extension.util.ExtensionInstallActivity"
|
|
||||||
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
|
|
||||||
|
|
||||||
<provider
|
|
||||||
android:name="androidx.core.content.FileProvider"
|
|
||||||
android:authorities="${applicationId}.provider"
|
|
||||||
android:exported="false"
|
|
||||||
android:grantUriPermissions="true">
|
|
||||||
<meta-data
|
|
||||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
|
||||||
android:resource="@xml/provider_paths" />
|
|
||||||
</provider>
|
|
||||||
|
|
||||||
<receiver
|
<receiver
|
||||||
android:name=".data.notification.NotificationReceiver"
|
android:name=".data.notification.NotificationReceiver"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
@ -183,6 +188,16 @@
|
|||||||
android:name=".data.backup.BackupRestoreService"
|
android:name=".data.backup.BackupRestoreService"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
|
<provider
|
||||||
|
android:name="androidx.core.content.FileProvider"
|
||||||
|
android:authorities="${applicationId}.provider"
|
||||||
|
android:exported="false"
|
||||||
|
android:grantUriPermissions="true">
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
|
android:resource="@xml/provider_paths" />
|
||||||
|
</provider>
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
@ -7,9 +7,9 @@ import android.content.BroadcastReceiver
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.IntentFilter
|
import android.content.IntentFilter
|
||||||
import android.content.res.Configuration
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.webkit.WebView
|
import android.webkit.WebView
|
||||||
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
import androidx.core.app.NotificationManagerCompat
|
import androidx.core.app.NotificationManagerCompat
|
||||||
import androidx.core.content.getSystemService
|
import androidx.core.content.getSystemService
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
@ -17,18 +17,19 @@ import androidx.lifecycle.LifecycleObserver
|
|||||||
import androidx.lifecycle.OnLifecycleEvent
|
import androidx.lifecycle.OnLifecycleEvent
|
||||||
import androidx.lifecycle.ProcessLifecycleOwner
|
import androidx.lifecycle.ProcessLifecycleOwner
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.multidex.MultiDex
|
|
||||||
import coil.ImageLoader
|
import coil.ImageLoader
|
||||||
import coil.ImageLoaderFactory
|
import coil.ImageLoaderFactory
|
||||||
import coil.decode.GifDecoder
|
import coil.decode.GifDecoder
|
||||||
import coil.decode.ImageDecoderDecoder
|
import coil.decode.ImageDecoderDecoder
|
||||||
import eu.kanade.tachiyomi.data.coil.ByteBufferFetcher
|
import eu.kanade.tachiyomi.data.coil.ByteBufferFetcher
|
||||||
import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher
|
import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher
|
||||||
|
import eu.kanade.tachiyomi.data.coil.TachiyomiImageDecoder
|
||||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
|
import eu.kanade.tachiyomi.data.preference.PreferenceValues
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
|
import eu.kanade.tachiyomi.data.preference.asImmediateFlow
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate
|
import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate
|
||||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
|
||||||
import eu.kanade.tachiyomi.util.system.notification
|
import eu.kanade.tachiyomi.util.system.notification
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
@ -68,8 +69,6 @@ open class App : Application(), LifecycleObserver, ImageLoaderFactory {
|
|||||||
setupAcra()
|
setupAcra()
|
||||||
setupNotificationChannels()
|
setupNotificationChannels()
|
||||||
|
|
||||||
LocaleHelper.updateConfiguration(this, resources.configuration)
|
|
||||||
|
|
||||||
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
|
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
|
||||||
|
|
||||||
// Show notification to disable Incognito Mode when it's enabled
|
// Show notification to disable Incognito Mode when it's enabled
|
||||||
@ -81,7 +80,7 @@ open class App : Application(), LifecycleObserver, ImageLoaderFactory {
|
|||||||
val notification = notification(Notifications.CHANNEL_INCOGNITO_MODE) {
|
val notification = notification(Notifications.CHANNEL_INCOGNITO_MODE) {
|
||||||
setContentTitle(getString(R.string.pref_incognito_mode))
|
setContentTitle(getString(R.string.pref_incognito_mode))
|
||||||
setContentText(getString(R.string.notification_incognito_text))
|
setContentText(getString(R.string.notification_incognito_text))
|
||||||
setSmallIcon(R.drawable.ic_glasses_black_24dp)
|
setSmallIcon(R.drawable.ic_glasses_24dp)
|
||||||
setOngoing(true)
|
setOngoing(true)
|
||||||
|
|
||||||
val pendingIntent = PendingIntent.getBroadcast(
|
val pendingIntent = PendingIntent.getBroadcast(
|
||||||
@ -99,21 +98,23 @@ open class App : Application(), LifecycleObserver, ImageLoaderFactory {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.launchIn(ProcessLifecycleOwner.get().lifecycleScope)
|
.launchIn(ProcessLifecycleOwner.get().lifecycleScope)
|
||||||
}
|
|
||||||
|
|
||||||
override fun attachBaseContext(base: Context) {
|
preferences.themeMode()
|
||||||
super.attachBaseContext(base)
|
.asImmediateFlow {
|
||||||
MultiDex.install(this)
|
AppCompatDelegate.setDefaultNightMode(
|
||||||
}
|
when (it) {
|
||||||
|
PreferenceValues.ThemeMode.light -> AppCompatDelegate.MODE_NIGHT_NO
|
||||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
PreferenceValues.ThemeMode.dark -> AppCompatDelegate.MODE_NIGHT_YES
|
||||||
super.onConfigurationChanged(newConfig)
|
PreferenceValues.ThemeMode.system -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
|
||||||
LocaleHelper.updateConfiguration(this, newConfig, true)
|
}
|
||||||
|
)
|
||||||
|
}.launchIn(ProcessLifecycleOwner.get().lifecycleScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun newImageLoader(): ImageLoader {
|
override fun newImageLoader(): ImageLoader {
|
||||||
return ImageLoader.Builder(this).apply {
|
return ImageLoader.Builder(this).apply {
|
||||||
componentRegistry {
|
componentRegistry {
|
||||||
|
add(TachiyomiImageDecoder(this@App.resources))
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
add(ImageDecoderDecoder(this@App))
|
add(ImageDecoderDecoder(this@App))
|
||||||
} else {
|
} else {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
package eu.kanade.tachiyomi
|
package eu.kanade.tachiyomi
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.os.Handler
|
import androidx.core.content.ContextCompat
|
||||||
import eu.kanade.tachiyomi.data.cache.ChapterCache
|
import eu.kanade.tachiyomi.data.cache.ChapterCache
|
||||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
@ -44,7 +44,7 @@ class AppModule(val app: Application) : InjektModule {
|
|||||||
addSingletonFactory { Json { ignoreUnknownKeys = true } }
|
addSingletonFactory { Json { ignoreUnknownKeys = true } }
|
||||||
|
|
||||||
// Asynchronously init expensive components for a faster cold start
|
// Asynchronously init expensive components for a faster cold start
|
||||||
Handler().post {
|
ContextCompat.getMainExecutor(app).execute {
|
||||||
get<PreferencesHelper>()
|
get<PreferencesHelper>()
|
||||||
|
|
||||||
get<NetworkHelper>()
|
get<NetworkHelper>()
|
||||||
|
@ -12,6 +12,8 @@ import eu.kanade.tachiyomi.data.updater.UpdaterJob
|
|||||||
import eu.kanade.tachiyomi.extension.ExtensionUpdateJob
|
import eu.kanade.tachiyomi.extension.ExtensionUpdateJob
|
||||||
import eu.kanade.tachiyomi.network.PREF_DOH_CLOUDFLARE
|
import eu.kanade.tachiyomi.network.PREF_DOH_CLOUDFLARE
|
||||||
import eu.kanade.tachiyomi.ui.library.LibrarySort
|
import eu.kanade.tachiyomi.ui.library.LibrarySort
|
||||||
|
import eu.kanade.tachiyomi.ui.library.setting.SortDirectionSetting
|
||||||
|
import eu.kanade.tachiyomi.ui.library.setting.SortModeSetting
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
|
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
|
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
|
||||||
@ -96,9 +98,15 @@ object Migrations {
|
|||||||
}
|
}
|
||||||
if (oldVersion < 44) {
|
if (oldVersion < 44) {
|
||||||
// Reset sorting preference if using removed sort by source
|
// Reset sorting preference if using removed sort by source
|
||||||
|
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
|
||||||
|
val oldSortingMode = prefs.getInt(PreferenceKeys.librarySortingMode, 0)
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
if (preferences.librarySortingMode().get() == LibrarySort.SOURCE) {
|
if (oldSortingMode == LibrarySort.SOURCE) {
|
||||||
preferences.librarySortingMode().set(LibrarySort.ALPHA)
|
prefs.edit {
|
||||||
|
putInt(PreferenceKeys.librarySortingMode, LibrarySort.ALPHA)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (oldVersion < 52) {
|
if (oldVersion < 52) {
|
||||||
@ -190,6 +198,45 @@ object Migrations {
|
|||||||
LibraryUpdateJob.setupTask(context, 3)
|
LibraryUpdateJob.setupTask(context, 3)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (oldVersion < 64) {
|
||||||
|
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
|
||||||
|
val oldSortingMode = prefs.getInt(PreferenceKeys.librarySortingMode, 0)
|
||||||
|
val oldSortingDirection = prefs.getBoolean(PreferenceKeys.librarySortingDirection, true)
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
val newSortingMode = when (oldSortingMode) {
|
||||||
|
LibrarySort.ALPHA -> SortModeSetting.ALPHABETICAL
|
||||||
|
LibrarySort.LAST_READ -> SortModeSetting.LAST_READ
|
||||||
|
LibrarySort.LAST_CHECKED -> SortModeSetting.LAST_CHECKED
|
||||||
|
LibrarySort.UNREAD -> SortModeSetting.UNREAD
|
||||||
|
LibrarySort.TOTAL -> SortModeSetting.TOTAL_CHAPTERS
|
||||||
|
LibrarySort.LATEST_CHAPTER -> SortModeSetting.LATEST_CHAPTER
|
||||||
|
LibrarySort.CHAPTER_FETCH_DATE -> SortModeSetting.DATE_FETCHED
|
||||||
|
LibrarySort.DATE_ADDED -> SortModeSetting.DATE_ADDED
|
||||||
|
else -> SortModeSetting.ALPHABETICAL
|
||||||
|
}
|
||||||
|
|
||||||
|
val newSortingDirection = when (oldSortingDirection) {
|
||||||
|
true -> SortDirectionSetting.ASCENDING
|
||||||
|
else -> SortDirectionSetting.DESCENDING
|
||||||
|
}
|
||||||
|
|
||||||
|
prefs.edit(commit = true) {
|
||||||
|
remove(PreferenceKeys.librarySortingMode)
|
||||||
|
remove(PreferenceKeys.librarySortingDirection)
|
||||||
|
}
|
||||||
|
|
||||||
|
prefs.edit {
|
||||||
|
putString(PreferenceKeys.librarySortingMode, newSortingMode.name)
|
||||||
|
putString(PreferenceKeys.librarySortingDirection, newSortingDirection.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (oldVersion < 65) {
|
||||||
|
if (preferences.lang().get() in listOf("en-US", "en-GB")) {
|
||||||
|
preferences.lang().set("en")
|
||||||
|
}
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,7 +96,7 @@ class BackupRestoreService : Service() {
|
|||||||
|
|
||||||
private fun destroyJob() {
|
private fun destroyJob() {
|
||||||
backupRestore?.job?.cancel()
|
backupRestore?.job?.cancel()
|
||||||
ioScope?.cancel()
|
ioScope.cancel()
|
||||||
if (wakeLock.isHeld) {
|
if (wakeLock.isHeld) {
|
||||||
wakeLock.release()
|
wakeLock.release()
|
||||||
}
|
}
|
||||||
|
@ -2,44 +2,52 @@ package eu.kanade.tachiyomi.data.backup.legacy
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import com.github.salomonbrys.kotson.fromJson
|
|
||||||
import com.github.salomonbrys.kotson.registerTypeAdapter
|
|
||||||
import com.github.salomonbrys.kotson.registerTypeHierarchyAdapter
|
|
||||||
import com.google.gson.Gson
|
|
||||||
import com.google.gson.GsonBuilder
|
|
||||||
import com.google.gson.JsonArray
|
|
||||||
import eu.kanade.tachiyomi.data.backup.AbstractBackupManager
|
import eu.kanade.tachiyomi.data.backup.AbstractBackupManager
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.CURRENT_VERSION
|
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.Companion.CURRENT_VERSION
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.models.DHistory
|
import eu.kanade.tachiyomi.data.backup.legacy.models.DHistory
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.serializer.CategoryTypeAdapter
|
import eu.kanade.tachiyomi.data.backup.legacy.serializer.CategoryImplTypeSerializer
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.serializer.ChapterTypeAdapter
|
import eu.kanade.tachiyomi.data.backup.legacy.serializer.CategoryTypeSerializer
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.serializer.HistoryTypeAdapter
|
import eu.kanade.tachiyomi.data.backup.legacy.serializer.ChapterImplTypeSerializer
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.serializer.MangaTypeAdapter
|
import eu.kanade.tachiyomi.data.backup.legacy.serializer.ChapterTypeSerializer
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.serializer.TrackTypeAdapter
|
import eu.kanade.tachiyomi.data.backup.legacy.serializer.HistoryTypeSerializer
|
||||||
import eu.kanade.tachiyomi.data.database.models.CategoryImpl
|
import eu.kanade.tachiyomi.data.backup.legacy.serializer.MangaImplTypeSerializer
|
||||||
|
import eu.kanade.tachiyomi.data.backup.legacy.serializer.MangaTypeSerializer
|
||||||
|
import eu.kanade.tachiyomi.data.backup.legacy.serializer.TrackImplTypeSerializer
|
||||||
|
import eu.kanade.tachiyomi.data.backup.legacy.serializer.TrackTypeSerializer
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Category
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
import eu.kanade.tachiyomi.data.database.models.ChapterImpl
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.History
|
import eu.kanade.tachiyomi.data.database.models.History
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaCategory
|
import eu.kanade.tachiyomi.data.database.models.MangaCategory
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaImpl
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
import eu.kanade.tachiyomi.data.database.models.TrackImpl
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.toMangaInfo
|
import eu.kanade.tachiyomi.data.database.models.toMangaInfo
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.source.model.toSManga
|
import eu.kanade.tachiyomi.source.model.toSManga
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlinx.serialization.modules.SerializersModule
|
||||||
|
import kotlinx.serialization.modules.contextual
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
|
|
||||||
class LegacyBackupManager(context: Context, version: Int = CURRENT_VERSION) : AbstractBackupManager(context) {
|
class LegacyBackupManager(context: Context, version: Int = CURRENT_VERSION) : AbstractBackupManager(context) {
|
||||||
|
|
||||||
val parser: Gson = when (version) {
|
val parser: Json = when (version) {
|
||||||
2 -> GsonBuilder()
|
2 -> Json {
|
||||||
.registerTypeAdapter<MangaImpl>(MangaTypeAdapter.build())
|
// Forks may have added items to backup
|
||||||
.registerTypeHierarchyAdapter<ChapterImpl>(ChapterTypeAdapter.build())
|
ignoreUnknownKeys = true
|
||||||
.registerTypeAdapter<CategoryImpl>(CategoryTypeAdapter.build())
|
|
||||||
.registerTypeAdapter<DHistory>(HistoryTypeAdapter.build())
|
// Register custom serializers
|
||||||
.registerTypeHierarchyAdapter<TrackImpl>(TrackTypeAdapter.build())
|
serializersModule = SerializersModule {
|
||||||
.create()
|
contextual(MangaTypeSerializer)
|
||||||
|
contextual(MangaImplTypeSerializer)
|
||||||
|
contextual(ChapterTypeSerializer)
|
||||||
|
contextual(ChapterImplTypeSerializer)
|
||||||
|
contextual(CategoryTypeSerializer)
|
||||||
|
contextual(CategoryImplTypeSerializer)
|
||||||
|
contextual(TrackTypeSerializer)
|
||||||
|
contextual(TrackImplTypeSerializer)
|
||||||
|
contextual(HistoryTypeSerializer)
|
||||||
|
}
|
||||||
|
}
|
||||||
else -> throw Exception("Unknown backup version")
|
else -> throw Exception("Unknown backup version")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -79,12 +87,11 @@ class LegacyBackupManager(context: Context, version: Int = CURRENT_VERSION) : Ab
|
|||||||
/**
|
/**
|
||||||
* Restore the categories from Json
|
* Restore the categories from Json
|
||||||
*
|
*
|
||||||
* @param jsonCategories array containing categories
|
* @param backupCategories array containing categories
|
||||||
*/
|
*/
|
||||||
internal fun restoreCategories(jsonCategories: JsonArray) {
|
internal fun restoreCategories(backupCategories: List<Category>) {
|
||||||
// Get categories from file and from db
|
// Get categories from file and from db
|
||||||
val dbCategories = databaseHelper.getCategories().executeAsBlocking()
|
val dbCategories = databaseHelper.getCategories().executeAsBlocking()
|
||||||
val backupCategories = parser.fromJson<List<CategoryImpl>>(jsonCategories)
|
|
||||||
|
|
||||||
// Iterate over them
|
// Iterate over them
|
||||||
backupCategories.forEach { category ->
|
backupCategories.forEach { category ->
|
||||||
|
@ -2,88 +2,80 @@ package eu.kanade.tachiyomi.data.backup.legacy
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import com.github.salomonbrys.kotson.fromJson
|
|
||||||
import com.google.gson.JsonArray
|
|
||||||
import com.google.gson.JsonElement
|
|
||||||
import com.google.gson.JsonObject
|
|
||||||
import com.google.gson.JsonParser
|
|
||||||
import com.google.gson.stream.JsonReader
|
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.backup.AbstractBackupRestore
|
import eu.kanade.tachiyomi.data.backup.AbstractBackupRestore
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupNotifier
|
import eu.kanade.tachiyomi.data.backup.BackupNotifier
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup
|
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.MANGAS
|
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.models.DHistory
|
import eu.kanade.tachiyomi.data.backup.legacy.models.DHistory
|
||||||
|
import eu.kanade.tachiyomi.data.backup.legacy.models.MangaObject
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Category
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
import eu.kanade.tachiyomi.data.database.models.ChapterImpl
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaImpl
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
import eu.kanade.tachiyomi.data.database.models.TrackImpl
|
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
|
import kotlinx.serialization.decodeFromString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlinx.serialization.json.JsonObject
|
||||||
|
import kotlinx.serialization.json.decodeFromJsonElement
|
||||||
|
import kotlinx.serialization.json.intOrNull
|
||||||
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
|
import okio.buffer
|
||||||
|
import okio.source
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
|
||||||
class LegacyBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBackupRestore<LegacyBackupManager>(context, notifier) {
|
class LegacyBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBackupRestore<LegacyBackupManager>(context, notifier) {
|
||||||
|
|
||||||
override suspend fun performRestore(uri: Uri): Boolean {
|
override suspend fun performRestore(uri: Uri): Boolean {
|
||||||
val reader = JsonReader(context.contentResolver.openInputStream(uri)!!.bufferedReader())
|
// Read the json and create a Json Object,
|
||||||
val json = JsonParser.parseReader(reader).asJsonObject
|
// cannot use the backupManager json deserializer one because its not initialized yet
|
||||||
|
val backupObject = Json.decodeFromString<JsonObject>(
|
||||||
|
context.contentResolver.openInputStream(uri)!!.source().buffer().use { it.readUtf8() }
|
||||||
|
)
|
||||||
|
|
||||||
val version = json.get(Backup.VERSION)?.asInt ?: 1
|
// Get parser version
|
||||||
|
val version = backupObject["version"]?.jsonPrimitive?.intOrNull ?: 1
|
||||||
|
|
||||||
|
// Initialize manager
|
||||||
backupManager = LegacyBackupManager(context, version)
|
backupManager = LegacyBackupManager(context, version)
|
||||||
|
|
||||||
val mangasJson = json.get(MANGAS).asJsonArray
|
// Decode the json object to a Backup object
|
||||||
restoreAmount = mangasJson.size() + 1 // +1 for categories
|
val backup = backupManager.parser.decodeFromJsonElement<Backup>(backupObject)
|
||||||
|
|
||||||
|
restoreAmount = backup.mangas.size + 1 // +1 for categories
|
||||||
|
|
||||||
// Restore categories
|
// Restore categories
|
||||||
json.get(Backup.CATEGORIES)?.let { restoreCategories(it) }
|
backup.categories?.let { restoreCategories(it) }
|
||||||
|
|
||||||
// Store source mapping for error messages
|
// Store source mapping for error messages
|
||||||
sourceMapping = LegacyBackupRestoreValidator.getSourceMapping(json)
|
sourceMapping = LegacyBackupRestoreValidator.getSourceMapping(backup.extensions ?: emptyList())
|
||||||
|
|
||||||
// Restore individual manga
|
// Restore individual manga
|
||||||
mangasJson.forEach {
|
backup.mangas.forEach {
|
||||||
if (job?.isActive != true) {
|
if (job?.isActive != true) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
restoreManga(it.asJsonObject)
|
restoreManga(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun restoreCategories(categoriesJson: JsonElement) {
|
private fun restoreCategories(categoriesJson: List<Category>) {
|
||||||
db.inTransaction {
|
db.inTransaction {
|
||||||
backupManager.restoreCategories(categoriesJson.asJsonArray)
|
backupManager.restoreCategories(categoriesJson)
|
||||||
}
|
}
|
||||||
|
|
||||||
restoreProgress += 1
|
restoreProgress += 1
|
||||||
showRestoreProgress(restoreProgress, restoreAmount, context.getString(R.string.categories))
|
showRestoreProgress(restoreProgress, restoreAmount, context.getString(R.string.categories))
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun restoreManga(mangaJson: JsonObject) {
|
private suspend fun restoreManga(mangaJson: MangaObject) {
|
||||||
val manga = backupManager.parser.fromJson<MangaImpl>(
|
val manga = mangaJson.manga
|
||||||
mangaJson.get(
|
val chapters = mangaJson.chapters ?: emptyList()
|
||||||
Backup.MANGA
|
val categories = mangaJson.categories ?: emptyList()
|
||||||
)
|
val history = mangaJson.history ?: emptyList()
|
||||||
)
|
val tracks = mangaJson.track ?: emptyList()
|
||||||
val chapters = backupManager.parser.fromJson<List<ChapterImpl>>(
|
|
||||||
mangaJson.get(Backup.CHAPTERS)
|
|
||||||
?: JsonArray()
|
|
||||||
)
|
|
||||||
val categories = backupManager.parser.fromJson<List<String>>(
|
|
||||||
mangaJson.get(Backup.CATEGORIES)
|
|
||||||
?: JsonArray()
|
|
||||||
)
|
|
||||||
val history = backupManager.parser.fromJson<List<DHistory>>(
|
|
||||||
mangaJson.get(Backup.HISTORY)
|
|
||||||
?: JsonArray()
|
|
||||||
)
|
|
||||||
val tracks = backupManager.parser.fromJson<List<TrackImpl>>(
|
|
||||||
mangaJson.get(Backup.TRACK)
|
|
||||||
?: JsonArray()
|
|
||||||
)
|
|
||||||
|
|
||||||
val source = backupManager.sourceManager.get(manga.source)
|
val source = backupManager.sourceManager.get(manga.source)
|
||||||
val sourceName = sourceMapping[manga.source] ?: manga.source.toString()
|
val sourceName = sourceMapping[manga.source] ?: manga.source.toString()
|
||||||
|
@ -2,12 +2,12 @@ package eu.kanade.tachiyomi.data.backup.legacy
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import com.google.gson.JsonObject
|
|
||||||
import com.google.gson.JsonParser
|
|
||||||
import com.google.gson.stream.JsonReader
|
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.backup.AbstractBackupRestoreValidator
|
import eu.kanade.tachiyomi.data.backup.AbstractBackupRestoreValidator
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup
|
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup
|
||||||
|
import kotlinx.serialization.decodeFromString
|
||||||
|
import okio.buffer
|
||||||
|
import okio.source
|
||||||
|
|
||||||
class LegacyBackupRestoreValidator : AbstractBackupRestoreValidator() {
|
class LegacyBackupRestoreValidator : AbstractBackupRestoreValidator() {
|
||||||
/**
|
/**
|
||||||
@ -17,30 +17,30 @@ class LegacyBackupRestoreValidator : AbstractBackupRestoreValidator() {
|
|||||||
* @return List of missing sources or missing trackers.
|
* @return List of missing sources or missing trackers.
|
||||||
*/
|
*/
|
||||||
override fun validate(context: Context, uri: Uri): Results {
|
override fun validate(context: Context, uri: Uri): Results {
|
||||||
val reader = JsonReader(context.contentResolver.openInputStream(uri)!!.bufferedReader())
|
val backupManager = LegacyBackupManager(context)
|
||||||
val json = JsonParser.parseReader(reader).asJsonObject
|
|
||||||
|
|
||||||
val version = json.get(Backup.VERSION)
|
val backup = backupManager.parser.decodeFromString<Backup>(
|
||||||
val mangasJson = json.get(Backup.MANGAS)
|
context.contentResolver.openInputStream(uri)!!.source().buffer().use { it.readUtf8() }
|
||||||
if (version == null || mangasJson == null) {
|
)
|
||||||
|
|
||||||
|
if (backup.version == null) {
|
||||||
throw Exception(context.getString(R.string.invalid_backup_file_missing_data))
|
throw Exception(context.getString(R.string.invalid_backup_file_missing_data))
|
||||||
}
|
}
|
||||||
|
|
||||||
val mangas = mangasJson.asJsonArray
|
if (backup.mangas.isEmpty()) {
|
||||||
if (mangas.size() == 0) {
|
|
||||||
throw Exception(context.getString(R.string.invalid_backup_file_missing_manga))
|
throw Exception(context.getString(R.string.invalid_backup_file_missing_manga))
|
||||||
}
|
}
|
||||||
|
|
||||||
val sources = getSourceMapping(json)
|
val sources = getSourceMapping(backup.extensions ?: emptyList())
|
||||||
val missingSources = sources
|
val missingSources = sources
|
||||||
.filter { sourceManager.get(it.key) == null }
|
.filter { sourceManager.get(it.key) == null }
|
||||||
.values
|
.values
|
||||||
.sorted()
|
.sorted()
|
||||||
|
|
||||||
val trackers = mangas
|
val trackers = backup.mangas
|
||||||
.filter { it.asJsonObject.has("track") }
|
.filterNot { it.track.isNullOrEmpty() }
|
||||||
.flatMap { it.asJsonObject["track"].asJsonArray }
|
.flatMap { it.track ?: emptyList() }
|
||||||
.map { it.asJsonObject["s"].asInt }
|
.map { it.sync_id }
|
||||||
.distinct()
|
.distinct()
|
||||||
val missingTrackers = trackers
|
val missingTrackers = trackers
|
||||||
.mapNotNull { trackManager.getService(it) }
|
.mapNotNull { trackManager.getService(it) }
|
||||||
@ -52,12 +52,10 @@ class LegacyBackupRestoreValidator : AbstractBackupRestoreValidator() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun getSourceMapping(json: JsonObject): Map<Long, String> {
|
fun getSourceMapping(extensionsMapping: List<String>): Map<Long, String> {
|
||||||
val extensionsMapping = json.get(Backup.EXTENSIONS) ?: return emptyMap()
|
return extensionsMapping
|
||||||
|
|
||||||
return extensionsMapping.asJsonArray
|
|
||||||
.map {
|
.map {
|
||||||
val items = it.asString.split(":")
|
val items = it.split(":")
|
||||||
items[0].toLong() to items[1]
|
items[0].toLong() to items[1]
|
||||||
}
|
}
|
||||||
.toMap()
|
.toMap()
|
||||||
|
@ -1,25 +1,37 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.legacy.models
|
package eu.kanade.tachiyomi.data.backup.legacy.models
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Category
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
|
import kotlinx.serialization.Contextual
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
/**
|
@Serializable
|
||||||
* Json values
|
data class Backup(
|
||||||
*/
|
val version: Int? = null,
|
||||||
object Backup {
|
var mangas: MutableList<MangaObject> = mutableListOf(),
|
||||||
const val CURRENT_VERSION = 2
|
var categories: List<@Contextual Category>? = null,
|
||||||
const val MANGA = "manga"
|
var extensions: List<String>? = null
|
||||||
const val MANGAS = "mangas"
|
) {
|
||||||
const val TRACK = "track"
|
companion object {
|
||||||
const val CHAPTERS = "chapters"
|
const val CURRENT_VERSION = 2
|
||||||
const val CATEGORIES = "categories"
|
|
||||||
const val EXTENSIONS = "extensions"
|
|
||||||
const val HISTORY = "history"
|
|
||||||
const val VERSION = "version"
|
|
||||||
|
|
||||||
fun getDefaultFilename(): String {
|
fun getDefaultFilename(): String {
|
||||||
val date = SimpleDateFormat("yyyy-MM-dd_HH-mm", Locale.getDefault()).format(Date())
|
val date = SimpleDateFormat("yyyy-MM-dd_HH-mm", Locale.getDefault()).format(Date())
|
||||||
return "tachiyomi_$date.json"
|
return "tachiyomi_$date.json"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class MangaObject(
|
||||||
|
var manga: @Contextual Manga,
|
||||||
|
var chapters: List<@Contextual Chapter>? = null,
|
||||||
|
var categories: List<String>? = null,
|
||||||
|
var track: List<@Contextual Track>? = null,
|
||||||
|
var history: List<@Contextual DHistory>? = null
|
||||||
|
)
|
||||||
|
@ -1,31 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.legacy.serializer
|
|
||||||
|
|
||||||
import com.github.salomonbrys.kotson.typeAdapter
|
|
||||||
import com.google.gson.TypeAdapter
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.CategoryImpl
|
|
||||||
|
|
||||||
/**
|
|
||||||
* JSON Serializer used to write / read [CategoryImpl] to / from json
|
|
||||||
*/
|
|
||||||
object CategoryTypeAdapter {
|
|
||||||
|
|
||||||
fun build(): TypeAdapter<CategoryImpl> {
|
|
||||||
return typeAdapter {
|
|
||||||
write {
|
|
||||||
beginArray()
|
|
||||||
value(it.name)
|
|
||||||
value(it.order)
|
|
||||||
endArray()
|
|
||||||
}
|
|
||||||
|
|
||||||
read {
|
|
||||||
beginArray()
|
|
||||||
val category = CategoryImpl()
|
|
||||||
category.name = nextString()
|
|
||||||
category.order = nextInt()
|
|
||||||
endArray()
|
|
||||||
category
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,49 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.legacy.serializer
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Category
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.CategoryImpl
|
||||||
|
import kotlinx.serialization.KSerializer
|
||||||
|
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||||
|
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
|
||||||
|
import kotlinx.serialization.encoding.Decoder
|
||||||
|
import kotlinx.serialization.encoding.Encoder
|
||||||
|
import kotlinx.serialization.json.JsonDecoder
|
||||||
|
import kotlinx.serialization.json.JsonEncoder
|
||||||
|
import kotlinx.serialization.json.add
|
||||||
|
import kotlinx.serialization.json.buildJsonArray
|
||||||
|
import kotlinx.serialization.json.int
|
||||||
|
import kotlinx.serialization.json.jsonArray
|
||||||
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON Serializer used to write / read [CategoryImpl] to / from json
|
||||||
|
*/
|
||||||
|
open class CategoryBaseSerializer<T : Category> : KSerializer<T> {
|
||||||
|
override val descriptor: SerialDescriptor = buildClassSerialDescriptor("Category")
|
||||||
|
|
||||||
|
override fun serialize(encoder: Encoder, value: T) {
|
||||||
|
encoder as JsonEncoder
|
||||||
|
encoder.encodeJsonElement(
|
||||||
|
buildJsonArray {
|
||||||
|
add(value.name)
|
||||||
|
add(value.order)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
override fun deserialize(decoder: Decoder): T {
|
||||||
|
// make a category impl and cast as T so that the serializer accepts it
|
||||||
|
return CategoryImpl().apply {
|
||||||
|
decoder as JsonDecoder
|
||||||
|
val array = decoder.decodeJsonElement().jsonArray
|
||||||
|
name = array[0].jsonPrimitive.content
|
||||||
|
order = array[1].jsonPrimitive.int
|
||||||
|
} as T
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow for serialization of a category and category impl
|
||||||
|
object CategoryTypeSerializer : CategoryBaseSerializer<Category>()
|
||||||
|
|
||||||
|
object CategoryImplTypeSerializer : CategoryBaseSerializer<CategoryImpl>()
|
@ -1,59 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.legacy.serializer
|
|
||||||
|
|
||||||
import com.github.salomonbrys.kotson.typeAdapter
|
|
||||||
import com.google.gson.TypeAdapter
|
|
||||||
import com.google.gson.stream.JsonToken
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.ChapterImpl
|
|
||||||
|
|
||||||
/**
|
|
||||||
* JSON Serializer used to write / read [ChapterImpl] to / from json
|
|
||||||
*/
|
|
||||||
object ChapterTypeAdapter {
|
|
||||||
|
|
||||||
private const val URL = "u"
|
|
||||||
private const val READ = "r"
|
|
||||||
private const val BOOKMARK = "b"
|
|
||||||
private const val LAST_READ = "l"
|
|
||||||
|
|
||||||
fun build(): TypeAdapter<ChapterImpl> {
|
|
||||||
return typeAdapter {
|
|
||||||
write {
|
|
||||||
if (it.read || it.bookmark || it.last_page_read != 0) {
|
|
||||||
beginObject()
|
|
||||||
name(URL)
|
|
||||||
value(it.url)
|
|
||||||
if (it.read) {
|
|
||||||
name(READ)
|
|
||||||
value(1)
|
|
||||||
}
|
|
||||||
if (it.bookmark) {
|
|
||||||
name(BOOKMARK)
|
|
||||||
value(1)
|
|
||||||
}
|
|
||||||
if (it.last_page_read != 0) {
|
|
||||||
name(LAST_READ)
|
|
||||||
value(it.last_page_read)
|
|
||||||
}
|
|
||||||
endObject()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
read {
|
|
||||||
val chapter = ChapterImpl()
|
|
||||||
beginObject()
|
|
||||||
while (hasNext()) {
|
|
||||||
if (peek() == JsonToken.NAME) {
|
|
||||||
when (nextName()) {
|
|
||||||
URL -> chapter.url = nextString()
|
|
||||||
READ -> chapter.read = nextInt() == 1
|
|
||||||
BOOKMARK -> chapter.bookmark = nextInt() == 1
|
|
||||||
LAST_READ -> chapter.last_page_read = nextInt()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
endObject()
|
|
||||||
chapter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,66 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.legacy.serializer
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.ChapterImpl
|
||||||
|
import kotlinx.serialization.KSerializer
|
||||||
|
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
|
||||||
|
import kotlinx.serialization.encoding.Decoder
|
||||||
|
import kotlinx.serialization.encoding.Encoder
|
||||||
|
import kotlinx.serialization.json.JsonDecoder
|
||||||
|
import kotlinx.serialization.json.JsonEncoder
|
||||||
|
import kotlinx.serialization.json.buildJsonObject
|
||||||
|
import kotlinx.serialization.json.intOrNull
|
||||||
|
import kotlinx.serialization.json.jsonObject
|
||||||
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
|
import kotlinx.serialization.json.put
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON Serializer used to write / read [ChapterImpl] to / from json
|
||||||
|
*/
|
||||||
|
open class ChapterBaseSerializer<T : Chapter> : KSerializer<T> {
|
||||||
|
|
||||||
|
override val descriptor = buildClassSerialDescriptor("Chapter")
|
||||||
|
|
||||||
|
override fun serialize(encoder: Encoder, value: T) {
|
||||||
|
encoder as JsonEncoder
|
||||||
|
encoder.encodeJsonElement(
|
||||||
|
buildJsonObject {
|
||||||
|
put(URL, value.url)
|
||||||
|
if (value.read) {
|
||||||
|
put(READ, 1)
|
||||||
|
}
|
||||||
|
if (value.bookmark) {
|
||||||
|
put(BOOKMARK, 1)
|
||||||
|
}
|
||||||
|
if (value.last_page_read != 0) {
|
||||||
|
put(LAST_READ, value.last_page_read)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
override fun deserialize(decoder: Decoder): T {
|
||||||
|
// make a chapter impl and cast as T so that the serializer accepts it
|
||||||
|
return ChapterImpl().apply {
|
||||||
|
decoder as JsonDecoder
|
||||||
|
val jsonObject = decoder.decodeJsonElement().jsonObject
|
||||||
|
url = jsonObject[URL]!!.jsonPrimitive.content
|
||||||
|
read = jsonObject[READ]?.jsonPrimitive?.intOrNull == 1
|
||||||
|
bookmark = jsonObject[BOOKMARK]?.jsonPrimitive?.intOrNull == 1
|
||||||
|
last_page_read = jsonObject[LAST_READ]?.jsonPrimitive?.intOrNull ?: last_page_read
|
||||||
|
} as T
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val URL = "u"
|
||||||
|
private const val READ = "r"
|
||||||
|
private const val BOOKMARK = "b"
|
||||||
|
private const val LAST_READ = "l"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow for serialization of a chapter and chapter impl
|
||||||
|
object ChapterTypeSerializer : ChapterBaseSerializer<Chapter>()
|
||||||
|
|
||||||
|
object ChapterImplTypeSerializer : ChapterBaseSerializer<ChapterImpl>()
|
@ -1,32 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.legacy.serializer
|
|
||||||
|
|
||||||
import com.github.salomonbrys.kotson.typeAdapter
|
|
||||||
import com.google.gson.TypeAdapter
|
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.models.DHistory
|
|
||||||
|
|
||||||
/**
|
|
||||||
* JSON Serializer used to write / read [DHistory] to / from json
|
|
||||||
*/
|
|
||||||
object HistoryTypeAdapter {
|
|
||||||
|
|
||||||
fun build(): TypeAdapter<DHistory> {
|
|
||||||
return typeAdapter {
|
|
||||||
write {
|
|
||||||
if (it.lastRead != 0L) {
|
|
||||||
beginArray()
|
|
||||||
value(it.url)
|
|
||||||
value(it.lastRead)
|
|
||||||
endArray()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
read {
|
|
||||||
beginArray()
|
|
||||||
val url = nextString()
|
|
||||||
val lastRead = nextLong()
|
|
||||||
endArray()
|
|
||||||
DHistory(url, lastRead)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,41 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.legacy.serializer
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.backup.legacy.models.DHistory
|
||||||
|
import kotlinx.serialization.KSerializer
|
||||||
|
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||||
|
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
|
||||||
|
import kotlinx.serialization.encoding.Decoder
|
||||||
|
import kotlinx.serialization.encoding.Encoder
|
||||||
|
import kotlinx.serialization.json.JsonDecoder
|
||||||
|
import kotlinx.serialization.json.JsonEncoder
|
||||||
|
import kotlinx.serialization.json.add
|
||||||
|
import kotlinx.serialization.json.buildJsonArray
|
||||||
|
import kotlinx.serialization.json.jsonArray
|
||||||
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
|
import kotlinx.serialization.json.long
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON Serializer used to write / read [DHistory] to / from json
|
||||||
|
*/
|
||||||
|
object HistoryTypeSerializer : KSerializer<DHistory> {
|
||||||
|
override val descriptor: SerialDescriptor = buildClassSerialDescriptor("History")
|
||||||
|
|
||||||
|
override fun serialize(encoder: Encoder, value: DHistory) {
|
||||||
|
encoder as JsonEncoder
|
||||||
|
encoder.encodeJsonElement(
|
||||||
|
buildJsonArray {
|
||||||
|
add(value.url)
|
||||||
|
add(value.lastRead)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun deserialize(decoder: Decoder): DHistory {
|
||||||
|
decoder as JsonDecoder
|
||||||
|
val array = decoder.decodeJsonElement().jsonArray
|
||||||
|
return DHistory(
|
||||||
|
url = array[0].jsonPrimitive.content,
|
||||||
|
lastRead = array[1].jsonPrimitive.long
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -1,37 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.legacy.serializer
|
|
||||||
|
|
||||||
import com.github.salomonbrys.kotson.typeAdapter
|
|
||||||
import com.google.gson.TypeAdapter
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaImpl
|
|
||||||
|
|
||||||
/**
|
|
||||||
* JSON Serializer used to write / read [MangaImpl] to / from json
|
|
||||||
*/
|
|
||||||
object MangaTypeAdapter {
|
|
||||||
|
|
||||||
fun build(): TypeAdapter<MangaImpl> {
|
|
||||||
return typeAdapter {
|
|
||||||
write {
|
|
||||||
beginArray()
|
|
||||||
value(it.url)
|
|
||||||
value(it.title)
|
|
||||||
value(it.source)
|
|
||||||
value(it.viewer_flags)
|
|
||||||
value(it.chapter_flags)
|
|
||||||
endArray()
|
|
||||||
}
|
|
||||||
|
|
||||||
read {
|
|
||||||
beginArray()
|
|
||||||
val manga = MangaImpl()
|
|
||||||
manga.url = nextString()
|
|
||||||
manga.title = nextString()
|
|
||||||
manga.source = nextLong()
|
|
||||||
manga.viewer_flags = nextInt()
|
|
||||||
manga.chapter_flags = nextInt()
|
|
||||||
endArray()
|
|
||||||
manga
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,56 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.legacy.serializer
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.MangaImpl
|
||||||
|
import kotlinx.serialization.KSerializer
|
||||||
|
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||||
|
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
|
||||||
|
import kotlinx.serialization.encoding.Decoder
|
||||||
|
import kotlinx.serialization.encoding.Encoder
|
||||||
|
import kotlinx.serialization.json.JsonDecoder
|
||||||
|
import kotlinx.serialization.json.JsonEncoder
|
||||||
|
import kotlinx.serialization.json.add
|
||||||
|
import kotlinx.serialization.json.buildJsonArray
|
||||||
|
import kotlinx.serialization.json.int
|
||||||
|
import kotlinx.serialization.json.jsonArray
|
||||||
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
|
import kotlinx.serialization.json.long
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON Serializer used to write / read [MangaImpl] to / from json
|
||||||
|
*/
|
||||||
|
open class MangaBaseSerializer<T : Manga> : KSerializer<T> {
|
||||||
|
override val descriptor: SerialDescriptor = buildClassSerialDescriptor("Manga")
|
||||||
|
|
||||||
|
override fun serialize(encoder: Encoder, value: T) {
|
||||||
|
encoder as JsonEncoder
|
||||||
|
encoder.encodeJsonElement(
|
||||||
|
buildJsonArray {
|
||||||
|
add(value.url)
|
||||||
|
add(value.title)
|
||||||
|
add(value.source)
|
||||||
|
add(value.viewer_flags)
|
||||||
|
add(value.chapter_flags)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
override fun deserialize(decoder: Decoder): T {
|
||||||
|
// make a manga impl and cast as T so that the serializer accepts it
|
||||||
|
return MangaImpl().apply {
|
||||||
|
decoder as JsonDecoder
|
||||||
|
val array = decoder.decodeJsonElement().jsonArray
|
||||||
|
url = array[0].jsonPrimitive.content
|
||||||
|
title = array[1].jsonPrimitive.content
|
||||||
|
source = array[2].jsonPrimitive.long
|
||||||
|
viewer_flags = array[3].jsonPrimitive.int
|
||||||
|
chapter_flags = array[4].jsonPrimitive.int
|
||||||
|
} as T
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow for serialization of a manga and manga impl
|
||||||
|
object MangaTypeSerializer : MangaBaseSerializer<Manga>()
|
||||||
|
|
||||||
|
object MangaImplTypeSerializer : MangaBaseSerializer<MangaImpl>()
|
@ -1,59 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.legacy.serializer
|
|
||||||
|
|
||||||
import com.github.salomonbrys.kotson.typeAdapter
|
|
||||||
import com.google.gson.TypeAdapter
|
|
||||||
import com.google.gson.stream.JsonToken
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.TrackImpl
|
|
||||||
|
|
||||||
/**
|
|
||||||
* JSON Serializer used to write / read [TrackImpl] to / from json
|
|
||||||
*/
|
|
||||||
object TrackTypeAdapter {
|
|
||||||
|
|
||||||
private const val SYNC = "s"
|
|
||||||
private const val MEDIA = "r"
|
|
||||||
private const val LIBRARY = "ml"
|
|
||||||
private const val TITLE = "t"
|
|
||||||
private const val LAST_READ = "l"
|
|
||||||
private const val TRACKING_URL = "u"
|
|
||||||
|
|
||||||
fun build(): TypeAdapter<TrackImpl> {
|
|
||||||
return typeAdapter {
|
|
||||||
write {
|
|
||||||
beginObject()
|
|
||||||
name(TITLE)
|
|
||||||
value(it.title)
|
|
||||||
name(SYNC)
|
|
||||||
value(it.sync_id)
|
|
||||||
name(MEDIA)
|
|
||||||
value(it.media_id)
|
|
||||||
name(LIBRARY)
|
|
||||||
value(it.library_id)
|
|
||||||
name(LAST_READ)
|
|
||||||
value(it.last_chapter_read)
|
|
||||||
name(TRACKING_URL)
|
|
||||||
value(it.tracking_url)
|
|
||||||
endObject()
|
|
||||||
}
|
|
||||||
|
|
||||||
read {
|
|
||||||
val track = TrackImpl()
|
|
||||||
beginObject()
|
|
||||||
while (hasNext()) {
|
|
||||||
if (peek() == JsonToken.NAME) {
|
|
||||||
when (nextName()) {
|
|
||||||
TITLE -> track.title = nextString()
|
|
||||||
SYNC -> track.sync_id = nextInt()
|
|
||||||
MEDIA -> track.media_id = nextInt()
|
|
||||||
LIBRARY -> track.library_id = nextLong()
|
|
||||||
LAST_READ -> track.last_chapter_read = nextInt()
|
|
||||||
TRACKING_URL -> track.tracking_url = nextString()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
endObject()
|
|
||||||
track
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,67 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.legacy.serializer
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.TrackImpl
|
||||||
|
import kotlinx.serialization.KSerializer
|
||||||
|
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||||
|
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
|
||||||
|
import kotlinx.serialization.encoding.Decoder
|
||||||
|
import kotlinx.serialization.encoding.Encoder
|
||||||
|
import kotlinx.serialization.json.JsonDecoder
|
||||||
|
import kotlinx.serialization.json.JsonEncoder
|
||||||
|
import kotlinx.serialization.json.buildJsonObject
|
||||||
|
import kotlinx.serialization.json.int
|
||||||
|
import kotlinx.serialization.json.jsonObject
|
||||||
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
|
import kotlinx.serialization.json.long
|
||||||
|
import kotlinx.serialization.json.put
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON Serializer used to write / read [TrackImpl] to / from json
|
||||||
|
*/
|
||||||
|
open class TrackBaseSerializer<T : Track> : KSerializer<T> {
|
||||||
|
override val descriptor: SerialDescriptor = buildClassSerialDescriptor("Track")
|
||||||
|
|
||||||
|
override fun serialize(encoder: Encoder, value: T) {
|
||||||
|
encoder as JsonEncoder
|
||||||
|
encoder.encodeJsonElement(
|
||||||
|
buildJsonObject {
|
||||||
|
put(TITLE, value.title)
|
||||||
|
put(SYNC, value.sync_id)
|
||||||
|
put(MEDIA, value.media_id)
|
||||||
|
put(LIBRARY, value.library_id)
|
||||||
|
put(LAST_READ, value.last_chapter_read)
|
||||||
|
put(TRACKING_URL, value.tracking_url)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
override fun deserialize(decoder: Decoder): T {
|
||||||
|
// make a track impl and cast as T so that the serializer accepts it
|
||||||
|
return TrackImpl().apply {
|
||||||
|
decoder as JsonDecoder
|
||||||
|
val jsonObject = decoder.decodeJsonElement().jsonObject
|
||||||
|
title = jsonObject[TITLE]!!.jsonPrimitive.content
|
||||||
|
sync_id = jsonObject[SYNC]!!.jsonPrimitive.int
|
||||||
|
media_id = jsonObject[MEDIA]!!.jsonPrimitive.int
|
||||||
|
library_id = jsonObject[LIBRARY]!!.jsonPrimitive.long
|
||||||
|
last_chapter_read = jsonObject[LAST_READ]!!.jsonPrimitive.int
|
||||||
|
tracking_url = jsonObject[TRACKING_URL]!!.jsonPrimitive.content
|
||||||
|
} as T
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val SYNC = "s"
|
||||||
|
private const val MEDIA = "r"
|
||||||
|
private const val LIBRARY = "ml"
|
||||||
|
private const val TITLE = "t"
|
||||||
|
private const val LAST_READ = "l"
|
||||||
|
private const val TRACKING_URL = "u"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow for serialization of a track and track impl
|
||||||
|
object TrackTypeSerializer : TrackBaseSerializer<Track>()
|
||||||
|
|
||||||
|
object TrackImplTypeSerializer : TrackBaseSerializer<TrackImpl>()
|
@ -27,7 +27,6 @@ import uy.kohesive.injekt.Injekt
|
|||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.Date
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Coil component that fetches [Manga] cover while using the cached file in disk when available.
|
* Coil component that fetches [Manga] cover while using the cached file in disk when available.
|
||||||
@ -62,14 +61,15 @@ class MangaCoverFetcher : Fetcher<Manga> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun httpLoader(manga: Manga, options: Options): FetchResult {
|
private suspend fun httpLoader(manga: Manga, options: Options): FetchResult {
|
||||||
val coverFile = coverCache.getCoverFile(manga) ?: error("No cover specified")
|
// Only cache separately if it's a library item
|
||||||
|
val coverCacheFile = if (manga.favorite) {
|
||||||
|
coverCache.getCoverFile(manga) ?: error("No cover specified")
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
// Use previously cached cover if exist
|
if (coverCacheFile?.exists() == true && options.diskCachePolicy.readEnabled) {
|
||||||
if (coverFile.exists() && options.diskCachePolicy.readEnabled) {
|
return fileLoader(coverCacheFile)
|
||||||
if (!manga.favorite) {
|
|
||||||
coverFile.setLastModified(Date().time)
|
|
||||||
}
|
|
||||||
return fileLoader(coverFile)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val (response, body) = awaitGetCall(manga, options)
|
val (response, body) = awaitGetCall(manga, options)
|
||||||
@ -78,18 +78,16 @@ class MangaCoverFetcher : Fetcher<Manga> {
|
|||||||
throw HttpException(response)
|
throw HttpException(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write to disk for future use
|
if (coverCacheFile != null && options.diskCachePolicy.writeEnabled) {
|
||||||
if (options.diskCachePolicy.writeEnabled) {
|
@Suppress("BlockingMethodInNonBlockingContext")
|
||||||
response.peekBody(Long.MAX_VALUE).source().use { input ->
|
response.peekBody(Long.MAX_VALUE).source().use { input ->
|
||||||
val tmpFile = File(coverFile.absolutePath + "_tmp")
|
coverCacheFile.parentFile?.mkdirs()
|
||||||
tmpFile.parentFile?.mkdirs()
|
if (coverCacheFile.exists()) {
|
||||||
tmpFile.sink().buffer().use { output ->
|
coverCacheFile.delete()
|
||||||
|
}
|
||||||
|
coverCacheFile.sink().buffer().use { output ->
|
||||||
output.writeAll(input)
|
output.writeAll(input)
|
||||||
}
|
}
|
||||||
if (coverFile.exists()) {
|
|
||||||
coverFile.delete()
|
|
||||||
}
|
|
||||||
tmpFile.renameTo(coverFile)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -108,10 +106,6 @@ class MangaCoverFetcher : Fetcher<Manga> {
|
|||||||
|
|
||||||
private fun getCall(manga: Manga, options: Options): Call {
|
private fun getCall(manga: Manga, options: Options): Call {
|
||||||
val source = sourceManager.get(manga.source) as? HttpSource
|
val source = sourceManager.get(manga.source) as? HttpSource
|
||||||
val client = source?.client ?: defaultClient
|
|
||||||
|
|
||||||
val newClient = client.newBuilder().build()
|
|
||||||
|
|
||||||
val request = Request.Builder().url(manga.thumbnail_url!!).also {
|
val request = Request.Builder().url(manga.thumbnail_url!!).also {
|
||||||
if (source != null) {
|
if (source != null) {
|
||||||
it.headers(source.headers)
|
it.headers(source.headers)
|
||||||
@ -135,7 +129,8 @@ class MangaCoverFetcher : Fetcher<Manga> {
|
|||||||
}
|
}
|
||||||
}.build()
|
}.build()
|
||||||
|
|
||||||
return newClient.newCall(request)
|
val client = source?.client?.newBuilder()?.cache(defaultClient.cache)?.build() ?: defaultClient
|
||||||
|
return client.newCall(request)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fileLoader(manga: Manga): FetchResult {
|
private fun fileLoader(manga: Manga): FetchResult {
|
||||||
@ -153,7 +148,7 @@ class MangaCoverFetcher : Fetcher<Manga> {
|
|||||||
private fun getResourceType(cover: String?): Type? {
|
private fun getResourceType(cover: String?): Type? {
|
||||||
return when {
|
return when {
|
||||||
cover.isNullOrEmpty() -> null
|
cover.isNullOrEmpty() -> null
|
||||||
cover.startsWith("http") || cover.startsWith("Custom-", true) -> Type.URL
|
cover.startsWith("http", true) || cover.startsWith("Custom-", true) -> Type.URL
|
||||||
cover.startsWith("/") || cover.startsWith("file://") -> Type.File
|
cover.startsWith("/") || cover.startsWith("file://") -> Type.File
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,53 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.coil
|
||||||
|
|
||||||
|
import android.content.res.Resources
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.core.graphics.drawable.toDrawable
|
||||||
|
import coil.bitmap.BitmapPool
|
||||||
|
import coil.decode.DecodeResult
|
||||||
|
import coil.decode.Decoder
|
||||||
|
import coil.decode.Options
|
||||||
|
import coil.size.Size
|
||||||
|
import eu.kanade.tachiyomi.util.system.ImageUtil
|
||||||
|
import okio.BufferedSource
|
||||||
|
import tachiyomi.decoder.ImageDecoder
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A [Decoder] that uses built-in [ImageDecoder] to decode images that is not supported by the system.
|
||||||
|
*/
|
||||||
|
class TachiyomiImageDecoder(private val resources: Resources) : Decoder {
|
||||||
|
|
||||||
|
override fun handles(source: BufferedSource, mimeType: String?): Boolean {
|
||||||
|
val type = source.peek().inputStream().use {
|
||||||
|
ImageUtil.findImageType(it)
|
||||||
|
}
|
||||||
|
return when (type) {
|
||||||
|
ImageUtil.ImageType.AVIF, ImageUtil.ImageType.JXL -> true
|
||||||
|
ImageUtil.ImageType.HEIF -> Build.VERSION.SDK_INT < Build.VERSION_CODES.O
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun decode(
|
||||||
|
pool: BitmapPool,
|
||||||
|
source: BufferedSource,
|
||||||
|
size: Size,
|
||||||
|
options: Options
|
||||||
|
): DecodeResult {
|
||||||
|
val decoder = source.use {
|
||||||
|
ImageDecoder.newInstance(it.inputStream())
|
||||||
|
}
|
||||||
|
|
||||||
|
check(decoder != null && decoder.width > 0 && decoder.height > 0) { "Failed to initialize decoder." }
|
||||||
|
|
||||||
|
val bitmap = decoder.decode(rgb565 = options.allowRgb565)
|
||||||
|
decoder.recycle()
|
||||||
|
|
||||||
|
check(bitmap != null) { "Failed to decode image." }
|
||||||
|
|
||||||
|
return DecodeResult(
|
||||||
|
drawable = bitmap.toDrawable(resources),
|
||||||
|
isSampled = false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -20,7 +20,7 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
|
|||||||
/**
|
/**
|
||||||
* Version of the database.
|
* Version of the database.
|
||||||
*/
|
*/
|
||||||
const val DATABASE_VERSION = 11
|
const val DATABASE_VERSION = 12
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate(db: SupportSQLiteDatabase) = with(db) {
|
override fun onCreate(db: SupportSQLiteDatabase) = with(db) {
|
||||||
@ -82,6 +82,9 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
|
|||||||
db.execSQL(MangaTable.addDateAdded)
|
db.execSQL(MangaTable.addDateAdded)
|
||||||
db.execSQL(MangaTable.backfillDateAdded)
|
db.execSQL(MangaTable.backfillDateAdded)
|
||||||
}
|
}
|
||||||
|
if (oldVersion < 12) {
|
||||||
|
db.execSQL(MangaTable.addNextUpdateCol)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onConfigure(db: SupportSQLiteDatabase) {
|
override fun onConfigure(db: SupportSQLiteDatabase) {
|
||||||
|
@ -22,6 +22,7 @@ import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_GENRE
|
|||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_ID
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_ID
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_INITIALIZED
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_INITIALIZED
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_LAST_UPDATE
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_LAST_UPDATE
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_NEXT_UPDATE
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_SOURCE
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_SOURCE
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_STATUS
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_STATUS
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_THUMBNAIL_URL
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_THUMBNAIL_URL
|
||||||
@ -62,6 +63,7 @@ class MangaPutResolver : DefaultPutResolver<Manga>() {
|
|||||||
COL_THUMBNAIL_URL to obj.thumbnail_url,
|
COL_THUMBNAIL_URL to obj.thumbnail_url,
|
||||||
COL_FAVORITE to obj.favorite,
|
COL_FAVORITE to obj.favorite,
|
||||||
COL_LAST_UPDATE to obj.last_update,
|
COL_LAST_UPDATE to obj.last_update,
|
||||||
|
COL_NEXT_UPDATE to obj.next_update,
|
||||||
COL_INITIALIZED to obj.initialized,
|
COL_INITIALIZED to obj.initialized,
|
||||||
COL_VIEWER to obj.viewer_flags,
|
COL_VIEWER to obj.viewer_flags,
|
||||||
COL_CHAPTER_FLAGS to obj.chapter_flags,
|
COL_CHAPTER_FLAGS to obj.chapter_flags,
|
||||||
@ -84,6 +86,7 @@ interface BaseMangaGetResolver {
|
|||||||
thumbnail_url = cursor.getString(cursor.getColumnIndex(COL_THUMBNAIL_URL))
|
thumbnail_url = cursor.getString(cursor.getColumnIndex(COL_THUMBNAIL_URL))
|
||||||
favorite = cursor.getInt(cursor.getColumnIndex(COL_FAVORITE)) == 1
|
favorite = cursor.getInt(cursor.getColumnIndex(COL_FAVORITE)) == 1
|
||||||
last_update = cursor.getLong(cursor.getColumnIndex(COL_LAST_UPDATE))
|
last_update = cursor.getLong(cursor.getColumnIndex(COL_LAST_UPDATE))
|
||||||
|
next_update = cursor.getLong(cursor.getColumnIndex(COL_NEXT_UPDATE))
|
||||||
initialized = cursor.getInt(cursor.getColumnIndex(COL_INITIALIZED)) == 1
|
initialized = cursor.getInt(cursor.getColumnIndex(COL_INITIALIZED)) == 1
|
||||||
viewer_flags = cursor.getInt(cursor.getColumnIndex(COL_VIEWER))
|
viewer_flags = cursor.getInt(cursor.getColumnIndex(COL_VIEWER))
|
||||||
chapter_flags = cursor.getInt(cursor.getColumnIndex(COL_CHAPTER_FLAGS))
|
chapter_flags = cursor.getInt(cursor.getColumnIndex(COL_CHAPTER_FLAGS))
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.models
|
package eu.kanade.tachiyomi.data.database.models
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.ui.library.setting.DisplayModeSetting
|
||||||
|
import eu.kanade.tachiyomi.ui.library.setting.SortDirectionSetting
|
||||||
|
import eu.kanade.tachiyomi.ui.library.setting.SortModeSetting
|
||||||
import java.io.Serializable
|
import java.io.Serializable
|
||||||
|
|
||||||
interface Category : Serializable {
|
interface Category : Serializable {
|
||||||
@ -12,6 +15,22 @@ interface Category : Serializable {
|
|||||||
|
|
||||||
var flags: Int
|
var flags: Int
|
||||||
|
|
||||||
|
private fun setFlags(flag: Int, mask: Int) {
|
||||||
|
flags = flags and mask.inv() or (flag and mask)
|
||||||
|
}
|
||||||
|
|
||||||
|
var displayMode: Int
|
||||||
|
get() = flags and DisplayModeSetting.MASK
|
||||||
|
set(mode) = setFlags(mode, DisplayModeSetting.MASK)
|
||||||
|
|
||||||
|
var sortMode: Int
|
||||||
|
get() = flags and SortModeSetting.MASK
|
||||||
|
set(mode) = setFlags(mode, SortModeSetting.MASK)
|
||||||
|
|
||||||
|
var sortDirection: Int
|
||||||
|
get() = flags and SortDirectionSetting.MASK
|
||||||
|
set(mode) = setFlags(mode, SortDirectionSetting.MASK)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
fun create(name: String): Category = CategoryImpl().apply {
|
fun create(name: String): Category = CategoryImpl().apply {
|
||||||
|
@ -13,8 +13,12 @@ interface Manga : SManga {
|
|||||||
|
|
||||||
var favorite: Boolean
|
var favorite: Boolean
|
||||||
|
|
||||||
|
// last time the chapter list changed in any way
|
||||||
var last_update: Long
|
var last_update: Long
|
||||||
|
|
||||||
|
// predicted next update time based on latest (by date) 4 chapters' deltas
|
||||||
|
var next_update: Long
|
||||||
|
|
||||||
var date_added: Long
|
var date_added: Long
|
||||||
|
|
||||||
var viewer_flags: Int
|
var viewer_flags: Int
|
||||||
|
@ -26,6 +26,8 @@ open class MangaImpl : Manga {
|
|||||||
|
|
||||||
override var last_update: Long = 0
|
override var last_update: Long = 0
|
||||||
|
|
||||||
|
override var next_update: Long = 0
|
||||||
|
|
||||||
override var date_added: Long = 0
|
override var date_added: Long = 0
|
||||||
|
|
||||||
override var initialized: Boolean = false
|
override var initialized: Boolean = false
|
||||||
|
@ -1,17 +1,13 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.queries
|
package eu.kanade.tachiyomi.data.database.queries
|
||||||
|
|
||||||
|
import com.pushtorefresh.storio.sqlite.operations.get.PreparedGetListOfObjects
|
||||||
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
||||||
import com.pushtorefresh.storio.sqlite.queries.Query
|
import com.pushtorefresh.storio.sqlite.queries.Query
|
||||||
import com.pushtorefresh.storio.sqlite.queries.RawQuery
|
import com.pushtorefresh.storio.sqlite.queries.RawQuery
|
||||||
import eu.kanade.tachiyomi.data.database.DbProvider
|
import eu.kanade.tachiyomi.data.database.DbProvider
|
||||||
import eu.kanade.tachiyomi.data.database.models.LibraryManga
|
import eu.kanade.tachiyomi.data.database.models.LibraryManga
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.LibraryMangaGetResolver
|
import eu.kanade.tachiyomi.data.database.resolvers.*
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaCoverLastModifiedPutResolver
|
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaFavoritePutResolver
|
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaFlagsPutResolver
|
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaLastUpdatedPutResolver
|
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaTitlePutResolver
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.CategoryTable
|
import eu.kanade.tachiyomi.data.database.tables.CategoryTable
|
||||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable
|
import eu.kanade.tachiyomi.data.database.tables.ChapterTable
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable
|
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable
|
||||||
@ -19,15 +15,6 @@ import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
|||||||
|
|
||||||
interface MangaQueries : DbProvider {
|
interface MangaQueries : DbProvider {
|
||||||
|
|
||||||
fun getMangas() = db.get()
|
|
||||||
.listOfObjects(Manga::class.java)
|
|
||||||
.withQuery(
|
|
||||||
Query.builder()
|
|
||||||
.table(MangaTable.TABLE)
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
.prepare()
|
|
||||||
|
|
||||||
fun getLibraryMangas() = db.get()
|
fun getLibraryMangas() = db.get()
|
||||||
.listOfObjects(LibraryManga::class.java)
|
.listOfObjects(LibraryManga::class.java)
|
||||||
.withQuery(
|
.withQuery(
|
||||||
@ -39,17 +26,21 @@ interface MangaQueries : DbProvider {
|
|||||||
.withGetResolver(LibraryMangaGetResolver.INSTANCE)
|
.withGetResolver(LibraryMangaGetResolver.INSTANCE)
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
fun getFavoriteMangas() = db.get()
|
fun getFavoriteMangas(sortByTitle: Boolean = true): PreparedGetListOfObjects<Manga> {
|
||||||
.listOfObjects(Manga::class.java)
|
var queryBuilder = Query.builder()
|
||||||
.withQuery(
|
.table(MangaTable.TABLE)
|
||||||
Query.builder()
|
.where("${MangaTable.COL_FAVORITE} = ?")
|
||||||
.table(MangaTable.TABLE)
|
.whereArgs(1)
|
||||||
.where("${MangaTable.COL_FAVORITE} = ?")
|
|
||||||
.whereArgs(1)
|
if (sortByTitle) {
|
||||||
.orderBy(MangaTable.COL_TITLE)
|
queryBuilder = queryBuilder.orderBy(MangaTable.COL_TITLE)
|
||||||
.build()
|
}
|
||||||
)
|
|
||||||
.prepare()
|
return db.get()
|
||||||
|
.listOfObjects(Manga::class.java)
|
||||||
|
.withQuery(queryBuilder.build())
|
||||||
|
.prepare()
|
||||||
|
}
|
||||||
|
|
||||||
fun getManga(url: String, sourceId: Long) = db.get()
|
fun getManga(url: String, sourceId: Long) = db.get()
|
||||||
.`object`(Manga::class.java)
|
.`object`(Manga::class.java)
|
||||||
@ -97,6 +88,11 @@ interface MangaQueries : DbProvider {
|
|||||||
.withPutResolver(MangaFlagsPutResolver(MangaTable.COL_VIEWER, Manga::viewer_flags, true))
|
.withPutResolver(MangaFlagsPutResolver(MangaTable.COL_VIEWER, Manga::viewer_flags, true))
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
|
fun updateNextUpdated(manga: Manga) = db.put()
|
||||||
|
.`object`(manga)
|
||||||
|
.withPutResolver(MangaNextUpdatedPutResolver())
|
||||||
|
.prepare()
|
||||||
|
|
||||||
fun updateLastUpdated(manga: Manga) = db.put()
|
fun updateLastUpdated(manga: Manga) = db.put()
|
||||||
.`object`(manga)
|
.`object`(manga)
|
||||||
.withPutResolver(MangaLastUpdatedPutResolver())
|
.withPutResolver(MangaLastUpdatedPutResolver())
|
||||||
|
@ -0,0 +1,31 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.database.resolvers
|
||||||
|
|
||||||
|
import android.content.ContentValues
|
||||||
|
import com.pushtorefresh.storio.sqlite.StorIOSQLite
|
||||||
|
import com.pushtorefresh.storio.sqlite.operations.put.PutResolver
|
||||||
|
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
|
||||||
|
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
|
||||||
|
import eu.kanade.tachiyomi.data.database.inTransactionReturn
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
||||||
|
|
||||||
|
class MangaNextUpdatedPutResolver : PutResolver<Manga>() {
|
||||||
|
|
||||||
|
override fun performPut(db: StorIOSQLite, manga: Manga) = db.inTransactionReturn {
|
||||||
|
val updateQuery = mapToUpdateQuery(manga)
|
||||||
|
val contentValues = mapToContentValues(manga)
|
||||||
|
|
||||||
|
val numberOfRowsUpdated = db.lowLevel().update(updateQuery, contentValues)
|
||||||
|
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder()
|
||||||
|
.table(MangaTable.TABLE)
|
||||||
|
.where("${MangaTable.COL_ID} = ?")
|
||||||
|
.whereArgs(manga.id)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
|
||||||
|
put(MangaTable.COL_NEXT_UPDATE, manga.next_update)
|
||||||
|
}
|
||||||
|
}
|
@ -28,6 +28,8 @@ object MangaTable {
|
|||||||
|
|
||||||
const val COL_LAST_UPDATE = "last_update"
|
const val COL_LAST_UPDATE = "last_update"
|
||||||
|
|
||||||
|
const val COL_NEXT_UPDATE = "next_update"
|
||||||
|
|
||||||
const val COL_DATE_ADDED = "date_added"
|
const val COL_DATE_ADDED = "date_added"
|
||||||
|
|
||||||
const val COL_INITIALIZED = "initialized"
|
const val COL_INITIALIZED = "initialized"
|
||||||
@ -57,6 +59,7 @@ object MangaTable {
|
|||||||
$COL_THUMBNAIL_URL TEXT,
|
$COL_THUMBNAIL_URL TEXT,
|
||||||
$COL_FAVORITE INTEGER NOT NULL,
|
$COL_FAVORITE INTEGER NOT NULL,
|
||||||
$COL_LAST_UPDATE LONG,
|
$COL_LAST_UPDATE LONG,
|
||||||
|
$COL_NEXT_UPDATE LONG,
|
||||||
$COL_INITIALIZED BOOLEAN NOT NULL,
|
$COL_INITIALIZED BOOLEAN NOT NULL,
|
||||||
$COL_VIEWER INTEGER NOT NULL,
|
$COL_VIEWER INTEGER NOT NULL,
|
||||||
$COL_CHAPTER_FLAGS INTEGER NOT NULL,
|
$COL_CHAPTER_FLAGS INTEGER NOT NULL,
|
||||||
@ -86,4 +89,7 @@ object MangaTable {
|
|||||||
"FROM $TABLE INNER JOIN ${ChapterTable.TABLE} " +
|
"FROM $TABLE INNER JOIN ${ChapterTable.TABLE} " +
|
||||||
"ON $TABLE.$COL_ID = ${ChapterTable.TABLE}.${ChapterTable.COL_MANGA_ID} " +
|
"ON $TABLE.$COL_ID = ${ChapterTable.TABLE}.${ChapterTable.COL_MANGA_ID} " +
|
||||||
"GROUP BY $TABLE.$COL_ID)"
|
"GROUP BY $TABLE.$COL_ID)"
|
||||||
|
|
||||||
|
val addNextUpdateCol: String
|
||||||
|
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_NEXT_UPDATE LONG DEFAULT 0"
|
||||||
}
|
}
|
||||||
|
@ -95,6 +95,23 @@ class DownloadManager(private val context: Context) {
|
|||||||
downloader.clearQueue(isNotification)
|
downloader.clearQueue(isNotification)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun startDownloadNow(chapter: Chapter) {
|
||||||
|
val download = downloader.queue.find { it.chapter.id == chapter.id } ?: return
|
||||||
|
val queue = downloader.queue.toMutableList()
|
||||||
|
queue.remove(download)
|
||||||
|
queue.add(0, download)
|
||||||
|
reorderQueue(queue)
|
||||||
|
if (isPaused()) {
|
||||||
|
if (DownloadService.isRunning(context)) {
|
||||||
|
downloader.start()
|
||||||
|
} else {
|
||||||
|
DownloadService.start(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isPaused() = downloader.isPaused()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reorders the download queue.
|
* Reorders the download queue.
|
||||||
*
|
*
|
||||||
|
@ -19,6 +19,7 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
|||||||
import eu.kanade.tachiyomi.util.lang.plusAssign
|
import eu.kanade.tachiyomi.util.lang.plusAssign
|
||||||
import eu.kanade.tachiyomi.util.system.acquireWakeLock
|
import eu.kanade.tachiyomi.util.system.acquireWakeLock
|
||||||
import eu.kanade.tachiyomi.util.system.connectivityManager
|
import eu.kanade.tachiyomi.util.system.connectivityManager
|
||||||
|
import eu.kanade.tachiyomi.util.system.isServiceRunning
|
||||||
import eu.kanade.tachiyomi.util.system.notification
|
import eu.kanade.tachiyomi.util.system.notification
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
@ -58,6 +59,16 @@ class DownloadService : Service() {
|
|||||||
fun stop(context: Context) {
|
fun stop(context: Context) {
|
||||||
context.stopService(Intent(context, DownloadService::class.java))
|
context.stopService(Intent(context, DownloadService::class.java))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the status of the service.
|
||||||
|
*
|
||||||
|
* @param context the application context.
|
||||||
|
* @return true if the service is running, false otherwise.
|
||||||
|
*/
|
||||||
|
fun isRunning(context: Context): Boolean {
|
||||||
|
return context.isServiceRunning(DownloadService::class.java)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val downloadManager: DownloadManager by injectLazy()
|
private val downloadManager: DownloadManager by injectLazy()
|
||||||
|
@ -157,6 +157,11 @@ class Downloader(
|
|||||||
notifier.paused = true
|
notifier.paused = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if downloader is paused
|
||||||
|
*/
|
||||||
|
fun isPaused() = !isRunning
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes everything from the queue.
|
* Removes everything from the queue.
|
||||||
*
|
*
|
||||||
|
@ -64,22 +64,25 @@ class LibraryUpdateNotifier(private val context: Context) {
|
|||||||
/**
|
/**
|
||||||
* Shows the notification containing the currently updating manga and the progress.
|
* Shows the notification containing the currently updating manga and the progress.
|
||||||
*
|
*
|
||||||
* @param manga the manga that's being updated.
|
* @param manga the manga that are being updated.
|
||||||
* @param current the current progress.
|
* @param current the current progress.
|
||||||
* @param total the total progress.
|
* @param total the total progress.
|
||||||
*/
|
*/
|
||||||
fun showProgressNotification(manga: Manga, current: Int, total: Int) {
|
fun showProgressNotification(manga: List<Manga>, current: Int, total: Int) {
|
||||||
val title = if (preferences.hideNotificationContent()) {
|
if (preferences.hideNotificationContent()) {
|
||||||
context.getString(R.string.notification_check_updates)
|
progressNotificationBuilder
|
||||||
|
.setContentTitle(context.getString(R.string.notification_check_updates))
|
||||||
|
.setContentText("($current/$total)")
|
||||||
} else {
|
} else {
|
||||||
manga.title
|
val updatingText = manga.joinToString("\n") { it.title.chop(40) }
|
||||||
|
progressNotificationBuilder
|
||||||
|
.setContentTitle(context.getString(R.string.notification_updating, current, total))
|
||||||
|
.setStyle(NotificationCompat.BigTextStyle().bigText(updatingText))
|
||||||
}
|
}
|
||||||
|
|
||||||
context.notificationManager.notify(
|
context.notificationManager.notify(
|
||||||
Notifications.ID_LIBRARY_PROGRESS,
|
Notifications.ID_LIBRARY_PROGRESS,
|
||||||
progressNotificationBuilder
|
progressNotificationBuilder
|
||||||
.setContentTitle(title.chop(40))
|
|
||||||
.setContentText("($current/$total)")
|
|
||||||
.setProgress(total, current, false)
|
.setProgress(total, current, false)
|
||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
@ -203,7 +206,7 @@ class LibraryUpdateNotifier(private val context: Context) {
|
|||||||
|
|
||||||
// Mark chapters as read action
|
// Mark chapters as read action
|
||||||
addAction(
|
addAction(
|
||||||
R.drawable.ic_glasses_black_24dp,
|
R.drawable.ic_glasses_24dp,
|
||||||
context.getString(R.string.action_mark_as_read),
|
context.getString(R.string.action_mark_as_read),
|
||||||
NotificationReceiver.markAsReadPendingBroadcast(
|
NotificationReceiver.markAsReadPendingBroadcast(
|
||||||
context,
|
context,
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
package eu.kanade.tachiyomi.data.library
|
package eu.kanade.tachiyomi.data.library
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import java.util.Collections
|
||||||
|
import kotlin.Comparator
|
||||||
|
import kotlin.math.abs
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class will provide various functions to rank manga to efficiently schedule manga to update.
|
* This class will provide various functions to rank manga to efficiently schedule manga to update.
|
||||||
@ -9,9 +12,26 @@ object LibraryUpdateRanker {
|
|||||||
|
|
||||||
val rankingScheme = listOf(
|
val rankingScheme = listOf(
|
||||||
(this::lexicographicRanking)(),
|
(this::lexicographicRanking)(),
|
||||||
(this::latestFirstRanking)()
|
(this::latestFirstRanking)(),
|
||||||
|
(this::nextFirstRanking)()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a total ordering over all the Mangas.
|
||||||
|
*
|
||||||
|
* Orders the manga based on the distance between the next expected update and now.
|
||||||
|
* The comparator is reversed, placing the smallest (and thus closest to updating now) first.
|
||||||
|
*/
|
||||||
|
fun nextFirstRanking(): Comparator<Manga> {
|
||||||
|
val time = System.currentTimeMillis()
|
||||||
|
return Collections.reverseOrder(
|
||||||
|
Comparator { mangaFirst: Manga,
|
||||||
|
mangaSecond: Manga ->
|
||||||
|
compareValues(abs(mangaSecond.next_update - time), abs(mangaFirst.next_update - time))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides a total ordering over all the [Manga]s.
|
* Provides a total ordering over all the [Manga]s.
|
||||||
*
|
*
|
||||||
|
@ -20,9 +20,9 @@ import eu.kanade.tachiyomi.data.library.LibraryUpdateRanker.rankingScheme
|
|||||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Companion.start
|
import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Companion.start
|
||||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
|
import eu.kanade.tachiyomi.data.track.EnhancedTrackService
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
import eu.kanade.tachiyomi.data.track.TrackService
|
import eu.kanade.tachiyomi.data.track.TrackService
|
||||||
import eu.kanade.tachiyomi.data.track.UnattendedTrackService
|
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.model.toSChapter
|
import eu.kanade.tachiyomi.source.model.toSChapter
|
||||||
@ -30,6 +30,7 @@ import eu.kanade.tachiyomi.source.model.toSManga
|
|||||||
import eu.kanade.tachiyomi.util.chapter.NoChaptersException
|
import eu.kanade.tachiyomi.util.chapter.NoChaptersException
|
||||||
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
|
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
|
||||||
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithTrackServiceTwoWay
|
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithTrackServiceTwoWay
|
||||||
|
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||||
import eu.kanade.tachiyomi.util.prepUpdateCover
|
import eu.kanade.tachiyomi.util.prepUpdateCover
|
||||||
import eu.kanade.tachiyomi.util.shouldDownloadNewChapters
|
import eu.kanade.tachiyomi.util.shouldDownloadNewChapters
|
||||||
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
||||||
@ -47,10 +48,14 @@ import kotlinx.coroutines.awaitAll
|
|||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.supervisorScope
|
import kotlinx.coroutines.supervisorScope
|
||||||
|
import kotlinx.coroutines.sync.Semaphore
|
||||||
|
import kotlinx.coroutines.sync.withPermit
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.util.concurrent.CopyOnWriteArrayList
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
import java.util.concurrent.atomic.AtomicInteger
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -270,50 +275,79 @@ class LibraryUpdateService(
|
|||||||
* @return an observable delivering the progress of each update.
|
* @return an observable delivering the progress of each update.
|
||||||
*/
|
*/
|
||||||
suspend fun updateChapterList() {
|
suspend fun updateChapterList() {
|
||||||
|
val semaphore = Semaphore(5)
|
||||||
val progressCount = AtomicInteger(0)
|
val progressCount = AtomicInteger(0)
|
||||||
val newUpdates = mutableListOf<Pair<LibraryManga, Array<Chapter>>>()
|
val currentlyUpdatingManga = CopyOnWriteArrayList<LibraryManga>()
|
||||||
val failedUpdates = mutableListOf<Pair<Manga, String?>>()
|
val newUpdates = CopyOnWriteArrayList<Pair<LibraryManga, Array<Chapter>>>()
|
||||||
var hasDownloads = false
|
val failedUpdates = CopyOnWriteArrayList<Pair<Manga, String?>>()
|
||||||
|
val hasDownloads = AtomicBoolean(false)
|
||||||
val loggedServices by lazy { trackManager.services.filter { it.isLogged } }
|
val loggedServices by lazy { trackManager.services.filter { it.isLogged } }
|
||||||
|
|
||||||
mangaToUpdate.forEach { manga ->
|
withIOContext {
|
||||||
if (updateJob?.isActive != true) {
|
mangaToUpdate.groupBy { it.source }
|
||||||
return
|
.values
|
||||||
}
|
.map { mangaInSource ->
|
||||||
|
async {
|
||||||
|
semaphore.withPermit {
|
||||||
|
mangaInSource.forEach { manga ->
|
||||||
|
if (updateJob?.isActive != true) {
|
||||||
|
return@async
|
||||||
|
}
|
||||||
|
|
||||||
notifier.showProgressNotification(manga, progressCount.andIncrement, mangaToUpdate.size)
|
currentlyUpdatingManga.add(manga)
|
||||||
|
notifier.showProgressNotification(
|
||||||
|
currentlyUpdatingManga,
|
||||||
|
progressCount.get(),
|
||||||
|
mangaToUpdate.size
|
||||||
|
)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val (newChapters, _) = updateManga(manga)
|
val (newChapters, _) = updateManga(manga)
|
||||||
|
|
||||||
if (newChapters.isNotEmpty()) {
|
if (newChapters.isNotEmpty()) {
|
||||||
if (manga.shouldDownloadNewChapters(db, preferences)) {
|
if (manga.shouldDownloadNewChapters(db, preferences)) {
|
||||||
downloadChapters(manga, newChapters)
|
downloadChapters(manga, newChapters)
|
||||||
hasDownloads = true
|
hasDownloads.set(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to the manga that contains new chapters
|
||||||
|
newUpdates.add(manga to newChapters.sortedByDescending { ch -> ch.source_order }.toTypedArray())
|
||||||
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
val errorMessage = if (e is NoChaptersException) {
|
||||||
|
getString(R.string.no_chapters_error)
|
||||||
|
} else if (e is SourceManager.SourceNotInstalledException) {
|
||||||
|
// failedUpdates will already have the source, don't need to copy it into the message
|
||||||
|
getString(R.string.loader_not_implemented_error)
|
||||||
|
} else {
|
||||||
|
e.message
|
||||||
|
}
|
||||||
|
failedUpdates.add(manga to errorMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preferences.autoUpdateTrackers()) {
|
||||||
|
updateTrackings(manga, loggedServices)
|
||||||
|
}
|
||||||
|
|
||||||
|
currentlyUpdatingManga.remove(manga)
|
||||||
|
progressCount.andIncrement
|
||||||
|
notifier.showProgressNotification(
|
||||||
|
currentlyUpdatingManga,
|
||||||
|
progressCount.get(),
|
||||||
|
mangaToUpdate.size
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert to the manga that contains new chapters
|
|
||||||
newUpdates.add(manga to newChapters.sortedByDescending { ch -> ch.source_order }.toTypedArray())
|
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
.awaitAll()
|
||||||
val errorMessage = if (e is NoChaptersException) {
|
|
||||||
getString(R.string.no_chapters_error)
|
|
||||||
} else {
|
|
||||||
e.message
|
|
||||||
}
|
|
||||||
failedUpdates.add(manga to errorMessage)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (preferences.autoUpdateTrackers()) {
|
|
||||||
updateTrackings(manga, loggedServices)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
notifier.cancelProgressNotification()
|
notifier.cancelProgressNotification()
|
||||||
|
|
||||||
if (newUpdates.isNotEmpty()) {
|
if (newUpdates.isNotEmpty()) {
|
||||||
notifier.showUpdateNotifications(newUpdates)
|
notifier.showUpdateNotifications(newUpdates)
|
||||||
if (hasDownloads) {
|
if (hasDownloads.get()) {
|
||||||
DownloadService.start(this)
|
DownloadService.start(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -369,29 +403,56 @@ class LibraryUpdateService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun updateCovers() {
|
private suspend fun updateCovers() {
|
||||||
var progressCount = 0
|
val semaphore = Semaphore(5)
|
||||||
|
val progressCount = AtomicInteger(0)
|
||||||
|
val currentlyUpdatingManga = CopyOnWriteArrayList<LibraryManga>()
|
||||||
|
|
||||||
mangaToUpdate.forEach { manga ->
|
withIOContext {
|
||||||
if (updateJob?.isActive != true) {
|
mangaToUpdate.groupBy { it.source }
|
||||||
return
|
.values
|
||||||
}
|
.map { mangaInSource ->
|
||||||
|
async {
|
||||||
|
semaphore.withPermit {
|
||||||
|
mangaInSource.forEach { manga ->
|
||||||
|
if (updateJob?.isActive != true) {
|
||||||
|
return@async
|
||||||
|
}
|
||||||
|
|
||||||
notifier.showProgressNotification(manga, progressCount++, mangaToUpdate.size)
|
currentlyUpdatingManga.add(manga)
|
||||||
|
notifier.showProgressNotification(
|
||||||
|
currentlyUpdatingManga,
|
||||||
|
progressCount.get(),
|
||||||
|
mangaToUpdate.size
|
||||||
|
)
|
||||||
|
|
||||||
sourceManager.get(manga.source)?.let { source ->
|
sourceManager.get(manga.source)?.let { source ->
|
||||||
try {
|
try {
|
||||||
val networkManga = source.getMangaDetails(manga.toMangaInfo())
|
val networkManga =
|
||||||
val sManga = networkManga.toSManga()
|
source.getMangaDetails(manga.toMangaInfo())
|
||||||
manga.prepUpdateCover(coverCache, sManga, true)
|
val sManga = networkManga.toSManga()
|
||||||
sManga.thumbnail_url?.let {
|
manga.prepUpdateCover(coverCache, sManga, true)
|
||||||
manga.thumbnail_url = it
|
sManga.thumbnail_url?.let {
|
||||||
db.insertManga(manga).executeAsBlocking()
|
manga.thumbnail_url = it
|
||||||
|
db.insertManga(manga).executeAsBlocking()
|
||||||
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
// Ignore errors and continue
|
||||||
|
Timber.e(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
currentlyUpdatingManga.remove(manga)
|
||||||
|
progressCount.andIncrement
|
||||||
|
notifier.showProgressNotification(
|
||||||
|
currentlyUpdatingManga,
|
||||||
|
progressCount.get(),
|
||||||
|
mangaToUpdate.size
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
|
||||||
// Ignore errors and continue
|
|
||||||
Timber.e(e)
|
|
||||||
}
|
}
|
||||||
}
|
.awaitAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
coverCache.clearMemoryCache()
|
coverCache.clearMemoryCache()
|
||||||
@ -411,8 +472,7 @@ class LibraryUpdateService(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notify manga that will update.
|
notifier.showProgressNotification(listOf(manga), progressCount++, mangaToUpdate.size)
|
||||||
notifier.showProgressNotification(manga, progressCount++, mangaToUpdate.size)
|
|
||||||
|
|
||||||
// Update the tracking details.
|
// Update the tracking details.
|
||||||
updateTrackings(manga, loggedServices)
|
updateTrackings(manga, loggedServices)
|
||||||
@ -432,7 +492,7 @@ class LibraryUpdateService(
|
|||||||
val updatedTrack = service.refresh(track)
|
val updatedTrack = service.refresh(track)
|
||||||
db.insertTrack(updatedTrack).executeAsBlocking()
|
db.insertTrack(updatedTrack).executeAsBlocking()
|
||||||
|
|
||||||
if (service is UnattendedTrackService) {
|
if (service is EnhancedTrackService) {
|
||||||
syncChaptersWithTrackServiceTwoWay(db, db.getChapters(manga).executeAsBlocking(), track, service)
|
syncChaptersWithTrackServiceTwoWay(db, db.getChapters(manga).executeAsBlocking(), track, service)
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
@ -454,9 +514,19 @@ class LibraryUpdateService(
|
|||||||
if (errors.isNotEmpty()) {
|
if (errors.isNotEmpty()) {
|
||||||
val file = createFileInCacheDir("tachiyomi_update_errors.txt")
|
val file = createFileInCacheDir("tachiyomi_update_errors.txt")
|
||||||
file.bufferedWriter().use { out ->
|
file.bufferedWriter().use { out ->
|
||||||
errors.forEach { (manga, error) ->
|
// Error file format:
|
||||||
val source = sourceManager.getOrStub(manga.source)
|
// ! Error
|
||||||
out.write("${manga.title} ($source): $error\n")
|
// # Source
|
||||||
|
// - Manga
|
||||||
|
errors.groupBy({ it.second }, { it.first }).forEach { (error, mangas) ->
|
||||||
|
out.write("! ${error}\n")
|
||||||
|
mangas.groupBy { it.source }.forEach { (srcId, mangas) ->
|
||||||
|
val source = sourceManager.getOrStub(srcId)
|
||||||
|
out.write(" # $source\n")
|
||||||
|
mangas.forEach {
|
||||||
|
out.write(" - ${it.title}\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return file
|
return file
|
||||||
|
@ -2,12 +2,11 @@ package eu.kanade.tachiyomi.data.notification
|
|||||||
|
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
import android.content.ClipData
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Handler
|
import androidx.core.content.ContextCompat
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupRestoreService
|
import eu.kanade.tachiyomi.data.backup.BackupRestoreService
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
@ -25,6 +24,7 @@ import eu.kanade.tachiyomi.util.lang.launchIO
|
|||||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||||
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
||||||
import eu.kanade.tachiyomi.util.system.notificationManager
|
import eu.kanade.tachiyomi.util.system.notificationManager
|
||||||
|
import eu.kanade.tachiyomi.util.system.toShareIntent
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
@ -130,16 +130,8 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
* @param notificationId id of notification
|
* @param notificationId id of notification
|
||||||
*/
|
*/
|
||||||
private fun shareImage(context: Context, path: String, notificationId: Int) {
|
private fun shareImage(context: Context, path: String, notificationId: Int) {
|
||||||
val intent = Intent(Intent.ACTION_SEND).apply {
|
|
||||||
val uri = File(path).getUriCompat(context)
|
|
||||||
putExtra(Intent.EXTRA_STREAM, uri)
|
|
||||||
clipData = ClipData.newRawUri(null, uri)
|
|
||||||
type = "image/*"
|
|
||||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
|
|
||||||
}
|
|
||||||
dismissNotification(context, notificationId)
|
dismissNotification(context, notificationId)
|
||||||
// Launch share activity
|
context.startActivity(File(path).getUriCompat(context).toShareIntent(context))
|
||||||
context.startActivity(intent)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -150,16 +142,8 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
* @param notificationId id of notification
|
* @param notificationId id of notification
|
||||||
*/
|
*/
|
||||||
private fun shareFile(context: Context, uri: Uri, fileMimeType: String, notificationId: Int) {
|
private fun shareFile(context: Context, uri: Uri, fileMimeType: String, notificationId: Int) {
|
||||||
val sendIntent = Intent(Intent.ACTION_SEND).apply {
|
|
||||||
putExtra(Intent.EXTRA_STREAM, uri)
|
|
||||||
clipData = ClipData.newRawUri(null, uri)
|
|
||||||
type = fileMimeType
|
|
||||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
|
|
||||||
}
|
|
||||||
// Dismiss notification
|
|
||||||
dismissNotification(context, notificationId)
|
dismissNotification(context, notificationId)
|
||||||
// Launch share activity
|
context.startActivity(uri.toShareIntent(context, fileMimeType))
|
||||||
context.startActivity(sendIntent)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -208,7 +192,7 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
*/
|
*/
|
||||||
private fun cancelRestore(context: Context, notificationId: Int) {
|
private fun cancelRestore(context: Context, notificationId: Int) {
|
||||||
BackupRestoreService.stop(context)
|
BackupRestoreService.stop(context)
|
||||||
Handler().post { dismissNotification(context, notificationId) }
|
ContextCompat.getMainExecutor(context).execute { dismissNotification(context, notificationId) }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -219,7 +203,7 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
*/
|
*/
|
||||||
private fun cancelLibraryUpdate(context: Context, notificationId: Int) {
|
private fun cancelLibraryUpdate(context: Context, notificationId: Int) {
|
||||||
LibraryUpdateService.stop(context)
|
LibraryUpdateService.stop(context)
|
||||||
Handler().post { dismissNotification(context, notificationId) }
|
ContextCompat.getMainExecutor(context).execute { dismissNotification(context, notificationId) }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -7,15 +7,15 @@ object PreferenceKeys {
|
|||||||
|
|
||||||
const val themeMode = "pref_theme_mode_key"
|
const val themeMode = "pref_theme_mode_key"
|
||||||
|
|
||||||
const val themeLight = "pref_theme_light_key"
|
const val appTheme = "pref_app_theme"
|
||||||
|
|
||||||
const val themeDark = "pref_theme_dark_key"
|
const val themeDarkAmoled = "pref_theme_dark_amoled_key"
|
||||||
|
|
||||||
const val confirmExit = "pref_confirm_exit"
|
const val confirmExit = "pref_confirm_exit"
|
||||||
|
|
||||||
const val hideBottomBarOnScroll = "pref_hide_bottom_bar_on_scroll"
|
const val hideBottomBarOnScroll = "pref_hide_bottom_bar_on_scroll"
|
||||||
|
|
||||||
const val showSideNavOnBottom = "pref_show_side_nav_on_bottom"
|
const val sideNavIconAlignment = "pref_side_nav_icon_alignment"
|
||||||
|
|
||||||
const val enableTransitions = "pref_enable_transitions_key"
|
const val enableTransitions = "pref_enable_transitions_key"
|
||||||
|
|
||||||
@ -99,8 +99,6 @@ object PreferenceKeys {
|
|||||||
|
|
||||||
const val autoUpdateTrack = "pref_auto_update_manga_sync_key"
|
const val autoUpdateTrack = "pref_auto_update_manga_sync_key"
|
||||||
|
|
||||||
const val autoAddTrack = "pref_auto_add_track_key"
|
|
||||||
|
|
||||||
const val lastUsedSource = "last_catalogue_source"
|
const val lastUsedSource = "last_catalogue_source"
|
||||||
|
|
||||||
const val lastUsedCategory = "last_used_category"
|
const val lastUsedCategory = "last_used_category"
|
||||||
@ -147,6 +145,7 @@ object PreferenceKeys {
|
|||||||
const val filterTracked = "pref_filter_library_tracked"
|
const val filterTracked = "pref_filter_library_tracked"
|
||||||
|
|
||||||
const val librarySortingMode = "library_sorting_mode"
|
const val librarySortingMode = "library_sorting_mode"
|
||||||
|
const val librarySortingDirection = "library_sorting_ascending"
|
||||||
|
|
||||||
const val automaticExtUpdates = "automatic_ext_updates"
|
const val automaticExtUpdates = "automatic_ext_updates"
|
||||||
|
|
||||||
@ -185,6 +184,8 @@ object PreferenceKeys {
|
|||||||
|
|
||||||
const val defaultCategory = "default_category"
|
const val defaultCategory = "default_category"
|
||||||
|
|
||||||
|
const val categorizedDisplay = "categorized_display"
|
||||||
|
|
||||||
const val skipRead = "skip_read"
|
const val skipRead = "skip_read"
|
||||||
|
|
||||||
const val skipFiltered = "skip_filtered"
|
const val skipFiltered = "skip_filtered"
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package eu.kanade.tachiyomi.data.preference
|
package eu.kanade.tachiyomi.data.preference
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
|
||||||
const val UNMETERED_NETWORK = "wifi"
|
const val UNMETERED_NETWORK = "wifi"
|
||||||
const val CHARGING = "ac"
|
const val CHARGING = "ac"
|
||||||
|
|
||||||
@ -17,35 +19,28 @@ object PreferenceValues {
|
|||||||
system,
|
system,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keys are lowercase to match legacy string values
|
|
||||||
enum class LightThemeVariant {
|
|
||||||
default,
|
|
||||||
blue,
|
|
||||||
strawberrydaiquiri,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keys are lowercase to match legacy string values
|
|
||||||
enum class DarkThemeVariant {
|
|
||||||
default,
|
|
||||||
blue,
|
|
||||||
greenapple,
|
|
||||||
midnightdusk,
|
|
||||||
amoled,
|
|
||||||
hotpink,
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ktlint-enable experimental:enum-entry-name-case */
|
/* ktlint-enable experimental:enum-entry-name-case */
|
||||||
|
|
||||||
enum class DisplayMode {
|
enum class AppTheme(val titleResId: Int?) {
|
||||||
COMPACT_GRID,
|
DEFAULT(R.string.theme_default),
|
||||||
COMFORTABLE_GRID,
|
MONET(R.string.theme_monet),
|
||||||
LIST,
|
BLUE(R.string.theme_blue),
|
||||||
|
GREEN_APPLE(R.string.theme_greenapple),
|
||||||
|
MIDNIGHT_DUSK(R.string.theme_midnightdusk),
|
||||||
|
STRAWBERRY_DAIQUIRI(R.string.theme_strawberrydaiquiri),
|
||||||
|
TAKO(R.string.theme_tako),
|
||||||
|
YINYANG(R.string.theme_yinyang),
|
||||||
|
YOTSUBA(R.string.theme_yotsuba),
|
||||||
|
|
||||||
|
// Deprecated
|
||||||
|
DARK_BLUE(null),
|
||||||
|
HOT_PINK(null),
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class TappingInvertMode(val shouldInvertHorizontal: Boolean = false, val shouldInvertVertical: Boolean = false) {
|
enum class TappingInvertMode(val shouldInvertHorizontal: Boolean = false, val shouldInvertVertical: Boolean = false) {
|
||||||
NONE,
|
NONE,
|
||||||
HORIZONTAL(shouldInvertHorizontal = true),
|
HORIZONTAL(shouldInvertHorizontal = true),
|
||||||
VERTICAL(shouldInvertVertical = true),
|
VERTICAL(shouldInvertVertical = true),
|
||||||
BOTH(shouldInvertHorizontal = true, shouldInvertVertical = true)
|
BOTH(shouldInvertHorizontal = true, shouldInvertVertical = true),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,9 +9,12 @@ import com.tfcporciuncula.flow.FlowSharedPreferences
|
|||||||
import com.tfcporciuncula.flow.Preference
|
import com.tfcporciuncula.flow.Preference
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues.DisplayMode
|
import eu.kanade.tachiyomi.data.preference.PreferenceValues.ThemeMode.*
|
||||||
import eu.kanade.tachiyomi.data.track.TrackService
|
import eu.kanade.tachiyomi.data.track.TrackService
|
||||||
import eu.kanade.tachiyomi.data.track.anilist.Anilist
|
import eu.kanade.tachiyomi.data.track.anilist.Anilist
|
||||||
|
import eu.kanade.tachiyomi.ui.library.setting.DisplayModeSetting
|
||||||
|
import eu.kanade.tachiyomi.ui.library.setting.SortDirectionSetting
|
||||||
|
import eu.kanade.tachiyomi.ui.library.setting.SortModeSetting
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
|
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
|
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
|
||||||
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
|
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
|
||||||
@ -66,7 +69,7 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun hideBottomBarOnScroll() = flowPrefs.getBoolean(Keys.hideBottomBarOnScroll, true)
|
fun hideBottomBarOnScroll() = flowPrefs.getBoolean(Keys.hideBottomBarOnScroll, true)
|
||||||
|
|
||||||
fun showSideNavOnBottom() = flowPrefs.getBoolean(Keys.showSideNavOnBottom, false)
|
fun sideNavIconAlignment() = flowPrefs.getInt(Keys.sideNavIconAlignment, 0)
|
||||||
|
|
||||||
fun useAuthenticator() = flowPrefs.getBoolean(Keys.useAuthenticator, false)
|
fun useAuthenticator() = flowPrefs.getBoolean(Keys.useAuthenticator, false)
|
||||||
|
|
||||||
@ -82,13 +85,13 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun autoUpdateTrackers() = prefs.getBoolean(Keys.autoUpdateTrackers, false)
|
fun autoUpdateTrackers() = prefs.getBoolean(Keys.autoUpdateTrackers, false)
|
||||||
|
|
||||||
fun showLibraryUpdateErrors() = prefs.getBoolean(Keys.showLibraryUpdateErrors, false)
|
fun showLibraryUpdateErrors() = prefs.getBoolean(Keys.showLibraryUpdateErrors, true)
|
||||||
|
|
||||||
fun themeMode() = flowPrefs.getEnum(Keys.themeMode, Values.ThemeMode.system)
|
fun themeMode() = flowPrefs.getEnum(Keys.themeMode, system)
|
||||||
|
|
||||||
fun themeLight() = flowPrefs.getEnum(Keys.themeLight, Values.LightThemeVariant.default)
|
fun appTheme() = flowPrefs.getEnum(Keys.appTheme, Values.AppTheme.DEFAULT)
|
||||||
|
|
||||||
fun themeDark() = flowPrefs.getEnum(Keys.themeDark, Values.DarkThemeVariant.default)
|
fun themeDarkAmoled() = flowPrefs.getBoolean(Keys.themeDarkAmoled, false)
|
||||||
|
|
||||||
fun pageTransitions() = flowPrefs.getBoolean(Keys.enableTransitions, true)
|
fun pageTransitions() = flowPrefs.getBoolean(Keys.enableTransitions, true)
|
||||||
|
|
||||||
@ -174,15 +177,13 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun autoUpdateTrack() = prefs.getBoolean(Keys.autoUpdateTrack, true)
|
fun autoUpdateTrack() = prefs.getBoolean(Keys.autoUpdateTrack, true)
|
||||||
|
|
||||||
fun autoAddTrack() = prefs.getBoolean(Keys.autoAddTrack, true)
|
|
||||||
|
|
||||||
fun lastUsedSource() = flowPrefs.getLong(Keys.lastUsedSource, -1)
|
fun lastUsedSource() = flowPrefs.getLong(Keys.lastUsedSource, -1)
|
||||||
|
|
||||||
fun lastUsedCategory() = flowPrefs.getInt(Keys.lastUsedCategory, 0)
|
fun lastUsedCategory() = flowPrefs.getInt(Keys.lastUsedCategory, 0)
|
||||||
|
|
||||||
fun lastVersionCode() = flowPrefs.getInt("last_version_code", 0)
|
fun lastVersionCode() = flowPrefs.getInt("last_version_code", 0)
|
||||||
|
|
||||||
fun sourceDisplayMode() = flowPrefs.getEnum(Keys.sourceDisplayMode, DisplayMode.COMPACT_GRID)
|
fun sourceDisplayMode() = flowPrefs.getEnum(Keys.sourceDisplayMode, DisplayModeSetting.COMPACT_GRID)
|
||||||
|
|
||||||
fun enabledLanguages() = flowPrefs.getStringSet(Keys.enabledLanguages, setOf("en", Locale.getDefault().language))
|
fun enabledLanguages() = flowPrefs.getStringSet(Keys.enabledLanguages, setOf("en", Locale.getDefault().language))
|
||||||
|
|
||||||
@ -233,7 +234,7 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun libraryUpdatePrioritization() = flowPrefs.getInt(Keys.libraryUpdatePrioritization, 0)
|
fun libraryUpdatePrioritization() = flowPrefs.getInt(Keys.libraryUpdatePrioritization, 0)
|
||||||
|
|
||||||
fun libraryDisplayMode() = flowPrefs.getEnum(Keys.libraryDisplayMode, DisplayMode.COMPACT_GRID)
|
fun libraryDisplayMode() = flowPrefs.getEnum(Keys.libraryDisplayMode, DisplayModeSetting.COMPACT_GRID)
|
||||||
|
|
||||||
fun downloadBadge() = flowPrefs.getBoolean(Keys.downloadBadge, false)
|
fun downloadBadge() = flowPrefs.getBoolean(Keys.downloadBadge, false)
|
||||||
|
|
||||||
@ -255,9 +256,8 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun filterTracking(name: Int) = flowPrefs.getInt("${Keys.filterTracked}_$name", ExtendedNavigationView.Item.TriStateGroup.State.IGNORE.value)
|
fun filterTracking(name: Int) = flowPrefs.getInt("${Keys.filterTracked}_$name", ExtendedNavigationView.Item.TriStateGroup.State.IGNORE.value)
|
||||||
|
|
||||||
fun librarySortingMode() = flowPrefs.getInt(Keys.librarySortingMode, 0)
|
fun librarySortingMode() = flowPrefs.getEnum(Keys.librarySortingMode, SortModeSetting.ALPHABETICAL)
|
||||||
|
fun librarySortingAscending() = flowPrefs.getEnum(Keys.librarySortingDirection, SortDirectionSetting.ASCENDING)
|
||||||
fun librarySortingAscending() = flowPrefs.getBoolean("library_sorting_ascending", true)
|
|
||||||
|
|
||||||
fun automaticExtUpdates() = flowPrefs.getBoolean(Keys.automaticExtUpdates, true)
|
fun automaticExtUpdates() = flowPrefs.getBoolean(Keys.automaticExtUpdates, true)
|
||||||
|
|
||||||
@ -280,10 +280,12 @@ class PreferencesHelper(val context: Context) {
|
|||||||
fun downloadNewCategories() = flowPrefs.getStringSet(Keys.downloadNewCategories, emptySet())
|
fun downloadNewCategories() = flowPrefs.getStringSet(Keys.downloadNewCategories, emptySet())
|
||||||
fun downloadNewCategoriesExclude() = flowPrefs.getStringSet(Keys.downloadNewCategoriesExclude, emptySet())
|
fun downloadNewCategoriesExclude() = flowPrefs.getStringSet(Keys.downloadNewCategoriesExclude, emptySet())
|
||||||
|
|
||||||
fun lang() = prefs.getString(Keys.lang, "")
|
fun lang() = flowPrefs.getString(Keys.lang, "")
|
||||||
|
|
||||||
fun defaultCategory() = prefs.getInt(Keys.defaultCategory, -1)
|
fun defaultCategory() = prefs.getInt(Keys.defaultCategory, -1)
|
||||||
|
|
||||||
|
fun categorisedDisplaySettings() = flowPrefs.getBoolean(Keys.categorizedDisplay, false)
|
||||||
|
|
||||||
fun skipRead() = prefs.getBoolean(Keys.skipRead, false)
|
fun skipRead() = prefs.getBoolean(Keys.skipRead, false)
|
||||||
|
|
||||||
fun skipFiltered() = prefs.getBoolean(Keys.skipFiltered, true)
|
fun skipFiltered() = prefs.getBoolean(Keys.skipFiltered, true)
|
||||||
|
@ -5,14 +5,21 @@ import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
|||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An Unattended Track Service will never prompt the user to match a manga with the remote.
|
* An Enhanced Track Service will never prompt the user to match a manga with the remote.
|
||||||
* It is expected that such Track Sercice can only work with specific sources and unique IDs.
|
* It is expected that such Track Service can only work with specific sources and unique IDs.
|
||||||
*/
|
*/
|
||||||
interface UnattendedTrackService {
|
interface EnhancedTrackService {
|
||||||
/**
|
/**
|
||||||
* This TrackService will only work with the sources that are accepted by this filter function.
|
* This TrackService will only work with the sources that are accepted by this filter function.
|
||||||
*/
|
*/
|
||||||
fun accept(source: Source): Boolean
|
fun accept(source: Source): Boolean {
|
||||||
|
return source::class.qualifiedName in getAcceptedSources()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fully qualified source classes that this track service is compatible with.
|
||||||
|
*/
|
||||||
|
fun getAcceptedSources(): List<String>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* match is similar to TrackService.search, but only return zero or one match.
|
* match is similar to TrackService.search, but only return zero or one match.
|
@ -36,6 +36,10 @@ abstract class TrackService(val id: Int) {
|
|||||||
|
|
||||||
abstract fun getStatus(status: Int): String
|
abstract fun getStatus(status: Int): String
|
||||||
|
|
||||||
|
abstract fun getReadingStatus(): Int
|
||||||
|
|
||||||
|
abstract fun getRereadingStatus(): Int
|
||||||
|
|
||||||
abstract fun getCompletionStatus(): Int
|
abstract fun getCompletionStatus(): Int
|
||||||
|
|
||||||
abstract fun getScoreList(): List<String>
|
abstract fun getScoreList(): List<String>
|
||||||
@ -46,9 +50,9 @@ abstract class TrackService(val id: Int) {
|
|||||||
|
|
||||||
abstract fun displayScore(track: Track): String
|
abstract fun displayScore(track: Track): String
|
||||||
|
|
||||||
abstract suspend fun update(track: Track): Track
|
abstract suspend fun update(track: Track, didReadChapter: Boolean = false): Track
|
||||||
|
|
||||||
abstract suspend fun bind(track: Track): Track
|
abstract suspend fun bind(track: Track, hasReadChapters: Boolean = false): Track
|
||||||
|
|
||||||
abstract suspend fun search(query: String): List<TrackSearch>
|
abstract suspend fun search(query: String): List<TrackSearch>
|
||||||
|
|
||||||
|
@ -72,6 +72,10 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getReadingStatus(): Int = READING
|
||||||
|
|
||||||
|
override fun getRereadingStatus(): Int = REPEATING
|
||||||
|
|
||||||
override fun getCompletionStatus(): Int = COMPLETED
|
override fun getCompletionStatus(): Int = COMPLETED
|
||||||
|
|
||||||
override fun getScoreList(): List<String> {
|
override fun getScoreList(): List<String> {
|
||||||
@ -134,7 +138,7 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
return api.addLibManga(track)
|
return api.addLibManga(track)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun update(track: Track): Track {
|
override suspend fun update(track: Track, didReadChapter: Boolean): Track {
|
||||||
// If user was using API v1 fetch library_id
|
// If user was using API v1 fetch library_id
|
||||||
if (track.library_id == null || track.library_id!! == 0L) {
|
if (track.library_id == null || track.library_id!! == 0L) {
|
||||||
val libManga = api.findLibManga(track, getUsername().toInt())
|
val libManga = api.findLibManga(track, getUsername().toInt())
|
||||||
@ -142,18 +146,30 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
track.library_id = libManga.library_id
|
track.library_id = libManga.library_id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (track.status != COMPLETED) {
|
||||||
|
if (track.status != REPEATING && didReadChapter) {
|
||||||
|
track.status = READING
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return api.updateLibManga(track)
|
return api.updateLibManga(track)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun bind(track: Track): Track {
|
override suspend fun bind(track: Track, hasReadChapters: Boolean): Track {
|
||||||
val remoteTrack = api.findLibManga(track, getUsername().toInt())
|
val remoteTrack = api.findLibManga(track, getUsername().toInt())
|
||||||
return if (remoteTrack != null) {
|
return if (remoteTrack != null) {
|
||||||
track.copyPersonalFrom(remoteTrack)
|
track.copyPersonalFrom(remoteTrack)
|
||||||
track.library_id = remoteTrack.library_id
|
track.library_id = remoteTrack.library_id
|
||||||
|
|
||||||
|
if (track.status != COMPLETED) {
|
||||||
|
val isRereading = track.status == REPEATING
|
||||||
|
track.status = if (isRereading.not() && hasReadChapters) READING else track.status
|
||||||
|
}
|
||||||
|
|
||||||
update(track)
|
update(track)
|
||||||
} else {
|
} else {
|
||||||
// Set default fields if it's not found in the list
|
// Set default fields if it's not found in the list
|
||||||
track.status = READING
|
track.status = if (hasReadChapters) READING else PLANNING
|
||||||
track.score = 0F
|
track.score = 0F
|
||||||
add(track)
|
add(track)
|
||||||
}
|
}
|
||||||
|
@ -2,9 +2,6 @@ package eu.kanade.tachiyomi.data.track.anilist
|
|||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import com.afollestad.date.dayOfMonth
|
|
||||||
import com.afollestad.date.month
|
|
||||||
import com.afollestad.date.year
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
import eu.kanade.tachiyomi.network.POST
|
import eu.kanade.tachiyomi.network.POST
|
||||||
@ -315,9 +312,9 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
|||||||
val calendar = Calendar.getInstance()
|
val calendar = Calendar.getInstance()
|
||||||
calendar.timeInMillis = dateValue
|
calendar.timeInMillis = dateValue
|
||||||
return buildJsonObject {
|
return buildJsonObject {
|
||||||
put("year", calendar.year)
|
put("year", calendar.get(Calendar.YEAR))
|
||||||
put("month", calendar.month + 1)
|
put("month", calendar.get(Calendar.MONTH) + 1)
|
||||||
put("day", calendar.dayOfMonth)
|
put("day", calendar.get(Calendar.DAY_OF_MONTH))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,24 +35,34 @@ class Bangumi(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
return api.addLibManga(track)
|
return api.addLibManga(track)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun update(track: Track): Track {
|
override suspend fun update(track: Track, didReadChapter: Boolean): Track {
|
||||||
|
if (track.status != COMPLETED) {
|
||||||
|
if (didReadChapter) {
|
||||||
|
track.status = READING
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return api.updateLibManga(track)
|
return api.updateLibManga(track)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun bind(track: Track): Track {
|
override suspend fun bind(track: Track, hasReadChapters: Boolean): Track {
|
||||||
val statusTrack = api.statusLibManga(track)
|
val statusTrack = api.statusLibManga(track)
|
||||||
val remoteTrack = api.findLibManga(track)
|
val remoteTrack = api.findLibManga(track)
|
||||||
return if (remoteTrack != null && statusTrack != null) {
|
return if (remoteTrack != null && statusTrack != null) {
|
||||||
track.copyPersonalFrom(remoteTrack)
|
track.copyPersonalFrom(remoteTrack)
|
||||||
track.library_id = remoteTrack.library_id
|
track.library_id = remoteTrack.library_id
|
||||||
track.status = statusTrack.status
|
|
||||||
|
if (track.status != COMPLETED) {
|
||||||
|
track.status = if (hasReadChapters) READING else statusTrack.status
|
||||||
|
}
|
||||||
|
|
||||||
track.score = statusTrack.score
|
track.score = statusTrack.score
|
||||||
track.last_chapter_read = statusTrack.last_chapter_read
|
track.last_chapter_read = statusTrack.last_chapter_read
|
||||||
track.total_chapters = remoteTrack.total_chapters
|
track.total_chapters = remoteTrack.total_chapters
|
||||||
refresh(track)
|
refresh(track)
|
||||||
} else {
|
} else {
|
||||||
// Set default fields if it's not found in the list
|
// Set default fields if it's not found in the list
|
||||||
track.status = READING
|
track.status = if (hasReadChapters) READING else PLANNING
|
||||||
track.score = 0F
|
track.score = 0F
|
||||||
add(track)
|
add(track)
|
||||||
update(track)
|
update(track)
|
||||||
@ -91,6 +101,10 @@ class Bangumi(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getReadingStatus(): Int = READING
|
||||||
|
|
||||||
|
override fun getRereadingStatus(): Int = -1
|
||||||
|
|
||||||
override fun getCompletionStatus(): Int = COMPLETED
|
override fun getCompletionStatus(): Int = COMPLETED
|
||||||
|
|
||||||
override suspend fun login(username: String, password: String) = login(password)
|
override suspend fun login(username: String, password: String) = login(password)
|
||||||
|
@ -26,6 +26,8 @@ class Kitsu(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
@StringRes
|
@StringRes
|
||||||
override fun nameRes() = R.string.tracker_kitsu
|
override fun nameRes() = R.string.tracker_kitsu
|
||||||
|
|
||||||
|
override val supportsReadingDates: Boolean = true
|
||||||
|
|
||||||
private val json: Json by injectLazy()
|
private val json: Json by injectLazy()
|
||||||
|
|
||||||
private val interceptor by lazy { KitsuInterceptor(this) }
|
private val interceptor by lazy { KitsuInterceptor(this) }
|
||||||
@ -51,6 +53,10 @@ class Kitsu(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getReadingStatus(): Int = READING
|
||||||
|
|
||||||
|
override fun getRereadingStatus(): Int = -1
|
||||||
|
|
||||||
override fun getCompletionStatus(): Int = COMPLETED
|
override fun getCompletionStatus(): Int = COMPLETED
|
||||||
|
|
||||||
override fun getScoreList(): List<String> {
|
override fun getScoreList(): List<String> {
|
||||||
@ -71,18 +77,29 @@ class Kitsu(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
return api.addLibManga(track, getUserId())
|
return api.addLibManga(track, getUserId())
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun update(track: Track): Track {
|
override suspend fun update(track: Track, didReadChapter: Boolean): Track {
|
||||||
|
if (track.status != COMPLETED) {
|
||||||
|
if (didReadChapter) {
|
||||||
|
track.status = READING
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return api.updateLibManga(track)
|
return api.updateLibManga(track)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun bind(track: Track): Track {
|
override suspend fun bind(track: Track, hasReadChapters: Boolean): Track {
|
||||||
val remoteTrack = api.findLibManga(track, getUserId())
|
val remoteTrack = api.findLibManga(track, getUserId())
|
||||||
return if (remoteTrack != null) {
|
return if (remoteTrack != null) {
|
||||||
track.copyPersonalFrom(remoteTrack)
|
track.copyPersonalFrom(remoteTrack)
|
||||||
track.media_id = remoteTrack.media_id
|
track.media_id = remoteTrack.media_id
|
||||||
|
|
||||||
|
if (track.status != COMPLETED) {
|
||||||
|
track.status = if (hasReadChapters) READING else track.status
|
||||||
|
}
|
||||||
|
|
||||||
update(track)
|
update(track)
|
||||||
} else {
|
} else {
|
||||||
track.status = READING
|
track.status = if (hasReadChapters) READING else PLAN_TO_READ
|
||||||
track.score = 0F
|
track.score = 0F
|
||||||
add(track)
|
add(track)
|
||||||
}
|
}
|
||||||
|
@ -84,6 +84,8 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
|
|||||||
put("status", track.toKitsuStatus())
|
put("status", track.toKitsuStatus())
|
||||||
put("progress", track.last_chapter_read)
|
put("progress", track.last_chapter_read)
|
||||||
put("ratingTwenty", track.toKitsuScore())
|
put("ratingTwenty", track.toKitsuScore())
|
||||||
|
put("startedAt", KitsuDateHelper.convert(track.started_reading_date))
|
||||||
|
put("finishedAt", KitsuDateHelper.convert(track.finished_reading_date))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,25 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.track.kitsu
|
||||||
|
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
object KitsuDateHelper {
|
||||||
|
|
||||||
|
private const val pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"
|
||||||
|
private val formatter = SimpleDateFormat(pattern, Locale.ENGLISH)
|
||||||
|
|
||||||
|
fun convert(dateValue: Long): String? {
|
||||||
|
if (dateValue == 0L) return null
|
||||||
|
|
||||||
|
return formatter.format(Date(dateValue))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun parse(dateString: String?): Long {
|
||||||
|
if (dateString == null) return 0L
|
||||||
|
|
||||||
|
val dateValue = formatter.parse(dateString)
|
||||||
|
|
||||||
|
return dateValue?.time ?: return 0
|
||||||
|
}
|
||||||
|
}
|
@ -58,6 +58,8 @@ class KitsuLibManga(obj: JsonObject, manga: JsonObject) {
|
|||||||
val original = manga["attributes"]!!.jsonObject["posterImage"]!!.jsonObject["original"]!!.jsonPrimitive.content
|
val original = manga["attributes"]!!.jsonObject["posterImage"]!!.jsonObject["original"]!!.jsonPrimitive.content
|
||||||
private val synopsis = manga["attributes"]!!.jsonObject["synopsis"]!!.jsonPrimitive.content
|
private val synopsis = manga["attributes"]!!.jsonObject["synopsis"]!!.jsonPrimitive.content
|
||||||
private val startDate = manga["attributes"]!!.jsonObject["startDate"]?.jsonPrimitive?.contentOrNull.orEmpty()
|
private val startDate = manga["attributes"]!!.jsonObject["startDate"]?.jsonPrimitive?.contentOrNull.orEmpty()
|
||||||
|
private val startedAt = obj["attributes"]!!.jsonObject["startedAt"]?.jsonPrimitive?.contentOrNull
|
||||||
|
private val finishedAt = obj["attributes"]!!.jsonObject["finishedAt"]?.jsonPrimitive?.contentOrNull
|
||||||
private val libraryId = obj["id"]!!.jsonPrimitive.int
|
private val libraryId = obj["id"]!!.jsonPrimitive.int
|
||||||
val status = obj["attributes"]!!.jsonObject["status"]!!.jsonPrimitive.content
|
val status = obj["attributes"]!!.jsonObject["status"]!!.jsonPrimitive.content
|
||||||
private val ratingTwenty = obj["attributes"]!!.jsonObject["ratingTwenty"]?.jsonPrimitive?.contentOrNull
|
private val ratingTwenty = obj["attributes"]!!.jsonObject["ratingTwenty"]?.jsonPrimitive?.contentOrNull
|
||||||
@ -73,6 +75,8 @@ class KitsuLibManga(obj: JsonObject, manga: JsonObject) {
|
|||||||
publishing_status = this@KitsuLibManga.status
|
publishing_status = this@KitsuLibManga.status
|
||||||
publishing_type = type
|
publishing_type = type
|
||||||
start_date = startDate
|
start_date = startDate
|
||||||
|
started_reading_date = KitsuDateHelper.parse(startedAt)
|
||||||
|
finished_reading_date = KitsuDateHelper.parse(finishedAt)
|
||||||
status = toTrackStatus()
|
status = toTrackStatus()
|
||||||
score = ratingTwenty?.let { it.toInt() / 2f } ?: 0f
|
score = ratingTwenty?.let { it.toInt() / 2f } ?: 0f
|
||||||
last_chapter_read = progress
|
last_chapter_read = progress
|
||||||
|
@ -6,22 +6,19 @@ import androidx.annotation.StringRes
|
|||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
|
import eu.kanade.tachiyomi.data.track.EnhancedTrackService
|
||||||
import eu.kanade.tachiyomi.data.track.NoLoginTrackService
|
import eu.kanade.tachiyomi.data.track.NoLoginTrackService
|
||||||
import eu.kanade.tachiyomi.data.track.TrackService
|
import eu.kanade.tachiyomi.data.track.TrackService
|
||||||
import eu.kanade.tachiyomi.data.track.UnattendedTrackService
|
|
||||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
import eu.kanade.tachiyomi.source.Source
|
|
||||||
import okhttp3.Dns
|
import okhttp3.Dns
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
|
|
||||||
class Komga(private val context: Context, id: Int) : TrackService(id), UnattendedTrackService, NoLoginTrackService {
|
class Komga(private val context: Context, id: Int) : TrackService(id), EnhancedTrackService, NoLoginTrackService {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val UNREAD = 1
|
const val UNREAD = 1
|
||||||
const val READING = 2
|
const val READING = 2
|
||||||
const val COMPLETED = 3
|
const val COMPLETED = 3
|
||||||
|
|
||||||
const val ACCEPTED_SOURCE = "eu.kanade.tachiyomi.extension.all.komga.Komga"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override val client: OkHttpClient =
|
override val client: OkHttpClient =
|
||||||
@ -49,17 +46,27 @@ class Komga(private val context: Context, id: Int) : TrackService(id), Unattende
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getReadingStatus(): Int = READING
|
||||||
|
|
||||||
|
override fun getRereadingStatus(): Int = -1
|
||||||
|
|
||||||
override fun getCompletionStatus(): Int = COMPLETED
|
override fun getCompletionStatus(): Int = COMPLETED
|
||||||
|
|
||||||
override fun getScoreList(): List<String> = emptyList()
|
override fun getScoreList(): List<String> = emptyList()
|
||||||
|
|
||||||
override fun displayScore(track: Track): String = ""
|
override fun displayScore(track: Track): String = ""
|
||||||
|
|
||||||
override suspend fun update(track: Track): Track {
|
override suspend fun update(track: Track, didReadChapter: Boolean): Track {
|
||||||
|
if (track.status != COMPLETED) {
|
||||||
|
if (didReadChapter) {
|
||||||
|
track.status = READING
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return api.updateProgress(track)
|
return api.updateProgress(track)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun bind(track: Track): Track {
|
override suspend fun bind(track: Track, hasReadChapters: Boolean): Track {
|
||||||
return track
|
return track
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,7 +75,7 @@ class Komga(private val context: Context, id: Int) : TrackService(id), Unattende
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun refresh(track: Track): Track {
|
override suspend fun refresh(track: Track): Track {
|
||||||
val remoteTrack = api.getTrackSearch(track.tracking_url)!!
|
val remoteTrack = api.getTrackSearch(track.tracking_url)
|
||||||
track.copyPersonalFrom(remoteTrack)
|
track.copyPersonalFrom(remoteTrack)
|
||||||
track.total_chapters = remoteTrack.total_chapters
|
track.total_chapters = remoteTrack.total_chapters
|
||||||
return track
|
return track
|
||||||
@ -84,7 +91,7 @@ class Komga(private val context: Context, id: Int) : TrackService(id), Unattende
|
|||||||
saveCredentials("user", "pass")
|
saveCredentials("user", "pass")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun accept(source: Source): Boolean = source::class.qualifiedName == ACCEPTED_SOURCE
|
override fun getAcceptedSources() = listOf("eu.kanade.tachiyomi.extension.all.komga.Komga")
|
||||||
|
|
||||||
override suspend fun match(manga: Manga): TrackSearch? =
|
override suspend fun match(manga: Manga): TrackSearch? =
|
||||||
try {
|
try {
|
||||||
|
@ -56,6 +56,10 @@ class MyAnimeList(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getReadingStatus(): Int = READING
|
||||||
|
|
||||||
|
override fun getRereadingStatus(): Int = REREADING
|
||||||
|
|
||||||
override fun getCompletionStatus(): Int = COMPLETED
|
override fun getCompletionStatus(): Int = COMPLETED
|
||||||
|
|
||||||
override fun getScoreList(): List<String> {
|
override fun getScoreList(): List<String> {
|
||||||
@ -67,22 +71,35 @@ class MyAnimeList(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun add(track: Track): Track {
|
private suspend fun add(track: Track): Track {
|
||||||
track.status = READING
|
|
||||||
track.score = 0F
|
|
||||||
return api.updateItem(track)
|
return api.updateItem(track)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun update(track: Track): Track {
|
override suspend fun update(track: Track, didReadChapter: Boolean): Track {
|
||||||
|
if (track.status != COMPLETED) {
|
||||||
|
if (track.status != REREADING && didReadChapter) {
|
||||||
|
track.status = READING
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return api.updateItem(track)
|
return api.updateItem(track)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun bind(track: Track): Track {
|
override suspend fun bind(track: Track, hasReadChapters: Boolean): Track {
|
||||||
val remoteTrack = api.findListItem(track)
|
val remoteTrack = api.findListItem(track)
|
||||||
return if (remoteTrack != null) {
|
return if (remoteTrack != null) {
|
||||||
track.copyPersonalFrom(remoteTrack)
|
track.copyPersonalFrom(remoteTrack)
|
||||||
track.media_id = remoteTrack.media_id
|
track.media_id = remoteTrack.media_id
|
||||||
|
|
||||||
|
if (track.status != COMPLETED) {
|
||||||
|
val isRereading = track.status == REREADING
|
||||||
|
track.status = if (isRereading.not() && hasReadChapters) READING else track.status
|
||||||
|
}
|
||||||
|
|
||||||
update(track)
|
update(track)
|
||||||
} else {
|
} else {
|
||||||
|
// Set default fields if it's not found in the list
|
||||||
|
track.status = if (hasReadChapters) READING else PLAN_TO_READ
|
||||||
|
track.score = 0F
|
||||||
add(track)
|
add(track)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -44,19 +44,31 @@ class Shikimori(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
return api.addLibManga(track, getUsername())
|
return api.addLibManga(track, getUsername())
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun update(track: Track): Track {
|
override suspend fun update(track: Track, didReadChapter: Boolean): Track {
|
||||||
|
if (track.status != COMPLETED) {
|
||||||
|
if (track.status != REPEATING && didReadChapter) {
|
||||||
|
track.status = READING
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return api.updateLibManga(track, getUsername())
|
return api.updateLibManga(track, getUsername())
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun bind(track: Track): Track {
|
override suspend fun bind(track: Track, hasReadChapters: Boolean): Track {
|
||||||
val remoteTrack = api.findLibManga(track, getUsername())
|
val remoteTrack = api.findLibManga(track, getUsername())
|
||||||
return if (remoteTrack != null) {
|
return if (remoteTrack != null) {
|
||||||
track.copyPersonalFrom(remoteTrack)
|
track.copyPersonalFrom(remoteTrack)
|
||||||
track.library_id = remoteTrack.library_id
|
track.library_id = remoteTrack.library_id
|
||||||
|
|
||||||
|
if (track.status != COMPLETED) {
|
||||||
|
val isRereading = track.status == REPEATING
|
||||||
|
track.status = if (isRereading.not() && hasReadChapters) READING else track.status
|
||||||
|
}
|
||||||
|
|
||||||
update(track)
|
update(track)
|
||||||
} else {
|
} else {
|
||||||
// Set default fields if it's not found in the list
|
// Set default fields if it's not found in the list
|
||||||
track.status = READING
|
track.status = if (hasReadChapters) READING else PLANNING
|
||||||
track.score = 0F
|
track.score = 0F
|
||||||
add(track)
|
add(track)
|
||||||
}
|
}
|
||||||
@ -94,6 +106,10 @@ class Shikimori(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getReadingStatus(): Int = READING
|
||||||
|
|
||||||
|
override fun getRereadingStatus(): Int = REPEATING
|
||||||
|
|
||||||
override fun getCompletionStatus(): Int = COMPLETED
|
override fun getCompletionStatus(): Int = COMPLETED
|
||||||
|
|
||||||
override suspend fun login(username: String, password: String) = login(password)
|
override suspend fun login(username: String, password: String) = login(password)
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.data.updater.github
|
package eu.kanade.tachiyomi.data.updater
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.data.updater.Release
|
import android.os.Build
|
||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@ -15,16 +15,25 @@ import kotlinx.serialization.Serializable
|
|||||||
@Serializable
|
@Serializable
|
||||||
class GithubRelease(
|
class GithubRelease(
|
||||||
@SerialName("tag_name") val version: String,
|
@SerialName("tag_name") val version: String,
|
||||||
@SerialName("body") override val info: String,
|
@SerialName("body") val info: String,
|
||||||
@SerialName("assets") private val assets: List<Assets>
|
@SerialName("assets") private val assets: List<Assets>
|
||||||
) : Release {
|
) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get download link of latest release from the assets.
|
* Get download link of latest release from the assets.
|
||||||
* @return download link of latest release.
|
* @return download link of latest release.
|
||||||
*/
|
*/
|
||||||
override val downloadLink: String
|
fun getDownloadLink(): String {
|
||||||
get() = assets[0].downloadLink
|
val apkVariant = when (Build.SUPPORTED_ABIS[0]) {
|
||||||
|
"arm64-v8a" -> "-arm64-v8a"
|
||||||
|
"armeabi-v7a" -> "-armeabi-v7a"
|
||||||
|
"x86", "x86_64" -> "-x86"
|
||||||
|
else -> ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return assets.find { it.downloadLink.contains("tachiyomi$apkVariant-") }?.downloadLink
|
||||||
|
?: assets[0].downloadLink
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Assets class containing download url.
|
* Assets class containing download url.
|
@ -1,7 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.data.updater.github
|
package eu.kanade.tachiyomi.data.updater
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.BuildConfig
|
import eu.kanade.tachiyomi.BuildConfig
|
||||||
import eu.kanade.tachiyomi.data.updater.UpdateResult
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
import eu.kanade.tachiyomi.network.await
|
import eu.kanade.tachiyomi.network.await
|
||||||
@ -21,7 +20,7 @@ class GithubUpdateChecker {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun checkForUpdate(): UpdateResult {
|
suspend fun checkForUpdate(): GithubUpdateResult {
|
||||||
return withIOContext {
|
return withIOContext {
|
||||||
networkService.client
|
networkService.client
|
||||||
.newCall(GET("https://api.github.com/repos/$repo/releases/latest"))
|
.newCall(GET("https://api.github.com/repos/$repo/releases/latest"))
|
||||||
@ -32,7 +31,7 @@ class GithubUpdateChecker {
|
|||||||
if (isNewVersion(it.version)) {
|
if (isNewVersion(it.version)) {
|
||||||
GithubUpdateResult.NewUpdate(it)
|
GithubUpdateResult.NewUpdate(it)
|
||||||
} else {
|
} else {
|
||||||
GithubUpdateResult.NoNewUpdate()
|
GithubUpdateResult.NoNewUpdate
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.updater
|
||||||
|
|
||||||
|
sealed class GithubUpdateResult {
|
||||||
|
class NewUpdate(val release: GithubRelease) : GithubUpdateResult()
|
||||||
|
object NoNewUpdate : GithubUpdateResult()
|
||||||
|
}
|
@ -1,12 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.updater
|
|
||||||
|
|
||||||
interface Release {
|
|
||||||
|
|
||||||
val info: String
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get download link of latest release.
|
|
||||||
* @return download link of latest release.
|
|
||||||
*/
|
|
||||||
val downloadLink: String
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.updater
|
|
||||||
|
|
||||||
abstract class UpdateResult {
|
|
||||||
|
|
||||||
open class NewUpdate<T : Release>(val release: T) : UpdateResult()
|
|
||||||
open class NoNewUpdate : UpdateResult()
|
|
||||||
}
|
|
@ -8,7 +8,6 @@ import androidx.work.PeriodicWorkRequestBuilder
|
|||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
import androidx.work.Worker
|
import androidx.work.Worker
|
||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
import eu.kanade.tachiyomi.data.updater.github.GithubUpdateChecker
|
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
@ -19,8 +18,8 @@ class UpdaterJob(private val context: Context, workerParams: WorkerParameters) :
|
|||||||
try {
|
try {
|
||||||
val result = GithubUpdateChecker().checkForUpdate()
|
val result = GithubUpdateChecker().checkForUpdate()
|
||||||
|
|
||||||
if (result is UpdateResult.NewUpdate<*>) {
|
if (result is GithubUpdateResult.NewUpdate) {
|
||||||
UpdaterNotifier(context).promptUpdate(result.release.downloadLink)
|
UpdaterNotifier(context).promptUpdate(result.release.getDownloadLink())
|
||||||
}
|
}
|
||||||
Result.success()
|
Result.success()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
@ -1,9 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.updater.github
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.data.updater.UpdateResult
|
|
||||||
|
|
||||||
sealed class GithubUpdateResult : UpdateResult() {
|
|
||||||
|
|
||||||
class NewUpdate(release: GithubRelease) : UpdateResult.NewUpdate<GithubRelease>(release)
|
|
||||||
class NoNewUpdate : UpdateResult.NoNewUpdate()
|
|
||||||
}
|
|
@ -79,9 +79,6 @@ internal class ExtensionGithubApi {
|
|||||||
fun getApkUrl(extension: Extension.Available): String {
|
fun getApkUrl(extension: Extension.Available): String {
|
||||||
return "${REPO_URL_PREFIX}apk/${extension.apkName}"
|
return "${REPO_URL_PREFIX}apk/${extension.apkName}"
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val BASE_URL = "https://raw.githubusercontent.com/"
|
|
||||||
const val REPO_URL_PREFIX = "${BASE_URL}tachiyomiorg/tachiyomi-extensions/repo/"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private const val REPO_URL_PREFIX = "https://raw.githubusercontent.com/tachiyomiorg/tachiyomi-extensions/repo/"
|
||||||
|
@ -2,11 +2,10 @@ package eu.kanade.tachiyomi.network.interceptor
|
|||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Handler
|
|
||||||
import android.os.Looper
|
|
||||||
import android.webkit.WebSettings
|
import android.webkit.WebSettings
|
||||||
import android.webkit.WebView
|
import android.webkit.WebView
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
@ -28,7 +27,7 @@ import java.util.concurrent.TimeUnit
|
|||||||
|
|
||||||
class CloudflareInterceptor(private val context: Context) : Interceptor {
|
class CloudflareInterceptor(private val context: Context) : Interceptor {
|
||||||
|
|
||||||
private val handler = Handler(Looper.getMainLooper())
|
private val executor = ContextCompat.getMainExecutor(context)
|
||||||
|
|
||||||
private val networkHelper: NetworkHelper by injectLazy()
|
private val networkHelper: NetworkHelper by injectLazy()
|
||||||
|
|
||||||
@ -92,7 +91,7 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
|
|||||||
val headers = request.headers.toMultimap().mapValues { it.value.getOrNull(0) ?: "" }.toMutableMap()
|
val headers = request.headers.toMultimap().mapValues { it.value.getOrNull(0) ?: "" }.toMutableMap()
|
||||||
headers["X-Requested-With"] = WebViewUtil.REQUESTED_WITH
|
headers["X-Requested-With"] = WebViewUtil.REQUESTED_WITH
|
||||||
|
|
||||||
handler.post {
|
executor.execute {
|
||||||
val webview = WebView(context)
|
val webview = WebView(context)
|
||||||
webView = webview
|
webView = webview
|
||||||
webview.setDefaultSettings()
|
webview.setDefaultSettings()
|
||||||
@ -146,7 +145,7 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
|
|||||||
// around 4 seconds but it can take more due to slow networks or server issues.
|
// around 4 seconds but it can take more due to slow networks or server issues.
|
||||||
latch.await(12, TimeUnit.SECONDS)
|
latch.await(12, TimeUnit.SECONDS)
|
||||||
|
|
||||||
handler.post {
|
executor.execute {
|
||||||
if (!cloudflareBypassed) {
|
if (!cloudflareBypassed) {
|
||||||
isWebViewOutdated = webView?.isOutdated() == true
|
isWebViewOutdated = webView?.isOutdated() == true
|
||||||
}
|
}
|
||||||
|
@ -29,7 +29,6 @@ class LocalSource(private val context: Context) : CatalogueSource {
|
|||||||
const val ID = 0L
|
const val ID = 0L
|
||||||
const val HELP_URL = "https://tachiyomi.org/help/guides/local-manga/"
|
const val HELP_URL = "https://tachiyomi.org/help/guides/local-manga/"
|
||||||
|
|
||||||
private const val COVER_NAME = "cover.jpg"
|
|
||||||
private val SUPPORTED_ARCHIVE_TYPES = setOf("zip", "rar", "cbr", "cbz", "epub")
|
private val SUPPORTED_ARCHIVE_TYPES = setOf("zip", "rar", "cbr", "cbz", "epub")
|
||||||
|
|
||||||
private val LATEST_THRESHOLD = TimeUnit.MILLISECONDS.convert(7, TimeUnit.DAYS)
|
private val LATEST_THRESHOLD = TimeUnit.MILLISECONDS.convert(7, TimeUnit.DAYS)
|
||||||
@ -40,18 +39,29 @@ class LocalSource(private val context: Context) : CatalogueSource {
|
|||||||
input.close()
|
input.close()
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
val cover = File("${dir.absolutePath}/${manga.url}", COVER_NAME)
|
val cover = getCoverFile(File("${dir.absolutePath}/${manga.url}"))
|
||||||
|
|
||||||
// It might not exist if using the external SD card
|
if (cover != null && cover.exists()) {
|
||||||
cover.parentFile?.mkdirs()
|
// It might not exist if using the external SD card
|
||||||
input.use {
|
cover.parentFile?.mkdirs()
|
||||||
cover.outputStream().use {
|
input.use {
|
||||||
input.copyTo(it)
|
cover.outputStream().use {
|
||||||
|
input.copyTo(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return cover
|
return cover
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns valid cover file inside [parent] directory.
|
||||||
|
*/
|
||||||
|
private fun getCoverFile(parent: File): File? {
|
||||||
|
return parent.listFiles()?.find { it.nameWithoutExtension == "cover" }?.takeIf {
|
||||||
|
it.isFile && ImageUtil.isImage(it.name) { it.inputStream() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun getBaseDirectories(context: Context): List<File> {
|
private fun getBaseDirectories(context: Context): List<File> {
|
||||||
val c = context.getString(R.string.app_name) + File.separator + "local"
|
val c = context.getString(R.string.app_name) + File.separator + "local"
|
||||||
return DiskUtil.getExternalStorages(context).map { File(it.absolutePath, c) }
|
return DiskUtil.getExternalStorages(context).map { File(it.absolutePath, c) }
|
||||||
@ -84,9 +94,9 @@ class LocalSource(private val context: Context) : CatalogueSource {
|
|||||||
when (state?.index) {
|
when (state?.index) {
|
||||||
0 -> {
|
0 -> {
|
||||||
mangaDirs = if (state.ascending) {
|
mangaDirs = if (state.ascending) {
|
||||||
mangaDirs.sortedBy { it.name.toLowerCase(Locale.ENGLISH) }
|
mangaDirs.sortedBy { it.name.lowercase(Locale.ENGLISH) }
|
||||||
} else {
|
} else {
|
||||||
mangaDirs.sortedByDescending { it.name.toLowerCase(Locale.ENGLISH) }
|
mangaDirs.sortedByDescending { it.name.lowercase(Locale.ENGLISH) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
1 -> {
|
1 -> {
|
||||||
@ -105,8 +115,8 @@ class LocalSource(private val context: Context) : CatalogueSource {
|
|||||||
|
|
||||||
// Try to find the cover
|
// Try to find the cover
|
||||||
for (dir in baseDirs) {
|
for (dir in baseDirs) {
|
||||||
val cover = File("${dir.absolutePath}/$url", COVER_NAME)
|
val cover = getCoverFile(File("${dir.absolutePath}/$url"))
|
||||||
if (cover.exists()) {
|
if (cover != null && cover.exists()) {
|
||||||
thumbnail_url = cover.absolutePath
|
thumbnail_url = cover.absolutePath
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@ -238,7 +248,7 @@ class LocalSource(private val context: Context) : CatalogueSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun isSupportedFile(extension: String): Boolean {
|
private fun isSupportedFile(extension: String): Boolean {
|
||||||
return extension.toLowerCase(Locale.ROOT) in SUPPORTED_ARCHIVE_TYPES
|
return extension.lowercase() in SUPPORTED_ARCHIVE_TYPES
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getFormat(chapter: SChapter): Format {
|
fun getFormat(chapter: SChapter): Format {
|
||||||
|
@ -70,8 +70,11 @@ open class SourceManager(private val context: Context) {
|
|||||||
return name
|
return name
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getSourceNotInstalledException(): Exception {
|
private fun getSourceNotInstalledException(): SourceNotInstalledException {
|
||||||
return Exception(context.getString(R.string.source_not_installed, id.toString()))
|
return SourceNotInstalledException(id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inner class SourceNotInstalledException(val id: Long) :
|
||||||
|
Exception(context.getString(R.string.source_not_installed, id.toString()))
|
||||||
}
|
}
|
||||||
|
@ -54,7 +54,7 @@ abstract class HttpSource : CatalogueSource {
|
|||||||
* Note the generated id sets the sign bit to 0.
|
* Note the generated id sets the sign bit to 0.
|
||||||
*/
|
*/
|
||||||
override val id by lazy {
|
override val id by lazy {
|
||||||
val key = "${name.toLowerCase()}/$lang/$versionId"
|
val key = "${name.lowercase()}/$lang/$versionId"
|
||||||
val bytes = MessageDigest.getInstance("MD5").digest(key.toByteArray())
|
val bytes = MessageDigest.getInstance("MD5").digest(key.toByteArray())
|
||||||
(0..7).map { bytes[it].toLong() and 0xff shl 8 * (7 - it) }.reduce(Long::or) and Long.MAX_VALUE
|
(0..7).map { bytes[it].toLong() and 0xff shl 8 * (7 - it) }.reduce(Long::or) and Long.MAX_VALUE
|
||||||
}
|
}
|
||||||
@ -80,7 +80,7 @@ abstract class HttpSource : CatalogueSource {
|
|||||||
/**
|
/**
|
||||||
* Visible name of the source.
|
* Visible name of the source.
|
||||||
*/
|
*/
|
||||||
override fun toString() = "$name (${lang.toUpperCase()})"
|
override fun toString() = "$name (${lang.uppercase()})"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an observable containing a page with a list of manga. Normally it's not needed to
|
* Returns an observable containing a page with a list of manga. Normally it's not needed to
|
||||||
@ -341,7 +341,7 @@ abstract class HttpSource : CatalogueSource {
|
|||||||
*/
|
*/
|
||||||
private fun getUrlWithoutDomain(orig: String): String {
|
private fun getUrlWithoutDomain(orig: String): String {
|
||||||
return try {
|
return try {
|
||||||
val uri = URI(orig)
|
val uri = URI(orig.replace(" ", "%20"))
|
||||||
var out = uri.path
|
var out = uri.path
|
||||||
if (uri.query != null) {
|
if (uri.query != null) {
|
||||||
out += "?" + uri.query
|
out += "?" + uri.query
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.ui.base.activity
|
package eu.kanade.tachiyomi.ui.base.activity
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.viewbinding.ViewBinding
|
import androidx.viewbinding.ViewBinding
|
||||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||||
@ -14,9 +15,8 @@ abstract class BaseRxActivity<VB : ViewBinding, P : BasePresenter<*>> : NucleusA
|
|||||||
|
|
||||||
lateinit var binding: VB
|
lateinit var binding: VB
|
||||||
|
|
||||||
init {
|
override fun attachBaseContext(newBase: Context) {
|
||||||
@Suppress("LeakingThis")
|
super.attachBaseContext(LocaleHelper.createLocaleWrapper(newBase))
|
||||||
LocaleHelper.updateConfiguration(this)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
@ -1,43 +1,68 @@
|
|||||||
package eu.kanade.tachiyomi.ui.base.activity
|
package eu.kanade.tachiyomi.ui.base.activity
|
||||||
|
|
||||||
import android.content.res.Configuration.UI_MODE_NIGHT_MASK
|
import android.content.Context
|
||||||
import android.content.res.Configuration.UI_MODE_NIGHT_YES
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues.DarkThemeVariant
|
import eu.kanade.tachiyomi.data.preference.PreferenceValues
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues.LightThemeVariant
|
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues.ThemeMode
|
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
|
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
abstract class BaseThemedActivity : AppCompatActivity() {
|
abstract class BaseThemedActivity : AppCompatActivity() {
|
||||||
|
|
||||||
val preferences: PreferencesHelper by injectLazy()
|
val preferences: PreferencesHelper by injectLazy()
|
||||||
|
|
||||||
|
override fun attachBaseContext(newBase: Context) {
|
||||||
|
super.attachBaseContext(LocaleHelper.createLocaleWrapper(newBase))
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
val isDarkMode = when (preferences.themeMode().get()) {
|
applyAppTheme(preferences)
|
||||||
ThemeMode.light -> false
|
|
||||||
ThemeMode.dark -> true
|
|
||||||
ThemeMode.system -> resources.configuration.uiMode and UI_MODE_NIGHT_MASK == UI_MODE_NIGHT_YES
|
|
||||||
}
|
|
||||||
val themeId = if (isDarkMode) {
|
|
||||||
when (preferences.themeDark().get()) {
|
|
||||||
DarkThemeVariant.default -> R.style.Theme_Tachiyomi_Dark
|
|
||||||
DarkThemeVariant.blue -> R.style.Theme_Tachiyomi_Dark_Blue
|
|
||||||
DarkThemeVariant.greenapple -> R.style.Theme_Tachiyomi_Dark_GreenApple
|
|
||||||
DarkThemeVariant.midnightdusk -> R.style.Theme_Tachiyomi_Dark_MidnightDusk
|
|
||||||
DarkThemeVariant.amoled -> R.style.Theme_Tachiyomi_Amoled
|
|
||||||
DarkThemeVariant.hotpink -> R.style.Theme_Tachiyomi_Amoled_HotPink
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
when (preferences.themeLight().get()) {
|
|
||||||
LightThemeVariant.default -> R.style.Theme_Tachiyomi_Light
|
|
||||||
LightThemeVariant.blue -> R.style.Theme_Tachiyomi_Light_Blue
|
|
||||||
LightThemeVariant.strawberrydaiquiri -> R.style.Theme_Tachiyomi_Light_StrawberryDaiquiri
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setTheme(themeId)
|
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun AppCompatActivity.applyAppTheme(preferences: PreferencesHelper) {
|
||||||
|
val resIds = mutableListOf<Int>()
|
||||||
|
when (preferences.appTheme().get()) {
|
||||||
|
PreferenceValues.AppTheme.MONET -> {
|
||||||
|
resIds += R.style.Theme_Tachiyomi_Monet
|
||||||
|
}
|
||||||
|
PreferenceValues.AppTheme.BLUE -> {
|
||||||
|
resIds += R.style.Theme_Tachiyomi_Blue
|
||||||
|
resIds += R.style.ThemeOverlay_Tachiyomi_ColoredBars
|
||||||
|
}
|
||||||
|
PreferenceValues.AppTheme.GREEN_APPLE -> {
|
||||||
|
resIds += R.style.Theme_Tachiyomi_GreenApple
|
||||||
|
}
|
||||||
|
PreferenceValues.AppTheme.MIDNIGHT_DUSK -> {
|
||||||
|
resIds += R.style.Theme_Tachiyomi_MidnightDusk
|
||||||
|
}
|
||||||
|
PreferenceValues.AppTheme.STRAWBERRY_DAIQUIRI -> {
|
||||||
|
resIds += R.style.Theme_Tachiyomi_StrawberryDaiquiri
|
||||||
|
}
|
||||||
|
PreferenceValues.AppTheme.TAKO -> {
|
||||||
|
resIds += R.style.Theme_Tachiyomi_Tako
|
||||||
|
}
|
||||||
|
PreferenceValues.AppTheme.YINYANG -> {
|
||||||
|
resIds += R.style.Theme_Tachiyomi_YinYang
|
||||||
|
}
|
||||||
|
PreferenceValues.AppTheme.YOTSUBA -> {
|
||||||
|
resIds += R.style.Theme_Tachiyomi_Yotsuba
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
resIds += R.style.Theme_Tachiyomi
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preferences.themeDarkAmoled().get()) {
|
||||||
|
resIds += R.style.ThemeOverlay_Tachiyomi_Amoled
|
||||||
|
}
|
||||||
|
|
||||||
|
resIds.forEach {
|
||||||
|
setTheme(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.ui.base.activity
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.viewbinding.ViewBinding
|
import androidx.viewbinding.ViewBinding
|
||||||
import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate
|
import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate
|
||||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
|
||||||
|
|
||||||
abstract class BaseViewBindingActivity<VB : ViewBinding> : BaseThemedActivity() {
|
abstract class BaseViewBindingActivity<VB : ViewBinding> : BaseThemedActivity() {
|
||||||
|
|
||||||
@ -12,11 +11,6 @@ abstract class BaseViewBindingActivity<VB : ViewBinding> : BaseThemedActivity()
|
|||||||
@Suppress("LeakingThis")
|
@Suppress("LeakingThis")
|
||||||
private val secureActivityDelegate = SecureActivityDelegate(this)
|
private val secureActivityDelegate = SecureActivityDelegate(this)
|
||||||
|
|
||||||
init {
|
|
||||||
@Suppress("LeakingThis")
|
|
||||||
LocaleHelper.updateConfiguration(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
@ -10,14 +10,12 @@ import androidx.viewbinding.ViewBinding
|
|||||||
import com.bluelinelabs.conductor.Controller
|
import com.bluelinelabs.conductor.Controller
|
||||||
import com.bluelinelabs.conductor.ControllerChangeHandler
|
import com.bluelinelabs.conductor.ControllerChangeHandler
|
||||||
import com.bluelinelabs.conductor.ControllerChangeType
|
import com.bluelinelabs.conductor.ControllerChangeType
|
||||||
import com.bluelinelabs.conductor.RestoreViewOnCreateController
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.MainScope
|
import kotlinx.coroutines.MainScope
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
abstract class BaseController<VB : ViewBinding>(bundle: Bundle? = null) :
|
abstract class BaseController<VB : ViewBinding>(bundle: Bundle? = null) : Controller(bundle) {
|
||||||
RestoreViewOnCreateController(bundle) {
|
|
||||||
|
|
||||||
protected lateinit var binding: VB
|
protected lateinit var binding: VB
|
||||||
private set
|
private set
|
||||||
|
@ -5,7 +5,7 @@ import android.os.Bundle
|
|||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import com.bluelinelabs.conductor.RestoreViewOnCreateController
|
import com.bluelinelabs.conductor.Controller
|
||||||
import com.bluelinelabs.conductor.Router
|
import com.bluelinelabs.conductor.Router
|
||||||
import com.bluelinelabs.conductor.RouterTransaction
|
import com.bluelinelabs.conductor.RouterTransaction
|
||||||
import com.bluelinelabs.conductor.changehandler.SimpleSwapChangeHandler
|
import com.bluelinelabs.conductor.changehandler.SimpleSwapChangeHandler
|
||||||
@ -16,7 +16,7 @@ import com.bluelinelabs.conductor.changehandler.SimpleSwapChangeHandler
|
|||||||
*
|
*
|
||||||
* Implementations should override this class and implement [.onCreateDialog] to create a custom dialog, such as an [android.app.AlertDialog]
|
* Implementations should override this class and implement [.onCreateDialog] to create a custom dialog, such as an [android.app.AlertDialog]
|
||||||
*/
|
*/
|
||||||
abstract class DialogController : RestoreViewOnCreateController {
|
abstract class DialogController : Controller {
|
||||||
|
|
||||||
protected var dialog: Dialog? = null
|
protected var dialog: Dialog? = null
|
||||||
private set
|
private set
|
||||||
|
@ -32,6 +32,12 @@ open class BasePresenter<V> : RxPresenter<V>() {
|
|||||||
presenterScope.cancel()
|
presenterScope.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We're trying to avoid using Rx, so we "undeprecate" this
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
override fun getView(): V? {
|
||||||
|
return super.getView()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Subscribes an observable with [deliverFirst] and adds it to the presenter's lifecycle
|
* Subscribes an observable with [deliverFirst] and adds it to the presenter's lifecycle
|
||||||
* subscription list.
|
* subscription list.
|
||||||
|
@ -9,7 +9,7 @@ import com.bluelinelabs.conductor.ControllerChangeHandler
|
|||||||
import com.bluelinelabs.conductor.ControllerChangeType
|
import com.bluelinelabs.conductor.ControllerChangeType
|
||||||
import com.bluelinelabs.conductor.Router
|
import com.bluelinelabs.conductor.Router
|
||||||
import com.bluelinelabs.conductor.RouterTransaction
|
import com.bluelinelabs.conductor.RouterTransaction
|
||||||
import com.bluelinelabs.conductor.support.RouterPagerAdapter
|
import com.bluelinelabs.conductor.viewpager.RouterPagerAdapter
|
||||||
import com.google.android.material.badge.BadgeDrawable
|
import com.google.android.material.badge.BadgeDrawable
|
||||||
import com.google.android.material.tabs.TabLayout
|
import com.google.android.material.tabs.TabLayout
|
||||||
import com.jakewharton.rxrelay.PublishRelay
|
import com.jakewharton.rxrelay.PublishRelay
|
||||||
|
@ -136,7 +136,7 @@ open class ExtensionController :
|
|||||||
}
|
}
|
||||||
|
|
||||||
searchView.queryTextChanges()
|
searchView.queryTextChanges()
|
||||||
.filter { router.backstack.lastOrNull()?.controller() == this }
|
.filter { router.backstack.lastOrNull()?.controller == this }
|
||||||
.onEach {
|
.onEach {
|
||||||
query = it.toString()
|
query = it.toString()
|
||||||
drawExtensions()
|
drawExtensions()
|
||||||
|
@ -40,7 +40,7 @@ class ExtensionHolder(view: View, val adapter: ExtensionAdapter) :
|
|||||||
extension is Extension.Installed && extension.isObsolete -> itemView.context.getString(R.string.ext_obsolete)
|
extension is Extension.Installed && extension.isObsolete -> itemView.context.getString(R.string.ext_obsolete)
|
||||||
extension.isNsfw && shouldLabelNsfw -> itemView.context.getString(R.string.ext_nsfw_short)
|
extension.isNsfw && shouldLabelNsfw -> itemView.context.getString(R.string.ext_nsfw_short)
|
||||||
else -> ""
|
else -> ""
|
||||||
}.toUpperCase()
|
}.uppercase()
|
||||||
|
|
||||||
binding.image.clear()
|
binding.image.clear()
|
||||||
if (extension is Extension.Available) {
|
if (extension is Extension.Available) {
|
||||||
|
@ -61,9 +61,9 @@ open class ExtensionPresenter(
|
|||||||
|
|
||||||
val items = mutableListOf<ExtensionItem>()
|
val items = mutableListOf<ExtensionItem>()
|
||||||
|
|
||||||
val updatesSorted = installed.filter { it.hasUpdate && (showNsfwExtensions || !it.isNsfw) }.sortedBy { it.pkgName }
|
val updatesSorted = installed.filter { it.hasUpdate && (showNsfwExtensions || !it.isNsfw) }.sortedBy { it.name }
|
||||||
val installedSorted = installed.filter { !it.hasUpdate && (showNsfwExtensions || !it.isNsfw) }.sortedWith(compareBy({ !it.isObsolete }, { it.pkgName }))
|
val installedSorted = installed.filter { !it.hasUpdate && (showNsfwExtensions || !it.isNsfw) }.sortedWith(compareBy({ !it.isObsolete }, { it.name }))
|
||||||
val untrustedSorted = untrusted.sortedBy { it.pkgName }
|
val untrustedSorted = untrusted.sortedBy { it.name }
|
||||||
val availableSorted = available
|
val availableSorted = available
|
||||||
// Filter out already installed extensions and disabled languages
|
// Filter out already installed extensions and disabled languages
|
||||||
.filter { avail ->
|
.filter { avail ->
|
||||||
@ -82,9 +82,11 @@ open class ExtensionPresenter(
|
|||||||
}
|
}
|
||||||
if (installedSorted.isNotEmpty() || untrustedSorted.isNotEmpty()) {
|
if (installedSorted.isNotEmpty() || untrustedSorted.isNotEmpty()) {
|
||||||
val header = ExtensionGroupItem(context.getString(R.string.ext_installed), installedSorted.size + untrustedSorted.size)
|
val header = ExtensionGroupItem(context.getString(R.string.ext_installed), installedSorted.size + untrustedSorted.size)
|
||||||
|
|
||||||
items += installedSorted.map { extension ->
|
items += installedSorted.map { extension ->
|
||||||
ExtensionItem(extension, header, currentDownloads[extension.pkgName])
|
ExtensionItem(extension, header, currentDownloads[extension.pkgName])
|
||||||
}
|
}
|
||||||
|
|
||||||
items += untrustedSorted.map { extension ->
|
items += untrustedSorted.map { extension ->
|
||||||
ExtensionItem(extension, header)
|
ExtensionItem(extension, header)
|
||||||
}
|
}
|
||||||
|
@ -3,8 +3,8 @@ package eu.kanade.tachiyomi.ui.browse.extension
|
|||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.core.os.bundleOf
|
import androidx.core.os.bundleOf
|
||||||
import com.afollestad.materialdialogs.MaterialDialog
|
|
||||||
import com.bluelinelabs.conductor.Controller
|
import com.bluelinelabs.conductor.Controller
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||||
|
|
||||||
@ -21,15 +21,16 @@ class ExtensionTrustDialog<T>(bundle: Bundle? = null) : DialogController(bundle)
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
||||||
return MaterialDialog(activity!!)
|
return MaterialAlertDialogBuilder(activity!!)
|
||||||
.title(R.string.untrusted_extension)
|
.setTitle(R.string.untrusted_extension)
|
||||||
.message(R.string.untrusted_extension_message)
|
.setMessage(R.string.untrusted_extension_message)
|
||||||
.positiveButton(R.string.ext_trust) {
|
.setPositiveButton(R.string.ext_trust) { _, _ ->
|
||||||
(targetController as? Listener)?.trustSignature(args.getString(SIGNATURE_KEY)!!)
|
(targetController as? Listener)?.trustSignature(args.getString(SIGNATURE_KEY)!!)
|
||||||
}
|
}
|
||||||
.negativeButton(R.string.ext_uninstall) {
|
.setNegativeButton(R.string.ext_uninstall) { _, _ ->
|
||||||
(targetController as? Listener)?.uninstallExtension(args.getString(PKGNAME_KEY)!!)
|
(targetController as? Listener)?.uninstallExtension(args.getString(PKGNAME_KEY)!!)
|
||||||
}
|
}
|
||||||
|
.create()
|
||||||
}
|
}
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
|
@ -114,7 +114,7 @@ class ExtensionDetailsController(bundle: Bundle? = null) :
|
|||||||
.forEach {
|
.forEach {
|
||||||
val preferenceBlock = {
|
val preferenceBlock = {
|
||||||
it.value
|
it.value
|
||||||
.sortedWith(compareBy({ !it.isEnabled() }, { it.name.toLowerCase() }))
|
.sortedWith(compareBy({ !it.isEnabled() }, { it.name.lowercase() }))
|
||||||
.forEach { source ->
|
.forEach { source ->
|
||||||
val sourcePrefs = mutableListOf<Preference>()
|
val sourcePrefs = mutableListOf<Preference>()
|
||||||
|
|
||||||
|
@ -3,10 +3,9 @@ package eu.kanade.tachiyomi.ui.browse.migration.search
|
|||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import com.afollestad.materialdialogs.MaterialDialog
|
|
||||||
import com.afollestad.materialdialogs.list.listItemsMultiChoice
|
|
||||||
import com.bluelinelabs.conductor.Controller
|
import com.bluelinelabs.conductor.Controller
|
||||||
import com.bluelinelabs.conductor.RouterTransaction
|
import com.bluelinelabs.conductor.RouterTransaction
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
@ -86,28 +85,29 @@ class SearchController(
|
|||||||
|
|
||||||
private val preferences: PreferencesHelper by injectLazy()
|
private val preferences: PreferencesHelper by injectLazy()
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
||||||
val prefValue = preferences.migrateFlags().get()
|
val prefValue = preferences.migrateFlags().get()
|
||||||
|
val enabledFlagsPositions = MigrationFlags.getEnabledFlagsPositions(prefValue)
|
||||||
|
val items = MigrationFlags.titles
|
||||||
|
.map { resources?.getString(it) }
|
||||||
|
.toTypedArray()
|
||||||
|
val selected = items
|
||||||
|
.mapIndexed { i, _ -> enabledFlagsPositions.contains(i) }
|
||||||
|
.toBooleanArray()
|
||||||
|
|
||||||
val preselected =
|
return MaterialAlertDialogBuilder(activity!!)
|
||||||
MigrationFlags.getEnabledFlagsPositions(
|
.setTitle(R.string.migration_dialog_what_to_include)
|
||||||
prefValue
|
.setMultiChoiceItems(items, selected) { _, which, checked ->
|
||||||
)
|
selected[which] = checked
|
||||||
|
|
||||||
return MaterialDialog(activity!!)
|
|
||||||
.title(R.string.migration_dialog_what_to_include)
|
|
||||||
.listItemsMultiChoice(
|
|
||||||
items = MigrationFlags.titles.map { resources?.getString(it) as CharSequence },
|
|
||||||
initialSelection = preselected.toIntArray()
|
|
||||||
) { _, positions, _ ->
|
|
||||||
// Save current settings for the next time
|
|
||||||
val newValue =
|
|
||||||
MigrationFlags.getFlagsFromPositions(
|
|
||||||
positions.toTypedArray()
|
|
||||||
)
|
|
||||||
preferences.migrateFlags().set(newValue)
|
|
||||||
}
|
}
|
||||||
.positiveButton(R.string.migrate) {
|
.setPositiveButton(R.string.migrate) { _, _ ->
|
||||||
|
// Save current settings for the next time
|
||||||
|
val selectedIndices = mutableListOf<Int>()
|
||||||
|
selected.forEachIndexed { i, b -> if (b) selectedIndices.add(i) }
|
||||||
|
val newValue = MigrationFlags.getFlagsFromPositions(selectedIndices.toTypedArray())
|
||||||
|
preferences.migrateFlags().set(newValue)
|
||||||
|
|
||||||
if (callingController != null) {
|
if (callingController != null) {
|
||||||
if (callingController.javaClass == SourceSearchController::class.java) {
|
if (callingController.javaClass == SourceSearchController::class.java) {
|
||||||
router.popController(callingController)
|
router.popController(callingController)
|
||||||
@ -115,7 +115,7 @@ class SearchController(
|
|||||||
}
|
}
|
||||||
(targetController as? SearchController)?.migrateManga(manga, newManga)
|
(targetController as? SearchController)?.migrateManga(manga, newManga)
|
||||||
}
|
}
|
||||||
.negativeButton(R.string.copy) {
|
.setNegativeButton(R.string.copy) { _, _, ->
|
||||||
if (callingController != null) {
|
if (callingController != null) {
|
||||||
if (callingController.javaClass == SourceSearchController::class.java) {
|
if (callingController.javaClass == SourceSearchController::class.java) {
|
||||||
router.popController(callingController)
|
router.popController(callingController)
|
||||||
@ -123,7 +123,8 @@ class SearchController(
|
|||||||
}
|
}
|
||||||
(targetController as? SearchController)?.copyManga(manga, newManga)
|
(targetController as? SearchController)?.copyManga(manga, newManga)
|
||||||
}
|
}
|
||||||
.neutralButton(android.R.string.cancel)
|
.setNeutralButton(android.R.string.cancel, null)
|
||||||
|
.create()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,13 +26,18 @@ class SourceSearchController(
|
|||||||
override fun onItemClick(view: View, position: Int): Boolean {
|
override fun onItemClick(view: View, position: Int): Boolean {
|
||||||
val item = adapter?.getItem(position) as? SourceItem ?: return false
|
val item = adapter?.getItem(position) as? SourceItem ?: return false
|
||||||
newManga = item.manga
|
newManga = item.manga
|
||||||
val searchController = router.backstack.findLast { it.controller().javaClass == SearchController::class.java }?.controller() as SearchController?
|
val searchController = router.backstack.findLast { it.controller.javaClass == SearchController::class.java }?.controller as SearchController?
|
||||||
val dialog =
|
val dialog =
|
||||||
SearchController.MigrationDialog(oldManga, newManga, this)
|
SearchController.MigrationDialog(oldManga, newManga, this)
|
||||||
dialog.targetController = searchController
|
dialog.targetController = searchController
|
||||||
dialog.showDialog(router)
|
dialog.showDialog(router)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onItemLongClick(position: Int) {
|
||||||
|
view?.let { super.onItemClick(it, position) }
|
||||||
|
}
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
const val MANGA_KEY = "oldManga"
|
const val MANGA_KEY = "oldManga"
|
||||||
}
|
}
|
||||||
|
@ -72,8 +72,6 @@ class MigrationSourcesController :
|
|||||||
parentController!!.router.pushController(controller.withFadeTransaction())
|
parentController!!.router.pushController(controller.withFadeTransaction())
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val HELP_URL = "https://tachiyomi.org/help/guides/source-migration/"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private const val HELP_URL = "https://tachiyomi.org/help/guides/source-migration/"
|
||||||
|
@ -34,7 +34,7 @@ class MigrationSourcesPresenter(
|
|||||||
val source = sourceManager.getOrStub(it.key)
|
val source = sourceManager.getOrStub(it.key)
|
||||||
SourceItem(source, it.value.size, header)
|
SourceItem(source, it.value.size, header)
|
||||||
}
|
}
|
||||||
.sortedBy { it.source.name.toLowerCase() }
|
.sortedBy { it.source.name.lowercase() }
|
||||||
.toList()
|
.toList()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,10 +9,9 @@ import android.view.MenuInflater
|
|||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import com.afollestad.materialdialogs.MaterialDialog
|
|
||||||
import com.afollestad.materialdialogs.list.listItems
|
|
||||||
import com.bluelinelabs.conductor.ControllerChangeHandler
|
import com.bluelinelabs.conductor.ControllerChangeHandler
|
||||||
import com.bluelinelabs.conductor.ControllerChangeType
|
import com.bluelinelabs.conductor.ControllerChangeType
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import dev.chrisbanes.insetter.applyInsetter
|
import dev.chrisbanes.insetter.applyInsetter
|
||||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||||
import eu.davidea.flexibleadapter.items.IFlexible
|
import eu.davidea.flexibleadapter.items.IFlexible
|
||||||
@ -32,6 +31,8 @@ import eu.kanade.tachiyomi.ui.browse.BrowseController
|
|||||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
|
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
|
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.latest.LatestUpdatesController
|
import eu.kanade.tachiyomi.ui.browse.source.latest.LatestUpdatesController
|
||||||
|
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||||
|
import eu.kanade.tachiyomi.util.view.onAnimationsFinished
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
@ -82,6 +83,9 @@ class SourceController :
|
|||||||
// Create recycler and set adapter.
|
// Create recycler and set adapter.
|
||||||
binding.recycler.layoutManager = LinearLayoutManager(view.context)
|
binding.recycler.layoutManager = LinearLayoutManager(view.context)
|
||||||
binding.recycler.adapter = adapter
|
binding.recycler.adapter = adapter
|
||||||
|
binding.recycler.onAnimationsFinished {
|
||||||
|
(activity as? MainActivity)?.ready = true
|
||||||
|
}
|
||||||
adapter?.fastScroller = binding.fastScroller
|
adapter?.fastScroller = binding.fastScroller
|
||||||
|
|
||||||
requestPermissionsSafe(arrayOf(WRITE_EXTERNAL_STORAGE), 301)
|
requestPermissionsSafe(arrayOf(WRITE_EXTERNAL_STORAGE), 301)
|
||||||
@ -238,15 +242,13 @@ class SourceController :
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
||||||
return MaterialDialog(activity!!)
|
return MaterialAlertDialogBuilder(activity!!)
|
||||||
.title(text = source)
|
.setTitle(source)
|
||||||
.listItems(
|
.setItems(items.map { it.first }.toTypedArray()) { dialog, which ->
|
||||||
items = items.map { it.first },
|
|
||||||
waitForPositiveButton = false
|
|
||||||
) { dialog, which, _ ->
|
|
||||||
items[which].second()
|
items[which].second()
|
||||||
dialog.dismiss()
|
dialog.dismiss()
|
||||||
}
|
}
|
||||||
|
.create()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,7 +42,7 @@ class SourceFilterController : SettingsController() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
orderedLangs.forEach { lang ->
|
orderedLangs.forEach { lang ->
|
||||||
val sources = sourcesByLang[lang].orEmpty().sortedBy { it.name.toLowerCase() }
|
val sources = sourcesByLang[lang].orEmpty().sortedBy { it.name.lowercase() }
|
||||||
|
|
||||||
// Create a preference group and set initial state and change listener
|
// Create a preference group and set initial state and change listener
|
||||||
switchPreferenceCategory {
|
switchPreferenceCategory {
|
||||||
|
@ -122,7 +122,7 @@ class SourcePresenter(
|
|||||||
return sourceManager.getCatalogueSources()
|
return sourceManager.getCatalogueSources()
|
||||||
.filter { it.lang in languages }
|
.filter { it.lang in languages }
|
||||||
.filterNot { it.id.toString() in disabledSourceIds }
|
.filterNot { it.id.toString() in disabledSourceIds }
|
||||||
.sortedBy { "(${it.lang}) ${it.name.toLowerCase()}" } +
|
.sortedBy { "(${it.lang}) ${it.name.lowercase()}" } +
|
||||||
sourceManager.get(LocalSource.ID) as LocalSource
|
sourceManager.get(LocalSource.ID) as LocalSource
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -13,8 +13,7 @@ import androidx.core.view.updatePadding
|
|||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.afollestad.materialdialogs.MaterialDialog
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import com.afollestad.materialdialogs.list.listItems
|
|
||||||
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.tfcporciuncula.flow.Preference
|
import com.tfcporciuncula.flow.Preference
|
||||||
@ -24,12 +23,12 @@ import eu.davidea.flexibleadapter.items.IFlexible
|
|||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.database.models.Category
|
import eu.kanade.tachiyomi.data.database.models.Category
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues.DisplayMode
|
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.data.preference.asImmediateFlow
|
import eu.kanade.tachiyomi.data.preference.asImmediateFlow
|
||||||
import eu.kanade.tachiyomi.databinding.SourceControllerBinding
|
import eu.kanade.tachiyomi.databinding.SourceControllerBinding
|
||||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||||
import eu.kanade.tachiyomi.source.LocalSource
|
import eu.kanade.tachiyomi.source.LocalSource
|
||||||
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.FabController
|
import eu.kanade.tachiyomi.ui.base.controller.FabController
|
||||||
@ -37,6 +36,7 @@ import eu.kanade.tachiyomi.ui.base.controller.SearchableNucleusController
|
|||||||
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
|
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
|
||||||
import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog
|
import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog
|
||||||
|
import eu.kanade.tachiyomi.ui.library.setting.DisplayModeSetting
|
||||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||||
import eu.kanade.tachiyomi.ui.more.MoreController
|
import eu.kanade.tachiyomi.ui.more.MoreController
|
||||||
@ -205,7 +205,7 @@ open class BrowseSourceController(bundle: Bundle) :
|
|||||||
binding.catalogueView.removeView(oldRecycler)
|
binding.catalogueView.removeView(oldRecycler)
|
||||||
}
|
}
|
||||||
|
|
||||||
val recycler = if (preferences.sourceDisplayMode().get() == DisplayMode.LIST) {
|
val recycler = if (preferences.sourceDisplayMode().get() == DisplayModeSetting.LIST) {
|
||||||
RecyclerView(view.context).apply {
|
RecyclerView(view.context).apply {
|
||||||
id = R.id.recycler
|
id = R.id.recycler
|
||||||
layoutManager = LinearLayoutManager(context)
|
layoutManager = LinearLayoutManager(context)
|
||||||
@ -261,7 +261,7 @@ open class BrowseSourceController(bundle: Bundle) :
|
|||||||
searchItem.fixExpand(
|
searchItem.fixExpand(
|
||||||
onExpand = { invalidateMenuOnExpand() },
|
onExpand = { invalidateMenuOnExpand() },
|
||||||
onCollapse = {
|
onCollapse = {
|
||||||
if (router.backstackSize >= 2 && router.backstack[router.backstackSize - 2].controller() is GlobalSearchController) {
|
if (router.backstackSize >= 2 && router.backstack[router.backstackSize - 2].controller is GlobalSearchController) {
|
||||||
router.popController(this)
|
router.popController(this)
|
||||||
} else {
|
} else {
|
||||||
nonSubmittedQuery = ""
|
nonSubmittedQuery = ""
|
||||||
@ -273,9 +273,9 @@ open class BrowseSourceController(bundle: Bundle) :
|
|||||||
)
|
)
|
||||||
|
|
||||||
val displayItem = when (preferences.sourceDisplayMode().get()) {
|
val displayItem = when (preferences.sourceDisplayMode().get()) {
|
||||||
DisplayMode.COMPACT_GRID -> R.id.action_compact_grid
|
DisplayModeSetting.COMPACT_GRID -> R.id.action_compact_grid
|
||||||
DisplayMode.COMFORTABLE_GRID -> R.id.action_comfortable_grid
|
DisplayModeSetting.COMFORTABLE_GRID -> R.id.action_comfortable_grid
|
||||||
DisplayMode.LIST -> R.id.action_list
|
DisplayModeSetting.LIST -> R.id.action_list
|
||||||
}
|
}
|
||||||
menu.findItem(displayItem).isChecked = true
|
menu.findItem(displayItem).isChecked = true
|
||||||
}
|
}
|
||||||
@ -297,9 +297,9 @@ open class BrowseSourceController(bundle: Bundle) :
|
|||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
when (item.itemId) {
|
when (item.itemId) {
|
||||||
R.id.action_search -> expandActionViewFromInteraction = true
|
R.id.action_search -> expandActionViewFromInteraction = true
|
||||||
R.id.action_compact_grid -> setDisplayMode(DisplayMode.COMPACT_GRID)
|
R.id.action_compact_grid -> setDisplayMode(DisplayModeSetting.COMPACT_GRID)
|
||||||
R.id.action_comfortable_grid -> setDisplayMode(DisplayMode.COMFORTABLE_GRID)
|
R.id.action_comfortable_grid -> setDisplayMode(DisplayModeSetting.COMFORTABLE_GRID)
|
||||||
R.id.action_list -> setDisplayMode(DisplayMode.LIST)
|
R.id.action_list -> setDisplayMode(DisplayModeSetting.LIST)
|
||||||
R.id.action_open_in_web_view -> openInWebView()
|
R.id.action_open_in_web_view -> openInWebView()
|
||||||
R.id.action_local_source_help -> openLocalSourceHelpGuide()
|
R.id.action_local_source_help -> openLocalSourceHelpGuide()
|
||||||
}
|
}
|
||||||
@ -335,6 +335,54 @@ open class BrowseSourceController(bundle: Bundle) :
|
|||||||
presenter.restartPager(newQuery)
|
presenter.restartPager(newQuery)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to restart the request with a new genre-filtered query.
|
||||||
|
* If the genre name can't be found the filters,
|
||||||
|
* the standard searchWithQuery search method is used instead.
|
||||||
|
*
|
||||||
|
* @param genreName the name of the genre
|
||||||
|
*/
|
||||||
|
fun searchWithGenre(genreName: String) {
|
||||||
|
presenter.sourceFilters = presenter.source.getFilterList()
|
||||||
|
|
||||||
|
var filterList: FilterList? = null
|
||||||
|
|
||||||
|
filter@ for (sourceFilter in presenter.sourceFilters) {
|
||||||
|
if (sourceFilter is Filter.Group<*>) {
|
||||||
|
for (filter in sourceFilter.state) {
|
||||||
|
if (filter is Filter<*> && filter.name.equals(genreName, true)) {
|
||||||
|
when (filter) {
|
||||||
|
is Filter.TriState -> filter.state = 1
|
||||||
|
is Filter.CheckBox -> filter.state = true
|
||||||
|
}
|
||||||
|
filterList = presenter.sourceFilters
|
||||||
|
break@filter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (sourceFilter is Filter.Select<*>) {
|
||||||
|
val index = sourceFilter.values.filterIsInstance<String>()
|
||||||
|
.indexOfFirst { it.equals(genreName, true) }
|
||||||
|
|
||||||
|
if (index != -1) {
|
||||||
|
sourceFilter.state = index
|
||||||
|
filterList = presenter.sourceFilters
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filterList != null) {
|
||||||
|
filterSheet?.setFilters(presenter.filterItems)
|
||||||
|
|
||||||
|
showProgressBar()
|
||||||
|
|
||||||
|
adapter?.clear()
|
||||||
|
presenter.restartPager("", filterList)
|
||||||
|
} else {
|
||||||
|
searchWithQuery(genreName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called from the presenter when the network request is received.
|
* Called from the presenter when the network request is received.
|
||||||
*
|
*
|
||||||
@ -446,7 +494,7 @@ open class BrowseSourceController(bundle: Bundle) :
|
|||||||
*
|
*
|
||||||
* @param mode the mode to change to
|
* @param mode the mode to change to
|
||||||
*/
|
*/
|
||||||
private fun setDisplayMode(mode: DisplayMode) {
|
private fun setDisplayMode(mode: DisplayModeSetting) {
|
||||||
val view = view ?: return
|
val view = view ?: return
|
||||||
val adapter = adapter ?: return
|
val adapter = adapter ?: return
|
||||||
|
|
||||||
@ -540,11 +588,9 @@ open class BrowseSourceController(bundle: Bundle) :
|
|||||||
val manga = (adapter?.getItem(position) as? SourceItem?)?.manga ?: return
|
val manga = (adapter?.getItem(position) as? SourceItem?)?.manga ?: return
|
||||||
|
|
||||||
if (manga.favorite) {
|
if (manga.favorite) {
|
||||||
MaterialDialog(activity)
|
MaterialAlertDialogBuilder(activity)
|
||||||
.listItems(
|
.setTitle(manga.title)
|
||||||
items = listOf(activity.getString(R.string.remove_from_library)),
|
.setItems(arrayOf(activity.getString(R.string.remove_from_library))) { _, which ->
|
||||||
waitForPositiveButton = false
|
|
||||||
) { _, which, _ ->
|
|
||||||
when (which) {
|
when (which) {
|
||||||
0 -> {
|
0 -> {
|
||||||
presenter.changeMangaFavorite(manga)
|
presenter.changeMangaFavorite(manga)
|
||||||
|
@ -9,9 +9,9 @@ import eu.kanade.tachiyomi.data.database.models.Manga
|
|||||||
import eu.kanade.tachiyomi.data.database.models.MangaCategory
|
import eu.kanade.tachiyomi.data.database.models.MangaCategory
|
||||||
import eu.kanade.tachiyomi.data.database.models.toMangaInfo
|
import eu.kanade.tachiyomi.data.database.models.toMangaInfo
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
|
import eu.kanade.tachiyomi.data.track.EnhancedTrackService
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
import eu.kanade.tachiyomi.data.track.TrackService
|
import eu.kanade.tachiyomi.data.track.TrackService
|
||||||
import eu.kanade.tachiyomi.data.track.UnattendedTrackService
|
|
||||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.source.model.Filter
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
@ -37,6 +37,7 @@ import eu.kanade.tachiyomi.util.chapter.syncChaptersWithTrackServiceTwoWay
|
|||||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||||
import eu.kanade.tachiyomi.util.removeCovers
|
import eu.kanade.tachiyomi.util.removeCovers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asFlow
|
import kotlinx.coroutines.flow.asFlow
|
||||||
import kotlinx.coroutines.flow.catch
|
import kotlinx.coroutines.flow.catch
|
||||||
@ -44,7 +45,6 @@ import kotlinx.coroutines.flow.collect
|
|||||||
import kotlinx.coroutines.flow.filter
|
import kotlinx.coroutines.flow.filter
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import rx.Observable
|
|
||||||
import rx.Subscription
|
import rx.Subscription
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
import rx.schedulers.Schedulers
|
import rx.schedulers.Schedulers
|
||||||
@ -104,7 +104,7 @@ open class BrowseSourcePresenter(
|
|||||||
/**
|
/**
|
||||||
* Subscription for one request from the pager.
|
* Subscription for one request from the pager.
|
||||||
*/
|
*/
|
||||||
private var pageSubscription: Subscription? = null
|
private var nextPageJob: Job? = null
|
||||||
|
|
||||||
private val loggedServices by lazy { Injekt.get<TrackManager>().services.filter { it.isLogged } }
|
private val loggedServices by lazy { Injekt.get<TrackManager>().services.filter { it.isLogged } }
|
||||||
|
|
||||||
@ -175,14 +175,14 @@ open class BrowseSourcePresenter(
|
|||||||
fun requestNext() {
|
fun requestNext() {
|
||||||
if (!hasNextPage()) return
|
if (!hasNextPage()) return
|
||||||
|
|
||||||
pageSubscription?.let { remove(it) }
|
nextPageJob?.cancel()
|
||||||
pageSubscription = Observable.defer { pager.requestNext() }
|
nextPageJob = launchIO {
|
||||||
.subscribeFirst(
|
try {
|
||||||
{ _, _ ->
|
pager.requestNextPage()
|
||||||
// Nothing to do when onNext is emitted.
|
} catch (e: Throwable) {
|
||||||
},
|
withUIContext { view?.onAddPageError(e) }
|
||||||
BrowseSourceController::onAddPageError
|
}
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -267,9 +267,7 @@ open class BrowseSourcePresenter(
|
|||||||
} else {
|
} else {
|
||||||
ChapterSettingsHelper.applySettingDefaults(manga)
|
ChapterSettingsHelper.applySettingDefaults(manga)
|
||||||
|
|
||||||
if (prefs.autoAddTrack()) {
|
autoAddTrack(manga)
|
||||||
autoAddTrack(manga)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
db.insertManga(manga).executeAsBlocking()
|
db.insertManga(manga).executeAsBlocking()
|
||||||
@ -277,7 +275,7 @@ open class BrowseSourcePresenter(
|
|||||||
|
|
||||||
private fun autoAddTrack(manga: Manga) {
|
private fun autoAddTrack(manga: Manga) {
|
||||||
loggedServices
|
loggedServices
|
||||||
.filterIsInstance<UnattendedTrackService>()
|
.filterIsInstance<EnhancedTrackService>()
|
||||||
.filter { it.accept(source) }
|
.filter { it.accept(source) }
|
||||||
.forEach { service ->
|
.forEach { service ->
|
||||||
launchIO {
|
launchIO {
|
||||||
|
@ -19,7 +19,7 @@ abstract class Pager(var currentPage: Int = 1) {
|
|||||||
return results.asObservable()
|
return results.asObservable()
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract fun requestNext(): Observable<MangasPage>
|
abstract suspend fun requestNextPage()
|
||||||
|
|
||||||
fun onPageReceived(mangasPage: MangasPage) {
|
fun onPageReceived(mangasPage: MangasPage) {
|
||||||
val page = currentPage
|
val page = currentPage
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
package eu.kanade.tachiyomi.ui.browse.source.browse
|
package eu.kanade.tachiyomi.ui.browse.source.browse
|
||||||
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import androidx.core.view.isVisible
|
||||||
import coil.clear
|
import coil.clear
|
||||||
import coil.imageLoader
|
import coil.imageLoader
|
||||||
import coil.request.CachePolicy
|
|
||||||
import coil.request.ImageRequest
|
import coil.request.ImageRequest
|
||||||
import coil.transition.CrossfadeTransition
|
import coil.transition.CrossfadeTransition
|
||||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||||
@ -38,6 +38,12 @@ class SourceComfortableGridHolder(private val view: View, private val adapter: F
|
|||||||
// Set alpha of thumbnail.
|
// Set alpha of thumbnail.
|
||||||
binding.thumbnail.alpha = if (manga.favorite) 0.3f else 1.0f
|
binding.thumbnail.alpha = if (manga.favorite) 0.3f else 1.0f
|
||||||
|
|
||||||
|
// For rounded corners
|
||||||
|
binding.badges.clipToOutline = true
|
||||||
|
|
||||||
|
// Set favorite badge
|
||||||
|
binding.favoriteText.isVisible = manga.favorite
|
||||||
|
|
||||||
setImage(manga)
|
setImage(manga)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,7 +59,6 @@ class SourceComfortableGridHolder(private val view: View, private val adapter: F
|
|||||||
val request = ImageRequest.Builder(view.context)
|
val request = ImageRequest.Builder(view.context)
|
||||||
.data(manga)
|
.data(manga)
|
||||||
.setParameter(MangaCoverFetcher.USE_CUSTOM_COVER, false)
|
.setParameter(MangaCoverFetcher.USE_CUSTOM_COVER, false)
|
||||||
.diskCachePolicy(CachePolicy.DISABLED)
|
|
||||||
.target(StateImageViewTarget(binding.thumbnail, binding.progress, crossfadeDuration))
|
.target(StateImageViewTarget(binding.thumbnail, binding.progress, crossfadeDuration))
|
||||||
.build()
|
.build()
|
||||||
itemView.context.imageLoader.enqueue(request)
|
itemView.context.imageLoader.enqueue(request)
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user