Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0413d502c1 |
@@ -1 +1,2 @@
|
|||||||
|
github: inorichi
|
||||||
ko_fi: inorichi
|
ko_fi: inorichi
|
||||||
|
|||||||
@@ -2,15 +2,9 @@
|
|||||||
|
|
||||||
I acknowledge that:
|
I acknowledge that:
|
||||||
|
|
||||||
- I have updated:
|
- I have updated to the latest version of the app (stable is v1.5.0)
|
||||||
- To the latest version of the app (stable is v1.8.1)
|
- I have updated all extensions
|
||||||
- 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
|
- 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**
|
**DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT**
|
||||||
|
|
||||||
@@ -30,5 +24,3 @@ Note that the issue will be automatically closed if you do not fill out the titl
|
|||||||
|
|
||||||
## Other details
|
## Other details
|
||||||
Additional details and attachments.
|
Additional details and attachments.
|
||||||
|
|
||||||
If you're experiencing crashes, share the crash logs from More → Settings → Advanced → Dump crash logs.
|
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
---
|
||||||
|
name: "🐞 Bug report"
|
||||||
|
about: Report a bug
|
||||||
|
title: "[Bug] <Write short description here>"
|
||||||
|
labels: "bug"
|
||||||
|
---
|
||||||
|
|
||||||
|
**PLEASE READ THIS**
|
||||||
|
|
||||||
|
I acknowledge that:
|
||||||
|
|
||||||
|
- I have updated to the latest version of the app (stable is v1.5.0)
|
||||||
|
- I have updated all extensions
|
||||||
|
- If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions
|
||||||
|
|
||||||
|
**DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Device information
|
||||||
|
* Tachiyomi version: ?
|
||||||
|
* Android version: ?
|
||||||
|
* Device: ?
|
||||||
|
|
||||||
|
## Steps to reproduce
|
||||||
|
1. First step
|
||||||
|
2. Second step
|
||||||
|
|
||||||
|
### Expected behavior
|
||||||
|
This should happen.
|
||||||
|
|
||||||
|
### Actual behavior
|
||||||
|
This happened instead.
|
||||||
|
|
||||||
|
## Other details
|
||||||
|
Additional details and attachments.
|
||||||
@@ -1,11 +1,8 @@
|
|||||||
blank_issues_enabled: false
|
blank_issues_enabled: false
|
||||||
contact_links:
|
contact_links:
|
||||||
- name: ⚠️ Extension/source issue
|
- name: Tachiyomi help website
|
||||||
url: https://github.com/tachiyomiorg/tachiyomi-extensions/issues/new/choose
|
|
||||||
about: Issues and requests for extensions and sources should be opened in the tachiyomi-extensions repository instead
|
|
||||||
- name: 📦 Tachiyomi extensions
|
|
||||||
url: https://tachiyomi.org/extensions
|
|
||||||
about: List of all available extensions with download links
|
|
||||||
- name: 🖥️ Tachiyomi website
|
|
||||||
url: https://tachiyomi.org/help/
|
url: https://tachiyomi.org/help/
|
||||||
about: Guides, troubleshooting, and answers to common questions
|
about: Common questions are answered here.
|
||||||
|
- name: Tachiyomi extensions GitHub repository
|
||||||
|
url: https://github.com/tachiyomiorg/tachiyomi-extensions
|
||||||
|
about: Issues about an extension/source/catalogue should be opened here instead.
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
---
|
||||||
|
name: "🌟 Feature request"
|
||||||
|
about: Suggest a feature to improve Tachiyomi
|
||||||
|
title: "[Feature Request] <Write short description here>"
|
||||||
|
labels: "feature"
|
||||||
|
---
|
||||||
|
|
||||||
|
**PLEASE READ THIS**
|
||||||
|
|
||||||
|
I acknowledge that:
|
||||||
|
|
||||||
|
- I have updated to the latest version of the app (stable is v1.5.0)
|
||||||
|
- I have updated all extensions
|
||||||
|
- If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions
|
||||||
|
|
||||||
|
**DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Why/User Benefit/User Problem
|
||||||
|
(explain why this feature should be added)
|
||||||
|
|
||||||
|
## What/Requirements
|
||||||
|
(explain how this feature would behave)
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
name: 🐞 Issue report
|
|
||||||
description: Report an issue in Tachiyomi
|
|
||||||
labels: [Bug]
|
|
||||||
body:
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: reproduce-steps
|
|
||||||
attributes:
|
|
||||||
label: Steps to reproduce
|
|
||||||
description: Provide an example of the issue.
|
|
||||||
placeholder: |
|
|
||||||
Example:
|
|
||||||
1. First step
|
|
||||||
2. Second step
|
|
||||||
3. Issue here
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: expected-behavior
|
|
||||||
attributes:
|
|
||||||
label: Expected behavior
|
|
||||||
description: Explain what you should expect to happen.
|
|
||||||
placeholder: |
|
|
||||||
Example:
|
|
||||||
"This should happen..."
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: actual-behavior
|
|
||||||
attributes:
|
|
||||||
label: Actual behavior
|
|
||||||
description: Explain what actually happens.
|
|
||||||
placeholder: |
|
|
||||||
Example:
|
|
||||||
"This happened instead..."
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: crash-logs
|
|
||||||
attributes:
|
|
||||||
label: Crash logs
|
|
||||||
description: |
|
|
||||||
If you're experiencing crashes, share the crash logs from **More → Settings → Advanced** then press **Dump crash logs**.
|
|
||||||
placeholder: |
|
|
||||||
You can paste the crash logs in pure text or upload it as an attachment.
|
|
||||||
|
|
||||||
- type: input
|
|
||||||
id: tachiyomi-version
|
|
||||||
attributes:
|
|
||||||
label: Tachiyomi version
|
|
||||||
description: You can find your Tachiyomi version in **More → About**.
|
|
||||||
placeholder: |
|
|
||||||
Example: "1.8.1"
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: input
|
|
||||||
id: android-version
|
|
||||||
attributes:
|
|
||||||
label: Android version
|
|
||||||
description: You can find this somewhere in your Android settings.
|
|
||||||
placeholder: |
|
|
||||||
Example: "Android 11"
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: input
|
|
||||||
id: device
|
|
||||||
attributes:
|
|
||||||
label: Device
|
|
||||||
description: List your device and model.
|
|
||||||
placeholder: |
|
|
||||||
Example: "Google Pixel 5"
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: other-details
|
|
||||||
attributes:
|
|
||||||
label: Other details
|
|
||||||
placeholder: |
|
|
||||||
Additional details and attachments.
|
|
||||||
|
|
||||||
- type: checkboxes
|
|
||||||
id: acknowledgements
|
|
||||||
attributes:
|
|
||||||
label: Acknowledgements
|
|
||||||
description: Read this carefully, we will close and ignore your issue if you skimmed through this.
|
|
||||||
options:
|
|
||||||
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open issue.
|
|
||||||
required: true
|
|
||||||
- label: I have written a short but informative title.
|
|
||||||
required: true
|
|
||||||
- label: If this is an issue with an extension, I should be opening an issue in the [extensions repository](https://github.com/tachiyomiorg/tachiyomi-extensions/issues/new/choose).
|
|
||||||
required: true
|
|
||||||
- label: I have tried the [troubleshooting guide](https://tachiyomi.org/help/guides/troubleshooting/).
|
|
||||||
required: true
|
|
||||||
- label: I have updated the app to version **[1.8.1](https://github.com/jobobby04/tachiyomisy/releases/latest)**.
|
|
||||||
required: true
|
|
||||||
- label: I have updated all installed extensions.
|
|
||||||
required: true
|
|
||||||
- label: I will fill out all of the requested information in this form.
|
|
||||||
required: true
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
name: ⭐ Feature request
|
|
||||||
description: Suggest a feature to improve Tachiyomi
|
|
||||||
labels: [Feature request]
|
|
||||||
body:
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: feature-description
|
|
||||||
attributes:
|
|
||||||
label: Describe your suggested feature
|
|
||||||
description: How can Tachiyomi be improved?
|
|
||||||
placeholder: |
|
|
||||||
Example:
|
|
||||||
"It should work like this..."
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: other-details
|
|
||||||
attributes:
|
|
||||||
label: Other details
|
|
||||||
placeholder: |
|
|
||||||
Additional details and attachments.
|
|
||||||
|
|
||||||
- type: checkboxes
|
|
||||||
id: acknowledgements
|
|
||||||
attributes:
|
|
||||||
label: Acknowledgements
|
|
||||||
description: Read this carefully, we will close and ignore your issue if you skimmed through this.
|
|
||||||
options:
|
|
||||||
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open issue.
|
|
||||||
required: true
|
|
||||||
- label: I have written a short but informative title.
|
|
||||||
required: true
|
|
||||||
- label: If this is an issue with an extension, I should be opening an issue in the [extensions repository](https://github.com/tachiyomiorg/tachiyomi-extensions/issues/new/choose).
|
|
||||||
required: true
|
|
||||||
- label: I have updated the app to version **[1.8.1](https://github.com/jobobby04/tachiyomisy/releases/latest)**.
|
|
||||||
required: true
|
|
||||||
- label: I will fill out all of the requested information in this form.
|
|
||||||
required: true
|
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
name: "Extension/source/catalogue issue"
|
||||||
|
about: "Do not open an issue here. See https://github.com/tachiyomiorg/tachiyomi-extensions"
|
||||||
|
title: "THIS ISSUE IS IN THE WRONG REPO; SEE https://github.com/tachiyomiorg/tachiyomi-extensions"
|
||||||
|
labels: "catalog, invalid"
|
||||||
|
---
|
||||||
|
|
||||||
|
DO NOT OPEN AN ISSUE IN THIS REPO. SEE https://github.com/tachiyomiorg/tachiyomi-extensions
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
<!--
|
|
||||||
Please include a summary of the change and which issue is fixed.
|
|
||||||
Also make sure you've tested your code and also done a self-review of it.
|
|
||||||
Don't forget to check all base themes and tablet mode for relevant changes.
|
|
||||||
|
|
||||||
If your changes are visual, please provide images below:
|
|
||||||
|
|
||||||
### Images
|
|
||||||
| Image 1 | Image 2 |
|
|
||||||
| ------- | ------- |
|
|
||||||
|  |  |
|
|
||||||
-->
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 489 KiB After Width: | Height: | Size: 1.7 MiB |
@@ -20,6 +20,7 @@ jobs:
|
|||||||
preview:
|
preview:
|
||||||
name: Build app preview
|
name: Build app preview
|
||||||
needs: check_wrapper
|
needs: check_wrapper
|
||||||
|
if: "!startsWith(github.event.head_commit.message, '[SKIP CI]')"
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ jobs:
|
|||||||
build:
|
build:
|
||||||
name: Build app
|
name: Build app
|
||||||
needs: check_wrapper
|
needs: check_wrapper
|
||||||
|
if: "!startsWith(github.event.head_commit.message, '[SKIP CI]')"
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@@ -31,10 +32,10 @@ jobs:
|
|||||||
- name: Clone repo
|
- name: Clone repo
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
- name: Set up JDK 11
|
- name: Set up JDK 1.8
|
||||||
uses: actions/setup-java@v1
|
uses: actions/setup-java@v1
|
||||||
with:
|
with:
|
||||||
java-version: 11
|
java-version: 1.8
|
||||||
|
|
||||||
- name: Copy CI gradle.properties
|
- name: Copy CI gradle.properties
|
||||||
run: |
|
run: |
|
||||||
@@ -52,9 +53,12 @@ jobs:
|
|||||||
write-mode: overwrite # optional, default is preserve
|
write-mode: overwrite # optional, default is preserve
|
||||||
|
|
||||||
- name: Build app
|
- name: Build app
|
||||||
uses: gradle/gradle-command-action@v2
|
uses: eskatos/gradle-command-action@v1
|
||||||
with:
|
with:
|
||||||
arguments: assembleStandardRelease --stacktrace
|
arguments: assembleRelease --stacktrace
|
||||||
|
wrapper-cache-enabled: true
|
||||||
|
dependencies-cache-enabled: true
|
||||||
|
configuration-cache-enabled: true
|
||||||
|
|
||||||
- name: Sign APK
|
- name: Sign APK
|
||||||
uses: r0adkll/sign-android-release@v1
|
uses: r0adkll/sign-android-release@v1
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ jobs:
|
|||||||
build:
|
build:
|
||||||
name: Build app
|
name: Build app
|
||||||
needs: check_wrapper
|
needs: check_wrapper
|
||||||
|
if: "!startsWith(github.event.head_commit.message, '[SKIP CI]')"
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@@ -27,10 +28,10 @@ jobs:
|
|||||||
- name: Clone repo
|
- name: Clone repo
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
- name: Set up JDK 11
|
- name: Set up JDK 1.8
|
||||||
uses: actions/setup-java@v1
|
uses: actions/setup-java@v1
|
||||||
with:
|
with:
|
||||||
java-version: 11
|
java-version: 1.8
|
||||||
|
|
||||||
- name: Copy CI gradle.properties
|
- name: Copy CI gradle.properties
|
||||||
run: |
|
run: |
|
||||||
@@ -38,10 +39,12 @@ jobs:
|
|||||||
cp .github/runner-files/ci-gradle.properties ~/.gradle/gradle.properties
|
cp .github/runner-files/ci-gradle.properties ~/.gradle/gradle.properties
|
||||||
|
|
||||||
- name: Build app
|
- name: Build app
|
||||||
uses: gradle/gradle-command-action@v2
|
uses: eskatos/gradle-command-action@v1
|
||||||
with:
|
with:
|
||||||
arguments: assembleDevDebug
|
arguments: assembleStandardDebug
|
||||||
|
wrapper-cache-enabled: true
|
||||||
|
dependencies-cache-enabled: true
|
||||||
|
configuration-cache-enabled: true
|
||||||
- name: Upload APK
|
- name: Upload APK
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v2
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -7,26 +7,31 @@ jobs:
|
|||||||
autoclose:
|
autoclose:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Autoclose issues
|
- name: Autoclose when created in wrong repo
|
||||||
uses: arkon/issue-closer-action@v3.4
|
uses: arkon/issue-closer-action@v1.1
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
rules: |
|
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."
|
||||||
"type": "body",
|
- name: Autoclose when no short description provided
|
||||||
"regex": ".*DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT.*",
|
uses: arkon/issue-closer-action@v1.1
|
||||||
"message": "The acknowledgment section was not removed."
|
with:
|
||||||
},
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
{
|
type: title
|
||||||
"type": "body",
|
regex: ".*<Write short description here>*"
|
||||||
"regex": ".*\\* (Tachiyomi version|Android version|Device): \\?.*",
|
message: "@${issue.user.login} this issue was automatically closed because you did not fill out the description in the title."
|
||||||
"message": "Requested information in the template was not filled out."
|
- name: Autoclose when body acknowledgement section not removed
|
||||||
},
|
uses: arkon/issue-closer-action@v1.1
|
||||||
{
|
with:
|
||||||
"type": "both",
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
"regex": "^(?!.*myanimelist.*).*(aniyomi|anime).*$",
|
type: body
|
||||||
"ignoreCase": true,
|
regex: ".*DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT.*"
|
||||||
"message": "Tachiyomi does not support anime, and has no plans to support anime. In addition Tachiyomi is not affiliated with Aniyomi https://github.com/jmir1/aniyomi"
|
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."
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
name: Issue moderator
|
|
||||||
|
|
||||||
on:
|
|
||||||
issue_comment:
|
|
||||||
types: [created]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
moderate:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Moderate issues
|
|
||||||
uses: tachiyomiorg/issue-moderator-action@v1
|
|
||||||
with:
|
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
name: Lock threads
|
|
||||||
|
|
||||||
on:
|
|
||||||
# Daily
|
|
||||||
schedule:
|
|
||||||
- cron: '0 0 * * *'
|
|
||||||
# Manual trigger
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
lock:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: dessant/lock-threads@v3
|
|
||||||
with:
|
|
||||||
github-token: ${{ github.token }}
|
|
||||||
issue-inactive-days: '2'
|
|
||||||
pr-inactive-days: '2'
|
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
# Contributor Covenant Code of Conduct
|
|
||||||
|
|
||||||
## Our Pledge
|
|
||||||
|
|
||||||
We as members, contributors, and leaders pledge to make participation in our
|
|
||||||
community a harassment-free experience for everyone, regardless of age, body
|
|
||||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
|
||||||
identity and expression, level of experience, education, socio-economic status,
|
|
||||||
nationality, personal appearance, race, caste, color, religion, or sexual identity
|
|
||||||
and orientation.
|
|
||||||
|
|
||||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
|
||||||
diverse, inclusive, and healthy community.
|
|
||||||
|
|
||||||
## Our Standards
|
|
||||||
|
|
||||||
Examples of behavior that contributes to a positive environment for our
|
|
||||||
community include:
|
|
||||||
|
|
||||||
* Demonstrating empathy and kindness toward other people
|
|
||||||
* Being respectful of differing opinions, viewpoints, and experiences
|
|
||||||
* Giving and gracefully accepting constructive feedback
|
|
||||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
|
||||||
and learning from the experience
|
|
||||||
* Focusing on what is best not just for us as individuals, but for the
|
|
||||||
overall community
|
|
||||||
|
|
||||||
Examples of unacceptable behavior include:
|
|
||||||
|
|
||||||
* The use of sexualized language or imagery, and sexual attention or
|
|
||||||
advances of any kind
|
|
||||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
|
||||||
* Public or private harassment
|
|
||||||
* Publishing others' private information, such as a physical or email
|
|
||||||
address, without their explicit permission
|
|
||||||
* Other conduct which could reasonably be considered inappropriate in a
|
|
||||||
professional setting
|
|
||||||
|
|
||||||
## Enforcement Responsibilities
|
|
||||||
|
|
||||||
Community moderators are responsible for clarifying and enforcing our standards of
|
|
||||||
acceptable behavior and will take appropriate and fair corrective action in
|
|
||||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
|
||||||
or harmful.
|
|
||||||
|
|
||||||
Community moderators 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, and will communicate reasons for moderation
|
|
||||||
decisions when appropriate.
|
|
||||||
|
|
||||||
## Scope
|
|
||||||
|
|
||||||
This Code of Conduct applies within all community spaces, and also applies when
|
|
||||||
an individual is officially representing the community in public spaces.
|
|
||||||
Examples of representing our community include using an official e-mail address,
|
|
||||||
posting via an official social media account, or acting as an appointed
|
|
||||||
representative at an online or offline event.
|
|
||||||
|
|
||||||
## Enforcement
|
|
||||||
|
|
||||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
|
||||||
reported to the community moderators responsible for enforcement at
|
|
||||||
the [Tachiyomi Discord server](https://discord.gg/tachiyomi).
|
|
||||||
All complaints will be reviewed and investigated promptly and fairly.
|
|
||||||
|
|
||||||
All community moderators are obligated to respect the privacy and security of the
|
|
||||||
reporter of any incident.
|
|
||||||
|
|
||||||
## Enforcement Guidelines
|
|
||||||
|
|
||||||
Community moderators will follow these Community Impact Guidelines in determining
|
|
||||||
the consequences for any action they deem in violation of this Code of Conduct:
|
|
||||||
|
|
||||||
### 1. Correction
|
|
||||||
|
|
||||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
|
||||||
unprofessional or unwelcome in the community.
|
|
||||||
|
|
||||||
**Consequence**: A private, written warning from community moderators, providing
|
|
||||||
clarity around the nature of the violation and an explanation of why the
|
|
||||||
behavior was inappropriate. A public apology may be requested.
|
|
||||||
|
|
||||||
### 2. Warning
|
|
||||||
|
|
||||||
**Community Impact**: A violation through a single incident or series
|
|
||||||
of actions.
|
|
||||||
|
|
||||||
**Consequence**: A warning with consequences for continued behavior. No
|
|
||||||
interaction with the people involved, including unsolicited interaction with
|
|
||||||
those enforcing the Code of Conduct, for a specified period of time. This
|
|
||||||
includes avoiding interactions in community spaces as well as external channels
|
|
||||||
like social media. Violating these terms may lead to a temporary or
|
|
||||||
permanent ban.
|
|
||||||
|
|
||||||
### 3. Temporary Ban
|
|
||||||
|
|
||||||
**Community Impact**: A serious violation of community standards, including
|
|
||||||
sustained inappropriate behavior.
|
|
||||||
|
|
||||||
**Consequence**: A temporary ban from any sort of interaction or public
|
|
||||||
communication with the community for a specified period of time. No public or
|
|
||||||
private interaction with the people involved, including unsolicited interaction
|
|
||||||
with those enforcing the Code of Conduct, is allowed during this period.
|
|
||||||
Violating these terms may lead to a permanent ban.
|
|
||||||
|
|
||||||
### 4. Permanent Ban
|
|
||||||
|
|
||||||
**Community Impact**: Demonstrating a pattern of violation of community
|
|
||||||
standards, including sustained inappropriate behavior, harassment of an
|
|
||||||
individual, or aggression toward or disparagement of classes of individuals.
|
|
||||||
|
|
||||||
**Consequence**: A permanent ban from any sort of public interaction within
|
|
||||||
the community.
|
|
||||||
|
|
||||||
## Attribution
|
|
||||||
|
|
||||||
This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org/),
|
|
||||||
version 2.1, available at
|
|
||||||
[v2.1](https://www.contributor-covenant.org/version/2/1/code_of_conduct.html).
|
|
||||||
|
|
||||||
Community Impact Guidelines were inspired by
|
|
||||||
[Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).
|
|
||||||
|
|
||||||
For answers to common questions about this code of conduct, see the FAQ at
|
|
||||||
[FAQ](https://www.contributor-covenant.org/faq). Translations are available
|
|
||||||
at [translations](https://www.contributor-covenant.org/translations).
|
|
||||||
+1
-2
@@ -10,7 +10,6 @@ Thanks for your interest in contributing to Tachiyomi!
|
|||||||
Pull requests are welcome!
|
Pull requests are welcome!
|
||||||
|
|
||||||
If you're interested in taking on [an open issue](https://github.com/tachiyomiorg/tachiyomi/issues), please comment on it so others are aware.
|
If you're interested in taking on [an open issue](https://github.com/tachiyomiorg/tachiyomi/issues), please comment on it so others are aware.
|
||||||
You do not need to ask for permission nor an assignment.
|
|
||||||
|
|
||||||
|
|
||||||
# Translations
|
# Translations
|
||||||
@@ -27,7 +26,7 @@ When creating a fork, remember to:
|
|||||||
- To avoid confusion with the main app:
|
- To avoid confusion with the main app:
|
||||||
- Change the app name
|
- Change the app name
|
||||||
- Change the app icon
|
- Change the app icon
|
||||||
- Change or disable the [app update checker](https://github.com/tachiyomiorg/tachiyomi/blob/master/app/src/main/java/eu/kanade/tachiyomi/data/updater/GithubUpdateChecker.kt)
|
- Change or disable the [app update checker](https://github.com/tachiyomiorg/tachiyomi/blob/master/app/src/main/java/eu/kanade/tachiyomi/data/updater/github/GithubUpdateChecker.kt)
|
||||||
- To avoid installation conflicts:
|
- To avoid installation conflicts:
|
||||||
- Change the `applicationId` in [`build.gradle.kts`](https://github.com/tachiyomiorg/tachiyomi/blob/master/app/build.gradle.kts)
|
- Change the `applicationId` in [`build.gradle.kts`](https://github.com/tachiyomiorg/tachiyomi/blob/master/app/build.gradle.kts)
|
||||||
- To avoid having your data polluting the main app's analytics and crash report services:
|
- To avoid having your data polluting the main app's analytics and crash report services:
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
| Preview Builds | Release Builds | Tachiyomi Support Server |
|
| Preview Builds | Release Builds | Tachiyomi Support Server |
|
||||||
|-------|----------|----------|
|
|-------|----------|----------|
|
||||||
| [](https://github.com/jobobby04/TachiyomiSYPreview/releases) | [](https://github.com/jobobby04/tachiyomisy/releases/latest) | [](https://discord.gg/tachiyomi) |
|
| [](https://github.com/jobobby04/TachiyomiSYPreview/releases) | [](https://github.com/jobobby04/tachiyomisy/releases/latest) | [](https://discord.gg/tachiyomi) |
|
||||||
|
|
||||||
|
|
||||||
# TachiyomiSY
|
# TachiyomiSY
|
||||||
Tachiyomi is a free and open source manga reader for Android 6.0 and above. This version of Tachiyomi, TachiyomiSY was based off TachiyomiAZ. This version is meant to push forward in the ways of usability and features. TachiyomiSY tries to push forward where it can, but staying in a place where it can easily grab updates and features from the main app, it tries to make new features, or take features from other forks like J2K and Neko.
|
Tachiyomi is a free and open source manga reader for Android 5.0 and above. This version of Tachiyomi, TachiyomiSY was based off TachiyomiAZ. This version is meant to push forward in the ways of usability and features. TachiyomiSY tries to push forward where it can, but staying in a place where it can easily grab updates and features from the main app, it tries to make new features, or take features from other forks like J2K and Neko.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
Features of Tachiyomi(original) include:
|
Features of Tachiyomi(original) include:
|
||||||
* Online reading from a variety of sources
|
* Online reading from sources such as MangaDex, MangaSee, Mangakakalot, [and more](https://github.com/tachiyomiorg/tachiyomi-extensions)
|
||||||
* Local reading of downloaded content
|
* Local reading of downloaded manga
|
||||||
* A configurable reader with multiple viewers, reading directions and other settings.
|
* A configurable reader with multiple viewers, reading directions and other settings.
|
||||||
* Tracker support: [MyAnimeList](https://myanimelist.net/), [AniList](https://anilist.co/), [Kitsu](https://kitsu.io/), [Shikimori](https://shikimori.one), and [Bangumi](https://bgm.tv/)
|
* [MyAnimeList](https://myanimelist.net/), [AniList](https://anilist.co/), [Kitsu](https://kitsu.io/), [Shikimori](https://shikimori.one), and [Bangumi](https://bgm.tv/) support
|
||||||
* Categories to organize your library
|
* Categories to organize your library
|
||||||
* Light and dark themes
|
* Light and dark themes
|
||||||
* Schedule updating your library for new chapters
|
* Schedule updating your library for new chapters
|
||||||
@@ -84,12 +84,13 @@ Please make sure to read the full guidelines. Your issue may be closed without w
|
|||||||
|
|
||||||
<details><summary>Bugs</summary>
|
<details><summary>Bugs</summary>
|
||||||
|
|
||||||
* Include version (More → About → Version)
|
* Include version (More > About > Version)
|
||||||
* If not latest, try updating, it may have already been solved
|
* If not latest, try updating, it may have already been solved
|
||||||
* Preview version is equal to the number of commits as seen in the main page
|
* Preview version is equal to the number of commits as seen in the main page
|
||||||
* Include steps to reproduce (if not obvious from description)
|
* Include steps to reproduce (if not obvious from description)
|
||||||
* Include screenshot (if needed)
|
* Include screenshot (if needed)
|
||||||
* If it could be device-dependent, try reproducing on another device (if possible)
|
* If it could be device-dependent, try reproducing on another device (if possible)
|
||||||
|
* For large logs use http://pastebin.com/ (or similar)
|
||||||
* Don't group unrelated requests into one issue
|
* Don't group unrelated requests into one issue
|
||||||
|
|
||||||
DO: https://github.com/tachiyomiorg/tachiyomi/issues/24 https://github.com/tachiyomiorg/tachiyomi/issues/71
|
DO: https://github.com/tachiyomiorg/tachiyomi/issues/24 https://github.com/tachiyomiorg/tachiyomi/issues/71
|
||||||
@@ -108,12 +109,7 @@ Source requests should be created at https://github.com/tachiyomiorg/tachiyomi-e
|
|||||||
|
|
||||||
<details><summary>Contributing</summary>
|
<details><summary>Contributing</summary>
|
||||||
|
|
||||||
See [CONTRIBUTING.md](./CONTRIBUTING.md).
|
See [CONTRIBUTING.md](https://github.com/tachiyomiorg/tachiyomi/blob/master/CONTRIBUTING.md).
|
||||||
</details>
|
|
||||||
|
|
||||||
<details><summary>Code of Conduct</summary>
|
|
||||||
|
|
||||||
See [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md).
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
## FAQ
|
## FAQ
|
||||||
|
|||||||
+132
-117
@@ -8,9 +8,12 @@ plugins {
|
|||||||
id("com.android.application")
|
id("com.android.application")
|
||||||
id("com.mikepenz.aboutlibraries.plugin")
|
id("com.mikepenz.aboutlibraries.plugin")
|
||||||
kotlin("android")
|
kotlin("android")
|
||||||
|
kotlin("kapt")
|
||||||
kotlin("plugin.parcelize")
|
kotlin("plugin.parcelize")
|
||||||
kotlin("plugin.serialization")
|
kotlin("plugin.serialization")
|
||||||
id("com.github.zellius.shortcut-helper")
|
id("com.github.zellius.shortcut-helper")
|
||||||
|
// Realm (EH)
|
||||||
|
id("realm-android")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!gradle.startParameter.taskRequests.toString().contains("Debug")) {
|
if (!gradle.startParameter.taskRequests.toString().contains("Debug")) {
|
||||||
@@ -22,25 +25,32 @@ if (!gradle.startParameter.taskRequests.toString().contains("Debug")) {
|
|||||||
shortcutHelper.setFilePath("./shortcuts.xml")
|
shortcutHelper.setFilePath("./shortcuts.xml")
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdk = AndroidConfig.compileSdk
|
compileSdkVersion(AndroidConfig.compileSdk)
|
||||||
|
buildToolsVersion(AndroidConfig.buildTools)
|
||||||
ndkVersion = AndroidConfig.ndk
|
ndkVersion = AndroidConfig.ndk
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "eu.kanade.tachiyomi.sy"
|
applicationId = "eu.kanade.tachiyomi.sy"
|
||||||
minSdk = AndroidConfig.minSdk
|
minSdkVersion(AndroidConfig.minSdk)
|
||||||
targetSdk = AndroidConfig.targetSdk
|
targetSdkVersion(AndroidConfig.targetSdk)
|
||||||
versionCode = 26
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
versionName = "1.8.1"
|
versionCode = 13
|
||||||
|
versionName = "1.5.0"
|
||||||
|
|
||||||
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
||||||
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
|
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
|
||||||
buildConfigField("String", "BUILD_TIME", "\"${getBuildTime()}\"")
|
buildConfigField("String", "BUILD_TIME", "\"${getBuildTime()}\"")
|
||||||
buildConfigField("boolean", "INCLUDE_UPDATER", "false")
|
buildConfigField("boolean", "INCLUDE_UPDATER", "false")
|
||||||
|
|
||||||
|
multiDexEnabled = true
|
||||||
|
|
||||||
ndk {
|
ndk {
|
||||||
abiFilters += setOf("armeabi-v7a", "arm64-v8a", "x86")
|
abiFilters += setOf("armeabi-v7a", "arm64-v8a", "x86")
|
||||||
}
|
}
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
}
|
||||||
|
|
||||||
|
buildFeatures {
|
||||||
|
viewBinding = true
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
@@ -52,16 +62,18 @@ android {
|
|||||||
applicationIdSuffix = ".rt"
|
applicationIdSuffix = ".rt"
|
||||||
//isMinifyEnabled = true
|
//isMinifyEnabled = true
|
||||||
//isShrinkResources = true
|
//isShrinkResources = true
|
||||||
setProguardFiles(listOf(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"))
|
isZipAlignEnabled = true
|
||||||
|
setProguardFiles(listOf(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"))
|
||||||
}
|
}
|
||||||
named("release") {
|
named("release") {
|
||||||
isMinifyEnabled = true
|
isMinifyEnabled = true
|
||||||
isShrinkResources = true
|
isShrinkResources = true
|
||||||
setProguardFiles(listOf(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"))
|
isZipAlignEnabled = true
|
||||||
|
setProguardFiles(listOf(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
flavorDimensions += "default"
|
flavorDimensions("default")
|
||||||
|
|
||||||
productFlavors {
|
productFlavors {
|
||||||
create("standard") {
|
create("standard") {
|
||||||
@@ -72,41 +84,30 @@ android {
|
|||||||
dimension = "default"
|
dimension = "default"
|
||||||
}
|
}
|
||||||
create("dev") {
|
create("dev") {
|
||||||
resourceConfigurations.addAll(listOf("en", "xxhdpi"))
|
resConfigs("en", "xxhdpi")
|
||||||
dimension = "default"
|
dimension = "default"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
packagingOptions {
|
packagingOptions {
|
||||||
resources.excludes.addAll(listOf(
|
exclude("META-INF/DEPENDENCIES")
|
||||||
"META-INF/DEPENDENCIES",
|
exclude("LICENSE.txt")
|
||||||
"LICENSE.txt",
|
exclude("META-INF/LICENSE")
|
||||||
"META-INF/LICENSE",
|
exclude("META-INF/LICENSE.txt")
|
||||||
"META-INF/LICENSE.txt",
|
exclude("META-INF/NOTICE")
|
||||||
"META-INF/README.md",
|
|
||||||
"META-INF/NOTICE",
|
// Compatibility for two RxJava versions (EXH)
|
||||||
"META-INF/*.kotlin_module",
|
exclude("META-INF/rxjava.properties")
|
||||||
"META-INF/*.version",
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dependenciesInfo {
|
dependenciesInfo {
|
||||||
includeInApk = false
|
includeInApk = false
|
||||||
}
|
}
|
||||||
|
|
||||||
buildFeatures {
|
lintOptions {
|
||||||
viewBinding = true
|
disable("MissingTranslation", "ExtraTranslation")
|
||||||
|
isAbortOnError = false
|
||||||
// Disable some unused things
|
isCheckReleaseBuilds = false
|
||||||
aidl = false
|
|
||||||
renderScript = false
|
|
||||||
shaders = false
|
|
||||||
}
|
|
||||||
|
|
||||||
lint {
|
|
||||||
disable.addAll(listOf("MissingTranslation", "ExtraTranslation"))
|
|
||||||
abortOnError = false
|
|
||||||
checkReleaseBuilds = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
@@ -120,79 +121,79 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(kotlin("reflect", version = BuildPluginsVersion.KOTLIN))
|
|
||||||
|
|
||||||
val coroutinesVersion = "1.6.0"
|
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion")
|
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion")
|
|
||||||
|
|
||||||
// Source models and interfaces from Tachiyomi 1.x
|
// Source models and interfaces from Tachiyomi 1.x
|
||||||
implementation("org.tachiyomi:source-api:1.1")
|
implementation("tachiyomi.sourceapi:source-api:1.1")
|
||||||
|
|
||||||
// AndroidX libraries
|
// AndroidX libraries
|
||||||
implementation("androidx.annotation:annotation:1.4.0-alpha01")
|
implementation("androidx.annotation:annotation:1.2.0-beta01")
|
||||||
implementation("androidx.appcompat:appcompat:1.4.1")
|
implementation("androidx.appcompat:appcompat:1.3.0-beta01")
|
||||||
implementation("androidx.biometric:biometric-ktx:1.2.0-alpha04")
|
implementation("androidx.biometric:biometric-ktx:1.2.0-alpha02")
|
||||||
implementation("androidx.browser:browser:1.4.0")
|
implementation("androidx.browser:browser:1.3.0")
|
||||||
implementation("androidx.constraintlayout:constraintlayout:2.1.3")
|
implementation("androidx.cardview:cardview:1.0.0")
|
||||||
implementation("androidx.coordinatorlayout:coordinatorlayout:1.2.0")
|
implementation("androidx.constraintlayout:constraintlayout:2.1.0-alpha2")
|
||||||
implementation("androidx.core:core-ktx:1.8.0-alpha02")
|
implementation("androidx.coordinatorlayout:coordinatorlayout:1.1.0")
|
||||||
implementation("androidx.core:core-splashscreen:1.0.0-alpha02")
|
implementation("androidx.core:core-ktx:1.5.0-beta01")
|
||||||
implementation("androidx.recyclerview:recyclerview:1.3.0-alpha01")
|
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.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01")
|
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01")
|
||||||
implementation("androidx.viewpager:viewpager:1.1.0-alpha01")
|
|
||||||
|
|
||||||
val lifecycleVersion = "2.4.0"
|
val lifecycleVersion = "2.3.0-rc01"
|
||||||
implementation("androidx.lifecycle:lifecycle-common:$lifecycleVersion")
|
implementation("androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion")
|
||||||
implementation("androidx.lifecycle:lifecycle-process:$lifecycleVersion")
|
implementation("androidx.lifecycle:lifecycle-process:$lifecycleVersion")
|
||||||
implementation("androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion")
|
implementation("androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion")
|
||||||
|
|
||||||
// Job scheduling
|
// Job scheduling
|
||||||
implementation("androidx.work:work-runtime-ktx:2.6.0")
|
implementation("androidx.work:work-runtime-ktx:2.5.0")
|
||||||
|
|
||||||
// RX
|
// UI library
|
||||||
|
implementation("com.google.android.material:material:1.3.0")
|
||||||
|
|
||||||
|
"standardImplementation"("com.google.firebase:firebase-core:18.0.2")
|
||||||
|
|
||||||
|
// ReactiveX
|
||||||
implementation("io.reactivex:rxandroid:1.2.1")
|
implementation("io.reactivex:rxandroid:1.2.1")
|
||||||
implementation("io.reactivex:rxjava:1.3.8")
|
implementation("io.reactivex:rxjava:1.3.8")
|
||||||
implementation("com.jakewharton.rxrelay:rxrelay:1.2.0")
|
implementation("com.jakewharton.rxrelay:rxrelay:1.2.0")
|
||||||
implementation("ru.beryukhov:flowreactivenetwork:1.0.4")
|
implementation("com.github.pwittchen:reactivenetwork:0.13.0")
|
||||||
|
|
||||||
// Network client
|
// Network client
|
||||||
val okhttpVersion = "4.9.1"
|
val okhttpVersion = "4.10.0-RC1"
|
||||||
implementation("com.squareup.okhttp3:okhttp:$okhttpVersion")
|
implementation("com.squareup.okhttp3:okhttp:$okhttpVersion")
|
||||||
implementation("com.squareup.okhttp3:logging-interceptor:$okhttpVersion")
|
implementation("com.squareup.okhttp3:logging-interceptor:$okhttpVersion")
|
||||||
implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:$okhttpVersion")
|
implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:$okhttpVersion")
|
||||||
implementation("com.squareup.okio:okio:3.0.0")
|
implementation("com.squareup.okio:okio:2.10.0")
|
||||||
|
|
||||||
// TLS 1.3 support for Android < 10
|
// TLS 1.3 support for Android < 10
|
||||||
implementation("org.conscrypt:conscrypt-android:2.5.2")
|
implementation("org.conscrypt:conscrypt-android:2.5.1")
|
||||||
|
|
||||||
// Data serialization (JSON, protobuf)
|
// JSON
|
||||||
val kotlinSerializationVersion = "1.3.2"
|
val kotlinSerializationVersion = "1.0.1"
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinSerializationVersion")
|
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinSerializationVersion")
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-protobuf:$kotlinSerializationVersion")
|
implementation("org.jetbrains.kotlinx:kotlinx-serialization-protobuf:$kotlinSerializationVersion")
|
||||||
|
implementation("com.google.code.gson:gson:2.8.6")
|
||||||
|
implementation("com.github.salomonbrys.kotson:kotson:2.5.0")
|
||||||
|
|
||||||
// JavaScript engine
|
// JavaScript engine
|
||||||
implementation("app.cash.quickjs:quickjs-android:0.9.2")
|
implementation("com.squareup.duktape:duktape-android:1.3.0")
|
||||||
// TODO: remove Duktape once all extensions are using QuickJS
|
|
||||||
implementation("com.squareup.duktape:duktape-android:1.4.0")
|
|
||||||
|
|
||||||
// HTML parser
|
|
||||||
implementation("org.jsoup:jsoup:1.14.3")
|
|
||||||
|
|
||||||
// Disk
|
// Disk
|
||||||
implementation("com.jakewharton:disklrucache:2.0.2")
|
implementation("com.jakewharton:disklrucache:2.0.2")
|
||||||
implementation("com.github.tachiyomiorg:unifile:17bec43")
|
implementation("com.github.inorichi:unifile:e9ee588")
|
||||||
implementation("com.github.junrar:junrar:7.4.0")
|
implementation("com.github.junrar:junrar:7.4.0")
|
||||||
|
|
||||||
|
// HTML parser
|
||||||
|
implementation("org.jsoup:jsoup:1.13.1")
|
||||||
|
|
||||||
// Database
|
// Database
|
||||||
implementation("androidx.sqlite:sqlite-ktx:2.2.0")
|
implementation("androidx.sqlite:sqlite-ktx:2.1.0")
|
||||||
implementation("com.github.inorichi.storio:storio-common:8be19de@aar")
|
implementation("com.github.inorichi.storio:storio-common:8be19de@aar")
|
||||||
implementation("com.github.inorichi.storio:storio-sqlite:8be19de@aar")
|
implementation("com.github.inorichi.storio:storio-sqlite:8be19de@aar")
|
||||||
implementation("com.github.requery:sqlite-android:3.36.0")
|
implementation("io.requery:sqlite-android:3.33.0")
|
||||||
|
|
||||||
// Preferences
|
// Preferences
|
||||||
implementation("androidx.preference:preference-ktx:1.2.0-rc01")
|
implementation("com.github.tfcporciuncula.flow-preferences:flow-preferences:1.3.3")
|
||||||
implementation("com.github.tfcporciuncula.flow-preferences:flow-preferences:1.4.0")
|
|
||||||
|
|
||||||
// Model View Presenter
|
// Model View Presenter
|
||||||
val nucleusVersion = "3.0.0"
|
val nucleusVersion = "3.0.0"
|
||||||
@@ -202,73 +203,81 @@ dependencies {
|
|||||||
// Dependency injection
|
// Dependency injection
|
||||||
implementation("com.github.inorichi.injekt:injekt-core:65b0440")
|
implementation("com.github.inorichi.injekt:injekt-core:65b0440")
|
||||||
|
|
||||||
// Image loading
|
// Image library
|
||||||
val coilVersion = "1.4.0"
|
val glideVersion = "4.11.0"
|
||||||
implementation("io.coil-kt:coil:$coilVersion")
|
implementation("com.github.bumptech.glide:glide:$glideVersion")
|
||||||
implementation("io.coil-kt:coil-gif:$coilVersion")
|
implementation("com.github.bumptech.glide:okhttp3-integration:$glideVersion")
|
||||||
|
kapt("com.github.bumptech.glide:compiler:$glideVersion")
|
||||||
|
|
||||||
implementation("com.github.tachiyomiorg:subsampling-scale-image-view:846abe0") {
|
implementation("com.github.tachiyomiorg:subsampling-scale-image-view:6caf219")
|
||||||
exclude(module = "image-decoder")
|
// TODO: switch to new decoder for stable releases
|
||||||
}
|
// implementation("com.github.tachiyomiorg:subsampling-scale-image-view:ca26317")
|
||||||
implementation("com.github.tachiyomiorg:image-decoder:7481a4a")
|
|
||||||
|
// Logging
|
||||||
|
implementation("com.jakewharton.timber:timber:4.7.1")
|
||||||
|
|
||||||
|
// Crash reports
|
||||||
|
//implementation("ch.acra:acra-http:5.7.0")
|
||||||
|
|
||||||
// Sort
|
// Sort
|
||||||
implementation("com.github.gpanther:java-nat-sort:natural-comparator-1.1")
|
implementation("com.github.gpanther:java-nat-sort:natural-comparator-1.1")
|
||||||
|
|
||||||
// UI libraries
|
// UI
|
||||||
implementation("com.google.android.material:material:1.6.0-alpha02")
|
implementation("com.dmitrymalkovich.android:material-design-dimens:1.4")
|
||||||
implementation("com.github.dmytrodanylyk.android-process-button:library:1.0.4")
|
implementation("com.github.dmytrodanylyk.android-process-button:library:1.0.4")
|
||||||
implementation("com.github.arkon.FlexibleAdapter:flexible-adapter:c8013533")
|
implementation("eu.davidea:flexible-adapter:5.1.0")
|
||||||
implementation("com.github.arkon.FlexibleAdapter:flexible-adapter-ui:c8013533")
|
implementation("eu.davidea:flexible-adapter-ui:1.0.0")
|
||||||
implementation("com.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.1.0")
|
implementation("com.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.1.0")
|
||||||
implementation("com.github.chrisbanes:PhotoView:2.3.0")
|
implementation("com.github.chrisbanes:PhotoView:2.3.0")
|
||||||
implementation("com.github.tachiyomiorg:DirectionalViewPager:1.0.0") {
|
implementation("com.github.tachiyomiorg:DirectionalViewPager:7d0617d")
|
||||||
exclude(group = "androidx.viewpager", module = "viewpager")
|
|
||||||
}
|
// 3.2.0+ introduces weird UI blinking or cut off issues on some devices
|
||||||
implementation("dev.chrisbanes.insetter:insetter:0.6.1")
|
val materialDialogsVersion = "3.1.1"
|
||||||
|
implementation("com.afollestad.material-dialogs:core:$materialDialogsVersion")
|
||||||
|
implementation("com.afollestad.material-dialogs:input:$materialDialogsVersion")
|
||||||
|
implementation("com.afollestad.material-dialogs:datetime:$materialDialogsVersion")
|
||||||
|
|
||||||
// Conductor
|
// Conductor
|
||||||
val conductorVersion = "3.1.2"
|
implementation("com.bluelinelabs:conductor:2.1.5")
|
||||||
implementation("com.bluelinelabs:conductor:$conductorVersion")
|
implementation("com.bluelinelabs:conductor-support:2.1.5") {
|
||||||
implementation("com.bluelinelabs:conductor-viewpager:$conductorVersion")
|
exclude(group = "com.android.support")
|
||||||
implementation("com.github.tachiyomiorg:conductor-support-preference:$conductorVersion")
|
}
|
||||||
|
implementation("com.github.tachiyomiorg:conductor-support-preference:1.1.1")
|
||||||
|
|
||||||
// FlowBinding
|
// FlowBinding
|
||||||
val flowbindingVersion = "1.2.0"
|
val flowbindingVersion = "0.12.0"
|
||||||
implementation("io.github.reactivecircus.flowbinding:flowbinding-android:$flowbindingVersion")
|
implementation("io.github.reactivecircus.flowbinding:flowbinding-android:$flowbindingVersion")
|
||||||
implementation("io.github.reactivecircus.flowbinding:flowbinding-appcompat:$flowbindingVersion")
|
implementation("io.github.reactivecircus.flowbinding:flowbinding-appcompat:$flowbindingVersion")
|
||||||
implementation("io.github.reactivecircus.flowbinding:flowbinding-recyclerview:$flowbindingVersion")
|
implementation("io.github.reactivecircus.flowbinding:flowbinding-recyclerview:$flowbindingVersion")
|
||||||
implementation("io.github.reactivecircus.flowbinding:flowbinding-swiperefreshlayout:$flowbindingVersion")
|
implementation("io.github.reactivecircus.flowbinding:flowbinding-swiperefreshlayout:$flowbindingVersion")
|
||||||
implementation("io.github.reactivecircus.flowbinding:flowbinding-viewpager:$flowbindingVersion")
|
implementation("io.github.reactivecircus.flowbinding:flowbinding-viewpager:$flowbindingVersion")
|
||||||
|
|
||||||
// Logging
|
|
||||||
implementation("com.squareup.logcat:logcat:0.1")
|
|
||||||
|
|
||||||
// Crash reports/analytics
|
|
||||||
//implementation("ch.acra:acra-http:5.8.4")
|
|
||||||
//"standardImplementation"("com.google.firebase:firebase-analytics-ktx:20.0.2")
|
|
||||||
|
|
||||||
// Licenses
|
// Licenses
|
||||||
implementation("com.mikepenz:aboutlibraries-core:${BuildPluginsVersion.ABOUTLIB_PLUGIN}")
|
implementation("com.mikepenz:aboutlibraries:${BuildPluginsVersion.ABOUTLIB_PLUGIN}")
|
||||||
|
|
||||||
// Shizuku
|
|
||||||
val shizukuVersion = "12.1.0"
|
|
||||||
implementation("dev.rikka.shizuku:api:$shizukuVersion")
|
|
||||||
implementation("dev.rikka.shizuku:provider:$shizukuVersion")
|
|
||||||
|
|
||||||
// Tests
|
// Tests
|
||||||
testImplementation("junit:junit:4.13.2")
|
testImplementation("junit:junit:4.13.1")
|
||||||
testImplementation("org.assertj:assertj-core:3.16.1")
|
testImplementation("org.assertj:assertj-core:3.16.1")
|
||||||
testImplementation("org.mockito:mockito-core:1.10.19")
|
testImplementation("org.mockito:mockito-core:1.10.19")
|
||||||
|
|
||||||
val robolectricVersion = "3.1.4"
|
val robolectricVersion = "3.1.4"
|
||||||
testImplementation("org.robolectric:robolectric:$robolectricVersion")
|
testImplementation("org.robolectric:robolectric:$robolectricVersion")
|
||||||
|
testImplementation("org.robolectric:shadows-multidex:$robolectricVersion")
|
||||||
testImplementation("org.robolectric:shadows-play-services:$robolectricVersion")
|
testImplementation("org.robolectric:shadows-play-services:$robolectricVersion")
|
||||||
|
|
||||||
|
implementation(kotlin("reflect", version = BuildPluginsVersion.KOTLIN))
|
||||||
|
|
||||||
|
val coroutinesVersion = "1.4.2"
|
||||||
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion")
|
||||||
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion")
|
||||||
|
|
||||||
// For detecting memory leaks; see https://square.github.io/leakcanary/
|
// For detecting memory leaks; see https://square.github.io/leakcanary/
|
||||||
// debugImplementation("com.squareup.leakcanary:leakcanary-android:2.7")
|
// debugImplementation("com.squareup.leakcanary:leakcanary-android:2.6")
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
|
// [EXH] Android 7 SSL Workaround
|
||||||
|
implementation("com.google.android.gms:play-services-safetynet:17.0.0")
|
||||||
|
|
||||||
// Changelog
|
// Changelog
|
||||||
implementation("com.github.gabrielemariotti.changeloglib:changelog:2.1.0")
|
implementation("com.github.gabrielemariotti.changeloglib:changelog:2.1.0")
|
||||||
|
|
||||||
@@ -276,11 +285,11 @@ dependencies {
|
|||||||
implementation ("info.debatty:java-string-similarity:2.0.0")
|
implementation ("info.debatty:java-string-similarity:2.0.0")
|
||||||
|
|
||||||
// Firebase (EH)
|
// Firebase (EH)
|
||||||
implementation("com.google.firebase:firebase-analytics-ktx:20.0.2")
|
implementation("com.google.firebase:firebase-analytics-ktx:18.0.0")
|
||||||
implementation("com.google.firebase:firebase-crashlytics-ktx:18.2.7")
|
implementation("com.google.firebase:firebase-crashlytics-ktx:17.3.0")
|
||||||
|
|
||||||
// Better logging (EH)
|
// Better logging (EH)
|
||||||
implementation("com.elvishew:xlog:1.11.0")
|
implementation("com.elvishew:xlog:1.7.1")
|
||||||
|
|
||||||
// Debug utils (EH)
|
// Debug utils (EH)
|
||||||
val debugOverlayVersion = "1.1.3"
|
val debugOverlayVersion = "1.1.3"
|
||||||
@@ -290,7 +299,15 @@ dependencies {
|
|||||||
testImplementation("com.ms-square:debugoverlay-no-op:$debugOverlayVersion")
|
testImplementation("com.ms-square:debugoverlay-no-op:$debugOverlayVersion")
|
||||||
|
|
||||||
// RatingBar (SY)
|
// RatingBar (SY)
|
||||||
implementation("me.zhanghai.android.materialratingbar:library:1.4.0")
|
implementation ("me.zhanghai.android.materialratingbar:library:1.4.0")
|
||||||
|
|
||||||
|
// JsonReader for similar manga
|
||||||
|
implementation("com.squareup.moshi:moshi:1.11.0")
|
||||||
|
|
||||||
|
implementation("androidx.gridlayout:gridlayout:1.0.0")
|
||||||
|
|
||||||
|
implementation("com.mikepenz:fastadapter:5.3.4")
|
||||||
|
// SY -->
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks {
|
tasks {
|
||||||
@@ -299,13 +316,11 @@ tasks {
|
|||||||
kotlinOptions.freeCompilerArgs += listOf(
|
kotlinOptions.freeCompilerArgs += listOf(
|
||||||
"-Xopt-in=kotlin.Experimental",
|
"-Xopt-in=kotlin.Experimental",
|
||||||
"-Xopt-in=kotlin.RequiresOptIn",
|
"-Xopt-in=kotlin.RequiresOptIn",
|
||||||
"-Xopt-in=kotlin.ExperimentalStdlibApi",
|
"-Xuse-experimental=kotlin.ExperimentalStdlibApi",
|
||||||
"-Xopt-in=kotlinx.coroutines.FlowPreview",
|
"-Xuse-experimental=kotlinx.coroutines.FlowPreview",
|
||||||
"-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
"-Xuse-experimental=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
||||||
"-Xopt-in=kotlinx.coroutines.InternalCoroutinesApi",
|
"-Xuse-experimental=kotlinx.coroutines.InternalCoroutinesApi",
|
||||||
"-Xopt-in=kotlinx.serialization.ExperimentalSerializationApi",
|
"-Xuse-experimental=kotlinx.serialization.ExperimentalSerializationApi"
|
||||||
"-Xopt-in=coil.annotation.ExperimentalCoilApi",
|
|
||||||
"-Xopt-in=kotlin.time.ExperimentalTime",
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
-allowaccessmodification
|
|
||||||
-dontusemixedcaseclassnames
|
|
||||||
-verbose
|
|
||||||
|
|
||||||
-keepattributes *Annotation*
|
|
||||||
|
|
||||||
-keepclasseswithmembernames,includedescriptorclasses class * {
|
|
||||||
native <methods>;
|
|
||||||
}
|
|
||||||
|
|
||||||
-keepclassmembers enum * {
|
|
||||||
public static **[] values();
|
|
||||||
public static ** valueOf(java.lang.String);
|
|
||||||
}
|
|
||||||
|
|
||||||
-keepclassmembers class * implements android.os.Parcelable {
|
|
||||||
public static final ** CREATOR;
|
|
||||||
}
|
|
||||||
|
|
||||||
-keep class androidx.annotation.Keep
|
|
||||||
|
|
||||||
-keep @androidx.annotation.Keep class * {*;}
|
|
||||||
|
|
||||||
-keepclasseswithmembers class * {
|
|
||||||
@androidx.annotation.Keep <methods>;
|
|
||||||
}
|
|
||||||
|
|
||||||
-keepclasseswithmembers class * {
|
|
||||||
@androidx.annotation.Keep <fields>;
|
|
||||||
}
|
|
||||||
|
|
||||||
-keepclasseswithmembers class * {
|
|
||||||
@androidx.annotation.Keep <init>(...);
|
|
||||||
}
|
|
||||||
Vendored
+67
-16
@@ -58,7 +58,6 @@
|
|||||||
kotlinx.serialization.KSerializer serializer(...);
|
kotlinx.serialization.KSerializer serializer(...);
|
||||||
}
|
}
|
||||||
|
|
||||||
# Filter serializer
|
|
||||||
-keep,includedescriptorclasses class xyz.nulldev.ts.api.http.serializer.**$$serializer { *; }
|
-keep,includedescriptorclasses class xyz.nulldev.ts.api.http.serializer.**$$serializer { *; }
|
||||||
-keepclassmembers class xyz.nulldev.ts.api.http.serializer.** {
|
-keepclassmembers class xyz.nulldev.ts.api.http.serializer.** {
|
||||||
*** Companion;
|
*** Companion;
|
||||||
@@ -67,25 +66,37 @@
|
|||||||
kotlinx.serialization.KSerializer serializer(...);
|
kotlinx.serialization.KSerializer serializer(...);
|
||||||
}
|
}
|
||||||
|
|
||||||
# Keep extension's common dependencies
|
# Madokami extension username and password crash fix
|
||||||
-keep,allowoptimization class eu.kanade.tachiyomi.** { public protected *; }
|
-keepclassmembers class androidx.preference.EditTextPreference {
|
||||||
-keep,allowoptimization class androidx.preference.** { *; }
|
*** mOnBindEditTextListener;
|
||||||
-keep,allowoptimization class kotlin.** { public protected *; }
|
*** mText;
|
||||||
-keep,allowoptimization class kotlinx.coroutines.** { public protected *; }
|
public *;
|
||||||
-keep,allowoptimization class okhttp3.** { public protected *; }
|
}
|
||||||
-keep,allowoptimization class okio.** { public protected *; }
|
|
||||||
-keep,allowoptimization class rx.** { public protected *; }
|
# Hitomi extension crash fix
|
||||||
-keep,allowoptimization class org.jsoup.** { public protected *; }
|
-keepclassmembers class rx.Single {
|
||||||
-keep,allowoptimization class com.google.gson.** { public protected *; }
|
*** onSubscribe;
|
||||||
-keep,allowoptimization class com.github.salomonbrys.kotson.** { public protected *; }
|
final *;
|
||||||
-keep,allowoptimization class com.squareup.duktape.** { public protected *; }
|
protected *;
|
||||||
-keep,allowoptimization class app.cash.quickjs.** { public protected *; }
|
public *;
|
||||||
-keep,allowoptimization class uy.kohesive.injekt.** { public protected *; }
|
}
|
||||||
-keep,allowoptimization class kotlinx.serialization.** { public protected *; }
|
|
||||||
|
|
||||||
# RxJava 1.1.0
|
# RxJava 1.1.0
|
||||||
-dontwarn sun.misc.**
|
-dontwarn sun.misc.**
|
||||||
|
|
||||||
|
-keepclassmembers class rx.internal.util.unsafe.*ArrayQueue*Field* {
|
||||||
|
long producerIndex;
|
||||||
|
long consumerIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
-keepclassmembers class rx.internal.util.unsafe.BaseLinkedQueueProducerNodeRef {
|
||||||
|
rx.internal.util.atomic.LinkedQueueNode producerNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
-keepclassmembers class rx.internal.util.unsafe.BaseLinkedQueueConsumerNodeRef {
|
||||||
|
rx.internal.util.atomic.LinkedQueueNode consumerNode;
|
||||||
|
}
|
||||||
|
|
||||||
-dontnote rx.internal.util.PlatformDependent
|
-dontnote rx.internal.util.PlatformDependent
|
||||||
|
|
||||||
# === Reactive network: https://github.com/pwittchen/ReactiveNetwork/tree/v0.12.4#proguard-configuration
|
# === Reactive network: https://github.com/pwittchen/ReactiveNetwork/tree/v0.12.4#proguard-configuration
|
||||||
@@ -107,6 +118,32 @@
|
|||||||
# === Okio: https://github.com/square/okio/tree/9b8545e7fa267c9d89753283990f24a35cd69cd6#proguard
|
# === Okio: https://github.com/square/okio/tree/9b8545e7fa267c9d89753283990f24a35cd69cd6#proguard
|
||||||
-dontwarn okio.**
|
-dontwarn okio.**
|
||||||
|
|
||||||
|
# === GSON: https://raw.githubusercontent.com/google/gson/master/examples/android-proguard-example/proguard.cfg
|
||||||
|
# Gson uses generic type information stored in a class file when working with fields. Proguard
|
||||||
|
# removes such information by default, so configure it to keep all of it.
|
||||||
|
-keepattributes Signature
|
||||||
|
|
||||||
|
# For using GSON @Expose annotation
|
||||||
|
-keepattributes *Annotation*
|
||||||
|
|
||||||
|
# Gson specific classes
|
||||||
|
-dontwarn sun.misc.**
|
||||||
|
#-keep class com.google.gson.stream.** { *; }
|
||||||
|
|
||||||
|
# Application classes that will be serialized/deserialized over Gson
|
||||||
|
-keep class com.google.gson.examples.android.model.** { <fields>; }
|
||||||
|
|
||||||
|
# Prevent proguard from stripping interface information from TypeAdapterFactory,
|
||||||
|
# JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter)
|
||||||
|
-keep class * implements com.google.gson.TypeAdapterFactory
|
||||||
|
-keep class * implements com.google.gson.JsonSerializer
|
||||||
|
-keep class * implements com.google.gson.JsonDeserializer
|
||||||
|
|
||||||
|
# Prevent R8 from leaving Data object members always null
|
||||||
|
-keepclassmembers,allowobfuscation class * {
|
||||||
|
@com.google.gson.annotations.SerializedName <fields>;
|
||||||
|
}
|
||||||
|
|
||||||
# == Nucleus
|
# == Nucleus
|
||||||
-keepclassmembers class * extends nucleus.presenter.Presenter {
|
-keepclassmembers class * extends nucleus.presenter.Presenter {
|
||||||
<init>();
|
<init>();
|
||||||
@@ -118,6 +155,20 @@
|
|||||||
## From original config: "Attempt to fix: java.lang.NoClassDefFoundError: uy.kohesive.injekt.registry.default.DefaultRegistrar$NOKEY$1"
|
## From original config: "Attempt to fix: java.lang.NoClassDefFoundError: uy.kohesive.injekt.registry.default.DefaultRegistrar$NOKEY$1"
|
||||||
-keep class uy.kohesive.injekt.** { *; }
|
-keep class uy.kohesive.injekt.** { *; }
|
||||||
|
|
||||||
|
|
||||||
|
# === Glide
|
||||||
|
-keep public class * implements com.bumptech.glide.module.GlideModule
|
||||||
|
-keep public class * extends com.bumptech.glide.module.AppGlideModule
|
||||||
|
-keep public enum com.bumptech.glide.load.ImageHeaderParser$** {
|
||||||
|
**[] $VALUES;
|
||||||
|
public *;
|
||||||
|
}
|
||||||
|
|
||||||
|
-dontwarn com.bumptech.glide.load.resource.bitmap.VideoDecoder
|
||||||
|
|
||||||
|
# === Glide-transformations: https://github.com/wasabeef/glide-transformations/blob/3aa8e53c6a51b8351d312f802ba1354c5b115168/transformations/proguard-rules.txt
|
||||||
|
-dontwarn jp.co.cyberagent.android.gpuimage.**
|
||||||
|
|
||||||
# === Conductor
|
# === Conductor
|
||||||
# This isn't in the consumer proguard rules yet: https://github.com/bluelinelabs/Conductor/pull/550/files
|
# This isn't in the consumer proguard rules yet: https://github.com/bluelinelabs/Conductor/pull/550/files
|
||||||
-keepclassmembers public class * extends com.bluelinelabs.conductor.ControllerChangeHandler {
|
-keepclassmembers public class * extends com.bluelinelabs.conductor.ControllerChangeHandler {
|
||||||
|
|||||||
+1
-1
@@ -17,7 +17,7 @@
|
|||||||
android:shortcutDisabledMessage="@string/app_not_available"
|
android:shortcutDisabledMessage="@string/app_not_available"
|
||||||
android:shortcutId="show_recently_updated"
|
android:shortcutId="show_recently_updated"
|
||||||
android:shortcutLongLabel="@string/label_recent_updates"
|
android:shortcutLongLabel="@string/label_recent_updates"
|
||||||
android:shortcutShortLabel="@string/label_recent_updates">
|
android:shortcutShortLabel="@string/short_recent_updates">
|
||||||
<intent
|
<intent
|
||||||
android:action="eu.kanade.tachiyomi.SHOW_RECENTLY_UPDATED"
|
android:action="eu.kanade.tachiyomi.SHOW_RECENTLY_UPDATED"
|
||||||
android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity" />
|
android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity" />
|
||||||
|
|||||||
+187
-189
@@ -14,18 +14,17 @@
|
|||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
|
||||||
|
|
||||||
<!-- For managing extensions -->
|
<!-- For managing extensions -->
|
||||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||||
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
|
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
|
||||||
<uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" />
|
|
||||||
<!-- To view extension packages in API 30+ -->
|
<!-- To view extension packages in API 30+ -->
|
||||||
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
|
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".App"
|
android:name=".App"
|
||||||
android:allowBackup="false"
|
android:allowBackup="true"
|
||||||
|
android:fullBackupContent="@xml/backup_rules"
|
||||||
android:hardwareAccelerated="true"
|
android:hardwareAccelerated="true"
|
||||||
android:hasFragileUserData="true"
|
android:hasFragileUserData="true"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
@@ -33,15 +32,12 @@
|
|||||||
android:largeHeap="true"
|
android:largeHeap="true"
|
||||||
android:requestLegacyExternalStorage="true"
|
android:requestLegacyExternalStorage="true"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:theme="@style/Theme.Tachiyomi"
|
android:theme="@style/Theme.Tachiyomi.Light"
|
||||||
android:supportsRtl="true"
|
|
||||||
android:networkSecurityConfig="@xml/network_security_config">
|
android:networkSecurityConfig="@xml/network_security_config">
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.main.MainActivity"
|
android:name=".ui.main.MainActivity"
|
||||||
android:launchMode="singleTop"
|
android:launchMode="singleTop"
|
||||||
android:theme="@style/Theme.Tachiyomi.SplashScreen"
|
android:theme="@style/Theme.Splash">
|
||||||
android:exported="true">
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
@@ -55,8 +51,7 @@
|
|||||||
android:name=".ui.main.DeepLinkActivity"
|
android:name=".ui.main.DeepLinkActivity"
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask"
|
||||||
android:theme="@android:style/Theme.NoDisplay"
|
android:theme="@android:style/Theme.NoDisplay"
|
||||||
android:label="@string/action_global_search"
|
android:label="@string/action_global_search">
|
||||||
android:exported="true">
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.SEARCH" />
|
<action android:name="android.intent.action.SEARCH" />
|
||||||
<action android:name="com.google.android.gms.actions.SEARCH_ACTION" />
|
<action android:name="com.google.android.gms.actions.SEARCH_ACTION" />
|
||||||
@@ -77,11 +72,9 @@
|
|||||||
android:name="android.app.searchable"
|
android:name="android.app.searchable"
|
||||||
android:resource="@xml/searchable" />
|
android:resource="@xml/searchable" />
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.reader.ReaderActivity"
|
android:name=".ui.reader.ReaderActivity"
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask">
|
||||||
android:exported="false">
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="com.samsung.android.support.REMOTE_ACTION" />
|
<action android:name="com.samsung.android.support.REMOTE_ACTION" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
@@ -89,26 +82,15 @@
|
|||||||
<meta-data android:name="com.samsung.android.support.REMOTE_ACTION"
|
<meta-data android:name="com.samsung.android.support.REMOTE_ACTION"
|
||||||
android:resource="@xml/s_pen_actions"/>
|
android:resource="@xml/s_pen_actions"/>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.security.UnlockActivity"
|
android:name=".ui.security.BiometricUnlockActivity"
|
||||||
android:theme="@style/Theme.Tachiyomi"
|
android:theme="@style/Theme.Splash" />
|
||||||
android:exported="false" />
|
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.webview.WebViewActivity"
|
android:name=".ui.webview.WebViewActivity"
|
||||||
android:configChanges="uiMode|orientation|screenSize"
|
android:configChanges="uiMode|orientation|screenSize" />
|
||||||
android:exported="false" />
|
|
||||||
|
|
||||||
<activity
|
|
||||||
android:name=".extension.util.ExtensionInstallActivity"
|
|
||||||
android:theme="@android:style/Theme.Translucent.NoTitleBar"
|
|
||||||
android:exported="false" />
|
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.setting.track.AnilistLoginActivity"
|
android:name=".ui.setting.track.AnilistLoginActivity"
|
||||||
android:label="Anilist"
|
android:label="Anilist">
|
||||||
android:exported="true">
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
@@ -122,8 +104,7 @@
|
|||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.setting.track.MyAnimeListLoginActivity"
|
android:name=".ui.setting.track.MyAnimeListLoginActivity"
|
||||||
android:label="MyAnimeList"
|
android:label="MyAnimeList">
|
||||||
android:exported="true">
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
@@ -137,8 +118,7 @@
|
|||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.setting.track.ShikimoriLoginActivity"
|
android:name=".ui.setting.track.ShikimoriLoginActivity"
|
||||||
android:label="Shikimori"
|
android:label="Shikimori">
|
||||||
android:exported="true">
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
@@ -152,8 +132,7 @@
|
|||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.setting.track.BangumiLoginActivity"
|
android:name=".ui.setting.track.BangumiLoginActivity"
|
||||||
android:label="Bangumi"
|
android:label="Bangumi">
|
||||||
android:exported="true">
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
@@ -167,9 +146,18 @@
|
|||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name="exh.ui.login.EhLoginActivity"
|
android:name=".extension.util.ExtensionInstallActivity"
|
||||||
android:label="EHentaiLogin"
|
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
|
||||||
android:exported="false"/>
|
|
||||||
|
<provider
|
||||||
|
android:name="androidx.core.content.FileProvider"
|
||||||
|
android:authorities="${applicationId}.provider"
|
||||||
|
android:exported="false"
|
||||||
|
android:grantUriPermissions="true">
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
|
android:resource="@xml/provider_paths" />
|
||||||
|
</provider>
|
||||||
|
|
||||||
<receiver
|
<receiver
|
||||||
android:name=".data.notification.NotificationReceiver"
|
android:name=".data.notification.NotificationReceiver"
|
||||||
@@ -184,7 +172,7 @@
|
|||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".data.updater.AppUpdateService"
|
android:name=".data.updater.UpdaterService"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
<service
|
<service
|
||||||
@@ -195,183 +183,193 @@
|
|||||||
android:name=".data.backup.BackupRestoreService"
|
android:name=".data.backup.BackupRestoreService"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
<service android:name=".extension.util.ExtensionInstallService"
|
<!-- EH -->
|
||||||
|
<service
|
||||||
|
android:name="exh.md.similar.SimilarUpdateService"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
<provider
|
<service
|
||||||
android:name="androidx.core.content.FileProvider"
|
android:name="exh.eh.EHentaiUpdateWorker"
|
||||||
android:authorities="${applicationId}.provider"
|
android:permission="android.permission.BIND_JOB_SERVICE"
|
||||||
android:exported="false"
|
android:exported="true" />
|
||||||
android:grantUriPermissions="true">
|
|
||||||
<meta-data
|
|
||||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
|
||||||
android:resource="@xml/provider_paths" />
|
|
||||||
</provider>
|
|
||||||
|
|
||||||
<provider
|
|
||||||
android:name="rikka.shizuku.ShizukuProvider"
|
|
||||||
android:authorities="${applicationId}.shizuku"
|
|
||||||
android:multiprocess="false"
|
|
||||||
android:enabled="true"
|
|
||||||
android:exported="true"
|
|
||||||
android:permission="android.permission.INTERACT_ACROSS_USERS_FULL" />
|
|
||||||
|
|
||||||
<meta-data android:name="android.webkit.WebView.EnableSafeBrowsing"
|
|
||||||
android:value="false" />
|
|
||||||
<meta-data android:name="android.webkit.WebView.MetricsOptOut"
|
|
||||||
android:value="true" />
|
|
||||||
|
|
||||||
<!-- EH -->
|
|
||||||
<activity
|
<activity
|
||||||
android:name="exh.ui.intercept.InterceptActivity"
|
android:name="exh.ui.intercept.InterceptActivity"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:theme="@style/Theme.Tachiyomi"
|
android:theme="@style/Theme.EHActivity">
|
||||||
android:exported="true">
|
|
||||||
<!-- E-Hentai -->
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
<data android:scheme="https" />
|
<!-- EH -->
|
||||||
<data android:scheme="http" />
|
<data
|
||||||
|
android:host="g.e-hentai.org"
|
||||||
|
android:pathPrefix="/g/"
|
||||||
|
android:scheme="http" />
|
||||||
|
<data
|
||||||
|
android:host="g.e-hentai.org"
|
||||||
|
android:pathPrefix="/g/"
|
||||||
|
android:scheme="https" />
|
||||||
|
<data
|
||||||
|
android:host="e-hentai.org"
|
||||||
|
android:pathPrefix="/g/"
|
||||||
|
android:scheme="http" />
|
||||||
|
<data
|
||||||
|
android:host="e-hentai.org"
|
||||||
|
android:pathPrefix="/g/"
|
||||||
|
android:scheme="https" />
|
||||||
|
|
||||||
<data android:host="e-hentai.org" />
|
<!-- EXH -->
|
||||||
<data android:host="www.e-hentai.org" />
|
<data
|
||||||
<data android:host="g.e-hentai.org" />
|
android:host="exhentai.org"
|
||||||
|
android:pathPrefix="/g/"
|
||||||
|
android:scheme="http" />
|
||||||
|
<data
|
||||||
|
android:host="exhentai.org"
|
||||||
|
android:pathPrefix="/g/"
|
||||||
|
android:scheme="https" />
|
||||||
|
|
||||||
<data android:pathPattern="/g/..*" />
|
<!-- nhentai -->
|
||||||
</intent-filter>
|
<data
|
||||||
<!-- ExHentai -->
|
android:host="nhentai.net"
|
||||||
<intent-filter>
|
android:pathPrefix="/g/"
|
||||||
<action android:name="android.intent.action.VIEW" />
|
android:scheme="http" />
|
||||||
|
<data
|
||||||
|
android:host="nhentai.net"
|
||||||
|
android:pathPrefix="/g/"
|
||||||
|
android:scheme="https" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<!-- Perv Eden -->
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
<data
|
||||||
|
android:host="www.perveden.com"
|
||||||
|
android:pathPattern="/.*/.*-manga/.*"
|
||||||
|
android:scheme="http" />
|
||||||
|
<data
|
||||||
|
android:host="www.perveden.com"
|
||||||
|
android:pathPattern="/.*/.*-manga/.*"
|
||||||
|
android:scheme="https" />
|
||||||
|
|
||||||
<data android:scheme="https" />
|
<!-- Hentai Cafe -->
|
||||||
<data android:scheme="http" />
|
<data
|
||||||
|
android:host="hentai.cafe"
|
||||||
|
android:pathPrefix="/hc.fyi/"
|
||||||
|
android:scheme="http" />
|
||||||
|
<data
|
||||||
|
android:host="hentai.cafe"
|
||||||
|
android:pathPrefix="/hc.fyi/"
|
||||||
|
android:scheme="https" />
|
||||||
|
|
||||||
<data android:host="exhentai.org" />
|
<!-- Tsumino -->
|
||||||
<data android:host="www.exhentai.org" />
|
<data
|
||||||
|
android:host="www.tsumino.com"
|
||||||
|
android:pathPrefix="/Book/Info/"
|
||||||
|
android:scheme="http" />
|
||||||
|
<data
|
||||||
|
android:host="www.tsumino.com"
|
||||||
|
android:pathPrefix="/Book/Info/"
|
||||||
|
android:scheme="https" />
|
||||||
|
<data
|
||||||
|
android:host="www.tsumino.com"
|
||||||
|
android:pathPrefix="/Read/View/"
|
||||||
|
android:scheme="http" />
|
||||||
|
<data
|
||||||
|
android:host="www.tsumino.com"
|
||||||
|
android:pathPrefix="/Read/View/"
|
||||||
|
android:scheme="https" />
|
||||||
|
|
||||||
<data android:pathPattern="/g/..*" />
|
<!-- Hitomi.la -->
|
||||||
</intent-filter>
|
<data
|
||||||
<!-- NHentai -->
|
android:host="hitomi.la"
|
||||||
<intent-filter>
|
android:pathPrefix="/galleries/"
|
||||||
<action android:name="android.intent.action.VIEW" />
|
android:scheme="http" />
|
||||||
|
<data
|
||||||
|
android:host="hitomi.la"
|
||||||
|
android:pathPrefix="/reader/"
|
||||||
|
android:scheme="http" />
|
||||||
|
<data
|
||||||
|
android:host="hitomi.la"
|
||||||
|
android:pathPrefix="/galleries/"
|
||||||
|
android:scheme="https" />
|
||||||
|
<data
|
||||||
|
android:host="hitomi.la"
|
||||||
|
android:pathPrefix="/reader/"
|
||||||
|
android:scheme="https" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<!-- Pururin.io -->
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
<data
|
||||||
|
android:host="pururin.io"
|
||||||
|
android:pathPrefix="/gallery/"
|
||||||
|
android:scheme="http" />
|
||||||
|
<data
|
||||||
|
android:host="pururin.io"
|
||||||
|
android:pathPrefix="/gallery/"
|
||||||
|
android:scheme="https" />
|
||||||
|
|
||||||
<data android:scheme="https" />
|
<!-- HBrowse -->
|
||||||
<data android:scheme="http" />
|
<data
|
||||||
|
android:host="www.hbrowse.com"
|
||||||
|
android:scheme="http" />
|
||||||
|
<data
|
||||||
|
android:host="www.hbrowse.com"
|
||||||
|
android:scheme="https" />
|
||||||
|
|
||||||
<data android:host="nhentai.net" />
|
<!-- MangaDex -->
|
||||||
<data android:host="www.nhentai.net" />
|
<data
|
||||||
|
android:scheme="https"
|
||||||
|
android:host="www.mangadex.org"
|
||||||
|
android:pathPrefix="/manga/" />
|
||||||
|
<data
|
||||||
|
android:scheme="https"
|
||||||
|
android:host="mangadex.org"
|
||||||
|
android:pathPrefix="/manga/" />
|
||||||
|
<data
|
||||||
|
android:scheme="https"
|
||||||
|
android:host="www.mangadex.cc"
|
||||||
|
android:pathPrefix="/manga/" />
|
||||||
|
<data
|
||||||
|
android:scheme="https"
|
||||||
|
android:host="www.mangadex.cc"
|
||||||
|
android:pathPrefix="/manga/" />
|
||||||
|
|
||||||
<data android:pathPattern="/g/..*" />
|
<data
|
||||||
</intent-filter>
|
android:scheme="https"
|
||||||
<!-- Perv Eden -->
|
android:host="www.mangadex.org"
|
||||||
<intent-filter>
|
android:pathPrefix="/title/" />
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<data
|
||||||
|
android:scheme="https"
|
||||||
|
android:host="mangadex.org"
|
||||||
|
android:pathPrefix="/title/" />
|
||||||
|
<data
|
||||||
|
android:scheme="https"
|
||||||
|
android:host="www.mangadex.cc"
|
||||||
|
android:pathPrefix="/title/" />
|
||||||
|
<data
|
||||||
|
android:scheme="https"
|
||||||
|
android:host="www.mangadex.cc"
|
||||||
|
android:pathPrefix="/title/" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<data
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
android:scheme="https"
|
||||||
|
android:host="www.mangadex.org"
|
||||||
<data android:scheme="https" />
|
android:pathPrefix="/chapter/" />
|
||||||
<data android:scheme="http" />
|
<data
|
||||||
|
android:scheme="https"
|
||||||
<data android:host="perveden.com" />
|
android:host="mangadex.org"
|
||||||
<data android:host="www.perveden.com" />
|
android:pathPrefix="/chapter/" />
|
||||||
|
<data
|
||||||
<data android:pathPattern="/.*/.*-manga/.*" />
|
android:scheme="https"
|
||||||
</intent-filter>
|
android:host="www.mangadex.cc"
|
||||||
<!-- Tsumino -->
|
android:pathPrefix="/chapter/" />
|
||||||
<intent-filter>
|
<data
|
||||||
<action android:name="android.intent.action.VIEW" />
|
android:scheme="https"
|
||||||
|
android:host="www.mangadex.cc"
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
android:pathPrefix="/chapter/" />
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
|
||||||
|
|
||||||
<data android:scheme="https" />
|
|
||||||
<data android:scheme="http" />
|
|
||||||
|
|
||||||
<data android:host="tsumino.com" />
|
|
||||||
<data android:host="www.tsumino.com" />
|
|
||||||
|
|
||||||
<data android:pathPattern="/Read/View/..*" />
|
|
||||||
<data android:pathPattern="/Book/Info/..*" />
|
|
||||||
</intent-filter>
|
|
||||||
<!-- Hitomi.la -->
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.VIEW" />
|
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
|
||||||
|
|
||||||
<data android:scheme="https" />
|
|
||||||
<data android:scheme="http" />
|
|
||||||
|
|
||||||
<data android:host="hitomi.la" />
|
|
||||||
<data android:host="www.hitomi.la" />
|
|
||||||
|
|
||||||
<data android:pathPattern="/reader/..*" />
|
|
||||||
<data android:pathPattern="/galleries/..*" />
|
|
||||||
</intent-filter>
|
|
||||||
<!-- Pururin -->
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.VIEW" />
|
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
|
||||||
|
|
||||||
<data android:scheme="https" />
|
|
||||||
<data android:scheme="http" />
|
|
||||||
|
|
||||||
<data android:host="pururin.io" />
|
|
||||||
|
|
||||||
<data android:pathPattern="/gallery/..*" />
|
|
||||||
</intent-filter>
|
|
||||||
<!-- HBrowse -->
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.VIEW" />
|
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
|
||||||
|
|
||||||
<data android:scheme="https" />
|
|
||||||
<data android:scheme="http" />
|
|
||||||
|
|
||||||
<data android:host="hbrowse.com" />
|
|
||||||
<data android:host="www.hbrowse.com" />
|
|
||||||
|
|
||||||
<!--<data android:pathPattern="/gallery/..*" />-->
|
|
||||||
</intent-filter>
|
|
||||||
<!-- Mangadex -->
|
|
||||||
<intent-filter android:autoVerify="true">
|
|
||||||
<action android:name="android.intent.action.VIEW" />
|
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
|
||||||
|
|
||||||
<data android:scheme="https" />
|
|
||||||
|
|
||||||
<data android:host="mangadex.org" />
|
|
||||||
<data android:host="mangadex.cc" />
|
|
||||||
<data android:host="www.mangadex.org" />
|
|
||||||
<data android:host="www.mangadex.cc" />
|
|
||||||
|
|
||||||
<data android:pathPattern="/manga/..*" />
|
|
||||||
<data android:pathPattern="/title/..*" />
|
|
||||||
<data android:pathPattern="/chapter/..*" />
|
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name="exh.ui.captcha.BrowserActionActivity"
|
android:name="exh.ui.captcha.BrowserActionActivity"
|
||||||
android:theme="@style/Theme.Tachiyomi"
|
android:theme="@style/Theme.EHActivity" />
|
||||||
android:exported="false"/>
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@@ -1,28 +1,16 @@
|
|||||||
package eu.kanade.tachiyomi
|
package eu.kanade.tachiyomi
|
||||||
|
|
||||||
import android.app.ActivityManager
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.app.PendingIntent
|
|
||||||
import android.content.BroadcastReceiver
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.res.Configuration
|
||||||
import android.content.IntentFilter
|
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import android.webkit.WebView
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.lifecycle.LifecycleObserver
|
||||||
import androidx.core.app.NotificationManagerCompat
|
import androidx.lifecycle.OnLifecycleEvent
|
||||||
import androidx.core.content.getSystemService
|
|
||||||
import androidx.lifecycle.DefaultLifecycleObserver
|
|
||||||
import androidx.lifecycle.LifecycleOwner
|
|
||||||
import androidx.lifecycle.ProcessLifecycleOwner
|
import androidx.lifecycle.ProcessLifecycleOwner
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.multidex.MultiDex
|
||||||
import coil.ImageLoader
|
|
||||||
import coil.ImageLoaderFactory
|
|
||||||
import coil.decode.GifDecoder
|
|
||||||
import coil.decode.ImageDecoderDecoder
|
|
||||||
import coil.util.DebugLogger
|
|
||||||
import com.elvishew.xlog.LogConfiguration
|
import com.elvishew.xlog.LogConfiguration
|
||||||
import com.elvishew.xlog.LogLevel
|
import com.elvishew.xlog.LogLevel
|
||||||
import com.elvishew.xlog.XLog
|
import com.elvishew.xlog.XLog
|
||||||
@@ -31,159 +19,144 @@ import com.elvishew.xlog.printer.Printer
|
|||||||
import com.elvishew.xlog.printer.file.backup.NeverBackupStrategy
|
import com.elvishew.xlog.printer.file.backup.NeverBackupStrategy
|
||||||
import com.elvishew.xlog.printer.file.clean.FileLastModifiedCleanStrategy
|
import com.elvishew.xlog.printer.file.clean.FileLastModifiedCleanStrategy
|
||||||
import com.elvishew.xlog.printer.file.naming.DateFileNameGenerator
|
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.analytics.ktx.analytics
|
||||||
import com.google.firebase.ktx.Firebase
|
import com.google.firebase.ktx.Firebase
|
||||||
import com.ms_square.debugoverlay.DebugOverlay
|
import com.ms_square.debugoverlay.DebugOverlay
|
||||||
import com.ms_square.debugoverlay.modules.FpsModule
|
import com.ms_square.debugoverlay.modules.FpsModule
|
||||||
import eu.kanade.tachiyomi.data.coil.ByteBufferFetcher
|
|
||||||
import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher
|
|
||||||
import eu.kanade.tachiyomi.data.coil.TachiyomiImageDecoder
|
|
||||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues
|
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
|
||||||
import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate
|
import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate
|
||||||
import eu.kanade.tachiyomi.util.preference.asImmediateFlow
|
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||||
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil
|
|
||||||
import eu.kanade.tachiyomi.util.system.animatorDurationScale
|
|
||||||
import eu.kanade.tachiyomi.util.system.logcat
|
|
||||||
import eu.kanade.tachiyomi.util.system.notification
|
|
||||||
import exh.debug.DebugToggles
|
import exh.debug.DebugToggles
|
||||||
import exh.log.CrashlyticsPrinter
|
import exh.log.CrashlyticsPrinter
|
||||||
import exh.log.EHDebugModeOverlay
|
import exh.log.EHDebugModeOverlay
|
||||||
import exh.log.EHLogLevel
|
import exh.log.EHLogLevel
|
||||||
import exh.log.EnhancedFilePrinter
|
import exh.log.EnhancedFilePrinter
|
||||||
import exh.log.XLogLogcatLogger
|
|
||||||
import exh.log.xLogD
|
|
||||||
import exh.log.xLogE
|
|
||||||
import exh.syDebugVersion
|
import exh.syDebugVersion
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import io.realm.Realm
|
||||||
import kotlinx.coroutines.flow.onEach
|
import io.realm.RealmConfiguration
|
||||||
import logcat.LogPriority
|
import kotlinx.coroutines.GlobalScope
|
||||||
import logcat.LogcatLogger
|
import kotlinx.coroutines.launch
|
||||||
import org.conscrypt.Conscrypt
|
import org.conscrypt.Conscrypt
|
||||||
|
import timber.log.Timber
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.security.NoSuchAlgorithmException
|
||||||
import java.security.Security
|
import java.security.Security
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import kotlin.time.Duration.Companion.days
|
import javax.net.ssl.SSLContext
|
||||||
|
import kotlin.concurrent.thread
|
||||||
|
import kotlin.time.ExperimentalTime
|
||||||
|
import kotlin.time.days
|
||||||
|
|
||||||
open class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
|
open class App : Application(), LifecycleObserver {
|
||||||
|
|
||||||
private val preferences: PreferencesHelper by injectLazy()
|
private val preferences: PreferencesHelper by injectLazy()
|
||||||
|
|
||||||
private val disableIncognitoReceiver = DisableIncognitoReceiver()
|
private lateinit var firebaseAnalytics: FirebaseAnalytics
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super<Application>.onCreate()
|
super.onCreate()
|
||||||
// if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
|
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
|
||||||
setupExhLogging() // EXH logging
|
setupExhLogging() // EXH logging
|
||||||
LogcatLogger.install(XLogLogcatLogger()) // SY Redirect Logcat to XLog
|
|
||||||
if (!BuildConfig.DEBUG) addAnalytics()
|
if (!BuildConfig.DEBUG) addAnalytics()
|
||||||
|
|
||||||
|
workaroundAndroid7BrokenSSL()
|
||||||
|
|
||||||
// TLS 1.3 support for Android < 10
|
// TLS 1.3 support for Android < 10
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||||
Security.insertProviderAt(Conscrypt.newProvider(), 1)
|
Security.insertProviderAt(Conscrypt.newProvider(), 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Avoid potential crashes
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
|
||||||
val process = getProcessName()
|
|
||||||
if (packageName != process) WebView.setDataDirectorySuffix(process)
|
|
||||||
}
|
|
||||||
|
|
||||||
Injekt.importModule(AppModule(this))
|
Injekt.importModule(AppModule(this))
|
||||||
|
|
||||||
setupNotificationChannels()
|
setupNotificationChannels()
|
||||||
|
Realm.init(this)
|
||||||
|
GlobalScope.launch { deleteOldMetadataRealm() } // Delete old metadata DB (EH)
|
||||||
if ((BuildConfig.DEBUG || BuildConfig.BUILD_TYPE == "releaseTest") && DebugToggles.ENABLE_DEBUG_OVERLAY.enabled) {
|
if ((BuildConfig.DEBUG || BuildConfig.BUILD_TYPE == "releaseTest") && DebugToggles.ENABLE_DEBUG_OVERLAY.enabled) {
|
||||||
setupDebugOverlay()
|
setupDebugOverlay()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LocaleHelper.updateConfiguration(this, resources.configuration)
|
||||||
|
|
||||||
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
|
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
|
||||||
|
|
||||||
// Show notification to disable Incognito Mode when it's enabled
|
|
||||||
preferences.incognitoMode().asFlow()
|
|
||||||
.onEach { enabled ->
|
|
||||||
val notificationManager = NotificationManagerCompat.from(this)
|
|
||||||
if (enabled) {
|
|
||||||
disableIncognitoReceiver.register()
|
|
||||||
val notification = notification(Notifications.CHANNEL_INCOGNITO_MODE) {
|
|
||||||
setContentTitle(getString(R.string.pref_incognito_mode))
|
|
||||||
setContentText(getString(R.string.notification_incognito_text))
|
|
||||||
setSmallIcon(R.drawable.ic_glasses_24dp)
|
|
||||||
setOngoing(true)
|
|
||||||
|
|
||||||
val pendingIntent = PendingIntent.getBroadcast(
|
|
||||||
this@App,
|
|
||||||
0,
|
|
||||||
Intent(ACTION_DISABLE_INCOGNITO_MODE),
|
|
||||||
PendingIntent.FLAG_ONE_SHOT
|
|
||||||
)
|
|
||||||
setContentIntent(pendingIntent)
|
|
||||||
}
|
|
||||||
notificationManager.notify(Notifications.ID_INCOGNITO_MODE, notification)
|
|
||||||
} else {
|
|
||||||
disableIncognitoReceiver.unregister()
|
|
||||||
notificationManager.cancel(Notifications.ID_INCOGNITO_MODE)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.launchIn(ProcessLifecycleOwner.get().lifecycleScope)
|
|
||||||
|
|
||||||
preferences.themeMode()
|
|
||||||
.asImmediateFlow {
|
|
||||||
AppCompatDelegate.setDefaultNightMode(
|
|
||||||
when (it) {
|
|
||||||
PreferenceValues.ThemeMode.light -> AppCompatDelegate.MODE_NIGHT_NO
|
|
||||||
PreferenceValues.ThemeMode.dark -> AppCompatDelegate.MODE_NIGHT_YES
|
|
||||||
PreferenceValues.ThemeMode.system -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}.launchIn(ProcessLifecycleOwner.get().lifecycleScope)
|
|
||||||
|
|
||||||
/*if (!LogcatLogger.isInstalled && preferences.verboseLogging()) {
|
|
||||||
LogcatLogger.install(AndroidLogcatLogger(LogPriority.VERBOSE))
|
|
||||||
}*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun newImageLoader(): ImageLoader {
|
override fun attachBaseContext(base: Context) {
|
||||||
return ImageLoader.Builder(this).apply {
|
super.attachBaseContext(base)
|
||||||
componentRegistry {
|
MultiDex.install(this)
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
|
||||||
add(ImageDecoderDecoder(this@App))
|
|
||||||
} else {
|
|
||||||
add(GifDecoder())
|
|
||||||
}
|
|
||||||
add(TachiyomiImageDecoder(this@App.resources))
|
|
||||||
add(ByteBufferFetcher())
|
|
||||||
add(MangaCoverFetcher())
|
|
||||||
}
|
|
||||||
okHttpClient(Injekt.get<NetworkHelper>().coilClient)
|
|
||||||
crossfade((300 * this@App.animatorDurationScale).toInt())
|
|
||||||
allowRgb565(getSystemService<ActivityManager>()!!.isLowRamDevice)
|
|
||||||
if (preferences.verboseLogging()) logger(DebugLogger())
|
|
||||||
}.build()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun addAnalytics() {
|
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||||
if (syDebugVersion != "0") {
|
super.onConfigurationChanged(newConfig)
|
||||||
Firebase.analytics.setUserProperty("preview_version", syDebugVersion)
|
LocaleHelper.updateConfiguration(this, newConfig, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun workaroundAndroid7BrokenSSL() {
|
||||||
|
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.N ||
|
||||||
|
Build.VERSION.SDK_INT == Build.VERSION_CODES.N_MR1
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
SSLContext.getInstance("TLSv1.2")
|
||||||
|
} catch (e: NoSuchAlgorithmException) {
|
||||||
|
XLog.tag("Init").e("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)
|
||||||
|
} catch (e: GooglePlayServicesNotAvailableException) {
|
||||||
|
XLog.tag("Init").e("Could not install Android 7 broken SSL workaround!", e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStop(owner: LifecycleOwner) {
|
private fun addAnalytics() {
|
||||||
if (!AuthenticatorUtil.isAuthenticating && preferences.lockAppAfter().get() >= 0) {
|
firebaseAnalytics = Firebase.analytics
|
||||||
|
if (syDebugVersion != "0") {
|
||||||
|
firebaseAnalytics.setUserProperty("preview_version", syDebugVersion)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
|
||||||
|
@Suppress("unused")
|
||||||
|
fun onAppBackgrounded() {
|
||||||
|
if (preferences.lockAppAfter().get() >= 0) {
|
||||||
SecureActivityDelegate.locked = true
|
SecureActivityDelegate.locked = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected open fun setupNotificationChannels() {
|
protected open fun setupNotificationChannels() {
|
||||||
try {
|
Notifications.createChannels(this)
|
||||||
Notifications.createChannels(this)
|
}
|
||||||
} catch (e: Exception) {
|
|
||||||
logcat(LogPriority.ERROR, e) { "Failed to modify notification channels" }
|
// 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,8 +165,8 @@ open class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
|
|||||||
EHLogLevel.init(this)
|
EHLogLevel.init(this)
|
||||||
|
|
||||||
val logLevel = when {
|
val logLevel = when {
|
||||||
EHLogLevel.shouldLog(EHLogLevel.EXTREME) -> LogLevel.ALL
|
EHLogLevel.shouldLog(EHLogLevel.EXTRA) -> LogLevel.ALL
|
||||||
EHLogLevel.shouldLog(EHLogLevel.EXTRA) || BuildConfig.DEBUG -> LogLevel.DEBUG
|
BuildConfig.DEBUG -> LogLevel.DEBUG
|
||||||
else -> LogLevel.WARN
|
else -> LogLevel.WARN
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,9 +186,11 @@ open class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
|
|||||||
|
|
||||||
val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault())
|
val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault())
|
||||||
|
|
||||||
|
@OptIn(ExperimentalTime::class)
|
||||||
printers += EnhancedFilePrinter
|
printers += EnhancedFilePrinter
|
||||||
.Builder(logFolder.absolutePath) {
|
.Builder(logFolder.absolutePath)
|
||||||
fileNameGenerator = object : DateFileNameGenerator() {
|
.fileNameGenerator(
|
||||||
|
object : DateFileNameGenerator() {
|
||||||
override fun generateFileName(logLevel: Int, timestamp: Long): String {
|
override fun generateFileName(logLevel: Int, timestamp: Long): String {
|
||||||
return super.generateFileName(
|
return super.generateFileName(
|
||||||
logLevel,
|
logLevel,
|
||||||
@@ -223,12 +198,13 @@ open class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
|
|||||||
) + "-${BuildConfig.BUILD_TYPE}.log"
|
) + "-${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.inWholeMilliseconds)
|
|
||||||
backupStrategy = NeverBackupStrategy()
|
|
||||||
}
|
}
|
||||||
|
.cleanStrategy(FileLastModifiedCleanStrategy(7.days.toLongMilliseconds()))
|
||||||
|
.backupStrategy(NeverBackupStrategy())
|
||||||
|
.build()
|
||||||
|
|
||||||
// Install Crashlytics in prod
|
// Install Crashlytics in prod
|
||||||
if (!BuildConfig.DEBUG) {
|
if (!BuildConfig.DEBUG) {
|
||||||
@@ -240,19 +216,17 @@ open class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
|
|||||||
*printers.toTypedArray()
|
*printers.toTypedArray()
|
||||||
)
|
)
|
||||||
|
|
||||||
xLogD("Application booting...")
|
XLog.tag("Init").d("Application booting...")
|
||||||
xLogD(
|
XLog.tag("Init").disableStackTrace().d(
|
||||||
"""
|
"App version: ${BuildConfig.VERSION_NAME} (${BuildConfig.FLAVOR}, ${BuildConfig.COMMIT_SHA}, ${BuildConfig.VERSION_CODE})\n" +
|
||||||
App version: ${BuildConfig.VERSION_NAME} (${BuildConfig.FLAVOR}, ${BuildConfig.COMMIT_SHA}, ${BuildConfig.VERSION_CODE})
|
"Preview build: $syDebugVersion\n" +
|
||||||
Preview build: $syDebugVersion
|
"Android version: ${Build.VERSION.RELEASE} (SDK ${Build.VERSION.SDK_INT}) \n" +
|
||||||
Android version: ${Build.VERSION.RELEASE} (SDK ${Build.VERSION.SDK_INT})
|
"Android build ID: ${Build.DISPLAY}\n" +
|
||||||
Android build ID: ${Build.DISPLAY}
|
"Device brand: ${Build.BRAND}\n" +
|
||||||
Device brand: ${Build.BRAND}
|
"Device manufacturer: ${Build.MANUFACTURER}\n" +
|
||||||
Device manufacturer: ${Build.MANUFACTURER}
|
"Device name: ${Build.DEVICE}\n" +
|
||||||
Device name: ${Build.DEVICE}
|
"Device model: ${Build.MODEL}\n" +
|
||||||
Device model: ${Build.MODEL}
|
"Device product name: ${Build.PRODUCT}"
|
||||||
Device product name: ${Build.PRODUCT}
|
|
||||||
""".trimIndent()
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,31 +242,7 @@ open class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
|
|||||||
.install()
|
.install()
|
||||||
} catch (e: IllegalStateException) {
|
} catch (e: IllegalStateException) {
|
||||||
// Crashes if app is in background
|
// Crashes if app is in background
|
||||||
xLogE("Failed to initialize debug overlay, app in background?", e)
|
XLog.tag("Init").e("Failed to initialize debug overlay, app in background?", e)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private inner class DisableIncognitoReceiver : BroadcastReceiver() {
|
|
||||||
private var registered = false
|
|
||||||
|
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
|
||||||
preferences.incognitoMode().set(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun register() {
|
|
||||||
if (!registered) {
|
|
||||||
registerReceiver(this, IntentFilter(ACTION_DISABLE_INCOGNITO_MODE))
|
|
||||||
registered = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun unregister() {
|
|
||||||
if (registered) {
|
|
||||||
unregisterReceiver(this)
|
|
||||||
registered = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private const val ACTION_DISABLE_INCOGNITO_MODE = "tachi.action.DISABLE_INCOGNITO_MODE"
|
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Used by extensions.
|
|
||||||
*
|
|
||||||
* @since extension-lib 1.3
|
|
||||||
*/
|
|
||||||
object AppInfo {
|
|
||||||
fun getVersionCode() = BuildConfig.VERSION_CODE
|
|
||||||
fun getVersionName() = BuildConfig.VERSION_NAME
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
package eu.kanade.tachiyomi
|
package eu.kanade.tachiyomi
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import androidx.core.content.ContextCompat
|
import android.os.Handler
|
||||||
|
import com.google.gson.Gson
|
||||||
import eu.kanade.tachiyomi.data.cache.ChapterCache
|
import eu.kanade.tachiyomi.data.cache.ChapterCache
|
||||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
@@ -9,7 +10,6 @@ import eu.kanade.tachiyomi.data.download.DownloadManager
|
|||||||
import eu.kanade.tachiyomi.data.library.CustomMangaManager
|
import eu.kanade.tachiyomi.data.library.CustomMangaManager
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
import eu.kanade.tachiyomi.data.track.job.DelayedTrackingStore
|
|
||||||
import eu.kanade.tachiyomi.extension.ExtensionManager
|
import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
@@ -26,8 +26,6 @@ class AppModule(val app: Application) : InjektModule {
|
|||||||
override fun InjektRegistrar.registerInjectables() {
|
override fun InjektRegistrar.registerInjectables() {
|
||||||
addSingleton(app)
|
addSingleton(app)
|
||||||
|
|
||||||
addSingletonFactory { Json { ignoreUnknownKeys = true } }
|
|
||||||
|
|
||||||
addSingletonFactory { PreferencesHelper(app) }
|
addSingletonFactory { PreferencesHelper(app) }
|
||||||
|
|
||||||
addSingletonFactory { DatabaseHelper(app) }
|
addSingletonFactory { DatabaseHelper(app) }
|
||||||
@@ -46,7 +44,9 @@ class AppModule(val app: Application) : InjektModule {
|
|||||||
|
|
||||||
addSingletonFactory { TrackManager(app) }
|
addSingletonFactory { TrackManager(app) }
|
||||||
|
|
||||||
addSingletonFactory { DelayedTrackingStore(app) }
|
addSingletonFactory { Gson() }
|
||||||
|
|
||||||
|
addSingletonFactory { Json { ignoreUnknownKeys = true } }
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
addSingletonFactory { CustomMangaManager(app) }
|
addSingletonFactory { CustomMangaManager(app) }
|
||||||
@@ -55,7 +55,7 @@ class AppModule(val app: Application) : InjektModule {
|
|||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
// Asynchronously init expensive components for a faster cold start
|
// Asynchronously init expensive components for a faster cold start
|
||||||
ContextCompat.getMainExecutor(app).execute {
|
Handler().post {
|
||||||
get<PreferencesHelper>()
|
get<PreferencesHelper>()
|
||||||
|
|
||||||
get<NetworkHelper>()
|
get<NetworkHelper>()
|
||||||
|
|||||||
@@ -1,23 +1,15 @@
|
|||||||
package eu.kanade.tachiyomi
|
package eu.kanade.tachiyomi
|
||||||
|
|
||||||
import android.os.Build
|
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreatorJob
|
import eu.kanade.tachiyomi.data.backup.BackupCreatorJob
|
||||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
|
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
|
||||||
import eu.kanade.tachiyomi.data.preference.MANGA_ONGOING
|
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferenceKeys
|
import eu.kanade.tachiyomi.data.preference.PreferenceKeys
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
import eu.kanade.tachiyomi.data.updater.AppUpdateJob
|
import eu.kanade.tachiyomi.data.updater.UpdaterJob
|
||||||
import eu.kanade.tachiyomi.extension.ExtensionUpdateJob
|
import eu.kanade.tachiyomi.extension.ExtensionUpdateJob
|
||||||
import eu.kanade.tachiyomi.network.PREF_DOH_CLOUDFLARE
|
|
||||||
import eu.kanade.tachiyomi.ui.library.LibrarySort
|
import eu.kanade.tachiyomi.ui.library.LibrarySort
|
||||||
import eu.kanade.tachiyomi.ui.library.setting.SortDirectionSetting
|
|
||||||
import eu.kanade.tachiyomi.ui.library.setting.SortModeSetting
|
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
|
|
||||||
import eu.kanade.tachiyomi.util.preference.minusAssign
|
|
||||||
import eu.kanade.tachiyomi.util.preference.plusAssign
|
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
|
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
@@ -37,29 +29,30 @@ object Migrations {
|
|||||||
fun upgrade(preferences: PreferencesHelper): Boolean {
|
fun upgrade(preferences: PreferencesHelper): Boolean {
|
||||||
val context = preferences.context
|
val context = preferences.context
|
||||||
|
|
||||||
|
// Cancel app updater job for debug builds that don't include it
|
||||||
|
if (BuildConfig.DEBUG && !BuildConfig.INCLUDE_UPDATER) {
|
||||||
|
UpdaterJob.cancelTask(context)
|
||||||
|
}
|
||||||
|
|
||||||
val oldVersion = preferences.lastVersionCode().get()
|
val oldVersion = preferences.lastVersionCode().get()
|
||||||
if (oldVersion < BuildConfig.VERSION_CODE) {
|
if (oldVersion < BuildConfig.VERSION_CODE) {
|
||||||
preferences.lastVersionCode().set(BuildConfig.VERSION_CODE)
|
preferences.lastVersionCode().set(BuildConfig.VERSION_CODE)
|
||||||
|
|
||||||
// Always set up background tasks to ensure they're running
|
|
||||||
if (BuildConfig.INCLUDE_UPDATER) {
|
|
||||||
AppUpdateJob.setupTask(context)
|
|
||||||
}
|
|
||||||
ExtensionUpdateJob.setupTask(context)
|
|
||||||
LibraryUpdateJob.setupTask(context)
|
|
||||||
BackupCreatorJob.setupTask(context)
|
|
||||||
|
|
||||||
// Fresh install
|
// Fresh install
|
||||||
if (oldVersion == 0) {
|
if (oldVersion == 0) {
|
||||||
|
// Set up default background tasks
|
||||||
|
if (BuildConfig.INCLUDE_UPDATER) {
|
||||||
|
UpdaterJob.setupTask(context)
|
||||||
|
}
|
||||||
|
ExtensionUpdateJob.setupTask(context)
|
||||||
|
LibraryUpdateJob.setupTask(context)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
|
||||||
|
|
||||||
if (oldVersion < 14) {
|
if (oldVersion < 14) {
|
||||||
// Restore jobs after upgrading to Evernote's job scheduler.
|
// Restore jobs after upgrading to Evernote's job scheduler.
|
||||||
if (BuildConfig.INCLUDE_UPDATER) {
|
if (BuildConfig.INCLUDE_UPDATER) {
|
||||||
AppUpdateJob.setupTask(context)
|
UpdaterJob.setupTask(context)
|
||||||
}
|
}
|
||||||
LibraryUpdateJob.setupTask(context)
|
LibraryUpdateJob.setupTask(context)
|
||||||
}
|
}
|
||||||
@@ -92,7 +85,7 @@ object Migrations {
|
|||||||
if (oldVersion < 43) {
|
if (oldVersion < 43) {
|
||||||
// Restore jobs after migrating from Evernote's job scheduler to WorkManager.
|
// Restore jobs after migrating from Evernote's job scheduler to WorkManager.
|
||||||
if (BuildConfig.INCLUDE_UPDATER) {
|
if (BuildConfig.INCLUDE_UPDATER) {
|
||||||
AppUpdateJob.setupTask(context)
|
UpdaterJob.setupTask(context)
|
||||||
}
|
}
|
||||||
LibraryUpdateJob.setupTask(context)
|
LibraryUpdateJob.setupTask(context)
|
||||||
BackupCreatorJob.setupTask(context)
|
BackupCreatorJob.setupTask(context)
|
||||||
@@ -102,17 +95,14 @@ object Migrations {
|
|||||||
}
|
}
|
||||||
if (oldVersion < 44) {
|
if (oldVersion < 44) {
|
||||||
// Reset sorting preference if using removed sort by source
|
// Reset sorting preference if using removed sort by source
|
||||||
val oldSortingMode = prefs.getInt(PreferenceKeys.librarySortingMode, 0)
|
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
if (oldSortingMode == LibrarySort.SOURCE) {
|
if (preferences.librarySortingMode().get() == LibrarySort.SOURCE) {
|
||||||
prefs.edit {
|
preferences.librarySortingMode().set(LibrarySort.ALPHA)
|
||||||
putInt(PreferenceKeys.librarySortingMode, LibrarySort.ALPHA)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (oldVersion < 52) {
|
if (oldVersion < 52) {
|
||||||
// Migrate library filters to tri-state versions
|
// Migrate library filters to tri-state versions
|
||||||
|
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
fun convertBooleanPrefToTriState(key: String): Int {
|
fun convertBooleanPrefToTriState(key: String): Int {
|
||||||
val oldPrefValue = prefs.getBoolean(key, false)
|
val oldPrefValue = prefs.getBoolean(key, false)
|
||||||
return if (oldPrefValue) ExtendedNavigationView.Item.TriStateGroup.State.INCLUDE.value
|
return if (oldPrefValue) ExtendedNavigationView.Item.TriStateGroup.State.INCLUDE.value
|
||||||
@@ -129,7 +119,7 @@ object Migrations {
|
|||||||
remove("pref_filter_completed_key")
|
remove("pref_filter_completed_key")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (oldVersion < 54) {
|
if (oldVersion < 53) {
|
||||||
// Force MAL log out due to login flow change
|
// Force MAL log out due to login flow change
|
||||||
// v52: switched from scraping to WebView
|
// v52: switched from scraping to WebView
|
||||||
// v53: switched from WebView to OAuth
|
// v53: switched from WebView to OAuth
|
||||||
@@ -139,115 +129,6 @@ object Migrations {
|
|||||||
context.toast(R.string.myanimelist_relogin)
|
context.toast(R.string.myanimelist_relogin)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (oldVersion < 57) {
|
|
||||||
// Migrate DNS over HTTPS setting
|
|
||||||
val wasDohEnabled = prefs.getBoolean("enable_doh", false)
|
|
||||||
if (wasDohEnabled) {
|
|
||||||
prefs.edit {
|
|
||||||
putInt(PreferenceKeys.dohProvider, PREF_DOH_CLOUDFLARE)
|
|
||||||
remove("enable_doh")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (oldVersion < 59) {
|
|
||||||
// Reset rotation to Free after replacing Lock
|
|
||||||
if (prefs.contains("pref_rotation_type_key")) {
|
|
||||||
prefs.edit {
|
|
||||||
putInt("pref_rotation_type_key", 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Disable update check for Android 5.x users
|
|
||||||
if (BuildConfig.INCLUDE_UPDATER && Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) {
|
|
||||||
AppUpdateJob.cancelTask(context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (oldVersion < 60) {
|
|
||||||
// Re-enable update check that was prevously accidentally disabled for M
|
|
||||||
if (BuildConfig.INCLUDE_UPDATER && Build.VERSION.SDK_INT == Build.VERSION_CODES.M) {
|
|
||||||
AppUpdateJob.setupTask(context)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Migrate Rotation and Viewer values to default values for viewer_flags
|
|
||||||
val newOrientation = when (prefs.getInt("pref_rotation_type_key", 1)) {
|
|
||||||
1 -> OrientationType.FREE.flagValue
|
|
||||||
2 -> OrientationType.PORTRAIT.flagValue
|
|
||||||
3 -> OrientationType.LANDSCAPE.flagValue
|
|
||||||
4 -> OrientationType.LOCKED_PORTRAIT.flagValue
|
|
||||||
5 -> OrientationType.LOCKED_LANDSCAPE.flagValue
|
|
||||||
else -> OrientationType.FREE.flagValue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reading mode flag and prefValue is the same value
|
|
||||||
val newReadingMode = prefs.getInt("pref_default_viewer_key", 1)
|
|
||||||
|
|
||||||
prefs.edit {
|
|
||||||
putInt("pref_default_orientation_type_key", newOrientation)
|
|
||||||
remove("pref_rotation_type_key")
|
|
||||||
putInt("pref_default_reading_mode_key", newReadingMode)
|
|
||||||
remove("pref_default_viewer_key")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (oldVersion < 61) {
|
|
||||||
// Handle removed every 1 or 2 hour library updates
|
|
||||||
val updateInterval = preferences.libraryUpdateInterval().get()
|
|
||||||
if (updateInterval == 1 || updateInterval == 2) {
|
|
||||||
preferences.libraryUpdateInterval().set(3)
|
|
||||||
LibraryUpdateJob.setupTask(context, 3)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (oldVersion < 64) {
|
|
||||||
val oldSortingMode = prefs.getInt(PreferenceKeys.librarySortingMode, 0)
|
|
||||||
val oldSortingDirection = prefs.getBoolean(PreferenceKeys.librarySortingDirection, true)
|
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
val newSortingMode = when (oldSortingMode) {
|
|
||||||
LibrarySort.ALPHA -> SortModeSetting.ALPHABETICAL
|
|
||||||
LibrarySort.LAST_READ -> SortModeSetting.LAST_READ
|
|
||||||
LibrarySort.LAST_CHECKED -> SortModeSetting.LAST_CHECKED
|
|
||||||
LibrarySort.UNREAD -> SortModeSetting.UNREAD
|
|
||||||
LibrarySort.TOTAL -> SortModeSetting.TOTAL_CHAPTERS
|
|
||||||
LibrarySort.LATEST_CHAPTER -> SortModeSetting.LATEST_CHAPTER
|
|
||||||
LibrarySort.CHAPTER_FETCH_DATE -> SortModeSetting.DATE_FETCHED
|
|
||||||
LibrarySort.DATE_ADDED -> SortModeSetting.DATE_ADDED
|
|
||||||
else -> SortModeSetting.ALPHABETICAL
|
|
||||||
}
|
|
||||||
|
|
||||||
val newSortingDirection = when (oldSortingDirection) {
|
|
||||||
true -> SortDirectionSetting.ASCENDING
|
|
||||||
else -> SortDirectionSetting.DESCENDING
|
|
||||||
}
|
|
||||||
|
|
||||||
prefs.edit(commit = true) {
|
|
||||||
remove(PreferenceKeys.librarySortingMode)
|
|
||||||
remove(PreferenceKeys.librarySortingDirection)
|
|
||||||
}
|
|
||||||
|
|
||||||
prefs.edit {
|
|
||||||
putString(PreferenceKeys.librarySortingMode, newSortingMode.name)
|
|
||||||
putString(PreferenceKeys.librarySortingDirection, newSortingDirection.name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (oldVersion < 70) {
|
|
||||||
if (preferences.enabledLanguages().isSet()) {
|
|
||||||
preferences.enabledLanguages() += "all"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (oldVersion < 71) {
|
|
||||||
// Handle removed every 3, 4, 6, and 8 hour library updates
|
|
||||||
val updateInterval = preferences.libraryUpdateInterval().get()
|
|
||||||
if (updateInterval in listOf(3, 4, 6, 8)) {
|
|
||||||
preferences.libraryUpdateInterval().set(12)
|
|
||||||
LibraryUpdateJob.setupTask(context, 12)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (oldVersion < 72) {
|
|
||||||
val oldUpdateOngoingOnly = prefs.getBoolean("pref_update_only_non_completed_key", true)
|
|
||||||
if (!oldUpdateOngoingOnly) {
|
|
||||||
preferences.libraryUpdateMangaRestriction() -= MANGA_ONGOING
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package eu.kanade.tachiyomi.annotations
|
||||||
|
|
||||||
|
@Retention(AnnotationRetention.RUNTIME)
|
||||||
|
@Target(AnnotationTarget.CLASS)
|
||||||
|
annotation class Nsfw
|
||||||
@@ -6,7 +6,6 @@ import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
|||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.database.models.toMangaInfo
|
import eu.kanade.tachiyomi.data.database.models.toMangaInfo
|
||||||
import eu.kanade.tachiyomi.data.library.CustomMangaManager
|
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
@@ -24,11 +23,7 @@ abstract class AbstractBackupManager(protected val context: Context) {
|
|||||||
internal val trackManager: TrackManager by injectLazy()
|
internal val trackManager: TrackManager by injectLazy()
|
||||||
protected val preferences: PreferencesHelper by injectLazy()
|
protected val preferences: PreferencesHelper by injectLazy()
|
||||||
|
|
||||||
// SY -->
|
abstract fun createBackup(uri: Uri, flags: Int, isJob: Boolean): String?
|
||||||
protected val customMangaManager: CustomMangaManager by injectLazy()
|
|
||||||
// SY <--
|
|
||||||
|
|
||||||
abstract fun createBackup(uri: Uri, flags: Int, isJob: Boolean): String
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns manga
|
* Returns manga
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
|||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
import eu.kanade.tachiyomi.data.library.CustomMangaManager
|
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.util.chapter.NoChaptersException
|
import eu.kanade.tachiyomi.util.chapter.NoChaptersException
|
||||||
@@ -25,10 +24,6 @@ abstract class AbstractBackupRestore<T : AbstractBackupManager>(protected val co
|
|||||||
protected val db: DatabaseHelper by injectLazy()
|
protected val db: DatabaseHelper by injectLazy()
|
||||||
protected val trackManager: TrackManager by injectLazy()
|
protected val trackManager: TrackManager by injectLazy()
|
||||||
|
|
||||||
// SY -->
|
|
||||||
protected val customMangaManager: CustomMangaManager by injectLazy()
|
|
||||||
// SY <--
|
|
||||||
|
|
||||||
var job: Job? = null
|
var job: Job? = null
|
||||||
|
|
||||||
protected lateinit var backupManager: T
|
protected lateinit var backupManager: T
|
||||||
|
|||||||
@@ -14,5 +14,3 @@ abstract class AbstractBackupRestoreValidator {
|
|||||||
|
|
||||||
data class Results(val missingSources: List<String>, val missingTrackers: List<String>)
|
data class Results(val missingSources: List<String>, val missingTrackers: List<String>)
|
||||||
}
|
}
|
||||||
|
|
||||||
class ValidatorParseException(e: Exception) : RuntimeException(e)
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ object BackupConst {
|
|||||||
const val EXTRA_URI = "$ID.$NAME.EXTRA_URI"
|
const val EXTRA_URI = "$ID.$NAME.EXTRA_URI"
|
||||||
const val EXTRA_FLAGS = "$ID.$NAME.EXTRA_FLAGS"
|
const val EXTRA_FLAGS = "$ID.$NAME.EXTRA_FLAGS"
|
||||||
const val EXTRA_MODE = "$ID.$NAME.EXTRA_MODE"
|
const val EXTRA_MODE = "$ID.$NAME.EXTRA_MODE"
|
||||||
|
const val EXTRA_TYPE = "$ID.$NAME.EXTRA_TYPE"
|
||||||
|
|
||||||
const val BACKUP_TYPE_LEGACY = 0
|
const val BACKUP_TYPE_LEGACY = 0
|
||||||
const val BACKUP_TYPE_FULL = 1
|
const val BACKUP_TYPE_FULL = 1
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import androidx.core.content.ContextCompat
|
|||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import com.hippo.unifile.UniFile
|
import com.hippo.unifile.UniFile
|
||||||
import eu.kanade.tachiyomi.data.backup.full.FullBackupManager
|
import eu.kanade.tachiyomi.data.backup.full.FullBackupManager
|
||||||
|
import eu.kanade.tachiyomi.data.backup.legacy.LegacyBackupManager
|
||||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
import eu.kanade.tachiyomi.util.system.acquireWakeLock
|
import eu.kanade.tachiyomi.util.system.acquireWakeLock
|
||||||
import eu.kanade.tachiyomi.util.system.isServiceRunning
|
import eu.kanade.tachiyomi.util.system.isServiceRunning
|
||||||
@@ -29,14 +30,7 @@ class BackupCreateService : Service() {
|
|||||||
internal const val BACKUP_HISTORY_MASK = 0x4
|
internal const val BACKUP_HISTORY_MASK = 0x4
|
||||||
internal const val BACKUP_TRACK = 0x8
|
internal const val BACKUP_TRACK = 0x8
|
||||||
internal const val BACKUP_TRACK_MASK = 0x8
|
internal const val BACKUP_TRACK_MASK = 0x8
|
||||||
|
internal const val BACKUP_ALL = 0xF
|
||||||
// SY -->
|
|
||||||
internal const val BACKUP_CUSTOM_INFO = 0x10
|
|
||||||
internal const val BACKUP_CUSTOM_INFO_MASK = 0x10
|
|
||||||
internal const val BACKUP_READ_MANGA = 0x20
|
|
||||||
internal const val BACKUP_READ_MANGA_MASK = 0x20
|
|
||||||
internal const val BACKUP_ALL = 0x3F
|
|
||||||
// SY <--
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the status of the service.
|
* Returns the status of the service.
|
||||||
@@ -54,11 +48,12 @@ class BackupCreateService : Service() {
|
|||||||
* @param uri path of Uri
|
* @param uri path of Uri
|
||||||
* @param flags determines what to backup
|
* @param flags determines what to backup
|
||||||
*/
|
*/
|
||||||
fun start(context: Context, uri: Uri, flags: Int) {
|
fun start(context: Context, uri: Uri, flags: Int, type: Int) {
|
||||||
if (!isRunning(context)) {
|
if (!isRunning(context)) {
|
||||||
val intent = Intent(context, BackupCreateService::class.java).apply {
|
val intent = Intent(context, BackupCreateService::class.java).apply {
|
||||||
putExtra(BackupConst.EXTRA_URI, uri)
|
putExtra(BackupConst.EXTRA_URI, uri)
|
||||||
putExtra(BackupConst.EXTRA_FLAGS, flags)
|
putExtra(BackupConst.EXTRA_FLAGS, flags)
|
||||||
|
putExtra(BackupConst.EXTRA_TYPE, type)
|
||||||
}
|
}
|
||||||
ContextCompat.startForegroundService(context, intent)
|
ContextCompat.startForegroundService(context, intent)
|
||||||
}
|
}
|
||||||
@@ -106,11 +101,17 @@ class BackupCreateService : Service() {
|
|||||||
if (intent == null) return START_NOT_STICKY
|
if (intent == null) return START_NOT_STICKY
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val uri = intent.getParcelableExtra<Uri>(BackupConst.EXTRA_URI)!!
|
val uri = intent.getParcelableExtra<Uri>(BackupConst.EXTRA_URI)
|
||||||
val backupFlags = intent.getIntExtra(BackupConst.EXTRA_FLAGS, 0)
|
val backupFlags = intent.getIntExtra(BackupConst.EXTRA_FLAGS, 0)
|
||||||
val backupFileUri = FullBackupManager(this).createBackup(uri, backupFlags, false)?.toUri()
|
val backupType = intent.getIntExtra(BackupConst.EXTRA_TYPE, BackupConst.BACKUP_TYPE_LEGACY)
|
||||||
|
val backupManager = when (backupType) {
|
||||||
|
BackupConst.BACKUP_TYPE_FULL -> FullBackupManager(this)
|
||||||
|
else -> LegacyBackupManager(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
val backupFileUri = backupManager.createBackup(uri, backupFlags, false)?.toUri()
|
||||||
val unifile = UniFile.fromUri(this, backupFileUri)
|
val unifile = UniFile.fromUri(this, backupFileUri)
|
||||||
notifier.showBackupComplete(unifile)
|
notifier.showBackupComplete(unifile, backupType == BackupConst.BACKUP_TYPE_LEGACY)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
notifier.showBackupError(e.message)
|
notifier.showBackupError(e.message)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,9 +8,8 @@ import androidx.work.WorkManager
|
|||||||
import androidx.work.Worker
|
import androidx.work.Worker
|
||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
import eu.kanade.tachiyomi.data.backup.full.FullBackupManager
|
import eu.kanade.tachiyomi.data.backup.full.FullBackupManager
|
||||||
|
import eu.kanade.tachiyomi.data.backup.legacy.LegacyBackupManager
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.util.system.logcat
|
|
||||||
import logcat.LogPriority
|
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
@@ -24,9 +23,11 @@ class BackupCreatorJob(private val context: Context, workerParams: WorkerParamet
|
|||||||
val flags = BackupCreateService.BACKUP_ALL
|
val flags = BackupCreateService.BACKUP_ALL
|
||||||
return try {
|
return try {
|
||||||
FullBackupManager(context).createBackup(uri, flags, true)
|
FullBackupManager(context).createBackup(uri, flags, true)
|
||||||
|
if (preferences.createLegacyBackup().get()) {
|
||||||
|
LegacyBackupManager(context).createBackup(uri, flags, true)
|
||||||
|
}
|
||||||
Result.success()
|
Result.success()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logcat(LogPriority.ERROR, e)
|
|
||||||
Result.failure()
|
Result.failure()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ class BackupNotifier(private val context: Context) {
|
|||||||
setSmallIcon(R.drawable.ic_tachi)
|
setSmallIcon(R.drawable.ic_tachi)
|
||||||
setAutoCancel(false)
|
setAutoCancel(false)
|
||||||
setOngoing(true)
|
setOngoing(true)
|
||||||
setOnlyAlertOnce(true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private val completeNotificationBuilder = context.notificationBuilder(Notifications.CHANNEL_BACKUP_RESTORE_COMPLETE) {
|
private val completeNotificationBuilder = context.notificationBuilder(Notifications.CHANNEL_BACKUP_RESTORE_COMPLETE) {
|
||||||
@@ -42,6 +41,7 @@ class BackupNotifier(private val context: Context) {
|
|||||||
setContentTitle(context.getString(R.string.creating_backup))
|
setContentTitle(context.getString(R.string.creating_backup))
|
||||||
|
|
||||||
setProgress(0, 0, true)
|
setProgress(0, 0, true)
|
||||||
|
setOnlyAlertOnce(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
builder.show(Notifications.ID_BACKUP_PROGRESS)
|
builder.show(Notifications.ID_BACKUP_PROGRESS)
|
||||||
@@ -60,7 +60,7 @@ class BackupNotifier(private val context: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showBackupComplete(unifile: UniFile) {
|
fun showBackupComplete(unifile: UniFile, isLegacyFormat: Boolean) {
|
||||||
context.notificationManager.cancel(Notifications.ID_BACKUP_PROGRESS)
|
context.notificationManager.cancel(Notifications.ID_BACKUP_PROGRESS)
|
||||||
|
|
||||||
with(completeNotificationBuilder) {
|
with(completeNotificationBuilder) {
|
||||||
@@ -73,7 +73,7 @@ class BackupNotifier(private val context: Context) {
|
|||||||
addAction(
|
addAction(
|
||||||
R.drawable.ic_share_24dp,
|
R.drawable.ic_share_24dp,
|
||||||
context.getString(R.string.action_share),
|
context.getString(R.string.action_share),
|
||||||
NotificationReceiver.shareBackupPendingBroadcast(context, unifile.uri, Notifications.ID_BACKUP_COMPLETE)
|
NotificationReceiver.shareBackupPendingBroadcast(context, unifile.uri, isLegacyFormat, Notifications.ID_BACKUP_COMPLETE)
|
||||||
)
|
)
|
||||||
|
|
||||||
show(Notifications.ID_BACKUP_COMPLETE)
|
show(Notifications.ID_BACKUP_COMPLETE)
|
||||||
@@ -139,12 +139,10 @@ class BackupNotifier(private val context: Context) {
|
|||||||
val destFile = File(path, file)
|
val destFile = File(path, file)
|
||||||
val uri = destFile.getUriCompat(context)
|
val uri = destFile.getUriCompat(context)
|
||||||
|
|
||||||
val errorLogIntent = NotificationReceiver.openErrorLogPendingActivity(context, uri)
|
|
||||||
setContentIntent(errorLogIntent)
|
|
||||||
addAction(
|
addAction(
|
||||||
R.drawable.ic_folder_24dp,
|
R.drawable.ic_folder_24dp,
|
||||||
context.getString(R.string.action_show_errors),
|
context.getString(R.string.action_open_log),
|
||||||
errorLogIntent,
|
NotificationReceiver.openErrorLogPendingActivity(context, uri)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,14 +13,13 @@ import eu.kanade.tachiyomi.data.backup.legacy.LegacyBackupRestore
|
|||||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
import eu.kanade.tachiyomi.util.system.acquireWakeLock
|
import eu.kanade.tachiyomi.util.system.acquireWakeLock
|
||||||
import eu.kanade.tachiyomi.util.system.isServiceRunning
|
import eu.kanade.tachiyomi.util.system.isServiceRunning
|
||||||
import eu.kanade.tachiyomi.util.system.logcat
|
|
||||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import logcat.LogPriority
|
import timber.log.Timber
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Restores backup.
|
* Restores backup.
|
||||||
@@ -44,11 +43,12 @@ class BackupRestoreService : Service() {
|
|||||||
* @param context context of application
|
* @param context context of application
|
||||||
* @param uri path of Uri
|
* @param uri path of Uri
|
||||||
*/
|
*/
|
||||||
fun start(context: Context, uri: Uri, mode: Int) {
|
fun start(context: Context, uri: Uri, mode: Int, online: Boolean?) {
|
||||||
if (!isRunning(context)) {
|
if (!isRunning(context)) {
|
||||||
val intent = Intent(context, BackupRestoreService::class.java).apply {
|
val intent = Intent(context, BackupRestoreService::class.java).apply {
|
||||||
putExtra(BackupConst.EXTRA_URI, uri)
|
putExtra(BackupConst.EXTRA_URI, uri)
|
||||||
putExtra(BackupConst.EXTRA_MODE, mode)
|
putExtra(BackupConst.EXTRA_MODE, mode)
|
||||||
|
online?.let { putExtra(BackupConst.EXTRA_TYPE, it) }
|
||||||
}
|
}
|
||||||
ContextCompat.startForegroundService(context, intent)
|
ContextCompat.startForegroundService(context, intent)
|
||||||
}
|
}
|
||||||
@@ -97,7 +97,7 @@ class BackupRestoreService : Service() {
|
|||||||
|
|
||||||
private fun destroyJob() {
|
private fun destroyJob() {
|
||||||
backupRestore?.job?.cancel()
|
backupRestore?.job?.cancel()
|
||||||
ioScope.cancel()
|
ioScope?.cancel()
|
||||||
if (wakeLock.isHeld) {
|
if (wakeLock.isHeld) {
|
||||||
wakeLock.release()
|
wakeLock.release()
|
||||||
}
|
}
|
||||||
@@ -119,17 +119,18 @@ class BackupRestoreService : Service() {
|
|||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
val uri = intent?.getParcelableExtra<Uri>(BackupConst.EXTRA_URI) ?: return START_NOT_STICKY
|
val uri = intent?.getParcelableExtra<Uri>(BackupConst.EXTRA_URI) ?: return START_NOT_STICKY
|
||||||
val mode = intent.getIntExtra(BackupConst.EXTRA_MODE, BackupConst.BACKUP_TYPE_FULL)
|
val mode = intent.getIntExtra(BackupConst.EXTRA_MODE, BackupConst.BACKUP_TYPE_FULL)
|
||||||
|
val online = intent.getBooleanExtra(BackupConst.EXTRA_TYPE, true)
|
||||||
|
|
||||||
// Cancel any previous job if needed.
|
// Cancel any previous job if needed.
|
||||||
backupRestore?.job?.cancel()
|
backupRestore?.job?.cancel()
|
||||||
|
|
||||||
backupRestore = when (mode) {
|
backupRestore = when (mode) {
|
||||||
BackupConst.BACKUP_TYPE_FULL -> FullBackupRestore(this, notifier)
|
BackupConst.BACKUP_TYPE_FULL -> FullBackupRestore(this, notifier, online)
|
||||||
else -> LegacyBackupRestore(this, notifier)
|
else -> LegacyBackupRestore(this, notifier)
|
||||||
}
|
}
|
||||||
|
|
||||||
val handler = CoroutineExceptionHandler { _, exception ->
|
val handler = CoroutineExceptionHandler { _, exception ->
|
||||||
logcat(LogPriority.ERROR, exception)
|
Timber.e(exception)
|
||||||
backupRestore?.writeErrorLog()
|
backupRestore?.writeErrorLog()
|
||||||
|
|
||||||
notifier.showRestoreError(exception.message)
|
notifier.showRestoreError(exception.message)
|
||||||
|
|||||||
@@ -8,12 +8,8 @@ import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CATE
|
|||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CATEGORY_MASK
|
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CATEGORY_MASK
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CHAPTER
|
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CHAPTER
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CHAPTER_MASK
|
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CHAPTER_MASK
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CUSTOM_INFO
|
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CUSTOM_INFO_MASK
|
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_HISTORY
|
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_HISTORY
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_HISTORY_MASK
|
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_HISTORY_MASK
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_READ_MANGA
|
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_READ_MANGA_MASK
|
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_TRACK
|
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_TRACK
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_TRACK_MASK
|
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_TRACK_MASK
|
||||||
import eu.kanade.tachiyomi.data.backup.full.models.Backup
|
import eu.kanade.tachiyomi.data.backup.full.models.Backup
|
||||||
@@ -33,9 +29,11 @@ import eu.kanade.tachiyomi.data.database.models.History
|
|||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaCategory
|
import eu.kanade.tachiyomi.data.database.models.MangaCategory
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.toMangaInfo
|
||||||
|
import eu.kanade.tachiyomi.source.Source
|
||||||
|
import eu.kanade.tachiyomi.source.model.toSManga
|
||||||
import eu.kanade.tachiyomi.source.online.MetadataSource
|
import eu.kanade.tachiyomi.source.online.MetadataSource
|
||||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
import eu.kanade.tachiyomi.source.online.all.MergedSource
|
||||||
import eu.kanade.tachiyomi.util.system.logcat
|
|
||||||
import exh.metadata.metadata.base.getFlatMetadataForManga
|
import exh.metadata.metadata.base.getFlatMetadataForManga
|
||||||
import exh.metadata.metadata.base.insertFlatMetadataAsync
|
import exh.metadata.metadata.base.insertFlatMetadataAsync
|
||||||
import exh.savedsearches.JsonSavedSearch
|
import exh.savedsearches.JsonSavedSearch
|
||||||
@@ -46,10 +44,10 @@ import kotlinx.serialization.decodeFromString
|
|||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.protobuf.ProtoBuf
|
import kotlinx.serialization.protobuf.ProtoBuf
|
||||||
import logcat.LogPriority
|
|
||||||
import okio.buffer
|
import okio.buffer
|
||||||
import okio.gzip
|
import okio.gzip
|
||||||
import okio.sink
|
import okio.sink
|
||||||
|
import timber.log.Timber
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
|
|
||||||
class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
||||||
@@ -62,29 +60,23 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
|||||||
* @param uri path of Uri
|
* @param uri path of Uri
|
||||||
* @param isJob backup called from job
|
* @param isJob backup called from job
|
||||||
*/
|
*/
|
||||||
override fun createBackup(uri: Uri, flags: Int, isJob: Boolean): String {
|
override fun createBackup(uri: Uri, flags: Int, isJob: Boolean): String? {
|
||||||
// Create root object
|
// Create root object
|
||||||
var backup: Backup? = null
|
var backup: Backup? = null
|
||||||
|
|
||||||
databaseHelper.inTransaction {
|
databaseHelper.inTransaction {
|
||||||
val databaseManga = getFavoriteManga() /* SY --> */ + if (flags and BACKUP_READ_MANGA_MASK == BACKUP_READ_MANGA) {
|
val databaseManga = getFavoriteManga() /* SY --> */ + getReadManga() + getMergedManga().filterNot { it.source == MERGED_SOURCE_ID } /* SY <-- */
|
||||||
getReadManga()
|
|
||||||
} else {
|
|
||||||
emptyList()
|
|
||||||
} + getMergedManga() /* SY <-- */
|
|
||||||
|
|
||||||
backup = Backup(
|
backup = Backup(
|
||||||
backupManga(databaseManga, flags),
|
backupManga(databaseManga, flags),
|
||||||
backupCategories(),
|
backupCategories(),
|
||||||
emptyList(),
|
|
||||||
backupExtensionInfo(databaseManga),
|
backupExtensionInfo(databaseManga),
|
||||||
backupSavedSearches()
|
backupSavedSearches()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
var file: UniFile? = null
|
|
||||||
try {
|
try {
|
||||||
file = (
|
val file: UniFile = (
|
||||||
if (isJob) {
|
if (isJob) {
|
||||||
// Get dir of file and create
|
// Get dir of file and create
|
||||||
var dir = UniFile.fromUri(context, uri)
|
var dir = UniFile.fromUri(context, uri)
|
||||||
@@ -109,15 +101,9 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
|||||||
|
|
||||||
val byteArray = parser.encodeToByteArray(BackupSerializer, backup!!)
|
val byteArray = parser.encodeToByteArray(BackupSerializer, backup!!)
|
||||||
file.openOutputStream().sink().gzip().buffer().use { it.write(byteArray) }
|
file.openOutputStream().sink().gzip().buffer().use { it.write(byteArray) }
|
||||||
val fileUri = file.uri
|
return file.uri.toString()
|
||||||
|
|
||||||
// Make sure it's a valid backup file
|
|
||||||
FullBackupRestoreValidator().validate(context, fileUri)
|
|
||||||
|
|
||||||
return fileUri.toString()
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logcat(LogPriority.ERROR, e)
|
Timber.e(e)
|
||||||
file?.delete()
|
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -156,8 +142,8 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
|||||||
* @return list of [BackupSavedSearch] to be backed up
|
* @return list of [BackupSavedSearch] to be backed up
|
||||||
*/
|
*/
|
||||||
private fun backupSavedSearches(): List<BackupSavedSearch> {
|
private fun backupSavedSearches(): List<BackupSavedSearch> {
|
||||||
return preferences.savedSearches().get().mapNotNull {
|
return preferences.savedSearches().get().map {
|
||||||
val sourceId = it.substringBefore(':').toLongOrNull() ?: return@mapNotNull null
|
val sourceId = it.substringBefore(':').toLong()
|
||||||
val content = Json.decodeFromString<JsonSavedSearch>(it.substringAfter(':'))
|
val content = Json.decodeFromString<JsonSavedSearch>(it.substringAfter(':'))
|
||||||
BackupSavedSearch(
|
BackupSavedSearch(
|
||||||
content.name,
|
content.name,
|
||||||
@@ -178,7 +164,7 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
|||||||
*/
|
*/
|
||||||
private fun backupMangaObject(manga: Manga, options: Int): BackupManga {
|
private fun backupMangaObject(manga: Manga, options: Int): BackupManga {
|
||||||
// Entry for this manga
|
// Entry for this manga
|
||||||
val mangaObject = BackupManga.copyFrom(manga /* SY --> */, if (options and BACKUP_CUSTOM_INFO_MASK == BACKUP_CUSTOM_INFO) customMangaManager else null /* SY <-- */)
|
val mangaObject = BackupManga.copyFrom(manga)
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
if (manga.source == MERGED_SOURCE_ID) {
|
if (manga.source == MERGED_SOURCE_ID) {
|
||||||
@@ -189,8 +175,8 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val source = sourceManager.get(manga.source)?.getMainSource<MetadataSource<*, *>>()
|
val source = sourceManager.get(manga.source)?.getMainSource()
|
||||||
if (source != null) {
|
if (source is MetadataSource<*, *>) {
|
||||||
manga.id?.let { mangaId ->
|
manga.id?.let { mangaId ->
|
||||||
databaseHelper.getFlatMetadataForManga(mangaId).executeAsBlocking()?.let { flatMetadata ->
|
databaseHelper.getFlatMetadataForManga(mangaId).executeAsBlocking()?.let { flatMetadata ->
|
||||||
mangaObject.flatMetadata = BackupFlatMetadata.copyFrom(flatMetadata)
|
mangaObject.flatMetadata = BackupFlatMetadata.copyFrom(flatMetadata)
|
||||||
@@ -251,13 +237,24 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
|||||||
/**
|
/**
|
||||||
* Fetches manga information
|
* Fetches manga information
|
||||||
*
|
*
|
||||||
|
* @param source source of manga
|
||||||
* @param manga manga that needs updating
|
* @param manga manga that needs updating
|
||||||
* @return Updated manga info.
|
* @return Updated manga info.
|
||||||
*/
|
*/
|
||||||
fun restoreManga(manga: Manga): Manga {
|
suspend fun restoreMangaFetch(source: Source?, manga: Manga, online: Boolean): Manga {
|
||||||
return manga.also {
|
return if (online && source != null /* SY --> */ && source !is MergedSource /* SY <-- */) {
|
||||||
it.initialized = it.description != null
|
val networkManga = source.getMangaDetails(manga.toMangaInfo())
|
||||||
it.id = insertManga(it)
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -366,26 +363,29 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
|||||||
val trackToUpdate = mutableListOf<Track>()
|
val trackToUpdate = mutableListOf<Track>()
|
||||||
|
|
||||||
tracks.forEach { track ->
|
tracks.forEach { track ->
|
||||||
var isInDatabase = false
|
val service = trackManager.getService(track.sync_id)
|
||||||
for (dbTrack in dbTracks) {
|
if (service != null && service.isLogged) {
|
||||||
if (track.sync_id == dbTrack.sync_id) {
|
var isInDatabase = false
|
||||||
// The sync is already in the db, only update its fields
|
for (dbTrack in dbTracks) {
|
||||||
if (track.media_id != dbTrack.media_id) {
|
if (track.sync_id == dbTrack.sync_id) {
|
||||||
dbTrack.media_id = track.media_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 (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) {
|
||||||
if (!isInDatabase) {
|
// Insert new sync. Let the db assign the id
|
||||||
// Insert new sync. Let the db assign the id
|
track.id = null
|
||||||
track.id = null
|
trackToUpdate.add(track)
|
||||||
trackToUpdate.add(track)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Update database
|
// Update database
|
||||||
@@ -394,7 +394,47 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun restoreChaptersForManga(manga: Manga, chapters: List<Chapter>) {
|
/**
|
||||||
|
* Restore the chapters for manga if chapters already in database
|
||||||
|
*
|
||||||
|
* @param manga manga of chapters
|
||||||
|
* @param chapters list containing chapters that get restored
|
||||||
|
* @return boolean answering if chapter fetch is not needed
|
||||||
|
*/
|
||||||
|
internal fun restoreChaptersForManga(manga: Manga, chapters: List<Chapter>): Boolean {
|
||||||
|
val dbChapters = databaseHelper.getChapters(manga).executeAsBlocking()
|
||||||
|
|
||||||
|
// Return if fetch is needed
|
||||||
|
if (dbChapters.isEmpty() || dbChapters.size < chapters.size) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
chapters.forEach { chapter ->
|
||||||
|
val dbChapter = dbChapters.find { it.url == chapter.url }
|
||||||
|
if (dbChapter != null) {
|
||||||
|
chapter.id = dbChapter.id
|
||||||
|
chapter.copyFrom(dbChapter)
|
||||||
|
if (dbChapter.read && !chapter.read) {
|
||||||
|
chapter.read = dbChapter.read
|
||||||
|
chapter.last_page_read = dbChapter.last_page_read
|
||||||
|
} else if (chapter.last_page_read == 0 && dbChapter.last_page_read != 0) {
|
||||||
|
chapter.last_page_read = dbChapter.last_page_read
|
||||||
|
}
|
||||||
|
if (!chapter.bookmark && dbChapter.bookmark) {
|
||||||
|
chapter.bookmark = dbChapter.bookmark
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
chapter.manga_id = manga.id
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter the chapters that couldn't be found.
|
||||||
|
updateChapters(chapters.filter { it.id != null })
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun restoreChaptersForMangaOffline(manga: Manga, chapters: List<Chapter>) {
|
||||||
val dbChapters = databaseHelper.getChapters(manga).executeAsBlocking()
|
val dbChapters = databaseHelper.getChapters(manga).executeAsBlocking()
|
||||||
|
|
||||||
chapters.forEach { chapter ->
|
chapters.forEach { chapter ->
|
||||||
@@ -423,13 +463,9 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
|||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
internal fun restoreSavedSearches(backupSavedSearches: List<BackupSavedSearch>) {
|
internal fun restoreSavedSearches(backupSavedSearches: List<BackupSavedSearch>) {
|
||||||
val currentSavedSearches = preferences.savedSearches().get().mapNotNull {
|
val currentSavedSearches = preferences.savedSearches().get().map {
|
||||||
val sourceId = it.substringBefore(':').toLongOrNull() ?: return@mapNotNull null
|
val sourceId = it.substringBefore(':').toLong()
|
||||||
val content = try {
|
val content = Json.decodeFromString<JsonSavedSearch>(it.substringAfter(':'))
|
||||||
Json.decodeFromString<JsonSavedSearch>(it.substringAfter(':'))
|
|
||||||
} catch (e: Exception) {
|
|
||||||
return@mapNotNull null
|
|
||||||
}
|
|
||||||
BackupSavedSearch(
|
BackupSavedSearch(
|
||||||
content.name,
|
content.name,
|
||||||
content.query,
|
content.query,
|
||||||
@@ -438,19 +474,22 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val newSavedSearches = backupSavedSearches.filter { backupSavedSearch ->
|
preferences.savedSearches()
|
||||||
currentSavedSearches.none { it.name == backupSavedSearch.name && it.source == backupSavedSearch.source }
|
.set(
|
||||||
}.map {
|
(
|
||||||
"${it.source}:" + Json.encodeToString(
|
backupSavedSearches.filter { backupSavedSearch -> currentSavedSearches.none { it.name == backupSavedSearch.name && it.source == backupSavedSearch.source } }
|
||||||
JsonSavedSearch(
|
.map {
|
||||||
it.name,
|
"${it.source}:" + Json.encodeToString(
|
||||||
it.query,
|
JsonSavedSearch(
|
||||||
Json.decodeFromString(it.filterList)
|
it.name,
|
||||||
)
|
it.query,
|
||||||
|
Json.decodeFromString(it.filterList)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} + preferences.savedSearches().get()
|
||||||
|
)
|
||||||
|
.toSet()
|
||||||
)
|
)
|
||||||
}.toSet()
|
|
||||||
|
|
||||||
preferences.savedSearches().set(newSavedSearches + preferences.savedSearches().get())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -488,9 +527,8 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun restoreFlatMetadata(manga: Manga, backupFlatMetadata: BackupFlatMetadata) {
|
internal suspend fun restoreFlatMetadata(manga: Manga, backupFlatMetadata: BackupFlatMetadata) {
|
||||||
val mangaId = manga.id ?: return
|
manga.id?.let { mangaId ->
|
||||||
launchIO {
|
|
||||||
databaseHelper.getFlatMetadataForManga(mangaId).executeOnIO().let {
|
databaseHelper.getFlatMetadataForManga(mangaId).executeOnIO().let {
|
||||||
if (it == null) {
|
if (it == null) {
|
||||||
val flatMetadata = backupFlatMetadata.getFlatMetadata(mangaId)
|
val flatMetadata = backupFlatMetadata.getFlatMetadata(mangaId)
|
||||||
|
|||||||
@@ -12,11 +12,11 @@ import eu.kanade.tachiyomi.data.backup.full.models.BackupManga
|
|||||||
import eu.kanade.tachiyomi.data.backup.full.models.BackupMergedMangaReference
|
import eu.kanade.tachiyomi.data.backup.full.models.BackupMergedMangaReference
|
||||||
import eu.kanade.tachiyomi.data.backup.full.models.BackupSavedSearch
|
import eu.kanade.tachiyomi.data.backup.full.models.BackupSavedSearch
|
||||||
import eu.kanade.tachiyomi.data.backup.full.models.BackupSerializer
|
import eu.kanade.tachiyomi.data.backup.full.models.BackupSerializer
|
||||||
import eu.kanade.tachiyomi.data.backup.full.models.BackupSource
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
import eu.kanade.tachiyomi.data.library.CustomMangaManager
|
import eu.kanade.tachiyomi.source.Source
|
||||||
|
import eu.kanade.tachiyomi.source.online.all.MergedSource
|
||||||
import exh.EXHMigrations
|
import exh.EXHMigrations
|
||||||
import exh.source.MERGED_SOURCE_ID
|
import exh.source.MERGED_SOURCE_ID
|
||||||
import okio.buffer
|
import okio.buffer
|
||||||
@@ -24,7 +24,7 @@ import okio.gzip
|
|||||||
import okio.source
|
import okio.source
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
|
||||||
class FullBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBackupRestore<FullBackupManager>(context, notifier) {
|
class FullBackupRestore(context: Context, notifier: BackupNotifier, private val online: Boolean) : AbstractBackupRestore<FullBackupManager>(context, notifier) {
|
||||||
|
|
||||||
override suspend fun performRestore(uri: Uri): Boolean {
|
override suspend fun performRestore(uri: Uri): Boolean {
|
||||||
// SY -->
|
// SY -->
|
||||||
@@ -49,8 +49,7 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBa
|
|||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
// Store source mapping for error messages
|
// Store source mapping for error messages
|
||||||
var backupMaps = backup.backupBrokenSources.map { BackupSource(it.name, it.sourceId) } + backup.backupSources
|
sourceMapping = backup.backupSources.map { it.sourceId to it.name }.toMap()
|
||||||
sourceMapping = backupMaps.map { it.sourceId to it.name }.toMap()
|
|
||||||
|
|
||||||
// Restore individual manga, sort by merged source so that merged source manga go last and merged references get the proper ids
|
// Restore individual manga, sort by merged source so that merged source manga go last and merged references get the proper ids
|
||||||
backup.backupManga /* SY --> */.sortedBy { it.source == MERGED_SOURCE_ID } /* SY <-- */.forEach {
|
backup.backupManga /* SY --> */.sortedBy { it.source == MERGED_SOURCE_ID } /* SY <-- */.forEach {
|
||||||
@@ -58,11 +57,9 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBa
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
restoreManga(it, backup.backupCategories)
|
restoreManga(it, backup.backupCategories, online)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: optionally trigger online library + tracker update
|
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,26 +81,31 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBa
|
|||||||
}
|
}
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
private fun restoreManga(backupManga: BackupManga, backupCategories: List<BackupCategory>) {
|
private suspend fun restoreManga(backupManga: BackupManga, backupCategories: List<BackupCategory>, online: Boolean) {
|
||||||
val manga = backupManga.getMangaImpl()
|
var manga = backupManga.getMangaImpl()
|
||||||
val chapters = backupManga.getChaptersImpl()
|
val chapters = backupManga.getChaptersImpl()
|
||||||
val categories = backupManga.categories
|
val categories = backupManga.categories
|
||||||
val history = backupManga.brokenHistory.map { BackupHistory(it.url, it.lastRead) } + backupManga.history
|
val history = backupManga.history
|
||||||
val tracks = backupManga.getTrackingImpl()
|
val tracks = backupManga.getTrackingImpl()
|
||||||
// SY -->
|
// SY -->
|
||||||
val mergedMangaReferences = backupManga.mergedMangaReferences
|
val mergedMangaReferences = backupManga.mergedMangaReferences
|
||||||
val flatMetadata = backupManga.flatMetadata
|
val flatMetadata = backupManga.flatMetadata
|
||||||
val customManga = backupManga.getCustomMangaInfo()
|
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
EXHMigrations.migrateBackupEntry(manga)
|
manga = EXHMigrations.migrateBackupEntry(manga)
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
|
val source = backupManager.sourceManager.get(manga.source)
|
||||||
|
val sourceName = sourceMapping[manga.source] ?: manga.source.toString()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
restoreMangaData(manga, chapters, categories, history, tracks, backupCategories/* SY --> */, mergedMangaReferences, flatMetadata, customManga/* SY <-- */)
|
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)}")
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
val sourceName = sourceMapping[manga.source] ?: manga.source.toString()
|
|
||||||
errors.add(Date() to "${manga.title} [$sourceName]: ${e.message}")
|
errors.add(Date() to "${manga.title} [$sourceName]: ${e.message}")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,35 +117,35 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBa
|
|||||||
* Returns a manga restore observable
|
* Returns a manga restore observable
|
||||||
*
|
*
|
||||||
* @param manga manga data from json
|
* @param manga manga data from json
|
||||||
|
* @param source source to get manga data from
|
||||||
* @param chapters chapters data from json
|
* @param chapters chapters data from json
|
||||||
* @param categories categories data from json
|
* @param categories categories data from json
|
||||||
* @param history history data from json
|
* @param history history data from json
|
||||||
* @param tracks tracking data from json
|
* @param tracks tracking data from json
|
||||||
*/
|
*/
|
||||||
private fun restoreMangaData(
|
private suspend fun restoreMangaData(
|
||||||
manga: Manga,
|
manga: Manga,
|
||||||
|
source: Source?,
|
||||||
chapters: List<Chapter>,
|
chapters: List<Chapter>,
|
||||||
categories: List<Int>,
|
categories: List<Int>,
|
||||||
history: List<BackupHistory>,
|
history: List<BackupHistory>,
|
||||||
tracks: List<Track>,
|
tracks: List<Track>,
|
||||||
backupCategories: List<BackupCategory>,
|
backupCategories: List<BackupCategory>,
|
||||||
// SY -->
|
|
||||||
mergedMangaReferences: List<BackupMergedMangaReference>,
|
mergedMangaReferences: List<BackupMergedMangaReference>,
|
||||||
flatMetadata: BackupFlatMetadata?,
|
flatMetadata: BackupFlatMetadata?,
|
||||||
customManga: CustomMangaManager.MangaJson?,
|
online: Boolean
|
||||||
// SY -->
|
|
||||||
) {
|
) {
|
||||||
|
val dbManga = backupManager.getMangaFromDatabase(manga)
|
||||||
|
|
||||||
db.inTransaction {
|
db.inTransaction {
|
||||||
val dbManga = backupManager.getMangaFromDatabase(manga)
|
|
||||||
if (dbManga == null) {
|
if (dbManga == null) {
|
||||||
// Manga not in database
|
// Manga not in database
|
||||||
restoreMangaFetch(manga, chapters, categories, history, tracks, backupCategories/* SY --> */, mergedMangaReferences, flatMetadata, customManga/* SY <-- */)
|
restoreMangaFetch(source, manga, chapters, categories, history, tracks, backupCategories, mergedMangaReferences, flatMetadata, online)
|
||||||
} else {
|
} else { // Manga in database
|
||||||
// Manga in database
|
|
||||||
// Copy information from manga already in database
|
// Copy information from manga already in database
|
||||||
backupManager.restoreMangaNoFetch(manga, dbManga)
|
backupManager.restoreMangaNoFetch(manga, dbManga)
|
||||||
// Fetch rest of manga information
|
// Fetch rest of manga information
|
||||||
restoreMangaNoFetch(manga, chapters, categories, history, tracks, backupCategories/* SY --> */, mergedMangaReferences, flatMetadata, customManga/* SY <-- */)
|
restoreMangaNoFetch(source, manga, chapters, categories, history, tracks, backupCategories, mergedMangaReferences, flatMetadata, online)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -155,60 +157,66 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBa
|
|||||||
* @param chapters chapters of manga that needs updating
|
* @param chapters chapters of manga that needs updating
|
||||||
* @param categories categories that need updating
|
* @param categories categories that need updating
|
||||||
*/
|
*/
|
||||||
private fun restoreMangaFetch(
|
private suspend fun restoreMangaFetch(
|
||||||
|
source: Source?,
|
||||||
manga: Manga,
|
manga: Manga,
|
||||||
chapters: List<Chapter>,
|
chapters: List<Chapter>,
|
||||||
categories: List<Int>,
|
categories: List<Int>,
|
||||||
history: List<BackupHistory>,
|
history: List<BackupHistory>,
|
||||||
tracks: List<Track>,
|
tracks: List<Track>,
|
||||||
backupCategories: List<BackupCategory>,
|
backupCategories: List<BackupCategory>,
|
||||||
// SY -->
|
|
||||||
mergedMangaReferences: List<BackupMergedMangaReference>,
|
mergedMangaReferences: List<BackupMergedMangaReference>,
|
||||||
flatMetadata: BackupFlatMetadata?,
|
flatMetadata: BackupFlatMetadata?,
|
||||||
customManga: CustomMangaManager.MangaJson?,
|
online: Boolean
|
||||||
// SY <--
|
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
val fetchedManga = backupManager.restoreManga(manga)
|
val fetchedManga = backupManager.restoreMangaFetch(source, manga, online)
|
||||||
fetchedManga.id ?: return
|
fetchedManga.id ?: return
|
||||||
backupManager.restoreChaptersForManga(fetchedManga, chapters)
|
|
||||||
|
|
||||||
restoreExtraForManga(fetchedManga, categories, history, tracks, backupCategories /* SY --> */, mergedMangaReferences, flatMetadata, customManga/* SY <-- */)
|
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)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
errors.add(Date() to "${manga.title} - ${e.message}")
|
errors.add(Date() to "${manga.title} - ${e.message}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun restoreMangaNoFetch(
|
private suspend fun restoreMangaNoFetch(
|
||||||
|
source: Source?,
|
||||||
backupManga: Manga,
|
backupManga: Manga,
|
||||||
chapters: List<Chapter>,
|
chapters: List<Chapter>,
|
||||||
categories: List<Int>,
|
categories: List<Int>,
|
||||||
history: List<BackupHistory>,
|
history: List<BackupHistory>,
|
||||||
tracks: List<Track>,
|
tracks: List<Track>,
|
||||||
backupCategories: List<BackupCategory>,
|
backupCategories: List<BackupCategory>,
|
||||||
// SY -->
|
|
||||||
mergedMangaReferences: List<BackupMergedMangaReference>,
|
mergedMangaReferences: List<BackupMergedMangaReference>,
|
||||||
flatMetadata: BackupFlatMetadata?,
|
flatMetadata: BackupFlatMetadata?,
|
||||||
customManga: CustomMangaManager.MangaJson?,
|
online: Boolean
|
||||||
// SY <--
|
|
||||||
) {
|
) {
|
||||||
backupManager.restoreChaptersForManga(backupManga, chapters)
|
if (online && source != null) {
|
||||||
|
if (/* SY --> */ source !is MergedSource && /* SY <-- */ !backupManager.restoreChaptersForManga(backupManga, chapters)) {
|
||||||
|
updateChapters(source, backupManga, chapters)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
backupManager.restoreChaptersForMangaOffline(backupManga, chapters)
|
||||||
|
}
|
||||||
|
|
||||||
restoreExtraForManga(backupManga, categories, history, tracks, backupCategories/* SY --> */, mergedMangaReferences, flatMetadata, customManga/* SY <-- */)
|
restoreExtraForManga(backupManga, categories, history, tracks, backupCategories, mergedMangaReferences, flatMetadata)
|
||||||
|
|
||||||
|
updateTracking(backupManga, tracks)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun restoreExtraForManga(
|
private suspend fun restoreExtraForManga(manga: Manga, categories: List<Int>, history: List<BackupHistory>, tracks: List<Track>, backupCategories: List<BackupCategory>, mergedMangaReferences: List<BackupMergedMangaReference>, flatMetadata: BackupFlatMetadata?) {
|
||||||
manga: Manga,
|
|
||||||
categories: List<Int>,
|
|
||||||
history: List<BackupHistory>,
|
|
||||||
tracks: List<Track>,
|
|
||||||
backupCategories: List<BackupCategory>,
|
|
||||||
// SY -->
|
|
||||||
mergedMangaReferences: List<BackupMergedMangaReference>,
|
|
||||||
flatMetadata: BackupFlatMetadata?,
|
|
||||||
customManga: CustomMangaManager.MangaJson?,
|
|
||||||
// SY <--
|
|
||||||
) {
|
|
||||||
// Restore categories
|
// Restore categories
|
||||||
backupManager.restoreCategoriesForManga(manga, categories, backupCategories)
|
backupManager.restoreCategoriesForManga(manga, categories, backupCategories)
|
||||||
|
|
||||||
@@ -224,10 +232,6 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBa
|
|||||||
|
|
||||||
// Restore flat metadata for metadata sources
|
// Restore flat metadata for metadata sources
|
||||||
flatMetadata?.let { backupManager.restoreFlatMetadata(manga, it) }
|
flatMetadata?.let { backupManager.restoreFlatMetadata(manga, it) }
|
||||||
|
|
||||||
// Restore Custom Info
|
|
||||||
customManga?.id = manga.id!!
|
|
||||||
customManga?.let { customMangaManager.saveMangaInfo(it) }
|
|
||||||
// SY <--
|
// SY <--
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-11
@@ -4,14 +4,12 @@ import android.content.Context
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.backup.AbstractBackupRestoreValidator
|
import eu.kanade.tachiyomi.data.backup.AbstractBackupRestoreValidator
|
||||||
import eu.kanade.tachiyomi.data.backup.ValidatorParseException
|
|
||||||
import eu.kanade.tachiyomi.data.backup.full.models.BackupSerializer
|
import eu.kanade.tachiyomi.data.backup.full.models.BackupSerializer
|
||||||
import okio.buffer
|
import okio.buffer
|
||||||
import okio.gzip
|
import okio.gzip
|
||||||
import okio.source
|
import okio.source
|
||||||
|
|
||||||
class FullBackupRestoreValidator : AbstractBackupRestoreValidator() {
|
class FullBackupRestoreValidator : AbstractBackupRestoreValidator() {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks for critical backup file data.
|
* Checks for critical backup file data.
|
||||||
*
|
*
|
||||||
@@ -21,20 +19,14 @@ class FullBackupRestoreValidator : AbstractBackupRestoreValidator() {
|
|||||||
override fun validate(context: Context, uri: Uri): Results {
|
override fun validate(context: Context, uri: Uri): Results {
|
||||||
val backupManager = FullBackupManager(context)
|
val backupManager = FullBackupManager(context)
|
||||||
|
|
||||||
val backup = try {
|
val backupString = context.contentResolver.openInputStream(uri)!!.source().gzip().buffer().use { it.readByteArray() }
|
||||||
val backupString =
|
val backup = backupManager.parser.decodeFromByteArray(BackupSerializer, backupString)
|
||||||
context.contentResolver.openInputStream(uri)!!.source().gzip().buffer()
|
|
||||||
.use { it.readByteArray() }
|
|
||||||
backupManager.parser.decodeFromByteArray(BackupSerializer, backupString)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
throw ValidatorParseException(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (backup.backupManga.isEmpty()) {
|
if (backup.backupManga.isEmpty()) {
|
||||||
throw Exception(context.getString(R.string.invalid_backup_file_missing_manga))
|
throw Exception(context.getString(R.string.invalid_backup_file_missing_manga))
|
||||||
}
|
}
|
||||||
|
|
||||||
val sources = backup.backupSources.associate { it.sourceId to it.name }
|
val sources = backup.backupSources.map { it.sourceId to it.name }.toMap()
|
||||||
val missingSources = sources
|
val missingSources = sources
|
||||||
.filter { sourceManager.get(it.key) == null }
|
.filter { sourceManager.get(it.key) == null }
|
||||||
.values
|
.values
|
||||||
|
|||||||
@@ -8,8 +8,7 @@ data class Backup(
|
|||||||
@ProtoNumber(1) val backupManga: List<BackupManga>,
|
@ProtoNumber(1) val backupManga: List<BackupManga>,
|
||||||
@ProtoNumber(2) var backupCategories: List<BackupCategory> = emptyList(),
|
@ProtoNumber(2) var backupCategories: List<BackupCategory> = emptyList(),
|
||||||
// Bump by 100 to specify this is a 0.x value
|
// Bump by 100 to specify this is a 0.x value
|
||||||
@ProtoNumber(100) var backupBrokenSources: List<BrokenBackupSource> = emptyList(),
|
@ProtoNumber(100) var backupSources: List<BackupSource> = emptyList(),
|
||||||
@ProtoNumber(101) var backupSources: List<BackupSource> = emptyList(),
|
|
||||||
// SY specific values
|
// SY specific values
|
||||||
@ProtoNumber(600) var backupSavedSearches: List<BackupSavedSearch> = emptyList()
|
@ProtoNumber(600) var backupSavedSearches: List<BackupSavedSearch> = emptyList()
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,13 +4,7 @@ import kotlinx.serialization.Serializable
|
|||||||
import kotlinx.serialization.protobuf.ProtoNumber
|
import kotlinx.serialization.protobuf.ProtoNumber
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class BrokenBackupHistory(
|
data class BackupHistory(
|
||||||
@ProtoNumber(0) var url: String,
|
@ProtoNumber(0) var url: String,
|
||||||
@ProtoNumber(1) var lastRead: Long
|
@ProtoNumber(1) var lastRead: Long
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class BackupHistory(
|
|
||||||
@ProtoNumber(1) var url: String,
|
|
||||||
@ProtoNumber(2) var lastRead: Long
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import eu.kanade.tachiyomi.data.database.models.ChapterImpl
|
|||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaImpl
|
import eu.kanade.tachiyomi.data.database.models.MangaImpl
|
||||||
import eu.kanade.tachiyomi.data.database.models.TrackImpl
|
import eu.kanade.tachiyomi.data.database.models.TrackImpl
|
||||||
import eu.kanade.tachiyomi.data.library.CustomMangaManager
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.protobuf.ProtoNumber
|
import kotlinx.serialization.protobuf.ProtoNumber
|
||||||
|
|
||||||
@@ -26,7 +25,7 @@ data class BackupManga(
|
|||||||
// @ProtoNumber(11) val lastUpdate: Long = 0, 1.x value, not used in 0.x
|
// @ProtoNumber(11) val lastUpdate: Long = 0, 1.x value, not used in 0.x
|
||||||
// @ProtoNumber(12) val lastInit: Long = 0, 1.x value, not used in 0.x
|
// @ProtoNumber(12) val lastInit: Long = 0, 1.x value, not used in 0.x
|
||||||
@ProtoNumber(13) var dateAdded: Long = 0,
|
@ProtoNumber(13) var dateAdded: Long = 0,
|
||||||
@ProtoNumber(14) var viewer: Int = 0, // Replaced by viewer_flags
|
@ProtoNumber(14) var viewer: Int = 0,
|
||||||
// @ProtoNumber(15) val flags: Int = 0, 1.x value, not used in 0.x
|
// @ProtoNumber(15) val flags: Int = 0, 1.x value, not used in 0.x
|
||||||
@ProtoNumber(16) var chapters: List<BackupChapter> = emptyList(),
|
@ProtoNumber(16) var chapters: List<BackupChapter> = emptyList(),
|
||||||
@ProtoNumber(17) var categories: List<Int> = emptyList(),
|
@ProtoNumber(17) var categories: List<Int> = emptyList(),
|
||||||
@@ -34,25 +33,10 @@ data class BackupManga(
|
|||||||
// Bump by 100 for values that are not saved/implemented in 1.x but are used in 0.x
|
// Bump by 100 for values that are not saved/implemented in 1.x but are used in 0.x
|
||||||
@ProtoNumber(100) var favorite: Boolean = true,
|
@ProtoNumber(100) var favorite: Boolean = true,
|
||||||
@ProtoNumber(101) var chapterFlags: Int = 0,
|
@ProtoNumber(101) var chapterFlags: Int = 0,
|
||||||
@ProtoNumber(102) var brokenHistory: List<BrokenBackupHistory> = emptyList(),
|
@ProtoNumber(102) var history: List<BackupHistory> = emptyList(),
|
||||||
@ProtoNumber(103) var viewer_flags: Int? = null,
|
|
||||||
@ProtoNumber(104) var history: List<BackupHistory> = emptyList(),
|
|
||||||
|
|
||||||
// SY specific values
|
// SY specific values
|
||||||
@ProtoNumber(600) var mergedMangaReferences: List<BackupMergedMangaReference> = emptyList(),
|
@ProtoNumber(600) var mergedMangaReferences: List<BackupMergedMangaReference> = emptyList(),
|
||||||
@ProtoNumber(601) var flatMetadata: BackupFlatMetadata? = null,
|
@ProtoNumber(601) var flatMetadata: BackupFlatMetadata? = null
|
||||||
@ProtoNumber(602) var customStatus: Int = 0,
|
|
||||||
|
|
||||||
// J2K specific values
|
|
||||||
@ProtoNumber(800) var customTitle: String? = null,
|
|
||||||
@ProtoNumber(801) var customArtist: String? = null,
|
|
||||||
@ProtoNumber(802) var customAuthor: String? = null,
|
|
||||||
// skipping 803 due to using duplicate value in previous builds
|
|
||||||
@ProtoNumber(804) var customDescription: String? = null,
|
|
||||||
@ProtoNumber(805) var customGenre: List<String>? = null,
|
|
||||||
|
|
||||||
// Neko specific values
|
|
||||||
@ProtoNumber(901) var filtered_scanlators: String? = null,
|
|
||||||
) {
|
) {
|
||||||
fun getMangaImpl(): MangaImpl {
|
fun getMangaImpl(): MangaImpl {
|
||||||
return MangaImpl().apply {
|
return MangaImpl().apply {
|
||||||
@@ -67,9 +51,8 @@ data class BackupManga(
|
|||||||
favorite = this@BackupManga.favorite
|
favorite = this@BackupManga.favorite
|
||||||
source = this@BackupManga.source
|
source = this@BackupManga.source
|
||||||
date_added = this@BackupManga.dateAdded
|
date_added = this@BackupManga.dateAdded
|
||||||
viewer_flags = this@BackupManga.viewer_flags ?: this@BackupManga.viewer
|
viewer = this@BackupManga.viewer
|
||||||
chapter_flags = this@BackupManga.chapterFlags
|
chapter_flags = this@BackupManga.chapterFlags
|
||||||
filtered_scanlators = this@BackupManga.filtered_scanlators
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,29 +62,6 @@ data class BackupManga(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SY -->
|
|
||||||
fun getCustomMangaInfo(): CustomMangaManager.MangaJson? {
|
|
||||||
if (customTitle != null ||
|
|
||||||
customArtist != null ||
|
|
||||||
customAuthor != null ||
|
|
||||||
customDescription != null ||
|
|
||||||
customGenre != null ||
|
|
||||||
customStatus != 0
|
|
||||||
) {
|
|
||||||
return CustomMangaManager.MangaJson(
|
|
||||||
id = 0L,
|
|
||||||
title = customTitle,
|
|
||||||
author = customAuthor,
|
|
||||||
artist = customArtist,
|
|
||||||
description = customDescription,
|
|
||||||
genre = customGenre,
|
|
||||||
status = customStatus.takeUnless { it == 0 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
// SY <--
|
|
||||||
|
|
||||||
fun getTrackingImpl(): List<TrackImpl> {
|
fun getTrackingImpl(): List<TrackImpl> {
|
||||||
return tracking.map {
|
return tracking.map {
|
||||||
it.getTrackingImpl()
|
it.getTrackingImpl()
|
||||||
@@ -109,37 +69,22 @@ data class BackupManga(
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun copyFrom(manga: Manga /* SY --> */, customMangaManager: CustomMangaManager?/* SY <-- */): BackupManga {
|
fun copyFrom(manga: Manga): BackupManga {
|
||||||
return BackupManga(
|
return BackupManga(
|
||||||
url = manga.url,
|
url = manga.url,
|
||||||
// SY -->
|
title = manga.title,
|
||||||
title = manga.originalTitle,
|
artist = manga.artist,
|
||||||
artist = manga.originalArtist,
|
author = manga.author,
|
||||||
author = manga.originalAuthor,
|
description = manga.description,
|
||||||
description = manga.originalDescription,
|
genre = manga.getGenres() ?: emptyList(),
|
||||||
genre = manga.getOriginalGenres() ?: emptyList(),
|
status = manga.status,
|
||||||
status = manga.originalStatus,
|
|
||||||
// SY <--
|
|
||||||
thumbnailUrl = manga.thumbnail_url,
|
thumbnailUrl = manga.thumbnail_url,
|
||||||
favorite = manga.favorite,
|
favorite = manga.favorite,
|
||||||
source = manga.source,
|
source = manga.source,
|
||||||
dateAdded = manga.date_added,
|
dateAdded = manga.date_added,
|
||||||
viewer = manga.readingModeType,
|
viewer = manga.viewer,
|
||||||
viewer_flags = manga.viewer_flags,
|
chapterFlags = manga.chapter_flags
|
||||||
chapterFlags = manga.chapter_flags,
|
)
|
||||||
filtered_scanlators = manga.filtered_scanlators
|
|
||||||
// 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 <--
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,15 +5,9 @@ import kotlinx.serialization.Serializable
|
|||||||
import kotlinx.serialization.protobuf.ProtoNumber
|
import kotlinx.serialization.protobuf.ProtoNumber
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class BrokenBackupSource(
|
data class BackupSource(
|
||||||
@ProtoNumber(0) var name: String = "",
|
@ProtoNumber(0) var name: String = "",
|
||||||
@ProtoNumber(1) var sourceId: Long
|
@ProtoNumber(1) var sourceId: Long
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class BackupSource(
|
|
||||||
@ProtoNumber(1) var name: String = "",
|
|
||||||
@ProtoNumber(2) var sourceId: Long
|
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
fun copyFrom(source: Source): BackupSource {
|
fun copyFrom(source: Source): BackupSource {
|
||||||
|
|||||||
@@ -32,7 +32,8 @@ data class BackupTracking(
|
|||||||
media_id = this@BackupTracking.mediaId
|
media_id = this@BackupTracking.mediaId
|
||||||
library_id = this@BackupTracking.libraryId
|
library_id = this@BackupTracking.libraryId
|
||||||
title = this@BackupTracking.title
|
title = this@BackupTracking.title
|
||||||
last_chapter_read = this@BackupTracking.lastChapterRead
|
// convert from float to int because of 1.x types
|
||||||
|
last_chapter_read = this@BackupTracking.lastChapterRead.toInt()
|
||||||
total_chapters = this@BackupTracking.totalChapters
|
total_chapters = this@BackupTracking.totalChapters
|
||||||
score = this@BackupTracking.score
|
score = this@BackupTracking.score
|
||||||
status = this@BackupTracking.status
|
status = this@BackupTracking.status
|
||||||
@@ -50,7 +51,8 @@ data class BackupTracking(
|
|||||||
// forced not null so its compatible with 1.x backup system
|
// forced not null so its compatible with 1.x backup system
|
||||||
libraryId = track.library_id!!,
|
libraryId = track.library_id!!,
|
||||||
title = track.title,
|
title = track.title,
|
||||||
lastChapterRead = track.last_chapter_read,
|
// convert to float for 1.x
|
||||||
|
lastChapterRead = track.last_chapter_read.toFloat(),
|
||||||
totalChapters = track.total_chapters,
|
totalChapters = track.total_chapters,
|
||||||
score = track.score,
|
score = track.score,
|
||||||
status = track.status,
|
status = track.status,
|
||||||
|
|||||||
@@ -2,27 +2,55 @@ package eu.kanade.tachiyomi.data.backup.legacy
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import com.github.salomonbrys.kotson.fromJson
|
||||||
|
import com.github.salomonbrys.kotson.registerTypeAdapter
|
||||||
|
import com.github.salomonbrys.kotson.registerTypeHierarchyAdapter
|
||||||
|
import com.github.salomonbrys.kotson.set
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import com.google.gson.GsonBuilder
|
||||||
|
import com.google.gson.JsonArray
|
||||||
|
import com.google.gson.JsonElement
|
||||||
|
import com.google.gson.JsonObject
|
||||||
|
import com.hippo.unifile.UniFile
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.backup.AbstractBackupManager
|
import eu.kanade.tachiyomi.data.backup.AbstractBackupManager
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.Companion.CURRENT_VERSION
|
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CATEGORY
|
||||||
|
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_HISTORY
|
||||||
|
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_HISTORY_MASK
|
||||||
|
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_TRACK
|
||||||
|
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_TRACK_MASK
|
||||||
|
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup
|
||||||
|
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.CATEGORIES
|
||||||
|
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.CHAPTERS
|
||||||
|
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.CURRENT_VERSION
|
||||||
|
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.EXTENSIONS
|
||||||
|
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.HISTORY
|
||||||
|
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.MANGA
|
||||||
|
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.MERGEDMANGAREFERENCES
|
||||||
|
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.SAVEDSEARCHES
|
||||||
|
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.TRACK
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.models.DHistory
|
import eu.kanade.tachiyomi.data.backup.legacy.models.DHistory
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.serializer.CategoryImplTypeSerializer
|
import eu.kanade.tachiyomi.data.backup.legacy.serializer.CategoryTypeAdapter
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.serializer.CategoryTypeSerializer
|
import eu.kanade.tachiyomi.data.backup.legacy.serializer.ChapterTypeAdapter
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.serializer.ChapterImplTypeSerializer
|
import eu.kanade.tachiyomi.data.backup.legacy.serializer.HistoryTypeAdapter
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.serializer.ChapterTypeSerializer
|
import eu.kanade.tachiyomi.data.backup.legacy.serializer.MangaTypeAdapter
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.serializer.HistoryTypeSerializer
|
import eu.kanade.tachiyomi.data.backup.legacy.serializer.MergedMangaReferenceTypeAdapter
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.serializer.MangaImplTypeSerializer
|
import eu.kanade.tachiyomi.data.backup.legacy.serializer.TrackTypeAdapter
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.serializer.MangaTypeSerializer
|
import eu.kanade.tachiyomi.data.database.models.CategoryImpl
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.serializer.MergedMangaTypeSerializer
|
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.serializer.TrackImplTypeSerializer
|
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.serializer.TrackTypeSerializer
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Category
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.ChapterImpl
|
||||||
import eu.kanade.tachiyomi.data.database.models.History
|
import eu.kanade.tachiyomi.data.database.models.History
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaCategory
|
import eu.kanade.tachiyomi.data.database.models.MangaCategory
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.MangaImpl
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.TrackImpl
|
||||||
import eu.kanade.tachiyomi.data.database.models.toMangaInfo
|
import eu.kanade.tachiyomi.data.database.models.toMangaInfo
|
||||||
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
|
import eu.kanade.tachiyomi.source.LocalSource
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.source.model.toSManga
|
import eu.kanade.tachiyomi.source.model.toSManga
|
||||||
import eu.kanade.tachiyomi.source.online.all.MergedSource
|
import eu.kanade.tachiyomi.source.online.all.MergedSource
|
||||||
@@ -33,33 +61,25 @@ import exh.source.MERGED_SOURCE_ID
|
|||||||
import kotlinx.serialization.decodeFromString
|
import kotlinx.serialization.decodeFromString
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.modules.SerializersModule
|
import timber.log.Timber
|
||||||
import kotlinx.serialization.modules.contextual
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
import java.lang.RuntimeException
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
|
|
||||||
class LegacyBackupManager(context: Context, version: Int = CURRENT_VERSION) : AbstractBackupManager(context) {
|
class LegacyBackupManager(context: Context, version: Int = CURRENT_VERSION) : AbstractBackupManager(context) {
|
||||||
|
|
||||||
val parser: Json = when (version) {
|
val parser: Gson = when (version) {
|
||||||
2 -> Json {
|
2 -> GsonBuilder()
|
||||||
// Forks may have added items to backup
|
.registerTypeAdapter<MangaImpl>(MangaTypeAdapter.build())
|
||||||
ignoreUnknownKeys = true
|
.registerTypeHierarchyAdapter<ChapterImpl>(ChapterTypeAdapter.build())
|
||||||
|
.registerTypeAdapter<CategoryImpl>(CategoryTypeAdapter.build())
|
||||||
// Register custom serializers
|
.registerTypeAdapter<DHistory>(HistoryTypeAdapter.build())
|
||||||
serializersModule = SerializersModule {
|
.registerTypeHierarchyAdapter<TrackImpl>(TrackTypeAdapter.build())
|
||||||
contextual(MangaTypeSerializer)
|
// SY -->
|
||||||
contextual(MangaImplTypeSerializer)
|
.registerTypeAdapter<MergedMangaReference>(MergedMangaReferenceTypeAdapter.build())
|
||||||
contextual(ChapterTypeSerializer)
|
// SY <--
|
||||||
contextual(ChapterImplTypeSerializer)
|
.create()
|
||||||
contextual(CategoryTypeSerializer)
|
|
||||||
contextual(CategoryImplTypeSerializer)
|
|
||||||
contextual(TrackTypeSerializer)
|
|
||||||
contextual(TrackImplTypeSerializer)
|
|
||||||
contextual(HistoryTypeSerializer)
|
|
||||||
// SY -->
|
|
||||||
contextual(MergedMangaTypeSerializer)
|
|
||||||
// SY <--
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> throw Exception("Unknown backup version")
|
else -> throw Exception("Unknown backup version")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,8 +89,180 @@ class LegacyBackupManager(context: Context, version: Int = CURRENT_VERSION) : Ab
|
|||||||
* @param uri path of Uri
|
* @param uri path of Uri
|
||||||
* @param isJob backup called from job
|
* @param isJob backup called from job
|
||||||
*/
|
*/
|
||||||
override fun createBackup(uri: Uri, flags: Int, isJob: Boolean) =
|
override fun createBackup(uri: Uri, flags: Int, isJob: Boolean): String? {
|
||||||
throw IllegalStateException("Legacy backup creation is not supported")
|
// Create root object
|
||||||
|
val root = JsonObject()
|
||||||
|
|
||||||
|
// Create manga array
|
||||||
|
val mangaEntries = JsonArray()
|
||||||
|
|
||||||
|
// Create category array
|
||||||
|
val categoryEntries = JsonArray()
|
||||||
|
|
||||||
|
// Create extension ID/name mapping
|
||||||
|
val extensionEntries = JsonArray()
|
||||||
|
|
||||||
|
// Merged Manga References
|
||||||
|
val mergedMangaReferenceEntries = JsonArray()
|
||||||
|
|
||||||
|
// Add value's to root
|
||||||
|
root[Backup.VERSION] = CURRENT_VERSION
|
||||||
|
root[Backup.MANGAS] = mangaEntries
|
||||||
|
root[CATEGORIES] = categoryEntries
|
||||||
|
root[EXTENSIONS] = extensionEntries
|
||||||
|
// SY -->
|
||||||
|
root[MERGEDMANGAREFERENCES] = mergedMangaReferenceEntries
|
||||||
|
// SY <--
|
||||||
|
|
||||||
|
databaseHelper.inTransaction {
|
||||||
|
val mangas = getFavoriteManga()/* SY --> */.filterNot { it.source == MERGED_SOURCE_ID } + getMergedManga().filterNot { it.source == MERGED_SOURCE_ID } /* SY <-- */
|
||||||
|
|
||||||
|
val extensions: MutableSet<String> = mutableSetOf()
|
||||||
|
|
||||||
|
// Backup library manga and its dependencies
|
||||||
|
mangas.forEach { manga ->
|
||||||
|
mangaEntries.add(backupMangaObject(manga, flags))
|
||||||
|
|
||||||
|
// Maintain set of extensions/sources used (excludes local source)
|
||||||
|
if (manga.source != LocalSource.ID) {
|
||||||
|
sourceManager.get(manga.source)?.let {
|
||||||
|
extensions.add("${manga.source}:${it.name}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backup categories
|
||||||
|
if ((flags and BACKUP_CATEGORY_MASK) == BACKUP_CATEGORY) {
|
||||||
|
backupCategories(categoryEntries)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backup extension ID/name mapping
|
||||||
|
backupExtensionInfo(extensionEntries, extensions)
|
||||||
|
// SY -->
|
||||||
|
root[SAVEDSEARCHES] =
|
||||||
|
Injekt.get<PreferencesHelper>().savedSearches().get().joinToString(separator = "***")
|
||||||
|
|
||||||
|
backupMergedMangaReferences(mergedMangaReferenceEntries)
|
||||||
|
// SY <--
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
val file: UniFile = (
|
||||||
|
if (isJob) {
|
||||||
|
// Get dir of file and create
|
||||||
|
var dir = UniFile.fromUri(context, uri)
|
||||||
|
dir = dir.createDirectory("automatic")
|
||||||
|
|
||||||
|
// Delete older backups
|
||||||
|
val numberOfBackups = numberOfBackups()
|
||||||
|
val backupRegex = Regex("""tachiyomi_\d+-\d+-\d+_\d+-\d+.json""")
|
||||||
|
dir.listFiles { _, filename -> backupRegex.matches(filename) }
|
||||||
|
.orEmpty()
|
||||||
|
.sortedByDescending { it.name }
|
||||||
|
.drop(numberOfBackups - 1)
|
||||||
|
.forEach { it.delete() }
|
||||||
|
|
||||||
|
// Create new file to place backup
|
||||||
|
dir.createFile(Backup.getDefaultFilename())
|
||||||
|
} else {
|
||||||
|
UniFile.fromUri(context, uri)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
?: throw Exception("Couldn't create backup file")
|
||||||
|
|
||||||
|
file.openOutputStream().bufferedWriter().use {
|
||||||
|
parser.toJson(root, it)
|
||||||
|
}
|
||||||
|
return file.uri.toString()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun backupExtensionInfo(root: JsonArray, extensions: Set<String>) {
|
||||||
|
extensions.sorted().forEach {
|
||||||
|
root.add(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SY -->
|
||||||
|
private fun backupMergedMangaReferences(root: JsonArray) {
|
||||||
|
val mergedMangaReferences = databaseHelper.getMergedMangaReferences().executeAsBlocking()
|
||||||
|
mergedMangaReferences.forEach { root.add(parser.toJsonTree(it)) }
|
||||||
|
}
|
||||||
|
// SY <--
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Backup the categories of library
|
||||||
|
*
|
||||||
|
* @param root root of categories json
|
||||||
|
*/
|
||||||
|
internal fun backupCategories(root: JsonArray) {
|
||||||
|
val categories = databaseHelper.getCategories().executeAsBlocking()
|
||||||
|
categories.forEach { root.add(parser.toJsonTree(it)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a manga to Json
|
||||||
|
*
|
||||||
|
* @param manga manga that gets converted
|
||||||
|
* @return [JsonElement] containing manga information
|
||||||
|
*/
|
||||||
|
internal fun backupMangaObject(manga: Manga, options: Int): JsonElement {
|
||||||
|
// Entry for this manga
|
||||||
|
val entry = JsonObject()
|
||||||
|
|
||||||
|
// Backup manga fields
|
||||||
|
entry[MANGA] = parser.toJsonTree(manga)
|
||||||
|
|
||||||
|
// Check if user wants chapter information in backup
|
||||||
|
if (options and BACKUP_CHAPTER_MASK == BACKUP_CHAPTER) {
|
||||||
|
// Backup all the chapters
|
||||||
|
val chapters = databaseHelper.getChapters(manga).executeAsBlocking()
|
||||||
|
if (chapters.isNotEmpty()) {
|
||||||
|
val chaptersJson = parser.toJsonTree(chapters)
|
||||||
|
if (chaptersJson.asJsonArray.size() > 0) {
|
||||||
|
entry[CHAPTERS] = chaptersJson
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user wants category information in backup
|
||||||
|
if (options and BACKUP_CATEGORY_MASK == BACKUP_CATEGORY) {
|
||||||
|
// Backup categories for this manga
|
||||||
|
val categoriesForManga = databaseHelper.getCategoriesForManga(manga).executeAsBlocking()
|
||||||
|
if (categoriesForManga.isNotEmpty()) {
|
||||||
|
val categoriesNames = categoriesForManga.map { it.name }
|
||||||
|
entry[CATEGORIES] = parser.toJsonTree(categoriesNames)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user wants track information in backup
|
||||||
|
if (options and BACKUP_TRACK_MASK == BACKUP_TRACK) {
|
||||||
|
val tracks = databaseHelper.getTracks(manga).executeAsBlocking()
|
||||||
|
if (tracks.isNotEmpty()) {
|
||||||
|
entry[TRACK] = parser.toJsonTree(tracks)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user wants history information in backup
|
||||||
|
if (options and BACKUP_HISTORY_MASK == BACKUP_HISTORY) {
|
||||||
|
val historyForManga = databaseHelper.getHistoryByMangaId(manga.id!!).executeAsBlocking()
|
||||||
|
if (historyForManga.isNotEmpty()) {
|
||||||
|
val historyData = historyForManga.mapNotNull { history ->
|
||||||
|
val url = databaseHelper.getChapter(history.chapter_id).executeAsBlocking()?.url
|
||||||
|
url?.let { DHistory(url, history.last_read) }
|
||||||
|
}
|
||||||
|
val historyJson = parser.toJsonTree(historyData)
|
||||||
|
if (historyJson.asJsonArray.size() > 0) {
|
||||||
|
entry[HISTORY] = historyJson
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return entry
|
||||||
|
}
|
||||||
|
|
||||||
fun restoreMangaNoFetch(manga: Manga, dbManga: Manga) {
|
fun restoreMangaNoFetch(manga: Manga, dbManga: Manga) {
|
||||||
manga.id = dbManga.id
|
manga.id = dbManga.id
|
||||||
@@ -120,11 +312,12 @@ class LegacyBackupManager(context: Context, version: Int = CURRENT_VERSION) : Ab
|
|||||||
/**
|
/**
|
||||||
* Restore the categories from Json
|
* Restore the categories from Json
|
||||||
*
|
*
|
||||||
* @param backupCategories array containing categories
|
* @param jsonCategories array containing categories
|
||||||
*/
|
*/
|
||||||
internal fun restoreCategories(backupCategories: List<Category>) {
|
internal fun restoreCategories(jsonCategories: JsonArray) {
|
||||||
// Get categories from file and from db
|
// Get categories from file and from db
|
||||||
val dbCategories = databaseHelper.getCategories().executeAsBlocking()
|
val dbCategories = databaseHelper.getCategories().executeAsBlocking()
|
||||||
|
val backupCategories = parser.fromJson<List<CategoryImpl>>(jsonCategories)
|
||||||
|
|
||||||
// Iterate over them
|
// Iterate over them
|
||||||
backupCategories.forEach { category ->
|
backupCategories.forEach { category ->
|
||||||
@@ -284,47 +477,58 @@ class LegacyBackupManager(context: Context, version: Int = CURRENT_VERSION) : Ab
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
internal fun restoreSavedSearches(jsonSavedSearches: String) {
|
internal fun restoreSavedSearches(jsonSavedSearches: JsonElement) {
|
||||||
val backupSavedSearches = jsonSavedSearches.split("***").toSet()
|
val backupSavedSearches = jsonSavedSearches.asString.split("***").toSet()
|
||||||
|
|
||||||
val newSavedSearches = backupSavedSearches.mapNotNull {
|
val newSavedSearches = backupSavedSearches.mapNotNull {
|
||||||
runCatching {
|
try {
|
||||||
val id = it.substringBefore(':').toLongOrNull() ?: return@mapNotNull null
|
val id = it.substringBefore(':').toLong()
|
||||||
val content = parser.decodeFromString<JsonSavedSearch>(it.substringAfter(':'))
|
val content = Json.decodeFromString<JsonSavedSearch>(it.substringAfter(':'))
|
||||||
id to content
|
id to content
|
||||||
}.getOrNull()
|
} catch (t: RuntimeException) {
|
||||||
}.toMutableSet()
|
// Load failed
|
||||||
|
Timber.e(t, "Failed to load saved search!")
|
||||||
|
t.printStackTrace()
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}.toMutableList()
|
||||||
|
|
||||||
val currentSources = newSavedSearches.map(Pair<Long, *>::first).toSet()
|
val currentSources = newSavedSearches.map { it.first }.toSet()
|
||||||
|
|
||||||
newSavedSearches += preferences.savedSearches().get().mapNotNull {
|
newSavedSearches += preferences.savedSearches().get().mapNotNull {
|
||||||
kotlin.runCatching {
|
try {
|
||||||
val id = it.substringBefore(':').toLongOrNull() ?: return@mapNotNull null
|
val id = it.substringBefore(':').toLong()
|
||||||
val content = parser.decodeFromString<JsonSavedSearch>(it.substringAfter(':'))
|
val content = Json.decodeFromString<JsonSavedSearch>(it.substringAfter(':'))
|
||||||
id to content
|
id to content
|
||||||
}.getOrNull()
|
} catch (t: RuntimeException) {
|
||||||
}
|
// Load failed
|
||||||
|
Timber.e(t, "Failed to load saved search!")
|
||||||
|
t.printStackTrace()
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}.toMutableList()
|
||||||
|
|
||||||
val otherSerialized = preferences.savedSearches().get().mapNotNull {
|
val otherSerialized = preferences.savedSearches().get().mapNotNull {
|
||||||
val sourceId = it.substringBefore(":").toLongOrNull() ?: return@mapNotNull null
|
val sourceId = it.split(":")[0].toLongOrNull() ?: return@mapNotNull null
|
||||||
if (sourceId in currentSources) return@mapNotNull null
|
if (sourceId in currentSources) return@mapNotNull null
|
||||||
it
|
it
|
||||||
}.toSet()
|
}
|
||||||
|
|
||||||
val newSerialized = newSavedSearches.map { (source, savedSearch) ->
|
val newSerialized = newSavedSearches.map {
|
||||||
"$source:" + Json.encodeToString(savedSearch)
|
"${it.first}:" + Json.encodeToString(it.second)
|
||||||
}.toSet()
|
}
|
||||||
preferences.savedSearches().set(otherSerialized + newSerialized)
|
preferences.savedSearches().set((otherSerialized + newSerialized).toSet())
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Restore the categories from Json
|
* Restore the categories from Json
|
||||||
*
|
*
|
||||||
* @param backupMergedMangaReferences array containing md manga references
|
* @param jsonMergedMangaReferences array containing md manga references
|
||||||
*/
|
*/
|
||||||
internal fun restoreMergedMangaReferences(backupMergedMangaReferences: List<MergedMangaReference>) {
|
internal fun restoreMergedMangaReferences(jsonMergedMangaReferences: JsonArray) {
|
||||||
// Get merged manga references from file and from db
|
// Get merged manga references from file and from db
|
||||||
val dbMergedMangaReferences = databaseHelper.getMergedMangaReferences().executeAsBlocking()
|
val dbMergedMangaReferences = databaseHelper.getMergedMangaReferences().executeAsBlocking()
|
||||||
|
val backupMergedMangaReferences = parser.fromJson<List<MergedMangaReference>>(jsonMergedMangaReferences)
|
||||||
var lastMergeManga: Manga? = null
|
var lastMergeManga: Manga? = null
|
||||||
|
|
||||||
// Iterate over them
|
// Iterate over them
|
||||||
|
|||||||
@@ -2,26 +2,26 @@ package eu.kanade.tachiyomi.data.backup.legacy
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import com.github.salomonbrys.kotson.fromJson
|
||||||
|
import com.google.gson.JsonArray
|
||||||
|
import com.google.gson.JsonElement
|
||||||
|
import com.google.gson.JsonObject
|
||||||
|
import com.google.gson.JsonParser
|
||||||
|
import com.google.gson.stream.JsonReader
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.backup.AbstractBackupRestore
|
import eu.kanade.tachiyomi.data.backup.AbstractBackupRestore
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupNotifier
|
import eu.kanade.tachiyomi.data.backup.BackupNotifier
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup
|
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup
|
||||||
|
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.MANGAS
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.models.DHistory
|
import eu.kanade.tachiyomi.data.backup.legacy.models.DHistory
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.models.MangaObject
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Category
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.ChapterImpl
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.MangaImpl
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.TrackImpl
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import exh.EXHMigrations
|
import exh.EXHMigrations
|
||||||
import exh.merged.sql.models.MergedMangaReference
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import kotlinx.serialization.json.JsonObject
|
|
||||||
import kotlinx.serialization.json.decodeFromJsonElement
|
|
||||||
import kotlinx.serialization.json.decodeFromStream
|
|
||||||
import kotlinx.serialization.json.intOrNull
|
|
||||||
import kotlinx.serialization.json.jsonPrimitive
|
|
||||||
import okio.source
|
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
|
||||||
class LegacyBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBackupRestore<LegacyBackupManager>(context, notifier) {
|
class LegacyBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBackupRestore<LegacyBackupManager>(context, notifier) {
|
||||||
@@ -30,59 +30,59 @@ class LegacyBackupRestore(context: Context, notifier: BackupNotifier) : Abstract
|
|||||||
// SY -->
|
// SY -->
|
||||||
throttleManager.resetThrottle()
|
throttleManager.resetThrottle()
|
||||||
// SY <--
|
// SY <--
|
||||||
|
val reader = JsonReader(context.contentResolver.openInputStream(uri)!!.bufferedReader())
|
||||||
|
val json = JsonParser.parseReader(reader).asJsonObject
|
||||||
|
|
||||||
// Read the json and create a Json Object,
|
val version = json.get(Backup.VERSION)?.asInt ?: 1
|
||||||
// cannot use the backupManager json deserializer one because its not initialized yet
|
|
||||||
val backupObject = Json.decodeFromStream<JsonObject>(
|
|
||||||
context.contentResolver.openInputStream(uri)!!
|
|
||||||
)
|
|
||||||
|
|
||||||
// Get parser version
|
|
||||||
val version = backupObject["version"]?.jsonPrimitive?.intOrNull ?: 1
|
|
||||||
|
|
||||||
// Initialize manager
|
|
||||||
backupManager = LegacyBackupManager(context, version)
|
backupManager = LegacyBackupManager(context, version)
|
||||||
|
|
||||||
// Decode the json object to a Backup object
|
val mangasJson = json.get(MANGAS).asJsonArray
|
||||||
val backup = backupManager.parser.decodeFromJsonElement<Backup>(backupObject)
|
restoreAmount = mangasJson.size() + 3 // +1 for categories, +1 for saved searches, +1 for merged manga references
|
||||||
|
|
||||||
restoreAmount = backup.mangas.size + 3 // +1 for categories, +1 for saved searches, +1 for merged manga references
|
|
||||||
|
|
||||||
// SY -->
|
|
||||||
backup.savedSearches?.let { restoreSavedSearches(it) }
|
|
||||||
|
|
||||||
backup.mergedMangaReferences?.let { restoreMergedMangaReferences(it) }
|
|
||||||
// SY <--
|
|
||||||
|
|
||||||
// Restore categories
|
// Restore categories
|
||||||
backup.categories?.let { restoreCategories(it) }
|
json.get(Backup.CATEGORIES)?.let { restoreCategories(it) }
|
||||||
|
|
||||||
|
// SY -->
|
||||||
|
json.get(Backup.SAVEDSEARCHES)?.let { restoreSavedSearches(it) }
|
||||||
|
|
||||||
|
json.get(Backup.MERGEDMANGAREFERENCES)?.let { restoreMergedMangaReferences(it) }
|
||||||
|
// SY <--
|
||||||
|
|
||||||
// Store source mapping for error messages
|
// Store source mapping for error messages
|
||||||
sourceMapping = LegacyBackupRestoreValidator.getSourceMapping(backup.extensions ?: emptyList())
|
sourceMapping = LegacyBackupRestoreValidator.getSourceMapping(json)
|
||||||
|
|
||||||
// Restore individual manga
|
// Restore individual manga
|
||||||
backup.mangas.forEach {
|
mangasJson.forEach {
|
||||||
if (job?.isActive != true) {
|
if (job?.isActive != true) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
restoreManga(it)
|
restoreManga(it.asJsonObject)
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun restoreCategories(categoriesJson: JsonElement) {
|
||||||
|
db.inTransaction {
|
||||||
|
backupManager.restoreCategories(categoriesJson.asJsonArray)
|
||||||
|
}
|
||||||
|
|
||||||
|
restoreProgress += 1
|
||||||
|
showRestoreProgress(restoreProgress, restoreAmount, context.getString(R.string.categories))
|
||||||
|
}
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
private fun restoreSavedSearches(savedSearches: String) {
|
private fun restoreSavedSearches(savedSearchesJson: JsonElement) {
|
||||||
backupManager.restoreSavedSearches(savedSearches)
|
backupManager.restoreSavedSearches(savedSearchesJson)
|
||||||
|
|
||||||
restoreProgress += 1
|
restoreProgress += 1
|
||||||
showRestoreProgress(restoreProgress, restoreAmount, context.getString(R.string.saved_searches))
|
showRestoreProgress(restoreProgress, restoreAmount, context.getString(R.string.saved_searches))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun restoreMergedMangaReferences(mergedMangaReferences: List<MergedMangaReference>) {
|
private fun restoreMergedMangaReferences(mergedMangaReferencesJson: JsonElement) {
|
||||||
db.inTransaction {
|
db.inTransaction {
|
||||||
backupManager.restoreMergedMangaReferences(mergedMangaReferences)
|
backupManager.restoreMergedMangaReferences(mergedMangaReferencesJson.asJsonArray)
|
||||||
}
|
}
|
||||||
|
|
||||||
restoreProgress += 1
|
restoreProgress += 1
|
||||||
@@ -90,24 +90,31 @@ class LegacyBackupRestore(context: Context, notifier: BackupNotifier) : Abstract
|
|||||||
}
|
}
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
private fun restoreCategories(categoriesJson: List<Category>) {
|
private suspend fun restoreManga(mangaJson: JsonObject) {
|
||||||
db.inTransaction {
|
/* SY --> */ var /* SY <-- */ manga = backupManager.parser.fromJson<MangaImpl>(
|
||||||
backupManager.restoreCategories(categoriesJson)
|
mangaJson.get(
|
||||||
}
|
Backup.MANGA
|
||||||
|
)
|
||||||
restoreProgress += 1
|
)
|
||||||
showRestoreProgress(restoreProgress, restoreAmount, context.getString(R.string.categories))
|
val chapters = backupManager.parser.fromJson<List<ChapterImpl>>(
|
||||||
}
|
mangaJson.get(Backup.CHAPTERS)
|
||||||
|
?: JsonArray()
|
||||||
private suspend fun restoreManga(mangaJson: MangaObject) {
|
)
|
||||||
val manga = mangaJson.manga
|
val categories = backupManager.parser.fromJson<List<String>>(
|
||||||
val chapters = mangaJson.chapters ?: emptyList()
|
mangaJson.get(Backup.CATEGORIES)
|
||||||
val categories = mangaJson.categories ?: emptyList()
|
?: JsonArray()
|
||||||
val history = mangaJson.history ?: emptyList()
|
)
|
||||||
val tracks = mangaJson.track ?: emptyList()
|
val history = backupManager.parser.fromJson<List<DHistory>>(
|
||||||
|
mangaJson.get(Backup.HISTORY)
|
||||||
|
?: JsonArray()
|
||||||
|
)
|
||||||
|
val tracks = backupManager.parser.fromJson<List<TrackImpl>>(
|
||||||
|
mangaJson.get(Backup.TRACK)
|
||||||
|
?: JsonArray()
|
||||||
|
)
|
||||||
|
|
||||||
// EXH -->
|
// EXH -->
|
||||||
EXHMigrations.migrateBackupEntry(manga)
|
manga = EXHMigrations.migrateBackupEntry(manga)
|
||||||
// <-- EXH
|
// <-- EXH
|
||||||
|
|
||||||
val source = backupManager.sourceManager.get(manga.source)
|
val source = backupManager.sourceManager.get(manga.source)
|
||||||
|
|||||||
+24
-24
@@ -2,14 +2,14 @@ package eu.kanade.tachiyomi.data.backup.legacy
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import com.google.gson.JsonObject
|
||||||
|
import com.google.gson.JsonParser
|
||||||
|
import com.google.gson.stream.JsonReader
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.backup.AbstractBackupRestoreValidator
|
import eu.kanade.tachiyomi.data.backup.AbstractBackupRestoreValidator
|
||||||
import eu.kanade.tachiyomi.data.backup.ValidatorParseException
|
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup
|
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup
|
||||||
import kotlinx.serialization.json.decodeFromStream
|
|
||||||
|
|
||||||
class LegacyBackupRestoreValidator : AbstractBackupRestoreValidator() {
|
class LegacyBackupRestoreValidator : AbstractBackupRestoreValidator() {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks for critical backup file data.
|
* Checks for critical backup file data.
|
||||||
*
|
*
|
||||||
@@ -17,34 +17,30 @@ class LegacyBackupRestoreValidator : AbstractBackupRestoreValidator() {
|
|||||||
* @return List of missing sources or missing trackers.
|
* @return List of missing sources or missing trackers.
|
||||||
*/
|
*/
|
||||||
override fun validate(context: Context, uri: Uri): Results {
|
override fun validate(context: Context, uri: Uri): Results {
|
||||||
val backupManager = LegacyBackupManager(context)
|
val reader = JsonReader(context.contentResolver.openInputStream(uri)!!.bufferedReader())
|
||||||
|
val json = JsonParser.parseReader(reader).asJsonObject
|
||||||
|
|
||||||
val backup = try {
|
val version = json.get(Backup.VERSION)
|
||||||
backupManager.parser.decodeFromStream<Backup>(
|
val mangasJson = json.get(Backup.MANGAS)
|
||||||
context.contentResolver.openInputStream(uri)!!
|
if (version == null || mangasJson == null) {
|
||||||
)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
throw ValidatorParseException(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (backup.version == null) {
|
|
||||||
throw Exception(context.getString(R.string.invalid_backup_file_missing_data))
|
throw Exception(context.getString(R.string.invalid_backup_file_missing_data))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (backup.mangas.isEmpty()) {
|
val mangas = mangasJson.asJsonArray
|
||||||
|
if (mangas.size() == 0) {
|
||||||
throw Exception(context.getString(R.string.invalid_backup_file_missing_manga))
|
throw Exception(context.getString(R.string.invalid_backup_file_missing_manga))
|
||||||
}
|
}
|
||||||
|
|
||||||
val sources = getSourceMapping(backup.extensions ?: emptyList())
|
val sources = getSourceMapping(json)
|
||||||
val missingSources = sources
|
val missingSources = sources
|
||||||
.filter { sourceManager.get(it.key) == null }
|
.filter { sourceManager.get(it.key) == null }
|
||||||
.values
|
.values
|
||||||
.sorted()
|
.sorted()
|
||||||
|
|
||||||
val trackers = backup.mangas
|
val trackers = mangas
|
||||||
.filterNot { it.track.isNullOrEmpty() }
|
.filter { it.asJsonObject.has("track") }
|
||||||
.flatMap { it.track ?: emptyList() }
|
.flatMap { it.asJsonObject["track"].asJsonArray }
|
||||||
.map { it.sync_id }
|
.map { it.asJsonObject["s"].asInt }
|
||||||
.distinct()
|
.distinct()
|
||||||
val missingTrackers = trackers
|
val missingTrackers = trackers
|
||||||
.mapNotNull { trackManager.getService(it) }
|
.mapNotNull { trackManager.getService(it) }
|
||||||
@@ -56,11 +52,15 @@ class LegacyBackupRestoreValidator : AbstractBackupRestoreValidator() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun getSourceMapping(extensionsMapping: List<String>): Map<Long, String> {
|
fun getSourceMapping(json: JsonObject): Map<Long, String> {
|
||||||
return extensionsMapping.associate {
|
val extensionsMapping = json.get(Backup.EXTENSIONS) ?: return emptyMap()
|
||||||
val items = it.split(":")
|
|
||||||
items[0].toLong() to items[1]
|
return extensionsMapping.asJsonArray
|
||||||
}
|
.map {
|
||||||
|
val items = it.asString.split(":")
|
||||||
|
items[0].toLong() to items[1]
|
||||||
|
}
|
||||||
|
.toMap()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,43 +1,30 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.legacy.models
|
package eu.kanade.tachiyomi.data.backup.legacy.models
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Category
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
|
||||||
import exh.merged.sql.models.MergedMangaReference
|
|
||||||
import kotlinx.serialization.Contextual
|
|
||||||
import kotlinx.serialization.SerialName
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
@Serializable
|
/**
|
||||||
data class Backup(
|
* Json values
|
||||||
val version: Int? = null,
|
*/
|
||||||
var mangas: MutableList<MangaObject> = mutableListOf(),
|
object Backup {
|
||||||
var categories: List<@Contextual Category>? = null,
|
const val CURRENT_VERSION = 2
|
||||||
var extensions: List<String>? = null,
|
const val MANGA = "manga"
|
||||||
// SY Specific values
|
const val MANGAS = "mangas"
|
||||||
@SerialName("mergedmangareferences")
|
const val TRACK = "track"
|
||||||
var mergedMangaReferences: List<@Contextual MergedMangaReference>? = null,
|
const val CHAPTERS = "chapters"
|
||||||
var savedSearches: String? = null
|
const val CATEGORIES = "categories"
|
||||||
) {
|
const val EXTENSIONS = "extensions"
|
||||||
companion object {
|
const val HISTORY = "history"
|
||||||
const val CURRENT_VERSION = 2
|
const val VERSION = "version"
|
||||||
|
|
||||||
fun getDefaultFilename(): String {
|
// SY -->
|
||||||
val date = SimpleDateFormat("yyyy-MM-dd_HH-mm", Locale.getDefault()).format(Date())
|
const val SAVEDSEARCHES = "savedsearches"
|
||||||
return "tachiyomi_$date.json"
|
const val MERGEDMANGAREFERENCES = "mergedmangareferences"
|
||||||
}
|
// SY <--
|
||||||
|
|
||||||
|
fun getDefaultFilename(): String {
|
||||||
|
val date = SimpleDateFormat("yyyy-MM-dd_HH-mm", Locale.getDefault()).format(Date())
|
||||||
|
return "tachiyomi_$date.json"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class MangaObject(
|
|
||||||
var manga: @Contextual Manga,
|
|
||||||
var chapters: List<@Contextual Chapter>? = null,
|
|
||||||
var categories: List<String>? = null,
|
|
||||||
var track: List<@Contextual Track>? = null,
|
|
||||||
var history: List<@Contextual DHistory>? = null
|
|
||||||
)
|
|
||||||
|
|||||||
+31
@@ -0,0 +1,31 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.legacy.serializer
|
||||||
|
|
||||||
|
import com.github.salomonbrys.kotson.typeAdapter
|
||||||
|
import com.google.gson.TypeAdapter
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.CategoryImpl
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON Serializer used to write / read [CategoryImpl] to / from json
|
||||||
|
*/
|
||||||
|
object CategoryTypeAdapter {
|
||||||
|
|
||||||
|
fun build(): TypeAdapter<CategoryImpl> {
|
||||||
|
return typeAdapter {
|
||||||
|
write {
|
||||||
|
beginArray()
|
||||||
|
value(it.name)
|
||||||
|
value(it.order)
|
||||||
|
endArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
read {
|
||||||
|
beginArray()
|
||||||
|
val category = CategoryImpl()
|
||||||
|
category.name = nextString()
|
||||||
|
category.order = nextInt()
|
||||||
|
endArray()
|
||||||
|
category
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
-49
@@ -1,49 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.legacy.serializer
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Category
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.CategoryImpl
|
|
||||||
import kotlinx.serialization.KSerializer
|
|
||||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
|
||||||
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
|
|
||||||
import kotlinx.serialization.encoding.Decoder
|
|
||||||
import kotlinx.serialization.encoding.Encoder
|
|
||||||
import kotlinx.serialization.json.JsonDecoder
|
|
||||||
import kotlinx.serialization.json.JsonEncoder
|
|
||||||
import kotlinx.serialization.json.add
|
|
||||||
import kotlinx.serialization.json.buildJsonArray
|
|
||||||
import kotlinx.serialization.json.int
|
|
||||||
import kotlinx.serialization.json.jsonArray
|
|
||||||
import kotlinx.serialization.json.jsonPrimitive
|
|
||||||
|
|
||||||
/**
|
|
||||||
* JSON Serializer used to write / read [CategoryImpl] to / from json
|
|
||||||
*/
|
|
||||||
open class CategoryBaseSerializer<T : Category> : KSerializer<T> {
|
|
||||||
override val descriptor: SerialDescriptor = buildClassSerialDescriptor("Category")
|
|
||||||
|
|
||||||
override fun serialize(encoder: Encoder, value: T) {
|
|
||||||
encoder as JsonEncoder
|
|
||||||
encoder.encodeJsonElement(
|
|
||||||
buildJsonArray {
|
|
||||||
add(value.name)
|
|
||||||
add(value.order)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
override fun deserialize(decoder: Decoder): T {
|
|
||||||
// make a category impl and cast as T so that the serializer accepts it
|
|
||||||
return CategoryImpl().apply {
|
|
||||||
decoder as JsonDecoder
|
|
||||||
val array = decoder.decodeJsonElement().jsonArray
|
|
||||||
name = array[0].jsonPrimitive.content
|
|
||||||
order = array[1].jsonPrimitive.int
|
|
||||||
} as T
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Allow for serialization of a category and category impl
|
|
||||||
object CategoryTypeSerializer : CategoryBaseSerializer<Category>()
|
|
||||||
|
|
||||||
object CategoryImplTypeSerializer : CategoryBaseSerializer<CategoryImpl>()
|
|
||||||
+59
@@ -0,0 +1,59 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.legacy.serializer
|
||||||
|
|
||||||
|
import com.github.salomonbrys.kotson.typeAdapter
|
||||||
|
import com.google.gson.TypeAdapter
|
||||||
|
import com.google.gson.stream.JsonToken
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.ChapterImpl
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON Serializer used to write / read [ChapterImpl] to / from json
|
||||||
|
*/
|
||||||
|
object ChapterTypeAdapter {
|
||||||
|
|
||||||
|
private const val URL = "u"
|
||||||
|
private const val READ = "r"
|
||||||
|
private const val BOOKMARK = "b"
|
||||||
|
private const val LAST_READ = "l"
|
||||||
|
|
||||||
|
fun build(): TypeAdapter<ChapterImpl> {
|
||||||
|
return typeAdapter {
|
||||||
|
write {
|
||||||
|
if (it.read || it.bookmark || it.last_page_read != 0) {
|
||||||
|
beginObject()
|
||||||
|
name(URL)
|
||||||
|
value(it.url)
|
||||||
|
if (it.read) {
|
||||||
|
name(READ)
|
||||||
|
value(1)
|
||||||
|
}
|
||||||
|
if (it.bookmark) {
|
||||||
|
name(BOOKMARK)
|
||||||
|
value(1)
|
||||||
|
}
|
||||||
|
if (it.last_page_read != 0) {
|
||||||
|
name(LAST_READ)
|
||||||
|
value(it.last_page_read)
|
||||||
|
}
|
||||||
|
endObject()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
read {
|
||||||
|
val chapter = ChapterImpl()
|
||||||
|
beginObject()
|
||||||
|
while (hasNext()) {
|
||||||
|
if (peek() == JsonToken.NAME) {
|
||||||
|
when (nextName()) {
|
||||||
|
URL -> chapter.url = nextString()
|
||||||
|
READ -> chapter.read = nextInt() == 1
|
||||||
|
BOOKMARK -> chapter.bookmark = nextInt() == 1
|
||||||
|
LAST_READ -> chapter.last_page_read = nextInt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
endObject()
|
||||||
|
chapter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
-66
@@ -1,66 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.legacy.serializer
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.ChapterImpl
|
|
||||||
import kotlinx.serialization.KSerializer
|
|
||||||
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
|
|
||||||
import kotlinx.serialization.encoding.Decoder
|
|
||||||
import kotlinx.serialization.encoding.Encoder
|
|
||||||
import kotlinx.serialization.json.JsonDecoder
|
|
||||||
import kotlinx.serialization.json.JsonEncoder
|
|
||||||
import kotlinx.serialization.json.buildJsonObject
|
|
||||||
import kotlinx.serialization.json.intOrNull
|
|
||||||
import kotlinx.serialization.json.jsonObject
|
|
||||||
import kotlinx.serialization.json.jsonPrimitive
|
|
||||||
import kotlinx.serialization.json.put
|
|
||||||
|
|
||||||
/**
|
|
||||||
* JSON Serializer used to write / read [ChapterImpl] to / from json
|
|
||||||
*/
|
|
||||||
open class ChapterBaseSerializer<T : Chapter> : KSerializer<T> {
|
|
||||||
|
|
||||||
override val descriptor = buildClassSerialDescriptor("Chapter")
|
|
||||||
|
|
||||||
override fun serialize(encoder: Encoder, value: T) {
|
|
||||||
encoder as JsonEncoder
|
|
||||||
encoder.encodeJsonElement(
|
|
||||||
buildJsonObject {
|
|
||||||
put(URL, value.url)
|
|
||||||
if (value.read) {
|
|
||||||
put(READ, 1)
|
|
||||||
}
|
|
||||||
if (value.bookmark) {
|
|
||||||
put(BOOKMARK, 1)
|
|
||||||
}
|
|
||||||
if (value.last_page_read != 0) {
|
|
||||||
put(LAST_READ, value.last_page_read)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
override fun deserialize(decoder: Decoder): T {
|
|
||||||
// make a chapter impl and cast as T so that the serializer accepts it
|
|
||||||
return ChapterImpl().apply {
|
|
||||||
decoder as JsonDecoder
|
|
||||||
val jsonObject = decoder.decodeJsonElement().jsonObject
|
|
||||||
url = jsonObject[URL]!!.jsonPrimitive.content
|
|
||||||
read = jsonObject[READ]?.jsonPrimitive?.intOrNull == 1
|
|
||||||
bookmark = jsonObject[BOOKMARK]?.jsonPrimitive?.intOrNull == 1
|
|
||||||
last_page_read = jsonObject[LAST_READ]?.jsonPrimitive?.intOrNull ?: last_page_read
|
|
||||||
} as T
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val URL = "u"
|
|
||||||
private const val READ = "r"
|
|
||||||
private const val BOOKMARK = "b"
|
|
||||||
private const val LAST_READ = "l"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Allow for serialization of a chapter and chapter impl
|
|
||||||
object ChapterTypeSerializer : ChapterBaseSerializer<Chapter>()
|
|
||||||
|
|
||||||
object ChapterImplTypeSerializer : ChapterBaseSerializer<ChapterImpl>()
|
|
||||||
+32
@@ -0,0 +1,32 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.legacy.serializer
|
||||||
|
|
||||||
|
import com.github.salomonbrys.kotson.typeAdapter
|
||||||
|
import com.google.gson.TypeAdapter
|
||||||
|
import eu.kanade.tachiyomi.data.backup.legacy.models.DHistory
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON Serializer used to write / read [DHistory] to / from json
|
||||||
|
*/
|
||||||
|
object HistoryTypeAdapter {
|
||||||
|
|
||||||
|
fun build(): TypeAdapter<DHistory> {
|
||||||
|
return typeAdapter {
|
||||||
|
write {
|
||||||
|
if (it.lastRead != 0L) {
|
||||||
|
beginArray()
|
||||||
|
value(it.url)
|
||||||
|
value(it.lastRead)
|
||||||
|
endArray()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
read {
|
||||||
|
beginArray()
|
||||||
|
val url = nextString()
|
||||||
|
val lastRead = nextLong()
|
||||||
|
endArray()
|
||||||
|
DHistory(url, lastRead)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
-41
@@ -1,41 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.legacy.serializer
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.models.DHistory
|
|
||||||
import kotlinx.serialization.KSerializer
|
|
||||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
|
||||||
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
|
|
||||||
import kotlinx.serialization.encoding.Decoder
|
|
||||||
import kotlinx.serialization.encoding.Encoder
|
|
||||||
import kotlinx.serialization.json.JsonDecoder
|
|
||||||
import kotlinx.serialization.json.JsonEncoder
|
|
||||||
import kotlinx.serialization.json.add
|
|
||||||
import kotlinx.serialization.json.buildJsonArray
|
|
||||||
import kotlinx.serialization.json.jsonArray
|
|
||||||
import kotlinx.serialization.json.jsonPrimitive
|
|
||||||
import kotlinx.serialization.json.long
|
|
||||||
|
|
||||||
/**
|
|
||||||
* JSON Serializer used to write / read [DHistory] to / from json
|
|
||||||
*/
|
|
||||||
object HistoryTypeSerializer : KSerializer<DHistory> {
|
|
||||||
override val descriptor: SerialDescriptor = buildClassSerialDescriptor("History")
|
|
||||||
|
|
||||||
override fun serialize(encoder: Encoder, value: DHistory) {
|
|
||||||
encoder as JsonEncoder
|
|
||||||
encoder.encodeJsonElement(
|
|
||||||
buildJsonArray {
|
|
||||||
add(value.url)
|
|
||||||
add(value.lastRead)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun deserialize(decoder: Decoder): DHistory {
|
|
||||||
decoder as JsonDecoder
|
|
||||||
val array = decoder.decodeJsonElement().jsonArray
|
|
||||||
return DHistory(
|
|
||||||
url = array[0].jsonPrimitive.content,
|
|
||||||
lastRead = array[1].jsonPrimitive.long
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+39
@@ -0,0 +1,39 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.legacy.serializer
|
||||||
|
|
||||||
|
import com.github.salomonbrys.kotson.typeAdapter
|
||||||
|
import com.google.gson.TypeAdapter
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.MangaImpl
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON Serializer used to write / read [MangaImpl] to / from json
|
||||||
|
*/
|
||||||
|
object MangaTypeAdapter {
|
||||||
|
|
||||||
|
fun build(): TypeAdapter<MangaImpl> {
|
||||||
|
return typeAdapter {
|
||||||
|
write {
|
||||||
|
beginArray()
|
||||||
|
value(it.url)
|
||||||
|
// SY -->
|
||||||
|
value(it.originalTitle)
|
||||||
|
// SY <--
|
||||||
|
value(it.source)
|
||||||
|
value(it.viewer)
|
||||||
|
value(it.chapter_flags)
|
||||||
|
endArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
read {
|
||||||
|
beginArray()
|
||||||
|
val manga = MangaImpl()
|
||||||
|
manga.url = nextString()
|
||||||
|
manga.title = nextString()
|
||||||
|
manga.source = nextLong()
|
||||||
|
manga.viewer = nextInt()
|
||||||
|
manga.chapter_flags = nextInt()
|
||||||
|
endArray()
|
||||||
|
manga
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
-56
@@ -1,56 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.legacy.serializer
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaImpl
|
|
||||||
import kotlinx.serialization.KSerializer
|
|
||||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
|
||||||
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
|
|
||||||
import kotlinx.serialization.encoding.Decoder
|
|
||||||
import kotlinx.serialization.encoding.Encoder
|
|
||||||
import kotlinx.serialization.json.JsonDecoder
|
|
||||||
import kotlinx.serialization.json.JsonEncoder
|
|
||||||
import kotlinx.serialization.json.add
|
|
||||||
import kotlinx.serialization.json.buildJsonArray
|
|
||||||
import kotlinx.serialization.json.int
|
|
||||||
import kotlinx.serialization.json.jsonArray
|
|
||||||
import kotlinx.serialization.json.jsonPrimitive
|
|
||||||
import kotlinx.serialization.json.long
|
|
||||||
|
|
||||||
/**
|
|
||||||
* JSON Serializer used to write / read [MangaImpl] to / from json
|
|
||||||
*/
|
|
||||||
open class MangaBaseSerializer<T : Manga> : KSerializer<T> {
|
|
||||||
override val descriptor: SerialDescriptor = buildClassSerialDescriptor("Manga")
|
|
||||||
|
|
||||||
override fun serialize(encoder: Encoder, value: T) {
|
|
||||||
encoder as JsonEncoder
|
|
||||||
encoder.encodeJsonElement(
|
|
||||||
buildJsonArray {
|
|
||||||
add(value.url)
|
|
||||||
add(value.title)
|
|
||||||
add(value.source)
|
|
||||||
add(value.viewer_flags)
|
|
||||||
add(value.chapter_flags)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
override fun deserialize(decoder: Decoder): T {
|
|
||||||
// make a manga impl and cast as T so that the serializer accepts it
|
|
||||||
return MangaImpl().apply {
|
|
||||||
decoder as JsonDecoder
|
|
||||||
val array = decoder.decodeJsonElement().jsonArray
|
|
||||||
url = array[0].jsonPrimitive.content
|
|
||||||
title = array[1].jsonPrimitive.content
|
|
||||||
source = array[2].jsonPrimitive.long
|
|
||||||
viewer_flags = array[3].jsonPrimitive.int
|
|
||||||
chapter_flags = array[4].jsonPrimitive.int
|
|
||||||
} as T
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Allow for serialization of a manga and manga impl
|
|
||||||
object MangaTypeSerializer : MangaBaseSerializer<Manga>()
|
|
||||||
|
|
||||||
object MangaImplTypeSerializer : MangaBaseSerializer<MangaImpl>()
|
|
||||||
+45
@@ -0,0 +1,45 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.legacy.serializer
|
||||||
|
|
||||||
|
import com.github.salomonbrys.kotson.typeAdapter
|
||||||
|
import com.google.gson.TypeAdapter
|
||||||
|
import exh.merged.sql.models.MergedMangaReference
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON Serializer used to write / read [MergedMangaReference] to / from json
|
||||||
|
*/
|
||||||
|
object MergedMangaReferenceTypeAdapter {
|
||||||
|
|
||||||
|
fun build(): TypeAdapter<MergedMangaReference> {
|
||||||
|
return typeAdapter {
|
||||||
|
write {
|
||||||
|
beginArray()
|
||||||
|
value(it.mangaUrl)
|
||||||
|
value(it.mergeUrl)
|
||||||
|
value(it.mangaSourceId)
|
||||||
|
value(it.chapterSortMode)
|
||||||
|
value(it.chapterPriority)
|
||||||
|
value(it.getChapterUpdates)
|
||||||
|
value(it.isInfoManga)
|
||||||
|
value(it.downloadChapters)
|
||||||
|
endArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
read {
|
||||||
|
beginArray()
|
||||||
|
MergedMangaReference(
|
||||||
|
id = null,
|
||||||
|
mangaUrl = nextString(),
|
||||||
|
mergeUrl = nextString(),
|
||||||
|
mangaSourceId = nextLong(),
|
||||||
|
chapterSortMode = nextInt(),
|
||||||
|
chapterPriority = nextInt(),
|
||||||
|
getChapterUpdates = nextBoolean(),
|
||||||
|
isInfoManga = nextBoolean(),
|
||||||
|
downloadChapters = nextBoolean(),
|
||||||
|
mangaId = null,
|
||||||
|
mergeId = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
-58
@@ -1,58 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.legacy.serializer
|
|
||||||
|
|
||||||
import exh.merged.sql.models.MergedMangaReference
|
|
||||||
import kotlinx.serialization.KSerializer
|
|
||||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
|
||||||
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
|
|
||||||
import kotlinx.serialization.encoding.Decoder
|
|
||||||
import kotlinx.serialization.encoding.Encoder
|
|
||||||
import kotlinx.serialization.json.JsonDecoder
|
|
||||||
import kotlinx.serialization.json.JsonEncoder
|
|
||||||
import kotlinx.serialization.json.add
|
|
||||||
import kotlinx.serialization.json.boolean
|
|
||||||
import kotlinx.serialization.json.buildJsonArray
|
|
||||||
import kotlinx.serialization.json.int
|
|
||||||
import kotlinx.serialization.json.jsonArray
|
|
||||||
import kotlinx.serialization.json.jsonPrimitive
|
|
||||||
import kotlinx.serialization.json.long
|
|
||||||
|
|
||||||
/**
|
|
||||||
* JSON Serializer used to write / read [MergedMangaReference] to / from json
|
|
||||||
*/
|
|
||||||
object MergedMangaTypeSerializer : KSerializer<MergedMangaReference> {
|
|
||||||
override val descriptor: SerialDescriptor = buildClassSerialDescriptor("Manga")
|
|
||||||
|
|
||||||
override fun serialize(encoder: Encoder, value: MergedMangaReference) {
|
|
||||||
encoder as JsonEncoder
|
|
||||||
encoder.encodeJsonElement(
|
|
||||||
buildJsonArray {
|
|
||||||
add(value.mangaUrl)
|
|
||||||
add(value.mergeUrl)
|
|
||||||
add(value.mangaSourceId)
|
|
||||||
add(value.chapterSortMode)
|
|
||||||
add(value.chapterPriority)
|
|
||||||
add(value.getChapterUpdates)
|
|
||||||
add(value.isInfoManga)
|
|
||||||
add(value.downloadChapters)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun deserialize(decoder: Decoder): MergedMangaReference {
|
|
||||||
decoder as JsonDecoder
|
|
||||||
val array = decoder.decodeJsonElement().jsonArray
|
|
||||||
return MergedMangaReference(
|
|
||||||
id = null,
|
|
||||||
mangaUrl = array[0].jsonPrimitive.content,
|
|
||||||
mergeUrl = array[1].jsonPrimitive.content,
|
|
||||||
mangaSourceId = array[2].jsonPrimitive.long,
|
|
||||||
chapterSortMode = array[3].jsonPrimitive.int,
|
|
||||||
chapterPriority = array[4].jsonPrimitive.int,
|
|
||||||
getChapterUpdates = array[5].jsonPrimitive.boolean,
|
|
||||||
isInfoManga = array[6].jsonPrimitive.boolean,
|
|
||||||
downloadChapters = array[7].jsonPrimitive.boolean,
|
|
||||||
mangaId = null,
|
|
||||||
mergeId = null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+59
@@ -0,0 +1,59 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.legacy.serializer
|
||||||
|
|
||||||
|
import com.github.salomonbrys.kotson.typeAdapter
|
||||||
|
import com.google.gson.TypeAdapter
|
||||||
|
import com.google.gson.stream.JsonToken
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.TrackImpl
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON Serializer used to write / read [TrackImpl] to / from json
|
||||||
|
*/
|
||||||
|
object TrackTypeAdapter {
|
||||||
|
|
||||||
|
private const val SYNC = "s"
|
||||||
|
private const val MEDIA = "r"
|
||||||
|
private const val LIBRARY = "ml"
|
||||||
|
private const val TITLE = "t"
|
||||||
|
private const val LAST_READ = "l"
|
||||||
|
private const val TRACKING_URL = "u"
|
||||||
|
|
||||||
|
fun build(): TypeAdapter<TrackImpl> {
|
||||||
|
return typeAdapter {
|
||||||
|
write {
|
||||||
|
beginObject()
|
||||||
|
name(TITLE)
|
||||||
|
value(it.title)
|
||||||
|
name(SYNC)
|
||||||
|
value(it.sync_id)
|
||||||
|
name(MEDIA)
|
||||||
|
value(it.media_id)
|
||||||
|
name(LIBRARY)
|
||||||
|
value(it.library_id)
|
||||||
|
name(LAST_READ)
|
||||||
|
value(it.last_chapter_read)
|
||||||
|
name(TRACKING_URL)
|
||||||
|
value(it.tracking_url)
|
||||||
|
endObject()
|
||||||
|
}
|
||||||
|
|
||||||
|
read {
|
||||||
|
val track = TrackImpl()
|
||||||
|
beginObject()
|
||||||
|
while (hasNext()) {
|
||||||
|
if (peek() == JsonToken.NAME) {
|
||||||
|
when (nextName()) {
|
||||||
|
TITLE -> track.title = nextString()
|
||||||
|
SYNC -> track.sync_id = nextInt()
|
||||||
|
MEDIA -> track.media_id = nextInt()
|
||||||
|
LIBRARY -> track.library_id = nextLong()
|
||||||
|
LAST_READ -> track.last_chapter_read = nextInt()
|
||||||
|
TRACKING_URL -> track.tracking_url = nextString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
endObject()
|
||||||
|
track
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
-68
@@ -1,68 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.legacy.serializer
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.TrackImpl
|
|
||||||
import kotlinx.serialization.KSerializer
|
|
||||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
|
||||||
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
|
|
||||||
import kotlinx.serialization.encoding.Decoder
|
|
||||||
import kotlinx.serialization.encoding.Encoder
|
|
||||||
import kotlinx.serialization.json.JsonDecoder
|
|
||||||
import kotlinx.serialization.json.JsonEncoder
|
|
||||||
import kotlinx.serialization.json.buildJsonObject
|
|
||||||
import kotlinx.serialization.json.float
|
|
||||||
import kotlinx.serialization.json.int
|
|
||||||
import kotlinx.serialization.json.jsonObject
|
|
||||||
import kotlinx.serialization.json.jsonPrimitive
|
|
||||||
import kotlinx.serialization.json.long
|
|
||||||
import kotlinx.serialization.json.put
|
|
||||||
|
|
||||||
/**
|
|
||||||
* JSON Serializer used to write / read [TrackImpl] to / from json
|
|
||||||
*/
|
|
||||||
open class TrackBaseSerializer<T : Track> : KSerializer<T> {
|
|
||||||
override val descriptor: SerialDescriptor = buildClassSerialDescriptor("Track")
|
|
||||||
|
|
||||||
override fun serialize(encoder: Encoder, value: T) {
|
|
||||||
encoder as JsonEncoder
|
|
||||||
encoder.encodeJsonElement(
|
|
||||||
buildJsonObject {
|
|
||||||
put(TITLE, value.title)
|
|
||||||
put(SYNC, value.sync_id)
|
|
||||||
put(MEDIA, value.media_id)
|
|
||||||
put(LIBRARY, value.library_id)
|
|
||||||
put(LAST_READ, value.last_chapter_read)
|
|
||||||
put(TRACKING_URL, value.tracking_url)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
override fun deserialize(decoder: Decoder): T {
|
|
||||||
// make a track impl and cast as T so that the serializer accepts it
|
|
||||||
return TrackImpl().apply {
|
|
||||||
decoder as JsonDecoder
|
|
||||||
val jsonObject = decoder.decodeJsonElement().jsonObject
|
|
||||||
title = jsonObject[TITLE]!!.jsonPrimitive.content
|
|
||||||
sync_id = jsonObject[SYNC]!!.jsonPrimitive.int
|
|
||||||
media_id = jsonObject[MEDIA]!!.jsonPrimitive.int
|
|
||||||
library_id = jsonObject[LIBRARY]!!.jsonPrimitive.long
|
|
||||||
last_chapter_read = jsonObject[LAST_READ]!!.jsonPrimitive.float
|
|
||||||
tracking_url = jsonObject[TRACKING_URL]!!.jsonPrimitive.content
|
|
||||||
} as T
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val SYNC = "s"
|
|
||||||
private const val MEDIA = "r"
|
|
||||||
private const val LIBRARY = "ml"
|
|
||||||
private const val TITLE = "t"
|
|
||||||
private const val LAST_READ = "l"
|
|
||||||
private const val TRACKING_URL = "u"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Allow for serialization of a track and track impl
|
|
||||||
object TrackTypeSerializer : TrackBaseSerializer<Track>()
|
|
||||||
|
|
||||||
object TrackImplTypeSerializer : TrackBaseSerializer<TrackImpl>()
|
|
||||||
@@ -2,6 +2,8 @@ package eu.kanade.tachiyomi.data.cache
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.text.format.Formatter
|
import android.text.format.Formatter
|
||||||
|
import com.github.salomonbrys.kotson.fromJson
|
||||||
|
import com.google.gson.Gson
|
||||||
import com.jakewharton.disklrucache.DiskLruCache
|
import com.jakewharton.disklrucache.DiskLruCache
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
@@ -13,12 +15,10 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.serialization.decodeFromString
|
|
||||||
import kotlinx.serialization.encodeToString
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import okio.buffer
|
import okio.buffer
|
||||||
import okio.sink
|
import okio.sink
|
||||||
|
import rx.Observable
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
@@ -48,12 +48,14 @@ class ChapterCache(private val context: Context) {
|
|||||||
private val scope = CoroutineScope(Job() + Dispatchers.Main)
|
private val scope = CoroutineScope(Job() + Dispatchers.Main)
|
||||||
|
|
||||||
/** Google Json class used for parsing JSON files. */
|
/** Google Json class used for parsing JSON files. */
|
||||||
private val json: Json by injectLazy()
|
private val gson: Gson by injectLazy()
|
||||||
|
|
||||||
// --> EH
|
// --> EH
|
||||||
private val prefs: PreferencesHelper by injectLazy()
|
private val prefs: PreferencesHelper by injectLazy()
|
||||||
|
// <-- EH
|
||||||
|
|
||||||
/** Cache class used for cache management. */
|
/** Cache class used for cache management. */
|
||||||
|
// --> EH
|
||||||
private var diskCache = setupDiskCache(prefs.cacheSize().get().toLong())
|
private var diskCache = setupDiskCache(prefs.cacheSize().get().toLong())
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@@ -71,7 +73,7 @@ class ChapterCache(private val context: Context) {
|
|||||||
/**
|
/**
|
||||||
* Returns directory of cache.
|
* Returns directory of cache.
|
||||||
*/
|
*/
|
||||||
private val cacheDir: File
|
val cacheDir: File
|
||||||
get() = diskCache.directory
|
get() = diskCache.directory
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -98,19 +100,43 @@ class ChapterCache(private val context: Context) {
|
|||||||
}
|
}
|
||||||
// <-- EH
|
// <-- 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.
|
* Get page list from cache.
|
||||||
*
|
*
|
||||||
* @param chapter the chapter.
|
* @param chapter the chapter.
|
||||||
* @return the list of pages.
|
* @return an observable of the list of pages.
|
||||||
*/
|
*/
|
||||||
fun getPageListFromCache(chapter: Chapter): List<Page> {
|
fun getPageListFromCache(chapter: Chapter): Observable<List<Page>> {
|
||||||
// Get the key for the chapter.
|
return Observable.fromCallable {
|
||||||
val key = DiskUtil.hashKeyForDisk(getKey(chapter))
|
// 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
|
// Convert JSON string to list of objects. Throws an exception if snapshot is null
|
||||||
return diskCache.get(key).use {
|
diskCache.get(key).use {
|
||||||
json.decodeFromString(it.getString(0))
|
gson.fromJson<List<Page>>(it.getString(0))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,7 +148,7 @@ class ChapterCache(private val context: Context) {
|
|||||||
*/
|
*/
|
||||||
fun putPageListToCache(chapter: Chapter, pages: List<Page>) {
|
fun putPageListToCache(chapter: Chapter, pages: List<Page>) {
|
||||||
// Convert list of pages to json string.
|
// Convert list of pages to json string.
|
||||||
val cachedValue = json.encodeToString(pages)
|
val cachedValue = gson.toJson(pages)
|
||||||
|
|
||||||
// Initialize the editor (edits the values for an entry).
|
// Initialize the editor (edits the values for an entry).
|
||||||
var editor: DiskLruCache.Editor? = null
|
var editor: DiskLruCache.Editor? = null
|
||||||
@@ -202,38 +228,6 @@ 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 {
|
private fun getKey(chapter: Chapter): String {
|
||||||
return "${chapter.manga_id}${chapter.url}"
|
return "${chapter.manga_id}${chapter.url}"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.data.cache
|
package eu.kanade.tachiyomi.data.cache
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import coil.imageLoader
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@@ -100,13 +99,6 @@ class CoverCache(private val context: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear coil's memory cache.
|
|
||||||
*/
|
|
||||||
fun clearMemoryCache() {
|
|
||||||
context.imageLoader.memoryCache.clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getCacheDir(dir: String): File {
|
private fun getCacheDir(dir: String): File {
|
||||||
return context.getExternalFilesDir(dir)
|
return context.getExternalFilesDir(dir)
|
||||||
?: File(context.filesDir, dir).also { it.mkdirs() }
|
?: File(context.filesDir, dir).also { it.mkdirs() }
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.coil
|
|
||||||
|
|
||||||
import coil.bitmap.BitmapPool
|
|
||||||
import coil.decode.DataSource
|
|
||||||
import coil.decode.Options
|
|
||||||
import coil.fetch.FetchResult
|
|
||||||
import coil.fetch.Fetcher
|
|
||||||
import coil.fetch.SourceResult
|
|
||||||
import coil.size.Size
|
|
||||||
import okio.buffer
|
|
||||||
import okio.source
|
|
||||||
import java.io.ByteArrayInputStream
|
|
||||||
import java.nio.ByteBuffer
|
|
||||||
|
|
||||||
class ByteBufferFetcher : Fetcher<ByteBuffer> {
|
|
||||||
override suspend fun fetch(pool: BitmapPool, data: ByteBuffer, size: Size, options: Options): FetchResult {
|
|
||||||
return SourceResult(
|
|
||||||
source = ByteArrayInputStream(data.array()).source().buffer(),
|
|
||||||
mimeType = null,
|
|
||||||
dataSource = DataSource.MEMORY
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun key(data: ByteBuffer): String? = null
|
|
||||||
}
|
|
||||||
@@ -1,168 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.coil
|
|
||||||
|
|
||||||
import coil.bitmap.BitmapPool
|
|
||||||
import coil.decode.DataSource
|
|
||||||
import coil.decode.Options
|
|
||||||
import coil.fetch.FetchResult
|
|
||||||
import coil.fetch.Fetcher
|
|
||||||
import coil.fetch.SourceResult
|
|
||||||
import coil.network.HttpException
|
|
||||||
import coil.request.get
|
|
||||||
import coil.size.Size
|
|
||||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
|
||||||
import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher.Companion.USE_CUSTOM_COVER
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
|
||||||
import eu.kanade.tachiyomi.network.await
|
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
|
||||||
import okhttp3.CacheControl
|
|
||||||
import okhttp3.Call
|
|
||||||
import okhttp3.Request
|
|
||||||
import okhttp3.Response
|
|
||||||
import okhttp3.ResponseBody
|
|
||||||
import okio.buffer
|
|
||||||
import okio.sink
|
|
||||||
import okio.source
|
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Coil component that fetches [Manga] cover while using the cached file in disk when available.
|
|
||||||
*
|
|
||||||
* Available request parameter:
|
|
||||||
* - [USE_CUSTOM_COVER]: Use custom cover if set by user, default is true
|
|
||||||
*/
|
|
||||||
class MangaCoverFetcher : Fetcher<Manga> {
|
|
||||||
private val coverCache: CoverCache by injectLazy()
|
|
||||||
private val sourceManager: SourceManager by injectLazy()
|
|
||||||
private val defaultClient = Injekt.get<NetworkHelper>().coilClient
|
|
||||||
|
|
||||||
override fun key(data: Manga): String? {
|
|
||||||
if (data.thumbnail_url.isNullOrBlank()) return null
|
|
||||||
return data.thumbnail_url!!
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun fetch(pool: BitmapPool, data: Manga, size: Size, options: Options): FetchResult {
|
|
||||||
// Use custom cover if exists
|
|
||||||
val useCustomCover = options.parameters[USE_CUSTOM_COVER] as? Boolean ?: true
|
|
||||||
val customCoverFile = coverCache.getCustomCoverFile(data)
|
|
||||||
if (useCustomCover && customCoverFile.exists()) {
|
|
||||||
return fileLoader(customCoverFile)
|
|
||||||
}
|
|
||||||
|
|
||||||
val cover = data.thumbnail_url
|
|
||||||
return when (getResourceType(cover)) {
|
|
||||||
Type.URL -> httpLoader(data, options)
|
|
||||||
Type.File -> fileLoader(data)
|
|
||||||
null -> error("Invalid image")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun httpLoader(manga: Manga, options: Options): FetchResult {
|
|
||||||
// Only cache separately if it's a library item
|
|
||||||
val coverCacheFile = if (manga.favorite) {
|
|
||||||
coverCache.getCoverFile(manga) ?: error("No cover specified")
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (coverCacheFile?.exists() == true && options.diskCachePolicy.readEnabled) {
|
|
||||||
return fileLoader(coverCacheFile)
|
|
||||||
}
|
|
||||||
|
|
||||||
val (response, body) = awaitGetCall(manga, options)
|
|
||||||
if (!response.isSuccessful) {
|
|
||||||
body.close()
|
|
||||||
throw HttpException(response)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (coverCacheFile != null && options.diskCachePolicy.writeEnabled) {
|
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
|
||||||
response.peekBody(Long.MAX_VALUE).source().use { input ->
|
|
||||||
coverCacheFile.parentFile?.mkdirs()
|
|
||||||
if (coverCacheFile.exists()) {
|
|
||||||
coverCacheFile.delete()
|
|
||||||
}
|
|
||||||
coverCacheFile.sink().buffer().use { output ->
|
|
||||||
output.writeAll(input)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return SourceResult(
|
|
||||||
source = body.source(),
|
|
||||||
mimeType = "image/*",
|
|
||||||
dataSource = if (response.cacheResponse != null) DataSource.DISK else DataSource.NETWORK
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun awaitGetCall(manga: Manga, options: Options): Pair<Response, ResponseBody> {
|
|
||||||
val call = getCall(manga, options)
|
|
||||||
val response = call.await()
|
|
||||||
return response to checkNotNull(response.body) { "Null response source" }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getCall(manga: Manga, options: Options): Call {
|
|
||||||
val source = sourceManager.get(manga.source) as? HttpSource
|
|
||||||
val request = Request.Builder().url(manga.thumbnail_url!!).also {
|
|
||||||
if (source != null) {
|
|
||||||
it.headers(source.headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
val networkRead = options.networkCachePolicy.readEnabled
|
|
||||||
val diskRead = options.diskCachePolicy.readEnabled
|
|
||||||
when {
|
|
||||||
!networkRead && diskRead -> {
|
|
||||||
it.cacheControl(CacheControl.FORCE_CACHE)
|
|
||||||
}
|
|
||||||
networkRead && !diskRead -> if (options.diskCachePolicy.writeEnabled) {
|
|
||||||
it.cacheControl(CacheControl.FORCE_NETWORK)
|
|
||||||
} else {
|
|
||||||
it.cacheControl(CACHE_CONTROL_FORCE_NETWORK_NO_CACHE)
|
|
||||||
}
|
|
||||||
!networkRead && !diskRead -> {
|
|
||||||
// This causes the request to fail with a 504 Unsatisfiable Request.
|
|
||||||
it.cacheControl(CACHE_CONTROL_NO_NETWORK_NO_CACHE)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.build()
|
|
||||||
|
|
||||||
val client = source?.client?.newBuilder()?.cache(defaultClient.cache)?.build() ?: defaultClient
|
|
||||||
return client.newCall(request)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun fileLoader(manga: Manga): FetchResult {
|
|
||||||
return fileLoader(File(manga.thumbnail_url!!.substringAfter("file://")))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun fileLoader(file: File): FetchResult {
|
|
||||||
return SourceResult(
|
|
||||||
source = file.source().buffer(),
|
|
||||||
mimeType = "image/*",
|
|
||||||
dataSource = DataSource.DISK
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getResourceType(cover: String?): Type? {
|
|
||||||
return when {
|
|
||||||
cover.isNullOrEmpty() -> null
|
|
||||||
cover.startsWith("http", true) || cover.startsWith("Custom-", true) -> Type.URL
|
|
||||||
cover.startsWith("/") || cover.startsWith("file://") -> Type.File
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private enum class Type {
|
|
||||||
File, URL
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val USE_CUSTOM_COVER = "use_custom_cover"
|
|
||||||
|
|
||||||
private val CACHE_CONTROL_FORCE_NETWORK_NO_CACHE = CacheControl.Builder().noCache().noStore().build()
|
|
||||||
private val CACHE_CONTROL_NO_NETWORK_NO_CACHE = CacheControl.Builder().noCache().onlyIfCached().build()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.coil
|
|
||||||
|
|
||||||
import android.content.res.Resources
|
|
||||||
import android.os.Build
|
|
||||||
import androidx.core.graphics.drawable.toDrawable
|
|
||||||
import coil.bitmap.BitmapPool
|
|
||||||
import coil.decode.DecodeResult
|
|
||||||
import coil.decode.Decoder
|
|
||||||
import coil.decode.Options
|
|
||||||
import coil.size.Size
|
|
||||||
import eu.kanade.tachiyomi.util.system.ImageUtil
|
|
||||||
import okio.BufferedSource
|
|
||||||
import tachiyomi.decoder.ImageDecoder
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A [Decoder] that uses built-in [ImageDecoder] to decode images that is not supported by the system.
|
|
||||||
*/
|
|
||||||
class TachiyomiImageDecoder(private val resources: Resources) : Decoder {
|
|
||||||
|
|
||||||
override fun handles(source: BufferedSource, mimeType: String?): Boolean {
|
|
||||||
val type = source.peek().inputStream().use {
|
|
||||||
ImageUtil.findImageType(it)
|
|
||||||
}
|
|
||||||
return when (type) {
|
|
||||||
ImageUtil.ImageType.AVIF, ImageUtil.ImageType.JXL -> true
|
|
||||||
ImageUtil.ImageType.HEIF -> Build.VERSION.SDK_INT < Build.VERSION_CODES.O
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun decode(
|
|
||||||
pool: BitmapPool,
|
|
||||||
source: BufferedSource,
|
|
||||||
size: Size,
|
|
||||||
options: Options
|
|
||||||
): DecodeResult {
|
|
||||||
val decoder = source.use {
|
|
||||||
ImageDecoder.newInstance(it.inputStream())
|
|
||||||
}
|
|
||||||
|
|
||||||
check(decoder != null && decoder.width > 0 && decoder.height > 0) { "Failed to initialize decoder." }
|
|
||||||
|
|
||||||
val bitmap = decoder.decode(rgb565 = options.allowRgb565)
|
|
||||||
decoder.recycle()
|
|
||||||
|
|
||||||
check(bitmap != null) { "Failed to decode image." }
|
|
||||||
|
|
||||||
return DecodeResult(
|
|
||||||
drawable = bitmap.toDrawable(resources),
|
|
||||||
isSampled = false
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -21,9 +21,9 @@ import eu.kanade.tachiyomi.data.database.queries.HistoryQueries
|
|||||||
import eu.kanade.tachiyomi.data.database.queries.MangaCategoryQueries
|
import eu.kanade.tachiyomi.data.database.queries.MangaCategoryQueries
|
||||||
import eu.kanade.tachiyomi.data.database.queries.MangaQueries
|
import eu.kanade.tachiyomi.data.database.queries.MangaQueries
|
||||||
import eu.kanade.tachiyomi.data.database.queries.TrackQueries
|
import eu.kanade.tachiyomi.data.database.queries.TrackQueries
|
||||||
import exh.favorites.sql.mappers.FavoriteEntryTypeMapping
|
import exh.md.similar.sql.mappers.SimilarTypeMapping
|
||||||
import exh.favorites.sql.models.FavoriteEntry
|
import exh.md.similar.sql.models.MangaSimilar
|
||||||
import exh.favorites.sql.queries.FavoriteEntryQueries
|
import exh.md.similar.sql.queries.SimilarQueries
|
||||||
import exh.merged.sql.mappers.MergedMangaTypeMapping
|
import exh.merged.sql.mappers.MergedMangaTypeMapping
|
||||||
import exh.merged.sql.models.MergedMangaReference
|
import exh.merged.sql.models.MergedMangaReference
|
||||||
import exh.merged.sql.queries.MergedQueries
|
import exh.merged.sql.queries.MergedQueries
|
||||||
@@ -42,7 +42,7 @@ import io.requery.android.database.sqlite.RequerySQLiteOpenHelperFactory
|
|||||||
* This class provides operations to manage the database through its interfaces.
|
* This class provides operations to manage the database through its interfaces.
|
||||||
*/
|
*/
|
||||||
open class DatabaseHelper(context: Context) :
|
open class DatabaseHelper(context: Context) :
|
||||||
MangaQueries, ChapterQueries, TrackQueries, CategoryQueries, MangaCategoryQueries, HistoryQueries /* SY --> */, SearchMetadataQueries, SearchTagQueries, SearchTitleQueries, MergedQueries, FavoriteEntryQueries /* SY <-- */ {
|
MangaQueries, ChapterQueries, TrackQueries, CategoryQueries, MangaCategoryQueries, HistoryQueries /* SY --> */, SearchMetadataQueries, SearchTagQueries, SearchTitleQueries, MergedQueries, SimilarQueries /* SY <-- */ {
|
||||||
|
|
||||||
private val configuration = SupportSQLiteOpenHelper.Configuration.builder(context)
|
private val configuration = SupportSQLiteOpenHelper.Configuration.builder(context)
|
||||||
.name(DbOpenCallback.DATABASE_NAME)
|
.name(DbOpenCallback.DATABASE_NAME)
|
||||||
@@ -62,7 +62,7 @@ open class DatabaseHelper(context: Context) :
|
|||||||
.addTypeMapping(SearchTag::class.java, SearchTagTypeMapping())
|
.addTypeMapping(SearchTag::class.java, SearchTagTypeMapping())
|
||||||
.addTypeMapping(SearchTitle::class.java, SearchTitleTypeMapping())
|
.addTypeMapping(SearchTitle::class.java, SearchTitleTypeMapping())
|
||||||
.addTypeMapping(MergedMangaReference::class.java, MergedMangaTypeMapping())
|
.addTypeMapping(MergedMangaReference::class.java, MergedMangaTypeMapping())
|
||||||
.addTypeMapping(FavoriteEntry::class.java, FavoriteEntryTypeMapping())
|
.addTypeMapping(MangaSimilar::class.java, SimilarTypeMapping())
|
||||||
// SY <--
|
// SY <--
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import eu.kanade.tachiyomi.data.database.tables.HistoryTable
|
|||||||
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable
|
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
||||||
import eu.kanade.tachiyomi.data.database.tables.TrackTable
|
import eu.kanade.tachiyomi.data.database.tables.TrackTable
|
||||||
import exh.favorites.sql.tables.FavoriteEntryTable
|
import exh.md.similar.sql.tables.SimilarTable
|
||||||
import exh.merged.sql.tables.MergedTable
|
import exh.merged.sql.tables.MergedTable
|
||||||
import exh.metadata.sql.tables.SearchMetadataTable
|
import exh.metadata.sql.tables.SearchMetadataTable
|
||||||
import exh.metadata.sql.tables.SearchTagTable
|
import exh.metadata.sql.tables.SearchTagTable
|
||||||
@@ -25,7 +25,7 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
|
|||||||
/**
|
/**
|
||||||
* Version of the database.
|
* Version of the database.
|
||||||
*/
|
*/
|
||||||
const val DATABASE_VERSION = /* SY --> */ 12 /* SY <-- */
|
const val DATABASE_VERSION = /* SY --> */ 5 /* SY <-- */
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate(db: SupportSQLiteDatabase) = with(db) {
|
override fun onCreate(db: SupportSQLiteDatabase) = with(db) {
|
||||||
@@ -40,7 +40,7 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
|
|||||||
execSQL(SearchTagTable.createTableQuery)
|
execSQL(SearchTagTable.createTableQuery)
|
||||||
execSQL(SearchTitleTable.createTableQuery)
|
execSQL(SearchTitleTable.createTableQuery)
|
||||||
execSQL(MergedTable.createTableQuery)
|
execSQL(MergedTable.createTableQuery)
|
||||||
execSQL(FavoriteEntryTable.createTableQuery)
|
execSQL(SimilarTable.createTableQuery)
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
// DB indexes
|
// DB indexes
|
||||||
@@ -57,6 +57,7 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
|
|||||||
execSQL(SearchTitleTable.createMangaIdIndexQuery)
|
execSQL(SearchTitleTable.createMangaIdIndexQuery)
|
||||||
execSQL(SearchTitleTable.createTitleIndexQuery)
|
execSQL(SearchTitleTable.createTitleIndexQuery)
|
||||||
execSQL(MergedTable.createIndexQuery)
|
execSQL(MergedTable.createIndexQuery)
|
||||||
|
execSQL(SimilarTable.createMangaIdIndexQuery)
|
||||||
// SY <--
|
// SY <--
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,33 +74,9 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
|
|||||||
db.execSQL(MergedTable.createTableQuery)
|
db.execSQL(MergedTable.createTableQuery)
|
||||||
db.execSQL(MergedTable.createIndexQuery)
|
db.execSQL(MergedTable.createIndexQuery)
|
||||||
}
|
}
|
||||||
/*if (oldVersion < 5) {
|
if (oldVersion < 5) {
|
||||||
db.execSQL(SimilarTable.createTableQuery)
|
db.execSQL(SimilarTable.createTableQuery)
|
||||||
db.execSQL(SimilarTable.createMangaIdIndexQuery)
|
db.execSQL(SimilarTable.createMangaIdIndexQuery)
|
||||||
}*/
|
|
||||||
if (oldVersion < 6) {
|
|
||||||
db.execSQL(MangaTable.addFilteredScanlators)
|
|
||||||
}
|
|
||||||
if (oldVersion < 7) {
|
|
||||||
db.execSQL("DROP TABLE IF EXISTS manga_related")
|
|
||||||
}
|
|
||||||
if (oldVersion < 8) {
|
|
||||||
db.execSQL(MangaTable.addNextUpdateCol)
|
|
||||||
}
|
|
||||||
if (oldVersion < 9) {
|
|
||||||
db.execSQL(TrackTable.renameTableToTemp)
|
|
||||||
db.execSQL(TrackTable.createTableQuery)
|
|
||||||
db.execSQL(TrackTable.insertFromTempTable)
|
|
||||||
db.execSQL(TrackTable.dropTempTable)
|
|
||||||
}
|
|
||||||
if (oldVersion < 10) {
|
|
||||||
db.execSQL(ChapterTable.fixDateUploadIfNeeded)
|
|
||||||
}
|
|
||||||
if (oldVersion < 11) {
|
|
||||||
db.execSQL(FavoriteEntryTable.createTableQuery)
|
|
||||||
}
|
|
||||||
if (oldVersion < 12) {
|
|
||||||
db.execSQL(FavoriteEntryTable.fixTableQuery)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -49,13 +49,13 @@ class CategoryPutResolver : DefaultPutResolver<Category>() {
|
|||||||
class CategoryGetResolver : DefaultGetResolver<Category>() {
|
class CategoryGetResolver : DefaultGetResolver<Category>() {
|
||||||
|
|
||||||
override fun mapFromCursor(cursor: Cursor): Category = CategoryImpl().apply {
|
override fun mapFromCursor(cursor: Cursor): Category = CategoryImpl().apply {
|
||||||
id = cursor.getInt(cursor.getColumnIndexOrThrow(COL_ID))
|
id = cursor.getInt(cursor.getColumnIndex(COL_ID))
|
||||||
name = cursor.getString(cursor.getColumnIndexOrThrow(COL_NAME))
|
name = cursor.getString(cursor.getColumnIndex(COL_NAME))
|
||||||
order = cursor.getInt(cursor.getColumnIndexOrThrow(COL_ORDER))
|
order = cursor.getInt(cursor.getColumnIndex(COL_ORDER))
|
||||||
flags = cursor.getInt(cursor.getColumnIndexOrThrow(COL_FLAGS))
|
flags = cursor.getInt(cursor.getColumnIndex(COL_FLAGS))
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
val orderString = cursor.getString(cursor.getColumnIndexOrThrow(COL_MANGA_ORDER))
|
val orderString = cursor.getString(cursor.getColumnIndex(COL_MANGA_ORDER))
|
||||||
mangaOrder = orderString?.split("/")?.mapNotNull { it.toLongOrNull() }.orEmpty()
|
mangaOrder = orderString?.split("/")?.mapNotNull { it.toLongOrNull() }.orEmpty()
|
||||||
// SY <--
|
// SY <--
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,18 +63,18 @@ class ChapterPutResolver : DefaultPutResolver<Chapter>() {
|
|||||||
class ChapterGetResolver : DefaultGetResolver<Chapter>() {
|
class ChapterGetResolver : DefaultGetResolver<Chapter>() {
|
||||||
|
|
||||||
override fun mapFromCursor(cursor: Cursor): Chapter = ChapterImpl().apply {
|
override fun mapFromCursor(cursor: Cursor): Chapter = ChapterImpl().apply {
|
||||||
id = cursor.getLong(cursor.getColumnIndexOrThrow(COL_ID))
|
id = cursor.getLong(cursor.getColumnIndex(COL_ID))
|
||||||
manga_id = cursor.getLong(cursor.getColumnIndexOrThrow(COL_MANGA_ID))
|
manga_id = cursor.getLong(cursor.getColumnIndex(COL_MANGA_ID))
|
||||||
url = cursor.getString(cursor.getColumnIndexOrThrow(COL_URL))
|
url = cursor.getString(cursor.getColumnIndex(COL_URL))
|
||||||
name = cursor.getString(cursor.getColumnIndexOrThrow(COL_NAME))
|
name = cursor.getString(cursor.getColumnIndex(COL_NAME))
|
||||||
scanlator = cursor.getString(cursor.getColumnIndexOrThrow(COL_SCANLATOR))
|
scanlator = cursor.getString(cursor.getColumnIndex(COL_SCANLATOR))
|
||||||
read = cursor.getInt(cursor.getColumnIndexOrThrow(COL_READ)) == 1
|
read = cursor.getInt(cursor.getColumnIndex(COL_READ)) == 1
|
||||||
bookmark = cursor.getInt(cursor.getColumnIndexOrThrow(COL_BOOKMARK)) == 1
|
bookmark = cursor.getInt(cursor.getColumnIndex(COL_BOOKMARK)) == 1
|
||||||
date_fetch = cursor.getLong(cursor.getColumnIndexOrThrow(COL_DATE_FETCH))
|
date_fetch = cursor.getLong(cursor.getColumnIndex(COL_DATE_FETCH))
|
||||||
date_upload = cursor.getLong(cursor.getColumnIndexOrThrow(COL_DATE_UPLOAD))
|
date_upload = cursor.getLong(cursor.getColumnIndex(COL_DATE_UPLOAD))
|
||||||
last_page_read = cursor.getInt(cursor.getColumnIndexOrThrow(COL_LAST_PAGE_READ))
|
last_page_read = cursor.getInt(cursor.getColumnIndex(COL_LAST_PAGE_READ))
|
||||||
chapter_number = cursor.getFloat(cursor.getColumnIndexOrThrow(COL_CHAPTER_NUMBER))
|
chapter_number = cursor.getFloat(cursor.getColumnIndex(COL_CHAPTER_NUMBER))
|
||||||
source_order = cursor.getInt(cursor.getColumnIndexOrThrow(COL_SOURCE_ORDER))
|
source_order = cursor.getInt(cursor.getColumnIndex(COL_SOURCE_ORDER))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -47,10 +47,10 @@ open class HistoryPutResolver : DefaultPutResolver<History>() {
|
|||||||
class HistoryGetResolver : DefaultGetResolver<History>() {
|
class HistoryGetResolver : DefaultGetResolver<History>() {
|
||||||
|
|
||||||
override fun mapFromCursor(cursor: Cursor): History = HistoryImpl().apply {
|
override fun mapFromCursor(cursor: Cursor): History = HistoryImpl().apply {
|
||||||
id = cursor.getLong(cursor.getColumnIndexOrThrow(COL_ID))
|
id = cursor.getLong(cursor.getColumnIndex(COL_ID))
|
||||||
chapter_id = cursor.getLong(cursor.getColumnIndexOrThrow(COL_CHAPTER_ID))
|
chapter_id = cursor.getLong(cursor.getColumnIndex(COL_CHAPTER_ID))
|
||||||
last_read = cursor.getLong(cursor.getColumnIndexOrThrow(COL_LAST_READ))
|
last_read = cursor.getLong(cursor.getColumnIndex(COL_LAST_READ))
|
||||||
time_read = cursor.getLong(cursor.getColumnIndexOrThrow(COL_TIME_READ))
|
time_read = cursor.getLong(cursor.getColumnIndex(COL_TIME_READ))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -44,9 +44,9 @@ class MangaCategoryPutResolver : DefaultPutResolver<MangaCategory>() {
|
|||||||
class MangaCategoryGetResolver : DefaultGetResolver<MangaCategory>() {
|
class MangaCategoryGetResolver : DefaultGetResolver<MangaCategory>() {
|
||||||
|
|
||||||
override fun mapFromCursor(cursor: Cursor): MangaCategory = MangaCategory().apply {
|
override fun mapFromCursor(cursor: Cursor): MangaCategory = MangaCategory().apply {
|
||||||
id = cursor.getLong(cursor.getColumnIndexOrThrow(COL_ID))
|
id = cursor.getLong(cursor.getColumnIndex(COL_ID))
|
||||||
manga_id = cursor.getLong(cursor.getColumnIndexOrThrow(COL_MANGA_ID))
|
manga_id = cursor.getLong(cursor.getColumnIndex(COL_MANGA_ID))
|
||||||
category_id = cursor.getInt(cursor.getColumnIndexOrThrow(COL_CATEGORY_ID))
|
category_id = cursor.getInt(cursor.getColumnIndex(COL_CATEGORY_ID))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_COVER_LAST_MODIFI
|
|||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_DATE_ADDED
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_DATE_ADDED
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_DESCRIPTION
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_DESCRIPTION
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_FAVORITE
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_FAVORITE
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_FILTERED_SCANLATORS
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_GENRE
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_GENRE
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_ID
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_ID
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_INITIALIZED
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_INITIALIZED
|
||||||
@@ -60,40 +59,38 @@ class MangaPutResolver : DefaultPutResolver<Manga>() {
|
|||||||
COL_DESCRIPTION to obj.originalDescription,
|
COL_DESCRIPTION to obj.originalDescription,
|
||||||
COL_GENRE to obj.originalGenre,
|
COL_GENRE to obj.originalGenre,
|
||||||
COL_TITLE to obj.originalTitle,
|
COL_TITLE to obj.originalTitle,
|
||||||
COL_STATUS to obj.originalStatus,
|
|
||||||
// SY <--
|
// SY <--
|
||||||
|
COL_STATUS to obj.status,
|
||||||
COL_THUMBNAIL_URL to obj.thumbnail_url,
|
COL_THUMBNAIL_URL to obj.thumbnail_url,
|
||||||
COL_FAVORITE to obj.favorite,
|
COL_FAVORITE to obj.favorite,
|
||||||
COL_LAST_UPDATE to obj.last_update,
|
COL_LAST_UPDATE to obj.last_update,
|
||||||
COL_INITIALIZED to obj.initialized,
|
COL_INITIALIZED to obj.initialized,
|
||||||
COL_VIEWER to obj.viewer_flags,
|
COL_VIEWER to obj.viewer,
|
||||||
COL_CHAPTER_FLAGS to obj.chapter_flags,
|
COL_CHAPTER_FLAGS to obj.chapter_flags,
|
||||||
COL_COVER_LAST_MODIFIED to obj.cover_last_modified,
|
COL_COVER_LAST_MODIFIED to obj.cover_last_modified,
|
||||||
COL_DATE_ADDED to obj.date_added,
|
COL_DATE_ADDED to obj.date_added
|
||||||
COL_FILTERED_SCANLATORS to obj.filtered_scanlators
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BaseMangaGetResolver {
|
interface BaseMangaGetResolver {
|
||||||
fun mapBaseFromCursor(manga: Manga, cursor: Cursor) = manga.apply {
|
fun mapBaseFromCursor(manga: Manga, cursor: Cursor) = manga.apply {
|
||||||
id = cursor.getLong(cursor.getColumnIndexOrThrow(COL_ID))
|
id = cursor.getLong(cursor.getColumnIndex(COL_ID))
|
||||||
source = cursor.getLong(cursor.getColumnIndexOrThrow(COL_SOURCE))
|
source = cursor.getLong(cursor.getColumnIndex(COL_SOURCE))
|
||||||
url = cursor.getString(cursor.getColumnIndexOrThrow(COL_URL))
|
url = cursor.getString(cursor.getColumnIndex(COL_URL))
|
||||||
artist = cursor.getString(cursor.getColumnIndexOrThrow(COL_ARTIST))
|
artist = cursor.getString(cursor.getColumnIndex(COL_ARTIST))
|
||||||
author = cursor.getString(cursor.getColumnIndexOrThrow(COL_AUTHOR))
|
author = cursor.getString(cursor.getColumnIndex(COL_AUTHOR))
|
||||||
description = cursor.getString(cursor.getColumnIndexOrThrow(COL_DESCRIPTION))
|
description = cursor.getString(cursor.getColumnIndex(COL_DESCRIPTION))
|
||||||
genre = cursor.getString(cursor.getColumnIndexOrThrow(COL_GENRE))
|
genre = cursor.getString(cursor.getColumnIndex(COL_GENRE))
|
||||||
title = cursor.getString(cursor.getColumnIndexOrThrow(COL_TITLE))
|
title = cursor.getString(cursor.getColumnIndex(COL_TITLE))
|
||||||
status = cursor.getInt(cursor.getColumnIndexOrThrow(COL_STATUS))
|
status = cursor.getInt(cursor.getColumnIndex(COL_STATUS))
|
||||||
thumbnail_url = cursor.getString(cursor.getColumnIndexOrThrow(COL_THUMBNAIL_URL))
|
thumbnail_url = cursor.getString(cursor.getColumnIndex(COL_THUMBNAIL_URL))
|
||||||
favorite = cursor.getInt(cursor.getColumnIndexOrThrow(COL_FAVORITE)) == 1
|
favorite = cursor.getInt(cursor.getColumnIndex(COL_FAVORITE)) == 1
|
||||||
last_update = cursor.getLong(cursor.getColumnIndexOrThrow(COL_LAST_UPDATE))
|
last_update = cursor.getLong(cursor.getColumnIndex(COL_LAST_UPDATE))
|
||||||
initialized = cursor.getInt(cursor.getColumnIndexOrThrow(COL_INITIALIZED)) == 1
|
initialized = cursor.getInt(cursor.getColumnIndex(COL_INITIALIZED)) == 1
|
||||||
viewer_flags = cursor.getInt(cursor.getColumnIndexOrThrow(COL_VIEWER))
|
viewer = cursor.getInt(cursor.getColumnIndex(COL_VIEWER))
|
||||||
chapter_flags = cursor.getInt(cursor.getColumnIndexOrThrow(COL_CHAPTER_FLAGS))
|
chapter_flags = cursor.getInt(cursor.getColumnIndex(COL_CHAPTER_FLAGS))
|
||||||
cover_last_modified = cursor.getLong(cursor.getColumnIndexOrThrow(COL_COVER_LAST_MODIFIED))
|
cover_last_modified = cursor.getLong(cursor.getColumnIndex(COL_COVER_LAST_MODIFIED))
|
||||||
date_added = cursor.getLong(cursor.getColumnIndexOrThrow(COL_DATE_ADDED))
|
date_added = cursor.getLong(cursor.getColumnIndex(COL_DATE_ADDED))
|
||||||
filtered_scanlators = cursor.getString(cursor.getColumnIndexOrThrow(COL_FILTERED_SCANLATORS))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -65,19 +65,19 @@ class TrackPutResolver : DefaultPutResolver<Track>() {
|
|||||||
class TrackGetResolver : DefaultGetResolver<Track>() {
|
class TrackGetResolver : DefaultGetResolver<Track>() {
|
||||||
|
|
||||||
override fun mapFromCursor(cursor: Cursor): Track = TrackImpl().apply {
|
override fun mapFromCursor(cursor: Cursor): Track = TrackImpl().apply {
|
||||||
id = cursor.getLong(cursor.getColumnIndexOrThrow(COL_ID))
|
id = cursor.getLong(cursor.getColumnIndex(COL_ID))
|
||||||
manga_id = cursor.getLong(cursor.getColumnIndexOrThrow(COL_MANGA_ID))
|
manga_id = cursor.getLong(cursor.getColumnIndex(COL_MANGA_ID))
|
||||||
sync_id = cursor.getInt(cursor.getColumnIndexOrThrow(COL_SYNC_ID))
|
sync_id = cursor.getInt(cursor.getColumnIndex(COL_SYNC_ID))
|
||||||
media_id = cursor.getInt(cursor.getColumnIndexOrThrow(COL_MEDIA_ID))
|
media_id = cursor.getInt(cursor.getColumnIndex(COL_MEDIA_ID))
|
||||||
library_id = cursor.getLong(cursor.getColumnIndexOrThrow(COL_LIBRARY_ID))
|
library_id = cursor.getLong(cursor.getColumnIndex(COL_LIBRARY_ID))
|
||||||
title = cursor.getString(cursor.getColumnIndexOrThrow(COL_TITLE))
|
title = cursor.getString(cursor.getColumnIndex(COL_TITLE))
|
||||||
last_chapter_read = cursor.getFloat(cursor.getColumnIndexOrThrow(COL_LAST_CHAPTER_READ))
|
last_chapter_read = cursor.getInt(cursor.getColumnIndex(COL_LAST_CHAPTER_READ))
|
||||||
total_chapters = cursor.getInt(cursor.getColumnIndexOrThrow(COL_TOTAL_CHAPTERS))
|
total_chapters = cursor.getInt(cursor.getColumnIndex(COL_TOTAL_CHAPTERS))
|
||||||
status = cursor.getInt(cursor.getColumnIndexOrThrow(COL_STATUS))
|
status = cursor.getInt(cursor.getColumnIndex(COL_STATUS))
|
||||||
score = cursor.getFloat(cursor.getColumnIndexOrThrow(COL_SCORE))
|
score = cursor.getFloat(cursor.getColumnIndex(COL_SCORE))
|
||||||
tracking_url = cursor.getString(cursor.getColumnIndexOrThrow(COL_TRACKING_URL))
|
tracking_url = cursor.getString(cursor.getColumnIndex(COL_TRACKING_URL))
|
||||||
started_reading_date = cursor.getLong(cursor.getColumnIndexOrThrow(COL_START_DATE))
|
started_reading_date = cursor.getLong(cursor.getColumnIndex(COL_START_DATE))
|
||||||
finished_reading_date = cursor.getLong(cursor.getColumnIndexOrThrow(COL_FINISH_DATE))
|
finished_reading_date = cursor.getLong(cursor.getColumnIndex(COL_FINISH_DATE))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,5 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.models
|
package eu.kanade.tachiyomi.data.database.models
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import eu.kanade.tachiyomi.R
|
|
||||||
import eu.kanade.tachiyomi.ui.library.setting.DisplayModeSetting
|
|
||||||
import eu.kanade.tachiyomi.ui.library.setting.SortDirectionSetting
|
|
||||||
import eu.kanade.tachiyomi.ui.library.setting.SortModeSetting
|
|
||||||
import java.io.Serializable
|
import java.io.Serializable
|
||||||
|
|
||||||
interface Category : Serializable {
|
interface Category : Serializable {
|
||||||
@@ -21,28 +16,12 @@ interface Category : Serializable {
|
|||||||
var mangaOrder: List<Long>
|
var mangaOrder: List<Long>
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
private fun setFlags(flag: Int, mask: Int) {
|
|
||||||
flags = flags and mask.inv() or (flag and mask)
|
|
||||||
}
|
|
||||||
|
|
||||||
var displayMode: Int
|
|
||||||
get() = flags and DisplayModeSetting.MASK
|
|
||||||
set(mode) = setFlags(mode, DisplayModeSetting.MASK)
|
|
||||||
|
|
||||||
var sortMode: Int
|
|
||||||
get() = flags and SortModeSetting.MASK
|
|
||||||
set(mode) = setFlags(mode, SortModeSetting.MASK)
|
|
||||||
|
|
||||||
var sortDirection: Int
|
|
||||||
get() = flags and SortDirectionSetting.MASK
|
|
||||||
set(mode) = setFlags(mode, SortDirectionSetting.MASK)
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
fun create(name: String): Category = CategoryImpl().apply {
|
fun create(name: String): Category = CategoryImpl().apply {
|
||||||
this.name = name
|
this.name = name
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createDefault(context: Context): Category = create(context.getString(R.string.label_default)).apply { id = 0 }
|
fun createDefault(): Category = create("Default").apply { id = 0 }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.models
|
package eu.kanade.tachiyomi.data.database.models
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
|
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
|
|
||||||
import tachiyomi.source.model.MangaInfo
|
import tachiyomi.source.model.MangaInfo
|
||||||
|
|
||||||
interface Manga : SManga {
|
interface Manga : SManga {
|
||||||
@@ -13,30 +11,26 @@ interface Manga : SManga {
|
|||||||
|
|
||||||
var favorite: Boolean
|
var favorite: Boolean
|
||||||
|
|
||||||
// last time the chapter list changed in any way
|
|
||||||
var last_update: Long
|
var last_update: Long
|
||||||
|
|
||||||
var date_added: Long
|
var date_added: Long
|
||||||
|
|
||||||
var viewer_flags: Int
|
var viewer: Int
|
||||||
|
|
||||||
var chapter_flags: Int
|
var chapter_flags: Int
|
||||||
|
|
||||||
var cover_last_modified: Long
|
var cover_last_modified: Long
|
||||||
|
|
||||||
var filtered_scanlators: String?
|
|
||||||
|
|
||||||
fun setChapterOrder(order: Int) {
|
fun setChapterOrder(order: Int) {
|
||||||
setChapterFlags(order, CHAPTER_SORT_MASK)
|
setFlags(order, SORT_MASK)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sortDescending(): Boolean {
|
fun sortDescending(): Boolean {
|
||||||
return chapter_flags and CHAPTER_SORT_MASK == CHAPTER_SORT_DESC
|
return chapter_flags and SORT_MASK == SORT_DESC
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getGenres(): List<String>? {
|
fun getGenres(): List<String>? {
|
||||||
if (genre.isNullOrBlank()) return null
|
return genre?.split(", ")?.map { it.trim() }
|
||||||
return genre?.split(", ")?.map { it.trim() }?.filterNot { it.isBlank() }?.distinct()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
@@ -45,72 +39,60 @@ interface Manga : SManga {
|
|||||||
}
|
}
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
private fun setChapterFlags(flag: Int, mask: Int) {
|
private fun setFlags(flag: Int, mask: Int) {
|
||||||
chapter_flags = chapter_flags and mask.inv() or (flag and mask)
|
chapter_flags = chapter_flags and mask.inv() or (flag and mask)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setViewerFlags(flag: Int, mask: Int) {
|
|
||||||
viewer_flags = viewer_flags and mask.inv() or (flag and mask)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Used to display the chapter's title one way or another
|
// Used to display the chapter's title one way or another
|
||||||
var displayMode: Int
|
var displayMode: Int
|
||||||
get() = chapter_flags and CHAPTER_DISPLAY_MASK
|
get() = chapter_flags and DISPLAY_MASK
|
||||||
set(mode) = setChapterFlags(mode, CHAPTER_DISPLAY_MASK)
|
set(mode) = setFlags(mode, DISPLAY_MASK)
|
||||||
|
|
||||||
var readFilter: Int
|
var readFilter: Int
|
||||||
get() = chapter_flags and CHAPTER_READ_MASK
|
get() = chapter_flags and READ_MASK
|
||||||
set(filter) = setChapterFlags(filter, CHAPTER_READ_MASK)
|
set(filter) = setFlags(filter, READ_MASK)
|
||||||
|
|
||||||
var downloadedFilter: Int
|
var downloadedFilter: Int
|
||||||
get() = chapter_flags and CHAPTER_DOWNLOADED_MASK
|
get() = chapter_flags and DOWNLOADED_MASK
|
||||||
set(filter) = setChapterFlags(filter, CHAPTER_DOWNLOADED_MASK)
|
set(filter) = setFlags(filter, DOWNLOADED_MASK)
|
||||||
|
|
||||||
var bookmarkedFilter: Int
|
var bookmarkedFilter: Int
|
||||||
get() = chapter_flags and CHAPTER_BOOKMARKED_MASK
|
get() = chapter_flags and BOOKMARKED_MASK
|
||||||
set(filter) = setChapterFlags(filter, CHAPTER_BOOKMARKED_MASK)
|
set(filter) = setFlags(filter, BOOKMARKED_MASK)
|
||||||
|
|
||||||
var sorting: Int
|
var sorting: Int
|
||||||
get() = chapter_flags and CHAPTER_SORTING_MASK
|
get() = chapter_flags and SORTING_MASK
|
||||||
set(sort) = setChapterFlags(sort, CHAPTER_SORTING_MASK)
|
set(sort) = setFlags(sort, SORTING_MASK)
|
||||||
|
|
||||||
var readingModeType: Int
|
|
||||||
get() = viewer_flags and ReadingModeType.MASK
|
|
||||||
set(readingMode) = setViewerFlags(readingMode, ReadingModeType.MASK)
|
|
||||||
|
|
||||||
var orientationType: Int
|
|
||||||
get() = viewer_flags and OrientationType.MASK
|
|
||||||
set(rotationType) = setViewerFlags(rotationType, OrientationType.MASK)
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
|
const val SORT_DESC = 0x00000000
|
||||||
|
const val SORT_ASC = 0x00000001
|
||||||
|
const val SORT_MASK = 0x00000001
|
||||||
|
|
||||||
// Generic filter that does not filter anything
|
// Generic filter that does not filter anything
|
||||||
const val SHOW_ALL = 0x00000000
|
const val SHOW_ALL = 0x00000000
|
||||||
|
|
||||||
const val CHAPTER_SORT_DESC = 0x00000000
|
const val SHOW_UNREAD = 0x00000002
|
||||||
const val CHAPTER_SORT_ASC = 0x00000001
|
const val SHOW_READ = 0x00000004
|
||||||
const val CHAPTER_SORT_MASK = 0x00000001
|
const val READ_MASK = 0x00000006
|
||||||
|
|
||||||
const val CHAPTER_SHOW_UNREAD = 0x00000002
|
const val SHOW_DOWNLOADED = 0x00000008
|
||||||
const val CHAPTER_SHOW_READ = 0x00000004
|
const val SHOW_NOT_DOWNLOADED = 0x00000010
|
||||||
const val CHAPTER_READ_MASK = 0x00000006
|
const val DOWNLOADED_MASK = 0x00000018
|
||||||
|
|
||||||
const val CHAPTER_SHOW_DOWNLOADED = 0x00000008
|
const val SHOW_BOOKMARKED = 0x00000020
|
||||||
const val CHAPTER_SHOW_NOT_DOWNLOADED = 0x00000010
|
const val SHOW_NOT_BOOKMARKED = 0x00000040
|
||||||
const val CHAPTER_DOWNLOADED_MASK = 0x00000018
|
const val BOOKMARKED_MASK = 0x00000060
|
||||||
|
|
||||||
const val CHAPTER_SHOW_BOOKMARKED = 0x00000020
|
const val SORTING_SOURCE = 0x00000000
|
||||||
const val CHAPTER_SHOW_NOT_BOOKMARKED = 0x00000040
|
const val SORTING_NUMBER = 0x00000100
|
||||||
const val CHAPTER_BOOKMARKED_MASK = 0x00000060
|
const val SORTING_UPLOAD_DATE = 0x00000200
|
||||||
|
const val SORTING_MASK = 0x00000300
|
||||||
|
|
||||||
const val CHAPTER_SORTING_SOURCE = 0x00000000
|
const val DISPLAY_NAME = 0x00000000
|
||||||
const val CHAPTER_SORTING_NUMBER = 0x00000100
|
const val DISPLAY_NUMBER = 0x00100000
|
||||||
const val CHAPTER_SORTING_UPLOAD_DATE = 0x00000200
|
const val DISPLAY_MASK = 0x00100000
|
||||||
const val CHAPTER_SORTING_MASK = 0x00000300
|
|
||||||
|
|
||||||
const val CHAPTER_DISPLAY_NAME = 0x00000000
|
|
||||||
const val CHAPTER_DISPLAY_NUMBER = 0x00100000
|
|
||||||
const val CHAPTER_DISPLAY_MASK = 0x00100000
|
|
||||||
|
|
||||||
fun create(source: Long): Manga = MangaImpl().apply {
|
fun create(source: Long): Manga = MangaImpl().apply {
|
||||||
this.source = source
|
this.source = source
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ open class MangaImpl : Manga {
|
|||||||
override lateinit var url: String
|
override lateinit var url: String
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
|
private val customMangaManager: CustomMangaManager by injectLazy()
|
||||||
|
|
||||||
override var title: String
|
override var title: String
|
||||||
get() = if (favorite) {
|
get() = if (favorite) {
|
||||||
val customTitle = customMangaManager.getManga(this)?.title
|
val customTitle = customMangaManager.getManga(this)?.title
|
||||||
@@ -38,12 +40,10 @@ open class MangaImpl : Manga {
|
|||||||
override var genre: String?
|
override var genre: String?
|
||||||
get() = if (favorite) customMangaManager.getManga(this)?.genre ?: ogGenre else ogGenre
|
get() = if (favorite) customMangaManager.getManga(this)?.genre ?: ogGenre else ogGenre
|
||||||
set(value) { ogGenre = value }
|
set(value) { ogGenre = value }
|
||||||
|
|
||||||
override var status: Int
|
|
||||||
get() = if (favorite) customMangaManager.getManga(this)?.status?.takeUnless { it == 0 } ?: ogStatus else ogStatus
|
|
||||||
set(value) { ogStatus = value }
|
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
|
override var status: Int = 0
|
||||||
|
|
||||||
override var thumbnail_url: String? = null
|
override var thumbnail_url: String? = null
|
||||||
|
|
||||||
override var favorite: Boolean = false
|
override var favorite: Boolean = false
|
||||||
@@ -54,14 +54,12 @@ open class MangaImpl : Manga {
|
|||||||
|
|
||||||
override var initialized: Boolean = false
|
override var initialized: Boolean = false
|
||||||
|
|
||||||
override var viewer_flags: Int = 0
|
override var viewer: Int = 0
|
||||||
|
|
||||||
override var chapter_flags: Int = 0
|
override var chapter_flags: Int = 0
|
||||||
|
|
||||||
override var cover_last_modified: Long = 0
|
override var cover_last_modified: Long = 0
|
||||||
|
|
||||||
override var filtered_scanlators: String? = null
|
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
lateinit var ogTitle: String
|
lateinit var ogTitle: String
|
||||||
private set
|
private set
|
||||||
@@ -73,8 +71,6 @@ open class MangaImpl : Manga {
|
|||||||
private set
|
private set
|
||||||
var ogGenre: String? = null
|
var ogGenre: String? = null
|
||||||
private set
|
private set
|
||||||
var ogStatus: Int = 0
|
|
||||||
private set
|
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
@@ -89,10 +85,4 @@ open class MangaImpl : Manga {
|
|||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int {
|
||||||
return url.hashCode() + id.hashCode()
|
return url.hashCode() + id.hashCode()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SY -->
|
|
||||||
companion object {
|
|
||||||
private val customMangaManager: CustomMangaManager by injectLazy()
|
|
||||||
}
|
|
||||||
// SY <--
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.models
|
|
||||||
|
|
||||||
data class SourceIdMangaCount(val source: Long, val count: Int)
|
|
||||||
@@ -16,7 +16,7 @@ interface Track : Serializable {
|
|||||||
|
|
||||||
var title: String
|
var title: String
|
||||||
|
|
||||||
var last_chapter_read: Float
|
var last_chapter_read: Int
|
||||||
|
|
||||||
var total_chapters: Int
|
var total_chapters: Int
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ class TrackImpl : Track {
|
|||||||
|
|
||||||
override lateinit var title: String
|
override lateinit var title: String
|
||||||
|
|
||||||
override var last_chapter_read: Float = 0F
|
override var last_chapter_read: Int = 0
|
||||||
|
|
||||||
override var total_chapters: Int = 0
|
override var total_chapters: Int = 0
|
||||||
|
|
||||||
|
|||||||
@@ -1,25 +1,20 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.queries
|
package eu.kanade.tachiyomi.data.database.queries
|
||||||
|
|
||||||
import com.pushtorefresh.storio.Queries
|
|
||||||
import com.pushtorefresh.storio.sqlite.operations.get.PreparedGetListOfObjects
|
|
||||||
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
||||||
import com.pushtorefresh.storio.sqlite.queries.Query
|
import com.pushtorefresh.storio.sqlite.queries.Query
|
||||||
import com.pushtorefresh.storio.sqlite.queries.RawQuery
|
import com.pushtorefresh.storio.sqlite.queries.RawQuery
|
||||||
import eu.kanade.tachiyomi.data.database.DbProvider
|
import eu.kanade.tachiyomi.data.database.DbProvider
|
||||||
import eu.kanade.tachiyomi.data.database.models.LibraryManga
|
import eu.kanade.tachiyomi.data.database.models.LibraryManga
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.database.models.SourceIdMangaCount
|
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.LibraryMangaGetResolver
|
import eu.kanade.tachiyomi.data.database.resolvers.LibraryMangaGetResolver
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaCoverLastModifiedPutResolver
|
import eu.kanade.tachiyomi.data.database.resolvers.MangaCoverLastModifiedPutResolver
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaFavoritePutResolver
|
import eu.kanade.tachiyomi.data.database.resolvers.MangaFavoritePutResolver
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaFilteredScanlatorsPutResolver
|
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaFlagsPutResolver
|
import eu.kanade.tachiyomi.data.database.resolvers.MangaFlagsPutResolver
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaInfoPutResolver
|
import eu.kanade.tachiyomi.data.database.resolvers.MangaInfoPutResolver
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaLastUpdatedPutResolver
|
import eu.kanade.tachiyomi.data.database.resolvers.MangaLastUpdatedPutResolver
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaMigrationPutResolver
|
import eu.kanade.tachiyomi.data.database.resolvers.MangaMigrationPutResolver
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaThumbnailPutResolver
|
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaTitlePutResolver
|
import eu.kanade.tachiyomi.data.database.resolvers.MangaTitlePutResolver
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.SourceIdMangaCountGetResolver
|
import eu.kanade.tachiyomi.data.database.resolvers.MangaViewerPutResolver
|
||||||
import eu.kanade.tachiyomi.data.database.tables.CategoryTable
|
import eu.kanade.tachiyomi.data.database.tables.CategoryTable
|
||||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable
|
import eu.kanade.tachiyomi.data.database.tables.ChapterTable
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable
|
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable
|
||||||
@@ -29,6 +24,15 @@ import exh.metadata.sql.tables.SearchMetadataTable
|
|||||||
|
|
||||||
interface MangaQueries : DbProvider {
|
interface MangaQueries : DbProvider {
|
||||||
|
|
||||||
|
fun getMangas() = db.get()
|
||||||
|
.listOfObjects(Manga::class.java)
|
||||||
|
.withQuery(
|
||||||
|
Query.builder()
|
||||||
|
.table(MangaTable.TABLE)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
.prepare()
|
||||||
|
|
||||||
fun getLibraryMangas() = db.get()
|
fun getLibraryMangas() = db.get()
|
||||||
.listOfObjects(LibraryManga::class.java)
|
.listOfObjects(LibraryManga::class.java)
|
||||||
.withQuery(
|
.withQuery(
|
||||||
@@ -40,21 +44,17 @@ interface MangaQueries : DbProvider {
|
|||||||
.withGetResolver(LibraryMangaGetResolver.INSTANCE)
|
.withGetResolver(LibraryMangaGetResolver.INSTANCE)
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
fun getFavoriteMangas(sortByTitle: Boolean = true): PreparedGetListOfObjects<Manga> {
|
fun getFavoriteMangas() = db.get()
|
||||||
var queryBuilder = Query.builder()
|
.listOfObjects(Manga::class.java)
|
||||||
.table(MangaTable.TABLE)
|
.withQuery(
|
||||||
.where("${MangaTable.COL_FAVORITE} = ?")
|
Query.builder()
|
||||||
.whereArgs(1)
|
.table(MangaTable.TABLE)
|
||||||
|
.where("${MangaTable.COL_FAVORITE} = ?")
|
||||||
if (sortByTitle) {
|
.whereArgs(1)
|
||||||
queryBuilder = queryBuilder.orderBy(MangaTable.COL_TITLE)
|
.orderBy(MangaTable.COL_TITLE)
|
||||||
}
|
.build()
|
||||||
|
)
|
||||||
return db.get()
|
.prepare()
|
||||||
.listOfObjects(Manga::class.java)
|
|
||||||
.withQuery(queryBuilder.build())
|
|
||||||
.prepare()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getManga(url: String, sourceId: Long) = db.get()
|
fun getManga(url: String, sourceId: Long) = db.get()
|
||||||
.`object`(Manga::class.java)
|
.`object`(Manga::class.java)
|
||||||
@@ -78,27 +78,7 @@ interface MangaQueries : DbProvider {
|
|||||||
)
|
)
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
fun getSourceIdsWithNonLibraryManga() = db.get()
|
|
||||||
.listOfObjects(SourceIdMangaCount::class.java)
|
|
||||||
.withQuery(
|
|
||||||
RawQuery.builder()
|
|
||||||
.query(getSourceIdsWithNonLibraryMangaQuery())
|
|
||||||
.observesTables(MangaTable.TABLE)
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
.withGetResolver(SourceIdMangaCountGetResolver.INSTANCE)
|
|
||||||
.prepare()
|
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
fun getMangas() = db.get()
|
|
||||||
.listOfObjects(Manga::class.java)
|
|
||||||
.withQuery(
|
|
||||||
Query.builder()
|
|
||||||
.table(MangaTable.TABLE)
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
.prepare()
|
|
||||||
|
|
||||||
fun getReadNotInLibraryMangas() = db.get()
|
fun getReadNotInLibraryMangas() = db.get()
|
||||||
.listOfObjects(Manga::class.java)
|
.listOfObjects(Manga::class.java)
|
||||||
.withQuery(
|
.withQuery(
|
||||||
@@ -122,35 +102,20 @@ interface MangaQueries : DbProvider {
|
|||||||
.`object`(manga)
|
.`object`(manga)
|
||||||
.withPutResolver(MangaMigrationPutResolver())
|
.withPutResolver(MangaMigrationPutResolver())
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
fun updateMangaThumbnail(manga: Manga) = db.put()
|
|
||||||
.`object`(manga)
|
|
||||||
.withPutResolver(MangaThumbnailPutResolver())
|
|
||||||
.prepare()
|
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
fun insertManga(manga: Manga) = db.put().`object`(manga).prepare()
|
fun insertManga(manga: Manga) = db.put().`object`(manga).prepare()
|
||||||
|
|
||||||
fun insertMangas(mangas: List<Manga>) = db.put().objects(mangas).prepare()
|
fun insertMangas(mangas: List<Manga>) = db.put().objects(mangas).prepare()
|
||||||
|
|
||||||
fun updateChapterFlags(manga: Manga) = db.put()
|
fun updateFlags(manga: Manga) = db.put()
|
||||||
.`object`(manga)
|
.`object`(manga)
|
||||||
.withPutResolver(MangaFlagsPutResolver(MangaTable.COL_CHAPTER_FLAGS, Manga::chapter_flags))
|
.withPutResolver(MangaFlagsPutResolver())
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
fun updateChapterFlags(manga: List<Manga>) = db.put()
|
fun updateFlags(mangas: List<Manga>) = db.put()
|
||||||
.objects(manga)
|
.objects(mangas)
|
||||||
.withPutResolver(MangaFlagsPutResolver(MangaTable.COL_CHAPTER_FLAGS, Manga::chapter_flags))
|
.withPutResolver(MangaFlagsPutResolver(true))
|
||||||
.prepare()
|
|
||||||
|
|
||||||
fun updateViewerFlags(manga: Manga) = db.put()
|
|
||||||
.`object`(manga)
|
|
||||||
.withPutResolver(MangaFlagsPutResolver(MangaTable.COL_VIEWER, Manga::viewer_flags))
|
|
||||||
.prepare()
|
|
||||||
|
|
||||||
fun updateViewerFlags(manga: List<Manga>) = db.put()
|
|
||||||
.objects(manga)
|
|
||||||
.withPutResolver(MangaFlagsPutResolver(MangaTable.COL_VIEWER, Manga::viewer_flags))
|
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
fun updateLastUpdated(manga: Manga) = db.put()
|
fun updateLastUpdated(manga: Manga) = db.put()
|
||||||
@@ -163,6 +128,11 @@ interface MangaQueries : DbProvider {
|
|||||||
.withPutResolver(MangaFavoritePutResolver())
|
.withPutResolver(MangaFavoritePutResolver())
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
|
fun updateMangaViewer(manga: Manga) = db.put()
|
||||||
|
.`object`(manga)
|
||||||
|
.withPutResolver(MangaViewerPutResolver())
|
||||||
|
.prepare()
|
||||||
|
|
||||||
fun updateMangaTitle(manga: Manga) = db.put()
|
fun updateMangaTitle(manga: Manga) = db.put()
|
||||||
.`object`(manga)
|
.`object`(manga)
|
||||||
.withPutResolver(MangaTitlePutResolver())
|
.withPutResolver(MangaTitlePutResolver())
|
||||||
@@ -173,54 +143,19 @@ interface MangaQueries : DbProvider {
|
|||||||
.withPutResolver(MangaCoverLastModifiedPutResolver())
|
.withPutResolver(MangaCoverLastModifiedPutResolver())
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
// SY -->
|
|
||||||
fun updateMangaFilteredScanlators(manga: Manga) = db.put()
|
|
||||||
.`object`(manga)
|
|
||||||
.withPutResolver(MangaFilteredScanlatorsPutResolver())
|
|
||||||
.prepare()
|
|
||||||
// SY <--
|
|
||||||
|
|
||||||
fun deleteManga(manga: Manga) = db.delete().`object`(manga).prepare()
|
fun deleteManga(manga: Manga) = db.delete().`object`(manga).prepare()
|
||||||
|
|
||||||
fun deleteMangas(mangas: List<Manga>) = db.delete().objects(mangas).prepare()
|
fun deleteMangas(mangas: List<Manga>) = db.delete().objects(mangas).prepare()
|
||||||
|
|
||||||
fun deleteMangasNotInLibraryBySourceIds(sourceIds: List<Long>) = db.delete()
|
fun deleteMangasNotInLibrary() = db.delete()
|
||||||
.byQuery(
|
.byQuery(
|
||||||
DeleteQuery.builder()
|
DeleteQuery.builder()
|
||||||
.table(MangaTable.TABLE)
|
.table(MangaTable.TABLE)
|
||||||
// SY -->
|
.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_SOURCE} IN (${Queries.placeholders(sourceIds.size)}) AND ${MangaTable.COL_ID} NOT IN (
|
|
||||||
SELECT ${MergedTable.COL_MANGA_ID} FROM ${MergedTable.TABLE} WHERE ${MergedTable.COL_MANGA_ID} != ${MergedTable.COL_MERGE_ID}
|
|
||||||
)
|
|
||||||
""".trimIndent()
|
|
||||||
)
|
|
||||||
// SY <--
|
|
||||||
.whereArgs(0, *sourceIds.toTypedArray())
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
.prepare()
|
|
||||||
|
|
||||||
// SY -->
|
|
||||||
fun deleteMangasNotInLibraryAndNotReadBySourceIds(sourceIds: List<Long>) = db.delete()
|
|
||||||
.byQuery(
|
|
||||||
DeleteQuery.builder()
|
|
||||||
.table(MangaTable.TABLE)
|
|
||||||
.where(
|
|
||||||
"""
|
|
||||||
${MangaTable.COL_FAVORITE} = ? AND ${MangaTable.COL_SOURCE} IN (${Queries.placeholders(sourceIds.size)}) 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)
|
.whereArgs(0)
|
||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
.prepare()
|
.prepare()
|
||||||
// SY <--
|
|
||||||
|
|
||||||
fun deleteMangas() = db.delete()
|
fun deleteMangas() = db.delete()
|
||||||
.byQuery(
|
.byQuery(
|
||||||
@@ -260,16 +195,6 @@ interface MangaQueries : DbProvider {
|
|||||||
)
|
)
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
fun getChapterFetchDateManga() = db.get()
|
|
||||||
.listOfObjects(Manga::class.java)
|
|
||||||
.withQuery(
|
|
||||||
RawQuery.builder()
|
|
||||||
.query(getChapterFetchDateMangaQuery())
|
|
||||||
.observesTables(MangaTable.TABLE)
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
.prepare()
|
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
fun getMangaWithMetadata() = db.get()
|
fun getMangaWithMetadata() = db.get()
|
||||||
.listOfObjects(Manga::class.java)
|
.listOfObjects(Manga::class.java)
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.queries
|
package eu.kanade.tachiyomi.data.database.queries
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.SourceIdMangaCountGetResolver
|
|
||||||
import exh.source.MERGED_SOURCE_ID
|
import exh.source.MERGED_SOURCE_ID
|
||||||
import eu.kanade.tachiyomi.data.database.tables.CategoryTable as Category
|
import eu.kanade.tachiyomi.data.database.tables.CategoryTable as Category
|
||||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable as Chapter
|
import eu.kanade.tachiyomi.data.database.tables.ChapterTable as Chapter
|
||||||
@@ -70,7 +69,7 @@ fun getReadMangaNotInLibraryQuery() =
|
|||||||
SELECT ${Manga.TABLE}.*
|
SELECT ${Manga.TABLE}.*
|
||||||
FROM ${Manga.TABLE}
|
FROM ${Manga.TABLE}
|
||||||
WHERE ${Manga.COL_FAVORITE} = 0 AND ${Manga.COL_ID} IN(
|
WHERE ${Manga.COL_FAVORITE} = 0 AND ${Manga.COL_ID} IN(
|
||||||
SELECT ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} FROM ${Chapter.TABLE} WHERE ${Chapter.COL_READ} = 1 OR ${Chapter.COL_LAST_PAGE_READ} != 0
|
SELECT ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} FROM ${Chapter.TABLE} WHERE ${Chapter.COL_READ} = 1
|
||||||
)
|
)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -222,16 +221,6 @@ fun getLatestChapterMangaQuery() =
|
|||||||
ORDER by max DESC
|
ORDER by max DESC
|
||||||
"""
|
"""
|
||||||
|
|
||||||
fun getChapterFetchDateMangaQuery() =
|
|
||||||
"""
|
|
||||||
SELECT ${Manga.TABLE}.*, MAX(${Chapter.TABLE}.${Chapter.COL_DATE_FETCH}) AS max
|
|
||||||
FROM ${Manga.TABLE}
|
|
||||||
JOIN ${Chapter.TABLE}
|
|
||||||
ON ${Manga.TABLE}.${Manga.COL_ID} = ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}
|
|
||||||
GROUP BY ${Manga.TABLE}.${Manga.COL_ID}
|
|
||||||
ORDER by max DESC
|
|
||||||
"""
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Query to get the categories for a manga.
|
* Query to get the categories for a manga.
|
||||||
*/
|
*/
|
||||||
@@ -242,14 +231,3 @@ fun getCategoriesForMangaQuery() =
|
|||||||
${MangaCategory.TABLE}.${MangaCategory.COL_CATEGORY_ID}
|
${MangaCategory.TABLE}.${MangaCategory.COL_CATEGORY_ID}
|
||||||
WHERE ${MangaCategory.COL_MANGA_ID} = ?
|
WHERE ${MangaCategory.COL_MANGA_ID} = ?
|
||||||
"""
|
"""
|
||||||
|
|
||||||
/** Query to get the list of sources in the database that have
|
|
||||||
* non-library manga, and how many
|
|
||||||
*/
|
|
||||||
fun getSourceIdsWithNonLibraryMangaQuery() =
|
|
||||||
"""
|
|
||||||
SELECT ${Manga.COL_SOURCE}, COUNT(*) as ${SourceIdMangaCountGetResolver.COL_COUNT}
|
|
||||||
FROM ${Manga.TABLE}
|
|
||||||
WHERE ${Manga.COL_FAVORITE} = 0
|
|
||||||
GROUP BY ${Manga.COL_SOURCE}
|
|
||||||
"""
|
|
||||||
|
|||||||
+14
-2
@@ -27,7 +27,9 @@ class HistoryLastReadPutResolver : HistoryPutResolver() {
|
|||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
|
|
||||||
cursor.use { putCursor ->
|
val putResult: PutResult
|
||||||
|
|
||||||
|
putResult = cursor.use { putCursor ->
|
||||||
if (putCursor.count == 0) {
|
if (putCursor.count == 0) {
|
||||||
val insertQuery = mapToInsertQuery(history)
|
val insertQuery = mapToInsertQuery(history)
|
||||||
val insertedId = db.lowLevel().insert(insertQuery, mapToContentValues(history))
|
val insertedId = db.lowLevel().insert(insertQuery, mapToContentValues(history))
|
||||||
@@ -37,15 +39,25 @@ class HistoryLastReadPutResolver : HistoryPutResolver() {
|
|||||||
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
|
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
putResult
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates update query
|
||||||
|
* @param obj history object
|
||||||
|
*/
|
||||||
override fun mapToUpdateQuery(obj: History) = UpdateQuery.builder()
|
override fun mapToUpdateQuery(obj: History) = UpdateQuery.builder()
|
||||||
.table(HistoryTable.TABLE)
|
.table(HistoryTable.TABLE)
|
||||||
.where("${HistoryTable.COL_CHAPTER_ID} = ?")
|
.where("${HistoryTable.COL_CHAPTER_ID} = ?")
|
||||||
.whereArgs(obj.chapter_id)
|
.whereArgs(obj.chapter_id)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
private fun mapToUpdateContentValues(history: History) =
|
/**
|
||||||
|
* Create content query
|
||||||
|
* @param history object
|
||||||
|
*/
|
||||||
|
fun mapToUpdateContentValues(history: History) =
|
||||||
contentValuesOf(
|
contentValuesOf(
|
||||||
HistoryTable.COL_LAST_READ to history.last_read
|
HistoryTable.COL_LAST_READ to history.last_read
|
||||||
)
|
)
|
||||||
|
|||||||
+3
-3
@@ -16,10 +16,10 @@ class LibraryMangaGetResolver : DefaultGetResolver<LibraryManga>(), BaseMangaGet
|
|||||||
val manga = LibraryManga()
|
val manga = LibraryManga()
|
||||||
|
|
||||||
mapBaseFromCursor(manga, cursor)
|
mapBaseFromCursor(manga, cursor)
|
||||||
manga.unread = cursor.getInt(cursor.getColumnIndexOrThrow(MangaTable.COL_UNREAD))
|
manga.unread = cursor.getInt(cursor.getColumnIndex(MangaTable.COL_UNREAD))
|
||||||
manga.category = cursor.getInt(cursor.getColumnIndexOrThrow(MangaTable.COL_CATEGORY))
|
manga.category = cursor.getInt(cursor.getColumnIndex(MangaTable.COL_CATEGORY))
|
||||||
// SY -->
|
// SY -->
|
||||||
manga.read = cursor.getInt(cursor.getColumnIndexOrThrow(MangaTable.COL_READ))
|
manga.read = cursor.getInt(cursor.getColumnIndex(MangaTable.COL_READ))
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
return manga
|
return manga
|
||||||
|
|||||||
+1
-1
@@ -20,7 +20,7 @@ class MangaChapterGetResolver : DefaultGetResolver<MangaChapter>() {
|
|||||||
val manga = mangaGetResolver.mapFromCursor(cursor)
|
val manga = mangaGetResolver.mapFromCursor(cursor)
|
||||||
val chapter = chapterGetResolver.mapFromCursor(cursor)
|
val chapter = chapterGetResolver.mapFromCursor(cursor)
|
||||||
manga.id = chapter.manga_id
|
manga.id = chapter.manga_id
|
||||||
manga.url = cursor.getString(cursor.getColumnIndexOrThrow("mangaUrl"))
|
manga.url = cursor.getString(cursor.getColumnIndex("mangaUrl"))
|
||||||
|
|
||||||
return MangaChapter(manga, chapter)
|
return MangaChapter(manga, chapter)
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -42,7 +42,7 @@ class MangaChapterHistoryGetResolver : DefaultGetResolver<MangaChapterHistory>()
|
|||||||
|
|
||||||
// Make certain column conflicts are dealt with
|
// Make certain column conflicts are dealt with
|
||||||
manga.id = chapter.manga_id
|
manga.id = chapter.manga_id
|
||||||
manga.url = cursor.getString(cursor.getColumnIndexOrThrow("mangaUrl"))
|
manga.url = cursor.getString(cursor.getColumnIndex("mangaUrl"))
|
||||||
chapter.id = history.chapter_id
|
chapter.id = history.chapter_id
|
||||||
|
|
||||||
// Return result
|
// Return result
|
||||||
|
|||||||
-32
@@ -1,32 +0,0 @@
|
|||||||
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
|
|
||||||
|
|
||||||
// [EXH]
|
|
||||||
class MangaFilteredScanlatorsPutResolver : 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_FILTERED_SCANLATORS to manga.filtered_scanlators
|
|
||||||
)
|
|
||||||
}
|
|
||||||
+17
-8
@@ -8,9 +8,8 @@ import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
|
|||||||
import eu.kanade.tachiyomi.data.database.inTransactionReturn
|
import eu.kanade.tachiyomi.data.database.inTransactionReturn
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
||||||
import kotlin.reflect.KProperty1
|
|
||||||
|
|
||||||
class MangaFlagsPutResolver(private val colName: String, private val fieldGetter: KProperty1<Manga, Int>) : PutResolver<Manga>() {
|
class MangaFlagsPutResolver(private val updateAll: Boolean = false) : PutResolver<Manga>() {
|
||||||
|
|
||||||
override fun performPut(db: StorIOSQLite, manga: Manga) = db.inTransactionReturn {
|
override fun performPut(db: StorIOSQLite, manga: Manga) = db.inTransactionReturn {
|
||||||
val updateQuery = mapToUpdateQuery(manga)
|
val updateQuery = mapToUpdateQuery(manga)
|
||||||
@@ -20,14 +19,24 @@ class MangaFlagsPutResolver(private val colName: String, private val fieldGetter
|
|||||||
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
|
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder()
|
fun mapToUpdateQuery(manga: Manga): UpdateQuery {
|
||||||
.table(MangaTable.TABLE)
|
val builder = UpdateQuery.builder()
|
||||||
.where("${MangaTable.COL_ID} = ?")
|
|
||||||
.whereArgs(manga.id)
|
return if (updateAll) {
|
||||||
.build()
|
builder
|
||||||
|
.table(MangaTable.TABLE)
|
||||||
|
.build()
|
||||||
|
} else {
|
||||||
|
builder
|
||||||
|
.table(MangaTable.TABLE)
|
||||||
|
.where("${MangaTable.COL_ID} = ?")
|
||||||
|
.whereArgs(manga.id)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun mapToContentValues(manga: Manga) =
|
fun mapToContentValues(manga: Manga) =
|
||||||
contentValuesOf(
|
contentValuesOf(
|
||||||
colName to fieldGetter.get(manga)
|
MangaTable.COL_CHAPTER_FLAGS to manga.chapter_flags
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-14
@@ -1,5 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.resolvers
|
package eu.kanade.tachiyomi.data.database.resolvers
|
||||||
|
|
||||||
|
import android.content.ContentValues
|
||||||
import androidx.core.content.contentValuesOf
|
import androidx.core.content.contentValuesOf
|
||||||
import com.pushtorefresh.storio.sqlite.StorIOSQLite
|
import com.pushtorefresh.storio.sqlite.StorIOSQLite
|
||||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResolver
|
import com.pushtorefresh.storio.sqlite.operations.put.PutResolver
|
||||||
@@ -8,7 +9,6 @@ import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
|
|||||||
import eu.kanade.tachiyomi.data.database.inTransactionReturn
|
import eu.kanade.tachiyomi.data.database.inTransactionReturn
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
||||||
import exh.util.nullIfZero
|
|
||||||
|
|
||||||
class MangaInfoPutResolver(val reset: Boolean = false) : PutResolver<Manga>() {
|
class MangaInfoPutResolver(val reset: Boolean = false) : PutResolver<Manga>() {
|
||||||
|
|
||||||
@@ -31,20 +31,15 @@ class MangaInfoPutResolver(val reset: Boolean = false) : PutResolver<Manga>() {
|
|||||||
MangaTable.COL_GENRE to manga.originalGenre,
|
MangaTable.COL_GENRE to manga.originalGenre,
|
||||||
MangaTable.COL_AUTHOR to manga.originalAuthor,
|
MangaTable.COL_AUTHOR to manga.originalAuthor,
|
||||||
MangaTable.COL_ARTIST to manga.originalArtist,
|
MangaTable.COL_ARTIST to manga.originalArtist,
|
||||||
MangaTable.COL_DESCRIPTION to manga.originalDescription,
|
MangaTable.COL_DESCRIPTION to manga.originalDescription
|
||||||
MangaTable.COL_STATUS to manga.originalStatus
|
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun resetToContentValues(manga: Manga) = contentValuesOf(
|
fun resetToContentValues(manga: Manga) = ContentValues(1).apply {
|
||||||
MangaTable.COL_TITLE to manga.title.split(splitter).last(),
|
val splitter = "▒ ▒∩▒"
|
||||||
MangaTable.COL_GENRE to manga.genre?.split(splitter)?.lastOrNull(),
|
put(MangaTable.COL_TITLE, manga.title.split(splitter).last())
|
||||||
MangaTable.COL_AUTHOR to manga.author?.split(splitter)?.lastOrNull(),
|
put(MangaTable.COL_GENRE, manga.genre?.split(splitter)?.lastOrNull())
|
||||||
MangaTable.COL_ARTIST to manga.artist?.split(splitter)?.lastOrNull(),
|
put(MangaTable.COL_AUTHOR, manga.author?.split(splitter)?.lastOrNull())
|
||||||
MangaTable.COL_DESCRIPTION to manga.description?.split(splitter)?.lastOrNull(),
|
put(MangaTable.COL_ARTIST, manga.artist?.split(splitter)?.lastOrNull())
|
||||||
MangaTable.COL_STATUS to manga.status.nullIfZero()?.toString()?.split(splitter)?.lastOrNull()
|
put(MangaTable.COL_DESCRIPTION, manga.description?.split(splitter)?.lastOrNull())
|
||||||
)
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val splitter = "▒ ▒∩▒"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -30,6 +30,6 @@ class MangaMigrationPutResolver : PutResolver<Manga>() {
|
|||||||
MangaTable.COL_DATE_ADDED to manga.date_added,
|
MangaTable.COL_DATE_ADDED to manga.date_added,
|
||||||
MangaTable.COL_TITLE to manga.title,
|
MangaTable.COL_TITLE to manga.title,
|
||||||
MangaTable.COL_CHAPTER_FLAGS to manga.chapter_flags,
|
MangaTable.COL_CHAPTER_FLAGS to manga.chapter_flags,
|
||||||
MangaTable.COL_VIEWER to manga.viewer_flags
|
MangaTable.COL_VIEWER to manga.viewer
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-5
@@ -9,8 +9,7 @@ import eu.kanade.tachiyomi.data.database.inTransactionReturn
|
|||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
||||||
|
|
||||||
// SY
|
class MangaViewerPutResolver : PutResolver<Manga>() {
|
||||||
class MangaThumbnailPutResolver : PutResolver<Manga>() {
|
|
||||||
|
|
||||||
override fun performPut(db: StorIOSQLite, manga: Manga) = db.inTransactionReturn {
|
override fun performPut(db: StorIOSQLite, manga: Manga) = db.inTransactionReturn {
|
||||||
val updateQuery = mapToUpdateQuery(manga)
|
val updateQuery = mapToUpdateQuery(manga)
|
||||||
@@ -26,7 +25,8 @@ class MangaThumbnailPutResolver : PutResolver<Manga>() {
|
|||||||
.whereArgs(manga.id)
|
.whereArgs(manga.id)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
fun mapToContentValues(manga: Manga) = contentValuesOf(
|
fun mapToContentValues(manga: Manga) =
|
||||||
MangaTable.COL_THUMBNAIL_URL to manga.thumbnail_url
|
contentValuesOf(
|
||||||
)
|
MangaTable.COL_VIEWER to manga.viewer
|
||||||
|
)
|
||||||
}
|
}
|
||||||
-23
@@ -1,23 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.resolvers
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.database.Cursor
|
|
||||||
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.SourceIdMangaCount
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
|
||||||
|
|
||||||
class SourceIdMangaCountGetResolver : DefaultGetResolver<SourceIdMangaCount>() {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
val INSTANCE = SourceIdMangaCountGetResolver()
|
|
||||||
const val COL_COUNT = "manga_count"
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("Range")
|
|
||||||
override fun mapFromCursor(cursor: Cursor): SourceIdMangaCount {
|
|
||||||
val sourceID = cursor.getLong(cursor.getColumnIndexOrThrow(MangaTable.COL_SOURCE))
|
|
||||||
val count = cursor.getInt(cursor.getColumnIndexOrThrow(COL_COUNT))
|
|
||||||
|
|
||||||
return SourceIdMangaCount(sourceID, count)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -62,7 +62,4 @@ object ChapterTable {
|
|||||||
|
|
||||||
val addScanlator: String
|
val addScanlator: String
|
||||||
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_SCANLATOR TEXT DEFAULT NULL"
|
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_SCANLATOR TEXT DEFAULT NULL"
|
||||||
|
|
||||||
val fixDateUploadIfNeeded: String
|
|
||||||
get() = "UPDATE $TABLE SET $COL_DATE_UPLOAD = $COL_DATE_FETCH WHERE $COL_DATE_UPLOAD = 0"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,9 +28,6 @@ object MangaTable {
|
|||||||
|
|
||||||
const val COL_LAST_UPDATE = "last_update"
|
const val COL_LAST_UPDATE = "last_update"
|
||||||
|
|
||||||
// Not actually used anymore
|
|
||||||
const val COL_NEXT_UPDATE = "next_update"
|
|
||||||
|
|
||||||
const val COL_DATE_ADDED = "date_added"
|
const val COL_DATE_ADDED = "date_added"
|
||||||
|
|
||||||
const val COL_INITIALIZED = "initialized"
|
const val COL_INITIALIZED = "initialized"
|
||||||
@@ -43,8 +40,6 @@ object MangaTable {
|
|||||||
|
|
||||||
// SY ->>
|
// SY ->>
|
||||||
const val COL_READ = "read"
|
const val COL_READ = "read"
|
||||||
|
|
||||||
const val COL_FILTERED_SCANLATORS = "filtered_scanlators"
|
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
const val COL_CATEGORY = "category"
|
const val COL_CATEGORY = "category"
|
||||||
@@ -66,13 +61,11 @@ object MangaTable {
|
|||||||
$COL_THUMBNAIL_URL TEXT,
|
$COL_THUMBNAIL_URL TEXT,
|
||||||
$COL_FAVORITE INTEGER NOT NULL,
|
$COL_FAVORITE INTEGER NOT NULL,
|
||||||
$COL_LAST_UPDATE LONG,
|
$COL_LAST_UPDATE LONG,
|
||||||
$COL_NEXT_UPDATE LONG,
|
|
||||||
$COL_INITIALIZED BOOLEAN NOT NULL,
|
$COL_INITIALIZED BOOLEAN NOT NULL,
|
||||||
$COL_VIEWER INTEGER NOT NULL,
|
$COL_VIEWER INTEGER NOT NULL,
|
||||||
$COL_CHAPTER_FLAGS INTEGER NOT NULL,
|
$COL_CHAPTER_FLAGS INTEGER NOT NULL,
|
||||||
$COL_COVER_LAST_MODIFIED LONG NOT NULL,
|
$COL_COVER_LAST_MODIFIED LONG NOT NULL,
|
||||||
$COL_DATE_ADDED LONG NOT NULL,
|
$COL_DATE_ADDED LONG NOT NULL
|
||||||
$COL_FILTERED_SCANLATORS TEXT
|
|
||||||
)"""
|
)"""
|
||||||
|
|
||||||
val createUrlIndexQuery: String
|
val createUrlIndexQuery: String
|
||||||
@@ -97,12 +90,4 @@ object MangaTable {
|
|||||||
"FROM $TABLE INNER JOIN ${ChapterTable.TABLE} " +
|
"FROM $TABLE INNER JOIN ${ChapterTable.TABLE} " +
|
||||||
"ON $TABLE.$COL_ID = ${ChapterTable.TABLE}.${ChapterTable.COL_MANGA_ID} " +
|
"ON $TABLE.$COL_ID = ${ChapterTable.TABLE}.${ChapterTable.COL_MANGA_ID} " +
|
||||||
"GROUP BY $TABLE.$COL_ID)"
|
"GROUP BY $TABLE.$COL_ID)"
|
||||||
|
|
||||||
val addNextUpdateCol: String
|
|
||||||
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_NEXT_UPDATE LONG DEFAULT 0"
|
|
||||||
|
|
||||||
// SY -->
|
|
||||||
val addFilteredScanlators: String
|
|
||||||
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_FILTERED_SCANLATORS TEXT"
|
|
||||||
// SY <--
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ object TrackTable {
|
|||||||
$COL_MEDIA_ID INTEGER NOT NULL,
|
$COL_MEDIA_ID INTEGER NOT NULL,
|
||||||
$COL_LIBRARY_ID INTEGER,
|
$COL_LIBRARY_ID INTEGER,
|
||||||
$COL_TITLE TEXT NOT NULL,
|
$COL_TITLE TEXT NOT NULL,
|
||||||
$COL_LAST_CHAPTER_READ REAL NOT NULL,
|
$COL_LAST_CHAPTER_READ INTEGER NOT NULL,
|
||||||
$COL_TOTAL_CHAPTERS INTEGER NOT NULL,
|
$COL_TOTAL_CHAPTERS INTEGER NOT NULL,
|
||||||
$COL_STATUS INTEGER NOT NULL,
|
$COL_STATUS INTEGER NOT NULL,
|
||||||
$COL_SCORE FLOAT NOT NULL,
|
$COL_SCORE FLOAT NOT NULL,
|
||||||
@@ -62,19 +62,4 @@ object TrackTable {
|
|||||||
|
|
||||||
val addFinishDate: String
|
val addFinishDate: String
|
||||||
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_FINISH_DATE LONG NOT NULL DEFAULT 0"
|
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_FINISH_DATE LONG NOT NULL DEFAULT 0"
|
||||||
|
|
||||||
val renameTableToTemp: String
|
|
||||||
get() =
|
|
||||||
"ALTER TABLE $TABLE RENAME TO ${TABLE}_tmp"
|
|
||||||
|
|
||||||
val insertFromTempTable: String
|
|
||||||
get() =
|
|
||||||
"""
|
|
||||||
|INSERT INTO $TABLE($COL_ID,$COL_MANGA_ID,$COL_SYNC_ID,$COL_MEDIA_ID,$COL_LIBRARY_ID,$COL_TITLE,$COL_LAST_CHAPTER_READ,$COL_TOTAL_CHAPTERS,$COL_STATUS,$COL_SCORE,$COL_TRACKING_URL,$COL_START_DATE,$COL_FINISH_DATE)
|
|
||||||
|SELECT $COL_ID,$COL_MANGA_ID,$COL_SYNC_ID,$COL_MEDIA_ID,$COL_LIBRARY_ID,$COL_TITLE,$COL_LAST_CHAPTER_READ,$COL_TOTAL_CHAPTERS,$COL_STATUS,$COL_SCORE,$COL_TRACKING_URL,$COL_START_DATE,$COL_FINISH_DATE
|
|
||||||
|FROM ${TABLE}_tmp
|
|
||||||
""".trimMargin()
|
|
||||||
|
|
||||||
val dropTempTable: String
|
|
||||||
get() = "DROP TABLE ${TABLE}_tmp"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ class DownloadCache(
|
|||||||
if (sourceDir != null) {
|
if (sourceDir != null) {
|
||||||
val mangaDir = sourceDir.files[provider.getMangaDirName(manga)]
|
val mangaDir = sourceDir.files[provider.getMangaDirName(manga)]
|
||||||
if (mangaDir != null) {
|
if (mangaDir != null) {
|
||||||
return provider.getValidChapterDirNames(chapter).any { it in mangaDir.files }
|
return provider.getValidChapterDirNames(chapter).any { it in mangaDir.files || "$it.cbz" in mangaDir.files }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
@@ -196,6 +196,8 @@ class DownloadCache(
|
|||||||
provider.getValidChapterDirNames(chapter).forEach {
|
provider.getValidChapterDirNames(chapter).forEach {
|
||||||
if (it in mangaDir.files) {
|
if (it in mangaDir.files) {
|
||||||
mangaDir.files -= it
|
mangaDir.files -= it
|
||||||
|
} else if ("$it.cbz" in mangaDir.files) {
|
||||||
|
mangaDir.files -= "$it.cbz"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -210,7 +212,6 @@ class DownloadCache(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -227,6 +228,8 @@ class DownloadCache(
|
|||||||
provider.getValidChapterDirNames(chapter).forEach {
|
provider.getValidChapterDirNames(chapter).forEach {
|
||||||
if (it in mangaDir.files) {
|
if (it in mangaDir.files) {
|
||||||
mangaDir.files -= it
|
mangaDir.files -= it
|
||||||
|
} else if ("$it.cbz" in mangaDir.files) {
|
||||||
|
mangaDir.files -= "$it.cbz"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import android.content.Context
|
|||||||
import com.hippo.unifile.UniFile
|
import com.hippo.unifile.UniFile
|
||||||
import com.jakewharton.rxrelay.BehaviorRelay
|
import com.jakewharton.rxrelay.BehaviorRelay
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.download.model.Download
|
import eu.kanade.tachiyomi.data.download.model.Download
|
||||||
@@ -13,14 +12,8 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
|||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
|
||||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
|
||||||
import eu.kanade.tachiyomi.util.system.logcat
|
|
||||||
import exh.log.xLogE
|
|
||||||
import logcat.LogPriority
|
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import uy.kohesive.injekt.Injekt
|
import timber.log.Timber
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -30,10 +23,7 @@ import uy.kohesive.injekt.injectLazy
|
|||||||
*
|
*
|
||||||
* @param context the application context.
|
* @param context the application context.
|
||||||
*/
|
*/
|
||||||
class DownloadManager(
|
class DownloadManager(/* SY private */ val context: Context) {
|
||||||
private val context: Context,
|
|
||||||
private val db: DatabaseHelper = Injekt.get()
|
|
||||||
) {
|
|
||||||
|
|
||||||
private val sourceManager: SourceManager by injectLazy()
|
private val sourceManager: SourceManager by injectLazy()
|
||||||
private val preferences: PreferencesHelper by injectLazy()
|
private val preferences: PreferencesHelper by injectLazy()
|
||||||
@@ -41,7 +31,7 @@ class DownloadManager(
|
|||||||
/**
|
/**
|
||||||
* Downloads provider, used to retrieve the folders where the chapters are or should be stored.
|
* Downloads provider, used to retrieve the folders where the chapters are or should be stored.
|
||||||
*/
|
*/
|
||||||
val provider = DownloadProvider(context)
|
private val provider = DownloadProvider(context)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cache of downloaded chapters.
|
* Cache of downloaded chapters.
|
||||||
@@ -104,23 +94,6 @@ class DownloadManager(
|
|||||||
downloader.clearQueue(isNotification)
|
downloader.clearQueue(isNotification)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDownloadNow(chapter: Chapter) {
|
|
||||||
val download = downloader.queue.find { it.chapter.id == chapter.id } ?: return
|
|
||||||
val queue = downloader.queue.toMutableList()
|
|
||||||
queue.remove(download)
|
|
||||||
queue.add(0, download)
|
|
||||||
reorderQueue(queue)
|
|
||||||
if (isPaused()) {
|
|
||||||
if (DownloadService.isRunning(context)) {
|
|
||||||
downloader.start()
|
|
||||||
} else {
|
|
||||||
DownloadService.start(context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isPaused() = downloader.isPaused()
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reorders the download queue.
|
* Reorders the download queue.
|
||||||
*
|
*
|
||||||
@@ -226,16 +199,7 @@ class DownloadManager(
|
|||||||
* @param download the download to cancel.
|
* @param download the download to cancel.
|
||||||
*/
|
*/
|
||||||
fun deletePendingDownload(download: Download) {
|
fun deletePendingDownload(download: Download) {
|
||||||
deleteChapters(listOf(download.chapter), download.manga, download.source, true)
|
deleteChapters(listOf(download.chapter), download.manga, download.source)
|
||||||
}
|
|
||||||
|
|
||||||
fun deletePendingDownloads(vararg downloads: Download) {
|
|
||||||
val downloadsByManga = downloads.groupBy { it.manga.id }
|
|
||||||
downloadsByManga.map { entry ->
|
|
||||||
val manga = entry.value.first().manga
|
|
||||||
val source = entry.value.first().source
|
|
||||||
deleteChapters(entry.value.map { it.chapter }, manga, source, true)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -244,25 +208,19 @@ class DownloadManager(
|
|||||||
* @param chapters the list of chapters to delete.
|
* @param chapters the list of chapters to delete.
|
||||||
* @param manga the manga of the chapters.
|
* @param manga the manga of the chapters.
|
||||||
* @param source the source of the chapters.
|
* @param source the source of the chapters.
|
||||||
* @param isCancelling true if it's simply cancelling a download
|
|
||||||
*/
|
*/
|
||||||
fun deleteChapters(chapters: List<Chapter>, manga: Manga, source: Source, isCancelling: Boolean = false): List<Chapter> {
|
fun deleteChapters(chapters: List<Chapter>, manga: Manga, source: Source): List<Chapter> {
|
||||||
val filteredChapters = if (isCancelling) {
|
val filteredChapters = getChaptersToDelete(chapters)
|
||||||
chapters
|
|
||||||
} else {
|
removeFromDownloadQueue(filteredChapters)
|
||||||
getChaptersToDelete(chapters, manga)
|
|
||||||
|
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()
|
||||||
}
|
}
|
||||||
|
|
||||||
launchIO {
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return filteredChapters
|
return filteredChapters
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -304,7 +262,7 @@ class DownloadManager(
|
|||||||
|
|
||||||
if (removeNonFavorite && !manga.favorite) {
|
if (removeNonFavorite && !manga.favorite) {
|
||||||
val mangaFolder = provider.getMangaDir(manga, source)
|
val mangaFolder = provider.getMangaDir(manga, source)
|
||||||
cleaned += 1 + mangaFolder.listFiles().orEmpty().size
|
cleaned += 1 + (mangaFolder.listFiles()?.size ?: 0)
|
||||||
mangaFolder.delete()
|
mangaFolder.delete()
|
||||||
cache.removeManga(manga)
|
cache.removeManga(manga)
|
||||||
return cleaned
|
return cleaned
|
||||||
@@ -325,11 +283,12 @@ class DownloadManager(
|
|||||||
|
|
||||||
if (cache.getDownloadCount(manga) == 0) {
|
if (cache.getDownloadCount(manga) == 0) {
|
||||||
val mangaFolder = provider.getMangaDir(manga, source)
|
val mangaFolder = provider.getMangaDir(manga, source)
|
||||||
if (!mangaFolder.listFiles().isNullOrEmpty()) {
|
val size = mangaFolder.listFiles()?.size ?: 0
|
||||||
|
if (size == 0) {
|
||||||
mangaFolder.delete()
|
mangaFolder.delete()
|
||||||
cache.removeManga(manga)
|
cache.removeManga(manga)
|
||||||
} else {
|
} else {
|
||||||
xLogE("Cache and download folder doesn't match for " + manga.title)
|
Timber.e("Cache and download folder doesn't match for %s", manga.title)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return cleaned
|
return cleaned
|
||||||
@@ -343,11 +302,9 @@ class DownloadManager(
|
|||||||
* @param source the source of the manga.
|
* @param source the source of the manga.
|
||||||
*/
|
*/
|
||||||
fun deleteManga(manga: Manga, source: Source) {
|
fun deleteManga(manga: Manga, source: Source) {
|
||||||
launchIO {
|
downloader.queue.remove(manga)
|
||||||
downloader.queue.remove(manga)
|
provider.findMangaDir(manga, source)?.delete()
|
||||||
provider.findMangaDir(manga, source)?.delete()
|
cache.removeManga(manga)
|
||||||
cache.removeManga(manga)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -357,7 +314,7 @@ class DownloadManager(
|
|||||||
* @param manga the manga of the chapters.
|
* @param manga the manga of the chapters.
|
||||||
*/
|
*/
|
||||||
fun enqueueDeleteChapters(chapters: List<Chapter>, manga: Manga) {
|
fun enqueueDeleteChapters(chapters: List<Chapter>, manga: Manga) {
|
||||||
pendingDeleter.addChapters(getChaptersToDelete(chapters, manga), manga)
|
pendingDeleter.addChapters(getChaptersToDelete(chapters), manga)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -381,46 +338,27 @@ class DownloadManager(
|
|||||||
*/
|
*/
|
||||||
fun renameChapter(source: Source, manga: Manga, oldChapter: Chapter, newChapter: Chapter) {
|
fun renameChapter(source: Source, manga: Manga, oldChapter: Chapter, newChapter: Chapter) {
|
||||||
val oldNames = provider.getValidChapterDirNames(oldChapter)
|
val oldNames = provider.getValidChapterDirNames(oldChapter)
|
||||||
|
val newName = provider.getChapterDirName(newChapter)
|
||||||
val mangaDir = provider.getMangaDir(manga, source)
|
val mangaDir = provider.getMangaDir(manga, source)
|
||||||
|
|
||||||
// Assume there's only 1 version of the chapter name formats present
|
// Assume there's only 1 version of the chapter name formats present
|
||||||
val oldDownload = oldNames.asSequence()
|
val oldFolder = oldNames.asSequence()
|
||||||
.mapNotNull { mangaDir.findFile(it) }
|
.mapNotNull { mangaDir.findFile(it) ?: mangaDir.findFile("$it.cbz") }
|
||||||
.firstOrNull() ?: return
|
.firstOrNull()
|
||||||
|
|
||||||
var newName = provider.getChapterDirName(newChapter)
|
if (oldFolder?.renameTo(newName + if (oldFolder.name?.endsWith(".cbz") == true) ".cbz" else "") == true) {
|
||||||
if (oldDownload.isFile && oldDownload.name?.endsWith(".cbz") == true) {
|
|
||||||
newName += ".cbz"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (oldDownload.renameTo(newName)) {
|
|
||||||
cache.removeChapter(oldChapter, manga)
|
cache.removeChapter(oldChapter, manga)
|
||||||
cache.addChapter(newName, mangaDir, manga)
|
cache.addChapter(newName + if (oldFolder.name?.endsWith(".cbz") == true) ".cbz" else "", mangaDir, manga)
|
||||||
} else {
|
} else {
|
||||||
logcat(LogPriority.ERROR) { "Could not rename downloaded chapter: ${oldNames.joinToString()}." }
|
Timber.e("Could not rename downloaded chapter: %s.", oldNames.joinToString())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getChaptersToDelete(chapters: List<Chapter>, manga: Manga): List<Chapter> {
|
private fun getChaptersToDelete(chapters: List<Chapter>): List<Chapter> {
|
||||||
// Retrieve the categories that are set to exclude from being deleted on read
|
return if (!preferences.removeBookmarkedChapters()) {
|
||||||
val categoriesToExclude = preferences.removeExcludeCategories().get().map(String::toInt)
|
|
||||||
val categoriesForManga = db.getCategoriesForManga(manga).executeAsBlocking()
|
|
||||||
.mapNotNull { it.id }
|
|
||||||
.takeUnless { it.isEmpty() }
|
|
||||||
?: listOf(0)
|
|
||||||
|
|
||||||
return if (categoriesForManga.intersect(categoriesToExclude).isNotEmpty()) {
|
|
||||||
chapters.filterNot { it.read }
|
|
||||||
} else if (!preferences.removeBookmarkedChapters()) {
|
|
||||||
chapters.filterNot { it.bookmark }
|
chapters.filterNot { it.bookmark }
|
||||||
} else {
|
} else {
|
||||||
chapters
|
chapters
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun renameMangaDir(oldTitle: String, newTitle: String, source: Long) {
|
|
||||||
val sourceDir = provider.findSourceDir(sourceManager.getOrStub(source)) ?: return
|
|
||||||
val mangaDir = sourceDir.findFile(DiskUtil.buildValidFilename(oldTitle), true) ?: return
|
|
||||||
mangaDir.renameTo(DiskUtil.buildValidFilename(newTitle))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,8 +27,6 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
private val progressNotificationBuilder by lazy {
|
private val progressNotificationBuilder by lazy {
|
||||||
context.notificationBuilder(Notifications.CHANNEL_DOWNLOADER_PROGRESS) {
|
context.notificationBuilder(Notifications.CHANNEL_DOWNLOADER_PROGRESS) {
|
||||||
setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher))
|
setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher))
|
||||||
setAutoCancel(false)
|
|
||||||
setOnlyAlertOnce(true)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,7 +50,7 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
/**
|
/**
|
||||||
* Updated when error is thrown
|
* Updated when error is thrown
|
||||||
*/
|
*/
|
||||||
private var errorThrown = false
|
var errorThrown = false
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updated when paused
|
* Updated when paused
|
||||||
@@ -83,8 +81,10 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
*/
|
*/
|
||||||
fun onProgressChange(download: Download) {
|
fun onProgressChange(download: Download) {
|
||||||
with(progressNotificationBuilder) {
|
with(progressNotificationBuilder) {
|
||||||
|
// Check if first call.
|
||||||
if (!isDownloading) {
|
if (!isDownloading) {
|
||||||
setSmallIcon(android.R.drawable.stat_sys_download)
|
setSmallIcon(android.R.drawable.stat_sys_download)
|
||||||
|
setAutoCancel(false)
|
||||||
clearActions()
|
clearActions()
|
||||||
// Open download manager when clicked
|
// Open download manager when clicked
|
||||||
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
||||||
@@ -105,7 +105,6 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
|
|
||||||
if (preferences.hideNotificationContent()) {
|
if (preferences.hideNotificationContent()) {
|
||||||
setContentTitle(downloadingProgressText)
|
setContentTitle(downloadingProgressText)
|
||||||
setContentText(null)
|
|
||||||
} else {
|
} else {
|
||||||
val title = download.manga.title.chop(15)
|
val title = download.manga.title.chop(15)
|
||||||
val quotedTitle = Pattern.quote(title)
|
val quotedTitle = Pattern.quote(title)
|
||||||
@@ -115,7 +114,6 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setProgress(download.pages!!.size, download.downloadedImages, false)
|
setProgress(download.pages!!.size, download.downloadedImages, false)
|
||||||
setOngoing(true)
|
|
||||||
|
|
||||||
show(Notifications.ID_DOWNLOAD_CHAPTER_PROGRESS)
|
show(Notifications.ID_DOWNLOAD_CHAPTER_PROGRESS)
|
||||||
}
|
}
|
||||||
@@ -129,8 +127,8 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
setContentTitle(context.getString(R.string.chapter_paused))
|
setContentTitle(context.getString(R.string.chapter_paused))
|
||||||
setContentText(context.getString(R.string.download_notifier_download_paused))
|
setContentText(context.getString(R.string.download_notifier_download_paused))
|
||||||
setSmallIcon(R.drawable.ic_pause_24dp)
|
setSmallIcon(R.drawable.ic_pause_24dp)
|
||||||
|
setAutoCancel(false)
|
||||||
setProgress(0, 0, false)
|
setProgress(0, 0, false)
|
||||||
setOngoing(false)
|
|
||||||
clearActions()
|
clearActions()
|
||||||
// Open download manager when clicked
|
// Open download manager when clicked
|
||||||
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
||||||
@@ -188,8 +186,8 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
fun onWarning(reason: String) {
|
fun onWarning(reason: String) {
|
||||||
with(errorNotificationBuilder) {
|
with(errorNotificationBuilder) {
|
||||||
setContentTitle(context.getString(R.string.download_notifier_downloader_title))
|
setContentTitle(context.getString(R.string.download_notifier_downloader_title))
|
||||||
setStyle(NotificationCompat.BigTextStyle().bigText(reason))
|
setContentText(reason)
|
||||||
setSmallIcon(R.drawable.ic_warning_white_24dp)
|
setSmallIcon(android.R.drawable.stat_sys_warning)
|
||||||
setAutoCancel(true)
|
setAutoCancel(true)
|
||||||
clearActions()
|
clearActions()
|
||||||
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
||||||
@@ -209,15 +207,17 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
* @param error string containing error information.
|
* @param error string containing error information.
|
||||||
* @param chapter string containing chapter title.
|
* @param chapter string containing chapter title.
|
||||||
*/
|
*/
|
||||||
fun onError(error: String? = null, chapter: String? = null, mangaTitle: String? = null) {
|
fun onError(error: String? = null, chapter: String? = null) {
|
||||||
// Create notification
|
// Create notification
|
||||||
with(errorNotificationBuilder) {
|
with(errorNotificationBuilder) {
|
||||||
setContentTitle(
|
setContentTitle(
|
||||||
mangaTitle?.plus(": $chapter") ?: context.getString(R.string.download_notifier_downloader_title)
|
chapter
|
||||||
|
?: context.getString(R.string.download_notifier_downloader_title)
|
||||||
)
|
)
|
||||||
setContentText(error ?: context.getString(R.string.download_notifier_unknown_error))
|
setContentText(error ?: context.getString(R.string.download_notifier_unknown_error))
|
||||||
setSmallIcon(R.drawable.ic_warning_white_24dp)
|
setSmallIcon(android.R.drawable.stat_sys_warning)
|
||||||
clearActions()
|
clearActions()
|
||||||
|
setAutoCancel(false)
|
||||||
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
||||||
setProgress(0, 0, false)
|
setProgress(0, 0, false)
|
||||||
|
|
||||||
|
|||||||
@@ -155,7 +155,7 @@ class DownloadPendingDeleter(context: Context) {
|
|||||||
* Returns a manga entry from a manga model.
|
* Returns a manga entry from a manga model.
|
||||||
*/
|
*/
|
||||||
private fun Manga.toEntry(): MangaEntry {
|
private fun Manga.toEntry(): MangaEntry {
|
||||||
return MangaEntry(id!!, url, originalTitle, source)
|
return MangaEntry(id!!, url, title, source)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -9,11 +9,10 @@ import eu.kanade.tachiyomi.data.database.models.Manga
|
|||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||||
import eu.kanade.tachiyomi.util.system.logcat
|
|
||||||
import kotlinx.coroutines.MainScope
|
import kotlinx.coroutines.MainScope
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import logcat.LogPriority
|
import timber.log.Timber
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -54,8 +53,8 @@ class DownloadProvider(private val context: Context) {
|
|||||||
return downloadsDir
|
return downloadsDir
|
||||||
.createDirectory(getSourceDirName(source))
|
.createDirectory(getSourceDirName(source))
|
||||||
.createDirectory(getMangaDirName(manga))
|
.createDirectory(getMangaDirName(manga))
|
||||||
} catch (e: Throwable) {
|
} catch (e: NullPointerException) {
|
||||||
logcat(LogPriority.ERROR, e) { "Invalid download directory" }
|
Timber.w(e)
|
||||||
throw Exception(context.getString(R.string.invalid_download_dir))
|
throw Exception(context.getString(R.string.invalid_download_dir))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -66,7 +65,7 @@ class DownloadProvider(private val context: Context) {
|
|||||||
* @param source the source to query.
|
* @param source the source to query.
|
||||||
*/
|
*/
|
||||||
fun findSourceDir(source: Source): UniFile? {
|
fun findSourceDir(source: Source): UniFile? {
|
||||||
return downloadsDir.findFile(getSourceDirName(source), true)
|
return downloadsDir.findFile(getSourceDirName(source))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -77,7 +76,7 @@ class DownloadProvider(private val context: Context) {
|
|||||||
*/
|
*/
|
||||||
fun findMangaDir(manga: Manga, source: Source): UniFile? {
|
fun findMangaDir(manga: Manga, source: Source): UniFile? {
|
||||||
val sourceDir = findSourceDir(source)
|
val sourceDir = findSourceDir(source)
|
||||||
return sourceDir?.findFile(getMangaDirName(manga), true)
|
return sourceDir?.findFile(getMangaDirName(manga))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -90,7 +89,7 @@ class DownloadProvider(private val context: Context) {
|
|||||||
fun findChapterDir(chapter: Chapter, manga: Manga, source: Source): UniFile? {
|
fun findChapterDir(chapter: Chapter, manga: Manga, source: Source): UniFile? {
|
||||||
val mangaDir = findMangaDir(manga, source)
|
val mangaDir = findMangaDir(manga, source)
|
||||||
return getValidChapterDirNames(chapter).asSequence()
|
return getValidChapterDirNames(chapter).asSequence()
|
||||||
.mapNotNull { mangaDir?.findFile(it, true) }
|
.mapNotNull { mangaDir?.findFile(it) ?: mangaDir?.findFile("$it.cbz") }
|
||||||
.firstOrNull()
|
.firstOrNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,7 +104,7 @@ class DownloadProvider(private val context: Context) {
|
|||||||
val mangaDir = findMangaDir(manga, source) ?: return emptyList()
|
val mangaDir = findMangaDir(manga, source) ?: return emptyList()
|
||||||
return chapters.mapNotNull { chapter ->
|
return chapters.mapNotNull { chapter ->
|
||||||
getValidChapterDirNames(chapter).asSequence()
|
getValidChapterDirNames(chapter).asSequence()
|
||||||
.mapNotNull { mangaDir.findFile(it) }
|
.mapNotNull { mangaDir.findFile(it) ?: mangaDir.findFile("$it.cbz") }
|
||||||
.firstOrNull()
|
.firstOrNull()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -124,12 +123,14 @@ class DownloadProvider(private val context: Context) {
|
|||||||
source: Source
|
source: Source
|
||||||
): List<UniFile> {
|
): List<UniFile> {
|
||||||
val mangaDir = findMangaDir(manga, source) ?: return emptyList()
|
val mangaDir = findMangaDir(manga, source) ?: return emptyList()
|
||||||
return mangaDir.listFiles().orEmpty().asList().filter {
|
return mangaDir.listFiles()!!.asList().filter {
|
||||||
chapters.find { chp ->
|
(
|
||||||
getValidChapterDirNames(chp).any { dir ->
|
chapters.find { chp ->
|
||||||
mangaDir.findFile(dir) != null
|
getValidChapterDirNames(chp).any { dir ->
|
||||||
}
|
mangaDir.findFile(dir) ?: mangaDir.findFile("$dir.cbz") != null
|
||||||
} == null || it.name?.endsWith(Downloader.TMP_DIR_SUFFIX) == true
|
}
|
||||||
|
} == null
|
||||||
|
) || it.name?.endsWith(Downloader.TMP_DIR_SUFFIX) == true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// SY <--
|
// SY <--
|
||||||
@@ -140,7 +141,7 @@ class DownloadProvider(private val context: Context) {
|
|||||||
* @param source the source to query.
|
* @param source the source to query.
|
||||||
*/
|
*/
|
||||||
fun getSourceDirName(source: Source): String {
|
fun getSourceDirName(source: Source): String {
|
||||||
return DiskUtil.buildValidFilename(source.toString())
|
return source.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -174,13 +175,8 @@ class DownloadProvider(private val context: Context) {
|
|||||||
* @param chapter the chapter to query.
|
* @param chapter the chapter to query.
|
||||||
*/
|
*/
|
||||||
fun getValidChapterDirNames(chapter: Chapter): List<String> {
|
fun getValidChapterDirNames(chapter: Chapter): List<String> {
|
||||||
val chapterName = getChapterDirName(chapter)
|
|
||||||
return listOf(
|
return listOf(
|
||||||
// Folder of images
|
getChapterDirName(chapter),
|
||||||
chapterName,
|
|
||||||
|
|
||||||
// Archived chapters
|
|
||||||
"$chapterName.cbz",
|
|
||||||
|
|
||||||
// Legacy chapter directory name used in v0.9.2 and before
|
// Legacy chapter directory name used in v0.9.2 and before
|
||||||
DiskUtil.buildValidFilename(chapter.name)
|
DiskUtil.buildValidFilename(chapter.name)
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user