Compare commits

..

1 Commits

Author SHA1 Message Date
Jobobby04 0413d502c1 Revert Jdk 11 update 2021-02-12 20:05:46 -05:00
1993 changed files with 105101 additions and 169090 deletions
-7
View File
@@ -1,7 +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
ij_kotlin_name_count_to_use_star_import = 2147483647
ij_kotlin_name_count_to_use_star_import_for_members = 2147483647
+1
View File
@@ -1 +1,2 @@
github: inorichi
ko_fi: inorichi
+2 -10
View File
@@ -2,15 +2,9 @@
I acknowledge that:
- I have updated:
- To the latest version of the app (stable is v1.9.0)
- All extensions
- I have tried the troubleshooting guide: https://tachiyomi.org/help/guides/troubleshooting-problems/
- I have updated to the latest version of the app (stable is v1.5.0)
- I have updated all extensions
- If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions
- I have searched the existing issues and this is new ticket **NOT** a duplicate or related to another open or closed 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**
@@ -30,5 +24,3 @@ Note that the issue will be automatically closed if you do not fill out the titl
## Other details
Additional details and attachments.
If you're experiencing crashes, share the crash logs from More → Settings → Advanced → Dump crash logs.
+36
View File
@@ -0,0 +1,36 @@
---
name: "🐞 Bug report"
about: Report a bug
title: "[Bug] <Write short description here>"
labels: "bug"
---
**PLEASE READ THIS**
I acknowledge that:
- I have updated to the latest version of the app (stable is v1.5.0)
- I have updated all extensions
- If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions
**DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT**
---
## Device information
* Tachiyomi version: ?
* Android version: ?
* Device: ?
## Steps to reproduce
1. First step
2. Second step
### Expected behavior
This should happen.
### Actual behavior
This happened instead.
## Other details
Additional details and attachments.
+5 -8
View File
@@ -1,11 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: ⚠️ Extension/source issue
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
- name: Tachiyomi help website
url: https://tachiyomi.org/help/
about: Guides, troubleshooting, and answers to common questions
about: Common questions are answered here.
- name: Tachiyomi extensions GitHub repository
url: https://github.com/tachiyomiorg/tachiyomi-extensions
about: Issues about an extension/source/catalogue should be opened here instead.
+24
View File
@@ -0,0 +1,24 @@
---
name: "🌟 Feature request"
about: Suggest a feature to improve Tachiyomi
title: "[Feature Request] <Write short description here>"
labels: "feature"
---
**PLEASE READ THIS**
I acknowledge that:
- I have updated to the latest version of the app (stable is v1.5.0)
- I have updated all extensions
- If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions
**DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT**
---
## Why/User Benefit/User Problem
(explain why this feature should be added)
## What/Requirements
(explain how this feature would behave)
-108
View File
@@ -1,108 +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 plain 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.9.0"
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: This is a TachiyomiSY specific issue that does not happen in [Tachiyomi Preview](https://github.com/tachiyomiorg/tachiyomi/).
required: true
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open or closed 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.9.0](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 or closed 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.9.0](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

@@ -0,0 +1,6 @@
org.gradle.daemon=false
org.gradle.jvmargs=-Xmx5120m
org.gradle.workers.max=2
kotlin.incremental=false
kotlin.compiler.execution.strategy=in-process
@@ -6,20 +6,29 @@ on:
- 'master'
jobs:
trigger_preview_build:
name: Trigger preview build
if: ${{ github.ref == 'refs/heads/master' }}
check_wrapper:
name: Validate Gradle Wrapper
runs-on: ubuntu-latest
steps:
- name: Clone repo
uses: actions/checkout@v3
uses: actions/checkout@v2
- name: Validate Gradle Wrapper
uses: gradle/wrapper-validation-action@v1
preview:
name: Build app preview
needs: check_wrapper
if: "!startsWith(github.event.head_commit.message, '[SKIP CI]')"
runs-on: ubuntu-latest
steps:
- name: Clone repo
uses: actions/checkout@v2
- name: TAG - Bump version and push tag
uses: anothrNick/github-tag-action@1.39.0
uses: anothrNick/github-tag-action@1.17.2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
WITH_V: true
@@ -0,0 +1,91 @@
name: Release Builder
on:
push:
branches:
- 'release'
jobs:
check_wrapper:
name: Validate Gradle Wrapper
runs-on: ubuntu-latest
steps:
- name: Clone repo
uses: actions/checkout@v2
- name: Validate Gradle Wrapper
uses: gradle/wrapper-validation-action@v1
build:
name: Build app
needs: check_wrapper
if: "!startsWith(github.event.head_commit.message, '[SKIP CI]')"
runs-on: ubuntu-latest
steps:
- name: Cancel previous runs
uses: styfle/cancel-workflow-action@0.5.0
with:
access_token: ${{ github.token }}
- name: Clone repo
uses: actions/checkout@v2
- name: Set up JDK 1.8
uses: actions/setup-java@v1
with:
java-version: 1.8
- name: Copy CI gradle.properties
run: |
mkdir -p ~/.gradle
cp .github/runner-files/ci-gradle.properties ~/.gradle/gradle.properties
- name: Write google-services.json
uses: DamianReeves/write-file-action@v1.0
with:
# The path to the file to write
path: app/google-services.json
# The contents of the file
contents: ${{ secrets.GOOGLE_SERVICES_TEXT }}
# The mode of writing to use: `overwrite`, `append`, or `preserve`.
write-mode: overwrite # optional, default is preserve
- name: Build app
uses: eskatos/gradle-command-action@v1
with:
arguments: assembleRelease --stacktrace
wrapper-cache-enabled: true
dependencies-cache-enabled: true
configuration-cache-enabled: true
- name: Sign APK
uses: r0adkll/sign-android-release@v1
with:
releaseDirectory: app/build/outputs/apk/standard/release
signingKeyBase64: ${{ secrets.SIGNING_KEY }}
alias: ${{ secrets.ALIAS }}
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
keyPassword: ${{ secrets.KEY_PASSWORD }}
- name: Create release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.run_number }}
release_name: TachiyomiSY
draft: true
prerelease: false
- name: Upload APK to release
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ${{ env.SIGNED_RELEASE_FILE }}
asset_name: TachiyomiSY.apk
asset_content_type: application/vnd.android.package-archive
+25 -17
View File
@@ -1,10 +1,6 @@
name: CI
on: [pull_request]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
check_wrapper:
name: Validate Gradle Wrapper
@@ -12,7 +8,7 @@ jobs:
steps:
- name: Clone repo
uses: actions/checkout@v3
uses: actions/checkout@v2
- name: Validate Gradle Wrapper
uses: gradle/wrapper-validation-action@v1
@@ -20,25 +16,37 @@ jobs:
build:
name: Build app
needs: check_wrapper
if: "!startsWith(github.event.head_commit.message, '[SKIP CI]')"
runs-on: ubuntu-latest
steps:
- name: Clone repo
uses: actions/checkout@v3
- name: Set up JDK 11
uses: actions/setup-java@v3
- name: Cancel previous runs
uses: styfle/cancel-workflow-action@0.5.0
with:
java-version: 11
distribution: adopt
access_token: ${{ github.token }}
- name: Clone repo
uses: actions/checkout@v2
- name: Set up JDK 1.8
uses: actions/setup-java@v1
with:
java-version: 1.8
- name: Copy CI gradle.properties
run: |
mkdir -p ~/.gradle
cp .github/runner-files/ci-gradle.properties ~/.gradle/gradle.properties
- name: Build app
uses: gradle/gradle-command-action@v2
uses: eskatos/gradle-command-action@v1
with:
arguments: assembleDevDebug
arguments: assembleStandardDebug
wrapper-cache-enabled: true
dependencies-cache-enabled: true
configuration-cache-enabled: true
- name: Upload APK
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v2
with:
name: TachiyomiSY-${{ github.sha }}.apk
path: app/build/outputs/apk/dev/debug/app-dev-debug.apk
path: app/build/outputs/apk/dev/debug/app-dev-debug.apk
-102
View File
@@ -1,102 +0,0 @@
name: Release Builder
on:
push:
branches:
- 'release'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
name: Build release app
runs-on: ubuntu-latest
steps:
- name: Clone repo
uses: actions/checkout@v3
- name: Validate Gradle Wrapper
uses: gradle/wrapper-validation-action@v1
- name: Set up JDK 11
uses: actions/setup-java@v3
with:
java-version: 11
distribution: adopt
# SY <--
- name: Write google-services.json
uses: DamianReeves/write-file-action@v1.2
with:
path: app/google-services.json
contents: ${{ secrets.GOOGLE_SERVICES_TEXT }}
write-mode: overwrite
# SY -->
- name: Build app and run unit tests
uses: gradle/gradle-command-action@v2
with:
arguments: assembleStandardRelease testStandardReleaseUnitTest --stacktrace
- name: Sign APK
uses: r0adkll/sign-android-release@v1
with:
releaseDirectory: app/build/outputs/apk/standard/release
signingKeyBase64: ${{ secrets.SIGNING_KEY }}
alias: ${{ secrets.ALIAS }}
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
keyPassword: ${{ secrets.KEY_PASSWORD }}
- name: Clean up build artifacts
run: |
set -e
mv app/build/outputs/apk/standard/release/app-standard-universal-release-unsigned-signed.apk TachiyomiSY.apk
sha=`sha256sum TachiyomiSY.apk | awk '{ print $1 }'`
echo "APK_UNIVERSAL_SHA=$sha" >> $GITHUB_ENV
cp app/build/outputs/apk/standard/release/app-standard-arm64-v8a-release-unsigned-signed.apk TachiyomiSY-arm64-v8a.apk
sha=`sha256sum TachiyomiSY-arm64-v8a.apk | awk '{ print $1 }'`
echo "APK_ARM64_V8A_SHA=$sha" >> $GITHUB_ENV
cp app/build/outputs/apk/standard/release/app-standard-armeabi-v7a-release-unsigned-signed.apk TachiyomiSY-armeabi-v7a.apk
sha=`sha256sum TachiyomiSY-armeabi-v7a.apk | awk '{ print $1 }'`
echo "APK_ARMEABI_V7A_SHA=$sha" >> $GITHUB_ENV
cp app/build/outputs/apk/standard/release/app-standard-x86-release-unsigned-signed.apk TachiyomiSY-x86.apk
sha=`sha256sum TachiyomiSY-x86.apk | awk '{ print $1 }'`
echo "APK_X86_SHA=$sha" >> $GITHUB_ENV
cp app/build/outputs/apk/standard/release/app-standard-x86_64-release-unsigned-signed.apk TachiyomiSY-x86_64.apk
sha=`sha256sum TachiyomiSY-x86_64.apk | awk '{ print $1 }'`
echo "APK_X86_64_SHA=$sha" >> $GITHUB_ENV
- name: Create release
uses: softprops/action-gh-release@v1
with:
tag_name: ${{ github.run_number }}
name: TachiyomiSY
body: |
---
### Checksums
| Variant | SHA-256 |
| ------- | ------- |
| Universal | ${{ env.APK_UNIVERSAL_SHA }} |
| arm64-v8a | ${{ env.APK_ARM64_V8A_SHA }} |
| armeabi-v7a | ${{ env.APK_ARMEABI_V7A_SHA }} |
| x86 | ${{ env.APK_X86_SHA }} |
| x86_64 | ${{ env.APK_X86_64_SHA }} |
files: |
TachiyomiSY.apk
TachiyomiSY-arm64-v8a.apk
TachiyomiSY-armeabi-v7a.apk
TachiyomiSY-x86.apk
TachiyomiSY-x86_64.apk
draft: true
prerelease: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+37
View File
@@ -0,0 +1,37 @@
name: Issue closer
on:
issues:
types: [opened, edited, reopened]
jobs:
autoclose:
runs-on: ubuntu-latest
steps:
- name: Autoclose when created in wrong repo
uses: arkon/issue-closer-action@v1.1
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
type: title
regex: ".*THIS ISSUE IS IN THE WRONG REPO.*"
message: "@${issue.user.login} this issue was automatically closed because it was not opened in the correct repo, as the template mentioned."
- name: Autoclose when no short description provided
uses: arkon/issue-closer-action@v1.1
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
type: title
regex: ".*<Write short description here>*"
message: "@${issue.user.login} this issue was automatically closed because you did not fill out the description in the title."
- name: Autoclose when body acknowledgement section not removed
uses: arkon/issue-closer-action@v1.1
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
type: body
regex: ".*DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT.*"
message: "@${issue.user.login} this issue was automatically closed because the acknowledgment section was not removed."
- name: Autoclose when body requested information not filled out
uses: arkon/issue-closer-action@v1.1
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
type: body
regex: ".*\\* (Tachiyomi version|Android version|Device): \\?.*"
message: "@${issue.user.login} this issue was automatically closed because the requested information was not filled out."
-35
View File
@@ -1,35 +0,0 @@
name: Issue moderator
on:
issues:
types: [opened, edited, reopened]
issue_comment:
types: [created]
jobs:
moderate:
runs-on: ubuntu-latest
steps:
- name: Moderate issues
uses: tachiyomiorg/issue-moderator-action@v1
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
auto-close-rules: |
[
{
"type": "body",
"regex": ".*DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT.*",
"message": "The acknowledgment section was not removed."
},
{
"type": "body",
"regex": ".*\\* (Tachiyomi version|Android version|Device): \\?.*",
"message": "Requested information in the template was not filled out."
},
{
"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"
}
]
-19
View File
@@ -1,19 +0,0 @@
name: Lock threads
on:
# Daily
schedule:
- cron: '0 0 * * *'
# Manual trigger
workflow_dispatch:
inputs:
jobs:
lock:
runs-on: ubuntu-latest
steps:
- uses: dessant/lock-threads@v4
with:
github-token: ${{ github.token }}
issue-inactive-days: '2'
pr-inactive-days: '2'
+2 -2
View File
@@ -14,8 +14,8 @@
*.apk
app/**/output.json
# Unnecessary file
*.swp
# Hebrew assets are copied on build
app/src/main/res/values-iw/
TODO.md
CHANGELOG.md
-126
View File
@@ -1,126 +0,0 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, caste, color, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community moderators are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community moderators have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community moderators responsible for enforcement at
the [Tachiyomi Discord server](https://discord.gg/tachiyomi).
All complaints will be reviewed and investigated promptly and fairly.
All community moderators are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community moderators will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community moderators, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org/),
version 2.1, available at
[v2.1](https://www.contributor-covenant.org/version/2/1/code_of_conduct.html).
Community Impact Guidelines were inspired by
[Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).
For answers to common questions about this code of conduct, see the FAQ at
[FAQ](https://www.contributor-covenant.org/faq). Translations are available
at [translations](https://www.contributor-covenant.org/translations).
+1 -17
View File
@@ -10,23 +10,7 @@ Thanks for your interest in contributing to Tachiyomi!
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.
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
@@ -42,7 +26,7 @@ When creating a fork, remember to:
- To avoid confusion with the main app:
- Change the app name
- 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:
- 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:
+14 -15
View File
@@ -1,20 +1,20 @@
| Preview Builds | Release Builds | Tachiyomi Support Server |
|-------|----------|----------|
| [![Preview](https://github.com/jobobby04/TachiyomiSYPreview/workflows/Remote%20Dispatch%20Build%20App/badge.svg)](https://github.com/jobobby04/TachiyomiSYPreview/releases) | [![stable release](https://img.shields.io/github/release/jobobby04/tachiyomisy.svg?maxAge=3600&label=download)](https://github.com/jobobby04/tachiyomisy/releases/latest) | [![Discord](https://img.shields.io/discord/349436576037732353.svg?label=discord&labelColor=7289da&color=2c2f33&style=flat)](https://discord.gg/tachiyomi) |
| [![Preview](https://github.com/jobobby04/TachiyomiSYPreview/workflows/Remote%20Dispatch%20Build%20App/badge.svg)](https://github.com/jobobby04/TachiyomiSYPreview/releases) | [![stable release](https://img.shields.io/github/release/jobobby04/tachiyomisy.svg?maxAge=3600&label=download)](https://github.com/jobobby04/tachiyomisy/releases/latest) | [![Discord](https://img.shields.io/discord/349436576037732353.svg)](https://discord.gg/tachiyomi) |
# ![app icon](./.github/readme-images/app-icon.png)TachiyomiSY
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)
## Features
Features of Tachiyomi(original) include:
* Online reading from a variety of sources
* Local reading of downloaded content
* Online reading from sources such as MangaDex, MangaSee, Mangakakalot, [and more](https://github.com/tachiyomiorg/tachiyomi-extensions)
* Local reading of downloaded manga
* 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/), [MangaUpdates](https://mangaupdates.com), [Shikimori](https://shikimori.one), and [Bangumi](https://bgm.tv/) support
* [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
* Light and dark themes
* Schedule updating your library for new chapters
@@ -58,8 +58,11 @@ Custom sources:
Additional features for some extensions, features include custom description, opening in app, batch add to library, and a bunch of other things based on the source:
* 8Muses (EroMuse)
* HBrowse
* HentaiCafe (inside Foolside)
* Hitomi.la
* Mangadex
* NHentai
* PervEden (EN and IT)
* Puruin
* Tsumino
@@ -81,12 +84,13 @@ Please make sure to read the full guidelines. Your issue may be closed without w
<details><summary>Bugs</summary>
* Include version (More About Version)
* 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
* Include version (More > About > Version)
* 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
* Include steps to reproduce (if not obvious from description)
* Include screenshot (if needed)
* 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
DO: https://github.com/tachiyomiorg/tachiyomi/issues/24 https://github.com/tachiyomiorg/tachiyomi/issues/71
@@ -105,15 +109,10 @@ Source requests should be created at https://github.com/tachiyomiorg/tachiyomi-e
<details><summary>Contributing</summary>
See [CONTRIBUTING.md](./CONTRIBUTING.md).
</details>
<details><summary>Code of Conduct</summary>
See [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md).
See [CONTRIBUTING.md](https://github.com/tachiyomiorg/tachiyomi/blob/master/CONTRIBUTING.md).
</details>
## FAQ
[See our website.](https://tachiyomi.org/)
You can also reach out to us on [Discord](https://discord.gg/tachiyomi).
You can also reach out to us on [Discord](https://discord.gg/tachiyomi).
+234 -231
View File
@@ -1,93 +1,79 @@
import org.gradle.api.tasks.testing.logging.TestLogEvent
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import org.jmailen.gradle.kotlinter.tasks.LintTask
import java.io.ByteArrayOutputStream
import java.text.SimpleDateFormat
import java.util.Date
import java.util.TimeZone
plugins {
id("com.android.application")
id("com.mikepenz.aboutlibraries.plugin")
kotlin("android")
kotlin("kapt")
kotlin("plugin.parcelize")
kotlin("plugin.serialization")
id("com.github.zellius.shortcut-helper")
id("com.squareup.sqldelight")
// Realm (EH)
id("realm-android")
}
if (gradle.startParameter.taskRequests.toString().contains("Standard")) {
apply<com.google.gms.googleservices.GoogleServicesPlugin>()
if (!gradle.startParameter.taskRequests.toString().contains("Debug")) {
apply(plugin = "com.google.gms.google-services")
// Firebase Crashlytics
apply(plugin = "com.google.firebase.crashlytics")
}
shortcutHelper.setFilePath("./shortcuts.xml")
val SUPPORTED_ABIS = setOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
android {
namespace = "eu.kanade.tachiyomi"
compileSdk = AndroidConfig.compileSdk
compileSdkVersion(AndroidConfig.compileSdk)
buildToolsVersion(AndroidConfig.buildTools)
ndkVersion = AndroidConfig.ndk
defaultConfig {
applicationId = "eu.kanade.tachiyomi.sy"
minSdk = AndroidConfig.minSdk
targetSdk = AndroidConfig.targetSdk
versionCode = 46
versionName = "1.9.0"
minSdkVersion(AndroidConfig.minSdk)
targetSdkVersion(AndroidConfig.targetSdk)
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
versionCode = 13
versionName = "1.5.0"
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
buildConfigField("String", "BUILD_TIME", "\"${getBuildTime()}\"")
buildConfigField("boolean", "INCLUDE_UPDATER", "false")
multiDexEnabled = true
ndk {
abiFilters += SUPPORTED_ABIS
abiFilters += setOf("armeabi-v7a", "arm64-v8a", "x86")
}
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
splits {
abi {
isEnable = true
reset()
include(*SUPPORTED_ABIS.toTypedArray())
isUniversalApk = true
}
buildFeatures {
viewBinding = true
}
buildTypes {
named("debug") {
versionNameSuffix = "-${getCommitCount()}"
applicationIdSuffix = ".debug"
isPseudoLocalesEnabled = true
}
create("releaseTest") {
applicationIdSuffix = ".rt"
//isMinifyEnabled = true
//isShrinkResources = true
setProguardFiles(listOf(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"))
matchingFallbacks.add("release")
isZipAlignEnabled = true
setProguardFiles(listOf(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"))
}
named("release") {
isMinifyEnabled = true
isShrinkResources = true
setProguardFiles(listOf(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"))
}
create("benchmark") {
initWith(getByName("release"))
signingConfig = signingConfigs.getByName("debug")
matchingFallbacks.add("release")
isDebuggable = false
versionNameSuffix = "-benchmark"
applicationIdSuffix = ".benchmark"
isZipAlignEnabled = true
setProguardFiles(listOf(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"))
}
}
sourceSets {
getByName("benchmark").res.srcDirs("src/debug/res")
}
flavorDimensions.add("default")
flavorDimensions("default")
productFlavors {
create("standard") {
@@ -98,275 +84,292 @@ android {
dimension = "default"
}
create("dev") {
// Include pseudolocales: https://developer.android.com/guide/topics/resources/pseudolocales
resourceConfigurations.addAll(listOf("en", "en_XA", "ar_XB", "xxhdpi"))
resConfigs("en", "xxhdpi")
dimension = "default"
}
}
packagingOptions {
resources.excludes.addAll(listOf(
"META-INF/DEPENDENCIES",
"LICENSE.txt",
"META-INF/LICENSE",
"META-INF/LICENSE.txt",
"META-INF/README.md",
"META-INF/NOTICE",
"META-INF/*.kotlin_module",
))
exclude("META-INF/DEPENDENCIES")
exclude("LICENSE.txt")
exclude("META-INF/LICENSE")
exclude("META-INF/LICENSE.txt")
exclude("META-INF/NOTICE")
// Compatibility for two RxJava versions (EXH)
exclude("META-INF/rxjava.properties")
}
dependenciesInfo {
includeInApk = false
}
buildFeatures {
viewBinding = true
compose = true
// Disable some unused things
aidl = false
renderScript = false
shaders = false
}
lint {
abortOnError = false
checkReleaseBuilds = false
}
composeOptions {
kotlinCompilerExtensionVersion = compose.versions.compiler.get()
lintOptions {
disable("MissingTranslation", "ExtraTranslation")
isAbortOnError = false
isCheckReleaseBuilds = false
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
isCoreLibraryDesugaringEnabled = true
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
}
sqldelight {
database("Database") {
packageName = "eu.kanade.tachiyomi"
dialect = "sqlite:3.24"
}
}
}
dependencies {
implementation(project(":i18n"))
implementation(project(":core"))
implementation(project(":source-api"))
coreLibraryDesugaring(libs.desugar)
// Compose
implementation(platform(compose.bom))
implementation(compose.activity)
implementation(compose.foundation)
implementation(compose.material3.core)
implementation(compose.material.core)
implementation(compose.material.icons)
implementation(compose.animation)
implementation(compose.animation.graphics)
implementation(compose.ui.tooling)
implementation(compose.ui.util)
implementation(compose.accompanist.webview)
implementation(compose.accompanist.flowlayout)
implementation(compose.accompanist.permissions)
implementation(compose.accompanist.themeadapter)
implementation(compose.accompanist.systemuicontroller)
implementation(androidx.paging.runtime)
implementation(androidx.paging.compose)
implementation(libs.bundles.sqlite)
implementation(libs.sqldelight.android.driver)
implementation(libs.sqldelight.coroutines)
implementation(libs.sqldelight.android.paging)
implementation(kotlinx.reflect)
implementation(platform(kotlinx.coroutines.bom))
implementation(kotlinx.bundles.coroutines)
// Source models and interfaces from Tachiyomi 1.x
implementation("tachiyomi.sourceapi:source-api:1.1")
// AndroidX libraries
implementation(androidx.annotation)
implementation(androidx.appcompat)
implementation(androidx.biometricktx)
implementation(androidx.constraintlayout)
implementation(androidx.coordinatorlayout)
implementation(androidx.corektx)
implementation(androidx.splashscreen)
implementation(androidx.recyclerview)
implementation(androidx.viewpager)
implementation(androidx.glance)
implementation(androidx.profileinstaller)
implementation("androidx.annotation:annotation:1.2.0-beta01")
implementation("androidx.appcompat:appcompat:1.3.0-beta01")
implementation("androidx.biometric:biometric-ktx:1.2.0-alpha02")
implementation("androidx.browser:browser:1.3.0")
implementation("androidx.cardview:cardview:1.0.0")
implementation("androidx.constraintlayout:constraintlayout:2.1.0-alpha2")
implementation("androidx.coordinatorlayout:coordinatorlayout:1.1.0")
implementation("androidx.core:core-ktx:1.5.0-beta01")
implementation("androidx.multidex:multidex:2.0.1")
implementation("androidx.preference:preference-ktx:1.1.1")
implementation("androidx.recyclerview:recyclerview:1.2.0-beta01")
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01")
implementation(androidx.bundles.lifecycle)
val lifecycleVersion = "2.3.0-rc01"
implementation("androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion")
implementation("androidx.lifecycle:lifecycle-process:$lifecycleVersion")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion")
// Job scheduling
implementation(androidx.bundles.workmanager)
implementation("androidx.work:work-runtime-ktx:2.5.0")
// RX
implementation(libs.bundles.reactivex)
implementation(libs.flowreactivenetwork)
// UI library
implementation("com.google.android.material:material:1.3.0")
"standardImplementation"("com.google.firebase:firebase-core:18.0.2")
// ReactiveX
implementation("io.reactivex:rxandroid:1.2.1")
implementation("io.reactivex:rxjava:1.3.8")
implementation("com.jakewharton.rxrelay:rxrelay:1.2.0")
implementation("com.github.pwittchen:reactivenetwork:0.13.0")
// Network client
implementation(libs.bundles.okhttp)
implementation(libs.okio)
val okhttpVersion = "4.10.0-RC1"
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
implementation(libs.conscrypt.android)
implementation("org.conscrypt:conscrypt-android:2.5.1")
// Data serialization (JSON, protobuf)
implementation(kotlinx.bundles.serialization)
// JSON
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")
// HTML parser
implementation(libs.jsoup)
// JavaScript engine
implementation("com.squareup.duktape:duktape-android:1.3.0")
// Disk
implementation(libs.disklrucache)
implementation(libs.unifile)
implementation(libs.junrar)
implementation("com.jakewharton:disklrucache:2.0.2")
implementation("com.github.inorichi:unifile:e9ee588")
implementation("com.github.junrar:junrar:7.4.0")
// HTML parser
implementation("org.jsoup:jsoup:1.13.1")
// Database
implementation("androidx.sqlite:sqlite-ktx:2.1.0")
implementation("com.github.inorichi.storio:storio-common:8be19de@aar")
implementation("com.github.inorichi.storio:storio-sqlite:8be19de@aar")
implementation("io.requery:sqlite-android:3.33.0")
// Preferences
implementation(libs.preferencektx)
implementation("com.github.tfcporciuncula.flow-preferences:flow-preferences:1.3.3")
// Model View Presenter
val nucleusVersion = "3.0.0"
implementation("info.android15.nucleus:nucleus:$nucleusVersion")
implementation("info.android15.nucleus:nucleus-support-v7:$nucleusVersion")
// Dependency injection
implementation(libs.injekt.core)
implementation("com.github.inorichi.injekt:injekt-core:65b0440")
// Image loading
implementation(libs.bundles.coil)
implementation(libs.subsamplingscaleimageview) {
exclude(module = "image-decoder")
}
implementation(libs.image.decoder)
// Image library
val glideVersion = "4.11.0"
implementation("com.github.bumptech.glide:glide:$glideVersion")
implementation("com.github.bumptech.glide:okhttp3-integration:$glideVersion")
kapt("com.github.bumptech.glide:compiler:$glideVersion")
// Sort
implementation(libs.natural.comparator)
// UI libraries
implementation(libs.material)
implementation(libs.flexible.adapter.core)
implementation(libs.flexible.adapter.ui)
implementation(libs.photoview)
implementation(libs.directionalviewpager) {
exclude(group = "androidx.viewpager", module = "viewpager")
}
implementation(libs.insetter)
implementation(libs.bundles.richtext)
implementation(libs.aboutLibraries.compose)
implementation(libs.cascade)
implementation(libs.bundles.voyager)
implementation(libs.wheelpicker)
implementation(libs.materialmotion.core)
implementation("com.github.tachiyomiorg:subsampling-scale-image-view:6caf219")
// TODO: switch to new decoder for stable releases
// implementation("com.github.tachiyomiorg:subsampling-scale-image-view:ca26317")
// Logging
implementation(libs.logcat)
implementation("com.jakewharton.timber:timber:4.7.1")
// Crash reports/analytics
// implementation(libs.acra.http)
// "standardImplementation"(libs.firebase.analytics)
// Crash reports
//implementation("ch.acra:acra-http:5.7.0")
// Shizuku
implementation(libs.bundles.shizuku)
// 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:7d0617d")
// 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:1.1.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
implementation("com.mikepenz:aboutlibraries:${BuildPluginsVersion.ABOUTLIB_PLUGIN}")
// Tests
testImplementation(libs.junit)
testImplementation("junit:junit:4.13.1")
testImplementation("org.assertj:assertj-core:3.16.1")
testImplementation("org.mockito:mockito-core:1.10.19")
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/
// debugImplementation(libs.leakcanary.android)
implementation(libs.leakcanary.plumber)
// debugImplementation("com.squareup.leakcanary:leakcanary-android:2.6")
// SY -->
// [EXH] Android 7 SSL Workaround
implementation("com.google.android.gms:play-services-safetynet:17.0.0")
// Changelog
implementation("com.github.gabrielemariotti.changeloglib:changelog:2.1.0")
// Text distance (EH)
implementation (sylibs.simularity)
implementation ("info.debatty:java-string-similarity:2.0.0")
// Firebase (EH)
implementation(sylibs.firebase.analytics)
implementation(sylibs.firebase.crashlytics.ktx)
implementation("com.google.firebase:firebase-analytics-ktx:18.0.0")
implementation("com.google.firebase:firebase-crashlytics-ktx:17.3.0")
// Better logging (EH)
implementation(sylibs.xlog)
implementation("com.elvishew:xlog:1.7.1")
// Debug utils (EH)
val debugOverlayVersion = "1.1.3"
debugImplementation("com.ms-square:debugoverlay:$debugOverlayVersion")
"releaseTestImplementation"("com.ms-square:debugoverlay-no-op:$debugOverlayVersion")
releaseImplementation("com.ms-square:debugoverlay-no-op:$debugOverlayVersion")
testImplementation("com.ms-square:debugoverlay-no-op:$debugOverlayVersion")
// RatingBar (SY)
implementation(sylibs.ratingbar)
implementation(sylibs.composeRatingbar)
}
implementation ("me.zhanghai.android.materialratingbar:library:1.4.0")
androidComponents {
beforeVariants { variantBuilder ->
// Disables standardBenchmark
if (variantBuilder.buildType == "benchmark") {
variantBuilder.enable = variantBuilder.productFlavors.containsAll(listOf("default" to "dev"))
}
}
onVariants(selector().withFlavor("default" to "standard")) {
// Only excluding in standard flavor because this breaks
// Layout Inspector's Compose tree
it.packaging.resources.excludes.add("META-INF/*.version")
}
// JsonReader for similar manga
implementation("com.squareup.moshi:moshi:1.11.0")
implementation("androidx.gridlayout:gridlayout:1.0.0")
implementation("com.mikepenz:fastadapter:5.3.4")
// SY -->
}
tasks {
withType<Test> {
useJUnitPlatform()
testLogging {
events(TestLogEvent.PASSED, TestLogEvent.SKIPPED, TestLogEvent.FAILED)
}
}
withType<LintTask>().configureEach {
exclude { it.file.path.contains("generated[\\\\/]".toRegex()) }
}
// See https://kotlinlang.org/docs/reference/experimental.html#experimental-status-of-experimental-api(-markers)
withType<KotlinCompile> {
kotlinOptions.freeCompilerArgs += listOf(
"-opt-in=coil.annotation.ExperimentalCoilApi",
"-opt-in=com.google.accompanist.permissions.ExperimentalPermissionsApi",
"-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi",
"-opt-in=androidx.compose.material.ExperimentalMaterialApi",
"-opt-in=androidx.compose.material3.ExperimentalMaterial3Api",
"-opt-in=androidx.compose.material.ExperimentalMaterialApi",
"-opt-in=androidx.compose.ui.ExperimentalComposeUiApi",
"-opt-in=androidx.compose.foundation.ExperimentalFoundationApi",
"-opt-in=androidx.compose.animation.ExperimentalAnimationApi",
"-opt-in=androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi",
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
"-opt-in=kotlinx.coroutines.FlowPreview",
"-opt-in=kotlinx.coroutines.InternalCoroutinesApi",
"-opt-in=kotlinx.serialization.ExperimentalSerializationApi",
"-Xopt-in=kotlin.Experimental",
"-Xopt-in=kotlin.RequiresOptIn",
"-Xuse-experimental=kotlin.ExperimentalStdlibApi",
"-Xuse-experimental=kotlinx.coroutines.FlowPreview",
"-Xuse-experimental=kotlinx.coroutines.ExperimentalCoroutinesApi",
"-Xuse-experimental=kotlinx.coroutines.InternalCoroutinesApi",
"-Xuse-experimental=kotlinx.serialization.ExperimentalSerializationApi"
)
}
if (project.findProperty("tachiyomi.enableComposeCompilerMetrics") == "true") {
kotlinOptions.freeCompilerArgs += listOf(
"-P",
"plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=" +
project.buildDir.absolutePath + "/compose_metrics"
)
kotlinOptions.freeCompilerArgs += listOf(
"-P",
"plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=" +
project.buildDir.absolutePath + "/compose_metrics"
)
}
// Duplicating Hebrew string assets due to some locale code issues on different devices
val copyHebrewStrings = task("copyHebrewStrings", type = Copy::class) {
from("./src/main/res/values-he")
into("./src/main/res/values-iw")
include("**/*")
}
preBuild {
val ktlintTask = if (System.getenv("GITHUB_BASE_REF") == null) formatKotlin else lintKotlin
dependsOn(ktlintTask)
dependsOn(formatKotlin, copyHebrewStrings)
}
}
buildscript {
repositories {
mavenCentral()
}
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>(...);
}
+121 -76
View File
@@ -1,29 +1,87 @@
-dontobfuscate
# Keep common dependencies used in extensions
-keep,allowoptimization class androidx.preference.** { public protected *; }
-keep,allowoptimization class kotlin.** { public protected *; }
-keep,allowoptimization class kotlinx.coroutines.** { public protected *; }
-keep,allowoptimization class kotlinx.serialization.** { public protected *; }
-keep,allowoptimization class okhttp3.** { public protected *; }
-keep,allowoptimization class okio.** { public protected *; }
-keep,allowoptimization class rx.** { public protected *; }
-keep,allowoptimization class org.jsoup.** { public protected *; }
-keep,allowoptimization class app.cash.quickjs.** { public protected *; }
-keep,allowoptimization class uy.kohesive.injekt.** { public protected *; }
# Extensions may require methods unused in the core app
-dontwarn eu.kanade.tachiyomi.**
-keep class eu.kanade.tachiyomi.** { public protected private *; }
# From extensions-lib
-keep,allowoptimization class eu.kanade.tachiyomi.network.interceptor.RateLimitInterceptorKt { public protected *; }
-keep,allowoptimization class eu.kanade.tachiyomi.network.interceptor.SpecificHostRateLimitInterceptorKt { public protected *; }
-keep,allowoptimization class eu.kanade.tachiyomi.network.NetworkHelper { public protected *; }
-keep,allowoptimization class eu.kanade.tachiyomi.network.OkHttpExtensionsKt { public protected *; }
-keep,allowoptimization class eu.kanade.tachiyomi.network.RequestsKt { public protected *; }
-keep,allowoptimization class eu.kanade.tachiyomi.AppInfo { public protected *; }
-keep class org.jsoup.** { *; }
-keep class kotlin.** { *; }
-keep class okhttp3.** { *; }
-keep class com.google.gson.** { *; }
-keep class com.github.salomonbrys.kotson.** { *; }
-keep class com.squareup.duktape.** { *; }
# Debug functions
-keep,allowoptimization class exh.debug.DebugFunctions { public *; }
# === Keep EH classes
-keep class exh.** { *; }
-keep class xyz.nulldev.** { *; }
##---------------Begin: proguard configuration for RxJava 1.x ----------
# === Keep RxAndroid, https://github.com/ReactiveX/RxAndroid/issues/350
-keep class rx.android.** { *; }
# Design library
-dontwarn com.google.android.material.**
-keep class com.google.android.material.** { *; }
-keep interface com.google.android.material.** { *; }
-keep public class com.google.android.material.R$* { *; }
-keep class com.hippo.image.** { *; }
-keep interface com.hippo.image.** { *; }
-keepclassmembers class * extends nucleus.presenter.Presenter {
<init>();
}
# Kotlin Serialization
-keepattributes *Annotation*, InnerClasses
-dontnote kotlinx.serialization.AnnotationsKt # core serialization annotations
# kotlinx-serialization-json specific. Add this if you have java.lang.NoClassDefFoundError kotlinx.serialization.json.JsonObjectSerializer
-keepclassmembers class kotlinx.serialization.json.** {
*** Companion;
}
-keepclasseswithmembers class kotlinx.serialization.json.** {
kotlinx.serialization.KSerializer serializer(...);
}
-keep,includedescriptorclasses class eu.kanade.tachiyomi.**$$serializer { *; }
-keepclassmembers class eu.kanade.tachiyomi.** {
*** Companion;
}
-keepclasseswithmembers class eu.kanade.tachiyomi.** {
kotlinx.serialization.KSerializer serializer(...);
}
-keep,includedescriptorclasses class exh.**$$serializer { *; }
-keepclassmembers class exh.** {
*** Companion;
}
-keepclasseswithmembers class exh.** {
kotlinx.serialization.KSerializer serializer(...);
}
-keep,includedescriptorclasses class xyz.nulldev.ts.api.http.serializer.**$$serializer { *; }
-keepclassmembers class xyz.nulldev.ts.api.http.serializer.** {
*** Companion;
}
-keepclasseswithmembers class xyz.nulldev.ts.api.http.serializer.** {
kotlinx.serialization.KSerializer serializer(...);
}
# Madokami extension username and password crash fix
-keepclassmembers class androidx.preference.EditTextPreference {
*** mOnBindEditTextListener;
*** mText;
public *;
}
# Hitomi extension crash fix
-keepclassmembers class rx.Single {
*** onSubscribe;
final *;
protected *;
public *;
}
# RxJava 1.1.0
-dontwarn sun.misc.**
-keepclassmembers class rx.internal.util.unsafe.*ArrayQueue*Field* {
@@ -40,50 +98,6 @@
}
-dontnote rx.internal.util.PlatformDependent
##---------------End: proguard configuration for RxJava 1.x ----------
##---------------Begin: proguard configuration for kotlinx.serialization ----------
-keepattributes *Annotation*, InnerClasses
-dontnote kotlinx.serialization.AnnotationsKt # core serialization annotations
# kotlinx-serialization-json specific. Add this if you have java.lang.NoClassDefFoundError kotlinx.serialization.json.JsonObjectSerializer
-keepclassmembers class kotlinx.serialization.json.** {
*** Companion;
}
-keepclasseswithmembers class kotlinx.serialization.json.** {
kotlinx.serialization.KSerializer serializer(...);
}
-keep,includedescriptorclasses class eu.kanade.**$$serializer { *; }
-keepclassmembers class eu.kanade.** {
*** Companion;
}
-keepclasseswithmembers class eu.kanade.** {
kotlinx.serialization.KSerializer serializer(...);
}
-keep,includedescriptorclasses class exh.**$$serializer { *; }
-keepclassmembers class exh.** {
*** Companion;
}
-keepclasseswithmembers class exh.** {
kotlinx.serialization.KSerializer serializer(...);
}
# Filter serializer
-keep,includedescriptorclasses class xyz.nulldev.ts.api.http.serializer.**$$serializer { *; }
-keepclassmembers class xyz.nulldev.ts.api.http.serializer.** {
*** Companion;
}
-keepclasseswithmembers class xyz.nulldev.ts.api.http.serializer.** {
kotlinx.serialization.KSerializer serializer(...);
}
-keep class kotlinx.serialization.**
-keepclassmembers class kotlinx.serialization.** {
<methods>;
}
##---------------End: proguard configuration for kotlinx.serialization ----------
# === Reactive network: https://github.com/pwittchen/ReactiveNetwork/tree/v0.12.4#proguard-configuration
-dontwarn com.github.pwittchen.reactivenetwork.library.rx2.ReactiveNetwork
@@ -104,20 +118,31 @@
# === Okio: https://github.com/square/okio/tree/9b8545e7fa267c9d89753283990f24a35cd69cd6#proguard
-dontwarn okio.**
# === Keep RxAndroid, https://github.com/ReactiveX/RxAndroid/issues/350
-keep class rx.android.** { *; }
# === 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
# XmlUtil
-keep public enum nl.adaptivity.xmlutil.EventType { *; }
# For using GSON @Expose annotation
-keepattributes *Annotation*
# Design library
-dontwarn com.google.android.material.**
-keep class com.google.android.material.** { *; }
-keep interface com.google.android.material.** { *; }
-keep public class com.google.android.material.R$* { *; }
# Gson specific classes
-dontwarn sun.misc.**
#-keep class com.google.gson.stream.** { *; }
-keep class com.hippo.image.** { *; }
-keep interface com.hippo.image.** { *; }
# 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
-keepclassmembers class * extends nucleus.presenter.Presenter {
@@ -130,6 +155,26 @@
## From original config: "Attempt to fix: java.lang.NoClassDefFoundError: uy.kohesive.injekt.registry.default.DefaultRegistrar$NOKEY$1"
-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
# 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 {
public <init>();
}
# === RxBinding
-dontwarn com.google.auto.value.AutoValue
+1 -1
View File
@@ -17,7 +17,7 @@
android:shortcutDisabledMessage="@string/app_not_available"
android:shortcutId="show_recently_updated"
android:shortcutLongLabel="@string/label_recent_updates"
android:shortcutShortLabel="@string/label_recent_updates">
android:shortcutShortLabel="@string/short_recent_updates">
<intent
android:action="eu.kanade.tachiyomi.SHOW_RECENTLY_UPDATED"
android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity" />
@@ -2,5 +2,4 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_tachi_monochrome_launcher" />
</adaptive-icon>
@@ -2,5 +2,4 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_tachi_monochrome_launcher" />
</adaptive-icon>
+202 -229
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
package="eu.kanade.tachiyomi">
<!-- Internet -->
<uses-permission android:name="android.permission.INTERNET" />
@@ -14,46 +14,30 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<!-- For managing extensions -->
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<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+ -->
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.READ_APP_SPECIFIC_LOCALES" />
<!-- Remove permission from Firebase dependency -->
<uses-permission android:name="com.google.android.gms.permission.AD_ID"
tools:node="remove" />
<application
android:name=".App"
android:allowBackup="false"
android:allowBackup="true"
android:fullBackupContent="@xml/backup_rules"
android:hardwareAccelerated="true"
android:hasFragileUserData="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:largeHeap="true"
android:localeConfig="@xml/locales_config"
android:requestLegacyExternalStorage="true"
android:roundIcon="@mipmap/ic_launcher_round"
android:theme="@style/Theme.Tachiyomi"
android:supportsRtl="true"
android:theme="@style/Theme.Tachiyomi.Light"
android:networkSecurityConfig="@xml/network_security_config">
<!-- enable profiling by macrobenchmark -->
<profileable
android:shell="true"
tools:targetApi="q" />
<activity
android:name=".ui.main.MainActivity"
android:launchMode="singleTop"
android:theme="@style/Theme.Tachiyomi.SplashScreen"
android:exported="true">
android:theme="@style/Theme.Splash">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
@@ -63,18 +47,11 @@
android:name="android.app.shortcuts"
android:resource="@xml/shortcuts" />
</activity>
<activity
android:process=":error_handler"
android:name=".crash.CrashActivity"
android:exported="false" />
<activity
android:name=".ui.main.DeepLinkActivity"
android:launchMode="singleTask"
android:theme="@android:style/Theme.NoDisplay"
android:label="@string/action_global_search"
android:exported="true">
android:label="@string/action_global_search">
<intent-filter>
<action android:name="android.intent.action.SEARCH" />
<action android:name="com.google.android.gms.actions.SEARCH_ACTION" />
@@ -95,11 +72,9 @@
android:name="android.app.searchable"
android:resource="@xml/searchable" />
</activity>
<activity
android:name=".ui.reader.ReaderActivity"
android:launchMode="singleTask"
android:exported="false">
android:launchMode="singleTask">
<intent-filter>
<action android:name="com.samsung.android.support.REMOTE_ACTION" />
</intent-filter>
@@ -107,26 +82,15 @@
<meta-data android:name="com.samsung.android.support.REMOTE_ACTION"
android:resource="@xml/s_pen_actions"/>
</activity>
<activity
android:name=".ui.security.UnlockActivity"
android:theme="@style/Theme.Tachiyomi"
android:exported="false" />
android:name=".ui.security.BiometricUnlockActivity"
android:theme="@style/Theme.Splash" />
<activity
android:name=".ui.webview.WebViewActivity"
android:configChanges="uiMode|orientation|screenSize"
android:exported="false" />
<activity
android:name=".extension.util.ExtensionInstallActivity"
android:theme="@android:style/Theme.Translucent.NoTitleBar"
android:exported="false" />
android:configChanges="uiMode|orientation|screenSize" />
<activity
android:name=".ui.setting.track.AnilistLoginActivity"
android:label="Anilist"
android:exported="true">
android:label="Anilist">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
@@ -140,8 +104,7 @@
</activity>
<activity
android:name=".ui.setting.track.MyAnimeListLoginActivity"
android:label="MyAnimeList"
android:exported="true">
android:label="MyAnimeList">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
@@ -155,8 +118,7 @@
</activity>
<activity
android:name=".ui.setting.track.ShikimoriLoginActivity"
android:label="Shikimori"
android:exported="true">
android:label="Shikimori">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
@@ -170,8 +132,7 @@
</activity>
<activity
android:name=".ui.setting.track.BangumiLoginActivity"
android:label="Bangumi"
android:exported="true">
android:label="Bangumi">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
@@ -185,55 +146,8 @@
</activity>
<activity
android:name="exh.ui.login.EhLoginActivity"
android:label="EHentaiLogin"
android:exported="false"/>
<receiver
android:name=".data.notification.NotificationReceiver"
android:exported="false" />
<receiver
android:name=".glance.UpdatesGridGlanceReceiver"
android:enabled="@bool/glance_appwidget_available"
android:exported="false"
android:label="@string/label_recent_updates">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/updates_grid_glance_widget_info" />
</receiver>
<service
android:name=".data.library.LibraryUpdateService"
android:exported="false" />
<service
android:name=".data.download.DownloadService"
android:exported="false" />
<service
android:name=".data.updater.AppUpdateService"
android:exported="false" />
<service
android:name=".data.backup.BackupRestoreService"
android:exported="false" />
<service android:name=".extension.util.ExtensionInstallService"
android:exported="false" />
<service
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
android:enabled="false"
android:exported="false">
<meta-data
android:name="autoStoreLocales"
android:value="true" />
</service>
android:name=".extension.util.ExtensionInstallActivity"
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
<provider
android:name="androidx.core.content.FileProvider"
@@ -245,158 +159,217 @@
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" />
<receiver
android:name=".data.notification.NotificationReceiver"
android:exported="false" />
<meta-data
android:name="android.webkit.WebView.EnableSafeBrowsing"
android:value="false" />
<meta-data
android:name="android.webkit.WebView.MetricsOptOut"
android:value="true" />
<service
android:name=".data.library.LibraryUpdateService"
android:exported="false" />
<!-- Disable advertising ID collection for Firebase -->
<meta-data
android:name="google_analytics_adid_collection_enabled"
android:value="false" />
<service
android:name=".data.download.DownloadService"
android:exported="false" />
<service
android:name=".data.updater.UpdaterService"
android:exported="false" />
<service
android:name=".data.backup.BackupCreateService"
android:exported="false" />
<service
android:name=".data.backup.BackupRestoreService"
android:exported="false" />
<!-- EH -->
<service
android:name="exh.md.similar.SimilarUpdateService"
android:exported="false" />
<service
android:name="exh.eh.EHentaiUpdateWorker"
android:permission="android.permission.BIND_JOB_SERVICE"
android:exported="true" />
<activity
android:name="exh.ui.intercept.InterceptActivity"
android:label="@string/app_name"
android:theme="@style/Theme.Tachiyomi"
android:exported="true">
<!-- E-Hentai -->
android:theme="@style/Theme.EHActivity">
<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" />
<!-- EH -->
<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" />
<data android:host="www.e-hentai.org" />
<data android:host="g.e-hentai.org" />
<!-- EXH -->
<data
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/..*" />
</intent-filter>
<!-- ExHentai -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<!-- nhentai -->
<data
android:host="nhentai.net"
android:pathPrefix="/g/"
android:scheme="http" />
<data
android:host="nhentai.net"
android:pathPrefix="/g/"
android:scheme="https" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<!-- Perv Eden -->
<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" />
<data android:scheme="http" />
<!-- Hentai Cafe -->
<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" />
<data android:host="www.exhentai.org" />
<!-- Tsumino -->
<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/..*" />
</intent-filter>
<!-- NHentai -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<!-- Hitomi.la -->
<data
android:host="hitomi.la"
android:pathPrefix="/galleries/"
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" />
<category android:name="android.intent.category.BROWSABLE" />
<!-- Pururin.io -->
<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" />
<data android:scheme="http" />
<!-- HBrowse -->
<data
android:host="www.hbrowse.com"
android:scheme="http" />
<data
android:host="www.hbrowse.com"
android:scheme="https" />
<data android:host="nhentai.net" />
<data android:host="www.nhentai.net" />
<data android:pathPattern="/g/..*" />
</intent-filter>
<!-- Tsumino -->
<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="tsumino.com" />
<data android:host="www.tsumino.com" />
<data android:pathPattern="/Read/View/..*" />
<data android:pathPattern="/Book/Info/..*" />
</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>
</activity>
<activity
android:name="exh.md.MangaDexLoginActivity"
android:label="MangaDexLogin"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<!-- MangaDex -->
<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:host="mangadex-auth"
android:scheme="tachiyomisy" />
android:scheme="https"
android:host="www.mangadex.org"
android:pathPrefix="/title/" />
<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/" />
<data
android:scheme="https"
android:host="www.mangadex.org"
android:pathPrefix="/chapter/" />
<data
android:scheme="https"
android:host="mangadex.org"
android:pathPrefix="/chapter/" />
<data
android:scheme="https"
android:host="www.mangadex.cc"
android:pathPrefix="/chapter/" />
<data
android:scheme="https"
android:host="www.mangadex.cc"
android:pathPrefix="/chapter/" />
</intent-filter>
</activity>
<activity
android:name="exh.ui.captcha.BrowserActionActivity"
android:theme="@style/Theme.EHActivity" />
</application>
</manifest>
File diff suppressed because it is too large Load Diff
@@ -1,55 +0,0 @@
package eu.kanade.core.prefs
import androidx.compose.ui.state.ToggleableState
sealed class CheckboxState<T>(open val value: T) {
abstract fun next(): CheckboxState<T>
sealed class State<T>(override val value: T) : CheckboxState<T>(value) {
data class Checked<T>(override val value: T) : State<T>(value)
data class None<T>(override val value: T) : State<T>(value)
val isChecked: Boolean
get() = this is Checked
override fun next(): CheckboxState<T> {
return when (this) {
is Checked -> None(value)
is None -> Checked(value)
}
}
}
sealed class TriState<T>(override val value: T) : CheckboxState<T>(value) {
data class Include<T>(override val value: T) : TriState<T>(value)
data class Exclude<T>(override val value: T) : TriState<T>(value)
data class None<T>(override val value: T) : TriState<T>(value)
override fun next(): CheckboxState<T> {
return when (this) {
is Exclude -> None(value)
is Include -> Exclude(value)
is None -> Include(value)
}
}
fun asState(): ToggleableState {
return when (this) {
is Exclude -> ToggleableState.Indeterminate
is Include -> ToggleableState.On
is None -> ToggleableState.Off
}
}
}
}
inline fun <T> T.asCheckboxState(condition: (T) -> Boolean): CheckboxState.State<T> {
return if (condition(this)) {
CheckboxState.State.Checked(this)
} else {
CheckboxState.State.None(this)
}
}
inline fun <T> List<T>.mapAsCheckboxState(condition: (T) -> Boolean): List<CheckboxState.State<T>> {
return this.map { it.asCheckboxState(condition) }
}
@@ -1,38 +0,0 @@
package eu.kanade.core.prefs
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import eu.kanade.tachiyomi.core.preference.Preference
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
class PreferenceMutableState<T>(
private val preference: Preference<T>,
scope: CoroutineScope,
) : MutableState<T> {
private val state = mutableStateOf(preference.get())
init {
preference.changes()
.onEach { state.value = it }
.launchIn(scope)
}
override var value: T
get() = state.value
set(value) {
preference.set(value)
}
override fun component1(): T {
return state.value
}
override fun component2(): (T) -> Unit {
return { preference.set(it) }
}
}
fun <T> Preference<T>.asState(scope: CoroutineScope) = PreferenceMutableState(this, scope)
@@ -1,148 +0,0 @@
package eu.kanade.core.util
import androidx.compose.ui.util.fastForEach
import java.util.concurrent.ConcurrentHashMap
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract
fun <T : R, R : Any> List<T>.insertSeparators(
generator: (T?, T?) -> R?,
): List<R> {
if (isEmpty()) return emptyList()
val newList = mutableListOf<R>()
for (i in -1..lastIndex) {
val before = getOrNull(i)
before?.let { newList.add(it) }
val after = getOrNull(i + 1)
val separator = generator.invoke(before, after)
separator?.let { newList.add(it) }
}
return newList
}
/**
* Returns a new map containing only the key entries of [transform] that are not null.
*/
inline fun <K, V, R> Map<out K, V>.mapNotNullKeys(transform: (Map.Entry<K?, V>) -> R?): ConcurrentHashMap<R, V> {
val mutableMap = ConcurrentHashMap<R, V>()
forEach { element -> transform(element)?.let { mutableMap[it] = element.value } }
return mutableMap
}
fun <E> HashSet<E>.addOrRemove(value: E, shouldAdd: Boolean) {
if (shouldAdd) {
add(value)
} else {
remove(value)
}
}
/**
* Returns a list containing only elements matching the given [predicate].
*
* **Do not use for collections that come from public APIs**, since they may not support random
* access in an efficient way, and this method may actually be a lot slower. Only use for
* collections that are created by code we control and are known to support random access.
*/
@OptIn(ExperimentalContracts::class)
inline fun <T> List<T>.fastFilter(predicate: (T) -> Boolean): List<T> {
contract { callsInPlace(predicate) }
val destination = ArrayList<T>()
fastForEach { if (predicate(it)) destination.add(it) }
return destination
}
/**
* Returns a list containing all elements not matching the given [predicate].
*
* **Do not use for collections that come from public APIs**, since they may not support random
* access in an efficient way, and this method may actually be a lot slower. Only use for
* collections that are created by code we control and are known to support random access.
*/
@OptIn(ExperimentalContracts::class)
inline fun <T> List<T>.fastFilterNot(predicate: (T) -> Boolean): List<T> {
contract { callsInPlace(predicate) }
val destination = ArrayList<T>()
fastForEach { if (!predicate(it)) destination.add(it) }
return destination
}
/**
* Returns a list containing only the non-null results of applying the
* given [transform] function to each element in the original collection.
*
* **Do not use for collections that come from public APIs**, since they may not support random
* access in an efficient way, and this method may actually be a lot slower. Only use for
* collections that are created by code we control and are known to support random access.
*/
@OptIn(ExperimentalContracts::class)
inline fun <T, R> List<T>.fastMapNotNull(transform: (T) -> R?): List<R> {
contract { callsInPlace(transform) }
val destination = ArrayList<R>()
fastForEach { element ->
transform(element)?.let { destination.add(it) }
}
return destination
}
/**
* Splits the original collection into pair of lists,
* where *first* list contains elements for which [predicate] yielded `true`,
* while *second* list contains elements for which [predicate] yielded `false`.
*
* **Do not use for collections that come from public APIs**, since they may not support random
* access in an efficient way, and this method may actually be a lot slower. Only use for
* collections that are created by code we control and are known to support random access.
*/
@OptIn(ExperimentalContracts::class)
inline fun <T> List<T>.fastPartition(predicate: (T) -> Boolean): Pair<List<T>, List<T>> {
contract { callsInPlace(predicate) }
val first = ArrayList<T>()
val second = ArrayList<T>()
fastForEach {
if (predicate(it)) {
first.add(it)
} else {
second.add(it)
}
}
return Pair(first, second)
}
/**
* Returns the number of entries not matching the given [predicate].
*
* **Do not use for collections that come from public APIs**, since they may not support random
* access in an efficient way, and this method may actually be a lot slower. Only use for
* collections that are created by code we control and are known to support random access.
*/
@OptIn(ExperimentalContracts::class)
inline fun <T> List<T>.fastCountNot(predicate: (T) -> Boolean): Int {
contract { callsInPlace(predicate) }
var count = size
fastForEach { if (predicate(it)) --count }
return count
}
/**
* Returns a list containing only elements from the given collection
* having distinct keys returned by the given [selector] function.
*
* Among elements of the given collection with equal keys, only the first one will be present in the resulting list.
* The elements in the resulting list are in the same order as they were in the source collection.
*
* **Do not use for collections that come from public APIs**, since they may not support random
* access in an efficient way, and this method may actually be a lot slower. Only use for
* collections that are created by code we control and are known to support random access.
*/
@OptIn(ExperimentalContracts::class)
inline fun <T, K> List<T>.fastDistinctBy(selector: (T) -> K): List<T> {
contract { callsInPlace(selector) }
val set = HashSet<K>()
val list = ArrayList<T>()
fastForEach {
val key = selector(it)
if (set.add(key)) list.add(it)
}
return list
}
@@ -1,16 +0,0 @@
package eu.kanade.core.util
import android.content.Context
import eu.kanade.tachiyomi.R
import kotlin.time.Duration
fun Duration.toDurationString(context: Context, fallback: String): String {
return toComponents { days, hours, minutes, seconds, _ ->
buildList(4) {
if (days != 0L) add(context.getString(R.string.day_short, days))
if (hours != 0) add(context.getString(R.string.hour_short, hours))
if (minutes != 0 && (days == 0L || hours == 0)) add(context.getString(R.string.minute_short, minutes))
if (seconds != 0 && days == 0L && hours == 0) add(context.getString(R.string.seconds_short, seconds))
}.joinToString(" ").ifBlank { fallback }
}
}
@@ -1,61 +0,0 @@
package eu.kanade.core.util
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.launch
import rx.Emitter
import rx.Observable
import rx.Observer
import kotlin.coroutines.CoroutineContext
fun <T : Any> Observable<T>.asFlow(): Flow<T> = callbackFlow {
val observer = object : Observer<T> {
override fun onNext(t: T) {
trySend(t)
}
override fun onError(e: Throwable) {
close(e)
}
override fun onCompleted() {
close()
}
}
val subscription = subscribe(observer)
awaitClose { subscription.unsubscribe() }
}
fun <T : Any> Flow<T>.asObservable(
context: CoroutineContext = Dispatchers.Unconfined,
backpressureMode: Emitter.BackpressureMode = Emitter.BackpressureMode.NONE,
): Observable<T> {
return Observable.create(
{ emitter ->
/*
* ATOMIC is used here to provide stable behaviour of subscribe+dispose pair even if
* asObservable is already invoked from unconfined
*/
val job = GlobalScope.launch(context = context, start = CoroutineStart.ATOMIC) {
try {
collect { emitter.onNext(it) }
emitter.onCompleted()
} catch (e: Throwable) {
// Ignore `CancellationException` as error, since it indicates "normal cancellation"
if (e !is CancellationException) {
emitter.onError(e)
} else {
emitter.onCompleted()
}
}
}
emitter.setCancellation { job.cancel() }
},
backpressureMode,
)
}
@@ -1,98 +0,0 @@
package eu.kanade.data
import androidx.paging.PagingSource
import com.squareup.sqldelight.Query
import com.squareup.sqldelight.db.SqlDriver
import com.squareup.sqldelight.runtime.coroutines.asFlow
import com.squareup.sqldelight.runtime.coroutines.mapToList
import com.squareup.sqldelight.runtime.coroutines.mapToOne
import com.squareup.sqldelight.runtime.coroutines.mapToOneOrNull
import eu.kanade.data.manga.LibraryQuery
import eu.kanade.data.updates.UpdatesQuery
import eu.kanade.tachiyomi.Database
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.withContext
class AndroidDatabaseHandler(
val db: Database,
private val driver: SqlDriver,
val queryDispatcher: CoroutineDispatcher = Dispatchers.IO,
val transactionDispatcher: CoroutineDispatcher = queryDispatcher,
) : DatabaseHandler {
val suspendingTransactionId = ThreadLocal<Int>()
override suspend fun <T> await(inTransaction: Boolean, block: suspend Database.() -> T): T {
return dispatch(inTransaction, block)
}
override suspend fun <T : Any> awaitList(
inTransaction: Boolean,
block: suspend Database.() -> Query<T>,
): List<T> {
return dispatch(inTransaction) { block(db).executeAsList() }
}
override suspend fun <T : Any> awaitOne(
inTransaction: Boolean,
block: suspend Database.() -> Query<T>,
): T {
return dispatch(inTransaction) { block(db).executeAsOne() }
}
override suspend fun <T : Any> awaitOneOrNull(
inTransaction: Boolean,
block: suspend Database.() -> Query<T>,
): T? {
return dispatch(inTransaction) { block(db).executeAsOneOrNull() }
}
override fun <T : Any> subscribeToList(block: Database.() -> Query<T>): Flow<List<T>> {
return block(db).asFlow().mapToList(queryDispatcher)
}
override fun <T : Any> subscribeToOne(block: Database.() -> Query<T>): Flow<T> {
return block(db).asFlow().mapToOne(queryDispatcher)
}
override fun <T : Any> subscribeToOneOrNull(block: Database.() -> Query<T>): Flow<T?> {
return block(db).asFlow().mapToOneOrNull(queryDispatcher)
}
override fun <T : Any> subscribeToPagingSource(
countQuery: Database.() -> Query<Long>,
queryProvider: Database.(Long, Long) -> Query<T>,
): PagingSource<Long, T> {
return QueryPagingSource(
handler = this,
countQuery = countQuery,
queryProvider = { limit, offset ->
queryProvider.invoke(db, limit, offset)
},
)
}
private suspend fun <T> dispatch(inTransaction: Boolean, block: suspend Database.() -> T): T {
// Create a transaction if needed and run the calling block inside it.
if (inTransaction) {
return withTransaction { block(db) }
}
// If we're currently in the transaction thread, there's no need to dispatch our query.
if (driver.currentTransaction() != null) {
return block(db)
}
// Get the current database context and run the calling block.
val context = getCurrentDatabaseContext()
return withContext(context) { block(db) }
}
// SY -->
fun getLibraryQuery() = LibraryQuery(driver)
fun getUpdatesQuery(after: Long) = UpdatesQuery(driver, after)
// SY <--
}
@@ -1,54 +0,0 @@
package eu.kanade.data
import com.squareup.sqldelight.ColumnAdapter
import eu.kanade.tachiyomi.source.model.UpdateStrategy
import java.util.Date
val dateAdapter = object : ColumnAdapter<Date, Long> {
override fun decode(databaseValue: Long): Date = Date(databaseValue)
override fun encode(value: Date): Long = value.time
}
private const val listOfStringsSeparator = ", "
val listOfStringsAdapter = object : ColumnAdapter<List<String>, String> {
override fun decode(databaseValue: String) =
if (databaseValue.isEmpty()) {
emptyList()
} else {
databaseValue.split(listOfStringsSeparator)
}
override fun encode(value: List<String>) = value.joinToString(separator = listOfStringsSeparator)
}
val updateStrategyAdapter = object : ColumnAdapter<UpdateStrategy, Long> {
private val enumValues by lazy { UpdateStrategy.values() }
override fun decode(databaseValue: Long): UpdateStrategy =
enumValues.getOrElse(databaseValue.toInt()) { UpdateStrategy.ALWAYS_UPDATE }
override fun encode(value: UpdateStrategy): Long = value.ordinal.toLong()
}
// SY -->
private const val listOfStringsAndSeparator = " & "
val listOfStringsAndAdapter = object : ColumnAdapter<List<String>, String> {
override fun decode(databaseValue: String) =
if (databaseValue.isEmpty()) {
emptyList()
} else {
databaseValue.split(listOfStringsAndSeparator)
}
override fun encode(value: List<String>) = value.joinToString(separator = listOfStringsAndSeparator)
}
private const val listOfLongsSeparator = "/"
val listOfLongsAdapter = object : ColumnAdapter<List<Long>, String> {
override fun decode(databaseValue: String) =
if (databaseValue.isEmpty()) {
emptyList()
} else {
databaseValue.split(listOfLongsSeparator).mapNotNull { it.toLongOrNull() }
}
override fun encode(value: List<Long>) = value.joinToString(separator = listOfLongsSeparator)
}
// SY <--
@@ -1,37 +0,0 @@
package eu.kanade.data
import androidx.paging.PagingSource
import com.squareup.sqldelight.Query
import eu.kanade.tachiyomi.Database
import kotlinx.coroutines.flow.Flow
interface DatabaseHandler {
suspend fun <T> await(inTransaction: Boolean = false, block: suspend Database.() -> T): T
suspend fun <T : Any> awaitList(
inTransaction: Boolean = false,
block: suspend Database.() -> Query<T>,
): List<T>
suspend fun <T : Any> awaitOne(
inTransaction: Boolean = false,
block: suspend Database.() -> Query<T>,
): T
suspend fun <T : Any> awaitOneOrNull(
inTransaction: Boolean = false,
block: suspend Database.() -> Query<T>,
): T?
fun <T : Any> subscribeToList(block: Database.() -> Query<T>): Flow<List<T>>
fun <T : Any> subscribeToOne(block: Database.() -> Query<T>): Flow<T>
fun <T : Any> subscribeToOneOrNull(block: Database.() -> Query<T>): Flow<T?>
fun <T : Any> subscribeToPagingSource(
countQuery: Database.() -> Query<Long>,
queryProvider: Database.(Long, Long) -> Query<T>,
): PagingSource<Long, T>
}
@@ -1,72 +0,0 @@
package eu.kanade.data
import androidx.paging.PagingSource
import androidx.paging.PagingState
import com.squareup.sqldelight.Query
import eu.kanade.tachiyomi.Database
import kotlin.properties.Delegates
class QueryPagingSource<RowType : Any>(
val handler: DatabaseHandler,
val countQuery: Database.() -> Query<Long>,
val queryProvider: Database.(Long, Long) -> Query<RowType>,
) : PagingSource<Long, RowType>(), Query.Listener {
override val jumpingSupported: Boolean = true
private var currentQuery: Query<RowType>? by Delegates.observable(null) { _, old, new ->
old?.removeListener(this)
new?.addListener(this)
}
init {
registerInvalidatedCallback {
currentQuery?.removeListener(this)
currentQuery = null
}
}
override suspend fun load(params: LoadParams<Long>): LoadResult<Long, RowType> {
try {
val key = params.key ?: 0L
val loadSize = params.loadSize
val count = handler.awaitOne { countQuery() }
val (offset, limit) = when (params) {
is LoadParams.Prepend -> key - loadSize to loadSize.toLong()
else -> key to loadSize.toLong()
}
val data = handler.awaitList {
queryProvider(limit, offset)
.also { currentQuery = it }
}
val (prevKey, nextKey) = when (params) {
is LoadParams.Append -> { offset - loadSize to offset + loadSize }
else -> { offset to offset + loadSize }
}
return LoadResult.Page(
data = data,
prevKey = if (offset <= 0L || prevKey < 0L) null else prevKey,
nextKey = if (offset + loadSize >= count) null else nextKey,
itemsBefore = maxOf(0L, offset).toInt(),
itemsAfter = maxOf(0L, count - (offset + loadSize)).toInt(),
)
} catch (e: Exception) {
return LoadResult.Error(throwable = e)
}
}
override fun getRefreshKey(state: PagingState<Long, RowType>): Long? {
return state.anchorPosition?.let { anchorPosition ->
val anchorPage = state.closestPageToPosition(anchorPosition)
anchorPage?.prevKey ?: anchorPage?.nextKey
}
}
override fun queryResultsChanged() {
invalidate()
}
}
@@ -1,161 +0,0 @@
package eu.kanade.data
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Job
import kotlinx.coroutines.asContextElement
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext
import java.util.concurrent.RejectedExecutionException
import java.util.concurrent.atomic.AtomicInteger
import kotlin.coroutines.ContinuationInterceptor
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
import kotlin.coroutines.coroutineContext
import kotlin.coroutines.resume
/**
* Returns the transaction dispatcher if we are on a transaction, or the database dispatchers.
*/
internal suspend fun AndroidDatabaseHandler.getCurrentDatabaseContext(): CoroutineContext {
return coroutineContext[TransactionElement]?.transactionDispatcher ?: queryDispatcher
}
/**
* Calls the specified suspending [block] in a database transaction. The transaction will be
* marked as successful unless an exception is thrown in the suspending [block] or the coroutine
* is cancelled.
*
* SQLDelight will only perform at most one transaction at a time, additional transactions are queued
* and executed on a first come, first serve order.
*
* Performing blocking database operations is not permitted in a coroutine scope other than the
* one received by the suspending block. It is recommended that all [Dao] function invoked within
* the [block] be suspending functions.
*
* The dispatcher used to execute the given [block] will utilize threads from SQLDelight's query executor.
*/
internal suspend fun <T> AndroidDatabaseHandler.withTransaction(block: suspend () -> T): T {
// Use inherited transaction context if available, this allows nested suspending transactions.
val transactionContext =
coroutineContext[TransactionElement]?.transactionDispatcher ?: createTransactionContext()
return withContext(transactionContext) {
val transactionElement = coroutineContext[TransactionElement]!!
transactionElement.acquire()
try {
db.transactionWithResult {
runBlocking(transactionContext) {
block()
}
}
} finally {
transactionElement.release()
}
}
}
/**
* Creates a [CoroutineContext] for performing database operations within a coroutine transaction.
*
* The context is a combination of a dispatcher, a [TransactionElement] and a thread local element.
*
* * The dispatcher will dispatch coroutines to a single thread that is taken over from the SQLDelight
* query executor. If the coroutine context is switched, suspending DAO functions will be able to
* dispatch to the transaction thread.
*
* * The [TransactionElement] serves as an indicator for inherited context, meaning, if there is a
* switch of context, suspending DAO methods will be able to use the indicator to dispatch the
* database operation to the transaction thread.
*
* * The thread local element serves as a second indicator and marks threads that are used to
* execute coroutines within the coroutine transaction, more specifically it allows us to identify
* if a blocking DAO method is invoked within the transaction coroutine. Never assign meaning to
* this value, for now all we care is if its present or not.
*/
private suspend fun AndroidDatabaseHandler.createTransactionContext(): CoroutineContext {
val controlJob = Job()
// make sure to tie the control job to this context to avoid blocking the transaction if
// context get cancelled before we can even start using this job. Otherwise, the acquired
// transaction thread will forever wait for the controlJob to be cancelled.
// see b/148181325
coroutineContext[Job]?.invokeOnCompletion {
controlJob.cancel()
}
val dispatcher = transactionDispatcher.acquireTransactionThread(controlJob)
val transactionElement = TransactionElement(controlJob, dispatcher)
val threadLocalElement =
suspendingTransactionId.asContextElement(System.identityHashCode(controlJob))
return dispatcher + transactionElement + threadLocalElement
}
/**
* Acquires a thread from the executor and returns a [ContinuationInterceptor] to dispatch
* coroutines to the acquired thread. The [controlJob] is used to control the release of the
* thread by cancelling the job.
*/
private suspend fun CoroutineDispatcher.acquireTransactionThread(
controlJob: Job,
): ContinuationInterceptor {
return suspendCancellableCoroutine { continuation ->
continuation.invokeOnCancellation {
// We got cancelled while waiting to acquire a thread, we can't stop our attempt to
// acquire a thread, but we can cancel the controlling job so once it gets acquired it
// is quickly released.
controlJob.cancel()
}
try {
dispatch(EmptyCoroutineContext) {
runBlocking {
// Thread acquired, resume coroutine
continuation.resume(coroutineContext[ContinuationInterceptor]!!)
controlJob.join()
}
}
} catch (ex: RejectedExecutionException) {
// Couldn't acquire a thread, cancel coroutine
continuation.cancel(
IllegalStateException(
"Unable to acquire a thread to perform the database transaction",
ex,
),
)
}
}
}
/**
* A [CoroutineContext.Element] that indicates there is an on-going database transaction.
*/
private class TransactionElement(
private val transactionThreadControlJob: Job,
val transactionDispatcher: ContinuationInterceptor,
) : CoroutineContext.Element {
companion object Key : CoroutineContext.Key<TransactionElement>
override val key: CoroutineContext.Key<TransactionElement>
get() = TransactionElement
/**
* Number of transactions (including nested ones) started with this element.
* Call [acquire] to increase the count and [release] to decrease it. If the count reaches zero
* when [release] is invoked then the transaction job is cancelled and the transaction thread
* is released.
*/
private val referenceCount = AtomicInteger(0)
fun acquire() {
referenceCount.incrementAndGet()
}
fun release() {
val count = referenceCount.decrementAndGet()
if (count < 0) {
throw IllegalStateException("Transaction was never started or was already released")
} else if (count == 0) {
// Cancel the job that controls the transaction thread, causing it to be released.
transactionThreadControlJob.cancel()
}
}
}
@@ -1,12 +0,0 @@
package eu.kanade.data.category
import eu.kanade.domain.category.model.Category
val categoryMapper: (Long, String, Long, Long) -> Category = { id, name, order, flags ->
Category(
id = id,
name = name,
order = order,
flags = flags,
)
}
@@ -1,87 +0,0 @@
package eu.kanade.data.category
import eu.kanade.data.DatabaseHandler
import eu.kanade.domain.category.model.Category
import eu.kanade.domain.category.model.CategoryUpdate
import eu.kanade.domain.category.repository.CategoryRepository
import eu.kanade.tachiyomi.Database
import kotlinx.coroutines.flow.Flow
class CategoryRepositoryImpl(
private val handler: DatabaseHandler,
) : CategoryRepository {
override suspend fun get(id: Long): Category? {
return handler.awaitOneOrNull { categoriesQueries.getCategory(id, categoryMapper) }
}
override suspend fun getAll(): List<Category> {
return handler.awaitList { categoriesQueries.getCategories(categoryMapper) }
}
override fun getAllAsFlow(): Flow<List<Category>> {
return handler.subscribeToList { categoriesQueries.getCategories(categoryMapper) }
}
override suspend fun getCategoriesByMangaId(mangaId: Long): List<Category> {
return handler.awaitList {
categoriesQueries.getCategoriesByMangaId(mangaId, categoryMapper)
}
}
override fun getCategoriesByMangaIdAsFlow(mangaId: Long): Flow<List<Category>> {
return handler.subscribeToList {
categoriesQueries.getCategoriesByMangaId(mangaId, categoryMapper)
}
}
// SY -->
override suspend fun insert(category: Category): Long {
return handler.awaitOne(true) {
categoriesQueries.insert(
name = category.name,
order = category.order,
flags = category.flags,
)
categoriesQueries.selectLastInsertedRowId()
}
}
// SY <--
override suspend fun updatePartial(update: CategoryUpdate) {
handler.await {
updatePartialBlocking(update)
}
}
override suspend fun updatePartial(updates: List<CategoryUpdate>) {
handler.await(inTransaction = true) {
for (update in updates) {
updatePartialBlocking(update)
}
}
}
private fun Database.updatePartialBlocking(update: CategoryUpdate) {
categoriesQueries.update(
name = update.name,
order = update.order,
flags = update.flags,
categoryId = update.id,
)
}
override suspend fun updateAllFlags(flags: Long?) {
handler.await {
categoriesQueries.updateAllFlags(flags)
}
}
override suspend fun delete(categoryId: Long) {
handler.await {
categoriesQueries.delete(
categoryId = categoryId,
)
}
}
}
@@ -1,21 +0,0 @@
package eu.kanade.data.chapter
import eu.kanade.domain.chapter.model.Chapter
val chapterMapper: (Long, Long, String, String, String?, Boolean, Boolean, Long, Float, Long, Long, Long) -> Chapter =
{ id, mangaId, url, name, scanlator, read, bookmark, lastPageRead, chapterNumber, sourceOrder, dateFetch, dateUpload ->
Chapter(
id = id,
mangaId = mangaId,
read = read,
bookmark = bookmark,
lastPageRead = lastPageRead,
dateFetch = dateFetch,
sourceOrder = sourceOrder,
url = url,
name = name,
dateUpload = dateUpload,
chapterNumber = chapterNumber,
scanlator = scanlator,
)
}
@@ -1,113 +0,0 @@
package eu.kanade.data.chapter
import eu.kanade.data.DatabaseHandler
import eu.kanade.domain.chapter.model.Chapter
import eu.kanade.domain.chapter.model.ChapterUpdate
import eu.kanade.domain.chapter.repository.ChapterRepository
import eu.kanade.tachiyomi.util.system.logcat
import eu.kanade.tachiyomi.util.system.toLong
import kotlinx.coroutines.flow.Flow
import logcat.LogPriority
class ChapterRepositoryImpl(
private val handler: DatabaseHandler,
) : ChapterRepository {
override suspend fun addAll(chapters: List<Chapter>): List<Chapter> {
return try {
handler.await(inTransaction = true) {
chapters.map { chapter ->
chaptersQueries.insert(
chapter.mangaId,
chapter.url,
chapter.name,
chapter.scanlator,
chapter.read,
chapter.bookmark,
chapter.lastPageRead,
chapter.chapterNumber,
chapter.sourceOrder,
chapter.dateFetch,
chapter.dateUpload,
)
val lastInsertId = chaptersQueries.selectLastInsertedRowId().executeAsOne()
chapter.copy(id = lastInsertId)
}
}
} catch (e: Exception) {
logcat(LogPriority.ERROR, e)
emptyList()
}
}
override suspend fun update(chapterUpdate: ChapterUpdate) {
partialUpdate(chapterUpdate)
}
override suspend fun updateAll(chapterUpdates: List<ChapterUpdate>) {
partialUpdate(*chapterUpdates.toTypedArray())
}
private suspend fun partialUpdate(vararg chapterUpdates: ChapterUpdate) {
handler.await(inTransaction = true) {
chapterUpdates.forEach { chapterUpdate ->
chaptersQueries.update(
mangaId = chapterUpdate.mangaId,
url = chapterUpdate.url,
name = chapterUpdate.name,
scanlator = chapterUpdate.scanlator,
read = chapterUpdate.read?.toLong(),
bookmark = chapterUpdate.bookmark?.toLong(),
lastPageRead = chapterUpdate.lastPageRead,
chapterNumber = chapterUpdate.chapterNumber?.toDouble(),
sourceOrder = chapterUpdate.sourceOrder,
dateFetch = chapterUpdate.dateFetch,
dateUpload = chapterUpdate.dateUpload,
chapterId = chapterUpdate.id,
)
}
}
}
override suspend fun removeChaptersWithIds(chapterIds: List<Long>) {
try {
handler.await { chaptersQueries.removeChaptersWithIds(chapterIds) }
} catch (e: Exception) {
logcat(LogPriority.ERROR, e)
}
}
override suspend fun getChapterByMangaId(mangaId: Long): List<Chapter> {
return handler.awaitList { chaptersQueries.getChaptersByMangaId(mangaId, chapterMapper) }
}
override suspend fun getBookmarkedChaptersByMangaId(mangaId: Long): List<Chapter> {
return handler.awaitList { chaptersQueries.getBookmarkedChaptersByMangaId(mangaId, chapterMapper) }
}
override suspend fun getChapterById(id: Long): Chapter? {
return handler.awaitOneOrNull { chaptersQueries.getChapterById(id, chapterMapper) }
}
override suspend fun getChapterByMangaIdAsFlow(mangaId: Long): Flow<List<Chapter>> {
return handler.subscribeToList { chaptersQueries.getChaptersByMangaId(mangaId, chapterMapper) }
}
override suspend fun getChapterByUrlAndMangaId(url: String, mangaId: Long): Chapter? {
return handler.awaitOneOrNull { chaptersQueries.getChapterByUrlAndMangaId(url, mangaId, chapterMapper) }
}
// SY -->
override suspend fun getChapterByUrl(url: String): List<Chapter> {
return handler.awaitList { chaptersQueries.getChapterByUrl(url, chapterMapper) }
}
override suspend fun getMergedChapterByMangaId(mangaId: Long): List<Chapter> {
return handler.awaitList { chaptersQueries.getMergedChaptersByMangaId(mangaId, chapterMapper) }
}
override suspend fun getMergedChapterByMangaIdAsFlow(mangaId: Long): Flow<List<Chapter>> {
return handler.subscribeToList { chaptersQueries.getMergedChaptersByMangaId(mangaId, chapterMapper) }
}
// SY <--
}
@@ -1,47 +0,0 @@
package eu.kanade.data.chapter
object CleanupChapterName {
fun await(chapterName: String, mangaTitle: String): String {
return chapterName
.trim()
.removePrefix(mangaTitle)
.trim(*CHAPTER_TRIM_CHARS)
}
private val CHAPTER_TRIM_CHARS = arrayOf(
// Whitespace
' ',
'\u0009',
'\u000A',
'\u000B',
'\u000C',
'\u000D',
'\u0020',
'\u0085',
'\u00A0',
'\u1680',
'\u2000',
'\u2001',
'\u2002',
'\u2003',
'\u2004',
'\u2005',
'\u2006',
'\u2007',
'\u2008',
'\u2009',
'\u200A',
'\u2028',
'\u2029',
'\u202F',
'\u205F',
'\u3000',
// Separators
'-',
'_',
',',
':',
).toCharArray()
}
@@ -1,14 +0,0 @@
package eu.kanade.data.exh
import exh.favorites.sql.models.FavoriteEntry
val favoriteEntryMapper: (Long, String, String, String, Long) -> FavoriteEntry =
{ id, title, gid, token, category ->
FavoriteEntry(
id = id,
title = title,
gid = gid,
token = token,
category = category.toInt(),
)
}
@@ -1,13 +0,0 @@
package eu.kanade.data.exh
import exh.savedsearches.models.FeedSavedSearch
val feedSavedSearchMapper: (Long, Long, Long?, Boolean) -> FeedSavedSearch =
{ id, source, savedSearch, global ->
FeedSavedSearch(
id = id,
source = source,
savedSearch = savedSearch,
global = global,
)
}
@@ -1,20 +0,0 @@
package eu.kanade.data.exh
import exh.merged.sql.models.MergedMangaReference
val mergedMangaReferenceMapper =
{ id: Long, isInfoManga: Boolean, getChapterUpdates: Boolean, chapterSortMode: Long, chapterPriority: Long, downloadChapters: Boolean, mergeId: Long, mergeUrl: String, mangaId: Long?, mangaUrl: String, mangaSourceId: Long ->
MergedMangaReference(
id = id,
isInfoManga = isInfoManga,
getChapterUpdates = getChapterUpdates,
chapterSortMode = chapterSortMode.toInt(),
chapterPriority = chapterPriority.toInt(),
downloadChapters = downloadChapters,
mergeId = mergeId,
mergeUrl = mergeUrl,
mangaId = mangaId,
mangaUrl = mangaUrl,
mangaSourceId = mangaSourceId,
)
}
@@ -1,14 +0,0 @@
package eu.kanade.data.exh
import exh.savedsearches.models.SavedSearch
val savedSearchMapper: (Long, Long, String, String?, String?) -> SavedSearch =
{ id, source, name, query, filtersJson ->
SavedSearch(
id = id,
source = source,
name = name,
query = query,
filtersJson = filtersJson,
)
}
@@ -1,14 +0,0 @@
package eu.kanade.data.exh
import exh.metadata.sql.models.SearchMetadata
val searchMetadataMapper: (Long, String?, String, String?, Int) -> SearchMetadata =
{ mangaId, uploader, extra, indexedExtra, extraVersion ->
SearchMetadata(
mangaId = mangaId,
uploader = uploader,
extra = extra,
indexedExtra = indexedExtra,
extraVersion = extraVersion,
)
}
@@ -1,14 +0,0 @@
package eu.kanade.data.exh
import exh.metadata.sql.models.SearchTag
val searchTagMapper: (Long, Long, String?, String, Int) -> SearchTag =
{ id, mangaId, namespace, name, type ->
SearchTag(
id = id,
mangaId = mangaId,
namespace = namespace,
name = name,
type = type,
)
}
@@ -1,13 +0,0 @@
package eu.kanade.data.exh
import exh.metadata.sql.models.SearchTitle
val searchTitleMapper: (Long, Long, String, Int) -> SearchTitle =
{ id, mangaId, title, type ->
SearchTitle(
id = id,
mangaId = mangaId,
title = title,
type = type,
)
}
@@ -1,37 +0,0 @@
package eu.kanade.data.history
import eu.kanade.domain.history.model.History
import eu.kanade.domain.history.model.HistoryWithRelations
import eu.kanade.domain.manga.model.MangaCover
import java.util.Date
val historyMapper: (Long, Long, Date?, Long) -> History = { id, chapterId, readAt, readDuration ->
History(
id = id,
chapterId = chapterId,
readAt = readAt,
readDuration = readDuration,
)
}
val historyWithRelationsMapper: (Long, Long, Long, String, String?, Long, Boolean, Long, Float, Date?, Long) -> HistoryWithRelations = {
historyId, mangaId, chapterId, title, thumbnailUrl, sourceId, isFavorite, coverLastModified, chapterNumber, readAt, readDuration ->
HistoryWithRelations(
id = historyId,
chapterId = chapterId,
mangaId = mangaId,
// SY -->
ogTitle = title,
// SY <--
chapterNumber = chapterNumber,
readAt = readAt,
readDuration = readDuration,
coverData = MangaCover(
mangaId = mangaId,
sourceId = sourceId,
isMangaFavorite = isFavorite,
url = thumbnailUrl,
lastModified = coverLastModified,
),
)
}
@@ -1,77 +0,0 @@
package eu.kanade.data.history
import eu.kanade.data.DatabaseHandler
import eu.kanade.domain.history.model.History
import eu.kanade.domain.history.model.HistoryUpdate
import eu.kanade.domain.history.model.HistoryWithRelations
import eu.kanade.domain.history.repository.HistoryRepository
import eu.kanade.tachiyomi.util.system.logcat
import kotlinx.coroutines.flow.Flow
import logcat.LogPriority
class HistoryRepositoryImpl(
private val handler: DatabaseHandler,
) : HistoryRepository {
override fun getHistory(query: String): Flow<List<HistoryWithRelations>> {
return handler.subscribeToList {
historyViewQueries.history(query, historyWithRelationsMapper)
}
}
override suspend fun getLastHistory(): HistoryWithRelations? {
return handler.awaitOneOrNull {
historyViewQueries.getLatestHistory(historyWithRelationsMapper)
}
}
override suspend fun getTotalReadDuration(): Long {
return handler.awaitOne { historyQueries.getReadDuration() }
}
override suspend fun resetHistory(historyId: Long) {
try {
handler.await { historyQueries.resetHistoryById(historyId) }
} catch (e: Exception) {
logcat(LogPriority.ERROR, throwable = e)
}
}
override suspend fun resetHistoryByMangaId(mangaId: Long) {
try {
handler.await { historyQueries.resetHistoryByMangaId(mangaId) }
} catch (e: Exception) {
logcat(LogPriority.ERROR, throwable = e)
}
}
override suspend fun deleteAllHistory(): Boolean {
return try {
handler.await { historyQueries.removeAllHistory() }
true
} catch (e: Exception) {
logcat(LogPriority.ERROR, throwable = e)
false
}
}
override suspend fun upsertHistory(historyUpdate: HistoryUpdate) {
try {
handler.await {
historyQueries.upsert(
historyUpdate.chapterId,
historyUpdate.readAt,
historyUpdate.sessionReadDuration,
)
}
} catch (e: Exception) {
logcat(LogPriority.ERROR, throwable = e)
}
}
// SY -->
override suspend fun getByMangaId(mangaId: Long): List<History> {
return handler.awaitList { historyQueries.getHistoryByMangaId(mangaId, historyMapper) }
}
// SY <--
}
@@ -1,26 +0,0 @@
package eu.kanade.data.manga
import eu.kanade.data.DatabaseHandler
import eu.kanade.data.exh.favoriteEntryMapper
import eu.kanade.domain.manga.repository.FavoritesEntryRepository
import exh.favorites.sql.models.FavoriteEntry
class FavoritesEntryRepositoryImpl(
private val handler: DatabaseHandler,
) : FavoritesEntryRepository {
override suspend fun deleteAll() {
handler.await { eh_favoritesQueries.deleteAll() }
}
override suspend fun insertAll(favoriteEntries: List<FavoriteEntry>) {
handler.await(true) {
favoriteEntries.forEach {
eh_favoritesQueries.insertEhFavorites(it.id, it.title, it.gid, it.token, it.category.toLong())
}
}
}
override suspend fun selectAll(): List<FavoriteEntry> {
return handler.awaitList { eh_favoritesQueries.selectAll(favoriteEntryMapper) }
}
}
@@ -1,123 +0,0 @@
package eu.kanade.data.manga
import com.squareup.sqldelight.Query
import com.squareup.sqldelight.db.SqlCursor
import com.squareup.sqldelight.db.SqlDriver
import com.squareup.sqldelight.internal.copyOnWriteList
import eu.kanade.data.listOfStringsAdapter
import eu.kanade.data.listOfStringsAndAdapter
import eu.kanade.data.updateStrategyAdapter
import eu.kanade.domain.library.model.LibraryManga
import exh.source.MERGED_SOURCE_ID
private val mapper = { cursor: SqlCursor ->
LibraryManga(
manga = mangaMapper(
cursor.getLong(0)!!,
cursor.getLong(1)!!,
cursor.getString(2)!!,
cursor.getString(3),
cursor.getString(4),
cursor.getString(5),
cursor.getString(6)?.let(listOfStringsAdapter::decode),
cursor.getString(7)!!,
cursor.getLong(8)!!,
cursor.getString(9),
cursor.getLong(10)!! == 1L,
cursor.getLong(11) ?: 0,
null,
cursor.getLong(13)!! == 1L,
cursor.getLong(14)!!,
cursor.getLong(15)!!,
cursor.getLong(16)!!,
cursor.getLong(17)!!,
cursor.getString(18)?.let(listOfStringsAndAdapter::decode),
updateStrategyAdapter.decode(cursor.getLong(19)!!),
),
totalChapters = cursor.getLong(20)!!,
readCount = cursor.getLong(21)!!,
latestUpload = cursor.getLong(22)!!,
chapterFetchedAt = cursor.getLong(23)!!,
lastRead = cursor.getLong(24)!!,
bookmarkCount = cursor.getLong(25)!!,
category = cursor.getLong(26)!!,
)
}
class LibraryQuery(val driver: SqlDriver) : Query<LibraryManga>(copyOnWriteList(), mapper) {
override fun execute(): SqlCursor {
return driver.executeQuery(
null,
"""
SELECT
M.*,
coalesce(C.total, 0) AS totalCount,
coalesce(C.readCount, 0) AS readCount,
coalesce(C.latestUpload, 0) AS latestUpload,
coalesce(C.fetchedAt, 0) AS chapterFetchedAt,
coalesce(C.lastRead, 0) AS lastRead,
coalesce(C.bookmarkCount, 0) AS bookmarkCount,
coalesce(MC.category_id, 0) AS category
FROM mangas M
LEFT JOIN(
SELECT
chapters.manga_id,
count(*) AS total,
sum(read) AS readCount,
coalesce(max(chapters.date_upload), 0) AS latestUpload,
coalesce(max(history.last_read), 0) AS lastRead,
coalesce(max(chapters.date_fetch), 0) AS fetchedAt,
sum(chapters.bookmark) AS bookmarkCount
FROM chapters
LEFT JOIN history
ON chapters._id = history.chapter_id
GROUP BY chapters.manga_id
) AS C
ON M._id = C.manga_id
LEFT JOIN mangas_categories AS MC
ON MC.manga_id = M._id
WHERE M.favorite = 1 AND M.source <> $MERGED_SOURCE_ID
UNION
SELECT
M.*,
coalesce(C.total, 0) AS totalCount,
coalesce(C.readCount, 0) AS readCount,
coalesce(C.latestUpload, 0) AS latestUpload,
coalesce(C.fetchedAt, 0) AS chapterFetchedAt,
coalesce(C.lastRead, 0) AS lastRead,
coalesce(C.bookmarkCount, 0) AS bookmarkCount,
coalesce(MC.category_id, 0) AS category
FROM mangas M
LEFT JOIN (
SELECT merged.manga_id,merged.merge_id
FROM merged
GROUP BY merged.merge_id
) as ME
ON ME.merge_id = M._id
LEFT JOIN(
SELECT
ME.merge_id,
count(*) AS total,
sum(read) AS readCount,
coalesce(max(chapters.date_upload), 0) AS latestUpload,
coalesce(max(history.last_read), 0) AS lastRead,
coalesce(max(chapters.date_fetch), 0) AS fetchedAt,
sum(chapters.bookmark) AS bookmarkCount
FROM chapters
LEFT JOIN history
ON chapters._id = history.chapter_id
LEFT JOIN merged as ME
ON ME.manga_id = chapters.manga_id
GROUP BY ME.merge_id
) AS C
ON ME.merge_id = C.merge_id
LEFT JOIN mangas_categories AS MC
ON MC.manga_id = M._id
WHERE M.favorite = 1 AND M.source = $MERGED_SOURCE_ID;
""".trimIndent(),
1,
)
}
override fun toString(): String = "LibraryQuery.sq:get"
}
@@ -1,71 +0,0 @@
package eu.kanade.data.manga
import eu.kanade.domain.library.model.LibraryManga
import eu.kanade.domain.manga.model.Manga
import eu.kanade.tachiyomi.source.model.UpdateStrategy
val mangaMapper: (Long, Long, String, String?, String?, String?, List<String>?, String, Long, String?, Boolean, Long?, Long?, Boolean, Long, Long, Long, Long, List<String>?, UpdateStrategy) -> Manga =
{ id, source, url, artist, author, description, genre, title, status, thumbnailUrl, favorite, lastUpdate, _, initialized, viewerFlags, chapterFlags, coverLastModified, dateAdded, filteredScanlators, updateStrategy ->
Manga(
id = id,
source = source,
favorite = favorite,
lastUpdate = lastUpdate ?: 0,
dateAdded = dateAdded,
viewerFlags = viewerFlags,
chapterFlags = chapterFlags,
coverLastModified = coverLastModified,
url = url,
// SY -->
ogTitle = title,
ogArtist = artist,
ogAuthor = author,
ogDescription = description,
ogGenre = genre,
ogStatus = status,
// SY <--
thumbnailUrl = thumbnailUrl,
updateStrategy = updateStrategy,
initialized = initialized,
// SY -->
filteredScanlators = filteredScanlators,
// SY <--
)
}
val libraryManga: (Long, Long, String, String?, String?, String?, List<String>?, String, Long, String?, Boolean, Long?, Long?, Boolean, Long, Long, Long, Long, List<String>?, UpdateStrategy, Long, Long, Long, Long, Long, Long, Long) -> LibraryManga =
{ id, source, url, artist, author, description, genre, title, status, thumbnailUrl, favorite, lastUpdate, nextUpdate, initialized, viewerFlags, chapterFlags, coverLastModified, dateAdded, filteredScanlators, updateStrategy, totalCount, readCount, latestUpload, chapterFetchedAt, lastRead, bookmarkCount, category ->
LibraryManga(
manga = mangaMapper(
id,
source,
url,
artist,
author,
description,
genre,
title,
status,
thumbnailUrl,
favorite,
lastUpdate,
nextUpdate,
initialized,
viewerFlags,
chapterFlags,
coverLastModified,
dateAdded,
// SY -->
filteredScanlators,
// SY <--
updateStrategy,
),
category = category,
totalChapters = totalCount,
readCount = readCount,
bookmarkCount = bookmarkCount,
latestUpload = latestUpload,
chapterFetchedAt = chapterFetchedAt,
lastRead = lastRead,
)
}
@@ -1,129 +0,0 @@
package eu.kanade.data.manga
import eu.kanade.data.DatabaseHandler
import eu.kanade.data.exh.mergedMangaReferenceMapper
import eu.kanade.domain.manga.model.Manga
import eu.kanade.domain.manga.model.MergeMangaSettingsUpdate
import eu.kanade.domain.manga.repository.MangaMergeRepository
import eu.kanade.tachiyomi.util.system.logcat
import eu.kanade.tachiyomi.util.system.toLong
import exh.merged.sql.models.MergedMangaReference
import kotlinx.coroutines.flow.Flow
import logcat.LogPriority
class MangaMergeRepositoryImpl(
private val handler: DatabaseHandler,
) : MangaMergeRepository {
override suspend fun getMergedManga(): List<Manga> {
return handler.awaitList { mergedQueries.selectAllMergedMangas(mangaMapper) }
}
override suspend fun subscribeMergedManga(): Flow<List<Manga>> {
return handler.subscribeToList { mergedQueries.selectAllMergedMangas(mangaMapper) }
}
override suspend fun getMergedMangaById(id: Long): List<Manga> {
return handler.awaitList { mergedQueries.selectMergedMangasById(id, mangaMapper) }
}
override suspend fun subscribeMergedMangaById(id: Long): Flow<List<Manga>> {
return handler.subscribeToList { mergedQueries.selectMergedMangasById(id, mangaMapper) }
}
override suspend fun getReferencesById(id: Long): List<MergedMangaReference> {
return handler.awaitList { mergedQueries.selectByMergeId(id, mergedMangaReferenceMapper) }
}
override suspend fun subscribeReferencesById(id: Long): Flow<List<MergedMangaReference>> {
return handler.subscribeToList { mergedQueries.selectByMergeId(id, mergedMangaReferenceMapper) }
}
override suspend fun updateSettings(update: MergeMangaSettingsUpdate): Boolean {
return try {
partialUpdate(update)
true
} catch (e: Exception) {
logcat(LogPriority.ERROR, e)
false
}
}
override suspend fun updateAllSettings(values: List<MergeMangaSettingsUpdate>): Boolean {
return try {
partialUpdate(*values.toTypedArray())
true
} catch (e: Exception) {
logcat(LogPriority.ERROR, e)
false
}
}
private suspend fun partialUpdate(vararg values: MergeMangaSettingsUpdate) {
handler.await(inTransaction = true) {
values.forEach { value ->
mergedQueries.updateSettingsById(
id = value.id,
getChapterUpdates = value.getChapterUpdates?.toLong(),
downloadChapters = value.downloadChapters?.toLong(),
infoManga = value.isInfoManga?.toLong(),
chapterPriority = value.chapterPriority?.toLong(),
chapterSortMode = value.chapterSortMode?.toLong(),
)
}
}
}
override suspend fun insert(reference: MergedMangaReference): Long? {
return handler.awaitOneOrNull {
mergedQueries.insert(
infoManga = reference.isInfoManga,
getChapterUpdates = reference.getChapterUpdates,
chapterSortMode = reference.chapterSortMode.toLong(),
chapterPriority = reference.chapterPriority.toLong(),
downloadChapters = reference.downloadChapters,
mergeId = reference.mergeId!!,
mergeUrl = reference.mergeUrl,
mangaId = reference.mangaId,
mangaUrl = reference.mangaUrl,
mangaSource = reference.mangaSourceId,
)
mergedQueries.selectLastInsertedRowId()
}
}
override suspend fun insertAll(references: List<MergedMangaReference>) {
handler.await(true) {
references.forEach { reference ->
mergedQueries.insert(
infoManga = reference.isInfoManga,
getChapterUpdates = reference.getChapterUpdates,
chapterSortMode = reference.chapterSortMode.toLong(),
chapterPriority = reference.chapterPriority.toLong(),
downloadChapters = reference.downloadChapters,
mergeId = reference.mergeId!!,
mergeUrl = reference.mergeUrl,
mangaId = reference.mangaId,
mangaUrl = reference.mangaUrl,
mangaSource = reference.mangaSourceId,
)
}
}
}
override suspend fun deleteById(id: Long) {
handler.await {
mergedQueries.deleteById(id)
}
}
override suspend fun deleteByMergeId(mergeId: Long) {
handler.await {
mergedQueries.deleteByMergeId(mergeId)
}
}
override suspend fun getMergeMangaForDownloading(mergeId: Long): List<Manga> {
return handler.awaitList { mergedQueries.selectMergedMangasForDownloadingById(mergeId, mangaMapper) }
}
}
@@ -1,74 +0,0 @@
package eu.kanade.data.manga
import eu.kanade.data.DatabaseHandler
import eu.kanade.data.exh.searchMetadataMapper
import eu.kanade.data.exh.searchTagMapper
import eu.kanade.data.exh.searchTitleMapper
import eu.kanade.domain.manga.model.Manga
import eu.kanade.domain.manga.repository.MangaMetadataRepository
import exh.metadata.metadata.base.FlatMetadata
import exh.metadata.sql.models.SearchMetadata
import exh.metadata.sql.models.SearchTag
import exh.metadata.sql.models.SearchTitle
import exh.source.EH_SOURCE_ID
import exh.source.EXH_SOURCE_ID
import kotlinx.coroutines.flow.Flow
class MangaMetadataRepositoryImpl(
private val handler: DatabaseHandler,
) : MangaMetadataRepository {
override suspend fun getMetadataById(id: Long): SearchMetadata? {
return handler.awaitOneOrNull { search_metadataQueries.selectByMangaId(id, searchMetadataMapper) }
}
override fun subscribeMetadataById(id: Long): Flow<SearchMetadata?> {
return handler.subscribeToOneOrNull { search_metadataQueries.selectByMangaId(id, searchMetadataMapper) }
}
override suspend fun getTagsById(id: Long): List<SearchTag> {
return handler.awaitList { search_tagsQueries.selectByMangaId(id, searchTagMapper) }
}
override fun subscribeTagsById(id: Long): Flow<List<SearchTag>> {
return handler.subscribeToList { search_tagsQueries.selectByMangaId(id, searchTagMapper) }
}
override suspend fun getTitlesById(id: Long): List<SearchTitle> {
return handler.awaitList { search_titlesQueries.selectByMangaId(id, searchTitleMapper) }
}
override fun subscribeTitlesById(id: Long): Flow<List<SearchTitle>> {
return handler.subscribeToList { search_titlesQueries.selectByMangaId(id, searchTitleMapper) }
}
override suspend fun insertFlatMetadata(flatMetadata: FlatMetadata) {
require(flatMetadata.metadata.mangaId != -1L)
handler.await(true) {
flatMetadata.metadata.run {
search_metadataQueries.upsert(mangaId, uploader, extra, indexedExtra, extraVersion)
}
search_tagsQueries.deleteByManga(flatMetadata.metadata.mangaId)
flatMetadata.tags.forEach {
search_tagsQueries.insert(it.mangaId, it.namespace, it.name, it.type)
}
search_titlesQueries.deleteByManga(flatMetadata.metadata.mangaId)
flatMetadata.titles.forEach {
search_titlesQueries.insert(it.mangaId, it.title, it.type)
}
}
}
override suspend fun getExhFavoriteMangaWithMetadata(): List<Manga> {
return handler.awaitList { mangasQueries.getEhMangaWithMetadata(EH_SOURCE_ID, EXH_SOURCE_ID, mangaMapper) }
}
override suspend fun getIdsOfFavoriteMangaWithMetadata(): List<Long> {
return handler.awaitList { mangasQueries.getIdsOfFavoriteMangaWithMetadata() }
}
override suspend fun getSearchMetadata(): List<SearchMetadata> {
return handler.awaitList { search_metadataQueries.selectAll(searchMetadataMapper) }
}
}
@@ -1,180 +0,0 @@
package eu.kanade.data.manga
import eu.kanade.data.AndroidDatabaseHandler
import eu.kanade.data.DatabaseHandler
import eu.kanade.data.listOfStringsAdapter
import eu.kanade.data.listOfStringsAndAdapter
import eu.kanade.data.updateStrategyAdapter
import eu.kanade.domain.library.model.LibraryManga
import eu.kanade.domain.manga.model.Manga
import eu.kanade.domain.manga.model.MangaUpdate
import eu.kanade.domain.manga.repository.MangaRepository
import eu.kanade.tachiyomi.util.system.logcat
import eu.kanade.tachiyomi.util.system.toLong
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import logcat.LogPriority
class MangaRepositoryImpl(
private val handler: DatabaseHandler,
) : MangaRepository {
override suspend fun getMangaById(id: Long): Manga {
return handler.awaitOne { mangasQueries.getMangaById(id, mangaMapper) }
}
override suspend fun getMangaByIdAsFlow(id: Long): Flow<Manga> {
return handler.subscribeToOne { mangasQueries.getMangaById(id, mangaMapper) }
}
override suspend fun getMangaByUrlAndSourceId(url: String, sourceId: Long): Manga? {
return handler.awaitOneOrNull(inTransaction = true) { mangasQueries.getMangaByUrlAndSource(url, sourceId, mangaMapper) }
}
override fun getMangaByUrlAndSourceIdAsFlow(url: String, sourceId: Long): Flow<Manga?> {
return handler.subscribeToOneOrNull { mangasQueries.getMangaByUrlAndSource(url, sourceId, mangaMapper) }
}
override suspend fun getFavorites(): List<Manga> {
return handler.awaitList { mangasQueries.getFavorites(mangaMapper) }
}
override suspend fun getLibraryManga(): List<LibraryManga> {
return handler.awaitList { (handler as AndroidDatabaseHandler).getLibraryQuery() }
// return handler.awaitList { libraryViewQueries.library(libraryManga) }
}
override fun getLibraryMangaAsFlow(): Flow<List<LibraryManga>> {
return handler.subscribeToList { libraryViewQueries.library(libraryManga) }
// SY -->
.map { getLibraryManga() }
// SY <--
}
override fun getFavoritesBySourceId(sourceId: Long): Flow<List<Manga>> {
return handler.subscribeToList { mangasQueries.getFavoriteBySourceId(sourceId, mangaMapper) }
}
override suspend fun getDuplicateLibraryManga(title: String, sourceId: Long): Manga? {
return handler.awaitOneOrNull {
mangasQueries.getDuplicateLibraryManga(title, sourceId, mangaMapper)
}
}
override suspend fun resetViewerFlags(): Boolean {
return try {
handler.await { mangasQueries.resetViewerFlags() }
true
} catch (e: Exception) {
logcat(LogPriority.ERROR, e)
false
}
}
override suspend fun setMangaCategories(mangaId: Long, categoryIds: List<Long>) {
handler.await(inTransaction = true) {
mangas_categoriesQueries.deleteMangaCategoryByMangaId(mangaId)
categoryIds.map { categoryId ->
mangas_categoriesQueries.insert(mangaId, categoryId)
}
}
}
override suspend fun insert(manga: Manga): Long? {
return handler.awaitOneOrNull(inTransaction = true) {
// SY -->
if (mangasQueries.getIdByUrlAndSource(manga.url, manga.source).executeAsOneOrNull() != null) {
return@awaitOneOrNull mangasQueries.getIdByUrlAndSource(manga.url, manga.source)
}
// SY <--
mangasQueries.insert(
source = manga.source,
url = manga.url,
artist = manga.artist,
author = manga.author,
description = manga.description,
genre = manga.genre,
title = manga.title,
status = manga.status,
thumbnailUrl = manga.thumbnailUrl,
favorite = manga.favorite,
lastUpdate = manga.lastUpdate,
nextUpdate = null,
initialized = manga.initialized,
viewerFlags = manga.viewerFlags,
chapterFlags = manga.chapterFlags,
coverLastModified = manga.coverLastModified,
dateAdded = manga.dateAdded,
// SY -->
filteredScanlators = manga.filteredScanlators,
// SY <--
updateStrategy = manga.updateStrategy,
)
mangasQueries.selectLastInsertedRowId()
}
}
override suspend fun update(update: MangaUpdate): Boolean {
return try {
partialUpdate(update)
true
} catch (e: Exception) {
logcat(LogPriority.ERROR, e)
false
}
}
override suspend fun updateAll(mangaUpdates: List<MangaUpdate>): Boolean {
return try {
partialUpdate(*mangaUpdates.toTypedArray())
true
} catch (e: Exception) {
logcat(LogPriority.ERROR, e)
false
}
}
private suspend fun partialUpdate(vararg mangaUpdates: MangaUpdate) {
handler.await(inTransaction = true) {
mangaUpdates.forEach { value ->
mangasQueries.update(
source = value.source,
url = value.url,
artist = value.artist,
author = value.author,
description = value.description,
genre = value.genre?.let(listOfStringsAdapter::encode),
title = value.title,
status = value.status,
thumbnailUrl = value.thumbnailUrl,
favorite = value.favorite?.toLong(),
lastUpdate = value.lastUpdate,
initialized = value.initialized?.toLong(),
viewer = value.viewerFlags,
chapterFlags = value.chapterFlags,
coverLastModified = value.coverLastModified,
dateAdded = value.dateAdded,
// SY -->
filteredScanlators = value.filteredScanlators?.let(listOfStringsAndAdapter::encode),
// SY <--
mangaId = value.id,
updateStrategy = value.updateStrategy?.let(updateStrategyAdapter::encode),
)
}
}
}
// SY -->
override suspend fun getMangaBySourceId(sourceId: Long): List<Manga> {
return handler.awaitList { mangasQueries.getBySource(sourceId, mangaMapper) }
}
override suspend fun getAll(): List<Manga> {
return handler.awaitList { mangasQueries.getAll(mangaMapper) }
}
override suspend fun deleteManga(mangaId: Long) {
handler.await { mangasQueries.deleteById(mangaId) }
}
// SY <--
}
@@ -1,45 +0,0 @@
package eu.kanade.data.source
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.MetadataMangasPage
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.all.EHentai
import eu.kanade.tachiyomi.util.lang.awaitSingle
import exh.metadata.metadata.base.RaisedSearchMetadata
abstract class EHentaiPagingSource(override val source: EHentai) : SourcePagingSource(source) {
override fun getPageLoadResult(
params: LoadParams<Long>,
mangasPage: MangasPage,
): LoadResult.Page<Long, Pair<SManga, RaisedSearchMetadata?>> {
mangasPage as MetadataMangasPage
val metadata = mangasPage.mangasMetadata
return LoadResult.Page(
data = mangasPage.mangas
.mapIndexed { index, sManga -> sManga to metadata.getOrNull(index) },
prevKey = null,
nextKey = mangasPage.nextKey,
)
}
}
class EHentaiSearchPagingSource(source: EHentai, val query: String, val filters: FilterList) : EHentaiPagingSource(source) {
override suspend fun requestNextPage(currentPage: Int): MangasPage {
return source.fetchSearchManga(currentPage, query, filters).awaitSingle()
}
}
class EHentaiPopularPagingSource(source: EHentai) : EHentaiPagingSource(source) {
override suspend fun requestNextPage(currentPage: Int): MangasPage {
return source.fetchPopularManga(currentPage).awaitSingle()
}
}
class EHentaiLatestPagingSource(source: EHentai) : EHentaiPagingSource(source) {
override suspend fun requestNextPage(currentPage: Int): MangasPage {
return source.fetchLatestUpdates(currentPage).awaitSingle()
}
}
@@ -1,73 +0,0 @@
package eu.kanade.data.source
import eu.kanade.data.DatabaseHandler
import eu.kanade.data.exh.feedSavedSearchMapper
import eu.kanade.data.exh.savedSearchMapper
import eu.kanade.domain.source.repository.FeedSavedSearchRepository
import exh.savedsearches.models.FeedSavedSearch
import exh.savedsearches.models.SavedSearch
import kotlinx.coroutines.flow.Flow
class FeedSavedSearchRepositoryImpl(
private val handler: DatabaseHandler,
) : FeedSavedSearchRepository {
override suspend fun getGlobal(): List<FeedSavedSearch> {
return handler.awaitList { feed_saved_searchQueries.selectAllGlobal(feedSavedSearchMapper) }
}
override fun getGlobalAsFlow(): Flow<List<FeedSavedSearch>> {
return handler.subscribeToList { feed_saved_searchQueries.selectAllGlobal(feedSavedSearchMapper) }
}
override suspend fun getGlobalFeedSavedSearch(): List<SavedSearch> {
return handler.awaitList { feed_saved_searchQueries.selectGlobalFeedSavedSearch(savedSearchMapper) }
}
override suspend fun countGlobal(): Long {
return handler.awaitOne { feed_saved_searchQueries.countGlobal() }
}
override suspend fun getBySourceId(sourceId: Long): List<FeedSavedSearch> {
return handler.awaitList { feed_saved_searchQueries.selectBySource(sourceId, feedSavedSearchMapper) }
}
override fun getBySourceIdAsFlow(sourceId: Long): Flow<List<FeedSavedSearch>> {
return handler.subscribeToList { feed_saved_searchQueries.selectBySource(sourceId, feedSavedSearchMapper) }
}
override suspend fun getBySourceIdFeedSavedSearch(sourceId: Long): List<SavedSearch> {
return handler.awaitList { feed_saved_searchQueries.selectSourceFeedSavedSearch(sourceId, savedSearchMapper) }
}
override suspend fun countBySourceId(sourceId: Long): Long {
return handler.awaitOne { feed_saved_searchQueries.countSourceFeedSavedSearch(sourceId) }
}
override suspend fun delete(feedSavedSearchId: Long) {
handler.await { feed_saved_searchQueries.deleteById(feedSavedSearchId) }
}
override suspend fun insert(feedSavedSearch: FeedSavedSearch): Long {
return handler.awaitOne(true) {
feed_saved_searchQueries.insert(
feedSavedSearch.source,
feedSavedSearch.savedSearch,
feedSavedSearch.global,
)
feed_saved_searchQueries.selectLastInsertedRowId()
}
}
override suspend fun insertAll(feedSavedSearch: List<FeedSavedSearch>) {
return handler.await(true) {
feedSavedSearch.forEach {
feed_saved_searchQueries.insert(
it.source,
it.savedSearch,
it.global,
)
}
}
}
}
@@ -1,3 +0,0 @@
package eu.kanade.data.source
class NoResultsException : Exception()
@@ -1,53 +0,0 @@
package eu.kanade.data.source
import eu.kanade.data.DatabaseHandler
import eu.kanade.data.exh.savedSearchMapper
import eu.kanade.domain.source.repository.SavedSearchRepository
import exh.savedsearches.models.SavedSearch
import kotlinx.coroutines.flow.Flow
class SavedSearchRepositoryImpl(
private val handler: DatabaseHandler,
) : SavedSearchRepository {
override suspend fun getById(savedSearchId: Long): SavedSearch? {
return handler.awaitOneOrNull { saved_searchQueries.selectById(savedSearchId, savedSearchMapper) }
}
override suspend fun getBySourceId(sourceId: Long): List<SavedSearch> {
return handler.awaitList { saved_searchQueries.selectBySource(sourceId, savedSearchMapper) }
}
override fun getBySourceIdAsFlow(sourceId: Long): Flow<List<SavedSearch>> {
return handler.subscribeToList { saved_searchQueries.selectBySource(sourceId, savedSearchMapper) }
}
override suspend fun delete(savedSearchId: Long) {
handler.await { saved_searchQueries.deleteById(savedSearchId) }
}
override suspend fun insert(savedSearch: SavedSearch): Long {
return handler.awaitOne(true) {
saved_searchQueries.insert(
savedSearch.source,
savedSearch.name,
savedSearch.query,
savedSearch.filtersJson,
)
saved_searchQueries.selectLastInsertedRowId()
}
}
override suspend fun insertAll(savedSearch: List<SavedSearch>) {
handler.await(true) {
savedSearch.forEach {
saved_searchQueries.insert(
it.source,
it.name,
it.query,
it.filtersJson,
)
}
}
}
}
@@ -1,23 +0,0 @@
package eu.kanade.data.source
import eu.kanade.data.DatabaseHandler
import eu.kanade.domain.source.model.SourceData
import eu.kanade.domain.source.repository.SourceDataRepository
import kotlinx.coroutines.flow.Flow
class SourceDataRepositoryImpl(
private val handler: DatabaseHandler,
) : SourceDataRepository {
override fun subscribeAll(): Flow<List<SourceData>> {
return handler.subscribeToList { sourcesQueries.findAll(sourceDataMapper) }
}
override suspend fun getSourceData(id: Long): SourceData? {
return handler.awaitOneOrNull { sourcesQueries.findOne(id, sourceDataMapper) }
}
override suspend fun upsertSourceData(id: Long, lang: String, name: String) {
handler.await { sourcesQueries.upsert(id, lang, name) }
}
}
@@ -1,24 +0,0 @@
package eu.kanade.data.source
import eu.kanade.domain.source.model.Source
import eu.kanade.domain.source.model.SourceData
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.SourceManager
val sourceMapper: (eu.kanade.tachiyomi.source.Source) -> Source = { source ->
Source(
source.id,
source.lang,
source.name,
supportsLatest = false,
isStub = source is SourceManager.StubSource,
)
}
val catalogueSourceMapper: (CatalogueSource) -> Source = { source ->
sourceMapper(source).copy(supportsLatest = source.supportsLatest)
}
val sourceDataMapper: (Long, String, String) -> SourceData = { id, lang, name ->
SourceData(id, lang, name)
}
@@ -1,85 +0,0 @@
package eu.kanade.data.source
import androidx.paging.PagingState
import eu.kanade.domain.source.model.SourcePagingSourceType
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.MetadataMangasPage
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.util.lang.awaitSingle
import eu.kanade.tachiyomi.util.lang.withIOContext
import exh.metadata.metadata.base.RaisedSearchMetadata
abstract class SourcePagingSource(
protected open val source: CatalogueSource,
) : SourcePagingSourceType() {
abstract suspend fun requestNextPage(currentPage: Int): MangasPage
override suspend fun load(params: LoadParams<Long>): LoadResult<Long, /*SY --> */ Pair<SManga, RaisedSearchMetadata?>/*SY <-- */> {
val page = params.key ?: 1
val mangasPage = try {
withIOContext {
requestNextPage(page.toInt())
.takeIf { it.mangas.isNotEmpty() }
?: throw NoResultsException()
}
} catch (e: Exception) {
return LoadResult.Error(e)
}
// SY -->
return getPageLoadResult(params, mangasPage)
// SY <--
}
// SY -->
open fun getPageLoadResult(params: LoadParams<Long>, mangasPage: MangasPage): LoadResult.Page<Long, /*SY --> */ Pair<SManga, RaisedSearchMetadata?>/*SY <-- */> {
val page = params.key ?: 1
// SY -->
val metadata = if (mangasPage is MetadataMangasPage) {
mangasPage.mangasMetadata
} else {
emptyList()
}
// SY <--
return LoadResult.Page(
data = mangasPage.mangas
// SY -->
.mapIndexed { index, sManga -> sManga to metadata.getOrNull(index) },
// SY <--
prevKey = null,
nextKey = if (mangasPage.hasNextPage) page + 1 else null,
)
}
// SY <--
override fun getRefreshKey(state: PagingState<Long, /*SY --> */ Pair<SManga, RaisedSearchMetadata?>/*SY <-- */>): Long? {
return state.anchorPosition?.let { anchorPosition ->
val anchorPage = state.closestPageToPosition(anchorPosition)
anchorPage?.prevKey ?: anchorPage?.nextKey
}
}
}
class SourceSearchPagingSource(source: CatalogueSource, val query: String, val filters: FilterList) : SourcePagingSource(source) {
override suspend fun requestNextPage(currentPage: Int): MangasPage {
return source.fetchSearchManga(currentPage, query, filters).awaitSingle()
}
}
class SourcePopularPagingSource(source: CatalogueSource) : SourcePagingSource(source) {
override suspend fun requestNextPage(currentPage: Int): MangasPage {
return source.fetchPopularManga(currentPage).awaitSingle()
}
}
class SourceLatestPagingSource(source: CatalogueSource) : SourcePagingSource(source) {
override suspend fun requestNextPage(currentPage: Int): MangasPage {
return source.fetchLatestUpdates(currentPage).awaitSingle()
}
}
@@ -1,91 +0,0 @@
package eu.kanade.data.source
import eu.kanade.data.DatabaseHandler
import eu.kanade.domain.source.model.Source
import eu.kanade.domain.source.model.SourcePagingSourceType
import eu.kanade.domain.source.model.SourceWithCount
import eu.kanade.domain.source.repository.SourceRepository
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.online.all.EHentai
import exh.source.MERGED_SOURCE_ID
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
class SourceRepositoryImpl(
private val sourceManager: SourceManager,
private val handler: DatabaseHandler,
) : SourceRepository {
override fun getSources(): Flow<List<Source>> {
return sourceManager.catalogueSources.map { sources ->
sources.map(catalogueSourceMapper)
}
}
override fun getOnlineSources(): Flow<List<Source>> {
return sourceManager.onlineSources.map { sources ->
sources.map(sourceMapper)
}
}
override fun getSourcesWithFavoriteCount(): Flow<List<Pair<Source, Long>>> {
val sourceIdWithFavoriteCount = handler.subscribeToList { mangasQueries.getSourceIdWithFavoriteCount() }
return sourceIdWithFavoriteCount.map { sourceIdsWithCount ->
sourceIdsWithCount
.filterNot { it.source == LocalSource.ID /* SY --> */ || it.source == MERGED_SOURCE_ID /* SY <-- */ }
.map { (sourceId, count) ->
val source = sourceManager.getOrStub(sourceId).run {
sourceMapper(this)
}
source to count
}
}
}
override fun getSourcesWithNonLibraryManga(): Flow<List<SourceWithCount>> {
val sourceIdWithNonLibraryManga = handler.subscribeToList { mangasQueries.getSourceIdsWithNonLibraryManga() }
return sourceIdWithNonLibraryManga.map { sourceId ->
sourceId.map { (sourceId, count) ->
val source = sourceManager.getOrStub(sourceId)
SourceWithCount(sourceMapper(source), count)
}
}
}
override fun search(
sourceId: Long,
query: String,
filterList: FilterList,
): SourcePagingSourceType {
val source = sourceManager.get(sourceId) as CatalogueSource
// SY -->
if (source is EHentai) {
return EHentaiSearchPagingSource(source, query, filterList)
}
// SY <--
return SourceSearchPagingSource(source, query, filterList)
}
override fun getPopular(sourceId: Long): SourcePagingSourceType {
val source = sourceManager.get(sourceId) as CatalogueSource
// SY -->
if (source is EHentai) {
return EHentaiPopularPagingSource(source)
}
// SY <--
return SourcePopularPagingSource(source)
}
override fun getLatest(sourceId: Long): SourcePagingSourceType {
val source = sourceManager.get(sourceId) as CatalogueSource
// SY -->
if (source is EHentai) {
return EHentaiLatestPagingSource(source)
}
// SY <--
return SourceLatestPagingSource(source)
}
}
@@ -1,22 +0,0 @@
package eu.kanade.data.track
import eu.kanade.domain.track.model.Track
val trackMapper: (Long, Long, Long, Long, Long?, String, Double, Long, Long, Float, String, Long, Long) -> Track =
{ id, mangaId, syncId, remoteId, libraryId, title, lastChapterRead, totalChapters, status, score, remoteUrl, startDate, finishDate ->
Track(
id = id,
mangaId = mangaId,
syncId = syncId,
remoteId = remoteId,
libraryId = libraryId,
title = title,
lastChapterRead = lastChapterRead,
totalChapters = totalChapters,
status = status,
score = score,
remoteUrl = remoteUrl,
startDate = startDate,
finishDate = finishDate,
)
}
@@ -1,85 +0,0 @@
package eu.kanade.data.track
import eu.kanade.data.DatabaseHandler
import eu.kanade.domain.track.model.Track
import eu.kanade.domain.track.repository.TrackRepository
import kotlinx.coroutines.flow.Flow
class TrackRepositoryImpl(
private val handler: DatabaseHandler,
) : TrackRepository {
override suspend fun getTrackById(id: Long): Track? {
return handler.awaitOneOrNull { manga_syncQueries.getTrackById(id, trackMapper) }
}
// SY -->
override suspend fun getTracks(): List<Track> {
return handler.awaitList {
manga_syncQueries.getTracks(trackMapper)
}
}
override suspend fun getTracksByMangaIds(mangaIds: List<Long>): List<Track> {
return handler.awaitList {
manga_syncQueries.getTracksByMangaIds(mangaIds, trackMapper)
}
}
// SY <--
override suspend fun getTracksByMangaId(mangaId: Long): List<Track> {
return handler.awaitList {
manga_syncQueries.getTracksByMangaId(mangaId, trackMapper)
}
}
override fun getTracksAsFlow(): Flow<List<Track>> {
return handler.subscribeToList {
manga_syncQueries.getTracks(trackMapper)
}
}
override fun getTracksByMangaIdAsFlow(mangaId: Long): Flow<List<Track>> {
return handler.subscribeToList {
manga_syncQueries.getTracksByMangaId(mangaId, trackMapper)
}
}
override suspend fun delete(mangaId: Long, syncId: Long) {
handler.await {
manga_syncQueries.delete(
mangaId = mangaId,
syncId = syncId,
)
}
}
override suspend fun insert(track: Track) {
insertValues(track)
}
override suspend fun insertAll(tracks: List<Track>) {
insertValues(*tracks.toTypedArray())
}
private suspend fun insertValues(vararg tracks: Track) {
handler.await(inTransaction = true) {
tracks.forEach { mangaTrack ->
manga_syncQueries.insert(
mangaId = mangaTrack.mangaId,
syncId = mangaTrack.syncId,
remoteId = mangaTrack.remoteId,
libraryId = mangaTrack.libraryId,
title = mangaTrack.title,
lastChapterRead = mangaTrack.lastChapterRead,
totalChapters = mangaTrack.totalChapters,
status = mangaTrack.status,
score = mangaTrack.score,
remoteUrl = mangaTrack.remoteUrl,
startDate = mangaTrack.startDate,
finishDate = mangaTrack.finishDate,
)
}
}
}
}
@@ -1,28 +0,0 @@
package eu.kanade.data.updates
import eu.kanade.domain.manga.model.MangaCover
import eu.kanade.domain.updates.model.UpdatesWithRelations
val updateWithRelationMapper: (Long, String, Long, String, String?, Boolean, Boolean, Long, Boolean, String?, Long, Long, Long) -> UpdatesWithRelations = {
mangaId, mangaTitle, chapterId, chapterName, scanlator, read, bookmark, sourceId, favorite, thumbnailUrl, coverLastModified, _, dateFetch ->
UpdatesWithRelations(
mangaId = mangaId,
// SY -->
ogMangaTitle = mangaTitle,
// SY <--
chapterId = chapterId,
chapterName = chapterName,
scanlator = scanlator,
read = read,
bookmark = bookmark,
sourceId = sourceId,
dateFetch = dateFetch,
coverData = MangaCover(
mangaId = mangaId,
sourceId = sourceId,
isMangaFavorite = favorite,
url = thumbnailUrl,
lastModified = coverLastModified,
),
)
}
@@ -1,89 +0,0 @@
package eu.kanade.data.updates
import com.squareup.sqldelight.Query
import com.squareup.sqldelight.db.SqlCursor
import com.squareup.sqldelight.db.SqlDriver
import com.squareup.sqldelight.internal.copyOnWriteList
import eu.kanade.domain.updates.model.UpdatesWithRelations
import exh.source.MERGED_SOURCE_ID
private val mapper = { cursor: SqlCursor ->
updateWithRelationMapper(
cursor.getLong(0)!!,
cursor.getString(1)!!,
cursor.getLong(2)!!,
cursor.getString(3)!!,
cursor.getString(4),
cursor.getLong(5)!! == 1L,
cursor.getLong(6)!! == 1L,
cursor.getLong(7)!!,
cursor.getLong(8)!! == 1L,
cursor.getString(9),
cursor.getLong(10)!!,
cursor.getLong(11)!!,
cursor.getLong(12)!!,
)
}
class UpdatesQuery(val driver: SqlDriver, val after: Long) : Query<UpdatesWithRelations>(copyOnWriteList(), mapper) {
override fun execute(): SqlCursor {
return driver.executeQuery(
null,
"""
SELECT
mangas._id AS mangaId,
mangas.title AS mangaTitle,
chapters._id AS chapterId,
chapters.name AS chapterName,
chapters.scanlator,
chapters.read,
chapters.bookmark,
mangas.source,
mangas.favorite,
mangas.thumbnail_url AS thumbnailUrl,
mangas.cover_last_modified AS coverLastModified,
chapters.date_upload AS dateUpload,
chapters.date_fetch AS datefetch
FROM mangas JOIN chapters
ON mangas._id = chapters.manga_id
WHERE favorite = 1 AND source <> $MERGED_SOURCE_ID
AND date_fetch > date_added
AND dateUpload > :after
UNION
SELECT
mangas._id AS mangaId,
mangas.title AS mangaTitle,
chapters._id AS chapterId,
chapters.name AS chapterName,
chapters.scanlator,
chapters.read,
chapters.bookmark,
mangas.source,
mangas.favorite,
mangas.thumbnail_url AS thumbnailUrl,
mangas.cover_last_modified AS coverLastModified,
chapters.date_upload AS dateUpload,
chapters.date_fetch AS datefetch
FROM mangas
LEFT JOIN (
SELECT merged.manga_id,merged.merge_id
FROM merged
GROUP BY merged.merge_id
) as ME
ON ME.merge_id = mangas._id
JOIN chapters
ON ME.manga_id = chapters.manga_id
WHERE favorite = 1 AND source = $MERGED_SOURCE_ID
AND date_fetch > date_added
AND dateUpload > :after
ORDER BY datefetch DESC;
""".trimIndent(),
1,
binders = {
bindLong(1, after)
},
)
}
override fun toString(): String = "LibraryQuery.sq:get"
}
@@ -1,21 +0,0 @@
package eu.kanade.data.updates
import eu.kanade.data.AndroidDatabaseHandler
import eu.kanade.data.DatabaseHandler
import eu.kanade.domain.updates.model.UpdatesWithRelations
import eu.kanade.domain.updates.repository.UpdatesRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
class UpdatesRepositoryImpl(
val databaseHandler: DatabaseHandler,
) : UpdatesRepository {
override fun subscribeAll(after: Long): Flow<List<UpdatesWithRelations>> {
return databaseHandler.subscribeToList {
updatesViewQueries.updates(after, updateWithRelationMapper)
}.map {
databaseHandler.awaitList { (databaseHandler as AndroidDatabaseHandler).getUpdatesQuery(after) }
}
}
}
@@ -1,147 +0,0 @@
package eu.kanade.domain
import eu.kanade.data.category.CategoryRepositoryImpl
import eu.kanade.data.chapter.ChapterRepositoryImpl
import eu.kanade.data.history.HistoryRepositoryImpl
import eu.kanade.data.manga.MangaRepositoryImpl
import eu.kanade.data.source.SourceDataRepositoryImpl
import eu.kanade.data.source.SourceRepositoryImpl
import eu.kanade.data.track.TrackRepositoryImpl
import eu.kanade.data.updates.UpdatesRepositoryImpl
import eu.kanade.domain.category.interactor.CreateCategoryWithName
import eu.kanade.domain.category.interactor.DeleteCategory
import eu.kanade.domain.category.interactor.GetCategories
import eu.kanade.domain.category.interactor.RenameCategory
import eu.kanade.domain.category.interactor.ReorderCategory
import eu.kanade.domain.category.interactor.ResetCategoryFlags
import eu.kanade.domain.category.interactor.SetDisplayModeForCategory
import eu.kanade.domain.category.interactor.SetMangaCategories
import eu.kanade.domain.category.interactor.SetSortModeForCategory
import eu.kanade.domain.category.interactor.UpdateCategory
import eu.kanade.domain.category.repository.CategoryRepository
import eu.kanade.domain.chapter.interactor.GetChapter
import eu.kanade.domain.chapter.interactor.GetChapterByMangaId
import eu.kanade.domain.chapter.interactor.SetMangaDefaultChapterFlags
import eu.kanade.domain.chapter.interactor.SetReadStatus
import eu.kanade.domain.chapter.interactor.ShouldUpdateDbChapter
import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource
import eu.kanade.domain.chapter.interactor.SyncChaptersWithTrackServiceTwoWay
import eu.kanade.domain.chapter.interactor.UpdateChapter
import eu.kanade.domain.chapter.repository.ChapterRepository
import eu.kanade.domain.download.interactor.DeleteDownload
import eu.kanade.domain.extension.interactor.GetExtensionLanguages
import eu.kanade.domain.extension.interactor.GetExtensionSources
import eu.kanade.domain.extension.interactor.GetExtensionsByType
import eu.kanade.domain.history.interactor.GetHistory
import eu.kanade.domain.history.interactor.GetNextChapters
import eu.kanade.domain.history.interactor.GetTotalReadDuration
import eu.kanade.domain.history.interactor.RemoveHistory
import eu.kanade.domain.history.interactor.UpsertHistory
import eu.kanade.domain.history.repository.HistoryRepository
import eu.kanade.domain.manga.interactor.GetDuplicateLibraryManga
import eu.kanade.domain.manga.interactor.GetFavorites
import eu.kanade.domain.manga.interactor.GetLibraryManga
import eu.kanade.domain.manga.interactor.GetManga
import eu.kanade.domain.manga.interactor.GetMangaWithChapters
import eu.kanade.domain.manga.interactor.NetworkToLocalManga
import eu.kanade.domain.manga.interactor.ResetViewerFlags
import eu.kanade.domain.manga.interactor.SetMangaChapterFlags
import eu.kanade.domain.manga.interactor.SetMangaViewerFlags
import eu.kanade.domain.manga.interactor.UpdateManga
import eu.kanade.domain.manga.repository.MangaRepository
import eu.kanade.domain.source.interactor.GetEnabledSources
import eu.kanade.domain.source.interactor.GetLanguagesWithSources
import eu.kanade.domain.source.interactor.GetRemoteManga
import eu.kanade.domain.source.interactor.GetSourcesWithFavoriteCount
import eu.kanade.domain.source.interactor.GetSourcesWithNonLibraryManga
import eu.kanade.domain.source.interactor.SetMigrateSorting
import eu.kanade.domain.source.interactor.ToggleLanguage
import eu.kanade.domain.source.interactor.ToggleSource
import eu.kanade.domain.source.interactor.ToggleSourcePin
import eu.kanade.domain.source.repository.SourceDataRepository
import eu.kanade.domain.source.repository.SourceRepository
import eu.kanade.domain.track.interactor.DeleteTrack
import eu.kanade.domain.track.interactor.GetTracks
import eu.kanade.domain.track.interactor.GetTracksPerManga
import eu.kanade.domain.track.interactor.InsertTrack
import eu.kanade.domain.track.repository.TrackRepository
import eu.kanade.domain.updates.interactor.GetUpdates
import eu.kanade.domain.updates.repository.UpdatesRepository
import uy.kohesive.injekt.api.InjektModule
import uy.kohesive.injekt.api.InjektRegistrar
import uy.kohesive.injekt.api.addFactory
import uy.kohesive.injekt.api.addSingletonFactory
import uy.kohesive.injekt.api.get
class DomainModule : InjektModule {
override fun InjektRegistrar.registerInjectables() {
addSingletonFactory<CategoryRepository> { CategoryRepositoryImpl(get()) }
addFactory { GetCategories(get()) }
addFactory { ResetCategoryFlags(get(), get()) }
addFactory { SetDisplayModeForCategory(get(), get()) }
addFactory { SetSortModeForCategory(get(), get()) }
addFactory { CreateCategoryWithName(get(), get()) }
addFactory { RenameCategory(get()) }
addFactory { ReorderCategory(get()) }
addFactory { UpdateCategory(get()) }
addFactory { DeleteCategory(get()) }
addSingletonFactory<MangaRepository> { MangaRepositoryImpl(get()) }
addFactory { GetDuplicateLibraryManga(get()) }
addFactory { GetFavorites(get()) }
addFactory { GetLibraryManga(get()) }
addFactory { GetMangaWithChapters(get(), get()) }
addFactory { GetManga(get()) }
addFactory { GetNextChapters(get(), get(), get(), get()) }
addFactory { ResetViewerFlags(get()) }
addFactory { SetMangaChapterFlags(get()) }
addFactory { SetMangaDefaultChapterFlags(get(), get(), get()) }
addFactory { SetMangaViewerFlags(get()) }
addFactory { NetworkToLocalManga(get()) }
addFactory { UpdateManga(get()) }
addFactory { SetMangaCategories(get()) }
addSingletonFactory<TrackRepository> { TrackRepositoryImpl(get()) }
addFactory { DeleteTrack(get()) }
addFactory { GetTracksPerManga(get()) }
addFactory { GetTracks(get()) }
addFactory { InsertTrack(get()) }
addSingletonFactory<ChapterRepository> { ChapterRepositoryImpl(get()) }
addFactory { GetChapter(get()) }
addFactory { GetChapterByMangaId(get()) }
addFactory { UpdateChapter(get()) }
addFactory { SetReadStatus(get(), get(), get(), get(), get()) }
addFactory { ShouldUpdateDbChapter() }
addFactory { SyncChaptersWithSource(get(), get(), get(), get()) }
addFactory { SyncChaptersWithTrackServiceTwoWay(get(), get()) }
addSingletonFactory<HistoryRepository> { HistoryRepositoryImpl(get()) }
addFactory { GetHistory(get()) }
addFactory { UpsertHistory(get()) }
addFactory { RemoveHistory(get()) }
addFactory { GetTotalReadDuration(get()) }
addFactory { DeleteDownload(get(), get()) }
addFactory { GetExtensionsByType(get(), get()) }
addFactory { GetExtensionSources(get()) }
addFactory { GetExtensionLanguages(get(), get()) }
addSingletonFactory<UpdatesRepository> { UpdatesRepositoryImpl(get()) }
addFactory { GetUpdates(get()) }
addSingletonFactory<SourceRepository> { SourceRepositoryImpl(get(), get()) }
addSingletonFactory<SourceDataRepository> { SourceDataRepositoryImpl(get()) }
addFactory { GetEnabledSources(get(), get()) }
addFactory { GetLanguagesWithSources(get(), get()) }
addFactory { GetRemoteManga(get()) }
addFactory { GetSourcesWithFavoriteCount(get(), get()) }
addFactory { GetSourcesWithNonLibraryManga(get()) }
addFactory { SetMigrateSorting(get()) }
addFactory { ToggleLanguage(get()) }
addFactory { ToggleSource(get()) }
addFactory { ToggleSourcePin(get()) }
}
}
@@ -1,152 +0,0 @@
package eu.kanade.domain
import eu.kanade.data.manga.FavoritesEntryRepositoryImpl
import eu.kanade.data.manga.MangaMergeRepositoryImpl
import eu.kanade.data.manga.MangaMetadataRepositoryImpl
import eu.kanade.data.source.FeedSavedSearchRepositoryImpl
import eu.kanade.data.source.SavedSearchRepositoryImpl
import eu.kanade.domain.chapter.interactor.DeleteChapters
import eu.kanade.domain.chapter.interactor.GetChapterByUrl
import eu.kanade.domain.chapter.interactor.GetMergedChapterByMangaId
import eu.kanade.domain.history.interactor.GetHistoryByMangaId
import eu.kanade.domain.manga.interactor.CreateSortTag
import eu.kanade.domain.manga.interactor.DeleteByMergeId
import eu.kanade.domain.manga.interactor.DeleteFavoriteEntries
import eu.kanade.domain.manga.interactor.DeleteMangaById
import eu.kanade.domain.manga.interactor.DeleteMergeById
import eu.kanade.domain.manga.interactor.DeleteSortTag
import eu.kanade.domain.manga.interactor.GetAllManga
import eu.kanade.domain.manga.interactor.GetExhFavoriteMangaWithMetadata
import eu.kanade.domain.manga.interactor.GetFavoriteEntries
import eu.kanade.domain.manga.interactor.GetFlatMetadataById
import eu.kanade.domain.manga.interactor.GetIdsOfFavoriteMangaWithMetadata
import eu.kanade.domain.manga.interactor.GetManga
import eu.kanade.domain.manga.interactor.GetMangaBySource
import eu.kanade.domain.manga.interactor.GetMergedManga
import eu.kanade.domain.manga.interactor.GetMergedMangaById
import eu.kanade.domain.manga.interactor.GetMergedMangaForDownloading
import eu.kanade.domain.manga.interactor.GetMergedReferencesById
import eu.kanade.domain.manga.interactor.GetPagePreviews
import eu.kanade.domain.manga.interactor.GetSearchMetadata
import eu.kanade.domain.manga.interactor.GetSearchTags
import eu.kanade.domain.manga.interactor.GetSearchTitles
import eu.kanade.domain.manga.interactor.GetSortTag
import eu.kanade.domain.manga.interactor.InsertFavoriteEntries
import eu.kanade.domain.manga.interactor.InsertFlatMetadata
import eu.kanade.domain.manga.interactor.InsertMergedReference
import eu.kanade.domain.manga.interactor.ReorderSortTag
import eu.kanade.domain.manga.interactor.SetMangaFilteredScanlators
import eu.kanade.domain.manga.interactor.UpdateMergedSettings
import eu.kanade.domain.manga.repository.FavoritesEntryRepository
import eu.kanade.domain.manga.repository.MangaMergeRepository
import eu.kanade.domain.manga.repository.MangaMetadataRepository
import eu.kanade.domain.source.interactor.CountFeedSavedSearchBySourceId
import eu.kanade.domain.source.interactor.CountFeedSavedSearchGlobal
import eu.kanade.domain.source.interactor.CreateSourceCategory
import eu.kanade.domain.source.interactor.CreateSourceRepo
import eu.kanade.domain.source.interactor.DeleteFeedSavedSearchById
import eu.kanade.domain.source.interactor.DeleteSavedSearchById
import eu.kanade.domain.source.interactor.DeleteSourceCategory
import eu.kanade.domain.source.interactor.DeleteSourceRepos
import eu.kanade.domain.source.interactor.GetExhSavedSearch
import eu.kanade.domain.source.interactor.GetFeedSavedSearchBySourceId
import eu.kanade.domain.source.interactor.GetFeedSavedSearchGlobal
import eu.kanade.domain.source.interactor.GetSavedSearchById
import eu.kanade.domain.source.interactor.GetSavedSearchBySourceId
import eu.kanade.domain.source.interactor.GetSavedSearchBySourceIdFeed
import eu.kanade.domain.source.interactor.GetSavedSearchGlobalFeed
import eu.kanade.domain.source.interactor.GetShowLatest
import eu.kanade.domain.source.interactor.GetSourceCategories
import eu.kanade.domain.source.interactor.GetSourceRepos
import eu.kanade.domain.source.interactor.InsertFeedSavedSearch
import eu.kanade.domain.source.interactor.InsertSavedSearch
import eu.kanade.domain.source.interactor.RenameSourceCategory
import eu.kanade.domain.source.interactor.SetSourceCategories
import eu.kanade.domain.source.interactor.ToggleExcludeFromDataSaver
import eu.kanade.domain.source.repository.FeedSavedSearchRepository
import eu.kanade.domain.source.repository.SavedSearchRepository
import eu.kanade.tachiyomi.source.online.MetadataSource
import exh.search.SearchEngine
import uy.kohesive.injekt.api.InjektModule
import uy.kohesive.injekt.api.InjektRegistrar
import uy.kohesive.injekt.api.addFactory
import uy.kohesive.injekt.api.addSingletonFactory
import uy.kohesive.injekt.api.get
import xyz.nulldev.ts.api.http.serializer.FilterSerializer
class SYDomainModule : InjektModule {
override fun InjektRegistrar.registerInjectables() {
addFactory { GetShowLatest(get()) }
addFactory { ToggleExcludeFromDataSaver(get()) }
addFactory { SetSourceCategories(get()) }
addFactory { SetMangaFilteredScanlators(get()) }
addFactory { GetAllManga(get()) }
addFactory { GetMangaBySource(get()) }
addFactory { DeleteChapters(get()) }
addFactory { DeleteMangaById(get()) }
addFactory { FilterSerializer() }
addFactory { GetHistoryByMangaId(get()) }
addFactory { GetChapterByUrl(get()) }
addFactory { CreateSourceRepo(get()) }
addFactory { DeleteSourceRepos(get()) }
addFactory { GetSourceRepos(get()) }
addFactory { GetSourceCategories(get()) }
addFactory { CreateSourceCategory(get()) }
addFactory { RenameSourceCategory(get(), get()) }
addFactory { DeleteSourceCategory(get()) }
addFactory { GetSortTag(get()) }
addFactory { CreateSortTag(get(), get()) }
addFactory { DeleteSortTag(get(), get()) }
addFactory { ReorderSortTag(get(), get()) }
addFactory { GetPagePreviews(get(), get()) }
addFactory { SearchEngine() }
// Required for [MetadataSource]
addFactory<MetadataSource.GetMangaId> { GetManga(get()) }
addFactory<MetadataSource.GetFlatMetadataById> { GetFlatMetadataById(get()) }
addFactory<MetadataSource.InsertFlatMetadata> { InsertFlatMetadata(get()) }
addSingletonFactory<MangaMetadataRepository> { MangaMetadataRepositoryImpl(get()) }
addFactory { GetFlatMetadataById(get()) }
addFactory { InsertFlatMetadata(get()) }
addFactory { GetExhFavoriteMangaWithMetadata(get()) }
addFactory { GetSearchMetadata(get()) }
addFactory { GetSearchTags(get()) }
addFactory { GetSearchTitles(get()) }
addFactory { GetIdsOfFavoriteMangaWithMetadata(get()) }
addSingletonFactory<MangaMergeRepository> { MangaMergeRepositoryImpl(get()) }
addFactory { GetMergedManga(get()) }
addFactory { GetMergedMangaById(get()) }
addFactory { GetMergedReferencesById(get()) }
addFactory { GetMergedChapterByMangaId(get(), get()) }
addFactory { InsertMergedReference(get()) }
addFactory { UpdateMergedSettings(get()) }
addFactory { DeleteByMergeId(get()) }
addFactory { DeleteMergeById(get()) }
addFactory { GetMergedMangaForDownloading(get()) }
addSingletonFactory<FavoritesEntryRepository> { FavoritesEntryRepositoryImpl(get()) }
addFactory { GetFavoriteEntries(get()) }
addFactory { InsertFavoriteEntries(get()) }
addFactory { DeleteFavoriteEntries(get()) }
addSingletonFactory<SavedSearchRepository> { SavedSearchRepositoryImpl(get()) }
addFactory { GetSavedSearchById(get()) }
addFactory { GetSavedSearchBySourceId(get()) }
addFactory { DeleteSavedSearchById(get()) }
addFactory { InsertSavedSearch(get()) }
addFactory { GetExhSavedSearch(get(), get(), get()) }
addSingletonFactory<FeedSavedSearchRepository> { FeedSavedSearchRepositoryImpl(get()) }
addFactory { InsertFeedSavedSearch(get()) }
addFactory { DeleteFeedSavedSearchById(get()) }
addFactory { GetFeedSavedSearchGlobal(get()) }
addFactory { GetFeedSavedSearchBySourceId(get()) }
addFactory { CountFeedSavedSearchGlobal(get()) }
addFactory { CountFeedSavedSearchBySourceId(get()) }
addFactory { GetSavedSearchGlobalFeed(get()) }
addFactory { GetSavedSearchBySourceIdFeed(get()) }
}
}
@@ -1,89 +0,0 @@
package eu.kanade.domain
import eu.kanade.tachiyomi.core.preference.PreferenceStore
class UnsortedPreferences(
private val preferenceStore: PreferenceStore,
) {
// SY -->
fun migrateFlags() = preferenceStore.getInt("migrate_flags", Int.MAX_VALUE)
fun defaultMangaOrder() = preferenceStore.getString("default_manga_order", "")
fun migrationSources() = preferenceStore.getString("migrate_sources", "")
fun smartMigration() = preferenceStore.getBoolean("smart_migrate", false)
fun useSourceWithMost() = preferenceStore.getBoolean("use_source_with_most", false)
fun skipPreMigration() = preferenceStore.getBoolean("skip_pre_migration", false)
fun hideNotFoundMigration() = preferenceStore.getBoolean("hide_not_found_migration", false)
fun isHentaiEnabled() = preferenceStore.getBoolean("eh_is_hentai_enabled", true)
fun enableExhentai() = preferenceStore.getBoolean("enable_exhentai", false)
fun imageQuality() = preferenceStore.getString("ehentai_quality", "auto")
fun useHentaiAtHome() = preferenceStore.getInt("eh_enable_hah", 0)
fun useJapaneseTitle() = preferenceStore.getBoolean("use_jp_title", false)
fun exhUseOriginalImages() = preferenceStore.getBoolean("eh_useOrigImages", false)
fun ehTagFilterValue() = preferenceStore.getInt("eh_tag_filtering_value", 0)
fun ehTagWatchingValue() = preferenceStore.getInt("eh_tag_watching_value", 0)
// EH Cookies
fun memberIdVal() = preferenceStore.getString("eh_ipb_member_id", "")
fun passHashVal() = preferenceStore.getString("eh_ipb_pass_hash", "")
fun igneousVal() = preferenceStore.getString("eh_igneous", "")
fun ehSettingsProfile() = preferenceStore.getInt("eh_ehSettingsProfile", -1)
fun exhSettingsProfile() = preferenceStore.getInt("eh_exhSettingsProfile", -1)
fun exhSettingsKey() = preferenceStore.getString("eh_settingsKey", "")
fun exhSessionCookie() = preferenceStore.getString("eh_sessionCookie", "")
fun exhHathPerksCookies() = preferenceStore.getString("eh_hathPerksCookie", "")
fun exhShowSyncIntro() = preferenceStore.getBoolean("eh_show_sync_intro", true)
fun exhReadOnlySync() = preferenceStore.getBoolean("eh_sync_read_only", false)
fun exhLenientSync() = preferenceStore.getBoolean("eh_lenient_sync", false)
fun exhShowSettingsUploadWarning() = preferenceStore.getBoolean("eh_showSettingsUploadWarning2", true)
fun logLevel() = preferenceStore.getInt("eh_log_level", 0)
fun exhAutoUpdateFrequency() = preferenceStore.getInt("eh_auto_update_frequency", 1)
fun exhAutoUpdateRequirements() = preferenceStore.getStringSet("eh_auto_update_restrictions", emptySet())
fun exhAutoUpdateStats() = preferenceStore.getString("eh_auto_update_stats", "")
fun exhWatchedListDefaultState() = preferenceStore.getBoolean("eh_watched_list_default_state", false)
fun exhSettingsLanguages() = preferenceStore.getString(
"eh_settings_languages",
"false*false*false\nfalse*false*false\nfalse*false*false\nfalse*false*false\nfalse*false*false\nfalse*false*false\nfalse*false*false\nfalse*false*false\nfalse*false*false\nfalse*false*false\nfalse*false*false\nfalse*false*false\nfalse*false*false\nfalse*false*false\nfalse*false*false\nfalse*false*false\nfalse*false*false",
)
fun exhEnabledCategories() = preferenceStore.getString(
"eh_enabled_categories",
"false,false,false,false,false,false,false,false,false,false",
)
fun enhancedEHentaiView() = preferenceStore.getBoolean("enhanced_e_hentai_view", true)
fun preferredMangaDexId() = preferenceStore.getString("preferred_mangaDex_id", "0")
fun mangadexSyncToLibraryIndexes() = preferenceStore.getStringSet("pref_mangadex_sync_to_library_indexes", emptySet())
fun allowLocalSourceHiddenFolders() = preferenceStore.getBoolean("allow_local_source_hidden_folders", false)
fun extensionRepos() = preferenceStore.getStringSet("extension_repos", emptySet())
}
@@ -1,16 +0,0 @@
package eu.kanade.domain.backup.service
import eu.kanade.tachiyomi.core.preference.PreferenceStore
import eu.kanade.tachiyomi.core.provider.FolderProvider
class BackupPreferences(
private val folderProvider: FolderProvider,
private val preferenceStore: PreferenceStore,
) {
fun backupsDirectory() = preferenceStore.getString("backup_directory", folderProvider.path())
fun numberOfBackups() = preferenceStore.getInt("backup_slots", 2)
fun backupInterval() = preferenceStore.getInt("backup_interval", 12)
}
@@ -1,30 +0,0 @@
package eu.kanade.domain.base
import android.content.Context
import eu.kanade.tachiyomi.core.preference.PreferenceStore
import eu.kanade.tachiyomi.core.preference.getEnum
import eu.kanade.tachiyomi.data.preference.PreferenceValues
import eu.kanade.tachiyomi.util.system.DeviceUtil
import eu.kanade.tachiyomi.util.system.isPreviewBuildType
import eu.kanade.tachiyomi.util.system.isReleaseBuildType
class BasePreferences(
val context: Context,
private val preferenceStore: PreferenceStore,
) {
fun confirmExit() = preferenceStore.getBoolean("pref_confirm_exit", false)
fun downloadedOnly() = preferenceStore.getBoolean("pref_downloaded_only", false)
fun incognitoMode() = preferenceStore.getBoolean("incognito_mode", false)
fun automaticExtUpdates() = preferenceStore.getBoolean("automatic_ext_updates", true)
fun extensionInstaller() = preferenceStore.getEnum(
"extension_installer",
if (DeviceUtil.isMiui) PreferenceValues.ExtensionInstaller.LEGACY else PreferenceValues.ExtensionInstaller.PACKAGEINSTALLER,
)
fun acraEnabled() = preferenceStore.getBoolean("acra.enable", isPreviewBuildType || isReleaseBuildType)
}
@@ -1,55 +0,0 @@
package eu.kanade.domain.category.interactor
import eu.kanade.domain.category.model.Category
import eu.kanade.domain.category.model.anyWithName
import eu.kanade.domain.category.repository.CategoryRepository
import eu.kanade.domain.library.service.LibraryPreferences
import eu.kanade.tachiyomi.util.lang.withNonCancellableContext
import eu.kanade.tachiyomi.util.system.logcat
import logcat.LogPriority
class CreateCategoryWithName(
private val categoryRepository: CategoryRepository,
private val preferences: LibraryPreferences,
) {
private val initialFlags: Long
get() {
val sort = preferences.librarySortingMode().get()
return preferences.libraryDisplayMode().get().flag or
sort.type.flag or
sort.direction.flag
}
suspend fun await(name: String): Result = withNonCancellableContext {
val categories = categoryRepository.getAll()
if (categories.anyWithName(name)) {
return@withNonCancellableContext Result.NameAlreadyExistsError
}
val nextOrder = categories.maxOfOrNull { it.order }?.plus(1) ?: 0
val newCategory = Category(
id = 0,
name = name,
order = nextOrder,
flags = initialFlags,
)
try {
categoryRepository.insert(newCategory)
Result.Success(/* SY --> */newCategory/* SY <-- */)
} catch (e: Exception) {
logcat(LogPriority.ERROR, e)
Result.InternalError(e)
}
}
sealed class Result {
// SY -->
data class Success(val category: Category) : Result()
// SY <--
object NameAlreadyExistsError : Result()
data class InternalError(val error: Throwable) : Result()
}
}
@@ -1,42 +0,0 @@
package eu.kanade.domain.category.interactor
import eu.kanade.domain.category.model.CategoryUpdate
import eu.kanade.domain.category.repository.CategoryRepository
import eu.kanade.tachiyomi.util.lang.withNonCancellableContext
import eu.kanade.tachiyomi.util.system.logcat
import logcat.LogPriority
class DeleteCategory(
private val categoryRepository: CategoryRepository,
) {
suspend fun await(categoryId: Long) = withNonCancellableContext {
try {
categoryRepository.delete(categoryId)
} catch (e: Exception) {
logcat(LogPriority.ERROR, e)
return@withNonCancellableContext Result.InternalError(e)
}
val categories = categoryRepository.getAll()
val updates = categories.mapIndexed { index, category ->
CategoryUpdate(
id = category.id,
order = index.toLong(),
)
}
try {
categoryRepository.updatePartial(updates)
Result.Success
} catch (e: Exception) {
logcat(LogPriority.ERROR, e)
Result.InternalError(e)
}
}
sealed class Result {
object Success : Result()
data class InternalError(val error: Throwable) : Result()
}
}
@@ -1,26 +0,0 @@
package eu.kanade.domain.category.interactor
import eu.kanade.domain.category.model.Category
import eu.kanade.domain.category.repository.CategoryRepository
import kotlinx.coroutines.flow.Flow
class GetCategories(
private val categoryRepository: CategoryRepository,
) {
fun subscribe(): Flow<List<Category>> {
return categoryRepository.getAllAsFlow()
}
fun subscribe(mangaId: Long): Flow<List<Category>> {
return categoryRepository.getCategoriesByMangaIdAsFlow(mangaId)
}
suspend fun await(): List<Category> {
return categoryRepository.getAll()
}
suspend fun await(mangaId: Long): List<Category> {
return categoryRepository.getCategoriesByMangaId(mangaId)
}
}
@@ -1,42 +0,0 @@
package eu.kanade.domain.category.interactor
import eu.kanade.domain.category.model.Category
import eu.kanade.domain.category.model.CategoryUpdate
import eu.kanade.domain.category.model.anyWithName
import eu.kanade.domain.category.repository.CategoryRepository
import eu.kanade.tachiyomi.util.lang.withNonCancellableContext
import eu.kanade.tachiyomi.util.system.logcat
import logcat.LogPriority
class RenameCategory(
private val categoryRepository: CategoryRepository,
) {
suspend fun await(categoryId: Long, name: String) = withNonCancellableContext {
val categories = categoryRepository.getAll()
if (categories.anyWithName(name)) {
return@withNonCancellableContext Result.NameAlreadyExistsError
}
val update = CategoryUpdate(
id = categoryId,
name = name,
)
try {
categoryRepository.updatePartial(update)
Result.Success
} catch (e: Exception) {
logcat(LogPriority.ERROR, e)
Result.InternalError(e)
}
}
suspend fun await(category: Category, name: String) = await(category.id, name)
sealed class Result {
object Success : Result()
object NameAlreadyExistsError : Result()
data class InternalError(val error: Throwable) : Result()
}
}
@@ -1,70 +0,0 @@
package eu.kanade.domain.category.interactor
import eu.kanade.domain.category.model.Category
import eu.kanade.domain.category.model.CategoryUpdate
import eu.kanade.domain.category.repository.CategoryRepository
import eu.kanade.tachiyomi.util.lang.withNonCancellableContext
import eu.kanade.tachiyomi.util.system.logcat
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import logcat.LogPriority
import java.util.Collections
class ReorderCategory(
private val categoryRepository: CategoryRepository,
) {
private val mutex = Mutex()
suspend fun moveUp(category: Category): Result =
await(category, MoveTo.UP)
suspend fun moveDown(category: Category): Result =
await(category, MoveTo.DOWN)
private suspend fun await(category: Category, moveTo: MoveTo) = withNonCancellableContext {
mutex.withLock {
val categories = categoryRepository.getAll()
.filterNot(Category::isSystemCategory)
.toMutableList()
val currentIndex = categories.indexOfFirst { it.id == category.id }
if (currentIndex == -1) {
return@withNonCancellableContext Result.Unchanged
}
val newPosition = when (moveTo) {
MoveTo.UP -> currentIndex - 1
MoveTo.DOWN -> currentIndex + 1
}.toInt()
try {
Collections.swap(categories, currentIndex, newPosition)
val updates = categories.mapIndexed { index, category ->
CategoryUpdate(
id = category.id,
order = index.toLong(),
)
}
categoryRepository.updatePartial(updates)
Result.Success
} catch (e: Exception) {
logcat(LogPriority.ERROR, e)
Result.InternalError(e)
}
}
}
sealed class Result {
object Success : Result()
object Unchanged : Result()
data class InternalError(val error: Throwable) : Result()
}
private enum class MoveTo {
UP,
DOWN,
}
}
@@ -1,17 +0,0 @@
package eu.kanade.domain.category.interactor
import eu.kanade.domain.category.repository.CategoryRepository
import eu.kanade.domain.library.model.plus
import eu.kanade.domain.library.service.LibraryPreferences
class ResetCategoryFlags(
private val preferences: LibraryPreferences,
private val categoryRepository: CategoryRepository,
) {
suspend fun await() {
val display = preferences.libraryDisplayMode().get()
val sort = preferences.librarySortingMode().get()
categoryRepository.updateAllFlags(display + sort.type + sort.direction)
}
}
@@ -1,41 +0,0 @@
package eu.kanade.domain.category.interactor
import eu.kanade.domain.category.model.Category
import eu.kanade.domain.category.model.CategoryUpdate
import eu.kanade.domain.category.repository.CategoryRepository
import eu.kanade.domain.library.model.LibraryDisplayMode
import eu.kanade.domain.library.model.LibraryGroup
import eu.kanade.domain.library.model.plus
import eu.kanade.domain.library.service.LibraryPreferences
class SetDisplayModeForCategory(
private val preferences: LibraryPreferences,
private val categoryRepository: CategoryRepository,
) {
suspend fun await(categoryId: Long, display: LibraryDisplayMode) {
// SY -->
if (preferences.groupLibraryBy().get() != LibraryGroup.BY_DEFAULT) {
preferences.libraryDisplayMode().set(display)
return
}
// SY <--
val category = categoryRepository.get(categoryId) ?: return
val flags = category.flags + display
if (preferences.categorizedDisplaySettings().get()) {
categoryRepository.updatePartial(
CategoryUpdate(
id = category.id,
flags = flags,
),
)
} else {
preferences.libraryDisplayMode().set(display)
categoryRepository.updateAllFlags(flags)
}
}
suspend fun await(category: Category, display: LibraryDisplayMode) {
await(category.id, display)
}
}
@@ -1,18 +0,0 @@
package eu.kanade.domain.category.interactor
import eu.kanade.domain.manga.repository.MangaRepository
import eu.kanade.tachiyomi.util.system.logcat
import logcat.LogPriority
class SetMangaCategories(
private val mangaRepository: MangaRepository,
) {
suspend fun await(mangaId: Long, categoryIds: List<Long>) {
try {
mangaRepository.setMangaCategories(mangaId, categoryIds)
} catch (e: Exception) {
logcat(LogPriority.ERROR, e)
}
}
}
@@ -1,41 +0,0 @@
package eu.kanade.domain.category.interactor
import eu.kanade.domain.category.model.Category
import eu.kanade.domain.category.model.CategoryUpdate
import eu.kanade.domain.category.repository.CategoryRepository
import eu.kanade.domain.library.model.LibraryGroup
import eu.kanade.domain.library.model.LibrarySort
import eu.kanade.domain.library.model.plus
import eu.kanade.domain.library.service.LibraryPreferences
class SetSortModeForCategory(
private val preferences: LibraryPreferences,
private val categoryRepository: CategoryRepository,
) {
suspend fun await(categoryId: Long, type: LibrarySort.Type, direction: LibrarySort.Direction) {
// SY -->
if (preferences.groupLibraryBy().get() != LibraryGroup.BY_DEFAULT) {
preferences.librarySortingMode().set(LibrarySort(type, direction))
return
}
// SY <--
val category = categoryRepository.get(categoryId) ?: return
val flags = category.flags + type + direction
if (preferences.categorizedDisplaySettings().get()) {
categoryRepository.updatePartial(
CategoryUpdate(
id = category.id,
flags = flags,
),
)
} else {
preferences.librarySortingMode().set(LibrarySort(type, direction))
categoryRepository.updateAllFlags(flags)
}
}
suspend fun await(category: Category, type: LibrarySort.Type, direction: LibrarySort.Direction) {
await(category.id, type, direction)
}
}
@@ -1,24 +0,0 @@
package eu.kanade.domain.category.interactor
import eu.kanade.domain.category.model.CategoryUpdate
import eu.kanade.domain.category.repository.CategoryRepository
import eu.kanade.tachiyomi.util.lang.withNonCancellableContext
class UpdateCategory(
private val categoryRepository: CategoryRepository,
) {
suspend fun await(payload: CategoryUpdate): Result = withNonCancellableContext {
try {
categoryRepository.updatePartial(payload)
Result.Success
} catch (e: Exception) {
Result.Error(e)
}
}
sealed class Result {
object Success : Result()
data class Error(val error: Exception) : Result()
}
}
@@ -1,21 +0,0 @@
package eu.kanade.domain.category.model
import java.io.Serializable
data class Category(
val id: Long,
val name: String,
val order: Long,
val flags: Long,
) : Serializable {
val isSystemCategory: Boolean = id == UNCATEGORIZED_ID
companion object {
const val UNCATEGORIZED_ID = 0L
}
}
internal fun List<Category>.anyWithName(name: String): Boolean {
return any { name == it.name }
}
@@ -1,8 +0,0 @@
package eu.kanade.domain.category.model
data class CategoryUpdate(
val id: Long,
val name: String? = null,
val order: Long? = null,
val flags: Long? = null,
)
@@ -1,30 +0,0 @@
package eu.kanade.domain.category.repository
import eu.kanade.domain.category.model.Category
import eu.kanade.domain.category.model.CategoryUpdate
import kotlinx.coroutines.flow.Flow
interface CategoryRepository {
suspend fun get(id: Long): Category?
suspend fun getAll(): List<Category>
fun getAllAsFlow(): Flow<List<Category>>
suspend fun getCategoriesByMangaId(mangaId: Long): List<Category>
fun getCategoriesByMangaIdAsFlow(mangaId: Long): Flow<List<Category>>
// SY -->
suspend fun insert(category: Category): Long
// SY <--
suspend fun updatePartial(update: CategoryUpdate)
suspend fun updatePartial(updates: List<CategoryUpdate>)
suspend fun updateAllFlags(flags: Long?)
suspend fun delete(categoryId: Long)
}
@@ -1,12 +0,0 @@
package eu.kanade.domain.chapter.interactor
import eu.kanade.domain.chapter.repository.ChapterRepository
class DeleteChapters(
private val chapterRepository: ChapterRepository,
) {
suspend fun await(chapters: List<Long>) {
chapterRepository.removeChaptersWithIds(chapters)
}
}
@@ -1,29 +0,0 @@
package eu.kanade.domain.chapter.interactor
import eu.kanade.domain.chapter.model.Chapter
import eu.kanade.domain.chapter.repository.ChapterRepository
import eu.kanade.tachiyomi.util.system.logcat
import logcat.LogPriority
class GetChapter(
private val chapterRepository: ChapterRepository,
) {
suspend fun await(id: Long): Chapter? {
return try {
chapterRepository.getChapterById(id)
} catch (e: Exception) {
logcat(LogPriority.ERROR, e)
null
}
}
suspend fun await(url: String, mangaId: Long): Chapter? {
return try {
chapterRepository.getChapterByUrlAndMangaId(url, mangaId)
} catch (e: Exception) {
logcat(LogPriority.ERROR, e)
null
}
}
}
@@ -1,20 +0,0 @@
package eu.kanade.domain.chapter.interactor
import eu.kanade.domain.chapter.model.Chapter
import eu.kanade.domain.chapter.repository.ChapterRepository
import eu.kanade.tachiyomi.util.system.logcat
import logcat.LogPriority
class GetChapterByMangaId(
private val chapterRepository: ChapterRepository,
) {
suspend fun await(mangaId: Long): List<Chapter> {
return try {
chapterRepository.getChapterByMangaId(mangaId)
} catch (e: Exception) {
logcat(LogPriority.ERROR, e)
emptyList()
}
}
}
@@ -1,20 +0,0 @@
package eu.kanade.domain.chapter.interactor
import eu.kanade.domain.chapter.model.Chapter
import eu.kanade.domain.chapter.repository.ChapterRepository
import eu.kanade.tachiyomi.util.system.logcat
import logcat.LogPriority
class GetChapterByUrl(
private val chapterRepository: ChapterRepository,
) {
suspend fun await(url: String): List<Chapter> {
return try {
chapterRepository.getChapterByUrl(url)
} catch (e: Exception) {
logcat(LogPriority.ERROR, e)
emptyList()
}
}
}
@@ -1,107 +0,0 @@
package eu.kanade.domain.chapter.interactor
import eu.kanade.domain.chapter.model.Chapter
import eu.kanade.domain.chapter.repository.ChapterRepository
import eu.kanade.domain.manga.interactor.GetMergedReferencesById
import eu.kanade.tachiyomi.util.system.logcat
import exh.merged.sql.models.MergedMangaReference
import exh.source.MERGED_SOURCE_ID
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flowOf
import logcat.LogPriority
class GetMergedChapterByMangaId(
private val chapterRepository: ChapterRepository,
private val getMergedReferencesById: GetMergedReferencesById,
) {
suspend fun await(mangaId: Long, dedupe: Boolean = true): List<Chapter> {
return transformMergedChapters(getMergedReferencesById.await(mangaId), getFromDatabase(mangaId), dedupe)
}
suspend fun subscribe(mangaId: Long, dedupe: Boolean = true): Flow<List<Chapter>> {
return try {
chapterRepository.getMergedChapterByMangaIdAsFlow(mangaId)
.combine(getMergedReferencesById.subscribe(mangaId)) { chapters, references ->
transformMergedChapters(references, chapters, dedupe)
}
} catch (e: Exception) {
logcat(LogPriority.ERROR, e)
flowOf(emptyList())
}
}
private suspend fun getFromDatabase(mangaId: Long): List<Chapter> {
return try {
chapterRepository.getMergedChapterByMangaId(mangaId)
} catch (e: Exception) {
logcat(LogPriority.ERROR, e)
emptyList()
}
}
fun transformMergedChapters(mangaReferences: List<MergedMangaReference>, chapterList: List<Chapter>, dedupe: Boolean): List<Chapter> {
return if (dedupe) dedupeChapterList(mangaReferences, chapterList) else chapterList
}
private fun dedupeChapterList(mangaReferences: List<MergedMangaReference>, chapterList: List<Chapter>): List<Chapter> {
return when (mangaReferences.firstOrNull { it.mangaSourceId == MERGED_SOURCE_ID }?.chapterSortMode) {
MergedMangaReference.CHAPTER_SORT_NO_DEDUPE, MergedMangaReference.CHAPTER_SORT_NONE -> chapterList
MergedMangaReference.CHAPTER_SORT_PRIORITY -> dedupeByPriority(mangaReferences, chapterList)
MergedMangaReference.CHAPTER_SORT_MOST_CHAPTERS -> {
findSourceWithMostChapters(chapterList)?.let { mangaId ->
chapterList.filter { it.mangaId == mangaId }
} ?: chapterList
}
MergedMangaReference.CHAPTER_SORT_HIGHEST_CHAPTER_NUMBER -> {
findSourceWithHighestChapterNumber(chapterList)?.let { mangaId ->
chapterList.filter { it.mangaId == mangaId }
} ?: chapterList
}
else -> chapterList
}
}
private fun findSourceWithMostChapters(chapterList: List<Chapter>): Long? {
return chapterList.groupBy { it.mangaId }.maxByOrNull { it.value.size }?.key
}
private fun findSourceWithHighestChapterNumber(chapterList: List<Chapter>): Long? {
return chapterList.maxByOrNull { it.chapterNumber }?.mangaId
}
private fun dedupeByPriority(mangaReferences: List<MergedMangaReference>, chapterList: List<Chapter>): List<Chapter> {
val sortedChapterList = mutableListOf<Chapter>()
var existingChapterIndex: Int
chapterList.groupBy { it.mangaId }
.entries
.sortedBy { (mangaId) ->
mangaReferences.find { it.mangaId == mangaId }?.chapterPriority ?: Int.MAX_VALUE
}
.forEach { (_, chapters) ->
existingChapterIndex = -1
chapters.forEach { chapter ->
val oldChapterIndex = existingChapterIndex
if (chapter.isRecognizedNumber) {
existingChapterIndex = sortedChapterList.indexOfFirst {
it.isRecognizedNumber && it.chapterNumber == chapter.chapterNumber && // check if the chapter is not already there
it.mangaId != chapter.mangaId // allow multiple chapters of the same number from the same source
}
if (existingChapterIndex == -1) {
sortedChapterList.add(oldChapterIndex + 1, chapter)
existingChapterIndex = oldChapterIndex + 1
}
} else {
sortedChapterList.add(oldChapterIndex + 1, chapter)
existingChapterIndex = oldChapterIndex + 1
}
}
}
return sortedChapterList.mapIndexed { index, chapter ->
chapter.copy(sourceOrder = index.toLong())
}
}
}
@@ -1,36 +0,0 @@
package eu.kanade.domain.chapter.interactor
import eu.kanade.domain.library.service.LibraryPreferences
import eu.kanade.domain.manga.interactor.GetFavorites
import eu.kanade.domain.manga.interactor.SetMangaChapterFlags
import eu.kanade.domain.manga.model.Manga
import eu.kanade.tachiyomi.util.lang.withNonCancellableContext
class SetMangaDefaultChapterFlags(
private val libraryPreferences: LibraryPreferences,
private val setMangaChapterFlags: SetMangaChapterFlags,
private val getFavorites: GetFavorites,
) {
suspend fun await(manga: Manga) {
withNonCancellableContext {
with(libraryPreferences) {
setMangaChapterFlags.awaitSetAllFlags(
mangaId = manga.id,
unreadFilter = filterChapterByRead().get(),
downloadedFilter = filterChapterByDownloaded().get(),
bookmarkedFilter = filterChapterByBookmarked().get(),
sortingMode = sortChapterBySourceOrNumber().get(),
sortingDirection = sortChapterByAscendingOrDescending().get(),
displayMode = displayChapterByNameOrNumber().get(),
)
}
}
}
suspend fun awaitAll() {
withNonCancellableContext {
getFavorites.await().forEach { await(it) }
}
}
}
@@ -1,98 +0,0 @@
package eu.kanade.domain.chapter.interactor
import eu.kanade.domain.chapter.model.Chapter
import eu.kanade.domain.chapter.model.ChapterUpdate
import eu.kanade.domain.chapter.repository.ChapterRepository
import eu.kanade.domain.download.interactor.DeleteDownload
import eu.kanade.domain.download.service.DownloadPreferences
import eu.kanade.domain.manga.model.Manga
import eu.kanade.domain.manga.repository.MangaRepository
import eu.kanade.tachiyomi.util.lang.withNonCancellableContext
import eu.kanade.tachiyomi.util.system.logcat
import exh.source.MERGED_SOURCE_ID
import logcat.LogPriority
class SetReadStatus(
private val downloadPreferences: DownloadPreferences,
private val deleteDownload: DeleteDownload,
private val mangaRepository: MangaRepository,
private val chapterRepository: ChapterRepository,
// SY -->
private val getMergedChapterByMangaId: GetMergedChapterByMangaId,
// SY <--
) {
private val mapper = { chapter: Chapter, read: Boolean ->
ChapterUpdate(
read = read,
lastPageRead = if (!read) 0 else null,
id = chapter.id,
)
}
suspend fun await(read: Boolean, vararg chapters: Chapter): Result = withNonCancellableContext {
val chaptersToUpdate = chapters.filter {
when (read) {
true -> !it.read
false -> it.read || it.lastPageRead > 0
}
}
if (chaptersToUpdate.isEmpty()) {
return@withNonCancellableContext Result.NoChapters
}
try {
chapterRepository.updateAll(
chaptersToUpdate.map { mapper(it, read) },
)
} catch (e: Exception) {
logcat(LogPriority.ERROR, e)
return@withNonCancellableContext Result.InternalError(e)
}
if (read && downloadPreferences.removeAfterMarkedAsRead().get()) {
chaptersToUpdate
.groupBy { it.mangaId }
.forEach { (mangaId, chapters) ->
deleteDownload.awaitAll(
manga = mangaRepository.getMangaById(mangaId),
chapters = chapters.toTypedArray(),
)
}
}
Result.Success
}
suspend fun await(mangaId: Long, read: Boolean): Result = withNonCancellableContext {
await(
read = read,
chapters = chapterRepository
.getChapterByMangaId(mangaId)
.toTypedArray(),
)
}
// SY -->
private suspend fun awaitMerged(mangaId: Long, read: Boolean) = withNonCancellableContext f@{
return@f await(
read = read,
chapters = getMergedChapterByMangaId
.await(mangaId, dedupe = false)
.toTypedArray(),
)
}
suspend fun await(manga: Manga, read: Boolean) = if (manga.source == MERGED_SOURCE_ID) {
awaitMerged(manga.id, read)
} else {
await(manga.id, read)
}
// SY <--
sealed class Result {
object Success : Result()
object NoChapters : Result()
data class InternalError(val error: Throwable) : Result()
}
}
@@ -1,13 +0,0 @@
package eu.kanade.domain.chapter.interactor
import eu.kanade.domain.chapter.model.Chapter
class ShouldUpdateDbChapter {
fun await(dbChapter: Chapter, sourceChapter: Chapter): Boolean {
return dbChapter.scanlator != sourceChapter.scanlator || dbChapter.name != sourceChapter.name ||
dbChapter.dateUpload != sourceChapter.dateUpload ||
dbChapter.chapterNumber != sourceChapter.chapterNumber ||
dbChapter.sourceOrder != sourceChapter.sourceOrder
}
}

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