mirror of
https://github.com/mihonapp/mihon.git
synced 2025-06-28 20:17:51 +02:00
Compare commits
4 Commits
Author | SHA1 | Date | |
---|---|---|---|
706fa82a37 | |||
57c54fa275 | |||
38c5234e46 | |||
767ee164e6 |
@ -1,8 +0,0 @@
|
|||||||
[*.{kt,kts}]
|
|
||||||
max_line_length = 120
|
|
||||||
indent_size = 4
|
|
||||||
insert_final_newline = true
|
|
||||||
ij_kotlin_allow_trailing_comma = true
|
|
||||||
ij_kotlin_allow_trailing_comma_on_call_site = true
|
|
||||||
ij_kotlin_name_count_to_use_star_import = 2147483647
|
|
||||||
ij_kotlin_name_count_to_use_star_import_for_members = 2147483647
|
|
24
.gitattributes
vendored
24
.gitattributes
vendored
@ -1,24 +0,0 @@
|
|||||||
* text=auto
|
|
||||||
* text eol=lf
|
|
||||||
|
|
||||||
# Windows forced line-endings
|
|
||||||
/.idea/* text eol=crlf
|
|
||||||
|
|
||||||
# Gradle wrapper
|
|
||||||
*.jar binary
|
|
||||||
|
|
||||||
# Images
|
|
||||||
*.webp binary
|
|
||||||
*.png binary
|
|
||||||
*.jpg binary
|
|
||||||
*.jpeg binary
|
|
||||||
*.gif binary
|
|
||||||
*.ico binary
|
|
||||||
*.gz binary
|
|
||||||
*.zip binary
|
|
||||||
*.7z binary
|
|
||||||
*.ttf binary
|
|
||||||
*.eot binary
|
|
||||||
*.woff binary
|
|
||||||
*.pyc binary
|
|
||||||
*.swp binary
|
|
31
.github/CONTRIBUTING.md
vendored
Normal file
31
.github/CONTRIBUTING.md
vendored
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# Bugs
|
||||||
|
* Include version (Setting > About > Version)
|
||||||
|
* If not latest, try updating, it may have already been solved
|
||||||
|
* Dev version is equal to the number of commits as seen in the main page
|
||||||
|
* Include steps to reproduce (if not obvious from description)
|
||||||
|
* Include screenshot (if needed)
|
||||||
|
* If it could be device-dependent, try reproducing on another device (if possible), include results and device names, OS, modifications (root, Xposed)
|
||||||
|
* **Before reporting a new issue, take a look at the [FAQ](https://github.com/inorichi/tachiyomi/wiki/FAQ), the [changelog](https://github.com/inorichi/tachiyomi/releases) and the already opened [issues](https://github.com/inorichi/tachiyomi/issues).**
|
||||||
|
* For large logs use http://pastebin.com/ (or similar)
|
||||||
|
* For multipart issues **use list** like this:
|
||||||
|
* [x] Done
|
||||||
|
* [ ] Not done
|
||||||
|
```
|
||||||
|
* [x] Done
|
||||||
|
* [ ] Not done
|
||||||
|
```
|
||||||
|
* Don't put together too many unrelated requests into one issue
|
||||||
|
|
||||||
|
DO: https://github.com/inorichi/tachiyomi/issues/24 https://github.com/inorichi/tachiyomi/issues/71
|
||||||
|
|
||||||
|
DON'T: https://github.com/inorichi/tachiyomi/issues/75
|
||||||
|
|
||||||
|
# Feature requests
|
||||||
|
|
||||||
|
* Write a detailed issue, explaning what it should do or how. Avoid writing just "like X app does"
|
||||||
|
* Include screenshot (if needed)
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
|
||||||
|
File `app/src/main/res/values/strings.xml` should be copied over to appropriate directories and then translated.
|
||||||
|
Consult [Android.com](http://developer.android.com/training/basics/supporting-devices/languages.html#CreateDirs)
|
1
.github/FUNDING.yml
vendored
1
.github/FUNDING.yml
vendored
@ -1 +0,0 @@
|
|||||||
ko_fi: inorichi
|
|
35
.github/ISSUE_TEMPLATE.md
vendored
35
.github/ISSUE_TEMPLATE.md
vendored
@ -1,34 +1,7 @@
|
|||||||
**PLEASE READ THIS**
|
**Please read https://github.com/inorichi/tachiyomi/blob/master/.github/CONTRIBUTING.md before posting**
|
||||||
|
|
||||||
I acknowledge that:
|
Remove line above and describe your issue here. Fill out version below. Use Preview.
|
||||||
|
|
||||||
- I have updated:
|
|
||||||
- To the latest version of the app (stable is v0.15.3)
|
|
||||||
- All extensions
|
|
||||||
- I have gone through the FAQ (https://mihon.app/docs/faq/general) and troubleshooting guide (https://mihon.app/docs/guides/troubleshooting/)
|
|
||||||
- If this is an issue with an official extension, that I should be opening an issue in https://github.com/tachiyomiorg/extensions
|
|
||||||
- I have searched the existing issues and this is new ticket **NOT** a duplicate or related to another open or closed issue
|
|
||||||
- I will fill out the title and the information in this template
|
|
||||||
|
|
||||||
Note that the issue will be automatically closed if you do not fill out the title or requested information.
|
Version: r000 or v0.0.0
|
||||||
|
(other relevant info like OS)
|
||||||
**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.
|
|
||||||
|
11
.github/ISSUE_TEMPLATE/config.yml
vendored
11
.github/ISSUE_TEMPLATE/config.yml
vendored
@ -1,11 +0,0 @@
|
|||||||
blank_issues_enabled: false
|
|
||||||
contact_links:
|
|
||||||
- name: ⚠️ Extension/source issue
|
|
||||||
url: https://github.com/tachiyomiorg/extensions/issues/new/choose
|
|
||||||
about: Issues and requests for official extensions and sources should be opened in the extensions repository instead
|
|
||||||
- name: 📦 Tachiyomi extensions
|
|
||||||
url: https://mihon.app/extensions/
|
|
||||||
about: List of all available extensions with download links
|
|
||||||
- name: 🖥️ Tachiyomi website
|
|
||||||
url: https://mihon.app/
|
|
||||||
about: Guides, troubleshooting, and answers to common questions
|
|
106
.github/ISSUE_TEMPLATE/report_issue.yml
vendored
106
.github/ISSUE_TEMPLATE/report_issue.yml
vendored
@ -1,106 +0,0 @@
|
|||||||
name: 🐞 Issue report
|
|
||||||
description: Report an issue in Tachiyomi
|
|
||||||
labels: [Bug]
|
|
||||||
body:
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: reproduce-steps
|
|
||||||
attributes:
|
|
||||||
label: Steps to reproduce
|
|
||||||
description: Provide an example of the issue.
|
|
||||||
placeholder: |
|
|
||||||
Example:
|
|
||||||
1. First step
|
|
||||||
2. Second step
|
|
||||||
3. Issue here
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: expected-behavior
|
|
||||||
attributes:
|
|
||||||
label: Expected behavior
|
|
||||||
description: Explain what you should expect to happen.
|
|
||||||
placeholder: |
|
|
||||||
Example:
|
|
||||||
"This should happen..."
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: actual-behavior
|
|
||||||
attributes:
|
|
||||||
label: Actual behavior
|
|
||||||
description: Explain what actually happens.
|
|
||||||
placeholder: |
|
|
||||||
Example:
|
|
||||||
"This happened instead..."
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: crash-logs
|
|
||||||
attributes:
|
|
||||||
label: Crash logs
|
|
||||||
description: |
|
|
||||||
If you're experiencing crashes, share the crash logs from **More → Settings → Advanced** then press **Dump crash logs**.
|
|
||||||
placeholder: |
|
|
||||||
You can paste the crash logs in plain text or upload it as an attachment.
|
|
||||||
|
|
||||||
- type: input
|
|
||||||
id: tachiyomi-version
|
|
||||||
attributes:
|
|
||||||
label: Tachiyomi version
|
|
||||||
description: You can find your Tachiyomi version in **More → About**.
|
|
||||||
placeholder: |
|
|
||||||
Example: "0.15.3"
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: input
|
|
||||||
id: android-version
|
|
||||||
attributes:
|
|
||||||
label: Android version
|
|
||||||
description: You can find this somewhere in your Android settings.
|
|
||||||
placeholder: |
|
|
||||||
Example: "Android 11"
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: input
|
|
||||||
id: device
|
|
||||||
attributes:
|
|
||||||
label: Device
|
|
||||||
description: List your device and model.
|
|
||||||
placeholder: |
|
|
||||||
Example: "Google Pixel 5"
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: other-details
|
|
||||||
attributes:
|
|
||||||
label: Other details
|
|
||||||
placeholder: |
|
|
||||||
Additional details and attachments.
|
|
||||||
|
|
||||||
- type: checkboxes
|
|
||||||
id: acknowledgements
|
|
||||||
attributes:
|
|
||||||
label: Acknowledgements
|
|
||||||
description: Read this carefully, we will close and ignore your issue if you skimmed through this.
|
|
||||||
options:
|
|
||||||
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open or closed issue.
|
|
||||||
required: true
|
|
||||||
- label: I have written a short but informative title.
|
|
||||||
required: true
|
|
||||||
- label: If this is an issue with an official extension, I should be opening an issue in the [extensions repository](https://github.com/tachiyomiorg/extensions/issues/new/choose).
|
|
||||||
required: true
|
|
||||||
- label: I have gone through the [FAQ](https://mihon.app/docs/faq/general) and [troubleshooting guide](https://mihon.app/docs/guides/troubleshooting/).
|
|
||||||
required: true
|
|
||||||
- label: I have updated the app to version **[0.15.3](https://github.com/mihonapp/mihon/releases/latest)**.
|
|
||||||
required: true
|
|
||||||
- label: I have updated all installed extensions.
|
|
||||||
required: true
|
|
||||||
- label: I will fill out all of the requested information in this form.
|
|
||||||
required: true
|
|
39
.github/ISSUE_TEMPLATE/request_feature.yml
vendored
39
.github/ISSUE_TEMPLATE/request_feature.yml
vendored
@ -1,39 +0,0 @@
|
|||||||
name: ⭐ Feature request
|
|
||||||
description: Suggest a feature to improve Tachiyomi
|
|
||||||
labels: [Feature request]
|
|
||||||
body:
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: feature-description
|
|
||||||
attributes:
|
|
||||||
label: Describe your suggested feature
|
|
||||||
description: How can Tachiyomi be improved?
|
|
||||||
placeholder: |
|
|
||||||
Example:
|
|
||||||
"It should work like this..."
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: other-details
|
|
||||||
attributes:
|
|
||||||
label: Other details
|
|
||||||
placeholder: |
|
|
||||||
Additional details and attachments.
|
|
||||||
|
|
||||||
- type: checkboxes
|
|
||||||
id: acknowledgements
|
|
||||||
attributes:
|
|
||||||
label: Acknowledgements
|
|
||||||
description: Read this carefully, we will close and ignore your issue if you skimmed through this.
|
|
||||||
options:
|
|
||||||
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open or closed issue.
|
|
||||||
required: true
|
|
||||||
- label: I have written a short but informative title.
|
|
||||||
required: true
|
|
||||||
- label: If this is an issue with an official extension, I should be opening an issue in the [extensions repository](https://github.com/tachiyomiorg/extensions/issues/new/choose).
|
|
||||||
required: true
|
|
||||||
- label: I have updated the app to version **[0.15.3](https://github.com/mihonapp/mihon/releases/latest)**.
|
|
||||||
required: true
|
|
||||||
- label: I will fill out all of the requested information in this form.
|
|
||||||
required: true
|
|
10
.github/mergify.yml
vendored
10
.github/mergify.yml
vendored
@ -1,10 +0,0 @@
|
|||||||
#pull_request_rules:
|
|
||||||
# - name: Automatically merge translations
|
|
||||||
# conditions:
|
|
||||||
# - "author = weblate"
|
|
||||||
# - "-conflict"
|
|
||||||
# - "current-day-of-week = Sat"
|
|
||||||
# - "created-at < 1 day ago"
|
|
||||||
# actions:
|
|
||||||
# merge:
|
|
||||||
# method: squash
|
|
12
.github/pull_request_template.md
vendored
12
.github/pull_request_template.md
vendored
@ -1,12 +0,0 @@
|
|||||||
<!--
|
|
||||||
Please include a summary of the change and which issue is fixed.
|
|
||||||
Also make sure you've tested your code and also done a self-review of it.
|
|
||||||
Don't forget to check all base themes and tablet mode for relevant changes.
|
|
||||||
|
|
||||||
If your changes are visual, please provide images below:
|
|
||||||
|
|
||||||
### Images
|
|
||||||
| Image 1 | Image 2 |
|
|
||||||
| ------- | ------- |
|
|
||||||
|  |  |
|
|
||||||
-->
|
|
BIN
.github/readme-images/app-icon.png
vendored
BIN
.github/readme-images/app-icon.png
vendored
Binary file not shown.
Before Width: | Height: | Size: 1.1 KiB |
17
.github/renovate.json5
vendored
17
.github/renovate.json5
vendored
@ -1,17 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
|
||||||
"extends": [
|
|
||||||
"config:base"
|
|
||||||
],
|
|
||||||
"schedule": ["every sunday"],
|
|
||||||
"packageRules": [
|
|
||||||
{
|
|
||||||
// Compiler plugins are tightly coupled to Kotlin version
|
|
||||||
"groupName": "Kotlin",
|
|
||||||
"matchPackagePrefixes": [
|
|
||||||
"androidx.compose.compiler",
|
|
||||||
"org.jetbrains.kotlin",
|
|
||||||
],
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
40
.github/workflows/build_pull_request.yml
vendored
40
.github/workflows/build_pull_request.yml
vendored
@ -1,40 +0,0 @@
|
|||||||
name: PR build check
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
paths-ignore:
|
|
||||||
- '**.md'
|
|
||||||
- 'i18n/src/commonMain/resources/**/strings.xml'
|
|
||||||
- 'i18n/src/commonMain/resources/**/plurals.xml'
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
name: Build app
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Clone repo
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Validate Gradle Wrapper
|
|
||||||
uses: gradle/wrapper-validation-action@v1
|
|
||||||
|
|
||||||
- name: Dependency Review
|
|
||||||
uses: actions/dependency-review-action@v3
|
|
||||||
|
|
||||||
- name: Set up JDK
|
|
||||||
uses: actions/setup-java@v4
|
|
||||||
with:
|
|
||||||
java-version: 17
|
|
||||||
distribution: adopt
|
|
||||||
|
|
||||||
- name: Build app and run unit tests
|
|
||||||
uses: gradle/gradle-command-action@v2
|
|
||||||
with:
|
|
||||||
arguments: ktlintCheck assembleStandardRelease testReleaseUnitTest
|
|
112
.github/workflows/build_push.yml
vendored
112
.github/workflows/build_push.yml
vendored
@ -1,112 +0,0 @@
|
|||||||
name: CI
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
tags:
|
|
||||||
- v*
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
name: Build app
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Clone repo
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Validate Gradle Wrapper
|
|
||||||
uses: gradle/wrapper-validation-action@v1
|
|
||||||
|
|
||||||
- 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: adopt
|
|
||||||
|
|
||||||
- name: Build app and run unit tests
|
|
||||||
uses: gradle/gradle-command-action@v2
|
|
||||||
with:
|
|
||||||
arguments: ktlintCheck assembleStandardRelease testReleaseUnitTest
|
|
||||||
|
|
||||||
# Sign APK and create release for tags
|
|
||||||
|
|
||||||
- name: Get tag name
|
|
||||||
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'mihonapp/mihon'
|
|
||||||
run: |
|
|
||||||
set -x
|
|
||||||
echo "VERSION_TAG=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Sign APK
|
|
||||||
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'mihonapp/mihon'
|
|
||||||
uses: r0adkll/sign-android-release@v1
|
|
||||||
with:
|
|
||||||
releaseDirectory: app/build/outputs/apk/standard/release
|
|
||||||
signingKeyBase64: ${{ secrets.SIGNING_KEY }}
|
|
||||||
alias: ${{ secrets.ALIAS }}
|
|
||||||
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
|
|
||||||
keyPassword: ${{ secrets.KEY_PASSWORD }}
|
|
||||||
|
|
||||||
- name: Clean up build artifacts
|
|
||||||
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'mihonapp/mihon'
|
|
||||||
run: |
|
|
||||||
set -e
|
|
||||||
|
|
||||||
mv app/build/outputs/apk/standard/release/app-standard-universal-release-unsigned-signed.apk mihon-${{ env.VERSION_TAG }}.apk
|
|
||||||
sha=`sha256sum mihon-${{ env.VERSION_TAG }}.apk | awk '{ print $1 }'`
|
|
||||||
echo "APK_UNIVERSAL_SHA=$sha" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
cp app/build/outputs/apk/standard/release/app-standard-arm64-v8a-release-unsigned-signed.apk mihon-arm64-v8a-${{ env.VERSION_TAG }}.apk
|
|
||||||
sha=`sha256sum mihon-arm64-v8a-${{ env.VERSION_TAG }}.apk | awk '{ print $1 }'`
|
|
||||||
echo "APK_ARM64_V8A_SHA=$sha" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
cp app/build/outputs/apk/standard/release/app-standard-armeabi-v7a-release-unsigned-signed.apk mihon-armeabi-v7a-${{ env.VERSION_TAG }}.apk
|
|
||||||
sha=`sha256sum mihon-armeabi-v7a-${{ env.VERSION_TAG }}.apk | awk '{ print $1 }'`
|
|
||||||
echo "APK_ARMEABI_V7A_SHA=$sha" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
cp app/build/outputs/apk/standard/release/app-standard-x86-release-unsigned-signed.apk mihon-x86-${{ env.VERSION_TAG }}.apk
|
|
||||||
sha=`sha256sum mihon-x86-${{ env.VERSION_TAG }}.apk | awk '{ print $1 }'`
|
|
||||||
echo "APK_X86_SHA=$sha" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
cp app/build/outputs/apk/standard/release/app-standard-x86_64-release-unsigned-signed.apk mihon-x86_64-${{ env.VERSION_TAG }}.apk
|
|
||||||
sha=`sha256sum mihon-x86_64-${{ env.VERSION_TAG }}.apk | awk '{ print $1 }'`
|
|
||||||
echo "APK_X86_64_SHA=$sha" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Create Release
|
|
||||||
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'mihonapp/mihon'
|
|
||||||
uses: softprops/action-gh-release@v1
|
|
||||||
with:
|
|
||||||
tag_name: ${{ env.VERSION_TAG }}
|
|
||||||
name: Mihon ${{ env.VERSION_TAG }}
|
|
||||||
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 mihon-${{ env.VERSION_TAG }}.apk
|
|
||||||
files: |
|
|
||||||
mihon-${{ env.VERSION_TAG }}.apk
|
|
||||||
mihon-arm64-v8a-${{ env.VERSION_TAG }}.apk
|
|
||||||
mihon-armeabi-v7a-${{ env.VERSION_TAG }}.apk
|
|
||||||
mihon-x86-${{ env.VERSION_TAG }}.apk
|
|
||||||
mihon-x86_64-${{ env.VERSION_TAG }}.apk
|
|
||||||
draft: true
|
|
||||||
prerelease: false
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.PAT }}
|
|
45
.github/workflows/issue_moderator.yml
vendored
45
.github/workflows/issue_moderator.yml
vendored
@ -1,45 +0,0 @@
|
|||||||
name: Issue moderator
|
|
||||||
|
|
||||||
on:
|
|
||||||
issues:
|
|
||||||
types: [opened, edited, reopened]
|
|
||||||
issue_comment:
|
|
||||||
types: [created]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
moderate:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Moderate issues
|
|
||||||
uses: tachiyomiorg/issue-moderator-action@v2
|
|
||||||
with:
|
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
duplicate-label: Duplicate
|
|
||||||
|
|
||||||
auto-close-rules: |
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"type": "body",
|
|
||||||
"regex": ".*DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT.*",
|
|
||||||
"message": "The acknowledgment section was not removed."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "body",
|
|
||||||
"regex": ".*\\* (Tachiyomi version|Android version|Device): \\?.*",
|
|
||||||
"message": "Requested information in the template was not filled out."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "both",
|
|
||||||
"regex": "^(?!.*myanimelist.*).*(aniyomi|anime).*$",
|
|
||||||
"ignoreCase": true,
|
|
||||||
"message": "Tachiyomi does not support anime, and has no plans to support anime. In addition Tachiyomi is not affiliated with Aniyomi https://github.com/jmir1/aniyomi"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "both",
|
|
||||||
"regex": ".*(?:fail(?:ed|ure|s)?|can\\s*(?:no|')?t|(?:not|un).*able|(?<!n[o']?t )blocked by|error) (?:to )?(?:get past|by ?pass|penetrate)?.*cloud ?fl?are.*",
|
|
||||||
"ignoreCase": true,
|
|
||||||
"labels": ["Cloudflare protected"],
|
|
||||||
"message": "Refer to the **Solving Cloudflare issues** section at https://mihon.app/docs/guides/troubleshooting/#cloudflare. If it doesn't work, migrate to other sources or wait until they lower their protection."
|
|
||||||
}
|
|
||||||
]
|
|
||||||
auto-close-ignore-label: do-not-autoclose
|
|
19
.github/workflows/lock.yml
vendored
19
.github/workflows/lock.yml
vendored
@ -1,19 +0,0 @@
|
|||||||
name: Lock threads
|
|
||||||
|
|
||||||
on:
|
|
||||||
# Daily
|
|
||||||
schedule:
|
|
||||||
- cron: '0 0 * * *'
|
|
||||||
# Manual trigger
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
lock:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: dessant/lock-threads@v5
|
|
||||||
with:
|
|
||||||
github-token: ${{ github.token }}
|
|
||||||
issue-inactive-days: '2'
|
|
||||||
pr-inactive-days: '2'
|
|
12
.gitignore
vendored
12
.gitignore
vendored
@ -2,16 +2,8 @@
|
|||||||
/local.properties
|
/local.properties
|
||||||
/.idea/workspace.xml
|
/.idea/workspace.xml
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.idea/*
|
/build
|
||||||
!.idea/icon.png
|
.idea/
|
||||||
*iml
|
*iml
|
||||||
*.iml
|
*.iml
|
||||||
|
|
||||||
# Built files
|
|
||||||
*/build
|
*/build
|
||||||
/build
|
|
||||||
*.apk
|
|
||||||
app/**/output.json
|
|
||||||
|
|
||||||
# Unnecessary file
|
|
||||||
*.swp
|
|
BIN
.idea/icon.png
generated
BIN
.idea/icon.png
generated
Binary file not shown.
Before Width: | Height: | Size: 14 KiB |
28
.travis.yml
Normal file
28
.travis.yml
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
language: android
|
||||||
|
android:
|
||||||
|
components:
|
||||||
|
- platform-tools
|
||||||
|
- tools
|
||||||
|
|
||||||
|
# The BuildTools version used by your project
|
||||||
|
- build-tools-23.0.3
|
||||||
|
- android-23
|
||||||
|
- extra-android-m2repository
|
||||||
|
- extra-google-m2repository
|
||||||
|
- extra-android-support
|
||||||
|
- extra-google-google_play_services
|
||||||
|
|
||||||
|
before_script:
|
||||||
|
- chmod +x gradlew
|
||||||
|
#Build, and run tests
|
||||||
|
script: "./gradlew clean buildDebug"
|
||||||
|
sudo: false
|
||||||
|
|
||||||
|
before_cache:
|
||||||
|
- rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
|
||||||
|
cache:
|
||||||
|
directories:
|
||||||
|
- $HOME/.gradle/caches/
|
||||||
|
- $HOME/.gradle/wrapper/
|
||||||
|
env:
|
||||||
|
- GRADLE_OPTS="-XX:MaxPermSize=1024m -XX:+CMSClassUnloadingEnabled -XX:+HeapDumpOnOutOfMemoryError -Xmx2048m"
|
@ -1,126 +0,0 @@
|
|||||||
# Contributor Covenant Code of Conduct
|
|
||||||
|
|
||||||
## Our Pledge
|
|
||||||
|
|
||||||
We as members, contributors, and leaders pledge to make participation in our
|
|
||||||
community a harassment-free experience for everyone, regardless of age, body
|
|
||||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
|
||||||
identity and expression, level of experience, education, socio-economic status,
|
|
||||||
nationality, personal appearance, race, caste, color, religion, or sexual identity
|
|
||||||
and orientation.
|
|
||||||
|
|
||||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
|
||||||
diverse, inclusive, and healthy community.
|
|
||||||
|
|
||||||
## Our Standards
|
|
||||||
|
|
||||||
Examples of behavior that contributes to a positive environment for our
|
|
||||||
community include:
|
|
||||||
|
|
||||||
* Demonstrating empathy and kindness toward other people
|
|
||||||
* Being respectful of differing opinions, viewpoints, and experiences
|
|
||||||
* Giving and gracefully accepting constructive feedback
|
|
||||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
|
||||||
and learning from the experience
|
|
||||||
* Focusing on what is best not just for us as individuals, but for the
|
|
||||||
overall community
|
|
||||||
|
|
||||||
Examples of unacceptable behavior include:
|
|
||||||
|
|
||||||
* The use of sexualized language or imagery, and sexual attention or
|
|
||||||
advances of any kind
|
|
||||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
|
||||||
* Public or private harassment
|
|
||||||
* Publishing others' private information, such as a physical or email
|
|
||||||
address, without their explicit permission
|
|
||||||
* Other conduct which could reasonably be considered inappropriate in a
|
|
||||||
professional setting
|
|
||||||
|
|
||||||
## Enforcement Responsibilities
|
|
||||||
|
|
||||||
Community moderators are responsible for clarifying and enforcing our standards of
|
|
||||||
acceptable behavior and will take appropriate and fair corrective action in
|
|
||||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
|
||||||
or harmful.
|
|
||||||
|
|
||||||
Community moderators have the right and responsibility to remove, edit, or reject
|
|
||||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
|
||||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
|
||||||
decisions when appropriate.
|
|
||||||
|
|
||||||
## Scope
|
|
||||||
|
|
||||||
This Code of Conduct applies within all community spaces, and also applies when
|
|
||||||
an individual is officially representing the community in public spaces.
|
|
||||||
Examples of representing our community include using an official e-mail address,
|
|
||||||
posting via an official social media account, or acting as an appointed
|
|
||||||
representative at an online or offline event.
|
|
||||||
|
|
||||||
## Enforcement
|
|
||||||
|
|
||||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
|
||||||
reported to the community moderators responsible for enforcement at
|
|
||||||
the [Mihon Discord server](https://discord.gg/mihon).
|
|
||||||
All complaints will be reviewed and investigated promptly and fairly.
|
|
||||||
|
|
||||||
All community moderators are obligated to respect the privacy and security of the
|
|
||||||
reporter of any incident.
|
|
||||||
|
|
||||||
## Enforcement Guidelines
|
|
||||||
|
|
||||||
Community moderators will follow these Community Impact Guidelines in determining
|
|
||||||
the consequences for any action they deem in violation of this Code of Conduct:
|
|
||||||
|
|
||||||
### 1. Correction
|
|
||||||
|
|
||||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
|
||||||
unprofessional or unwelcome in the community.
|
|
||||||
|
|
||||||
**Consequence**: A private, written warning from community moderators, providing
|
|
||||||
clarity around the nature of the violation and an explanation of why the
|
|
||||||
behavior was inappropriate. A public apology may be requested.
|
|
||||||
|
|
||||||
### 2. Warning
|
|
||||||
|
|
||||||
**Community Impact**: A violation through a single incident or series
|
|
||||||
of actions.
|
|
||||||
|
|
||||||
**Consequence**: A warning with consequences for continued behavior. No
|
|
||||||
interaction with the people involved, including unsolicited interaction with
|
|
||||||
those enforcing the Code of Conduct, for a specified period of time. This
|
|
||||||
includes avoiding interactions in community spaces as well as external channels
|
|
||||||
like social media. Violating these terms may lead to a temporary or
|
|
||||||
permanent ban.
|
|
||||||
|
|
||||||
### 3. Temporary Ban
|
|
||||||
|
|
||||||
**Community Impact**: A serious violation of community standards, including
|
|
||||||
sustained inappropriate behavior.
|
|
||||||
|
|
||||||
**Consequence**: A temporary ban from any sort of interaction or public
|
|
||||||
communication with the community for a specified period of time. No public or
|
|
||||||
private interaction with the people involved, including unsolicited interaction
|
|
||||||
with those enforcing the Code of Conduct, is allowed during this period.
|
|
||||||
Violating these terms may lead to a permanent ban.
|
|
||||||
|
|
||||||
### 4. Permanent Ban
|
|
||||||
|
|
||||||
**Community Impact**: Demonstrating a pattern of violation of community
|
|
||||||
standards, including sustained inappropriate behavior, harassment of an
|
|
||||||
individual, or aggression toward or disparagement of classes of individuals.
|
|
||||||
|
|
||||||
**Consequence**: A permanent ban from any sort of public interaction within
|
|
||||||
the community.
|
|
||||||
|
|
||||||
## Attribution
|
|
||||||
|
|
||||||
This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org/),
|
|
||||||
version 2.1, available at
|
|
||||||
[v2.1](https://www.contributor-covenant.org/version/2/1/code_of_conduct.html).
|
|
||||||
|
|
||||||
Community Impact Guidelines were inspired by
|
|
||||||
[Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).
|
|
||||||
|
|
||||||
For answers to common questions about this code of conduct, see the FAQ at
|
|
||||||
[FAQ](https://www.contributor-covenant.org/faq). Translations are available
|
|
||||||
at [translations](https://www.contributor-covenant.org/translations).
|
|
@ -1,53 +0,0 @@
|
|||||||
Looking to report an issue/bug or make a feature request? Please refer to the [README file](https://github.com/mihonapp/mihon#issues-feature-requests-and-contributing).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Thanks for your interest in contributing to Mihon!
|
|
||||||
|
|
||||||
|
|
||||||
# Code contributions
|
|
||||||
|
|
||||||
Pull requests are welcome!
|
|
||||||
|
|
||||||
If you're interested in taking on [an open issue](https://github.com/mihonapp/mihon/issues), please comment on it so others are aware.
|
|
||||||
You do not need to ask for permission nor an assignment.
|
|
||||||
|
|
||||||
## Prerequisites
|
|
||||||
|
|
||||||
Before you start, please note that the ability to use following technologies is **required** and that existing contributors will not actively teach them to you.
|
|
||||||
|
|
||||||
- Basic [Android development](https://developer.android.com/)
|
|
||||||
- [Kotlin](https://kotlinlang.org/)
|
|
||||||
|
|
||||||
### Tools
|
|
||||||
|
|
||||||
- [Android Studio](https://developer.android.com/studio)
|
|
||||||
- Emulator or phone with developer options enabled to test changes.
|
|
||||||
|
|
||||||
## Linting
|
|
||||||
|
|
||||||
To auto-fix some linting errors, run the `ktlintFormat` Gradle task.
|
|
||||||
|
|
||||||
## Getting help
|
|
||||||
|
|
||||||
- Join [the Discord server](https://discord.gg/mihon) for online help and to ask questions while developing.
|
|
||||||
|
|
||||||
# Translations
|
|
||||||
|
|
||||||
Translations are done externally via Weblate. See [our website](https://mihon.app/docs/contribute#translation) for more details.
|
|
||||||
|
|
||||||
|
|
||||||
# Forks
|
|
||||||
|
|
||||||
Forks are allowed so long as they abide by [the project's LICENSE](https://github.com/mihonapp/mihon/blob/main/LICENSE).
|
|
||||||
|
|
||||||
When creating a fork, remember to:
|
|
||||||
|
|
||||||
- To avoid confusion with the main app:
|
|
||||||
- Change the app name
|
|
||||||
- Change the app icon
|
|
||||||
- Change or disable the [app update checker](https://github.com/mihonapp/mihon/blob/main/app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateChecker.kt)
|
|
||||||
- To avoid installation conflicts:
|
|
||||||
- Change the `applicationId` in [`build.gradle.kts`](https://github.com/mihonapp/mihon/blob/main/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/mihonapp/mihon/blob/main/app/src/standard/google-services.json) with your own
|
|
26
LICENSE
26
LICENSE
@ -174,3 +174,29 @@
|
|||||||
of your accepting any such warranty or additional liability.
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright {yyyy} {name of copyright owner}
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
|
||||||
|
82
README.md
82
README.md
@ -1,72 +1,24 @@
|
|||||||
| Build | Stable | Weekly Preview | Support Server |
|
| Build | Download | Auto Update |
|
||||||
|-------|----------|---------|---------|
|
|-------|----------|-------------|
|
||||||
| [](https://github.com/mihonapp/mihon/actions/workflows/build_push.yml) | [](https://github.com/mihonapp/mihon/releases) | [](https://github.com/mihonapp/mihon-preview/releases) | [](https://discord.gg/mihon) |
|
| [](https://teamcity.kanade.eu/project.html?projectId=tachiyomi) [](https://travis-ci.org/inorichi/tachiyomi) | [](https://github.com/inorichi/tachiyomi/releases) [](http://tachiyomi.kanade.eu/latest/app-debug.apk) | [](https://f-droid.org/repository/browse/?fdid=eu.kanade.tachiyomi) [](//github.com/inorichi/tachiyomi/wiki/FDroid-for-debug-versions) |
|
||||||
|
|
||||||
# Mihon
|
## [Report an issue](https://github.com/inorichi/tachiyomi/blob/master/.github/CONTRIBUTING.md)
|
||||||
Mihon is a free and open source manga reader for Android 8.0 and above.
|
|
||||||
|
|
||||||
## Features
|
**Before reporting a new issue, take a look at the [FAQ](https://github.com/inorichi/tachiyomi/wiki/FAQ), the [changelog](https://github.com/inorichi/tachiyomi/releases) and the already opened issues.**
|
||||||
|
|
||||||
Features include:
|
Tachiyomi is a free and open source manga reader for Android.
|
||||||
* Online reading from a variety of sources
|
|
||||||
* Local reading of downloaded content
|
Keep in mind it's still a beta, so expect it to crash sometimes.
|
||||||
* A configurable reader with multiple viewers, reading directions and other settings.
|
|
||||||
* Tracker support: [MyAnimeList](https://myanimelist.net/), [AniList](https://anilist.co/), [Kitsu](https://kitsu.io/), [MangaUpdates](https://mangaupdates.com), [Shikimori](https://shikimori.one), and [Bangumi](https://bgm.tv/) support
|
# Features
|
||||||
|
|
||||||
|
* Online and offline reading
|
||||||
|
* Configurable reader with multiple viewers and settings
|
||||||
|
* MyAnimeList support
|
||||||
|
* Resume from the next unread chapter
|
||||||
|
* Chapter filtering
|
||||||
|
* Schedule searching for updates
|
||||||
* Categories to organize your library
|
* Categories to organize your library
|
||||||
* Light and dark themes
|
|
||||||
* Schedule updating your library for new chapters
|
|
||||||
* Create backups locally to read offline or to your desired cloud service
|
|
||||||
|
|
||||||
## Download
|
|
||||||
Get the app from our [releases page](https://github.com/mihonapp/mihon/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/mihonapp/mihon-preview/releases).
|
|
||||||
|
|
||||||
## Issues, Feature Requests and Contributing
|
|
||||||
|
|
||||||
Please make sure to read the full guidelines. Your issue may be closed without warning if you do not.
|
|
||||||
|
|
||||||
<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://mihon.app/changelogs/) and the already opened [issues](https://github.com/mihonapp/mihon/issues).**
|
|
||||||
2. If you are unsure, ask here: [](https://discord.gg/mihon)
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details><summary>Bugs</summary>
|
|
||||||
|
|
||||||
* Include version (More → About → Version)
|
|
||||||
* If not latest, try updating, it may have already been solved
|
|
||||||
* Preview version is equal to the number of commits as seen on the main page
|
|
||||||
* Include steps to reproduce (if not obvious from description)
|
|
||||||
* Include screenshot (if needed)
|
|
||||||
* If it could be device-dependent, try reproducing on another device (if possible)
|
|
||||||
* Don't group unrelated requests into one issue
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details><summary>Feature Requests</summary>
|
|
||||||
|
|
||||||
* Write a detailed issue, explaining what it should do or how. Avoid writing just "like X app does"
|
|
||||||
* Include screenshot (if needed)
|
|
||||||
|
|
||||||
Source requests are not accepted.
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details><summary>Contributing</summary>
|
|
||||||
|
|
||||||
See [CONTRIBUTING.md](./CONTRIBUTING.md).
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details><summary>Code of Conduct</summary>
|
|
||||||
|
|
||||||
See [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md).
|
|
||||||
</details>
|
|
||||||
|
|
||||||
## FAQ
|
|
||||||
|
|
||||||
[See our website.](https://mihon.app/)
|
|
||||||
You can also reach out to us on [Discord](https://discord.gg/mihon).
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
1
app/.gitignore
vendored
1
app/.gitignore
vendored
@ -1,3 +1,4 @@
|
|||||||
/build
|
/build
|
||||||
*iml
|
*iml
|
||||||
*.iml
|
*.iml
|
||||||
|
.idea
|
204
app/build.gradle
Normal file
204
app/build.gradle
Normal file
@ -0,0 +1,204 @@
|
|||||||
|
import java.text.SimpleDateFormat
|
||||||
|
|
||||||
|
apply plugin: 'com.android.application'
|
||||||
|
apply plugin: 'kotlin-android'
|
||||||
|
apply plugin: 'kotlin-android-extensions'
|
||||||
|
|
||||||
|
ext {
|
||||||
|
// Git is needed in your system PATH for these commands to work.
|
||||||
|
// If it's not installed, you can return a random value as a workaround
|
||||||
|
getCommitCount = {
|
||||||
|
return 'git rev-list --count HEAD'.execute().text.trim()
|
||||||
|
// return "1"
|
||||||
|
}
|
||||||
|
|
||||||
|
getGitSha = {
|
||||||
|
return 'git rev-parse --short HEAD'.execute().text.trim()
|
||||||
|
// return "1"
|
||||||
|
}
|
||||||
|
|
||||||
|
getBuildTime = {
|
||||||
|
def df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'")
|
||||||
|
df.setTimeZone(TimeZone.getTimeZone("UTC"))
|
||||||
|
return df.format(new Date())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def includeUpdater() {
|
||||||
|
return hasProperty("include_updater")
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
compileSdkVersion 23
|
||||||
|
buildToolsVersion "23.0.3"
|
||||||
|
publishNonDefault true
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
applicationId "eu.kanade.tachiyomi"
|
||||||
|
minSdkVersion 16
|
||||||
|
targetSdkVersion 23
|
||||||
|
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||||
|
versionCode 9
|
||||||
|
versionName "0.2.2-1"
|
||||||
|
|
||||||
|
buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\""
|
||||||
|
buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\""
|
||||||
|
buildConfigField "String", "BUILD_TIME", "\"${getBuildTime()}\""
|
||||||
|
buildConfigField "boolean", "INCLUDE_UPDATER", "${includeUpdater()}"
|
||||||
|
|
||||||
|
vectorDrawables.useSupportLibrary = true
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
debug {
|
||||||
|
versionNameSuffix ".${getCommitCount()}"
|
||||||
|
applicationIdSuffix ".debug"
|
||||||
|
}
|
||||||
|
release {
|
||||||
|
minifyEnabled true
|
||||||
|
shrinkResources true
|
||||||
|
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
packagingOptions {
|
||||||
|
exclude 'META-INF/DEPENDENCIES'
|
||||||
|
exclude 'LICENSE.txt'
|
||||||
|
exclude 'META-INF/LICENSE'
|
||||||
|
exclude 'META-INF/LICENSE.txt'
|
||||||
|
exclude 'META-INF/NOTICE'
|
||||||
|
}
|
||||||
|
|
||||||
|
lintOptions {
|
||||||
|
abortOnError false
|
||||||
|
checkReleaseBuilds false
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceSets {
|
||||||
|
main.java.srcDirs += 'src/main/kotlin'
|
||||||
|
}
|
||||||
|
|
||||||
|
// http://stackoverflow.com/questions/32759529/androidhttpclient-not-found-when-running-robolectric
|
||||||
|
useLibrary 'org.apache.http.legacy'
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
kapt {
|
||||||
|
generateStubs = true
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
final SUPPORT_LIBRARY_VERSION = '23.4.0'
|
||||||
|
final DAGGER_VERSION = '2.4'
|
||||||
|
final RETROFIT_VERSION = '2.0.2'
|
||||||
|
final NUCLEUS_VERSION = '3.0.0'
|
||||||
|
final STORIO_VERSION = '1.8.0'
|
||||||
|
final MOCKITO_VERSION = '1.10.19'
|
||||||
|
|
||||||
|
// Modified dependencies
|
||||||
|
compile 'com.github.inorichi:subsampling-scale-image-view:421fb81'
|
||||||
|
compile 'com.github.inorichi:ReactiveNetwork:69092ed'
|
||||||
|
|
||||||
|
// Android support library
|
||||||
|
compile "com.android.support:support-v4:$SUPPORT_LIBRARY_VERSION"
|
||||||
|
compile "com.android.support:appcompat-v7:$SUPPORT_LIBRARY_VERSION"
|
||||||
|
compile "com.android.support:cardview-v7:$SUPPORT_LIBRARY_VERSION"
|
||||||
|
compile "com.android.support:design:$SUPPORT_LIBRARY_VERSION"
|
||||||
|
compile "com.android.support:recyclerview-v7:$SUPPORT_LIBRARY_VERSION"
|
||||||
|
compile "com.android.support:support-annotations:$SUPPORT_LIBRARY_VERSION"
|
||||||
|
compile "com.android.support:preference-v7:$SUPPORT_LIBRARY_VERSION"
|
||||||
|
compile "com.android.support:preference-v14:$SUPPORT_LIBRARY_VERSION"
|
||||||
|
compile "com.android.support:customtabs:$SUPPORT_LIBRARY_VERSION"
|
||||||
|
|
||||||
|
// ReactiveX
|
||||||
|
compile 'io.reactivex:rxandroid:1.2.0'
|
||||||
|
compile 'io.reactivex:rxjava:1.1.5'
|
||||||
|
compile 'com.f2prateek.rx.preferences:rx-preferences:1.0.1'
|
||||||
|
|
||||||
|
// Network client
|
||||||
|
compile "com.squareup.okhttp3:okhttp:3.3.1"
|
||||||
|
|
||||||
|
// REST
|
||||||
|
compile "com.squareup.retrofit2:retrofit:$RETROFIT_VERSION"
|
||||||
|
compile "com.squareup.retrofit2:converter-gson:$RETROFIT_VERSION"
|
||||||
|
compile "com.squareup.retrofit2:adapter-rxjava:$RETROFIT_VERSION"
|
||||||
|
|
||||||
|
// IO
|
||||||
|
compile 'com.squareup.okio:okio:1.8.0'
|
||||||
|
|
||||||
|
// JSON
|
||||||
|
compile 'com.google.code.gson:gson:2.6.2'
|
||||||
|
|
||||||
|
// YAML
|
||||||
|
compile 'org.yaml:snakeyaml:1.17'
|
||||||
|
|
||||||
|
// JavaScript engine
|
||||||
|
compile 'com.squareup.duktape:duktape-android:0.9.5'
|
||||||
|
|
||||||
|
// Disk cache
|
||||||
|
compile 'com.jakewharton:disklrucache:2.0.2'
|
||||||
|
|
||||||
|
// Parse HTML
|
||||||
|
compile 'org.jsoup:jsoup:1.9.2'
|
||||||
|
|
||||||
|
// Changelog
|
||||||
|
compile 'com.github.gabrielemariotti.changeloglib:changelog:2.1.0'
|
||||||
|
|
||||||
|
// Database
|
||||||
|
compile "com.pushtorefresh.storio:sqlite:$STORIO_VERSION"
|
||||||
|
compile "com.pushtorefresh.storio:sqlite-annotations:$STORIO_VERSION"
|
||||||
|
kapt "com.pushtorefresh.storio:sqlite-annotations-processor:$STORIO_VERSION"
|
||||||
|
|
||||||
|
// Model View Presenter
|
||||||
|
compile "info.android15.nucleus:nucleus:$NUCLEUS_VERSION"
|
||||||
|
compile "info.android15.nucleus:nucleus-support-v4:$NUCLEUS_VERSION"
|
||||||
|
compile "info.android15.nucleus:nucleus-support-v7:$NUCLEUS_VERSION"
|
||||||
|
|
||||||
|
// Dependency injection
|
||||||
|
compile "com.google.dagger:dagger:$DAGGER_VERSION"
|
||||||
|
kapt "com.google.dagger:dagger-compiler:$DAGGER_VERSION"
|
||||||
|
provided 'org.glassfish:javax.annotation:10.0-b28'
|
||||||
|
|
||||||
|
// Image library
|
||||||
|
compile 'com.github.bumptech.glide:glide:3.7.0'
|
||||||
|
compile 'com.github.bumptech.glide:okhttp3-integration:1.4.0@aar'
|
||||||
|
|
||||||
|
// Logging
|
||||||
|
compile 'com.jakewharton.timber:timber:4.1.2'
|
||||||
|
|
||||||
|
// Crash reports
|
||||||
|
compile 'ch.acra:acra:4.8.5'
|
||||||
|
|
||||||
|
// UI
|
||||||
|
compile 'com.github.dmytrodanylyk.android-process-button:library:1.0.4'
|
||||||
|
compile 'eu.davidea:flexible-adapter:4.2.0'
|
||||||
|
compile 'com.nononsenseapps:filepicker:2.5.2'
|
||||||
|
compile 'com.github.amulyakhare:TextDrawable:558677e'
|
||||||
|
compile 'com.afollestad.material-dialogs:core:0.8.5.9'
|
||||||
|
|
||||||
|
// Tests
|
||||||
|
testCompile 'junit:junit:4.12'
|
||||||
|
testCompile 'org.assertj:assertj-core:1.7.1'
|
||||||
|
testCompile "org.mockito:mockito-core:$MOCKITO_VERSION"
|
||||||
|
testCompile('org.robolectric:robolectric:3.0') {
|
||||||
|
exclude group: 'commons-logging', module: 'commons-logging'
|
||||||
|
exclude group: 'org.apache.httpcomponents', module: 'httpclient'
|
||||||
|
}
|
||||||
|
|
||||||
|
kaptTest "com.google.dagger:dagger-compiler:$DAGGER_VERSION"
|
||||||
|
compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||||
|
}
|
||||||
|
|
||||||
|
buildscript {
|
||||||
|
ext.kotlin_version = '1.0.2'
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
dependencies {
|
||||||
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
@ -1,315 +0,0 @@
|
|||||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
|
||||||
|
|
||||||
plugins {
|
|
||||||
id("com.android.application")
|
|
||||||
id("com.mikepenz.aboutlibraries.plugin")
|
|
||||||
kotlin("android")
|
|
||||||
kotlin("plugin.serialization")
|
|
||||||
id("com.github.zellius.shortcut-helper")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (gradle.startParameter.taskRequests.toString().contains("Standard")) {
|
|
||||||
apply<com.google.gms.googleservices.GoogleServicesPlugin>()
|
|
||||||
}
|
|
||||||
|
|
||||||
shortcutHelper.setFilePath("./shortcuts.xml")
|
|
||||||
|
|
||||||
val SUPPORTED_ABIS = setOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
|
|
||||||
|
|
||||||
android {
|
|
||||||
namespace = "eu.kanade.tachiyomi"
|
|
||||||
|
|
||||||
defaultConfig {
|
|
||||||
applicationId = "app.mihon"
|
|
||||||
|
|
||||||
versionCode = 1
|
|
||||||
versionName = "0.16.0"
|
|
||||||
|
|
||||||
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
|
||||||
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
|
|
||||||
buildConfigField("String", "BUILD_TIME", "\"${getBuildTime()}\"")
|
|
||||||
buildConfigField("boolean", "INCLUDE_UPDATER", "false")
|
|
||||||
buildConfigField("boolean", "PREVIEW", "false")
|
|
||||||
|
|
||||||
ndk {
|
|
||||||
abiFilters += SUPPORTED_ABIS
|
|
||||||
}
|
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
|
||||||
}
|
|
||||||
|
|
||||||
splits {
|
|
||||||
abi {
|
|
||||||
isEnable = true
|
|
||||||
reset()
|
|
||||||
include(*SUPPORTED_ABIS.toTypedArray())
|
|
||||||
isUniversalApk = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
buildTypes {
|
|
||||||
named("debug") {
|
|
||||||
versionNameSuffix = "-${getCommitCount()}"
|
|
||||||
applicationIdSuffix = ".debug"
|
|
||||||
isPseudoLocalesEnabled = true
|
|
||||||
}
|
|
||||||
named("release") {
|
|
||||||
isShrinkResources = true
|
|
||||||
isMinifyEnabled = true
|
|
||||||
proguardFiles("proguard-android-optimize.txt", "proguard-rules.pro")
|
|
||||||
}
|
|
||||||
create("preview") {
|
|
||||||
initWith(getByName("release"))
|
|
||||||
buildConfigField("boolean", "PREVIEW", "true")
|
|
||||||
|
|
||||||
signingConfig = signingConfigs.getByName("debug")
|
|
||||||
matchingFallbacks.add("release")
|
|
||||||
val debugType = getByName("debug")
|
|
||||||
versionNameSuffix = debugType.versionNameSuffix
|
|
||||||
applicationIdSuffix = debugType.applicationIdSuffix
|
|
||||||
}
|
|
||||||
create("benchmark") {
|
|
||||||
initWith(getByName("release"))
|
|
||||||
|
|
||||||
signingConfig = signingConfigs.getByName("debug")
|
|
||||||
matchingFallbacks.add("release")
|
|
||||||
isDebuggable = false
|
|
||||||
isProfileable = true
|
|
||||||
versionNameSuffix = "-benchmark"
|
|
||||||
applicationIdSuffix = ".benchmark"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sourceSets {
|
|
||||||
getByName("preview").res.srcDirs("src/debug/res")
|
|
||||||
getByName("benchmark").res.srcDirs("src/debug/res")
|
|
||||||
}
|
|
||||||
|
|
||||||
flavorDimensions.add("default")
|
|
||||||
|
|
||||||
productFlavors {
|
|
||||||
create("standard") {
|
|
||||||
buildConfigField("boolean", "INCLUDE_UPDATER", "true")
|
|
||||||
dimension = "default"
|
|
||||||
}
|
|
||||||
create("dev") {
|
|
||||||
// Include pseudolocales: https://developer.android.com/guide/topics/resources/pseudolocales
|
|
||||||
resourceConfigurations.addAll(listOf("en", "en_XA", "ar_XB", "xxhdpi"))
|
|
||||||
dimension = "default"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
packaging {
|
|
||||||
resources.excludes.addAll(
|
|
||||||
listOf(
|
|
||||||
"META-INF/DEPENDENCIES",
|
|
||||||
"LICENSE.txt",
|
|
||||||
"META-INF/LICENSE",
|
|
||||||
"META-INF/LICENSE.txt",
|
|
||||||
"META-INF/README.md",
|
|
||||||
"META-INF/NOTICE",
|
|
||||||
"META-INF/*.kotlin_module",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
dependenciesInfo {
|
|
||||||
includeInApk = false
|
|
||||||
}
|
|
||||||
|
|
||||||
buildFeatures {
|
|
||||||
viewBinding = true
|
|
||||||
compose = true
|
|
||||||
buildConfig = true
|
|
||||||
|
|
||||||
// Disable some unused things
|
|
||||||
aidl = false
|
|
||||||
renderScript = false
|
|
||||||
shaders = false
|
|
||||||
}
|
|
||||||
|
|
||||||
lint {
|
|
||||||
abortOnError = false
|
|
||||||
checkReleaseBuilds = false
|
|
||||||
}
|
|
||||||
|
|
||||||
composeOptions {
|
|
||||||
kotlinCompilerExtensionVersion = compose.versions.compiler.get()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation(project(":i18n"))
|
|
||||||
implementation(project(":core"))
|
|
||||||
implementation(project(":core-metadata"))
|
|
||||||
implementation(project(":source-api"))
|
|
||||||
implementation(project(":source-local"))
|
|
||||||
implementation(project(":data"))
|
|
||||||
implementation(project(":domain"))
|
|
||||||
implementation(project(":presentation-core"))
|
|
||||||
implementation(project(":presentation-widget"))
|
|
||||||
|
|
||||||
// Compose
|
|
||||||
implementation(platform(compose.bom))
|
|
||||||
implementation(compose.activity)
|
|
||||||
implementation(compose.foundation)
|
|
||||||
implementation(compose.material3.core)
|
|
||||||
implementation(compose.material.core)
|
|
||||||
implementation(compose.material.icons)
|
|
||||||
implementation(compose.animation)
|
|
||||||
implementation(compose.animation.graphics)
|
|
||||||
debugImplementation(compose.ui.tooling)
|
|
||||||
implementation(compose.ui.tooling.preview)
|
|
||||||
implementation(compose.ui.util)
|
|
||||||
implementation(compose.accompanist.webview)
|
|
||||||
implementation(compose.accompanist.systemuicontroller)
|
|
||||||
lintChecks(compose.lintchecks)
|
|
||||||
|
|
||||||
implementation(androidx.paging.runtime)
|
|
||||||
implementation(androidx.paging.compose)
|
|
||||||
|
|
||||||
implementation(libs.bundles.sqlite)
|
|
||||||
|
|
||||||
implementation(kotlinx.reflect)
|
|
||||||
implementation(kotlinx.immutables)
|
|
||||||
|
|
||||||
implementation(platform(kotlinx.coroutines.bom))
|
|
||||||
implementation(kotlinx.bundles.coroutines)
|
|
||||||
|
|
||||||
// AndroidX libraries
|
|
||||||
implementation(androidx.annotation)
|
|
||||||
implementation(androidx.appcompat)
|
|
||||||
implementation(androidx.biometricktx)
|
|
||||||
implementation(androidx.constraintlayout)
|
|
||||||
implementation(androidx.corektx)
|
|
||||||
implementation(androidx.splashscreen)
|
|
||||||
implementation(androidx.recyclerview)
|
|
||||||
implementation(androidx.viewpager)
|
|
||||||
implementation(androidx.profileinstaller)
|
|
||||||
|
|
||||||
implementation(androidx.bundles.lifecycle)
|
|
||||||
|
|
||||||
// Job scheduling
|
|
||||||
implementation(androidx.workmanager)
|
|
||||||
|
|
||||||
// RxJava
|
|
||||||
implementation(libs.rxjava)
|
|
||||||
|
|
||||||
// Networking
|
|
||||||
implementation(libs.bundles.okhttp)
|
|
||||||
implementation(libs.okio)
|
|
||||||
implementation(libs.conscrypt.android) // TLS 1.3 support for Android < 10
|
|
||||||
|
|
||||||
// Data serialization (JSON, protobuf, xml)
|
|
||||||
implementation(kotlinx.bundles.serialization)
|
|
||||||
|
|
||||||
// HTML parser
|
|
||||||
implementation(libs.jsoup)
|
|
||||||
|
|
||||||
// Disk
|
|
||||||
implementation(libs.disklrucache)
|
|
||||||
implementation(libs.unifile)
|
|
||||||
implementation(libs.junrar)
|
|
||||||
|
|
||||||
// Preferences
|
|
||||||
implementation(libs.preferencektx)
|
|
||||||
|
|
||||||
// Dependency injection
|
|
||||||
implementation(libs.injekt.core)
|
|
||||||
|
|
||||||
// Image loading
|
|
||||||
implementation(platform(libs.coil.bom))
|
|
||||||
implementation(libs.bundles.coil)
|
|
||||||
implementation(libs.subsamplingscaleimageview) {
|
|
||||||
exclude(module = "image-decoder")
|
|
||||||
}
|
|
||||||
implementation(libs.image.decoder)
|
|
||||||
|
|
||||||
// UI libraries
|
|
||||||
implementation(libs.material)
|
|
||||||
implementation(libs.flexible.adapter.core)
|
|
||||||
implementation(libs.photoview)
|
|
||||||
implementation(libs.directionalviewpager) {
|
|
||||||
exclude(group = "androidx.viewpager", module = "viewpager")
|
|
||||||
}
|
|
||||||
implementation(libs.insetter)
|
|
||||||
implementation(libs.bundles.richtext)
|
|
||||||
implementation(libs.aboutLibraries.compose)
|
|
||||||
implementation(libs.bundles.voyager)
|
|
||||||
implementation(libs.compose.materialmotion)
|
|
||||||
implementation(libs.swipe)
|
|
||||||
|
|
||||||
// Logging
|
|
||||||
implementation(libs.logcat)
|
|
||||||
|
|
||||||
// Crash reports/analytics
|
|
||||||
"standardImplementation"(libs.firebase.analytics)
|
|
||||||
|
|
||||||
// Shizuku
|
|
||||||
implementation(libs.bundles.shizuku)
|
|
||||||
|
|
||||||
// Tests
|
|
||||||
testImplementation(libs.bundles.test)
|
|
||||||
|
|
||||||
// For detecting memory leaks; see https://square.github.io/leakcanary/
|
|
||||||
// debugImplementation(libs.leakcanary.android)
|
|
||||||
implementation(libs.leakcanary.plumber)
|
|
||||||
}
|
|
||||||
|
|
||||||
androidComponents {
|
|
||||||
beforeVariants { variantBuilder ->
|
|
||||||
// Disables standardBenchmark
|
|
||||||
if (variantBuilder.buildType == "benchmark") {
|
|
||||||
variantBuilder.enable = variantBuilder.productFlavors.containsAll(
|
|
||||||
listOf("default" to "dev"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onVariants(selector().withFlavor("default" to "standard")) {
|
|
||||||
// Only excluding in standard flavor because this breaks
|
|
||||||
// Layout Inspector's Compose tree
|
|
||||||
it.packaging.resources.excludes.add("META-INF/*.version")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks {
|
|
||||||
// See https://kotlinlang.org/docs/reference/experimental.html#experimental-status-of-experimental-api(-markers)
|
|
||||||
withType<KotlinCompile> {
|
|
||||||
kotlinOptions.freeCompilerArgs += listOf(
|
|
||||||
"-Xcontext-receivers",
|
|
||||||
"-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi",
|
|
||||||
"-opt-in=androidx.compose.material.ExperimentalMaterialApi",
|
|
||||||
"-opt-in=androidx.compose.material3.ExperimentalMaterial3Api",
|
|
||||||
"-opt-in=androidx.compose.material.ExperimentalMaterialApi",
|
|
||||||
"-opt-in=androidx.compose.ui.ExperimentalComposeUiApi",
|
|
||||||
"-opt-in=androidx.compose.foundation.ExperimentalFoundationApi",
|
|
||||||
"-opt-in=androidx.compose.animation.ExperimentalAnimationApi",
|
|
||||||
"-opt-in=androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi",
|
|
||||||
"-opt-in=coil.annotation.ExperimentalCoilApi",
|
|
||||||
"-opt-in=com.google.accompanist.permissions.ExperimentalPermissionsApi",
|
|
||||||
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
|
||||||
"-opt-in=kotlinx.coroutines.FlowPreview",
|
|
||||||
"-opt-in=kotlinx.coroutines.InternalCoroutinesApi",
|
|
||||||
"-opt-in=kotlinx.serialization.ExperimentalSerializationApi",
|
|
||||||
)
|
|
||||||
|
|
||||||
if (project.findProperty("tachiyomi.enableComposeCompilerMetrics") == "true") {
|
|
||||||
kotlinOptions.freeCompilerArgs += listOf(
|
|
||||||
"-P",
|
|
||||||
"plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=" +
|
|
||||||
project.layout.buildDirectory.dir("compose_metrics").get().asFile.absolutePath,
|
|
||||||
)
|
|
||||||
kotlinOptions.freeCompilerArgs += listOf(
|
|
||||||
"-P",
|
|
||||||
"plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=" +
|
|
||||||
project.layout.buildDirectory.dir("compose_metrics").get().asFile.absolutePath,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
buildscript {
|
|
||||||
dependencies {
|
|
||||||
classpath(kotlinx.gradle)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,34 +0,0 @@
|
|||||||
-dontusemixedcaseclassnames
|
|
||||||
-ignorewarnings
|
|
||||||
-verbose
|
|
||||||
|
|
||||||
-keepattributes *Annotation*
|
|
||||||
|
|
||||||
-keepclasseswithmembernames,includedescriptorclasses class * {
|
|
||||||
native <methods>;
|
|
||||||
}
|
|
||||||
|
|
||||||
-keepclassmembers enum * {
|
|
||||||
public static **[] values();
|
|
||||||
public static ** valueOf(java.lang.String);
|
|
||||||
}
|
|
||||||
|
|
||||||
-keepclassmembers class * implements android.os.Parcelable {
|
|
||||||
public static final ** CREATOR;
|
|
||||||
}
|
|
||||||
|
|
||||||
-keep class androidx.annotation.Keep
|
|
||||||
|
|
||||||
-keep @androidx.annotation.Keep class * {*;}
|
|
||||||
|
|
||||||
-keepclasseswithmembers class * {
|
|
||||||
@androidx.annotation.Keep <methods>;
|
|
||||||
}
|
|
||||||
|
|
||||||
-keepclasseswithmembers class * {
|
|
||||||
@androidx.annotation.Keep <fields>;
|
|
||||||
}
|
|
||||||
|
|
||||||
-keepclasseswithmembers class * {
|
|
||||||
@androidx.annotation.Keep <init>(...);
|
|
||||||
}
|
|
148
app/proguard-rules.pro
vendored
148
app/proguard-rules.pro
vendored
@ -1,30 +1,30 @@
|
|||||||
-dontobfuscate
|
-dontobfuscate
|
||||||
|
|
||||||
-keep,allowoptimization class eu.kanade.**
|
-keep class eu.kanade.tachiyomi.injection.** { *; }
|
||||||
-keep,allowoptimization class tachiyomi.**
|
|
||||||
|
|
||||||
# Keep common dependencies used in extensions
|
# OkHttp
|
||||||
-keep,allowoptimization class androidx.preference.** { public protected *; }
|
-keepattributes Signature
|
||||||
-keep,allowoptimization class kotlin.** { public protected *; }
|
-keepattributes *Annotation*
|
||||||
-keep,allowoptimization class kotlinx.coroutines.** { public protected *; }
|
-keep class okhttp3.** { *; }
|
||||||
-keep,allowoptimization class kotlinx.serialization.** { public protected *; }
|
-keep interface okhttp3.** { *; }
|
||||||
-keep,allowoptimization class kotlin.time.** { public protected *; }
|
-dontwarn okhttp3.**
|
||||||
-keep,allowoptimization class okhttp3.** { public protected *; }
|
-dontwarn okio.**
|
||||||
-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
|
# Okio
|
||||||
-keep,allowoptimization class eu.kanade.tachiyomi.network.interceptor.RateLimitInterceptorKt { public protected *; }
|
-keep class sun.misc.Unsafe { *; }
|
||||||
-keep,allowoptimization class eu.kanade.tachiyomi.network.interceptor.SpecificHostRateLimitInterceptorKt { public protected *; }
|
-dontwarn java.nio.file.*
|
||||||
-keep,allowoptimization class eu.kanade.tachiyomi.network.NetworkHelper { public protected *; }
|
-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
|
||||||
-keep,allowoptimization class eu.kanade.tachiyomi.network.OkHttpExtensionsKt { public protected *; }
|
-dontwarn okio.**
|
||||||
-keep,allowoptimization class eu.kanade.tachiyomi.network.RequestsKt { public protected *; }
|
|
||||||
-keep,allowoptimization class eu.kanade.tachiyomi.AppInfo { public protected *; }
|
|
||||||
|
|
||||||
##---------------Begin: proguard configuration for RxJava 1.x ----------
|
# Glide specific rules #
|
||||||
|
# https://github.com/bumptech/glide
|
||||||
|
-keep public class * implements com.bumptech.glide.module.GlideModule
|
||||||
|
-keep public enum com.bumptech.glide.load.resource.bitmap.ImageHeaderParser$** {
|
||||||
|
**[] $VALUES;
|
||||||
|
public *;
|
||||||
|
}
|
||||||
|
|
||||||
|
# RxJava 1.1.0
|
||||||
-dontwarn sun.misc.**
|
-dontwarn sun.misc.**
|
||||||
|
|
||||||
-keepclassmembers class rx.internal.util.unsafe.*ArrayQueue*Field* {
|
-keepclassmembers class rx.internal.util.unsafe.*ArrayQueue*Field* {
|
||||||
@ -40,38 +40,90 @@
|
|||||||
rx.internal.util.atomic.LinkedQueueNode consumerNode;
|
rx.internal.util.atomic.LinkedQueueNode consumerNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
-dontnote rx.internal.util.PlatformDependent
|
# Retrofit 2.X
|
||||||
##---------------End: proguard configuration for RxJava 1.x ----------
|
## https://square.github.io/retrofit/ ##
|
||||||
|
|
||||||
##---------------Begin: proguard configuration for kotlinx.serialization ----------
|
-dontwarn retrofit2.**
|
||||||
-keepattributes *Annotation*, InnerClasses
|
-keep class retrofit2.** { *; }
|
||||||
-dontnote kotlinx.serialization.** # core serialization annotations
|
-keepattributes Signature
|
||||||
|
-keepattributes Exceptions
|
||||||
|
|
||||||
# kotlinx-serialization-json specific. Add this if you have java.lang.NoClassDefFoundError kotlinx.serialization.json.JsonObjectSerializer
|
-keepclasseswithmembers class * {
|
||||||
-keepclassmembers class kotlinx.serialization.json.** {
|
@retrofit2.http.* <methods>;
|
||||||
*** Companion;
|
|
||||||
}
|
|
||||||
-keepclasseswithmembers class kotlinx.serialization.json.** {
|
|
||||||
kotlinx.serialization.KSerializer serializer(...);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
-keep,includedescriptorclasses class eu.kanade.**$$serializer { *; }
|
# AppCombat
|
||||||
-keepclassmembers class eu.kanade.** {
|
-keep public class android.support.v7.widget.** { *; }
|
||||||
*** Companion;
|
-keep public class android.support.v7.internal.widget.** { *; }
|
||||||
}
|
-keep public class android.support.v7.internal.view.menu.** { *; }
|
||||||
-keepclasseswithmembers class eu.kanade.** {
|
|
||||||
kotlinx.serialization.KSerializer serializer(...);
|
-keep public class * extends android.support.v4.view.ActionProvider {
|
||||||
|
public <init>(android.content.Context);
|
||||||
}
|
}
|
||||||
|
|
||||||
-keep class kotlinx.serialization.**
|
## GSON 2.2.4 specific rules ##
|
||||||
-keepclassmembers class kotlinx.serialization.** {
|
|
||||||
<methods>;
|
# Gson uses generic type information stored in a class file when working with fields. Proguard
|
||||||
|
# removes such information by default, so configure it to keep all of it.
|
||||||
|
-keepattributes Signature
|
||||||
|
|
||||||
|
# For using GSON @Expose annotation
|
||||||
|
-keepattributes *Annotation*
|
||||||
|
|
||||||
|
-keepattributes EnclosingMethod
|
||||||
|
|
||||||
|
# Gson specific classes
|
||||||
|
-keep class sun.misc.Unsafe { *; }
|
||||||
|
-keep class com.google.gson.stream.** { *; }
|
||||||
|
|
||||||
|
## ACRA 4.5.0 specific rules ##
|
||||||
|
|
||||||
|
# we need line numbers in our stack traces otherwise they are pretty useless
|
||||||
|
-renamesourcefileattribute SourceFile
|
||||||
|
-keepattributes SourceFile,LineNumberTable
|
||||||
|
|
||||||
|
# ACRA needs "annotations" so add this...
|
||||||
|
-keepattributes *Annotation*
|
||||||
|
|
||||||
|
# keep this class so that logging will show 'ACRA' and not a obfuscated name like 'a'.
|
||||||
|
# Note: if you are removing log messages elsewhere in this file then this isn't necessary
|
||||||
|
-keep class org.acra.ACRA {
|
||||||
|
*;
|
||||||
}
|
}
|
||||||
##---------------End: proguard configuration for kotlinx.serialization ----------
|
|
||||||
|
|
||||||
# XmlUtil
|
# keep this around for some enums that ACRA needs
|
||||||
-keep public enum nl.adaptivity.xmlutil.EventType { *; }
|
-keep class org.acra.ReportingInteractionMode {
|
||||||
|
*;
|
||||||
|
}
|
||||||
|
|
||||||
# Firebase
|
-keepnames class org.acra.sender.HttpSender$** {
|
||||||
-keep class com.google.firebase.installations.** { *; }
|
*;
|
||||||
-keep interface com.google.firebase.installations.** { *; }
|
}
|
||||||
|
|
||||||
|
-keepnames class org.acra.ReportField {
|
||||||
|
*;
|
||||||
|
}
|
||||||
|
|
||||||
|
# keep this otherwise it is removed by ProGuard
|
||||||
|
-keep public class org.acra.ErrorReporter {
|
||||||
|
public void addCustomData(java.lang.String,java.lang.String);
|
||||||
|
public void putCustomData(java.lang.String,java.lang.String);
|
||||||
|
public void removeCustomData(java.lang.String);
|
||||||
|
}
|
||||||
|
|
||||||
|
# keep this otherwise it is removed by ProGuard
|
||||||
|
-keep public class org.acra.ErrorReporter {
|
||||||
|
public void handleSilentException(java.lang.Throwable);
|
||||||
|
}
|
||||||
|
|
||||||
|
# Keep the support library
|
||||||
|
-keep class org.acra.** { *; }
|
||||||
|
-keep interface org.acra.** { *; }
|
||||||
|
|
||||||
|
# SnakeYaml
|
||||||
|
-keep class org.yaml.snakeyaml.** { public protected private *; }
|
||||||
|
-keep class org.yaml.snakeyaml.** { public protected private *; }
|
||||||
|
-dontwarn org.yaml.snakeyaml.**
|
||||||
|
|
||||||
|
# Duktape
|
||||||
|
-keep class com.squareup.duktape.** { *; }
|
@ -1,46 +0,0 @@
|
|||||||
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<shortcut
|
|
||||||
android:enabled="true"
|
|
||||||
android:icon="@drawable/sc_collections_bookmark_48dp"
|
|
||||||
android:shortcutDisabledMessage="@string/app_not_available"
|
|
||||||
android:shortcutId="show_library"
|
|
||||||
android:shortcutLongLabel="@string/label_library"
|
|
||||||
android:shortcutShortLabel="@string/label_library">
|
|
||||||
<intent
|
|
||||||
android:action="eu.kanade.tachiyomi.SHOW_LIBRARY"
|
|
||||||
android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity" />
|
|
||||||
</shortcut>
|
|
||||||
<shortcut
|
|
||||||
android:enabled="true"
|
|
||||||
android:icon="@drawable/sc_new_releases_48dp"
|
|
||||||
android:shortcutDisabledMessage="@string/app_not_available"
|
|
||||||
android:shortcutId="show_recently_updated"
|
|
||||||
android:shortcutLongLabel="@string/label_recent_updates"
|
|
||||||
android:shortcutShortLabel="@string/label_recent_updates">
|
|
||||||
<intent
|
|
||||||
android:action="eu.kanade.tachiyomi.SHOW_RECENTLY_UPDATED"
|
|
||||||
android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity" />
|
|
||||||
</shortcut>
|
|
||||||
<shortcut
|
|
||||||
android:enabled="true"
|
|
||||||
android:icon="@drawable/sc_history_48dp"
|
|
||||||
android:shortcutDisabledMessage="@string/app_not_available"
|
|
||||||
android:shortcutId="show_recently_read"
|
|
||||||
android:shortcutLongLabel="@string/label_recent_manga"
|
|
||||||
android:shortcutShortLabel="@string/label_recent_manga">
|
|
||||||
<intent
|
|
||||||
android:action="eu.kanade.tachiyomi.SHOW_RECENTLY_READ"
|
|
||||||
android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity" />
|
|
||||||
</shortcut>
|
|
||||||
<shortcut
|
|
||||||
android:enabled="true"
|
|
||||||
android:icon="@drawable/sc_explore_48dp"
|
|
||||||
android:shortcutDisabledMessage="@string/app_not_available"
|
|
||||||
android:shortcutId="show_catalogues"
|
|
||||||
android:shortcutLongLabel="@string/browse"
|
|
||||||
android:shortcutShortLabel="@string/browse">
|
|
||||||
<intent
|
|
||||||
android:action="eu.kanade.tachiyomi.SHOW_CATALOGUES"
|
|
||||||
android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity" />
|
|
||||||
</shortcut>
|
|
||||||
</shortcuts>
|
|
@ -1,21 +0,0 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="108dp"
|
|
||||||
android:height="108dp"
|
|
||||||
android:viewportWidth="432"
|
|
||||||
android:viewportHeight="432">
|
|
||||||
<group android:scaleX="0.67"
|
|
||||||
android:scaleY="0.67"
|
|
||||||
android:translateX="71.28"
|
|
||||||
android:translateY="71.28">
|
|
||||||
<path
|
|
||||||
android:pathData="M0,0h432v432h-432z"
|
|
||||||
android:fillColor="#2E3943"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M377,216C377,302.16 304.92,372 216,372C127.08,372 55,302.16 55,216C55,129.84 127.08,60 216,60C304.92,60 377,129.84 377,216Z"
|
|
||||||
android:fillColor="#F2FAFF"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M216,342.75C288.25,342.75 346.81,286 346.81,216C346.81,146 288.25,89.25 216,89.25C143.75,89.25 85.19,146 85.19,216C85.19,286 143.75,342.75 216,342.75ZM216,372C304.92,372 377,302.16 377,216C377,129.84 304.92,60 216,60C127.08,60 55,129.84 55,216C55,302.16 127.08,372 216,372Z"
|
|
||||||
android:fillColor="#7EBBED"
|
|
||||||
android:fillType="evenOdd"/>
|
|
||||||
</group>
|
|
||||||
</vector>
|
|
@ -1,14 +0,0 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="108dp"
|
|
||||||
android:height="108dp"
|
|
||||||
android:viewportWidth="432"
|
|
||||||
android:viewportHeight="432">
|
|
||||||
<group android:scaleX="0.67"
|
|
||||||
android:scaleY="0.67"
|
|
||||||
android:translateX="71.28"
|
|
||||||
android:translateY="71.28">
|
|
||||||
<path
|
|
||||||
android:pathData="M162.3,173.59L161.22,148.63C164.47,149.25 168.35,149.41 177.18,149.41C187.87,149.41 201.98,148.79 209.26,147.86C212.36,147.55 213.6,147.09 215.61,146L232.35,160.26C230.8,162.43 230.34,163.36 228.63,167.7C227.24,171.11 220.88,190.79 218.4,199.16C229.87,201.48 236.22,203.18 244.9,206.75C245.99,199.16 246.14,195.13 246.14,181.33C246.14,177.77 245.99,175.76 245.52,172.5L272.49,173.43C271.71,177.15 271.56,178.7 271.4,184.74C270.78,199.31 270.16,206.29 268.61,216.82C279.31,222.25 279.31,222.25 284.73,225.19C287.52,226.74 288.14,227.05 290,227.67L281.01,256.65C276.67,252.78 270.63,248.59 261.8,243.63C254.05,262.08 241.18,275.56 221.66,286.25C215.15,277.57 210.19,272.3 202.29,266.11C213.75,260.68 219.02,257.27 225.07,251.54C230.96,245.8 234.83,240.22 238.55,231.85C228.63,227.36 222.28,225.35 211.27,223.02C204.92,241.93 199.8,254.02 195.31,261.3C189.27,271.06 181.05,276.18 171.6,276.18C164.32,276.18 156.88,272.92 151.45,267.35C145.25,260.99 142,252.16 142,241.93C142,226.74 149.28,213.57 161.99,205.35C170.21,200.09 178.88,197.76 192.68,196.99C195.47,187.84 197.79,179.94 199.96,171.11C193.14,171.73 184.62,172.19 174.24,172.65C168.66,172.81 166.8,172.96 162.3,173.59ZM185.86,220.7C178.57,221.94 174.24,224.26 170.36,229.22C167.42,232.63 166.02,236.66 166.02,241C166.02,245.8 168.35,249.37 171.29,249.37C174.85,249.37 178.88,241.31 185.86,220.7Z"
|
|
||||||
android:fillColor="#031019"/>
|
|
||||||
</group>
|
|
||||||
</vector>
|
|
@ -1,245 +1,108 @@
|
|||||||
<?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 -->
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
|
||||||
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
<!-- Storage -->
|
|
||||||
<uses-permission
|
|
||||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
|
||||||
tools:ignore="ScopedStorage" />
|
|
||||||
|
|
||||||
<!-- For background jobs -->
|
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||||
|
|
||||||
<!-- For managing extensions -->
|
|
||||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
|
||||||
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
|
|
||||||
<uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" />
|
|
||||||
<!-- To view extension packages in API 30+ -->
|
|
||||||
<uses-permission
|
|
||||||
android:name="android.permission.QUERY_ALL_PACKAGES"
|
|
||||||
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 permission from Firebase dependency -->
|
|
||||||
<uses-permission
|
|
||||||
android:name="com.google.android.gms.permission.AD_ID"
|
|
||||||
tools:node="remove" />
|
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".App"
|
android:name=".App"
|
||||||
android:allowBackup="false"
|
android:allowBackup="true"
|
||||||
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:theme="@style/Theme.Tachiyomi" >
|
||||||
android:networkSecurityConfig="@xml/network_security_config"
|
|
||||||
android:preserveLegacyExternalStorage="true"
|
|
||||||
android:requestLegacyExternalStorage="true"
|
|
||||||
android:roundIcon="@mipmap/ic_launcher"
|
|
||||||
android:supportsRtl="true"
|
|
||||||
android:theme="@style/Theme.Tachiyomi">
|
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.main.MainActivity"
|
android:name=".ui.main.MainActivity"
|
||||||
android:exported="true"
|
android:theme="@style/Theme.BrandedLaunch">
|
||||||
android:launchMode="singleTop"
|
|
||||||
android:theme="@style/Theme.Tachiyomi.SplashScreen">
|
|
||||||
<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 -->
|
|
||||||
<meta-data
|
|
||||||
android:name="android.app.shortcuts"
|
|
||||||
android:resource="@xml/shortcuts" />
|
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".crash.CrashActivity"
|
android:name=".ui.manga.MangaActivity"
|
||||||
android:exported="false"
|
android:parentActivityName=".ui.main.MainActivity" >
|
||||||
android:process=":error_handler" />
|
|
||||||
|
|
||||||
<activity
|
|
||||||
android:name=".ui.deeplink.DeepLinkActivity"
|
|
||||||
android:exported="true"
|
|
||||||
android:label="@string/action_search"
|
|
||||||
android:launchMode="singleTask"
|
|
||||||
android:theme="@android:style/Theme.NoDisplay">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.SEARCH" />
|
|
||||||
<action android:name="com.google.android.gms.actions.SEARCH_ACTION" />
|
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
</intent-filter>
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="eu.kanade.tachiyomi.SEARCH" />
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
</intent-filter>
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.SEND" />
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
<data android:mimeType="text/plain" />
|
|
||||||
</intent-filter>
|
|
||||||
|
|
||||||
<meta-data
|
|
||||||
android:name="android.app.searchable"
|
|
||||||
android:resource="@xml/searchable" />
|
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.reader.ReaderActivity"
|
android:name=".ui.reader.ReaderActivity"
|
||||||
android:exported="false"
|
android:theme="@style/Theme.Reader">
|
||||||
android:launchMode="singleTask">
|
</activity>
|
||||||
<intent-filter>
|
<activity
|
||||||
<action android:name="com.samsung.android.support.REMOTE_ACTION" />
|
android:name=".ui.setting.SettingsActivity"
|
||||||
</intent-filter>
|
android:label="@string/label_settings"
|
||||||
|
android:parentActivityName=".ui.main.MainActivity" >
|
||||||
<meta-data
|
</activity>
|
||||||
android:name="com.samsung.android.support.REMOTE_ACTION"
|
<activity
|
||||||
android:resource="@xml/s_pen_actions" />
|
android:name=".ui.category.CategoryActivity"
|
||||||
|
android:label="@string/label_categories"
|
||||||
|
android:parentActivityName=".ui.main.MainActivity">
|
||||||
|
</activity>
|
||||||
|
<activity
|
||||||
|
android:name=".ui.setting.SettingsDownloadsFragment$CustomLayoutPickerActivity"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:theme="@style/FilePickerTheme">
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<activity
|
<service android:name=".data.library.LibraryUpdateService"
|
||||||
android:name=".ui.security.UnlockActivity"
|
android:exported="false"/>
|
||||||
android:exported="false"
|
|
||||||
android:theme="@style/Theme.Tachiyomi" />
|
|
||||||
|
|
||||||
<activity
|
<service android:name=".data.download.DownloadService"
|
||||||
android:name=".ui.webview.WebViewActivity"
|
android:exported="false"/>
|
||||||
android:configChanges="uiMode|orientation|screenSize"
|
|
||||||
android:exported="false" />
|
|
||||||
|
|
||||||
<activity
|
<service android:name=".data.mangasync.UpdateMangaSyncService"
|
||||||
android:name=".extension.util.ExtensionInstallActivity"
|
android:exported="false"/>
|
||||||
android:exported="false"
|
|
||||||
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
|
|
||||||
|
|
||||||
<activity
|
|
||||||
android:name=".ui.setting.track.TrackLoginActivity"
|
|
||||||
android:exported="true"
|
|
||||||
android:label="@string/track_activity_name">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.VIEW" />
|
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
|
||||||
|
|
||||||
<data android:scheme="mihon" />
|
|
||||||
|
|
||||||
<data android:host="anilist-auth" />
|
|
||||||
<data android:host="bangumi-auth" />
|
|
||||||
<data android:host="myanimelist-auth" />
|
|
||||||
<data android:host="shikimori-auth" />
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
|
||||||
|
|
||||||
<receiver
|
<receiver
|
||||||
android:name=".data.notification.NotificationReceiver"
|
android:name=".data.library.LibraryUpdateService$SyncOnConnectionAvailable"
|
||||||
android:exported="false" />
|
android:enabled="false">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.net.conn.CONNECTIVITY_CHANGE" />
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
|
|
||||||
<service
|
<receiver
|
||||||
android:name=".extension.util.ExtensionInstallService"
|
android:name=".data.library.LibraryUpdateService$SyncOnPowerConnected"
|
||||||
android:exported="false"
|
android:enabled="false">
|
||||||
android:foregroundServiceType="shortService" />
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.ACTION_POWER_CONNECTED" />
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
|
|
||||||
<service
|
<receiver
|
||||||
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
|
android:name=".data.library.LibraryUpdateService$CancelUpdateReceiver">
|
||||||
android:enabled="false"
|
</receiver>
|
||||||
android:exported="false">
|
|
||||||
<meta-data
|
|
||||||
android:name="autoStoreLocales"
|
|
||||||
android:value="true" />
|
|
||||||
</service>
|
|
||||||
|
|
||||||
<service
|
<receiver
|
||||||
android:name="androidx.work.impl.foreground.SystemForegroundService"
|
android:name=".data.updater.UpdateDownloader$InstallOnReceived">
|
||||||
android:foregroundServiceType="dataSync"
|
</receiver>
|
||||||
tools:node="merge" />
|
|
||||||
|
|
||||||
<provider
|
<receiver
|
||||||
android:name="androidx.core.content.FileProvider"
|
android:name=".data.library.LibraryUpdateAlarm">
|
||||||
android:authorities="${applicationId}.provider"
|
<intent-filter>
|
||||||
android:exported="false"
|
<action android:name="android.intent.action.BOOT_COMPLETED"/>
|
||||||
android:grantUriPermissions="true">
|
<action android:name="eu.kanade.UPDATE_LIBRARY" />
|
||||||
<meta-data
|
</intent-filter>
|
||||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
</receiver>
|
||||||
android:resource="@xml/provider_paths" />
|
|
||||||
</provider>
|
<receiver
|
||||||
|
android:name=".data.updater.UpdateDownloaderAlarm">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.BOOT_COMPLETED"/>
|
||||||
|
<action android:name="eu.kanade.CHECK_UPDATE"/>
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
|
|
||||||
<provider
|
|
||||||
android:name="rikka.shizuku.ShizukuProvider"
|
|
||||||
android:authorities="${applicationId}.shizuku"
|
|
||||||
android:enabled="true"
|
|
||||||
android:exported="true"
|
|
||||||
android:multiprocess="false"
|
|
||||||
android:permission="android.permission.INTERACT_ACROSS_USERS_FULL" />
|
|
||||||
|
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.webkit.WebView.EnableSafeBrowsing"
|
android:name="eu.kanade.tachiyomi.data.glide.AppGlideModule"
|
||||||
android:value="false" />
|
android:value="GlideModule" />
|
||||||
<meta-data
|
|
||||||
android:name="android.webkit.WebView.MetricsOptOut"
|
|
||||||
android:value="true" />
|
|
||||||
|
|
||||||
<!-- Disable advertising ID collection for Firebase -->
|
|
||||||
<meta-data
|
|
||||||
android:name="google_analytics_adid_collection_enabled"
|
|
||||||
android:value="false" />
|
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
|
BIN
app/src/main/assets/fonts/PTSans-Narrow.ttf
Normal file
BIN
app/src/main/assets/fonts/PTSans-Narrow.ttf
Normal file
Binary file not shown.
BIN
app/src/main/assets/fonts/PTSans-NarrowBold.ttf
Normal file
BIN
app/src/main/assets/fonts/PTSans-NarrowBold.ttf
Normal file
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 25 KiB |
@ -1,10 +0,0 @@
|
|||||||
package eu.kanade.core.preference
|
|
||||||
|
|
||||||
import androidx.compose.ui.state.ToggleableState
|
|
||||||
import tachiyomi.core.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.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,138 +0,0 @@
|
|||||||
package eu.kanade.core.util
|
|
||||||
|
|
||||||
import androidx.compose.ui.util.fastForEach
|
|
||||||
import kotlin.contracts.ExperimentalContracts
|
|
||||||
import kotlin.contracts.contract
|
|
||||||
|
|
||||||
fun <T : R, R : Any> List<T>.insertSeparators(
|
|
||||||
generator: (T?, T?) -> R?,
|
|
||||||
): List<R> {
|
|
||||||
if (isEmpty()) return emptyList()
|
|
||||||
val newList = mutableListOf<R>()
|
|
||||||
for (i in -1..lastIndex) {
|
|
||||||
val before = getOrNull(i)
|
|
||||||
before?.let(newList::add)
|
|
||||||
val after = getOrNull(i + 1)
|
|
||||||
val separator = generator.invoke(before, after)
|
|
||||||
separator?.let(newList::add)
|
|
||||||
}
|
|
||||||
return newList
|
|
||||||
}
|
|
||||||
|
|
||||||
fun <E> HashSet<E>.addOrRemove(value: E, shouldAdd: Boolean) {
|
|
||||||
if (shouldAdd) {
|
|
||||||
add(value)
|
|
||||||
} else {
|
|
||||||
remove(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a list containing only elements matching the given [predicate].
|
|
||||||
*
|
|
||||||
* **Do not use for collections that come from public APIs**, since they may not support random
|
|
||||||
* access in an efficient way, and this method may actually be a lot slower. Only use for
|
|
||||||
* collections that are created by code we control and are known to support random access.
|
|
||||||
*/
|
|
||||||
@OptIn(ExperimentalContracts::class)
|
|
||||||
inline fun <T> List<T>.fastFilter(predicate: (T) -> Boolean): List<T> {
|
|
||||||
contract { callsInPlace(predicate) }
|
|
||||||
val destination = ArrayList<T>()
|
|
||||||
fastForEach { if (predicate(it)) destination.add(it) }
|
|
||||||
return destination
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a list containing all elements not matching the given [predicate].
|
|
||||||
*
|
|
||||||
* **Do not use for collections that come from public APIs**, since they may not support random
|
|
||||||
* access in an efficient way, and this method may actually be a lot slower. Only use for
|
|
||||||
* collections that are created by code we control and are known to support random access.
|
|
||||||
*/
|
|
||||||
@OptIn(ExperimentalContracts::class)
|
|
||||||
inline fun <T> List<T>.fastFilterNot(predicate: (T) -> Boolean): List<T> {
|
|
||||||
contract { callsInPlace(predicate) }
|
|
||||||
val destination = ArrayList<T>()
|
|
||||||
fastForEach { if (!predicate(it)) destination.add(it) }
|
|
||||||
return destination
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a list containing only the non-null results of applying the
|
|
||||||
* given [transform] function to each element in the original collection.
|
|
||||||
*
|
|
||||||
* **Do not use for collections that come from public APIs**, since they may not support random
|
|
||||||
* access in an efficient way, and this method may actually be a lot slower. Only use for
|
|
||||||
* collections that are created by code we control and are known to support random access.
|
|
||||||
*/
|
|
||||||
@OptIn(ExperimentalContracts::class)
|
|
||||||
inline fun <T, R> List<T>.fastMapNotNull(transform: (T) -> R?): List<R> {
|
|
||||||
contract { callsInPlace(transform) }
|
|
||||||
val destination = ArrayList<R>()
|
|
||||||
fastForEach { element ->
|
|
||||||
transform(element)?.let(destination::add)
|
|
||||||
}
|
|
||||||
return destination
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Splits the original collection into pair of lists,
|
|
||||||
* where *first* list contains elements for which [predicate] yielded `true`,
|
|
||||||
* while *second* list contains elements for which [predicate] yielded `false`.
|
|
||||||
*
|
|
||||||
* **Do not use for collections that come from public APIs**, since they may not support random
|
|
||||||
* access in an efficient way, and this method may actually be a lot slower. Only use for
|
|
||||||
* collections that are created by code we control and are known to support random access.
|
|
||||||
*/
|
|
||||||
@OptIn(ExperimentalContracts::class)
|
|
||||||
inline fun <T> List<T>.fastPartition(predicate: (T) -> Boolean): Pair<List<T>, List<T>> {
|
|
||||||
contract { callsInPlace(predicate) }
|
|
||||||
val first = ArrayList<T>()
|
|
||||||
val second = ArrayList<T>()
|
|
||||||
fastForEach {
|
|
||||||
if (predicate(it)) {
|
|
||||||
first.add(it)
|
|
||||||
} else {
|
|
||||||
second.add(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Pair(first, second)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the number of entries not matching the given [predicate].
|
|
||||||
*
|
|
||||||
* **Do not use for collections that come from public APIs**, since they may not support random
|
|
||||||
* access in an efficient way, and this method may actually be a lot slower. Only use for
|
|
||||||
* collections that are created by code we control and are known to support random access.
|
|
||||||
*/
|
|
||||||
@OptIn(ExperimentalContracts::class)
|
|
||||||
inline fun <T> List<T>.fastCountNot(predicate: (T) -> Boolean): Int {
|
|
||||||
contract { callsInPlace(predicate) }
|
|
||||||
var count = size
|
|
||||||
fastForEach { if (predicate(it)) --count }
|
|
||||||
return count
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a list containing only elements from the given collection
|
|
||||||
* having distinct keys returned by the given [selector] function.
|
|
||||||
*
|
|
||||||
* Among elements of the given collection with equal keys, only the first one will be present in the resulting list.
|
|
||||||
* The elements in the resulting list are in the same order as they were in the source collection.
|
|
||||||
*
|
|
||||||
* **Do not use for collections that come from public APIs**, since they may not support random
|
|
||||||
* access in an efficient way, and this method may actually be a lot slower. Only use for
|
|
||||||
* collections that are created by code we control and are known to support random access.
|
|
||||||
*/
|
|
||||||
@OptIn(ExperimentalContracts::class)
|
|
||||||
inline fun <T, K> List<T>.fastDistinctBy(selector: (T) -> K): List<T> {
|
|
||||||
contract { callsInPlace(selector) }
|
|
||||||
val set = HashSet<K>()
|
|
||||||
val list = ArrayList<T>()
|
|
||||||
fastForEach {
|
|
||||||
val key = selector(it)
|
|
||||||
if (set.add(key)) list.add(it)
|
|
||||||
}
|
|
||||||
return list
|
|
||||||
}
|
|
@ -1,180 +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.CreateExtensionRepo
|
|
||||||
import eu.kanade.domain.extension.interactor.DeleteExtensionRepo
|
|
||||||
import eu.kanade.domain.extension.interactor.GetExtensionLanguages
|
|
||||||
import eu.kanade.domain.extension.interactor.GetExtensionRepos
|
|
||||||
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.GetLanguagesWithSources
|
|
||||||
import eu.kanade.domain.source.interactor.GetSourcesWithFavoriteCount
|
|
||||||
import eu.kanade.domain.source.interactor.SetMigrateSorting
|
|
||||||
import eu.kanade.domain.source.interactor.ToggleLanguage
|
|
||||||
import eu.kanade.domain.source.interactor.ToggleSource
|
|
||||||
import eu.kanade.domain.source.interactor.ToggleSourcePin
|
|
||||||
import eu.kanade.domain.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 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.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.InjektModule
|
|
||||||
import uy.kohesive.injekt.api.InjektRegistrar
|
|
||||||
import uy.kohesive.injekt.api.addFactory
|
|
||||||
import uy.kohesive.injekt.api.addSingletonFactory
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
|
|
||||||
class DomainModule : InjektModule {
|
|
||||||
|
|
||||||
override fun InjektRegistrar.registerInjectables() {
|
|
||||||
addSingletonFactory<CategoryRepository> { CategoryRepositoryImpl(get()) }
|
|
||||||
addFactory { GetCategories(get()) }
|
|
||||||
addFactory { ResetCategoryFlags(get(), get()) }
|
|
||||||
addFactory { SetDisplayMode(get()) }
|
|
||||||
addFactory { SetSortModeForCategory(get(), get()) }
|
|
||||||
addFactory { CreateCategoryWithName(get(), get()) }
|
|
||||||
addFactory { RenameCategory(get()) }
|
|
||||||
addFactory { ReorderCategory(get()) }
|
|
||||||
addFactory { UpdateCategory(get()) }
|
|
||||||
addFactory { DeleteCategory(get()) }
|
|
||||||
|
|
||||||
addSingletonFactory<MangaRepository> { MangaRepositoryImpl(get()) }
|
|
||||||
addFactory { GetDuplicateLibraryManga(get()) }
|
|
||||||
addFactory { GetFavorites(get()) }
|
|
||||||
addFactory { GetLibraryManga(get()) }
|
|
||||||
addFactory { GetMangaWithChapters(get(), get()) }
|
|
||||||
addFactory { GetMangaByUrlAndSourceId(get()) }
|
|
||||||
addFactory { GetManga(get()) }
|
|
||||||
addFactory { GetNextChapters(get(), get(), 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 { 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()) }
|
|
||||||
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()) }
|
|
||||||
addFactory { ShouldUpdateDbChapter() }
|
|
||||||
addFactory { SyncChaptersWithSource(get(), get(), get(), get(), get(), get(), get(), get()) }
|
|
||||||
addFactory { GetAvailableScanlators(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()) }
|
|
||||||
|
|
||||||
addFactory { CreateExtensionRepo(get()) }
|
|
||||||
addFactory { DeleteExtensionRepo(get()) }
|
|
||||||
addFactory { GetExtensionRepos(get()) }
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,31 +0,0 @@
|
|||||||
package eu.kanade.domain.base
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import dev.icerock.moko.resources.StringResource
|
|
||||||
import tachiyomi.core.preference.Preference
|
|
||||||
import tachiyomi.core.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),
|
|
||||||
}
|
|
||||||
}
|
|
@ -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.preference.Preference
|
|
||||||
import tachiyomi.core.preference.PreferenceStore
|
|
||||||
import tachiyomi.core.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,24 +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() }
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,80 +0,0 @@
|
|||||||
package eu.kanade.domain.chapter.interactor
|
|
||||||
|
|
||||||
import eu.kanade.domain.download.interactor.DeleteDownload
|
|
||||||
import logcat.LogPriority
|
|
||||||
import tachiyomi.core.util.lang.withNonCancellableContext
|
|
||||||
import tachiyomi.core.util.system.logcat
|
|
||||||
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,
|
|
||||||
) {
|
|
||||||
|
|
||||||
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(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun await(manga: Manga, read: Boolean) =
|
|
||||||
await(manga.id, read)
|
|
||||||
|
|
||||||
sealed interface Result {
|
|
||||||
data object Success : Result
|
|
||||||
data object NoChapters : Result
|
|
||||||
data class InternalError(val error: Throwable) : Result
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,210 +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 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.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,
|
|
||||||
) {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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, manga.title, 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 reAdded = mutableListOf<Chapter>()
|
|
||||||
|
|
||||||
val deletedChapterNumbers = TreeSet<Double>()
|
|
||||||
val deletedReadChapterNumbers = TreeSet<Double>()
|
|
||||||
val deletedBookmarkedChapterNumbers = TreeSet<Double>()
|
|
||||||
|
|
||||||
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 }
|
|
||||||
|
|
||||||
// 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.isRecognizedNumber.not() || 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
reAdded.add(chapter)
|
|
||||||
|
|
||||||
chapter
|
|
||||||
}
|
|
||||||
|
|
||||||
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 reAddedUrls = reAdded.map { it.url }.toHashSet()
|
|
||||||
|
|
||||||
val excludedScanlators = getExcludedScanlators.await(manga.id).toHashSet()
|
|
||||||
|
|
||||||
return updatedToAdd.filterNot {
|
|
||||||
it.url in reAddedUrls || it.scanlator in excludedScanlators
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,42 +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()
|
|
||||||
}
|
|
@ -1,52 +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): 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 ->
|
|
||||||
applyFilter(downloadedFilter) {
|
|
||||||
val downloaded = downloadManager.isChapterDownloaded(
|
|
||||||
chapter.name,
|
|
||||||
chapter.scanlator,
|
|
||||||
manga.title,
|
|
||||||
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.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,25 +0,0 @@
|
|||||||
package eu.kanade.domain.extension.interactor
|
|
||||||
|
|
||||||
import eu.kanade.domain.source.service.SourcePreferences
|
|
||||||
import tachiyomi.core.preference.plusAssign
|
|
||||||
|
|
||||||
class CreateExtensionRepo(private val preferences: SourcePreferences) {
|
|
||||||
|
|
||||||
fun await(name: String): Result {
|
|
||||||
// Do not allow invalid formats
|
|
||||||
if (!name.matches(repoRegex)) {
|
|
||||||
return Result.InvalidUrl
|
|
||||||
}
|
|
||||||
|
|
||||||
preferences.extensionRepos() += name.removeSuffix("/index.min.json")
|
|
||||||
|
|
||||||
return Result.Success
|
|
||||||
}
|
|
||||||
|
|
||||||
sealed interface Result {
|
|
||||||
data object InvalidUrl : Result
|
|
||||||
data object Success : Result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val repoRegex = """^https://.*/index\.min\.json$""".toRegex()
|
|
@ -1,11 +0,0 @@
|
|||||||
package eu.kanade.domain.extension.interactor
|
|
||||||
|
|
||||||
import eu.kanade.domain.source.service.SourcePreferences
|
|
||||||
import tachiyomi.core.preference.minusAssign
|
|
||||||
|
|
||||||
class DeleteExtensionRepo(private val preferences: SourcePreferences) {
|
|
||||||
|
|
||||||
fun await(repo: String) {
|
|
||||||
preferences.extensionRepos() -= repo
|
|
||||||
}
|
|
||||||
}
|
|
@ -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,11 +0,0 @@
|
|||||||
package eu.kanade.domain.extension.interactor
|
|
||||||
|
|
||||||
import eu.kanade.domain.source.service.SourcePreferences
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
|
|
||||||
class GetExtensionRepos(private val preferences: SourcePreferences) {
|
|
||||||
|
|
||||||
fun subscribe(): Flow<Set<String>> {
|
|
||||||
return preferences.extensionRepos().changes()
|
|
||||||
}
|
|
||||||
}
|
|
@ -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.not(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data class ExtensionSourceItem(
|
|
||||||
val source: Source,
|
|
||||||
val enabled: Boolean,
|
|
||||||
val labelAsName: Boolean,
|
|
||||||
)
|
|
@ -1,60 +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,
|
|
||||||
) { _activeLanguages, _installed, _untrusted, _available ->
|
|
||||||
val (updates, installed) = _installed
|
|
||||||
.filter { (showNsfwSources || it.isNsfw.not()) }
|
|
||||||
.sortedWith(
|
|
||||||
compareBy<Extension.Installed> { it.isObsolete.not() }
|
|
||||||
.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.not())
|
|
||||||
}
|
|
||||||
.flatMap { ext ->
|
|
||||||
if (ext.sources.isEmpty()) {
|
|
||||||
return@flatMap if (ext.lang in _activeLanguages) listOf(ext) else emptyList()
|
|
||||||
}
|
|
||||||
ext.sources.filter { it.lang in _activeLanguages }
|
|
||||||
.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,31 +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 tachiyomi.core.preference.getAndSet
|
|
||||||
|
|
||||||
class TrustExtension(
|
|
||||||
private val preferences: SourcePreferences,
|
|
||||||
) {
|
|
||||||
|
|
||||||
fun isTrusted(pkgInfo: PackageInfo, signatureHash: String): Boolean {
|
|
||||||
val key = "${pkgInfo.packageName}:${PackageInfoCompat.getLongVersionCode(pkgInfo)}:$signatureHash"
|
|
||||||
return 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,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,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,106 +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.source.model.SManga
|
|
||||||
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(),
|
|
||||||
): Boolean {
|
|
||||||
val remoteTitle = try {
|
|
||||||
remoteManga.title
|
|
||||||
} catch (_: UninitializedPropertyAccessException) {
|
|
||||||
""
|
|
||||||
}
|
|
||||||
|
|
||||||
// if the manga isn't a favorite, set its title from source and update in db
|
|
||||||
val title = if (remoteTitle.isEmpty() || localManga.favorite) null else remoteTitle
|
|
||||||
|
|
||||||
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() }
|
|
||||||
|
|
||||||
return 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,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
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 tachiyomi.core.metadata.comicinfo.ComicInfo
|
|
||||||
import tachiyomi.core.metadata.comicinfo.ComicInfoPublishingStatus
|
|
||||||
import tachiyomi.core.preference.TriState
|
|
||||||
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 (forceDownloaded()) 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.forceDownloaded(): Boolean {
|
|
||||||
return favorite && Injekt.get<BasePreferences>().downloadedOnly().get()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Manga.toSManga(): SManga = SManga.create().also {
|
|
||||||
it.url = url
|
|
||||||
it.title = title
|
|
||||||
it.artist = artist
|
|
||||||
it.author = author
|
|
||||||
it.description = description
|
|
||||||
it.genre = genre.orEmpty().joinToString()
|
|
||||||
it.status = status.toInt()
|
|
||||||
it.thumbnail_url = thumbnailUrl
|
|
||||||
it.initialized = initialized
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Manga.copyFrom(other: SManga): Manga {
|
|
||||||
val author = other.author ?: author
|
|
||||||
val artist = other.artist ?: artist
|
|
||||||
val description = other.description ?: description
|
|
||||||
val genres = if (other.genre != null) {
|
|
||||||
other.getGenres()
|
|
||||||
} else {
|
|
||||||
genre
|
|
||||||
}
|
|
||||||
val thumbnailUrl = other.thumbnail_url ?: thumbnailUrl
|
|
||||||
return this.copy(
|
|
||||||
author = author,
|
|
||||||
artist = artist,
|
|
||||||
description = description,
|
|
||||||
genre = genres,
|
|
||||||
thumbnailUrl = thumbnailUrl,
|
|
||||||
status = other.status.toLong(),
|
|
||||||
updateStrategy = other.update_strategy,
|
|
||||||
initialized = other.initialized && initialized,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun SManga.toDomainManga(sourceId: Long): Manga {
|
|
||||||
return Manga.create().copy(
|
|
||||||
url = url,
|
|
||||||
title = title,
|
|
||||||
artist = artist,
|
|
||||||
author = author,
|
|
||||||
description = description,
|
|
||||||
genre = getGenres(),
|
|
||||||
status = status.toLong(),
|
|
||||||
thumbnailUrl = thumbnail_url,
|
|
||||||
updateStrategy = update_strategy,
|
|
||||||
initialized = initialized,
|
|
||||||
source = sourceId,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
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, chapterUrl: String, categories: List<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(chapterUrl),
|
|
||||||
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()) },
|
|
||||||
inker = null,
|
|
||||||
colorist = null,
|
|
||||||
letterer = null,
|
|
||||||
coverArtist = null,
|
|
||||||
tags = null,
|
|
||||||
)
|
|
@ -1,42 +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 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(),
|
|
||||||
preferences.enabledLanguages().changes(),
|
|
||||||
preferences.disabledSources().changes(),
|
|
||||||
preferences.lastUsedSource().changes(),
|
|
||||||
repository.getSources(),
|
|
||||||
) { pinnedSourceIds, enabledLanguages, disabledSources, lastUsedSource, sources ->
|
|
||||||
sources
|
|
||||||
.filter { it.lang in enabledLanguages || it.isLocal() }
|
|
||||||
.filterNot { it.id.toString() in disabledSources }
|
|
||||||
.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name })
|
|
||||||
.flatMap {
|
|
||||||
val flag = if ("${it.id}" in pinnedSourceIds) Pins.pinned else Pins.unpinned
|
|
||||||
val source = it.copy(pin = flag)
|
|
||||||
val toFlatten = mutableListOf(source)
|
|
||||||
if (source.id == lastUsedSource) {
|
|
||||||
toFlatten.add(source.copy(isUsedLast = true, pin = source.pin - Pin.Actual))
|
|
||||||
}
|
|
||||||
toFlatten
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.distinctUntilChanged()
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,34 +0,0 @@
|
|||||||
package eu.kanade.domain.source.interactor
|
|
||||||
|
|
||||||
import eu.kanade.domain.source.service.SourcePreferences
|
|
||||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
|
||||||
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.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,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.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.not() -> -1
|
|
||||||
b.first.isStub && a.first.isStub.not() -> 1
|
|
||||||
else -> a.first.name.lowercase().compareToWithCollator(b.first.name.lowercase())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
SetMigrateSorting.Mode.TOTAL -> {
|
|
||||||
when {
|
|
||||||
a.first.isStub && b.first.isStub.not() -> -1
|
|
||||||
b.first.isStub && a.first.isStub.not() -> 1
|
|
||||||
else -> a.second.compareTo(b.second)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return when (direction) {
|
|
||||||
SetMigrateSorting.Direction.ASCENDING -> Comparator(sortFn)
|
|
||||||
SetMigrateSorting.Direction.DESCENDING -> Collections.reverseOrder(sortFn)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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,16 +0,0 @@
|
|||||||
package eu.kanade.domain.source.interactor
|
|
||||||
|
|
||||||
import eu.kanade.domain.source.service.SourcePreferences
|
|
||||||
import tachiyomi.core.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.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.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,51 +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.preference.Preference
|
|
||||||
import tachiyomi.core.preference.PreferenceStore
|
|
||||||
import tachiyomi.core.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 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(),
|
|
||||||
)
|
|
||||||
}
|
|
@ -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.source.Source
|
|
||||||
import eu.kanade.tachiyomi.util.lang.convertEpochMillisZone
|
|
||||||
import logcat.LogPriority
|
|
||||||
import tachiyomi.core.util.lang.withIOContext
|
|
||||||
import tachiyomi.core.util.lang.withNonCancellableContext
|
|
||||||
import tachiyomi.core.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.GetTracks
|
|
||||||
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 getTracks: GetTracks,
|
|
||||||
private val insertTrack: InsertTrack,
|
|
||||||
private val syncChapterProgressWithTrack: SyncChapterProgressWithTrack,
|
|
||||||
private val getChaptersByMangaId: GetChaptersByMangaId,
|
|
||||||
) {
|
|
||||||
|
|
||||||
// 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 {
|
|
||||||
getTracks.await(manga.id)
|
|
||||||
.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()!!)
|
|
||||||
|
|
||||||
syncChapterProgressWithTrack.await(
|
|
||||||
manga.id,
|
|
||||||
track.toDomainTrack()!!,
|
|
||||||
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())
|
|
||||||
insertTrack.await(updatedTrack.toDomainTrack()!!)
|
|
||||||
syncChapterProgressWithTrack.await(mangaId, track, service)
|
|
||||||
null
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
service to e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.awaitAll()
|
|
||||||
.filterNotNull()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,49 +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.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
|
|
||||||
|
|
||||||
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 updatedTrack = remoteTrack.copy(lastChapterRead = localLastRead.toDouble())
|
|
||||||
|
|
||||||
try {
|
|
||||||
tracker.update(updatedTrack.toDbTrack())
|
|
||||||
updateChapter.awaitAll(chapterUpdates)
|
|
||||||
insertTrack.await(updatedTrack)
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
logcat(LogPriority.WARN, e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,57 +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 kotlinx.coroutines.async
|
|
||||||
import kotlinx.coroutines.awaitAll
|
|
||||||
import logcat.LogPriority
|
|
||||||
import tachiyomi.core.util.lang.withNonCancellableContext
|
|
||||||
import tachiyomi.core.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) {
|
|
||||||
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) {
|
|
||||||
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)
|
|
||||||
DelayedTrackingUpdateJob.setupTask(context)
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.awaitAll()
|
|
||||||
.mapNotNull { it.exceptionOrNull() }
|
|
||||||
.forEach { logcat(LogPriority.WARN, it) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,50 +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,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
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.toFloat()
|
|
||||||
it.total_chapters = totalChapters.toInt()
|
|
||||||
it.status = status.toInt()
|
|
||||||
it.score = score.toFloat()
|
|
||||||
it.tracking_url = remoteUrl
|
|
||||||
it.started_reading_date = startDate
|
|
||||||
it.finished_reading_date = finishDate
|
|
||||||
}
|
|
||||||
|
|
||||||
fun DbTrack.toDomainTrack(idRequired: Boolean = true): Track? {
|
|
||||||
val trackId = id ?: if (idRequired.not()) -1 else return null
|
|
||||||
return Track(
|
|
||||||
id = trackId,
|
|
||||||
mangaId = manga_id,
|
|
||||||
trackerId = tracker_id.toLong(),
|
|
||||||
remoteId = remote_id,
|
|
||||||
libraryId = library_id,
|
|
||||||
title = title,
|
|
||||||
lastChapterRead = last_chapter_read.toDouble(),
|
|
||||||
totalChapters = total_chapters.toLong(),
|
|
||||||
status = status.toLong(),
|
|
||||||
// Jank workaround due to precision issues while converting
|
|
||||||
// See https://github.com/tachiyomiorg/tachiyomi/issues/10343
|
|
||||||
score = score.toString().toDouble(),
|
|
||||||
remoteUrl = tracking_url,
|
|
||||||
startDate = started_reading_date,
|
|
||||||
finishDate = finished_reading_date,
|
|
||||||
)
|
|
||||||
}
|
|
@ -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.util.lang.withIOContext
|
|
||||||
import tachiyomi.core.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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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,32 +0,0 @@
|
|||||||
package eu.kanade.domain.track.service
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.data.track.Tracker
|
|
||||||
import eu.kanade.tachiyomi.data.track.anilist.Anilist
|
|
||||||
import tachiyomi.core.preference.Preference
|
|
||||||
import tachiyomi.core.preference.PreferenceStore
|
|
||||||
|
|
||||||
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 setCredentials(tracker: Tracker, username: String, password: String) {
|
|
||||||
trackUsername(tracker).set(username)
|
|
||||||
trackPassword(tracker).set(password)
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
@ -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.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,39 +0,0 @@
|
|||||||
package eu.kanade.domain.ui
|
|
||||||
|
|
||||||
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.preference.PreferenceStore
|
|
||||||
import tachiyomi.core.preference.getEnum
|
|
||||||
import java.text.DateFormat
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
class UiPreferences(
|
|
||||||
private val preferenceStore: PreferenceStore,
|
|
||||||
) {
|
|
||||||
|
|
||||||
fun themeMode() = preferenceStore.getEnum("pref_theme_mode_key", ThemeMode.SYSTEM)
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun dateFormat(format: String): DateFormat = when (format) {
|
|
||||||
"" -> DateFormat.getDateInstance(DateFormat.SHORT)
|
|
||||||
else -> SimpleDateFormat(format, Locale.getDefault())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,28 +0,0 @@
|
|||||||
package eu.kanade.domain.ui.model
|
|
||||||
|
|
||||||
import dev.icerock.moko.resources.StringResource
|
|
||||||
import eu.kanade.tachiyomi.util.system.isDevFlavor
|
|
||||||
import eu.kanade.tachiyomi.util.system.isPreviewBuildType
|
|
||||||
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),
|
|
||||||
|
|
||||||
// TODO: re-enable for preview
|
|
||||||
NORD(MR.strings.theme_nord.takeIf { isDevFlavor || isPreviewBuildType }),
|
|
||||||
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),
|
|
||||||
|
|
||||||
// Deprecated
|
|
||||||
DARK_BLUE(null),
|
|
||||||
HOT_PINK(null),
|
|
||||||
BLUE(null),
|
|
||||||
}
|
|
@ -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,168 +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.BrowseSourceList
|
|
||||||
import eu.kanade.presentation.components.AppBar
|
|
||||||
import eu.kanade.presentation.util.formattedMessage
|
|
||||||
import eu.kanade.tachiyomi.source.Source
|
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
|
||||||
import tachiyomi.core.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<Manga>>,
|
|
||||||
columns: GridCells,
|
|
||||||
displayMode: LibraryDisplayMode,
|
|
||||||
snackbarHostState: SnackbarHostState,
|
|
||||||
contentPadding: PaddingValues,
|
|
||||||
onWebViewClick: () -> Unit,
|
|
||||||
onHelpClick: () -> Unit,
|
|
||||||
onLocalSourceHelpClick: () -> Unit,
|
|
||||||
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 && errorState != null && errorState is LoadState.Error) {
|
|
||||||
EmptyScreen(
|
|
||||||
modifier = Modifier.padding(contentPadding),
|
|
||||||
message = getErrorMessage(errorState),
|
|
||||||
actions = if (source is LocalSource) {
|
|
||||||
persistentListOf(
|
|
||||||
EmptyScreenAction(
|
|
||||||
stringRes = MR.strings.local_source_help_guide,
|
|
||||||
icon = Icons.AutoMirrored.Outlined.HelpOutline,
|
|
||||||
onClick = onLocalSourceHelpClick,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
persistentListOf(
|
|
||||||
EmptyScreenAction(
|
|
||||||
stringRes = MR.strings.action_retry,
|
|
||||||
icon = Icons.Outlined.Refresh,
|
|
||||||
onClick = mangaList::refresh,
|
|
||||||
),
|
|
||||||
EmptyScreenAction(
|
|
||||||
stringRes = MR.strings.action_open_in_web_view,
|
|
||||||
icon = Icons.Outlined.Public,
|
|
||||||
onClick = onWebViewClick,
|
|
||||||
),
|
|
||||||
EmptyScreenAction(
|
|
||||||
stringRes = MR.strings.label_help,
|
|
||||||
icon = Icons.AutoMirrored.Outlined.HelpOutline,
|
|
||||||
onClick = onHelpClick,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mangaList.itemCount == 0 && mangaList.loadState.refresh is LoadState.Loading) {
|
|
||||||
LoadingScreen(
|
|
||||||
modifier = Modifier.padding(contentPadding),
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
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,423 +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.interaction.MutableInteractionSource
|
|
||||||
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.platform.LocalContext
|
|
||||||
import androidx.compose.ui.platform.LocalUriHandler
|
|
||||||
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.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 kotlinx.collections.immutable.ImmutableList
|
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
|
||||||
import tachiyomi.i18n.MR
|
|
||||||
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,
|
|
||||||
) {
|
|
||||||
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,
|
|
||||||
onClickSourcePreferences = onClickSourcePreferences,
|
|
||||||
onClickUninstall = onClickUninstall,
|
|
||||||
onClickSource = onClickSource,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun ExtensionDetails(
|
|
||||||
contentPadding: PaddingValues,
|
|
||||||
extension: Extension.Installed,
|
|
||||||
sources: ImmutableList<ExtensionSourceItem>,
|
|
||||||
onClickSourcePreferences: (sourceId: Long) -> Unit,
|
|
||||||
onClickUninstall: () -> Unit,
|
|
||||||
onClickSource: (sourceId: Long) -> Unit,
|
|
||||||
) {
|
|
||||||
val context = LocalContext.current
|
|
||||||
var showNsfwWarning by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
ScrollbarLazyColumn(
|
|
||||||
contentPadding = contentPadding,
|
|
||||||
) {
|
|
||||||
if (extension.isObsolete) {
|
|
||||||
item {
|
|
||||||
WarningBanner(MR.strings.obsolete_extension_message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
item {
|
|
||||||
DetailsHeader(
|
|
||||||
extension = extension,
|
|
||||||
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
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
items(
|
|
||||||
items = sources,
|
|
||||||
key = { it.source.id },
|
|
||||||
) { source ->
|
|
||||||
SourceSwitchPreference(
|
|
||||||
modifier = Modifier.animateItemPlacement(),
|
|
||||||
source = source,
|
|
||||||
onClickSourcePreferences = onClickSourcePreferences,
|
|
||||||
onClickSource = onClickSource,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (showNsfwWarning) {
|
|
||||||
NsfwWarningDialog(
|
|
||||||
onClickConfirm = {
|
|
||||||
showNsfwWarning = false
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun DetailsHeader(
|
|
||||||
extension: Extension,
|
|
||||||
onClickAgeRating: () -> Unit,
|
|
||||||
onClickUninstall: () -> Unit,
|
|
||||||
onClickAppInfo: (() -> Unit)?,
|
|
||||||
) {
|
|
||||||
val context = LocalContext.current
|
|
||||||
|
|
||||||
Column {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(
|
|
||||||
start = MaterialTheme.padding.medium,
|
|
||||||
end = MaterialTheme.padding.medium,
|
|
||||||
top = MaterialTheme.padding.medium,
|
|
||||||
bottom = MaterialTheme.padding.small,
|
|
||||||
),
|
|
||||||
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(
|
|
||||||
start = MaterialTheme.padding.medium,
|
|
||||||
end = MaterialTheme.padding.medium,
|
|
||||||
top = MaterialTheme.padding.small,
|
|
||||||
bottom = MaterialTheme.padding.medium,
|
|
||||||
),
|
|
||||||
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,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
HorizontalDivider()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun InfoText(
|
|
||||||
primaryText: String,
|
|
||||||
secondaryText: String,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
primaryTextStyle: TextStyle = MaterialTheme.typography.bodyLarge,
|
|
||||||
onClick: (() -> Unit)? = null,
|
|
||||||
) {
|
|
||||||
val interactionSource = remember { MutableInteractionSource() }
|
|
||||||
|
|
||||||
val clickableModifier = if (onClick != null) {
|
|
||||||
Modifier.clickable(interactionSource, indication = null) { 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.animateItemPlacement(),
|
|
||||||
title = LocaleHelper.getSourceDisplayName(language, context),
|
|
||||||
checked = language in state.enabledLanguages,
|
|
||||||
onCheckedChanged = { onClickLang(language) },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,531 +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.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.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(
|
|
||||||
stringRes = 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.animateItemPlacement(),
|
|
||||||
action = action,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
is ExtensionUiModel.Header.Text -> {
|
|
||||||
ExtensionHeader(
|
|
||||||
text = header.text,
|
|
||||||
modifier = Modifier.animateItemPlacement(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
items(
|
|
||||||
items = items,
|
|
||||||
contentType = { "item" },
|
|
||||||
key = { "extension-${it.hashCode()}" },
|
|
||||||
) { item ->
|
|
||||||
ExtensionItem(
|
|
||||||
modifier = Modifier.animateItemPlacement(),
|
|
||||||
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,
|
|
||||||
label = "iconPadding",
|
|
||||||
)
|
|
||||||
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
|
|
||||||
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,
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,103 +0,0 @@
|
|||||||
package eu.kanade.presentation.browse
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.State
|
|
||||||
import eu.kanade.presentation.browse.components.GlobalSearchCardRow
|
|
||||||
import eu.kanade.presentation.browse.components.GlobalSearchErrorResultItem
|
|
||||||
import eu.kanade.presentation.browse.components.GlobalSearchLoadingResultItem
|
|
||||||
import eu.kanade.presentation.browse.components.GlobalSearchResultItem
|
|
||||||
import eu.kanade.presentation.browse.components.GlobalSearchToolbar
|
|
||||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchItemResult
|
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchScreenModel
|
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SourceFilter
|
|
||||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
|
||||||
import tachiyomi.domain.manga.model.Manga
|
|
||||||
import tachiyomi.presentation.core.components.material.Scaffold
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun GlobalSearchScreen(
|
|
||||||
state: SearchScreenModel.State,
|
|
||||||
navigateUp: () -> Unit,
|
|
||||||
onChangeSearchQuery: (String?) -> Unit,
|
|
||||||
onSearch: (String) -> Unit,
|
|
||||||
onChangeSearchFilter: (SourceFilter) -> Unit,
|
|
||||||
onToggleResults: () -> Unit,
|
|
||||||
getManga: @Composable (Manga) -> State<Manga>,
|
|
||||||
onClickSource: (CatalogueSource) -> Unit,
|
|
||||||
onClickItem: (Manga) -> Unit,
|
|
||||||
onLongClickItem: (Manga) -> Unit,
|
|
||||||
) {
|
|
||||||
Scaffold(
|
|
||||||
topBar = { scrollBehavior ->
|
|
||||||
GlobalSearchToolbar(
|
|
||||||
searchQuery = state.searchQuery,
|
|
||||||
progress = state.progress,
|
|
||||||
total = state.total,
|
|
||||||
navigateUp = navigateUp,
|
|
||||||
onChangeSearchQuery = onChangeSearchQuery,
|
|
||||||
onSearch = onSearch,
|
|
||||||
sourceFilter = state.sourceFilter,
|
|
||||||
onChangeSearchFilter = onChangeSearchFilter,
|
|
||||||
onlyShowHasResults = state.onlyShowHasResults,
|
|
||||||
onToggleResults = onToggleResults,
|
|
||||||
scrollBehavior = scrollBehavior,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
) { paddingValues ->
|
|
||||||
GlobalSearchContent(
|
|
||||||
items = state.filteredItems,
|
|
||||||
contentPadding = paddingValues,
|
|
||||||
getManga = getManga,
|
|
||||||
onClickSource = onClickSource,
|
|
||||||
onClickItem = onClickItem,
|
|
||||||
onLongClickItem = onLongClickItem,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
internal fun GlobalSearchContent(
|
|
||||||
items: Map<CatalogueSource, SearchItemResult>,
|
|
||||||
contentPadding: PaddingValues,
|
|
||||||
getManga: @Composable (Manga) -> State<Manga>,
|
|
||||||
onClickSource: (CatalogueSource) -> Unit,
|
|
||||||
onClickItem: (Manga) -> Unit,
|
|
||||||
onLongClickItem: (Manga) -> Unit,
|
|
||||||
fromSourceId: Long? = null,
|
|
||||||
) {
|
|
||||||
LazyColumn(
|
|
||||||
contentPadding = contentPadding,
|
|
||||||
) {
|
|
||||||
items.forEach { (source, result) ->
|
|
||||||
item(key = source.id) {
|
|
||||||
GlobalSearchResultItem(
|
|
||||||
title = fromSourceId?.let {
|
|
||||||
"▶ ${source.name}".takeIf { source.id == fromSourceId }
|
|
||||||
} ?: source.name,
|
|
||||||
subtitle = LocaleHelper.getLocalizedDisplayName(source.lang),
|
|
||||||
onClick = { onClickSource(source) },
|
|
||||||
) {
|
|
||||||
when (result) {
|
|
||||||
SearchItemResult.Loading -> {
|
|
||||||
GlobalSearchLoadingResultItem()
|
|
||||||
}
|
|
||||||
is SearchItemResult.Success -> {
|
|
||||||
GlobalSearchCardRow(
|
|
||||||
titles = result.result,
|
|
||||||
getManga = getManga,
|
|
||||||
onClick = onClickItem,
|
|
||||||
onLongClick = onLongClickItem,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
is SearchItemResult.Error -> {
|
|
||||||
GlobalSearchErrorResultItem(message = result.throwable.message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,84 +0,0 @@
|
|||||||
package eu.kanade.presentation.browse
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.lazy.items
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import eu.kanade.presentation.components.AppBar
|
|
||||||
import eu.kanade.presentation.manga.components.BaseMangaListItem
|
|
||||||
import eu.kanade.tachiyomi.ui.browse.migration.manga.MigrateMangaScreenModel
|
|
||||||
import tachiyomi.domain.manga.model.Manga
|
|
||||||
import tachiyomi.i18n.MR
|
|
||||||
import tachiyomi.presentation.core.components.FastScrollLazyColumn
|
|
||||||
import tachiyomi.presentation.core.components.material.Scaffold
|
|
||||||
import tachiyomi.presentation.core.screens.EmptyScreen
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun MigrateMangaScreen(
|
|
||||||
navigateUp: () -> Unit,
|
|
||||||
title: String?,
|
|
||||||
state: MigrateMangaScreenModel.State,
|
|
||||||
onClickItem: (Manga) -> Unit,
|
|
||||||
onClickCover: (Manga) -> Unit,
|
|
||||||
) {
|
|
||||||
Scaffold(
|
|
||||||
topBar = { scrollBehavior ->
|
|
||||||
AppBar(
|
|
||||||
title = title,
|
|
||||||
navigateUp = navigateUp,
|
|
||||||
scrollBehavior = scrollBehavior,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
) { contentPadding ->
|
|
||||||
if (state.isEmpty) {
|
|
||||||
EmptyScreen(
|
|
||||||
stringRes = MR.strings.empty_screen,
|
|
||||||
modifier = Modifier.padding(contentPadding),
|
|
||||||
)
|
|
||||||
return@Scaffold
|
|
||||||
}
|
|
||||||
|
|
||||||
MigrateMangaContent(
|
|
||||||
contentPadding = contentPadding,
|
|
||||||
state = state,
|
|
||||||
onClickItem = onClickItem,
|
|
||||||
onClickCover = onClickCover,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun MigrateMangaContent(
|
|
||||||
contentPadding: PaddingValues,
|
|
||||||
state: MigrateMangaScreenModel.State,
|
|
||||||
onClickItem: (Manga) -> Unit,
|
|
||||||
onClickCover: (Manga) -> Unit,
|
|
||||||
) {
|
|
||||||
FastScrollLazyColumn(
|
|
||||||
contentPadding = contentPadding,
|
|
||||||
) {
|
|
||||||
items(state.titles) { manga ->
|
|
||||||
MigrateMangaItem(
|
|
||||||
manga = manga,
|
|
||||||
onClickItem = onClickItem,
|
|
||||||
onClickCover = onClickCover,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun MigrateMangaItem(
|
|
||||||
manga: Manga,
|
|
||||||
onClickItem: (Manga) -> Unit,
|
|
||||||
onClickCover: (Manga) -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
BaseMangaListItem(
|
|
||||||
modifier = modifier,
|
|
||||||
manga = manga,
|
|
||||||
onClickItem = { onClickItem(manga) },
|
|
||||||
onClickCover = { onClickCover(manga) },
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,53 +0,0 @@
|
|||||||
package eu.kanade.presentation.browse
|
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.State
|
|
||||||
import eu.kanade.presentation.browse.components.GlobalSearchToolbar
|
|
||||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchScreenModel
|
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SourceFilter
|
|
||||||
import tachiyomi.domain.manga.model.Manga
|
|
||||||
import tachiyomi.presentation.core.components.material.Scaffold
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun MigrateSearchScreen(
|
|
||||||
state: SearchScreenModel.State,
|
|
||||||
fromSourceId: Long?,
|
|
||||||
navigateUp: () -> Unit,
|
|
||||||
onChangeSearchQuery: (String?) -> Unit,
|
|
||||||
onSearch: (String) -> Unit,
|
|
||||||
onChangeSearchFilter: (SourceFilter) -> Unit,
|
|
||||||
onToggleResults: () -> Unit,
|
|
||||||
getManga: @Composable (Manga) -> State<Manga>,
|
|
||||||
onClickSource: (CatalogueSource) -> Unit,
|
|
||||||
onClickItem: (Manga) -> Unit,
|
|
||||||
onLongClickItem: (Manga) -> Unit,
|
|
||||||
) {
|
|
||||||
Scaffold(
|
|
||||||
topBar = { scrollBehavior ->
|
|
||||||
GlobalSearchToolbar(
|
|
||||||
searchQuery = state.searchQuery,
|
|
||||||
progress = state.progress,
|
|
||||||
total = state.total,
|
|
||||||
navigateUp = navigateUp,
|
|
||||||
onChangeSearchQuery = onChangeSearchQuery,
|
|
||||||
onSearch = onSearch,
|
|
||||||
sourceFilter = state.sourceFilter,
|
|
||||||
onChangeSearchFilter = onChangeSearchFilter,
|
|
||||||
onlyShowHasResults = state.onlyShowHasResults,
|
|
||||||
onToggleResults = onToggleResults,
|
|
||||||
scrollBehavior = scrollBehavior,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
) { paddingValues ->
|
|
||||||
GlobalSearchContent(
|
|
||||||
fromSourceId = fromSourceId,
|
|
||||||
items = state.filteredItems,
|
|
||||||
contentPadding = paddingValues,
|
|
||||||
getManga = getManga,
|
|
||||||
onClickSource = onClickSource,
|
|
||||||
onClickItem = onClickItem,
|
|
||||||
onLongClickItem = onLongClickItem,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,205 +0,0 @@
|
|||||||
package eu.kanade.presentation.browse
|
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
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.padding
|
|
||||||
import androidx.compose.foundation.lazy.items
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.outlined.ArrowDownward
|
|
||||||
import androidx.compose.material.icons.outlined.ArrowUpward
|
|
||||||
import androidx.compose.material.icons.outlined.Numbers
|
|
||||||
import androidx.compose.material.icons.outlined.SortByAlpha
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import eu.kanade.domain.source.interactor.SetMigrateSorting
|
|
||||||
import eu.kanade.presentation.browse.components.BaseSourceItem
|
|
||||||
import eu.kanade.presentation.browse.components.SourceIcon
|
|
||||||
import eu.kanade.tachiyomi.ui.browse.migration.sources.MigrateSourceScreenModel
|
|
||||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
|
||||||
import tachiyomi.domain.source.model.Source
|
|
||||||
import tachiyomi.i18n.MR
|
|
||||||
import tachiyomi.presentation.core.components.Badge
|
|
||||||
import tachiyomi.presentation.core.components.BadgeGroup
|
|
||||||
import tachiyomi.presentation.core.components.ScrollbarLazyColumn
|
|
||||||
import tachiyomi.presentation.core.components.Scroller.STICKY_HEADER_KEY_PREFIX
|
|
||||||
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.LoadingScreen
|
|
||||||
import tachiyomi.presentation.core.theme.header
|
|
||||||
import tachiyomi.presentation.core.util.plus
|
|
||||||
import tachiyomi.presentation.core.util.secondaryItemAlpha
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun MigrateSourceScreen(
|
|
||||||
state: MigrateSourceScreenModel.State,
|
|
||||||
contentPadding: PaddingValues,
|
|
||||||
onClickItem: (Source) -> Unit,
|
|
||||||
onToggleSortingDirection: () -> Unit,
|
|
||||||
onToggleSortingMode: () -> Unit,
|
|
||||||
) {
|
|
||||||
val context = LocalContext.current
|
|
||||||
when {
|
|
||||||
state.isLoading -> LoadingScreen(Modifier.padding(contentPadding))
|
|
||||||
state.isEmpty -> EmptyScreen(
|
|
||||||
stringRes = MR.strings.information_empty_library,
|
|
||||||
modifier = Modifier.padding(contentPadding),
|
|
||||||
)
|
|
||||||
else ->
|
|
||||||
MigrateSourceList(
|
|
||||||
list = state.items,
|
|
||||||
contentPadding = contentPadding,
|
|
||||||
onClickItem = onClickItem,
|
|
||||||
onLongClickItem = { source ->
|
|
||||||
val sourceId = source.id.toString()
|
|
||||||
context.copyToClipboard(sourceId, sourceId)
|
|
||||||
},
|
|
||||||
sortingMode = state.sortingMode,
|
|
||||||
onToggleSortingMode = onToggleSortingMode,
|
|
||||||
sortingDirection = state.sortingDirection,
|
|
||||||
onToggleSortingDirection = onToggleSortingDirection,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun MigrateSourceList(
|
|
||||||
list: ImmutableList<Pair<Source, Long>>,
|
|
||||||
contentPadding: PaddingValues,
|
|
||||||
onClickItem: (Source) -> Unit,
|
|
||||||
onLongClickItem: (Source) -> Unit,
|
|
||||||
sortingMode: SetMigrateSorting.Mode,
|
|
||||||
onToggleSortingMode: () -> Unit,
|
|
||||||
sortingDirection: SetMigrateSorting.Direction,
|
|
||||||
onToggleSortingDirection: () -> Unit,
|
|
||||||
) {
|
|
||||||
ScrollbarLazyColumn(
|
|
||||||
contentPadding = contentPadding + topSmallPaddingValues,
|
|
||||||
) {
|
|
||||||
stickyHeader(key = STICKY_HEADER_KEY_PREFIX) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.background(MaterialTheme.colorScheme.background)
|
|
||||||
.padding(start = MaterialTheme.padding.medium),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(MR.strings.migration_selection_prompt),
|
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
style = MaterialTheme.typography.header,
|
|
||||||
)
|
|
||||||
|
|
||||||
IconButton(onClick = onToggleSortingMode) {
|
|
||||||
when (sortingMode) {
|
|
||||||
SetMigrateSorting.Mode.ALPHABETICAL -> Icon(
|
|
||||||
Icons.Outlined.SortByAlpha,
|
|
||||||
contentDescription = stringResource(MR.strings.action_sort_alpha),
|
|
||||||
)
|
|
||||||
SetMigrateSorting.Mode.TOTAL -> Icon(
|
|
||||||
Icons.Outlined.Numbers,
|
|
||||||
contentDescription = stringResource(MR.strings.action_sort_count),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
IconButton(onClick = onToggleSortingDirection) {
|
|
||||||
when (sortingDirection) {
|
|
||||||
SetMigrateSorting.Direction.ASCENDING -> Icon(
|
|
||||||
Icons.Outlined.ArrowUpward,
|
|
||||||
contentDescription = stringResource(MR.strings.action_asc),
|
|
||||||
)
|
|
||||||
SetMigrateSorting.Direction.DESCENDING -> Icon(
|
|
||||||
Icons.Outlined.ArrowDownward,
|
|
||||||
contentDescription = stringResource(MR.strings.action_desc),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
items(
|
|
||||||
items = list,
|
|
||||||
key = { (source, _) -> "migrate-${source.id}" },
|
|
||||||
) { (source, count) ->
|
|
||||||
MigrateSourceItem(
|
|
||||||
modifier = Modifier.animateItemPlacement(),
|
|
||||||
source = source,
|
|
||||||
count = count,
|
|
||||||
onClickItem = { onClickItem(source) },
|
|
||||||
onLongClickItem = { onLongClickItem(source) },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun MigrateSourceItem(
|
|
||||||
source: Source,
|
|
||||||
count: Long,
|
|
||||||
onClickItem: () -> Unit,
|
|
||||||
onLongClickItem: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
BaseSourceItem(
|
|
||||||
modifier = modifier,
|
|
||||||
source = source,
|
|
||||||
showLanguageInContent = source.lang != "",
|
|
||||||
onClickItem = onClickItem,
|
|
||||||
onLongClickItem = onLongClickItem,
|
|
||||||
icon = { SourceIcon(source = source) },
|
|
||||||
action = {
|
|
||||||
BadgeGroup {
|
|
||||||
Badge(text = "$count")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
content = { _, sourceLangString ->
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(horizontal = MaterialTheme.padding.medium)
|
|
||||||
.weight(1f),
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = source.name.ifBlank { source.id.toString() },
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
)
|
|
||||||
Row(
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
) {
|
|
||||||
if (sourceLangString != null) {
|
|
||||||
Text(
|
|
||||||
modifier = Modifier.secondaryItemAlpha(),
|
|
||||||
text = sourceLangString,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (source.isStub) {
|
|
||||||
Text(
|
|
||||||
modifier = Modifier.secondaryItemAlpha(),
|
|
||||||
text = stringResource(MR.strings.not_installed),
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
color = MaterialTheme.colorScheme.error,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,126 +0,0 @@
|
|||||||
package eu.kanade.presentation.browse
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.lazy.items
|
|
||||||
import androidx.compose.material3.Checkbox
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import eu.kanade.presentation.browse.components.BaseSourceItem
|
|
||||||
import eu.kanade.presentation.components.AppBar
|
|
||||||
import eu.kanade.presentation.more.settings.widget.SwitchPreferenceWidget
|
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.SourcesFilterScreenModel
|
|
||||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
|
||||||
import tachiyomi.domain.source.model.Source
|
|
||||||
import tachiyomi.i18n.MR
|
|
||||||
import tachiyomi.presentation.core.components.FastScrollLazyColumn
|
|
||||||
import tachiyomi.presentation.core.components.material.Scaffold
|
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
|
||||||
import tachiyomi.presentation.core.screens.EmptyScreen
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun SourcesFilterScreen(
|
|
||||||
navigateUp: () -> Unit,
|
|
||||||
state: SourcesFilterScreenModel.State.Success,
|
|
||||||
onClickLanguage: (String) -> Unit,
|
|
||||||
onClickSource: (Source) -> Unit,
|
|
||||||
) {
|
|
||||||
Scaffold(
|
|
||||||
topBar = { scrollBehavior ->
|
|
||||||
AppBar(
|
|
||||||
title = stringResource(MR.strings.label_sources),
|
|
||||||
navigateUp = navigateUp,
|
|
||||||
scrollBehavior = scrollBehavior,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
) { contentPadding ->
|
|
||||||
if (state.isEmpty) {
|
|
||||||
EmptyScreen(
|
|
||||||
stringRes = MR.strings.source_filter_empty_screen,
|
|
||||||
modifier = Modifier.padding(contentPadding),
|
|
||||||
)
|
|
||||||
return@Scaffold
|
|
||||||
}
|
|
||||||
SourcesFilterContent(
|
|
||||||
contentPadding = contentPadding,
|
|
||||||
state = state,
|
|
||||||
onClickLanguage = onClickLanguage,
|
|
||||||
onClickSource = onClickSource,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun SourcesFilterContent(
|
|
||||||
contentPadding: PaddingValues,
|
|
||||||
state: SourcesFilterScreenModel.State.Success,
|
|
||||||
onClickLanguage: (String) -> Unit,
|
|
||||||
onClickSource: (Source) -> Unit,
|
|
||||||
) {
|
|
||||||
FastScrollLazyColumn(
|
|
||||||
contentPadding = contentPadding,
|
|
||||||
) {
|
|
||||||
state.items.forEach { (language, sources) ->
|
|
||||||
val enabled = language in state.enabledLanguages
|
|
||||||
item(
|
|
||||||
key = language,
|
|
||||||
contentType = "source-filter-header",
|
|
||||||
) {
|
|
||||||
SourcesFilterHeader(
|
|
||||||
modifier = Modifier.animateItemPlacement(),
|
|
||||||
language = language,
|
|
||||||
enabled = enabled,
|
|
||||||
onClickItem = onClickLanguage,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (enabled) {
|
|
||||||
items(
|
|
||||||
items = sources,
|
|
||||||
key = { "source-filter-${it.key()}" },
|
|
||||||
contentType = { "source-filter-item" },
|
|
||||||
) { source ->
|
|
||||||
SourcesFilterItem(
|
|
||||||
modifier = Modifier.animateItemPlacement(),
|
|
||||||
source = source,
|
|
||||||
enabled = "${source.id}" !in state.disabledSources,
|
|
||||||
onClickItem = onClickSource,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun SourcesFilterHeader(
|
|
||||||
language: String,
|
|
||||||
enabled: Boolean,
|
|
||||||
onClickItem: (String) -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
SwitchPreferenceWidget(
|
|
||||||
modifier = modifier,
|
|
||||||
title = LocaleHelper.getSourceDisplayName(language, LocalContext.current),
|
|
||||||
checked = enabled,
|
|
||||||
onCheckedChanged = { onClickItem(language) },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun SourcesFilterItem(
|
|
||||||
source: Source,
|
|
||||||
enabled: Boolean,
|
|
||||||
onClickItem: (Source) -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
BaseSourceItem(
|
|
||||||
modifier = modifier,
|
|
||||||
source = source,
|
|
||||||
showLanguageInContent = false,
|
|
||||||
onClickItem = { onClickItem(source) },
|
|
||||||
action = {
|
|
||||||
Checkbox(checked = enabled, onCheckedChange = null)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,204 +0,0 @@
|
|||||||
package eu.kanade.presentation.browse
|
|
||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.lazy.items
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.filled.PushPin
|
|
||||||
import androidx.compose.material.icons.outlined.PushPin
|
|
||||||
import androidx.compose.material3.AlertDialog
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.LocalTextStyle
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.material3.TextButton
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import eu.kanade.presentation.browse.components.BaseSourceItem
|
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.SourcesScreenModel
|
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreenModel.Listing
|
|
||||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
|
||||||
import tachiyomi.domain.source.model.Pin
|
|
||||||
import tachiyomi.domain.source.model.Source
|
|
||||||
import tachiyomi.i18n.MR
|
|
||||||
import tachiyomi.presentation.core.components.ScrollbarLazyColumn
|
|
||||||
import tachiyomi.presentation.core.components.material.SecondaryItemAlpha
|
|
||||||
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.LoadingScreen
|
|
||||||
import tachiyomi.presentation.core.theme.header
|
|
||||||
import tachiyomi.presentation.core.util.plus
|
|
||||||
import tachiyomi.source.local.isLocal
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun SourcesScreen(
|
|
||||||
state: SourcesScreenModel.State,
|
|
||||||
contentPadding: PaddingValues,
|
|
||||||
onClickItem: (Source, Listing) -> Unit,
|
|
||||||
onClickPin: (Source) -> Unit,
|
|
||||||
onLongClickItem: (Source) -> Unit,
|
|
||||||
) {
|
|
||||||
when {
|
|
||||||
state.isLoading -> LoadingScreen(Modifier.padding(contentPadding))
|
|
||||||
state.isEmpty -> EmptyScreen(
|
|
||||||
stringRes = MR.strings.source_empty_screen,
|
|
||||||
modifier = Modifier.padding(contentPadding),
|
|
||||||
)
|
|
||||||
else -> {
|
|
||||||
ScrollbarLazyColumn(
|
|
||||||
contentPadding = contentPadding + topSmallPaddingValues,
|
|
||||||
) {
|
|
||||||
items(
|
|
||||||
items = state.items,
|
|
||||||
contentType = {
|
|
||||||
when (it) {
|
|
||||||
is SourceUiModel.Header -> "header"
|
|
||||||
is SourceUiModel.Item -> "item"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
key = {
|
|
||||||
when (it) {
|
|
||||||
is SourceUiModel.Header -> it.hashCode()
|
|
||||||
is SourceUiModel.Item -> "source-${it.source.key()}"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
) { model ->
|
|
||||||
when (model) {
|
|
||||||
is SourceUiModel.Header -> {
|
|
||||||
SourceHeader(
|
|
||||||
modifier = Modifier.animateItemPlacement(),
|
|
||||||
language = model.language,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
is SourceUiModel.Item -> SourceItem(
|
|
||||||
modifier = Modifier.animateItemPlacement(),
|
|
||||||
source = model.source,
|
|
||||||
onClickItem = onClickItem,
|
|
||||||
onLongClickItem = onLongClickItem,
|
|
||||||
onClickPin = onClickPin,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun SourceHeader(
|
|
||||||
language: String,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
val context = LocalContext.current
|
|
||||||
Text(
|
|
||||||
text = LocaleHelper.getSourceDisplayName(language, context),
|
|
||||||
modifier = modifier
|
|
||||||
.padding(horizontal = MaterialTheme.padding.medium, vertical = MaterialTheme.padding.small),
|
|
||||||
style = MaterialTheme.typography.header,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun SourceItem(
|
|
||||||
source: Source,
|
|
||||||
onClickItem: (Source, Listing) -> Unit,
|
|
||||||
onLongClickItem: (Source) -> Unit,
|
|
||||||
onClickPin: (Source) -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
BaseSourceItem(
|
|
||||||
modifier = modifier,
|
|
||||||
source = source,
|
|
||||||
onClickItem = { onClickItem(source, Listing.Popular) },
|
|
||||||
onLongClickItem = { onLongClickItem(source) },
|
|
||||||
action = {
|
|
||||||
if (source.supportsLatest) {
|
|
||||||
TextButton(onClick = { onClickItem(source, Listing.Latest) }) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(MR.strings.latest),
|
|
||||||
style = LocalTextStyle.current.copy(
|
|
||||||
color = MaterialTheme.colorScheme.primary,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
SourcePinButton(
|
|
||||||
isPinned = Pin.Pinned in source.pin,
|
|
||||||
onClick = { onClickPin(source) },
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun SourcePinButton(
|
|
||||||
isPinned: Boolean,
|
|
||||||
onClick: () -> Unit,
|
|
||||||
) {
|
|
||||||
val icon = if (isPinned) Icons.Filled.PushPin else Icons.Outlined.PushPin
|
|
||||||
val tint = if (isPinned) {
|
|
||||||
MaterialTheme.colorScheme.primary
|
|
||||||
} else {
|
|
||||||
MaterialTheme.colorScheme.onBackground.copy(
|
|
||||||
alpha = SecondaryItemAlpha,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
val description = if (isPinned) MR.strings.action_unpin else MR.strings.action_pin
|
|
||||||
IconButton(onClick = onClick) {
|
|
||||||
Icon(
|
|
||||||
imageVector = icon,
|
|
||||||
tint = tint,
|
|
||||||
contentDescription = stringResource(description),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun SourceOptionsDialog(
|
|
||||||
source: Source,
|
|
||||||
onClickPin: () -> Unit,
|
|
||||||
onClickDisable: () -> Unit,
|
|
||||||
onDismiss: () -> Unit,
|
|
||||||
) {
|
|
||||||
AlertDialog(
|
|
||||||
title = {
|
|
||||||
Text(text = source.visualName)
|
|
||||||
},
|
|
||||||
text = {
|
|
||||||
Column {
|
|
||||||
val textId = if (Pin.Pinned in source.pin) MR.strings.action_unpin else MR.strings.action_pin
|
|
||||||
Text(
|
|
||||||
text = stringResource(textId),
|
|
||||||
modifier = Modifier
|
|
||||||
.clickable(onClick = onClickPin)
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(vertical = 16.dp),
|
|
||||||
)
|
|
||||||
if (!source.isLocal()) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(MR.strings.action_disable),
|
|
||||||
modifier = Modifier
|
|
||||||
.clickable(onClick = onClickDisable)
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(vertical = 16.dp),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onDismissRequest = onDismiss,
|
|
||||||
confirmButton = {},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
sealed interface SourceUiModel {
|
|
||||||
data class Item(val source: Source) : SourceUiModel
|
|
||||||
data class Header(val language: String) : SourceUiModel
|
|
||||||
}
|
|
@ -1,35 +0,0 @@
|
|||||||
package eu.kanade.presentation.browse.components
|
|
||||||
|
|
||||||
import androidx.compose.foundation.combinedClickable
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.RowScope
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import tachiyomi.presentation.core.components.material.padding
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun BaseBrowseItem(
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
onClickItem: () -> Unit = {},
|
|
||||||
onLongClickItem: () -> Unit = {},
|
|
||||||
icon: @Composable RowScope.() -> Unit = {},
|
|
||||||
action: @Composable RowScope.() -> Unit = {},
|
|
||||||
content: @Composable RowScope.() -> Unit = {},
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = modifier
|
|
||||||
.combinedClickable(
|
|
||||||
onClick = onClickItem,
|
|
||||||
onLongClick = onLongClickItem,
|
|
||||||
)
|
|
||||||
.padding(horizontal = MaterialTheme.padding.medium, vertical = MaterialTheme.padding.small),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
) {
|
|
||||||
icon()
|
|
||||||
content()
|
|
||||||
action()
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,67 +0,0 @@
|
|||||||
package eu.kanade.presentation.browse.components
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.RowScope
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
|
||||||
import tachiyomi.domain.source.model.Source
|
|
||||||
import tachiyomi.presentation.core.components.material.padding
|
|
||||||
import tachiyomi.presentation.core.util.secondaryItemAlpha
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun BaseSourceItem(
|
|
||||||
source: Source,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
showLanguageInContent: Boolean = true,
|
|
||||||
onClickItem: () -> Unit = {},
|
|
||||||
onLongClickItem: () -> Unit = {},
|
|
||||||
icon: @Composable RowScope.(Source) -> Unit = defaultIcon,
|
|
||||||
action: @Composable RowScope.(Source) -> Unit = {},
|
|
||||||
content: @Composable RowScope.(Source, String?) -> Unit = defaultContent,
|
|
||||||
) {
|
|
||||||
val sourceLangString = LocaleHelper.getSourceDisplayName(source.lang, LocalContext.current).takeIf {
|
|
||||||
showLanguageInContent
|
|
||||||
}
|
|
||||||
BaseBrowseItem(
|
|
||||||
modifier = modifier,
|
|
||||||
onClickItem = onClickItem,
|
|
||||||
onLongClickItem = onLongClickItem,
|
|
||||||
icon = { icon.invoke(this, source) },
|
|
||||||
action = { action.invoke(this, source) },
|
|
||||||
content = { content.invoke(this, source, sourceLangString) },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val defaultIcon: @Composable RowScope.(Source) -> Unit = { source ->
|
|
||||||
SourceIcon(source = source)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val defaultContent: @Composable RowScope.(Source, String?) -> Unit = { source, sourceLangString ->
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(horizontal = MaterialTheme.padding.medium)
|
|
||||||
.weight(1f),
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = source.name,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
)
|
|
||||||
if (sourceLangString != null) {
|
|
||||||
Text(
|
|
||||||
modifier = Modifier.secondaryItemAlpha(),
|
|
||||||
text = sourceLangString,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,15 +0,0 @@
|
|||||||
package eu.kanade.presentation.browse.components
|
|
||||||
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.outlined.CollectionsBookmark
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import tachiyomi.presentation.core.components.Badge
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
internal fun InLibraryBadge(enabled: Boolean) {
|
|
||||||
if (enabled) {
|
|
||||||
Badge(
|
|
||||||
imageVector = Icons.Outlined.CollectionsBookmark,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,148 +0,0 @@
|
|||||||
package eu.kanade.presentation.browse.components
|
|
||||||
|
|
||||||
import android.util.DisplayMetrics
|
|
||||||
import androidx.compose.foundation.Image
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.aspectRatio
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.filled.Dangerous
|
|
||||||
import androidx.compose.material.icons.filled.Warning
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.State
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.produceState
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.graphics.ColorFilter
|
|
||||||
import androidx.compose.ui.graphics.ImageBitmap
|
|
||||||
import androidx.compose.ui.graphics.asImageBitmap
|
|
||||||
import androidx.compose.ui.graphics.painter.ColorPainter
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.imageResource
|
|
||||||
import androidx.compose.ui.res.painterResource
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.core.graphics.drawable.toBitmap
|
|
||||||
import coil.compose.AsyncImage
|
|
||||||
import eu.kanade.domain.source.model.icon
|
|
||||||
import eu.kanade.presentation.util.rememberResourceBitmapPainter
|
|
||||||
import eu.kanade.tachiyomi.R
|
|
||||||
import eu.kanade.tachiyomi.extension.model.Extension
|
|
||||||
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
|
|
||||||
import tachiyomi.core.util.lang.withIOContext
|
|
||||||
import tachiyomi.domain.source.model.Source
|
|
||||||
import tachiyomi.source.local.isLocal
|
|
||||||
|
|
||||||
private val defaultModifier = Modifier
|
|
||||||
.height(40.dp)
|
|
||||||
.aspectRatio(1f)
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun SourceIcon(
|
|
||||||
source: Source,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
val icon = source.icon
|
|
||||||
|
|
||||||
when {
|
|
||||||
source.isStub && icon == null -> {
|
|
||||||
Image(
|
|
||||||
imageVector = Icons.Filled.Warning,
|
|
||||||
contentDescription = null,
|
|
||||||
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.error),
|
|
||||||
modifier = modifier.then(defaultModifier),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
icon != null -> {
|
|
||||||
Image(
|
|
||||||
bitmap = icon,
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = modifier.then(defaultModifier),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
source.isLocal() -> {
|
|
||||||
Image(
|
|
||||||
painter = painterResource(R.mipmap.ic_local_source),
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = modifier.then(defaultModifier),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
Image(
|
|
||||||
painter = painterResource(R.mipmap.ic_default_source),
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = modifier.then(defaultModifier),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun ExtensionIcon(
|
|
||||||
extension: Extension,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
density: Int = DisplayMetrics.DENSITY_DEFAULT,
|
|
||||||
) {
|
|
||||||
when (extension) {
|
|
||||||
is Extension.Available -> {
|
|
||||||
AsyncImage(
|
|
||||||
model = extension.iconUrl,
|
|
||||||
contentDescription = null,
|
|
||||||
placeholder = ColorPainter(Color(0x1F888888)),
|
|
||||||
error = rememberResourceBitmapPainter(id = R.drawable.cover_error),
|
|
||||||
modifier = modifier
|
|
||||||
.clip(MaterialTheme.shapes.extraSmall),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
is Extension.Installed -> {
|
|
||||||
val icon by extension.getIcon(density)
|
|
||||||
when (icon) {
|
|
||||||
Result.Loading -> Box(modifier = modifier)
|
|
||||||
is Result.Success -> Image(
|
|
||||||
bitmap = (icon as Result.Success<ImageBitmap>).value,
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = modifier,
|
|
||||||
)
|
|
||||||
Result.Error -> Image(
|
|
||||||
bitmap = ImageBitmap.imageResource(id = R.mipmap.ic_default_source),
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = modifier,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is Extension.Untrusted -> Image(
|
|
||||||
imageVector = Icons.Filled.Dangerous,
|
|
||||||
contentDescription = null,
|
|
||||||
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.error),
|
|
||||||
modifier = modifier.then(defaultModifier),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun Extension.getIcon(density: Int = DisplayMetrics.DENSITY_DEFAULT): State<Result<ImageBitmap>> {
|
|
||||||
val context = LocalContext.current
|
|
||||||
return produceState<Result<ImageBitmap>>(initialValue = Result.Loading, this) {
|
|
||||||
withIOContext {
|
|
||||||
value = try {
|
|
||||||
val appInfo = ExtensionLoader.getExtensionPackageInfoFromPkgName(context, pkgName)!!.applicationInfo
|
|
||||||
val appResources = context.packageManager.getResourcesForApplication(appInfo)
|
|
||||||
Result.Success(
|
|
||||||
appResources.getDrawableForDensity(appInfo.icon, density, null)!!
|
|
||||||
.toBitmap()
|
|
||||||
.asImageBitmap(),
|
|
||||||
)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Result.Error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sealed class Result<out T> {
|
|
||||||
data object Loading : Result<Nothing>()
|
|
||||||
data object Error : Result<Nothing>()
|
|
||||||
data class Success<out T>(val value: T) : Result<T>()
|
|
||||||
}
|
|
@ -1,80 +0,0 @@
|
|||||||
package eu.kanade.presentation.browse.components
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
|
||||||
import androidx.compose.foundation.lazy.grid.GridCells
|
|
||||||
import androidx.compose.foundation.lazy.grid.GridItemSpan
|
|
||||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.collectAsState
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.paging.LoadState
|
|
||||||
import androidx.paging.compose.LazyPagingItems
|
|
||||||
import eu.kanade.presentation.library.components.CommonMangaItemDefaults
|
|
||||||
import eu.kanade.presentation.library.components.MangaComfortableGridItem
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
|
||||||
import tachiyomi.domain.manga.model.Manga
|
|
||||||
import tachiyomi.domain.manga.model.MangaCover
|
|
||||||
import tachiyomi.presentation.core.util.plus
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun BrowseSourceComfortableGrid(
|
|
||||||
mangaList: LazyPagingItems<StateFlow<Manga>>,
|
|
||||||
columns: GridCells,
|
|
||||||
contentPadding: PaddingValues,
|
|
||||||
onMangaClick: (Manga) -> Unit,
|
|
||||||
onMangaLongClick: (Manga) -> Unit,
|
|
||||||
) {
|
|
||||||
LazyVerticalGrid(
|
|
||||||
columns = columns,
|
|
||||||
contentPadding = contentPadding + PaddingValues(8.dp),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(CommonMangaItemDefaults.GridVerticalSpacer),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(CommonMangaItemDefaults.GridHorizontalSpacer),
|
|
||||||
) {
|
|
||||||
if (mangaList.loadState.prepend is LoadState.Loading) {
|
|
||||||
item(span = { GridItemSpan(maxLineSpan) }) {
|
|
||||||
BrowseSourceLoadingItem()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
items(count = mangaList.itemCount) { index ->
|
|
||||||
val manga by mangaList[index]?.collectAsState() ?: return@items
|
|
||||||
BrowseSourceComfortableGridItem(
|
|
||||||
manga = manga,
|
|
||||||
onClick = { onMangaClick(manga) },
|
|
||||||
onLongClick = { onMangaLongClick(manga) },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mangaList.loadState.refresh is LoadState.Loading || mangaList.loadState.append is LoadState.Loading) {
|
|
||||||
item(span = { GridItemSpan(maxLineSpan) }) {
|
|
||||||
BrowseSourceLoadingItem()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun BrowseSourceComfortableGridItem(
|
|
||||||
manga: Manga,
|
|
||||||
onClick: () -> Unit = {},
|
|
||||||
onLongClick: () -> Unit = onClick,
|
|
||||||
) {
|
|
||||||
MangaComfortableGridItem(
|
|
||||||
title = manga.title,
|
|
||||||
coverData = MangaCover(
|
|
||||||
mangaId = manga.id,
|
|
||||||
sourceId = manga.source,
|
|
||||||
isMangaFavorite = manga.favorite,
|
|
||||||
url = manga.thumbnailUrl,
|
|
||||||
lastModified = manga.coverLastModified,
|
|
||||||
),
|
|
||||||
coverAlpha = if (manga.favorite) CommonMangaItemDefaults.BrowseFavoriteCoverAlpha else 1f,
|
|
||||||
coverBadgeStart = {
|
|
||||||
InLibraryBadge(enabled = manga.favorite)
|
|
||||||
},
|
|
||||||
onLongClick = onLongClick,
|
|
||||||
onClick = onClick,
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,80 +0,0 @@
|
|||||||
package eu.kanade.presentation.browse.components
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
|
||||||
import androidx.compose.foundation.lazy.grid.GridCells
|
|
||||||
import androidx.compose.foundation.lazy.grid.GridItemSpan
|
|
||||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.collectAsState
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.paging.LoadState
|
|
||||||
import androidx.paging.compose.LazyPagingItems
|
|
||||||
import eu.kanade.presentation.library.components.CommonMangaItemDefaults
|
|
||||||
import eu.kanade.presentation.library.components.MangaCompactGridItem
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
|
||||||
import tachiyomi.domain.manga.model.Manga
|
|
||||||
import tachiyomi.domain.manga.model.MangaCover
|
|
||||||
import tachiyomi.presentation.core.util.plus
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun BrowseSourceCompactGrid(
|
|
||||||
mangaList: LazyPagingItems<StateFlow<Manga>>,
|
|
||||||
columns: GridCells,
|
|
||||||
contentPadding: PaddingValues,
|
|
||||||
onMangaClick: (Manga) -> Unit,
|
|
||||||
onMangaLongClick: (Manga) -> Unit,
|
|
||||||
) {
|
|
||||||
LazyVerticalGrid(
|
|
||||||
columns = columns,
|
|
||||||
contentPadding = contentPadding + PaddingValues(8.dp),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(CommonMangaItemDefaults.GridVerticalSpacer),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(CommonMangaItemDefaults.GridHorizontalSpacer),
|
|
||||||
) {
|
|
||||||
if (mangaList.loadState.prepend is LoadState.Loading) {
|
|
||||||
item(span = { GridItemSpan(maxLineSpan) }) {
|
|
||||||
BrowseSourceLoadingItem()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
items(count = mangaList.itemCount) { index ->
|
|
||||||
val manga by mangaList[index]?.collectAsState() ?: return@items
|
|
||||||
BrowseSourceCompactGridItem(
|
|
||||||
manga = manga,
|
|
||||||
onClick = { onMangaClick(manga) },
|
|
||||||
onLongClick = { onMangaLongClick(manga) },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mangaList.loadState.refresh is LoadState.Loading || mangaList.loadState.append is LoadState.Loading) {
|
|
||||||
item(span = { GridItemSpan(maxLineSpan) }) {
|
|
||||||
BrowseSourceLoadingItem()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun BrowseSourceCompactGridItem(
|
|
||||||
manga: Manga,
|
|
||||||
onClick: () -> Unit = {},
|
|
||||||
onLongClick: () -> Unit = onClick,
|
|
||||||
) {
|
|
||||||
MangaCompactGridItem(
|
|
||||||
title = manga.title,
|
|
||||||
coverData = MangaCover(
|
|
||||||
mangaId = manga.id,
|
|
||||||
sourceId = manga.source,
|
|
||||||
isMangaFavorite = manga.favorite,
|
|
||||||
url = manga.thumbnailUrl,
|
|
||||||
lastModified = manga.coverLastModified,
|
|
||||||
),
|
|
||||||
coverAlpha = if (manga.favorite) CommonMangaItemDefaults.BrowseFavoriteCoverAlpha else 1f,
|
|
||||||
coverBadgeStart = {
|
|
||||||
InLibraryBadge(enabled = manga.favorite)
|
|
||||||
},
|
|
||||||
onLongClick = onLongClick,
|
|
||||||
onClick = onClick,
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,41 +0,0 @@
|
|||||||
package eu.kanade.presentation.browse.components
|
|
||||||
|
|
||||||
import androidx.compose.material3.AlertDialog
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.material3.TextButton
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import tachiyomi.domain.manga.model.Manga
|
|
||||||
import tachiyomi.i18n.MR
|
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun RemoveMangaDialog(
|
|
||||||
onDismissRequest: () -> Unit,
|
|
||||||
onConfirm: () -> Unit,
|
|
||||||
mangaToRemove: Manga,
|
|
||||||
) {
|
|
||||||
AlertDialog(
|
|
||||||
onDismissRequest = onDismissRequest,
|
|
||||||
dismissButton = {
|
|
||||||
TextButton(onClick = onDismissRequest) {
|
|
||||||
Text(text = stringResource(MR.strings.action_cancel))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
confirmButton = {
|
|
||||||
TextButton(
|
|
||||||
onClick = {
|
|
||||||
onDismissRequest()
|
|
||||||
onConfirm()
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
Text(text = stringResource(MR.strings.action_remove))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
title = {
|
|
||||||
Text(text = stringResource(MR.strings.are_you_sure))
|
|
||||||
},
|
|
||||||
text = {
|
|
||||||
Text(text = stringResource(MR.strings.remove_manga, mangaToRemove.title))
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,73 +0,0 @@
|
|||||||
package eu.kanade.presentation.browse.components
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.collectAsState
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.paging.LoadState
|
|
||||||
import androidx.paging.compose.LazyPagingItems
|
|
||||||
import eu.kanade.presentation.library.components.CommonMangaItemDefaults
|
|
||||||
import eu.kanade.presentation.library.components.MangaListItem
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
|
||||||
import tachiyomi.domain.manga.model.Manga
|
|
||||||
import tachiyomi.domain.manga.model.MangaCover
|
|
||||||
import tachiyomi.presentation.core.util.plus
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun BrowseSourceList(
|
|
||||||
mangaList: LazyPagingItems<StateFlow<Manga>>,
|
|
||||||
contentPadding: PaddingValues,
|
|
||||||
onMangaClick: (Manga) -> Unit,
|
|
||||||
onMangaLongClick: (Manga) -> Unit,
|
|
||||||
) {
|
|
||||||
LazyColumn(
|
|
||||||
contentPadding = contentPadding + PaddingValues(vertical = 8.dp),
|
|
||||||
) {
|
|
||||||
item {
|
|
||||||
if (mangaList.loadState.prepend is LoadState.Loading) {
|
|
||||||
BrowseSourceLoadingItem()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
items(count = mangaList.itemCount) { index ->
|
|
||||||
val manga by mangaList[index]?.collectAsState() ?: return@items
|
|
||||||
BrowseSourceListItem(
|
|
||||||
manga = manga,
|
|
||||||
onClick = { onMangaClick(manga) },
|
|
||||||
onLongClick = { onMangaLongClick(manga) },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
item {
|
|
||||||
if (mangaList.loadState.refresh is LoadState.Loading || mangaList.loadState.append is LoadState.Loading) {
|
|
||||||
BrowseSourceLoadingItem()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun BrowseSourceListItem(
|
|
||||||
manga: Manga,
|
|
||||||
onClick: () -> Unit = {},
|
|
||||||
onLongClick: () -> Unit = onClick,
|
|
||||||
) {
|
|
||||||
MangaListItem(
|
|
||||||
title = manga.title,
|
|
||||||
coverData = MangaCover(
|
|
||||||
mangaId = manga.id,
|
|
||||||
sourceId = manga.source,
|
|
||||||
isMangaFavorite = manga.favorite,
|
|
||||||
url = manga.thumbnailUrl,
|
|
||||||
lastModified = manga.coverLastModified,
|
|
||||||
),
|
|
||||||
coverAlpha = if (manga.favorite) CommonMangaItemDefaults.BrowseFavoriteCoverAlpha else 1f,
|
|
||||||
badge = {
|
|
||||||
InLibraryBadge(enabled = manga.favorite)
|
|
||||||
},
|
|
||||||
onLongClick = onLongClick,
|
|
||||||
onClick = onClick,
|
|
||||||
)
|
|
||||||
}
|
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user