mirror of
https://github.com/mihonapp/mihon.git
synced 2025-06-26 19:17:51 +02:00
Compare commits
364 Commits
Author | SHA1 | Date | |
---|---|---|---|
b690de55e5 | |||
83fda20078 | |||
f656a37045 | |||
c58b495433 | |||
242aeb6a68 | |||
d9969cea8a | |||
d61db5931e | |||
0ea3ac9807 | |||
f9e43f574f | |||
5ef11e61d0 | |||
48546c3db4 | |||
4d87ed496c | |||
06d12e6562 | |||
477e3d9b94 | |||
3c16082636 | |||
29aee68ec7 | |||
75e23299b4 | |||
935ff1ee98 | |||
c672cb81ec | |||
7559c133c0 | |||
589bdba0b1 | |||
aca65f13bb | |||
7bf30a094a | |||
5454279a8e | |||
006bcdf934 | |||
b00f00730d | |||
f2c48480b6 | |||
1730dd6af1 | |||
2501fef9e4 | |||
12e41b6e6f | |||
c892c793a8 | |||
3a82b4d924 | |||
b4b3a4d286 | |||
448702e5be | |||
2ef1f07aae | |||
1a319601de | |||
cdf242e8c8 | |||
aee785a8bb | |||
d45fc1e245 | |||
14500ba4f8 | |||
345e9c2a9a | |||
b53e24e0db | |||
d3a73fc228 | |||
c2812fca24 | |||
856847a60a | |||
748e2480d3 | |||
2ebc8d9ae5 | |||
e28b015580 | |||
e4bc8990fb | |||
a179327d9d | |||
823749fc1e | |||
2b5d9fd76b | |||
7a972dfdb7 | |||
c31e75f02f | |||
b56b8b55b4 | |||
2695a4d8c7 | |||
1a4dad72a9 | |||
b7e6b4c28a | |||
dc2d470413 | |||
293b967858 | |||
c637172ee0 | |||
e468554fd9 | |||
5b5eb92184 | |||
58ebf14691 | |||
992bab4f79 | |||
6fe650319d | |||
f301dc64f0 | |||
33a2219716 | |||
62480f090b | |||
e7937fe562 | |||
287489d7d0 | |||
2df0236669 | |||
c54d77333f | |||
8c494f314c | |||
8cea78de83 | |||
b6468c7e31 | |||
1967923a94 | |||
91004ad514 | |||
a2ee4e63ae | |||
4d8289cd36 | |||
289264878e | |||
768bb7b503 | |||
db4ae134aa | |||
7329f03bc5 | |||
82ea643c7d | |||
741c10e0b9 | |||
34bb90f3c2 | |||
f04cf72c0c | |||
157438e0c1 | |||
75b23c99ec | |||
6bb3070c57 | |||
7df10b076c | |||
2245658363 | |||
46774771ec | |||
6263817bb4 | |||
60456fe0e9 | |||
a0f47d3f1b | |||
6efcb8ccfa | |||
0d128b75e2 | |||
0067d474c8 | |||
cf393b217b | |||
e265b929a1 | |||
4cd01428ed | |||
3be05fbf9b | |||
5d90ba8aa0 | |||
48cab708ce | |||
5d9753d6a7 | |||
425e48bec6 | |||
a42be4a833 | |||
30e030bb8e | |||
2a3c3d8d6a | |||
7b026cec8d | |||
d8b528a4e0 | |||
0f45907144 | |||
c4c9931ae2 | |||
68345e636e | |||
0861c5618c | |||
817418f7c9 | |||
4eb2cd85b2 | |||
086eac5975 | |||
addd6bffbd | |||
1e65313fa7 | |||
c4c6e41c46 | |||
920ca405a2 | |||
6d3a3b3f39 | |||
50d46fe7f6 | |||
91e282d7e5 | |||
a0f10f868e | |||
6a423f0650 | |||
5cc84403e1 | |||
ab61a65b4a | |||
01ec26842d | |||
bbf5817805 | |||
50981cb102 | |||
611ec8103c | |||
12c672667c | |||
db3c98fe72 | |||
f401574f5a | |||
3251fb36c8 | |||
94a410f50f | |||
a14c01c1de | |||
ca3b948628 | |||
a8230ad574 | |||
8e1b5b4803 | |||
8552838bda | |||
46417fe427 | |||
dac04f2929 | |||
63da463e02 | |||
817e144ff6 | |||
9d2d78ae5b | |||
c44db54d9f | |||
376bbeb724 | |||
0e2bdb7863 | |||
235bc77457 | |||
593172f891 | |||
e20c66b156 | |||
5f4825465e | |||
bc6a12a4f7 | |||
90db3acefd | |||
2f2f59279d | |||
4992f87cb1 | |||
7608cb0da3 | |||
9dd9e741f3 | |||
171db639ff | |||
3ede42252c | |||
a94ca175e2 | |||
3749cee28f | |||
ca500da4d8 | |||
820ed6a468 | |||
7cbe18d325 | |||
8937e22ce4 | |||
82a3a98a5a | |||
d97eab0328 | |||
a61e2799db | |||
1009e15aa6 | |||
01c6e46a71 | |||
ed5e013874 | |||
f8e4153dbf | |||
f7a92cf6ac | |||
e748d91d4a | |||
2c4ddca38e | |||
6ca32710be | |||
f05e251991 | |||
a3f3f9d562 | |||
410fcb73c5 | |||
b6d6de6b9f | |||
09cebf20f3 | |||
a8c732d67b | |||
843c9c7e57 | |||
c88b79fa17 | |||
3f9820ac79 | |||
c288e6b8fa | |||
8945ef8880 | |||
99a717f849 | |||
4622b18c99 | |||
4f5270cb7d | |||
719d427956 | |||
d7a21771a5 | |||
be854b3e90 | |||
47f079891f | |||
696dc59ea5 | |||
5f6666a438 | |||
f284a656d7 | |||
1c3d566f8d | |||
373463e995 | |||
7be9b49143 | |||
059a79debb | |||
1a70ebe7ea | |||
beda99bbe0 | |||
bb1e7816e1 | |||
b0dc20e00c | |||
3d66eaea83 | |||
5313a5d5d2 | |||
5b189a909b | |||
75a687138d | |||
ba91b483a0 | |||
3a8b5e1b5e | |||
94d1b68598 | |||
8eda4df71f | |||
8ad9337863 | |||
cd13e187cf | |||
bcc21e55bd | |||
5fbecfd7b7 | |||
3480b45098 | |||
5076ab3049 | |||
44366ac058 | |||
4f2a794fba | |||
fe6aa4358f | |||
f99b62a069 | |||
ac1bed38f9 | |||
217b03a292 | |||
28bceffc6f | |||
09266a155c | |||
3d7591feca | |||
e14909fff4 | |||
fe579c4865 | |||
37118088d4 | |||
5c9e9bd2c4 | |||
db35ba53b1 | |||
758d223776 | |||
21a9bf2463 | |||
a54d9912d0 | |||
7e74949d38 | |||
a8c5780963 | |||
f4ac754d02 | |||
0347d3970a | |||
acc2312384 | |||
7d34ff214c | |||
e2179a6669 | |||
5c37347cec | |||
ef3a6c80a7 | |||
2a2c6cee5f | |||
7dff3cc6cb | |||
8c1171a722 | |||
2c850d0e33 | |||
f1b85ff39d | |||
b7fa25777d | |||
2d86f69caa | |||
e22896a956 | |||
be5802e473 | |||
434c90d378 | |||
eb6ba96b57 | |||
5325e590ec | |||
6ad6dae191 | |||
3f34fa1f58 | |||
d12ea86b55 | |||
a8e45beb51 | |||
ba2a528886 | |||
d60367768b | |||
db6528d3fa | |||
f5873d70c6 | |||
10e349f76e | |||
b1ccebf329 | |||
3407eb84c5 | |||
6017229d1b | |||
4f00af3173 | |||
9da232dcd8 | |||
acd43005df | |||
c31cf2a03a | |||
51c964de3a | |||
dad24e785b | |||
a908283e86 | |||
d8725c7b7f | |||
262f8449b4 | |||
bdf035d60a | |||
0270878748 | |||
6ada3c90ff | |||
4e628fe6de | |||
a8eebd824a | |||
afa0a0a0e2 | |||
92b039fac7 | |||
acc65529a0 | |||
3061f198e9 | |||
6fc1f4fc21 | |||
a0f49b16c5 | |||
c6c4c1c393 | |||
811931ccc0 | |||
08d5633d81 | |||
c76d5dd30c | |||
340357d158 | |||
11ed47397d | |||
6ce54eb845 | |||
d0236aaecf | |||
00059848b4 | |||
e45f6d0c92 | |||
18ccde082d | |||
21bc0f1952 | |||
a37be747e9 | |||
bc3bb82651 | |||
ba00d9e5d2 | |||
bf9edda04c | |||
9c9357639a | |||
3733871d2f | |||
54471a014f | |||
8749be518f | |||
6d880c938a | |||
34aa4eb291 | |||
280b0f42db | |||
65387d0089 | |||
d41c103a72 | |||
0b93b9e059 | |||
ea3f933e95 | |||
b006fe3a22 | |||
37ff3b4920 | |||
1e93d785e5 | |||
999bd4efee | |||
3222247969 | |||
dd6c9ce2fe | |||
7446b28ff1 | |||
38c6702b8f | |||
afcf4b2988 | |||
ebb96a6ff4 | |||
8b0affe9bd | |||
1a25cea0d6 | |||
2ecbcdf4bd | |||
642b392d44 | |||
8dce7b3e9e | |||
33e90d6449 | |||
50b17d5d34 | |||
7818885406 | |||
26af7ccc77 | |||
5d1f79012e | |||
cac80daa71 | |||
fc184f1cfa | |||
725fcbba0e | |||
bdeb209d43 | |||
a078f1ab1b | |||
86c3d8c064 | |||
156191af44 | |||
57bba9e5ab | |||
dd1923fe88 | |||
df773ee15c | |||
f5451a6881 | |||
fcec1581b7 | |||
11cc789e36 | |||
16f9fb2f40 | |||
6bfaa85e84 | |||
8f43fb9530 | |||
04d2a3399b | |||
054bf8ec5d | |||
8417f5a63c | |||
26b46cace0 | |||
0849111247 | |||
f9c25b350e |
2
.github/ISSUE_TEMPLATE.md
vendored
2
.github/ISSUE_TEMPLATE.md
vendored
@ -3,7 +3,7 @@
|
||||
I acknowledge that:
|
||||
|
||||
- I have updated:
|
||||
- To the latest version of the app (stable is v0.14.1)
|
||||
- To the latest version of the app (stable is v0.14.5)
|
||||
- All extensions
|
||||
- I have tried the troubleshooting guide: https://tachiyomi.org/help/guides/troubleshooting-problems/
|
||||
- If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions
|
||||
|
4
.github/ISSUE_TEMPLATE/report_issue.yml
vendored
4
.github/ISSUE_TEMPLATE/report_issue.yml
vendored
@ -53,7 +53,7 @@ body:
|
||||
label: Tachiyomi version
|
||||
description: You can find your Tachiyomi version in **More → About**.
|
||||
placeholder: |
|
||||
Example: "0.14.1"
|
||||
Example: "0.14.5"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@ -98,7 +98,7 @@ body:
|
||||
required: true
|
||||
- label: I have tried the [troubleshooting guide](https://tachiyomi.org/help/guides/troubleshooting/).
|
||||
required: true
|
||||
- label: I have updated the app to version **[0.14.1](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**.
|
||||
- label: I have updated the app to version **[0.14.5](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**.
|
||||
required: true
|
||||
- label: I have updated all installed extensions.
|
||||
required: true
|
||||
|
2
.github/ISSUE_TEMPLATE/request_feature.yml
vendored
2
.github/ISSUE_TEMPLATE/request_feature.yml
vendored
@ -33,7 +33,7 @@ body:
|
||||
required: true
|
||||
- label: If this is an issue with an extension, I should be opening an issue in the [extensions repository](https://github.com/tachiyomiorg/tachiyomi-extensions/issues/new/choose).
|
||||
required: true
|
||||
- label: I have updated the app to version **[0.14.1](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**.
|
||||
- label: I have updated the app to version **[0.14.5](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**.
|
||||
required: true
|
||||
- label: I will fill out all of the requested information in this form.
|
||||
required: true
|
||||
|
6
.github/renovate.json
vendored
6
.github/renovate.json
vendored
@ -5,10 +5,8 @@
|
||||
"schedule": ["every sunday"],
|
||||
"ignoreDeps": [
|
||||
"androidx.core:core-splashscreen",
|
||||
"androidx.work:work-runtime-ktx",
|
||||
"info.android15.nucleus:nucleus-support-v7",
|
||||
"info.android15.nucleus:nucleus",
|
||||
"com.android.tools:r8",
|
||||
"com.google.guava:guava"
|
||||
"com.google.guava:guava",
|
||||
"com.github.commandiron:WheelPickerCompose"
|
||||
]
|
||||
}
|
||||
|
4
.github/workflows/build_pull_request.yml
vendored
4
.github/workflows/build_pull_request.yml
vendored
@ -25,7 +25,7 @@ jobs:
|
||||
uses: gradle/wrapper-validation-action@v1
|
||||
|
||||
- name: Dependency Review
|
||||
uses: actions/dependency-review-action@v2
|
||||
uses: actions/dependency-review-action@v3
|
||||
|
||||
- name: Set up JDK 11
|
||||
uses: actions/setup-java@v3
|
||||
@ -36,4 +36,4 @@ jobs:
|
||||
- name: Build app and run unit tests
|
||||
uses: gradle/gradle-command-action@v2
|
||||
with:
|
||||
arguments: assembleStandardRelease testStandardReleaseUnitTest
|
||||
arguments: lintKotlin assembleStandardRelease testStandardReleaseUnitTest
|
2
.github/workflows/build_push.yml
vendored
2
.github/workflows/build_push.yml
vendored
@ -31,7 +31,7 @@ jobs:
|
||||
- name: Build app and run unit tests
|
||||
uses: gradle/gradle-command-action@v2
|
||||
with:
|
||||
arguments: assembleStandardRelease testStandardReleaseUnitTest
|
||||
arguments: lintKotlin assembleStandardRelease testStandardReleaseUnitTest
|
||||
|
||||
# Sign APK and create release for tags
|
||||
|
||||
|
2
.github/workflows/lock.yml
vendored
2
.github/workflows/lock.yml
vendored
@ -12,7 +12,7 @@ jobs:
|
||||
lock:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/lock-threads@v3
|
||||
- uses: dessant/lock-threads@v4
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
issue-inactive-days: '2'
|
||||
|
5
.gitignore
vendored
5
.gitignore
vendored
@ -10,4 +10,7 @@
|
||||
*/build
|
||||
/build
|
||||
*.apk
|
||||
app/**/output.json
|
||||
app/**/output.json
|
||||
|
||||
# Unnecessary file
|
||||
*.swp
|
@ -1,5 +1,5 @@
|
||||
import org.gradle.api.tasks.testing.logging.TestLogEvent
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
import org.jmailen.gradle.kotlinter.tasks.LintTask
|
||||
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
@ -7,7 +7,6 @@ plugins {
|
||||
kotlin("android")
|
||||
kotlin("plugin.serialization")
|
||||
id("com.github.zellius.shortcut-helper")
|
||||
id("com.squareup.sqldelight")
|
||||
}
|
||||
|
||||
if (gradle.startParameter.taskRequests.toString().contains("Standard")) {
|
||||
@ -20,15 +19,11 @@ val SUPPORTED_ABIS = setOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
|
||||
|
||||
android {
|
||||
namespace = "eu.kanade.tachiyomi"
|
||||
compileSdk = AndroidConfig.compileSdk
|
||||
ndkVersion = AndroidConfig.ndk
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "eu.kanade.tachiyomi"
|
||||
minSdk = AndroidConfig.minSdk
|
||||
targetSdk = AndroidConfig.targetSdk
|
||||
versionCode = 90
|
||||
versionName = "0.14.1"
|
||||
versionCode = 98
|
||||
versionName = "0.14.5"
|
||||
|
||||
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
||||
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
|
||||
@ -59,6 +54,7 @@ android {
|
||||
named("debug") {
|
||||
versionNameSuffix = "-${getCommitCount()}"
|
||||
applicationIdSuffix = ".debug"
|
||||
isPseudoLocalesEnabled = true
|
||||
}
|
||||
named("release") {
|
||||
isShrinkResources = true
|
||||
@ -99,7 +95,8 @@ android {
|
||||
dimension = "default"
|
||||
}
|
||||
create("dev") {
|
||||
resourceConfigurations.addAll(listOf("en", "xxhdpi"))
|
||||
// Include pseudolocales: https://developer.android.com/guide/topics/resources/pseudolocales
|
||||
resourceConfigurations.addAll(listOf("en", "en_XA", "ar_XB", "xxhdpi"))
|
||||
dimension = "default"
|
||||
}
|
||||
}
|
||||
@ -113,7 +110,6 @@ android {
|
||||
"META-INF/README.md",
|
||||
"META-INF/NOTICE",
|
||||
"META-INF/*.kotlin_module",
|
||||
"META-INF/*.version",
|
||||
))
|
||||
}
|
||||
|
||||
@ -139,57 +135,42 @@ android {
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = compose.versions.compiler.get()
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_1_8.toString()
|
||||
}
|
||||
|
||||
sqldelight {
|
||||
database("Database") {
|
||||
packageName = "eu.kanade.tachiyomi"
|
||||
dialect = "sqlite:3.24"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":i18n"))
|
||||
implementation(project(":core"))
|
||||
implementation(project(":source-api"))
|
||||
implementation(project(":data"))
|
||||
implementation(project(":domain"))
|
||||
implementation(project(":presentation-core"))
|
||||
implementation(project(":presentation-widget"))
|
||||
|
||||
// Compose
|
||||
implementation(platform(compose.bom))
|
||||
implementation(compose.activity)
|
||||
implementation(compose.foundation)
|
||||
implementation(compose.material3.core)
|
||||
implementation(compose.material3.adapter)
|
||||
implementation(compose.material.core)
|
||||
implementation(compose.material.icons)
|
||||
implementation(compose.animation)
|
||||
implementation(compose.animation.graphics)
|
||||
implementation(compose.ui.tooling)
|
||||
implementation(compose.ui.util)
|
||||
implementation(compose.accompanist.webview)
|
||||
implementation(compose.accompanist.swiperefresh)
|
||||
implementation(compose.accompanist.flowlayout)
|
||||
implementation(compose.accompanist.pager.core)
|
||||
implementation(compose.accompanist.pager.indicators)
|
||||
implementation(compose.accompanist.permissions)
|
||||
implementation(compose.accompanist.themeadapter)
|
||||
implementation(compose.accompanist.systemuicontroller)
|
||||
|
||||
implementation(androidx.paging.runtime)
|
||||
implementation(androidx.paging.compose)
|
||||
|
||||
implementation(libs.bundles.sqlite)
|
||||
implementation(androidx.sqlite)
|
||||
implementation(libs.sqldelight.android.driver)
|
||||
implementation(libs.sqldelight.coroutines)
|
||||
implementation(libs.sqldelight.android.paging)
|
||||
|
||||
implementation(kotlinx.reflect)
|
||||
|
||||
implementation(platform(kotlinx.coroutines.bom))
|
||||
implementation(kotlinx.bundles.coroutines)
|
||||
|
||||
// AndroidX libraries
|
||||
@ -202,7 +183,6 @@ dependencies {
|
||||
implementation(androidx.splashscreen)
|
||||
implementation(androidx.recyclerview)
|
||||
implementation(androidx.viewpager)
|
||||
implementation(androidx.glance)
|
||||
implementation(androidx.profileinstaller)
|
||||
|
||||
implementation(androidx.bundles.lifecycle)
|
||||
@ -235,15 +215,11 @@ dependencies {
|
||||
// Preferences
|
||||
implementation(libs.preferencektx)
|
||||
|
||||
// Model View Presenter
|
||||
implementation(libs.bundles.nucleus)
|
||||
|
||||
// Dependency injection
|
||||
implementation(libs.injekt.core)
|
||||
|
||||
// Image loading
|
||||
implementation(libs.bundles.coil)
|
||||
|
||||
implementation(libs.subsamplingscaleimageview) {
|
||||
exclude(module = "image-decoder")
|
||||
}
|
||||
@ -261,17 +237,12 @@ dependencies {
|
||||
exclude(group = "androidx.viewpager", module = "viewpager")
|
||||
}
|
||||
implementation(libs.insetter)
|
||||
implementation(libs.markwon)
|
||||
implementation(libs.bundles.richtext)
|
||||
implementation(libs.aboutLibraries.compose)
|
||||
implementation(libs.cascade)
|
||||
implementation(libs.numberpicker)
|
||||
implementation(libs.bundles.voyager)
|
||||
|
||||
// Conductor
|
||||
implementation(libs.bundles.conductor)
|
||||
|
||||
// FlowBinding
|
||||
implementation(libs.bundles.flowbinding)
|
||||
implementation(libs.wheelpicker)
|
||||
implementation(libs.materialmotion.core)
|
||||
|
||||
// Logging
|
||||
implementation(libs.logcat)
|
||||
@ -298,17 +269,16 @@ androidComponents {
|
||||
variantBuilder.enable = variantBuilder.productFlavors.containsAll(listOf("default" to "dev"))
|
||||
}
|
||||
}
|
||||
onVariants(selector().withFlavor("default" to "standard")) {
|
||||
// Only excluding in standard flavor because this breaks
|
||||
// Layout Inspector's Compose tree
|
||||
it.packaging.resources.excludes.add("META-INF/*.version")
|
||||
}
|
||||
}
|
||||
|
||||
tasks {
|
||||
withType<Test> {
|
||||
useJUnitPlatform()
|
||||
testLogging {
|
||||
events(TestLogEvent.PASSED, TestLogEvent.SKIPPED, TestLogEvent.FAILED)
|
||||
}
|
||||
}
|
||||
|
||||
withType<org.jmailen.gradle.kotlinter.tasks.LintTask>().configureEach {
|
||||
withType<LintTask>().configureEach {
|
||||
exclude { it.file.path.contains("generated[\\\\/]".toRegex()) }
|
||||
}
|
||||
|
||||
@ -316,10 +286,11 @@ tasks {
|
||||
withType<KotlinCompile> {
|
||||
kotlinOptions.freeCompilerArgs += listOf(
|
||||
"-opt-in=coil.annotation.ExperimentalCoilApi",
|
||||
"-opt-in=com.google.accompanist.pager.ExperimentalPagerApi",
|
||||
"-opt-in=com.google.accompanist.permissions.ExperimentalPermissionsApi",
|
||||
"-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi",
|
||||
"-opt-in=androidx.compose.material.ExperimentalMaterialApi",
|
||||
"-opt-in=androidx.compose.material3.ExperimentalMaterial3Api",
|
||||
"-opt-in=androidx.compose.material.ExperimentalMaterialApi",
|
||||
"-opt-in=androidx.compose.ui.ExperimentalComposeUiApi",
|
||||
"-opt-in=androidx.compose.foundation.ExperimentalFoundationApi",
|
||||
"-opt-in=androidx.compose.animation.ExperimentalAnimationApi",
|
||||
@ -343,11 +314,6 @@ tasks {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
preBuild {
|
||||
val ktlintTask = if (System.getenv("GITHUB_BASE_REF") == null) formatKotlin else lintKotlin
|
||||
dependsOn(ktlintTask)
|
||||
}
|
||||
}
|
||||
|
||||
buildscript {
|
||||
|
@ -2,4 +2,5 @@
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@android:color/transparent"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
<monochrome android:drawable="@drawable/ic_tachi_monochrome_launcher" />
|
||||
</adaptive-icon>
|
@ -23,6 +23,7 @@
|
||||
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
|
||||
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.READ_APP_SPECIFIC_LOCALES" />
|
||||
|
||||
<!-- Remove permission from Firebase dependency -->
|
||||
<uses-permission android:name="com.google.android.gms.permission.AD_ID"
|
||||
@ -187,7 +188,7 @@
|
||||
android:exported="false" />
|
||||
|
||||
<receiver
|
||||
android:name=".glance.UpdatesGridGlanceReceiver"
|
||||
android:name="tachiyomi.presentation.widget.UpdatesGridGlanceReceiver"
|
||||
android:enabled="@bool/glance_appwidget_available"
|
||||
android:exported="false"
|
||||
android:label="@string/label_recent_updates">
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -2,10 +2,10 @@ package eu.kanade.core.prefs
|
||||
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import eu.kanade.tachiyomi.core.preference.Preference
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import tachiyomi.core.preference.Preference
|
||||
|
||||
class PreferenceMutableState<T>(
|
||||
private val preference: Preference<T>,
|
||||
@ -34,3 +34,5 @@ class PreferenceMutableState<T>(
|
||||
return { preference.set(it) }
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> Preference<T>.asState(scope: CoroutineScope) = PreferenceMutableState(this, scope)
|
||||
|
148
app/src/main/java/eu/kanade/core/util/CollectionUtils.kt
Normal file
148
app/src/main/java/eu/kanade/core/util/CollectionUtils.kt
Normal file
@ -0,0 +1,148 @@
|
||||
package eu.kanade.core.util
|
||||
|
||||
import androidx.compose.ui.util.fastForEach
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import kotlin.contracts.ExperimentalContracts
|
||||
import kotlin.contracts.contract
|
||||
|
||||
fun <T : R, R : Any> List<T>.insertSeparators(
|
||||
generator: (T?, T?) -> R?,
|
||||
): List<R> {
|
||||
if (isEmpty()) return emptyList()
|
||||
val newList = mutableListOf<R>()
|
||||
for (i in -1..lastIndex) {
|
||||
val before = getOrNull(i)
|
||||
before?.let { newList.add(it) }
|
||||
val after = getOrNull(i + 1)
|
||||
val separator = generator.invoke(before, after)
|
||||
separator?.let { newList.add(it) }
|
||||
}
|
||||
return newList
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new map containing only the key entries of [transform] that are not null.
|
||||
*/
|
||||
inline fun <K, V, R> Map<out K, V>.mapNotNullKeys(transform: (Map.Entry<K?, V>) -> R?): ConcurrentHashMap<R, V> {
|
||||
val mutableMap = ConcurrentHashMap<R, V>()
|
||||
forEach { element -> transform(element)?.let { mutableMap[it] = element.value } }
|
||||
return mutableMap
|
||||
}
|
||||
|
||||
fun <E> HashSet<E>.addOrRemove(value: E, shouldAdd: Boolean) {
|
||||
if (shouldAdd) {
|
||||
add(value)
|
||||
} else {
|
||||
remove(value)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list containing only elements matching the given [predicate].
|
||||
*
|
||||
* **Do not use for collections that come from public APIs**, since they may not support random
|
||||
* access in an efficient way, and this method may actually be a lot slower. Only use for
|
||||
* collections that are created by code we control and are known to support random access.
|
||||
*/
|
||||
@OptIn(ExperimentalContracts::class)
|
||||
inline fun <T> List<T>.fastFilter(predicate: (T) -> Boolean): List<T> {
|
||||
contract { callsInPlace(predicate) }
|
||||
val destination = ArrayList<T>()
|
||||
fastForEach { if (predicate(it)) destination.add(it) }
|
||||
return destination
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list containing all elements not matching the given [predicate].
|
||||
*
|
||||
* **Do not use for collections that come from public APIs**, since they may not support random
|
||||
* access in an efficient way, and this method may actually be a lot slower. Only use for
|
||||
* collections that are created by code we control and are known to support random access.
|
||||
*/
|
||||
@OptIn(ExperimentalContracts::class)
|
||||
inline fun <T> List<T>.fastFilterNot(predicate: (T) -> Boolean): List<T> {
|
||||
contract { callsInPlace(predicate) }
|
||||
val destination = ArrayList<T>()
|
||||
fastForEach { if (!predicate(it)) destination.add(it) }
|
||||
return destination
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list containing only the non-null results of applying the
|
||||
* given [transform] function to each element in the original collection.
|
||||
*
|
||||
* **Do not use for collections that come from public APIs**, since they may not support random
|
||||
* access in an efficient way, and this method may actually be a lot slower. Only use for
|
||||
* collections that are created by code we control and are known to support random access.
|
||||
*/
|
||||
@OptIn(ExperimentalContracts::class)
|
||||
inline fun <T, R> List<T>.fastMapNotNull(transform: (T) -> R?): List<R> {
|
||||
contract { callsInPlace(transform) }
|
||||
val destination = ArrayList<R>()
|
||||
fastForEach { element ->
|
||||
transform(element)?.let { destination.add(it) }
|
||||
}
|
||||
return destination
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits the original collection into pair of lists,
|
||||
* where *first* list contains elements for which [predicate] yielded `true`,
|
||||
* while *second* list contains elements for which [predicate] yielded `false`.
|
||||
*
|
||||
* **Do not use for collections that come from public APIs**, since they may not support random
|
||||
* access in an efficient way, and this method may actually be a lot slower. Only use for
|
||||
* collections that are created by code we control and are known to support random access.
|
||||
*/
|
||||
@OptIn(ExperimentalContracts::class)
|
||||
inline fun <T> List<T>.fastPartition(predicate: (T) -> Boolean): Pair<List<T>, List<T>> {
|
||||
contract { callsInPlace(predicate) }
|
||||
val first = ArrayList<T>()
|
||||
val second = ArrayList<T>()
|
||||
fastForEach {
|
||||
if (predicate(it)) {
|
||||
first.add(it)
|
||||
} else {
|
||||
second.add(it)
|
||||
}
|
||||
}
|
||||
return Pair(first, second)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of entries not matching the given [predicate].
|
||||
*
|
||||
* **Do not use for collections that come from public APIs**, since they may not support random
|
||||
* access in an efficient way, and this method may actually be a lot slower. Only use for
|
||||
* collections that are created by code we control and are known to support random access.
|
||||
*/
|
||||
@OptIn(ExperimentalContracts::class)
|
||||
inline fun <T> List<T>.fastCountNot(predicate: (T) -> Boolean): Int {
|
||||
contract { callsInPlace(predicate) }
|
||||
var count = size
|
||||
fastForEach { if (predicate(it)) --count }
|
||||
return count
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list containing only elements from the given collection
|
||||
* having distinct keys returned by the given [selector] function.
|
||||
*
|
||||
* Among elements of the given collection with equal keys, only the first one will be present in the resulting list.
|
||||
* The elements in the resulting list are in the same order as they were in the source collection.
|
||||
*
|
||||
* **Do not use for collections that come from public APIs**, since they may not support random
|
||||
* access in an efficient way, and this method may actually be a lot slower. Only use for
|
||||
* collections that are created by code we control and are known to support random access.
|
||||
*/
|
||||
@OptIn(ExperimentalContracts::class)
|
||||
inline fun <T, K> List<T>.fastDistinctBy(selector: (T) -> K): List<T> {
|
||||
contract { callsInPlace(selector) }
|
||||
val set = HashSet<K>()
|
||||
val list = ArrayList<T>()
|
||||
fastForEach {
|
||||
val key = selector(it)
|
||||
if (set.add(key)) list.add(it)
|
||||
}
|
||||
return list
|
||||
}
|
16
app/src/main/java/eu/kanade/core/util/DurationUtils.kt
Normal file
16
app/src/main/java/eu/kanade/core/util/DurationUtils.kt
Normal file
@ -0,0 +1,16 @@
|
||||
package eu.kanade.core.util
|
||||
|
||||
import android.content.Context
|
||||
import eu.kanade.tachiyomi.R
|
||||
import kotlin.time.Duration
|
||||
|
||||
fun Duration.toDurationString(context: Context, fallback: String): String {
|
||||
return toComponents { days, hours, minutes, seconds, _ ->
|
||||
buildList(4) {
|
||||
if (days != 0L) add(context.getString(R.string.day_short, days))
|
||||
if (hours != 0) add(context.getString(R.string.hour_short, hours))
|
||||
if (minutes != 0 && (days == 0L || hours == 0)) add(context.getString(R.string.minute_short, minutes))
|
||||
if (seconds != 0 && days == 0L && hours == 0) add(context.getString(R.string.seconds_short, seconds))
|
||||
}.joinToString(" ").ifBlank { fallback }
|
||||
}
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
package eu.kanade.core.util
|
||||
|
||||
fun <T : R, R : Any> List<T>.insertSeparators(
|
||||
generator: (T?, T?) -> R?,
|
||||
): List<R> {
|
||||
if (isEmpty()) return emptyList()
|
||||
val newList = mutableListOf<R>()
|
||||
for (i in -1..lastIndex) {
|
||||
val before = getOrNull(i)
|
||||
before?.let { newList.add(it) }
|
||||
val after = getOrNull(i + 1)
|
||||
val separator = generator.invoke(before, after)
|
||||
separator?.let { newList.add(it) }
|
||||
}
|
||||
return newList
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
package eu.kanade.data.source
|
||||
|
||||
class NoResultsException : Exception()
|
@ -1,9 +1,8 @@
|
||||
package eu.kanade.data.source
|
||||
|
||||
import eu.kanade.domain.source.model.Source
|
||||
import eu.kanade.domain.source.model.SourceData
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import tachiyomi.domain.source.model.Source
|
||||
|
||||
val sourceMapper: (eu.kanade.tachiyomi.source.Source) -> Source = { source ->
|
||||
Source(
|
||||
@ -18,7 +17,3 @@ val sourceMapper: (eu.kanade.tachiyomi.source.Source) -> Source = { source ->
|
||||
val catalogueSourceMapper: (CatalogueSource) -> Source = { source ->
|
||||
sourceMapper(source).copy(supportsLatest = source.supportsLatest)
|
||||
}
|
||||
|
||||
val sourceDataMapper: (Long, String, String) -> SourceData = { id, lang, name ->
|
||||
SourceData(id, lang, name)
|
||||
}
|
||||
|
@ -6,8 +6,8 @@ import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.util.lang.awaitSingle
|
||||
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||
import tachiyomi.core.util.lang.awaitSingle
|
||||
import tachiyomi.core.util.lang.withIOContext
|
||||
|
||||
abstract class SourcePagingSource(
|
||||
protected val source: CatalogueSource,
|
||||
@ -60,3 +60,5 @@ class SourceLatestPagingSource(source: CatalogueSource) : SourcePagingSource(sou
|
||||
return source.fetchLatestUpdates(currentPage).awaitSingle()
|
||||
}
|
||||
}
|
||||
|
||||
class NoResultsException : Exception()
|
||||
|
@ -1,9 +1,6 @@
|
||||
package eu.kanade.data.source
|
||||
|
||||
import eu.kanade.data.DatabaseHandler
|
||||
import eu.kanade.domain.source.model.Source
|
||||
import eu.kanade.domain.source.model.SourcePagingSourceType
|
||||
import eu.kanade.domain.source.model.SourceWithCount
|
||||
import eu.kanade.domain.source.repository.SourceRepository
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.LocalSource
|
||||
@ -11,6 +8,9 @@ import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import tachiyomi.data.DatabaseHandler
|
||||
import tachiyomi.domain.source.model.Source
|
||||
import tachiyomi.domain.source.model.SourceWithCount
|
||||
|
||||
class SourceRepositoryImpl(
|
||||
private val sourceManager: SourceManager,
|
||||
|
@ -1,16 +1,8 @@
|
||||
package eu.kanade.domain
|
||||
|
||||
import eu.kanade.data.category.CategoryRepositoryImpl
|
||||
import eu.kanade.data.chapter.ChapterRepositoryImpl
|
||||
import eu.kanade.data.history.HistoryRepositoryImpl
|
||||
import eu.kanade.data.manga.MangaRepositoryImpl
|
||||
import eu.kanade.data.source.SourceDataRepositoryImpl
|
||||
import eu.kanade.data.source.SourceRepositoryImpl
|
||||
import eu.kanade.data.track.TrackRepositoryImpl
|
||||
import eu.kanade.data.updates.UpdatesRepositoryImpl
|
||||
import eu.kanade.domain.category.interactor.CreateCategoryWithName
|
||||
import eu.kanade.domain.category.interactor.DeleteCategory
|
||||
import eu.kanade.domain.category.interactor.GetCategories
|
||||
import eu.kanade.domain.category.interactor.RenameCategory
|
||||
import eu.kanade.domain.category.interactor.ReorderCategory
|
||||
import eu.kanade.domain.category.interactor.ResetCategoryFlags
|
||||
@ -18,27 +10,18 @@ import eu.kanade.domain.category.interactor.SetDisplayModeForCategory
|
||||
import eu.kanade.domain.category.interactor.SetMangaCategories
|
||||
import eu.kanade.domain.category.interactor.SetSortModeForCategory
|
||||
import eu.kanade.domain.category.interactor.UpdateCategory
|
||||
import eu.kanade.domain.category.repository.CategoryRepository
|
||||
import eu.kanade.domain.chapter.interactor.GetChapter
|
||||
import eu.kanade.domain.chapter.interactor.GetChapterByMangaId
|
||||
import eu.kanade.domain.chapter.interactor.SetMangaDefaultChapterFlags
|
||||
import eu.kanade.domain.chapter.interactor.SetReadStatus
|
||||
import eu.kanade.domain.chapter.interactor.ShouldUpdateDbChapter
|
||||
import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource
|
||||
import eu.kanade.domain.chapter.interactor.SyncChaptersWithTrackServiceTwoWay
|
||||
import eu.kanade.domain.chapter.interactor.UpdateChapter
|
||||
import eu.kanade.domain.chapter.repository.ChapterRepository
|
||||
import eu.kanade.domain.download.interactor.DeleteDownload
|
||||
import eu.kanade.domain.extension.interactor.GetExtensionLanguages
|
||||
import eu.kanade.domain.extension.interactor.GetExtensionSources
|
||||
import eu.kanade.domain.extension.interactor.GetExtensionsByType
|
||||
import eu.kanade.domain.history.interactor.DeleteAllHistory
|
||||
import eu.kanade.domain.history.interactor.GetHistory
|
||||
import eu.kanade.domain.history.interactor.GetNextChapter
|
||||
import eu.kanade.domain.history.interactor.RemoveHistoryById
|
||||
import eu.kanade.domain.history.interactor.RemoveHistoryByMangaId
|
||||
import eu.kanade.domain.history.interactor.UpsertHistory
|
||||
import eu.kanade.domain.history.repository.HistoryRepository
|
||||
import eu.kanade.domain.history.interactor.GetNextChapters
|
||||
import eu.kanade.domain.manga.interactor.GetDuplicateLibraryManga
|
||||
import eu.kanade.domain.manga.interactor.GetFavorites
|
||||
import eu.kanade.domain.manga.interactor.GetLibraryManga
|
||||
@ -49,7 +32,6 @@ import eu.kanade.domain.manga.interactor.ResetViewerFlags
|
||||
import eu.kanade.domain.manga.interactor.SetMangaChapterFlags
|
||||
import eu.kanade.domain.manga.interactor.SetMangaViewerFlags
|
||||
import eu.kanade.domain.manga.interactor.UpdateManga
|
||||
import eu.kanade.domain.manga.repository.MangaRepository
|
||||
import eu.kanade.domain.source.interactor.GetEnabledSources
|
||||
import eu.kanade.domain.source.interactor.GetLanguagesWithSources
|
||||
import eu.kanade.domain.source.interactor.GetRemoteManga
|
||||
@ -59,15 +41,32 @@ import eu.kanade.domain.source.interactor.SetMigrateSorting
|
||||
import eu.kanade.domain.source.interactor.ToggleLanguage
|
||||
import eu.kanade.domain.source.interactor.ToggleSource
|
||||
import eu.kanade.domain.source.interactor.ToggleSourcePin
|
||||
import eu.kanade.domain.source.repository.SourceDataRepository
|
||||
import eu.kanade.domain.source.repository.SourceRepository
|
||||
import eu.kanade.domain.track.interactor.DeleteTrack
|
||||
import eu.kanade.domain.track.interactor.GetTracks
|
||||
import eu.kanade.domain.track.interactor.GetTracksPerManga
|
||||
import eu.kanade.domain.track.interactor.InsertTrack
|
||||
import eu.kanade.domain.track.repository.TrackRepository
|
||||
import eu.kanade.domain.updates.interactor.GetUpdates
|
||||
import eu.kanade.domain.updates.repository.UpdatesRepository
|
||||
import tachiyomi.data.category.CategoryRepositoryImpl
|
||||
import tachiyomi.data.chapter.ChapterRepositoryImpl
|
||||
import tachiyomi.data.history.HistoryRepositoryImpl
|
||||
import tachiyomi.data.manga.MangaRepositoryImpl
|
||||
import tachiyomi.data.source.SourceDataRepositoryImpl
|
||||
import tachiyomi.data.track.TrackRepositoryImpl
|
||||
import tachiyomi.data.updates.UpdatesRepositoryImpl
|
||||
import tachiyomi.domain.category.interactor.GetCategories
|
||||
import tachiyomi.domain.category.repository.CategoryRepository
|
||||
import tachiyomi.domain.chapter.interactor.ShouldUpdateDbChapter
|
||||
import tachiyomi.domain.chapter.repository.ChapterRepository
|
||||
import tachiyomi.domain.history.interactor.GetHistory
|
||||
import tachiyomi.domain.history.interactor.GetTotalReadDuration
|
||||
import tachiyomi.domain.history.interactor.RemoveHistory
|
||||
import tachiyomi.domain.history.interactor.UpsertHistory
|
||||
import tachiyomi.domain.history.repository.HistoryRepository
|
||||
import tachiyomi.domain.manga.repository.MangaRepository
|
||||
import tachiyomi.domain.source.repository.SourceDataRepository
|
||||
import tachiyomi.domain.track.repository.TrackRepository
|
||||
import tachiyomi.domain.updates.interactor.GetUpdates
|
||||
import tachiyomi.domain.updates.repository.UpdatesRepository
|
||||
import uy.kohesive.injekt.api.InjektModule
|
||||
import uy.kohesive.injekt.api.InjektRegistrar
|
||||
import uy.kohesive.injekt.api.addFactory
|
||||
@ -94,7 +93,7 @@ class DomainModule : InjektModule {
|
||||
addFactory { GetLibraryManga(get()) }
|
||||
addFactory { GetMangaWithChapters(get(), get()) }
|
||||
addFactory { GetManga(get()) }
|
||||
addFactory { GetNextChapter(get(), get(), get(), get()) }
|
||||
addFactory { GetNextChapters(get(), get(), get()) }
|
||||
addFactory { ResetViewerFlags(get()) }
|
||||
addFactory { SetMangaChapterFlags(get()) }
|
||||
addFactory { SetMangaDefaultChapterFlags(get(), get(), get()) }
|
||||
@ -119,11 +118,10 @@ class DomainModule : InjektModule {
|
||||
addFactory { SyncChaptersWithTrackServiceTwoWay(get(), get()) }
|
||||
|
||||
addSingletonFactory<HistoryRepository> { HistoryRepositoryImpl(get()) }
|
||||
addFactory { DeleteAllHistory(get()) }
|
||||
addFactory { GetHistory(get()) }
|
||||
addFactory { UpsertHistory(get()) }
|
||||
addFactory { RemoveHistoryById(get()) }
|
||||
addFactory { RemoveHistoryByMangaId(get()) }
|
||||
addFactory { RemoveHistory(get()) }
|
||||
addFactory { GetTotalReadDuration(get()) }
|
||||
|
||||
addFactory { DeleteDownload(get(), get()) }
|
||||
|
||||
@ -132,7 +130,7 @@ class DomainModule : InjektModule {
|
||||
addFactory { GetExtensionLanguages(get(), get()) }
|
||||
|
||||
addSingletonFactory<UpdatesRepository> { UpdatesRepositoryImpl(get()) }
|
||||
addFactory { GetUpdates(get(), get()) }
|
||||
addFactory { GetUpdates(get()) }
|
||||
|
||||
addSingletonFactory<SourceRepository> { SourceRepositoryImpl(get(), get()) }
|
||||
addSingletonFactory<SourceDataRepository> { SourceDataRepositoryImpl(get()) }
|
||||
|
@ -1,7 +1,7 @@
|
||||
package eu.kanade.domain.backup.service
|
||||
|
||||
import eu.kanade.tachiyomi.core.preference.PreferenceStore
|
||||
import eu.kanade.tachiyomi.core.provider.FolderProvider
|
||||
import tachiyomi.core.preference.PreferenceStore
|
||||
import tachiyomi.core.provider.FolderProvider
|
||||
|
||||
class BackupPreferences(
|
||||
private val folderProvider: FolderProvider,
|
||||
|
@ -1,12 +1,9 @@
|
||||
package eu.kanade.domain.base
|
||||
|
||||
import android.content.Context
|
||||
import eu.kanade.tachiyomi.core.preference.PreferenceStore
|
||||
import eu.kanade.tachiyomi.core.preference.getEnum
|
||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues
|
||||
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
||||
import eu.kanade.tachiyomi.util.system.isPreviewBuildType
|
||||
import eu.kanade.tachiyomi.util.system.isReleaseBuildType
|
||||
import tachiyomi.core.preference.PreferenceStore
|
||||
|
||||
class BasePreferences(
|
||||
val context: Context,
|
||||
@ -21,10 +18,7 @@ class BasePreferences(
|
||||
|
||||
fun automaticExtUpdates() = preferenceStore.getBoolean("automatic_ext_updates", true)
|
||||
|
||||
fun extensionInstaller() = preferenceStore.getEnum(
|
||||
"extension_installer",
|
||||
if (DeviceUtil.isMiui) PreferenceValues.ExtensionInstaller.LEGACY else PreferenceValues.ExtensionInstaller.PACKAGEINSTALLER,
|
||||
)
|
||||
fun extensionInstaller() = ExtensionInstallerPreference(context, preferenceStore)
|
||||
|
||||
fun acraEnabled() = preferenceStore.getBoolean("acra.enable", isPreviewBuildType || isReleaseBuildType)
|
||||
}
|
||||
|
@ -0,0 +1,68 @@
|
||||
package eu.kanade.domain.base
|
||||
|
||||
import android.content.Context
|
||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues.ExtensionInstaller
|
||||
import eu.kanade.tachiyomi.util.system.hasMiuiPackageInstaller
|
||||
import eu.kanade.tachiyomi.util.system.isShizukuInstalled
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import tachiyomi.core.preference.Preference
|
||||
import tachiyomi.core.preference.PreferenceStore
|
||||
import tachiyomi.core.preference.getEnum
|
||||
|
||||
class ExtensionInstallerPreference(
|
||||
private val context: Context,
|
||||
preferenceStore: PreferenceStore,
|
||||
) : Preference<ExtensionInstaller> {
|
||||
|
||||
private val basePref = preferenceStore.getEnum(key(), defaultValue())
|
||||
|
||||
override fun key() = "extension_installer"
|
||||
|
||||
val entries get() = ExtensionInstaller.values().run {
|
||||
if (context.hasMiuiPackageInstaller) {
|
||||
filter { it != ExtensionInstaller.PACKAGEINSTALLER }
|
||||
} else {
|
||||
toList()
|
||||
}
|
||||
}
|
||||
|
||||
override fun defaultValue() = if (context.hasMiuiPackageInstaller) {
|
||||
ExtensionInstaller.LEGACY
|
||||
} else {
|
||||
ExtensionInstaller.PACKAGEINSTALLER
|
||||
}
|
||||
|
||||
private fun check(value: ExtensionInstaller): ExtensionInstaller {
|
||||
when (value) {
|
||||
ExtensionInstaller.PACKAGEINSTALLER -> {
|
||||
if (context.hasMiuiPackageInstaller) return ExtensionInstaller.LEGACY
|
||||
}
|
||||
ExtensionInstaller.SHIZUKU -> {
|
||||
if (!context.isShizukuInstalled) return defaultValue()
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
override fun get(): ExtensionInstaller {
|
||||
val value = basePref.get()
|
||||
val checkedValue = check(value)
|
||||
if (value != checkedValue) {
|
||||
basePref.set(checkedValue)
|
||||
}
|
||||
return checkedValue
|
||||
}
|
||||
|
||||
override fun set(value: ExtensionInstaller) {
|
||||
basePref.set(check(value))
|
||||
}
|
||||
|
||||
override fun isSet() = basePref.isSet()
|
||||
|
||||
override fun delete() = basePref.delete()
|
||||
|
||||
override fun changes() = basePref.changes()
|
||||
|
||||
override fun stateIn(scope: CoroutineScope) = basePref.stateIn(scope)
|
||||
}
|
@ -1,12 +1,11 @@
|
||||
package eu.kanade.domain.category.interactor
|
||||
|
||||
import eu.kanade.domain.category.model.Category
|
||||
import eu.kanade.domain.category.model.anyWithName
|
||||
import eu.kanade.domain.category.repository.CategoryRepository
|
||||
import eu.kanade.domain.library.service.LibraryPreferences
|
||||
import eu.kanade.tachiyomi.util.lang.withNonCancellableContext
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import logcat.LogPriority
|
||||
import tachiyomi.core.util.lang.withNonCancellableContext
|
||||
import tachiyomi.core.util.system.logcat
|
||||
import tachiyomi.domain.category.model.Category
|
||||
import tachiyomi.domain.category.repository.CategoryRepository
|
||||
|
||||
class CreateCategoryWithName(
|
||||
private val categoryRepository: CategoryRepository,
|
||||
@ -23,10 +22,6 @@ class CreateCategoryWithName(
|
||||
|
||||
suspend fun await(name: String): Result = withNonCancellableContext {
|
||||
val categories = categoryRepository.getAll()
|
||||
if (categories.anyWithName(name)) {
|
||||
return@withNonCancellableContext Result.NameAlreadyExistsError
|
||||
}
|
||||
|
||||
val nextOrder = categories.maxOfOrNull { it.order }?.plus(1) ?: 0
|
||||
val newCategory = Category(
|
||||
id = 0,
|
||||
@ -46,7 +41,6 @@ class CreateCategoryWithName(
|
||||
|
||||
sealed class Result {
|
||||
object Success : Result()
|
||||
object NameAlreadyExistsError : Result()
|
||||
data class InternalError(val error: Throwable) : Result()
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,10 @@
|
||||
package eu.kanade.domain.category.interactor
|
||||
|
||||
import eu.kanade.domain.category.model.CategoryUpdate
|
||||
import eu.kanade.domain.category.repository.CategoryRepository
|
||||
import eu.kanade.tachiyomi.util.lang.withNonCancellableContext
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import logcat.LogPriority
|
||||
import tachiyomi.core.util.lang.withNonCancellableContext
|
||||
import tachiyomi.core.util.system.logcat
|
||||
import tachiyomi.domain.category.model.CategoryUpdate
|
||||
import tachiyomi.domain.category.repository.CategoryRepository
|
||||
|
||||
class DeleteCategory(
|
||||
private val categoryRepository: CategoryRepository,
|
||||
|
@ -1,23 +1,17 @@
|
||||
package eu.kanade.domain.category.interactor
|
||||
|
||||
import eu.kanade.domain.category.model.Category
|
||||
import eu.kanade.domain.category.model.CategoryUpdate
|
||||
import eu.kanade.domain.category.model.anyWithName
|
||||
import eu.kanade.domain.category.repository.CategoryRepository
|
||||
import eu.kanade.tachiyomi.util.lang.withNonCancellableContext
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import logcat.LogPriority
|
||||
import tachiyomi.core.util.lang.withNonCancellableContext
|
||||
import tachiyomi.core.util.system.logcat
|
||||
import tachiyomi.domain.category.model.Category
|
||||
import tachiyomi.domain.category.model.CategoryUpdate
|
||||
import tachiyomi.domain.category.repository.CategoryRepository
|
||||
|
||||
class RenameCategory(
|
||||
private val categoryRepository: CategoryRepository,
|
||||
) {
|
||||
|
||||
suspend fun await(categoryId: Long, name: String) = withNonCancellableContext {
|
||||
val categories = categoryRepository.getAll()
|
||||
if (categories.anyWithName(name)) {
|
||||
return@withNonCancellableContext Result.NameAlreadyExistsError
|
||||
}
|
||||
|
||||
val update = CategoryUpdate(
|
||||
id = categoryId,
|
||||
name = name,
|
||||
@ -36,7 +30,6 @@ class RenameCategory(
|
||||
|
||||
sealed class Result {
|
||||
object Success : Result()
|
||||
object NameAlreadyExistsError : Result()
|
||||
data class InternalError(val error: Throwable) : Result()
|
||||
}
|
||||
}
|
||||
|
@ -1,50 +1,70 @@
|
||||
package eu.kanade.domain.category.interactor
|
||||
|
||||
import eu.kanade.domain.category.model.Category
|
||||
import eu.kanade.domain.category.model.CategoryUpdate
|
||||
import eu.kanade.domain.category.repository.CategoryRepository
|
||||
import eu.kanade.tachiyomi.util.lang.withNonCancellableContext
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import logcat.LogPriority
|
||||
import tachiyomi.core.util.lang.withNonCancellableContext
|
||||
import tachiyomi.core.util.system.logcat
|
||||
import tachiyomi.domain.category.model.Category
|
||||
import tachiyomi.domain.category.model.CategoryUpdate
|
||||
import tachiyomi.domain.category.repository.CategoryRepository
|
||||
import java.util.Collections
|
||||
|
||||
class ReorderCategory(
|
||||
private val categoryRepository: CategoryRepository,
|
||||
) {
|
||||
|
||||
suspend fun await(categoryId: Long, newPosition: Int) = withNonCancellableContext {
|
||||
val categories = categoryRepository.getAll().filterNot(Category::isSystemCategory)
|
||||
private val mutex = Mutex()
|
||||
|
||||
val currentIndex = categories.indexOfFirst { it.id == categoryId }
|
||||
if (currentIndex == newPosition) {
|
||||
return@withNonCancellableContext Result.Unchanged
|
||||
}
|
||||
suspend fun moveUp(category: Category): Result =
|
||||
await(category, MoveTo.UP)
|
||||
|
||||
val reorderedCategories = categories.toMutableList()
|
||||
val reorderedCategory = reorderedCategories.removeAt(currentIndex)
|
||||
reorderedCategories.add(newPosition, reorderedCategory)
|
||||
suspend fun moveDown(category: Category): Result =
|
||||
await(category, MoveTo.DOWN)
|
||||
|
||||
val updates = reorderedCategories.mapIndexed { index, category ->
|
||||
CategoryUpdate(
|
||||
id = category.id,
|
||||
order = index.toLong(),
|
||||
)
|
||||
}
|
||||
private suspend fun await(category: Category, moveTo: MoveTo) = withNonCancellableContext {
|
||||
mutex.withLock {
|
||||
val categories = categoryRepository.getAll()
|
||||
.filterNot(Category::isSystemCategory)
|
||||
.toMutableList()
|
||||
|
||||
try {
|
||||
categoryRepository.updatePartial(updates)
|
||||
Result.Success
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR, e)
|
||||
Result.InternalError(e)
|
||||
val currentIndex = categories.indexOfFirst { it.id == category.id }
|
||||
if (currentIndex == -1) {
|
||||
return@withNonCancellableContext Result.Unchanged
|
||||
}
|
||||
|
||||
val newPosition = when (moveTo) {
|
||||
MoveTo.UP -> currentIndex - 1
|
||||
MoveTo.DOWN -> currentIndex + 1
|
||||
}.toInt()
|
||||
|
||||
try {
|
||||
Collections.swap(categories, currentIndex, newPosition)
|
||||
|
||||
val updates = categories.mapIndexed { index, category ->
|
||||
CategoryUpdate(
|
||||
id = category.id,
|
||||
order = index.toLong(),
|
||||
)
|
||||
}
|
||||
|
||||
categoryRepository.updatePartial(updates)
|
||||
Result.Success
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR, e)
|
||||
Result.InternalError(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun await(category: Category, newPosition: Long): Result =
|
||||
await(category.id, newPosition.toInt())
|
||||
|
||||
sealed class Result {
|
||||
object Success : Result()
|
||||
object Unchanged : Result()
|
||||
data class InternalError(val error: Throwable) : Result()
|
||||
}
|
||||
|
||||
private enum class MoveTo {
|
||||
UP,
|
||||
DOWN,
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
package eu.kanade.domain.category.interactor
|
||||
|
||||
import eu.kanade.domain.category.repository.CategoryRepository
|
||||
import eu.kanade.domain.library.model.plus
|
||||
import eu.kanade.domain.library.service.LibraryPreferences
|
||||
import tachiyomi.domain.category.repository.CategoryRepository
|
||||
import tachiyomi.domain.library.model.plus
|
||||
|
||||
class ResetCategoryFlags(
|
||||
private val preferences: LibraryPreferences,
|
||||
|
@ -1,11 +1,11 @@
|
||||
package eu.kanade.domain.category.interactor
|
||||
|
||||
import eu.kanade.domain.category.model.Category
|
||||
import eu.kanade.domain.category.model.CategoryUpdate
|
||||
import eu.kanade.domain.category.repository.CategoryRepository
|
||||
import eu.kanade.domain.library.model.LibraryDisplayMode
|
||||
import eu.kanade.domain.library.model.plus
|
||||
import eu.kanade.domain.library.service.LibraryPreferences
|
||||
import tachiyomi.domain.category.model.Category
|
||||
import tachiyomi.domain.category.model.CategoryUpdate
|
||||
import tachiyomi.domain.category.repository.CategoryRepository
|
||||
import tachiyomi.domain.library.model.LibraryDisplayMode
|
||||
import tachiyomi.domain.library.model.plus
|
||||
|
||||
class SetDisplayModeForCategory(
|
||||
private val preferences: LibraryPreferences,
|
||||
|
@ -1,8 +1,8 @@
|
||||
package eu.kanade.domain.category.interactor
|
||||
|
||||
import eu.kanade.domain.manga.repository.MangaRepository
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import logcat.LogPriority
|
||||
import tachiyomi.core.util.system.logcat
|
||||
import tachiyomi.domain.manga.repository.MangaRepository
|
||||
|
||||
class SetMangaCategories(
|
||||
private val mangaRepository: MangaRepository,
|
||||
|
@ -1,11 +1,11 @@
|
||||
package eu.kanade.domain.category.interactor
|
||||
|
||||
import eu.kanade.domain.category.model.Category
|
||||
import eu.kanade.domain.category.model.CategoryUpdate
|
||||
import eu.kanade.domain.category.repository.CategoryRepository
|
||||
import eu.kanade.domain.library.model.LibrarySort
|
||||
import eu.kanade.domain.library.model.plus
|
||||
import eu.kanade.domain.library.service.LibraryPreferences
|
||||
import tachiyomi.domain.category.model.Category
|
||||
import tachiyomi.domain.category.model.CategoryUpdate
|
||||
import tachiyomi.domain.category.repository.CategoryRepository
|
||||
import tachiyomi.domain.library.model.LibrarySort
|
||||
import tachiyomi.domain.library.model.plus
|
||||
|
||||
class SetSortModeForCategory(
|
||||
private val preferences: LibraryPreferences,
|
||||
|
@ -1,8 +1,8 @@
|
||||
package eu.kanade.domain.category.interactor
|
||||
|
||||
import eu.kanade.domain.category.model.CategoryUpdate
|
||||
import eu.kanade.domain.category.repository.CategoryRepository
|
||||
import eu.kanade.tachiyomi.util.lang.withNonCancellableContext
|
||||
import tachiyomi.core.util.lang.withNonCancellableContext
|
||||
import tachiyomi.domain.category.model.CategoryUpdate
|
||||
import tachiyomi.domain.category.repository.CategoryRepository
|
||||
|
||||
class UpdateCategory(
|
||||
private val categoryRepository: CategoryRepository,
|
||||
|
@ -1,9 +1,9 @@
|
||||
package eu.kanade.domain.chapter.interactor
|
||||
|
||||
import eu.kanade.domain.chapter.model.Chapter
|
||||
import eu.kanade.domain.chapter.repository.ChapterRepository
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import logcat.LogPriority
|
||||
import tachiyomi.core.util.system.logcat
|
||||
import tachiyomi.domain.chapter.model.Chapter
|
||||
import tachiyomi.domain.chapter.repository.ChapterRepository
|
||||
|
||||
class GetChapter(
|
||||
private val chapterRepository: ChapterRepository,
|
||||
|
@ -1,9 +1,9 @@
|
||||
package eu.kanade.domain.chapter.interactor
|
||||
|
||||
import eu.kanade.domain.chapter.model.Chapter
|
||||
import eu.kanade.domain.chapter.repository.ChapterRepository
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import logcat.LogPriority
|
||||
import tachiyomi.core.util.system.logcat
|
||||
import tachiyomi.domain.chapter.model.Chapter
|
||||
import tachiyomi.domain.chapter.repository.ChapterRepository
|
||||
|
||||
class GetChapterByMangaId(
|
||||
private val chapterRepository: ChapterRepository,
|
||||
|
@ -3,8 +3,8 @@ package eu.kanade.domain.chapter.interactor
|
||||
import eu.kanade.domain.library.service.LibraryPreferences
|
||||
import eu.kanade.domain.manga.interactor.GetFavorites
|
||||
import eu.kanade.domain.manga.interactor.SetMangaChapterFlags
|
||||
import eu.kanade.domain.manga.model.Manga
|
||||
import eu.kanade.tachiyomi.util.lang.withNonCancellableContext
|
||||
import tachiyomi.core.util.lang.withNonCancellableContext
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
|
||||
class SetMangaDefaultChapterFlags(
|
||||
private val libraryPreferences: LibraryPreferences,
|
||||
|
@ -1,15 +1,15 @@
|
||||
package eu.kanade.domain.chapter.interactor
|
||||
|
||||
import eu.kanade.domain.chapter.model.Chapter
|
||||
import eu.kanade.domain.chapter.model.ChapterUpdate
|
||||
import eu.kanade.domain.chapter.repository.ChapterRepository
|
||||
import eu.kanade.domain.download.interactor.DeleteDownload
|
||||
import eu.kanade.domain.download.service.DownloadPreferences
|
||||
import eu.kanade.domain.manga.model.Manga
|
||||
import eu.kanade.domain.manga.repository.MangaRepository
|
||||
import eu.kanade.tachiyomi.util.lang.withNonCancellableContext
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import logcat.LogPriority
|
||||
import tachiyomi.core.util.lang.withNonCancellableContext
|
||||
import tachiyomi.core.util.system.logcat
|
||||
import tachiyomi.domain.chapter.model.Chapter
|
||||
import tachiyomi.domain.chapter.model.ChapterUpdate
|
||||
import tachiyomi.domain.chapter.repository.ChapterRepository
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.domain.manga.repository.MangaRepository
|
||||
|
||||
class SetReadStatus(
|
||||
private val downloadPreferences: DownloadPreferences,
|
||||
|
@ -1,13 +1,9 @@
|
||||
package eu.kanade.domain.chapter.interactor
|
||||
|
||||
import eu.kanade.data.chapter.CleanupChapterName
|
||||
import eu.kanade.data.chapter.NoChaptersException
|
||||
import eu.kanade.domain.chapter.model.Chapter
|
||||
import eu.kanade.domain.chapter.model.toChapterUpdate
|
||||
import eu.kanade.domain.chapter.model.toDbChapter
|
||||
import eu.kanade.domain.chapter.repository.ChapterRepository
|
||||
import eu.kanade.domain.chapter.model.copyFromSChapter
|
||||
import eu.kanade.domain.chapter.model.toSChapter
|
||||
import eu.kanade.domain.manga.interactor.UpdateManga
|
||||
import eu.kanade.domain.manga.model.Manga
|
||||
import eu.kanade.domain.manga.model.toSManga
|
||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||
import eu.kanade.tachiyomi.data.download.DownloadProvider
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
@ -15,6 +11,13 @@ import eu.kanade.tachiyomi.source.isLocal
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.util.chapter.ChapterRecognition
|
||||
import tachiyomi.data.chapter.ChapterSanitizer
|
||||
import tachiyomi.domain.chapter.interactor.ShouldUpdateDbChapter
|
||||
import tachiyomi.domain.chapter.model.Chapter
|
||||
import tachiyomi.domain.chapter.model.NoChaptersException
|
||||
import tachiyomi.domain.chapter.model.toChapterUpdate
|
||||
import tachiyomi.domain.chapter.repository.ChapterRepository
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.lang.Long.max
|
||||
@ -53,7 +56,7 @@ class SyncChaptersWithSource(
|
||||
.mapIndexed { i, sChapter ->
|
||||
Chapter.create()
|
||||
.copyFromSChapter(sChapter)
|
||||
.copy(name = CleanupChapterName.await(sChapter.name, manga.title))
|
||||
.copy(name = with(ChapterSanitizer) { sChapter.name.sanitize(manga.title) })
|
||||
.copy(mangaId = manga.id, sourceOrder = i.toLong())
|
||||
}
|
||||
|
||||
@ -111,7 +114,7 @@ class SyncChaptersWithSource(
|
||||
downloadManager.isChapterDownloaded(dbChapter.name, dbChapter.scanlator, manga.title, manga.source)
|
||||
|
||||
if (shouldRenameChapter) {
|
||||
downloadManager.renameChapter(source, manga, dbChapter.toDbChapter(), chapter.toDbChapter())
|
||||
downloadManager.renameChapter(source, manga, dbChapter, chapter)
|
||||
}
|
||||
var toChangeChapter = dbChapter.copy(
|
||||
name = chapter.name,
|
||||
|
@ -1,13 +1,13 @@
|
||||
package eu.kanade.domain.chapter.interactor
|
||||
|
||||
import eu.kanade.domain.chapter.model.Chapter
|
||||
import eu.kanade.domain.chapter.model.toChapterUpdate
|
||||
import eu.kanade.domain.track.interactor.InsertTrack
|
||||
import eu.kanade.domain.track.model.Track
|
||||
import eu.kanade.domain.track.model.toDbTrack
|
||||
import eu.kanade.tachiyomi.data.track.TrackService
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import logcat.LogPriority
|
||||
import tachiyomi.core.util.system.logcat
|
||||
import tachiyomi.domain.chapter.model.Chapter
|
||||
import tachiyomi.domain.chapter.model.toChapterUpdate
|
||||
import tachiyomi.domain.track.model.Track
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
|
@ -1,9 +1,9 @@
|
||||
package eu.kanade.domain.chapter.interactor
|
||||
|
||||
import eu.kanade.domain.chapter.model.ChapterUpdate
|
||||
import eu.kanade.domain.chapter.repository.ChapterRepository
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import logcat.LogPriority
|
||||
import tachiyomi.core.util.system.logcat
|
||||
import tachiyomi.domain.chapter.model.ChapterUpdate
|
||||
import tachiyomi.domain.chapter.repository.ChapterRepository
|
||||
|
||||
class UpdateChapter(
|
||||
private val chapterRepository: ChapterRepository,
|
||||
|
@ -2,64 +2,30 @@ package eu.kanade.domain.chapter.model
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.models.ChapterImpl
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import tachiyomi.domain.chapter.model.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter as DbChapter
|
||||
|
||||
data class Chapter(
|
||||
val id: Long,
|
||||
val mangaId: Long,
|
||||
val read: Boolean,
|
||||
val bookmark: Boolean,
|
||||
val lastPageRead: Long,
|
||||
val dateFetch: Long,
|
||||
val sourceOrder: Long,
|
||||
val url: String,
|
||||
val name: String,
|
||||
val dateUpload: Long,
|
||||
val chapterNumber: Float,
|
||||
val scanlator: String?,
|
||||
) {
|
||||
val isRecognizedNumber: Boolean
|
||||
get() = chapterNumber >= 0f
|
||||
|
||||
fun toSChapter(): SChapter {
|
||||
return SChapter.create().also {
|
||||
it.url = url
|
||||
it.name = name
|
||||
it.date_upload = dateUpload
|
||||
it.chapter_number = chapterNumber
|
||||
it.scanlator = scanlator
|
||||
}
|
||||
}
|
||||
|
||||
fun copyFromSChapter(sChapter: SChapter): Chapter {
|
||||
return this.copy(
|
||||
name = sChapter.name,
|
||||
url = sChapter.url,
|
||||
dateUpload = sChapter.date_upload,
|
||||
chapterNumber = sChapter.chapter_number,
|
||||
scanlator = sChapter.scanlator?.ifBlank { null },
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun create() = Chapter(
|
||||
id = -1,
|
||||
mangaId = -1,
|
||||
read = false,
|
||||
bookmark = false,
|
||||
lastPageRead = 0,
|
||||
dateFetch = 0,
|
||||
sourceOrder = 0,
|
||||
url = "",
|
||||
name = "",
|
||||
dateUpload = -1,
|
||||
chapterNumber = -1f,
|
||||
scanlator = null,
|
||||
)
|
||||
// TODO: Remove when all deps are migrated
|
||||
fun Chapter.toSChapter(): SChapter {
|
||||
return SChapter.create().also {
|
||||
it.url = url
|
||||
it.name = name
|
||||
it.date_upload = dateUpload
|
||||
it.chapter_number = chapterNumber
|
||||
it.scanlator = scanlator
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Remove when all deps are migrated
|
||||
fun Chapter.copyFromSChapter(sChapter: SChapter): Chapter {
|
||||
return this.copy(
|
||||
name = sChapter.name,
|
||||
url = sChapter.url,
|
||||
dateUpload = sChapter.date_upload,
|
||||
chapterNumber = sChapter.chapter_number,
|
||||
scanlator = sChapter.scanlator?.ifBlank { null },
|
||||
)
|
||||
}
|
||||
|
||||
fun Chapter.toDbChapter(): DbChapter = ChapterImpl().also {
|
||||
it.id = id
|
||||
it.manga_id = mangaId
|
||||
|
@ -0,0 +1,84 @@
|
||||
package eu.kanade.domain.chapter.model
|
||||
|
||||
import eu.kanade.domain.manga.model.downloadedFilter
|
||||
import eu.kanade.domain.manga.model.isLocal
|
||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||
import eu.kanade.tachiyomi.data.download.model.Download
|
||||
import eu.kanade.tachiyomi.ui.manga.ChapterItem
|
||||
import eu.kanade.tachiyomi.util.chapter.getChapterSort
|
||||
import tachiyomi.domain.chapter.model.Chapter
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.domain.manga.model.TriStateFilter
|
||||
|
||||
/**
|
||||
* Applies the view filters to the list of chapters obtained from the database.
|
||||
* @return an observable of the list of chapters filtered and sorted.
|
||||
*/
|
||||
fun List<Chapter>.applyFilters(manga: Manga, downloadManager: DownloadManager): List<Chapter> {
|
||||
val isLocalManga = manga.isLocal()
|
||||
val unreadFilter = manga.unreadFilter
|
||||
val downloadedFilter = manga.downloadedFilter
|
||||
val bookmarkedFilter = manga.bookmarkedFilter
|
||||
|
||||
return filter { chapter ->
|
||||
when (unreadFilter) {
|
||||
TriStateFilter.DISABLED -> true
|
||||
TriStateFilter.ENABLED_IS -> !chapter.read
|
||||
TriStateFilter.ENABLED_NOT -> chapter.read
|
||||
}
|
||||
}
|
||||
.filter { chapter ->
|
||||
when (bookmarkedFilter) {
|
||||
TriStateFilter.DISABLED -> true
|
||||
TriStateFilter.ENABLED_IS -> chapter.bookmark
|
||||
TriStateFilter.ENABLED_NOT -> !chapter.bookmark
|
||||
}
|
||||
}
|
||||
.filter { chapter ->
|
||||
val downloaded = downloadManager.isChapterDownloaded(chapter.name, chapter.scanlator, manga.title, manga.source)
|
||||
val downloadState = when {
|
||||
downloaded -> Download.State.DOWNLOADED
|
||||
else -> Download.State.NOT_DOWNLOADED
|
||||
}
|
||||
when (downloadedFilter) {
|
||||
TriStateFilter.DISABLED -> true
|
||||
TriStateFilter.ENABLED_IS -> downloadState == Download.State.DOWNLOADED || isLocalManga
|
||||
TriStateFilter.ENABLED_NOT -> downloadState != Download.State.DOWNLOADED && !isLocalManga
|
||||
}
|
||||
}
|
||||
.sortedWith(getChapterSort(manga))
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the view filters to the list of chapters obtained from the database.
|
||||
* @return an observable of the list of chapters filtered and sorted.
|
||||
*/
|
||||
fun List<ChapterItem>.applyFilters(manga: Manga): Sequence<ChapterItem> {
|
||||
val isLocalManga = manga.isLocal()
|
||||
val unreadFilter = manga.unreadFilter
|
||||
val downloadedFilter = manga.downloadedFilter
|
||||
val bookmarkedFilter = manga.bookmarkedFilter
|
||||
return asSequence()
|
||||
.filter { (chapter) ->
|
||||
when (unreadFilter) {
|
||||
TriStateFilter.DISABLED -> true
|
||||
TriStateFilter.ENABLED_IS -> !chapter.read
|
||||
TriStateFilter.ENABLED_NOT -> chapter.read
|
||||
}
|
||||
}
|
||||
.filter { (chapter) ->
|
||||
when (bookmarkedFilter) {
|
||||
TriStateFilter.DISABLED -> true
|
||||
TriStateFilter.ENABLED_IS -> chapter.bookmark
|
||||
TriStateFilter.ENABLED_NOT -> !chapter.bookmark
|
||||
}
|
||||
}
|
||||
.filter {
|
||||
when (downloadedFilter) {
|
||||
TriStateFilter.DISABLED -> true
|
||||
TriStateFilter.ENABLED_IS -> it.isDownloaded || isLocalManga
|
||||
TriStateFilter.ENABLED_NOT -> !it.isDownloaded && !isLocalManga
|
||||
}
|
||||
}
|
||||
.sortedWith { (chapter1), (chapter2) -> getChapterSort(manga).invoke(chapter1, chapter2) }
|
||||
}
|
@ -1,11 +1,10 @@
|
||||
package eu.kanade.domain.download.interactor
|
||||
|
||||
import eu.kanade.domain.chapter.model.Chapter
|
||||
import eu.kanade.domain.chapter.model.toDbChapter
|
||||
import eu.kanade.domain.manga.model.Manga
|
||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.util.lang.withNonCancellableContext
|
||||
import tachiyomi.core.util.lang.withNonCancellableContext
|
||||
import tachiyomi.domain.chapter.model.Chapter
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
|
||||
class DeleteDownload(
|
||||
private val sourceManager: SourceManager,
|
||||
@ -14,7 +13,7 @@ class DeleteDownload(
|
||||
|
||||
suspend fun awaitAll(manga: Manga, vararg chapters: Chapter) = withNonCancellableContext {
|
||||
sourceManager.get(manga.source)?.let { source ->
|
||||
downloadManager.deleteChapters(chapters.map { it.toDbChapter() }, manga, source)
|
||||
downloadManager.deleteChapters(chapters.toList(), manga, source)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
package eu.kanade.domain.download.service
|
||||
|
||||
import eu.kanade.tachiyomi.core.preference.PreferenceStore
|
||||
import eu.kanade.tachiyomi.core.provider.FolderProvider
|
||||
import tachiyomi.core.preference.PreferenceStore
|
||||
import tachiyomi.core.provider.FolderProvider
|
||||
|
||||
class DownloadPreferences(
|
||||
private val folderProvider: FolderProvider,
|
||||
|
@ -25,10 +25,7 @@ class GetExtensionLanguages(
|
||||
}
|
||||
.distinct()
|
||||
.sortedWith(
|
||||
compareBy(
|
||||
{ it !in enabledLanguage },
|
||||
{ LocaleHelper.getDisplayName(it) },
|
||||
),
|
||||
compareBy<String> { it !in enabledLanguage }.then(LocaleHelper.comparator),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,6 @@ package eu.kanade.domain.extension.interactor
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import eu.kanade.tachiyomi.extension.model.Extension
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionSourceItem
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
@ -30,3 +29,9 @@ class GetExtensionSources(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class ExtensionSourceItem(
|
||||
val source: Source,
|
||||
val enabled: Boolean,
|
||||
val labelAsName: Boolean,
|
||||
)
|
||||
|
@ -1,12 +0,0 @@
|
||||
package eu.kanade.domain.history.interactor
|
||||
|
||||
import eu.kanade.domain.history.repository.HistoryRepository
|
||||
|
||||
class DeleteAllHistory(
|
||||
private val repository: HistoryRepository,
|
||||
) {
|
||||
|
||||
suspend fun await(): Boolean {
|
||||
return repository.deleteAllHistory()
|
||||
}
|
||||
}
|
@ -1,51 +0,0 @@
|
||||
package eu.kanade.domain.history.interactor
|
||||
|
||||
import eu.kanade.domain.chapter.interactor.GetChapter
|
||||
import eu.kanade.domain.chapter.interactor.GetChapterByMangaId
|
||||
import eu.kanade.domain.chapter.model.Chapter
|
||||
import eu.kanade.domain.history.repository.HistoryRepository
|
||||
import eu.kanade.domain.manga.interactor.GetManga
|
||||
import eu.kanade.domain.manga.model.Manga
|
||||
import eu.kanade.tachiyomi.util.chapter.getChapterSort
|
||||
|
||||
class GetNextChapter(
|
||||
private val getChapter: GetChapter,
|
||||
private val getChapterByMangaId: GetChapterByMangaId,
|
||||
private val getManga: GetManga,
|
||||
private val historyRepository: HistoryRepository,
|
||||
) {
|
||||
|
||||
suspend fun await(): Chapter? {
|
||||
val history = historyRepository.getLastHistory() ?: return null
|
||||
return await(history.mangaId, history.chapterId)
|
||||
}
|
||||
|
||||
suspend fun await(mangaId: Long, chapterId: Long): Chapter? {
|
||||
val chapter = getChapter.await(chapterId) ?: return null
|
||||
val manga = getManga.await(mangaId) ?: return null
|
||||
|
||||
if (!chapter.read) return chapter
|
||||
|
||||
val chapters = getChapterByMangaId.await(mangaId)
|
||||
.sortedWith(getChapterSort(manga, sortDescending = false))
|
||||
|
||||
val currChapterIndex = chapters.indexOfFirst { chapter.id == it.id }
|
||||
return when (manga.sorting) {
|
||||
Manga.CHAPTER_SORTING_SOURCE -> chapters.getOrNull(currChapterIndex + 1)
|
||||
Manga.CHAPTER_SORTING_NUMBER -> {
|
||||
val chapterNumber = chapter.chapterNumber
|
||||
|
||||
((currChapterIndex + 1) until chapters.size)
|
||||
.map { chapters[it] }
|
||||
.firstOrNull {
|
||||
it.chapterNumber > chapterNumber && it.chapterNumber <= chapterNumber + 1
|
||||
}
|
||||
}
|
||||
Manga.CHAPTER_SORTING_UPLOAD_DATE -> {
|
||||
chapters.drop(currChapterIndex + 1)
|
||||
.firstOrNull { it.dateUpload >= chapter.dateUpload }
|
||||
}
|
||||
else -> throw NotImplementedError("Invalid chapter sorting method: ${manga.sorting}")
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
package eu.kanade.domain.history.interactor
|
||||
|
||||
import eu.kanade.domain.chapter.interactor.GetChapterByMangaId
|
||||
import eu.kanade.domain.manga.interactor.GetManga
|
||||
import eu.kanade.tachiyomi.util.chapter.getChapterSort
|
||||
import tachiyomi.domain.chapter.model.Chapter
|
||||
import tachiyomi.domain.history.repository.HistoryRepository
|
||||
import kotlin.math.max
|
||||
|
||||
class GetNextChapters(
|
||||
private val getChapterByMangaId: GetChapterByMangaId,
|
||||
private val getManga: GetManga,
|
||||
private val historyRepository: HistoryRepository,
|
||||
) {
|
||||
|
||||
suspend fun await(onlyUnread: Boolean = true): List<Chapter> {
|
||||
val history = historyRepository.getLastHistory() ?: return emptyList()
|
||||
return await(history.mangaId, history.chapterId, onlyUnread)
|
||||
}
|
||||
|
||||
suspend fun await(mangaId: Long, onlyUnread: Boolean = true): List<Chapter> {
|
||||
val manga = getManga.await(mangaId) ?: return emptyList()
|
||||
val chapters = getChapterByMangaId.await(mangaId)
|
||||
.sortedWith(getChapterSort(manga, sortDescending = false))
|
||||
|
||||
return if (onlyUnread) {
|
||||
chapters.filterNot { it.read }
|
||||
} else {
|
||||
chapters
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun await(mangaId: Long, fromChapterId: Long, onlyUnread: Boolean = true): List<Chapter> {
|
||||
val chapters = await(mangaId, onlyUnread)
|
||||
val currChapterIndex = chapters.indexOfFirst { it.id == fromChapterId }
|
||||
val nextChapters = chapters.subList(max(0, currChapterIndex), chapters.size)
|
||||
|
||||
if (onlyUnread) {
|
||||
return nextChapters
|
||||
}
|
||||
|
||||
// The "next chapter" is either:
|
||||
// - The current chapter if it isn't completely read
|
||||
// - The chapters after the current chapter if the current one is completely read
|
||||
val fromChapter = chapters.getOrNull(currChapterIndex)
|
||||
return if (fromChapter != null && !fromChapter.read) {
|
||||
nextChapters
|
||||
} else {
|
||||
nextChapters.drop(1)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
package eu.kanade.domain.history.interactor
|
||||
|
||||
import eu.kanade.domain.history.model.HistoryWithRelations
|
||||
import eu.kanade.domain.history.repository.HistoryRepository
|
||||
|
||||
class RemoveHistoryById(
|
||||
private val repository: HistoryRepository,
|
||||
) {
|
||||
|
||||
suspend fun await(history: HistoryWithRelations) {
|
||||
repository.resetHistory(history.id)
|
||||
}
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
package eu.kanade.domain.history.interactor
|
||||
|
||||
import eu.kanade.domain.history.repository.HistoryRepository
|
||||
|
||||
class RemoveHistoryByMangaId(
|
||||
private val repository: HistoryRepository,
|
||||
) {
|
||||
|
||||
suspend fun await(mangaId: Long) {
|
||||
repository.resetHistoryByMangaId(mangaId)
|
||||
}
|
||||
}
|
@ -1,14 +1,14 @@
|
||||
package eu.kanade.domain.library.service
|
||||
|
||||
import eu.kanade.domain.library.model.LibraryDisplayMode
|
||||
import eu.kanade.domain.library.model.LibrarySort
|
||||
import eu.kanade.domain.manga.model.Manga
|
||||
import eu.kanade.tachiyomi.core.preference.PreferenceStore
|
||||
import eu.kanade.tachiyomi.data.preference.DEVICE_ONLY_ON_WIFI
|
||||
import eu.kanade.tachiyomi.data.preference.MANGA_HAS_UNREAD
|
||||
import eu.kanade.tachiyomi.data.preference.MANGA_NON_COMPLETED
|
||||
import eu.kanade.tachiyomi.data.preference.MANGA_NON_READ
|
||||
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
|
||||
import tachiyomi.core.preference.PreferenceStore
|
||||
import tachiyomi.domain.library.model.LibraryDisplayMode
|
||||
import tachiyomi.domain.library.model.LibrarySort
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
|
||||
class LibraryPreferences(
|
||||
private val preferenceStore: PreferenceStore,
|
||||
@ -32,6 +32,8 @@ class LibraryPreferences(
|
||||
|
||||
fun autoUpdateTrackers() = preferenceStore.getBoolean("auto_update_trackers", false)
|
||||
|
||||
fun showContinueReadingButton() = preferenceStore.getBoolean("display_continue_reading_button", false)
|
||||
|
||||
// region Filter
|
||||
|
||||
fun filterDownloaded() = preferenceStore.getInt("pref_filter_library_downloaded", ExtendedNavigationView.Item.TriStateGroup.State.IGNORE.value)
|
||||
@ -54,12 +56,10 @@ class LibraryPreferences(
|
||||
|
||||
fun localBadge() = preferenceStore.getBoolean("display_local_badge", true)
|
||||
|
||||
fun unreadBadge() = preferenceStore.getBoolean("display_unread_badge", true)
|
||||
|
||||
fun languageBadge() = preferenceStore.getBoolean("display_language_badge", false)
|
||||
|
||||
fun showUpdatesNavBadge() = preferenceStore.getBoolean("library_update_show_tab_badge", false)
|
||||
fun unreadUpdatesCount() = preferenceStore.getInt("library_unread_updates_count", 0)
|
||||
fun newShowUpdatesCount() = preferenceStore.getBoolean("library_show_updates_count", true)
|
||||
fun newUpdatesCount() = preferenceStore.getInt("library_unseen_updates_count", 0)
|
||||
|
||||
// endregion
|
||||
|
||||
|
@ -1,13 +1,13 @@
|
||||
package eu.kanade.domain.manga.interactor
|
||||
|
||||
import eu.kanade.domain.manga.model.Manga
|
||||
import eu.kanade.domain.manga.repository.MangaRepository
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.domain.manga.repository.MangaRepository
|
||||
|
||||
class GetDuplicateLibraryManga(
|
||||
private val mangaRepository: MangaRepository,
|
||||
) {
|
||||
|
||||
suspend fun await(title: String, sourceId: Long): Manga? {
|
||||
return mangaRepository.getDuplicateLibraryManga(title.lowercase(), sourceId)
|
||||
suspend fun await(title: String): Manga? {
|
||||
return mangaRepository.getDuplicateLibraryManga(title.lowercase())
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
package eu.kanade.domain.manga.interactor
|
||||
|
||||
import eu.kanade.domain.manga.model.Manga
|
||||
import eu.kanade.domain.manga.repository.MangaRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.domain.manga.repository.MangaRepository
|
||||
|
||||
class GetFavorites(
|
||||
private val mangaRepository: MangaRepository,
|
||||
|
@ -1,8 +1,8 @@
|
||||
package eu.kanade.domain.manga.interactor
|
||||
|
||||
import eu.kanade.domain.library.model.LibraryManga
|
||||
import eu.kanade.domain.manga.repository.MangaRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import tachiyomi.domain.library.model.LibraryManga
|
||||
import tachiyomi.domain.manga.repository.MangaRepository
|
||||
|
||||
class GetLibraryManga(
|
||||
private val mangaRepository: MangaRepository,
|
||||
|
@ -1,10 +1,10 @@
|
||||
package eu.kanade.domain.manga.interactor
|
||||
|
||||
import eu.kanade.domain.manga.model.Manga
|
||||
import eu.kanade.domain.manga.repository.MangaRepository
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import logcat.LogPriority
|
||||
import tachiyomi.core.util.system.logcat
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.domain.manga.repository.MangaRepository
|
||||
|
||||
class GetManga(
|
||||
private val mangaRepository: MangaRepository,
|
||||
|
@ -1,11 +1,11 @@
|
||||
package eu.kanade.domain.manga.interactor
|
||||
|
||||
import eu.kanade.domain.chapter.model.Chapter
|
||||
import eu.kanade.domain.chapter.repository.ChapterRepository
|
||||
import eu.kanade.domain.manga.model.Manga
|
||||
import eu.kanade.domain.manga.repository.MangaRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import tachiyomi.domain.chapter.model.Chapter
|
||||
import tachiyomi.domain.chapter.repository.ChapterRepository
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.domain.manga.repository.MangaRepository
|
||||
|
||||
class GetMangaWithChapters(
|
||||
private val mangaRepository: MangaRepository,
|
||||
|
@ -1,17 +1,17 @@
|
||||
package eu.kanade.domain.manga.interactor
|
||||
|
||||
import eu.kanade.domain.manga.model.Manga
|
||||
import eu.kanade.domain.manga.repository.MangaRepository
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.domain.manga.repository.MangaRepository
|
||||
|
||||
class NetworkToLocalManga(
|
||||
private val mangaRepository: MangaRepository,
|
||||
) {
|
||||
|
||||
suspend fun await(manga: Manga, sourceId: Long): Manga {
|
||||
val localManga = getManga(manga.url, sourceId)
|
||||
suspend fun await(manga: Manga): Manga {
|
||||
val localManga = getManga(manga.url, manga.source)
|
||||
return when {
|
||||
localManga == null -> {
|
||||
val id = insertManga(manga.copy(source = sourceId))
|
||||
val id = insertManga(manga)
|
||||
manga.copy(id = id!!)
|
||||
}
|
||||
!localManga.favorite -> {
|
||||
|
@ -1,6 +1,6 @@
|
||||
package eu.kanade.domain.manga.interactor
|
||||
|
||||
import eu.kanade.domain.manga.repository.MangaRepository
|
||||
import tachiyomi.domain.manga.repository.MangaRepository
|
||||
|
||||
class ResetViewerFlags(
|
||||
private val mangaRepository: MangaRepository,
|
||||
|
@ -1,8 +1,8 @@
|
||||
package eu.kanade.domain.manga.interactor
|
||||
|
||||
import eu.kanade.domain.manga.model.Manga
|
||||
import eu.kanade.domain.manga.model.MangaUpdate
|
||||
import eu.kanade.domain.manga.repository.MangaRepository
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.domain.manga.model.MangaUpdate
|
||||
import tachiyomi.domain.manga.repository.MangaRepository
|
||||
|
||||
class SetMangaChapterFlags(
|
||||
private val mangaRepository: MangaRepository,
|
||||
|
@ -1,28 +1,30 @@
|
||||
package eu.kanade.domain.manga.interactor
|
||||
|
||||
import eu.kanade.domain.manga.model.MangaUpdate
|
||||
import eu.kanade.domain.manga.repository.MangaRepository
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
|
||||
import tachiyomi.domain.manga.model.MangaUpdate
|
||||
import tachiyomi.domain.manga.repository.MangaRepository
|
||||
|
||||
class SetMangaViewerFlags(
|
||||
private val mangaRepository: MangaRepository,
|
||||
) {
|
||||
|
||||
suspend fun awaitSetMangaReadingMode(id: Long, flag: Long) {
|
||||
val manga = mangaRepository.getMangaById(id)
|
||||
mangaRepository.update(
|
||||
MangaUpdate(
|
||||
id = id,
|
||||
viewerFlags = flag.setFlag(flag, ReadingModeType.MASK.toLong()),
|
||||
viewerFlags = manga.viewerFlags.setFlag(flag, ReadingModeType.MASK.toLong()),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun awaitSetOrientationType(id: Long, flag: Long) {
|
||||
val manga = mangaRepository.getMangaById(id)
|
||||
mangaRepository.update(
|
||||
MangaUpdate(
|
||||
id = id,
|
||||
viewerFlags = flag.setFlag(flag, OrientationType.MASK.toLong()),
|
||||
viewerFlags = manga.viewerFlags.setFlag(flag, OrientationType.MASK.toLong()),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
@ -1,13 +1,12 @@
|
||||
package eu.kanade.domain.manga.interactor
|
||||
|
||||
import eu.kanade.domain.manga.model.Manga
|
||||
import eu.kanade.domain.manga.model.MangaUpdate
|
||||
import eu.kanade.domain.manga.model.hasCustomCover
|
||||
import eu.kanade.domain.manga.model.isLocal
|
||||
import eu.kanade.domain.manga.model.toDbManga
|
||||
import eu.kanade.domain.manga.repository.MangaRepository
|
||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.domain.manga.model.MangaUpdate
|
||||
import tachiyomi.domain.manga.repository.MangaRepository
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.util.Date
|
||||
@ -46,11 +45,11 @@ class UpdateManga(
|
||||
!manualFetch && localManga.thumbnailUrl == remoteManga.thumbnail_url -> null
|
||||
localManga.isLocal() -> Date().time
|
||||
localManga.hasCustomCover(coverCache) -> {
|
||||
coverCache.deleteFromCache(localManga.toDbManga(), false)
|
||||
coverCache.deleteFromCache(localManga, false)
|
||||
null
|
||||
}
|
||||
else -> {
|
||||
coverCache.deleteFromCache(localManga.toDbManga(), false)
|
||||
coverCache.deleteFromCache(localManga, false)
|
||||
Date().time
|
||||
}
|
||||
}
|
||||
|
@ -1,60 +1,176 @@
|
||||
package eu.kanade.domain.manga.model
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import kotlinx.serialization.Serializable
|
||||
import nl.adaptivity.xmlutil.serialization.XmlElement
|
||||
import nl.adaptivity.xmlutil.serialization.XmlSerialName
|
||||
import nl.adaptivity.xmlutil.serialization.XmlValue
|
||||
import tachiyomi.domain.chapter.model.Chapter
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
|
||||
const val COMIC_INFO_FILE = "ComicInfo.xml"
|
||||
|
||||
/**
|
||||
* Creates a ComicInfo instance based on the manga and chapter metadata.
|
||||
*/
|
||||
fun getComicInfo(manga: Manga, chapter: Chapter, chapterUrl: String) = ComicInfo(
|
||||
title = ComicInfo.Title(chapter.name),
|
||||
series = ComicInfo.Series(manga.title),
|
||||
web = ComicInfo.Web(chapterUrl),
|
||||
summary = manga.description?.let { ComicInfo.Summary(it) },
|
||||
writer = manga.author?.let { ComicInfo.Writer(it) },
|
||||
penciller = manga.artist?.let { ComicInfo.Penciller(it) },
|
||||
translator = chapter.scanlator?.let { ComicInfo.Translator(it) },
|
||||
genre = manga.genre?.let { ComicInfo.Genre(it.joinToString()) },
|
||||
publishingStatus = ComicInfo.PublishingStatusTachiyomi(
|
||||
ComicInfoPublishingStatus.toComicInfoValue(manga.status),
|
||||
),
|
||||
inker = null,
|
||||
colorist = null,
|
||||
letterer = null,
|
||||
coverArtist = null,
|
||||
tags = null,
|
||||
)
|
||||
|
||||
fun SManga.copyFromComicInfo(comicInfo: ComicInfo) {
|
||||
comicInfo.series?.let { title = it.value }
|
||||
comicInfo.writer?.let { author = it.value }
|
||||
comicInfo.summary?.let { description = it.value }
|
||||
|
||||
listOfNotNull(
|
||||
comicInfo.genre?.value,
|
||||
comicInfo.tags?.value,
|
||||
)
|
||||
.flatMap { it.split(", ") }
|
||||
.distinct()
|
||||
.joinToString(", ") { it.trim() }
|
||||
.takeIf { it.isNotEmpty() }
|
||||
?.let { genre = it }
|
||||
|
||||
listOfNotNull(
|
||||
comicInfo.penciller?.value,
|
||||
comicInfo.inker?.value,
|
||||
comicInfo.colorist?.value,
|
||||
comicInfo.letterer?.value,
|
||||
comicInfo.coverArtist?.value,
|
||||
)
|
||||
.flatMap { it.split(", ") }
|
||||
.distinct()
|
||||
.joinToString(", ") { it.trim() }
|
||||
.takeIf { it.isNotEmpty() }
|
||||
?.let { artist = it }
|
||||
|
||||
status = ComicInfoPublishingStatus.toSMangaValue(comicInfo.publishingStatus?.value)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
@XmlSerialName("ComicInfo", "", "")
|
||||
data class ComicInfo(
|
||||
val series: ComicInfoSeries?,
|
||||
val summary: ComicInfoSummary?,
|
||||
val writer: ComicInfoWriter?,
|
||||
val penciller: ComicInfoPenciller?,
|
||||
val inker: ComicInfoInker?,
|
||||
val colorist: ComicInfoColorist?,
|
||||
val letterer: ComicInfoLetterer?,
|
||||
val coverArtist: ComicInfoCoverArtist?,
|
||||
val genre: ComicInfoGenre?,
|
||||
val tags: ComicInfoTags?,
|
||||
)
|
||||
val title: Title?,
|
||||
val series: Series?,
|
||||
val summary: Summary?,
|
||||
val writer: Writer?,
|
||||
val penciller: Penciller?,
|
||||
val inker: Inker?,
|
||||
val colorist: Colorist?,
|
||||
val letterer: Letterer?,
|
||||
val coverArtist: CoverArtist?,
|
||||
val translator: Translator?,
|
||||
val genre: Genre?,
|
||||
val tags: Tags?,
|
||||
val web: Web?,
|
||||
val publishingStatus: PublishingStatusTachiyomi?,
|
||||
) {
|
||||
@Suppress("UNUSED")
|
||||
@XmlElement(false)
|
||||
@XmlSerialName("xmlns:xsd", "", "")
|
||||
val xmlSchema: String = "http://www.w3.org/2001/XMLSchema"
|
||||
|
||||
@Serializable
|
||||
@XmlSerialName("Series", "", "")
|
||||
data class ComicInfoSeries(@XmlValue(true) val value: String = "")
|
||||
@Suppress("UNUSED")
|
||||
@XmlElement(false)
|
||||
@XmlSerialName("xmlns:xsi", "", "")
|
||||
val xmlSchemaInstance: String = "http://www.w3.org/2001/XMLSchema-instance"
|
||||
|
||||
@Serializable
|
||||
@XmlSerialName("Summary", "", "")
|
||||
data class ComicInfoSummary(@XmlValue(true) val value: String = "")
|
||||
@Serializable
|
||||
@XmlSerialName("Title", "", "")
|
||||
data class Title(@XmlValue(true) val value: String = "")
|
||||
|
||||
@Serializable
|
||||
@XmlSerialName("Writer", "", "")
|
||||
data class ComicInfoWriter(@XmlValue(true) val value: String = "")
|
||||
@Serializable
|
||||
@XmlSerialName("Series", "", "")
|
||||
data class Series(@XmlValue(true) val value: String = "")
|
||||
|
||||
@Serializable
|
||||
@XmlSerialName("Penciller", "", "")
|
||||
data class ComicInfoPenciller(@XmlValue(true) val value: String = "")
|
||||
@Serializable
|
||||
@XmlSerialName("Summary", "", "")
|
||||
data class Summary(@XmlValue(true) val value: String = "")
|
||||
|
||||
@Serializable
|
||||
@XmlSerialName("Inker", "", "")
|
||||
data class ComicInfoInker(@XmlValue(true) val value: String = "")
|
||||
@Serializable
|
||||
@XmlSerialName("Writer", "", "")
|
||||
data class Writer(@XmlValue(true) val value: String = "")
|
||||
|
||||
@Serializable
|
||||
@XmlSerialName("Colorist", "", "")
|
||||
data class ComicInfoColorist(@XmlValue(true) val value: String = "")
|
||||
@Serializable
|
||||
@XmlSerialName("Penciller", "", "")
|
||||
data class Penciller(@XmlValue(true) val value: String = "")
|
||||
|
||||
@Serializable
|
||||
@XmlSerialName("Letterer", "", "")
|
||||
data class ComicInfoLetterer(@XmlValue(true) val value: String = "")
|
||||
@Serializable
|
||||
@XmlSerialName("Inker", "", "")
|
||||
data class Inker(@XmlValue(true) val value: String = "")
|
||||
|
||||
@Serializable
|
||||
@XmlSerialName("CoverArtist", "", "")
|
||||
data class ComicInfoCoverArtist(@XmlValue(true) val value: String = "")
|
||||
@Serializable
|
||||
@XmlSerialName("Colorist", "", "")
|
||||
data class Colorist(@XmlValue(true) val value: String = "")
|
||||
|
||||
@Serializable
|
||||
@XmlSerialName("Genre", "", "")
|
||||
data class ComicInfoGenre(@XmlValue(true) val value: String = "")
|
||||
@Serializable
|
||||
@XmlSerialName("Letterer", "", "")
|
||||
data class Letterer(@XmlValue(true) val value: String = "")
|
||||
|
||||
@Serializable
|
||||
@XmlSerialName("Tags", "", "")
|
||||
data class ComicInfoTags(@XmlValue(true) val value: String = "")
|
||||
@Serializable
|
||||
@XmlSerialName("CoverArtist", "", "")
|
||||
data class CoverArtist(@XmlValue(true) val value: String = "")
|
||||
|
||||
@Serializable
|
||||
@XmlSerialName("Translator", "", "")
|
||||
data class Translator(@XmlValue(true) val value: String = "")
|
||||
|
||||
@Serializable
|
||||
@XmlSerialName("Genre", "", "")
|
||||
data class Genre(@XmlValue(true) val value: String = "")
|
||||
|
||||
@Serializable
|
||||
@XmlSerialName("Tags", "", "")
|
||||
data class Tags(@XmlValue(true) val value: String = "")
|
||||
|
||||
@Serializable
|
||||
@XmlSerialName("Web", "", "")
|
||||
data class Web(@XmlValue(true) val value: String = "")
|
||||
|
||||
// The spec doesn't have a good field for this
|
||||
@Serializable
|
||||
@XmlSerialName("PublishingStatusTachiyomi", "http://www.w3.org/2001/XMLSchema", "ty")
|
||||
data class PublishingStatusTachiyomi(@XmlValue(true) val value: String = "")
|
||||
}
|
||||
|
||||
private enum class ComicInfoPublishingStatus(
|
||||
val comicInfoValue: String,
|
||||
val sMangaModelValue: Int,
|
||||
) {
|
||||
ONGOING("Ongoing", SManga.ONGOING),
|
||||
COMPLETED("Completed", SManga.COMPLETED),
|
||||
LICENSED("Licensed", SManga.LICENSED),
|
||||
PUBLISHING_FINISHED("Publishing finished", SManga.PUBLISHING_FINISHED),
|
||||
CANCELLED("Cancelled", SManga.CANCELLED),
|
||||
ON_HIATUS("On hiatus", SManga.ON_HIATUS),
|
||||
UNKNOWN("Unknown", SManga.UNKNOWN),
|
||||
;
|
||||
|
||||
companion object {
|
||||
fun toComicInfoValue(value: Long): String {
|
||||
return values().firstOrNull { it.sMangaModelValue == value.toInt() }?.comicInfoValue
|
||||
?: UNKNOWN.comicInfoValue
|
||||
}
|
||||
|
||||
fun toSMangaValue(value: String?): Int {
|
||||
return values().firstOrNull { it.comicInfoValue == value }?.sMangaModelValue
|
||||
?: UNKNOWN.sMangaModelValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,238 +1,76 @@
|
||||
package eu.kanade.domain.manga.model
|
||||
|
||||
import eu.kanade.data.listOfStringsAdapter
|
||||
import eu.kanade.domain.base.BasePreferences
|
||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaImpl
|
||||
import eu.kanade.tachiyomi.source.LocalSource
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
||||
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.domain.manga.model.TriStateFilter
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.Serializable
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga as DbManga
|
||||
|
||||
data class Manga(
|
||||
val id: Long,
|
||||
val source: Long,
|
||||
val favorite: Boolean,
|
||||
val lastUpdate: Long,
|
||||
val dateAdded: Long,
|
||||
val viewerFlags: Long,
|
||||
val chapterFlags: Long,
|
||||
val coverLastModified: Long,
|
||||
val url: String,
|
||||
val title: String,
|
||||
val artist: String?,
|
||||
val author: String?,
|
||||
val description: String?,
|
||||
val genre: List<String>?,
|
||||
val status: Long,
|
||||
val thumbnailUrl: String?,
|
||||
val updateStrategy: UpdateStrategy,
|
||||
val initialized: Boolean,
|
||||
) : Serializable {
|
||||
// TODO: move these into the domain model
|
||||
val Manga.readingModeType: Long
|
||||
get() = viewerFlags and ReadingModeType.MASK.toLong()
|
||||
|
||||
val sorting: Long
|
||||
get() = chapterFlags and CHAPTER_SORTING_MASK
|
||||
val Manga.orientationType: Long
|
||||
get() = viewerFlags and OrientationType.MASK.toLong()
|
||||
|
||||
val displayMode: Long
|
||||
get() = chapterFlags and CHAPTER_DISPLAY_MASK
|
||||
|
||||
val unreadFilterRaw: Long
|
||||
get() = chapterFlags and CHAPTER_UNREAD_MASK
|
||||
|
||||
val downloadedFilterRaw: Long
|
||||
get() = chapterFlags and CHAPTER_DOWNLOADED_MASK
|
||||
|
||||
val bookmarkedFilterRaw: Long
|
||||
get() = chapterFlags and CHAPTER_BOOKMARKED_MASK
|
||||
|
||||
val unreadFilter: TriStateFilter
|
||||
get() = when (unreadFilterRaw) {
|
||||
CHAPTER_SHOW_UNREAD -> TriStateFilter.ENABLED_IS
|
||||
CHAPTER_SHOW_READ -> TriStateFilter.ENABLED_NOT
|
||||
val Manga.downloadedFilter: TriStateFilter
|
||||
get() {
|
||||
if (forceDownloaded()) return TriStateFilter.ENABLED_IS
|
||||
return when (downloadedFilterRaw) {
|
||||
Manga.CHAPTER_SHOW_DOWNLOADED -> TriStateFilter.ENABLED_IS
|
||||
Manga.CHAPTER_SHOW_NOT_DOWNLOADED -> TriStateFilter.ENABLED_NOT
|
||||
else -> TriStateFilter.DISABLED
|
||||
}
|
||||
|
||||
val downloadedFilter: TriStateFilter
|
||||
get() {
|
||||
if (forceDownloaded()) return TriStateFilter.ENABLED_IS
|
||||
return when (downloadedFilterRaw) {
|
||||
CHAPTER_SHOW_DOWNLOADED -> TriStateFilter.ENABLED_IS
|
||||
CHAPTER_SHOW_NOT_DOWNLOADED -> TriStateFilter.ENABLED_NOT
|
||||
else -> TriStateFilter.DISABLED
|
||||
}
|
||||
}
|
||||
|
||||
val bookmarkedFilter: TriStateFilter
|
||||
get() = when (bookmarkedFilterRaw) {
|
||||
CHAPTER_SHOW_BOOKMARKED -> TriStateFilter.ENABLED_IS
|
||||
CHAPTER_SHOW_NOT_BOOKMARKED -> TriStateFilter.ENABLED_NOT
|
||||
else -> TriStateFilter.DISABLED
|
||||
}
|
||||
|
||||
fun chaptersFiltered(): Boolean {
|
||||
return unreadFilter != TriStateFilter.DISABLED ||
|
||||
downloadedFilter != TriStateFilter.DISABLED ||
|
||||
bookmarkedFilter != TriStateFilter.DISABLED
|
||||
}
|
||||
|
||||
fun forceDownloaded(): Boolean {
|
||||
return favorite && Injekt.get<BasePreferences>().downloadedOnly().get()
|
||||
}
|
||||
|
||||
fun sortDescending(): Boolean {
|
||||
return chapterFlags and CHAPTER_SORT_DIR_MASK == CHAPTER_SORT_DESC
|
||||
}
|
||||
|
||||
fun toSManga(): SManga = SManga.create().also {
|
||||
it.url = url
|
||||
it.title = title
|
||||
it.artist = artist
|
||||
it.author = author
|
||||
it.description = description
|
||||
it.genre = genre.orEmpty().joinToString()
|
||||
it.status = status.toInt()
|
||||
it.thumbnail_url = thumbnailUrl
|
||||
it.initialized = initialized
|
||||
}
|
||||
|
||||
fun copyFrom(other: SManga): Manga {
|
||||
val author = other.author ?: author
|
||||
val artist = other.artist ?: artist
|
||||
val description = other.description ?: description
|
||||
val genres = if (other.genre != null) {
|
||||
other.getGenres()
|
||||
} else {
|
||||
genre
|
||||
}
|
||||
val thumbnailUrl = other.thumbnail_url ?: thumbnailUrl
|
||||
return this.copy(
|
||||
author = author,
|
||||
artist = artist,
|
||||
description = description,
|
||||
genre = genres,
|
||||
thumbnailUrl = thumbnailUrl,
|
||||
status = other.status.toLong(),
|
||||
updateStrategy = other.update_strategy,
|
||||
initialized = other.initialized && initialized,
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
// Generic filter that does not filter anything
|
||||
const val SHOW_ALL = 0x00000000L
|
||||
|
||||
const val CHAPTER_SORT_DESC = 0x00000000L
|
||||
const val CHAPTER_SORT_ASC = 0x00000001L
|
||||
const val CHAPTER_SORT_DIR_MASK = 0x00000001L
|
||||
|
||||
const val CHAPTER_SHOW_UNREAD = 0x00000002L
|
||||
const val CHAPTER_SHOW_READ = 0x00000004L
|
||||
const val CHAPTER_UNREAD_MASK = 0x00000006L
|
||||
|
||||
const val CHAPTER_SHOW_DOWNLOADED = 0x00000008L
|
||||
const val CHAPTER_SHOW_NOT_DOWNLOADED = 0x00000010L
|
||||
const val CHAPTER_DOWNLOADED_MASK = 0x00000018L
|
||||
|
||||
const val CHAPTER_SHOW_BOOKMARKED = 0x00000020L
|
||||
const val CHAPTER_SHOW_NOT_BOOKMARKED = 0x00000040L
|
||||
const val CHAPTER_BOOKMARKED_MASK = 0x00000060L
|
||||
|
||||
const val CHAPTER_SORTING_SOURCE = 0x00000000L
|
||||
const val CHAPTER_SORTING_NUMBER = 0x00000100L
|
||||
const val CHAPTER_SORTING_UPLOAD_DATE = 0x00000200L
|
||||
const val CHAPTER_SORTING_MASK = 0x00000300L
|
||||
|
||||
const val CHAPTER_DISPLAY_NAME = 0x00000000L
|
||||
const val CHAPTER_DISPLAY_NUMBER = 0x00100000L
|
||||
const val CHAPTER_DISPLAY_MASK = 0x00100000L
|
||||
|
||||
fun create() = Manga(
|
||||
id = -1L,
|
||||
url = "",
|
||||
title = "",
|
||||
source = -1L,
|
||||
favorite = false,
|
||||
lastUpdate = 0L,
|
||||
dateAdded = 0L,
|
||||
viewerFlags = 0L,
|
||||
chapterFlags = 0L,
|
||||
coverLastModified = 0L,
|
||||
artist = null,
|
||||
author = null,
|
||||
description = null,
|
||||
genre = null,
|
||||
status = 0L,
|
||||
thumbnailUrl = null,
|
||||
updateStrategy = UpdateStrategy.ALWAYS_UPDATE,
|
||||
initialized = false,
|
||||
)
|
||||
}
|
||||
fun Manga.chaptersFiltered(): Boolean {
|
||||
return unreadFilter != TriStateFilter.DISABLED ||
|
||||
downloadedFilter != TriStateFilter.DISABLED ||
|
||||
bookmarkedFilter != TriStateFilter.DISABLED
|
||||
}
|
||||
fun Manga.forceDownloaded(): Boolean {
|
||||
return favorite && Injekt.get<BasePreferences>().downloadedOnly().get()
|
||||
}
|
||||
|
||||
enum class TriStateFilter {
|
||||
DISABLED, // Disable filter
|
||||
ENABLED_IS, // Enabled with "is" filter
|
||||
ENABLED_NOT, // Enabled with "not" filter
|
||||
}
|
||||
|
||||
fun TriStateFilter.toTriStateGroupState(): ExtendedNavigationView.Item.TriStateGroup.State {
|
||||
return when (this) {
|
||||
TriStateFilter.DISABLED -> ExtendedNavigationView.Item.TriStateGroup.State.IGNORE
|
||||
TriStateFilter.ENABLED_IS -> ExtendedNavigationView.Item.TriStateGroup.State.INCLUDE
|
||||
TriStateFilter.ENABLED_NOT -> ExtendedNavigationView.Item.TriStateGroup.State.EXCLUDE
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Remove when all deps are migrated
|
||||
fun Manga.toDbManga(): DbManga = MangaImpl().also {
|
||||
it.id = id
|
||||
it.source = source
|
||||
it.favorite = favorite
|
||||
it.last_update = lastUpdate
|
||||
it.date_added = dateAdded
|
||||
it.viewer_flags = viewerFlags.toInt()
|
||||
it.chapter_flags = chapterFlags.toInt()
|
||||
it.cover_last_modified = coverLastModified
|
||||
fun Manga.toSManga(): SManga = SManga.create().also {
|
||||
it.url = url
|
||||
it.title = title
|
||||
it.artist = artist
|
||||
it.author = author
|
||||
it.description = description
|
||||
it.genre = genre?.let(listOfStringsAdapter::encode)
|
||||
it.genre = genre.orEmpty().joinToString()
|
||||
it.status = status.toInt()
|
||||
it.thumbnail_url = thumbnailUrl
|
||||
it.update_strategy = updateStrategy
|
||||
it.initialized = initialized
|
||||
}
|
||||
|
||||
fun Manga.toMangaUpdate(): MangaUpdate {
|
||||
return MangaUpdate(
|
||||
id = id,
|
||||
source = source,
|
||||
favorite = favorite,
|
||||
lastUpdate = lastUpdate,
|
||||
dateAdded = dateAdded,
|
||||
viewerFlags = viewerFlags,
|
||||
chapterFlags = chapterFlags,
|
||||
coverLastModified = coverLastModified,
|
||||
url = url,
|
||||
title = title,
|
||||
artist = artist,
|
||||
fun Manga.copyFrom(other: SManga): Manga {
|
||||
val author = other.author ?: author
|
||||
val artist = other.artist ?: artist
|
||||
val description = other.description ?: description
|
||||
val genres = if (other.genre != null) {
|
||||
other.getGenres()
|
||||
} else {
|
||||
genre
|
||||
}
|
||||
val thumbnailUrl = other.thumbnail_url ?: thumbnailUrl
|
||||
return this.copy(
|
||||
author = author,
|
||||
artist = artist,
|
||||
description = description,
|
||||
genre = genre,
|
||||
status = status,
|
||||
genre = genres,
|
||||
thumbnailUrl = thumbnailUrl,
|
||||
updateStrategy = updateStrategy,
|
||||
initialized = initialized,
|
||||
status = other.status.toLong(),
|
||||
updateStrategy = other.update_strategy,
|
||||
initialized = other.initialized && initialized,
|
||||
)
|
||||
}
|
||||
|
||||
fun SManga.toDomainManga(): Manga {
|
||||
fun SManga.toDomainManga(sourceId: Long): Manga {
|
||||
return Manga.create().copy(
|
||||
url = url,
|
||||
title = title,
|
||||
@ -244,6 +82,7 @@ fun SManga.toDomainManga(): Manga {
|
||||
thumbnailUrl = thumbnail_url,
|
||||
updateStrategy = update_strategy,
|
||||
initialized = initialized,
|
||||
source = sourceId,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -1,12 +0,0 @@
|
||||
package eu.kanade.domain.manga.model
|
||||
|
||||
/**
|
||||
* Contains the required data for MangaCoverFetcher
|
||||
*/
|
||||
data class MangaCover(
|
||||
val mangaId: Long,
|
||||
val sourceId: Long,
|
||||
val isMangaFavorite: Boolean,
|
||||
val url: String?,
|
||||
val lastModified: Long,
|
||||
)
|
@ -1,14 +1,14 @@
|
||||
package eu.kanade.domain.source.interactor
|
||||
|
||||
import eu.kanade.domain.source.model.Pin
|
||||
import eu.kanade.domain.source.model.Pins
|
||||
import eu.kanade.domain.source.model.Source
|
||||
import eu.kanade.domain.source.repository.SourceRepository
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import eu.kanade.tachiyomi.source.LocalSource
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import tachiyomi.domain.source.model.Pin
|
||||
import tachiyomi.domain.source.model.Pins
|
||||
import tachiyomi.domain.source.model.Source
|
||||
|
||||
class GetEnabledSources(
|
||||
private val repository: SourceRepository,
|
||||
@ -23,7 +23,6 @@ class GetEnabledSources(
|
||||
preferences.lastUsedSource().changes(),
|
||||
repository.getSources(),
|
||||
) { pinnedSourceIds, enabledLanguages, disabledSources, lastUsedSource, sources ->
|
||||
val duplicatePins = preferences.duplicatePinnedSources().get()
|
||||
sources
|
||||
.filter { it.lang in enabledLanguages || it.id == LocalSource.ID }
|
||||
.filterNot { it.id.toString() in disabledSources }
|
||||
@ -35,10 +34,6 @@ class GetEnabledSources(
|
||||
if (source.id == lastUsedSource) {
|
||||
toFlatten.add(source.copy(isUsedLast = true, pin = source.pin - Pin.Actual))
|
||||
}
|
||||
if (duplicatePins && Pin.Pinned in source.pin) {
|
||||
toFlatten[0] = toFlatten[0].copy(pin = source.pin + Pin.Forced)
|
||||
toFlatten.add(source.copy(pin = source.pin - Pin.Actual))
|
||||
}
|
||||
toFlatten
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,11 @@
|
||||
package eu.kanade.domain.source.interactor
|
||||
|
||||
import eu.kanade.domain.source.model.Source
|
||||
import eu.kanade.domain.source.repository.SourceRepository
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import tachiyomi.domain.source.model.Source
|
||||
|
||||
class GetLanguagesWithSources(
|
||||
private val repository: SourceRepository,
|
||||
@ -25,10 +25,7 @@ class GetLanguagesWithSources(
|
||||
|
||||
sortedSources.groupBy { it.lang }
|
||||
.toSortedMap(
|
||||
compareBy(
|
||||
{ it !in enabledLanguage },
|
||||
{ LocaleHelper.getDisplayName(it) },
|
||||
),
|
||||
compareBy<String> { it !in enabledLanguage }.then(LocaleHelper.comparator),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,10 @@
|
||||
package eu.kanade.domain.source.interactor
|
||||
|
||||
import eu.kanade.domain.source.model.Source
|
||||
import eu.kanade.domain.source.repository.SourceRepository
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import tachiyomi.domain.source.model.Source
|
||||
import java.text.Collator
|
||||
import java.util.Collections
|
||||
import java.util.Locale
|
||||
|
@ -1,8 +1,8 @@
|
||||
package eu.kanade.domain.source.interactor
|
||||
|
||||
import eu.kanade.domain.source.model.SourceWithCount
|
||||
import eu.kanade.domain.source.repository.SourceRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import tachiyomi.domain.source.model.SourceWithCount
|
||||
|
||||
class GetSourcesWithNonLibraryManga(
|
||||
private val repository: SourceRepository,
|
||||
|
@ -1,7 +1,7 @@
|
||||
package eu.kanade.domain.source.interactor
|
||||
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import eu.kanade.tachiyomi.core.preference.getAndSet
|
||||
import tachiyomi.core.preference.getAndSet
|
||||
|
||||
class ToggleLanguage(
|
||||
val preferences: SourcePreferences,
|
||||
|
@ -1,8 +1,8 @@
|
||||
package eu.kanade.domain.source.interactor
|
||||
|
||||
import eu.kanade.domain.source.model.Source
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import eu.kanade.tachiyomi.core.preference.getAndSet
|
||||
import tachiyomi.core.preference.getAndSet
|
||||
import tachiyomi.domain.source.model.Source
|
||||
|
||||
class ToggleSource(
|
||||
private val preferences: SourcePreferences,
|
||||
@ -18,6 +18,13 @@ class ToggleSource(
|
||||
}
|
||||
}
|
||||
|
||||
fun await(sourceIds: List<Long>, enable: Boolean) {
|
||||
val transformedSourceIds = sourceIds.map { it.toString() }
|
||||
preferences.disabledSources().getAndSet { disabled ->
|
||||
if (enable) disabled.minus(transformedSourceIds) else disabled.plus(transformedSourceIds)
|
||||
}
|
||||
}
|
||||
|
||||
private fun isEnabled(sourceId: Long): Boolean {
|
||||
return sourceId.toString() in preferences.disabledSources().get()
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
package eu.kanade.domain.source.interactor
|
||||
|
||||
import eu.kanade.domain.source.model.Source
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import eu.kanade.tachiyomi.core.preference.getAndSet
|
||||
import tachiyomi.core.preference.getAndSet
|
||||
import tachiyomi.domain.source.model.Source
|
||||
|
||||
class ToggleSourcePin(
|
||||
private val preferences: SourcePreferences,
|
||||
|
@ -4,79 +4,13 @@ import androidx.compose.ui.graphics.ImageBitmap
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||
import tachiyomi.domain.source.model.Source
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
data class Source(
|
||||
val id: Long,
|
||||
val lang: String,
|
||||
val name: String,
|
||||
val supportsLatest: Boolean,
|
||||
val isStub: Boolean,
|
||||
val pin: Pins = Pins.unpinned,
|
||||
val isUsedLast: Boolean = false,
|
||||
) {
|
||||
|
||||
val visualName: String
|
||||
get() = when {
|
||||
lang.isEmpty() -> name
|
||||
else -> "$name (${lang.uppercase()})"
|
||||
}
|
||||
|
||||
val icon: ImageBitmap?
|
||||
get() {
|
||||
return Injekt.get<ExtensionManager>().getAppIconForSource(id)
|
||||
?.toBitmap()
|
||||
?.asImageBitmap()
|
||||
}
|
||||
|
||||
val key: () -> String = {
|
||||
when {
|
||||
isUsedLast -> "$id-lastused"
|
||||
Pin.Forced in pin -> "$id-forced"
|
||||
else -> "$id"
|
||||
}
|
||||
val Source.icon: ImageBitmap?
|
||||
get() {
|
||||
return Injekt.get<ExtensionManager>().getAppIconForSource(id)
|
||||
?.toBitmap()
|
||||
?.asImageBitmap()
|
||||
}
|
||||
}
|
||||
|
||||
sealed class Pin(val code: Int) {
|
||||
object Unpinned : Pin(0b00)
|
||||
object Pinned : Pin(0b01)
|
||||
object Actual : Pin(0b10)
|
||||
object Forced : Pin(0b100)
|
||||
}
|
||||
|
||||
inline fun Pins(builder: Pins.PinsBuilder.() -> Unit = {}): Pins {
|
||||
return Pins.PinsBuilder().apply(builder).flags()
|
||||
}
|
||||
|
||||
fun Pins(vararg pins: Pin) = Pins {
|
||||
pins.forEach { +it }
|
||||
}
|
||||
|
||||
data class Pins(val code: Int = Pin.Unpinned.code) {
|
||||
|
||||
operator fun contains(pin: Pin): Boolean = pin.code and code == pin.code
|
||||
|
||||
operator fun plus(pin: Pin): Pins = Pins(code or pin.code)
|
||||
|
||||
operator fun minus(pin: Pin): Pins = Pins(code xor pin.code)
|
||||
|
||||
companion object {
|
||||
val unpinned = Pins(Pin.Unpinned)
|
||||
|
||||
val pinned = Pins(Pin.Pinned, Pin.Actual)
|
||||
}
|
||||
|
||||
class PinsBuilder(var code: Int = 0) {
|
||||
operator fun Pin.unaryPlus() {
|
||||
this@PinsBuilder.code = code or this@PinsBuilder.code
|
||||
}
|
||||
|
||||
operator fun Pin.unaryMinus() {
|
||||
this@PinsBuilder.code = code or this@PinsBuilder.code
|
||||
}
|
||||
|
||||
fun flags(): Pins = Pins(code)
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,10 @@
|
||||
package eu.kanade.domain.source.repository
|
||||
|
||||
import eu.kanade.domain.source.model.Source
|
||||
import eu.kanade.domain.source.model.SourcePagingSourceType
|
||||
import eu.kanade.domain.source.model.SourceWithCount
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import tachiyomi.domain.source.model.Source
|
||||
import tachiyomi.domain.source.model.SourceWithCount
|
||||
|
||||
interface SourceRepository {
|
||||
|
||||
|
@ -1,10 +1,10 @@
|
||||
package eu.kanade.domain.source.service
|
||||
|
||||
import eu.kanade.domain.library.model.LibraryDisplayMode
|
||||
import eu.kanade.domain.source.interactor.SetMigrateSorting
|
||||
import eu.kanade.tachiyomi.core.preference.PreferenceStore
|
||||
import eu.kanade.tachiyomi.core.preference.getEnum
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
import tachiyomi.core.preference.PreferenceStore
|
||||
import tachiyomi.core.preference.getEnum
|
||||
import tachiyomi.domain.library.model.LibraryDisplayMode
|
||||
|
||||
class SourcePreferences(
|
||||
private val preferenceStore: PreferenceStore,
|
||||
@ -18,8 +18,6 @@ class SourcePreferences(
|
||||
|
||||
fun pinnedSources() = preferenceStore.getStringSet("pinned_catalogues", emptySet())
|
||||
|
||||
fun duplicatePinnedSources() = preferenceStore.getBoolean("duplicate_pinned_sources", false)
|
||||
|
||||
fun lastUsedSource() = preferenceStore.getLong("last_catalogue_source", -1)
|
||||
|
||||
fun showNsfwSource() = preferenceStore.getBoolean("show_nsfw_source", true)
|
||||
|
@ -1,8 +1,8 @@
|
||||
package eu.kanade.domain.track.interactor
|
||||
|
||||
import eu.kanade.domain.track.repository.TrackRepository
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import logcat.LogPriority
|
||||
import tachiyomi.core.util.system.logcat
|
||||
import tachiyomi.domain.track.repository.TrackRepository
|
||||
|
||||
class DeleteTrack(
|
||||
private val trackRepository: TrackRepository,
|
||||
|
@ -1,15 +1,24 @@
|
||||
package eu.kanade.domain.track.interactor
|
||||
|
||||
import eu.kanade.domain.track.model.Track
|
||||
import eu.kanade.domain.track.repository.TrackRepository
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import logcat.LogPriority
|
||||
import tachiyomi.core.util.system.logcat
|
||||
import tachiyomi.domain.track.model.Track
|
||||
import tachiyomi.domain.track.repository.TrackRepository
|
||||
|
||||
class GetTracks(
|
||||
private val trackRepository: TrackRepository,
|
||||
) {
|
||||
|
||||
suspend fun awaitOne(id: Long): Track? {
|
||||
return try {
|
||||
trackRepository.getTrackById(id)
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR, e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun await(mangaId: Long): List<Track> {
|
||||
return try {
|
||||
trackRepository.getTracksByMangaId(mangaId)
|
||||
|
@ -1,8 +1,8 @@
|
||||
package eu.kanade.domain.track.interactor
|
||||
|
||||
import eu.kanade.domain.track.repository.TrackRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import tachiyomi.domain.track.repository.TrackRepository
|
||||
|
||||
class GetTracksPerManga(
|
||||
private val trackRepository: TrackRepository,
|
||||
|
@ -1,9 +1,9 @@
|
||||
package eu.kanade.domain.track.interactor
|
||||
|
||||
import eu.kanade.domain.track.model.Track
|
||||
import eu.kanade.domain.track.repository.TrackRepository
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import logcat.LogPriority
|
||||
import tachiyomi.core.util.system.logcat
|
||||
import tachiyomi.domain.track.model.Track
|
||||
import tachiyomi.domain.track.repository.TrackRepository
|
||||
|
||||
class InsertTrack(
|
||||
private val trackRepository: TrackRepository,
|
||||
|
@ -1,31 +1,16 @@
|
||||
package eu.kanade.domain.track.model
|
||||
|
||||
import tachiyomi.domain.track.model.Track
|
||||
import eu.kanade.tachiyomi.data.database.models.Track as DbTrack
|
||||
|
||||
data class Track(
|
||||
val id: Long,
|
||||
val mangaId: Long,
|
||||
val syncId: Long,
|
||||
val remoteId: Long,
|
||||
val libraryId: Long?,
|
||||
val title: String,
|
||||
val lastChapterRead: Double,
|
||||
val totalChapters: Long,
|
||||
val status: Long,
|
||||
val score: Float,
|
||||
val remoteUrl: String,
|
||||
val startDate: Long,
|
||||
val finishDate: Long,
|
||||
) {
|
||||
fun copyPersonalFrom(other: Track): Track {
|
||||
return this.copy(
|
||||
lastChapterRead = other.lastChapterRead,
|
||||
score = other.score,
|
||||
status = other.status,
|
||||
startDate = other.startDate,
|
||||
finishDate = other.finishDate,
|
||||
)
|
||||
}
|
||||
fun Track.copyPersonalFrom(other: Track): Track {
|
||||
return this.copy(
|
||||
lastChapterRead = other.lastChapterRead,
|
||||
score = other.score,
|
||||
status = other.status,
|
||||
startDate = other.startDate,
|
||||
finishDate = other.finishDate,
|
||||
)
|
||||
}
|
||||
|
||||
fun Track.toDbTrack(): DbTrack = DbTrack.create(syncId).also {
|
||||
|
@ -1,4 +1,4 @@
|
||||
package eu.kanade.tachiyomi.data.track.job
|
||||
package eu.kanade.domain.track.service
|
||||
|
||||
import android.content.Context
|
||||
import androidx.work.BackoffPolicy
|
||||
@ -9,14 +9,14 @@ import androidx.work.NetworkType
|
||||
import androidx.work.OneTimeWorkRequestBuilder
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.WorkerParameters
|
||||
import eu.kanade.domain.manga.interactor.GetManga
|
||||
import eu.kanade.domain.track.interactor.GetTracks
|
||||
import eu.kanade.domain.track.interactor.InsertTrack
|
||||
import eu.kanade.domain.track.model.toDbTrack
|
||||
import eu.kanade.domain.track.store.DelayedTrackingStore
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import logcat.LogPriority
|
||||
import tachiyomi.core.util.lang.withIOContext
|
||||
import tachiyomi.core.util.system.logcat
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.util.concurrent.TimeUnit
|
||||
@ -25,7 +25,6 @@ class DelayedTrackingUpdateJob(context: Context, workerParams: WorkerParameters)
|
||||
CoroutineWorker(context, workerParams) {
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
val getManga = Injekt.get<GetManga>()
|
||||
val getTracks = Injekt.get<GetTracks>()
|
||||
val insertTrack = Injekt.get<InsertTrack>()
|
||||
|
||||
@ -34,10 +33,11 @@ class DelayedTrackingUpdateJob(context: Context, workerParams: WorkerParameters)
|
||||
|
||||
withIOContext {
|
||||
val tracks = delayedTrackingStore.getItems().mapNotNull {
|
||||
val manga = getManga.await(it.mangaId) ?: return@withIOContext
|
||||
getTracks.await(manga.id)
|
||||
.find { track -> track.id == it.trackId }
|
||||
?.copy(lastChapterRead = it.lastChapterRead.toDouble())
|
||||
val track = getTracks.awaitOne(it.trackId)
|
||||
if (track == null) {
|
||||
delayedTrackingStore.remove(it.trackId)
|
||||
}
|
||||
track
|
||||
}
|
||||
|
||||
tracks.forEach { track ->
|
||||
@ -47,7 +47,7 @@ class DelayedTrackingUpdateJob(context: Context, workerParams: WorkerParameters)
|
||||
service.update(track.toDbTrack(), true)
|
||||
insertTrack.await(track)
|
||||
}
|
||||
delayedTrackingStore.remove(track)
|
||||
delayedTrackingStore.remove(track.id)
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR, e)
|
||||
}
|
@ -1,8 +1,8 @@
|
||||
package eu.kanade.domain.track.service
|
||||
|
||||
import eu.kanade.tachiyomi.core.preference.PreferenceStore
|
||||
import eu.kanade.tachiyomi.data.track.TrackService
|
||||
import eu.kanade.tachiyomi.data.track.anilist.Anilist
|
||||
import tachiyomi.core.preference.PreferenceStore
|
||||
|
||||
class TrackPreferences(
|
||||
private val preferenceStore: PreferenceStore,
|
||||
|
@ -0,0 +1,46 @@
|
||||
package eu.kanade.domain.track.store
|
||||
|
||||
import android.content.Context
|
||||
import androidx.core.content.edit
|
||||
import logcat.LogPriority
|
||||
import tachiyomi.core.util.system.logcat
|
||||
import tachiyomi.domain.track.model.Track
|
||||
|
||||
class DelayedTrackingStore(context: Context) {
|
||||
|
||||
/**
|
||||
* Preference file where queued tracking updates are stored.
|
||||
*/
|
||||
private val preferences = context.getSharedPreferences("tracking_queue", Context.MODE_PRIVATE)
|
||||
|
||||
fun addItem(track: Track) {
|
||||
val trackId = track.id.toString()
|
||||
val lastChapterRead = preferences.getFloat(trackId, 0f)
|
||||
if (track.lastChapterRead > lastChapterRead) {
|
||||
logcat(LogPriority.DEBUG) { "Queuing track item: $trackId, last chapter read: ${track.lastChapterRead}" }
|
||||
preferences.edit {
|
||||
putFloat(trackId, track.lastChapterRead.toFloat())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun remove(trackId: Long) {
|
||||
preferences.edit {
|
||||
remove(trackId.toString())
|
||||
}
|
||||
}
|
||||
|
||||
fun getItems(): List<DelayedTrackingItem> {
|
||||
return preferences.all.mapNotNull {
|
||||
DelayedTrackingItem(
|
||||
trackId = it.key.toLong(),
|
||||
lastChapterRead = it.value.toString().toFloat(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class DelayedTrackingItem(
|
||||
val trackId: Long,
|
||||
val lastChapterRead: Float,
|
||||
)
|
||||
}
|
@ -4,10 +4,10 @@ import android.os.Build
|
||||
import eu.kanade.domain.ui.model.AppTheme
|
||||
import eu.kanade.domain.ui.model.TabletUiMode
|
||||
import eu.kanade.domain.ui.model.ThemeMode
|
||||
import eu.kanade.tachiyomi.core.preference.PreferenceStore
|
||||
import eu.kanade.tachiyomi.core.preference.getEnum
|
||||
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
||||
import eu.kanade.tachiyomi.util.system.isDynamicColorAvailable
|
||||
import tachiyomi.core.preference.PreferenceStore
|
||||
import tachiyomi.core.preference.getEnum
|
||||
import java.text.DateFormat
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
@ -1,24 +0,0 @@
|
||||
package eu.kanade.domain.updates.interactor
|
||||
|
||||
import eu.kanade.domain.library.service.LibraryPreferences
|
||||
import eu.kanade.domain.updates.model.UpdatesWithRelations
|
||||
import eu.kanade.domain.updates.repository.UpdatesRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import java.util.Calendar
|
||||
|
||||
class GetUpdates(
|
||||
private val repository: UpdatesRepository,
|
||||
private val preferences: LibraryPreferences,
|
||||
) {
|
||||
|
||||
fun subscribe(calendar: Calendar): Flow<List<UpdatesWithRelations>> = subscribe(calendar.time.time)
|
||||
|
||||
fun subscribe(after: Long): Flow<List<UpdatesWithRelations>> {
|
||||
return repository.subscribeAll(after)
|
||||
.onEach { updates ->
|
||||
// Set unread chapter count for bottom bar badge
|
||||
preferences.unreadUpdatesCount().set(updates.count { !it.read })
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
package eu.kanade.presentation.browse
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import eu.kanade.presentation.components.Badge
|
||||
import eu.kanade.tachiyomi.R
|
||||
|
||||
@Composable
|
||||
fun InLibraryBadge(enabled: Boolean) {
|
||||
if (enabled) {
|
||||
Badge(text = stringResource(R.string.in_library))
|
||||
}
|
||||
}
|
@ -1,213 +1,42 @@
|
||||
package eu.kanade.presentation.browse
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.horizontalScroll
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Favorite
|
||||
import androidx.compose.material.icons.outlined.FilterList
|
||||
import androidx.compose.material.icons.outlined.HelpOutline
|
||||
import androidx.compose.material.icons.outlined.NewReleases
|
||||
import androidx.compose.material.icons.outlined.Public
|
||||
import androidx.compose.material.icons.outlined.Refresh
|
||||
import androidx.compose.material3.FilterChip
|
||||
import androidx.compose.material3.FilterChipDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SnackbarDuration
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.SnackbarResult
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.paging.LoadState
|
||||
import androidx.paging.compose.LazyPagingItems
|
||||
import androidx.paging.compose.collectAsLazyPagingItems
|
||||
import eu.kanade.data.source.NoResultsException
|
||||
import eu.kanade.domain.library.model.LibraryDisplayMode
|
||||
import eu.kanade.domain.manga.model.Manga
|
||||
import eu.kanade.domain.source.interactor.GetRemoteManga
|
||||
import eu.kanade.presentation.browse.components.BrowseSourceComfortableGrid
|
||||
import eu.kanade.presentation.browse.components.BrowseSourceCompactGrid
|
||||
import eu.kanade.presentation.browse.components.BrowseSourceList
|
||||
import eu.kanade.presentation.browse.components.BrowseSourceToolbar
|
||||
import eu.kanade.presentation.components.AppStateBanners
|
||||
import eu.kanade.presentation.components.Divider
|
||||
import eu.kanade.presentation.components.AppBar
|
||||
import eu.kanade.presentation.components.EmptyScreen
|
||||
import eu.kanade.presentation.components.EmptyScreenAction
|
||||
import eu.kanade.presentation.components.ExtendedFloatingActionButton
|
||||
import eu.kanade.presentation.components.LoadingScreen
|
||||
import eu.kanade.presentation.components.Scaffold
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.source.LocalSource
|
||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourcePresenter
|
||||
import eu.kanade.tachiyomi.ui.more.MoreController
|
||||
|
||||
@Composable
|
||||
fun BrowseSourceScreen(
|
||||
presenter: BrowseSourcePresenter,
|
||||
navigateUp: () -> Unit,
|
||||
openFilterSheet: () -> Unit,
|
||||
onMangaClick: (Manga) -> Unit,
|
||||
onMangaLongClick: (Manga) -> Unit,
|
||||
onWebViewClick: () -> Unit,
|
||||
incognitoMode: Boolean,
|
||||
downloadedOnlyMode: Boolean,
|
||||
) {
|
||||
val columns by presenter.getColumnsPreferenceForCurrentOrientation()
|
||||
|
||||
val mangaList = presenter.getMangaList().collectAsLazyPagingItems()
|
||||
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
|
||||
val uriHandler = LocalUriHandler.current
|
||||
|
||||
val onHelpClick = {
|
||||
uriHandler.openUri(LocalSource.HELP_URL)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
Column(modifier = Modifier.background(MaterialTheme.colorScheme.surface)) {
|
||||
BrowseSourceToolbar(
|
||||
state = presenter,
|
||||
source = presenter.source,
|
||||
displayMode = presenter.displayMode,
|
||||
onDisplayModeChange = { presenter.displayMode = it },
|
||||
navigateUp = navigateUp,
|
||||
onWebViewClick = onWebViewClick,
|
||||
onHelpClick = onHelpClick,
|
||||
onSearch = { presenter.search(it) },
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.horizontalScroll(rememberScrollState())
|
||||
.padding(horizontal = 8.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
FilterChip(
|
||||
selected = presenter.currentFilter == BrowseSourcePresenter.Filter.Popular,
|
||||
onClick = {
|
||||
presenter.reset()
|
||||
presenter.search(GetRemoteManga.QUERY_POPULAR)
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Favorite,
|
||||
contentDescription = "",
|
||||
modifier = Modifier
|
||||
.size(FilterChipDefaults.IconSize),
|
||||
)
|
||||
},
|
||||
label = {
|
||||
Text(text = stringResource(R.string.popular))
|
||||
},
|
||||
)
|
||||
if (presenter.source?.supportsLatest == true) {
|
||||
FilterChip(
|
||||
selected = presenter.currentFilter == BrowseSourcePresenter.Filter.Latest,
|
||||
onClick = {
|
||||
presenter.reset()
|
||||
presenter.search(GetRemoteManga.QUERY_LATEST)
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.NewReleases,
|
||||
contentDescription = "",
|
||||
modifier = Modifier
|
||||
.size(FilterChipDefaults.IconSize),
|
||||
)
|
||||
},
|
||||
label = {
|
||||
Text(text = stringResource(R.string.latest))
|
||||
},
|
||||
)
|
||||
}
|
||||
if (presenter.filters.isNotEmpty()) {
|
||||
FilterChip(
|
||||
selected = presenter.currentFilter is BrowseSourcePresenter.Filter.UserInput,
|
||||
onClick = openFilterSheet,
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.FilterList,
|
||||
contentDescription = "",
|
||||
modifier = Modifier
|
||||
.size(FilterChipDefaults.IconSize),
|
||||
)
|
||||
},
|
||||
label = {
|
||||
Text(text = stringResource(R.string.action_filter))
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Divider()
|
||||
|
||||
AppStateBanners(downloadedOnlyMode, incognitoMode)
|
||||
}
|
||||
},
|
||||
snackbarHost = {
|
||||
SnackbarHost(hostState = snackbarHostState)
|
||||
},
|
||||
) { paddingValues ->
|
||||
BrowseSourceContent(
|
||||
state = presenter,
|
||||
mangaList = mangaList,
|
||||
getMangaState = { presenter.getManga(it) },
|
||||
columns = columns,
|
||||
displayMode = presenter.displayMode,
|
||||
snackbarHostState = snackbarHostState,
|
||||
contentPadding = paddingValues,
|
||||
onWebViewClick = onWebViewClick,
|
||||
onHelpClick = { uriHandler.openUri(MoreController.URL_HELP) },
|
||||
onLocalSourceHelpClick = onHelpClick,
|
||||
onMangaClick = onMangaClick,
|
||||
onMangaLongClick = onMangaLongClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BrowseSourceFloatingActionButton(
|
||||
modifier: Modifier = Modifier.navigationBarsPadding(),
|
||||
isVisible: Boolean,
|
||||
onFabClick: () -> Unit,
|
||||
) {
|
||||
AnimatedVisibility(visible = isVisible) {
|
||||
ExtendedFloatingActionButton(
|
||||
modifier = modifier,
|
||||
text = { Text(text = stringResource(R.string.action_filter)) },
|
||||
icon = { Icon(Icons.Outlined.FilterList, contentDescription = "") },
|
||||
onClick = onFabClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import tachiyomi.domain.library.model.LibraryDisplayMode
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
|
||||
@Composable
|
||||
fun BrowseSourceContent(
|
||||
state: BrowseSourceState,
|
||||
mangaList: LazyPagingItems<Manga>,
|
||||
getMangaState: @Composable ((Manga) -> State<Manga>),
|
||||
source: Source?,
|
||||
mangaList: LazyPagingItems<StateFlow<Manga>>,
|
||||
columns: GridCells,
|
||||
displayMode: LibraryDisplayMode,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
@ -249,7 +78,7 @@ fun BrowseSourceContent(
|
||||
if (mangaList.itemCount <= 0 && errorState != null && errorState is LoadState.Error) {
|
||||
EmptyScreen(
|
||||
message = getErrorMessage(errorState),
|
||||
actions = if (state.source is LocalSource) {
|
||||
actions = if (source is LocalSource) {
|
||||
listOf(
|
||||
EmptyScreenAction(
|
||||
stringResId = R.string.local_source_help_guide,
|
||||
@ -290,7 +119,6 @@ fun BrowseSourceContent(
|
||||
LibraryDisplayMode.ComfortableGrid -> {
|
||||
BrowseSourceComfortableGrid(
|
||||
mangaList = mangaList,
|
||||
getMangaState = getMangaState,
|
||||
columns = columns,
|
||||
contentPadding = contentPadding,
|
||||
onMangaClick = onMangaClick,
|
||||
@ -300,16 +128,14 @@ fun BrowseSourceContent(
|
||||
LibraryDisplayMode.List -> {
|
||||
BrowseSourceList(
|
||||
mangaList = mangaList,
|
||||
getMangaState = getMangaState,
|
||||
contentPadding = contentPadding,
|
||||
onMangaClick = onMangaClick,
|
||||
onMangaLongClick = onMangaLongClick,
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
LibraryDisplayMode.CompactGrid, LibraryDisplayMode.CoverOnlyGrid -> {
|
||||
BrowseSourceCompactGrid(
|
||||
mangaList = mangaList,
|
||||
getMangaState = getMangaState,
|
||||
columns = columns,
|
||||
contentPadding = contentPadding,
|
||||
onMangaClick = onMangaClick,
|
||||
@ -318,3 +144,24 @@ fun BrowseSourceContent(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MissingSourceScreen(
|
||||
source: SourceManager.StubSource,
|
||||
navigateUp: () -> Unit,
|
||||
) {
|
||||
Scaffold(
|
||||
topBar = { scrollBehavior ->
|
||||
AppBar(
|
||||
title = source.name,
|
||||
navigateUp = navigateUp,
|
||||
scrollBehavior = scrollBehavior,
|
||||
)
|
||||
},
|
||||
) { paddingValues ->
|
||||
EmptyScreen(
|
||||
message = source.getSourceNotInstalledException().message!!,
|
||||
modifier = Modifier.padding(paddingValues),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -1,41 +0,0 @@
|
||||
package eu.kanade.presentation.browse
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourcePresenter
|
||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourcePresenter.Filter
|
||||
import eu.kanade.tachiyomi.ui.browse.source.browse.toItems
|
||||
|
||||
@Stable
|
||||
interface BrowseSourceState {
|
||||
val source: CatalogueSource?
|
||||
var searchQuery: String?
|
||||
val currentFilter: Filter
|
||||
val isUserQuery: Boolean
|
||||
val filters: FilterList
|
||||
val filterItems: List<IFlexible<*>>
|
||||
var dialog: BrowseSourcePresenter.Dialog?
|
||||
}
|
||||
|
||||
fun BrowseSourceState(initialQuery: String?): BrowseSourceState {
|
||||
return when (val filter = Filter.valueOf(initialQuery ?: "")) {
|
||||
Filter.Latest, Filter.Popular -> BrowseSourceStateImpl(initialCurrentFilter = filter)
|
||||
is Filter.UserInput -> BrowseSourceStateImpl(initialQuery = initialQuery, initialCurrentFilter = filter)
|
||||
}
|
||||
}
|
||||
|
||||
class BrowseSourceStateImpl(initialQuery: String? = null, initialCurrentFilter: Filter) : BrowseSourceState {
|
||||
override var source: CatalogueSource? by mutableStateOf(null)
|
||||
override var searchQuery: String? by mutableStateOf(initialQuery)
|
||||
override var currentFilter: Filter by mutableStateOf(initialCurrentFilter)
|
||||
override val isUserQuery: Boolean by derivedStateOf { currentFilter is Filter.UserInput && currentFilter.query.isNotEmpty() }
|
||||
override var filters: FilterList by mutableStateOf(FilterList())
|
||||
override val filterItems: List<IFlexible<*>> by derivedStateOf { filters.toItems() }
|
||||
override var dialog: BrowseSourcePresenter.Dialog? by mutableStateOf(null)
|
||||
}
|
@ -4,12 +4,9 @@ import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.provider.Settings
|
||||
import android.util.DisplayMetrics
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
@ -26,7 +23,6 @@ import androidx.compose.material.icons.outlined.History
|
||||
import androidx.compose.material.icons.outlined.Settings
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Divider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
@ -42,38 +38,43 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.domain.extension.interactor.ExtensionSourceItem
|
||||
import eu.kanade.presentation.browse.components.ExtensionIcon
|
||||
import eu.kanade.presentation.components.AppBar
|
||||
import eu.kanade.presentation.components.AppBarActions
|
||||
import eu.kanade.presentation.components.DIVIDER_ALPHA
|
||||
import eu.kanade.presentation.components.Divider
|
||||
import eu.kanade.presentation.components.EmptyScreen
|
||||
import eu.kanade.presentation.components.LoadingScreen
|
||||
import eu.kanade.presentation.components.PreferenceRow
|
||||
import eu.kanade.presentation.components.Scaffold
|
||||
import eu.kanade.presentation.components.ScrollbarLazyColumn
|
||||
import eu.kanade.presentation.util.horizontalPadding
|
||||
import eu.kanade.presentation.components.WarningBanner
|
||||
import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget
|
||||
import eu.kanade.presentation.more.settings.widget.TrailingWidgetBuffer
|
||||
import eu.kanade.presentation.util.padding
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.extension.model.Extension
|
||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||
import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionDetailsPresenter
|
||||
import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionSourceItem
|
||||
import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionDetailsState
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
|
||||
@Composable
|
||||
fun ExtensionDetailsScreen(
|
||||
navigateUp: () -> Unit,
|
||||
presenter: ExtensionDetailsPresenter,
|
||||
state: ExtensionDetailsState,
|
||||
onClickSourcePreferences: (sourceId: Long) -> Unit,
|
||||
onClickWhatsNew: () -> Unit,
|
||||
onClickReadme: () -> Unit,
|
||||
onClickEnableAll: () -> Unit,
|
||||
onClickDisableAll: () -> Unit,
|
||||
onClickClearCookies: () -> Unit,
|
||||
onClickUninstall: () -> Unit,
|
||||
onClickSource: (sourceId: Long) -> Unit,
|
||||
) {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
|
||||
Scaffold(
|
||||
topBar = { scrollBehavior ->
|
||||
AppBar(
|
||||
@ -82,19 +83,19 @@ fun ExtensionDetailsScreen(
|
||||
actions = {
|
||||
AppBarActions(
|
||||
actions = buildList {
|
||||
if (presenter.extension?.isUnofficial == false) {
|
||||
if (state.extension?.isUnofficial == false) {
|
||||
add(
|
||||
AppBar.Action(
|
||||
title = stringResource(R.string.whats_new),
|
||||
icon = Icons.Outlined.History,
|
||||
onClick = { uriHandler.openUri(presenter.getChangelogUrl()) },
|
||||
onClick = onClickWhatsNew,
|
||||
),
|
||||
)
|
||||
add(
|
||||
AppBar.Action(
|
||||
title = stringResource(R.string.action_faq_and_guides),
|
||||
icon = Icons.Outlined.HelpOutline,
|
||||
onClick = { uriHandler.openUri(presenter.getReadmeUrl()) },
|
||||
onClick = onClickReadme,
|
||||
),
|
||||
)
|
||||
}
|
||||
@ -102,15 +103,15 @@ fun ExtensionDetailsScreen(
|
||||
listOf(
|
||||
AppBar.OverflowAction(
|
||||
title = stringResource(R.string.action_enable_all),
|
||||
onClick = { presenter.toggleSources(true) },
|
||||
onClick = onClickEnableAll,
|
||||
),
|
||||
AppBar.OverflowAction(
|
||||
title = stringResource(R.string.action_disable_all),
|
||||
onClick = { presenter.toggleSources(false) },
|
||||
onClick = onClickDisableAll,
|
||||
),
|
||||
AppBar.OverflowAction(
|
||||
title = stringResource(R.string.pref_clear_cookies),
|
||||
onClick = { presenter.clearCookies() },
|
||||
onClick = onClickClearCookies,
|
||||
),
|
||||
),
|
||||
)
|
||||
@ -121,93 +122,84 @@ fun ExtensionDetailsScreen(
|
||||
)
|
||||
},
|
||||
) { paddingValues ->
|
||||
ExtensionDetails(paddingValues, presenter, onClickSourcePreferences)
|
||||
if (state.extension == null) {
|
||||
EmptyScreen(
|
||||
textResource = R.string.empty_screen,
|
||||
modifier = Modifier.padding(paddingValues),
|
||||
)
|
||||
return@Scaffold
|
||||
}
|
||||
|
||||
ExtensionDetails(
|
||||
contentPadding = paddingValues,
|
||||
extension = state.extension,
|
||||
sources = state.sources,
|
||||
onClickSourcePreferences = onClickSourcePreferences,
|
||||
onClickUninstall = onClickUninstall,
|
||||
onClickSource = onClickSource,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ExtensionDetails(
|
||||
contentPadding: PaddingValues,
|
||||
presenter: ExtensionDetailsPresenter,
|
||||
extension: Extension.Installed,
|
||||
sources: List<ExtensionSourceItem>,
|
||||
onClickSourcePreferences: (sourceId: Long) -> Unit,
|
||||
onClickUninstall: () -> Unit,
|
||||
onClickSource: (sourceId: Long) -> Unit,
|
||||
) {
|
||||
when {
|
||||
presenter.isLoading -> LoadingScreen()
|
||||
presenter.extension == null -> EmptyScreen(
|
||||
textResource = R.string.empty_screen,
|
||||
modifier = Modifier.padding(contentPadding),
|
||||
)
|
||||
else -> {
|
||||
val context = LocalContext.current
|
||||
val extension = presenter.extension
|
||||
var showNsfwWarning by remember { mutableStateOf(false) }
|
||||
|
||||
ScrollbarLazyColumn(
|
||||
contentPadding = contentPadding,
|
||||
) {
|
||||
when {
|
||||
extension.isUnofficial ->
|
||||
item {
|
||||
WarningBanner(R.string.unofficial_extension_message)
|
||||
}
|
||||
extension.isObsolete ->
|
||||
item {
|
||||
WarningBanner(R.string.obsolete_extension_message)
|
||||
}
|
||||
}
|
||||
val context = LocalContext.current
|
||||
var showNsfwWarning by remember { mutableStateOf(false) }
|
||||
|
||||
ScrollbarLazyColumn(
|
||||
contentPadding = contentPadding,
|
||||
) {
|
||||
when {
|
||||
extension.isUnofficial ->
|
||||
item {
|
||||
DetailsHeader(
|
||||
extension = extension,
|
||||
onClickUninstall = { presenter.uninstallExtension() },
|
||||
onClickAppInfo = {
|
||||
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
|
||||
data = Uri.fromParts("package", extension.pkgName, null)
|
||||
context.startActivity(this)
|
||||
}
|
||||
},
|
||||
onClickAgeRating = {
|
||||
showNsfwWarning = true
|
||||
},
|
||||
)
|
||||
WarningBanner(R.string.unofficial_extension_message)
|
||||
}
|
||||
extension.isObsolete ->
|
||||
item {
|
||||
WarningBanner(R.string.obsolete_extension_message)
|
||||
}
|
||||
}
|
||||
|
||||
items(
|
||||
items = presenter.sources,
|
||||
key = { it.source.id },
|
||||
) { source ->
|
||||
SourceSwitchPreference(
|
||||
modifier = Modifier.animateItemPlacement(),
|
||||
source = source,
|
||||
onClickSourcePreferences = onClickSourcePreferences,
|
||||
onClickSource = { presenter.toggleSource(it) },
|
||||
)
|
||||
}
|
||||
}
|
||||
if (showNsfwWarning) {
|
||||
NsfwWarningDialog(
|
||||
onClickConfirm = {
|
||||
showNsfwWarning = false
|
||||
},
|
||||
)
|
||||
}
|
||||
item {
|
||||
DetailsHeader(
|
||||
extension = extension,
|
||||
onClickUninstall = onClickUninstall,
|
||||
onClickAppInfo = {
|
||||
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
|
||||
data = Uri.fromParts("package", extension.pkgName, null)
|
||||
context.startActivity(this)
|
||||
}
|
||||
},
|
||||
onClickAgeRating = {
|
||||
showNsfwWarning = true
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
items(
|
||||
items = sources,
|
||||
key = { it.source.id },
|
||||
) { source ->
|
||||
SourceSwitchPreference(
|
||||
modifier = Modifier.animateItemPlacement(),
|
||||
source = source,
|
||||
onClickSourcePreferences = onClickSourcePreferences,
|
||||
onClickSource = onClickSource,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WarningBanner(@StringRes textRes: Int) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(MaterialTheme.colorScheme.error)
|
||||
.padding(16.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(textRes),
|
||||
color = MaterialTheme.colorScheme.onError,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
if (showNsfwWarning) {
|
||||
NsfwWarningDialog(
|
||||
onClickConfirm = {
|
||||
showNsfwWarning = false
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -226,10 +218,10 @@ private fun DetailsHeader(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
start = horizontalPadding,
|
||||
end = horizontalPadding,
|
||||
top = 16.dp,
|
||||
bottom = 8.dp,
|
||||
start = MaterialTheme.padding.medium,
|
||||
end = MaterialTheme.padding.medium,
|
||||
top = MaterialTheme.padding.medium,
|
||||
bottom = MaterialTheme.padding.small,
|
||||
),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
@ -243,6 +235,7 @@ private fun DetailsHeader(
|
||||
Text(
|
||||
text = extension.name,
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
|
||||
val strippedPkgName = extension.pkgName.substringAfter("eu.kanade.tachiyomi.extension.")
|
||||
@ -257,8 +250,8 @@ private fun DetailsHeader(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
horizontal = horizontalPadding * 2,
|
||||
vertical = 8.dp,
|
||||
horizontal = MaterialTheme.padding.extraLarge,
|
||||
vertical = MaterialTheme.padding.small,
|
||||
),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
@ -295,10 +288,10 @@ private fun DetailsHeader(
|
||||
|
||||
Row(
|
||||
modifier = Modifier.padding(
|
||||
start = horizontalPadding,
|
||||
end = horizontalPadding,
|
||||
top = 8.dp,
|
||||
bottom = 16.dp,
|
||||
start = MaterialTheme.padding.medium,
|
||||
end = MaterialTheme.padding.medium,
|
||||
top = MaterialTheme.padding.small,
|
||||
bottom = MaterialTheme.padding.medium,
|
||||
),
|
||||
) {
|
||||
OutlinedButton(
|
||||
@ -380,15 +373,14 @@ private fun SourceSwitchPreference(
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
PreferenceRow(
|
||||
TextPreferenceWidget(
|
||||
modifier = modifier,
|
||||
title = if (source.labelAsName) {
|
||||
source.source.toString()
|
||||
} else {
|
||||
LocaleHelper.getSourceDisplayName(source.source.lang, context)
|
||||
},
|
||||
onClick = { onClickSource(source.source.id) },
|
||||
action = {
|
||||
widget = {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
@ -402,9 +394,14 @@ private fun SourceSwitchPreference(
|
||||
}
|
||||
}
|
||||
|
||||
Switch(checked = source.enabled, onCheckedChange = null)
|
||||
Switch(
|
||||
checked = source.enabled,
|
||||
onCheckedChange = null,
|
||||
modifier = Modifier.padding(start = TrailingWidgetBuffer),
|
||||
)
|
||||
}
|
||||
},
|
||||
onPreferenceClick = { onClickSource(source.source.id) },
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -1,25 +0,0 @@
|
||||
package eu.kanade.presentation.browse
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import eu.kanade.tachiyomi.extension.model.Extension
|
||||
import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionSourceItem
|
||||
|
||||
@Stable
|
||||
interface ExtensionDetailsState {
|
||||
val isLoading: Boolean
|
||||
val extension: Extension.Installed?
|
||||
val sources: List<ExtensionSourceItem>
|
||||
}
|
||||
|
||||
fun ExtensionDetailsState(): ExtensionDetailsState {
|
||||
return ExtensionDetailsStateImpl()
|
||||
}
|
||||
|
||||
class ExtensionDetailsStateImpl : ExtensionDetailsState {
|
||||
override var isLoading: Boolean by mutableStateOf(true)
|
||||
override var extension: Extension.Installed? by mutableStateOf(null)
|
||||
override var sources: List<ExtensionSourceItem> by mutableStateOf(emptyList())
|
||||
}
|
@ -3,30 +3,25 @@ package eu.kanade.presentation.browse
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import eu.kanade.presentation.components.AppBar
|
||||
import eu.kanade.presentation.components.EmptyScreen
|
||||
import eu.kanade.presentation.components.LazyColumn
|
||||
import eu.kanade.presentation.components.LoadingScreen
|
||||
import eu.kanade.presentation.components.PreferenceRow
|
||||
import eu.kanade.presentation.components.FastScrollLazyColumn
|
||||
import eu.kanade.presentation.components.Scaffold
|
||||
import eu.kanade.presentation.more.settings.widget.SwitchPreferenceWidget
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionFilterPresenter
|
||||
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionFilterState
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
|
||||
@Composable
|
||||
fun ExtensionFilterScreen(
|
||||
navigateUp: () -> Unit,
|
||||
presenter: ExtensionFilterPresenter,
|
||||
state: ExtensionFilterState.Success,
|
||||
onClickToggle: (String) -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
Scaffold(
|
||||
topBar = { scrollBehavior ->
|
||||
AppBar(
|
||||
@ -36,69 +31,38 @@ fun ExtensionFilterScreen(
|
||||
)
|
||||
},
|
||||
) { contentPadding ->
|
||||
when {
|
||||
presenter.isLoading -> LoadingScreen()
|
||||
presenter.isEmpty -> EmptyScreen(
|
||||
if (state.isEmpty) {
|
||||
EmptyScreen(
|
||||
textResource = R.string.empty_screen,
|
||||
modifier = Modifier.padding(contentPadding),
|
||||
)
|
||||
else -> {
|
||||
SourceFilterContent(
|
||||
contentPadding = contentPadding,
|
||||
state = presenter,
|
||||
onClickLang = {
|
||||
presenter.toggleLanguage(it)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
presenter.events.collectLatest {
|
||||
when (it) {
|
||||
ExtensionFilterPresenter.Event.FailedFetchingLanguages -> {
|
||||
context.toast(R.string.internal_error)
|
||||
}
|
||||
}
|
||||
return@Scaffold
|
||||
}
|
||||
ExtensionFilterContent(
|
||||
contentPadding = contentPadding,
|
||||
state = state,
|
||||
onClickLang = onClickToggle,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SourceFilterContent(
|
||||
private fun ExtensionFilterContent(
|
||||
contentPadding: PaddingValues,
|
||||
state: ExtensionFilterState,
|
||||
state: ExtensionFilterState.Success,
|
||||
onClickLang: (String) -> Unit,
|
||||
) {
|
||||
LazyColumn(
|
||||
val context = LocalContext.current
|
||||
FastScrollLazyColumn(
|
||||
contentPadding = contentPadding,
|
||||
) {
|
||||
items(
|
||||
items = state.items,
|
||||
) { model ->
|
||||
ExtensionFilterItem(
|
||||
items(state.languages) { language ->
|
||||
SwitchPreferenceWidget(
|
||||
modifier = Modifier.animateItemPlacement(),
|
||||
lang = model.lang,
|
||||
enabled = model.enabled,
|
||||
onClickItem = onClickLang,
|
||||
title = LocaleHelper.getSourceDisplayName(language, context),
|
||||
checked = language in state.enabledLanguages,
|
||||
onCheckedChanged = { onClickLang(language) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ExtensionFilterItem(
|
||||
modifier: Modifier,
|
||||
lang: String,
|
||||
enabled: Boolean,
|
||||
onClickItem: (String) -> Unit,
|
||||
) {
|
||||
PreferenceRow(
|
||||
modifier = modifier,
|
||||
title = LocaleHelper.getSourceDisplayName(lang, LocalContext.current),
|
||||
action = {
|
||||
Switch(checked = enabled, onCheckedChange = null)
|
||||
},
|
||||
onClick = { onClickItem(lang) },
|
||||
)
|
||||
}
|
||||
|
@ -1,25 +0,0 @@
|
||||
package eu.kanade.presentation.browse
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import eu.kanade.tachiyomi.ui.browse.extension.FilterUiModel
|
||||
|
||||
@Stable
|
||||
interface ExtensionFilterState {
|
||||
val isLoading: Boolean
|
||||
val items: List<FilterUiModel>
|
||||
val isEmpty: Boolean
|
||||
}
|
||||
|
||||
fun ExtensionFilterState(): ExtensionFilterState {
|
||||
return ExtensionFilterStateImpl()
|
||||
}
|
||||
|
||||
class ExtensionFilterStateImpl : ExtensionFilterState {
|
||||
override var isLoading: Boolean by mutableStateOf(true)
|
||||
override var items: List<FilterUiModel> by mutableStateOf(emptyList())
|
||||
override val isEmpty: Boolean by derivedStateOf { items.isEmpty() }
|
||||
}
|
@ -40,24 +40,25 @@ import eu.kanade.presentation.browse.components.ExtensionIcon
|
||||
import eu.kanade.presentation.components.EmptyScreen
|
||||
import eu.kanade.presentation.components.FastScrollLazyColumn
|
||||
import eu.kanade.presentation.components.LoadingScreen
|
||||
import eu.kanade.presentation.components.SwipeRefresh
|
||||
import eu.kanade.presentation.components.PullRefresh
|
||||
import eu.kanade.presentation.manga.components.DotSeparatorNoSpaceText
|
||||
import eu.kanade.presentation.theme.header
|
||||
import eu.kanade.presentation.util.horizontalPadding
|
||||
import eu.kanade.presentation.util.padding
|
||||
import eu.kanade.presentation.util.plus
|
||||
import eu.kanade.presentation.util.secondaryItemAlpha
|
||||
import eu.kanade.presentation.util.topPaddingValues
|
||||
import eu.kanade.presentation.util.topSmallPaddingValues
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.extension.model.Extension
|
||||
import eu.kanade.tachiyomi.extension.model.InstallStep
|
||||
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionUiModel
|
||||
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionsPresenter
|
||||
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionsState
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
|
||||
@Composable
|
||||
fun ExtensionScreen(
|
||||
presenter: ExtensionsPresenter,
|
||||
state: ExtensionsState,
|
||||
contentPadding: PaddingValues,
|
||||
searchQuery: String?,
|
||||
onLongClickItem: (Extension) -> Unit,
|
||||
onClickItemCancel: (Extension) -> Unit,
|
||||
onInstallExtension: (Extension.Available) -> Unit,
|
||||
@ -68,20 +69,27 @@ fun ExtensionScreen(
|
||||
onClickUpdateAll: () -> Unit,
|
||||
onRefresh: () -> Unit,
|
||||
) {
|
||||
SwipeRefresh(
|
||||
refreshing = presenter.isRefreshing,
|
||||
PullRefresh(
|
||||
refreshing = state.isRefreshing,
|
||||
onRefresh = onRefresh,
|
||||
enabled = !presenter.isLoading,
|
||||
enabled = !state.isLoading,
|
||||
) {
|
||||
when {
|
||||
presenter.isLoading -> LoadingScreen()
|
||||
presenter.isEmpty -> EmptyScreen(
|
||||
textResource = R.string.empty_screen,
|
||||
modifier = Modifier.padding(contentPadding),
|
||||
)
|
||||
state.isLoading -> LoadingScreen(modifier = Modifier.padding(contentPadding))
|
||||
state.isEmpty -> {
|
||||
val msg = if (!searchQuery.isNullOrEmpty()) {
|
||||
R.string.no_results_found
|
||||
} else {
|
||||
R.string.empty_screen
|
||||
}
|
||||
EmptyScreen(
|
||||
textResource = msg,
|
||||
modifier = Modifier.padding(contentPadding),
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
ExtensionContent(
|
||||
state = presenter,
|
||||
state = state,
|
||||
contentPadding = contentPadding,
|
||||
onLongClickItem = onLongClickItem,
|
||||
onClickItemCancel = onClickItemCancel,
|
||||
@ -113,81 +121,77 @@ private fun ExtensionContent(
|
||||
var trustState by remember { mutableStateOf<Extension.Untrusted?>(null) }
|
||||
|
||||
FastScrollLazyColumn(
|
||||
contentPadding = contentPadding + topPaddingValues,
|
||||
contentPadding = contentPadding + topSmallPaddingValues,
|
||||
) {
|
||||
items(
|
||||
items = state.items,
|
||||
contentType = {
|
||||
when (it) {
|
||||
is ExtensionUiModel.Header -> "header"
|
||||
is ExtensionUiModel.Item -> "item"
|
||||
}
|
||||
},
|
||||
key = {
|
||||
when (it) {
|
||||
is ExtensionUiModel.Header -> "extensionHeader-${it.hashCode()}"
|
||||
is ExtensionUiModel.Item -> "extension-${it.hashCode()}"
|
||||
}
|
||||
},
|
||||
) { item ->
|
||||
when (item) {
|
||||
is ExtensionUiModel.Header.Resource -> {
|
||||
val action: @Composable RowScope.() -> Unit =
|
||||
if (item.textRes == R.string.ext_updates_pending) {
|
||||
{
|
||||
Button(onClick = { onClickUpdateAll() }) {
|
||||
Text(
|
||||
text = stringResource(R.string.ext_update_all),
|
||||
style = LocalTextStyle.current.copy(
|
||||
color = MaterialTheme.colorScheme.onPrimary,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
{}
|
||||
}
|
||||
ExtensionHeader(
|
||||
textRes = item.textRes,
|
||||
modifier = Modifier.animateItemPlacement(),
|
||||
action = action,
|
||||
)
|
||||
}
|
||||
is ExtensionUiModel.Header.Text -> {
|
||||
ExtensionHeader(
|
||||
text = item.text,
|
||||
modifier = Modifier.animateItemPlacement(),
|
||||
)
|
||||
}
|
||||
is ExtensionUiModel.Item -> {
|
||||
ExtensionItem(
|
||||
modifier = Modifier.animateItemPlacement(),
|
||||
item = item,
|
||||
onClickItem = {
|
||||
when (it) {
|
||||
is Extension.Available -> onInstallExtension(it)
|
||||
is Extension.Installed -> onOpenExtension(it)
|
||||
is Extension.Untrusted -> { trustState = it }
|
||||
}
|
||||
},
|
||||
onLongClickItem = onLongClickItem,
|
||||
onClickItemCancel = onClickItemCancel,
|
||||
onClickItemAction = {
|
||||
when (it) {
|
||||
is Extension.Available -> onInstallExtension(it)
|
||||
is Extension.Installed -> {
|
||||
if (it.hasUpdate) {
|
||||
onUpdateExtension(it)
|
||||
} else {
|
||||
onOpenExtension(it)
|
||||
state.items.forEach { (header, items) ->
|
||||
item(
|
||||
contentType = "header",
|
||||
key = "extensionHeader-${header.hashCode()}",
|
||||
) {
|
||||
when (header) {
|
||||
is ExtensionUiModel.Header.Resource -> {
|
||||
val action: @Composable RowScope.() -> Unit =
|
||||
if (header.textRes == R.string.ext_updates_pending) {
|
||||
{
|
||||
Button(onClick = { onClickUpdateAll() }) {
|
||||
Text(
|
||||
text = stringResource(R.string.ext_update_all),
|
||||
style = LocalTextStyle.current.copy(
|
||||
color = MaterialTheme.colorScheme.onPrimary,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
is Extension.Untrusted -> { trustState = it }
|
||||
} else {
|
||||
{}
|
||||
}
|
||||
},
|
||||
)
|
||||
ExtensionHeader(
|
||||
textRes = header.textRes,
|
||||
modifier = Modifier.animateItemPlacement(),
|
||||
action = action,
|
||||
)
|
||||
}
|
||||
is ExtensionUiModel.Header.Text -> {
|
||||
ExtensionHeader(
|
||||
text = header.text,
|
||||
modifier = Modifier.animateItemPlacement(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
items(
|
||||
items = items,
|
||||
contentType = { "item" },
|
||||
key = { "extension-${it.hashCode()}" },
|
||||
) { item ->
|
||||
ExtensionItem(
|
||||
modifier = Modifier.animateItemPlacement(),
|
||||
item = item,
|
||||
onClickItem = {
|
||||
when (it) {
|
||||
is Extension.Available -> onInstallExtension(it)
|
||||
is Extension.Installed -> onOpenExtension(it)
|
||||
is Extension.Untrusted -> { trustState = it }
|
||||
}
|
||||
},
|
||||
onLongClickItem = onLongClickItem,
|
||||
onClickItemCancel = onClickItemCancel,
|
||||
onClickItemAction = {
|
||||
when (it) {
|
||||
is Extension.Available -> onInstallExtension(it)
|
||||
is Extension.Installed -> {
|
||||
if (it.hasUpdate) {
|
||||
onUpdateExtension(it)
|
||||
} else {
|
||||
onOpenExtension(it)
|
||||
}
|
||||
}
|
||||
is Extension.Untrusted -> { trustState = it }
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (trustState != null) {
|
||||
@ -272,7 +276,7 @@ private fun ExtensionItemContent(
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier.padding(start = horizontalPadding),
|
||||
modifier = modifier.padding(start = MaterialTheme.padding.medium),
|
||||
) {
|
||||
Text(
|
||||
text = extension.name,
|
||||
@ -396,7 +400,7 @@ private fun ExtensionHeader(
|
||||
action: @Composable RowScope.() -> Unit = {},
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier.padding(horizontal = horizontalPadding),
|
||||
modifier = modifier.padding(horizontal = MaterialTheme.padding.medium),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
|
@ -1,27 +0,0 @@
|
||||
package eu.kanade.presentation.browse
|
||||
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionUiModel
|
||||
|
||||
interface ExtensionsState {
|
||||
val isLoading: Boolean
|
||||
val isRefreshing: Boolean
|
||||
val items: List<ExtensionUiModel>
|
||||
val updates: Int
|
||||
val isEmpty: Boolean
|
||||
}
|
||||
|
||||
fun ExtensionState(): ExtensionsState {
|
||||
return ExtensionsStateImpl()
|
||||
}
|
||||
|
||||
class ExtensionsStateImpl : ExtensionsState {
|
||||
override var isLoading: Boolean by mutableStateOf(true)
|
||||
override var isRefreshing: Boolean by mutableStateOf(false)
|
||||
override var items: List<ExtensionUiModel> by mutableStateOf(emptyList())
|
||||
override var updates: Int by mutableStateOf(0)
|
||||
override val isEmpty: Boolean by derivedStateOf { items.isEmpty() }
|
||||
}
|
@ -0,0 +1,112 @@
|
||||
package eu.kanade.presentation.browse
|
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import eu.kanade.presentation.browse.components.GlobalSearchCardRow
|
||||
import eu.kanade.presentation.browse.components.GlobalSearchErrorResultItem
|
||||
import eu.kanade.presentation.browse.components.GlobalSearchLoadingResultItem
|
||||
import eu.kanade.presentation.browse.components.GlobalSearchResultItem
|
||||
import eu.kanade.presentation.browse.components.GlobalSearchToolbar
|
||||
import eu.kanade.presentation.components.LazyColumn
|
||||
import eu.kanade.presentation.components.Scaffold
|
||||
import eu.kanade.presentation.util.padding
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchState
|
||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchItemResult
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
|
||||
@Composable
|
||||
fun GlobalSearchScreen(
|
||||
state: GlobalSearchState,
|
||||
navigateUp: () -> Unit,
|
||||
onChangeSearchQuery: (String?) -> Unit,
|
||||
onSearch: (String) -> Unit,
|
||||
getManga: @Composable (CatalogueSource, Manga) -> State<Manga>,
|
||||
onClickSource: (CatalogueSource) -> Unit,
|
||||
onClickItem: (Manga) -> Unit,
|
||||
onLongClickItem: (Manga) -> Unit,
|
||||
) {
|
||||
Scaffold(
|
||||
topBar = { scrollBehavior ->
|
||||
GlobalSearchToolbar(
|
||||
searchQuery = state.searchQuery,
|
||||
progress = state.progress,
|
||||
total = state.total,
|
||||
navigateUp = navigateUp,
|
||||
onChangeSearchQuery = onChangeSearchQuery,
|
||||
onSearch = onSearch,
|
||||
scrollBehavior = scrollBehavior,
|
||||
)
|
||||
},
|
||||
) { paddingValues ->
|
||||
GlobalSearchContent(
|
||||
items = state.items,
|
||||
contentPadding = paddingValues,
|
||||
getManga = getManga,
|
||||
onClickSource = onClickSource,
|
||||
onClickItem = onClickItem,
|
||||
onLongClickItem = onLongClickItem,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun GlobalSearchContent(
|
||||
items: Map<CatalogueSource, SearchItemResult>,
|
||||
contentPadding: PaddingValues,
|
||||
getManga: @Composable (CatalogueSource, Manga) -> State<Manga>,
|
||||
onClickSource: (CatalogueSource) -> Unit,
|
||||
onClickItem: (Manga) -> Unit,
|
||||
onLongClickItem: (Manga) -> Unit,
|
||||
) {
|
||||
LazyColumn(
|
||||
contentPadding = contentPadding,
|
||||
) {
|
||||
items.forEach { (source, result) ->
|
||||
item(key = source.id) {
|
||||
GlobalSearchResultItem(
|
||||
title = source.name,
|
||||
subtitle = LocaleHelper.getDisplayName(source.lang),
|
||||
onClick = { onClickSource(source) },
|
||||
) {
|
||||
when (result) {
|
||||
SearchItemResult.Loading -> {
|
||||
GlobalSearchLoadingResultItem()
|
||||
}
|
||||
is SearchItemResult.Success -> {
|
||||
if (result.isEmpty) {
|
||||
Text(
|
||||
text = stringResource(R.string.no_results_found),
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
horizontal = MaterialTheme.padding.medium,
|
||||
vertical = MaterialTheme.padding.small,
|
||||
),
|
||||
)
|
||||
return@GlobalSearchResultItem
|
||||
}
|
||||
|
||||
GlobalSearchCardRow(
|
||||
titles = result.result,
|
||||
getManga = { getManga(source, it) },
|
||||
onClick = onClickItem,
|
||||
onLongClick = onLongClickItem,
|
||||
)
|
||||
}
|
||||
is SearchItemResult.Error -> {
|
||||
GlobalSearchErrorResultItem(message = result.throwable.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -4,31 +4,24 @@ import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import eu.kanade.domain.manga.model.Manga
|
||||
import eu.kanade.presentation.components.AppBar
|
||||
import eu.kanade.presentation.components.EmptyScreen
|
||||
import eu.kanade.presentation.components.LoadingScreen
|
||||
import eu.kanade.presentation.components.FastScrollLazyColumn
|
||||
import eu.kanade.presentation.components.Scaffold
|
||||
import eu.kanade.presentation.components.ScrollbarLazyColumn
|
||||
import eu.kanade.presentation.manga.components.BaseMangaListItem
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.ui.browse.migration.manga.MigrateMangaPresenter
|
||||
import eu.kanade.tachiyomi.ui.browse.migration.manga.MigrateMangaPresenter.Event
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import eu.kanade.tachiyomi.ui.browse.migration.manga.MigrateMangaState
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
|
||||
@Composable
|
||||
fun MigrateMangaScreen(
|
||||
navigateUp: () -> Unit,
|
||||
title: String?,
|
||||
presenter: MigrateMangaPresenter,
|
||||
state: MigrateMangaState,
|
||||
onClickItem: (Manga) -> Unit,
|
||||
onClickCover: (Manga) -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
Scaffold(
|
||||
topBar = { scrollBehavior ->
|
||||
AppBar(
|
||||
@ -38,30 +31,20 @@ fun MigrateMangaScreen(
|
||||
)
|
||||
},
|
||||
) { contentPadding ->
|
||||
when {
|
||||
presenter.isLoading -> LoadingScreen()
|
||||
presenter.isEmpty -> EmptyScreen(
|
||||
if (state.isEmpty) {
|
||||
EmptyScreen(
|
||||
textResource = R.string.empty_screen,
|
||||
modifier = Modifier.padding(contentPadding),
|
||||
)
|
||||
else -> {
|
||||
MigrateMangaContent(
|
||||
contentPadding = contentPadding,
|
||||
state = presenter,
|
||||
onClickItem = onClickItem,
|
||||
onClickCover = onClickCover,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
presenter.events.collectLatest { event ->
|
||||
when (event) {
|
||||
Event.FailedFetchingFavorites -> {
|
||||
context.toast(R.string.internal_error)
|
||||
}
|
||||
}
|
||||
return@Scaffold
|
||||
}
|
||||
|
||||
MigrateMangaContent(
|
||||
contentPadding = contentPadding,
|
||||
state = state,
|
||||
onClickItem = onClickItem,
|
||||
onClickCover = onClickCover,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -72,10 +55,10 @@ private fun MigrateMangaContent(
|
||||
onClickItem: (Manga) -> Unit,
|
||||
onClickCover: (Manga) -> Unit,
|
||||
) {
|
||||
ScrollbarLazyColumn(
|
||||
FastScrollLazyColumn(
|
||||
contentPadding = contentPadding,
|
||||
) {
|
||||
items(state.items) { manga ->
|
||||
items(state.titles) { manga ->
|
||||
MigrateMangaItem(
|
||||
manga = manga,
|
||||
onClickItem = onClickItem,
|
||||
|
@ -1,23 +0,0 @@
|
||||
package eu.kanade.presentation.browse
|
||||
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import eu.kanade.domain.manga.model.Manga
|
||||
|
||||
interface MigrateMangaState {
|
||||
val isLoading: Boolean
|
||||
val items: List<Manga>
|
||||
val isEmpty: Boolean
|
||||
}
|
||||
|
||||
fun MigrationMangaState(): MigrateMangaState {
|
||||
return MigrateMangaStateImpl()
|
||||
}
|
||||
|
||||
class MigrateMangaStateImpl : MigrateMangaState {
|
||||
override var isLoading: Boolean by mutableStateOf(true)
|
||||
override var items: List<Manga> by mutableStateOf(emptyList())
|
||||
override val isEmpty: Boolean by derivedStateOf { items.isEmpty() }
|
||||
}
|
@ -0,0 +1,101 @@
|
||||
package eu.kanade.presentation.browse
|
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import eu.kanade.presentation.browse.components.GlobalSearchCardRow
|
||||
import eu.kanade.presentation.browse.components.GlobalSearchEmptyResultItem
|
||||
import eu.kanade.presentation.browse.components.GlobalSearchErrorResultItem
|
||||
import eu.kanade.presentation.browse.components.GlobalSearchLoadingResultItem
|
||||
import eu.kanade.presentation.browse.components.GlobalSearchResultItem
|
||||
import eu.kanade.presentation.browse.components.GlobalSearchToolbar
|
||||
import eu.kanade.presentation.components.LazyColumn
|
||||
import eu.kanade.presentation.components.Scaffold
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.ui.browse.migration.search.MigrateSearchState
|
||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchItemResult
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
|
||||
@Composable
|
||||
fun MigrateSearchScreen(
|
||||
navigateUp: () -> Unit,
|
||||
state: MigrateSearchState,
|
||||
getManga: @Composable (CatalogueSource, Manga) -> State<Manga>,
|
||||
onChangeSearchQuery: (String?) -> Unit,
|
||||
onSearch: (String) -> Unit,
|
||||
onClickSource: (CatalogueSource) -> Unit,
|
||||
onClickItem: (Manga) -> Unit,
|
||||
onLongClickItem: (Manga) -> Unit,
|
||||
) {
|
||||
Scaffold(
|
||||
topBar = { scrollBehavior ->
|
||||
GlobalSearchToolbar(
|
||||
searchQuery = state.searchQuery,
|
||||
progress = state.progress,
|
||||
total = state.total,
|
||||
navigateUp = navigateUp,
|
||||
onChangeSearchQuery = onChangeSearchQuery,
|
||||
onSearch = onSearch,
|
||||
scrollBehavior = scrollBehavior,
|
||||
)
|
||||
},
|
||||
) { paddingValues ->
|
||||
MigrateSearchContent(
|
||||
sourceId = state.manga?.source ?: -1,
|
||||
items = state.items,
|
||||
contentPadding = paddingValues,
|
||||
getManga = getManga,
|
||||
onClickSource = onClickSource,
|
||||
onClickItem = onClickItem,
|
||||
onLongClickItem = onLongClickItem,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MigrateSearchContent(
|
||||
sourceId: Long,
|
||||
items: Map<CatalogueSource, SearchItemResult>,
|
||||
contentPadding: PaddingValues,
|
||||
getManga: @Composable (CatalogueSource, Manga) -> State<Manga>,
|
||||
onClickSource: (CatalogueSource) -> Unit,
|
||||
onClickItem: (Manga) -> Unit,
|
||||
onLongClickItem: (Manga) -> Unit,
|
||||
) {
|
||||
LazyColumn(
|
||||
contentPadding = contentPadding,
|
||||
) {
|
||||
items.forEach { (source, result) ->
|
||||
item(key = source.id) {
|
||||
GlobalSearchResultItem(
|
||||
title = if (source.id == sourceId) "▶ ${source.name}" else source.name,
|
||||
subtitle = LocaleHelper.getDisplayName(source.lang),
|
||||
onClick = { onClickSource(source) },
|
||||
) {
|
||||
when (result) {
|
||||
SearchItemResult.Loading -> {
|
||||
GlobalSearchLoadingResultItem()
|
||||
}
|
||||
is SearchItemResult.Success -> {
|
||||
if (result.isEmpty) {
|
||||
GlobalSearchEmptyResultItem()
|
||||
return@GlobalSearchResultItem
|
||||
}
|
||||
|
||||
GlobalSearchCardRow(
|
||||
titles = result.result,
|
||||
getManga = { getManga(source, it) },
|
||||
onClick = onClickItem,
|
||||
onLongClick = onLongClickItem,
|
||||
)
|
||||
}
|
||||
is SearchItemResult.Error -> {
|
||||
GlobalSearchErrorResultItem(message = result.throwable.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -22,9 +22,7 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.domain.source.interactor.SetMigrateSorting
|
||||
import eu.kanade.domain.source.model.Source
|
||||
import eu.kanade.presentation.browse.components.BaseSourceItem
|
||||
import eu.kanade.presentation.browse.components.SourceIcon
|
||||
import eu.kanade.presentation.components.Badge
|
||||
@ -34,40 +32,43 @@ import eu.kanade.presentation.components.LoadingScreen
|
||||
import eu.kanade.presentation.components.ScrollbarLazyColumn
|
||||
import eu.kanade.presentation.components.Scroller.STICKY_HEADER_KEY_PREFIX
|
||||
import eu.kanade.presentation.theme.header
|
||||
import eu.kanade.presentation.util.horizontalPadding
|
||||
import eu.kanade.presentation.util.padding
|
||||
import eu.kanade.presentation.util.plus
|
||||
import eu.kanade.presentation.util.secondaryItemAlpha
|
||||
import eu.kanade.presentation.util.topPaddingValues
|
||||
import eu.kanade.presentation.util.topSmallPaddingValues
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.ui.browse.migration.sources.MigrationSourcesPresenter
|
||||
import eu.kanade.tachiyomi.ui.browse.migration.sources.MigrateSourceState
|
||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||
import tachiyomi.domain.source.model.Source
|
||||
|
||||
@Composable
|
||||
fun MigrateSourceScreen(
|
||||
presenter: MigrationSourcesPresenter,
|
||||
state: MigrateSourceState,
|
||||
contentPadding: PaddingValues,
|
||||
onClickItem: (Source) -> Unit,
|
||||
onToggleSortingDirection: () -> Unit,
|
||||
onToggleSortingMode: () -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
when {
|
||||
presenter.isLoading -> LoadingScreen()
|
||||
presenter.isEmpty -> EmptyScreen(
|
||||
state.isLoading -> LoadingScreen(modifier = Modifier.padding(contentPadding))
|
||||
state.isEmpty -> EmptyScreen(
|
||||
textResource = R.string.information_empty_library,
|
||||
modifier = Modifier.padding(contentPadding),
|
||||
)
|
||||
else ->
|
||||
MigrateSourceList(
|
||||
list = presenter.items,
|
||||
list = state.items,
|
||||
contentPadding = contentPadding,
|
||||
onClickItem = onClickItem,
|
||||
onLongClickItem = { source ->
|
||||
val sourceId = source.id.toString()
|
||||
context.copyToClipboard(sourceId, sourceId)
|
||||
},
|
||||
sortingMode = presenter.sortingMode,
|
||||
onToggleSortingMode = { presenter.toggleSortingMode() },
|
||||
sortingDirection = presenter.sortingDirection,
|
||||
onToggleSortingDirection = { presenter.toggleSortingDirection() },
|
||||
sortingMode = state.sortingMode,
|
||||
onToggleSortingMode = onToggleSortingMode,
|
||||
sortingDirection = state.sortingDirection,
|
||||
onToggleSortingDirection = onToggleSortingDirection,
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -84,13 +85,13 @@ private fun MigrateSourceList(
|
||||
onToggleSortingDirection: () -> Unit,
|
||||
) {
|
||||
ScrollbarLazyColumn(
|
||||
contentPadding = contentPadding + topPaddingValues,
|
||||
contentPadding = contentPadding + topSmallPaddingValues,
|
||||
) {
|
||||
stickyHeader(key = STICKY_HEADER_KEY_PREFIX) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
.padding(start = horizontalPadding),
|
||||
.padding(start = MaterialTheme.padding.medium),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
@ -152,7 +153,7 @@ private fun MigrateSourceItem(
|
||||
content = { _, sourceLangString ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = horizontalPadding)
|
||||
.padding(horizontal = MaterialTheme.padding.medium)
|
||||
.weight(1f),
|
||||
) {
|
||||
Text(
|
||||
@ -162,7 +163,7 @@ private fun MigrateSourceItem(
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
if (sourceLangString != null) {
|
||||
|
@ -1,28 +0,0 @@
|
||||
package eu.kanade.presentation.browse
|
||||
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import eu.kanade.domain.source.interactor.SetMigrateSorting
|
||||
import eu.kanade.domain.source.model.Source
|
||||
|
||||
interface MigrateSourceState {
|
||||
val isLoading: Boolean
|
||||
val items: List<Pair<Source, Long>>
|
||||
val isEmpty: Boolean
|
||||
val sortingMode: SetMigrateSorting.Mode
|
||||
val sortingDirection: SetMigrateSorting.Direction
|
||||
}
|
||||
|
||||
fun MigrateSourceState(): MigrateSourceState {
|
||||
return MigrateSourceStateImpl()
|
||||
}
|
||||
|
||||
class MigrateSourceStateImpl : MigrateSourceState {
|
||||
override var isLoading: Boolean by mutableStateOf(true)
|
||||
override var items: List<Pair<Source, Long>> by mutableStateOf(emptyList())
|
||||
override val isEmpty: Boolean by derivedStateOf { items.isEmpty() }
|
||||
override var sortingMode: SetMigrateSorting.Mode by mutableStateOf(SetMigrateSorting.Mode.ALPHABETICAL)
|
||||
override var sortingDirection: SetMigrateSorting.Direction by mutableStateOf(SetMigrateSorting.Direction.ASCENDING)
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user