mirror of
https://github.com/mihonapp/mihon.git
synced 2025-06-26 19:17:51 +02:00
Compare commits
225 Commits
Author | SHA1 | Date | |
---|---|---|---|
1f79444a53 | |||
8811d951d0 | |||
a89651810d | |||
431c04e54f | |||
f461c71625 | |||
b635789740 | |||
f00e03e5ea | |||
6db2becd30 | |||
e58945a209 | |||
03e4eb1061 | |||
09a3509d79 | |||
b3a11eca0f | |||
650c2dc6e7 | |||
d4adb664cc | |||
5194bdb229 | |||
87ec71142b | |||
85f2996ae9 | |||
e296d56e09 | |||
dd676b6d14 | |||
7c7bd72c8e | |||
c7e44aa22f | |||
ac4f98e152 | |||
e0d23cd688 | |||
3966a917ee | |||
be33a57d43 | |||
4a71022a60 | |||
34ac39e7e5 | |||
26ddc6e3aa | |||
1dc4a52f61 | |||
473a4fec70 | |||
1919c2d925 | |||
71e31e6c03 | |||
c01df7f0a1 | |||
6024f6175b | |||
33500e5b69 | |||
17899a6d6d | |||
4c3eb68d3a | |||
29ced9642d | |||
af82591d85 | |||
5bc4a446ec | |||
83e93b254e | |||
49c7dd0cac | |||
96d2fb62e4 | |||
c76a136d3f | |||
940409a4c3 | |||
071dd88ef8 | |||
a58a4634e2 | |||
5979e72662 | |||
010436e797 | |||
980709cccb | |||
fe80356756 | |||
cecf532ffd | |||
6cb255e60a | |||
b46fb7d1e1 | |||
8874193927 | |||
a4515ad251 | |||
55b0b57699 | |||
aab7795b4c | |||
196a8e6829 | |||
972cd98d7b | |||
a16b5d241b | |||
bfa918140f | |||
0721de5b81 | |||
a409fde519 | |||
8e34a30dce | |||
ba43462041 | |||
c8ae936ce9 | |||
853f949140 | |||
615b01a006 | |||
0eb5a3176b | |||
867a5a3ea0 | |||
42eaaa497f | |||
96c894ce5b | |||
c0214103a9 | |||
2b76a97989 | |||
9d77052d9c | |||
b4981058a2 | |||
032aa64195 | |||
7c8e8317a8 | |||
eb1cfc4cd4 | |||
f1e5cccee7 | |||
bc2ed763bd | |||
a35995b898 | |||
b1f46ed830 | |||
6c1565a7d4 | |||
2ca6b655ad | |||
a83a481ac8 | |||
65a8b63b3b | |||
b20ca36db9 | |||
189f92d7e8 | |||
cdd4ec6233 | |||
ef1bb4e800 | |||
c475acd1ea | |||
7d50d7ff52 | |||
28522f4f90 | |||
ec3a227a02 | |||
89decf3474 | |||
0b2794e843 | |||
554dfb5874 | |||
9c30fa1da3 | |||
e81bd61e24 | |||
7a0b54bb38 | |||
f060daf8c4 | |||
c1976ef599 | |||
f16fb4e1e4 | |||
5da2c82f47 | |||
d443245d66 | |||
9be3eea5fd | |||
07a9fd061d | |||
2a070c0b1e | |||
7b5106d206 | |||
821d9cdb02 | |||
28575936d3 | |||
83a04da4a0 | |||
0894b1394f | |||
eb33d3c991 | |||
d7f01abf3a | |||
80635343ae | |||
2b38b4e022 | |||
4ecde9fc39 | |||
445ee274c5 | |||
f2bdc514e8 | |||
5afff31f72 | |||
2dfafa387b | |||
7318f4f5dd | |||
175b77fe6f | |||
346652e508 | |||
f0eb42e72d | |||
37100f0937 | |||
ac980a4dbf | |||
a8b53499af | |||
a8aeae329e | |||
52911539b8 | |||
3026ff241b | |||
2466a079d5 | |||
ed9fdf49e2 | |||
668d962233 | |||
996f770935 | |||
041a6dd919 | |||
dbad60d03b | |||
27a60423dc | |||
5a37d38a84 | |||
6f566e67d5 | |||
dd490f2ac9 | |||
5409af0a6c | |||
0ed0d903cc | |||
85be4c492d | |||
c06ad8b87e | |||
b89acb5853 | |||
7890511a53 | |||
3aa4e6eb93 | |||
f8eb9f94f4 | |||
c581b9eeb9 | |||
ffd9c6995a | |||
ef600c0956 | |||
5c0a43e8d6 | |||
8e332dba30 | |||
cd07027192 | |||
da2b30268a | |||
1163aa4e4e | |||
ddb856edc7 | |||
9c426bc216 | |||
382852d0bd | |||
87ae86e1be | |||
9547311d7d | |||
1613d561c1 | |||
538478cac8 | |||
267ecce958 | |||
fae43fedfa | |||
c447022092 | |||
56042ad0b6 | |||
45da036789 | |||
b47b702a52 | |||
869424cd16 | |||
b9fd01315b | |||
a72098b862 | |||
86016de6cb | |||
592b9fedb9 | |||
d06984e3a3 | |||
6b55ee250d | |||
10eef282fa | |||
f312936629 | |||
d53bb4c337 | |||
1a605e27bc | |||
08ee858f64 | |||
af70fe3e7e | |||
29c5c0af50 | |||
9420b750d2 | |||
6f5328f663 | |||
90214d02d7 | |||
2f07f226b8 | |||
a8ad19a89d | |||
57c07250fd | |||
4a3e4a7c5c | |||
c284a23afb | |||
fad1449de3 | |||
f18d161eaf | |||
88054b453a | |||
c560373596 | |||
d698d03521 | |||
d8c8d7c588 | |||
9120e82517 | |||
e214746536 | |||
142396400c | |||
51d48bdde6 | |||
44b055c019 | |||
790d7b9170 | |||
d8719ceee9 | |||
71ddb16574 | |||
2932ed670f | |||
ae2a6a3d4f | |||
30061ada58 | |||
a131e28b60 | |||
8c1662cfdb | |||
299e52e877 | |||
95b253db09 | |||
067cb2452e | |||
45e4092335 | |||
7659a997cf | |||
aa5e428222 | |||
319e4360c8 | |||
f5c6e80dbb | |||
7108993936 | |||
b6553bdc34 | |||
19fe689969 |
7
.editorconfig
Normal file
7
.editorconfig
Normal file
@ -0,0 +1,7 @@
|
||||
[*.{kt,kts}]
|
||||
indent_size=4
|
||||
insert_final_newline=true
|
||||
ij_kotlin_allow_trailing_comma=true
|
||||
ij_kotlin_allow_trailing_comma_on_call_site=true
|
||||
ij_kotlin_name_count_to_use_star_import = 2147483647
|
||||
ij_kotlin_name_count_to_use_star_import_for_members = 2147483647
|
2
.github/ISSUE_TEMPLATE.md
vendored
2
.github/ISSUE_TEMPLATE.md
vendored
@ -3,7 +3,7 @@
|
||||
I acknowledge that:
|
||||
|
||||
- I have updated:
|
||||
- To the latest version of the app (stable is v0.13.1)
|
||||
- To the latest version of the app (stable is v0.13.6)
|
||||
- All extensions
|
||||
- I have tried the troubleshooting guide: https://tachiyomi.org/help/guides/troubleshooting-problems/
|
||||
- If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions
|
||||
|
4
.github/ISSUE_TEMPLATE/report_issue.yml
vendored
4
.github/ISSUE_TEMPLATE/report_issue.yml
vendored
@ -53,7 +53,7 @@ body:
|
||||
label: Tachiyomi version
|
||||
description: You can find your Tachiyomi version in **More → About**.
|
||||
placeholder: |
|
||||
Example: "0.13.1"
|
||||
Example: "0.13.6"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@ -98,7 +98,7 @@ body:
|
||||
required: true
|
||||
- label: I have tried the [troubleshooting guide](https://tachiyomi.org/help/guides/troubleshooting/).
|
||||
required: true
|
||||
- label: I have updated the app to version **[0.13.1](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**.
|
||||
- label: I have updated the app to version **[0.13.6](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**.
|
||||
required: true
|
||||
- label: I have updated all installed extensions.
|
||||
required: true
|
||||
|
2
.github/ISSUE_TEMPLATE/request_feature.yml
vendored
2
.github/ISSUE_TEMPLATE/request_feature.yml
vendored
@ -33,7 +33,7 @@ body:
|
||||
required: true
|
||||
- label: If this is an issue with an extension, I should be opening an issue in the [extensions repository](https://github.com/tachiyomiorg/tachiyomi-extensions/issues/new/choose).
|
||||
required: true
|
||||
- label: I have updated the app to version **[0.13.1](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**.
|
||||
- label: I have updated the app to version **[0.13.6](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**.
|
||||
required: true
|
||||
- label: I will fill out all of the requested information in this form.
|
||||
required: true
|
||||
|
6
.github/runner-files/ci-gradle.properties
vendored
6
.github/runner-files/ci-gradle.properties
vendored
@ -1,6 +0,0 @@
|
||||
org.gradle.daemon=false
|
||||
org.gradle.jvmargs=-Xmx5120m
|
||||
org.gradle.workers.max=2
|
||||
|
||||
kotlin.incremental=false
|
||||
kotlin.compiler.execution.strategy=in-process
|
24
.github/workflows/build_pull_request.yml
vendored
24
.github/workflows/build_pull_request.yml
vendored
@ -5,6 +5,13 @@ on:
|
||||
- '**.md'
|
||||
- 'app/src/main/res/**/strings.xml'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build app
|
||||
@ -12,22 +19,21 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Clone repo
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Validate Gradle Wrapper
|
||||
uses: gradle/wrapper-validation-action@v1
|
||||
|
||||
- name: Dependency Review
|
||||
uses: actions/dependency-review-action@v2
|
||||
|
||||
- name: Set up JDK 11
|
||||
uses: actions/setup-java@v1
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: 11
|
||||
distribution: adopt
|
||||
|
||||
- name: Copy CI gradle.properties
|
||||
run: |
|
||||
mkdir -p ~/.gradle
|
||||
cp .github/runner-files/ci-gradle.properties ~/.gradle/gradle.properties
|
||||
|
||||
- name: Build app
|
||||
- name: Build app and run unit tests
|
||||
uses: gradle/gradle-command-action@v2
|
||||
with:
|
||||
arguments: assembleStandardRelease
|
||||
arguments: assembleStandardRelease testStandardReleaseUnitTest
|
24
.github/workflows/build_push.yml
vendored
24
.github/workflows/build_push.yml
vendored
@ -6,38 +6,32 @@ on:
|
||||
tags:
|
||||
- v*
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build app
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Cancel previous runs
|
||||
uses: styfle/cancel-workflow-action@0.9.1
|
||||
with:
|
||||
access_token: ${{ github.token }}
|
||||
all_but_latest: true
|
||||
|
||||
- name: Clone repo
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Validate Gradle Wrapper
|
||||
uses: gradle/wrapper-validation-action@v1
|
||||
|
||||
- name: Set up JDK 11
|
||||
uses: actions/setup-java@v1
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: 11
|
||||
distribution: adopt
|
||||
|
||||
- name: Copy CI gradle.properties
|
||||
run: |
|
||||
mkdir -p ~/.gradle
|
||||
cp .github/runner-files/ci-gradle.properties ~/.gradle/gradle.properties
|
||||
|
||||
- name: Build app
|
||||
- name: Build app and run unit tests
|
||||
uses: gradle/gradle-command-action@v2
|
||||
with:
|
||||
arguments: assembleStandardRelease
|
||||
arguments: assembleStandardRelease testStandardReleaseUnitTest
|
||||
|
||||
# Sign APK and create release for tags
|
||||
|
||||
|
16
.github/workflows/cancel_pull_request.yml
vendored
16
.github/workflows/cancel_pull_request.yml
vendored
@ -1,16 +0,0 @@
|
||||
name: Cancel old pull request workflows
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["PR build check"]
|
||||
types:
|
||||
- requested
|
||||
|
||||
jobs:
|
||||
cancel:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: styfle/cancel-workflow-action@0.9.1
|
||||
with:
|
||||
all_but_latest: true
|
||||
workflow_id: ${{ github.event.workflow.id }}
|
32
.github/workflows/issue_closer.yml
vendored
32
.github/workflows/issue_closer.yml
vendored
@ -1,32 +0,0 @@
|
||||
name: Issue closer
|
||||
on:
|
||||
issues:
|
||||
types: [opened, edited, reopened]
|
||||
|
||||
jobs:
|
||||
autoclose:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Autoclose issues
|
||||
uses: arkon/issue-closer-action@v3.4
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
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"
|
||||
}
|
||||
]
|
21
.github/workflows/issue_moderator.yml
vendored
21
.github/workflows/issue_moderator.yml
vendored
@ -1,6 +1,8 @@
|
||||
name: Issue moderator
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened, edited, reopened]
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
@ -12,3 +14,22 @@ jobs:
|
||||
uses: tachiyomiorg/issue-moderator-action@v1
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
auto-close-rules: |
|
||||
[
|
||||
{
|
||||
"type": "body",
|
||||
"regex": ".*DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT.*",
|
||||
"message": "The acknowledgment section was not removed."
|
||||
},
|
||||
{
|
||||
"type": "body",
|
||||
"regex": ".*\\* (Tachiyomi version|Android version|Device): \\?.*",
|
||||
"message": "Requested information in the template was not filled out."
|
||||
},
|
||||
{
|
||||
"type": "both",
|
||||
"regex": "^(?!.*myanimelist.*).*(aniyomi|anime).*$",
|
||||
"ignoreCase": true,
|
||||
"message": "Tachiyomi does not support anime, and has no plans to support anime. In addition Tachiyomi is not affiliated with Aniyomi https://github.com/jmir1/aniyomi"
|
||||
}
|
||||
]
|
||||
|
@ -12,6 +12,21 @@ Pull requests are welcome!
|
||||
If you're interested in taking on [an open issue](https://github.com/tachiyomiorg/tachiyomi/issues), please comment on it so others are aware.
|
||||
You do not need to ask for permission nor an assignment.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before you start, please note that the ability to use following technologies is **required** and that existing contributors will not actively teach them to you.
|
||||
|
||||
- Basic [Android development](https://developer.android.com/)
|
||||
- [Kotlin](https://kotlinlang.org/)
|
||||
|
||||
### Tools
|
||||
|
||||
- [Android Studio](https://developer.android.com/studio)
|
||||
- Emulator or phone with developer options enabled to test changes.
|
||||
|
||||
## Getting help
|
||||
|
||||
- Join [the Discord server](https://discord.gg/tachiyomi) for online help and to ask questions while developing.
|
||||
|
||||
# Translations
|
||||
|
||||
@ -27,7 +42,7 @@ When creating a fork, remember to:
|
||||
- To avoid confusion with the main app:
|
||||
- Change the app name
|
||||
- Change the app icon
|
||||
- Change or disable the [app update checker](https://github.com/tachiyomiorg/tachiyomi/blob/master/app/src/main/java/eu/kanade/tachiyomi/data/updater/GithubUpdateChecker.kt)
|
||||
- Change or disable the [app update checker](https://github.com/tachiyomiorg/tachiyomi/blob/master/app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateChecker.kt)
|
||||
- To avoid installation conflicts:
|
||||
- Change the `applicationId` in [`build.gradle.kts`](https://github.com/tachiyomiorg/tachiyomi/blob/master/app/build.gradle.kts)
|
||||
- To avoid having your data polluting the main app's analytics and crash report services:
|
||||
|
@ -1,8 +1,5 @@
|
||||
import org.gradle.api.tasks.testing.logging.TestLogEvent
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.TimeZone
|
||||
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
@ -13,7 +10,7 @@ plugins {
|
||||
}
|
||||
|
||||
if (gradle.startParameter.taskRequests.toString().contains("Standard")) {
|
||||
apply(plugin = "com.google.gms.google-services")
|
||||
apply<com.google.gms.googleservices.GoogleServicesPlugin>()
|
||||
}
|
||||
|
||||
shortcutHelper.setFilePath("./shortcuts.xml")
|
||||
@ -21,6 +18,7 @@ shortcutHelper.setFilePath("./shortcuts.xml")
|
||||
val SUPPORTED_ABIS = setOf("armeabi-v7a", "arm64-v8a", "x86")
|
||||
|
||||
android {
|
||||
namespace = "eu.kanade.tachiyomi"
|
||||
compileSdk = AndroidConfig.compileSdk
|
||||
ndkVersion = AndroidConfig.ndk
|
||||
|
||||
@ -28,8 +26,8 @@ android {
|
||||
applicationId = "eu.kanade.tachiyomi"
|
||||
minSdk = AndroidConfig.minSdk
|
||||
targetSdk = AndroidConfig.targetSdk
|
||||
versionCode = 74
|
||||
versionName = "0.13.1"
|
||||
versionCode = 82
|
||||
versionName = "0.13.6"
|
||||
|
||||
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
||||
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
|
||||
@ -137,172 +135,145 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(kotlin("reflect", version = BuildPluginsVersion.KOTLIN))
|
||||
implementation(kotlinx.reflect)
|
||||
|
||||
val coroutinesVersion = "1.6.0"
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion")
|
||||
implementation(kotlinx.bundles.coroutines)
|
||||
|
||||
// Source models and interfaces from Tachiyomi 1.x
|
||||
implementation("org.tachiyomi:source-api:1.1")
|
||||
implementation(libs.tachiyomi.api)
|
||||
|
||||
// AndroidX libraries
|
||||
implementation("androidx.annotation:annotation:1.4.0-alpha01")
|
||||
implementation("androidx.appcompat:appcompat:1.4.1")
|
||||
implementation("androidx.biometric:biometric-ktx:1.2.0-alpha04")
|
||||
implementation("androidx.browser:browser:1.4.0")
|
||||
implementation("androidx.constraintlayout:constraintlayout:2.1.3")
|
||||
implementation("androidx.coordinatorlayout:coordinatorlayout:1.2.0")
|
||||
implementation("androidx.core:core-ktx:1.8.0-alpha02")
|
||||
implementation("androidx.core:core-splashscreen:1.0.0-alpha02")
|
||||
implementation("androidx.recyclerview:recyclerview:1.3.0-alpha01")
|
||||
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01")
|
||||
implementation("androidx.viewpager:viewpager:1.1.0-alpha01")
|
||||
implementation(androidx.annotation)
|
||||
implementation(androidx.appcompat)
|
||||
implementation(androidx.biometricktx)
|
||||
implementation(androidx.constraintlayout)
|
||||
implementation(androidx.coordinatorlayout)
|
||||
implementation(androidx.corektx)
|
||||
implementation(androidx.splashscreen)
|
||||
implementation(androidx.recyclerview)
|
||||
implementation(androidx.swiperefreshlayout)
|
||||
implementation(androidx.viewpager)
|
||||
|
||||
val lifecycleVersion = "2.4.0"
|
||||
implementation("androidx.lifecycle:lifecycle-common:$lifecycleVersion")
|
||||
implementation("androidx.lifecycle:lifecycle-process:$lifecycleVersion")
|
||||
implementation("androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion")
|
||||
implementation(androidx.bundles.lifecycle)
|
||||
|
||||
// Job scheduling
|
||||
implementation("androidx.work:work-runtime-ktx:2.6.0")
|
||||
implementation(androidx.bundles.workmanager)
|
||||
|
||||
// RX
|
||||
implementation("io.reactivex:rxandroid:1.2.1")
|
||||
implementation("io.reactivex:rxjava:1.3.8")
|
||||
implementation("com.jakewharton.rxrelay:rxrelay:1.2.0")
|
||||
implementation("ru.beryukhov:flowreactivenetwork:1.0.4")
|
||||
implementation(libs.bundles.reactivex)
|
||||
implementation(libs.flowreactivenetwork)
|
||||
|
||||
// Network client
|
||||
val okhttpVersion = "4.9.1"
|
||||
implementation("com.squareup.okhttp3:okhttp:$okhttpVersion")
|
||||
implementation("com.squareup.okhttp3:logging-interceptor:$okhttpVersion")
|
||||
implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:$okhttpVersion")
|
||||
implementation("com.squareup.okio:okio:3.0.0")
|
||||
implementation(libs.bundles.okhttp)
|
||||
implementation(libs.okio)
|
||||
|
||||
// TLS 1.3 support for Android < 10
|
||||
implementation("org.conscrypt:conscrypt-android:2.5.2")
|
||||
implementation(libs.conscrypt.android)
|
||||
|
||||
// Data serialization (JSON, protobuf)
|
||||
val kotlinSerializationVersion = "1.3.2"
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinSerializationVersion")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-protobuf:$kotlinSerializationVersion")
|
||||
implementation(kotlinx.bundles.serialization)
|
||||
|
||||
// JavaScript engine
|
||||
implementation("app.cash.quickjs:quickjs-android:0.9.2")
|
||||
// TODO: remove Duktape once all extensions are using QuickJS
|
||||
implementation("com.squareup.duktape:duktape-android:1.4.0")
|
||||
implementation(libs.bundles.js.engine)
|
||||
|
||||
// HTML parser
|
||||
implementation("org.jsoup:jsoup:1.14.3")
|
||||
implementation(libs.jsoup)
|
||||
|
||||
// Disk
|
||||
implementation("com.jakewharton:disklrucache:2.0.2")
|
||||
implementation("com.github.tachiyomiorg:unifile:17bec43")
|
||||
implementation("com.github.junrar:junrar:7.4.0")
|
||||
implementation(libs.disklrucache)
|
||||
implementation(libs.unifile)
|
||||
implementation(libs.junrar)
|
||||
|
||||
// Database
|
||||
implementation("androidx.sqlite:sqlite-ktx:2.2.0")
|
||||
implementation(libs.bundles.sqlite)
|
||||
implementation("com.github.inorichi.storio:storio-common:8be19de@aar")
|
||||
implementation("com.github.inorichi.storio:storio-sqlite:8be19de@aar")
|
||||
implementation("com.github.requery:sqlite-android:3.36.0")
|
||||
|
||||
// Preferences
|
||||
implementation("androidx.preference:preference-ktx:1.2.0")
|
||||
implementation("com.fredporciuncula:flow-preferences:1.6.0")
|
||||
implementation(libs.preferencektx)
|
||||
implementation(libs.flowpreferences)
|
||||
|
||||
// Model View Presenter
|
||||
val nucleusVersion = "3.0.0"
|
||||
implementation("info.android15.nucleus:nucleus:$nucleusVersion")
|
||||
implementation("info.android15.nucleus:nucleus-support-v7:$nucleusVersion")
|
||||
implementation(libs.bundles.nucleus)
|
||||
|
||||
// Dependency injection
|
||||
implementation("com.github.inorichi.injekt:injekt-core:65b0440")
|
||||
implementation(libs.injekt.core)
|
||||
|
||||
// Image loading
|
||||
val coilVersion = "1.4.0"
|
||||
implementation("io.coil-kt:coil:$coilVersion")
|
||||
implementation("io.coil-kt:coil-gif:$coilVersion")
|
||||
implementation(libs.bundles.coil)
|
||||
|
||||
implementation("com.github.tachiyomiorg:subsampling-scale-image-view:846abe0") {
|
||||
implementation(libs.subsamplingscaleimageview) {
|
||||
exclude(module = "image-decoder")
|
||||
}
|
||||
implementation("com.github.tachiyomiorg:image-decoder:7481a4a")
|
||||
implementation(libs.image.decoder)
|
||||
|
||||
// Sort
|
||||
implementation("com.github.gpanther:java-nat-sort:natural-comparator-1.1")
|
||||
implementation(libs.natural.comparator)
|
||||
|
||||
// UI libraries
|
||||
implementation("com.google.android.material:material:1.6.0-alpha02")
|
||||
implementation("com.github.dmytrodanylyk.android-process-button:library:1.0.4")
|
||||
implementation("com.github.arkon.FlexibleAdapter:flexible-adapter:c8013533")
|
||||
implementation("com.github.arkon.FlexibleAdapter:flexible-adapter-ui:c8013533")
|
||||
implementation("com.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.1.0")
|
||||
implementation("com.github.chrisbanes:PhotoView:2.3.0")
|
||||
implementation("com.github.tachiyomiorg:DirectionalViewPager:1.0.0") {
|
||||
implementation(libs.material)
|
||||
implementation(libs.androidprocessbutton)
|
||||
implementation(libs.flexible.adapter.core)
|
||||
implementation(libs.flexible.adapter.ui)
|
||||
implementation(libs.viewstatepageradapter)
|
||||
implementation(libs.photoview)
|
||||
implementation(libs.directionalviewpager) {
|
||||
exclude(group = "androidx.viewpager", module = "viewpager")
|
||||
}
|
||||
implementation("dev.chrisbanes.insetter:insetter:0.6.1")
|
||||
implementation(libs.insetter)
|
||||
implementation(libs.markwon)
|
||||
|
||||
// Conductor
|
||||
val conductorVersion = "3.1.2"
|
||||
implementation("com.bluelinelabs:conductor:$conductorVersion")
|
||||
implementation("com.bluelinelabs:conductor-viewpager:$conductorVersion")
|
||||
implementation("com.github.tachiyomiorg:conductor-support-preference:$conductorVersion")
|
||||
implementation(libs.bundles.conductor)
|
||||
|
||||
// FlowBinding
|
||||
val flowbindingVersion = "1.2.0"
|
||||
implementation("io.github.reactivecircus.flowbinding:flowbinding-android:$flowbindingVersion")
|
||||
implementation("io.github.reactivecircus.flowbinding:flowbinding-appcompat:$flowbindingVersion")
|
||||
implementation("io.github.reactivecircus.flowbinding:flowbinding-recyclerview:$flowbindingVersion")
|
||||
implementation("io.github.reactivecircus.flowbinding:flowbinding-swiperefreshlayout:$flowbindingVersion")
|
||||
implementation("io.github.reactivecircus.flowbinding:flowbinding-viewpager:$flowbindingVersion")
|
||||
implementation(libs.bundles.flowbinding)
|
||||
|
||||
// Logging
|
||||
implementation("com.squareup.logcat:logcat:0.1")
|
||||
implementation(libs.logcat)
|
||||
|
||||
// Crash reports/analytics
|
||||
implementation("ch.acra:acra-http:5.8.4")
|
||||
"standardImplementation"("com.google.firebase:firebase-analytics-ktx:20.0.2")
|
||||
implementation(libs.acra.http)
|
||||
"standardImplementation"(libs.firebase.analytics)
|
||||
|
||||
// Licenses
|
||||
implementation("com.mikepenz:aboutlibraries-core:${BuildPluginsVersion.ABOUTLIB_PLUGIN}")
|
||||
implementation(libs.aboutlibraries.core)
|
||||
|
||||
// Shizuku
|
||||
val shizukuVersion = "12.1.0"
|
||||
implementation("dev.rikka.shizuku:api:$shizukuVersion")
|
||||
implementation("dev.rikka.shizuku:provider:$shizukuVersion")
|
||||
implementation(libs.bundles.shizuku)
|
||||
|
||||
// Tests
|
||||
testImplementation("junit:junit:4.13.2")
|
||||
testImplementation("org.assertj:assertj-core:3.16.1")
|
||||
testImplementation("org.mockito:mockito-core:1.10.19")
|
||||
|
||||
val robolectricVersion = "3.1.4"
|
||||
testImplementation("org.robolectric:robolectric:$robolectricVersion")
|
||||
testImplementation("org.robolectric:shadows-play-services:$robolectricVersion")
|
||||
testImplementation(libs.junit)
|
||||
|
||||
// For detecting memory leaks; see https://square.github.io/leakcanary/
|
||||
// debugImplementation("com.squareup.leakcanary:leakcanary-android:2.7")
|
||||
// debugImplementation(libs.leakcanary.android)
|
||||
implementation(libs.leakcanary.plumber)
|
||||
}
|
||||
|
||||
tasks {
|
||||
withType<Test> {
|
||||
useJUnitPlatform()
|
||||
testLogging {
|
||||
events(TestLogEvent.PASSED, TestLogEvent.SKIPPED, TestLogEvent.FAILED)
|
||||
}
|
||||
}
|
||||
|
||||
// See https://kotlinlang.org/docs/reference/experimental.html#experimental-status-of-experimental-api(-markers)
|
||||
withType<KotlinCompile> {
|
||||
kotlinOptions.freeCompilerArgs += listOf(
|
||||
"-Xopt-in=kotlin.Experimental",
|
||||
"-Xopt-in=kotlin.RequiresOptIn",
|
||||
"-Xopt-in=kotlin.ExperimentalStdlibApi",
|
||||
"-Xopt-in=kotlinx.coroutines.FlowPreview",
|
||||
"-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
||||
"-Xopt-in=kotlinx.coroutines.InternalCoroutinesApi",
|
||||
"-Xopt-in=kotlinx.serialization.ExperimentalSerializationApi",
|
||||
"-Xopt-in=coil.annotation.ExperimentalCoilApi",
|
||||
"-opt-in=kotlin.Experimental",
|
||||
"-opt-in=kotlin.RequiresOptIn",
|
||||
"-opt-in=kotlin.ExperimentalStdlibApi",
|
||||
"-opt-in=kotlinx.coroutines.FlowPreview",
|
||||
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
||||
"-opt-in=kotlinx.coroutines.InternalCoroutinesApi",
|
||||
"-opt-in=kotlinx.serialization.ExperimentalSerializationApi",
|
||||
"-opt-in=coil.annotation.ExperimentalCoilApi",
|
||||
)
|
||||
}
|
||||
|
||||
// Duplicating Hebrew string assets due to some locale code issues on different devices
|
||||
val copyHebrewStrings = task("copyHebrewStrings", type = Copy::class) {
|
||||
val copyHebrewStrings by registering(Copy::class) {
|
||||
from("./src/main/res/values-he")
|
||||
into("./src/main/res/values-iw")
|
||||
include("**/*")
|
||||
@ -313,40 +284,8 @@ tasks {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
buildscript {
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath(kotlin("gradle-plugin", version = BuildPluginsVersion.KOTLIN))
|
||||
classpath(kotlinx.gradle)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Git is needed in your system PATH for these commands to work.
|
||||
// If it's not installed, you can return a random value as a workaround
|
||||
fun getCommitCount(): String {
|
||||
return runCommand("git rev-list --count HEAD")
|
||||
// return "1"
|
||||
}
|
||||
|
||||
fun getGitSha(): String {
|
||||
return runCommand("git rev-parse --short HEAD")
|
||||
// return "1"
|
||||
}
|
||||
|
||||
fun getBuildTime(): String {
|
||||
val df = SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'")
|
||||
df.timeZone = TimeZone.getTimeZone("UTC")
|
||||
return df.format(Date())
|
||||
}
|
||||
|
||||
fun runCommand(command: String): String {
|
||||
val byteOut = ByteArrayOutputStream()
|
||||
project.exec {
|
||||
commandLine = command.split(" ")
|
||||
standardOutput = byteOut
|
||||
}
|
||||
return String(byteOut.toByteArray()).trim()
|
||||
}
|
||||
|
2
app/proguard-rules.pro
vendored
2
app/proguard-rules.pro
vendored
@ -82,4 +82,4 @@
|
||||
-keepclassmembers class kotlinx.serialization.** {
|
||||
<methods>;
|
||||
}
|
||||
##---------------End: proguard configuration for kotlinx.serialization ----------
|
||||
##---------------End: proguard configuration for kotlinx.serialization ----------
|
@ -2,4 +2,5 @@
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@android:color/transparent"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
<monochrome android:drawable="@drawable/ic_tachi_monochrome_launcher" />
|
||||
</adaptive-icon>
|
@ -1,6 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="eu.kanade.tachiyomi">
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<!-- Internet -->
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
@ -26,7 +25,6 @@
|
||||
android:name=".App"
|
||||
android:allowBackup="false"
|
||||
android:hardwareAccelerated="true"
|
||||
android:hasFragileUserData="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:largeHeap="true"
|
||||
@ -181,10 +179,6 @@
|
||||
android:name=".data.updater.AppUpdateService"
|
||||
android:exported="false" />
|
||||
|
||||
<service
|
||||
android:name=".data.backup.BackupCreateService"
|
||||
android:exported="false" />
|
||||
|
||||
<service
|
||||
android:name=".data.backup.BackupRestoreService"
|
||||
android:exported="false" />
|
||||
|
@ -1,5 +1,6 @@
|
||||
package eu.kanade.tachiyomi
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.ActivityManager
|
||||
import android.app.Application
|
||||
import android.app.PendingIntent
|
||||
@ -8,6 +9,7 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.os.Build
|
||||
import android.os.Looper
|
||||
import android.webkit.WebView
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
@ -20,18 +22,21 @@ import coil.ImageLoader
|
||||
import coil.ImageLoaderFactory
|
||||
import coil.decode.GifDecoder
|
||||
import coil.decode.ImageDecoderDecoder
|
||||
import coil.disk.DiskCache
|
||||
import coil.util.DebugLogger
|
||||
import eu.kanade.tachiyomi.data.coil.ByteBufferFetcher
|
||||
import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher
|
||||
import eu.kanade.tachiyomi.data.coil.MangaCoverKeyer
|
||||
import eu.kanade.tachiyomi.data.coil.TachiyomiImageDecoder
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate
|
||||
import eu.kanade.tachiyomi.ui.base.delegate.SecureActivityDelegate
|
||||
import eu.kanade.tachiyomi.util.preference.asImmediateFlow
|
||||
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil
|
||||
import eu.kanade.tachiyomi.util.system.WebViewUtil
|
||||
import eu.kanade.tachiyomi.util.system.animatorDurationScale
|
||||
import eu.kanade.tachiyomi.util.system.isDevFlavor
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import eu.kanade.tachiyomi.util.system.notification
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
@ -47,6 +52,7 @@ import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.security.Security
|
||||
import java.util.Date
|
||||
|
||||
open class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
|
||||
|
||||
@ -54,6 +60,7 @@ open class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
|
||||
|
||||
private val disableIncognitoReceiver = DisableIncognitoReceiver()
|
||||
|
||||
@SuppressLint("LaunchActivityFromNotification")
|
||||
override fun onCreate() {
|
||||
super<Application>.onCreate()
|
||||
|
||||
@ -91,7 +98,7 @@ open class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
|
||||
this@App,
|
||||
0,
|
||||
Intent(ACTION_DISABLE_INCOGNITO_MODE),
|
||||
PendingIntent.FLAG_ONE_SHOT
|
||||
PendingIntent.FLAG_ONE_SHOT,
|
||||
)
|
||||
setContentIntent(pendingIntent)
|
||||
}
|
||||
@ -110,7 +117,7 @@ open class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
|
||||
PreferenceValues.ThemeMode.light -> AppCompatDelegate.MODE_NIGHT_NO
|
||||
PreferenceValues.ThemeMode.dark -> AppCompatDelegate.MODE_NIGHT_YES
|
||||
PreferenceValues.ThemeMode.system -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
|
||||
}
|
||||
},
|
||||
)
|
||||
}.launchIn(ProcessLifecycleOwner.get().lifecycleScope)
|
||||
|
||||
@ -121,17 +128,20 @@ open class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
|
||||
|
||||
override fun newImageLoader(): ImageLoader {
|
||||
return ImageLoader.Builder(this).apply {
|
||||
componentRegistry {
|
||||
val callFactoryInit = { Injekt.get<NetworkHelper>().client }
|
||||
val diskCacheInit = { CoilDiskCache.get(this@App) }
|
||||
components {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
add(ImageDecoderDecoder(this@App))
|
||||
add(ImageDecoderDecoder.Factory())
|
||||
} else {
|
||||
add(GifDecoder())
|
||||
add(GifDecoder.Factory())
|
||||
}
|
||||
add(TachiyomiImageDecoder(this@App.resources))
|
||||
add(ByteBufferFetcher())
|
||||
add(MangaCoverFetcher())
|
||||
add(TachiyomiImageDecoder.Factory())
|
||||
add(MangaCoverFetcher.Factory(lazy(callFactoryInit), lazy(diskCacheInit)))
|
||||
add(MangaCoverKeyer())
|
||||
}
|
||||
okHttpClient(Injekt.get<NetworkHelper>().coilClient)
|
||||
callFactory(callFactoryInit)
|
||||
diskCache(diskCacheInit)
|
||||
crossfade((300 * this@App.animatorDurationScale).toInt())
|
||||
allowRgb565(getSystemService<ActivityManager>()!!.isLowRamDevice)
|
||||
if (preferences.verboseLogging()) logger(DebugLogger())
|
||||
@ -139,16 +149,38 @@ open class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
|
||||
}
|
||||
|
||||
override fun onStop(owner: LifecycleOwner) {
|
||||
preferences.lastAppClosed().set(Date().time)
|
||||
if (!AuthenticatorUtil.isAuthenticating && preferences.lockAppAfter().get() >= 0) {
|
||||
SecureActivityDelegate.locked = true
|
||||
}
|
||||
}
|
||||
|
||||
override fun getPackageName(): String {
|
||||
// This causes freezes in Android 6/7 for some reason
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
try {
|
||||
// Override the value passed as X-Requested-With in WebView requests
|
||||
val stackTrace = Looper.getMainLooper().thread.stackTrace
|
||||
val chromiumElement = stackTrace.find {
|
||||
it.className.equals(
|
||||
"org.chromium.base.BuildInfo",
|
||||
ignoreCase = true,
|
||||
)
|
||||
}
|
||||
if (chromiumElement?.methodName.equals("getAll", ignoreCase = true)) {
|
||||
return WebViewUtil.SPOOF_PACKAGE_NAME
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
}
|
||||
}
|
||||
return super.getPackageName()
|
||||
}
|
||||
|
||||
protected open fun setupAcra() {
|
||||
if (BuildConfig.FLAVOR != "dev") {
|
||||
if (isDevFlavor.not()) {
|
||||
initAcra {
|
||||
buildConfigClass = BuildConfig::class.java
|
||||
excludeMatchingSharedPreferencesKeys = arrayOf(".*username.*", ".*password.*", ".*token.*")
|
||||
excludeMatchingSharedPreferencesKeys = listOf(".*username.*", ".*password.*", ".*token.*")
|
||||
|
||||
httpSender {
|
||||
uri = BuildConfig.ACRA_URI
|
||||
@ -190,3 +222,24 @@ open class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
|
||||
}
|
||||
|
||||
private const val ACTION_DISABLE_INCOGNITO_MODE = "tachi.action.DISABLE_INCOGNITO_MODE"
|
||||
|
||||
/**
|
||||
* Direct copy of Coil's internal SingletonDiskCache so that [MangaCoverFetcher] can access it.
|
||||
*/
|
||||
internal object CoilDiskCache {
|
||||
|
||||
private const val FOLDER_NAME = "image_cache"
|
||||
private var instance: DiskCache? = null
|
||||
|
||||
@Synchronized
|
||||
fun get(context: Context): DiskCache {
|
||||
return instance ?: run {
|
||||
val safeCacheDir = context.cacheDir.apply { mkdirs() }
|
||||
// Create the singleton disk cache instance.
|
||||
DiskCache.Builder()
|
||||
.directory(safeCacheDir.resolve(FOLDER_NAME))
|
||||
.build()
|
||||
.also { instance = it }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.saver.ImageSaver
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.data.track.job.DelayedTrackingStore
|
||||
import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||
@ -46,6 +47,8 @@ class AppModule(val app: Application) : InjektModule {
|
||||
|
||||
addSingletonFactory { DelayedTrackingStore(app) }
|
||||
|
||||
addSingletonFactory { ImageSaver(app) }
|
||||
|
||||
// Asynchronously init expensive components for a faster cold start
|
||||
ContextCompat.getMainExecutor(app).execute {
|
||||
get<PreferencesHelper>()
|
||||
|
@ -5,19 +5,20 @@ import androidx.core.content.edit
|
||||
import androidx.preference.PreferenceManager
|
||||
import eu.kanade.tachiyomi.data.backup.BackupCreatorJob
|
||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
|
||||
import eu.kanade.tachiyomi.data.preference.MANGA_ONGOING
|
||||
import eu.kanade.tachiyomi.data.preference.MANGA_NON_COMPLETED
|
||||
import eu.kanade.tachiyomi.data.preference.PreferenceKeys
|
||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.data.updater.AppUpdateJob
|
||||
import eu.kanade.tachiyomi.extension.ExtensionUpdateJob
|
||||
import eu.kanade.tachiyomi.network.PREF_DOH_CLOUDFLARE
|
||||
import eu.kanade.tachiyomi.ui.library.LibrarySort
|
||||
import eu.kanade.tachiyomi.ui.library.setting.SortDirectionSetting
|
||||
import eu.kanade.tachiyomi.ui.library.setting.SortModeSetting
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
|
||||
import eu.kanade.tachiyomi.util.preference.minusAssign
|
||||
import eu.kanade.tachiyomi.util.preference.plusAssign
|
||||
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
|
||||
import uy.kohesive.injekt.Injekt
|
||||
@ -102,10 +103,9 @@ object Migrations {
|
||||
// Reset sorting preference if using removed sort by source
|
||||
val oldSortingMode = prefs.getInt(PreferenceKeys.librarySortingMode, 0)
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
if (oldSortingMode == LibrarySort.SOURCE) {
|
||||
if (oldSortingMode == 5 /* SOURCE */) {
|
||||
prefs.edit {
|
||||
putInt(PreferenceKeys.librarySortingMode, LibrarySort.ALPHA)
|
||||
putInt(PreferenceKeys.librarySortingMode, 0 /* ALPHABETICAL */)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -198,16 +198,15 @@ object Migrations {
|
||||
val oldSortingMode = prefs.getInt(PreferenceKeys.librarySortingMode, 0)
|
||||
val oldSortingDirection = prefs.getBoolean(PreferenceKeys.librarySortingDirection, true)
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
val newSortingMode = when (oldSortingMode) {
|
||||
LibrarySort.ALPHA -> SortModeSetting.ALPHABETICAL
|
||||
LibrarySort.LAST_READ -> SortModeSetting.LAST_READ
|
||||
LibrarySort.LAST_CHECKED -> SortModeSetting.LAST_CHECKED
|
||||
LibrarySort.UNREAD -> SortModeSetting.UNREAD
|
||||
LibrarySort.TOTAL -> SortModeSetting.TOTAL_CHAPTERS
|
||||
LibrarySort.LATEST_CHAPTER -> SortModeSetting.LATEST_CHAPTER
|
||||
LibrarySort.CHAPTER_FETCH_DATE -> SortModeSetting.DATE_FETCHED
|
||||
LibrarySort.DATE_ADDED -> SortModeSetting.DATE_ADDED
|
||||
0 -> SortModeSetting.ALPHABETICAL
|
||||
1 -> SortModeSetting.LAST_READ
|
||||
2 -> SortModeSetting.LAST_CHECKED
|
||||
3 -> SortModeSetting.UNREAD
|
||||
4 -> SortModeSetting.TOTAL_CHAPTERS
|
||||
6 -> SortModeSetting.LATEST_CHAPTER
|
||||
8 -> SortModeSetting.DATE_FETCHED
|
||||
7 -> SortModeSetting.DATE_ADDED
|
||||
else -> SortModeSetting.ALPHABETICAL
|
||||
}
|
||||
|
||||
@ -242,7 +241,26 @@ object Migrations {
|
||||
if (oldVersion < 72) {
|
||||
val oldUpdateOngoingOnly = prefs.getBoolean("pref_update_only_non_completed_key", true)
|
||||
if (!oldUpdateOngoingOnly) {
|
||||
preferences.libraryUpdateMangaRestriction() -= MANGA_ONGOING
|
||||
preferences.libraryUpdateMangaRestriction() -= MANGA_NON_COMPLETED
|
||||
}
|
||||
}
|
||||
if (oldVersion < 75) {
|
||||
val oldSecureScreen = prefs.getBoolean("secure_screen", false)
|
||||
if (oldSecureScreen) {
|
||||
preferences.secureScreen().set(PreferenceValues.SecureScreenMode.ALWAYS)
|
||||
}
|
||||
if (DeviceUtil.isMiui && preferences.extensionInstaller().get() == PreferenceValues.ExtensionInstaller.PACKAGEINSTALLER) {
|
||||
preferences.extensionInstaller().set(PreferenceValues.ExtensionInstaller.LEGACY)
|
||||
}
|
||||
}
|
||||
if (oldVersion < 76) {
|
||||
BackupCreatorJob.setupTask(context)
|
||||
}
|
||||
if (oldVersion < 77) {
|
||||
val oldReaderTap = prefs.getBoolean("reader_tap", false)
|
||||
if (!oldReaderTap) {
|
||||
preferences.navigationModePager().set(5)
|
||||
preferences.navigationModeWebtoon().set(5)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -21,7 +21,7 @@ abstract class AbstractBackupManager(protected val context: Context) {
|
||||
internal val trackManager: TrackManager by injectLazy()
|
||||
protected val preferences: PreferencesHelper by injectLazy()
|
||||
|
||||
abstract fun createBackup(uri: Uri, flags: Int, isJob: Boolean): String
|
||||
abstract fun createBackup(uri: Uri, flags: Int, isAutoBackup: Boolean): String
|
||||
|
||||
/**
|
||||
* Returns manga
|
||||
|
@ -112,7 +112,7 @@ abstract class AbstractBackupRestore<T : AbstractBackupManager>(protected val co
|
||||
internal fun showRestoreProgress(
|
||||
progress: Int,
|
||||
amount: Int,
|
||||
title: String
|
||||
title: String,
|
||||
) {
|
||||
notifier.showRestoreProgress(title, progress, amount)
|
||||
}
|
||||
|
@ -11,4 +11,15 @@ object BackupConst {
|
||||
|
||||
const val BACKUP_TYPE_LEGACY = 0
|
||||
const val BACKUP_TYPE_FULL = 1
|
||||
|
||||
// Filter options
|
||||
internal const val BACKUP_CATEGORY = 0x1
|
||||
internal const val BACKUP_CATEGORY_MASK = 0x1
|
||||
internal const val BACKUP_CHAPTER = 0x2
|
||||
internal const val BACKUP_CHAPTER_MASK = 0x2
|
||||
internal const val BACKUP_HISTORY = 0x4
|
||||
internal const val BACKUP_HISTORY_MASK = 0x4
|
||||
internal const val BACKUP_TRACK = 0x8
|
||||
internal const val BACKUP_TRACK_MASK = 0x8
|
||||
internal const val BACKUP_ALL = 0xF
|
||||
}
|
||||
|
@ -1,114 +0,0 @@
|
||||
package eu.kanade.tachiyomi.data.backup
|
||||
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.IBinder
|
||||
import android.os.PowerManager
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.net.toUri
|
||||
import com.hippo.unifile.UniFile
|
||||
import eu.kanade.tachiyomi.data.backup.full.FullBackupManager
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import eu.kanade.tachiyomi.util.system.acquireWakeLock
|
||||
import eu.kanade.tachiyomi.util.system.isServiceRunning
|
||||
|
||||
/**
|
||||
* Service for backing up library information to a JSON file.
|
||||
*/
|
||||
class BackupCreateService : Service() {
|
||||
|
||||
companion object {
|
||||
// Filter options
|
||||
internal const val BACKUP_CATEGORY = 0x1
|
||||
internal const val BACKUP_CATEGORY_MASK = 0x1
|
||||
internal const val BACKUP_CHAPTER = 0x2
|
||||
internal const val BACKUP_CHAPTER_MASK = 0x2
|
||||
internal const val BACKUP_HISTORY = 0x4
|
||||
internal const val BACKUP_HISTORY_MASK = 0x4
|
||||
internal const val BACKUP_TRACK = 0x8
|
||||
internal const val BACKUP_TRACK_MASK = 0x8
|
||||
internal const val BACKUP_ALL = 0xF
|
||||
|
||||
/**
|
||||
* Returns the status of the service.
|
||||
*
|
||||
* @param context the application context.
|
||||
* @return true if the service is running, false otherwise.
|
||||
*/
|
||||
fun isRunning(context: Context): Boolean =
|
||||
context.isServiceRunning(BackupCreateService::class.java)
|
||||
|
||||
/**
|
||||
* Make a backup from library
|
||||
*
|
||||
* @param context context of application
|
||||
* @param uri path of Uri
|
||||
* @param flags determines what to backup
|
||||
*/
|
||||
fun start(context: Context, uri: Uri, flags: Int) {
|
||||
if (!isRunning(context)) {
|
||||
val intent = Intent(context, BackupCreateService::class.java).apply {
|
||||
putExtra(BackupConst.EXTRA_URI, uri)
|
||||
putExtra(BackupConst.EXTRA_FLAGS, flags)
|
||||
}
|
||||
ContextCompat.startForegroundService(context, intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wake lock that will be held until the service is destroyed.
|
||||
*/
|
||||
private lateinit var wakeLock: PowerManager.WakeLock
|
||||
|
||||
private lateinit var notifier: BackupNotifier
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
notifier = BackupNotifier(this)
|
||||
wakeLock = acquireWakeLock(javaClass.name)
|
||||
|
||||
startForeground(Notifications.ID_BACKUP_PROGRESS, notifier.showBackupProgress().build())
|
||||
}
|
||||
|
||||
override fun stopService(name: Intent?): Boolean {
|
||||
destroyJob()
|
||||
return super.stopService(name)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
destroyJob()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun destroyJob() {
|
||||
if (wakeLock.isHeld) {
|
||||
wakeLock.release()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method needs to be implemented, but it's not used/needed.
|
||||
*/
|
||||
override fun onBind(intent: Intent): IBinder? = null
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
if (intent == null) return START_NOT_STICKY
|
||||
|
||||
try {
|
||||
val uri = intent.getParcelableExtra<Uri>(BackupConst.EXTRA_URI)!!
|
||||
val backupFlags = intent.getIntExtra(BackupConst.EXTRA_FLAGS, 0)
|
||||
val backupFileUri = FullBackupManager(this).createBackup(uri, backupFlags, false)?.toUri()
|
||||
val unifile = UniFile.fromUri(this, backupFileUri)
|
||||
notifier.showBackupComplete(unifile)
|
||||
} catch (e: Exception) {
|
||||
notifier.showBackupError(e.message)
|
||||
}
|
||||
|
||||
stopSelf(startId)
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
}
|
@ -1,15 +1,23 @@
|
||||
package eu.kanade.tachiyomi.data.backup
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.core.net.toUri
|
||||
import androidx.work.ExistingPeriodicWorkPolicy
|
||||
import androidx.work.ExistingWorkPolicy
|
||||
import androidx.work.OneTimeWorkRequestBuilder
|
||||
import androidx.work.PeriodicWorkRequestBuilder
|
||||
import androidx.work.WorkInfo
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.Worker
|
||||
import androidx.work.WorkerParameters
|
||||
import androidx.work.workDataOf
|
||||
import com.hippo.unifile.UniFile
|
||||
import eu.kanade.tachiyomi.data.backup.full.FullBackupManager
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import eu.kanade.tachiyomi.util.system.notificationManager
|
||||
import logcat.LogPriority
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
@ -20,37 +28,71 @@ class BackupCreatorJob(private val context: Context, workerParams: WorkerParamet
|
||||
|
||||
override fun doWork(): Result {
|
||||
val preferences = Injekt.get<PreferencesHelper>()
|
||||
val uri = preferences.backupsDirectory().get().toUri()
|
||||
val flags = BackupCreateService.BACKUP_ALL
|
||||
val notifier = BackupNotifier(context)
|
||||
val uri = inputData.getString(LOCATION_URI_KEY)?.let { Uri.parse(it) }
|
||||
?: preferences.backupsDirectory().get().toUri()
|
||||
val flags = inputData.getInt(BACKUP_FLAGS_KEY, BackupConst.BACKUP_ALL)
|
||||
val isAutoBackup = inputData.getBoolean(IS_AUTO_BACKUP_KEY, true)
|
||||
|
||||
context.notificationManager.notify(Notifications.ID_BACKUP_PROGRESS, notifier.showBackupProgress().build())
|
||||
return try {
|
||||
FullBackupManager(context).createBackup(uri, flags, true)
|
||||
val location = FullBackupManager(context).createBackup(uri, flags, isAutoBackup)
|
||||
if (!isAutoBackup) notifier.showBackupComplete(UniFile.fromUri(context, location.toUri()))
|
||||
Result.success()
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR, e)
|
||||
if (!isAutoBackup) notifier.showBackupError(e.message)
|
||||
Result.failure()
|
||||
} finally {
|
||||
context.notificationManager.cancel(Notifications.ID_BACKUP_PROGRESS)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "BackupCreator"
|
||||
fun isManualJobRunning(context: Context): Boolean {
|
||||
val list = WorkManager.getInstance(context).getWorkInfosByTag(TAG_MANUAL).get()
|
||||
return list.find { it.state == WorkInfo.State.RUNNING } != null
|
||||
}
|
||||
|
||||
fun setupTask(context: Context, prefInterval: Int? = null) {
|
||||
val preferences = Injekt.get<PreferencesHelper>()
|
||||
val interval = prefInterval ?: preferences.backupInterval().get()
|
||||
val workManager = WorkManager.getInstance(context)
|
||||
if (interval > 0) {
|
||||
val request = PeriodicWorkRequestBuilder<BackupCreatorJob>(
|
||||
interval.toLong(),
|
||||
TimeUnit.HOURS,
|
||||
10,
|
||||
TimeUnit.MINUTES
|
||||
TimeUnit.MINUTES,
|
||||
)
|
||||
.addTag(TAG)
|
||||
.addTag(TAG_AUTO)
|
||||
.setInputData(workDataOf(IS_AUTO_BACKUP_KEY to true))
|
||||
.build()
|
||||
|
||||
WorkManager.getInstance(context).enqueueUniquePeriodicWork(TAG, ExistingPeriodicWorkPolicy.REPLACE, request)
|
||||
workManager.enqueueUniquePeriodicWork(TAG_AUTO, ExistingPeriodicWorkPolicy.REPLACE, request)
|
||||
} else {
|
||||
WorkManager.getInstance(context).cancelAllWorkByTag(TAG)
|
||||
workManager.cancelUniqueWork(TAG_AUTO)
|
||||
}
|
||||
}
|
||||
|
||||
fun startNow(context: Context, uri: Uri, flags: Int) {
|
||||
val inputData = workDataOf(
|
||||
IS_AUTO_BACKUP_KEY to false,
|
||||
LOCATION_URI_KEY to uri.toString(),
|
||||
BACKUP_FLAGS_KEY to flags,
|
||||
)
|
||||
val request = OneTimeWorkRequestBuilder<BackupCreatorJob>()
|
||||
.addTag(TAG_MANUAL)
|
||||
.setInputData(inputData)
|
||||
.build()
|
||||
WorkManager.getInstance(context).enqueueUniqueWork(TAG_MANUAL, ExistingWorkPolicy.KEEP, request)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const val TAG_AUTO = "BackupCreator"
|
||||
private const val TAG_MANUAL = "$TAG_AUTO:manual"
|
||||
|
||||
private const val IS_AUTO_BACKUP_KEY = "is_auto_backup" // Boolean
|
||||
private const val LOCATION_URI_KEY = "location_uri" // String
|
||||
private const val BACKUP_FLAGS_KEY = "backup_flags" // Int
|
||||
|
@ -73,7 +73,7 @@ class BackupNotifier(private val context: Context) {
|
||||
addAction(
|
||||
R.drawable.ic_share_24dp,
|
||||
context.getString(R.string.action_share),
|
||||
NotificationReceiver.shareBackupPendingBroadcast(context, unifile.uri, Notifications.ID_BACKUP_COMPLETE)
|
||||
NotificationReceiver.shareBackupPendingBroadcast(context, unifile.uri, Notifications.ID_BACKUP_COMPLETE),
|
||||
)
|
||||
|
||||
show(Notifications.ID_BACKUP_COMPLETE)
|
||||
@ -97,7 +97,7 @@ class BackupNotifier(private val context: Context) {
|
||||
addAction(
|
||||
R.drawable.ic_close_24dp,
|
||||
context.getString(R.string.action_stop),
|
||||
NotificationReceiver.cancelRestorePendingBroadcast(context, Notifications.ID_RESTORE_PROGRESS)
|
||||
NotificationReceiver.cancelRestorePendingBroadcast(context, Notifications.ID_RESTORE_PROGRESS),
|
||||
)
|
||||
}
|
||||
|
||||
@ -124,8 +124,8 @@ class BackupNotifier(private val context: Context) {
|
||||
R.string.restore_duration,
|
||||
TimeUnit.MILLISECONDS.toMinutes(time),
|
||||
TimeUnit.MILLISECONDS.toSeconds(time) - TimeUnit.MINUTES.toSeconds(
|
||||
TimeUnit.MILLISECONDS.toMinutes(time)
|
||||
)
|
||||
TimeUnit.MILLISECONDS.toMinutes(time),
|
||||
),
|
||||
)
|
||||
|
||||
with(completeNotificationBuilder) {
|
||||
|
@ -3,15 +3,16 @@ package eu.kanade.tachiyomi.data.backup.full
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import com.hippo.unifile.UniFile
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.backup.AbstractBackupManager
|
||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CATEGORY
|
||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CATEGORY_MASK
|
||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CHAPTER
|
||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CHAPTER_MASK
|
||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_HISTORY
|
||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_HISTORY_MASK
|
||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_TRACK
|
||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_TRACK_MASK
|
||||
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CATEGORY
|
||||
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CATEGORY_MASK
|
||||
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CHAPTER
|
||||
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CHAPTER_MASK
|
||||
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_HISTORY
|
||||
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_HISTORY_MASK
|
||||
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_TRACK
|
||||
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_TRACK_MASK
|
||||
import eu.kanade.tachiyomi.data.backup.full.models.Backup
|
||||
import eu.kanade.tachiyomi.data.backup.full.models.BackupCategory
|
||||
import eu.kanade.tachiyomi.data.backup.full.models.BackupChapter
|
||||
@ -32,6 +33,7 @@ import logcat.LogPriority
|
||||
import okio.buffer
|
||||
import okio.gzip
|
||||
import okio.sink
|
||||
import java.io.FileOutputStream
|
||||
import kotlin.math.max
|
||||
|
||||
class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
||||
@ -42,9 +44,9 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
||||
* Create backup Json file from database
|
||||
*
|
||||
* @param uri path of Uri
|
||||
* @param isJob backup called from job
|
||||
* @param isAutoBackup backup called from scheduled backup job
|
||||
*/
|
||||
override fun createBackup(uri: Uri, flags: Int, isJob: Boolean): String {
|
||||
override fun createBackup(uri: Uri, flags: Int, isAutoBackup: Boolean): String {
|
||||
// Create root object
|
||||
var backup: Backup? = null
|
||||
|
||||
@ -53,16 +55,16 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
||||
|
||||
backup = Backup(
|
||||
backupManga(databaseManga, flags),
|
||||
backupCategories(),
|
||||
backupCategories(flags),
|
||||
emptyList(),
|
||||
backupExtensionInfo(databaseManga)
|
||||
backupExtensionInfo(databaseManga),
|
||||
)
|
||||
}
|
||||
|
||||
var file: UniFile? = null
|
||||
try {
|
||||
file = (
|
||||
if (isJob) {
|
||||
if (isAutoBackup) {
|
||||
// Get dir of file and create
|
||||
var dir = UniFile.fromUri(context, uri)
|
||||
dir = dir.createDirectory("automatic")
|
||||
@ -84,8 +86,19 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
||||
)
|
||||
?: throw Exception("Couldn't create backup file")
|
||||
|
||||
if (!file.isFile) {
|
||||
throw IllegalStateException("Failed to get handle on file")
|
||||
}
|
||||
|
||||
val byteArray = parser.encodeToByteArray(BackupSerializer, backup!!)
|
||||
file.openOutputStream().sink().gzip().buffer().use { it.write(byteArray) }
|
||||
if (byteArray.isEmpty()) {
|
||||
throw IllegalStateException(context.getString(R.string.empty_backup_error))
|
||||
}
|
||||
|
||||
file.openOutputStream().also {
|
||||
// Force overwrite old file
|
||||
(it as? FileOutputStream)?.channel?.truncate(0)
|
||||
}.sink().gzip().buffer().use { it.write(byteArray) }
|
||||
val fileUri = file.uri
|
||||
|
||||
// Make sure it's a valid backup file
|
||||
@ -120,10 +133,15 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
||||
*
|
||||
* @return list of [BackupCategory] to be backed up
|
||||
*/
|
||||
private fun backupCategories(): List<BackupCategory> {
|
||||
return databaseHelper.getCategories()
|
||||
.executeAsBlocking()
|
||||
.map { BackupCategory.copyFrom(it) }
|
||||
private fun backupCategories(options: Int): List<BackupCategory> {
|
||||
// Check if user wants category information in backup
|
||||
return if (options and BACKUP_CATEGORY_MASK == BACKUP_CATEGORY) {
|
||||
databaseHelper.getCategories()
|
||||
.executeAsBlocking()
|
||||
.map { BackupCategory.copyFrom(it) }
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -93,7 +93,7 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBa
|
||||
categories: List<Int>,
|
||||
history: List<BackupHistory>,
|
||||
tracks: List<Track>,
|
||||
backupCategories: List<BackupCategory>
|
||||
backupCategories: List<BackupCategory>,
|
||||
) {
|
||||
db.inTransaction {
|
||||
val dbManga = backupManager.getMangaFromDatabase(manga)
|
||||
@ -123,7 +123,7 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBa
|
||||
categories: List<Int>,
|
||||
history: List<BackupHistory>,
|
||||
tracks: List<Track>,
|
||||
backupCategories: List<BackupCategory>
|
||||
backupCategories: List<BackupCategory>,
|
||||
) {
|
||||
try {
|
||||
val fetchedManga = backupManager.restoreManga(manga)
|
||||
@ -143,7 +143,7 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBa
|
||||
categories: List<Int>,
|
||||
history: List<BackupHistory>,
|
||||
tracks: List<Track>,
|
||||
backupCategories: List<BackupCategory>
|
||||
backupCategories: List<BackupCategory>,
|
||||
) {
|
||||
backupManager.restoreChaptersForManga(backupManga, chapters)
|
||||
|
||||
|
@ -9,5 +9,5 @@ data class Backup(
|
||||
@ProtoNumber(2) var backupCategories: List<BackupCategory> = emptyList(),
|
||||
// Bump by 100 to specify this is a 0.x value
|
||||
@ProtoNumber(100) var backupBrokenSources: List<BrokenBackupSource> = emptyList(),
|
||||
@ProtoNumber(101) var backupSources: List<BackupSource> = emptyList()
|
||||
@ProtoNumber(101) var backupSources: List<BackupSource> = emptyList(),
|
||||
)
|
||||
|
@ -26,7 +26,7 @@ class BackupCategory(
|
||||
return BackupCategory(
|
||||
name = category.name,
|
||||
order = category.order,
|
||||
flags = category.flags
|
||||
flags = category.flags,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -49,7 +49,7 @@ data class BackupChapter(
|
||||
lastPageRead = chapter.last_page_read,
|
||||
dateFetch = chapter.date_fetch,
|
||||
dateUpload = chapter.date_upload,
|
||||
sourceOrder = chapter.source_order
|
||||
sourceOrder = chapter.source_order,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -6,11 +6,11 @@ import kotlinx.serialization.protobuf.ProtoNumber
|
||||
@Serializable
|
||||
data class BrokenBackupHistory(
|
||||
@ProtoNumber(0) var url: String,
|
||||
@ProtoNumber(1) var lastRead: Long
|
||||
@ProtoNumber(1) var lastRead: Long,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class BackupHistory(
|
||||
@ProtoNumber(1) var url: String,
|
||||
@ProtoNumber(2) var lastRead: Long
|
||||
@ProtoNumber(2) var lastRead: Long,
|
||||
)
|
||||
|
@ -35,7 +35,7 @@ data class BackupManga(
|
||||
@ProtoNumber(101) var chapterFlags: Int = 0,
|
||||
@ProtoNumber(102) var brokenHistory: List<BrokenBackupHistory> = emptyList(),
|
||||
@ProtoNumber(103) var viewer_flags: Int? = null,
|
||||
@ProtoNumber(104) var history: List<BackupHistory> = emptyList()
|
||||
@ProtoNumber(104) var history: List<BackupHistory> = emptyList(),
|
||||
) {
|
||||
fun getMangaImpl(): MangaImpl {
|
||||
return MangaImpl().apply {
|
||||
@ -83,7 +83,7 @@ data class BackupManga(
|
||||
dateAdded = manga.date_added,
|
||||
viewer = manga.readingModeType,
|
||||
viewer_flags = manga.viewer_flags,
|
||||
chapterFlags = manga.chapter_flags
|
||||
chapterFlags = manga.chapter_flags,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -7,19 +7,19 @@ import kotlinx.serialization.protobuf.ProtoNumber
|
||||
@Serializable
|
||||
data class BrokenBackupSource(
|
||||
@ProtoNumber(0) var name: String = "",
|
||||
@ProtoNumber(1) var sourceId: Long
|
||||
@ProtoNumber(1) var sourceId: Long,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class BackupSource(
|
||||
@ProtoNumber(1) var name: String = "",
|
||||
@ProtoNumber(2) var sourceId: Long
|
||||
@ProtoNumber(2) var sourceId: Long,
|
||||
) {
|
||||
companion object {
|
||||
fun copyFrom(source: Source): BackupSource {
|
||||
return BackupSource(
|
||||
name = source.name,
|
||||
sourceId = source.id
|
||||
sourceId = source.id,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -56,7 +56,7 @@ data class BackupTracking(
|
||||
status = track.status,
|
||||
startedReadingDate = track.started_reading_date,
|
||||
finishedReadingDate = track.finished_reading_date,
|
||||
trackingUrl = track.tracking_url
|
||||
trackingUrl = track.tracking_url,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -55,9 +55,9 @@ class LegacyBackupManager(context: Context, version: Int = CURRENT_VERSION) : Ab
|
||||
* Create backup Json file from database
|
||||
*
|
||||
* @param uri path of Uri
|
||||
* @param isJob backup called from job
|
||||
* @param isAutoBackup backup called from scheduled backup job
|
||||
*/
|
||||
override fun createBackup(uri: Uri, flags: Int, isJob: Boolean) =
|
||||
override fun createBackup(uri: Uri, flags: Int, isAutoBackup: Boolean) =
|
||||
throw IllegalStateException("Legacy backup creation is not supported")
|
||||
|
||||
fun restoreMangaNoFetch(manga: Manga, dbManga: Manga) {
|
||||
|
@ -28,7 +28,7 @@ class LegacyBackupRestore(context: Context, notifier: BackupNotifier) : Abstract
|
||||
// Read the json and create a Json Object,
|
||||
// cannot use the backupManager json deserializer one because its not initialized yet
|
||||
val backupObject = Json.decodeFromStream<JsonObject>(
|
||||
context.contentResolver.openInputStream(uri)!!
|
||||
context.contentResolver.openInputStream(uri)!!,
|
||||
)
|
||||
|
||||
// Get parser version
|
||||
@ -109,7 +109,7 @@ class LegacyBackupRestore(context: Context, notifier: BackupNotifier) : Abstract
|
||||
chapters: List<Chapter>,
|
||||
categories: List<String>,
|
||||
history: List<DHistory>,
|
||||
tracks: List<Track>
|
||||
tracks: List<Track>,
|
||||
) {
|
||||
val dbManga = backupManager.getMangaFromDatabase(manga)
|
||||
|
||||
@ -139,7 +139,7 @@ class LegacyBackupRestore(context: Context, notifier: BackupNotifier) : Abstract
|
||||
chapters: List<Chapter>,
|
||||
categories: List<String>,
|
||||
history: List<DHistory>,
|
||||
tracks: List<Track>
|
||||
tracks: List<Track>,
|
||||
) {
|
||||
try {
|
||||
val fetchedManga = backupManager.fetchManga(source, manga)
|
||||
@ -161,7 +161,7 @@ class LegacyBackupRestore(context: Context, notifier: BackupNotifier) : Abstract
|
||||
chapters: List<Chapter>,
|
||||
categories: List<String>,
|
||||
history: List<DHistory>,
|
||||
tracks: List<Track>
|
||||
tracks: List<Track>,
|
||||
) {
|
||||
if (!backupManager.restoreChaptersForManga(backupManga, chapters)) {
|
||||
updateChapters(source, backupManga, chapters)
|
||||
|
@ -21,7 +21,7 @@ class LegacyBackupRestoreValidator : AbstractBackupRestoreValidator() {
|
||||
|
||||
val backup = try {
|
||||
backupManager.parser.decodeFromStream<Backup>(
|
||||
context.contentResolver.openInputStream(uri)!!
|
||||
context.contentResolver.openInputStream(uri)!!,
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
throw ValidatorParseException(e)
|
||||
|
@ -15,7 +15,7 @@ data class Backup(
|
||||
val version: Int? = null,
|
||||
var mangas: MutableList<MangaObject> = mutableListOf(),
|
||||
var categories: List<@Contextual Category>? = null,
|
||||
var extensions: List<String>? = null
|
||||
var extensions: List<String>? = null,
|
||||
) {
|
||||
companion object {
|
||||
const val CURRENT_VERSION = 2
|
||||
@ -33,5 +33,5 @@ data class MangaObject(
|
||||
var chapters: List<@Contextual Chapter>? = null,
|
||||
var categories: List<String>? = null,
|
||||
var track: List<@Contextual Track>? = null,
|
||||
var history: List<@Contextual DHistory>? = null
|
||||
var history: List<@Contextual DHistory>? = null,
|
||||
)
|
||||
|
@ -27,7 +27,7 @@ open class CategoryBaseSerializer<T : Category> : KSerializer<T> {
|
||||
buildJsonArray {
|
||||
add(value.name)
|
||||
add(value.order)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -35,7 +35,7 @@ open class ChapterBaseSerializer<T : Chapter> : KSerializer<T> {
|
||||
if (value.last_page_read != 0) {
|
||||
put(LAST_READ, value.last_page_read)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -26,7 +26,7 @@ object HistoryTypeSerializer : KSerializer<DHistory> {
|
||||
buildJsonArray {
|
||||
add(value.url)
|
||||
add(value.lastRead)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@ -35,7 +35,7 @@ object HistoryTypeSerializer : KSerializer<DHistory> {
|
||||
val array = decoder.decodeJsonElement().jsonArray
|
||||
return DHistory(
|
||||
url = array[0].jsonPrimitive.content,
|
||||
lastRead = array[1].jsonPrimitive.long
|
||||
lastRead = array[1].jsonPrimitive.long,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -31,7 +31,7 @@ open class MangaBaseSerializer<T : Manga> : KSerializer<T> {
|
||||
add(value.source)
|
||||
add(value.viewer_flags)
|
||||
add(value.chapter_flags)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -33,7 +33,7 @@ open class TrackBaseSerializer<T : Track> : KSerializer<T> {
|
||||
put(LIBRARY, value.library_id)
|
||||
put(LAST_READ, value.last_chapter_read)
|
||||
put(TRACKING_URL, value.tracking_url)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -49,7 +49,7 @@ class ChapterCache(private val context: Context) {
|
||||
File(context.cacheDir, PARAMETER_CACHE_DIRECTORY),
|
||||
PARAMETER_APP_VERSION,
|
||||
PARAMETER_VALUE_COUNT,
|
||||
PARAMETER_CACHE_SIZE
|
||||
PARAMETER_CACHE_SIZE,
|
||||
)
|
||||
|
||||
/**
|
||||
|
@ -104,7 +104,7 @@ class CoverCache(private val context: Context) {
|
||||
* Clear coil's memory cache.
|
||||
*/
|
||||
fun clearMemoryCache() {
|
||||
context.imageLoader.memoryCache.clear()
|
||||
context.imageLoader.memoryCache?.clear()
|
||||
}
|
||||
|
||||
private fun getCacheDir(dir: String): File {
|
||||
|
@ -1,25 +0,0 @@
|
||||
package eu.kanade.tachiyomi.data.coil
|
||||
|
||||
import coil.bitmap.BitmapPool
|
||||
import coil.decode.DataSource
|
||||
import coil.decode.Options
|
||||
import coil.fetch.FetchResult
|
||||
import coil.fetch.Fetcher
|
||||
import coil.fetch.SourceResult
|
||||
import coil.size.Size
|
||||
import okio.buffer
|
||||
import okio.source
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
class ByteBufferFetcher : Fetcher<ByteBuffer> {
|
||||
override suspend fun fetch(pool: BitmapPool, data: ByteBuffer, size: Size, options: Options): FetchResult {
|
||||
return SourceResult(
|
||||
source = ByteArrayInputStream(data.array()).source().buffer(),
|
||||
mimeType = null,
|
||||
dataSource = DataSource.MEMORY
|
||||
)
|
||||
}
|
||||
|
||||
override fun key(data: ByteBuffer): String? = null
|
||||
}
|
@ -1,149 +1,261 @@
|
||||
package eu.kanade.tachiyomi.data.coil
|
||||
|
||||
import coil.bitmap.BitmapPool
|
||||
import coil.ImageLoader
|
||||
import coil.decode.DataSource
|
||||
import coil.decode.Options
|
||||
import coil.decode.ImageSource
|
||||
import coil.disk.DiskCache
|
||||
import coil.fetch.FetchResult
|
||||
import coil.fetch.Fetcher
|
||||
import coil.fetch.SourceResult
|
||||
import coil.network.HttpException
|
||||
import coil.request.get
|
||||
import coil.size.Size
|
||||
import coil.request.Options
|
||||
import coil.request.Parameters
|
||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||
import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher.Companion.USE_CUSTOM_COVER
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.network.await
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import logcat.LogPriority
|
||||
import okhttp3.CacheControl
|
||||
import okhttp3.Call
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import okhttp3.ResponseBody
|
||||
import okhttp3.internal.closeQuietly
|
||||
import okio.Path.Companion.toOkioPath
|
||||
import okio.Source
|
||||
import okio.buffer
|
||||
import okio.sink
|
||||
import okio.source
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.File
|
||||
import java.net.HttpURLConnection
|
||||
|
||||
/**
|
||||
* Coil component that fetches [Manga] cover while using the cached file in disk when available.
|
||||
* A [Fetcher] that fetches cover image for [Manga] object.
|
||||
*
|
||||
* It uses [Manga.thumbnail_url] if custom cover is not set by the user.
|
||||
* Disk caching for library items is handled by [CoverCache], otherwise
|
||||
* handled by Coil's [DiskCache].
|
||||
*
|
||||
* Available request parameter:
|
||||
* - [USE_CUSTOM_COVER]: Use custom cover if set by user, default is true
|
||||
*/
|
||||
class MangaCoverFetcher : Fetcher<Manga> {
|
||||
private val coverCache: CoverCache by injectLazy()
|
||||
private val sourceManager: SourceManager by injectLazy()
|
||||
private val defaultClient = Injekt.get<NetworkHelper>().coilClient
|
||||
class MangaCoverFetcher(
|
||||
private val manga: Manga,
|
||||
private val sourceLazy: Lazy<HttpSource?>,
|
||||
private val options: Options,
|
||||
private val coverCache: CoverCache,
|
||||
private val callFactoryLazy: Lazy<Call.Factory>,
|
||||
private val diskCacheLazy: Lazy<DiskCache>,
|
||||
) : Fetcher {
|
||||
|
||||
override fun key(data: Manga): String? {
|
||||
if (data.thumbnail_url.isNullOrBlank()) return null
|
||||
return data.thumbnail_url!!
|
||||
}
|
||||
// For non-custom cover
|
||||
private val diskCacheKey: String? by lazy { MangaCoverKeyer().key(manga, options) }
|
||||
private lateinit var url: String
|
||||
|
||||
override suspend fun fetch(pool: BitmapPool, data: Manga, size: Size, options: Options): FetchResult {
|
||||
override suspend fun fetch(): FetchResult {
|
||||
// Use custom cover if exists
|
||||
val useCustomCover = options.parameters[USE_CUSTOM_COVER] as? Boolean ?: true
|
||||
val customCoverFile = coverCache.getCustomCoverFile(data)
|
||||
val useCustomCover = options.parameters.value(USE_CUSTOM_COVER) ?: true
|
||||
val customCoverFile = coverCache.getCustomCoverFile(manga)
|
||||
if (useCustomCover && customCoverFile.exists()) {
|
||||
return fileLoader(customCoverFile)
|
||||
}
|
||||
|
||||
val cover = data.thumbnail_url
|
||||
return when (getResourceType(cover)) {
|
||||
Type.URL -> httpLoader(data, options)
|
||||
Type.File -> fileLoader(data)
|
||||
// diskCacheKey is thumbnail_url
|
||||
url = diskCacheKey ?: error("No cover specified")
|
||||
return when (getResourceType(url)) {
|
||||
Type.URL -> httpLoader()
|
||||
Type.File -> fileLoader(File(url.substringAfter("file://")))
|
||||
null -> error("Invalid image")
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun httpLoader(manga: Manga, options: Options): FetchResult {
|
||||
private fun fileLoader(file: File): FetchResult {
|
||||
return SourceResult(
|
||||
source = ImageSource(file = file.toOkioPath(), diskCacheKey = diskCacheKey),
|
||||
mimeType = "image/*",
|
||||
dataSource = DataSource.DISK,
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun httpLoader(): FetchResult {
|
||||
// Only cache separately if it's a library item
|
||||
val coverCacheFile = if (manga.favorite) {
|
||||
val libraryCoverCacheFile = if (manga.favorite) {
|
||||
coverCache.getCoverFile(manga) ?: error("No cover specified")
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
if (coverCacheFile?.exists() == true && options.diskCachePolicy.readEnabled) {
|
||||
return fileLoader(coverCacheFile)
|
||||
if (libraryCoverCacheFile?.exists() == true && options.diskCachePolicy.readEnabled) {
|
||||
return fileLoader(libraryCoverCacheFile)
|
||||
}
|
||||
|
||||
val (response, body) = awaitGetCall(manga, options)
|
||||
if (!response.isSuccessful) {
|
||||
body.close()
|
||||
var snapshot = readFromDiskCache()
|
||||
try {
|
||||
// Fetch from disk cache
|
||||
if (snapshot != null) {
|
||||
val snapshotCoverCache = moveSnapshotToCoverCache(snapshot, libraryCoverCacheFile)
|
||||
if (snapshotCoverCache != null) {
|
||||
// Read from cover cache after added to library
|
||||
return fileLoader(snapshotCoverCache)
|
||||
}
|
||||
|
||||
// Read from snapshot
|
||||
return SourceResult(
|
||||
source = snapshot.toImageSource(),
|
||||
mimeType = "image/*",
|
||||
dataSource = DataSource.DISK,
|
||||
)
|
||||
}
|
||||
|
||||
// Fetch from network
|
||||
val response = executeNetworkRequest()
|
||||
val responseBody = checkNotNull(response.body) { "Null response source" }
|
||||
try {
|
||||
// Read from cover cache after library manga cover updated
|
||||
val responseCoverCache = writeResponseToCoverCache(response, libraryCoverCacheFile)
|
||||
if (responseCoverCache != null) {
|
||||
return fileLoader(responseCoverCache)
|
||||
}
|
||||
|
||||
// Read from disk cache
|
||||
snapshot = writeToDiskCache(snapshot, response)
|
||||
if (snapshot != null) {
|
||||
return SourceResult(
|
||||
source = snapshot.toImageSource(),
|
||||
mimeType = "image/*",
|
||||
dataSource = DataSource.NETWORK,
|
||||
)
|
||||
}
|
||||
|
||||
// Read from response if cache is unused or unusable
|
||||
return SourceResult(
|
||||
source = ImageSource(source = responseBody.source(), context = options.context),
|
||||
mimeType = "image/*",
|
||||
dataSource = if (response.cacheResponse != null) DataSource.DISK else DataSource.NETWORK,
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
responseBody.closeQuietly()
|
||||
throw e
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
snapshot?.closeQuietly()
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun executeNetworkRequest(): Response {
|
||||
val client = sourceLazy.value?.client ?: callFactoryLazy.value
|
||||
val response = client.newCall(newRequest()).await()
|
||||
if (!response.isSuccessful && response.code != HttpURLConnection.HTTP_NOT_MODIFIED) {
|
||||
response.body?.closeQuietly()
|
||||
throw HttpException(response)
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
if (coverCacheFile != null && options.diskCachePolicy.writeEnabled) {
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
response.peekBody(Long.MAX_VALUE).source().use { input ->
|
||||
coverCacheFile.parentFile?.mkdirs()
|
||||
if (coverCacheFile.exists()) {
|
||||
coverCacheFile.delete()
|
||||
}
|
||||
coverCacheFile.sink().buffer().use { output ->
|
||||
output.writeAll(input)
|
||||
}
|
||||
private fun newRequest(): Request {
|
||||
val request = Request.Builder()
|
||||
.url(url)
|
||||
.headers(sourceLazy.value?.headers ?: options.headers)
|
||||
// Support attaching custom data to the network request.
|
||||
.tag(Parameters::class.java, options.parameters)
|
||||
|
||||
val diskRead = options.diskCachePolicy.readEnabled
|
||||
val networkRead = options.networkCachePolicy.readEnabled
|
||||
when {
|
||||
!networkRead && diskRead -> {
|
||||
request.cacheControl(CacheControl.FORCE_CACHE)
|
||||
}
|
||||
networkRead && !diskRead -> if (options.diskCachePolicy.writeEnabled) {
|
||||
request.cacheControl(CacheControl.FORCE_NETWORK)
|
||||
} else {
|
||||
request.cacheControl(CACHE_CONTROL_FORCE_NETWORK_NO_CACHE)
|
||||
}
|
||||
!networkRead && !diskRead -> {
|
||||
// This causes the request to fail with a 504 Unsatisfiable Request.
|
||||
request.cacheControl(CACHE_CONTROL_NO_NETWORK_NO_CACHE)
|
||||
}
|
||||
}
|
||||
|
||||
return SourceResult(
|
||||
source = body.source(),
|
||||
mimeType = "image/*",
|
||||
dataSource = if (response.cacheResponse != null) DataSource.DISK else DataSource.NETWORK
|
||||
)
|
||||
return request.build()
|
||||
}
|
||||
|
||||
private suspend fun awaitGetCall(manga: Manga, options: Options): Pair<Response, ResponseBody> {
|
||||
val call = getCall(manga, options)
|
||||
val response = call.await()
|
||||
return response to checkNotNull(response.body) { "Null response source" }
|
||||
}
|
||||
|
||||
private fun getCall(manga: Manga, options: Options): Call {
|
||||
val source = sourceManager.get(manga.source) as? HttpSource
|
||||
val request = Request.Builder().url(manga.thumbnail_url!!).also {
|
||||
if (source != null) {
|
||||
it.headers(source.headers)
|
||||
private fun moveSnapshotToCoverCache(snapshot: DiskCache.Snapshot, cacheFile: File?): File? {
|
||||
if (cacheFile == null) return null
|
||||
return try {
|
||||
diskCacheLazy.value.run {
|
||||
fileSystem.source(snapshot.data).use { input ->
|
||||
writeSourceToCoverCache(input, cacheFile)
|
||||
}
|
||||
remove(diskCacheKey!!)
|
||||
}
|
||||
cacheFile.takeIf { it.exists() }
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR, e) { "Failed to write snapshot data to cover cache ${cacheFile.name}" }
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
val networkRead = options.networkCachePolicy.readEnabled
|
||||
val diskRead = options.diskCachePolicy.readEnabled
|
||||
when {
|
||||
!networkRead && diskRead -> {
|
||||
it.cacheControl(CacheControl.FORCE_CACHE)
|
||||
}
|
||||
networkRead && !diskRead -> if (options.diskCachePolicy.writeEnabled) {
|
||||
it.cacheControl(CacheControl.FORCE_NETWORK)
|
||||
} else {
|
||||
it.cacheControl(CACHE_CONTROL_FORCE_NETWORK_NO_CACHE)
|
||||
}
|
||||
!networkRead && !diskRead -> {
|
||||
// This causes the request to fail with a 504 Unsatisfiable Request.
|
||||
it.cacheControl(CACHE_CONTROL_NO_NETWORK_NO_CACHE)
|
||||
}
|
||||
private fun writeResponseToCoverCache(response: Response, cacheFile: File?): File? {
|
||||
if (cacheFile == null || !options.diskCachePolicy.writeEnabled) return null
|
||||
return try {
|
||||
response.peekBody(Long.MAX_VALUE).source().use { input ->
|
||||
writeSourceToCoverCache(input, cacheFile)
|
||||
}
|
||||
}.build()
|
||||
|
||||
val client = source?.client?.newBuilder()?.cache(defaultClient.cache)?.build() ?: defaultClient
|
||||
return client.newCall(request)
|
||||
cacheFile.takeIf { it.exists() }
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR, e) { "Failed to write response data to cover cache ${cacheFile.name}" }
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun fileLoader(manga: Manga): FetchResult {
|
||||
return fileLoader(File(manga.thumbnail_url!!.substringAfter("file://")))
|
||||
private fun writeSourceToCoverCache(input: Source, cacheFile: File) {
|
||||
cacheFile.parentFile?.mkdirs()
|
||||
cacheFile.delete()
|
||||
try {
|
||||
cacheFile.sink().buffer().use { output ->
|
||||
output.writeAll(input)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
cacheFile.delete()
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
private fun fileLoader(file: File): FetchResult {
|
||||
return SourceResult(
|
||||
source = file.source().buffer(),
|
||||
mimeType = "image/*",
|
||||
dataSource = DataSource.DISK
|
||||
)
|
||||
private fun readFromDiskCache(): DiskCache.Snapshot? {
|
||||
return if (options.diskCachePolicy.readEnabled) diskCacheLazy.value[diskCacheKey!!] else null
|
||||
}
|
||||
|
||||
private fun writeToDiskCache(
|
||||
snapshot: DiskCache.Snapshot?,
|
||||
response: Response,
|
||||
): DiskCache.Snapshot? {
|
||||
if (!options.diskCachePolicy.writeEnabled) {
|
||||
snapshot?.closeQuietly()
|
||||
return null
|
||||
}
|
||||
val editor = if (snapshot != null) {
|
||||
snapshot.closeAndEdit()
|
||||
} else {
|
||||
diskCacheLazy.value.edit(diskCacheKey!!)
|
||||
} ?: return null
|
||||
try {
|
||||
diskCacheLazy.value.fileSystem.write(editor.data) {
|
||||
response.body!!.source().readAll(this)
|
||||
}
|
||||
return editor.commitAndGet()
|
||||
} catch (e: Exception) {
|
||||
try {
|
||||
editor.abort()
|
||||
} catch (ignored: Exception) {
|
||||
}
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
private fun DiskCache.Snapshot.toImageSource(): ImageSource {
|
||||
return ImageSource(file = data, diskCacheKey = diskCacheKey, closeable = this)
|
||||
}
|
||||
|
||||
private fun getResourceType(cover: String?): Type? {
|
||||
@ -159,6 +271,20 @@ class MangaCoverFetcher : Fetcher<Manga> {
|
||||
File, URL
|
||||
}
|
||||
|
||||
class Factory(
|
||||
private val callFactoryLazy: Lazy<Call.Factory>,
|
||||
private val diskCacheLazy: Lazy<DiskCache>,
|
||||
) : Fetcher.Factory<Manga> {
|
||||
|
||||
private val coverCache: CoverCache by injectLazy()
|
||||
private val sourceManager: SourceManager by injectLazy()
|
||||
|
||||
override fun create(data: Manga, options: Options, imageLoader: ImageLoader): Fetcher {
|
||||
val source = lazy { sourceManager.get(data.source) as? HttpSource }
|
||||
return MangaCoverFetcher(data, source, options, coverCache, callFactoryLazy, diskCacheLazy)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val USE_CUSTOM_COVER = "use_custom_cover"
|
||||
|
||||
|
@ -0,0 +1,11 @@
|
||||
package eu.kanade.tachiyomi.data.coil
|
||||
|
||||
import coil.key.Keyer
|
||||
import coil.request.Options
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
|
||||
class MangaCoverKeyer : Keyer<Manga> {
|
||||
override fun key(data: Manga, options: Options): String? {
|
||||
return data.thumbnail_url?.takeIf { it.isNotBlank() }
|
||||
}
|
||||
}
|
@ -1,13 +1,14 @@
|
||||
package eu.kanade.tachiyomi.data.coil
|
||||
|
||||
import android.content.res.Resources
|
||||
import android.os.Build
|
||||
import androidx.core.graphics.drawable.toDrawable
|
||||
import coil.bitmap.BitmapPool
|
||||
import coil.ImageLoader
|
||||
import coil.decode.DecodeResult
|
||||
import coil.decode.Decoder
|
||||
import coil.decode.Options
|
||||
import coil.size.Size
|
||||
import coil.decode.ImageDecoderDecoder
|
||||
import coil.decode.ImageSource
|
||||
import coil.fetch.SourceResult
|
||||
import coil.request.Options
|
||||
import eu.kanade.tachiyomi.util.system.ImageUtil
|
||||
import okio.BufferedSource
|
||||
import tachiyomi.decoder.ImageDecoder
|
||||
@ -15,26 +16,10 @@ import tachiyomi.decoder.ImageDecoder
|
||||
/**
|
||||
* A [Decoder] that uses built-in [ImageDecoder] to decode images that is not supported by the system.
|
||||
*/
|
||||
class TachiyomiImageDecoder(private val resources: Resources) : Decoder {
|
||||
class TachiyomiImageDecoder(private val resources: ImageSource, private val options: Options) : Decoder {
|
||||
|
||||
override fun handles(source: BufferedSource, mimeType: String?): Boolean {
|
||||
val type = source.peek().inputStream().use {
|
||||
ImageUtil.findImageType(it)
|
||||
}
|
||||
return when (type) {
|
||||
ImageUtil.ImageType.AVIF, ImageUtil.ImageType.JXL -> true
|
||||
ImageUtil.ImageType.HEIF -> Build.VERSION.SDK_INT < Build.VERSION_CODES.O
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun decode(
|
||||
pool: BitmapPool,
|
||||
source: BufferedSource,
|
||||
size: Size,
|
||||
options: Options
|
||||
): DecodeResult {
|
||||
val decoder = source.use {
|
||||
override suspend fun decode(): DecodeResult {
|
||||
val decoder = resources.sourceOrNull()?.use {
|
||||
ImageDecoder.newInstance(it.inputStream())
|
||||
}
|
||||
|
||||
@ -46,8 +31,31 @@ class TachiyomiImageDecoder(private val resources: Resources) : Decoder {
|
||||
check(bitmap != null) { "Failed to decode image." }
|
||||
|
||||
return DecodeResult(
|
||||
drawable = bitmap.toDrawable(resources),
|
||||
isSampled = false
|
||||
drawable = bitmap.toDrawable(options.context.resources),
|
||||
isSampled = false,
|
||||
)
|
||||
}
|
||||
|
||||
class Factory : Decoder.Factory {
|
||||
|
||||
override fun create(result: SourceResult, options: Options, imageLoader: ImageLoader): Decoder? {
|
||||
if (!isApplicable(result.source.source())) return null
|
||||
return TachiyomiImageDecoder(result.source, options)
|
||||
}
|
||||
|
||||
private fun isApplicable(source: BufferedSource): Boolean {
|
||||
val type = source.peek().inputStream().use {
|
||||
ImageUtil.findImageType(it)
|
||||
}
|
||||
return when (type) {
|
||||
ImageUtil.ImageType.AVIF, ImageUtil.ImageType.JXL -> true
|
||||
ImageUtil.ImageType.HEIF -> Build.VERSION.SDK_INT < Build.VERSION_CODES.O
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
override fun equals(other: Any?) = other is ImageDecoderDecoder.Factory
|
||||
|
||||
override fun hashCode() = javaClass.hashCode()
|
||||
}
|
||||
}
|
||||
|
@ -46,7 +46,7 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
|
||||
// Fix kissmanga covers after supporting cloudflare
|
||||
db.execSQL(
|
||||
"""UPDATE mangas SET thumbnail_url =
|
||||
REPLACE(thumbnail_url, '93.174.95.110', 'kissmanga.com') WHERE source = 4"""
|
||||
REPLACE(thumbnail_url, '93.174.95.110', 'kissmanga.com') WHERE source = 4""",
|
||||
)
|
||||
}
|
||||
if (oldVersion < 3) {
|
||||
@ -98,5 +98,14 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
|
||||
|
||||
override fun onConfigure(db: SupportSQLiteDatabase) {
|
||||
db.setForeignKeyConstraintsEnabled(true)
|
||||
setPragma(db, "foreign_keys = ON")
|
||||
setPragma(db, "journal_mode = WAL")
|
||||
setPragma(db, "synchronous = NORMAL")
|
||||
}
|
||||
|
||||
private fun setPragma(db: SupportSQLiteDatabase, pragma: String) {
|
||||
val cursor = db.query("PRAGMA $pragma")
|
||||
cursor.moveToFirst()
|
||||
cursor.close()
|
||||
}
|
||||
}
|
||||
|
@ -20,7 +20,7 @@ import eu.kanade.tachiyomi.data.database.tables.CategoryTable.TABLE
|
||||
class CategoryTypeMapping : SQLiteTypeMapping<Category>(
|
||||
CategoryPutResolver(),
|
||||
CategoryGetResolver(),
|
||||
CategoryDeleteResolver()
|
||||
CategoryDeleteResolver(),
|
||||
)
|
||||
|
||||
class CategoryPutResolver : DefaultPutResolver<Category>() {
|
||||
@ -40,7 +40,7 @@ class CategoryPutResolver : DefaultPutResolver<Category>() {
|
||||
COL_ID to obj.id,
|
||||
COL_NAME to obj.name,
|
||||
COL_ORDER to obj.order,
|
||||
COL_FLAGS to obj.flags
|
||||
COL_FLAGS to obj.flags,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -28,7 +28,7 @@ import eu.kanade.tachiyomi.data.database.tables.ChapterTable.TABLE
|
||||
class ChapterTypeMapping : SQLiteTypeMapping<Chapter>(
|
||||
ChapterPutResolver(),
|
||||
ChapterGetResolver(),
|
||||
ChapterDeleteResolver()
|
||||
ChapterDeleteResolver(),
|
||||
)
|
||||
|
||||
class ChapterPutResolver : DefaultPutResolver<Chapter>() {
|
||||
@ -56,7 +56,7 @@ class ChapterPutResolver : DefaultPutResolver<Chapter>() {
|
||||
COL_DATE_UPLOAD to obj.date_upload,
|
||||
COL_LAST_PAGE_READ to obj.last_page_read,
|
||||
COL_CHAPTER_NUMBER to obj.chapter_number,
|
||||
COL_SOURCE_ORDER to obj.source_order
|
||||
COL_SOURCE_ORDER to obj.source_order,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -20,7 +20,7 @@ import eu.kanade.tachiyomi.data.database.tables.HistoryTable.TABLE
|
||||
class HistoryTypeMapping : SQLiteTypeMapping<History>(
|
||||
HistoryPutResolver(),
|
||||
HistoryGetResolver(),
|
||||
HistoryDeleteResolver()
|
||||
HistoryDeleteResolver(),
|
||||
)
|
||||
|
||||
open class HistoryPutResolver : DefaultPutResolver<History>() {
|
||||
@ -40,7 +40,7 @@ open class HistoryPutResolver : DefaultPutResolver<History>() {
|
||||
COL_ID to obj.id,
|
||||
COL_CHAPTER_ID to obj.chapter_id,
|
||||
COL_LAST_READ to obj.last_read,
|
||||
COL_TIME_READ to obj.time_read
|
||||
COL_TIME_READ to obj.time_read,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -18,7 +18,7 @@ import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable.TABLE
|
||||
class MangaCategoryTypeMapping : SQLiteTypeMapping<MangaCategory>(
|
||||
MangaCategoryPutResolver(),
|
||||
MangaCategoryGetResolver(),
|
||||
MangaCategoryDeleteResolver()
|
||||
MangaCategoryDeleteResolver(),
|
||||
)
|
||||
|
||||
class MangaCategoryPutResolver : DefaultPutResolver<MangaCategory>() {
|
||||
@ -37,7 +37,7 @@ class MangaCategoryPutResolver : DefaultPutResolver<MangaCategory>() {
|
||||
contentValuesOf(
|
||||
COL_ID to obj.id,
|
||||
COL_MANGA_ID to obj.manga_id,
|
||||
COL_CATEGORY_ID to obj.category_id
|
||||
COL_CATEGORY_ID to obj.category_id,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -33,7 +33,7 @@ import eu.kanade.tachiyomi.data.database.tables.MangaTable.TABLE
|
||||
class MangaTypeMapping : SQLiteTypeMapping<Manga>(
|
||||
MangaPutResolver(),
|
||||
MangaGetResolver(),
|
||||
MangaDeleteResolver()
|
||||
MangaDeleteResolver(),
|
||||
)
|
||||
|
||||
class MangaPutResolver : DefaultPutResolver<Manga>() {
|
||||
@ -66,7 +66,7 @@ class MangaPutResolver : DefaultPutResolver<Manga>() {
|
||||
COL_VIEWER to obj.viewer_flags,
|
||||
COL_CHAPTER_FLAGS to obj.chapter_flags,
|
||||
COL_COVER_LAST_MODIFIED to obj.cover_last_modified,
|
||||
COL_DATE_ADDED to obj.date_added
|
||||
COL_DATE_ADDED to obj.date_added,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -29,7 +29,7 @@ import eu.kanade.tachiyomi.data.database.tables.TrackTable.TABLE
|
||||
class TrackTypeMapping : SQLiteTypeMapping<Track>(
|
||||
TrackPutResolver(),
|
||||
TrackGetResolver(),
|
||||
TrackDeleteResolver()
|
||||
TrackDeleteResolver(),
|
||||
)
|
||||
|
||||
class TrackPutResolver : DefaultPutResolver<Track>() {
|
||||
@ -58,7 +58,7 @@ class TrackPutResolver : DefaultPutResolver<Track>() {
|
||||
COL_TRACKING_URL to obj.tracking_url,
|
||||
COL_SCORE to obj.score,
|
||||
COL_START_DATE to obj.started_reading_date,
|
||||
COL_FINISH_DATE to obj.finished_reading_date
|
||||
COL_FINISH_DATE to obj.finished_reading_date,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -2,7 +2,14 @@ package eu.kanade.tachiyomi.data.database.models
|
||||
|
||||
class LibraryManga : MangaImpl() {
|
||||
|
||||
var unread: Int = 0
|
||||
var unreadCount: Int = 0
|
||||
var readCount: Int = 0
|
||||
|
||||
val totalChapters
|
||||
get() = readCount + unreadCount
|
||||
|
||||
val hasStarted
|
||||
get() = readCount > 0
|
||||
|
||||
var category: Int = 0
|
||||
}
|
||||
|
@ -32,11 +32,6 @@ interface Manga : SManga {
|
||||
return chapter_flags and CHAPTER_SORT_MASK == CHAPTER_SORT_DESC
|
||||
}
|
||||
|
||||
fun getGenres(): List<String>? {
|
||||
if (genre.isNullOrBlank()) return null
|
||||
return genre?.split(", ")?.map { it.trim() }?.filterNot { it.isBlank() }?.distinct()
|
||||
}
|
||||
|
||||
private fun setChapterFlags(flag: Int, mask: Int) {
|
||||
chapter_flags = chapter_flags and mask.inv() or (flag and mask)
|
||||
}
|
||||
@ -125,6 +120,6 @@ fun Manga.toMangaInfo(): MangaInfo {
|
||||
genres = this.getGenres() ?: emptyList(),
|
||||
key = this.url,
|
||||
status = this.status,
|
||||
title = this.title
|
||||
title = this.title,
|
||||
)
|
||||
}
|
||||
|
@ -15,7 +15,7 @@ interface CategoryQueries : DbProvider {
|
||||
Query.builder()
|
||||
.table(CategoryTable.TABLE)
|
||||
.orderBy(CategoryTable.COL_ORDER)
|
||||
.build()
|
||||
.build(),
|
||||
)
|
||||
.prepare()
|
||||
|
||||
@ -25,7 +25,7 @@ interface CategoryQueries : DbProvider {
|
||||
RawQuery.builder()
|
||||
.query(getCategoriesForMangaQuery())
|
||||
.args(manga.id)
|
||||
.build()
|
||||
.build(),
|
||||
)
|
||||
.prepare()
|
||||
|
||||
|
@ -23,7 +23,7 @@ interface ChapterQueries : DbProvider {
|
||||
.table(ChapterTable.TABLE)
|
||||
.where("${ChapterTable.COL_MANGA_ID} = ?")
|
||||
.whereArgs(manga.id)
|
||||
.build()
|
||||
.build(),
|
||||
)
|
||||
.prepare()
|
||||
|
||||
@ -34,7 +34,7 @@ interface ChapterQueries : DbProvider {
|
||||
.query(getRecentsQuery())
|
||||
.args(date.time)
|
||||
.observesTables(ChapterTable.TABLE)
|
||||
.build()
|
||||
.build(),
|
||||
)
|
||||
.withGetResolver(MangaChapterGetResolver.INSTANCE)
|
||||
.prepare()
|
||||
@ -46,7 +46,7 @@ interface ChapterQueries : DbProvider {
|
||||
.table(ChapterTable.TABLE)
|
||||
.where("${ChapterTable.COL_ID} = ?")
|
||||
.whereArgs(id)
|
||||
.build()
|
||||
.build(),
|
||||
)
|
||||
.prepare()
|
||||
|
||||
@ -57,7 +57,7 @@ interface ChapterQueries : DbProvider {
|
||||
.table(ChapterTable.TABLE)
|
||||
.where("${ChapterTable.COL_URL} = ?")
|
||||
.whereArgs(url)
|
||||
.build()
|
||||
.build(),
|
||||
)
|
||||
.prepare()
|
||||
|
||||
@ -68,7 +68,7 @@ interface ChapterQueries : DbProvider {
|
||||
.table(ChapterTable.TABLE)
|
||||
.where("${ChapterTable.COL_URL} = ? AND ${ChapterTable.COL_MANGA_ID} = ?")
|
||||
.whereArgs(url, mangaId)
|
||||
.build()
|
||||
.build(),
|
||||
)
|
||||
.prepare()
|
||||
|
||||
|
@ -32,7 +32,7 @@ interface HistoryQueries : DbProvider {
|
||||
.query(getRecentMangasQuery(search))
|
||||
.args(date.time, limit, offset)
|
||||
.observesTables(HistoryTable.TABLE)
|
||||
.build()
|
||||
.build(),
|
||||
)
|
||||
.withGetResolver(MangaChapterHistoryGetResolver.INSTANCE)
|
||||
.prepare()
|
||||
@ -44,7 +44,7 @@ interface HistoryQueries : DbProvider {
|
||||
.query(getHistoryByMangaId())
|
||||
.args(mangaId)
|
||||
.observesTables(HistoryTable.TABLE)
|
||||
.build()
|
||||
.build(),
|
||||
)
|
||||
.prepare()
|
||||
|
||||
@ -55,7 +55,7 @@ interface HistoryQueries : DbProvider {
|
||||
.query(getHistoryByChapterUrl())
|
||||
.args(chapterUrl)
|
||||
.observesTables(HistoryTable.TABLE)
|
||||
.build()
|
||||
.build(),
|
||||
)
|
||||
.prepare()
|
||||
|
||||
@ -83,7 +83,7 @@ interface HistoryQueries : DbProvider {
|
||||
.byQuery(
|
||||
DeleteQuery.builder()
|
||||
.table(HistoryTable.TABLE)
|
||||
.build()
|
||||
.build(),
|
||||
)
|
||||
.prepare()
|
||||
|
||||
@ -93,7 +93,7 @@ interface HistoryQueries : DbProvider {
|
||||
.table(HistoryTable.TABLE)
|
||||
.where("${HistoryTable.COL_LAST_READ} = ?")
|
||||
.whereArgs(0)
|
||||
.build()
|
||||
.build(),
|
||||
)
|
||||
.prepare()
|
||||
}
|
||||
|
@ -20,7 +20,7 @@ interface MangaCategoryQueries : DbProvider {
|
||||
.table(MangaCategoryTable.TABLE)
|
||||
.where("${MangaCategoryTable.COL_MANGA_ID} IN (${Queries.placeholders(mangas.size)})")
|
||||
.whereArgs(*mangas.map { it.id }.toTypedArray())
|
||||
.build()
|
||||
.build(),
|
||||
)
|
||||
.prepare()
|
||||
|
||||
|
@ -29,11 +29,26 @@ interface MangaQueries : DbProvider {
|
||||
RawQuery.builder()
|
||||
.query(libraryQuery)
|
||||
.observesTables(MangaTable.TABLE, ChapterTable.TABLE, MangaCategoryTable.TABLE, CategoryTable.TABLE)
|
||||
.build()
|
||||
.build(),
|
||||
)
|
||||
.withGetResolver(LibraryMangaGetResolver.INSTANCE)
|
||||
.prepare()
|
||||
|
||||
fun getDuplicateLibraryManga(manga: Manga) = db.get()
|
||||
.`object`(Manga::class.java)
|
||||
.withQuery(
|
||||
Query.builder()
|
||||
.table(MangaTable.TABLE)
|
||||
.where("${MangaTable.COL_FAVORITE} = 1 AND LOWER(${MangaTable.COL_TITLE}) = ? AND ${MangaTable.COL_SOURCE} != ?")
|
||||
.whereArgs(
|
||||
manga.title.lowercase(),
|
||||
manga.source,
|
||||
)
|
||||
.limit(1)
|
||||
.build(),
|
||||
)
|
||||
.prepare()
|
||||
|
||||
fun getFavoriteMangas(sortByTitle: Boolean = true): PreparedGetListOfObjects<Manga> {
|
||||
var queryBuilder = Query.builder()
|
||||
.table(MangaTable.TABLE)
|
||||
@ -57,7 +72,7 @@ interface MangaQueries : DbProvider {
|
||||
.table(MangaTable.TABLE)
|
||||
.where("${MangaTable.COL_URL} = ? AND ${MangaTable.COL_SOURCE} = ?")
|
||||
.whereArgs(url, sourceId)
|
||||
.build()
|
||||
.build(),
|
||||
)
|
||||
.prepare()
|
||||
|
||||
@ -68,7 +83,7 @@ interface MangaQueries : DbProvider {
|
||||
.table(MangaTable.TABLE)
|
||||
.where("${MangaTable.COL_ID} = ?")
|
||||
.whereArgs(id)
|
||||
.build()
|
||||
.build(),
|
||||
)
|
||||
.prepare()
|
||||
|
||||
@ -78,7 +93,7 @@ interface MangaQueries : DbProvider {
|
||||
RawQuery.builder()
|
||||
.query(getSourceIdsWithNonLibraryMangaQuery())
|
||||
.observesTables(MangaTable.TABLE)
|
||||
.build()
|
||||
.build(),
|
||||
)
|
||||
.withGetResolver(SourceIdMangaCountGetResolver.INSTANCE)
|
||||
.prepare()
|
||||
@ -137,7 +152,7 @@ interface MangaQueries : DbProvider {
|
||||
.table(MangaTable.TABLE)
|
||||
.where("${MangaTable.COL_FAVORITE} = ? AND ${MangaTable.COL_SOURCE} IN (${Queries.placeholders(sourceIds.size)})")
|
||||
.whereArgs(0, *sourceIds.toTypedArray())
|
||||
.build()
|
||||
.build(),
|
||||
)
|
||||
.prepare()
|
||||
|
||||
@ -145,7 +160,7 @@ interface MangaQueries : DbProvider {
|
||||
.byQuery(
|
||||
DeleteQuery.builder()
|
||||
.table(MangaTable.TABLE)
|
||||
.build()
|
||||
.build(),
|
||||
)
|
||||
.prepare()
|
||||
|
||||
@ -155,7 +170,7 @@ interface MangaQueries : DbProvider {
|
||||
RawQuery.builder()
|
||||
.query(getLastReadMangaQuery())
|
||||
.observesTables(MangaTable.TABLE)
|
||||
.build()
|
||||
.build(),
|
||||
)
|
||||
.prepare()
|
||||
|
||||
@ -165,7 +180,7 @@ interface MangaQueries : DbProvider {
|
||||
RawQuery.builder()
|
||||
.query(getTotalChapterMangaQuery())
|
||||
.observesTables(MangaTable.TABLE)
|
||||
.build()
|
||||
.build(),
|
||||
)
|
||||
.prepare()
|
||||
|
||||
@ -175,7 +190,7 @@ interface MangaQueries : DbProvider {
|
||||
RawQuery.builder()
|
||||
.query(getLatestChapterMangaQuery())
|
||||
.observesTables(MangaTable.TABLE)
|
||||
.build()
|
||||
.build(),
|
||||
)
|
||||
.prepare()
|
||||
|
||||
@ -185,7 +200,7 @@ interface MangaQueries : DbProvider {
|
||||
RawQuery.builder()
|
||||
.query(getChapterFetchDateMangaQuery())
|
||||
.observesTables(MangaTable.TABLE)
|
||||
.build()
|
||||
.build(),
|
||||
)
|
||||
.prepare()
|
||||
}
|
||||
|
@ -8,21 +8,28 @@ import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable as MangaCateg
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable as Manga
|
||||
|
||||
/**
|
||||
* Query to get the manga from the library, with their categories and unread count.
|
||||
* Query to get the manga from the library, with their categories, read and unread count.
|
||||
*/
|
||||
val libraryQuery =
|
||||
"""
|
||||
SELECT M.*, COALESCE(MC.${MangaCategory.COL_CATEGORY_ID}, 0) AS ${Manga.COL_CATEGORY}
|
||||
FROM (
|
||||
SELECT ${Manga.TABLE}.*, COALESCE(C.unread, 0) AS ${Manga.COL_UNREAD}
|
||||
SELECT ${Manga.TABLE}.*, COALESCE(C.unreadCount, 0) AS ${Manga.COMPUTED_COL_UNREAD_COUNT}, COALESCE(R.readCount, 0) AS ${Manga.COMPUTED_COL_READ_COUNT}
|
||||
FROM ${Manga.TABLE}
|
||||
LEFT JOIN (
|
||||
SELECT ${Chapter.COL_MANGA_ID}, COUNT(*) AS unread
|
||||
SELECT ${Chapter.COL_MANGA_ID}, COUNT(*) AS unreadCount
|
||||
FROM ${Chapter.TABLE}
|
||||
WHERE ${Chapter.COL_READ} = 0
|
||||
GROUP BY ${Chapter.COL_MANGA_ID}
|
||||
) AS C
|
||||
ON ${Manga.COL_ID} = C.${Chapter.COL_MANGA_ID}
|
||||
LEFT JOIN (
|
||||
SELECT ${Chapter.COL_MANGA_ID}, COUNT(*) AS readCount
|
||||
FROM ${Chapter.TABLE}
|
||||
WHERE ${Chapter.COL_READ} = 1
|
||||
GROUP BY ${Chapter.COL_MANGA_ID}
|
||||
) AS R
|
||||
ON ${Manga.COL_ID} = R.${Chapter.COL_MANGA_ID}
|
||||
WHERE ${Manga.COL_FAVORITE} = 1
|
||||
GROUP BY ${Manga.COL_ID}
|
||||
ORDER BY ${Manga.COL_TITLE}
|
||||
|
@ -15,7 +15,7 @@ interface TrackQueries : DbProvider {
|
||||
.withQuery(
|
||||
Query.builder()
|
||||
.table(TrackTable.TABLE)
|
||||
.build()
|
||||
.build(),
|
||||
)
|
||||
.prepare()
|
||||
|
||||
@ -26,7 +26,7 @@ interface TrackQueries : DbProvider {
|
||||
.table(TrackTable.TABLE)
|
||||
.where("${TrackTable.COL_MANGA_ID} = ?")
|
||||
.whereArgs(manga.id)
|
||||
.build()
|
||||
.build(),
|
||||
)
|
||||
.prepare()
|
||||
|
||||
@ -40,7 +40,7 @@ interface TrackQueries : DbProvider {
|
||||
.table(TrackTable.TABLE)
|
||||
.where("${TrackTable.COL_MANGA_ID} = ? AND ${TrackTable.COL_SYNC_ID} = ?")
|
||||
.whereArgs(manga.id, sync.id)
|
||||
.build()
|
||||
.build(),
|
||||
)
|
||||
.prepare()
|
||||
}
|
||||
|
@ -29,6 +29,6 @@ class ChapterBackupPutResolver : PutResolver<Chapter>() {
|
||||
contentValuesOf(
|
||||
ChapterTable.COL_READ to chapter.read,
|
||||
ChapterTable.COL_BOOKMARK to chapter.bookmark,
|
||||
ChapterTable.COL_LAST_PAGE_READ to chapter.last_page_read
|
||||
ChapterTable.COL_LAST_PAGE_READ to chapter.last_page_read,
|
||||
)
|
||||
}
|
||||
|
@ -29,6 +29,6 @@ class ChapterKnownBackupPutResolver : PutResolver<Chapter>() {
|
||||
contentValuesOf(
|
||||
ChapterTable.COL_READ to chapter.read,
|
||||
ChapterTable.COL_BOOKMARK to chapter.bookmark,
|
||||
ChapterTable.COL_LAST_PAGE_READ to chapter.last_page_read
|
||||
ChapterTable.COL_LAST_PAGE_READ to chapter.last_page_read,
|
||||
)
|
||||
}
|
||||
|
@ -29,6 +29,6 @@ class ChapterProgressPutResolver : PutResolver<Chapter>() {
|
||||
contentValuesOf(
|
||||
ChapterTable.COL_READ to chapter.read,
|
||||
ChapterTable.COL_BOOKMARK to chapter.bookmark,
|
||||
ChapterTable.COL_LAST_PAGE_READ to chapter.last_page_read
|
||||
ChapterTable.COL_LAST_PAGE_READ to chapter.last_page_read,
|
||||
)
|
||||
}
|
||||
|
@ -27,6 +27,6 @@ class ChapterSourceOrderPutResolver : PutResolver<Chapter>() {
|
||||
|
||||
fun mapToContentValues(chapter: Chapter) =
|
||||
contentValuesOf(
|
||||
ChapterTable.COL_SOURCE_ORDER to chapter.source_order
|
||||
ChapterTable.COL_SOURCE_ORDER to chapter.source_order,
|
||||
)
|
||||
}
|
||||
|
@ -24,7 +24,7 @@ class HistoryLastReadPutResolver : HistoryPutResolver() {
|
||||
.table(updateQuery.table())
|
||||
.where(updateQuery.where())
|
||||
.whereArgs(updateQuery.whereArgs())
|
||||
.build()
|
||||
.build(),
|
||||
)
|
||||
|
||||
cursor.use { putCursor ->
|
||||
@ -47,6 +47,6 @@ class HistoryLastReadPutResolver : HistoryPutResolver() {
|
||||
|
||||
private fun mapToUpdateContentValues(history: History) =
|
||||
contentValuesOf(
|
||||
HistoryTable.COL_LAST_READ to history.last_read
|
||||
HistoryTable.COL_LAST_READ to history.last_read,
|
||||
)
|
||||
}
|
||||
|
@ -16,8 +16,9 @@ class LibraryMangaGetResolver : DefaultGetResolver<LibraryManga>(), BaseMangaGet
|
||||
val manga = LibraryManga()
|
||||
|
||||
mapBaseFromCursor(manga, cursor)
|
||||
manga.unread = cursor.getInt(cursor.getColumnIndexOrThrow(MangaTable.COL_UNREAD))
|
||||
manga.unreadCount = cursor.getInt(cursor.getColumnIndexOrThrow(MangaTable.COMPUTED_COL_UNREAD_COUNT))
|
||||
manga.category = cursor.getInt(cursor.getColumnIndexOrThrow(MangaTable.COL_CATEGORY))
|
||||
manga.readCount = cursor.getInt(cursor.getColumnIndexOrThrow(MangaTable.COMPUTED_COL_READ_COUNT))
|
||||
|
||||
return manga
|
||||
}
|
||||
|
@ -27,6 +27,6 @@ class MangaCoverLastModifiedPutResolver : PutResolver<Manga>() {
|
||||
|
||||
fun mapToContentValues(manga: Manga) =
|
||||
contentValuesOf(
|
||||
MangaTable.COL_COVER_LAST_MODIFIED to manga.cover_last_modified
|
||||
MangaTable.COL_COVER_LAST_MODIFIED to manga.cover_last_modified,
|
||||
)
|
||||
}
|
||||
|
@ -27,6 +27,6 @@ class MangaFavoritePutResolver : PutResolver<Manga>() {
|
||||
|
||||
fun mapToContentValues(manga: Manga) =
|
||||
contentValuesOf(
|
||||
MangaTable.COL_FAVORITE to manga.favorite
|
||||
MangaTable.COL_FAVORITE to manga.favorite,
|
||||
)
|
||||
}
|
||||
|
@ -28,6 +28,6 @@ class MangaFlagsPutResolver(private val colName: String, private val fieldGetter
|
||||
|
||||
fun mapToContentValues(manga: Manga) =
|
||||
contentValuesOf(
|
||||
colName to fieldGetter.get(manga)
|
||||
colName to fieldGetter.get(manga),
|
||||
)
|
||||
}
|
||||
|
@ -27,6 +27,6 @@ class MangaLastUpdatedPutResolver : PutResolver<Manga>() {
|
||||
|
||||
fun mapToContentValues(manga: Manga) =
|
||||
contentValuesOf(
|
||||
MangaTable.COL_LAST_UPDATE to manga.last_update
|
||||
MangaTable.COL_LAST_UPDATE to manga.last_update,
|
||||
)
|
||||
}
|
||||
|
@ -27,6 +27,6 @@ class MangaTitlePutResolver : PutResolver<Manga>() {
|
||||
|
||||
fun mapToContentValues(manga: Manga) =
|
||||
contentValuesOf(
|
||||
MangaTable.COL_TITLE to manga.title
|
||||
MangaTable.COL_TITLE to manga.title,
|
||||
)
|
||||
}
|
||||
|
@ -39,12 +39,15 @@ object MangaTable {
|
||||
|
||||
const val COL_CHAPTER_FLAGS = "chapter_flags"
|
||||
|
||||
const val COL_UNREAD = "unread"
|
||||
|
||||
const val COL_CATEGORY = "category"
|
||||
|
||||
const val COL_COVER_LAST_MODIFIED = "cover_last_modified"
|
||||
|
||||
// Not an actual value but computed when created
|
||||
const val COMPUTED_COL_UNREAD_COUNT = "unread_count"
|
||||
|
||||
const val COMPUTED_COL_READ_COUNT = "read_count"
|
||||
|
||||
val createTableQuery: String
|
||||
get() =
|
||||
"""CREATE TABLE $TABLE(
|
||||
|
@ -73,7 +73,7 @@ object TrackTable {
|
||||
|INSERT INTO $TABLE($COL_ID,$COL_MANGA_ID,$COL_SYNC_ID,$COL_MEDIA_ID,$COL_LIBRARY_ID,$COL_TITLE,$COL_LAST_CHAPTER_READ,$COL_TOTAL_CHAPTERS,$COL_STATUS,$COL_SCORE,$COL_TRACKING_URL,$COL_START_DATE,$COL_FINISH_DATE)
|
||||
|SELECT $COL_ID,$COL_MANGA_ID,$COL_SYNC_ID,$COL_MEDIA_ID,$COL_LIBRARY_ID,$COL_TITLE,$COL_LAST_CHAPTER_READ,$COL_TOTAL_CHAPTERS,$COL_STATUS,$COL_SCORE,$COL_TRACKING_URL,$COL_START_DATE,$COL_FINISH_DATE
|
||||
|FROM ${TABLE}_tmp
|
||||
""".trimMargin()
|
||||
""".trimMargin()
|
||||
|
||||
val dropTempTable: String
|
||||
get() = "DROP TABLE ${TABLE}_tmp"
|
||||
|
@ -27,7 +27,7 @@ class DownloadCache(
|
||||
private val context: Context,
|
||||
private val provider: DownloadProvider,
|
||||
private val sourceManager: SourceManager,
|
||||
private val preferences: PreferencesHelper = Injekt.get()
|
||||
private val preferences: PreferencesHelper = Injekt.get(),
|
||||
) {
|
||||
|
||||
/**
|
||||
@ -236,7 +236,7 @@ class DownloadCache(
|
||||
*/
|
||||
private class RootDirectory(
|
||||
val dir: UniFile,
|
||||
var files: Map<Long, SourceDirectory> = hashMapOf()
|
||||
var files: Map<Long, SourceDirectory> = hashMapOf(),
|
||||
)
|
||||
|
||||
/**
|
||||
@ -244,7 +244,7 @@ class DownloadCache(
|
||||
*/
|
||||
private class SourceDirectory(
|
||||
val dir: UniFile,
|
||||
var files: Map<String, MangaDirectory> = hashMapOf()
|
||||
var files: Map<String, MangaDirectory> = hashMapOf(),
|
||||
)
|
||||
|
||||
/**
|
||||
@ -252,7 +252,7 @@ class DownloadCache(
|
||||
*/
|
||||
private class MangaDirectory(
|
||||
val dir: UniFile,
|
||||
var files: Set<String> = hashSetOf()
|
||||
var files: Set<String> = hashSetOf(),
|
||||
)
|
||||
|
||||
/**
|
||||
|
@ -30,7 +30,7 @@ import uy.kohesive.injekt.injectLazy
|
||||
*/
|
||||
class DownloadManager(
|
||||
private val context: Context,
|
||||
private val db: DatabaseHelper = Injekt.get()
|
||||
private val db: DatabaseHelper = Injekt.get(),
|
||||
) {
|
||||
|
||||
private val sourceManager: SourceManager by injectLazy()
|
||||
|
@ -1,5 +1,6 @@
|
||||
package eu.kanade.tachiyomi.data.download
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.graphics.BitmapFactory
|
||||
import androidx.core.app.NotificationCompat
|
||||
@ -93,14 +94,14 @@ internal class DownloadNotifier(private val context: Context) {
|
||||
addAction(
|
||||
R.drawable.ic_pause_24dp,
|
||||
context.getString(R.string.action_pause),
|
||||
NotificationReceiver.pauseDownloadsPendingBroadcast(context)
|
||||
NotificationReceiver.pauseDownloadsPendingBroadcast(context),
|
||||
)
|
||||
}
|
||||
|
||||
val downloadingProgressText = context.getString(
|
||||
R.string.chapter_downloading_progress,
|
||||
download.downloadedImages,
|
||||
download.pages!!.size
|
||||
download.pages!!.size,
|
||||
)
|
||||
|
||||
if (preferences.hideNotificationContent()) {
|
||||
@ -138,13 +139,13 @@ internal class DownloadNotifier(private val context: Context) {
|
||||
addAction(
|
||||
R.drawable.ic_play_arrow_24dp,
|
||||
context.getString(R.string.action_resume),
|
||||
NotificationReceiver.resumeDownloadsPendingBroadcast(context)
|
||||
NotificationReceiver.resumeDownloadsPendingBroadcast(context),
|
||||
)
|
||||
// Clear action
|
||||
addAction(
|
||||
R.drawable.ic_close_24dp,
|
||||
context.getString(R.string.action_cancel_all),
|
||||
NotificationReceiver.clearDownloadsPendingBroadcast(context)
|
||||
NotificationReceiver.clearDownloadsPendingBroadcast(context),
|
||||
)
|
||||
|
||||
show(Notifications.ID_DOWNLOAD_CHAPTER_PROGRESS)
|
||||
@ -184,8 +185,10 @@ internal class DownloadNotifier(private val context: Context) {
|
||||
* Called when the downloader receives a warning.
|
||||
*
|
||||
* @param reason the text to show.
|
||||
* @param timeout duration after which to automatically dismiss the notification.
|
||||
* Only works on Android 8+.
|
||||
*/
|
||||
fun onWarning(reason: String) {
|
||||
fun onWarning(reason: String, timeout: Long? = null, contentIntent: PendingIntent? = null) {
|
||||
with(errorNotificationBuilder) {
|
||||
setContentTitle(context.getString(R.string.download_notifier_downloader_title))
|
||||
setStyle(NotificationCompat.BigTextStyle().bigText(reason))
|
||||
@ -194,6 +197,8 @@ internal class DownloadNotifier(private val context: Context) {
|
||||
clearActions()
|
||||
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
||||
setProgress(0, 0, false)
|
||||
timeout?.let { setTimeoutAfter(it) }
|
||||
contentIntent?.let { setContentIntent(it) }
|
||||
|
||||
show(Notifications.ID_DOWNLOAD_CHAPTER_ERROR)
|
||||
}
|
||||
@ -213,7 +218,7 @@ internal class DownloadNotifier(private val context: Context) {
|
||||
// Create notification
|
||||
with(errorNotificationBuilder) {
|
||||
setContentTitle(
|
||||
mangaTitle?.plus(": $chapter") ?: context.getString(R.string.download_notifier_downloader_title)
|
||||
mangaTitle?.plus(": $chapter") ?: context.getString(R.string.download_notifier_downloader_title),
|
||||
)
|
||||
setContentText(error ?: context.getString(R.string.download_notifier_unknown_error))
|
||||
setSmallIcon(R.drawable.ic_warning_white_24dp)
|
||||
|
@ -126,7 +126,7 @@ class DownloadPendingDeleter(context: Context) {
|
||||
@Serializable
|
||||
private data class Entry(
|
||||
val chapters: List<ChapterEntry>,
|
||||
val manga: MangaEntry
|
||||
val manga: MangaEntry,
|
||||
)
|
||||
|
||||
/**
|
||||
@ -137,7 +137,7 @@ class DownloadPendingDeleter(context: Context) {
|
||||
val id: Long,
|
||||
val url: String,
|
||||
val name: String,
|
||||
val scanlator: String? = null
|
||||
val scanlator: String? = null,
|
||||
)
|
||||
|
||||
/**
|
||||
@ -148,7 +148,7 @@ class DownloadPendingDeleter(context: Context) {
|
||||
val id: Long,
|
||||
val url: String,
|
||||
val title: String,
|
||||
val source: Long
|
||||
val source: Long,
|
||||
)
|
||||
|
||||
/**
|
||||
|
@ -138,7 +138,7 @@ class DownloadProvider(private val context: Context) {
|
||||
when {
|
||||
chapter.scanlator != null -> "${chapter.scanlator}_${chapter.name}"
|
||||
else -> chapter.name
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@ -157,7 +157,7 @@ class DownloadProvider(private val context: Context) {
|
||||
"$chapterName.cbz",
|
||||
|
||||
// Legacy chapter directory name used in v0.9.2 and before
|
||||
DiskUtil.buildValidFilename(chapter.name)
|
||||
DiskUtil.buildValidFilename(chapter.name),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -177,7 +177,9 @@ class DownloadService : Service() {
|
||||
*/
|
||||
private fun listenDownloaderState() {
|
||||
subscriptions += downloadManager.runningRelay
|
||||
.doOnError { /* Swallow wakelock error */ }
|
||||
.doOnError {
|
||||
/* Swallow wakelock error */
|
||||
}
|
||||
.subscribe { running ->
|
||||
if (running) {
|
||||
wakeLock.acquireIfNeeded()
|
||||
|
@ -20,7 +20,7 @@ import uy.kohesive.injekt.injectLazy
|
||||
*/
|
||||
class DownloadStore(
|
||||
context: Context,
|
||||
private val sourceManager: SourceManager
|
||||
private val sourceManager: SourceManager,
|
||||
) {
|
||||
|
||||
/**
|
||||
|
@ -1,7 +1,6 @@
|
||||
package eu.kanade.tachiyomi.data.download
|
||||
|
||||
import android.content.Context
|
||||
import android.webkit.MimeTypeMap
|
||||
import com.hippo.unifile.UniFile
|
||||
import com.jakewharton.rxrelay.BehaviorRelay
|
||||
import com.jakewharton.rxrelay.PublishRelay
|
||||
@ -11,6 +10,8 @@ import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.download.model.Download
|
||||
import eu.kanade.tachiyomi.data.download.model.DownloadQueue
|
||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateNotifier
|
||||
import eu.kanade.tachiyomi.data.notification.NotificationHandler
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.UnmeteredSource
|
||||
@ -21,6 +22,7 @@ import eu.kanade.tachiyomi.util.lang.RetryWithDelay
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import eu.kanade.tachiyomi.util.lang.launchNow
|
||||
import eu.kanade.tachiyomi.util.lang.plusAssign
|
||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||
import eu.kanade.tachiyomi.util.storage.saveTo
|
||||
import eu.kanade.tachiyomi.util.system.ImageUtil
|
||||
@ -57,7 +59,7 @@ class Downloader(
|
||||
private val context: Context,
|
||||
private val provider: DownloadProvider,
|
||||
private val cache: DownloadCache,
|
||||
private val sourceManager: SourceManager
|
||||
private val sourceManager: SourceManager,
|
||||
) {
|
||||
|
||||
private val chapterCache: ChapterCache by injectLazy()
|
||||
@ -208,7 +210,7 @@ class Downloader(
|
||||
downloadChapter(download).subscribeOn(Schedulers.io())
|
||||
}
|
||||
},
|
||||
5
|
||||
5,
|
||||
)
|
||||
.onBackpressureLatest()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
@ -220,7 +222,7 @@ class Downloader(
|
||||
DownloadService.stop(context)
|
||||
logcat(LogPriority.ERROR, error)
|
||||
notifier.onError(error.message)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@ -271,15 +273,23 @@ class Downloader(
|
||||
|
||||
// Start downloader if needed
|
||||
if (autoStart && wasEmpty) {
|
||||
val queuedDownloads = queue.count { it.source !is UnmeteredSource }
|
||||
val maxDownloadsFromSource = queue
|
||||
.groupBy { it.source }
|
||||
.filterKeys { it !is UnmeteredSource }
|
||||
.maxOf { it.value.size }
|
||||
// TODO: re-enable warning
|
||||
if (maxDownloadsFromSource > CHAPTERS_PER_SOURCE_QUEUE_WARNING_THRESHOLD) {
|
||||
// withUIContext {
|
||||
// context.toast(R.string.download_queue_size_warning, Toast.LENGTH_LONG)
|
||||
// }
|
||||
.maxOfOrNull { it.value.size }
|
||||
?: 0
|
||||
if (
|
||||
queuedDownloads > DOWNLOADS_QUEUED_WARNING_THRESHOLD ||
|
||||
maxDownloadsFromSource > CHAPTERS_PER_SOURCE_QUEUE_WARNING_THRESHOLD
|
||||
) {
|
||||
withUIContext {
|
||||
notifier.onWarning(
|
||||
context.getString(R.string.download_queue_size_warning),
|
||||
WARNING_NOTIF_TIMEOUT_MS,
|
||||
NotificationHandler.openUrl(context, LibraryUpdateNotifier.HELP_WARNING_URL),
|
||||
)
|
||||
}
|
||||
}
|
||||
DownloadService.start(context)
|
||||
}
|
||||
@ -331,8 +341,8 @@ class Downloader(
|
||||
// Get all the URLs to the source images, fetch pages if necessary
|
||||
.flatMap { download.source.fetchAllImageUrlsFromPageList(it) }
|
||||
// Start downloading images, consider we can have downloaded images already
|
||||
// Concurrently do 5 pages at a time
|
||||
.flatMap({ page -> getOrDownloadImage(page, download, tmpDir) }, 5)
|
||||
// Concurrently do 2 pages at a time
|
||||
.flatMap({ page -> getOrDownloadImage(page, download, tmpDir).subscribeOn(Schedulers.io()) }, 2)
|
||||
.onBackpressureLatest()
|
||||
// Do when page is downloaded.
|
||||
.doOnNext { notifier.onProgressChange(download) }
|
||||
@ -342,6 +352,7 @@ class Downloader(
|
||||
.doOnNext { ensureSuccessfulDownload(download, mangaDir, tmpDir, chapterDirname) }
|
||||
// If the page list threw, it will resume here
|
||||
.onErrorReturn { error ->
|
||||
logcat(LogPriority.ERROR, error)
|
||||
download.status = Download.State.ERROR
|
||||
notifier.onError(error.message, download.chapter.name, download.manga.title)
|
||||
download
|
||||
@ -369,7 +380,7 @@ class Downloader(
|
||||
tmpFile?.delete()
|
||||
|
||||
// Try to find the image file.
|
||||
val imageFile = tmpDir.listFiles()!!.find { it.name!!.startsWith("$filename.") }
|
||||
val imageFile = tmpDir.listFiles()!!.find { it.name!!.startsWith("$filename.") || it.name!!.contains("${filename}__001") }
|
||||
|
||||
// If the image is already downloaded, do nothing. Otherwise download from network
|
||||
val pageObservable = when {
|
||||
@ -379,8 +390,12 @@ class Downloader(
|
||||
}
|
||||
|
||||
return pageObservable
|
||||
// When the image is ready, set image path, progress (just in case) and status
|
||||
// When the page is ready, set page path, progress (just in case) and status
|
||||
.doOnNext { file ->
|
||||
val success = splitTallImageIfNeeded(page, tmpDir)
|
||||
if (success.not()) {
|
||||
notifier.onError(context.getString(R.string.download_notifier_split_failed), download.chapter.name, download.manga.title)
|
||||
}
|
||||
page.uri = file.uri
|
||||
page.progress = 100
|
||||
download.downloadedImages++
|
||||
@ -391,6 +406,7 @@ class Downloader(
|
||||
.onErrorReturn {
|
||||
page.progress = 0
|
||||
page.status = Page.ERROR
|
||||
notifier.onError(it.message, download.chapter.name, download.manga.title)
|
||||
page
|
||||
}
|
||||
}
|
||||
@ -455,13 +471,33 @@ class Downloader(
|
||||
*/
|
||||
private fun getImageExtension(response: Response, file: UniFile): String {
|
||||
// Read content type if available.
|
||||
val mime = response.body?.contentType()?.let { ct -> "${ct.type}/${ct.subtype}" }
|
||||
val mime = response.body?.contentType()?.run { if (type == "image") "image/$subtype" else null }
|
||||
// Else guess from the uri.
|
||||
?: context.contentResolver.getType(file.uri)
|
||||
// Else read magic numbers.
|
||||
?: ImageUtil.findImageType { file.openInputStream() }?.mime
|
||||
|
||||
return MimeTypeMap.getSingleton().getExtensionFromMimeType(mime) ?: "jpg"
|
||||
return ImageUtil.getExtensionFromMimeType(mime)
|
||||
}
|
||||
|
||||
private fun splitTallImageIfNeeded(page: Page, tmpDir: UniFile): Boolean {
|
||||
if (!preferences.splitTallImages().get()) return true
|
||||
|
||||
val filename = String.format("%03d", page.number)
|
||||
val imageFile = tmpDir.listFiles()?.find { it.name!!.startsWith(filename) }
|
||||
?: throw Error(context.getString(R.string.download_notifier_split_page_not_found, page.number))
|
||||
val imageFilePath = imageFile.filePath
|
||||
?: throw Error(context.getString(R.string.download_notifier_split_page_path_not_found, page.number))
|
||||
|
||||
// check if the original page was previously splitted before then skip.
|
||||
if (imageFile.name!!.contains("__")) return true
|
||||
|
||||
return try {
|
||||
ImageUtil.splitTallImage(imageFile, imageFilePath)
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR, e)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -476,19 +512,13 @@ class Downloader(
|
||||
download: Download,
|
||||
mangaDir: UniFile,
|
||||
tmpDir: UniFile,
|
||||
dirname: String
|
||||
dirname: String,
|
||||
) {
|
||||
// Ensure that the chapter folder has all the images.
|
||||
val downloadedImages = tmpDir.listFiles().orEmpty().filterNot { it.name!!.endsWith(".tmp") }
|
||||
val downloadedImages = tmpDir.listFiles().orEmpty().filterNot { it.name!!.endsWith(".tmp") || (it.name!!.contains("__") && !it.name!!.contains("__001.jpg")) }
|
||||
|
||||
download.status = if (downloadedImages.size == download.pages!!.size) {
|
||||
Download.State.DOWNLOADED
|
||||
} else {
|
||||
Download.State.ERROR
|
||||
}
|
||||
|
||||
// Only rename the directory if it's downloaded.
|
||||
if (download.status == Download.State.DOWNLOADED) {
|
||||
// Only rename the directory if it's downloaded.
|
||||
if (preferences.saveChaptersAsCBZ().get()) {
|
||||
archiveChapter(mangaDir, dirname, tmpDir)
|
||||
} else {
|
||||
@ -497,6 +527,10 @@ class Downloader(
|
||||
cache.addChapter(dirname, mangaDir, download.manga)
|
||||
|
||||
DiskUtil.createNoMediaFile(tmpDir, context)
|
||||
|
||||
Download.State.DOWNLOADED
|
||||
} else {
|
||||
Download.State.ERROR
|
||||
}
|
||||
}
|
||||
|
||||
@ -557,9 +591,11 @@ class Downloader(
|
||||
|
||||
companion object {
|
||||
const val TMP_DIR_SUFFIX = "_tmp"
|
||||
const val WARNING_NOTIF_TIMEOUT_MS = 30_000L
|
||||
const val CHAPTERS_PER_SOURCE_QUEUE_WARNING_THRESHOLD = 15
|
||||
private const val DOWNLOADS_QUEUED_WARNING_THRESHOLD = 30
|
||||
}
|
||||
}
|
||||
|
||||
// Arbitrary minimum required space to start a download: 50 MB
|
||||
private const val MIN_DISK_SPACE = 50 * 1024 * 1024
|
||||
// Arbitrary minimum required space to start a download: 200 MB
|
||||
private const val MIN_DISK_SPACE = 200L * 1024 * 1024
|
||||
|
@ -11,7 +11,7 @@ import java.util.concurrent.CopyOnWriteArrayList
|
||||
|
||||
class DownloadQueue(
|
||||
private val store: DownloadStore,
|
||||
private val queue: MutableList<Download> = CopyOnWriteArrayList()
|
||||
private val queue: MutableList<Download> = CopyOnWriteArrayList(),
|
||||
) : List<Download> by queue {
|
||||
|
||||
private val statusSubject = PublishSubject.create<Download>()
|
||||
|
@ -8,9 +8,7 @@ import androidx.work.PeriodicWorkRequestBuilder
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.Worker
|
||||
import androidx.work.WorkerParameters
|
||||
import eu.kanade.tachiyomi.data.preference.DEVICE_CHARGING
|
||||
import eu.kanade.tachiyomi.data.preference.DEVICE_ONLY_ON_WIFI
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.preference.*
|
||||
import eu.kanade.tachiyomi.util.system.isConnectedToWifi
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
@ -21,8 +19,9 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
||||
|
||||
override fun doWork(): Result {
|
||||
val preferences = Injekt.get<PreferencesHelper>()
|
||||
if (requiresWifiConnection(preferences) && !context.isConnectedToWifi()) {
|
||||
Result.failure()
|
||||
val restrictions = preferences.libraryUpdateDeviceRestriction().get()
|
||||
if ((DEVICE_ONLY_ON_WIFI in restrictions) && !context.isConnectedToWifi()) {
|
||||
return Result.failure()
|
||||
}
|
||||
|
||||
return if (LibraryUpdateService.start(context)) {
|
||||
@ -41,15 +40,16 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
||||
if (interval > 0) {
|
||||
val restrictions = preferences.libraryUpdateDeviceRestriction().get()
|
||||
val constraints = Constraints.Builder()
|
||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
.setRequiredNetworkType(if (DEVICE_NETWORK_NOT_METERED in restrictions) { NetworkType.UNMETERED } else { NetworkType.CONNECTED })
|
||||
.setRequiresCharging(DEVICE_CHARGING in restrictions)
|
||||
.setRequiresBatteryNotLow(DEVICE_BATTERY_NOT_LOW in restrictions)
|
||||
.build()
|
||||
|
||||
val request = PeriodicWorkRequestBuilder<LibraryUpdateJob>(
|
||||
interval.toLong(),
|
||||
TimeUnit.HOURS,
|
||||
10,
|
||||
TimeUnit.MINUTES
|
||||
TimeUnit.MINUTES,
|
||||
)
|
||||
.addTag(TAG)
|
||||
.setConstraints(constraints)
|
||||
@ -60,10 +60,5 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
||||
WorkManager.getInstance(context).cancelAllWorkByTag(TAG)
|
||||
}
|
||||
}
|
||||
|
||||
fun requiresWifiConnection(preferences: PreferencesHelper): Boolean {
|
||||
val restrictions = preferences.libraryUpdateDeviceRestriction().get()
|
||||
return DEVICE_ONLY_ON_WIFI in restrictions
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -17,6 +17,7 @@ import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.download.Downloader
|
||||
import eu.kanade.tachiyomi.data.notification.NotificationHandler
|
||||
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
@ -85,31 +86,68 @@ class LibraryUpdateNotifier(private val context: Context) {
|
||||
Notifications.ID_LIBRARY_PROGRESS,
|
||||
progressNotificationBuilder
|
||||
.setProgress(total, current, false)
|
||||
.build()
|
||||
.build(),
|
||||
)
|
||||
}
|
||||
|
||||
fun showQueueSizeWarningNotification() {
|
||||
val notificationBuilder = context.notificationBuilder(Notifications.CHANNEL_LIBRARY_PROGRESS) {
|
||||
setContentTitle(context.getString(R.string.label_warning))
|
||||
setStyle(NotificationCompat.BigTextStyle().bigText(context.getString(R.string.notification_size_warning)))
|
||||
setSmallIcon(R.drawable.ic_warning_white_24dp)
|
||||
setTimeoutAfter(Downloader.WARNING_NOTIF_TIMEOUT_MS)
|
||||
setContentIntent(NotificationHandler.openUrl(context, HELP_WARNING_URL))
|
||||
}
|
||||
|
||||
context.notificationManager.notify(
|
||||
Notifications.ID_LIBRARY_SIZE_WARNING,
|
||||
notificationBuilder.build(),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows notification containing update entries that failed with action to open full log.
|
||||
*
|
||||
* @param errors List of entry titles that failed to update.
|
||||
* @param failed Number of entries that failed to update.
|
||||
* @param uri Uri for error log file containing all titles that failed.
|
||||
*/
|
||||
fun showUpdateErrorNotification(errors: List<String>, uri: Uri) {
|
||||
if (errors.isEmpty()) {
|
||||
fun showUpdateErrorNotification(failed: Int, uri: Uri) {
|
||||
if (failed == 0) {
|
||||
return
|
||||
}
|
||||
|
||||
context.notificationManager.notify(
|
||||
Notifications.ID_LIBRARY_ERROR,
|
||||
context.notificationBuilder(Notifications.CHANNEL_LIBRARY_ERROR) {
|
||||
setContentTitle(context.resources.getQuantityString(R.plurals.notification_update_error, errors.size, errors.size))
|
||||
setContentTitle(context.resources.getString(R.string.notification_update_error, failed))
|
||||
setContentText(context.getString(R.string.action_show_errors))
|
||||
setSmallIcon(R.drawable.ic_tachi)
|
||||
|
||||
setContentIntent(NotificationReceiver.openErrorLogPendingActivity(context, uri))
|
||||
}
|
||||
.build()
|
||||
.build(),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows notification containing update entries that were skipped.
|
||||
*
|
||||
* @param skipped Number of entries that were skipped during the update.
|
||||
*/
|
||||
fun showUpdateSkippedNotification(skipped: Int) {
|
||||
if (skipped == 0) {
|
||||
return
|
||||
}
|
||||
|
||||
context.notificationManager.notify(
|
||||
Notifications.ID_LIBRARY_SKIPPED,
|
||||
context.notificationBuilder(Notifications.CHANNEL_LIBRARY_SKIPPED) {
|
||||
setContentTitle(context.resources.getString(R.string.notification_update_skipped, skipped))
|
||||
setContentText(context.getString(R.string.learn_more))
|
||||
setSmallIcon(R.drawable.ic_tachi)
|
||||
setContentIntent(NotificationHandler.openUrl(context, HELP_SKIPPED_URL))
|
||||
}
|
||||
.build(),
|
||||
)
|
||||
}
|
||||
|
||||
@ -139,8 +177,8 @@ class LibraryUpdateNotifier(private val context: Context) {
|
||||
NotificationCompat.BigTextStyle().bigText(
|
||||
updates.joinToString("\n") {
|
||||
it.first.title.chop(NOTIF_TITLE_MAX_LEN)
|
||||
}
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -155,7 +193,7 @@ class LibraryUpdateNotifier(private val context: Context) {
|
||||
|
||||
setContentIntent(getNotificationIntent())
|
||||
setAutoCancel(true)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
// Per-manga notification
|
||||
@ -200,8 +238,8 @@ class LibraryUpdateNotifier(private val context: Context) {
|
||||
context,
|
||||
manga,
|
||||
chapters,
|
||||
Notifications.ID_NEW_CHAPTERS
|
||||
)
|
||||
Notifications.ID_NEW_CHAPTERS,
|
||||
),
|
||||
)
|
||||
// View chapters action
|
||||
addAction(
|
||||
@ -210,8 +248,8 @@ class LibraryUpdateNotifier(private val context: Context) {
|
||||
NotificationReceiver.openChapterPendingActivity(
|
||||
context,
|
||||
manga,
|
||||
Notifications.ID_NEW_CHAPTERS
|
||||
)
|
||||
Notifications.ID_NEW_CHAPTERS,
|
||||
),
|
||||
)
|
||||
// Download chapters action
|
||||
// Only add the action when chapters is within threshold
|
||||
@ -223,8 +261,8 @@ class LibraryUpdateNotifier(private val context: Context) {
|
||||
context,
|
||||
manga,
|
||||
chapters,
|
||||
Notifications.ID_NEW_CHAPTERS
|
||||
)
|
||||
Notifications.ID_NEW_CHAPTERS,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -251,7 +289,7 @@ class LibraryUpdateNotifier(private val context: Context) {
|
||||
val formatter = DecimalFormat(
|
||||
"#.###",
|
||||
DecimalFormatSymbols()
|
||||
.apply { decimalSeparator = '.' }
|
||||
.apply { decimalSeparator = '.' },
|
||||
)
|
||||
|
||||
val displayableChapterNumbers = chapters
|
||||
@ -305,8 +343,11 @@ class LibraryUpdateNotifier(private val context: Context) {
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val NOTIF_MAX_CHAPTERS = 5
|
||||
private const val NOTIF_TITLE_MAX_LEN = 45
|
||||
private const val NOTIF_ICON_SIZE = 192
|
||||
const val HELP_WARNING_URL = "https://tachiyomi.org/help/faq/#why-does-the-app-warn-about-large-bulk-updates-and-downloads"
|
||||
}
|
||||
}
|
||||
|
||||
private const val NOTIF_MAX_CHAPTERS = 5
|
||||
private const val NOTIF_TITLE_MAX_LEN = 45
|
||||
private const val NOTIF_ICON_SIZE = 192
|
||||
private const val HELP_SKIPPED_URL = "https://tachiyomi.org/help/faq/#why-does-global-update-skip-some-entries"
|
||||
|
@ -18,8 +18,9 @@ import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||
import eu.kanade.tachiyomi.data.download.DownloadService
|
||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Companion.start
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import eu.kanade.tachiyomi.data.preference.MANGA_FULLY_READ
|
||||
import eu.kanade.tachiyomi.data.preference.MANGA_ONGOING
|
||||
import eu.kanade.tachiyomi.data.preference.MANGA_HAS_UNREAD
|
||||
import eu.kanade.tachiyomi.data.preference.MANGA_NON_COMPLETED
|
||||
import eu.kanade.tachiyomi.data.preference.MANGA_NON_READ
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.track.EnhancedTrackService
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
@ -74,7 +75,7 @@ class LibraryUpdateService(
|
||||
val preferences: PreferencesHelper = Injekt.get(),
|
||||
val downloadManager: DownloadManager = Injekt.get(),
|
||||
val trackManager: TrackManager = Injekt.get(),
|
||||
val coverCache: CoverCache = Injekt.get()
|
||||
val coverCache: CoverCache = Injekt.get(),
|
||||
) : Service() {
|
||||
|
||||
private lateinit var wakeLock: PowerManager.WakeLock
|
||||
@ -138,7 +139,7 @@ class LibraryUpdateService(
|
||||
|
||||
true
|
||||
} else {
|
||||
instance?.addMangaToQueue(category?.id ?: -1, target)
|
||||
instance?.addMangaToQueue(category?.id ?: -1)
|
||||
false
|
||||
}
|
||||
}
|
||||
@ -173,6 +174,8 @@ class LibraryUpdateService(
|
||||
*/
|
||||
override fun onDestroy() {
|
||||
updateJob?.cancel()
|
||||
// Despite what Android Studio
|
||||
// states this can be null
|
||||
ioScope?.cancel()
|
||||
if (wakeLock.isHeld) {
|
||||
wakeLock.release()
|
||||
@ -210,7 +213,7 @@ class LibraryUpdateService(
|
||||
|
||||
// Update favorite manga
|
||||
val categoryId = intent.getIntExtra(KEY_CATEGORY, -1)
|
||||
addMangaToQueue(categoryId, target)
|
||||
addMangaToQueue(categoryId)
|
||||
|
||||
// Destroy service when completed or in case of an error.
|
||||
val handler = CoroutineExceptionHandler { _, exception ->
|
||||
@ -232,13 +235,12 @@ class LibraryUpdateService(
|
||||
/**
|
||||
* Adds list of manga to be updated.
|
||||
*
|
||||
* @param category the ID of the category to update, or -1 if no category specified.
|
||||
* @param target the target to update.
|
||||
* @param categoryId the ID of the category to update, or -1 if no category specified.
|
||||
*/
|
||||
fun addMangaToQueue(categoryId: Int, target: Target) {
|
||||
fun addMangaToQueue(categoryId: Int) {
|
||||
val libraryManga = db.getLibraryMangas().executeAsBlocking()
|
||||
|
||||
var listToUpdate = if (categoryId != -1) {
|
||||
val listToUpdate = if (categoryId != -1) {
|
||||
libraryManga.filter { it.category == categoryId }
|
||||
} else {
|
||||
val categoriesToUpdate = preferences.libraryUpdateCategories().get().map(String::toInt)
|
||||
@ -258,16 +260,6 @@ class LibraryUpdateService(
|
||||
listToInclude.minus(listToExclude)
|
||||
}
|
||||
|
||||
if (target == Target.CHAPTERS) {
|
||||
val restrictions = preferences.libraryUpdateMangaRestriction().get()
|
||||
if (MANGA_ONGOING in restrictions) {
|
||||
listToUpdate = listToUpdate.filterNot { it.status == SManga.COMPLETED }
|
||||
}
|
||||
if (MANGA_FULLY_READ in restrictions) {
|
||||
listToUpdate = listToUpdate.filter { it.unread == 0 }
|
||||
}
|
||||
}
|
||||
|
||||
mangaToUpdate = listToUpdate
|
||||
.distinctBy { it.id }
|
||||
.sortedBy { it.title }
|
||||
@ -277,19 +269,17 @@ class LibraryUpdateService(
|
||||
.groupBy { it.source }
|
||||
.filterKeys { sourceManager.get(it) !is UnmeteredSource }
|
||||
.maxOfOrNull { it.value.size } ?: 0
|
||||
// TODO: re-enable warning
|
||||
if (maxUpdatesFromSource > MANGA_PER_SOURCE_QUEUE_WARNING_THRESHOLD) {
|
||||
// toast(R.string.notification_size_warning, Toast.LENGTH_LONG)
|
||||
notifier.showQueueSizeWarningNotification()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method that updates the given list of manga. It's called in a background thread, so it's safe
|
||||
* Method that updates manga in [mangaToUpdate]. It's called in a background thread, so it's safe
|
||||
* to do heavy operations or network calls here.
|
||||
* For each manga it calls [updateManga] and updates the notification showing the current
|
||||
* progress.
|
||||
*
|
||||
* @param mangaToUpdate the list to update
|
||||
* @return an observable delivering the progress of each update.
|
||||
*/
|
||||
suspend fun updateChapterList() {
|
||||
@ -297,10 +287,12 @@ class LibraryUpdateService(
|
||||
val progressCount = AtomicInteger(0)
|
||||
val currentlyUpdatingManga = CopyOnWriteArrayList<LibraryManga>()
|
||||
val newUpdates = CopyOnWriteArrayList<Pair<LibraryManga, Array<Chapter>>>()
|
||||
val skippedUpdates = CopyOnWriteArrayList<Pair<Manga, String?>>()
|
||||
val failedUpdates = CopyOnWriteArrayList<Pair<Manga, String?>>()
|
||||
val hasDownloads = AtomicBoolean(false)
|
||||
val loggedServices by lazy { trackManager.services.filter { it.isLogged } }
|
||||
val currentUnreadUpdatesCount = preferences.unreadUpdatesCount().get()
|
||||
val restrictions = preferences.libraryUpdateMangaRestriction().get()
|
||||
|
||||
withIOContext {
|
||||
mangaToUpdate.groupBy { it.source }
|
||||
@ -313,25 +305,42 @@ class LibraryUpdateService(
|
||||
return@async
|
||||
}
|
||||
|
||||
// Don't continue to update if manga not in library
|
||||
db.getManga(manga.id!!).executeAsBlocking() ?: return@forEach
|
||||
|
||||
withUpdateNotification(
|
||||
currentlyUpdatingManga,
|
||||
progressCount,
|
||||
manga,
|
||||
) { manga ->
|
||||
) { mangaWithNotif ->
|
||||
try {
|
||||
val (newChapters, _) = updateManga(manga)
|
||||
when {
|
||||
MANGA_NON_COMPLETED in restrictions && mangaWithNotif.status == SManga.COMPLETED ->
|
||||
skippedUpdates.add(mangaWithNotif to getString(R.string.skipped_reason_completed))
|
||||
|
||||
if (newChapters.isNotEmpty()) {
|
||||
if (manga.shouldDownloadNewChapters(db, preferences)) {
|
||||
downloadChapters(manga, newChapters)
|
||||
hasDownloads.set(true)
|
||||
MANGA_HAS_UNREAD in restrictions && mangaWithNotif.unreadCount != 0 ->
|
||||
skippedUpdates.add(mangaWithNotif to getString(R.string.skipped_reason_not_caught_up))
|
||||
|
||||
MANGA_NON_READ in restrictions && mangaWithNotif.totalChapters > 0 && !mangaWithNotif.hasStarted ->
|
||||
skippedUpdates.add(mangaWithNotif to getString(R.string.skipped_reason_not_started))
|
||||
|
||||
else -> {
|
||||
// Convert to the manga that contains new chapters
|
||||
val (newChapters, _) = updateManga(mangaWithNotif)
|
||||
|
||||
if (newChapters.isNotEmpty()) {
|
||||
if (mangaWithNotif.shouldDownloadNewChapters(db, preferences)) {
|
||||
downloadChapters(mangaWithNotif, newChapters)
|
||||
hasDownloads.set(true)
|
||||
}
|
||||
|
||||
// Convert to the manga that contains new chapters
|
||||
newUpdates.add(
|
||||
mangaWithNotif to newChapters.sortedByDescending { ch -> ch.source_order }
|
||||
.toTypedArray(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Convert to the manga that contains new chapters
|
||||
newUpdates.add(
|
||||
manga to newChapters.sortedByDescending { ch -> ch.source_order }
|
||||
.toTypedArray()
|
||||
)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
val errorMessage = when (e) {
|
||||
@ -346,11 +355,11 @@ class LibraryUpdateService(
|
||||
e.message
|
||||
}
|
||||
}
|
||||
failedUpdates.add(manga to errorMessage)
|
||||
failedUpdates.add(mangaWithNotif to errorMessage)
|
||||
}
|
||||
|
||||
if (preferences.autoUpdateTrackers()) {
|
||||
updateTrackings(manga, loggedServices)
|
||||
updateTrackings(mangaWithNotif, loggedServices)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -374,10 +383,13 @@ class LibraryUpdateService(
|
||||
if (failedUpdates.isNotEmpty()) {
|
||||
val errorFile = writeErrorFile(failedUpdates)
|
||||
notifier.showUpdateErrorNotification(
|
||||
failedUpdates.map { it.first.title },
|
||||
errorFile.getUriCompat(this)
|
||||
failedUpdates.size,
|
||||
errorFile.getUriCompat(this),
|
||||
)
|
||||
}
|
||||
if (skippedUpdates.isNotEmpty()) {
|
||||
notifier.showUpdateSkippedNotification(skippedUpdates.size)
|
||||
}
|
||||
}
|
||||
|
||||
private fun downloadChapters(manga: Manga, chapters: List<Chapter>) {
|
||||
@ -395,6 +407,7 @@ class LibraryUpdateService(
|
||||
suspend fun updateManga(manga: Manga): Pair<List<Chapter>, List<Chapter>> {
|
||||
val source = sourceManager.getOrStub(manga.source)
|
||||
|
||||
var networkSManga: SManga? = null
|
||||
// Update manga details metadata
|
||||
if (preferences.autoUpdateMetadata()) {
|
||||
val updatedManga = source.getMangaDetails(manga.toMangaInfo())
|
||||
@ -406,14 +419,26 @@ class LibraryUpdateService(
|
||||
sManga.thumbnail_url = manga.thumbnail_url
|
||||
}
|
||||
|
||||
manga.copyFrom(sManga)
|
||||
db.insertManga(manga).executeAsBlocking()
|
||||
networkSManga = sManga
|
||||
}
|
||||
|
||||
val chapters = source.getChapterList(manga.toMangaInfo())
|
||||
.map { it.toSChapter() }
|
||||
|
||||
return syncChaptersWithSource(db, chapters, manga, source)
|
||||
// Get manga from database to account for if it was removed
|
||||
// from library or database
|
||||
val dbManga = db.getManga(manga.id!!).executeAsBlocking()
|
||||
?: return Pair(emptyList(), emptyList())
|
||||
|
||||
// Copy into [dbManga] to retain favourite value
|
||||
networkSManga?.let {
|
||||
dbManga.copyFrom(it)
|
||||
db.insertManga(dbManga).executeAsBlocking()
|
||||
}
|
||||
|
||||
// [dbmanga] was used so that manga data doesn't get overwritten
|
||||
// incase manga gets new chapter
|
||||
return syncChaptersWithSource(db, chapters, dbManga, source)
|
||||
}
|
||||
|
||||
private suspend fun updateCovers() {
|
||||
@ -436,16 +461,16 @@ class LibraryUpdateService(
|
||||
currentlyUpdatingManga,
|
||||
progressCount,
|
||||
manga,
|
||||
) { manga ->
|
||||
sourceManager.get(manga.source)?.let { source ->
|
||||
) { mangaWithNotif ->
|
||||
sourceManager.get(mangaWithNotif.source)?.let { source ->
|
||||
try {
|
||||
val networkManga =
|
||||
source.getMangaDetails(manga.toMangaInfo())
|
||||
source.getMangaDetails(mangaWithNotif.toMangaInfo())
|
||||
val sManga = networkManga.toSManga()
|
||||
manga.prepUpdateCover(coverCache, sManga, true)
|
||||
mangaWithNotif.prepUpdateCover(coverCache, sManga, true)
|
||||
sManga.thumbnail_url?.let {
|
||||
manga.thumbnail_url = it
|
||||
db.insertManga(manga).executeAsBlocking()
|
||||
mangaWithNotif.thumbnail_url = it
|
||||
db.insertManga(mangaWithNotif).executeAsBlocking()
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
// Ignore errors and continue
|
||||
@ -525,7 +550,7 @@ class LibraryUpdateService(
|
||||
notifier.showProgressNotification(
|
||||
updatingManga,
|
||||
completed.get(),
|
||||
mangaToUpdate.size
|
||||
mangaToUpdate.size,
|
||||
)
|
||||
|
||||
block(manga)
|
||||
@ -539,7 +564,7 @@ class LibraryUpdateService(
|
||||
notifier.showProgressNotification(
|
||||
updatingManga,
|
||||
completed.get(),
|
||||
mangaToUpdate.size
|
||||
mangaToUpdate.size,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -6,8 +6,6 @@ import android.content.Intent
|
||||
import android.net.Uri
|
||||
import eu.kanade.tachiyomi.extension.util.ExtensionInstaller
|
||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* Class that manages [PendingIntent] of activity's
|
||||
@ -23,7 +21,7 @@ object NotificationHandler {
|
||||
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
|
||||
action = MainActivity.SHORTCUT_DOWNLOADS
|
||||
}
|
||||
return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -32,13 +30,12 @@ object NotificationHandler {
|
||||
* @param context context of application
|
||||
* @param file file containing image
|
||||
*/
|
||||
internal fun openImagePendingActivity(context: Context, file: File): PendingIntent {
|
||||
internal fun openImagePendingActivity(context: Context, uri: Uri): PendingIntent {
|
||||
val intent = Intent(Intent.ACTION_VIEW).apply {
|
||||
val uri = file.getUriCompat(context)
|
||||
setDataAndType(uri, "image/*")
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
}
|
||||
return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -52,6 +49,11 @@ object NotificationHandler {
|
||||
setDataAndType(uri, ExtensionInstaller.APK_MIME)
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
}
|
||||
return PendingIntent.getActivity(context, 0, intent, 0)
|
||||
return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE)
|
||||
}
|
||||
|
||||
fun openUrl(context: Context, url: String): PendingIntent {
|
||||
val notificationIntent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
|
||||
return PendingIntent.getActivity(context, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE)
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.net.toUri
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.backup.BackupRestoreService
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
@ -59,14 +60,14 @@ class NotificationReceiver : BroadcastReceiver() {
|
||||
shareImage(
|
||||
context,
|
||||
intent.getStringExtra(EXTRA_FILE_LOCATION)!!,
|
||||
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)
|
||||
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1),
|
||||
)
|
||||
// Delete image from path and dismiss notification
|
||||
ACTION_DELETE_IMAGE ->
|
||||
deleteImage(
|
||||
context,
|
||||
intent.getStringExtra(EXTRA_FILE_LOCATION)!!,
|
||||
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)
|
||||
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1),
|
||||
)
|
||||
// Share backup file
|
||||
ACTION_SHARE_BACKUP ->
|
||||
@ -74,11 +75,11 @@ class NotificationReceiver : BroadcastReceiver() {
|
||||
context,
|
||||
intent.getParcelableExtra(EXTRA_URI)!!,
|
||||
"application/x-protobuf+gzip",
|
||||
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)
|
||||
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1),
|
||||
)
|
||||
ACTION_CANCEL_RESTORE -> cancelRestore(
|
||||
context,
|
||||
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)
|
||||
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1),
|
||||
)
|
||||
// Cancel library update and dismiss notification
|
||||
ACTION_CANCEL_LIBRARY_UPDATE -> cancelLibraryUpdate(context, Notifications.ID_LIBRARY_PROGRESS)
|
||||
@ -87,7 +88,7 @@ class NotificationReceiver : BroadcastReceiver() {
|
||||
openChapter(
|
||||
context,
|
||||
intent.getLongExtra(EXTRA_MANGA_ID, -1),
|
||||
intent.getLongExtra(EXTRA_CHAPTER_ID, -1)
|
||||
intent.getLongExtra(EXTRA_CHAPTER_ID, -1),
|
||||
)
|
||||
}
|
||||
// Mark updated manga chapters as read
|
||||
@ -120,7 +121,7 @@ class NotificationReceiver : BroadcastReceiver() {
|
||||
context,
|
||||
intent.getParcelableExtra(EXTRA_URI)!!,
|
||||
"text/plain",
|
||||
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)
|
||||
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1),
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -193,7 +194,7 @@ class NotificationReceiver : BroadcastReceiver() {
|
||||
val file = File(path)
|
||||
file.delete()
|
||||
|
||||
DiskUtil.scanMedia(context, file)
|
||||
DiskUtil.scanMedia(context, file.toUri())
|
||||
}
|
||||
|
||||
/**
|
||||
@ -461,7 +462,7 @@ class NotificationReceiver : BroadcastReceiver() {
|
||||
context: Context,
|
||||
manga: Manga,
|
||||
chapters: Array<Chapter>,
|
||||
groupId: Int
|
||||
groupId: Int,
|
||||
): PendingIntent {
|
||||
val newIntent = Intent(context, NotificationReceiver::class.java).apply {
|
||||
action = ACTION_MARK_AS_READ
|
||||
@ -483,7 +484,7 @@ class NotificationReceiver : BroadcastReceiver() {
|
||||
context: Context,
|
||||
manga: Manga,
|
||||
chapters: Array<Chapter>,
|
||||
groupId: Int
|
||||
groupId: Int,
|
||||
): PendingIntent {
|
||||
val newIntent = Intent(context, NotificationReceiver::class.java).apply {
|
||||
action = ACTION_DOWNLOAD_CHAPTER
|
||||
|
@ -26,8 +26,11 @@ object Notifications {
|
||||
private const val GROUP_LIBRARY = "group_library"
|
||||
const val CHANNEL_LIBRARY_PROGRESS = "library_progress_channel"
|
||||
const val ID_LIBRARY_PROGRESS = -101
|
||||
const val ID_LIBRARY_SIZE_WARNING = -103
|
||||
const val CHANNEL_LIBRARY_ERROR = "library_errors_channel"
|
||||
const val ID_LIBRARY_ERROR = -102
|
||||
const val CHANNEL_LIBRARY_SKIPPED = "library_skipped_channel"
|
||||
const val ID_LIBRARY_SKIPPED = -104
|
||||
|
||||
/**
|
||||
* Notification channel and ids used by the downloader.
|
||||
@ -114,7 +117,7 @@ object Notifications {
|
||||
buildNotificationChannelGroup(GROUP_APK_UPDATES) {
|
||||
setName(context.getString(R.string.label_recent_updates))
|
||||
},
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
notificationService.createNotificationChannelsCompat(
|
||||
@ -132,6 +135,11 @@ object Notifications {
|
||||
setGroup(GROUP_LIBRARY)
|
||||
setShowBadge(false)
|
||||
},
|
||||
buildNotificationChannel(CHANNEL_LIBRARY_SKIPPED, IMPORTANCE_LOW) {
|
||||
setName(context.getString(R.string.channel_skipped))
|
||||
setGroup(GROUP_LIBRARY)
|
||||
setShowBadge(false)
|
||||
},
|
||||
buildNotificationChannel(CHANNEL_NEW_CHAPTERS, IMPORTANCE_DEFAULT) {
|
||||
setName(context.getString(R.string.channel_new_chapters))
|
||||
},
|
||||
@ -175,7 +183,7 @@ object Notifications {
|
||||
setGroup(GROUP_APK_UPDATES)
|
||||
setName(context.getString(R.string.channel_ext_updates))
|
||||
},
|
||||
)
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -31,6 +31,8 @@ object PreferenceKeys {
|
||||
|
||||
const val filterUnread = "pref_filter_library_unread"
|
||||
|
||||
const val filterStarted = "pref_filter_library_started"
|
||||
|
||||
const val filterCompleted = "pref_filter_library_completed"
|
||||
|
||||
const val filterTracked = "pref_filter_library_tracked"
|
||||
@ -61,6 +63,8 @@ object PreferenceKeys {
|
||||
|
||||
const val dohProvider = "doh_provider"
|
||||
|
||||
const val defaultUserAgent = "default_user_agent"
|
||||
|
||||
const val defaultChapterFilterByRead = "default_chapter_filter_by_read"
|
||||
|
||||
const val defaultChapterFilterByDownloaded = "default_chapter_filter_by_downloaded"
|
||||
|
@ -3,10 +3,13 @@ package eu.kanade.tachiyomi.data.preference
|
||||
import eu.kanade.tachiyomi.R
|
||||
|
||||
const val DEVICE_ONLY_ON_WIFI = "wifi"
|
||||
const val DEVICE_NETWORK_NOT_METERED = "network_not_metered"
|
||||
const val DEVICE_CHARGING = "ac"
|
||||
const val DEVICE_BATTERY_NOT_LOW = "battery_not_low"
|
||||
|
||||
const val MANGA_ONGOING = "manga_ongoing"
|
||||
const val MANGA_FULLY_READ = "manga_fully_read"
|
||||
const val MANGA_NON_COMPLETED = "manga_ongoing"
|
||||
const val MANGA_HAS_UNREAD = "manga_fully_read"
|
||||
const val MANGA_NON_READ = "manga_started"
|
||||
|
||||
/**
|
||||
* This class stores the values for the preferences in the application.
|
||||
@ -27,13 +30,14 @@ object PreferenceValues {
|
||||
enum class AppTheme(val titleResId: Int?) {
|
||||
DEFAULT(R.string.label_default),
|
||||
MONET(R.string.theme_monet),
|
||||
GREEN_APPLE(R.string.theme_greenapple),
|
||||
LAVENDER(R.string.theme_lavender),
|
||||
MIDNIGHT_DUSK(R.string.theme_midnightdusk),
|
||||
STRAWBERRY_DAIQUIRI(R.string.theme_strawberrydaiquiri),
|
||||
YOTSUBA(R.string.theme_yotsuba),
|
||||
TAKO(R.string.theme_tako),
|
||||
GREEN_APPLE(R.string.theme_greenapple),
|
||||
TEALTURQUOISE(R.string.theme_tealturquoise),
|
||||
YINYANG(R.string.theme_yinyang),
|
||||
YOTSUBA(R.string.theme_yotsuba),
|
||||
|
||||
// Deprecated
|
||||
DARK_BLUE(null),
|
||||
@ -55,16 +59,22 @@ object PreferenceValues {
|
||||
LOWEST(47),
|
||||
}
|
||||
|
||||
enum class TabletUiMode {
|
||||
AUTOMATIC,
|
||||
ALWAYS,
|
||||
LANDSCAPE,
|
||||
NEVER,
|
||||
enum class TabletUiMode(val titleResId: Int) {
|
||||
AUTOMATIC(R.string.automatic_background),
|
||||
ALWAYS(R.string.lock_always),
|
||||
LANDSCAPE(R.string.landscape),
|
||||
NEVER(R.string.lock_never),
|
||||
}
|
||||
|
||||
enum class ExtensionInstaller {
|
||||
LEGACY,
|
||||
PACKAGEINSTALLER,
|
||||
SHIZUKU,
|
||||
enum class ExtensionInstaller(val titleResId: Int) {
|
||||
LEGACY(R.string.ext_installer_legacy),
|
||||
PACKAGEINSTALLER(R.string.ext_installer_packageinstaller),
|
||||
SHIZUKU(R.string.ext_installer_shizuku),
|
||||
}
|
||||
|
||||
enum class SecureScreenMode(val titleResId: Int) {
|
||||
ALWAYS(R.string.lock_always),
|
||||
INCOGNITO(R.string.pref_incognito_mode),
|
||||
NEVER(R.string.lock_never),
|
||||
}
|
||||
}
|
||||
|
@ -18,6 +18,7 @@ import eu.kanade.tachiyomi.ui.library.setting.SortModeSetting
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
|
||||
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
||||
import eu.kanade.tachiyomi.util.system.isDevFlavor
|
||||
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
|
||||
import java.io.File
|
||||
import java.text.DateFormat
|
||||
@ -34,13 +35,13 @@ class PreferencesHelper(val context: Context) {
|
||||
private val defaultDownloadsDir = File(
|
||||
Environment.getExternalStorageDirectory().absolutePath + File.separator +
|
||||
context.getString(R.string.app_name),
|
||||
"downloads"
|
||||
"downloads",
|
||||
).toUri()
|
||||
|
||||
private val defaultBackupDir = File(
|
||||
Environment.getExternalStorageDirectory().absolutePath + File.separator +
|
||||
context.getString(R.string.app_name),
|
||||
"backup"
|
||||
"backup",
|
||||
).toUri()
|
||||
|
||||
fun startScreen() = prefs.getInt(Keys.startScreen, 1)
|
||||
@ -55,9 +56,9 @@ class PreferencesHelper(val context: Context) {
|
||||
|
||||
fun lockAppAfter() = flowPrefs.getInt("lock_app_after", 0)
|
||||
|
||||
fun lastAppUnlock() = flowPrefs.getLong("last_app_unlock", 0)
|
||||
fun lastAppClosed() = flowPrefs.getLong("last_app_closed", 0)
|
||||
|
||||
fun secureScreen() = flowPrefs.getBoolean("secure_screen", false)
|
||||
fun secureScreen() = flowPrefs.getEnum("secure_screen_v2", Values.SecureScreenMode.INCOGNITO)
|
||||
|
||||
fun hideNotificationContent() = prefs.getBoolean(Keys.hideNotificationContent, false)
|
||||
|
||||
@ -67,12 +68,12 @@ class PreferencesHelper(val context: Context) {
|
||||
|
||||
fun themeMode() = flowPrefs.getEnum(
|
||||
"pref_theme_mode_key",
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { Values.ThemeMode.system } else { Values.ThemeMode.light }
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { Values.ThemeMode.system } else { Values.ThemeMode.light },
|
||||
)
|
||||
|
||||
fun appTheme() = flowPrefs.getEnum(
|
||||
"pref_app_theme",
|
||||
if (DeviceUtil.isDynamicColorAvailable) { Values.AppTheme.MONET } else { Values.AppTheme.DEFAULT }
|
||||
if (DeviceUtil.isDynamicColorAvailable) { Values.AppTheme.MONET } else { Values.AppTheme.DEFAULT },
|
||||
)
|
||||
|
||||
fun themeDarkAmoled() = flowPrefs.getBoolean("pref_theme_dark_amoled_key", false)
|
||||
@ -129,12 +130,14 @@ class PreferencesHelper(val context: Context) {
|
||||
|
||||
fun cropBorders() = flowPrefs.getBoolean("crop_borders", false)
|
||||
|
||||
fun navigateToPan() = flowPrefs.getBoolean("navigate_pan", true)
|
||||
|
||||
fun landscapeZoom() = flowPrefs.getBoolean("landscape_zoom", true)
|
||||
|
||||
fun cropBordersWebtoon() = flowPrefs.getBoolean("crop_borders_webtoon", false)
|
||||
|
||||
fun webtoonSidePadding() = flowPrefs.getInt("webtoon_side_padding", 0)
|
||||
|
||||
fun readWithTapping() = flowPrefs.getBoolean("reader_tap", true)
|
||||
|
||||
fun pagerNavInverted() = flowPrefs.getEnum("reader_tapping_inverted", Values.TappingInvertMode.NONE)
|
||||
|
||||
fun webtoonNavInverted() = flowPrefs.getEnum("reader_tapping_inverted_webtoon", Values.TappingInvertMode.NONE)
|
||||
@ -201,11 +204,13 @@ class PreferencesHelper(val context: Context) {
|
||||
|
||||
fun downloadOnlyOverWifi() = prefs.getBoolean(Keys.downloadOnlyOverWifi, true)
|
||||
|
||||
fun saveChaptersAsCBZ() = flowPrefs.getBoolean("save_chapter_as_cbz", false)
|
||||
fun saveChaptersAsCBZ() = flowPrefs.getBoolean("save_chapter_as_cbz", true)
|
||||
|
||||
fun splitTallImages() = flowPrefs.getBoolean("split_tall_images", false)
|
||||
|
||||
fun folderPerManga() = prefs.getBoolean(Keys.folderPerManga, false)
|
||||
|
||||
fun numberOfBackups() = flowPrefs.getInt("backup_slots", 1)
|
||||
fun numberOfBackups() = flowPrefs.getInt("backup_slots", 2)
|
||||
|
||||
fun backupInterval() = flowPrefs.getInt("backup_interval", 0)
|
||||
|
||||
@ -220,7 +225,7 @@ class PreferencesHelper(val context: Context) {
|
||||
fun libraryUpdateInterval() = flowPrefs.getInt("pref_library_update_interval_key", 24)
|
||||
|
||||
fun libraryUpdateDeviceRestriction() = flowPrefs.getStringSet("library_update_restriction", setOf(DEVICE_ONLY_ON_WIFI))
|
||||
fun libraryUpdateMangaRestriction() = flowPrefs.getStringSet("library_update_manga_restriction", setOf(MANGA_FULLY_READ, MANGA_ONGOING))
|
||||
fun libraryUpdateMangaRestriction() = flowPrefs.getStringSet("library_update_manga_restriction", setOf(MANGA_HAS_UNREAD, MANGA_NON_COMPLETED, MANGA_NON_READ))
|
||||
|
||||
fun showUpdatesNavBadge() = flowPrefs.getBoolean("library_update_show_tab_badge", false)
|
||||
fun unreadUpdatesCount() = flowPrefs.getInt("library_unread_updates_count", 0)
|
||||
@ -248,6 +253,8 @@ class PreferencesHelper(val context: Context) {
|
||||
|
||||
fun filterUnread() = flowPrefs.getInt(Keys.filterUnread, ExtendedNavigationView.Item.TriStateGroup.State.IGNORE.value)
|
||||
|
||||
fun filterStarted() = flowPrefs.getInt(Keys.filterStarted, ExtendedNavigationView.Item.TriStateGroup.State.IGNORE.value)
|
||||
|
||||
fun filterCompleted() = flowPrefs.getInt(Keys.filterCompleted, ExtendedNavigationView.Item.TriStateGroup.State.IGNORE.value)
|
||||
|
||||
fun filterTracking(name: Int) = flowPrefs.getInt("${Keys.filterTracked}_$name", ExtendedNavigationView.Item.TriStateGroup.State.IGNORE.value)
|
||||
@ -273,10 +280,10 @@ class PreferencesHelper(val context: Context) {
|
||||
|
||||
fun pinnedSources() = flowPrefs.getStringSet("pinned_catalogues", emptySet())
|
||||
|
||||
fun downloadNew() = flowPrefs.getBoolean("download_new", false)
|
||||
fun downloadNewChapter() = flowPrefs.getBoolean("download_new", false)
|
||||
|
||||
fun downloadNewCategories() = flowPrefs.getStringSet("download_new_categories", emptySet())
|
||||
fun downloadNewCategoriesExclude() = flowPrefs.getStringSet("download_new_categories_exclude", emptySet())
|
||||
fun downloadNewChapterCategories() = flowPrefs.getStringSet("download_new_categories", emptySet())
|
||||
fun downloadNewChapterCategoriesExclude() = flowPrefs.getStringSet("download_new_categories_exclude", emptySet())
|
||||
|
||||
fun defaultCategory() = prefs.getInt(Keys.defaultCategory, -1)
|
||||
|
||||
@ -292,6 +299,8 @@ class PreferencesHelper(val context: Context) {
|
||||
|
||||
fun dohProvider() = prefs.getInt(Keys.dohProvider, -1)
|
||||
|
||||
fun defaultUserAgent() = flowPrefs.getString(Keys.defaultUserAgent, "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.5005.124 Safari/537.36 Edg/102.0.1245.44")
|
||||
|
||||
fun lastSearchQuerySearchSettings() = flowPrefs.getString("last_search_query", "")
|
||||
|
||||
fun filterChapterByRead() = prefs.getInt(Keys.defaultChapterFilterByRead, Manga.SHOW_ALL)
|
||||
@ -312,10 +321,10 @@ class PreferencesHelper(val context: Context) {
|
||||
|
||||
fun extensionInstaller() = flowPrefs.getEnum(
|
||||
"extension_installer",
|
||||
if (DeviceUtil.isMiui) Values.ExtensionInstaller.LEGACY else Values.ExtensionInstaller.PACKAGEINSTALLER
|
||||
if (DeviceUtil.isMiui) Values.ExtensionInstaller.LEGACY else Values.ExtensionInstaller.PACKAGEINSTALLER,
|
||||
)
|
||||
|
||||
fun verboseLogging() = prefs.getBoolean(Keys.verboseLogging, false)
|
||||
fun verboseLogging() = prefs.getBoolean(Keys.verboseLogging, isDevFlavor)
|
||||
|
||||
fun autoClearChapterCache() = prefs.getBoolean(Keys.autoClearChapterCache, false)
|
||||
|
||||
|
152
app/src/main/java/eu/kanade/tachiyomi/data/saver/ImageSaver.kt
Normal file
152
app/src/main/java/eu/kanade/tachiyomi/data/saver/ImageSaver.kt
Normal file
@ -0,0 +1,152 @@
|
||||
package eu.kanade.tachiyomi.data.saver
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.provider.MediaStore
|
||||
import androidx.core.net.toUri
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||
import eu.kanade.tachiyomi.util.storage.cacheImageDir
|
||||
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
||||
import eu.kanade.tachiyomi.util.system.ImageUtil
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import logcat.LogPriority
|
||||
import okio.IOException
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
|
||||
class ImageSaver(
|
||||
val context: Context,
|
||||
) {
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
fun save(image: Image): Uri {
|
||||
val data = image.data
|
||||
|
||||
val type = ImageUtil.findImageType(data) ?: throw Exception("Not an image")
|
||||
val filename = DiskUtil.buildValidFilename("${image.name}.${type.extension}")
|
||||
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q || image.location !is Location.Pictures) {
|
||||
return save(data(), image.location.directory(context), filename)
|
||||
}
|
||||
|
||||
val pictureDir =
|
||||
MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
|
||||
|
||||
val contentValues = ContentValues().apply {
|
||||
put(MediaStore.Images.Media.DISPLAY_NAME, image.name)
|
||||
put(MediaStore.Images.Media.MIME_TYPE, type.mime)
|
||||
put(
|
||||
MediaStore.Images.Media.RELATIVE_PATH,
|
||||
"${Environment.DIRECTORY_PICTURES}/${context.getString(R.string.app_name)}/" +
|
||||
(image.location as Location.Pictures).relativePath,
|
||||
)
|
||||
}
|
||||
|
||||
val picture = context.contentResolver.insert(
|
||||
pictureDir,
|
||||
contentValues,
|
||||
) ?: throw IOException(context.getString(R.string.error_saving_picture))
|
||||
|
||||
try {
|
||||
data().use { input ->
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
context.contentResolver.openOutputStream(picture, "w").use { output ->
|
||||
input.copyTo(output!!)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR, e)
|
||||
throw IOException(context.getString(R.string.error_saving_picture))
|
||||
}
|
||||
|
||||
DiskUtil.scanMedia(context, picture)
|
||||
|
||||
return picture
|
||||
}
|
||||
|
||||
private fun save(inputStream: InputStream, directory: File, filename: String): Uri {
|
||||
directory.mkdirs()
|
||||
|
||||
val destFile = File(directory, filename)
|
||||
|
||||
inputStream.use { input ->
|
||||
destFile.outputStream().use { output ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
}
|
||||
|
||||
DiskUtil.scanMedia(context, destFile.toUri())
|
||||
|
||||
return destFile.getUriCompat(context)
|
||||
}
|
||||
}
|
||||
|
||||
sealed class Image(
|
||||
open val name: String,
|
||||
open val location: Location,
|
||||
) {
|
||||
data class Cover(
|
||||
val bitmap: Bitmap,
|
||||
override val name: String,
|
||||
override val location: Location,
|
||||
) : Image(name, location)
|
||||
|
||||
data class Page(
|
||||
val inputStream: () -> InputStream,
|
||||
override val name: String,
|
||||
override val location: Location,
|
||||
) : Image(name, location)
|
||||
|
||||
val data: () -> InputStream
|
||||
get() {
|
||||
return when (this) {
|
||||
is Cover -> {
|
||||
{
|
||||
val baos = ByteArrayOutputStream()
|
||||
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, baos)
|
||||
ByteArrayInputStream(baos.toByteArray())
|
||||
}
|
||||
}
|
||||
is Page -> inputStream
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed class Location {
|
||||
data class Pictures private constructor(val relativePath: String) : Location() {
|
||||
companion object {
|
||||
fun create(relativePath: String = ""): Pictures {
|
||||
return Pictures(relativePath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object Cache : Location()
|
||||
|
||||
fun directory(context: Context): File {
|
||||
return when (this) {
|
||||
Cache -> context.cacheImageDir
|
||||
is Pictures -> {
|
||||
val file = File(
|
||||
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES),
|
||||
context.getString(R.string.app_name),
|
||||
)
|
||||
if (relativePath.isNotEmpty()) {
|
||||
return File(
|
||||
file,
|
||||
relativePath,
|
||||
)
|
||||
}
|
||||
file
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -43,7 +43,8 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
| status
|
||||
|}
|
||||
|}
|
||||
|""".trimMargin()
|
||||
|
|
||||
""".trimMargin()
|
||||
val payload = buildJsonObject {
|
||||
put("query", query)
|
||||
putJsonObject("variables") {
|
||||
@ -55,8 +56,8 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
authClient.newCall(
|
||||
POST(
|
||||
apiUrl,
|
||||
body = payload.toString().toRequestBody(jsonMime)
|
||||
)
|
||||
body = payload.toString().toRequestBody(jsonMime),
|
||||
),
|
||||
)
|
||||
.await()
|
||||
.parseAs<JsonObject>()
|
||||
@ -84,7 +85,8 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
|progress
|
||||
|}
|
||||
|}
|
||||
|""".trimMargin()
|
||||
|
|
||||
""".trimMargin()
|
||||
val payload = buildJsonObject {
|
||||
put("query", query)
|
||||
putJsonObject("variables") {
|
||||
@ -127,7 +129,8 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
|}
|
||||
|}
|
||||
|}
|
||||
|""".trimMargin()
|
||||
|
|
||||
""".trimMargin()
|
||||
val payload = buildJsonObject {
|
||||
put("query", query)
|
||||
putJsonObject("variables") {
|
||||
@ -137,8 +140,8 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
authClient.newCall(
|
||||
POST(
|
||||
apiUrl,
|
||||
body = payload.toString().toRequestBody(jsonMime)
|
||||
)
|
||||
body = payload.toString().toRequestBody(jsonMime),
|
||||
),
|
||||
)
|
||||
.await()
|
||||
.parseAs<JsonObject>()
|
||||
@ -193,7 +196,8 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
|}
|
||||
|}
|
||||
|}
|
||||
|""".trimMargin()
|
||||
|
|
||||
""".trimMargin()
|
||||
val payload = buildJsonObject {
|
||||
put("query", query)
|
||||
putJsonObject("variables") {
|
||||
@ -204,8 +208,8 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
authClient.newCall(
|
||||
POST(
|
||||
apiUrl,
|
||||
body = payload.toString().toRequestBody(jsonMime)
|
||||
)
|
||||
body = payload.toString().toRequestBody(jsonMime),
|
||||
),
|
||||
)
|
||||
.await()
|
||||
.parseAs<JsonObject>()
|
||||
@ -238,15 +242,16 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
|}
|
||||
|}
|
||||
|}
|
||||
|""".trimMargin()
|
||||
|
|
||||
""".trimMargin()
|
||||
val payload = buildJsonObject {
|
||||
put("query", query)
|
||||
}
|
||||
authClient.newCall(
|
||||
POST(
|
||||
apiUrl,
|
||||
body = payload.toString().toRequestBody(jsonMime)
|
||||
)
|
||||
body = payload.toString().toRequestBody(jsonMime),
|
||||
),
|
||||
)
|
||||
.await()
|
||||
.parseAs<JsonObject>()
|
||||
@ -255,7 +260,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
val viewer = data["Viewer"]!!.jsonObject
|
||||
Pair(
|
||||
viewer["id"]!!.jsonPrimitive.int,
|
||||
viewer["mediaListOptions"]!!.jsonObject["scoreFormat"]!!.jsonPrimitive.content
|
||||
viewer["mediaListOptions"]!!.jsonObject["scoreFormat"]!!.jsonPrimitive.content,
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -270,7 +275,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
struct["format"]!!.jsonPrimitive.content.replace("_", "-"),
|
||||
struct["status"]!!.jsonPrimitive.contentOrNull ?: "",
|
||||
parseDate(struct, "startDate"),
|
||||
struct["chapters"]!!.jsonPrimitive.intOrNull ?: 0
|
||||
struct["chapters"]!!.jsonPrimitive.intOrNull ?: 0,
|
||||
)
|
||||
}
|
||||
|
||||
@ -282,7 +287,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
struct["progress"]!!.jsonPrimitive.int,
|
||||
parseDate(struct, "startedAt"),
|
||||
parseDate(struct, "completedAt"),
|
||||
jsonToALManga(struct["media"]!!.jsonObject)
|
||||
jsonToALManga(struct["media"]!!.jsonObject),
|
||||
)
|
||||
}
|
||||
|
||||
@ -292,7 +297,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
date.set(
|
||||
struct[dateKey]!!.jsonObject["year"]!!.jsonPrimitive.int,
|
||||
struct[dateKey]!!.jsonObject["month"]!!.jsonPrimitive.int - 1,
|
||||
struct[dateKey]!!.jsonObject["day"]!!.jsonPrimitive.int
|
||||
struct[dateKey]!!.jsonObject["day"]!!.jsonPrimitive.int,
|
||||
)
|
||||
date.timeInMillis
|
||||
} catch (_: Exception) {
|
||||
|
@ -16,7 +16,7 @@ data class ALManga(
|
||||
val format: String,
|
||||
val publishing_status: String,
|
||||
val start_date_fuzzy: Long,
|
||||
val total_chapters: Int
|
||||
val total_chapters: Int,
|
||||
) {
|
||||
|
||||
fun toTrack() = TrackSearch.create(TrackManager.ANILIST).apply {
|
||||
@ -46,7 +46,7 @@ data class ALUserManga(
|
||||
val chapters_read: Int,
|
||||
val start_date_fuzzy: Long,
|
||||
val completed_date_fuzzy: Long,
|
||||
val manga: ALManga
|
||||
val manga: ALManga,
|
||||
) {
|
||||
|
||||
fun toTrack() = Track.create(TrackManager.ANILIST).apply {
|
||||
|
@ -7,7 +7,7 @@ data class OAuth(
|
||||
val access_token: String,
|
||||
val token_type: String,
|
||||
val expires: Long,
|
||||
val expires_in: Long
|
||||
val expires_in: Long,
|
||||
) {
|
||||
|
||||
fun isExpired() = System.currentTimeMillis() > expires
|
||||
|
@ -6,5 +6,5 @@ import kotlinx.serialization.Serializable
|
||||
data class Avatar(
|
||||
val large: String? = "",
|
||||
val medium: String? = "",
|
||||
val small: String? = ""
|
||||
val small: String? = "",
|
||||
)
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user