Compare commits
272 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| be1e7f28ef | |||
| 4118b13e5b | |||
| 7e0f2950c1 | |||
| a8c4da9e2b | |||
| 72d315b6ed | |||
| b886f0a55a | |||
| 63fa1ee75e | |||
| 7d1ad7efb6 | |||
| 56400febd1 | |||
| aa56698dac | |||
| d37b24adb1 | |||
| d3778ac6e1 | |||
| e43777bba7 | |||
| 0b3209284a | |||
| a1be070e99 | |||
| eda47cd546 | |||
| 54d8748c58 | |||
| 77c17f2556 | |||
| d81e4158cb | |||
| 77061067ee | |||
| 707af702c1 | |||
| 1b0b98b140 | |||
| 2220b6a91d | |||
| 22b8f51fa3 | |||
| 08f0e515d5 | |||
| 28b4281683 | |||
| 676f716fcb | |||
| 665784a241 | |||
| 0d16609f95 | |||
| b46500c837 | |||
| fb5872ef51 | |||
| bc28e2d617 | |||
| de0c55117d | |||
| 936997b52e | |||
| 885c251fb4 | |||
| b8e907cea2 | |||
| f4c6b2e09c | |||
| 13f4bfa7bc | |||
| 780c1e68a6 | |||
| f10944521c | |||
| af5ebeca56 | |||
| 01ad3dc92b | |||
| 5c1423be86 | |||
| 382c23e0fd | |||
| 189b15fee6 | |||
| d3d937fe17 | |||
| 5af0e7e847 | |||
| 142bdd14b7 | |||
| 0483097fc3 | |||
| a3c26c63d4 | |||
| fbe10151f4 | |||
| 92fc5ea4a0 | |||
| d2b620f485 | |||
| 78aa57579d | |||
| b0a2d8908f | |||
| de36cd0626 | |||
| b322ecd34a | |||
| 540e234562 | |||
| f6be2c7a2a | |||
| c5df8725de | |||
| 738e2f7cf1 | |||
| 0ac56750c8 | |||
| 3fb1b4affa | |||
| 2602c49756 | |||
| d23b3c82ba | |||
| 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 |
@@ -1,2 +1 @@
|
||||
github: inorichi
|
||||
ko_fi: inorichi
|
||||
|
||||
@@ -2,9 +2,15 @@
|
||||
|
||||
I acknowledge that:
|
||||
|
||||
- I have updated to the latest version of the app (stable is v1.5.0)
|
||||
- I have updated all extensions
|
||||
- I have updated:
|
||||
- To the latest version of the app (stable is v1.6.1)
|
||||
- All extensions
|
||||
- I have tried the troubleshooting guide: https://tachiyomi.org/help/guides/troubleshooting-problems/
|
||||
- If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions
|
||||
- I have searched the existing issues and this is new ticket **NOT** a duplicate or related to another open issue
|
||||
- I will fill out the title and the information in this template
|
||||
|
||||
Note that the issue will be automatically closed if you do not fill out the title or requested information.
|
||||
|
||||
**DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT**
|
||||
|
||||
@@ -24,3 +30,5 @@ I acknowledge that:
|
||||
|
||||
## Other details
|
||||
Additional details and attachments.
|
||||
|
||||
If you're experiencing crashes, share the crash logs from More → Settings → Advanced → Dump crash logs.
|
||||
|
||||
@@ -9,9 +9,15 @@ labels: "bug"
|
||||
|
||||
I acknowledge that:
|
||||
|
||||
- I have updated to the latest version of the app (stable is v1.5.0)
|
||||
- I have updated all extensions
|
||||
- I have updated:
|
||||
- To the latest version of the app (stable is v1.6.1)
|
||||
- All extensions
|
||||
- I have tried the troubleshooting guide: https://tachiyomi.org/help/guides/troubleshooting-problems/
|
||||
- If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions
|
||||
- I have searched the existing issues and this is new ticket **NOT** a duplicate or related to another open issue
|
||||
- I will fill out the title and the information in this template
|
||||
|
||||
Note that the issue will be automatically closed if you do not fill out the title or requested information.
|
||||
|
||||
**DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT**
|
||||
|
||||
@@ -34,3 +40,5 @@ This happened instead.
|
||||
|
||||
## Other details
|
||||
Additional details and attachments.
|
||||
|
||||
If you're experiencing crashes, share the crash logs from More → Settings → Advanced → Dump crash logs.
|
||||
|
||||
@@ -9,9 +9,14 @@ labels: "feature"
|
||||
|
||||
I acknowledge that:
|
||||
|
||||
- I have updated to the latest version of the app (stable is v1.5.0)
|
||||
- I have updated all extensions
|
||||
- I have updated:
|
||||
- To the latest version of the app (stable is v1.6.1)
|
||||
- All extensions
|
||||
- If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions
|
||||
- I have searched the existing issues and this is new ticket **NOT** a duplicate or related to another open issue
|
||||
- I will fill out the title and the information in this template
|
||||
|
||||
Note that the issue will be automatically closed if you do not fill out the title or requested information.
|
||||
|
||||
**DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT**
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.7 MiB After Width: | Height: | Size: 489 KiB |
@@ -7,31 +7,30 @@ jobs:
|
||||
autoclose:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Autoclose when created in wrong repo
|
||||
uses: arkon/issue-closer-action@v1.1
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
type: title
|
||||
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
|
||||
uses: arkon/issue-closer-action@v1.1
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
type: title
|
||||
regex: ".*<Write short description here>*"
|
||||
message: "@${issue.user.login} this issue was automatically closed because you did not fill out the description in the title."
|
||||
- name: Autoclose when body acknowledgement section not removed
|
||||
uses: arkon/issue-closer-action@v1.1
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
type: body
|
||||
regex: ".*DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT.*"
|
||||
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
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
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."
|
||||
- name: Autoclose issues
|
||||
uses: arkon/issue-closer-action@v3.0
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
rules: |
|
||||
[
|
||||
{
|
||||
"type": "title",
|
||||
"regex": ".*THIS ISSUE IS IN THE WRONG REPO.*",
|
||||
"message": "It was not opened in the correct repo, as the template mentioned."
|
||||
},
|
||||
{
|
||||
"type": "title",
|
||||
"regex": ".*<Write short description here>*",
|
||||
"message": "The description in the title was not filled out."
|
||||
},
|
||||
{
|
||||
"type": "body",
|
||||
"regex": ".*DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT.*",
|
||||
"message": "The acknowledgment section was not removed."
|
||||
},
|
||||
{
|
||||
"type": "body",
|
||||
"regex": ".*\\* (Tachiyomi version|Android version|Device): \\?.*",
|
||||
"message": "Requested information in the template 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 |
|
||||
|-------|----------|----------|
|
||||
| [](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
|
||||
@@ -11,7 +11,7 @@ Tachiyomi is a free and open source manga reader for Android 5.0 and above. This
|
||||
## Features
|
||||
|
||||
Features of Tachiyomi(original) include:
|
||||
* Online reading from sources such as MangaDex, MangaSee, Mangakakalot, [and more](https://github.com/tachiyomiorg/tachiyomi-extensions)
|
||||
* Online reading from [a variety of sources](https://github.com/tachiyomiorg/tachiyomi-extensions)
|
||||
* Local reading of downloaded manga
|
||||
* A configurable reader with multiple viewers, reading directions and other settings.
|
||||
* [MyAnimeList](https://myanimelist.net/), [AniList](https://anilist.co/), [Kitsu](https://kitsu.io/), [Shikimori](https://shikimori.one), and [Bangumi](https://bgm.tv/) support
|
||||
@@ -109,7 +109,12 @@ Source requests should be created at https://github.com/tachiyomiorg/tachiyomi-e
|
||||
|
||||
<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>
|
||||
|
||||
## FAQ
|
||||
|
||||
+29
-31
@@ -34,8 +34,8 @@ android {
|
||||
minSdkVersion(AndroidConfig.minSdk)
|
||||
targetSdkVersion(AndroidConfig.targetSdk)
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
versionCode = 13
|
||||
versionName = "1.5.0"
|
||||
versionCode = 15
|
||||
versionName = "1.6.1"
|
||||
|
||||
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
||||
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
|
||||
@@ -95,6 +95,7 @@ android {
|
||||
exclude("META-INF/LICENSE")
|
||||
exclude("META-INF/LICENSE.txt")
|
||||
exclude("META-INF/NOTICE")
|
||||
exclude("META-INF/*.kotlin_module")
|
||||
|
||||
// Compatibility for two RxJava versions (EXH)
|
||||
exclude("META-INF/rxjava.properties")
|
||||
@@ -126,20 +127,20 @@ dependencies {
|
||||
implementation("tachiyomi.sourceapi:source-api:1.1")
|
||||
|
||||
// AndroidX libraries
|
||||
implementation("androidx.annotation:annotation:1.2.0-beta01")
|
||||
implementation("androidx.appcompat:appcompat:1.3.0-beta01")
|
||||
implementation("androidx.biometric:biometric-ktx:1.2.0-alpha02")
|
||||
implementation("androidx.annotation:annotation:1.3.0-alpha01")
|
||||
implementation("androidx.appcompat:appcompat:1.3.0-rc01")
|
||||
implementation("androidx.biometric:biometric-ktx:1.2.0-alpha03")
|
||||
implementation("androidx.browser:browser:1.3.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.core:core-ktx:1.5.0-beta01")
|
||||
implementation("androidx.core:core-ktx:1.3.2")
|
||||
implementation("androidx.multidex:multidex:2.0.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")
|
||||
|
||||
val lifecycleVersion = "2.3.0-rc01"
|
||||
val lifecycleVersion = "2.3.0"
|
||||
implementation("androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion")
|
||||
implementation("androidx.lifecycle:lifecycle-process:$lifecycleVersion")
|
||||
implementation("androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion")
|
||||
@@ -150,7 +151,7 @@ dependencies {
|
||||
// UI library
|
||||
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
|
||||
implementation("io.reactivex:rxandroid:1.2.1")
|
||||
@@ -159,7 +160,7 @@ dependencies {
|
||||
implementation("com.github.pwittchen:reactivenetwork:0.13.0")
|
||||
|
||||
// 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:logging-interceptor:$okhttpVersion")
|
||||
implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:$okhttpVersion")
|
||||
@@ -169,7 +170,7 @@ dependencies {
|
||||
implementation("org.conscrypt:conscrypt-android:2.5.1")
|
||||
|
||||
// JSON
|
||||
val kotlinSerializationVersion = "1.0.1"
|
||||
val kotlinSerializationVersion = "1.1.0"
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinSerializationVersion")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-protobuf:$kotlinSerializationVersion")
|
||||
implementation("com.google.code.gson:gson:2.8.6")
|
||||
@@ -180,7 +181,7 @@ dependencies {
|
||||
|
||||
// Disk
|
||||
implementation("com.jakewharton:disklrucache:2.0.2")
|
||||
implementation("com.github.inorichi:unifile:e9ee588")
|
||||
implementation("com.github.tachiyomiorg:unifile:17bec43")
|
||||
implementation("com.github.junrar:junrar:7.4.0")
|
||||
|
||||
// HTML parser
|
||||
@@ -193,7 +194,7 @@ dependencies {
|
||||
implementation("io.requery:sqlite-android:3.33.0")
|
||||
|
||||
// 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
|
||||
val nucleusVersion = "3.0.0"
|
||||
@@ -204,14 +205,12 @@ dependencies {
|
||||
implementation("com.github.inorichi.injekt:injekt-core:65b0440")
|
||||
|
||||
// Image library
|
||||
val glideVersion = "4.11.0"
|
||||
val glideVersion = "4.12.0"
|
||||
implementation("com.github.bumptech.glide:glide:$glideVersion")
|
||||
implementation("com.github.bumptech.glide:okhttp3-integration:$glideVersion")
|
||||
kapt("com.github.bumptech.glide:compiler:$glideVersion")
|
||||
|
||||
implementation("com.github.tachiyomiorg:subsampling-scale-image-view:6caf219")
|
||||
// TODO: switch to new decoder for stable releases
|
||||
// implementation("com.github.tachiyomiorg:subsampling-scale-image-view:ca26317")
|
||||
implementation("com.github.tachiyomiorg:subsampling-scale-image-view:547d9c0")
|
||||
|
||||
// Logging
|
||||
implementation("com.jakewharton.timber:timber:4.7.1")
|
||||
@@ -229,7 +228,8 @@ dependencies {
|
||||
implementation("eu.davidea:flexible-adapter-ui:1.0.0")
|
||||
implementation("com.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.1.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
|
||||
val materialDialogsVersion = "3.1.1"
|
||||
@@ -242,7 +242,7 @@ dependencies {
|
||||
implementation("com.bluelinelabs:conductor-support:2.1.5") {
|
||||
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
|
||||
val flowbindingVersion = "0.12.0"
|
||||
@@ -256,7 +256,7 @@ dependencies {
|
||||
implementation("com.mikepenz:aboutlibraries:${BuildPluginsVersion.ABOUTLIB_PLUGIN}")
|
||||
|
||||
// Tests
|
||||
testImplementation("junit:junit:4.13.1")
|
||||
testImplementation("junit:junit:4.13.2")
|
||||
testImplementation("org.assertj:assertj-core:3.16.1")
|
||||
testImplementation("org.mockito:mockito-core:1.10.19")
|
||||
|
||||
@@ -267,12 +267,12 @@ dependencies {
|
||||
|
||||
implementation(kotlin("reflect", version = BuildPluginsVersion.KOTLIN))
|
||||
|
||||
val coroutinesVersion = "1.4.2"
|
||||
val coroutinesVersion = "1.4.3"
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion")
|
||||
|
||||
// For detecting memory leaks; see https://square.github.io/leakcanary/
|
||||
// debugImplementation("com.squareup.leakcanary:leakcanary-android:2.6")
|
||||
// debugImplementation("com.squareup.leakcanary:leakcanary-android:2.7")
|
||||
|
||||
// SY -->
|
||||
// [EXH] Android 7 SSL Workaround
|
||||
@@ -285,11 +285,11 @@ dependencies {
|
||||
implementation ("info.debatty:java-string-similarity:2.0.0")
|
||||
|
||||
// Firebase (EH)
|
||||
implementation("com.google.firebase:firebase-analytics-ktx:18.0.0")
|
||||
implementation("com.google.firebase:firebase-crashlytics-ktx:17.3.0")
|
||||
implementation("com.google.firebase:firebase-analytics-ktx:18.0.3")
|
||||
implementation("com.google.firebase:firebase-crashlytics-ktx:17.4.1")
|
||||
|
||||
// Better logging (EH)
|
||||
implementation("com.elvishew:xlog:1.7.1")
|
||||
implementation("com.elvishew:xlog:1.9.0")
|
||||
|
||||
// Debug utils (EH)
|
||||
val debugOverlayVersion = "1.1.3"
|
||||
@@ -302,12 +302,10 @@ dependencies {
|
||||
implementation ("me.zhanghai.android.materialratingbar:library:1.4.0")
|
||||
|
||||
// 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.3.4")
|
||||
// SY -->
|
||||
implementation("com.mikepenz:fastadapter:5.4.0")
|
||||
// SY <--
|
||||
}
|
||||
|
||||
tasks {
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
|
||||
<!-- For managing extensions -->
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||
@@ -32,7 +33,7 @@
|
||||
android:largeHeap="true"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:theme="@style/Theme.Tachiyomi.Light"
|
||||
android:theme="@style/Theme.Base"
|
||||
android:networkSecurityConfig="@xml/network_security_config">
|
||||
<activity
|
||||
android:name=".ui.main.MainActivity"
|
||||
@@ -84,7 +85,7 @@
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ui.security.BiometricUnlockActivity"
|
||||
android:theme="@style/Theme.Splash" />
|
||||
android:theme="@style/Theme.Base" />
|
||||
<activity
|
||||
android:name=".ui.webview.WebViewActivity"
|
||||
android:configChanges="uiMode|orientation|screenSize" />
|
||||
@@ -149,6 +150,10 @@
|
||||
android:name=".extension.util.ExtensionInstallActivity"
|
||||
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
|
||||
|
||||
<activity
|
||||
android:name="exh.ui.login.EhLoginActivity"
|
||||
android:label="EHentaiLogin" />
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.provider"
|
||||
@@ -195,7 +200,7 @@
|
||||
<activity
|
||||
android:name="exh.ui.intercept.InterceptActivity"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/Theme.EHActivity">
|
||||
android:theme="@style/Theme.Base">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
@@ -315,7 +320,7 @@
|
||||
android:scheme="https" />
|
||||
|
||||
<!-- MangaDex -->
|
||||
<data
|
||||
<!--<data
|
||||
android:scheme="https"
|
||||
android:host="www.mangadex.org"
|
||||
android:pathPrefix="/manga/" />
|
||||
@@ -364,12 +369,12 @@
|
||||
<data
|
||||
android:scheme="https"
|
||||
android:host="www.mangadex.cc"
|
||||
android:pathPrefix="/chapter/" />
|
||||
android:pathPrefix="/chapter/" />-->
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name="exh.ui.captcha.BrowserActionActivity"
|
||||
android:theme="@style/Theme.EHActivity" />
|
||||
android:theme="@style/Theme.Base" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
||||
@@ -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.GooglePlayServicesRepairableException
|
||||
import com.google.android.gms.security.ProviderInstaller
|
||||
import com.google.firebase.analytics.FirebaseAnalytics
|
||||
import com.google.firebase.analytics.ktx.analytics
|
||||
import com.google.firebase.ktx.Firebase
|
||||
import com.ms_square.debugoverlay.DebugOverlay
|
||||
@@ -36,11 +35,11 @@ import exh.log.CrashlyticsPrinter
|
||||
import exh.log.EHDebugModeOverlay
|
||||
import exh.log.EHLogLevel
|
||||
import exh.log.EnhancedFilePrinter
|
||||
import exh.log.XLogTree
|
||||
import exh.log.xLogD
|
||||
import exh.log.xLogE
|
||||
import exh.syDebugVersion
|
||||
import io.realm.Realm
|
||||
import io.realm.RealmConfiguration
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.conscrypt.Conscrypt
|
||||
import timber.log.Timber
|
||||
import uy.kohesive.injekt.Injekt
|
||||
@@ -51,7 +50,6 @@ import java.security.Security
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
import javax.net.ssl.SSLContext
|
||||
import kotlin.concurrent.thread
|
||||
import kotlin.time.ExperimentalTime
|
||||
import kotlin.time.days
|
||||
|
||||
@@ -59,12 +57,11 @@ open class App : Application(), LifecycleObserver {
|
||||
|
||||
private val preferences: PreferencesHelper by injectLazy()
|
||||
|
||||
private lateinit var firebaseAnalytics: FirebaseAnalytics
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
|
||||
// if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
|
||||
setupExhLogging() // EXH logging
|
||||
Timber.plant(XLogTree()) // SY Redirect Timber to XLog
|
||||
if (!BuildConfig.DEBUG) addAnalytics()
|
||||
|
||||
workaroundAndroid7BrokenSSL()
|
||||
@@ -78,7 +75,6 @@ open class App : Application(), LifecycleObserver {
|
||||
|
||||
setupNotificationChannels()
|
||||
Realm.init(this)
|
||||
GlobalScope.launch { deleteOldMetadataRealm() } // Delete old metadata DB (EH)
|
||||
if ((BuildConfig.DEBUG || BuildConfig.BUILD_TYPE == "releaseTest") && DebugToggles.ENABLE_DEBUG_OVERLAY.enabled) {
|
||||
setupDebugOverlay()
|
||||
}
|
||||
@@ -105,23 +101,22 @@ open class App : Application(), LifecycleObserver {
|
||||
try {
|
||||
SSLContext.getInstance("TLSv1.2")
|
||||
} 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 {
|
||||
ProviderInstaller.installIfNeeded(applicationContext)
|
||||
} 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) {
|
||||
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() {
|
||||
firebaseAnalytics = Firebase.analytics
|
||||
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)
|
||||
}
|
||||
|
||||
// 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
|
||||
private fun setupExhLogging() {
|
||||
EHLogLevel.init(this)
|
||||
|
||||
val logLevel = when {
|
||||
EHLogLevel.shouldLog(EHLogLevel.EXTRA) -> LogLevel.ALL
|
||||
BuildConfig.DEBUG -> LogLevel.DEBUG
|
||||
EHLogLevel.shouldLog(EHLogLevel.EXTREME) -> LogLevel.ALL
|
||||
EHLogLevel.shouldLog(EHLogLevel.EXTRA) || BuildConfig.DEBUG -> LogLevel.DEBUG
|
||||
else -> LogLevel.WARN
|
||||
}
|
||||
|
||||
@@ -188,9 +160,8 @@ open class App : Application(), LifecycleObserver {
|
||||
|
||||
@OptIn(ExperimentalTime::class)
|
||||
printers += EnhancedFilePrinter
|
||||
.Builder(logFolder.absolutePath)
|
||||
.fileNameGenerator(
|
||||
object : DateFileNameGenerator() {
|
||||
.Builder(logFolder.absolutePath) {
|
||||
fileNameGenerator = object : DateFileNameGenerator() {
|
||||
override fun generateFileName(logLevel: Int, timestamp: Long): String {
|
||||
return super.generateFileName(
|
||||
logLevel,
|
||||
@@ -198,13 +169,12 @@ open class App : Application(), LifecycleObserver {
|
||||
) + "-${BuildConfig.BUILD_TYPE}.log"
|
||||
}
|
||||
}
|
||||
)
|
||||
.flattener { timeMillis, level, tag, message ->
|
||||
"${dateFormat.format(timeMillis)} ${LogLevel.getShortLevelName(level)}/$tag: $message"
|
||||
flattener { timeMillis, 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
|
||||
if (!BuildConfig.DEBUG) {
|
||||
@@ -216,8 +186,8 @@ open class App : Application(), LifecycleObserver {
|
||||
*printers.toTypedArray()
|
||||
)
|
||||
|
||||
XLog.tag("Init").d("Application booting...")
|
||||
XLog.tag("Init").disableStackTrace().d(
|
||||
xLogD("Application booting...")
|
||||
xLogD(
|
||||
"App version: ${BuildConfig.VERSION_NAME} (${BuildConfig.FLAVOR}, ${BuildConfig.COMMIT_SHA}, ${BuildConfig.VERSION_CODE})\n" +
|
||||
"Preview build: $syDebugVersion\n" +
|
||||
"Android version: ${Build.VERSION.RELEASE} (SDK ${Build.VERSION.SDK_INT}) \n" +
|
||||
@@ -242,7 +212,7 @@ open class App : Application(), LifecycleObserver {
|
||||
.install()
|
||||
} catch (e: IllegalStateException) {
|
||||
// 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.updater.UpdaterJob
|
||||
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.util.system.toast
|
||||
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
|
||||
@@ -129,6 +130,17 @@ object Migrations {
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -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.Manga
|
||||
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.track.TrackManager
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
@@ -23,6 +24,10 @@ abstract class AbstractBackupManager(protected val context: Context) {
|
||||
internal val trackManager: TrackManager 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?
|
||||
|
||||
/**
|
||||
|
||||
@@ -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.Manga
|
||||
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.source.Source
|
||||
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 trackManager: TrackManager by injectLazy()
|
||||
|
||||
// SY -->
|
||||
protected val customMangaManager: CustomMangaManager by injectLazy()
|
||||
// SY <--
|
||||
|
||||
var job: Job? = null
|
||||
|
||||
protected lateinit var backupManager: T
|
||||
|
||||
@@ -30,7 +30,12 @@ class BackupCreateService : Service() {
|
||||
internal const val BACKUP_HISTORY_MASK = 0x4
|
||||
internal const val BACKUP_TRACK = 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.
|
||||
|
||||
@@ -24,6 +24,7 @@ class BackupNotifier(private val context: Context) {
|
||||
setSmallIcon(R.drawable.ic_tachi)
|
||||
setAutoCancel(false)
|
||||
setOngoing(true)
|
||||
setOnlyAlertOnce(true)
|
||||
}
|
||||
|
||||
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))
|
||||
|
||||
setProgress(0, 0, true)
|
||||
setOnlyAlertOnce(true)
|
||||
}
|
||||
|
||||
builder.show(Notifications.ID_BACKUP_PROGRESS)
|
||||
@@ -141,7 +141,7 @@ class BackupNotifier(private val context: Context) {
|
||||
|
||||
addAction(
|
||||
R.drawable.ic_folder_24dp,
|
||||
context.getString(R.string.action_open_log),
|
||||
context.getString(R.string.action_show_errors),
|
||||
NotificationReceiver.openErrorLogPendingActivity(context, uri)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -43,12 +43,11 @@ class BackupRestoreService : Service() {
|
||||
* @param context context of application
|
||||
* @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)) {
|
||||
val intent = Intent(context, BackupRestoreService::class.java).apply {
|
||||
putExtra(BackupConst.EXTRA_URI, uri)
|
||||
putExtra(BackupConst.EXTRA_MODE, mode)
|
||||
online?.let { putExtra(BackupConst.EXTRA_TYPE, it) }
|
||||
}
|
||||
ContextCompat.startForegroundService(context, intent)
|
||||
}
|
||||
@@ -119,13 +118,12 @@ class BackupRestoreService : Service() {
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
val uri = intent?.getParcelableExtra<Uri>(BackupConst.EXTRA_URI) ?: return START_NOT_STICKY
|
||||
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.
|
||||
backupRestore?.job?.cancel()
|
||||
|
||||
backupRestore = when (mode) {
|
||||
BackupConst.BACKUP_TYPE_FULL -> FullBackupRestore(this, notifier, online)
|
||||
BackupConst.BACKUP_TYPE_FULL -> FullBackupRestore(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_CHAPTER
|
||||
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_MASK
|
||||
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.MangaCategory
|
||||
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.all.MergedSource
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import exh.metadata.metadata.base.getFlatMetadataForManga
|
||||
import exh.metadata.metadata.base.insertFlatMetadataAsync
|
||||
import exh.savedsearches.JsonSavedSearch
|
||||
@@ -164,7 +163,7 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
||||
*/
|
||||
private fun backupMangaObject(manga: Manga, options: Int): BackupManga {
|
||||
// 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 -->
|
||||
if (manga.source == MERGED_SOURCE_ID) {
|
||||
@@ -237,24 +236,13 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
||||
/**
|
||||
* Fetches manga information
|
||||
*
|
||||
* @param source source of manga
|
||||
* @param manga manga that needs updating
|
||||
* @return Updated manga info.
|
||||
*/
|
||||
suspend fun restoreMangaFetch(source: Source?, manga: Manga, online: Boolean): Manga {
|
||||
return if (online && source != null /* SY --> */ && source !is MergedSource /* SY <-- */) {
|
||||
val networkManga = source.getMangaDetails(manga.toMangaInfo())
|
||||
manga.also {
|
||||
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)
|
||||
}
|
||||
fun restoreManga(manga: Manga): Manga {
|
||||
return 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>()
|
||||
|
||||
tracks.forEach { track ->
|
||||
val service = trackManager.getService(track.sync_id)
|
||||
if (service != null && service.isLogged) {
|
||||
var isInDatabase = false
|
||||
for (dbTrack in dbTracks) {
|
||||
if (track.sync_id == dbTrack.sync_id) {
|
||||
// The sync is already in the db, only update its fields
|
||||
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
|
||||
var isInDatabase = false
|
||||
for (dbTrack in dbTracks) {
|
||||
if (track.sync_id == dbTrack.sync_id) {
|
||||
// The sync is already in the db, only update its fields
|
||||
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 (!isInDatabase) {
|
||||
// Insert new sync. Let the db assign the id
|
||||
track.id = null
|
||||
trackToUpdate.add(track)
|
||||
}
|
||||
}
|
||||
if (!isInDatabase) {
|
||||
// Insert new sync. Let the db assign the id
|
||||
track.id = null
|
||||
trackToUpdate.add(track)
|
||||
}
|
||||
}
|
||||
// Update database
|
||||
@@ -394,47 +379,7 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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>) {
|
||||
internal fun restoreChaptersForManga(manga: Manga, chapters: List<Chapter>) {
|
||||
val dbChapters = databaseHelper.getChapters(manga).executeAsBlocking()
|
||||
|
||||
chapters.forEach { chapter ->
|
||||
@@ -527,8 +472,9 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
||||
}
|
||||
}
|
||||
|
||||
internal suspend fun restoreFlatMetadata(manga: Manga, backupFlatMetadata: BackupFlatMetadata) {
|
||||
manga.id?.let { mangaId ->
|
||||
internal fun restoreFlatMetadata(manga: Manga, backupFlatMetadata: BackupFlatMetadata) {
|
||||
val mangaId = manga.id ?: return
|
||||
launchIO {
|
||||
databaseHelper.getFlatMetadataForManga(mangaId).executeOnIO().let {
|
||||
if (it == null) {
|
||||
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.Manga
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.online.all.MergedSource
|
||||
import eu.kanade.tachiyomi.data.library.CustomMangaManager
|
||||
import exh.EXHMigrations
|
||||
import exh.source.MERGED_SOURCE_ID
|
||||
import okio.buffer
|
||||
@@ -24,7 +23,7 @@ import okio.gzip
|
||||
import okio.source
|
||||
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 {
|
||||
// SY -->
|
||||
@@ -57,9 +56,11 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier, private val
|
||||
return false
|
||||
}
|
||||
|
||||
restoreManga(it, backup.backupCategories, online)
|
||||
restoreManga(it, backup.backupCategories)
|
||||
}
|
||||
|
||||
// TODO: optionally trigger online library + tracker update
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -81,8 +82,8 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier, private val
|
||||
}
|
||||
// SY <--
|
||||
|
||||
private suspend fun restoreManga(backupManga: BackupManga, backupCategories: List<BackupCategory>, online: Boolean) {
|
||||
var manga = backupManga.getMangaImpl()
|
||||
private fun restoreManga(backupManga: BackupManga, backupCategories: List<BackupCategory>) {
|
||||
val manga = backupManga.getMangaImpl()
|
||||
val chapters = backupManga.getChaptersImpl()
|
||||
val categories = backupManga.categories
|
||||
val history = backupManga.history
|
||||
@@ -90,22 +91,17 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier, private val
|
||||
// SY -->
|
||||
val mergedMangaReferences = backupManga.mergedMangaReferences
|
||||
val flatMetadata = backupManga.flatMetadata
|
||||
val customManga = backupManga.getCustomMangaInfo()
|
||||
// SY <--
|
||||
|
||||
// SY -->
|
||||
manga = EXHMigrations.migrateBackupEntry(manga)
|
||||
EXHMigrations.migrateBackupEntry(manga)
|
||||
// SY <--
|
||||
|
||||
val source = backupManager.sourceManager.get(manga.source)
|
||||
val sourceName = sourceMapping[manga.source] ?: manga.source.toString()
|
||||
|
||||
try {
|
||||
if (source != null || !online) {
|
||||
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)}")
|
||||
}
|
||||
restoreMangaData(manga, chapters, categories, history, tracks, backupCategories/* SY --> */, mergedMangaReferences, flatMetadata, customManga/* SY <-- */)
|
||||
} catch (e: Exception) {
|
||||
val sourceName = sourceMapping[manga.source] ?: manga.source.toString()
|
||||
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
|
||||
*
|
||||
* @param manga manga data from json
|
||||
* @param source source to get manga data from
|
||||
* @param chapters chapters data from json
|
||||
* @param categories categories data from json
|
||||
* @param history history data from json
|
||||
* @param tracks tracking data from json
|
||||
*/
|
||||
private suspend fun restoreMangaData(
|
||||
private fun restoreMangaData(
|
||||
manga: Manga,
|
||||
source: Source?,
|
||||
chapters: List<Chapter>,
|
||||
categories: List<Int>,
|
||||
history: List<BackupHistory>,
|
||||
tracks: List<Track>,
|
||||
backupCategories: List<BackupCategory>,
|
||||
// SY -->
|
||||
mergedMangaReferences: List<BackupMergedMangaReference>,
|
||||
flatMetadata: BackupFlatMetadata?,
|
||||
online: Boolean
|
||||
customManga: CustomMangaManager.MangaJson?,
|
||||
// SY -->
|
||||
) {
|
||||
val dbManga = backupManager.getMangaFromDatabase(manga)
|
||||
|
||||
db.inTransaction {
|
||||
val dbManga = backupManager.getMangaFromDatabase(manga)
|
||||
if (dbManga == null) {
|
||||
// Manga not in database
|
||||
restoreMangaFetch(source, manga, chapters, categories, history, tracks, backupCategories, mergedMangaReferences, flatMetadata, online)
|
||||
} else { // Manga in database
|
||||
restoreMangaFetch(manga, chapters, categories, history, tracks, backupCategories/* SY --> */, mergedMangaReferences, flatMetadata, customManga/* SY <-- */)
|
||||
} else {
|
||||
// Manga in database
|
||||
// Copy information from manga already in database
|
||||
backupManager.restoreMangaNoFetch(manga, dbManga)
|
||||
// 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 categories categories that need updating
|
||||
*/
|
||||
private suspend fun restoreMangaFetch(
|
||||
source: Source?,
|
||||
private fun restoreMangaFetch(
|
||||
manga: Manga,
|
||||
chapters: List<Chapter>,
|
||||
categories: List<Int>,
|
||||
history: List<BackupHistory>,
|
||||
tracks: List<Track>,
|
||||
backupCategories: List<BackupCategory>,
|
||||
// SY -->
|
||||
mergedMangaReferences: List<BackupMergedMangaReference>,
|
||||
flatMetadata: BackupFlatMetadata?,
|
||||
online: Boolean
|
||||
customManga: CustomMangaManager.MangaJson?,
|
||||
// SY <--
|
||||
) {
|
||||
try {
|
||||
val fetchedManga = backupManager.restoreMangaFetch(source, manga, online)
|
||||
val fetchedManga = backupManager.restoreManga(manga)
|
||||
fetchedManga.id ?: return
|
||||
backupManager.restoreChaptersForManga(fetchedManga, chapters)
|
||||
|
||||
if (online && source != null) {
|
||||
// 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)
|
||||
restoreExtraForManga(fetchedManga, categories, history, tracks, backupCategories /* SY --> */, mergedMangaReferences, flatMetadata, customManga/* SY <-- */)
|
||||
} catch (e: Exception) {
|
||||
errors.add(Date() to "${manga.title} - ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun restoreMangaNoFetch(
|
||||
source: Source?,
|
||||
private fun restoreMangaNoFetch(
|
||||
backupManga: Manga,
|
||||
chapters: List<Chapter>,
|
||||
categories: List<Int>,
|
||||
history: List<BackupHistory>,
|
||||
tracks: List<Track>,
|
||||
backupCategories: List<BackupCategory>,
|
||||
// SY -->
|
||||
mergedMangaReferences: List<BackupMergedMangaReference>,
|
||||
flatMetadata: BackupFlatMetadata?,
|
||||
online: Boolean
|
||||
customManga: CustomMangaManager.MangaJson?,
|
||||
// SY <--
|
||||
) {
|
||||
if (online && source != null) {
|
||||
if (/* SY --> */ source !is MergedSource && /* SY <-- */ !backupManager.restoreChaptersForManga(backupManga, chapters)) {
|
||||
updateChapters(source, backupManga, chapters)
|
||||
}
|
||||
} else {
|
||||
backupManager.restoreChaptersForMangaOffline(backupManga, chapters)
|
||||
}
|
||||
backupManager.restoreChaptersForManga(backupManga, chapters)
|
||||
|
||||
restoreExtraForManga(backupManga, categories, history, tracks, backupCategories, mergedMangaReferences, flatMetadata)
|
||||
|
||||
updateTracking(backupManga, tracks)
|
||||
restoreExtraForManga(backupManga, categories, history, tracks, backupCategories/* SY --> */, mergedMangaReferences, flatMetadata, customManga/* SY <-- */)
|
||||
}
|
||||
|
||||
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
|
||||
backupManager.restoreCategoriesForManga(manga, categories, backupCategories)
|
||||
|
||||
@@ -232,6 +222,10 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier, private val
|
||||
|
||||
// Restore flat metadata for metadata sources
|
||||
flatMetadata?.let { backupManager.restoreFlatMetadata(manga, it) }
|
||||
|
||||
// Restore Custom Info
|
||||
customManga?.id = manga.id!!
|
||||
customManga?.let { customMangaManager.saveMangaInfo(it) }
|
||||
// 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.MangaImpl
|
||||
import eu.kanade.tachiyomi.data.database.models.TrackImpl
|
||||
import eu.kanade.tachiyomi.data.library.CustomMangaManager
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.protobuf.ProtoNumber
|
||||
|
||||
@@ -36,7 +37,15 @@ data class BackupManga(
|
||||
@ProtoNumber(102) var history: List<BackupHistory> = emptyList(),
|
||||
// SY specific values
|
||||
@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 {
|
||||
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> {
|
||||
return tracking.map {
|
||||
it.getTrackingImpl()
|
||||
@@ -69,22 +101,35 @@ data class BackupManga(
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun copyFrom(manga: Manga): BackupManga {
|
||||
fun copyFrom(manga: Manga /* SY --> */, customMangaManager: CustomMangaManager?/* SY <-- */): BackupManga {
|
||||
return BackupManga(
|
||||
url = manga.url,
|
||||
title = manga.title,
|
||||
artist = manga.artist,
|
||||
author = manga.author,
|
||||
description = manga.description,
|
||||
genre = manga.getGenres() ?: emptyList(),
|
||||
status = manga.status,
|
||||
// SY -->
|
||||
title = manga.originalTitle,
|
||||
artist = manga.originalArtist,
|
||||
author = manga.originalAuthor,
|
||||
description = manga.originalDescription,
|
||||
genre = manga.getOriginalGenres() ?: emptyList(),
|
||||
status = manga.originalStatus,
|
||||
// SY <--
|
||||
thumbnailUrl = manga.thumbnail_url,
|
||||
favorite = manga.favorite,
|
||||
source = manga.source,
|
||||
dateAdded = manga.date_added,
|
||||
viewer = manga.viewer,
|
||||
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 <--
|
||||
|
||||
private suspend fun restoreManga(mangaJson: JsonObject) {
|
||||
/* SY --> */ var /* SY <-- */ manga = backupManager.parser.fromJson<MangaImpl>(
|
||||
val manga = backupManager.parser.fromJson<MangaImpl>(
|
||||
mangaJson.get(
|
||||
Backup.MANGA
|
||||
)
|
||||
@@ -114,7 +114,7 @@ class LegacyBackupRestore(context: Context, notifier: BackupNotifier) : Abstract
|
||||
)
|
||||
|
||||
// EXH -->
|
||||
manga = EXHMigrations.migrateBackupEntry(manga)
|
||||
EXHMigrations.migrateBackupEntry(manga)
|
||||
// <-- EXH
|
||||
|
||||
val source = backupManager.sourceManager.get(manga.source)
|
||||
|
||||
@@ -2,8 +2,6 @@ package eu.kanade.tachiyomi.data.cache
|
||||
|
||||
import android.content.Context
|
||||
import android.text.format.Formatter
|
||||
import com.github.salomonbrys.kotson.fromJson
|
||||
import com.google.gson.Gson
|
||||
import com.jakewharton.disklrucache.DiskLruCache
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
@@ -15,10 +13,12 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.Response
|
||||
import okio.buffer
|
||||
import okio.sink
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
@@ -48,14 +48,12 @@ class ChapterCache(private val context: Context) {
|
||||
private val scope = CoroutineScope(Job() + Dispatchers.Main)
|
||||
|
||||
/** Google Json class used for parsing JSON files. */
|
||||
private val gson: Gson by injectLazy()
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
// --> EH
|
||||
private val prefs: PreferencesHelper by injectLazy()
|
||||
// <-- EH
|
||||
|
||||
/** Cache class used for cache management. */
|
||||
// --> EH
|
||||
private var diskCache = setupDiskCache(prefs.cacheSize().get().toLong())
|
||||
|
||||
init {
|
||||
@@ -73,7 +71,7 @@ class ChapterCache(private val context: Context) {
|
||||
/**
|
||||
* Returns directory of cache.
|
||||
*/
|
||||
val cacheDir: File
|
||||
private val cacheDir: File
|
||||
get() = diskCache.directory
|
||||
|
||||
/**
|
||||
@@ -100,43 +98,19 @@ class ChapterCache(private val context: Context) {
|
||||
}
|
||||
// <-- EH
|
||||
|
||||
/**
|
||||
* Remove file from cache.
|
||||
*
|
||||
* @param file name of file "md5.0".
|
||||
* @return status of deletion for the file.
|
||||
*/
|
||||
fun removeFileFromCache(file: String): Boolean {
|
||||
// Make sure we don't delete the journal file (keeps track of cache).
|
||||
if (file == "journal" || file.startsWith("journal.")) {
|
||||
return false
|
||||
}
|
||||
|
||||
return try {
|
||||
// Remove the extension from the file to get the key of the cache
|
||||
val key = file.substringBeforeLast(".")
|
||||
// Remove file from cache.
|
||||
diskCache.remove(key)
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get page list from cache.
|
||||
*
|
||||
* @param chapter the chapter.
|
||||
* @return an observable of the list of pages.
|
||||
* @return the list of pages.
|
||||
*/
|
||||
fun getPageListFromCache(chapter: Chapter): Observable<List<Page>> {
|
||||
return Observable.fromCallable {
|
||||
// Get the key for the chapter.
|
||||
val key = DiskUtil.hashKeyForDisk(getKey(chapter))
|
||||
fun getPageListFromCache(chapter: Chapter): List<Page> {
|
||||
// Get the key for the chapter.
|
||||
val key = DiskUtil.hashKeyForDisk(getKey(chapter))
|
||||
|
||||
// Convert JSON string to list of objects. Throws an exception if snapshot is null
|
||||
diskCache.get(key).use {
|
||||
gson.fromJson<List<Page>>(it.getString(0))
|
||||
}
|
||||
// Convert JSON string to list of objects. Throws an exception if snapshot is null
|
||||
return diskCache.get(key).use {
|
||||
json.decodeFromString(it.getString(0))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,7 +122,7 @@ class ChapterCache(private val context: Context) {
|
||||
*/
|
||||
fun putPageListToCache(chapter: Chapter, pages: List<Page>) {
|
||||
// Convert list of pages to json string.
|
||||
val cachedValue = gson.toJson(pages)
|
||||
val cachedValue = json.encodeToString(pages)
|
||||
|
||||
// Initialize the editor (edits the values for an entry).
|
||||
var editor: DiskLruCache.Editor? = null
|
||||
@@ -228,6 +202,38 @@ class ChapterCache(private val context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
fun clear(): Int {
|
||||
var deletedFiles = 0
|
||||
cacheDir.listFiles()?.forEach {
|
||||
if (removeFileFromCache(it.name)) {
|
||||
deletedFiles++
|
||||
}
|
||||
}
|
||||
return deletedFiles
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove file from cache.
|
||||
*
|
||||
* @param file name of file "md5.0".
|
||||
* @return status of deletion for the file.
|
||||
*/
|
||||
private fun removeFileFromCache(file: String): Boolean {
|
||||
// Make sure we don't delete the journal file (keeps track of cache).
|
||||
if (file == "journal" || file.startsWith("journal.")) {
|
||||
return false
|
||||
}
|
||||
|
||||
return try {
|
||||
// Remove the extension from the file to get the key of the cache
|
||||
val key = file.substringBeforeLast(".")
|
||||
// Remove file from cache.
|
||||
diskCache.remove(key)
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun getKey(chapter: Chapter): String {
|
||||
return "${chapter.manga_id}${chapter.url}"
|
||||
}
|
||||
|
||||
@@ -59,8 +59,8 @@ class MangaPutResolver : DefaultPutResolver<Manga>() {
|
||||
COL_DESCRIPTION to obj.originalDescription,
|
||||
COL_GENRE to obj.originalGenre,
|
||||
COL_TITLE to obj.originalTitle,
|
||||
COL_STATUS to obj.originalStatus,
|
||||
// SY <--
|
||||
COL_STATUS to obj.status,
|
||||
COL_THUMBNAIL_URL to obj.thumbnail_url,
|
||||
COL_FAVORITE to obj.favorite,
|
||||
COL_LAST_UPDATE to obj.last_update,
|
||||
|
||||
@@ -40,9 +40,11 @@ open class MangaImpl : Manga {
|
||||
override var genre: String?
|
||||
get() = if (favorite) customMangaManager.getManga(this)?.genre ?: ogGenre else ogGenre
|
||||
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
|
||||
|
||||
@@ -71,6 +73,8 @@ open class MangaImpl : Manga {
|
||||
private set
|
||||
var ogGenre: String? = null
|
||||
private set
|
||||
var ogStatus: Int = 0
|
||||
private set
|
||||
// SY <--
|
||||
|
||||
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.MangaLastUpdatedPutResolver
|
||||
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.MangaViewerPutResolver
|
||||
import eu.kanade.tachiyomi.data.database.tables.CategoryTable
|
||||
@@ -102,6 +103,11 @@ interface MangaQueries : DbProvider {
|
||||
.`object`(manga)
|
||||
.withPutResolver(MangaMigrationPutResolver())
|
||||
.prepare()
|
||||
|
||||
fun updateMangaThumbnail(manga: Manga) = db.put()
|
||||
.`object`(manga)
|
||||
.withPutResolver(MangaThumbnailPutResolver())
|
||||
.prepare()
|
||||
// SY <--
|
||||
|
||||
fun insertManga(manga: Manga) = db.put().`object`(manga).prepare()
|
||||
@@ -151,12 +157,38 @@ interface MangaQueries : DbProvider {
|
||||
.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(
|
||||
"""
|
||||
${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)
|
||||
.build()
|
||||
)
|
||||
.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()
|
||||
.byQuery(
|
||||
DeleteQuery.builder()
|
||||
@@ -195,6 +227,16 @@ interface MangaQueries : DbProvider {
|
||||
)
|
||||
.prepare()
|
||||
|
||||
fun getChapterFetchDateManga() = db.get()
|
||||
.listOfObjects(Manga::class.java)
|
||||
.withQuery(
|
||||
RawQuery.builder()
|
||||
.query(getChapterFetchDateMangaQuery())
|
||||
.observesTables(MangaTable.TABLE)
|
||||
.build()
|
||||
)
|
||||
.prepare()
|
||||
|
||||
// SY -->
|
||||
fun getMangaWithMetadata() = db.get()
|
||||
.listOfObjects(Manga::class.java)
|
||||
|
||||
@@ -69,7 +69,7 @@ fun getReadMangaNotInLibraryQuery() =
|
||||
SELECT ${Manga.TABLE}.*
|
||||
FROM ${Manga.TABLE}
|
||||
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
|
||||
"""
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
+14
-9
@@ -1,6 +1,5 @@
|
||||
package eu.kanade.tachiyomi.data.database.resolvers
|
||||
|
||||
import android.content.ContentValues
|
||||
import androidx.core.content.contentValuesOf
|
||||
import com.pushtorefresh.storio.sqlite.StorIOSQLite
|
||||
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.models.Manga
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
||||
import exh.util.nullIfZero
|
||||
|
||||
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_AUTHOR to manga.originalAuthor,
|
||||
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 {
|
||||
val splitter = "▒ ▒∩▒"
|
||||
put(MangaTable.COL_TITLE, manga.title.split(splitter).last())
|
||||
put(MangaTable.COL_GENRE, manga.genre?.split(splitter)?.lastOrNull())
|
||||
put(MangaTable.COL_AUTHOR, manga.author?.split(splitter)?.lastOrNull())
|
||||
put(MangaTable.COL_ARTIST, manga.artist?.split(splitter)?.lastOrNull())
|
||||
put(MangaTable.COL_DESCRIPTION, manga.description?.split(splitter)?.lastOrNull())
|
||||
private fun resetToContentValues(manga: Manga) = contentValuesOf(
|
||||
MangaTable.COL_TITLE to manga.title.split(splitter).last(),
|
||||
MangaTable.COL_GENRE to manga.genre?.split(splitter)?.lastOrNull(),
|
||||
MangaTable.COL_AUTHOR to manga.author?.split(splitter)?.lastOrNull(),
|
||||
MangaTable.COL_ARTIST to manga.artist?.split(splitter)?.lastOrNull(),
|
||||
MangaTable.COL_DESCRIPTION to 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.SourceManager
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import rx.Observable
|
||||
import timber.log.Timber
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
@@ -23,7 +24,7 @@ import uy.kohesive.injekt.injectLazy
|
||||
*
|
||||
* @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 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> {
|
||||
val filteredChapters = getChaptersToDelete(chapters)
|
||||
launchIO {
|
||||
removeFromDownloadQueue(filteredChapters)
|
||||
|
||||
removeFromDownloadQueue(filteredChapters)
|
||||
|
||||
val chapterDirs = provider.findChapterDirs(filteredChapters, manga, source)
|
||||
chapterDirs.forEach { it.delete() }
|
||||
cache.removeChapters(filteredChapters, manga)
|
||||
if (cache.getDownloadCount(manga) == 0) { // Delete manga directory if empty
|
||||
chapterDirs.firstOrNull()?.parentFile?.delete()
|
||||
val chapterDirs = provider.findChapterDirs(filteredChapters, manga, source)
|
||||
chapterDirs.forEach { it.delete() }
|
||||
cache.removeChapters(filteredChapters, manga)
|
||||
if (cache.getDownloadCount(manga) == 0) { // Delete manga directory if empty
|
||||
chapterDirs.firstOrNull()?.parentFile?.delete()
|
||||
}
|
||||
}
|
||||
|
||||
return filteredChapters
|
||||
}
|
||||
|
||||
@@ -302,9 +303,11 @@ class DownloadManager(/* SY private */ val context: Context) {
|
||||
* @param source the source of the manga.
|
||||
*/
|
||||
fun deleteManga(manga: Manga, source: Source) {
|
||||
downloader.queue.remove(manga)
|
||||
provider.findMangaDir(manga, source)?.delete()
|
||||
cache.removeManga(manga)
|
||||
launchIO {
|
||||
downloader.queue.remove(manga)
|
||||
provider.findMangaDir(manga, source)?.delete()
|
||||
cache.removeManga(manga)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -27,6 +27,8 @@ internal class DownloadNotifier(private val context: Context) {
|
||||
private val progressNotificationBuilder by lazy {
|
||||
context.notificationBuilder(Notifications.CHANNEL_DOWNLOADER_PROGRESS) {
|
||||
setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher))
|
||||
setAutoCancel(false)
|
||||
setOnlyAlertOnce(true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,10 +83,8 @@ internal class DownloadNotifier(private val context: Context) {
|
||||
*/
|
||||
fun onProgressChange(download: Download) {
|
||||
with(progressNotificationBuilder) {
|
||||
// Check if first call.
|
||||
if (!isDownloading) {
|
||||
setSmallIcon(android.R.drawable.stat_sys_download)
|
||||
setAutoCancel(false)
|
||||
clearActions()
|
||||
// Open download manager when clicked
|
||||
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
||||
@@ -114,6 +114,7 @@ internal class DownloadNotifier(private val context: Context) {
|
||||
}
|
||||
|
||||
setProgress(download.pages!!.size, download.downloadedImages, false)
|
||||
setOngoing(true)
|
||||
|
||||
show(Notifications.ID_DOWNLOAD_CHAPTER_PROGRESS)
|
||||
}
|
||||
@@ -127,8 +128,8 @@ internal class DownloadNotifier(private val context: Context) {
|
||||
setContentTitle(context.getString(R.string.chapter_paused))
|
||||
setContentText(context.getString(R.string.download_notifier_download_paused))
|
||||
setSmallIcon(R.drawable.ic_pause_24dp)
|
||||
setAutoCancel(false)
|
||||
setProgress(0, 0, false)
|
||||
setOngoing(false)
|
||||
clearActions()
|
||||
// Open download manager when clicked
|
||||
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
||||
@@ -217,7 +218,6 @@ internal class DownloadNotifier(private val context: Context) {
|
||||
setContentText(error ?: context.getString(R.string.download_notifier_unknown_error))
|
||||
setSmallIcon(android.R.drawable.stat_sys_warning)
|
||||
clearActions()
|
||||
setAutoCancel(false)
|
||||
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
||||
setProgress(0, 0, false)
|
||||
|
||||
|
||||
@@ -53,8 +53,8 @@ class DownloadProvider(private val context: Context) {
|
||||
return downloadsDir
|
||||
.createDirectory(getSourceDirName(source))
|
||||
.createDirectory(getMangaDirName(manga))
|
||||
} catch (e: NullPointerException) {
|
||||
Timber.w(e)
|
||||
} catch (e: Throwable) {
|
||||
Timber.e(e, "Invalid download directory")
|
||||
throw Exception(context.getString(R.string.invalid_download_dir))
|
||||
}
|
||||
}
|
||||
@@ -65,7 +65,7 @@ class DownloadProvider(private val context: Context) {
|
||||
* @param source the source to query.
|
||||
*/
|
||||
fun findSourceDir(source: Source): UniFile? {
|
||||
return downloadsDir.findFile(getSourceDirName(source))
|
||||
return downloadsDir.findFile(getSourceDirName(source), true)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -8,7 +8,6 @@ import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.io.File
|
||||
import java.util.Scanner
|
||||
|
||||
class CustomMangaManager(val context: Context) {
|
||||
|
||||
@@ -23,7 +22,7 @@ class CustomMangaManager(val context: Context) {
|
||||
|
||||
val json = try {
|
||||
Json.decodeFromString<MangaList>(
|
||||
Scanner(editJson).useDelimiter("\\Z").next()
|
||||
editJson.bufferedReader().use { it.readText() }
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
@@ -32,30 +31,15 @@ class CustomMangaManager(val context: Context) {
|
||||
val mangasJson = json.mangas ?: return mutableMapOf()
|
||||
return mangasJson.mapNotNull { mangaJson ->
|
||||
val id = mangaJson.id ?: return@mapNotNull null
|
||||
val manga = MangaImpl().apply {
|
||||
this.id = id
|
||||
title = mangaJson.title ?: ""
|
||||
author = mangaJson.author
|
||||
artist = mangaJson.artist
|
||||
description = mangaJson.description
|
||||
genre = mangaJson.genre?.joinToString(", ")
|
||||
}
|
||||
id to manga
|
||||
id to mangaJson.toManga()
|
||||
}.toMap().toMutableMap()
|
||||
}
|
||||
|
||||
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!!)
|
||||
} else {
|
||||
customMangaMap[manga.id!!] = MangaImpl().apply {
|
||||
id = manga.id
|
||||
title = manga.title ?: ""
|
||||
author = manga.author
|
||||
artist = manga.artist
|
||||
description = manga.description
|
||||
genre = manga.genre?.joinToString(", ")
|
||||
}
|
||||
customMangaMap[manga.id!!] = manga.toManga()
|
||||
}
|
||||
saveCustomInfo()
|
||||
}
|
||||
@@ -75,7 +59,8 @@ class CustomMangaManager(val context: Context) {
|
||||
author,
|
||||
artist,
|
||||
description,
|
||||
genre?.split(", ")
|
||||
genre?.split(", "),
|
||||
status
|
||||
)
|
||||
}
|
||||
|
||||
@@ -86,24 +71,23 @@ class CustomMangaManager(val context: Context) {
|
||||
|
||||
@Serializable
|
||||
data class MangaJson(
|
||||
val id: Long? = null,
|
||||
var id: Long? = null,
|
||||
val title: String? = null,
|
||||
val author: String? = null,
|
||||
val artist: 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 {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
other as MangaJson
|
||||
if (id != other.id) return false
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return id.hashCode()
|
||||
fun toManga() = MangaImpl().apply {
|
||||
id = this@MangaJson.id
|
||||
title = this@MangaJson.title ?: ""
|
||||
author = this@MangaJson.author
|
||||
artist = this@MangaJson.artist
|
||||
description = this@MangaJson.description
|
||||
genre = this@MangaJson.genre?.joinToString(", ")
|
||||
status = this@MangaJson.status ?: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,7 +111,7 @@ class LibraryUpdateNotifier(private val context: Context) {
|
||||
setContentIntent(errorLogIntent)
|
||||
addAction(
|
||||
R.drawable.ic_folder_24dp,
|
||||
context.getString(R.string.action_open_log),
|
||||
context.getString(R.string.action_show_errors),
|
||||
errorLogIntent
|
||||
)
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ import eu.kanade.tachiyomi.util.system.createFileInCacheDir
|
||||
import eu.kanade.tachiyomi.util.system.isServiceRunning
|
||||
import exh.md.utils.FollowStatus
|
||||
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.MERGED_SOURCE_ID
|
||||
import exh.source.getMainSource
|
||||
@@ -88,6 +88,7 @@ class LibraryUpdateService(
|
||||
private lateinit var notifier: LibraryUpdateNotifier
|
||||
private lateinit var ioScope: CoroutineScope
|
||||
|
||||
private var mangaToUpdate: List<LibraryManga> = mutableListOf()
|
||||
private var updateJob: Job? = null
|
||||
|
||||
/**
|
||||
@@ -109,6 +110,8 @@ class LibraryUpdateService(
|
||||
|
||||
companion object {
|
||||
|
||||
private var instance: LibraryUpdateService? = null
|
||||
|
||||
/**
|
||||
* Key for category to update.
|
||||
*/
|
||||
@@ -147,7 +150,7 @@ class LibraryUpdateService(
|
||||
* @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 {
|
||||
if (!isRunning(context)) {
|
||||
return if (!isRunning(context)) {
|
||||
val intent = Intent(context, LibraryUpdateService::class.java).apply {
|
||||
putExtra(KEY_TARGET, target)
|
||||
category?.let { putExtra(KEY_CATEGORY, it.id) }
|
||||
@@ -158,10 +161,11 @@ class LibraryUpdateService(
|
||||
}
|
||||
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) {
|
||||
wakeLock.release()
|
||||
}
|
||||
if (instance == this) {
|
||||
instance = null
|
||||
}
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
@@ -221,23 +228,27 @@ class LibraryUpdateService(
|
||||
val target = intent.getSerializableExtra(KEY_TARGET) as? Target
|
||||
?: return START_NOT_STICKY
|
||||
|
||||
// Unsubscribe from any previous subscription if needed.
|
||||
instance = this
|
||||
|
||||
// Unsubscribe from any previous subscription if needed
|
||||
updateJob?.cancel()
|
||||
|
||||
// Update favorite manga. Destroy service when completed or in case of an error.
|
||||
val selectedScheme = preferences.libraryUpdatePrioritization().get()
|
||||
val mangaList = getMangaToUpdate(intent, target)
|
||||
.sortedWith(rankingScheme[selectedScheme])
|
||||
// Update favorite manga
|
||||
val categoryId = intent.getIntExtra(KEY_CATEGORY, -1)
|
||||
val group = intent.getIntExtra(KEY_GROUP, LibraryGroup.BY_DEFAULT)
|
||||
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 ->
|
||||
Timber.e(exception)
|
||||
stopSelf(startId)
|
||||
}
|
||||
updateJob = ioScope.launch(handler) {
|
||||
when (target) {
|
||||
Target.CHAPTERS -> updateChapterList(mangaList)
|
||||
Target.COVERS -> updateCovers(mangaList)
|
||||
Target.TRACKING -> updateTrackings(mangaList)
|
||||
Target.CHAPTERS -> updateChapterList()
|
||||
Target.COVERS -> updateCovers()
|
||||
Target.TRACKING -> updateTrackings()
|
||||
// SY -->
|
||||
Target.SYNC_FOLLOWS -> syncFollows()
|
||||
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.
|
||||
* @return a list of manga to update
|
||||
*/
|
||||
fun getMangaToUpdate(intent: Intent, target: Target): List<LibraryManga> {
|
||||
val categoryId = intent.getIntExtra(KEY_CATEGORY, -1)
|
||||
fun addMangaToQueue(categoryId: Int, group: Int, groupExtra: String?, target: Target) {
|
||||
val libraryManga = db.getLibraryMangas().executeAsBlocking()
|
||||
// SY -->
|
||||
val group = intent.getIntExtra(KEY_GROUP, LibraryGroup.BY_DEFAULT)
|
||||
val groupLibraryUpdateType = preferences.groupLibraryUpdateType().get()
|
||||
// SY <--
|
||||
|
||||
var listToUpdate = if (categoryId != -1) {
|
||||
db.getLibraryMangas().executeAsBlocking().filter { it.category == categoryId }
|
||||
libraryManga.filter { it.category == categoryId }
|
||||
// SY -->
|
||||
} 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)
|
||||
if (categoriesToUpdate.isNotEmpty()) {
|
||||
db.getLibraryMangas().executeAsBlocking()
|
||||
.filter { it.category in categoriesToUpdate }
|
||||
.distinctBy { it.id }
|
||||
val listToInclude = if (categoriesToUpdate.isNotEmpty()) {
|
||||
libraryManga.filter { it.category in categoriesToUpdate }
|
||||
} 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 {
|
||||
val libraryManga = db.getLibraryMangas().executeAsBlocking().distinctBy { it.id }
|
||||
when (group) {
|
||||
LibraryGroup.BY_TRACK_STATUS -> {
|
||||
val trackingExtra = intent.getStringExtra(KEY_GROUP_EXTRA)?.toIntOrNull() ?: -1
|
||||
val trackingExtra = groupExtra?.toIntOrNull() ?: -1
|
||||
libraryManga.filter {
|
||||
val loggedServices = trackManager.services.filter { it.isLogged }
|
||||
val status: String = run {
|
||||
@@ -298,12 +313,12 @@ class LibraryUpdateService(
|
||||
}
|
||||
}
|
||||
LibraryGroup.BY_SOURCE -> {
|
||||
val sourceExtra = intent.getStringExtra(KEY_GROUP_EXTRA).nullIfBlank()
|
||||
val sourceExtra = groupExtra.nullIfBlank()
|
||||
val source = sourceManager.getCatalogueSources().find { it.name == sourceExtra }
|
||||
if (source != null) libraryManga.filter { it.source == source.id } else emptyList()
|
||||
}
|
||||
LibraryGroup.BY_STATUS -> {
|
||||
val statusExtra = intent.getStringExtra(KEY_GROUP_EXTRA)?.toIntOrNull() ?: -1
|
||||
val statusExtra = groupExtra?.toIntOrNull() ?: -1
|
||||
libraryManga.filter {
|
||||
it.status == statusExtra
|
||||
}
|
||||
@@ -314,10 +329,13 @@ class LibraryUpdateService(
|
||||
// SY <--
|
||||
}
|
||||
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
|
||||
* @return an observable delivering the progress of each update.
|
||||
*/
|
||||
suspend fun updateChapterList(mangaToUpdate: List<LibraryManga>) {
|
||||
suspend fun updateChapterList() {
|
||||
val semaphore = Semaphore(5)
|
||||
val progressCount = AtomicInteger(0)
|
||||
val newUpdates = mutableListOf<Pair<LibraryManga, Array<Chapter>>>()
|
||||
@@ -463,7 +481,7 @@ class LibraryUpdateService(
|
||||
return syncChaptersWithSource(db, chapters, manga, source)
|
||||
}
|
||||
|
||||
private suspend fun updateCovers(mangaToUpdate: List<LibraryManga>) {
|
||||
private suspend fun updateCovers() {
|
||||
var progressCount = 0
|
||||
|
||||
mangaToUpdate.forEach { manga ->
|
||||
@@ -496,7 +514,7 @@ class LibraryUpdateService(
|
||||
* 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.
|
||||
*/
|
||||
private suspend fun updateTrackings(mangaToUpdate: List<LibraryManga>) {
|
||||
private suspend fun updateTrackings() {
|
||||
var progressCount = 0
|
||||
val loggedServices = trackManager.services.filter { it.isLogged }
|
||||
|
||||
@@ -539,11 +557,12 @@ class LibraryUpdateService(
|
||||
private suspend fun syncFollows() {
|
||||
val count = AtomicInteger(0)
|
||||
val mangaDex = MdUtil.getEnabledMangaDex(preferences, sourceManager) ?: return
|
||||
val syncFollowStatusInts = preferences.mangadexSyncToLibraryIndexes().get().map { it.toInt() }
|
||||
|
||||
val size: Int
|
||||
mangaDex.fetchAllFollows(true)
|
||||
.filter { (_, metadata) ->
|
||||
metadata.follow_status == FollowStatus.RE_READING.int || metadata.follow_status == FollowStatus.READING.int
|
||||
syncFollowStatusInts.contains(metadata.follow_status)
|
||||
}
|
||||
.also { size = it.size }
|
||||
.forEach { (networkManga, metadata) ->
|
||||
@@ -569,7 +588,7 @@ class LibraryUpdateService(
|
||||
val id = db.insertManga(dbManga).executeOnIO().insertedId()
|
||||
if (id != null) {
|
||||
metadata.mangaId = id
|
||||
db.insertFlatMetadata(metadata.flatten()).await()
|
||||
db.insertFlatMetadataAsync(metadata.flatten()).await()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ class NotificationReceiver : BroadcastReceiver() {
|
||||
shareFile(
|
||||
context,
|
||||
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)
|
||||
)
|
||||
ACTION_CANCEL_RESTORE -> cancelRestore(
|
||||
|
||||
@@ -17,13 +17,21 @@ object PreferenceKeys {
|
||||
|
||||
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 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"
|
||||
|
||||
@@ -73,6 +81,10 @@ object PreferenceKeys {
|
||||
|
||||
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 portraitColumns = "pref_library_columns_portrait_key"
|
||||
@@ -114,6 +126,7 @@ object PreferenceKeys {
|
||||
const val libraryUpdateRestriction = "library_update_restriction"
|
||||
|
||||
const val libraryUpdateCategories = "library_update_categories"
|
||||
const val libraryUpdateCategoriesExclude = "library_update_categories_exclude"
|
||||
|
||||
const val libraryUpdatePrioritization = "library_update_prioritization"
|
||||
|
||||
@@ -158,6 +171,7 @@ object PreferenceKeys {
|
||||
const val downloadNew = "download_new"
|
||||
|
||||
const val downloadNewCategories = "download_new_categories"
|
||||
const val downloadNewCategoriesExclude = "download_new_categories_exclude"
|
||||
|
||||
const val libraryDisplayMode = "pref_display_mode_library"
|
||||
|
||||
@@ -183,7 +197,7 @@ object PreferenceKeys {
|
||||
|
||||
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"
|
||||
|
||||
@@ -199,6 +213,8 @@ object PreferenceKeys {
|
||||
|
||||
const val incognitoMode = "incognito_mode"
|
||||
|
||||
const val createLegacyBackup = "create_legacy_backup"
|
||||
|
||||
fun trackUsername(syncId: Int) = "pref_mangasync_username_$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 mangadexSyncToLibraryIndexes = "pref_mangadex_sync_to_library_indexes"
|
||||
|
||||
const val preferredMangaDexId = "preferred_mangaDex_id"
|
||||
|
||||
const val dataSaver = "data_saver"
|
||||
@@ -339,11 +357,15 @@ object PreferenceKeys {
|
||||
|
||||
const val sortTagsForLibrary = "sort_tags_for_library"
|
||||
|
||||
const val createLegacyBackup = "create_legacy_backup"
|
||||
|
||||
const val dontDeleteFromCategories = "dont_delete_from_categories"
|
||||
|
||||
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 {
|
||||
|
||||
/* ktlint-disable experimental:enum-entry-name-case */
|
||||
|
||||
// Keys are lowercase to match legacy string values
|
||||
enum class ThemeMode {
|
||||
light,
|
||||
@@ -25,8 +27,11 @@ object PreferenceValues {
|
||||
amoled,
|
||||
red,
|
||||
midnightdusk,
|
||||
hotpink,
|
||||
}
|
||||
|
||||
/* ktlint-enable experimental:enum-entry-name-case */
|
||||
|
||||
enum class DisplayMode {
|
||||
COMPACT_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.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())
|
||||
return asFlow()
|
||||
.onEach { block(it) }
|
||||
@@ -36,6 +36,10 @@ operator fun <T> Preference<Set<T>>.minusAssign(item: T) {
|
||||
set(get() - item)
|
||||
}
|
||||
|
||||
fun Preference<Boolean>.toggle() {
|
||||
set(!get())
|
||||
}
|
||||
|
||||
class PreferencesHelper(val context: Context) {
|
||||
|
||||
private val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
@@ -83,13 +87,21 @@ class PreferencesHelper(val context: Context) {
|
||||
|
||||
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 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)
|
||||
|
||||
@@ -143,6 +155,10 @@ class PreferencesHelper(val context: Context) {
|
||||
|
||||
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 landscapeColumns() = flowPrefs.getInt(Keys.landscapeColumns, 0)
|
||||
@@ -204,6 +220,7 @@ class PreferencesHelper(val context: Context) {
|
||||
fun libraryUpdateRestriction() = prefs.getStringSet(Keys.libraryUpdateRestriction, setOf("wifi"))
|
||||
|
||||
fun libraryUpdateCategories() = flowPrefs.getStringSet(Keys.libraryUpdateCategories, emptySet())
|
||||
fun libraryUpdateCategoriesExclude() = flowPrefs.getStringSet(Keys.libraryUpdateCategoriesExclude, emptySet())
|
||||
|
||||
fun libraryUpdatePrioritization() = flowPrefs.getInt(Keys.libraryUpdatePrioritization, 0)
|
||||
|
||||
@@ -254,6 +271,7 @@ class PreferencesHelper(val context: Context) {
|
||||
fun downloadNew() = flowPrefs.getBoolean(Keys.downloadNew, false)
|
||||
|
||||
fun downloadNewCategories() = flowPrefs.getStringSet(Keys.downloadNewCategories, emptySet())
|
||||
fun downloadNewCategoriesExclude() = flowPrefs.getStringSet(Keys.downloadNewCategoriesExclude, emptySet())
|
||||
|
||||
fun lang() = prefs.getString(Keys.lang, "")
|
||||
|
||||
@@ -267,7 +285,7 @@ class PreferencesHelper(val context: Context) {
|
||||
|
||||
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", "")
|
||||
|
||||
@@ -425,6 +443,8 @@ class PreferencesHelper(val context: Context) {
|
||||
|
||||
fun mangadexSimilarOnlyOverWifi() = flowPrefs.getBoolean(Keys.mangadexSimilarOnlyOverWifi, true)
|
||||
|
||||
fun mangadexSyncToLibraryIndexes() = flowPrefs.getStringSet(Keys.mangadexSyncToLibraryIndexes, emptySet())
|
||||
|
||||
fun mangadexSimilarUpdateInterval() = flowPrefs.getInt(Keys.mangadexSimilarUpdateInterval, 2)
|
||||
|
||||
fun dataSaver() = flowPrefs.getBoolean(Keys.dataSaver, false)
|
||||
@@ -455,5 +475,11 @@ class PreferencesHelper(val context: Context) {
|
||||
|
||||
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) }
|
||||
|
||||
override val supportsReadingDates: Boolean = true
|
||||
|
||||
private val scorePreference = preferences.anilistScoreType()
|
||||
|
||||
init {
|
||||
|
||||
@@ -2,6 +2,9 @@ package eu.kanade.tachiyomi.data.track.anilist
|
||||
|
||||
import android.net.Uri
|
||||
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.track.model.TrackSearch
|
||||
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.parseAs
|
||||
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||
import kotlinx.serialization.json.JsonNull
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.contentOrNull
|
||||
@@ -30,8 +34,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
|
||||
suspend fun addLibManga(track: Track): Track {
|
||||
return withIOContext {
|
||||
val query =
|
||||
"""
|
||||
val query = """
|
||||
|mutation AddManga(${'$'}mangaId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus) {
|
||||
|SaveMediaListEntry (mediaId: ${'$'}mangaId, progress: ${'$'}progress, status: ${'$'}status) {
|
||||
| id
|
||||
@@ -65,10 +68,15 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
|
||||
suspend fun updateLibManga(track: Track): Track {
|
||||
return withIOContext {
|
||||
val query =
|
||||
"""
|
||||
|mutation UpdateManga(${'$'}listId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus, ${'$'}score: Int) {
|
||||
|SaveMediaListEntry (id: ${'$'}listId, progress: ${'$'}progress, status: ${'$'}status, scoreRaw: ${'$'}score) {
|
||||
val query = """
|
||||
|mutation UpdateManga(
|
||||
|${'$'}listId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus,
|
||||
|${'$'}score: Int, ${'$'}startedAt: FuzzyDateInput, ${'$'}completedAt: FuzzyDateInput
|
||||
|) {
|
||||
|SaveMediaListEntry(
|
||||
|id: ${'$'}listId, progress: ${'$'}progress, status: ${'$'}status,
|
||||
|scoreRaw: ${'$'}score, startedAt: ${'$'}startedAt, completedAt: ${'$'}completedAt
|
||||
|) {
|
||||
|id
|
||||
|status
|
||||
|progress
|
||||
@@ -82,6 +90,8 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
put("progress", track.last_chapter_read)
|
||||
put("status", track.toAnilistStatus())
|
||||
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)))
|
||||
@@ -92,8 +102,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
|
||||
suspend fun search(search: String): List<TrackSearch> {
|
||||
return withIOContext {
|
||||
val query =
|
||||
"""
|
||||
val query = """
|
||||
|query Search(${'$'}query: String) {
|
||||
|Page (perPage: 50) {
|
||||
|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? {
|
||||
return withIOContext {
|
||||
val query =
|
||||
"""
|
||||
val query = """
|
||||
|query (${'$'}id: Int!, ${'$'}manga_id: Int!) {
|
||||
|Page {
|
||||
|mediaList(userId: ${'$'}id, type: MANGA, mediaId: ${'$'}manga_id) {
|
||||
@@ -152,6 +160,16 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
|status
|
||||
|scoreRaw: score(format: POINT_100)
|
||||
|progress
|
||||
|startedAt {
|
||||
|year
|
||||
|month
|
||||
|day
|
||||
|}
|
||||
|completedAt {
|
||||
|year
|
||||
|month
|
||||
|day
|
||||
|}
|
||||
|media {
|
||||
|id
|
||||
|title {
|
||||
@@ -209,8 +227,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
|
||||
suspend fun getCurrentUser(): Pair<Int, String> {
|
||||
return withIOContext {
|
||||
val query =
|
||||
"""
|
||||
val query = """
|
||||
|query User {
|
||||
|Viewer {
|
||||
|id
|
||||
@@ -243,21 +260,6 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
}
|
||||
|
||||
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(
|
||||
struct["id"]!!.jsonPrimitive.int,
|
||||
struct["title"]!!.jsonObject["romaji"]!!.jsonPrimitive.content,
|
||||
@@ -265,7 +267,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
struct["description"]!!.jsonPrimitive.contentOrNull,
|
||||
struct["type"]!!.jsonPrimitive.content,
|
||||
struct["status"]!!.jsonPrimitive.contentOrNull ?: "",
|
||||
date,
|
||||
parseDate(struct, "startDate"),
|
||||
struct["chapters"]!!.jsonPrimitive.intOrNull ?: 0
|
||||
)
|
||||
}
|
||||
@@ -276,10 +278,44 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
struct["status"]!!.jsonPrimitive.content,
|
||||
struct["scoreRaw"]!!.jsonPrimitive.int,
|
||||
struct["progress"]!!.jsonPrimitive.int,
|
||||
parseDate(struct, "startedAt"),
|
||||
parseDate(struct, "completedAt"),
|
||||
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 {
|
||||
private const val clientId = "385"
|
||||
private const val apiUrl = "https://graphql.anilist.co/"
|
||||
|
||||
@@ -44,6 +44,8 @@ data class ALUserManga(
|
||||
val list_status: String,
|
||||
val score_raw: Int,
|
||||
val chapters_read: Int,
|
||||
val start_date_fuzzy: Long,
|
||||
val completed_date_fuzzy: Long,
|
||||
val manga: ALManga
|
||||
) {
|
||||
|
||||
@@ -51,6 +53,8 @@ data class ALUserManga(
|
||||
media_id = manga.media_id
|
||||
status = toTrackStatus()
|
||||
score = score_raw.toFloat()
|
||||
started_reading_date = start_date_fuzzy
|
||||
finished_reading_date = completed_date_fuzzy
|
||||
last_chapter_read = chapters_read
|
||||
library_id = this@ALUserManga.library_id
|
||||
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) {
|
||||
track.copyPersonalFrom(remoteTrack)
|
||||
track.library_id = remoteTrack.library_id
|
||||
track.status = remoteTrack.status
|
||||
track.last_chapter_read = remoteTrack.last_chapter_read
|
||||
track.status = statusTrack.status
|
||||
track.score = statusTrack.score
|
||||
track.last_chapter_read = statusTrack.last_chapter_read
|
||||
track.total_chapters = remoteTrack.total_chapters
|
||||
refresh(track)
|
||||
} else {
|
||||
// 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!!)
|
||||
api.findLibManga(track)?.let { remoteTrack ->
|
||||
track.total_chapters = remoteTrack.total_chapters
|
||||
track.status = remoteTrack.status
|
||||
}
|
||||
return track
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.contentOrNull
|
||||
import kotlinx.serialization.json.int
|
||||
import kotlinx.serialization.json.jsonArray
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
@@ -46,6 +47,7 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept
|
||||
return withIOContext {
|
||||
// read status update
|
||||
val sbody = FormBody.Builder()
|
||||
.add("rating", track.score.toInt().toString())
|
||||
.add("status", track.toBangumiStatus())
|
||||
.build()
|
||||
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 {
|
||||
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 {
|
||||
media_id = obj["id"]!!.jsonPrimitive.int
|
||||
title = obj["name_cn"]!!.jsonPrimitive.content
|
||||
cover_url = obj["images"]!!.jsonObject["common"]!!.jsonPrimitive.content
|
||||
cover_url = coverUrl
|
||||
summary = obj["name"]!!.jsonPrimitive.content
|
||||
tracking_url = obj["url"]!!.jsonPrimitive.content
|
||||
total_chapters = totalChapters
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,14 +133,21 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept
|
||||
.build()
|
||||
|
||||
// TODO: get user readed chapter here
|
||||
authClient.newCall(requestUserRead)
|
||||
.await()
|
||||
.parseAs<Collection>()
|
||||
.let {
|
||||
var response = authClient.newCall(requestUserRead).await()
|
||||
var responseBody = response.body?.string().orEmpty()
|
||||
if (responseBody.isEmpty()) {
|
||||
throw Exception("Null Response")
|
||||
}
|
||||
if (responseBody.contains("\"code\":400")) {
|
||||
null
|
||||
} else {
|
||||
json.decodeFromString<Collection>(responseBody).let {
|
||||
track.status = it.status?.id!!
|
||||
track.last_chapter_read = it.ep_status!!
|
||||
track.score = it.rating!!
|
||||
track
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ data class Collection(
|
||||
val comment: String? = "",
|
||||
val ep_status: Int? = 0,
|
||||
val lasttouch: Int? = 0,
|
||||
val rating: Int? = 0,
|
||||
val rating: Float? = 0f,
|
||||
val status: Status? = Status(),
|
||||
val tag: List<String?>? = listOf(),
|
||||
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.util.lang.awaitSingle
|
||||
import eu.kanade.tachiyomi.util.lang.runAsObservable
|
||||
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||
import exh.md.utils.FollowStatus
|
||||
import exh.md.utils.MdUtil
|
||||
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 update(track: Track): Track {
|
||||
val mdex = mdex ?: throw MangaDexNotFoundException()
|
||||
return withIOContext {
|
||||
val mdex = mdex ?: throw MangaDexNotFoundException()
|
||||
|
||||
val remoteTrack = mdex.fetchTrackingInfo(track.tracking_url)
|
||||
val followStatus = FollowStatus.fromInt(track.status)
|
||||
val remoteTrack = mdex.fetchTrackingInfo(track.tracking_url)
|
||||
val followStatus = FollowStatus.fromInt(track.status)
|
||||
|
||||
// this updates the follow status in the metadata
|
||||
// allow follow status to update
|
||||
if (remoteTrack.status != followStatus.int) {
|
||||
mdex.updateFollowStatus(MdUtil.getMangaId(track.tracking_url), followStatus)
|
||||
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()
|
||||
// this updates the follow status in the metadata
|
||||
// allow follow status to update
|
||||
if (remoteTrack.status != followStatus.int) {
|
||||
mdex.updateFollowStatus(MdUtil.getMangaId(track.tracking_url), followStatus)
|
||||
remoteTrack.status = followStatus.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
|
||||
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
|
||||
}
|
||||
|
||||
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 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 {
|
||||
val mdex = mdex ?: throw MangaDexNotFoundException()
|
||||
val (remoteTrack, mangaMetadata) = mdex.getTrackingAndMangaInfo(track)
|
||||
track.copyPersonalFrom(remoteTrack)
|
||||
if (track.total_chapters == 0 && mangaMetadata.status == SManga.COMPLETED) {
|
||||
track.total_chapters = mangaMetadata.maxChapterNumber ?: 0
|
||||
return withIOContext {
|
||||
val mdex = mdex ?: throw MangaDexNotFoundException()
|
||||
val (remoteTrack, mangaMetadata) = mdex.getTrackingAndMangaInfo(track)
|
||||
track.copyPersonalFrom(remoteTrack)
|
||||
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 {
|
||||
val track = Track.create(TrackManager.MDLIST)
|
||||
track.manga_id = dbManga.id!!
|
||||
track.status = FollowStatus.UNFOLLOWED.int
|
||||
track.tracking_url = MdUtil.baseUrl + mdManga.url
|
||||
track.title = mdManga.title
|
||||
return track
|
||||
return Track.create(TrackManager.MDLIST).apply {
|
||||
manga_id = dbManga.id!!
|
||||
status = FollowStatus.UNFOLLOWED.int
|
||||
tracking_url = MdUtil.baseUrl + mdManga.url
|
||||
title = mdManga.title
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun search(query: String): List<TrackSearch> {
|
||||
val mdex = mdex ?: throw MangaDexNotFoundException()
|
||||
return mdex.fetchSearchManga(0, query, mdex.getFilterList())
|
||||
.flatMap { page ->
|
||||
runAsObservable({
|
||||
page.mangas.map {
|
||||
toTrackSearch(mdex.getMangaDetails(it.toMangaInfo()))
|
||||
}
|
||||
})
|
||||
}
|
||||
.awaitSingle()
|
||||
return withIOContext {
|
||||
val mdex = mdex ?: throw MangaDexNotFoundException()
|
||||
mdex.fetchSearchManga(0, query, mdex.getFilterList())
|
||||
.flatMap { page ->
|
||||
runAsObservable({
|
||||
page.mangas.map {
|
||||
toTrackSearch(mdex.getMangaDetails(it.toMangaInfo()))
|
||||
}
|
||||
})
|
||||
}
|
||||
.awaitSingle()
|
||||
}
|
||||
}
|
||||
|
||||
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 val isLogged: Boolean
|
||||
get() = false
|
||||
|
||||
class MangaDexNotFoundException : Exception("Mangadex not enabled")
|
||||
}
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
package eu.kanade.tachiyomi.data.updater
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.work.Constraints
|
||||
import androidx.work.ExistingPeriodicWorkPolicy
|
||||
import androidx.work.NetworkType
|
||||
@@ -11,52 +8,26 @@ import androidx.work.PeriodicWorkRequestBuilder
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.Worker
|
||||
import androidx.work.WorkerParameters
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import eu.kanade.tachiyomi.data.updater.github.GithubUpdateChecker
|
||||
import eu.kanade.tachiyomi.util.system.notificationManager
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class UpdaterJob(private val context: Context, workerParams: WorkerParameters) :
|
||||
Worker(context, workerParams) {
|
||||
|
||||
override fun doWork(): Result {
|
||||
return runBlocking {
|
||||
try {
|
||||
val result = GithubUpdateChecker().checkForUpdate()
|
||||
override fun doWork() = runBlocking {
|
||||
try {
|
||||
val result = GithubUpdateChecker().checkForUpdate()
|
||||
|
||||
if (result is UpdateResult.NewUpdate<*>) {
|
||||
val url = result.release.downloadLink
|
||||
|
||||
val intent = Intent(context, UpdaterService::class.java).apply {
|
||||
putExtra(UpdaterService.EXTRA_DOWNLOAD_URL, url)
|
||||
}
|
||||
|
||||
NotificationCompat.Builder(context, Notifications.CHANNEL_COMMON).update {
|
||||
setContentTitle(context.getString(R.string.app_name))
|
||||
setContentText(context.getString(R.string.update_check_notification_update_available))
|
||||
setSmallIcon(android.R.drawable.stat_sys_download_done)
|
||||
// Download action
|
||||
addAction(
|
||||
android.R.drawable.stat_sys_download_done,
|
||||
context.getString(R.string.action_download),
|
||||
PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
)
|
||||
}
|
||||
}
|
||||
Result.success()
|
||||
} catch (e: Exception) {
|
||||
Result.failure()
|
||||
if (result is UpdateResult.NewUpdate<*>) {
|
||||
UpdaterNotifier(context).promptUpdate(result.release.downloadLink)
|
||||
}
|
||||
Result.success()
|
||||
} catch (e: Exception) {
|
||||
Result.failure()
|
||||
}
|
||||
}
|
||||
|
||||
fun NotificationCompat.Builder.update(block: NotificationCompat.Builder.() -> Unit) {
|
||||
block()
|
||||
context.notificationManager.notify(Notifications.ID_UPDATER, build())
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "UpdateChecker"
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package eu.kanade.tachiyomi.data.updater
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.core.app.NotificationCompat
|
||||
import eu.kanade.tachiyomi.R
|
||||
@@ -28,6 +30,27 @@ internal class UpdaterNotifier(private val context: Context) {
|
||||
context.notificationManager.notify(id, build())
|
||||
}
|
||||
|
||||
fun promptUpdate(url: String) {
|
||||
val intent = Intent(context, UpdaterService::class.java).apply {
|
||||
putExtra(UpdaterService.EXTRA_DOWNLOAD_URL, url)
|
||||
}
|
||||
val pendingIntent = PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
with(notificationBuilder) {
|
||||
setContentTitle(context.getString(R.string.app_name))
|
||||
setContentText(context.getString(R.string.update_check_notification_update_available))
|
||||
setSmallIcon(android.R.drawable.stat_sys_download_done)
|
||||
setContentIntent(pendingIntent)
|
||||
|
||||
clearActions()
|
||||
addAction(
|
||||
android.R.drawable.stat_sys_download_done,
|
||||
context.getString(R.string.action_download),
|
||||
pendingIntent
|
||||
)
|
||||
}
|
||||
notificationBuilder.show()
|
||||
}
|
||||
|
||||
/**
|
||||
* Call when apk download starts.
|
||||
*
|
||||
@@ -63,19 +86,20 @@ internal class UpdaterNotifier(private val context: Context) {
|
||||
* @param uri path location of apk.
|
||||
*/
|
||||
fun onDownloadFinished(uri: Uri) {
|
||||
val installIntent = NotificationHandler.installApkPendingActivity(context, uri)
|
||||
with(notificationBuilder) {
|
||||
setContentText(context.getString(R.string.update_check_notification_download_complete))
|
||||
setSmallIcon(android.R.drawable.stat_sys_download_done)
|
||||
setOnlyAlertOnce(false)
|
||||
setProgress(0, 0, false)
|
||||
// Install action
|
||||
setContentIntent(NotificationHandler.installApkPendingActivity(context, uri))
|
||||
setContentIntent(installIntent)
|
||||
|
||||
clearActions()
|
||||
addAction(
|
||||
R.drawable.ic_system_update_alt_white_24dp,
|
||||
context.getString(R.string.action_install),
|
||||
NotificationHandler.installApkPendingActivity(context, uri)
|
||||
installIntent
|
||||
)
|
||||
// Cancel action
|
||||
addAction(
|
||||
R.drawable.ic_close_24dp,
|
||||
context.getString(R.string.action_cancel),
|
||||
@@ -96,13 +120,13 @@ internal class UpdaterNotifier(private val context: Context) {
|
||||
setSmallIcon(android.R.drawable.stat_sys_warning)
|
||||
setOnlyAlertOnce(false)
|
||||
setProgress(0, 0, false)
|
||||
// Retry action
|
||||
|
||||
clearActions()
|
||||
addAction(
|
||||
R.drawable.ic_refresh_24dp,
|
||||
context.getString(R.string.action_retry),
|
||||
UpdaterService.downloadApkPendingService(context, url)
|
||||
)
|
||||
// Cancel action
|
||||
addAction(
|
||||
R.drawable.ic_close_24dp,
|
||||
context.getString(R.string.action_cancel),
|
||||
|
||||
@@ -32,8 +32,8 @@ class GithubUpdateChecker {
|
||||
.parseAs<GithubRelease>()
|
||||
.let {
|
||||
// Check if latest version is different from current version
|
||||
if (/* SY --> */ isNewVersionSY(it.version) /* SY <-- */) {
|
||||
GithubUpdateResult.NewUpdate(it)
|
||||
if (/* SY --> */ isNewVersionSY(it.version) /* SY <-- */) {
|
||||
GithubUpdateResult.NewUpdate(it)
|
||||
} else {
|
||||
GithubUpdateResult.NoNewUpdate()
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.extension
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.Drawable
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.elvishew.xlog.XLog
|
||||
import com.jakewharton.rxrelay.BehaviorRelay
|
||||
import eu.kanade.tachiyomi.R
|
||||
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.util.lang.launchNow
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import exh.log.xLogD
|
||||
import exh.source.BlacklistedSources
|
||||
import exh.source.EH_SOURCE_ID
|
||||
import exh.source.EXH_SOURCE_ID
|
||||
@@ -156,15 +156,15 @@ class ExtensionManager(
|
||||
// EXH -->
|
||||
private fun <T : Extension> Iterable<T>.filterNotBlacklisted(): List<T> {
|
||||
val blacklistEnabled = preferences.enableSourceBlacklist().get()
|
||||
return filter {
|
||||
if (it.isBlacklisted(blacklistEnabled)) {
|
||||
XLog.tag("ExtensionManager").d("Removing blacklisted extension: (name: %s, pkgName: %s)!", it.name, it.pkgName)
|
||||
false
|
||||
} else true
|
||||
return filterNot { extension ->
|
||||
extension.isBlacklisted(blacklistEnabled)
|
||||
.also {
|
||||
if (it) this@ExtensionManager.xLogD("Removing blacklisted extension: (name: %s, pkgName: %s)!", extension.name, extension.pkgName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
// EXH <--
|
||||
@@ -333,7 +333,7 @@ class ExtensionManager(
|
||||
private fun registerNewExtension(extension: Extension.Installed) {
|
||||
// SY -->
|
||||
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
|
||||
}
|
||||
// SY <--
|
||||
@@ -351,7 +351,7 @@ class ExtensionManager(
|
||||
private fun registerUpdatedExtension(extension: Extension.Installed) {
|
||||
// SY -->
|
||||
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
|
||||
}
|
||||
// SY <--
|
||||
|
||||
@@ -32,7 +32,7 @@ internal class ExtensionGithubApi {
|
||||
.let { parseResponse(it) }
|
||||
} /* SY --> */ + preferences.extensionRepos().get().flatMap { repoPath ->
|
||||
val url = "$BASE_URL$repoPath/repo/"
|
||||
networkService.client
|
||||
networkService.client
|
||||
.newCall(GET("${url}index.min.json"))
|
||||
.await()
|
||||
.parseAs<JsonArray>()
|
||||
|
||||
@@ -163,7 +163,7 @@ internal object ExtensionLoader {
|
||||
else -> throw Exception("Unknown source class type! ${obj.javaClass}")
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Timber.e(e, "Extension load error: $extName.")
|
||||
Timber.e(e, "Extension load error: $extName ($it)")
|
||||
return LoadResult.Error(e)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,6 +171,6 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
|
||||
|
||||
companion object {
|
||||
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.data.preference.PreferencesHelper
|
||||
import okhttp3.Cache
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.dnsoverhttps.DnsOverHttps
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.File
|
||||
import java.net.InetAddress
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/* SY --> */ open /* SY <-- */ class NetworkHelper(context: Context) {
|
||||
@@ -38,25 +35,9 @@ import java.util.concurrent.TimeUnit
|
||||
builder.addInterceptor(httpLoggingInterceptor)
|
||||
}
|
||||
|
||||
if (preferences.enableDoh()) {
|
||||
builder.dns(
|
||||
DnsOverHttps.Builder().client(builder.build())
|
||||
.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()
|
||||
)
|
||||
when (preferences.dohProvider()) {
|
||||
PREF_DOH_CLOUDFLARE -> builder.dohCloudflare()
|
||||
PREF_DOH_GOOGLE -> builder.dohGoogle()
|
||||
}
|
||||
|
||||
builder.build()
|
||||
|
||||
@@ -33,7 +33,7 @@ import java.util.zip.ZipFile
|
||||
class LocalSource(private val context: Context) : CatalogueSource {
|
||||
companion object {
|
||||
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 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"
|
||||
return DiskUtil.getExternalStorages(context).map { File(it.absolutePath, c) }
|
||||
}
|
||||
|
||||
// SY -->
|
||||
val json = Json {
|
||||
prettyPrint = true
|
||||
}
|
||||
// SY <--
|
||||
}
|
||||
|
||||
override val id = ID
|
||||
@@ -151,19 +157,16 @@ class LocalSource(private val context: Context) : CatalogueSource {
|
||||
|
||||
// SY -->
|
||||
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()
|
||||
} ?: return
|
||||
val json = Json {
|
||||
prettyPrint = true
|
||||
}
|
||||
val existingFileName = directory.listFiles()?.find { it.extension == "json" }?.name
|
||||
val file = File(directory, existingFileName ?: "info.json")
|
||||
file.writeText(json.encodeToString(manga.toJson()))
|
||||
}
|
||||
|
||||
private fun SManga.toJson(): MangaJson {
|
||||
return MangaJson(title, author, artist, description, genre?.split(", ")?.toTypedArray())
|
||||
return MangaJson(title, author, artist, description, genre?.split(", "), status)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
@@ -172,7 +175,8 @@ class LocalSource(private val context: Context) : CatalogueSource {
|
||||
val author: String?,
|
||||
val artist: String?,
|
||||
val description: String?,
|
||||
val genre: Array<String>?
|
||||
val genre: List<String>?,
|
||||
val status: Int
|
||||
) {
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package eu.kanade.tachiyomi.source
|
||||
|
||||
import android.content.Context
|
||||
import com.elvishew.xlog.XLog
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
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.all.EHentai
|
||||
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.NHentai
|
||||
import eu.kanade.tachiyomi.source.online.all.PervEden
|
||||
import eu.kanade.tachiyomi.source.online.english.EightMuses
|
||||
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.Tsumino
|
||||
import exh.log.xLogD
|
||||
import exh.source.BlacklistedSources
|
||||
import exh.source.DelegatedHttpSource
|
||||
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.EnhancedHttpSource
|
||||
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_IT_SOURCE_ID
|
||||
import exh.source.PURURIN_SOURCE_ID
|
||||
@@ -115,7 +112,7 @@ open class SourceManager(private val context: Context) {
|
||||
} else DELEGATED_SOURCES[sourceQName]
|
||||
} else 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(
|
||||
source,
|
||||
delegate.newSourceClass.constructors.find { it.parameters.size == 2 }!!.call(source, context)
|
||||
@@ -132,7 +129,7 @@ open class SourceManager(private val context: Context) {
|
||||
} else source
|
||||
|
||||
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
|
||||
}
|
||||
// EXH <--
|
||||
@@ -155,13 +152,12 @@ open class SourceManager(private val context: Context) {
|
||||
|
||||
// SY -->
|
||||
private fun createEHSources(): List<Source> {
|
||||
val exSrcs = mutableListOf<HttpSource>(
|
||||
val sources = listOf<HttpSource>(
|
||||
EHentai(EH_SOURCE_ID, false, context)
|
||||
)
|
||||
if (prefs.enableExhentai().get()) {
|
||||
exSrcs += EHentai(EXH_SOURCE_ID, true, context)
|
||||
}
|
||||
return exSrcs
|
||||
return if (prefs.enableExhentai().get()) {
|
||||
sources + EHentai(EXH_SOURCE_ID, true, context)
|
||||
} else sources
|
||||
}
|
||||
// SY <--
|
||||
|
||||
@@ -195,12 +191,6 @@ open class SourceManager(private val context: Context) {
|
||||
companion object {
|
||||
private const val fillInSourceId = Long.MAX_VALUE
|
||||
val DELEGATED_SOURCES = listOf(
|
||||
DelegatedSource(
|
||||
"Hentai Cafe",
|
||||
HENTAI_CAFE_SOURCE_ID,
|
||||
"eu.kanade.tachiyomi.extension.all.foolslide.HentaiCafe",
|
||||
HentaiCafe::class
|
||||
),
|
||||
DelegatedSource(
|
||||
"Pururin",
|
||||
PURURIN_SOURCE_ID,
|
||||
@@ -213,13 +203,13 @@ open class SourceManager(private val context: Context) {
|
||||
"eu.kanade.tachiyomi.extension.en.tsumino.Tsumino",
|
||||
Tsumino::class
|
||||
),
|
||||
DelegatedSource(
|
||||
/*DelegatedSource(
|
||||
"MangaDex",
|
||||
fillInSourceId,
|
||||
"eu.kanade.tachiyomi.extension.all.mangadex",
|
||||
MangaDex::class,
|
||||
true
|
||||
),
|
||||
),*/
|
||||
DelegatedSource(
|
||||
"HBrowse",
|
||||
HBROWSE_SOURCE_ID,
|
||||
@@ -229,7 +219,7 @@ open class SourceManager(private val context: Context) {
|
||||
DelegatedSource(
|
||||
"8Muses",
|
||||
EIGHTMUSES_SOURCE_ID,
|
||||
"eu.kanade.tachiyomi.extension.all.eromuse.EroMuse",
|
||||
"eu.kanade.tachiyomi.extension.en.eightmuses.EightMuses",
|
||||
EightMuses::class
|
||||
),
|
||||
DelegatedSource(
|
||||
@@ -276,7 +266,7 @@ open class SourceManager(private val context: Context) {
|
||||
get() = internalMap.size
|
||||
override fun containsKey(key: K): Boolean = internalMap.containsKey(key)
|
||||
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 val entries: MutableSet<MutableMap.MutableEntry<K, V>>
|
||||
get() = internalMap.entries
|
||||
|
||||
@@ -2,8 +2,26 @@ package eu.kanade.tachiyomi.source.model
|
||||
|
||||
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 -->
|
||||
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 <--
|
||||
|
||||
@@ -35,6 +35,8 @@ interface SManga : Serializable {
|
||||
get() = (this as? MangaImpl)?.ogDesc ?: description
|
||||
val originalGenre: String?
|
||||
get() = (this as? MangaImpl)?.ogGenre ?: genre
|
||||
val originalStatus: Int
|
||||
get() = (this as? MangaImpl)?.ogStatus ?: status
|
||||
// SY <--
|
||||
|
||||
fun copyFrom(other: SManga) {
|
||||
|
||||
@@ -1,17 +1,25 @@
|
||||
package eu.kanade.tachiyomi.source.online
|
||||
|
||||
import android.app.Activity
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||
|
||||
interface LoginSource : Source {
|
||||
val needsLogin: Boolean
|
||||
val requiresLogin: Boolean
|
||||
|
||||
val twoFactorAuth: AuthSupport
|
||||
|
||||
fun isLogged(): Boolean
|
||||
|
||||
fun getLoginDialog(source: Source, activity: Activity): DialogController
|
||||
fun getUsername(): String
|
||||
|
||||
suspend fun login(username: String, password: String, twoFactorCode: String = ""): Boolean
|
||||
fun getPassword(): String
|
||||
|
||||
suspend fun login(username: String, password: String, twoFactorCode: String?): Boolean
|
||||
|
||||
suspend fun logout(): Boolean
|
||||
|
||||
enum class AuthSupport {
|
||||
NOT_SUPPORTED,
|
||||
SUPPORTED,
|
||||
REQUIRED
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||
import exh.metadata.metadata.base.RaisedSearchMetadata
|
||||
import exh.metadata.metadata.base.getFlatMetadataForManga
|
||||
import exh.metadata.metadata.base.insertFlatMetadata
|
||||
import exh.metadata.metadata.base.insertFlatMetadataAsync
|
||||
import exh.metadata.metadata.base.insertFlatMetadataCompletable
|
||||
import exh.util.executeOnIO
|
||||
import rx.Completable
|
||||
import rx.Single
|
||||
@@ -72,7 +72,7 @@ interface MetadataSource<M : RaisedSearchMetadata, I> : CatalogueSource {
|
||||
}.flatMapCompletable {
|
||||
if (mangaId != null) {
|
||||
it.mangaId = mangaId
|
||||
db.insertFlatMetadata(it.flatten())
|
||||
db.insertFlatMetadataCompletable(it.flatten())
|
||||
} else Completable.complete()
|
||||
}
|
||||
}
|
||||
@@ -87,7 +87,7 @@ interface MetadataSource<M : RaisedSearchMetadata, I> : CatalogueSource {
|
||||
parseInfoIntoMetadata(metadata, input)
|
||||
if (mangaId != null) {
|
||||
metadata.mangaId = mangaId
|
||||
db.insertFlatMetadataAsync(metadata.flatten()).await()
|
||||
db.insertFlatMetadata(metadata.flatten())
|
||||
}
|
||||
|
||||
return metadata.createMangaInfo(manga)
|
||||
@@ -119,7 +119,7 @@ interface MetadataSource<M : RaisedSearchMetadata, I> : CatalogueSource {
|
||||
val newMetaSingle = Single.just(newMeta)
|
||||
if (mangaId != null) {
|
||||
newMeta.mangaId = mangaId
|
||||
db.insertFlatMetadata(newMeta.flatten()).andThen(newMetaSingle)
|
||||
db.insertFlatMetadataCompletable(newMeta.flatten()).andThen(newMetaSingle)
|
||||
} else newMetaSingle
|
||||
}
|
||||
} else Single.just(existingMeta)
|
||||
@@ -146,7 +146,7 @@ interface MetadataSource<M : RaisedSearchMetadata, I> : CatalogueSource {
|
||||
parseInfoIntoMetadata(newMeta, input)
|
||||
if (mangaId != null) {
|
||||
newMeta.mangaId = mangaId
|
||||
db.insertFlatMetadataAsync(newMeta.flatten()).await().let { newMeta }
|
||||
db.insertFlatMetadata(newMeta.flatten()).let { newMeta }
|
||||
} else newMeta
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.source.online.all
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.core.net.toUri
|
||||
import com.elvishew.xlog.XLog
|
||||
import eu.kanade.tachiyomi.annotations.Nsfw
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
@@ -31,6 +30,7 @@ import exh.eh.EHTags
|
||||
import exh.eh.EHentaiUpdateHelper
|
||||
import exh.eh.EHentaiUpdateWorkerConstants
|
||||
import exh.eh.GalleryEntry
|
||||
import exh.log.xLogD
|
||||
import exh.metadata.MetadataUtil
|
||||
import exh.metadata.metadata.EHentaiSearchMetadata
|
||||
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.toGenreString
|
||||
import exh.metadata.metadata.base.RaisedTag
|
||||
import exh.ui.login.LoginController
|
||||
import exh.ui.login.EhLoginActivity
|
||||
import exh.ui.metadata.adapters.EHentaiDescriptionAdapter
|
||||
import exh.util.UriFilter
|
||||
import exh.util.UriGroup
|
||||
@@ -229,7 +229,7 @@ class EHentai(
|
||||
} else {
|
||||
parsedLocation.queryParameter(REVERSE_PARAM)!!.toBoolean()
|
||||
}
|
||||
Pair(parsedMangas, hasNextPage)
|
||||
parsedMangas to hasNextPage
|
||||
}
|
||||
|
||||
private fun getGenre(element: Element? = null, genreString: String? = null): String? {
|
||||
@@ -325,7 +325,7 @@ class EHentai(
|
||||
url = EHentaiSearchMetadata.normalizeUrl(parentLink)
|
||||
} else break
|
||||
} else {
|
||||
XLog.tag("EHentai").d("Parent cache hit: %s!", gid)
|
||||
this@EHentai.xLogD("Parent cache hit: %s!", gid)
|
||||
url = EHentaiSearchMetadata.idAndTokenToUrl(
|
||||
cachedParent.gId,
|
||||
cachedParent.gToken
|
||||
@@ -613,7 +613,7 @@ class EHentai(
|
||||
lastUpdateCheck - datePosted!! > EHentaiUpdateWorkerConstants.GALLERY_AGE_TIME
|
||||
) {
|
||||
aged = true
|
||||
XLog.tag("EHentai").d("aged %s - too old", title)
|
||||
this@EHentai.xLogD("aged %s - too old", title)
|
||||
}
|
||||
|
||||
// Parse ratings
|
||||
@@ -713,7 +713,7 @@ class EHentai(
|
||||
page++
|
||||
} while (parsed.second)
|
||||
|
||||
return Pair(result.toList(), favNames!!)
|
||||
return Pair(result.toList(), favNames.orEmpty())
|
||||
}
|
||||
|
||||
fun spPref() = if (exh) {
|
||||
@@ -725,9 +725,9 @@ class EHentai(
|
||||
private fun rawCookies(sp: Int): Map<String, String> {
|
||||
val cookies: MutableMap<String, String> = mutableMapOf()
|
||||
if (preferences.enableExhentai().get()) {
|
||||
cookies[LoginController.MEMBER_ID_COOKIE] = preferences.memberIdVal().get()
|
||||
cookies[LoginController.PASS_HASH_COOKIE] = preferences.passHashVal().get()
|
||||
cookies[LoginController.IGNEOUS_COOKIE] = preferences.igneousVal().get()
|
||||
cookies[EhLoginActivity.MEMBER_ID_COOKIE] = preferences.memberIdVal().get()
|
||||
cookies[EhLoginActivity.PASS_HASH_COOKIE] = preferences.passHashVal().get()
|
||||
cookies[EhLoginActivity.IGNEOUS_COOKIE] = preferences.igneousVal().get()
|
||||
cookies["sp"] = sp.toString()
|
||||
|
||||
val sessionKey = preferences.exhSettingsKey().get()
|
||||
@@ -879,13 +879,20 @@ class EHentai(
|
||||
stringBuilder.append(" ")
|
||||
}
|
||||
|
||||
XLog.tag("EHentai").d(stringBuilder.toString())
|
||||
return stringBuilder.toString().trim()
|
||||
return stringBuilder.toString().trim().also { xLogD(it) }
|
||||
}
|
||||
|
||||
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 MaxPagesOption : PageOption("Maximum Pages", "f_spt")
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
package eu.kanade.tachiyomi.source.online.all
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.net.Uri
|
||||
import androidx.core.text.HtmlCompat
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
@@ -13,7 +11,6 @@ import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import eu.kanade.tachiyomi.network.await
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
@@ -27,7 +24,6 @@ import eu.kanade.tachiyomi.source.online.MetadataSource
|
||||
import eu.kanade.tachiyomi.source.online.RandomMangaSource
|
||||
import eu.kanade.tachiyomi.source.online.UrlImportableSource
|
||||
import eu.kanade.tachiyomi.ui.base.controller.BaseController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||
import eu.kanade.tachiyomi.util.lang.runAsObservable
|
||||
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||
@@ -47,13 +43,18 @@ import exh.metadata.metadata.MangaDexSearchMetadata
|
||||
import exh.source.DelegatedHttpSource
|
||||
import exh.ui.metadata.adapters.MangaDexDescriptionAdapter
|
||||
import exh.util.urlImportFetchSearchManga
|
||||
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.FormBody
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import okhttp3.internal.closeQuietly
|
||||
import okio.EOFException
|
||||
import rx.Observable
|
||||
import tachiyomi.source.model.ChapterInfo
|
||||
import tachiyomi.source.model.MangaInfo
|
||||
@@ -73,11 +74,10 @@ class MangaDex(delegate: HttpSource, val context: Context) :
|
||||
RandomMangaSource {
|
||||
override val lang: String = delegate.lang
|
||||
|
||||
override val headers: Headers
|
||||
get() = super.headers.newBuilder().apply {
|
||||
add("X-Requested-With", "XMLHttpRequest")
|
||||
add("Referer", MdUtil.baseUrl)
|
||||
}.build()
|
||||
override val headers: Headers = super.headers.newBuilder().apply {
|
||||
add("X-Requested-With", "XMLHttpRequest")
|
||||
add("Referer", MdUtil.baseUrl)
|
||||
}.build()
|
||||
|
||||
private val mdLang by lazy {
|
||||
MdLang.values().find { it.lang == lang }?.dexLang ?: lang
|
||||
@@ -176,21 +176,27 @@ class MangaDex(delegate: HttpSource, val context: Context) :
|
||||
return FollowsHandler(client, headers, Injekt.get(), useLowQualityThumbnail()).fetchFollows()
|
||||
}
|
||||
|
||||
override val needsLogin: Boolean = true
|
||||
override val requiresLogin: Boolean = true
|
||||
|
||||
override fun getLoginDialog(source: Source, activity: Activity): DialogController {
|
||||
return MangadexLoginDialog(source as MangaDex)
|
||||
}
|
||||
override val twoFactorAuth = LoginSource.AuthSupport.SUPPORTED
|
||||
|
||||
override fun isLogged(): Boolean {
|
||||
val httpUrl = MdUtil.baseUrl.toHttpUrl()
|
||||
return trackManager.mdList.isLogged && network.cookieManager.get(httpUrl).any { it.name == REMEMBER_ME }
|
||||
}
|
||||
|
||||
override fun getUsername(): String {
|
||||
return trackManager.mdList.getUsername()
|
||||
}
|
||||
|
||||
override fun getPassword(): String {
|
||||
return trackManager.mdList.getPassword()
|
||||
}
|
||||
|
||||
override suspend fun login(
|
||||
username: String,
|
||||
password: String,
|
||||
twoFactorCode: String
|
||||
twoFactorCode: String?
|
||||
): Boolean {
|
||||
return withIOContext {
|
||||
val formBody = FormBody.Builder().apply {
|
||||
@@ -198,27 +204,29 @@ class MangaDex(delegate: HttpSource, val context: Context) :
|
||||
add("login_password", password)
|
||||
add("no_js", "1")
|
||||
add("remember_me", "1")
|
||||
add("two_factor", twoFactorCode ?: "")
|
||||
}
|
||||
|
||||
twoFactorCode.let {
|
||||
formBody.add("two_factor", it)
|
||||
runCatching {
|
||||
client.newCall(
|
||||
POST(
|
||||
"${MdUtil.baseUrl}/ajax/actions.ajax.php?function=login",
|
||||
headers,
|
||||
formBody.build()
|
||||
)
|
||||
).await().closeQuietly()
|
||||
}
|
||||
|
||||
val response = client.newCall(
|
||||
POST(
|
||||
"${MdUtil.baseUrl}/ajax/actions.ajax.php?function=login",
|
||||
headers,
|
||||
formBody.build()
|
||||
)
|
||||
).await()
|
||||
val response = client.newCall(GET(MdUtil.apiUrl + MdUtil.isLoggedInApi, headers)).await()
|
||||
|
||||
withIOContext { response.body?.string() }.let { result ->
|
||||
if (result != null && result.isEmpty()) {
|
||||
true
|
||||
withIOContext { response.body?.string() }.let { jsonData ->
|
||||
if (jsonData != null) {
|
||||
MdUtil.jsonParser.decodeFromString<JsonObject>(jsonData)["code"]?.let { it as? JsonPrimitive }?.int == 200
|
||||
} else {
|
||||
val error = result?.let { HtmlCompat.fromHtml(it, HtmlCompat.FROM_HTML_MODE_COMPACT).toString() }
|
||||
throw Exception(error)
|
||||
throw Exception("Json data was null")
|
||||
}
|
||||
}.also {
|
||||
preferences.setTrackCredentials(trackManager.mdList, username, password)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -233,11 +241,17 @@ class MangaDex(delegate: HttpSource, val context: Context) :
|
||||
if (token.isNullOrEmpty()) {
|
||||
return@withIOContext true
|
||||
}
|
||||
val result = client.newCall(
|
||||
POST("${MdUtil.baseUrl}/ajax/actions.ajax.php?function=logout", headers).newBuilder().addHeader(REMEMBER_ME, token).build()
|
||||
).await()
|
||||
val resultStr = withIOContext { result.body?.string() }
|
||||
if (resultStr?.contains("success", true) == true) {
|
||||
try {
|
||||
val result = client.newCall(
|
||||
POST("${MdUtil.baseUrl}/ajax/actions.ajax.php?function=logout", headers).newBuilder().addHeader(REMEMBER_ME, token).build()
|
||||
).await()
|
||||
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)
|
||||
trackManager.mdList.logout()
|
||||
return@withIOContext true
|
||||
@@ -269,7 +283,7 @@ class MangaDex(delegate: HttpSource, val context: Context) :
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -281,7 +295,7 @@ class MangaDex(delegate: HttpSource, val context: Context) :
|
||||
}
|
||||
|
||||
override suspend fun fetchRandomMangaUrl(): String {
|
||||
return MangaHandler(client, headers, mdLang).fetchRandomMangaId()
|
||||
return withIOContext { MangaHandler(client, headers, mdLang).fetchRandomMangaId() }
|
||||
}
|
||||
|
||||
fun fetchMangaSimilar(manga: Manga): Observable<MangasPage> {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
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.models.Chapter
|
||||
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.lang.withIOContext
|
||||
import eu.kanade.tachiyomi.util.shouldDownloadNewChapters
|
||||
import exh.log.xLogW
|
||||
import exh.merged.sql.models.MergedMangaReference
|
||||
import exh.source.MERGED_SOURCE_ID
|
||||
import exh.util.executeOnIO
|
||||
@@ -181,11 +181,11 @@ class MergedSource : HttpSource() {
|
||||
}
|
||||
manga.copyFrom(source.getMangaDetails(manga.toMangaInfo()).toSManga())
|
||||
try {
|
||||
manga.id = db.insertManga(manga).executeOnIO().insertedId()
|
||||
manga.id = db.insertManga(manga).executeAsBlocking().insertedId()
|
||||
mangaId = manga.id
|
||||
db.insertNewMergedMangaId(this).executeOnIO()
|
||||
db.insertNewMergedMangaId(this).executeAsBlocking()
|
||||
} catch (e: Exception) {
|
||||
XLog.tag("MergedSource").enableStackTrace(e.stackTrace.contentToString(), 5)
|
||||
xLogW("Error inserting merged manga id", e)
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -1,67 +1,42 @@
|
||||
package eu.kanade.tachiyomi.ui.base.activity
|
||||
|
||||
import android.content.res.Configuration
|
||||
import android.os.Build
|
||||
import android.content.res.Configuration.UI_MODE_NIGHT_MASK
|
||||
import android.content.res.Configuration.UI_MODE_NIGHT_YES
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues.DarkThemeVariant
|
||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues.LightThemeVariant
|
||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues.ThemeMode
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues as Values
|
||||
|
||||
abstract class BaseThemedActivity : AppCompatActivity() {
|
||||
|
||||
val preferences: PreferencesHelper by injectLazy()
|
||||
|
||||
private val isDarkMode: Boolean by lazy {
|
||||
val themeMode = preferences.themeMode().get()
|
||||
(themeMode == Values.ThemeMode.dark) ||
|
||||
(
|
||||
themeMode == Values.ThemeMode.system &&
|
||||
(resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES)
|
||||
)
|
||||
}
|
||||
|
||||
private val lightTheme: Int by lazy {
|
||||
when (preferences.themeLight().get()) {
|
||||
Values.LightThemeVariant.blue -> R.style.Theme_Tachiyomi_LightBlue
|
||||
else -> {
|
||||
when {
|
||||
// Light status + navigation bar
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1 -> {
|
||||
R.style.Theme_Tachiyomi_Light_Api27
|
||||
}
|
||||
// Light status bar + fallback gray navigation bar
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> {
|
||||
R.style.Theme_Tachiyomi_Light_Api23
|
||||
}
|
||||
// Fallback gray status + navigation bar
|
||||
else -> {
|
||||
R.style.Theme_Tachiyomi_Light
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val darkTheme: Int by lazy {
|
||||
when (preferences.themeDark().get()) {
|
||||
Values.DarkThemeVariant.blue -> R.style.Theme_Tachiyomi_DarkBlue
|
||||
Values.DarkThemeVariant.amoled -> R.style.Theme_Tachiyomi_Amoled
|
||||
Values.DarkThemeVariant.red -> R.style.Theme_Tachiyomi_Red
|
||||
Values.DarkThemeVariant.midnightdusk -> R.style.Theme_Tachiyomi_MidnightDusk
|
||||
else -> R.style.Theme_Tachiyomi_Dark
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
setTheme(
|
||||
when {
|
||||
isDarkMode -> darkTheme
|
||||
else -> lightTheme
|
||||
val isDarkMode = when (preferences.themeMode().get()) {
|
||||
ThemeMode.light -> false
|
||||
ThemeMode.dark -> true
|
||||
ThemeMode.system -> resources.configuration.uiMode and UI_MODE_NIGHT_MASK == UI_MODE_NIGHT_YES
|
||||
}
|
||||
val themeId = if (isDarkMode) {
|
||||
when (preferences.themeDark().get()) {
|
||||
DarkThemeVariant.default -> R.style.Theme_Tachiyomi_Dark
|
||||
DarkThemeVariant.blue -> R.style.Theme_Tachiyomi_Dark_Blue
|
||||
DarkThemeVariant.amoled -> R.style.Theme_Tachiyomi_Dark_Amoled
|
||||
DarkThemeVariant.red -> R.style.Theme_Tachiyomi_Dark_Red
|
||||
DarkThemeVariant.midnightdusk -> R.style.Theme_Tachiyomi_Dark_MidnightDusk
|
||||
DarkThemeVariant.hotpink -> R.style.Theme_Tachiyomi_Dark_HotPink
|
||||
}
|
||||
)
|
||||
|
||||
} else {
|
||||
when (preferences.themeLight().get()) {
|
||||
LightThemeVariant.default -> R.style.Theme_Tachiyomi_Light
|
||||
LightThemeVariant.blue -> R.style.Theme_Tachiyomi_Light_Blue
|
||||
}
|
||||
}
|
||||
setTheme(themeId)
|
||||
super.onCreate(savedInstanceState)
|
||||
}
|
||||
}
|
||||
|
||||
+3
-6
@@ -7,11 +7,12 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import com.bluelinelabs.conductor.ControllerChangeHandler
|
||||
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(removesFromViewOnPush: Boolean) : super(removesFromViewOnPush)
|
||||
constructor(duration: Long) : super(duration)
|
||||
@@ -33,10 +34,6 @@ class OneWayFadeChangeHandler : AnimatorChangeHandler {
|
||||
return animator
|
||||
}
|
||||
|
||||
override fun resetFromView(from: View) {
|
||||
from.alpha = 1f
|
||||
}
|
||||
|
||||
override fun copy(): ControllerChangeHandler {
|
||||
return OneWayFadeChangeHandler(animationDuration, removesFromViewOnPush())
|
||||
}
|
||||
|
||||
@@ -19,7 +19,8 @@ import timber.log.Timber
|
||||
abstract class BaseController<VB : ViewBinding>(bundle: Bundle? = null) :
|
||||
RestoreViewOnCreateController(bundle) {
|
||||
|
||||
lateinit var binding: VB
|
||||
protected lateinit var binding: VB
|
||||
private set
|
||||
|
||||
lateinit var viewScope: CoroutineScope
|
||||
|
||||
@@ -51,11 +52,12 @@ abstract class BaseController<VB : ViewBinding>(bundle: Bundle? = null) :
|
||||
)
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup, savedViewState: Bundle?): View {
|
||||
return inflateView(inflater, container)
|
||||
}
|
||||
abstract fun createBinding(inflater: LayoutInflater): VB
|
||||
|
||||
abstract fun inflateView(inflater: LayoutInflater, container: ViewGroup): View
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup, savedViewState: Bundle?): View {
|
||||
binding = createBinding(inflater)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
open fun onViewCreated(view: View) {}
|
||||
|
||||
@@ -121,7 +123,7 @@ abstract class BaseController<VB : ViewBinding>(bundle: Bundle? = null) :
|
||||
* [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]
|
||||
*/
|
||||
fun invalidateMenuOnExpand(): Boolean {
|
||||
open fun invalidateMenuOnExpand(): Boolean {
|
||||
return if (expandActionViewFromInteraction) {
|
||||
activity?.invalidateOptionsMenu()
|
||||
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
|
||||
|
||||
/**
|
||||
* Query from the view where applicable
|
||||
*/
|
||||
var query: String = ""
|
||||
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
try {
|
||||
super.onCreate(savedState)
|
||||
|
||||
@@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.ui.browse
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.os.bundleOf
|
||||
import com.bluelinelabs.conductor.Controller
|
||||
import com.bluelinelabs.conductor.ControllerChangeHandler
|
||||
@@ -51,10 +50,7 @@ class BrowseController :
|
||||
return resources!!.getString(R.string.browse)
|
||||
}
|
||||
|
||||
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
|
||||
binding = PagerControllerBinding.inflate(inflater)
|
||||
return binding.root
|
||||
}
|
||||
override fun createBinding(inflater: LayoutInflater) = PagerControllerBinding.inflate(inflater)
|
||||
|
||||
override fun onViewCreated(view: View) {
|
||||
super.onViewCreated(view)
|
||||
|
||||
@@ -5,11 +5,11 @@ import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.bluelinelabs.conductor.ControllerChangeHandler
|
||||
import com.bluelinelabs.conductor.ControllerChangeType
|
||||
import dev.chrisbanes.insetter.applyInsetter
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.kanade.tachiyomi.R
|
||||
@@ -56,14 +56,17 @@ open class ExtensionController :
|
||||
return ExtensionPresenter()
|
||||
}
|
||||
|
||||
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
|
||||
binding = ExtensionControllerBinding.inflate(inflater)
|
||||
return binding.root
|
||||
}
|
||||
override fun createBinding(inflater: LayoutInflater) = ExtensionControllerBinding.inflate(inflater)
|
||||
|
||||
override fun onViewCreated(view: View) {
|
||||
super.onViewCreated(view)
|
||||
|
||||
binding.recycler.applyInsetter {
|
||||
type(navigationBars = true) {
|
||||
padding()
|
||||
}
|
||||
}
|
||||
|
||||
binding.swipeRefresh.isRefreshing = true
|
||||
binding.swipeRefresh.refreshes()
|
||||
.onEach { presenter.findAvailableExtensions() }
|
||||
@@ -104,6 +107,8 @@ open class ExtensionController :
|
||||
override fun onButtonClick(position: Int) {
|
||||
val extension = (adapter?.getItem(position) as? ExtensionItem)?.extension ?: return
|
||||
when (extension) {
|
||||
is Extension.Available -> presenter.installExtension(extension)
|
||||
is Extension.Untrusted -> openTrustDialog(extension)
|
||||
is Extension.Installed -> {
|
||||
if (!extension.hasUpdate) {
|
||||
openDetails(extension)
|
||||
@@ -111,12 +116,6 @@ open class ExtensionController :
|
||||
presenter.updateExtension(extension)
|
||||
}
|
||||
}
|
||||
is Extension.Available -> {
|
||||
presenter.installExtension(extension)
|
||||
}
|
||||
is Extension.Untrusted -> {
|
||||
openTrustDialog(extension)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,12 +146,11 @@ open class ExtensionController :
|
||||
|
||||
override fun onItemClick(view: View, position: Int): Boolean {
|
||||
val extension = (adapter?.getItem(position) as? ExtensionItem)?.extension ?: return false
|
||||
if (extension is Extension.Installed) {
|
||||
openDetails(extension)
|
||||
} else if (extension is Extension.Untrusted) {
|
||||
openTrustDialog(extension)
|
||||
when (extension) {
|
||||
is Extension.Available -> presenter.installExtension(extension)
|
||||
is Extension.Untrusted -> openTrustDialog(extension)
|
||||
is Extension.Installed -> openDetails(extension)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
@@ -4,12 +4,12 @@ import android.annotation.SuppressLint
|
||||
import android.view.View
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.viewholders.FlexibleViewHolder
|
||||
import eu.kanade.tachiyomi.databinding.SourceMainControllerCardHeaderBinding
|
||||
import eu.kanade.tachiyomi.databinding.SectionHeaderItemBinding
|
||||
|
||||
class ExtensionGroupHolder(view: View, adapter: FlexibleAdapter<*>) :
|
||||
FlexibleViewHolder(view, adapter) {
|
||||
|
||||
private val binding = SourceMainControllerCardHeaderBinding.bind(view)
|
||||
private val binding = SectionHeaderItemBinding.bind(view)
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
fun bind(item: ExtensionGroupItem) {
|
||||
|
||||
@@ -19,7 +19,7 @@ data class ExtensionGroupItem(val name: String, val size: Int, val showSize: Boo
|
||||
* Returns the layout resource of this item.
|
||||
*/
|
||||
override fun getLayoutRes(): Int {
|
||||
return R.layout.source_main_controller_card_header
|
||||
return R.layout.section_header_item
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+9
-4
@@ -12,7 +12,6 @@ import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.view.ContextThemeWrapper
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.preference.Preference
|
||||
@@ -22,6 +21,7 @@ import androidx.preference.PreferenceScreen
|
||||
import androidx.preference.SwitchPreferenceCompat
|
||||
import androidx.recyclerview.widget.ConcatAdapter
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import dev.chrisbanes.insetter.applyInsetter
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.preference.EmptyPreferenceDataStore
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
@@ -64,10 +64,9 @@ class ExtensionDetailsController(bundle: Bundle? = null) :
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
|
||||
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
|
||||
override fun createBinding(inflater: LayoutInflater): ExtensionDetailControllerBinding {
|
||||
val themedInflater = inflater.cloneInContext(getPreferenceThemeContext())
|
||||
binding = ExtensionDetailControllerBinding.inflate(themedInflater)
|
||||
return binding.root
|
||||
return ExtensionDetailControllerBinding.inflate(themedInflater)
|
||||
}
|
||||
|
||||
override fun createPresenter(): ExtensionDetailsPresenter {
|
||||
@@ -82,6 +81,12 @@ class ExtensionDetailsController(bundle: Bundle? = null) :
|
||||
override fun onViewCreated(view: View) {
|
||||
super.onViewCreated(view)
|
||||
|
||||
binding.extensionPrefsRecycler.applyInsetter {
|
||||
type(navigationBars = true) {
|
||||
padding()
|
||||
}
|
||||
}
|
||||
|
||||
val extension = presenter.extension ?: return
|
||||
val context = view.context
|
||||
|
||||
|
||||
+2
-4
@@ -6,7 +6,6 @@ import android.os.Bundle
|
||||
import android.util.TypedValue
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.view.ContextThemeWrapper
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.preference.DialogPreference
|
||||
@@ -46,10 +45,9 @@ class SourcePreferencesController(bundle: Bundle? = null) :
|
||||
bundleOf(SOURCE_ID to sourceId)
|
||||
)
|
||||
|
||||
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
|
||||
override fun createBinding(inflater: LayoutInflater): SourcePreferencesControllerBinding {
|
||||
val themedInflater = inflater.cloneInContext(getPreferenceThemeContext())
|
||||
binding = SourcePreferencesControllerBinding.inflate(themedInflater)
|
||||
return binding.root
|
||||
return SourcePreferencesControllerBinding.inflate(themedInflater)
|
||||
}
|
||||
|
||||
override fun createPresenter(): SourcePreferencesPresenter {
|
||||
|
||||
@@ -2,11 +2,9 @@ package eu.kanade.tachiyomi.ui.browse.latest
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import dev.chrisbanes.insetter.applyInsetter
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.preference.asImmediateFlow
|
||||
@@ -33,22 +31,6 @@ open class LatestController :
|
||||
*/
|
||||
protected var adapter: LatestAdapter? = null
|
||||
|
||||
/*init {
|
||||
setHasOptionsMenu(true)
|
||||
}*/
|
||||
|
||||
/**
|
||||
* Initiate the view with [R.layout.global_search_controller].
|
||||
*
|
||||
* @param inflater used to load the layout xml.
|
||||
* @param container containing parent views.
|
||||
* @return inflated view
|
||||
*/
|
||||
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
|
||||
binding = LatestControllerBinding.inflate(inflater)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun getTitle(): String? {
|
||||
return applicationContext?.getString(R.string.latest)
|
||||
}
|
||||
@@ -82,33 +64,7 @@ open class LatestController :
|
||||
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
|
||||
}
|
||||
})*/
|
||||
}
|
||||
override fun createBinding(inflater: LayoutInflater): LatestControllerBinding = LatestControllerBinding.inflate(inflater)
|
||||
|
||||
/**
|
||||
* Called when the view is created
|
||||
@@ -118,6 +74,12 @@ open class LatestController :
|
||||
override fun onViewCreated(view: View) {
|
||||
super.onViewCreated(view)
|
||||
|
||||
binding.recycler.applyInsetter {
|
||||
type(navigationBars = true) {
|
||||
padding()
|
||||
}
|
||||
}
|
||||
|
||||
adapter = LatestAdapter(this)
|
||||
|
||||
// Create recycler and set adapter.
|
||||
|
||||
@@ -30,7 +30,6 @@ import uy.kohesive.injekt.api.get
|
||||
* @param preferences manages the preference calls.
|
||||
*/
|
||||
open class LatestPresenter(
|
||||
private val sourcesToUse: List<CatalogueSource>? = null,
|
||||
val sourceManager: SourceManager = Injekt.get(),
|
||||
val db: DatabaseHelper = Injekt.get(),
|
||||
val preferences: PreferencesHelper = Injekt.get()
|
||||
|
||||
+8
-5
@@ -6,12 +6,12 @@ import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.bluelinelabs.conductor.Router
|
||||
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||
import dev.chrisbanes.insetter.applyInsetter
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
@@ -45,14 +45,17 @@ class PreMigrationController(bundle: Bundle? = null) :
|
||||
|
||||
override fun getTitle() = view?.context?.getString(R.string.select_sources)
|
||||
|
||||
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
|
||||
binding = PreMigrationControllerBinding.inflate(inflater)
|
||||
return binding.root
|
||||
}
|
||||
override fun createBinding(inflater: LayoutInflater) = PreMigrationControllerBinding.inflate(inflater)
|
||||
|
||||
override fun onViewCreated(view: View) {
|
||||
super.onViewCreated(view)
|
||||
|
||||
binding.recycler.applyInsetter {
|
||||
type(navigationBars = true) {
|
||||
padding()
|
||||
}
|
||||
}
|
||||
|
||||
val ourAdapter = adapter ?: MigrationSourceAdapter(
|
||||
getEnabledSources().map { MigrationSourceItem(it, isEnabled(it.id.toString())) },
|
||||
this
|
||||
|
||||
+1
-9
@@ -24,7 +24,7 @@ class MigratingManga(
|
||||
|
||||
val migrationJob = parentContext + SupervisorJob() + Dispatchers.Default
|
||||
|
||||
var migrationStatus: Int = MigrationStatus.RUNNING
|
||||
var migrationStatus = MigrationStatus.RUNNING
|
||||
|
||||
@Volatile
|
||||
private var manga: Manga? = null
|
||||
@@ -42,11 +42,3 @@ class MigratingManga(
|
||||
return MigrationProcessItem(this)
|
||||
}
|
||||
}
|
||||
|
||||
class MigrationStatus {
|
||||
companion object {
|
||||
const val RUNNING = 0
|
||||
const val MANGA_FOUND = 1
|
||||
const val MANGA_NOT_FOUND = 2
|
||||
}
|
||||
}
|
||||
|
||||
+16
-9
@@ -8,7 +8,6 @@ import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.core.graphics.ColorUtils
|
||||
import androidx.core.os.bundleOf
|
||||
@@ -16,6 +15,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import com.bluelinelabs.conductor.changehandler.FadeChangeHandler
|
||||
import dev.chrisbanes.insetter.applyInsetter
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
@@ -35,7 +35,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.manga.MangaController
|
||||
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.withIOContext
|
||||
import eu.kanade.tachiyomi.util.system.getResourceColor
|
||||
@@ -48,7 +47,9 @@ import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Semaphore
|
||||
import kotlinx.coroutines.sync.withPermit
|
||||
import timber.log.Timber
|
||||
@@ -73,6 +74,7 @@ class MigrationListController(bundle: Bundle? = null) :
|
||||
|
||||
private val smartSearchEngine = SmartSearchEngine(config?.extraSearchParams)
|
||||
|
||||
private val migrationScope = CoroutineScope(Job() + Dispatchers.IO)
|
||||
var migrationsJob: Job? = null
|
||||
private set
|
||||
private var migratingManga: MutableList<MigratingManga>? = null
|
||||
@@ -81,25 +83,29 @@ class MigrationListController(bundle: Bundle? = null) :
|
||||
|
||||
private val throttleManager = EHentaiThrottleManager()
|
||||
|
||||
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
|
||||
binding = MigrationListControllerBinding.inflate(inflater)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun getTitle(): String {
|
||||
return resources?.getString(R.string.migration) + " (${adapter?.items?.count {
|
||||
it.manga.migrationStatus != MigrationStatus.RUNNING
|
||||
}}/${adapter?.itemCount ?: 0})"
|
||||
}
|
||||
|
||||
override fun createBinding(inflater: LayoutInflater) = MigrationListControllerBinding.inflate(inflater)
|
||||
|
||||
override fun onViewCreated(view: View) {
|
||||
super.onViewCreated(view)
|
||||
|
||||
binding.recycler.applyInsetter {
|
||||
type(navigationBars = true) {
|
||||
padding()
|
||||
}
|
||||
}
|
||||
|
||||
setTitle()
|
||||
val config = this.config ?: return
|
||||
|
||||
val newMigratingManga = migratingManga ?: run {
|
||||
val new = config.mangaIds.map {
|
||||
MigratingManga(db, sourceManager, it, viewScope.coroutineContext + Dispatchers.IO)
|
||||
MigratingManga(db, sourceManager, it, migrationScope.coroutineContext)
|
||||
}
|
||||
migratingManga = new.toMutableList()
|
||||
new
|
||||
@@ -114,7 +120,7 @@ class MigrationListController(bundle: Bundle? = null) :
|
||||
adapter?.updateDataSet(newMigratingManga.map { it.toModal() })
|
||||
|
||||
if (migrationsJob == null) {
|
||||
migrationsJob = viewScope.launchIO {
|
||||
migrationsJob = migrationScope.launch {
|
||||
runMigrations(newMigratingManga)
|
||||
}
|
||||
}
|
||||
@@ -275,6 +281,7 @@ class MigrationListController(bundle: Bundle? = null) :
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
|
||||
migrationScope.cancel()
|
||||
activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
||||
}
|
||||
|
||||
|
||||
+21
-6
@@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.ui.browse.migration.advanced.process
|
||||
import android.view.MenuItem
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.History
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaCategory
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
@@ -102,16 +103,30 @@ class MigrationProcessAdapter(
|
||||
// Update chapters read
|
||||
if (MigrationFlags.hasChapters(flags)) {
|
||||
val prevMangaChapters = db.getChapters(prevManga).executeAsBlocking()
|
||||
val maxChapterRead = prevMangaChapters.filter { it.read }.maxByOrNull { it.chapter_number }?.chapter_number
|
||||
if (maxChapterRead != null) {
|
||||
val dbChapters = db.getChapters(manga).executeAsBlocking()
|
||||
for (chapter in dbChapters) {
|
||||
if (chapter.isRecognizedNumber && chapter.chapter_number <= maxChapterRead) {
|
||||
val maxChapterRead =
|
||||
prevMangaChapters.filter { it.read }.maxOfOrNull { it.chapter_number }
|
||||
val dbChapters = db.getChapters(manga).executeAsBlocking()
|
||||
val prevHistoryList = db.getHistoryByMangaId(prevManga.id!!).executeAsBlocking()
|
||||
val historyList = mutableListOf<History>()
|
||||
for (chapter in dbChapters) {
|
||||
if (chapter.isRecognizedNumber) {
|
||||
val prevChapter =
|
||||
prevMangaChapters.find { it.isRecognizedNumber && it.chapter_number == chapter.chapter_number }
|
||||
if (prevChapter != null) {
|
||||
chapter.bookmark = prevChapter.bookmark
|
||||
chapter.read = prevChapter.read
|
||||
chapter.date_fetch = prevChapter.date_fetch
|
||||
prevHistoryList.find { it.chapter_id == prevChapter.id }?.let { prevHistory ->
|
||||
val history = History.create(chapter).apply { last_read = prevHistory.last_read }
|
||||
historyList.add(history)
|
||||
}
|
||||
} else if (maxChapterRead != null && chapter.chapter_number <= maxChapterRead) {
|
||||
chapter.read = true
|
||||
}
|
||||
}
|
||||
db.insertChapters(dbChapters).executeAsBlocking()
|
||||
}
|
||||
db.insertChapters(dbChapters).executeAsBlocking()
|
||||
db.updateHistoryLastRead(historyList).executeAsBlocking()
|
||||
}
|
||||
// Update categories
|
||||
if (MigrationFlags.hasCategories(flags)) {
|
||||
|
||||
+6
-6
@@ -55,14 +55,12 @@ class MigrationProcessHolder(
|
||||
|
||||
binding.migrationMenu.setVectorCompat(
|
||||
R.drawable.ic_more_vert_24dp,
|
||||
view.context
|
||||
.getResourceColor(R.attr.colorOnPrimary)
|
||||
view.context.getResourceColor(R.attr.colorOnPrimary)
|
||||
)
|
||||
binding.skipManga.setVectorCompat(
|
||||
R.drawable.ic_close_24dp,
|
||||
view.context.getResourceColor(
|
||||
R
|
||||
.attr.colorOnPrimary
|
||||
R.attr.colorOnPrimary
|
||||
)
|
||||
)
|
||||
binding.migrationMenu.isInvisible = true
|
||||
@@ -79,7 +77,8 @@ class MigrationProcessHolder(
|
||||
true
|
||||
).withFadeTransaction()
|
||||
)
|
||||
}.launchIn(adapter.controller.viewScope)
|
||||
}
|
||||
.launchIn(adapter.controller.viewScope)
|
||||
}
|
||||
|
||||
/*launchUI {
|
||||
@@ -115,7 +114,8 @@ class MigrationProcessHolder(
|
||||
true
|
||||
).withFadeTransaction()
|
||||
)
|
||||
}.launchIn(adapter.controller.viewScope)
|
||||
}
|
||||
.launchIn(adapter.controller.viewScope)
|
||||
} else {
|
||||
binding.migrationMangaCardTo.loadingGroup.isVisible = false
|
||||
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
|
||||
}
|
||||
+8
-5
@@ -3,9 +3,9 @@ package eu.kanade.tachiyomi.ui.browse.migration.manga
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import dev.chrisbanes.insetter.applyInsetter
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
@@ -51,14 +51,17 @@ class MigrationMangaController :
|
||||
return MigrationMangaPresenter(sourceId)
|
||||
}
|
||||
|
||||
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
|
||||
binding = MigrationMangaControllerBinding.inflate(inflater)
|
||||
return binding.root
|
||||
}
|
||||
override fun createBinding(inflater: LayoutInflater) = MigrationMangaControllerBinding.inflate(inflater)
|
||||
|
||||
override fun onViewCreated(view: View) {
|
||||
super.onViewCreated(view)
|
||||
|
||||
binding.recycler.applyInsetter {
|
||||
type(navigationBars = true) {
|
||||
padding()
|
||||
}
|
||||
}
|
||||
|
||||
adapter = MigrationMangaAdapter(this)
|
||||
binding.recycler.layoutManager = LinearLayoutManager(view.context)
|
||||
binding.recycler.adapter = adapter
|
||||
|
||||
+72
-25
@@ -5,9 +5,12 @@ import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.core.os.bundleOf
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import com.afollestad.materialdialogs.list.listItemsMultiChoice
|
||||
import com.bluelinelabs.conductor.Controller
|
||||
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.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
@@ -24,17 +27,37 @@ import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import reactivecircus.flowbinding.appcompat.QueryTextEvent
|
||||
import reactivecircus.flowbinding.appcompat.queryTextEvents
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class SearchController(
|
||||
private var manga: Manga? = 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 progress = 1
|
||||
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.
|
||||
*/
|
||||
@@ -58,31 +81,15 @@ class SearchController(
|
||||
)
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
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() {
|
||||
fun migrateManga(manga: Manga, newManga: Manga) {
|
||||
val target = targetController as? MigrationInterface ?: return
|
||||
val manga = manga ?: return
|
||||
val newManga = newManga ?: return
|
||||
|
||||
val nextManga = target.migrateManga(manga, newManga, true)
|
||||
replaceWithNewSearchController(nextManga)
|
||||
}
|
||||
|
||||
fun copyManga() {
|
||||
fun copyManga(manga: Manga, newManga: Manga) {
|
||||
val target = targetController as? MigrationInterface ?: return
|
||||
val manga = manga ?: return
|
||||
val newManga = newManga ?: return
|
||||
|
||||
val nextManga = target.migrateManga(manga, newManga, false)
|
||||
replaceWithNewSearchController(nextManga)
|
||||
@@ -102,14 +109,15 @@ class SearchController(
|
||||
override fun onMangaClick(manga: Manga) {
|
||||
if (targetController is MigrationListController) {
|
||||
val migrationListController = targetController as? MigrationListController
|
||||
val sourceManager: SourceManager by injectLazy()
|
||||
val sourceManager = Injekt.get<SourceManager>()
|
||||
val source = sourceManager.get(manga.source) ?: return
|
||||
migrationListController?.useMangaForMigration(manga, source)
|
||||
router.popCurrentController()
|
||||
return
|
||||
}
|
||||
newManga = manga
|
||||
val dialog = MigrationDialog()
|
||||
val dialog =
|
||||
MigrationDialog(this.manga ?: return, newManga ?: return, this)
|
||||
dialog.targetController = this
|
||||
dialog.showDialog(router)
|
||||
}
|
||||
@@ -119,12 +127,26 @@ class SearchController(
|
||||
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()
|
||||
|
||||
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
||||
val prefValue = preferences.migrateFlags().get()
|
||||
val callingController = callingController
|
||||
|
||||
val preselected =
|
||||
MigrationFlags.getEnabledFlagsPositions(
|
||||
@@ -132,7 +154,7 @@ class SearchController(
|
||||
)
|
||||
|
||||
return MaterialDialog(activity!!)
|
||||
.message(R.string.migration_dialog_what_to_include)
|
||||
.title(R.string.migration_dialog_what_to_include)
|
||||
.listItemsMultiChoice(
|
||||
items = MigrationFlags.titles.map { resources?.getString(it) as CharSequence },
|
||||
initialSelection = preselected.toIntArray()
|
||||
@@ -145,13 +167,27 @@ class SearchController(
|
||||
preferences.migrateFlags().set(newValue)
|
||||
}
|
||||
.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) {
|
||||
(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)
|
||||
}
|
||||
companion object {
|
||||
const val MANGA_KEY = "manga_key"
|
||||
const val NEW_MANGA_KEY = "new_manga_key"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -183,4 +219,15 @@ class SearchController(
|
||||
}
|
||||
.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"
|
||||
}
|
||||
}
|
||||
+8
-5
@@ -5,8 +5,8 @@ import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import dev.chrisbanes.insetter.applyInsetter
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
@@ -41,14 +41,17 @@ class MigrationSourcesController :
|
||||
return MigrationSourcesPresenter()
|
||||
}
|
||||
|
||||
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
|
||||
binding = MigrationSourcesControllerBinding.inflate(inflater)
|
||||
return binding.root
|
||||
}
|
||||
override fun createBinding(inflater: LayoutInflater) = MigrationSourcesControllerBinding.inflate(inflater)
|
||||
|
||||
override fun onViewCreated(view: View) {
|
||||
super.onViewCreated(view)
|
||||
|
||||
binding.recycler.applyInsetter {
|
||||
type(navigationBars = true) {
|
||||
padding()
|
||||
}
|
||||
}
|
||||
|
||||
adapter = SourceAdapter(this)
|
||||
binding.recycler.layoutManager = LinearLayoutManager(view.context)
|
||||
binding.recycler.adapter = adapter
|
||||
|
||||
+9
-4
@@ -28,9 +28,14 @@ class MigrationSourcesPresenter(
|
||||
|
||||
private fun findSourcesWithManga(library: List<Manga>): List<SourceItem> {
|
||||
val header = SelectionHeader()
|
||||
return library.asSequence().map { it.source }.toSet()
|
||||
.mapNotNull { if (it != LocalSource.ID /* SY --> */ && it != MERGED_SOURCE_ID /* SY <-- */) sourceManager.getOrStub(it) else null }
|
||||
.sortedBy { it.name.toLowerCase() }
|
||||
.map { SourceItem(it, header) }.toList()
|
||||
return library
|
||||
.groupBy { it.source }
|
||||
.filterKeys { it != LocalSource.ID /* SY --> */ && it != MERGED_SOURCE_ID /* SY <-- */ }
|
||||
.map {
|
||||
val source = sourceManager.getOrStub(it.key)
|
||||
SourceItem(source, it.value.size, header)
|
||||
}
|
||||
.sortedBy { it.source.name.toLowerCase() }
|
||||
.toList()
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -7,7 +7,7 @@ import eu.davidea.flexibleadapter.items.AbstractHeaderItem
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.davidea.viewholders.FlexibleViewHolder
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.databinding.SourceMainControllerCardHeaderBinding
|
||||
import eu.kanade.tachiyomi.databinding.SectionHeaderItemBinding
|
||||
|
||||
/**
|
||||
* Item that contains the selection header.
|
||||
@@ -18,7 +18,7 @@ class SelectionHeader : AbstractHeaderItem<SelectionHeader.Holder>() {
|
||||
* Returns the layout resource of this item.
|
||||
*/
|
||||
override fun getLayoutRes(): Int {
|
||||
return R.layout.source_main_controller_card_header
|
||||
return R.layout.section_header_item
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -46,7 +46,7 @@ class SelectionHeader : AbstractHeaderItem<SelectionHeader.Holder>() {
|
||||
}
|
||||
|
||||
class Holder(view: View, adapter: FlexibleAdapter</* SY --> */ IFlexible<RecyclerView.ViewHolder> /* SY <-- */>) : FlexibleViewHolder(view, adapter) {
|
||||
private val binding = SourceMainControllerCardHeaderBinding.bind(view)
|
||||
private val binding = SectionHeaderItemBinding.bind(view)
|
||||
|
||||
init {
|
||||
binding.title.text = view.context.getString(/* SY --> */ R.string.select_a_source_to_migrate_from /* SY <-- */)
|
||||
|
||||
@@ -26,8 +26,8 @@ class SourceHolder(view: View, val adapter: SourceAdapter) :
|
||||
fun bind(item: SourceItem) {
|
||||
val source = item.source
|
||||
|
||||
binding.title.text = source.name
|
||||
binding.subtitle.isVisible = true
|
||||
binding.title.text = "${source.name} (${item.mangaCount})"
|
||||
binding.subtitle.isVisible = source.lang != ""
|
||||
binding.subtitle.text = LocaleHelper.getDisplayName(source.lang)
|
||||
|
||||
itemView.post {
|
||||
|
||||
@@ -14,7 +14,7 @@ import eu.kanade.tachiyomi.source.Source
|
||||
* @param source Instance of [Source] containing source information.
|
||||
* @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) {
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,13 +3,13 @@ package eu.kanade.tachiyomi.ui.browse.source
|
||||
import android.view.View
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.viewholders.FlexibleViewHolder
|
||||
import eu.kanade.tachiyomi.databinding.SourceMainControllerCardHeaderBinding
|
||||
import eu.kanade.tachiyomi.databinding.SectionHeaderItemBinding
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
|
||||
class LangHolder(view: View, adapter: FlexibleAdapter<*>) :
|
||||
FlexibleViewHolder(view, adapter) {
|
||||
|
||||
private val binding = SourceMainControllerCardHeaderBinding.bind(view)
|
||||
private val binding = SectionHeaderItemBinding.bind(view)
|
||||
|
||||
fun bind(item: LangItem) {
|
||||
binding.title.text = LocaleHelper.getSourceDisplayName(item.code, itemView.context)
|
||||
|
||||
@@ -18,7 +18,7 @@ data class LangItem(val code: String) : AbstractHeaderItem<LangHolder>() {
|
||||
* Returns the layout resource of this item.
|
||||
*/
|
||||
override fun getLayoutRes(): Int {
|
||||
return R.layout.source_main_controller_card_header
|
||||
return R.layout.section_header_item
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -9,14 +9,13 @@ import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import com.afollestad.materialdialogs.list.listItems
|
||||
import com.bluelinelabs.conductor.ControllerChangeHandler
|
||||
import com.bluelinelabs.conductor.ControllerChangeType
|
||||
import dev.chrisbanes.insetter.applyInsetter
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.kanade.tachiyomi.R
|
||||
@@ -28,7 +27,7 @@ import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.LocalSource
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
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.withFadeTransaction
|
||||
import eu.kanade.tachiyomi.ui.browse.BrowseController
|
||||
@@ -39,12 +38,7 @@ import eu.kanade.tachiyomi.ui.browse.source.latest.LatestUpdatesController
|
||||
import eu.kanade.tachiyomi.ui.category.sources.ChangeSourceCategoriesDialog
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
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 reactivecircus.flowbinding.appcompat.QueryTextEvent
|
||||
import reactivecircus.flowbinding.appcompat.queryTextEvents
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
@@ -55,7 +49,7 @@ import uy.kohesive.injekt.api.get
|
||||
* [SourceAdapter.OnLatestClickListener] call function data on latest item click
|
||||
*/
|
||||
class SourceController(bundle: Bundle? = null) :
|
||||
NucleusController<SourceMainControllerBinding, SourcePresenter>(bundle),
|
||||
SearchableNucleusController<SourceMainControllerBinding, SourcePresenter>(bundle),
|
||||
FlexibleAdapter.OnItemClickListener,
|
||||
FlexibleAdapter.OnItemLongClickListener,
|
||||
SourceAdapter.OnSourceClickListener,
|
||||
@@ -93,21 +87,17 @@ class SourceController(bundle: Bundle? = null) :
|
||||
return SourcePresenter(/* SY --> */ controllerMode = mode /* SY <-- */)
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiate the view with [R.layout.source_main_controller].
|
||||
*
|
||||
* @param inflater used to load the layout xml.
|
||||
* @param container containing parent views.
|
||||
* @return inflated view.
|
||||
*/
|
||||
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
|
||||
binding = SourceMainControllerBinding.inflate(inflater)
|
||||
return binding.root
|
||||
}
|
||||
override fun createBinding(inflater: LayoutInflater) = SourceMainControllerBinding.inflate(inflater)
|
||||
|
||||
override fun onViewCreated(view: View) {
|
||||
super.onViewCreated(view)
|
||||
|
||||
binding.recycler.applyInsetter {
|
||||
type(navigationBars = true) {
|
||||
padding()
|
||||
}
|
||||
}
|
||||
|
||||
adapter = SourceAdapter(this)
|
||||
|
||||
// Create recycler and set adapter.
|
||||
@@ -203,7 +193,7 @@ class SourceController(bundle: Bundle? = null) :
|
||||
|
||||
items.add(
|
||||
Pair(
|
||||
activity.getString(R.string.label_categories),
|
||||
activity.getString(R.string.categories),
|
||||
{ addToCategories(item.source) }
|
||||
)
|
||||
)
|
||||
@@ -333,44 +323,6 @@ class SourceController(bundle: Bundle? = null) :
|
||||
}
|
||||
// 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.
|
||||
*
|
||||
@@ -431,6 +383,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 -->
|
||||
@Parcelize
|
||||
data class SmartSearchConfig(val origTitle: String, val origMangaId: Long? = null) : Parcelable
|
||||
|
||||
@@ -8,7 +8,6 @@ import eu.kanade.tachiyomi.databinding.SourceMainControllerCardItemBinding
|
||||
import eu.kanade.tachiyomi.source.LocalSource
|
||||
import eu.kanade.tachiyomi.source.icon
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
import eu.kanade.tachiyomi.util.system.getResourceColor
|
||||
import eu.kanade.tachiyomi.util.view.setVectorCompat
|
||||
|
||||
class SourceHolder(private val view: View, val adapter: SourceAdapter /* SY --> */, private val showLatest: Boolean, private val showPins: Boolean /* SY <-- */) :
|
||||
@@ -52,9 +51,9 @@ class SourceHolder(private val view: View, val adapter: SourceAdapter /* SY -->
|
||||
|
||||
binding.pin.isVisible = showPins
|
||||
if (item.isPinned) {
|
||||
binding.pin.setVectorCompat(R.drawable.ic_push_pin_24dp, view.context.getResourceColor(R.attr.colorAccent))
|
||||
binding.pin.setVectorCompat(R.drawable.ic_push_pin_24dp, R.attr.colorAccent)
|
||||
} else {
|
||||
binding.pin.setVectorCompat(R.drawable.ic_push_pin_outline_24dp, view.context.getResourceColor(android.R.attr.textColorHint))
|
||||
binding.pin.setVectorCompat(R.drawable.ic_push_pin_outline_24dp, android.R.attr.textColorHint)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+21
-34
@@ -8,7 +8,6 @@ import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
@@ -17,10 +16,10 @@ import androidx.recyclerview.widget.RecyclerView
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import com.afollestad.materialdialogs.input.input
|
||||
import com.afollestad.materialdialogs.list.listItems
|
||||
import com.elvishew.xlog.XLog
|
||||
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.tfcporciuncula.flow.Preference
|
||||
import dev.chrisbanes.insetter.applyInsetter
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
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.all.MangaDex
|
||||
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.browse.extension.details.SourcePreferencesController
|
||||
import eu.kanade.tachiyomi.ui.browse.source.SourceController
|
||||
@@ -57,25 +56,23 @@ import eu.kanade.tachiyomi.util.view.shrinkOnScroll
|
||||
import eu.kanade.tachiyomi.util.view.snack
|
||||
import eu.kanade.tachiyomi.widget.AutofitRecyclerView
|
||||
import eu.kanade.tachiyomi.widget.EmptyView
|
||||
import exh.log.xLogW
|
||||
import exh.md.similar.ui.EnableMangaDexSimilarDialogController
|
||||
import exh.savedsearches.EXHSavedSearch
|
||||
import exh.source.getMainSource
|
||||
import exh.source.isEhBasedSource
|
||||
import exh.widget.preference.MangadexLoginDialog
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.drop
|
||||
import kotlinx.coroutines.flow.filter
|
||||
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
|
||||
|
||||
/**
|
||||
* Controller to manage the catalogues available in the app.
|
||||
*/
|
||||
open class BrowseSourceController(bundle: Bundle) :
|
||||
NucleusController<SourceControllerBinding, BrowseSourcePresenter>(bundle),
|
||||
SearchableNucleusController<SourceControllerBinding, BrowseSourcePresenter>(bundle),
|
||||
FabController,
|
||||
FlexibleAdapter.OnItemClickListener,
|
||||
FlexibleAdapter.OnItemLongClickListener,
|
||||
@@ -162,10 +159,7 @@ open class BrowseSourceController(bundle: Bundle) :
|
||||
// SY <--
|
||||
}
|
||||
|
||||
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
|
||||
binding = SourceControllerBinding.inflate(inflater)
|
||||
return binding.root
|
||||
}
|
||||
override fun createBinding(inflater: LayoutInflater) = SourceControllerBinding.inflate(inflater)
|
||||
|
||||
override fun onViewCreated(view: View) {
|
||||
super.onViewCreated(view)
|
||||
@@ -186,8 +180,8 @@ open class BrowseSourceController(bundle: Bundle) :
|
||||
preferences.shownMangaDexSimilarAskDialog().set(true)
|
||||
}
|
||||
|
||||
if (mainSource is LoginSource && mainSource.needsLogin && !mainSource.isLogged()) {
|
||||
val dialog = mainSource.getLoginDialog(mainSource, activity!!)
|
||||
if (mainSource is LoginSource && mainSource.requiresLogin && !mainSource.isLogged()) {
|
||||
val dialog = MangadexLoginDialog(mainSource)
|
||||
dialog.showDialog(router)
|
||||
}
|
||||
// SY <--
|
||||
@@ -389,6 +383,11 @@ open class BrowseSourceController(bundle: Bundle) :
|
||||
actionFab?.shrinkOnScroll(recycler)
|
||||
}
|
||||
|
||||
recycler.applyInsetter {
|
||||
type(navigationBars = true) {
|
||||
padding()
|
||||
}
|
||||
}
|
||||
recycler.setHasFixedSize(true)
|
||||
recycler.adapter = adapter
|
||||
|
||||
@@ -401,25 +400,8 @@ open class BrowseSourceController(bundle: Bundle) :
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.source_browse, menu)
|
||||
|
||||
// Initialize search menu
|
||||
createOptionsMenu(menu, inflater, R.menu.source_browse, 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(
|
||||
onExpand = { invalidateMenuOnExpand() },
|
||||
@@ -427,6 +409,7 @@ open class BrowseSourceController(bundle: Bundle) :
|
||||
if (router.backstackSize >= 2 && router.backstack[router.backstackSize - 2].controller() is GlobalSearchController) {
|
||||
router.popController(this)
|
||||
} else {
|
||||
nonSubmittedQuery = ""
|
||||
searchWithQuery("")
|
||||
}
|
||||
|
||||
@@ -450,6 +433,10 @@ open class BrowseSourceController(bundle: Bundle) :
|
||||
// SY <--
|
||||
}
|
||||
|
||||
override fun onSearchViewQueryTextSubmit(query: String?) {
|
||||
searchWithQuery(query ?: "")
|
||||
}
|
||||
|
||||
override fun onPrepareOptionsMenu(menu: Menu) {
|
||||
super.onPrepareOptionsMenu(menu)
|
||||
|
||||
@@ -542,8 +529,8 @@ open class BrowseSourceController(bundle: Bundle) :
|
||||
*/
|
||||
/* SY --> */ open /* SY <-- */fun onAddPageError(error: Throwable) {
|
||||
// SY -->
|
||||
XLog.tag("BrowseSourceController").enableStackTrace(2).w("> Failed to load next catalogue page!", error)
|
||||
XLog.tag("BrowseSourceController").enableStackTrace(2).w(
|
||||
xLogW("> Failed to load next catalogue page!", error)
|
||||
xLogW(
|
||||
"> (source.id: %s, source.name: %s)",
|
||||
presenter.source.id,
|
||||
presenter.source.name
|
||||
|
||||
+4
-6
@@ -81,12 +81,6 @@ open class BrowseSourcePresenter(
|
||||
*/
|
||||
lateinit var source: CatalogueSource
|
||||
|
||||
/**
|
||||
* Query from the view.
|
||||
*/
|
||||
var query = searchQuery ?: ""
|
||||
private set
|
||||
|
||||
/**
|
||||
* Modifiable list of filters.
|
||||
*/
|
||||
@@ -129,6 +123,10 @@ open class BrowseSourcePresenter(
|
||||
private val filterSerializer = FilterSerializer()
|
||||
// SY <--
|
||||
|
||||
init {
|
||||
query = searchQuery ?: ""
|
||||
}
|
||||
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
super.onCreate(savedState)
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import android.view.ViewGroup
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.ConcatAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.chip.Chip
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
@@ -37,10 +38,17 @@ class SourceFilterSheet(
|
||||
// EXH <--
|
||||
) : BaseBottomSheetDialog(activity) {
|
||||
|
||||
private var filterNavView: FilterNavigationView
|
||||
private var filterNavView: FilterNavigationView = FilterNavigationView(
|
||||
activity,
|
||||
// SY -->
|
||||
searches = searches,
|
||||
source = source,
|
||||
controller = controller
|
||||
// SY <--
|
||||
)
|
||||
private val sheetBehavior: BottomSheetBehavior<*>
|
||||
|
||||
init {
|
||||
filterNavView = FilterNavigationView(activity /* SY --> */, searches = searches, source = source, controller = controller/* SY <-- */)
|
||||
filterNavView.onFilterClicked = {
|
||||
onFilterClicked()
|
||||
this.dismiss()
|
||||
@@ -56,6 +64,13 @@ class SourceFilterSheet(
|
||||
// EXH <--
|
||||
|
||||
setContentView(filterNavView)
|
||||
|
||||
sheetBehavior = BottomSheetBehavior.from(filterNavView.parent as ViewGroup)
|
||||
}
|
||||
|
||||
override fun show() {
|
||||
super.show()
|
||||
sheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED
|
||||
}
|
||||
|
||||
fun setFilters(items: List<IFlexible<*>>) {
|
||||
@@ -72,7 +87,15 @@ class SourceFilterSheet(
|
||||
}
|
||||
// SY <--
|
||||
|
||||
class FilterNavigationView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null /* SY --> */, searches: List<EXHSavedSearch> = emptyList(), source: CatalogueSource? = null, controller: BaseController<*>? = null/* SY <-- */) :
|
||||
class FilterNavigationView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
// SY -->
|
||||
searches: List<EXHSavedSearch> = emptyList(),
|
||||
source: CatalogueSource? = null,
|
||||
controller: BaseController<*>? = null
|
||||
// SY <--
|
||||
) :
|
||||
SimpleNavigationView(context, attrs) {
|
||||
|
||||
var onFilterClicked = {}
|
||||
@@ -91,8 +114,13 @@ class SourceFilterSheet(
|
||||
// SY <--
|
||||
|
||||
val adapter: FlexibleAdapter<IFlexible<*>> = FlexibleAdapter<IFlexible<*>>(null)
|
||||
.setDisplayHeadersAtStartUp(true)
|
||||
|
||||
private val binding = SourceFilterSheetBinding.inflate(LayoutInflater.from(context), null, false)
|
||||
private val binding = SourceFilterSheetBinding.inflate(
|
||||
LayoutInflater.from(context),
|
||||
null,
|
||||
false
|
||||
)
|
||||
|
||||
init {
|
||||
// SY -->
|
||||
|
||||
@@ -7,7 +7,6 @@ import android.widget.AutoCompleteTextView
|
||||
import android.widget.TextView
|
||||
import androidx.core.widget.addTextChangedListener
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.elvishew.xlog.XLog
|
||||
import com.google.android.material.chip.Chip
|
||||
import com.google.android.material.chip.ChipGroup
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
@@ -17,6 +16,7 @@ import eu.davidea.viewholders.FlexibleViewHolder
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.widget.AutoCompleteAdapter
|
||||
import exh.log.xLogD
|
||||
|
||||
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)
|
||||
filter.state += name
|
||||
} else {
|
||||
XLog.tag("AutoComplete").d("Invalid tag: $name")
|
||||
xLogD("Invalid tag: %s", name)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user