mirror of
https://github.com/mihonapp/mihon.git
synced 2025-06-27 19:47:51 +02:00
Compare commits
172 Commits
Author | SHA1 | Date | |
---|---|---|---|
0dc4862d79 | |||
a3f1b72126 | |||
5ff10799e4 | |||
a82e5f5452 | |||
e10cb0e632 | |||
c7e07a6df0 | |||
2e0c778090 | |||
592050c668 | |||
02c9191525 | |||
d421401626 | |||
b2d4e5ab84 | |||
84e023607c | |||
f145fd0dec | |||
42a9f911d8 | |||
9567d55312 | |||
531cd99247 | |||
f3660d88dd | |||
3accb9a08b | |||
63ce7371bb | |||
01c3498dbf | |||
b3471234ad | |||
b2d697131c | |||
ef49fc91d8 | |||
6222b47a4f | |||
f58e3c390a | |||
7504621a24 | |||
88e49a9b8b | |||
5b23f29d06 | |||
c1bdebee78 | |||
ddd4cc10ff | |||
0ca62a4acc | |||
4f1275ac01 | |||
b2fee7035f | |||
e15d7cb548 | |||
3257cbe21f | |||
1237af1ff3 | |||
68600b337e | |||
dac2072eaa | |||
1b921f9845 | |||
a3992d9fbe | |||
efd2a0cb7b | |||
fba428257b | |||
ff36901007 | |||
940d8389b5 | |||
f7a6cbe5e2 | |||
7aa379a857 | |||
443024cebb | |||
1657f04d55 | |||
407e798fdb | |||
4054f2a6a0 | |||
468cdf603c | |||
988ec6a224 | |||
bdbdf211e2 | |||
0437703cbf | |||
71aa592111 | |||
d501c02f8b | |||
9daf0e78b8 | |||
dfa07a5f35 | |||
437c995d12 | |||
cc6ae9d1a8 | |||
c58e4f4dee | |||
c87b0e77de | |||
355d5af8ae | |||
3d99a8ebdb | |||
c4b975b777 | |||
2911fe7a1a | |||
14c114756d | |||
e7a8107279 | |||
bff73b1b40 | |||
c255f57d95 | |||
64c47bbaed | |||
e0b7698d40 | |||
a01792ac9a | |||
3ba078f64c | |||
a16240f123 | |||
e5a120e778 | |||
2ba60e9114 | |||
472ce5a5e4 | |||
99ba84c810 | |||
78285bdf37 | |||
5a7f2684b3 | |||
d912a42249 | |||
6d8c4fb8b1 | |||
a63cecbfcb | |||
4a5bceb4e4 | |||
86541445b7 | |||
4e826aa8e7 | |||
b6e6f490e9 | |||
2145e878a4 | |||
355f6db255 | |||
bc7632bf02 | |||
609d8c9685 | |||
2f08515455 | |||
7f450e185d | |||
747879b4ec | |||
4193870fa6 | |||
cdc5de3f1b | |||
bc34d4fa88 | |||
6fd4af8736 | |||
b5c2934270 | |||
94f5117941 | |||
112e233498 | |||
18b1326f3a | |||
1e58b05ead | |||
938919bd9b | |||
b6b78994d8 | |||
fddd8ce305 | |||
ccff337975 | |||
fde6b7af4f | |||
0657db7dcb | |||
d1c2eaf6d5 | |||
91bb6b9016 | |||
90351c6e9e | |||
dd4740e54f | |||
48e7cbd76c | |||
f51e32f39b | |||
ae42f59102 | |||
5c8006f9b7 | |||
aa5861d3ca | |||
7a64bf55cb | |||
d4c9ab793f | |||
48d2849d97 | |||
776610d0e6 | |||
3a790f3d66 | |||
7382042288 | |||
33992d80bf | |||
a92b0e567b | |||
829a65e515 | |||
03ad48c055 | |||
89837e4ced | |||
ace1db21d1 | |||
8bb69c455b | |||
2dae706198 | |||
3eda2a220a | |||
61e5440b7c | |||
2e2663bad9 | |||
f4dd150b70 | |||
2b35d22e25 | |||
f590378761 | |||
f5f592be91 | |||
7a373fb43a | |||
aded11e599 | |||
41d7cee020 | |||
f2ef6a20e6 | |||
a398c3fb81 | |||
2a454b44cc | |||
7b66ece895 | |||
b5017eebbf | |||
aa67229daf | |||
5af68186d6 | |||
545bc0e605 | |||
291168f4de | |||
9facb51f22 | |||
5b7d8c5e37 | |||
5945937e4b | |||
9f9f9872eb | |||
3566072f4a | |||
b85cd86b24 | |||
79c3767fff | |||
cf1609a429 | |||
3aeac7e7b5 | |||
1557f713f4 | |||
b63d24ac1a | |||
348c1ff29d | |||
717e55497f | |||
d84b5e8b46 | |||
5f9ddf9ff5 | |||
bbee093c63 | |||
e8c35ae4e1 | |||
1607658c30 | |||
2e9ef373f3 | |||
ec6eef6d37 |
4
.github/ISSUE_TEMPLATE.md
vendored
4
.github/ISSUE_TEMPLATE.md
vendored
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
I acknowledge that:
|
I acknowledge that:
|
||||||
|
|
||||||
- I have updated to the latest version of the app (stable is v0.10.8)
|
- I have updated to the latest version of the app (stable is v0.10.10)
|
||||||
- I have updated all extensions
|
- I have updated all extensions
|
||||||
- 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
|
||||||
|
|
||||||
@ -24,3 +24,5 @@ I acknowledge that:
|
|||||||
|
|
||||||
## Other details
|
## Other details
|
||||||
Additional details and attachments.
|
Additional details and attachments.
|
||||||
|
|
||||||
|
If you're experiencing crashes, share the crash logs from More → Settings → Advanced → Dump crash logs.
|
||||||
|
4
.github/ISSUE_TEMPLATE/bug_report.md
vendored
4
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -9,7 +9,7 @@ labels: "bug"
|
|||||||
|
|
||||||
I acknowledge that:
|
I acknowledge that:
|
||||||
|
|
||||||
- I have updated to the latest version of the app (stable is v0.10.8)
|
- I have updated to the latest version of the app (stable is v0.10.10)
|
||||||
- I have updated all extensions
|
- I have updated all extensions
|
||||||
- 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
|
||||||
|
|
||||||
@ -34,3 +34,5 @@ This happened instead.
|
|||||||
|
|
||||||
## Other details
|
## Other details
|
||||||
Additional details and attachments.
|
Additional details and attachments.
|
||||||
|
|
||||||
|
If you're experiencing crashes, share the crash logs from More → Settings → Advanced → Dump crash logs.
|
||||||
|
2
.github/ISSUE_TEMPLATE/feature_request.md
vendored
2
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@ -9,7 +9,7 @@ labels: "feature"
|
|||||||
|
|
||||||
I acknowledge that:
|
I acknowledge that:
|
||||||
|
|
||||||
- I have updated to the latest version of the app (stable is v0.10.8)
|
- I have updated to the latest version of the app (stable is v0.10.10)
|
||||||
- I have updated all extensions
|
- I have updated all extensions
|
||||||
- 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
|
||||||
|
|
||||||
|
31
.github/workflows/build.yml
vendored
31
.github/workflows/build.yml
vendored
@ -71,25 +71,24 @@ jobs:
|
|||||||
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
|
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
|
||||||
keyPassword: ${{ secrets.KEY_PASSWORD }}
|
keyPassword: ${{ secrets.KEY_PASSWORD }}
|
||||||
|
|
||||||
- name: Create release
|
- name: Clean up build artifacts
|
||||||
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'tachiyomiorg/tachiyomi'
|
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'tachiyomiorg/tachiyomi'
|
||||||
id: create_release
|
run: |
|
||||||
uses: actions/create-release@v1
|
cp ${{ env.SIGNED_RELEASE_FILE }} tachiyomi-${{ env.VERSION_TAG }}.apk
|
||||||
env:
|
md5=`md5sum tachiyomi-${{ env.VERSION_TAG }}.apk | awk '{ print $1 }'`
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
echo "APK_MD5=$md5" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Create Release
|
||||||
|
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'tachiyomiorg/tachiyomi'
|
||||||
|
uses: softprops/action-gh-release@v1
|
||||||
with:
|
with:
|
||||||
tag_name: ${{ env.VERSION_TAG }}
|
tag_name: ${{ env.VERSION_TAG }}
|
||||||
release_name: Tachiyomi ${{ env.VERSION_TAG }}
|
name: Tachiyomi ${{ env.VERSION_TAG }}
|
||||||
draft: true
|
body: |
|
||||||
|
MD5: ${{ env.APK_MD5 }}
|
||||||
|
files: |
|
||||||
|
tachiyomi-${{ env.VERSION_TAG }}.apk
|
||||||
|
draft: ${{ github.event.inputs.dry-run != '' }}
|
||||||
prerelease: false
|
prerelease: false
|
||||||
|
|
||||||
- name: Upload APK to release
|
|
||||||
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'tachiyomiorg/tachiyomi'
|
|
||||||
uses: actions/upload-release-asset@v1
|
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
|
||||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
|
||||||
asset_path: ${{ env.SIGNED_RELEASE_FILE }}
|
|
||||||
asset_name: tachiyomi-${{ env.VERSION_TAG }}.apk
|
|
||||||
asset_content_type: application/vnd.android.package-archive
|
|
||||||
|
55
.github/workflows/issue_closer.yml
vendored
55
.github/workflows/issue_closer.yml
vendored
@ -7,31 +7,30 @@ jobs:
|
|||||||
autoclose:
|
autoclose:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Autoclose when created in wrong repo
|
- name: Autoclose issues
|
||||||
uses: arkon/issue-closer-action@v1.1
|
uses: arkon/issue-closer-action@v3.0
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
type: title
|
rules: |
|
||||||
regex: ".*THIS ISSUE IS IN THE WRONG REPO.*"
|
[
|
||||||
message: "@${issue.user.login} this issue was automatically closed because it was not opened in the correct repo, as the template mentioned."
|
{
|
||||||
- name: Autoclose when no short description provided
|
"type": "title",
|
||||||
uses: arkon/issue-closer-action@v1.1
|
"regex": ".*THIS ISSUE IS IN THE WRONG REPO.*",
|
||||||
with:
|
"message": "It was not opened in the correct repo, as the template mentioned."
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
},
|
||||||
type: title
|
{
|
||||||
regex: ".*<Write short description here>*"
|
"type": "title",
|
||||||
message: "@${issue.user.login} this issue was automatically closed because you did not fill out the description in the title."
|
"regex": ".*<Write short description here>*",
|
||||||
- name: Autoclose when body acknowledgement section not removed
|
"message": "The description in the title was not filled out."
|
||||||
uses: arkon/issue-closer-action@v1.1
|
},
|
||||||
with:
|
{
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
"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.*"
|
"message": "The acknowledgment section was not removed."
|
||||||
message: "@${issue.user.login} this issue was automatically closed because the acknowledgment section was not removed."
|
},
|
||||||
- name: Autoclose when body requested information not filled out
|
{
|
||||||
uses: arkon/issue-closer-action@v1.1
|
"type": "body",
|
||||||
with:
|
"regex": ".*\\* (Tachiyomi version|Android version|Device): \\?.*",
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
"message": "Requested information in the template was not filled out."
|
||||||
type: body
|
}
|
||||||
regex: ".*\\* (Tachiyomi version|Android version|Device): \\?.*"
|
]
|
||||||
message: "@${issue.user.login} this issue was automatically closed because the requested information was not filled out."
|
|
||||||
|
19
.github/workflows/lock.yml
vendored
Normal file
19
.github/workflows/lock.yml
vendored
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
name: Lock threads
|
||||||
|
|
||||||
|
on:
|
||||||
|
# Daily
|
||||||
|
schedule:
|
||||||
|
- cron: '0 * * * *'
|
||||||
|
# Manual trigger
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lock:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: dessant/lock-threads@v2
|
||||||
|
with:
|
||||||
|
github-token: ${{ github.token }}
|
||||||
|
issue-lock-inactive-days: '2'
|
||||||
|
pr-lock-inactive-days: '2'
|
76
CODE_OF_CONDUCT.md
Normal file
76
CODE_OF_CONDUCT.md
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
# Code of Conduct
|
||||||
|
|
||||||
|
## Our Pledge
|
||||||
|
|
||||||
|
In the interest of fostering an open and welcoming environment, we as
|
||||||
|
contributors and maintainers pledge to making participation in our project and
|
||||||
|
our community a harassment-free experience for everyone, regardless of age, body
|
||||||
|
size, disability, ethnicity, sex characteristics, gender identity and expression,
|
||||||
|
level of experience, education, socio-economic status, nationality, personal
|
||||||
|
appearance, race, religion, or sexual identity and orientation.
|
||||||
|
|
||||||
|
## Our Standards
|
||||||
|
|
||||||
|
Examples of behavior that contributes to creating a positive environment
|
||||||
|
include:
|
||||||
|
|
||||||
|
* Using welcoming and inclusive language
|
||||||
|
* Being respectful of differing viewpoints and experiences
|
||||||
|
* Gracefully accepting constructive criticism
|
||||||
|
* Focusing on what is best for the community
|
||||||
|
* Showing empathy towards other community members
|
||||||
|
|
||||||
|
Examples of unacceptable behavior by participants include:
|
||||||
|
|
||||||
|
* The use of sexualized language or imagery and unwelcome sexual attention or
|
||||||
|
advances
|
||||||
|
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||||
|
* Public or private harassment
|
||||||
|
* Publishing others' private information, such as a physical or electronic
|
||||||
|
address, without explicit permission
|
||||||
|
* Other conduct which could reasonably be considered inappropriate in a
|
||||||
|
professional setting
|
||||||
|
|
||||||
|
## Our Responsibilities
|
||||||
|
|
||||||
|
Project maintainers are responsible for clarifying the standards of acceptable
|
||||||
|
behavior and are expected to take appropriate and fair corrective action in
|
||||||
|
response to any instances of unacceptable behavior.
|
||||||
|
|
||||||
|
Project maintainers have the right and responsibility to remove, edit, or
|
||||||
|
reject comments, commits, code, wiki edits, issues, and other contributions
|
||||||
|
that are not aligned to this Code of Conduct, or to ban temporarily or
|
||||||
|
permanently any contributor for other behaviors that they deem inappropriate,
|
||||||
|
threatening, offensive, or harmful.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This Code of Conduct applies both within project spaces and in public spaces
|
||||||
|
when an individual is representing the project or its community. Examples of
|
||||||
|
representing a project or community include using an official project e-mail
|
||||||
|
address, posting via an official social media account, or acting as an appointed
|
||||||
|
representative at an online or offline event. Representation of a project may be
|
||||||
|
further defined and clarified by project maintainers.
|
||||||
|
|
||||||
|
## Enforcement
|
||||||
|
|
||||||
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||||
|
reported by contacting the project team at the Tachiyomi [Discord server](https://discord.gg/tachiyomi). All
|
||||||
|
complaints will be reviewed and investigated and will result in a response that
|
||||||
|
is deemed necessary and appropriate to the circumstances. The project team is
|
||||||
|
obligated to maintain confidentiality with regard to the reporter of an incident.
|
||||||
|
Further details of specific enforcement policies may be posted separately.
|
||||||
|
|
||||||
|
Project maintainers who do not follow or enforce the Code of Conduct in good
|
||||||
|
faith may face temporary or permanent repercussions as determined by other
|
||||||
|
members of the project's leadership.
|
||||||
|
|
||||||
|
## Attribution
|
||||||
|
|
||||||
|
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
||||||
|
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
|
||||||
|
|
||||||
|
[homepage]: https://www.contributor-covenant.org
|
||||||
|
|
||||||
|
For answers to common questions about this code of conduct, see
|
||||||
|
https://www.contributor-covenant.org/faq
|
@ -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
|
||||||
@ -63,7 +63,12 @@ Source requests should be created at https://github.com/tachiyomiorg/tachiyomi-e
|
|||||||
|
|
||||||
<details><summary>Contributing</summary>
|
<details><summary>Contributing</summary>
|
||||||
|
|
||||||
See [CONTRIBUTING.md](https://github.com/tachiyomiorg/tachiyomi/blob/master/CONTRIBUTING.md).
|
See [CONTRIBUTING.md](./CONTRIBUTING.md).
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details><summary>Code of Conduct</summary>
|
||||||
|
|
||||||
|
See [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md).
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
## FAQ
|
## FAQ
|
||||||
|
@ -9,7 +9,6 @@ plugins {
|
|||||||
id("com.mikepenz.aboutlibraries.plugin")
|
id("com.mikepenz.aboutlibraries.plugin")
|
||||||
kotlin("android")
|
kotlin("android")
|
||||||
kotlin("kapt")
|
kotlin("kapt")
|
||||||
kotlin("plugin.parcelize")
|
|
||||||
kotlin("plugin.serialization")
|
kotlin("plugin.serialization")
|
||||||
id("com.github.zellius.shortcut-helper")
|
id("com.github.zellius.shortcut-helper")
|
||||||
}
|
}
|
||||||
@ -30,8 +29,8 @@ android {
|
|||||||
minSdkVersion(AndroidConfig.minSdk)
|
minSdkVersion(AndroidConfig.minSdk)
|
||||||
targetSdkVersion(AndroidConfig.targetSdk)
|
targetSdkVersion(AndroidConfig.targetSdk)
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
versionCode = 55
|
versionCode = 57
|
||||||
versionName = "0.10.8"
|
versionName = "0.10.10"
|
||||||
|
|
||||||
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
||||||
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
|
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
|
||||||
@ -92,6 +91,7 @@ android {
|
|||||||
exclude("META-INF/LICENSE")
|
exclude("META-INF/LICENSE")
|
||||||
exclude("META-INF/LICENSE.txt")
|
exclude("META-INF/LICENSE.txt")
|
||||||
exclude("META-INF/NOTICE")
|
exclude("META-INF/NOTICE")
|
||||||
|
exclude("META-INF/*.kotlin_module")
|
||||||
}
|
}
|
||||||
|
|
||||||
dependenciesInfo {
|
dependenciesInfo {
|
||||||
@ -120,20 +120,20 @@ dependencies {
|
|||||||
implementation("tachiyomi.sourceapi:source-api:1.1")
|
implementation("tachiyomi.sourceapi:source-api:1.1")
|
||||||
|
|
||||||
// AndroidX libraries
|
// AndroidX libraries
|
||||||
implementation("androidx.annotation:annotation:1.2.0-beta01")
|
implementation("androidx.annotation:annotation:1.3.0-alpha01")
|
||||||
implementation("androidx.appcompat:appcompat:1.3.0-beta01")
|
implementation("androidx.appcompat:appcompat:1.3.0-rc01")
|
||||||
implementation("androidx.biometric:biometric-ktx:1.2.0-alpha02")
|
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-alpha2")
|
implementation("androidx.constraintlayout:constraintlayout:2.1.0-beta01")
|
||||||
implementation("androidx.coordinatorlayout:coordinatorlayout:1.1.0")
|
implementation("androidx.coordinatorlayout:coordinatorlayout:1.1.0")
|
||||||
implementation("androidx.core:core-ktx:1.5.0-beta01")
|
implementation("androidx.core:core-ktx:1.3.2")
|
||||||
implementation("androidx.multidex:multidex:2.0.1")
|
implementation("androidx.multidex:multidex:2.0.1")
|
||||||
implementation("androidx.preference:preference-ktx:1.1.1")
|
implementation("androidx.preference:preference-ktx:1.1.1")
|
||||||
implementation("androidx.recyclerview:recyclerview:1.2.0-beta01")
|
implementation("androidx.recyclerview:recyclerview:1.2.0")
|
||||||
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01")
|
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01")
|
||||||
|
|
||||||
val lifecycleVersion = "2.3.0-rc01"
|
val lifecycleVersion = "2.3.0"
|
||||||
implementation("androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion")
|
implementation("androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion")
|
||||||
implementation("androidx.lifecycle:lifecycle-process:$lifecycleVersion")
|
implementation("androidx.lifecycle:lifecycle-process:$lifecycleVersion")
|
||||||
implementation("androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion")
|
implementation("androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion")
|
||||||
@ -144,7 +144,7 @@ dependencies {
|
|||||||
// UI library
|
// UI library
|
||||||
implementation("com.google.android.material:material:1.3.0")
|
implementation("com.google.android.material:material:1.3.0")
|
||||||
|
|
||||||
"standardImplementation"("com.google.firebase:firebase-core:18.0.2")
|
"standardImplementation"("com.google.firebase:firebase-core:18.0.3")
|
||||||
|
|
||||||
// ReactiveX
|
// ReactiveX
|
||||||
implementation("io.reactivex:rxandroid:1.2.1")
|
implementation("io.reactivex:rxandroid:1.2.1")
|
||||||
@ -153,7 +153,7 @@ dependencies {
|
|||||||
implementation("com.github.pwittchen:reactivenetwork:0.13.0")
|
implementation("com.github.pwittchen:reactivenetwork:0.13.0")
|
||||||
|
|
||||||
// Network client
|
// Network client
|
||||||
val okhttpVersion = "4.10.0-RC1"
|
val okhttpVersion = "5.0.0-alpha.2"
|
||||||
implementation("com.squareup.okhttp3:okhttp:$okhttpVersion")
|
implementation("com.squareup.okhttp3:okhttp:$okhttpVersion")
|
||||||
implementation("com.squareup.okhttp3:logging-interceptor:$okhttpVersion")
|
implementation("com.squareup.okhttp3:logging-interceptor:$okhttpVersion")
|
||||||
implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:$okhttpVersion")
|
implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:$okhttpVersion")
|
||||||
@ -174,7 +174,7 @@ dependencies {
|
|||||||
|
|
||||||
// Disk
|
// Disk
|
||||||
implementation("com.jakewharton:disklrucache:2.0.2")
|
implementation("com.jakewharton:disklrucache:2.0.2")
|
||||||
implementation("com.github.inorichi:unifile:e9ee588")
|
implementation("com.github.tachiyomiorg:unifile:e9e3a40")
|
||||||
implementation("com.github.junrar:junrar:7.4.0")
|
implementation("com.github.junrar:junrar:7.4.0")
|
||||||
|
|
||||||
// HTML parser
|
// HTML parser
|
||||||
@ -187,7 +187,7 @@ dependencies {
|
|||||||
implementation("io.requery:sqlite-android:3.33.0")
|
implementation("io.requery:sqlite-android:3.33.0")
|
||||||
|
|
||||||
// Preferences
|
// Preferences
|
||||||
implementation("com.github.tfcporciuncula.flow-preferences:flow-preferences:1.3.3")
|
implementation("com.github.tfcporciuncula.flow-preferences:flow-preferences:1.3.4")
|
||||||
|
|
||||||
// Model View Presenter
|
// Model View Presenter
|
||||||
val nucleusVersion = "3.0.0"
|
val nucleusVersion = "3.0.0"
|
||||||
@ -198,14 +198,12 @@ dependencies {
|
|||||||
implementation("com.github.inorichi.injekt:injekt-core:65b0440")
|
implementation("com.github.inorichi.injekt:injekt-core:65b0440")
|
||||||
|
|
||||||
// Image library
|
// Image library
|
||||||
val glideVersion = "4.11.0"
|
val glideVersion = "4.12.0"
|
||||||
implementation("com.github.bumptech.glide:glide:$glideVersion")
|
implementation("com.github.bumptech.glide:glide:$glideVersion")
|
||||||
implementation("com.github.bumptech.glide:okhttp3-integration:$glideVersion")
|
implementation("com.github.bumptech.glide:okhttp3-integration:$glideVersion")
|
||||||
kapt("com.github.bumptech.glide:compiler:$glideVersion")
|
kapt("com.github.bumptech.glide:compiler:$glideVersion")
|
||||||
|
|
||||||
implementation("com.github.tachiyomiorg:subsampling-scale-image-view:6caf219")
|
implementation("com.github.tachiyomiorg:subsampling-scale-image-view:547d9c0")
|
||||||
// TODO: switch to new decoder for stable releases
|
|
||||||
// implementation("com.github.tachiyomiorg:subsampling-scale-image-view:ca26317")
|
|
||||||
|
|
||||||
// Logging
|
// Logging
|
||||||
implementation("com.jakewharton.timber:timber:4.7.1")
|
implementation("com.jakewharton.timber:timber:4.7.1")
|
||||||
@ -223,7 +221,8 @@ dependencies {
|
|||||||
implementation("eu.davidea:flexible-adapter-ui:1.0.0")
|
implementation("eu.davidea:flexible-adapter-ui:1.0.0")
|
||||||
implementation("com.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.1.0")
|
implementation("com.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.1.0")
|
||||||
implementation("com.github.chrisbanes:PhotoView:2.3.0")
|
implementation("com.github.chrisbanes:PhotoView:2.3.0")
|
||||||
implementation("com.github.tachiyomiorg:DirectionalViewPager:7d0617d")
|
implementation("com.github.tachiyomiorg:DirectionalViewPager:1.0.0")
|
||||||
|
implementation("dev.chrisbanes.insetter:insetter:0.5.0")
|
||||||
|
|
||||||
// 3.2.0+ introduces weird UI blinking or cut off issues on some devices
|
// 3.2.0+ introduces weird UI blinking or cut off issues on some devices
|
||||||
val materialDialogsVersion = "3.1.1"
|
val materialDialogsVersion = "3.1.1"
|
||||||
@ -236,7 +235,7 @@ dependencies {
|
|||||||
implementation("com.bluelinelabs:conductor-support:2.1.5") {
|
implementation("com.bluelinelabs:conductor-support:2.1.5") {
|
||||||
exclude(group = "com.android.support")
|
exclude(group = "com.android.support")
|
||||||
}
|
}
|
||||||
implementation("com.github.tachiyomiorg:conductor-support-preference:1.1.1")
|
implementation("com.github.tachiyomiorg:conductor-support-preference:2.0.1")
|
||||||
|
|
||||||
// FlowBinding
|
// FlowBinding
|
||||||
val flowbindingVersion = "0.12.0"
|
val flowbindingVersion = "0.12.0"
|
||||||
@ -250,7 +249,7 @@ dependencies {
|
|||||||
implementation("com.mikepenz:aboutlibraries:${BuildPluginsVersion.ABOUTLIB_PLUGIN}")
|
implementation("com.mikepenz:aboutlibraries:${BuildPluginsVersion.ABOUTLIB_PLUGIN}")
|
||||||
|
|
||||||
// Tests
|
// Tests
|
||||||
testImplementation("junit:junit:4.13.1")
|
testImplementation("junit:junit:4.13.2")
|
||||||
testImplementation("org.assertj:assertj-core:3.16.1")
|
testImplementation("org.assertj:assertj-core:3.16.1")
|
||||||
testImplementation("org.mockito:mockito-core:1.10.19")
|
testImplementation("org.mockito:mockito-core:1.10.19")
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
|||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
import eu.kanade.tachiyomi.data.updater.UpdaterJob
|
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.ui.library.LibrarySort
|
import eu.kanade.tachiyomi.ui.library.LibrarySort
|
||||||
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
|
||||||
@ -127,6 +128,17 @@ object Migrations {
|
|||||||
context.toast(R.string.myanimelist_relogin)
|
context.toast(R.string.myanimelist_relogin)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (oldVersion < 57) {
|
||||||
|
// Migrate DNS over HTTPS setting
|
||||||
|
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
val wasDohEnabled = prefs.getBoolean("enable_doh", false)
|
||||||
|
if (wasDohEnabled) {
|
||||||
|
prefs.edit {
|
||||||
|
putInt(PreferenceKeys.dohProvider, PREF_DOH_CLOUDFLARE)
|
||||||
|
remove("enable_doh")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -80,6 +80,13 @@ abstract class AbstractBackupManager(protected val context: Context) {
|
|||||||
databaseHelper.updateChaptersBackup(chapters).executeAsBlocking()
|
databaseHelper.updateChaptersBackup(chapters).executeAsBlocking()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates a list of chapters with known database ids
|
||||||
|
*/
|
||||||
|
protected fun updateKnownChapters(chapters: List<Chapter>) {
|
||||||
|
databaseHelper.updateKnownChaptersBackup(chapters).executeAsBlocking()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return number of backups.
|
* Return number of backups.
|
||||||
*
|
*
|
||||||
|
@ -37,9 +37,9 @@ abstract class AbstractBackupRestore<T : AbstractBackupManager>(protected val co
|
|||||||
|
|
||||||
protected val errors = mutableListOf<Pair<Date, String>>()
|
protected val errors = mutableListOf<Pair<Date, String>>()
|
||||||
|
|
||||||
abstract fun performRestore(uri: Uri): Boolean
|
abstract suspend fun performRestore(uri: Uri): Boolean
|
||||||
|
|
||||||
fun restoreBackup(uri: Uri): Boolean {
|
suspend fun restoreBackup(uri: Uri): Boolean {
|
||||||
val startTime = System.currentTimeMillis()
|
val startTime = System.currentTimeMillis()
|
||||||
restoreProgress = 0
|
restoreProgress = 0
|
||||||
errors.clear()
|
errors.clear()
|
||||||
|
@ -111,7 +111,7 @@ class BackupCreateService : Service() {
|
|||||||
|
|
||||||
val backupFileUri = backupManager.createBackup(uri, backupFlags, false)?.toUri()
|
val backupFileUri = backupManager.createBackup(uri, backupFlags, false)?.toUri()
|
||||||
val unifile = UniFile.fromUri(this, backupFileUri)
|
val unifile = UniFile.fromUri(this, backupFileUri)
|
||||||
notifier.showBackupComplete(unifile)
|
notifier.showBackupComplete(unifile, backupType == BackupConst.BACKUP_TYPE_LEGACY)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
notifier.showBackupError(e.message)
|
notifier.showBackupError(e.message)
|
||||||
}
|
}
|
||||||
|
@ -24,6 +24,7 @@ class BackupNotifier(private val context: Context) {
|
|||||||
setSmallIcon(R.drawable.ic_tachi)
|
setSmallIcon(R.drawable.ic_tachi)
|
||||||
setAutoCancel(false)
|
setAutoCancel(false)
|
||||||
setOngoing(true)
|
setOngoing(true)
|
||||||
|
setOnlyAlertOnce(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val completeNotificationBuilder = context.notificationBuilder(Notifications.CHANNEL_BACKUP_RESTORE_COMPLETE) {
|
private val completeNotificationBuilder = context.notificationBuilder(Notifications.CHANNEL_BACKUP_RESTORE_COMPLETE) {
|
||||||
@ -41,7 +42,6 @@ class BackupNotifier(private val context: Context) {
|
|||||||
setContentTitle(context.getString(R.string.creating_backup))
|
setContentTitle(context.getString(R.string.creating_backup))
|
||||||
|
|
||||||
setProgress(0, 0, true)
|
setProgress(0, 0, true)
|
||||||
setOnlyAlertOnce(true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
builder.show(Notifications.ID_BACKUP_PROGRESS)
|
builder.show(Notifications.ID_BACKUP_PROGRESS)
|
||||||
@ -60,7 +60,7 @@ class BackupNotifier(private val context: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showBackupComplete(unifile: UniFile) {
|
fun showBackupComplete(unifile: UniFile, isLegacyFormat: Boolean) {
|
||||||
context.notificationManager.cancel(Notifications.ID_BACKUP_PROGRESS)
|
context.notificationManager.cancel(Notifications.ID_BACKUP_PROGRESS)
|
||||||
|
|
||||||
with(completeNotificationBuilder) {
|
with(completeNotificationBuilder) {
|
||||||
@ -73,7 +73,7 @@ class BackupNotifier(private val context: Context) {
|
|||||||
addAction(
|
addAction(
|
||||||
R.drawable.ic_share_24dp,
|
R.drawable.ic_share_24dp,
|
||||||
context.getString(R.string.action_share),
|
context.getString(R.string.action_share),
|
||||||
NotificationReceiver.shareBackupPendingBroadcast(context, unifile.uri, Notifications.ID_BACKUP_COMPLETE)
|
NotificationReceiver.shareBackupPendingBroadcast(context, unifile.uri, isLegacyFormat, Notifications.ID_BACKUP_COMPLETE)
|
||||||
)
|
)
|
||||||
|
|
||||||
show(Notifications.ID_BACKUP_COMPLETE)
|
show(Notifications.ID_BACKUP_COMPLETE)
|
||||||
@ -141,7 +141,7 @@ class BackupNotifier(private val context: Context) {
|
|||||||
|
|
||||||
addAction(
|
addAction(
|
||||||
R.drawable.ic_folder_24dp,
|
R.drawable.ic_folder_24dp,
|
||||||
context.getString(R.string.action_open_log),
|
context.getString(R.string.action_show_errors),
|
||||||
NotificationReceiver.openErrorLogPendingActivity(context, uri)
|
NotificationReceiver.openErrorLogPendingActivity(context, uri)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,10 @@ import eu.kanade.tachiyomi.data.notification.Notifications
|
|||||||
import eu.kanade.tachiyomi.util.system.acquireWakeLock
|
import eu.kanade.tachiyomi.util.system.acquireWakeLock
|
||||||
import eu.kanade.tachiyomi.util.system.isServiceRunning
|
import eu.kanade.tachiyomi.util.system.isServiceRunning
|
||||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
@ -40,12 +43,11 @@ class BackupRestoreService : Service() {
|
|||||||
* @param context context of application
|
* @param context context of application
|
||||||
* @param uri path of Uri
|
* @param uri path of Uri
|
||||||
*/
|
*/
|
||||||
fun start(context: Context, uri: Uri, mode: Int, online: Boolean?) {
|
fun start(context: Context, uri: Uri, mode: Int) {
|
||||||
if (!isRunning(context)) {
|
if (!isRunning(context)) {
|
||||||
val intent = Intent(context, BackupRestoreService::class.java).apply {
|
val intent = Intent(context, BackupRestoreService::class.java).apply {
|
||||||
putExtra(BackupConst.EXTRA_URI, uri)
|
putExtra(BackupConst.EXTRA_URI, uri)
|
||||||
putExtra(BackupConst.EXTRA_MODE, mode)
|
putExtra(BackupConst.EXTRA_MODE, mode)
|
||||||
online?.let { putExtra(BackupConst.EXTRA_TYPE, it) }
|
|
||||||
}
|
}
|
||||||
ContextCompat.startForegroundService(context, intent)
|
ContextCompat.startForegroundService(context, intent)
|
||||||
}
|
}
|
||||||
@ -68,12 +70,14 @@ class BackupRestoreService : Service() {
|
|||||||
*/
|
*/
|
||||||
private lateinit var wakeLock: PowerManager.WakeLock
|
private lateinit var wakeLock: PowerManager.WakeLock
|
||||||
|
|
||||||
|
private lateinit var ioScope: CoroutineScope
|
||||||
private var backupRestore: AbstractBackupRestore<*>? = null
|
private var backupRestore: AbstractBackupRestore<*>? = null
|
||||||
private lateinit var notifier: BackupNotifier
|
private lateinit var notifier: BackupNotifier
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
|
||||||
|
ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||||
notifier = BackupNotifier(this)
|
notifier = BackupNotifier(this)
|
||||||
wakeLock = acquireWakeLock(javaClass.name)
|
wakeLock = acquireWakeLock(javaClass.name)
|
||||||
|
|
||||||
@ -92,6 +96,7 @@ class BackupRestoreService : Service() {
|
|||||||
|
|
||||||
private fun destroyJob() {
|
private fun destroyJob() {
|
||||||
backupRestore?.job?.cancel()
|
backupRestore?.job?.cancel()
|
||||||
|
ioScope?.cancel()
|
||||||
if (wakeLock.isHeld) {
|
if (wakeLock.isHeld) {
|
||||||
wakeLock.release()
|
wakeLock.release()
|
||||||
}
|
}
|
||||||
@ -113,15 +118,15 @@ class BackupRestoreService : Service() {
|
|||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
val uri = intent?.getParcelableExtra<Uri>(BackupConst.EXTRA_URI) ?: return START_NOT_STICKY
|
val uri = intent?.getParcelableExtra<Uri>(BackupConst.EXTRA_URI) ?: return START_NOT_STICKY
|
||||||
val mode = intent.getIntExtra(BackupConst.EXTRA_MODE, BackupConst.BACKUP_TYPE_FULL)
|
val mode = intent.getIntExtra(BackupConst.EXTRA_MODE, BackupConst.BACKUP_TYPE_FULL)
|
||||||
val online = intent.getBooleanExtra(BackupConst.EXTRA_TYPE, true)
|
|
||||||
|
|
||||||
// Cancel any previous job if needed.
|
// Cancel any previous job if needed.
|
||||||
backupRestore?.job?.cancel()
|
backupRestore?.job?.cancel()
|
||||||
|
|
||||||
backupRestore = when (mode) {
|
backupRestore = when (mode) {
|
||||||
BackupConst.BACKUP_TYPE_FULL -> FullBackupRestore(this, notifier, online)
|
BackupConst.BACKUP_TYPE_FULL -> FullBackupRestore(this, notifier)
|
||||||
else -> LegacyBackupRestore(this, notifier)
|
else -> LegacyBackupRestore(this, notifier)
|
||||||
}
|
}
|
||||||
|
|
||||||
val handler = CoroutineExceptionHandler { _, exception ->
|
val handler = CoroutineExceptionHandler { _, exception ->
|
||||||
Timber.e(exception)
|
Timber.e(exception)
|
||||||
backupRestore?.writeErrorLog()
|
backupRestore?.writeErrorLog()
|
||||||
@ -129,14 +134,15 @@ class BackupRestoreService : Service() {
|
|||||||
notifier.showRestoreError(exception.message)
|
notifier.showRestoreError(exception.message)
|
||||||
stopSelf(startId)
|
stopSelf(startId)
|
||||||
}
|
}
|
||||||
backupRestore?.job = GlobalScope.launch(handler) {
|
val job = ioScope.launch(handler) {
|
||||||
if (backupRestore?.restoreBackup(uri) == false) {
|
if (backupRestore?.restoreBackup(uri) == false) {
|
||||||
notifier.showRestoreError(getString(R.string.restoring_backup_canceled))
|
notifier.showRestoreError(getString(R.string.restoring_backup_canceled))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
backupRestore?.job?.invokeOnCompletion {
|
job.invokeOnCompletion {
|
||||||
stopSelf(startId)
|
stopSelf(startId)
|
||||||
}
|
}
|
||||||
|
backupRestore?.job = job
|
||||||
|
|
||||||
return START_NOT_STICKY
|
return START_NOT_STICKY
|
||||||
}
|
}
|
||||||
|
@ -26,10 +26,6 @@ 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.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
import eu.kanade.tachiyomi.data.database.models.toMangaInfo
|
|
||||||
import eu.kanade.tachiyomi.source.Source
|
|
||||||
import eu.kanade.tachiyomi.source.model.toSManga
|
|
||||||
import kotlinx.serialization.ExperimentalSerializationApi
|
|
||||||
import kotlinx.serialization.protobuf.ProtoBuf
|
import kotlinx.serialization.protobuf.ProtoBuf
|
||||||
import okio.buffer
|
import okio.buffer
|
||||||
import okio.gzip
|
import okio.gzip
|
||||||
@ -37,7 +33,6 @@ import okio.sink
|
|||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
|
|
||||||
@OptIn(ExperimentalSerializationApi::class)
|
|
||||||
class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
||||||
|
|
||||||
val parser = ProtoBuf
|
val parser = ProtoBuf
|
||||||
@ -185,24 +180,13 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
|||||||
/**
|
/**
|
||||||
* Fetches manga information
|
* Fetches manga information
|
||||||
*
|
*
|
||||||
* @param source source of manga
|
|
||||||
* @param manga manga that needs updating
|
* @param manga manga that needs updating
|
||||||
* @return Updated manga info.
|
* @return Updated manga info.
|
||||||
*/
|
*/
|
||||||
suspend fun restoreMangaFetch(source: Source?, manga: Manga, online: Boolean): Manga {
|
fun restoreManga(manga: Manga): Manga {
|
||||||
return if (online && source != null) {
|
return manga.also {
|
||||||
val networkManga = source.getMangaDetails(manga.toMangaInfo())
|
it.initialized = it.description != null
|
||||||
manga.also {
|
it.id = insertManga(it)
|
||||||
it.copyFrom(networkManga.toSManga())
|
|
||||||
it.favorite = manga.favorite
|
|
||||||
it.initialized = true
|
|
||||||
it.id = insertManga(manga)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
manga.also {
|
|
||||||
it.initialized = it.description != null
|
|
||||||
it.id = insertManga(it)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -247,7 +231,7 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
|||||||
*/
|
*/
|
||||||
internal fun restoreCategoriesForManga(manga: Manga, categories: List<Int>, backupCategories: List<BackupCategory>) {
|
internal fun restoreCategoriesForManga(manga: Manga, categories: List<Int>, backupCategories: List<BackupCategory>) {
|
||||||
val dbCategories = databaseHelper.getCategories().executeAsBlocking()
|
val dbCategories = databaseHelper.getCategories().executeAsBlocking()
|
||||||
val mangaCategoriesToUpdate = mutableListOf<MangaCategory>()
|
val mangaCategoriesToUpdate = ArrayList<MangaCategory>(categories.size)
|
||||||
categories.forEach { backupCategoryOrder ->
|
categories.forEach { backupCategoryOrder ->
|
||||||
backupCategories.firstOrNull {
|
backupCategories.firstOrNull {
|
||||||
it.order == backupCategoryOrder
|
it.order == backupCategoryOrder
|
||||||
@ -274,7 +258,7 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
|||||||
*/
|
*/
|
||||||
internal fun restoreHistoryForManga(history: List<BackupHistory>) {
|
internal fun restoreHistoryForManga(history: List<BackupHistory>) {
|
||||||
// List containing history to be updated
|
// List containing history to be updated
|
||||||
val historyToBeUpdated = mutableListOf<History>()
|
val historyToBeUpdated = ArrayList<History>(history.size)
|
||||||
for ((url, lastRead) in history) {
|
for ((url, lastRead) in history) {
|
||||||
val dbHistory = databaseHelper.getHistoryByChapterUrl(url).executeAsBlocking()
|
val dbHistory = databaseHelper.getHistoryByChapterUrl(url).executeAsBlocking()
|
||||||
// Check if history already in database and update
|
// Check if history already in database and update
|
||||||
@ -311,29 +295,26 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
|||||||
val trackToUpdate = mutableListOf<Track>()
|
val trackToUpdate = mutableListOf<Track>()
|
||||||
|
|
||||||
tracks.forEach { track ->
|
tracks.forEach { track ->
|
||||||
val service = trackManager.getService(track.sync_id)
|
var isInDatabase = false
|
||||||
if (service != null && service.isLogged) {
|
for (dbTrack in dbTracks) {
|
||||||
var isInDatabase = false
|
if (track.sync_id == dbTrack.sync_id) {
|
||||||
for (dbTrack in dbTracks) {
|
// The sync is already in the db, only update its fields
|
||||||
if (track.sync_id == dbTrack.sync_id) {
|
if (track.media_id != dbTrack.media_id) {
|
||||||
// The sync is already in the db, only update its fields
|
dbTrack.media_id = track.media_id
|
||||||
if (track.media_id != dbTrack.media_id) {
|
|
||||||
dbTrack.media_id = track.media_id
|
|
||||||
}
|
|
||||||
if (track.library_id != dbTrack.library_id) {
|
|
||||||
dbTrack.library_id = track.library_id
|
|
||||||
}
|
|
||||||
dbTrack.last_chapter_read = max(dbTrack.last_chapter_read, track.last_chapter_read)
|
|
||||||
isInDatabase = true
|
|
||||||
trackToUpdate.add(dbTrack)
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
if (track.library_id != dbTrack.library_id) {
|
||||||
|
dbTrack.library_id = track.library_id
|
||||||
|
}
|
||||||
|
dbTrack.last_chapter_read = max(dbTrack.last_chapter_read, track.last_chapter_read)
|
||||||
|
isInDatabase = true
|
||||||
|
trackToUpdate.add(dbTrack)
|
||||||
|
break
|
||||||
}
|
}
|
||||||
if (!isInDatabase) {
|
}
|
||||||
// Insert new sync. Let the db assign the id
|
if (!isInDatabase) {
|
||||||
track.id = null
|
// Insert new sync. Let the db assign the id
|
||||||
trackToUpdate.add(track)
|
track.id = null
|
||||||
}
|
trackToUpdate.add(track)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Update database
|
// Update database
|
||||||
@ -342,25 +323,12 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
internal fun restoreChaptersForManga(manga: Manga, chapters: List<Chapter>) {
|
||||||
* Restore the chapters for manga if chapters already in database
|
|
||||||
*
|
|
||||||
* @param manga manga of chapters
|
|
||||||
* @param chapters list containing chapters that get restored
|
|
||||||
* @return boolean answering if chapter fetch is not needed
|
|
||||||
*/
|
|
||||||
internal fun restoreChaptersForManga(manga: Manga, chapters: List<Chapter>): Boolean {
|
|
||||||
val dbChapters = databaseHelper.getChapters(manga).executeAsBlocking()
|
val dbChapters = databaseHelper.getChapters(manga).executeAsBlocking()
|
||||||
|
|
||||||
// Return if fetch is needed
|
|
||||||
if (dbChapters.isEmpty() || dbChapters.size < chapters.size) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
chapters.forEach { chapter ->
|
chapters.forEach { chapter ->
|
||||||
val pos = dbChapters.indexOfFirst { it.url == chapter.url }
|
val dbChapter = dbChapters.find { it.url == chapter.url }
|
||||||
if (pos != -1) {
|
if (dbChapter != null) {
|
||||||
val dbChapter = dbChapters[pos]
|
|
||||||
chapter.id = dbChapter.id
|
chapter.id = dbChapter.id
|
||||||
chapter.copyFrom(dbChapter)
|
chapter.copyFrom(dbChapter)
|
||||||
if (dbChapter.read && !chapter.read) {
|
if (dbChapter.read && !chapter.read) {
|
||||||
@ -373,38 +341,12 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
|||||||
chapter.bookmark = dbChapter.bookmark
|
chapter.bookmark = dbChapter.bookmark
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
chapter.manga_id = manga.id
|
||||||
}
|
}
|
||||||
// Filter the chapters that couldn't be found.
|
|
||||||
chapters.filter { it.id != null }
|
|
||||||
chapters.map { it.manga_id = manga.id }
|
|
||||||
|
|
||||||
updateChapters(chapters)
|
val newChapters = chapters.groupBy { it.id != null }
|
||||||
return true
|
newChapters[true]?.let { updateKnownChapters(it) }
|
||||||
}
|
newChapters[false]?.let { insertChapters(it) }
|
||||||
|
|
||||||
internal fun restoreChaptersForMangaOffline(manga: Manga, chapters: List<Chapter>) {
|
|
||||||
val dbChapters = databaseHelper.getChapters(manga).executeAsBlocking()
|
|
||||||
|
|
||||||
chapters.forEach { chapter ->
|
|
||||||
val pos = dbChapters.indexOfFirst { it.url == chapter.url }
|
|
||||||
if (pos != -1) {
|
|
||||||
val dbChapter = dbChapters[pos]
|
|
||||||
chapter.id = dbChapter.id
|
|
||||||
chapter.copyFrom(dbChapter)
|
|
||||||
if (dbChapter.read && !chapter.read) {
|
|
||||||
chapter.read = dbChapter.read
|
|
||||||
chapter.last_page_read = dbChapter.last_page_read
|
|
||||||
} else if (chapter.last_page_read == 0 && dbChapter.last_page_read != 0) {
|
|
||||||
chapter.last_page_read = dbChapter.last_page_read
|
|
||||||
}
|
|
||||||
if (!chapter.bookmark && dbChapter.bookmark) {
|
|
||||||
chapter.bookmark = dbChapter.bookmark
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
chapters.map { it.manga_id = manga.id }
|
|
||||||
|
|
||||||
updateChapters(chapters.filter { it.id != null })
|
|
||||||
insertChapters(chapters.filter { it.id == null })
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,18 +12,14 @@ import eu.kanade.tachiyomi.data.backup.full.models.BackupSerializer
|
|||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
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.source.Source
|
|
||||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
|
||||||
import kotlinx.serialization.ExperimentalSerializationApi
|
|
||||||
import okio.buffer
|
import okio.buffer
|
||||||
import okio.gzip
|
import okio.gzip
|
||||||
import okio.source
|
import okio.source
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
|
||||||
@OptIn(ExperimentalSerializationApi::class)
|
class FullBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBackupRestore<FullBackupManager>(context, notifier) {
|
||||||
class FullBackupRestore(context: Context, notifier: BackupNotifier, private val online: Boolean) : AbstractBackupRestore<FullBackupManager>(context, notifier) {
|
|
||||||
|
|
||||||
override fun performRestore(uri: Uri): Boolean {
|
override suspend fun performRestore(uri: Uri): Boolean {
|
||||||
backupManager = FullBackupManager(context)
|
backupManager = FullBackupManager(context)
|
||||||
|
|
||||||
val backupString = context.contentResolver.openInputStream(uri)!!.source().gzip().buffer().use { it.readByteArray() }
|
val backupString = context.contentResolver.openInputStream(uri)!!.source().gzip().buffer().use { it.readByteArray() }
|
||||||
@ -45,9 +41,11 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier, private val
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
restoreManga(it, backup.backupCategories, online)
|
restoreManga(it, backup.backupCategories)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: optionally trigger online library + tracker update
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -60,23 +58,17 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier, private val
|
|||||||
showRestoreProgress(restoreProgress, restoreAmount, context.getString(R.string.categories))
|
showRestoreProgress(restoreProgress, restoreAmount, context.getString(R.string.categories))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun restoreManga(backupManga: BackupManga, backupCategories: List<BackupCategory>, online: Boolean) {
|
private fun restoreManga(backupManga: BackupManga, backupCategories: List<BackupCategory>) {
|
||||||
val manga = backupManga.getMangaImpl()
|
val manga = backupManga.getMangaImpl()
|
||||||
val chapters = backupManga.getChaptersImpl()
|
val chapters = backupManga.getChaptersImpl()
|
||||||
val categories = backupManga.categories
|
val categories = backupManga.categories
|
||||||
val history = backupManga.history
|
val history = backupManga.history
|
||||||
val tracks = backupManga.getTrackingImpl()
|
val tracks = backupManga.getTrackingImpl()
|
||||||
|
|
||||||
val source = backupManager.sourceManager.get(manga.source)
|
|
||||||
val sourceName = sourceMapping[manga.source] ?: manga.source.toString()
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (source != null || !online) {
|
restoreMangaData(manga, chapters, categories, history, tracks, backupCategories)
|
||||||
restoreMangaData(manga, source, chapters, categories, history, tracks, backupCategories, online)
|
|
||||||
} else {
|
|
||||||
errors.add(Date() to "${manga.title} [$sourceName]: ${context.getString(R.string.source_not_found_name, sourceName)}")
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
val sourceName = sourceMapping[manga.source] ?: manga.source.toString()
|
||||||
errors.add(Date() to "${manga.title} [$sourceName]: ${e.message}")
|
errors.add(Date() to "${manga.title} [$sourceName]: ${e.message}")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -88,7 +80,6 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier, private val
|
|||||||
* Returns a manga restore observable
|
* Returns a manga restore observable
|
||||||
*
|
*
|
||||||
* @param manga manga data from json
|
* @param manga manga data from json
|
||||||
* @param source source to get manga data from
|
|
||||||
* @param chapters chapters data from json
|
* @param chapters chapters data from json
|
||||||
* @param categories categories data from json
|
* @param categories categories data from json
|
||||||
* @param history history data from json
|
* @param history history data from json
|
||||||
@ -96,25 +87,23 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier, private val
|
|||||||
*/
|
*/
|
||||||
private fun restoreMangaData(
|
private fun restoreMangaData(
|
||||||
manga: Manga,
|
manga: Manga,
|
||||||
source: Source?,
|
|
||||||
chapters: List<Chapter>,
|
chapters: List<Chapter>,
|
||||||
categories: List<Int>,
|
categories: List<Int>,
|
||||||
history: List<BackupHistory>,
|
history: List<BackupHistory>,
|
||||||
tracks: List<Track>,
|
tracks: List<Track>,
|
||||||
backupCategories: List<BackupCategory>,
|
backupCategories: List<BackupCategory>
|
||||||
online: Boolean
|
|
||||||
) {
|
) {
|
||||||
val dbManga = backupManager.getMangaFromDatabase(manga)
|
|
||||||
|
|
||||||
db.inTransaction {
|
db.inTransaction {
|
||||||
|
val dbManga = backupManager.getMangaFromDatabase(manga)
|
||||||
if (dbManga == null) {
|
if (dbManga == null) {
|
||||||
// Manga not in database
|
// Manga not in database
|
||||||
restoreMangaFetch(source, manga, chapters, categories, history, tracks, backupCategories, online)
|
restoreMangaFetch(manga, chapters, categories, history, tracks, backupCategories)
|
||||||
} else { // Manga in database
|
} else {
|
||||||
|
// Manga in database
|
||||||
// Copy information from manga already in database
|
// Copy information from manga already in database
|
||||||
backupManager.restoreMangaNoFetch(manga, dbManga)
|
backupManager.restoreMangaNoFetch(manga, dbManga)
|
||||||
// Fetch rest of manga information
|
// Fetch rest of manga information
|
||||||
restoreMangaNoFetch(source, manga, chapters, categories, history, tracks, backupCategories, online)
|
restoreMangaNoFetch(manga, chapters, categories, history, tracks, backupCategories)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -127,58 +116,36 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier, private val
|
|||||||
* @param categories categories that need updating
|
* @param categories categories that need updating
|
||||||
*/
|
*/
|
||||||
private fun restoreMangaFetch(
|
private fun restoreMangaFetch(
|
||||||
source: Source?,
|
|
||||||
manga: Manga,
|
manga: Manga,
|
||||||
chapters: List<Chapter>,
|
chapters: List<Chapter>,
|
||||||
categories: List<Int>,
|
categories: List<Int>,
|
||||||
history: List<BackupHistory>,
|
history: List<BackupHistory>,
|
||||||
tracks: List<Track>,
|
tracks: List<Track>,
|
||||||
backupCategories: List<BackupCategory>,
|
backupCategories: List<BackupCategory>
|
||||||
online: Boolean
|
|
||||||
) {
|
) {
|
||||||
launchIO {
|
try {
|
||||||
try {
|
val fetchedManga = backupManager.restoreManga(manga)
|
||||||
val fetchedManga = backupManager.restoreMangaFetch(source, manga, online)
|
fetchedManga.id ?: return
|
||||||
fetchedManga.id ?: (return@launchIO)
|
|
||||||
|
|
||||||
if (online && source != null) {
|
backupManager.restoreChaptersForManga(fetchedManga, chapters)
|
||||||
updateChapters(source, fetchedManga, chapters)
|
|
||||||
} else {
|
|
||||||
backupManager.restoreChaptersForMangaOffline(fetchedManga, chapters)
|
|
||||||
}
|
|
||||||
|
|
||||||
restoreExtraForManga(fetchedManga, categories, history, tracks, backupCategories)
|
restoreExtraForManga(fetchedManga, categories, history, tracks, backupCategories)
|
||||||
|
} catch (e: Exception) {
|
||||||
updateTracking(fetchedManga, tracks)
|
errors.add(Date() to "${manga.title} - ${e.message}")
|
||||||
} catch (e: Exception) {
|
|
||||||
errors.add(Date() to "${manga.title} - ${e.message}")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun restoreMangaNoFetch(
|
private fun restoreMangaNoFetch(
|
||||||
source: Source?,
|
|
||||||
backupManga: Manga,
|
backupManga: Manga,
|
||||||
chapters: List<Chapter>,
|
chapters: List<Chapter>,
|
||||||
categories: List<Int>,
|
categories: List<Int>,
|
||||||
history: List<BackupHistory>,
|
history: List<BackupHistory>,
|
||||||
tracks: List<Track>,
|
tracks: List<Track>,
|
||||||
backupCategories: List<BackupCategory>,
|
backupCategories: List<BackupCategory>
|
||||||
online: Boolean
|
|
||||||
) {
|
) {
|
||||||
launchIO {
|
backupManager.restoreChaptersForManga(backupManga, chapters)
|
||||||
if (online && source != null) {
|
|
||||||
if (!backupManager.restoreChaptersForManga(backupManga, chapters)) {
|
|
||||||
updateChapters(source, backupManga, chapters)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
backupManager.restoreChaptersForMangaOffline(backupManga, chapters)
|
|
||||||
}
|
|
||||||
|
|
||||||
restoreExtraForManga(backupManga, categories, history, tracks, backupCategories)
|
restoreExtraForManga(backupManga, categories, history, tracks, backupCategories)
|
||||||
|
|
||||||
updateTracking(backupManga, tracks)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun restoreExtraForManga(manga: Manga, categories: List<Int>, history: List<BackupHistory>, tracks: List<Track>, backupCategories: List<BackupCategory>) {
|
private fun restoreExtraForManga(manga: Manga, categories: List<Int>, history: List<BackupHistory>, tracks: List<Track>, backupCategories: List<BackupCategory>) {
|
||||||
|
@ -5,12 +5,10 @@ import android.net.Uri
|
|||||||
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.full.models.BackupSerializer
|
import eu.kanade.tachiyomi.data.backup.full.models.BackupSerializer
|
||||||
import kotlinx.serialization.ExperimentalSerializationApi
|
|
||||||
import okio.buffer
|
import okio.buffer
|
||||||
import okio.gzip
|
import okio.gzip
|
||||||
import okio.source
|
import okio.source
|
||||||
|
|
||||||
@OptIn(ExperimentalSerializationApi::class)
|
|
||||||
class FullBackupRestoreValidator : AbstractBackupRestoreValidator() {
|
class FullBackupRestoreValidator : AbstractBackupRestoreValidator() {
|
||||||
/**
|
/**
|
||||||
* Checks for critical backup file data.
|
* Checks for critical backup file data.
|
||||||
|
@ -53,30 +53,14 @@ import kotlin.math.max
|
|||||||
|
|
||||||
class LegacyBackupManager(context: Context, version: Int = CURRENT_VERSION) : AbstractBackupManager(context) {
|
class LegacyBackupManager(context: Context, version: Int = CURRENT_VERSION) : AbstractBackupManager(context) {
|
||||||
|
|
||||||
var parserVersion: Int = version
|
val parser: Gson = when (version) {
|
||||||
private set
|
2 -> GsonBuilder()
|
||||||
|
.registerTypeAdapter<MangaImpl>(MangaTypeAdapter.build())
|
||||||
var parser: Gson = initParser()
|
.registerTypeHierarchyAdapter<ChapterImpl>(ChapterTypeAdapter.build())
|
||||||
|
.registerTypeAdapter<CategoryImpl>(CategoryTypeAdapter.build())
|
||||||
/**
|
.registerTypeAdapter<DHistory>(HistoryTypeAdapter.build())
|
||||||
* Set version of parser
|
.registerTypeHierarchyAdapter<TrackImpl>(TrackTypeAdapter.build())
|
||||||
*
|
.create()
|
||||||
* @param version version of parser
|
|
||||||
*/
|
|
||||||
internal fun setVersion(version: Int) {
|
|
||||||
this.parserVersion = version
|
|
||||||
parser = initParser()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initParser(): Gson = when (parserVersion) {
|
|
||||||
2 ->
|
|
||||||
GsonBuilder()
|
|
||||||
.registerTypeAdapter<MangaImpl>(MangaTypeAdapter.build())
|
|
||||||
.registerTypeHierarchyAdapter<ChapterImpl>(ChapterTypeAdapter.build())
|
|
||||||
.registerTypeAdapter<CategoryImpl>(CategoryTypeAdapter.build())
|
|
||||||
.registerTypeAdapter<DHistory>(HistoryTypeAdapter.build())
|
|
||||||
.registerTypeHierarchyAdapter<TrackImpl>(TrackTypeAdapter.build())
|
|
||||||
.create()
|
|
||||||
else -> throw Exception("Unknown backup version")
|
else -> throw Exception("Unknown backup version")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -308,7 +292,7 @@ class LegacyBackupManager(context: Context, version: Int = CURRENT_VERSION) : Ab
|
|||||||
*/
|
*/
|
||||||
internal fun restoreCategoriesForManga(manga: Manga, categories: List<String>) {
|
internal fun restoreCategoriesForManga(manga: Manga, categories: List<String>) {
|
||||||
val dbCategories = databaseHelper.getCategories().executeAsBlocking()
|
val dbCategories = databaseHelper.getCategories().executeAsBlocking()
|
||||||
val mangaCategoriesToUpdate = mutableListOf<MangaCategory>()
|
val mangaCategoriesToUpdate = ArrayList<MangaCategory>(categories.size)
|
||||||
for (backupCategoryStr in categories) {
|
for (backupCategoryStr in categories) {
|
||||||
for (dbCategory in dbCategories) {
|
for (dbCategory in dbCategories) {
|
||||||
if (backupCategoryStr == dbCategory.name) {
|
if (backupCategoryStr == dbCategory.name) {
|
||||||
@ -332,7 +316,7 @@ class LegacyBackupManager(context: Context, version: Int = CURRENT_VERSION) : Ab
|
|||||||
*/
|
*/
|
||||||
internal fun restoreHistoryForManga(history: List<DHistory>) {
|
internal fun restoreHistoryForManga(history: List<DHistory>) {
|
||||||
// List containing history to be updated
|
// List containing history to be updated
|
||||||
val historyToBeUpdated = mutableListOf<History>()
|
val historyToBeUpdated = ArrayList<History>(history.size)
|
||||||
for ((url, lastRead) in history) {
|
for ((url, lastRead) in history) {
|
||||||
val dbHistory = databaseHelper.getHistoryByChapterUrl(url).executeAsBlocking()
|
val dbHistory = databaseHelper.getHistoryByChapterUrl(url).executeAsBlocking()
|
||||||
// Check if history already in database and update
|
// Check if history already in database and update
|
||||||
@ -361,14 +345,14 @@ class LegacyBackupManager(context: Context, version: Int = CURRENT_VERSION) : Ab
|
|||||||
* @param tracks the track list to restore.
|
* @param tracks the track list to restore.
|
||||||
*/
|
*/
|
||||||
internal fun restoreTrackForManga(manga: Manga, tracks: List<Track>) {
|
internal fun restoreTrackForManga(manga: Manga, tracks: List<Track>) {
|
||||||
// Fix foreign keys with the current manga id
|
|
||||||
tracks.map { it.manga_id = manga.id!! }
|
|
||||||
|
|
||||||
// Get tracks from database
|
// Get tracks from database
|
||||||
val dbTracks = databaseHelper.getTracks(manga).executeAsBlocking()
|
val dbTracks = databaseHelper.getTracks(manga).executeAsBlocking()
|
||||||
val trackToUpdate = mutableListOf<Track>()
|
val trackToUpdate = ArrayList<Track>(tracks.size)
|
||||||
|
|
||||||
tracks.forEach { track ->
|
tracks.forEach { track ->
|
||||||
|
// Fix foreign keys with the current manga id
|
||||||
|
track.manga_id = manga.id!!
|
||||||
|
|
||||||
val service = trackManager.getService(track.sync_id)
|
val service = trackManager.getService(track.sync_id)
|
||||||
if (service != null && service.isLogged) {
|
if (service != null && service.isLogged) {
|
||||||
var isInDatabase = false
|
var isInDatabase = false
|
||||||
@ -423,12 +407,13 @@ class LegacyBackupManager(context: Context, version: Int = CURRENT_VERSION) : Ab
|
|||||||
chapter.copyFrom(dbChapter)
|
chapter.copyFrom(dbChapter)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
|
||||||
// Filter the chapters that couldn't be found.
|
|
||||||
chapters.filter { it.id != null }
|
|
||||||
chapters.map { it.manga_id = manga.id }
|
|
||||||
|
|
||||||
updateChapters(chapters)
|
chapter.manga_id = manga.id
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter the chapters that couldn't be found.
|
||||||
|
updateChapters(chapters.filter { it.id != null })
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,12 +21,11 @@ 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.TrackImpl
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
|
||||||
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 fun performRestore(uri: Uri): Boolean {
|
override suspend fun performRestore(uri: Uri): Boolean {
|
||||||
val reader = JsonReader(context.contentResolver.openInputStream(uri)!!.bufferedReader())
|
val reader = JsonReader(context.contentResolver.openInputStream(uri)!!.bufferedReader())
|
||||||
val json = JsonParser.parseReader(reader).asJsonObject
|
val json = JsonParser.parseReader(reader).asJsonObject
|
||||||
|
|
||||||
@ -63,7 +62,7 @@ class LegacyBackupRestore(context: Context, notifier: BackupNotifier) : Abstract
|
|||||||
showRestoreProgress(restoreProgress, restoreAmount, context.getString(R.string.categories))
|
showRestoreProgress(restoreProgress, restoreAmount, context.getString(R.string.categories))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun restoreManga(mangaJson: JsonObject) {
|
private suspend fun restoreManga(mangaJson: JsonObject) {
|
||||||
val manga = backupManager.parser.fromJson<MangaImpl>(
|
val manga = backupManager.parser.fromJson<MangaImpl>(
|
||||||
mangaJson.get(
|
mangaJson.get(
|
||||||
Backup.MANGA
|
Backup.MANGA
|
||||||
@ -113,7 +112,7 @@ class LegacyBackupRestore(context: Context, notifier: BackupNotifier) : Abstract
|
|||||||
* @param history history data from json
|
* @param history history data from json
|
||||||
* @param tracks tracking data from json
|
* @param tracks tracking data from json
|
||||||
*/
|
*/
|
||||||
private fun restoreMangaData(
|
private suspend fun restoreMangaData(
|
||||||
manga: Manga,
|
manga: Manga,
|
||||||
source: Source,
|
source: Source,
|
||||||
chapters: List<Chapter>,
|
chapters: List<Chapter>,
|
||||||
@ -143,7 +142,7 @@ class LegacyBackupRestore(context: Context, notifier: BackupNotifier) : Abstract
|
|||||||
* @param chapters chapters of manga that needs updating
|
* @param chapters chapters of manga that needs updating
|
||||||
* @param categories categories that need updating
|
* @param categories categories that need updating
|
||||||
*/
|
*/
|
||||||
private fun restoreMangaFetch(
|
private suspend fun restoreMangaFetch(
|
||||||
source: Source,
|
source: Source,
|
||||||
manga: Manga,
|
manga: Manga,
|
||||||
chapters: List<Chapter>,
|
chapters: List<Chapter>,
|
||||||
@ -151,23 +150,21 @@ class LegacyBackupRestore(context: Context, notifier: BackupNotifier) : Abstract
|
|||||||
history: List<DHistory>,
|
history: List<DHistory>,
|
||||||
tracks: List<Track>
|
tracks: List<Track>
|
||||||
) {
|
) {
|
||||||
launchIO {
|
try {
|
||||||
try {
|
val fetchedManga = backupManager.fetchManga(source, manga)
|
||||||
val fetchedManga = backupManager.fetchManga(source, manga)
|
fetchedManga.id ?: return
|
||||||
fetchedManga.id ?: (return@launchIO)
|
|
||||||
|
|
||||||
updateChapters(source, fetchedManga, chapters)
|
updateChapters(source, fetchedManga, chapters)
|
||||||
|
|
||||||
restoreExtraForManga(fetchedManga, categories, history, tracks)
|
restoreExtraForManga(fetchedManga, categories, history, tracks)
|
||||||
|
|
||||||
updateTracking(fetchedManga, tracks)
|
updateTracking(fetchedManga, tracks)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
errors.add(Date() to "${manga.title} - ${e.message}")
|
errors.add(Date() to "${manga.title} - ${e.message}")
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun restoreMangaNoFetch(
|
private suspend fun restoreMangaNoFetch(
|
||||||
source: Source,
|
source: Source,
|
||||||
backupManga: Manga,
|
backupManga: Manga,
|
||||||
chapters: List<Chapter>,
|
chapters: List<Chapter>,
|
||||||
@ -175,15 +172,13 @@ class LegacyBackupRestore(context: Context, notifier: BackupNotifier) : Abstract
|
|||||||
history: List<DHistory>,
|
history: List<DHistory>,
|
||||||
tracks: List<Track>
|
tracks: List<Track>
|
||||||
) {
|
) {
|
||||||
launchIO {
|
if (!backupManager.restoreChaptersForManga(backupManga, chapters)) {
|
||||||
if (!backupManager.restoreChaptersForManga(backupManga, chapters)) {
|
updateChapters(source, backupManga, chapters)
|
||||||
updateChapters(source, backupManga, chapters)
|
|
||||||
}
|
|
||||||
|
|
||||||
restoreExtraForManga(backupManga, categories, history, tracks)
|
|
||||||
|
|
||||||
updateTracking(backupManga, tracks)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
restoreExtraForManga(backupManga, categories, history, tracks)
|
||||||
|
|
||||||
|
updateTracking(backupManga, tracks)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun restoreExtraForManga(manga: Manga, categories: List<String>, history: List<DHistory>, tracks: List<Track>) {
|
private fun restoreExtraForManga(manga: Manga, categories: List<String>, history: List<DHistory>, tracks: List<Track>) {
|
||||||
|
@ -7,6 +7,7 @@ import eu.kanade.tachiyomi.data.database.models.Chapter
|
|||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaChapter
|
import eu.kanade.tachiyomi.data.database.models.MangaChapter
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.ChapterBackupPutResolver
|
import eu.kanade.tachiyomi.data.database.resolvers.ChapterBackupPutResolver
|
||||||
|
import eu.kanade.tachiyomi.data.database.resolvers.ChapterKnownBackupPutResolver
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.ChapterProgressPutResolver
|
import eu.kanade.tachiyomi.data.database.resolvers.ChapterProgressPutResolver
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.ChapterSourceOrderPutResolver
|
import eu.kanade.tachiyomi.data.database.resolvers.ChapterSourceOrderPutResolver
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaChapterGetResolver
|
import eu.kanade.tachiyomi.data.database.resolvers.MangaChapterGetResolver
|
||||||
@ -84,6 +85,11 @@ interface ChapterQueries : DbProvider {
|
|||||||
.withPutResolver(ChapterBackupPutResolver())
|
.withPutResolver(ChapterBackupPutResolver())
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
|
fun updateKnownChaptersBackup(chapters: List<Chapter>) = db.put()
|
||||||
|
.objects(chapters)
|
||||||
|
.withPutResolver(ChapterKnownBackupPutResolver())
|
||||||
|
.prepare()
|
||||||
|
|
||||||
fun updateChapterProgress(chapter: Chapter) = db.put()
|
fun updateChapterProgress(chapter: Chapter) = db.put()
|
||||||
.`object`(chapter)
|
.`object`(chapter)
|
||||||
.withPutResolver(ChapterProgressPutResolver())
|
.withPutResolver(ChapterProgressPutResolver())
|
||||||
|
@ -164,4 +164,14 @@ interface MangaQueries : DbProvider {
|
|||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
|
fun getChapterFetchDateManga() = db.get()
|
||||||
|
.listOfObjects(Manga::class.java)
|
||||||
|
.withQuery(
|
||||||
|
RawQuery.builder()
|
||||||
|
.query(getChapterFetchDateMangaQuery())
|
||||||
|
.observesTables(MangaTable.TABLE)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
.prepare()
|
||||||
}
|
}
|
||||||
|
@ -122,6 +122,16 @@ fun getLatestChapterMangaQuery() =
|
|||||||
ORDER by max DESC
|
ORDER by max DESC
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
fun getChapterFetchDateMangaQuery() =
|
||||||
|
"""
|
||||||
|
SELECT ${Manga.TABLE}.*, MAX(${Chapter.TABLE}.${Chapter.COL_DATE_FETCH}) AS max
|
||||||
|
FROM ${Manga.TABLE}
|
||||||
|
JOIN ${Chapter.TABLE}
|
||||||
|
ON ${Manga.TABLE}.${Manga.COL_ID} = ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}
|
||||||
|
GROUP BY ${Manga.TABLE}.${Manga.COL_ID}
|
||||||
|
ORDER by max DESC
|
||||||
|
"""
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Query to get the categories for a manga.
|
* Query to get the categories for a manga.
|
||||||
*/
|
*/
|
||||||
|
@ -0,0 +1,34 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.database.resolvers
|
||||||
|
|
||||||
|
import androidx.core.content.contentValuesOf
|
||||||
|
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.Chapter
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.ChapterTable
|
||||||
|
|
||||||
|
class ChapterKnownBackupPutResolver : PutResolver<Chapter>() {
|
||||||
|
|
||||||
|
override fun performPut(db: StorIOSQLite, chapter: Chapter) = db.inTransactionReturn {
|
||||||
|
val updateQuery = mapToUpdateQuery(chapter)
|
||||||
|
val contentValues = mapToContentValues(chapter)
|
||||||
|
|
||||||
|
val numberOfRowsUpdated = db.lowLevel().update(updateQuery, contentValues)
|
||||||
|
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun mapToUpdateQuery(chapter: Chapter) = UpdateQuery.builder()
|
||||||
|
.table(ChapterTable.TABLE)
|
||||||
|
.where("${ChapterTable.COL_ID} = ?")
|
||||||
|
.whereArgs(chapter.id)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
fun mapToContentValues(chapter: Chapter) =
|
||||||
|
contentValuesOf(
|
||||||
|
ChapterTable.COL_READ to chapter.read,
|
||||||
|
ChapterTable.COL_BOOKMARK to chapter.bookmark,
|
||||||
|
ChapterTable.COL_LAST_PAGE_READ to chapter.last_page_read
|
||||||
|
)
|
||||||
|
}
|
@ -12,6 +12,7 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
|||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
|
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
@ -211,16 +212,16 @@ class DownloadManager(private val context: Context) {
|
|||||||
*/
|
*/
|
||||||
fun deleteChapters(chapters: List<Chapter>, manga: Manga, source: Source): List<Chapter> {
|
fun deleteChapters(chapters: List<Chapter>, manga: Manga, source: Source): List<Chapter> {
|
||||||
val filteredChapters = getChaptersToDelete(chapters)
|
val filteredChapters = getChaptersToDelete(chapters)
|
||||||
|
launchIO {
|
||||||
|
removeFromDownloadQueue(filteredChapters)
|
||||||
|
|
||||||
removeFromDownloadQueue(filteredChapters)
|
val chapterDirs = provider.findChapterDirs(filteredChapters, manga, source)
|
||||||
|
chapterDirs.forEach { it.delete() }
|
||||||
val chapterDirs = provider.findChapterDirs(filteredChapters, manga, source)
|
cache.removeChapters(filteredChapters, manga)
|
||||||
chapterDirs.forEach { it.delete() }
|
if (cache.getDownloadCount(manga) == 0) { // Delete manga directory if empty
|
||||||
cache.removeChapters(filteredChapters, manga)
|
chapterDirs.firstOrNull()?.parentFile?.delete()
|
||||||
if (cache.getDownloadCount(manga) == 0) { // Delete manga directory if empty
|
}
|
||||||
chapterDirs.firstOrNull()?.parentFile?.delete()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return filteredChapters
|
return filteredChapters
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -249,9 +250,11 @@ class DownloadManager(private val context: Context) {
|
|||||||
* @param source the source of the manga.
|
* @param source the source of the manga.
|
||||||
*/
|
*/
|
||||||
fun deleteManga(manga: Manga, source: Source) {
|
fun deleteManga(manga: Manga, source: Source) {
|
||||||
downloader.queue.remove(manga)
|
launchIO {
|
||||||
provider.findMangaDir(manga, source)?.delete()
|
downloader.queue.remove(manga)
|
||||||
cache.removeManga(manga)
|
provider.findMangaDir(manga, source)?.delete()
|
||||||
|
cache.removeManga(manga)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -27,6 +27,9 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
private val progressNotificationBuilder by lazy {
|
private val progressNotificationBuilder by lazy {
|
||||||
context.notificationBuilder(Notifications.CHANNEL_DOWNLOADER_PROGRESS) {
|
context.notificationBuilder(Notifications.CHANNEL_DOWNLOADER_PROGRESS) {
|
||||||
setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher))
|
setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher))
|
||||||
|
setAutoCancel(false)
|
||||||
|
setOngoing(true)
|
||||||
|
setOnlyAlertOnce(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -84,7 +87,6 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
// Check if first call.
|
// Check if first call.
|
||||||
if (!isDownloading) {
|
if (!isDownloading) {
|
||||||
setSmallIcon(android.R.drawable.stat_sys_download)
|
setSmallIcon(android.R.drawable.stat_sys_download)
|
||||||
setAutoCancel(false)
|
|
||||||
clearActions()
|
clearActions()
|
||||||
// Open download manager when clicked
|
// Open download manager when clicked
|
||||||
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
||||||
@ -127,7 +129,6 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
setContentTitle(context.getString(R.string.chapter_paused))
|
setContentTitle(context.getString(R.string.chapter_paused))
|
||||||
setContentText(context.getString(R.string.download_notifier_download_paused))
|
setContentText(context.getString(R.string.download_notifier_download_paused))
|
||||||
setSmallIcon(R.drawable.ic_pause_24dp)
|
setSmallIcon(R.drawable.ic_pause_24dp)
|
||||||
setAutoCancel(false)
|
|
||||||
setProgress(0, 0, false)
|
setProgress(0, 0, false)
|
||||||
clearActions()
|
clearActions()
|
||||||
// Open download manager when clicked
|
// Open download manager when clicked
|
||||||
@ -217,7 +218,6 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
setContentText(error ?: context.getString(R.string.download_notifier_unknown_error))
|
setContentText(error ?: context.getString(R.string.download_notifier_unknown_error))
|
||||||
setSmallIcon(android.R.drawable.stat_sys_warning)
|
setSmallIcon(android.R.drawable.stat_sys_warning)
|
||||||
clearActions()
|
clearActions()
|
||||||
setAutoCancel(false)
|
|
||||||
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
||||||
setProgress(0, 0, false)
|
setProgress(0, 0, false)
|
||||||
|
|
||||||
|
@ -53,8 +53,8 @@ class DownloadProvider(private val context: Context) {
|
|||||||
return downloadsDir
|
return downloadsDir
|
||||||
.createDirectory(getSourceDirName(source))
|
.createDirectory(getSourceDirName(source))
|
||||||
.createDirectory(getMangaDirName(manga))
|
.createDirectory(getMangaDirName(manga))
|
||||||
} catch (e: NullPointerException) {
|
} catch (e: Throwable) {
|
||||||
Timber.w(e)
|
Timber.e(e, "Invalid download directory")
|
||||||
throw Exception(context.getString(R.string.invalid_download_dir))
|
throw Exception(context.getString(R.string.invalid_download_dir))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,8 +16,8 @@ import eu.kanade.tachiyomi.source.model.Page
|
|||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.source.online.fetchAllImageUrlsFromPageList
|
import eu.kanade.tachiyomi.source.online.fetchAllImageUrlsFromPageList
|
||||||
import eu.kanade.tachiyomi.util.lang.RetryWithDelay
|
import eu.kanade.tachiyomi.util.lang.RetryWithDelay
|
||||||
|
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||||
import eu.kanade.tachiyomi.util.lang.launchNow
|
import eu.kanade.tachiyomi.util.lang.launchNow
|
||||||
import eu.kanade.tachiyomi.util.lang.launchUI
|
|
||||||
import eu.kanade.tachiyomi.util.lang.plusAssign
|
import eu.kanade.tachiyomi.util.lang.plusAssign
|
||||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||||
import eu.kanade.tachiyomi.util.storage.saveTo
|
import eu.kanade.tachiyomi.util.storage.saveTo
|
||||||
@ -228,8 +228,8 @@ class Downloader(
|
|||||||
* @param chapters the list of chapters to download.
|
* @param chapters the list of chapters to download.
|
||||||
* @param autoStart whether to start the downloader after enqueing the chapters.
|
* @param autoStart whether to start the downloader after enqueing the chapters.
|
||||||
*/
|
*/
|
||||||
fun queueChapters(manga: Manga, chapters: List<Chapter>, autoStart: Boolean) = launchUI {
|
fun queueChapters(manga: Manga, chapters: List<Chapter>, autoStart: Boolean) = launchIO {
|
||||||
val source = sourceManager.get(manga.source) as? HttpSource ?: return@launchUI
|
val source = sourceManager.get(manga.source) as? HttpSource ?: return@launchIO
|
||||||
val wasEmpty = queue.isEmpty()
|
val wasEmpty = queue.isEmpty()
|
||||||
// Called in background thread, the operation can be slow with SAF.
|
// Called in background thread, the operation can be slow with SAF.
|
||||||
val chaptersWithoutDir = async {
|
val chaptersWithoutDir = async {
|
||||||
|
@ -110,7 +110,7 @@ class LibraryUpdateNotifier(private val context: Context) {
|
|||||||
setContentIntent(errorLogIntent)
|
setContentIntent(errorLogIntent)
|
||||||
addAction(
|
addAction(
|
||||||
R.drawable.ic_folder_24dp,
|
R.drawable.ic_folder_24dp,
|
||||||
context.getString(R.string.action_open_log),
|
context.getString(R.string.action_show_errors),
|
||||||
errorLogIntent
|
errorLogIntent
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -33,10 +33,10 @@ import eu.kanade.tachiyomi.util.storage.getUriCompat
|
|||||||
import eu.kanade.tachiyomi.util.system.acquireWakeLock
|
import eu.kanade.tachiyomi.util.system.acquireWakeLock
|
||||||
import eu.kanade.tachiyomi.util.system.createFileInCacheDir
|
import eu.kanade.tachiyomi.util.system.createFileInCacheDir
|
||||||
import eu.kanade.tachiyomi.util.system.isServiceRunning
|
import eu.kanade.tachiyomi.util.system.isServiceRunning
|
||||||
import kotlinx.coroutines.CancellationException
|
|
||||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
@ -71,6 +71,7 @@ class LibraryUpdateService(
|
|||||||
private lateinit var notifier: LibraryUpdateNotifier
|
private lateinit var notifier: LibraryUpdateNotifier
|
||||||
private lateinit var ioScope: CoroutineScope
|
private lateinit var ioScope: CoroutineScope
|
||||||
|
|
||||||
|
private var mangaToUpdate: List<LibraryManga> = mutableListOf()
|
||||||
private var updateJob: Job? = null
|
private var updateJob: Job? = null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -86,6 +87,8 @@ class LibraryUpdateService(
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
|
private var instance: LibraryUpdateService? = null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Key for category to update.
|
* Key for category to update.
|
||||||
*/
|
*/
|
||||||
@ -116,17 +119,18 @@ class LibraryUpdateService(
|
|||||||
* @return true if service newly started, false otherwise
|
* @return true if service newly started, false otherwise
|
||||||
*/
|
*/
|
||||||
fun start(context: Context, category: Category? = null, target: Target = Target.CHAPTERS): Boolean {
|
fun start(context: Context, category: Category? = null, target: Target = Target.CHAPTERS): Boolean {
|
||||||
if (!isRunning(context)) {
|
return if (!isRunning(context)) {
|
||||||
val intent = Intent(context, LibraryUpdateService::class.java).apply {
|
val intent = Intent(context, LibraryUpdateService::class.java).apply {
|
||||||
putExtra(KEY_TARGET, target)
|
putExtra(KEY_TARGET, target)
|
||||||
category?.let { putExtra(KEY_CATEGORY, it.id) }
|
category?.let { putExtra(KEY_CATEGORY, it.id) }
|
||||||
}
|
}
|
||||||
ContextCompat.startForegroundService(context, intent)
|
ContextCompat.startForegroundService(context, intent)
|
||||||
|
|
||||||
return true
|
true
|
||||||
|
} else {
|
||||||
|
instance?.addMangaToQueue(category?.id ?: -1, target)
|
||||||
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -158,11 +162,14 @@ class LibraryUpdateService(
|
|||||||
* lock.
|
* lock.
|
||||||
*/
|
*/
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
ioScope?.cancel()
|
|
||||||
updateJob?.cancel()
|
updateJob?.cancel()
|
||||||
|
ioScope?.cancel()
|
||||||
if (wakeLock.isHeld) {
|
if (wakeLock.isHeld) {
|
||||||
wakeLock.release()
|
wakeLock.release()
|
||||||
}
|
}
|
||||||
|
if (instance == this) {
|
||||||
|
instance = null
|
||||||
|
}
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -186,23 +193,25 @@ class LibraryUpdateService(
|
|||||||
val target = intent.getSerializableExtra(KEY_TARGET) as? Target
|
val target = intent.getSerializableExtra(KEY_TARGET) as? Target
|
||||||
?: return START_NOT_STICKY
|
?: return START_NOT_STICKY
|
||||||
|
|
||||||
// Unsubscribe from any previous subscription if needed.
|
instance = this
|
||||||
|
|
||||||
|
// Unsubscribe from any previous subscription if needed
|
||||||
updateJob?.cancel()
|
updateJob?.cancel()
|
||||||
|
|
||||||
// Update favorite manga. Destroy service when completed or in case of an error.
|
// Update favorite manga
|
||||||
val selectedScheme = preferences.libraryUpdatePrioritization().get()
|
val categoryId = intent.getIntExtra(KEY_CATEGORY, -1)
|
||||||
val mangaList = getMangaToUpdate(intent, target)
|
addMangaToQueue(categoryId, target)
|
||||||
.sortedWith(rankingScheme[selectedScheme])
|
|
||||||
|
|
||||||
|
// Destroy service when completed or in case of an error.
|
||||||
val handler = CoroutineExceptionHandler { _, exception ->
|
val handler = CoroutineExceptionHandler { _, exception ->
|
||||||
Timber.e(exception)
|
Timber.e(exception)
|
||||||
stopSelf(startId)
|
stopSelf(startId)
|
||||||
}
|
}
|
||||||
updateJob = ioScope.launch(handler) {
|
updateJob = ioScope.launch(handler) {
|
||||||
when (target) {
|
when (target) {
|
||||||
Target.CHAPTERS -> updateChapterList(mangaList)
|
Target.CHAPTERS -> updateChapterList()
|
||||||
Target.COVERS -> updateCovers(mangaList)
|
Target.COVERS -> updateCovers()
|
||||||
Target.TRACKING -> updateTrackings(mangaList)
|
Target.TRACKING -> updateTrackings()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
updateJob?.invokeOnCompletion { stopSelf(startId) }
|
updateJob?.invokeOnCompletion { stopSelf(startId) }
|
||||||
@ -211,32 +220,41 @@ class LibraryUpdateService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the list of manga to be updated.
|
* Adds list of manga to be updated.
|
||||||
*
|
*
|
||||||
* @param intent the update intent.
|
* @param category the ID of the category to update, or -1 if no category specified.
|
||||||
* @param target the target to update.
|
* @param target the target to update.
|
||||||
* @return a list of manga to update
|
|
||||||
*/
|
*/
|
||||||
fun getMangaToUpdate(intent: Intent, target: Target): List<LibraryManga> {
|
fun addMangaToQueue(categoryId: Int, target: Target) {
|
||||||
val categoryId = intent.getIntExtra(KEY_CATEGORY, -1)
|
val libraryManga = db.getLibraryMangas().executeAsBlocking()
|
||||||
|
|
||||||
var listToUpdate = if (categoryId != -1) {
|
var listToUpdate = if (categoryId != -1) {
|
||||||
db.getLibraryMangas().executeAsBlocking().filter { it.category == categoryId }
|
libraryManga.filter { it.category == categoryId }
|
||||||
} else {
|
} else {
|
||||||
val categoriesToUpdate = preferences.libraryUpdateCategories().get().map(String::toInt)
|
val categoriesToUpdate = preferences.libraryUpdateCategories().get().map(String::toInt)
|
||||||
if (categoriesToUpdate.isNotEmpty()) {
|
val listToInclude = if (categoriesToUpdate.isNotEmpty()) {
|
||||||
db.getLibraryMangas().executeAsBlocking()
|
libraryManga.filter { it.category in categoriesToUpdate }
|
||||||
.filter { it.category in categoriesToUpdate }
|
|
||||||
.distinctBy { it.id }
|
|
||||||
} else {
|
} else {
|
||||||
db.getLibraryMangas().executeAsBlocking().distinctBy { it.id }
|
libraryManga
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val categoriesToExclude = preferences.libraryUpdateCategoriesExclude().get().map(String::toInt)
|
||||||
|
val listToExclude = if (categoriesToExclude.isNotEmpty()) {
|
||||||
|
libraryManga.filter { it.category in categoriesToExclude }
|
||||||
|
} else {
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
listToInclude.minus(listToExclude)
|
||||||
}
|
}
|
||||||
if (target == Target.CHAPTERS && preferences.updateOnlyNonCompleted()) {
|
if (target == Target.CHAPTERS && preferences.updateOnlyNonCompleted()) {
|
||||||
listToUpdate = listToUpdate.filter { it.status != SManga.COMPLETED }
|
listToUpdate = listToUpdate.filterNot { it.status == SManga.COMPLETED }
|
||||||
}
|
}
|
||||||
|
|
||||||
return listToUpdate
|
val selectedScheme = preferences.libraryUpdatePrioritization().get()
|
||||||
|
mangaToUpdate = listToUpdate
|
||||||
|
.distinctBy { it.id }
|
||||||
|
.sortedWith(rankingScheme[selectedScheme])
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -248,54 +266,41 @@ class LibraryUpdateService(
|
|||||||
* @param mangaToUpdate the list to update
|
* @param mangaToUpdate the list to update
|
||||||
* @return an observable delivering the progress of each update.
|
* @return an observable delivering the progress of each update.
|
||||||
*/
|
*/
|
||||||
suspend fun updateChapterList(mangaToUpdate: List<LibraryManga>) {
|
suspend fun updateChapterList() {
|
||||||
val progressCount = AtomicInteger(0)
|
val progressCount = AtomicInteger(0)
|
||||||
val newUpdates = mutableListOf<Pair<LibraryManga, Array<Chapter>>>()
|
val newUpdates = mutableListOf<Pair<LibraryManga, Array<Chapter>>>()
|
||||||
val failedUpdates = mutableListOf<Pair<Manga, String?>>()
|
val failedUpdates = mutableListOf<Pair<Manga, String?>>()
|
||||||
var hasDownloads = false
|
var hasDownloads = false
|
||||||
|
|
||||||
mangaToUpdate
|
mangaToUpdate.forEach { manga ->
|
||||||
.map { manga ->
|
if (updateJob?.isActive != true) {
|
||||||
if (updateJob?.isActive != true) {
|
return
|
||||||
throw CancellationException()
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Notify manga that will update.
|
notifier.showProgressNotification(manga, progressCount.andIncrement, mangaToUpdate.size)
|
||||||
notifier.showProgressNotification(manga, progressCount.andIncrement, mangaToUpdate.size)
|
|
||||||
|
|
||||||
// Update the chapters of the manga
|
try {
|
||||||
try {
|
val (newChapters, _) = updateManga(manga)
|
||||||
val newChapters = updateManga(manga).first
|
|
||||||
Pair(manga, newChapters)
|
if (newChapters.isNotEmpty()) {
|
||||||
} catch (e: Throwable) {
|
if (manga.shouldDownloadNewChapters(db, preferences)) {
|
||||||
// If there's any error, return empty update and continue.
|
downloadChapters(manga, newChapters)
|
||||||
val errorMessage = if (e is NoChaptersException) {
|
hasDownloads = true
|
||||||
getString(R.string.no_chapters_error)
|
|
||||||
} else {
|
|
||||||
e.message
|
|
||||||
}
|
}
|
||||||
failedUpdates.add(Pair(manga, errorMessage))
|
|
||||||
Pair(manga, emptyList())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Filter out mangas without new chapters (or failed).
|
|
||||||
.filter { (_, newChapters) -> newChapters.isNotEmpty() }
|
|
||||||
.forEach { (manga, newChapters) ->
|
|
||||||
if (manga.shouldDownloadNewChapters(db, preferences)) {
|
|
||||||
downloadChapters(manga, newChapters)
|
|
||||||
hasDownloads = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert to the manga that contains new chapters.
|
// Convert to the manga that contains new chapters
|
||||||
newUpdates.add(
|
newUpdates.add(manga to newChapters.sortedByDescending { ch -> ch.source_order }.toTypedArray())
|
||||||
Pair(
|
}
|
||||||
manga,
|
} catch (e: Throwable) {
|
||||||
newChapters.sortedByDescending { ch -> ch.source_order }.toTypedArray()
|
val errorMessage = if (e is NoChaptersException) {
|
||||||
)
|
getString(R.string.no_chapters_error)
|
||||||
)
|
} else {
|
||||||
|
e.message
|
||||||
|
}
|
||||||
|
failedUpdates.add(manga to errorMessage)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Notify result of the overall update.
|
|
||||||
notifier.cancelProgressNotification()
|
notifier.cancelProgressNotification()
|
||||||
|
|
||||||
if (newUpdates.isNotEmpty()) {
|
if (newUpdates.isNotEmpty()) {
|
||||||
@ -334,7 +339,7 @@ class LibraryUpdateService(
|
|||||||
val handler = CoroutineExceptionHandler { _, exception ->
|
val handler = CoroutineExceptionHandler { _, exception ->
|
||||||
Timber.e(exception)
|
Timber.e(exception)
|
||||||
}
|
}
|
||||||
ioScope.launch(handler) {
|
GlobalScope.launch(Dispatchers.IO + handler) {
|
||||||
val updatedManga = source.getMangaDetails(manga.toMangaInfo())
|
val updatedManga = source.getMangaDetails(manga.toMangaInfo())
|
||||||
val sManga = updatedManga.toSManga()
|
val sManga = updatedManga.toSManga()
|
||||||
// Avoid "losing" existing cover
|
// Avoid "losing" existing cover
|
||||||
@ -355,12 +360,12 @@ class LibraryUpdateService(
|
|||||||
return syncChaptersWithSource(db, chapters, manga, source)
|
return syncChaptersWithSource(db, chapters, manga, source)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun updateCovers(mangaToUpdate: List<LibraryManga>) {
|
private suspend fun updateCovers() {
|
||||||
var progressCount = 0
|
var progressCount = 0
|
||||||
|
|
||||||
mangaToUpdate.forEach { manga ->
|
mangaToUpdate.forEach { manga ->
|
||||||
if (updateJob?.isActive != true) {
|
if (updateJob?.isActive != true) {
|
||||||
throw CancellationException()
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
notifier.showProgressNotification(manga, progressCount++, mangaToUpdate.size)
|
notifier.showProgressNotification(manga, progressCount++, mangaToUpdate.size)
|
||||||
@ -388,13 +393,13 @@ class LibraryUpdateService(
|
|||||||
* Method that updates the metadata of the connected tracking services. It's called in a
|
* Method that updates the metadata of the connected tracking services. It's called in a
|
||||||
* background thread, so it's safe to do heavy operations or network calls here.
|
* background thread, so it's safe to do heavy operations or network calls here.
|
||||||
*/
|
*/
|
||||||
private suspend fun updateTrackings(mangaToUpdate: List<LibraryManga>) {
|
private suspend fun updateTrackings() {
|
||||||
var progressCount = 0
|
var progressCount = 0
|
||||||
val loggedServices = trackManager.services.filter { it.isLogged }
|
val loggedServices = trackManager.services.filter { it.isLogged }
|
||||||
|
|
||||||
mangaToUpdate.forEach { manga ->
|
mangaToUpdate.forEach { manga ->
|
||||||
if (updateJob?.isActive != true) {
|
if (updateJob?.isActive != true) {
|
||||||
throw CancellationException()
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notify manga that will update.
|
// Notify manga that will update.
|
||||||
|
@ -2,6 +2,7 @@ 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
|
||||||
@ -69,9 +70,10 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
)
|
)
|
||||||
// Share backup file
|
// Share backup file
|
||||||
ACTION_SHARE_BACKUP ->
|
ACTION_SHARE_BACKUP ->
|
||||||
shareBackup(
|
shareFile(
|
||||||
context,
|
context,
|
||||||
intent.getParcelableExtra(EXTRA_URI),
|
intent.getParcelableExtra(EXTRA_URI),
|
||||||
|
if (intent.getBooleanExtra(EXTRA_IS_LEGACY_BACKUP, false)) "application/json" else "application/x-protobuf+gzip",
|
||||||
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)
|
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)
|
||||||
)
|
)
|
||||||
ACTION_CANCEL_RESTORE -> cancelRestore(
|
ACTION_CANCEL_RESTORE -> cancelRestore(
|
||||||
@ -100,6 +102,14 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
markAsRead(urls, mangaId)
|
markAsRead(urls, mangaId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Share crash dump file
|
||||||
|
ACTION_SHARE_CRASH_LOG ->
|
||||||
|
shareFile(
|
||||||
|
context,
|
||||||
|
intent.getParcelableExtra(EXTRA_URI),
|
||||||
|
"text/plain",
|
||||||
|
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -120,14 +130,13 @@ 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) {
|
||||||
// Create intent
|
|
||||||
val intent = Intent(Intent.ACTION_SEND).apply {
|
val intent = Intent(Intent.ACTION_SEND).apply {
|
||||||
val uri = File(path).getUriCompat(context)
|
val uri = File(path).getUriCompat(context)
|
||||||
putExtra(Intent.EXTRA_STREAM, uri)
|
putExtra(Intent.EXTRA_STREAM, uri)
|
||||||
|
clipData = ClipData.newRawUri(null, uri)
|
||||||
type = "image/*"
|
type = "image/*"
|
||||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||||
}
|
}
|
||||||
// Dismiss notification
|
|
||||||
dismissNotification(context, notificationId)
|
dismissNotification(context, notificationId)
|
||||||
// Launch share activity
|
// Launch share activity
|
||||||
context.startActivity(intent)
|
context.startActivity(intent)
|
||||||
@ -140,10 +149,11 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
* @param path path of file
|
* @param path path of file
|
||||||
* @param notificationId id of notification
|
* @param notificationId id of notification
|
||||||
*/
|
*/
|
||||||
private fun shareBackup(context: Context, uri: Uri, notificationId: Int) {
|
private fun shareFile(context: Context, uri: Uri, fileMimeType: String, notificationId: Int) {
|
||||||
val sendIntent = Intent(Intent.ACTION_SEND).apply {
|
val sendIntent = Intent(Intent.ACTION_SEND).apply {
|
||||||
putExtra(Intent.EXTRA_STREAM, uri)
|
putExtra(Intent.EXTRA_STREAM, uri)
|
||||||
type = "application/json"
|
clipData = ClipData.newRawUri(null, uri)
|
||||||
|
type = fileMimeType
|
||||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||||
}
|
}
|
||||||
// Dismiss notification
|
// Dismiss notification
|
||||||
@ -244,59 +254,34 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
companion object {
|
companion object {
|
||||||
private const val NAME = "NotificationReceiver"
|
private const val NAME = "NotificationReceiver"
|
||||||
|
|
||||||
// Called to launch share intent.
|
|
||||||
private const val ACTION_SHARE_IMAGE = "$ID.$NAME.SHARE_IMAGE"
|
private const val ACTION_SHARE_IMAGE = "$ID.$NAME.SHARE_IMAGE"
|
||||||
|
|
||||||
// Called to delete image.
|
|
||||||
private const val ACTION_DELETE_IMAGE = "$ID.$NAME.DELETE_IMAGE"
|
private const val ACTION_DELETE_IMAGE = "$ID.$NAME.DELETE_IMAGE"
|
||||||
|
|
||||||
// Called to launch send intent.
|
|
||||||
private const val ACTION_SHARE_BACKUP = "$ID.$NAME.SEND_BACKUP"
|
private const val ACTION_SHARE_BACKUP = "$ID.$NAME.SEND_BACKUP"
|
||||||
|
|
||||||
// Called to cancel backup restore job.
|
private const val ACTION_SHARE_CRASH_LOG = "$ID.$NAME.SEND_CRASH_LOG"
|
||||||
|
|
||||||
private const val ACTION_CANCEL_RESTORE = "$ID.$NAME.CANCEL_RESTORE"
|
private const val ACTION_CANCEL_RESTORE = "$ID.$NAME.CANCEL_RESTORE"
|
||||||
|
|
||||||
// Called to cancel library update.
|
|
||||||
private const val ACTION_CANCEL_LIBRARY_UPDATE = "$ID.$NAME.CANCEL_LIBRARY_UPDATE"
|
private const val ACTION_CANCEL_LIBRARY_UPDATE = "$ID.$NAME.CANCEL_LIBRARY_UPDATE"
|
||||||
|
|
||||||
// Called to mark manga chapters as read.
|
|
||||||
private const val ACTION_MARK_AS_READ = "$ID.$NAME.MARK_AS_READ"
|
private const val ACTION_MARK_AS_READ = "$ID.$NAME.MARK_AS_READ"
|
||||||
|
|
||||||
// Called to open chapter.
|
|
||||||
private const val ACTION_OPEN_CHAPTER = "$ID.$NAME.ACTION_OPEN_CHAPTER"
|
private const val ACTION_OPEN_CHAPTER = "$ID.$NAME.ACTION_OPEN_CHAPTER"
|
||||||
|
|
||||||
// Value containing file location.
|
|
||||||
private const val EXTRA_FILE_LOCATION = "$ID.$NAME.FILE_LOCATION"
|
|
||||||
|
|
||||||
// Called to resume downloads.
|
|
||||||
private const val ACTION_RESUME_DOWNLOADS = "$ID.$NAME.ACTION_RESUME_DOWNLOADS"
|
private const val ACTION_RESUME_DOWNLOADS = "$ID.$NAME.ACTION_RESUME_DOWNLOADS"
|
||||||
|
|
||||||
// Called to pause downloads.
|
|
||||||
private const val ACTION_PAUSE_DOWNLOADS = "$ID.$NAME.ACTION_PAUSE_DOWNLOADS"
|
private const val ACTION_PAUSE_DOWNLOADS = "$ID.$NAME.ACTION_PAUSE_DOWNLOADS"
|
||||||
|
|
||||||
// Called to clear downloads.
|
|
||||||
private const val ACTION_CLEAR_DOWNLOADS = "$ID.$NAME.ACTION_CLEAR_DOWNLOADS"
|
private const val ACTION_CLEAR_DOWNLOADS = "$ID.$NAME.ACTION_CLEAR_DOWNLOADS"
|
||||||
|
|
||||||
// Called to dismiss notification.
|
|
||||||
private const val ACTION_DISMISS_NOTIFICATION = "$ID.$NAME.ACTION_DISMISS_NOTIFICATION"
|
private const val ACTION_DISMISS_NOTIFICATION = "$ID.$NAME.ACTION_DISMISS_NOTIFICATION"
|
||||||
|
|
||||||
// Value containing uri.
|
private const val EXTRA_FILE_LOCATION = "$ID.$NAME.FILE_LOCATION"
|
||||||
private const val EXTRA_URI = "$ID.$NAME.URI"
|
private const val EXTRA_URI = "$ID.$NAME.URI"
|
||||||
|
|
||||||
// Value containing notification id.
|
|
||||||
private const val EXTRA_NOTIFICATION_ID = "$ID.$NAME.NOTIFICATION_ID"
|
private const val EXTRA_NOTIFICATION_ID = "$ID.$NAME.NOTIFICATION_ID"
|
||||||
|
|
||||||
// Value containing group id.
|
|
||||||
private const val EXTRA_GROUP_ID = "$ID.$NAME.EXTRA_GROUP_ID"
|
private const val EXTRA_GROUP_ID = "$ID.$NAME.EXTRA_GROUP_ID"
|
||||||
|
|
||||||
// Value containing manga id.
|
|
||||||
private const val EXTRA_MANGA_ID = "$ID.$NAME.EXTRA_MANGA_ID"
|
private const val EXTRA_MANGA_ID = "$ID.$NAME.EXTRA_MANGA_ID"
|
||||||
|
|
||||||
// Value containing chapter id.
|
|
||||||
private const val EXTRA_CHAPTER_ID = "$ID.$NAME.EXTRA_CHAPTER_ID"
|
private const val EXTRA_CHAPTER_ID = "$ID.$NAME.EXTRA_CHAPTER_ID"
|
||||||
|
|
||||||
// Value containing chapter url.
|
|
||||||
private const val EXTRA_CHAPTER_URL = "$ID.$NAME.EXTRA_CHAPTER_URL"
|
private const val EXTRA_CHAPTER_URL = "$ID.$NAME.EXTRA_CHAPTER_URL"
|
||||||
|
private const val EXTRA_IS_LEGACY_BACKUP = "$ID.$NAME.EXTRA_IS_LEGACY_BACKUP"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a [PendingIntent] that resumes the download of a chapter
|
* Returns a [PendingIntent] that resumes the download of a chapter
|
||||||
@ -509,10 +494,11 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
* @param notificationId id of notification
|
* @param notificationId id of notification
|
||||||
* @return [PendingIntent]
|
* @return [PendingIntent]
|
||||||
*/
|
*/
|
||||||
internal fun shareBackupPendingBroadcast(context: Context, uri: Uri, notificationId: Int): PendingIntent {
|
internal fun shareBackupPendingBroadcast(context: Context, uri: Uri, isLegacyFormat: Boolean, notificationId: Int): PendingIntent {
|
||||||
val intent = Intent(context, NotificationReceiver::class.java).apply {
|
val intent = Intent(context, NotificationReceiver::class.java).apply {
|
||||||
action = ACTION_SHARE_BACKUP
|
action = ACTION_SHARE_BACKUP
|
||||||
putExtra(EXTRA_URI, uri)
|
putExtra(EXTRA_URI, uri)
|
||||||
|
putExtra(EXTRA_IS_LEGACY_BACKUP, isLegacyFormat)
|
||||||
putExtra(EXTRA_NOTIFICATION_ID, notificationId)
|
putExtra(EXTRA_NOTIFICATION_ID, notificationId)
|
||||||
}
|
}
|
||||||
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||||
@ -534,6 +520,23 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
return PendingIntent.getActivity(context, 0, intent, 0)
|
return PendingIntent.getActivity(context, 0, intent, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns [PendingIntent] that starts a share activity for a crash log dump file.
|
||||||
|
*
|
||||||
|
* @param context context of application
|
||||||
|
* @param uri uri of file
|
||||||
|
* @param notificationId id of notification
|
||||||
|
* @return [PendingIntent]
|
||||||
|
*/
|
||||||
|
internal fun shareCrashLogPendingBroadcast(context: Context, uri: Uri, notificationId: Int): PendingIntent {
|
||||||
|
val intent = Intent(context, NotificationReceiver::class.java).apply {
|
||||||
|
action = ACTION_SHARE_CRASH_LOG
|
||||||
|
putExtra(EXTRA_URI, uri)
|
||||||
|
putExtra(EXTRA_NOTIFICATION_ID, notificationId)
|
||||||
|
}
|
||||||
|
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns [PendingIntent] that cancels a backup restore job.
|
* Returns [PendingIntent] that cancels a backup restore job.
|
||||||
*
|
*
|
||||||
|
@ -23,6 +23,14 @@ object PreferenceKeys {
|
|||||||
|
|
||||||
const val showPageNumber = "pref_show_page_number_key"
|
const val showPageNumber = "pref_show_page_number_key"
|
||||||
|
|
||||||
|
const val dualPageSplitPaged = "pref_dual_page_split"
|
||||||
|
|
||||||
|
const val dualPageSplitWebtoon = "pref_dual_page_split_webtoon"
|
||||||
|
|
||||||
|
const val dualPageInvertPaged = "pref_dual_page_invert"
|
||||||
|
|
||||||
|
const val dualPageInvertWebtoon = "pref_dual_page_invert_webtoon"
|
||||||
|
|
||||||
const val showReadingMode = "pref_show_reading_mode"
|
const val showReadingMode = "pref_show_reading_mode"
|
||||||
|
|
||||||
const val trueColor = "pref_true_color_key"
|
const val trueColor = "pref_true_color_key"
|
||||||
@ -71,6 +79,10 @@ object PreferenceKeys {
|
|||||||
|
|
||||||
const val navigationModeWebtoon = "reader_navigation_mode_webtoon"
|
const val navigationModeWebtoon = "reader_navigation_mode_webtoon"
|
||||||
|
|
||||||
|
const val showNavigationOverlayNewUser = "reader_navigation_overlay_new_user"
|
||||||
|
|
||||||
|
const val showNavigationOverlayOnStart = "reader_navigation_overlay_on_start"
|
||||||
|
|
||||||
const val webtoonSidePadding = "webtoon_side_padding"
|
const val webtoonSidePadding = "webtoon_side_padding"
|
||||||
|
|
||||||
const val portraitColumns = "pref_library_columns_portrait_key"
|
const val portraitColumns = "pref_library_columns_portrait_key"
|
||||||
@ -112,6 +124,7 @@ object PreferenceKeys {
|
|||||||
const val libraryUpdateRestriction = "library_update_restriction"
|
const val libraryUpdateRestriction = "library_update_restriction"
|
||||||
|
|
||||||
const val libraryUpdateCategories = "library_update_categories"
|
const val libraryUpdateCategories = "library_update_categories"
|
||||||
|
const val libraryUpdateCategoriesExclude = "library_update_categories_exclude"
|
||||||
|
|
||||||
const val libraryUpdatePrioritization = "library_update_prioritization"
|
const val libraryUpdatePrioritization = "library_update_prioritization"
|
||||||
|
|
||||||
@ -152,6 +165,7 @@ object PreferenceKeys {
|
|||||||
const val downloadNew = "download_new"
|
const val downloadNew = "download_new"
|
||||||
|
|
||||||
const val downloadNewCategories = "download_new_categories"
|
const val downloadNewCategories = "download_new_categories"
|
||||||
|
const val downloadNewCategoriesExclude = "download_new_categories_exclude"
|
||||||
|
|
||||||
const val libraryDisplayMode = "pref_display_mode_library"
|
const val libraryDisplayMode = "pref_display_mode_library"
|
||||||
|
|
||||||
@ -177,7 +191,7 @@ object PreferenceKeys {
|
|||||||
|
|
||||||
const val searchPinnedSourcesOnly = "search_pinned_sources_only"
|
const val searchPinnedSourcesOnly = "search_pinned_sources_only"
|
||||||
|
|
||||||
const val enableDoh = "enable_doh"
|
const val dohProvider = "doh_provider"
|
||||||
|
|
||||||
const val defaultChapterFilterByRead = "default_chapter_filter_by_read"
|
const val defaultChapterFilterByRead = "default_chapter_filter_by_read"
|
||||||
|
|
||||||
|
@ -5,6 +5,8 @@ package eu.kanade.tachiyomi.data.preference
|
|||||||
*/
|
*/
|
||||||
object PreferenceValues {
|
object PreferenceValues {
|
||||||
|
|
||||||
|
/* ktlint-disable experimental:enum-entry-name-case */
|
||||||
|
|
||||||
// Keys are lowercase to match legacy string values
|
// Keys are lowercase to match legacy string values
|
||||||
enum class ThemeMode {
|
enum class ThemeMode {
|
||||||
light,
|
light,
|
||||||
@ -25,6 +27,8 @@ object PreferenceValues {
|
|||||||
amoled,
|
amoled,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ktlint-enable experimental:enum-entry-name-case */
|
||||||
|
|
||||||
enum class DisplayMode {
|
enum class DisplayMode {
|
||||||
COMPACT_GRID,
|
COMPACT_GRID,
|
||||||
COMFORTABLE_GRID,
|
COMFORTABLE_GRID,
|
||||||
|
@ -22,7 +22,7 @@ import java.util.Locale
|
|||||||
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
|
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues as Values
|
import eu.kanade.tachiyomi.data.preference.PreferenceValues as Values
|
||||||
|
|
||||||
fun <T> Preference<T>.asImmediateFlow(block: (value: T) -> Unit): Flow<T> {
|
fun <T> Preference<T>.asImmediateFlow(block: (T) -> Unit): Flow<T> {
|
||||||
block(get())
|
block(get())
|
||||||
return asFlow()
|
return asFlow()
|
||||||
.onEach { block(it) }
|
.onEach { block(it) }
|
||||||
@ -36,6 +36,10 @@ operator fun <T> Preference<Set<T>>.minusAssign(item: T) {
|
|||||||
set(get() - item)
|
set(get() - item)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun Preference<Boolean>.toggle() {
|
||||||
|
set(!get())
|
||||||
|
}
|
||||||
|
|
||||||
class PreferencesHelper(val context: Context) {
|
class PreferencesHelper(val context: Context) {
|
||||||
|
|
||||||
private val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
private val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
@ -89,6 +93,14 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun showPageNumber() = flowPrefs.getBoolean(Keys.showPageNumber, true)
|
fun showPageNumber() = flowPrefs.getBoolean(Keys.showPageNumber, true)
|
||||||
|
|
||||||
|
fun dualPageSplitPaged() = flowPrefs.getBoolean(Keys.dualPageSplitPaged, false)
|
||||||
|
|
||||||
|
fun dualPageSplitWebtoon() = flowPrefs.getBoolean(Keys.dualPageSplitWebtoon, false)
|
||||||
|
|
||||||
|
fun dualPageInvertPaged() = flowPrefs.getBoolean(Keys.dualPageInvertPaged, false)
|
||||||
|
|
||||||
|
fun dualPageInvertWebtoon() = flowPrefs.getBoolean(Keys.dualPageInvertWebtoon, false)
|
||||||
|
|
||||||
fun showReadingMode() = prefs.getBoolean(Keys.showReadingMode, true)
|
fun showReadingMode() = prefs.getBoolean(Keys.showReadingMode, true)
|
||||||
|
|
||||||
fun trueColor() = flowPrefs.getBoolean(Keys.trueColor, false)
|
fun trueColor() = flowPrefs.getBoolean(Keys.trueColor, false)
|
||||||
@ -141,6 +153,10 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun navigationModeWebtoon() = flowPrefs.getInt(Keys.navigationModeWebtoon, 0)
|
fun navigationModeWebtoon() = flowPrefs.getInt(Keys.navigationModeWebtoon, 0)
|
||||||
|
|
||||||
|
fun showNavigationOverlayNewUser() = flowPrefs.getBoolean(Keys.showNavigationOverlayNewUser, true)
|
||||||
|
|
||||||
|
fun showNavigationOverlayOnStart() = flowPrefs.getBoolean(Keys.showNavigationOverlayOnStart, false)
|
||||||
|
|
||||||
fun portraitColumns() = flowPrefs.getInt(Keys.portraitColumns, 0)
|
fun portraitColumns() = flowPrefs.getInt(Keys.portraitColumns, 0)
|
||||||
|
|
||||||
fun landscapeColumns() = flowPrefs.getInt(Keys.landscapeColumns, 0)
|
fun landscapeColumns() = flowPrefs.getInt(Keys.landscapeColumns, 0)
|
||||||
@ -202,6 +218,7 @@ class PreferencesHelper(val context: Context) {
|
|||||||
fun libraryUpdateRestriction() = prefs.getStringSet(Keys.libraryUpdateRestriction, setOf("wifi"))
|
fun libraryUpdateRestriction() = prefs.getStringSet(Keys.libraryUpdateRestriction, setOf("wifi"))
|
||||||
|
|
||||||
fun libraryUpdateCategories() = flowPrefs.getStringSet(Keys.libraryUpdateCategories, emptySet())
|
fun libraryUpdateCategories() = flowPrefs.getStringSet(Keys.libraryUpdateCategories, emptySet())
|
||||||
|
fun libraryUpdateCategoriesExclude() = flowPrefs.getStringSet(Keys.libraryUpdateCategoriesExclude, emptySet())
|
||||||
|
|
||||||
fun libraryUpdatePrioritization() = flowPrefs.getInt(Keys.libraryUpdatePrioritization, 0)
|
fun libraryUpdatePrioritization() = flowPrefs.getInt(Keys.libraryUpdatePrioritization, 0)
|
||||||
|
|
||||||
@ -248,6 +265,7 @@ class PreferencesHelper(val context: Context) {
|
|||||||
fun downloadNew() = flowPrefs.getBoolean(Keys.downloadNew, false)
|
fun downloadNew() = flowPrefs.getBoolean(Keys.downloadNew, false)
|
||||||
|
|
||||||
fun downloadNewCategories() = flowPrefs.getStringSet(Keys.downloadNewCategories, emptySet())
|
fun downloadNewCategories() = flowPrefs.getStringSet(Keys.downloadNewCategories, emptySet())
|
||||||
|
fun downloadNewCategoriesExclude() = flowPrefs.getStringSet(Keys.downloadNewCategoriesExclude, emptySet())
|
||||||
|
|
||||||
fun lang() = prefs.getString(Keys.lang, "")
|
fun lang() = prefs.getString(Keys.lang, "")
|
||||||
|
|
||||||
@ -261,7 +279,7 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun trustedSignatures() = flowPrefs.getStringSet("trusted_signatures", emptySet())
|
fun trustedSignatures() = flowPrefs.getStringSet("trusted_signatures", emptySet())
|
||||||
|
|
||||||
fun enableDoh() = prefs.getBoolean(Keys.enableDoh, false)
|
fun dohProvider() = prefs.getInt(Keys.dohProvider, -1)
|
||||||
|
|
||||||
fun lastSearchQuerySearchSettings() = flowPrefs.getString("last_search_query", "")
|
fun lastSearchQuerySearchSettings() = flowPrefs.getString("last_search_query", "")
|
||||||
|
|
||||||
|
@ -35,6 +35,8 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
|
|
||||||
private val api by lazy { AnilistApi(client, interceptor) }
|
private val api by lazy { AnilistApi(client, interceptor) }
|
||||||
|
|
||||||
|
override val supportsReadingDates: Boolean = true
|
||||||
|
|
||||||
private val scorePreference = preferences.anilistScoreType()
|
private val scorePreference = preferences.anilistScoreType()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
@ -2,6 +2,9 @@ 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
|
||||||
@ -9,6 +12,7 @@ import eu.kanade.tachiyomi.network.await
|
|||||||
import eu.kanade.tachiyomi.network.jsonMime
|
import eu.kanade.tachiyomi.network.jsonMime
|
||||||
import eu.kanade.tachiyomi.network.parseAs
|
import eu.kanade.tachiyomi.network.parseAs
|
||||||
import eu.kanade.tachiyomi.util.lang.withIOContext
|
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||||
|
import kotlinx.serialization.json.JsonNull
|
||||||
import kotlinx.serialization.json.JsonObject
|
import kotlinx.serialization.json.JsonObject
|
||||||
import kotlinx.serialization.json.buildJsonObject
|
import kotlinx.serialization.json.buildJsonObject
|
||||||
import kotlinx.serialization.json.contentOrNull
|
import kotlinx.serialization.json.contentOrNull
|
||||||
@ -30,8 +34,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
|||||||
|
|
||||||
suspend fun addLibManga(track: Track): Track {
|
suspend fun addLibManga(track: Track): Track {
|
||||||
return withIOContext {
|
return withIOContext {
|
||||||
val query =
|
val query = """
|
||||||
"""
|
|
||||||
|mutation AddManga(${'$'}mangaId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus) {
|
|mutation AddManga(${'$'}mangaId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus) {
|
||||||
|SaveMediaListEntry (mediaId: ${'$'}mangaId, progress: ${'$'}progress, status: ${'$'}status) {
|
|SaveMediaListEntry (mediaId: ${'$'}mangaId, progress: ${'$'}progress, status: ${'$'}status) {
|
||||||
| id
|
| id
|
||||||
@ -65,10 +68,15 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
|||||||
|
|
||||||
suspend fun updateLibManga(track: Track): Track {
|
suspend fun updateLibManga(track: Track): Track {
|
||||||
return withIOContext {
|
return withIOContext {
|
||||||
val query =
|
val query = """
|
||||||
"""
|
|mutation UpdateManga(
|
||||||
|mutation UpdateManga(${'$'}listId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus, ${'$'}score: Int) {
|
|${'$'}listId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus,
|
||||||
|SaveMediaListEntry (id: ${'$'}listId, progress: ${'$'}progress, status: ${'$'}status, scoreRaw: ${'$'}score) {
|
|${'$'}score: Int, ${'$'}startedAt: FuzzyDateInput, ${'$'}completedAt: FuzzyDateInput
|
||||||
|
|) {
|
||||||
|
|SaveMediaListEntry(
|
||||||
|
|id: ${'$'}listId, progress: ${'$'}progress, status: ${'$'}status,
|
||||||
|
|scoreRaw: ${'$'}score, startedAt: ${'$'}startedAt, completedAt: ${'$'}completedAt
|
||||||
|
|) {
|
||||||
|id
|
|id
|
||||||
|status
|
|status
|
||||||
|progress
|
|progress
|
||||||
@ -82,6 +90,8 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
|||||||
put("progress", track.last_chapter_read)
|
put("progress", track.last_chapter_read)
|
||||||
put("status", track.toAnilistStatus())
|
put("status", track.toAnilistStatus())
|
||||||
put("score", track.score.toInt())
|
put("score", track.score.toInt())
|
||||||
|
put("startedAt", createDate(track.started_reading_date))
|
||||||
|
put("completedAt", createDate(track.finished_reading_date))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
authClient.newCall(POST(apiUrl, body = payload.toString().toRequestBody(jsonMime)))
|
authClient.newCall(POST(apiUrl, body = payload.toString().toRequestBody(jsonMime)))
|
||||||
@ -92,8 +102,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
|||||||
|
|
||||||
suspend fun search(search: String): List<TrackSearch> {
|
suspend fun search(search: String): List<TrackSearch> {
|
||||||
return withIOContext {
|
return withIOContext {
|
||||||
val query =
|
val query = """
|
||||||
"""
|
|
||||||
|query Search(${'$'}query: String) {
|
|query Search(${'$'}query: String) {
|
||||||
|Page (perPage: 50) {
|
|Page (perPage: 50) {
|
||||||
|media(search: ${'$'}query, type: MANGA, format_not_in: [NOVEL]) {
|
|media(search: ${'$'}query, type: MANGA, format_not_in: [NOVEL]) {
|
||||||
@ -143,8 +152,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
|||||||
|
|
||||||
suspend fun findLibManga(track: Track, userid: Int): Track? {
|
suspend fun findLibManga(track: Track, userid: Int): Track? {
|
||||||
return withIOContext {
|
return withIOContext {
|
||||||
val query =
|
val query = """
|
||||||
"""
|
|
||||||
|query (${'$'}id: Int!, ${'$'}manga_id: Int!) {
|
|query (${'$'}id: Int!, ${'$'}manga_id: Int!) {
|
||||||
|Page {
|
|Page {
|
||||||
|mediaList(userId: ${'$'}id, type: MANGA, mediaId: ${'$'}manga_id) {
|
|mediaList(userId: ${'$'}id, type: MANGA, mediaId: ${'$'}manga_id) {
|
||||||
@ -152,6 +160,16 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
|||||||
|status
|
|status
|
||||||
|scoreRaw: score(format: POINT_100)
|
|scoreRaw: score(format: POINT_100)
|
||||||
|progress
|
|progress
|
||||||
|
|startedAt {
|
||||||
|
|year
|
||||||
|
|month
|
||||||
|
|day
|
||||||
|
|}
|
||||||
|
|completedAt {
|
||||||
|
|year
|
||||||
|
|month
|
||||||
|
|day
|
||||||
|
|}
|
||||||
|media {
|
|media {
|
||||||
|id
|
|id
|
||||||
|title {
|
|title {
|
||||||
@ -209,8 +227,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
|||||||
|
|
||||||
suspend fun getCurrentUser(): Pair<Int, String> {
|
suspend fun getCurrentUser(): Pair<Int, String> {
|
||||||
return withIOContext {
|
return withIOContext {
|
||||||
val query =
|
val query = """
|
||||||
"""
|
|
||||||
|query User {
|
|query User {
|
||||||
|Viewer {
|
|Viewer {
|
||||||
|id
|
|id
|
||||||
@ -243,21 +260,6 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun jsonToALManga(struct: JsonObject): ALManga {
|
private fun jsonToALManga(struct: JsonObject): ALManga {
|
||||||
val date = try {
|
|
||||||
val date = Calendar.getInstance()
|
|
||||||
date.set(
|
|
||||||
struct["startDate"]!!.jsonObject["year"]!!.jsonPrimitive.intOrNull ?: 0,
|
|
||||||
(
|
|
||||||
struct["startDate"]!!.jsonObject["month"]!!.jsonPrimitive.intOrNull
|
|
||||||
?: 0
|
|
||||||
) - 1,
|
|
||||||
struct["startDate"]!!.jsonObject["day"]!!.jsonPrimitive.intOrNull ?: 0
|
|
||||||
)
|
|
||||||
date.timeInMillis
|
|
||||||
} catch (_: Exception) {
|
|
||||||
0L
|
|
||||||
}
|
|
||||||
|
|
||||||
return ALManga(
|
return ALManga(
|
||||||
struct["id"]!!.jsonPrimitive.int,
|
struct["id"]!!.jsonPrimitive.int,
|
||||||
struct["title"]!!.jsonObject["romaji"]!!.jsonPrimitive.content,
|
struct["title"]!!.jsonObject["romaji"]!!.jsonPrimitive.content,
|
||||||
@ -265,7 +267,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
|||||||
struct["description"]!!.jsonPrimitive.contentOrNull,
|
struct["description"]!!.jsonPrimitive.contentOrNull,
|
||||||
struct["type"]!!.jsonPrimitive.content,
|
struct["type"]!!.jsonPrimitive.content,
|
||||||
struct["status"]!!.jsonPrimitive.contentOrNull ?: "",
|
struct["status"]!!.jsonPrimitive.contentOrNull ?: "",
|
||||||
date,
|
parseDate(struct, "startDate"),
|
||||||
struct["chapters"]!!.jsonPrimitive.intOrNull ?: 0
|
struct["chapters"]!!.jsonPrimitive.intOrNull ?: 0
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -276,10 +278,44 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
|||||||
struct["status"]!!.jsonPrimitive.content,
|
struct["status"]!!.jsonPrimitive.content,
|
||||||
struct["scoreRaw"]!!.jsonPrimitive.int,
|
struct["scoreRaw"]!!.jsonPrimitive.int,
|
||||||
struct["progress"]!!.jsonPrimitive.int,
|
struct["progress"]!!.jsonPrimitive.int,
|
||||||
|
parseDate(struct, "startedAt"),
|
||||||
|
parseDate(struct, "completedAt"),
|
||||||
jsonToALManga(struct["media"]!!.jsonObject)
|
jsonToALManga(struct["media"]!!.jsonObject)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun parseDate(struct: JsonObject, dateKey: String): Long {
|
||||||
|
return try {
|
||||||
|
val date = Calendar.getInstance()
|
||||||
|
date.set(
|
||||||
|
struct[dateKey]!!.jsonObject["year"]!!.jsonPrimitive.int,
|
||||||
|
struct[dateKey]!!.jsonObject["month"]!!.jsonPrimitive.int - 1,
|
||||||
|
struct[dateKey]!!.jsonObject["day"]!!.jsonPrimitive.int
|
||||||
|
)
|
||||||
|
date.timeInMillis
|
||||||
|
} catch (_: Exception) {
|
||||||
|
0L
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createDate(dateValue: Long): JsonObject {
|
||||||
|
if (dateValue == 0L) {
|
||||||
|
return buildJsonObject {
|
||||||
|
put("year", JsonNull)
|
||||||
|
put("month", JsonNull)
|
||||||
|
put("day", JsonNull)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val calendar = Calendar.getInstance()
|
||||||
|
calendar.timeInMillis = dateValue
|
||||||
|
return buildJsonObject {
|
||||||
|
put("year", calendar.year)
|
||||||
|
put("month", calendar.month + 1)
|
||||||
|
put("day", calendar.dayOfMonth)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val clientId = "385"
|
private const val clientId = "385"
|
||||||
private const val apiUrl = "https://graphql.anilist.co/"
|
private const val apiUrl = "https://graphql.anilist.co/"
|
||||||
|
@ -44,6 +44,8 @@ data class ALUserManga(
|
|||||||
val list_status: String,
|
val list_status: String,
|
||||||
val score_raw: Int,
|
val score_raw: Int,
|
||||||
val chapters_read: Int,
|
val chapters_read: Int,
|
||||||
|
val start_date_fuzzy: Long,
|
||||||
|
val completed_date_fuzzy: Long,
|
||||||
val manga: ALManga
|
val manga: ALManga
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@ -51,6 +53,8 @@ data class ALUserManga(
|
|||||||
media_id = manga.media_id
|
media_id = manga.media_id
|
||||||
status = toTrackStatus()
|
status = toTrackStatus()
|
||||||
score = score_raw.toFloat()
|
score = score_raw.toFloat()
|
||||||
|
started_reading_date = start_date_fuzzy
|
||||||
|
finished_reading_date = completed_date_fuzzy
|
||||||
last_chapter_read = chapters_read
|
last_chapter_read = chapters_read
|
||||||
library_id = this@ALUserManga.library_id
|
library_id = this@ALUserManga.library_id
|
||||||
total_chapters = manga.total_chapters
|
total_chapters = manga.total_chapters
|
||||||
|
@ -45,8 +45,10 @@ class Bangumi(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
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 = remoteTrack.status
|
track.status = statusTrack.status
|
||||||
track.last_chapter_read = remoteTrack.last_chapter_read
|
track.score = statusTrack.score
|
||||||
|
track.last_chapter_read = statusTrack.last_chapter_read
|
||||||
|
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
|
||||||
@ -66,7 +68,6 @@ class Bangumi(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
track.copyPersonalFrom(remoteStatusTrack!!)
|
track.copyPersonalFrom(remoteStatusTrack!!)
|
||||||
api.findLibManga(track)?.let { remoteTrack ->
|
api.findLibManga(track)?.let { remoteTrack ->
|
||||||
track.total_chapters = remoteTrack.total_chapters
|
track.total_chapters = remoteTrack.total_chapters
|
||||||
track.status = remoteTrack.status
|
|
||||||
}
|
}
|
||||||
return track
|
return track
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,7 @@ import eu.kanade.tachiyomi.util.lang.withIOContext
|
|||||||
import kotlinx.serialization.decodeFromString
|
import kotlinx.serialization.decodeFromString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.JsonObject
|
import kotlinx.serialization.json.JsonObject
|
||||||
|
import kotlinx.serialization.json.contentOrNull
|
||||||
import kotlinx.serialization.json.int
|
import kotlinx.serialization.json.int
|
||||||
import kotlinx.serialization.json.jsonArray
|
import kotlinx.serialization.json.jsonArray
|
||||||
import kotlinx.serialization.json.jsonObject
|
import kotlinx.serialization.json.jsonObject
|
||||||
@ -46,6 +47,7 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept
|
|||||||
return withIOContext {
|
return withIOContext {
|
||||||
// read status update
|
// read status update
|
||||||
val sbody = FormBody.Builder()
|
val sbody = FormBody.Builder()
|
||||||
|
.add("rating", track.score.toInt().toString())
|
||||||
.add("status", track.toBangumiStatus())
|
.add("status", track.toBangumiStatus())
|
||||||
.build()
|
.build()
|
||||||
authClient.newCall(POST("$apiUrl/collection/${track.media_id}/update", body = sbody))
|
authClient.newCall(POST("$apiUrl/collection/${track.media_id}/update", body = sbody))
|
||||||
@ -91,12 +93,24 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun jsonToSearch(obj: JsonObject): TrackSearch {
|
private fun jsonToSearch(obj: JsonObject): TrackSearch {
|
||||||
|
val coverUrl = if (obj["images"] is JsonObject) {
|
||||||
|
obj["images"]?.jsonObject?.get("common")?.jsonPrimitive?.contentOrNull ?: ""
|
||||||
|
} else {
|
||||||
|
// Sometimes JsonNull
|
||||||
|
""
|
||||||
|
}
|
||||||
|
val totalChapters = if (obj["eps_count"] != null) {
|
||||||
|
obj["eps_count"]!!.jsonPrimitive.int
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
return TrackSearch.create(TrackManager.BANGUMI).apply {
|
return TrackSearch.create(TrackManager.BANGUMI).apply {
|
||||||
media_id = obj["id"]!!.jsonPrimitive.int
|
media_id = obj["id"]!!.jsonPrimitive.int
|
||||||
title = obj["name_cn"]!!.jsonPrimitive.content
|
title = obj["name_cn"]!!.jsonPrimitive.content
|
||||||
cover_url = obj["images"]!!.jsonObject["common"]!!.jsonPrimitive.content
|
cover_url = coverUrl
|
||||||
summary = obj["name"]!!.jsonPrimitive.content
|
summary = obj["name"]!!.jsonPrimitive.content
|
||||||
tracking_url = obj["url"]!!.jsonPrimitive.content
|
tracking_url = obj["url"]!!.jsonPrimitive.content
|
||||||
|
total_chapters = totalChapters
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -119,14 +133,21 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept
|
|||||||
.build()
|
.build()
|
||||||
|
|
||||||
// TODO: get user readed chapter here
|
// TODO: get user readed chapter here
|
||||||
authClient.newCall(requestUserRead)
|
var response = authClient.newCall(requestUserRead).await()
|
||||||
.await()
|
var responseBody = response.body?.string().orEmpty()
|
||||||
.parseAs<Collection>()
|
if (responseBody.isEmpty()) {
|
||||||
.let {
|
throw Exception("Null Response")
|
||||||
|
}
|
||||||
|
if (responseBody.contains("\"code\":400")) {
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
json.decodeFromString<Collection>(responseBody).let {
|
||||||
track.status = it.status?.id!!
|
track.status = it.status?.id!!
|
||||||
track.last_chapter_read = it.ep_status!!
|
track.last_chapter_read = it.ep_status!!
|
||||||
|
track.score = it.rating!!
|
||||||
track
|
track
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ data class Collection(
|
|||||||
val comment: String? = "",
|
val comment: String? = "",
|
||||||
val ep_status: Int? = 0,
|
val ep_status: Int? = 0,
|
||||||
val lasttouch: Int? = 0,
|
val lasttouch: Int? = 0,
|
||||||
val rating: Int? = 0,
|
val rating: Float? = 0f,
|
||||||
val status: Status? = Status(),
|
val status: Status? = Status(),
|
||||||
val tag: List<String?>? = listOf(),
|
val tag: List<String?>? = listOf(),
|
||||||
val user: User? = User(),
|
val user: User? = User(),
|
||||||
|
@ -18,6 +18,7 @@ sealed class Extension {
|
|||||||
override val versionCode: Int,
|
override val versionCode: Int,
|
||||||
override val lang: String,
|
override val lang: String,
|
||||||
override val isNsfw: Boolean,
|
override val isNsfw: Boolean,
|
||||||
|
val pkgFactory: String?,
|
||||||
val sources: List<Source>,
|
val sources: List<Source>,
|
||||||
val hasUpdate: Boolean = false,
|
val hasUpdate: Boolean = false,
|
||||||
val isObsolete: Boolean = false,
|
val isObsolete: Boolean = false,
|
||||||
|
@ -31,6 +31,7 @@ internal object ExtensionLoader {
|
|||||||
|
|
||||||
private const val EXTENSION_FEATURE = "tachiyomi.extension"
|
private const val EXTENSION_FEATURE = "tachiyomi.extension"
|
||||||
private const val METADATA_SOURCE_CLASS = "tachiyomi.extension.class"
|
private const val METADATA_SOURCE_CLASS = "tachiyomi.extension.class"
|
||||||
|
private const val METADATA_SOURCE_FACTORY = "tachiyomi.extension.factory"
|
||||||
private const val METADATA_NSFW = "tachiyomi.extension.nsfw"
|
private const val METADATA_NSFW = "tachiyomi.extension.nsfw"
|
||||||
const val LIB_VERSION_MIN = 1.2
|
const val LIB_VERSION_MIN = 1.2
|
||||||
const val LIB_VERSION_MAX = 1.2
|
const val LIB_VERSION_MAX = 1.2
|
||||||
@ -162,7 +163,7 @@ internal object ExtensionLoader {
|
|||||||
else -> throw Exception("Unknown source class type! ${obj.javaClass}")
|
else -> throw Exception("Unknown source class type! ${obj.javaClass}")
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Timber.e(e, "Extension load error: $extName.")
|
Timber.w(e, "Extension load error: $extName ($it)")
|
||||||
return LoadResult.Error(e)
|
return LoadResult.Error(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -184,7 +185,8 @@ internal object ExtensionLoader {
|
|||||||
versionCode,
|
versionCode,
|
||||||
lang,
|
lang,
|
||||||
isNsfw,
|
isNsfw,
|
||||||
sources,
|
sources = sources,
|
||||||
|
pkgFactory = appInfo.metaData.getString(METADATA_SOURCE_FACTORY),
|
||||||
isUnofficial = signatureHash != officialSignature
|
isUnofficial = signatureHash != officialSignature
|
||||||
)
|
)
|
||||||
return LoadResult.Success(extension)
|
return LoadResult.Success(extension)
|
||||||
|
@ -9,6 +9,7 @@ import android.webkit.WebSettings
|
|||||||
import android.webkit.WebView
|
import android.webkit.WebView
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.util.lang.launchUI
|
import eu.kanade.tachiyomi.util.lang.launchUI
|
||||||
import eu.kanade.tachiyomi.util.system.WebViewClientCompat
|
import eu.kanade.tachiyomi.util.system.WebViewClientCompat
|
||||||
import eu.kanade.tachiyomi.util.system.WebViewUtil
|
import eu.kanade.tachiyomi.util.system.WebViewUtil
|
||||||
@ -98,7 +99,7 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
|
|||||||
|
|
||||||
// Avoid sending empty User-Agent, Chromium WebView will reset to default if empty
|
// Avoid sending empty User-Agent, Chromium WebView will reset to default if empty
|
||||||
webview.settings.userAgentString = request.header("User-Agent")
|
webview.settings.userAgentString = request.header("User-Agent")
|
||||||
?: WebViewUtil.DEFAULT_USER_AGENT
|
?: HttpSource.DEFAULT_USER_AGENT
|
||||||
|
|
||||||
webview.webViewClient = object : WebViewClientCompat() {
|
webview.webViewClient = object : WebViewClientCompat() {
|
||||||
override fun onPageFinished(view: WebView, url: String) {
|
override fun onPageFinished(view: WebView, url: String) {
|
||||||
@ -170,6 +171,6 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val SERVER_CHECK = arrayOf("cloudflare-nginx", "cloudflare")
|
private val SERVER_CHECK = arrayOf("cloudflare-nginx", "cloudflare")
|
||||||
private val COOKIE_NAMES = listOf("__cfduid", "cf_clearance")
|
private val COOKIE_NAMES = listOf("cf_clearance")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,40 @@
|
|||||||
|
package eu.kanade.tachiyomi.network
|
||||||
|
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.dnsoverhttps.DnsOverHttps
|
||||||
|
import java.net.InetAddress
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Based on https://github.com/square/okhttp/blob/ef5d0c83f7bbd3a0c0534e7ca23cbc4ee7550f3b/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DohProviders.java
|
||||||
|
*/
|
||||||
|
|
||||||
|
const val PREF_DOH_CLOUDFLARE = 1
|
||||||
|
const val PREF_DOH_GOOGLE = 2
|
||||||
|
|
||||||
|
fun OkHttpClient.Builder.dohCloudflare() = dns(
|
||||||
|
DnsOverHttps.Builder().client(build())
|
||||||
|
.url("https://cloudflare-dns.com/dns-query".toHttpUrl())
|
||||||
|
.bootstrapDnsHosts(
|
||||||
|
InetAddress.getByName("162.159.36.1"),
|
||||||
|
InetAddress.getByName("162.159.46.1"),
|
||||||
|
InetAddress.getByName("1.1.1.1"),
|
||||||
|
InetAddress.getByName("1.0.0.1"),
|
||||||
|
InetAddress.getByName("162.159.132.53"),
|
||||||
|
InetAddress.getByName("2606:4700:4700::1111"),
|
||||||
|
InetAddress.getByName("2606:4700:4700::1001"),
|
||||||
|
InetAddress.getByName("2606:4700:4700::0064"),
|
||||||
|
InetAddress.getByName("2606:4700:4700::6400")
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
|
||||||
|
fun OkHttpClient.Builder.dohGoogle() = dns(
|
||||||
|
DnsOverHttps.Builder().client(build())
|
||||||
|
.url("https://dns.google/dns-query".toHttpUrl())
|
||||||
|
.bootstrapDnsHosts(
|
||||||
|
InetAddress.getByName("8.8.4.4"),
|
||||||
|
InetAddress.getByName("8.8.8.8")
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
)
|
@ -4,13 +4,10 @@ import android.content.Context
|
|||||||
import eu.kanade.tachiyomi.BuildConfig
|
import eu.kanade.tachiyomi.BuildConfig
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import okhttp3.Cache
|
import okhttp3.Cache
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.dnsoverhttps.DnsOverHttps
|
|
||||||
import okhttp3.logging.HttpLoggingInterceptor
|
import okhttp3.logging.HttpLoggingInterceptor
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.net.InetAddress
|
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
class NetworkHelper(context: Context) {
|
class NetworkHelper(context: Context) {
|
||||||
@ -38,25 +35,9 @@ class NetworkHelper(context: Context) {
|
|||||||
builder.addInterceptor(httpLoggingInterceptor)
|
builder.addInterceptor(httpLoggingInterceptor)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (preferences.enableDoh()) {
|
when (preferences.dohProvider()) {
|
||||||
builder.dns(
|
PREF_DOH_CLOUDFLARE -> builder.dohCloudflare()
|
||||||
DnsOverHttps.Builder().client(builder.build())
|
PREF_DOH_GOOGLE -> builder.dohGoogle()
|
||||||
.url("https://cloudflare-dns.com/dns-query".toHttpUrl())
|
|
||||||
.bootstrapDnsHosts(
|
|
||||||
listOf(
|
|
||||||
InetAddress.getByName("162.159.36.1"),
|
|
||||||
InetAddress.getByName("162.159.46.1"),
|
|
||||||
InetAddress.getByName("1.1.1.1"),
|
|
||||||
InetAddress.getByName("1.0.0.1"),
|
|
||||||
InetAddress.getByName("162.159.132.53"),
|
|
||||||
InetAddress.getByName("2606:4700:4700::1111"),
|
|
||||||
InetAddress.getByName("2606:4700:4700::1001"),
|
|
||||||
InetAddress.getByName("2606:4700:4700::0064"),
|
|
||||||
InetAddress.getByName("2606:4700:4700::6400")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
builder.build()
|
builder.build()
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.network
|
package eu.kanade.tachiyomi.network
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.util.system.WebViewUtil
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
|
|
||||||
@ -12,7 +12,7 @@ class UserAgentInterceptor : Interceptor {
|
|||||||
val newRequest = originalRequest
|
val newRequest = originalRequest
|
||||||
.newBuilder()
|
.newBuilder()
|
||||||
.removeHeader("User-Agent")
|
.removeHeader("User-Agent")
|
||||||
.addHeader("User-Agent", WebViewUtil.DEFAULT_USER_AGENT)
|
.addHeader("User-Agent", HttpSource.DEFAULT_USER_AGENT)
|
||||||
.build()
|
.build()
|
||||||
chain.proceed(newRequest)
|
chain.proceed(newRequest)
|
||||||
} else {
|
} else {
|
||||||
|
@ -27,7 +27,7 @@ import java.util.zip.ZipFile
|
|||||||
class LocalSource(private val context: Context) : CatalogueSource {
|
class LocalSource(private val context: Context) : CatalogueSource {
|
||||||
companion object {
|
companion object {
|
||||||
const val ID = 0L
|
const val ID = 0L
|
||||||
const val HELP_URL = "https://tachiyomi.org/help/guides/reading-local-manga/"
|
const val HELP_URL = "https://tachiyomi.org/help/guides/local-manga/"
|
||||||
|
|
||||||
private const val COVER_NAME = "cover.jpg"
|
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")
|
||||||
|
@ -10,7 +10,6 @@ import eu.kanade.tachiyomi.source.model.MangasPage
|
|||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.util.system.WebViewUtil
|
|
||||||
import okhttp3.Headers
|
import okhttp3.Headers
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
@ -75,7 +74,7 @@ abstract class HttpSource : CatalogueSource {
|
|||||||
* Headers builder for requests. Implementations can override this method for custom headers.
|
* Headers builder for requests. Implementations can override this method for custom headers.
|
||||||
*/
|
*/
|
||||||
protected open fun headersBuilder() = Headers.Builder().apply {
|
protected open fun headersBuilder() = Headers.Builder().apply {
|
||||||
add("User-Agent", WebViewUtil.DEFAULT_USER_AGENT)
|
add("User-Agent", DEFAULT_USER_AGENT)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -370,4 +369,8 @@ abstract class HttpSource : CatalogueSource {
|
|||||||
* Returns the list of filters for the source.
|
* Returns the list of filters for the source.
|
||||||
*/
|
*/
|
||||||
override fun getFilterList() = FilterList()
|
override fun getFilterList() = FilterList()
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val DEFAULT_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.150 Safari/537.36 Edg/88.0.705.63"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,7 @@ abstract class BaseThemedActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
val preferences: PreferencesHelper by injectLazy()
|
val preferences: PreferencesHelper by injectLazy()
|
||||||
|
|
||||||
private val isDarkMode: Boolean by lazy {
|
val isDarkMode: Boolean by lazy {
|
||||||
val themeMode = preferences.themeMode().get()
|
val themeMode = preferences.themeMode().get()
|
||||||
(themeMode == Values.ThemeMode.dark) ||
|
(themeMode == Values.ThemeMode.dark) ||
|
||||||
(
|
(
|
||||||
|
@ -11,15 +11,13 @@ 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 com.bluelinelabs.conductor.RestoreViewOnCreateController
|
||||||
import kotlinx.android.extensions.LayoutContainer
|
|
||||||
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) :
|
||||||
RestoreViewOnCreateController(bundle),
|
RestoreViewOnCreateController(bundle) {
|
||||||
LayoutContainer {
|
|
||||||
|
|
||||||
lateinit var binding: VB
|
lateinit var binding: VB
|
||||||
|
|
||||||
@ -53,9 +51,6 @@ abstract class BaseController<VB : ViewBinding>(bundle: Bundle? = null) :
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override val containerView: View?
|
|
||||||
get() = view
|
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup, savedViewState: Bundle?): View {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup, savedViewState: Bundle?): View {
|
||||||
return inflateView(inflater, container)
|
return inflateView(inflater, container)
|
||||||
}
|
}
|
||||||
@ -126,7 +121,7 @@ abstract class BaseController<VB : ViewBinding>(bundle: Bundle? = null) :
|
|||||||
* [expandActionViewFromInteraction] should be set to true in [onOptionsItemSelected] when the expandable item is selected
|
* [expandActionViewFromInteraction] should be set to true in [onOptionsItemSelected] when the expandable item is selected
|
||||||
* This method should be called as part of [MenuItem.OnActionExpandListener.onMenuItemActionExpand]
|
* This method should be called as part of [MenuItem.OnActionExpandListener.onMenuItemActionExpand]
|
||||||
*/
|
*/
|
||||||
fun invalidateMenuOnExpand(): Boolean {
|
open fun invalidateMenuOnExpand(): Boolean {
|
||||||
return if (expandActionViewFromInteraction) {
|
return if (expandActionViewFromInteraction) {
|
||||||
activity?.invalidateOptionsMenu()
|
activity?.invalidateOptionsMenu()
|
||||||
false
|
false
|
||||||
|
@ -1,11 +1,14 @@
|
|||||||
package eu.kanade.tachiyomi.ui.base.controller
|
package eu.kanade.tachiyomi.ui.base.controller
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager.PERMISSION_GRANTED
|
import android.content.pm.PackageManager.PERMISSION_GRANTED
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.net.toUri
|
||||||
import com.bluelinelabs.conductor.Controller
|
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 eu.kanade.tachiyomi.util.system.toast
|
||||||
|
|
||||||
fun Router.popControllerWithTag(tag: String): Boolean {
|
fun Router.popControllerWithTag(tag: String): Boolean {
|
||||||
val controller = getControllerWithTag(tag)
|
val controller = getControllerWithTag(tag)
|
||||||
@ -32,3 +35,12 @@ fun Controller.withFadeTransaction(): RouterTransaction {
|
|||||||
.pushChangeHandler(OneWayFadeChangeHandler())
|
.pushChangeHandler(OneWayFadeChangeHandler())
|
||||||
.popChangeHandler(OneWayFadeChangeHandler())
|
.popChangeHandler(OneWayFadeChangeHandler())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun Controller.openInBrowser(url: String) {
|
||||||
|
try {
|
||||||
|
val intent = Intent(Intent.ACTION_VIEW, url.toUri())
|
||||||
|
startActivity(intent)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
activity?.toast(e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -0,0 +1,196 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.base.controller
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.Menu
|
||||||
|
import android.view.MenuInflater
|
||||||
|
import android.view.MenuItem
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.appcompat.widget.SearchView
|
||||||
|
import androidx.viewbinding.ViewBinding
|
||||||
|
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import reactivecircus.flowbinding.appcompat.QueryTextEvent
|
||||||
|
import reactivecircus.flowbinding.appcompat.queryTextEvents
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation of the NucleusController that has a built-in ViewSearch
|
||||||
|
*/
|
||||||
|
abstract class SearchableNucleusController<VB : ViewBinding, P : BasePresenter<*>>
|
||||||
|
(bundle: Bundle? = null) : NucleusController<VB, P>(bundle) {
|
||||||
|
|
||||||
|
enum class SearchViewState { LOADING, LOADED, COLLAPSING, FOCUSED }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to bypass the initial searchView being set to empty string after an onResume
|
||||||
|
*/
|
||||||
|
private var currentSearchViewState: SearchViewState = SearchViewState.LOADING
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store the query text that has not been submitted to reassign it after an onResume, UI-only
|
||||||
|
*/
|
||||||
|
protected var nonSubmittedQuery: String = ""
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To be called by classes that extend this subclass in onCreateOptionsMenu
|
||||||
|
*/
|
||||||
|
protected fun createOptionsMenu(
|
||||||
|
menu: Menu,
|
||||||
|
inflater: MenuInflater,
|
||||||
|
menuId: Int,
|
||||||
|
searchItemId: Int,
|
||||||
|
@StringRes queryHint: Int? = null,
|
||||||
|
restoreCurrentQuery: Boolean = true
|
||||||
|
) {
|
||||||
|
// Inflate menu
|
||||||
|
inflater.inflate(menuId, menu)
|
||||||
|
|
||||||
|
// Initialize search option.
|
||||||
|
val searchItem = menu.findItem(searchItemId)
|
||||||
|
val searchView = searchItem.actionView as SearchView
|
||||||
|
searchItem.fixExpand(onExpand = { invalidateMenuOnExpand() })
|
||||||
|
searchView.maxWidth = Int.MAX_VALUE
|
||||||
|
|
||||||
|
searchView.queryTextEvents()
|
||||||
|
.onEach {
|
||||||
|
val newText = it.queryText.toString()
|
||||||
|
|
||||||
|
if (newText.isNotBlank() or acceptEmptyQuery()) {
|
||||||
|
if (it is QueryTextEvent.QuerySubmitted) {
|
||||||
|
// Abstract function for implementation
|
||||||
|
// Run it first in case the old query data is needed (like BrowseSourceController)
|
||||||
|
onSearchViewQueryTextSubmit(newText)
|
||||||
|
presenter.query = newText
|
||||||
|
nonSubmittedQuery = ""
|
||||||
|
} else if ((it is QueryTextEvent.QueryChanged) && (presenter.query != newText)) {
|
||||||
|
nonSubmittedQuery = newText
|
||||||
|
|
||||||
|
// Abstract function for implementation
|
||||||
|
onSearchViewQueryTextChange(newText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// clear the collapsing flag
|
||||||
|
setCurrentSearchViewState(SearchViewState.LOADED, SearchViewState.COLLAPSING)
|
||||||
|
}
|
||||||
|
.launchIn(viewScope)
|
||||||
|
|
||||||
|
val query = presenter.query
|
||||||
|
|
||||||
|
// Restoring a query the user had not submitted
|
||||||
|
if (nonSubmittedQuery.isNotBlank() and (nonSubmittedQuery != query)) {
|
||||||
|
searchItem.expandActionView()
|
||||||
|
searchView.setQuery(nonSubmittedQuery, false)
|
||||||
|
onSearchViewQueryTextChange(nonSubmittedQuery)
|
||||||
|
} else {
|
||||||
|
if (queryHint != null) {
|
||||||
|
searchView.queryHint = applicationContext?.getString(queryHint)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (restoreCurrentQuery) {
|
||||||
|
// Restoring a query the user had submitted
|
||||||
|
if (query.isNotBlank()) {
|
||||||
|
searchItem.expandActionView()
|
||||||
|
searchView.setQuery(query, true)
|
||||||
|
searchView.clearFocus()
|
||||||
|
onSearchViewQueryTextChange(query)
|
||||||
|
onSearchViewQueryTextSubmit(query)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Workaround for weird behavior where searchView gets empty text change despite
|
||||||
|
// query being set already, prevents the query from being cleared
|
||||||
|
binding.root.post {
|
||||||
|
setCurrentSearchViewState(SearchViewState.LOADED, SearchViewState.LOADING)
|
||||||
|
}
|
||||||
|
|
||||||
|
searchView.setOnQueryTextFocusChangeListener { _, hasFocus ->
|
||||||
|
if (hasFocus) {
|
||||||
|
setCurrentSearchViewState(SearchViewState.FOCUSED)
|
||||||
|
} else {
|
||||||
|
setCurrentSearchViewState(SearchViewState.LOADED, SearchViewState.FOCUSED)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
searchItem.setOnActionExpandListener(
|
||||||
|
object : MenuItem.OnActionExpandListener {
|
||||||
|
override fun onMenuItemActionExpand(item: MenuItem?): Boolean {
|
||||||
|
onSearchMenuItemActionExpand(item)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMenuItemActionCollapse(item: MenuItem?): Boolean {
|
||||||
|
val localSearchView = searchItem.actionView as SearchView
|
||||||
|
|
||||||
|
// if it is blank the flow event won't trigger so we would stay in a COLLAPSING state
|
||||||
|
if (localSearchView.toString().isNotBlank()) {
|
||||||
|
setCurrentSearchViewState(SearchViewState.COLLAPSING)
|
||||||
|
}
|
||||||
|
|
||||||
|
onSearchMenuItemActionCollapse(item)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActivityResumed(activity: Activity) {
|
||||||
|
super.onActivityResumed(activity)
|
||||||
|
// Until everything is up and running don't accept empty queries
|
||||||
|
setCurrentSearchViewState(SearchViewState.LOADING)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun acceptEmptyQuery(): Boolean {
|
||||||
|
return when (currentSearchViewState) {
|
||||||
|
SearchViewState.COLLAPSING, SearchViewState.FOCUSED -> true
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setCurrentSearchViewState(to: SearchViewState, from: SearchViewState? = null) {
|
||||||
|
// When loading ignore all requests other than loaded
|
||||||
|
if ((currentSearchViewState == SearchViewState.LOADING) && (to != SearchViewState.LOADED)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent changing back to an unwanted state when using async flows (ie onFocus event doing
|
||||||
|
// COLLAPSING -> LOADED)
|
||||||
|
if ((from != null) && (currentSearchViewState != from)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
currentSearchViewState = to
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called by the SearchView since since the implementation of these can vary in subclasses
|
||||||
|
* Not abstract as they are optional
|
||||||
|
*/
|
||||||
|
protected open fun onSearchViewQueryTextChange(newText: String?) {
|
||||||
|
}
|
||||||
|
|
||||||
|
protected open fun onSearchViewQueryTextSubmit(query: String?) {
|
||||||
|
}
|
||||||
|
|
||||||
|
protected open fun onSearchMenuItemActionExpand(item: MenuItem?) {
|
||||||
|
}
|
||||||
|
|
||||||
|
protected open fun onSearchMenuItemActionCollapse(item: MenuItem?) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* During the conversion to SearchableNucleusController (after which I plan to merge its code
|
||||||
|
* into BaseController) this addresses an issue where the searchView.onTextFocus event is not
|
||||||
|
* triggered
|
||||||
|
*/
|
||||||
|
override fun invalidateMenuOnExpand(): Boolean {
|
||||||
|
return if (expandActionViewFromInteraction) {
|
||||||
|
activity?.invalidateOptionsMenu()
|
||||||
|
setCurrentSearchViewState(SearchViewState.FOCUSED) // we are technically focused here
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,11 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.ui.base.holder
|
|
||||||
|
|
||||||
import android.view.View
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import kotlinx.android.extensions.LayoutContainer
|
|
||||||
|
|
||||||
abstract class BaseViewHolder(view: View) : RecyclerView.ViewHolder(view), LayoutContainer {
|
|
||||||
|
|
||||||
override val containerView: View?
|
|
||||||
get() = itemView
|
|
||||||
}
|
|
@ -12,6 +12,11 @@ open class BasePresenter<V> : RxPresenter<V>() {
|
|||||||
|
|
||||||
lateinit var presenterScope: CoroutineScope
|
lateinit var presenterScope: CoroutineScope
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query from the view where applicable
|
||||||
|
*/
|
||||||
|
var query: String = ""
|
||||||
|
|
||||||
override fun onCreate(savedState: Bundle?) {
|
override fun onCreate(savedState: Bundle?) {
|
||||||
try {
|
try {
|
||||||
super.onCreate(savedState)
|
super.onCreate(savedState)
|
||||||
|
@ -10,6 +10,7 @@ import androidx.appcompat.widget.SearchView
|
|||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import com.bluelinelabs.conductor.ControllerChangeHandler
|
import com.bluelinelabs.conductor.ControllerChangeHandler
|
||||||
import com.bluelinelabs.conductor.ControllerChangeType
|
import com.bluelinelabs.conductor.ControllerChangeType
|
||||||
|
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
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
@ -58,6 +59,11 @@ open class ExtensionController :
|
|||||||
|
|
||||||
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
|
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
|
||||||
binding = ExtensionControllerBinding.inflate(inflater)
|
binding = ExtensionControllerBinding.inflate(inflater)
|
||||||
|
binding.recycler.applyInsetter {
|
||||||
|
type(navigationBars = true) {
|
||||||
|
padding()
|
||||||
|
}
|
||||||
|
}
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -104,6 +110,8 @@ open class ExtensionController :
|
|||||||
override fun onButtonClick(position: Int) {
|
override fun onButtonClick(position: Int) {
|
||||||
val extension = (adapter?.getItem(position) as? ExtensionItem)?.extension ?: return
|
val extension = (adapter?.getItem(position) as? ExtensionItem)?.extension ?: return
|
||||||
when (extension) {
|
when (extension) {
|
||||||
|
is Extension.Available -> presenter.installExtension(extension)
|
||||||
|
is Extension.Untrusted -> openTrustDialog(extension)
|
||||||
is Extension.Installed -> {
|
is Extension.Installed -> {
|
||||||
if (!extension.hasUpdate) {
|
if (!extension.hasUpdate) {
|
||||||
openDetails(extension)
|
openDetails(extension)
|
||||||
@ -111,12 +119,6 @@ open class ExtensionController :
|
|||||||
presenter.updateExtension(extension)
|
presenter.updateExtension(extension)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is Extension.Available -> {
|
|
||||||
presenter.installExtension(extension)
|
|
||||||
}
|
|
||||||
is Extension.Untrusted -> {
|
|
||||||
openTrustDialog(extension)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -147,12 +149,11 @@ open class ExtensionController :
|
|||||||
|
|
||||||
override fun onItemClick(view: View, position: Int): Boolean {
|
override fun onItemClick(view: View, position: Int): Boolean {
|
||||||
val extension = (adapter?.getItem(position) as? ExtensionItem)?.extension ?: return false
|
val extension = (adapter?.getItem(position) as? ExtensionItem)?.extension ?: return false
|
||||||
if (extension is Extension.Installed) {
|
when (extension) {
|
||||||
openDetails(extension)
|
is Extension.Available -> presenter.installExtension(extension)
|
||||||
} else if (extension is Extension.Untrusted) {
|
is Extension.Untrusted -> openTrustDialog(extension)
|
||||||
openTrustDialog(extension)
|
is Extension.Installed -> openDetails(extension)
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,7 +14,6 @@ import android.view.MenuItem
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.appcompat.view.ContextThemeWrapper
|
import androidx.appcompat.view.ContextThemeWrapper
|
||||||
import androidx.core.net.toUri
|
|
||||||
import androidx.core.os.bundleOf
|
import androidx.core.os.bundleOf
|
||||||
import androidx.preference.Preference
|
import androidx.preference.Preference
|
||||||
import androidx.preference.PreferenceGroupAdapter
|
import androidx.preference.PreferenceGroupAdapter
|
||||||
@ -23,6 +22,7 @@ import androidx.preference.PreferenceScreen
|
|||||||
import androidx.preference.SwitchPreferenceCompat
|
import androidx.preference.SwitchPreferenceCompat
|
||||||
import androidx.recyclerview.widget.ConcatAdapter
|
import androidx.recyclerview.widget.ConcatAdapter
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import dev.chrisbanes.insetter.applyInsetter
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.preference.EmptyPreferenceDataStore
|
import eu.kanade.tachiyomi.data.preference.EmptyPreferenceDataStore
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
@ -36,6 +36,7 @@ import eu.kanade.tachiyomi.source.Source
|
|||||||
import eu.kanade.tachiyomi.source.getPreferenceKey
|
import eu.kanade.tachiyomi.source.getPreferenceKey
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.ToolbarLiftOnScrollController
|
import eu.kanade.tachiyomi.ui.base.controller.ToolbarLiftOnScrollController
|
||||||
|
import eu.kanade.tachiyomi.ui.base.controller.openInBrowser
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
||||||
import eu.kanade.tachiyomi.util.preference.DSL
|
import eu.kanade.tachiyomi.util.preference.DSL
|
||||||
import eu.kanade.tachiyomi.util.preference.onChange
|
import eu.kanade.tachiyomi.util.preference.onChange
|
||||||
@ -67,6 +68,11 @@ class ExtensionDetailsController(bundle: Bundle? = null) :
|
|||||||
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
|
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
|
||||||
val themedInflater = inflater.cloneInContext(getPreferenceThemeContext())
|
val themedInflater = inflater.cloneInContext(getPreferenceThemeContext())
|
||||||
binding = ExtensionDetailControllerBinding.inflate(themedInflater)
|
binding = ExtensionDetailControllerBinding.inflate(themedInflater)
|
||||||
|
binding.extensionPrefsRecycler.applyInsetter {
|
||||||
|
type(navigationBars = true) {
|
||||||
|
padding()
|
||||||
|
}
|
||||||
|
}
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -208,9 +214,12 @@ class ExtensionDetailsController(bundle: Bundle? = null) :
|
|||||||
|
|
||||||
private fun openCommitHistory() {
|
private fun openCommitHistory() {
|
||||||
val pkgName = presenter.extension!!.pkgName.substringAfter("eu.kanade.tachiyomi.extension.")
|
val pkgName = presenter.extension!!.pkgName.substringAfter("eu.kanade.tachiyomi.extension.")
|
||||||
val url = "https://github.com/tachiyomiorg/tachiyomi-extensions/commits/master/src/${pkgName.replace(".", "/")}"
|
val pkgFactory = presenter.extension!!.pkgFactory
|
||||||
val intent = Intent(Intent.ACTION_VIEW, url.toUri())
|
val url = when {
|
||||||
startActivity(intent)
|
!pkgFactory.isNullOrEmpty() -> "$URL_EXTENSION_COMMITS/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/$pkgFactory"
|
||||||
|
else -> "$URL_EXTENSION_COMMITS/src/${pkgName.replace(".", "/")}"
|
||||||
|
}
|
||||||
|
openInBrowser(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun openInSettings() {
|
private fun openInSettings() {
|
||||||
@ -232,5 +241,7 @@ class ExtensionDetailsController(bundle: Bundle? = null) :
|
|||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
const val PKGNAME_KEY = "pkg_name"
|
const val PKGNAME_KEY = "pkg_name"
|
||||||
|
|
||||||
|
private const val URL_EXTENSION_COMMITS = "https://github.com/tachiyomiorg/tachiyomi-extensions/commits/master"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,10 +4,10 @@ import android.annotation.SuppressLint
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
import android.view.ContextThemeWrapper
|
|
||||||
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 androidx.appcompat.view.ContextThemeWrapper
|
||||||
import androidx.core.os.bundleOf
|
import androidx.core.os.bundleOf
|
||||||
import androidx.preference.DialogPreference
|
import androidx.preference.DialogPreference
|
||||||
import androidx.preference.EditTextPreference
|
import androidx.preference.EditTextPreference
|
||||||
|
@ -0,0 +1,14 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.browse.migration.manga
|
||||||
|
|
||||||
|
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||||
|
import eu.davidea.flexibleadapter.items.IFlexible
|
||||||
|
|
||||||
|
class MigrationMangaAdapter(controller: MigrationMangaController) :
|
||||||
|
FlexibleAdapter<IFlexible<*>>(null, controller, true) {
|
||||||
|
|
||||||
|
val coverClickListener: OnCoverClickListener = controller
|
||||||
|
|
||||||
|
interface OnCoverClickListener {
|
||||||
|
fun onCoverClick(position: Int)
|
||||||
|
}
|
||||||
|
}
|
@ -7,17 +7,18 @@ import android.view.ViewGroup
|
|||||||
import androidx.core.os.bundleOf
|
import androidx.core.os.bundleOf
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||||
import eu.davidea.flexibleadapter.items.IFlexible
|
|
||||||
import eu.kanade.tachiyomi.databinding.MigrationMangaControllerBinding
|
import eu.kanade.tachiyomi.databinding.MigrationMangaControllerBinding
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
||||||
import eu.kanade.tachiyomi.ui.browse.migration.search.SearchController
|
import eu.kanade.tachiyomi.ui.browse.migration.search.SearchController
|
||||||
|
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||||
|
|
||||||
class MigrationMangaController :
|
class MigrationMangaController :
|
||||||
NucleusController<MigrationMangaControllerBinding, MigrationMangaPresenter>,
|
NucleusController<MigrationMangaControllerBinding, MigrationMangaPresenter>,
|
||||||
FlexibleAdapter.OnItemClickListener {
|
FlexibleAdapter.OnItemClickListener,
|
||||||
|
MigrationMangaAdapter.OnCoverClickListener {
|
||||||
|
|
||||||
private var adapter: FlexibleAdapter<IFlexible<*>>? = null
|
private var adapter: MigrationMangaAdapter? = null
|
||||||
|
|
||||||
constructor(sourceId: Long, sourceName: String?) : super(
|
constructor(sourceId: Long, sourceName: String?) : super(
|
||||||
bundleOf(
|
bundleOf(
|
||||||
@ -51,7 +52,7 @@ class MigrationMangaController :
|
|||||||
override fun onViewCreated(view: View) {
|
override fun onViewCreated(view: View) {
|
||||||
super.onViewCreated(view)
|
super.onViewCreated(view)
|
||||||
|
|
||||||
adapter = FlexibleAdapter<IFlexible<*>>(null, this)
|
adapter = MigrationMangaAdapter(this)
|
||||||
binding.recycler.layoutManager = LinearLayoutManager(view.context)
|
binding.recycler.layoutManager = LinearLayoutManager(view.context)
|
||||||
binding.recycler.adapter = adapter
|
binding.recycler.adapter = adapter
|
||||||
adapter?.fastScroller = binding.fastScroller
|
adapter?.fastScroller = binding.fastScroller
|
||||||
@ -62,17 +63,22 @@ class MigrationMangaController :
|
|||||||
super.onDestroyView(view)
|
super.onDestroyView(view)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setManga(manga: List<MangaItem>) {
|
fun setManga(manga: List<MigrationMangaItem>) {
|
||||||
adapter?.updateDataSet(manga)
|
adapter?.updateDataSet(manga)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onItemClick(view: View, position: Int): Boolean {
|
override fun onItemClick(view: View, position: Int): Boolean {
|
||||||
val item = adapter?.getItem(position) as? MangaItem ?: return false
|
val item = adapter?.getItem(position) as? MigrationMangaItem ?: return false
|
||||||
val controller = SearchController(item.manga)
|
val controller = SearchController(item.manga)
|
||||||
router.pushController(controller.withFadeTransaction())
|
router.pushController(controller.withFadeTransaction())
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onCoverClick(position: Int) {
|
||||||
|
val mangaItem = adapter?.getItem(position) as? MigrationMangaItem ?: return
|
||||||
|
router.pushController(MangaController(mangaItem.manga).withFadeTransaction())
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val SOURCE_ID_EXTRA = "source_id_extra"
|
const val SOURCE_ID_EXTRA = "source_id_extra"
|
||||||
const val SOURCE_NAME_EXTRA = "source_name_extra"
|
const val SOURCE_NAME_EXTRA = "source_name_extra"
|
||||||
|
@ -5,29 +5,27 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy
|
|||||||
import com.bumptech.glide.load.resource.bitmap.CenterCrop
|
import com.bumptech.glide.load.resource.bitmap.CenterCrop
|
||||||
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
|
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
|
||||||
import com.bumptech.glide.request.RequestOptions
|
import com.bumptech.glide.request.RequestOptions
|
||||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
|
||||||
import eu.davidea.viewholders.FlexibleViewHolder
|
import eu.davidea.viewholders.FlexibleViewHolder
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.glide.GlideApp
|
import eu.kanade.tachiyomi.data.glide.GlideApp
|
||||||
import eu.kanade.tachiyomi.data.glide.toMangaThumbnail
|
import eu.kanade.tachiyomi.data.glide.toMangaThumbnail
|
||||||
import eu.kanade.tachiyomi.databinding.SourceListItemBinding
|
import eu.kanade.tachiyomi.databinding.SourceListItemBinding
|
||||||
|
|
||||||
class MangaHolder(
|
class MigrationMangaHolder(
|
||||||
view: View,
|
view: View,
|
||||||
adapter: FlexibleAdapter<*>
|
private val adapter: MigrationMangaAdapter
|
||||||
) : FlexibleViewHolder(view, adapter) {
|
) : FlexibleViewHolder(view, adapter) {
|
||||||
|
|
||||||
private val binding = SourceListItemBinding.bind(view)
|
private val binding = SourceListItemBinding.bind(view)
|
||||||
|
|
||||||
fun bind(item: MangaItem) {
|
init {
|
||||||
// Update the title of the manga.
|
|
||||||
binding.title.text = item.manga.title
|
|
||||||
|
|
||||||
// Create thumbnail onclick to simulate long click
|
|
||||||
binding.thumbnail.setOnClickListener {
|
binding.thumbnail.setOnClickListener {
|
||||||
// Simulate long click on this view to enter selection mode
|
adapter.coverClickListener.onCoverClick(bindingAdapterPosition)
|
||||||
onLongClick(itemView)
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun bind(item: MigrationMangaItem) {
|
||||||
|
binding.title.text = item.manga.title
|
||||||
|
|
||||||
// Update the cover.
|
// Update the cover.
|
||||||
GlideApp.with(itemView.context).clear(binding.thumbnail)
|
GlideApp.with(itemView.context).clear(binding.thumbnail)
|
@ -1,6 +1,5 @@
|
|||||||
package eu.kanade.tachiyomi.ui.browse.migration.manga
|
package eu.kanade.tachiyomi.ui.browse.migration.manga
|
||||||
|
|
||||||
import android.os.Parcelable
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||||
@ -8,25 +7,20 @@ import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
|
|||||||
import eu.davidea.flexibleadapter.items.IFlexible
|
import eu.davidea.flexibleadapter.items.IFlexible
|
||||||
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 kotlinx.parcelize.Parcelize
|
|
||||||
|
|
||||||
@Parcelize
|
class MigrationMangaItem(val manga: Manga) : AbstractFlexibleItem<MigrationMangaHolder>() {
|
||||||
class MangaItem(val manga: Manga) : AbstractFlexibleItem<MangaHolder>(), Parcelable {
|
|
||||||
|
|
||||||
override fun getLayoutRes(): Int {
|
override fun getLayoutRes(): Int {
|
||||||
return R.layout.source_list_item
|
return R.layout.source_list_item
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): MangaHolder {
|
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): MigrationMangaHolder {
|
||||||
return MangaHolder(
|
return MigrationMangaHolder(view, adapter as MigrationMangaAdapter)
|
||||||
view,
|
|
||||||
adapter
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun bindViewHolder(
|
override fun bindViewHolder(
|
||||||
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
|
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
|
||||||
holder: MangaHolder,
|
holder: MigrationMangaHolder,
|
||||||
position: Int,
|
position: Int,
|
||||||
payloads: List<Any?>?
|
payloads: List<Any?>?
|
||||||
) {
|
) {
|
||||||
@ -34,7 +28,7 @@ class MangaItem(val manga: Manga) : AbstractFlexibleItem<MangaHolder>(), Parcela
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
if (other is MangaItem) {
|
if (other is MigrationMangaItem) {
|
||||||
return manga.id == other.manga.id
|
return manga.id == other.manga.id
|
||||||
}
|
}
|
||||||
return false
|
return false
|
@ -23,9 +23,9 @@ class MigrationMangaPresenter(
|
|||||||
.subscribeLatestCache(MigrationMangaController::setManga)
|
.subscribeLatestCache(MigrationMangaController::setManga)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun libraryToMigrationItem(library: List<Manga>): List<MangaItem> {
|
private fun libraryToMigrationItem(library: List<Manga>): List<MigrationMangaItem> {
|
||||||
return library.filter { it.source == sourceId }
|
return library.filter { it.source == sourceId }
|
||||||
.sortedBy { it.title }
|
.sortedBy { it.title }
|
||||||
.map { MangaItem(it) }
|
.map { MigrationMangaItem(it) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,10 +5,13 @@ import android.os.Bundle
|
|||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import com.afollestad.materialdialogs.MaterialDialog
|
import com.afollestad.materialdialogs.MaterialDialog
|
||||||
import com.afollestad.materialdialogs.list.listItemsMultiChoice
|
import com.afollestad.materialdialogs.list.listItemsMultiChoice
|
||||||
|
import com.bluelinelabs.conductor.Controller
|
||||||
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
|
||||||
|
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||||
|
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
||||||
import eu.kanade.tachiyomi.ui.browse.migration.MigrationFlags
|
import eu.kanade.tachiyomi.ui.browse.migration.MigrationFlags
|
||||||
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.globalsearch.GlobalSearchPresenter
|
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchPresenter
|
||||||
@ -39,16 +42,16 @@ class SearchController(
|
|||||||
newManga = savedInstanceState.getSerializable(::newManga.name) as? Manga
|
newManga = savedInstanceState.getSerializable(::newManga.name) as? Manga
|
||||||
}
|
}
|
||||||
|
|
||||||
fun migrateManga() {
|
fun migrateManga(manga: Manga? = null, newManga: Manga?) {
|
||||||
val manga = manga ?: return
|
manga ?: return
|
||||||
val newManga = newManga ?: return
|
newManga ?: return
|
||||||
|
|
||||||
(presenter as? SearchPresenter)?.migrateManga(manga, newManga, true)
|
(presenter as? SearchPresenter)?.migrateManga(manga, newManga, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun copyManga() {
|
fun copyManga(manga: Manga? = null, newManga: Manga?) {
|
||||||
val manga = manga ?: return
|
manga ?: return
|
||||||
val newManga = newManga ?: return
|
newManga ?: return
|
||||||
|
|
||||||
(presenter as? SearchPresenter)?.migrateManga(manga, newManga, false)
|
(presenter as? SearchPresenter)?.migrateManga(manga, newManga, false)
|
||||||
}
|
}
|
||||||
@ -56,7 +59,7 @@ class SearchController(
|
|||||||
override fun onMangaClick(manga: Manga) {
|
override fun onMangaClick(manga: Manga) {
|
||||||
newManga = manga
|
newManga = manga
|
||||||
val dialog =
|
val dialog =
|
||||||
MigrationDialog()
|
MigrationDialog(this.manga, newManga, this)
|
||||||
dialog.targetController = this
|
dialog.targetController = this
|
||||||
dialog.showDialog(router)
|
dialog.showDialog(router)
|
||||||
}
|
}
|
||||||
@ -75,7 +78,7 @@ class SearchController(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class MigrationDialog : DialogController() {
|
class MigrationDialog(private val manga: Manga? = null, private val newManga: Manga? = null, private val callingController: Controller? = null) : DialogController() {
|
||||||
|
|
||||||
private val preferences: PreferencesHelper by injectLazy()
|
private val preferences: PreferencesHelper by injectLazy()
|
||||||
|
|
||||||
@ -88,7 +91,7 @@ class SearchController(
|
|||||||
)
|
)
|
||||||
|
|
||||||
return MaterialDialog(activity!!)
|
return MaterialDialog(activity!!)
|
||||||
.message(R.string.migration_dialog_what_to_include)
|
.title(R.string.migration_dialog_what_to_include)
|
||||||
.listItemsMultiChoice(
|
.listItemsMultiChoice(
|
||||||
items = MigrationFlags.titles.map { resources?.getString(it) as CharSequence },
|
items = MigrationFlags.titles.map { resources?.getString(it) as CharSequence },
|
||||||
initialSelection = preselected.toIntArray()
|
initialSelection = preselected.toIntArray()
|
||||||
@ -101,12 +104,28 @@ class SearchController(
|
|||||||
preferences.migrateFlags().set(newValue)
|
preferences.migrateFlags().set(newValue)
|
||||||
}
|
}
|
||||||
.positiveButton(R.string.migrate) {
|
.positiveButton(R.string.migrate) {
|
||||||
(targetController as? SearchController)?.migrateManga()
|
if (callingController != null) {
|
||||||
|
if (callingController.javaClass == SourceSearchController::class.java) {
|
||||||
|
router.popController(callingController)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(targetController as? SearchController)?.migrateManga(manga, newManga)
|
||||||
}
|
}
|
||||||
.negativeButton(R.string.copy) {
|
.negativeButton(R.string.copy) {
|
||||||
(targetController as? SearchController)?.copyManga()
|
if (callingController != null) {
|
||||||
|
if (callingController.javaClass == SourceSearchController::class.java) {
|
||||||
|
router.popController(callingController)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(targetController as? SearchController)?.copyManga(manga, newManga)
|
||||||
}
|
}
|
||||||
.neutralButton(android.R.string.cancel)
|
.neutralButton(android.R.string.cancel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onTitleClick(source: CatalogueSource) {
|
||||||
|
presenter.preferences.lastUsedSource().set(source.id)
|
||||||
|
|
||||||
|
router.pushController(SourceSearchController(manga, source, presenter.query).withFadeTransaction())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,8 @@ import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchPresenter
|
|||||||
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
|
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
|
||||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||||
import eu.kanade.tachiyomi.util.lang.launchUI
|
import eu.kanade.tachiyomi.util.lang.launchUI
|
||||||
|
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||||
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
|
||||||
class SearchPresenter(
|
class SearchPresenter(
|
||||||
@ -56,11 +58,15 @@ class SearchPresenter(
|
|||||||
replacingMangaRelay.call(true)
|
replacingMangaRelay.call(true)
|
||||||
|
|
||||||
presenterScope.launchIO {
|
presenterScope.launchIO {
|
||||||
val chapters = source.getChapterList(manga.toMangaInfo())
|
try {
|
||||||
.map { it.toSChapter() }
|
val chapters = source.getChapterList(manga.toMangaInfo())
|
||||||
|
.map { it.toSChapter() }
|
||||||
|
|
||||||
|
migrateMangaInternal(source, chapters, prevManga, manga, replace)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
withUIContext { view?.applicationContext?.toast(e.message) }
|
||||||
|
}
|
||||||
|
|
||||||
migrateMangaInternal(source, chapters, prevManga, manga, replace)
|
|
||||||
}.invokeOnCompletion {
|
|
||||||
presenterScope.launchUI { replacingMangaRelay.call(false) }
|
presenterScope.launchUI { replacingMangaRelay.call(false) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,39 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.browse.migration.search
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||||
|
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
|
||||||
|
import eu.kanade.tachiyomi.ui.browse.source.browse.SourceItem
|
||||||
|
|
||||||
|
class SourceSearchController(
|
||||||
|
bundle: Bundle
|
||||||
|
) : BrowseSourceController(bundle) {
|
||||||
|
|
||||||
|
constructor(manga: Manga? = null, source: CatalogueSource, searchQuery: String? = null) : this(
|
||||||
|
Bundle().apply {
|
||||||
|
putLong(SOURCE_ID_KEY, source.id)
|
||||||
|
putSerializable(MANGA_KEY, manga)
|
||||||
|
if (searchQuery != null) {
|
||||||
|
putString(SEARCH_QUERY_KEY, searchQuery)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
private var oldManga: Manga? = args.getSerializable(MANGA_KEY) as Manga?
|
||||||
|
private var newManga: Manga? = null
|
||||||
|
|
||||||
|
override fun onItemClick(view: View, position: Int): Boolean {
|
||||||
|
val item = adapter?.getItem(position) as? SourceItem ?: return false
|
||||||
|
newManga = item.manga
|
||||||
|
val searchController = router.backstack.findLast { it.controller().javaClass == SearchController::class.java }?.controller() as SearchController?
|
||||||
|
val dialog =
|
||||||
|
SearchController.MigrationDialog(oldManga, newManga, this)
|
||||||
|
dialog.targetController = searchController
|
||||||
|
dialog.showDialog(router)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
private companion object {
|
||||||
|
const val MANGA_KEY = "oldManga"
|
||||||
|
}
|
||||||
|
}
|
@ -7,6 +7,7 @@ import android.view.MenuItem
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import dev.chrisbanes.insetter.applyInsetter
|
||||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.databinding.MigrationSourcesControllerBinding
|
import eu.kanade.tachiyomi.databinding.MigrationSourcesControllerBinding
|
||||||
@ -31,6 +32,11 @@ class MigrationSourcesController :
|
|||||||
|
|
||||||
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
|
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
|
||||||
binding = MigrationSourcesControllerBinding.inflate(inflater)
|
binding = MigrationSourcesControllerBinding.inflate(inflater)
|
||||||
|
binding.recycler.applyInsetter {
|
||||||
|
type(navigationBars = true) {
|
||||||
|
padding()
|
||||||
|
}
|
||||||
|
}
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,9 +27,14 @@ class MigrationSourcesPresenter(
|
|||||||
|
|
||||||
private fun findSourcesWithManga(library: List<Manga>): List<SourceItem> {
|
private fun findSourcesWithManga(library: List<Manga>): List<SourceItem> {
|
||||||
val header = SelectionHeader()
|
val header = SelectionHeader()
|
||||||
return library.asSequence().map { it.source }.toSet()
|
return library
|
||||||
.mapNotNull { if (it != LocalSource.ID) sourceManager.getOrStub(it) else null }
|
.groupBy { it.source }
|
||||||
.sortedBy { it.name.toLowerCase() }
|
.filterKeys { it != LocalSource.ID }
|
||||||
.map { SourceItem(it, header) }.toList()
|
.map {
|
||||||
|
val source = sourceManager.getOrStub(it.key)
|
||||||
|
SourceItem(source, it.value.size, header)
|
||||||
|
}
|
||||||
|
.sortedBy { it.source.name.toLowerCase() }
|
||||||
|
.toList()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
package eu.kanade.tachiyomi.ui.browse.migration.sources
|
package eu.kanade.tachiyomi.ui.browse.migration.sources
|
||||||
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import androidx.core.view.isVisible
|
||||||
import eu.davidea.viewholders.FlexibleViewHolder
|
import eu.davidea.viewholders.FlexibleViewHolder
|
||||||
import eu.kanade.tachiyomi.databinding.SourceMainControllerCardItemBinding
|
import eu.kanade.tachiyomi.databinding.SourceMainControllerCardItemBinding
|
||||||
import eu.kanade.tachiyomi.source.icon
|
import eu.kanade.tachiyomi.source.icon
|
||||||
|
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||||
|
|
||||||
class SourceHolder(view: View, val adapter: SourceAdapter) :
|
class SourceHolder(view: View, val adapter: SourceAdapter) :
|
||||||
FlexibleViewHolder(view, adapter) {
|
FlexibleViewHolder(view, adapter) {
|
||||||
@ -13,10 +15,10 @@ class SourceHolder(view: View, val adapter: SourceAdapter) :
|
|||||||
fun bind(item: SourceItem) {
|
fun bind(item: SourceItem) {
|
||||||
val source = item.source
|
val source = item.source
|
||||||
|
|
||||||
// Set source name
|
binding.title.text = "${source.name} (${item.mangaCount})"
|
||||||
binding.title.text = source.name
|
binding.subtitle.isVisible = source.lang != ""
|
||||||
|
binding.subtitle.text = LocaleHelper.getDisplayName(source.lang)
|
||||||
|
|
||||||
// Set source icon
|
|
||||||
itemView.post {
|
itemView.post {
|
||||||
binding.image.setImageDrawable(source.icon())
|
binding.image.setImageDrawable(source.icon())
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,7 @@ import eu.kanade.tachiyomi.source.Source
|
|||||||
* @param source Instance of [Source] containing source information.
|
* @param source Instance of [Source] containing source information.
|
||||||
* @param header The header for this item.
|
* @param header The header for this item.
|
||||||
*/
|
*/
|
||||||
data class SourceItem(val source: Source, val header: SelectionHeader) :
|
data class SourceItem(val source: Source, val mangaCount: Int, val header: SelectionHeader) :
|
||||||
AbstractSectionableItem<SourceHolder, SelectionHeader>(header) {
|
AbstractSectionableItem<SourceHolder, SelectionHeader>(header) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -9,12 +9,12 @@ import android.view.MenuInflater
|
|||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.appcompat.widget.SearchView
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import com.afollestad.materialdialogs.MaterialDialog
|
import com.afollestad.materialdialogs.MaterialDialog
|
||||||
import com.afollestad.materialdialogs.list.listItems
|
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 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
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
@ -26,18 +26,13 @@ import eu.kanade.tachiyomi.source.CatalogueSource
|
|||||||
import eu.kanade.tachiyomi.source.LocalSource
|
import eu.kanade.tachiyomi.source.LocalSource
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
import eu.kanade.tachiyomi.ui.base.controller.SearchableNucleusController
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.requestPermissionsSafe
|
import eu.kanade.tachiyomi.ui.base.controller.requestPermissionsSafe
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
||||||
import eu.kanade.tachiyomi.ui.browse.BrowseController
|
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 kotlinx.coroutines.flow.filterIsInstance
|
|
||||||
import kotlinx.coroutines.flow.launchIn
|
|
||||||
import kotlinx.coroutines.flow.onEach
|
|
||||||
import reactivecircus.flowbinding.appcompat.QueryTextEvent
|
|
||||||
import reactivecircus.flowbinding.appcompat.queryTextEvents
|
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
@ -48,7 +43,7 @@ import uy.kohesive.injekt.api.get
|
|||||||
* [SourceAdapter.OnLatestClickListener] call function data on latest item click
|
* [SourceAdapter.OnLatestClickListener] call function data on latest item click
|
||||||
*/
|
*/
|
||||||
class SourceController :
|
class SourceController :
|
||||||
NucleusController<SourceMainControllerBinding, SourcePresenter>(),
|
SearchableNucleusController<SourceMainControllerBinding, SourcePresenter>(),
|
||||||
FlexibleAdapter.OnItemClickListener,
|
FlexibleAdapter.OnItemClickListener,
|
||||||
FlexibleAdapter.OnItemLongClickListener,
|
FlexibleAdapter.OnItemLongClickListener,
|
||||||
SourceAdapter.OnSourceClickListener {
|
SourceAdapter.OnSourceClickListener {
|
||||||
@ -81,6 +76,11 @@ class SourceController :
|
|||||||
*/
|
*/
|
||||||
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
|
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
|
||||||
binding = SourceMainControllerBinding.inflate(inflater)
|
binding = SourceMainControllerBinding.inflate(inflater)
|
||||||
|
binding.recycler.applyInsetter {
|
||||||
|
type(navigationBars = true) {
|
||||||
|
padding()
|
||||||
|
}
|
||||||
|
}
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -200,37 +200,6 @@ class SourceController :
|
|||||||
parentController!!.router.pushController(controller.withFadeTransaction())
|
parentController!!.router.pushController(controller.withFadeTransaction())
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds items to the options menu.
|
|
||||||
*
|
|
||||||
* @param menu menu containing options.
|
|
||||||
* @param inflater used to load the menu xml.
|
|
||||||
*/
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
|
||||||
// Inflate menu
|
|
||||||
inflater.inflate(R.menu.source_main, menu)
|
|
||||||
|
|
||||||
// Initialize search option.
|
|
||||||
val searchItem = menu.findItem(R.id.action_search)
|
|
||||||
val searchView = searchItem.actionView as SearchView
|
|
||||||
searchView.maxWidth = Int.MAX_VALUE
|
|
||||||
|
|
||||||
// Change hint to show global search.
|
|
||||||
searchView.queryHint = applicationContext?.getString(R.string.action_global_search_hint)
|
|
||||||
|
|
||||||
// Create query listener which opens the global search view.
|
|
||||||
searchView.queryTextEvents()
|
|
||||||
.filterIsInstance<QueryTextEvent.QuerySubmitted>()
|
|
||||||
.onEach { performGlobalSearch(it.queryText.toString()) }
|
|
||||||
.launchIn(viewScope)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun performGlobalSearch(query: String) {
|
|
||||||
parentController!!.router.pushController(
|
|
||||||
GlobalSearchController(query).withFadeTransaction()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when an option menu item has been selected by the user.
|
* Called when an option menu item has been selected by the user.
|
||||||
*
|
*
|
||||||
@ -290,4 +259,21 @@ class SourceController :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||||
|
createOptionsMenu(
|
||||||
|
menu,
|
||||||
|
inflater,
|
||||||
|
R.menu.source_main,
|
||||||
|
R.id.action_search,
|
||||||
|
R.string.action_global_search_hint,
|
||||||
|
false // GlobalSearch handles the searching here
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSearchViewQueryTextSubmit(query: String?) {
|
||||||
|
parentController!!.router.pushController(
|
||||||
|
GlobalSearchController(query).withFadeTransaction()
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -49,12 +49,17 @@ data class SourceItem(
|
|||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
if (other is SourceItem) {
|
if (other is SourceItem) {
|
||||||
return source.id == other.source.id && getHeader()?.code == other.getHeader()?.code
|
return source.id == other.source.id &&
|
||||||
|
getHeader()?.code == other.getHeader()?.code &&
|
||||||
|
isPinned == other.isPinned
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int {
|
||||||
return source.id.hashCode() + (getHeader()?.code?.hashCode() ?: 0).toInt()
|
var result = source.id.hashCode()
|
||||||
|
result = 31 * result + (header?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + isPinned.hashCode()
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,6 @@ import android.view.MenuInflater
|
|||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.appcompat.widget.SearchView
|
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.core.view.updatePadding
|
import androidx.core.view.updatePadding
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
@ -19,6 +18,7 @@ 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
|
||||||
|
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
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
@ -33,12 +33,13 @@ import eu.kanade.tachiyomi.source.LocalSource
|
|||||||
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
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
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.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.webview.WebViewActivity
|
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
|
||||||
import eu.kanade.tachiyomi.util.system.connectivityManager
|
import eu.kanade.tachiyomi.util.system.connectivityManager
|
||||||
import eu.kanade.tachiyomi.util.system.openInBrowser
|
import eu.kanade.tachiyomi.util.system.openInBrowser
|
||||||
@ -50,12 +51,8 @@ import eu.kanade.tachiyomi.widget.AutofitRecyclerView
|
|||||||
import eu.kanade.tachiyomi.widget.EmptyView
|
import eu.kanade.tachiyomi.widget.EmptyView
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.drop
|
import kotlinx.coroutines.flow.drop
|
||||||
import kotlinx.coroutines.flow.filter
|
|
||||||
import kotlinx.coroutines.flow.filterIsInstance
|
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import reactivecircus.flowbinding.appcompat.QueryTextEvent
|
|
||||||
import reactivecircus.flowbinding.appcompat.queryTextEvents
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
@ -63,7 +60,7 @@ import uy.kohesive.injekt.injectLazy
|
|||||||
* Controller to manage the catalogues available in the app.
|
* Controller to manage the catalogues available in the app.
|
||||||
*/
|
*/
|
||||||
open class BrowseSourceController(bundle: Bundle) :
|
open class BrowseSourceController(bundle: Bundle) :
|
||||||
NucleusController<SourceControllerBinding, BrowseSourcePresenter>(bundle),
|
SearchableNucleusController<SourceControllerBinding, BrowseSourcePresenter>(bundle),
|
||||||
FabController,
|
FabController,
|
||||||
FlexibleAdapter.OnItemClickListener,
|
FlexibleAdapter.OnItemClickListener,
|
||||||
FlexibleAdapter.OnItemLongClickListener,
|
FlexibleAdapter.OnItemLongClickListener,
|
||||||
@ -85,7 +82,7 @@ open class BrowseSourceController(bundle: Bundle) :
|
|||||||
/**
|
/**
|
||||||
* Adapter containing the list of manga from the catalogue.
|
* Adapter containing the list of manga from the catalogue.
|
||||||
*/
|
*/
|
||||||
private var adapter: FlexibleAdapter<IFlexible<*>>? = null
|
protected var adapter: FlexibleAdapter<IFlexible<*>>? = null
|
||||||
|
|
||||||
private var actionFab: ExtendedFloatingActionButton? = null
|
private var actionFab: ExtendedFloatingActionButton? = null
|
||||||
private var actionFabScrollListener: RecyclerView.OnScrollListener? = null
|
private var actionFabScrollListener: RecyclerView.OnScrollListener? = null
|
||||||
@ -246,6 +243,11 @@ open class BrowseSourceController(bundle: Bundle) :
|
|||||||
actionFab?.shrinkOnScroll(recycler)
|
actionFab?.shrinkOnScroll(recycler)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
recycler.applyInsetter {
|
||||||
|
type(navigationBars = true) {
|
||||||
|
padding()
|
||||||
|
}
|
||||||
|
}
|
||||||
recycler.setHasFixedSize(true)
|
recycler.setHasFixedSize(true)
|
||||||
recycler.adapter = adapter
|
recycler.adapter = adapter
|
||||||
|
|
||||||
@ -258,25 +260,8 @@ open class BrowseSourceController(bundle: Bundle) :
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||||
inflater.inflate(R.menu.source_browse, menu)
|
createOptionsMenu(menu, inflater, R.menu.source_browse, R.id.action_search)
|
||||||
|
|
||||||
// Initialize search menu
|
|
||||||
val searchItem = menu.findItem(R.id.action_search)
|
val searchItem = menu.findItem(R.id.action_search)
|
||||||
val searchView = searchItem.actionView as SearchView
|
|
||||||
searchView.maxWidth = Int.MAX_VALUE
|
|
||||||
|
|
||||||
val query = presenter.query
|
|
||||||
if (query.isNotBlank()) {
|
|
||||||
searchItem.expandActionView()
|
|
||||||
searchView.setQuery(query, true)
|
|
||||||
searchView.clearFocus()
|
|
||||||
}
|
|
||||||
|
|
||||||
searchView.queryTextEvents()
|
|
||||||
.filter { router.backstack.lastOrNull()?.controller() == this@BrowseSourceController }
|
|
||||||
.filterIsInstance<QueryTextEvent.QuerySubmitted>()
|
|
||||||
.onEach { searchWithQuery(it.queryText.toString()) }
|
|
||||||
.launchIn(viewScope)
|
|
||||||
|
|
||||||
searchItem.fixExpand(
|
searchItem.fixExpand(
|
||||||
onExpand = { invalidateMenuOnExpand() },
|
onExpand = { invalidateMenuOnExpand() },
|
||||||
@ -299,6 +284,10 @@ open class BrowseSourceController(bundle: Bundle) :
|
|||||||
menu.findItem(displayItem).isChecked = true
|
menu.findItem(displayItem).isChecked = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onSearchViewQueryTextSubmit(query: String?) {
|
||||||
|
searchWithQuery(query ?: "")
|
||||||
|
}
|
||||||
|
|
||||||
override fun onPrepareOptionsMenu(menu: Menu) {
|
override fun onPrepareOptionsMenu(menu: Menu) {
|
||||||
super.onPrepareOptionsMenu(menu)
|
super.onPrepareOptionsMenu(menu)
|
||||||
|
|
||||||
@ -391,16 +380,16 @@ open class BrowseSourceController(bundle: Bundle) :
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (adapter.isEmpty) {
|
if (adapter.isEmpty) {
|
||||||
val actions = emptyList<EmptyView.Action>().toMutableList()
|
val actions = if (presenter.source is LocalSource) {
|
||||||
|
listOf(
|
||||||
if (presenter.source is LocalSource) {
|
EmptyView.Action(R.string.local_source_help_guide, R.drawable.ic_help_24dp) { openLocalSourceHelpGuide() }
|
||||||
actions += EmptyView.Action(R.string.local_source_help_guide) { openLocalSourceHelpGuide() }
|
)
|
||||||
} else {
|
} else {
|
||||||
actions += EmptyView.Action(R.string.action_retry, retryAction)
|
listOf(
|
||||||
}
|
EmptyView.Action(R.string.action_retry, R.drawable.ic_refresh_24dp, retryAction),
|
||||||
|
EmptyView.Action(R.string.action_open_in_web_view, R.drawable.ic_public_24dp) { openInWebView() },
|
||||||
if (presenter.source is HttpSource) {
|
EmptyView.Action(R.string.label_help, R.drawable.ic_help_24dp) { activity?.openInBrowser(MoreController.URL_HELP) }
|
||||||
actions += EmptyView.Action(R.string.action_open_in_web_view) { openInWebView() }
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.emptyView.show(message, actions)
|
binding.emptyView.show(message, actions)
|
||||||
|
@ -66,12 +66,6 @@ open class BrowseSourcePresenter(
|
|||||||
*/
|
*/
|
||||||
lateinit var source: CatalogueSource
|
lateinit var source: CatalogueSource
|
||||||
|
|
||||||
/**
|
|
||||||
* Query from the view.
|
|
||||||
*/
|
|
||||||
var query = searchQuery ?: ""
|
|
||||||
private set
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Modifiable list of filters.
|
* Modifiable list of filters.
|
||||||
*/
|
*/
|
||||||
@ -108,6 +102,10 @@ open class BrowseSourcePresenter(
|
|||||||
*/
|
*/
|
||||||
private var pageSubscription: Subscription? = null
|
private var pageSubscription: Subscription? = null
|
||||||
|
|
||||||
|
init {
|
||||||
|
query = searchQuery ?: ""
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedState: Bundle?) {
|
override fun onCreate(savedState: Bundle?) {
|
||||||
super.onCreate(savedState)
|
super.onCreate(savedState)
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.ui.browse.source.filter
|
|||||||
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.EditText
|
import android.widget.EditText
|
||||||
|
import androidx.core.widget.doOnTextChanged
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.google.android.material.textfield.TextInputLayout
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||||
@ -10,7 +11,6 @@ import eu.davidea.flexibleadapter.items.IFlexible
|
|||||||
import eu.davidea.viewholders.FlexibleViewHolder
|
import eu.davidea.viewholders.FlexibleViewHolder
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.source.model.Filter
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
import eu.kanade.tachiyomi.widget.SimpleTextWatcher
|
|
||||||
|
|
||||||
open class TextItem(val filter: Filter.Text) : AbstractFlexibleItem<TextItem.Holder>() {
|
open class TextItem(val filter: Filter.Text) : AbstractFlexibleItem<TextItem.Holder>() {
|
||||||
|
|
||||||
@ -25,11 +25,9 @@ open class TextItem(val filter: Filter.Text) : AbstractFlexibleItem<TextItem.Hol
|
|||||||
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>, holder: Holder, position: Int, payloads: List<Any?>?) {
|
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>, holder: Holder, position: Int, payloads: List<Any?>?) {
|
||||||
holder.wrapper.hint = filter.name
|
holder.wrapper.hint = filter.name
|
||||||
holder.edit.setText(filter.state)
|
holder.edit.setText(filter.state)
|
||||||
holder.edit.addTextChangedListener(object : SimpleTextWatcher() {
|
holder.edit.doOnTextChanged { text, _, _, _ ->
|
||||||
override fun onTextChanged(text: CharSequence, start: Int, before: Int, count: Int) {
|
filter.state = text.toString()
|
||||||
filter.state = text.toString()
|
}
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
|
@ -10,20 +10,16 @@ import android.view.ViewGroup
|
|||||||
import androidx.appcompat.widget.SearchView
|
import androidx.appcompat.widget.SearchView
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import dev.chrisbanes.insetter.applyInsetter
|
||||||
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
|
||||||
import eu.kanade.tachiyomi.databinding.GlobalSearchControllerBinding
|
import eu.kanade.tachiyomi.databinding.GlobalSearchControllerBinding
|
||||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
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.browse.BrowseSourceController
|
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
|
||||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||||
import kotlinx.coroutines.flow.filterIsInstance
|
|
||||||
import kotlinx.coroutines.flow.launchIn
|
|
||||||
import kotlinx.coroutines.flow.onEach
|
|
||||||
import reactivecircus.flowbinding.appcompat.QueryTextEvent
|
|
||||||
import reactivecircus.flowbinding.appcompat.queryTextEvents
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -34,7 +30,7 @@ import uy.kohesive.injekt.injectLazy
|
|||||||
open class GlobalSearchController(
|
open class GlobalSearchController(
|
||||||
protected val initialQuery: String? = null,
|
protected val initialQuery: String? = null,
|
||||||
protected val extensionFilter: String? = null
|
protected val extensionFilter: String? = null
|
||||||
) : NucleusController<GlobalSearchControllerBinding, GlobalSearchPresenter>(),
|
) : SearchableNucleusController<GlobalSearchControllerBinding, GlobalSearchPresenter>(),
|
||||||
GlobalSearchCardAdapter.OnMangaClickListener,
|
GlobalSearchCardAdapter.OnMangaClickListener,
|
||||||
GlobalSearchAdapter.OnTitleClickListener {
|
GlobalSearchAdapter.OnTitleClickListener {
|
||||||
|
|
||||||
@ -45,6 +41,11 @@ open class GlobalSearchController(
|
|||||||
*/
|
*/
|
||||||
protected var adapter: GlobalSearchAdapter? = null
|
protected var adapter: GlobalSearchAdapter? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ref to the OptionsMenu.SearchItem created in onCreateOptionsMenu
|
||||||
|
*/
|
||||||
|
private var optionsMenuSearchItem: MenuItem? = null
|
||||||
|
|
||||||
init {
|
init {
|
||||||
setHasOptionsMenu(true)
|
setHasOptionsMenu(true)
|
||||||
}
|
}
|
||||||
@ -58,6 +59,11 @@ open class GlobalSearchController(
|
|||||||
*/
|
*/
|
||||||
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
|
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
|
||||||
binding = GlobalSearchControllerBinding.inflate(inflater)
|
binding = GlobalSearchControllerBinding.inflate(inflater)
|
||||||
|
binding.recycler.applyInsetter {
|
||||||
|
type(navigationBars = true) {
|
||||||
|
padding()
|
||||||
|
}
|
||||||
|
}
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -100,36 +106,32 @@ open class GlobalSearchController(
|
|||||||
* @param inflater used to load the menu xml.
|
* @param inflater used to load the menu xml.
|
||||||
*/
|
*/
|
||||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||||
// Inflate menu.
|
createOptionsMenu(
|
||||||
inflater.inflate(R.menu.global_search, menu)
|
menu,
|
||||||
|
inflater,
|
||||||
// Initialize search menu
|
R.menu.global_search,
|
||||||
val searchItem = menu.findItem(R.id.action_search)
|
R.id.action_search,
|
||||||
val searchView = searchItem.actionView as SearchView
|
null,
|
||||||
searchView.maxWidth = Int.MAX_VALUE
|
false // the onMenuItemActionExpand will handle this
|
||||||
|
|
||||||
searchItem.setOnActionExpandListener(
|
|
||||||
object : MenuItem.OnActionExpandListener {
|
|
||||||
override fun onMenuItemActionExpand(item: MenuItem?): Boolean {
|
|
||||||
searchView.onActionViewExpanded() // Required to show the query in the view
|
|
||||||
searchView.setQuery(presenter.query, false)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onMenuItemActionCollapse(item: MenuItem?): Boolean {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
searchView.queryTextEvents()
|
optionsMenuSearchItem = menu.findItem(R.id.action_search)
|
||||||
.filterIsInstance<QueryTextEvent.QuerySubmitted>()
|
}
|
||||||
.onEach {
|
|
||||||
presenter.search(it.queryText.toString())
|
override fun onSearchMenuItemActionExpand(item: MenuItem?) {
|
||||||
searchItem.collapseActionView()
|
super.onSearchMenuItemActionExpand(item)
|
||||||
setTitle() // Update toolbar title
|
val searchView = optionsMenuSearchItem?.actionView as SearchView
|
||||||
}
|
searchView.onActionViewExpanded() // Required to show the query in the view
|
||||||
.launchIn(viewScope)
|
|
||||||
|
if (nonSubmittedQuery.isBlank()) {
|
||||||
|
searchView.setQuery(presenter.query, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSearchViewQueryTextSubmit(query: String?) {
|
||||||
|
presenter.search(query ?: "")
|
||||||
|
optionsMenuSearchItem?.collapseActionView()
|
||||||
|
setTitle() // Update toolbar title
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -47,12 +47,6 @@ open class GlobalSearchPresenter(
|
|||||||
*/
|
*/
|
||||||
val sources by lazy { getSourcesToQuery() }
|
val sources by lazy { getSourcesToQuery() }
|
||||||
|
|
||||||
/**
|
|
||||||
* Query from the view.
|
|
||||||
*/
|
|
||||||
var query = ""
|
|
||||||
private set
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches the different sources by user settings.
|
* Fetches the different sources by user settings.
|
||||||
*/
|
*/
|
||||||
|
@ -11,6 +11,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
|||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
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 dev.chrisbanes.insetter.applyInsetter
|
||||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||||
import eu.davidea.flexibleadapter.SelectableAdapter
|
import eu.davidea.flexibleadapter.SelectableAdapter
|
||||||
import eu.davidea.flexibleadapter.helpers.UndoHelper
|
import eu.davidea.flexibleadapter.helpers.UndoHelper
|
||||||
@ -75,6 +76,11 @@ class CategoryController :
|
|||||||
*/
|
*/
|
||||||
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
|
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
|
||||||
binding = CategoriesControllerBinding.inflate(inflater)
|
binding = CategoriesControllerBinding.inflate(inflater)
|
||||||
|
binding.recycler.applyInsetter {
|
||||||
|
type(navigationBars = true) {
|
||||||
|
padding()
|
||||||
|
}
|
||||||
|
}
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,6 +10,7 @@ import androidx.core.view.isVisible
|
|||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||||
|
import dev.chrisbanes.insetter.applyInsetter
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.download.DownloadService
|
import eu.kanade.tachiyomi.data.download.DownloadService
|
||||||
import eu.kanade.tachiyomi.data.download.model.Download
|
import eu.kanade.tachiyomi.data.download.model.Download
|
||||||
@ -56,6 +57,11 @@ class DownloadController :
|
|||||||
|
|
||||||
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
|
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
|
||||||
binding = DownloadControllerBinding.inflate(inflater)
|
binding = DownloadControllerBinding.inflate(inflater)
|
||||||
|
binding.recycler.applyInsetter {
|
||||||
|
type(navigationBars = true) {
|
||||||
|
padding()
|
||||||
|
}
|
||||||
|
}
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,9 +14,7 @@ class ChangeMangaCategoriesDialog<T>(bundle: Bundle? = null) :
|
|||||||
DialogController(bundle) where T : Controller, T : ChangeMangaCategoriesDialog.Listener {
|
DialogController(bundle) where T : Controller, T : ChangeMangaCategoriesDialog.Listener {
|
||||||
|
|
||||||
private var mangas = emptyList<Manga>()
|
private var mangas = emptyList<Manga>()
|
||||||
|
|
||||||
private var categories = emptyList<Category>()
|
private var categories = emptyList<Category>()
|
||||||
|
|
||||||
private var preselected = emptyArray<Int>()
|
private var preselected = emptyArray<Int>()
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
@ -6,6 +6,7 @@ import android.view.View
|
|||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import dev.chrisbanes.insetter.applyInsetter
|
||||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||||
import eu.davidea.flexibleadapter.SelectableAdapter
|
import eu.davidea.flexibleadapter.SelectableAdapter
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
@ -82,6 +83,12 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
recycler.applyInsetter {
|
||||||
|
type(navigationBars = true) {
|
||||||
|
padding()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
adapter = LibraryCategoryAdapter(this)
|
adapter = LibraryCategoryAdapter(this)
|
||||||
|
|
||||||
recycler.setHasFixedSize(true)
|
recycler.setHasFixedSize(true)
|
||||||
|
@ -10,7 +10,6 @@ import android.view.View
|
|||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.appcompat.view.ActionMode
|
import androidx.appcompat.view.ActionMode
|
||||||
import androidx.appcompat.widget.SearchView
|
|
||||||
import androidx.core.graphics.drawable.DrawableCompat
|
import androidx.core.graphics.drawable.DrawableCompat
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import com.bluelinelabs.conductor.ControllerChangeHandler
|
import com.bluelinelabs.conductor.ControllerChangeHandler
|
||||||
@ -27,8 +26,8 @@ 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.LibraryControllerBinding
|
import eu.kanade.tachiyomi.databinding.LibraryControllerBinding
|
||||||
import eu.kanade.tachiyomi.source.LocalSource
|
import eu.kanade.tachiyomi.source.LocalSource
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.RootController
|
import eu.kanade.tachiyomi.ui.base.controller.RootController
|
||||||
|
import eu.kanade.tachiyomi.ui.base.controller.SearchableNucleusController
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.TabbedController
|
import eu.kanade.tachiyomi.ui.base.controller.TabbedController
|
||||||
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
|
||||||
@ -37,11 +36,9 @@ import eu.kanade.tachiyomi.ui.manga.MangaController
|
|||||||
import eu.kanade.tachiyomi.util.system.getResourceColor
|
import eu.kanade.tachiyomi.util.system.getResourceColor
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
import kotlinx.coroutines.flow.drop
|
import kotlinx.coroutines.flow.drop
|
||||||
import kotlinx.coroutines.flow.filter
|
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import reactivecircus.flowbinding.android.view.clicks
|
import reactivecircus.flowbinding.android.view.clicks
|
||||||
import reactivecircus.flowbinding.appcompat.queryTextChanges
|
|
||||||
import reactivecircus.flowbinding.viewpager.pageSelections
|
import reactivecircus.flowbinding.viewpager.pageSelections
|
||||||
import rx.Subscription
|
import rx.Subscription
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
@ -50,7 +47,7 @@ import uy.kohesive.injekt.api.get
|
|||||||
class LibraryController(
|
class LibraryController(
|
||||||
bundle: Bundle? = null,
|
bundle: Bundle? = null,
|
||||||
private val preferences: PreferencesHelper = Injekt.get()
|
private val preferences: PreferencesHelper = Injekt.get()
|
||||||
) : NucleusController<LibraryControllerBinding, LibraryPresenter>(bundle),
|
) : SearchableNucleusController<LibraryControllerBinding, LibraryPresenter>(bundle),
|
||||||
RootController,
|
RootController,
|
||||||
TabbedController,
|
TabbedController,
|
||||||
ActionMode.Callback,
|
ActionMode.Callback,
|
||||||
@ -67,11 +64,6 @@ class LibraryController(
|
|||||||
*/
|
*/
|
||||||
private var actionMode: ActionMode? = null
|
private var actionMode: ActionMode? = null
|
||||||
|
|
||||||
/**
|
|
||||||
* Library search query.
|
|
||||||
*/
|
|
||||||
private var query: String = ""
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Currently selected mangas.
|
* Currently selected mangas.
|
||||||
*/
|
*/
|
||||||
@ -212,12 +204,12 @@ class LibraryController(
|
|||||||
binding.btnGlobalSearch.clicks()
|
binding.btnGlobalSearch.clicks()
|
||||||
.onEach {
|
.onEach {
|
||||||
router.pushController(
|
router.pushController(
|
||||||
GlobalSearchController(query).withFadeTransaction()
|
GlobalSearchController(presenter.query).withFadeTransaction()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.launchIn(viewScope)
|
.launchIn(viewScope)
|
||||||
|
|
||||||
(activity!! as MainActivity).fixViewToBottom(binding.actionToolbar)
|
(activity as? MainActivity)?.fixViewToBottom(binding.actionToolbar)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
|
override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
|
||||||
@ -230,7 +222,7 @@ class LibraryController(
|
|||||||
|
|
||||||
override fun onDestroyView(view: View) {
|
override fun onDestroyView(view: View) {
|
||||||
destroyActionModeIfNeeded()
|
destroyActionModeIfNeeded()
|
||||||
(activity!! as MainActivity).clearFixViewToBottom(binding.actionToolbar)
|
(activity as? MainActivity)?.clearFixViewToBottom(binding.actionToolbar)
|
||||||
binding.actionToolbar.destroy()
|
binding.actionToolbar.destroy()
|
||||||
adapter?.onDestroy()
|
adapter?.onDestroy()
|
||||||
adapter = null
|
adapter = null
|
||||||
@ -384,52 +376,21 @@ class LibraryController(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||||
inflater.inflate(R.menu.library, menu)
|
createOptionsMenu(menu, inflater, R.menu.library, R.id.action_search)
|
||||||
|
|
||||||
val searchItem = menu.findItem(R.id.action_search)
|
|
||||||
val searchView = searchItem.actionView as SearchView
|
|
||||||
searchView.maxWidth = Int.MAX_VALUE
|
|
||||||
searchItem.fixExpand(onExpand = { invalidateMenuOnExpand() })
|
|
||||||
|
|
||||||
if (query.isNotEmpty()) {
|
|
||||||
searchItem.expandActionView()
|
|
||||||
searchView.setQuery(query, true)
|
|
||||||
searchView.clearFocus()
|
|
||||||
|
|
||||||
performSearch()
|
|
||||||
|
|
||||||
// Workaround for weird behavior where searchview gets empty text change despite
|
|
||||||
// query being set already
|
|
||||||
searchView.postDelayed({ initSearchHandler(searchView) }, 500)
|
|
||||||
} else {
|
|
||||||
initSearchHandler(searchView)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mutate the filter icon because it needs to be tinted and the resource is shared.
|
// Mutate the filter icon because it needs to be tinted and the resource is shared.
|
||||||
menu.findItem(R.id.action_filter).icon.mutate()
|
menu.findItem(R.id.action_filter).icon.mutate()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun search(query: String) {
|
fun search(query: String) {
|
||||||
this.query = query
|
presenter.query = query
|
||||||
}
|
|
||||||
|
|
||||||
private fun initSearchHandler(searchView: SearchView) {
|
|
||||||
searchView.queryTextChanges()
|
|
||||||
// Ignore events if this controller isn't at the top to avoid query being reset
|
|
||||||
.filter { router.backstack.lastOrNull()?.controller() == this }
|
|
||||||
.onEach {
|
|
||||||
query = it.toString()
|
|
||||||
performSearch()
|
|
||||||
}
|
|
||||||
.launchIn(viewScope)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun performSearch() {
|
private fun performSearch() {
|
||||||
searchRelay.call(query)
|
searchRelay.call(presenter.query)
|
||||||
if (query.isNotEmpty()) {
|
if (presenter.query.isNotEmpty()) {
|
||||||
binding.btnGlobalSearch.isVisible = true
|
binding.btnGlobalSearch.isVisible = true
|
||||||
binding.btnGlobalSearch.text =
|
binding.btnGlobalSearch.text =
|
||||||
resources?.getString(R.string.action_global_search_query, query)
|
resources?.getString(R.string.action_global_search_query, presenter.query)
|
||||||
} else {
|
} else {
|
||||||
binding.btnGlobalSearch.isVisible = false
|
binding.btnGlobalSearch.isVisible = false
|
||||||
}
|
}
|
||||||
@ -611,4 +572,12 @@ class LibraryController(
|
|||||||
selectInverseRelay.call(it)
|
selectInverseRelay.call(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onSearchViewQueryTextChange(newText: String?) {
|
||||||
|
// Ignore events if this controller isn't at the top to avoid query being reset
|
||||||
|
if (router.backstack.lastOrNull()?.controller() == this) {
|
||||||
|
presenter.query = newText ?: ""
|
||||||
|
performSearch()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -90,6 +90,7 @@ class LibraryItem(val manga: LibraryManga, private val libraryDisplayMode: Prefe
|
|||||||
return manga.title.contains(constraint, true) ||
|
return manga.title.contains(constraint, true) ||
|
||||||
(manga.author?.contains(constraint, true) ?: false) ||
|
(manga.author?.contains(constraint, true) ?: false) ||
|
||||||
(manga.artist?.contains(constraint, true) ?: false) ||
|
(manga.artist?.contains(constraint, true) ?: false) ||
|
||||||
|
(manga.description?.contains(constraint, true) ?: false) ||
|
||||||
sourceManager.getOrStub(manga.source).name.contains(constraint, true) ||
|
sourceManager.getOrStub(manga.source).name.contains(constraint, true) ||
|
||||||
if (constraint.contains(",")) {
|
if (constraint.contains(",")) {
|
||||||
constraint.split(",").all { containsGenre(it.trim(), manga.getGenres()) }
|
constraint.split(",").all { containsGenre(it.trim(), manga.getGenres()) }
|
||||||
|
@ -235,7 +235,12 @@ class LibraryPresenter(
|
|||||||
var counter = 0
|
var counter = 0
|
||||||
db.getLatestChapterManga().executeAsBlocking().associate { it.id!! to counter++ }
|
db.getLatestChapterManga().executeAsBlocking().associate { it.id!! to counter++ }
|
||||||
}
|
}
|
||||||
|
val chapterFetchDateManga by lazy {
|
||||||
|
var counter = 0
|
||||||
|
db.getChapterFetchDateManga().executeAsBlocking().associate { it.id!! to counter++ }
|
||||||
|
}
|
||||||
|
|
||||||
|
val sortAscending = preferences.librarySortingAscending().get()
|
||||||
val sortFn: (LibraryItem, LibraryItem) -> Int = { i1, i2 ->
|
val sortFn: (LibraryItem, LibraryItem) -> Int = { i1, i2 ->
|
||||||
when (sortingMode) {
|
when (sortingMode) {
|
||||||
LibrarySort.ALPHA -> i1.manga.title.compareTo(i2.manga.title, true)
|
LibrarySort.ALPHA -> i1.manga.title.compareTo(i2.manga.title, true)
|
||||||
@ -246,7 +251,13 @@ class LibraryPresenter(
|
|||||||
manga1LastRead.compareTo(manga2LastRead)
|
manga1LastRead.compareTo(manga2LastRead)
|
||||||
}
|
}
|
||||||
LibrarySort.LAST_CHECKED -> i2.manga.last_update.compareTo(i1.manga.last_update)
|
LibrarySort.LAST_CHECKED -> i2.manga.last_update.compareTo(i1.manga.last_update)
|
||||||
LibrarySort.UNREAD -> i1.manga.unread.compareTo(i2.manga.unread)
|
LibrarySort.UNREAD -> when {
|
||||||
|
// Ensure unread content comes first
|
||||||
|
i1.manga.unread == i2.manga.unread -> 0
|
||||||
|
i1.manga.unread == 0 -> if (sortAscending) 1 else -1
|
||||||
|
i2.manga.unread == 0 -> if (sortAscending) -1 else 1
|
||||||
|
else -> i1.manga.unread.compareTo(i2.manga.unread)
|
||||||
|
}
|
||||||
LibrarySort.TOTAL -> {
|
LibrarySort.TOTAL -> {
|
||||||
val manga1TotalChapter = totalChapterManga[i1.manga.id!!] ?: 0
|
val manga1TotalChapter = totalChapterManga[i1.manga.id!!] ?: 0
|
||||||
val mange2TotalChapter = totalChapterManga[i2.manga.id!!] ?: 0
|
val mange2TotalChapter = totalChapterManga[i2.manga.id!!] ?: 0
|
||||||
@ -259,12 +270,19 @@ class LibraryPresenter(
|
|||||||
?: latestChapterManga.size
|
?: latestChapterManga.size
|
||||||
manga1latestChapter.compareTo(manga2latestChapter)
|
manga1latestChapter.compareTo(manga2latestChapter)
|
||||||
}
|
}
|
||||||
|
LibrarySort.CHAPTER_FETCH_DATE -> {
|
||||||
|
val manga1chapterFetchDate = chapterFetchDateManga[i1.manga.id!!]
|
||||||
|
?: chapterFetchDateManga.size
|
||||||
|
val manga2chapterFetchDate = chapterFetchDateManga[i2.manga.id!!]
|
||||||
|
?: chapterFetchDateManga.size
|
||||||
|
manga1chapterFetchDate.compareTo(manga2chapterFetchDate)
|
||||||
|
}
|
||||||
LibrarySort.DATE_ADDED -> i2.manga.date_added.compareTo(i1.manga.date_added)
|
LibrarySort.DATE_ADDED -> i2.manga.date_added.compareTo(i1.manga.date_added)
|
||||||
else -> throw Exception("Unknown sorting mode")
|
else -> throw Exception("Unknown sorting mode")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val comparator = if (preferences.librarySortingAscending().get()) {
|
val comparator = if (sortAscending) {
|
||||||
Comparator(sortFn)
|
Comparator(sortFn)
|
||||||
} else {
|
} else {
|
||||||
Collections.reverseOrder(sortFn)
|
Collections.reverseOrder(sortFn)
|
||||||
|
@ -20,7 +20,7 @@ class LibrarySettingsSheet(
|
|||||||
router: Router,
|
router: Router,
|
||||||
private val trackManager: TrackManager = Injekt.get(),
|
private val trackManager: TrackManager = Injekt.get(),
|
||||||
onGroupClickListener: (ExtendedNavigationView.Group) -> Unit
|
onGroupClickListener: (ExtendedNavigationView.Group) -> Unit
|
||||||
) : TabbedBottomSheetDialog(router) {
|
) : TabbedBottomSheetDialog(router.activity!!) {
|
||||||
|
|
||||||
val filters: Filter
|
val filters: Filter
|
||||||
private val sort: Sort
|
private val sort: Sort
|
||||||
@ -157,11 +157,12 @@ class LibrarySettingsSheet(
|
|||||||
private val lastChecked = Item.MultiSort(R.string.action_sort_last_checked, this)
|
private val lastChecked = Item.MultiSort(R.string.action_sort_last_checked, this)
|
||||||
private val unread = Item.MultiSort(R.string.action_filter_unread, this)
|
private val unread = Item.MultiSort(R.string.action_filter_unread, this)
|
||||||
private val latestChapter = Item.MultiSort(R.string.action_sort_latest_chapter, this)
|
private val latestChapter = Item.MultiSort(R.string.action_sort_latest_chapter, this)
|
||||||
|
private val chapterFetchDate = Item.MultiSort(R.string.action_sort_chapter_fetch_date, this)
|
||||||
private val dateAdded = Item.MultiSort(R.string.action_sort_date_added, this)
|
private val dateAdded = Item.MultiSort(R.string.action_sort_date_added, this)
|
||||||
|
|
||||||
override val header = null
|
override val header = null
|
||||||
override val items =
|
override val items =
|
||||||
listOf(alphabetically, lastRead, lastChecked, unread, total, latestChapter, dateAdded)
|
listOf(alphabetically, lastRead, lastChecked, unread, total, latestChapter, chapterFetchDate, dateAdded)
|
||||||
override val footer = null
|
override val footer = null
|
||||||
|
|
||||||
override fun initModels() {
|
override fun initModels() {
|
||||||
@ -184,6 +185,8 @@ class LibrarySettingsSheet(
|
|||||||
if (sorting == LibrarySort.TOTAL) order else Item.MultiSort.SORT_NONE
|
if (sorting == LibrarySort.TOTAL) order else Item.MultiSort.SORT_NONE
|
||||||
latestChapter.state =
|
latestChapter.state =
|
||||||
if (sorting == LibrarySort.LATEST_CHAPTER) order else Item.MultiSort.SORT_NONE
|
if (sorting == LibrarySort.LATEST_CHAPTER) order else Item.MultiSort.SORT_NONE
|
||||||
|
chapterFetchDate.state =
|
||||||
|
if (sorting == LibrarySort.CHAPTER_FETCH_DATE) order else Item.MultiSort.SORT_NONE
|
||||||
dateAdded.state =
|
dateAdded.state =
|
||||||
if (sorting == LibrarySort.DATE_ADDED) order else Item.MultiSort.SORT_NONE
|
if (sorting == LibrarySort.DATE_ADDED) order else Item.MultiSort.SORT_NONE
|
||||||
}
|
}
|
||||||
@ -211,6 +214,7 @@ class LibrarySettingsSheet(
|
|||||||
unread -> LibrarySort.UNREAD
|
unread -> LibrarySort.UNREAD
|
||||||
total -> LibrarySort.TOTAL
|
total -> LibrarySort.TOTAL
|
||||||
latestChapter -> LibrarySort.LATEST_CHAPTER
|
latestChapter -> LibrarySort.LATEST_CHAPTER
|
||||||
|
chapterFetchDate -> LibrarySort.CHAPTER_FETCH_DATE
|
||||||
dateAdded -> LibrarySort.DATE_ADDED
|
dateAdded -> LibrarySort.DATE_ADDED
|
||||||
else -> throw Exception("Unknown sorting")
|
else -> throw Exception("Unknown sorting")
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ object LibrarySort {
|
|||||||
const val UNREAD = 3
|
const val UNREAD = 3
|
||||||
const val TOTAL = 4
|
const val TOTAL = 4
|
||||||
const val LATEST_CHAPTER = 6
|
const val LATEST_CHAPTER = 6
|
||||||
|
const val CHAPTER_FETCH_DATE = 8
|
||||||
const val DATE_ADDED = 7
|
const val DATE_ADDED = 7
|
||||||
|
|
||||||
@Deprecated("Removed in favor of searching by source")
|
@Deprecated("Removed in favor of searching by source")
|
||||||
|
@ -2,11 +2,16 @@ package eu.kanade.tachiyomi.ui.main
|
|||||||
|
|
||||||
import android.app.SearchManager
|
import android.app.SearchManager
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
import androidx.core.view.WindowCompat
|
||||||
|
import androidx.core.view.WindowInsetsCompat
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.core.view.updateLayoutParams
|
import androidx.core.view.updateLayoutParams
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
@ -18,6 +23,7 @@ import com.bluelinelabs.conductor.Router
|
|||||||
import com.bluelinelabs.conductor.RouterTransaction
|
import com.bluelinelabs.conductor.RouterTransaction
|
||||||
import com.google.android.material.appbar.AppBarLayout
|
import com.google.android.material.appbar.AppBarLayout
|
||||||
import com.google.android.material.behavior.HideBottomViewOnScrollBehavior
|
import com.google.android.material.behavior.HideBottomViewOnScrollBehavior
|
||||||
|
import dev.chrisbanes.insetter.applyInsetter
|
||||||
import eu.kanade.tachiyomi.BuildConfig
|
import eu.kanade.tachiyomi.BuildConfig
|
||||||
import eu.kanade.tachiyomi.Migrations
|
import eu.kanade.tachiyomi.Migrations
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
@ -43,6 +49,7 @@ import eu.kanade.tachiyomi.ui.recent.history.HistoryController
|
|||||||
import eu.kanade.tachiyomi.ui.recent.updates.UpdatesController
|
import eu.kanade.tachiyomi.ui.recent.updates.UpdatesController
|
||||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||||
import eu.kanade.tachiyomi.util.lang.launchUI
|
import eu.kanade.tachiyomi.util.lang.launchUI
|
||||||
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
@ -84,6 +91,35 @@ class MainActivity : BaseViewBindingActivity<MainActivityBinding>() {
|
|||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
setSupportActionBar(binding.toolbar)
|
setSupportActionBar(binding.toolbar)
|
||||||
|
|
||||||
|
// Draw edge-to-edge
|
||||||
|
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||||
|
binding.appbar.applyInsetter {
|
||||||
|
type(navigationBars = true, statusBars = true) {
|
||||||
|
padding(left = true, top = true, right = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
binding.bottomNav.applyInsetter {
|
||||||
|
type(navigationBars = true) {
|
||||||
|
padding()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
binding.rootFab.applyInsetter {
|
||||||
|
type(navigationBars = true) {
|
||||||
|
margin()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure navigation bar is on bottom when making it transparent
|
||||||
|
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { _, insets ->
|
||||||
|
if (insets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom > 0) {
|
||||||
|
// Keep scrim on light theme if windowLightNavigationBar is not available
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1 || isDarkMode) {
|
||||||
|
window.navigationBarColor = Color.TRANSPARENT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
insets
|
||||||
|
}
|
||||||
|
|
||||||
tabAnimator = ViewHeightAnimator(binding.tabs, 0L)
|
tabAnimator = ViewHeightAnimator(binding.tabs, 0L)
|
||||||
bottomNavAnimator = ViewHeightAnimator(binding.bottomNav)
|
bottomNavAnimator = ViewHeightAnimator(binding.bottomNav)
|
||||||
|
|
||||||
@ -301,11 +337,8 @@ class MainActivity : BaseViewBindingActivity<MainActivityBinding>() {
|
|||||||
|
|
||||||
private suspend fun resetExitConfirmation() {
|
private suspend fun resetExitConfirmation() {
|
||||||
isConfirmingExit = true
|
isConfirmingExit = true
|
||||||
val toast = Toast.makeText(this, R.string.confirm_exit, Toast.LENGTH_LONG)
|
val toast = toast(R.string.confirm_exit, Toast.LENGTH_LONG)
|
||||||
toast.show()
|
|
||||||
|
|
||||||
delay(2000)
|
delay(2000)
|
||||||
|
|
||||||
toast.cancel()
|
toast.cancel()
|
||||||
isConfirmingExit = false
|
isConfirmingExit = false
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,12 @@
|
|||||||
package eu.kanade.tachiyomi.ui.main
|
package eu.kanade.tachiyomi.ui.main
|
||||||
|
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.core.net.toUri
|
|
||||||
import com.afollestad.materialdialogs.MaterialDialog
|
import com.afollestad.materialdialogs.MaterialDialog
|
||||||
import eu.kanade.tachiyomi.BuildConfig
|
import eu.kanade.tachiyomi.BuildConfig
|
||||||
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
|
||||||
|
import eu.kanade.tachiyomi.ui.base.controller.openInBrowser
|
||||||
|
|
||||||
class WhatsNewDialogController(bundle: Bundle? = null) : DialogController(bundle) {
|
class WhatsNewDialogController(bundle: Bundle? = null) : DialogController(bundle) {
|
||||||
|
|
||||||
@ -16,9 +15,7 @@ class WhatsNewDialogController(bundle: Bundle? = null) : DialogController(bundle
|
|||||||
.title(text = activity!!.getString(R.string.updated_version, BuildConfig.VERSION_NAME))
|
.title(text = activity!!.getString(R.string.updated_version, BuildConfig.VERSION_NAME))
|
||||||
.positiveButton(android.R.string.ok)
|
.positiveButton(android.R.string.ok)
|
||||||
.neutralButton(R.string.whats_new) {
|
.neutralButton(R.string.whats_new) {
|
||||||
val url = "https://github.com/tachiyomiorg/tachiyomi/releases/tag/v${BuildConfig.VERSION_NAME}"
|
openInBrowser("https://github.com/tachiyomiorg/tachiyomi/releases/tag/v${BuildConfig.VERSION_NAME}")
|
||||||
val intent = Intent(Intent.ACTION_VIEW, url.toUri())
|
|
||||||
startActivity(intent)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,6 +26,7 @@ import com.bluelinelabs.conductor.ControllerChangeHandler
|
|||||||
import com.bluelinelabs.conductor.ControllerChangeType
|
import com.bluelinelabs.conductor.ControllerChangeType
|
||||||
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 dev.chrisbanes.insetter.applyInsetter
|
||||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||||
import eu.davidea.flexibleadapter.SelectableAdapter
|
import eu.davidea.flexibleadapter.SelectableAdapter
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
@ -72,6 +73,7 @@ import eu.kanade.tachiyomi.ui.recent.updates.UpdatesController
|
|||||||
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
|
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
|
||||||
import eu.kanade.tachiyomi.util.chapter.NoChaptersException
|
import eu.kanade.tachiyomi.util.chapter.NoChaptersException
|
||||||
import eu.kanade.tachiyomi.util.hasCustomCover
|
import eu.kanade.tachiyomi.util.hasCustomCover
|
||||||
|
import eu.kanade.tachiyomi.util.lang.launchUI
|
||||||
import eu.kanade.tachiyomi.util.system.getResourceColor
|
import eu.kanade.tachiyomi.util.system.getResourceColor
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
import eu.kanade.tachiyomi.util.view.getCoordinates
|
import eu.kanade.tachiyomi.util.view.getCoordinates
|
||||||
@ -199,6 +201,11 @@ class MangaController :
|
|||||||
|
|
||||||
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
|
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
|
||||||
binding = MangaControllerBinding.inflate(inflater)
|
binding = MangaControllerBinding.inflate(inflater)
|
||||||
|
binding.recycler.applyInsetter {
|
||||||
|
type(navigationBars = true) {
|
||||||
|
padding()
|
||||||
|
}
|
||||||
|
}
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -242,7 +249,7 @@ class MangaController :
|
|||||||
}
|
}
|
||||||
.launchIn(viewScope)
|
.launchIn(viewScope)
|
||||||
|
|
||||||
(activity!! as MainActivity).fixViewToBottom(binding.actionToolbar)
|
(activity as? MainActivity)?.fixViewToBottom(binding.actionToolbar)
|
||||||
|
|
||||||
settingsSheet = ChaptersSettingsSheet(router, presenter) { group ->
|
settingsSheet = ChaptersSettingsSheet(router, presenter) { group ->
|
||||||
if (group is ChaptersSettingsSheet.Filter.FilterGroup) {
|
if (group is ChaptersSettingsSheet.Filter.FilterGroup) {
|
||||||
@ -321,7 +328,7 @@ class MangaController :
|
|||||||
|
|
||||||
override fun onDestroyView(view: View) {
|
override fun onDestroyView(view: View) {
|
||||||
destroyActionModeIfNeeded()
|
destroyActionModeIfNeeded()
|
||||||
(activity!! as MainActivity).clearFixViewToBottom(binding.actionToolbar)
|
(activity as? MainActivity)?.clearFixViewToBottom(binding.actionToolbar)
|
||||||
binding.actionToolbar.destroy()
|
binding.actionToolbar.destroy()
|
||||||
mangaInfoAdapter = null
|
mangaInfoAdapter = null
|
||||||
chaptersHeaderAdapter = null
|
chaptersHeaderAdapter = null
|
||||||
@ -608,8 +615,9 @@ class MangaController :
|
|||||||
|
|
||||||
override fun openMangaCoverPicker(manga: Manga) {
|
override fun openMangaCoverPicker(manga: Manga) {
|
||||||
if (manga.favorite) {
|
if (manga.favorite) {
|
||||||
val intent = Intent(Intent.ACTION_GET_CONTENT)
|
val intent = Intent(Intent.ACTION_GET_CONTENT).apply {
|
||||||
intent.type = "image/*"
|
type = "image/*"
|
||||||
|
}
|
||||||
startActivityForResult(
|
startActivityForResult(
|
||||||
Intent.createChooser(
|
Intent.createChooser(
|
||||||
intent,
|
intent,
|
||||||
@ -988,7 +996,9 @@ class MangaController :
|
|||||||
chapters.forEach {
|
chapters.forEach {
|
||||||
chaptersAdapter?.updateItem(it)
|
chaptersAdapter?.updateItem(it)
|
||||||
}
|
}
|
||||||
chaptersAdapter?.notifyDataSetChanged()
|
launchUI {
|
||||||
|
chaptersAdapter?.notifyDataSetChanged()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onChaptersDeletedError(error: Throwable) {
|
fun onChaptersDeletedError(error: Throwable) {
|
||||||
|
@ -316,17 +316,26 @@ class MangaPresenter(
|
|||||||
private fun observeDownloads() {
|
private fun observeDownloads() {
|
||||||
observeDownloadsStatusSubscription?.let { remove(it) }
|
observeDownloadsStatusSubscription?.let { remove(it) }
|
||||||
observeDownloadsStatusSubscription = downloadManager.queue.getStatusObservable()
|
observeDownloadsStatusSubscription = downloadManager.queue.getStatusObservable()
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(Schedulers.io())
|
||||||
|
.onBackpressureLatest()
|
||||||
.filter { download -> download.manga.id == manga.id }
|
.filter { download -> download.manga.id == manga.id }
|
||||||
.doOnNext { onDownloadStatusChange(it) }
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribeLatestCache(MangaController::onChapterDownloadUpdate) { _, error ->
|
.subscribeLatestCache(
|
||||||
Timber.e(error)
|
{ view, it ->
|
||||||
}
|
onDownloadStatusChange(it)
|
||||||
|
view.onChapterDownloadUpdate(it)
|
||||||
|
},
|
||||||
|
{ _, error ->
|
||||||
|
Timber.e(error)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
observeDownloadsPageSubscription?.let { remove(it) }
|
observeDownloadsPageSubscription?.let { remove(it) }
|
||||||
observeDownloadsPageSubscription = downloadManager.queue.getProgressObservable()
|
observeDownloadsPageSubscription = downloadManager.queue.getProgressObservable()
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(Schedulers.io())
|
||||||
|
.onBackpressureLatest()
|
||||||
.filter { download -> download.manga.id == manga.id }
|
.filter { download -> download.manga.id == manga.id }
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribeLatestCache(MangaController::onChapterDownloadUpdate) { _, error ->
|
.subscribeLatestCache(MangaController::onChapterDownloadUpdate) { _, error ->
|
||||||
Timber.e(error)
|
Timber.e(error)
|
||||||
}
|
}
|
||||||
@ -484,7 +493,7 @@ class MangaPresenter(
|
|||||||
db.updateChaptersProgress(chapters).executeAsBlocking()
|
db.updateChaptersProgress(chapters).executeAsBlocking()
|
||||||
|
|
||||||
if (preferences.removeAfterMarkedAsRead()) {
|
if (preferences.removeAfterMarkedAsRead()) {
|
||||||
deleteChapters(chapters)
|
deleteChapters(chapters.filter { it.read })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,9 @@ class ChapterDownloadView @JvmOverloads constructor(context: Context, attrs: Att
|
|||||||
|
|
||||||
private val binding: ChapterDownloadViewBinding
|
private val binding: ChapterDownloadViewBinding
|
||||||
|
|
||||||
|
private var state = Download.State.NOT_DOWNLOADED
|
||||||
|
private var progress = 0
|
||||||
|
|
||||||
private var downloadIconAnimator: ObjectAnimator? = null
|
private var downloadIconAnimator: ObjectAnimator? = null
|
||||||
private var isAnimating = false
|
private var isAnimating = false
|
||||||
|
|
||||||
@ -23,6 +26,17 @@ class ChapterDownloadView @JvmOverloads constructor(context: Context, attrs: Att
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun setState(state: Download.State, progress: Int = 0) {
|
fun setState(state: Download.State, progress: Int = 0) {
|
||||||
|
val isDirty = this.state.value != state.value || this.progress != progress
|
||||||
|
|
||||||
|
this.state = state
|
||||||
|
this.progress = progress
|
||||||
|
|
||||||
|
if (isDirty) {
|
||||||
|
updateLayout()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateLayout() {
|
||||||
binding.downloadIconBorder.isVisible = state == Download.State.NOT_DOWNLOADED
|
binding.downloadIconBorder.isVisible = state == Download.State.NOT_DOWNLOADED
|
||||||
|
|
||||||
binding.downloadIcon.isVisible = state == Download.State.NOT_DOWNLOADED || state == Download.State.DOWNLOADING
|
binding.downloadIcon.isVisible = state == Download.State.NOT_DOWNLOADED || state == Download.State.DOWNLOADING
|
||||||
|
@ -17,7 +17,7 @@ class ChaptersSettingsSheet(
|
|||||||
private val router: Router,
|
private val router: Router,
|
||||||
private val presenter: MangaPresenter,
|
private val presenter: MangaPresenter,
|
||||||
onGroupClickListener: (ExtendedNavigationView.Group) -> Unit
|
onGroupClickListener: (ExtendedNavigationView.Group) -> Unit
|
||||||
) : TabbedBottomSheetDialog(router) {
|
) : TabbedBottomSheetDialog(router.activity!!) {
|
||||||
|
|
||||||
val filters: Filter
|
val filters: Filter
|
||||||
private val sort: Sort
|
private val sort: Sort
|
||||||
|
@ -2,13 +2,13 @@ package eu.kanade.tachiyomi.ui.manga.track
|
|||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.databinding.TrackItemBinding
|
import eu.kanade.tachiyomi.databinding.TrackItemBinding
|
||||||
import eu.kanade.tachiyomi.ui.base.holder.BaseViewHolder
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.text.DateFormat
|
import java.text.DateFormat
|
||||||
|
|
||||||
class TrackHolder(private val binding: TrackItemBinding, adapter: TrackAdapter) : BaseViewHolder(binding.root) {
|
class TrackHolder(private val binding: TrackItemBinding, adapter: TrackAdapter) : RecyclerView.ViewHolder(binding.root) {
|
||||||
|
|
||||||
private val preferences: PreferencesHelper by injectLazy()
|
private val preferences: PreferencesHelper by injectLazy()
|
||||||
|
|
||||||
|
@ -55,24 +55,24 @@ class TrackSearchAdapter(context: Context) :
|
|||||||
.into(binding.trackSearchCover)
|
.into(binding.trackSearchCover)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (track.publishing_status.isBlank()) {
|
val hasStatus = track.publishing_status.isNotBlank()
|
||||||
binding.trackSearchStatus.isVisible = false
|
binding.trackSearchStatus.isVisible = hasStatus
|
||||||
binding.trackSearchStatusResult.isVisible = false
|
binding.trackSearchStatusResult.isVisible = hasStatus
|
||||||
} else {
|
if (hasStatus) {
|
||||||
binding.trackSearchStatusResult.text = track.publishing_status.capitalize()
|
binding.trackSearchStatusResult.text = track.publishing_status.capitalize()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (track.publishing_type.isBlank()) {
|
val hasType = track.publishing_type.isNotBlank()
|
||||||
binding.trackSearchType.isVisible = false
|
binding.trackSearchType.isVisible = hasType
|
||||||
binding.trackSearchTypeResult.isVisible = false
|
binding.trackSearchTypeResult.isVisible = hasType
|
||||||
} else {
|
if (hasType) {
|
||||||
binding.trackSearchTypeResult.text = track.publishing_type.capitalize()
|
binding.trackSearchTypeResult.text = track.publishing_type.capitalize()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (track.start_date.isBlank()) {
|
val hasStartDate = track.start_date.isNotBlank()
|
||||||
binding.trackSearchStart.isVisible = false
|
binding.trackSearchStart.isVisible = hasStartDate
|
||||||
binding.trackSearchStartResult.isVisible = false
|
binding.trackSearchStartResult.isVisible = hasStartDate
|
||||||
} else {
|
if (hasStartDate) {
|
||||||
binding.trackSearchStartResult.text = track.start_date
|
binding.trackSearchStartResult.text = track.start_date
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,12 @@
|
|||||||
package eu.kanade.tachiyomi.ui.manga.track
|
package eu.kanade.tachiyomi.ui.manga.track
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.core.net.toUri
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.databinding.TrackControllerBinding
|
import eu.kanade.tachiyomi.databinding.TrackControllerBinding
|
||||||
|
import eu.kanade.tachiyomi.ui.base.controller.openInBrowser
|
||||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||||
import eu.kanade.tachiyomi.widget.sheet.BaseBottomSheetDialog
|
import eu.kanade.tachiyomi.widget.sheet.BaseBottomSheetDialog
|
||||||
@ -65,7 +64,7 @@ class TrackSheet(
|
|||||||
val track = adapter.getItem(position)?.track ?: return
|
val track = adapter.getItem(position)?.track ?: return
|
||||||
|
|
||||||
if (track.tracking_url.isNotBlank()) {
|
if (track.tracking_url.isNotBlank()) {
|
||||||
controller.activity?.startActivity(Intent(Intent.ACTION_VIEW, track.tracking_url.toUri()))
|
controller.openInBrowser(track.tracking_url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,10 +1,8 @@
|
|||||||
package eu.kanade.tachiyomi.ui.more
|
package eu.kanade.tachiyomi.ui.more
|
||||||
|
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.core.net.toUri
|
|
||||||
import androidx.core.os.bundleOf
|
import androidx.core.os.bundleOf
|
||||||
import androidx.preference.PreferenceScreen
|
import androidx.preference.PreferenceScreen
|
||||||
import com.afollestad.materialdialogs.MaterialDialog
|
import com.afollestad.materialdialogs.MaterialDialog
|
||||||
@ -15,6 +13,7 @@ import eu.kanade.tachiyomi.data.updater.UpdateResult
|
|||||||
import eu.kanade.tachiyomi.data.updater.UpdaterService
|
import eu.kanade.tachiyomi.data.updater.UpdaterService
|
||||||
import eu.kanade.tachiyomi.data.updater.github.GithubUpdateChecker
|
import eu.kanade.tachiyomi.data.updater.github.GithubUpdateChecker
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||||
|
import eu.kanade.tachiyomi.ui.base.controller.openInBrowser
|
||||||
import eu.kanade.tachiyomi.ui.setting.SettingsController
|
import eu.kanade.tachiyomi.ui.setting.SettingsController
|
||||||
import eu.kanade.tachiyomi.util.lang.launchNow
|
import eu.kanade.tachiyomi.util.lang.launchNow
|
||||||
import eu.kanade.tachiyomi.util.lang.toDateTimestampString
|
import eu.kanade.tachiyomi.util.lang.toDateTimestampString
|
||||||
@ -76,19 +75,15 @@ class AboutController : SettingsController() {
|
|||||||
} else {
|
} else {
|
||||||
"https://github.com/tachiyomiorg/tachiyomi/releases/tag/v${BuildConfig.VERSION_NAME}"
|
"https://github.com/tachiyomiorg/tachiyomi/releases/tag/v${BuildConfig.VERSION_NAME}"
|
||||||
}
|
}
|
||||||
|
openInBrowser(url)
|
||||||
val intent = Intent(Intent.ACTION_VIEW, url.toUri())
|
|
||||||
startActivity(intent)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (BuildConfig.DEBUG) {
|
if (BuildConfig.DEBUG) {
|
||||||
preference {
|
preference {
|
||||||
key = "pref_about_notices"
|
key = "pref_about_notices"
|
||||||
titleRes = R.string.notices
|
titleRes = R.string.notices
|
||||||
|
|
||||||
onClick {
|
onClick {
|
||||||
val intent = Intent(Intent.ACTION_VIEW, "https://github.com/tachiyomiorg/tachiyomi/blob/master/PREVIEW_RELEASE_NOTES.md".toUri())
|
openInBrowser("https://github.com/tachiyomiorg/tachiyomi/blob/master/PREVIEW_RELEASE_NOTES.md")
|
||||||
startActivity(intent)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -97,47 +92,46 @@ class AboutController : SettingsController() {
|
|||||||
preference {
|
preference {
|
||||||
key = "pref_about_website"
|
key = "pref_about_website"
|
||||||
titleRes = R.string.website
|
titleRes = R.string.website
|
||||||
val url = "https://tachiyomi.org"
|
"https://tachiyomi.org".also {
|
||||||
summary = url
|
summary = it
|
||||||
onClick {
|
onClick { openInBrowser(it) }
|
||||||
val intent = Intent(Intent.ACTION_VIEW, url.toUri())
|
}
|
||||||
startActivity(intent)
|
}
|
||||||
|
preference {
|
||||||
|
key = "pref_about_twitter"
|
||||||
|
title = "Twitter"
|
||||||
|
"https://twitter.com/tachiyomiorg".also {
|
||||||
|
summary = it
|
||||||
|
onClick { openInBrowser(it) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
preference {
|
preference {
|
||||||
key = "pref_about_discord"
|
key = "pref_about_discord"
|
||||||
title = "Discord"
|
title = "Discord"
|
||||||
val url = "https://discord.gg/tachiyomi"
|
"https://discord.gg/tachiyomi".also {
|
||||||
summary = url
|
summary = it
|
||||||
onClick {
|
onClick { openInBrowser(it) }
|
||||||
val intent = Intent(Intent.ACTION_VIEW, url.toUri())
|
|
||||||
startActivity(intent)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
preference {
|
preference {
|
||||||
key = "pref_about_github"
|
key = "pref_about_github"
|
||||||
title = "GitHub"
|
title = "GitHub"
|
||||||
val url = "https://github.com/tachiyomiorg/tachiyomi"
|
"https://github.com/tachiyomiorg/tachiyomi".also {
|
||||||
summary = url
|
summary = it
|
||||||
onClick {
|
onClick { openInBrowser(it) }
|
||||||
val intent = Intent(Intent.ACTION_VIEW, url.toUri())
|
|
||||||
startActivity(intent)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
preference {
|
preference {
|
||||||
key = "pref_about_label_extensions"
|
key = "pref_about_label_extensions"
|
||||||
titleRes = R.string.label_extensions
|
titleRes = R.string.label_extensions
|
||||||
val url = "https://github.com/tachiyomiorg/tachiyomi-extensions"
|
"https://github.com/tachiyomiorg/tachiyomi-extensions".also {
|
||||||
summary = url
|
summary = it
|
||||||
onClick {
|
onClick { openInBrowser(it) }
|
||||||
val intent = Intent(Intent.ACTION_VIEW, url.toUri())
|
|
||||||
startActivity(intent)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
preference {
|
preference {
|
||||||
key = "pref_about_licenses"
|
key = "pref_about_licenses"
|
||||||
titleRes = R.string.licenses
|
titleRes = R.string.licenses
|
||||||
|
|
||||||
onClick {
|
onClick {
|
||||||
LibsBuilder()
|
LibsBuilder()
|
||||||
.withActivityTitle(activity!!.getString(R.string.licenses))
|
.withActivityTitle(activity!!.getString(R.string.licenses))
|
||||||
|
@ -78,7 +78,7 @@ class MoreController :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
preference {
|
preference {
|
||||||
titleRes = R.string.label_categories
|
titleRes = R.string.categories
|
||||||
iconRes = R.drawable.ic_label_24dp
|
iconRes = R.drawable.ic_label_24dp
|
||||||
iconTint = tintColor
|
iconTint = tintColor
|
||||||
onClick {
|
onClick {
|
||||||
@ -151,6 +151,6 @@ class MoreController :
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val URL_HELP = "https://tachiyomi.org/help/"
|
const val URL_HELP = "https://tachiyomi.org/help/"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,30 +3,29 @@ package eu.kanade.tachiyomi.ui.reader
|
|||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.annotation.TargetApi
|
import android.annotation.TargetApi
|
||||||
import android.app.ProgressDialog
|
import android.app.ProgressDialog
|
||||||
|
import android.content.ClipData
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.ActivityInfo
|
|
||||||
import android.content.res.Configuration
|
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.view.Gravity
|
||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
import android.view.View
|
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
import android.view.animation.Animation
|
import android.view.animation.Animation
|
||||||
import android.view.animation.AnimationUtils
|
import android.view.animation.AnimationUtils
|
||||||
import android.widget.SeekBar
|
import android.widget.SeekBar
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.core.view.ViewCompat
|
import androidx.core.view.ViewCompat
|
||||||
import androidx.core.view.WindowInsetsCompat
|
import androidx.core.view.WindowInsetsCompat
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.core.view.setPadding
|
import androidx.core.view.setPadding
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
||||||
import com.google.android.material.snackbar.Snackbar
|
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
@ -34,14 +33,20 @@ import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
|||||||
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.preference.asImmediateFlow
|
import eu.kanade.tachiyomi.data.preference.asImmediateFlow
|
||||||
|
import eu.kanade.tachiyomi.data.preference.toggle
|
||||||
import eu.kanade.tachiyomi.databinding.ReaderActivityBinding
|
import eu.kanade.tachiyomi.databinding.ReaderActivityBinding
|
||||||
import eu.kanade.tachiyomi.ui.base.activity.BaseRxActivity
|
import eu.kanade.tachiyomi.ui.base.activity.BaseRxActivity
|
||||||
|
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||||
|
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||||
import eu.kanade.tachiyomi.ui.reader.ReaderPresenter.SetAsCoverResult.AddToLibraryFirst
|
import eu.kanade.tachiyomi.ui.reader.ReaderPresenter.SetAsCoverResult.AddToLibraryFirst
|
||||||
import eu.kanade.tachiyomi.ui.reader.ReaderPresenter.SetAsCoverResult.Error
|
import eu.kanade.tachiyomi.ui.reader.ReaderPresenter.SetAsCoverResult.Error
|
||||||
import eu.kanade.tachiyomi.ui.reader.ReaderPresenter.SetAsCoverResult.Success
|
import eu.kanade.tachiyomi.ui.reader.ReaderPresenter.SetAsCoverResult.Success
|
||||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
|
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
|
||||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
||||||
import eu.kanade.tachiyomi.ui.reader.model.ViewerChapters
|
import eu.kanade.tachiyomi.ui.reader.model.ViewerChapters
|
||||||
|
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
|
||||||
|
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsSheet
|
||||||
|
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
|
||||||
import eu.kanade.tachiyomi.ui.reader.viewer.BaseViewer
|
import eu.kanade.tachiyomi.ui.reader.viewer.BaseViewer
|
||||||
import eu.kanade.tachiyomi.ui.reader.viewer.pager.L2RPagerViewer
|
import eu.kanade.tachiyomi.ui.reader.viewer.pager.L2RPagerViewer
|
||||||
import eu.kanade.tachiyomi.ui.reader.viewer.pager.R2LPagerViewer
|
import eu.kanade.tachiyomi.ui.reader.viewer.pager.R2LPagerViewer
|
||||||
@ -54,8 +59,8 @@ import eu.kanade.tachiyomi.util.system.toast
|
|||||||
import eu.kanade.tachiyomi.util.view.defaultBar
|
import eu.kanade.tachiyomi.util.view.defaultBar
|
||||||
import eu.kanade.tachiyomi.util.view.hideBar
|
import eu.kanade.tachiyomi.util.view.hideBar
|
||||||
import eu.kanade.tachiyomi.util.view.isDefaultBar
|
import eu.kanade.tachiyomi.util.view.isDefaultBar
|
||||||
|
import eu.kanade.tachiyomi.util.view.setTooltip
|
||||||
import eu.kanade.tachiyomi.util.view.showBar
|
import eu.kanade.tachiyomi.util.view.showBar
|
||||||
import eu.kanade.tachiyomi.util.view.snack
|
|
||||||
import eu.kanade.tachiyomi.widget.SimpleAnimationListener
|
import eu.kanade.tachiyomi.widget.SimpleAnimationListener
|
||||||
import eu.kanade.tachiyomi.widget.SimpleSeekBarListener
|
import eu.kanade.tachiyomi.widget.SimpleSeekBarListener
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
@ -76,6 +81,16 @@ import kotlin.math.abs
|
|||||||
@RequiresPresenter(ReaderPresenter::class)
|
@RequiresPresenter(ReaderPresenter::class)
|
||||||
class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>() {
|
class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun newIntent(context: Context, manga: Manga, chapter: Chapter): Intent {
|
||||||
|
return Intent(context, ReaderActivity::class.java).apply {
|
||||||
|
putExtra("manga", manga.id)
|
||||||
|
putExtra("chapter", chapter.id)
|
||||||
|
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private val preferences: PreferencesHelper by injectLazy()
|
private val preferences: PreferencesHelper by injectLazy()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -108,22 +123,9 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
|
|||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
private var progressDialog: ProgressDialog? = null
|
private var progressDialog: ProgressDialog? = null
|
||||||
|
|
||||||
companion object {
|
private var menuToggleToast: Toast? = null
|
||||||
@Suppress("unused")
|
|
||||||
const val LEFT_TO_RIGHT = 1
|
|
||||||
const val RIGHT_TO_LEFT = 2
|
|
||||||
const val VERTICAL = 3
|
|
||||||
const val WEBTOON = 4
|
|
||||||
const val VERTICAL_PLUS = 5
|
|
||||||
|
|
||||||
fun newIntent(context: Context, manga: Manga, chapter: Chapter): Intent {
|
private var readingModeToast: Toast? = null
|
||||||
return Intent(context, ReaderActivity::class.java).apply {
|
|
||||||
putExtra("manga", manga.id)
|
|
||||||
putExtra("chapter", chapter.id)
|
|
||||||
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when the activity is created. Initializes the presenter and configuration.
|
* Called when the activity is created. Initializes the presenter and configuration.
|
||||||
@ -173,6 +175,8 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
|
|||||||
viewer?.destroy()
|
viewer?.destroy()
|
||||||
viewer = null
|
viewer = null
|
||||||
config = null
|
config = null
|
||||||
|
menuToggleToast?.cancel()
|
||||||
|
readingModeToast?.cancel()
|
||||||
progressDialog?.dismiss()
|
progressDialog?.dismiss()
|
||||||
progressDialog = null
|
progressDialog = null
|
||||||
}
|
}
|
||||||
@ -236,18 +240,6 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
|
|||||||
presenter.bookmarkCurrentChapter(false)
|
presenter.bookmarkCurrentChapter(false)
|
||||||
invalidateOptionsMenu()
|
invalidateOptionsMenu()
|
||||||
}
|
}
|
||||||
R.id.action_settings -> ReaderSettingsSheet(this).show()
|
|
||||||
R.id.action_custom_filter -> {
|
|
||||||
val sheet = ReaderColorFilterSheet(this)
|
|
||||||
// Remove dimmed backdrop so changes can be previewed
|
|
||||||
.apply { window?.setDimAmount(0f) }
|
|
||||||
|
|
||||||
// Hide toolbars while sheet is open for better preview
|
|
||||||
sheet.setOnDismissListener { setMenuVisibility(true) }
|
|
||||||
setMenuVisibility(false)
|
|
||||||
|
|
||||||
sheet.show()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return super.onOptionsItemSelected(item)
|
return super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
@ -293,7 +285,6 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
|
|||||||
* Initializes the reader menu. It sets up click listeners and the initial visibility.
|
* Initializes the reader menu. It sets up click listeners and the initial visibility.
|
||||||
*/
|
*/
|
||||||
private fun initializeMenu() {
|
private fun initializeMenu() {
|
||||||
// Set toolbar
|
|
||||||
setSupportActionBar(binding.toolbar)
|
setSupportActionBar(binding.toolbar)
|
||||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
binding.toolbar.setNavigationOnClickListener {
|
binding.toolbar.setNavigationOnClickListener {
|
||||||
@ -313,6 +304,18 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
|
|||||||
insets
|
insets
|
||||||
}
|
}
|
||||||
|
|
||||||
|
binding.toolbar.setOnClickListener {
|
||||||
|
presenter.manga?.id?.let { id ->
|
||||||
|
startActivity(
|
||||||
|
Intent(this, MainActivity::class.java).apply {
|
||||||
|
action = MainActivity.SHORTCUT_MANGA
|
||||||
|
putExtra(MangaController.MANGA_EXTRA, id)
|
||||||
|
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Init listeners on bottom menu
|
// Init listeners on bottom menu
|
||||||
binding.pageSeekbar.setOnSeekBarChangeListener(
|
binding.pageSeekbar.setOnSeekBarChangeListener(
|
||||||
object : SimpleSeekBarListener() {
|
object : SimpleSeekBarListener() {
|
||||||
@ -342,15 +345,105 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
initBottomShortcuts()
|
||||||
|
|
||||||
// Set initial visibility
|
// Set initial visibility
|
||||||
setMenuVisibility(menuVisible)
|
setMenuVisibility(menuVisible)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun initBottomShortcuts() {
|
||||||
|
// Reading mode
|
||||||
|
with(binding.actionReadingMode) {
|
||||||
|
setTooltip(R.string.viewer)
|
||||||
|
|
||||||
|
setOnClickListener {
|
||||||
|
val newReadingMode =
|
||||||
|
ReadingModeType.getNextReadingMode(presenter.getMangaViewer(resolveDefault = false))
|
||||||
|
presenter.setMangaViewer(newReadingMode.prefValue)
|
||||||
|
|
||||||
|
menuToggleToast?.cancel()
|
||||||
|
if (!preferences.showReadingMode()) {
|
||||||
|
menuToggleToast = toast(newReadingMode.stringRes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rotation
|
||||||
|
with(binding.actionRotation) {
|
||||||
|
setTooltip(R.string.pref_rotation_type)
|
||||||
|
|
||||||
|
setOnClickListener {
|
||||||
|
val newOrientation =
|
||||||
|
OrientationType.getNextOrientation(preferences.rotation().get(), resources)
|
||||||
|
|
||||||
|
preferences.rotation().set(newOrientation.prefValue)
|
||||||
|
setOrientation(newOrientation.flag)
|
||||||
|
|
||||||
|
menuToggleToast?.cancel()
|
||||||
|
menuToggleToast = toast(newOrientation.stringRes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
preferences.rotation().asImmediateFlow { updateRotationShortcut(it) }
|
||||||
|
.launchIn(lifecycleScope)
|
||||||
|
|
||||||
|
// Crop borders
|
||||||
|
with(binding.actionCropBorders) {
|
||||||
|
setTooltip(R.string.pref_crop_borders)
|
||||||
|
|
||||||
|
setOnClickListener {
|
||||||
|
val isPagerType = ReadingModeType.isPagerType(presenter.getMangaViewer())
|
||||||
|
if (isPagerType) {
|
||||||
|
preferences.cropBorders().toggle()
|
||||||
|
} else {
|
||||||
|
preferences.cropBordersWebtoon().toggle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updateCropBordersShortcut()
|
||||||
|
listOf(preferences.cropBorders(), preferences.cropBordersWebtoon())
|
||||||
|
.forEach { pref ->
|
||||||
|
pref.asFlow()
|
||||||
|
.onEach { updateCropBordersShortcut() }
|
||||||
|
.launchIn(lifecycleScope)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Settings sheet
|
||||||
|
with(binding.actionSettings) {
|
||||||
|
setTooltip(R.string.action_settings)
|
||||||
|
|
||||||
|
setOnClickListener {
|
||||||
|
ReaderSettingsSheet(this@ReaderActivity).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateRotationShortcut(preference: Int) {
|
||||||
|
val orientation = OrientationType.fromPreference(preference, resources)
|
||||||
|
binding.actionRotation.setImageResource(orientation.iconRes)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateCropBordersShortcut() {
|
||||||
|
val isPagerType = ReadingModeType.isPagerType(presenter.getMangaViewer())
|
||||||
|
val enabled = if (isPagerType) {
|
||||||
|
preferences.cropBorders().get()
|
||||||
|
} else {
|
||||||
|
preferences.cropBordersWebtoon().get()
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.actionCropBorders.setImageResource(
|
||||||
|
if (enabled) {
|
||||||
|
R.drawable.ic_crop_24dp
|
||||||
|
} else {
|
||||||
|
R.drawable.ic_crop_off_24dp
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the visibility of the menu according to [visible] and with an optional parameter to
|
* Sets the visibility of the menu according to [visible] and with an optional parameter to
|
||||||
* [animate] the views.
|
* [animate] the views.
|
||||||
*/
|
*/
|
||||||
private fun setMenuVisibility(visible: Boolean, animate: Boolean = true) {
|
fun setMenuVisibility(visible: Boolean, animate: Boolean = true) {
|
||||||
menuVisible = visible
|
menuVisible = visible
|
||||||
if (visible) {
|
if (visible) {
|
||||||
if (preferences.fullscreen().get()) {
|
if (preferences.fullscreen().get()) {
|
||||||
@ -365,7 +458,7 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
|
|||||||
toolbarAnimation.setAnimationListener(
|
toolbarAnimation.setAnimationListener(
|
||||||
object : SimpleAnimationListener() {
|
object : SimpleAnimationListener() {
|
||||||
override fun onAnimationStart(animation: Animation) {
|
override fun onAnimationStart(animation: Animation) {
|
||||||
// Fix status bar being translucent the first time it's opened.
|
// Fix status bar being translucent the first time it's opened.
|
||||||
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
|
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -421,12 +514,16 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
|
|||||||
*/
|
*/
|
||||||
fun setManga(manga: Manga) {
|
fun setManga(manga: Manga) {
|
||||||
val prevViewer = viewer
|
val prevViewer = viewer
|
||||||
|
|
||||||
|
val viewerMode = ReadingModeType.fromPreference(presenter.getMangaViewer(resolveDefault = false))
|
||||||
|
binding.actionReadingMode.setImageResource(viewerMode.iconRes)
|
||||||
|
|
||||||
val newViewer = when (presenter.getMangaViewer()) {
|
val newViewer = when (presenter.getMangaViewer()) {
|
||||||
RIGHT_TO_LEFT -> R2LPagerViewer(this)
|
ReadingModeType.LEFT_TO_RIGHT.prefValue -> L2RPagerViewer(this)
|
||||||
VERTICAL -> VerticalPagerViewer(this)
|
ReadingModeType.VERTICAL.prefValue -> VerticalPagerViewer(this)
|
||||||
WEBTOON -> WebtoonViewer(this)
|
ReadingModeType.WEBTOON.prefValue -> WebtoonViewer(this)
|
||||||
VERTICAL_PLUS -> WebtoonViewer(this, isContinuous = false)
|
ReadingModeType.CONTINUOUS_VERTICAL.prefValue -> WebtoonViewer(this, isContinuous = false)
|
||||||
else -> L2RPagerViewer(this)
|
else -> R2LPagerViewer(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Destroy previous viewer if there was one
|
// Destroy previous viewer if there was one
|
||||||
@ -438,20 +535,30 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
|
|||||||
binding.viewerContainer.addView(newViewer.getView())
|
binding.viewerContainer.addView(newViewer.getView())
|
||||||
|
|
||||||
if (preferences.showReadingMode()) {
|
if (preferences.showReadingMode()) {
|
||||||
showReadingModeSnackbar(presenter.getMangaViewer())
|
showReadingModeToast(presenter.getMangaViewer())
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.toolbar.title = manga.title
|
binding.toolbar.title = manga.title
|
||||||
|
|
||||||
binding.pageSeekbar.isRTL = newViewer is R2LPagerViewer
|
binding.pageSeekbar.isRTL = newViewer is R2LPagerViewer
|
||||||
|
if (newViewer is R2LPagerViewer) {
|
||||||
|
binding.leftChapter.setTooltip(R.string.action_next_chapter)
|
||||||
|
binding.rightChapter.setTooltip(R.string.action_previous_chapter)
|
||||||
|
} else {
|
||||||
|
binding.leftChapter.setTooltip(R.string.action_previous_chapter)
|
||||||
|
binding.rightChapter.setTooltip(R.string.action_next_chapter)
|
||||||
|
}
|
||||||
|
|
||||||
binding.pleaseWait.isVisible = true
|
binding.pleaseWait.isVisible = true
|
||||||
binding.pleaseWait.startAnimation(AnimationUtils.loadAnimation(this, R.anim.fade_in_long))
|
binding.pleaseWait.startAnimation(AnimationUtils.loadAnimation(this, R.anim.fade_in_long))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showReadingModeSnackbar(mode: Int) {
|
private fun showReadingModeToast(mode: Int) {
|
||||||
val strings = resources.getStringArray(R.array.viewers_selector)
|
val strings = resources.getStringArray(R.array.viewers_selector)
|
||||||
binding.root.snack(strings[mode], Snackbar.LENGTH_SHORT)
|
readingModeToast?.cancel()
|
||||||
|
readingModeToast = toast(strings[mode]) {
|
||||||
|
it.setGravity(Gravity.CENTER_VERTICAL or Gravity.CENTER_HORIZONTAL, 0, 0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -595,10 +702,11 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
|
|||||||
val manga = presenter.manga ?: return
|
val manga = presenter.manga ?: return
|
||||||
val chapter = page.chapter.chapter
|
val chapter = page.chapter.chapter
|
||||||
|
|
||||||
val stream = file.getUriCompat(this)
|
val uri = file.getUriCompat(this)
|
||||||
val intent = Intent(Intent.ACTION_SEND).apply {
|
val intent = Intent(Intent.ACTION_SEND).apply {
|
||||||
putExtra(Intent.EXTRA_TEXT, getString(R.string.share_page_info, manga.title, chapter.name, page.number))
|
putExtra(Intent.EXTRA_TEXT, getString(R.string.share_page_info, manga.title, chapter.name, page.number))
|
||||||
putExtra(Intent.EXTRA_STREAM, stream)
|
putExtra(Intent.EXTRA_STREAM, uri)
|
||||||
|
clipData = ClipData.newRawUri(null, uri)
|
||||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||||
type = "image/*"
|
type = "image/*"
|
||||||
}
|
}
|
||||||
@ -650,6 +758,16 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Forces the user preferred [orientation] on the activity.
|
||||||
|
*/
|
||||||
|
private fun setOrientation(orientation: Int) {
|
||||||
|
val newOrientation = OrientationType.fromPreference(orientation, resources)
|
||||||
|
if (newOrientation.flag != requestedOrientation) {
|
||||||
|
requestedOrientation = newOrientation.flag
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class that handles the user preferences of the reader.
|
* Class that handles the user preferences of the reader.
|
||||||
*/
|
*/
|
||||||
@ -703,38 +821,11 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
|
|||||||
.launchIn(lifecycleScope)
|
.launchIn(lifecycleScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Forces the user preferred [orientation] on the activity.
|
|
||||||
*/
|
|
||||||
private fun setOrientation(orientation: Int) {
|
|
||||||
val newOrientation = when (orientation) {
|
|
||||||
// Lock in current orientation
|
|
||||||
2 -> {
|
|
||||||
val currentOrientation = resources.configuration.orientation
|
|
||||||
if (currentOrientation == Configuration.ORIENTATION_PORTRAIT) {
|
|
||||||
ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT
|
|
||||||
} else {
|
|
||||||
ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Lock in portrait
|
|
||||||
3 -> ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT
|
|
||||||
// Lock in landscape
|
|
||||||
4 -> ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
|
|
||||||
// Rotation free
|
|
||||||
else -> ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newOrientation != requestedOrientation) {
|
|
||||||
requestedOrientation = newOrientation
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the visibility of the bottom page indicator according to [visible].
|
* Sets the visibility of the bottom page indicator according to [visible].
|
||||||
*/
|
*/
|
||||||
fun setPageNumberVisibility(visible: Boolean) {
|
fun setPageNumberVisibility(visible: Boolean) {
|
||||||
binding.pageNumber.visibility = if (visible) View.VISIBLE else View.INVISIBLE
|
binding.pageNumber.isVisible = visible
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -0,0 +1,119 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.reader
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.graphics.Paint
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.MotionEvent
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewPropertyAnimator
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation
|
||||||
|
import kotlin.math.abs
|
||||||
|
|
||||||
|
class ReaderNavigationOverlayView(context: Context, attributeSet: AttributeSet) : View(context, attributeSet) {
|
||||||
|
|
||||||
|
private var viewPropertyAnimator: ViewPropertyAnimator? = null
|
||||||
|
|
||||||
|
private var navigation: ViewerNavigation? = null
|
||||||
|
|
||||||
|
fun setNavigation(navigation: ViewerNavigation, showOnStart: Boolean) {
|
||||||
|
if (!showOnStart && this.navigation == null) {
|
||||||
|
this.navigation = navigation
|
||||||
|
isVisible = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.navigation = navigation
|
||||||
|
invalidate()
|
||||||
|
|
||||||
|
if (isVisible) return
|
||||||
|
|
||||||
|
viewPropertyAnimator = animate()
|
||||||
|
.alpha(1f)
|
||||||
|
.setDuration(FADE_DURATION)
|
||||||
|
.withStartAction {
|
||||||
|
isVisible = true
|
||||||
|
}
|
||||||
|
.withEndAction {
|
||||||
|
viewPropertyAnimator = null
|
||||||
|
}
|
||||||
|
viewPropertyAnimator?.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val regionPaint = Paint()
|
||||||
|
|
||||||
|
private val textPaint = Paint().apply {
|
||||||
|
textAlign = Paint.Align.CENTER
|
||||||
|
color = Color.WHITE
|
||||||
|
textSize = 64f
|
||||||
|
}
|
||||||
|
|
||||||
|
private val textBorderPaint = Paint().apply {
|
||||||
|
textAlign = Paint.Align.CENTER
|
||||||
|
color = Color.BLACK
|
||||||
|
textSize = 64f
|
||||||
|
style = Paint.Style.STROKE
|
||||||
|
strokeWidth = 8f
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDraw(canvas: Canvas?) {
|
||||||
|
if (navigation == null) return
|
||||||
|
|
||||||
|
navigation?.regions?.forEach { region ->
|
||||||
|
val rect = region.rectF
|
||||||
|
|
||||||
|
canvas?.save()
|
||||||
|
|
||||||
|
// Scale rect from 1f,1f to screen width and height
|
||||||
|
canvas?.scale(width.toFloat(), height.toFloat())
|
||||||
|
regionPaint.color = ContextCompat.getColor(context, region.type.colorRes)
|
||||||
|
canvas?.drawRect(rect, regionPaint)
|
||||||
|
|
||||||
|
canvas?.restore()
|
||||||
|
// Don't want scale anymore because it messes with drawText
|
||||||
|
canvas?.save()
|
||||||
|
|
||||||
|
// Translate origin to rect start (left, top)
|
||||||
|
canvas?.translate((width * rect.left), (height * rect.top))
|
||||||
|
|
||||||
|
// Calculate center of rect width on screen
|
||||||
|
val x = width * (abs(rect.left - rect.right) / 2)
|
||||||
|
|
||||||
|
// Calculate center of rect height on screen
|
||||||
|
val y = height * (abs(rect.top - rect.bottom) / 2)
|
||||||
|
|
||||||
|
canvas?.drawText(context.getString(region.type.nameRes), x, y, textBorderPaint)
|
||||||
|
canvas?.drawText(context.getString(region.type.nameRes), x, y, textPaint)
|
||||||
|
|
||||||
|
canvas?.restore()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun performClick(): Boolean {
|
||||||
|
super.performClick()
|
||||||
|
|
||||||
|
if (viewPropertyAnimator == null && isVisible) {
|
||||||
|
viewPropertyAnimator = animate()
|
||||||
|
.alpha(0f)
|
||||||
|
.setDuration(FADE_DURATION)
|
||||||
|
.withEndAction {
|
||||||
|
isVisible = false
|
||||||
|
viewPropertyAnimator = null
|
||||||
|
}
|
||||||
|
viewPropertyAnimator?.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTouchEvent(event: MotionEvent?): Boolean {
|
||||||
|
// Hide overlay if user start tapping or swiping
|
||||||
|
performClick()
|
||||||
|
return super.onTouchEvent(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private const val FADE_DURATION = 1000L
|
@ -489,9 +489,9 @@ class ReaderPresenter(
|
|||||||
/**
|
/**
|
||||||
* Returns the viewer position used by this manga or the default one.
|
* Returns the viewer position used by this manga or the default one.
|
||||||
*/
|
*/
|
||||||
fun getMangaViewer(): Int {
|
fun getMangaViewer(resolveDefault: Boolean = true): Int {
|
||||||
val manga = manga ?: return preferences.defaultViewer()
|
val manga = manga ?: return preferences.defaultViewer()
|
||||||
return if (manga.viewer == 0) preferences.defaultViewer() else manga.viewer
|
return if (resolveDefault && manga.viewer == 0) preferences.defaultViewer() else manga.viewer
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -559,7 +559,7 @@ class ReaderPresenter(
|
|||||||
val destDir = File(
|
val destDir = File(
|
||||||
Environment.getExternalStorageDirectory().absolutePath +
|
Environment.getExternalStorageDirectory().absolutePath +
|
||||||
File.separator + Environment.DIRECTORY_PICTURES +
|
File.separator + Environment.DIRECTORY_PICTURES +
|
||||||
File.separator + "Tachiyomi"
|
File.separator + context.getString(R.string.app_name)
|
||||||
)
|
)
|
||||||
|
|
||||||
// Copy file in background.
|
// Copy file in background.
|
||||||
|
@ -1,156 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.ui.reader
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.widget.CompoundButton
|
|
||||||
import android.widget.Spinner
|
|
||||||
import androidx.annotation.ArrayRes
|
|
||||||
import androidx.core.view.isVisible
|
|
||||||
import androidx.core.widget.NestedScrollView
|
|
||||||
import com.tfcporciuncula.flow.Preference
|
|
||||||
import eu.kanade.tachiyomi.R
|
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
|
||||||
import eu.kanade.tachiyomi.databinding.ReaderSettingsSheetBinding
|
|
||||||
import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerViewer
|
|
||||||
import eu.kanade.tachiyomi.ui.reader.viewer.webtoon.WebtoonViewer
|
|
||||||
import eu.kanade.tachiyomi.widget.IgnoreFirstSpinnerListener
|
|
||||||
import eu.kanade.tachiyomi.widget.sheet.BaseBottomSheetDialog
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sheet to show reader and viewer preferences.
|
|
||||||
*/
|
|
||||||
class ReaderSettingsSheet(private val activity: ReaderActivity) : BaseBottomSheetDialog(activity) {
|
|
||||||
|
|
||||||
private val preferences: PreferencesHelper by injectLazy()
|
|
||||||
|
|
||||||
private val binding = ReaderSettingsSheetBinding.inflate(activity.layoutInflater, null, false)
|
|
||||||
|
|
||||||
init {
|
|
||||||
val scroll = NestedScrollView(activity)
|
|
||||||
scroll.addView(binding.root)
|
|
||||||
setContentView(scroll)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when the sheet is created. It initializes the listeners and values of the preferences.
|
|
||||||
*/
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
|
|
||||||
initGeneralPreferences()
|
|
||||||
|
|
||||||
when (activity.viewer) {
|
|
||||||
is PagerViewer -> initPagerPreferences()
|
|
||||||
is WebtoonViewer -> initWebtoonPreferences()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Init general reader preferences.
|
|
||||||
*/
|
|
||||||
private fun initGeneralPreferences() {
|
|
||||||
binding.viewer.onItemSelectedListener = IgnoreFirstSpinnerListener { position ->
|
|
||||||
activity.presenter.setMangaViewer(position)
|
|
||||||
|
|
||||||
val mangaViewer = activity.presenter.getMangaViewer()
|
|
||||||
if (mangaViewer == ReaderActivity.WEBTOON || mangaViewer == ReaderActivity.VERTICAL_PLUS) {
|
|
||||||
initWebtoonPreferences()
|
|
||||||
} else {
|
|
||||||
initPagerPreferences()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
binding.viewer.setSelection(activity.presenter.manga?.viewer ?: 0, false)
|
|
||||||
|
|
||||||
binding.rotationMode.bindToPreference(preferences.rotation(), 1)
|
|
||||||
binding.backgroundColor.bindToIntPreference(preferences.readerTheme(), R.array.reader_themes_values)
|
|
||||||
binding.showPageNumber.bindToPreference(preferences.showPageNumber())
|
|
||||||
binding.fullscreen.bindToPreference(preferences.fullscreen())
|
|
||||||
binding.keepscreen.bindToPreference(preferences.keepScreenOn())
|
|
||||||
binding.longTap.bindToPreference(preferences.readWithLongTap())
|
|
||||||
binding.alwaysShowChapterTransition.bindToPreference(preferences.alwaysShowChapterTransition())
|
|
||||||
binding.pageTransitions.bindToPreference(preferences.pageTransitions())
|
|
||||||
|
|
||||||
// If the preference is explicitly disabled, that means the setting was configured since there is a cutout
|
|
||||||
if (activity.hasCutout || !preferences.cutoutShort().get()) {
|
|
||||||
binding.cutoutShort.isVisible = true
|
|
||||||
binding.cutoutShort.bindToPreference(preferences.cutoutShort())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Init the preferences for the pager reader.
|
|
||||||
*/
|
|
||||||
private fun initPagerPreferences() {
|
|
||||||
binding.webtoonPrefsGroup.root.isVisible = false
|
|
||||||
binding.pagerPrefsGroup.root.isVisible = true
|
|
||||||
|
|
||||||
binding.pagerPrefsGroup.tappingPrefsGroup.isVisible = preferences.readWithTapping().get()
|
|
||||||
|
|
||||||
binding.pagerPrefsGroup.tappingInverted.bindToPreference(preferences.pagerNavInverted())
|
|
||||||
|
|
||||||
binding.pagerPrefsGroup.pagerNav.bindToPreference(preferences.navigationModePager())
|
|
||||||
binding.pagerPrefsGroup.scaleType.bindToPreference(preferences.imageScaleType(), 1)
|
|
||||||
binding.pagerPrefsGroup.zoomStart.bindToPreference(preferences.zoomStart(), 1)
|
|
||||||
binding.pagerPrefsGroup.cropBorders.bindToPreference(preferences.cropBorders())
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Init the preferences for the webtoon reader.
|
|
||||||
*/
|
|
||||||
private fun initWebtoonPreferences() {
|
|
||||||
binding.pagerPrefsGroup.root.isVisible = false
|
|
||||||
binding.webtoonPrefsGroup.root.isVisible = true
|
|
||||||
|
|
||||||
binding.webtoonPrefsGroup.tappingPrefsGroup.isVisible = preferences.readWithTapping().get()
|
|
||||||
|
|
||||||
binding.webtoonPrefsGroup.tappingInverted.bindToPreference(preferences.webtoonNavInverted())
|
|
||||||
|
|
||||||
binding.webtoonPrefsGroup.webtoonNav.bindToPreference(preferences.navigationModeWebtoon())
|
|
||||||
binding.webtoonPrefsGroup.cropBordersWebtoon.bindToPreference(preferences.cropBordersWebtoon())
|
|
||||||
binding.webtoonPrefsGroup.webtoonSidePadding.bindToIntPreference(preferences.webtoonSidePadding(), R.array.webtoon_side_padding_values)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Binds a checkbox or switch view with a boolean preference.
|
|
||||||
*/
|
|
||||||
private fun CompoundButton.bindToPreference(pref: Preference<Boolean>) {
|
|
||||||
isChecked = pref.get()
|
|
||||||
setOnCheckedChangeListener { _, isChecked -> pref.set(isChecked) }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Binds a spinner to an int preference with an optional offset for the value.
|
|
||||||
*/
|
|
||||||
private fun Spinner.bindToPreference(pref: Preference<Int>, offset: Int = 0) {
|
|
||||||
onItemSelectedListener = IgnoreFirstSpinnerListener { position ->
|
|
||||||
pref.set(position + offset)
|
|
||||||
}
|
|
||||||
setSelection(pref.get() - offset, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Binds a spinner to an enum preference.
|
|
||||||
*/
|
|
||||||
private inline fun <reified T : Enum<T>> Spinner.bindToPreference(pref: Preference<T>) {
|
|
||||||
val enumConstants = T::class.java.enumConstants
|
|
||||||
|
|
||||||
onItemSelectedListener = IgnoreFirstSpinnerListener { position ->
|
|
||||||
enumConstants?.get(position)?.let { pref.set(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
enumConstants?.indexOf(pref.get())?.let { setSelection(it, false) }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Binds a spinner to an int preference. The position of the spinner item must
|
|
||||||
* correlate with the [intValues] resource item (in arrays.xml), which is a <string-array>
|
|
||||||
* of int values that will be parsed here and applied to the preference.
|
|
||||||
*/
|
|
||||||
private fun Spinner.bindToIntPreference(pref: Preference<Int>, @ArrayRes intValuesResource: Int) {
|
|
||||||
val intValues = resources.getStringArray(intValuesResource).map { it.toIntOrNull() }
|
|
||||||
onItemSelectedListener = IgnoreFirstSpinnerListener { position ->
|
|
||||||
pref.set(intValues[position]!!)
|
|
||||||
}
|
|
||||||
setSelection(intValues.indexOf(pref.get()), false)
|
|
||||||
}
|
|
||||||
}
|
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user