mirror of
https://github.com/mihonapp/mihon.git
synced 2025-06-30 04:57:50 +02:00
Compare commits
266 Commits
Author | SHA1 | Date | |
---|---|---|---|
a6ac2fbc9a | |||
3da8677e32 | |||
4d0d7d5ad6 | |||
8c4ece4b2d | |||
bf3bb8a378 | |||
cf5e60f8eb | |||
7de707c60a | |||
5cd11ad8c3 | |||
6bba52a2b6 | |||
54b476df4e | |||
a68f123594 | |||
08ad4f96b9 | |||
77a3acf5cc | |||
dea585e69b | |||
879dacfba6 | |||
b459234ddc | |||
76d2c676fd | |||
d5015d37e1 | |||
1b71e4cee7 | |||
18ef5c6ff9 | |||
35e0561950 | |||
adab8e3ed8 | |||
89dbb4d300 | |||
e3f3686b8a | |||
9984e983b4 | |||
4ebe67ef53 | |||
1a11d4153e | |||
cd7cf3583e | |||
66a180bc36 | |||
eb06667455 | |||
0ff8966a27 | |||
2cc6794db5 | |||
0cb4094dd9 | |||
edd213343b | |||
46ec655db5 | |||
769efd9d06 | |||
49cb3b6aa7 | |||
8ad98b67d2 | |||
8a8f1d3205 | |||
4a27f0546c | |||
727a7e4b2d | |||
2b5e8241ab | |||
3dc4fd8dd1 | |||
375a27a93d | |||
544387d1a0 | |||
cb8120d38f | |||
78a261f5d3 | |||
b8f7653fb2 | |||
e0d2a01bc8 | |||
560be9f553 | |||
47723042c5 | |||
d04d676d2f | |||
3435636ca0 | |||
2e1572d7cc | |||
938339690e | |||
dbb2c523c1 | |||
0b9d436753 | |||
2d03f3ce1e | |||
c4a476d0d2 | |||
5122aed332 | |||
5336c5b46e | |||
22615f5981 | |||
bdf4b4b679 | |||
548e300c4b | |||
8a5d8c96ef | |||
78c2631b6f | |||
7c246ffc71 | |||
8bb85753cc | |||
abfdde28ef | |||
9801f1edfa | |||
fc3a200a63 | |||
6a00658119 | |||
353485054e | |||
800583b5e2 | |||
2db2b7348d | |||
f3718257f5 | |||
5500762acd | |||
4c8f5e1f7a | |||
733cf99bb4 | |||
58c2f22120 | |||
42accebeca | |||
1c5c370c12 | |||
448645d83a | |||
09b6a3b41e | |||
74206d60ce | |||
c3a0de7fab | |||
7edf7a434f | |||
b701821550 | |||
d022bf2673 | |||
7eed8c440c | |||
1ab12e380a | |||
728e14e8e4 | |||
8aa402526a | |||
4793ee4786 | |||
a09d6c0470 | |||
9e83130bd8 | |||
2ed01af723 | |||
afc80d6a7c | |||
532a1b1aba | |||
65062b4bcb | |||
c16206d816 | |||
185283f864 | |||
7d1f5c7383 | |||
945afc71ef | |||
818fe50f77 | |||
6fddad7a77 | |||
38d131be37 | |||
aeff846e1f | |||
6b52fc1e2d | |||
0671b530ba | |||
207f9c26ae | |||
6367ce5e5e | |||
ba1a2e9942 | |||
7f998ecdbd | |||
ecd5414287 | |||
6107f5f3d2 | |||
13afa9f476 | |||
cd87c7e88e | |||
ed4dea8686 | |||
808177f8c9 | |||
aed51251b3 | |||
1c2730163d | |||
0de86dfe6f | |||
7a1b99be46 | |||
9b64b0139c | |||
0a6160d7cf | |||
e51a6d332e | |||
a9d2741e6a | |||
12bd7268d2 | |||
be0a23d9ad | |||
458a0e608a | |||
32f3a50def | |||
7de4226d80 | |||
6a39c8fc13 | |||
dc39669321 | |||
be4f27028c | |||
60e73e2d1f | |||
e8f284d377 | |||
3ea3b0bf2e | |||
e1a43d2e7d | |||
2e918fe1d6 | |||
601309c7cc | |||
10ddeeb799 | |||
3463d6c752 | |||
8acce011b5 | |||
fe9ea50356 | |||
e6f29ae57f | |||
6cfd2c510b | |||
430ff80198 | |||
230fa76d57 | |||
46a4b0e0b6 | |||
5b3cadb7a8 | |||
3153071a8a | |||
bba7372556 | |||
9fe1a7e2ae | |||
98822a39d9 | |||
a2c830b908 | |||
bdef2cfdfb | |||
f229a5e2ec | |||
845e061382 | |||
e7d4eb1ae3 | |||
b4ba56bfb4 | |||
25784d1fe5 | |||
619eca7a51 | |||
f3d85655a0 | |||
9600675677 | |||
ce8a759192 | |||
88bc0bf613 | |||
b508e4208a | |||
c74d8cf499 | |||
a34c2b082f | |||
ad49a02879 | |||
e985ffc690 | |||
6cbb02f02d | |||
c0d0ff66b6 | |||
1e4d7f8c6e | |||
a8a761aa5f | |||
41952f0215 | |||
bfcc883f01 | |||
39722055f5 | |||
f85dfa90b8 | |||
0a4163d236 | |||
78de11a9e3 | |||
d2fc6d9f44 | |||
abf31f4a79 | |||
f28dd4f4de | |||
55b64899f5 | |||
d4aeeadb26 | |||
7ce0110158 | |||
7c1e55eb7f | |||
27542bc81d | |||
9ebbfb2d90 | |||
701b1ee744 | |||
0edc981cd2 | |||
da5942b398 | |||
709de81814 | |||
90b312a56e | |||
459759bfe5 | |||
00817aacfe | |||
e306eb0874 | |||
33a02b47d5 | |||
f0a5557e60 | |||
58a871c8cc | |||
4f56071786 | |||
f8b2c79aef | |||
8f00d34b0b | |||
6129519e5a | |||
593091a5e3 | |||
22ed163c8f | |||
93e2b88d41 | |||
7cd54dc8f0 | |||
ccd7c8df53 | |||
5b3bd3f470 | |||
bf1b7f44b6 | |||
538dd60580 | |||
f453236840 | |||
bfe7aa1ed2 | |||
9e2ef82902 | |||
9352e249ee | |||
3800065230 | |||
ebc2c4f73a | |||
f057440cc1 | |||
506f9cfca8 | |||
8a70c3353f | |||
3d8f123e05 | |||
a8c8f15e07 | |||
21e647017b | |||
2a1bb3dc27 | |||
55a3094a65 | |||
b4490e209b | |||
9aa676333c | |||
71b23e57ff | |||
2c76bc99fc | |||
bb06895145 | |||
684965f3e5 | |||
e621f4e2fa | |||
028ea57232 | |||
718fa25c10 | |||
90c9f28818 | |||
cb9c5a35cb | |||
fadaefeaef | |||
b17b882a3b | |||
f0f3afd5f1 | |||
42026b49bf | |||
151193c4c3 | |||
3448751e0e | |||
aae011ed83 | |||
c95a269460 | |||
98c0e5271f | |||
f343131802 | |||
ea34ba53b9 | |||
b8d8cf19d9 | |||
c9be4093e7 | |||
082eef708f | |||
9106fc5b94 | |||
918502742d | |||
f32f1eeaa5 | |||
2d1404d155 | |||
a56997e98c | |||
ef918078d1 | |||
7e61900cf5 | |||
e98f90b099 | |||
2e127dff1f | |||
828db19e02 | |||
99aa3f5713 | |||
1a568e2961 |
8
.github/workflows/build_pull_request.yml
vendored
8
.github/workflows/build_pull_request.yml
vendored
@ -1,6 +1,9 @@
|
|||||||
name: PR build check
|
name: PR build check
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
|
paths-ignore:
|
||||||
|
- '**.md'
|
||||||
|
- 'app/src/main/res/**/strings.xml'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@ -25,9 +28,6 @@ jobs:
|
|||||||
cp .github/runner-files/ci-gradle.properties ~/.gradle/gradle.properties
|
cp .github/runner-files/ci-gradle.properties ~/.gradle/gradle.properties
|
||||||
|
|
||||||
- name: Build app
|
- name: Build app
|
||||||
uses: gradle/gradle-command-action@v1
|
uses: gradle/gradle-command-action@v2
|
||||||
with:
|
with:
|
||||||
arguments: assembleStandardRelease
|
arguments: assembleStandardRelease
|
||||||
distributions-cache-enabled: true
|
|
||||||
dependencies-cache-enabled: true
|
|
||||||
configuration-cache-enabled: true
|
|
||||||
|
8
.github/workflows/build_push.yml
vendored
8
.github/workflows/build_push.yml
vendored
@ -13,9 +13,10 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Cancel previous runs
|
- name: Cancel previous runs
|
||||||
uses: styfle/cancel-workflow-action@0.5.0
|
uses: styfle/cancel-workflow-action@0.9.1
|
||||||
with:
|
with:
|
||||||
access_token: ${{ github.token }}
|
access_token: ${{ github.token }}
|
||||||
|
all_but_latest: true
|
||||||
|
|
||||||
- name: Clone repo
|
- name: Clone repo
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
@ -34,12 +35,9 @@ jobs:
|
|||||||
cp .github/runner-files/ci-gradle.properties ~/.gradle/gradle.properties
|
cp .github/runner-files/ci-gradle.properties ~/.gradle/gradle.properties
|
||||||
|
|
||||||
- name: Build app
|
- name: Build app
|
||||||
uses: gradle/gradle-command-action@v1
|
uses: gradle/gradle-command-action@v2
|
||||||
with:
|
with:
|
||||||
arguments: assembleStandardRelease
|
arguments: assembleStandardRelease
|
||||||
distributions-cache-enabled: true
|
|
||||||
dependencies-cache-enabled: true
|
|
||||||
configuration-cache-enabled: true
|
|
||||||
|
|
||||||
# Sign APK and create release for tags
|
# Sign APK and create release for tags
|
||||||
|
|
||||||
|
3
.github/workflows/cancel_pull_request.yml
vendored
3
.github/workflows/cancel_pull_request.yml
vendored
@ -10,6 +10,7 @@ jobs:
|
|||||||
cancel:
|
cancel:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: styfle/cancel-workflow-action@0.8.0
|
- uses: styfle/cancel-workflow-action@0.9.1
|
||||||
with:
|
with:
|
||||||
|
all_but_latest: true
|
||||||
workflow_id: ${{ github.event.workflow.id }}
|
workflow_id: ${{ github.event.workflow.id }}
|
||||||
|
2
.github/workflows/issue_moderator.yml
vendored
2
.github/workflows/issue_moderator.yml
vendored
@ -9,6 +9,6 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Moderate issues
|
- name: Moderate issues
|
||||||
uses: tachiyomiorg/issue-moderator-action@v1.1
|
uses: tachiyomiorg/issue-moderator-action@v1
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
8
.github/workflows/lock.yml
vendored
8
.github/workflows/lock.yml
vendored
@ -3,7 +3,7 @@ name: Lock threads
|
|||||||
on:
|
on:
|
||||||
# Daily
|
# Daily
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '0 * * * *'
|
- cron: '0 0 * * *'
|
||||||
# Manual trigger
|
# Manual trigger
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
@ -12,8 +12,8 @@ jobs:
|
|||||||
lock:
|
lock:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: dessant/lock-threads@v2
|
- uses: dessant/lock-threads@v3
|
||||||
with:
|
with:
|
||||||
github-token: ${{ github.token }}
|
github-token: ${{ github.token }}
|
||||||
issue-lock-inactive-days: '2'
|
issue-inactive-days: '2'
|
||||||
pr-lock-inactive-days: '2'
|
pr-inactive-days: '2'
|
||||||
|
@ -10,6 +10,7 @@ Thanks for your interest in contributing to Tachiyomi!
|
|||||||
Pull requests are welcome!
|
Pull requests are welcome!
|
||||||
|
|
||||||
If you're interested in taking on [an open issue](https://github.com/tachiyomiorg/tachiyomi/issues), please comment on it so others are aware.
|
If you're interested in taking on [an open issue](https://github.com/tachiyomiorg/tachiyomi/issues), please comment on it so others are aware.
|
||||||
|
You do not need to ask for permission nor an assignment.
|
||||||
|
|
||||||
|
|
||||||
# Translations
|
# Translations
|
||||||
|
@ -28,14 +28,14 @@ android {
|
|||||||
applicationId = "eu.kanade.tachiyomi"
|
applicationId = "eu.kanade.tachiyomi"
|
||||||
minSdk = AndroidConfig.minSdk
|
minSdk = AndroidConfig.minSdk
|
||||||
targetSdk = AndroidConfig.targetSdk
|
targetSdk = AndroidConfig.targetSdk
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
versionCode = 73
|
||||||
versionCode = 69
|
versionName = "0.13.0"
|
||||||
versionName = "0.12.3"
|
|
||||||
|
|
||||||
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
||||||
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
|
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
|
||||||
buildConfigField("String", "BUILD_TIME", "\"${getBuildTime()}\"")
|
buildConfigField("String", "BUILD_TIME", "\"${getBuildTime()}\"")
|
||||||
buildConfigField("boolean", "INCLUDE_UPDATER", "false")
|
buildConfigField("boolean", "INCLUDE_UPDATER", "false")
|
||||||
|
buildConfigField("boolean", "PREVIEW", "false")
|
||||||
|
|
||||||
// Please disable ACRA or use your own instance in forked versions of the project
|
// Please disable ACRA or use your own instance in forked versions of the project
|
||||||
buildConfigField("String", "ACRA_URI", "\"https://tachiyomi.kanade.eu/crash_report\"")
|
buildConfigField("String", "ACRA_URI", "\"https://tachiyomi.kanade.eu/crash_report\"")
|
||||||
@ -43,6 +43,8 @@ android {
|
|||||||
ndk {
|
ndk {
|
||||||
abiFilters += SUPPORTED_ABIS
|
abiFilters += SUPPORTED_ABIS
|
||||||
}
|
}
|
||||||
|
|
||||||
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
|
|
||||||
splits {
|
splits {
|
||||||
@ -58,25 +60,25 @@ android {
|
|||||||
named("debug") {
|
named("debug") {
|
||||||
versionNameSuffix = "-${getCommitCount()}"
|
versionNameSuffix = "-${getCommitCount()}"
|
||||||
applicationIdSuffix = ".debug"
|
applicationIdSuffix = ".debug"
|
||||||
|
|
||||||
isShrinkResources = true
|
|
||||||
isMinifyEnabled = true
|
|
||||||
proguardFiles("proguard-android-optimize.txt", "proguard-rules.pro")
|
|
||||||
}
|
|
||||||
create("debugFull") { // Debug without R8
|
|
||||||
initWith(getByName("debug"))
|
|
||||||
isShrinkResources = false
|
|
||||||
isMinifyEnabled = false
|
|
||||||
}
|
}
|
||||||
named("release") {
|
named("release") {
|
||||||
isShrinkResources = true
|
isShrinkResources = true
|
||||||
isMinifyEnabled = true
|
isMinifyEnabled = true
|
||||||
proguardFiles("proguard-android-optimize.txt", "proguard-rules.pro")
|
proguardFiles("proguard-android-optimize.txt", "proguard-rules.pro")
|
||||||
}
|
}
|
||||||
|
create("preview") {
|
||||||
|
initWith(getByName("release"))
|
||||||
|
buildConfigField("boolean", "PREVIEW", "true")
|
||||||
|
|
||||||
|
val debugType = getByName("debug")
|
||||||
|
signingConfig = debugType.signingConfig
|
||||||
|
versionNameSuffix = debugType.versionNameSuffix
|
||||||
|
applicationIdSuffix = debugType.applicationIdSuffix
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
getByName("debugFull").res.srcDirs("src/debug/res")
|
getByName("preview").res.srcDirs("src/debug/res")
|
||||||
}
|
}
|
||||||
|
|
||||||
flavorDimensions.add("default")
|
flavorDimensions.add("default")
|
||||||
@ -98,8 +100,10 @@ android {
|
|||||||
"LICENSE.txt",
|
"LICENSE.txt",
|
||||||
"META-INF/LICENSE",
|
"META-INF/LICENSE",
|
||||||
"META-INF/LICENSE.txt",
|
"META-INF/LICENSE.txt",
|
||||||
|
"META-INF/README.md",
|
||||||
"META-INF/NOTICE",
|
"META-INF/NOTICE",
|
||||||
"META-INF/*.kotlin_module",
|
"META-INF/*.kotlin_module",
|
||||||
|
"META-INF/*.version",
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -109,12 +113,17 @@ android {
|
|||||||
|
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
viewBinding = true
|
viewBinding = true
|
||||||
|
|
||||||
|
// Disable some unused things
|
||||||
|
aidl = false
|
||||||
|
renderScript = false
|
||||||
|
shaders = false
|
||||||
}
|
}
|
||||||
|
|
||||||
lint {
|
lint {
|
||||||
disable("MissingTranslation", "ExtraTranslation")
|
disable.addAll(listOf("MissingTranslation", "ExtraTranslation"))
|
||||||
isAbortOnError = false
|
abortOnError = false
|
||||||
isCheckReleaseBuilds = false
|
checkReleaseBuilds = false
|
||||||
}
|
}
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
@ -130,7 +139,7 @@ android {
|
|||||||
dependencies {
|
dependencies {
|
||||||
implementation(kotlin("reflect", version = BuildPluginsVersion.KOTLIN))
|
implementation(kotlin("reflect", version = BuildPluginsVersion.KOTLIN))
|
||||||
|
|
||||||
val coroutinesVersion = "1.5.2"
|
val coroutinesVersion = "1.6.0"
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion")
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion")
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion")
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion")
|
||||||
|
|
||||||
@ -138,19 +147,19 @@ dependencies {
|
|||||||
implementation("org.tachiyomi:source-api:1.1")
|
implementation("org.tachiyomi:source-api:1.1")
|
||||||
|
|
||||||
// AndroidX libraries
|
// AndroidX libraries
|
||||||
implementation("androidx.annotation:annotation:1.3.0-beta01")
|
implementation("androidx.annotation:annotation:1.4.0-alpha01")
|
||||||
implementation("androidx.appcompat:appcompat:1.4.0-alpha03")
|
implementation("androidx.appcompat:appcompat:1.4.1")
|
||||||
implementation("androidx.biometric:biometric-ktx:1.2.0-alpha03")
|
implementation("androidx.biometric:biometric-ktx:1.2.0-alpha04")
|
||||||
implementation("androidx.browser:browser:1.4.0-beta01")
|
implementation("androidx.browser:browser:1.4.0")
|
||||||
implementation("androidx.constraintlayout:constraintlayout:2.1.1")
|
implementation("androidx.constraintlayout:constraintlayout:2.1.3")
|
||||||
implementation("androidx.coordinatorlayout:coordinatorlayout:1.1.0")
|
implementation("androidx.coordinatorlayout:coordinatorlayout:1.2.0")
|
||||||
implementation("androidx.core:core-ktx:1.7.0-beta02")
|
implementation("androidx.core:core-ktx:1.8.0-alpha02")
|
||||||
implementation("androidx.core:core-splashscreen:1.0.0-alpha02")
|
implementation("androidx.core:core-splashscreen:1.0.0-alpha02")
|
||||||
implementation("androidx.recyclerview:recyclerview:1.3.0-alpha01")
|
implementation("androidx.recyclerview:recyclerview:1.3.0-alpha01")
|
||||||
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01")
|
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01")
|
||||||
implementation("androidx.viewpager:viewpager:1.1.0-alpha01")
|
implementation("androidx.viewpager:viewpager:1.1.0-alpha01")
|
||||||
|
|
||||||
val lifecycleVersion = "2.4.0-beta01"
|
val lifecycleVersion = "2.4.0"
|
||||||
implementation("androidx.lifecycle:lifecycle-common:$lifecycleVersion")
|
implementation("androidx.lifecycle:lifecycle-common:$lifecycleVersion")
|
||||||
implementation("androidx.lifecycle:lifecycle-process:$lifecycleVersion")
|
implementation("androidx.lifecycle:lifecycle-process:$lifecycleVersion")
|
||||||
implementation("androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion")
|
implementation("androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion")
|
||||||
@ -169,25 +178,23 @@ dependencies {
|
|||||||
implementation("com.squareup.okhttp3:okhttp:$okhttpVersion")
|
implementation("com.squareup.okhttp3:okhttp:$okhttpVersion")
|
||||||
implementation("com.squareup.okhttp3:logging-interceptor:$okhttpVersion")
|
implementation("com.squareup.okhttp3:logging-interceptor:$okhttpVersion")
|
||||||
implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:$okhttpVersion")
|
implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:$okhttpVersion")
|
||||||
implementation("com.squareup.okio:okio:2.10.0")
|
implementation("com.squareup.okio:okio:3.0.0")
|
||||||
|
|
||||||
// TLS 1.3 support for Android < 10
|
// TLS 1.3 support for Android < 10
|
||||||
implementation("org.conscrypt:conscrypt-android:2.5.2")
|
implementation("org.conscrypt:conscrypt-android:2.5.2")
|
||||||
|
|
||||||
// Data serialization (JSON, protobuf)
|
// Data serialization (JSON, protobuf)
|
||||||
val kotlinSerializationVersion = "1.3.0"
|
val kotlinSerializationVersion = "1.3.2"
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinSerializationVersion")
|
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinSerializationVersion")
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-protobuf:$kotlinSerializationVersion")
|
implementation("org.jetbrains.kotlinx:kotlinx-serialization-protobuf:$kotlinSerializationVersion")
|
||||||
|
|
||||||
// TODO: remove these once they're no longer used in any extensions
|
|
||||||
implementation("com.google.code.gson:gson:2.8.7")
|
|
||||||
implementation("com.github.salomonbrys.kotson:kotson:2.5.0")
|
|
||||||
|
|
||||||
// JavaScript engine
|
// 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("com.squareup.duktape:duktape-android:1.4.0")
|
||||||
|
|
||||||
// HTML parser
|
// HTML parser
|
||||||
implementation("org.jsoup:jsoup:1.14.2")
|
implementation("org.jsoup:jsoup:1.14.3")
|
||||||
|
|
||||||
// Disk
|
// Disk
|
||||||
implementation("com.jakewharton:disklrucache:2.0.2")
|
implementation("com.jakewharton:disklrucache:2.0.2")
|
||||||
@ -195,13 +202,13 @@ dependencies {
|
|||||||
implementation("com.github.junrar:junrar:7.4.0")
|
implementation("com.github.junrar:junrar:7.4.0")
|
||||||
|
|
||||||
// Database
|
// Database
|
||||||
implementation("androidx.sqlite:sqlite-ktx:2.1.0")
|
implementation("androidx.sqlite:sqlite-ktx:2.2.0")
|
||||||
implementation("com.github.inorichi.storio:storio-common:8be19de@aar")
|
implementation("com.github.inorichi.storio:storio-common:8be19de@aar")
|
||||||
implementation("com.github.inorichi.storio:storio-sqlite:8be19de@aar")
|
implementation("com.github.inorichi.storio:storio-sqlite:8be19de@aar")
|
||||||
implementation("com.github.requery:sqlite-android:3.36.0")
|
implementation("com.github.requery:sqlite-android:3.36.0")
|
||||||
|
|
||||||
// Preferences
|
// Preferences
|
||||||
implementation("androidx.preference:preference-ktx:1.1.1")
|
implementation("androidx.preference:preference-ktx:1.2.0-rc01")
|
||||||
implementation("com.github.tfcporciuncula.flow-preferences:flow-preferences:1.4.0")
|
implementation("com.github.tfcporciuncula.flow-preferences:flow-preferences:1.4.0")
|
||||||
|
|
||||||
// Model View Presenter
|
// Model View Presenter
|
||||||
@ -213,7 +220,7 @@ dependencies {
|
|||||||
implementation("com.github.inorichi.injekt:injekt-core:65b0440")
|
implementation("com.github.inorichi.injekt:injekt-core:65b0440")
|
||||||
|
|
||||||
// Image loading
|
// Image loading
|
||||||
val coilVersion = "1.3.2"
|
val coilVersion = "1.4.0"
|
||||||
implementation("io.coil-kt:coil:$coilVersion")
|
implementation("io.coil-kt:coil:$coilVersion")
|
||||||
implementation("io.coil-kt:coil-gif:$coilVersion")
|
implementation("io.coil-kt:coil-gif:$coilVersion")
|
||||||
|
|
||||||
@ -226,19 +233,19 @@ dependencies {
|
|||||||
implementation("com.github.gpanther:java-nat-sort:natural-comparator-1.1")
|
implementation("com.github.gpanther:java-nat-sort:natural-comparator-1.1")
|
||||||
|
|
||||||
// UI libraries
|
// UI libraries
|
||||||
implementation("com.google.android.material:material:1.5.0-alpha04")
|
implementation("com.google.android.material:material:1.6.0-alpha02")
|
||||||
implementation("com.github.dmytrodanylyk.android-process-button:library:1.0.4")
|
implementation("com.github.dmytrodanylyk.android-process-button:library:1.0.4")
|
||||||
implementation("eu.davidea:flexible-adapter:5.1.0")
|
implementation("com.github.arkon.FlexibleAdapter:flexible-adapter:c8013533")
|
||||||
implementation("eu.davidea:flexible-adapter-ui:1.0.0")
|
implementation("com.github.arkon.FlexibleAdapter:flexible-adapter-ui:c8013533")
|
||||||
implementation("com.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.1.0")
|
implementation("com.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.1.0")
|
||||||
implementation("com.github.chrisbanes:PhotoView:2.3.0")
|
implementation("com.github.chrisbanes:PhotoView:2.3.0")
|
||||||
implementation("com.github.tachiyomiorg:DirectionalViewPager:1.0.0") {
|
implementation("com.github.tachiyomiorg:DirectionalViewPager:1.0.0") {
|
||||||
exclude(group = "androidx.viewpager", module = "viewpager")
|
exclude(group = "androidx.viewpager", module = "viewpager")
|
||||||
}
|
}
|
||||||
implementation("dev.chrisbanes.insetter:insetter:0.6.0")
|
implementation("dev.chrisbanes.insetter:insetter:0.6.1")
|
||||||
|
|
||||||
// Conductor
|
// Conductor
|
||||||
val conductorVersion = "3.0.0"
|
val conductorVersion = "3.1.2"
|
||||||
implementation("com.bluelinelabs:conductor:$conductorVersion")
|
implementation("com.bluelinelabs:conductor:$conductorVersion")
|
||||||
implementation("com.bluelinelabs:conductor-viewpager:$conductorVersion")
|
implementation("com.bluelinelabs:conductor-viewpager:$conductorVersion")
|
||||||
implementation("com.github.tachiyomiorg:conductor-support-preference:$conductorVersion")
|
implementation("com.github.tachiyomiorg:conductor-support-preference:$conductorVersion")
|
||||||
@ -252,17 +259,17 @@ dependencies {
|
|||||||
implementation("io.github.reactivecircus.flowbinding:flowbinding-viewpager:$flowbindingVersion")
|
implementation("io.github.reactivecircus.flowbinding:flowbinding-viewpager:$flowbindingVersion")
|
||||||
|
|
||||||
// Logging
|
// Logging
|
||||||
implementation("com.jakewharton.timber:timber:5.0.1")
|
implementation("com.squareup.logcat:logcat:0.1")
|
||||||
|
|
||||||
// Crash reports/analytics
|
// Crash reports/analytics
|
||||||
implementation("ch.acra:acra-http:5.8.1")
|
implementation("ch.acra:acra-http:5.8.4")
|
||||||
"standardImplementation"("com.google.firebase:firebase-analytics:19.0.1")
|
"standardImplementation"("com.google.firebase:firebase-analytics-ktx:20.0.2")
|
||||||
|
|
||||||
// Licenses
|
// Licenses
|
||||||
implementation("com.mikepenz:aboutlibraries-core:${BuildPluginsVersion.ABOUTLIB_PLUGIN}")
|
implementation("com.mikepenz:aboutlibraries-core:${BuildPluginsVersion.ABOUTLIB_PLUGIN}")
|
||||||
|
|
||||||
// Shizuku
|
// Shizuku
|
||||||
val shizukuVersion = "12.0.0"
|
val shizukuVersion = "12.1.0"
|
||||||
implementation("dev.rikka.shizuku:api:$shizukuVersion")
|
implementation("dev.rikka.shizuku:api:$shizukuVersion")
|
||||||
implementation("dev.rikka.shizuku:provider:$shizukuVersion")
|
implementation("dev.rikka.shizuku:provider:$shizukuVersion")
|
||||||
|
|
||||||
@ -285,12 +292,12 @@ tasks {
|
|||||||
kotlinOptions.freeCompilerArgs += listOf(
|
kotlinOptions.freeCompilerArgs += listOf(
|
||||||
"-Xopt-in=kotlin.Experimental",
|
"-Xopt-in=kotlin.Experimental",
|
||||||
"-Xopt-in=kotlin.RequiresOptIn",
|
"-Xopt-in=kotlin.RequiresOptIn",
|
||||||
"-Xuse-experimental=kotlin.ExperimentalStdlibApi",
|
"-Xopt-in=kotlin.ExperimentalStdlibApi",
|
||||||
"-Xuse-experimental=kotlinx.coroutines.FlowPreview",
|
"-Xopt-in=kotlinx.coroutines.FlowPreview",
|
||||||
"-Xuse-experimental=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
"-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
||||||
"-Xuse-experimental=kotlinx.coroutines.InternalCoroutinesApi",
|
"-Xopt-in=kotlinx.coroutines.InternalCoroutinesApi",
|
||||||
"-Xuse-experimental=kotlinx.serialization.ExperimentalSerializationApi",
|
"-Xopt-in=kotlinx.serialization.ExperimentalSerializationApi",
|
||||||
"-Xuse-experimental=coil.annotation.ExperimentalCoilApi",
|
"-Xopt-in=coil.annotation.ExperimentalCoilApi",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
1
app/proguard-rules.pro
vendored
1
app/proguard-rules.pro
vendored
@ -12,6 +12,7 @@
|
|||||||
-keep,allowoptimization class com.google.gson.** { public protected *; }
|
-keep,allowoptimization class com.google.gson.** { public protected *; }
|
||||||
-keep,allowoptimization class com.github.salomonbrys.kotson.** { public protected *; }
|
-keep,allowoptimization class com.github.salomonbrys.kotson.** { public protected *; }
|
||||||
-keep,allowoptimization class com.squareup.duktape.** { public protected *; }
|
-keep,allowoptimization class com.squareup.duktape.** { public protected *; }
|
||||||
|
-keep,allowoptimization class app.cash.quickjs.** { public protected *; }
|
||||||
-keep,allowoptimization class uy.kohesive.injekt.** { public protected *; }
|
-keep,allowoptimization class uy.kohesive.injekt.** { public protected *; }
|
||||||
|
|
||||||
##---------------Begin: proguard configuration for RxJava 1.x ----------
|
##---------------Begin: proguard configuration for RxJava 1.x ----------
|
||||||
|
@ -178,7 +178,7 @@
|
|||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".data.updater.UpdaterService"
|
android:name=".data.updater.AppUpdateService"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
<service
|
<service
|
||||||
|
@ -1,100 +0,0 @@
|
|||||||
package com.google.android.material.appbar
|
|
||||||
|
|
||||||
import android.animation.ValueAnimator
|
|
||||||
import android.view.View
|
|
||||||
import android.view.animation.DecelerateInterpolator
|
|
||||||
import androidx.appcompat.widget.Toolbar
|
|
||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
|
||||||
import androidx.core.animation.doOnEnd
|
|
||||||
import androidx.core.view.ViewCompat
|
|
||||||
import androidx.core.view.marginTop
|
|
||||||
import eu.kanade.tachiyomi.util.system.animatorDurationScale
|
|
||||||
import eu.kanade.tachiyomi.util.view.findChild
|
|
||||||
import eu.kanade.tachiyomi.widget.ElevationAppBarLayout
|
|
||||||
import kotlin.math.roundToLong
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hide toolbar on scroll behavior for [AppBarLayout].
|
|
||||||
*
|
|
||||||
* Inside this package to access some package-private methods.
|
|
||||||
*/
|
|
||||||
class HideToolbarOnScrollBehavior : AppBarLayout.Behavior() {
|
|
||||||
|
|
||||||
@ViewCompat.NestedScrollType
|
|
||||||
private var lastStartedType: Int = 0
|
|
||||||
|
|
||||||
private var offsetAnimator: ValueAnimator? = null
|
|
||||||
|
|
||||||
private var toolbarHeight: Int = 0
|
|
||||||
|
|
||||||
override fun onStartNestedScroll(
|
|
||||||
parent: CoordinatorLayout,
|
|
||||||
child: AppBarLayout,
|
|
||||||
directTargetChild: View,
|
|
||||||
target: View,
|
|
||||||
nestedScrollAxes: Int,
|
|
||||||
type: Int
|
|
||||||
): Boolean {
|
|
||||||
lastStartedType = type
|
|
||||||
offsetAnimator?.cancel()
|
|
||||||
return super.onStartNestedScroll(parent, child, directTargetChild, target, nestedScrollAxes, type)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStopNestedScroll(
|
|
||||||
parent: CoordinatorLayout,
|
|
||||||
layout: AppBarLayout,
|
|
||||||
target: View,
|
|
||||||
type: Int
|
|
||||||
) {
|
|
||||||
super.onStopNestedScroll(parent, layout, target, type)
|
|
||||||
if (toolbarHeight == 0) {
|
|
||||||
toolbarHeight = layout.findChild<Toolbar>()?.height ?: 0
|
|
||||||
}
|
|
||||||
if (lastStartedType == ViewCompat.TYPE_TOUCH || type == ViewCompat.TYPE_NON_TOUCH) {
|
|
||||||
animateToolbarVisibility(
|
|
||||||
parent,
|
|
||||||
layout,
|
|
||||||
getTopBottomOffsetForScrollingSibling(layout) > -toolbarHeight / 2
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFlingFinished(parent: CoordinatorLayout, layout: AppBarLayout) {
|
|
||||||
super.onFlingFinished(parent, layout)
|
|
||||||
animateToolbarVisibility(
|
|
||||||
parent,
|
|
||||||
layout,
|
|
||||||
getTopBottomOffsetForScrollingSibling(layout) > -toolbarHeight / 2
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getTopBottomOffsetForScrollingSibling(abl: AppBarLayout): Int {
|
|
||||||
return topBottomOffsetForScrollingSibling - abl.marginTop
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun animateToolbarVisibility(
|
|
||||||
coordinatorLayout: CoordinatorLayout,
|
|
||||||
child: AppBarLayout,
|
|
||||||
isVisible: Boolean
|
|
||||||
) {
|
|
||||||
val current = getTopBottomOffsetForScrollingSibling(child)
|
|
||||||
val target = if (isVisible) 0 else -toolbarHeight
|
|
||||||
if (current == target) return
|
|
||||||
|
|
||||||
offsetAnimator?.cancel()
|
|
||||||
offsetAnimator = ValueAnimator().apply {
|
|
||||||
interpolator = DecelerateInterpolator()
|
|
||||||
duration = (150 * child.context.animatorDurationScale).roundToLong()
|
|
||||||
addUpdateListener {
|
|
||||||
setHeaderTopBottomOffset(coordinatorLayout, child, it.animatedValue as Int)
|
|
||||||
}
|
|
||||||
doOnEnd {
|
|
||||||
if ((child as? ElevationAppBarLayout)?.isTransparentWhenNotLifted == true) {
|
|
||||||
child.isLifted = !isVisible
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setIntValues(current, target)
|
|
||||||
start()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -20,25 +20,29 @@ import coil.ImageLoader
|
|||||||
import coil.ImageLoaderFactory
|
import coil.ImageLoaderFactory
|
||||||
import coil.decode.GifDecoder
|
import coil.decode.GifDecoder
|
||||||
import coil.decode.ImageDecoderDecoder
|
import coil.decode.ImageDecoderDecoder
|
||||||
|
import coil.util.DebugLogger
|
||||||
import eu.kanade.tachiyomi.data.coil.ByteBufferFetcher
|
import eu.kanade.tachiyomi.data.coil.ByteBufferFetcher
|
||||||
import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher
|
import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher
|
||||||
import eu.kanade.tachiyomi.data.coil.TachiyomiImageDecoder
|
import eu.kanade.tachiyomi.data.coil.TachiyomiImageDecoder
|
||||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues
|
import eu.kanade.tachiyomi.data.preference.PreferenceValues
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.data.preference.asImmediateFlow
|
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate
|
import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate
|
||||||
|
import eu.kanade.tachiyomi.util.preference.asImmediateFlow
|
||||||
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil
|
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil
|
||||||
import eu.kanade.tachiyomi.util.system.animatorDurationScale
|
import eu.kanade.tachiyomi.util.system.animatorDurationScale
|
||||||
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
import eu.kanade.tachiyomi.util.system.notification
|
import eu.kanade.tachiyomi.util.system.notification
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import logcat.AndroidLogcatLogger
|
||||||
|
import logcat.LogPriority
|
||||||
|
import logcat.LogcatLogger
|
||||||
import org.acra.config.httpSender
|
import org.acra.config.httpSender
|
||||||
import org.acra.ktx.initAcra
|
import org.acra.ktx.initAcra
|
||||||
import org.acra.sender.HttpSender
|
import org.acra.sender.HttpSender
|
||||||
import org.conscrypt.Conscrypt
|
import org.conscrypt.Conscrypt
|
||||||
import timber.log.Timber
|
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
@ -52,7 +56,6 @@ open class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
|
|||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super<Application>.onCreate()
|
super<Application>.onCreate()
|
||||||
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
|
|
||||||
|
|
||||||
// TLS 1.3 support for Android < 10
|
// TLS 1.3 support for Android < 10
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||||
@ -110,6 +113,10 @@ open class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}.launchIn(ProcessLifecycleOwner.get().lifecycleScope)
|
}.launchIn(ProcessLifecycleOwner.get().lifecycleScope)
|
||||||
|
|
||||||
|
if (!LogcatLogger.isInstalled && preferences.verboseLogging()) {
|
||||||
|
LogcatLogger.install(AndroidLogcatLogger(LogPriority.VERBOSE))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun newImageLoader(): ImageLoader {
|
override fun newImageLoader(): ImageLoader {
|
||||||
@ -127,6 +134,7 @@ open class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
|
|||||||
okHttpClient(Injekt.get<NetworkHelper>().coilClient)
|
okHttpClient(Injekt.get<NetworkHelper>().coilClient)
|
||||||
crossfade((300 * this@App.animatorDurationScale).toInt())
|
crossfade((300 * this@App.animatorDurationScale).toInt())
|
||||||
allowRgb565(getSystemService<ActivityManager>()!!.isLowRamDevice)
|
allowRgb565(getSystemService<ActivityManager>()!!.isLowRamDevice)
|
||||||
|
if (preferences.verboseLogging()) logger(DebugLogger())
|
||||||
}.build()
|
}.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -151,7 +159,11 @@ open class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected open fun setupNotificationChannels() {
|
protected open fun setupNotificationChannels() {
|
||||||
Notifications.createChannels(this)
|
try {
|
||||||
|
Notifications.createChannels(this)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logcat(LogPriority.ERROR, e) { "Failed to modify notification channels" }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private inner class DisableIncognitoReceiver : BroadcastReceiver() {
|
private inner class DisableIncognitoReceiver : BroadcastReceiver() {
|
||||||
|
11
app/src/main/java/eu/kanade/tachiyomi/AppInfo.kt
Normal file
11
app/src/main/java/eu/kanade/tachiyomi/AppInfo.kt
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
package eu.kanade.tachiyomi
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used by extensions.
|
||||||
|
*
|
||||||
|
* @since extension-lib 1.3
|
||||||
|
*/
|
||||||
|
object AppInfo {
|
||||||
|
fun getVersionCode() = BuildConfig.VERSION_CODE
|
||||||
|
fun getVersionName() = BuildConfig.VERSION_NAME
|
||||||
|
}
|
@ -5,16 +5,19 @@ import androidx.core.content.edit
|
|||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreatorJob
|
import eu.kanade.tachiyomi.data.backup.BackupCreatorJob
|
||||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
|
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
|
||||||
|
import eu.kanade.tachiyomi.data.preference.MANGA_ONGOING
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferenceKeys
|
import eu.kanade.tachiyomi.data.preference.PreferenceKeys
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
import eu.kanade.tachiyomi.data.updater.UpdaterJob
|
import eu.kanade.tachiyomi.data.updater.AppUpdateJob
|
||||||
import eu.kanade.tachiyomi.extension.ExtensionUpdateJob
|
import eu.kanade.tachiyomi.extension.ExtensionUpdateJob
|
||||||
import eu.kanade.tachiyomi.network.PREF_DOH_CLOUDFLARE
|
import eu.kanade.tachiyomi.network.PREF_DOH_CLOUDFLARE
|
||||||
import eu.kanade.tachiyomi.ui.library.LibrarySort
|
import eu.kanade.tachiyomi.ui.library.LibrarySort
|
||||||
import eu.kanade.tachiyomi.ui.library.setting.SortDirectionSetting
|
import eu.kanade.tachiyomi.ui.library.setting.SortDirectionSetting
|
||||||
import eu.kanade.tachiyomi.ui.library.setting.SortModeSetting
|
import eu.kanade.tachiyomi.ui.library.setting.SortModeSetting
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
|
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.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
|
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
@ -38,7 +41,7 @@ object Migrations {
|
|||||||
|
|
||||||
// Always set up background tasks to ensure they're running
|
// Always set up background tasks to ensure they're running
|
||||||
if (BuildConfig.INCLUDE_UPDATER) {
|
if (BuildConfig.INCLUDE_UPDATER) {
|
||||||
UpdaterJob.setupTask(context)
|
AppUpdateJob.setupTask(context)
|
||||||
}
|
}
|
||||||
ExtensionUpdateJob.setupTask(context)
|
ExtensionUpdateJob.setupTask(context)
|
||||||
LibraryUpdateJob.setupTask(context)
|
LibraryUpdateJob.setupTask(context)
|
||||||
@ -49,10 +52,12 @@ object Migrations {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
|
||||||
if (oldVersion < 14) {
|
if (oldVersion < 14) {
|
||||||
// Restore jobs after upgrading to Evernote's job scheduler.
|
// Restore jobs after upgrading to Evernote's job scheduler.
|
||||||
if (BuildConfig.INCLUDE_UPDATER) {
|
if (BuildConfig.INCLUDE_UPDATER) {
|
||||||
UpdaterJob.setupTask(context)
|
AppUpdateJob.setupTask(context)
|
||||||
}
|
}
|
||||||
LibraryUpdateJob.setupTask(context)
|
LibraryUpdateJob.setupTask(context)
|
||||||
}
|
}
|
||||||
@ -85,7 +90,7 @@ object Migrations {
|
|||||||
if (oldVersion < 43) {
|
if (oldVersion < 43) {
|
||||||
// Restore jobs after migrating from Evernote's job scheduler to WorkManager.
|
// Restore jobs after migrating from Evernote's job scheduler to WorkManager.
|
||||||
if (BuildConfig.INCLUDE_UPDATER) {
|
if (BuildConfig.INCLUDE_UPDATER) {
|
||||||
UpdaterJob.setupTask(context)
|
AppUpdateJob.setupTask(context)
|
||||||
}
|
}
|
||||||
LibraryUpdateJob.setupTask(context)
|
LibraryUpdateJob.setupTask(context)
|
||||||
BackupCreatorJob.setupTask(context)
|
BackupCreatorJob.setupTask(context)
|
||||||
@ -95,8 +100,6 @@ object Migrations {
|
|||||||
}
|
}
|
||||||
if (oldVersion < 44) {
|
if (oldVersion < 44) {
|
||||||
// Reset sorting preference if using removed sort by source
|
// Reset sorting preference if using removed sort by source
|
||||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
|
||||||
|
|
||||||
val oldSortingMode = prefs.getInt(PreferenceKeys.librarySortingMode, 0)
|
val oldSortingMode = prefs.getInt(PreferenceKeys.librarySortingMode, 0)
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
@ -108,7 +111,6 @@ object Migrations {
|
|||||||
}
|
}
|
||||||
if (oldVersion < 52) {
|
if (oldVersion < 52) {
|
||||||
// Migrate library filters to tri-state versions
|
// Migrate library filters to tri-state versions
|
||||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
|
||||||
fun convertBooleanPrefToTriState(key: String): Int {
|
fun convertBooleanPrefToTriState(key: String): Int {
|
||||||
val oldPrefValue = prefs.getBoolean(key, false)
|
val oldPrefValue = prefs.getBoolean(key, false)
|
||||||
return if (oldPrefValue) ExtendedNavigationView.Item.TriStateGroup.State.INCLUDE.value
|
return if (oldPrefValue) ExtendedNavigationView.Item.TriStateGroup.State.INCLUDE.value
|
||||||
@ -137,7 +139,6 @@ object Migrations {
|
|||||||
}
|
}
|
||||||
if (oldVersion < 57) {
|
if (oldVersion < 57) {
|
||||||
// Migrate DNS over HTTPS setting
|
// Migrate DNS over HTTPS setting
|
||||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
|
||||||
val wasDohEnabled = prefs.getBoolean("enable_doh", false)
|
val wasDohEnabled = prefs.getBoolean("enable_doh", false)
|
||||||
if (wasDohEnabled) {
|
if (wasDohEnabled) {
|
||||||
prefs.edit {
|
prefs.edit {
|
||||||
@ -148,7 +149,6 @@ object Migrations {
|
|||||||
}
|
}
|
||||||
if (oldVersion < 59) {
|
if (oldVersion < 59) {
|
||||||
// Reset rotation to Free after replacing Lock
|
// Reset rotation to Free after replacing Lock
|
||||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
|
||||||
if (prefs.contains("pref_rotation_type_key")) {
|
if (prefs.contains("pref_rotation_type_key")) {
|
||||||
prefs.edit {
|
prefs.edit {
|
||||||
putInt("pref_rotation_type_key", 1)
|
putInt("pref_rotation_type_key", 1)
|
||||||
@ -157,17 +157,16 @@ object Migrations {
|
|||||||
|
|
||||||
// Disable update check for Android 5.x users
|
// Disable update check for Android 5.x users
|
||||||
if (BuildConfig.INCLUDE_UPDATER && Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) {
|
if (BuildConfig.INCLUDE_UPDATER && Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) {
|
||||||
UpdaterJob.cancelTask(context)
|
AppUpdateJob.cancelTask(context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (oldVersion < 60) {
|
if (oldVersion < 60) {
|
||||||
// Re-enable update check that was prevously accidentally disabled for M
|
// Re-enable update check that was prevously accidentally disabled for M
|
||||||
if (BuildConfig.INCLUDE_UPDATER && Build.VERSION.SDK_INT == Build.VERSION_CODES.M) {
|
if (BuildConfig.INCLUDE_UPDATER && Build.VERSION.SDK_INT == Build.VERSION_CODES.M) {
|
||||||
UpdaterJob.setupTask(context)
|
AppUpdateJob.setupTask(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Migrate Rotation and Viewer values to default values for viewer_flags
|
// Migrate Rotation and Viewer values to default values for viewer_flags
|
||||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
|
||||||
val newOrientation = when (prefs.getInt("pref_rotation_type_key", 1)) {
|
val newOrientation = when (prefs.getInt("pref_rotation_type_key", 1)) {
|
||||||
1 -> OrientationType.FREE.flagValue
|
1 -> OrientationType.FREE.flagValue
|
||||||
2 -> OrientationType.PORTRAIT.flagValue
|
2 -> OrientationType.PORTRAIT.flagValue
|
||||||
@ -196,8 +195,6 @@ object Migrations {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (oldVersion < 64) {
|
if (oldVersion < 64) {
|
||||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
|
||||||
|
|
||||||
val oldSortingMode = prefs.getInt(PreferenceKeys.librarySortingMode, 0)
|
val oldSortingMode = prefs.getInt(PreferenceKeys.librarySortingMode, 0)
|
||||||
val oldSortingDirection = prefs.getBoolean(PreferenceKeys.librarySortingDirection, true)
|
val oldSortingDirection = prefs.getBoolean(PreferenceKeys.librarySortingDirection, true)
|
||||||
|
|
||||||
@ -229,6 +226,25 @@ object Migrations {
|
|||||||
putString(PreferenceKeys.librarySortingDirection, newSortingDirection.name)
|
putString(PreferenceKeys.librarySortingDirection, newSortingDirection.name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (oldVersion < 70) {
|
||||||
|
if (preferences.enabledLanguages().isSet()) {
|
||||||
|
preferences.enabledLanguages() += "all"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (oldVersion < 71) {
|
||||||
|
// Handle removed every 3, 4, 6, and 8 hour library updates
|
||||||
|
val updateInterval = preferences.libraryUpdateInterval().get()
|
||||||
|
if (updateInterval in listOf(3, 4, 6, 8)) {
|
||||||
|
preferences.libraryUpdateInterval().set(12)
|
||||||
|
LibraryUpdateJob.setupTask(context, 12)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (oldVersion < 72) {
|
||||||
|
val oldUpdateOngoingOnly = prefs.getBoolean("pref_update_only_non_completed_key", true)
|
||||||
|
if (!oldUpdateOngoingOnly) {
|
||||||
|
preferences.libraryUpdateMangaRestriction() -= MANGA_ONGOING
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -1,6 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.annotations
|
|
||||||
|
|
||||||
// TODO: remove this when no longer used in extensions
|
|
||||||
@Retention(AnnotationRetention.RUNTIME)
|
|
||||||
@Target(AnnotationTarget.CLASS)
|
|
||||||
annotation class Nsfw
|
|
@ -21,7 +21,7 @@ abstract class AbstractBackupManager(protected val context: Context) {
|
|||||||
internal val trackManager: TrackManager by injectLazy()
|
internal val trackManager: TrackManager by injectLazy()
|
||||||
protected val preferences: PreferencesHelper 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, isJob: Boolean): String
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns manga
|
* Returns manga
|
||||||
|
@ -14,3 +14,5 @@ abstract class AbstractBackupRestoreValidator {
|
|||||||
|
|
||||||
data class Results(val missingSources: List<String>, val missingTrackers: List<String>)
|
data class Results(val missingSources: List<String>, val missingTrackers: List<String>)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ValidatorParseException(e: Exception) : RuntimeException(e)
|
||||||
|
@ -9,6 +9,8 @@ import androidx.work.Worker
|
|||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
import eu.kanade.tachiyomi.data.backup.full.FullBackupManager
|
import eu.kanade.tachiyomi.data.backup.full.FullBackupManager
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
|
import logcat.LogPriority
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
@ -24,6 +26,7 @@ class BackupCreatorJob(private val context: Context, workerParams: WorkerParamet
|
|||||||
FullBackupManager(context).createBackup(uri, flags, true)
|
FullBackupManager(context).createBackup(uri, flags, true)
|
||||||
Result.success()
|
Result.success()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
logcat(LogPriority.ERROR, e)
|
||||||
Result.failure()
|
Result.failure()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -139,10 +139,12 @@ class BackupNotifier(private val context: Context) {
|
|||||||
val destFile = File(path, file)
|
val destFile = File(path, file)
|
||||||
val uri = destFile.getUriCompat(context)
|
val uri = destFile.getUriCompat(context)
|
||||||
|
|
||||||
|
val errorLogIntent = NotificationReceiver.openErrorLogPendingActivity(context, uri)
|
||||||
|
setContentIntent(errorLogIntent)
|
||||||
addAction(
|
addAction(
|
||||||
R.drawable.ic_folder_24dp,
|
R.drawable.ic_folder_24dp,
|
||||||
context.getString(R.string.action_show_errors),
|
context.getString(R.string.action_show_errors),
|
||||||
NotificationReceiver.openErrorLogPendingActivity(context, uri)
|
errorLogIntent,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -13,13 +13,14 @@ import eu.kanade.tachiyomi.data.backup.legacy.LegacyBackupRestore
|
|||||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
import eu.kanade.tachiyomi.util.system.acquireWakeLock
|
import eu.kanade.tachiyomi.util.system.acquireWakeLock
|
||||||
import eu.kanade.tachiyomi.util.system.isServiceRunning
|
import eu.kanade.tachiyomi.util.system.isServiceRunning
|
||||||
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import logcat.LogPriority
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Restores backup.
|
* Restores backup.
|
||||||
@ -128,7 +129,7 @@ class BackupRestoreService : Service() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val handler = CoroutineExceptionHandler { _, exception ->
|
val handler = CoroutineExceptionHandler { _, exception ->
|
||||||
Timber.e(exception)
|
logcat(LogPriority.ERROR, exception)
|
||||||
backupRestore?.writeErrorLog()
|
backupRestore?.writeErrorLog()
|
||||||
|
|
||||||
notifier.showRestoreError(exception.message)
|
notifier.showRestoreError(exception.message)
|
||||||
|
@ -26,11 +26,12 @@ import eu.kanade.tachiyomi.data.database.models.History
|
|||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaCategory
|
import eu.kanade.tachiyomi.data.database.models.MangaCategory
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
import kotlinx.serialization.protobuf.ProtoBuf
|
import kotlinx.serialization.protobuf.ProtoBuf
|
||||||
|
import logcat.LogPriority
|
||||||
import okio.buffer
|
import okio.buffer
|
||||||
import okio.gzip
|
import okio.gzip
|
||||||
import okio.sink
|
import okio.sink
|
||||||
import timber.log.Timber
|
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
|
|
||||||
class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
||||||
@ -43,7 +44,7 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
|||||||
* @param uri path of Uri
|
* @param uri path of Uri
|
||||||
* @param isJob backup called from job
|
* @param isJob backup called from job
|
||||||
*/
|
*/
|
||||||
override fun createBackup(uri: Uri, flags: Int, isJob: Boolean): String? {
|
override fun createBackup(uri: Uri, flags: Int, isJob: Boolean): String {
|
||||||
// Create root object
|
// Create root object
|
||||||
var backup: Backup? = null
|
var backup: Backup? = null
|
||||||
|
|
||||||
@ -58,8 +59,9 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var file: UniFile? = null
|
||||||
try {
|
try {
|
||||||
val file: UniFile = (
|
file = (
|
||||||
if (isJob) {
|
if (isJob) {
|
||||||
// Get dir of file and create
|
// Get dir of file and create
|
||||||
var dir = UniFile.fromUri(context, uri)
|
var dir = UniFile.fromUri(context, uri)
|
||||||
@ -84,9 +86,15 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
|||||||
|
|
||||||
val byteArray = parser.encodeToByteArray(BackupSerializer, backup!!)
|
val byteArray = parser.encodeToByteArray(BackupSerializer, backup!!)
|
||||||
file.openOutputStream().sink().gzip().buffer().use { it.write(byteArray) }
|
file.openOutputStream().sink().gzip().buffer().use { it.write(byteArray) }
|
||||||
return file.uri.toString()
|
val fileUri = file.uri
|
||||||
|
|
||||||
|
// Make sure it's a valid backup file
|
||||||
|
FullBackupRestoreValidator().validate(context, fileUri)
|
||||||
|
|
||||||
|
return fileUri.toString()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e)
|
logcat(LogPriority.ERROR, e)
|
||||||
|
file?.delete()
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,12 +4,14 @@ import android.content.Context
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.backup.AbstractBackupRestoreValidator
|
import eu.kanade.tachiyomi.data.backup.AbstractBackupRestoreValidator
|
||||||
|
import eu.kanade.tachiyomi.data.backup.ValidatorParseException
|
||||||
import eu.kanade.tachiyomi.data.backup.full.models.BackupSerializer
|
import eu.kanade.tachiyomi.data.backup.full.models.BackupSerializer
|
||||||
import okio.buffer
|
import okio.buffer
|
||||||
import okio.gzip
|
import okio.gzip
|
||||||
import okio.source
|
import okio.source
|
||||||
|
|
||||||
class FullBackupRestoreValidator : AbstractBackupRestoreValidator() {
|
class FullBackupRestoreValidator : AbstractBackupRestoreValidator() {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks for critical backup file data.
|
* Checks for critical backup file data.
|
||||||
*
|
*
|
||||||
@ -19,14 +21,20 @@ class FullBackupRestoreValidator : AbstractBackupRestoreValidator() {
|
|||||||
override fun validate(context: Context, uri: Uri): Results {
|
override fun validate(context: Context, uri: Uri): Results {
|
||||||
val backupManager = FullBackupManager(context)
|
val backupManager = FullBackupManager(context)
|
||||||
|
|
||||||
val backupString = context.contentResolver.openInputStream(uri)!!.source().gzip().buffer().use { it.readByteArray() }
|
val backup = try {
|
||||||
val backup = backupManager.parser.decodeFromByteArray(BackupSerializer, backupString)
|
val backupString =
|
||||||
|
context.contentResolver.openInputStream(uri)!!.source().gzip().buffer()
|
||||||
|
.use { it.readByteArray() }
|
||||||
|
backupManager.parser.decodeFromByteArray(BackupSerializer, backupString)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw ValidatorParseException(e)
|
||||||
|
}
|
||||||
|
|
||||||
if (backup.backupManga.isEmpty()) {
|
if (backup.backupManga.isEmpty()) {
|
||||||
throw Exception(context.getString(R.string.invalid_backup_file_missing_manga))
|
throw Exception(context.getString(R.string.invalid_backup_file_missing_manga))
|
||||||
}
|
}
|
||||||
|
|
||||||
val sources = backup.backupSources.map { it.sourceId to it.name }.toMap()
|
val sources = backup.backupSources.associate { it.sourceId to it.name }
|
||||||
val missingSources = sources
|
val missingSources = sources
|
||||||
.filter { sourceManager.get(it.key) == null }
|
.filter { sourceManager.get(it.key) == null }
|
||||||
.values
|
.values
|
||||||
|
@ -4,10 +4,12 @@ import android.content.Context
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.backup.AbstractBackupRestoreValidator
|
import eu.kanade.tachiyomi.data.backup.AbstractBackupRestoreValidator
|
||||||
|
import eu.kanade.tachiyomi.data.backup.ValidatorParseException
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup
|
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup
|
||||||
import kotlinx.serialization.json.decodeFromStream
|
import kotlinx.serialization.json.decodeFromStream
|
||||||
|
|
||||||
class LegacyBackupRestoreValidator : AbstractBackupRestoreValidator() {
|
class LegacyBackupRestoreValidator : AbstractBackupRestoreValidator() {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks for critical backup file data.
|
* Checks for critical backup file data.
|
||||||
*
|
*
|
||||||
@ -17,9 +19,13 @@ class LegacyBackupRestoreValidator : AbstractBackupRestoreValidator() {
|
|||||||
override fun validate(context: Context, uri: Uri): Results {
|
override fun validate(context: Context, uri: Uri): Results {
|
||||||
val backupManager = LegacyBackupManager(context)
|
val backupManager = LegacyBackupManager(context)
|
||||||
|
|
||||||
val backup = backupManager.parser.decodeFromStream<Backup>(
|
val backup = try {
|
||||||
context.contentResolver.openInputStream(uri)!!
|
backupManager.parser.decodeFromStream<Backup>(
|
||||||
)
|
context.contentResolver.openInputStream(uri)!!
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw ValidatorParseException(e)
|
||||||
|
}
|
||||||
|
|
||||||
if (backup.version == null) {
|
if (backup.version == null) {
|
||||||
throw Exception(context.getString(R.string.invalid_backup_file_missing_data))
|
throw Exception(context.getString(R.string.invalid_backup_file_missing_data))
|
||||||
@ -51,12 +57,10 @@ class LegacyBackupRestoreValidator : AbstractBackupRestoreValidator() {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun getSourceMapping(extensionsMapping: List<String>): Map<Long, String> {
|
fun getSourceMapping(extensionsMapping: List<String>): Map<Long, String> {
|
||||||
return extensionsMapping
|
return extensionsMapping.associate {
|
||||||
.map {
|
val items = it.split(":")
|
||||||
val items = it.split(":")
|
items[0].toLong() to items[1]
|
||||||
items[0].toLong() to items[1]
|
}
|
||||||
}
|
|
||||||
.toMap()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,7 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
|
|||||||
/**
|
/**
|
||||||
* Version of the database.
|
* Version of the database.
|
||||||
*/
|
*/
|
||||||
const val DATABASE_VERSION = 13
|
const val DATABASE_VERSION = 14
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate(db: SupportSQLiteDatabase) = with(db) {
|
override fun onCreate(db: SupportSQLiteDatabase) = with(db) {
|
||||||
@ -91,6 +91,9 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
|
|||||||
db.execSQL(TrackTable.insertFromTempTable)
|
db.execSQL(TrackTable.insertFromTempTable)
|
||||||
db.execSQL(TrackTable.dropTempTable)
|
db.execSQL(TrackTable.dropTempTable)
|
||||||
}
|
}
|
||||||
|
if (oldVersion < 14) {
|
||||||
|
db.execSQL(ChapterTable.fixDateUploadIfNeeded)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onConfigure(db: SupportSQLiteDatabase) {
|
override fun onConfigure(db: SupportSQLiteDatabase) {
|
||||||
|
@ -47,10 +47,10 @@ class CategoryPutResolver : DefaultPutResolver<Category>() {
|
|||||||
class CategoryGetResolver : DefaultGetResolver<Category>() {
|
class CategoryGetResolver : DefaultGetResolver<Category>() {
|
||||||
|
|
||||||
override fun mapFromCursor(cursor: Cursor): Category = CategoryImpl().apply {
|
override fun mapFromCursor(cursor: Cursor): Category = CategoryImpl().apply {
|
||||||
id = cursor.getInt(cursor.getColumnIndex(COL_ID))
|
id = cursor.getInt(cursor.getColumnIndexOrThrow(COL_ID))
|
||||||
name = cursor.getString(cursor.getColumnIndex(COL_NAME))
|
name = cursor.getString(cursor.getColumnIndexOrThrow(COL_NAME))
|
||||||
order = cursor.getInt(cursor.getColumnIndex(COL_ORDER))
|
order = cursor.getInt(cursor.getColumnIndexOrThrow(COL_ORDER))
|
||||||
flags = cursor.getInt(cursor.getColumnIndex(COL_FLAGS))
|
flags = cursor.getInt(cursor.getColumnIndexOrThrow(COL_FLAGS))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -63,18 +63,18 @@ class ChapterPutResolver : DefaultPutResolver<Chapter>() {
|
|||||||
class ChapterGetResolver : DefaultGetResolver<Chapter>() {
|
class ChapterGetResolver : DefaultGetResolver<Chapter>() {
|
||||||
|
|
||||||
override fun mapFromCursor(cursor: Cursor): Chapter = ChapterImpl().apply {
|
override fun mapFromCursor(cursor: Cursor): Chapter = ChapterImpl().apply {
|
||||||
id = cursor.getLong(cursor.getColumnIndex(COL_ID))
|
id = cursor.getLong(cursor.getColumnIndexOrThrow(COL_ID))
|
||||||
manga_id = cursor.getLong(cursor.getColumnIndex(COL_MANGA_ID))
|
manga_id = cursor.getLong(cursor.getColumnIndexOrThrow(COL_MANGA_ID))
|
||||||
url = cursor.getString(cursor.getColumnIndex(COL_URL))
|
url = cursor.getString(cursor.getColumnIndexOrThrow(COL_URL))
|
||||||
name = cursor.getString(cursor.getColumnIndex(COL_NAME))
|
name = cursor.getString(cursor.getColumnIndexOrThrow(COL_NAME))
|
||||||
scanlator = cursor.getString(cursor.getColumnIndex(COL_SCANLATOR))
|
scanlator = cursor.getString(cursor.getColumnIndexOrThrow(COL_SCANLATOR))
|
||||||
read = cursor.getInt(cursor.getColumnIndex(COL_READ)) == 1
|
read = cursor.getInt(cursor.getColumnIndexOrThrow(COL_READ)) == 1
|
||||||
bookmark = cursor.getInt(cursor.getColumnIndex(COL_BOOKMARK)) == 1
|
bookmark = cursor.getInt(cursor.getColumnIndexOrThrow(COL_BOOKMARK)) == 1
|
||||||
date_fetch = cursor.getLong(cursor.getColumnIndex(COL_DATE_FETCH))
|
date_fetch = cursor.getLong(cursor.getColumnIndexOrThrow(COL_DATE_FETCH))
|
||||||
date_upload = cursor.getLong(cursor.getColumnIndex(COL_DATE_UPLOAD))
|
date_upload = cursor.getLong(cursor.getColumnIndexOrThrow(COL_DATE_UPLOAD))
|
||||||
last_page_read = cursor.getInt(cursor.getColumnIndex(COL_LAST_PAGE_READ))
|
last_page_read = cursor.getInt(cursor.getColumnIndexOrThrow(COL_LAST_PAGE_READ))
|
||||||
chapter_number = cursor.getFloat(cursor.getColumnIndex(COL_CHAPTER_NUMBER))
|
chapter_number = cursor.getFloat(cursor.getColumnIndexOrThrow(COL_CHAPTER_NUMBER))
|
||||||
source_order = cursor.getInt(cursor.getColumnIndex(COL_SOURCE_ORDER))
|
source_order = cursor.getInt(cursor.getColumnIndexOrThrow(COL_SOURCE_ORDER))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,10 +47,10 @@ open class HistoryPutResolver : DefaultPutResolver<History>() {
|
|||||||
class HistoryGetResolver : DefaultGetResolver<History>() {
|
class HistoryGetResolver : DefaultGetResolver<History>() {
|
||||||
|
|
||||||
override fun mapFromCursor(cursor: Cursor): History = HistoryImpl().apply {
|
override fun mapFromCursor(cursor: Cursor): History = HistoryImpl().apply {
|
||||||
id = cursor.getLong(cursor.getColumnIndex(COL_ID))
|
id = cursor.getLong(cursor.getColumnIndexOrThrow(COL_ID))
|
||||||
chapter_id = cursor.getLong(cursor.getColumnIndex(COL_CHAPTER_ID))
|
chapter_id = cursor.getLong(cursor.getColumnIndexOrThrow(COL_CHAPTER_ID))
|
||||||
last_read = cursor.getLong(cursor.getColumnIndex(COL_LAST_READ))
|
last_read = cursor.getLong(cursor.getColumnIndexOrThrow(COL_LAST_READ))
|
||||||
time_read = cursor.getLong(cursor.getColumnIndex(COL_TIME_READ))
|
time_read = cursor.getLong(cursor.getColumnIndexOrThrow(COL_TIME_READ))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,9 +44,9 @@ class MangaCategoryPutResolver : DefaultPutResolver<MangaCategory>() {
|
|||||||
class MangaCategoryGetResolver : DefaultGetResolver<MangaCategory>() {
|
class MangaCategoryGetResolver : DefaultGetResolver<MangaCategory>() {
|
||||||
|
|
||||||
override fun mapFromCursor(cursor: Cursor): MangaCategory = MangaCategory().apply {
|
override fun mapFromCursor(cursor: Cursor): MangaCategory = MangaCategory().apply {
|
||||||
id = cursor.getLong(cursor.getColumnIndex(COL_ID))
|
id = cursor.getLong(cursor.getColumnIndexOrThrow(COL_ID))
|
||||||
manga_id = cursor.getLong(cursor.getColumnIndex(COL_MANGA_ID))
|
manga_id = cursor.getLong(cursor.getColumnIndexOrThrow(COL_MANGA_ID))
|
||||||
category_id = cursor.getInt(cursor.getColumnIndex(COL_CATEGORY_ID))
|
category_id = cursor.getInt(cursor.getColumnIndexOrThrow(COL_CATEGORY_ID))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,7 +22,6 @@ import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_GENRE
|
|||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_ID
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_ID
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_INITIALIZED
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_INITIALIZED
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_LAST_UPDATE
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_LAST_UPDATE
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_NEXT_UPDATE
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_SOURCE
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_SOURCE
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_STATUS
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_STATUS
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_THUMBNAIL_URL
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_THUMBNAIL_URL
|
||||||
@ -63,7 +62,6 @@ class MangaPutResolver : DefaultPutResolver<Manga>() {
|
|||||||
COL_THUMBNAIL_URL to obj.thumbnail_url,
|
COL_THUMBNAIL_URL to obj.thumbnail_url,
|
||||||
COL_FAVORITE to obj.favorite,
|
COL_FAVORITE to obj.favorite,
|
||||||
COL_LAST_UPDATE to obj.last_update,
|
COL_LAST_UPDATE to obj.last_update,
|
||||||
COL_NEXT_UPDATE to obj.next_update,
|
|
||||||
COL_INITIALIZED to obj.initialized,
|
COL_INITIALIZED to obj.initialized,
|
||||||
COL_VIEWER to obj.viewer_flags,
|
COL_VIEWER to obj.viewer_flags,
|
||||||
COL_CHAPTER_FLAGS to obj.chapter_flags,
|
COL_CHAPTER_FLAGS to obj.chapter_flags,
|
||||||
@ -74,24 +72,23 @@ class MangaPutResolver : DefaultPutResolver<Manga>() {
|
|||||||
|
|
||||||
interface BaseMangaGetResolver {
|
interface BaseMangaGetResolver {
|
||||||
fun mapBaseFromCursor(manga: Manga, cursor: Cursor) = manga.apply {
|
fun mapBaseFromCursor(manga: Manga, cursor: Cursor) = manga.apply {
|
||||||
id = cursor.getLong(cursor.getColumnIndex(COL_ID))
|
id = cursor.getLong(cursor.getColumnIndexOrThrow(COL_ID))
|
||||||
source = cursor.getLong(cursor.getColumnIndex(COL_SOURCE))
|
source = cursor.getLong(cursor.getColumnIndexOrThrow(COL_SOURCE))
|
||||||
url = cursor.getString(cursor.getColumnIndex(COL_URL))
|
url = cursor.getString(cursor.getColumnIndexOrThrow(COL_URL))
|
||||||
artist = cursor.getString(cursor.getColumnIndex(COL_ARTIST))
|
artist = cursor.getString(cursor.getColumnIndexOrThrow(COL_ARTIST))
|
||||||
author = cursor.getString(cursor.getColumnIndex(COL_AUTHOR))
|
author = cursor.getString(cursor.getColumnIndexOrThrow(COL_AUTHOR))
|
||||||
description = cursor.getString(cursor.getColumnIndex(COL_DESCRIPTION))
|
description = cursor.getString(cursor.getColumnIndexOrThrow(COL_DESCRIPTION))
|
||||||
genre = cursor.getString(cursor.getColumnIndex(COL_GENRE))
|
genre = cursor.getString(cursor.getColumnIndexOrThrow(COL_GENRE))
|
||||||
title = cursor.getString(cursor.getColumnIndex(COL_TITLE))
|
title = cursor.getString(cursor.getColumnIndexOrThrow(COL_TITLE))
|
||||||
status = cursor.getInt(cursor.getColumnIndex(COL_STATUS))
|
status = cursor.getInt(cursor.getColumnIndexOrThrow(COL_STATUS))
|
||||||
thumbnail_url = cursor.getString(cursor.getColumnIndex(COL_THUMBNAIL_URL))
|
thumbnail_url = cursor.getString(cursor.getColumnIndexOrThrow(COL_THUMBNAIL_URL))
|
||||||
favorite = cursor.getInt(cursor.getColumnIndex(COL_FAVORITE)) == 1
|
favorite = cursor.getInt(cursor.getColumnIndexOrThrow(COL_FAVORITE)) == 1
|
||||||
last_update = cursor.getLong(cursor.getColumnIndex(COL_LAST_UPDATE))
|
last_update = cursor.getLong(cursor.getColumnIndexOrThrow(COL_LAST_UPDATE))
|
||||||
next_update = cursor.getLong(cursor.getColumnIndex(COL_NEXT_UPDATE))
|
initialized = cursor.getInt(cursor.getColumnIndexOrThrow(COL_INITIALIZED)) == 1
|
||||||
initialized = cursor.getInt(cursor.getColumnIndex(COL_INITIALIZED)) == 1
|
viewer_flags = cursor.getInt(cursor.getColumnIndexOrThrow(COL_VIEWER))
|
||||||
viewer_flags = cursor.getInt(cursor.getColumnIndex(COL_VIEWER))
|
chapter_flags = cursor.getInt(cursor.getColumnIndexOrThrow(COL_CHAPTER_FLAGS))
|
||||||
chapter_flags = cursor.getInt(cursor.getColumnIndex(COL_CHAPTER_FLAGS))
|
cover_last_modified = cursor.getLong(cursor.getColumnIndexOrThrow(COL_COVER_LAST_MODIFIED))
|
||||||
cover_last_modified = cursor.getLong(cursor.getColumnIndex(COL_COVER_LAST_MODIFIED))
|
date_added = cursor.getLong(cursor.getColumnIndexOrThrow(COL_DATE_ADDED))
|
||||||
date_added = cursor.getLong(cursor.getColumnIndex(COL_DATE_ADDED))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -65,19 +65,19 @@ class TrackPutResolver : DefaultPutResolver<Track>() {
|
|||||||
class TrackGetResolver : DefaultGetResolver<Track>() {
|
class TrackGetResolver : DefaultGetResolver<Track>() {
|
||||||
|
|
||||||
override fun mapFromCursor(cursor: Cursor): Track = TrackImpl().apply {
|
override fun mapFromCursor(cursor: Cursor): Track = TrackImpl().apply {
|
||||||
id = cursor.getLong(cursor.getColumnIndex(COL_ID))
|
id = cursor.getLong(cursor.getColumnIndexOrThrow(COL_ID))
|
||||||
manga_id = cursor.getLong(cursor.getColumnIndex(COL_MANGA_ID))
|
manga_id = cursor.getLong(cursor.getColumnIndexOrThrow(COL_MANGA_ID))
|
||||||
sync_id = cursor.getInt(cursor.getColumnIndex(COL_SYNC_ID))
|
sync_id = cursor.getInt(cursor.getColumnIndexOrThrow(COL_SYNC_ID))
|
||||||
media_id = cursor.getInt(cursor.getColumnIndex(COL_MEDIA_ID))
|
media_id = cursor.getInt(cursor.getColumnIndexOrThrow(COL_MEDIA_ID))
|
||||||
library_id = cursor.getLong(cursor.getColumnIndex(COL_LIBRARY_ID))
|
library_id = cursor.getLong(cursor.getColumnIndexOrThrow(COL_LIBRARY_ID))
|
||||||
title = cursor.getString(cursor.getColumnIndex(COL_TITLE))
|
title = cursor.getString(cursor.getColumnIndexOrThrow(COL_TITLE))
|
||||||
last_chapter_read = cursor.getFloat(cursor.getColumnIndex(COL_LAST_CHAPTER_READ))
|
last_chapter_read = cursor.getFloat(cursor.getColumnIndexOrThrow(COL_LAST_CHAPTER_READ))
|
||||||
total_chapters = cursor.getInt(cursor.getColumnIndex(COL_TOTAL_CHAPTERS))
|
total_chapters = cursor.getInt(cursor.getColumnIndexOrThrow(COL_TOTAL_CHAPTERS))
|
||||||
status = cursor.getInt(cursor.getColumnIndex(COL_STATUS))
|
status = cursor.getInt(cursor.getColumnIndexOrThrow(COL_STATUS))
|
||||||
score = cursor.getFloat(cursor.getColumnIndex(COL_SCORE))
|
score = cursor.getFloat(cursor.getColumnIndexOrThrow(COL_SCORE))
|
||||||
tracking_url = cursor.getString(cursor.getColumnIndex(COL_TRACKING_URL))
|
tracking_url = cursor.getString(cursor.getColumnIndexOrThrow(COL_TRACKING_URL))
|
||||||
started_reading_date = cursor.getLong(cursor.getColumnIndex(COL_START_DATE))
|
started_reading_date = cursor.getLong(cursor.getColumnIndexOrThrow(COL_START_DATE))
|
||||||
finished_reading_date = cursor.getLong(cursor.getColumnIndex(COL_FINISH_DATE))
|
finished_reading_date = cursor.getLong(cursor.getColumnIndexOrThrow(COL_FINISH_DATE))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,9 +16,6 @@ interface Manga : SManga {
|
|||||||
// last time the chapter list changed in any way
|
// last time the chapter list changed in any way
|
||||||
var last_update: Long
|
var last_update: Long
|
||||||
|
|
||||||
// predicted next update time based on latest (by date) 4 chapters' deltas
|
|
||||||
var next_update: Long
|
|
||||||
|
|
||||||
var date_added: Long
|
var date_added: Long
|
||||||
|
|
||||||
var viewer_flags: Int
|
var viewer_flags: Int
|
||||||
|
@ -26,8 +26,6 @@ open class MangaImpl : Manga {
|
|||||||
|
|
||||||
override var last_update: Long = 0
|
override var last_update: Long = 0
|
||||||
|
|
||||||
override var next_update: Long = 0
|
|
||||||
|
|
||||||
override var date_added: Long = 0
|
override var date_added: Long = 0
|
||||||
|
|
||||||
override var initialized: Boolean = false
|
override var initialized: Boolean = false
|
||||||
|
@ -0,0 +1,3 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.database.models
|
||||||
|
|
||||||
|
data class SourceIdMangaCount(val source: Long, val count: Int)
|
@ -1,5 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.queries
|
package eu.kanade.tachiyomi.data.database.queries
|
||||||
|
|
||||||
|
import com.pushtorefresh.storio.Queries
|
||||||
import com.pushtorefresh.storio.sqlite.operations.get.PreparedGetListOfObjects
|
import com.pushtorefresh.storio.sqlite.operations.get.PreparedGetListOfObjects
|
||||||
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
||||||
import com.pushtorefresh.storio.sqlite.queries.Query
|
import com.pushtorefresh.storio.sqlite.queries.Query
|
||||||
@ -7,13 +8,14 @@ import com.pushtorefresh.storio.sqlite.queries.RawQuery
|
|||||||
import eu.kanade.tachiyomi.data.database.DbProvider
|
import eu.kanade.tachiyomi.data.database.DbProvider
|
||||||
import eu.kanade.tachiyomi.data.database.models.LibraryManga
|
import eu.kanade.tachiyomi.data.database.models.LibraryManga
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.SourceIdMangaCount
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.LibraryMangaGetResolver
|
import eu.kanade.tachiyomi.data.database.resolvers.LibraryMangaGetResolver
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaCoverLastModifiedPutResolver
|
import eu.kanade.tachiyomi.data.database.resolvers.MangaCoverLastModifiedPutResolver
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaFavoritePutResolver
|
import eu.kanade.tachiyomi.data.database.resolvers.MangaFavoritePutResolver
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaFlagsPutResolver
|
import eu.kanade.tachiyomi.data.database.resolvers.MangaFlagsPutResolver
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaLastUpdatedPutResolver
|
import eu.kanade.tachiyomi.data.database.resolvers.MangaLastUpdatedPutResolver
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaNextUpdatedPutResolver
|
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaTitlePutResolver
|
import eu.kanade.tachiyomi.data.database.resolvers.MangaTitlePutResolver
|
||||||
|
import eu.kanade.tachiyomi.data.database.resolvers.SourceIdMangaCountGetResolver
|
||||||
import eu.kanade.tachiyomi.data.database.tables.CategoryTable
|
import eu.kanade.tachiyomi.data.database.tables.CategoryTable
|
||||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable
|
import eu.kanade.tachiyomi.data.database.tables.ChapterTable
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable
|
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable
|
||||||
@ -70,6 +72,17 @@ interface MangaQueries : DbProvider {
|
|||||||
)
|
)
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
|
fun getSourceIdsWithNonLibraryManga() = db.get()
|
||||||
|
.listOfObjects(SourceIdMangaCount::class.java)
|
||||||
|
.withQuery(
|
||||||
|
RawQuery.builder()
|
||||||
|
.query(getSourceIdsWithNonLibraryMangaQuery())
|
||||||
|
.observesTables(MangaTable.TABLE)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
.withGetResolver(SourceIdMangaCountGetResolver.INSTANCE)
|
||||||
|
.prepare()
|
||||||
|
|
||||||
fun insertManga(manga: Manga) = db.put().`object`(manga).prepare()
|
fun insertManga(manga: Manga) = db.put().`object`(manga).prepare()
|
||||||
|
|
||||||
fun insertMangas(mangas: List<Manga>) = db.put().objects(mangas).prepare()
|
fun insertMangas(mangas: List<Manga>) = db.put().objects(mangas).prepare()
|
||||||
@ -94,11 +107,6 @@ interface MangaQueries : DbProvider {
|
|||||||
.withPutResolver(MangaFlagsPutResolver(MangaTable.COL_VIEWER, Manga::viewer_flags))
|
.withPutResolver(MangaFlagsPutResolver(MangaTable.COL_VIEWER, Manga::viewer_flags))
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
fun updateNextUpdated(manga: Manga) = db.put()
|
|
||||||
.`object`(manga)
|
|
||||||
.withPutResolver(MangaNextUpdatedPutResolver())
|
|
||||||
.prepare()
|
|
||||||
|
|
||||||
fun updateLastUpdated(manga: Manga) = db.put()
|
fun updateLastUpdated(manga: Manga) = db.put()
|
||||||
.`object`(manga)
|
.`object`(manga)
|
||||||
.withPutResolver(MangaLastUpdatedPutResolver())
|
.withPutResolver(MangaLastUpdatedPutResolver())
|
||||||
@ -123,12 +131,12 @@ interface MangaQueries : DbProvider {
|
|||||||
|
|
||||||
fun deleteMangas(mangas: List<Manga>) = db.delete().objects(mangas).prepare()
|
fun deleteMangas(mangas: List<Manga>) = db.delete().objects(mangas).prepare()
|
||||||
|
|
||||||
fun deleteMangasNotInLibrary() = db.delete()
|
fun deleteMangasNotInLibraryBySourceIds(sourceIds: List<Long>) = db.delete()
|
||||||
.byQuery(
|
.byQuery(
|
||||||
DeleteQuery.builder()
|
DeleteQuery.builder()
|
||||||
.table(MangaTable.TABLE)
|
.table(MangaTable.TABLE)
|
||||||
.where("${MangaTable.COL_FAVORITE} = ?")
|
.where("${MangaTable.COL_FAVORITE} = ? AND ${MangaTable.COL_SOURCE} IN (${Queries.placeholders(sourceIds.size)})")
|
||||||
.whereArgs(0)
|
.whereArgs(0, *sourceIds.toTypedArray())
|
||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
.prepare()
|
.prepare()
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.queries
|
package eu.kanade.tachiyomi.data.database.queries
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.database.resolvers.SourceIdMangaCountGetResolver
|
||||||
import eu.kanade.tachiyomi.data.database.tables.CategoryTable as Category
|
import eu.kanade.tachiyomi.data.database.tables.CategoryTable as Category
|
||||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable as Chapter
|
import eu.kanade.tachiyomi.data.database.tables.ChapterTable as Chapter
|
||||||
import eu.kanade.tachiyomi.data.database.tables.HistoryTable as History
|
import eu.kanade.tachiyomi.data.database.tables.HistoryTable as History
|
||||||
@ -142,3 +143,14 @@ fun getCategoriesForMangaQuery() =
|
|||||||
${MangaCategory.TABLE}.${MangaCategory.COL_CATEGORY_ID}
|
${MangaCategory.TABLE}.${MangaCategory.COL_CATEGORY_ID}
|
||||||
WHERE ${MangaCategory.COL_MANGA_ID} = ?
|
WHERE ${MangaCategory.COL_MANGA_ID} = ?
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
/** Query to get the list of sources in the database that have
|
||||||
|
* non-library manga, and how many
|
||||||
|
*/
|
||||||
|
fun getSourceIdsWithNonLibraryMangaQuery() =
|
||||||
|
"""
|
||||||
|
SELECT ${Manga.COL_SOURCE}, COUNT(*) as ${SourceIdMangaCountGetResolver.COL_COUNT}
|
||||||
|
FROM ${Manga.TABLE}
|
||||||
|
WHERE ${Manga.COL_FAVORITE} = 0
|
||||||
|
GROUP BY ${Manga.COL_SOURCE}
|
||||||
|
"""
|
||||||
|
@ -16,8 +16,8 @@ class LibraryMangaGetResolver : DefaultGetResolver<LibraryManga>(), BaseMangaGet
|
|||||||
val manga = LibraryManga()
|
val manga = LibraryManga()
|
||||||
|
|
||||||
mapBaseFromCursor(manga, cursor)
|
mapBaseFromCursor(manga, cursor)
|
||||||
manga.unread = cursor.getInt(cursor.getColumnIndex(MangaTable.COL_UNREAD))
|
manga.unread = cursor.getInt(cursor.getColumnIndexOrThrow(MangaTable.COL_UNREAD))
|
||||||
manga.category = cursor.getInt(cursor.getColumnIndex(MangaTable.COL_CATEGORY))
|
manga.category = cursor.getInt(cursor.getColumnIndexOrThrow(MangaTable.COL_CATEGORY))
|
||||||
|
|
||||||
return manga
|
return manga
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,7 @@ class MangaChapterGetResolver : DefaultGetResolver<MangaChapter>() {
|
|||||||
val manga = mangaGetResolver.mapFromCursor(cursor)
|
val manga = mangaGetResolver.mapFromCursor(cursor)
|
||||||
val chapter = chapterGetResolver.mapFromCursor(cursor)
|
val chapter = chapterGetResolver.mapFromCursor(cursor)
|
||||||
manga.id = chapter.manga_id
|
manga.id = chapter.manga_id
|
||||||
manga.url = cursor.getString(cursor.getColumnIndex("mangaUrl"))
|
manga.url = cursor.getString(cursor.getColumnIndexOrThrow("mangaUrl"))
|
||||||
|
|
||||||
return MangaChapter(manga, chapter)
|
return MangaChapter(manga, chapter)
|
||||||
}
|
}
|
||||||
|
@ -42,7 +42,7 @@ class MangaChapterHistoryGetResolver : DefaultGetResolver<MangaChapterHistory>()
|
|||||||
|
|
||||||
// Make certain column conflicts are dealt with
|
// Make certain column conflicts are dealt with
|
||||||
manga.id = chapter.manga_id
|
manga.id = chapter.manga_id
|
||||||
manga.url = cursor.getString(cursor.getColumnIndex("mangaUrl"))
|
manga.url = cursor.getString(cursor.getColumnIndexOrThrow("mangaUrl"))
|
||||||
chapter.id = history.chapter_id
|
chapter.id = history.chapter_id
|
||||||
|
|
||||||
// Return result
|
// Return result
|
||||||
|
@ -1,31 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.resolvers
|
|
||||||
|
|
||||||
import androidx.core.content.contentValuesOf
|
|
||||||
import com.pushtorefresh.storio.sqlite.StorIOSQLite
|
|
||||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResolver
|
|
||||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
|
|
||||||
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
|
|
||||||
import eu.kanade.tachiyomi.data.database.inTransactionReturn
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
|
||||||
|
|
||||||
class MangaNextUpdatedPutResolver : PutResolver<Manga>() {
|
|
||||||
|
|
||||||
override fun performPut(db: StorIOSQLite, manga: Manga) = db.inTransactionReturn {
|
|
||||||
val updateQuery = mapToUpdateQuery(manga)
|
|
||||||
val contentValues = mapToContentValues(manga)
|
|
||||||
|
|
||||||
val numberOfRowsUpdated = db.lowLevel().update(updateQuery, contentValues)
|
|
||||||
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
|
|
||||||
}
|
|
||||||
|
|
||||||
fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder()
|
|
||||||
.table(MangaTable.TABLE)
|
|
||||||
.where("${MangaTable.COL_ID} = ?")
|
|
||||||
.whereArgs(manga.id)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
fun mapToContentValues(manga: Manga) = contentValuesOf(
|
|
||||||
MangaTable.COL_NEXT_UPDATE to manga.next_update
|
|
||||||
)
|
|
||||||
}
|
|
@ -0,0 +1,23 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.database.resolvers
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.database.Cursor
|
||||||
|
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.SourceIdMangaCount
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
||||||
|
|
||||||
|
class SourceIdMangaCountGetResolver : DefaultGetResolver<SourceIdMangaCount>() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val INSTANCE = SourceIdMangaCountGetResolver()
|
||||||
|
const val COL_COUNT = "manga_count"
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("Range")
|
||||||
|
override fun mapFromCursor(cursor: Cursor): SourceIdMangaCount {
|
||||||
|
val sourceID = cursor.getLong(cursor.getColumnIndexOrThrow(MangaTable.COL_SOURCE))
|
||||||
|
val count = cursor.getInt(cursor.getColumnIndexOrThrow(COL_COUNT))
|
||||||
|
|
||||||
|
return SourceIdMangaCount(sourceID, count)
|
||||||
|
}
|
||||||
|
}
|
@ -62,4 +62,7 @@ object ChapterTable {
|
|||||||
|
|
||||||
val addScanlator: String
|
val addScanlator: String
|
||||||
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_SCANLATOR TEXT DEFAULT NULL"
|
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_SCANLATOR TEXT DEFAULT NULL"
|
||||||
|
|
||||||
|
val fixDateUploadIfNeeded: String
|
||||||
|
get() = "UPDATE $TABLE SET $COL_DATE_UPLOAD = $COL_DATE_FETCH WHERE $COL_DATE_UPLOAD = 0"
|
||||||
}
|
}
|
||||||
|
@ -28,6 +28,7 @@ object MangaTable {
|
|||||||
|
|
||||||
const val COL_LAST_UPDATE = "last_update"
|
const val COL_LAST_UPDATE = "last_update"
|
||||||
|
|
||||||
|
// Not actually used anymore
|
||||||
const val COL_NEXT_UPDATE = "next_update"
|
const val COL_NEXT_UPDATE = "next_update"
|
||||||
|
|
||||||
const val COL_DATE_ADDED = "date_added"
|
const val COL_DATE_ADDED = "date_added"
|
||||||
|
@ -143,7 +143,7 @@ class DownloadCache(
|
|||||||
mangaDirs.values.forEach { mangaDir ->
|
mangaDirs.values.forEach { mangaDir ->
|
||||||
val chapterDirs = mangaDir.dir.listFiles()
|
val chapterDirs = mangaDir.dir.listFiles()
|
||||||
.orEmpty()
|
.orEmpty()
|
||||||
.mapNotNull { it.name }
|
.mapNotNull { it.name?.replace(".cbz", "") }
|
||||||
.toHashSet()
|
.toHashSet()
|
||||||
|
|
||||||
mangaDir.files = chapterDirs
|
mangaDir.files = chapterDirs
|
||||||
|
@ -14,8 +14,9 @@ import eu.kanade.tachiyomi.source.Source
|
|||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||||
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
|
import logcat.LogPriority
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import timber.log.Timber
|
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
@ -38,7 +39,7 @@ class DownloadManager(
|
|||||||
/**
|
/**
|
||||||
* Downloads provider, used to retrieve the folders where the chapters are or should be stored.
|
* Downloads provider, used to retrieve the folders where the chapters are or should be stored.
|
||||||
*/
|
*/
|
||||||
private val provider = DownloadProvider(context)
|
val provider = DownloadProvider(context)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cache of downloaded chapters.
|
* Cache of downloaded chapters.
|
||||||
@ -326,19 +327,23 @@ class DownloadManager(
|
|||||||
*/
|
*/
|
||||||
fun renameChapter(source: Source, manga: Manga, oldChapter: Chapter, newChapter: Chapter) {
|
fun renameChapter(source: Source, manga: Manga, oldChapter: Chapter, newChapter: Chapter) {
|
||||||
val oldNames = provider.getValidChapterDirNames(oldChapter)
|
val oldNames = provider.getValidChapterDirNames(oldChapter)
|
||||||
val newName = provider.getChapterDirName(newChapter)
|
|
||||||
val mangaDir = provider.getMangaDir(manga, source)
|
val mangaDir = provider.getMangaDir(manga, source)
|
||||||
|
|
||||||
// Assume there's only 1 version of the chapter name formats present
|
// Assume there's only 1 version of the chapter name formats present
|
||||||
val oldFolder = oldNames.asSequence()
|
val oldDownload = oldNames.asSequence()
|
||||||
.mapNotNull { mangaDir.findFile(it) }
|
.mapNotNull { mangaDir.findFile(it) }
|
||||||
.firstOrNull()
|
.firstOrNull() ?: return
|
||||||
|
|
||||||
if (oldFolder?.renameTo(newName) == true) {
|
var newName = provider.getChapterDirName(newChapter)
|
||||||
|
if (oldDownload.isFile && oldDownload.name?.endsWith(".cbz") == true) {
|
||||||
|
newName += ".cbz"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldDownload.renameTo(newName)) {
|
||||||
cache.removeChapter(oldChapter, manga)
|
cache.removeChapter(oldChapter, manga)
|
||||||
cache.addChapter(newName, mangaDir, manga)
|
cache.addChapter(newName, mangaDir, manga)
|
||||||
} else {
|
} else {
|
||||||
Timber.e("Could not rename downloaded chapter: %s.", oldNames.joinToString())
|
logcat(LogPriority.ERROR) { "Could not rename downloaded chapter: ${oldNames.joinToString()}." }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -105,6 +105,7 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
|
|
||||||
if (preferences.hideNotificationContent()) {
|
if (preferences.hideNotificationContent()) {
|
||||||
setContentTitle(downloadingProgressText)
|
setContentTitle(downloadingProgressText)
|
||||||
|
setContentText(null)
|
||||||
} else {
|
} else {
|
||||||
val title = download.manga.title.chop(15)
|
val title = download.manga.title.chop(15)
|
||||||
val quotedTitle = Pattern.quote(title)
|
val quotedTitle = Pattern.quote(title)
|
||||||
@ -187,8 +188,8 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
fun onWarning(reason: String) {
|
fun onWarning(reason: String) {
|
||||||
with(errorNotificationBuilder) {
|
with(errorNotificationBuilder) {
|
||||||
setContentTitle(context.getString(R.string.download_notifier_downloader_title))
|
setContentTitle(context.getString(R.string.download_notifier_downloader_title))
|
||||||
setContentText(reason)
|
setStyle(NotificationCompat.BigTextStyle().bigText(reason))
|
||||||
setSmallIcon(android.R.drawable.stat_sys_warning)
|
setSmallIcon(R.drawable.ic_warning_white_24dp)
|
||||||
setAutoCancel(true)
|
setAutoCancel(true)
|
||||||
clearActions()
|
clearActions()
|
||||||
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
||||||
@ -208,15 +209,14 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
* @param error string containing error information.
|
* @param error string containing error information.
|
||||||
* @param chapter string containing chapter title.
|
* @param chapter string containing chapter title.
|
||||||
*/
|
*/
|
||||||
fun onError(error: String? = null, chapter: String? = null) {
|
fun onError(error: String? = null, chapter: String? = null, mangaTitle: String? = null) {
|
||||||
// Create notification
|
// Create notification
|
||||||
with(errorNotificationBuilder) {
|
with(errorNotificationBuilder) {
|
||||||
setContentTitle(
|
setContentTitle(
|
||||||
chapter
|
mangaTitle?.plus(": $chapter") ?: context.getString(R.string.download_notifier_downloader_title)
|
||||||
?: context.getString(R.string.download_notifier_downloader_title)
|
|
||||||
)
|
)
|
||||||
setContentText(error ?: context.getString(R.string.download_notifier_unknown_error))
|
setContentText(error ?: context.getString(R.string.download_notifier_unknown_error))
|
||||||
setSmallIcon(android.R.drawable.stat_sys_warning)
|
setSmallIcon(R.drawable.ic_warning_white_24dp)
|
||||||
clearActions()
|
clearActions()
|
||||||
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
||||||
setProgress(0, 0, false)
|
setProgress(0, 0, false)
|
||||||
|
@ -9,10 +9,11 @@ import eu.kanade.tachiyomi.data.database.models.Manga
|
|||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||||
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
import kotlinx.coroutines.MainScope
|
import kotlinx.coroutines.MainScope
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import timber.log.Timber
|
import logcat.LogPriority
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -54,7 +55,7 @@ class DownloadProvider(private val context: Context) {
|
|||||||
.createDirectory(getSourceDirName(source))
|
.createDirectory(getSourceDirName(source))
|
||||||
.createDirectory(getMangaDirName(manga))
|
.createDirectory(getMangaDirName(manga))
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Timber.e(e, "Invalid download directory")
|
logcat(LogPriority.ERROR, e) { "Invalid download directory" }
|
||||||
throw Exception(context.getString(R.string.invalid_download_dir))
|
throw Exception(context.getString(R.string.invalid_download_dir))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -147,10 +148,14 @@ class DownloadProvider(private val context: Context) {
|
|||||||
* @param chapter the chapter to query.
|
* @param chapter the chapter to query.
|
||||||
*/
|
*/
|
||||||
fun getValidChapterDirNames(chapter: Chapter): List<String> {
|
fun getValidChapterDirNames(chapter: Chapter): List<String> {
|
||||||
|
val chapterName = getChapterDirName(chapter)
|
||||||
return listOf(
|
return listOf(
|
||||||
getChapterDirName(chapter),
|
// Folder of images
|
||||||
|
chapterName,
|
||||||
|
|
||||||
|
// Archived chapters
|
||||||
|
"$chapterName.cbz",
|
||||||
|
|
||||||
// TODO: remove this
|
|
||||||
// Legacy chapter directory name used in v0.9.2 and before
|
// Legacy chapter directory name used in v0.9.2 and before
|
||||||
DiskUtil.buildValidFilename(chapter.name)
|
DiskUtil.buildValidFilename(chapter.name)
|
||||||
)
|
)
|
||||||
|
@ -18,6 +18,7 @@ import eu.kanade.tachiyomi.util.system.acquireWakeLock
|
|||||||
import eu.kanade.tachiyomi.util.system.isConnectedToWifi
|
import eu.kanade.tachiyomi.util.system.isConnectedToWifi
|
||||||
import eu.kanade.tachiyomi.util.system.isOnline
|
import eu.kanade.tachiyomi.util.system.isOnline
|
||||||
import eu.kanade.tachiyomi.util.system.isServiceRunning
|
import eu.kanade.tachiyomi.util.system.isServiceRunning
|
||||||
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
import eu.kanade.tachiyomi.util.system.notification
|
import eu.kanade.tachiyomi.util.system.notification
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
@ -27,9 +28,9 @@ import kotlinx.coroutines.cancel
|
|||||||
import kotlinx.coroutines.flow.catch
|
import kotlinx.coroutines.flow.catch
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import logcat.LogPriority
|
||||||
import ru.beryukhov.reactivenetwork.ReactiveNetwork
|
import ru.beryukhov.reactivenetwork.ReactiveNetwork
|
||||||
import rx.subscriptions.CompositeSubscription
|
import rx.subscriptions.CompositeSubscription
|
||||||
import timber.log.Timber
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -143,7 +144,7 @@ class DownloadService : Service() {
|
|||||||
}
|
}
|
||||||
.catch { error ->
|
.catch { error ->
|
||||||
withUIContext {
|
withUIContext {
|
||||||
Timber.e(error)
|
logcat(LogPriority.ERROR, error)
|
||||||
toast(R.string.download_queue_error)
|
toast(R.string.download_queue_error)
|
||||||
stopSelf()
|
stopSelf()
|
||||||
}
|
}
|
||||||
@ -175,13 +176,15 @@ class DownloadService : Service() {
|
|||||||
* Listens to downloader status. Enables or disables the wake lock depending on the status.
|
* Listens to downloader status. Enables or disables the wake lock depending on the status.
|
||||||
*/
|
*/
|
||||||
private fun listenDownloaderState() {
|
private fun listenDownloaderState() {
|
||||||
subscriptions += downloadManager.runningRelay.subscribe { running ->
|
subscriptions += downloadManager.runningRelay
|
||||||
if (running) {
|
.doOnError { /* Swallow wakelock error */ }
|
||||||
wakeLock.acquireIfNeeded()
|
.subscribe { running ->
|
||||||
} else {
|
if (running) {
|
||||||
wakeLock.releaseIfNeeded()
|
wakeLock.acquireIfNeeded()
|
||||||
|
} else {
|
||||||
|
wakeLock.releaseIfNeeded()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -11,7 +11,9 @@ import eu.kanade.tachiyomi.data.database.models.Chapter
|
|||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.download.model.Download
|
import eu.kanade.tachiyomi.data.download.model.Download
|
||||||
import eu.kanade.tachiyomi.data.download.model.DownloadQueue
|
import eu.kanade.tachiyomi.data.download.model.DownloadQueue
|
||||||
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
|
import eu.kanade.tachiyomi.source.UnmeteredSource
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.source.online.fetchAllImageUrlsFromPageList
|
import eu.kanade.tachiyomi.source.online.fetchAllImageUrlsFromPageList
|
||||||
@ -22,15 +24,20 @@ import eu.kanade.tachiyomi.util.lang.plusAssign
|
|||||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||||
import eu.kanade.tachiyomi.util.storage.saveTo
|
import eu.kanade.tachiyomi.util.storage.saveTo
|
||||||
import eu.kanade.tachiyomi.util.system.ImageUtil
|
import eu.kanade.tachiyomi.util.system.ImageUtil
|
||||||
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
|
import logcat.LogPriority
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
import rx.schedulers.Schedulers
|
import rx.schedulers.Schedulers
|
||||||
import rx.subscriptions.CompositeSubscription
|
import rx.subscriptions.CompositeSubscription
|
||||||
import timber.log.Timber
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import java.io.BufferedOutputStream
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.util.zip.CRC32
|
||||||
|
import java.util.zip.ZipEntry
|
||||||
|
import java.util.zip.ZipOutputStream
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class is the one in charge of downloading chapters.
|
* This class is the one in charge of downloading chapters.
|
||||||
@ -55,6 +62,8 @@ class Downloader(
|
|||||||
|
|
||||||
private val chapterCache: ChapterCache by injectLazy()
|
private val chapterCache: ChapterCache by injectLazy()
|
||||||
|
|
||||||
|
private val preferences: PreferencesHelper by injectLazy()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Store for persisting downloads across restarts.
|
* Store for persisting downloads across restarts.
|
||||||
*/
|
*/
|
||||||
@ -209,7 +218,7 @@ class Downloader(
|
|||||||
},
|
},
|
||||||
{ error ->
|
{ error ->
|
||||||
DownloadService.stop(context)
|
DownloadService.stop(context)
|
||||||
Timber.e(error)
|
logcat(LogPriority.ERROR, error)
|
||||||
notifier.onError(error.message)
|
notifier.onError(error.message)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -262,7 +271,17 @@ class Downloader(
|
|||||||
|
|
||||||
// Start downloader if needed
|
// Start downloader if needed
|
||||||
if (autoStart && wasEmpty) {
|
if (autoStart && wasEmpty) {
|
||||||
DownloadService.start(this@Downloader.context)
|
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)
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
DownloadService.start(context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -278,7 +297,7 @@ class Downloader(
|
|||||||
val availSpace = DiskUtil.getAvailableStorageSpace(mangaDir)
|
val availSpace = DiskUtil.getAvailableStorageSpace(mangaDir)
|
||||||
if (availSpace != -1L && availSpace < MIN_DISK_SPACE) {
|
if (availSpace != -1L && availSpace < MIN_DISK_SPACE) {
|
||||||
download.status = Download.State.ERROR
|
download.status = Download.State.ERROR
|
||||||
notifier.onError(context.getString(R.string.download_insufficient_space), download.chapter.name)
|
notifier.onError(context.getString(R.string.download_insufficient_space), download.chapter.name, download.manga.title)
|
||||||
return@defer Observable.just(download)
|
return@defer Observable.just(download)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -324,7 +343,7 @@ class Downloader(
|
|||||||
// If the page list threw, it will resume here
|
// If the page list threw, it will resume here
|
||||||
.onErrorReturn { error ->
|
.onErrorReturn { error ->
|
||||||
download.status = Download.State.ERROR
|
download.status = Download.State.ERROR
|
||||||
notifier.onError(error.message, download.chapter.name)
|
notifier.onError(error.message, download.chapter.name, download.manga.title)
|
||||||
download
|
download
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -470,13 +489,51 @@ class Downloader(
|
|||||||
|
|
||||||
// Only rename the directory if it's downloaded.
|
// Only rename the directory if it's downloaded.
|
||||||
if (download.status == Download.State.DOWNLOADED) {
|
if (download.status == Download.State.DOWNLOADED) {
|
||||||
tmpDir.renameTo(dirname)
|
if (preferences.saveChaptersAsCBZ().get()) {
|
||||||
|
archiveChapter(mangaDir, dirname, tmpDir)
|
||||||
|
} else {
|
||||||
|
tmpDir.renameTo(dirname)
|
||||||
|
}
|
||||||
cache.addChapter(dirname, mangaDir, download.manga)
|
cache.addChapter(dirname, mangaDir, download.manga)
|
||||||
|
|
||||||
DiskUtil.createNoMediaFile(tmpDir, context)
|
DiskUtil.createNoMediaFile(tmpDir, context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Archive the chapter pages as a CBZ.
|
||||||
|
*/
|
||||||
|
private fun archiveChapter(
|
||||||
|
mangaDir: UniFile,
|
||||||
|
dirname: String,
|
||||||
|
tmpDir: UniFile,
|
||||||
|
) {
|
||||||
|
val zip = mangaDir.createFile("$dirname.cbz.tmp")
|
||||||
|
ZipOutputStream(BufferedOutputStream(zip.openOutputStream())).use { zipOut ->
|
||||||
|
zipOut.setMethod(ZipEntry.STORED)
|
||||||
|
|
||||||
|
tmpDir.listFiles()?.forEach { img ->
|
||||||
|
img.openInputStream().use { input ->
|
||||||
|
val data = input.readBytes()
|
||||||
|
val size = img.length()
|
||||||
|
val entry = ZipEntry(img.name).apply {
|
||||||
|
val crc = CRC32().apply {
|
||||||
|
update(data)
|
||||||
|
}
|
||||||
|
setCrc(crc.value)
|
||||||
|
|
||||||
|
compressedSize = size
|
||||||
|
setSize(size)
|
||||||
|
}
|
||||||
|
zipOut.putNextEntry(entry)
|
||||||
|
zipOut.write(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
zip.renameTo("$dirname.cbz")
|
||||||
|
tmpDir.delete()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Completes a download. This method is called in the main thread.
|
* Completes a download. This method is called in the main thread.
|
||||||
*/
|
*/
|
||||||
@ -500,8 +557,9 @@ class Downloader(
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val TMP_DIR_SUFFIX = "_tmp"
|
const val TMP_DIR_SUFFIX = "_tmp"
|
||||||
|
const val CHAPTERS_PER_SOURCE_QUEUE_WARNING_THRESHOLD = 15
|
||||||
// Arbitrary minimum required space to start a download: 50 MB
|
|
||||||
const val MIN_DISK_SPACE = 50 * 1024 * 1024
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Arbitrary minimum required space to start a download: 50 MB
|
||||||
|
private const val MIN_DISK_SPACE = 50 * 1024 * 1024
|
||||||
|
@ -8,9 +8,10 @@ import androidx.work.PeriodicWorkRequestBuilder
|
|||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
import androidx.work.Worker
|
import androidx.work.Worker
|
||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
import eu.kanade.tachiyomi.data.preference.CHARGING
|
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.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.data.preference.UNMETERED_NETWORK
|
import eu.kanade.tachiyomi.util.system.isConnectedToWifi
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
@ -19,6 +20,11 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
|||||||
Worker(context, workerParams) {
|
Worker(context, workerParams) {
|
||||||
|
|
||||||
override fun doWork(): Result {
|
override fun doWork(): Result {
|
||||||
|
val preferences = Injekt.get<PreferencesHelper>()
|
||||||
|
if (requiresWifiConnection(preferences) && !context.isConnectedToWifi()) {
|
||||||
|
Result.failure()
|
||||||
|
}
|
||||||
|
|
||||||
return if (LibraryUpdateService.start(context)) {
|
return if (LibraryUpdateService.start(context)) {
|
||||||
Result.success()
|
Result.success()
|
||||||
} else {
|
} else {
|
||||||
@ -33,17 +39,10 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
|||||||
val preferences = Injekt.get<PreferencesHelper>()
|
val preferences = Injekt.get<PreferencesHelper>()
|
||||||
val interval = prefInterval ?: preferences.libraryUpdateInterval().get()
|
val interval = prefInterval ?: preferences.libraryUpdateInterval().get()
|
||||||
if (interval > 0) {
|
if (interval > 0) {
|
||||||
val restrictions = preferences.libraryUpdateRestriction().get()
|
val restrictions = preferences.libraryUpdateDeviceRestriction().get()
|
||||||
val acRestriction = CHARGING in restrictions
|
|
||||||
val wifiRestriction = if (UNMETERED_NETWORK in restrictions) {
|
|
||||||
NetworkType.UNMETERED
|
|
||||||
} else {
|
|
||||||
NetworkType.CONNECTED
|
|
||||||
}
|
|
||||||
|
|
||||||
val constraints = Constraints.Builder()
|
val constraints = Constraints.Builder()
|
||||||
.setRequiredNetworkType(wifiRestriction)
|
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||||
.setRequiresCharging(acRestriction)
|
.setRequiresCharging(DEVICE_CHARGING in restrictions)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val request = PeriodicWorkRequestBuilder<LibraryUpdateJob>(
|
val request = PeriodicWorkRequestBuilder<LibraryUpdateJob>(
|
||||||
@ -61,5 +60,10 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
|||||||
WorkManager.getInstance(context).cancelAllWorkByTag(TAG)
|
WorkManager.getInstance(context).cancelAllWorkByTag(TAG)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun requiresWifiConnection(preferences: PreferencesHelper): Boolean {
|
||||||
|
val restrictions = preferences.libraryUpdateDeviceRestriction().get()
|
||||||
|
return DEVICE_ONLY_ON_WIFI in restrictions
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,7 @@ import coil.transform.CircleCropTransformation
|
|||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.data.download.Downloader
|
||||||
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
||||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
@ -103,23 +104,10 @@ class LibraryUpdateNotifier(private val context: Context) {
|
|||||||
Notifications.ID_LIBRARY_ERROR,
|
Notifications.ID_LIBRARY_ERROR,
|
||||||
context.notificationBuilder(Notifications.CHANNEL_LIBRARY_ERROR) {
|
context.notificationBuilder(Notifications.CHANNEL_LIBRARY_ERROR) {
|
||||||
setContentTitle(context.resources.getQuantityString(R.plurals.notification_update_error, errors.size, errors.size))
|
setContentTitle(context.resources.getQuantityString(R.plurals.notification_update_error, errors.size, errors.size))
|
||||||
setStyle(
|
setContentText(context.getString(R.string.action_show_errors))
|
||||||
NotificationCompat.BigTextStyle().bigText(
|
|
||||||
errors.joinToString("\n") {
|
|
||||||
it.chop(NOTIF_TITLE_MAX_LEN)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
setSmallIcon(R.drawable.ic_tachi)
|
setSmallIcon(R.drawable.ic_tachi)
|
||||||
|
|
||||||
val errorLogIntent = NotificationReceiver.openErrorLogPendingActivity(context, uri)
|
setContentIntent(NotificationReceiver.openErrorLogPendingActivity(context, uri))
|
||||||
|
|
||||||
setContentIntent(errorLogIntent)
|
|
||||||
addAction(
|
|
||||||
R.drawable.ic_folder_24dp,
|
|
||||||
context.getString(R.string.action_show_errors),
|
|
||||||
errorLogIntent
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
@ -225,6 +213,20 @@ class LibraryUpdateNotifier(private val context: Context) {
|
|||||||
Notifications.ID_NEW_CHAPTERS
|
Notifications.ID_NEW_CHAPTERS
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
// Download chapters action
|
||||||
|
// Only add the action when chapters is within threshold
|
||||||
|
if (chapters.size <= Downloader.CHAPTERS_PER_SOURCE_QUEUE_WARNING_THRESHOLD) {
|
||||||
|
addAction(
|
||||||
|
android.R.drawable.stat_sys_download_done,
|
||||||
|
context.getString(R.string.action_download),
|
||||||
|
NotificationReceiver.downloadChaptersPendingBroadcast(
|
||||||
|
context,
|
||||||
|
manga,
|
||||||
|
chapters,
|
||||||
|
Notifications.ID_NEW_CHAPTERS
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,58 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.library
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
|
||||||
import java.util.Collections
|
|
||||||
import kotlin.math.abs
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This class will provide various functions to rank manga to efficiently schedule manga to update.
|
|
||||||
*/
|
|
||||||
object LibraryUpdateRanker {
|
|
||||||
|
|
||||||
val rankingScheme = listOf(
|
|
||||||
(this::lexicographicRanking)(),
|
|
||||||
(this::latestFirstRanking)(),
|
|
||||||
(this::nextFirstRanking)()
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provides a total ordering over all the Mangas.
|
|
||||||
*
|
|
||||||
* Orders the manga based on the distance between the next expected update and now.
|
|
||||||
* The comparator is reversed, placing the smallest (and thus closest to updating now) first.
|
|
||||||
*/
|
|
||||||
fun nextFirstRanking(): Comparator<Manga> {
|
|
||||||
val time = System.currentTimeMillis()
|
|
||||||
return Collections.reverseOrder(
|
|
||||||
Comparator { mangaFirst: Manga,
|
|
||||||
mangaSecond: Manga ->
|
|
||||||
compareValues(abs(mangaSecond.next_update - time), abs(mangaFirst.next_update - time))
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provides a total ordering over all the [Manga]s.
|
|
||||||
*
|
|
||||||
* Assumption: An active [Manga] mActive is expected to have been last updated after an
|
|
||||||
* inactive [Manga] mInactive.
|
|
||||||
*
|
|
||||||
* Using this insight, function returns a Comparator for which mActive appears before mInactive.
|
|
||||||
* @return a Comparator that ranks manga based on relevance.
|
|
||||||
*/
|
|
||||||
private fun latestFirstRanking(): Comparator<Manga> =
|
|
||||||
Comparator { first: Manga, second: Manga ->
|
|
||||||
compareValues(second.last_update, first.last_update)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provides a total ordering over all the [Manga]s.
|
|
||||||
*
|
|
||||||
* Order the manga lexicographically.
|
|
||||||
* @return a Comparator that ranks manga lexicographically based on the title.
|
|
||||||
*/
|
|
||||||
private fun lexicographicRanking(): Comparator<Manga> =
|
|
||||||
Comparator { first: Manga, second: Manga ->
|
|
||||||
compareValues(first.title, second.title)
|
|
||||||
}
|
|
||||||
}
|
|
@ -16,14 +16,16 @@ import eu.kanade.tachiyomi.data.database.models.Manga
|
|||||||
import eu.kanade.tachiyomi.data.database.models.toMangaInfo
|
import eu.kanade.tachiyomi.data.database.models.toMangaInfo
|
||||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||||
import eu.kanade.tachiyomi.data.download.DownloadService
|
import eu.kanade.tachiyomi.data.download.DownloadService
|
||||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateRanker.rankingScheme
|
|
||||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Companion.start
|
import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Companion.start
|
||||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
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.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.data.track.EnhancedTrackService
|
import eu.kanade.tachiyomi.data.track.EnhancedTrackService
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
import eu.kanade.tachiyomi.data.track.TrackService
|
import eu.kanade.tachiyomi.data.track.TrackService
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
|
import eu.kanade.tachiyomi.source.UnmeteredSource
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.model.toSChapter
|
import eu.kanade.tachiyomi.source.model.toSChapter
|
||||||
import eu.kanade.tachiyomi.source.model.toSManga
|
import eu.kanade.tachiyomi.source.model.toSManga
|
||||||
@ -37,10 +39,10 @@ import eu.kanade.tachiyomi.util.storage.getUriCompat
|
|||||||
import eu.kanade.tachiyomi.util.system.acquireWakeLock
|
import eu.kanade.tachiyomi.util.system.acquireWakeLock
|
||||||
import eu.kanade.tachiyomi.util.system.createFileInCacheDir
|
import eu.kanade.tachiyomi.util.system.createFileInCacheDir
|
||||||
import eu.kanade.tachiyomi.util.system.isServiceRunning
|
import eu.kanade.tachiyomi.util.system.isServiceRunning
|
||||||
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
@ -50,7 +52,7 @@ import kotlinx.coroutines.launch
|
|||||||
import kotlinx.coroutines.supervisorScope
|
import kotlinx.coroutines.supervisorScope
|
||||||
import kotlinx.coroutines.sync.Semaphore
|
import kotlinx.coroutines.sync.Semaphore
|
||||||
import kotlinx.coroutines.sync.withPermit
|
import kotlinx.coroutines.sync.withPermit
|
||||||
import timber.log.Timber
|
import logcat.LogPriority
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@ -212,7 +214,7 @@ class LibraryUpdateService(
|
|||||||
|
|
||||||
// Destroy service when completed or in case of an error.
|
// Destroy service when completed or in case of an error.
|
||||||
val handler = CoroutineExceptionHandler { _, exception ->
|
val handler = CoroutineExceptionHandler { _, exception ->
|
||||||
Timber.e(exception)
|
logcat(LogPriority.ERROR, exception)
|
||||||
stopSelf(startId)
|
stopSelf(startId)
|
||||||
}
|
}
|
||||||
updateJob = ioScope.launch(handler) {
|
updateJob = ioScope.launch(handler) {
|
||||||
@ -255,14 +257,30 @@ class LibraryUpdateService(
|
|||||||
|
|
||||||
listToInclude.minus(listToExclude)
|
listToInclude.minus(listToExclude)
|
||||||
}
|
}
|
||||||
if (target == Target.CHAPTERS && preferences.updateOnlyNonCompleted()) {
|
|
||||||
listToUpdate = listToUpdate.filterNot { it.status == SManga.COMPLETED }
|
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 }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val selectedScheme = preferences.libraryUpdatePrioritization().get()
|
|
||||||
mangaToUpdate = listToUpdate
|
mangaToUpdate = listToUpdate
|
||||||
.distinctBy { it.id }
|
.distinctBy { it.id }
|
||||||
.sortedWith(rankingScheme[selectedScheme])
|
.sortedBy { it.title }
|
||||||
|
|
||||||
|
// Warn when excessively checking a single source
|
||||||
|
val maxUpdatesFromSource = mangaToUpdate
|
||||||
|
.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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -282,6 +300,7 @@ class LibraryUpdateService(
|
|||||||
val failedUpdates = CopyOnWriteArrayList<Pair<Manga, String?>>()
|
val failedUpdates = CopyOnWriteArrayList<Pair<Manga, String?>>()
|
||||||
val hasDownloads = AtomicBoolean(false)
|
val hasDownloads = AtomicBoolean(false)
|
||||||
val loggedServices by lazy { trackManager.services.filter { it.isLogged } }
|
val loggedServices by lazy { trackManager.services.filter { it.isLogged } }
|
||||||
|
val currentUnreadUpdatesCount = preferences.unreadUpdatesCount().get()
|
||||||
|
|
||||||
withIOContext {
|
withIOContext {
|
||||||
mangaToUpdate.groupBy { it.source }
|
mangaToUpdate.groupBy { it.source }
|
||||||
@ -345,6 +364,8 @@ class LibraryUpdateService(
|
|||||||
|
|
||||||
if (newUpdates.isNotEmpty()) {
|
if (newUpdates.isNotEmpty()) {
|
||||||
notifier.showUpdateNotifications(newUpdates)
|
notifier.showUpdateNotifications(newUpdates)
|
||||||
|
val newChapterCount = newUpdates.sumOf { it.second.size }
|
||||||
|
preferences.unreadUpdatesCount().set(currentUnreadUpdatesCount + newChapterCount)
|
||||||
if (hasDownloads.get()) {
|
if (hasDownloads.get()) {
|
||||||
DownloadService.start(this)
|
DownloadService.start(this)
|
||||||
}
|
}
|
||||||
@ -374,24 +395,19 @@ class LibraryUpdateService(
|
|||||||
suspend fun updateManga(manga: Manga): Pair<List<Chapter>, List<Chapter>> {
|
suspend fun updateManga(manga: Manga): Pair<List<Chapter>, List<Chapter>> {
|
||||||
val source = sourceManager.getOrStub(manga.source)
|
val source = sourceManager.getOrStub(manga.source)
|
||||||
|
|
||||||
// Update manga details metadata in the background
|
// Update manga details metadata
|
||||||
if (preferences.autoUpdateMetadata()) {
|
if (preferences.autoUpdateMetadata()) {
|
||||||
val handler = CoroutineExceptionHandler { _, exception ->
|
val updatedManga = source.getMangaDetails(manga.toMangaInfo())
|
||||||
Timber.e(exception)
|
val sManga = updatedManga.toSManga()
|
||||||
|
// Avoid "losing" existing cover
|
||||||
|
if (!sManga.thumbnail_url.isNullOrEmpty()) {
|
||||||
|
manga.prepUpdateCover(coverCache, sManga, false)
|
||||||
|
} else {
|
||||||
|
sManga.thumbnail_url = manga.thumbnail_url
|
||||||
}
|
}
|
||||||
GlobalScope.launch(Dispatchers.IO + handler) {
|
|
||||||
val updatedManga = source.getMangaDetails(manga.toMangaInfo())
|
|
||||||
val sManga = updatedManga.toSManga()
|
|
||||||
// Avoid "losing" existing cover
|
|
||||||
if (!sManga.thumbnail_url.isNullOrEmpty()) {
|
|
||||||
manga.prepUpdateCover(coverCache, sManga, false)
|
|
||||||
} else {
|
|
||||||
sManga.thumbnail_url = manga.thumbnail_url
|
|
||||||
}
|
|
||||||
|
|
||||||
manga.copyFrom(sManga)
|
manga.copyFrom(sManga)
|
||||||
db.insertManga(manga).executeAsBlocking()
|
db.insertManga(manga).executeAsBlocking()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val chapters = source.getChapterList(manga.toMangaInfo())
|
val chapters = source.getChapterList(manga.toMangaInfo())
|
||||||
@ -433,17 +449,9 @@ class LibraryUpdateService(
|
|||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
// Ignore errors and continue
|
// Ignore errors and continue
|
||||||
Timber.e(e)
|
logcat(LogPriority.ERROR, e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
currentlyUpdatingManga.remove(manga)
|
|
||||||
progressCount.andIncrement
|
|
||||||
notifier.showProgressNotification(
|
|
||||||
currentlyUpdatingManga,
|
|
||||||
progressCount.get(),
|
|
||||||
mangaToUpdate.size
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -494,7 +502,7 @@ class LibraryUpdateService(
|
|||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
// Ignore errors and continue
|
// Ignore errors and continue
|
||||||
Timber.e(e)
|
logcat(LogPriority.ERROR, e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -543,12 +551,13 @@ class LibraryUpdateService(
|
|||||||
if (errors.isNotEmpty()) {
|
if (errors.isNotEmpty()) {
|
||||||
val file = createFileInCacheDir("tachiyomi_update_errors.txt")
|
val file = createFileInCacheDir("tachiyomi_update_errors.txt")
|
||||||
file.bufferedWriter().use { out ->
|
file.bufferedWriter().use { out ->
|
||||||
|
out.write(getString(R.string.library_errors_help, ERROR_LOG_HELP_URL) + "\n\n")
|
||||||
// Error file format:
|
// Error file format:
|
||||||
// ! Error
|
// ! Error
|
||||||
// # Source
|
// # Source
|
||||||
// - Manga
|
// - Manga
|
||||||
errors.groupBy({ it.second }, { it.first }).forEach { (error, mangas) ->
|
errors.groupBy({ it.second }, { it.first }).forEach { (error, mangas) ->
|
||||||
out.write("! ${error}\n")
|
out.write("\n! ${error}\n")
|
||||||
mangas.groupBy { it.source }.forEach { (srcId, mangas) ->
|
mangas.groupBy { it.source }.forEach { (srcId, mangas) ->
|
||||||
val source = sourceManager.getOrStub(srcId)
|
val source = sourceManager.getOrStub(srcId)
|
||||||
out.write(" # $source\n")
|
out.write(" # $source\n")
|
||||||
@ -566,3 +575,6 @@ class LibraryUpdateService(
|
|||||||
return File("")
|
return File("")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private const val MANGA_PER_SOURCE_QUEUE_WARNING_THRESHOLD = 60
|
||||||
|
private const val ERROR_LOG_HELP_URL = "https://tachiyomi.org/help/guides/troubleshooting"
|
||||||
|
@ -20,10 +20,10 @@ object NotificationHandler {
|
|||||||
*/
|
*/
|
||||||
internal fun openDownloadManagerPendingActivity(context: Context): PendingIntent {
|
internal fun openDownloadManagerPendingActivity(context: Context): PendingIntent {
|
||||||
val intent = Intent(context, MainActivity::class.java).apply {
|
val intent = Intent(context, MainActivity::class.java).apply {
|
||||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_REORDER_TO_FRONT
|
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
|
||||||
action = MainActivity.SHORTCUT_DOWNLOADS
|
action = MainActivity.SHORTCUT_DOWNLOADS
|
||||||
}
|
}
|
||||||
return PendingIntent.getActivity(context, 0, intent, 0)
|
return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -102,6 +102,18 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
markAsRead(urls, mangaId)
|
markAsRead(urls, mangaId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Download manga chapters
|
||||||
|
ACTION_DOWNLOAD_CHAPTER -> {
|
||||||
|
val notificationId = intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)
|
||||||
|
if (notificationId > -1) {
|
||||||
|
dismissNotification(context, notificationId, intent.getIntExtra(EXTRA_GROUP_ID, 0))
|
||||||
|
}
|
||||||
|
val urls = intent.getStringArrayExtra(EXTRA_CHAPTER_URL) ?: return
|
||||||
|
val mangaId = intent.getLongExtra(EXTRA_MANGA_ID, -1)
|
||||||
|
if (mangaId > -1) {
|
||||||
|
downloadChapters(urls, mangaId)
|
||||||
|
}
|
||||||
|
}
|
||||||
// Share crash dump file
|
// Share crash dump file
|
||||||
ACTION_SHARE_CRASH_LOG ->
|
ACTION_SHARE_CRASH_LOG ->
|
||||||
shareFile(
|
shareFile(
|
||||||
@ -235,6 +247,24 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method called when user wants to download chapters
|
||||||
|
*
|
||||||
|
* @param chapterUrls URLs of chapter to download
|
||||||
|
* @param mangaId id of manga
|
||||||
|
*/
|
||||||
|
private fun downloadChapters(chapterUrls: Array<String>, mangaId: Long) {
|
||||||
|
val db: DatabaseHelper = Injekt.get()
|
||||||
|
|
||||||
|
launchIO {
|
||||||
|
val chapters = chapterUrls.mapNotNull { db.getChapter(it, mangaId).executeAsBlocking() }
|
||||||
|
val manga = db.getManga(mangaId).executeAsBlocking()
|
||||||
|
if (chapters.isNotEmpty() && manga != null) {
|
||||||
|
downloadManager.downloadChapters(manga, chapters)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val NAME = "NotificationReceiver"
|
private const val NAME = "NotificationReceiver"
|
||||||
|
|
||||||
@ -251,6 +281,7 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
|
|
||||||
private const val ACTION_MARK_AS_READ = "$ID.$NAME.MARK_AS_READ"
|
private const val ACTION_MARK_AS_READ = "$ID.$NAME.MARK_AS_READ"
|
||||||
private const val ACTION_OPEN_CHAPTER = "$ID.$NAME.ACTION_OPEN_CHAPTER"
|
private const val ACTION_OPEN_CHAPTER = "$ID.$NAME.ACTION_OPEN_CHAPTER"
|
||||||
|
private const val ACTION_DOWNLOAD_CHAPTER = "$ID.$NAME.ACTION_DOWNLOAD_CHAPTER"
|
||||||
|
|
||||||
private const val ACTION_RESUME_DOWNLOADS = "$ID.$NAME.ACTION_RESUME_DOWNLOADS"
|
private const val ACTION_RESUME_DOWNLOADS = "$ID.$NAME.ACTION_RESUME_DOWNLOADS"
|
||||||
private const val ACTION_PAUSE_DOWNLOADS = "$ID.$NAME.ACTION_PAUSE_DOWNLOADS"
|
private const val ACTION_PAUSE_DOWNLOADS = "$ID.$NAME.ACTION_PAUSE_DOWNLOADS"
|
||||||
@ -442,6 +473,28 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
return PendingIntent.getBroadcast(context, manga.id.hashCode(), newIntent, PendingIntent.FLAG_UPDATE_CURRENT)
|
return PendingIntent.getBroadcast(context, manga.id.hashCode(), newIntent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns [PendingIntent] that downloads chapters
|
||||||
|
*
|
||||||
|
* @param context context of application
|
||||||
|
* @param manga manga of chapter
|
||||||
|
*/
|
||||||
|
internal fun downloadChaptersPendingBroadcast(
|
||||||
|
context: Context,
|
||||||
|
manga: Manga,
|
||||||
|
chapters: Array<Chapter>,
|
||||||
|
groupId: Int
|
||||||
|
): PendingIntent {
|
||||||
|
val newIntent = Intent(context, NotificationReceiver::class.java).apply {
|
||||||
|
action = ACTION_DOWNLOAD_CHAPTER
|
||||||
|
putExtra(EXTRA_CHAPTER_URL, chapters.map { it.url }.toTypedArray())
|
||||||
|
putExtra(EXTRA_MANGA_ID, manga.id)
|
||||||
|
putExtra(EXTRA_NOTIFICATION_ID, manga.id.hashCode())
|
||||||
|
putExtra(EXTRA_GROUP_ID, groupId)
|
||||||
|
}
|
||||||
|
return PendingIntent.getBroadcast(context, manga.id.hashCode(), newIntent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns [PendingIntent] that starts a service which stops the library update
|
* Returns [PendingIntent] that starts a service which stops the library update
|
||||||
*
|
*
|
||||||
|
@ -18,7 +18,6 @@ object Notifications {
|
|||||||
* Common notification channel and ids used anywhere.
|
* Common notification channel and ids used anywhere.
|
||||||
*/
|
*/
|
||||||
const val CHANNEL_COMMON = "common_channel"
|
const val CHANNEL_COMMON = "common_channel"
|
||||||
const val ID_UPDATER = 1
|
|
||||||
const val ID_DOWNLOAD_IMAGE = 2
|
const val ID_DOWNLOAD_IMAGE = 2
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -48,13 +47,6 @@ object Notifications {
|
|||||||
const val ID_NEW_CHAPTERS = -301
|
const val ID_NEW_CHAPTERS = -301
|
||||||
const val GROUP_NEW_CHAPTERS = "eu.kanade.tachiyomi.NEW_CHAPTERS"
|
const val GROUP_NEW_CHAPTERS = "eu.kanade.tachiyomi.NEW_CHAPTERS"
|
||||||
|
|
||||||
/**
|
|
||||||
* Notification channel and ids used by the library updater.
|
|
||||||
*/
|
|
||||||
const val CHANNEL_UPDATES_TO_EXTS = "updates_ext_channel"
|
|
||||||
const val ID_UPDATES_TO_EXTS = -401
|
|
||||||
const val ID_EXTENSION_INSTALLER = -402
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Notification channel and ids used by the backup/restore system.
|
* Notification channel and ids used by the backup/restore system.
|
||||||
*/
|
*/
|
||||||
@ -78,10 +70,22 @@ object Notifications {
|
|||||||
const val CHANNEL_INCOGNITO_MODE = "incognito_mode_channel"
|
const val CHANNEL_INCOGNITO_MODE = "incognito_mode_channel"
|
||||||
const val ID_INCOGNITO_MODE = -701
|
const val ID_INCOGNITO_MODE = -701
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notification channel and ids used for app and extension updates.
|
||||||
|
*/
|
||||||
|
private const val GROUP_APK_UPDATES = "group_apk_updates"
|
||||||
|
const val CHANNEL_APP_UPDATE = "app_apk_update_channel"
|
||||||
|
const val ID_APP_UPDATER = 1
|
||||||
|
const val CHANNEL_EXTENSIONS_UPDATE = "ext_apk_update_channel"
|
||||||
|
const val ID_UPDATES_TO_EXTS = -401
|
||||||
|
const val ID_EXTENSION_INSTALLER = -402
|
||||||
|
|
||||||
private val deprecatedChannels = listOf(
|
private val deprecatedChannels = listOf(
|
||||||
"downloader_channel",
|
"downloader_channel",
|
||||||
"backup_restore_complete_channel",
|
"backup_restore_complete_channel",
|
||||||
"library_channel",
|
"library_channel",
|
||||||
|
"library_progress_channel",
|
||||||
|
"updates_ext_channel",
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -93,6 +97,9 @@ object Notifications {
|
|||||||
fun createChannels(context: Context) {
|
fun createChannels(context: Context) {
|
||||||
val notificationService = NotificationManagerCompat.from(context)
|
val notificationService = NotificationManagerCompat.from(context)
|
||||||
|
|
||||||
|
// Delete old notification channels
|
||||||
|
deprecatedChannels.forEach(notificationService::deleteNotificationChannel)
|
||||||
|
|
||||||
notificationService.createNotificationChannelGroupsCompat(
|
notificationService.createNotificationChannelGroupsCompat(
|
||||||
listOf(
|
listOf(
|
||||||
buildNotificationChannelGroup(GROUP_BACKUP_RESTORE) {
|
buildNotificationChannelGroup(GROUP_BACKUP_RESTORE) {
|
||||||
@ -104,6 +111,9 @@ object Notifications {
|
|||||||
buildNotificationChannelGroup(GROUP_LIBRARY) {
|
buildNotificationChannelGroup(GROUP_LIBRARY) {
|
||||||
setName(context.getString(R.string.label_library))
|
setName(context.getString(R.string.label_library))
|
||||||
},
|
},
|
||||||
|
buildNotificationChannelGroup(GROUP_APK_UPDATES) {
|
||||||
|
setName(context.getString(R.string.label_recent_updates))
|
||||||
|
},
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -157,13 +167,15 @@ object Notifications {
|
|||||||
buildNotificationChannel(CHANNEL_INCOGNITO_MODE, IMPORTANCE_LOW) {
|
buildNotificationChannel(CHANNEL_INCOGNITO_MODE, IMPORTANCE_LOW) {
|
||||||
setName(context.getString(R.string.pref_incognito_mode))
|
setName(context.getString(R.string.pref_incognito_mode))
|
||||||
},
|
},
|
||||||
buildNotificationChannel(CHANNEL_UPDATES_TO_EXTS, IMPORTANCE_DEFAULT) {
|
buildNotificationChannel(CHANNEL_APP_UPDATE, IMPORTANCE_DEFAULT) {
|
||||||
|
setGroup(GROUP_APK_UPDATES)
|
||||||
|
setName(context.getString(R.string.channel_app_updates))
|
||||||
|
},
|
||||||
|
buildNotificationChannel(CHANNEL_EXTENSIONS_UPDATE, IMPORTANCE_DEFAULT) {
|
||||||
|
setGroup(GROUP_APK_UPDATES)
|
||||||
setName(context.getString(R.string.channel_ext_updates))
|
setName(context.getString(R.string.channel_ext_updates))
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
// Delete old notification channels
|
|
||||||
deprecatedChannels.forEach(notificationService::deleteNotificationChannel)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,141 +5,28 @@ package eu.kanade.tachiyomi.data.preference
|
|||||||
*/
|
*/
|
||||||
object PreferenceKeys {
|
object PreferenceKeys {
|
||||||
|
|
||||||
const val themeMode = "pref_theme_mode_key"
|
|
||||||
|
|
||||||
const val appTheme = "pref_app_theme"
|
|
||||||
|
|
||||||
const val themeDarkAmoled = "pref_theme_dark_amoled_key"
|
|
||||||
|
|
||||||
const val confirmExit = "pref_confirm_exit"
|
const val confirmExit = "pref_confirm_exit"
|
||||||
|
|
||||||
const val hideBottomBarOnScroll = "pref_hide_bottom_bar_on_scroll"
|
|
||||||
|
|
||||||
const val sideNavIconAlignment = "pref_side_nav_icon_alignment"
|
|
||||||
|
|
||||||
const val enableTransitions = "pref_enable_transitions_key"
|
|
||||||
|
|
||||||
const val doubleTapAnimationSpeed = "pref_double_tap_anim_speed"
|
|
||||||
|
|
||||||
const val showPageNumber = "pref_show_page_number_key"
|
|
||||||
|
|
||||||
const val dualPageSplitPaged = "pref_dual_page_split"
|
|
||||||
|
|
||||||
const val dualPageSplitWebtoon = "pref_dual_page_split_webtoon"
|
|
||||||
|
|
||||||
const val dualPageInvertPaged = "pref_dual_page_invert"
|
|
||||||
|
|
||||||
const val dualPageInvertWebtoon = "pref_dual_page_invert_webtoon"
|
|
||||||
|
|
||||||
const val showReadingMode = "pref_show_reading_mode"
|
const val showReadingMode = "pref_show_reading_mode"
|
||||||
|
|
||||||
const val trueColor = "pref_true_color_key"
|
|
||||||
|
|
||||||
const val fullscreen = "fullscreen"
|
|
||||||
|
|
||||||
const val cutoutShort = "cutout_short"
|
|
||||||
|
|
||||||
const val keepScreenOn = "pref_keep_screen_on_key"
|
|
||||||
|
|
||||||
const val customBrightness = "pref_custom_brightness_key"
|
|
||||||
|
|
||||||
const val customBrightnessValue = "custom_brightness_value"
|
|
||||||
|
|
||||||
const val colorFilter = "pref_color_filter_key"
|
|
||||||
|
|
||||||
const val colorFilterValue = "color_filter_value"
|
|
||||||
|
|
||||||
const val colorFilterMode = "color_filter_mode"
|
|
||||||
|
|
||||||
const val grayscale = "pref_grayscale"
|
|
||||||
|
|
||||||
const val invertedColors = "pref_inverted_colors"
|
|
||||||
|
|
||||||
const val defaultReadingMode = "pref_default_reading_mode_key"
|
const val defaultReadingMode = "pref_default_reading_mode_key"
|
||||||
|
|
||||||
const val defaultOrientationType = "pref_default_orientation_type_key"
|
const val defaultOrientationType = "pref_default_orientation_type_key"
|
||||||
|
|
||||||
const val imageScaleType = "pref_image_scale_type_key"
|
|
||||||
|
|
||||||
const val zoomStart = "pref_zoom_start_key"
|
|
||||||
|
|
||||||
const val readerTheme = "pref_reader_theme_key"
|
|
||||||
|
|
||||||
const val cropBorders = "crop_borders"
|
|
||||||
|
|
||||||
const val cropBordersWebtoon = "crop_borders_webtoon"
|
|
||||||
|
|
||||||
const val readWithTapping = "reader_tap"
|
|
||||||
|
|
||||||
const val pagerNavInverted = "reader_tapping_inverted"
|
|
||||||
|
|
||||||
const val webtoonNavInverted = "reader_tapping_inverted_webtoon"
|
|
||||||
|
|
||||||
const val readWithLongTap = "reader_long_tap"
|
|
||||||
|
|
||||||
const val readWithVolumeKeys = "reader_volume_keys"
|
|
||||||
|
|
||||||
const val readWithVolumeKeysInverted = "reader_volume_keys_inverted"
|
|
||||||
|
|
||||||
const val navigationModePager = "reader_navigation_mode_pager"
|
|
||||||
|
|
||||||
const val navigationModeWebtoon = "reader_navigation_mode_webtoon"
|
|
||||||
|
|
||||||
const val showNavigationOverlayNewUser = "reader_navigation_overlay_new_user"
|
|
||||||
|
|
||||||
const val showNavigationOverlayOnStart = "reader_navigation_overlay_on_start"
|
|
||||||
|
|
||||||
const val readerHideThreshold = "reader_hide_threshold"
|
|
||||||
|
|
||||||
const val webtoonSidePadding = "webtoon_side_padding"
|
|
||||||
|
|
||||||
const val portraitColumns = "pref_library_columns_portrait_key"
|
|
||||||
|
|
||||||
const val landscapeColumns = "pref_library_columns_landscape_key"
|
|
||||||
|
|
||||||
const val jumpToChapters = "jump_to_chapters"
|
const val jumpToChapters = "jump_to_chapters"
|
||||||
|
|
||||||
const val updateOnlyNonCompleted = "pref_update_only_non_completed_key"
|
|
||||||
|
|
||||||
const val autoUpdateTrack = "pref_auto_update_manga_sync_key"
|
const val autoUpdateTrack = "pref_auto_update_manga_sync_key"
|
||||||
|
|
||||||
const val lastUsedSource = "last_catalogue_source"
|
|
||||||
|
|
||||||
const val lastUsedCategory = "last_used_category"
|
|
||||||
|
|
||||||
const val sourceDisplayMode = "pref_display_mode_catalogue"
|
|
||||||
|
|
||||||
const val enabledLanguages = "source_languages"
|
|
||||||
|
|
||||||
const val backupDirectory = "backup_directory"
|
|
||||||
|
|
||||||
const val downloadsDirectory = "download_directory"
|
|
||||||
|
|
||||||
const val downloadOnlyOverWifi = "pref_download_only_over_wifi_key"
|
const val downloadOnlyOverWifi = "pref_download_only_over_wifi_key"
|
||||||
|
|
||||||
const val folderPerManga = "create_folder_per_manga"
|
const val folderPerManga = "create_folder_per_manga"
|
||||||
|
|
||||||
const val numberOfBackups = "backup_slots"
|
|
||||||
|
|
||||||
const val backupInterval = "backup_interval"
|
|
||||||
|
|
||||||
const val removeAfterReadSlots = "remove_after_read_slots"
|
const val removeAfterReadSlots = "remove_after_read_slots"
|
||||||
|
|
||||||
const val removeAfterMarkedAsRead = "pref_remove_after_marked_as_read_key"
|
const val removeAfterMarkedAsRead = "pref_remove_after_marked_as_read_key"
|
||||||
|
|
||||||
const val removeBookmarkedChapters = "pref_remove_bookmarked"
|
const val removeBookmarkedChapters = "pref_remove_bookmarked"
|
||||||
|
|
||||||
const val libraryUpdateInterval = "pref_library_update_interval_key"
|
|
||||||
|
|
||||||
const val libraryUpdateRestriction = "library_update_restriction"
|
|
||||||
|
|
||||||
const val libraryUpdateCategories = "library_update_categories"
|
|
||||||
const val libraryUpdateCategoriesExclude = "library_update_categories_exclude"
|
|
||||||
|
|
||||||
const val libraryUpdatePrioritization = "library_update_prioritization"
|
|
||||||
|
|
||||||
const val downloadedOnly = "pref_downloaded_only"
|
|
||||||
|
|
||||||
const val filterDownloaded = "pref_filter_library_downloaded"
|
const val filterDownloaded = "pref_filter_library_downloaded"
|
||||||
|
|
||||||
const val filterUnread = "pref_filter_library_unread"
|
const val filterUnread = "pref_filter_library_unread"
|
||||||
@ -154,57 +41,22 @@ object PreferenceKeys {
|
|||||||
const val migrationSortingMode = "pref_migration_sorting"
|
const val migrationSortingMode = "pref_migration_sorting"
|
||||||
const val migrationSortingDirection = "pref_migration_direction"
|
const val migrationSortingDirection = "pref_migration_direction"
|
||||||
|
|
||||||
const val automaticExtUpdates = "automatic_ext_updates"
|
|
||||||
|
|
||||||
const val showNsfwSource = "show_nsfw_source"
|
|
||||||
|
|
||||||
const val startScreen = "start_screen"
|
const val startScreen = "start_screen"
|
||||||
|
|
||||||
const val useAuthenticator = "use_biometric_lock"
|
|
||||||
|
|
||||||
const val lockAppAfter = "lock_app_after"
|
|
||||||
|
|
||||||
const val lastAppUnlock = "last_app_unlock"
|
|
||||||
|
|
||||||
const val secureScreen = "secure_screen"
|
|
||||||
|
|
||||||
const val hideNotificationContent = "hide_notification_content"
|
const val hideNotificationContent = "hide_notification_content"
|
||||||
|
|
||||||
const val autoUpdateMetadata = "auto_update_metadata"
|
const val autoUpdateMetadata = "auto_update_metadata"
|
||||||
|
|
||||||
const val autoUpdateTrackers = "auto_update_trackers"
|
const val autoUpdateTrackers = "auto_update_trackers"
|
||||||
|
|
||||||
const val downloadNew = "download_new"
|
|
||||||
|
|
||||||
const val downloadNewCategories = "download_new_categories"
|
|
||||||
const val downloadNewCategoriesExclude = "download_new_categories_exclude"
|
|
||||||
const val removeExcludeCategories = "remove_exclude_categories"
|
|
||||||
|
|
||||||
const val libraryDisplayMode = "pref_display_mode_library"
|
|
||||||
|
|
||||||
const val relativeTime: String = "relative_time"
|
|
||||||
const val dateFormat = "app_date_format"
|
const val dateFormat = "app_date_format"
|
||||||
|
|
||||||
const val defaultCategory = "default_category"
|
const val defaultCategory = "default_category"
|
||||||
|
|
||||||
const val categorizedDisplay = "categorized_display"
|
|
||||||
|
|
||||||
const val skipRead = "skip_read"
|
const val skipRead = "skip_read"
|
||||||
|
|
||||||
const val skipFiltered = "skip_filtered"
|
const val skipFiltered = "skip_filtered"
|
||||||
|
|
||||||
const val downloadBadge = "display_download_badge"
|
|
||||||
|
|
||||||
const val unreadBadge = "display_unread_badge"
|
|
||||||
|
|
||||||
const val localBadge = "display_local_badge"
|
|
||||||
|
|
||||||
const val categoryTabs = "display_category_tabs"
|
|
||||||
|
|
||||||
const val categoryNumberOfItems = "display_number_of_items"
|
|
||||||
|
|
||||||
const val alwaysShowChapterTransition = "always_show_chapter_transition"
|
|
||||||
|
|
||||||
const val searchPinnedSourcesOnly = "search_pinned_sources_only"
|
const val searchPinnedSourcesOnly = "search_pinned_sources_only"
|
||||||
|
|
||||||
const val dohProvider = "doh_provider"
|
const val dohProvider = "doh_provider"
|
||||||
@ -221,11 +73,9 @@ object PreferenceKeys {
|
|||||||
|
|
||||||
const val defaultChapterDisplayByNameOrNumber = "default_chapter_display_by_name_or_number"
|
const val defaultChapterDisplayByNameOrNumber = "default_chapter_display_by_name_or_number"
|
||||||
|
|
||||||
const val incognitoMode = "incognito_mode"
|
const val verboseLogging = "verbose_logging"
|
||||||
|
|
||||||
const val tabletUiMode = "tablet_ui_mode"
|
const val autoClearChapterCache = "auto_clear_chapter_cache"
|
||||||
|
|
||||||
const val extensionInstaller = "extension_installer"
|
|
||||||
|
|
||||||
fun trackUsername(syncId: Int) = "pref_mangasync_username_$syncId"
|
fun trackUsername(syncId: Int) = "pref_mangasync_username_$syncId"
|
||||||
|
|
||||||
|
@ -2,8 +2,11 @@ package eu.kanade.tachiyomi.data.preference
|
|||||||
|
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
|
|
||||||
const val UNMETERED_NETWORK = "wifi"
|
const val DEVICE_ONLY_ON_WIFI = "wifi"
|
||||||
const val CHARGING = "ac"
|
const val DEVICE_CHARGING = "ac"
|
||||||
|
|
||||||
|
const val MANGA_ONGOING = "manga_ongoing"
|
||||||
|
const val MANGA_FULLY_READ = "manga_fully_read"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class stores the values for the preferences in the application.
|
* This class stores the values for the preferences in the application.
|
||||||
@ -31,11 +34,11 @@ object PreferenceValues {
|
|||||||
GREEN_APPLE(R.string.theme_greenapple),
|
GREEN_APPLE(R.string.theme_greenapple),
|
||||||
TEALTURQUOISE(R.string.theme_tealturquoise),
|
TEALTURQUOISE(R.string.theme_tealturquoise),
|
||||||
YINYANG(R.string.theme_yinyang),
|
YINYANG(R.string.theme_yinyang),
|
||||||
BLUE(R.string.theme_blue),
|
|
||||||
|
|
||||||
// Deprecated
|
// Deprecated
|
||||||
DARK_BLUE(null),
|
DARK_BLUE(null),
|
||||||
HOT_PINK(null),
|
HOT_PINK(null),
|
||||||
|
BLUE(null),
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class TappingInvertMode(val shouldInvertHorizontal: Boolean = false, val shouldInvertVertical: Boolean = false) {
|
enum class TappingInvertMode(val shouldInvertHorizontal: Boolean = false, val shouldInvertVertical: Boolean = false) {
|
||||||
@ -53,6 +56,7 @@ object PreferenceValues {
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum class TabletUiMode {
|
enum class TabletUiMode {
|
||||||
|
AUTOMATIC,
|
||||||
ALWAYS,
|
ALWAYS,
|
||||||
LANDSCAPE,
|
LANDSCAPE,
|
||||||
NEVER,
|
NEVER,
|
||||||
@ -61,6 +65,6 @@ object PreferenceValues {
|
|||||||
enum class ExtensionInstaller {
|
enum class ExtensionInstaller {
|
||||||
LEGACY,
|
LEGACY,
|
||||||
PACKAGEINSTALLER,
|
PACKAGEINSTALLER,
|
||||||
SHIZUKU
|
SHIZUKU,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,14 @@
|
|||||||
package eu.kanade.tachiyomi.data.preference
|
package eu.kanade.tachiyomi.data.preference
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.os.Build
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import com.tfcporciuncula.flow.FlowSharedPreferences
|
import com.tfcporciuncula.flow.FlowSharedPreferences
|
||||||
import com.tfcporciuncula.flow.Preference
|
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues.ThemeMode.system
|
|
||||||
import eu.kanade.tachiyomi.data.track.TrackService
|
import eu.kanade.tachiyomi.data.track.TrackService
|
||||||
import eu.kanade.tachiyomi.data.track.anilist.Anilist
|
import eu.kanade.tachiyomi.data.track.anilist.Anilist
|
||||||
import eu.kanade.tachiyomi.ui.browse.migration.sources.MigrationSourcesController
|
import eu.kanade.tachiyomi.ui.browse.migration.sources.MigrationSourcesController
|
||||||
@ -18,11 +17,8 @@ import eu.kanade.tachiyomi.ui.library.setting.SortDirectionSetting
|
|||||||
import eu.kanade.tachiyomi.ui.library.setting.SortModeSetting
|
import eu.kanade.tachiyomi.ui.library.setting.SortModeSetting
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
|
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
|
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
|
||||||
import eu.kanade.tachiyomi.util.system.MiuiUtil
|
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
||||||
import eu.kanade.tachiyomi.util.system.isTablet
|
|
||||||
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
|
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.flow.onEach
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.text.DateFormat
|
import java.text.DateFormat
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
@ -30,25 +26,6 @@ import java.util.Locale
|
|||||||
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
|
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues as Values
|
import eu.kanade.tachiyomi.data.preference.PreferenceValues as Values
|
||||||
|
|
||||||
fun <T> Preference<T>.asImmediateFlow(block: (T) -> Unit): Flow<T> {
|
|
||||||
block(get())
|
|
||||||
return asFlow()
|
|
||||||
.onEach { block(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
operator fun <T> Preference<Set<T>>.plusAssign(item: T) {
|
|
||||||
set(get() + item)
|
|
||||||
}
|
|
||||||
|
|
||||||
operator fun <T> Preference<Set<T>>.minusAssign(item: T) {
|
|
||||||
set(get() - item)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Preference<Boolean>.toggle(): Boolean {
|
|
||||||
set(!get())
|
|
||||||
return get()
|
|
||||||
}
|
|
||||||
|
|
||||||
class PreferencesHelper(val context: Context) {
|
class PreferencesHelper(val context: Context) {
|
||||||
|
|
||||||
private val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
private val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
@ -70,17 +47,17 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun confirmExit() = prefs.getBoolean(Keys.confirmExit, false)
|
fun confirmExit() = prefs.getBoolean(Keys.confirmExit, false)
|
||||||
|
|
||||||
fun hideBottomBarOnScroll() = flowPrefs.getBoolean(Keys.hideBottomBarOnScroll, true)
|
fun hideBottomBarOnScroll() = flowPrefs.getBoolean("pref_hide_bottom_bar_on_scroll", true)
|
||||||
|
|
||||||
fun sideNavIconAlignment() = flowPrefs.getInt(Keys.sideNavIconAlignment, 0)
|
fun sideNavIconAlignment() = flowPrefs.getInt("pref_side_nav_icon_alignment", 0)
|
||||||
|
|
||||||
fun useAuthenticator() = flowPrefs.getBoolean(Keys.useAuthenticator, false)
|
fun useAuthenticator() = flowPrefs.getBoolean("use_biometric_lock", false)
|
||||||
|
|
||||||
fun lockAppAfter() = flowPrefs.getInt(Keys.lockAppAfter, 0)
|
fun lockAppAfter() = flowPrefs.getInt("lock_app_after", 0)
|
||||||
|
|
||||||
fun lastAppUnlock() = flowPrefs.getLong(Keys.lastAppUnlock, 0)
|
fun lastAppUnlock() = flowPrefs.getLong("last_app_unlock", 0)
|
||||||
|
|
||||||
fun secureScreen() = flowPrefs.getBoolean(Keys.secureScreen, false)
|
fun secureScreen() = flowPrefs.getBoolean("secure_screen", false)
|
||||||
|
|
||||||
fun hideNotificationContent() = prefs.getBoolean(Keys.hideNotificationContent, false)
|
fun hideNotificationContent() = prefs.getBoolean(Keys.hideNotificationContent, false)
|
||||||
|
|
||||||
@ -88,109 +65,113 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun autoUpdateTrackers() = prefs.getBoolean(Keys.autoUpdateTrackers, false)
|
fun autoUpdateTrackers() = prefs.getBoolean(Keys.autoUpdateTrackers, false)
|
||||||
|
|
||||||
fun themeMode() = flowPrefs.getEnum(Keys.themeMode, system)
|
fun themeMode() = flowPrefs.getEnum(
|
||||||
|
"pref_theme_mode_key",
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { Values.ThemeMode.system } else { Values.ThemeMode.light }
|
||||||
|
)
|
||||||
|
|
||||||
fun appTheme() = flowPrefs.getEnum(Keys.appTheme, Values.AppTheme.DEFAULT)
|
fun appTheme() = flowPrefs.getEnum(
|
||||||
|
"pref_app_theme",
|
||||||
|
if (DeviceUtil.isDynamicColorAvailable) { Values.AppTheme.MONET } else { Values.AppTheme.DEFAULT }
|
||||||
|
)
|
||||||
|
|
||||||
fun themeDarkAmoled() = flowPrefs.getBoolean(Keys.themeDarkAmoled, false)
|
fun themeDarkAmoled() = flowPrefs.getBoolean("pref_theme_dark_amoled_key", false)
|
||||||
|
|
||||||
fun pageTransitions() = flowPrefs.getBoolean(Keys.enableTransitions, true)
|
fun pageTransitions() = flowPrefs.getBoolean("pref_enable_transitions_key", true)
|
||||||
|
|
||||||
fun doubleTapAnimSpeed() = flowPrefs.getInt(Keys.doubleTapAnimationSpeed, 500)
|
fun doubleTapAnimSpeed() = flowPrefs.getInt("pref_double_tap_anim_speed", 500)
|
||||||
|
|
||||||
fun showPageNumber() = flowPrefs.getBoolean(Keys.showPageNumber, true)
|
fun showPageNumber() = flowPrefs.getBoolean("pref_show_page_number_key", true)
|
||||||
|
|
||||||
fun dualPageSplitPaged() = flowPrefs.getBoolean(Keys.dualPageSplitPaged, false)
|
fun dualPageSplitPaged() = flowPrefs.getBoolean("pref_dual_page_split", false)
|
||||||
|
|
||||||
fun dualPageSplitWebtoon() = flowPrefs.getBoolean(Keys.dualPageSplitWebtoon, false)
|
fun dualPageSplitWebtoon() = flowPrefs.getBoolean("pref_dual_page_split_webtoon", false)
|
||||||
|
|
||||||
fun dualPageInvertPaged() = flowPrefs.getBoolean(Keys.dualPageInvertPaged, false)
|
fun dualPageInvertPaged() = flowPrefs.getBoolean("pref_dual_page_invert", false)
|
||||||
|
|
||||||
fun dualPageInvertWebtoon() = flowPrefs.getBoolean(Keys.dualPageInvertWebtoon, false)
|
fun dualPageInvertWebtoon() = flowPrefs.getBoolean("pref_dual_page_invert_webtoon", false)
|
||||||
|
|
||||||
fun showReadingMode() = prefs.getBoolean(Keys.showReadingMode, true)
|
fun showReadingMode() = prefs.getBoolean(Keys.showReadingMode, true)
|
||||||
|
|
||||||
fun trueColor() = flowPrefs.getBoolean(Keys.trueColor, false)
|
fun trueColor() = flowPrefs.getBoolean("pref_true_color_key", false)
|
||||||
|
|
||||||
fun fullscreen() = flowPrefs.getBoolean(Keys.fullscreen, true)
|
fun fullscreen() = flowPrefs.getBoolean("fullscreen", true)
|
||||||
|
|
||||||
fun cutoutShort() = flowPrefs.getBoolean(Keys.cutoutShort, true)
|
fun cutoutShort() = flowPrefs.getBoolean("cutout_short", true)
|
||||||
|
|
||||||
fun keepScreenOn() = flowPrefs.getBoolean(Keys.keepScreenOn, true)
|
fun keepScreenOn() = flowPrefs.getBoolean("pref_keep_screen_on_key", true)
|
||||||
|
|
||||||
fun customBrightness() = flowPrefs.getBoolean(Keys.customBrightness, false)
|
fun customBrightness() = flowPrefs.getBoolean("pref_custom_brightness_key", false)
|
||||||
|
|
||||||
fun customBrightnessValue() = flowPrefs.getInt(Keys.customBrightnessValue, 0)
|
fun customBrightnessValue() = flowPrefs.getInt("custom_brightness_value", 0)
|
||||||
|
|
||||||
fun colorFilter() = flowPrefs.getBoolean(Keys.colorFilter, false)
|
fun colorFilter() = flowPrefs.getBoolean("pref_color_filter_key", false)
|
||||||
|
|
||||||
fun colorFilterValue() = flowPrefs.getInt(Keys.colorFilterValue, 0)
|
fun colorFilterValue() = flowPrefs.getInt("color_filter_value", 0)
|
||||||
|
|
||||||
fun colorFilterMode() = flowPrefs.getInt(Keys.colorFilterMode, 0)
|
fun colorFilterMode() = flowPrefs.getInt("color_filter_mode", 0)
|
||||||
|
|
||||||
fun grayscale() = flowPrefs.getBoolean(Keys.grayscale, false)
|
fun grayscale() = flowPrefs.getBoolean("pref_grayscale", false)
|
||||||
|
|
||||||
fun invertedColors() = flowPrefs.getBoolean(Keys.invertedColors, false)
|
fun invertedColors() = flowPrefs.getBoolean("pref_inverted_colors", false)
|
||||||
|
|
||||||
fun defaultReadingMode() = prefs.getInt(Keys.defaultReadingMode, ReadingModeType.RIGHT_TO_LEFT.flagValue)
|
fun defaultReadingMode() = prefs.getInt(Keys.defaultReadingMode, ReadingModeType.RIGHT_TO_LEFT.flagValue)
|
||||||
|
|
||||||
fun defaultOrientationType() = prefs.getInt(Keys.defaultOrientationType, OrientationType.FREE.flagValue)
|
fun defaultOrientationType() = prefs.getInt(Keys.defaultOrientationType, OrientationType.FREE.flagValue)
|
||||||
|
|
||||||
fun imageScaleType() = flowPrefs.getInt(Keys.imageScaleType, 1)
|
fun imageScaleType() = flowPrefs.getInt("pref_image_scale_type_key", 1)
|
||||||
|
|
||||||
fun zoomStart() = flowPrefs.getInt(Keys.zoomStart, 1)
|
fun zoomStart() = flowPrefs.getInt("pref_zoom_start_key", 1)
|
||||||
|
|
||||||
fun readerTheme() = flowPrefs.getInt(Keys.readerTheme, 1)
|
fun readerTheme() = flowPrefs.getInt("pref_reader_theme_key", 1)
|
||||||
|
|
||||||
fun alwaysShowChapterTransition() = flowPrefs.getBoolean(Keys.alwaysShowChapterTransition, true)
|
fun alwaysShowChapterTransition() = flowPrefs.getBoolean("always_show_chapter_transition", true)
|
||||||
|
|
||||||
fun cropBorders() = flowPrefs.getBoolean(Keys.cropBorders, false)
|
fun cropBorders() = flowPrefs.getBoolean("crop_borders", false)
|
||||||
|
|
||||||
fun cropBordersWebtoon() = flowPrefs.getBoolean(Keys.cropBordersWebtoon, false)
|
fun cropBordersWebtoon() = flowPrefs.getBoolean("crop_borders_webtoon", false)
|
||||||
|
|
||||||
fun webtoonSidePadding() = flowPrefs.getInt(Keys.webtoonSidePadding, 0)
|
fun webtoonSidePadding() = flowPrefs.getInt("webtoon_side_padding", 0)
|
||||||
|
|
||||||
fun readWithTapping() = flowPrefs.getBoolean(Keys.readWithTapping, true)
|
fun readWithTapping() = flowPrefs.getBoolean("reader_tap", true)
|
||||||
|
|
||||||
fun pagerNavInverted() = flowPrefs.getEnum(Keys.pagerNavInverted, Values.TappingInvertMode.NONE)
|
fun pagerNavInverted() = flowPrefs.getEnum("reader_tapping_inverted", Values.TappingInvertMode.NONE)
|
||||||
|
|
||||||
fun webtoonNavInverted() = flowPrefs.getEnum(Keys.webtoonNavInverted, Values.TappingInvertMode.NONE)
|
fun webtoonNavInverted() = flowPrefs.getEnum("reader_tapping_inverted_webtoon", Values.TappingInvertMode.NONE)
|
||||||
|
|
||||||
fun readWithLongTap() = flowPrefs.getBoolean(Keys.readWithLongTap, true)
|
fun readWithLongTap() = flowPrefs.getBoolean("reader_long_tap", true)
|
||||||
|
|
||||||
fun readWithVolumeKeys() = flowPrefs.getBoolean(Keys.readWithVolumeKeys, false)
|
fun readWithVolumeKeys() = flowPrefs.getBoolean("reader_volume_keys", false)
|
||||||
|
|
||||||
fun readWithVolumeKeysInverted() = flowPrefs.getBoolean(Keys.readWithVolumeKeysInverted, false)
|
fun readWithVolumeKeysInverted() = flowPrefs.getBoolean("reader_volume_keys_inverted", false)
|
||||||
|
|
||||||
fun navigationModePager() = flowPrefs.getInt(Keys.navigationModePager, 0)
|
fun navigationModePager() = flowPrefs.getInt("reader_navigation_mode_pager", 0)
|
||||||
|
|
||||||
fun navigationModeWebtoon() = flowPrefs.getInt(Keys.navigationModeWebtoon, 0)
|
fun navigationModeWebtoon() = flowPrefs.getInt("reader_navigation_mode_webtoon", 0)
|
||||||
|
|
||||||
fun showNavigationOverlayNewUser() = flowPrefs.getBoolean(Keys.showNavigationOverlayNewUser, true)
|
fun showNavigationOverlayNewUser() = flowPrefs.getBoolean("reader_navigation_overlay_new_user", true)
|
||||||
|
|
||||||
fun showNavigationOverlayOnStart() = flowPrefs.getBoolean(Keys.showNavigationOverlayOnStart, false)
|
fun showNavigationOverlayOnStart() = flowPrefs.getBoolean("reader_navigation_overlay_on_start", false)
|
||||||
|
|
||||||
fun readerHideTreshold() = flowPrefs.getEnum(Keys.readerHideThreshold, Values.ReaderHideThreshold.LOW)
|
fun readerHideThreshold() = flowPrefs.getEnum("reader_hide_threshold", Values.ReaderHideThreshold.LOW)
|
||||||
|
|
||||||
fun portraitColumns() = flowPrefs.getInt(Keys.portraitColumns, 0)
|
fun portraitColumns() = flowPrefs.getInt("pref_library_columns_portrait_key", 0)
|
||||||
|
|
||||||
fun landscapeColumns() = flowPrefs.getInt(Keys.landscapeColumns, 0)
|
fun landscapeColumns() = flowPrefs.getInt("pref_library_columns_landscape_key", 0)
|
||||||
|
|
||||||
fun jumpToChapters() = prefs.getBoolean(Keys.jumpToChapters, false)
|
fun jumpToChapters() = prefs.getBoolean(Keys.jumpToChapters, false)
|
||||||
|
|
||||||
fun updateOnlyNonCompleted() = prefs.getBoolean(Keys.updateOnlyNonCompleted, false)
|
|
||||||
|
|
||||||
fun autoUpdateTrack() = prefs.getBoolean(Keys.autoUpdateTrack, true)
|
fun autoUpdateTrack() = prefs.getBoolean(Keys.autoUpdateTrack, true)
|
||||||
|
|
||||||
fun lastUsedSource() = flowPrefs.getLong(Keys.lastUsedSource, -1)
|
fun lastUsedSource() = flowPrefs.getLong("last_catalogue_source", -1)
|
||||||
|
|
||||||
fun lastUsedCategory() = flowPrefs.getInt(Keys.lastUsedCategory, 0)
|
fun lastUsedCategory() = flowPrefs.getInt("last_used_category", 0)
|
||||||
|
|
||||||
fun lastVersionCode() = flowPrefs.getInt("last_version_code", 0)
|
fun lastVersionCode() = flowPrefs.getInt("last_version_code", 0)
|
||||||
|
|
||||||
fun sourceDisplayMode() = flowPrefs.getEnum(Keys.sourceDisplayMode, DisplayModeSetting.COMPACT_GRID)
|
fun sourceDisplayMode() = flowPrefs.getEnum("pref_display_mode_catalogue", DisplayModeSetting.COMPACT_GRID)
|
||||||
|
|
||||||
fun enabledLanguages() = flowPrefs.getStringSet(Keys.enabledLanguages, setOf("all", "en", Locale.getDefault().language))
|
fun enabledLanguages() = flowPrefs.getStringSet("source_languages", setOf("all", "en", Locale.getDefault().language))
|
||||||
|
|
||||||
fun trackUsername(sync: TrackService) = prefs.getString(Keys.trackUsername(sync.id), "")
|
fun trackUsername(sync: TrackService) = prefs.getString(Keys.trackUsername(sync.id), "")
|
||||||
|
|
||||||
@ -207,24 +188,26 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun anilistScoreType() = flowPrefs.getString("anilist_score_type", Anilist.POINT_10)
|
fun anilistScoreType() = flowPrefs.getString("anilist_score_type", Anilist.POINT_10)
|
||||||
|
|
||||||
fun backupsDirectory() = flowPrefs.getString(Keys.backupDirectory, defaultBackupDir.toString())
|
fun backupsDirectory() = flowPrefs.getString("backup_directory", defaultBackupDir.toString())
|
||||||
|
|
||||||
fun relativeTime() = flowPrefs.getInt(Keys.relativeTime, 7)
|
fun relativeTime() = flowPrefs.getInt("relative_time", 7)
|
||||||
|
|
||||||
fun dateFormat(format: String = flowPrefs.getString(Keys.dateFormat, "").get()): DateFormat = when (format) {
|
fun dateFormat(format: String = flowPrefs.getString(Keys.dateFormat, "").get()): DateFormat = when (format) {
|
||||||
"" -> DateFormat.getDateInstance(DateFormat.SHORT)
|
"" -> DateFormat.getDateInstance(DateFormat.SHORT)
|
||||||
else -> SimpleDateFormat(format, Locale.getDefault())
|
else -> SimpleDateFormat(format, Locale.getDefault())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun downloadsDirectory() = flowPrefs.getString(Keys.downloadsDirectory, defaultDownloadsDir.toString())
|
fun downloadsDirectory() = flowPrefs.getString("download_directory", defaultDownloadsDir.toString())
|
||||||
|
|
||||||
fun downloadOnlyOverWifi() = prefs.getBoolean(Keys.downloadOnlyOverWifi, true)
|
fun downloadOnlyOverWifi() = prefs.getBoolean(Keys.downloadOnlyOverWifi, true)
|
||||||
|
|
||||||
|
fun saveChaptersAsCBZ() = flowPrefs.getBoolean("save_chapter_as_cbz", false)
|
||||||
|
|
||||||
fun folderPerManga() = prefs.getBoolean(Keys.folderPerManga, false)
|
fun folderPerManga() = prefs.getBoolean(Keys.folderPerManga, false)
|
||||||
|
|
||||||
fun numberOfBackups() = flowPrefs.getInt(Keys.numberOfBackups, 1)
|
fun numberOfBackups() = flowPrefs.getInt("backup_slots", 1)
|
||||||
|
|
||||||
fun backupInterval() = flowPrefs.getInt(Keys.backupInterval, 0)
|
fun backupInterval() = flowPrefs.getInt("backup_interval", 0)
|
||||||
|
|
||||||
fun removeAfterReadSlots() = prefs.getInt(Keys.removeAfterReadSlots, -1)
|
fun removeAfterReadSlots() = prefs.getInt(Keys.removeAfterReadSlots, -1)
|
||||||
|
|
||||||
@ -232,30 +215,34 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun removeBookmarkedChapters() = prefs.getBoolean(Keys.removeBookmarkedChapters, false)
|
fun removeBookmarkedChapters() = prefs.getBoolean(Keys.removeBookmarkedChapters, false)
|
||||||
|
|
||||||
fun removeExcludeCategories() = flowPrefs.getStringSet(Keys.removeExcludeCategories, emptySet())
|
fun removeExcludeCategories() = flowPrefs.getStringSet("remove_exclude_categories", emptySet())
|
||||||
|
|
||||||
fun libraryUpdateInterval() = flowPrefs.getInt(Keys.libraryUpdateInterval, 24)
|
fun libraryUpdateInterval() = flowPrefs.getInt("pref_library_update_interval_key", 24)
|
||||||
|
|
||||||
fun libraryUpdateRestriction() = flowPrefs.getStringSet(Keys.libraryUpdateRestriction, setOf(UNMETERED_NETWORK))
|
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 libraryUpdateCategories() = flowPrefs.getStringSet(Keys.libraryUpdateCategories, emptySet())
|
fun showUpdatesNavBadge() = flowPrefs.getBoolean("library_update_show_tab_badge", false)
|
||||||
fun libraryUpdateCategoriesExclude() = flowPrefs.getStringSet(Keys.libraryUpdateCategoriesExclude, emptySet())
|
fun unreadUpdatesCount() = flowPrefs.getInt("library_unread_updates_count", 0)
|
||||||
|
|
||||||
fun libraryUpdatePrioritization() = flowPrefs.getInt(Keys.libraryUpdatePrioritization, 0)
|
fun libraryUpdateCategories() = flowPrefs.getStringSet("library_update_categories", emptySet())
|
||||||
|
fun libraryUpdateCategoriesExclude() = flowPrefs.getStringSet("library_update_categories_exclude", emptySet())
|
||||||
|
|
||||||
fun libraryDisplayMode() = flowPrefs.getEnum(Keys.libraryDisplayMode, DisplayModeSetting.COMPACT_GRID)
|
fun libraryDisplayMode() = flowPrefs.getEnum("pref_display_mode_library", DisplayModeSetting.COMPACT_GRID)
|
||||||
|
|
||||||
fun downloadBadge() = flowPrefs.getBoolean(Keys.downloadBadge, false)
|
fun downloadBadge() = flowPrefs.getBoolean("display_download_badge", false)
|
||||||
|
|
||||||
fun localBadge() = flowPrefs.getBoolean(Keys.localBadge, true)
|
fun localBadge() = flowPrefs.getBoolean("display_local_badge", true)
|
||||||
|
|
||||||
fun downloadedOnly() = flowPrefs.getBoolean(Keys.downloadedOnly, false)
|
fun downloadedOnly() = flowPrefs.getBoolean("pref_downloaded_only", false)
|
||||||
|
|
||||||
fun unreadBadge() = flowPrefs.getBoolean(Keys.unreadBadge, true)
|
fun unreadBadge() = flowPrefs.getBoolean("display_unread_badge", true)
|
||||||
|
|
||||||
fun categoryTabs() = flowPrefs.getBoolean(Keys.categoryTabs, true)
|
fun languageBadge() = flowPrefs.getBoolean("display_language_badge", false)
|
||||||
|
|
||||||
fun categoryNumberOfItems() = flowPrefs.getBoolean(Keys.categoryNumberOfItems, false)
|
fun categoryTabs() = flowPrefs.getBoolean("display_category_tabs", true)
|
||||||
|
|
||||||
|
fun categoryNumberOfItems() = flowPrefs.getBoolean("display_number_of_items", false)
|
||||||
|
|
||||||
fun filterDownloaded() = flowPrefs.getInt(Keys.filterDownloaded, ExtendedNavigationView.Item.TriStateGroup.State.IGNORE.value)
|
fun filterDownloaded() = flowPrefs.getInt(Keys.filterDownloaded, ExtendedNavigationView.Item.TriStateGroup.State.IGNORE.value)
|
||||||
|
|
||||||
@ -271,9 +258,9 @@ class PreferencesHelper(val context: Context) {
|
|||||||
fun migrationSortingMode() = flowPrefs.getEnum(Keys.migrationSortingMode, MigrationSourcesController.SortSetting.ALPHABETICAL)
|
fun migrationSortingMode() = flowPrefs.getEnum(Keys.migrationSortingMode, MigrationSourcesController.SortSetting.ALPHABETICAL)
|
||||||
fun migrationSortingDirection() = flowPrefs.getEnum(Keys.migrationSortingDirection, MigrationSourcesController.DirectionSetting.ASCENDING)
|
fun migrationSortingDirection() = flowPrefs.getEnum(Keys.migrationSortingDirection, MigrationSourcesController.DirectionSetting.ASCENDING)
|
||||||
|
|
||||||
fun automaticExtUpdates() = flowPrefs.getBoolean(Keys.automaticExtUpdates, true)
|
fun automaticExtUpdates() = flowPrefs.getBoolean("automatic_ext_updates", true)
|
||||||
|
|
||||||
fun showNsfwSource() = flowPrefs.getBoolean(Keys.showNsfwSource, true)
|
fun showNsfwSource() = flowPrefs.getBoolean("show_nsfw_source", true)
|
||||||
|
|
||||||
fun extensionUpdatesCount() = flowPrefs.getInt("ext_updates_count", 0)
|
fun extensionUpdatesCount() = flowPrefs.getInt("ext_updates_count", 0)
|
||||||
|
|
||||||
@ -286,14 +273,14 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun pinnedSources() = flowPrefs.getStringSet("pinned_catalogues", emptySet())
|
fun pinnedSources() = flowPrefs.getStringSet("pinned_catalogues", emptySet())
|
||||||
|
|
||||||
fun downloadNew() = flowPrefs.getBoolean(Keys.downloadNew, false)
|
fun downloadNew() = flowPrefs.getBoolean("download_new", false)
|
||||||
|
|
||||||
fun downloadNewCategories() = flowPrefs.getStringSet(Keys.downloadNewCategories, emptySet())
|
fun downloadNewCategories() = flowPrefs.getStringSet("download_new_categories", emptySet())
|
||||||
fun downloadNewCategoriesExclude() = flowPrefs.getStringSet(Keys.downloadNewCategoriesExclude, emptySet())
|
fun downloadNewCategoriesExclude() = flowPrefs.getStringSet("download_new_categories_exclude", emptySet())
|
||||||
|
|
||||||
fun defaultCategory() = prefs.getInt(Keys.defaultCategory, -1)
|
fun defaultCategory() = prefs.getInt(Keys.defaultCategory, -1)
|
||||||
|
|
||||||
fun categorisedDisplaySettings() = flowPrefs.getBoolean(Keys.categorizedDisplay, false)
|
fun categorizedDisplaySettings() = flowPrefs.getBoolean("categorized_display", false)
|
||||||
|
|
||||||
fun skipRead() = prefs.getBoolean(Keys.skipRead, false)
|
fun skipRead() = prefs.getBoolean(Keys.skipRead, false)
|
||||||
|
|
||||||
@ -319,18 +306,19 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun sortChapterByAscendingOrDescending() = prefs.getInt(Keys.defaultChapterSortByAscendingOrDescending, Manga.CHAPTER_SORT_DESC)
|
fun sortChapterByAscendingOrDescending() = prefs.getInt(Keys.defaultChapterSortByAscendingOrDescending, Manga.CHAPTER_SORT_DESC)
|
||||||
|
|
||||||
fun incognitoMode() = flowPrefs.getBoolean(Keys.incognitoMode, false)
|
fun incognitoMode() = flowPrefs.getBoolean("incognito_mode", false)
|
||||||
|
|
||||||
fun tabletUiMode() = flowPrefs.getEnum(
|
fun tabletUiMode() = flowPrefs.getEnum("tablet_ui_mode", Values.TabletUiMode.AUTOMATIC)
|
||||||
Keys.tabletUiMode,
|
|
||||||
if (context.applicationContext.isTablet()) Values.TabletUiMode.ALWAYS else Values.TabletUiMode.NEVER
|
|
||||||
)
|
|
||||||
|
|
||||||
fun extensionInstaller() = flowPrefs.getEnum(
|
fun extensionInstaller() = flowPrefs.getEnum(
|
||||||
Keys.extensionInstaller,
|
"extension_installer",
|
||||||
if (MiuiUtil.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 autoClearChapterCache() = prefs.getBoolean(Keys.autoClearChapterCache, false)
|
||||||
|
|
||||||
fun setChapterSettingsDefault(manga: Manga) {
|
fun setChapterSettingsDefault(manga: Manga) {
|
||||||
prefs.edit {
|
prefs.edit {
|
||||||
putInt(Keys.defaultChapterFilterByRead, manga.readFilter)
|
putInt(Keys.defaultChapterFilterByRead, manga.readFilter)
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package eu.kanade.tachiyomi.data.track
|
package eu.kanade.tachiyomi.data.track
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
|
|
||||||
@ -25,4 +26,14 @@ interface EnhancedTrackService {
|
|||||||
* match is similar to TrackService.search, but only return zero or one match.
|
* match is similar to TrackService.search, but only return zero or one match.
|
||||||
*/
|
*/
|
||||||
suspend fun match(manga: Manga): TrackSearch?
|
suspend fun match(manga: Manga): TrackSearch?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether the provided source/track/manga triplet is from this TrackService
|
||||||
|
*/
|
||||||
|
fun isTrackFrom(track: Track, manga: Manga, source: Source?): Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migrates the given track for the manga to the newSource, if possible
|
||||||
|
*/
|
||||||
|
fun migrateTrack(track: Track, manga: Manga, newSource: Source): Track?
|
||||||
}
|
}
|
||||||
|
@ -17,10 +17,10 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
companion object {
|
companion object {
|
||||||
const val READING = 1
|
const val READING = 1
|
||||||
const val COMPLETED = 2
|
const val COMPLETED = 2
|
||||||
const val PAUSED = 3
|
const val ON_HOLD = 3
|
||||||
const val DROPPED = 4
|
const val DROPPED = 4
|
||||||
const val PLANNING = 5
|
const val PLAN_TO_READ = 5
|
||||||
const val REPEATING = 6
|
const val REREADING = 6
|
||||||
|
|
||||||
const val POINT_100 = "POINT_100"
|
const val POINT_100 = "POINT_100"
|
||||||
const val POINT_10 = "POINT_10"
|
const val POINT_10 = "POINT_10"
|
||||||
@ -57,24 +57,24 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
override fun getLogoColor() = Color.rgb(18, 25, 35)
|
override fun getLogoColor() = Color.rgb(18, 25, 35)
|
||||||
|
|
||||||
override fun getStatusList(): List<Int> {
|
override fun getStatusList(): List<Int> {
|
||||||
return listOf(READING, PLANNING, COMPLETED, REPEATING, PAUSED, DROPPED)
|
return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ, REREADING)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getStatus(status: Int): String = with(context) {
|
override fun getStatus(status: Int): String = with(context) {
|
||||||
when (status) {
|
when (status) {
|
||||||
READING -> getString(R.string.reading)
|
READING -> getString(R.string.reading)
|
||||||
PLANNING -> getString(R.string.plan_to_read)
|
PLAN_TO_READ -> getString(R.string.plan_to_read)
|
||||||
COMPLETED -> getString(R.string.completed)
|
COMPLETED -> getString(R.string.completed)
|
||||||
REPEATING -> getString(R.string.repeating)
|
ON_HOLD -> getString(R.string.on_hold)
|
||||||
PAUSED -> getString(R.string.paused)
|
|
||||||
DROPPED -> getString(R.string.dropped)
|
DROPPED -> getString(R.string.dropped)
|
||||||
|
REREADING -> getString(R.string.repeating)
|
||||||
else -> ""
|
else -> ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getReadingStatus(): Int = READING
|
override fun getReadingStatus(): Int = READING
|
||||||
|
|
||||||
override fun getRereadingStatus(): Int = REPEATING
|
override fun getRereadingStatus(): Int = REREADING
|
||||||
|
|
||||||
override fun getCompletionStatus(): Int = COMPLETED
|
override fun getCompletionStatus(): Int = COMPLETED
|
||||||
|
|
||||||
@ -147,8 +147,16 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (track.status != COMPLETED) {
|
if (track.status != COMPLETED) {
|
||||||
if (track.status != REPEATING && didReadChapter) {
|
if (didReadChapter) {
|
||||||
track.status = READING
|
if (track.last_chapter_read.toInt() == track.total_chapters && track.total_chapters > 0) {
|
||||||
|
track.status = COMPLETED
|
||||||
|
track.finished_reading_date = System.currentTimeMillis()
|
||||||
|
} else if (track.status != REREADING) {
|
||||||
|
track.status = READING
|
||||||
|
if (track.last_chapter_read == 1F) {
|
||||||
|
track.started_reading_date = System.currentTimeMillis()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -162,14 +170,14 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
track.library_id = remoteTrack.library_id
|
track.library_id = remoteTrack.library_id
|
||||||
|
|
||||||
if (track.status != COMPLETED) {
|
if (track.status != COMPLETED) {
|
||||||
val isRereading = track.status == REPEATING
|
val isRereading = track.status == REREADING
|
||||||
track.status = if (isRereading.not() && hasReadChapters) READING else track.status
|
track.status = if (isRereading.not() && hasReadChapters) READING else track.status
|
||||||
}
|
}
|
||||||
|
|
||||||
update(track)
|
update(track)
|
||||||
} else {
|
} else {
|
||||||
// Set default fields if it's not found in the list
|
// Set default fields if it's not found in the list
|
||||||
track.status = if (hasReadChapters) READING else PLANNING
|
track.status = if (hasReadChapters) READING else PLAN_TO_READ
|
||||||
track.score = 0F
|
track.score = 0F
|
||||||
add(track)
|
add(track)
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,7 @@ import eu.kanade.tachiyomi.data.database.models.Track
|
|||||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
import eu.kanade.tachiyomi.network.POST
|
import eu.kanade.tachiyomi.network.POST
|
||||||
import eu.kanade.tachiyomi.network.await
|
import eu.kanade.tachiyomi.network.await
|
||||||
import eu.kanade.tachiyomi.network.interceptor.RateLimitInterceptor
|
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
||||||
import eu.kanade.tachiyomi.network.jsonMime
|
import eu.kanade.tachiyomi.network.jsonMime
|
||||||
import eu.kanade.tachiyomi.network.parseAs
|
import eu.kanade.tachiyomi.network.parseAs
|
||||||
import eu.kanade.tachiyomi.util.lang.withIOContext
|
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||||
@ -25,13 +25,13 @@ import kotlinx.serialization.json.putJsonObject
|
|||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
import java.util.Calendar
|
import java.util.Calendar
|
||||||
import java.util.concurrent.TimeUnit.MINUTES
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||||
|
|
||||||
private val authClient = client.newBuilder()
|
private val authClient = client.newBuilder()
|
||||||
.addInterceptor(interceptor)
|
.addInterceptor(interceptor)
|
||||||
.addInterceptor(RateLimitInterceptor(85, 1, MINUTES))
|
.rateLimit(permits = 85, period = 1, unit = TimeUnit.MINUTES)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
suspend fun addLibManga(track: Track): Track {
|
suspend fun addLibManga(track: Track): Track {
|
||||||
|
@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.data.track.anilist
|
|||||||
|
|
||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
class AnilistInterceptor(val anilist: Anilist, private var token: String?) : Interceptor {
|
class AnilistInterceptor(val anilist: Anilist, private var token: String?) : Interceptor {
|
||||||
|
|
||||||
@ -28,12 +29,12 @@ class AnilistInterceptor(val anilist: Anilist, private var token: String?) : Int
|
|||||||
// Refresh access token if null or expired.
|
// Refresh access token if null or expired.
|
||||||
if (oauth!!.isExpired()) {
|
if (oauth!!.isExpired()) {
|
||||||
anilist.logout()
|
anilist.logout()
|
||||||
throw Exception("Token expired")
|
throw IOException("Token expired")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Throw on null auth.
|
// Throw on null auth.
|
||||||
if (oauth == null) {
|
if (oauth == null) {
|
||||||
throw Exception("No authentication token")
|
throw IOException("No authentication token")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the authorization header to the original request.
|
// Add the authorization header to the original request.
|
||||||
|
@ -64,10 +64,10 @@ data class ALUserManga(
|
|||||||
fun toTrackStatus() = when (list_status) {
|
fun toTrackStatus() = when (list_status) {
|
||||||
"CURRENT" -> Anilist.READING
|
"CURRENT" -> Anilist.READING
|
||||||
"COMPLETED" -> Anilist.COMPLETED
|
"COMPLETED" -> Anilist.COMPLETED
|
||||||
"PAUSED" -> Anilist.PAUSED
|
"PAUSED" -> Anilist.ON_HOLD
|
||||||
"DROPPED" -> Anilist.DROPPED
|
"DROPPED" -> Anilist.DROPPED
|
||||||
"PLANNING" -> Anilist.PLANNING
|
"PLANNING" -> Anilist.PLAN_TO_READ
|
||||||
"REPEATING" -> Anilist.REPEATING
|
"REPEATING" -> Anilist.REREADING
|
||||||
else -> throw NotImplementedError("Unknown status: $list_status")
|
else -> throw NotImplementedError("Unknown status: $list_status")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -75,10 +75,10 @@ data class ALUserManga(
|
|||||||
fun Track.toAnilistStatus() = when (status) {
|
fun Track.toAnilistStatus() = when (status) {
|
||||||
Anilist.READING -> "CURRENT"
|
Anilist.READING -> "CURRENT"
|
||||||
Anilist.COMPLETED -> "COMPLETED"
|
Anilist.COMPLETED -> "COMPLETED"
|
||||||
Anilist.PAUSED -> "PAUSED"
|
Anilist.ON_HOLD -> "PAUSED"
|
||||||
Anilist.DROPPED -> "DROPPED"
|
Anilist.DROPPED -> "DROPPED"
|
||||||
Anilist.PLANNING -> "PLANNING"
|
Anilist.PLAN_TO_READ -> "PLANNING"
|
||||||
Anilist.REPEATING -> "REPEATING"
|
Anilist.REREADING -> "REPEATING"
|
||||||
else -> throw NotImplementedError("Unknown status: $status")
|
else -> throw NotImplementedError("Unknown status: $status")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,7 +38,11 @@ class Bangumi(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
override suspend fun update(track: Track, didReadChapter: Boolean): Track {
|
override suspend fun update(track: Track, didReadChapter: Boolean): Track {
|
||||||
if (track.status != COMPLETED) {
|
if (track.status != COMPLETED) {
|
||||||
if (didReadChapter) {
|
if (didReadChapter) {
|
||||||
track.status = READING
|
if (track.last_chapter_read.toInt() == track.total_chapters && track.total_chapters > 0) {
|
||||||
|
track.status = COMPLETED
|
||||||
|
} else {
|
||||||
|
track.status = READING
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -62,7 +66,7 @@ class Bangumi(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
refresh(track)
|
refresh(track)
|
||||||
} else {
|
} else {
|
||||||
// Set default fields if it's not found in the list
|
// Set default fields if it's not found in the list
|
||||||
track.status = if (hasReadChapters) READING else PLANNING
|
track.status = if (hasReadChapters) READING else PLAN_TO_READ
|
||||||
track.score = 0F
|
track.score = 0F
|
||||||
add(track)
|
add(track)
|
||||||
update(track)
|
update(track)
|
||||||
@ -87,16 +91,16 @@ class Bangumi(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
override fun getLogoColor() = Color.rgb(240, 145, 153)
|
override fun getLogoColor() = Color.rgb(240, 145, 153)
|
||||||
|
|
||||||
override fun getStatusList(): List<Int> {
|
override fun getStatusList(): List<Int> {
|
||||||
return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLANNING)
|
return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getStatus(status: Int): String = with(context) {
|
override fun getStatus(status: Int): String = with(context) {
|
||||||
when (status) {
|
when (status) {
|
||||||
READING -> getString(R.string.reading)
|
READING -> getString(R.string.reading)
|
||||||
|
PLAN_TO_READ -> getString(R.string.plan_to_read)
|
||||||
COMPLETED -> getString(R.string.completed)
|
COMPLETED -> getString(R.string.completed)
|
||||||
ON_HOLD -> getString(R.string.on_hold)
|
ON_HOLD -> getString(R.string.on_hold)
|
||||||
DROPPED -> getString(R.string.dropped)
|
DROPPED -> getString(R.string.dropped)
|
||||||
PLANNING -> getString(R.string.plan_to_read)
|
|
||||||
else -> ""
|
else -> ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -142,6 +146,6 @@ class Bangumi(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
const val COMPLETED = 2
|
const val COMPLETED = 2
|
||||||
const val ON_HOLD = 4
|
const val ON_HOLD = 4
|
||||||
const val DROPPED = 5
|
const val DROPPED = 5
|
||||||
const val PLANNING = 1
|
const val PLAN_TO_READ = 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -24,6 +24,7 @@ import okhttp3.OkHttpClient
|
|||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.net.URLEncoder
|
import java.net.URLEncoder
|
||||||
|
import java.nio.charset.StandardCharsets
|
||||||
|
|
||||||
class BangumiApi(private val client: OkHttpClient, interceptor: BangumiInterceptor) {
|
class BangumiApi(private val client: OkHttpClient, interceptor: BangumiInterceptor) {
|
||||||
|
|
||||||
@ -70,7 +71,7 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept
|
|||||||
|
|
||||||
suspend fun search(search: String): List<TrackSearch> {
|
suspend fun search(search: String): List<TrackSearch> {
|
||||||
return withIOContext {
|
return withIOContext {
|
||||||
val url = "$apiUrl/search/subject/${URLEncoder.encode(search, Charsets.UTF_8.name())}"
|
val url = "$apiUrl/search/subject/${URLEncoder.encode(search, StandardCharsets.UTF_8.name())}"
|
||||||
.toUri()
|
.toUri()
|
||||||
.buildUpon()
|
.buildUpon()
|
||||||
.appendQueryParameter("max_results", "20")
|
.appendQueryParameter("max_results", "20")
|
||||||
|
@ -16,15 +16,6 @@ class BangumiInterceptor(val bangumi: Bangumi) : Interceptor {
|
|||||||
*/
|
*/
|
||||||
private var oauth: OAuth? = bangumi.restoreToken()
|
private var oauth: OAuth? = bangumi.restoreToken()
|
||||||
|
|
||||||
fun addToken(token: String, oidFormBody: FormBody): FormBody {
|
|
||||||
val newFormBody = FormBody.Builder()
|
|
||||||
for (i in 0 until oidFormBody.size) {
|
|
||||||
newFormBody.add(oidFormBody.name(i), oidFormBody.value(i))
|
|
||||||
}
|
|
||||||
newFormBody.add("access_token", token)
|
|
||||||
return newFormBody.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun intercept(chain: Interceptor.Chain): Response {
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
val originalRequest = chain.request()
|
val originalRequest = chain.request()
|
||||||
|
|
||||||
@ -65,4 +56,13 @@ class BangumiInterceptor(val bangumi: Bangumi) : Interceptor {
|
|||||||
|
|
||||||
bangumi.saveToken(oauth)
|
bangumi.saveToken(oauth)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun addToken(token: String, oidFormBody: FormBody): FormBody {
|
||||||
|
val newFormBody = FormBody.Builder()
|
||||||
|
for (i in 0 until oidFormBody.size) {
|
||||||
|
newFormBody.add(oidFormBody.name(i), oidFormBody.value(i))
|
||||||
|
}
|
||||||
|
newFormBody.add("access_token", token)
|
||||||
|
return newFormBody.build()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,7 @@ fun Track.toBangumiStatus() = when (status) {
|
|||||||
Bangumi.COMPLETED -> "collect"
|
Bangumi.COMPLETED -> "collect"
|
||||||
Bangumi.ON_HOLD -> "on_hold"
|
Bangumi.ON_HOLD -> "on_hold"
|
||||||
Bangumi.DROPPED -> "dropped"
|
Bangumi.DROPPED -> "dropped"
|
||||||
Bangumi.PLANNING -> "wish"
|
Bangumi.PLAN_TO_READ -> "wish"
|
||||||
else -> throw NotImplementedError("Unknown status: $status")
|
else -> throw NotImplementedError("Unknown status: $status")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -16,6 +16,6 @@ fun toTrackStatus(status: String) = when (status) {
|
|||||||
"collect" -> Bangumi.COMPLETED
|
"collect" -> Bangumi.COMPLETED
|
||||||
"on_hold" -> Bangumi.ON_HOLD
|
"on_hold" -> Bangumi.ON_HOLD
|
||||||
"dropped" -> Bangumi.DROPPED
|
"dropped" -> Bangumi.DROPPED
|
||||||
"wish" -> Bangumi.PLANNING
|
"wish" -> Bangumi.PLAN_TO_READ
|
||||||
else -> throw NotImplementedError("Unknown status: $status")
|
else -> throw NotImplementedError("Unknown status: $status")
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,8 @@ package eu.kanade.tachiyomi.data.track.job
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
import timber.log.Timber
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
|
import logcat.LogPriority
|
||||||
|
|
||||||
class DelayedTrackingStore(context: Context) {
|
class DelayedTrackingStore(context: Context) {
|
||||||
|
|
||||||
@ -17,7 +18,7 @@ class DelayedTrackingStore(context: Context) {
|
|||||||
val (_, lastChapterRead) = preferences.getString(trackId, "0:0.0")!!.split(":")
|
val (_, lastChapterRead) = preferences.getString(trackId, "0:0.0")!!.split(":")
|
||||||
if (track.last_chapter_read > lastChapterRead.toFloat()) {
|
if (track.last_chapter_read > lastChapterRead.toFloat()) {
|
||||||
val value = "${track.manga_id}:${track.last_chapter_read}"
|
val value = "${track.manga_id}:${track.last_chapter_read}"
|
||||||
Timber.i("Queuing track item: $trackId, $value")
|
logcat(LogPriority.DEBUG) { "Queuing track item: $trackId, $value" }
|
||||||
preferences.edit {
|
preferences.edit {
|
||||||
putString(trackId, value)
|
putString(trackId, value)
|
||||||
}
|
}
|
||||||
@ -30,6 +31,7 @@ class DelayedTrackingStore(context: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
fun getItems(): List<DelayedTrackingItem> {
|
fun getItems(): List<DelayedTrackingItem> {
|
||||||
return (preferences.all as Map<String, String>).entries
|
return (preferences.all as Map<String, String>).entries
|
||||||
.map {
|
.map {
|
||||||
|
@ -11,9 +11,10 @@ import androidx.work.WorkManager
|
|||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import timber.log.Timber
|
import logcat.LogPriority
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
@ -44,7 +45,7 @@ class DelayedTrackingUpdateJob(context: Context, workerParams: WorkerParameters)
|
|||||||
db.insertTrack(track).executeAsBlocking()
|
db.insertTrack(track).executeAsBlocking()
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e)
|
logcat(LogPriority.ERROR, e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,13 +39,13 @@ class Kitsu(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
override fun getLogoColor() = Color.rgb(51, 37, 50)
|
override fun getLogoColor() = Color.rgb(51, 37, 50)
|
||||||
|
|
||||||
override fun getStatusList(): List<Int> {
|
override fun getStatusList(): List<Int> {
|
||||||
return listOf(READING, PLAN_TO_READ, COMPLETED, ON_HOLD, DROPPED)
|
return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getStatus(status: Int): String = with(context) {
|
override fun getStatus(status: Int): String = with(context) {
|
||||||
when (status) {
|
when (status) {
|
||||||
READING -> getString(R.string.currently_reading)
|
READING -> getString(R.string.reading)
|
||||||
PLAN_TO_READ -> getString(R.string.want_to_read)
|
PLAN_TO_READ -> getString(R.string.plan_to_read)
|
||||||
COMPLETED -> getString(R.string.completed)
|
COMPLETED -> getString(R.string.completed)
|
||||||
ON_HOLD -> getString(R.string.on_hold)
|
ON_HOLD -> getString(R.string.on_hold)
|
||||||
DROPPED -> getString(R.string.dropped)
|
DROPPED -> getString(R.string.dropped)
|
||||||
@ -80,7 +80,15 @@ class Kitsu(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
override suspend fun update(track: Track, didReadChapter: Boolean): Track {
|
override suspend fun update(track: Track, didReadChapter: Boolean): Track {
|
||||||
if (track.status != COMPLETED) {
|
if (track.status != COMPLETED) {
|
||||||
if (didReadChapter) {
|
if (didReadChapter) {
|
||||||
track.status = READING
|
if (track.last_chapter_read.toInt() == track.total_chapters && track.total_chapters > 0) {
|
||||||
|
track.status = COMPLETED
|
||||||
|
track.finished_reading_date = System.currentTimeMillis()
|
||||||
|
} else {
|
||||||
|
track.status = READING
|
||||||
|
if (track.last_chapter_read == 1F) {
|
||||||
|
track.started_reading_date = System.currentTimeMillis()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,6 +24,8 @@ import okhttp3.OkHttpClient
|
|||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.RequestBody
|
import okhttp3.RequestBody
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
|
import java.net.URLEncoder
|
||||||
|
import java.nio.charset.StandardCharsets
|
||||||
|
|
||||||
class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) {
|
class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) {
|
||||||
|
|
||||||
@ -125,7 +127,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
|
|||||||
private suspend fun algoliaSearch(key: String, query: String): List<TrackSearch> {
|
private suspend fun algoliaSearch(key: String, query: String): List<TrackSearch> {
|
||||||
return withIOContext {
|
return withIOContext {
|
||||||
val jsonObject = buildJsonObject {
|
val jsonObject = buildJsonObject {
|
||||||
put("params", "query=$query$algoliaFilter")
|
put("params", "query=${URLEncoder.encode(query, StandardCharsets.UTF_8.name())}$algoliaFilter")
|
||||||
}
|
}
|
||||||
|
|
||||||
client.newCall(
|
client.newCall(
|
||||||
|
@ -10,6 +10,7 @@ import eu.kanade.tachiyomi.data.track.EnhancedTrackService
|
|||||||
import eu.kanade.tachiyomi.data.track.NoLoginTrackService
|
import eu.kanade.tachiyomi.data.track.NoLoginTrackService
|
||||||
import eu.kanade.tachiyomi.data.track.TrackService
|
import eu.kanade.tachiyomi.data.track.TrackService
|
||||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import okhttp3.Dns
|
import okhttp3.Dns
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
|
|
||||||
@ -40,7 +41,7 @@ class Komga(private val context: Context, id: Int) : TrackService(id), EnhancedT
|
|||||||
override fun getStatus(status: Int): String = with(context) {
|
override fun getStatus(status: Int): String = with(context) {
|
||||||
when (status) {
|
when (status) {
|
||||||
UNREAD -> getString(R.string.unread)
|
UNREAD -> getString(R.string.unread)
|
||||||
READING -> getString(R.string.currently_reading)
|
READING -> getString(R.string.reading)
|
||||||
COMPLETED -> getString(R.string.completed)
|
COMPLETED -> getString(R.string.completed)
|
||||||
else -> ""
|
else -> ""
|
||||||
}
|
}
|
||||||
@ -59,7 +60,11 @@ class Komga(private val context: Context, id: Int) : TrackService(id), EnhancedT
|
|||||||
override suspend fun update(track: Track, didReadChapter: Boolean): Track {
|
override suspend fun update(track: Track, didReadChapter: Boolean): Track {
|
||||||
if (track.status != COMPLETED) {
|
if (track.status != COMPLETED) {
|
||||||
if (didReadChapter) {
|
if (didReadChapter) {
|
||||||
track.status = READING
|
if (track.last_chapter_read.toInt() == track.total_chapters && track.total_chapters > 0) {
|
||||||
|
track.status = COMPLETED
|
||||||
|
} else {
|
||||||
|
track.status = READING
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -99,4 +104,14 @@ class Komga(private val context: Context, id: Int) : TrackService(id), EnhancedT
|
|||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun isTrackFrom(track: Track, manga: Manga, source: Source?): Boolean =
|
||||||
|
track.tracking_url == manga.url && source?.let { accept(it) } == true
|
||||||
|
|
||||||
|
override fun migrateTrack(track: Track, manga: Manga, newSource: Source): Track? =
|
||||||
|
if (accept(newSource)) {
|
||||||
|
track.also { track.tracking_url = manga.url }
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,13 +7,14 @@ import eu.kanade.tachiyomi.network.GET
|
|||||||
import eu.kanade.tachiyomi.network.await
|
import eu.kanade.tachiyomi.network.await
|
||||||
import eu.kanade.tachiyomi.network.parseAs
|
import eu.kanade.tachiyomi.network.parseAs
|
||||||
import eu.kanade.tachiyomi.util.lang.withIOContext
|
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||||
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
import logcat.LogPriority
|
||||||
import okhttp3.MediaType.Companion.toMediaType
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
import timber.log.Timber
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
const val READLIST_API = "/api/v1/readlists"
|
const val READLIST_API = "/api/v1/readlists"
|
||||||
@ -56,7 +57,7 @@ class KomgaApi(private val client: OkHttpClient) {
|
|||||||
last_chapter_read = progress.lastReadContinuousNumberSort
|
last_chapter_read = progress.lastReadContinuousNumberSort
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.w(e, "Could not get item: $url")
|
logcat(LogPriority.WARN, e) { "Could not get item: $url" }
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -47,10 +47,10 @@ class MyAnimeList(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
override fun getStatus(status: Int): String = with(context) {
|
override fun getStatus(status: Int): String = with(context) {
|
||||||
when (status) {
|
when (status) {
|
||||||
READING -> getString(R.string.reading)
|
READING -> getString(R.string.reading)
|
||||||
|
PLAN_TO_READ -> getString(R.string.plan_to_read)
|
||||||
COMPLETED -> getString(R.string.completed)
|
COMPLETED -> getString(R.string.completed)
|
||||||
ON_HOLD -> getString(R.string.on_hold)
|
ON_HOLD -> getString(R.string.on_hold)
|
||||||
DROPPED -> getString(R.string.dropped)
|
DROPPED -> getString(R.string.dropped)
|
||||||
PLAN_TO_READ -> getString(R.string.plan_to_read)
|
|
||||||
REREADING -> getString(R.string.repeating)
|
REREADING -> getString(R.string.repeating)
|
||||||
else -> ""
|
else -> ""
|
||||||
}
|
}
|
||||||
@ -76,8 +76,16 @@ class MyAnimeList(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
|
|
||||||
override suspend fun update(track: Track, didReadChapter: Boolean): Track {
|
override suspend fun update(track: Track, didReadChapter: Boolean): Track {
|
||||||
if (track.status != COMPLETED) {
|
if (track.status != COMPLETED) {
|
||||||
if (track.status != REREADING && didReadChapter) {
|
if (didReadChapter) {
|
||||||
track.status = READING
|
if (track.last_chapter_read.toInt() == track.total_chapters && track.total_chapters > 0) {
|
||||||
|
track.status = COMPLETED
|
||||||
|
track.finished_reading_date = System.currentTimeMillis()
|
||||||
|
} else if (track.status != REREADING) {
|
||||||
|
track.status = READING
|
||||||
|
if (track.last_chapter_read == 1F) {
|
||||||
|
track.started_reading_date = System.currentTimeMillis()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -62,7 +62,8 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
|
|||||||
suspend fun search(query: String): List<TrackSearch> {
|
suspend fun search(query: String): List<TrackSearch> {
|
||||||
return withIOContext {
|
return withIOContext {
|
||||||
val url = "$baseApiUrl/manga".toUri().buildUpon()
|
val url = "$baseApiUrl/manga".toUri().buildUpon()
|
||||||
.appendQueryParameter("q", query)
|
// MAL API throws a 400 when the query is over 64 characters...
|
||||||
|
.appendQueryParameter("q", query.take(64))
|
||||||
.appendQueryParameter("nsfw", "true")
|
.appendQueryParameter("nsfw", "true")
|
||||||
.build()
|
.build()
|
||||||
authClient.newCall(GET(url.toString()))
|
authClient.newCall(GET(url.toString()))
|
||||||
@ -76,7 +77,7 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
|
|||||||
async { getMangaDetails(id) }
|
async { getMangaDetails(id) }
|
||||||
}
|
}
|
||||||
.awaitAll()
|
.awaitAll()
|
||||||
.filter { trackSearch -> trackSearch.publishing_type != "novel" }
|
.filter { trackSearch -> !trackSearch.publishing_type.contains("novel") }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ import kotlinx.serialization.json.Json
|
|||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
class MyAnimeListInterceptor(private val myanimelist: MyAnimeList, private var token: String?) : Interceptor {
|
class MyAnimeListInterceptor(private val myanimelist: MyAnimeList, private var token: String?) : Interceptor {
|
||||||
|
|
||||||
@ -16,7 +17,7 @@ class MyAnimeListInterceptor(private val myanimelist: MyAnimeList, private var t
|
|||||||
val originalRequest = chain.request()
|
val originalRequest = chain.request()
|
||||||
|
|
||||||
if (token.isNullOrEmpty()) {
|
if (token.isNullOrEmpty()) {
|
||||||
throw Exception("Not authenticated with MyAnimeList")
|
throw IOException("Not authenticated with MyAnimeList")
|
||||||
}
|
}
|
||||||
if (oauth == null) {
|
if (oauth == null) {
|
||||||
oauth = myanimelist.loadOAuth()
|
oauth = myanimelist.loadOAuth()
|
||||||
@ -30,7 +31,7 @@ class MyAnimeListInterceptor(private val myanimelist: MyAnimeList, private var t
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (oauth == null) {
|
if (oauth == null) {
|
||||||
throw Exception("No authentication token")
|
throw IOException("No authentication token")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the authorization header to the original request
|
// Add the authorization header to the original request
|
||||||
|
@ -19,8 +19,8 @@ class Shikimori(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
const val COMPLETED = 2
|
const val COMPLETED = 2
|
||||||
const val ON_HOLD = 3
|
const val ON_HOLD = 3
|
||||||
const val DROPPED = 4
|
const val DROPPED = 4
|
||||||
const val PLANNING = 5
|
const val PLAN_TO_READ = 5
|
||||||
const val REPEATING = 6
|
const val REREADING = 6
|
||||||
}
|
}
|
||||||
|
|
||||||
private val json: Json by injectLazy()
|
private val json: Json by injectLazy()
|
||||||
@ -46,8 +46,12 @@ class Shikimori(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
|
|
||||||
override suspend fun update(track: Track, didReadChapter: Boolean): Track {
|
override suspend fun update(track: Track, didReadChapter: Boolean): Track {
|
||||||
if (track.status != COMPLETED) {
|
if (track.status != COMPLETED) {
|
||||||
if (track.status != REPEATING && didReadChapter) {
|
if (didReadChapter) {
|
||||||
track.status = READING
|
if (track.last_chapter_read.toInt() == track.total_chapters && track.total_chapters > 0) {
|
||||||
|
track.status = COMPLETED
|
||||||
|
} else if (track.status != REREADING) {
|
||||||
|
track.status = READING
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -61,14 +65,14 @@ class Shikimori(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
track.library_id = remoteTrack.library_id
|
track.library_id = remoteTrack.library_id
|
||||||
|
|
||||||
if (track.status != COMPLETED) {
|
if (track.status != COMPLETED) {
|
||||||
val isRereading = track.status == REPEATING
|
val isRereading = track.status == REREADING
|
||||||
track.status = if (isRereading.not() && hasReadChapters) READING else track.status
|
track.status = if (isRereading.not() && hasReadChapters) READING else track.status
|
||||||
}
|
}
|
||||||
|
|
||||||
update(track)
|
update(track)
|
||||||
} else {
|
} else {
|
||||||
// Set default fields if it's not found in the list
|
// Set default fields if it's not found in the list
|
||||||
track.status = if (hasReadChapters) READING else PLANNING
|
track.status = if (hasReadChapters) READING else PLAN_TO_READ
|
||||||
track.score = 0F
|
track.score = 0F
|
||||||
add(track)
|
add(track)
|
||||||
}
|
}
|
||||||
@ -91,24 +95,24 @@ class Shikimori(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
override fun getLogoColor() = Color.rgb(40, 40, 40)
|
override fun getLogoColor() = Color.rgb(40, 40, 40)
|
||||||
|
|
||||||
override fun getStatusList(): List<Int> {
|
override fun getStatusList(): List<Int> {
|
||||||
return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLANNING, REPEATING)
|
return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ, REREADING)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getStatus(status: Int): String = with(context) {
|
override fun getStatus(status: Int): String = with(context) {
|
||||||
when (status) {
|
when (status) {
|
||||||
READING -> getString(R.string.reading)
|
READING -> getString(R.string.reading)
|
||||||
|
PLAN_TO_READ -> getString(R.string.plan_to_read)
|
||||||
COMPLETED -> getString(R.string.completed)
|
COMPLETED -> getString(R.string.completed)
|
||||||
ON_HOLD -> getString(R.string.on_hold)
|
ON_HOLD -> getString(R.string.on_hold)
|
||||||
DROPPED -> getString(R.string.dropped)
|
DROPPED -> getString(R.string.dropped)
|
||||||
PLANNING -> getString(R.string.plan_to_read)
|
REREADING -> getString(R.string.repeating)
|
||||||
REPEATING -> getString(R.string.repeating)
|
|
||||||
else -> ""
|
else -> ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getReadingStatus(): Int = READING
|
override fun getReadingStatus(): Int = READING
|
||||||
|
|
||||||
override fun getRereadingStatus(): Int = REPEATING
|
override fun getRereadingStatus(): Int = REREADING
|
||||||
|
|
||||||
override fun getCompletionStatus(): Int = COMPLETED
|
override fun getCompletionStatus(): Int = COMPLETED
|
||||||
|
|
||||||
|
@ -7,8 +7,8 @@ fun Track.toShikimoriStatus() = when (status) {
|
|||||||
Shikimori.COMPLETED -> "completed"
|
Shikimori.COMPLETED -> "completed"
|
||||||
Shikimori.ON_HOLD -> "on_hold"
|
Shikimori.ON_HOLD -> "on_hold"
|
||||||
Shikimori.DROPPED -> "dropped"
|
Shikimori.DROPPED -> "dropped"
|
||||||
Shikimori.PLANNING -> "planned"
|
Shikimori.PLAN_TO_READ -> "planned"
|
||||||
Shikimori.REPEATING -> "rewatching"
|
Shikimori.REREADING -> "rewatching"
|
||||||
else -> throw NotImplementedError("Unknown status: $status")
|
else -> throw NotImplementedError("Unknown status: $status")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -17,7 +17,7 @@ fun toTrackStatus(status: String) = when (status) {
|
|||||||
"completed" -> Shikimori.COMPLETED
|
"completed" -> Shikimori.COMPLETED
|
||||||
"on_hold" -> Shikimori.ON_HOLD
|
"on_hold" -> Shikimori.ON_HOLD
|
||||||
"dropped" -> Shikimori.DROPPED
|
"dropped" -> Shikimori.DROPPED
|
||||||
"planned" -> Shikimori.PLANNING
|
"planned" -> Shikimori.PLAN_TO_READ
|
||||||
"rewatching" -> Shikimori.REPEATING
|
"rewatching" -> Shikimori.REREADING
|
||||||
else -> throw NotImplementedError("Unknown status: $status")
|
else -> throw NotImplementedError("Unknown status: $status")
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.data.updater
|
package eu.kanade.tachiyomi.data.updater
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import eu.kanade.tachiyomi.BuildConfig
|
import eu.kanade.tachiyomi.BuildConfig
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
@ -9,24 +10,22 @@ import eu.kanade.tachiyomi.network.parseAs
|
|||||||
import eu.kanade.tachiyomi.util.lang.withIOContext
|
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
class AppUpdateChecker {
|
class AppUpdateChecker {
|
||||||
|
|
||||||
private val networkService: NetworkHelper by injectLazy()
|
private val networkService: NetworkHelper by injectLazy()
|
||||||
private val preferences: PreferencesHelper by injectLazy()
|
private val preferences: PreferencesHelper by injectLazy()
|
||||||
|
|
||||||
private val repo: String by lazy {
|
suspend fun checkForUpdate(context: Context, isUserPrompt: Boolean = false): AppUpdateResult {
|
||||||
if (BuildConfig.DEBUG) {
|
// Limit checks to once a day at most
|
||||||
"tachiyomiorg/tachiyomi-preview"
|
if (isUserPrompt.not() && Date().time < preferences.lastAppCheck().get() + TimeUnit.DAYS.toMillis(1)) {
|
||||||
} else {
|
return AppUpdateResult.NoNewUpdate
|
||||||
"tachiyomiorg/tachiyomi"
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun checkForUpdate(): AppUpdateResult {
|
|
||||||
return withIOContext {
|
return withIOContext {
|
||||||
networkService.client
|
val result = networkService.client
|
||||||
.newCall(GET("https://api.github.com/repos/$repo/releases/latest"))
|
.newCall(GET("https://api.github.com/repos/$GITHUB_REPO/releases/latest"))
|
||||||
.await()
|
.await()
|
||||||
.parseAs<GithubRelease>()
|
.parseAs<GithubRelease>()
|
||||||
.let {
|
.let {
|
||||||
@ -39,6 +38,12 @@ class AppUpdateChecker {
|
|||||||
AppUpdateResult.NoNewUpdate
|
AppUpdateResult.NoNewUpdate
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (result is AppUpdateResult.NewUpdate) {
|
||||||
|
AppUpdateNotifier(context).promptUpdate(result.release)
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -46,7 +51,7 @@ class AppUpdateChecker {
|
|||||||
// Removes prefixes like "r" or "v"
|
// Removes prefixes like "r" or "v"
|
||||||
val newVersion = versionTag.replace("[^\\d.]".toRegex(), "")
|
val newVersion = versionTag.replace("[^\\d.]".toRegex(), "")
|
||||||
|
|
||||||
return if (BuildConfig.DEBUG) {
|
return if (BuildConfig.PREVIEW) {
|
||||||
// Preview builds: based on releases in "tachiyomiorg/tachiyomi-preview" repo
|
// Preview builds: based on releases in "tachiyomiorg/tachiyomi-preview" repo
|
||||||
// tagged as something like "r1234"
|
// tagged as something like "r1234"
|
||||||
newVersion.toInt() > BuildConfig.COMMIT_COUNT.toInt()
|
newVersion.toInt() > BuildConfig.COMMIT_COUNT.toInt()
|
||||||
@ -57,3 +62,21 @@ class AppUpdateChecker {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val GITHUB_REPO: String by lazy {
|
||||||
|
if (BuildConfig.PREVIEW) {
|
||||||
|
"tachiyomiorg/tachiyomi-preview"
|
||||||
|
} else {
|
||||||
|
"tachiyomiorg/tachiyomi"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val RELEASE_TAG: String by lazy {
|
||||||
|
if (BuildConfig.PREVIEW) {
|
||||||
|
"r${BuildConfig.COMMIT_COUNT}"
|
||||||
|
} else {
|
||||||
|
"v${BuildConfig.VERSION_NAME}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val RELEASE_URL = "https://github.com/$GITHUB_REPO/releases/tag/$RELEASE_TAG"
|
||||||
|
@ -2,28 +2,27 @@ package eu.kanade.tachiyomi.data.updater
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.work.Constraints
|
import androidx.work.Constraints
|
||||||
|
import androidx.work.CoroutineWorker
|
||||||
import androidx.work.ExistingPeriodicWorkPolicy
|
import androidx.work.ExistingPeriodicWorkPolicy
|
||||||
import androidx.work.NetworkType
|
import androidx.work.NetworkType
|
||||||
import androidx.work.PeriodicWorkRequestBuilder
|
import androidx.work.PeriodicWorkRequestBuilder
|
||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
import androidx.work.Worker
|
|
||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
import eu.kanade.tachiyomi.BuildConfig
|
import eu.kanade.tachiyomi.BuildConfig
|
||||||
import kotlinx.coroutines.runBlocking
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
|
import kotlinx.coroutines.coroutineScope
|
||||||
|
import logcat.LogPriority
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
class UpdaterJob(private val context: Context, workerParams: WorkerParameters) :
|
class AppUpdateJob(private val context: Context, workerParams: WorkerParameters) :
|
||||||
Worker(context, workerParams) {
|
CoroutineWorker(context, workerParams) {
|
||||||
|
|
||||||
override fun doWork() = runBlocking {
|
override suspend fun doWork() = coroutineScope {
|
||||||
try {
|
try {
|
||||||
val result = AppUpdateChecker().checkForUpdate()
|
AppUpdateChecker().checkForUpdate(context)
|
||||||
|
|
||||||
if (result is AppUpdateResult.NewUpdate) {
|
|
||||||
UpdaterNotifier(context).promptUpdate(result.release.getDownloadLink())
|
|
||||||
}
|
|
||||||
Result.success()
|
Result.success()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
logcat(LogPriority.ERROR, e)
|
||||||
Result.failure()
|
Result.failure()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -32,8 +31,8 @@ class UpdaterJob(private val context: Context, workerParams: WorkerParameters) :
|
|||||||
private const val TAG = "UpdateChecker"
|
private const val TAG = "UpdateChecker"
|
||||||
|
|
||||||
fun setupTask(context: Context) {
|
fun setupTask(context: Context) {
|
||||||
// Never check for updates in debug builds that don't include the updater
|
// Never check for updates in builds that don't include the updater
|
||||||
if (BuildConfig.DEBUG && !BuildConfig.INCLUDE_UPDATER) {
|
if (!BuildConfig.INCLUDE_UPDATER) {
|
||||||
cancelTask(context)
|
cancelTask(context)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -42,7 +41,7 @@ class UpdaterJob(private val context: Context, workerParams: WorkerParameters) :
|
|||||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val request = PeriodicWorkRequestBuilder<UpdaterJob>(
|
val request = PeriodicWorkRequestBuilder<AppUpdateJob>(
|
||||||
7,
|
7,
|
||||||
TimeUnit.DAYS,
|
TimeUnit.DAYS,
|
||||||
3,
|
3,
|
@ -5,6 +5,7 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
|
import androidx.core.net.toUri
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.notification.NotificationHandler
|
import eu.kanade.tachiyomi.data.notification.NotificationHandler
|
||||||
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
||||||
@ -12,40 +13,46 @@ import eu.kanade.tachiyomi.data.notification.Notifications
|
|||||||
import eu.kanade.tachiyomi.util.system.notificationBuilder
|
import eu.kanade.tachiyomi.util.system.notificationBuilder
|
||||||
import eu.kanade.tachiyomi.util.system.notificationManager
|
import eu.kanade.tachiyomi.util.system.notificationManager
|
||||||
|
|
||||||
/**
|
internal class AppUpdateNotifier(private val context: Context) {
|
||||||
* DownloadNotifier is used to show notifications when downloading and update.
|
|
||||||
*
|
|
||||||
* @param context context of application.
|
|
||||||
*/
|
|
||||||
internal class UpdaterNotifier(private val context: Context) {
|
|
||||||
|
|
||||||
private val notificationBuilder = context.notificationBuilder(Notifications.CHANNEL_COMMON)
|
private val notificationBuilder = context.notificationBuilder(Notifications.CHANNEL_APP_UPDATE)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Call to show notification.
|
* Call to show notification.
|
||||||
*
|
*
|
||||||
* @param id id of the notification channel.
|
* @param id id of the notification channel.
|
||||||
*/
|
*/
|
||||||
private fun NotificationCompat.Builder.show(id: Int = Notifications.ID_UPDATER) {
|
private fun NotificationCompat.Builder.show(id: Int = Notifications.ID_APP_UPDATER) {
|
||||||
context.notificationManager.notify(id, build())
|
context.notificationManager.notify(id, build())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun promptUpdate(url: String) {
|
fun promptUpdate(release: GithubRelease) {
|
||||||
val intent = Intent(context, UpdaterService::class.java).apply {
|
val intent = Intent(context, AppUpdateService::class.java).apply {
|
||||||
putExtra(UpdaterService.EXTRA_DOWNLOAD_URL, url)
|
putExtra(AppUpdateService.EXTRA_DOWNLOAD_URL, release.getDownloadLink())
|
||||||
}
|
}
|
||||||
val pendingIntent = PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
val updateIntent = PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||||
|
|
||||||
|
val releaseIntent = Intent(Intent.ACTION_VIEW, release.releaseLink.toUri()).apply {
|
||||||
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||||
|
}
|
||||||
|
val releaseInfoIntent = PendingIntent.getActivity(context, release.hashCode(), releaseIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
|
||||||
|
|
||||||
with(notificationBuilder) {
|
with(notificationBuilder) {
|
||||||
setContentTitle(context.getString(R.string.app_name))
|
setContentTitle(context.getString(R.string.update_check_notification_update_available))
|
||||||
setContentText(context.getString(R.string.update_check_notification_update_available))
|
setContentText(release.version)
|
||||||
setSmallIcon(android.R.drawable.stat_sys_download_done)
|
setSmallIcon(android.R.drawable.stat_sys_download_done)
|
||||||
setContentIntent(pendingIntent)
|
setContentIntent(updateIntent)
|
||||||
|
|
||||||
clearActions()
|
clearActions()
|
||||||
addAction(
|
addAction(
|
||||||
android.R.drawable.stat_sys_download_done,
|
android.R.drawable.stat_sys_download_done,
|
||||||
context.getString(R.string.action_download),
|
context.getString(R.string.action_download),
|
||||||
pendingIntent
|
updateIntent,
|
||||||
|
)
|
||||||
|
addAction(
|
||||||
|
R.drawable.ic_info_24dp,
|
||||||
|
context.getString(R.string.whats_new),
|
||||||
|
releaseInfoIntent,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
notificationBuilder.show()
|
notificationBuilder.show()
|
||||||
@ -103,7 +110,7 @@ internal class UpdaterNotifier(private val context: Context) {
|
|||||||
addAction(
|
addAction(
|
||||||
R.drawable.ic_close_24dp,
|
R.drawable.ic_close_24dp,
|
||||||
context.getString(R.string.action_cancel),
|
context.getString(R.string.action_cancel),
|
||||||
NotificationReceiver.dismissNotificationPendingBroadcast(context, Notifications.ID_UPDATER)
|
NotificationReceiver.dismissNotificationPendingBroadcast(context, Notifications.ID_APP_UPDATER)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
notificationBuilder.show()
|
notificationBuilder.show()
|
||||||
@ -117,7 +124,7 @@ internal class UpdaterNotifier(private val context: Context) {
|
|||||||
fun onDownloadError(url: String) {
|
fun onDownloadError(url: String) {
|
||||||
with(notificationBuilder) {
|
with(notificationBuilder) {
|
||||||
setContentText(context.getString(R.string.update_check_notification_download_error))
|
setContentText(context.getString(R.string.update_check_notification_download_error))
|
||||||
setSmallIcon(android.R.drawable.stat_sys_warning)
|
setSmallIcon(R.drawable.ic_warning_white_24dp)
|
||||||
setOnlyAlertOnce(false)
|
setOnlyAlertOnce(false)
|
||||||
setProgress(0, 0, false)
|
setProgress(0, 0, false)
|
||||||
|
|
||||||
@ -125,14 +132,14 @@ internal class UpdaterNotifier(private val context: Context) {
|
|||||||
addAction(
|
addAction(
|
||||||
R.drawable.ic_refresh_24dp,
|
R.drawable.ic_refresh_24dp,
|
||||||
context.getString(R.string.action_retry),
|
context.getString(R.string.action_retry),
|
||||||
UpdaterService.downloadApkPendingService(context, url)
|
AppUpdateService.downloadApkPendingService(context, url)
|
||||||
)
|
)
|
||||||
addAction(
|
addAction(
|
||||||
R.drawable.ic_close_24dp,
|
R.drawable.ic_close_24dp,
|
||||||
context.getString(R.string.action_cancel),
|
context.getString(R.string.action_cancel),
|
||||||
NotificationReceiver.dismissNotificationPendingBroadcast(context, Notifications.ID_UPDATER)
|
NotificationReceiver.dismissNotificationPendingBroadcast(context, Notifications.ID_APP_UPDATER)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
notificationBuilder.show(Notifications.ID_UPDATER)
|
notificationBuilder.show(Notifications.ID_APP_UPDATER)
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -20,11 +20,12 @@ import eu.kanade.tachiyomi.util.storage.getUriCompat
|
|||||||
import eu.kanade.tachiyomi.util.storage.saveTo
|
import eu.kanade.tachiyomi.util.storage.saveTo
|
||||||
import eu.kanade.tachiyomi.util.system.acquireWakeLock
|
import eu.kanade.tachiyomi.util.system.acquireWakeLock
|
||||||
import eu.kanade.tachiyomi.util.system.isServiceRunning
|
import eu.kanade.tachiyomi.util.system.isServiceRunning
|
||||||
import timber.log.Timber
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
|
import logcat.LogPriority
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
class UpdaterService : Service() {
|
class AppUpdateService : Service() {
|
||||||
|
|
||||||
private val network: NetworkHelper by injectLazy()
|
private val network: NetworkHelper by injectLazy()
|
||||||
|
|
||||||
@ -33,15 +34,15 @@ class UpdaterService : Service() {
|
|||||||
*/
|
*/
|
||||||
private lateinit var wakeLock: PowerManager.WakeLock
|
private lateinit var wakeLock: PowerManager.WakeLock
|
||||||
|
|
||||||
private lateinit var notifier: UpdaterNotifier
|
private lateinit var notifier: AppUpdateNotifier
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
|
||||||
notifier = UpdaterNotifier(this)
|
notifier = AppUpdateNotifier(this)
|
||||||
wakeLock = acquireWakeLock(javaClass.name)
|
wakeLock = acquireWakeLock(javaClass.name)
|
||||||
|
|
||||||
startForeground(Notifications.ID_UPDATER, notifier.onDownloadStarted().build())
|
startForeground(Notifications.ID_APP_UPDATER, notifier.onDownloadStarted().build())
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -121,7 +122,7 @@ class UpdaterService : Service() {
|
|||||||
}
|
}
|
||||||
notifier.onDownloadFinished(apkFile.getUriCompat(this))
|
notifier.onDownloadFinished(apkFile.getUriCompat(this))
|
||||||
} catch (error: Exception) {
|
} catch (error: Exception) {
|
||||||
Timber.e(error)
|
logcat(LogPriority.ERROR, error)
|
||||||
notifier.onDownloadError(url)
|
notifier.onDownloadError(url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -138,7 +139,7 @@ class UpdaterService : Service() {
|
|||||||
* @return true if the service is running, false otherwise.
|
* @return true if the service is running, false otherwise.
|
||||||
*/
|
*/
|
||||||
private fun isRunning(context: Context): Boolean =
|
private fun isRunning(context: Context): Boolean =
|
||||||
context.isServiceRunning(UpdaterService::class.java)
|
context.isServiceRunning(AppUpdateService::class.java)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Downloads a new update and let the user install the new version from a notification.
|
* Downloads a new update and let the user install the new version from a notification.
|
||||||
@ -148,7 +149,7 @@ class UpdaterService : Service() {
|
|||||||
*/
|
*/
|
||||||
fun start(context: Context, url: String, title: String = context.getString(R.string.app_name)) {
|
fun start(context: Context, url: String, title: String = context.getString(R.string.app_name)) {
|
||||||
if (!isRunning(context)) {
|
if (!isRunning(context)) {
|
||||||
val intent = Intent(context, UpdaterService::class.java).apply {
|
val intent = Intent(context, AppUpdateService::class.java).apply {
|
||||||
putExtra(EXTRA_DOWNLOAD_TITLE, title)
|
putExtra(EXTRA_DOWNLOAD_TITLE, title)
|
||||||
putExtra(EXTRA_DOWNLOAD_URL, url)
|
putExtra(EXTRA_DOWNLOAD_URL, url)
|
||||||
}
|
}
|
||||||
@ -163,7 +164,7 @@ class UpdaterService : Service() {
|
|||||||
* @return [PendingIntent]
|
* @return [PendingIntent]
|
||||||
*/
|
*/
|
||||||
internal fun downloadApkPendingService(context: Context, url: String): PendingIntent {
|
internal fun downloadApkPendingService(context: Context, url: String): PendingIntent {
|
||||||
val intent = Intent(context, UpdaterService::class.java).apply {
|
val intent = Intent(context, AppUpdateService::class.java).apply {
|
||||||
putExtra(EXTRA_DOWNLOAD_URL, url)
|
putExtra(EXTRA_DOWNLOAD_URL, url)
|
||||||
}
|
}
|
||||||
return PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
return PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
@ -5,17 +5,13 @@ import kotlinx.serialization.SerialName
|
|||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Release object.
|
|
||||||
* Contains information about the latest release from GitHub.
|
* Contains information about the latest release from GitHub.
|
||||||
*
|
|
||||||
* @param version version of latest release.
|
|
||||||
* @param info log of latest release.
|
|
||||||
* @param assets assets of latest release.
|
|
||||||
*/
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
class GithubRelease(
|
data class GithubRelease(
|
||||||
@SerialName("tag_name") val version: String,
|
@SerialName("tag_name") val version: String,
|
||||||
@SerialName("body") val info: String,
|
@SerialName("body") val info: String,
|
||||||
|
@SerialName("html_url") val releaseLink: String,
|
||||||
@SerialName("assets") private val assets: List<Assets>
|
@SerialName("assets") private val assets: List<Assets>
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@ -37,8 +33,7 @@ class GithubRelease(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Assets class containing download url.
|
* Assets class containing download url.
|
||||||
* @param downloadLink download url.
|
|
||||||
*/
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
class Assets(@SerialName("browser_download_url") val downloadLink: String)
|
data class Assets(@SerialName("browser_download_url") val downloadLink: String)
|
||||||
}
|
}
|
||||||
|
@ -3,8 +3,8 @@ package eu.kanade.tachiyomi.extension
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import com.jakewharton.rxrelay.BehaviorRelay
|
import com.jakewharton.rxrelay.BehaviorRelay
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.data.preference.plusAssign
|
|
||||||
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
|
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
|
||||||
import eu.kanade.tachiyomi.extension.model.Extension
|
import eu.kanade.tachiyomi.extension.model.Extension
|
||||||
import eu.kanade.tachiyomi.extension.model.InstallStep
|
import eu.kanade.tachiyomi.extension.model.InstallStep
|
||||||
@ -15,8 +15,11 @@ import eu.kanade.tachiyomi.extension.util.ExtensionLoader
|
|||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.util.lang.launchNow
|
import eu.kanade.tachiyomi.util.lang.launchNow
|
||||||
|
import eu.kanade.tachiyomi.util.preference.plusAssign
|
||||||
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
|
import logcat.LogPriority
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
@ -160,7 +163,8 @@ class ExtensionManager(
|
|||||||
val extensions: List<Extension.Available> = try {
|
val extensions: List<Extension.Available> = try {
|
||||||
api.findExtensions()
|
api.findExtensions()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
context.toast(e.message)
|
logcat(LogPriority.ERROR, e)
|
||||||
|
context.toast(R.string.extension_api_error)
|
||||||
emptyList()
|
emptyList()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,8 +15,10 @@ import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
|||||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
|
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
|
||||||
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
import eu.kanade.tachiyomi.util.system.notification
|
import eu.kanade.tachiyomi.util.system.notification
|
||||||
import kotlinx.coroutines.coroutineScope
|
import kotlinx.coroutines.coroutineScope
|
||||||
|
import logcat.LogPriority
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
@ -28,6 +30,7 @@ class ExtensionUpdateJob(private val context: Context, workerParams: WorkerParam
|
|||||||
val pendingUpdates = try {
|
val pendingUpdates = try {
|
||||||
ExtensionGithubApi().checkForUpdates(context)
|
ExtensionGithubApi().checkForUpdates(context)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
logcat(LogPriority.ERROR, e)
|
||||||
return@coroutineScope Result.failure()
|
return@coroutineScope Result.failure()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -42,7 +45,7 @@ class ExtensionUpdateJob(private val context: Context, workerParams: WorkerParam
|
|||||||
NotificationManagerCompat.from(context).apply {
|
NotificationManagerCompat.from(context).apply {
|
||||||
notify(
|
notify(
|
||||||
Notifications.ID_UPDATES_TO_EXTS,
|
Notifications.ID_UPDATES_TO_EXTS,
|
||||||
context.notification(Notifications.CHANNEL_UPDATES_TO_EXTS) {
|
context.notification(Notifications.CHANNEL_EXTENSIONS_UPDATE) {
|
||||||
setContentTitle(
|
setContentTitle(
|
||||||
context.resources.getQuantityString(
|
context.resources.getQuantityString(
|
||||||
R.plurals.update_check_notification_ext_updates,
|
R.plurals.update_check_notification_ext_updates,
|
||||||
|
@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.extension.api
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
|
import eu.kanade.tachiyomi.extension.model.AvailableExtensionSources
|
||||||
import eu.kanade.tachiyomi.extension.model.Extension
|
import eu.kanade.tachiyomi.extension.model.Extension
|
||||||
import eu.kanade.tachiyomi.extension.model.LoadResult
|
import eu.kanade.tachiyomi.extension.model.LoadResult
|
||||||
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
|
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
|
||||||
@ -13,6 +14,7 @@ import eu.kanade.tachiyomi.util.lang.withIOContext
|
|||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
internal class ExtensionGithubApi {
|
internal class ExtensionGithubApi {
|
||||||
|
|
||||||
@ -21,15 +23,28 @@ internal class ExtensionGithubApi {
|
|||||||
|
|
||||||
suspend fun findExtensions(): List<Extension.Available> {
|
suspend fun findExtensions(): List<Extension.Available> {
|
||||||
return withIOContext {
|
return withIOContext {
|
||||||
networkService.client
|
val extensions = networkService.client
|
||||||
.newCall(GET("${REPO_URL_PREFIX}index.min.json"))
|
.newCall(GET("${REPO_URL_PREFIX}index.min.json"))
|
||||||
.await()
|
.await()
|
||||||
.parseAs<List<ExtensionJsonObject>>()
|
.parseAs<List<ExtensionJsonObject>>()
|
||||||
.toExtensions()
|
.toExtensions()
|
||||||
|
|
||||||
|
// Sanity check - a small number of extensions probably means something broke
|
||||||
|
// with the repo generator
|
||||||
|
if (extensions.size < 100) {
|
||||||
|
throw Exception()
|
||||||
|
}
|
||||||
|
|
||||||
|
extensions
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun checkForUpdates(context: Context): List<Extension.Installed> {
|
suspend fun checkForUpdates(context: Context): List<Extension.Installed> {
|
||||||
|
// Limit checks to once a day at most
|
||||||
|
if (Date().time < preferences.lastExtCheck().get() + TimeUnit.DAYS.toMillis(1)) {
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
val extensions = findExtensions()
|
val extensions = findExtensions()
|
||||||
|
|
||||||
preferences.lastExtCheck().set(Date().time)
|
preferences.lastExtCheck().set(Date().time)
|
||||||
@ -66,12 +81,25 @@ internal class ExtensionGithubApi {
|
|||||||
versionCode = it.code,
|
versionCode = it.code,
|
||||||
lang = it.lang,
|
lang = it.lang,
|
||||||
isNsfw = it.nsfw == 1,
|
isNsfw = it.nsfw == 1,
|
||||||
|
hasReadme = it.hasReadme == 1,
|
||||||
|
hasChangelog = it.hasChangelog == 1,
|
||||||
|
sources = it.sources?.toExtensionSources() ?: emptyList(),
|
||||||
apkName = it.apk,
|
apkName = it.apk,
|
||||||
iconUrl = "${REPO_URL_PREFIX}icon/${it.apk.replace(".apk", ".png")}"
|
iconUrl = "${REPO_URL_PREFIX}icon/${it.apk.replace(".apk", ".png")}"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun List<ExtensionSourceJsonObject>.toExtensionSources(): List<AvailableExtensionSources> {
|
||||||
|
return this.map {
|
||||||
|
AvailableExtensionSources(
|
||||||
|
name = it.name,
|
||||||
|
id = it.id,
|
||||||
|
baseUrl = it.baseUrl
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun getApkUrl(extension: Extension.Available): String {
|
fun getApkUrl(extension: Extension.Available): String {
|
||||||
return "${REPO_URL_PREFIX}apk/${extension.apkName}"
|
return "${REPO_URL_PREFIX}apk/${extension.apkName}"
|
||||||
}
|
}
|
||||||
@ -84,8 +112,19 @@ private data class ExtensionJsonObject(
|
|||||||
val name: String,
|
val name: String,
|
||||||
val pkg: String,
|
val pkg: String,
|
||||||
val apk: String,
|
val apk: String,
|
||||||
val version: String,
|
|
||||||
val code: Long,
|
|
||||||
val lang: String,
|
val lang: String,
|
||||||
|
val code: Long,
|
||||||
|
val version: String,
|
||||||
val nsfw: Int,
|
val nsfw: Int,
|
||||||
|
val hasReadme: Int = 0,
|
||||||
|
val hasChangelog: Int = 0,
|
||||||
|
val sources: List<ExtensionSourceJsonObject>?,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
private data class ExtensionSourceJsonObject(
|
||||||
|
val name: String,
|
||||||
|
val id: Long,
|
||||||
|
val baseUrl: String
|
||||||
|
|
||||||
)
|
)
|
||||||
|
@ -11,7 +11,8 @@ import android.os.Build
|
|||||||
import eu.kanade.tachiyomi.extension.model.InstallStep
|
import eu.kanade.tachiyomi.extension.model.InstallStep
|
||||||
import eu.kanade.tachiyomi.util.lang.use
|
import eu.kanade.tachiyomi.util.lang.use
|
||||||
import eu.kanade.tachiyomi.util.system.getUriSize
|
import eu.kanade.tachiyomi.util.system.getUriSize
|
||||||
import timber.log.Timber
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
|
import logcat.LogPriority
|
||||||
|
|
||||||
class PackageInstallerInstaller(private val service: Service) : Installer(service) {
|
class PackageInstallerInstaller(private val service: Service) : Installer(service) {
|
||||||
|
|
||||||
@ -23,7 +24,7 @@ class PackageInstallerInstaller(private val service: Service) : Installer(servic
|
|||||||
PackageInstaller.STATUS_PENDING_USER_ACTION -> {
|
PackageInstaller.STATUS_PENDING_USER_ACTION -> {
|
||||||
val userAction = intent.getParcelableExtra<Intent>(Intent.EXTRA_INTENT)
|
val userAction = intent.getParcelableExtra<Intent>(Intent.EXTRA_INTENT)
|
||||||
if (userAction == null) {
|
if (userAction == null) {
|
||||||
Timber.e("Fatal error for $intent")
|
logcat(LogPriority.ERROR) { "Fatal error for $intent" }
|
||||||
continueQueue(InstallStep.Error)
|
continueQueue(InstallStep.Error)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -74,7 +75,7 @@ class PackageInstallerInstaller(private val service: Service) : Installer(servic
|
|||||||
session.commit(intentSender)
|
session.commit(intentSender)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e, "Failed to install extension ${entry.downloadId} ${entry.uri}")
|
logcat(LogPriority.ERROR) { "Failed to install extension ${entry.downloadId} ${entry.uri}" }
|
||||||
activeSession?.let { (_, sessionId) ->
|
activeSession?.let { (_, sessionId) ->
|
||||||
packageInstaller.abandonSession(sessionId)
|
packageInstaller.abandonSession(sessionId)
|
||||||
}
|
}
|
||||||
|
@ -6,14 +6,15 @@ import android.os.Build
|
|||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.extension.model.InstallStep
|
import eu.kanade.tachiyomi.extension.model.InstallStep
|
||||||
import eu.kanade.tachiyomi.util.system.getUriSize
|
import eu.kanade.tachiyomi.util.system.getUriSize
|
||||||
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import logcat.LogPriority
|
||||||
import rikka.shizuku.Shizuku
|
import rikka.shizuku.Shizuku
|
||||||
import timber.log.Timber
|
|
||||||
import java.io.BufferedReader
|
import java.io.BufferedReader
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
|
||||||
@ -22,7 +23,7 @@ class ShizukuInstaller(private val service: Service) : Installer(service) {
|
|||||||
private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||||
|
|
||||||
private val shizukuDeadListener = Shizuku.OnBinderDeadListener {
|
private val shizukuDeadListener = Shizuku.OnBinderDeadListener {
|
||||||
Timber.e("Shizuku was killed prematurely")
|
logcat { "Shizuku was killed prematurely" }
|
||||||
service.stopSelf()
|
service.stopSelf()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,7 +73,7 @@ class ShizukuInstaller(private val service: Service) : Installer(service) {
|
|||||||
continueQueue(InstallStep.Installed)
|
continueQueue(InstallStep.Installed)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e, "Failed to install extension ${entry.downloadId} ${entry.uri}")
|
logcat(LogPriority.ERROR, e) { "Failed to install extension ${entry.downloadId} ${entry.uri}" }
|
||||||
if (sessionId != null) {
|
if (sessionId != null) {
|
||||||
exec("pm install-abandon $sessionId")
|
exec("pm install-abandon $sessionId")
|
||||||
}
|
}
|
||||||
@ -115,7 +116,7 @@ class ShizukuInstaller(private val service: Service) : Installer(service) {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Timber.e("Shizuku is not ready to use.")
|
logcat(LogPriority.ERROR) { "Shizuku is not ready to use." }
|
||||||
service.toast(R.string.ext_installer_shizuku_stopped)
|
service.toast(R.string.ext_installer_shizuku_stopped)
|
||||||
service.stopSelf()
|
service.stopSelf()
|
||||||
false
|
false
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.extension.model
|
package eu.kanade.tachiyomi.extension.model
|
||||||
|
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
|
|
||||||
sealed class Extension {
|
sealed class Extension {
|
||||||
@ -10,6 +11,8 @@ sealed class Extension {
|
|||||||
abstract val versionCode: Long
|
abstract val versionCode: Long
|
||||||
abstract val lang: String?
|
abstract val lang: String?
|
||||||
abstract val isNsfw: Boolean
|
abstract val isNsfw: Boolean
|
||||||
|
abstract val hasReadme: Boolean
|
||||||
|
abstract val hasChangelog: Boolean
|
||||||
|
|
||||||
data class Installed(
|
data class Installed(
|
||||||
override val name: String,
|
override val name: String,
|
||||||
@ -18,8 +21,11 @@ sealed class Extension {
|
|||||||
override val versionCode: Long,
|
override val versionCode: Long,
|
||||||
override val lang: String,
|
override val lang: String,
|
||||||
override val isNsfw: Boolean,
|
override val isNsfw: Boolean,
|
||||||
|
override val hasReadme: Boolean,
|
||||||
|
override val hasChangelog: Boolean,
|
||||||
val pkgFactory: String?,
|
val pkgFactory: String?,
|
||||||
val sources: List<Source>,
|
val sources: List<Source>,
|
||||||
|
val icon: Drawable?,
|
||||||
val hasUpdate: Boolean = false,
|
val hasUpdate: Boolean = false,
|
||||||
val isObsolete: Boolean = false,
|
val isObsolete: Boolean = false,
|
||||||
val isUnofficial: Boolean = false
|
val isUnofficial: Boolean = false
|
||||||
@ -32,6 +38,9 @@ sealed class Extension {
|
|||||||
override val versionCode: Long,
|
override val versionCode: Long,
|
||||||
override val lang: String,
|
override val lang: String,
|
||||||
override val isNsfw: Boolean,
|
override val isNsfw: Boolean,
|
||||||
|
override val hasReadme: Boolean,
|
||||||
|
override val hasChangelog: Boolean,
|
||||||
|
val sources: List<AvailableExtensionSources>,
|
||||||
val apkName: String,
|
val apkName: String,
|
||||||
val iconUrl: String
|
val iconUrl: String
|
||||||
) : Extension()
|
) : Extension()
|
||||||
@ -43,6 +52,14 @@ sealed class Extension {
|
|||||||
override val versionCode: Long,
|
override val versionCode: Long,
|
||||||
val signatureHash: String,
|
val signatureHash: String,
|
||||||
override val lang: String? = null,
|
override val lang: String? = null,
|
||||||
override val isNsfw: Boolean = false
|
override val isNsfw: Boolean = false,
|
||||||
|
override val hasReadme: Boolean = false,
|
||||||
|
override val hasChangelog: Boolean = false,
|
||||||
) : Extension()
|
) : Extension()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class AvailableExtensionSources(
|
||||||
|
val name: String,
|
||||||
|
val id: Long,
|
||||||
|
val baseUrl: String
|
||||||
|
)
|
||||||
|
@ -12,8 +12,9 @@ import eu.kanade.tachiyomi.extension.installer.Installer
|
|||||||
import eu.kanade.tachiyomi.extension.installer.PackageInstallerInstaller
|
import eu.kanade.tachiyomi.extension.installer.PackageInstallerInstaller
|
||||||
import eu.kanade.tachiyomi.extension.installer.ShizukuInstaller
|
import eu.kanade.tachiyomi.extension.installer.ShizukuInstaller
|
||||||
import eu.kanade.tachiyomi.extension.util.ExtensionInstaller.Companion.EXTRA_DOWNLOAD_ID
|
import eu.kanade.tachiyomi.extension.util.ExtensionInstaller.Companion.EXTRA_DOWNLOAD_ID
|
||||||
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
import eu.kanade.tachiyomi.util.system.notificationBuilder
|
import eu.kanade.tachiyomi.util.system.notificationBuilder
|
||||||
import timber.log.Timber
|
import logcat.LogPriority
|
||||||
|
|
||||||
class ExtensionInstallService : Service() {
|
class ExtensionInstallService : Service() {
|
||||||
|
|
||||||
@ -21,7 +22,7 @@ class ExtensionInstallService : Service() {
|
|||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
val notification = notificationBuilder(Notifications.CHANNEL_DOWNLOADER_PROGRESS) {
|
val notification = notificationBuilder(Notifications.CHANNEL_EXTENSIONS_UPDATE) {
|
||||||
setSmallIcon(R.drawable.ic_tachi)
|
setSmallIcon(R.drawable.ic_tachi)
|
||||||
setAutoCancel(false)
|
setAutoCancel(false)
|
||||||
setOngoing(true)
|
setOngoing(true)
|
||||||
@ -46,7 +47,7 @@ class ExtensionInstallService : Service() {
|
|||||||
PreferenceValues.ExtensionInstaller.PACKAGEINSTALLER -> PackageInstallerInstaller(this)
|
PreferenceValues.ExtensionInstaller.PACKAGEINSTALLER -> PackageInstallerInstaller(this)
|
||||||
PreferenceValues.ExtensionInstaller.SHIZUKU -> ShizukuInstaller(this)
|
PreferenceValues.ExtensionInstaller.SHIZUKU -> ShizukuInstaller(this)
|
||||||
else -> {
|
else -> {
|
||||||
Timber.e("Not implemented for installer $installerUsed")
|
logcat(LogPriority.ERROR) { "Not implemented for installer $installerUsed" }
|
||||||
stopSelf()
|
stopSelf()
|
||||||
return START_NOT_STICKY
|
return START_NOT_STICKY
|
||||||
}
|
}
|
||||||
|
@ -17,9 +17,10 @@ import eu.kanade.tachiyomi.extension.installer.Installer
|
|||||||
import eu.kanade.tachiyomi.extension.model.Extension
|
import eu.kanade.tachiyomi.extension.model.Extension
|
||||||
import eu.kanade.tachiyomi.extension.model.InstallStep
|
import eu.kanade.tachiyomi.extension.model.InstallStep
|
||||||
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
||||||
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
|
import logcat.LogPriority
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
import timber.log.Timber
|
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@ -109,7 +110,7 @@ internal class ExtensionInstaller(private val context: Context) {
|
|||||||
.map {
|
.map {
|
||||||
downloadManager.query(query).use { cursor ->
|
downloadManager.query(query).use { cursor ->
|
||||||
cursor.moveToFirst()
|
cursor.moveToFirst()
|
||||||
cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS))
|
cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Ignore duplicate results
|
// Ignore duplicate results
|
||||||
@ -239,7 +240,7 @@ internal class ExtensionInstaller(private val context: Context) {
|
|||||||
|
|
||||||
// Set next installation step
|
// Set next installation step
|
||||||
if (uri == null) {
|
if (uri == null) {
|
||||||
Timber.e("Couldn't locate downloaded APK")
|
logcat(LogPriority.ERROR) { "Couldn't locate downloaded APK" }
|
||||||
downloadsRelay.call(id to InstallStep.Error)
|
downloadsRelay.call(id to InstallStep.Error)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -248,7 +249,7 @@ internal class ExtensionInstaller(private val context: Context) {
|
|||||||
downloadManager.query(query).use { cursor ->
|
downloadManager.query(query).use { cursor ->
|
||||||
if (cursor.moveToFirst()) {
|
if (cursor.moveToFirst()) {
|
||||||
val localUri = cursor.getString(
|
val localUri = cursor.getString(
|
||||||
cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI)
|
cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_LOCAL_URI)
|
||||||
).removePrefix(FILE_SCHEME)
|
).removePrefix(FILE_SCHEME)
|
||||||
|
|
||||||
installApk(id, File(localUri).getUriCompat(context))
|
installApk(id, File(localUri).getUriCompat(context))
|
||||||
|
@ -13,9 +13,11 @@ import eu.kanade.tachiyomi.source.CatalogueSource
|
|||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.source.SourceFactory
|
import eu.kanade.tachiyomi.source.SourceFactory
|
||||||
import eu.kanade.tachiyomi.util.lang.Hash
|
import eu.kanade.tachiyomi.util.lang.Hash
|
||||||
|
import eu.kanade.tachiyomi.util.system.getApplicationIcon
|
||||||
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import timber.log.Timber
|
import logcat.LogPriority
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -33,8 +35,10 @@ internal object ExtensionLoader {
|
|||||||
private const val METADATA_SOURCE_CLASS = "tachiyomi.extension.class"
|
private const val METADATA_SOURCE_CLASS = "tachiyomi.extension.class"
|
||||||
private const val METADATA_SOURCE_FACTORY = "tachiyomi.extension.factory"
|
private const val METADATA_SOURCE_FACTORY = "tachiyomi.extension.factory"
|
||||||
private const val METADATA_NSFW = "tachiyomi.extension.nsfw"
|
private const val METADATA_NSFW = "tachiyomi.extension.nsfw"
|
||||||
|
private const val METADATA_HAS_README = "tachiyomi.extension.hasReadme"
|
||||||
|
private const val METADATA_HAS_CHANGELOG = "tachiyomi.extension.hasChangelog"
|
||||||
const val LIB_VERSION_MIN = 1.2
|
const val LIB_VERSION_MIN = 1.2
|
||||||
const val LIB_VERSION_MAX = 1.2
|
const val LIB_VERSION_MAX = 1.3
|
||||||
|
|
||||||
private const val PACKAGE_FLAGS = PackageManager.GET_CONFIGURATIONS or PackageManager.GET_SIGNATURES
|
private const val PACKAGE_FLAGS = PackageManager.GET_CONFIGURATIONS or PackageManager.GET_SIGNATURES
|
||||||
|
|
||||||
@ -107,7 +111,7 @@ internal object ExtensionLoader {
|
|||||||
|
|
||||||
if (versionName.isNullOrEmpty()) {
|
if (versionName.isNullOrEmpty()) {
|
||||||
val exception = Exception("Missing versionName for extension $extName")
|
val exception = Exception("Missing versionName for extension $extName")
|
||||||
Timber.w(exception)
|
logcat(LogPriority.WARN, exception)
|
||||||
return LoadResult.Error(exception)
|
return LoadResult.Error(exception)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -118,7 +122,7 @@ internal object ExtensionLoader {
|
|||||||
"Lib version is $libVersion, while only versions " +
|
"Lib version is $libVersion, while only versions " +
|
||||||
"$LIB_VERSION_MIN to $LIB_VERSION_MAX are allowed"
|
"$LIB_VERSION_MIN to $LIB_VERSION_MAX are allowed"
|
||||||
)
|
)
|
||||||
Timber.w(exception)
|
logcat(LogPriority.WARN, exception)
|
||||||
return LoadResult.Error(exception)
|
return LoadResult.Error(exception)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -128,7 +132,7 @@ internal object ExtensionLoader {
|
|||||||
return LoadResult.Error("Package $pkgName isn't signed")
|
return LoadResult.Error("Package $pkgName isn't signed")
|
||||||
} else if (signatureHash !in trustedSignatures) {
|
} else if (signatureHash !in trustedSignatures) {
|
||||||
val extension = Extension.Untrusted(extName, pkgName, versionName, versionCode, signatureHash)
|
val extension = Extension.Untrusted(extName, pkgName, versionName, versionCode, signatureHash)
|
||||||
Timber.w("Extension $pkgName isn't trusted")
|
logcat(LogPriority.WARN) { "Extension $pkgName isn't trusted" }
|
||||||
return LoadResult.Untrusted(extension)
|
return LoadResult.Untrusted(extension)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -137,6 +141,9 @@ internal object ExtensionLoader {
|
|||||||
return LoadResult.Error("NSFW extension $pkgName not allowed")
|
return LoadResult.Error("NSFW extension $pkgName not allowed")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val hasReadme = appInfo.metaData.getInt(METADATA_HAS_README, 0) == 1
|
||||||
|
val hasChangelog = appInfo.metaData.getInt(METADATA_HAS_CHANGELOG, 0) == 1
|
||||||
|
|
||||||
val classLoader = PathClassLoader(appInfo.sourceDir, null, context.classLoader)
|
val classLoader = PathClassLoader(appInfo.sourceDir, null, context.classLoader)
|
||||||
|
|
||||||
val sources = appInfo.metaData.getString(METADATA_SOURCE_CLASS)!!
|
val sources = appInfo.metaData.getString(METADATA_SOURCE_CLASS)!!
|
||||||
@ -157,7 +164,7 @@ internal object ExtensionLoader {
|
|||||||
else -> throw Exception("Unknown source class type! ${obj.javaClass}")
|
else -> throw Exception("Unknown source class type! ${obj.javaClass}")
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Timber.e(e, "Extension load error: $extName ($it)")
|
logcat(LogPriority.ERROR, e) { "Extension load error: $extName ($it)" }
|
||||||
return LoadResult.Error(e)
|
return LoadResult.Error(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -178,9 +185,12 @@ internal object ExtensionLoader {
|
|||||||
versionCode,
|
versionCode,
|
||||||
lang,
|
lang,
|
||||||
isNsfw,
|
isNsfw,
|
||||||
|
hasReadme,
|
||||||
|
hasChangelog,
|
||||||
sources = sources,
|
sources = sources,
|
||||||
pkgFactory = appInfo.metaData.getString(METADATA_SOURCE_FACTORY),
|
pkgFactory = appInfo.metaData.getString(METADATA_SOURCE_FACTORY),
|
||||||
isUnofficial = signatureHash != officialSignature
|
isUnofficial = signatureHash != officialSignature,
|
||||||
|
icon = context.getApplicationIcon(pkgName)
|
||||||
)
|
)
|
||||||
return LoadResult.Success(extension)
|
return LoadResult.Success(extension)
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.network
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import coil.util.CoilUtils
|
import coil.util.CoilUtils
|
||||||
import eu.kanade.tachiyomi.BuildConfig
|
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.network.interceptor.CloudflareInterceptor
|
import eu.kanade.tachiyomi.network.interceptor.CloudflareInterceptor
|
||||||
import eu.kanade.tachiyomi.network.interceptor.UserAgentInterceptor
|
import eu.kanade.tachiyomi.network.interceptor.UserAgentInterceptor
|
||||||
@ -31,7 +30,7 @@ class NetworkHelper(context: Context) {
|
|||||||
.readTimeout(30, TimeUnit.SECONDS)
|
.readTimeout(30, TimeUnit.SECONDS)
|
||||||
.addInterceptor(UserAgentInterceptor())
|
.addInterceptor(UserAgentInterceptor())
|
||||||
|
|
||||||
if (BuildConfig.DEBUG) {
|
if (preferences.verboseLogging()) {
|
||||||
val httpLoggingInterceptor = HttpLoggingInterceptor().apply {
|
val httpLoggingInterceptor = HttpLoggingInterceptor().apply {
|
||||||
level = HttpLoggingInterceptor.Level.HEADERS
|
level = HttpLoggingInterceptor.Level.HEADERS
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.network.interceptor
|
|||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.os.Build
|
||||||
import android.webkit.WebSettings
|
import android.webkit.WebSettings
|
||||||
import android.webkit.WebView
|
import android.webkit.WebView
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
@ -10,6 +11,7 @@ import eu.kanade.tachiyomi.R
|
|||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.util.lang.launchUI
|
import eu.kanade.tachiyomi.util.lang.launchUI
|
||||||
|
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
||||||
import eu.kanade.tachiyomi.util.system.WebViewClientCompat
|
import eu.kanade.tachiyomi.util.system.WebViewClientCompat
|
||||||
import eu.kanade.tachiyomi.util.system.WebViewUtil
|
import eu.kanade.tachiyomi.util.system.WebViewUtil
|
||||||
import eu.kanade.tachiyomi.util.system.isOutdated
|
import eu.kanade.tachiyomi.util.system.isOutdated
|
||||||
@ -37,6 +39,13 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
|
|||||||
* Application class.
|
* Application class.
|
||||||
*/
|
*/
|
||||||
private val initWebView by lazy {
|
private val initWebView by lazy {
|
||||||
|
// Crashes on some devices. We skip this in some cases since the only impact is slower
|
||||||
|
// WebView init in those rare cases.
|
||||||
|
// See https://bugs.chromium.org/p/chromium/issues/detail?id=1279562
|
||||||
|
if (DeviceUtil.isMiui || Build.VERSION.SDK_INT == Build.VERSION_CODES.S && DeviceUtil.isSamsung) {
|
||||||
|
return@lazy
|
||||||
|
}
|
||||||
|
|
||||||
WebSettings.getDefaultUserAgent(context)
|
WebSettings.getDefaultUserAgent(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -56,7 +65,7 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
|
|||||||
val response = chain.proceed(originalRequest)
|
val response = chain.proceed(originalRequest)
|
||||||
|
|
||||||
// Check if Cloudflare anti-bot is on
|
// Check if Cloudflare anti-bot is on
|
||||||
if (response.code != 503 || response.header("Server") !in SERVER_CHECK) {
|
if (response.code !in ERROR_CODES || response.header("Server") !in SERVER_CHECK) {
|
||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,9 +77,12 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
|
|||||||
resolveWithWebView(originalRequest, oldCookie)
|
resolveWithWebView(originalRequest, oldCookie)
|
||||||
|
|
||||||
return chain.proceed(originalRequest)
|
return chain.proceed(originalRequest)
|
||||||
|
}
|
||||||
|
// Because OkHttp's enqueue only handles IOExceptions, wrap the exception so that
|
||||||
|
// we don't crash the entire app
|
||||||
|
catch (e: CloudflareBypassException) {
|
||||||
|
throw IOException(context.getString(R.string.information_cloudflare_bypass_failure))
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
// Because OkHttp's enqueue only handles IOExceptions, wrap the exception so that
|
|
||||||
// we don't crash the entire app
|
|
||||||
throw IOException(e)
|
throw IOException(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -127,7 +139,7 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
|
|||||||
isMainFrame: Boolean
|
isMainFrame: Boolean
|
||||||
) {
|
) {
|
||||||
if (isMainFrame) {
|
if (isMainFrame) {
|
||||||
if (errorCode == 503) {
|
if (errorCode in ERROR_CODES) {
|
||||||
// Found the Cloudflare challenge page.
|
// Found the Cloudflare challenge page.
|
||||||
challengeFound = true
|
challengeFound = true
|
||||||
} else {
|
} else {
|
||||||
@ -162,12 +174,15 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
|
|||||||
context.toast(R.string.information_webview_outdated, Toast.LENGTH_LONG)
|
context.toast(R.string.information_webview_outdated, Toast.LENGTH_LONG)
|
||||||
}
|
}
|
||||||
|
|
||||||
throw Exception(context.getString(R.string.information_cloudflare_bypass_failure))
|
throw CloudflareBypassException()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
private val ERROR_CODES = listOf(403, 503)
|
||||||
private val SERVER_CHECK = arrayOf("cloudflare-nginx", "cloudflare")
|
private val SERVER_CHECK = arrayOf("cloudflare-nginx", "cloudflare")
|
||||||
private val COOKIE_NAMES = listOf("cf_clearance")
|
private val COOKIE_NAMES = listOf("cf_clearance")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class CloudflareBypassException : Exception()
|
||||||
|
@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.network.interceptor
|
|||||||
|
|
||||||
import android.os.SystemClock
|
import android.os.SystemClock
|
||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
@ -13,14 +14,22 @@ import java.util.concurrent.TimeUnit
|
|||||||
* permits = 5, period = 1, unit = seconds => 5 requests per second
|
* permits = 5, period = 1, unit = seconds => 5 requests per second
|
||||||
* permits = 10, period = 2, unit = minutes => 10 requests per 2 minutes
|
* permits = 10, period = 2, unit = minutes => 10 requests per 2 minutes
|
||||||
*
|
*
|
||||||
|
* @since extension-lib 1.3
|
||||||
|
*
|
||||||
* @param permits {Int} Number of requests allowed within a period of units.
|
* @param permits {Int} Number of requests allowed within a period of units.
|
||||||
* @param period {Long} The limiting duration. Defaults to 1.
|
* @param period {Long} The limiting duration. Defaults to 1.
|
||||||
* @param unit {TimeUnit} The unit of time for the period. Defaults to seconds.
|
* @param unit {TimeUnit} The unit of time for the period. Defaults to seconds.
|
||||||
*/
|
*/
|
||||||
class RateLimitInterceptor(
|
fun OkHttpClient.Builder.rateLimit(
|
||||||
|
permits: Int,
|
||||||
|
period: Long = 1,
|
||||||
|
unit: TimeUnit = TimeUnit.SECONDS,
|
||||||
|
) = addInterceptor(RateLimitInterceptor(permits, period, unit))
|
||||||
|
|
||||||
|
private class RateLimitInterceptor(
|
||||||
private val permits: Int,
|
private val permits: Int,
|
||||||
private val period: Long = 1,
|
period: Long,
|
||||||
private val unit: TimeUnit = TimeUnit.SECONDS
|
unit: TimeUnit,
|
||||||
) : Interceptor {
|
) : Interceptor {
|
||||||
|
|
||||||
private val requestQueue = ArrayList<Long>(permits)
|
private val requestQueue = ArrayList<Long>(permits)
|
||||||
|
@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.network.interceptor
|
|||||||
import android.os.SystemClock
|
import android.os.SystemClock
|
||||||
import okhttp3.HttpUrl
|
import okhttp3.HttpUrl
|
||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
@ -14,16 +15,25 @@ import java.util.concurrent.TimeUnit
|
|||||||
* httpUrl = "api.manga.com".toHttpUrlOrNull(), permits = 5, period = 1, unit = seconds => 5 requests per second to api.manga.com
|
* httpUrl = "api.manga.com".toHttpUrlOrNull(), permits = 5, period = 1, unit = seconds => 5 requests per second to api.manga.com
|
||||||
* httpUrl = "imagecdn.manga.com".toHttpUrlOrNull(), permits = 10, period = 2, unit = minutes => 10 requests per 2 minutes to imagecdn.manga.com
|
* httpUrl = "imagecdn.manga.com".toHttpUrlOrNull(), permits = 10, period = 2, unit = minutes => 10 requests per 2 minutes to imagecdn.manga.com
|
||||||
*
|
*
|
||||||
|
* @since extension-lib 1.3
|
||||||
|
*
|
||||||
* @param httpUrl {HttpUrl} The url host that this interceptor should handle. Will get url's host by using HttpUrl.host()
|
* @param httpUrl {HttpUrl} The url host that this interceptor should handle. Will get url's host by using HttpUrl.host()
|
||||||
* @param permits {Int} Number of requests allowed within a period of units.
|
* @param permits {Int} Number of requests allowed within a period of units.
|
||||||
* @param period {Long} The limiting duration. Defaults to 1.
|
* @param period {Long} The limiting duration. Defaults to 1.
|
||||||
* @param unit {TimeUnit} The unit of time for the period. Defaults to seconds.
|
* @param unit {TimeUnit} The unit of time for the period. Defaults to seconds.
|
||||||
*/
|
*/
|
||||||
|
fun OkHttpClient.Builder.rateLimitHost(
|
||||||
|
httpUrl: HttpUrl,
|
||||||
|
permits: Int,
|
||||||
|
period: Long = 1,
|
||||||
|
unit: TimeUnit = TimeUnit.SECONDS,
|
||||||
|
) = addInterceptor(SpecificHostRateLimitInterceptor(httpUrl, permits, period, unit))
|
||||||
|
|
||||||
class SpecificHostRateLimitInterceptor(
|
class SpecificHostRateLimitInterceptor(
|
||||||
private val httpUrl: HttpUrl,
|
httpUrl: HttpUrl,
|
||||||
private val permits: Int,
|
private val permits: Int,
|
||||||
private val period: Long = 1,
|
period: Long,
|
||||||
private val unit: TimeUnit = TimeUnit.SECONDS
|
unit: TimeUnit,
|
||||||
) : Interceptor {
|
) : Interceptor {
|
||||||
|
|
||||||
private val requestQueue = ArrayList<Long>(permits)
|
private val requestQueue = ArrayList<Long>(permits)
|
||||||
|
@ -6,14 +6,19 @@ import eu.kanade.tachiyomi.R
|
|||||||
import eu.kanade.tachiyomi.source.model.Filter
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
import eu.kanade.tachiyomi.source.model.toChapterInfo
|
||||||
|
import eu.kanade.tachiyomi.source.model.toMangaInfo
|
||||||
|
import eu.kanade.tachiyomi.source.model.toSChapter
|
||||||
|
import eu.kanade.tachiyomi.source.model.toSManga
|
||||||
import eu.kanade.tachiyomi.util.chapter.ChapterRecognition
|
import eu.kanade.tachiyomi.util.chapter.ChapterRecognition
|
||||||
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
|
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
|
||||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||||
import eu.kanade.tachiyomi.util.storage.EpubFile
|
import eu.kanade.tachiyomi.util.storage.EpubFile
|
||||||
import eu.kanade.tachiyomi.util.system.ImageUtil
|
import eu.kanade.tachiyomi.util.system.ImageUtil
|
||||||
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.JsonObject
|
import kotlinx.serialization.json.JsonObject
|
||||||
import kotlinx.serialization.json.contentOrNull
|
import kotlinx.serialization.json.contentOrNull
|
||||||
@ -21,23 +26,24 @@ import kotlinx.serialization.json.decodeFromStream
|
|||||||
import kotlinx.serialization.json.intOrNull
|
import kotlinx.serialization.json.intOrNull
|
||||||
import kotlinx.serialization.json.jsonArray
|
import kotlinx.serialization.json.jsonArray
|
||||||
import kotlinx.serialization.json.jsonPrimitive
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
|
import logcat.LogPriority
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import timber.log.Timber
|
import tachiyomi.source.model.ChapterInfo
|
||||||
|
import tachiyomi.source.model.MangaInfo
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileInputStream
|
import java.io.FileInputStream
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.util.Locale
|
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import java.util.zip.ZipFile
|
import java.util.zip.ZipFile
|
||||||
|
|
||||||
class LocalSource(private val context: Context) : CatalogueSource {
|
class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSource {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val ID = 0L
|
const val ID = 0L
|
||||||
const val HELP_URL = "https://tachiyomi.org/help/guides/local-manga/"
|
const val HELP_URL = "https://tachiyomi.org/help/guides/local-manga/"
|
||||||
|
|
||||||
private val SUPPORTED_ARCHIVE_TYPES = setOf("zip", "rar", "cbr", "cbz", "epub")
|
private const val COVER_NAME = "cover.jpg"
|
||||||
|
|
||||||
private val LATEST_THRESHOLD = TimeUnit.MILLISECONDS.convert(7, TimeUnit.DAYS)
|
private val LATEST_THRESHOLD = TimeUnit.MILLISECONDS.convert(7, TimeUnit.DAYS)
|
||||||
|
|
||||||
fun updateCover(context: Context, manga: SManga, input: InputStream): File? {
|
fun updateCover(context: Context, manga: SManga, input: InputStream): File? {
|
||||||
@ -46,17 +52,18 @@ class LocalSource(private val context: Context) : CatalogueSource {
|
|||||||
input.close()
|
input.close()
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
val cover = getCoverFile(File("${dir.absolutePath}/${manga.url}"))
|
var cover = getCoverFile(File("${dir.absolutePath}/${manga.url}"))
|
||||||
|
if (cover == null) {
|
||||||
if (cover != null && cover.exists()) {
|
cover = File("${dir.absolutePath}/${manga.url}", COVER_NAME)
|
||||||
// It might not exist if using the external SD card
|
}
|
||||||
cover.parentFile?.mkdirs()
|
// It might not exist if using the external SD card
|
||||||
input.use {
|
cover.parentFile?.mkdirs()
|
||||||
cover.outputStream().use {
|
input.use {
|
||||||
input.copyTo(it)
|
cover.outputStream().use {
|
||||||
}
|
input.copyTo(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
manga.thumbnail_url = cover.absolutePath
|
||||||
return cover
|
return cover
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -79,10 +86,10 @@ class LocalSource(private val context: Context) : CatalogueSource {
|
|||||||
|
|
||||||
override val id = ID
|
override val id = ID
|
||||||
override val name = context.getString(R.string.local_source)
|
override val name = context.getString(R.string.local_source)
|
||||||
override val lang = ""
|
override val lang = "other"
|
||||||
override val supportsLatest = true
|
override val supportsLatest = true
|
||||||
|
|
||||||
override fun toString() = context.getString(R.string.local_source)
|
override fun toString() = name
|
||||||
|
|
||||||
override fun fetchPopularManga(page: Int) = fetchSearchManga(page, "", POPULAR_FILTERS)
|
override fun fetchPopularManga(page: Int) = fetchSearchManga(page, "", POPULAR_FILTERS)
|
||||||
|
|
||||||
@ -103,9 +110,9 @@ class LocalSource(private val context: Context) : CatalogueSource {
|
|||||||
when (state?.index) {
|
when (state?.index) {
|
||||||
0 -> {
|
0 -> {
|
||||||
mangaDirs = if (state.ascending) {
|
mangaDirs = if (state.ascending) {
|
||||||
mangaDirs.sortedBy { it.name.lowercase(Locale.ENGLISH) }
|
mangaDirs.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER, { it.name }))
|
||||||
} else {
|
} else {
|
||||||
mangaDirs.sortedByDescending { it.name.lowercase(Locale.ENGLISH) }
|
mangaDirs.sortedWith(compareByDescending(String.CASE_INSENSITIVE_ORDER, { it.name }))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
1 -> {
|
1 -> {
|
||||||
@ -131,23 +138,27 @@ class LocalSource(private val context: Context) : CatalogueSource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val chapters = fetchChapterList(this).toBlocking().first()
|
val sManga = this
|
||||||
if (chapters.isNotEmpty()) {
|
val mangaInfo = this.toMangaInfo()
|
||||||
val chapter = chapters.last()
|
runBlocking {
|
||||||
val format = getFormat(chapter)
|
val chapters = getChapterList(mangaInfo)
|
||||||
if (format is Format.Epub) {
|
if (chapters.isNotEmpty()) {
|
||||||
EpubFile(format.file).use { epub ->
|
val chapter = chapters.last().toSChapter()
|
||||||
epub.fillMangaMetadata(this)
|
val format = getFormat(chapter)
|
||||||
|
if (format is Format.Epub) {
|
||||||
|
EpubFile(format.file).use { epub ->
|
||||||
|
epub.fillMangaMetadata(sManga)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Copy the cover from the first chapter found.
|
// Copy the cover from the first chapter found.
|
||||||
if (thumbnail_url == null) {
|
if (thumbnail_url == null) {
|
||||||
try {
|
try {
|
||||||
val dest = updateCover(chapter, this)
|
val dest = updateCover(chapter, sManga)
|
||||||
thumbnail_url = dest?.absolutePath
|
thumbnail_url = dest?.absolutePath
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e)
|
logcat(LogPriority.ERROR, e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -159,36 +170,40 @@ class LocalSource(private val context: Context) : CatalogueSource {
|
|||||||
|
|
||||||
override fun fetchLatestUpdates(page: Int) = fetchSearchManga(page, "", LATEST_FILTERS)
|
override fun fetchLatestUpdates(page: Int) = fetchSearchManga(page, "", LATEST_FILTERS)
|
||||||
|
|
||||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
override suspend fun getMangaDetails(manga: MangaInfo): MangaInfo {
|
||||||
getBaseDirectories(context)
|
val localDetails = getBaseDirectories(context)
|
||||||
.asSequence()
|
.asSequence()
|
||||||
.mapNotNull { File(it, manga.url).listFiles()?.toList() }
|
.mapNotNull { File(it, manga.key).listFiles()?.toList() }
|
||||||
.flatten()
|
.flatten()
|
||||||
.firstOrNull { it.extension == "json" }
|
.firstOrNull { it.extension.equals("json", ignoreCase = true) }
|
||||||
?.apply {
|
|
||||||
val obj = json.decodeFromStream<JsonObject>(inputStream())
|
|
||||||
|
|
||||||
manga.title = obj["title"]?.jsonPrimitive?.contentOrNull ?: manga.title
|
return if (localDetails != null) {
|
||||||
manga.author = obj["author"]?.jsonPrimitive?.contentOrNull ?: manga.author
|
val obj = json.decodeFromStream<JsonObject>(localDetails.inputStream())
|
||||||
manga.artist = obj["artist"]?.jsonPrimitive?.contentOrNull ?: manga.artist
|
|
||||||
manga.description = obj["description"]?.jsonPrimitive?.contentOrNull ?: manga.description
|
|
||||||
manga.genre = obj["genre"]?.jsonArray?.joinToString(", ") { it.jsonPrimitive.content }
|
|
||||||
?: manga.genre
|
|
||||||
manga.status = obj["status"]?.jsonPrimitive?.intOrNull ?: manga.status
|
|
||||||
}
|
|
||||||
|
|
||||||
return Observable.just(manga)
|
manga.copy(
|
||||||
|
title = obj["title"]?.jsonPrimitive?.contentOrNull ?: manga.title,
|
||||||
|
author = obj["author"]?.jsonPrimitive?.contentOrNull ?: manga.author,
|
||||||
|
artist = obj["artist"]?.jsonPrimitive?.contentOrNull ?: manga.artist,
|
||||||
|
description = obj["description"]?.jsonPrimitive?.contentOrNull ?: manga.description,
|
||||||
|
genres = obj["genre"]?.jsonArray?.map { it.jsonPrimitive.content } ?: manga.genres,
|
||||||
|
status = obj["status"]?.jsonPrimitive?.intOrNull ?: manga.status,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
manga
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
override suspend fun getChapterList(manga: MangaInfo): List<ChapterInfo> {
|
||||||
|
val sManga = manga.toSManga()
|
||||||
|
|
||||||
val chapters = getBaseDirectories(context)
|
val chapters = getBaseDirectories(context)
|
||||||
.asSequence()
|
.asSequence()
|
||||||
.mapNotNull { File(it, manga.url).listFiles()?.toList() }
|
.mapNotNull { File(it, manga.key).listFiles()?.toList() }
|
||||||
.flatten()
|
.flatten()
|
||||||
.filter { it.isDirectory || isSupportedFile(it.extension) }
|
.filter { it.isDirectory || isSupportedFile(it.extension) }
|
||||||
.map { chapterFile ->
|
.map { chapterFile ->
|
||||||
SChapter.create().apply {
|
SChapter.create().apply {
|
||||||
url = "${manga.url}/${chapterFile.name}"
|
url = "${manga.key}/${chapterFile.name}"
|
||||||
name = if (chapterFile.isDirectory) {
|
name = if (chapterFile.isDirectory) {
|
||||||
chapterFile.name
|
chapterFile.name
|
||||||
} else {
|
} else {
|
||||||
@ -196,63 +211,36 @@ class LocalSource(private val context: Context) : CatalogueSource {
|
|||||||
}
|
}
|
||||||
date_upload = chapterFile.lastModified()
|
date_upload = chapterFile.lastModified()
|
||||||
|
|
||||||
val format = getFormat(this)
|
val format = getFormat(chapterFile)
|
||||||
if (format is Format.Epub) {
|
if (format is Format.Epub) {
|
||||||
EpubFile(format.file).use { epub ->
|
EpubFile(format.file).use { epub ->
|
||||||
epub.fillChapterMetadata(this)
|
epub.fillChapterMetadata(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val chapNameCut = stripMangaTitle(name, manga.title)
|
name = getCleanChapterTitle(name, manga.title)
|
||||||
if (chapNameCut.isNotEmpty()) name = chapNameCut
|
ChapterRecognition.parseChapterNumber(this, sManga)
|
||||||
ChapterRecognition.parseChapterNumber(this, manga)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.map { it.toChapterInfo() }
|
||||||
.sortedWith { c1, c2 ->
|
.sortedWith { c1, c2 ->
|
||||||
val c = c2.chapter_number.compareTo(c1.chapter_number)
|
val c = c2.number.compareTo(c1.number)
|
||||||
if (c == 0) c2.name.compareToCaseInsensitiveNaturalOrder(c1.name) else c
|
if (c == 0) c2.name.compareToCaseInsensitiveNaturalOrder(c1.name) else c
|
||||||
}
|
}
|
||||||
.toList()
|
.toList()
|
||||||
|
|
||||||
return Observable.just(chapters)
|
return chapters
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun getPageList(chapter: ChapterInfo) = throw Exception("Unused")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Strips the manga title from a chapter name, matching only based on alphanumeric and whitespace
|
* Strips the manga title from a chapter name and trim whitespace/delimiter characters.
|
||||||
* characters.
|
|
||||||
*/
|
*/
|
||||||
private fun stripMangaTitle(chapterName: String, mangaTitle: String): String {
|
private fun getCleanChapterTitle(chapterName: String, mangaTitle: String): String {
|
||||||
var chapterNameIndex = 0
|
return chapterName
|
||||||
var mangaTitleIndex = 0
|
.replace(mangaTitle, "")
|
||||||
while (chapterNameIndex < chapterName.length && mangaTitleIndex < mangaTitle.length) {
|
.trim(*WHITESPACE_CHARS.toCharArray(), '-', '_', ',', ':')
|
||||||
val chapterChar = chapterName[chapterNameIndex]
|
|
||||||
val mangaChar = mangaTitle[mangaTitleIndex]
|
|
||||||
if (!chapterChar.equals(mangaChar, true)) {
|
|
||||||
val invalidChapterChar = !chapterChar.isLetterOrDigit() && !chapterChar.isWhitespace()
|
|
||||||
val invalidMangaChar = !mangaChar.isLetterOrDigit() && !mangaChar.isWhitespace()
|
|
||||||
|
|
||||||
if (!invalidChapterChar && !invalidMangaChar) {
|
|
||||||
return chapterName
|
|
||||||
}
|
|
||||||
|
|
||||||
if (invalidChapterChar) {
|
|
||||||
chapterNameIndex++
|
|
||||||
}
|
|
||||||
|
|
||||||
if (invalidMangaChar) {
|
|
||||||
mangaTitleIndex++
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
chapterNameIndex++
|
|
||||||
mangaTitleIndex++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return chapterName.substring(chapterNameIndex).trimStart(' ', '-', '_', ',', ':')
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
|
|
||||||
return Observable.error(Exception("Unused"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isSupportedFile(extension: String): Boolean {
|
private fun isSupportedFile(extension: String): Boolean {
|
||||||
@ -338,3 +326,34 @@ class LocalSource(private val context: Context) : CatalogueSource {
|
|||||||
data class Epub(val file: File) : Format()
|
data class Epub(val file: File) : Format()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val SUPPORTED_ARCHIVE_TYPES = listOf("zip", "cbz", "rar", "cbr", "epub")
|
||||||
|
|
||||||
|
private val WHITESPACE_CHARS = arrayOf(
|
||||||
|
' ',
|
||||||
|
'\u0009',
|
||||||
|
'\u000A',
|
||||||
|
'\u000B',
|
||||||
|
'\u000C',
|
||||||
|
'\u000D',
|
||||||
|
'\u0020',
|
||||||
|
'\u0085',
|
||||||
|
'\u00A0',
|
||||||
|
'\u1680',
|
||||||
|
'\u2000',
|
||||||
|
'\u2001',
|
||||||
|
'\u2002',
|
||||||
|
'\u2003',
|
||||||
|
'\u2004',
|
||||||
|
'\u2005',
|
||||||
|
'\u2006',
|
||||||
|
'\u2007',
|
||||||
|
'\u2008',
|
||||||
|
'\u2009',
|
||||||
|
'\u200A',
|
||||||
|
'\u2028',
|
||||||
|
'\u2029',
|
||||||
|
'\u202F',
|
||||||
|
'\u205F',
|
||||||
|
'\u3000',
|
||||||
|
)
|
||||||
|
@ -40,24 +40,34 @@ interface Source : tachiyomi.source.Source {
|
|||||||
*
|
*
|
||||||
* @param manga the manga to update.
|
* @param manga the manga to update.
|
||||||
*/
|
*/
|
||||||
@Deprecated("Use getMangaDetails instead")
|
@Deprecated(
|
||||||
fun fetchMangaDetails(manga: SManga): Observable<SManga>
|
"Use the 1.x API instead",
|
||||||
|
ReplaceWith("getMangaDetails")
|
||||||
|
)
|
||||||
|
fun fetchMangaDetails(manga: SManga): Observable<SManga> = throw IllegalStateException("Not used")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an observable with all the available chapters for a manga.
|
* Returns an observable with all the available chapters for a manga.
|
||||||
*
|
*
|
||||||
* @param manga the manga to update.
|
* @param manga the manga to update.
|
||||||
*/
|
*/
|
||||||
@Deprecated("Use getChapterList instead")
|
@Deprecated(
|
||||||
fun fetchChapterList(manga: SManga): Observable<List<SChapter>>
|
"Use the 1.x API instead",
|
||||||
|
ReplaceWith("getChapterList")
|
||||||
|
)
|
||||||
|
fun fetchChapterList(manga: SManga): Observable<List<SChapter>> = throw IllegalStateException("Not used")
|
||||||
|
|
||||||
|
// TODO: remove direct usages on this method
|
||||||
/**
|
/**
|
||||||
* Returns an observable with the list of pages a chapter has.
|
* Returns an observable with the list of pages a chapter has.
|
||||||
*
|
*
|
||||||
* @param chapter the chapter.
|
* @param chapter the chapter.
|
||||||
*/
|
*/
|
||||||
@Deprecated("Use getPageList instead")
|
@Deprecated(
|
||||||
fun fetchPageList(chapter: SChapter): Observable<List<Page>>
|
"Use the 1.x API instead",
|
||||||
|
ReplaceWith("getPageList")
|
||||||
|
)
|
||||||
|
fun fetchPageList(chapter: SChapter): Observable<List<Page>> = Observable.empty()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* [1.x API] Get the updated details for a manga.
|
* [1.x API] Get the updated details for a manga.
|
||||||
|
@ -0,0 +1,8 @@
|
|||||||
|
package eu.kanade.tachiyomi.source
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A source that explicitly doesn't require traffic considerations.
|
||||||
|
*
|
||||||
|
* This typically applies for self-hosted sources.
|
||||||
|
*/
|
||||||
|
interface UnmeteredSource
|
@ -2,9 +2,12 @@ package eu.kanade.tachiyomi.source.model
|
|||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import eu.kanade.tachiyomi.network.ProgressListener
|
import eu.kanade.tachiyomi.network.ProgressListener
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.Transient
|
||||||
import rx.subjects.Subject
|
import rx.subjects.Subject
|
||||||
import tachiyomi.source.model.PageUrl
|
import tachiyomi.source.model.PageUrl
|
||||||
|
|
||||||
|
@Serializable
|
||||||
open class Page(
|
open class Page(
|
||||||
val index: Int,
|
val index: Int,
|
||||||
val url: String = "",
|
val url: String = "",
|
||||||
|
@ -56,6 +56,9 @@ interface SManga : Serializable {
|
|||||||
const val ONGOING = 1
|
const val ONGOING = 1
|
||||||
const val COMPLETED = 2
|
const val COMPLETED = 2
|
||||||
const val LICENSED = 3
|
const val LICENSED = 3
|
||||||
|
const val PUBLISHING_FINISHED = 4
|
||||||
|
const val CANCELLED = 5
|
||||||
|
const val ON_HIATUS = 6
|
||||||
|
|
||||||
fun create(): SManga {
|
fun create(): SManga {
|
||||||
return SMangaImpl()
|
return SMangaImpl()
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user