Compare commits

..

1 Commits

Author SHA1 Message Date
Jobobby04 0413d502c1 Revert Jdk 11 update 2021-02-12 20:05:46 -05:00
1102 changed files with 50601 additions and 72017 deletions
+1
View File
@@ -1 +1,2 @@
github: inorichi
ko_fi: inorichi ko_fi: inorichi
+2 -10
View File
@@ -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.
+36
View File
@@ -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.
+5 -8
View File
@@ -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.
+24
View File
@@ -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)
-106
View File
@@ -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
+8
View File
@@ -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
-12
View File
@@ -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 |
| ------- | ------- |
| ![](https://github.githubassets.com/images/modules/logos_page/Octocat.png) | ![](https://github.githubassets.com/images/modules/logos_page/Octocat.png) |
-->
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
+8 -5
View File
@@ -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:
+28 -23
View File
@@ -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."
-14
View File
@@ -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 }}
-19
View File
@@ -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'
-126
View File
@@ -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
View File
@@ -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:
+8 -12
View File
@@ -1,20 +1,20 @@
| Preview Builds | Release Builds | Tachiyomi Support Server | | Preview Builds | Release Builds | Tachiyomi Support Server |
|-------|----------|----------| |-------|----------|----------|
| [![Preview](https://github.com/jobobby04/TachiyomiSYPreview/workflows/Remote%20Dispatch%20Build%20App/badge.svg)](https://github.com/jobobby04/TachiyomiSYPreview/releases) | [![stable release](https://img.shields.io/github/release/jobobby04/tachiyomisy.svg?maxAge=3600&label=download)](https://github.com/jobobby04/tachiyomisy/releases/latest) | [![Discord](https://img.shields.io/discord/349436576037732353.svg?label=discord&labelColor=7289da&color=2c2f33&style=flat)](https://discord.gg/tachiyomi) | | [![Preview](https://github.com/jobobby04/TachiyomiSYPreview/workflows/Remote%20Dispatch%20Build%20App/badge.svg)](https://github.com/jobobby04/TachiyomiSYPreview/releases) | [![stable release](https://img.shields.io/github/release/jobobby04/tachiyomisy.svg?maxAge=3600&label=download)](https://github.com/jobobby04/tachiyomisy/releases/latest) | [![Discord](https://img.shields.io/discord/349436576037732353.svg)](https://discord.gg/tachiyomi) |
# ![app icon](./.github/readme-images/app-icon.png)TachiyomiSY # ![app icon](./.github/readme-images/app-icon.png)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.
![screenshots of app](./.github/readme-images/screens.png) ![screenshots of app](./.github/readme-images/screens.png)
## 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
View File
@@ -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",
) )
} }
-34
View File
@@ -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>(...);
}
+67 -16
View File
@@ -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
View File
@@ -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
View File
@@ -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>
+113 -163
View File
@@ -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 <--
} }
} }
@@ -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)
@@ -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
)
@@ -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
}
}
}
}
@@ -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>()
@@ -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
}
}
}
}
@@ -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>()
@@ -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)
}
}
}
}
@@ -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
)
}
}
@@ -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
}
}
}
}
@@ -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>()
@@ -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
)
}
}
}
}
@@ -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
)
}
}
@@ -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
}
}
}
}
@@ -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))
} }
} }
@@ -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}
"""
@@ -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
) )
@@ -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
@@ -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)
} }
@@ -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
@@ -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
)
}
@@ -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
) )
} }
@@ -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 = "▒ ▒∩▒"
} }
} }
@@ -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
) )
} }
@@ -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
)
} }
@@ -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