mirror of
https://github.com/mihonapp/mihon.git
synced 2025-06-30 21:17:50 +02:00
Compare commits
209 Commits
Author | SHA1 | Date | |
---|---|---|---|
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 |
2
.github/ISSUE_TEMPLATE.md
vendored
2
.github/ISSUE_TEMPLATE.md
vendored
@ -3,7 +3,7 @@
|
|||||||
I acknowledge that:
|
I acknowledge that:
|
||||||
|
|
||||||
- I have updated:
|
- I have updated:
|
||||||
- To the latest version of the app (stable is v0.14.2)
|
- To the latest version of the app (stable is v0.14.3)
|
||||||
- All extensions
|
- All extensions
|
||||||
- I have tried the troubleshooting guide: https://tachiyomi.org/help/guides/troubleshooting-problems/
|
- 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
|
- 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
|
label: Tachiyomi version
|
||||||
description: You can find your Tachiyomi version in **More → About**.
|
description: You can find your Tachiyomi version in **More → About**.
|
||||||
placeholder: |
|
placeholder: |
|
||||||
Example: "0.14.2"
|
Example: "0.14.3"
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
@ -98,7 +98,7 @@ body:
|
|||||||
required: true
|
required: true
|
||||||
- label: I have tried the [troubleshooting guide](https://tachiyomi.org/help/guides/troubleshooting/).
|
- label: I have tried the [troubleshooting guide](https://tachiyomi.org/help/guides/troubleshooting/).
|
||||||
required: true
|
required: true
|
||||||
- label: I have updated the app to version **[0.14.2](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**.
|
- label: I have updated the app to version **[0.14.3](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**.
|
||||||
required: true
|
required: true
|
||||||
- label: I have updated all installed extensions.
|
- label: I have updated all installed extensions.
|
||||||
required: true
|
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
|
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).
|
- label: If this is an issue with an extension, I should be opening an issue in the [extensions repository](https://github.com/tachiyomiorg/tachiyomi-extensions/issues/new/choose).
|
||||||
required: true
|
required: true
|
||||||
- label: I have updated the app to version **[0.14.2](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**.
|
- label: I have updated the app to version **[0.14.3](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**.
|
||||||
required: true
|
required: true
|
||||||
- label: I will fill out all of the requested information in this form.
|
- label: I will fill out all of the requested information in this form.
|
||||||
required: true
|
required: true
|
||||||
|
6
.github/renovate.json
vendored
6
.github/renovate.json
vendored
@ -5,10 +5,8 @@
|
|||||||
"schedule": ["every sunday"],
|
"schedule": ["every sunday"],
|
||||||
"ignoreDeps": [
|
"ignoreDeps": [
|
||||||
"androidx.core:core-splashscreen",
|
"androidx.core:core-splashscreen",
|
||||||
"androidx.work:work-runtime-ktx",
|
|
||||||
"info.android15.nucleus:nucleus-support-v7",
|
|
||||||
"info.android15.nucleus:nucleus",
|
|
||||||
"com.android.tools:r8",
|
"com.android.tools:r8",
|
||||||
"com.google.guava:guava"
|
"com.google.guava:guava",
|
||||||
|
"com.github.commandiron:WheelPickerCompose"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
2
.github/workflows/build_pull_request.yml
vendored
2
.github/workflows/build_pull_request.yml
vendored
@ -25,7 +25,7 @@ jobs:
|
|||||||
uses: gradle/wrapper-validation-action@v1
|
uses: gradle/wrapper-validation-action@v1
|
||||||
|
|
||||||
- name: Dependency Review
|
- name: Dependency Review
|
||||||
uses: actions/dependency-review-action@v2
|
uses: actions/dependency-review-action@v3
|
||||||
|
|
||||||
- name: Set up JDK 11
|
- name: Set up JDK 11
|
||||||
uses: actions/setup-java@v3
|
uses: actions/setup-java@v3
|
||||||
|
2
.github/workflows/lock.yml
vendored
2
.github/workflows/lock.yml
vendored
@ -12,7 +12,7 @@ jobs:
|
|||||||
lock:
|
lock:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: dessant/lock-threads@v3
|
- uses: dessant/lock-threads@v4
|
||||||
with:
|
with:
|
||||||
github-token: ${{ github.token }}
|
github-token: ${{ github.token }}
|
||||||
issue-inactive-days: '2'
|
issue-inactive-days: '2'
|
||||||
|
5
.gitignore
vendored
5
.gitignore
vendored
@ -10,4 +10,7 @@
|
|||||||
*/build
|
*/build
|
||||||
/build
|
/build
|
||||||
*.apk
|
*.apk
|
||||||
app/**/output.json
|
app/**/output.json
|
||||||
|
|
||||||
|
# Unnecessary file
|
||||||
|
*.swp
|
@ -1,5 +1,6 @@
|
|||||||
import org.gradle.api.tasks.testing.logging.TestLogEvent
|
import org.gradle.api.tasks.testing.logging.TestLogEvent
|
||||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||||
|
import org.jmailen.gradle.kotlinter.tasks.LintTask
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("com.android.application")
|
id("com.android.application")
|
||||||
@ -27,8 +28,8 @@ android {
|
|||||||
applicationId = "eu.kanade.tachiyomi"
|
applicationId = "eu.kanade.tachiyomi"
|
||||||
minSdk = AndroidConfig.minSdk
|
minSdk = AndroidConfig.minSdk
|
||||||
targetSdk = AndroidConfig.targetSdk
|
targetSdk = AndroidConfig.targetSdk
|
||||||
versionCode = 91
|
versionCode = 93
|
||||||
versionName = "0.14.2"
|
versionName = "0.14.3"
|
||||||
|
|
||||||
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
||||||
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
|
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
|
||||||
@ -59,6 +60,7 @@ android {
|
|||||||
named("debug") {
|
named("debug") {
|
||||||
versionNameSuffix = "-${getCommitCount()}"
|
versionNameSuffix = "-${getCommitCount()}"
|
||||||
applicationIdSuffix = ".debug"
|
applicationIdSuffix = ".debug"
|
||||||
|
isPseudoLocalesEnabled = true
|
||||||
}
|
}
|
||||||
named("release") {
|
named("release") {
|
||||||
isShrinkResources = true
|
isShrinkResources = true
|
||||||
@ -99,7 +101,8 @@ android {
|
|||||||
dimension = "default"
|
dimension = "default"
|
||||||
}
|
}
|
||||||
create("dev") {
|
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"
|
dimension = "default"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -142,6 +145,8 @@ android {
|
|||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||||
targetCompatibility = JavaVersion.VERSION_1_8
|
targetCompatibility = JavaVersion.VERSION_1_8
|
||||||
|
|
||||||
|
isCoreLibraryDesugaringEnabled = true
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
@ -161,27 +166,29 @@ dependencies {
|
|||||||
implementation(project(":core"))
|
implementation(project(":core"))
|
||||||
implementation(project(":source-api"))
|
implementation(project(":source-api"))
|
||||||
|
|
||||||
|
coreLibraryDesugaring(libs.desugar)
|
||||||
|
|
||||||
// Compose
|
// Compose
|
||||||
implementation(platform(compose.bom))
|
implementation(platform(compose.bom))
|
||||||
implementation(compose.activity)
|
implementation(compose.activity)
|
||||||
implementation(compose.foundation)
|
implementation(compose.foundation)
|
||||||
implementation(compose.material3.core)
|
implementation(compose.material3.core)
|
||||||
implementation(compose.material3.adapter)
|
implementation(compose.material.core)
|
||||||
implementation(compose.material.icons)
|
implementation(compose.material.icons)
|
||||||
implementation(compose.animation)
|
implementation(compose.animation)
|
||||||
implementation(compose.animation.graphics)
|
implementation(compose.animation.graphics)
|
||||||
implementation(compose.ui.tooling)
|
implementation(compose.ui.tooling)
|
||||||
implementation(compose.ui.util)
|
implementation(compose.ui.util)
|
||||||
implementation(compose.accompanist.webview)
|
implementation(compose.accompanist.webview)
|
||||||
implementation(compose.accompanist.swiperefresh)
|
|
||||||
implementation(compose.accompanist.flowlayout)
|
implementation(compose.accompanist.flowlayout)
|
||||||
implementation(compose.accompanist.permissions)
|
implementation(compose.accompanist.permissions)
|
||||||
|
implementation(compose.accompanist.themeadapter)
|
||||||
|
implementation(compose.accompanist.systemuicontroller)
|
||||||
|
|
||||||
implementation(androidx.paging.runtime)
|
implementation(androidx.paging.runtime)
|
||||||
implementation(androidx.paging.compose)
|
implementation(androidx.paging.compose)
|
||||||
|
|
||||||
implementation(libs.bundles.sqlite)
|
implementation(libs.bundles.sqlite)
|
||||||
implementation(androidx.sqlite)
|
|
||||||
implementation(libs.sqldelight.android.driver)
|
implementation(libs.sqldelight.android.driver)
|
||||||
implementation(libs.sqldelight.coroutines)
|
implementation(libs.sqldelight.coroutines)
|
||||||
implementation(libs.sqldelight.android.paging)
|
implementation(libs.sqldelight.android.paging)
|
||||||
@ -234,15 +241,11 @@ dependencies {
|
|||||||
// Preferences
|
// Preferences
|
||||||
implementation(libs.preferencektx)
|
implementation(libs.preferencektx)
|
||||||
|
|
||||||
// Model View Presenter
|
|
||||||
implementation(libs.bundles.nucleus)
|
|
||||||
|
|
||||||
// Dependency injection
|
// Dependency injection
|
||||||
implementation(libs.injekt.core)
|
implementation(libs.injekt.core)
|
||||||
|
|
||||||
// Image loading
|
// Image loading
|
||||||
implementation(libs.bundles.coil)
|
implementation(libs.bundles.coil)
|
||||||
|
|
||||||
implementation(libs.subsamplingscaleimageview) {
|
implementation(libs.subsamplingscaleimageview) {
|
||||||
exclude(module = "image-decoder")
|
exclude(module = "image-decoder")
|
||||||
}
|
}
|
||||||
@ -260,17 +263,12 @@ dependencies {
|
|||||||
exclude(group = "androidx.viewpager", module = "viewpager")
|
exclude(group = "androidx.viewpager", module = "viewpager")
|
||||||
}
|
}
|
||||||
implementation(libs.insetter)
|
implementation(libs.insetter)
|
||||||
implementation(libs.markwon)
|
implementation(libs.bundles.richtext)
|
||||||
implementation(libs.aboutLibraries.compose)
|
implementation(libs.aboutLibraries.compose)
|
||||||
implementation(libs.cascade)
|
implementation(libs.cascade)
|
||||||
implementation(libs.numberpicker)
|
|
||||||
implementation(libs.bundles.voyager)
|
implementation(libs.bundles.voyager)
|
||||||
|
implementation(libs.wheelpicker)
|
||||||
// Conductor
|
implementation(libs.materialmotion.core)
|
||||||
implementation(libs.bundles.conductor)
|
|
||||||
|
|
||||||
// FlowBinding
|
|
||||||
implementation(libs.bundles.flowbinding)
|
|
||||||
|
|
||||||
// Logging
|
// Logging
|
||||||
implementation(libs.logcat)
|
implementation(libs.logcat)
|
||||||
@ -312,7 +310,7 @@ tasks {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
withType<org.jmailen.gradle.kotlinter.tasks.LintTask>().configureEach {
|
withType<LintTask>().configureEach {
|
||||||
exclude { it.file.path.contains("generated[\\\\/]".toRegex()) }
|
exclude { it.file.path.contains("generated[\\\\/]".toRegex()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -320,10 +318,11 @@ tasks {
|
|||||||
withType<KotlinCompile> {
|
withType<KotlinCompile> {
|
||||||
kotlinOptions.freeCompilerArgs += listOf(
|
kotlinOptions.freeCompilerArgs += listOf(
|
||||||
"-opt-in=coil.annotation.ExperimentalCoilApi",
|
"-opt-in=coil.annotation.ExperimentalCoilApi",
|
||||||
"-opt-in=com.google.accompanist.pager.ExperimentalPagerApi",
|
|
||||||
"-opt-in=com.google.accompanist.permissions.ExperimentalPermissionsApi",
|
"-opt-in=com.google.accompanist.permissions.ExperimentalPermissionsApi",
|
||||||
|
"-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi",
|
||||||
"-opt-in=androidx.compose.material.ExperimentalMaterialApi",
|
"-opt-in=androidx.compose.material.ExperimentalMaterialApi",
|
||||||
"-opt-in=androidx.compose.material3.ExperimentalMaterial3Api",
|
"-opt-in=androidx.compose.material3.ExperimentalMaterial3Api",
|
||||||
|
"-opt-in=androidx.compose.material.ExperimentalMaterialApi",
|
||||||
"-opt-in=androidx.compose.ui.ExperimentalComposeUiApi",
|
"-opt-in=androidx.compose.ui.ExperimentalComposeUiApi",
|
||||||
"-opt-in=androidx.compose.foundation.ExperimentalFoundationApi",
|
"-opt-in=androidx.compose.foundation.ExperimentalFoundationApi",
|
||||||
"-opt-in=androidx.compose.animation.ExperimentalAnimationApi",
|
"-opt-in=androidx.compose.animation.ExperimentalAnimationApi",
|
||||||
|
@ -2,4 +2,5 @@
|
|||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@android:color/transparent"/>
|
<background android:drawable="@android:color/transparent"/>
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
|
<monochrome android:drawable="@drawable/ic_tachi_monochrome_launcher" />
|
||||||
</adaptive-icon>
|
</adaptive-icon>
|
@ -23,6 +23,7 @@
|
|||||||
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
|
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
|
<uses-permission android:name="android.permission.READ_APP_SPECIFIC_LOCALES" />
|
||||||
|
|
||||||
<!-- Remove permission from Firebase dependency -->
|
<!-- Remove permission from Firebase dependency -->
|
||||||
<uses-permission android:name="com.google.android.gms.permission.AD_ID"
|
<uses-permission android:name="com.google.android.gms.permission.AD_ID"
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -34,3 +34,5 @@ class PreferenceMutableState<T>(
|
|||||||
return { preference.set(it) }
|
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
|
|
||||||
}
|
|
@ -13,7 +13,7 @@ private const val listOfStringsSeparator = ", "
|
|||||||
val listOfStringsAdapter = object : ColumnAdapter<List<String>, String> {
|
val listOfStringsAdapter = object : ColumnAdapter<List<String>, String> {
|
||||||
override fun decode(databaseValue: String) =
|
override fun decode(databaseValue: String) =
|
||||||
if (databaseValue.isEmpty()) {
|
if (databaseValue.isEmpty()) {
|
||||||
listOf()
|
emptyList()
|
||||||
} else {
|
} else {
|
||||||
databaseValue.split(listOfStringsSeparator)
|
databaseValue.split(listOfStringsSeparator)
|
||||||
}
|
}
|
||||||
|
@ -107,16 +107,16 @@ private suspend fun CoroutineDispatcher.acquireTransactionThread(
|
|||||||
try {
|
try {
|
||||||
dispatch(EmptyCoroutineContext) {
|
dispatch(EmptyCoroutineContext) {
|
||||||
runBlocking {
|
runBlocking {
|
||||||
// Thread acquired, resume coroutine.
|
// Thread acquired, resume coroutine
|
||||||
continuation.resume(coroutineContext[ContinuationInterceptor]!!)
|
continuation.resume(coroutineContext[ContinuationInterceptor]!!)
|
||||||
controlJob.join()
|
controlJob.join()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (ex: RejectedExecutionException) {
|
} catch (ex: RejectedExecutionException) {
|
||||||
// Couldn't acquire a thread, cancel coroutine.
|
// Couldn't acquire a thread, cancel coroutine
|
||||||
continuation.cancel(
|
continuation.cancel(
|
||||||
IllegalStateException(
|
IllegalStateException(
|
||||||
"Unable to acquire a thread to perform the database transaction.",
|
"Unable to acquire a thread to perform the database transaction",
|
||||||
ex,
|
ex,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@ -152,7 +152,7 @@ private class TransactionElement(
|
|||||||
fun release() {
|
fun release() {
|
||||||
val count = referenceCount.decrementAndGet()
|
val count = referenceCount.decrementAndGet()
|
||||||
if (count < 0) {
|
if (count < 0) {
|
||||||
throw IllegalStateException("Transaction was never started or was already released.")
|
throw IllegalStateException("Transaction was never started or was already released")
|
||||||
} else if (count == 0) {
|
} else if (count == 0) {
|
||||||
// Cancel the job that controls the transaction thread, causing it to be released.
|
// Cancel the job that controls the transaction thread, causing it to be released.
|
||||||
transactionThreadControlJob.cancel()
|
transactionThreadControlJob.cancel()
|
||||||
|
@ -24,6 +24,10 @@ class HistoryRepositoryImpl(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun getTotalReadDuration(): Long {
|
||||||
|
return handler.awaitOne { historyQueries.getReadDuration() }
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun resetHistory(historyId: Long) {
|
override suspend fun resetHistory(historyId: Long) {
|
||||||
try {
|
try {
|
||||||
handler.await { historyQueries.resetHistoryById(historyId) }
|
handler.await { historyQueries.resetHistoryById(historyId) }
|
||||||
|
@ -9,6 +9,10 @@ class TrackRepositoryImpl(
|
|||||||
private val handler: DatabaseHandler,
|
private val handler: DatabaseHandler,
|
||||||
) : TrackRepository {
|
) : TrackRepository {
|
||||||
|
|
||||||
|
override suspend fun getTrackById(id: Long): Track? {
|
||||||
|
return handler.awaitOneOrNull { manga_syncQueries.getTrackById(id, trackMapper) }
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun getTracksByMangaId(mangaId: Long): List<Track> {
|
override suspend fun getTracksByMangaId(mangaId: Long): List<Track> {
|
||||||
return handler.awaitList {
|
return handler.awaitList {
|
||||||
manga_syncQueries.getTracksByMangaId(mangaId, trackMapper)
|
manga_syncQueries.getTracksByMangaId(mangaId, trackMapper)
|
||||||
|
@ -32,11 +32,10 @@ import eu.kanade.domain.download.interactor.DeleteDownload
|
|||||||
import eu.kanade.domain.extension.interactor.GetExtensionLanguages
|
import eu.kanade.domain.extension.interactor.GetExtensionLanguages
|
||||||
import eu.kanade.domain.extension.interactor.GetExtensionSources
|
import eu.kanade.domain.extension.interactor.GetExtensionSources
|
||||||
import eu.kanade.domain.extension.interactor.GetExtensionsByType
|
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.GetHistory
|
||||||
import eu.kanade.domain.history.interactor.GetNextUnreadChapters
|
import eu.kanade.domain.history.interactor.GetNextChapters
|
||||||
import eu.kanade.domain.history.interactor.RemoveHistoryById
|
import eu.kanade.domain.history.interactor.GetTotalReadDuration
|
||||||
import eu.kanade.domain.history.interactor.RemoveHistoryByMangaId
|
import eu.kanade.domain.history.interactor.RemoveHistory
|
||||||
import eu.kanade.domain.history.interactor.UpsertHistory
|
import eu.kanade.domain.history.interactor.UpsertHistory
|
||||||
import eu.kanade.domain.history.repository.HistoryRepository
|
import eu.kanade.domain.history.repository.HistoryRepository
|
||||||
import eu.kanade.domain.manga.interactor.GetDuplicateLibraryManga
|
import eu.kanade.domain.manga.interactor.GetDuplicateLibraryManga
|
||||||
@ -94,7 +93,7 @@ class DomainModule : InjektModule {
|
|||||||
addFactory { GetLibraryManga(get()) }
|
addFactory { GetLibraryManga(get()) }
|
||||||
addFactory { GetMangaWithChapters(get(), get()) }
|
addFactory { GetMangaWithChapters(get(), get()) }
|
||||||
addFactory { GetManga(get()) }
|
addFactory { GetManga(get()) }
|
||||||
addFactory { GetNextUnreadChapters(get(), get(), get()) }
|
addFactory { GetNextChapters(get(), get(), get()) }
|
||||||
addFactory { ResetViewerFlags(get()) }
|
addFactory { ResetViewerFlags(get()) }
|
||||||
addFactory { SetMangaChapterFlags(get()) }
|
addFactory { SetMangaChapterFlags(get()) }
|
||||||
addFactory { SetMangaDefaultChapterFlags(get(), get(), get()) }
|
addFactory { SetMangaDefaultChapterFlags(get(), get(), get()) }
|
||||||
@ -119,11 +118,10 @@ class DomainModule : InjektModule {
|
|||||||
addFactory { SyncChaptersWithTrackServiceTwoWay(get(), get()) }
|
addFactory { SyncChaptersWithTrackServiceTwoWay(get(), get()) }
|
||||||
|
|
||||||
addSingletonFactory<HistoryRepository> { HistoryRepositoryImpl(get()) }
|
addSingletonFactory<HistoryRepository> { HistoryRepositoryImpl(get()) }
|
||||||
addFactory { DeleteAllHistory(get()) }
|
|
||||||
addFactory { GetHistory(get()) }
|
addFactory { GetHistory(get()) }
|
||||||
addFactory { UpsertHistory(get()) }
|
addFactory { UpsertHistory(get()) }
|
||||||
addFactory { RemoveHistoryById(get()) }
|
addFactory { RemoveHistory(get()) }
|
||||||
addFactory { RemoveHistoryByMangaId(get()) }
|
addFactory { GetTotalReadDuration(get()) }
|
||||||
|
|
||||||
addFactory { DeleteDownload(get(), get()) }
|
addFactory { DeleteDownload(get(), get()) }
|
||||||
|
|
||||||
@ -132,7 +130,7 @@ class DomainModule : InjektModule {
|
|||||||
addFactory { GetExtensionLanguages(get(), get()) }
|
addFactory { GetExtensionLanguages(get(), get()) }
|
||||||
|
|
||||||
addSingletonFactory<UpdatesRepository> { UpdatesRepositoryImpl(get()) }
|
addSingletonFactory<UpdatesRepository> { UpdatesRepositoryImpl(get()) }
|
||||||
addFactory { GetUpdates(get(), get()) }
|
addFactory { GetUpdates(get()) }
|
||||||
|
|
||||||
addSingletonFactory<SourceRepository> { SourceRepositoryImpl(get(), get()) }
|
addSingletonFactory<SourceRepository> { SourceRepositoryImpl(get(), get()) }
|
||||||
addSingletonFactory<SourceDataRepository> { SourceDataRepositoryImpl(get()) }
|
addSingletonFactory<SourceDataRepository> { SourceDataRepositoryImpl(get()) }
|
||||||
|
@ -5,46 +5,66 @@ import eu.kanade.domain.category.model.CategoryUpdate
|
|||||||
import eu.kanade.domain.category.repository.CategoryRepository
|
import eu.kanade.domain.category.repository.CategoryRepository
|
||||||
import eu.kanade.tachiyomi.util.lang.withNonCancellableContext
|
import eu.kanade.tachiyomi.util.lang.withNonCancellableContext
|
||||||
import eu.kanade.tachiyomi.util.system.logcat
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
|
import java.util.Collections
|
||||||
|
|
||||||
class ReorderCategory(
|
class ReorderCategory(
|
||||||
private val categoryRepository: CategoryRepository,
|
private val categoryRepository: CategoryRepository,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
suspend fun await(categoryId: Long, newPosition: Int) = withNonCancellableContext {
|
private val mutex = Mutex()
|
||||||
val categories = categoryRepository.getAll().filterNot(Category::isSystemCategory)
|
|
||||||
|
|
||||||
val currentIndex = categories.indexOfFirst { it.id == categoryId }
|
suspend fun moveUp(category: Category): Result =
|
||||||
if (currentIndex == newPosition) {
|
await(category, MoveTo.UP)
|
||||||
return@withNonCancellableContext Result.Unchanged
|
|
||||||
}
|
|
||||||
|
|
||||||
val reorderedCategories = categories.toMutableList()
|
suspend fun moveDown(category: Category): Result =
|
||||||
val reorderedCategory = reorderedCategories.removeAt(currentIndex)
|
await(category, MoveTo.DOWN)
|
||||||
reorderedCategories.add(newPosition, reorderedCategory)
|
|
||||||
|
|
||||||
val updates = reorderedCategories.mapIndexed { index, category ->
|
private suspend fun await(category: Category, moveTo: MoveTo) = withNonCancellableContext {
|
||||||
CategoryUpdate(
|
mutex.withLock {
|
||||||
id = category.id,
|
val categories = categoryRepository.getAll()
|
||||||
order = index.toLong(),
|
.filterNot(Category::isSystemCategory)
|
||||||
)
|
.toMutableList()
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
val currentIndex = categories.indexOfFirst { it.id == category.id }
|
||||||
categoryRepository.updatePartial(updates)
|
if (currentIndex == -1) {
|
||||||
Result.Success
|
return@withNonCancellableContext Result.Unchanged
|
||||||
} catch (e: Exception) {
|
}
|
||||||
logcat(LogPriority.ERROR, e)
|
|
||||||
Result.InternalError(e)
|
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 {
|
sealed class Result {
|
||||||
object Success : Result()
|
object Success : Result()
|
||||||
object Unchanged : Result()
|
object Unchanged : Result()
|
||||||
data class InternalError(val error: Throwable) : Result()
|
data class InternalError(val error: Throwable) : Result()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private enum class MoveTo {
|
||||||
|
UP,
|
||||||
|
DOWN,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,6 @@ data class Category(
|
|||||||
val isSystemCategory: Boolean = id == UNCATEGORIZED_ID
|
val isSystemCategory: Boolean = id == UNCATEGORIZED_ID
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
const val UNCATEGORIZED_ID = 0L
|
const val UNCATEGORIZED_ID = 0L
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,6 @@ import eu.kanade.data.chapter.CleanupChapterName
|
|||||||
import eu.kanade.data.chapter.NoChaptersException
|
import eu.kanade.data.chapter.NoChaptersException
|
||||||
import eu.kanade.domain.chapter.model.Chapter
|
import eu.kanade.domain.chapter.model.Chapter
|
||||||
import eu.kanade.domain.chapter.model.toChapterUpdate
|
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.repository.ChapterRepository
|
||||||
import eu.kanade.domain.manga.interactor.UpdateManga
|
import eu.kanade.domain.manga.interactor.UpdateManga
|
||||||
import eu.kanade.domain.manga.model.Manga
|
import eu.kanade.domain.manga.model.Manga
|
||||||
@ -111,7 +110,7 @@ class SyncChaptersWithSource(
|
|||||||
downloadManager.isChapterDownloaded(dbChapter.name, dbChapter.scanlator, manga.title, manga.source)
|
downloadManager.isChapterDownloaded(dbChapter.name, dbChapter.scanlator, manga.title, manga.source)
|
||||||
|
|
||||||
if (shouldRenameChapter) {
|
if (shouldRenameChapter) {
|
||||||
downloadManager.renameChapter(source, manga, dbChapter.toDbChapter(), chapter.toDbChapter())
|
downloadManager.renameChapter(source, manga, dbChapter, chapter)
|
||||||
}
|
}
|
||||||
var toChangeChapter = dbChapter.copy(
|
var toChangeChapter = dbChapter.copy(
|
||||||
name = chapter.name,
|
name = chapter.name,
|
||||||
|
@ -0,0 +1,82 @@
|
|||||||
|
package eu.kanade.domain.chapter.model
|
||||||
|
|
||||||
|
import eu.kanade.domain.manga.model.Manga
|
||||||
|
import eu.kanade.domain.manga.model.TriStateFilter
|
||||||
|
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
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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,7 +1,6 @@
|
|||||||
package eu.kanade.domain.download.interactor
|
package eu.kanade.domain.download.interactor
|
||||||
|
|
||||||
import eu.kanade.domain.chapter.model.Chapter
|
import eu.kanade.domain.chapter.model.Chapter
|
||||||
import eu.kanade.domain.chapter.model.toDbChapter
|
|
||||||
import eu.kanade.domain.manga.model.Manga
|
import eu.kanade.domain.manga.model.Manga
|
||||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
@ -14,7 +13,7 @@ class DeleteDownload(
|
|||||||
|
|
||||||
suspend fun awaitAll(manga: Manga, vararg chapters: Chapter) = withNonCancellableContext {
|
suspend fun awaitAll(manga: Manga, vararg chapters: Chapter) = withNonCancellableContext {
|
||||||
sourceManager.get(manga.source)?.let { source ->
|
sourceManager.get(manga.source)?.let { source ->
|
||||||
downloadManager.deleteChapters(chapters.map { it.toDbChapter() }, manga, source)
|
downloadManager.deleteChapters(chapters.toList(), manga, source)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,10 +25,7 @@ class GetExtensionLanguages(
|
|||||||
}
|
}
|
||||||
.distinct()
|
.distinct()
|
||||||
.sortedWith(
|
.sortedWith(
|
||||||
compareBy(
|
compareBy<String> { it !in enabledLanguage }.then(LocaleHelper.comparator),
|
||||||
{ it !in enabledLanguage },
|
|
||||||
{ LocaleHelper.getDisplayName(it) },
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,6 @@ package eu.kanade.domain.extension.interactor
|
|||||||
import eu.kanade.domain.source.service.SourcePreferences
|
import eu.kanade.domain.source.service.SourcePreferences
|
||||||
import eu.kanade.tachiyomi.extension.model.Extension
|
import eu.kanade.tachiyomi.extension.model.Extension
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionSourceItem
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
|
||||||
@ -30,3 +29,9 @@ class GetExtensionSources(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class ExtensionSourceItem(
|
||||||
|
val source: Source,
|
||||||
|
val enabled: Boolean,
|
||||||
|
val labelAsName: Boolean,
|
||||||
|
)
|
||||||
|
@ -7,6 +7,7 @@ import kotlinx.coroutines.flow.Flow
|
|||||||
class GetHistory(
|
class GetHistory(
|
||||||
private val repository: HistoryRepository,
|
private val repository: HistoryRepository,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun subscribe(query: String): Flow<List<HistoryWithRelations>> {
|
fun subscribe(query: String): Flow<List<HistoryWithRelations>> {
|
||||||
return repository.getHistory(query)
|
return repository.getHistory(query)
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,52 @@
|
|||||||
|
package eu.kanade.domain.history.interactor
|
||||||
|
|
||||||
|
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.tachiyomi.util.chapter.getChapterSort
|
||||||
|
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,33 +0,0 @@
|
|||||||
package eu.kanade.domain.history.interactor
|
|
||||||
|
|
||||||
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.tachiyomi.util.chapter.getChapterSort
|
|
||||||
import kotlin.math.max
|
|
||||||
|
|
||||||
class GetNextUnreadChapters(
|
|
||||||
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).firstOrNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun await(mangaId: Long): List<Chapter> {
|
|
||||||
val manga = getManga.await(mangaId) ?: return emptyList()
|
|
||||||
return getChapterByMangaId.await(mangaId)
|
|
||||||
.sortedWith(getChapterSort(manga, sortDescending = false))
|
|
||||||
.filterNot { it.read }
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun await(mangaId: Long, fromChapterId: Long): List<Chapter> {
|
|
||||||
val unreadChapters = await(mangaId)
|
|
||||||
val currChapterIndex = unreadChapters.indexOfFirst { it.id == fromChapterId }
|
|
||||||
return unreadChapters.subList(max(0, currChapterIndex), unreadChapters.size)
|
|
||||||
}
|
|
||||||
}
|
|
@ -2,11 +2,11 @@ package eu.kanade.domain.history.interactor
|
|||||||
|
|
||||||
import eu.kanade.domain.history.repository.HistoryRepository
|
import eu.kanade.domain.history.repository.HistoryRepository
|
||||||
|
|
||||||
class DeleteAllHistory(
|
class GetTotalReadDuration(
|
||||||
private val repository: HistoryRepository,
|
private val repository: HistoryRepository,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
suspend fun await(): Boolean {
|
suspend fun await(): Long {
|
||||||
return repository.deleteAllHistory()
|
return repository.getTotalReadDuration()
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -3,11 +3,19 @@ package eu.kanade.domain.history.interactor
|
|||||||
import eu.kanade.domain.history.model.HistoryWithRelations
|
import eu.kanade.domain.history.model.HistoryWithRelations
|
||||||
import eu.kanade.domain.history.repository.HistoryRepository
|
import eu.kanade.domain.history.repository.HistoryRepository
|
||||||
|
|
||||||
class RemoveHistoryById(
|
class RemoveHistory(
|
||||||
private val repository: HistoryRepository,
|
private val repository: HistoryRepository,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
suspend fun awaitAll(): Boolean {
|
||||||
|
return repository.deleteAllHistory()
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun await(history: HistoryWithRelations) {
|
suspend fun await(history: HistoryWithRelations) {
|
||||||
repository.resetHistory(history.id)
|
repository.resetHistory(history.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun await(mangaId: Long) {
|
||||||
|
repository.resetHistoryByMangaId(mangaId)
|
||||||
|
}
|
||||||
}
|
}
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -10,6 +10,8 @@ interface HistoryRepository {
|
|||||||
|
|
||||||
suspend fun getLastHistory(): HistoryWithRelations?
|
suspend fun getLastHistory(): HistoryWithRelations?
|
||||||
|
|
||||||
|
suspend fun getTotalReadDuration(): Long
|
||||||
|
|
||||||
suspend fun resetHistory(historyId: Long)
|
suspend fun resetHistory(historyId: Long)
|
||||||
|
|
||||||
suspend fun resetHistoryByMangaId(mangaId: Long)
|
suspend fun resetHistoryByMangaId(mangaId: Long)
|
||||||
|
@ -32,7 +32,6 @@ data class LibrarySort(
|
|||||||
object DateAdded : Type(0b00011100)
|
object DateAdded : Type(0b00011100)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
fun valueOf(flag: Long): Type {
|
fun valueOf(flag: Long): Type {
|
||||||
return types.find { type -> type.flag == flag and type.mask } ?: default.type
|
return types.find { type -> type.flag == flag and type.mask } ?: default.type
|
||||||
}
|
}
|
||||||
@ -49,7 +48,6 @@ data class LibrarySort(
|
|||||||
object Descending : Direction(0b00000000)
|
object Descending : Direction(0b00000000)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
fun valueOf(flag: Long): Direction {
|
fun valueOf(flag: Long): Direction {
|
||||||
return directions.find { direction -> direction.flag == flag and direction.mask } ?: default.direction
|
return directions.find { direction -> direction.flag == flag and direction.mask } ?: default.direction
|
||||||
}
|
}
|
||||||
|
@ -32,6 +32,8 @@ class LibraryPreferences(
|
|||||||
|
|
||||||
fun autoUpdateTrackers() = preferenceStore.getBoolean("auto_update_trackers", false)
|
fun autoUpdateTrackers() = preferenceStore.getBoolean("auto_update_trackers", false)
|
||||||
|
|
||||||
|
fun showContinueReadingButton() = preferenceStore.getBoolean("display_continue_reading_button", false)
|
||||||
|
|
||||||
// region Filter
|
// region Filter
|
||||||
|
|
||||||
fun filterDownloaded() = preferenceStore.getInt("pref_filter_library_downloaded", ExtendedNavigationView.Item.TriStateGroup.State.IGNORE.value)
|
fun filterDownloaded() = preferenceStore.getInt("pref_filter_library_downloaded", ExtendedNavigationView.Item.TriStateGroup.State.IGNORE.value)
|
||||||
@ -58,8 +60,8 @@ class LibraryPreferences(
|
|||||||
|
|
||||||
fun languageBadge() = preferenceStore.getBoolean("display_language_badge", false)
|
fun languageBadge() = preferenceStore.getBoolean("display_language_badge", false)
|
||||||
|
|
||||||
fun showUpdatesNavBadge() = preferenceStore.getBoolean("library_update_show_tab_badge", false)
|
fun newShowUpdatesCount() = preferenceStore.getBoolean("library_show_updates_count", true)
|
||||||
fun unreadUpdatesCount() = preferenceStore.getInt("library_unread_updates_count", 0)
|
fun newUpdatesCount() = preferenceStore.getInt("library_unseen_updates_count", 0)
|
||||||
|
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
|
@ -10,19 +10,21 @@ class SetMangaViewerFlags(
|
|||||||
) {
|
) {
|
||||||
|
|
||||||
suspend fun awaitSetMangaReadingMode(id: Long, flag: Long) {
|
suspend fun awaitSetMangaReadingMode(id: Long, flag: Long) {
|
||||||
|
val manga = mangaRepository.getMangaById(id)
|
||||||
mangaRepository.update(
|
mangaRepository.update(
|
||||||
MangaUpdate(
|
MangaUpdate(
|
||||||
id = id,
|
id = id,
|
||||||
viewerFlags = flag.setFlag(flag, ReadingModeType.MASK.toLong()),
|
viewerFlags = manga.viewerFlags.setFlag(flag, ReadingModeType.MASK.toLong()),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun awaitSetOrientationType(id: Long, flag: Long) {
|
suspend fun awaitSetOrientationType(id: Long, flag: Long) {
|
||||||
|
val manga = mangaRepository.getMangaById(id)
|
||||||
mangaRepository.update(
|
mangaRepository.update(
|
||||||
MangaUpdate(
|
MangaUpdate(
|
||||||
id = id,
|
id = id,
|
||||||
viewerFlags = flag.setFlag(flag, OrientationType.MASK.toLong()),
|
viewerFlags = manga.viewerFlags.setFlag(flag, OrientationType.MASK.toLong()),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,6 @@ import eu.kanade.domain.manga.model.Manga
|
|||||||
import eu.kanade.domain.manga.model.MangaUpdate
|
import eu.kanade.domain.manga.model.MangaUpdate
|
||||||
import eu.kanade.domain.manga.model.hasCustomCover
|
import eu.kanade.domain.manga.model.hasCustomCover
|
||||||
import eu.kanade.domain.manga.model.isLocal
|
import eu.kanade.domain.manga.model.isLocal
|
||||||
import eu.kanade.domain.manga.model.toDbManga
|
|
||||||
import eu.kanade.domain.manga.repository.MangaRepository
|
import eu.kanade.domain.manga.repository.MangaRepository
|
||||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
@ -46,11 +45,11 @@ class UpdateManga(
|
|||||||
!manualFetch && localManga.thumbnailUrl == remoteManga.thumbnail_url -> null
|
!manualFetch && localManga.thumbnailUrl == remoteManga.thumbnail_url -> null
|
||||||
localManga.isLocal() -> Date().time
|
localManga.isLocal() -> Date().time
|
||||||
localManga.hasCustomCover(coverCache) -> {
|
localManga.hasCustomCover(coverCache) -> {
|
||||||
coverCache.deleteFromCache(localManga.toDbManga(), false)
|
coverCache.deleteFromCache(localManga, false)
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
coverCache.deleteFromCache(localManga.toDbManga(), false)
|
coverCache.deleteFromCache(localManga, false)
|
||||||
Date().time
|
Date().time
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,60 +1,175 @@
|
|||||||
package eu.kanade.domain.manga.model
|
package eu.kanade.domain.manga.model
|
||||||
|
|
||||||
|
import eu.kanade.domain.chapter.model.Chapter
|
||||||
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
import nl.adaptivity.xmlutil.serialization.XmlElement
|
||||||
import nl.adaptivity.xmlutil.serialization.XmlSerialName
|
import nl.adaptivity.xmlutil.serialization.XmlSerialName
|
||||||
import nl.adaptivity.xmlutil.serialization.XmlValue
|
import nl.adaptivity.xmlutil.serialization.XmlValue
|
||||||
|
|
||||||
|
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
|
@Serializable
|
||||||
@XmlSerialName("ComicInfo", "", "")
|
@XmlSerialName("ComicInfo", "", "")
|
||||||
data class ComicInfo(
|
data class ComicInfo(
|
||||||
val series: ComicInfoSeries?,
|
val title: Title?,
|
||||||
val summary: ComicInfoSummary?,
|
val series: Series?,
|
||||||
val writer: ComicInfoWriter?,
|
val summary: Summary?,
|
||||||
val penciller: ComicInfoPenciller?,
|
val writer: Writer?,
|
||||||
val inker: ComicInfoInker?,
|
val penciller: Penciller?,
|
||||||
val colorist: ComicInfoColorist?,
|
val inker: Inker?,
|
||||||
val letterer: ComicInfoLetterer?,
|
val colorist: Colorist?,
|
||||||
val coverArtist: ComicInfoCoverArtist?,
|
val letterer: Letterer?,
|
||||||
val genre: ComicInfoGenre?,
|
val coverArtist: CoverArtist?,
|
||||||
val tags: ComicInfoTags?,
|
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
|
@Suppress("UNUSED")
|
||||||
@XmlSerialName("Series", "", "")
|
@XmlElement(false)
|
||||||
data class ComicInfoSeries(@XmlValue(true) val value: String = "")
|
@XmlSerialName("xmlns:xsi", "", "")
|
||||||
|
val xmlSchemaInstance: String = "http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@XmlSerialName("Summary", "", "")
|
@XmlSerialName("Title", "", "")
|
||||||
data class ComicInfoSummary(@XmlValue(true) val value: String = "")
|
data class Title(@XmlValue(true) val value: String = "")
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@XmlSerialName("Writer", "", "")
|
@XmlSerialName("Series", "", "")
|
||||||
data class ComicInfoWriter(@XmlValue(true) val value: String = "")
|
data class Series(@XmlValue(true) val value: String = "")
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@XmlSerialName("Penciller", "", "")
|
@XmlSerialName("Summary", "", "")
|
||||||
data class ComicInfoPenciller(@XmlValue(true) val value: String = "")
|
data class Summary(@XmlValue(true) val value: String = "")
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@XmlSerialName("Inker", "", "")
|
@XmlSerialName("Writer", "", "")
|
||||||
data class ComicInfoInker(@XmlValue(true) val value: String = "")
|
data class Writer(@XmlValue(true) val value: String = "")
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@XmlSerialName("Colorist", "", "")
|
@XmlSerialName("Penciller", "", "")
|
||||||
data class ComicInfoColorist(@XmlValue(true) val value: String = "")
|
data class Penciller(@XmlValue(true) val value: String = "")
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@XmlSerialName("Letterer", "", "")
|
@XmlSerialName("Inker", "", "")
|
||||||
data class ComicInfoLetterer(@XmlValue(true) val value: String = "")
|
data class Inker(@XmlValue(true) val value: String = "")
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@XmlSerialName("CoverArtist", "", "")
|
@XmlSerialName("Colorist", "", "")
|
||||||
data class ComicInfoCoverArtist(@XmlValue(true) val value: String = "")
|
data class Colorist(@XmlValue(true) val value: String = "")
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@XmlSerialName("Genre", "", "")
|
@XmlSerialName("Letterer", "", "")
|
||||||
data class ComicInfoGenre(@XmlValue(true) val value: String = "")
|
data class Letterer(@XmlValue(true) val value: String = "")
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@XmlSerialName("Tags", "", "")
|
@XmlSerialName("CoverArtist", "", "")
|
||||||
data class ComicInfoTags(@XmlValue(true) val value: String = "")
|
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,17 +1,16 @@
|
|||||||
package eu.kanade.domain.manga.model
|
package eu.kanade.domain.manga.model
|
||||||
|
|
||||||
import eu.kanade.data.listOfStringsAdapter
|
|
||||||
import eu.kanade.domain.base.BasePreferences
|
import eu.kanade.domain.base.BasePreferences
|
||||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
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.LocalSource
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
||||||
|
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
|
||||||
|
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
|
||||||
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
|
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.io.Serializable
|
import java.io.Serializable
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga as DbManga
|
|
||||||
|
|
||||||
data class Manga(
|
data class Manga(
|
||||||
val id: Long,
|
val id: Long,
|
||||||
@ -49,6 +48,12 @@ data class Manga(
|
|||||||
val bookmarkedFilterRaw: Long
|
val bookmarkedFilterRaw: Long
|
||||||
get() = chapterFlags and CHAPTER_BOOKMARKED_MASK
|
get() = chapterFlags and CHAPTER_BOOKMARKED_MASK
|
||||||
|
|
||||||
|
val readingModeType: Long
|
||||||
|
get() = viewerFlags and ReadingModeType.MASK.toLong()
|
||||||
|
|
||||||
|
val orientationType: Long
|
||||||
|
get() = viewerFlags and OrientationType.MASK.toLong()
|
||||||
|
|
||||||
val unreadFilter: TriStateFilter
|
val unreadFilter: TriStateFilter
|
||||||
get() = when (unreadFilterRaw) {
|
get() = when (unreadFilterRaw) {
|
||||||
CHAPTER_SHOW_UNREAD -> TriStateFilter.ENABLED_IS
|
CHAPTER_SHOW_UNREAD -> TriStateFilter.ENABLED_IS
|
||||||
@ -187,28 +192,6 @@ fun TriStateFilter.toTriStateGroupState(): ExtendedNavigationView.Item.TriStateG
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
|
||||||
it.url = url
|
|
||||||
it.title = title
|
|
||||||
it.artist = artist
|
|
||||||
it.author = author
|
|
||||||
it.description = description
|
|
||||||
it.genre = genre?.let(listOfStringsAdapter::encode)
|
|
||||||
it.status = status.toInt()
|
|
||||||
it.thumbnail_url = thumbnailUrl
|
|
||||||
it.update_strategy = updateStrategy
|
|
||||||
it.initialized = initialized
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Manga.toMangaUpdate(): MangaUpdate {
|
fun Manga.toMangaUpdate(): MangaUpdate {
|
||||||
return MangaUpdate(
|
return MangaUpdate(
|
||||||
id = id,
|
id = id,
|
||||||
|
@ -10,3 +10,13 @@ data class MangaCover(
|
|||||||
val url: String?,
|
val url: String?,
|
||||||
val lastModified: Long,
|
val lastModified: Long,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
fun Manga.asMangaCover(): MangaCover {
|
||||||
|
return MangaCover(
|
||||||
|
mangaId = id,
|
||||||
|
sourceId = source,
|
||||||
|
isMangaFavorite = favorite,
|
||||||
|
url = thumbnailUrl,
|
||||||
|
lastModified = coverLastModified,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
@ -23,7 +23,6 @@ class GetEnabledSources(
|
|||||||
preferences.lastUsedSource().changes(),
|
preferences.lastUsedSource().changes(),
|
||||||
repository.getSources(),
|
repository.getSources(),
|
||||||
) { pinnedSourceIds, enabledLanguages, disabledSources, lastUsedSource, sources ->
|
) { pinnedSourceIds, enabledLanguages, disabledSources, lastUsedSource, sources ->
|
||||||
val duplicatePins = preferences.duplicatePinnedSources().get()
|
|
||||||
sources
|
sources
|
||||||
.filter { it.lang in enabledLanguages || it.id == LocalSource.ID }
|
.filter { it.lang in enabledLanguages || it.id == LocalSource.ID }
|
||||||
.filterNot { it.id.toString() in disabledSources }
|
.filterNot { it.id.toString() in disabledSources }
|
||||||
@ -35,10 +34,6 @@ class GetEnabledSources(
|
|||||||
if (source.id == lastUsedSource) {
|
if (source.id == lastUsedSource) {
|
||||||
toFlatten.add(source.copy(isUsedLast = true, pin = source.pin - Pin.Actual))
|
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
|
toFlatten
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,10 +25,7 @@ class GetLanguagesWithSources(
|
|||||||
|
|
||||||
sortedSources.groupBy { it.lang }
|
sortedSources.groupBy { it.lang }
|
||||||
.toSortedMap(
|
.toSortedMap(
|
||||||
compareBy(
|
compareBy<String> { it !in enabledLanguage }.then(LocaleHelper.comparator),
|
||||||
{ it !in enabledLanguage },
|
|
||||||
{ LocaleHelper.getDisplayName(it) },
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
private fun isEnabled(sourceId: Long): Boolean {
|
||||||
return sourceId.toString() in preferences.disabledSources().get()
|
return sourceId.toString() in preferences.disabledSources().get()
|
||||||
}
|
}
|
||||||
|
@ -33,7 +33,6 @@ data class Source(
|
|||||||
val key: () -> String = {
|
val key: () -> String = {
|
||||||
when {
|
when {
|
||||||
isUsedLast -> "$id-lastused"
|
isUsedLast -> "$id-lastused"
|
||||||
Pin.Forced in pin -> "$id-forced"
|
|
||||||
else -> "$id"
|
else -> "$id"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -43,7 +42,6 @@ sealed class Pin(val code: Int) {
|
|||||||
object Unpinned : Pin(0b00)
|
object Unpinned : Pin(0b00)
|
||||||
object Pinned : Pin(0b01)
|
object Pinned : Pin(0b01)
|
||||||
object Actual : Pin(0b10)
|
object Actual : Pin(0b10)
|
||||||
object Forced : Pin(0b100)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
inline fun Pins(builder: Pins.PinsBuilder.() -> Unit = {}): Pins {
|
inline fun Pins(builder: Pins.PinsBuilder.() -> Unit = {}): Pins {
|
||||||
|
@ -10,6 +10,15 @@ class GetTracks(
|
|||||||
private val trackRepository: TrackRepository,
|
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> {
|
suspend fun await(mangaId: Long): List<Track> {
|
||||||
return try {
|
return try {
|
||||||
trackRepository.getTracksByMangaId(mangaId)
|
trackRepository.getTracksByMangaId(mangaId)
|
||||||
|
@ -5,6 +5,8 @@ import kotlinx.coroutines.flow.Flow
|
|||||||
|
|
||||||
interface TrackRepository {
|
interface TrackRepository {
|
||||||
|
|
||||||
|
suspend fun getTrackById(id: Long): Track?
|
||||||
|
|
||||||
suspend fun getTracksByMangaId(mangaId: Long): List<Track>
|
suspend fun getTracksByMangaId(mangaId: Long): List<Track>
|
||||||
|
|
||||||
fun getTracksAsFlow(): Flow<List<Track>>
|
fun getTracksAsFlow(): Flow<List<Track>>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
package eu.kanade.tachiyomi.data.track.job
|
package eu.kanade.domain.track.service
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.work.BackoffPolicy
|
import androidx.work.BackoffPolicy
|
||||||
@ -9,10 +9,10 @@ import androidx.work.NetworkType
|
|||||||
import androidx.work.OneTimeWorkRequestBuilder
|
import androidx.work.OneTimeWorkRequestBuilder
|
||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
import eu.kanade.domain.manga.interactor.GetManga
|
|
||||||
import eu.kanade.domain.track.interactor.GetTracks
|
import eu.kanade.domain.track.interactor.GetTracks
|
||||||
import eu.kanade.domain.track.interactor.InsertTrack
|
import eu.kanade.domain.track.interactor.InsertTrack
|
||||||
import eu.kanade.domain.track.model.toDbTrack
|
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.data.track.TrackManager
|
||||||
import eu.kanade.tachiyomi.util.lang.withIOContext
|
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||||
import eu.kanade.tachiyomi.util.system.logcat
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
@ -25,7 +25,6 @@ class DelayedTrackingUpdateJob(context: Context, workerParams: WorkerParameters)
|
|||||||
CoroutineWorker(context, workerParams) {
|
CoroutineWorker(context, workerParams) {
|
||||||
|
|
||||||
override suspend fun doWork(): Result {
|
override suspend fun doWork(): Result {
|
||||||
val getManga = Injekt.get<GetManga>()
|
|
||||||
val getTracks = Injekt.get<GetTracks>()
|
val getTracks = Injekt.get<GetTracks>()
|
||||||
val insertTrack = Injekt.get<InsertTrack>()
|
val insertTrack = Injekt.get<InsertTrack>()
|
||||||
|
|
||||||
@ -34,10 +33,11 @@ class DelayedTrackingUpdateJob(context: Context, workerParams: WorkerParameters)
|
|||||||
|
|
||||||
withIOContext {
|
withIOContext {
|
||||||
val tracks = delayedTrackingStore.getItems().mapNotNull {
|
val tracks = delayedTrackingStore.getItems().mapNotNull {
|
||||||
val manga = getManga.await(it.mangaId) ?: return@withIOContext
|
val track = getTracks.awaitOne(it.trackId)
|
||||||
getTracks.await(manga.id)
|
if (track == null) {
|
||||||
.find { track -> track.id == it.trackId }
|
delayedTrackingStore.remove(it.trackId)
|
||||||
?.copy(lastChapterRead = it.lastChapterRead.toDouble())
|
}
|
||||||
|
track
|
||||||
}
|
}
|
||||||
|
|
||||||
tracks.forEach { track ->
|
tracks.forEach { track ->
|
||||||
@ -47,7 +47,7 @@ class DelayedTrackingUpdateJob(context: Context, workerParams: WorkerParameters)
|
|||||||
service.update(track.toDbTrack(), true)
|
service.update(track.toDbTrack(), true)
|
||||||
insertTrack.await(track)
|
insertTrack.await(track)
|
||||||
}
|
}
|
||||||
delayedTrackingStore.remove(track)
|
delayedTrackingStore.remove(track.id)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logcat(LogPriority.ERROR, e)
|
logcat(LogPriority.ERROR, e)
|
||||||
}
|
}
|
@ -0,0 +1,46 @@
|
|||||||
|
package eu.kanade.domain.track.store
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.core.content.edit
|
||||||
|
import eu.kanade.domain.track.model.Track
|
||||||
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
|
import logcat.LogPriority
|
||||||
|
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
}
|
@ -1,24 +1,17 @@
|
|||||||
package eu.kanade.domain.updates.interactor
|
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.model.UpdatesWithRelations
|
||||||
import eu.kanade.domain.updates.repository.UpdatesRepository
|
import eu.kanade.domain.updates.repository.UpdatesRepository
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.onEach
|
|
||||||
import java.util.Calendar
|
import java.util.Calendar
|
||||||
|
|
||||||
class GetUpdates(
|
class GetUpdates(
|
||||||
private val repository: UpdatesRepository,
|
private val repository: UpdatesRepository,
|
||||||
private val preferences: LibraryPreferences,
|
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun subscribe(calendar: Calendar): Flow<List<UpdatesWithRelations>> = subscribe(calendar.time.time)
|
fun subscribe(calendar: Calendar): Flow<List<UpdatesWithRelations>> = subscribe(calendar.time.time)
|
||||||
|
|
||||||
fun subscribe(after: Long): Flow<List<UpdatesWithRelations>> {
|
fun subscribe(after: Long): Flow<List<UpdatesWithRelations>> {
|
||||||
return repository.subscribeAll(after)
|
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,37 @@
|
|||||||
package eu.kanade.presentation.browse
|
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.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.lazy.grid.GridCells
|
||||||
import androidx.compose.foundation.rememberScrollState
|
|
||||||
import androidx.compose.material.icons.Icons
|
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.HelpOutline
|
||||||
import androidx.compose.material.icons.outlined.NewReleases
|
|
||||||
import androidx.compose.material.icons.outlined.Public
|
import androidx.compose.material.icons.outlined.Public
|
||||||
import androidx.compose.material.icons.outlined.Refresh
|
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.SnackbarDuration
|
||||||
import androidx.compose.material3.SnackbarHost
|
|
||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.material3.SnackbarResult
|
import androidx.compose.material3.SnackbarResult
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
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.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.LoadState
|
||||||
import androidx.paging.compose.LazyPagingItems
|
import androidx.paging.compose.LazyPagingItems
|
||||||
import androidx.paging.compose.collectAsLazyPagingItems
|
|
||||||
import eu.kanade.data.source.NoResultsException
|
import eu.kanade.data.source.NoResultsException
|
||||||
import eu.kanade.domain.library.model.LibraryDisplayMode
|
import eu.kanade.domain.library.model.LibraryDisplayMode
|
||||||
import eu.kanade.domain.manga.model.Manga
|
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.BrowseSourceComfortableGrid
|
||||||
import eu.kanade.presentation.browse.components.BrowseSourceCompactGrid
|
import eu.kanade.presentation.browse.components.BrowseSourceCompactGrid
|
||||||
import eu.kanade.presentation.browse.components.BrowseSourceList
|
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.EmptyScreen
|
import eu.kanade.presentation.components.EmptyScreen
|
||||||
import eu.kanade.presentation.components.EmptyScreenAction
|
import eu.kanade.presentation.components.EmptyScreenAction
|
||||||
import eu.kanade.presentation.components.ExtendedFloatingActionButton
|
|
||||||
import eu.kanade.presentation.components.LoadingScreen
|
import eu.kanade.presentation.components.LoadingScreen
|
||||||
import eu.kanade.presentation.components.Scaffold
|
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||||
import eu.kanade.tachiyomi.source.LocalSource
|
import eu.kanade.tachiyomi.source.LocalSource
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourcePresenter
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
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,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun BrowseSourceContent(
|
fun BrowseSourceContent(
|
||||||
state: BrowseSourceState,
|
source: CatalogueSource?,
|
||||||
mangaList: LazyPagingItems<Manga>,
|
mangaList: LazyPagingItems<StateFlow<Manga>>,
|
||||||
getMangaState: @Composable ((Manga) -> State<Manga>),
|
|
||||||
columns: GridCells,
|
columns: GridCells,
|
||||||
displayMode: LibraryDisplayMode,
|
displayMode: LibraryDisplayMode,
|
||||||
snackbarHostState: SnackbarHostState,
|
snackbarHostState: SnackbarHostState,
|
||||||
@ -249,7 +73,7 @@ fun BrowseSourceContent(
|
|||||||
if (mangaList.itemCount <= 0 && errorState != null && errorState is LoadState.Error) {
|
if (mangaList.itemCount <= 0 && errorState != null && errorState is LoadState.Error) {
|
||||||
EmptyScreen(
|
EmptyScreen(
|
||||||
message = getErrorMessage(errorState),
|
message = getErrorMessage(errorState),
|
||||||
actions = if (state.source is LocalSource) {
|
actions = if (source is LocalSource) {
|
||||||
listOf(
|
listOf(
|
||||||
EmptyScreenAction(
|
EmptyScreenAction(
|
||||||
stringResId = R.string.local_source_help_guide,
|
stringResId = R.string.local_source_help_guide,
|
||||||
@ -290,7 +114,6 @@ fun BrowseSourceContent(
|
|||||||
LibraryDisplayMode.ComfortableGrid -> {
|
LibraryDisplayMode.ComfortableGrid -> {
|
||||||
BrowseSourceComfortableGrid(
|
BrowseSourceComfortableGrid(
|
||||||
mangaList = mangaList,
|
mangaList = mangaList,
|
||||||
getMangaState = getMangaState,
|
|
||||||
columns = columns,
|
columns = columns,
|
||||||
contentPadding = contentPadding,
|
contentPadding = contentPadding,
|
||||||
onMangaClick = onMangaClick,
|
onMangaClick = onMangaClick,
|
||||||
@ -300,16 +123,14 @@ fun BrowseSourceContent(
|
|||||||
LibraryDisplayMode.List -> {
|
LibraryDisplayMode.List -> {
|
||||||
BrowseSourceList(
|
BrowseSourceList(
|
||||||
mangaList = mangaList,
|
mangaList = mangaList,
|
||||||
getMangaState = getMangaState,
|
|
||||||
contentPadding = contentPadding,
|
contentPadding = contentPadding,
|
||||||
onMangaClick = onMangaClick,
|
onMangaClick = onMangaClick,
|
||||||
onMangaLongClick = onMangaLongClick,
|
onMangaLongClick = onMangaLongClick,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
else -> {
|
LibraryDisplayMode.CompactGrid, LibraryDisplayMode.CoverOnlyGrid -> {
|
||||||
BrowseSourceCompactGrid(
|
BrowseSourceCompactGrid(
|
||||||
mangaList = mangaList,
|
mangaList = mangaList,
|
||||||
getMangaState = getMangaState,
|
|
||||||
columns = columns,
|
columns = columns,
|
||||||
contentPadding = contentPadding,
|
contentPadding = contentPadding,
|
||||||
onMangaClick = onMangaClick,
|
onMangaClick = onMangaClick,
|
||||||
|
@ -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)
|
|
||||||
}
|
|
@ -23,7 +23,6 @@ import androidx.compose.material.icons.outlined.History
|
|||||||
import androidx.compose.material.icons.outlined.Settings
|
import androidx.compose.material.icons.outlined.Settings
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.Divider
|
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
@ -39,40 +38,43 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalUriHandler
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import eu.kanade.domain.extension.interactor.ExtensionSourceItem
|
||||||
import eu.kanade.presentation.browse.components.ExtensionIcon
|
import eu.kanade.presentation.browse.components.ExtensionIcon
|
||||||
import eu.kanade.presentation.components.AppBar
|
import eu.kanade.presentation.components.AppBar
|
||||||
import eu.kanade.presentation.components.AppBarActions
|
import eu.kanade.presentation.components.AppBarActions
|
||||||
import eu.kanade.presentation.components.DIVIDER_ALPHA
|
import eu.kanade.presentation.components.DIVIDER_ALPHA
|
||||||
import eu.kanade.presentation.components.Divider
|
import eu.kanade.presentation.components.Divider
|
||||||
import eu.kanade.presentation.components.EmptyScreen
|
import eu.kanade.presentation.components.EmptyScreen
|
||||||
import eu.kanade.presentation.components.LoadingScreen
|
|
||||||
import eu.kanade.presentation.components.Scaffold
|
import eu.kanade.presentation.components.Scaffold
|
||||||
import eu.kanade.presentation.components.ScrollbarLazyColumn
|
import eu.kanade.presentation.components.ScrollbarLazyColumn
|
||||||
import eu.kanade.presentation.components.WarningBanner
|
import eu.kanade.presentation.components.WarningBanner
|
||||||
import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget
|
import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget
|
||||||
import eu.kanade.presentation.more.settings.widget.TrailingWidgetBuffer
|
import eu.kanade.presentation.more.settings.widget.TrailingWidgetBuffer
|
||||||
import eu.kanade.presentation.util.horizontalPadding
|
import eu.kanade.presentation.util.padding
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.extension.model.Extension
|
import eu.kanade.tachiyomi.extension.model.Extension
|
||||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||||
import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionDetailsPresenter
|
import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionDetailsState
|
||||||
import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionSourceItem
|
|
||||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ExtensionDetailsScreen(
|
fun ExtensionDetailsScreen(
|
||||||
navigateUp: () -> Unit,
|
navigateUp: () -> Unit,
|
||||||
presenter: ExtensionDetailsPresenter,
|
state: ExtensionDetailsState,
|
||||||
onClickSourcePreferences: (sourceId: Long) -> Unit,
|
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(
|
Scaffold(
|
||||||
topBar = { scrollBehavior ->
|
topBar = { scrollBehavior ->
|
||||||
AppBar(
|
AppBar(
|
||||||
@ -81,19 +83,19 @@ fun ExtensionDetailsScreen(
|
|||||||
actions = {
|
actions = {
|
||||||
AppBarActions(
|
AppBarActions(
|
||||||
actions = buildList {
|
actions = buildList {
|
||||||
if (presenter.extension?.isUnofficial == false) {
|
if (state.extension?.isUnofficial == false) {
|
||||||
add(
|
add(
|
||||||
AppBar.Action(
|
AppBar.Action(
|
||||||
title = stringResource(R.string.whats_new),
|
title = stringResource(R.string.whats_new),
|
||||||
icon = Icons.Outlined.History,
|
icon = Icons.Outlined.History,
|
||||||
onClick = { uriHandler.openUri(presenter.getChangelogUrl()) },
|
onClick = onClickWhatsNew,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
add(
|
add(
|
||||||
AppBar.Action(
|
AppBar.Action(
|
||||||
title = stringResource(R.string.action_faq_and_guides),
|
title = stringResource(R.string.action_faq_and_guides),
|
||||||
icon = Icons.Outlined.HelpOutline,
|
icon = Icons.Outlined.HelpOutline,
|
||||||
onClick = { uriHandler.openUri(presenter.getReadmeUrl()) },
|
onClick = onClickReadme,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -101,15 +103,15 @@ fun ExtensionDetailsScreen(
|
|||||||
listOf(
|
listOf(
|
||||||
AppBar.OverflowAction(
|
AppBar.OverflowAction(
|
||||||
title = stringResource(R.string.action_enable_all),
|
title = stringResource(R.string.action_enable_all),
|
||||||
onClick = { presenter.toggleSources(true) },
|
onClick = onClickEnableAll,
|
||||||
),
|
),
|
||||||
AppBar.OverflowAction(
|
AppBar.OverflowAction(
|
||||||
title = stringResource(R.string.action_disable_all),
|
title = stringResource(R.string.action_disable_all),
|
||||||
onClick = { presenter.toggleSources(false) },
|
onClick = onClickDisableAll,
|
||||||
),
|
),
|
||||||
AppBar.OverflowAction(
|
AppBar.OverflowAction(
|
||||||
title = stringResource(R.string.pref_clear_cookies),
|
title = stringResource(R.string.pref_clear_cookies),
|
||||||
onClick = { presenter.clearCookies() },
|
onClick = onClickClearCookies,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@ -120,77 +122,85 @@ fun ExtensionDetailsScreen(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
) { paddingValues ->
|
) { 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
|
@Composable
|
||||||
private fun ExtensionDetails(
|
private fun ExtensionDetails(
|
||||||
contentPadding: PaddingValues,
|
contentPadding: PaddingValues,
|
||||||
presenter: ExtensionDetailsPresenter,
|
extension: Extension.Installed,
|
||||||
|
sources: List<ExtensionSourceItem>,
|
||||||
onClickSourcePreferences: (sourceId: Long) -> Unit,
|
onClickSourcePreferences: (sourceId: Long) -> Unit,
|
||||||
|
onClickUninstall: () -> Unit,
|
||||||
|
onClickSource: (sourceId: Long) -> Unit,
|
||||||
) {
|
) {
|
||||||
when {
|
val context = LocalContext.current
|
||||||
presenter.isLoading -> LoadingScreen()
|
var showNsfwWarning by remember { mutableStateOf(false) }
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
ScrollbarLazyColumn(
|
||||||
|
contentPadding = contentPadding,
|
||||||
|
) {
|
||||||
|
when {
|
||||||
|
extension.isUnofficial ->
|
||||||
item {
|
item {
|
||||||
DetailsHeader(
|
WarningBanner(R.string.unofficial_extension_message)
|
||||||
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
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
extension.isObsolete ->
|
||||||
items(
|
item {
|
||||||
items = presenter.sources,
|
WarningBanner(R.string.obsolete_extension_message)
|
||||||
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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (showNsfwWarning) {
|
||||||
|
NsfwWarningDialog(
|
||||||
|
onClickConfirm = {
|
||||||
|
showNsfwWarning = false
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -208,10 +218,10 @@ private fun DetailsHeader(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(
|
.padding(
|
||||||
start = horizontalPadding,
|
start = MaterialTheme.padding.medium,
|
||||||
end = horizontalPadding,
|
end = MaterialTheme.padding.medium,
|
||||||
top = 16.dp,
|
top = MaterialTheme.padding.medium,
|
||||||
bottom = 8.dp,
|
bottom = MaterialTheme.padding.small,
|
||||||
),
|
),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
) {
|
) {
|
||||||
@ -225,6 +235,7 @@ private fun DetailsHeader(
|
|||||||
Text(
|
Text(
|
||||||
text = extension.name,
|
text = extension.name,
|
||||||
style = MaterialTheme.typography.headlineSmall,
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
)
|
)
|
||||||
|
|
||||||
val strippedPkgName = extension.pkgName.substringAfter("eu.kanade.tachiyomi.extension.")
|
val strippedPkgName = extension.pkgName.substringAfter("eu.kanade.tachiyomi.extension.")
|
||||||
@ -239,8 +250,8 @@ private fun DetailsHeader(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(
|
.padding(
|
||||||
horizontal = horizontalPadding * 2,
|
horizontal = MaterialTheme.padding.extraLarge,
|
||||||
vertical = 8.dp,
|
vertical = MaterialTheme.padding.small,
|
||||||
),
|
),
|
||||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
@ -277,10 +288,10 @@ private fun DetailsHeader(
|
|||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.padding(
|
modifier = Modifier.padding(
|
||||||
start = horizontalPadding,
|
start = MaterialTheme.padding.medium,
|
||||||
end = horizontalPadding,
|
end = MaterialTheme.padding.medium,
|
||||||
top = 8.dp,
|
top = MaterialTheme.padding.small,
|
||||||
bottom = 16.dp,
|
bottom = MaterialTheme.padding.medium,
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
|
@ -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())
|
|
||||||
}
|
|
@ -4,28 +4,24 @@ import androidx.compose.foundation.layout.PaddingValues
|
|||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import eu.kanade.presentation.components.AppBar
|
import eu.kanade.presentation.components.AppBar
|
||||||
import eu.kanade.presentation.components.EmptyScreen
|
import eu.kanade.presentation.components.EmptyScreen
|
||||||
import eu.kanade.presentation.components.FastScrollLazyColumn
|
import eu.kanade.presentation.components.FastScrollLazyColumn
|
||||||
import eu.kanade.presentation.components.LoadingScreen
|
|
||||||
import eu.kanade.presentation.components.Scaffold
|
import eu.kanade.presentation.components.Scaffold
|
||||||
import eu.kanade.presentation.more.settings.widget.SwitchPreferenceWidget
|
import eu.kanade.presentation.more.settings.widget.SwitchPreferenceWidget
|
||||||
import eu.kanade.tachiyomi.R
|
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.LocaleHelper
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ExtensionFilterScreen(
|
fun ExtensionFilterScreen(
|
||||||
navigateUp: () -> Unit,
|
navigateUp: () -> Unit,
|
||||||
presenter: ExtensionFilterPresenter,
|
state: ExtensionFilterState.Success,
|
||||||
|
onClickToggle: (String) -> Unit,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = { scrollBehavior ->
|
topBar = { scrollBehavior ->
|
||||||
AppBar(
|
AppBar(
|
||||||
@ -35,50 +31,37 @@ fun ExtensionFilterScreen(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
) { contentPadding ->
|
) { contentPadding ->
|
||||||
when {
|
if (state.isEmpty) {
|
||||||
presenter.isLoading -> LoadingScreen()
|
EmptyScreen(
|
||||||
presenter.isEmpty -> EmptyScreen(
|
|
||||||
textResource = R.string.empty_screen,
|
textResource = R.string.empty_screen,
|
||||||
modifier = Modifier.padding(contentPadding),
|
modifier = Modifier.padding(contentPadding),
|
||||||
)
|
)
|
||||||
else -> ExtensionFilterContent(
|
return@Scaffold
|
||||||
contentPadding = contentPadding,
|
|
||||||
state = presenter,
|
|
||||||
onClickLang = {
|
|
||||||
presenter.toggleLanguage(it)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
presenter.events.collectLatest {
|
|
||||||
when (it) {
|
|
||||||
ExtensionFilterPresenter.Event.FailedFetchingLanguages -> {
|
|
||||||
context.toast(R.string.internal_error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
ExtensionFilterContent(
|
||||||
|
contentPadding = contentPadding,
|
||||||
|
state = state,
|
||||||
|
onClickLang = onClickToggle,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ExtensionFilterContent(
|
private fun ExtensionFilterContent(
|
||||||
contentPadding: PaddingValues,
|
contentPadding: PaddingValues,
|
||||||
state: ExtensionFilterState,
|
state: ExtensionFilterState.Success,
|
||||||
onClickLang: (String) -> Unit,
|
onClickLang: (String) -> Unit,
|
||||||
) {
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
FastScrollLazyColumn(
|
FastScrollLazyColumn(
|
||||||
contentPadding = contentPadding,
|
contentPadding = contentPadding,
|
||||||
) {
|
) {
|
||||||
items(
|
items(state.languages) { language ->
|
||||||
items = state.items,
|
|
||||||
) { model ->
|
|
||||||
val lang = model.lang
|
|
||||||
SwitchPreferenceWidget(
|
SwitchPreferenceWidget(
|
||||||
modifier = Modifier.animateItemPlacement(),
|
modifier = Modifier.animateItemPlacement(),
|
||||||
title = LocaleHelper.getSourceDisplayName(lang, LocalContext.current),
|
title = LocaleHelper.getSourceDisplayName(language, context),
|
||||||
checked = model.enabled,
|
checked = language in state.enabledLanguages,
|
||||||
onCheckedChanged = { onClickLang(lang) },
|
onCheckedChanged = { onClickLang(language) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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() }
|
|
||||||
}
|
|
@ -2,6 +2,7 @@ package eu.kanade.presentation.browse
|
|||||||
|
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.compose.animation.core.animateDpAsState
|
import androidx.compose.animation.core.animateDpAsState
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.combinedClickable
|
import androidx.compose.foundation.combinedClickable
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@ -31,6 +32,7 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalUriHandler
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@ -40,24 +42,27 @@ import eu.kanade.presentation.browse.components.ExtensionIcon
|
|||||||
import eu.kanade.presentation.components.EmptyScreen
|
import eu.kanade.presentation.components.EmptyScreen
|
||||||
import eu.kanade.presentation.components.FastScrollLazyColumn
|
import eu.kanade.presentation.components.FastScrollLazyColumn
|
||||||
import eu.kanade.presentation.components.LoadingScreen
|
import eu.kanade.presentation.components.LoadingScreen
|
||||||
import eu.kanade.presentation.components.SwipeRefresh
|
import eu.kanade.presentation.components.PullRefresh
|
||||||
|
import eu.kanade.presentation.components.WarningBanner
|
||||||
import eu.kanade.presentation.manga.components.DotSeparatorNoSpaceText
|
import eu.kanade.presentation.manga.components.DotSeparatorNoSpaceText
|
||||||
import eu.kanade.presentation.theme.header
|
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.plus
|
||||||
import eu.kanade.presentation.util.secondaryItemAlpha
|
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.R
|
||||||
import eu.kanade.tachiyomi.extension.model.Extension
|
import eu.kanade.tachiyomi.extension.model.Extension
|
||||||
import eu.kanade.tachiyomi.extension.model.InstallStep
|
import eu.kanade.tachiyomi.extension.model.InstallStep
|
||||||
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionUiModel
|
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.DeviceUtil
|
||||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ExtensionScreen(
|
fun ExtensionScreen(
|
||||||
presenter: ExtensionsPresenter,
|
state: ExtensionsState,
|
||||||
contentPadding: PaddingValues,
|
contentPadding: PaddingValues,
|
||||||
|
searchQuery: String? = null,
|
||||||
onLongClickItem: (Extension) -> Unit,
|
onLongClickItem: (Extension) -> Unit,
|
||||||
onClickItemCancel: (Extension) -> Unit,
|
onClickItemCancel: (Extension) -> Unit,
|
||||||
onInstallExtension: (Extension.Available) -> Unit,
|
onInstallExtension: (Extension.Available) -> Unit,
|
||||||
@ -68,20 +73,27 @@ fun ExtensionScreen(
|
|||||||
onClickUpdateAll: () -> Unit,
|
onClickUpdateAll: () -> Unit,
|
||||||
onRefresh: () -> Unit,
|
onRefresh: () -> Unit,
|
||||||
) {
|
) {
|
||||||
SwipeRefresh(
|
PullRefresh(
|
||||||
refreshing = presenter.isRefreshing,
|
refreshing = state.isRefreshing,
|
||||||
onRefresh = onRefresh,
|
onRefresh = onRefresh,
|
||||||
enabled = !presenter.isLoading,
|
enabled = !state.isLoading,
|
||||||
) {
|
) {
|
||||||
when {
|
when {
|
||||||
presenter.isLoading -> LoadingScreen()
|
state.isLoading -> LoadingScreen(modifier = Modifier.padding(contentPadding))
|
||||||
presenter.isEmpty -> EmptyScreen(
|
state.isEmpty -> {
|
||||||
textResource = R.string.empty_screen,
|
val msg = if (!searchQuery.isNullOrEmpty()) {
|
||||||
modifier = Modifier.padding(contentPadding),
|
R.string.no_results_found
|
||||||
)
|
} else {
|
||||||
|
R.string.empty_screen
|
||||||
|
}
|
||||||
|
EmptyScreen(
|
||||||
|
textResource = msg,
|
||||||
|
modifier = Modifier.padding(contentPadding),
|
||||||
|
)
|
||||||
|
}
|
||||||
else -> {
|
else -> {
|
||||||
ExtensionContent(
|
ExtensionContent(
|
||||||
state = presenter,
|
state = state,
|
||||||
contentPadding = contentPadding,
|
contentPadding = contentPadding,
|
||||||
onLongClickItem = onLongClickItem,
|
onLongClickItem = onLongClickItem,
|
||||||
onClickItemCancel = onClickItemCancel,
|
onClickItemCancel = onClickItemCancel,
|
||||||
@ -111,10 +123,29 @@ private fun ExtensionContent(
|
|||||||
onClickUpdateAll: () -> Unit,
|
onClickUpdateAll: () -> Unit,
|
||||||
) {
|
) {
|
||||||
var trustState by remember { mutableStateOf<Extension.Untrusted?>(null) }
|
var trustState by remember { mutableStateOf<Extension.Untrusted?>(null) }
|
||||||
|
val showMiuiWarning = DeviceUtil.isMiui && !DeviceUtil.isMiuiOptimizationDisabled()
|
||||||
|
val uriHandler = LocalUriHandler.current
|
||||||
|
|
||||||
FastScrollLazyColumn(
|
FastScrollLazyColumn(
|
||||||
contentPadding = contentPadding + topPaddingValues,
|
contentPadding = if (showMiuiWarning) {
|
||||||
|
contentPadding
|
||||||
|
} else {
|
||||||
|
contentPadding + topSmallPaddingValues
|
||||||
|
},
|
||||||
) {
|
) {
|
||||||
|
if (showMiuiWarning) {
|
||||||
|
item {
|
||||||
|
WarningBanner(
|
||||||
|
textRes = R.string.ext_miui_warning,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(bottom = MaterialTheme.padding.small)
|
||||||
|
.clickable {
|
||||||
|
uriHandler.openUri("https://tachiyomi.org/extensions")
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
items(
|
items(
|
||||||
items = state.items,
|
items = state.items,
|
||||||
contentType = {
|
contentType = {
|
||||||
@ -272,7 +303,7 @@ private fun ExtensionItemContent(
|
|||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier.padding(start = horizontalPadding),
|
modifier = modifier.padding(start = MaterialTheme.padding.medium),
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = extension.name,
|
text = extension.name,
|
||||||
@ -396,7 +427,7 @@ private fun ExtensionHeader(
|
|||||||
action: @Composable RowScope.() -> Unit = {},
|
action: @Composable RowScope.() -> Unit = {},
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = modifier.padding(horizontal = horizontalPadding),
|
modifier = modifier.padding(horizontal = MaterialTheme.padding.medium),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
Text(
|
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.domain.manga.model.Manga
|
||||||
|
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
|
||||||
|
|
||||||
|
@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 {
|
||||||
|
GlobalSearchResultItem(
|
||||||
|
title = source.name,
|
||||||
|
subtitle = LocaleHelper.getDisplayName(source.lang),
|
||||||
|
onClick = { onClickSource(source) },
|
||||||
|
) {
|
||||||
|
when (result) {
|
||||||
|
is SearchItemResult.Error -> {
|
||||||
|
GlobalSearchErrorResultItem(message = result.throwable.message)
|
||||||
|
}
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -4,31 +4,24 @@ import androidx.compose.foundation.layout.PaddingValues
|
|||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import eu.kanade.domain.manga.model.Manga
|
import eu.kanade.domain.manga.model.Manga
|
||||||
import eu.kanade.presentation.components.AppBar
|
import eu.kanade.presentation.components.AppBar
|
||||||
import eu.kanade.presentation.components.EmptyScreen
|
import eu.kanade.presentation.components.EmptyScreen
|
||||||
import eu.kanade.presentation.components.FastScrollLazyColumn
|
import eu.kanade.presentation.components.FastScrollLazyColumn
|
||||||
import eu.kanade.presentation.components.LoadingScreen
|
|
||||||
import eu.kanade.presentation.components.Scaffold
|
import eu.kanade.presentation.components.Scaffold
|
||||||
import eu.kanade.presentation.manga.components.BaseMangaListItem
|
import eu.kanade.presentation.manga.components.BaseMangaListItem
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.ui.browse.migration.manga.MigrateMangaPresenter
|
import eu.kanade.tachiyomi.ui.browse.migration.manga.MigrateMangaState
|
||||||
import eu.kanade.tachiyomi.ui.browse.migration.manga.MigrateMangaPresenter.Event
|
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MigrateMangaScreen(
|
fun MigrateMangaScreen(
|
||||||
navigateUp: () -> Unit,
|
navigateUp: () -> Unit,
|
||||||
title: String?,
|
title: String?,
|
||||||
presenter: MigrateMangaPresenter,
|
state: MigrateMangaState,
|
||||||
onClickItem: (Manga) -> Unit,
|
onClickItem: (Manga) -> Unit,
|
||||||
onClickCover: (Manga) -> Unit,
|
onClickCover: (Manga) -> Unit,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = { scrollBehavior ->
|
topBar = { scrollBehavior ->
|
||||||
AppBar(
|
AppBar(
|
||||||
@ -38,30 +31,20 @@ fun MigrateMangaScreen(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
) { contentPadding ->
|
) { contentPadding ->
|
||||||
when {
|
if (state.isEmpty) {
|
||||||
presenter.isLoading -> LoadingScreen()
|
EmptyScreen(
|
||||||
presenter.isEmpty -> EmptyScreen(
|
|
||||||
textResource = R.string.empty_screen,
|
textResource = R.string.empty_screen,
|
||||||
modifier = Modifier.padding(contentPadding),
|
modifier = Modifier.padding(contentPadding),
|
||||||
)
|
)
|
||||||
else -> {
|
return@Scaffold
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MigrateMangaContent(
|
||||||
|
contentPadding = contentPadding,
|
||||||
|
state = state,
|
||||||
|
onClickItem = onClickItem,
|
||||||
|
onClickCover = onClickCover,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -75,7 +58,7 @@ private fun MigrateMangaContent(
|
|||||||
FastScrollLazyColumn(
|
FastScrollLazyColumn(
|
||||||
contentPadding = contentPadding,
|
contentPadding = contentPadding,
|
||||||
) {
|
) {
|
||||||
items(state.items) { manga ->
|
items(state.titles) { manga ->
|
||||||
MigrateMangaItem(
|
MigrateMangaItem(
|
||||||
manga = manga,
|
manga = manga,
|
||||||
onClickItem = onClickItem,
|
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.domain.manga.model.Manga
|
||||||
|
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
|
||||||
|
|
||||||
|
@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 {
|
||||||
|
GlobalSearchResultItem(
|
||||||
|
title = if (source.id == sourceId) "▶ ${source.name}" else source.name,
|
||||||
|
subtitle = LocaleHelper.getDisplayName(source.lang),
|
||||||
|
onClick = { onClickSource(source) },
|
||||||
|
) {
|
||||||
|
when (result) {
|
||||||
|
is SearchItemResult.Error -> {
|
||||||
|
GlobalSearchErrorResultItem(message = result.throwable.message)
|
||||||
|
}
|
||||||
|
SearchItemResult.Loading -> {
|
||||||
|
GlobalSearchLoadingResultItem()
|
||||||
|
}
|
||||||
|
is SearchItemResult.Success -> {
|
||||||
|
if (result.isEmpty) {
|
||||||
|
GlobalSearchEmptyResultItem()
|
||||||
|
return@GlobalSearchResultItem
|
||||||
|
}
|
||||||
|
|
||||||
|
GlobalSearchCardRow(
|
||||||
|
titles = result.result,
|
||||||
|
getManga = { getManga(source, it) },
|
||||||
|
onClick = onClickItem,
|
||||||
|
onLongClick = onLongClickItem,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -34,40 +34,42 @@ import eu.kanade.presentation.components.LoadingScreen
|
|||||||
import eu.kanade.presentation.components.ScrollbarLazyColumn
|
import eu.kanade.presentation.components.ScrollbarLazyColumn
|
||||||
import eu.kanade.presentation.components.Scroller.STICKY_HEADER_KEY_PREFIX
|
import eu.kanade.presentation.components.Scroller.STICKY_HEADER_KEY_PREFIX
|
||||||
import eu.kanade.presentation.theme.header
|
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.plus
|
||||||
import eu.kanade.presentation.util.secondaryItemAlpha
|
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.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 eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MigrateSourceScreen(
|
fun MigrateSourceScreen(
|
||||||
presenter: MigrationSourcesPresenter,
|
state: MigrateSourceState,
|
||||||
contentPadding: PaddingValues,
|
contentPadding: PaddingValues,
|
||||||
onClickItem: (Source) -> Unit,
|
onClickItem: (Source) -> Unit,
|
||||||
|
onToggleSortingDirection: () -> Unit,
|
||||||
|
onToggleSortingMode: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
when {
|
when {
|
||||||
presenter.isLoading -> LoadingScreen()
|
state.isLoading -> LoadingScreen(modifier = Modifier.padding(contentPadding))
|
||||||
presenter.isEmpty -> EmptyScreen(
|
state.isEmpty -> EmptyScreen(
|
||||||
textResource = R.string.information_empty_library,
|
textResource = R.string.information_empty_library,
|
||||||
modifier = Modifier.padding(contentPadding),
|
modifier = Modifier.padding(contentPadding),
|
||||||
)
|
)
|
||||||
else ->
|
else ->
|
||||||
MigrateSourceList(
|
MigrateSourceList(
|
||||||
list = presenter.items,
|
list = state.items,
|
||||||
contentPadding = contentPadding,
|
contentPadding = contentPadding,
|
||||||
onClickItem = onClickItem,
|
onClickItem = onClickItem,
|
||||||
onLongClickItem = { source ->
|
onLongClickItem = { source ->
|
||||||
val sourceId = source.id.toString()
|
val sourceId = source.id.toString()
|
||||||
context.copyToClipboard(sourceId, sourceId)
|
context.copyToClipboard(sourceId, sourceId)
|
||||||
},
|
},
|
||||||
sortingMode = presenter.sortingMode,
|
sortingMode = state.sortingMode,
|
||||||
onToggleSortingMode = { presenter.toggleSortingMode() },
|
onToggleSortingMode = onToggleSortingMode,
|
||||||
sortingDirection = presenter.sortingDirection,
|
sortingDirection = state.sortingDirection,
|
||||||
onToggleSortingDirection = { presenter.toggleSortingDirection() },
|
onToggleSortingDirection = onToggleSortingDirection,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -84,13 +86,13 @@ private fun MigrateSourceList(
|
|||||||
onToggleSortingDirection: () -> Unit,
|
onToggleSortingDirection: () -> Unit,
|
||||||
) {
|
) {
|
||||||
ScrollbarLazyColumn(
|
ScrollbarLazyColumn(
|
||||||
contentPadding = contentPadding + topPaddingValues,
|
contentPadding = contentPadding + topSmallPaddingValues,
|
||||||
) {
|
) {
|
||||||
stickyHeader(key = STICKY_HEADER_KEY_PREFIX) {
|
stickyHeader(key = STICKY_HEADER_KEY_PREFIX) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.background(MaterialTheme.colorScheme.background)
|
.background(MaterialTheme.colorScheme.background)
|
||||||
.padding(start = horizontalPadding),
|
.padding(start = MaterialTheme.padding.medium),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
@ -152,7 +154,7 @@ private fun MigrateSourceItem(
|
|||||||
content = { _, sourceLangString ->
|
content = { _, sourceLangString ->
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(horizontal = horizontalPadding)
|
.padding(horizontal = MaterialTheme.padding.medium)
|
||||||
.weight(1f),
|
.weight(1f),
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
|
@ -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)
|
|
||||||
}
|
|
@ -1,72 +0,0 @@
|
|||||||
package eu.kanade.presentation.browse
|
|
||||||
|
|
||||||
import androidx.compose.material3.SnackbarHost
|
|
||||||
import androidx.compose.material3.SnackbarHostState
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.platform.LocalUriHandler
|
|
||||||
import androidx.paging.compose.collectAsLazyPagingItems
|
|
||||||
import eu.kanade.domain.manga.model.Manga
|
|
||||||
import eu.kanade.presentation.components.Scaffold
|
|
||||||
import eu.kanade.presentation.components.SearchToolbar
|
|
||||||
import eu.kanade.tachiyomi.source.LocalSource
|
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourcePresenter
|
|
||||||
import eu.kanade.tachiyomi.ui.more.MoreController
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun SourceSearchScreen(
|
|
||||||
presenter: BrowseSourcePresenter,
|
|
||||||
navigateUp: () -> Unit,
|
|
||||||
onFabClick: () -> Unit,
|
|
||||||
onMangaClick: (Manga) -> Unit,
|
|
||||||
onWebViewClick: () -> Unit,
|
|
||||||
) {
|
|
||||||
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 = { scrollBehavior ->
|
|
||||||
SearchToolbar(
|
|
||||||
searchQuery = presenter.searchQuery ?: "",
|
|
||||||
onChangeSearchQuery = { presenter.searchQuery = it },
|
|
||||||
onClickCloseSearch = navigateUp,
|
|
||||||
onSearch = { presenter.search(it) },
|
|
||||||
scrollBehavior = scrollBehavior,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
floatingActionButton = {
|
|
||||||
BrowseSourceFloatingActionButton(
|
|
||||||
isVisible = presenter.filters.isNotEmpty(),
|
|
||||||
onFabClick = onFabClick,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
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 = onMangaClick,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
@ -5,7 +5,6 @@ import androidx.compose.foundation.layout.padding
|
|||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.material3.Checkbox
|
import androidx.compose.material3.Checkbox
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
@ -14,24 +13,19 @@ import eu.kanade.presentation.browse.components.BaseSourceItem
|
|||||||
import eu.kanade.presentation.components.AppBar
|
import eu.kanade.presentation.components.AppBar
|
||||||
import eu.kanade.presentation.components.EmptyScreen
|
import eu.kanade.presentation.components.EmptyScreen
|
||||||
import eu.kanade.presentation.components.FastScrollLazyColumn
|
import eu.kanade.presentation.components.FastScrollLazyColumn
|
||||||
import eu.kanade.presentation.components.LoadingScreen
|
|
||||||
import eu.kanade.presentation.components.Scaffold
|
import eu.kanade.presentation.components.Scaffold
|
||||||
import eu.kanade.presentation.more.settings.widget.SwitchPreferenceWidget
|
import eu.kanade.presentation.more.settings.widget.SwitchPreferenceWidget
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.FilterUiModel
|
import eu.kanade.tachiyomi.ui.browse.source.SourcesFilterState
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.SourcesFilterPresenter
|
|
||||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SourcesFilterScreen(
|
fun SourcesFilterScreen(
|
||||||
navigateUp: () -> Unit,
|
navigateUp: () -> Unit,
|
||||||
presenter: SourcesFilterPresenter,
|
state: SourcesFilterState.Success,
|
||||||
onClickLang: (String) -> Unit,
|
onClickLanguage: (String) -> Unit,
|
||||||
onClickSource: (Source) -> Unit,
|
onClickSource: (Source) -> Unit,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = { scrollBehavior ->
|
topBar = { scrollBehavior ->
|
||||||
AppBar(
|
AppBar(
|
||||||
@ -41,69 +35,55 @@ fun SourcesFilterScreen(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
) { contentPadding ->
|
) { contentPadding ->
|
||||||
when {
|
if (state.isEmpty) {
|
||||||
presenter.isLoading -> LoadingScreen()
|
EmptyScreen(
|
||||||
presenter.isEmpty -> EmptyScreen(
|
|
||||||
textResource = R.string.source_filter_empty_screen,
|
textResource = R.string.source_filter_empty_screen,
|
||||||
modifier = Modifier.padding(contentPadding),
|
modifier = Modifier.padding(contentPadding),
|
||||||
)
|
)
|
||||||
else -> {
|
return@Scaffold
|
||||||
SourcesFilterContent(
|
|
||||||
contentPadding = contentPadding,
|
|
||||||
state = presenter,
|
|
||||||
onClickLang = onClickLang,
|
|
||||||
onClickSource = onClickSource,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
presenter.events.collectLatest { event ->
|
|
||||||
when (event) {
|
|
||||||
SourcesFilterPresenter.Event.FailedFetchingLanguages -> {
|
|
||||||
context.toast(R.string.internal_error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
SourcesFilterContent(
|
||||||
|
contentPadding = contentPadding,
|
||||||
|
state = state,
|
||||||
|
onClickLanguage = onClickLanguage,
|
||||||
|
onClickSource = onClickSource,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun SourcesFilterContent(
|
private fun SourcesFilterContent(
|
||||||
contentPadding: PaddingValues,
|
contentPadding: PaddingValues,
|
||||||
state: SourcesFilterState,
|
state: SourcesFilterState.Success,
|
||||||
onClickLang: (String) -> Unit,
|
onClickLanguage: (String) -> Unit,
|
||||||
onClickSource: (Source) -> Unit,
|
onClickSource: (Source) -> Unit,
|
||||||
) {
|
) {
|
||||||
FastScrollLazyColumn(
|
FastScrollLazyColumn(
|
||||||
contentPadding = contentPadding,
|
contentPadding = contentPadding,
|
||||||
) {
|
) {
|
||||||
items(
|
state.items.forEach { (language, sources) ->
|
||||||
items = state.items,
|
val enabled = language in state.enabledLanguages
|
||||||
contentType = {
|
item(
|
||||||
when (it) {
|
key = language.hashCode(),
|
||||||
is FilterUiModel.Header -> "header"
|
contentType = "source-filter-header",
|
||||||
is FilterUiModel.Item -> "item"
|
) {
|
||||||
}
|
SourcesFilterHeader(
|
||||||
},
|
|
||||||
key = {
|
|
||||||
when (it) {
|
|
||||||
is FilterUiModel.Header -> it.hashCode()
|
|
||||||
is FilterUiModel.Item -> "source-filter-${it.source.key()}"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
) { model ->
|
|
||||||
when (model) {
|
|
||||||
is FilterUiModel.Header -> SourcesFilterHeader(
|
|
||||||
modifier = Modifier.animateItemPlacement(),
|
modifier = Modifier.animateItemPlacement(),
|
||||||
language = model.language,
|
language = language,
|
||||||
enabled = model.enabled,
|
enabled = enabled,
|
||||||
onClickItem = onClickLang,
|
onClickItem = onClickLanguage,
|
||||||
)
|
)
|
||||||
is FilterUiModel.Item -> SourcesFilterItem(
|
}
|
||||||
|
if (!enabled) return@forEach
|
||||||
|
items(
|
||||||
|
items = sources,
|
||||||
|
key = { "source-filter-${it.key()}" },
|
||||||
|
contentType = { "source-filter-item" },
|
||||||
|
) { source ->
|
||||||
|
SourcesFilterItem(
|
||||||
modifier = Modifier.animateItemPlacement(),
|
modifier = Modifier.animateItemPlacement(),
|
||||||
source = model.source,
|
source = source,
|
||||||
enabled = model.enabled,
|
enabled = "${source.id}" !in state.disabledSources,
|
||||||
onClickItem = onClickSource,
|
onClickItem = onClickSource,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -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.tachiyomi.ui.browse.source.FilterUiModel
|
|
||||||
|
|
||||||
interface SourcesFilterState {
|
|
||||||
val isLoading: Boolean
|
|
||||||
val items: List<FilterUiModel>
|
|
||||||
val isEmpty: Boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
fun SourcesFilterState(): SourcesFilterState {
|
|
||||||
return SourcesFilterStateImpl()
|
|
||||||
}
|
|
||||||
|
|
||||||
class SourcesFilterStateImpl : SourcesFilterState {
|
|
||||||
override var isLoading: Boolean by mutableStateOf(true)
|
|
||||||
override var items: List<FilterUiModel> by mutableStateOf(emptyList())
|
|
||||||
override val isEmpty: Boolean by derivedStateOf { items.isEmpty() }
|
|
||||||
}
|
|
@ -17,12 +17,10 @@ import androidx.compose.material3.MaterialTheme
|
|||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import eu.kanade.domain.source.interactor.GetRemoteManga
|
|
||||||
import eu.kanade.domain.source.model.Pin
|
import eu.kanade.domain.source.model.Pin
|
||||||
import eu.kanade.domain.source.model.Source
|
import eu.kanade.domain.source.model.Source
|
||||||
import eu.kanade.presentation.browse.components.BaseSourceItem
|
import eu.kanade.presentation.browse.components.BaseSourceItem
|
||||||
@ -30,113 +28,69 @@ import eu.kanade.presentation.components.EmptyScreen
|
|||||||
import eu.kanade.presentation.components.LoadingScreen
|
import eu.kanade.presentation.components.LoadingScreen
|
||||||
import eu.kanade.presentation.components.ScrollbarLazyColumn
|
import eu.kanade.presentation.components.ScrollbarLazyColumn
|
||||||
import eu.kanade.presentation.theme.header
|
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.plus
|
||||||
import eu.kanade.presentation.util.topPaddingValues
|
import eu.kanade.presentation.util.topSmallPaddingValues
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.source.LocalSource
|
import eu.kanade.tachiyomi.source.LocalSource
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.SourcesPresenter
|
import eu.kanade.tachiyomi.ui.browse.source.SourcesState
|
||||||
|
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreenModel.Listing
|
||||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SourcesScreen(
|
fun SourcesScreen(
|
||||||
presenter: SourcesPresenter,
|
state: SourcesState,
|
||||||
contentPadding: PaddingValues,
|
contentPadding: PaddingValues,
|
||||||
onClickItem: (Source, String) -> Unit,
|
onClickItem: (Source, Listing) -> Unit,
|
||||||
onClickDisable: (Source) -> Unit,
|
|
||||||
onClickPin: (Source) -> Unit,
|
onClickPin: (Source) -> Unit,
|
||||||
|
onLongClickItem: (Source) -> Unit,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
|
||||||
when {
|
when {
|
||||||
presenter.isLoading -> LoadingScreen()
|
state.isLoading -> LoadingScreen(modifier = Modifier.padding(contentPadding))
|
||||||
presenter.isEmpty -> EmptyScreen(
|
state.isEmpty -> EmptyScreen(
|
||||||
textResource = R.string.source_empty_screen,
|
textResource = R.string.source_empty_screen,
|
||||||
modifier = Modifier.padding(contentPadding),
|
modifier = Modifier.padding(contentPadding),
|
||||||
)
|
)
|
||||||
else -> {
|
else -> {
|
||||||
SourceList(
|
ScrollbarLazyColumn(
|
||||||
state = presenter,
|
contentPadding = contentPadding + topSmallPaddingValues,
|
||||||
contentPadding = contentPadding,
|
) {
|
||||||
onClickItem = onClickItem,
|
items(
|
||||||
onClickDisable = onClickDisable,
|
items = state.items,
|
||||||
onClickPin = onClickPin,
|
contentType = {
|
||||||
)
|
when (it) {
|
||||||
}
|
is SourceUiModel.Header -> "header"
|
||||||
}
|
is SourceUiModel.Item -> "item"
|
||||||
LaunchedEffect(Unit) {
|
}
|
||||||
presenter.events.collectLatest { event ->
|
},
|
||||||
when (event) {
|
key = {
|
||||||
SourcesPresenter.Event.FailedFetchingSources -> {
|
when (it) {
|
||||||
context.toast(R.string.internal_error)
|
is SourceUiModel.Header -> it.hashCode()
|
||||||
|
is SourceUiModel.Item -> "source-${it.source.key()}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) { model ->
|
||||||
|
when (model) {
|
||||||
|
is SourceUiModel.Header -> {
|
||||||
|
SourceHeader(
|
||||||
|
modifier = Modifier.animateItemPlacement(),
|
||||||
|
language = model.language,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is SourceUiModel.Item -> SourceItem(
|
||||||
|
modifier = Modifier.animateItemPlacement(),
|
||||||
|
source = model.source,
|
||||||
|
onClickItem = onClickItem,
|
||||||
|
onLongClickItem = onLongClickItem,
|
||||||
|
onClickPin = onClickPin,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun SourceList(
|
|
||||||
state: SourcesState,
|
|
||||||
contentPadding: PaddingValues,
|
|
||||||
onClickItem: (Source, String) -> Unit,
|
|
||||||
onClickDisable: (Source) -> Unit,
|
|
||||||
onClickPin: (Source) -> Unit,
|
|
||||||
) {
|
|
||||||
ScrollbarLazyColumn(
|
|
||||||
contentPadding = contentPadding + topPaddingValues,
|
|
||||||
) {
|
|
||||||
items(
|
|
||||||
items = state.items,
|
|
||||||
contentType = {
|
|
||||||
when (it) {
|
|
||||||
is SourceUiModel.Header -> "header"
|
|
||||||
is SourceUiModel.Item -> "item"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
key = {
|
|
||||||
when (it) {
|
|
||||||
is SourceUiModel.Header -> it.hashCode()
|
|
||||||
is SourceUiModel.Item -> "source-${it.source.key()}"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
) { model ->
|
|
||||||
when (model) {
|
|
||||||
is SourceUiModel.Header -> {
|
|
||||||
SourceHeader(
|
|
||||||
modifier = Modifier.animateItemPlacement(),
|
|
||||||
language = model.language,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
is SourceUiModel.Item -> SourceItem(
|
|
||||||
modifier = Modifier.animateItemPlacement(),
|
|
||||||
source = model.source,
|
|
||||||
onClickItem = onClickItem,
|
|
||||||
onLongClickItem = { state.dialog = SourcesPresenter.Dialog(it) },
|
|
||||||
onClickPin = onClickPin,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.dialog != null) {
|
|
||||||
val source = state.dialog!!.source
|
|
||||||
SourceOptionsDialog(
|
|
||||||
source = source,
|
|
||||||
onClickPin = {
|
|
||||||
onClickPin(source)
|
|
||||||
state.dialog = null
|
|
||||||
},
|
|
||||||
onClickDisable = {
|
|
||||||
onClickDisable(source)
|
|
||||||
state.dialog = null
|
|
||||||
},
|
|
||||||
onDismiss = { state.dialog = null },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun SourceHeader(
|
private fun SourceHeader(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
@ -146,7 +100,7 @@ private fun SourceHeader(
|
|||||||
Text(
|
Text(
|
||||||
text = LocaleHelper.getSourceDisplayName(language, context),
|
text = LocaleHelper.getSourceDisplayName(language, context),
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.padding(horizontal = horizontalPadding, vertical = 8.dp),
|
.padding(horizontal = MaterialTheme.padding.medium, vertical = MaterialTheme.padding.small),
|
||||||
style = MaterialTheme.typography.header,
|
style = MaterialTheme.typography.header,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -155,18 +109,18 @@ private fun SourceHeader(
|
|||||||
private fun SourceItem(
|
private fun SourceItem(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
source: Source,
|
source: Source,
|
||||||
onClickItem: (Source, String) -> Unit,
|
onClickItem: (Source, Listing) -> Unit,
|
||||||
onLongClickItem: (Source) -> Unit,
|
onLongClickItem: (Source) -> Unit,
|
||||||
onClickPin: (Source) -> Unit,
|
onClickPin: (Source) -> Unit,
|
||||||
) {
|
) {
|
||||||
BaseSourceItem(
|
BaseSourceItem(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
source = source,
|
source = source,
|
||||||
onClickItem = { onClickItem(source, GetRemoteManga.QUERY_POPULAR) },
|
onClickItem = { onClickItem(source, Listing.Popular) },
|
||||||
onLongClickItem = { onLongClickItem(source) },
|
onLongClickItem = { onLongClickItem(source) },
|
||||||
action = {
|
action = {
|
||||||
if (source.supportsLatest) {
|
if (source.supportsLatest) {
|
||||||
TextButton(onClick = { onClickItem(source, GetRemoteManga.QUERY_LATEST) }) {
|
TextButton(onClick = { onClickItem(source, Listing.Latest) }) {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.latest),
|
text = stringResource(R.string.latest),
|
||||||
style = LocalTextStyle.current.copy(
|
style = LocalTextStyle.current.copy(
|
||||||
@ -201,7 +155,7 @@ private fun SourcePinButton(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun SourceOptionsDialog(
|
fun SourceOptionsDialog(
|
||||||
source: Source,
|
source: Source,
|
||||||
onClickPin: () -> Unit,
|
onClickPin: () -> Unit,
|
||||||
onClickDisable: () -> Unit,
|
onClickDisable: () -> Unit,
|
||||||
|
@ -1,27 +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.source.SourcesPresenter
|
|
||||||
|
|
||||||
@Stable
|
|
||||||
interface SourcesState {
|
|
||||||
var dialog: SourcesPresenter.Dialog?
|
|
||||||
val isLoading: Boolean
|
|
||||||
val items: List<SourceUiModel>
|
|
||||||
val isEmpty: Boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
fun SourcesState(): SourcesState {
|
|
||||||
return SourcesStateImpl()
|
|
||||||
}
|
|
||||||
|
|
||||||
class SourcesStateImpl : SourcesState {
|
|
||||||
override var dialog: SourcesPresenter.Dialog? by mutableStateOf(null)
|
|
||||||
override var isLoading: Boolean by mutableStateOf(true)
|
|
||||||
override var items: List<SourceUiModel> by mutableStateOf(emptyList())
|
|
||||||
override val isEmpty: Boolean by derivedStateOf { items.isEmpty() }
|
|
||||||
}
|
|
@ -4,11 +4,11 @@ import androidx.compose.foundation.combinedClickable
|
|||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.RowScope
|
import androidx.compose.foundation.layout.RowScope
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import eu.kanade.presentation.util.padding
|
||||||
import eu.kanade.presentation.util.horizontalPadding
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun BaseBrowseItem(
|
fun BaseBrowseItem(
|
||||||
@ -25,7 +25,7 @@ fun BaseBrowseItem(
|
|||||||
onClick = onClickItem,
|
onClick = onClickItem,
|
||||||
onLongClick = onLongClickItem,
|
onLongClick = onLongClickItem,
|
||||||
)
|
)
|
||||||
.padding(horizontal = horizontalPadding, vertical = 8.dp),
|
.padding(horizontal = MaterialTheme.padding.medium, vertical = MaterialTheme.padding.small),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
icon()
|
icon()
|
||||||
|
@ -10,7 +10,7 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import eu.kanade.domain.source.model.Source
|
import eu.kanade.domain.source.model.Source
|
||||||
import eu.kanade.presentation.util.horizontalPadding
|
import eu.kanade.presentation.util.padding
|
||||||
import eu.kanade.presentation.util.secondaryItemAlpha
|
import eu.kanade.presentation.util.secondaryItemAlpha
|
||||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||||
|
|
||||||
@ -43,7 +43,7 @@ private val defaultIcon: @Composable RowScope.(Source) -> Unit = { source ->
|
|||||||
private val defaultContent: @Composable RowScope.(Source, String?) -> Unit = { source, sourceLangString ->
|
private val defaultContent: @Composable RowScope.(Source, String?) -> Unit = { source, sourceLangString ->
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(horizontal = horizontalPadding)
|
.padding(horizontal = MaterialTheme.padding.medium)
|
||||||
.weight(1f),
|
.weight(1f),
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
|
@ -62,7 +62,7 @@ fun SourceIcon(
|
|||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
Image(
|
Image(
|
||||||
painter = painterResource(id = R.mipmap.ic_local_source),
|
painter = painterResource(R.mipmap.ic_local_source),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = modifier.then(defaultModifier),
|
modifier = modifier.then(defaultModifier),
|
||||||
)
|
)
|
||||||
|
@ -6,24 +6,22 @@ import androidx.compose.foundation.lazy.grid.GridCells
|
|||||||
import androidx.compose.foundation.lazy.grid.GridItemSpan
|
import androidx.compose.foundation.lazy.grid.GridItemSpan
|
||||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.State
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.paging.LoadState
|
import androidx.paging.LoadState
|
||||||
import androidx.paging.compose.LazyPagingItems
|
import androidx.paging.compose.LazyPagingItems
|
||||||
import eu.kanade.domain.manga.model.Manga
|
import eu.kanade.domain.manga.model.Manga
|
||||||
import eu.kanade.domain.manga.model.MangaCover
|
import eu.kanade.domain.manga.model.MangaCover
|
||||||
import eu.kanade.presentation.components.Badge
|
import eu.kanade.presentation.browse.InLibraryBadge
|
||||||
import eu.kanade.presentation.components.CommonMangaItemDefaults
|
import eu.kanade.presentation.components.CommonMangaItemDefaults
|
||||||
import eu.kanade.presentation.components.MangaComfortableGridItem
|
import eu.kanade.presentation.components.MangaComfortableGridItem
|
||||||
import eu.kanade.presentation.util.plus
|
import eu.kanade.presentation.util.plus
|
||||||
import eu.kanade.tachiyomi.R
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun BrowseSourceComfortableGrid(
|
fun BrowseSourceComfortableGrid(
|
||||||
mangaList: LazyPagingItems<Manga>,
|
mangaList: LazyPagingItems<StateFlow<Manga>>,
|
||||||
getMangaState: @Composable ((Manga) -> State<Manga>),
|
|
||||||
columns: GridCells,
|
columns: GridCells,
|
||||||
contentPadding: PaddingValues,
|
contentPadding: PaddingValues,
|
||||||
onMangaClick: (Manga) -> Unit,
|
onMangaClick: (Manga) -> Unit,
|
||||||
@ -42,8 +40,7 @@ fun BrowseSourceComfortableGrid(
|
|||||||
}
|
}
|
||||||
|
|
||||||
items(mangaList.itemCount) { index ->
|
items(mangaList.itemCount) { index ->
|
||||||
val initialManga = mangaList[index] ?: return@items
|
val manga by mangaList[index]?.collectAsState() ?: return@items
|
||||||
val manga by getMangaState(initialManga)
|
|
||||||
BrowseSourceComfortableGridItem(
|
BrowseSourceComfortableGridItem(
|
||||||
manga = manga,
|
manga = manga,
|
||||||
onClick = { onMangaClick(manga) },
|
onClick = { onMangaClick(manga) },
|
||||||
@ -76,9 +73,7 @@ fun BrowseSourceComfortableGridItem(
|
|||||||
),
|
),
|
||||||
coverAlpha = if (manga.favorite) CommonMangaItemDefaults.BrowseFavoriteCoverAlpha else 1f,
|
coverAlpha = if (manga.favorite) CommonMangaItemDefaults.BrowseFavoriteCoverAlpha else 1f,
|
||||||
coverBadgeStart = {
|
coverBadgeStart = {
|
||||||
if (manga.favorite) {
|
InLibraryBadge(enabled = manga.favorite)
|
||||||
Badge(text = stringResource(R.string.in_library))
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
onLongClick = onLongClick,
|
onLongClick = onLongClick,
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
|
@ -6,24 +6,22 @@ import androidx.compose.foundation.lazy.grid.GridCells
|
|||||||
import androidx.compose.foundation.lazy.grid.GridItemSpan
|
import androidx.compose.foundation.lazy.grid.GridItemSpan
|
||||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.State
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.paging.LoadState
|
import androidx.paging.LoadState
|
||||||
import androidx.paging.compose.LazyPagingItems
|
import androidx.paging.compose.LazyPagingItems
|
||||||
import eu.kanade.domain.manga.model.Manga
|
import eu.kanade.domain.manga.model.Manga
|
||||||
import eu.kanade.domain.manga.model.MangaCover
|
import eu.kanade.domain.manga.model.MangaCover
|
||||||
import eu.kanade.presentation.components.Badge
|
import eu.kanade.presentation.browse.InLibraryBadge
|
||||||
import eu.kanade.presentation.components.CommonMangaItemDefaults
|
import eu.kanade.presentation.components.CommonMangaItemDefaults
|
||||||
import eu.kanade.presentation.components.MangaCompactGridItem
|
import eu.kanade.presentation.components.MangaCompactGridItem
|
||||||
import eu.kanade.presentation.util.plus
|
import eu.kanade.presentation.util.plus
|
||||||
import eu.kanade.tachiyomi.R
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun BrowseSourceCompactGrid(
|
fun BrowseSourceCompactGrid(
|
||||||
mangaList: LazyPagingItems<Manga>,
|
mangaList: LazyPagingItems<StateFlow<Manga>>,
|
||||||
getMangaState: @Composable ((Manga) -> State<Manga>),
|
|
||||||
columns: GridCells,
|
columns: GridCells,
|
||||||
contentPadding: PaddingValues,
|
contentPadding: PaddingValues,
|
||||||
onMangaClick: (Manga) -> Unit,
|
onMangaClick: (Manga) -> Unit,
|
||||||
@ -42,8 +40,7 @@ fun BrowseSourceCompactGrid(
|
|||||||
}
|
}
|
||||||
|
|
||||||
items(mangaList.itemCount) { index ->
|
items(mangaList.itemCount) { index ->
|
||||||
val initialManga = mangaList[index] ?: return@items
|
val manga by mangaList[index]?.collectAsState() ?: return@items
|
||||||
val manga by getMangaState(initialManga)
|
|
||||||
BrowseSourceCompactGridItem(
|
BrowseSourceCompactGridItem(
|
||||||
manga = manga,
|
manga = manga,
|
||||||
onClick = { onMangaClick(manga) },
|
onClick = { onMangaClick(manga) },
|
||||||
@ -76,9 +73,7 @@ private fun BrowseSourceCompactGridItem(
|
|||||||
),
|
),
|
||||||
coverAlpha = if (manga.favorite) CommonMangaItemDefaults.BrowseFavoriteCoverAlpha else 1f,
|
coverAlpha = if (manga.favorite) CommonMangaItemDefaults.BrowseFavoriteCoverAlpha else 1f,
|
||||||
coverBadgeStart = {
|
coverBadgeStart = {
|
||||||
if (manga.favorite) {
|
InLibraryBadge(enabled = manga.favorite)
|
||||||
Badge(text = stringResource(R.string.in_library))
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
onLongClick = onLongClick,
|
onLongClick = onLongClick,
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
|
@ -2,26 +2,24 @@ package eu.kanade.presentation.browse.components
|
|||||||
|
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.State
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.paging.LoadState
|
import androidx.paging.LoadState
|
||||||
import androidx.paging.compose.LazyPagingItems
|
import androidx.paging.compose.LazyPagingItems
|
||||||
import androidx.paging.compose.items
|
import androidx.paging.compose.items
|
||||||
import eu.kanade.domain.manga.model.Manga
|
import eu.kanade.domain.manga.model.Manga
|
||||||
import eu.kanade.domain.manga.model.MangaCover
|
import eu.kanade.domain.manga.model.MangaCover
|
||||||
import eu.kanade.presentation.components.Badge
|
import eu.kanade.presentation.browse.InLibraryBadge
|
||||||
import eu.kanade.presentation.components.CommonMangaItemDefaults
|
import eu.kanade.presentation.components.CommonMangaItemDefaults
|
||||||
import eu.kanade.presentation.components.LazyColumn
|
import eu.kanade.presentation.components.LazyColumn
|
||||||
import eu.kanade.presentation.components.MangaListItem
|
import eu.kanade.presentation.components.MangaListItem
|
||||||
import eu.kanade.presentation.util.plus
|
import eu.kanade.presentation.util.plus
|
||||||
import eu.kanade.tachiyomi.R
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun BrowseSourceList(
|
fun BrowseSourceList(
|
||||||
mangaList: LazyPagingItems<Manga>,
|
mangaList: LazyPagingItems<StateFlow<Manga>>,
|
||||||
getMangaState: @Composable ((Manga) -> State<Manga>),
|
|
||||||
contentPadding: PaddingValues,
|
contentPadding: PaddingValues,
|
||||||
onMangaClick: (Manga) -> Unit,
|
onMangaClick: (Manga) -> Unit,
|
||||||
onMangaLongClick: (Manga) -> Unit,
|
onMangaLongClick: (Manga) -> Unit,
|
||||||
@ -35,9 +33,9 @@ fun BrowseSourceList(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
items(mangaList) { initialManga ->
|
items(mangaList) { mangaflow ->
|
||||||
initialManga ?: return@items
|
mangaflow ?: return@items
|
||||||
val manga by getMangaState(initialManga)
|
val manga by mangaflow.collectAsState()
|
||||||
BrowseSourceListItem(
|
BrowseSourceListItem(
|
||||||
manga = manga,
|
manga = manga,
|
||||||
onClick = { onMangaClick(manga) },
|
onClick = { onMangaClick(manga) },
|
||||||
@ -70,9 +68,7 @@ fun BrowseSourceListItem(
|
|||||||
),
|
),
|
||||||
coverAlpha = if (manga.favorite) CommonMangaItemDefaults.BrowseFavoriteCoverAlpha else 1f,
|
coverAlpha = if (manga.favorite) CommonMangaItemDefaults.BrowseFavoriteCoverAlpha else 1f,
|
||||||
badge = {
|
badge = {
|
||||||
if (manga.favorite) {
|
InLibraryBadge(enabled = manga.favorite)
|
||||||
Badge(text = stringResource(R.string.in_library))
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
onLongClick = onLongClick,
|
onLongClick = onLongClick,
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
|
@ -14,7 +14,6 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import eu.kanade.domain.library.model.LibraryDisplayMode
|
import eu.kanade.domain.library.model.LibraryDisplayMode
|
||||||
import eu.kanade.presentation.browse.BrowseSourceState
|
|
||||||
import eu.kanade.presentation.components.AppBar
|
import eu.kanade.presentation.components.AppBar
|
||||||
import eu.kanade.presentation.components.AppBarActions
|
import eu.kanade.presentation.components.AppBarActions
|
||||||
import eu.kanade.presentation.components.AppBarTitle
|
import eu.kanade.presentation.components.AppBarTitle
|
||||||
@ -27,7 +26,8 @@ import eu.kanade.tachiyomi.source.LocalSource
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun BrowseSourceToolbar(
|
fun BrowseSourceToolbar(
|
||||||
state: BrowseSourceState,
|
searchQuery: String?,
|
||||||
|
onSearchQueryChange: (String?) -> Unit,
|
||||||
source: CatalogueSource?,
|
source: CatalogueSource?,
|
||||||
displayMode: LibraryDisplayMode,
|
displayMode: LibraryDisplayMode,
|
||||||
onDisplayModeChange: (LibraryDisplayMode) -> Unit,
|
onDisplayModeChange: (LibraryDisplayMode) -> Unit,
|
||||||
@ -44,8 +44,8 @@ fun BrowseSourceToolbar(
|
|||||||
SearchToolbar(
|
SearchToolbar(
|
||||||
navigateUp = navigateUp,
|
navigateUp = navigateUp,
|
||||||
titleContent = { AppBarTitle(title) },
|
titleContent = { AppBarTitle(title) },
|
||||||
searchQuery = state.searchQuery,
|
searchQuery = searchQuery,
|
||||||
onChangeSearchQuery = { state.searchQuery = it },
|
onChangeSearchQuery = onSearchQueryChange,
|
||||||
onSearch = onSearch,
|
onSearch = onSearch,
|
||||||
onClickCloseSearch = navigateUp,
|
onClickCloseSearch = navigateUp,
|
||||||
actions = {
|
actions = {
|
||||||
|
@ -0,0 +1,40 @@
|
|||||||
|
package eu.kanade.presentation.browse.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.State
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import eu.kanade.domain.manga.model.Manga
|
||||||
|
import eu.kanade.domain.manga.model.asMangaCover
|
||||||
|
import eu.kanade.presentation.util.padding
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun GlobalSearchCardRow(
|
||||||
|
titles: List<Manga>,
|
||||||
|
getManga: @Composable (Manga) -> State<Manga>,
|
||||||
|
onClick: (Manga) -> Unit,
|
||||||
|
onLongClick: (Manga) -> Unit,
|
||||||
|
) {
|
||||||
|
LazyRow(
|
||||||
|
contentPadding = PaddingValues(
|
||||||
|
horizontal = MaterialTheme.padding.medium,
|
||||||
|
vertical = MaterialTheme.padding.small,
|
||||||
|
),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
||||||
|
) {
|
||||||
|
items(titles) { title ->
|
||||||
|
val title by getManga(title)
|
||||||
|
GlobalSearchCard(
|
||||||
|
title = title.title,
|
||||||
|
cover = title.asMangaCover(),
|
||||||
|
isFavorite = title.favorite,
|
||||||
|
onClick = { onClick(title) },
|
||||||
|
onLongClick = { onLongClick(title) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,111 @@
|
|||||||
|
package eu.kanade.presentation.browse.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.ArrowForward
|
||||||
|
import androidx.compose.material.icons.outlined.Error
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import eu.kanade.presentation.util.padding
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun GlobalSearchResultItem(
|
||||||
|
title: String,
|
||||||
|
subtitle: String,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
content: @Composable () -> Unit,
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(
|
||||||
|
start = MaterialTheme.padding.medium,
|
||||||
|
end = MaterialTheme.padding.tiny,
|
||||||
|
)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable(onClick = onClick),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
)
|
||||||
|
Text(text = subtitle)
|
||||||
|
}
|
||||||
|
IconButton(onClick = onClick) {
|
||||||
|
Icon(imageVector = Icons.Outlined.ArrowForward, contentDescription = null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun GlobalSearchEmptyResultItem() {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.no_results_found),
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(
|
||||||
|
horizontal = MaterialTheme.padding.medium,
|
||||||
|
vertical = MaterialTheme.padding.small,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun GlobalSearchLoadingResultItem() {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = MaterialTheme.padding.medium),
|
||||||
|
) {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(16.dp)
|
||||||
|
.align(Alignment.Center),
|
||||||
|
strokeWidth = 2.dp,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun GlobalSearchErrorResultItem(message: String?) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(
|
||||||
|
horizontal = MaterialTheme.padding.medium,
|
||||||
|
vertical = MaterialTheme.padding.small,
|
||||||
|
)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
) {
|
||||||
|
Icon(imageVector = Icons.Outlined.Error, contentDescription = null)
|
||||||
|
Spacer(Modifier.height(4.dp))
|
||||||
|
Text(
|
||||||
|
text = message ?: stringResource(R.string.unknown_error),
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,40 @@
|
|||||||
|
package eu.kanade.presentation.browse.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.material3.LinearProgressIndicator
|
||||||
|
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import eu.kanade.presentation.components.SearchToolbar
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun GlobalSearchToolbar(
|
||||||
|
searchQuery: String?,
|
||||||
|
progress: Int,
|
||||||
|
total: Int,
|
||||||
|
navigateUp: () -> Unit,
|
||||||
|
onChangeSearchQuery: (String?) -> Unit,
|
||||||
|
onSearch: (String) -> Unit,
|
||||||
|
scrollBehavior: TopAppBarScrollBehavior,
|
||||||
|
) {
|
||||||
|
Box {
|
||||||
|
SearchToolbar(
|
||||||
|
searchQuery = searchQuery,
|
||||||
|
onChangeSearchQuery = onChangeSearchQuery,
|
||||||
|
onSearch = onSearch,
|
||||||
|
onClickCloseSearch = navigateUp,
|
||||||
|
navigateUp = navigateUp,
|
||||||
|
scrollBehavior = scrollBehavior,
|
||||||
|
)
|
||||||
|
if (progress in 1 until total) {
|
||||||
|
LinearProgressIndicator(
|
||||||
|
progress = progress / total.toFloat(),
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.BottomStart)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
package eu.kanade.presentation.browse.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import eu.kanade.domain.manga.model.MangaCover
|
||||||
|
import eu.kanade.presentation.browse.InLibraryBadge
|
||||||
|
import eu.kanade.presentation.components.CommonMangaItemDefaults
|
||||||
|
import eu.kanade.presentation.components.MangaComfortableGridItem
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun GlobalSearchCard(
|
||||||
|
title: String,
|
||||||
|
cover: MangaCover,
|
||||||
|
isFavorite: Boolean,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
onLongClick: () -> Unit,
|
||||||
|
) {
|
||||||
|
Box(modifier = Modifier.width(128.dp)) {
|
||||||
|
MangaComfortableGridItem(
|
||||||
|
title = title,
|
||||||
|
coverData = cover,
|
||||||
|
coverBadgeStart = {
|
||||||
|
InLibraryBadge(enabled = isFavorite)
|
||||||
|
},
|
||||||
|
coverAlpha = if (isFavorite) CommonMangaItemDefaults.BrowseFavoriteCoverAlpha else 1f,
|
||||||
|
onClick = onClick,
|
||||||
|
onLongClick = onLongClick,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -3,32 +3,30 @@ package eu.kanade.presentation.category
|
|||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import eu.kanade.domain.category.model.Category
|
||||||
import eu.kanade.presentation.category.components.CategoryContent
|
import eu.kanade.presentation.category.components.CategoryContent
|
||||||
import eu.kanade.presentation.category.components.CategoryCreateDialog
|
|
||||||
import eu.kanade.presentation.category.components.CategoryDeleteDialog
|
|
||||||
import eu.kanade.presentation.category.components.CategoryFloatingActionButton
|
import eu.kanade.presentation.category.components.CategoryFloatingActionButton
|
||||||
import eu.kanade.presentation.category.components.CategoryRenameDialog
|
|
||||||
import eu.kanade.presentation.components.AppBar
|
import eu.kanade.presentation.components.AppBar
|
||||||
import eu.kanade.presentation.components.EmptyScreen
|
import eu.kanade.presentation.components.EmptyScreen
|
||||||
import eu.kanade.presentation.components.LoadingScreen
|
|
||||||
import eu.kanade.presentation.components.Scaffold
|
import eu.kanade.presentation.components.Scaffold
|
||||||
import eu.kanade.presentation.util.horizontalPadding
|
import eu.kanade.presentation.util.padding
|
||||||
import eu.kanade.presentation.util.plus
|
import eu.kanade.presentation.util.plus
|
||||||
import eu.kanade.presentation.util.topPaddingValues
|
import eu.kanade.presentation.util.topSmallPaddingValues
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.ui.category.CategoryPresenter
|
import eu.kanade.tachiyomi.ui.category.CategoryScreenState
|
||||||
import eu.kanade.tachiyomi.ui.category.CategoryPresenter.Dialog
|
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CategoryScreen(
|
fun CategoryScreen(
|
||||||
presenter: CategoryPresenter,
|
state: CategoryScreenState.Success,
|
||||||
|
onClickCreate: () -> Unit,
|
||||||
|
onClickRename: (Category) -> Unit,
|
||||||
|
onClickDelete: (Category) -> Unit,
|
||||||
|
onClickMoveUp: (Category) -> Unit,
|
||||||
|
onClickMoveDown: (Category) -> Unit,
|
||||||
navigateUp: () -> Unit,
|
navigateUp: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val lazyListState = rememberLazyListState()
|
val lazyListState = rememberLazyListState()
|
||||||
@ -43,63 +41,26 @@ fun CategoryScreen(
|
|||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
CategoryFloatingActionButton(
|
CategoryFloatingActionButton(
|
||||||
lazyListState = lazyListState,
|
lazyListState = lazyListState,
|
||||||
onCreate = { presenter.dialog = Dialog.Create },
|
onCreate = onClickCreate,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
val context = LocalContext.current
|
if (state.isEmpty) {
|
||||||
when {
|
EmptyScreen(
|
||||||
presenter.isLoading -> LoadingScreen()
|
|
||||||
presenter.isEmpty -> EmptyScreen(
|
|
||||||
textResource = R.string.information_empty_category,
|
textResource = R.string.information_empty_category,
|
||||||
modifier = Modifier.padding(paddingValues),
|
modifier = Modifier.padding(paddingValues),
|
||||||
)
|
)
|
||||||
else -> {
|
return@Scaffold
|
||||||
CategoryContent(
|
|
||||||
state = presenter,
|
|
||||||
lazyListState = lazyListState,
|
|
||||||
paddingValues = paddingValues + topPaddingValues + PaddingValues(horizontal = horizontalPadding),
|
|
||||||
onMoveUp = { presenter.moveUp(it) },
|
|
||||||
onMoveDown = { presenter.moveDown(it) },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val onDismissRequest = { presenter.dialog = null }
|
CategoryContent(
|
||||||
when (val dialog = presenter.dialog) {
|
categories = state.categories,
|
||||||
Dialog.Create -> {
|
lazyListState = lazyListState,
|
||||||
CategoryCreateDialog(
|
paddingValues = paddingValues + topSmallPaddingValues + PaddingValues(horizontal = MaterialTheme.padding.medium),
|
||||||
onDismissRequest = onDismissRequest,
|
onClickRename = onClickRename,
|
||||||
onCreate = { presenter.createCategory(it) },
|
onClickDelete = onClickDelete,
|
||||||
)
|
onMoveUp = onClickMoveUp,
|
||||||
}
|
onMoveDown = onClickMoveDown,
|
||||||
is Dialog.Rename -> {
|
)
|
||||||
CategoryRenameDialog(
|
|
||||||
onDismissRequest = onDismissRequest,
|
|
||||||
onRename = { presenter.renameCategory(dialog.category, it) },
|
|
||||||
category = dialog.category,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
is Dialog.Delete -> {
|
|
||||||
CategoryDeleteDialog(
|
|
||||||
onDismissRequest = onDismissRequest,
|
|
||||||
onDelete = { presenter.deleteCategory(dialog.category) },
|
|
||||||
category = dialog.category,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
else -> {}
|
|
||||||
}
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
presenter.events.collectLatest { event ->
|
|
||||||
when (event) {
|
|
||||||
is CategoryPresenter.Event.CategoryWithNameAlreadyExists -> {
|
|
||||||
context.toast(R.string.error_category_exists)
|
|
||||||
}
|
|
||||||
is CategoryPresenter.Event.InternalError -> {
|
|
||||||
context.toast(R.string.internal_error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,28 +0,0 @@
|
|||||||
package eu.kanade.presentation.category
|
|
||||||
|
|
||||||
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.domain.category.model.Category
|
|
||||||
import eu.kanade.tachiyomi.ui.category.CategoryPresenter
|
|
||||||
|
|
||||||
@Stable
|
|
||||||
interface CategoryState {
|
|
||||||
val isLoading: Boolean
|
|
||||||
var dialog: CategoryPresenter.Dialog?
|
|
||||||
val categories: List<Category>
|
|
||||||
val isEmpty: Boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
fun CategoryState(): CategoryState {
|
|
||||||
return CategoryStateImpl()
|
|
||||||
}
|
|
||||||
|
|
||||||
class CategoryStateImpl : CategoryState {
|
|
||||||
override var isLoading: Boolean by mutableStateOf(true)
|
|
||||||
override var dialog: CategoryPresenter.Dialog? by mutableStateOf(null)
|
|
||||||
override var categories: List<Category> by mutableStateOf(emptyList())
|
|
||||||
override val isEmpty: Boolean by derivedStateOf { categories.isEmpty() }
|
|
||||||
}
|
|
@ -8,19 +8,18 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import eu.kanade.domain.category.model.Category
|
import eu.kanade.domain.category.model.Category
|
||||||
import eu.kanade.presentation.category.CategoryState
|
|
||||||
import eu.kanade.presentation.components.LazyColumn
|
import eu.kanade.presentation.components.LazyColumn
|
||||||
import eu.kanade.tachiyomi.ui.category.CategoryPresenter.Dialog
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CategoryContent(
|
fun CategoryContent(
|
||||||
state: CategoryState,
|
categories: List<Category>,
|
||||||
lazyListState: LazyListState,
|
lazyListState: LazyListState,
|
||||||
paddingValues: PaddingValues,
|
paddingValues: PaddingValues,
|
||||||
|
onClickRename: (Category) -> Unit,
|
||||||
|
onClickDelete: (Category) -> Unit,
|
||||||
onMoveUp: (Category) -> Unit,
|
onMoveUp: (Category) -> Unit,
|
||||||
onMoveDown: (Category) -> Unit,
|
onMoveDown: (Category) -> Unit,
|
||||||
) {
|
) {
|
||||||
val categories = state.categories
|
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
state = lazyListState,
|
state = lazyListState,
|
||||||
contentPadding = paddingValues,
|
contentPadding = paddingValues,
|
||||||
@ -37,8 +36,8 @@ fun CategoryContent(
|
|||||||
canMoveDown = index != categories.lastIndex,
|
canMoveDown = index != categories.lastIndex,
|
||||||
onMoveUp = onMoveUp,
|
onMoveUp = onMoveUp,
|
||||||
onMoveDown = onMoveDown,
|
onMoveDown = onMoveDown,
|
||||||
onRename = { state.dialog = Dialog.Rename(category) },
|
onRename = { onClickRename(category) },
|
||||||
onDelete = { state.dialog = Dialog.Delete(category) },
|
onDelete = { onClickDelete(category) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,13 +14,14 @@ import androidx.compose.material.icons.outlined.Label
|
|||||||
import androidx.compose.material3.ElevatedCard
|
import androidx.compose.material3.ElevatedCard
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import eu.kanade.domain.category.model.Category
|
import eu.kanade.domain.category.model.Category
|
||||||
import eu.kanade.presentation.util.horizontalPadding
|
import eu.kanade.presentation.util.padding
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@ -41,14 +42,18 @@ fun CategoryListItem(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clickable { onRename() }
|
.clickable { onRename() }
|
||||||
.padding(start = horizontalPadding, top = horizontalPadding, end = horizontalPadding),
|
.padding(
|
||||||
|
start = MaterialTheme.padding.medium,
|
||||||
|
top = MaterialTheme.padding.medium,
|
||||||
|
end = MaterialTheme.padding.medium,
|
||||||
|
),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
Icon(imageVector = Icons.Outlined.Label, contentDescription = "")
|
Icon(imageVector = Icons.Outlined.Label, contentDescription = "")
|
||||||
Text(
|
Text(
|
||||||
text = category.name,
|
text = category.name,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(start = horizontalPadding),
|
.padding(start = MaterialTheme.padding.medium),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Row {
|
Row {
|
||||||
|
@ -0,0 +1,344 @@
|
|||||||
|
package eu.kanade.presentation.components
|
||||||
|
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.animation.with
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.gestures.Orientation
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
|
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||||
|
import androidx.compose.foundation.layout.asPaddingValues
|
||||||
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.navigationBars
|
||||||
|
import androidx.compose.foundation.layout.offset
|
||||||
|
import androidx.compose.foundation.layout.only
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.requiredWidthIn
|
||||||
|
import androidx.compose.foundation.layout.systemBars
|
||||||
|
import androidx.compose.foundation.layout.systemBarsPadding
|
||||||
|
import androidx.compose.foundation.layout.widthIn
|
||||||
|
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||||
|
import androidx.compose.foundation.shape.ZeroCornerSize
|
||||||
|
import androidx.compose.material.SwipeableState
|
||||||
|
import androidx.compose.material.rememberSwipeableState
|
||||||
|
import androidx.compose.material.swipeable
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.runtime.snapshotFlow
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.alpha
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||||
|
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
||||||
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.IntOffset
|
||||||
|
import androidx.compose.ui.unit.Velocity
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import cafe.adriel.voyager.core.lifecycle.DisposableEffectIgnoringConfiguration
|
||||||
|
import cafe.adriel.voyager.core.screen.Screen
|
||||||
|
import cafe.adriel.voyager.navigator.Navigator
|
||||||
|
import cafe.adriel.voyager.transitions.ScreenTransition
|
||||||
|
import eu.kanade.presentation.util.isTabletUi
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import kotlinx.coroutines.flow.drop
|
||||||
|
import kotlinx.coroutines.flow.filter
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
|
private const val SheetAnimationDuration = 500
|
||||||
|
private val SheetAnimationSpec = tween<Float>(durationMillis = SheetAnimationDuration)
|
||||||
|
private const val ScrimAnimationDuration = 350
|
||||||
|
private val ScrimAnimationSpec = tween<Float>(durationMillis = ScrimAnimationDuration)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun NavigatorAdaptiveSheet(
|
||||||
|
screen: Screen,
|
||||||
|
tonalElevation: Dp = 1.dp,
|
||||||
|
enableSwipeDismiss: (Navigator) -> Boolean = { true },
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
) {
|
||||||
|
Navigator(
|
||||||
|
screen = screen,
|
||||||
|
content = { sheetNavigator ->
|
||||||
|
AdaptiveSheet(
|
||||||
|
tonalElevation = tonalElevation,
|
||||||
|
enableSwipeDismiss = enableSwipeDismiss(sheetNavigator),
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
) {
|
||||||
|
ScreenTransition(
|
||||||
|
navigator = sheetNavigator,
|
||||||
|
transition = {
|
||||||
|
fadeIn(animationSpec = tween(220, delayMillis = 90)) with
|
||||||
|
fadeOut(animationSpec = tween(90))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
BackHandler(
|
||||||
|
enabled = sheetNavigator.size > 1,
|
||||||
|
onBack = sheetNavigator::pop,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure screens are disposed no matter what
|
||||||
|
if (sheetNavigator.parent?.disposeBehavior?.disposeNestedNavigators == false) {
|
||||||
|
DisposableEffectIgnoringConfiguration {
|
||||||
|
onDispose {
|
||||||
|
sheetNavigator.items
|
||||||
|
.asReversed()
|
||||||
|
.forEach(sheetNavigator::dispose)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sheet with adaptive position aligned to bottom on small screen, otherwise aligned to center
|
||||||
|
* and will not be able to dismissed with swipe gesture.
|
||||||
|
*
|
||||||
|
* Max width of the content is set to 460 dp.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun AdaptiveSheet(
|
||||||
|
tonalElevation: Dp = 1.dp,
|
||||||
|
enableSwipeDismiss: Boolean = true,
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
content: @Composable (PaddingValues) -> Unit,
|
||||||
|
) {
|
||||||
|
val isTabletUi = isTabletUi()
|
||||||
|
AdaptiveSheetImpl(
|
||||||
|
isTabletUi = isTabletUi,
|
||||||
|
tonalElevation = tonalElevation,
|
||||||
|
enableSwipeDismiss = enableSwipeDismiss,
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
) {
|
||||||
|
val contentPadding = if (isTabletUi) {
|
||||||
|
PaddingValues()
|
||||||
|
} else {
|
||||||
|
WindowInsets.navigationBars.only(WindowInsetsSides.Bottom).asPaddingValues()
|
||||||
|
}
|
||||||
|
content(contentPadding)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AdaptiveSheetImpl(
|
||||||
|
isTabletUi: Boolean,
|
||||||
|
tonalElevation: Dp,
|
||||||
|
enableSwipeDismiss: Boolean,
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
content: @Composable () -> Unit,
|
||||||
|
) {
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
if (isTabletUi) {
|
||||||
|
var targetAlpha by remember { mutableStateOf(0f) }
|
||||||
|
val alpha by animateFloatAsState(
|
||||||
|
targetValue = targetAlpha,
|
||||||
|
animationSpec = ScrimAnimationSpec,
|
||||||
|
)
|
||||||
|
val internalOnDismissRequest: () -> Unit = {
|
||||||
|
scope.launch {
|
||||||
|
targetAlpha = 0f
|
||||||
|
delay(ScrimAnimationSpec.durationMillis.milliseconds)
|
||||||
|
onDismissRequest()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BoxWithConstraints(
|
||||||
|
modifier = Modifier
|
||||||
|
.clickable(
|
||||||
|
enabled = true,
|
||||||
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
|
indication = null,
|
||||||
|
onClick = internalOnDismissRequest,
|
||||||
|
)
|
||||||
|
.fillMaxSize()
|
||||||
|
.alpha(alpha),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.matchParentSize()
|
||||||
|
.background(MaterialTheme.colorScheme.scrim.copy(alpha = 0.32f)),
|
||||||
|
)
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier
|
||||||
|
.requiredWidthIn(max = 460.dp)
|
||||||
|
.clickable(
|
||||||
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
|
indication = null,
|
||||||
|
onClick = {},
|
||||||
|
)
|
||||||
|
.systemBarsPadding()
|
||||||
|
.padding(vertical = 16.dp),
|
||||||
|
shape = MaterialTheme.shapes.extraLarge,
|
||||||
|
tonalElevation = tonalElevation,
|
||||||
|
content = {
|
||||||
|
BackHandler(enabled = alpha > 0f, onBack = internalOnDismissRequest)
|
||||||
|
content()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
targetAlpha = 1f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val swipeState = rememberSwipeableState(
|
||||||
|
initialValue = 1,
|
||||||
|
animationSpec = SheetAnimationSpec,
|
||||||
|
)
|
||||||
|
val internalOnDismissRequest: () -> Unit = { if (swipeState.currentValue == 0) scope.launch { swipeState.animateTo(1) } }
|
||||||
|
BoxWithConstraints(
|
||||||
|
modifier = Modifier
|
||||||
|
.clickable(
|
||||||
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
|
indication = null,
|
||||||
|
onClick = internalOnDismissRequest,
|
||||||
|
)
|
||||||
|
.fillMaxSize(),
|
||||||
|
contentAlignment = Alignment.BottomCenter,
|
||||||
|
) {
|
||||||
|
val fullHeight = constraints.maxHeight.toFloat()
|
||||||
|
val anchors = mapOf(0f to 0, fullHeight to 1)
|
||||||
|
val scrimAlpha by animateFloatAsState(
|
||||||
|
targetValue = if (swipeState.targetValue == 1) 0f else 1f,
|
||||||
|
animationSpec = ScrimAnimationSpec,
|
||||||
|
)
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.matchParentSize()
|
||||||
|
.alpha(scrimAlpha)
|
||||||
|
.background(MaterialTheme.colorScheme.scrim.copy(alpha = 0.32f)),
|
||||||
|
)
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier
|
||||||
|
.widthIn(max = 460.dp)
|
||||||
|
.clickable(
|
||||||
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
|
indication = null,
|
||||||
|
onClick = {},
|
||||||
|
)
|
||||||
|
.nestedScroll(
|
||||||
|
remember(enableSwipeDismiss, anchors) {
|
||||||
|
swipeState.preUpPostDownNestedScrollConnection(
|
||||||
|
enabled = enableSwipeDismiss,
|
||||||
|
anchor = anchors,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.offset {
|
||||||
|
IntOffset(
|
||||||
|
0,
|
||||||
|
swipeState.offset.value.roundToInt(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.swipeable(
|
||||||
|
enabled = enableSwipeDismiss,
|
||||||
|
state = swipeState,
|
||||||
|
anchors = anchors,
|
||||||
|
orientation = Orientation.Vertical,
|
||||||
|
resistance = null,
|
||||||
|
)
|
||||||
|
.windowInsetsPadding(
|
||||||
|
WindowInsets.systemBars
|
||||||
|
.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
|
||||||
|
)
|
||||||
|
.consumeWindowInsets(
|
||||||
|
WindowInsets.systemBars
|
||||||
|
.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
|
||||||
|
),
|
||||||
|
shape = MaterialTheme.shapes.extraLarge.copy(bottomStart = ZeroCornerSize, bottomEnd = ZeroCornerSize),
|
||||||
|
tonalElevation = tonalElevation,
|
||||||
|
content = {
|
||||||
|
BackHandler(enabled = swipeState.targetValue == 0, onBack = internalOnDismissRequest)
|
||||||
|
content()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
LaunchedEffect(swipeState) {
|
||||||
|
scope.launch { swipeState.animateTo(0) }
|
||||||
|
snapshotFlow { swipeState.currentValue }
|
||||||
|
.drop(1)
|
||||||
|
.filter { it == 1 }
|
||||||
|
.collectLatest {
|
||||||
|
delay(ScrimAnimationSpec.durationMillis.milliseconds)
|
||||||
|
onDismissRequest()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Yoinked from Swipeable.kt with modifications to disable
|
||||||
|
*/
|
||||||
|
private fun <T> SwipeableState<T>.preUpPostDownNestedScrollConnection(
|
||||||
|
enabled: Boolean = true,
|
||||||
|
anchor: Map<Float, T>,
|
||||||
|
) = object : NestedScrollConnection {
|
||||||
|
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
|
||||||
|
val delta = available.toFloat()
|
||||||
|
return if (enabled && delta < 0 && source == NestedScrollSource.Drag) {
|
||||||
|
performDrag(delta).toOffset()
|
||||||
|
} else {
|
||||||
|
Offset.Zero
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPostScroll(
|
||||||
|
consumed: Offset,
|
||||||
|
available: Offset,
|
||||||
|
source: NestedScrollSource,
|
||||||
|
): Offset {
|
||||||
|
return if (enabled && source == NestedScrollSource.Drag) {
|
||||||
|
performDrag(available.toFloat()).toOffset()
|
||||||
|
} else {
|
||||||
|
Offset.Zero
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun onPreFling(available: Velocity): Velocity {
|
||||||
|
val toFling = Offset(available.x, available.y).toFloat()
|
||||||
|
return if (enabled && toFling < 0 && offset.value > anchor.keys.minOrNull()!!) {
|
||||||
|
performFling(velocity = toFling)
|
||||||
|
// since we go to the anchor with tween settling, consume all for the best UX
|
||||||
|
available
|
||||||
|
} else {
|
||||||
|
Velocity.Zero
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
|
||||||
|
return if (enabled) {
|
||||||
|
performFling(velocity = Offset(available.x, available.y).toFloat())
|
||||||
|
available
|
||||||
|
} else {
|
||||||
|
Velocity.Zero
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Float.toOffset(): Offset = Offset(0f, this)
|
||||||
|
|
||||||
|
private fun Offset.toFloat(): Float = this.y
|
||||||
|
}
|
@ -0,0 +1,93 @@
|
|||||||
|
package eu.kanade.presentation.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.sizeIn
|
||||||
|
import androidx.compose.material3.LocalContentColor
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.ProvideTextStyle
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AlertDialogContent(
|
||||||
|
buttons: @Composable () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
icon: (@Composable () -> Unit)? = null,
|
||||||
|
title: (@Composable () -> Unit)? = null,
|
||||||
|
text: @Composable (() -> Unit)? = null,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = modifier
|
||||||
|
.sizeIn(minWidth = MinWidth, maxWidth = MaxWidth)
|
||||||
|
.padding(DialogPadding),
|
||||||
|
) {
|
||||||
|
icon?.let {
|
||||||
|
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.secondary) {
|
||||||
|
Box(
|
||||||
|
Modifier
|
||||||
|
.padding(IconPadding)
|
||||||
|
.align(Alignment.CenterHorizontally),
|
||||||
|
) {
|
||||||
|
icon()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
title?.let {
|
||||||
|
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurface) {
|
||||||
|
val textStyle = MaterialTheme.typography.headlineSmall
|
||||||
|
ProvideTextStyle(textStyle) {
|
||||||
|
Box(
|
||||||
|
// Align the title to the center when an icon is present.
|
||||||
|
Modifier
|
||||||
|
.padding(TitlePadding)
|
||||||
|
.align(
|
||||||
|
if (icon == null) {
|
||||||
|
Alignment.Start
|
||||||
|
} else {
|
||||||
|
Alignment.CenterHorizontally
|
||||||
|
},
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
title()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
text?.let {
|
||||||
|
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurfaceVariant) {
|
||||||
|
val textStyle = MaterialTheme.typography.bodyMedium
|
||||||
|
ProvideTextStyle(textStyle) {
|
||||||
|
Box(
|
||||||
|
Modifier
|
||||||
|
.weight(weight = 1f, fill = false)
|
||||||
|
.padding(TextPadding)
|
||||||
|
.align(Alignment.Start),
|
||||||
|
) {
|
||||||
|
text()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Box(modifier = Modifier.align(Alignment.End)) {
|
||||||
|
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.primary) {
|
||||||
|
val textStyle = MaterialTheme.typography.labelLarge
|
||||||
|
ProvideTextStyle(value = textStyle, content = buttons)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Paddings for each of the dialog's parts.
|
||||||
|
private val DialogPadding = PaddingValues(all = 24.dp)
|
||||||
|
private val IconPadding = PaddingValues(bottom = 16.dp)
|
||||||
|
private val TitlePadding = PaddingValues(bottom = 16.dp)
|
||||||
|
private val TextPadding = PaddingValues(bottom = 24.dp)
|
||||||
|
|
||||||
|
private val MinWidth = 280.dp
|
||||||
|
private val MaxWidth = 560.dp
|
@ -44,6 +44,7 @@ import androidx.compose.ui.text.input.VisualTransformation
|
|||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import eu.kanade.presentation.util.runOnEnterKeyPressed
|
||||||
import eu.kanade.presentation.util.secondaryItemAlpha
|
import eu.kanade.presentation.util.secondaryItemAlpha
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
|
|
||||||
@ -62,9 +63,6 @@ fun AppBar(
|
|||||||
actionModeCounter: Int = 0,
|
actionModeCounter: Int = 0,
|
||||||
onCancelActionMode: () -> Unit = {},
|
onCancelActionMode: () -> Unit = {},
|
||||||
actionModeActions: @Composable RowScope.() -> Unit = {},
|
actionModeActions: @Composable RowScope.() -> Unit = {},
|
||||||
// Banners
|
|
||||||
downloadedOnlyMode: Boolean = false,
|
|
||||||
incognitoMode: Boolean = false,
|
|
||||||
|
|
||||||
scrollBehavior: TopAppBarScrollBehavior? = null,
|
scrollBehavior: TopAppBarScrollBehavior? = null,
|
||||||
) {
|
) {
|
||||||
@ -92,8 +90,6 @@ fun AppBar(
|
|||||||
},
|
},
|
||||||
isActionMode = isActionMode,
|
isActionMode = isActionMode,
|
||||||
onCancelActionMode = onCancelActionMode,
|
onCancelActionMode = onCancelActionMode,
|
||||||
downloadedOnlyMode = downloadedOnlyMode,
|
|
||||||
incognitoMode = incognitoMode,
|
|
||||||
scrollBehavior = scrollBehavior,
|
scrollBehavior = scrollBehavior,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -111,9 +107,6 @@ fun AppBar(
|
|||||||
// Action mode
|
// Action mode
|
||||||
isActionMode: Boolean = false,
|
isActionMode: Boolean = false,
|
||||||
onCancelActionMode: () -> Unit = {},
|
onCancelActionMode: () -> Unit = {},
|
||||||
// Banners
|
|
||||||
downloadedOnlyMode: Boolean = false,
|
|
||||||
incognitoMode: Boolean = false,
|
|
||||||
|
|
||||||
scrollBehavior: TopAppBarScrollBehavior? = null,
|
scrollBehavior: TopAppBarScrollBehavior? = null,
|
||||||
) {
|
) {
|
||||||
@ -149,8 +142,6 @@ fun AppBar(
|
|||||||
),
|
),
|
||||||
scrollBehavior = scrollBehavior,
|
scrollBehavior = scrollBehavior,
|
||||||
)
|
)
|
||||||
|
|
||||||
AppStateBanners(downloadedOnlyMode, incognitoMode)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -235,8 +226,6 @@ fun SearchToolbar(
|
|||||||
onSearch: (String) -> Unit = {},
|
onSearch: (String) -> Unit = {},
|
||||||
onClickCloseSearch: () -> Unit = { onChangeSearchQuery(null) },
|
onClickCloseSearch: () -> Unit = { onChangeSearchQuery(null) },
|
||||||
actions: @Composable RowScope.() -> Unit = {},
|
actions: @Composable RowScope.() -> Unit = {},
|
||||||
incognitoMode: Boolean = false,
|
|
||||||
downloadedOnlyMode: Boolean = false,
|
|
||||||
scrollBehavior: TopAppBarScrollBehavior? = null,
|
scrollBehavior: TopAppBarScrollBehavior? = null,
|
||||||
visualTransformation: VisualTransformation = VisualTransformation.None,
|
visualTransformation: VisualTransformation = VisualTransformation.None,
|
||||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||||
@ -251,25 +240,27 @@ fun SearchToolbar(
|
|||||||
val keyboardController = LocalSoftwareKeyboardController.current
|
val keyboardController = LocalSoftwareKeyboardController.current
|
||||||
val focusManager = LocalFocusManager.current
|
val focusManager = LocalFocusManager.current
|
||||||
|
|
||||||
|
val searchAndClearFocus: () -> Unit = f@{
|
||||||
|
if (searchQuery.isBlank()) return@f
|
||||||
|
onSearch(searchQuery)
|
||||||
|
focusManager.clearFocus()
|
||||||
|
keyboardController?.hide()
|
||||||
|
}
|
||||||
|
|
||||||
BasicTextField(
|
BasicTextField(
|
||||||
value = searchQuery,
|
value = searchQuery,
|
||||||
onValueChange = onChangeSearchQuery,
|
onValueChange = onChangeSearchQuery,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.focusRequester(focusRequester),
|
.focusRequester(focusRequester)
|
||||||
|
.runOnEnterKeyPressed(action = searchAndClearFocus),
|
||||||
textStyle = MaterialTheme.typography.titleMedium.copy(
|
textStyle = MaterialTheme.typography.titleMedium.copy(
|
||||||
color = MaterialTheme.colorScheme.onBackground,
|
color = MaterialTheme.colorScheme.onBackground,
|
||||||
fontWeight = FontWeight.Normal,
|
fontWeight = FontWeight.Normal,
|
||||||
fontSize = 18.sp,
|
fontSize = 18.sp,
|
||||||
),
|
),
|
||||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
|
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
|
||||||
keyboardActions = KeyboardActions(
|
keyboardActions = KeyboardActions(onSearch = { searchAndClearFocus() }),
|
||||||
onSearch = {
|
|
||||||
onSearch(searchQuery)
|
|
||||||
focusManager.clearFocus()
|
|
||||||
keyboardController?.hide()
|
|
||||||
},
|
|
||||||
),
|
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
cursorBrush = SolidColor(MaterialTheme.colorScheme.onBackground),
|
cursorBrush = SolidColor(MaterialTheme.colorScheme.onBackground),
|
||||||
visualTransformation = visualTransformation,
|
visualTransformation = visualTransformation,
|
||||||
@ -324,12 +315,11 @@ fun SearchToolbar(
|
|||||||
key("actions") { actions() }
|
key("actions") { actions() }
|
||||||
},
|
},
|
||||||
isActionMode = false,
|
isActionMode = false,
|
||||||
downloadedOnlyMode = downloadedOnlyMode,
|
|
||||||
incognitoMode = incognitoMode,
|
|
||||||
scrollBehavior = scrollBehavior,
|
scrollBehavior = scrollBehavior,
|
||||||
)
|
)
|
||||||
LaunchedEffect(searchClickCount) {
|
LaunchedEffect(searchClickCount) {
|
||||||
if (searchQuery == null) return@LaunchedEffect
|
if (searchQuery == null) return@LaunchedEffect
|
||||||
|
if (searchClickCount == 0 && searchQuery.isNotEmpty()) return@LaunchedEffect
|
||||||
try {
|
try {
|
||||||
focusRequester.requestFocus()
|
focusRequester.requestFocus()
|
||||||
} catch (_: Throwable) {
|
} catch (_: Throwable) {
|
||||||
|
@ -1,19 +1,46 @@
|
|||||||
package eu.kanade.presentation.components
|
package eu.kanade.presentation.components
|
||||||
|
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.expandVertically
|
||||||
|
import androidx.compose.animation.shrinkVertically
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.requiredSize
|
||||||
|
import androidx.compose.foundation.layout.statusBars
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.layout.SubcomposeLayout
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.util.fastForEach
|
||||||
|
import androidx.compose.ui.util.fastMap
|
||||||
|
import androidx.compose.ui.util.fastMaxBy
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
|
|
||||||
|
val DownloadedOnlyBannerBackgroundColor
|
||||||
|
@Composable get() = MaterialTheme.colorScheme.tertiary
|
||||||
|
val IncognitoModeBannerBackgroundColor
|
||||||
|
@Composable get() = MaterialTheme.colorScheme.primary
|
||||||
|
val IndexingBannerBackgroundColor
|
||||||
|
@Composable get() = MaterialTheme.colorScheme.secondary
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun WarningBanner(
|
fun WarningBanner(
|
||||||
@StringRes textRes: Int,
|
@StringRes textRes: Int,
|
||||||
@ -35,26 +62,77 @@ fun WarningBanner(
|
|||||||
fun AppStateBanners(
|
fun AppStateBanners(
|
||||||
downloadedOnlyMode: Boolean,
|
downloadedOnlyMode: Boolean,
|
||||||
incognitoMode: Boolean,
|
incognitoMode: Boolean,
|
||||||
|
indexing: Boolean,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Column(modifier = modifier) {
|
val density = LocalDensity.current
|
||||||
if (downloadedOnlyMode) {
|
val mainInsets = WindowInsets.statusBars
|
||||||
DownloadedOnlyModeBanner()
|
val mainInsetsTop = mainInsets.getTop(density)
|
||||||
}
|
SubcomposeLayout(modifier = modifier) { constraints ->
|
||||||
if (incognitoMode) {
|
val indexingPlaceable = subcompose(0) {
|
||||||
IncognitoModeBanner()
|
AnimatedVisibility(
|
||||||
|
visible = indexing,
|
||||||
|
enter = expandVertically(),
|
||||||
|
exit = shrinkVertically(),
|
||||||
|
) {
|
||||||
|
IndexingDownloadBanner(
|
||||||
|
modifier = Modifier.windowInsetsPadding(mainInsets),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}.fastMap { it.measure(constraints) }
|
||||||
|
val indexingHeight = indexingPlaceable.fastMaxBy { it.height }?.height ?: 0
|
||||||
|
|
||||||
|
val downloadedOnlyPlaceable = subcompose(1) {
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = downloadedOnlyMode,
|
||||||
|
enter = expandVertically(),
|
||||||
|
exit = shrinkVertically(),
|
||||||
|
) {
|
||||||
|
val top = (mainInsetsTop - indexingHeight).coerceAtLeast(0)
|
||||||
|
DownloadedOnlyModeBanner(
|
||||||
|
modifier = Modifier.windowInsetsPadding(WindowInsets(top = top)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}.fastMap { it.measure(constraints) }
|
||||||
|
val downloadedOnlyHeight = downloadedOnlyPlaceable.fastMaxBy { it.height }?.height ?: 0
|
||||||
|
|
||||||
|
val incognitoPlaceable = subcompose(2) {
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = incognitoMode,
|
||||||
|
enter = expandVertically(),
|
||||||
|
exit = shrinkVertically(),
|
||||||
|
) {
|
||||||
|
val top = (mainInsetsTop - indexingHeight - downloadedOnlyHeight).coerceAtLeast(0)
|
||||||
|
IncognitoModeBanner(
|
||||||
|
modifier = Modifier.windowInsetsPadding(WindowInsets(top = top)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}.fastMap { it.measure(constraints) }
|
||||||
|
val incognitoHeight = incognitoPlaceable.fastMaxBy { it.height }?.height ?: 0
|
||||||
|
|
||||||
|
layout(constraints.maxWidth, indexingHeight + downloadedOnlyHeight + incognitoHeight) {
|
||||||
|
indexingPlaceable.fastForEach {
|
||||||
|
it.place(0, 0)
|
||||||
|
}
|
||||||
|
downloadedOnlyPlaceable.fastForEach {
|
||||||
|
it.place(0, indexingHeight)
|
||||||
|
}
|
||||||
|
incognitoPlaceable.fastForEach {
|
||||||
|
it.place(0, indexingHeight + downloadedOnlyHeight)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun DownloadedOnlyModeBanner() {
|
private fun DownloadedOnlyModeBanner(modifier: Modifier = Modifier) {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.label_downloaded_only),
|
text = stringResource(R.string.label_downloaded_only),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.background(color = MaterialTheme.colorScheme.tertiary)
|
.background(DownloadedOnlyBannerBackgroundColor)
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(4.dp),
|
.padding(4.dp)
|
||||||
|
.then(modifier),
|
||||||
color = MaterialTheme.colorScheme.onTertiary,
|
color = MaterialTheme.colorScheme.onTertiary,
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
style = MaterialTheme.typography.labelMedium,
|
style = MaterialTheme.typography.labelMedium,
|
||||||
@ -62,15 +140,48 @@ private fun DownloadedOnlyModeBanner() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun IncognitoModeBanner() {
|
private fun IncognitoModeBanner(modifier: Modifier = Modifier) {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.pref_incognito_mode),
|
text = stringResource(R.string.pref_incognito_mode),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.background(color = MaterialTheme.colorScheme.primary)
|
.background(IncognitoModeBannerBackgroundColor)
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(4.dp),
|
.padding(4.dp)
|
||||||
|
.then(modifier),
|
||||||
color = MaterialTheme.colorScheme.onPrimary,
|
color = MaterialTheme.colorScheme.onPrimary,
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
style = MaterialTheme.typography.labelMedium,
|
style = MaterialTheme.typography.labelMedium,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun IndexingDownloadBanner(modifier: Modifier = Modifier) {
|
||||||
|
val density = LocalDensity.current
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(color = IndexingBannerBackgroundColor)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(8.dp)
|
||||||
|
.then(modifier),
|
||||||
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
) {
|
||||||
|
var textHeight by remember { mutableStateOf(0.dp) }
|
||||||
|
CircularProgressIndicator(
|
||||||
|
modifier = Modifier.requiredSize(textHeight),
|
||||||
|
color = MaterialTheme.colorScheme.onSecondary,
|
||||||
|
strokeWidth = textHeight / 8,
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.download_notifier_cache_renewal),
|
||||||
|
color = MaterialTheme.colorScheme.onSecondary,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
onTextLayout = {
|
||||||
|
with(density) {
|
||||||
|
textHeight = it.size.height.toDp()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -10,6 +10,7 @@ import androidx.compose.foundation.rememberScrollState
|
|||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.Checkbox
|
import androidx.compose.material3.Checkbox
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TriStateCheckbox
|
import androidx.compose.material3.TriStateCheckbox
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@ -23,7 +24,7 @@ import androidx.compose.ui.res.stringResource
|
|||||||
import eu.kanade.core.prefs.CheckboxState
|
import eu.kanade.core.prefs.CheckboxState
|
||||||
import eu.kanade.domain.category.model.Category
|
import eu.kanade.domain.category.model.Category
|
||||||
import eu.kanade.presentation.category.visualName
|
import eu.kanade.presentation.category.visualName
|
||||||
import eu.kanade.presentation.util.horizontalPadding
|
import eu.kanade.presentation.util.padding
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@ -95,8 +96,7 @@ fun ChangeCategoryDialog(
|
|||||||
val index = selection.indexOf(it)
|
val index = selection.indexOf(it)
|
||||||
if (index != -1) {
|
if (index != -1) {
|
||||||
val mutableList = selection.toMutableList()
|
val mutableList = selection.toMutableList()
|
||||||
mutableList.removeAt(index)
|
mutableList[index] = it.next()
|
||||||
mutableList.add(index, it.next())
|
|
||||||
selection = mutableList.toList()
|
selection = mutableList.toList()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -123,7 +123,7 @@ fun ChangeCategoryDialog(
|
|||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = checkbox.value.visualName,
|
text = checkbox.value.visualName,
|
||||||
modifier = Modifier.padding(horizontal = horizontalPadding),
|
modifier = Modifier.padding(horizontal = MaterialTheme.padding.medium),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -95,7 +95,7 @@ private fun NotDownloadedIndicator(
|
|||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(id = R.drawable.ic_download_chapter_24dp),
|
painter = painterResource(R.drawable.ic_download_chapter_24dp),
|
||||||
contentDescription = stringResource(R.string.manga_download),
|
contentDescription = stringResource(R.string.manga_download),
|
||||||
modifier = Modifier.size(IndicatorSize),
|
modifier = Modifier.size(IndicatorSize),
|
||||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
@ -188,7 +188,7 @@ private fun DownloadedIndicator(
|
|||||||
.size(IconButtonTokens.StateLayerSize)
|
.size(IconButtonTokens.StateLayerSize)
|
||||||
.commonClickable(
|
.commonClickable(
|
||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
onLongClick = { onClick(ChapterDownloadAction.DELETE) },
|
onLongClick = { isMenuExpanded = true },
|
||||||
onClick = { isMenuExpanded = true },
|
onClick = { isMenuExpanded = true },
|
||||||
),
|
),
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
|
@ -12,10 +12,17 @@ import androidx.compose.foundation.layout.fillMaxHeight
|
|||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.PlayArrow
|
||||||
|
import androidx.compose.material3.FilledIconButton
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButtonDefaults
|
||||||
import androidx.compose.material3.LocalContentColor
|
import androidx.compose.material3.LocalContentColor
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.contentColorFor
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
@ -41,6 +48,10 @@ object CommonMangaItemDefaults {
|
|||||||
const val BrowseFavoriteCoverAlpha = 0.34f
|
const val BrowseFavoriteCoverAlpha = 0.34f
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val ContinueReadingButtonSize = 32.dp
|
||||||
|
private val ContinueReadingButtonGridPadding = 6.dp
|
||||||
|
private val ContinueReadingButtonListSpacing = 8.dp
|
||||||
|
|
||||||
private const val GridSelectedCoverAlpha = 0.76f
|
private const val GridSelectedCoverAlpha = 0.76f
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -53,10 +64,11 @@ fun MangaCompactGridItem(
|
|||||||
title: String? = null,
|
title: String? = null,
|
||||||
coverData: eu.kanade.domain.manga.model.MangaCover,
|
coverData: eu.kanade.domain.manga.model.MangaCover,
|
||||||
coverAlpha: Float = 1f,
|
coverAlpha: Float = 1f,
|
||||||
coverBadgeStart: (@Composable RowScope.() -> Unit)? = null,
|
coverBadgeStart: @Composable (RowScope.() -> Unit)? = null,
|
||||||
coverBadgeEnd: (@Composable RowScope.() -> Unit)? = null,
|
coverBadgeEnd: @Composable (RowScope.() -> Unit)? = null,
|
||||||
onLongClick: () -> Unit,
|
onLongClick: () -> Unit,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
|
onClickContinueReading: (() -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
GridItemSelectable(
|
GridItemSelectable(
|
||||||
isSelected = isSelected,
|
isSelected = isSelected,
|
||||||
@ -76,7 +88,17 @@ fun MangaCompactGridItem(
|
|||||||
badgesEnd = coverBadgeEnd,
|
badgesEnd = coverBadgeEnd,
|
||||||
content = {
|
content = {
|
||||||
if (title != null) {
|
if (title != null) {
|
||||||
CoverTextOverlay(title = title)
|
CoverTextOverlay(
|
||||||
|
title = title,
|
||||||
|
onClickContinueReading = onClickContinueReading,
|
||||||
|
)
|
||||||
|
} else if (onClickContinueReading != null) {
|
||||||
|
ContinueReadingButton(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(ContinueReadingButtonGridPadding)
|
||||||
|
.align(Alignment.BottomEnd),
|
||||||
|
onClickContinueReading = onClickContinueReading,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@ -87,7 +109,10 @@ fun MangaCompactGridItem(
|
|||||||
* Title overlay for [MangaCompactGridItem]
|
* Title overlay for [MangaCompactGridItem]
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
private fun BoxScope.CoverTextOverlay(title: String) {
|
private fun BoxScope.CoverTextOverlay(
|
||||||
|
title: String,
|
||||||
|
onClickContinueReading: (() -> Unit)? = null,
|
||||||
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clip(RoundedCornerShape(bottomStart = 4.dp, bottomEnd = 4.dp))
|
.clip(RoundedCornerShape(bottomStart = 4.dp, bottomEnd = 4.dp))
|
||||||
@ -101,19 +126,33 @@ private fun BoxScope.CoverTextOverlay(title: String) {
|
|||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.align(Alignment.BottomCenter),
|
.align(Alignment.BottomCenter),
|
||||||
)
|
)
|
||||||
GridItemTitle(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier.align(Alignment.BottomStart),
|
||||||
.padding(8.dp)
|
verticalAlignment = Alignment.Bottom,
|
||||||
.align(Alignment.BottomStart),
|
) {
|
||||||
title = title,
|
GridItemTitle(
|
||||||
style = MaterialTheme.typography.titleSmall.copy(
|
modifier = Modifier
|
||||||
color = Color.White,
|
.weight(1f)
|
||||||
shadow = Shadow(
|
.padding(8.dp),
|
||||||
color = Color.Black,
|
title = title,
|
||||||
blurRadius = 4f,
|
style = MaterialTheme.typography.titleSmall.copy(
|
||||||
|
color = Color.White,
|
||||||
|
shadow = Shadow(
|
||||||
|
color = Color.Black,
|
||||||
|
blurRadius = 4f,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
)
|
||||||
)
|
if (onClickContinueReading != null) {
|
||||||
|
ContinueReadingButton(
|
||||||
|
modifier = Modifier.padding(
|
||||||
|
end = ContinueReadingButtonGridPadding,
|
||||||
|
bottom = ContinueReadingButtonGridPadding,
|
||||||
|
),
|
||||||
|
onClickContinueReading = onClickContinueReading,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -129,6 +168,7 @@ fun MangaComfortableGridItem(
|
|||||||
coverBadgeEnd: (@Composable RowScope.() -> Unit)? = null,
|
coverBadgeEnd: (@Composable RowScope.() -> Unit)? = null,
|
||||||
onLongClick: () -> Unit,
|
onLongClick: () -> Unit,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
|
onClickContinueReading: (() -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
GridItemSelectable(
|
GridItemSelectable(
|
||||||
isSelected = isSelected,
|
isSelected = isSelected,
|
||||||
@ -147,6 +187,16 @@ fun MangaComfortableGridItem(
|
|||||||
},
|
},
|
||||||
badgesStart = coverBadgeStart,
|
badgesStart = coverBadgeStart,
|
||||||
badgesEnd = coverBadgeEnd,
|
badgesEnd = coverBadgeEnd,
|
||||||
|
content = {
|
||||||
|
if (onClickContinueReading != null) {
|
||||||
|
ContinueReadingButton(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(ContinueReadingButtonGridPadding)
|
||||||
|
.align(Alignment.BottomEnd),
|
||||||
|
onClickContinueReading = onClickContinueReading,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
)
|
)
|
||||||
GridItemTitle(
|
GridItemTitle(
|
||||||
modifier = Modifier.padding(4.dp),
|
modifier = Modifier.padding(4.dp),
|
||||||
@ -259,7 +309,7 @@ private fun Modifier.selectedOutline(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return this then modifierElementOf(
|
return this then modifierElementOf(
|
||||||
params = isSelected.hashCode() + color.hashCode(),
|
key = isSelected.hashCode() + color.hashCode(),
|
||||||
create = { SelectedOutlineNode(isSelected, color) },
|
create = { SelectedOutlineNode(isSelected, color) },
|
||||||
update = {
|
update = {
|
||||||
it.selected = isSelected
|
it.selected = isSelected
|
||||||
@ -282,9 +332,10 @@ fun MangaListItem(
|
|||||||
title: String,
|
title: String,
|
||||||
coverData: eu.kanade.domain.manga.model.MangaCover,
|
coverData: eu.kanade.domain.manga.model.MangaCover,
|
||||||
coverAlpha: Float = 1f,
|
coverAlpha: Float = 1f,
|
||||||
badge: @Composable RowScope.() -> Unit,
|
badge: @Composable (RowScope.() -> Unit),
|
||||||
onLongClick: () -> Unit,
|
onLongClick: () -> Unit,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
|
onClickContinueReading: (() -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@ -313,5 +364,35 @@ fun MangaListItem(
|
|||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
)
|
)
|
||||||
BadgeGroup(content = badge)
|
BadgeGroup(content = badge)
|
||||||
|
if (onClickContinueReading != null) {
|
||||||
|
ContinueReadingButton(
|
||||||
|
modifier = Modifier.padding(start = ContinueReadingButtonListSpacing),
|
||||||
|
onClickContinueReading = onClickContinueReading,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ContinueReadingButton(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onClickContinueReading: () -> Unit,
|
||||||
|
) {
|
||||||
|
Box(modifier = modifier) {
|
||||||
|
FilledIconButton(
|
||||||
|
onClick = onClickContinueReading,
|
||||||
|
modifier = Modifier.size(ContinueReadingButtonSize),
|
||||||
|
shape = MaterialTheme.shapes.small,
|
||||||
|
colors = IconButtonDefaults.filledIconButtonColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.9f),
|
||||||
|
contentColor = contentColorFor(MaterialTheme.colorScheme.primaryContainer),
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.PlayArrow,
|
||||||
|
contentDescription = "",
|
||||||
|
modifier = Modifier.size(16.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -64,8 +64,7 @@ fun DeleteLibraryMangaDialog(
|
|||||||
val onCheck = {
|
val onCheck = {
|
||||||
val index = list.indexOf(state)
|
val index = list.indexOf(state)
|
||||||
val mutableList = list.toMutableList()
|
val mutableList = list.toMutableList()
|
||||||
mutableList.removeAt(index)
|
mutableList[index] = state.next() as CheckboxState.State<Int>
|
||||||
mutableList.add(index, state.next() as CheckboxState.State<Int>)
|
|
||||||
list = mutableList.toList()
|
list = mutableList.toList()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,17 +1,44 @@
|
|||||||
package eu.kanade.presentation.components
|
package eu.kanade.presentation.components
|
||||||
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.material3.DividerDefaults
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.alpha
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
const val DIVIDER_ALPHA = 0.2f
|
const val DIVIDER_ALPHA = 0.2f
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun Divider(
|
fun Divider(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
color: Color = DividerDefaults.color,
|
||||||
) {
|
) {
|
||||||
androidx.compose.material3.Divider(
|
Box(
|
||||||
modifier = modifier,
|
modifier
|
||||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = DIVIDER_ALPHA),
|
.fillMaxWidth()
|
||||||
|
.height(1.dp)
|
||||||
|
.background(color = color)
|
||||||
|
.alpha(DIVIDER_ALPHA),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun VerticalDivider(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
color: Color = DividerDefaults.color,
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier
|
||||||
|
.fillMaxHeight()
|
||||||
|
.width(1.dp)
|
||||||
|
.background(color = color)
|
||||||
|
.alpha(DIVIDER_ALPHA),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
package eu.kanade.presentation.components
|
package eu.kanade.presentation.components
|
||||||
|
|
||||||
import android.content.res.Configuration.UI_MODE_NIGHT_YES
|
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@ -23,14 +22,11 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.layout.Layout
|
|
||||||
import androidx.compose.ui.layout.layoutId
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.util.fastFirstOrNull
|
|
||||||
import eu.kanade.presentation.theme.TachiyomiTheme
|
import eu.kanade.presentation.theme.TachiyomiTheme
|
||||||
|
import eu.kanade.presentation.util.ThemePreviews
|
||||||
import eu.kanade.presentation.util.secondaryItemAlpha
|
import eu.kanade.presentation.util.secondaryItemAlpha
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
@ -55,68 +51,45 @@ fun EmptyScreen(
|
|||||||
actions: List<EmptyScreenAction>? = null,
|
actions: List<EmptyScreenAction>? = null,
|
||||||
) {
|
) {
|
||||||
val face = remember { getRandomErrorFace() }
|
val face = remember { getRandomErrorFace() }
|
||||||
Layout(
|
Column(
|
||||||
content = {
|
modifier = modifier
|
||||||
Column(
|
.fillMaxSize()
|
||||||
modifier = Modifier
|
.padding(horizontal = 24.dp),
|
||||||
.layoutId("face")
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
.padding(horizontal = 24.dp),
|
verticalArrangement = Arrangement.Center,
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
) {
|
||||||
) {
|
Text(
|
||||||
Text(
|
text = face,
|
||||||
text = face,
|
modifier = Modifier.secondaryItemAlpha(),
|
||||||
modifier = Modifier.secondaryItemAlpha(),
|
style = MaterialTheme.typography.displayMedium,
|
||||||
style = MaterialTheme.typography.displayMedium,
|
)
|
||||||
)
|
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = message,
|
text = message,
|
||||||
modifier = Modifier.paddingFromBaseline(top = 24.dp).secondaryItemAlpha(),
|
modifier = Modifier.paddingFromBaseline(top = 24.dp).secondaryItemAlpha(),
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
)
|
)
|
||||||
}
|
|
||||||
if (!actions.isNullOrEmpty()) {
|
if (!actions.isNullOrEmpty()) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.layoutId("actions")
|
.padding(
|
||||||
.padding(
|
top = 24.dp,
|
||||||
top = 24.dp,
|
start = 24.dp,
|
||||||
start = horizontalPadding,
|
end = 24.dp,
|
||||||
end = horizontalPadding,
|
),
|
||||||
),
|
horizontalArrangement = Arrangement.spacedBy(space = 8.dp),
|
||||||
horizontalArrangement = Arrangement.spacedBy(space = 8.dp),
|
) {
|
||||||
) {
|
actions.forEach {
|
||||||
actions.forEach {
|
ActionButton(
|
||||||
ActionButton(
|
modifier = Modifier.weight(1f),
|
||||||
modifier = Modifier.weight(1f),
|
title = stringResource(it.stringResId),
|
||||||
title = stringResource(it.stringResId),
|
icon = it.icon,
|
||||||
icon = it.icon,
|
onClick = it.onClick,
|
||||||
onClick = it.onClick,
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
modifier = modifier.fillMaxSize(),
|
|
||||||
) { measurables, constraints ->
|
|
||||||
val looseConstraints = constraints.copy(minWidth = 0, minHeight = 0)
|
|
||||||
val facePlaceable = measurables.fastFirstOrNull { it.layoutId == "face" }!!
|
|
||||||
.measure(looseConstraints)
|
|
||||||
val actionsPlaceable = measurables.fastFirstOrNull { it.layoutId == "actions" }
|
|
||||||
?.measure(looseConstraints)
|
|
||||||
|
|
||||||
layout(constraints.maxWidth, constraints.maxHeight) {
|
|
||||||
val faceY = (constraints.maxHeight - facePlaceable.height) / 2
|
|
||||||
facePlaceable.placeRelative(
|
|
||||||
x = (constraints.maxWidth - facePlaceable.width) / 2,
|
|
||||||
y = faceY,
|
|
||||||
)
|
|
||||||
|
|
||||||
actionsPlaceable?.placeRelative(
|
|
||||||
x = (constraints.maxWidth - actionsPlaceable.width) / 2,
|
|
||||||
y = faceY + facePlaceable.height,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -146,17 +119,7 @@ private fun ActionButton(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview(
|
@ThemePreviews
|
||||||
name = "Light",
|
|
||||||
widthDp = 400,
|
|
||||||
heightDp = 400,
|
|
||||||
)
|
|
||||||
@Preview(
|
|
||||||
name = "Dark",
|
|
||||||
widthDp = 400,
|
|
||||||
heightDp = 400,
|
|
||||||
uiMode = UI_MODE_NIGHT_YES,
|
|
||||||
)
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun NoActionPreview() {
|
private fun NoActionPreview() {
|
||||||
TachiyomiTheme {
|
TachiyomiTheme {
|
||||||
@ -168,17 +131,7 @@ private fun NoActionPreview() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview(
|
@ThemePreviews
|
||||||
name = "Light",
|
|
||||||
widthDp = 400,
|
|
||||||
heightDp = 400,
|
|
||||||
)
|
|
||||||
@Preview(
|
|
||||||
name = "Dark",
|
|
||||||
widthDp = 400,
|
|
||||||
heightDp = 400,
|
|
||||||
uiMode = UI_MODE_NIGHT_YES,
|
|
||||||
)
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun WithActionPreview() {
|
private fun WithActionPreview() {
|
||||||
TachiyomiTheme {
|
TachiyomiTheme {
|
||||||
@ -208,8 +161,6 @@ data class EmptyScreenAction(
|
|||||||
val onClick: () -> Unit,
|
val onClick: () -> Unit,
|
||||||
)
|
)
|
||||||
|
|
||||||
private val horizontalPadding = 24.dp
|
|
||||||
|
|
||||||
private val ERROR_FACES = listOf(
|
private val ERROR_FACES = listOf(
|
||||||
"(・o・;)",
|
"(・o・;)",
|
||||||
"Σ(ಠ_ಠ)",
|
"Σ(ಠ_ಠ)",
|
||||||
|
@ -0,0 +1,141 @@
|
|||||||
|
package eu.kanade.presentation.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.Newspaper
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.NavigationBarDefaults
|
||||||
|
import androidx.compose.material3.OutlinedButton
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.drawBehind
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.zIndex
|
||||||
|
import eu.kanade.presentation.theme.TachiyomiTheme
|
||||||
|
import eu.kanade.presentation.util.ThemePreviews
|
||||||
|
import eu.kanade.presentation.util.padding
|
||||||
|
import eu.kanade.presentation.util.secondaryItemAlpha
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun InfoScaffold(
|
||||||
|
icon: ImageVector,
|
||||||
|
headingText: String,
|
||||||
|
subtitleText: String,
|
||||||
|
acceptText: String,
|
||||||
|
onAcceptClick: () -> Unit,
|
||||||
|
rejectText: String,
|
||||||
|
onRejectClick: () -> Unit,
|
||||||
|
content: @Composable ColumnScope.() -> Unit,
|
||||||
|
) {
|
||||||
|
Scaffold(
|
||||||
|
bottomBar = {
|
||||||
|
val strokeWidth = Dp.Hairline
|
||||||
|
val borderColor = MaterialTheme.colorScheme.outline
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(MaterialTheme.colorScheme.background)
|
||||||
|
.drawBehind {
|
||||||
|
drawLine(
|
||||||
|
borderColor,
|
||||||
|
Offset(0f, 0f),
|
||||||
|
Offset(size.width, 0f),
|
||||||
|
strokeWidth.value,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.windowInsetsPadding(NavigationBarDefaults.windowInsets)
|
||||||
|
.padding(
|
||||||
|
horizontal = MaterialTheme.padding.medium,
|
||||||
|
vertical = MaterialTheme.padding.small,
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
androidx.compose.material3.Button(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
onClick = onAcceptClick,
|
||||||
|
) {
|
||||||
|
Text(text = acceptText)
|
||||||
|
}
|
||||||
|
OutlinedButton(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
onClick = onRejectClick,
|
||||||
|
) {
|
||||||
|
Text(text = rejectText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) { paddingValues ->
|
||||||
|
// Status bar scrim
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.zIndex(2f)
|
||||||
|
.secondaryItemAlpha()
|
||||||
|
.background(MaterialTheme.colorScheme.background)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(paddingValues.calculateTopPadding()),
|
||||||
|
)
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(paddingValues)
|
||||||
|
.padding(top = 48.dp)
|
||||||
|
.padding(horizontal = MaterialTheme.padding.medium),
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(bottom = MaterialTheme.padding.small)
|
||||||
|
.size(48.dp),
|
||||||
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = headingText,
|
||||||
|
style = MaterialTheme.typography.headlineLarge,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = subtitleText,
|
||||||
|
modifier = Modifier
|
||||||
|
.secondaryItemAlpha()
|
||||||
|
.padding(vertical = MaterialTheme.padding.small),
|
||||||
|
style = MaterialTheme.typography.titleSmall,
|
||||||
|
)
|
||||||
|
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ThemePreviews
|
||||||
|
@Composable
|
||||||
|
private fun InfoScaffoldPreview() {
|
||||||
|
TachiyomiTheme {
|
||||||
|
InfoScaffold(
|
||||||
|
icon = Icons.Outlined.Newspaper,
|
||||||
|
headingText = "Heading",
|
||||||
|
subtitleText = "Subtitle",
|
||||||
|
acceptText = "Accept",
|
||||||
|
onAcceptClick = {},
|
||||||
|
rejectText = "Reject",
|
||||||
|
onRejectClick = {},
|
||||||
|
) {
|
||||||
|
Text("Hello world")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,27 @@
|
|||||||
|
package eu.kanade.presentation.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import eu.kanade.presentation.util.padding
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ListGroupHeader(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
text: String,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
modifier = modifier
|
||||||
|
.padding(
|
||||||
|
horizontal = MaterialTheme.padding.medium,
|
||||||
|
vertical = MaterialTheme.padding.small,
|
||||||
|
),
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
)
|
||||||
|
}
|
@ -8,9 +8,9 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun LoadingScreen() {
|
fun LoadingScreen(modifier: Modifier = Modifier) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = modifier.fillMaxSize(),
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
CircularProgressIndicator()
|
CircularProgressIndicator()
|
||||||
|
@ -2,6 +2,7 @@ package eu.kanade.presentation.components
|
|||||||
|
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.animation.expandVertically
|
import androidx.compose.animation.expandVertically
|
||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
@ -16,10 +17,10 @@ import androidx.compose.foundation.layout.WindowInsets
|
|||||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||||
import androidx.compose.foundation.layout.asPaddingValues
|
import androidx.compose.foundation.layout.asPaddingValues
|
||||||
import androidx.compose.foundation.layout.navigationBars
|
import androidx.compose.foundation.layout.navigationBars
|
||||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
|
||||||
import androidx.compose.foundation.layout.only
|
import androidx.compose.foundation.layout.only
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||||
import androidx.compose.foundation.shape.ZeroCornerSize
|
import androidx.compose.foundation.shape.ZeroCornerSize
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.outlined.BookmarkAdd
|
import androidx.compose.material.icons.outlined.BookmarkAdd
|
||||||
@ -95,7 +96,11 @@ fun MangaBottomActionMenu(
|
|||||||
}
|
}
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(WindowInsets.navigationBars.only(WindowInsetsSides.Bottom).asPaddingValues())
|
.padding(
|
||||||
|
WindowInsets.navigationBars
|
||||||
|
.only(WindowInsetsSides.Bottom)
|
||||||
|
.asPaddingValues(),
|
||||||
|
)
|
||||||
.padding(horizontal = 8.dp, vertical = 12.dp),
|
.padding(horizontal = 8.dp, vertical = 12.dp),
|
||||||
) {
|
) {
|
||||||
if (onBookmarkClicked != null) {
|
if (onBookmarkClicked != null) {
|
||||||
@ -213,16 +218,16 @@ private fun RowScope.Button(
|
|||||||
fun LibraryBottomActionMenu(
|
fun LibraryBottomActionMenu(
|
||||||
visible: Boolean,
|
visible: Boolean,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onChangeCategoryClicked: (() -> Unit)?,
|
onChangeCategoryClicked: () -> Unit,
|
||||||
onMarkAsReadClicked: (() -> Unit)?,
|
onMarkAsReadClicked: () -> Unit,
|
||||||
onMarkAsUnreadClicked: (() -> Unit)?,
|
onMarkAsUnreadClicked: () -> Unit,
|
||||||
onDownloadClicked: ((DownloadAction) -> Unit)?,
|
onDownloadClicked: ((DownloadAction) -> Unit)?,
|
||||||
onDeleteClicked: (() -> Unit)?,
|
onDeleteClicked: () -> Unit,
|
||||||
) {
|
) {
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = visible,
|
visible = visible,
|
||||||
enter = expandVertically(expandFrom = Alignment.Bottom),
|
enter = expandVertically(animationSpec = tween(delayMillis = 300)),
|
||||||
exit = shrinkVertically(shrinkTowards = Alignment.Bottom),
|
exit = shrinkVertically(animationSpec = tween()),
|
||||||
) {
|
) {
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
Surface(
|
Surface(
|
||||||
@ -244,36 +249,33 @@ fun LibraryBottomActionMenu(
|
|||||||
}
|
}
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.navigationBarsPadding()
|
.windowInsetsPadding(
|
||||||
|
WindowInsets.navigationBars
|
||||||
|
.only(WindowInsetsSides.Bottom),
|
||||||
|
)
|
||||||
.padding(horizontal = 8.dp, vertical = 12.dp),
|
.padding(horizontal = 8.dp, vertical = 12.dp),
|
||||||
) {
|
) {
|
||||||
if (onChangeCategoryClicked != null) {
|
Button(
|
||||||
Button(
|
title = stringResource(R.string.action_move_category),
|
||||||
title = stringResource(R.string.action_move_category),
|
icon = Icons.Outlined.Label,
|
||||||
icon = Icons.Outlined.Label,
|
toConfirm = confirm[0],
|
||||||
toConfirm = confirm[0],
|
onLongClick = { onLongClickItem(0) },
|
||||||
onLongClick = { onLongClickItem(0) },
|
onClick = onChangeCategoryClicked,
|
||||||
onClick = onChangeCategoryClicked,
|
)
|
||||||
)
|
Button(
|
||||||
}
|
title = stringResource(R.string.action_mark_as_read),
|
||||||
if (onMarkAsReadClicked != null) {
|
icon = Icons.Outlined.DoneAll,
|
||||||
Button(
|
toConfirm = confirm[1],
|
||||||
title = stringResource(R.string.action_mark_as_read),
|
onLongClick = { onLongClickItem(1) },
|
||||||
icon = Icons.Outlined.DoneAll,
|
onClick = onMarkAsReadClicked,
|
||||||
toConfirm = confirm[1],
|
)
|
||||||
onLongClick = { onLongClickItem(1) },
|
Button(
|
||||||
onClick = onMarkAsReadClicked,
|
title = stringResource(R.string.action_mark_as_unread),
|
||||||
)
|
icon = Icons.Outlined.RemoveDone,
|
||||||
}
|
toConfirm = confirm[2],
|
||||||
if (onMarkAsUnreadClicked != null) {
|
onLongClick = { onLongClickItem(2) },
|
||||||
Button(
|
onClick = onMarkAsUnreadClicked,
|
||||||
title = stringResource(R.string.action_mark_as_unread),
|
)
|
||||||
icon = Icons.Outlined.RemoveDone,
|
|
||||||
toConfirm = confirm[2],
|
|
||||||
onLongClick = { onLongClickItem(2) },
|
|
||||||
onClick = onMarkAsUnreadClicked,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (onDownloadClicked != null) {
|
if (onDownloadClicked != null) {
|
||||||
var downloadExpanded by remember { mutableStateOf(false) }
|
var downloadExpanded by remember { mutableStateOf(false) }
|
||||||
Button(
|
Button(
|
||||||
@ -292,15 +294,13 @@ fun LibraryBottomActionMenu(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (onDeleteClicked != null) {
|
Button(
|
||||||
Button(
|
title = stringResource(R.string.action_delete),
|
||||||
title = stringResource(R.string.action_delete),
|
icon = Icons.Outlined.Delete,
|
||||||
icon = Icons.Outlined.Delete,
|
toConfirm = confirm[4],
|
||||||
toConfirm = confirm[4],
|
onLongClick = { onLongClickItem(4) },
|
||||||
onLongClick = { onLongClickItem(4) },
|
onClick = onDeleteClicked,
|
||||||
onClick = onDeleteClicked,
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,48 @@
|
|||||||
|
package eu.kanade.presentation.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.RowScope
|
||||||
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||||
|
import androidx.compose.foundation.selection.selectableGroup
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.NavigationBarDefaults
|
||||||
|
import androidx.compose.material3.contentColorFor
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
/**
|
||||||
|
* M3 Navbar with no horizontal spacer
|
||||||
|
*
|
||||||
|
* @see [androidx.compose.material3.NavigationBar]
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun NavigationBar(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
containerColor: Color = NavigationBarDefaults.containerColor,
|
||||||
|
contentColor: Color = MaterialTheme.colorScheme.contentColorFor(containerColor),
|
||||||
|
tonalElevation: Dp = NavigationBarDefaults.Elevation,
|
||||||
|
windowInsets: WindowInsets = NavigationBarDefaults.windowInsets,
|
||||||
|
content: @Composable RowScope.() -> Unit,
|
||||||
|
) {
|
||||||
|
androidx.compose.material3.Surface(
|
||||||
|
color = containerColor,
|
||||||
|
contentColor = contentColor,
|
||||||
|
tonalElevation = tonalElevation,
|
||||||
|
modifier = modifier,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.windowInsetsPadding(windowInsets)
|
||||||
|
.height(80.dp)
|
||||||
|
.selectableGroup(),
|
||||||
|
content = content,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user