Compare commits
208 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cbf82a9d6a | |||
| 6fa67c9a5f | |||
| 7a85d6b163 | |||
| 5a909f48b6 | |||
| 4d22db919d | |||
| 8a9f2cce10 | |||
| ede0892cda | |||
| 5df0eb7ed1 | |||
| 67cb42ff30 | |||
| e65ea94a08 | |||
| fdac8a0380 | |||
| 1c56624d13 | |||
| 7c05c59501 | |||
| af77a58dcb | |||
| 5ee87ce8fc | |||
| 348ef2cf0f | |||
| 828944950b | |||
| 1c67e82325 | |||
| a45e273e2c | |||
| 45cf4adb5b | |||
| eb823cb208 | |||
| 056358fb9d | |||
| 9e40625c08 | |||
| 9684e34241 | |||
| 84fdd097e0 | |||
| a3c44fc5ad | |||
| 196e437da5 | |||
| 5e8b5ef6cf | |||
| 1ba07466ef | |||
| eb88c9c94b | |||
| d6cab9f9a5 | |||
| bcc120056c | |||
| 0e8aec7929 | |||
| 2d4e589db8 | |||
| 3eecf5cb20 | |||
| 6b08889c15 | |||
| 3bf070d88a | |||
| 6d9753f361 | |||
| f6b9867ce8 | |||
| 03366ae7e5 | |||
| a70a6cbe49 | |||
| b5a109440f | |||
| f6acf9325a | |||
| b0c0b12499 | |||
| 30ed1f11ee | |||
| 3077dc24ec | |||
| c2e882cb5b | |||
| 835351f206 | |||
| cee8335518 | |||
| 3aa5a36fdd | |||
| 74795bcc5e | |||
| 38a46825e2 | |||
| 7073e9b9e5 | |||
| 620887f90b | |||
| e38a0d47ac | |||
| eb9de3e6f1 | |||
| 37d9a51706 | |||
| acb9bafa0a | |||
| 7c4e89cbc5 | |||
| 5842765eda | |||
| 0925bd6a37 | |||
| 2ddf5f5037 | |||
| 367d95c825 | |||
| 6951314744 | |||
| d294db3e4e | |||
| b2cf1266ba | |||
| fb01b547de | |||
| d3482ef734 | |||
| d622c659eb | |||
| d1c497aa60 | |||
| 29a882eebb | |||
| 90ffb8cdf6 | |||
| dc760c0596 | |||
| 7be8062a2e | |||
| de9ce8f949 | |||
| 3c3f5cf35d | |||
| 7407e22b4e | |||
| 3a18e76089 | |||
| fa67ff165e | |||
| b9d2591e2a | |||
| 404a6a621a | |||
| aa376dc3a5 | |||
| 4ee110e225 | |||
| 26d52f5ad7 | |||
| 8b37c27a73 | |||
| 6e9043c633 | |||
| 2988524fd8 | |||
| 95c828bed6 | |||
| 8721d8c9ec | |||
| 5b9d2175e2 | |||
| 75f0ab2f40 | |||
| 709f76d53d | |||
| ac654340d8 | |||
| 438f64a358 | |||
| 41aec8bc96 | |||
| 97342723bf | |||
| a1cb3afe77 | |||
| 1165c57ffa | |||
| 565f005692 | |||
| 3a148c73ac | |||
| 12962b3486 | |||
| 75da7dcbdd | |||
| f02e3ae28f | |||
| c6369ed73f | |||
| fae2bd7ab7 | |||
| 03912407d5 | |||
| 879b41e97d | |||
| 6c3a957733 | |||
| 3d7c00c057 | |||
| 6e1adf6e04 | |||
| 23091cf50a | |||
| 78d49b0742 | |||
| 30250e350f | |||
| d9b3b7b266 | |||
| 5558790e15 | |||
| a1a9b4b812 | |||
| aac2fcb7d4 | |||
| 69ddd04256 | |||
| 7624abbebd | |||
| 67310ada53 | |||
| aa73670d50 | |||
| 2bde782211 | |||
| 7b01f0c608 | |||
| 781f4e393e | |||
| 93c92b674d | |||
| 368f565942 | |||
| 01c298bbc1 | |||
| 1399042efb | |||
| b2bfccdeae | |||
| 0d46e00b31 | |||
| 9aca115977 | |||
| e31e71ad44 | |||
| df950219f5 | |||
| 23e4b661bc | |||
| 7164f686d4 | |||
| 3122f783a9 | |||
| 6be8e2de3c | |||
| c092127404 | |||
| e7dd5f3c25 | |||
| 142fc0e4a6 | |||
| 300e04e8f6 | |||
| 07f684ac9e | |||
| 6840382df2 | |||
| c7b6216d24 | |||
| a989426d95 | |||
| d255ee805b | |||
| 21240cad06 | |||
| 5b8b10a96b | |||
| c600d45e84 | |||
| e9fd6ab470 | |||
| 3d507600cb | |||
| 84abe044a3 | |||
| 04200bb590 | |||
| 42d49b7cba | |||
| 5dace4fd74 | |||
| ccdae6bb9a | |||
| 984956ce95 | |||
| 0fd9b2a8f6 | |||
| 39f4949189 | |||
| f7d52e0372 | |||
| 6cad8411fe | |||
| f35abccfd9 | |||
| f3573d16b4 | |||
| e6f288e2c9 | |||
| 833bd6e655 | |||
| 4a30c68cfc | |||
| 346bd5f57a | |||
| c2e3b4d35a | |||
| 1fdb03f7db | |||
| da3681e602 | |||
| d64a8907eb | |||
| 7e91ae02f1 | |||
| 9457b832fc | |||
| d0561705fe | |||
| 3601968342 | |||
| fa2cde79ba | |||
| 1827fe0ce1 | |||
| 3447e0c237 | |||
| 4f9ae9cc75 | |||
| cd1c6cbc89 | |||
| 66cd4c9b40 | |||
| 2e1cf49d99 | |||
| 0c150694e7 | |||
| a4c10394b6 | |||
| f78836dac4 | |||
| c88de1ab1b | |||
| 9694c8310c | |||
| 1b09eecfce | |||
| 853e8faec5 | |||
| cfd2d43f1c | |||
| 1d3542b648 | |||
| 6dc7b9de92 | |||
| 48a63e26f3 | |||
| 33b1c93949 | |||
| 7a115d8080 | |||
| 9be7c5e6e1 | |||
| e38d1dfdc4 | |||
| f1f993bf38 | |||
| 2845d8cc98 | |||
| 0185d5f7d6 | |||
| 079ca1d0b3 | |||
| 5a67d8169d | |||
| f1cb4c38a2 | |||
| 50a5ec45b3 | |||
| f76216c038 | |||
| d55692dc0d | |||
| ded8f15913 | |||
| 845dbbfa1e |
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
I acknowledge that:
|
I acknowledge that:
|
||||||
|
|
||||||
- I have updated to the latest version of the app (stable is v1.5.0)
|
- I have updated to the latest version of the app (stable is v1.6.0)
|
||||||
- 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.
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ labels: "bug"
|
|||||||
|
|
||||||
I acknowledge that:
|
I acknowledge that:
|
||||||
|
|
||||||
- I have updated to the latest version of the app (stable is v1.5.0)
|
- I have updated to the latest version of the app (stable is v1.6.0)
|
||||||
- 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.
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ labels: "feature"
|
|||||||
|
|
||||||
I acknowledge that:
|
I acknowledge that:
|
||||||
|
|
||||||
- I have updated to the latest version of the app (stable is v1.5.0)
|
- I have updated to the latest version of the app (stable is v1.6.0)
|
||||||
- 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
|
||||||
|
|
||||||
|
|||||||
@@ -32,10 +32,10 @@ jobs:
|
|||||||
- name: Clone repo
|
- name: Clone repo
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
- name: Set up JDK 11
|
- name: Set up JDK 1.8
|
||||||
uses: actions/setup-java@v1
|
uses: actions/setup-java@v1
|
||||||
with:
|
with:
|
||||||
java-version: 11
|
java-version: 1.8
|
||||||
|
|
||||||
- name: Copy CI gradle.properties
|
- name: Copy CI gradle.properties
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -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."
|
|
||||||
|
|||||||
@@ -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'
|
||||||
@@ -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 @@
|
|||||||
| Preview Builds | Release Builds | Tachiyomi Support Server |
|
| Preview Builds | Release Builds | Tachiyomi Support Server |
|
||||||
|-------|----------|----------|
|
|-------|----------|----------|
|
||||||
| [](https://github.com/jobobby04/TachiyomiSYPreview/releases) | [](https://github.com/jobobby04/tachiyomisy/releases/latest) | [](https://discord.gg/tachiyomi) |
|
| [](https://github.com/jobobby04/TachiyomiSYPreview/releases) | [](https://github.com/jobobby04/tachiyomisy/releases/latest) | [](https://discord.gg/tachiyomi) |
|
||||||
|
|
||||||
|
|
||||||
# TachiyomiSY
|
# TachiyomiSY
|
||||||
@@ -109,7 +109,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
|
||||||
|
|||||||
+25
-27
@@ -34,8 +34,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 = 13
|
versionCode = 14
|
||||||
versionName = "1.5.0"
|
versionName = "1.6.0"
|
||||||
|
|
||||||
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
||||||
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
|
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
|
||||||
@@ -95,6 +95,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")
|
||||||
|
|
||||||
// Compatibility for two RxJava versions (EXH)
|
// Compatibility for two RxJava versions (EXH)
|
||||||
exclude("META-INF/rxjava.properties")
|
exclude("META-INF/rxjava.properties")
|
||||||
@@ -126,20 +127,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")
|
||||||
@@ -150,7 +151,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")
|
||||||
@@ -159,7 +160,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")
|
||||||
@@ -193,7 +194,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"
|
||||||
@@ -204,14 +205,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")
|
||||||
@@ -229,7 +228,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"
|
||||||
@@ -242,7 +242,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"
|
||||||
@@ -256,7 +256,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")
|
||||||
|
|
||||||
@@ -285,11 +285,11 @@ dependencies {
|
|||||||
implementation ("info.debatty:java-string-similarity:2.0.0")
|
implementation ("info.debatty:java-string-similarity:2.0.0")
|
||||||
|
|
||||||
// Firebase (EH)
|
// Firebase (EH)
|
||||||
implementation("com.google.firebase:firebase-analytics-ktx:18.0.0")
|
implementation("com.google.firebase:firebase-analytics-ktx:18.0.3")
|
||||||
implementation("com.google.firebase:firebase-crashlytics-ktx:17.3.0")
|
implementation("com.google.firebase:firebase-crashlytics-ktx:17.4.1")
|
||||||
|
|
||||||
// Better logging (EH)
|
// Better logging (EH)
|
||||||
implementation("com.elvishew:xlog:1.7.1")
|
implementation("com.elvishew:xlog:1.9.0")
|
||||||
|
|
||||||
// Debug utils (EH)
|
// Debug utils (EH)
|
||||||
val debugOverlayVersion = "1.1.3"
|
val debugOverlayVersion = "1.1.3"
|
||||||
@@ -302,12 +302,10 @@ dependencies {
|
|||||||
implementation ("me.zhanghai.android.materialratingbar:library:1.4.0")
|
implementation ("me.zhanghai.android.materialratingbar:library:1.4.0")
|
||||||
|
|
||||||
// JsonReader for similar manga
|
// JsonReader for similar manga
|
||||||
implementation("com.squareup.moshi:moshi:1.11.0")
|
implementation("com.squareup.moshi:moshi:1.12.0")
|
||||||
|
|
||||||
implementation("androidx.gridlayout:gridlayout:1.0.0")
|
implementation("com.mikepenz:fastadapter:5.4.0")
|
||||||
|
// SY <--
|
||||||
implementation("com.mikepenz:fastadapter:5.3.4")
|
|
||||||
// SY -->
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks {
|
tasks {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||||
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||||
|
|
||||||
<!-- For managing extensions -->
|
<!-- For managing extensions -->
|
||||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||||
@@ -149,6 +150,10 @@
|
|||||||
android:name=".extension.util.ExtensionInstallActivity"
|
android:name=".extension.util.ExtensionInstallActivity"
|
||||||
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
|
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name="exh.ui.login.EhLoginActivity"
|
||||||
|
android:label="EHentaiLogin" />
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
android:name="androidx.core.content.FileProvider"
|
android:name="androidx.core.content.FileProvider"
|
||||||
android:authorities="${applicationId}.provider"
|
android:authorities="${applicationId}.provider"
|
||||||
@@ -315,7 +320,7 @@
|
|||||||
android:scheme="https" />
|
android:scheme="https" />
|
||||||
|
|
||||||
<!-- MangaDex -->
|
<!-- MangaDex -->
|
||||||
<data
|
<!--<data
|
||||||
android:scheme="https"
|
android:scheme="https"
|
||||||
android:host="www.mangadex.org"
|
android:host="www.mangadex.org"
|
||||||
android:pathPrefix="/manga/" />
|
android:pathPrefix="/manga/" />
|
||||||
@@ -364,7 +369,7 @@
|
|||||||
<data
|
<data
|
||||||
android:scheme="https"
|
android:scheme="https"
|
||||||
android:host="www.mangadex.cc"
|
android:host="www.mangadex.cc"
|
||||||
android:pathPrefix="/chapter/" />
|
android:pathPrefix="/chapter/" />-->
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import com.elvishew.xlog.printer.file.naming.DateFileNameGenerator
|
|||||||
import com.google.android.gms.common.GooglePlayServicesNotAvailableException
|
import com.google.android.gms.common.GooglePlayServicesNotAvailableException
|
||||||
import com.google.android.gms.common.GooglePlayServicesRepairableException
|
import com.google.android.gms.common.GooglePlayServicesRepairableException
|
||||||
import com.google.android.gms.security.ProviderInstaller
|
import com.google.android.gms.security.ProviderInstaller
|
||||||
import com.google.firebase.analytics.FirebaseAnalytics
|
|
||||||
import com.google.firebase.analytics.ktx.analytics
|
import com.google.firebase.analytics.ktx.analytics
|
||||||
import com.google.firebase.ktx.Firebase
|
import com.google.firebase.ktx.Firebase
|
||||||
import com.ms_square.debugoverlay.DebugOverlay
|
import com.ms_square.debugoverlay.DebugOverlay
|
||||||
@@ -36,11 +35,11 @@ import exh.log.CrashlyticsPrinter
|
|||||||
import exh.log.EHDebugModeOverlay
|
import exh.log.EHDebugModeOverlay
|
||||||
import exh.log.EHLogLevel
|
import exh.log.EHLogLevel
|
||||||
import exh.log.EnhancedFilePrinter
|
import exh.log.EnhancedFilePrinter
|
||||||
|
import exh.log.XLogTree
|
||||||
|
import exh.log.xLogD
|
||||||
|
import exh.log.xLogE
|
||||||
import exh.syDebugVersion
|
import exh.syDebugVersion
|
||||||
import io.realm.Realm
|
import io.realm.Realm
|
||||||
import io.realm.RealmConfiguration
|
|
||||||
import kotlinx.coroutines.GlobalScope
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import org.conscrypt.Conscrypt
|
import org.conscrypt.Conscrypt
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
@@ -51,7 +50,6 @@ import java.security.Security
|
|||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import javax.net.ssl.SSLContext
|
import javax.net.ssl.SSLContext
|
||||||
import kotlin.concurrent.thread
|
|
||||||
import kotlin.time.ExperimentalTime
|
import kotlin.time.ExperimentalTime
|
||||||
import kotlin.time.days
|
import kotlin.time.days
|
||||||
|
|
||||||
@@ -59,12 +57,11 @@ open class App : Application(), LifecycleObserver {
|
|||||||
|
|
||||||
private val preferences: PreferencesHelper by injectLazy()
|
private val preferences: PreferencesHelper by injectLazy()
|
||||||
|
|
||||||
private lateinit var firebaseAnalytics: FirebaseAnalytics
|
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
|
// if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
|
||||||
setupExhLogging() // EXH logging
|
setupExhLogging() // EXH logging
|
||||||
|
Timber.plant(XLogTree()) // SY Redirect Timber to XLog
|
||||||
if (!BuildConfig.DEBUG) addAnalytics()
|
if (!BuildConfig.DEBUG) addAnalytics()
|
||||||
|
|
||||||
workaroundAndroid7BrokenSSL()
|
workaroundAndroid7BrokenSSL()
|
||||||
@@ -78,7 +75,6 @@ open class App : Application(), LifecycleObserver {
|
|||||||
|
|
||||||
setupNotificationChannels()
|
setupNotificationChannels()
|
||||||
Realm.init(this)
|
Realm.init(this)
|
||||||
GlobalScope.launch { deleteOldMetadataRealm() } // Delete old metadata DB (EH)
|
|
||||||
if ((BuildConfig.DEBUG || BuildConfig.BUILD_TYPE == "releaseTest") && DebugToggles.ENABLE_DEBUG_OVERLAY.enabled) {
|
if ((BuildConfig.DEBUG || BuildConfig.BUILD_TYPE == "releaseTest") && DebugToggles.ENABLE_DEBUG_OVERLAY.enabled) {
|
||||||
setupDebugOverlay()
|
setupDebugOverlay()
|
||||||
}
|
}
|
||||||
@@ -105,23 +101,22 @@ open class App : Application(), LifecycleObserver {
|
|||||||
try {
|
try {
|
||||||
SSLContext.getInstance("TLSv1.2")
|
SSLContext.getInstance("TLSv1.2")
|
||||||
} catch (e: NoSuchAlgorithmException) {
|
} catch (e: NoSuchAlgorithmException) {
|
||||||
XLog.tag("Init").e("Could not install Android 7 broken SSL workaround!", e)
|
xLogE("Could not install Android 7 broken SSL workaround!", e)
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
ProviderInstaller.installIfNeeded(applicationContext)
|
ProviderInstaller.installIfNeeded(applicationContext)
|
||||||
} catch (e: GooglePlayServicesRepairableException) {
|
} catch (e: GooglePlayServicesRepairableException) {
|
||||||
XLog.tag("Init").e("Could not install Android 7 broken SSL workaround!", e)
|
xLogE("Could not install Android 7 broken SSL workaround!", e)
|
||||||
} catch (e: GooglePlayServicesNotAvailableException) {
|
} catch (e: GooglePlayServicesNotAvailableException) {
|
||||||
XLog.tag("Init").e("Could not install Android 7 broken SSL workaround!", e)
|
xLogE("Could not install Android 7 broken SSL workaround!", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun addAnalytics() {
|
private fun addAnalytics() {
|
||||||
firebaseAnalytics = Firebase.analytics
|
|
||||||
if (syDebugVersion != "0") {
|
if (syDebugVersion != "0") {
|
||||||
firebaseAnalytics.setUserProperty("preview_version", syDebugVersion)
|
Firebase.analytics.setUserProperty("preview_version", syDebugVersion)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,36 +132,13 @@ open class App : Application(), LifecycleObserver {
|
|||||||
Notifications.createChannels(this)
|
Notifications.createChannels(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
// EXH
|
|
||||||
private fun deleteOldMetadataRealm() {
|
|
||||||
val config = RealmConfiguration.Builder()
|
|
||||||
.name("gallery-metadata.realm")
|
|
||||||
.schemaVersion(3)
|
|
||||||
.deleteRealmIfMigrationNeeded()
|
|
||||||
.build()
|
|
||||||
Realm.deleteRealm(config)
|
|
||||||
|
|
||||||
// Delete old paper db files
|
|
||||||
listOf(
|
|
||||||
File(filesDir, "gallery-ex"),
|
|
||||||
File(filesDir, "gallery-perveden"),
|
|
||||||
File(filesDir, "gallery-nhentai")
|
|
||||||
).forEach {
|
|
||||||
if (it.exists()) {
|
|
||||||
thread {
|
|
||||||
it.deleteRecursively()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// EXH
|
// EXH
|
||||||
private fun setupExhLogging() {
|
private fun setupExhLogging() {
|
||||||
EHLogLevel.init(this)
|
EHLogLevel.init(this)
|
||||||
|
|
||||||
val logLevel = when {
|
val logLevel = when {
|
||||||
EHLogLevel.shouldLog(EHLogLevel.EXTRA) -> LogLevel.ALL
|
EHLogLevel.shouldLog(EHLogLevel.EXTREME) -> LogLevel.ALL
|
||||||
BuildConfig.DEBUG -> LogLevel.DEBUG
|
EHLogLevel.shouldLog(EHLogLevel.EXTRA) || BuildConfig.DEBUG -> LogLevel.DEBUG
|
||||||
else -> LogLevel.WARN
|
else -> LogLevel.WARN
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,9 +160,8 @@ open class App : Application(), LifecycleObserver {
|
|||||||
|
|
||||||
@OptIn(ExperimentalTime::class)
|
@OptIn(ExperimentalTime::class)
|
||||||
printers += EnhancedFilePrinter
|
printers += EnhancedFilePrinter
|
||||||
.Builder(logFolder.absolutePath)
|
.Builder(logFolder.absolutePath) {
|
||||||
.fileNameGenerator(
|
fileNameGenerator = object : DateFileNameGenerator() {
|
||||||
object : DateFileNameGenerator() {
|
|
||||||
override fun generateFileName(logLevel: Int, timestamp: Long): String {
|
override fun generateFileName(logLevel: Int, timestamp: Long): String {
|
||||||
return super.generateFileName(
|
return super.generateFileName(
|
||||||
logLevel,
|
logLevel,
|
||||||
@@ -198,13 +169,12 @@ open class App : Application(), LifecycleObserver {
|
|||||||
) + "-${BuildConfig.BUILD_TYPE}.log"
|
) + "-${BuildConfig.BUILD_TYPE}.log"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
flattener { timeMillis, level, tag, message ->
|
||||||
.flattener { timeMillis, level, tag, message ->
|
"${dateFormat.format(timeMillis)} ${LogLevel.getShortLevelName(level)}/$tag: $message"
|
||||||
"${dateFormat.format(timeMillis)} ${LogLevel.getShortLevelName(level)}/$tag: $message"
|
}
|
||||||
|
cleanStrategy = FileLastModifiedCleanStrategy(7.days.toLongMilliseconds())
|
||||||
|
backupStrategy = NeverBackupStrategy()
|
||||||
}
|
}
|
||||||
.cleanStrategy(FileLastModifiedCleanStrategy(7.days.toLongMilliseconds()))
|
|
||||||
.backupStrategy(NeverBackupStrategy())
|
|
||||||
.build()
|
|
||||||
|
|
||||||
// Install Crashlytics in prod
|
// Install Crashlytics in prod
|
||||||
if (!BuildConfig.DEBUG) {
|
if (!BuildConfig.DEBUG) {
|
||||||
@@ -216,8 +186,8 @@ open class App : Application(), LifecycleObserver {
|
|||||||
*printers.toTypedArray()
|
*printers.toTypedArray()
|
||||||
)
|
)
|
||||||
|
|
||||||
XLog.tag("Init").d("Application booting...")
|
xLogD("Application booting...")
|
||||||
XLog.tag("Init").disableStackTrace().d(
|
xLogD(
|
||||||
"App version: ${BuildConfig.VERSION_NAME} (${BuildConfig.FLAVOR}, ${BuildConfig.COMMIT_SHA}, ${BuildConfig.VERSION_CODE})\n" +
|
"App version: ${BuildConfig.VERSION_NAME} (${BuildConfig.FLAVOR}, ${BuildConfig.COMMIT_SHA}, ${BuildConfig.VERSION_CODE})\n" +
|
||||||
"Preview build: $syDebugVersion\n" +
|
"Preview build: $syDebugVersion\n" +
|
||||||
"Android version: ${Build.VERSION.RELEASE} (SDK ${Build.VERSION.SDK_INT}) \n" +
|
"Android version: ${Build.VERSION.RELEASE} (SDK ${Build.VERSION.SDK_INT}) \n" +
|
||||||
@@ -242,7 +212,7 @@ open class App : Application(), LifecycleObserver {
|
|||||||
.install()
|
.install()
|
||||||
} catch (e: IllegalStateException) {
|
} catch (e: IllegalStateException) {
|
||||||
// Crashes if app is in background
|
// Crashes if app is in background
|
||||||
XLog.tag("Init").e("Failed to initialize debug overlay, app in background?", e)
|
xLogE("Failed to initialize debug overlay, app in background?", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -129,6 +130,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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
|||||||
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.toMangaInfo
|
import eu.kanade.tachiyomi.data.database.models.toMangaInfo
|
||||||
|
import eu.kanade.tachiyomi.data.library.CustomMangaManager
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
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.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
@@ -23,6 +24,10 @@ abstract class AbstractBackupManager(protected val context: Context) {
|
|||||||
internal val trackManager: TrackManager by injectLazy()
|
internal val trackManager: TrackManager by injectLazy()
|
||||||
protected val preferences: PreferencesHelper by injectLazy()
|
protected val preferences: PreferencesHelper by injectLazy()
|
||||||
|
|
||||||
|
// SY -->
|
||||||
|
protected val customMangaManager: CustomMangaManager by injectLazy()
|
||||||
|
// SY <--
|
||||||
|
|
||||||
abstract fun createBackup(uri: Uri, flags: Int, isJob: Boolean): String?
|
abstract fun createBackup(uri: Uri, flags: Int, isJob: Boolean): String?
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
|||||||
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.data.library.CustomMangaManager
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.util.chapter.NoChaptersException
|
import eu.kanade.tachiyomi.util.chapter.NoChaptersException
|
||||||
@@ -24,6 +25,10 @@ abstract class AbstractBackupRestore<T : AbstractBackupManager>(protected val co
|
|||||||
protected val db: DatabaseHelper by injectLazy()
|
protected val db: DatabaseHelper by injectLazy()
|
||||||
protected val trackManager: TrackManager by injectLazy()
|
protected val trackManager: TrackManager by injectLazy()
|
||||||
|
|
||||||
|
// SY -->
|
||||||
|
protected val customMangaManager: CustomMangaManager by injectLazy()
|
||||||
|
// SY <--
|
||||||
|
|
||||||
var job: Job? = null
|
var job: Job? = null
|
||||||
|
|
||||||
protected lateinit var backupManager: T
|
protected lateinit var backupManager: T
|
||||||
|
|||||||
@@ -30,7 +30,12 @@ class BackupCreateService : Service() {
|
|||||||
internal const val BACKUP_HISTORY_MASK = 0x4
|
internal const val BACKUP_HISTORY_MASK = 0x4
|
||||||
internal const val BACKUP_TRACK = 0x8
|
internal const val BACKUP_TRACK = 0x8
|
||||||
internal const val BACKUP_TRACK_MASK = 0x8
|
internal const val BACKUP_TRACK_MASK = 0x8
|
||||||
internal const val BACKUP_ALL = 0xF
|
|
||||||
|
// SY -->
|
||||||
|
internal const val BACKUP_CUSTOM_INFO = 0x10
|
||||||
|
internal const val BACKUP_CUSTOM_INFO_MASK = 0x10
|
||||||
|
internal const val BACKUP_ALL = 0x1F
|
||||||
|
// SY <--
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the status of the service.
|
* Returns the status of the service.
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,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)
|
||||||
}
|
}
|
||||||
@@ -119,13 +118,12 @@ 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CATE
|
|||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CATEGORY_MASK
|
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CATEGORY_MASK
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CHAPTER
|
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CHAPTER
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CHAPTER_MASK
|
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CHAPTER_MASK
|
||||||
|
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CUSTOM_INFO
|
||||||
|
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CUSTOM_INFO_MASK
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_HISTORY
|
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_HISTORY
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_HISTORY_MASK
|
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_HISTORY_MASK
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_TRACK
|
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_TRACK
|
||||||
@@ -29,11 +31,8 @@ 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 eu.kanade.tachiyomi.source.online.MetadataSource
|
import eu.kanade.tachiyomi.source.online.MetadataSource
|
||||||
import eu.kanade.tachiyomi.source.online.all.MergedSource
|
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||||
import exh.metadata.metadata.base.getFlatMetadataForManga
|
import exh.metadata.metadata.base.getFlatMetadataForManga
|
||||||
import exh.metadata.metadata.base.insertFlatMetadataAsync
|
import exh.metadata.metadata.base.insertFlatMetadataAsync
|
||||||
import exh.savedsearches.JsonSavedSearch
|
import exh.savedsearches.JsonSavedSearch
|
||||||
@@ -164,7 +163,7 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
|||||||
*/
|
*/
|
||||||
private fun backupMangaObject(manga: Manga, options: Int): BackupManga {
|
private fun backupMangaObject(manga: Manga, options: Int): BackupManga {
|
||||||
// Entry for this manga
|
// Entry for this manga
|
||||||
val mangaObject = BackupManga.copyFrom(manga)
|
val mangaObject = BackupManga.copyFrom(manga /* SY --> */, if (options and BACKUP_CUSTOM_INFO_MASK == BACKUP_CUSTOM_INFO) customMangaManager else null /* SY <-- */)
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
if (manga.source == MERGED_SOURCE_ID) {
|
if (manga.source == MERGED_SOURCE_ID) {
|
||||||
@@ -237,24 +236,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 /* SY --> */ && source !is MergedSource /* SY <-- */) {
|
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -363,29 +351,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
|
||||||
@@ -394,47 +379,7 @@ 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()
|
|
||||||
|
|
||||||
// Return if fetch is needed
|
|
||||||
if (dbChapters.isEmpty() || dbChapters.size < chapters.size) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
chapters.forEach { chapter ->
|
|
||||||
val dbChapter = dbChapters.find { it.url == chapter.url }
|
|
||||||
if (dbChapter != null) {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
chapter.manga_id = manga.id
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter the chapters that couldn't be found.
|
|
||||||
updateChapters(chapters.filter { it.id != null })
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
internal fun restoreChaptersForMangaOffline(manga: Manga, chapters: List<Chapter>) {
|
|
||||||
val dbChapters = databaseHelper.getChapters(manga).executeAsBlocking()
|
val dbChapters = databaseHelper.getChapters(manga).executeAsBlocking()
|
||||||
|
|
||||||
chapters.forEach { chapter ->
|
chapters.forEach { chapter ->
|
||||||
@@ -527,8 +472,9 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal suspend fun restoreFlatMetadata(manga: Manga, backupFlatMetadata: BackupFlatMetadata) {
|
internal fun restoreFlatMetadata(manga: Manga, backupFlatMetadata: BackupFlatMetadata) {
|
||||||
manga.id?.let { mangaId ->
|
val mangaId = manga.id ?: return
|
||||||
|
launchIO {
|
||||||
databaseHelper.getFlatMetadataForManga(mangaId).executeOnIO().let {
|
databaseHelper.getFlatMetadataForManga(mangaId).executeOnIO().let {
|
||||||
if (it == null) {
|
if (it == null) {
|
||||||
val flatMetadata = backupFlatMetadata.getFlatMetadata(mangaId)
|
val flatMetadata = backupFlatMetadata.getFlatMetadata(mangaId)
|
||||||
|
|||||||
@@ -15,8 +15,7 @@ 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.data.library.CustomMangaManager
|
||||||
import eu.kanade.tachiyomi.source.online.all.MergedSource
|
|
||||||
import exh.EXHMigrations
|
import exh.EXHMigrations
|
||||||
import exh.source.MERGED_SOURCE_ID
|
import exh.source.MERGED_SOURCE_ID
|
||||||
import okio.buffer
|
import okio.buffer
|
||||||
@@ -24,7 +23,7 @@ import okio.gzip
|
|||||||
import okio.source
|
import okio.source
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
|
||||||
class FullBackupRestore(context: Context, notifier: BackupNotifier, private val online: Boolean) : AbstractBackupRestore<FullBackupManager>(context, notifier) {
|
class FullBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBackupRestore<FullBackupManager>(context, notifier) {
|
||||||
|
|
||||||
override suspend fun performRestore(uri: Uri): Boolean {
|
override suspend fun performRestore(uri: Uri): Boolean {
|
||||||
// SY -->
|
// SY -->
|
||||||
@@ -57,9 +56,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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,8 +82,8 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier, private val
|
|||||||
}
|
}
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
private suspend fun restoreManga(backupManga: BackupManga, backupCategories: List<BackupCategory>, online: Boolean) {
|
private fun restoreManga(backupManga: BackupManga, backupCategories: List<BackupCategory>) {
|
||||||
var 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
|
||||||
@@ -90,22 +91,17 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier, private val
|
|||||||
// SY -->
|
// SY -->
|
||||||
val mergedMangaReferences = backupManga.mergedMangaReferences
|
val mergedMangaReferences = backupManga.mergedMangaReferences
|
||||||
val flatMetadata = backupManga.flatMetadata
|
val flatMetadata = backupManga.flatMetadata
|
||||||
|
val customManga = backupManga.getCustomMangaInfo()
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
manga = EXHMigrations.migrateBackupEntry(manga)
|
EXHMigrations.migrateBackupEntry(manga)
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
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/* SY --> */, mergedMangaReferences, flatMetadata, customManga/* SY <-- */)
|
||||||
restoreMangaData(manga, source, chapters, categories, history, tracks, backupCategories, mergedMangaReferences, flatMetadata, 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}")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,35 +113,35 @@ 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
|
||||||
* @param tracks tracking data from json
|
* @param tracks tracking data from json
|
||||||
*/
|
*/
|
||||||
private suspend 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>,
|
||||||
|
// SY -->
|
||||||
mergedMangaReferences: List<BackupMergedMangaReference>,
|
mergedMangaReferences: List<BackupMergedMangaReference>,
|
||||||
flatMetadata: BackupFlatMetadata?,
|
flatMetadata: BackupFlatMetadata?,
|
||||||
online: Boolean
|
customManga: CustomMangaManager.MangaJson?,
|
||||||
|
// SY -->
|
||||||
) {
|
) {
|
||||||
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, mergedMangaReferences, flatMetadata, online)
|
restoreMangaFetch(manga, chapters, categories, history, tracks, backupCategories/* SY --> */, mergedMangaReferences, flatMetadata, customManga/* SY <-- */)
|
||||||
} 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, mergedMangaReferences, flatMetadata, online)
|
restoreMangaNoFetch(manga, chapters, categories, history, tracks, backupCategories/* SY --> */, mergedMangaReferences, flatMetadata, customManga/* SY <-- */)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -157,66 +153,60 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier, private val
|
|||||||
* @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 suspend 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>,
|
||||||
|
// SY -->
|
||||||
mergedMangaReferences: List<BackupMergedMangaReference>,
|
mergedMangaReferences: List<BackupMergedMangaReference>,
|
||||||
flatMetadata: BackupFlatMetadata?,
|
flatMetadata: BackupFlatMetadata?,
|
||||||
online: Boolean
|
customManga: CustomMangaManager.MangaJson?,
|
||||||
|
// SY <--
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
val fetchedManga = backupManager.restoreMangaFetch(source, manga, online)
|
val fetchedManga = backupManager.restoreManga(manga)
|
||||||
fetchedManga.id ?: return
|
fetchedManga.id ?: return
|
||||||
|
backupManager.restoreChaptersForManga(fetchedManga, chapters)
|
||||||
|
|
||||||
if (online && source != null) {
|
restoreExtraForManga(fetchedManga, categories, history, tracks, backupCategories /* SY --> */, mergedMangaReferences, flatMetadata, customManga/* SY <-- */)
|
||||||
// SY -->
|
|
||||||
if (source !is MergedSource) {
|
|
||||||
updateChapters(source, fetchedManga, chapters)
|
|
||||||
}
|
|
||||||
// SY <--
|
|
||||||
} else {
|
|
||||||
backupManager.restoreChaptersForMangaOffline(fetchedManga, chapters)
|
|
||||||
}
|
|
||||||
|
|
||||||
restoreExtraForManga(fetchedManga, categories, history, tracks, backupCategories, mergedMangaReferences, flatMetadata)
|
|
||||||
|
|
||||||
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 suspend 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>,
|
||||||
|
// SY -->
|
||||||
mergedMangaReferences: List<BackupMergedMangaReference>,
|
mergedMangaReferences: List<BackupMergedMangaReference>,
|
||||||
flatMetadata: BackupFlatMetadata?,
|
flatMetadata: BackupFlatMetadata?,
|
||||||
online: Boolean
|
customManga: CustomMangaManager.MangaJson?,
|
||||||
|
// SY <--
|
||||||
) {
|
) {
|
||||||
if (online && source != null) {
|
backupManager.restoreChaptersForManga(backupManga, chapters)
|
||||||
if (/* SY --> */ source !is MergedSource && /* SY <-- */ !backupManager.restoreChaptersForManga(backupManga, chapters)) {
|
|
||||||
updateChapters(source, backupManga, chapters)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
backupManager.restoreChaptersForMangaOffline(backupManga, chapters)
|
|
||||||
}
|
|
||||||
|
|
||||||
restoreExtraForManga(backupManga, categories, history, tracks, backupCategories, mergedMangaReferences, flatMetadata)
|
restoreExtraForManga(backupManga, categories, history, tracks, backupCategories/* SY --> */, mergedMangaReferences, flatMetadata, customManga/* SY <-- */)
|
||||||
|
|
||||||
updateTracking(backupManga, tracks)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun restoreExtraForManga(manga: Manga, categories: List<Int>, history: List<BackupHistory>, tracks: List<Track>, backupCategories: List<BackupCategory>, mergedMangaReferences: List<BackupMergedMangaReference>, flatMetadata: BackupFlatMetadata?) {
|
private fun restoreExtraForManga(
|
||||||
|
manga: Manga,
|
||||||
|
categories: List<Int>,
|
||||||
|
history: List<BackupHistory>,
|
||||||
|
tracks: List<Track>,
|
||||||
|
backupCategories: List<BackupCategory>,
|
||||||
|
// SY -->
|
||||||
|
mergedMangaReferences: List<BackupMergedMangaReference>,
|
||||||
|
flatMetadata: BackupFlatMetadata?,
|
||||||
|
customManga: CustomMangaManager.MangaJson?,
|
||||||
|
// SY <--
|
||||||
|
) {
|
||||||
// Restore categories
|
// Restore categories
|
||||||
backupManager.restoreCategoriesForManga(manga, categories, backupCategories)
|
backupManager.restoreCategoriesForManga(manga, categories, backupCategories)
|
||||||
|
|
||||||
@@ -232,6 +222,10 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier, private val
|
|||||||
|
|
||||||
// Restore flat metadata for metadata sources
|
// Restore flat metadata for metadata sources
|
||||||
flatMetadata?.let { backupManager.restoreFlatMetadata(manga, it) }
|
flatMetadata?.let { backupManager.restoreFlatMetadata(manga, it) }
|
||||||
|
|
||||||
|
// Restore Custom Info
|
||||||
|
customManga?.id = manga.id!!
|
||||||
|
customManga?.let { customMangaManager.saveMangaInfo(it) }
|
||||||
// SY <--
|
// SY <--
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import eu.kanade.tachiyomi.data.database.models.ChapterImpl
|
|||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaImpl
|
import eu.kanade.tachiyomi.data.database.models.MangaImpl
|
||||||
import eu.kanade.tachiyomi.data.database.models.TrackImpl
|
import eu.kanade.tachiyomi.data.database.models.TrackImpl
|
||||||
|
import eu.kanade.tachiyomi.data.library.CustomMangaManager
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.protobuf.ProtoNumber
|
import kotlinx.serialization.protobuf.ProtoNumber
|
||||||
|
|
||||||
@@ -36,7 +37,15 @@ data class BackupManga(
|
|||||||
@ProtoNumber(102) var history: List<BackupHistory> = emptyList(),
|
@ProtoNumber(102) var history: List<BackupHistory> = emptyList(),
|
||||||
// SY specific values
|
// SY specific values
|
||||||
@ProtoNumber(600) var mergedMangaReferences: List<BackupMergedMangaReference> = emptyList(),
|
@ProtoNumber(600) var mergedMangaReferences: List<BackupMergedMangaReference> = emptyList(),
|
||||||
@ProtoNumber(601) var flatMetadata: BackupFlatMetadata? = null
|
@ProtoNumber(601) var flatMetadata: BackupFlatMetadata? = null,
|
||||||
|
@ProtoNumber(602) var customStatus: Int = 0,
|
||||||
|
|
||||||
|
// J2K specific values
|
||||||
|
@ProtoNumber(800) var customTitle: String? = null,
|
||||||
|
@ProtoNumber(801) var customArtist: String? = null,
|
||||||
|
@ProtoNumber(802) var customAuthor: String? = null,
|
||||||
|
@ProtoNumber(803) var customDescription: String? = null,
|
||||||
|
@ProtoNumber(803) var customGenre: List<String>? = null
|
||||||
) {
|
) {
|
||||||
fun getMangaImpl(): MangaImpl {
|
fun getMangaImpl(): MangaImpl {
|
||||||
return MangaImpl().apply {
|
return MangaImpl().apply {
|
||||||
@@ -62,6 +71,29 @@ data class BackupManga(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SY -->
|
||||||
|
fun getCustomMangaInfo(): CustomMangaManager.MangaJson? {
|
||||||
|
if (customTitle != null ||
|
||||||
|
customArtist != null ||
|
||||||
|
customAuthor != null ||
|
||||||
|
customDescription != null ||
|
||||||
|
customGenre != null ||
|
||||||
|
customStatus != 0
|
||||||
|
) {
|
||||||
|
return CustomMangaManager.MangaJson(
|
||||||
|
id = 0L,
|
||||||
|
title = customTitle,
|
||||||
|
author = customAuthor,
|
||||||
|
artist = customArtist,
|
||||||
|
description = customDescription,
|
||||||
|
genre = customGenre,
|
||||||
|
status = customStatus.takeUnless { it == 0 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
// SY <--
|
||||||
|
|
||||||
fun getTrackingImpl(): List<TrackImpl> {
|
fun getTrackingImpl(): List<TrackImpl> {
|
||||||
return tracking.map {
|
return tracking.map {
|
||||||
it.getTrackingImpl()
|
it.getTrackingImpl()
|
||||||
@@ -69,22 +101,35 @@ data class BackupManga(
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun copyFrom(manga: Manga): BackupManga {
|
fun copyFrom(manga: Manga /* SY --> */, customMangaManager: CustomMangaManager?/* SY <-- */): BackupManga {
|
||||||
return BackupManga(
|
return BackupManga(
|
||||||
url = manga.url,
|
url = manga.url,
|
||||||
title = manga.title,
|
// SY -->
|
||||||
artist = manga.artist,
|
title = manga.originalTitle,
|
||||||
author = manga.author,
|
artist = manga.originalArtist,
|
||||||
description = manga.description,
|
author = manga.originalAuthor,
|
||||||
genre = manga.getGenres() ?: emptyList(),
|
description = manga.originalDescription,
|
||||||
status = manga.status,
|
genre = manga.getOriginalGenres() ?: emptyList(),
|
||||||
|
status = manga.originalStatus,
|
||||||
|
// SY <--
|
||||||
thumbnailUrl = manga.thumbnail_url,
|
thumbnailUrl = manga.thumbnail_url,
|
||||||
favorite = manga.favorite,
|
favorite = manga.favorite,
|
||||||
source = manga.source,
|
source = manga.source,
|
||||||
dateAdded = manga.date_added,
|
dateAdded = manga.date_added,
|
||||||
viewer = manga.viewer,
|
viewer = manga.viewer,
|
||||||
chapterFlags = manga.chapter_flags
|
chapterFlags = manga.chapter_flags
|
||||||
)
|
// SY -->
|
||||||
|
).also { backupManga ->
|
||||||
|
customMangaManager?.getManga(manga)?.let {
|
||||||
|
backupManga.customTitle = it.title
|
||||||
|
backupManga.customArtist = it.artist
|
||||||
|
backupManga.customAuthor = it.author
|
||||||
|
backupManga.customDescription = it.description
|
||||||
|
backupManga.customGenre = it.getGenres()
|
||||||
|
backupManga.customStatus = it.status
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// SY <--
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ class LegacyBackupRestore(context: Context, notifier: BackupNotifier) : Abstract
|
|||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
private suspend fun restoreManga(mangaJson: JsonObject) {
|
private suspend fun restoreManga(mangaJson: JsonObject) {
|
||||||
/* SY --> */ var /* SY <-- */ manga = backupManager.parser.fromJson<MangaImpl>(
|
val manga = backupManager.parser.fromJson<MangaImpl>(
|
||||||
mangaJson.get(
|
mangaJson.get(
|
||||||
Backup.MANGA
|
Backup.MANGA
|
||||||
)
|
)
|
||||||
@@ -114,7 +114,7 @@ class LegacyBackupRestore(context: Context, notifier: BackupNotifier) : Abstract
|
|||||||
)
|
)
|
||||||
|
|
||||||
// EXH -->
|
// EXH -->
|
||||||
manga = EXHMigrations.migrateBackupEntry(manga)
|
EXHMigrations.migrateBackupEntry(manga)
|
||||||
// <-- EXH
|
// <-- EXH
|
||||||
|
|
||||||
val source = backupManager.sourceManager.get(manga.source)
|
val source = backupManager.sourceManager.get(manga.source)
|
||||||
|
|||||||
@@ -59,8 +59,8 @@ class MangaPutResolver : DefaultPutResolver<Manga>() {
|
|||||||
COL_DESCRIPTION to obj.originalDescription,
|
COL_DESCRIPTION to obj.originalDescription,
|
||||||
COL_GENRE to obj.originalGenre,
|
COL_GENRE to obj.originalGenre,
|
||||||
COL_TITLE to obj.originalTitle,
|
COL_TITLE to obj.originalTitle,
|
||||||
|
COL_STATUS to obj.originalStatus,
|
||||||
// SY <--
|
// SY <--
|
||||||
COL_STATUS to obj.status,
|
|
||||||
COL_THUMBNAIL_URL to obj.thumbnail_url,
|
COL_THUMBNAIL_URL to obj.thumbnail_url,
|
||||||
COL_FAVORITE to obj.favorite,
|
COL_FAVORITE to obj.favorite,
|
||||||
COL_LAST_UPDATE to obj.last_update,
|
COL_LAST_UPDATE to obj.last_update,
|
||||||
|
|||||||
@@ -40,9 +40,11 @@ open class MangaImpl : Manga {
|
|||||||
override var genre: String?
|
override var genre: String?
|
||||||
get() = if (favorite) customMangaManager.getManga(this)?.genre ?: ogGenre else ogGenre
|
get() = if (favorite) customMangaManager.getManga(this)?.genre ?: ogGenre else ogGenre
|
||||||
set(value) { ogGenre = value }
|
set(value) { ogGenre = value }
|
||||||
// SY <--
|
|
||||||
|
|
||||||
override var status: Int = 0
|
override var status: Int
|
||||||
|
get() = if (favorite) customMangaManager.getManga(this)?.status?.takeUnless { it == 0 } ?: ogStatus else ogStatus
|
||||||
|
set(value) { ogStatus = value }
|
||||||
|
// SY <--
|
||||||
|
|
||||||
override var thumbnail_url: String? = null
|
override var thumbnail_url: String? = null
|
||||||
|
|
||||||
@@ -71,6 +73,8 @@ open class MangaImpl : Manga {
|
|||||||
private set
|
private set
|
||||||
var ogGenre: String? = null
|
var ogGenre: String? = null
|
||||||
private set
|
private set
|
||||||
|
var ogStatus: Int = 0
|
||||||
|
private set
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import eu.kanade.tachiyomi.data.database.resolvers.MangaFlagsPutResolver
|
|||||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaInfoPutResolver
|
import eu.kanade.tachiyomi.data.database.resolvers.MangaInfoPutResolver
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaLastUpdatedPutResolver
|
import eu.kanade.tachiyomi.data.database.resolvers.MangaLastUpdatedPutResolver
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaMigrationPutResolver
|
import eu.kanade.tachiyomi.data.database.resolvers.MangaMigrationPutResolver
|
||||||
|
import eu.kanade.tachiyomi.data.database.resolvers.MangaThumbnailPutResolver
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaTitlePutResolver
|
import eu.kanade.tachiyomi.data.database.resolvers.MangaTitlePutResolver
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaViewerPutResolver
|
import eu.kanade.tachiyomi.data.database.resolvers.MangaViewerPutResolver
|
||||||
import eu.kanade.tachiyomi.data.database.tables.CategoryTable
|
import eu.kanade.tachiyomi.data.database.tables.CategoryTable
|
||||||
@@ -102,6 +103,11 @@ interface MangaQueries : DbProvider {
|
|||||||
.`object`(manga)
|
.`object`(manga)
|
||||||
.withPutResolver(MangaMigrationPutResolver())
|
.withPutResolver(MangaMigrationPutResolver())
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
|
fun updateMangaThumbnail(manga: Manga) = db.put()
|
||||||
|
.`object`(manga)
|
||||||
|
.withPutResolver(MangaThumbnailPutResolver())
|
||||||
|
.prepare()
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
fun insertManga(manga: Manga) = db.put().`object`(manga).prepare()
|
fun insertManga(manga: Manga) = db.put().`object`(manga).prepare()
|
||||||
@@ -151,12 +157,38 @@ interface MangaQueries : DbProvider {
|
|||||||
.byQuery(
|
.byQuery(
|
||||||
DeleteQuery.builder()
|
DeleteQuery.builder()
|
||||||
.table(MangaTable.TABLE)
|
.table(MangaTable.TABLE)
|
||||||
.where("${MangaTable.COL_FAVORITE} = ? AND ${MangaTable.COL_ID} NOT IN (SELECT ${MergedTable.COL_MANGA_ID} FROM ${MergedTable.TABLE})")
|
.where(
|
||||||
|
"""
|
||||||
|
${MangaTable.COL_FAVORITE} = ? AND ${MangaTable.COL_ID} NOT IN (
|
||||||
|
SELECT ${MergedTable.COL_MANGA_ID} FROM ${MergedTable.TABLE} WHERE ${MergedTable.COL_MANGA_ID} != ${MergedTable.COL_MERGE_ID}
|
||||||
|
)
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
.whereArgs(0)
|
.whereArgs(0)
|
||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
|
// SY -->
|
||||||
|
fun deleteMangasNotInLibraryAndNotRead() = db.delete()
|
||||||
|
.byQuery(
|
||||||
|
DeleteQuery.builder()
|
||||||
|
.table(MangaTable.TABLE)
|
||||||
|
.where(
|
||||||
|
"""
|
||||||
|
${MangaTable.COL_FAVORITE} = ? AND ${MangaTable.COL_ID} NOT IN (
|
||||||
|
SELECT ${MergedTable.COL_MANGA_ID} FROM ${MergedTable.TABLE} WHERE ${MergedTable.COL_MANGA_ID} != ${MergedTable.COL_MERGE_ID}
|
||||||
|
) AND ${MangaTable.COL_ID} NOT IN (
|
||||||
|
SELECT ${ChapterTable.COL_MANGA_ID} FROM ${ChapterTable.TABLE} WHERE ${ChapterTable.COL_READ} = 1 OR ${ChapterTable.COL_LAST_PAGE_READ} != 0
|
||||||
|
)
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
.whereArgs(0)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
.prepare()
|
||||||
|
// SY <--
|
||||||
|
|
||||||
fun deleteMangas() = db.delete()
|
fun deleteMangas() = db.delete()
|
||||||
.byQuery(
|
.byQuery(
|
||||||
DeleteQuery.builder()
|
DeleteQuery.builder()
|
||||||
@@ -195,6 +227,16 @@ interface MangaQueries : DbProvider {
|
|||||||
)
|
)
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
|
fun getChapterFetchDateManga() = db.get()
|
||||||
|
.listOfObjects(Manga::class.java)
|
||||||
|
.withQuery(
|
||||||
|
RawQuery.builder()
|
||||||
|
.query(getChapterFetchDateMangaQuery())
|
||||||
|
.observesTables(MangaTable.TABLE)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
.prepare()
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
fun getMangaWithMetadata() = db.get()
|
fun getMangaWithMetadata() = db.get()
|
||||||
.listOfObjects(Manga::class.java)
|
.listOfObjects(Manga::class.java)
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ fun getReadMangaNotInLibraryQuery() =
|
|||||||
SELECT ${Manga.TABLE}.*
|
SELECT ${Manga.TABLE}.*
|
||||||
FROM ${Manga.TABLE}
|
FROM ${Manga.TABLE}
|
||||||
WHERE ${Manga.COL_FAVORITE} = 0 AND ${Manga.COL_ID} IN(
|
WHERE ${Manga.COL_FAVORITE} = 0 AND ${Manga.COL_ID} IN(
|
||||||
SELECT ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} FROM ${Chapter.TABLE} WHERE ${Chapter.COL_READ} = 1
|
SELECT ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} FROM ${Chapter.TABLE} WHERE ${Chapter.COL_READ} = 1 OR ${Chapter.COL_LAST_PAGE_READ} != 0
|
||||||
)
|
)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -221,6 +221,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.
|
||||||
*/
|
*/
|
||||||
|
|||||||
+14
-9
@@ -1,6 +1,5 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.resolvers
|
package eu.kanade.tachiyomi.data.database.resolvers
|
||||||
|
|
||||||
import android.content.ContentValues
|
|
||||||
import androidx.core.content.contentValuesOf
|
import androidx.core.content.contentValuesOf
|
||||||
import com.pushtorefresh.storio.sqlite.StorIOSQLite
|
import com.pushtorefresh.storio.sqlite.StorIOSQLite
|
||||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResolver
|
import com.pushtorefresh.storio.sqlite.operations.put.PutResolver
|
||||||
@@ -9,6 +8,7 @@ import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
|
|||||||
import eu.kanade.tachiyomi.data.database.inTransactionReturn
|
import eu.kanade.tachiyomi.data.database.inTransactionReturn
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
||||||
|
import exh.util.nullIfZero
|
||||||
|
|
||||||
class MangaInfoPutResolver(val reset: Boolean = false) : PutResolver<Manga>() {
|
class MangaInfoPutResolver(val reset: Boolean = false) : PutResolver<Manga>() {
|
||||||
|
|
||||||
@@ -31,15 +31,20 @@ class MangaInfoPutResolver(val reset: Boolean = false) : PutResolver<Manga>() {
|
|||||||
MangaTable.COL_GENRE to manga.originalGenre,
|
MangaTable.COL_GENRE to manga.originalGenre,
|
||||||
MangaTable.COL_AUTHOR to manga.originalAuthor,
|
MangaTable.COL_AUTHOR to manga.originalAuthor,
|
||||||
MangaTable.COL_ARTIST to manga.originalArtist,
|
MangaTable.COL_ARTIST to manga.originalArtist,
|
||||||
MangaTable.COL_DESCRIPTION to manga.originalDescription
|
MangaTable.COL_DESCRIPTION to manga.originalDescription,
|
||||||
|
MangaTable.COL_STATUS to manga.originalStatus
|
||||||
)
|
)
|
||||||
|
|
||||||
fun resetToContentValues(manga: Manga) = ContentValues(1).apply {
|
private fun resetToContentValues(manga: Manga) = contentValuesOf(
|
||||||
val splitter = "▒ ▒∩▒"
|
MangaTable.COL_TITLE to manga.title.split(splitter).last(),
|
||||||
put(MangaTable.COL_TITLE, manga.title.split(splitter).last())
|
MangaTable.COL_GENRE to manga.genre?.split(splitter)?.lastOrNull(),
|
||||||
put(MangaTable.COL_GENRE, manga.genre?.split(splitter)?.lastOrNull())
|
MangaTable.COL_AUTHOR to manga.author?.split(splitter)?.lastOrNull(),
|
||||||
put(MangaTable.COL_AUTHOR, manga.author?.split(splitter)?.lastOrNull())
|
MangaTable.COL_ARTIST to manga.artist?.split(splitter)?.lastOrNull(),
|
||||||
put(MangaTable.COL_ARTIST, manga.artist?.split(splitter)?.lastOrNull())
|
MangaTable.COL_DESCRIPTION to manga.description?.split(splitter)?.lastOrNull(),
|
||||||
put(MangaTable.COL_DESCRIPTION, manga.description?.split(splitter)?.lastOrNull())
|
MangaTable.COL_STATUS to manga.status.nullIfZero()?.toString()?.split(splitter)?.lastOrNull()
|
||||||
|
)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val splitter = "▒ ▒∩▒"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+32
@@ -0,0 +1,32 @@
|
|||||||
|
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.Manga
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
||||||
|
|
||||||
|
// SY
|
||||||
|
class MangaThumbnailPutResolver : PutResolver<Manga>() {
|
||||||
|
|
||||||
|
override fun performPut(db: StorIOSQLite, manga: Manga) = db.inTransactionReturn {
|
||||||
|
val updateQuery = mapToUpdateQuery(manga)
|
||||||
|
val contentValues = mapToContentValues(manga)
|
||||||
|
|
||||||
|
val numberOfRowsUpdated = db.lowLevel().update(updateQuery, contentValues)
|
||||||
|
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder()
|
||||||
|
.table(MangaTable.TABLE)
|
||||||
|
.where("${MangaTable.COL_ID} = ?")
|
||||||
|
.whereArgs(manga.id)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
fun mapToContentValues(manga: Manga) = contentValuesOf(
|
||||||
|
MangaTable.COL_THUMBNAIL_URL to manga.thumbnail_url
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -23,7 +24,7 @@ import uy.kohesive.injekt.injectLazy
|
|||||||
*
|
*
|
||||||
* @param context the application context.
|
* @param context the application context.
|
||||||
*/
|
*/
|
||||||
class DownloadManager(/* SY private */ val context: Context) {
|
class DownloadManager(private val context: Context) {
|
||||||
|
|
||||||
private val sourceManager: SourceManager by injectLazy()
|
private val sourceManager: SourceManager by injectLazy()
|
||||||
private val preferences: PreferencesHelper by injectLazy()
|
private val preferences: PreferencesHelper by injectLazy()
|
||||||
@@ -211,16 +212,16 @@ class DownloadManager(/* SY 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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -302,9 +303,11 @@ class DownloadManager(/* SY 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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import kotlinx.serialization.decodeFromString
|
|||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.Scanner
|
|
||||||
|
|
||||||
class CustomMangaManager(val context: Context) {
|
class CustomMangaManager(val context: Context) {
|
||||||
|
|
||||||
@@ -23,7 +22,7 @@ class CustomMangaManager(val context: Context) {
|
|||||||
|
|
||||||
val json = try {
|
val json = try {
|
||||||
Json.decodeFromString<MangaList>(
|
Json.decodeFromString<MangaList>(
|
||||||
Scanner(editJson).useDelimiter("\\Z").next()
|
editJson.bufferedReader().use { it.readText() }
|
||||||
)
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
null
|
null
|
||||||
@@ -32,30 +31,15 @@ class CustomMangaManager(val context: Context) {
|
|||||||
val mangasJson = json.mangas ?: return mutableMapOf()
|
val mangasJson = json.mangas ?: return mutableMapOf()
|
||||||
return mangasJson.mapNotNull { mangaJson ->
|
return mangasJson.mapNotNull { mangaJson ->
|
||||||
val id = mangaJson.id ?: return@mapNotNull null
|
val id = mangaJson.id ?: return@mapNotNull null
|
||||||
val manga = MangaImpl().apply {
|
id to mangaJson.toManga()
|
||||||
this.id = id
|
|
||||||
title = mangaJson.title ?: ""
|
|
||||||
author = mangaJson.author
|
|
||||||
artist = mangaJson.artist
|
|
||||||
description = mangaJson.description
|
|
||||||
genre = mangaJson.genre?.joinToString(", ")
|
|
||||||
}
|
|
||||||
id to manga
|
|
||||||
}.toMap().toMutableMap()
|
}.toMap().toMutableMap()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveMangaInfo(manga: MangaJson) {
|
fun saveMangaInfo(manga: MangaJson) {
|
||||||
if (manga.title == null && manga.author == null && manga.artist == null && manga.description == null && manga.genre == null) {
|
if (manga.title == null && manga.author == null && manga.artist == null && manga.description == null && manga.genre == null && manga.status == null) {
|
||||||
customMangaMap.remove(manga.id!!)
|
customMangaMap.remove(manga.id!!)
|
||||||
} else {
|
} else {
|
||||||
customMangaMap[manga.id!!] = MangaImpl().apply {
|
customMangaMap[manga.id!!] = manga.toManga()
|
||||||
id = manga.id
|
|
||||||
title = manga.title ?: ""
|
|
||||||
author = manga.author
|
|
||||||
artist = manga.artist
|
|
||||||
description = manga.description
|
|
||||||
genre = manga.genre?.joinToString(", ")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
saveCustomInfo()
|
saveCustomInfo()
|
||||||
}
|
}
|
||||||
@@ -75,7 +59,8 @@ class CustomMangaManager(val context: Context) {
|
|||||||
author,
|
author,
|
||||||
artist,
|
artist,
|
||||||
description,
|
description,
|
||||||
genre?.split(", ")
|
genre?.split(", "),
|
||||||
|
status
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,24 +71,23 @@ class CustomMangaManager(val context: Context) {
|
|||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class MangaJson(
|
data class MangaJson(
|
||||||
val id: Long? = null,
|
var id: Long? = null,
|
||||||
val title: String? = null,
|
val title: String? = null,
|
||||||
val author: String? = null,
|
val author: String? = null,
|
||||||
val artist: String? = null,
|
val artist: String? = null,
|
||||||
val description: String? = null,
|
val description: String? = null,
|
||||||
val genre: List<String>? = null
|
val genre: List<String>? = null,
|
||||||
|
val status: Int? = null
|
||||||
) {
|
) {
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
fun toManga() = MangaImpl().apply {
|
||||||
if (this === other) return true
|
id = this@MangaJson.id
|
||||||
if (javaClass != other?.javaClass) return false
|
title = this@MangaJson.title ?: ""
|
||||||
other as MangaJson
|
author = this@MangaJson.author
|
||||||
if (id != other.id) return false
|
artist = this@MangaJson.artist
|
||||||
return true
|
description = this@MangaJson.description
|
||||||
}
|
genre = this@MangaJson.genre?.joinToString(", ")
|
||||||
|
status = this@MangaJson.status ?: 0
|
||||||
override fun hashCode(): Int {
|
|
||||||
return id.hashCode()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -111,7 +111,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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ import eu.kanade.tachiyomi.util.system.createFileInCacheDir
|
|||||||
import eu.kanade.tachiyomi.util.system.isServiceRunning
|
import eu.kanade.tachiyomi.util.system.isServiceRunning
|
||||||
import exh.md.utils.FollowStatus
|
import exh.md.utils.FollowStatus
|
||||||
import exh.md.utils.MdUtil
|
import exh.md.utils.MdUtil
|
||||||
import exh.metadata.metadata.base.insertFlatMetadata
|
import exh.metadata.metadata.base.insertFlatMetadataAsync
|
||||||
import exh.source.LIBRARY_UPDATE_EXCLUDED_SOURCES
|
import exh.source.LIBRARY_UPDATE_EXCLUDED_SOURCES
|
||||||
import exh.source.MERGED_SOURCE_ID
|
import exh.source.MERGED_SOURCE_ID
|
||||||
import exh.source.getMainSource
|
import exh.source.getMainSource
|
||||||
@@ -88,6 +88,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
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -109,6 +110,8 @@ class LibraryUpdateService(
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
|
private var instance: LibraryUpdateService? = null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Key for category to update.
|
* Key for category to update.
|
||||||
*/
|
*/
|
||||||
@@ -147,7 +150,7 @@ 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 /* SY --> */, group: Int = LibraryGroup.BY_DEFAULT, groupExtra: String? = null /* SY <-- */): Boolean {
|
fun start(context: Context, category: Category? = null, target: Target = Target.CHAPTERS /* SY --> */, group: Int = LibraryGroup.BY_DEFAULT, groupExtra: String? = null /* SY <-- */): 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) }
|
||||||
@@ -158,10 +161,11 @@ class LibraryUpdateService(
|
|||||||
}
|
}
|
||||||
ContextCompat.startForegroundService(context, intent)
|
ContextCompat.startForegroundService(context, intent)
|
||||||
|
|
||||||
return true
|
true
|
||||||
|
} else {
|
||||||
|
instance?.addMangaToQueue(category?.id ?: -1, group, groupExtra, target)
|
||||||
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -198,6 +202,9 @@ class LibraryUpdateService(
|
|||||||
if (wakeLock.isHeld) {
|
if (wakeLock.isHeld) {
|
||||||
wakeLock.release()
|
wakeLock.release()
|
||||||
}
|
}
|
||||||
|
if (instance == this) {
|
||||||
|
instance = null
|
||||||
|
}
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,23 +228,27 @@ 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)
|
val group = intent.getIntExtra(KEY_GROUP, LibraryGroup.BY_DEFAULT)
|
||||||
.sortedWith(rankingScheme[selectedScheme])
|
val groupExtra = intent.getStringExtra(KEY_GROUP_EXTRA)
|
||||||
|
addMangaToQueue(categoryId, group, groupExtra, target)
|
||||||
|
|
||||||
|
// 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()
|
||||||
// SY -->
|
// SY -->
|
||||||
Target.SYNC_FOLLOWS -> syncFollows()
|
Target.SYNC_FOLLOWS -> syncFollows()
|
||||||
Target.PUSH_FAVORITES -> pushFavorites()
|
Target.PUSH_FAVORITES -> pushFavorites()
|
||||||
@@ -250,36 +261,40 @@ 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, group: Int, groupExtra: String?, target: Target) {
|
||||||
val categoryId = intent.getIntExtra(KEY_CATEGORY, -1)
|
val libraryManga = db.getLibraryMangas().executeAsBlocking()
|
||||||
// SY -->
|
// SY -->
|
||||||
val group = intent.getIntExtra(KEY_GROUP, LibraryGroup.BY_DEFAULT)
|
|
||||||
val groupLibraryUpdateType = preferences.groupLibraryUpdateType().get()
|
val groupLibraryUpdateType = preferences.groupLibraryUpdateType().get()
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
var listToUpdate = if (categoryId != -1) {
|
var listToUpdate = if (categoryId != -1) {
|
||||||
db.getLibraryMangas().executeAsBlocking().filter { it.category == categoryId }
|
libraryManga.filter { it.category == categoryId }
|
||||||
// SY -->
|
// SY -->
|
||||||
} else if (group == LibraryGroup.BY_DEFAULT || groupLibraryUpdateType == PreferenceValues.GroupLibraryMode.GLOBAL || (groupLibraryUpdateType == PreferenceValues.GroupLibraryMode.ALL_BUT_UNGROUPED && group == LibraryGroup.UNGROUPED)) {
|
} else if (group == LibraryGroup.BY_DEFAULT || groupLibraryUpdateType == PreferenceValues.GroupLibraryMode.GLOBAL || (groupLibraryUpdateType == PreferenceValues.GroupLibraryMode.ALL_BUT_UNGROUPED && group == LibraryGroup.UNGROUPED)) {
|
||||||
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)
|
||||||
} else {
|
} else {
|
||||||
val libraryManga = db.getLibraryMangas().executeAsBlocking().distinctBy { it.id }
|
|
||||||
when (group) {
|
when (group) {
|
||||||
LibraryGroup.BY_TRACK_STATUS -> {
|
LibraryGroup.BY_TRACK_STATUS -> {
|
||||||
val trackingExtra = intent.getStringExtra(KEY_GROUP_EXTRA)?.toIntOrNull() ?: -1
|
val trackingExtra = groupExtra?.toIntOrNull() ?: -1
|
||||||
libraryManga.filter {
|
libraryManga.filter {
|
||||||
val loggedServices = trackManager.services.filter { it.isLogged }
|
val loggedServices = trackManager.services.filter { it.isLogged }
|
||||||
val status: String = run {
|
val status: String = run {
|
||||||
@@ -298,12 +313,12 @@ class LibraryUpdateService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
LibraryGroup.BY_SOURCE -> {
|
LibraryGroup.BY_SOURCE -> {
|
||||||
val sourceExtra = intent.getStringExtra(KEY_GROUP_EXTRA).nullIfBlank()
|
val sourceExtra = groupExtra.nullIfBlank()
|
||||||
val source = sourceManager.getCatalogueSources().find { it.name == sourceExtra }
|
val source = sourceManager.getCatalogueSources().find { it.name == sourceExtra }
|
||||||
if (source != null) libraryManga.filter { it.source == source.id } else emptyList()
|
if (source != null) libraryManga.filter { it.source == source.id } else emptyList()
|
||||||
}
|
}
|
||||||
LibraryGroup.BY_STATUS -> {
|
LibraryGroup.BY_STATUS -> {
|
||||||
val statusExtra = intent.getStringExtra(KEY_GROUP_EXTRA)?.toIntOrNull() ?: -1
|
val statusExtra = groupExtra?.toIntOrNull() ?: -1
|
||||||
libraryManga.filter {
|
libraryManga.filter {
|
||||||
it.status == statusExtra
|
it.status == statusExtra
|
||||||
}
|
}
|
||||||
@@ -314,10 +329,13 @@ class LibraryUpdateService(
|
|||||||
// SY <--
|
// SY <--
|
||||||
}
|
}
|
||||||
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])
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -329,7 +347,7 @@ 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 semaphore = Semaphore(5)
|
val semaphore = Semaphore(5)
|
||||||
val progressCount = AtomicInteger(0)
|
val progressCount = AtomicInteger(0)
|
||||||
val newUpdates = mutableListOf<Pair<LibraryManga, Array<Chapter>>>()
|
val newUpdates = mutableListOf<Pair<LibraryManga, Array<Chapter>>>()
|
||||||
@@ -463,7 +481,7 @@ 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 ->
|
||||||
@@ -496,7 +514,7 @@ 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 }
|
||||||
|
|
||||||
@@ -539,11 +557,12 @@ class LibraryUpdateService(
|
|||||||
private suspend fun syncFollows() {
|
private suspend fun syncFollows() {
|
||||||
val count = AtomicInteger(0)
|
val count = AtomicInteger(0)
|
||||||
val mangaDex = MdUtil.getEnabledMangaDex(preferences, sourceManager) ?: return
|
val mangaDex = MdUtil.getEnabledMangaDex(preferences, sourceManager) ?: return
|
||||||
|
val syncFollowStatusInts = preferences.mangadexSyncToLibraryIndexes().get().map { it.toInt() }
|
||||||
|
|
||||||
val size: Int
|
val size: Int
|
||||||
mangaDex.fetchAllFollows(true)
|
mangaDex.fetchAllFollows(true)
|
||||||
.filter { (_, metadata) ->
|
.filter { (_, metadata) ->
|
||||||
metadata.follow_status == FollowStatus.RE_READING.int || metadata.follow_status == FollowStatus.READING.int
|
syncFollowStatusInts.contains(metadata.follow_status)
|
||||||
}
|
}
|
||||||
.also { size = it.size }
|
.also { size = it.size }
|
||||||
.forEach { (networkManga, metadata) ->
|
.forEach { (networkManga, metadata) ->
|
||||||
@@ -569,7 +588,7 @@ class LibraryUpdateService(
|
|||||||
val id = db.insertManga(dbManga).executeOnIO().insertedId()
|
val id = db.insertManga(dbManga).executeOnIO().insertedId()
|
||||||
if (id != null) {
|
if (id != null) {
|
||||||
metadata.mangaId = id
|
metadata.mangaId = id
|
||||||
db.insertFlatMetadata(metadata.flatten()).await()
|
db.insertFlatMetadataAsync(metadata.flatten()).await()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
shareFile(
|
shareFile(
|
||||||
context,
|
context,
|
||||||
intent.getParcelableExtra(EXTRA_URI),
|
intent.getParcelableExtra(EXTRA_URI),
|
||||||
if (intent.getBooleanExtra(EXTRA_IS_LEGACY_BACKUP, false)) "application/json" else "application/octet-stream+gzip",
|
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(
|
||||||
|
|||||||
@@ -17,13 +17,21 @@ object PreferenceKeys {
|
|||||||
|
|
||||||
const val rotation = "pref_rotation_type_key"
|
const val rotation = "pref_rotation_type_key"
|
||||||
|
|
||||||
const val enableTransitions = "pref_enable_transitions_key"
|
const val enableTransitionsPager = "pref_enable_transitions_pager_key"
|
||||||
|
|
||||||
|
const val enableTransitionsWebtoon = "pref_enable_transitions_webtoon_key"
|
||||||
|
|
||||||
const val doubleTapAnimationSpeed = "pref_double_tap_anim_speed"
|
const val doubleTapAnimationSpeed = "pref_double_tap_anim_speed"
|
||||||
|
|
||||||
const val showPageNumber = "pref_show_page_number_key"
|
const val showPageNumber = "pref_show_page_number_key"
|
||||||
|
|
||||||
const val dualPageSplit = "pref_dual_page_split"
|
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"
|
||||||
|
|
||||||
@@ -73,6 +81,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"
|
||||||
@@ -114,6 +126,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"
|
||||||
|
|
||||||
@@ -158,6 +171,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"
|
||||||
|
|
||||||
@@ -183,7 +197,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"
|
||||||
|
|
||||||
@@ -199,6 +213,8 @@ object PreferenceKeys {
|
|||||||
|
|
||||||
const val incognitoMode = "incognito_mode"
|
const val incognitoMode = "incognito_mode"
|
||||||
|
|
||||||
|
const val createLegacyBackup = "create_legacy_backup"
|
||||||
|
|
||||||
fun trackUsername(syncId: Int) = "pref_mangasync_username_$syncId"
|
fun trackUsername(syncId: Int) = "pref_mangasync_username_$syncId"
|
||||||
|
|
||||||
fun trackPassword(syncId: Int) = "pref_mangasync_password_$syncId"
|
fun trackPassword(syncId: Int) = "pref_mangasync_password_$syncId"
|
||||||
@@ -313,6 +329,8 @@ object PreferenceKeys {
|
|||||||
|
|
||||||
const val mangadexSimilarOnlyOverWifi = "pref_simular_only_over_wifi_key"
|
const val mangadexSimilarOnlyOverWifi = "pref_simular_only_over_wifi_key"
|
||||||
|
|
||||||
|
const val mangadexSyncToLibraryIndexes = "pref_mangadex_sync_to_library_indexes"
|
||||||
|
|
||||||
const val preferredMangaDexId = "preferred_mangaDex_id"
|
const val preferredMangaDexId = "preferred_mangaDex_id"
|
||||||
|
|
||||||
const val dataSaver = "data_saver"
|
const val dataSaver = "data_saver"
|
||||||
@@ -339,11 +357,15 @@ object PreferenceKeys {
|
|||||||
|
|
||||||
const val sortTagsForLibrary = "sort_tags_for_library"
|
const val sortTagsForLibrary = "sort_tags_for_library"
|
||||||
|
|
||||||
const val createLegacyBackup = "create_legacy_backup"
|
|
||||||
|
|
||||||
const val dontDeleteFromCategories = "dont_delete_from_categories"
|
const val dontDeleteFromCategories = "dont_delete_from_categories"
|
||||||
|
|
||||||
const val extensionRepos = "extension_repos"
|
const val extensionRepos = "extension_repos"
|
||||||
|
|
||||||
const val cropBordersContinuesVertical = "crop_borders_continues_vertical"
|
const val cropBordersContinuousVertical = "crop_borders_continues_vertical"
|
||||||
|
|
||||||
|
const val landscapeVerticalSeekbar = "pref_show_vert_seekbar_landscape"
|
||||||
|
|
||||||
|
const val leftVerticalSeekbar = "pref_left_handed_vertical_seekbar"
|
||||||
|
|
||||||
|
const val forceHorizontalSeekbar = "pref_force_horz_seekbar"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,8 +27,11 @@ object PreferenceValues {
|
|||||||
amoled,
|
amoled,
|
||||||
red,
|
red,
|
||||||
midnightdusk,
|
midnightdusk,
|
||||||
|
hotpink,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 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)
|
||||||
@@ -83,13 +87,21 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun rotation() = flowPrefs.getInt(Keys.rotation, 1)
|
fun rotation() = flowPrefs.getInt(Keys.rotation, 1)
|
||||||
|
|
||||||
fun pageTransitions() = flowPrefs.getBoolean(Keys.enableTransitions, true)
|
fun pageTransitionsPager() = flowPrefs.getBoolean(Keys.enableTransitionsPager, true)
|
||||||
|
|
||||||
|
fun pageTransitionsWebtoon() = flowPrefs.getBoolean(Keys.enableTransitionsWebtoon, true)
|
||||||
|
|
||||||
fun doubleTapAnimSpeed() = flowPrefs.getInt(Keys.doubleTapAnimationSpeed, 500)
|
fun doubleTapAnimSpeed() = flowPrefs.getInt(Keys.doubleTapAnimationSpeed, 500)
|
||||||
|
|
||||||
fun showPageNumber() = flowPrefs.getBoolean(Keys.showPageNumber, true)
|
fun showPageNumber() = flowPrefs.getBoolean(Keys.showPageNumber, true)
|
||||||
|
|
||||||
fun dualPageSplit() = flowPrefs.getBoolean(Keys.dualPageSplit, false)
|
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)
|
||||||
|
|
||||||
@@ -143,6 +155,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)
|
||||||
@@ -204,6 +220,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)
|
||||||
|
|
||||||
@@ -254,6 +271,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, "")
|
||||||
|
|
||||||
@@ -267,7 +285,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", "")
|
||||||
|
|
||||||
@@ -425,6 +443,8 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun mangadexSimilarOnlyOverWifi() = flowPrefs.getBoolean(Keys.mangadexSimilarOnlyOverWifi, true)
|
fun mangadexSimilarOnlyOverWifi() = flowPrefs.getBoolean(Keys.mangadexSimilarOnlyOverWifi, true)
|
||||||
|
|
||||||
|
fun mangadexSyncToLibraryIndexes() = flowPrefs.getStringSet(Keys.mangadexSyncToLibraryIndexes, emptySet())
|
||||||
|
|
||||||
fun mangadexSimilarUpdateInterval() = flowPrefs.getInt(Keys.mangadexSimilarUpdateInterval, 2)
|
fun mangadexSimilarUpdateInterval() = flowPrefs.getInt(Keys.mangadexSimilarUpdateInterval, 2)
|
||||||
|
|
||||||
fun dataSaver() = flowPrefs.getBoolean(Keys.dataSaver, false)
|
fun dataSaver() = flowPrefs.getBoolean(Keys.dataSaver, false)
|
||||||
@@ -455,5 +475,11 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun extensionRepos() = flowPrefs.getStringSet(Keys.extensionRepos, emptySet())
|
fun extensionRepos() = flowPrefs.getStringSet(Keys.extensionRepos, emptySet())
|
||||||
|
|
||||||
fun cropBordersContinuesVertical() = flowPrefs.getBoolean(Keys.cropBordersContinuesVertical, false)
|
fun cropBordersContinuousVertical() = flowPrefs.getBoolean(Keys.cropBordersContinuousVertical, false)
|
||||||
|
|
||||||
|
fun forceHorizontalSeekbar() = flowPrefs.getBoolean(Keys.forceHorizontalSeekbar, false)
|
||||||
|
|
||||||
|
fun landscapeVerticalSeekbar() = flowPrefs.getBoolean(Keys.landscapeVerticalSeekbar, false)
|
||||||
|
|
||||||
|
fun leftVerticalSeekbar() = flowPrefs.getBoolean(Keys.leftVerticalSeekbar, false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import eu.kanade.tachiyomi.source.model.SManga
|
|||||||
import eu.kanade.tachiyomi.source.model.toMangaInfo
|
import eu.kanade.tachiyomi.source.model.toMangaInfo
|
||||||
import eu.kanade.tachiyomi.util.lang.awaitSingle
|
import eu.kanade.tachiyomi.util.lang.awaitSingle
|
||||||
import eu.kanade.tachiyomi.util.lang.runAsObservable
|
import eu.kanade.tachiyomi.util.lang.runAsObservable
|
||||||
|
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||||
import exh.md.utils.FollowStatus
|
import exh.md.utils.FollowStatus
|
||||||
import exh.md.utils.MdUtil
|
import exh.md.utils.MdUtil
|
||||||
import tachiyomi.source.model.MangaInfo
|
import tachiyomi.source.model.MangaInfo
|
||||||
@@ -46,80 +47,84 @@ class MdList(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
override suspend fun add(track: Track): Track = update(track)
|
override suspend fun add(track: Track): Track = update(track)
|
||||||
|
|
||||||
override suspend fun update(track: Track): Track {
|
override suspend fun update(track: Track): Track {
|
||||||
val mdex = mdex ?: throw MangaDexNotFoundException()
|
return withIOContext {
|
||||||
|
val mdex = mdex ?: throw MangaDexNotFoundException()
|
||||||
|
|
||||||
val remoteTrack = mdex.fetchTrackingInfo(track.tracking_url)
|
val remoteTrack = mdex.fetchTrackingInfo(track.tracking_url)
|
||||||
val followStatus = FollowStatus.fromInt(track.status)
|
val followStatus = FollowStatus.fromInt(track.status)
|
||||||
|
|
||||||
// this updates the follow status in the metadata
|
// this updates the follow status in the metadata
|
||||||
// allow follow status to update
|
// allow follow status to update
|
||||||
if (remoteTrack.status != followStatus.int) {
|
if (remoteTrack.status != followStatus.int) {
|
||||||
mdex.updateFollowStatus(MdUtil.getMangaId(track.tracking_url), followStatus)
|
mdex.updateFollowStatus(MdUtil.getMangaId(track.tracking_url), followStatus)
|
||||||
remoteTrack.status = followStatus.int
|
remoteTrack.status = followStatus.int
|
||||||
// db.insertFlatMetadataAsync(mangaMetadata.flatten()).await()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (track.score.toInt() > 0) {
|
|
||||||
mdex.updateRating(track)
|
|
||||||
}
|
|
||||||
|
|
||||||
// mangadex wont update chapters if manga is not follows this prevents unneeded network call
|
|
||||||
|
|
||||||
if (followStatus != FollowStatus.UNFOLLOWED) {
|
|
||||||
if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
|
|
||||||
track.status = FollowStatus.COMPLETED.int
|
|
||||||
mdex.updateFollowStatus(MdUtil.getMangaId(track.tracking_url), FollowStatus.COMPLETED)
|
|
||||||
}
|
|
||||||
if (followStatus == FollowStatus.PLAN_TO_READ && track.last_chapter_read > 0) {
|
|
||||||
val newFollowStatus = FollowStatus.READING
|
|
||||||
track.status = FollowStatus.READING.int
|
|
||||||
mdex.updateFollowStatus(MdUtil.getMangaId(track.tracking_url), newFollowStatus)
|
|
||||||
remoteTrack.status = newFollowStatus.int
|
|
||||||
// db.insertFlatMetadataAsync(mangaMetadata.flatten()).await()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mdex.updateReadingProgress(track)
|
if (track.score.toInt() > 0) {
|
||||||
} else if (track.last_chapter_read != 0) {
|
mdex.updateRating(track)
|
||||||
// When followStatus has been changed to unfollowed 0 out read chapters since dex does
|
}
|
||||||
track.last_chapter_read = 0
|
|
||||||
|
// mangadex wont update chapters if manga is not follows this prevents unneeded network call
|
||||||
|
|
||||||
|
if (followStatus != FollowStatus.UNFOLLOWED) {
|
||||||
|
if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
|
||||||
|
track.status = FollowStatus.COMPLETED.int
|
||||||
|
mdex.updateFollowStatus(MdUtil.getMangaId(track.tracking_url), FollowStatus.COMPLETED)
|
||||||
|
}
|
||||||
|
if (followStatus == FollowStatus.PLAN_TO_READ && track.last_chapter_read > 0) {
|
||||||
|
val newFollowStatus = FollowStatus.READING
|
||||||
|
track.status = FollowStatus.READING.int
|
||||||
|
mdex.updateFollowStatus(MdUtil.getMangaId(track.tracking_url), newFollowStatus)
|
||||||
|
remoteTrack.status = newFollowStatus.int
|
||||||
|
}
|
||||||
|
|
||||||
|
mdex.updateReadingProgress(track)
|
||||||
|
} else if (track.last_chapter_read != 0) {
|
||||||
|
// When followStatus has been changed to unfollowed 0 out read chapters since dex does
|
||||||
|
track.last_chapter_read = 0
|
||||||
|
}
|
||||||
|
track
|
||||||
}
|
}
|
||||||
return track
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getCompletionStatus(): Int = FollowStatus.COMPLETED.int
|
override fun getCompletionStatus(): Int = FollowStatus.COMPLETED.int
|
||||||
|
|
||||||
override suspend fun bind(track: Track): Track = update(refresh(track))
|
override suspend fun bind(track: Track): Track = update(refresh(track).also { if (it.status == FollowStatus.UNFOLLOWED.int) it.status = FollowStatus.READING.int })
|
||||||
|
|
||||||
override suspend fun refresh(track: Track): Track {
|
override suspend fun refresh(track: Track): Track {
|
||||||
val mdex = mdex ?: throw MangaDexNotFoundException()
|
return withIOContext {
|
||||||
val (remoteTrack, mangaMetadata) = mdex.getTrackingAndMangaInfo(track)
|
val mdex = mdex ?: throw MangaDexNotFoundException()
|
||||||
track.copyPersonalFrom(remoteTrack)
|
val (remoteTrack, mangaMetadata) = mdex.getTrackingAndMangaInfo(track)
|
||||||
if (track.total_chapters == 0 && mangaMetadata.status == SManga.COMPLETED) {
|
track.copyPersonalFrom(remoteTrack)
|
||||||
track.total_chapters = mangaMetadata.maxChapterNumber ?: 0
|
if (track.total_chapters == 0 && mangaMetadata.status == SManga.COMPLETED) {
|
||||||
|
track.total_chapters = mangaMetadata.maxChapterNumber ?: 0
|
||||||
|
}
|
||||||
|
track
|
||||||
}
|
}
|
||||||
return track
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createInitialTracker(dbManga: Manga, mdManga: Manga = dbManga): Track {
|
fun createInitialTracker(dbManga: Manga, mdManga: Manga = dbManga): Track {
|
||||||
val track = Track.create(TrackManager.MDLIST)
|
return Track.create(TrackManager.MDLIST).apply {
|
||||||
track.manga_id = dbManga.id!!
|
manga_id = dbManga.id!!
|
||||||
track.status = FollowStatus.UNFOLLOWED.int
|
status = FollowStatus.UNFOLLOWED.int
|
||||||
track.tracking_url = MdUtil.baseUrl + mdManga.url
|
tracking_url = MdUtil.baseUrl + mdManga.url
|
||||||
track.title = mdManga.title
|
title = mdManga.title
|
||||||
return track
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun search(query: String): List<TrackSearch> {
|
override suspend fun search(query: String): List<TrackSearch> {
|
||||||
val mdex = mdex ?: throw MangaDexNotFoundException()
|
return withIOContext {
|
||||||
return mdex.fetchSearchManga(0, query, mdex.getFilterList())
|
val mdex = mdex ?: throw MangaDexNotFoundException()
|
||||||
.flatMap { page ->
|
mdex.fetchSearchManga(0, query, mdex.getFilterList())
|
||||||
runAsObservable({
|
.flatMap { page ->
|
||||||
page.mangas.map {
|
runAsObservable({
|
||||||
toTrackSearch(mdex.getMangaDetails(it.toMangaInfo()))
|
page.mangas.map {
|
||||||
}
|
toTrackSearch(mdex.getMangaDetails(it.toMangaInfo()))
|
||||||
})
|
}
|
||||||
}
|
})
|
||||||
.awaitSingle()
|
}
|
||||||
|
.awaitSingle()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun toTrackSearch(mangaInfo: MangaInfo): TrackSearch = TrackSearch.create(TrackManager.MDLIST).apply {
|
private fun toTrackSearch(mangaInfo: MangaInfo): TrackSearch = TrackSearch.create(TrackManager.MDLIST).apply {
|
||||||
@@ -131,5 +136,8 @@ class MdList(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
|
|
||||||
override suspend fun login(username: String, password: String): Unit = throw Exception("not used")
|
override suspend fun login(username: String, password: String): Unit = throw Exception("not used")
|
||||||
|
|
||||||
|
override val isLogged: Boolean
|
||||||
|
get() = false
|
||||||
|
|
||||||
class MangaDexNotFoundException : Exception("Mangadex not enabled")
|
class MangaDexNotFoundException : Exception("Mangadex not enabled")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,8 +32,8 @@ class GithubUpdateChecker {
|
|||||||
.parseAs<GithubRelease>()
|
.parseAs<GithubRelease>()
|
||||||
.let {
|
.let {
|
||||||
// Check if latest version is different from current version
|
// Check if latest version is different from current version
|
||||||
if (/* SY --> */ isNewVersionSY(it.version) /* SY <-- */) {
|
if (/* SY --> */ isNewVersionSY(it.version) /* SY <-- */) {
|
||||||
GithubUpdateResult.NewUpdate(it)
|
GithubUpdateResult.NewUpdate(it)
|
||||||
} else {
|
} else {
|
||||||
GithubUpdateResult.NoNewUpdate()
|
GithubUpdateResult.NoNewUpdate()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.extension
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import com.elvishew.xlog.XLog
|
|
||||||
import com.jakewharton.rxrelay.BehaviorRelay
|
import com.jakewharton.rxrelay.BehaviorRelay
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
@@ -19,6 +18,7 @@ import eu.kanade.tachiyomi.source.Source
|
|||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.util.lang.launchNow
|
import eu.kanade.tachiyomi.util.lang.launchNow
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
|
import exh.log.xLogD
|
||||||
import exh.source.BlacklistedSources
|
import exh.source.BlacklistedSources
|
||||||
import exh.source.EH_SOURCE_ID
|
import exh.source.EH_SOURCE_ID
|
||||||
import exh.source.EXH_SOURCE_ID
|
import exh.source.EXH_SOURCE_ID
|
||||||
@@ -156,15 +156,15 @@ class ExtensionManager(
|
|||||||
// EXH -->
|
// EXH -->
|
||||||
private fun <T : Extension> Iterable<T>.filterNotBlacklisted(): List<T> {
|
private fun <T : Extension> Iterable<T>.filterNotBlacklisted(): List<T> {
|
||||||
val blacklistEnabled = preferences.enableSourceBlacklist().get()
|
val blacklistEnabled = preferences.enableSourceBlacklist().get()
|
||||||
return filter {
|
return filterNot { extension ->
|
||||||
if (it.isBlacklisted(blacklistEnabled)) {
|
extension.isBlacklisted(blacklistEnabled)
|
||||||
XLog.tag("ExtensionManager").d("Removing blacklisted extension: (name: %s, pkgName: %s)!", it.name, it.pkgName)
|
.also {
|
||||||
false
|
if (it) this@ExtensionManager.xLogD("Removing blacklisted extension: (name: %s, pkgName: %s)!", extension.name, extension.pkgName)
|
||||||
} else true
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Extension.isBlacklisted(blacklistEnabled: Boolean = preferences.enableSourceBlacklist().get()): Boolean {
|
private fun Extension.isBlacklisted(blacklistEnabled: Boolean = preferences.enableSourceBlacklist().get()): Boolean {
|
||||||
return pkgName in BlacklistedSources.BLACKLISTED_EXTENSIONS && blacklistEnabled
|
return pkgName in BlacklistedSources.BLACKLISTED_EXTENSIONS && blacklistEnabled
|
||||||
}
|
}
|
||||||
// EXH <--
|
// EXH <--
|
||||||
@@ -333,7 +333,7 @@ class ExtensionManager(
|
|||||||
private fun registerNewExtension(extension: Extension.Installed) {
|
private fun registerNewExtension(extension: Extension.Installed) {
|
||||||
// SY -->
|
// SY -->
|
||||||
if (extension.isBlacklisted()) {
|
if (extension.isBlacklisted()) {
|
||||||
XLog.tag("ExtensionManager").d("Removing blacklisted extension: (name: String, pkgName: %s)!", extension.name, extension.pkgName)
|
xLogD("Removing blacklisted extension: (name: String, pkgName: %s)!", extension.name, extension.pkgName)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// SY <--
|
// SY <--
|
||||||
@@ -351,7 +351,7 @@ class ExtensionManager(
|
|||||||
private fun registerUpdatedExtension(extension: Extension.Installed) {
|
private fun registerUpdatedExtension(extension: Extension.Installed) {
|
||||||
// SY -->
|
// SY -->
|
||||||
if (extension.isBlacklisted()) {
|
if (extension.isBlacklisted()) {
|
||||||
XLog.tag("ExtensionManager").d("Removing blacklisted extension: (name: String, pkgName: %s)!", extension.name, extension.pkgName)
|
xLogD("Removing blacklisted extension: (name: %s, pkgName: %s)!", extension.name, extension.pkgName)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ internal class ExtensionGithubApi {
|
|||||||
.let { parseResponse(it) }
|
.let { parseResponse(it) }
|
||||||
} /* SY --> */ + preferences.extensionRepos().get().flatMap { repoPath ->
|
} /* SY --> */ + preferences.extensionRepos().get().flatMap { repoPath ->
|
||||||
val url = "$BASE_URL$repoPath/repo/"
|
val url = "$BASE_URL$repoPath/repo/"
|
||||||
networkService.client
|
networkService.client
|
||||||
.newCall(GET("${url}index.min.json"))
|
.newCall(GET("${url}index.min.json"))
|
||||||
.await()
|
.await()
|
||||||
.parseAs<JsonArray>()
|
.parseAs<JsonArray>()
|
||||||
|
|||||||
@@ -163,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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -171,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
|
||||||
|
|
||||||
/* SY --> */ open /* SY <-- */ class NetworkHelper(context: Context) {
|
/* SY --> */ open /* SY <-- */ class NetworkHelper(context: Context) {
|
||||||
@@ -38,25 +35,9 @@ import java.util.concurrent.TimeUnit
|
|||||||
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()
|
||||||
|
|||||||
@@ -33,7 +33,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")
|
||||||
@@ -64,6 +64,12 @@ class LocalSource(private val context: Context) : CatalogueSource {
|
|||||||
val c = context.getString(R.string.app_name) + File.separator + "local"
|
val c = context.getString(R.string.app_name) + File.separator + "local"
|
||||||
return DiskUtil.getExternalStorages(context).map { File(it.absolutePath, c) }
|
return DiskUtil.getExternalStorages(context).map { File(it.absolutePath, c) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SY -->
|
||||||
|
val json = Json {
|
||||||
|
prettyPrint = true
|
||||||
|
}
|
||||||
|
// SY <--
|
||||||
}
|
}
|
||||||
|
|
||||||
override val id = ID
|
override val id = ID
|
||||||
@@ -151,19 +157,16 @@ class LocalSource(private val context: Context) : CatalogueSource {
|
|||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
fun updateMangaInfo(manga: SManga) {
|
fun updateMangaInfo(manga: SManga) {
|
||||||
val directory = getBaseDirectories(context).mapNotNull { File(it, manga.url) }.find {
|
val directory = getBaseDirectories(context).map { File(it, manga.url) }.find {
|
||||||
it.exists()
|
it.exists()
|
||||||
} ?: return
|
} ?: return
|
||||||
val json = Json {
|
|
||||||
prettyPrint = true
|
|
||||||
}
|
|
||||||
val existingFileName = directory.listFiles()?.find { it.extension == "json" }?.name
|
val existingFileName = directory.listFiles()?.find { it.extension == "json" }?.name
|
||||||
val file = File(directory, existingFileName ?: "info.json")
|
val file = File(directory, existingFileName ?: "info.json")
|
||||||
file.writeText(json.encodeToString(manga.toJson()))
|
file.writeText(json.encodeToString(manga.toJson()))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun SManga.toJson(): MangaJson {
|
private fun SManga.toJson(): MangaJson {
|
||||||
return MangaJson(title, author, artist, description, genre?.split(", ")?.toTypedArray())
|
return MangaJson(title, author, artist, description, genre?.split(", "), status)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@@ -172,7 +175,8 @@ class LocalSource(private val context: Context) : CatalogueSource {
|
|||||||
val author: String?,
|
val author: String?,
|
||||||
val artist: String?,
|
val artist: String?,
|
||||||
val description: String?,
|
val description: String?,
|
||||||
val genre: Array<String>?
|
val genre: List<String>?,
|
||||||
|
val status: Int
|
||||||
) {
|
) {
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.source
|
package eu.kanade.tachiyomi.source
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.elvishew.xlog.XLog
|
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
@@ -10,15 +9,14 @@ import eu.kanade.tachiyomi.source.model.SManga
|
|||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.source.online.all.EHentai
|
import eu.kanade.tachiyomi.source.online.all.EHentai
|
||||||
import eu.kanade.tachiyomi.source.online.all.Hitomi
|
import eu.kanade.tachiyomi.source.online.all.Hitomi
|
||||||
import eu.kanade.tachiyomi.source.online.all.MangaDex
|
|
||||||
import eu.kanade.tachiyomi.source.online.all.MergedSource
|
import eu.kanade.tachiyomi.source.online.all.MergedSource
|
||||||
import eu.kanade.tachiyomi.source.online.all.NHentai
|
import eu.kanade.tachiyomi.source.online.all.NHentai
|
||||||
import eu.kanade.tachiyomi.source.online.all.PervEden
|
import eu.kanade.tachiyomi.source.online.all.PervEden
|
||||||
import eu.kanade.tachiyomi.source.online.english.EightMuses
|
import eu.kanade.tachiyomi.source.online.english.EightMuses
|
||||||
import eu.kanade.tachiyomi.source.online.english.HBrowse
|
import eu.kanade.tachiyomi.source.online.english.HBrowse
|
||||||
import eu.kanade.tachiyomi.source.online.english.HentaiCafe
|
|
||||||
import eu.kanade.tachiyomi.source.online.english.Pururin
|
import eu.kanade.tachiyomi.source.online.english.Pururin
|
||||||
import eu.kanade.tachiyomi.source.online.english.Tsumino
|
import eu.kanade.tachiyomi.source.online.english.Tsumino
|
||||||
|
import exh.log.xLogD
|
||||||
import exh.source.BlacklistedSources
|
import exh.source.BlacklistedSources
|
||||||
import exh.source.DelegatedHttpSource
|
import exh.source.DelegatedHttpSource
|
||||||
import exh.source.EH_SOURCE_ID
|
import exh.source.EH_SOURCE_ID
|
||||||
@@ -26,7 +24,6 @@ import exh.source.EIGHTMUSES_SOURCE_ID
|
|||||||
import exh.source.EXH_SOURCE_ID
|
import exh.source.EXH_SOURCE_ID
|
||||||
import exh.source.EnhancedHttpSource
|
import exh.source.EnhancedHttpSource
|
||||||
import exh.source.HBROWSE_SOURCE_ID
|
import exh.source.HBROWSE_SOURCE_ID
|
||||||
import exh.source.HENTAI_CAFE_SOURCE_ID
|
|
||||||
import exh.source.PERV_EDEN_EN_SOURCE_ID
|
import exh.source.PERV_EDEN_EN_SOURCE_ID
|
||||||
import exh.source.PERV_EDEN_IT_SOURCE_ID
|
import exh.source.PERV_EDEN_IT_SOURCE_ID
|
||||||
import exh.source.PURURIN_SOURCE_ID
|
import exh.source.PURURIN_SOURCE_ID
|
||||||
@@ -115,7 +112,7 @@ open class SourceManager(private val context: Context) {
|
|||||||
} else DELEGATED_SOURCES[sourceQName]
|
} else DELEGATED_SOURCES[sourceQName]
|
||||||
} else null
|
} else null
|
||||||
val newSource = if (source is HttpSource && delegate != null) {
|
val newSource = if (source is HttpSource && delegate != null) {
|
||||||
XLog.tag("SourceManager").d("Delegating source: %s -> %s!", sourceQName, delegate.newSourceClass.qualifiedName)
|
xLogD("Delegating source: %s -> %s!", sourceQName, delegate.newSourceClass.qualifiedName)
|
||||||
val enhancedSource = EnhancedHttpSource(
|
val enhancedSource = EnhancedHttpSource(
|
||||||
source,
|
source,
|
||||||
delegate.newSourceClass.constructors.find { it.parameters.size == 2 }!!.call(source, context)
|
delegate.newSourceClass.constructors.find { it.parameters.size == 2 }!!.call(source, context)
|
||||||
@@ -132,7 +129,7 @@ open class SourceManager(private val context: Context) {
|
|||||||
} else source
|
} else source
|
||||||
|
|
||||||
if (source.id in BlacklistedSources.BLACKLISTED_EXT_SOURCES) {
|
if (source.id in BlacklistedSources.BLACKLISTED_EXT_SOURCES) {
|
||||||
XLog.tag("SourceManager").d("Removing blacklisted source: (id: %s, name: %s, lang: %s)!", source.id, source.name, (source as? CatalogueSource)?.lang)
|
xLogD("Removing blacklisted source: (id: %s, name: %s, lang: %s)!", source.id, source.name, (source as? CatalogueSource)?.lang)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// EXH <--
|
// EXH <--
|
||||||
@@ -155,13 +152,12 @@ open class SourceManager(private val context: Context) {
|
|||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
private fun createEHSources(): List<Source> {
|
private fun createEHSources(): List<Source> {
|
||||||
val exSrcs = mutableListOf<HttpSource>(
|
val sources = listOf<HttpSource>(
|
||||||
EHentai(EH_SOURCE_ID, false, context)
|
EHentai(EH_SOURCE_ID, false, context)
|
||||||
)
|
)
|
||||||
if (prefs.enableExhentai().get()) {
|
return if (prefs.enableExhentai().get()) {
|
||||||
exSrcs += EHentai(EXH_SOURCE_ID, true, context)
|
sources + EHentai(EXH_SOURCE_ID, true, context)
|
||||||
}
|
} else sources
|
||||||
return exSrcs
|
|
||||||
}
|
}
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
@@ -195,12 +191,6 @@ open class SourceManager(private val context: Context) {
|
|||||||
companion object {
|
companion object {
|
||||||
private const val fillInSourceId = Long.MAX_VALUE
|
private const val fillInSourceId = Long.MAX_VALUE
|
||||||
val DELEGATED_SOURCES = listOf(
|
val DELEGATED_SOURCES = listOf(
|
||||||
DelegatedSource(
|
|
||||||
"Hentai Cafe",
|
|
||||||
HENTAI_CAFE_SOURCE_ID,
|
|
||||||
"eu.kanade.tachiyomi.extension.all.foolslide.HentaiCafe",
|
|
||||||
HentaiCafe::class
|
|
||||||
),
|
|
||||||
DelegatedSource(
|
DelegatedSource(
|
||||||
"Pururin",
|
"Pururin",
|
||||||
PURURIN_SOURCE_ID,
|
PURURIN_SOURCE_ID,
|
||||||
@@ -213,13 +203,13 @@ open class SourceManager(private val context: Context) {
|
|||||||
"eu.kanade.tachiyomi.extension.en.tsumino.Tsumino",
|
"eu.kanade.tachiyomi.extension.en.tsumino.Tsumino",
|
||||||
Tsumino::class
|
Tsumino::class
|
||||||
),
|
),
|
||||||
DelegatedSource(
|
/*DelegatedSource(
|
||||||
"MangaDex",
|
"MangaDex",
|
||||||
fillInSourceId,
|
fillInSourceId,
|
||||||
"eu.kanade.tachiyomi.extension.all.mangadex",
|
"eu.kanade.tachiyomi.extension.all.mangadex",
|
||||||
MangaDex::class,
|
MangaDex::class,
|
||||||
true
|
true
|
||||||
),
|
),*/
|
||||||
DelegatedSource(
|
DelegatedSource(
|
||||||
"HBrowse",
|
"HBrowse",
|
||||||
HBROWSE_SOURCE_ID,
|
HBROWSE_SOURCE_ID,
|
||||||
@@ -229,7 +219,7 @@ open class SourceManager(private val context: Context) {
|
|||||||
DelegatedSource(
|
DelegatedSource(
|
||||||
"8Muses",
|
"8Muses",
|
||||||
EIGHTMUSES_SOURCE_ID,
|
EIGHTMUSES_SOURCE_ID,
|
||||||
"eu.kanade.tachiyomi.extension.all.eromuse.EroMuse",
|
"eu.kanade.tachiyomi.extension.en.eightmuses.EightMuses",
|
||||||
EightMuses::class
|
EightMuses::class
|
||||||
),
|
),
|
||||||
DelegatedSource(
|
DelegatedSource(
|
||||||
@@ -276,7 +266,7 @@ open class SourceManager(private val context: Context) {
|
|||||||
get() = internalMap.size
|
get() = internalMap.size
|
||||||
override fun containsKey(key: K): Boolean = internalMap.containsKey(key)
|
override fun containsKey(key: K): Boolean = internalMap.containsKey(key)
|
||||||
override fun containsValue(value: V): Boolean = internalMap.containsValue(value)
|
override fun containsValue(value: V): Boolean = internalMap.containsValue(value)
|
||||||
override fun get(key: K): V? = get(key)
|
override fun get(key: K): V? = internalMap[key]
|
||||||
override fun isEmpty(): Boolean = internalMap.isEmpty()
|
override fun isEmpty(): Boolean = internalMap.isEmpty()
|
||||||
override val entries: MutableSet<MutableMap.MutableEntry<K, V>>
|
override val entries: MutableSet<MutableMap.MutableEntry<K, V>>
|
||||||
get() = internalMap.entries
|
get() = internalMap.entries
|
||||||
|
|||||||
@@ -2,8 +2,26 @@ package eu.kanade.tachiyomi.source.model
|
|||||||
|
|
||||||
import exh.metadata.metadata.base.RaisedSearchMetadata
|
import exh.metadata.metadata.base.RaisedSearchMetadata
|
||||||
|
|
||||||
/* SY --> */ open /* SY <-- */ class MangasPage(val mangas: List<SManga>, val hasNextPage: Boolean)
|
/* SY --> */ open /* SY <-- */ class MangasPage(open val mangas: List<SManga>, open val hasNextPage: Boolean) {
|
||||||
|
// SY -->
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (other !is MangasPage) return false
|
||||||
|
|
||||||
|
if (mangas != other.mangas) return false
|
||||||
|
if (hasNextPage != other.hasNextPage) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = mangas.hashCode()
|
||||||
|
result = 31 * result + hasNextPage.hashCode()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
// SY <--
|
||||||
|
}
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
class MetadataMangasPage(mangas: List<SManga>, hasNextPage: Boolean, val mangasMetadata: List<RaisedSearchMetadata>) : MangasPage(mangas, hasNextPage)
|
data class MetadataMangasPage(override val mangas: List<SManga>, override val hasNextPage: Boolean, val mangasMetadata: List<RaisedSearchMetadata>) : MangasPage(mangas, hasNextPage)
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ interface SManga : Serializable {
|
|||||||
get() = (this as? MangaImpl)?.ogDesc ?: description
|
get() = (this as? MangaImpl)?.ogDesc ?: description
|
||||||
val originalGenre: String?
|
val originalGenre: String?
|
||||||
get() = (this as? MangaImpl)?.ogGenre ?: genre
|
get() = (this as? MangaImpl)?.ogGenre ?: genre
|
||||||
|
val originalStatus: Int
|
||||||
|
get() = (this as? MangaImpl)?.ogStatus ?: status
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
fun copyFrom(other: SManga) {
|
fun copyFrom(other: SManga) {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import eu.kanade.tachiyomi.ui.manga.MangaController
|
|||||||
import exh.metadata.metadata.base.RaisedSearchMetadata
|
import exh.metadata.metadata.base.RaisedSearchMetadata
|
||||||
import exh.metadata.metadata.base.getFlatMetadataForManga
|
import exh.metadata.metadata.base.getFlatMetadataForManga
|
||||||
import exh.metadata.metadata.base.insertFlatMetadata
|
import exh.metadata.metadata.base.insertFlatMetadata
|
||||||
import exh.metadata.metadata.base.insertFlatMetadataAsync
|
import exh.metadata.metadata.base.insertFlatMetadataCompletable
|
||||||
import exh.util.executeOnIO
|
import exh.util.executeOnIO
|
||||||
import rx.Completable
|
import rx.Completable
|
||||||
import rx.Single
|
import rx.Single
|
||||||
@@ -72,7 +72,7 @@ interface MetadataSource<M : RaisedSearchMetadata, I> : CatalogueSource {
|
|||||||
}.flatMapCompletable {
|
}.flatMapCompletable {
|
||||||
if (mangaId != null) {
|
if (mangaId != null) {
|
||||||
it.mangaId = mangaId
|
it.mangaId = mangaId
|
||||||
db.insertFlatMetadata(it.flatten())
|
db.insertFlatMetadataCompletable(it.flatten())
|
||||||
} else Completable.complete()
|
} else Completable.complete()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -87,7 +87,7 @@ interface MetadataSource<M : RaisedSearchMetadata, I> : CatalogueSource {
|
|||||||
parseInfoIntoMetadata(metadata, input)
|
parseInfoIntoMetadata(metadata, input)
|
||||||
if (mangaId != null) {
|
if (mangaId != null) {
|
||||||
metadata.mangaId = mangaId
|
metadata.mangaId = mangaId
|
||||||
db.insertFlatMetadataAsync(metadata.flatten()).await()
|
db.insertFlatMetadata(metadata.flatten())
|
||||||
}
|
}
|
||||||
|
|
||||||
return metadata.createMangaInfo(manga)
|
return metadata.createMangaInfo(manga)
|
||||||
@@ -119,7 +119,7 @@ interface MetadataSource<M : RaisedSearchMetadata, I> : CatalogueSource {
|
|||||||
val newMetaSingle = Single.just(newMeta)
|
val newMetaSingle = Single.just(newMeta)
|
||||||
if (mangaId != null) {
|
if (mangaId != null) {
|
||||||
newMeta.mangaId = mangaId
|
newMeta.mangaId = mangaId
|
||||||
db.insertFlatMetadata(newMeta.flatten()).andThen(newMetaSingle)
|
db.insertFlatMetadataCompletable(newMeta.flatten()).andThen(newMetaSingle)
|
||||||
} else newMetaSingle
|
} else newMetaSingle
|
||||||
}
|
}
|
||||||
} else Single.just(existingMeta)
|
} else Single.just(existingMeta)
|
||||||
@@ -146,7 +146,7 @@ interface MetadataSource<M : RaisedSearchMetadata, I> : CatalogueSource {
|
|||||||
parseInfoIntoMetadata(newMeta, input)
|
parseInfoIntoMetadata(newMeta, input)
|
||||||
if (mangaId != null) {
|
if (mangaId != null) {
|
||||||
newMeta.mangaId = mangaId
|
newMeta.mangaId = mangaId
|
||||||
db.insertFlatMetadataAsync(newMeta.flatten()).await().let { newMeta }
|
db.insertFlatMetadata(newMeta.flatten()).let { newMeta }
|
||||||
} else newMeta
|
} else newMeta
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.source.online.all
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import com.elvishew.xlog.XLog
|
|
||||||
import eu.kanade.tachiyomi.annotations.Nsfw
|
import eu.kanade.tachiyomi.annotations.Nsfw
|
||||||
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
|
||||||
@@ -31,6 +30,7 @@ import exh.eh.EHTags
|
|||||||
import exh.eh.EHentaiUpdateHelper
|
import exh.eh.EHentaiUpdateHelper
|
||||||
import exh.eh.EHentaiUpdateWorkerConstants
|
import exh.eh.EHentaiUpdateWorkerConstants
|
||||||
import exh.eh.GalleryEntry
|
import exh.eh.GalleryEntry
|
||||||
|
import exh.log.xLogD
|
||||||
import exh.metadata.MetadataUtil
|
import exh.metadata.MetadataUtil
|
||||||
import exh.metadata.metadata.EHentaiSearchMetadata
|
import exh.metadata.metadata.EHentaiSearchMetadata
|
||||||
import exh.metadata.metadata.EHentaiSearchMetadata.Companion.EH_GENRE_NAMESPACE
|
import exh.metadata.metadata.EHentaiSearchMetadata.Companion.EH_GENRE_NAMESPACE
|
||||||
@@ -40,7 +40,7 @@ import exh.metadata.metadata.EHentaiSearchMetadata.Companion.TAG_TYPE_WEAK
|
|||||||
import exh.metadata.metadata.base.RaisedSearchMetadata.Companion.TAG_TYPE_VIRTUAL
|
import exh.metadata.metadata.base.RaisedSearchMetadata.Companion.TAG_TYPE_VIRTUAL
|
||||||
import exh.metadata.metadata.base.RaisedSearchMetadata.Companion.toGenreString
|
import exh.metadata.metadata.base.RaisedSearchMetadata.Companion.toGenreString
|
||||||
import exh.metadata.metadata.base.RaisedTag
|
import exh.metadata.metadata.base.RaisedTag
|
||||||
import exh.ui.login.LoginController
|
import exh.ui.login.EhLoginActivity
|
||||||
import exh.ui.metadata.adapters.EHentaiDescriptionAdapter
|
import exh.ui.metadata.adapters.EHentaiDescriptionAdapter
|
||||||
import exh.util.UriFilter
|
import exh.util.UriFilter
|
||||||
import exh.util.UriGroup
|
import exh.util.UriGroup
|
||||||
@@ -229,7 +229,7 @@ class EHentai(
|
|||||||
} else {
|
} else {
|
||||||
parsedLocation.queryParameter(REVERSE_PARAM)!!.toBoolean()
|
parsedLocation.queryParameter(REVERSE_PARAM)!!.toBoolean()
|
||||||
}
|
}
|
||||||
Pair(parsedMangas, hasNextPage)
|
parsedMangas to hasNextPage
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getGenre(element: Element? = null, genreString: String? = null): String? {
|
private fun getGenre(element: Element? = null, genreString: String? = null): String? {
|
||||||
@@ -325,7 +325,7 @@ class EHentai(
|
|||||||
url = EHentaiSearchMetadata.normalizeUrl(parentLink)
|
url = EHentaiSearchMetadata.normalizeUrl(parentLink)
|
||||||
} else break
|
} else break
|
||||||
} else {
|
} else {
|
||||||
XLog.tag("EHentai").d("Parent cache hit: %s!", gid)
|
this@EHentai.xLogD("Parent cache hit: %s!", gid)
|
||||||
url = EHentaiSearchMetadata.idAndTokenToUrl(
|
url = EHentaiSearchMetadata.idAndTokenToUrl(
|
||||||
cachedParent.gId,
|
cachedParent.gId,
|
||||||
cachedParent.gToken
|
cachedParent.gToken
|
||||||
@@ -613,7 +613,7 @@ class EHentai(
|
|||||||
lastUpdateCheck - datePosted!! > EHentaiUpdateWorkerConstants.GALLERY_AGE_TIME
|
lastUpdateCheck - datePosted!! > EHentaiUpdateWorkerConstants.GALLERY_AGE_TIME
|
||||||
) {
|
) {
|
||||||
aged = true
|
aged = true
|
||||||
XLog.tag("EHentai").d("aged %s - too old", title)
|
this@EHentai.xLogD("aged %s - too old", title)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse ratings
|
// Parse ratings
|
||||||
@@ -713,7 +713,7 @@ class EHentai(
|
|||||||
page++
|
page++
|
||||||
} while (parsed.second)
|
} while (parsed.second)
|
||||||
|
|
||||||
return Pair(result.toList(), favNames!!)
|
return Pair(result.toList(), favNames.orEmpty())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun spPref() = if (exh) {
|
fun spPref() = if (exh) {
|
||||||
@@ -725,9 +725,9 @@ class EHentai(
|
|||||||
private fun rawCookies(sp: Int): Map<String, String> {
|
private fun rawCookies(sp: Int): Map<String, String> {
|
||||||
val cookies: MutableMap<String, String> = mutableMapOf()
|
val cookies: MutableMap<String, String> = mutableMapOf()
|
||||||
if (preferences.enableExhentai().get()) {
|
if (preferences.enableExhentai().get()) {
|
||||||
cookies[LoginController.MEMBER_ID_COOKIE] = preferences.memberIdVal().get()
|
cookies[EhLoginActivity.MEMBER_ID_COOKIE] = preferences.memberIdVal().get()
|
||||||
cookies[LoginController.PASS_HASH_COOKIE] = preferences.passHashVal().get()
|
cookies[EhLoginActivity.PASS_HASH_COOKIE] = preferences.passHashVal().get()
|
||||||
cookies[LoginController.IGNEOUS_COOKIE] = preferences.igneousVal().get()
|
cookies[EhLoginActivity.IGNEOUS_COOKIE] = preferences.igneousVal().get()
|
||||||
cookies["sp"] = sp.toString()
|
cookies["sp"] = sp.toString()
|
||||||
|
|
||||||
val sessionKey = preferences.exhSettingsKey().get()
|
val sessionKey = preferences.exhSettingsKey().get()
|
||||||
@@ -879,13 +879,20 @@ class EHentai(
|
|||||||
stringBuilder.append(" ")
|
stringBuilder.append(" ")
|
||||||
}
|
}
|
||||||
|
|
||||||
XLog.tag("EHentai").d(stringBuilder.toString())
|
return stringBuilder.toString().trim().also { xLogD(it) }
|
||||||
return stringBuilder.toString().trim()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data class AdvSearchEntry(val search: Pair<String, String>, val exclude: Boolean)
|
data class AdvSearchEntry(val search: Pair<String, String>, val exclude: Boolean)
|
||||||
|
|
||||||
class AutoCompleteTags(tags: List<String>, skipAutoFillTags: List<String>, excludePrefix: String) : Filter.AutoComplete(name = "Tags", hint = "Search tags here (limit of 8)", values = tags, skipAutoFillTags = skipAutoFillTags, excludePrefix = excludePrefix, state = emptyList())
|
class AutoCompleteTags(tags: List<String>, skipAutoFillTags: List<String>, excludePrefix: String) :
|
||||||
|
Filter.AutoComplete(
|
||||||
|
name = "Tags",
|
||||||
|
hint = "Search tags here (limit of 8)",
|
||||||
|
values = tags,
|
||||||
|
skipAutoFillTags = skipAutoFillTags,
|
||||||
|
excludePrefix = excludePrefix,
|
||||||
|
state = emptyList()
|
||||||
|
)
|
||||||
|
|
||||||
class MinPagesOption : PageOption("Minimum Pages", "f_spf")
|
class MinPagesOption : PageOption("Minimum Pages", "f_spf")
|
||||||
class MaxPagesOption : PageOption("Maximum Pages", "f_spt")
|
class MaxPagesOption : PageOption("Maximum Pages", "f_spt")
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import android.app.Activity
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.core.text.HtmlCompat
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
@@ -48,12 +47,18 @@ import exh.source.DelegatedHttpSource
|
|||||||
import exh.ui.metadata.adapters.MangaDexDescriptionAdapter
|
import exh.ui.metadata.adapters.MangaDexDescriptionAdapter
|
||||||
import exh.util.urlImportFetchSearchManga
|
import exh.util.urlImportFetchSearchManga
|
||||||
import exh.widget.preference.MangadexLoginDialog
|
import exh.widget.preference.MangadexLoginDialog
|
||||||
|
import kotlinx.serialization.decodeFromString
|
||||||
|
import kotlinx.serialization.json.JsonObject
|
||||||
|
import kotlinx.serialization.json.JsonPrimitive
|
||||||
|
import kotlinx.serialization.json.int
|
||||||
import okhttp3.CacheControl
|
import okhttp3.CacheControl
|
||||||
import okhttp3.FormBody
|
import okhttp3.FormBody
|
||||||
import okhttp3.Headers
|
import okhttp3.Headers
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
|
import okhttp3.internal.closeQuietly
|
||||||
|
import okio.EOFException
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import tachiyomi.source.model.ChapterInfo
|
import tachiyomi.source.model.ChapterInfo
|
||||||
import tachiyomi.source.model.MangaInfo
|
import tachiyomi.source.model.MangaInfo
|
||||||
@@ -73,11 +78,10 @@ class MangaDex(delegate: HttpSource, val context: Context) :
|
|||||||
RandomMangaSource {
|
RandomMangaSource {
|
||||||
override val lang: String = delegate.lang
|
override val lang: String = delegate.lang
|
||||||
|
|
||||||
override val headers: Headers
|
override val headers: Headers = super.headers.newBuilder().apply {
|
||||||
get() = super.headers.newBuilder().apply {
|
add("X-Requested-With", "XMLHttpRequest")
|
||||||
add("X-Requested-With", "XMLHttpRequest")
|
add("Referer", MdUtil.baseUrl)
|
||||||
add("Referer", MdUtil.baseUrl)
|
}.build()
|
||||||
}.build()
|
|
||||||
|
|
||||||
private val mdLang by lazy {
|
private val mdLang by lazy {
|
||||||
MdLang.values().find { it.lang == lang }?.dexLang ?: lang
|
MdLang.values().find { it.lang == lang }?.dexLang ?: lang
|
||||||
@@ -198,26 +202,26 @@ class MangaDex(delegate: HttpSource, val context: Context) :
|
|||||||
add("login_password", password)
|
add("login_password", password)
|
||||||
add("no_js", "1")
|
add("no_js", "1")
|
||||||
add("remember_me", "1")
|
add("remember_me", "1")
|
||||||
|
add("two_factor", twoFactorCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
twoFactorCode.let {
|
runCatching {
|
||||||
formBody.add("two_factor", it)
|
client.newCall(
|
||||||
|
POST(
|
||||||
|
"${MdUtil.baseUrl}/ajax/actions.ajax.php?function=login",
|
||||||
|
headers,
|
||||||
|
formBody.build()
|
||||||
|
)
|
||||||
|
).await().closeQuietly()
|
||||||
}
|
}
|
||||||
|
|
||||||
val response = client.newCall(
|
val response = client.newCall(GET(MdUtil.apiUrl + MdUtil.isLoggedInApi, headers)).await()
|
||||||
POST(
|
|
||||||
"${MdUtil.baseUrl}/ajax/actions.ajax.php?function=login",
|
|
||||||
headers,
|
|
||||||
formBody.build()
|
|
||||||
)
|
|
||||||
).await()
|
|
||||||
|
|
||||||
withIOContext { response.body?.string() }.let { result ->
|
withIOContext { response.body?.string() }.let { jsonData ->
|
||||||
if (result != null && result.isEmpty()) {
|
if (jsonData != null) {
|
||||||
true
|
MdUtil.jsonParser.decodeFromString<JsonObject>(jsonData)["code"]?.let { it as? JsonPrimitive }?.int == 200
|
||||||
} else {
|
} else {
|
||||||
val error = result?.let { HtmlCompat.fromHtml(it, HtmlCompat.FROM_HTML_MODE_COMPACT).toString() }
|
throw Exception("Json data was null")
|
||||||
throw Exception(error)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -233,11 +237,17 @@ class MangaDex(delegate: HttpSource, val context: Context) :
|
|||||||
if (token.isNullOrEmpty()) {
|
if (token.isNullOrEmpty()) {
|
||||||
return@withIOContext true
|
return@withIOContext true
|
||||||
}
|
}
|
||||||
val result = client.newCall(
|
try {
|
||||||
POST("${MdUtil.baseUrl}/ajax/actions.ajax.php?function=logout", headers).newBuilder().addHeader(REMEMBER_ME, token).build()
|
val result = client.newCall(
|
||||||
).await()
|
POST("${MdUtil.baseUrl}/ajax/actions.ajax.php?function=logout", headers).newBuilder().addHeader(REMEMBER_ME, token).build()
|
||||||
val resultStr = withIOContext { result.body?.string() }
|
).await()
|
||||||
if (resultStr?.contains("success", true) == true) {
|
val resultStr = withIOContext { result.body?.string() }
|
||||||
|
if (resultStr?.contains("success", true) == true) {
|
||||||
|
network.cookieManager.remove(httpUrl)
|
||||||
|
trackManager.mdList.logout()
|
||||||
|
return@withIOContext true
|
||||||
|
}
|
||||||
|
} catch (e: EOFException) {
|
||||||
network.cookieManager.remove(httpUrl)
|
network.cookieManager.remove(httpUrl)
|
||||||
trackManager.mdList.logout()
|
trackManager.mdList.logout()
|
||||||
return@withIOContext true
|
return@withIOContext true
|
||||||
@@ -269,7 +279,7 @@ class MangaDex(delegate: HttpSource, val context: Context) :
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getTrackingAndMangaInfo(track: Track): Pair<Track, MangaDexSearchMetadata> {
|
suspend fun getTrackingAndMangaInfo(track: Track): Pair<Track, MangaDexSearchMetadata> {
|
||||||
return MangaHandler(client, headers, lang).getTrackingInfo(track, useLowQualityThumbnail())
|
return MangaHandler(client, headers, mdLang).getTrackingInfo(track, useLowQualityThumbnail())
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun updateFollowStatus(mangaID: String, followStatus: FollowStatus): Boolean {
|
override suspend fun updateFollowStatus(mangaID: String, followStatus: FollowStatus): Boolean {
|
||||||
@@ -281,7 +291,7 @@ class MangaDex(delegate: HttpSource, val context: Context) :
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun fetchRandomMangaUrl(): String {
|
override suspend fun fetchRandomMangaUrl(): String {
|
||||||
return MangaHandler(client, headers, mdLang).fetchRandomMangaId()
|
return withIOContext { MangaHandler(client, headers, mdLang).fetchRandomMangaId() }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun fetchMangaSimilar(manga: Manga): Observable<MangasPage> {
|
fun fetchMangaSimilar(manga: Manga): Observable<MangasPage> {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package eu.kanade.tachiyomi.source.online.all
|
package eu.kanade.tachiyomi.source.online.all
|
||||||
|
|
||||||
import com.elvishew.xlog.XLog
|
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
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
|
||||||
@@ -19,6 +18,7 @@ import eu.kanade.tachiyomi.source.online.HttpSource
|
|||||||
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
|
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
|
||||||
import eu.kanade.tachiyomi.util.lang.withIOContext
|
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||||
import eu.kanade.tachiyomi.util.shouldDownloadNewChapters
|
import eu.kanade.tachiyomi.util.shouldDownloadNewChapters
|
||||||
|
import exh.log.xLogW
|
||||||
import exh.merged.sql.models.MergedMangaReference
|
import exh.merged.sql.models.MergedMangaReference
|
||||||
import exh.source.MERGED_SOURCE_ID
|
import exh.source.MERGED_SOURCE_ID
|
||||||
import exh.util.executeOnIO
|
import exh.util.executeOnIO
|
||||||
@@ -181,11 +181,11 @@ class MergedSource : HttpSource() {
|
|||||||
}
|
}
|
||||||
manga.copyFrom(source.getMangaDetails(manga.toMangaInfo()).toSManga())
|
manga.copyFrom(source.getMangaDetails(manga.toMangaInfo()).toSManga())
|
||||||
try {
|
try {
|
||||||
manga.id = db.insertManga(manga).executeOnIO().insertedId()
|
manga.id = db.insertManga(manga).executeAsBlocking().insertedId()
|
||||||
mangaId = manga.id
|
mangaId = manga.id
|
||||||
db.insertNewMergedMangaId(this).executeOnIO()
|
db.insertNewMergedMangaId(this).executeAsBlocking()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
XLog.tag("MergedSource").enableStackTrace(e.stackTrace.contentToString(), 5)
|
xLogW("Error inserting merged manga id", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return LoadedMangaSource(source, manga, this)
|
return LoadedMangaSource(source, manga, this)
|
||||||
|
|||||||
@@ -1,143 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.source.online.english
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.net.Uri
|
|
||||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
|
||||||
import eu.kanade.tachiyomi.network.await
|
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
|
||||||
import eu.kanade.tachiyomi.source.model.toSManga
|
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
|
||||||
import eu.kanade.tachiyomi.source.online.MetadataSource
|
|
||||||
import eu.kanade.tachiyomi.source.online.UrlImportableSource
|
|
||||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
|
||||||
import eu.kanade.tachiyomi.util.lang.runAsObservable
|
|
||||||
import exh.metadata.metadata.HentaiCafeSearchMetadata
|
|
||||||
import exh.metadata.metadata.HentaiCafeSearchMetadata.Companion.TAG_TYPE_DEFAULT
|
|
||||||
import exh.metadata.metadata.base.RaisedSearchMetadata.Companion.TAG_TYPE_VIRTUAL
|
|
||||||
import exh.metadata.metadata.base.RaisedTag
|
|
||||||
import exh.source.DelegatedHttpSource
|
|
||||||
import exh.ui.metadata.adapters.HentaiCafeDescriptionAdapter
|
|
||||||
import exh.util.urlImportFetchSearchManga
|
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
|
||||||
import org.jsoup.nodes.Document
|
|
||||||
import rx.Observable
|
|
||||||
import tachiyomi.source.model.ChapterInfo
|
|
||||||
import tachiyomi.source.model.MangaInfo
|
|
||||||
|
|
||||||
class HentaiCafe(delegate: HttpSource, val context: Context) :
|
|
||||||
DelegatedHttpSource(delegate),
|
|
||||||
MetadataSource<HentaiCafeSearchMetadata, Document>,
|
|
||||||
UrlImportableSource {
|
|
||||||
/**
|
|
||||||
* An ISO 639-1 compliant language code (two letters in lower case).
|
|
||||||
*/
|
|
||||||
override val lang = "en"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The class of the metadata used by this source
|
|
||||||
*/
|
|
||||||
override val metaClass = HentaiCafeSearchMetadata::class
|
|
||||||
|
|
||||||
// Support direct URL importing
|
|
||||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList) =
|
|
||||||
urlImportFetchSearchManga(context, query) {
|
|
||||||
super.fetchSearchManga(page, query, filters)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
|
||||||
return client.newCall(mangaDetailsRequest(manga))
|
|
||||||
.asObservableSuccess()
|
|
||||||
.flatMap {
|
|
||||||
parseToManga(manga, it.asJsoup()).andThen(
|
|
||||||
Observable.just(
|
|
||||||
manga.apply {
|
|
||||||
initialized = true
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getMangaDetails(manga: MangaInfo): MangaInfo {
|
|
||||||
val response = client.newCall(mangaDetailsRequest(manga.toSManga())).await()
|
|
||||||
return parseToManga(manga, response.asJsoup())
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse the supplied input into the supplied metadata object
|
|
||||||
*/
|
|
||||||
override fun parseIntoMetadata(metadata: HentaiCafeSearchMetadata, input: Document) {
|
|
||||||
with(metadata) {
|
|
||||||
url = input.location()
|
|
||||||
title = input.select("h3").text()
|
|
||||||
val contentElement = input.select(".entry-content").first()
|
|
||||||
thumbnailUrl = contentElement.child(0).child(0).attr("src")
|
|
||||||
|
|
||||||
fun filterableTagsOfType(type: String) = contentElement.select("a")
|
|
||||||
.filter { "$baseUrl/hc.fyi/$type/" in it.attr("href") }
|
|
||||||
.map { it.text() }
|
|
||||||
|
|
||||||
tags.clear()
|
|
||||||
tags += filterableTagsOfType("tag").map {
|
|
||||||
RaisedTag(null, it, TAG_TYPE_DEFAULT)
|
|
||||||
}
|
|
||||||
|
|
||||||
val artists = filterableTagsOfType("artist")
|
|
||||||
|
|
||||||
artist = artists.joinToString()
|
|
||||||
tags += artists.map {
|
|
||||||
RaisedTag("artist", it, TAG_TYPE_VIRTUAL)
|
|
||||||
}
|
|
||||||
|
|
||||||
readerId = input.select("[title=Read]").attr("href").toHttpUrlOrNull()!!.pathSegments[2]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun fetchChapterList(manga: SManga) = runAsObservable({
|
|
||||||
fetchOrLoadMetadata(manga.id) {
|
|
||||||
val response = client.newCall(mangaDetailsRequest(manga)).await()
|
|
||||||
response.asJsoup()
|
|
||||||
}
|
|
||||||
}).map {
|
|
||||||
listOf(
|
|
||||||
SChapter.create().apply {
|
|
||||||
url = "/manga/read/${it.readerId}/en/0/1/"
|
|
||||||
name = "Chapter"
|
|
||||||
chapter_number = 0.0f
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getChapterList(manga: MangaInfo): List<ChapterInfo> {
|
|
||||||
val metadata = fetchOrLoadMetadata(manga.id()) {
|
|
||||||
val response = client.newCall(mangaDetailsRequest(manga.toSManga())).await()
|
|
||||||
response.asJsoup()
|
|
||||||
}
|
|
||||||
return listOf(
|
|
||||||
ChapterInfo(
|
|
||||||
key = "/manga/read/${metadata.readerId}/en/0/1/",
|
|
||||||
name = "Chapter",
|
|
||||||
number = 0F
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override val matchingHosts = listOf(
|
|
||||||
"hentai.cafe"
|
|
||||||
)
|
|
||||||
|
|
||||||
override suspend fun mapUrlToMangaUrl(uri: Uri): String? {
|
|
||||||
val lcFirstPathSegment = uri.pathSegments.firstOrNull()?.takeUnless { it.equals("manga", true) } ?: return null
|
|
||||||
|
|
||||||
return if (lcFirstPathSegment.equals("hc.fyi", true)) {
|
|
||||||
"/$lcFirstPathSegment/${uri.pathSegments[1]}"
|
|
||||||
} else null
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getDescriptionAdapter(controller: MangaController): HentaiCafeDescriptionAdapter {
|
|
||||||
return HentaiCafeDescriptionAdapter(controller)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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) ||
|
||||||
(
|
(
|
||||||
@@ -50,6 +50,7 @@ abstract class BaseThemedActivity : AppCompatActivity() {
|
|||||||
Values.DarkThemeVariant.amoled -> R.style.Theme_Tachiyomi_Amoled
|
Values.DarkThemeVariant.amoled -> R.style.Theme_Tachiyomi_Amoled
|
||||||
Values.DarkThemeVariant.red -> R.style.Theme_Tachiyomi_Red
|
Values.DarkThemeVariant.red -> R.style.Theme_Tachiyomi_Red
|
||||||
Values.DarkThemeVariant.midnightdusk -> R.style.Theme_Tachiyomi_MidnightDusk
|
Values.DarkThemeVariant.midnightdusk -> R.style.Theme_Tachiyomi_MidnightDusk
|
||||||
|
Values.DarkThemeVariant.hotpink -> R.style.Theme_Tachiyomi_HotPink
|
||||||
else -> R.style.Theme_Tachiyomi_Dark
|
else -> R.style.Theme_Tachiyomi_Dark
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-6
@@ -7,11 +7,12 @@ import android.view.View
|
|||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import com.bluelinelabs.conductor.ControllerChangeHandler
|
import com.bluelinelabs.conductor.ControllerChangeHandler
|
||||||
import com.bluelinelabs.conductor.changehandler.AnimatorChangeHandler
|
import com.bluelinelabs.conductor.changehandler.AnimatorChangeHandler
|
||||||
|
import com.bluelinelabs.conductor.changehandler.FadeChangeHandler
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An [AnimatorChangeHandler] that will cross fade two views
|
* An [AnimatorChangeHandler] that will remove the from view and fade in the to view
|
||||||
*/
|
*/
|
||||||
class OneWayFadeChangeHandler : AnimatorChangeHandler {
|
class OneWayFadeChangeHandler : FadeChangeHandler {
|
||||||
constructor()
|
constructor()
|
||||||
constructor(removesFromViewOnPush: Boolean) : super(removesFromViewOnPush)
|
constructor(removesFromViewOnPush: Boolean) : super(removesFromViewOnPush)
|
||||||
constructor(duration: Long) : super(duration)
|
constructor(duration: Long) : super(duration)
|
||||||
@@ -33,10 +34,6 @@ class OneWayFadeChangeHandler : AnimatorChangeHandler {
|
|||||||
return animator
|
return animator
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun resetFromView(from: View) {
|
|
||||||
from.alpha = 1f
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun copy(): ControllerChangeHandler {
|
override fun copy(): ControllerChangeHandler {
|
||||||
return OneWayFadeChangeHandler(animationDuration, removesFromViewOnPush())
|
return OneWayFadeChangeHandler(animationDuration, removesFromViewOnPush())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -121,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
|
||||||
|
|||||||
+196
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+6
@@ -22,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
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,11 +2,10 @@ package eu.kanade.tachiyomi.ui.browse.latest
|
|||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.Menu
|
|
||||||
import android.view.MenuInflater
|
|
||||||
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.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.asImmediateFlow
|
import eu.kanade.tachiyomi.data.preference.asImmediateFlow
|
||||||
@@ -33,10 +32,6 @@ open class LatestController :
|
|||||||
*/
|
*/
|
||||||
protected var adapter: LatestAdapter? = null
|
protected var adapter: LatestAdapter? = null
|
||||||
|
|
||||||
/*init {
|
|
||||||
setHasOptionsMenu(true)
|
|
||||||
}*/
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initiate the view with [R.layout.global_search_controller].
|
* Initiate the view with [R.layout.global_search_controller].
|
||||||
*
|
*
|
||||||
@@ -46,6 +41,11 @@ open class LatestController :
|
|||||||
*/
|
*/
|
||||||
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
|
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
|
||||||
binding = LatestControllerBinding.inflate(inflater)
|
binding = LatestControllerBinding.inflate(inflater)
|
||||||
|
binding.recycler.applyInsetter {
|
||||||
|
type(navigationBars = true) {
|
||||||
|
padding()
|
||||||
|
}
|
||||||
|
}
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,34 +82,6 @@ open class LatestController :
|
|||||||
onMangaClick(manga)
|
onMangaClick(manga)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 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.global_search, menu)
|
|
||||||
|
|
||||||
// Initialize search menu
|
|
||||||
val searchItem = menu.findItem(R.id.action_search)
|
|
||||||
val searchView = searchItem.actionView as SearchView
|
|
||||||
searchView.maxWidth = Int.MAX_VALUE
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
})*/
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when the view is created
|
* Called when the view is created
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ import uy.kohesive.injekt.api.get
|
|||||||
* @param preferences manages the preference calls.
|
* @param preferences manages the preference calls.
|
||||||
*/
|
*/
|
||||||
open class LatestPresenter(
|
open class LatestPresenter(
|
||||||
private val sourcesToUse: List<CatalogueSource>? = null,
|
|
||||||
val sourceManager: SourceManager = Injekt.get(),
|
val sourceManager: SourceManager = Injekt.get(),
|
||||||
val db: DatabaseHelper = Injekt.get(),
|
val db: DatabaseHelper = Injekt.get(),
|
||||||
val preferences: PreferencesHelper = Injekt.get()
|
val preferences: PreferencesHelper = Injekt.get()
|
||||||
|
|||||||
+6
@@ -12,6 +12,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
|||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.bluelinelabs.conductor.Router
|
import com.bluelinelabs.conductor.Router
|
||||||
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||||
|
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.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
@@ -47,6 +48,11 @@ class PreMigrationController(bundle: Bundle? = null) :
|
|||||||
|
|
||||||
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
|
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
|
||||||
binding = PreMigrationControllerBinding.inflate(inflater)
|
binding = PreMigrationControllerBinding.inflate(inflater)
|
||||||
|
binding.recycler.applyInsetter {
|
||||||
|
type(navigationBars = true) {
|
||||||
|
padding()
|
||||||
|
}
|
||||||
|
}
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-9
@@ -24,7 +24,7 @@ class MigratingManga(
|
|||||||
|
|
||||||
val migrationJob = parentContext + SupervisorJob() + Dispatchers.Default
|
val migrationJob = parentContext + SupervisorJob() + Dispatchers.Default
|
||||||
|
|
||||||
var migrationStatus: Int = MigrationStatus.RUNNING
|
var migrationStatus = MigrationStatus.RUNNING
|
||||||
|
|
||||||
@Volatile
|
@Volatile
|
||||||
private var manga: Manga? = null
|
private var manga: Manga? = null
|
||||||
@@ -42,11 +42,3 @@ class MigratingManga(
|
|||||||
return MigrationProcessItem(this)
|
return MigrationProcessItem(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class MigrationStatus {
|
|
||||||
companion object {
|
|
||||||
const val RUNNING = 0
|
|
||||||
const val MANGA_FOUND = 1
|
|
||||||
const val MANGA_NOT_FOUND = 2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
+12
-3
@@ -16,6 +16,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
|||||||
import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat
|
import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat
|
||||||
import com.afollestad.materialdialogs.MaterialDialog
|
import com.afollestad.materialdialogs.MaterialDialog
|
||||||
import com.bluelinelabs.conductor.changehandler.FadeChangeHandler
|
import com.bluelinelabs.conductor.changehandler.FadeChangeHandler
|
||||||
|
import dev.chrisbanes.insetter.applyInsetter
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
@@ -35,7 +36,6 @@ import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.PreMigrationContr
|
|||||||
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
|
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||||
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.launchUI
|
import eu.kanade.tachiyomi.util.lang.launchUI
|
||||||
import eu.kanade.tachiyomi.util.lang.withIOContext
|
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||||
import eu.kanade.tachiyomi.util.system.getResourceColor
|
import eu.kanade.tachiyomi.util.system.getResourceColor
|
||||||
@@ -48,7 +48,9 @@ import kotlinx.coroutines.CoroutineScope
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.isActive
|
import kotlinx.coroutines.isActive
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.sync.Semaphore
|
import kotlinx.coroutines.sync.Semaphore
|
||||||
import kotlinx.coroutines.sync.withPermit
|
import kotlinx.coroutines.sync.withPermit
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
@@ -73,6 +75,7 @@ class MigrationListController(bundle: Bundle? = null) :
|
|||||||
|
|
||||||
private val smartSearchEngine = SmartSearchEngine(config?.extraSearchParams)
|
private val smartSearchEngine = SmartSearchEngine(config?.extraSearchParams)
|
||||||
|
|
||||||
|
private val migrationScope = CoroutineScope(Job() + Dispatchers.IO)
|
||||||
var migrationsJob: Job? = null
|
var migrationsJob: Job? = null
|
||||||
private set
|
private set
|
||||||
private var migratingManga: MutableList<MigratingManga>? = null
|
private var migratingManga: MutableList<MigratingManga>? = null
|
||||||
@@ -83,6 +86,11 @@ class MigrationListController(bundle: Bundle? = null) :
|
|||||||
|
|
||||||
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
|
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
|
||||||
binding = MigrationListControllerBinding.inflate(inflater)
|
binding = MigrationListControllerBinding.inflate(inflater)
|
||||||
|
binding.recycler.applyInsetter {
|
||||||
|
type(navigationBars = true) {
|
||||||
|
padding()
|
||||||
|
}
|
||||||
|
}
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,7 +107,7 @@ class MigrationListController(bundle: Bundle? = null) :
|
|||||||
|
|
||||||
val newMigratingManga = migratingManga ?: run {
|
val newMigratingManga = migratingManga ?: run {
|
||||||
val new = config.mangaIds.map {
|
val new = config.mangaIds.map {
|
||||||
MigratingManga(db, sourceManager, it, viewScope.coroutineContext + Dispatchers.IO)
|
MigratingManga(db, sourceManager, it, migrationScope.coroutineContext)
|
||||||
}
|
}
|
||||||
migratingManga = new.toMutableList()
|
migratingManga = new.toMutableList()
|
||||||
new
|
new
|
||||||
@@ -114,7 +122,7 @@ class MigrationListController(bundle: Bundle? = null) :
|
|||||||
adapter?.updateDataSet(newMigratingManga.map { it.toModal() })
|
adapter?.updateDataSet(newMigratingManga.map { it.toModal() })
|
||||||
|
|
||||||
if (migrationsJob == null) {
|
if (migrationsJob == null) {
|
||||||
migrationsJob = viewScope.launchIO {
|
migrationsJob = migrationScope.launch {
|
||||||
runMigrations(newMigratingManga)
|
runMigrations(newMigratingManga)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -275,6 +283,7 @@ class MigrationListController(bundle: Bundle? = null) :
|
|||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
|
|
||||||
|
migrationScope.cancel()
|
||||||
activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+6
-6
@@ -55,14 +55,12 @@ class MigrationProcessHolder(
|
|||||||
|
|
||||||
binding.migrationMenu.setVectorCompat(
|
binding.migrationMenu.setVectorCompat(
|
||||||
R.drawable.ic_more_vert_24dp,
|
R.drawable.ic_more_vert_24dp,
|
||||||
view.context
|
view.context.getResourceColor(R.attr.colorOnPrimary)
|
||||||
.getResourceColor(R.attr.colorOnPrimary)
|
|
||||||
)
|
)
|
||||||
binding.skipManga.setVectorCompat(
|
binding.skipManga.setVectorCompat(
|
||||||
R.drawable.ic_close_24dp,
|
R.drawable.ic_close_24dp,
|
||||||
view.context.getResourceColor(
|
view.context.getResourceColor(
|
||||||
R
|
R.attr.colorOnPrimary
|
||||||
.attr.colorOnPrimary
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
binding.migrationMenu.isInvisible = true
|
binding.migrationMenu.isInvisible = true
|
||||||
@@ -79,7 +77,8 @@ class MigrationProcessHolder(
|
|||||||
true
|
true
|
||||||
).withFadeTransaction()
|
).withFadeTransaction()
|
||||||
)
|
)
|
||||||
}.launchIn(adapter.controller.viewScope)
|
}
|
||||||
|
.launchIn(adapter.controller.viewScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
/*launchUI {
|
/*launchUI {
|
||||||
@@ -115,7 +114,8 @@ class MigrationProcessHolder(
|
|||||||
true
|
true
|
||||||
).withFadeTransaction()
|
).withFadeTransaction()
|
||||||
)
|
)
|
||||||
}.launchIn(adapter.controller.viewScope)
|
}
|
||||||
|
.launchIn(adapter.controller.viewScope)
|
||||||
} else {
|
} else {
|
||||||
binding.migrationMangaCardTo.loadingGroup.isVisible = false
|
binding.migrationMangaCardTo.loadingGroup.isVisible = false
|
||||||
binding.migrationMangaCardTo.title.text = view.context.applicationContext
|
binding.migrationMangaCardTo.title.text = view.context.applicationContext
|
||||||
|
|||||||
+7
@@ -0,0 +1,7 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.browse.migration.advanced.process
|
||||||
|
|
||||||
|
enum class MigrationStatus {
|
||||||
|
RUNNING,
|
||||||
|
MANGA_FOUND,
|
||||||
|
MANGA_NOT_FOUND
|
||||||
|
}
|
||||||
+6
@@ -6,6 +6,7 @@ import android.view.View
|
|||||||
import android.view.ViewGroup
|
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 dev.chrisbanes.insetter.applyInsetter
|
||||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||||
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
|
||||||
@@ -53,6 +54,11 @@ class MigrationMangaController :
|
|||||||
|
|
||||||
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
|
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
|
||||||
binding = MigrationMangaControllerBinding.inflate(inflater)
|
binding = MigrationMangaControllerBinding.inflate(inflater)
|
||||||
|
binding.recycler.applyInsetter {
|
||||||
|
type(navigationBars = true) {
|
||||||
|
padding()
|
||||||
|
}
|
||||||
|
}
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+72
-25
@@ -5,9 +5,12 @@ import android.os.Bundle
|
|||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuInflater
|
import android.view.MenuInflater
|
||||||
import androidx.appcompat.widget.SearchView
|
import androidx.appcompat.widget.SearchView
|
||||||
|
import androidx.core.os.bundleOf
|
||||||
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.DatabaseHelper
|
||||||
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.source.CatalogueSource
|
||||||
@@ -24,17 +27,37 @@ import kotlinx.coroutines.flow.launchIn
|
|||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import reactivecircus.flowbinding.appcompat.QueryTextEvent
|
import reactivecircus.flowbinding.appcompat.QueryTextEvent
|
||||||
import reactivecircus.flowbinding.appcompat.queryTextEvents
|
import reactivecircus.flowbinding.appcompat.queryTextEvents
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
class SearchController(
|
class SearchController(
|
||||||
private var manga: Manga? = null,
|
private var manga: Manga? = null,
|
||||||
private var sources: List<CatalogueSource>? = null
|
private var sources: List<CatalogueSource>? = null
|
||||||
) : GlobalSearchController(manga?.originalTitle) {
|
) : GlobalSearchController(
|
||||||
|
manga?.originalTitle,
|
||||||
|
bundle = bundleOf(
|
||||||
|
OLD_MANGA to manga?.id,
|
||||||
|
SOURCES to sources?.map { it.id }?.toLongArray()
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
|
||||||
private var newManga: Manga? = null
|
private var newManga: Manga? = null
|
||||||
private var progress = 1
|
private var progress = 1
|
||||||
var totalProgress = 0
|
var totalProgress = 0
|
||||||
|
|
||||||
|
constructor(mangaId: Long, sources: LongArray) :
|
||||||
|
this(
|
||||||
|
Injekt.get<DatabaseHelper>().getManga(mangaId).executeAsBlocking(),
|
||||||
|
sources.map { Injekt.get<SourceManager>().getOrStub(it) }.filterIsInstance<CatalogueSource>()
|
||||||
|
)
|
||||||
|
|
||||||
|
@Suppress("unused")
|
||||||
|
constructor(bundle: Bundle) : this(
|
||||||
|
bundle.getLong(OLD_MANGA),
|
||||||
|
bundle.getLongArray(SOURCES) ?: LongArray(0)
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when controller is initialized.
|
* Called when controller is initialized.
|
||||||
*/
|
*/
|
||||||
@@ -58,31 +81,15 @@ class SearchController(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
fun migrateManga(manga: Manga, newManga: Manga) {
|
||||||
outState.putSerializable(::manga.name, manga)
|
|
||||||
outState.putSerializable(::newManga.name, newManga)
|
|
||||||
super.onSaveInstanceState(outState)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
|
|
||||||
super.onRestoreInstanceState(savedInstanceState)
|
|
||||||
manga = savedInstanceState.getSerializable(::manga.name) as? Manga
|
|
||||||
newManga = savedInstanceState.getSerializable(::newManga.name) as? Manga
|
|
||||||
}
|
|
||||||
|
|
||||||
fun migrateManga() {
|
|
||||||
val target = targetController as? MigrationInterface ?: return
|
val target = targetController as? MigrationInterface ?: return
|
||||||
val manga = manga ?: return
|
|
||||||
val newManga = newManga ?: return
|
|
||||||
|
|
||||||
val nextManga = target.migrateManga(manga, newManga, true)
|
val nextManga = target.migrateManga(manga, newManga, true)
|
||||||
replaceWithNewSearchController(nextManga)
|
replaceWithNewSearchController(nextManga)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun copyManga() {
|
fun copyManga(manga: Manga, newManga: Manga) {
|
||||||
val target = targetController as? MigrationInterface ?: return
|
val target = targetController as? MigrationInterface ?: return
|
||||||
val manga = manga ?: return
|
|
||||||
val newManga = newManga ?: return
|
|
||||||
|
|
||||||
val nextManga = target.migrateManga(manga, newManga, false)
|
val nextManga = target.migrateManga(manga, newManga, false)
|
||||||
replaceWithNewSearchController(nextManga)
|
replaceWithNewSearchController(nextManga)
|
||||||
@@ -102,14 +109,15 @@ class SearchController(
|
|||||||
override fun onMangaClick(manga: Manga) {
|
override fun onMangaClick(manga: Manga) {
|
||||||
if (targetController is MigrationListController) {
|
if (targetController is MigrationListController) {
|
||||||
val migrationListController = targetController as? MigrationListController
|
val migrationListController = targetController as? MigrationListController
|
||||||
val sourceManager: SourceManager by injectLazy()
|
val sourceManager = Injekt.get<SourceManager>()
|
||||||
val source = sourceManager.get(manga.source) ?: return
|
val source = sourceManager.get(manga.source) ?: return
|
||||||
migrationListController?.useMangaForMigration(manga, source)
|
migrationListController?.useMangaForMigration(manga, source)
|
||||||
router.popCurrentController()
|
router.popCurrentController()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
newManga = manga
|
newManga = manga
|
||||||
val dialog = MigrationDialog()
|
val dialog =
|
||||||
|
MigrationDialog(this.manga ?: return, newManga ?: return, this)
|
||||||
dialog.targetController = this
|
dialog.targetController = this
|
||||||
dialog.showDialog(router)
|
dialog.showDialog(router)
|
||||||
}
|
}
|
||||||
@@ -119,12 +127,26 @@ class SearchController(
|
|||||||
super.onMangaClick(manga)
|
super.onMangaClick(manga)
|
||||||
}
|
}
|
||||||
|
|
||||||
class MigrationDialog : DialogController() {
|
class MigrationDialog(bundle: Bundle) : DialogController(bundle) {
|
||||||
|
|
||||||
|
constructor(manga: Manga, newManga: Manga, callingController: Controller) : this(
|
||||||
|
bundleOf(
|
||||||
|
MANGA_KEY to manga,
|
||||||
|
NEW_MANGA_KEY to newManga
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
this.callingController = callingController
|
||||||
|
}
|
||||||
|
|
||||||
|
private val manga: Manga = args.getSerializable(MANGA_KEY) as Manga
|
||||||
|
private val newManga: Manga = args.getSerializable(NEW_MANGA_KEY) as Manga
|
||||||
|
private var callingController: Controller? = null
|
||||||
|
|
||||||
private val preferences: PreferencesHelper by injectLazy()
|
private val preferences: PreferencesHelper by injectLazy()
|
||||||
|
|
||||||
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
||||||
val prefValue = preferences.migrateFlags().get()
|
val prefValue = preferences.migrateFlags().get()
|
||||||
|
val callingController = callingController
|
||||||
|
|
||||||
val preselected =
|
val preselected =
|
||||||
MigrationFlags.getEnabledFlagsPositions(
|
MigrationFlags.getEnabledFlagsPositions(
|
||||||
@@ -132,7 +154,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()
|
||||||
@@ -145,13 +167,27 @@ 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)
|
||||||
}
|
}
|
||||||
|
companion object {
|
||||||
|
const val MANGA_KEY = "manga_key"
|
||||||
|
const val NEW_MANGA_KEY = "new_manga_key"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -183,4 +219,15 @@ class SearchController(
|
|||||||
}
|
}
|
||||||
.launchIn(viewScope)
|
.launchIn(viewScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onTitleClick(source: CatalogueSource) {
|
||||||
|
presenter.preferences.lastUsedSource().set(source.id)
|
||||||
|
|
||||||
|
router.pushController(SourceSearchController(manga!!, source, presenter.query).withFadeTransaction())
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val OLD_MANGA = "old_manga"
|
||||||
|
const val SOURCES = "sources"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+38
@@ -0,0 +1,38 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.browse.migration.search
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
|
import androidx.core.os.bundleOf
|
||||||
|
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, source: CatalogueSource, searchQuery: String? = null) : this(
|
||||||
|
bundleOf(
|
||||||
|
SOURCE_ID_KEY to source.id,
|
||||||
|
MANGA_KEY to manga,
|
||||||
|
SEARCH_QUERY_KEY to 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
+6
@@ -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.data.database.DatabaseHelper
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
@@ -43,6 +44,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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+9
-4
@@ -28,9 +28,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 /* SY --> */ && it != MERGED_SOURCE_ID /* SY <-- */) sourceManager.getOrStub(it) else null }
|
.groupBy { it.source }
|
||||||
.sortedBy { it.name.toLowerCase() }
|
.filterKeys { it != LocalSource.ID /* SY --> */ && it != MERGED_SOURCE_ID /* SY <-- */ }
|
||||||
.map { SourceItem(it, header) }.toList()
|
.map {
|
||||||
|
val source = sourceManager.getOrStub(it.key)
|
||||||
|
SourceItem(source, it.value.size, header)
|
||||||
|
}
|
||||||
|
.sortedBy { it.source.name.toLowerCase() }
|
||||||
|
.toList()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,8 +26,8 @@ class SourceHolder(view: View, val adapter: SourceAdapter) :
|
|||||||
fun bind(item: SourceItem) {
|
fun bind(item: SourceItem) {
|
||||||
val source = item.source
|
val source = item.source
|
||||||
|
|
||||||
binding.title.text = source.name
|
binding.title.text = "${source.name} (${item.mangaCount})"
|
||||||
binding.subtitle.isVisible = true
|
binding.subtitle.isVisible = source.lang != ""
|
||||||
binding.subtitle.text = LocaleHelper.getDisplayName(source.lang)
|
binding.subtitle.text = LocaleHelper.getDisplayName(source.lang)
|
||||||
|
|
||||||
itemView.post {
|
itemView.post {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -10,13 +10,13 @@ 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.os.bundleOf
|
import androidx.core.os.bundleOf
|
||||||
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
|
||||||
@@ -28,7 +28,7 @@ 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
|
||||||
@@ -39,12 +39,7 @@ import eu.kanade.tachiyomi.ui.browse.source.latest.LatestUpdatesController
|
|||||||
import eu.kanade.tachiyomi.ui.category.sources.ChangeSourceCategoriesDialog
|
import eu.kanade.tachiyomi.ui.category.sources.ChangeSourceCategoriesDialog
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
import exh.ui.smartsearch.SmartSearchController
|
import exh.ui.smartsearch.SmartSearchController
|
||||||
import kotlinx.coroutines.flow.filterIsInstance
|
|
||||||
import kotlinx.coroutines.flow.launchIn
|
|
||||||
import kotlinx.coroutines.flow.onEach
|
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
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
|
||||||
|
|
||||||
@@ -55,7 +50,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(bundle: Bundle? = null) :
|
class SourceController(bundle: Bundle? = null) :
|
||||||
NucleusController<SourceMainControllerBinding, SourcePresenter>(bundle),
|
SearchableNucleusController<SourceMainControllerBinding, SourcePresenter>(bundle),
|
||||||
FlexibleAdapter.OnItemClickListener,
|
FlexibleAdapter.OnItemClickListener,
|
||||||
FlexibleAdapter.OnItemLongClickListener,
|
FlexibleAdapter.OnItemLongClickListener,
|
||||||
SourceAdapter.OnSourceClickListener,
|
SourceAdapter.OnSourceClickListener,
|
||||||
@@ -102,6 +97,11 @@ class SourceController(bundle: Bundle? = null) :
|
|||||||
*/
|
*/
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,7 +203,7 @@ class SourceController(bundle: Bundle? = null) :
|
|||||||
|
|
||||||
items.add(
|
items.add(
|
||||||
Pair(
|
Pair(
|
||||||
activity.getString(R.string.label_categories),
|
activity.getString(R.string.categories),
|
||||||
{ addToCategories(item.source) }
|
{ addToCategories(item.source) }
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -333,44 +333,6 @@ class SourceController(bundle: Bundle? = null) :
|
|||||||
}
|
}
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
/**
|
|
||||||
* 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)
|
|
||||||
|
|
||||||
// SY -->
|
|
||||||
if (mode == Mode.SMART_SEARCH) {
|
|
||||||
menu.findItem(R.id.action_search).isVisible = false
|
|
||||||
menu.findItem(R.id.action_settings).isVisible = false
|
|
||||||
}
|
|
||||||
// SY <--
|
|
||||||
|
|
||||||
// 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.
|
||||||
*
|
*
|
||||||
@@ -431,6 +393,29 @@ class SourceController(bundle: Bundle? = null) :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||||
|
if (mode == Mode.CATALOGUE) {
|
||||||
|
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?) {
|
||||||
|
// SY -->
|
||||||
|
if (mode == Mode.CATALOGUE) {
|
||||||
|
parentController!!.router.pushController(
|
||||||
|
GlobalSearchController(query).withFadeTransaction()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// SY <--
|
||||||
|
}
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class SmartSearchConfig(val origTitle: String, val origMangaId: Long? = null) : Parcelable
|
data class SmartSearchConfig(val origTitle: String, val origMangaId: Long? = null) : Parcelable
|
||||||
|
|||||||
+16
-28
@@ -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
|
||||||
@@ -17,10 +16,10 @@ import androidx.recyclerview.widget.RecyclerView
|
|||||||
import com.afollestad.materialdialogs.MaterialDialog
|
import com.afollestad.materialdialogs.MaterialDialog
|
||||||
import com.afollestad.materialdialogs.input.input
|
import com.afollestad.materialdialogs.input.input
|
||||||
import com.afollestad.materialdialogs.list.listItems
|
import com.afollestad.materialdialogs.list.listItems
|
||||||
import com.elvishew.xlog.XLog
|
|
||||||
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
|
||||||
@@ -38,7 +37,7 @@ import eu.kanade.tachiyomi.source.online.HttpSource
|
|||||||
import eu.kanade.tachiyomi.source.online.LoginSource
|
import eu.kanade.tachiyomi.source.online.LoginSource
|
||||||
import eu.kanade.tachiyomi.source.online.all.MangaDex
|
import eu.kanade.tachiyomi.source.online.all.MangaDex
|
||||||
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.extension.details.SourcePreferencesController
|
import eu.kanade.tachiyomi.ui.browse.extension.details.SourcePreferencesController
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.SourceController
|
import eu.kanade.tachiyomi.ui.browse.source.SourceController
|
||||||
@@ -57,25 +56,22 @@ import eu.kanade.tachiyomi.util.view.shrinkOnScroll
|
|||||||
import eu.kanade.tachiyomi.util.view.snack
|
import eu.kanade.tachiyomi.util.view.snack
|
||||||
import eu.kanade.tachiyomi.widget.AutofitRecyclerView
|
import eu.kanade.tachiyomi.widget.AutofitRecyclerView
|
||||||
import eu.kanade.tachiyomi.widget.EmptyView
|
import eu.kanade.tachiyomi.widget.EmptyView
|
||||||
|
import exh.log.xLogW
|
||||||
import exh.md.similar.ui.EnableMangaDexSimilarDialogController
|
import exh.md.similar.ui.EnableMangaDexSimilarDialogController
|
||||||
import exh.savedsearches.EXHSavedSearch
|
import exh.savedsearches.EXHSavedSearch
|
||||||
import exh.source.getMainSource
|
import exh.source.getMainSource
|
||||||
import exh.source.isEhBasedSource
|
import exh.source.isEhBasedSource
|
||||||
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 uy.kohesive.injekt.injectLazy
|
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,
|
||||||
@@ -389,6 +385,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
|
||||||
|
|
||||||
@@ -401,25 +402,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() },
|
||||||
@@ -450,6 +434,10 @@ open class BrowseSourceController(bundle: Bundle) :
|
|||||||
// SY <--
|
// SY <--
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onSearchViewQueryTextSubmit(query: String?) {
|
||||||
|
searchWithQuery(query ?: "")
|
||||||
|
}
|
||||||
|
|
||||||
override fun onPrepareOptionsMenu(menu: Menu) {
|
override fun onPrepareOptionsMenu(menu: Menu) {
|
||||||
super.onPrepareOptionsMenu(menu)
|
super.onPrepareOptionsMenu(menu)
|
||||||
|
|
||||||
@@ -542,8 +530,8 @@ open class BrowseSourceController(bundle: Bundle) :
|
|||||||
*/
|
*/
|
||||||
/* SY --> */ open /* SY <-- */fun onAddPageError(error: Throwable) {
|
/* SY --> */ open /* SY <-- */fun onAddPageError(error: Throwable) {
|
||||||
// SY -->
|
// SY -->
|
||||||
XLog.tag("BrowseSourceController").enableStackTrace(2).w("> Failed to load next catalogue page!", error)
|
xLogW("> Failed to load next catalogue page!", error)
|
||||||
XLog.tag("BrowseSourceController").enableStackTrace(2).w(
|
xLogW(
|
||||||
"> (source.id: %s, source.name: %s)",
|
"> (source.id: %s, source.name: %s)",
|
||||||
presenter.source.id,
|
presenter.source.id,
|
||||||
presenter.source.name
|
presenter.source.name
|
||||||
|
|||||||
+4
-6
@@ -81,12 +81,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.
|
||||||
*/
|
*/
|
||||||
@@ -129,6 +123,10 @@ open class BrowseSourcePresenter(
|
|||||||
private val filterSerializer = FilterSerializer()
|
private val filterSerializer = FilterSerializer()
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
|
init {
|
||||||
|
query = searchQuery ?: ""
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedState: Bundle?) {
|
override fun onCreate(savedState: Bundle?) {
|
||||||
super.onCreate(savedState)
|
super.onCreate(savedState)
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import android.widget.AutoCompleteTextView
|
|||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.core.widget.addTextChangedListener
|
import androidx.core.widget.addTextChangedListener
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.elvishew.xlog.XLog
|
|
||||||
import com.google.android.material.chip.Chip
|
import com.google.android.material.chip.Chip
|
||||||
import com.google.android.material.chip.ChipGroup
|
import com.google.android.material.chip.ChipGroup
|
||||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||||
@@ -17,6 +16,7 @@ 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.AutoCompleteAdapter
|
import eu.kanade.tachiyomi.widget.AutoCompleteAdapter
|
||||||
|
import exh.log.xLogD
|
||||||
|
|
||||||
open class AutoComplete(val filter: Filter.AutoComplete) : AbstractFlexibleItem<AutoComplete.Holder>() {
|
open class AutoComplete(val filter: Filter.AutoComplete) : AbstractFlexibleItem<AutoComplete.Holder>() {
|
||||||
|
|
||||||
@@ -97,7 +97,7 @@ open class AutoComplete(val filter: Filter.AutoComplete) : AbstractFlexibleItem<
|
|||||||
addChipToGroup(name, holder)
|
addChipToGroup(name, holder)
|
||||||
filter.state += name
|
filter.state += name
|
||||||
} else {
|
} else {
|
||||||
XLog.tag("AutoComplete").d("Invalid tag: $name")
|
xLogD("Invalid tag: %s", name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
+39
-36
@@ -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
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -33,8 +29,9 @@ 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>(),
|
bundle: Bundle? = null
|
||||||
|
) : SearchableNucleusController<GlobalSearchControllerBinding, GlobalSearchPresenter>(bundle),
|
||||||
GlobalSearchCardAdapter.OnMangaClickListener,
|
GlobalSearchCardAdapter.OnMangaClickListener,
|
||||||
GlobalSearchAdapter.OnTitleClickListener {
|
GlobalSearchAdapter.OnTitleClickListener {
|
||||||
|
|
||||||
@@ -45,6 +42,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 +60,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 +107,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
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
-6
@@ -48,12 +48,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.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,140 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.ui.browse.source.index
|
|
||||||
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.core.view.isVisible
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
|
||||||
import eu.kanade.tachiyomi.databinding.IndexAdapterBinding
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adapter that holds the search cards.
|
|
||||||
*
|
|
||||||
* @param controller instance of [IndexController].
|
|
||||||
*/
|
|
||||||
class IndexAdapter(val controller: IndexController) :
|
|
||||||
RecyclerView.Adapter<IndexAdapter.ViewHolder>() {
|
|
||||||
|
|
||||||
val clickListener: ClickListener = controller
|
|
||||||
|
|
||||||
private lateinit var binding: IndexAdapterBinding
|
|
||||||
|
|
||||||
var holder: IndexAdapter.ViewHolder? = null
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): IndexAdapter.ViewHolder {
|
|
||||||
binding = IndexAdapterBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
|
||||||
return ViewHolder(binding.root)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: IndexAdapter.ViewHolder, position: Int) {
|
|
||||||
this.holder = holder
|
|
||||||
holder.bindBrowse(null)
|
|
||||||
holder.bindLatest(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
// stores and recycles views as they are scrolled off screen
|
|
||||||
inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
|
||||||
private val latestAdapter = IndexCardAdapter(controller)
|
|
||||||
private var latestLastBoundResults: List<IndexCardItem>? = null
|
|
||||||
|
|
||||||
private val browseAdapter = IndexCardAdapter(controller)
|
|
||||||
private var browseLastBoundResults: List<IndexCardItem>? = null
|
|
||||||
|
|
||||||
init {
|
|
||||||
binding.browseBarWrapper.setOnClickListener {
|
|
||||||
clickListener.onBrowseClick()
|
|
||||||
}
|
|
||||||
binding.latestBarWrapper.setOnClickListener {
|
|
||||||
clickListener.onLatestClick()
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.latestRecycler.layoutManager = LinearLayoutManager(itemView.context, LinearLayoutManager.HORIZONTAL, false)
|
|
||||||
binding.latestRecycler.adapter = latestAdapter
|
|
||||||
|
|
||||||
binding.browseRecycler.layoutManager = LinearLayoutManager(itemView.context, LinearLayoutManager.HORIZONTAL, false)
|
|
||||||
binding.browseRecycler.adapter = browseAdapter
|
|
||||||
}
|
|
||||||
|
|
||||||
fun bindLatest(latestResults: List<IndexCardItem>?) {
|
|
||||||
when {
|
|
||||||
latestResults == null -> {
|
|
||||||
binding.latestProgress.isVisible = true
|
|
||||||
showLatestResultsHolder()
|
|
||||||
}
|
|
||||||
latestResults.isEmpty() -> {
|
|
||||||
binding.latestProgress.isVisible = false
|
|
||||||
showLatestNoResults()
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
binding.latestProgress.isVisible = false
|
|
||||||
showLatestResultsHolder()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (latestResults !== latestLastBoundResults) {
|
|
||||||
latestAdapter.updateDataSet(latestResults)
|
|
||||||
latestLastBoundResults = latestResults
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun bindBrowse(browseResults: List<IndexCardItem>?) {
|
|
||||||
when {
|
|
||||||
browseResults == null -> {
|
|
||||||
binding.browseProgress.isVisible = true
|
|
||||||
showBrowseResultsHolder()
|
|
||||||
}
|
|
||||||
browseResults.isEmpty() -> {
|
|
||||||
binding.browseProgress.isVisible = false
|
|
||||||
showBrowseNoResults()
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
binding.browseProgress.isVisible = false
|
|
||||||
showBrowseResultsHolder()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (browseResults !== browseLastBoundResults) {
|
|
||||||
browseAdapter.updateDataSet(browseResults)
|
|
||||||
browseLastBoundResults = browseResults
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showLatestResultsHolder() {
|
|
||||||
binding.latestNoResultsFound.isVisible = false
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showLatestNoResults() {
|
|
||||||
binding.latestNoResultsFound.isVisible = true
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showBrowseResultsHolder() {
|
|
||||||
binding.browseNoResultsFound.isVisible = false
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showBrowseNoResults() {
|
|
||||||
binding.browseNoResultsFound.isVisible = true
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setLatestImage(manga: Manga) {
|
|
||||||
latestAdapter.allBoundViewHolders.forEach {
|
|
||||||
if (it !is IndexCardHolder) return@forEach
|
|
||||||
if (latestAdapter.getItem(it.bindingAdapterPosition)?.manga?.id != manga.id) return@forEach
|
|
||||||
it.setImage(manga)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fun setBrowseImage(manga: Manga) {
|
|
||||||
browseAdapter.allBoundViewHolders.forEach {
|
|
||||||
if (it !is IndexCardHolder) return@forEach
|
|
||||||
if (browseAdapter.getItem(it.bindingAdapterPosition)?.manga?.id != manga.id) return@forEach
|
|
||||||
it.setImage(manga)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ClickListener {
|
|
||||||
fun onBrowseClick(search: String? = null, filters: String? = null)
|
|
||||||
fun onLatestClick()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemCount(): Int = 1
|
|
||||||
}
|
|
||||||
@@ -6,33 +6,29 @@ import android.view.Menu
|
|||||||
import android.view.MenuInflater
|
import android.view.MenuInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.appcompat.widget.SearchView
|
|
||||||
import androidx.core.os.bundleOf
|
import androidx.core.os.bundleOf
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import com.afollestad.materialdialogs.MaterialDialog
|
import com.afollestad.materialdialogs.MaterialDialog
|
||||||
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||||
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.databinding.LatestControllerBinding
|
import eu.kanade.tachiyomi.databinding.IndexControllerBinding
|
||||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
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.browse.BrowseSourceController
|
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.browse.SourceFilterSheet
|
import eu.kanade.tachiyomi.ui.browse.source.browse.SourceFilterSheet
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.latest.LatestUpdatesController
|
import eu.kanade.tachiyomi.ui.browse.source.latest.LatestUpdatesController
|
||||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||||
import exh.util.nullIfBlank
|
import exh.util.nullIfBlank
|
||||||
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 kotlinx.serialization.json.buildJsonObject
|
import kotlinx.serialization.json.buildJsonObject
|
||||||
import reactivecircus.flowbinding.appcompat.QueryTextEvent
|
import reactivecircus.flowbinding.android.view.clicks
|
||||||
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
|
||||||
import xyz.nulldev.ts.api.http.serializer.FilterSerializer
|
import xyz.nulldev.ts.api.http.serializer.FilterSerializer
|
||||||
@@ -43,10 +39,9 @@ import xyz.nulldev.ts.api.http.serializer.FilterSerializer
|
|||||||
* [IndexCardAdapter.OnMangaClickListener] called when manga is clicked in global search
|
* [IndexCardAdapter.OnMangaClickListener] called when manga is clicked in global search
|
||||||
*/
|
*/
|
||||||
open class IndexController :
|
open class IndexController :
|
||||||
NucleusController<LatestControllerBinding, IndexPresenter>,
|
SearchableNucleusController<IndexControllerBinding, IndexPresenter>,
|
||||||
FabController,
|
FabController,
|
||||||
IndexCardAdapter.OnMangaClickListener,
|
IndexCardAdapter.OnMangaClickListener {
|
||||||
IndexAdapter.ClickListener {
|
|
||||||
|
|
||||||
constructor(source: CatalogueSource?) : super(
|
constructor(source: CatalogueSource?) : super(
|
||||||
bundleOf(
|
bundleOf(
|
||||||
@@ -65,13 +60,10 @@ open class IndexController :
|
|||||||
|
|
||||||
var source: CatalogueSource? = null
|
var source: CatalogueSource? = null
|
||||||
|
|
||||||
/**
|
private var latestAdapter: IndexCardAdapter? = null
|
||||||
* Adapter containing search results grouped by lang.
|
private var browseAdapter: IndexCardAdapter? = null
|
||||||
*/
|
|
||||||
protected var adapter: IndexAdapter? = null
|
|
||||||
|
|
||||||
private var actionFab: ExtendedFloatingActionButton? = null
|
private var actionFab: ExtendedFloatingActionButton? = null
|
||||||
private var actionFabScrollListener: RecyclerView.OnScrollListener? = null
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sheet containing filter items.
|
* Sheet containing filter items.
|
||||||
@@ -90,7 +82,7 @@ open class IndexController :
|
|||||||
* @return inflated view
|
* @return inflated view
|
||||||
*/
|
*/
|
||||||
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
|
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
|
||||||
binding = LatestControllerBinding.inflate(inflater)
|
binding = IndexControllerBinding.inflate(inflater)
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,35 +126,17 @@ open class IndexController :
|
|||||||
* @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(menu, inflater, R.menu.global_search, R.id.action_search)
|
||||||
inflater.inflate(R.menu.global_search, menu)
|
}
|
||||||
|
|
||||||
// Initialize search menu
|
override fun onSearchViewQueryTextSubmit(query: String?) {
|
||||||
val searchItem = menu.findItem(R.id.action_search)
|
onBrowseClick(query.nullIfBlank())
|
||||||
val searchView = searchItem.actionView as SearchView
|
}
|
||||||
searchView.maxWidth = Int.MAX_VALUE
|
|
||||||
|
|
||||||
val query = presenter.query
|
override fun onSearchViewQueryTextChange(newText: String?) {
|
||||||
if (query.isNotBlank()) {
|
if (router.backstack.lastOrNull()?.controller() == this) {
|
||||||
searchItem.expandActionView()
|
presenter.query = newText ?: ""
|
||||||
searchView.setQuery(query, true)
|
|
||||||
searchView.clearFocus()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
searchView.queryTextEvents()
|
|
||||||
.filter { router.backstack.lastOrNull()?.controller() == this@IndexController }
|
|
||||||
.onEach {
|
|
||||||
if (it is QueryTextEvent.QueryChanged) {
|
|
||||||
presenter.query = it.queryText.toString()
|
|
||||||
} else if (it is QueryTextEvent.QuerySubmitted) {
|
|
||||||
onBrowseClick(presenter.query.nullIfBlank())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.launchIn(viewScope)
|
|
||||||
|
|
||||||
searchItem.fixExpand(
|
|
||||||
onExpand = { invalidateMenuOnExpand() }
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -176,11 +150,39 @@ open class IndexController :
|
|||||||
// Prepare filter sheet
|
// Prepare filter sheet
|
||||||
initFilterSheet()
|
initFilterSheet()
|
||||||
|
|
||||||
adapter = IndexAdapter(this)
|
latestAdapter = IndexCardAdapter(this)
|
||||||
|
|
||||||
// Create recycler and set adapter.
|
binding.latestRecycler.layoutManager = LinearLayoutManager(view.context, LinearLayoutManager.HORIZONTAL, false)
|
||||||
binding.recycler.layoutManager = LinearLayoutManager(view.context)
|
binding.latestRecycler.adapter = latestAdapter
|
||||||
binding.recycler.adapter = adapter
|
|
||||||
|
browseAdapter = IndexCardAdapter(this)
|
||||||
|
|
||||||
|
binding.browseRecycler.layoutManager = LinearLayoutManager(view.context, LinearLayoutManager.HORIZONTAL, false)
|
||||||
|
binding.browseRecycler.adapter = browseAdapter
|
||||||
|
|
||||||
|
binding.latestBarWrapper.clicks()
|
||||||
|
.onEach {
|
||||||
|
onLatestClick()
|
||||||
|
}
|
||||||
|
.launchIn(viewScope)
|
||||||
|
|
||||||
|
binding.browseBarWrapper.clicks()
|
||||||
|
.onEach {
|
||||||
|
onBrowseClick()
|
||||||
|
}
|
||||||
|
.launchIn(viewScope)
|
||||||
|
|
||||||
|
presenter.latestItems
|
||||||
|
.onEach {
|
||||||
|
bind(it, true)
|
||||||
|
}
|
||||||
|
.launchIn(viewScope)
|
||||||
|
|
||||||
|
presenter.browseItems
|
||||||
|
.onEach {
|
||||||
|
bind(it, false)
|
||||||
|
}
|
||||||
|
.launchIn(viewScope)
|
||||||
|
|
||||||
presenter.getLatest()
|
presenter.getLatest()
|
||||||
}
|
}
|
||||||
@@ -261,28 +263,64 @@ open class IndexController :
|
|||||||
|
|
||||||
override fun cleanupFab(fab: ExtendedFloatingActionButton) {
|
override fun cleanupFab(fab: ExtendedFloatingActionButton) {
|
||||||
fab.setOnClickListener(null)
|
fab.setOnClickListener(null)
|
||||||
actionFabScrollListener?.let { binding.recycler.removeOnScrollListener(it) }
|
|
||||||
actionFab = null
|
actionFab = null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setLatestManga(results: List<IndexCardItem>?) {
|
private fun bind(results: List<IndexCardItem>?, isLatest: Boolean) {
|
||||||
adapter?.holder?.bindLatest(results)
|
val progress = if (isLatest) binding.latestProgress else binding.browseProgress
|
||||||
|
when {
|
||||||
|
results == null -> {
|
||||||
|
progress.isVisible = true
|
||||||
|
showResultsHolder(isLatest)
|
||||||
|
}
|
||||||
|
results.isEmpty() -> {
|
||||||
|
progress.isVisible = false
|
||||||
|
showNoResults(isLatest)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
progress.isVisible = false
|
||||||
|
showResultsHolder(isLatest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val adapter = if (isLatest) {
|
||||||
|
latestAdapter
|
||||||
|
} else {
|
||||||
|
browseAdapter
|
||||||
|
}
|
||||||
|
adapter?.updateDataSet(results)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setBrowseManga(results: List<IndexCardItem>?) {
|
fun onError(e: Exception, isLatest: Boolean) {
|
||||||
adapter?.holder?.bindBrowse(results)
|
e.message?.let {
|
||||||
|
val textView = if (isLatest) {
|
||||||
|
binding.latestNoResultsFound
|
||||||
|
} else {
|
||||||
|
binding.browseNoResultsFound
|
||||||
|
}
|
||||||
|
textView.text = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showResultsHolder(isLatest: Boolean) {
|
||||||
|
(if (isLatest) binding.latestNoResultsFound else binding.browseNoResultsFound).isVisible = false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showNoResults(isLatest: Boolean) {
|
||||||
|
(if (isLatest) binding.latestNoResultsFound else binding.browseNoResultsFound).isVisible = true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroyView(view: View) {
|
override fun onDestroyView(view: View) {
|
||||||
adapter = null
|
latestAdapter = null
|
||||||
|
browseAdapter = null
|
||||||
super.onDestroyView(view)
|
super.onDestroyView(view)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBrowseClick(search: String?, filters: String?) {
|
fun onBrowseClick(search: String? = null, filters: String? = null) {
|
||||||
router.replaceTopController(BrowseSourceController(presenter.source, search, filterList = filters).withFadeTransaction())
|
router.replaceTopController(BrowseSourceController(presenter.source, search, filterList = filters).withFadeTransaction())
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onLatestClick() {
|
private fun onLatestClick() {
|
||||||
router.replaceTopController(LatestUpdatesController(presenter.source).withFadeTransaction())
|
router.replaceTopController(LatestUpdatesController(presenter.source).withFadeTransaction())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -292,8 +330,14 @@ open class IndexController :
|
|||||||
* @param manga the initialized manga.
|
* @param manga the initialized manga.
|
||||||
*/
|
*/
|
||||||
fun onMangaInitialized(manga: Manga, isLatest: Boolean) {
|
fun onMangaInitialized(manga: Manga, isLatest: Boolean) {
|
||||||
if (isLatest) adapter?.holder?.setLatestImage(manga)
|
val adapter = if (isLatest) latestAdapter else browseAdapter
|
||||||
else adapter?.holder?.setBrowseImage(manga)
|
adapter ?: return
|
||||||
|
|
||||||
|
adapter.allBoundViewHolders.forEach {
|
||||||
|
if (it !is IndexCardHolder) return@forEach
|
||||||
|
if (adapter.getItem(it.bindingAdapterPosition)?.manga?.id != manga.id) return@forEach
|
||||||
|
it.setImage(manga)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import eu.kanade.tachiyomi.util.lang.withUIContext
|
|||||||
import exh.savedsearches.EXHSavedSearch
|
import exh.savedsearches.EXHSavedSearch
|
||||||
import exh.savedsearches.JsonSavedSearch
|
import exh.savedsearches.JsonSavedSearch
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.serialization.decodeFromString
|
import kotlinx.serialization.decodeFromString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
@@ -52,11 +53,6 @@ open class IndexPresenter(
|
|||||||
*/
|
*/
|
||||||
private var fetchSourcesSubscription: Subscription? = null
|
private var fetchSourcesSubscription: Subscription? = null
|
||||||
|
|
||||||
/**
|
|
||||||
* Query from the view.
|
|
||||||
*/
|
|
||||||
var query = ""
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Subject which fetches image of given manga.
|
* Subject which fetches image of given manga.
|
||||||
*/
|
*/
|
||||||
@@ -78,6 +74,14 @@ open class IndexPresenter(
|
|||||||
*/
|
*/
|
||||||
private var fetchImageSubscription: Subscription? = null
|
private var fetchImageSubscription: Subscription? = null
|
||||||
|
|
||||||
|
val latestItems = MutableStateFlow<List<IndexCardItem>?>(null)
|
||||||
|
|
||||||
|
val browseItems = MutableStateFlow<List<IndexCardItem>?>(null)
|
||||||
|
|
||||||
|
init {
|
||||||
|
query = ""
|
||||||
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
fetchSourcesSubscription?.unsubscribe()
|
fetchSourcesSubscription?.unsubscribe()
|
||||||
fetchImageSubscription?.unsubscribe()
|
fetchImageSubscription?.unsubscribe()
|
||||||
@@ -98,54 +102,43 @@ open class IndexPresenter(
|
|||||||
initializeFetchImageSubscription()
|
initializeFetchImageSubscription()
|
||||||
|
|
||||||
presenterScope.launch(Dispatchers.IO) {
|
presenterScope.launch(Dispatchers.IO) {
|
||||||
withUIContext {
|
if (latestItems.value != null) return@launch
|
||||||
Observable.just(null).subscribeLatestCache({ view, results ->
|
val results = if (source.supportsLatest) {
|
||||||
view.setLatestManga(results)
|
try {
|
||||||
})
|
|
||||||
}
|
|
||||||
if (source.supportsLatest) {
|
|
||||||
val results = try {
|
|
||||||
source.fetchLatestUpdates(1)
|
source.fetchLatestUpdates(1)
|
||||||
.awaitSingle()
|
.awaitSingle()
|
||||||
.mangas
|
.mangas
|
||||||
.take(10)
|
|
||||||
.map { networkToLocalManga(it, source.id) }
|
.map { networkToLocalManga(it, source.id) }
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
withUIContext {
|
||||||
|
view?.onError(e, true)
|
||||||
|
}
|
||||||
emptyList()
|
emptyList()
|
||||||
}
|
}
|
||||||
fetchImage(results, true)
|
} else emptyList()
|
||||||
|
|
||||||
withUIContext {
|
fetchImage(results, true)
|
||||||
Observable.just(results.map { IndexCardItem(it) }).subscribeLatestCache({ view, results ->
|
|
||||||
view.setLatestManga(results)
|
latestItems.value = results.map { IndexCardItem(it) }
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
presenterScope.launch(Dispatchers.IO) {
|
presenterScope.launch(Dispatchers.IO) {
|
||||||
withUIContext {
|
if (browseItems.value != null) return@launch
|
||||||
Observable.just(null).subscribeLatestCache({ view, results ->
|
|
||||||
view.setBrowseManga(results)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
val results = try {
|
val results = try {
|
||||||
source.fetchPopularManga(1)
|
source.fetchPopularManga(1)
|
||||||
.awaitSingle()
|
.awaitSingle()
|
||||||
.mangas
|
.mangas
|
||||||
.take(10)
|
|
||||||
.map { networkToLocalManga(it, source.id) }
|
.map { networkToLocalManga(it, source.id) }
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
withUIContext {
|
||||||
|
view?.onError(e, true)
|
||||||
|
}
|
||||||
emptyList()
|
emptyList()
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchImage(results, false)
|
fetchImage(results, false)
|
||||||
|
|
||||||
withUIContext {
|
browseItems.value = results.map { IndexCardItem(it) }
|
||||||
Observable.just(results.map { IndexCardItem(it) }).subscribeLatestCache({ view, results ->
|
|
||||||
view.setBrowseManga(results)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,10 +214,10 @@ open class IndexPresenter(
|
|||||||
|
|
||||||
fun loadSearches(): List<EXHSavedSearch> {
|
fun loadSearches(): List<EXHSavedSearch> {
|
||||||
val loaded = preferences.savedSearches().get()
|
val loaded = preferences.savedSearches().get()
|
||||||
return loaded.map {
|
return loaded.mapNotNull {
|
||||||
try {
|
try {
|
||||||
val id = it.substringBefore(':').toLong()
|
val id = it.substringBefore(':').toLong()
|
||||||
if (id != source.id) return@map null
|
if (id != source.id) return@mapNotNull null
|
||||||
val content = Json.decodeFromString<JsonSavedSearch>(it.substringAfter(':'))
|
val content = Json.decodeFromString<JsonSavedSearch>(it.substringAfter(':'))
|
||||||
val originalFilters = source.getFilterList()
|
val originalFilters = source.getFilterList()
|
||||||
filterSerializer.deserialize(originalFilters, content.filters)
|
filterSerializer.deserialize(originalFilters, content.filters)
|
||||||
@@ -239,6 +232,6 @@ open class IndexPresenter(
|
|||||||
t.printStackTrace()
|
t.printStackTrace()
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}.filterNotNull()
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+6
@@ -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 BiometricTimesController :
|
|||||||
*/
|
*/
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -5,9 +5,9 @@ import android.os.Bundle
|
|||||||
import com.afollestad.materialdialogs.MaterialDialog
|
import com.afollestad.materialdialogs.MaterialDialog
|
||||||
import com.afollestad.materialdialogs.datetime.timePicker
|
import com.afollestad.materialdialogs.datetime.timePicker
|
||||||
import com.bluelinelabs.conductor.Controller
|
import com.bluelinelabs.conductor.Controller
|
||||||
import com.elvishew.xlog.XLog
|
|
||||||
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 exh.log.xLogD
|
||||||
import java.util.Calendar
|
import java.util.Calendar
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
import kotlin.time.ExperimentalTime
|
import kotlin.time.ExperimentalTime
|
||||||
@@ -48,9 +48,9 @@ class BiometricTimesCreateDialog<T>(bundle: Bundle? = null) : DialogController(b
|
|||||||
.title(if (startTime == null) R.string.biometric_lock_start_time else R.string.biometric_lock_end_time)
|
.title(if (startTime == null) R.string.biometric_lock_start_time else R.string.biometric_lock_end_time)
|
||||||
.timePicker(show24HoursView = false) { _, datetime ->
|
.timePicker(show24HoursView = false) { _, datetime ->
|
||||||
val hour = datetime.get(Calendar.HOUR_OF_DAY)
|
val hour = datetime.get(Calendar.HOUR_OF_DAY)
|
||||||
XLog.disableStackTrace().d(hour)
|
xLogD(hour)
|
||||||
val minute = datetime.get(Calendar.MINUTE)
|
val minute = datetime.get(Calendar.MINUTE)
|
||||||
XLog.disableStackTrace().d(minute)
|
xLogD(minute)
|
||||||
if (hour !in 0..24 || minute !in 0..60) return@timePicker
|
if (hour !in 0..24 || minute !in 0..60) return@timePicker
|
||||||
if (startTime != null) {
|
if (startTime != null) {
|
||||||
endTime = hour.hours + minute.minutes
|
endTime = hour.hours + minute.minutes
|
||||||
|
|||||||
+3
-3
@@ -1,10 +1,10 @@
|
|||||||
package eu.kanade.tachiyomi.ui.category.biometric
|
package eu.kanade.tachiyomi.ui.category.biometric
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import com.elvishew.xlog.XLog
|
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.data.preference.plusAssign
|
import eu.kanade.tachiyomi.data.preference.plusAssign
|
||||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||||
|
import exh.log.xLogD
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
@@ -36,7 +36,7 @@ class BiometricTimesPresenter : BasePresenter<BiometricTimesController>() {
|
|||||||
|
|
||||||
preferences.biometricTimeRanges().asFlow().onEach { prefTimeRanges ->
|
preferences.biometricTimeRanges().asFlow().onEach { prefTimeRanges ->
|
||||||
timeRanges = prefTimeRanges.toList()
|
timeRanges = prefTimeRanges.toList()
|
||||||
.mapNotNull { TimeRange.fromPreferenceString(it) }.onEach { XLog.disableStackTrace().d(it) }
|
.mapNotNull { TimeRange.fromPreferenceString(it) }.onEach { xLogD(it) }
|
||||||
|
|
||||||
Observable.just(timeRanges)
|
Observable.just(timeRanges)
|
||||||
.map { it.map(::BiometricTimesItem) }
|
.map { it.map(::BiometricTimesItem) }
|
||||||
@@ -57,7 +57,7 @@ class BiometricTimesPresenter : BasePresenter<BiometricTimesController>() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
XLog.disableStackTrace().d(timeRange)
|
xLogD(timeRange)
|
||||||
|
|
||||||
preferences.biometricTimeRanges() += timeRange.toPreferenceString()
|
preferences.biometricTimeRanges() += timeRange.toPreferenceString()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import androidx.recyclerview.widget.RecyclerView
|
|||||||
import com.afollestad.materialdialogs.MaterialDialog
|
import com.afollestad.materialdialogs.MaterialDialog
|
||||||
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
|
||||||
@@ -81,6 +82,11 @@ class SortTagController :
|
|||||||
*/
|
*/
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -72,6 +73,11 @@ class RepoController :
|
|||||||
*/
|
*/
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -79,6 +79,6 @@ class RepoPresenter(
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val repoRegex = """^[a-zA-Z-_.]*?\/[a-zA-Z-_.]*?$""".toRegex()
|
val repoRegex = """^[a-zA-Z0-9-_.]*?\/[a-zA-Z0-9-_.]*?$""".toRegex()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -73,6 +74,11 @@ class SourceCategoryController :
|
|||||||
*/
|
*/
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user