Compare commits

..

No commits in common. "master" and "1.6.1" have entirely different histories.

2623 changed files with 135366 additions and 247409 deletions

View File

@ -1,40 +0,0 @@
root = true
[*]
charset = utf-8
indent_size = 2
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
[*.{xml,sq,sqm}]
indent_size = 4
# noinspection EditorConfigKeyCorrectness
[*.{kt,kts}]
indent_size = 4
max_line_length = 120
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
ktlint_code_style = intellij_idea
ktlint_function_naming_ignore_when_annotated_with = Composable
ktlint_standard_class-signature = disabled
ktlint_standard_discouraged-comment-location = disabled
ktlint_standard_function-expression-body = disabled
ktlint_standard_function-signature = disabled
# SY
ktlint_standard_filename = disabled
ktlint_standard_argument-list-wrapping = disabled
ktlint_standard_function-naming = disabled
ktlint_standard_property-naming = disabled
ktlint_standard_multiline-expression-wrapping = disabled
ktlint_standard_string-template-indent = disabled
ktlint_standard_comment-wrapping = disabled
ktlint_standard_max-line-length = disabled
ktlint_standard_type-argument-comment = disabled
ktlint_standard_value-argument-comment = disabled
ktlint_standard_value-parameter-comment = disabled

1
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1 @@
ko_fi: inorichi

34
.github/ISSUE_TEMPLATE.md vendored Executable file
View File

@ -0,0 +1,34 @@
**PLEASE READ THIS**
I acknowledge that:
- I have updated:
- To the latest version of the app (stable is v1.6.1)
- All extensions
- I have tried the troubleshooting guide: https://tachiyomi.org/help/guides/troubleshooting-problems/
- If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions
- I have searched the existing issues and this is new ticket **NOT** a duplicate or related to another open issue
- I will fill out the title and the information in this template
Note that the issue will be automatically closed if you do not fill out the title or requested information.
**DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT**
---
## Device information
* Tachiyomi version: ?
* Android version: ?
* Device: ?
## Steps to reproduce
1. First step
2. Second step
## Issue/Request
?
## Other details
Additional details and attachments.
If you're experiencing crashes, share the crash logs from More → Settings → Advanced → Dump crash logs.

44
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@ -0,0 +1,44 @@
---
name: "🐞 Bug report"
about: Report a bug
title: "[Bug] <Write short description here>"
labels: "bug"
---
**PLEASE READ THIS**
I acknowledge that:
- I have updated:
- To the latest version of the app (stable is v1.6.1)
- All extensions
- I have tried the troubleshooting guide: https://tachiyomi.org/help/guides/troubleshooting-problems/
- If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions
- I have searched the existing issues and this is new ticket **NOT** a duplicate or related to another open issue
- I will fill out the title and the information in this template
Note that the issue will be automatically closed if you do not fill out the title or requested information.
**DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT**
---
## Device information
* Tachiyomi version: ?
* Android version: ?
* Device: ?
## Steps to reproduce
1. First step
2. Second step
### Expected behavior
This should happen.
### Actual behavior
This happened instead.
## Other details
Additional details and attachments.
If you're experiencing crashes, share the crash logs from More → Settings → Advanced → Dump crash logs.

View File

@ -1,8 +1,8 @@
blank_issues_enabled: false blank_issues_enabled: false
contact_links: contact_links:
- name: ❌ Help with Extensions - name: Tachiyomi help website
url: https://mihon.app/docs/faq/browse/extensions url: https://tachiyomi.org/help/
about: For extension-related questions/issues about: Common questions are answered here.
- name: 🖥️ Mihon website - name: Tachiyomi extensions GitHub repository
url: https://mihon.app/ url: https://github.com/tachiyomiorg/tachiyomi-extensions
about: Guides, troubleshooting, and answers to common questions about: Issues about an extension/source/catalogue should be opened here instead.

View File

@ -0,0 +1,29 @@
---
name: "🌟 Feature request"
about: Suggest a feature to improve Tachiyomi
title: "[Feature Request] <Write short description here>"
labels: "feature"
---
**PLEASE READ THIS**
I acknowledge that:
- I have updated:
- To the latest version of the app (stable is v1.6.1)
- All extensions
- If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions
- I have searched the existing issues and this is new ticket **NOT** a duplicate or related to another open issue
- I will fill out the title and the information in this template
Note that the issue will be automatically closed if you do not fill out the title or requested information.
**DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT**
---
## Why/User Benefit/User Problem
(explain why this feature should be added)
## What/Requirements
(explain how this feature would behave)

View File

@ -1,104 +0,0 @@
name: 🐞 Issue report
description: Report an issue in TachiyomiSY
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, if possible, go to the app's **More → Settings → Advanced** page, press **Dump crash logs** and share the crash logs here.
placeholder: |
You can upload the crash log file as an attachment, or paste the crash logs in plain text if needed.
- type: input
id: tachiyomisy-version
attributes:
label: TachiyomiSY version
description: You can find your TachiyomiSY version in **More → About**.
placeholder: |
Example: "1.12.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 14"
validations:
required: true
- type: input
id: device
attributes:
label: Device
description: List your device and model.
placeholder: |
Example: "Google Pixel 8"
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: I have gone through the [FAQ](https://mihon.app/docs/faq/general) and [troubleshooting guide](https://mihon.app/docs/guides/troubleshooting/).
required: true
- label: I have updated the app to version **[1.12.0](https://github.com/jobobby04/tachiyomisy/releases/latest)**.
required: true
- label: I have filled out all of the requested information in this form, including specific version numbers.
required: true
- label: I understand that **Mihon does not have or fix any extensions**, and I **will not receive help** for any issues related to sources or extensions.
required: true

View File

@ -1,37 +0,0 @@
name: ⭐ Feature request
description: Suggest a feature to improve TachiyomiSY
labels: [Feature request]
body:
- type: textarea
id: feature-description
attributes:
label: Describe your suggested feature
description: How can TachiyomiSY 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: I have updated the app to version **[1.12.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

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

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: 713 KiB

After

Width:  |  Height:  |  Size: 489 KiB

View File

@ -1,7 +0,0 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:base"],
"labels": ["Dependencies"],
"includePaths": [".github/workflows/*", "gradle/sy.versions.toml"],
"semanticCommits": "disabled"
}

View File

@ -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

View File

@ -0,0 +1,43 @@
name: Remote Dispatch Action Initiator
on:
push:
branches:
- 'master'
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
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.17.2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
WITH_V: true
RELEASE_BRANCHES: master
DEFAULT_BUMP: patch
- name: PING - Dispatch initiating repository event
run: |
curl -X POST https://api.github.com/repos/jobobby04/TachiyomiSYPreview/dispatches \
-H 'Accept: application/vnd.github.everest-preview+json' \
-u ${{ secrets.ACCESS_TOKEN }} \
--data '{"event_type": "ping", "client_payload": { "repository": "'"$GITHUB_REPOSITORY"'" }}'

View File

@ -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

View File

@ -1,33 +1,52 @@
name: CI name: CI
on: [pull_request] on: [pull_request]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs: jobs:
build: check_wrapper:
name: Build app name: Validate Gradle Wrapper
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Clone repo - name: Clone repo
uses: actions/checkout@v4 uses: actions/checkout@v2
- name: Set up JDK - name: Validate Gradle Wrapper
uses: actions/setup-java@v4 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: with:
java-version: 17 access_token: ${{ github.token }}
distribution: temurin
- name: Set up gradle - name: Clone repo
uses: gradle/actions/setup-gradle@v4 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 - name: Build app
run: ./gradlew spotlessCheck assembleDevDebug uses: eskatos/gradle-command-action@v1
with:
arguments: assembleStandardDebug
wrapper-cache-enabled: true
dependencies-cache-enabled: true
configuration-cache-enabled: true
- name: Upload APK - name: Upload APK
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v2
with: with:
name: TachiyomiSY-${{ github.sha }}.apk 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

View File

@ -1,121 +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@v4
- name: Setup Android SDK
run: |
${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager "build-tools;29.0.3"
- name: Set up JDK
uses: actions/setup-java@v4
with:
java-version: 17
distribution: temurin
- name: Set up gradle
uses: gradle/actions/setup-gradle@v4
# SY -->
- name: Write google-services.json
uses: DamianReeves/write-file-action@v1.3
with:
path: app/google-services.json
contents: ${{ secrets.GOOGLE_SERVICES_TEXT }}
write-mode: overwrite
- name: Write client_secrets.json
uses: DamianReeves/write-file-action@v1.3
with:
path: app/src/main/assets/client_secrets.json
contents: ${{ secrets.CLIENT_SECRETS_TEXT }}
write-mode: overwrite
# SY <--
- name: Check code format
run: ./gradlew spotlessCheck
- name: Build app
run: ./gradlew assembleStandardRelease
- name: Run unit tests
run: ./gradlew testReleaseUnitTest testStandardReleaseUnitTest
- 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 }}
env:
BUILD_TOOLS_VERSION: '35.0.1'
- 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
mv 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
mv 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
mv 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
mv 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@v2
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 }} |
## If you are unsure which version to choose then go with TachiyomiSY.apk
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 }}

View File

@ -1,39 +0,0 @@
name: Remote Dispatch Action Initiator
on:
push:
branches:
- 'preview'
jobs:
trigger_preview_build:
name: Trigger preview build
runs-on: ubuntu-latest
steps:
- name: Clone repo
uses: actions/checkout@v4
- name: Set up JDK
uses: actions/setup-java@v4
with:
java-version: 17
distribution: temurin
- name: Set up gradle
uses: gradle/actions/setup-gradle@v4
- name: Create Tag
run: |
git tag "preview-${{ github.run_number }}"
git push origin "preview-${{ github.run_number }}"
- name: PING - Dispatch initiating repository event
run: |
curl -X POST https://api.github.com/repos/jobobby04/TachiyomiSYPreview/dispatches \
-H 'Accept: application/vnd.github.everest-preview+json' \
-u ${{ secrets.ACCESS_TOKEN }} \
--data '{"event_type": "ping", "client_payload": { "repository": "'"$GITHUB_REPOSITORY"'" }}'
- name: Run unit tests
run: ./gradlew testDebugUnitTest testDevDebugUnitTest

36
.github/workflows/issue_closer.yml vendored Normal file
View File

@ -0,0 +1,36 @@
name: Issue closer
on:
issues:
types: [opened, edited, reopened]
jobs:
autoclose:
runs-on: ubuntu-latest
steps:
- name: Autoclose issues
uses: arkon/issue-closer-action@v3.0
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
rules: |
[
{
"type": "title",
"regex": ".*THIS ISSUE IS IN THE WRONG REPO.*",
"message": "It was not opened in the correct repo, as the template mentioned."
},
{
"type": "title",
"regex": ".*<Write short description here>*",
"message": "The description in the title was not filled out."
},
{
"type": "body",
"regex": ".*DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT.*",
"message": "The acknowledgment section was not removed."
},
{
"type": "body",
"regex": ".*\\* (Tachiyomi version|Android version|Device): \\?.*",
"message": "Requested information in the template was not filled out."
}
]

View File

@ -1,51 +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@v2.6.1
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
duplicate-label: Duplicate
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"
},
{
"type": "both",
"regex": ".*(?:fail(?:ed|ure|s)?|can\\s*(?:no|')?t|(?:not|un).*able|(?<!n[o']?t )blocked by|error) (?:to )?(?:get past|by ?pass|penetrate)?.*cloud ?fl?are.*",
"ignoreCase": true,
"labels": ["Cloudflare protected"],
"message": "Refer to the **Solving Cloudflare issues** section at https://mihon.app/docs/guides/troubleshooting/#cloudflare. If it doesn't work, migrate to other sources or wait until they lower their protection."
},
{
"type": "both",
"regex": "^.*(myanimelist|mal).*$",
"ignoreCase": true,
"message": "For issues with linking MyAnimeList, please follow these steps:\n1. Update Mihon to version 0.16.4 or newer\n2. Change your default User-Agent (`More → Settings → Advanced → Default user agent string`)\n3. Close and restart Mihon\n4. Attempt to link MyAnimeList again\n\nIf you had MyAnimeList linked before, try to unlink it first before trying these steps."
}
]
auto-close-ignore-label: do-not-autoclose

19
.github/workflows/lock.yml vendored Normal file
View File

@ -0,0 +1,19 @@
name: Lock threads
on:
# Daily
schedule:
- cron: '0 * * * *'
# Manual trigger
workflow_dispatch:
inputs:
jobs:
lock:
runs-on: ubuntu-latest
steps:
- uses: dessant/lock-threads@v2
with:
github-token: ${{ github.token }}
issue-lock-inactive-days: '2'
pr-lock-inactive-days: '2'

View File

@ -1,26 +0,0 @@
name: Label PRs
on:
pull_request:
types: [opened]
jobs:
label_pr:
runs-on: ubuntu-latest
steps:
- name: Check PR and Add Label
uses: actions/github-script@v7
with:
script: |
const prAuthor = context.payload.pull_request.user.login;
if (prAuthor === 'weblate') {
const labels = ['Translations'];
await github.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.pull_request.number,
labels: labels
});
}

37
.gitignore vendored
View File

@ -1,24 +1,23 @@
# Build files
.gradle .gradle
.kotlin /local.properties
build /.idea/workspace.xml
# IDE files
*.iml
.idea/*
!.idea/icon.png
/captures
# Configuration files
local.properties
# macOS specific files
.DS_Store .DS_Store
.idea/
*iml
*.iml
/mainframer
/.mainframer
# SY ignores # Built files
google-services.json */build
/app/src/main/assets/client_secrets.json /build
*.apk *.apk
app/**/output.json
# Custom ignores # Hebrew assets are copied on build
/keys app/src/main/res/values-iw/
TODO.md
CHANGELOG.md
/captures
build.sh

BIN
.idea/icon.png generated

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

View File

@ -1,126 +1,76 @@
# Contributor Covenant Code of Conduct # Code of Conduct
## Our Pledge ## Our Pledge
We as members, contributors, and leaders pledge to make participation in our In the interest of fostering an open and welcoming environment, we as
community a harassment-free experience for everyone, regardless of age, body contributors and maintainers pledge to making participation in our project and
size, visible or invisible disability, ethnicity, sex characteristics, gender our community a harassment-free experience for everyone, regardless of age, body
identity and expression, level of experience, education, socio-economic status, size, disability, ethnicity, sex characteristics, gender identity and expression,
nationality, personal appearance, race, caste, color, religion, or sexual identity level of experience, education, socio-economic status, nationality, personal
and orientation. appearance, race, religion, or sexual identity and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards ## Our Standards
Examples of behavior that contributes to a positive environment for our Examples of behavior that contributes to creating a positive environment
community include: include:
* Demonstrating empathy and kindness toward other people * Using welcoming and inclusive language
* Being respectful of differing opinions, viewpoints, and experiences * Being respectful of differing viewpoints and experiences
* Giving and gracefully accepting constructive feedback * Gracefully accepting constructive criticism
* Accepting responsibility and apologizing to those affected by our mistakes, * Focusing on what is best for the community
and learning from the experience * Showing empathy towards other community members
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include: Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery, and sexual attention or * The use of sexualized language or imagery and unwelcome sexual attention or
advances of any kind advances
* Trolling, insulting or derogatory comments, and personal or political attacks * Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment * Public or private harassment
* Publishing others' private information, such as a physical or email * Publishing others' private information, such as a physical or electronic
address, without their explicit permission address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a * Other conduct which could reasonably be considered inappropriate in a
professional setting professional setting
## Enforcement Responsibilities ## Our Responsibilities
Community moderators are responsible for clarifying and enforcing our standards of Project maintainers are responsible for clarifying the standards of acceptable
acceptable behavior and will take appropriate and fair corrective action in behavior and are expected to take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive, response to any instances of unacceptable behavior.
or harmful.
Community moderators have the right and responsibility to remove, edit, or reject Project maintainers have the right and responsibility to remove, edit, or
comments, commits, code, wiki edits, issues, and other contributions that are reject comments, commits, code, wiki edits, issues, and other contributions
not aligned to this Code of Conduct, and will communicate reasons for moderation that are not aligned to this Code of Conduct, or to ban temporarily or
decisions when appropriate. permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope ## Scope
This Code of Conduct applies within all community spaces, and also applies when This Code of Conduct applies both within project spaces and in public spaces
an individual is officially representing the community in public spaces. when an individual is representing the project or its community. Examples of
Examples of representing our community include using an official e-mail address, representing a project or community include using an official project e-mail
posting via an official social media account, or acting as an appointed address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement ## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community moderators responsible for enforcement at reported by contacting the project team at the Tachiyomi [Discord server](https://discord.gg/tachiyomi). All
the [Mihon Discord server](https://discord.gg/mihon). complaints will be reviewed and investigated and will result in a response that
All complaints will be reviewed and investigated promptly and fairly. is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
All community moderators are obligated to respect the privacy and security of the Project maintainers who do not follow or enforce the Code of Conduct in good
reporter of any incident. faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Enforcement Guidelines
Community moderators will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community moderators, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution ## Attribution
This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org/), This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
version 2.1, available at available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[v2.1](https://www.contributor-covenant.org/version/2/1/code_of_conduct.html).
Community Impact Guidelines were inspired by [homepage]: https://www.contributor-covenant.org
[Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).
For answers to common questions about this code of conduct, see the FAQ at For answers to common questions about this code of conduct, see
[FAQ](https://www.contributor-covenant.org/faq). Translations are available https://www.contributor-covenant.org/faq
at [translations](https://www.contributor-covenant.org/translations).

View File

@ -1,4 +1,4 @@
Looking to report an issue/bug or make a feature request? Please refer to the [README file](/README.md#issues-feature-requests-and-contributing). Looking to report an issue/bug or make a feature request? Please refer to the [README file](https://github.com/tachiyomiorg/tachiyomi#issues-feature-requests-and-contributing).
--- ---
@ -9,56 +9,26 @@ Thanks for your interest in contributing to Tachiyomi!
Pull requests are welcome! Pull requests are welcome!
If you're interested in taking on [an open issue](https://github.com/jobobby04/TachiyomiSY/issues), please comment on it so others are aware. If you're interested in taking on [an open issue](https://github.com/tachiyomiorg/tachiyomi/issues), please comment on it so others are aware.
You do not need to ask for permission nor an assignment.
## Prerequisites
Before you start, please note that the ability to use following technologies is **required** and that existing contributors will not actively teach them to you.
- Basic [Android development](https://developer.android.com/)
- [Kotlin](https://kotlinlang.org/)
### Tools
- [Android Studio](https://developer.android.com/studio)
- Emulator or phone with developer options enabled to test changes.
## Getting help
- Join [the Discord server](https://discord.gg/mihon) for online help and to ask questions while developing.
# Translations # Translations
Translations are done externally via Weblate. See [our website](https://mihon.app/docs/contribute#translation) for more details. Translations are done externally via Weblate. See [our website](https://tachiyomi.org/help/contribution/#translation) for more details.
# Forks # Forks
Forks are allowed so long as they abide by [the project's LICENSE](/LICENSE). Forks are allowed so long as they abide by [the project's LICENSE](https://github.com/tachiyomiorg/tachiyomi/blob/master/LICENSE).
When creating a fork, remember to: When creating a fork, remember to:
- To avoid confusion with the main app: - To avoid confusion with the main app:
- Change the app name - Change the app name
- Change the app icon - Change the app icon
- Change or disable the [app update checker](/app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateChecker.kt) - Change or disable the [app update checker](https://github.com/tachiyomiorg/tachiyomi/blob/master/app/src/main/java/eu/kanade/tachiyomi/data/updater/github/GithubUpdateChecker.kt)
- To avoid installation conflicts: - To avoid installation conflicts:
- Change the `applicationId` in [`build.gradle.kts`](/app/build.gradle.kts) - Change the `applicationId` in [`build.gradle.kts`](https://github.com/tachiyomiorg/tachiyomi/blob/master/app/build.gradle.kts)
- To avoid having your data polluting the main app's analytics and crash report services:
- If you want to use Firebase analytics, replace [`google-services.json`](https://github.com/tachiyomiorg/tachiyomi/blob/master/app/src/standard/google-services.json) with your own
### Supporting Cloud Sync - Google Drive Implementation - If you want to use ACRA crash reporting, replace the `ACRA_URI` endpoint in [`build.gradle.kts`](https://github.com/tachiyomiorg/tachiyomi/blob/master/app/build.gradle.kts) with your own
1. Go to [Google Cloud Console](https://console.cloud.google.com)
2. Create a new project
3. Go to API & Services -> Library -> Google Drive API and click enable
4. Go to API & Services -> Oauth consent screen
5. Create it, fill in the app name, user support email, and developer contact information
6. In the next screen, click add or remove scopes, and add the `.../auth/drive.appdata` and `.../auth/drive.file` scopes
7. Don't add any test users and go back to the dashboard
8. Click publish
9. Go to API & Services -> Credentials
10. Click Create credentials -> Oauth client ID
11. Select Android, give it a name, and set `eu.kanade.google.oauth` as the package name
12. To get the SHA-1 key, run `keytool -printcert -jarfile app-standard-universal-release.apk` on your apk, and copy the listed SHA-1
13. Expand advanced settings, and enable Custom URL scheme
14. After that just download the json, name it to `client_secrets.json` and put it in `app/src/main/assets/`

View File

@ -1,25 +1,20 @@
| Preview Builds | Release Builds | Mihon Support Server | | Preview Builds | Release Builds | Tachiyomi Support Server |
|-------|----------|----------| |-------|----------|----------|
| [![Preview](https://github.com/jobobby04/TachiyomiSYPreview/workflows/Remote%20Dispatch%20Build%20App/badge.svg)](https://github.com/jobobby04/TachiyomiSYPreview/releases) | [![stable release](https://img.shields.io/github/release/jobobby04/tachiyomisy.svg?maxAge=3600&label=download)](https://github.com/jobobby04/tachiyomisy/releases/latest) | [![Discord](https://img.shields.io/discord/1195734228319617024.svg?label=discord&labelColor=7289da&color=2c2f33&style=flat)](https://discord.gg/mihon) | | [![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) |
# ![app icon](./.github/readme-images/app-icon.png)TachiyomiSY # ![app icon](./.github/readme-images/app-icon.png)TachiyomiSY
Mihon is a free and open source manga reader for Android 6.0 and above. This version of Mihon, TachiyomiSY was based off TachiyomiAZ. This version is meant to push forward in the ways of usability and features. TachiyomiSY tries to push forward where it can, but staying in a place where it can easily grab updates and features from the main app, it tries to make new features, or take features from other forks like J2K and Neko. Tachiyomi is a free and open source manga reader for Android 5.0 and above. This version of Tachiyomi, TachiyomiSY was based off TachiyomiAZ. This version is meant to push forward in the ways of usability and features. TachiyomiSY tries to push forward where it can, but staying in a place where it can easily grab updates and features from the main app, it tries to make new features, or take features from other forks like J2K and Neko.
![screenshots of app](./.github/readme-images/screens.png) ![screenshots of app](./.github/readme-images/screens.png)
## Features ## Features
Features of SY-Plus include: Features of Tachiyomi(original) include:
* Automatically trust extensions * Online reading from [a variety of sources](https://github.com/tachiyomiorg/tachiyomi-extensions)
* Removal of rate limits * Local reading of downloaded manga
* Re addition of more frequent library update timers
Features of Mihon(original) include:
* Online reading from a variety of sources
* Local reading of downloaded content
* A configurable reader with multiple viewers, reading directions and other settings. * A configurable reader with multiple viewers, reading directions and other settings.
* Tracker support: [MyAnimeList](https://myanimelist.net/), [AniList](https://anilist.co/), [Kitsu](https://kitsu.app/), [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 * Categories to organize your library
* Light and dark themes * Light and dark themes
* Schedule updating your library for new chapters * Schedule updating your library for new chapters
@ -47,6 +42,7 @@ Features of TachiyomiSY include:
* Page preload customization * Page preload customization
* Customize image cache size * Customize image cache size
* Batch import of custom sources and featured extensions * Batch import of custom sources and featured extensions
* Automatic CAPTCHA solving
* Advanced source settings page, searching, enable/disable all * Advanced source settings page, searching, enable/disable all
* Click tag for local search, long click tag for global search * Click tag for local search, long click tag for global search
* Merge multiple of the same manga from different sources * Merge multiple of the same manga from different sources
@ -62,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: 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) * 8Muses (EroMuse)
* HBrowse * HBrowse
* HentaiCafe (inside Foolside)
* Hitomi.la
* Mangadex * Mangadex
* NHentai * NHentai
* PervEden (EN and IT)
* Puruin * Puruin
* Tsumino * Tsumino
@ -72,37 +71,31 @@ Get the app from our [releases page](https://github.com/jobobby04/tachiyomisy/re
If you want to try new features before they get to the stable release, you can download the preview version [here](https://github.com/jobobby04/tachiyomisypreview/releases). If you want to try new features before they get to the stable release, you can download the preview version [here](https://github.com/jobobby04/tachiyomisypreview/releases).
## Translation
Feel free to translate the project on [Weblate](https://hosted.weblate.org/projects/mihon/tachiyomisy/)
<details><summary>Translation Progress</summary>
<a href="https://hosted.weblate.org/engage/mihon/">
<img src="https://hosted.weblate.org/widgets/mihon/-/tachiyomisy/multi-auto.svg" alt="Translation status" />
</a>
</details>
## Issues, Feature Requests and Contributing ## Issues, Feature Requests and Contributing
Please make sure to read the full guidelines. Your issue may be closed without warning if you do not. Please make sure to read the full guidelines. Your issue may be closed without warning if you do not.
<details><summary>Issues</summary> <details><summary>Issues</summary>
1. **Before reporting a new issue, take a look at the [FAQ](https://mihon.app/docs/faq/general), the [changelog](https://github.com/jobobby04/tachiyomisy/releases) and the already opened [issues](https://github.com/jobobby04/tachiyomisy/issues).** 1. **Before reporting a new issue, take a look at the [FAQ](https://tachiyomi.org/help/faq/), the [changelog](https://github.com/jobobby04/tachiyomisy/releases) and the already opened [issues](https://github.com/jobobby04/tachiyomisy/issues).**
2. If you are unsure, ask here: [![Discord](https://img.shields.io/discord/1195734228319617024.svg)](https://discord.gg/mihon) 2. If you are unsure, ask here: [![Discord](https://img.shields.io/discord/349436576037732353.svg)](https://discord.gg/tachiyomi)
</details> </details>
<details><summary>Bugs</summary> <details><summary>Bugs</summary>
* Include version (More → About → Version) * Include version (More > About > Version)
* If not latest, try updating, it may have already been solved * If not latest, try updating, it may have already been solved
* Preview version is equal to the number of commits as seen on the main page * Preview version is equal to the number of commits as seen in the main page
* Include steps to reproduce (if not obvious from description) * Include steps to reproduce (if not obvious from description)
* Include screenshot (if needed) * Include screenshot (if needed)
* If it could be device-dependent, try reproducing on another device (if possible) * If it could be device-dependent, try reproducing on another device (if possible)
* For large logs use http://pastebin.com/ (or similar)
* Don't group unrelated requests into one issue * Don't group unrelated requests into one issue
Use the [issue forms](https://github.com/jobobby04/TachiyomiSY/issues/new/choose) to submit a bug. DO: https://github.com/tachiyomiorg/tachiyomi/issues/24 https://github.com/tachiyomiorg/tachiyomi/issues/71
DON'T: https://github.com/tachiyomiorg/tachiyomi/issues/75
</details> </details>
@ -111,7 +104,7 @@ Use the [issue forms](https://github.com/jobobby04/TachiyomiSY/issues/new/choose
* Write a detailed issue, explaining what it should do or how. Avoid writing just "like X app does" * Write a detailed issue, explaining what it should do or how. Avoid writing just "like X app does"
* Include screenshot (if needed) * Include screenshot (if needed)
Source requests are not accepted. Source requests should be created at https://github.com/tachiyomiorg/tachiyomi-extensions, they do not belong in this repository.
</details> </details>
<details><summary>Contributing</summary> <details><summary>Contributing</summary>
@ -126,5 +119,5 @@ See [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md).
## FAQ ## FAQ
[See our website.](https://mihon.app/) [See our website.](https://tachiyomi.org/)
You can also reach out to us on [Discord](https://discord.gg/mihon). You can also reach out to us on [Discord](https://discord.gg/tachiyomi).

6
app/.gitignore vendored Executable file
View File

@ -0,0 +1,6 @@
/build
*iml
*.iml
custom.gradle
google-services.json
output.json

View File

@ -1,96 +1,79 @@
@file:Suppress("ChromeOsAbiSupport") import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import java.io.ByteArrayOutputStream
import mihon.buildlogic.getBuildTime import java.text.SimpleDateFormat
import mihon.buildlogic.getCommitCount import java.util.Date
import mihon.buildlogic.getGitSha import java.util.TimeZone
plugins { plugins {
id("mihon.android.application") id("com.android.application")
id("mihon.android.application.compose") id("com.mikepenz.aboutlibraries.plugin")
kotlin("android")
kotlin("kapt")
kotlin("plugin.parcelize") kotlin("plugin.parcelize")
kotlin("plugin.serialization") kotlin("plugin.serialization")
// id("com.github.zellius.shortcut-helper") id("com.github.zellius.shortcut-helper")
alias(libs.plugins.aboutLibraries) // Realm (EH)
id("com.github.ben-manes.versions") id("realm-android")
} }
if (gradle.startParameter.taskRequests.toString().contains("Standard")) { if (!gradle.startParameter.taskRequests.toString().contains("Debug")) {
pluginManager.apply { apply(plugin = "com.google.gms.google-services")
apply(libs.plugins.google.services.get().pluginId) // Firebase Crashlytics
apply(libs.plugins.firebase.crashlytics.get().pluginId) apply(plugin = "com.google.firebase.crashlytics")
}
} }
// shortcutHelper.setFilePath("./shortcuts.xml") shortcutHelper.setFilePath("./shortcuts.xml")
val supportedAbis = setOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
android { android {
namespace = "eu.kanade.tachiyomi" compileSdkVersion(AndroidConfig.compileSdk)
buildToolsVersion(AndroidConfig.buildTools)
ndkVersion = AndroidConfig.ndk
defaultConfig { defaultConfig {
applicationId = "eu.kanade.tachiyomi.sy" applicationId = "eu.kanade.tachiyomi.sy"
minSdkVersion(AndroidConfig.minSdk)
versionCode = 75 targetSdkVersion(AndroidConfig.targetSdk)
versionName = "1.12.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
versionCode = 15
versionName = "1.6.1"
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"") buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"") buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
buildConfigField("String", "BUILD_TIME", "\"${getBuildTime(useLastCommitTime = false)}\"") buildConfigField("String", "BUILD_TIME", "\"${getBuildTime()}\"")
buildConfigField("boolean", "INCLUDE_UPDATER", "false") buildConfigField("boolean", "INCLUDE_UPDATER", "false")
multiDexEnabled = true
ndk { ndk {
abiFilters += supportedAbis abiFilters += setOf("armeabi-v7a", "arm64-v8a", "x86")
} }
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
} }
splits { buildFeatures {
abi { viewBinding = true
isEnable = true
reset()
include(*supportedAbis.toTypedArray())
isUniversalApk = true
}
} }
buildTypes { buildTypes {
named("debug") { named("debug") {
versionNameSuffix = "-${getCommitCount()}" versionNameSuffix = "-${getCommitCount()}"
applicationIdSuffix = ".debug" applicationIdSuffix = ".debug"
isPseudoLocalesEnabled = true
} }
create("releaseTest") { create("releaseTest") {
applicationIdSuffix = ".rt" applicationIdSuffix = ".rt"
//isMinifyEnabled = true //isMinifyEnabled = true
//isShrinkResources = true //isShrinkResources = true
setProguardFiles(listOf(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")) isZipAlignEnabled = true
matchingFallbacks.add("release") setProguardFiles(listOf(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"))
} }
named("release") { named("release") {
isMinifyEnabled = true isMinifyEnabled = true
isShrinkResources = true isShrinkResources = true
setProguardFiles(listOf(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")) isZipAlignEnabled = true
setProguardFiles(listOf(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"))
buildConfigField("String", "BUILD_TIME", "\"${getBuildTime(useLastCommitTime = true)}\"")
}
create("benchmark") {
initWith(getByName("release"))
signingConfig = signingConfigs.getByName("debug")
matchingFallbacks.add("release")
isDebuggable = false
isProfileable = true
versionNameSuffix = "-benchmark"
applicationIdSuffix = ".benchmark"
} }
} }
sourceSets { flavorDimensions("default")
getByName("benchmark").res.srcDirs("src/debug/res")
}
flavorDimensions.add("default")
productFlavors { productFlavors {
create("standard") { create("standard") {
@ -101,230 +84,290 @@ android {
dimension = "default" dimension = "default"
} }
create("dev") { create("dev") {
resConfigs("en", "xxhdpi")
dimension = "default" dimension = "default"
} }
} }
packaging { packagingOptions {
resources.excludes.addAll( exclude("META-INF/DEPENDENCIES")
listOf( exclude("LICENSE.txt")
"kotlin-tooling-metadata.json", exclude("META-INF/LICENSE")
"META-INF/DEPENDENCIES", exclude("META-INF/LICENSE.txt")
"LICENSE.txt", exclude("META-INF/NOTICE")
"META-INF/LICENSE", exclude("META-INF/*.kotlin_module")
"META-INF/**/LICENSE.txt",
"META-INF/*.properties", // Compatibility for two RxJava versions (EXH)
"META-INF/**/*.properties", exclude("META-INF/rxjava.properties")
"META-INF/README.md",
"META-INF/NOTICE",
"META-INF/*.version",
),
)
} }
dependenciesInfo { dependenciesInfo {
includeInApk = false includeInApk = false
} }
buildFeatures { lintOptions {
viewBinding = true disable("MissingTranslation", "ExtraTranslation")
buildConfig = true isAbortOnError = false
isCheckReleaseBuilds = false
// Disable some unused things
aidl = false
renderScript = false
shaders = false
} }
lint { compileOptions {
abortOnError = false sourceCompatibility = JavaVersion.VERSION_1_8
checkReleaseBuilds = false targetCompatibility = JavaVersion.VERSION_1_8
}
} }
kotlin { kotlinOptions {
compilerOptions { jvmTarget = JavaVersion.VERSION_1_8.toString()
freeCompilerArgs.addAll(
"-opt-in=androidx.compose.animation.ExperimentalAnimationApi",
"-opt-in=androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi",
"-opt-in=androidx.compose.foundation.ExperimentalFoundationApi",
"-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi",
"-opt-in=androidx.compose.material3.ExperimentalMaterial3Api",
"-opt-in=androidx.compose.ui.ExperimentalComposeUiApi",
"-opt-in=coil3.annotation.ExperimentalCoilApi",
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
"-opt-in=kotlinx.coroutines.FlowPreview",
"-opt-in=kotlinx.coroutines.InternalCoroutinesApi",
"-opt-in=kotlinx.serialization.ExperimentalSerializationApi",
)
} }
} }
dependencies { dependencies {
implementation(projects.i18n)
// SY -->
implementation(projects.i18nSy)
// SY <--
implementation(projects.core.common)
implementation(projects.coreMetadata)
implementation(projects.sourceApi)
implementation(projects.sourceLocal)
implementation(projects.data)
implementation(projects.domain)
implementation(projects.presentationCore)
implementation(projects.presentationWidget)
// Compose // Source models and interfaces from Tachiyomi 1.x
implementation(compose.activity) implementation("tachiyomi.sourceapi:source-api:1.1")
implementation(compose.foundation)
implementation(compose.material3.core)
implementation(compose.material.icons)
implementation(compose.animation)
implementation(compose.animation.graphics)
debugImplementation(compose.ui.tooling)
implementation(compose.ui.tooling.preview)
implementation(compose.ui.util)
implementation(androidx.interpolator)
implementation(androidx.paging.runtime)
implementation(androidx.paging.compose)
implementation(libs.bundles.sqlite)
// SY -->
implementation(sylibs.sqlcipher)
// SY <--
implementation(kotlinx.reflect)
implementation(kotlinx.immutables)
implementation(platform(kotlinx.coroutines.bom))
implementation(kotlinx.bundles.coroutines)
// AndroidX libraries // AndroidX libraries
implementation(androidx.annotation) implementation("androidx.annotation:annotation:1.3.0-alpha01")
implementation(androidx.appcompat) implementation("androidx.appcompat:appcompat:1.3.0-rc01")
implementation(androidx.biometricktx) implementation("androidx.biometric:biometric-ktx:1.2.0-alpha03")
implementation(androidx.constraintlayout) implementation("androidx.browser:browser:1.3.0")
implementation(androidx.corektx) implementation("androidx.cardview:cardview:1.0.0")
implementation(androidx.splashscreen) implementation("androidx.constraintlayout:constraintlayout:2.1.0-beta01")
implementation(androidx.recyclerview) implementation("androidx.coordinatorlayout:coordinatorlayout:1.1.0")
implementation(androidx.viewpager) implementation("androidx.core:core-ktx:1.3.2")
implementation(androidx.profileinstaller) implementation("androidx.multidex:multidex:2.0.1")
implementation("androidx.preference:preference-ktx:1.1.1")
implementation("androidx.recyclerview:recyclerview:1.2.0")
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01")
implementation(androidx.bundles.lifecycle) val lifecycleVersion = "2.3.0"
implementation("androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion")
implementation("androidx.lifecycle:lifecycle-process:$lifecycleVersion")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion")
// Job scheduling // Job scheduling
implementation(androidx.workmanager) implementation("androidx.work:work-runtime-ktx:2.5.0")
// RxJava // UI library
implementation(libs.rxjava) implementation("com.google.android.material:material:1.3.0")
// Networking "standardImplementation"("com.google.firebase:firebase-core:18.0.3")
implementation(libs.bundles.okhttp)
implementation(libs.okio)
implementation(libs.conscrypt.android) // TLS 1.3 support for Android < 10
// Data serialization (JSON, protobuf, xml) // ReactiveX
implementation(kotlinx.bundles.serialization) 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")
// HTML parser // Network client
implementation(libs.jsoup) val okhttpVersion = "5.0.0-alpha.2"
implementation("com.squareup.okhttp3:okhttp:$okhttpVersion")
implementation("com.squareup.okhttp3:logging-interceptor:$okhttpVersion")
implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:$okhttpVersion")
implementation("com.squareup.okio:okio:2.10.0")
// TLS 1.3 support for Android < 10
implementation("org.conscrypt:conscrypt-android:2.5.1")
// JSON
val kotlinSerializationVersion = "1.1.0"
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinSerializationVersion")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-protobuf:$kotlinSerializationVersion")
implementation("com.google.code.gson:gson:2.8.6")
implementation("com.github.salomonbrys.kotson:kotson:2.5.0")
// JavaScript engine
implementation("com.squareup.duktape:duktape-android:1.3.0")
// Disk // Disk
implementation(libs.disklrucache) implementation("com.jakewharton:disklrucache:2.0.2")
implementation(libs.unifile) implementation("com.github.tachiyomiorg:unifile:17bec43")
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 // Preferences
implementation(libs.preferencektx) implementation("com.github.tfcporciuncula.flow-preferences:flow-preferences:1.3.4")
// Model View Presenter
val nucleusVersion = "3.0.0"
implementation("info.android15.nucleus:nucleus:$nucleusVersion")
implementation("info.android15.nucleus:nucleus-support-v7:$nucleusVersion")
// Dependency injection // Dependency injection
implementation(libs.injekt) implementation("com.github.inorichi.injekt:injekt-core:65b0440")
// Image loading // Image library
implementation(platform(libs.coil.bom)) val glideVersion = "4.12.0"
implementation(libs.bundles.coil) implementation("com.github.bumptech.glide:glide:$glideVersion")
implementation(libs.subsamplingscaleimageview) { implementation("com.github.bumptech.glide:okhttp3-integration:$glideVersion")
exclude(module = "image-decoder") kapt("com.github.bumptech.glide:compiler:$glideVersion")
}
implementation(libs.image.decoder)
// UI libraries implementation("com.github.tachiyomiorg:subsampling-scale-image-view:547d9c0")
implementation(libs.material)
implementation(libs.flexible.adapter.core)
implementation(libs.photoview)
implementation(libs.directionalviewpager) {
exclude(group = "androidx.viewpager", module = "viewpager")
}
implementation(libs.insetter)
implementation(libs.richeditor.compose)
implementation(libs.aboutLibraries.compose)
implementation(libs.bundles.voyager)
implementation(libs.compose.materialmotion)
implementation(libs.swipe)
implementation(libs.compose.webview)
implementation(libs.compose.grid)
implementation(libs.reorderable)
implementation(libs.bundles.markdown)
// Logging // Logging
implementation(libs.logcat) implementation("com.jakewharton.timber:timber:4.7.1")
// Crash reports/analytics // Crash reports
// "standardImplementation"(platform(libs.firebase.bom)) //implementation("ch.acra:acra-http:5.7.0")
// "standardImplementation"(libs.firebase.analytics)
// "standardImplementation"(libs.firebase.crashlytics)
// Shizuku // Sort
implementation(libs.bundles.shizuku) implementation("com.github.gpanther:java-nat-sort:natural-comparator-1.1")
// UI
implementation("com.dmitrymalkovich.android:material-design-dimens:1.4")
implementation("com.github.dmytrodanylyk.android-process-button:library:1.0.4")
implementation("eu.davidea:flexible-adapter:5.1.0")
implementation("eu.davidea:flexible-adapter-ui:1.0.0")
implementation("com.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.1.0")
implementation("com.github.chrisbanes:PhotoView:2.3.0")
implementation("com.github.tachiyomiorg:DirectionalViewPager:1.0.0")
implementation("dev.chrisbanes.insetter:insetter:0.5.0")
// 3.2.0+ introduces weird UI blinking or cut off issues on some devices
val materialDialogsVersion = "3.1.1"
implementation("com.afollestad.material-dialogs:core:$materialDialogsVersion")
implementation("com.afollestad.material-dialogs:input:$materialDialogsVersion")
implementation("com.afollestad.material-dialogs:datetime:$materialDialogsVersion")
// Conductor
implementation("com.bluelinelabs:conductor:2.1.5")
implementation("com.bluelinelabs:conductor-support:2.1.5") {
exclude(group = "com.android.support")
}
implementation("com.github.tachiyomiorg:conductor-support-preference:2.0.1")
// FlowBinding
val flowbindingVersion = "0.12.0"
implementation("io.github.reactivecircus.flowbinding:flowbinding-android:$flowbindingVersion")
implementation("io.github.reactivecircus.flowbinding:flowbinding-appcompat:$flowbindingVersion")
implementation("io.github.reactivecircus.flowbinding:flowbinding-recyclerview:$flowbindingVersion")
implementation("io.github.reactivecircus.flowbinding:flowbinding-swiperefreshlayout:$flowbindingVersion")
implementation("io.github.reactivecircus.flowbinding:flowbinding-viewpager:$flowbindingVersion")
// Licenses
implementation("com.mikepenz:aboutlibraries:${BuildPluginsVersion.ABOUTLIB_PLUGIN}")
// Tests // Tests
testImplementation(libs.bundles.test) testImplementation("junit:junit:4.13.2")
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.3"
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion")
// For detecting memory leaks; see https://square.github.io/leakcanary/ // For detecting memory leaks; see https://square.github.io/leakcanary/
// debugImplementation(libs.leakcanary.android) // debugImplementation("com.squareup.leakcanary:leakcanary-android:2.7")
implementation(libs.leakcanary.plumber)
testImplementation(kotlinx.coroutines.test)
// SY --> // 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) // Text distance (EH)
implementation(sylibs.simularity) implementation ("info.debatty:java-string-similarity:2.0.0")
// Firebase (EH) // Firebase (EH)
implementation(platform(libs.firebase.bom)) implementation("com.google.firebase:firebase-analytics-ktx:18.0.3")
implementation(libs.firebase.analytics) implementation("com.google.firebase:firebase-crashlytics-ktx:17.4.1")
implementation(libs.firebase.crashlytics)
// Better logging (EH) // Better logging (EH)
implementation(sylibs.xlog) implementation("com.elvishew:xlog:1.9.0")
// 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) // RatingBar (SY)
implementation(sylibs.ratingbar) implementation ("me.zhanghai.android.materialratingbar:library:1.4.0")
implementation(sylibs.composeRatingbar)
// Google drive // JsonReader for similar manga
implementation(sylibs.google.api.services.drive) implementation("com.squareup.moshi:moshi:1.12.0")
implementation(sylibs.google.api.client.oauth)
// Koin implementation("com.mikepenz:fastadapter:5.4.0")
implementation(sylibs.koin.core) // SY <--
implementation(sylibs.koin.android)
// ZXing Android Embedded
implementation(sylibs.zxing.android.embedded)
} }
androidComponents { tasks {
onVariants(selector().withFlavor("default" to "standard")) { // See https://kotlinlang.org/docs/reference/experimental.html#experimental-status-of-experimental-api(-markers)
// Only excluding in standard flavor because this breaks withType<KotlinCompile> {
// Layout Inspector's Compose tree kotlinOptions.freeCompilerArgs += listOf(
it.packaging.resources.excludes.add("META-INF/*.version") "-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"
)
}
// 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 {
dependsOn(formatKotlin, copyHebrewStrings)
} }
} }
buildscript { buildscript {
repositories {
mavenCentral()
}
dependencies { dependencies {
classpath(kotlinx.gradle) classpath(kotlin("gradle-plugin", version = BuildPluginsVersion.KOTLIN))
} }
} }
// Git is needed in your system PATH for these commands to work.
// If it's not installed, you can return a random value as a workaround
fun getCommitCount(): String {
return runCommand("git rev-list --count HEAD")
// return "1"
}
fun getGitSha(): String {
return runCommand("git rev-parse --short HEAD")
// return "1"
}
fun getBuildTime(): String {
val df = SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'")
df.timeZone = TimeZone.getTimeZone("UTC")
return df.format(Date())
}
fun runCommand(command: String): String {
val byteOut = ByteArrayOutputStream()
project.exec {
commandLine = command.split(" ")
standardOutput = byteOut
}
return String(byteOut.toByteArray()).trim()
}

View File

@ -1,35 +0,0 @@
-allowaccessmodification
-dontusemixedcaseclassnames
-ignorewarnings
-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>(...);
}

265
app/proguard-rules.pro vendored
View File

@ -1,34 +1,87 @@
-dontobfuscate -dontobfuscate
-keep,allowoptimization class eu.kanade.** # Extensions may require methods unused in the core app
-keep,allowoptimization class tachiyomi.** -dontwarn eu.kanade.tachiyomi.**
-keep,allowoptimization class mihon.** -keep class eu.kanade.tachiyomi.** { public protected private *; }
# Keep common dependencies used in extensions -keep class org.jsoup.** { *; }
-keep,allowoptimization class androidx.preference.** { public protected *; } -keep class kotlin.** { *; }
-keep,allowoptimization class kotlin.** { public protected *; } -keep class okhttp3.** { *; }
-keep,allowoptimization class kotlinx.coroutines.** { public protected *; } -keep class com.google.gson.** { *; }
-keep,allowoptimization class kotlinx.serialization.** { public protected *; } -keep class com.github.salomonbrys.kotson.** { *; }
-keep,allowoptimization class kotlin.time.** { public protected *; } -keep class com.squareup.duktape.** { *; }
-keep,allowoptimization class okhttp3.** { public protected *; }
-keep,allowoptimization class okio.** { public protected *; }
-keep,allowoptimization class org.jsoup.** { public protected *; }
-keep,allowoptimization class rx.** { public protected *; }
-keep,allowoptimization class app.cash.quickjs.** { public protected *; }
-keep,allowoptimization class uy.kohesive.injekt.** { public protected *; }
# From extensions-lib # === Keep EH classes
-keep,allowoptimization class eu.kanade.tachiyomi.network.interceptor.RateLimitInterceptorKt { public protected *; } -keep class exh.** { *; }
-keep,allowoptimization class eu.kanade.tachiyomi.network.interceptor.SpecificHostRateLimitInterceptorKt { public protected *; } -keep class xyz.nulldev.** { *; }
-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 *; }
# Debug functions # === Keep RxAndroid, https://github.com/ReactiveX/RxAndroid/issues/350
-keep,allowoptimization class exh.debug.DebugFunctions { public *; } -keep class rx.android.** { *; }
##---------------Begin: proguard configuration for RxJava 1.x ---------- # 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.** -dontwarn sun.misc.**
-keepclassmembers class rx.internal.util.unsafe.*ArrayQueue*Field* { -keepclassmembers class rx.internal.util.unsafe.*ArrayQueue*Field* {
@ -45,62 +98,6 @@
} }
-dontnote rx.internal.util.PlatformDependent -dontnote rx.internal.util.PlatformDependent
##---------------End: proguard configuration for RxJava 1.x ----------
##---------------Begin: proguard configuration for okhttp ----------
-keepclasseswithmembers class okhttp3.MultipartBody$Builder { *; }
##---------------End: proguard configuration for okhttp ----------
##---------------Begin: proguard configuration for kotlinx.serialization ----------
-keepattributes *Annotation*, InnerClasses
-dontnote kotlinx.serialization.** # 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 tachiyomi.**$$serializer { *; }
-keepclassmembers class tachiyomi.** {
*** Companion;
}
-keepclasseswithmembers class tachiyomi.** {
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 # === Reactive network: https://github.com/pwittchen/ReactiveNetwork/tree/v0.12.4#proguard-configuration
-dontwarn com.github.pwittchen.reactivenetwork.library.rx2.ReactiveNetwork -dontwarn com.github.pwittchen.reactivenetwork.library.rx2.ReactiveNetwork
@ -121,44 +118,63 @@
# === Okio: https://github.com/square/okio/tree/9b8545e7fa267c9d89753283990f24a35cd69cd6#proguard # === Okio: https://github.com/square/okio/tree/9b8545e7fa267c9d89753283990f24a35cd69cd6#proguard
-dontwarn okio.** -dontwarn okio.**
# === Keep RxAndroid, https://github.com/ReactiveX/RxAndroid/issues/350 # === GSON: https://raw.githubusercontent.com/google/gson/master/examples/android-proguard-example/proguard.cfg
-keep class rx.android.** { *; } # 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 # For using GSON @Expose annotation
-keep public enum nl.adaptivity.xmlutil.EventType { *; } -keepattributes *Annotation*
# Firebase # Gson specific classes
-keep class com.google.firebase.installations.** { *; } -dontwarn sun.misc.**
-keep interface com.google.firebase.installations.** { *; } #-keep class com.google.gson.stream.** { *; }
# Google Drive # Application classes that will be serialized/deserialized over Gson
-keep class com.google.api.services.** { *; } -keep class com.google.gson.examples.android.model.** { <fields>; }
# Google OAuth # Prevent proguard from stripping interface information from TypeAdapterFactory,
-keep class com.google.api.client.** { *; } # 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
# SY --> # Prevent R8 from leaving Data object members always null
# SqlCipher -keepclassmembers,allowobfuscation class * {
-keepclassmembers class net.zetetic.database.sqlcipher.SQLiteCustomFunction { *; } @com.google.gson.annotations.SerializedName <fields>;
-keepclassmembers class net.zetetic.database.sqlcipher.SQLiteConnection { *; } }
-keepclassmembers class net.zetetic.database.sqlcipher.SQLiteGlobal { *; }
-keepclassmembers class net.zetetic.database.sqlcipher.SQLiteDebug { *; }
-keepclassmembers class net.zetetic.database.sqlcipher.SQLiteDebug$* { *; }
# SY <--
# Design library # == Nucleus
-dontwarn com.google.android.material.** -keepclassmembers class * extends nucleus.presenter.Presenter {
-keep class com.google.android.material.** { *; } <init>();
-keep interface com.google.android.material.** { *; } }
-keep public class com.google.android.material.R$* { *; }
-keep class com.hippo.image.** { *; } # TODO Changeloglib? Does it need proguard?
-keep interface com.hippo.image.** { *; }
# === Injekt # === Injekt
## From original config: "Attempt to fix: java.lang.NoClassDefFoundError: uy.kohesive.injekt.registry.default.DefaultRegistrar$NOKEY$1" ## From original config: "Attempt to fix: java.lang.NoClassDefFoundError: uy.kohesive.injekt.registry.default.DefaultRegistrar$NOKEY$1"
-keep class uy.kohesive.injekt.** { *; } -keep class uy.kohesive.injekt.** { *; }
# === Glide
-keep public class * implements com.bumptech.glide.module.GlideModule
-keep public class * extends com.bumptech.glide.module.AppGlideModule
-keep public enum com.bumptech.glide.load.ImageHeaderParser$** {
**[] $VALUES;
public *;
}
-dontwarn com.bumptech.glide.load.resource.bitmap.VideoDecoder
# === Glide-transformations: https://github.com/wasabeef/glide-transformations/blob/3aa8e53c6a51b8351d312f802ba1354c5b115168/transformations/proguard-rules.txt
-dontwarn jp.co.cyberagent.android.gpuimage.**
# === Conductor
# 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 # === RxBinding
-dontwarn com.google.auto.value.AutoValue -dontwarn com.google.auto.value.AutoValue
@ -264,38 +280,3 @@
-keep class com.google.apphosting.api.ApiProxy { -keep class com.google.apphosting.api.ApiProxy {
static *** getCurrentEnvironment (...); static *** getCurrentEnvironment (...);
} }
# R8 full mode
-keepattributes Signature
-keep,allowoptimization class kotlin.coroutines.Continuation
-keep,allowoptimization class * extends uy.kohesive.injekt.api.TypeReference
-keep,allowoptimization public class io.requery.android.database.sqlite.SQLiteConnection { *; }
# Keep apache http client
-keep class org.apache.http.** { *; }
# Suggested rules
-dontwarn com.oracle.svm.core.annotate.AutomaticFeature
-dontwarn com.oracle.svm.core.annotate.Delete
-dontwarn com.oracle.svm.core.annotate.Substitute
-dontwarn com.oracle.svm.core.annotate.TargetClass
-dontwarn com.oracle.svm.core.configure.ResourcesRegistry
-dontwarn org.graalvm.nativeimage.ImageSingletons
-dontwarn org.graalvm.nativeimage.hosted.Feature$BeforeAnalysisAccess
-dontwarn org.graalvm.nativeimage.hosted.Feature
-dontwarn org.slf4j.impl.StaticLoggerBinder
-dontwarn java.lang.Module
-dontwarn org.graalvm.nativeimage.hosted.RuntimeResourceAccess
-dontwarn org.jspecify.annotations.NullMarked
-dontwarn javax.naming.InvalidNameException
-dontwarn javax.naming.NamingException
-dontwarn javax.naming.directory.Attribute
-dontwarn javax.naming.directory.Attributes
-dontwarn javax.naming.ldap.LdapName
-dontwarn javax.naming.ldap.Rdn
-dontwarn org.ietf.jgss.GSSContext
-dontwarn org.ietf.jgss.GSSCredential
-dontwarn org.ietf.jgss.GSSException
-dontwarn org.ietf.jgss.GSSManager
-dontwarn org.ietf.jgss.GSSName
-dontwarn org.ietf.jgss.Oid

View File

@ -1,4 +1,5 @@
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android"> <shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
<shortcut <shortcut
android:enabled="true" android:enabled="true"
android:icon="@drawable/sc_collections_bookmark_48dp" android:icon="@drawable/sc_collections_bookmark_48dp"
@ -8,7 +9,6 @@
android:shortcutShortLabel="@string/label_library"> android:shortcutShortLabel="@string/label_library">
<intent <intent
android:action="eu.kanade.tachiyomi.SHOW_LIBRARY" android:action="eu.kanade.tachiyomi.SHOW_LIBRARY"
android:targetPackage="eu.kanade.tachiyomi.sy"
android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity" /> android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity" />
</shortcut> </shortcut>
<shortcut <shortcut
@ -17,10 +17,9 @@
android:shortcutDisabledMessage="@string/app_not_available" android:shortcutDisabledMessage="@string/app_not_available"
android:shortcutId="show_recently_updated" android:shortcutId="show_recently_updated"
android:shortcutLongLabel="@string/label_recent_updates" android:shortcutLongLabel="@string/label_recent_updates"
android:shortcutShortLabel="@string/label_recent_updates"> android:shortcutShortLabel="@string/short_recent_updates">
<intent <intent
android:action="eu.kanade.tachiyomi.SHOW_RECENTLY_UPDATED" android:action="eu.kanade.tachiyomi.SHOW_RECENTLY_UPDATED"
android:targetPackage="eu.kanade.tachiyomi.sy"
android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity" /> android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity" />
</shortcut> </shortcut>
<shortcut <shortcut
@ -32,7 +31,6 @@
android:shortcutShortLabel="@string/label_recent_manga"> android:shortcutShortLabel="@string/label_recent_manga">
<intent <intent
android:action="eu.kanade.tachiyomi.SHOW_RECENTLY_READ" android:action="eu.kanade.tachiyomi.SHOW_RECENTLY_READ"
android:targetPackage="eu.kanade.tachiyomi.sy"
android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity" /> android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity" />
</shortcut> </shortcut>
<shortcut <shortcut
@ -44,7 +42,6 @@
android:shortcutShortLabel="@string/browse"> android:shortcutShortLabel="@string/browse">
<intent <intent
android:action="eu.kanade.tachiyomi.SHOW_CATALOGUES" android:action="eu.kanade.tachiyomi.SHOW_CATALOGUES"
android:targetPackage="eu.kanade.tachiyomi.sy"
android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity" /> android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity" />
</shortcut> </shortcut>
</shortcuts> </shortcuts>

View File

@ -2,5 +2,4 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/> <background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/> <foreground android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_tachi_monochrome_launcher" />
</adaptive-icon> </adaptive-icon>

View File

@ -2,5 +2,4 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/> <background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/> <foreground android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_tachi_monochrome_launcher" />
</adaptive-icon> </adaptive-icon>

View File

@ -1,11 +0,0 @@
package mihon.core.firebase
import android.content.Context
object FirebaseConfig {
fun init(context: Context) = Unit
fun setAnalyticsEnabled(enabled: Boolean) = Unit
fun setCrashlyticsEnabled(enabled: Boolean) = Unit
}

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"> package="eu.kanade.tachiyomi">
<!-- Internet --> <!-- Internet -->
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
@ -8,9 +8,7 @@
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<!-- Storage --> <!-- Storage -->
<uses-permission <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
<!-- For background jobs --> <!-- For background jobs -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
@ -21,118 +19,40 @@
<!-- For managing extensions --> <!-- For managing extensions -->
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" /> <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" /> <uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
<uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" />
<!-- To view extension packages in API 30+ --> <!-- To view extension packages in API 30+ -->
<uses-permission <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission
android:name="android.permission.READ_APP_SPECIFIC_LOCALES"
tools:ignore="ProtectedPermissions" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<!-- Remove unnecessary permissions from Firebase dependency -->
<uses-permission
android:name="com.google.android.finsky.permission.BIND_GET_INSTALL_REFERRER_SERVICE"
tools:node="remove" />
<uses-permission
android:name="com.google.android.gms.permission.AD_ID"
tools:node="remove" />
<uses-permission
android:name="android.permission.ACCESS_ADSERVICES_ATTRIBUTION"
tools:node="remove" />
<uses-permission
android:name="android.permission.ACCESS_ADSERVICES_AD_ID"
tools:node="remove" />
<application <application
android:name=".App" android:name=".App"
android:allowBackup="false" android:allowBackup="true"
android:enableOnBackInvokedCallback="true" android:fullBackupContent="@xml/backup_rules"
android:hardwareAccelerated="true" android:hardwareAccelerated="true"
android:hasFragileUserData="true"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:largeHeap="true" android:largeHeap="true"
android:localeConfig="@xml/locales_config"
android:networkSecurityConfig="@xml/network_security_config"
android:preserveLegacyExternalStorage="true"
android:requestLegacyExternalStorage="true" android:requestLegacyExternalStorage="true"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:theme="@style/Theme.Base"
android:theme="@style/Theme.Tachiyomi"> android:networkSecurityConfig="@xml/network_security_config">
<activity <activity
android:name=".ui.main.MainActivity" android:name=".ui.main.MainActivity"
android:exported="true"
android:launchMode="singleTop" android:launchMode="singleTop"
android:theme="@style/Theme.Tachiyomi.SplashScreen"> android:theme="@style/Theme.Splash">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
<!-- Deep link to add repos -->
<intent-filter android:label="@string/action_add_repo">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="tachiyomi" />
<data android:host="add-repo" />
</intent-filter>
<!-- Open backup files -->
<intent-filter android:label="@string/pref_restore_backup">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="file" />
<data android:scheme="content" />
<data android:host="*" />
<data android:mimeType="*/*" />
<!--
Work around Android's ugly primitive PatternMatcher
implementation that can't cope with finding a . early in
the path unless it's explicitly matched.
See https://stackoverflow.com/a/31028507
-->
<data android:pathPattern=".*\\.tachibk" />
<data android:pathPattern=".*\\..*\\.tachibk" />
<data android:pathPattern=".*\\..*\\..*\\.tachibk" />
<data android:pathPattern=".*\\..*\\..*\\..*\\.tachibk" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\.tachibk" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\.tachibk" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\.tachibk" />
</intent-filter>
<!--suppress AndroidDomInspection --> <!--suppress AndroidDomInspection -->
<meta-data <meta-data
android:name="android.app.shortcuts" android:name="android.app.shortcuts"
android:resource="@xml/shortcuts" /> android:resource="@xml/shortcuts" />
</activity> </activity>
<activity <activity
android:name=".crash.CrashActivity" android:name=".ui.main.DeepLinkActivity"
android:exported="false"
android:process=":error_handler" />
<activity
android:name=".ui.deeplink.DeepLinkActivity"
android:exported="true"
android:label="@string/action_search"
android:launchMode="singleTask" android:launchMode="singleTask"
android:theme="@android:style/Theme.NoDisplay"> android:theme="@android:style/Theme.NoDisplay"
android:label="@string/action_global_search">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.SEARCH" /> <action android:name="android.intent.action.SEARCH" />
<action android:name="com.google.android.gms.actions.SEARCH_ACTION" /> <action android:name="com.google.android.gms.actions.SEARCH_ACTION" />
@ -153,57 +73,25 @@
android:name="android.app.searchable" android:name="android.app.searchable"
android:resource="@xml/searchable" /> android:resource="@xml/searchable" />
</activity> </activity>
<activity <activity
android:name=".ui.reader.ReaderActivity" android:name=".ui.reader.ReaderActivity"
android:exported="false"
android:launchMode="singleTask"> android:launchMode="singleTask">
<intent-filter> <intent-filter>
<action android:name="com.samsung.android.support.REMOTE_ACTION" /> <action android:name="com.samsung.android.support.REMOTE_ACTION" />
</intent-filter> </intent-filter>
<meta-data <meta-data android:name="com.samsung.android.support.REMOTE_ACTION"
android:name="com.samsung.android.support.REMOTE_ACTION"
android:resource="@xml/s_pen_actions"/> android:resource="@xml/s_pen_actions"/>
</activity> </activity>
<activity <activity
android:name=".ui.security.UnlockActivity" android:name=".ui.security.BiometricUnlockActivity"
android:exported="false" android:theme="@style/Theme.Base" />
android:theme="@style/Theme.Tachiyomi" />
<activity <activity
android:name=".ui.webview.WebViewActivity" android:name=".ui.webview.WebViewActivity"
android:configChanges="uiMode|orientation|screenSize" android:configChanges="uiMode|orientation|screenSize" />
android:exported="false" />
<activity <activity
android:name=".extension.util.ExtensionInstallActivity" android:name=".ui.setting.track.AnilistLoginActivity"
android:exported="false" android:label="Anilist">
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
<activity
android:name=".ui.setting.track.TrackLoginActivity"
android:exported="true"
android:label="@string/track_activity_name">
<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="mihon" />
<data android:host="anilist-auth" />
<data android:host="bangumi-auth" />
<data android:host="myanimelist-auth" />
<data android:host="shikimori-auth" />
</intent-filter>
</activity>
<activity
android:name=".ui.setting.track.GoogleDriveLoginActivity"
android:label="GoogleDrive"
android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
@ -211,37 +99,60 @@
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
<data <data
android:scheme="eu.kanade.google.oauth" /> android:host="anilist-auth"
android:scheme="tachiyomi" />
</intent-filter>
</activity>
<activity
android:name=".ui.setting.track.MyAnimeListLoginActivity"
android:label="MyAnimeList">
<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:host="myanimelist-auth"
android:scheme="tachiyomi" />
</intent-filter>
</activity>
<activity
android:name=".ui.setting.track.ShikimoriLoginActivity"
android:label="Shikimori">
<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:host="shikimori-auth"
android:scheme="tachiyomi" />
</intent-filter>
</activity>
<activity
android:name=".ui.setting.track.BangumiLoginActivity"
android:label="Bangumi">
<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:host="bangumi-auth"
android:scheme="tachiyomi" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity
android:name=".extension.util.ExtensionInstallActivity"
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
<activity <activity
android:name="exh.ui.login.EhLoginActivity" android:name="exh.ui.login.EhLoginActivity"
android:label="EHentaiLogin" android:label="EHentaiLogin" />
android:exported="false"/>
<receiver
android:name=".data.notification.NotificationReceiver"
android:exported="false" />
<service
android:name=".extension.util.ExtensionInstallService"
android:exported="false"
android:foregroundServiceType="shortService" />
<service
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
android:enabled="false"
android:exported="false">
<meta-data
android:name="autoStoreLocales"
android:value="true" />
</service>
<service
android:name="androidx.work.impl.foreground.SystemForegroundService"
android:foregroundServiceType="dataSync"
tools:node="merge" />
<provider <provider
android:name="androidx.core.content.FileProvider" android:name="androidx.core.content.FileProvider"
@ -253,173 +164,217 @@
android:resource="@xml/provider_paths" /> android:resource="@xml/provider_paths" />
</provider> </provider>
<provider <receiver
android:name="rikka.shizuku.ShizukuProvider" android:name=".data.notification.NotificationReceiver"
android:authorities="${applicationId}.shizuku" android:exported="false" />
android:enabled="true"
android:exported="true"
android:multiprocess="false"
android:permission="android.permission.INTERACT_ACROSS_USERS_FULL" />
<meta-data <service
android:name="android.webkit.WebView.EnableSafeBrowsing" android:name=".data.library.LibraryUpdateService"
android:value="false" /> android:exported="false" />
<meta-data
android:name="android.webkit.WebView.MetricsOptOut"
android:value="true" />
<!-- Disable for manual opt-in --> <service
<meta-data android:name=".data.download.DownloadService"
android:name="firebase_analytics_collection_enabled" android:exported="false" />
android:value="false" />
<meta-data
android:name="firebase_crashlytics_collection_enabled"
android:value="false" />
<!-- Disable advertising ID collection for Firebase --> <service
<meta-data android:name=".data.updater.UpdaterService"
android:name="google_analytics_adid_collection_enabled" android:exported="false" />
android:value="false" />
<service
android:name=".data.backup.BackupCreateService"
android:exported="false" />
<service
android:name=".data.backup.BackupRestoreService"
android:exported="false" />
<!-- EH --> <!-- 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 <activity
android:name="exh.ui.intercept.InterceptActivity" android:name="exh.ui.intercept.InterceptActivity"
android:label="@string/app_name" android:label="@string/app_name"
android:theme="@style/Theme.Tachiyomi" android:theme="@style/Theme.Base">
android:exported="true">
<!-- E-Hentai -->
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" /> <!-- EH -->
<data android:scheme="http" /> <data
android:host="g.e-hentai.org"
android:pathPrefix="/g/"
android:scheme="http" />
<data
android:host="g.e-hentai.org"
android:pathPrefix="/g/"
android:scheme="https" />
<data
android:host="e-hentai.org"
android:pathPrefix="/g/"
android:scheme="http" />
<data
android:host="e-hentai.org"
android:pathPrefix="/g/"
android:scheme="https" />
<data android:host="e-hentai.org" /> <!-- EXH -->
<data android:host="www.e-hentai.org" /> <data
<data android:host="g.e-hentai.org" /> android:host="exhentai.org"
android:pathPrefix="/g/"
android:scheme="http" />
<data
android:host="exhentai.org"
android:pathPrefix="/g/"
android:scheme="https" />
<data android:pathPattern="/g/..*" /> <!-- nhentai -->
</intent-filter> <data
<!-- ExHentai --> android:host="nhentai.net"
<intent-filter> android:pathPrefix="/g/"
<action android:name="android.intent.action.VIEW" /> android:scheme="http" />
<data
android:host="nhentai.net"
android:pathPrefix="/g/"
android:scheme="https" />
<category android:name="android.intent.category.DEFAULT" /> <!-- Perv Eden -->
<category android:name="android.intent.category.BROWSABLE" /> <data
android:host="www.perveden.com"
android:pathPattern="/.*/.*-manga/.*"
android:scheme="http" />
<data
android:host="www.perveden.com"
android:pathPattern="/.*/.*-manga/.*"
android:scheme="https" />
<data android:scheme="https" /> <!-- Hentai Cafe -->
<data android:scheme="http" /> <data
android:host="hentai.cafe"
android:pathPrefix="/hc.fyi/"
android:scheme="http" />
<data
android:host="hentai.cafe"
android:pathPrefix="/hc.fyi/"
android:scheme="https" />
<data android:host="exhentai.org" />
<data android:host="www.exhentai.org" />
<data android:pathPattern="/g/..*" />
</intent-filter>
<!-- NHentai -->
<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="nhentai.net" />
<data android:host="www.nhentai.net" />
<data android:pathPattern="/g/..*" />
</intent-filter>
<!-- Tsumino --> <!-- Tsumino -->
<intent-filter> <data
<action android:name="android.intent.action.VIEW" /> 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" />
<category android:name="android.intent.category.DEFAULT" /> <!-- Hitomi.la -->
<category android:name="android.intent.category.BROWSABLE" /> <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" />
<data android:scheme="https" /> <!-- Pururin.io -->
<data android:scheme="http" /> <data
android:host="pururin.io"
android:pathPrefix="/gallery/"
android:scheme="http" />
<data
android:host="pururin.io"
android:pathPrefix="/gallery/"
android:scheme="https" />
<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.me" />
<data android:pathPattern="/gallery/..*" />
</intent-filter>
<!-- HBrowse --> <!-- HBrowse -->
<intent-filter> <data
<action android:name="android.intent.action.VIEW" /> android:host="www.hbrowse.com"
android:scheme="http" />
<data
android:host="www.hbrowse.com"
android:scheme="https" />
<category android:name="android.intent.category.DEFAULT" /> <!-- MangaDex -->
<category android:name="android.intent.category.BROWSABLE" /> <!--<data
android:scheme="https"
<data android:scheme="https" /> android:host="www.mangadex.org"
<data android:scheme="http" /> android:pathPrefix="/manga/" />
<data
<data android:host="hbrowse.com" /> android:scheme="https"
<data android:host="www.hbrowse.com" /> android:host="mangadex.org"
android:pathPrefix="/manga/" />
<!--<data android:pathPattern="/gallery/..*" />--> <data
</intent-filter> android:scheme="https"
<!-- Mangadex --> android:host="www.mangadex.cc"
<intent-filter android:autoVerify="true"> android:pathPrefix="/manga/" />
<action android:name="android.intent.action.VIEW" /> <data
android:scheme="https"
<category android:name="android.intent.category.DEFAULT" /> android:host="www.mangadex.cc"
<category android:name="android.intent.category.BROWSABLE" /> android:pathPrefix="/manga/" />
<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" />
<data <data
android:host="mangadex-auth" android:scheme="https"
android:scheme="tachiyomisy" /> 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> </intent-filter>
</activity> </activity>
<activity <activity
android:name="com.journeyapps.barcodescanner.CaptureActivity" android:name="exh.ui.captcha.BrowserActionActivity"
tools:remove="screenOrientation" /> android:theme="@style/Theme.Base" />
</application> </application>
<uses-sdk tools:overrideLibrary="rikka.shizuku.api"
tools:ignore="ManifestOrder" />
</manifest> </manifest>

File diff suppressed because it is too large Load Diff

View File

@ -1,10 +0,0 @@
package eu.kanade.core.preference
import androidx.compose.ui.state.ToggleableState
import tachiyomi.core.common.preference.CheckboxState
fun <T> CheckboxState.TriState<T>.asToggleableState() = when (this) {
is CheckboxState.TriState.Exclude -> ToggleableState.Indeterminate
is CheckboxState.TriState.Include -> ToggleableState.On
is CheckboxState.TriState.None -> ToggleableState.Off
}

View File

@ -1,38 +0,0 @@
package eu.kanade.core.preference
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import tachiyomi.core.common.preference.Preference
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
}
}
fun <T> Preference<T>.asState(scope: CoroutineScope) = PreferenceMutableState(this, scope)

View File

@ -1,99 +0,0 @@
package eu.kanade.core.util
import androidx.compose.ui.util.fastFilter
import androidx.compose.ui.util.fastForEach
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract
fun <T : R, R : Any> List<T>.insertSeparators(
generator: (before: T?, after: 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)
val after = getOrNull(i + 1)
val separator = generator.invoke(before, after)
separator?.let(newList::add)
}
return newList
}
/**
* Similar to [eu.kanade.core.util.insertSeparators] but iterates from last to first element
*/
fun <T : R, R : Any> List<T>.insertSeparatorsReversed(
generator: (before: T?, after: T?) -> R?,
): List<R> {
if (isEmpty()) return emptyList()
val newList = mutableListOf<R>()
for (i in size downTo 0) {
val after = getOrNull(i)
after?.let(newList::add)
val before = getOrNull(i - 1)
val separator = generator.invoke(before, after)
separator?.let(newList::add)
}
return newList.asReversed()
}
fun <E> HashSet<E>.addOrRemove(value: E, shouldAdd: Boolean) {
if (shouldAdd) {
add(value)
} else {
remove(value)
}
}
/**
* 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) }
return fastFilter { !predicate(it) }
}
/**
* 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
}

View File

@ -1,200 +0,0 @@
package eu.kanade.domain
import eu.kanade.domain.chapter.interactor.GetAvailableScanlators
import eu.kanade.domain.chapter.interactor.SetReadStatus
import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource
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.extension.interactor.TrustExtension
import eu.kanade.domain.manga.interactor.GetExcludedScanlators
import eu.kanade.domain.manga.interactor.SetExcludedScanlators
import eu.kanade.domain.manga.interactor.SetMangaViewerFlags
import eu.kanade.domain.manga.interactor.UpdateManga
import eu.kanade.domain.source.interactor.GetEnabledSources
import eu.kanade.domain.source.interactor.GetIncognitoState
import eu.kanade.domain.source.interactor.GetLanguagesWithSources
import eu.kanade.domain.source.interactor.GetSourcesWithFavoriteCount
import eu.kanade.domain.source.interactor.SetMigrateSorting
import eu.kanade.domain.source.interactor.ToggleIncognito
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.track.interactor.AddTracks
import eu.kanade.domain.track.interactor.RefreshTracks
import eu.kanade.domain.track.interactor.SyncChapterProgressWithTrack
import eu.kanade.domain.track.interactor.TrackChapter
import eu.kanade.tachiyomi.di.InjektModule
import eu.kanade.tachiyomi.di.addFactory
import eu.kanade.tachiyomi.di.addSingletonFactory
import mihon.data.repository.ExtensionRepoRepositoryImpl
import mihon.domain.chapter.interactor.FilterChaptersForDownload
import mihon.domain.extensionrepo.interactor.CreateExtensionRepo
import mihon.domain.extensionrepo.interactor.DeleteExtensionRepo
import mihon.domain.extensionrepo.interactor.GetExtensionRepo
import mihon.domain.extensionrepo.interactor.GetExtensionRepoCount
import mihon.domain.extensionrepo.interactor.ReplaceExtensionRepo
import mihon.domain.extensionrepo.interactor.UpdateExtensionRepo
import mihon.domain.extensionrepo.repository.ExtensionRepoRepository
import mihon.domain.extensionrepo.service.ExtensionRepoService
import mihon.domain.upcoming.interactor.GetUpcomingManga
import tachiyomi.data.category.CategoryRepositoryImpl
import tachiyomi.data.chapter.ChapterRepositoryImpl
import tachiyomi.data.history.HistoryRepositoryImpl
import tachiyomi.data.manga.MangaRepositoryImpl
import tachiyomi.data.release.ReleaseServiceImpl
import tachiyomi.data.source.SourceRepositoryImpl
import tachiyomi.data.source.StubSourceRepositoryImpl
import tachiyomi.data.track.TrackRepositoryImpl
import tachiyomi.data.updates.UpdatesRepositoryImpl
import tachiyomi.domain.category.interactor.CreateCategoryWithName
import tachiyomi.domain.category.interactor.DeleteCategory
import tachiyomi.domain.category.interactor.GetCategories
import tachiyomi.domain.category.interactor.RenameCategory
import tachiyomi.domain.category.interactor.ReorderCategory
import tachiyomi.domain.category.interactor.ResetCategoryFlags
import tachiyomi.domain.category.interactor.SetDisplayMode
import tachiyomi.domain.category.interactor.SetMangaCategories
import tachiyomi.domain.category.interactor.SetSortModeForCategory
import tachiyomi.domain.category.interactor.UpdateCategory
import tachiyomi.domain.category.repository.CategoryRepository
import tachiyomi.domain.chapter.interactor.GetChapter
import tachiyomi.domain.chapter.interactor.GetChapterByUrlAndMangaId
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
import tachiyomi.domain.chapter.interactor.SetMangaDefaultChapterFlags
import tachiyomi.domain.chapter.interactor.ShouldUpdateDbChapter
import tachiyomi.domain.chapter.interactor.UpdateChapter
import tachiyomi.domain.chapter.repository.ChapterRepository
import tachiyomi.domain.history.interactor.GetHistory
import tachiyomi.domain.history.interactor.GetNextChapters
import tachiyomi.domain.history.interactor.GetTotalReadDuration
import tachiyomi.domain.history.interactor.RemoveHistory
import tachiyomi.domain.history.interactor.UpsertHistory
import tachiyomi.domain.history.repository.HistoryRepository
import tachiyomi.domain.manga.interactor.FetchInterval
import tachiyomi.domain.manga.interactor.GetDuplicateLibraryManga
import tachiyomi.domain.manga.interactor.GetFavorites
import tachiyomi.domain.manga.interactor.GetLibraryManga
import tachiyomi.domain.manga.interactor.GetManga
import tachiyomi.domain.manga.interactor.GetMangaByUrlAndSourceId
import tachiyomi.domain.manga.interactor.GetMangaWithChapters
import tachiyomi.domain.manga.interactor.NetworkToLocalManga
import tachiyomi.domain.manga.interactor.ResetViewerFlags
import tachiyomi.domain.manga.interactor.SetMangaChapterFlags
import tachiyomi.domain.manga.interactor.UpdateMangaNotes
import tachiyomi.domain.manga.repository.MangaRepository
import tachiyomi.domain.release.interactor.GetApplicationRelease
import tachiyomi.domain.release.service.ReleaseService
import tachiyomi.domain.source.interactor.GetRemoteManga
import tachiyomi.domain.source.interactor.GetSourcesWithNonLibraryManga
import tachiyomi.domain.source.repository.SourceRepository
import tachiyomi.domain.source.repository.StubSourceRepository
import tachiyomi.domain.track.interactor.DeleteTrack
import tachiyomi.domain.track.interactor.GetTracks
import tachiyomi.domain.track.interactor.GetTracksPerManga
import tachiyomi.domain.track.interactor.InsertTrack
import tachiyomi.domain.track.repository.TrackRepository
import tachiyomi.domain.updates.interactor.GetUpdates
import tachiyomi.domain.updates.repository.UpdatesRepository
import uy.kohesive.injekt.api.InjektRegistrar
class DomainModule : InjektModule {
override fun InjektRegistrar.registerInjectables() {
addSingletonFactory<CategoryRepository> { CategoryRepositoryImpl(get()) }
addFactory { GetCategories(get()) }
addFactory { ResetCategoryFlags(get(), get()) }
addFactory { SetDisplayMode(get()) }
addFactory { SetSortModeForCategory(get(), get()) }
addFactory { CreateCategoryWithName(get(), get()) }
addFactory { RenameCategory(get()) }
addFactory { ReorderCategory(get()) }
addFactory { UpdateCategory(get()) }
addFactory { DeleteCategory(get(), get(), get()) }
addSingletonFactory<MangaRepository> { MangaRepositoryImpl(get()) }
addFactory { GetDuplicateLibraryManga(get()) }
addFactory { GetFavorites(get()) }
addFactory { GetLibraryManga(get()) }
addFactory { GetMangaWithChapters(get(), get()) }
addFactory { GetMangaByUrlAndSourceId(get()) }
addFactory { GetManga(get()) }
addFactory { GetNextChapters(get(), get(), get(), get()) }
addFactory { GetUpcomingManga(get()) }
addFactory { ResetViewerFlags(get()) }
addFactory { SetMangaChapterFlags(get()) }
addFactory { FetchInterval(get()) }
addFactory { SetMangaDefaultChapterFlags(get(), get(), get()) }
addFactory { SetMangaViewerFlags(get()) }
addFactory { NetworkToLocalManga(get()) }
addFactory { UpdateManga(get(), get()) }
addFactory { UpdateMangaNotes(get()) }
addFactory { SetMangaCategories(get()) }
addFactory { GetExcludedScanlators(get()) }
addFactory { SetExcludedScanlators(get()) }
addSingletonFactory<ReleaseService> { ReleaseServiceImpl(get(), get()) }
addFactory { GetApplicationRelease(get(), get()) }
addSingletonFactory<TrackRepository> { TrackRepositoryImpl(get()) }
addFactory { TrackChapter(get(), get(), get(), get()) }
addFactory { AddTracks(get(), get(), get(), get()) }
addFactory { RefreshTracks(get(), get(), get(), get()) }
addFactory { DeleteTrack(get()) }
addFactory { GetTracksPerManga(get(), get()) }
addFactory { GetTracks(get()) }
addFactory { InsertTrack(get()) }
addFactory { SyncChapterProgressWithTrack(get(), get(), get()) }
addSingletonFactory<ChapterRepository> { ChapterRepositoryImpl(get()) }
addFactory { GetChapter(get()) }
addFactory { GetChaptersByMangaId(get()) }
addFactory { GetChapterByUrlAndMangaId(get()) }
addFactory { UpdateChapter(get()) }
addFactory { SetReadStatus(get(), get(), get(), get(), get()) }
addFactory { ShouldUpdateDbChapter() }
addFactory { SyncChaptersWithSource(get(), get(), get(), get(), get(), get(), get(), get(), get()) }
addFactory { GetAvailableScanlators(get()) }
addFactory { FilterChaptersForDownload(get(), get(), 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<StubSourceRepository> { StubSourceRepositoryImpl(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()) }
addFactory { TrustExtension(get(), get()) }
addSingletonFactory<ExtensionRepoRepository> { ExtensionRepoRepositoryImpl(get()) }
addFactory { ExtensionRepoService(get(), get()) }
addFactory { GetExtensionRepo(get()) }
addFactory { GetExtensionRepoCount(get()) }
addFactory { CreateExtensionRepo(get(), get()) }
addFactory { DeleteExtensionRepo(get()) }
addFactory { ReplaceExtensionRepo(get()) }
addFactory { UpdateExtensionRepo(get(), get()) }
addFactory { ToggleIncognito(get()) }
addFactory { GetIncognitoState(get(), get(), get()) }
}
}

View File

@ -1,156 +0,0 @@
package eu.kanade.domain
import android.app.Application
import eu.kanade.domain.manga.interactor.CreateSortTag
import eu.kanade.domain.manga.interactor.DeleteSortTag
import eu.kanade.domain.manga.interactor.GetPagePreviews
import eu.kanade.domain.manga.interactor.GetSortTag
import eu.kanade.domain.manga.interactor.ReorderSortTag
import eu.kanade.domain.source.interactor.CreateSourceCategory
import eu.kanade.domain.source.interactor.DeleteSourceCategory
import eu.kanade.domain.source.interactor.GetExhSavedSearch
import eu.kanade.domain.source.interactor.GetShowLatest
import eu.kanade.domain.source.interactor.GetSourceCategories
import eu.kanade.domain.source.interactor.RenameSourceCategory
import eu.kanade.domain.source.interactor.SetSourceCategories
import eu.kanade.domain.source.interactor.ToggleExcludeFromDataSaver
import eu.kanade.tachiyomi.di.InjektModule
import eu.kanade.tachiyomi.di.addFactory
import eu.kanade.tachiyomi.di.addSingletonFactory
import eu.kanade.tachiyomi.source.online.MetadataSource
import exh.search.SearchEngine
import tachiyomi.data.manga.CustomMangaRepositoryImpl
import tachiyomi.data.manga.FavoritesEntryRepositoryImpl
import tachiyomi.data.manga.MangaMergeRepositoryImpl
import tachiyomi.data.manga.MangaMetadataRepositoryImpl
import tachiyomi.data.source.FeedSavedSearchRepositoryImpl
import tachiyomi.data.source.SavedSearchRepositoryImpl
import tachiyomi.domain.chapter.interactor.DeleteChapters
import tachiyomi.domain.chapter.interactor.GetChapterByUrl
import tachiyomi.domain.chapter.interactor.GetMergedChaptersByMangaId
import tachiyomi.domain.manga.interactor.DeleteByMergeId
import tachiyomi.domain.manga.interactor.DeleteFavoriteEntries
import tachiyomi.domain.manga.interactor.DeleteMangaById
import tachiyomi.domain.manga.interactor.DeleteMergeById
import tachiyomi.domain.manga.interactor.GetAllManga
import tachiyomi.domain.manga.interactor.GetCustomMangaInfo
import tachiyomi.domain.manga.interactor.GetExhFavoriteMangaWithMetadata
import tachiyomi.domain.manga.interactor.GetFavoriteEntries
import tachiyomi.domain.manga.interactor.GetFlatMetadataById
import tachiyomi.domain.manga.interactor.GetIdsOfFavoriteMangaWithMetadata
import tachiyomi.domain.manga.interactor.GetManga
import tachiyomi.domain.manga.interactor.GetMangaBySource
import tachiyomi.domain.manga.interactor.GetMergedManga
import tachiyomi.domain.manga.interactor.GetMergedMangaById
import tachiyomi.domain.manga.interactor.GetMergedMangaForDownloading
import tachiyomi.domain.manga.interactor.GetMergedReferencesById
import tachiyomi.domain.manga.interactor.GetReadMangaNotInLibraryView
import tachiyomi.domain.manga.interactor.GetSearchMetadata
import tachiyomi.domain.manga.interactor.GetSearchTags
import tachiyomi.domain.manga.interactor.GetSearchTitles
import tachiyomi.domain.manga.interactor.InsertFavoriteEntries
import tachiyomi.domain.manga.interactor.InsertFavoriteEntryAlternative
import tachiyomi.domain.manga.interactor.InsertFlatMetadata
import tachiyomi.domain.manga.interactor.InsertMergedReference
import tachiyomi.domain.manga.interactor.SetCustomMangaInfo
import tachiyomi.domain.manga.interactor.UpdateMergedSettings
import tachiyomi.domain.manga.repository.CustomMangaRepository
import tachiyomi.domain.manga.repository.FavoritesEntryRepository
import tachiyomi.domain.manga.repository.MangaMergeRepository
import tachiyomi.domain.manga.repository.MangaMetadataRepository
import tachiyomi.domain.source.interactor.CountFeedSavedSearchBySourceId
import tachiyomi.domain.source.interactor.CountFeedSavedSearchGlobal
import tachiyomi.domain.source.interactor.DeleteFeedSavedSearchById
import tachiyomi.domain.source.interactor.DeleteSavedSearchById
import tachiyomi.domain.source.interactor.GetFeedSavedSearchBySourceId
import tachiyomi.domain.source.interactor.GetFeedSavedSearchGlobal
import tachiyomi.domain.source.interactor.GetSavedSearchById
import tachiyomi.domain.source.interactor.GetSavedSearchBySourceId
import tachiyomi.domain.source.interactor.GetSavedSearchBySourceIdFeed
import tachiyomi.domain.source.interactor.GetSavedSearchGlobalFeed
import tachiyomi.domain.source.interactor.InsertFeedSavedSearch
import tachiyomi.domain.source.interactor.InsertSavedSearch
import tachiyomi.domain.source.repository.FeedSavedSearchRepository
import tachiyomi.domain.source.repository.SavedSearchRepository
import tachiyomi.domain.track.interactor.IsTrackUnfollowed
import uy.kohesive.injekt.api.InjektRegistrar
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 { GetAllManga(get()) }
addFactory { GetMangaBySource(get()) }
addFactory { DeleteChapters(get()) }
addFactory { DeleteMangaById(get()) }
addFactory { FilterSerializer() }
addFactory { GetChapterByUrl(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() }
addFactory { IsTrackUnfollowed() }
addFactory { GetReadMangaNotInLibraryView(get()) }
// 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 { GetMergedChaptersByMangaId(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()) }
addFactory { InsertFavoriteEntryAlternative(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()) }
addSingletonFactory<CustomMangaRepository> { CustomMangaRepositoryImpl(get<Application>()) }
addFactory { GetCustomMangaInfo(get()) }
addFactory { SetCustomMangaInfo(get()) }
}
}

View File

@ -1,38 +0,0 @@
package eu.kanade.domain.base
import android.content.Context
import dev.icerock.moko.resources.StringResource
import eu.kanade.tachiyomi.util.system.GLUtil
import tachiyomi.core.common.preference.Preference
import tachiyomi.core.common.preference.PreferenceStore
import tachiyomi.i18n.MR
class BasePreferences(
val context: Context,
private val preferenceStore: PreferenceStore,
) {
fun downloadedOnly() = preferenceStore.getBoolean(
Preference.appStateKey("pref_downloaded_only"),
false,
)
fun incognitoMode() = preferenceStore.getBoolean(Preference.appStateKey("incognito_mode"), false)
fun extensionInstaller() = ExtensionInstallerPreference(context, preferenceStore)
fun shownOnboardingFlow() = preferenceStore.getBoolean(Preference.appStateKey("onboarding_complete"), false)
enum class ExtensionInstaller(val titleRes: StringResource, val requiresSystemPermission: Boolean) {
LEGACY(MR.strings.ext_installer_legacy, true),
PACKAGEINSTALLER(MR.strings.ext_installer_packageinstaller, true),
SHIZUKU(MR.strings.ext_installer_shizuku, false),
PRIVATE(MR.strings.ext_installer_private, false),
}
fun displayProfile() = preferenceStore.getString("pref_display_profile_key", "")
fun hardwareBitmapThreshold() = preferenceStore.getInt("pref_hardware_bitmap_threshold", GLUtil.SAFE_TEXTURE_LIMIT)
fun alwaysDecodeLongStripWithSSIV() = preferenceStore.getBoolean("pref_always_decode_long_strip_with_ssiv", false)
}

View File

@ -1,68 +0,0 @@
package eu.kanade.domain.base
import android.content.Context
import eu.kanade.domain.base.BasePreferences.ExtensionInstaller
import eu.kanade.tachiyomi.util.system.hasMiuiPackageInstaller
import eu.kanade.tachiyomi.util.system.isShizukuInstalled
import kotlinx.coroutines.CoroutineScope
import tachiyomi.core.common.preference.Preference
import tachiyomi.core.common.preference.PreferenceStore
import tachiyomi.core.common.preference.getEnum
class ExtensionInstallerPreference(
private val context: Context,
preferenceStore: PreferenceStore,
) : Preference<ExtensionInstaller> {
private val basePref = preferenceStore.getEnum(key(), defaultValue())
override fun key() = "extension_installer"
val entries get() = ExtensionInstaller.entries.run {
if (context.hasMiuiPackageInstaller) {
filter { it != ExtensionInstaller.PACKAGEINSTALLER }
} else {
toList()
}
}
override fun defaultValue() = if (context.hasMiuiPackageInstaller) {
ExtensionInstaller.LEGACY
} else {
ExtensionInstaller.PACKAGEINSTALLER
}
private fun check(value: ExtensionInstaller): ExtensionInstaller {
when (value) {
ExtensionInstaller.PACKAGEINSTALLER -> {
if (context.hasMiuiPackageInstaller) return ExtensionInstaller.LEGACY
}
ExtensionInstaller.SHIZUKU -> {
if (!context.isShizukuInstalled) return defaultValue()
}
else -> {}
}
return value
}
override fun get(): ExtensionInstaller {
val value = basePref.get()
val checkedValue = check(value)
if (value != checkedValue) {
basePref.set(checkedValue)
}
return checkedValue
}
override fun set(value: ExtensionInstaller) {
basePref.set(check(value))
}
override fun isSet() = basePref.isSet()
override fun delete() = basePref.delete()
override fun changes() = basePref.changes()
override fun stateIn(scope: CoroutineScope) = basePref.stateIn(scope)
}

View File

@ -1,36 +0,0 @@
package eu.kanade.domain.chapter.interactor
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import tachiyomi.domain.chapter.repository.ChapterRepository
class GetAvailableScanlators(
private val repository: ChapterRepository,
) {
private fun List<String>.cleanupAvailableScanlators(): Set<String> {
return mapNotNull { it.ifBlank { null } }.toSet()
}
suspend fun await(mangaId: Long): Set<String> {
return repository.getScanlatorsByMangaId(mangaId)
.cleanupAvailableScanlators()
}
fun subscribe(mangaId: Long): Flow<Set<String>> {
return repository.getScanlatorsByMangaIdAsFlow(mangaId)
.map { it.cleanupAvailableScanlators() }
}
// SY -->
suspend fun awaitMerge(mangaId: Long): Set<String> {
return repository.getScanlatorsByMergeId(mangaId)
.cleanupAvailableScanlators()
}
fun subscribeMerge(mangaId: Long): Flow<Set<String>> {
return repository.getScanlatorsByMergeIdAsFlow(mangaId)
.map { it.cleanupAvailableScanlators() }
}
// SY <--
}

View File

@ -1,99 +0,0 @@
package eu.kanade.domain.chapter.interactor
import eu.kanade.domain.download.interactor.DeleteDownload
import exh.source.MERGED_SOURCE_ID
import logcat.LogPriority
import tachiyomi.core.common.util.lang.withNonCancellableContext
import tachiyomi.core.common.util.system.logcat
import tachiyomi.domain.chapter.interactor.GetMergedChaptersByMangaId
import tachiyomi.domain.chapter.model.Chapter
import tachiyomi.domain.chapter.model.ChapterUpdate
import tachiyomi.domain.chapter.repository.ChapterRepository
import tachiyomi.domain.download.service.DownloadPreferences
import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.manga.repository.MangaRepository
class SetReadStatus(
private val downloadPreferences: DownloadPreferences,
private val deleteDownload: DeleteDownload,
private val mangaRepository: MangaRepository,
private val chapterRepository: ChapterRepository,
// SY -->
private val getMergedChaptersByMangaId: GetMergedChaptersByMangaId,
// 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 = getMergedChaptersByMangaId
.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 interface Result {
data object Success : Result
data object NoChapters : Result
data class InternalError(val error: Throwable) : Result
}
}

View File

@ -1,248 +0,0 @@
package eu.kanade.domain.chapter.interactor
import eu.kanade.domain.chapter.model.copyFromSChapter
import eu.kanade.domain.chapter.model.toSChapter
import eu.kanade.domain.manga.interactor.GetExcludedScanlators
import eu.kanade.domain.manga.interactor.UpdateManga
import eu.kanade.domain.manga.model.toSManga
import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.download.DownloadProvider
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.online.HttpSource
import exh.source.isEhBasedManga
import tachiyomi.data.chapter.ChapterSanitizer
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
import tachiyomi.domain.chapter.interactor.ShouldUpdateDbChapter
import tachiyomi.domain.chapter.interactor.UpdateChapter
import tachiyomi.domain.chapter.model.Chapter
import tachiyomi.domain.chapter.model.NoChaptersException
import tachiyomi.domain.chapter.model.toChapterUpdate
import tachiyomi.domain.chapter.repository.ChapterRepository
import tachiyomi.domain.chapter.service.ChapterRecognition
import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.domain.manga.model.Manga
import tachiyomi.source.local.isLocal
import java.lang.Long.max
import java.time.ZonedDateTime
import java.util.TreeSet
class SyncChaptersWithSource(
private val downloadManager: DownloadManager,
private val downloadProvider: DownloadProvider,
private val chapterRepository: ChapterRepository,
private val shouldUpdateDbChapter: ShouldUpdateDbChapter,
private val updateManga: UpdateManga,
private val updateChapter: UpdateChapter,
private val getChaptersByMangaId: GetChaptersByMangaId,
private val getExcludedScanlators: GetExcludedScanlators,
private val libraryPreferences: LibraryPreferences,
) {
/**
* Method to synchronize db chapters with source ones
*
* @param rawSourceChapters the chapters from the source.
* @param manga the manga the chapters belong to.
* @param source the source the manga belongs to.
* @return Newly added chapters
*/
suspend fun await(
rawSourceChapters: List<SChapter>,
manga: Manga,
source: Source,
manualFetch: Boolean = false,
fetchWindow: Pair<Long, Long> = Pair(0, 0),
): List<Chapter> {
if (rawSourceChapters.isEmpty() && !source.isLocal()) {
throw NoChaptersException()
}
val now = ZonedDateTime.now()
val nowMillis = now.toInstant().toEpochMilli()
val sourceChapters = rawSourceChapters
.distinctBy { it.url }
.mapIndexed { i, sChapter ->
Chapter.create()
.copyFromSChapter(sChapter)
.copy(name = with(ChapterSanitizer) { sChapter.name.sanitize(manga.title) })
.copy(mangaId = manga.id, sourceOrder = i.toLong())
}
val dbChapters = getChaptersByMangaId.await(manga.id)
val newChapters = mutableListOf<Chapter>()
val updatedChapters = mutableListOf<Chapter>()
val removedChapters = dbChapters.filterNot { dbChapter ->
sourceChapters.any { sourceChapter ->
dbChapter.url == sourceChapter.url
}
}
// Used to not set upload date of older chapters
// to a higher value than newer chapters
var maxSeenUploadDate = 0L
for (sourceChapter in sourceChapters) {
var chapter = sourceChapter
// Update metadata from source if necessary.
if (source is HttpSource) {
val sChapter = chapter.toSChapter()
source.prepareNewChapter(sChapter, manga.toSManga())
chapter = chapter.copyFromSChapter(sChapter)
}
// Recognize chapter number for the chapter.
val chapterNumber = ChapterRecognition.parseChapterNumber(
manga.title,
chapter.name,
chapter.chapterNumber,
)
chapter = chapter.copy(chapterNumber = chapterNumber)
val dbChapter = dbChapters.find { it.url == chapter.url }
if (dbChapter == null) {
val toAddChapter = if (chapter.dateUpload == 0L) {
val altDateUpload = if (maxSeenUploadDate == 0L) nowMillis else maxSeenUploadDate
chapter.copy(dateUpload = altDateUpload)
} else {
maxSeenUploadDate = max(maxSeenUploadDate, sourceChapter.dateUpload)
chapter
}
newChapters.add(toAddChapter)
} else {
if (shouldUpdateDbChapter.await(dbChapter, chapter)) {
val shouldRenameChapter = downloadProvider.isChapterDirNameChanged(dbChapter, chapter) &&
downloadManager.isChapterDownloaded(
dbChapter.name,
dbChapter.scanlator,
/* SY --> */ manga.ogTitle /* SY <-- */,
manga.source,
)
if (shouldRenameChapter) {
downloadManager.renameChapter(source, manga, dbChapter, chapter)
}
var toChangeChapter = dbChapter.copy(
name = chapter.name,
chapterNumber = chapter.chapterNumber,
scanlator = chapter.scanlator,
sourceOrder = chapter.sourceOrder,
)
if (chapter.dateUpload != 0L) {
toChangeChapter = toChangeChapter.copy(dateUpload = chapter.dateUpload)
}
updatedChapters.add(toChangeChapter)
}
}
}
// Return if there's nothing to add, delete, or update to avoid unnecessary db transactions.
if (newChapters.isEmpty() && removedChapters.isEmpty() && updatedChapters.isEmpty()) {
if (manualFetch || manga.fetchInterval == 0 || manga.nextUpdate < fetchWindow.first) {
updateManga.awaitUpdateFetchInterval(
manga,
now,
fetchWindow,
)
}
return emptyList()
}
val changedOrDuplicateReadUrls = mutableSetOf<String>()
val deletedChapterNumbers = TreeSet<Double>()
val deletedReadChapterNumbers = TreeSet<Double>()
val deletedBookmarkedChapterNumbers = TreeSet<Double>()
val readChapterNumbers = dbChapters
.asSequence()
.filter { it.read && it.isRecognizedNumber }
.map { it.chapterNumber }
.toSet()
removedChapters.forEach { chapter ->
if (chapter.read) deletedReadChapterNumbers.add(chapter.chapterNumber)
if (chapter.bookmark) deletedBookmarkedChapterNumbers.add(chapter.chapterNumber)
deletedChapterNumbers.add(chapter.chapterNumber)
}
val deletedChapterNumberDateFetchMap = removedChapters.sortedByDescending { it.dateFetch }
.associate { it.chapterNumber to it.dateFetch }
val markDuplicateAsRead = libraryPreferences.markDuplicateReadChapterAsRead().get()
.contains(LibraryPreferences.MARK_DUPLICATE_CHAPTER_READ_NEW)
// Date fetch is set in such a way that the upper ones will have bigger value than the lower ones
// Sources MUST return the chapters from most to less recent, which is common.
var itemCount = newChapters.size
var updatedToAdd = newChapters.map { toAddItem ->
var chapter = toAddItem.copy(dateFetch = nowMillis + itemCount--)
if (chapter.chapterNumber in readChapterNumbers && markDuplicateAsRead) {
changedOrDuplicateReadUrls.add(chapter.url)
chapter = chapter.copy(read = true)
}
if (!chapter.isRecognizedNumber || chapter.chapterNumber !in deletedChapterNumbers) return@map chapter
chapter = chapter.copy(
read = chapter.chapterNumber in deletedReadChapterNumbers,
bookmark = chapter.chapterNumber in deletedBookmarkedChapterNumbers,
)
// Try to to use the fetch date of the original entry to not pollute 'Updates' tab
deletedChapterNumberDateFetchMap[chapter.chapterNumber]?.let {
chapter = chapter.copy(dateFetch = it)
}
changedOrDuplicateReadUrls.add(chapter.url)
chapter
}
// --> EXH (carry over reading progress)
if (manga.isEhBasedManga()) {
val finalAdded = updatedToAdd.filterNot { it.url in changedOrDuplicateReadUrls }
if (finalAdded.isNotEmpty()) {
val max = dbChapters.maxOfOrNull { it.lastPageRead }
if (max != null && max > 0) {
updatedToAdd = updatedToAdd.map {
if (it.url !in changedOrDuplicateReadUrls) {
it.copy(lastPageRead = max)
} else {
it
}
}
}
}
}
// <-- EXH
if (removedChapters.isNotEmpty()) {
val toDeleteIds = removedChapters.map { it.id }
chapterRepository.removeChaptersWithIds(toDeleteIds)
}
if (updatedToAdd.isNotEmpty()) {
updatedToAdd = chapterRepository.addAll(updatedToAdd)
}
if (updatedChapters.isNotEmpty()) {
val chapterUpdates = updatedChapters.map { it.toChapterUpdate() }
updateChapter.awaitAll(chapterUpdates)
}
updateManga.awaitUpdateFetchInterval(manga, now, fetchWindow)
// Set this manga as updated since chapters were changed
// Note that last_update actually represents last time the chapter list changed at all
updateManga.awaitUpdateLastUpdate(manga.id)
val excludedScanlators = getExcludedScanlators.await(manga.id).toHashSet()
return updatedToAdd.filterNot { it.url in changedOrDuplicateReadUrls || it.scanlator in excludedScanlators }
}
}

View File

@ -1,43 +0,0 @@
package eu.kanade.domain.chapter.model
import eu.kanade.tachiyomi.data.database.models.ChapterImpl
import eu.kanade.tachiyomi.source.model.SChapter
import tachiyomi.domain.chapter.model.Chapter
import eu.kanade.tachiyomi.data.database.models.Chapter as DbChapter
// TODO: Remove when all deps are migrated
fun Chapter.toSChapter(): SChapter {
return SChapter.create().also {
it.url = url
it.name = name
it.date_upload = dateUpload
it.chapter_number = chapterNumber.toFloat()
it.scanlator = scanlator
}
}
fun Chapter.copyFromSChapter(sChapter: SChapter): Chapter {
return this.copy(
name = sChapter.name,
url = sChapter.url,
dateUpload = sChapter.date_upload,
chapterNumber = sChapter.chapter_number.toDouble(),
scanlator = sChapter.scanlator?.ifBlank { null }?.trim(),
)
}
fun Chapter.toDbChapter(): DbChapter = ChapterImpl().also {
it.id = id
it.manga_id = mangaId
it.url = url
it.name = name
it.scanlator = scanlator
it.read = read
it.bookmark = bookmark
it.last_page_read = lastPageRead.toInt()
it.date_fetch = dateFetch
it.date_upload = dateUpload
it.chapter_number = chapterNumber.toFloat()
it.source_order = sourceOrder.toInt()
it.last_modified = lastModifiedAt
}

View File

@ -1,60 +0,0 @@
package eu.kanade.domain.chapter.model
import eu.kanade.domain.manga.model.downloadedFilter
import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.ui.manga.ChapterList
import tachiyomi.domain.chapter.model.Chapter
import tachiyomi.domain.chapter.service.getChapterSort
import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.manga.model.applyFilter
import tachiyomi.source.local.isLocal
/**
* Applies the view filters to the list of chapters obtained from the database.
* @return an observable of the list of chapters filtered and sorted.
*/
fun List<Chapter>.applyFilters(
manga: Manga,
downloadManager: DownloadManager, /* SY --> */
mergedManga: Map<Long, Manga>, /* SY <-- */
): List<Chapter> {
val isLocalManga = manga.isLocal()
val unreadFilter = manga.unreadFilter
val downloadedFilter = manga.downloadedFilter
val bookmarkedFilter = manga.bookmarkedFilter
return filter { chapter -> applyFilter(unreadFilter) { !chapter.read } }
.filter { chapter -> applyFilter(bookmarkedFilter) { chapter.bookmark } }
.filter { chapter ->
// SY -->
@Suppress("NAME_SHADOWING")
val manga = mergedManga.getOrElse(chapter.mangaId) { manga }
// SY <--
applyFilter(downloadedFilter) {
val downloaded = downloadManager.isChapterDownloaded(
chapter.name,
chapter.scanlator,
/* SY --> */ manga.ogTitle /* SY <-- */,
manga.source,
)
downloaded || isLocalManga
}
}
.sortedWith(getChapterSort(manga))
}
/**
* Applies the view filters to the list of chapters obtained from the database.
* @return an observable of the list of chapters filtered and sorted.
*/
fun List<ChapterList.Item>.applyFilters(manga: Manga): Sequence<ChapterList.Item> {
val isLocalManga = manga.isLocal()
val unreadFilter = manga.unreadFilter
val downloadedFilter = manga.downloadedFilter
val bookmarkedFilter = manga.bookmarkedFilter
return asSequence()
.filter { (chapter) -> applyFilter(unreadFilter) { !chapter.read } }
.filter { (chapter) -> applyFilter(bookmarkedFilter) { chapter.bookmark } }
.filter { applyFilter(downloadedFilter) { it.isDownloaded || isLocalManga } }
.sortedWith { (chapter1), (chapter2) -> getChapterSort(manga).invoke(chapter1, chapter2) }
}

View File

@ -1,19 +0,0 @@
package eu.kanade.domain.download.interactor
import eu.kanade.tachiyomi.data.download.DownloadManager
import tachiyomi.core.common.util.lang.withNonCancellableContext
import tachiyomi.domain.chapter.model.Chapter
import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.source.service.SourceManager
class DeleteDownload(
private val sourceManager: SourceManager,
private val downloadManager: DownloadManager,
) {
suspend fun awaitAll(manga: Manga, vararg chapters: Chapter) = withNonCancellableContext {
sourceManager.get(manga.source)?.let { source ->
downloadManager.deleteChapters(chapters.toList(), manga, source)
}
}
}

View File

@ -1,32 +0,0 @@
package eu.kanade.domain.extension.interactor
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.util.system.LocaleHelper
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
class GetExtensionLanguages(
private val preferences: SourcePreferences,
private val extensionManager: ExtensionManager,
) {
fun subscribe(): Flow<List<String>> {
return combine(
preferences.enabledLanguages().changes(),
extensionManager.availableExtensionsFlow,
) { enabledLanguage, availableExtensions ->
availableExtensions
.flatMap { ext ->
if (ext.sources.isEmpty()) {
listOf(ext.lang)
} else {
ext.sources.map { it.lang }
}
}
.distinct()
.sortedWith(
compareBy<String> { it !in enabledLanguage }.then(LocaleHelper.comparator),
)
}
}
}

View File

@ -1,37 +0,0 @@
package eu.kanade.domain.extension.interactor
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.source.Source
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
class GetExtensionSources(
private val preferences: SourcePreferences,
) {
fun subscribe(extension: Extension.Installed): Flow<List<ExtensionSourceItem>> {
val isMultiSource = extension.sources.size > 1
val isMultiLangSingleSource =
isMultiSource && extension.sources.map { it.name }.distinct().size == 1
return preferences.disabledSources().changes().map { disabledSources ->
fun Source.isEnabled() = id.toString() !in disabledSources
extension.sources
.map { source ->
ExtensionSourceItem(
source = source,
enabled = source.isEnabled(),
labelAsName = isMultiSource && !isMultiLangSingleSource,
)
}
}
}
}
data class ExtensionSourceItem(
val source: Source,
val enabled: Boolean,
val labelAsName: Boolean,
)

View File

@ -1,61 +0,0 @@
package eu.kanade.domain.extension.interactor
import eu.kanade.domain.extension.model.Extensions
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.extension.model.Extension
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
class GetExtensionsByType(
private val preferences: SourcePreferences,
private val extensionManager: ExtensionManager,
) {
fun subscribe(): Flow<Extensions> {
val showNsfwSources = preferences.showNsfwSource().get()
return combine(
preferences.enabledLanguages().changes(),
extensionManager.installedExtensionsFlow,
extensionManager.untrustedExtensionsFlow,
extensionManager.availableExtensionsFlow,
) { enabledLanguages, _installed, _untrusted, _available ->
val (updates, installed) = _installed
.filter { (showNsfwSources || !it.isNsfw) }
.sortedWith(
compareBy<Extension.Installed> {
!it.isObsolete /* SY --> */ && !it.isRedundant /* SY <-- */
}.thenBy(String.CASE_INSENSITIVE_ORDER) { it.name },
)
.partition { it.hasUpdate }
val untrusted = _untrusted
.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name })
val available = _available
.filter { extension ->
_installed.none { it.pkgName == extension.pkgName } &&
_untrusted.none { it.pkgName == extension.pkgName } &&
(showNsfwSources || !extension.isNsfw)
}
.flatMap { ext ->
if (ext.sources.isEmpty()) {
return@flatMap if (ext.lang in enabledLanguages) listOf(ext) else emptyList()
}
ext.sources.filter { it.lang in enabledLanguages }
.map {
ext.copy(
name = it.name,
lang = it.lang,
pkgName = "${ext.pkgName}-${it.id}",
sources = listOf(it),
)
}
}
.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name })
Extensions(updates, installed, available, untrusted)
}
}
}

View File

@ -1,32 +0,0 @@
package eu.kanade.domain.extension.interactor
import android.content.pm.PackageInfo
import androidx.core.content.pm.PackageInfoCompat
import eu.kanade.domain.source.service.SourcePreferences
import mihon.domain.extensionrepo.repository.ExtensionRepoRepository
import tachiyomi.core.common.preference.getAndSet
class TrustExtension(
private val extensionRepoRepository: ExtensionRepoRepository,
private val preferences: SourcePreferences,
) {
suspend fun isTrusted(pkgInfo: PackageInfo, fingerprints: List<String>): Boolean {
val trustedFingerprints = extensionRepoRepository.getAll().map { it.signingKeyFingerprint }.toHashSet()
val key = "${pkgInfo.packageName}:${PackageInfoCompat.getLongVersionCode(pkgInfo)}:${fingerprints.last()}"
return trustedFingerprints.any { fingerprints.contains(it) } || key in preferences.trustedExtensions().get()
}
fun trust(pkgName: String, versionCode: Long, signatureHash: String) {
preferences.trustedExtensions().getAndSet { exts ->
// Remove previously trusted versions
val removed = exts.filterNot { it.startsWith("$pkgName:") }.toMutableSet()
removed.also { it += "$pkgName:$versionCode:$signatureHash" }
}
}
fun revokeAll() {
preferences.trustedExtensions().delete()
}
}

View File

@ -1,10 +0,0 @@
package eu.kanade.domain.extension.model
import eu.kanade.tachiyomi.extension.model.Extension
data class Extensions(
val updates: List<Extension.Installed>,
val installed: List<Extension.Installed>,
val available: List<Extension.Available>,
val untrusted: List<Extension.Untrusted>,
)

View File

@ -1,40 +0,0 @@
package eu.kanade.domain.manga.interactor
import tachiyomi.core.common.preference.plusAssign
import tachiyomi.domain.library.service.LibraryPreferences
class CreateSortTag(
private val preferences: LibraryPreferences,
private val getSortTag: GetSortTag,
) {
fun await(tag: String): Result {
// Do not allow duplicate categories.
// Do not allow duplicate categories.
if (tagExists(tag.trim())) {
return Result.TagExists
}
val size = preferences.sortTagsForLibrary().get().size
preferences.sortTagsForLibrary() += encodeTag(size, tag)
return Result.Success
}
sealed class Result {
data object TagExists : Result()
data object Success : Result()
}
/**
* Returns true if a tag with the given name already exists.
*/
private fun tagExists(name: String): Boolean {
return getSortTag.await().any { it.equals(name) }
}
companion object {
fun encodeTag(index: Int, tag: String) = "$index|${tag.trim()}"
}
}

View File

@ -1,17 +0,0 @@
package eu.kanade.domain.manga.interactor
import tachiyomi.domain.library.service.LibraryPreferences
class DeleteSortTag(
private val preferences: LibraryPreferences,
private val getSortTag: GetSortTag,
) {
fun await(tag: String) {
preferences.sortTagsForLibrary().set(
(getSortTag.await() - tag).mapIndexed { index, s ->
CreateSortTag.encodeTag(index, s)
}.toSet(),
)
}
}

View File

@ -1,24 +0,0 @@
package eu.kanade.domain.manga.interactor
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import tachiyomi.data.DatabaseHandler
class GetExcludedScanlators(
private val handler: DatabaseHandler,
) {
suspend fun await(mangaId: Long): Set<String> {
return handler.awaitList {
excluded_scanlatorsQueries.getExcludedScanlatorsByMangaId(mangaId)
}
.toSet()
}
fun subscribe(mangaId: Long): Flow<Set<String>> {
return handler.subscribeToList {
excluded_scanlatorsQueries.getExcludedScanlatorsByMangaId(mangaId)
}
.map { it.toSet() }
}
}

View File

@ -1,52 +0,0 @@
package eu.kanade.domain.manga.interactor
import eu.kanade.domain.chapter.model.toSChapter
import eu.kanade.domain.manga.model.PagePreview
import eu.kanade.domain.manga.model.toSManga
import eu.kanade.tachiyomi.data.cache.PagePreviewCache
import eu.kanade.tachiyomi.source.PagePreviewSource
import eu.kanade.tachiyomi.source.Source
import exh.source.getMainSource
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
import tachiyomi.domain.manga.model.Manga
class GetPagePreviews(
private val pagePreviewCache: PagePreviewCache,
private val getChaptersByMangaId: GetChaptersByMangaId,
) {
suspend fun await(manga: Manga, source: Source, page: Int): Result {
@Suppress("NAME_SHADOWING")
val source = source.getMainSource<PagePreviewSource>() ?: return Result.Unused
val chapters = getChaptersByMangaId.await(manga.id).sortedByDescending { it.sourceOrder }
val chapterIds = chapters.map { it.id }
return try {
val pagePreviews = try {
pagePreviewCache.getPageListFromCache(manga, chapterIds, page)
} catch (_: Exception) {
source.getPagePreviewList(manga.toSManga(), chapters.map { it.toSChapter() }, page).also {
pagePreviewCache.putPageListToCache(manga, chapterIds, it)
}
}
Result.Success(
pagePreviews.pagePreviews.map {
PagePreview(it.index, it.imageUrl, source.id)
},
pagePreviews.hasNextPage,
pagePreviews.pagePreviewPages,
)
} catch (e: Exception) {
Result.Error(e)
}
}
sealed class Result {
data object Unused : Result()
data class Success(
val pagePreviews: List<PagePreview>,
val hasNextPage: Boolean,
val pageCount: Int?,
) : Result()
data class Error(val error: Throwable) : Result()
}
}

View File

@ -1,29 +0,0 @@
package eu.kanade.domain.manga.interactor
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import tachiyomi.domain.library.service.LibraryPreferences
class GetSortTag(private val preferences: LibraryPreferences) {
fun subscribe(): Flow<List<String>> {
return preferences.sortTagsForLibrary().changes()
.map(::mapSortTags)
}
fun await() = getSortTags(preferences).let(::mapSortTags)
companion object {
fun getSortTags(preferences: LibraryPreferences) = preferences.sortTagsForLibrary().get()
fun mapSortTags(tags: Set<String>) = tags.mapNotNull {
val index = it.indexOf('|')
if (index != -1) {
(it.substring(0, index).toIntOrNull() ?: return@mapNotNull null) to it.substring(index + 1)
} else {
null
}
}
.sortedBy { it.first }.map { it.second }
}
}

View File

@ -1,40 +0,0 @@
package eu.kanade.domain.manga.interactor
import tachiyomi.domain.library.service.LibraryPreferences
class ReorderSortTag(
private val preferences: LibraryPreferences,
private val getSortTag: GetSortTag,
) {
fun await(tag: String, newPosition: Int): Result {
val tags = getSortTag.await()
val currentIndex = tags.indexOfFirst { it == tag }
if (currentIndex == -1) {
return Result.InternalError
}
if (currentIndex == newPosition) {
return Result.Unchanged
}
val reorderedTags = tags.toMutableList()
val reorderedTag = reorderedTags.removeAt(currentIndex)
reorderedTags.add(newPosition, reorderedTag)
preferences.sortTagsForLibrary().set(
reorderedTags.mapIndexed { index, s ->
CreateSortTag.encodeTag(index, s)
}.toSet(),
)
return Result.Success
}
sealed class Result {
data object Success : Result()
data object Unchanged : Result()
data object InternalError : Result()
}
}

View File

@ -1,22 +0,0 @@
package eu.kanade.domain.manga.interactor
import tachiyomi.data.DatabaseHandler
class SetExcludedScanlators(
private val handler: DatabaseHandler,
) {
suspend fun await(mangaId: Long, excludedScanlators: Set<String>) {
handler.await(inTransaction = true) {
val currentExcluded = handler.awaitList {
excluded_scanlatorsQueries.getExcludedScanlatorsByMangaId(mangaId)
}.toSet()
val toAdd = excludedScanlators.minus(currentExcluded)
for (scanlator in toAdd) {
excluded_scanlatorsQueries.insert(mangaId, scanlator)
}
val toRemove = currentExcluded.minus(excludedScanlators)
excluded_scanlatorsQueries.remove(mangaId, toRemove)
}
}
}

View File

@ -1,35 +0,0 @@
package eu.kanade.domain.manga.interactor
import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation
import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode
import tachiyomi.domain.manga.model.MangaUpdate
import tachiyomi.domain.manga.repository.MangaRepository
class SetMangaViewerFlags(
private val mangaRepository: MangaRepository,
) {
suspend fun awaitSetReadingMode(id: Long, flag: Long) {
val manga = mangaRepository.getMangaById(id)
mangaRepository.update(
MangaUpdate(
id = id,
viewerFlags = manga.viewerFlags.setFlag(flag, ReadingMode.MASK.toLong()),
),
)
}
suspend fun awaitSetOrientation(id: Long, flag: Long) {
val manga = mangaRepository.getMangaById(id)
mangaRepository.update(
MangaUpdate(
id = id,
viewerFlags = manga.viewerFlags.setFlag(flag, ReaderOrientation.MASK.toLong()),
),
)
}
private fun Long.setFlag(flag: Long, mask: Long): Long {
return this and mask.inv() or (flag and mask)
}
}

View File

@ -1,119 +0,0 @@
package eu.kanade.domain.manga.interactor
import eu.kanade.domain.manga.model.hasCustomCover
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.source.model.SManga
import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.domain.manga.interactor.FetchInterval
import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.manga.model.MangaUpdate
import tachiyomi.domain.manga.repository.MangaRepository
import tachiyomi.source.local.isLocal
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.time.Instant
import java.time.ZonedDateTime
class UpdateManga(
private val mangaRepository: MangaRepository,
private val fetchInterval: FetchInterval,
) {
suspend fun await(mangaUpdate: MangaUpdate): Boolean {
return mangaRepository.update(mangaUpdate)
}
suspend fun awaitAll(mangaUpdates: List<MangaUpdate>): Boolean {
return mangaRepository.updateAll(mangaUpdates)
}
suspend fun awaitUpdateFromSource(
localManga: Manga,
remoteManga: SManga,
manualFetch: Boolean,
coverCache: CoverCache = Injekt.get(),
libraryPreferences: LibraryPreferences = Injekt.get(),
downloadManager: DownloadManager = Injekt.get(),
): Boolean {
val remoteTitle = try {
remoteManga.title
} catch (_: UninitializedPropertyAccessException) {
""
}
// if the manga isn't a favorite (or 'update titles' preference is enabled), set its title from source and update in db
val title =
if (remoteTitle.isNotEmpty() && (!localManga.favorite || libraryPreferences.updateMangaTitles().get())) {
remoteTitle
} else {
null
}
val coverLastModified =
when {
// Never refresh covers if the url is empty to avoid "losing" existing covers
remoteManga.thumbnail_url.isNullOrEmpty() -> null
!manualFetch && localManga.thumbnailUrl == remoteManga.thumbnail_url -> null
localManga.isLocal() -> Instant.now().toEpochMilli()
localManga.hasCustomCover(coverCache) -> {
coverCache.deleteFromCache(localManga, false)
null
}
else -> {
coverCache.deleteFromCache(localManga, false)
Instant.now().toEpochMilli()
}
}
val thumbnailUrl = remoteManga.thumbnail_url?.takeIf { it.isNotEmpty() }
val success = mangaRepository.update(
MangaUpdate(
id = localManga.id,
title = title,
coverLastModified = coverLastModified,
author = remoteManga.author,
artist = remoteManga.artist,
description = remoteManga.description,
genre = remoteManga.getGenres(),
thumbnailUrl = thumbnailUrl,
status = remoteManga.status.toLong(),
updateStrategy = remoteManga.update_strategy,
initialized = true,
),
)
if (success && title != null) {
downloadManager.renameManga(localManga, title)
}
return success
}
suspend fun awaitUpdateFetchInterval(
manga: Manga,
dateTime: ZonedDateTime = ZonedDateTime.now(),
window: Pair<Long, Long> = fetchInterval.getWindow(dateTime),
): Boolean {
return mangaRepository.update(
fetchInterval.toMangaUpdate(manga, dateTime, window),
)
}
suspend fun awaitUpdateLastUpdate(mangaId: Long): Boolean {
return mangaRepository.update(MangaUpdate(id = mangaId, lastUpdate = Instant.now().toEpochMilli()))
}
suspend fun awaitUpdateCoverLastModified(mangaId: Long): Boolean {
return mangaRepository.update(MangaUpdate(id = mangaId, coverLastModified = Instant.now().toEpochMilli()))
}
suspend fun awaitUpdateFavorite(mangaId: Long, favorite: Boolean): Boolean {
val dateAdded = when (favorite) {
true -> Instant.now().toEpochMilli()
false -> 0
}
return mangaRepository.update(
MangaUpdate(id = mangaId, favorite = favorite, dateAdded = dateAdded),
)
}
}

View File

@ -1,123 +0,0 @@
package eu.kanade.domain.manga.model
import eu.kanade.domain.base.BasePreferences
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation
import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode
import eu.kanade.tachiyomi.util.storage.CbzCrypto
import tachiyomi.core.common.preference.TriState
import tachiyomi.core.metadata.comicinfo.ComicInfo
import tachiyomi.core.metadata.comicinfo.ComicInfoPublishingStatus
import tachiyomi.domain.chapter.model.Chapter
import tachiyomi.domain.manga.model.Manga
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
// TODO: move these into the domain model
val Manga.readingMode: Long
get() = viewerFlags and ReadingMode.MASK.toLong()
val Manga.readerOrientation: Long
get() = viewerFlags and ReaderOrientation.MASK.toLong()
val Manga.downloadedFilter: TriState
get() {
if (Injekt.get<BasePreferences>().downloadedOnly().get()) return TriState.ENABLED_IS
return when (downloadedFilterRaw) {
Manga.CHAPTER_SHOW_DOWNLOADED -> TriState.ENABLED_IS
Manga.CHAPTER_SHOW_NOT_DOWNLOADED -> TriState.ENABLED_NOT
else -> TriState.DISABLED
}
}
fun Manga.chaptersFiltered(): Boolean {
return unreadFilter != TriState.DISABLED ||
downloadedFilter != TriState.DISABLED ||
bookmarkedFilter != TriState.DISABLED
}
fun Manga.toSManga(): SManga = SManga.create().also {
it.url = url
// SY -->
it.title = ogTitle
it.artist = ogArtist
it.author = ogAuthor
it.description = ogDescription
it.genre = ogGenre.orEmpty().joinToString()
it.status = ogStatus.toInt()
// SY <--
it.thumbnail_url = thumbnailUrl
it.initialized = initialized
}
fun Manga.copyFrom(other: SManga): Manga {
// SY -->
val author = other.author ?: ogAuthor
val artist = other.artist ?: ogArtist
val thumbnailUrl = other.thumbnail_url ?: ogThumbnailUrl
val description = other.description ?: ogDescription
val genres = if (other.genre != null) {
other.getGenres()
} else {
ogGenre
}
// SY <--
return this.copy(
// SY -->
ogAuthor = author,
ogArtist = artist,
ogThumbnailUrl = thumbnailUrl,
ogDescription = description,
ogGenre = genres,
// SY <--
// SY -->
ogStatus = other.status.toLong(),
// SY <--
updateStrategy = other.update_strategy,
initialized = other.initialized && initialized,
)
}
fun Manga.hasCustomCover(coverCache: CoverCache = Injekt.get()): Boolean {
return coverCache.getCustomCoverFile(id).exists()
}
/**
* Creates a ComicInfo instance based on the manga and chapter metadata.
*/
fun getComicInfo(
manga: Manga,
chapter: Chapter,
urls: List<String>,
categories: List<String>?,
sourceName: String,
) = ComicInfo(
title = ComicInfo.Title(chapter.name),
series = ComicInfo.Series(manga.title),
number = chapter.chapterNumber.takeIf { it >= 0 }?.let {
if ((it.rem(1) == 0.0)) {
ComicInfo.Number(it.toInt().toString())
} else {
ComicInfo.Number(it.toString())
}
},
web = ComicInfo.Web(urls.joinToString(" ")),
summary = manga.description?.let { ComicInfo.Summary(it) },
writer = manga.author?.let { ComicInfo.Writer(it) },
penciller = manga.artist?.let { ComicInfo.Penciller(it) },
translator = chapter.scanlator?.let { ComicInfo.Translator(it) },
genre = manga.genre?.let { ComicInfo.Genre(it.joinToString()) },
publishingStatus = ComicInfo.PublishingStatusTachiyomi(
ComicInfoPublishingStatus.toComicInfoValue(manga.status),
),
categories = categories?.let { ComicInfo.CategoriesTachiyomi(it.joinToString()) },
source = ComicInfo.SourceMihon(sourceName),
// SY -->
padding = CbzCrypto.createComicInfoPadding()?.let { ComicInfo.PaddingTachiyomiSY(it) },
// SY <--
inker = null,
colorist = null,
letterer = null,
coverArtist = null,
tags = null,
)

View File

@ -1,22 +0,0 @@
package eu.kanade.domain.manga.model
import eu.kanade.tachiyomi.source.PagePreviewInfo
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
@Serializable
data class PagePreview(
val index: Int,
val imageUrl: String,
val source: Long,
) {
@Transient
private val _progress: MutableStateFlow<Int> = MutableStateFlow(-1)
@Transient
val progress = _progress.asStateFlow()
fun getPagePreviewInfo() = PagePreviewInfo(index, imageUrl, _progress)
}

View File

@ -1,23 +0,0 @@
package eu.kanade.domain.source.interactor
import eu.kanade.domain.source.service.SourcePreferences
import tachiyomi.core.common.preference.plusAssign
class CreateSourceCategory(private val preferences: SourcePreferences) {
fun await(category: String): Result {
if (category.contains("|")) {
return Result.InvalidName
}
// Create category.
preferences.sourcesTabCategories() += category
return Result.Success
}
sealed class Result {
data object InvalidName : Result()
data object Success : Result()
}
}

View File

@ -1,15 +0,0 @@
package eu.kanade.domain.source.interactor
import eu.kanade.domain.source.service.SourcePreferences
import tachiyomi.core.common.preference.getAndSet
import tachiyomi.core.common.preference.minusAssign
class DeleteSourceCategory(private val preferences: SourcePreferences) {
fun await(category: String) {
preferences.sourcesTabSourcesInCategories().getAndSet { sourcesInCategories ->
sourcesInCategories.filterNot { it.substringAfter("|") == category }.toSet()
}
preferences.sourcesTabCategories() -= category
}
}

View File

@ -1,83 +0,0 @@
package eu.kanade.domain.source.interactor
import eu.kanade.domain.source.service.SourcePreferences
import exh.source.BlacklistedSources
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import tachiyomi.domain.source.model.Pin
import tachiyomi.domain.source.model.Pins
import tachiyomi.domain.source.model.Source
import tachiyomi.domain.source.repository.SourceRepository
import tachiyomi.source.local.isLocal
class GetEnabledSources(
private val repository: SourceRepository,
private val preferences: SourcePreferences,
) {
fun subscribe(): Flow<List<Source>> {
return combine(
preferences.pinnedSources().changes(),
combine(
preferences.enabledLanguages().changes(),
preferences.disabledSources().changes(),
preferences.lastUsedSource().changes(),
) { a, b, c -> Triple(a, b, c) },
// SY -->
combine(
preferences.dataSaverExcludedSources().changes(),
preferences.sourcesTabSourcesInCategories().changes(),
preferences.sourcesTabCategoriesFilter().changes(),
) { a, b, c -> Triple(a, b, c) },
// SY <--
repository.getSources(),
) {
pinnedSourceIds,
(enabledLanguages, disabledSources, lastUsedSource),
(excludedFromDataSaver, sourcesInCategories, sourceCategoriesFilter),
sources,
->
val sourcesAndCategories = sourcesInCategories.map {
it.split('|').let { (source, test) -> source.toLong() to test }
}
val sourcesInSourceCategories = sourcesAndCategories.map { it.first }
sources
.filter { it.lang in enabledLanguages || it.isLocal() }
.filterNot { it.id.toString() in disabledSources || it.id in BlacklistedSources.HIDDEN_SOURCES }
.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name })
.flatMap {
val flag = if ("${it.id}" in pinnedSourceIds) Pins.pinned else Pins.unpinned
// SY -->
val categories = sourcesAndCategories.filter { (id) -> id == it.id }
.map(Pair<*, String>::second)
.toSet()
// SY <--
val source = it.copy(
pin = flag,
isExcludedFromDataSaver = it.id.toString() in excludedFromDataSaver,
categories = categories,
)
val toFlatten = mutableListOf(source)
if (source.id == lastUsedSource) {
toFlatten.add(source.copy(isUsedLast = true, pin = source.pin - Pin.Actual))
}
// SY -->
categories.forEach { category ->
toFlatten.add(source.copy(category = category, pin = source.pin - Pin.Actual))
}
if (
sourceCategoriesFilter &&
Pin.Actual !in toFlatten[0].pin &&
source.id in sourcesInSourceCategories
) {
toFlatten.removeAt(0)
}
// SY <--
toFlatten
}
}
.distinctUntilChanged()
}
}

View File

@ -1,73 +0,0 @@
package eu.kanade.domain.source.interactor
import eu.kanade.tachiyomi.source.model.FilterList
import exh.log.xLogE
import exh.util.nullIfBlank
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray
import tachiyomi.core.common.util.lang.withIOContext
import tachiyomi.domain.source.interactor.GetSavedSearchById
import tachiyomi.domain.source.interactor.GetSavedSearchBySourceId
import tachiyomi.domain.source.model.EXHSavedSearch
import tachiyomi.domain.source.model.SavedSearch
import xyz.nulldev.ts.api.http.serializer.FilterSerializer
class GetExhSavedSearch(
private val getSavedSearchById: GetSavedSearchById,
private val getSavedSearchBySourceId: GetSavedSearchBySourceId,
private val filterSerializer: FilterSerializer,
) {
suspend fun awaitOne(savedSearchId: Long, getFilterList: () -> FilterList): EXHSavedSearch? {
val search = getSavedSearchById.awaitOrNull(savedSearchId) ?: return null
return withIOContext { loadSearch(search, getFilterList) }
}
suspend fun await(sourceId: Long, getFilterList: () -> FilterList): List<EXHSavedSearch> {
return withIOContext { loadSearches(getSavedSearchBySourceId.await(sourceId), getFilterList) }
}
fun subscribe(sourceId: Long, getFilterList: () -> FilterList): Flow<List<EXHSavedSearch>> {
return getSavedSearchBySourceId.subscribe(sourceId)
.map { loadSearches(it, getFilterList) }
.flowOn(Dispatchers.IO)
}
private fun loadSearches(searches: List<SavedSearch>, getFilterList: () -> FilterList): List<EXHSavedSearch> {
return searches.map { loadSearch(it, getFilterList) }
}
private fun loadSearch(search: SavedSearch, getFilterList: () -> FilterList): EXHSavedSearch {
val filters = getFilters(search.filtersJson)
return EXHSavedSearch(
id = search.id,
name = search.name,
query = search.query?.nullIfBlank(),
filterList = filters?.let { deserializeFilters(it, getFilterList) },
)
}
private fun getFilters(filtersJson: String?): JsonArray? {
return runCatching {
filtersJson?.let { Json.decodeFromString<JsonArray>(it) }
}.onFailure {
xLogE("Failed to load saved search!", it)
}.getOrNull()
}
private fun deserializeFilters(filters: JsonArray, getFilterList: () -> FilterList): FilterList? {
return runCatching {
val originalFilters = getFilterList()
filterSerializer.deserialize(originalFilters, filters)
originalFilters
}.onFailure {
xLogE("Failed to load saved search!", it)
}.getOrNull()
}
}

View File

@ -1,35 +0,0 @@
package eu.kanade.domain.source.interactor
import eu.kanade.domain.base.BasePreferences
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.tachiyomi.extension.ExtensionManager
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
class GetIncognitoState(
private val basePreferences: BasePreferences,
private val sourcePreferences: SourcePreferences,
private val extensionManager: ExtensionManager,
) {
fun await(sourceId: Long?): Boolean {
if (basePreferences.incognitoMode().get()) return true
if (sourceId == null) return false
val extensionPackage = extensionManager.getExtensionPackage(sourceId) ?: return false
return extensionPackage in sourcePreferences.incognitoExtensions().get()
}
fun subscribe(sourceId: Long?): Flow<Boolean> {
if (sourceId == null) return basePreferences.incognitoMode().changes()
return combine(
basePreferences.incognitoMode().changes(),
sourcePreferences.incognitoExtensions().changes(),
extensionManager.getExtensionPackageAsFlow(sourceId),
) { incognito, incognitoExtensions, extensionPackage ->
incognito || (extensionPackage in incognitoExtensions)
}
.distinctUntilChanged()
}
}

View File

@ -1,35 +0,0 @@
package eu.kanade.domain.source.interactor
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.tachiyomi.util.system.LocaleHelper
import exh.source.BlacklistedSources
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import tachiyomi.domain.source.model.Source
import tachiyomi.domain.source.repository.SourceRepository
import java.util.SortedMap
class GetLanguagesWithSources(
private val repository: SourceRepository,
private val preferences: SourcePreferences,
) {
fun subscribe(): Flow<SortedMap<String, List<Source>>> {
return combine(
preferences.enabledLanguages().changes(),
preferences.disabledSources().changes(),
repository.getOnlineSources(),
) { enabledLanguage, disabledSource, onlineSources ->
val sortedSources = onlineSources.filterNot { it.id in BlacklistedSources.HIDDEN_SOURCES }.sortedWith(
compareBy<Source> { it.id.toString() in disabledSource }
.thenBy(String.CASE_INSENSITIVE_ORDER) { it.name },
)
sortedSources
.groupBy { it.lang }
.toSortedMap(
compareBy<String> { it !in enabledLanguage }.then(LocaleHelper.comparator),
)
}
}
}

View File

@ -1,17 +0,0 @@
package eu.kanade.domain.source.interactor
import eu.kanade.domain.ui.UiPreferences
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
class GetShowLatest(
private val preferences: UiPreferences,
) {
fun subscribe(hasSmartSearchConfig: Boolean): Flow<Boolean> {
return preferences.useNewSourceNavigation().changes()
.map {
!hasSmartSearchConfig && !it
}
}
}

View File

@ -1,14 +0,0 @@
package eu.kanade.domain.source.interactor
import eu.kanade.domain.source.service.SourcePreferences
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
class GetSourceCategories(
private val preferences: SourcePreferences,
) {
fun subscribe(): Flow<List<String>> {
return preferences.sourcesTabCategories().changes().map { it.sortedWith(String.CASE_INSENSITIVE_ORDER) }
}
}

View File

@ -1,57 +0,0 @@
package eu.kanade.domain.source.interactor
import eu.kanade.domain.source.service.SourcePreferences
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import tachiyomi.core.common.util.lang.compareToWithCollator
import tachiyomi.domain.source.model.Source
import tachiyomi.domain.source.repository.SourceRepository
import tachiyomi.source.local.isLocal
import java.util.Collections
class GetSourcesWithFavoriteCount(
private val repository: SourceRepository,
private val preferences: SourcePreferences,
) {
fun subscribe(): Flow<List<Pair<Source, Long>>> {
return combine(
preferences.migrationSortingDirection().changes(),
preferences.migrationSortingMode().changes(),
repository.getSourcesWithFavoriteCount(),
) { direction, mode, list ->
list
.filterNot { it.first.isLocal() }
.sortedWith(sortFn(direction, mode))
}
}
private fun sortFn(
direction: SetMigrateSorting.Direction,
sorting: SetMigrateSorting.Mode,
): java.util.Comparator<Pair<Source, Long>> {
val sortFn: (Pair<Source, Long>, Pair<Source, Long>) -> Int = { a, b ->
when (sorting) {
SetMigrateSorting.Mode.ALPHABETICAL -> {
when {
a.first.isStub && !b.first.isStub -> -1
b.first.isStub && !a.first.isStub -> 1
else -> a.first.name.lowercase().compareToWithCollator(b.first.name.lowercase())
}
}
SetMigrateSorting.Mode.TOTAL -> {
when {
a.first.isStub && !b.first.isStub -> -1
b.first.isStub && !a.first.isStub -> 1
else -> a.second.compareTo(b.second)
}
}
}
}
return when (direction) {
SetMigrateSorting.Direction.ASCENDING -> Comparator(sortFn)
SetMigrateSorting.Direction.DESCENDING -> Collections.reverseOrder(sortFn)
}
}
}

View File

@ -1,33 +0,0 @@
package eu.kanade.domain.source.interactor
import eu.kanade.domain.source.service.SourcePreferences
import tachiyomi.core.common.preference.getAndSet
class RenameSourceCategory(
private val preferences: SourcePreferences,
private val createSourceCategory: CreateSourceCategory,
) {
fun await(categoryOld: String, categoryNew: String): CreateSourceCategory.Result {
when (val result = createSourceCategory.await(categoryNew)) {
CreateSourceCategory.Result.InvalidName -> return result
CreateSourceCategory.Result.Success -> {}
}
preferences.sourcesTabSourcesInCategories().getAndSet { sourcesInCategories ->
sourcesInCategories.map {
val index = it.indexOf('|')
if (index != -1 && it.substring(index + 1) == categoryOld) {
it.substring(0, index + 1) + categoryNew
} else {
it
}
}.toSet()
}
preferences.sourcesTabCategories().getAndSet {
it.minus(categoryOld).plus(categoryNew)
}
return CreateSourceCategory.Result.Success
}
}

View File

@ -1,23 +0,0 @@
package eu.kanade.domain.source.interactor
import eu.kanade.domain.source.service.SourcePreferences
class SetMigrateSorting(
private val preferences: SourcePreferences,
) {
fun await(mode: Mode, direction: Direction) {
preferences.migrationSortingMode().set(mode)
preferences.migrationSortingDirection().set(direction)
}
enum class Mode {
ALPHABETICAL,
TOTAL,
}
enum class Direction {
ASCENDING,
DESCENDING,
}
}

View File

@ -1,23 +0,0 @@
package eu.kanade.domain.source.interactor
import eu.kanade.domain.source.service.SourcePreferences
import tachiyomi.core.common.preference.getAndSet
import tachiyomi.domain.source.model.Source
class SetSourceCategories(
private val preferences: SourcePreferences,
) {
fun await(source: Source, sourceCategories: List<String>) {
val sourceIdString = source.id.toString()
preferences.sourcesTabSourcesInCategories().getAndSet { sourcesInCategories ->
val currentSourceCategories = sourcesInCategories.filterNot {
it.substringBefore('|') == sourceIdString
}
val newSourceCategories = currentSourceCategories + sourceCategories.map {
"$sourceIdString|$it"
}
newSourceCategories.toSet()
}
}
}

View File

@ -1,20 +0,0 @@
package eu.kanade.domain.source.interactor
import eu.kanade.domain.source.service.SourcePreferences
import tachiyomi.core.common.preference.getAndSet
import tachiyomi.domain.source.model.Source
class ToggleExcludeFromDataSaver(
private val preferences: SourcePreferences,
) {
fun await(source: Source) {
preferences.dataSaverExcludedSources().getAndSet {
if (source.id.toString() in it) {
it - source.id.toString()
} else {
it + source.id.toString()
}
}
}
}

View File

@ -1,14 +0,0 @@
package eu.kanade.domain.source.interactor
import eu.kanade.domain.source.service.SourcePreferences
import tachiyomi.core.common.preference.getAndSet
class ToggleIncognito(
private val preferences: SourcePreferences,
) {
fun await(extensions: String, enable: Boolean) {
preferences.incognitoExtensions().getAndSet {
if (enable) it.plus(extensions) else it.minus(extensions)
}
}
}

View File

@ -1,16 +0,0 @@
package eu.kanade.domain.source.interactor
import eu.kanade.domain.source.service.SourcePreferences
import tachiyomi.core.common.preference.getAndSet
class ToggleLanguage(
val preferences: SourcePreferences,
) {
fun await(language: String) {
val isEnabled = language in preferences.enabledLanguages().get()
preferences.enabledLanguages().getAndSet { enabled ->
if (isEnabled) enabled.minus(language) else enabled.plus(language)
}
}
}

View File

@ -1,31 +0,0 @@
package eu.kanade.domain.source.interactor
import eu.kanade.domain.source.service.SourcePreferences
import tachiyomi.core.common.preference.getAndSet
import tachiyomi.domain.source.model.Source
class ToggleSource(
private val preferences: SourcePreferences,
) {
fun await(source: Source, enable: Boolean = isEnabled(source.id)) {
await(source.id, enable)
}
fun await(sourceId: Long, enable: Boolean = isEnabled(sourceId)) {
preferences.disabledSources().getAndSet { disabled ->
if (enable) disabled.minus("$sourceId") else disabled.plus("$sourceId")
}
}
fun await(sourceIds: List<Long>, enable: Boolean) {
val transformedSourceIds = sourceIds.map { it.toString() }
preferences.disabledSources().getAndSet { disabled ->
if (enable) disabled.minus(transformedSourceIds) else disabled.plus(transformedSourceIds)
}
}
private fun isEnabled(sourceId: Long): Boolean {
return sourceId.toString() in preferences.disabledSources().get()
}
}

View File

@ -1,17 +0,0 @@
package eu.kanade.domain.source.interactor
import eu.kanade.domain.source.service.SourcePreferences
import tachiyomi.core.common.preference.getAndSet
import tachiyomi.domain.source.model.Source
class ToggleSourcePin(
private val preferences: SourcePreferences,
) {
fun await(source: Source) {
val isPinned = source.id.toString() in preferences.pinnedSources().get()
preferences.pinnedSources().getAndSet { pinned ->
if (isPinned) pinned.minus("${source.id}") else pinned.plus("${source.id}")
}
}
}

View File

@ -1,16 +0,0 @@
package eu.kanade.domain.source.model
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asImageBitmap
import androidx.core.graphics.drawable.toBitmap
import eu.kanade.tachiyomi.extension.ExtensionManager
import tachiyomi.domain.source.model.Source
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
val Source.icon: ImageBitmap?
get() {
return Injekt.get<ExtensionManager>().getAppIconForSource(id)
?.toBitmap()
?.asImageBitmap()
}

View File

@ -1,119 +0,0 @@
package eu.kanade.domain.source.service
import eu.kanade.domain.source.interactor.SetMigrateSorting
import eu.kanade.tachiyomi.util.system.LocaleHelper
import tachiyomi.core.common.preference.Preference
import tachiyomi.core.common.preference.PreferenceStore
import tachiyomi.core.common.preference.getEnum
import tachiyomi.domain.library.model.LibraryDisplayMode
class SourcePreferences(
private val preferenceStore: PreferenceStore,
) {
fun sourceDisplayMode() = preferenceStore.getObject(
"pref_display_mode_catalogue",
LibraryDisplayMode.default,
LibraryDisplayMode.Serializer::serialize,
LibraryDisplayMode.Serializer::deserialize,
)
fun enabledLanguages() = preferenceStore.getStringSet("source_languages", LocaleHelper.getDefaultEnabledLanguages())
fun disabledSources() = preferenceStore.getStringSet("hidden_catalogues", emptySet())
fun incognitoExtensions() = preferenceStore.getStringSet("incognito_extensions", emptySet())
fun pinnedSources() = preferenceStore.getStringSet("pinned_catalogues", emptySet())
fun lastUsedSource() = preferenceStore.getLong(
Preference.appStateKey("last_catalogue_source"),
-1,
)
fun showNsfwSource() = preferenceStore.getBoolean("show_nsfw_source", true)
fun migrationSortingMode() = preferenceStore.getEnum("pref_migration_sorting", SetMigrateSorting.Mode.ALPHABETICAL)
fun migrationSortingDirection() = preferenceStore.getEnum(
"pref_migration_direction",
SetMigrateSorting.Direction.ASCENDING,
)
fun hideInLibraryItems() = preferenceStore.getBoolean("browse_hide_in_library_items", false)
fun extensionRepos() = preferenceStore.getStringSet("extension_repos", emptySet())
fun extensionUpdatesCount() = preferenceStore.getInt("ext_updates_count", 0)
fun trustedExtensions() = preferenceStore.getStringSet(
Preference.appStateKey("trusted_extensions"),
emptySet(),
)
fun globalSearchFilterState() = preferenceStore.getBoolean(
Preference.appStateKey("has_filters_toggle_state"),
false,
)
// SY -->
fun enableSourceBlacklist() = preferenceStore.getBoolean("eh_enable_source_blacklist", true)
fun sourcesTabCategories() = preferenceStore.getStringSet("sources_tab_categories", mutableSetOf())
fun sourcesTabCategoriesFilter() = preferenceStore.getBoolean("sources_tab_categories_filter", false)
fun sourcesTabSourcesInCategories() = preferenceStore.getStringSet("sources_tab_source_categories", mutableSetOf())
fun dataSaver() = preferenceStore.getEnum("data_saver", DataSaver.NONE)
fun dataSaverIgnoreJpeg() = preferenceStore.getBoolean("ignore_jpeg", false)
fun dataSaverIgnoreGif() = preferenceStore.getBoolean("ignore_gif", true)
fun dataSaverImageQuality() = preferenceStore.getInt("data_saver_image_quality", 80)
fun dataSaverImageFormatJpeg() = preferenceStore.getBoolean("data_saver_image_format_jpeg", false)
fun dataSaverServer() = preferenceStore.getString("data_saver_server", "")
fun dataSaverColorBW() = preferenceStore.getBoolean("data_saver_color_bw", false)
fun dataSaverExcludedSources() = preferenceStore.getStringSet("data_saver_excluded", emptySet())
fun dataSaverDownloader() = preferenceStore.getBoolean("data_saver_downloader", true)
enum class DataSaver {
NONE,
BANDWIDTH_HERO,
WSRV_NL,
}
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(Preference.appStateKey("skip_pre_migration"), false)
fun hideNotFoundMigration() = preferenceStore.getBoolean("hide_not_found_migration", false)
fun showOnlyUpdatesMigration() = preferenceStore.getBoolean("show_only_updates_migration", false)
fun allowLocalSourceHiddenFolders() = preferenceStore.getBoolean("allow_local_source_hidden_folders", false)
fun preferredMangaDexId() = preferenceStore.getString("preferred_mangaDex_id", "0")
fun mangadexSyncToLibraryIndexes() = preferenceStore.getStringSet(
"pref_mangadex_sync_to_library_indexes",
emptySet(),
)
fun recommendationSearchFlags() = preferenceStore.getInt("rec_search_flags", Int.MAX_VALUE)
// SY <--
}

View File

@ -1,105 +0,0 @@
package eu.kanade.domain.sync
import eu.kanade.domain.sync.models.SyncSettings
import eu.kanade.tachiyomi.data.sync.models.SyncTriggerOptions
import tachiyomi.core.common.preference.Preference
import tachiyomi.core.common.preference.PreferenceStore
import java.util.UUID
class SyncPreferences(
private val preferenceStore: PreferenceStore,
) {
fun clientHost() = preferenceStore.getString("sync_client_host", "https://sync.tachiyomi.org")
fun clientAPIKey() = preferenceStore.getString("sync_client_api_key", "")
fun lastSyncTimestamp() = preferenceStore.getLong(Preference.appStateKey("last_sync_timestamp"), 0L)
fun lastSyncEtag() = preferenceStore.getString("sync_etag", "")
fun syncInterval() = preferenceStore.getInt("sync_interval", 0)
fun syncService() = preferenceStore.getInt("sync_service", 0)
fun googleDriveAccessToken() = preferenceStore.getString(
Preference.appStateKey("google_drive_access_token"),
"",
)
fun googleDriveRefreshToken() = preferenceStore.getString(
Preference.appStateKey("google_drive_refresh_token"),
"",
)
fun uniqueDeviceID(): String {
val uniqueIDPreference = preferenceStore.getString(Preference.appStateKey("unique_device_id"), "")
// Retrieve the current value of the preference
var uniqueID = uniqueIDPreference.get()
if (uniqueID.isBlank()) {
uniqueID = UUID.randomUUID().toString()
uniqueIDPreference.set(uniqueID)
}
return uniqueID
}
fun isSyncEnabled(): Boolean {
return syncService().get() != 0
}
fun getSyncSettings(): SyncSettings {
return SyncSettings(
libraryEntries = preferenceStore.getBoolean("library_entries", true).get(),
categories = preferenceStore.getBoolean("categories", true).get(),
chapters = preferenceStore.getBoolean("chapters", true).get(),
tracking = preferenceStore.getBoolean("tracking", true).get(),
history = preferenceStore.getBoolean("history", true).get(),
appSettings = preferenceStore.getBoolean("appSettings", true).get(),
extensionRepoSettings = preferenceStore.getBoolean("extensionRepoSettings", true).get(),
sourceSettings = preferenceStore.getBoolean("sourceSettings", true).get(),
privateSettings = preferenceStore.getBoolean("privateSettings", true).get(),
// SY -->
customInfo = preferenceStore.getBoolean("customInfo", true).get(),
readEntries = preferenceStore.getBoolean("readEntries", true).get(),
savedSearches = preferenceStore.getBoolean("savedSearches", true).get(),
// SY <--
)
}
fun setSyncSettings(syncSettings: SyncSettings) {
preferenceStore.getBoolean("library_entries", true).set(syncSettings.libraryEntries)
preferenceStore.getBoolean("categories", true).set(syncSettings.categories)
preferenceStore.getBoolean("chapters", true).set(syncSettings.chapters)
preferenceStore.getBoolean("tracking", true).set(syncSettings.tracking)
preferenceStore.getBoolean("history", true).set(syncSettings.history)
preferenceStore.getBoolean("appSettings", true).set(syncSettings.appSettings)
preferenceStore.getBoolean("extensionRepoSettings", true).set(syncSettings.extensionRepoSettings)
preferenceStore.getBoolean("sourceSettings", true).set(syncSettings.sourceSettings)
preferenceStore.getBoolean("privateSettings", true).set(syncSettings.privateSettings)
// SY -->
preferenceStore.getBoolean("customInfo", true).set(syncSettings.customInfo)
preferenceStore.getBoolean("readEntries", true).set(syncSettings.readEntries)
preferenceStore.getBoolean("savedSearches", true).set(syncSettings.savedSearches)
// SY <--
}
fun getSyncTriggerOptions(): SyncTriggerOptions {
return SyncTriggerOptions(
syncOnChapterRead = preferenceStore.getBoolean("sync_on_chapter_read", false).get(),
syncOnChapterOpen = preferenceStore.getBoolean("sync_on_chapter_open", false).get(),
syncOnAppStart = preferenceStore.getBoolean("sync_on_app_start", false).get(),
syncOnAppResume = preferenceStore.getBoolean("sync_on_app_resume", false).get(),
)
}
fun setSyncTriggerOptions(syncTriggerOptions: SyncTriggerOptions) {
preferenceStore.getBoolean("sync_on_chapter_read", false)
.set(syncTriggerOptions.syncOnChapterRead)
preferenceStore.getBoolean("sync_on_chapter_open", false)
.set(syncTriggerOptions.syncOnChapterOpen)
preferenceStore.getBoolean("sync_on_app_start", false)
.set(syncTriggerOptions.syncOnAppStart)
preferenceStore.getBoolean("sync_on_app_resume", false)
.set(syncTriggerOptions.syncOnAppResume)
}
}

View File

@ -1,19 +0,0 @@
package eu.kanade.domain.sync.models
data class SyncSettings(
val libraryEntries: Boolean = true,
val categories: Boolean = true,
val chapters: Boolean = true,
val tracking: Boolean = true,
val history: Boolean = true,
val appSettings: Boolean = true,
val extensionRepoSettings: Boolean = true,
val sourceSettings: Boolean = true,
val privateSettings: Boolean = false,
// SY -->
val customInfo: Boolean = true,
val readEntries: Boolean = true,
val savedSearches: Boolean = true,
// SY <--
)

View File

@ -1,107 +0,0 @@
package eu.kanade.domain.track.interactor
import eu.kanade.domain.track.model.toDbTrack
import eu.kanade.domain.track.model.toDomainTrack
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.EnhancedTracker
import eu.kanade.tachiyomi.data.track.Tracker
import eu.kanade.tachiyomi.data.track.TrackerManager
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.util.lang.convertEpochMillisZone
import logcat.LogPriority
import tachiyomi.core.common.util.lang.withIOContext
import tachiyomi.core.common.util.lang.withNonCancellableContext
import tachiyomi.core.common.util.system.logcat
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
import tachiyomi.domain.history.interactor.GetHistory
import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.track.interactor.InsertTrack
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.time.ZoneOffset
class AddTracks(
private val insertTrack: InsertTrack,
private val syncChapterProgressWithTrack: SyncChapterProgressWithTrack,
private val getChaptersByMangaId: GetChaptersByMangaId,
private val trackerManager: TrackerManager,
) {
// TODO: update all trackers based on common data
suspend fun bind(tracker: Tracker, item: Track, mangaId: Long) = withNonCancellableContext {
withIOContext {
val allChapters = getChaptersByMangaId.await(mangaId)
val hasReadChapters = allChapters.any { it.read }
tracker.bind(item, hasReadChapters)
var track = item.toDomainTrack(idRequired = false) ?: return@withIOContext
insertTrack.await(track)
// TODO: merge into [SyncChapterProgressWithTrack]?
// Update chapter progress if newer chapters marked read locally
if (hasReadChapters) {
val latestLocalReadChapterNumber = allChapters
.sortedBy { it.chapterNumber }
.takeWhile { it.read }
.lastOrNull()
?.chapterNumber ?: -1.0
if (latestLocalReadChapterNumber > track.lastChapterRead) {
track = track.copy(
lastChapterRead = latestLocalReadChapterNumber,
)
tracker.setRemoteLastChapterRead(track.toDbTrack(), latestLocalReadChapterNumber.toInt())
}
if (track.startDate <= 0) {
val firstReadChapterDate = Injekt.get<GetHistory>().await(mangaId)
.sortedBy { it.readAt }
.firstOrNull()
?.readAt
firstReadChapterDate?.let {
val startDate = firstReadChapterDate.time.convertEpochMillisZone(
ZoneOffset.systemDefault(),
ZoneOffset.UTC,
)
track = track.copy(
startDate = startDate,
)
tracker.setRemoteStartDate(track.toDbTrack(), startDate)
}
}
}
syncChapterProgressWithTrack.await(mangaId, track, tracker)
}
}
suspend fun bindEnhancedTrackers(manga: Manga, source: Source) = withNonCancellableContext {
withIOContext {
trackerManager.loggedInTrackers()
.filterIsInstance<EnhancedTracker>()
.filter { it.accept(source) }
.forEach { service ->
try {
service.match(manga)?.let { track ->
track.manga_id = manga.id
(service as Tracker).bind(track)
insertTrack.await(track.toDomainTrack(idRequired = false)!!)
syncChapterProgressWithTrack.await(
manga.id,
track.toDomainTrack(idRequired = false)!!,
service,
)
}
} catch (e: Exception) {
logcat(
LogPriority.WARN,
e,
) { "Could not match manga: ${manga.title} with service $service" }
}
}
}
}
}

View File

@ -1,46 +0,0 @@
package eu.kanade.domain.track.interactor
import eu.kanade.domain.track.model.toDbTrack
import eu.kanade.domain.track.model.toDomainTrack
import eu.kanade.tachiyomi.data.track.Tracker
import eu.kanade.tachiyomi.data.track.TrackerManager
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.supervisorScope
import tachiyomi.domain.track.interactor.GetTracks
import tachiyomi.domain.track.interactor.InsertTrack
class RefreshTracks(
private val getTracks: GetTracks,
private val trackerManager: TrackerManager,
private val insertTrack: InsertTrack,
private val syncChapterProgressWithTrack: SyncChapterProgressWithTrack,
) {
/**
* Fetches updated tracking data from all logged in trackers.
*
* @return Failed updates.
*/
suspend fun await(mangaId: Long): List<Pair<Tracker?, Throwable>> {
return supervisorScope {
return@supervisorScope getTracks.await(mangaId)
.map { it to trackerManager.get(it.trackerId) }
.filter { (_, service) -> service?.isLoggedIn == true }
.map { (track, service) ->
async {
return@async try {
val updatedTrack = service!!.refresh(track.toDbTrack()).toDomainTrack()!!
insertTrack.await(updatedTrack)
syncChapterProgressWithTrack.await(mangaId, updatedTrack, service)
null
} catch (e: Throwable) {
service to e
}
}
}
.awaitAll()
.filterNotNull()
}
}
}

View File

@ -1,51 +0,0 @@
package eu.kanade.domain.track.interactor
import eu.kanade.domain.track.model.toDbTrack
import eu.kanade.tachiyomi.data.track.EnhancedTracker
import eu.kanade.tachiyomi.data.track.Tracker
import logcat.LogPriority
import tachiyomi.core.common.util.system.logcat
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
import tachiyomi.domain.chapter.interactor.UpdateChapter
import tachiyomi.domain.chapter.model.toChapterUpdate
import tachiyomi.domain.track.interactor.InsertTrack
import tachiyomi.domain.track.model.Track
import kotlin.math.max
class SyncChapterProgressWithTrack(
private val updateChapter: UpdateChapter,
private val insertTrack: InsertTrack,
private val getChaptersByMangaId: GetChaptersByMangaId,
) {
suspend fun await(
mangaId: Long,
remoteTrack: Track,
tracker: Tracker,
) {
if (tracker !is EnhancedTracker) {
return
}
val sortedChapters = getChaptersByMangaId.await(mangaId)
.sortedBy { it.chapterNumber }
.filter { it.isRecognizedNumber }
val chapterUpdates = sortedChapters
.filter { chapter -> chapter.chapterNumber <= remoteTrack.lastChapterRead && !chapter.read }
.map { it.copy(read = true).toChapterUpdate() }
// only take into account continuous reading
val localLastRead = sortedChapters.takeWhile { it.read }.lastOrNull()?.chapterNumber ?: 0F
val lastRead = max(remoteTrack.lastChapterRead, localLastRead.toDouble())
val updatedTrack = remoteTrack.copy(lastChapterRead = lastRead)
try {
tracker.update(updatedTrack.toDbTrack())
updateChapter.awaitAll(chapterUpdates)
insertTrack.await(updatedTrack)
} catch (e: Throwable) {
logcat(LogPriority.WARN, e)
}
}
}

View File

@ -1,66 +0,0 @@
package eu.kanade.domain.track.interactor
import android.content.Context
import eu.kanade.domain.track.model.toDbTrack
import eu.kanade.domain.track.model.toDomainTrack
import eu.kanade.domain.track.service.DelayedTrackingUpdateJob
import eu.kanade.domain.track.store.DelayedTrackingStore
import eu.kanade.tachiyomi.data.track.TrackerManager
import eu.kanade.tachiyomi.data.track.mdlist.MdList
import exh.md.utils.FollowStatus
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import logcat.LogPriority
import tachiyomi.core.common.util.lang.withNonCancellableContext
import tachiyomi.core.common.util.system.logcat
import tachiyomi.domain.track.interactor.GetTracks
import tachiyomi.domain.track.interactor.InsertTrack
class TrackChapter(
private val getTracks: GetTracks,
private val trackerManager: TrackerManager,
private val insertTrack: InsertTrack,
private val delayedTrackingStore: DelayedTrackingStore,
) {
suspend fun await(context: Context, mangaId: Long, chapterNumber: Double, setupJobOnFailure: Boolean = true) {
withNonCancellableContext {
val tracks = getTracks.await(mangaId)
if (tracks.isEmpty()) return@withNonCancellableContext
tracks.mapNotNull { track ->
val service = trackerManager.get(track.trackerId)
if (
service == null ||
!service.isLoggedIn ||
chapterNumber <= track.lastChapterRead /* SY --> */ ||
(service is MdList && track.status == FollowStatus.UNFOLLOWED.long)/* SY <-- */
) {
return@mapNotNull null
}
async {
runCatching {
try {
val updatedTrack = service.refresh(track.toDbTrack())
.toDomainTrack(idRequired = true)!!
.copy(lastChapterRead = chapterNumber)
service.update(updatedTrack.toDbTrack(), true)
insertTrack.await(updatedTrack)
delayedTrackingStore.remove(track.id)
} catch (e: Exception) {
delayedTrackingStore.add(track.id, chapterNumber)
if (setupJobOnFailure) {
DelayedTrackingUpdateJob.setupTask(context)
}
throw e
}
}
}
}
.awaitAll()
.mapNotNull { it.exceptionOrNull() }
.forEach { logcat(LogPriority.WARN, it) }
}
}
}

View File

@ -1,10 +0,0 @@
package eu.kanade.domain.track.model
import dev.icerock.moko.resources.StringResource
import tachiyomi.i18n.MR
enum class AutoTrackState(val titleRes: StringResource) {
ALWAYS(MR.strings.lock_always),
ASK(MR.strings.default_category_summary),
NEVER(MR.strings.lock_never),
}

View File

@ -1,51 +0,0 @@
package eu.kanade.domain.track.model
import tachiyomi.domain.track.model.Track
import eu.kanade.tachiyomi.data.database.models.Track as DbTrack
fun Track.copyPersonalFrom(other: Track): Track {
return this.copy(
lastChapterRead = other.lastChapterRead,
score = other.score,
status = other.status,
startDate = other.startDate,
finishDate = other.finishDate,
private = other.private,
)
}
fun Track.toDbTrack(): DbTrack = DbTrack.create(trackerId).also {
it.id = id
it.manga_id = mangaId
it.remote_id = remoteId
it.library_id = libraryId
it.title = title
it.last_chapter_read = lastChapterRead
it.total_chapters = totalChapters
it.status = status
it.score = score
it.tracking_url = remoteUrl
it.started_reading_date = startDate
it.finished_reading_date = finishDate
it.private = private
}
fun DbTrack.toDomainTrack(idRequired: Boolean = true): Track? {
val trackId = id ?: if (!idRequired) -1 else return null
return Track(
id = trackId,
mangaId = manga_id,
trackerId = tracker_id,
remoteId = remote_id,
libraryId = library_id,
title = title,
lastChapterRead = last_chapter_read,
totalChapters = total_chapters,
status = status,
score = score,
remoteUrl = tracking_url,
startDate = started_reading_date,
finishDate = finished_reading_date,
private = private,
)
}

View File

@ -1,72 +0,0 @@
package eu.kanade.domain.track.service
import android.content.Context
import androidx.work.BackoffPolicy
import androidx.work.Constraints
import androidx.work.CoroutineWorker
import androidx.work.ExistingWorkPolicy
import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkerParameters
import eu.kanade.domain.track.interactor.TrackChapter
import eu.kanade.domain.track.store.DelayedTrackingStore
import eu.kanade.tachiyomi.util.system.workManager
import logcat.LogPriority
import tachiyomi.core.common.util.lang.withIOContext
import tachiyomi.core.common.util.system.logcat
import tachiyomi.domain.track.interactor.GetTracks
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.concurrent.TimeUnit
class DelayedTrackingUpdateJob(private val context: Context, workerParams: WorkerParameters) :
CoroutineWorker(context, workerParams) {
override suspend fun doWork(): Result {
if (runAttemptCount > 3) {
return Result.failure()
}
val getTracks = Injekt.get<GetTracks>()
val trackChapter = Injekt.get<TrackChapter>()
val delayedTrackingStore = Injekt.get<DelayedTrackingStore>()
withIOContext {
delayedTrackingStore.getItems()
.mapNotNull {
val track = getTracks.awaitOne(it.trackId)
if (track == null) {
delayedTrackingStore.remove(it.trackId)
}
track?.copy(lastChapterRead = it.lastChapterRead.toDouble())
}
.forEach { track ->
logcat(LogPriority.DEBUG) {
"Updating delayed track item: ${track.mangaId}, last chapter read: ${track.lastChapterRead}"
}
trackChapter.await(context, track.mangaId, track.lastChapterRead, setupJobOnFailure = false)
}
}
return if (delayedTrackingStore.getItems().isEmpty()) Result.success() else Result.retry()
}
companion object {
private const val TAG = "DelayedTrackingUpdate"
fun setupTask(context: Context) {
val constraints = Constraints(
requiredNetworkType = NetworkType.CONNECTED,
)
val request = OneTimeWorkRequestBuilder<DelayedTrackingUpdateJob>()
.setConstraints(constraints)
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 5, TimeUnit.MINUTES)
.addTag(TAG)
.build()
context.workManager.enqueueUniqueWork(TAG, ExistingWorkPolicy.REPLACE, request)
}
}
}

View File

@ -1,52 +0,0 @@
package eu.kanade.domain.track.service
import eu.kanade.domain.track.model.AutoTrackState
import eu.kanade.tachiyomi.data.track.Tracker
import eu.kanade.tachiyomi.data.track.anilist.Anilist
import tachiyomi.core.common.preference.Preference
import tachiyomi.core.common.preference.PreferenceStore
import tachiyomi.core.common.preference.getEnum
class TrackPreferences(
private val preferenceStore: PreferenceStore,
) {
fun trackUsername(tracker: Tracker) = preferenceStore.getString(
Preference.privateKey("pref_mangasync_username_${tracker.id}"),
"",
)
fun trackPassword(tracker: Tracker) = preferenceStore.getString(
Preference.privateKey("pref_mangasync_password_${tracker.id}"),
"",
)
fun trackAuthExpired(tracker: Tracker) = preferenceStore.getBoolean(
Preference.privateKey("pref_tracker_auth_expired_${tracker.id}"),
false,
)
fun setCredentials(tracker: Tracker, username: String, password: String) {
trackUsername(tracker).set(username)
trackPassword(tracker).set(password)
trackAuthExpired(tracker).set(false)
}
fun trackToken(tracker: Tracker) = preferenceStore.getString(Preference.privateKey("track_token_${tracker.id}"), "")
fun anilistScoreType() = preferenceStore.getString("anilist_score_type", Anilist.POINT_10)
fun autoUpdateTrack() = preferenceStore.getBoolean("pref_auto_update_manga_sync_key", true)
fun autoUpdateTrackOnMarkRead() = preferenceStore.getEnum(
"pref_auto_update_manga_on_mark_read",
AutoTrackState.ALWAYS,
)
// SY -->
fun resolveUsingSourceMetadata() = preferenceStore.getBoolean(
"pref_resolve_using_source_metadata_key",
true,
)
// SY <--
}

View File

@ -1,44 +0,0 @@
package eu.kanade.domain.track.store
import android.content.Context
import androidx.core.content.edit
import logcat.LogPriority
import tachiyomi.core.common.util.system.logcat
class DelayedTrackingStore(context: Context) {
/**
* Preference file where queued tracking updates are stored.
*/
private val preferences = context.getSharedPreferences("tracking_queue", Context.MODE_PRIVATE)
fun add(trackId: Long, lastChapterRead: Double) {
val previousLastChapterRead = preferences.getFloat(trackId.toString(), 0f)
if (lastChapterRead > previousLastChapterRead) {
logcat(LogPriority.DEBUG) { "Queuing track item: $trackId, last chapter read: $lastChapterRead" }
preferences.edit {
putFloat(trackId.toString(), lastChapterRead.toFloat())
}
}
}
fun remove(trackId: Long) {
preferences.edit {
remove(trackId.toString())
}
}
fun getItems(): List<DelayedTrackingItem> {
return preferences.all.mapNotNull {
DelayedTrackingItem(
trackId = it.key.toLong(),
lastChapterRead = it.value.toString().toFloat(),
)
}
}
data class DelayedTrackingItem(
val trackId: Long,
val lastChapterRead: Float,
)
}

View File

@ -1,75 +0,0 @@
package eu.kanade.domain.ui
import android.os.Build
import eu.kanade.domain.ui.model.AppTheme
import eu.kanade.domain.ui.model.TabletUiMode
import eu.kanade.domain.ui.model.ThemeMode
import eu.kanade.tachiyomi.util.system.DeviceUtil
import eu.kanade.tachiyomi.util.system.isDynamicColorAvailable
import tachiyomi.core.common.preference.PreferenceStore
import tachiyomi.core.common.preference.getEnum
import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle
import java.util.Locale
class UiPreferences(
private val preferenceStore: PreferenceStore,
) {
fun themeMode() = preferenceStore.getEnum(
"pref_theme_mode_key",
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
ThemeMode.SYSTEM
} else {
ThemeMode.LIGHT
},
)
fun appTheme() = preferenceStore.getEnum(
"pref_app_theme",
if (DeviceUtil.isDynamicColorAvailable) {
AppTheme.MONET
} else {
AppTheme.DEFAULT
},
)
fun themeDarkAmoled() = preferenceStore.getBoolean("pref_theme_dark_amoled_key", false)
fun relativeTime() = preferenceStore.getBoolean("relative_time_v2", true)
fun dateFormat() = preferenceStore.getString("app_date_format", "")
fun tabletUiMode() = preferenceStore.getEnum("tablet_ui_mode", TabletUiMode.AUTOMATIC)
// SY -->
fun expandFilters() = preferenceStore.getBoolean("eh_expand_filters", false)
fun hideFeedTab() = preferenceStore.getBoolean("hide_latest_tab", false)
fun feedTabInFront() = preferenceStore.getBoolean("latest_tab_position", false)
fun recommendsInOverflow() = preferenceStore.getBoolean("recommends_in_overflow", false)
fun mergeInOverflow() = preferenceStore.getBoolean("merge_in_overflow", true)
fun previewsRowCount() = preferenceStore.getInt("pref_previews_row_count", 4)
fun useNewSourceNavigation() = preferenceStore.getBoolean("use_new_source_navigation", true)
fun bottomBarLabels() = preferenceStore.getBoolean("pref_show_bottom_bar_labels", true)
fun showNavUpdates() = preferenceStore.getBoolean("pref_show_updates_button", true)
fun showNavHistory() = preferenceStore.getBoolean("pref_show_history_button", true)
// SY <--
companion object {
fun dateFormat(format: String): DateTimeFormatter = when (format) {
"" -> DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT)
else -> DateTimeFormatter.ofPattern(format, Locale.getDefault())
}
}
}

View File

@ -1,29 +0,0 @@
package eu.kanade.domain.ui.model
import dev.icerock.moko.resources.StringResource
import tachiyomi.i18n.MR
enum class AppTheme(val titleRes: StringResource?) {
DEFAULT(MR.strings.label_default),
MONET(MR.strings.theme_monet),
GREEN_APPLE(MR.strings.theme_greenapple),
LAVENDER(MR.strings.theme_lavender),
MIDNIGHT_DUSK(MR.strings.theme_midnightdusk),
NORD(MR.strings.theme_nord),
STRAWBERRY_DAIQUIRI(MR.strings.theme_strawberrydaiquiri),
TAKO(MR.strings.theme_tako),
TEALTURQUOISE(MR.strings.theme_tealturquoise),
TIDAL_WAVE(MR.strings.theme_tidalwave),
YINYANG(MR.strings.theme_yinyang),
YOTSUBA(MR.strings.theme_yotsuba),
MONOCHROME(MR.strings.theme_monochrome),
// Deprecated
DARK_BLUE(null),
HOT_PINK(null),
BLUE(null),
// SY -->
PURE_RED(null),
// SY <--
}

View File

@ -1,11 +0,0 @@
package eu.kanade.domain.ui.model
import dev.icerock.moko.resources.StringResource
import tachiyomi.i18n.MR
enum class TabletUiMode(val titleRes: StringResource) {
AUTOMATIC(MR.strings.automatic_background),
ALWAYS(MR.strings.lock_always),
LANDSCAPE(MR.strings.landscape),
NEVER(MR.strings.lock_never),
}

View File

@ -1,19 +0,0 @@
package eu.kanade.domain.ui.model
import androidx.appcompat.app.AppCompatDelegate
enum class ThemeMode {
LIGHT,
DARK,
SYSTEM,
}
fun setAppCompatDelegateThemeMode(themeMode: ThemeMode) {
AppCompatDelegate.setDefaultNightMode(
when (themeMode) {
ThemeMode.LIGHT -> AppCompatDelegate.MODE_NIGHT_NO
ThemeMode.DARK -> AppCompatDelegate.MODE_NIGHT_YES
ThemeMode.SYSTEM -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
},
)
}

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