Compare commits

..

1 Commits

Author SHA1 Message Date
Jobobby04 cbf82a9d6a Hide dedupe by priority 2021-04-11 21:53:50 -04:00
1232 changed files with 53060 additions and 74119 deletions
-5
View File
@@ -1,5 +0,0 @@
[*.{kt,kts}]
indent_size=4
insert_final_newline=true
ij_kotlin_allow_trailing_comma=true
ij_kotlin_allow_trailing_comma_on_call_site=true
+1
View File
@@ -1 +1,2 @@
github: inorichi
ko_fi: inorichi ko_fi: inorichi
+2 -8
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.6.0)
- To the latest version of the app (stable is v1.8.2) - 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**
+38
View File
@@ -0,0 +1,38 @@
---
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.6.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.
If you're experiencing crashes, share the crash logs from More → Settings → Advanced → Dump crash logs.
+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.6.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.2"
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.2](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.2](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

@@ -3,3 +3,4 @@ org.gradle.jvmargs=-Xmx5120m
org.gradle.workers.max=2 org.gradle.workers.max=2
kotlin.incremental=false kotlin.incremental=false
kotlin.compiler.execution.strategy=in-process
@@ -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:
@@ -1,21 +1,28 @@
name: Issue moderator name: Issue closer
on: on:
issues: issues:
types: [opened, edited, reopened] types: [opened, edited, reopened]
issue_comment:
types: [created]
jobs: jobs:
moderate: autoclose:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Moderate issues - name: Autoclose issues
uses: tachiyomiorg/issue-moderator-action@v1 uses: arkon/issue-closer-action@v3.0
with: with:
repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token: ${{ secrets.GITHUB_TOKEN }}
auto-close-rules: | rules: |
[ [
{
"type": "title",
"regex": ".*THIS ISSUE IS IN THE WRONG REPO.*",
"message": "It was not opened in the correct repo, as the template mentioned."
},
{
"type": "title",
"regex": ".*<Write short description here>*",
"message": "The description in the title was not filled out."
},
{ {
"type": "body", "type": "body",
"regex": ".*DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT.*", "regex": ".*DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT.*",
@@ -25,11 +32,5 @@ jobs:
"type": "body", "type": "body",
"regex": ".*\\* (Tachiyomi version|Android version|Device): \\?.*", "regex": ".*\\* (Tachiyomi version|Android version|Device): \\?.*",
"message": "Requested information in the template was not filled out." "message": "Requested information in the template was not filled out."
},
{
"type": "both",
"regex": "^(?!.*myanimelist.*).*(aniyomi|anime).*$",
"ignoreCase": true,
"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"
} }
] ]
+4 -4
View File
@@ -3,7 +3,7 @@ name: Lock threads
on: on:
# Daily # Daily
schedule: schedule:
- cron: '0 0 * * *' - cron: '0 * * * *'
# Manual trigger # Manual trigger
workflow_dispatch: workflow_dispatch:
inputs: inputs:
@@ -12,8 +12,8 @@ jobs:
lock: lock:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: dessant/lock-threads@v3 - uses: dessant/lock-threads@v2
with: with:
github-token: ${{ github.token }} github-token: ${{ github.token }}
issue-inactive-days: '2' issue-lock-inactive-days: '2'
pr-inactive-days: '2' pr-lock-inactive-days: '2'
+49 -99
View File
@@ -1,126 +1,76 @@
# Contributor Covenant Code of Conduct # Code of Conduct
## Our Pledge ## Our Pledge
We as members, contributors, and leaders pledge to make participation in our In the interest of fostering an open and welcoming environment, we as
community a harassment-free experience for everyone, regardless of age, body contributors and maintainers pledge to making participation in our project and
size, visible or invisible disability, ethnicity, sex characteristics, gender our community a harassment-free experience for everyone, regardless of age, body
identity and expression, level of experience, education, socio-economic status, size, disability, ethnicity, sex characteristics, gender identity and expression,
nationality, personal appearance, race, caste, color, religion, or sexual identity level of experience, education, socio-economic status, nationality, personal
and orientation. appearance, race, 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 ## Our Standards
Examples of behavior that contributes to a positive environment for our Examples of behavior that contributes to creating a positive environment
community include: include:
* Demonstrating empathy and kindness toward other people * Using welcoming and inclusive language
* Being respectful of differing opinions, viewpoints, and experiences * Being respectful of differing viewpoints and experiences
* Giving and gracefully accepting constructive feedback * Gracefully accepting constructive criticism
* Accepting responsibility and apologizing to those affected by our mistakes, * Focusing on what is best for the community
and learning from the experience * Showing empathy towards other community members
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include: Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery, and sexual attention or * The use of sexualized language or imagery and unwelcome sexual attention or
advances of any kind advances
* Trolling, insulting or derogatory comments, and personal or political attacks * Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment * Public or private harassment
* Publishing others' private information, such as a physical or email * Publishing others' private information, such as a physical or electronic
address, without their explicit permission address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a * Other conduct which could reasonably be considered inappropriate in a
professional setting professional setting
## Enforcement Responsibilities ## Our Responsibilities
Community moderators are responsible for clarifying and enforcing our standards of Project maintainers are responsible for clarifying the standards of acceptable
acceptable behavior and will take appropriate and fair corrective action in behavior and are expected to take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive, response to any instances of unacceptable behavior.
or harmful.
Community moderators have the right and responsibility to remove, edit, or reject Project maintainers have the right and responsibility to remove, edit, or
comments, commits, code, wiki edits, issues, and other contributions that are reject comments, commits, code, wiki edits, issues, and other contributions
not aligned to this Code of Conduct, and will communicate reasons for moderation that are not aligned to this Code of Conduct, or to ban temporarily or
decisions when appropriate. permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope ## Scope
This Code of Conduct applies within all community spaces, and also applies when This Code of Conduct applies both within project spaces and in public spaces
an individual is officially representing the community in public spaces. when an individual is representing the project or its community. Examples of
Examples of representing our community include using an official e-mail address, representing a project or community include using an official project e-mail
posting via an official social media account, or acting as an appointed address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement ## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community moderators responsible for enforcement at reported by contacting the project team at the Tachiyomi [Discord server](https://discord.gg/tachiyomi). All
the [Tachiyomi Discord server](https://discord.gg/tachiyomi). complaints will be reviewed and investigated and will result in a response that
All complaints will be reviewed and investigated promptly and fairly. is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
All community moderators are obligated to respect the privacy and security of the Project maintainers who do not follow or enforce the Code of Conduct in good
reporter of any incident. faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## 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 ## Attribution
This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org/), This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
version 2.1, available at available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[v2.1](https://www.contributor-covenant.org/version/2/1/code_of_conduct.html).
Community Impact Guidelines were inspired by [homepage]: https://www.contributor-covenant.org
[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 For answers to common questions about this code of conduct, see
[FAQ](https://www.contributor-covenant.org/faq). Translations are available https://www.contributor-covenant.org/faq
at [translations](https://www.contributor-covenant.org/translations).
+1 -17
View File
@@ -10,23 +10,7 @@ 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.
## Prerequisites
Before you start, please note that the ability to use following technologies is **required** and that existing contributors will not actively teach them to you.
- Basic [Android development](https://developer.android.com/)
- [Kotlin](https://kotlinlang.org/)
### Tools
- [Android Studio](https://developer.android.com/studio)
- Emulator or phone with developer options enabled to test changes.
## Getting help
- Join [the Discord server](https://discord.gg/tachiyomi) for online help and to ask questions while developing.
# Translations # Translations
@@ -42,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/AppUpdateChecker.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:
+6 -5
View File
@@ -4,17 +4,17 @@
# ![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
+211 -129
View File
@@ -1,16 +1,23 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import java.io.ByteArrayOutputStream
import java.text.SimpleDateFormat
import java.util.Date
import java.util.TimeZone
plugins { 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("Standard")) { if (!gradle.startParameter.taskRequests.toString().contains("Debug")) {
apply<com.google.gms.googleservices.GoogleServicesPlugin>() apply(plugin = "com.google.gms.google-services")
// Firebase Crashlytics // Firebase Crashlytics
apply(plugin = "com.google.firebase.crashlytics") apply(plugin = "com.google.firebase.crashlytics")
} }
@@ -18,25 +25,32 @@ if (gradle.startParameter.taskRequests.toString().contains("Standard")) {
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 = 33 testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
versionName = "1.8.2" versionCode = 14
versionName = "1.6.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 {
@@ -48,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") {
@@ -68,41 +84,31 @@ 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", exclude("META-INF/*.kotlin_module")
"META-INF/NOTICE",
"META-INF/*.kotlin_module", // Compatibility for two RxJava versions (EXH)
"META-INF/*.version", exclude("META-INF/rxjava.properties")
))
} }
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 {
@@ -116,144 +122,190 @@ android {
} }
dependencies { dependencies {
implementation(kotlinx.reflect)
implementation(kotlinx.bundles.coroutines)
// Source models and interfaces from Tachiyomi 1.x // Source models and interfaces from Tachiyomi 1.x
implementation(libs.tachiyomi.api) implementation("tachiyomi.sourceapi:source-api:1.1")
// AndroidX libraries // AndroidX libraries
implementation(androidx.annotation) implementation("androidx.annotation:annotation:1.3.0-alpha01")
implementation(androidx.appcompat) implementation("androidx.appcompat:appcompat:1.3.0-rc01")
implementation(androidx.biometricktx) implementation("androidx.biometric:biometric-ktx:1.2.0-alpha03")
implementation(androidx.constraintlayout) implementation("androidx.browser:browser:1.3.0")
implementation(androidx.coordinatorlayout) implementation("androidx.cardview:cardview:1.0.0")
implementation(androidx.corektx) implementation("androidx.constraintlayout:constraintlayout:2.1.0-beta01")
implementation(androidx.splashscreen) implementation("androidx.coordinatorlayout:coordinatorlayout:1.1.0")
implementation(androidx.recyclerview) implementation("androidx.core:core-ktx:1.3.2")
implementation(androidx.swiperefreshlayout) implementation("androidx.multidex:multidex:2.0.1")
implementation(androidx.viewpager) implementation("androidx.preference:preference-ktx:1.1.1")
implementation("androidx.recyclerview:recyclerview:1.2.0")
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01")
implementation(androidx.bundles.lifecycle) val lifecycleVersion = "2.3.0"
implementation("androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion")
implementation("androidx.lifecycle:lifecycle-process:$lifecycleVersion")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion")
// Job scheduling // Job scheduling
implementation(androidx.bundles.workmanager) implementation("androidx.work:work-runtime-ktx:2.5.0")
// RX // UI library
implementation(libs.bundles.reactivex) implementation("com.google.android.material:material:1.3.0")
implementation(libs.flowreactivenetwork)
"standardImplementation"("com.google.firebase:firebase-core:18.0.3")
// ReactiveX
implementation("io.reactivex:rxandroid:1.2.1")
implementation("io.reactivex:rxjava:1.3.8")
implementation("com.jakewharton.rxrelay:rxrelay:1.2.0")
implementation("com.github.pwittchen:reactivenetwork:0.13.0")
// Network client // Network client
implementation(libs.bundles.okhttp) val okhttpVersion = "5.0.0-alpha.2"
implementation(libs.okio) implementation("com.squareup.okhttp3:okhttp:$okhttpVersion")
implementation("com.squareup.okhttp3:logging-interceptor:$okhttpVersion")
implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:$okhttpVersion")
implementation("com.squareup.okio:okio:2.10.0")
// TLS 1.3 support for Android < 10 // TLS 1.3 support for Android < 10
implementation(libs.conscrypt.android) implementation("org.conscrypt:conscrypt-android:2.5.1")
// Data serialization (JSON, protobuf) // JSON
implementation(kotlinx.bundles.serialization) val kotlinSerializationVersion = "1.0.1"
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinSerializationVersion")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-protobuf:$kotlinSerializationVersion")
implementation("com.google.code.gson:gson:2.8.6")
implementation("com.github.salomonbrys.kotson:kotson:2.5.0")
// JavaScript engine // JavaScript engine
implementation(libs.bundles.js.engine) implementation("com.squareup.duktape:duktape-android:1.3.0")
// HTML parser
implementation(libs.jsoup)
// Disk // Disk
implementation(libs.disklrucache) implementation("com.jakewharton:disklrucache:2.0.2")
implementation(libs.unifile) implementation("com.github.inorichi:unifile:e9ee588")
implementation(libs.junrar) implementation("com.github.junrar:junrar:7.4.0")
// HTML parser
implementation("org.jsoup:jsoup:1.13.1")
// Database // Database
implementation(libs.bundles.sqlite) 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("io.requery:sqlite-android:3.33.0")
// Preferences // Preferences
implementation(libs.preferencektx) implementation("com.github.tfcporciuncula.flow-preferences:flow-preferences:1.3.4")
implementation(libs.flowpreferences)
// Model View Presenter // Model View Presenter
implementation(libs.bundles.nucleus) val nucleusVersion = "3.0.0"
implementation("info.android15.nucleus:nucleus:$nucleusVersion")
implementation("info.android15.nucleus:nucleus-support-v7:$nucleusVersion")
// Dependency injection // Dependency injection
implementation(libs.injekt.core) implementation("com.github.inorichi.injekt:injekt-core:65b0440")
// Image loading // Image library
implementation(libs.bundles.coil) val glideVersion = "4.12.0"
implementation("com.github.bumptech.glide:glide:$glideVersion")
implementation("com.github.bumptech.glide:okhttp3-integration:$glideVersion")
kapt("com.github.bumptech.glide:compiler:$glideVersion")
implementation(libs.subsamplingscaleimageview) { implementation("com.github.tachiyomiorg:subsampling-scale-image-view:547d9c0")
exclude(module = "image-decoder")
}
implementation(libs.image.decoder)
// Sort
implementation(libs.natural.comparator)
// UI libraries
implementation(libs.material)
implementation(libs.androidprocessbutton)
implementation(libs.flexible.adapter.core)
implementation(libs.flexible.adapter.ui)
implementation(libs.viewstatepageradapter)
implementation(libs.photoview)
implementation(libs.directionalviewpager) {
exclude(group = "androidx.viewpager", module = "viewpager")
}
implementation(libs.insetter)
// Conductor
implementation(libs.bundles.conductor)
// FlowBinding
implementation(libs.bundles.flowbinding)
// Logging // Logging
implementation(libs.logcat) implementation("com.jakewharton.timber:timber:4.7.1")
// Crash reports/analytics // Crash reports
// implementation(libs.acra.http) //implementation("ch.acra:acra-http:5.7.0")
// "standardImplementation"(libs.firebase.analytics)
// Sort
implementation("com.github.gpanther:java-nat-sort:natural-comparator-1.1")
// UI
implementation("com.dmitrymalkovich.android:material-design-dimens:1.4")
implementation("com.github.dmytrodanylyk.android-process-button:library:1.0.4")
implementation("eu.davidea:flexible-adapter:5.1.0")
implementation("eu.davidea:flexible-adapter-ui:1.0.0")
implementation("com.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.1.0")
implementation("com.github.chrisbanes:PhotoView:2.3.0")
implementation("com.github.tachiyomiorg:DirectionalViewPager:1.0.0")
implementation("dev.chrisbanes.insetter:insetter:0.5.0")
// 3.2.0+ introduces weird UI blinking or cut off issues on some devices
val materialDialogsVersion = "3.1.1"
implementation("com.afollestad.material-dialogs:core:$materialDialogsVersion")
implementation("com.afollestad.material-dialogs:input:$materialDialogsVersion")
implementation("com.afollestad.material-dialogs:datetime:$materialDialogsVersion")
// Conductor
implementation("com.bluelinelabs:conductor:2.1.5")
implementation("com.bluelinelabs:conductor-support:2.1.5") {
exclude(group = "com.android.support")
}
implementation("com.github.tachiyomiorg:conductor-support-preference:2.0.1")
// FlowBinding
val flowbindingVersion = "0.12.0"
implementation("io.github.reactivecircus.flowbinding:flowbinding-android:$flowbindingVersion")
implementation("io.github.reactivecircus.flowbinding:flowbinding-appcompat:$flowbindingVersion")
implementation("io.github.reactivecircus.flowbinding:flowbinding-recyclerview:$flowbindingVersion")
implementation("io.github.reactivecircus.flowbinding:flowbinding-swiperefreshlayout:$flowbindingVersion")
implementation("io.github.reactivecircus.flowbinding:flowbinding-viewpager:$flowbindingVersion")
// Licenses // Licenses
implementation(libs.aboutlibraries.core) implementation("com.mikepenz:aboutlibraries:${BuildPluginsVersion.ABOUTLIB_PLUGIN}")
// Shizuku
implementation(libs.bundles.shizuku)
// Tests // Tests
testImplementation(libs.junit) testImplementation("junit:junit:4.13.2")
testImplementation(libs.assertj.core) testImplementation("org.assertj:assertj-core:3.16.1")
testImplementation(libs.mockito.core) testImplementation("org.mockito:mockito-core:1.10.19")
testImplementation(libs.bundles.robolectric) val robolectricVersion = "3.1.4"
testImplementation("org.robolectric:robolectric:$robolectricVersion")
testImplementation("org.robolectric:shadows-multidex:$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(libs.leakcanary.android) // 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(sylibs.changelog) implementation("com.github.gabrielemariotti.changeloglib:changelog:2.1.0")
// Text distance (EH) // Text distance (EH)
implementation (sylibs.simularity) implementation ("info.debatty:java-string-similarity:2.0.0")
// Firebase (EH) // Firebase (EH)
implementation(sylibs.firebase.analytics) implementation("com.google.firebase:firebase-analytics-ktx:18.0.3")
implementation(sylibs.firebase.crashlytics.ktx) implementation("com.google.firebase:firebase-crashlytics-ktx:17.4.1")
// Better logging (EH) // Better logging (EH)
implementation(sylibs.xlog) implementation("com.elvishew:xlog:1.9.0")
// Debug utils (EH) // Debug utils (EH)
debugImplementation(sylibs.debugOverlay.standard) val debugOverlayVersion = "1.1.3"
"releaseTestImplementation"(sylibs.debugOverlay.noop) debugImplementation("com.ms-square:debugoverlay:$debugOverlayVersion")
releaseImplementation(sylibs.debugOverlay.noop) "releaseTestImplementation"("com.ms-square:debugoverlay-no-op:$debugOverlayVersion")
testImplementation(sylibs.debugOverlay.noop) releaseImplementation("com.ms-square:debugoverlay-no-op:$debugOverlayVersion")
testImplementation("com.ms-square:debugoverlay-no-op:$debugOverlayVersion")
// RatingBar (SY) // RatingBar (SY)
implementation(sylibs.ratingbar) implementation ("me.zhanghai.android.materialratingbar:library:1.4.0")
// JsonReader for similar manga
implementation("com.squareup.moshi:moshi:1.12.0")
implementation("com.mikepenz:fastadapter:5.4.0")
// SY <--
} }
tasks { tasks {
@@ -262,13 +314,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",
) )
} }
@@ -284,8 +334,40 @@ tasks {
} }
} }
buildscript { buildscript {
repositories {
mavenCentral()
}
dependencies { dependencies {
classpath(kotlinx.gradle) classpath(kotlin("gradle-plugin", version = BuildPluginsVersion.KOTLIN))
} }
} }
// Git is needed in your system PATH for these commands to work.
// If it's not installed, you can return a random value as a workaround
fun getCommitCount(): String {
return runCommand("git rev-list --count HEAD")
// return "1"
}
fun getGitSha(): String {
return runCommand("git rev-parse --short HEAD")
// return "1"
}
fun getBuildTime(): String {
val df = SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'")
df.timeZone = TimeZone.getTimeZone("UTC")
return df.format(Date())
}
fun runCommand(command: String): String {
val byteOut = ByteArrayOutputStream()
project.exec {
commandLine = command.split(" ")
standardOutput = byteOut
}
return String(byteOut.toByteArray()).trim()
}
-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" />
@@ -2,5 +2,4 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/> <background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/> <foreground android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_tachi_monochrome_launcher" />
</adaptive-icon> </adaptive-icon>
+195 -187
View File
@@ -19,28 +19,26 @@
<!-- 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:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
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" />
@@ -54,8 +52,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" />
@@ -76,11 +73,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>
@@ -88,26 +83,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" />
@@ -121,8 +105,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" />
@@ -136,8 +119,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" />
@@ -151,8 +133,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" />
@@ -165,10 +146,23 @@
</intent-filter> </intent-filter>
</activity> </activity>
<activity
android:name=".extension.util.ExtensionInstallActivity"
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
<activity <activity
android:name="exh.ui.login.EhLoginActivity" android:name="exh.ui.login.EhLoginActivity"
android:label="EHentaiLogin" android:label="EHentaiLogin" />
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"
@@ -183,190 +177,204 @@
android:exported="false" /> android:exported="false" />
<service <service
android:name=".data.updater.AppUpdateService" android:name=".data.updater.UpdaterService"
android:exported="false" />
<service
android:name=".data.backup.BackupCreateService"
android:exported="false" /> android:exported="false" />
<service <service
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>
+71 -201
View File
@@ -1,31 +1,16 @@
package eu.kanade.tachiyomi package eu.kanade.tachiyomi
import android.annotation.SuppressLint
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.os.Looper import androidx.lifecycle.Lifecycle
import android.webkit.WebView import androidx.lifecycle.LifecycleObserver
import androidx.appcompat.app.AppCompatDelegate import androidx.lifecycle.OnLifecycleEvent
import androidx.core.app.NotificationManagerCompat
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.disk.DiskCache
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
@@ -34,145 +19,99 @@ 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.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.MangaCoverFetcher
import eu.kanade.tachiyomi.data.coil.MangaCoverKeyer
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.base.delegate.SecureActivityDelegate import eu.kanade.tachiyomi.util.system.LocaleHelper
import eu.kanade.tachiyomi.util.preference.asImmediateFlow
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil
import eu.kanade.tachiyomi.util.system.WebViewUtil
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.XLogTree
import exh.log.xLogD import exh.log.xLogD
import exh.log.xLogE 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 logcat.LogPriority
import logcat.LogcatLogger
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.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()
@SuppressLint("LaunchActivityFromNotification")
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 Timber.plant(XLogTree()) // SY Redirect Timber 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)
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)
val callFactoryInit = { Injekt.get<NetworkHelper>().client } MultiDex.install(this)
val diskCacheInit = { CoilDiskCache.get(this@App) } }
components {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { override fun onConfigurationChanged(newConfig: Configuration) {
add(ImageDecoderDecoder.Factory()) super.onConfigurationChanged(newConfig)
} else { LocaleHelper.updateConfiguration(this, newConfig, true)
add(GifDecoder.Factory()) }
}
add(TachiyomiImageDecoder.Factory()) private fun workaroundAndroid7BrokenSSL() {
add(MangaCoverFetcher.Factory(lazy(callFactoryInit), lazy(diskCacheInit))) if (Build.VERSION.SDK_INT == Build.VERSION_CODES.N ||
add(MangaCoverKeyer()) Build.VERSION.SDK_INT == Build.VERSION_CODES.N_MR1
) {
try {
SSLContext.getInstance("TLSv1.2")
} catch (e: NoSuchAlgorithmException) {
xLogE("Could not install Android 7 broken SSL workaround!", e)
} }
callFactory(callFactoryInit)
diskCache(diskCacheInit) try {
crossfade((300 * this@App.animatorDurationScale).toInt()) ProviderInstaller.installIfNeeded(applicationContext)
allowRgb565(getSystemService<ActivityManager>()!!.isLowRamDevice) } catch (e: GooglePlayServicesRepairableException) {
if (preferences.verboseLogging()) logger(DebugLogger()) xLogE("Could not install Android 7 broken SSL workaround!", e)
}.build() } catch (e: GooglePlayServicesNotAvailableException) {
xLogE("Could not install Android 7 broken SSL workaround!", e)
}
}
} }
private fun addAnalytics() { private fun addAnalytics() {
@@ -181,39 +120,16 @@ open class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
} }
} }
override fun onStop(owner: LifecycleOwner) { @OnLifecycleEvent(Lifecycle.Event.ON_STOP)
if (!AuthenticatorUtil.isAuthenticating && preferences.lockAppAfter().get() >= 0) { @Suppress("unused")
fun onAppBackgrounded() {
if (preferences.lockAppAfter().get() >= 0) {
SecureActivityDelegate.locked = true SecureActivityDelegate.locked = true
} }
} }
override fun getPackageName(): String {
// This causes freezes in Android 6/7 for some reason
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
try {
// Override the value passed as X-Requested-With in WebView requests
val stackTrace = Looper.getMainLooper().thread.stackTrace
val chromiumElement = stackTrace.find {
it.className.equals(
"org.chromium.base.BuildInfo",
ignoreCase = true,
)
}
if (chromiumElement?.methodName.equals("getAll", ignoreCase = true)) {
return WebViewUtil.SPOOF_PACKAGE_NAME
}
} catch (e: Exception) {
}
}
return super.getPackageName()
}
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 // EXH
@@ -237,25 +153,26 @@ open class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
val logFolder = File( val logFolder = File(
Environment.getExternalStorageDirectory().absolutePath + File.separator + Environment.getExternalStorageDirectory().absolutePath + File.separator +
getString(R.string.app_name), getString(R.string.app_name),
"logs", "logs"
) )
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,
timestamp, timestamp
) + "-${BuildConfig.BUILD_TYPE}.log" ) + "-${BuildConfig.BUILD_TYPE}.log"
} }
} }
flattener { timeMillis, level, tag, message -> flattener { timeMillis, level, tag, message ->
"${dateFormat.format(timeMillis)} ${LogLevel.getShortLevelName(level)}/$tag: $message" "${dateFormat.format(timeMillis)} ${LogLevel.getShortLevelName(level)}/$tag: $message"
} }
cleanStrategy = FileLastModifiedCleanStrategy(7.days.inWholeMilliseconds) cleanStrategy = FileLastModifiedCleanStrategy(7.days.toLongMilliseconds())
backupStrategy = NeverBackupStrategy() backupStrategy = NeverBackupStrategy()
} }
@@ -266,22 +183,20 @@ open class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
XLog.init( XLog.init(
logConfig, logConfig,
*printers.toTypedArray(), *printers.toTypedArray()
) )
xLogD("Application booting...") xLogD("Application booting...")
xLogD( xLogD(
""" "App version: ${BuildConfig.VERSION_NAME} (${BuildConfig.FLAVOR}, ${BuildConfig.COMMIT_SHA}, ${BuildConfig.VERSION_CODE})\n" +
App version: ${BuildConfig.VERSION_NAME} (${BuildConfig.FLAVOR}, ${BuildConfig.COMMIT_SHA}, ${BuildConfig.VERSION_CODE}) "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(),
) )
} }
@@ -300,49 +215,4 @@ open class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
xLogE("Failed to initialize debug overlay, app in background?", e) xLogE("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"
/**
* Direct copy of Coil's internal SingletonDiskCache so that [MangaCoverFetcher] can access it.
*/
internal object CoilDiskCache {
private const val FOLDER_NAME = "image_cache"
private var instance: DiskCache? = null
@Synchronized
fun get(context: Context): DiskCache {
return instance ?: run {
val safeCacheDir = context.cacheDir.apply { mkdirs() }
// Create the singleton disk cache instance.
DiskCache.Builder()
.directory(safeCacheDir.resolve(FOLDER_NAME))
.build()
.also { instance = it }
}
}
} }
@@ -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,16 +1,15 @@
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
import eu.kanade.tachiyomi.data.download.DownloadManager 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.saver.ImageSaver
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
@@ -27,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) }
@@ -47,9 +44,9 @@ class AppModule(val app: Application) : InjektModule {
addSingletonFactory { TrackManager(app) } addSingletonFactory { TrackManager(app) }
addSingletonFactory { DelayedTrackingStore(app) } addSingletonFactory { Gson() }
addSingletonFactory { ImageSaver(app) } addSingletonFactory { Json { ignoreUnknownKeys = true } }
// SY --> // SY -->
addSingletonFactory { CustomMangaManager(app) } addSingletonFactory { CustomMangaManager(app) }
@@ -58,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,25 +1,16 @@
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_NON_COMPLETED
import eu.kanade.tachiyomi.data.preference.PreferenceKeys import eu.kanade.tachiyomi.data.preference.PreferenceKeys
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.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.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.DeviceUtil
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
@@ -39,29 +30,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)
} }
@@ -94,7 +86,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)
@@ -104,17 +96,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
@@ -131,7 +120,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
@@ -143,6 +132,7 @@ object Migrations {
} }
if (oldVersion < 57) { if (oldVersion < 57) {
// Migrate DNS over HTTPS setting // Migrate DNS over HTTPS setting
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
val wasDohEnabled = prefs.getBoolean("enable_doh", false) val wasDohEnabled = prefs.getBoolean("enable_doh", false)
if (wasDohEnabled) { if (wasDohEnabled) {
prefs.edit { prefs.edit {
@@ -151,124 +141,6 @@ object Migrations {
} }
} }
} }
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_NON_COMPLETED
}
}
if (oldVersion < 75) {
val oldSecureScreen = prefs.getBoolean("secure_screen", false)
if (oldSecureScreen) {
preferences.secureScreen().set(PreferenceValues.SecureScreenMode.ALWAYS)
}
if (DeviceUtil.isMiui && preferences.extensionInstaller().get() == PreferenceValues.ExtensionInstaller.PACKAGEINSTALLER) {
preferences.extensionInstaller().set(PreferenceValues.ExtensionInstaller.LEGACY)
}
}
if (oldVersion < 76) {
BackupCreatorJob.setupTask(context)
}
if (oldVersion < 77) {
val oldReaderTap = prefs.getBoolean("reader_tap", false)
if (!oldReaderTap) {
preferences.navigationModePager().set(5)
preferences.navigationModeWebtoon().set(5)
}
}
return true return true
} }
@@ -0,0 +1,5 @@
package eu.kanade.tachiyomi.annotations
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.CLASS)
annotation class Nsfw
@@ -28,7 +28,7 @@ abstract class AbstractBackupManager(protected val context: Context) {
protected val customMangaManager: CustomMangaManager by injectLazy() protected val customMangaManager: CustomMangaManager by injectLazy()
// SY <-- // SY <--
abstract fun createBackup(uri: Uri, flags: Int, isAutoBackup: Boolean): String abstract fun createBackup(uri: Uri, flags: Int, isJob: Boolean): String?
/** /**
* Returns manga * Returns manga
@@ -122,7 +122,7 @@ abstract class AbstractBackupRestore<T : AbstractBackupManager>(protected val co
internal fun showRestoreProgress( internal fun showRestoreProgress(
progress: Int, progress: Int,
amount: Int, amount: Int,
title: String, title: String
) { ) {
notifier.showRestoreProgress(title, progress, amount) notifier.showRestoreProgress(title, progress, amount)
} }
@@ -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,25 +8,8 @@ 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
// Filter options
internal const val BACKUP_CATEGORY = 0x1
internal const val BACKUP_CATEGORY_MASK = 0x1
internal const val BACKUP_CHAPTER = 0x2
internal const val BACKUP_CHAPTER_MASK = 0x2
internal const val BACKUP_HISTORY = 0x4
internal const val BACKUP_HISTORY_MASK = 0x4
internal const val BACKUP_TRACK = 0x8
internal const val BACKUP_TRACK_MASK = 0x8
// 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 <--
} }
@@ -0,0 +1,127 @@
package eu.kanade.tachiyomi.data.backup
import android.app.Service
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.IBinder
import android.os.PowerManager
import androidx.core.content.ContextCompat
import androidx.core.net.toUri
import com.hippo.unifile.UniFile
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.util.system.acquireWakeLock
import eu.kanade.tachiyomi.util.system.isServiceRunning
/**
* Service for backing up library information to a JSON file.
*/
class BackupCreateService : Service() {
companion object {
// Filter options
internal const val BACKUP_CATEGORY = 0x1
internal const val BACKUP_CATEGORY_MASK = 0x1
internal const val BACKUP_CHAPTER = 0x2
internal const val BACKUP_CHAPTER_MASK = 0x2
internal const val BACKUP_HISTORY = 0x4
internal const val BACKUP_HISTORY_MASK = 0x4
internal const val BACKUP_TRACK = 0x8
internal const val BACKUP_TRACK_MASK = 0x8
// SY -->
internal const val BACKUP_CUSTOM_INFO = 0x10
internal const val BACKUP_CUSTOM_INFO_MASK = 0x10
internal const val BACKUP_ALL = 0x1F
// SY <--
/**
* Returns the status of the service.
*
* @param context the application context.
* @return true if the service is running, false otherwise.
*/
fun isRunning(context: Context): Boolean =
context.isServiceRunning(BackupCreateService::class.java)
/**
* Make a backup from library
*
* @param context context of application
* @param uri path of Uri
* @param flags determines what to backup
*/
fun start(context: Context, uri: Uri, flags: Int, type: Int) {
if (!isRunning(context)) {
val intent = Intent(context, BackupCreateService::class.java).apply {
putExtra(BackupConst.EXTRA_URI, uri)
putExtra(BackupConst.EXTRA_FLAGS, flags)
putExtra(BackupConst.EXTRA_TYPE, type)
}
ContextCompat.startForegroundService(context, intent)
}
}
}
/**
* Wake lock that will be held until the service is destroyed.
*/
private lateinit var wakeLock: PowerManager.WakeLock
private lateinit var notifier: BackupNotifier
override fun onCreate() {
super.onCreate()
notifier = BackupNotifier(this)
wakeLock = acquireWakeLock(javaClass.name)
startForeground(Notifications.ID_BACKUP_PROGRESS, notifier.showBackupProgress().build())
}
override fun stopService(name: Intent?): Boolean {
destroyJob()
return super.stopService(name)
}
override fun onDestroy() {
destroyJob()
super.onDestroy()
}
private fun destroyJob() {
if (wakeLock.isHeld) {
wakeLock.release()
}
}
/**
* This method needs to be implemented, but it's not used/needed.
*/
override fun onBind(intent: Intent): IBinder? = null
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (intent == null) return START_NOT_STICKY
try {
val uri = intent.getParcelableExtra<Uri>(BackupConst.EXTRA_URI)
val backupFlags = intent.getIntExtra(BackupConst.EXTRA_FLAGS, 0)
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)
notifier.showBackupComplete(unifile, backupType == BackupConst.BACKUP_TYPE_LEGACY)
} catch (e: Exception) {
notifier.showBackupError(e.message)
}
stopSelf(startId)
return START_NOT_STICKY
}
}
@@ -1,24 +1,15 @@
package eu.kanade.tachiyomi.data.backup package eu.kanade.tachiyomi.data.backup
import android.content.Context import android.content.Context
import android.net.Uri
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.work.ExistingPeriodicWorkPolicy import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.PeriodicWorkRequestBuilder import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkInfo
import androidx.work.WorkManager import androidx.work.WorkManager
import androidx.work.Worker import androidx.work.Worker
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import androidx.work.workDataOf
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.notification.Notifications 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 eu.kanade.tachiyomi.util.system.notificationManager
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
@@ -28,71 +19,39 @@ class BackupCreatorJob(private val context: Context, workerParams: WorkerParamet
override fun doWork(): Result { override fun doWork(): Result {
val preferences = Injekt.get<PreferencesHelper>() val preferences = Injekt.get<PreferencesHelper>()
val notifier = BackupNotifier(context) val uri = preferences.backupsDirectory().get().toUri()
val uri = inputData.getString(LOCATION_URI_KEY)?.let { Uri.parse(it) } val flags = BackupCreateService.BACKUP_ALL
?: preferences.backupsDirectory().get().toUri()
val flags = inputData.getInt(BACKUP_FLAGS_KEY, BackupConst.BACKUP_ALL)
val isAutoBackup = inputData.getBoolean(IS_AUTO_BACKUP_KEY, true)
context.notificationManager.notify(Notifications.ID_BACKUP_PROGRESS, notifier.showBackupProgress().build())
return try { return try {
val location = FullBackupManager(context).createBackup(uri, flags, isAutoBackup) FullBackupManager(context).createBackup(uri, flags, true)
if (!isAutoBackup) notifier.showBackupComplete(UniFile.fromUri(context, location.toUri())) if (preferences.createLegacyBackup().get()) {
LegacyBackupManager(context).createBackup(uri, flags, true)
}
Result.success() Result.success()
} catch (e: Exception) { } catch (e: Exception) {
logcat(LogPriority.ERROR, e)
if (!isAutoBackup) notifier.showBackupError(e.message)
Result.failure() Result.failure()
} finally {
context.notificationManager.cancel(Notifications.ID_BACKUP_PROGRESS)
} }
} }
companion object { companion object {
fun isManualJobRunning(context: Context): Boolean { private const val TAG = "BackupCreator"
val list = WorkManager.getInstance(context).getWorkInfosByTag(TAG_MANUAL).get()
return list.find { it.state == WorkInfo.State.RUNNING } != null
}
fun setupTask(context: Context, prefInterval: Int? = null) { fun setupTask(context: Context, prefInterval: Int? = null) {
val preferences = Injekt.get<PreferencesHelper>() val preferences = Injekt.get<PreferencesHelper>()
val interval = prefInterval ?: preferences.backupInterval().get() val interval = prefInterval ?: preferences.backupInterval().get()
val workManager = WorkManager.getInstance(context)
if (interval > 0) { if (interval > 0) {
val request = PeriodicWorkRequestBuilder<BackupCreatorJob>( val request = PeriodicWorkRequestBuilder<BackupCreatorJob>(
interval.toLong(), interval.toLong(),
TimeUnit.HOURS, TimeUnit.HOURS,
10, 10,
TimeUnit.MINUTES, TimeUnit.MINUTES
) )
.addTag(TAG_AUTO) .addTag(TAG)
.setInputData(workDataOf(IS_AUTO_BACKUP_KEY to true))
.build() .build()
workManager.enqueueUniquePeriodicWork(TAG_AUTO, ExistingPeriodicWorkPolicy.REPLACE, request) WorkManager.getInstance(context).enqueueUniquePeriodicWork(TAG, ExistingPeriodicWorkPolicy.REPLACE, request)
} else { } else {
workManager.cancelUniqueWork(TAG_AUTO) WorkManager.getInstance(context).cancelAllWorkByTag(TAG)
} }
} }
fun startNow(context: Context, uri: Uri, flags: Int) {
val inputData = workDataOf(
IS_AUTO_BACKUP_KEY to false,
LOCATION_URI_KEY to uri.toString(),
BACKUP_FLAGS_KEY to flags,
)
val request = OneTimeWorkRequestBuilder<BackupCreatorJob>()
.addTag(TAG_MANUAL)
.setInputData(inputData)
.build()
WorkManager.getInstance(context).enqueueUniqueWork(TAG_MANUAL, ExistingWorkPolicy.KEEP, request)
}
} }
} }
private const val TAG_AUTO = "BackupCreator"
private const val TAG_MANUAL = "$TAG_AUTO:manual"
private const val IS_AUTO_BACKUP_KEY = "is_auto_backup" // Boolean
private const val LOCATION_URI_KEY = "location_uri" // String
private const val BACKUP_FLAGS_KEY = "backup_flags" // Int
@@ -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)
@@ -97,7 +97,7 @@ class BackupNotifier(private val context: Context) {
addAction( addAction(
R.drawable.ic_close_24dp, R.drawable.ic_close_24dp,
context.getString(R.string.action_stop), context.getString(R.string.action_stop),
NotificationReceiver.cancelRestorePendingBroadcast(context, Notifications.ID_RESTORE_PROGRESS), NotificationReceiver.cancelRestorePendingBroadcast(context, Notifications.ID_RESTORE_PROGRESS)
) )
} }
@@ -124,8 +124,8 @@ class BackupNotifier(private val context: Context) {
R.string.restore_duration, R.string.restore_duration,
TimeUnit.MILLISECONDS.toMinutes(time), TimeUnit.MILLISECONDS.toMinutes(time),
TimeUnit.MILLISECONDS.toSeconds(time) - TimeUnit.MINUTES.toSeconds( TimeUnit.MILLISECONDS.toSeconds(time) - TimeUnit.MINUTES.toSeconds(
TimeUnit.MILLISECONDS.toMinutes(time), TimeUnit.MILLISECONDS.toMinutes(time)
), )
) )
with(completeNotificationBuilder) { with(completeNotificationBuilder) {
@@ -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_show_errors),
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.
@@ -97,7 +96,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()
} }
@@ -129,7 +128,7 @@ class BackupRestoreService : Service() {
} }
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)
@@ -4,18 +4,16 @@ import android.content.Context
import android.net.Uri import android.net.Uri
import com.hippo.unifile.UniFile import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.data.backup.AbstractBackupManager import eu.kanade.tachiyomi.data.backup.AbstractBackupManager
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CATEGORY import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CATEGORY
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CATEGORY_MASK import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CATEGORY_MASK
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CHAPTER import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CHAPTER
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CHAPTER_MASK import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CHAPTER_MASK
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CUSTOM_INFO import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CUSTOM_INFO
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CUSTOM_INFO_MASK import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CUSTOM_INFO_MASK
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_HISTORY import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_HISTORY
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_HISTORY_MASK import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_HISTORY_MASK
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_READ_MANGA import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_TRACK
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_READ_MANGA_MASK import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_TRACK_MASK
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_TRACK
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_TRACK_MASK
import eu.kanade.tachiyomi.data.backup.full.models.Backup import eu.kanade.tachiyomi.data.backup.full.models.Backup
import eu.kanade.tachiyomi.data.backup.full.models.BackupCategory import eu.kanade.tachiyomi.data.backup.full.models.BackupCategory
import eu.kanade.tachiyomi.data.backup.full.models.BackupChapter import eu.kanade.tachiyomi.data.backup.full.models.BackupChapter
@@ -35,20 +33,20 @@ 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.source.online.MetadataSource import eu.kanade.tachiyomi.source.online.MetadataSource
import eu.kanade.tachiyomi.util.lang.launchIO import eu.kanade.tachiyomi.util.lang.launchIO
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.models.SavedSearch import exh.savedsearches.JsonSavedSearch
import exh.source.MERGED_SOURCE_ID import exh.source.MERGED_SOURCE_ID
import exh.source.getMainSource import exh.source.getMainSource
import exh.util.executeOnIO import exh.util.executeOnIO
import exh.util.nullIfBlank import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
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 java.io.FileOutputStream import timber.log.Timber
import kotlin.math.max import kotlin.math.max
class FullBackupManager(context: Context) : AbstractBackupManager(context) { class FullBackupManager(context: Context) : AbstractBackupManager(context) {
@@ -59,32 +57,26 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
* Create backup Json file from database * Create backup Json file from database
* *
* @param uri path of Uri * @param uri path of Uri
* @param isAutoBackup backup called from scheduled backup job * @param isJob backup called from job
*/ */
override fun createBackup(uri: Uri, flags: Int, isAutoBackup: 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 (isAutoBackup) { 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)
dir = dir.createDirectory("automatic") dir = dir.createDirectory("automatic")
@@ -106,24 +98,11 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
) )
?: throw Exception("Couldn't create backup file") ?: throw Exception("Couldn't create backup file")
if (!file.isFile) {
throw IllegalStateException("Failed to get handle on file")
}
val byteArray = parser.encodeToByteArray(BackupSerializer, backup!!) val byteArray = parser.encodeToByteArray(BackupSerializer, backup!!)
file.openOutputStream().also { file.openOutputStream().sink().gzip().buffer().use { it.write(byteArray) }
// Force overwrite old file return file.uri.toString()
(it as? FileOutputStream)?.channel?.truncate(0)
}.sink().gzip().buffer().use { it.write(byteArray) }
val fileUri = file.uri
// 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
} }
} }
@@ -162,12 +141,14 @@ 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 databaseHelper.getSavedSearches().executeAsBlocking().map { return preferences.savedSearches().get().map {
val sourceId = it.substringBefore(':').toLong()
val content = Json.decodeFromString<JsonSavedSearch>(it.substringAfter(':'))
BackupSavedSearch( BackupSavedSearch(
it.name, content.name,
it.query.orEmpty(), content.query,
it.filtersJson ?: "[]", content.filters.toString(),
it.source, sourceId
) )
} }
} }
@@ -193,8 +174,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)
@@ -427,25 +408,33 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
// SY --> // SY -->
internal fun restoreSavedSearches(backupSavedSearches: List<BackupSavedSearch>) { internal fun restoreSavedSearches(backupSavedSearches: List<BackupSavedSearch>) {
val currentSavedSearches = databaseHelper.getSavedSearches() val currentSavedSearches = preferences.savedSearches().get().map {
.executeAsBlocking() val sourceId = it.substringBefore(':').toLong()
val content = Json.decodeFromString<JsonSavedSearch>(it.substringAfter(':'))
val newSavedSearches = backupSavedSearches.filter { backupSavedSearch -> BackupSavedSearch(
currentSavedSearches.none { it.name == backupSavedSearch.name && it.source == backupSavedSearch.source } content.name,
}.map { content.query,
SavedSearch( content.filters.toString(),
id = null, sourceId
it.source,
it.name,
it.query.nullIfBlank(),
filtersJson = it.filterList.nullIfBlank()
?.takeUnless { it == "[]" },
) )
}.ifEmpty { null }
if (newSavedSearches != null) {
databaseHelper.insertSavedSearches(newSavedSearches)
} }
preferences.savedSearches()
.set(
(
backupSavedSearches.filter { backupSavedSearch -> currentSavedSearches.none { it.name == backupSavedSearch.name && it.source == backupSavedSearch.source } }
.map {
"${it.source}:" + Json.encodeToString(
JsonSavedSearch(
it.name,
it.query,
Json.decodeFromString(it.filterList)
)
)
} + preferences.savedSearches().get()
)
.toSet()
)
} }
/** /**
@@ -12,7 +12,6 @@ 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
@@ -49,8 +48,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 {
@@ -88,7 +86,7 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBa
val manga = backupManga.getMangaImpl() val manga = backupManga.getMangaImpl()
val chapters = backupManga.getChaptersImpl() val chapters = backupManga.getChaptersImpl()
val categories = backupManga.categories val categories = backupManga.categories
val history = backupManga.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
@@ -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()
) )
@@ -13,7 +13,7 @@ class BackupCategory(
// 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 flags: Int = 0, @ProtoNumber(100) var flags: Int = 0,
// SY specific values // SY specific values
@ProtoNumber(600) var mangaOrder: List<Long> = emptyList(), @ProtoNumber(600) var mangaOrder: List<Long> = emptyList()
) { ) {
fun getCategoryImpl(): CategoryImpl { fun getCategoryImpl(): CategoryImpl {
return CategoryImpl().apply { return CategoryImpl().apply {
@@ -30,7 +30,7 @@ class BackupCategory(
name = category.name, name = category.name,
order = category.order, order = category.order,
flags = category.flags, flags = category.flags,
mangaOrder = category.mangaOrder, mangaOrder = category.mangaOrder
) )
} }
} }
@@ -49,7 +49,7 @@ data class BackupChapter(
lastPageRead = chapter.last_page_read, lastPageRead = chapter.last_page_read,
dateFetch = chapter.date_fetch, dateFetch = chapter.date_fetch,
dateUpload = chapter.date_upload, dateUpload = chapter.date_upload,
sourceOrder = chapter.source_order, sourceOrder = chapter.source_order
) )
} }
} }
@@ -11,13 +11,13 @@ import kotlinx.serialization.protobuf.ProtoNumber
data class BackupFlatMetadata( data class BackupFlatMetadata(
@ProtoNumber(1) var searchMetadata: BackupSearchMetadata, @ProtoNumber(1) var searchMetadata: BackupSearchMetadata,
@ProtoNumber(2) var searchTags: List<BackupSearchTag> = emptyList(), @ProtoNumber(2) var searchTags: List<BackupSearchTag> = emptyList(),
@ProtoNumber(3) var searchTitles: List<BackupSearchTitle> = emptyList(), @ProtoNumber(3) var searchTitles: List<BackupSearchTitle> = emptyList()
) { ) {
fun getFlatMetadata(mangaId: Long): FlatMetadata { fun getFlatMetadata(mangaId: Long): FlatMetadata {
return FlatMetadata( return FlatMetadata(
metadata = searchMetadata.getSearchMetadata(mangaId), metadata = searchMetadata.getSearchMetadata(mangaId),
tags = searchTags.map { it.getSearchTag(mangaId) }, tags = searchTags.map { it.getSearchTag(mangaId) },
titles = searchTitles.map { it.getSearchTitle(mangaId) }, titles = searchTitles.map { it.getSearchTitle(mangaId) }
) )
} }
@@ -26,7 +26,7 @@ data class BackupFlatMetadata(
return BackupFlatMetadata( return BackupFlatMetadata(
searchMetadata = BackupSearchMetadata.copyFrom(flatMetadata.metadata), searchMetadata = BackupSearchMetadata.copyFrom(flatMetadata.metadata),
searchTags = flatMetadata.tags.map { BackupSearchTag.copyFrom(it) }, searchTags = flatMetadata.tags.map { BackupSearchTag.copyFrom(it) },
searchTitles = flatMetadata.titles.map { BackupSearchTitle.copyFrom(it) }, searchTitles = flatMetadata.titles.map { BackupSearchTitle.copyFrom(it) }
) )
} }
} }
@@ -3,14 +3,8 @@ package eu.kanade.tachiyomi.data.backup.full.models
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.protobuf.ProtoNumber import kotlinx.serialization.protobuf.ProtoNumber
@Serializable
data class BrokenBackupHistory(
@ProtoNumber(0) var url: String,
@ProtoNumber(1) var lastRead: Long,
)
@Serializable @Serializable
data class BackupHistory( data class BackupHistory(
@ProtoNumber(1) var url: String, @ProtoNumber(0) var url: String,
@ProtoNumber(2) var lastRead: Long, @ProtoNumber(1) var lastRead: Long
) )
@@ -26,7 +26,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,10 +34,7 @@ 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,
@@ -47,12 +44,8 @@ data class BackupManga(
@ProtoNumber(800) var customTitle: String? = null, @ProtoNumber(800) var customTitle: String? = null,
@ProtoNumber(801) var customArtist: String? = null, @ProtoNumber(801) var customArtist: String? = null,
@ProtoNumber(802) var customAuthor: String? = null, @ProtoNumber(802) var customAuthor: String? = null,
// skipping 803 due to using duplicate value in previous builds @ProtoNumber(803) var customDescription: String? = null,
@ProtoNumber(804) var customDescription: String? = null, @ProtoNumber(803) var customGenre: List<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 +60,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
} }
} }
@@ -95,7 +87,7 @@ data class BackupManga(
artist = customArtist, artist = customArtist,
description = customDescription, description = customDescription,
genre = customGenre, genre = customGenre,
status = customStatus.takeUnless { it == 0 }, status = customStatus.takeUnless { it == 0 }
) )
} }
return null return null
@@ -124,10 +116,8 @@ data class BackupManga(
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 --> // SY -->
).also { backupManga -> ).also { backupManga ->
customMangaManager?.getManga(manga)?.let { customMangaManager?.getManga(manga)?.let {
@@ -16,7 +16,7 @@ data class BackupMergedMangaReference(
@ProtoNumber(5) var downloadChapters: Boolean, @ProtoNumber(5) var downloadChapters: Boolean,
@ProtoNumber(6) var mergeUrl: String, @ProtoNumber(6) var mergeUrl: String,
@ProtoNumber(7) var mangaUrl: String, @ProtoNumber(7) var mangaUrl: String,
@ProtoNumber(8) var mangaSourceId: Long, @ProtoNumber(8) var mangaSourceId: Long
) { ) {
fun getMergedMangaReference(): MergedMangaReference { fun getMergedMangaReference(): MergedMangaReference {
return MergedMangaReference( return MergedMangaReference(
@@ -30,7 +30,7 @@ data class BackupMergedMangaReference(
mangaSourceId = mangaSourceId, mangaSourceId = mangaSourceId,
mergeId = null, mergeId = null,
mangaId = null, mangaId = null,
id = null, id = null
) )
} }
@@ -44,7 +44,7 @@ data class BackupMergedMangaReference(
downloadChapters = mergedMangaReference.downloadChapters, downloadChapters = mergedMangaReference.downloadChapters,
mergeUrl = mergedMangaReference.mergeUrl, mergeUrl = mergedMangaReference.mergeUrl,
mangaUrl = mergedMangaReference.mangaUrl, mangaUrl = mergedMangaReference.mangaUrl,
mangaSourceId = mergedMangaReference.mangaSourceId, mangaSourceId = mergedMangaReference.mangaSourceId
) )
} }
} }
@@ -11,5 +11,5 @@ data class BackupSavedSearch(
@ProtoNumber(1) val name: String, @ProtoNumber(1) val name: String,
@ProtoNumber(2) val query: String = "", @ProtoNumber(2) val query: String = "",
@ProtoNumber(3) val filterList: String = "", @ProtoNumber(3) val filterList: String = "",
@ProtoNumber(4) val source: Long = 0, @ProtoNumber(4) val source: Long = 0
) )
@@ -4,22 +4,16 @@ import eu.kanade.tachiyomi.source.Source
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.protobuf.ProtoNumber import kotlinx.serialization.protobuf.ProtoNumber
@Serializable
data class BrokenBackupSource(
@ProtoNumber(0) var name: String = "",
@ProtoNumber(1) var sourceId: Long,
)
@Serializable @Serializable
data class BackupSource( data class BackupSource(
@ProtoNumber(1) var name: String = "", @ProtoNumber(0) var name: String = "",
@ProtoNumber(2) var sourceId: Long, @ProtoNumber(1) var sourceId: Long
) { ) {
companion object { companion object {
fun copyFrom(source: Source): BackupSource { fun copyFrom(source: Source): BackupSource {
return BackupSource( return BackupSource(
name = source.name, name = source.name,
sourceId = source.id, sourceId = source.id
) )
} }
} }
@@ -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,13 +51,14 @@ 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,
startedReadingDate = track.started_reading_date, startedReadingDate = track.started_reading_date,
finishedReadingDate = track.finished_reading_date, finishedReadingDate = track.finished_reading_date,
trackingUrl = track.tracking_url, trackingUrl = track.tracking_url
) )
} }
} }
@@ -9,7 +9,7 @@ data class BackupSearchMetadata(
@ProtoNumber(1) var uploader: String? = null, @ProtoNumber(1) var uploader: String? = null,
@ProtoNumber(2) var extra: String, @ProtoNumber(2) var extra: String,
@ProtoNumber(3) var indexedExtra: String? = null, @ProtoNumber(3) var indexedExtra: String? = null,
@ProtoNumber(4) var extraVersion: Int, @ProtoNumber(4) var extraVersion: Int
) { ) {
fun getSearchMetadata(mangaId: Long): SearchMetadata { fun getSearchMetadata(mangaId: Long): SearchMetadata {
return SearchMetadata( return SearchMetadata(
@@ -17,7 +17,7 @@ data class BackupSearchMetadata(
uploader = uploader, uploader = uploader,
extra = extra, extra = extra,
indexedExtra = indexedExtra, indexedExtra = indexedExtra,
extraVersion = extraVersion, extraVersion = extraVersion
) )
} }
@@ -27,7 +27,7 @@ data class BackupSearchMetadata(
uploader = searchMetadata.uploader, uploader = searchMetadata.uploader,
extra = searchMetadata.extra, extra = searchMetadata.extra,
indexedExtra = searchMetadata.indexedExtra, indexedExtra = searchMetadata.indexedExtra,
extraVersion = searchMetadata.extraVersion, extraVersion = searchMetadata.extraVersion
) )
} }
} }
@@ -8,7 +8,7 @@ import kotlinx.serialization.protobuf.ProtoNumber
data class BackupSearchTag( data class BackupSearchTag(
@ProtoNumber(1) var namespace: String? = null, @ProtoNumber(1) var namespace: String? = null,
@ProtoNumber(2) var name: String, @ProtoNumber(2) var name: String,
@ProtoNumber(3) var type: Int, @ProtoNumber(3) var type: Int
) { ) {
fun getSearchTag(mangaId: Long): SearchTag { fun getSearchTag(mangaId: Long): SearchTag {
return SearchTag( return SearchTag(
@@ -16,7 +16,7 @@ data class BackupSearchTag(
mangaId = mangaId, mangaId = mangaId,
namespace = namespace, namespace = namespace,
name = name, name = name,
type = type, type = type
) )
} }
@@ -25,7 +25,7 @@ data class BackupSearchTag(
return BackupSearchTag( return BackupSearchTag(
namespace = searchTag.namespace, namespace = searchTag.namespace,
name = searchTag.name, name = searchTag.name,
type = searchTag.type, type = searchTag.type
) )
} }
} }
@@ -7,14 +7,14 @@ import kotlinx.serialization.protobuf.ProtoNumber
@Serializable @Serializable
data class BackupSearchTitle( data class BackupSearchTitle(
@ProtoNumber(1) var title: String, @ProtoNumber(1) var title: String,
@ProtoNumber(2) var type: Int, @ProtoNumber(2) var type: Int
) { ) {
fun getSearchTitle(mangaId: Long): SearchTitle { fun getSearchTitle(mangaId: Long): SearchTitle {
return SearchTitle( return SearchTitle(
id = null, id = null,
mangaId = mangaId, mangaId = mangaId,
title = title, title = title,
type = type, type = type
) )
} }
@@ -22,7 +22,7 @@ data class BackupSearchTitle(
fun copyFrom(searchTitle: SearchTitle): BackupSearchTitle { fun copyFrom(searchTitle: SearchTitle): BackupSearchTitle {
return BackupSearchTitle( return BackupSearchTitle(
title = searchTitle.title, title = searchTitle.title,
type = searchTitle.type, type = searchTitle.type
) )
} }
} }
@@ -2,69 +2,84 @@ 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
import exh.eh.EHentaiThrottleManager import exh.eh.EHentaiThrottleManager
import exh.merged.sql.models.MergedMangaReference import exh.merged.sql.models.MergedMangaReference
import exh.savedsearches.models.SavedSearch import exh.savedsearches.JsonSavedSearch
import exh.source.MERGED_SOURCE_ID import exh.source.MERGED_SOURCE_ID
import exh.util.nullIfBlank
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.json.JsonObject import timber.log.Timber
import kotlinx.serialization.json.contentOrNull import uy.kohesive.injekt.Injekt
import kotlinx.serialization.json.jsonArray import uy.kohesive.injekt.api.get
import kotlinx.serialization.json.jsonPrimitive import java.lang.RuntimeException
import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.modules.contextual
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")
} }
@@ -72,10 +87,182 @@ class LegacyBackupManager(context: Context, version: Int = CURRENT_VERSION) : Ab
* Create backup Json file from database * Create backup Json file from database
* *
* @param uri path of Uri * @param uri path of Uri
* @param isAutoBackup backup called from scheduled backup job * @param isJob backup called from job
*/ */
override fun createBackup(uri: Uri, flags: Int, isAutoBackup: 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
@@ -125,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 ->
@@ -289,39 +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 currentSavedSearches = databaseHelper.getSavedSearches().executeAsBlocking()
val newSavedSearches = backupSavedSearches.mapNotNull { val newSavedSearches = backupSavedSearches.mapNotNull {
runCatching { try {
val content = parser.decodeFromString<JsonObject>(it.substringAfter(':')) val id = it.substringBefore(':').toLong()
SavedSearch( val content = Json.decodeFromString<JsonSavedSearch>(it.substringAfter(':'))
id = null, id to content
source = it.substringBefore(':').toLongOrNull() ?: return@mapNotNull null, } catch (t: RuntimeException) {
content["name"]!!.jsonPrimitive.content, // Load failed
content["query"]!!.jsonPrimitive.contentOrNull?.nullIfBlank(), Timber.e(t, "Failed to load saved search!")
Json.encodeToString(content["filters"]!!.jsonArray), t.printStackTrace()
) null
}.getOrNull() }
}.filter { backupSavedSearch -> }.toMutableList()
currentSavedSearches.none { it.name == backupSavedSearch.name && it.source == backupSavedSearch.source }
}.ifEmpty { null }
if (newSavedSearches != null) { val currentSources = newSavedSearches.map { it.first }.toSet()
databaseHelper.insertSavedSearches(newSavedSearches)
newSavedSearches += preferences.savedSearches().get().mapNotNull {
try {
val id = it.substringBefore(':').toLong()
val content = Json.decodeFromString<JsonSavedSearch>(it.substringAfter(':'))
id to content
} catch (t: RuntimeException) {
// Load failed
Timber.e(t, "Failed to load saved search!")
t.printStackTrace()
null
}
}.toMutableList()
val otherSerialized = preferences.savedSearches().get().mapNotNull {
val sourceId = it.split(":")[0].toLongOrNull() ?: return@mapNotNull null
if (sourceId in currentSources) return@mapNotNull null
it
} }
val newSerialized = newSavedSearches.map {
"${it.first}:" + Json.encodeToString(it.second)
}
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,21 +90,28 @@ class LegacyBackupRestore(context: Context, notifier: BackupNotifier) : Abstract
} }
// SY <-- // SY <--
private fun restoreCategories(categoriesJson: List<Category>) { private suspend fun restoreManga(mangaJson: JsonObject) {
db.inTransaction { val 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) EXHMigrations.migrateBackupEntry(manga)
@@ -143,7 +150,7 @@ class LegacyBackupRestore(context: Context, notifier: BackupNotifier) : Abstract
chapters: List<Chapter>, chapters: List<Chapter>,
categories: List<String>, categories: List<String>,
history: List<DHistory>, history: List<DHistory>,
tracks: List<Track>, tracks: List<Track>
) { ) {
val dbManga = backupManager.getMangaFromDatabase(manga) val dbManga = backupManager.getMangaFromDatabase(manga)
@@ -173,7 +180,7 @@ class LegacyBackupRestore(context: Context, notifier: BackupNotifier) : Abstract
chapters: List<Chapter>, chapters: List<Chapter>,
categories: List<String>, categories: List<String>,
history: List<DHistory>, history: List<DHistory>,
tracks: List<Track>, tracks: List<Track>
) { ) {
try { try {
val fetchedManga = backupManager.fetchManga(source, manga) val fetchedManga = backupManager.fetchManga(source, manga)
@@ -195,7 +202,7 @@ class LegacyBackupRestore(context: Context, notifier: BackupNotifier) : Abstract
chapters: List<Chapter>, chapters: List<Chapter>,
categories: List<String>, categories: List<String>,
history: List<DHistory>, history: List<DHistory>,
tracks: List<Track>, tracks: List<Track>
) { ) {
if (!backupManager.restoreChaptersForManga(backupManga, chapters)) { if (!backupManager.restoreChaptersForManga(backupManga, chapters)) {
updateChapters(source, backupManga, chapters) updateChapters(source, backupManga, chapters)
@@ -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
/** /**
@@ -93,24 +95,48 @@ class ChapterCache(private val context: Context) {
File(context.cacheDir, PARAMETER_CACHE_DIRECTORY), File(context.cacheDir, PARAMETER_CACHE_DIRECTORY),
PARAMETER_APP_VERSION, PARAMETER_APP_VERSION,
PARAMETER_VALUE_COUNT, PARAMETER_VALUE_COUNT,
cacheSize * 1024 * 1024, cacheSize * 1024 * 1024
) )
} }
// <-- 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,294 +0,0 @@
package eu.kanade.tachiyomi.data.coil
import coil.ImageLoader
import coil.decode.DataSource
import coil.decode.ImageSource
import coil.disk.DiskCache
import coil.fetch.FetchResult
import coil.fetch.Fetcher
import coil.fetch.SourceResult
import coil.network.HttpException
import coil.request.Options
import coil.request.Parameters
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.await
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.system.logcat
import logcat.LogPriority
import okhttp3.CacheControl
import okhttp3.Call
import okhttp3.Request
import okhttp3.Response
import okhttp3.internal.closeQuietly
import okio.Path.Companion.toOkioPath
import okio.Source
import okio.buffer
import okio.sink
import uy.kohesive.injekt.injectLazy
import java.io.File
import java.net.HttpURLConnection
/**
* A [Fetcher] that fetches cover image for [Manga] object.
*
* It uses [Manga.thumbnail_url] if custom cover is not set by the user.
* Disk caching for library items is handled by [CoverCache], otherwise
* handled by Coil's [DiskCache].
*
* Available request parameter:
* - [USE_CUSTOM_COVER]: Use custom cover if set by user, default is true
*/
class MangaCoverFetcher(
private val manga: Manga,
private val sourceLazy: Lazy<HttpSource?>,
private val options: Options,
private val coverCache: CoverCache,
private val callFactoryLazy: Lazy<Call.Factory>,
private val diskCacheLazy: Lazy<DiskCache>,
) : Fetcher {
// For non-custom cover
private val diskCacheKey: String? by lazy { MangaCoverKeyer().key(manga, options) }
private lateinit var url: String
override suspend fun fetch(): FetchResult {
// Use custom cover if exists
val useCustomCover = options.parameters.value(USE_CUSTOM_COVER) ?: true
val customCoverFile = coverCache.getCustomCoverFile(manga)
if (useCustomCover && customCoverFile.exists()) {
return fileLoader(customCoverFile)
}
// diskCacheKey is thumbnail_url
url = diskCacheKey ?: error("No cover specified")
return when (getResourceType(url)) {
Type.URL -> httpLoader()
Type.File -> fileLoader(File(url.substringAfter("file://")))
null -> error("Invalid image")
}
}
private fun fileLoader(file: File): FetchResult {
return SourceResult(
source = ImageSource(file = file.toOkioPath(), diskCacheKey = diskCacheKey),
mimeType = "image/*",
dataSource = DataSource.DISK,
)
}
private suspend fun httpLoader(): FetchResult {
// Only cache separately if it's a library item
val libraryCoverCacheFile = if (manga.favorite) {
coverCache.getCoverFile(manga) ?: error("No cover specified")
} else {
null
}
if (libraryCoverCacheFile?.exists() == true && options.diskCachePolicy.readEnabled) {
return fileLoader(libraryCoverCacheFile)
}
var snapshot = readFromDiskCache()
try {
// Fetch from disk cache
if (snapshot != null) {
val snapshotCoverCache = moveSnapshotToCoverCache(snapshot, libraryCoverCacheFile)
if (snapshotCoverCache != null) {
// Read from cover cache after added to library
return fileLoader(snapshotCoverCache)
}
// Read from snapshot
return SourceResult(
source = snapshot.toImageSource(),
mimeType = "image/*",
dataSource = DataSource.DISK,
)
}
// Fetch from network
val response = executeNetworkRequest()
val responseBody = checkNotNull(response.body) { "Null response source" }
try {
// Read from cover cache after library manga cover updated
val responseCoverCache = writeResponseToCoverCache(response, libraryCoverCacheFile)
if (responseCoverCache != null) {
return fileLoader(responseCoverCache)
}
// Read from disk cache
snapshot = writeToDiskCache(snapshot, response)
if (snapshot != null) {
return SourceResult(
source = snapshot.toImageSource(),
mimeType = "image/*",
dataSource = DataSource.NETWORK,
)
}
// Read from response if cache is unused or unusable
return SourceResult(
source = ImageSource(source = responseBody.source(), context = options.context),
mimeType = "image/*",
dataSource = if (response.cacheResponse != null) DataSource.DISK else DataSource.NETWORK,
)
} catch (e: Exception) {
responseBody.closeQuietly()
throw e
}
} catch (e: Exception) {
snapshot?.closeQuietly()
throw e
}
}
private suspend fun executeNetworkRequest(): Response {
val client = sourceLazy.value?.client ?: callFactoryLazy.value
val response = client.newCall(newRequest()).await()
if (!response.isSuccessful && response.code != HttpURLConnection.HTTP_NOT_MODIFIED) {
response.body?.closeQuietly()
throw HttpException(response)
}
return response
}
private fun newRequest(): Request {
val request = Request.Builder()
.url(url)
.headers(sourceLazy.value?.headers ?: options.headers)
// Support attaching custom data to the network request.
.tag(Parameters::class.java, options.parameters)
val diskRead = options.diskCachePolicy.readEnabled
val networkRead = options.networkCachePolicy.readEnabled
when {
!networkRead && diskRead -> {
request.cacheControl(CacheControl.FORCE_CACHE)
}
networkRead && !diskRead -> if (options.diskCachePolicy.writeEnabled) {
request.cacheControl(CacheControl.FORCE_NETWORK)
} else {
request.cacheControl(CACHE_CONTROL_FORCE_NETWORK_NO_CACHE)
}
!networkRead && !diskRead -> {
// This causes the request to fail with a 504 Unsatisfiable Request.
request.cacheControl(CACHE_CONTROL_NO_NETWORK_NO_CACHE)
}
}
return request.build()
}
private fun moveSnapshotToCoverCache(snapshot: DiskCache.Snapshot, cacheFile: File?): File? {
if (cacheFile == null) return null
return try {
diskCacheLazy.value.run {
fileSystem.source(snapshot.data).use { input ->
writeSourceToCoverCache(input, cacheFile)
}
remove(diskCacheKey!!)
}
cacheFile.takeIf { it.exists() }
} catch (e: Exception) {
logcat(LogPriority.ERROR, e) { "Failed to write snapshot data to cover cache ${cacheFile.name}" }
null
}
}
private fun writeResponseToCoverCache(response: Response, cacheFile: File?): File? {
if (cacheFile == null || !options.diskCachePolicy.writeEnabled) return null
return try {
response.peekBody(Long.MAX_VALUE).source().use { input ->
writeSourceToCoverCache(input, cacheFile)
}
cacheFile.takeIf { it.exists() }
} catch (e: Exception) {
logcat(LogPriority.ERROR, e) { "Failed to write response data to cover cache ${cacheFile.name}" }
null
}
}
private fun writeSourceToCoverCache(input: Source, cacheFile: File) {
cacheFile.parentFile?.mkdirs()
cacheFile.delete()
try {
cacheFile.sink().buffer().use { output ->
output.writeAll(input)
}
} catch (e: Exception) {
cacheFile.delete()
throw e
}
}
private fun readFromDiskCache(): DiskCache.Snapshot? {
return if (options.diskCachePolicy.readEnabled) diskCacheLazy.value[diskCacheKey!!] else null
}
private fun writeToDiskCache(
snapshot: DiskCache.Snapshot?,
response: Response,
): DiskCache.Snapshot? {
if (!options.diskCachePolicy.writeEnabled) {
snapshot?.closeQuietly()
return null
}
val editor = if (snapshot != null) {
snapshot.closeAndEdit()
} else {
diskCacheLazy.value.edit(diskCacheKey!!)
} ?: return null
try {
diskCacheLazy.value.fileSystem.write(editor.data) {
response.body!!.source().readAll(this)
}
return editor.commitAndGet()
} catch (e: Exception) {
try {
editor.abort()
} catch (ignored: Exception) {
}
throw e
}
}
private fun DiskCache.Snapshot.toImageSource(): ImageSource {
return ImageSource(file = data, diskCacheKey = diskCacheKey, closeable = this)
}
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
}
class Factory(
private val callFactoryLazy: Lazy<Call.Factory>,
private val diskCacheLazy: Lazy<DiskCache>,
) : Fetcher.Factory<Manga> {
private val coverCache: CoverCache by injectLazy()
private val sourceManager: SourceManager by injectLazy()
override fun create(data: Manga, options: Options, imageLoader: ImageLoader): Fetcher {
val source = lazy { sourceManager.get(data.source) as? HttpSource }
return MangaCoverFetcher(data, source, options, coverCache, callFactoryLazy, diskCacheLazy)
}
}
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,11 +0,0 @@
package eu.kanade.tachiyomi.data.coil
import coil.key.Keyer
import coil.request.Options
import eu.kanade.tachiyomi.data.database.models.Manga
class MangaCoverKeyer : Keyer<Manga> {
override fun key(data: Manga, options: Options): String? {
return data.thumbnail_url?.takeIf { it.isNotBlank() }
}
}
@@ -1,61 +0,0 @@
package eu.kanade.tachiyomi.data.coil
import android.os.Build
import androidx.core.graphics.drawable.toDrawable
import coil.ImageLoader
import coil.decode.DecodeResult
import coil.decode.Decoder
import coil.decode.ImageDecoderDecoder
import coil.decode.ImageSource
import coil.fetch.SourceResult
import coil.request.Options
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: ImageSource, private val options: Options) : Decoder {
override suspend fun decode(): DecodeResult {
val decoder = resources.sourceOrNull()?.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(options.context.resources),
isSampled = false,
)
}
class Factory : Decoder.Factory {
override fun create(result: SourceResult, options: Options, imageLoader: ImageLoader): Decoder? {
if (!isApplicable(result.source.source())) return null
return TachiyomiImageDecoder(result.source, options)
}
private fun isApplicable(source: BufferedSource): 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 fun equals(other: Any?) = other is ImageDecoderDecoder.Factory
override fun hashCode() = javaClass.hashCode()
}
}
@@ -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
@@ -36,33 +36,13 @@ import exh.metadata.sql.models.SearchTitle
import exh.metadata.sql.queries.SearchMetadataQueries import exh.metadata.sql.queries.SearchMetadataQueries
import exh.metadata.sql.queries.SearchTagQueries import exh.metadata.sql.queries.SearchTagQueries
import exh.metadata.sql.queries.SearchTitleQueries import exh.metadata.sql.queries.SearchTitleQueries
import exh.savedsearches.mappers.FeedSavedSearchTypeMapping
import exh.savedsearches.mappers.SavedSearchTypeMapping
import exh.savedsearches.models.FeedSavedSearch
import exh.savedsearches.models.SavedSearch
import exh.savedsearches.queries.FeedSavedSearchQueries
import exh.savedsearches.queries.SavedSearchQueries
import io.requery.android.database.sqlite.RequerySQLiteOpenHelperFactory 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, MangaQueries, ChapterQueries, TrackQueries, CategoryQueries, MangaCategoryQueries, HistoryQueries /* SY --> */, SearchMetadataQueries, SearchTagQueries, SearchTitleQueries, MergedQueries, SimilarQueries /* SY <-- */ {
ChapterQueries,
TrackQueries,
CategoryQueries,
MangaCategoryQueries,
HistoryQueries,
/* SY --> */
SearchMetadataQueries,
SearchTagQueries,
SearchTitleQueries,
MergedQueries,
FavoriteEntryQueries,
SavedSearchQueries,
FeedSavedSearchQueries
/* SY <-- */ {
private val configuration = SupportSQLiteOpenHelper.Configuration.builder(context) private val configuration = SupportSQLiteOpenHelper.Configuration.builder(context)
.name(DbOpenCallback.DATABASE_NAME) .name(DbOpenCallback.DATABASE_NAME)
@@ -82,9 +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())
.addTypeMapping(SavedSearch::class.java, SavedSearchTypeMapping())
.addTypeMapping(FeedSavedSearch::class.java, FeedSavedSearchTypeMapping())
// SY <-- // SY <--
.build() .build()
@@ -8,13 +8,11 @@ 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
import exh.metadata.sql.tables.SearchTitleTable import exh.metadata.sql.tables.SearchTitleTable
import exh.savedsearches.tables.FeedSavedSearchTable
import exh.savedsearches.tables.SavedSearchTable
class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) { class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
@@ -27,7 +25,7 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
/** /**
* Version of the database. * Version of the database.
*/ */
const val DATABASE_VERSION = /* SY --> */ 13 // SY <-- const val DATABASE_VERSION = /* SY --> */ 5 /* SY <-- */
} }
override fun onCreate(db: SupportSQLiteDatabase) = with(db) { override fun onCreate(db: SupportSQLiteDatabase) = with(db) {
@@ -42,9 +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)
execSQL(SavedSearchTable.createTableQuery)
execSQL(FeedSavedSearchTable.createTableQuery)
// SY <-- // SY <--
// DB indexes // DB indexes
@@ -61,7 +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(FeedSavedSearchTable.createSavedSearchIdIndexQuery) execSQL(SimilarTable.createMangaIdIndexQuery)
// SY <-- // SY <--
} }
@@ -78,38 +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)
}
if (oldVersion < 13) {
db.execSQL(SavedSearchTable.createTableQuery)
db.execSQL(FeedSavedSearchTable.createTableQuery)
db.execSQL(FeedSavedSearchTable.createSavedSearchIdIndexQuery)
} }
} }
@@ -21,7 +21,7 @@ import eu.kanade.tachiyomi.data.database.tables.CategoryTable.TABLE
class CategoryTypeMapping : SQLiteTypeMapping<Category>( class CategoryTypeMapping : SQLiteTypeMapping<Category>(
CategoryPutResolver(), CategoryPutResolver(),
CategoryGetResolver(), CategoryGetResolver(),
CategoryDeleteResolver(), CategoryDeleteResolver()
) )
class CategoryPutResolver : DefaultPutResolver<Category>() { class CategoryPutResolver : DefaultPutResolver<Category>() {
@@ -42,20 +42,20 @@ class CategoryPutResolver : DefaultPutResolver<Category>() {
COL_NAME to obj.name, COL_NAME to obj.name,
COL_ORDER to obj.order, COL_ORDER to obj.order,
COL_FLAGS to obj.flags, COL_FLAGS to obj.flags,
COL_MANGA_ORDER to obj.mangaOrder.joinToString("/"), COL_MANGA_ORDER to obj.mangaOrder.joinToString("/")
) )
} }
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 <--
} }
@@ -28,7 +28,7 @@ import eu.kanade.tachiyomi.data.database.tables.ChapterTable.TABLE
class ChapterTypeMapping : SQLiteTypeMapping<Chapter>( class ChapterTypeMapping : SQLiteTypeMapping<Chapter>(
ChapterPutResolver(), ChapterPutResolver(),
ChapterGetResolver(), ChapterGetResolver(),
ChapterDeleteResolver(), ChapterDeleteResolver()
) )
class ChapterPutResolver : DefaultPutResolver<Chapter>() { class ChapterPutResolver : DefaultPutResolver<Chapter>() {
@@ -56,25 +56,25 @@ class ChapterPutResolver : DefaultPutResolver<Chapter>() {
COL_DATE_UPLOAD to obj.date_upload, COL_DATE_UPLOAD to obj.date_upload,
COL_LAST_PAGE_READ to obj.last_page_read, COL_LAST_PAGE_READ to obj.last_page_read,
COL_CHAPTER_NUMBER to obj.chapter_number, COL_CHAPTER_NUMBER to obj.chapter_number,
COL_SOURCE_ORDER to obj.source_order, COL_SOURCE_ORDER to obj.source_order
) )
} }
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))
} }
} }
@@ -20,7 +20,7 @@ import eu.kanade.tachiyomi.data.database.tables.HistoryTable.TABLE
class HistoryTypeMapping : SQLiteTypeMapping<History>( class HistoryTypeMapping : SQLiteTypeMapping<History>(
HistoryPutResolver(), HistoryPutResolver(),
HistoryGetResolver(), HistoryGetResolver(),
HistoryDeleteResolver(), HistoryDeleteResolver()
) )
open class HistoryPutResolver : DefaultPutResolver<History>() { open class HistoryPutResolver : DefaultPutResolver<History>() {
@@ -40,17 +40,17 @@ open class HistoryPutResolver : DefaultPutResolver<History>() {
COL_ID to obj.id, COL_ID to obj.id,
COL_CHAPTER_ID to obj.chapter_id, COL_CHAPTER_ID to obj.chapter_id,
COL_LAST_READ to obj.last_read, COL_LAST_READ to obj.last_read,
COL_TIME_READ to obj.time_read, COL_TIME_READ to obj.time_read
) )
} }
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))
} }
} }
@@ -18,7 +18,7 @@ import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable.TABLE
class MangaCategoryTypeMapping : SQLiteTypeMapping<MangaCategory>( class MangaCategoryTypeMapping : SQLiteTypeMapping<MangaCategory>(
MangaCategoryPutResolver(), MangaCategoryPutResolver(),
MangaCategoryGetResolver(), MangaCategoryGetResolver(),
MangaCategoryDeleteResolver(), MangaCategoryDeleteResolver()
) )
class MangaCategoryPutResolver : DefaultPutResolver<MangaCategory>() { class MangaCategoryPutResolver : DefaultPutResolver<MangaCategory>() {
@@ -37,16 +37,16 @@ class MangaCategoryPutResolver : DefaultPutResolver<MangaCategory>() {
contentValuesOf( contentValuesOf(
COL_ID to obj.id, COL_ID to obj.id,
COL_MANGA_ID to obj.manga_id, COL_MANGA_ID to obj.manga_id,
COL_CATEGORY_ID to obj.category_id, COL_CATEGORY_ID to obj.category_id
) )
} }
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
@@ -34,7 +33,7 @@ import eu.kanade.tachiyomi.data.database.tables.MangaTable.TABLE
class MangaTypeMapping : SQLiteTypeMapping<Manga>( class MangaTypeMapping : SQLiteTypeMapping<Manga>(
MangaPutResolver(), MangaPutResolver(),
MangaGetResolver(), MangaGetResolver(),
MangaDeleteResolver(), MangaDeleteResolver()
) )
class MangaPutResolver : DefaultPutResolver<Manga>() { class MangaPutResolver : DefaultPutResolver<Manga>() {
@@ -66,34 +65,32 @@ class MangaPutResolver : DefaultPutResolver<Manga>() {
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))
} }
} }
@@ -29,7 +29,7 @@ import eu.kanade.tachiyomi.data.database.tables.TrackTable.TABLE
class TrackTypeMapping : SQLiteTypeMapping<Track>( class TrackTypeMapping : SQLiteTypeMapping<Track>(
TrackPutResolver(), TrackPutResolver(),
TrackGetResolver(), TrackGetResolver(),
TrackDeleteResolver(), TrackDeleteResolver()
) )
class TrackPutResolver : DefaultPutResolver<Track>() { class TrackPutResolver : DefaultPutResolver<Track>() {
@@ -58,26 +58,26 @@ class TrackPutResolver : DefaultPutResolver<Track>() {
COL_TRACKING_URL to obj.tracking_url, COL_TRACKING_URL to obj.tracking_url,
COL_SCORE to obj.score, COL_SCORE to obj.score,
COL_START_DATE to obj.started_reading_date, COL_START_DATE to obj.started_reading_date,
COL_FINISH_DATE to obj.finished_reading_date, COL_FINISH_DATE to obj.finished_reading_date
) )
} }
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 }
} }
} }
@@ -2,14 +2,7 @@ package eu.kanade.tachiyomi.data.database.models
class LibraryManga : MangaImpl() { class LibraryManga : MangaImpl() {
var unreadCount: Int = 0 var unread: Int = 0
var readCount: Int = 0
val totalChapters
get() = readCount + unreadCount
val hasStarted
get() = readCount > 0
var category: Int = 0 var category: Int = 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
@@ -133,6 +115,6 @@ fun Manga.toMangaInfo(): MangaInfo {
genres = this.getGenres() ?: emptyList(), genres = this.getGenres() ?: emptyList(),
key = this.url, key = this.url,
status = this.status, status = this.status,
title = this.title, title = this.title
) )
} }
@@ -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
@@ -54,14 +56,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
@@ -89,10 +89,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
@@ -15,7 +15,7 @@ interface CategoryQueries : DbProvider {
Query.builder() Query.builder()
.table(CategoryTable.TABLE) .table(CategoryTable.TABLE)
.orderBy(CategoryTable.COL_ORDER) .orderBy(CategoryTable.COL_ORDER)
.build(), .build()
) )
.prepare() .prepare()
@@ -25,7 +25,7 @@ interface CategoryQueries : DbProvider {
RawQuery.builder() RawQuery.builder()
.query(getCategoriesForMangaQuery()) .query(getCategoriesForMangaQuery())
.args(manga.id) .args(manga.id)
.build(), .build()
) )
.prepare() .prepare()
@@ -25,7 +25,7 @@ interface ChapterQueries : DbProvider {
.table(ChapterTable.TABLE) .table(ChapterTable.TABLE)
.where("${ChapterTable.COL_MANGA_ID} = ?") .where("${ChapterTable.COL_MANGA_ID} = ?")
.whereArgs(mangaId) .whereArgs(mangaId)
.build(), .build()
) )
.prepare() .prepare()
// SY <-- // SY <--
@@ -37,7 +37,7 @@ interface ChapterQueries : DbProvider {
.query(getRecentsQuery()) .query(getRecentsQuery())
.args(date.time) .args(date.time)
.observesTables(ChapterTable.TABLE) .observesTables(ChapterTable.TABLE)
.build(), .build()
) )
.withGetResolver(MangaChapterGetResolver.INSTANCE) .withGetResolver(MangaChapterGetResolver.INSTANCE)
.prepare() .prepare()
@@ -49,7 +49,7 @@ interface ChapterQueries : DbProvider {
.table(ChapterTable.TABLE) .table(ChapterTable.TABLE)
.where("${ChapterTable.COL_ID} = ?") .where("${ChapterTable.COL_ID} = ?")
.whereArgs(id) .whereArgs(id)
.build(), .build()
) )
.prepare() .prepare()
@@ -60,7 +60,7 @@ interface ChapterQueries : DbProvider {
.table(ChapterTable.TABLE) .table(ChapterTable.TABLE)
.where("${ChapterTable.COL_URL} = ?") .where("${ChapterTable.COL_URL} = ?")
.whereArgs(url) .whereArgs(url)
.build(), .build()
) )
.prepare() .prepare()
@@ -71,7 +71,7 @@ interface ChapterQueries : DbProvider {
.table(ChapterTable.TABLE) .table(ChapterTable.TABLE)
.where("${ChapterTable.COL_URL} = ? AND ${ChapterTable.COL_MANGA_ID} = ?") .where("${ChapterTable.COL_URL} = ? AND ${ChapterTable.COL_MANGA_ID} = ?")
.whereArgs(url, mangaId) .whereArgs(url, mangaId)
.build(), .build()
) )
.prepare() .prepare()
@@ -83,7 +83,7 @@ interface ChapterQueries : DbProvider {
.table(ChapterTable.TABLE) .table(ChapterTable.TABLE)
.where("${ChapterTable.COL_URL} = ?") .where("${ChapterTable.COL_URL} = ?")
.whereArgs(url) .whereArgs(url)
.build(), .build()
) )
.prepare() .prepare()
@@ -94,7 +94,7 @@ interface ChapterQueries : DbProvider {
.table(ChapterTable.TABLE) .table(ChapterTable.TABLE)
.where("${ChapterTable.COL_URL} IN (?) AND (${ChapterTable.COL_READ} = 1 OR ${ChapterTable.COL_LAST_PAGE_READ} != 0)") .where("${ChapterTable.COL_URL} IN (?) AND (${ChapterTable.COL_READ} = 1 OR ${ChapterTable.COL_LAST_PAGE_READ} != 0)")
.whereArgs(urls.joinToString { "\"$it\"" }) .whereArgs(urls.joinToString { "\"$it\"" })
.build(), .build()
) )
.prepare() .prepare()
// SY <-- // SY <--
@@ -5,7 +5,6 @@ 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.History import eu.kanade.tachiyomi.data.database.models.History
import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory
import eu.kanade.tachiyomi.data.database.resolvers.HistoryChapterIdPutResolver
import eu.kanade.tachiyomi.data.database.resolvers.HistoryLastReadPutResolver import eu.kanade.tachiyomi.data.database.resolvers.HistoryLastReadPutResolver
import eu.kanade.tachiyomi.data.database.resolvers.MangaChapterHistoryGetResolver import eu.kanade.tachiyomi.data.database.resolvers.MangaChapterHistoryGetResolver
import eu.kanade.tachiyomi.data.database.tables.HistoryTable import eu.kanade.tachiyomi.data.database.tables.HistoryTable
@@ -33,7 +32,7 @@ interface HistoryQueries : DbProvider {
.query(getRecentMangasQuery(search)) .query(getRecentMangasQuery(search))
.args(date.time, limit, offset) .args(date.time, limit, offset)
.observesTables(HistoryTable.TABLE) .observesTables(HistoryTable.TABLE)
.build(), .build()
) )
.withGetResolver(MangaChapterHistoryGetResolver.INSTANCE) .withGetResolver(MangaChapterHistoryGetResolver.INSTANCE)
.prepare() .prepare()
@@ -45,7 +44,7 @@ interface HistoryQueries : DbProvider {
.query(getHistoryByMangaId()) .query(getHistoryByMangaId())
.args(mangaId) .args(mangaId)
.observesTables(HistoryTable.TABLE) .observesTables(HistoryTable.TABLE)
.build(), .build()
) )
.prepare() .prepare()
@@ -56,7 +55,7 @@ interface HistoryQueries : DbProvider {
.query(getHistoryByChapterUrl()) .query(getHistoryByChapterUrl())
.args(chapterUrl) .args(chapterUrl)
.observesTables(HistoryTable.TABLE) .observesTables(HistoryTable.TABLE)
.build(), .build()
) )
.prepare() .prepare()
@@ -84,7 +83,7 @@ interface HistoryQueries : DbProvider {
.byQuery( .byQuery(
DeleteQuery.builder() DeleteQuery.builder()
.table(HistoryTable.TABLE) .table(HistoryTable.TABLE)
.build(), .build()
) )
.prepare() .prepare()
@@ -94,24 +93,7 @@ interface HistoryQueries : DbProvider {
.table(HistoryTable.TABLE) .table(HistoryTable.TABLE)
.where("${HistoryTable.COL_LAST_READ} = ?") .where("${HistoryTable.COL_LAST_READ} = ?")
.whereArgs(0) .whereArgs(0)
.build(), .build()
) )
.prepare() .prepare()
// SY -->
fun updateHistoryChapterIds(history: List<History>) = db.put()
.objects(history)
.withPutResolver(HistoryChapterIdPutResolver())
.prepare()
fun deleteHistoryIds(ids: List<Long>) = db.delete()
.byQuery(
DeleteQuery.builder()
.table(HistoryTable.TABLE)
.where("${HistoryTable.COL_ID} IN (?)")
.whereArgs(ids.joinToString())
.build(),
)
.prepare()
// SY <--
} }
@@ -20,7 +20,7 @@ interface MangaCategoryQueries : DbProvider {
.table(MangaCategoryTable.TABLE) .table(MangaCategoryTable.TABLE)
.where("${MangaCategoryTable.COL_MANGA_ID} IN (${Queries.placeholders(mangas.size)})") .where("${MangaCategoryTable.COL_MANGA_ID} IN (${Queries.placeholders(mangas.size)})")
.whereArgs(*mangas.map { it.id }.toTypedArray()) .whereArgs(*mangas.map { it.id }.toTypedArray())
.build(), .build()
) )
.prepare() .prepare()
@@ -1,25 +1,21 @@
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.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,48 +25,38 @@ 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(
RawQuery.builder() RawQuery.builder()
.query(libraryQuery) .query(libraryQuery)
.observesTables(MangaTable.TABLE, ChapterTable.TABLE, MangaCategoryTable.TABLE, CategoryTable.TABLE) .observesTables(MangaTable.TABLE, ChapterTable.TABLE, MangaCategoryTable.TABLE, CategoryTable.TABLE)
.build(), .build()
) )
.withGetResolver(LibraryMangaGetResolver.INSTANCE) .withGetResolver(LibraryMangaGetResolver.INSTANCE)
.prepare() .prepare()
fun getDuplicateLibraryManga(manga: Manga) = db.get() fun getFavoriteMangas() = db.get()
.`object`(Manga::class.java) .listOfObjects(Manga::class.java)
.withQuery( .withQuery(
Query.builder() Query.builder()
.table(MangaTable.TABLE) .table(MangaTable.TABLE)
.where("${MangaTable.COL_FAVORITE} = 1 AND LOWER(${MangaTable.COL_TITLE}) = ? AND ${MangaTable.COL_SOURCE} != ?") .where("${MangaTable.COL_FAVORITE} = ?")
.whereArgs( .whereArgs(1)
manga.title.lowercase(), .orderBy(MangaTable.COL_TITLE)
manga.source, .build()
)
.limit(1)
.build(),
) )
.prepare() .prepare()
fun getFavoriteMangas(sortByTitle: Boolean = true): PreparedGetListOfObjects<Manga> {
var queryBuilder = Query.builder()
.table(MangaTable.TABLE)
.where("${MangaTable.COL_FAVORITE} = ?")
.whereArgs(1)
if (sortByTitle) {
queryBuilder = queryBuilder.orderBy(MangaTable.COL_TITLE)
}
return db.get()
.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)
.withQuery( .withQuery(
@@ -78,7 +64,7 @@ interface MangaQueries : DbProvider {
.table(MangaTable.TABLE) .table(MangaTable.TABLE)
.where("${MangaTable.COL_URL} = ? AND ${MangaTable.COL_SOURCE} = ?") .where("${MangaTable.COL_URL} = ? AND ${MangaTable.COL_SOURCE} = ?")
.whereArgs(url, sourceId) .whereArgs(url, sourceId)
.build(), .build()
) )
.prepare() .prepare()
@@ -89,37 +75,17 @@ interface MangaQueries : DbProvider {
.table(MangaTable.TABLE) .table(MangaTable.TABLE)
.where("${MangaTable.COL_ID} = ?") .where("${MangaTable.COL_ID} = ?")
.whereArgs(id) .whereArgs(id)
.build(), .build()
) )
.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(
RawQuery.builder() RawQuery.builder()
.query(getReadMangaNotInLibraryQuery()) .query(getReadMangaNotInLibraryQuery())
.build(), .build()
) )
.prepare() .prepare()
@@ -148,24 +114,14 @@ interface MangaQueries : DbProvider {
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()
@@ -178,6 +134,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())
@@ -188,51 +149,42 @@ 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( .where(
""" """
${MangaTable.COL_FAVORITE} = ? AND ${MangaTable.COL_SOURCE} IN (${Queries.placeholders(sourceIds.size)}) AND ${MangaTable.COL_ID} NOT IN ( ${MangaTable.COL_FAVORITE} = ? AND ${MangaTable.COL_ID} NOT IN (
SELECT ${MergedTable.COL_MANGA_ID} FROM ${MergedTable.TABLE} WHERE ${MergedTable.COL_MANGA_ID} != ${MergedTable.COL_MERGE_ID} SELECT ${MergedTable.COL_MANGA_ID} FROM ${MergedTable.TABLE} WHERE ${MergedTable.COL_MANGA_ID} != ${MergedTable.COL_MERGE_ID}
) )
""".trimIndent(), """.trimIndent()
) )
// SY <-- .whereArgs(0)
.whereArgs(0, *sourceIds.toTypedArray()) .build()
.build(),
) )
.prepare() .prepare()
// SY --> // SY -->
fun deleteMangasNotInLibraryAndNotReadBySourceIds(sourceIds: List<Long>) = db.delete() fun deleteMangasNotInLibraryAndNotRead() = db.delete()
.byQuery( .byQuery(
DeleteQuery.builder() DeleteQuery.builder()
.table(MangaTable.TABLE) .table(MangaTable.TABLE)
.where( .where(
""" """
${MangaTable.COL_FAVORITE} = ? AND ${MangaTable.COL_SOURCE} IN (${Queries.placeholders(sourceIds.size)}) AND ${MangaTable.COL_ID} NOT IN ( ${MangaTable.COL_FAVORITE} = ? AND ${MangaTable.COL_ID} NOT IN (
SELECT ${MergedTable.COL_MANGA_ID} FROM ${MergedTable.TABLE} WHERE ${MergedTable.COL_MANGA_ID} != ${MergedTable.COL_MERGE_ID} SELECT ${MergedTable.COL_MANGA_ID} FROM ${MergedTable.TABLE} WHERE ${MergedTable.COL_MANGA_ID} != ${MergedTable.COL_MERGE_ID}
) AND ${MangaTable.COL_ID} NOT IN ( ) 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 SELECT ${ChapterTable.COL_MANGA_ID} FROM ${ChapterTable.TABLE} WHERE ${ChapterTable.COL_READ} = 1 OR ${ChapterTable.COL_LAST_PAGE_READ} != 0
) )
""".trimIndent(), """.trimIndent()
) )
.whereArgs(0, *sourceIds.toTypedArray()) .whereArgs(0)
.build(), .build()
) )
.prepare() .prepare()
// SY <-- // SY <--
@@ -241,7 +193,7 @@ interface MangaQueries : DbProvider {
.byQuery( .byQuery(
DeleteQuery.builder() DeleteQuery.builder()
.table(MangaTable.TABLE) .table(MangaTable.TABLE)
.build(), .build()
) )
.prepare() .prepare()
@@ -251,7 +203,7 @@ interface MangaQueries : DbProvider {
RawQuery.builder() RawQuery.builder()
.query(getLastReadMangaQuery()) .query(getLastReadMangaQuery())
.observesTables(MangaTable.TABLE) .observesTables(MangaTable.TABLE)
.build(), .build()
) )
.prepare() .prepare()
@@ -261,7 +213,7 @@ interface MangaQueries : DbProvider {
RawQuery.builder() RawQuery.builder()
.query(getTotalChapterMangaQuery()) .query(getTotalChapterMangaQuery())
.observesTables(MangaTable.TABLE) .observesTables(MangaTable.TABLE)
.build(), .build()
) )
.prepare() .prepare()
@@ -271,7 +223,7 @@ interface MangaQueries : DbProvider {
RawQuery.builder() RawQuery.builder()
.query(getLatestChapterMangaQuery()) .query(getLatestChapterMangaQuery())
.observesTables(MangaTable.TABLE) .observesTables(MangaTable.TABLE)
.build(), .build()
) )
.prepare() .prepare()
@@ -281,7 +233,7 @@ interface MangaQueries : DbProvider {
RawQuery.builder() RawQuery.builder()
.query(getChapterFetchDateMangaQuery()) .query(getChapterFetchDateMangaQuery())
.observesTables(MangaTable.TABLE) .observesTables(MangaTable.TABLE)
.build(), .build()
) )
.prepare() .prepare()
@@ -296,9 +248,9 @@ interface MangaQueries : DbProvider {
INNER JOIN ${SearchMetadataTable.TABLE} INNER JOIN ${SearchMetadataTable.TABLE}
ON ${MangaTable.TABLE}.${MangaTable.COL_ID} = ${SearchMetadataTable.TABLE}.${SearchMetadataTable.COL_MANGA_ID} ON ${MangaTable.TABLE}.${MangaTable.COL_ID} = ${SearchMetadataTable.TABLE}.${SearchMetadataTable.COL_MANGA_ID}
ORDER BY ${MangaTable.TABLE}.${MangaTable.COL_ID} ORDER BY ${MangaTable.TABLE}.${MangaTable.COL_ID}
""".trimIndent(), """.trimIndent()
) )
.build(), .build()
) )
.prepare() .prepare()
@@ -313,9 +265,9 @@ interface MangaQueries : DbProvider {
ON ${MangaTable.TABLE}.${MangaTable.COL_ID} = ${SearchMetadataTable.TABLE}.${SearchMetadataTable.COL_MANGA_ID} ON ${MangaTable.TABLE}.${MangaTable.COL_ID} = ${SearchMetadataTable.TABLE}.${SearchMetadataTable.COL_MANGA_ID}
WHERE ${MangaTable.TABLE}.${MangaTable.COL_FAVORITE} = 1 WHERE ${MangaTable.TABLE}.${MangaTable.COL_FAVORITE} = 1
ORDER BY ${MangaTable.TABLE}.${MangaTable.COL_ID} ORDER BY ${MangaTable.TABLE}.${MangaTable.COL_ID}
""".trimIndent(), """.trimIndent()
) )
.build(), .build()
) )
.prepare() .prepare()
@@ -330,9 +282,9 @@ interface MangaQueries : DbProvider {
ON ${MangaTable.TABLE}.${MangaTable.COL_ID} = ${SearchMetadataTable.TABLE}.${SearchMetadataTable.COL_MANGA_ID} ON ${MangaTable.TABLE}.${MangaTable.COL_ID} = ${SearchMetadataTable.TABLE}.${SearchMetadataTable.COL_MANGA_ID}
WHERE ${MangaTable.TABLE}.${MangaTable.COL_FAVORITE} = 1 WHERE ${MangaTable.TABLE}.${MangaTable.COL_FAVORITE} = 1
ORDER BY ${MangaTable.TABLE}.${MangaTable.COL_ID} ORDER BY ${MangaTable.TABLE}.${MangaTable.COL_ID}
""".trimIndent(), """.trimIndent()
) )
.build(), .build()
) )
.prepare() .prepare()
// SY <-- // SY <--
@@ -1,8 +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.savedsearches.tables.FeedSavedSearchTable
import exh.savedsearches.tables.SavedSearchTable
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
@@ -77,49 +74,23 @@ fun getReadMangaNotInLibraryQuery() =
""" """
/** /**
* Query to get the global feed saved searches * Query to get the manga from the library, with their categories and unread count.
*/
fun getGlobalFeedSavedSearchQuery() =
"""
SELECT ${SavedSearchTable.TABLE}.*
FROM (
SELECT ${FeedSavedSearchTable.COL_SAVED_SEARCH_ID} FROM ${FeedSavedSearchTable.TABLE} WHERE ${FeedSavedSearchTable.COL_GLOBAL} = 1
) AS M
JOIN ${SavedSearchTable.TABLE}
ON ${SavedSearchTable.TABLE}.${SavedSearchTable.COL_ID} = M.${FeedSavedSearchTable.COL_SAVED_SEARCH_ID}
"""
/**
* Query to get the source feed saved searches
*/
fun getSourceFeedSavedSearchQuery() =
"""
SELECT ${SavedSearchTable.TABLE}.*
FROM (
SELECT ${FeedSavedSearchTable.COL_SAVED_SEARCH_ID} FROM ${FeedSavedSearchTable.TABLE} WHERE ${FeedSavedSearchTable.COL_GLOBAL} = 0 AND ${FeedSavedSearchTable.COL_SOURCE} = ?
) AS M
JOIN ${SavedSearchTable.TABLE}
ON ${SavedSearchTable.TABLE}.${SavedSearchTable.COL_ID} = M.${FeedSavedSearchTable.COL_SAVED_SEARCH_ID}
"""
/**
* Query to get the manga from the library, with their categories, read and unread count.
*/ */
val libraryQuery = val libraryQuery =
""" """
SELECT M.*, COALESCE(MC.${MangaCategory.COL_CATEGORY_ID}, 0) AS ${Manga.COL_CATEGORY} SELECT M.*, COALESCE(MC.${MangaCategory.COL_CATEGORY_ID}, 0) AS ${Manga.COL_CATEGORY}
FROM ( FROM (
SELECT ${Manga.TABLE}.*, COALESCE(C.unreadCount, 0) AS ${Manga.COMPUTED_COL_UNREAD_COUNT}, COALESCE(R.readCount, 0) AS ${Manga.COMPUTED_COL_READ_COUNT} SELECT ${Manga.TABLE}.*, COALESCE(C.unread, 0) AS ${Manga.COL_UNREAD}, COALESCE(R.read, 0) AS ${Manga.COL_READ}
FROM ${Manga.TABLE} FROM ${Manga.TABLE}
LEFT JOIN ( LEFT JOIN (
SELECT ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}, COUNT(*) AS unreadCount SELECT ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}, COUNT(*) AS unread
FROM ${Chapter.TABLE} FROM ${Chapter.TABLE}
WHERE ${Chapter.COL_READ} = 0 WHERE ${Chapter.COL_READ} = 0
GROUP BY ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} GROUP BY ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}
) AS C ) AS C
ON ${Manga.TABLE}.${Manga.COL_ID} = C.${Chapter.COL_MANGA_ID} ON ${Manga.TABLE}.${Manga.COL_ID} = C.${Chapter.COL_MANGA_ID}
LEFT JOIN ( LEFT JOIN (
SELECT ${Chapter.COL_MANGA_ID}, COUNT(*) AS readCount SELECT ${Chapter.COL_MANGA_ID}, COUNT(*) AS read
FROM ${Chapter.TABLE} FROM ${Chapter.TABLE}
WHERE ${Chapter.COL_READ} = 1 WHERE ${Chapter.COL_READ} = 1
GROUP BY ${Chapter.COL_MANGA_ID} GROUP BY ${Chapter.COL_MANGA_ID}
@@ -128,10 +99,10 @@ val libraryQuery =
WHERE ${Manga.COL_FAVORITE} = 1 AND ${Manga.COL_SOURCE} <> $MERGED_SOURCE_ID WHERE ${Manga.COL_FAVORITE} = 1 AND ${Manga.COL_SOURCE} <> $MERGED_SOURCE_ID
GROUP BY ${Manga.TABLE}.${Manga.COL_ID} GROUP BY ${Manga.TABLE}.${Manga.COL_ID}
UNION UNION
SELECT ${Manga.TABLE}.*, COALESCE(C.unreadCount, 0) AS ${Manga.COMPUTED_COL_UNREAD_COUNT}, COALESCE(R.readCount, 0) AS ${Manga.COMPUTED_COL_READ_COUNT} SELECT ${Manga.TABLE}.*, COALESCE(C.unread, 0) AS ${Manga.COL_UNREAD}, COALESCE(R.read, 0) AS ${Manga.COL_READ}
FROM ${Manga.TABLE} FROM ${Manga.TABLE}
LEFT JOIN ( LEFT JOIN (
SELECT ${Merged.TABLE}.${Merged.COL_MERGE_ID}, COUNT(*) as unreadCount SELECT ${Merged.TABLE}.${Merged.COL_MERGE_ID}, COUNT(*) as unread
FROM ${Merged.TABLE} FROM ${Merged.TABLE}
JOIN ${Chapter.TABLE} JOIN ${Chapter.TABLE}
ON ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} = ${Merged.TABLE}.${Merged.COL_MANGA_ID} ON ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} = ${Merged.TABLE}.${Merged.COL_MANGA_ID}
@@ -140,7 +111,7 @@ val libraryQuery =
) AS C ) AS C
ON ${Manga.TABLE}.${Manga.COL_ID} = C.${Merged.COL_MERGE_ID} ON ${Manga.TABLE}.${Manga.COL_ID} = C.${Merged.COL_MERGE_ID}
LEFT JOIN ( LEFT JOIN (
SELECT ${Merged.TABLE}.${Merged.COL_MERGE_ID}, COUNT(*) as readCount SELECT ${Merged.TABLE}.${Merged.COL_MERGE_ID}, COUNT(*) as read
FROM ${Merged.TABLE} FROM ${Merged.TABLE}
JOIN ${Chapter.TABLE} JOIN ${Chapter.TABLE}
ON ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} = ${Merged.TABLE}.${Merged.COL_MANGA_ID} ON ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} = ${Merged.TABLE}.${Merged.COL_MANGA_ID}
@@ -270,14 +241,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}
"""
@@ -15,7 +15,7 @@ interface TrackQueries : DbProvider {
.withQuery( .withQuery(
Query.builder() Query.builder()
.table(TrackTable.TABLE) .table(TrackTable.TABLE)
.build(), .build()
) )
.prepare() .prepare()
@@ -26,7 +26,7 @@ interface TrackQueries : DbProvider {
.table(TrackTable.TABLE) .table(TrackTable.TABLE)
.where("${TrackTable.COL_MANGA_ID} = ?") .where("${TrackTable.COL_MANGA_ID} = ?")
.whereArgs(manga.id) .whereArgs(manga.id)
.build(), .build()
) )
.prepare() .prepare()
@@ -40,7 +40,7 @@ interface TrackQueries : DbProvider {
.table(TrackTable.TABLE) .table(TrackTable.TABLE)
.where("${TrackTable.COL_MANGA_ID} = ? AND ${TrackTable.COL_SYNC_ID} = ?") .where("${TrackTable.COL_MANGA_ID} = ? AND ${TrackTable.COL_SYNC_ID} = ?")
.whereArgs(manga.id, sync.id) .whereArgs(manga.id, sync.id)
.build(), .build()
) )
.prepare() .prepare()
} }
@@ -29,6 +29,6 @@ class ChapterBackupPutResolver : PutResolver<Chapter>() {
contentValuesOf( contentValuesOf(
ChapterTable.COL_READ to chapter.read, ChapterTable.COL_READ to chapter.read,
ChapterTable.COL_BOOKMARK to chapter.bookmark, ChapterTable.COL_BOOKMARK to chapter.bookmark,
ChapterTable.COL_LAST_PAGE_READ to chapter.last_page_read, ChapterTable.COL_LAST_PAGE_READ to chapter.last_page_read
) )
} }
@@ -29,6 +29,6 @@ class ChapterKnownBackupPutResolver : PutResolver<Chapter>() {
contentValuesOf( contentValuesOf(
ChapterTable.COL_READ to chapter.read, ChapterTable.COL_READ to chapter.read,
ChapterTable.COL_BOOKMARK to chapter.bookmark, ChapterTable.COL_BOOKMARK to chapter.bookmark,
ChapterTable.COL_LAST_PAGE_READ to chapter.last_page_read, ChapterTable.COL_LAST_PAGE_READ to chapter.last_page_read
) )
} }

Some files were not shown because too many files have changed in this diff Show More