Compare commits
No commits in common. "master" and "1.8.3" have entirely different histories.
@ -1,40 +1,5 @@
|
|||||||
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}]
|
[*.{kt,kts}]
|
||||||
indent_size = 4
|
indent_size=4
|
||||||
max_line_length = 120
|
insert_final_newline=true
|
||||||
|
ij_kotlin_allow_trailing_comma=true
|
||||||
ij_kotlin_allow_trailing_comma = true
|
ij_kotlin_allow_trailing_comma_on_call_site=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
1
.github/FUNDING.yml
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
ko_fi: inorichi
|
||||||
34
.github/ISSUE_TEMPLATE.md
vendored
Executable file
34
.github/ISSUE_TEMPLATE.md
vendored
Executable file
@ -0,0 +1,34 @@
|
|||||||
|
**PLEASE READ THIS**
|
||||||
|
|
||||||
|
I acknowledge that:
|
||||||
|
|
||||||
|
- I have updated:
|
||||||
|
- To the latest version of the app (stable is v1.8.3)
|
||||||
|
- 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.
|
||||||
13
.github/ISSUE_TEMPLATE/config.yml
vendored
13
.github/ISSUE_TEMPLATE/config.yml
vendored
@ -1,8 +1,11 @@
|
|||||||
blank_issues_enabled: false
|
blank_issues_enabled: false
|
||||||
contact_links:
|
contact_links:
|
||||||
- name: ❌ Help with Extensions
|
- name: ⚠️ Extension/source issue
|
||||||
url: https://mihon.app/docs/faq/browse/extensions
|
url: https://github.com/tachiyomiorg/tachiyomi-extensions/issues/new/choose
|
||||||
about: For extension-related questions/issues
|
about: Issues and requests for extensions and sources should be opened in the tachiyomi-extensions repository instead
|
||||||
- name: 🖥️ Mihon website
|
- name: 📦 Tachiyomi extensions
|
||||||
url: https://mihon.app/
|
url: https://tachiyomi.org/extensions
|
||||||
|
about: List of all available extensions with download links
|
||||||
|
- name: 🖥️ Tachiyomi website
|
||||||
|
url: https://tachiyomi.org/help/
|
||||||
about: Guides, troubleshooting, and answers to common questions
|
about: Guides, troubleshooting, and answers to common questions
|
||||||
|
|||||||
30
.github/ISSUE_TEMPLATE/report_issue.yml
vendored
30
.github/ISSUE_TEMPLATE/report_issue.yml
vendored
@ -1,5 +1,5 @@
|
|||||||
name: 🐞 Issue report
|
name: 🐞 Issue report
|
||||||
description: Report an issue in TachiyomiSY
|
description: Report an issue in Tachiyomi
|
||||||
labels: [Bug]
|
labels: [Bug]
|
||||||
body:
|
body:
|
||||||
|
|
||||||
@ -43,17 +43,17 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: Crash logs
|
label: Crash logs
|
||||||
description: |
|
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.
|
If you're experiencing crashes, share the crash logs from **More → Settings → Advanced** then press **Dump crash logs**.
|
||||||
placeholder: |
|
placeholder: |
|
||||||
You can upload the crash log file as an attachment, or paste the crash logs in plain text if needed.
|
You can paste the crash logs in pure text or upload it as an attachment.
|
||||||
|
|
||||||
- type: input
|
- type: input
|
||||||
id: tachiyomisy-version
|
id: tachiyomi-version
|
||||||
attributes:
|
attributes:
|
||||||
label: TachiyomiSY version
|
label: Tachiyomi version
|
||||||
description: You can find your TachiyomiSY version in **More → About**.
|
description: You can find your Tachiyomi version in **More → About**.
|
||||||
placeholder: |
|
placeholder: |
|
||||||
Example: "1.12.0"
|
Example: "1.8.3"
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
@ -63,7 +63,7 @@ body:
|
|||||||
label: Android version
|
label: Android version
|
||||||
description: You can find this somewhere in your Android settings.
|
description: You can find this somewhere in your Android settings.
|
||||||
placeholder: |
|
placeholder: |
|
||||||
Example: "Android 14"
|
Example: "Android 11"
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
@ -73,7 +73,7 @@ body:
|
|||||||
label: Device
|
label: Device
|
||||||
description: List your device and model.
|
description: List your device and model.
|
||||||
placeholder: |
|
placeholder: |
|
||||||
Example: "Google Pixel 8"
|
Example: "Google Pixel 5"
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
@ -90,15 +90,17 @@ body:
|
|||||||
label: Acknowledgements
|
label: Acknowledgements
|
||||||
description: Read this carefully, we will close and ignore your issue if you skimmed through this.
|
description: Read this carefully, we will close and ignore your issue if you skimmed through this.
|
||||||
options:
|
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.
|
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open issue.
|
||||||
required: true
|
required: true
|
||||||
- label: I have written a short but informative title.
|
- label: I have written a short but informative title.
|
||||||
required: true
|
required: true
|
||||||
- label: I have gone through the [FAQ](https://mihon.app/docs/faq/general) and [troubleshooting guide](https://mihon.app/docs/guides/troubleshooting/).
|
- label: If this is an issue with an extension, I should be opening an issue in the [extensions repository](https://github.com/tachiyomiorg/tachiyomi-extensions/issues/new/choose).
|
||||||
required: true
|
required: true
|
||||||
- label: I have updated the app to version **[1.12.0](https://github.com/jobobby04/tachiyomisy/releases/latest)**.
|
- label: I have tried the [troubleshooting guide](https://tachiyomi.org/help/guides/troubleshooting/).
|
||||||
required: true
|
required: true
|
||||||
- label: I have filled out all of the requested information in this form, including specific version numbers.
|
- label: I have updated the app to version **[1.8.3](https://github.com/jobobby04/tachiyomisy/releases/latest)**.
|
||||||
required: true
|
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.
|
- label: I have updated all installed extensions.
|
||||||
|
required: true
|
||||||
|
- label: I will fill out all of the requested information in this form.
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
10
.github/ISSUE_TEMPLATE/request_feature.yml
vendored
10
.github/ISSUE_TEMPLATE/request_feature.yml
vendored
@ -1,5 +1,5 @@
|
|||||||
name: ⭐ Feature request
|
name: ⭐ Feature request
|
||||||
description: Suggest a feature to improve TachiyomiSY
|
description: Suggest a feature to improve Tachiyomi
|
||||||
labels: [Feature request]
|
labels: [Feature request]
|
||||||
body:
|
body:
|
||||||
|
|
||||||
@ -7,7 +7,7 @@ body:
|
|||||||
id: feature-description
|
id: feature-description
|
||||||
attributes:
|
attributes:
|
||||||
label: Describe your suggested feature
|
label: Describe your suggested feature
|
||||||
description: How can TachiyomiSY be improved?
|
description: How can Tachiyomi be improved?
|
||||||
placeholder: |
|
placeholder: |
|
||||||
Example:
|
Example:
|
||||||
"It should work like this..."
|
"It should work like this..."
|
||||||
@ -27,11 +27,13 @@ body:
|
|||||||
label: Acknowledgements
|
label: Acknowledgements
|
||||||
description: Read this carefully, we will close and ignore your issue if you skimmed through this.
|
description: Read this carefully, we will close and ignore your issue if you skimmed through this.
|
||||||
options:
|
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.
|
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open issue.
|
||||||
required: true
|
required: true
|
||||||
- label: I have written a short but informative title.
|
- label: I have written a short but informative title.
|
||||||
required: true
|
required: true
|
||||||
- label: I have updated the app to version **[1.12.0](https://github.com/jobobby04/tachiyomisy/releases/latest)**.
|
- label: If this is an issue with an extension, I should be opening an issue in the [extensions repository](https://github.com/tachiyomiorg/tachiyomi-extensions/issues/new/choose).
|
||||||
|
required: true
|
||||||
|
- label: I have updated the app to version **[1.8.3](https://github.com/jobobby04/tachiyomisy/releases/latest)**.
|
||||||
required: true
|
required: true
|
||||||
- label: I will fill out all of the requested information in this form.
|
- label: I will fill out all of the requested information in this form.
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
BIN
.github/readme-images/screens.png
vendored
BIN
.github/readme-images/screens.png
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 713 KiB After Width: | Height: | Size: 489 KiB |
7
.github/renovate.json5
vendored
7
.github/renovate.json5
vendored
@ -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"
|
|
||||||
}
|
|
||||||
5
.github/runner-files/ci-gradle.properties
vendored
Normal file
5
.github/runner-files/ci-gradle.properties
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
org.gradle.daemon=false
|
||||||
|
org.gradle.jvmargs=-Xmx5120m
|
||||||
|
org.gradle.workers.max=2
|
||||||
|
|
||||||
|
kotlin.incremental=false
|
||||||
42
.github/workflows/TachiyomiSY-Preview-Builder.yml
vendored
Normal file
42
.github/workflows/TachiyomiSY-Preview-Builder.yml
vendored
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
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
|
||||||
|
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"'" }}'
|
||||||
87
.github/workflows/TachiyomiSY-Release-Builder.yml
vendored
Normal file
87
.github/workflows/TachiyomiSY-Release-Builder.yml
vendored
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
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
|
||||||
|
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 11
|
||||||
|
uses: actions/setup-java@v1
|
||||||
|
with:
|
||||||
|
java-version: 11
|
||||||
|
|
||||||
|
- 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: gradle/gradle-command-action@v2
|
||||||
|
with:
|
||||||
|
arguments: assembleStandardRelease --stacktrace
|
||||||
|
|
||||||
|
- name: Sign APK
|
||||||
|
uses: r0adkll/sign-android-release@v1
|
||||||
|
with:
|
||||||
|
releaseDirectory: app/build/outputs/apk/standard/release
|
||||||
|
signingKeyBase64: ${{ secrets.SIGNING_KEY }}
|
||||||
|
alias: ${{ secrets.ALIAS }}
|
||||||
|
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
|
||||||
|
keyPassword: ${{ secrets.KEY_PASSWORD }}
|
||||||
|
|
||||||
|
- name: 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
|
||||||
48
.github/workflows/build_check.yml
vendored
48
.github/workflows/build_check.yml
vendored
@ -1,33 +1,49 @@
|
|||||||
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
|
||||||
|
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 11
|
||||||
|
uses: actions/setup-java@v1
|
||||||
|
with:
|
||||||
|
java-version: 11
|
||||||
|
|
||||||
|
- 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: gradle/gradle-command-action@v2
|
||||||
|
with:
|
||||||
|
arguments: assembleDevDebug
|
||||||
|
|
||||||
- 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
|
||||||
121
.github/workflows/build_push.yml
vendored
121
.github/workflows/build_push.yml
vendored
@ -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 }}
|
|
||||||
39
.github/workflows/build_push_preview.yml
vendored
39
.github/workflows/build_push_preview.yml
vendored
@ -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
|
|
||||||
18
.github/workflows/issue_moderator.yml
vendored
18
.github/workflows/issue_moderator.yml
vendored
@ -11,11 +11,9 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Moderate issues
|
- name: Moderate issues
|
||||||
uses: tachiyomiorg/issue-moderator-action@v2.6.1
|
uses: tachiyomiorg/issue-moderator-action@v1
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
duplicate-label: Duplicate
|
|
||||||
|
|
||||||
auto-close-rules: |
|
auto-close-rules: |
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
@ -33,19 +31,5 @@ jobs:
|
|||||||
"regex": "^(?!.*myanimelist.*).*(aniyomi|anime).*$",
|
"regex": "^(?!.*myanimelist.*).*(aniyomi|anime).*$",
|
||||||
"ignoreCase": true,
|
"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"
|
"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
19
.github/workflows/lock.yml
vendored
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
name: Lock threads
|
||||||
|
|
||||||
|
on:
|
||||||
|
# Daily
|
||||||
|
schedule:
|
||||||
|
- cron: '0 0 * * *'
|
||||||
|
# Manual trigger
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lock:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: dessant/lock-threads@v3
|
||||||
|
with:
|
||||||
|
github-token: ${{ github.token }}
|
||||||
|
issue-inactive-days: '2'
|
||||||
|
pr-inactive-days: '2'
|
||||||
26
.github/workflows/pr_label.yml
vendored
26
.github/workflows/pr_label.yml
vendored
@ -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
37
.gitignore
vendored
@ -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
BIN
.idea/icon.png
generated
Binary file not shown.
|
Before Width: | Height: | Size: 14 KiB |
@ -60,7 +60,7 @@ representative at an online or offline event.
|
|||||||
|
|
||||||
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 to the community moderators responsible for enforcement at
|
||||||
the [Mihon Discord server](https://discord.gg/mihon).
|
the [Tachiyomi Discord server](https://discord.gg/tachiyomi).
|
||||||
All complaints will be reviewed and investigated promptly and fairly.
|
All complaints will be reviewed and investigated promptly and fairly.
|
||||||
|
|
||||||
All community moderators are obligated to respect the privacy and security of the
|
All community moderators are obligated to respect the privacy and security of the
|
||||||
|
|||||||
@ -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,7 +9,7 @@ Thanks for your interest in contributing to Tachiyomi!
|
|||||||
|
|
||||||
Pull requests are welcome!
|
Pull requests are welcome!
|
||||||
|
|
||||||
If you're interested in taking on [an open issue](https://github.com/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.
|
You do not need to ask for permission nor an assignment.
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
@ -26,39 +26,25 @@ Before you start, please note that the ability to use following technologies is
|
|||||||
|
|
||||||
## Getting help
|
## Getting help
|
||||||
|
|
||||||
- Join [the Discord server](https://discord.gg/mihon) for online help and to ask questions while developing.
|
- Join [the Discord server](https://discord.gg/tachiyomi) 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/AppUpdateChecker.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/`
|
|
||||||
|
|||||||
46
README.md
46
README.md
@ -1,25 +1,20 @@
|
|||||||
| Preview Builds | Release Builds | Mihon Support Server |
|
| Preview Builds | Release Builds | Tachiyomi Support Server |
|
||||||
|-------|----------|----------|
|
|-------|----------|----------|
|
||||||
| [](https://github.com/jobobby04/TachiyomiSYPreview/releases) | [](https://github.com/jobobby04/tachiyomisy/releases/latest) | [](https://discord.gg/mihon) |
|
| [](https://github.com/jobobby04/TachiyomiSYPreview/releases) | [](https://github.com/jobobby04/tachiyomisy/releases/latest) | [](https://discord.gg/tachiyomi) |
|
||||||
|
|
||||||
|
|
||||||
# TachiyomiSY
|
# 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 6.0 and above. This version of Tachiyomi, TachiyomiSY was based off TachiyomiAZ. This version is meant to push forward in the ways of usability and features. TachiyomiSY tries to push forward where it can, but staying in a place where it can easily grab updates and features from the main app, it tries to make new features, or take features from other forks like J2K and Neko.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
Features of SY-Plus include:
|
Features of Tachiyomi(original) include:
|
||||||
* Automatically trust extensions
|
|
||||||
* Removal of rate limits
|
|
||||||
* Re addition of more frequent library update timers
|
|
||||||
|
|
||||||
Features of Mihon(original) include:
|
|
||||||
* Online reading from a variety of sources
|
* Online reading from a variety of sources
|
||||||
* Local reading of downloaded content
|
* 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
|
* Tracker support: [MyAnimeList](https://myanimelist.net/), [AniList](https://anilist.co/), [Kitsu](https://kitsu.io/), [Shikimori](https://shikimori.one), and [Bangumi](https://bgm.tv/)
|
||||||
* 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,30 @@ 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: [](https://discord.gg/mihon)
|
2. If you are unsure, ask here: [](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)
|
||||||
* 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 +103,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 +118,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
6
app/.gitignore
vendored
Executable file
@ -0,0 +1,6 @@
|
|||||||
|
/build
|
||||||
|
*iml
|
||||||
|
*.iml
|
||||||
|
custom.gradle
|
||||||
|
google-services.json
|
||||||
|
output.json
|
||||||
@ -1,96 +1,63 @@
|
|||||||
@file:Suppress("ChromeOsAbiSupport")
|
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||||
|
|
||||||
import mihon.buildlogic.getBuildTime
|
|
||||||
import mihon.buildlogic.getCommitCount
|
|
||||||
import mihon.buildlogic.getGitSha
|
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("mihon.android.application")
|
id("com.android.application")
|
||||||
id("mihon.android.application.compose")
|
id("com.mikepenz.aboutlibraries.plugin")
|
||||||
|
kotlin("android")
|
||||||
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)
|
|
||||||
id("com.github.ben-manes.versions")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (gradle.startParameter.taskRequests.toString().contains("Standard")) {
|
if (gradle.startParameter.taskRequests.toString().contains("Standard")) {
|
||||||
pluginManager.apply {
|
apply<com.google.gms.googleservices.GoogleServicesPlugin>()
|
||||||
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"
|
compileSdk = AndroidConfig.compileSdk
|
||||||
|
ndkVersion = AndroidConfig.ndk
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "eu.kanade.tachiyomi.sy"
|
applicationId = "eu.kanade.tachiyomi.sy"
|
||||||
|
minSdk = AndroidConfig.minSdk
|
||||||
versionCode = 75
|
targetSdk = AndroidConfig.targetSdk
|
||||||
versionName = "1.12.0"
|
versionCode = 34
|
||||||
|
versionName = "1.8.3"
|
||||||
|
|
||||||
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")
|
||||||
|
|
||||||
ndk {
|
ndk {
|
||||||
abiFilters += supportedAbis
|
abiFilters += setOf("armeabi-v7a", "arm64-v8a", "x86")
|
||||||
}
|
}
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
|
|
||||||
splits {
|
|
||||||
abi {
|
|
||||||
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"))
|
setProguardFiles(listOf(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"))
|
||||||
matchingFallbacks.add("release")
|
|
||||||
}
|
}
|
||||||
named("release") {
|
named("release") {
|
||||||
isMinifyEnabled = true
|
isMinifyEnabled = true
|
||||||
isShrinkResources = true
|
isShrinkResources = true
|
||||||
setProguardFiles(listOf(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"))
|
setProguardFiles(listOf(getDefaultProguardFile("proguard-android-optimize.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,25 +68,22 @@ android {
|
|||||||
dimension = "default"
|
dimension = "default"
|
||||||
}
|
}
|
||||||
create("dev") {
|
create("dev") {
|
||||||
|
resourceConfigurations.addAll(listOf("en", "xxhdpi"))
|
||||||
dimension = "default"
|
dimension = "default"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
packaging {
|
packagingOptions {
|
||||||
resources.excludes.addAll(
|
resources.excludes.addAll(listOf(
|
||||||
listOf(
|
"META-INF/DEPENDENCIES",
|
||||||
"kotlin-tooling-metadata.json",
|
"LICENSE.txt",
|
||||||
"META-INF/DEPENDENCIES",
|
"META-INF/LICENSE",
|
||||||
"LICENSE.txt",
|
"META-INF/LICENSE.txt",
|
||||||
"META-INF/LICENSE",
|
"META-INF/README.md",
|
||||||
"META-INF/**/LICENSE.txt",
|
"META-INF/NOTICE",
|
||||||
"META-INF/*.properties",
|
"META-INF/*.kotlin_module",
|
||||||
"META-INF/**/*.properties",
|
"META-INF/*.version",
|
||||||
"META-INF/README.md",
|
))
|
||||||
"META-INF/NOTICE",
|
|
||||||
"META-INF/*.version",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dependenciesInfo {
|
dependenciesInfo {
|
||||||
@ -128,7 +92,6 @@ android {
|
|||||||
|
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
viewBinding = true
|
viewBinding = true
|
||||||
buildConfig = true
|
|
||||||
|
|
||||||
// Disable some unused things
|
// Disable some unused things
|
||||||
aidl = false
|
aidl = false
|
||||||
@ -137,189 +100,188 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
lint {
|
lint {
|
||||||
|
disable.addAll(listOf("MissingTranslation", "ExtraTranslation"))
|
||||||
abortOnError = false
|
abortOnError = false
|
||||||
checkReleaseBuilds = false
|
checkReleaseBuilds = false
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
kotlin {
|
compileOptions {
|
||||||
compilerOptions {
|
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||||
freeCompilerArgs.addAll(
|
targetCompatibility = JavaVersion.VERSION_1_8
|
||||||
"-opt-in=androidx.compose.animation.ExperimentalAnimationApi",
|
}
|
||||||
"-opt-in=androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi",
|
|
||||||
"-opt-in=androidx.compose.foundation.ExperimentalFoundationApi",
|
kotlinOptions {
|
||||||
"-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi",
|
jvmTarget = JavaVersion.VERSION_1_8.toString()
|
||||||
"-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
|
|
||||||
implementation(compose.activity)
|
|
||||||
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.reflect)
|
||||||
implementation(kotlinx.immutables)
|
|
||||||
|
|
||||||
implementation(platform(kotlinx.coroutines.bom))
|
|
||||||
implementation(kotlinx.bundles.coroutines)
|
implementation(kotlinx.bundles.coroutines)
|
||||||
|
|
||||||
|
// Source models and interfaces from Tachiyomi 1.x
|
||||||
|
implementation(libs.tachiyomi.api)
|
||||||
|
|
||||||
// AndroidX libraries
|
// AndroidX libraries
|
||||||
implementation(androidx.annotation)
|
implementation(androidx.annotation)
|
||||||
implementation(androidx.appcompat)
|
implementation(androidx.appcompat)
|
||||||
implementation(androidx.biometricktx)
|
implementation(androidx.biometricktx)
|
||||||
implementation(androidx.constraintlayout)
|
implementation(androidx.constraintlayout)
|
||||||
|
implementation(androidx.coordinatorlayout)
|
||||||
implementation(androidx.corektx)
|
implementation(androidx.corektx)
|
||||||
implementation(androidx.splashscreen)
|
implementation(androidx.splashscreen)
|
||||||
implementation(androidx.recyclerview)
|
implementation(androidx.recyclerview)
|
||||||
|
implementation(androidx.swiperefreshlayout)
|
||||||
implementation(androidx.viewpager)
|
implementation(androidx.viewpager)
|
||||||
implementation(androidx.profileinstaller)
|
|
||||||
|
|
||||||
implementation(androidx.bundles.lifecycle)
|
implementation(androidx.bundles.lifecycle)
|
||||||
|
|
||||||
// Job scheduling
|
// Job scheduling
|
||||||
implementation(androidx.workmanager)
|
implementation(androidx.bundles.workmanager)
|
||||||
|
|
||||||
// RxJava
|
// RX
|
||||||
implementation(libs.rxjava)
|
implementation(libs.bundles.reactivex)
|
||||||
|
implementation(libs.flowreactivenetwork)
|
||||||
|
|
||||||
// Networking
|
// Network client
|
||||||
implementation(libs.bundles.okhttp)
|
implementation(libs.bundles.okhttp)
|
||||||
implementation(libs.okio)
|
implementation(libs.okio)
|
||||||
implementation(libs.conscrypt.android) // TLS 1.3 support for Android < 10
|
|
||||||
|
|
||||||
// Data serialization (JSON, protobuf, xml)
|
// TLS 1.3 support for Android < 10
|
||||||
|
implementation(libs.conscrypt.android)
|
||||||
|
|
||||||
|
// Data serialization (JSON, protobuf)
|
||||||
implementation(kotlinx.bundles.serialization)
|
implementation(kotlinx.bundles.serialization)
|
||||||
|
|
||||||
|
// JavaScript engine
|
||||||
|
implementation(libs.bundles.js.engine)
|
||||||
|
|
||||||
// HTML parser
|
// HTML parser
|
||||||
implementation(libs.jsoup)
|
implementation(libs.jsoup)
|
||||||
|
|
||||||
// Disk
|
// Disk
|
||||||
implementation(libs.disklrucache)
|
implementation(libs.disklrucache)
|
||||||
implementation(libs.unifile)
|
implementation(libs.unifile)
|
||||||
|
implementation(libs.junrar)
|
||||||
|
|
||||||
|
// Database
|
||||||
|
implementation(libs.bundles.sqlite)
|
||||||
|
implementation("com.github.inorichi.storio:storio-common:8be19de@aar")
|
||||||
|
implementation("com.github.inorichi.storio:storio-sqlite:8be19de@aar")
|
||||||
|
|
||||||
// Preferences
|
// Preferences
|
||||||
implementation(libs.preferencektx)
|
implementation(libs.preferencektx)
|
||||||
|
implementation(libs.flowpreferences)
|
||||||
|
|
||||||
|
// Model View Presenter
|
||||||
|
implementation(libs.bundles.nucleus)
|
||||||
|
|
||||||
// Dependency injection
|
// Dependency injection
|
||||||
implementation(libs.injekt)
|
implementation(libs.injekt.core)
|
||||||
|
|
||||||
// Image loading
|
// Image loading
|
||||||
implementation(platform(libs.coil.bom))
|
|
||||||
implementation(libs.bundles.coil)
|
implementation(libs.bundles.coil)
|
||||||
|
|
||||||
implementation(libs.subsamplingscaleimageview) {
|
implementation(libs.subsamplingscaleimageview) {
|
||||||
exclude(module = "image-decoder")
|
exclude(module = "image-decoder")
|
||||||
}
|
}
|
||||||
implementation(libs.image.decoder)
|
implementation(libs.image.decoder)
|
||||||
|
|
||||||
|
// Sort
|
||||||
|
implementation(libs.natural.comparator)
|
||||||
|
|
||||||
// UI libraries
|
// UI libraries
|
||||||
implementation(libs.material)
|
implementation(libs.material)
|
||||||
|
implementation(libs.androidprocessbutton)
|
||||||
implementation(libs.flexible.adapter.core)
|
implementation(libs.flexible.adapter.core)
|
||||||
|
implementation(libs.flexible.adapter.ui)
|
||||||
|
implementation(libs.viewstatepageradapter)
|
||||||
implementation(libs.photoview)
|
implementation(libs.photoview)
|
||||||
implementation(libs.directionalviewpager) {
|
implementation(libs.directionalviewpager) {
|
||||||
exclude(group = "androidx.viewpager", module = "viewpager")
|
exclude(group = "androidx.viewpager", module = "viewpager")
|
||||||
}
|
}
|
||||||
implementation(libs.insetter)
|
implementation(libs.insetter)
|
||||||
implementation(libs.richeditor.compose)
|
implementation(libs.markwon)
|
||||||
implementation(libs.aboutLibraries.compose)
|
|
||||||
implementation(libs.bundles.voyager)
|
// Conductor
|
||||||
implementation(libs.compose.materialmotion)
|
implementation(libs.bundles.conductor)
|
||||||
implementation(libs.swipe)
|
|
||||||
implementation(libs.compose.webview)
|
// FlowBinding
|
||||||
implementation(libs.compose.grid)
|
implementation(libs.bundles.flowbinding)
|
||||||
implementation(libs.reorderable)
|
|
||||||
implementation(libs.bundles.markdown)
|
|
||||||
|
|
||||||
// Logging
|
// Logging
|
||||||
implementation(libs.logcat)
|
implementation(libs.logcat)
|
||||||
|
|
||||||
// Crash reports/analytics
|
// Crash reports/analytics
|
||||||
// "standardImplementation"(platform(libs.firebase.bom))
|
// implementation(libs.acra.http)
|
||||||
// "standardImplementation"(libs.firebase.analytics)
|
// "standardImplementation"(libs.firebase.analytics)
|
||||||
// "standardImplementation"(libs.firebase.crashlytics)
|
|
||||||
|
// Licenses
|
||||||
|
implementation(libs.aboutlibraries.core)
|
||||||
|
|
||||||
// Shizuku
|
// Shizuku
|
||||||
implementation(libs.bundles.shizuku)
|
implementation(libs.bundles.shizuku)
|
||||||
|
|
||||||
// Tests
|
// Tests
|
||||||
testImplementation(libs.bundles.test)
|
testImplementation(libs.junit)
|
||||||
|
testImplementation(libs.assertj.core)
|
||||||
|
testImplementation(libs.mockito.core)
|
||||||
|
|
||||||
|
testImplementation(libs.bundles.robolectric)
|
||||||
|
|
||||||
// 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(libs.leakcanary.android)
|
||||||
implementation(libs.leakcanary.plumber)
|
|
||||||
|
|
||||||
testImplementation(kotlinx.coroutines.test)
|
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
|
// Changelog
|
||||||
|
implementation(sylibs.changelog)
|
||||||
|
|
||||||
// Text distance (EH)
|
// Text distance (EH)
|
||||||
implementation(sylibs.simularity)
|
implementation (sylibs.simularity)
|
||||||
|
|
||||||
// Firebase (EH)
|
// Firebase (EH)
|
||||||
implementation(platform(libs.firebase.bom))
|
implementation(sylibs.firebase.analytics)
|
||||||
implementation(libs.firebase.analytics)
|
implementation(sylibs.firebase.crashlytics.ktx)
|
||||||
implementation(libs.firebase.crashlytics)
|
|
||||||
|
|
||||||
// Better logging (EH)
|
// Better logging (EH)
|
||||||
implementation(sylibs.xlog)
|
implementation(sylibs.xlog)
|
||||||
|
|
||||||
|
// Debug utils (EH)
|
||||||
|
debugImplementation(sylibs.debugOverlay.standard)
|
||||||
|
"releaseTestImplementation"(sylibs.debugOverlay.noop)
|
||||||
|
releaseImplementation(sylibs.debugOverlay.noop)
|
||||||
|
testImplementation(sylibs.debugOverlay.noop)
|
||||||
|
|
||||||
// RatingBar (SY)
|
// RatingBar (SY)
|
||||||
implementation(sylibs.ratingbar)
|
implementation(sylibs.ratingbar)
|
||||||
implementation(sylibs.composeRatingbar)
|
|
||||||
|
|
||||||
// Google drive
|
|
||||||
implementation(sylibs.google.api.services.drive)
|
|
||||||
implementation(sylibs.google.api.client.oauth)
|
|
||||||
|
|
||||||
// Koin
|
|
||||||
implementation(sylibs.koin.core)
|
|
||||||
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",
|
||||||
|
"-Xopt-in=kotlin.ExperimentalStdlibApi",
|
||||||
|
"-Xopt-in=kotlinx.coroutines.FlowPreview",
|
||||||
|
"-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
||||||
|
"-Xopt-in=kotlinx.coroutines.InternalCoroutinesApi",
|
||||||
|
"-Xopt-in=kotlinx.serialization.ExperimentalSerializationApi",
|
||||||
|
"-Xopt-in=coil.annotation.ExperimentalCoilApi",
|
||||||
|
"-Xopt-in=kotlin.time.ExperimentalTime",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
-allowaccessmodification
|
-allowaccessmodification
|
||||||
-dontusemixedcaseclassnames
|
-dontusemixedcaseclassnames
|
||||||
-ignorewarnings
|
|
||||||
-verbose
|
-verbose
|
||||||
|
|
||||||
-keepattributes *Annotation*
|
-keepattributes *Annotation*
|
||||||
@ -15,7 +14,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
-keepclassmembers class * implements android.os.Parcelable {
|
-keepclassmembers class * implements android.os.Parcelable {
|
||||||
public static final ** CREATOR;
|
public static final ** CREATOR;
|
||||||
}
|
}
|
||||||
|
|
||||||
-keep class androidx.annotation.Keep
|
-keep class androidx.annotation.Keep
|
||||||
|
|||||||
188
app/proguard-rules.pro
vendored
188
app/proguard-rules.pro
vendored
@ -1,59 +1,38 @@
|
|||||||
-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 sun.misc.**
|
-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$* { *; }
|
||||||
|
|
||||||
-keepclassmembers class rx.internal.util.unsafe.*ArrayQueue*Field* {
|
-keep class com.hippo.image.** { *; }
|
||||||
long producerIndex;
|
-keep interface com.hippo.image.** { *; }
|
||||||
long consumerIndex;
|
-keepclassmembers class * extends nucleus.presenter.Presenter {
|
||||||
|
<init>();
|
||||||
}
|
}
|
||||||
|
|
||||||
-keepclassmembers class rx.internal.util.unsafe.BaseLinkedQueueProducerNodeRef {
|
# Kotlin Serialization
|
||||||
rx.internal.util.atomic.LinkedQueueNode producerNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
-keepclassmembers class rx.internal.util.unsafe.BaseLinkedQueueConsumerNodeRef {
|
|
||||||
rx.internal.util.atomic.LinkedQueueNode consumerNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
-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
|
-keepattributes *Annotation*, InnerClasses
|
||||||
-dontnote kotlinx.serialization.** # core serialization annotations
|
-dontnote kotlinx.serialization.AnnotationsKt # core serialization annotations
|
||||||
|
|
||||||
# kotlinx-serialization-json specific. Add this if you have java.lang.NoClassDefFoundError kotlinx.serialization.json.JsonObjectSerializer
|
# kotlinx-serialization-json specific. Add this if you have java.lang.NoClassDefFoundError kotlinx.serialization.json.JsonObjectSerializer
|
||||||
-keepclassmembers class kotlinx.serialization.json.** {
|
-keepclassmembers class kotlinx.serialization.json.** {
|
||||||
@ -63,19 +42,11 @@
|
|||||||
kotlinx.serialization.KSerializer serializer(...);
|
kotlinx.serialization.KSerializer serializer(...);
|
||||||
}
|
}
|
||||||
|
|
||||||
-keep,includedescriptorclasses class eu.kanade.**$$serializer { *; }
|
-keep,includedescriptorclasses class eu.kanade.tachiyomi.**$$serializer { *; }
|
||||||
-keepclassmembers class eu.kanade.** {
|
-keepclassmembers class eu.kanade.tachiyomi.** {
|
||||||
*** Companion;
|
*** Companion;
|
||||||
}
|
}
|
||||||
-keepclasseswithmembers class eu.kanade.** {
|
-keepclasseswithmembers class eu.kanade.tachiyomi.** {
|
||||||
kotlinx.serialization.KSerializer serializer(...);
|
|
||||||
}
|
|
||||||
|
|
||||||
-keep,includedescriptorclasses class tachiyomi.**$$serializer { *; }
|
|
||||||
-keepclassmembers class tachiyomi.** {
|
|
||||||
*** Companion;
|
|
||||||
}
|
|
||||||
-keepclasseswithmembers class tachiyomi.** {
|
|
||||||
kotlinx.serialization.KSerializer serializer(...);
|
kotlinx.serialization.KSerializer serializer(...);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -96,11 +67,26 @@
|
|||||||
kotlinx.serialization.KSerializer serializer(...);
|
kotlinx.serialization.KSerializer serializer(...);
|
||||||
}
|
}
|
||||||
|
|
||||||
-keep class kotlinx.serialization.**
|
# Keep extension's common dependencies
|
||||||
-keepclassmembers class kotlinx.serialization.** {
|
-keep,allowoptimization class eu.kanade.tachiyomi.** { public protected *; }
|
||||||
<methods>;
|
-keep,allowoptimization class androidx.preference.** { *; }
|
||||||
}
|
-keep,allowoptimization class kotlin.** { public protected *; }
|
||||||
##---------------End: proguard configuration for kotlinx.serialization ----------
|
-keep,allowoptimization class kotlinx.coroutines.** { public protected *; }
|
||||||
|
-keep,allowoptimization class okhttp3.** { public protected *; }
|
||||||
|
-keep,allowoptimization class okio.** { public protected *; }
|
||||||
|
-keep,allowoptimization class rx.** { public protected *; }
|
||||||
|
-keep,allowoptimization class org.jsoup.** { public protected *; }
|
||||||
|
-keep,allowoptimization class com.google.gson.** { public protected *; }
|
||||||
|
-keep,allowoptimization class com.github.salomonbrys.kotson.** { public protected *; }
|
||||||
|
-keep,allowoptimization class com.squareup.duktape.** { public protected *; }
|
||||||
|
-keep,allowoptimization class app.cash.quickjs.** { public protected *; }
|
||||||
|
-keep,allowoptimization class uy.kohesive.injekt.** { public protected *; }
|
||||||
|
-keep,allowoptimization class kotlinx.serialization.** { public protected *; }
|
||||||
|
|
||||||
|
# RxJava 1.1.0
|
||||||
|
-dontwarn sun.misc.**
|
||||||
|
|
||||||
|
-dontnote rx.internal.util.PlatformDependent
|
||||||
|
|
||||||
# === Reactive network: https://github.com/pwittchen/ReactiveNetwork/tree/v0.12.4#proguard-configuration
|
# === Reactive network: https://github.com/pwittchen/ReactiveNetwork/tree/v0.12.4#proguard-configuration
|
||||||
-dontwarn com.github.pwittchen.reactivenetwork.library.rx2.ReactiveNetwork
|
-dontwarn com.github.pwittchen.reactivenetwork.library.rx2.ReactiveNetwork
|
||||||
@ -121,44 +107,23 @@
|
|||||||
# === 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
|
# == Nucleus
|
||||||
-keep class rx.android.** { *; }
|
-keepclassmembers class * extends nucleus.presenter.Presenter {
|
||||||
|
<init>();
|
||||||
|
}
|
||||||
|
|
||||||
# XmlUtil
|
# TODO Changeloglib? Does it need proguard?
|
||||||
-keep public enum nl.adaptivity.xmlutil.EventType { *; }
|
|
||||||
|
|
||||||
# Firebase
|
|
||||||
-keep class com.google.firebase.installations.** { *; }
|
|
||||||
-keep interface com.google.firebase.installations.** { *; }
|
|
||||||
|
|
||||||
# Google Drive
|
|
||||||
-keep class com.google.api.services.** { *; }
|
|
||||||
|
|
||||||
# Google OAuth
|
|
||||||
-keep class com.google.api.client.** { *; }
|
|
||||||
|
|
||||||
# SY -->
|
|
||||||
# SqlCipher
|
|
||||||
-keepclassmembers class net.zetetic.database.sqlcipher.SQLiteCustomFunction { *; }
|
|
||||||
-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
|
|
||||||
-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.** { *; }
|
|
||||||
|
|
||||||
# === 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.** { *; }
|
||||||
|
|
||||||
|
# === 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 +229,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
|
|
||||||
@ -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
|
||||||
@ -20,7 +20,6 @@
|
|||||||
android:shortcutShortLabel="@string/label_recent_updates">
|
android:shortcutShortLabel="@string/label_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>
|
||||||
@ -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>
|
|
||||||
@ -3,4 +3,4 @@
|
|||||||
<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" />
|
<monochrome android:drawable="@drawable/ic_tachi_monochrome_launcher" />
|
||||||
</adaptive-icon>
|
</adaptive-icon>
|
||||||
@ -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
|
|
||||||
}
|
|
||||||
@ -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" />
|
||||||
@ -23,116 +21,41 @@
|
|||||||
<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" />
|
<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="false"
|
||||||
android:enableOnBackInvokedCallback="true"
|
|
||||||
android:hardwareAccelerated="true"
|
android:hardwareAccelerated="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:theme="@style/Theme.Tachiyomi"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
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.Tachiyomi.SplashScreen"
|
||||||
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</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"
|
||||||
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.SEARCH" />
|
<action android:name="android.intent.action.SEARCH" />
|
||||||
<action android:name="com.google.android.gms.actions.SEARCH_ACTION" />
|
<action android:name="com.google.android.gms.actions.SEARCH_ACTION" />
|
||||||
@ -156,21 +79,20 @@
|
|||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.reader.ReaderActivity"
|
android:name=".ui.reader.ReaderActivity"
|
||||||
android:exported="false"
|
android:launchMode="singleTask"
|
||||||
android:launchMode="singleTask">
|
android:exported="false">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="com.samsung.android.support.REMOTE_ACTION" />
|
<action android:name="com.samsung.android.support.REMOTE_ACTION" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
<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.UnlockActivity"
|
||||||
android:exported="false"
|
android:theme="@style/Theme.Tachiyomi"
|
||||||
android:theme="@style/Theme.Tachiyomi" />
|
android:exported="false" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.webview.WebViewActivity"
|
android:name=".ui.webview.WebViewActivity"
|
||||||
@ -179,30 +101,12 @@
|
|||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".extension.util.ExtensionInstallActivity"
|
android:name=".extension.util.ExtensionInstallActivity"
|
||||||
android:exported="false"
|
android:theme="@android:style/Theme.Translucent.NoTitleBar"
|
||||||
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
|
android:exported="false" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.setting.track.TrackLoginActivity"
|
android:name=".ui.setting.track.AnilistLoginActivity"
|
||||||
android:exported="true"
|
android:label="Anilist"
|
||||||
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">
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
@ -211,7 +115,53 @@
|
|||||||
<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"
|
||||||
|
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
|
||||||
|
android:host="myanimelist-auth"
|
||||||
|
android:scheme="tachiyomi" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
<activity
|
||||||
|
android:name=".ui.setting.track.ShikimoriLoginActivity"
|
||||||
|
android:label="Shikimori"
|
||||||
|
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
|
||||||
|
android:host="shikimori-auth"
|
||||||
|
android:scheme="tachiyomi" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
<activity
|
||||||
|
android:name=".ui.setting.track.BangumiLoginActivity"
|
||||||
|
android:label="Bangumi"
|
||||||
|
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
|
||||||
|
android:host="bangumi-auth"
|
||||||
|
android:scheme="tachiyomi" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
@ -225,23 +175,23 @@
|
|||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".extension.util.ExtensionInstallService"
|
android:name=".data.library.LibraryUpdateService"
|
||||||
android:exported="false"
|
android:exported="false" />
|
||||||
android:foregroundServiceType="shortService" />
|
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
|
android:name=".data.download.DownloadService"
|
||||||
android:enabled="false"
|
android:exported="false" />
|
||||||
android:exported="false">
|
|
||||||
<meta-data
|
|
||||||
android:name="autoStoreLocales"
|
|
||||||
android:value="true" />
|
|
||||||
</service>
|
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name="androidx.work.impl.foreground.SystemForegroundService"
|
android:name=".data.updater.AppUpdateService"
|
||||||
android:foregroundServiceType="dataSync"
|
android:exported="false" />
|
||||||
tools:node="merge" />
|
|
||||||
|
<service
|
||||||
|
android:name=".data.backup.BackupRestoreService"
|
||||||
|
android:exported="false" />
|
||||||
|
|
||||||
|
<service android:name=".extension.util.ExtensionInstallService"
|
||||||
|
android:exported="false" />
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
android:name="androidx.core.content.FileProvider"
|
android:name="androidx.core.content.FileProvider"
|
||||||
@ -256,31 +206,16 @@
|
|||||||
<provider
|
<provider
|
||||||
android:name="rikka.shizuku.ShizukuProvider"
|
android:name="rikka.shizuku.ShizukuProvider"
|
||||||
android:authorities="${applicationId}.shizuku"
|
android:authorities="${applicationId}.shizuku"
|
||||||
|
android:multiprocess="false"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:multiprocess="false"
|
|
||||||
android:permission="android.permission.INTERACT_ACROSS_USERS_FULL" />
|
android:permission="android.permission.INTERACT_ACROSS_USERS_FULL" />
|
||||||
|
|
||||||
<meta-data
|
<meta-data android:name="android.webkit.WebView.EnableSafeBrowsing"
|
||||||
android:name="android.webkit.WebView.EnableSafeBrowsing"
|
|
||||||
android:value="false" />
|
android:value="false" />
|
||||||
<meta-data
|
<meta-data android:name="android.webkit.WebView.MetricsOptOut"
|
||||||
android:name="android.webkit.WebView.MetricsOptOut"
|
|
||||||
android:value="true" />
|
android:value="true" />
|
||||||
|
|
||||||
<!-- Disable for manual opt-in -->
|
|
||||||
<meta-data
|
|
||||||
android:name="firebase_analytics_collection_enabled"
|
|
||||||
android:value="false" />
|
|
||||||
<meta-data
|
|
||||||
android:name="firebase_crashlytics_collection_enabled"
|
|
||||||
android:value="false" />
|
|
||||||
|
|
||||||
<!-- Disable advertising ID collection for Firebase -->
|
|
||||||
<meta-data
|
|
||||||
android:name="google_analytics_adid_collection_enabled"
|
|
||||||
android:value="false" />
|
|
||||||
|
|
||||||
<!-- EH -->
|
<!-- EH -->
|
||||||
<activity
|
<activity
|
||||||
android:name="exh.ui.intercept.InterceptActivity"
|
android:name="exh.ui.intercept.InterceptActivity"
|
||||||
@ -333,6 +268,21 @@
|
|||||||
|
|
||||||
<data android:pathPattern="/g/..*" />
|
<data android:pathPattern="/g/..*" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
<!-- Perv Eden -->
|
||||||
|
<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="perveden.com" />
|
||||||
|
<data android:host="www.perveden.com" />
|
||||||
|
|
||||||
|
<data android:pathPattern="/.*/.*-manga/.*" />
|
||||||
|
</intent-filter>
|
||||||
<!-- Tsumino -->
|
<!-- Tsumino -->
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
@ -349,6 +299,22 @@
|
|||||||
<data android:pathPattern="/Read/View/..*" />
|
<data android:pathPattern="/Read/View/..*" />
|
||||||
<data android:pathPattern="/Book/Info/..*" />
|
<data android:pathPattern="/Book/Info/..*" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
<!-- Hitomi.la -->
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
|
<data android:scheme="https" />
|
||||||
|
<data android:scheme="http" />
|
||||||
|
|
||||||
|
<data android:host="hitomi.la" />
|
||||||
|
<data android:host="www.hitomi.la" />
|
||||||
|
|
||||||
|
<data android:pathPattern="/reader/..*" />
|
||||||
|
<data android:pathPattern="/galleries/..*" />
|
||||||
|
</intent-filter>
|
||||||
<!-- Pururin -->
|
<!-- Pururin -->
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
@ -359,7 +325,7 @@
|
|||||||
<data android:scheme="https" />
|
<data android:scheme="https" />
|
||||||
<data android:scheme="http" />
|
<data android:scheme="http" />
|
||||||
|
|
||||||
<data android:host="pururin.me" />
|
<data android:host="pururin.io" />
|
||||||
|
|
||||||
<data android:pathPattern="/gallery/..*" />
|
<data android:pathPattern="/gallery/..*" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
@ -397,29 +363,10 @@
|
|||||||
<data android:pathPattern="/chapter/..*" />
|
<data android:pathPattern="/chapter/..*" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name="exh.md.MangaDexLoginActivity"
|
android:name="exh.ui.captcha.BrowserActionActivity"
|
||||||
android:label="MangaDexLogin"
|
android:theme="@style/Theme.Tachiyomi"
|
||||||
android:exported="true">
|
android:exported="false"/>
|
||||||
<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="mangadex-auth"
|
|
||||||
android:scheme="tachiyomisy" />
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
|
||||||
|
|
||||||
<activity
|
|
||||||
android:name="com.journeyapps.barcodescanner.CaptureActivity"
|
|
||||||
tools:remove="screenOrientation" />
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
<uses-sdk tools:overrideLibrary="rikka.shizuku.api"
|
|
||||||
tools:ignore="ManifestOrder" />
|
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -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
|
|
||||||
}
|
|
||||||
@ -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)
|
|
||||||
@ -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
|
|
||||||
}
|
|
||||||
@ -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()) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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()) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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)
|
|
||||||
}
|
|
||||||
@ -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)
|
|
||||||
}
|
|
||||||
@ -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 <--
|
|
||||||
}
|
|
||||||
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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 }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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
|
|
||||||
}
|
|
||||||
@ -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) }
|
|
||||||
}
|
|
||||||
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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,
|
|
||||||
)
|
|
||||||
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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>,
|
|
||||||
)
|
|
||||||
@ -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()}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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 }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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,
|
|
||||||
)
|
|
||||||
@ -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)
|
|
||||||
}
|
|
||||||
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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()
|
|
||||||
}
|
|
||||||
@ -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 <--
|
|
||||||
}
|
|
||||||
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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 <--
|
|
||||||
)
|
|
||||||
@ -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" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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),
|
|
||||||
}
|
|
||||||
@ -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,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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 <--
|
|
||||||
}
|
|
||||||
@ -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,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -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())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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 <--
|
|
||||||
}
|
|
||||||
@ -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),
|
|
||||||
}
|
|
||||||
@ -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
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,200 +0,0 @@
|
|||||||
package eu.kanade.presentation.browse
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.lazy.grid.GridCells
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.automirrored.outlined.HelpOutline
|
|
||||||
import androidx.compose.material.icons.outlined.Public
|
|
||||||
import androidx.compose.material.icons.outlined.Refresh
|
|
||||||
import androidx.compose.material3.SnackbarDuration
|
|
||||||
import androidx.compose.material3.SnackbarHostState
|
|
||||||
import androidx.compose.material3.SnackbarResult
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.paging.LoadState
|
|
||||||
import androidx.paging.compose.LazyPagingItems
|
|
||||||
import eu.kanade.presentation.browse.components.BrowseSourceComfortableGrid
|
|
||||||
import eu.kanade.presentation.browse.components.BrowseSourceCompactGrid
|
|
||||||
import eu.kanade.presentation.browse.components.BrowseSourceEHentaiList
|
|
||||||
import eu.kanade.presentation.browse.components.BrowseSourceList
|
|
||||||
import eu.kanade.presentation.components.AppBar
|
|
||||||
import eu.kanade.presentation.util.formattedMessage
|
|
||||||
import eu.kanade.tachiyomi.source.Source
|
|
||||||
import exh.metadata.metadata.RaisedSearchMetadata
|
|
||||||
import exh.source.isEhBasedSource
|
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
|
||||||
import tachiyomi.core.common.i18n.stringResource
|
|
||||||
import tachiyomi.domain.library.model.LibraryDisplayMode
|
|
||||||
import tachiyomi.domain.manga.model.Manga
|
|
||||||
import tachiyomi.domain.source.model.StubSource
|
|
||||||
import tachiyomi.i18n.MR
|
|
||||||
import tachiyomi.presentation.core.components.material.Scaffold
|
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
|
||||||
import tachiyomi.presentation.core.screens.EmptyScreen
|
|
||||||
import tachiyomi.presentation.core.screens.EmptyScreenAction
|
|
||||||
import tachiyomi.presentation.core.screens.LoadingScreen
|
|
||||||
import tachiyomi.source.local.LocalSource
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun BrowseSourceContent(
|
|
||||||
source: Source?,
|
|
||||||
mangaList: LazyPagingItems<StateFlow</* SY --> */Pair<Manga, RaisedSearchMetadata?>/* SY <-- */>>,
|
|
||||||
columns: GridCells,
|
|
||||||
// SY -->
|
|
||||||
ehentaiBrowseDisplayMode: Boolean,
|
|
||||||
// SY <--
|
|
||||||
displayMode: LibraryDisplayMode,
|
|
||||||
snackbarHostState: SnackbarHostState,
|
|
||||||
contentPadding: PaddingValues,
|
|
||||||
// SY -->
|
|
||||||
onWebViewClick: (() -> Unit)?,
|
|
||||||
onHelpClick: (() -> Unit)?,
|
|
||||||
onLocalSourceHelpClick: (() -> Unit)?,
|
|
||||||
// SY <--
|
|
||||||
onMangaClick: (Manga) -> Unit,
|
|
||||||
onMangaLongClick: (Manga) -> Unit,
|
|
||||||
) {
|
|
||||||
val context = LocalContext.current
|
|
||||||
|
|
||||||
val errorState = mangaList.loadState.refresh.takeIf { it is LoadState.Error }
|
|
||||||
?: mangaList.loadState.append.takeIf { it is LoadState.Error }
|
|
||||||
|
|
||||||
val getErrorMessage: (LoadState.Error) -> String = { state ->
|
|
||||||
with(context) { state.error.formattedMessage }
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(errorState) {
|
|
||||||
if (mangaList.itemCount > 0 && errorState != null && errorState is LoadState.Error) {
|
|
||||||
val result = snackbarHostState.showSnackbar(
|
|
||||||
message = getErrorMessage(errorState),
|
|
||||||
actionLabel = context.stringResource(MR.strings.action_retry),
|
|
||||||
duration = SnackbarDuration.Indefinite,
|
|
||||||
)
|
|
||||||
when (result) {
|
|
||||||
SnackbarResult.Dismissed -> snackbarHostState.currentSnackbarData?.dismiss()
|
|
||||||
SnackbarResult.ActionPerformed -> mangaList.retry()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mangaList.itemCount == 0 && mangaList.loadState.refresh is LoadState.Loading) {
|
|
||||||
LoadingScreen(Modifier.padding(contentPadding))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mangaList.itemCount == 0) {
|
|
||||||
EmptyScreen(
|
|
||||||
modifier = Modifier.padding(contentPadding),
|
|
||||||
message = when (errorState) {
|
|
||||||
is LoadState.Error -> getErrorMessage(errorState)
|
|
||||||
else -> stringResource(MR.strings.no_results_found)
|
|
||||||
},
|
|
||||||
actions = if (source is LocalSource /* SY --> */ && onLocalSourceHelpClick != null /* SY <-- */) {
|
|
||||||
persistentListOf(
|
|
||||||
EmptyScreenAction(
|
|
||||||
stringRes = MR.strings.local_source_help_guide,
|
|
||||||
icon = Icons.AutoMirrored.Outlined.HelpOutline,
|
|
||||||
onClick = onLocalSourceHelpClick,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
listOfNotNull(
|
|
||||||
EmptyScreenAction(
|
|
||||||
stringRes = MR.strings.action_retry,
|
|
||||||
icon = Icons.Outlined.Refresh,
|
|
||||||
onClick = mangaList::refresh,
|
|
||||||
),
|
|
||||||
// SY -->
|
|
||||||
if (onWebViewClick != null) {
|
|
||||||
EmptyScreenAction(
|
|
||||||
stringRes = MR.strings.action_open_in_web_view,
|
|
||||||
icon = Icons.Outlined.Public,
|
|
||||||
onClick = onWebViewClick,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
},
|
|
||||||
if (onHelpClick != null) {
|
|
||||||
EmptyScreenAction(
|
|
||||||
stringRes = MR.strings.label_help,
|
|
||||||
icon = Icons.AutoMirrored.Outlined.HelpOutline,
|
|
||||||
onClick = onHelpClick,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
},
|
|
||||||
// SY <--
|
|
||||||
).toImmutableList()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// SY -->
|
|
||||||
if (source?.isEhBasedSource() == true && ehentaiBrowseDisplayMode) {
|
|
||||||
BrowseSourceEHentaiList(
|
|
||||||
mangaList = mangaList,
|
|
||||||
contentPadding = contentPadding,
|
|
||||||
onMangaClick = onMangaClick,
|
|
||||||
onMangaLongClick = onMangaLongClick,
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// SY <--
|
|
||||||
|
|
||||||
when (displayMode) {
|
|
||||||
LibraryDisplayMode.ComfortableGrid -> {
|
|
||||||
BrowseSourceComfortableGrid(
|
|
||||||
mangaList = mangaList,
|
|
||||||
columns = columns,
|
|
||||||
contentPadding = contentPadding,
|
|
||||||
onMangaClick = onMangaClick,
|
|
||||||
onMangaLongClick = onMangaLongClick,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
LibraryDisplayMode.List -> {
|
|
||||||
BrowseSourceList(
|
|
||||||
mangaList = mangaList,
|
|
||||||
contentPadding = contentPadding,
|
|
||||||
onMangaClick = onMangaClick,
|
|
||||||
onMangaLongClick = onMangaLongClick,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
LibraryDisplayMode.CompactGrid, LibraryDisplayMode.CoverOnlyGrid -> {
|
|
||||||
BrowseSourceCompactGrid(
|
|
||||||
mangaList = mangaList,
|
|
||||||
columns = columns,
|
|
||||||
contentPadding = contentPadding,
|
|
||||||
onMangaClick = onMangaClick,
|
|
||||||
onMangaLongClick = onMangaLongClick,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
internal fun MissingSourceScreen(
|
|
||||||
source: StubSource,
|
|
||||||
navigateUp: () -> Unit,
|
|
||||||
) {
|
|
||||||
Scaffold(
|
|
||||||
topBar = { scrollBehavior ->
|
|
||||||
AppBar(
|
|
||||||
title = source.name,
|
|
||||||
navigateUp = navigateUp,
|
|
||||||
scrollBehavior = scrollBehavior,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
) { paddingValues ->
|
|
||||||
EmptyScreen(
|
|
||||||
message = stringResource(MR.strings.source_not_installed, source.toString()),
|
|
||||||
modifier = Modifier.padding(paddingValues),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,31 +0,0 @@
|
|||||||
package eu.kanade.presentation.browse
|
|
||||||
|
|
||||||
import androidx.compose.material3.SnackbarHost
|
|
||||||
import androidx.compose.material3.SnackbarHostState
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import eu.kanade.presentation.components.AppBar
|
|
||||||
import eu.kanade.presentation.components.AppBarActions
|
|
||||||
import eu.kanade.presentation.components.TabContent
|
|
||||||
import tachiyomi.presentation.core.components.material.Scaffold
|
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun BrowseTabWrapper(tab: TabContent, onBackPressed: (() -> Unit)? = null) {
|
|
||||||
val snackbarHostState = remember { SnackbarHostState() }
|
|
||||||
Scaffold(
|
|
||||||
topBar = { scrollBehavior ->
|
|
||||||
AppBar(
|
|
||||||
title = stringResource(tab.titleRes),
|
|
||||||
actions = {
|
|
||||||
AppBarActions(tab.actions)
|
|
||||||
},
|
|
||||||
navigateUp = onBackPressed,
|
|
||||||
scrollBehavior = scrollBehavior,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
|
|
||||||
) { paddingValues ->
|
|
||||||
tab.content(paddingValues, snackbarHostState)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,479 +0,0 @@
|
|||||||
package eu.kanade.presentation.browse
|
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import android.net.Uri
|
|
||||||
import android.provider.Settings
|
|
||||||
import android.util.DisplayMetrics
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.lazy.items
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.automirrored.outlined.Launch
|
|
||||||
import androidx.compose.material.icons.outlined.Settings
|
|
||||||
import androidx.compose.material3.AlertDialog
|
|
||||||
import androidx.compose.material3.Button
|
|
||||||
import androidx.compose.material3.HorizontalDivider
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.OutlinedButton
|
|
||||||
import androidx.compose.material3.Switch
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.material3.TextButton
|
|
||||||
import androidx.compose.material3.VerticalDivider
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.platform.LocalUriHandler
|
|
||||||
import androidx.compose.ui.res.vectorResource
|
|
||||||
import androidx.compose.ui.text.TextStyle
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import eu.kanade.domain.extension.interactor.ExtensionSourceItem
|
|
||||||
import eu.kanade.presentation.browse.components.ExtensionIcon
|
|
||||||
import eu.kanade.presentation.components.AppBar
|
|
||||||
import eu.kanade.presentation.components.AppBarActions
|
|
||||||
import eu.kanade.presentation.components.WarningBanner
|
|
||||||
import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget
|
|
||||||
import eu.kanade.presentation.more.settings.widget.TrailingWidgetBuffer
|
|
||||||
import eu.kanade.tachiyomi.R
|
|
||||||
import eu.kanade.tachiyomi.extension.model.Extension
|
|
||||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
|
||||||
import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionDetailsScreenModel
|
|
||||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
|
||||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
|
||||||
import tachiyomi.i18n.MR
|
|
||||||
import tachiyomi.i18n.sy.SYMR
|
|
||||||
import tachiyomi.presentation.core.components.ScrollbarLazyColumn
|
|
||||||
import tachiyomi.presentation.core.components.material.Scaffold
|
|
||||||
import tachiyomi.presentation.core.components.material.padding
|
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
|
||||||
import tachiyomi.presentation.core.screens.EmptyScreen
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun ExtensionDetailsScreen(
|
|
||||||
navigateUp: () -> Unit,
|
|
||||||
state: ExtensionDetailsScreenModel.State,
|
|
||||||
onClickSourcePreferences: (sourceId: Long) -> Unit,
|
|
||||||
onClickEnableAll: () -> Unit,
|
|
||||||
onClickDisableAll: () -> Unit,
|
|
||||||
onClickClearCookies: () -> Unit,
|
|
||||||
onClickUninstall: () -> Unit,
|
|
||||||
onClickSource: (sourceId: Long) -> Unit,
|
|
||||||
onClickIncognito: (Boolean) -> Unit,
|
|
||||||
) {
|
|
||||||
val uriHandler = LocalUriHandler.current
|
|
||||||
val url = remember(state.extension) {
|
|
||||||
val regex = """https://raw.githubusercontent.com/(.+?)/(.+?)/.+""".toRegex()
|
|
||||||
regex.find(state.extension?.repoUrl.orEmpty())
|
|
||||||
?.let {
|
|
||||||
val (user, repo) = it.destructured
|
|
||||||
"https://github.com/$user/$repo"
|
|
||||||
}
|
|
||||||
?: state.extension?.repoUrl
|
|
||||||
}
|
|
||||||
|
|
||||||
Scaffold(
|
|
||||||
topBar = { scrollBehavior ->
|
|
||||||
AppBar(
|
|
||||||
title = stringResource(MR.strings.label_extension_info),
|
|
||||||
navigateUp = navigateUp,
|
|
||||||
actions = {
|
|
||||||
AppBarActions(
|
|
||||||
actions = persistentListOf<AppBar.AppBarAction>().builder()
|
|
||||||
.apply {
|
|
||||||
if (url != null) {
|
|
||||||
add(
|
|
||||||
AppBar.Action(
|
|
||||||
title = stringResource(MR.strings.action_open_repo),
|
|
||||||
icon = Icons.AutoMirrored.Outlined.Launch,
|
|
||||||
onClick = {
|
|
||||||
uriHandler.openUri(url)
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
addAll(
|
|
||||||
listOf(
|
|
||||||
AppBar.OverflowAction(
|
|
||||||
title = stringResource(MR.strings.action_enable_all),
|
|
||||||
onClick = onClickEnableAll,
|
|
||||||
),
|
|
||||||
AppBar.OverflowAction(
|
|
||||||
title = stringResource(MR.strings.action_disable_all),
|
|
||||||
onClick = onClickDisableAll,
|
|
||||||
),
|
|
||||||
AppBar.OverflowAction(
|
|
||||||
title = stringResource(MR.strings.pref_clear_cookies),
|
|
||||||
onClick = onClickClearCookies,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.build(),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
scrollBehavior = scrollBehavior,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
) { paddingValues ->
|
|
||||||
if (state.extension == null) {
|
|
||||||
EmptyScreen(
|
|
||||||
MR.strings.empty_screen,
|
|
||||||
modifier = Modifier.padding(paddingValues),
|
|
||||||
)
|
|
||||||
return@Scaffold
|
|
||||||
}
|
|
||||||
|
|
||||||
ExtensionDetails(
|
|
||||||
contentPadding = paddingValues,
|
|
||||||
extension = state.extension,
|
|
||||||
sources = state.sources,
|
|
||||||
incognitoMode = state.isIncognito,
|
|
||||||
onClickSourcePreferences = onClickSourcePreferences,
|
|
||||||
onClickUninstall = onClickUninstall,
|
|
||||||
onClickSource = onClickSource,
|
|
||||||
onClickIncognito = onClickIncognito,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun ExtensionDetails(
|
|
||||||
contentPadding: PaddingValues,
|
|
||||||
extension: Extension.Installed,
|
|
||||||
sources: ImmutableList<ExtensionSourceItem>,
|
|
||||||
incognitoMode: Boolean,
|
|
||||||
onClickSourcePreferences: (sourceId: Long) -> Unit,
|
|
||||||
onClickUninstall: () -> Unit,
|
|
||||||
onClickSource: (sourceId: Long) -> Unit,
|
|
||||||
onClickIncognito: (Boolean) -> Unit,
|
|
||||||
) {
|
|
||||||
val context = LocalContext.current
|
|
||||||
var showNsfwWarning by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
ScrollbarLazyColumn(
|
|
||||||
contentPadding = contentPadding,
|
|
||||||
) {
|
|
||||||
// SY -->
|
|
||||||
if (extension.isRedundant) {
|
|
||||||
item {
|
|
||||||
WarningBanner(SYMR.strings.redundant_extension_message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// SY <--
|
|
||||||
if (extension.isObsolete) {
|
|
||||||
item {
|
|
||||||
WarningBanner(MR.strings.obsolete_extension_message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
item {
|
|
||||||
DetailsHeader(
|
|
||||||
extension = extension,
|
|
||||||
extIncognitoMode = incognitoMode,
|
|
||||||
onClickUninstall = onClickUninstall,
|
|
||||||
onClickAppInfo = {
|
|
||||||
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
|
|
||||||
data = Uri.fromParts("package", extension.pkgName, null)
|
|
||||||
context.startActivity(this)
|
|
||||||
}
|
|
||||||
Unit
|
|
||||||
}.takeIf { extension.isShared },
|
|
||||||
onClickAgeRating = {
|
|
||||||
showNsfwWarning = true
|
|
||||||
},
|
|
||||||
onExtIncognitoChange = onClickIncognito,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
items(
|
|
||||||
items = sources,
|
|
||||||
key = { it.source.id },
|
|
||||||
) { source ->
|
|
||||||
SourceSwitchPreference(
|
|
||||||
modifier = Modifier.animateItem(),
|
|
||||||
source = source,
|
|
||||||
onClickSourcePreferences = onClickSourcePreferences,
|
|
||||||
onClickSource = onClickSource,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (showNsfwWarning) {
|
|
||||||
NsfwWarningDialog(
|
|
||||||
onClickConfirm = {
|
|
||||||
showNsfwWarning = false
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun DetailsHeader(
|
|
||||||
extension: Extension,
|
|
||||||
extIncognitoMode: Boolean,
|
|
||||||
onClickAgeRating: () -> Unit,
|
|
||||||
onClickUninstall: () -> Unit,
|
|
||||||
onClickAppInfo: (() -> Unit)?,
|
|
||||||
onExtIncognitoChange: (Boolean) -> Unit,
|
|
||||||
) {
|
|
||||||
val context = LocalContext.current
|
|
||||||
|
|
||||||
Column {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = MaterialTheme.padding.medium)
|
|
||||||
.padding(
|
|
||||||
top = MaterialTheme.padding.medium,
|
|
||||||
bottom = MaterialTheme.padding.small,
|
|
||||||
)
|
|
||||||
.clickable {
|
|
||||||
val extDebugInfo = buildString {
|
|
||||||
append(
|
|
||||||
"""
|
|
||||||
Extension name: ${extension.name} (lang: ${extension.lang}; package: ${extension.pkgName})
|
|
||||||
Extension version: ${extension.versionName} (lib: ${extension.libVersion}; version code: ${extension.versionCode})
|
|
||||||
NSFW: ${extension.isNsfw}
|
|
||||||
""".trimIndent(),
|
|
||||||
)
|
|
||||||
|
|
||||||
if (extension is Extension.Installed) {
|
|
||||||
append("\n\n")
|
|
||||||
append(
|
|
||||||
"""
|
|
||||||
Update available: ${extension.hasUpdate}
|
|
||||||
Obsolete: ${extension.isObsolete}
|
|
||||||
Shared: ${extension.isShared}
|
|
||||||
Repository: ${extension.repoUrl}
|
|
||||||
""".trimIndent(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
context.copyToClipboard("Extension Debug information", extDebugInfo)
|
|
||||||
},
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
) {
|
|
||||||
ExtensionIcon(
|
|
||||||
modifier = Modifier
|
|
||||||
.size(112.dp),
|
|
||||||
extension = extension,
|
|
||||||
density = DisplayMetrics.DENSITY_XXXHIGH,
|
|
||||||
)
|
|
||||||
|
|
||||||
Text(
|
|
||||||
text = extension.name,
|
|
||||||
style = MaterialTheme.typography.headlineSmall,
|
|
||||||
textAlign = TextAlign.Center,
|
|
||||||
)
|
|
||||||
|
|
||||||
val strippedPkgName = extension.pkgName.substringAfter("eu.kanade.tachiyomi.extension.")
|
|
||||||
|
|
||||||
Text(
|
|
||||||
text = strippedPkgName,
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(
|
|
||||||
horizontal = MaterialTheme.padding.extraLarge,
|
|
||||||
vertical = MaterialTheme.padding.small,
|
|
||||||
),
|
|
||||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
) {
|
|
||||||
InfoText(
|
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
primaryText = extension.versionName,
|
|
||||||
secondaryText = stringResource(MR.strings.ext_info_version),
|
|
||||||
)
|
|
||||||
|
|
||||||
InfoDivider()
|
|
||||||
|
|
||||||
InfoText(
|
|
||||||
modifier = Modifier.weight(if (extension.isNsfw) 1.5f else 1f),
|
|
||||||
primaryText = LocaleHelper.getSourceDisplayName(extension.lang, context),
|
|
||||||
secondaryText = stringResource(MR.strings.ext_info_language),
|
|
||||||
)
|
|
||||||
|
|
||||||
if (extension.isNsfw) {
|
|
||||||
InfoDivider()
|
|
||||||
|
|
||||||
InfoText(
|
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
primaryText = stringResource(MR.strings.ext_nsfw_short),
|
|
||||||
primaryTextStyle = MaterialTheme.typography.bodyLarge.copy(
|
|
||||||
color = MaterialTheme.colorScheme.error,
|
|
||||||
fontWeight = FontWeight.Medium,
|
|
||||||
),
|
|
||||||
secondaryText = stringResource(MR.strings.ext_info_age_rating),
|
|
||||||
onClick = onClickAgeRating,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(horizontal = MaterialTheme.padding.medium)
|
|
||||||
.padding(top = MaterialTheme.padding.small),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.medium),
|
|
||||||
) {
|
|
||||||
OutlinedButton(
|
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
onClick = onClickUninstall,
|
|
||||||
) {
|
|
||||||
Text(stringResource(MR.strings.ext_uninstall))
|
|
||||||
}
|
|
||||||
|
|
||||||
if (onClickAppInfo != null) {
|
|
||||||
Button(
|
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
onClick = onClickAppInfo,
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(MR.strings.ext_app_info),
|
|
||||||
color = MaterialTheme.colorScheme.onPrimary,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TextPreferenceWidget(
|
|
||||||
modifier = Modifier.padding(horizontal = MaterialTheme.padding.small),
|
|
||||||
title = stringResource(MR.strings.pref_incognito_mode),
|
|
||||||
subtitle = stringResource(MR.strings.pref_incognito_mode_extension_summary),
|
|
||||||
icon = ImageVector.vectorResource(R.drawable.ic_glasses_24dp),
|
|
||||||
widget = {
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
) {
|
|
||||||
Switch(
|
|
||||||
checked = extIncognitoMode,
|
|
||||||
onCheckedChange = onExtIncognitoChange,
|
|
||||||
modifier = Modifier.padding(start = TrailingWidgetBuffer),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
HorizontalDivider()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun InfoText(
|
|
||||||
primaryText: String,
|
|
||||||
secondaryText: String,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
primaryTextStyle: TextStyle = MaterialTheme.typography.bodyLarge,
|
|
||||||
onClick: (() -> Unit)? = null,
|
|
||||||
) {
|
|
||||||
val clickableModifier = if (onClick != null) {
|
|
||||||
Modifier.clickable(interactionSource = null, indication = null, onClick = onClick)
|
|
||||||
} else {
|
|
||||||
Modifier
|
|
||||||
}
|
|
||||||
|
|
||||||
Column(
|
|
||||||
modifier = modifier.then(clickableModifier),
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
verticalArrangement = Arrangement.Center,
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = primaryText,
|
|
||||||
textAlign = TextAlign.Center,
|
|
||||||
style = primaryTextStyle,
|
|
||||||
)
|
|
||||||
|
|
||||||
Text(
|
|
||||||
text = secondaryText + if (onClick != null) " ⓘ" else "",
|
|
||||||
textAlign = TextAlign.Center,
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun InfoDivider() {
|
|
||||||
VerticalDivider(
|
|
||||||
modifier = Modifier.height(20.dp),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun SourceSwitchPreference(
|
|
||||||
source: ExtensionSourceItem,
|
|
||||||
onClickSourcePreferences: (sourceId: Long) -> Unit,
|
|
||||||
onClickSource: (sourceId: Long) -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
val context = LocalContext.current
|
|
||||||
|
|
||||||
TextPreferenceWidget(
|
|
||||||
modifier = modifier,
|
|
||||||
title = if (source.labelAsName) {
|
|
||||||
source.source.toString()
|
|
||||||
} else {
|
|
||||||
LocaleHelper.getSourceDisplayName(source.source.lang, context)
|
|
||||||
},
|
|
||||||
widget = {
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
) {
|
|
||||||
if (source.source is ConfigurableSource) {
|
|
||||||
IconButton(onClick = { onClickSourcePreferences(source.source.id) }) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Outlined.Settings,
|
|
||||||
contentDescription = stringResource(MR.strings.label_settings),
|
|
||||||
tint = MaterialTheme.colorScheme.onSurface,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Switch(
|
|
||||||
checked = source.enabled,
|
|
||||||
onCheckedChange = null,
|
|
||||||
modifier = Modifier.padding(start = TrailingWidgetBuffer),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onPreferenceClick = { onClickSource(source.source.id) },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun NsfwWarningDialog(
|
|
||||||
onClickConfirm: () -> Unit,
|
|
||||||
) {
|
|
||||||
AlertDialog(
|
|
||||||
text = {
|
|
||||||
Text(text = stringResource(MR.strings.ext_nsfw_warning))
|
|
||||||
},
|
|
||||||
confirmButton = {
|
|
||||||
TextButton(onClick = onClickConfirm) {
|
|
||||||
Text(text = stringResource(MR.strings.action_ok))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onDismissRequest = onClickConfirm,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,68 +0,0 @@
|
|||||||
package eu.kanade.presentation.browse
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
|
||||||
import androidx.compose.foundation.lazy.items
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import eu.kanade.presentation.components.AppBar
|
|
||||||
import eu.kanade.presentation.more.settings.widget.SwitchPreferenceWidget
|
|
||||||
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionFilterState
|
|
||||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
|
||||||
import tachiyomi.i18n.MR
|
|
||||||
import tachiyomi.presentation.core.components.material.Scaffold
|
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
|
||||||
import tachiyomi.presentation.core.screens.EmptyScreen
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun ExtensionFilterScreen(
|
|
||||||
navigateUp: () -> Unit,
|
|
||||||
state: ExtensionFilterState.Success,
|
|
||||||
onClickToggle: (String) -> Unit,
|
|
||||||
) {
|
|
||||||
Scaffold(
|
|
||||||
topBar = { scrollBehavior ->
|
|
||||||
AppBar(
|
|
||||||
title = stringResource(MR.strings.label_extensions),
|
|
||||||
navigateUp = navigateUp,
|
|
||||||
scrollBehavior = scrollBehavior,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
) { contentPadding ->
|
|
||||||
if (state.isEmpty) {
|
|
||||||
EmptyScreen(
|
|
||||||
stringRes = MR.strings.empty_screen,
|
|
||||||
modifier = Modifier.padding(contentPadding),
|
|
||||||
)
|
|
||||||
return@Scaffold
|
|
||||||
}
|
|
||||||
ExtensionFilterContent(
|
|
||||||
contentPadding = contentPadding,
|
|
||||||
state = state,
|
|
||||||
onClickLang = onClickToggle,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun ExtensionFilterContent(
|
|
||||||
contentPadding: PaddingValues,
|
|
||||||
state: ExtensionFilterState.Success,
|
|
||||||
onClickLang: (String) -> Unit,
|
|
||||||
) {
|
|
||||||
val context = LocalContext.current
|
|
||||||
LazyColumn(
|
|
||||||
contentPadding = contentPadding,
|
|
||||||
) {
|
|
||||||
items(state.languages) { language ->
|
|
||||||
SwitchPreferenceWidget(
|
|
||||||
modifier = Modifier.animateItem(),
|
|
||||||
title = LocaleHelper.getSourceDisplayName(language, context),
|
|
||||||
checked = language in state.enabledLanguages,
|
|
||||||
onCheckedChanged = { onClickLang(language) },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,543 +0,0 @@
|
|||||||
package eu.kanade.presentation.browse
|
|
||||||
|
|
||||||
import androidx.compose.animation.core.animateDpAsState
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.combinedClickable
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.FlowRow
|
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.RowScope
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.lazy.items
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.outlined.Close
|
|
||||||
import androidx.compose.material.icons.outlined.GetApp
|
|
||||||
import androidx.compose.material.icons.outlined.Public
|
|
||||||
import androidx.compose.material.icons.outlined.Refresh
|
|
||||||
import androidx.compose.material.icons.outlined.Settings
|
|
||||||
import androidx.compose.material.icons.outlined.VerifiedUser
|
|
||||||
import androidx.compose.material3.AlertDialog
|
|
||||||
import androidx.compose.material3.Button
|
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.LocalTextStyle
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.ProvideTextStyle
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.material3.TextButton
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
|
||||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
|
||||||
import dev.icerock.moko.resources.StringResource
|
|
||||||
import eu.kanade.presentation.browse.components.BaseBrowseItem
|
|
||||||
import eu.kanade.presentation.browse.components.ExtensionIcon
|
|
||||||
import eu.kanade.presentation.components.WarningBanner
|
|
||||||
import eu.kanade.presentation.manga.components.DotSeparatorNoSpaceText
|
|
||||||
import eu.kanade.presentation.more.settings.screen.browse.ExtensionReposScreen
|
|
||||||
import eu.kanade.presentation.util.animateItemFastScroll
|
|
||||||
import eu.kanade.presentation.util.rememberRequestPackageInstallsPermissionState
|
|
||||||
import eu.kanade.tachiyomi.extension.model.Extension
|
|
||||||
import eu.kanade.tachiyomi.extension.model.InstallStep
|
|
||||||
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionUiModel
|
|
||||||
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionsScreenModel
|
|
||||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
|
||||||
import eu.kanade.tachiyomi.util.system.launchRequestPackageInstallsPermission
|
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
|
||||||
import tachiyomi.i18n.MR
|
|
||||||
import tachiyomi.i18n.sy.SYMR
|
|
||||||
import tachiyomi.presentation.core.components.FastScrollLazyColumn
|
|
||||||
import tachiyomi.presentation.core.components.material.PullRefresh
|
|
||||||
import tachiyomi.presentation.core.components.material.padding
|
|
||||||
import tachiyomi.presentation.core.components.material.topSmallPaddingValues
|
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
|
||||||
import tachiyomi.presentation.core.screens.EmptyScreen
|
|
||||||
import tachiyomi.presentation.core.screens.EmptyScreenAction
|
|
||||||
import tachiyomi.presentation.core.screens.LoadingScreen
|
|
||||||
import tachiyomi.presentation.core.theme.header
|
|
||||||
import tachiyomi.presentation.core.util.plus
|
|
||||||
import tachiyomi.presentation.core.util.secondaryItemAlpha
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun ExtensionScreen(
|
|
||||||
state: ExtensionsScreenModel.State,
|
|
||||||
contentPadding: PaddingValues,
|
|
||||||
searchQuery: String?,
|
|
||||||
onLongClickItem: (Extension) -> Unit,
|
|
||||||
onClickItemCancel: (Extension) -> Unit,
|
|
||||||
onOpenWebView: (Extension.Available) -> Unit,
|
|
||||||
onInstallExtension: (Extension.Available) -> Unit,
|
|
||||||
onUninstallExtension: (Extension) -> Unit,
|
|
||||||
onUpdateExtension: (Extension.Installed) -> Unit,
|
|
||||||
onTrustExtension: (Extension.Untrusted) -> Unit,
|
|
||||||
onOpenExtension: (Extension.Installed) -> Unit,
|
|
||||||
onClickUpdateAll: () -> Unit,
|
|
||||||
onRefresh: () -> Unit,
|
|
||||||
) {
|
|
||||||
val navigator = LocalNavigator.currentOrThrow
|
|
||||||
|
|
||||||
PullRefresh(
|
|
||||||
refreshing = state.isRefreshing,
|
|
||||||
onRefresh = onRefresh,
|
|
||||||
enabled = !state.isLoading,
|
|
||||||
) {
|
|
||||||
when {
|
|
||||||
state.isLoading -> LoadingScreen(Modifier.padding(contentPadding))
|
|
||||||
state.isEmpty -> {
|
|
||||||
val msg = if (!searchQuery.isNullOrEmpty()) {
|
|
||||||
MR.strings.no_results_found
|
|
||||||
} else {
|
|
||||||
MR.strings.empty_screen
|
|
||||||
}
|
|
||||||
EmptyScreen(
|
|
||||||
msg,
|
|
||||||
modifier = Modifier.padding(contentPadding),
|
|
||||||
actions = persistentListOf(
|
|
||||||
EmptyScreenAction(
|
|
||||||
stringRes = MR.strings.label_extension_repos,
|
|
||||||
icon = Icons.Outlined.Settings,
|
|
||||||
onClick = { navigator.push(ExtensionReposScreen()) },
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
ExtensionContent(
|
|
||||||
state = state,
|
|
||||||
contentPadding = contentPadding,
|
|
||||||
onLongClickItem = onLongClickItem,
|
|
||||||
onClickItemCancel = onClickItemCancel,
|
|
||||||
onOpenWebView = onOpenWebView,
|
|
||||||
onInstallExtension = onInstallExtension,
|
|
||||||
onUninstallExtension = onUninstallExtension,
|
|
||||||
onUpdateExtension = onUpdateExtension,
|
|
||||||
onTrustExtension = onTrustExtension,
|
|
||||||
onOpenExtension = onOpenExtension,
|
|
||||||
onClickUpdateAll = onClickUpdateAll,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun ExtensionContent(
|
|
||||||
state: ExtensionsScreenModel.State,
|
|
||||||
contentPadding: PaddingValues,
|
|
||||||
onLongClickItem: (Extension) -> Unit,
|
|
||||||
onClickItemCancel: (Extension) -> Unit,
|
|
||||||
onOpenWebView: (Extension.Available) -> Unit,
|
|
||||||
onInstallExtension: (Extension.Available) -> Unit,
|
|
||||||
onUninstallExtension: (Extension) -> Unit,
|
|
||||||
onUpdateExtension: (Extension.Installed) -> Unit,
|
|
||||||
onTrustExtension: (Extension.Untrusted) -> Unit,
|
|
||||||
onOpenExtension: (Extension.Installed) -> Unit,
|
|
||||||
onClickUpdateAll: () -> Unit,
|
|
||||||
) {
|
|
||||||
val context = LocalContext.current
|
|
||||||
var trustState by remember { mutableStateOf<Extension.Untrusted?>(null) }
|
|
||||||
val installGranted = rememberRequestPackageInstallsPermissionState(initialValue = true)
|
|
||||||
|
|
||||||
FastScrollLazyColumn(
|
|
||||||
contentPadding = contentPadding + topSmallPaddingValues,
|
|
||||||
) {
|
|
||||||
if (!installGranted && state.installer?.requiresSystemPermission == true) {
|
|
||||||
item(key = "extension-permissions-warning") {
|
|
||||||
WarningBanner(
|
|
||||||
textRes = MR.strings.ext_permission_install_apps_warning,
|
|
||||||
modifier = Modifier.clickable {
|
|
||||||
context.launchRequestPackageInstallsPermission()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
state.items.forEach { (header, items) ->
|
|
||||||
item(
|
|
||||||
contentType = "header",
|
|
||||||
key = "extensionHeader-${header.hashCode()}",
|
|
||||||
) {
|
|
||||||
when (header) {
|
|
||||||
is ExtensionUiModel.Header.Resource -> {
|
|
||||||
val action: @Composable RowScope.() -> Unit =
|
|
||||||
if (header.textRes == MR.strings.ext_updates_pending) {
|
|
||||||
{
|
|
||||||
Button(onClick = { onClickUpdateAll() }) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(MR.strings.ext_update_all),
|
|
||||||
style = LocalTextStyle.current.copy(
|
|
||||||
color = MaterialTheme.colorScheme.onPrimary,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
{}
|
|
||||||
}
|
|
||||||
ExtensionHeader(
|
|
||||||
textRes = header.textRes,
|
|
||||||
modifier = Modifier.animateItemFastScroll(),
|
|
||||||
action = action,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
is ExtensionUiModel.Header.Text -> {
|
|
||||||
ExtensionHeader(
|
|
||||||
text = header.text,
|
|
||||||
modifier = Modifier.animateItemFastScroll(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
items(
|
|
||||||
items = items,
|
|
||||||
contentType = { "item" },
|
|
||||||
key = { item ->
|
|
||||||
when (item.extension) {
|
|
||||||
is Extension.Untrusted -> "extension-untrusted-${item.hashCode()}"
|
|
||||||
is Extension.Installed -> "extension-installed-${item.hashCode()}"
|
|
||||||
is Extension.Available -> "extension-available-${item.hashCode()}"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
) { item ->
|
|
||||||
ExtensionItem(
|
|
||||||
modifier = Modifier.animateItemFastScroll(),
|
|
||||||
item = item,
|
|
||||||
onClickItem = {
|
|
||||||
when (it) {
|
|
||||||
is Extension.Available -> onInstallExtension(it)
|
|
||||||
is Extension.Installed -> onOpenExtension(it)
|
|
||||||
is Extension.Untrusted -> {
|
|
||||||
trustState = it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onLongClickItem = onLongClickItem,
|
|
||||||
onClickItemSecondaryAction = {
|
|
||||||
when (it) {
|
|
||||||
is Extension.Available -> onOpenWebView(it)
|
|
||||||
is Extension.Installed -> onOpenExtension(it)
|
|
||||||
else -> {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onClickItemCancel = onClickItemCancel,
|
|
||||||
onClickItemAction = {
|
|
||||||
when (it) {
|
|
||||||
is Extension.Available -> onInstallExtension(it)
|
|
||||||
is Extension.Installed -> {
|
|
||||||
if (it.hasUpdate) {
|
|
||||||
onUpdateExtension(it)
|
|
||||||
} else {
|
|
||||||
onOpenExtension(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is Extension.Untrusted -> {
|
|
||||||
trustState = it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (trustState != null) {
|
|
||||||
ExtensionTrustDialog(
|
|
||||||
onClickConfirm = {
|
|
||||||
onTrustExtension(trustState!!)
|
|
||||||
trustState = null
|
|
||||||
},
|
|
||||||
onClickDismiss = {
|
|
||||||
onUninstallExtension(trustState!!)
|
|
||||||
trustState = null
|
|
||||||
},
|
|
||||||
onDismissRequest = {
|
|
||||||
trustState = null
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun ExtensionItem(
|
|
||||||
item: ExtensionUiModel.Item,
|
|
||||||
onClickItem: (Extension) -> Unit,
|
|
||||||
onLongClickItem: (Extension) -> Unit,
|
|
||||||
onClickItemCancel: (Extension) -> Unit,
|
|
||||||
onClickItemAction: (Extension) -> Unit,
|
|
||||||
onClickItemSecondaryAction: (Extension) -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
val (extension, installStep) = item
|
|
||||||
BaseBrowseItem(
|
|
||||||
modifier = modifier
|
|
||||||
.combinedClickable(
|
|
||||||
onClick = { onClickItem(extension) },
|
|
||||||
onLongClick = { onLongClickItem(extension) },
|
|
||||||
),
|
|
||||||
onClickItem = { onClickItem(extension) },
|
|
||||||
onLongClickItem = { onLongClickItem(extension) },
|
|
||||||
icon = {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.size(40.dp),
|
|
||||||
contentAlignment = Alignment.Center,
|
|
||||||
) {
|
|
||||||
val idle = installStep.isCompleted()
|
|
||||||
if (!idle) {
|
|
||||||
CircularProgressIndicator(
|
|
||||||
modifier = Modifier.size(40.dp),
|
|
||||||
strokeWidth = 2.dp,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val padding by animateDpAsState(targetValue = if (idle) 0.dp else 8.dp)
|
|
||||||
ExtensionIcon(
|
|
||||||
extension = extension,
|
|
||||||
modifier = Modifier
|
|
||||||
.matchParentSize()
|
|
||||||
.padding(padding),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
action = {
|
|
||||||
ExtensionItemActions(
|
|
||||||
extension = extension,
|
|
||||||
installStep = installStep,
|
|
||||||
onClickItemCancel = onClickItemCancel,
|
|
||||||
onClickItemAction = onClickItemAction,
|
|
||||||
onClickItemSecondaryAction = onClickItemSecondaryAction,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
ExtensionItemContent(
|
|
||||||
extension = extension,
|
|
||||||
installStep = installStep,
|
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun ExtensionItemContent(
|
|
||||||
extension: Extension,
|
|
||||||
installStep: InstallStep,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = modifier.padding(start = MaterialTheme.padding.medium),
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = extension.name,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
)
|
|
||||||
// Won't look good but it's not like we can ellipsize overflowing content
|
|
||||||
FlowRow(
|
|
||||||
modifier = Modifier.secondaryItemAlpha(),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
|
|
||||||
) {
|
|
||||||
ProvideTextStyle(value = MaterialTheme.typography.bodySmall) {
|
|
||||||
if (extension is Extension.Installed && extension.lang.isNotEmpty()) {
|
|
||||||
Text(
|
|
||||||
text = LocaleHelper.getSourceDisplayName(extension.lang, LocalContext.current),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (extension.versionName.isNotEmpty()) {
|
|
||||||
Text(
|
|
||||||
text = extension.versionName,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val warning = when {
|
|
||||||
extension is Extension.Untrusted -> MR.strings.ext_untrusted
|
|
||||||
extension is Extension.Installed && extension.isObsolete -> MR.strings.ext_obsolete
|
|
||||||
// SY -->
|
|
||||||
extension is Extension.Installed && extension.isRedundant -> SYMR.strings.ext_redundant
|
|
||||||
// SY <--
|
|
||||||
extension.isNsfw -> MR.strings.ext_nsfw_short
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
if (warning != null) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(warning).uppercase(),
|
|
||||||
color = MaterialTheme.colorScheme.error,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!installStep.isCompleted()) {
|
|
||||||
DotSeparatorNoSpaceText()
|
|
||||||
Text(
|
|
||||||
text = when (installStep) {
|
|
||||||
InstallStep.Pending -> stringResource(MR.strings.ext_pending)
|
|
||||||
InstallStep.Downloading -> stringResource(MR.strings.ext_downloading)
|
|
||||||
InstallStep.Installing -> stringResource(MR.strings.ext_installing)
|
|
||||||
else -> error("Must not show non-install process text")
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun ExtensionItemActions(
|
|
||||||
extension: Extension,
|
|
||||||
installStep: InstallStep,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
onClickItemCancel: (Extension) -> Unit = {},
|
|
||||||
onClickItemAction: (Extension) -> Unit = {},
|
|
||||||
onClickItemSecondaryAction: (Extension) -> Unit = {},
|
|
||||||
) {
|
|
||||||
val isIdle = installStep.isCompleted()
|
|
||||||
|
|
||||||
Row(
|
|
||||||
modifier = modifier,
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
|
||||||
) {
|
|
||||||
when {
|
|
||||||
!isIdle -> {
|
|
||||||
IconButton(onClick = { onClickItemCancel(extension) }) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Outlined.Close,
|
|
||||||
contentDescription = stringResource(MR.strings.action_cancel),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
installStep == InstallStep.Error -> {
|
|
||||||
IconButton(onClick = { onClickItemAction(extension) }) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Outlined.Refresh,
|
|
||||||
contentDescription = stringResource(MR.strings.action_retry),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
installStep == InstallStep.Idle -> {
|
|
||||||
when (extension) {
|
|
||||||
is Extension.Installed -> {
|
|
||||||
IconButton(onClick = { onClickItemSecondaryAction(extension) }) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Outlined.Settings,
|
|
||||||
contentDescription = stringResource(MR.strings.action_settings),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (extension.hasUpdate) {
|
|
||||||
IconButton(onClick = { onClickItemAction(extension) }) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Outlined.GetApp,
|
|
||||||
contentDescription = stringResource(MR.strings.ext_update),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is Extension.Untrusted -> {
|
|
||||||
IconButton(onClick = { onClickItemAction(extension) }) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Outlined.VerifiedUser,
|
|
||||||
contentDescription = stringResource(MR.strings.ext_trust),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is Extension.Available -> {
|
|
||||||
if (extension.sources.isNotEmpty()) {
|
|
||||||
IconButton(
|
|
||||||
onClick = { onClickItemSecondaryAction(extension) },
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Outlined.Public,
|
|
||||||
contentDescription = stringResource(MR.strings.action_open_in_web_view),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
IconButton(onClick = { onClickItemAction(extension) }) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Outlined.GetApp,
|
|
||||||
contentDescription = stringResource(MR.strings.ext_install),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun ExtensionHeader(
|
|
||||||
textRes: StringResource,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
action: @Composable RowScope.() -> Unit = {},
|
|
||||||
) {
|
|
||||||
ExtensionHeader(
|
|
||||||
text = stringResource(textRes),
|
|
||||||
modifier = modifier,
|
|
||||||
action = action,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun ExtensionHeader(
|
|
||||||
text: String,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
action: @Composable RowScope.() -> Unit = {},
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = modifier.padding(horizontal = MaterialTheme.padding.medium),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = text,
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(vertical = 8.dp)
|
|
||||||
.weight(1f),
|
|
||||||
style = MaterialTheme.typography.header,
|
|
||||||
)
|
|
||||||
action()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun ExtensionTrustDialog(
|
|
||||||
onClickConfirm: () -> Unit,
|
|
||||||
onClickDismiss: () -> Unit,
|
|
||||||
onDismissRequest: () -> Unit,
|
|
||||||
) {
|
|
||||||
AlertDialog(
|
|
||||||
title = {
|
|
||||||
Text(text = stringResource(MR.strings.untrusted_extension))
|
|
||||||
},
|
|
||||||
text = {
|
|
||||||
Text(text = stringResource(MR.strings.untrusted_extension_message))
|
|
||||||
},
|
|
||||||
confirmButton = {
|
|
||||||
TextButton(onClick = onClickConfirm) {
|
|
||||||
Text(text = stringResource(MR.strings.ext_trust))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
dismissButton = {
|
|
||||||
TextButton(onClick = onClickDismiss) {
|
|
||||||
Text(text = stringResource(MR.strings.ext_uninstall))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onDismissRequest = onDismissRequest,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user