Compare commits
191 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fd120c5081 | |||
| 34e9d9f146 | |||
| b7f7187293 | |||
| 4abadea4f9 | |||
| 1b3d76398b | |||
| 688fdecaf8 | |||
| 0bedee1778 | |||
| bb89f9f636 | |||
| f8011981eb | |||
| 7e17e52e07 | |||
| b65990ad29 | |||
| d9560d40de | |||
| 036ab3351d | |||
| 769293355f | |||
| 850d81600e | |||
| ce96b53f10 | |||
| b98dfd65b5 | |||
| 612e0a00bc | |||
| d286cf3267 | |||
| 1a28c7fb35 | |||
| 5909f90003 | |||
| 48f7b701dc | |||
| b17530ccc3 | |||
| f844a48b67 | |||
| 66929e097c | |||
| be30814d35 | |||
| 5d56c1961d | |||
| 4aa52a2576 | |||
| f7a1869066 | |||
| 2f1d76cbac | |||
| 5c5e08b99b | |||
| cc16d53ecc | |||
| 28fa3855c2 | |||
| 5a47a58e1e | |||
| c86714ef59 | |||
| 75fe57b851 | |||
| b9fffc45cc | |||
| de6cd169d0 | |||
| 95e8a02e33 | |||
| c720f0ac5c | |||
| 76af3b59f0 | |||
| 3f8cce8a32 | |||
| 26cfb4811f | |||
| e5a6d1b456 | |||
| f0b621dfe5 | |||
| d88f570f65 | |||
| b430e31da4 | |||
| 271f2d37bb | |||
| c2e36b4c5c | |||
| cb25deb5ac | |||
| a6c6cf77bb | |||
| e3dae57e0b | |||
| 226321f334 | |||
| 2187731d70 | |||
| fd32f2e879 | |||
| 5a094850d1 | |||
| e74053e989 | |||
| 798db44908 | |||
| 7715b5bdd0 | |||
| 084e11f21d | |||
| 01792c0618 | |||
| 0b93ceaa8f | |||
| bfdbe18509 | |||
| e3245d0610 | |||
| c2df6ee54a | |||
| ffc1e2d97b | |||
| d0c8b0c98a | |||
| f206ab8b32 | |||
| a443629234 | |||
| 3de4711e03 | |||
| 106f63a657 | |||
| 3c09343f7b | |||
| 86e1406565 | |||
| b48556aa9f | |||
| f3e905513f | |||
| 633a1892b3 | |||
| 74cf08b47b | |||
| cc7ce80abf | |||
| e06941f82d | |||
| a8a290d03d | |||
| b49ca3ce4c | |||
| c51c364cdd | |||
| 366415d323 | |||
| 14f6fd7908 | |||
| 15f1ee2205 | |||
| 651579b243 | |||
| 8f596069fa | |||
| a28d526102 | |||
| bbaa74d99c | |||
| 310b1ad69b | |||
| 7f37989c4e | |||
| 185920b984 | |||
| 4639077756 | |||
| 0bf1519c25 | |||
| 45a36cef32 | |||
| dece1bc0cb | |||
| eaffd3f2dc | |||
| aabe409ee5 | |||
| e626cdd030 | |||
| b161c333ec | |||
| e587bb7f44 | |||
| 6cf7ef7bba | |||
| 91d61a75e3 | |||
| 95ae5211a7 | |||
| 62afbf8ff3 | |||
| 2ea8449eb7 | |||
| 697b0de226 | |||
| 41e523e074 | |||
| dee543c7c5 | |||
| 788d3797cb | |||
| 6464c00503 | |||
| dc88ea8f63 | |||
| 95cbb35152 | |||
| 558ce084c8 | |||
| 943555c0af | |||
| 216bc2c57d | |||
| cde3002355 | |||
| db907cf270 | |||
| a269802af9 | |||
| affab50a02 | |||
| 2f102db19d | |||
| 457e5f963b | |||
| 2bd9a914c1 | |||
| f6d2d0bd48 | |||
| 91ae683b74 | |||
| bccd1eff2b | |||
| 9ed90eb6f2 | |||
| a246d897de | |||
| 4923ba0b54 | |||
| bd278b1878 | |||
| ea0816a6c1 | |||
| af3c7a0753 | |||
| 4a9184bfc1 | |||
| 77d75de855 | |||
| d7fbdb1b35 | |||
| 6ad9eb098f | |||
| a6667bc91d | |||
| 1e7b6d488c | |||
| d0ef7bcd54 | |||
| e29e7c9169 | |||
| 4eb8dc35b9 | |||
| 1077820d59 | |||
| ccf1f3b6ef | |||
| 73d91a8537 | |||
| a141e63408 | |||
| 3e1c346a04 | |||
| 6872dc449f | |||
| c672548491 | |||
| 74abed9abd | |||
| cb813908a6 | |||
| 049a395790 | |||
| bc79694eae | |||
| 812f76b8f5 | |||
| 4d8b5fc8a1 | |||
| a40c54e60c | |||
| e8ff402fff | |||
| 0753ffe425 | |||
| 03aa27fb6b | |||
| 51b9004a2d | |||
| 23285587a7 | |||
| 5dcc02c44f | |||
| ecd38d9429 | |||
| 3b3e3f5d35 | |||
| 0d66d03f56 | |||
| a68bb60126 | |||
| 1e9f7612f0 | |||
| 51229ca511 | |||
| b98e198e15 | |||
| 3cac63ed91 | |||
| 6bb2bc03f3 | |||
| da3823daed | |||
| 3edb03de32 | |||
| 3408ef635d | |||
| 365cd0b14d | |||
| 96439afce4 | |||
| c1c615000b | |||
| 58df8b79fb | |||
| f524763854 | |||
| 402f5e6bad | |||
| 0d13c6187c | |||
| 1853a86a73 | |||
| 77e6e06cfa | |||
| 21440a0290 | |||
| d6ffef15e1 | |||
| 3cc250e122 | |||
| 051c559840 | |||
| 3972d7fe4b | |||
| 44fd9f3564 | |||
| f36906df45 | |||
| efbaf1a4ca | |||
| 2f8efe0526 |
@@ -53,7 +53,7 @@ body:
|
|||||||
label: TachiyomiSY version
|
label: TachiyomiSY version
|
||||||
description: You can find your TachiyomiSY version in **More → About**.
|
description: You can find your TachiyomiSY version in **More → About**.
|
||||||
placeholder: |
|
placeholder: |
|
||||||
Example: "1.10.5"
|
Example: "1.11.0"
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
@@ -63,7 +63,7 @@ body:
|
|||||||
label: Android version
|
label: Android version
|
||||||
description: You can find this somewhere in your Android settings.
|
description: You can find this somewhere in your Android settings.
|
||||||
placeholder: |
|
placeholder: |
|
||||||
Example: "Android 11"
|
Example: "Android 14"
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
@@ -73,7 +73,7 @@ body:
|
|||||||
label: Device
|
label: Device
|
||||||
description: List your device and model.
|
description: List your device and model.
|
||||||
placeholder: |
|
placeholder: |
|
||||||
Example: "Google Pixel 5"
|
Example: "Google Pixel 8"
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
@@ -96,7 +96,7 @@ body:
|
|||||||
required: true
|
required: true
|
||||||
- label: I have gone through the [FAQ](https://mihon.app/docs/faq/general) and [troubleshooting guide](https:/mihon.app/docs/guides/troubleshooting/).
|
- label: I have gone through the [FAQ](https://mihon.app/docs/faq/general) and [troubleshooting guide](https:/mihon.app/docs/guides/troubleshooting/).
|
||||||
required: true
|
required: true
|
||||||
- label: I have updated the app to version **[1.10.5](https://github.com/jobobby04/tachiyomisy/releases/latest)**.
|
- label: I have updated the app to version **[1.11.0](https://github.com/jobobby04/tachiyomisy/releases/latest)**.
|
||||||
required: true
|
required: true
|
||||||
- label: I have updated all installed extensions.
|
- label: I have updated all installed extensions.
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ body:
|
|||||||
required: true
|
required: true
|
||||||
- label: I have written a short but informative title.
|
- label: I have written a short but informative title.
|
||||||
required: true
|
required: true
|
||||||
- label: I have updated the app to version **[1.10.5](https://github.com/jobobby04/tachiyomisy/releases/latest)**.
|
- label: I have updated the app to version **[1.11.0](https://github.com/jobobby04/tachiyomisy/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
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||||
"extends": [
|
"extends": ["config:base"],
|
||||||
"config:base"
|
|
||||||
],
|
|
||||||
"labels": ["Dependencies"],
|
"labels": ["Dependencies"],
|
||||||
"includePaths": [".github/workflows/*", "gradle/sy.versions.toml"],
|
"includePaths": [".github/workflows/*", "gradle/sy.versions.toml"],
|
||||||
|
"semanticCommits": "disabled"
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-20
@@ -1,26 +1,21 @@
|
|||||||
|
# Build files
|
||||||
.gradle
|
.gradle
|
||||||
.kotlin
|
.kotlin
|
||||||
/local.properties
|
build
|
||||||
/.idea/workspace.xml
|
|
||||||
.DS_Store
|
# IDE files
|
||||||
|
*.iml
|
||||||
.idea/*
|
.idea/*
|
||||||
!.idea/icon.png
|
!.idea/icon.png
|
||||||
*iml
|
|
||||||
*.iml
|
|
||||||
/mainframer
|
|
||||||
/.mainframer
|
|
||||||
|
|
||||||
# Built files
|
|
||||||
*/build
|
|
||||||
/build
|
|
||||||
*.apk
|
|
||||||
app/**/output.json
|
|
||||||
|
|
||||||
# Unnecessary file
|
|
||||||
*.swp
|
|
||||||
|
|
||||||
TODO.md
|
|
||||||
CHANGELOG.md
|
|
||||||
/captures
|
/captures
|
||||||
build.sh
|
|
||||||
|
# Configuration files
|
||||||
|
local.properties
|
||||||
|
|
||||||
|
# macOS specific files
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# SY ignores
|
||||||
|
google-services.json
|
||||||
/app/src/main/assets/client_secrets.json
|
/app/src/main/assets/client_secrets.json
|
||||||
|
*.apk
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
/build
|
|
||||||
*iml
|
|
||||||
*.iml
|
|
||||||
google-services.json
|
|
||||||
+42
-35
@@ -1,22 +1,24 @@
|
|||||||
|
@file:Suppress("ChromeOsAbiSupport")
|
||||||
|
|
||||||
import mihon.buildlogic.getBuildTime
|
import mihon.buildlogic.getBuildTime
|
||||||
import mihon.buildlogic.getCommitCount
|
import mihon.buildlogic.getCommitCount
|
||||||
import mihon.buildlogic.getGitSha
|
import mihon.buildlogic.getGitSha
|
||||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("mihon.android.application")
|
id("mihon.android.application")
|
||||||
id("mihon.android.application.compose")
|
id("mihon.android.application.compose")
|
||||||
id("com.mikepenz.aboutlibraries.plugin")
|
|
||||||
kotlin("plugin.parcelize")
|
kotlin("plugin.parcelize")
|
||||||
kotlin("plugin.serialization")
|
kotlin("plugin.serialization")
|
||||||
// id("com.github.zellius.shortcut-helper")
|
// id("com.github.zellius.shortcut-helper")
|
||||||
|
alias(libs.plugins.aboutLibraries)
|
||||||
id("com.github.ben-manes.versions")
|
id("com.github.ben-manes.versions")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (gradle.startParameter.taskRequests.toString().contains("Standard")) {
|
if (gradle.startParameter.taskRequests.toString().contains("Standard")) {
|
||||||
apply<com.google.gms.googleservices.GoogleServicesPlugin>()
|
pluginManager.apply {
|
||||||
// Firebase Crashlytics
|
apply(libs.plugins.google.services.get().pluginId)
|
||||||
apply(plugin = "com.google.firebase.crashlytics")
|
apply(libs.plugins.firebase.crashlytics.get().pluginId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// shortcutHelper.setFilePath("./shortcuts.xml")
|
// shortcutHelper.setFilePath("./shortcuts.xml")
|
||||||
@@ -29,8 +31,8 @@ android {
|
|||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "eu.kanade.tachiyomi.sy"
|
applicationId = "eu.kanade.tachiyomi.sy"
|
||||||
|
|
||||||
versionCode = 69
|
versionCode = 71
|
||||||
versionName = "1.10.5"
|
versionName = "1.11.0"
|
||||||
|
|
||||||
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
||||||
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
|
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
|
||||||
@@ -106,13 +108,16 @@ android {
|
|||||||
packaging {
|
packaging {
|
||||||
resources.excludes.addAll(
|
resources.excludes.addAll(
|
||||||
listOf(
|
listOf(
|
||||||
|
"kotlin-tooling-metadata.json",
|
||||||
"META-INF/DEPENDENCIES",
|
"META-INF/DEPENDENCIES",
|
||||||
"LICENSE.txt",
|
"LICENSE.txt",
|
||||||
"META-INF/LICENSE",
|
"META-INF/LICENSE",
|
||||||
"META-INF/LICENSE.txt",
|
"META-INF/**/LICENSE.txt",
|
||||||
|
"META-INF/*.properties",
|
||||||
|
"META-INF/**/*.properties",
|
||||||
"META-INF/README.md",
|
"META-INF/README.md",
|
||||||
"META-INF/NOTICE",
|
"META-INF/NOTICE",
|
||||||
"META-INF/*.kotlin_module",
|
"META-INF/*.version",
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -137,6 +142,24 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
compilerOptions {
|
||||||
|
freeCompilerArgs.addAll(
|
||||||
|
"-opt-in=androidx.compose.animation.ExperimentalAnimationApi",
|
||||||
|
"-opt-in=androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi",
|
||||||
|
"-opt-in=androidx.compose.foundation.ExperimentalFoundationApi",
|
||||||
|
"-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi",
|
||||||
|
"-opt-in=androidx.compose.material3.ExperimentalMaterial3Api",
|
||||||
|
"-opt-in=androidx.compose.ui.ExperimentalComposeUiApi",
|
||||||
|
"-opt-in=coil3.annotation.ExperimentalCoilApi",
|
||||||
|
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
||||||
|
"-opt-in=kotlinx.coroutines.FlowPreview",
|
||||||
|
"-opt-in=kotlinx.coroutines.InternalCoroutinesApi",
|
||||||
|
"-opt-in=kotlinx.serialization.ExperimentalSerializationApi",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(projects.i18n)
|
implementation(projects.i18n)
|
||||||
// SY -->
|
// SY -->
|
||||||
@@ -161,7 +184,6 @@ dependencies {
|
|||||||
debugImplementation(compose.ui.tooling)
|
debugImplementation(compose.ui.tooling)
|
||||||
implementation(compose.ui.tooling.preview)
|
implementation(compose.ui.tooling.preview)
|
||||||
implementation(compose.ui.util)
|
implementation(compose.ui.util)
|
||||||
implementation(compose.accompanist.systemuicontroller)
|
|
||||||
|
|
||||||
implementation(androidx.interpolator)
|
implementation(androidx.interpolator)
|
||||||
|
|
||||||
@@ -247,7 +269,9 @@ dependencies {
|
|||||||
implementation(libs.logcat)
|
implementation(libs.logcat)
|
||||||
|
|
||||||
// Crash reports/analytics
|
// Crash reports/analytics
|
||||||
// "standardImplementation"(libs.firebase.analytics)
|
// "standardImplementation"(platform(libs.firebase.bom))
|
||||||
|
// "standardImplementation"(libs.firebase.analytics)
|
||||||
|
// "standardImplementation"(libs.firebase.crashlytics)
|
||||||
|
|
||||||
// Shizuku
|
// Shizuku
|
||||||
implementation(libs.bundles.shizuku)
|
implementation(libs.bundles.shizuku)
|
||||||
@@ -266,8 +290,9 @@ dependencies {
|
|||||||
implementation(sylibs.simularity)
|
implementation(sylibs.simularity)
|
||||||
|
|
||||||
// Firebase (EH)
|
// Firebase (EH)
|
||||||
implementation(sylibs.firebase.analytics)
|
implementation(platform(libs.firebase.bom))
|
||||||
implementation(sylibs.firebase.crashlytics.ktx)
|
implementation(libs.firebase.analytics)
|
||||||
|
implementation(libs.firebase.crashlytics)
|
||||||
|
|
||||||
// Better logging (EH)
|
// Better logging (EH)
|
||||||
implementation(sylibs.xlog)
|
implementation(sylibs.xlog)
|
||||||
@@ -279,6 +304,10 @@ dependencies {
|
|||||||
// Google drive
|
// Google drive
|
||||||
implementation(sylibs.google.api.services.drive)
|
implementation(sylibs.google.api.services.drive)
|
||||||
implementation(sylibs.google.api.client.oauth)
|
implementation(sylibs.google.api.client.oauth)
|
||||||
|
|
||||||
|
// Koin
|
||||||
|
implementation(sylibs.koin.core)
|
||||||
|
implementation(sylibs.koin.android)
|
||||||
}
|
}
|
||||||
|
|
||||||
androidComponents {
|
androidComponents {
|
||||||
@@ -297,28 +326,6 @@ androidComponents {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks {
|
|
||||||
// See https://kotlinlang.org/docs/reference/experimental.html#experimental-status-of-experimental-api(-markers)
|
|
||||||
withType<KotlinCompile> {
|
|
||||||
compilerOptions.freeCompilerArgs.addAll(
|
|
||||||
"-Xcontext-receivers",
|
|
||||||
"-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi",
|
|
||||||
"-opt-in=androidx.compose.material.ExperimentalMaterialApi",
|
|
||||||
"-opt-in=androidx.compose.material3.ExperimentalMaterial3Api",
|
|
||||||
"-opt-in=androidx.compose.material.ExperimentalMaterialApi",
|
|
||||||
"-opt-in=androidx.compose.ui.ExperimentalComposeUiApi",
|
|
||||||
"-opt-in=androidx.compose.foundation.ExperimentalFoundationApi",
|
|
||||||
"-opt-in=androidx.compose.animation.ExperimentalAnimationApi",
|
|
||||||
"-opt-in=androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi",
|
|
||||||
"-opt-in=coil3.annotation.ExperimentalCoilApi",
|
|
||||||
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
|
||||||
"-opt-in=kotlinx.coroutines.FlowPreview",
|
|
||||||
"-opt-in=kotlinx.coroutines.InternalCoroutinesApi",
|
|
||||||
"-opt-in=kotlinx.serialization.ExperimentalSerializationApi",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
buildscript {
|
buildscript {
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath(kotlinx.gradle)
|
classpath(kotlinx.gradle)
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package mihon.core.firebase
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
|
||||||
|
object FirebaseConfig {
|
||||||
|
fun init(context: Context) = Unit
|
||||||
|
|
||||||
|
fun setAnalyticsEnabled(enabled: Boolean) = Unit
|
||||||
|
|
||||||
|
fun setCrashlyticsEnabled(enabled: Boolean) = Unit
|
||||||
|
}
|
||||||
@@ -34,11 +34,23 @@
|
|||||||
|
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||||
|
|
||||||
<!-- Remove permission from Firebase dependency -->
|
<!-- Remove unnecessary permissions from Firebase dependency -->
|
||||||
|
<uses-permission
|
||||||
|
android:name="com.google.android.finsky.permission.BIND_GET_INSTALL_REFERRER_SERVICE"
|
||||||
|
tools:node="remove" />
|
||||||
|
|
||||||
<uses-permission
|
<uses-permission
|
||||||
android:name="com.google.android.gms.permission.AD_ID"
|
android:name="com.google.android.gms.permission.AD_ID"
|
||||||
tools:node="remove" />
|
tools:node="remove" />
|
||||||
|
|
||||||
|
<uses-permission
|
||||||
|
android:name="android.permission.ACCESS_ADSERVICES_ATTRIBUTION"
|
||||||
|
tools:node="remove" />
|
||||||
|
|
||||||
|
<uses-permission
|
||||||
|
android:name="android.permission.ACCESS_ADSERVICES_AD_ID"
|
||||||
|
tools:node="remove" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".App"
|
android:name=".App"
|
||||||
android:allowBackup="false"
|
android:allowBackup="false"
|
||||||
@@ -256,6 +268,14 @@
|
|||||||
android:name="android.webkit.WebView.MetricsOptOut"
|
android:name="android.webkit.WebView.MetricsOptOut"
|
||||||
android:value="true" />
|
android:value="true" />
|
||||||
|
|
||||||
|
<!-- Disable for manual opt-in -->
|
||||||
|
<meta-data
|
||||||
|
android:name="firebase_analytics_collection_enabled"
|
||||||
|
android:value="false" />
|
||||||
|
<meta-data
|
||||||
|
android:name="firebase_crashlytics_collection_enabled"
|
||||||
|
android:value="false" />
|
||||||
|
|
||||||
<!-- Disable advertising ID collection for Firebase -->
|
<!-- Disable advertising ID collection for Firebase -->
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="google_analytics_adid_collection_enabled"
|
android:name="google_analytics_adid_collection_enabled"
|
||||||
@@ -395,4 +415,7 @@
|
|||||||
</activity>
|
</activity>
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
|
<uses-sdk tools:overrideLibrary="rikka.shizuku.api"
|
||||||
|
tools:ignore="ManifestOrder" />
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
package eu.kanade.core.util
|
package eu.kanade.core.util
|
||||||
|
|
||||||
|
import androidx.compose.ui.util.fastFilter
|
||||||
import androidx.compose.ui.util.fastForEach
|
import androidx.compose.ui.util.fastForEach
|
||||||
import kotlin.contracts.ExperimentalContracts
|
import kotlin.contracts.ExperimentalContracts
|
||||||
import kotlin.contracts.contract
|
import kotlin.contracts.contract
|
||||||
|
|
||||||
fun <T : R, R : Any> List<T>.insertSeparators(
|
fun <T : R, R : Any> List<T>.insertSeparators(
|
||||||
generator: (T?, T?) -> R?,
|
generator: (before: T?, after: T?) -> R?,
|
||||||
): List<R> {
|
): List<R> {
|
||||||
if (isEmpty()) return emptyList()
|
if (isEmpty()) return emptyList()
|
||||||
val newList = mutableListOf<R>()
|
val newList = mutableListOf<R>()
|
||||||
@@ -19,6 +20,24 @@ fun <T : R, R : Any> List<T>.insertSeparators(
|
|||||||
return newList
|
return newList
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Similar to [eu.kanade.core.util.insertSeparators] but iterates from last to first element
|
||||||
|
*/
|
||||||
|
fun <T : R, R : Any> List<T>.insertSeparatorsReversed(
|
||||||
|
generator: (before: T?, after: T?) -> R?,
|
||||||
|
): List<R> {
|
||||||
|
if (isEmpty()) return emptyList()
|
||||||
|
val newList = mutableListOf<R>()
|
||||||
|
for (i in size downTo 0) {
|
||||||
|
val after = getOrNull(i)
|
||||||
|
after?.let(newList::add)
|
||||||
|
val before = getOrNull(i - 1)
|
||||||
|
val separator = generator.invoke(before, after)
|
||||||
|
separator?.let(newList::add)
|
||||||
|
}
|
||||||
|
return newList.asReversed()
|
||||||
|
}
|
||||||
|
|
||||||
fun <E> HashSet<E>.addOrRemove(value: E, shouldAdd: Boolean) {
|
fun <E> HashSet<E>.addOrRemove(value: E, shouldAdd: Boolean) {
|
||||||
if (shouldAdd) {
|
if (shouldAdd) {
|
||||||
add(value)
|
add(value)
|
||||||
@@ -27,21 +46,6 @@ fun <E> HashSet<E>.addOrRemove(value: E, shouldAdd: Boolean) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 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].
|
* Returns a list containing all elements not matching the given [predicate].
|
||||||
*
|
*
|
||||||
@@ -52,27 +56,7 @@ inline fun <T> List<T>.fastFilter(predicate: (T) -> Boolean): List<T> {
|
|||||||
@OptIn(ExperimentalContracts::class)
|
@OptIn(ExperimentalContracts::class)
|
||||||
inline fun <T> List<T>.fastFilterNot(predicate: (T) -> Boolean): List<T> {
|
inline fun <T> List<T>.fastFilterNot(predicate: (T) -> Boolean): List<T> {
|
||||||
contract { callsInPlace(predicate) }
|
contract { callsInPlace(predicate) }
|
||||||
val destination = ArrayList<T>()
|
return fastFilter { !predicate(it) }
|
||||||
fastForEach { if (!predicate(it)) destination.add(it) }
|
|
||||||
return destination
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a list containing only the non-null results of applying the
|
|
||||||
* given [transform] function to each element in the original collection.
|
|
||||||
*
|
|
||||||
* **Do not use for collections that come from public APIs**, since they may not support random
|
|
||||||
* access in an efficient way, and this method may actually be a lot slower. Only use for
|
|
||||||
* collections that are created by code we control and are known to support random access.
|
|
||||||
*/
|
|
||||||
@OptIn(ExperimentalContracts::class)
|
|
||||||
inline fun <T, R> List<T>.fastMapNotNull(transform: (T) -> R?): List<R> {
|
|
||||||
contract { callsInPlace(transform) }
|
|
||||||
val destination = ArrayList<R>()
|
|
||||||
fastForEach { element ->
|
|
||||||
transform(element)?.let(destination::add)
|
|
||||||
}
|
|
||||||
return destination
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -113,26 +97,3 @@ inline fun <T> List<T>.fastCountNot(predicate: (T) -> Boolean): Int {
|
|||||||
fastForEach { if (predicate(it)) --count }
|
fastForEach { if (predicate(it)) --count }
|
||||||
return 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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -23,6 +23,9 @@ import eu.kanade.domain.track.interactor.AddTracks
|
|||||||
import eu.kanade.domain.track.interactor.RefreshTracks
|
import eu.kanade.domain.track.interactor.RefreshTracks
|
||||||
import eu.kanade.domain.track.interactor.SyncChapterProgressWithTrack
|
import eu.kanade.domain.track.interactor.SyncChapterProgressWithTrack
|
||||||
import eu.kanade.domain.track.interactor.TrackChapter
|
import eu.kanade.domain.track.interactor.TrackChapter
|
||||||
|
import eu.kanade.tachiyomi.di.InjektModule
|
||||||
|
import eu.kanade.tachiyomi.di.addFactory
|
||||||
|
import eu.kanade.tachiyomi.di.addSingletonFactory
|
||||||
import mihon.data.repository.ExtensionRepoRepositoryImpl
|
import mihon.data.repository.ExtensionRepoRepositoryImpl
|
||||||
import mihon.domain.chapter.interactor.FilterChaptersForDownload
|
import mihon.domain.chapter.interactor.FilterChaptersForDownload
|
||||||
import mihon.domain.extensionrepo.interactor.CreateExtensionRepo
|
import mihon.domain.extensionrepo.interactor.CreateExtensionRepo
|
||||||
@@ -91,11 +94,7 @@ import tachiyomi.domain.track.interactor.InsertTrack
|
|||||||
import tachiyomi.domain.track.repository.TrackRepository
|
import tachiyomi.domain.track.repository.TrackRepository
|
||||||
import tachiyomi.domain.updates.interactor.GetUpdates
|
import tachiyomi.domain.updates.interactor.GetUpdates
|
||||||
import tachiyomi.domain.updates.repository.UpdatesRepository
|
import tachiyomi.domain.updates.repository.UpdatesRepository
|
||||||
import uy.kohesive.injekt.api.InjektModule
|
|
||||||
import uy.kohesive.injekt.api.InjektRegistrar
|
import uy.kohesive.injekt.api.InjektRegistrar
|
||||||
import uy.kohesive.injekt.api.addFactory
|
|
||||||
import uy.kohesive.injekt.api.addSingletonFactory
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
|
|
||||||
class DomainModule : InjektModule {
|
class DomainModule : InjektModule {
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ import eu.kanade.domain.source.interactor.GetSourceCategories
|
|||||||
import eu.kanade.domain.source.interactor.RenameSourceCategory
|
import eu.kanade.domain.source.interactor.RenameSourceCategory
|
||||||
import eu.kanade.domain.source.interactor.SetSourceCategories
|
import eu.kanade.domain.source.interactor.SetSourceCategories
|
||||||
import eu.kanade.domain.source.interactor.ToggleExcludeFromDataSaver
|
import eu.kanade.domain.source.interactor.ToggleExcludeFromDataSaver
|
||||||
|
import eu.kanade.tachiyomi.di.InjektModule
|
||||||
|
import eu.kanade.tachiyomi.di.addFactory
|
||||||
|
import eu.kanade.tachiyomi.di.addSingletonFactory
|
||||||
import eu.kanade.tachiyomi.source.online.MetadataSource
|
import eu.kanade.tachiyomi.source.online.MetadataSource
|
||||||
import exh.search.SearchEngine
|
import exh.search.SearchEngine
|
||||||
import tachiyomi.data.manga.CustomMangaRepositoryImpl
|
import tachiyomi.data.manga.CustomMangaRepositoryImpl
|
||||||
@@ -42,7 +45,7 @@ import tachiyomi.domain.manga.interactor.GetMergedManga
|
|||||||
import tachiyomi.domain.manga.interactor.GetMergedMangaById
|
import tachiyomi.domain.manga.interactor.GetMergedMangaById
|
||||||
import tachiyomi.domain.manga.interactor.GetMergedMangaForDownloading
|
import tachiyomi.domain.manga.interactor.GetMergedMangaForDownloading
|
||||||
import tachiyomi.domain.manga.interactor.GetMergedReferencesById
|
import tachiyomi.domain.manga.interactor.GetMergedReferencesById
|
||||||
import tachiyomi.domain.manga.interactor.GetReadMangaNotInLibrary
|
import tachiyomi.domain.manga.interactor.GetReadMangaNotInLibraryView
|
||||||
import tachiyomi.domain.manga.interactor.GetSearchMetadata
|
import tachiyomi.domain.manga.interactor.GetSearchMetadata
|
||||||
import tachiyomi.domain.manga.interactor.GetSearchTags
|
import tachiyomi.domain.manga.interactor.GetSearchTags
|
||||||
import tachiyomi.domain.manga.interactor.GetSearchTitles
|
import tachiyomi.domain.manga.interactor.GetSearchTitles
|
||||||
@@ -71,11 +74,7 @@ import tachiyomi.domain.source.interactor.InsertSavedSearch
|
|||||||
import tachiyomi.domain.source.repository.FeedSavedSearchRepository
|
import tachiyomi.domain.source.repository.FeedSavedSearchRepository
|
||||||
import tachiyomi.domain.source.repository.SavedSearchRepository
|
import tachiyomi.domain.source.repository.SavedSearchRepository
|
||||||
import tachiyomi.domain.track.interactor.IsTrackUnfollowed
|
import tachiyomi.domain.track.interactor.IsTrackUnfollowed
|
||||||
import uy.kohesive.injekt.api.InjektModule
|
|
||||||
import uy.kohesive.injekt.api.InjektRegistrar
|
import uy.kohesive.injekt.api.InjektRegistrar
|
||||||
import uy.kohesive.injekt.api.addFactory
|
|
||||||
import uy.kohesive.injekt.api.addSingletonFactory
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
import xyz.nulldev.ts.api.http.serializer.FilterSerializer
|
import xyz.nulldev.ts.api.http.serializer.FilterSerializer
|
||||||
|
|
||||||
class SYDomainModule : InjektModule {
|
class SYDomainModule : InjektModule {
|
||||||
@@ -102,7 +101,7 @@ class SYDomainModule : InjektModule {
|
|||||||
addFactory { GetPagePreviews(get(), get()) }
|
addFactory { GetPagePreviews(get(), get()) }
|
||||||
addFactory { SearchEngine() }
|
addFactory { SearchEngine() }
|
||||||
addFactory { IsTrackUnfollowed() }
|
addFactory { IsTrackUnfollowed() }
|
||||||
addFactory { GetReadMangaNotInLibrary(get()) }
|
addFactory { GetReadMangaNotInLibraryView(get()) }
|
||||||
|
|
||||||
// Required for [MetadataSource]
|
// Required for [MetadataSource]
|
||||||
addFactory<MetadataSource.GetMangaId> { GetManga(get()) }
|
addFactory<MetadataSource.GetMangaId> { GetManga(get()) }
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package eu.kanade.domain.base
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import dev.icerock.moko.resources.StringResource
|
import dev.icerock.moko.resources.StringResource
|
||||||
|
import eu.kanade.tachiyomi.util.system.GLUtil
|
||||||
import tachiyomi.core.common.preference.Preference
|
import tachiyomi.core.common.preference.Preference
|
||||||
import tachiyomi.core.common.preference.PreferenceStore
|
import tachiyomi.core.common.preference.PreferenceStore
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
@@ -30,4 +31,6 @@ class BasePreferences(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun displayProfile() = preferenceStore.getString("pref_display_profile_key", "")
|
fun displayProfile() = preferenceStore.getString("pref_display_profile_key", "")
|
||||||
|
|
||||||
|
fun hardwareBitmapThreshold() = preferenceStore.getInt("pref_hardware_bitmap_threshold", GLUtil.SAFE_TEXTURE_LIMIT)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import eu.kanade.domain.track.model.toDomainTrack
|
|||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
import eu.kanade.tachiyomi.data.track.EnhancedTracker
|
import eu.kanade.tachiyomi.data.track.EnhancedTracker
|
||||||
import eu.kanade.tachiyomi.data.track.Tracker
|
import eu.kanade.tachiyomi.data.track.Tracker
|
||||||
|
import eu.kanade.tachiyomi.data.track.TrackerManager
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.util.lang.convertEpochMillisZone
|
import eu.kanade.tachiyomi.util.lang.convertEpochMillisZone
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
@@ -14,17 +15,16 @@ import tachiyomi.core.common.util.system.logcat
|
|||||||
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
|
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
|
||||||
import tachiyomi.domain.history.interactor.GetHistory
|
import tachiyomi.domain.history.interactor.GetHistory
|
||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
import tachiyomi.domain.track.interactor.GetTracks
|
|
||||||
import tachiyomi.domain.track.interactor.InsertTrack
|
import tachiyomi.domain.track.interactor.InsertTrack
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.time.ZoneOffset
|
import java.time.ZoneOffset
|
||||||
|
|
||||||
class AddTracks(
|
class AddTracks(
|
||||||
private val getTracks: GetTracks,
|
|
||||||
private val insertTrack: InsertTrack,
|
private val insertTrack: InsertTrack,
|
||||||
private val syncChapterProgressWithTrack: SyncChapterProgressWithTrack,
|
private val syncChapterProgressWithTrack: SyncChapterProgressWithTrack,
|
||||||
private val getChaptersByMangaId: GetChaptersByMangaId,
|
private val getChaptersByMangaId: GetChaptersByMangaId,
|
||||||
|
private val trackerManager: TrackerManager,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
// TODO: update all trackers based on common data
|
// TODO: update all trackers based on common data
|
||||||
@@ -79,7 +79,7 @@ class AddTracks(
|
|||||||
|
|
||||||
suspend fun bindEnhancedTrackers(manga: Manga, source: Source) = withNonCancellableContext {
|
suspend fun bindEnhancedTrackers(manga: Manga, source: Source) = withNonCancellableContext {
|
||||||
withIOContext {
|
withIOContext {
|
||||||
getTracks.await(manga.id)
|
trackerManager.loggedInTrackers()
|
||||||
.filterIsInstance<EnhancedTracker>()
|
.filterIsInstance<EnhancedTracker>()
|
||||||
.filter { it.accept(source) }
|
.filter { it.accept(source) }
|
||||||
.forEach { service ->
|
.forEach { service ->
|
||||||
@@ -87,11 +87,11 @@ class AddTracks(
|
|||||||
service.match(manga)?.let { track ->
|
service.match(manga)?.let { track ->
|
||||||
track.manga_id = manga.id
|
track.manga_id = manga.id
|
||||||
(service as Tracker).bind(track)
|
(service as Tracker).bind(track)
|
||||||
insertTrack.await(track.toDomainTrack()!!)
|
insertTrack.await(track.toDomainTrack(idRequired = false)!!)
|
||||||
|
|
||||||
syncChapterProgressWithTrack.await(
|
syncChapterProgressWithTrack.await(
|
||||||
manga.id,
|
manga.id,
|
||||||
track.toDomainTrack()!!,
|
track.toDomainTrack(idRequired = false)!!,
|
||||||
service,
|
service,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import tachiyomi.domain.chapter.interactor.UpdateChapter
|
|||||||
import tachiyomi.domain.chapter.model.toChapterUpdate
|
import tachiyomi.domain.chapter.model.toChapterUpdate
|
||||||
import tachiyomi.domain.track.interactor.InsertTrack
|
import tachiyomi.domain.track.interactor.InsertTrack
|
||||||
import tachiyomi.domain.track.model.Track
|
import tachiyomi.domain.track.model.Track
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
class SyncChapterProgressWithTrack(
|
class SyncChapterProgressWithTrack(
|
||||||
private val updateChapter: UpdateChapter,
|
private val updateChapter: UpdateChapter,
|
||||||
@@ -36,7 +37,8 @@ class SyncChapterProgressWithTrack(
|
|||||||
|
|
||||||
// only take into account continuous reading
|
// only take into account continuous reading
|
||||||
val localLastRead = sortedChapters.takeWhile { it.read }.lastOrNull()?.chapterNumber ?: 0F
|
val localLastRead = sortedChapters.takeWhile { it.read }.lastOrNull()?.chapterNumber ?: 0F
|
||||||
val updatedTrack = remoteTrack.copy(lastChapterRead = localLastRead.toDouble())
|
val lastRead = max(remoteTrack.lastChapterRead, localLastRead.toDouble())
|
||||||
|
val updatedTrack = remoteTrack.copy(lastChapterRead = lastRead)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
tracker.update(updatedTrack.toDbTrack())
|
tracker.update(updatedTrack.toDbTrack())
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package eu.kanade.domain.track.model
|
||||||
|
|
||||||
|
import dev.icerock.moko.resources.StringResource
|
||||||
|
import tachiyomi.i18n.MR
|
||||||
|
|
||||||
|
enum class AutoTrackState(val titleRes: StringResource) {
|
||||||
|
ALWAYS(MR.strings.lock_always),
|
||||||
|
ASK(MR.strings.default_category_summary),
|
||||||
|
NEVER(MR.strings.lock_never),
|
||||||
|
}
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
package eu.kanade.domain.track.service
|
package eu.kanade.domain.track.service
|
||||||
|
|
||||||
|
import eu.kanade.domain.track.model.AutoTrackState
|
||||||
import eu.kanade.tachiyomi.data.track.Tracker
|
import eu.kanade.tachiyomi.data.track.Tracker
|
||||||
import eu.kanade.tachiyomi.data.track.anilist.Anilist
|
import eu.kanade.tachiyomi.data.track.anilist.Anilist
|
||||||
import tachiyomi.core.common.preference.Preference
|
import tachiyomi.core.common.preference.Preference
|
||||||
import tachiyomi.core.common.preference.PreferenceStore
|
import tachiyomi.core.common.preference.PreferenceStore
|
||||||
|
import tachiyomi.core.common.preference.getEnum
|
||||||
|
|
||||||
class TrackPreferences(
|
class TrackPreferences(
|
||||||
private val preferenceStore: PreferenceStore,
|
private val preferenceStore: PreferenceStore,
|
||||||
@@ -35,4 +37,9 @@ class TrackPreferences(
|
|||||||
fun anilistScoreType() = preferenceStore.getString("anilist_score_type", Anilist.POINT_10)
|
fun anilistScoreType() = preferenceStore.getString("anilist_score_type", Anilist.POINT_10)
|
||||||
|
|
||||||
fun autoUpdateTrack() = preferenceStore.getBoolean("pref_auto_update_manga_sync_key", true)
|
fun autoUpdateTrack() = preferenceStore.getBoolean("pref_auto_update_manga_sync_key", true)
|
||||||
|
|
||||||
|
fun autoUpdateTrackOnMarkRead() = preferenceStore.getEnum(
|
||||||
|
"pref_auto_update_manga_on_mark_read",
|
||||||
|
AutoTrackState.ALWAYS,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -198,7 +198,7 @@ private fun ExtensionDetails(
|
|||||||
key = { it.source.id },
|
key = { it.source.id },
|
||||||
) { source ->
|
) { source ->
|
||||||
SourceSwitchPreference(
|
SourceSwitchPreference(
|
||||||
modifier = Modifier.animateItemPlacement(),
|
modifier = Modifier.animateItem(),
|
||||||
source = source,
|
source = source,
|
||||||
onClickSourcePreferences = onClickSourcePreferences,
|
onClickSourcePreferences = onClickSourcePreferences,
|
||||||
onClickSource = onClickSource,
|
onClickSource = onClickSource,
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ private fun ExtensionFilterContent(
|
|||||||
) {
|
) {
|
||||||
items(state.languages) { language ->
|
items(state.languages) { language ->
|
||||||
SwitchPreferenceWidget(
|
SwitchPreferenceWidget(
|
||||||
modifier = Modifier.animateItemPlacement(),
|
modifier = Modifier.animateItem(),
|
||||||
title = LocaleHelper.getSourceDisplayName(language, context),
|
title = LocaleHelper.getSourceDisplayName(language, context),
|
||||||
checked = language in state.enabledLanguages,
|
checked = language in state.enabledLanguages,
|
||||||
onCheckedChanged = { onClickLang(language) },
|
onCheckedChanged = { onClickLang(language) },
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ import eu.kanade.presentation.browse.components.ExtensionIcon
|
|||||||
import eu.kanade.presentation.components.WarningBanner
|
import eu.kanade.presentation.components.WarningBanner
|
||||||
import eu.kanade.presentation.manga.components.DotSeparatorNoSpaceText
|
import eu.kanade.presentation.manga.components.DotSeparatorNoSpaceText
|
||||||
import eu.kanade.presentation.more.settings.screen.browse.ExtensionReposScreen
|
import eu.kanade.presentation.more.settings.screen.browse.ExtensionReposScreen
|
||||||
|
import eu.kanade.presentation.util.animateItemFastScroll
|
||||||
import eu.kanade.presentation.util.rememberRequestPackageInstallsPermissionState
|
import eu.kanade.presentation.util.rememberRequestPackageInstallsPermissionState
|
||||||
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
|
||||||
@@ -91,7 +92,7 @@ fun ExtensionScreen(
|
|||||||
PullRefresh(
|
PullRefresh(
|
||||||
refreshing = state.isRefreshing,
|
refreshing = state.isRefreshing,
|
||||||
onRefresh = onRefresh,
|
onRefresh = onRefresh,
|
||||||
enabled = { !state.isLoading },
|
enabled = !state.isLoading,
|
||||||
) {
|
) {
|
||||||
when {
|
when {
|
||||||
state.isLoading -> LoadingScreen(Modifier.padding(contentPadding))
|
state.isLoading -> LoadingScreen(Modifier.padding(contentPadding))
|
||||||
@@ -188,14 +189,14 @@ private fun ExtensionContent(
|
|||||||
}
|
}
|
||||||
ExtensionHeader(
|
ExtensionHeader(
|
||||||
textRes = header.textRes,
|
textRes = header.textRes,
|
||||||
modifier = Modifier.animateItemPlacement(),
|
modifier = Modifier.animateItemFastScroll(),
|
||||||
action = action,
|
action = action,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is ExtensionUiModel.Header.Text -> {
|
is ExtensionUiModel.Header.Text -> {
|
||||||
ExtensionHeader(
|
ExtensionHeader(
|
||||||
text = header.text,
|
text = header.text,
|
||||||
modifier = Modifier.animateItemPlacement(),
|
modifier = Modifier.animateItemFastScroll(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -213,7 +214,7 @@ private fun ExtensionContent(
|
|||||||
},
|
},
|
||||||
) { item ->
|
) { item ->
|
||||||
ExtensionItem(
|
ExtensionItem(
|
||||||
modifier = Modifier.animateItemPlacement(),
|
modifier = Modifier.animateItemFastScroll(),
|
||||||
item = item,
|
item = item,
|
||||||
onClickItem = {
|
onClickItem = {
|
||||||
when (it) {
|
when (it) {
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ fun FeedScreen(
|
|||||||
refreshing = true
|
refreshing = true
|
||||||
onRefresh()
|
onRefresh()
|
||||||
},
|
},
|
||||||
enabled = { !state.isLoadingItems },
|
enabled = !state.isLoadingItems,
|
||||||
) {
|
) {
|
||||||
ScrollbarLazyColumn(
|
ScrollbarLazyColumn(
|
||||||
contentPadding = contentPadding + topSmallPaddingValues,
|
contentPadding = contentPadding + topSmallPaddingValues,
|
||||||
@@ -103,7 +103,6 @@ fun FeedScreen(
|
|||||||
key = { it.feed.id },
|
key = { it.feed.id },
|
||||||
) { item ->
|
) { item ->
|
||||||
GlobalSearchResultItem(
|
GlobalSearchResultItem(
|
||||||
modifier = Modifier.animateItemPlacement(),
|
|
||||||
title = item.title,
|
title = item.title,
|
||||||
subtitle = item.subtitle,
|
subtitle = item.subtitle,
|
||||||
onLongClick = {
|
onLongClick = {
|
||||||
@@ -116,6 +115,7 @@ fun FeedScreen(
|
|||||||
onClickSource(item.source)
|
onClickSource(item.source)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
modifier = Modifier.animateItem(),
|
||||||
) {
|
) {
|
||||||
FeedItem(
|
FeedItem(
|
||||||
item = item,
|
item = item,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import androidx.compose.foundation.layout.PaddingValues
|
|||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.State
|
import androidx.compose.runtime.State
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
import eu.kanade.presentation.browse.components.GlobalSearchCardRow
|
import eu.kanade.presentation.browse.components.GlobalSearchCardRow
|
||||||
import eu.kanade.presentation.browse.components.GlobalSearchErrorResultItem
|
import eu.kanade.presentation.browse.components.GlobalSearchErrorResultItem
|
||||||
import eu.kanade.presentation.browse.components.GlobalSearchLoadingResultItem
|
import eu.kanade.presentation.browse.components.GlobalSearchLoadingResultItem
|
||||||
@@ -80,6 +81,7 @@ internal fun GlobalSearchContent(
|
|||||||
} ?: source.name,
|
} ?: source.name,
|
||||||
subtitle = LocaleHelper.getLocalizedDisplayName(source.lang),
|
subtitle = LocaleHelper.getLocalizedDisplayName(source.lang),
|
||||||
onClick = { onClickSource(source) },
|
onClick = { onClickSource(source) },
|
||||||
|
modifier = Modifier.animateItem(),
|
||||||
) {
|
) {
|
||||||
when (result) {
|
when (result) {
|
||||||
SearchItemResult.Loading -> {
|
SearchItemResult.Loading -> {
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ private fun MigrateSourceList(
|
|||||||
key = { (source, _) -> "migrate-${source.id}" },
|
key = { (source, _) -> "migrate-${source.id}" },
|
||||||
) { (source, count) ->
|
) { (source, count) ->
|
||||||
MigrateSourceItem(
|
MigrateSourceItem(
|
||||||
modifier = Modifier.animateItemPlacement(),
|
modifier = Modifier.animateItem(),
|
||||||
source = source,
|
source = source,
|
||||||
count = count,
|
count = count,
|
||||||
onClickItem = { onClickItem(source) },
|
onClickItem = { onClickItem(source) },
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import eu.kanade.presentation.browse.components.MigrationItem
|
|||||||
import eu.kanade.presentation.browse.components.MigrationItemResult
|
import eu.kanade.presentation.browse.components.MigrationItemResult
|
||||||
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.util.animateItemFastScroll
|
||||||
import eu.kanade.tachiyomi.ui.browse.migration.advanced.process.MigratingManga
|
import eu.kanade.tachiyomi.ui.browse.migration.advanced.process.MigratingManga
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
@@ -95,7 +96,7 @@ fun MigrationListScreen(
|
|||||||
Row(
|
Row(
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.animateItemPlacement()
|
.animateItemFastScroll()
|
||||||
.padding(horizontal = 16.dp)
|
.padding(horizontal = 16.dp)
|
||||||
.height(IntrinsicSize.Min),
|
.height(IntrinsicSize.Min),
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import eu.kanade.presentation.browse.components.GlobalSearchLoadingResultItem
|
|||||||
import eu.kanade.presentation.browse.components.GlobalSearchResultItem
|
import eu.kanade.presentation.browse.components.GlobalSearchResultItem
|
||||||
import eu.kanade.presentation.components.AppBarTitle
|
import eu.kanade.presentation.components.AppBarTitle
|
||||||
import eu.kanade.presentation.components.SearchToolbar
|
import eu.kanade.presentation.components.SearchToolbar
|
||||||
|
import eu.kanade.presentation.util.animateItemFastScroll
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
import tachiyomi.domain.source.model.FeedSavedSearch
|
import tachiyomi.domain.source.model.FeedSavedSearch
|
||||||
@@ -153,7 +154,7 @@ fun SourceFeedList(
|
|||||||
key = { it.id },
|
key = { it.id },
|
||||||
) { item ->
|
) { item ->
|
||||||
GlobalSearchResultItem(
|
GlobalSearchResultItem(
|
||||||
modifier = Modifier.animateItemPlacement(),
|
modifier = Modifier.animateItemFastScroll(),
|
||||||
title = item.title,
|
title = item.title,
|
||||||
subtitle = null,
|
subtitle = null,
|
||||||
onLongClick = if (item is SourceFeedUI.SourceSavedSearch) {
|
onLongClick = if (item is SourceFeedUI.SourceSavedSearch) {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import androidx.compose.ui.platform.LocalContext
|
|||||||
import eu.kanade.presentation.browse.components.BaseSourceItem
|
import eu.kanade.presentation.browse.components.BaseSourceItem
|
||||||
import eu.kanade.presentation.components.AppBar
|
import eu.kanade.presentation.components.AppBar
|
||||||
import eu.kanade.presentation.more.settings.widget.SwitchPreferenceWidget
|
import eu.kanade.presentation.more.settings.widget.SwitchPreferenceWidget
|
||||||
|
import eu.kanade.presentation.util.animateItemFastScroll
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.SourcesFilterScreenModel
|
import eu.kanade.tachiyomi.ui.browse.source.SourcesFilterScreenModel
|
||||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||||
import tachiyomi.domain.source.model.Source
|
import tachiyomi.domain.source.model.Source
|
||||||
@@ -79,7 +80,7 @@ private fun SourcesFilterContent(
|
|||||||
contentType = "source-filter-header",
|
contentType = "source-filter-header",
|
||||||
) {
|
) {
|
||||||
SourcesFilterHeader(
|
SourcesFilterHeader(
|
||||||
modifier = Modifier.animateItemPlacement(),
|
modifier = Modifier.animateItemFastScroll(),
|
||||||
language = language,
|
language = language,
|
||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
onClickItem = onClickLanguage,
|
onClickItem = onClickLanguage,
|
||||||
@@ -95,7 +96,7 @@ private fun SourcesFilterContent(
|
|||||||
sources.none { it.id.toString() in state.disabledSources }
|
sources.none { it.id.toString() in state.disabledSources }
|
||||||
}
|
}
|
||||||
SourcesFilterToggle(
|
SourcesFilterToggle(
|
||||||
modifier = Modifier.animateItemPlacement(),
|
modifier = Modifier.animateItem(),
|
||||||
isEnabled = toggleEnabled,
|
isEnabled = toggleEnabled,
|
||||||
onClickItem = {
|
onClickItem = {
|
||||||
onClickSources(!toggleEnabled, sources)
|
onClickSources(!toggleEnabled, sources)
|
||||||
@@ -109,7 +110,7 @@ private fun SourcesFilterContent(
|
|||||||
contentType = { "source-filter-item" },
|
contentType = { "source-filter-item" },
|
||||||
) { source ->
|
) { source ->
|
||||||
SourcesFilterItem(
|
SourcesFilterItem(
|
||||||
modifier = Modifier.animateItemPlacement(),
|
modifier = Modifier.animateItemFastScroll(),
|
||||||
source = source,
|
source = source,
|
||||||
enabled = "${source.id}" !in state.disabledSources,
|
enabled = "${source.id}" !in state.disabledSources,
|
||||||
onClickItem = onClickSource,
|
onClickItem = onClickSource,
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ fun SourcesScreen(
|
|||||||
when (model) {
|
when (model) {
|
||||||
is SourceUiModel.Header -> {
|
is SourceUiModel.Header -> {
|
||||||
SourceHeader(
|
SourceHeader(
|
||||||
modifier = Modifier.animateItemPlacement(),
|
modifier = Modifier.animateItem(),
|
||||||
language = model.language,
|
language = model.language,
|
||||||
// SY -->
|
// SY -->
|
||||||
isCategory = model.isCategory,
|
isCategory = model.isCategory,
|
||||||
@@ -89,7 +89,7 @@ fun SourcesScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
is SourceUiModel.Item -> SourceItem(
|
is SourceUiModel.Item -> SourceItem(
|
||||||
modifier = Modifier.animateItemPlacement(),
|
modifier = Modifier.animateItem(),
|
||||||
source = model.source,
|
source = model.source,
|
||||||
// SY -->
|
// SY -->
|
||||||
showLatest = state.showLatest,
|
showLatest = state.showLatest,
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ private fun Extension.getIcon(density: Int = DisplayMetrics.DENSITY_DEFAULT): St
|
|||||||
return produceState<Result<ImageBitmap>>(initialValue = Result.Loading, this) {
|
return produceState<Result<ImageBitmap>>(initialValue = Result.Loading, this) {
|
||||||
withIOContext {
|
withIOContext {
|
||||||
value = try {
|
value = try {
|
||||||
val appInfo = ExtensionLoader.getExtensionPackageInfoFromPkgName(context, pkgName)!!.applicationInfo
|
val appInfo = ExtensionLoader.getExtensionPackageInfoFromPkgName(context, pkgName)!!.applicationInfo!!
|
||||||
val appResources = context.packageManager.getResourcesForApplication(appInfo)
|
val appResources = context.packageManager.getResourcesForApplication(appInfo)
|
||||||
Result.Success(
|
Result.Success(
|
||||||
appResources.getDrawableForDensity(appInfo.icon, density, null)!!
|
appResources.getDrawableForDensity(appInfo.icon, density, null)!!
|
||||||
|
|||||||
+2
-4
@@ -30,9 +30,6 @@ import tachiyomi.presentation.core.i18n.stringResource
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun GlobalSearchResultItem(
|
fun GlobalSearchResultItem(
|
||||||
// SY -->
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
// SY <--
|
|
||||||
title: String,
|
title: String,
|
||||||
// SY -->
|
// SY -->
|
||||||
subtitle: String?,
|
subtitle: String?,
|
||||||
@@ -41,9 +38,10 @@ fun GlobalSearchResultItem(
|
|||||||
// SY -->
|
// SY -->
|
||||||
onLongClick: (() -> Unit)? = null,
|
onLongClick: (() -> Unit)? = null,
|
||||||
// SY <--
|
// SY <--
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
content: @Composable () -> Unit,
|
content: @Composable () -> Unit,
|
||||||
) {
|
) {
|
||||||
Column(modifier) {
|
Column(modifier = modifier) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(
|
.padding(
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ private fun CategoryContent(
|
|||||||
key = { _, category -> "category-${category.id}" },
|
key = { _, category -> "category-${category.id}" },
|
||||||
) { index, category ->
|
) { index, category ->
|
||||||
CategoryListItem(
|
CategoryListItem(
|
||||||
modifier = Modifier.animateItemPlacement(),
|
modifier = Modifier.animateItem(),
|
||||||
category = category,
|
category = category,
|
||||||
canMoveUp = index != 0,
|
canMoveUp = index != 0,
|
||||||
canMoveDown = index != categories.lastIndex,
|
canMoveDown = index != categories.lastIndex,
|
||||||
|
|||||||
+2
-3
@@ -10,8 +10,7 @@ import androidx.compose.ui.Modifier
|
|||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.components.material.ExtendedFloatingActionButton
|
import tachiyomi.presentation.core.components.material.ExtendedFloatingActionButton
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
import tachiyomi.presentation.core.util.isScrolledToEnd
|
import tachiyomi.presentation.core.util.shouldExpandFAB
|
||||||
import tachiyomi.presentation.core.util.isScrollingUp
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CategoryFloatingActionButton(
|
fun CategoryFloatingActionButton(
|
||||||
@@ -23,7 +22,7 @@ fun CategoryFloatingActionButton(
|
|||||||
text = { Text(text = stringResource(MR.strings.action_add)) },
|
text = { Text(text = stringResource(MR.strings.action_add)) },
|
||||||
icon = { Icon(imageVector = Icons.Outlined.Add, contentDescription = null) },
|
icon = { Icon(imageVector = Icons.Outlined.Add, contentDescription = null) },
|
||||||
onClick = onCreate,
|
onClick = onCreate,
|
||||||
expanded = lazyListState.isScrollingUp() || lazyListState.isScrolledToEnd(),
|
expanded = lazyListState.shouldExpandFAB(),
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -26,7 +26,7 @@ fun BiometricTimesContent(
|
|||||||
) {
|
) {
|
||||||
items(timeRanges, key = { it.formattedString }) { timeRange ->
|
items(timeRanges, key = { it.formattedString }) { timeRange ->
|
||||||
BiometricTimesListItem(
|
BiometricTimesListItem(
|
||||||
modifier = Modifier.animateItemPlacement(),
|
modifier = Modifier.animateItem(),
|
||||||
timeRange = timeRange,
|
timeRange = timeRange,
|
||||||
onDelete = { onClickDelete(timeRange) },
|
onDelete = { onClickDelete(timeRange) },
|
||||||
)
|
)
|
||||||
|
|||||||
+1
-1
@@ -27,7 +27,7 @@ fun SortTagContent(
|
|||||||
) {
|
) {
|
||||||
itemsIndexed(tags, key = { _, tag -> tag }) { index, tag ->
|
itemsIndexed(tags, key = { _, tag -> tag }) { index, tag ->
|
||||||
SortTagListItem(
|
SortTagListItem(
|
||||||
modifier = Modifier.animateItemPlacement(),
|
modifier = Modifier.animateItem(),
|
||||||
tag = tag,
|
tag = tag,
|
||||||
canMoveUp = index != 0,
|
canMoveUp = index != 0,
|
||||||
canMoveDown = index != tags.lastIndex,
|
canMoveDown = index != tags.lastIndex,
|
||||||
|
|||||||
+1
-1
@@ -26,7 +26,7 @@ fun SourceCategoryContent(
|
|||||||
) {
|
) {
|
||||||
items(categories, key = { it }) { category ->
|
items(categories, key = { it }) { category ->
|
||||||
SourceCategoryListItem(
|
SourceCategoryListItem(
|
||||||
modifier = Modifier.animateItemPlacement(),
|
modifier = Modifier.animateItem(),
|
||||||
category = category,
|
category = category,
|
||||||
onRename = { onClickRename(category) },
|
onRename = { onClickRename(category) },
|
||||||
onDelete = { onClickDelete(category) },
|
onDelete = { onClickDelete(category) },
|
||||||
|
|||||||
@@ -179,7 +179,7 @@ fun AppBarTitle(
|
|||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
modifier = Modifier.basicMarquee(
|
modifier = Modifier.basicMarquee(
|
||||||
delayMillis = 2_000,
|
repeatDelayMillis = 2_000,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.calculateStartPadding
|
|||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.pager.HorizontalPager
|
import androidx.compose.foundation.pager.HorizontalPager
|
||||||
|
import androidx.compose.foundation.pager.PagerState
|
||||||
import androidx.compose.foundation.pager.rememberPagerState
|
import androidx.compose.foundation.pager.rememberPagerState
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.PrimaryTabRow
|
import androidx.compose.material3.PrimaryTabRow
|
||||||
@@ -14,7 +15,6 @@ import androidx.compose.material3.SnackbarHost
|
|||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.material3.Tab
|
import androidx.compose.material3.Tab
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
@@ -33,20 +33,13 @@ import tachiyomi.presentation.core.i18n.stringResource
|
|||||||
fun TabbedScreen(
|
fun TabbedScreen(
|
||||||
titleRes: StringResource,
|
titleRes: StringResource,
|
||||||
tabs: ImmutableList<TabContent>,
|
tabs: ImmutableList<TabContent>,
|
||||||
startIndex: Int? = null,
|
state: PagerState = rememberPagerState { tabs.size },
|
||||||
searchQuery: String? = null,
|
searchQuery: String? = null,
|
||||||
onChangeSearchQuery: (String?) -> Unit = {},
|
onChangeSearchQuery: (String?) -> Unit = {},
|
||||||
) {
|
) {
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val state = rememberPagerState { tabs.size }
|
|
||||||
val snackbarHostState = remember { SnackbarHostState() }
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
|
|
||||||
LaunchedEffect(startIndex) {
|
|
||||||
if (startIndex != null) {
|
|
||||||
state.scrollToPage(startIndex)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
val tab = tabs[state.currentPage]
|
val tab = tabs[state.currentPage]
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import eu.kanade.presentation.components.SearchToolbar
|
|||||||
import eu.kanade.presentation.components.relativeDateText
|
import eu.kanade.presentation.components.relativeDateText
|
||||||
import eu.kanade.presentation.history.components.HistoryItem
|
import eu.kanade.presentation.history.components.HistoryItem
|
||||||
import eu.kanade.presentation.theme.TachiyomiPreviewTheme
|
import eu.kanade.presentation.theme.TachiyomiPreviewTheme
|
||||||
|
import eu.kanade.presentation.util.animateItemFastScroll
|
||||||
import eu.kanade.tachiyomi.ui.history.HistoryScreenModel
|
import eu.kanade.tachiyomi.ui.history.HistoryScreenModel
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
@@ -114,14 +115,14 @@ private fun HistoryScreenContent(
|
|||||||
when (item) {
|
when (item) {
|
||||||
is HistoryUiModel.Header -> {
|
is HistoryUiModel.Header -> {
|
||||||
ListGroupHeader(
|
ListGroupHeader(
|
||||||
modifier = Modifier.animateItemPlacement(),
|
modifier = Modifier.animateItemFastScroll(),
|
||||||
text = relativeDateText(item.date),
|
text = relativeDateText(item.date),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is HistoryUiModel.Item -> {
|
is HistoryUiModel.Item -> {
|
||||||
val value = item.item
|
val value = item.item
|
||||||
HistoryItem(
|
HistoryItem(
|
||||||
modifier = Modifier.animateItemPlacement(),
|
modifier = Modifier.animateItemFastScroll(),
|
||||||
history = value,
|
history = value,
|
||||||
onClickCover = { onClickCover(value) },
|
onClickCover = { onClickCover(value) },
|
||||||
onClickResume = { onClickResume(value) },
|
onClickResume = { onClickResume(value) },
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import androidx.compose.foundation.layout.ColumnScope
|
|||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Refresh
|
||||||
import androidx.compose.material3.FilterChip
|
import androidx.compose.material3.FilterChip
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -35,6 +37,7 @@ import tachiyomi.domain.library.model.sort
|
|||||||
import tachiyomi.domain.library.service.LibraryPreferences
|
import tachiyomi.domain.library.service.LibraryPreferences
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.i18n.sy.SYMR
|
import tachiyomi.i18n.sy.SYMR
|
||||||
|
import tachiyomi.presentation.core.components.BaseSortItem
|
||||||
import tachiyomi.presentation.core.components.CheckboxItem
|
import tachiyomi.presentation.core.components.CheckboxItem
|
||||||
import tachiyomi.presentation.core.components.HeadingItem
|
import tachiyomi.presentation.core.components.HeadingItem
|
||||||
import tachiyomi.presentation.core.components.IconItem
|
import tachiyomi.presentation.core.components.IconItem
|
||||||
@@ -197,39 +200,58 @@ private fun ColumnScope.SortPage(
|
|||||||
globalSortMode.type
|
globalSortMode.type
|
||||||
}
|
}
|
||||||
val sortDescending = if (screenModel.grouping == LibraryGroup.BY_DEFAULT) {
|
val sortDescending = if (screenModel.grouping == LibraryGroup.BY_DEFAULT) {
|
||||||
category.sort.isAscending
|
!category.sort.isAscending
|
||||||
} else {
|
} else {
|
||||||
globalSortMode.isAscending
|
!globalSortMode.isAscending
|
||||||
}.not()
|
}
|
||||||
val hasSortTags by remember {
|
val hasSortTags by remember {
|
||||||
screenModel.libraryPreferences.sortTagsForLibrary().changes()
|
screenModel.libraryPreferences.sortTagsForLibrary().changes()
|
||||||
.map { it.isNotEmpty() }
|
.map { it.isNotEmpty() }
|
||||||
}.collectAsState(initial = screenModel.libraryPreferences.sortTagsForLibrary().get().isNotEmpty())
|
}.collectAsState(initial = screenModel.libraryPreferences.sortTagsForLibrary().get().isNotEmpty())
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
val trackerSortOption = if (trackers.isEmpty()) {
|
val options = remember(trackers.isEmpty()/* SY --> */, hasSortTags/* SY <-- */) {
|
||||||
emptyList()
|
val trackerMeanPair = if (trackers.isNotEmpty()) {
|
||||||
} else {
|
MR.strings.action_sort_tracker_score to LibrarySort.Type.TrackerMean
|
||||||
listOf(MR.strings.action_sort_tracker_score to LibrarySort.Type.TrackerMean)
|
} else {
|
||||||
}
|
null
|
||||||
|
}
|
||||||
listOfNotNull(
|
|
||||||
MR.strings.action_sort_alpha to LibrarySort.Type.Alphabetical,
|
|
||||||
MR.strings.action_sort_total to LibrarySort.Type.TotalChapters,
|
|
||||||
MR.strings.action_sort_last_read to LibrarySort.Type.LastRead,
|
|
||||||
MR.strings.action_sort_last_manga_update to LibrarySort.Type.LastUpdate,
|
|
||||||
MR.strings.action_sort_unread_count to LibrarySort.Type.UnreadCount,
|
|
||||||
MR.strings.action_sort_latest_chapter to LibrarySort.Type.LatestChapter,
|
|
||||||
MR.strings.action_sort_chapter_fetch_date to LibrarySort.Type.ChapterFetchDate,
|
|
||||||
MR.strings.action_sort_date_added to LibrarySort.Type.DateAdded,
|
|
||||||
// SY -->
|
// SY -->
|
||||||
if (hasSortTags) {
|
val tagSortPair = if (hasSortTags) {
|
||||||
SYMR.strings.tag_sorting to LibrarySort.Type.TagList
|
SYMR.strings.tag_sorting to LibrarySort.Type.TagList
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
},
|
}
|
||||||
// SY <--
|
// SY <--
|
||||||
).plus(trackerSortOption).map { (titleRes, mode) ->
|
listOfNotNull(
|
||||||
|
MR.strings.action_sort_alpha to LibrarySort.Type.Alphabetical,
|
||||||
|
MR.strings.action_sort_total to LibrarySort.Type.TotalChapters,
|
||||||
|
MR.strings.action_sort_last_read to LibrarySort.Type.LastRead,
|
||||||
|
MR.strings.action_sort_last_manga_update to LibrarySort.Type.LastUpdate,
|
||||||
|
MR.strings.action_sort_unread_count to LibrarySort.Type.UnreadCount,
|
||||||
|
MR.strings.action_sort_latest_chapter to LibrarySort.Type.LatestChapter,
|
||||||
|
MR.strings.action_sort_chapter_fetch_date to LibrarySort.Type.ChapterFetchDate,
|
||||||
|
MR.strings.action_sort_date_added to LibrarySort.Type.DateAdded,
|
||||||
|
trackerMeanPair,
|
||||||
|
// SY -->
|
||||||
|
tagSortPair,
|
||||||
|
// SY <--
|
||||||
|
MR.strings.action_sort_random to LibrarySort.Type.Random,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
options.map { (titleRes, mode) ->
|
||||||
|
if (mode == LibrarySort.Type.Random) {
|
||||||
|
BaseSortItem(
|
||||||
|
label = stringResource(titleRes),
|
||||||
|
icon = Icons.Default.Refresh
|
||||||
|
.takeIf { sortingMode == LibrarySort.Type.Random },
|
||||||
|
onClick = {
|
||||||
|
screenModel.setSort(category, mode, LibrarySort.Direction.Ascending)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return@map
|
||||||
|
}
|
||||||
SortItem(
|
SortItem(
|
||||||
label = stringResource(titleRes),
|
label = stringResource(titleRes),
|
||||||
sortDescending = sortDescending.takeIf { sortingMode == mode },
|
sortDescending = sortDescending.takeIf { sortingMode == mode },
|
||||||
@@ -241,7 +263,11 @@ private fun ColumnScope.SortPage(
|
|||||||
} else {
|
} else {
|
||||||
LibrarySort.Direction.Descending
|
LibrarySort.Direction.Descending
|
||||||
}
|
}
|
||||||
else -> if (sortDescending) LibrarySort.Direction.Descending else LibrarySort.Direction.Ascending
|
else -> if (sortDescending) {
|
||||||
|
LibrarySort.Direction.Descending
|
||||||
|
} else {
|
||||||
|
LibrarySort.Direction.Ascending
|
||||||
|
}
|
||||||
}
|
}
|
||||||
screenModel.setSort(category, mode, direction)
|
screenModel.setSort(category, mode, direction)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ fun LibraryContent(
|
|||||||
isRefreshing = false
|
isRefreshing = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
enabled = { notSelectionMode },
|
enabled = notSelectionMode,
|
||||||
) {
|
) {
|
||||||
LibraryPager(
|
LibraryPager(
|
||||||
state = pagerState,
|
state = pagerState,
|
||||||
|
|||||||
@@ -21,9 +21,7 @@ internal fun LibraryTabs(
|
|||||||
getNumberOfMangaForCategory: (Category) -> Int?,
|
getNumberOfMangaForCategory: (Category) -> Int?,
|
||||||
onTabItemClick: (Int) -> Unit,
|
onTabItemClick: (Int) -> Unit,
|
||||||
) {
|
) {
|
||||||
// SY -->
|
|
||||||
val currentPageIndex = pagerState.currentPage.coerceAtMost(categories.lastIndex)
|
val currentPageIndex = pagerState.currentPage.coerceAtMost(categories.lastIndex)
|
||||||
// SY <--
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.zIndex(1f),
|
modifier = Modifier.zIndex(1f),
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ fun LibraryToolbar(
|
|||||||
onClickSyncNow: () -> Unit,
|
onClickSyncNow: () -> Unit,
|
||||||
// SY -->
|
// SY -->
|
||||||
onClickSyncExh: (() -> Unit)?,
|
onClickSyncExh: (() -> Unit)?,
|
||||||
|
isSyncEnabled: Boolean,
|
||||||
// SY <--
|
// SY <--
|
||||||
searchQuery: String?,
|
searchQuery: String?,
|
||||||
onSearchQueryChange: (String?) -> Unit,
|
onSearchQueryChange: (String?) -> Unit,
|
||||||
@@ -64,6 +65,7 @@ fun LibraryToolbar(
|
|||||||
onClickSyncNow = onClickSyncNow,
|
onClickSyncNow = onClickSyncNow,
|
||||||
// SY -->
|
// SY -->
|
||||||
onClickSyncExh = onClickSyncExh,
|
onClickSyncExh = onClickSyncExh,
|
||||||
|
isSyncEnabled = isSyncEnabled,
|
||||||
// SY <--
|
// SY <--
|
||||||
scrollBehavior = scrollBehavior,
|
scrollBehavior = scrollBehavior,
|
||||||
)
|
)
|
||||||
@@ -82,6 +84,7 @@ private fun LibraryRegularToolbar(
|
|||||||
onClickSyncNow: () -> Unit,
|
onClickSyncNow: () -> Unit,
|
||||||
// SY -->
|
// SY -->
|
||||||
onClickSyncExh: (() -> Unit)?,
|
onClickSyncExh: (() -> Unit)?,
|
||||||
|
isSyncEnabled: Boolean,
|
||||||
// SY <--
|
// SY <--
|
||||||
scrollBehavior: TopAppBarScrollBehavior?,
|
scrollBehavior: TopAppBarScrollBehavior?,
|
||||||
) {
|
) {
|
||||||
@@ -128,10 +131,6 @@ private fun LibraryRegularToolbar(
|
|||||||
title = stringResource(MR.strings.action_open_random_manga),
|
title = stringResource(MR.strings.action_open_random_manga),
|
||||||
onClick = onClickOpenRandomManga,
|
onClick = onClickOpenRandomManga,
|
||||||
),
|
),
|
||||||
AppBar.OverflowAction(
|
|
||||||
title = stringResource(SYMR.strings.sync_library),
|
|
||||||
onClick = onClickSyncNow,
|
|
||||||
),
|
|
||||||
).builder().apply {
|
).builder().apply {
|
||||||
// SY -->
|
// SY -->
|
||||||
if (onClickSyncExh != null) {
|
if (onClickSyncExh != null) {
|
||||||
@@ -142,6 +141,14 @@ private fun LibraryRegularToolbar(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if (isSyncEnabled) {
|
||||||
|
add(
|
||||||
|
AppBar.OverflowAction(
|
||||||
|
title = stringResource(SYMR.strings.sync_library),
|
||||||
|
onClick = onClickSyncNow,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
// SY <--
|
// SY <--
|
||||||
}.build(),
|
}.build(),
|
||||||
)
|
)
|
||||||
|
|||||||
+119
-25
@@ -15,7 +15,6 @@ import androidx.compose.ui.window.DialogProperties
|
|||||||
import exh.favorites.FavoritesSyncStatus
|
import exh.favorites.FavoritesSyncStatus
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import tachiyomi.core.common.i18n.stringResource
|
import tachiyomi.core.common.i18n.stringResource
|
||||||
import tachiyomi.domain.manga.model.Manga
|
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.i18n.sy.SYMR
|
import tachiyomi.i18n.sy.SYMR
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
@@ -23,7 +22,6 @@ import kotlin.time.Duration.Companion.seconds
|
|||||||
data class SyncFavoritesProgressProperties(
|
data class SyncFavoritesProgressProperties(
|
||||||
val title: String,
|
val title: String,
|
||||||
val text: String,
|
val text: String,
|
||||||
val canDismiss: Boolean,
|
|
||||||
val positiveButtonText: String? = null,
|
val positiveButtonText: String? = null,
|
||||||
val positiveButton: (() -> Unit)? = null,
|
val positiveButton: (() -> Unit)? = null,
|
||||||
val negativeButtonText: String? = null,
|
val negativeButtonText: String? = null,
|
||||||
@@ -34,18 +32,23 @@ data class SyncFavoritesProgressProperties(
|
|||||||
fun SyncFavoritesProgressDialog(
|
fun SyncFavoritesProgressDialog(
|
||||||
status: FavoritesSyncStatus,
|
status: FavoritesSyncStatus,
|
||||||
setStatusIdle: () -> Unit,
|
setStatusIdle: () -> Unit,
|
||||||
openManga: (Manga) -> Unit,
|
openManga: (Long) -> Unit,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val properties by produceState<SyncFavoritesProgressProperties?>(initialValue = null, status) {
|
val properties by produceState<SyncFavoritesProgressProperties?>(initialValue = null, status) {
|
||||||
when (status) {
|
when (status) {
|
||||||
is FavoritesSyncStatus.BadLibraryState.MangaInMultipleCategories -> value = SyncFavoritesProgressProperties(
|
is FavoritesSyncStatus.BadLibraryState.MangaInMultipleCategories -> value = SyncFavoritesProgressProperties(
|
||||||
title = context.stringResource(SYMR.strings.favorites_sync_error),
|
title = context.stringResource(SYMR.strings.favorites_sync_error),
|
||||||
text = context.stringResource(SYMR.strings.favorites_sync_bad_library_state, status.message),
|
text = context.stringResource(
|
||||||
canDismiss = false,
|
SYMR.strings.favorites_sync_bad_library_state,
|
||||||
|
context.stringResource(
|
||||||
|
SYMR.strings.favorites_sync_gallery_in_multiple_categories, status.mangaTitle,
|
||||||
|
status.categories.joinToString(),
|
||||||
|
),
|
||||||
|
),
|
||||||
positiveButtonText = context.stringResource(SYMR.strings.show_gallery),
|
positiveButtonText = context.stringResource(SYMR.strings.show_gallery),
|
||||||
positiveButton = {
|
positiveButton = {
|
||||||
openManga(status.manga)
|
openManga(status.mangaId)
|
||||||
setStatusIdle()
|
setStatusIdle()
|
||||||
},
|
},
|
||||||
negativeButtonText = context.stringResource(MR.strings.action_ok),
|
negativeButtonText = context.stringResource(MR.strings.action_ok),
|
||||||
@@ -53,31 +56,122 @@ fun SyncFavoritesProgressDialog(
|
|||||||
)
|
)
|
||||||
is FavoritesSyncStatus.CompleteWithErrors -> value = SyncFavoritesProgressProperties(
|
is FavoritesSyncStatus.CompleteWithErrors -> value = SyncFavoritesProgressProperties(
|
||||||
title = context.stringResource(SYMR.strings.favorites_sync_done_errors),
|
title = context.stringResource(SYMR.strings.favorites_sync_done_errors),
|
||||||
text = context.stringResource(SYMR.strings.favorites_sync_done_errors_message, status.message),
|
text = context.stringResource(
|
||||||
canDismiss = false,
|
SYMR.strings.favorites_sync_done_errors_message,
|
||||||
positiveButtonText = context.stringResource(MR.strings.action_ok),
|
status.messages.joinToString(separator = "\n") {
|
||||||
positiveButton = setStatusIdle,
|
when (it) {
|
||||||
)
|
is FavoritesSyncStatus.SyncError.GallerySyncError.GalleryAddFail ->
|
||||||
is FavoritesSyncStatus.Error -> value = SyncFavoritesProgressProperties(
|
context.stringResource(SYMR.strings.favorites_sync_failed_to_add_to_local) +
|
||||||
title = context.stringResource(SYMR.strings.favorites_sync_error),
|
context.stringResource(
|
||||||
text = context.stringResource(SYMR.strings.favorites_sync_error_string, status.message),
|
SYMR.strings.favorites_sync_failed_to_add_to_local_error, it.title, it.reason,
|
||||||
canDismiss = false,
|
)
|
||||||
|
is FavoritesSyncStatus.SyncError.GallerySyncError.InvalidGalleryFail ->
|
||||||
|
context.stringResource(SYMR.strings.favorites_sync_failed_to_add_to_local) +
|
||||||
|
context.stringResource(
|
||||||
|
SYMR.strings.favorites_sync_failed_to_add_to_local_unknown_type, it.title, it.url,
|
||||||
|
)
|
||||||
|
is FavoritesSyncStatus.SyncError.GallerySyncError.UnableToAddGalleryToRemote ->
|
||||||
|
context.stringResource(SYMR.strings.favorites_sync_unable_to_add_to_remote, it.title, it.gid)
|
||||||
|
FavoritesSyncStatus.SyncError.GallerySyncError.UnableToDeleteFromRemote ->
|
||||||
|
context.stringResource(SYMR.strings.favorites_sync_unable_to_delete)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
positiveButtonText = context.stringResource(MR.strings.action_ok),
|
positiveButtonText = context.stringResource(MR.strings.action_ok),
|
||||||
positiveButton = setStatusIdle,
|
positiveButton = setStatusIdle,
|
||||||
)
|
)
|
||||||
is FavoritesSyncStatus.Idle -> value = null
|
is FavoritesSyncStatus.Idle -> value = null
|
||||||
is FavoritesSyncStatus.Initializing, is FavoritesSyncStatus.Processing -> {
|
is FavoritesSyncStatus.Initializing -> {
|
||||||
value = SyncFavoritesProgressProperties(
|
value = SyncFavoritesProgressProperties(
|
||||||
title = context.stringResource(SYMR.strings.favorites_syncing),
|
title = context.stringResource(SYMR.strings.favorites_syncing),
|
||||||
text = status.message,
|
text = context.stringResource(SYMR.strings.favorites_sync_initializing),
|
||||||
canDismiss = false,
|
|
||||||
)
|
)
|
||||||
if (status is FavoritesSyncStatus.Processing && status.title != null) {
|
}
|
||||||
|
|
||||||
|
is FavoritesSyncStatus.SyncError -> value = SyncFavoritesProgressProperties(
|
||||||
|
title = context.stringResource(SYMR.strings.favorites_sync_error),
|
||||||
|
text = context.stringResource(
|
||||||
|
SYMR.strings.favorites_sync_error_string,
|
||||||
|
when (status) {
|
||||||
|
FavoritesSyncStatus.SyncError.NotLoggedInSyncError -> context.stringResource(SYMR.strings.please_login)
|
||||||
|
FavoritesSyncStatus.SyncError.FailedToFetchFavorites ->
|
||||||
|
context.stringResource(SYMR.strings.favorites_sync_failed_to_featch)
|
||||||
|
is FavoritesSyncStatus.SyncError.UnknownSyncError ->
|
||||||
|
context.stringResource(SYMR.strings.favorites_sync_unknown_error, status.message)
|
||||||
|
is FavoritesSyncStatus.SyncError.GallerySyncError.GalleryAddFail ->
|
||||||
|
context.stringResource(SYMR.strings.favorites_sync_failed_to_add_to_local) +
|
||||||
|
context.stringResource(
|
||||||
|
SYMR.strings.favorites_sync_failed_to_add_to_local_error, status.title, status.reason,
|
||||||
|
)
|
||||||
|
is FavoritesSyncStatus.SyncError.GallerySyncError.InvalidGalleryFail ->
|
||||||
|
context.stringResource(SYMR.strings.favorites_sync_failed_to_add_to_local) +
|
||||||
|
context.stringResource(
|
||||||
|
SYMR.strings.favorites_sync_failed_to_add_to_local_unknown_type, status.title, status.url,
|
||||||
|
)
|
||||||
|
is FavoritesSyncStatus.SyncError.GallerySyncError.UnableToAddGalleryToRemote ->
|
||||||
|
context.stringResource(SYMR.strings.favorites_sync_unable_to_add_to_remote, status.title, status.gid)
|
||||||
|
FavoritesSyncStatus.SyncError.GallerySyncError.UnableToDeleteFromRemote ->
|
||||||
|
context.stringResource(SYMR.strings.favorites_sync_unable_to_delete)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
positiveButtonText = context.stringResource(MR.strings.action_ok),
|
||||||
|
positiveButton = setStatusIdle,
|
||||||
|
)
|
||||||
|
is FavoritesSyncStatus.Processing -> {
|
||||||
|
val properties = SyncFavoritesProgressProperties(
|
||||||
|
title = context.stringResource(SYMR.strings.favorites_syncing),
|
||||||
|
text = when (status) {
|
||||||
|
FavoritesSyncStatus.Processing.VerifyingLibrary ->
|
||||||
|
context.stringResource(SYMR.strings.favorites_sync_verifying_library)
|
||||||
|
FavoritesSyncStatus.Processing.DownloadingFavorites ->
|
||||||
|
context.stringResource(SYMR.strings.favorites_sync_downloading)
|
||||||
|
FavoritesSyncStatus.Processing.CalculatingRemoteChanges ->
|
||||||
|
context.stringResource(SYMR.strings.favorites_sync_calculating_remote_changes)
|
||||||
|
FavoritesSyncStatus.Processing.CalculatingLocalChanges ->
|
||||||
|
context.stringResource(SYMR.strings.favorites_sync_calculating_local_changes)
|
||||||
|
FavoritesSyncStatus.Processing.SyncingCategoryNames ->
|
||||||
|
context.stringResource(SYMR.strings.favorites_sync_syncing_category_names)
|
||||||
|
is FavoritesSyncStatus.Processing.RemovingRemoteGalleries ->
|
||||||
|
context.stringResource(SYMR.strings.favorites_sync_removing_galleries, status.galleryCount)
|
||||||
|
is FavoritesSyncStatus.Processing.AddingGalleryToRemote ->
|
||||||
|
if (status.isThrottling) {
|
||||||
|
context.stringResource(
|
||||||
|
SYMR.strings.favorites_sync_processing_throttle,
|
||||||
|
context.stringResource(SYMR.strings.favorites_sync_adding_to_remote, status.index, status.total),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
context.stringResource(SYMR.strings.favorites_sync_adding_to_remote, status.index, status.total)
|
||||||
|
}
|
||||||
|
is FavoritesSyncStatus.Processing.RemovingGalleryFromLocal ->
|
||||||
|
context.stringResource(SYMR.strings.favorites_sync_remove_from_local, status.index, status.total)
|
||||||
|
is FavoritesSyncStatus.Processing.AddingGalleryToLocal ->
|
||||||
|
if (status.isThrottling) {
|
||||||
|
context.stringResource(
|
||||||
|
SYMR.strings.favorites_sync_processing_throttle,
|
||||||
|
context.stringResource(SYMR.strings.favorites_sync_add_to_local, status.index, status.total),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
context.stringResource(SYMR.strings.favorites_sync_add_to_local, status.index, status.total)
|
||||||
|
}
|
||||||
|
|
||||||
|
FavoritesSyncStatus.Processing.CleaningUp ->
|
||||||
|
context.stringResource(SYMR.strings.favorites_sync_cleaning_up)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
value = properties
|
||||||
|
if (
|
||||||
|
status is FavoritesSyncStatus.Processing.AddingGalleryToRemote ||
|
||||||
|
status is FavoritesSyncStatus.Processing.AddingGalleryToLocal
|
||||||
|
) {
|
||||||
delay(5.seconds)
|
delay(5.seconds)
|
||||||
value = SyncFavoritesProgressProperties(
|
value = properties.copy(
|
||||||
title = context.stringResource(SYMR.strings.favorites_syncing),
|
text = when (status) {
|
||||||
text = status.delayedMessage ?: status.message,
|
is FavoritesSyncStatus.Processing.AddingGalleryToRemote ->
|
||||||
canDismiss = false,
|
properties.text + "\n\n" + status.title
|
||||||
|
is FavoritesSyncStatus.Processing.AddingGalleryToLocal ->
|
||||||
|
properties.text + "\n\n" + status.title
|
||||||
|
else -> properties.text
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -112,8 +206,8 @@ fun SyncFavoritesProgressDialog(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
properties = DialogProperties(
|
properties = DialogProperties(
|
||||||
dismissOnClickOutside = dialog.canDismiss,
|
dismissOnClickOutside = false,
|
||||||
dismissOnBackPress = dialog.canDismiss,
|
dismissOnBackPress = false,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -103,8 +103,7 @@ import tachiyomi.presentation.core.components.material.ExtendedFloatingActionBut
|
|||||||
import tachiyomi.presentation.core.components.material.PullRefresh
|
import tachiyomi.presentation.core.components.material.PullRefresh
|
||||||
import tachiyomi.presentation.core.components.material.Scaffold
|
import tachiyomi.presentation.core.components.material.Scaffold
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
import tachiyomi.presentation.core.util.isScrolledToEnd
|
import tachiyomi.presentation.core.util.shouldExpandFAB
|
||||||
import tachiyomi.presentation.core.util.isScrollingUp
|
|
||||||
import tachiyomi.source.local.isLocal
|
import tachiyomi.source.local.isLocal
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.time.ZoneId
|
import java.time.ZoneId
|
||||||
@@ -432,7 +431,7 @@ private fun MangaScreenSmallImpl(
|
|||||||
},
|
},
|
||||||
icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) },
|
icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) },
|
||||||
onClick = onContinueReading,
|
onClick = onContinueReading,
|
||||||
expanded = chapterListState.isScrollingUp() || chapterListState.isScrolledToEnd(),
|
expanded = chapterListState.shouldExpandFAB(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -442,7 +441,7 @@ private fun MangaScreenSmallImpl(
|
|||||||
PullRefresh(
|
PullRefresh(
|
||||||
refreshing = state.isRefreshingData,
|
refreshing = state.isRefreshingData,
|
||||||
onRefresh = onRefresh,
|
onRefresh = onRefresh,
|
||||||
enabled = { !isAnySelected },
|
enabled = !isAnySelected,
|
||||||
indicatorPadding = PaddingValues(top = topPadding),
|
indicatorPadding = PaddingValues(top = topPadding),
|
||||||
) {
|
) {
|
||||||
val layoutDirection = LocalLayoutDirection.current
|
val layoutDirection = LocalLayoutDirection.current
|
||||||
@@ -467,13 +466,9 @@ private fun MangaScreenSmallImpl(
|
|||||||
MangaInfoBox(
|
MangaInfoBox(
|
||||||
isTabletUi = false,
|
isTabletUi = false,
|
||||||
appBarPadding = topPadding,
|
appBarPadding = topPadding,
|
||||||
title = state.manga.title,
|
manga = state.manga,
|
||||||
author = state.manga.author,
|
|
||||||
artist = state.manga.artist,
|
|
||||||
sourceName = remember { state.source.getNameForMangaInfo(state.mergedData?.sources) },
|
sourceName = remember { state.source.getNameForMangaInfo(state.mergedData?.sources) },
|
||||||
isStubSource = remember { state.source is StubSource },
|
isStubSource = remember { state.source is StubSource },
|
||||||
coverDataProvider = { state.manga },
|
|
||||||
status = state.manga.status,
|
|
||||||
onCoverClick = onCoverClicked,
|
onCoverClick = onCoverClicked,
|
||||||
doSearch = onSearch,
|
doSearch = onSearch,
|
||||||
)
|
)
|
||||||
@@ -757,7 +752,7 @@ fun MangaScreenLargeImpl(
|
|||||||
},
|
},
|
||||||
icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) },
|
icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) },
|
||||||
onClick = onContinueReading,
|
onClick = onContinueReading,
|
||||||
expanded = chapterListState.isScrollingUp() || chapterListState.isScrolledToEnd(),
|
expanded = chapterListState.shouldExpandFAB(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -765,7 +760,7 @@ fun MangaScreenLargeImpl(
|
|||||||
PullRefresh(
|
PullRefresh(
|
||||||
refreshing = state.isRefreshingData,
|
refreshing = state.isRefreshingData,
|
||||||
onRefresh = onRefresh,
|
onRefresh = onRefresh,
|
||||||
enabled = { !isAnySelected },
|
enabled = !isAnySelected,
|
||||||
indicatorPadding = PaddingValues(
|
indicatorPadding = PaddingValues(
|
||||||
start = insetPadding.calculateStartPadding(layoutDirection),
|
start = insetPadding.calculateStartPadding(layoutDirection),
|
||||||
top = with(density) { topBarHeight.toDp() },
|
top = with(density) { topBarHeight.toDp() },
|
||||||
@@ -786,13 +781,9 @@ fun MangaScreenLargeImpl(
|
|||||||
MangaInfoBox(
|
MangaInfoBox(
|
||||||
isTabletUi = true,
|
isTabletUi = true,
|
||||||
appBarPadding = contentPadding.calculateTopPadding(),
|
appBarPadding = contentPadding.calculateTopPadding(),
|
||||||
title = state.manga.title,
|
manga = state.manga,
|
||||||
author = state.manga.author,
|
|
||||||
artist = state.manga.artist,
|
|
||||||
sourceName = remember { state.source.getNameForMangaInfo(state.mergedData?.sources) },
|
sourceName = remember { state.source.getNameForMangaInfo(state.mergedData?.sources) },
|
||||||
isStubSource = remember { state.source is StubSource },
|
isStubSource = remember { state.source is StubSource },
|
||||||
coverDataProvider = { state.manga },
|
|
||||||
status = state.manga.status,
|
|
||||||
onCoverClick = onCoverClicked,
|
onCoverClick = onCoverClicked,
|
||||||
doSearch = onSearch,
|
doSearch = onSearch,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ import tachiyomi.presentation.core.util.clickableNoIndication
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MangaCoverDialog(
|
fun MangaCoverDialog(
|
||||||
coverDataProvider: () -> Manga,
|
manga: Manga,
|
||||||
isCustomCover: Boolean,
|
isCustomCover: Boolean,
|
||||||
snackbarHostState: SnackbarHostState,
|
snackbarHostState: SnackbarHostState,
|
||||||
onShareClick: () -> Unit,
|
onShareClick: () -> Unit,
|
||||||
@@ -167,7 +167,7 @@ fun MangaCoverDialog(
|
|||||||
},
|
},
|
||||||
update = { view ->
|
update = { view ->
|
||||||
val request = ImageRequest.Builder(view.context)
|
val request = ImageRequest.Builder(view.context)
|
||||||
.data(coverDataProvider())
|
.data(manga)
|
||||||
.size(Size.ORIGINAL)
|
.size(Size.ORIGINAL)
|
||||||
.memoryCachePolicy(CachePolicy.DISABLED)
|
.memoryCachePolicy(CachePolicy.DISABLED)
|
||||||
.target { image ->
|
.target { image ->
|
||||||
|
|||||||
@@ -75,6 +75,8 @@ import androidx.compose.ui.unit.Dp
|
|||||||
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 coil3.compose.AsyncImage
|
import coil3.compose.AsyncImage
|
||||||
|
import coil3.request.ImageRequest
|
||||||
|
import coil3.request.crossfade
|
||||||
import eu.kanade.presentation.components.DropdownMenu
|
import eu.kanade.presentation.components.DropdownMenu
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
@@ -99,13 +101,9 @@ private val whitespaceLineRegex = Regex("[\\r\\n]{2,}", setOf(RegexOption.MULTIL
|
|||||||
fun MangaInfoBox(
|
fun MangaInfoBox(
|
||||||
isTabletUi: Boolean,
|
isTabletUi: Boolean,
|
||||||
appBarPadding: Dp,
|
appBarPadding: Dp,
|
||||||
title: String,
|
manga: Manga,
|
||||||
author: String?,
|
|
||||||
artist: String?,
|
|
||||||
sourceName: String,
|
sourceName: String,
|
||||||
isStubSource: Boolean,
|
isStubSource: Boolean,
|
||||||
coverDataProvider: () -> Manga,
|
|
||||||
status: Long,
|
|
||||||
onCoverClick: () -> Unit,
|
onCoverClick: () -> Unit,
|
||||||
doSearch: (query: String, global: Boolean) -> Unit,
|
doSearch: (query: String, global: Boolean) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
@@ -117,7 +115,10 @@ fun MangaInfoBox(
|
|||||||
MaterialTheme.colorScheme.background,
|
MaterialTheme.colorScheme.background,
|
||||||
)
|
)
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
model = coverDataProvider(),
|
model = ImageRequest.Builder(LocalContext.current)
|
||||||
|
.data(manga)
|
||||||
|
.crossfade(true)
|
||||||
|
.build(),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
contentScale = ContentScale.Crop,
|
contentScale = ContentScale.Crop,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -137,28 +138,20 @@ fun MangaInfoBox(
|
|||||||
if (!isTabletUi) {
|
if (!isTabletUi) {
|
||||||
MangaAndSourceTitlesSmall(
|
MangaAndSourceTitlesSmall(
|
||||||
appBarPadding = appBarPadding,
|
appBarPadding = appBarPadding,
|
||||||
coverDataProvider = coverDataProvider,
|
manga = manga,
|
||||||
onCoverClick = onCoverClick,
|
|
||||||
title = title,
|
|
||||||
doSearch = doSearch,
|
|
||||||
author = author,
|
|
||||||
artist = artist,
|
|
||||||
status = status,
|
|
||||||
sourceName = sourceName,
|
sourceName = sourceName,
|
||||||
isStubSource = isStubSource,
|
isStubSource = isStubSource,
|
||||||
|
onCoverClick = onCoverClick,
|
||||||
|
doSearch = doSearch,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
MangaAndSourceTitlesLarge(
|
MangaAndSourceTitlesLarge(
|
||||||
appBarPadding = appBarPadding,
|
appBarPadding = appBarPadding,
|
||||||
coverDataProvider = coverDataProvider,
|
manga = manga,
|
||||||
onCoverClick = onCoverClick,
|
|
||||||
title = title,
|
|
||||||
doSearch = doSearch,
|
|
||||||
author = author,
|
|
||||||
artist = artist,
|
|
||||||
status = status,
|
|
||||||
sourceName = sourceName,
|
sourceName = sourceName,
|
||||||
isStubSource = isStubSource,
|
isStubSource = isStubSource,
|
||||||
|
onCoverClick = onCoverClick,
|
||||||
|
doSearch = doSearch,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -377,15 +370,11 @@ fun ExpandableMangaDescription(
|
|||||||
@Composable
|
@Composable
|
||||||
private fun MangaAndSourceTitlesLarge(
|
private fun MangaAndSourceTitlesLarge(
|
||||||
appBarPadding: Dp,
|
appBarPadding: Dp,
|
||||||
coverDataProvider: () -> Manga,
|
manga: Manga,
|
||||||
onCoverClick: () -> Unit,
|
|
||||||
title: String,
|
|
||||||
doSearch: (query: String, global: Boolean) -> Unit,
|
|
||||||
author: String?,
|
|
||||||
artist: String?,
|
|
||||||
status: Long,
|
|
||||||
sourceName: String,
|
sourceName: String,
|
||||||
isStubSource: Boolean,
|
isStubSource: Boolean,
|
||||||
|
onCoverClick: () -> Unit,
|
||||||
|
doSearch: (query: String, global: Boolean) -> Unit,
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -395,19 +384,22 @@ private fun MangaAndSourceTitlesLarge(
|
|||||||
) {
|
) {
|
||||||
MangaCover.Book(
|
MangaCover.Book(
|
||||||
modifier = Modifier.fillMaxWidth(0.65f),
|
modifier = Modifier.fillMaxWidth(0.65f),
|
||||||
data = coverDataProvider(),
|
data = ImageRequest.Builder(LocalContext.current)
|
||||||
|
.data(manga)
|
||||||
|
.crossfade(true)
|
||||||
|
.build(),
|
||||||
contentDescription = stringResource(MR.strings.manga_cover),
|
contentDescription = stringResource(MR.strings.manga_cover),
|
||||||
onClick = onCoverClick,
|
onClick = onCoverClick,
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
MangaContentInfo(
|
MangaContentInfo(
|
||||||
title = title,
|
title = manga.title,
|
||||||
doSearch = doSearch,
|
author = manga.author,
|
||||||
author = author,
|
artist = manga.artist,
|
||||||
artist = artist,
|
status = manga.status,
|
||||||
status = status,
|
|
||||||
sourceName = sourceName,
|
sourceName = sourceName,
|
||||||
isStubSource = isStubSource,
|
isStubSource = isStubSource,
|
||||||
|
doSearch = doSearch,
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -416,15 +408,11 @@ private fun MangaAndSourceTitlesLarge(
|
|||||||
@Composable
|
@Composable
|
||||||
private fun MangaAndSourceTitlesSmall(
|
private fun MangaAndSourceTitlesSmall(
|
||||||
appBarPadding: Dp,
|
appBarPadding: Dp,
|
||||||
coverDataProvider: () -> Manga,
|
manga: Manga,
|
||||||
onCoverClick: () -> Unit,
|
|
||||||
title: String,
|
|
||||||
doSearch: (query: String, global: Boolean) -> Unit,
|
|
||||||
author: String?,
|
|
||||||
artist: String?,
|
|
||||||
status: Long,
|
|
||||||
sourceName: String,
|
sourceName: String,
|
||||||
isStubSource: Boolean,
|
isStubSource: Boolean,
|
||||||
|
onCoverClick: () -> Unit,
|
||||||
|
doSearch: (query: String, global: Boolean) -> Unit,
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -437,7 +425,10 @@ private fun MangaAndSourceTitlesSmall(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.sizeIn(maxWidth = 100.dp)
|
.sizeIn(maxWidth = 100.dp)
|
||||||
.align(Alignment.Top),
|
.align(Alignment.Top),
|
||||||
data = coverDataProvider(),
|
data = ImageRequest.Builder(LocalContext.current)
|
||||||
|
.data(manga)
|
||||||
|
.crossfade(true)
|
||||||
|
.build(),
|
||||||
contentDescription = stringResource(MR.strings.manga_cover),
|
contentDescription = stringResource(MR.strings.manga_cover),
|
||||||
onClick = onCoverClick,
|
onClick = onCoverClick,
|
||||||
)
|
)
|
||||||
@@ -445,13 +436,13 @@ private fun MangaAndSourceTitlesSmall(
|
|||||||
verticalArrangement = Arrangement.spacedBy(2.dp),
|
verticalArrangement = Arrangement.spacedBy(2.dp),
|
||||||
) {
|
) {
|
||||||
MangaContentInfo(
|
MangaContentInfo(
|
||||||
title = title,
|
title = manga.title,
|
||||||
doSearch = doSearch,
|
author = manga.author,
|
||||||
author = author,
|
artist = manga.artist,
|
||||||
artist = artist,
|
status = manga.status,
|
||||||
status = status,
|
|
||||||
sourceName = sourceName,
|
sourceName = sourceName,
|
||||||
isStubSource = isStubSource,
|
isStubSource = isStubSource,
|
||||||
|
doSearch = doSearch,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -460,12 +451,12 @@ private fun MangaAndSourceTitlesSmall(
|
|||||||
@Composable
|
@Composable
|
||||||
private fun ColumnScope.MangaContentInfo(
|
private fun ColumnScope.MangaContentInfo(
|
||||||
title: String,
|
title: String,
|
||||||
doSearch: (query: String, global: Boolean) -> Unit,
|
|
||||||
author: String?,
|
author: String?,
|
||||||
artist: String?,
|
artist: String?,
|
||||||
status: Long,
|
status: Long,
|
||||||
sourceName: String,
|
sourceName: String,
|
||||||
isStubSource: Boolean,
|
isStubSource: Boolean,
|
||||||
|
doSearch: (query: String, global: Boolean) -> Unit,
|
||||||
textAlign: TextAlign? = LocalTextStyle.current.textAlign,
|
textAlign: TextAlign? = LocalTextStyle.current.textAlign,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import androidx.compose.foundation.layout.FlowRow
|
|||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
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.material3.LocalMinimumInteractiveComponentEnforcement
|
import androidx.compose.material3.LocalMinimumInteractiveComponentSize
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.SuggestionChip
|
import androidx.compose.material3.SuggestionChip
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
@@ -141,7 +141,7 @@ fun TagsChip(
|
|||||||
border: ChipBorder? = SuggestionChipDefaults.suggestionChipBorder(),
|
border: ChipBorder? = SuggestionChipDefaults.suggestionChipBorder(),
|
||||||
borderM3: BorderStroke? = SuggestionChipDefaultsM3.suggestionChipBorder(enabled = true),
|
borderM3: BorderStroke? = SuggestionChipDefaultsM3.suggestionChipBorder(enabled = true),
|
||||||
) {
|
) {
|
||||||
CompositionLocalProvider(LocalMinimumInteractiveComponentEnforcement provides false) {
|
CompositionLocalProvider(LocalMinimumInteractiveComponentSize provides 0.dp) {
|
||||||
if (onClick != null) {
|
if (onClick != null) {
|
||||||
SuggestionChip(
|
SuggestionChip(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
|
|||||||
@@ -32,8 +32,6 @@ import tachiyomi.i18n.MR
|
|||||||
import tachiyomi.presentation.core.components.material.TextButton
|
import tachiyomi.presentation.core.components.material.TextButton
|
||||||
import tachiyomi.presentation.core.components.material.padding
|
import tachiyomi.presentation.core.components.material.padding
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
import tachiyomi.presentation.core.util.isScrolledToEnd
|
|
||||||
import tachiyomi.presentation.core.util.isScrolledToStart
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ScanlatorFilterDialog(
|
fun ScanlatorFilterDialog(
|
||||||
@@ -97,8 +95,8 @@ fun ScanlatorFilterDialog(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!state.isScrolledToStart()) HorizontalDivider(modifier = Modifier.align(Alignment.TopCenter))
|
if (state.canScrollBackward) HorizontalDivider(modifier = Modifier.align(Alignment.TopCenter))
|
||||||
if (!state.isScrolledToEnd()) HorizontalDivider(modifier = Modifier.align(Alignment.BottomCenter))
|
if (state.canScrollForward) HorizontalDivider(modifier = Modifier.align(Alignment.BottomCenter))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
properties = DialogProperties(
|
properties = DialogProperties(
|
||||||
|
|||||||
@@ -14,11 +14,13 @@ import androidx.compose.foundation.layout.Column
|
|||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Check
|
import androidx.compose.material.icons.filled.Check
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.ListItem
|
import androidx.compose.material3.ListItem
|
||||||
import androidx.compose.material3.ListItemDefaults
|
import androidx.compose.material3.ListItemDefaults
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedButton
|
import androidx.compose.material3.OutlinedButton
|
||||||
|
import androidx.compose.material3.Switch
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
@@ -28,19 +30,24 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.content.getSystemService
|
import androidx.core.content.getSystemService
|
||||||
import androidx.lifecycle.DefaultLifecycleObserver
|
import androidx.lifecycle.DefaultLifecycleObserver
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||||
import eu.kanade.presentation.util.rememberRequestPackageInstallsPermissionState
|
import eu.kanade.presentation.util.rememberRequestPackageInstallsPermissionState
|
||||||
|
import eu.kanade.tachiyomi.core.security.PrivacyPreferences
|
||||||
import eu.kanade.tachiyomi.util.system.launchRequestPackageInstallsPermission
|
import eu.kanade.tachiyomi.util.system.launchRequestPackageInstallsPermission
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
|
import tachiyomi.presentation.core.util.collectAsState
|
||||||
import tachiyomi.presentation.core.util.secondaryItemAlpha
|
import tachiyomi.presentation.core.util.secondaryItemAlpha
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
internal class PermissionStep : OnboardingStep {
|
internal class PermissionStep : OnboardingStep {
|
||||||
|
|
||||||
|
private val privacyPreferences: PrivacyPreferences by injectLazy()
|
||||||
|
|
||||||
private var notificationGranted by mutableStateOf(false)
|
private var notificationGranted by mutableStateOf(false)
|
||||||
private var batteryGranted by mutableStateOf(false)
|
private var batteryGranted by mutableStateOf(false)
|
||||||
|
|
||||||
@@ -73,7 +80,7 @@ internal class PermissionStep : OnboardingStep {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
PermissionItem(
|
PermissionCheckbox(
|
||||||
title = stringResource(MR.strings.onboarding_permission_install_apps),
|
title = stringResource(MR.strings.onboarding_permission_install_apps),
|
||||||
subtitle = stringResource(MR.strings.onboarding_permission_install_apps_description),
|
subtitle = stringResource(MR.strings.onboarding_permission_install_apps_description),
|
||||||
granted = installGranted,
|
granted = installGranted,
|
||||||
@@ -89,7 +96,7 @@ internal class PermissionStep : OnboardingStep {
|
|||||||
// no-op. resulting checks is being done on resume
|
// no-op. resulting checks is being done on resume
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
PermissionItem(
|
PermissionCheckbox(
|
||||||
title = stringResource(MR.strings.onboarding_permission_notifications),
|
title = stringResource(MR.strings.onboarding_permission_notifications),
|
||||||
subtitle = stringResource(MR.strings.onboarding_permission_notifications_description),
|
subtitle = stringResource(MR.strings.onboarding_permission_notifications_description),
|
||||||
granted = notificationGranted,
|
granted = notificationGranted,
|
||||||
@@ -97,7 +104,7 @@ internal class PermissionStep : OnboardingStep {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
PermissionItem(
|
PermissionCheckbox(
|
||||||
title = stringResource(MR.strings.onboarding_permission_ignore_battery_opts),
|
title = stringResource(MR.strings.onboarding_permission_ignore_battery_opts),
|
||||||
subtitle = stringResource(MR.strings.onboarding_permission_ignore_battery_opts_description),
|
subtitle = stringResource(MR.strings.onboarding_permission_ignore_battery_opts_description),
|
||||||
granted = batteryGranted,
|
granted = batteryGranted,
|
||||||
@@ -109,6 +116,29 @@ internal class PermissionStep : OnboardingStep {
|
|||||||
context.startActivity(intent)
|
context.startActivity(intent)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
HorizontalDivider(
|
||||||
|
modifier = Modifier.padding(vertical = 8.dp, horizontal = 16.dp),
|
||||||
|
color = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||||
|
)
|
||||||
|
|
||||||
|
val crashlyticsPref = privacyPreferences.crashlytics()
|
||||||
|
val crashlytics by crashlyticsPref.collectAsState()
|
||||||
|
PermissionSwitch(
|
||||||
|
title = stringResource(MR.strings.onboarding_permission_crashlytics),
|
||||||
|
subtitle = stringResource(MR.strings.onboarding_permission_crashlytics_description),
|
||||||
|
granted = crashlytics,
|
||||||
|
onToggleChange = crashlyticsPref::set,
|
||||||
|
)
|
||||||
|
|
||||||
|
val analyticsPref = privacyPreferences.analytics()
|
||||||
|
val analytics by analyticsPref.collectAsState()
|
||||||
|
PermissionSwitch(
|
||||||
|
title = stringResource(MR.strings.onboarding_permission_analytics),
|
||||||
|
subtitle = stringResource(MR.strings.onboarding_permission_analytics_description),
|
||||||
|
granted = analytics,
|
||||||
|
onToggleChange = analyticsPref::set,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,7 +157,7 @@ internal class PermissionStep : OnboardingStep {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun PermissionItem(
|
private fun PermissionCheckbox(
|
||||||
title: String,
|
title: String,
|
||||||
subtitle: String,
|
subtitle: String,
|
||||||
granted: Boolean,
|
granted: Boolean,
|
||||||
@@ -157,4 +187,26 @@ internal class PermissionStep : OnboardingStep {
|
|||||||
colors = ListItemDefaults.colors(containerColor = Color.Transparent),
|
colors = ListItemDefaults.colors(containerColor = Color.Transparent),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun PermissionSwitch(
|
||||||
|
title: String,
|
||||||
|
subtitle: String,
|
||||||
|
granted: Boolean,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onToggleChange: (Boolean) -> Unit,
|
||||||
|
) {
|
||||||
|
ListItem(
|
||||||
|
modifier = modifier,
|
||||||
|
headlineContent = { Text(text = title) },
|
||||||
|
supportingContent = { Text(text = subtitle) },
|
||||||
|
trailingContent = {
|
||||||
|
Switch(
|
||||||
|
checked = granted,
|
||||||
|
onCheckedChange = onToggleChange,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
colors = ListItemDefaults.colors(containerColor = Color.Transparent),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -165,12 +165,12 @@ sealed class Preference {
|
|||||||
|
|
||||||
data class CustomPreference(
|
data class CustomPreference(
|
||||||
override val title: String,
|
override val title: String,
|
||||||
val content: @Composable (PreferenceItem<String>) -> Unit,
|
val content: @Composable () -> Unit,
|
||||||
) : PreferenceItem<String>() {
|
) : PreferenceItem<Unit>() {
|
||||||
override val enabled: Boolean = true
|
override val enabled: Boolean = true
|
||||||
override val subtitle: String? = null
|
override val subtitle: String? = null
|
||||||
override val icon: ImageVector? = null
|
override val icon: ImageVector? = null
|
||||||
override val onValueChanged: suspend (newValue: String) -> Boolean = { true }
|
override val onValueChanged: suspend (newValue: Unit) -> Boolean = { true }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ internal fun PreferenceItem(
|
|||||||
InfoWidget(text = item.title)
|
InfoWidget(text = item.title)
|
||||||
}
|
}
|
||||||
is Preference.PreferenceItem.CustomPreference -> {
|
is Preference.PreferenceItem.CustomPreference -> {
|
||||||
item.content(item)
|
item.content()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+23
@@ -59,6 +59,7 @@ import eu.kanade.tachiyomi.source.AndroidSourceManager
|
|||||||
import eu.kanade.tachiyomi.ui.more.OnboardingScreen
|
import eu.kanade.tachiyomi.ui.more.OnboardingScreen
|
||||||
import eu.kanade.tachiyomi.util.CrashLogUtil
|
import eu.kanade.tachiyomi.util.CrashLogUtil
|
||||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||||
|
import eu.kanade.tachiyomi.util.system.GLUtil
|
||||||
import eu.kanade.tachiyomi.util.system.isDevFlavor
|
import eu.kanade.tachiyomi.util.system.isDevFlavor
|
||||||
import eu.kanade.tachiyomi.util.system.isPreviewBuildType
|
import eu.kanade.tachiyomi.util.system.isPreviewBuildType
|
||||||
import eu.kanade.tachiyomi.util.system.isShizukuInstalled
|
import eu.kanade.tachiyomi.util.system.isShizukuInstalled
|
||||||
@@ -83,6 +84,7 @@ import tachiyomi.core.common.i18n.pluralStringResource
|
|||||||
import tachiyomi.core.common.i18n.stringResource
|
import tachiyomi.core.common.i18n.stringResource
|
||||||
import tachiyomi.core.common.util.lang.launchNonCancellable
|
import tachiyomi.core.common.util.lang.launchNonCancellable
|
||||||
import tachiyomi.core.common.util.lang.withUIContext
|
import tachiyomi.core.common.util.lang.withUIContext
|
||||||
|
import tachiyomi.core.common.util.system.ImageUtil
|
||||||
import tachiyomi.core.common.util.system.logcat
|
import tachiyomi.core.common.util.system.logcat
|
||||||
import tachiyomi.domain.UnsortedPreferences
|
import tachiyomi.domain.UnsortedPreferences
|
||||||
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
|
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
|
||||||
@@ -299,6 +301,7 @@ object SettingsAdvancedScreen : SearchableSettings {
|
|||||||
try {
|
try {
|
||||||
// OkHttp checks for valid values internally
|
// OkHttp checks for valid values internally
|
||||||
Headers.Builder().add("User-Agent", it)
|
Headers.Builder().add("User-Agent", it)
|
||||||
|
context.toast(MR.strings.requires_app_restart)
|
||||||
} catch (_: IllegalArgumentException) {
|
} catch (_: IllegalArgumentException) {
|
||||||
context.toast(MR.strings.error_user_agent_string_invalid)
|
context.toast(MR.strings.error_user_agent_string_invalid)
|
||||||
return@EditTextPreference false
|
return@EditTextPreference false
|
||||||
@@ -368,6 +371,26 @@ object SettingsAdvancedScreen : SearchableSettings {
|
|||||||
return Preference.PreferenceGroup(
|
return Preference.PreferenceGroup(
|
||||||
title = stringResource(MR.strings.pref_category_reader),
|
title = stringResource(MR.strings.pref_category_reader),
|
||||||
preferenceItems = persistentListOf(
|
preferenceItems = persistentListOf(
|
||||||
|
Preference.PreferenceItem.ListPreference(
|
||||||
|
pref = basePreferences.hardwareBitmapThreshold(),
|
||||||
|
title = stringResource(MR.strings.pref_hardware_bitmap_threshold),
|
||||||
|
subtitleProvider = { value, options ->
|
||||||
|
stringResource(MR.strings.pref_hardware_bitmap_threshold_summary, options[value].orEmpty())
|
||||||
|
},
|
||||||
|
enabled = !ImageUtil.HARDWARE_BITMAP_UNSUPPORTED &&
|
||||||
|
GLUtil.DEVICE_TEXTURE_LIMIT > GLUtil.SAFE_TEXTURE_LIMIT,
|
||||||
|
entries = GLUtil.CUSTOM_TEXTURE_LIMIT_OPTIONS
|
||||||
|
.mapIndexed { index, option ->
|
||||||
|
val display = if (index == 0) {
|
||||||
|
stringResource(MR.strings.pref_hardware_bitmap_threshold_default, option)
|
||||||
|
} else {
|
||||||
|
option.toString()
|
||||||
|
}
|
||||||
|
option to display
|
||||||
|
}
|
||||||
|
.toMap()
|
||||||
|
.toImmutableMap(),
|
||||||
|
),
|
||||||
Preference.PreferenceItem.TextPreference(
|
Preference.PreferenceItem.TextPreference(
|
||||||
title = stringResource(MR.strings.pref_display_profile),
|
title = stringResource(MR.strings.pref_display_profile),
|
||||||
subtitle = basePreferences.displayProfile().get(),
|
subtitle = basePreferences.displayProfile().get(),
|
||||||
|
|||||||
@@ -537,7 +537,7 @@ object SettingsDataScreen : SearchableSettings {
|
|||||||
subtitle = stringResource(SYMR.strings.pref_sync_now_subtitle),
|
subtitle = stringResource(SYMR.strings.pref_sync_now_subtitle),
|
||||||
onClick = {
|
onClick = {
|
||||||
if (!SyncDataJob.isRunning(context)) {
|
if (!SyncDataJob.isRunning(context)) {
|
||||||
SyncDataJob.startNow(context)
|
SyncDataJob.startNow(context, manual = true)
|
||||||
} else {
|
} else {
|
||||||
context.toast(SYMR.strings.sync_in_progress)
|
context.toast(SYMR.strings.sync_in_progress)
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-2
@@ -15,7 +15,6 @@ import eu.kanade.presentation.more.settings.widget.TriStateListDialog
|
|||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.collections.immutable.persistentMapOf
|
import kotlinx.collections.immutable.persistentMapOf
|
||||||
import kotlinx.collections.immutable.toImmutableMap
|
import kotlinx.collections.immutable.toImmutableMap
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import tachiyomi.domain.category.interactor.GetCategories
|
import tachiyomi.domain.category.interactor.GetCategories
|
||||||
import tachiyomi.domain.category.model.Category
|
import tachiyomi.domain.category.model.Category
|
||||||
import tachiyomi.domain.download.service.DownloadPreferences
|
import tachiyomi.domain.download.service.DownloadPreferences
|
||||||
@@ -35,7 +34,7 @@ object SettingsDownloadScreen : SearchableSettings {
|
|||||||
@Composable
|
@Composable
|
||||||
override fun getPreferences(): List<Preference> {
|
override fun getPreferences(): List<Preference> {
|
||||||
val getCategories = remember { Injekt.get<GetCategories>() }
|
val getCategories = remember { Injekt.get<GetCategories>() }
|
||||||
val allCategories by getCategories.subscribe().collectAsState(initial = runBlocking { getCategories.await() })
|
val allCategories by getCategories.subscribe().collectAsState(initial = emptyList())
|
||||||
|
|
||||||
val downloadPreferences = remember { Injekt.get<DownloadPreferences>() }
|
val downloadPreferences = remember { Injekt.get<DownloadPreferences>() }
|
||||||
return listOf(
|
return listOf(
|
||||||
|
|||||||
+1
-4
@@ -25,7 +25,6 @@ import kotlinx.collections.immutable.persistentListOf
|
|||||||
import kotlinx.collections.immutable.persistentMapOf
|
import kotlinx.collections.immutable.persistentMapOf
|
||||||
import kotlinx.collections.immutable.toImmutableMap
|
import kotlinx.collections.immutable.toImmutableMap
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import tachiyomi.domain.UnsortedPreferences
|
import tachiyomi.domain.UnsortedPreferences
|
||||||
import tachiyomi.domain.category.interactor.GetCategories
|
import tachiyomi.domain.category.interactor.GetCategories
|
||||||
import tachiyomi.domain.category.interactor.ResetCategoryFlags
|
import tachiyomi.domain.category.interactor.ResetCategoryFlags
|
||||||
@@ -57,9 +56,7 @@ object SettingsLibraryScreen : SearchableSettings {
|
|||||||
override fun getPreferences(): List<Preference> {
|
override fun getPreferences(): List<Preference> {
|
||||||
val getCategories = remember { Injekt.get<GetCategories>() }
|
val getCategories = remember { Injekt.get<GetCategories>() }
|
||||||
val libraryPreferences = remember { Injekt.get<LibraryPreferences>() }
|
val libraryPreferences = remember { Injekt.get<LibraryPreferences>() }
|
||||||
val allCategories by getCategories.subscribe().collectAsState(
|
val allCategories by getCategories.subscribe().collectAsState(initial = emptyList())
|
||||||
initial = runBlocking { getCategories.await() },
|
|
||||||
)
|
|
||||||
// SY -->
|
// SY -->
|
||||||
val unsortedPreferences = remember { Injekt.get<UnsortedPreferences>() }
|
val unsortedPreferences = remember { Injekt.get<UnsortedPreferences>() }
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|||||||
+1
-1
@@ -139,7 +139,7 @@ object SettingsMangadexScreen : SearchableSettings {
|
|||||||
title = mdex.name + " Login",
|
title = mdex.name + " Login",
|
||||||
content = {
|
content = {
|
||||||
BasePreferenceWidget(
|
BasePreferenceWidget(
|
||||||
title = it.title,
|
title = mdex.name + " Login",
|
||||||
widget = {
|
widget = {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Outlined.PeopleAlt,
|
imageVector = Icons.Outlined.PeopleAlt,
|
||||||
|
|||||||
+12
-17
@@ -13,8 +13,10 @@ import androidx.compose.foundation.lazy.LazyListState
|
|||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.foundation.text.BasicTextField
|
import androidx.compose.foundation.text.BasicTextField
|
||||||
import androidx.compose.foundation.text.KeyboardActions
|
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.foundation.text.input.TextFieldLineLimits
|
||||||
|
import androidx.compose.foundation.text.input.clearText
|
||||||
|
import androidx.compose.foundation.text.input.rememberTextFieldState
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.outlined.Close
|
import androidx.compose.material.icons.outlined.Close
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
@@ -28,11 +30,8 @@ import androidx.compose.runtime.DisposableEffect
|
|||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.NonRestartableComposable
|
import androidx.compose.runtime.NonRestartableComposable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.produceState
|
import androidx.compose.runtime.produceState
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
|
||||||
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.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
@@ -43,7 +42,6 @@ import androidx.compose.ui.platform.LocalLayoutDirection
|
|||||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.input.ImeAction
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
import androidx.compose.ui.text.input.TextFieldValue
|
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.LayoutDirection
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@@ -88,9 +86,7 @@ class SettingsSearchScreen : Screen() {
|
|||||||
focusRequester.requestFocus()
|
focusRequester.requestFocus()
|
||||||
}
|
}
|
||||||
|
|
||||||
var textFieldValue by rememberSaveable(stateSaver = TextFieldValue.Saver) {
|
val textFieldState = rememberTextFieldState()
|
||||||
mutableStateOf(TextFieldValue())
|
|
||||||
}
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
Column {
|
Column {
|
||||||
@@ -105,20 +101,19 @@ class SettingsSearchScreen : Screen() {
|
|||||||
},
|
},
|
||||||
title = {
|
title = {
|
||||||
BasicTextField(
|
BasicTextField(
|
||||||
value = textFieldValue,
|
state = textFieldState,
|
||||||
onValueChange = { textFieldValue = it },
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.focusRequester(focusRequester)
|
.focusRequester(focusRequester)
|
||||||
.runOnEnterKeyPressed(action = focusManager::clearFocus),
|
.runOnEnterKeyPressed(action = focusManager::clearFocus),
|
||||||
textStyle = MaterialTheme.typography.bodyLarge
|
textStyle = MaterialTheme.typography.bodyLarge
|
||||||
.copy(color = MaterialTheme.colorScheme.onSurface),
|
.copy(color = MaterialTheme.colorScheme.onSurface),
|
||||||
singleLine = true,
|
lineLimits = TextFieldLineLimits.SingleLine,
|
||||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
|
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
|
||||||
keyboardActions = KeyboardActions(onSearch = { focusManager.clearFocus() }),
|
onKeyboardAction = { focusManager.clearFocus() },
|
||||||
cursorBrush = SolidColor(MaterialTheme.colorScheme.primary),
|
cursorBrush = SolidColor(MaterialTheme.colorScheme.primary),
|
||||||
decorationBox = {
|
decorator = {
|
||||||
if (textFieldValue.text.isEmpty()) {
|
if (textFieldState.text.isEmpty()) {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(MR.strings.action_search_settings),
|
text = stringResource(MR.strings.action_search_settings),
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
@@ -130,8 +125,8 @@ class SettingsSearchScreen : Screen() {
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
actions = {
|
actions = {
|
||||||
if (textFieldValue.text.isNotEmpty()) {
|
if (textFieldState.text.isNotEmpty()) {
|
||||||
IconButton(onClick = { textFieldValue = TextFieldValue() }) {
|
IconButton(onClick = { textFieldState.clearText() }) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Outlined.Close,
|
imageVector = Icons.Outlined.Close,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
@@ -146,7 +141,7 @@ class SettingsSearchScreen : Screen() {
|
|||||||
},
|
},
|
||||||
) { contentPadding ->
|
) { contentPadding ->
|
||||||
SearchResult(
|
SearchResult(
|
||||||
searchKey = textFieldValue.text,
|
searchKey = textFieldState.text.toString(),
|
||||||
listState = listState,
|
listState = listState,
|
||||||
contentPadding = contentPadding,
|
contentPadding = contentPadding,
|
||||||
) { result ->
|
) { result ->
|
||||||
|
|||||||
+151
-115
@@ -45,6 +45,7 @@ import cafe.adriel.voyager.navigator.LocalNavigator
|
|||||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||||
import dev.icerock.moko.resources.StringResource
|
import dev.icerock.moko.resources.StringResource
|
||||||
import eu.kanade.presentation.more.settings.Preference
|
import eu.kanade.presentation.more.settings.Preference
|
||||||
|
import eu.kanade.tachiyomi.core.security.PrivacyPreferences
|
||||||
import eu.kanade.tachiyomi.core.security.SecurityPreferences
|
import eu.kanade.tachiyomi.core.security.SecurityPreferences
|
||||||
import eu.kanade.tachiyomi.ui.base.delegate.SecureActivityDelegate
|
import eu.kanade.tachiyomi.ui.base.delegate.SecureActivityDelegate
|
||||||
import eu.kanade.tachiyomi.ui.category.biometric.BiometricTimesScreen
|
import eu.kanade.tachiyomi.ui.category.biometric.BiometricTimesScreen
|
||||||
@@ -70,10 +71,20 @@ object SettingsSecurityScreen : SearchableSettings {
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun getPreferences(): List<Preference> {
|
override fun getPreferences(): List<Preference> {
|
||||||
val context = LocalContext.current
|
|
||||||
val securityPreferences = remember { Injekt.get<SecurityPreferences>() }
|
val securityPreferences = remember { Injekt.get<SecurityPreferences>() }
|
||||||
val authSupported = remember { context.isAuthenticationSupported() }
|
val privacyPreferences = remember { Injekt.get<PrivacyPreferences>() }
|
||||||
|
return listOf(
|
||||||
|
getSecurityGroup(securityPreferences),
|
||||||
|
getFirebaseGroup(privacyPreferences),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun getSecurityGroup(
|
||||||
|
securityPreferences: SecurityPreferences,
|
||||||
|
): Preference.PreferenceGroup {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val authSupported = remember { context.isAuthenticationSupported() }
|
||||||
val useAuthPref = securityPreferences.useAuthenticator()
|
val useAuthPref = securityPreferences.useAuthenticator()
|
||||||
val useAuth by useAuthPref.collectAsState()
|
val useAuth by useAuthPref.collectAsState()
|
||||||
|
|
||||||
@@ -81,129 +92,132 @@ object SettingsSecurityScreen : SearchableSettings {
|
|||||||
val isCbzPasswordSet by remember { CbzCrypto.isPasswordSetState(scope) }.collectAsState()
|
val isCbzPasswordSet by remember { CbzCrypto.isPasswordSetState(scope) }.collectAsState()
|
||||||
val passwordProtectDownloads by securityPreferences.passwordProtectDownloads().collectAsState()
|
val passwordProtectDownloads by securityPreferences.passwordProtectDownloads().collectAsState()
|
||||||
|
|
||||||
return listOf(
|
return Preference.PreferenceGroup(
|
||||||
Preference.PreferenceItem.SwitchPreference(
|
title = stringResource(MR.strings.pref_security),
|
||||||
pref = useAuthPref,
|
preferenceItems = persistentListOf(
|
||||||
title = stringResource(MR.strings.lock_with_biometrics),
|
Preference.PreferenceItem.SwitchPreference(
|
||||||
enabled = authSupported,
|
pref = useAuthPref,
|
||||||
onValueChanged = {
|
title = stringResource(MR.strings.lock_with_biometrics),
|
||||||
(context as FragmentActivity).authenticate(
|
enabled = authSupported,
|
||||||
title = context.stringResource(MR.strings.lock_with_biometrics),
|
onValueChanged = {
|
||||||
)
|
(context as FragmentActivity).authenticate(
|
||||||
},
|
title = context.stringResource(MR.strings.lock_with_biometrics),
|
||||||
),
|
)
|
||||||
Preference.PreferenceItem.ListPreference(
|
},
|
||||||
pref = securityPreferences.lockAppAfter(),
|
),
|
||||||
title = stringResource(MR.strings.lock_when_idle),
|
Preference.PreferenceItem.ListPreference(
|
||||||
enabled = authSupported && useAuth,
|
pref = securityPreferences.lockAppAfter(),
|
||||||
entries = LockAfterValues
|
title = stringResource(MR.strings.lock_when_idle),
|
||||||
.associateWith {
|
enabled = authSupported && useAuth,
|
||||||
when (it) {
|
entries = LockAfterValues
|
||||||
-1 -> stringResource(MR.strings.lock_never)
|
.associateWith {
|
||||||
0 -> stringResource(MR.strings.lock_always)
|
when (it) {
|
||||||
else -> pluralStringResource(MR.plurals.lock_after_mins, count = it, it)
|
-1 -> stringResource(MR.strings.lock_never)
|
||||||
|
0 -> stringResource(MR.strings.lock_always)
|
||||||
|
else -> pluralStringResource(MR.plurals.lock_after_mins, count = it, it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
.toImmutableMap(),
|
||||||
|
onValueChanged = {
|
||||||
|
(context as FragmentActivity).authenticate(
|
||||||
|
title = context.stringResource(MR.strings.lock_when_idle),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Preference.PreferenceItem.SwitchPreference(
|
||||||
|
pref = securityPreferences.hideNotificationContent(),
|
||||||
|
title = stringResource(MR.strings.hide_notification_content),
|
||||||
|
),
|
||||||
|
Preference.PreferenceItem.ListPreference(
|
||||||
|
pref = securityPreferences.secureScreen(),
|
||||||
|
title = stringResource(MR.strings.secure_screen),
|
||||||
|
entries = SecurityPreferences.SecureScreenMode.entries
|
||||||
|
.associateWith { stringResource(it.titleRes) }
|
||||||
|
.toImmutableMap(),
|
||||||
|
),
|
||||||
|
// SY -->
|
||||||
|
Preference.PreferenceItem.SwitchPreference(
|
||||||
|
pref = securityPreferences.passwordProtectDownloads(),
|
||||||
|
title = stringResource(SYMR.strings.password_protect_downloads),
|
||||||
|
subtitle = stringResource(SYMR.strings.password_protect_downloads_summary),
|
||||||
|
enabled = isCbzPasswordSet,
|
||||||
|
),
|
||||||
|
Preference.PreferenceItem.ListPreference(
|
||||||
|
pref = securityPreferences.encryptionType(),
|
||||||
|
title = stringResource(SYMR.strings.encryption_type),
|
||||||
|
entries = SecurityPreferences.EncryptionType.entries
|
||||||
|
.associateWith { stringResource(it.titleRes) }
|
||||||
|
.toImmutableMap(),
|
||||||
|
enabled = passwordProtectDownloads,
|
||||||
|
|
||||||
|
),
|
||||||
|
kotlin.run {
|
||||||
|
var dialogOpen by remember { mutableStateOf(false) }
|
||||||
|
if (dialogOpen) {
|
||||||
|
PasswordDialog(
|
||||||
|
onDismissRequest = { dialogOpen = false },
|
||||||
|
onReturnPassword = { password ->
|
||||||
|
dialogOpen = false
|
||||||
|
|
||||||
|
CbzCrypto.deleteKeyCbz()
|
||||||
|
securityPreferences.cbzPassword().set(CbzCrypto.encryptCbz(password.replace("\n", "")))
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
.toImmutableMap(),
|
Preference.PreferenceItem.TextPreference(
|
||||||
onValueChanged = {
|
title = stringResource(SYMR.strings.set_cbz_zip_password),
|
||||||
(context as FragmentActivity).authenticate(
|
onClick = {
|
||||||
title = context.stringResource(MR.strings.lock_when_idle),
|
dialogOpen = true
|
||||||
)
|
|
||||||
},
|
|
||||||
),
|
|
||||||
Preference.PreferenceItem.SwitchPreference(
|
|
||||||
pref = securityPreferences.hideNotificationContent(),
|
|
||||||
title = stringResource(MR.strings.hide_notification_content),
|
|
||||||
),
|
|
||||||
Preference.PreferenceItem.ListPreference(
|
|
||||||
pref = securityPreferences.secureScreen(),
|
|
||||||
title = stringResource(MR.strings.secure_screen),
|
|
||||||
entries = SecurityPreferences.SecureScreenMode.entries
|
|
||||||
.associateWith { stringResource(it.titleRes) }
|
|
||||||
.toImmutableMap(),
|
|
||||||
),
|
|
||||||
// SY -->
|
|
||||||
Preference.PreferenceItem.SwitchPreference(
|
|
||||||
pref = securityPreferences.passwordProtectDownloads(),
|
|
||||||
title = stringResource(SYMR.strings.password_protect_downloads),
|
|
||||||
subtitle = stringResource(SYMR.strings.password_protect_downloads_summary),
|
|
||||||
enabled = isCbzPasswordSet,
|
|
||||||
),
|
|
||||||
Preference.PreferenceItem.ListPreference(
|
|
||||||
pref = securityPreferences.encryptionType(),
|
|
||||||
title = stringResource(SYMR.strings.encryption_type),
|
|
||||||
entries = SecurityPreferences.EncryptionType.entries
|
|
||||||
.associateWith { stringResource(it.titleRes) }
|
|
||||||
.toImmutableMap(),
|
|
||||||
enabled = passwordProtectDownloads,
|
|
||||||
|
|
||||||
),
|
|
||||||
kotlin.run {
|
|
||||||
var dialogOpen by remember { mutableStateOf(false) }
|
|
||||||
if (dialogOpen) {
|
|
||||||
PasswordDialog(
|
|
||||||
onDismissRequest = { dialogOpen = false },
|
|
||||||
onReturnPassword = { password ->
|
|
||||||
dialogOpen = false
|
|
||||||
|
|
||||||
CbzCrypto.deleteKeyCbz()
|
|
||||||
securityPreferences.cbzPassword().set(CbzCrypto.encryptCbz(password.replace("\n", "")))
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
|
||||||
Preference.PreferenceItem.TextPreference(
|
|
||||||
title = stringResource(SYMR.strings.set_cbz_zip_password),
|
|
||||||
onClick = {
|
|
||||||
dialogOpen = true
|
|
||||||
},
|
|
||||||
)
|
|
||||||
},
|
|
||||||
Preference.PreferenceItem.TextPreference(
|
|
||||||
title = stringResource(SYMR.strings.delete_cbz_archive_password),
|
|
||||||
onClick = {
|
|
||||||
CbzCrypto.deleteKeyCbz()
|
|
||||||
securityPreferences.cbzPassword().set("")
|
|
||||||
},
|
},
|
||||||
enabled = isCbzPasswordSet,
|
|
||||||
),
|
|
||||||
kotlin.run {
|
|
||||||
val navigator = LocalNavigator.currentOrThrow
|
|
||||||
val count by securityPreferences.authenticatorTimeRanges().collectAsState()
|
|
||||||
Preference.PreferenceItem.TextPreference(
|
Preference.PreferenceItem.TextPreference(
|
||||||
title = stringResource(SYMR.strings.action_edit_biometric_lock_times),
|
title = stringResource(SYMR.strings.delete_cbz_archive_password),
|
||||||
subtitle = pluralStringResource(
|
|
||||||
SYMR.plurals.num_lock_times,
|
|
||||||
count.size,
|
|
||||||
count.size,
|
|
||||||
),
|
|
||||||
onClick = {
|
onClick = {
|
||||||
navigator.push(BiometricTimesScreen())
|
CbzCrypto.deleteKeyCbz()
|
||||||
|
securityPreferences.cbzPassword().set("")
|
||||||
},
|
},
|
||||||
enabled = useAuth,
|
enabled = isCbzPasswordSet,
|
||||||
)
|
),
|
||||||
},
|
kotlin.run {
|
||||||
kotlin.run {
|
val navigator = LocalNavigator.currentOrThrow
|
||||||
val selection by securityPreferences.authenticatorDays().collectAsState()
|
val count by securityPreferences.authenticatorTimeRanges().collectAsState()
|
||||||
var dialogOpen by remember { mutableStateOf(false) }
|
Preference.PreferenceItem.TextPreference(
|
||||||
if (dialogOpen) {
|
title = stringResource(SYMR.strings.action_edit_biometric_lock_times),
|
||||||
SetLockedDaysDialog(
|
subtitle = pluralStringResource(
|
||||||
onDismissRequest = { dialogOpen = false },
|
SYMR.plurals.num_lock_times,
|
||||||
initialSelection = selection,
|
count.size,
|
||||||
onDaysSelected = {
|
count.size,
|
||||||
dialogOpen = false
|
),
|
||||||
securityPreferences.authenticatorDays().set(it)
|
onClick = {
|
||||||
|
navigator.push(BiometricTimesScreen())
|
||||||
},
|
},
|
||||||
|
enabled = useAuth,
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
Preference.PreferenceItem.TextPreference(
|
kotlin.run {
|
||||||
title = stringResource(SYMR.strings.biometric_lock_days),
|
val selection by securityPreferences.authenticatorDays().collectAsState()
|
||||||
subtitle = stringResource(SYMR.strings.biometric_lock_days_summary),
|
var dialogOpen by remember { mutableStateOf(false) }
|
||||||
onClick = { dialogOpen = true },
|
if (dialogOpen) {
|
||||||
enabled = useAuth,
|
SetLockedDaysDialog(
|
||||||
)
|
onDismissRequest = { dialogOpen = false },
|
||||||
},
|
initialSelection = selection,
|
||||||
// SY <--
|
onDaysSelected = {
|
||||||
Preference.PreferenceItem.InfoPreference(stringResource(MR.strings.secure_screen_summary)),
|
dialogOpen = false
|
||||||
|
securityPreferences.authenticatorDays().set(it)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Preference.PreferenceItem.TextPreference(
|
||||||
|
title = stringResource(SYMR.strings.biometric_lock_days),
|
||||||
|
subtitle = stringResource(SYMR.strings.biometric_lock_days_summary),
|
||||||
|
onClick = { dialogOpen = true },
|
||||||
|
enabled = useAuth,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
// SY <--
|
||||||
|
Preference.PreferenceItem.InfoPreference(stringResource(MR.strings.secure_screen_summary)),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -361,6 +375,28 @@ object SettingsSecurityScreen : SearchableSettings {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun getFirebaseGroup(
|
||||||
|
privacyPreferences: PrivacyPreferences,
|
||||||
|
): Preference.PreferenceGroup {
|
||||||
|
return Preference.PreferenceGroup(
|
||||||
|
title = stringResource(MR.strings.pref_firebase),
|
||||||
|
preferenceItems = persistentListOf(
|
||||||
|
Preference.PreferenceItem.SwitchPreference(
|
||||||
|
pref = privacyPreferences.crashlytics(),
|
||||||
|
title = stringResource(MR.strings.onboarding_permission_crashlytics),
|
||||||
|
subtitle = stringResource(MR.strings.onboarding_permission_crashlytics_description),
|
||||||
|
),
|
||||||
|
Preference.PreferenceItem.SwitchPreference(
|
||||||
|
pref = privacyPreferences.analytics(),
|
||||||
|
title = stringResource(MR.strings.onboarding_permission_analytics),
|
||||||
|
subtitle = stringResource(MR.strings.onboarding_permission_analytics_description),
|
||||||
|
),
|
||||||
|
Preference.PreferenceItem.InfoPreference(stringResource(MR.strings.firebase_summary)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val LockAfterValues = persistentListOf(
|
private val LockAfterValues = persistentListOf(
|
||||||
|
|||||||
+10
@@ -40,6 +40,7 @@ import androidx.compose.ui.text.input.VisualTransformation
|
|||||||
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 dev.icerock.moko.resources.StringResource
|
import dev.icerock.moko.resources.StringResource
|
||||||
|
import eu.kanade.domain.track.model.AutoTrackState
|
||||||
import eu.kanade.domain.track.service.TrackPreferences
|
import eu.kanade.domain.track.service.TrackPreferences
|
||||||
import eu.kanade.presentation.more.settings.Preference
|
import eu.kanade.presentation.more.settings.Preference
|
||||||
import eu.kanade.tachiyomi.data.track.EnhancedTracker
|
import eu.kanade.tachiyomi.data.track.EnhancedTracker
|
||||||
@@ -53,6 +54,7 @@ import eu.kanade.tachiyomi.util.system.openInBrowser
|
|||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
import kotlinx.collections.immutable.toPersistentMap
|
||||||
import tachiyomi.core.common.util.lang.launchIO
|
import tachiyomi.core.common.util.lang.launchIO
|
||||||
import tachiyomi.core.common.util.lang.withUIContext
|
import tachiyomi.core.common.util.lang.withUIContext
|
||||||
import tachiyomi.domain.source.service.SourceManager
|
import tachiyomi.domain.source.service.SourceManager
|
||||||
@@ -85,6 +87,7 @@ object SettingsTrackingScreen : SearchableSettings {
|
|||||||
val trackPreferences = remember { Injekt.get<TrackPreferences>() }
|
val trackPreferences = remember { Injekt.get<TrackPreferences>() }
|
||||||
val trackerManager = remember { Injekt.get<TrackerManager>() }
|
val trackerManager = remember { Injekt.get<TrackerManager>() }
|
||||||
val sourceManager = remember { Injekt.get<SourceManager>() }
|
val sourceManager = remember { Injekt.get<SourceManager>() }
|
||||||
|
val autoTrackStatePref = trackPreferences.autoUpdateTrackOnMarkRead()
|
||||||
|
|
||||||
var dialog by remember { mutableStateOf<Any?>(null) }
|
var dialog by remember { mutableStateOf<Any?>(null) }
|
||||||
dialog?.run {
|
dialog?.run {
|
||||||
@@ -125,6 +128,13 @@ object SettingsTrackingScreen : SearchableSettings {
|
|||||||
pref = trackPreferences.autoUpdateTrack(),
|
pref = trackPreferences.autoUpdateTrack(),
|
||||||
title = stringResource(MR.strings.pref_auto_update_manga_sync),
|
title = stringResource(MR.strings.pref_auto_update_manga_sync),
|
||||||
),
|
),
|
||||||
|
Preference.PreferenceItem.ListPreference(
|
||||||
|
pref = trackPreferences.autoUpdateTrackOnMarkRead(),
|
||||||
|
title = stringResource(MR.strings.pref_auto_update_manga_on_mark_read),
|
||||||
|
entries = AutoTrackState.entries
|
||||||
|
.associateWith { stringResource(it.titleRes) }
|
||||||
|
.toPersistentMap(),
|
||||||
|
),
|
||||||
Preference.PreferenceGroup(
|
Preference.PreferenceGroup(
|
||||||
title = stringResource(MR.strings.services),
|
title = stringResource(MR.strings.services),
|
||||||
preferenceItems = persistentListOf(
|
preferenceItems = persistentListOf(
|
||||||
|
|||||||
+1
-1
@@ -25,7 +25,7 @@ import androidx.compose.ui.unit.dp
|
|||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.util.system.isPreviewBuildType
|
import eu.kanade.tachiyomi.util.system.isPreviewBuildType
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import nl.adaptivity.xmlutil.AndroidXmlReader
|
import nl.adaptivity.xmlutil.core.AndroidXmlReader
|
||||||
import nl.adaptivity.xmlutil.serialization.XML
|
import nl.adaptivity.xmlutil.serialization.XML
|
||||||
import nl.adaptivity.xmlutil.serialization.XmlSerialName
|
import nl.adaptivity.xmlutil.serialization.XmlSerialName
|
||||||
import nl.adaptivity.xmlutil.serialization.XmlValue
|
import nl.adaptivity.xmlutil.serialization.XmlValue
|
||||||
|
|||||||
+1
-1
@@ -46,7 +46,7 @@ fun ExtensionReposContent(
|
|||||||
repos.forEach {
|
repos.forEach {
|
||||||
item {
|
item {
|
||||||
ExtensionRepoListItem(
|
ExtensionRepoListItem(
|
||||||
modifier = Modifier.animateItemPlacement(),
|
modifier = Modifier.animateItem(),
|
||||||
repo = it,
|
repo = it,
|
||||||
onOpenWebsite = { onOpenWebsite(it) },
|
onOpenWebsite = { onOpenWebsite(it) },
|
||||||
onDelete = { onClickDelete(it.baseUrl) },
|
onDelete = { onClickDelete(it.baseUrl) },
|
||||||
|
|||||||
+15
-23
@@ -29,32 +29,24 @@ import androidx.compose.material3.MaterialTheme
|
|||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
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.remember
|
||||||
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.draw.alpha
|
import androidx.compose.ui.draw.alpha
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.app.ActivityCompat
|
import androidx.core.app.ActivityCompat
|
||||||
import eu.kanade.domain.ui.UiPreferences
|
|
||||||
import eu.kanade.domain.ui.model.AppTheme
|
import eu.kanade.domain.ui.model.AppTheme
|
||||||
import eu.kanade.presentation.manga.components.MangaCover
|
import eu.kanade.presentation.manga.components.MangaCover
|
||||||
import eu.kanade.presentation.theme.TachiyomiTheme
|
import eu.kanade.presentation.theme.TachiyomiTheme
|
||||||
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
||||||
import eu.kanade.tachiyomi.util.system.isDynamicColorAvailable
|
import eu.kanade.tachiyomi.util.system.isDynamicColorAvailable
|
||||||
import tachiyomi.core.common.preference.InMemoryPreferenceStore
|
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.components.material.padding
|
import tachiyomi.presentation.core.components.material.padding
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
import tachiyomi.presentation.core.util.secondaryItemAlpha
|
import tachiyomi.presentation.core.util.secondaryItemAlpha
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.fullType
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun AppThemePreferenceWidget(
|
internal fun AppThemePreferenceWidget(
|
||||||
@@ -257,18 +249,18 @@ fun AppThemePreviewItem(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@PreviewLightDark
|
// @PreviewLightDark
|
||||||
@Composable
|
// @Composable
|
||||||
private fun AppThemesListPreview() {
|
// private fun AppThemesListPreview() {
|
||||||
var appTheme by remember { mutableStateOf(AppTheme.DEFAULT) }
|
// var appTheme by remember { mutableStateOf(AppTheme.DEFAULT) }
|
||||||
Injekt.addSingleton(fullType<UiPreferences>(), UiPreferences(InMemoryPreferenceStore()))
|
// Injekt.addSingleton(fullType<UiPreferences>(), UiPreferences(InMemoryPreferenceStore()))
|
||||||
TachiyomiTheme(appTheme = appTheme) {
|
// TachiyomiTheme(appTheme = appTheme) {
|
||||||
Surface {
|
// Surface {
|
||||||
AppThemesList(
|
// AppThemesList(
|
||||||
currentTheme = appTheme,
|
// currentTheme = appTheme,
|
||||||
amoled = false,
|
// amoled = false,
|
||||||
onItemClick = { appTheme = it },
|
// onItemClick = { appTheme = it },
|
||||||
)
|
// )
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|||||||
+2
-4
@@ -26,8 +26,6 @@ import androidx.compose.ui.unit.dp
|
|||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.components.ScrollbarLazyColumn
|
import tachiyomi.presentation.core.components.ScrollbarLazyColumn
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
import tachiyomi.presentation.core.util.isScrolledToEnd
|
|
||||||
import tachiyomi.presentation.core.util.isScrolledToStart
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun <T> ListPreferenceWidget(
|
fun <T> ListPreferenceWidget(
|
||||||
@@ -69,8 +67,8 @@ fun <T> ListPreferenceWidget(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!state.isScrolledToStart()) HorizontalDivider(modifier = Modifier.align(Alignment.TopCenter))
|
if (state.canScrollBackward) HorizontalDivider(modifier = Modifier.align(Alignment.TopCenter))
|
||||||
if (!state.isScrolledToEnd()) HorizontalDivider(modifier = Modifier.align(Alignment.BottomCenter))
|
if (state.canScrollForward) HorizontalDivider(modifier = Modifier.align(Alignment.BottomCenter))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
|
|||||||
+2
-12
@@ -30,8 +30,6 @@ import androidx.compose.ui.draw.clip
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
import tachiyomi.presentation.core.util.isScrolledToEnd
|
|
||||||
import tachiyomi.presentation.core.util.isScrolledToStart
|
|
||||||
|
|
||||||
private enum class State {
|
private enum class State {
|
||||||
CHECKED,
|
CHECKED,
|
||||||
@@ -117,16 +115,8 @@ fun <T> TriStateListDialog(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!listState.isScrolledToStart()) {
|
if (listState.canScrollBackward) HorizontalDivider(modifier = Modifier.align(Alignment.TopCenter))
|
||||||
HorizontalDivider(
|
if (listState.canScrollForward) HorizontalDivider(modifier = Modifier.align(Alignment.BottomCenter))
|
||||||
modifier = Modifier.align(Alignment.TopCenter),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (!listState.isScrolledToEnd()) {
|
|
||||||
HorizontalDivider(
|
|
||||||
modifier = Modifier.align(Alignment.BottomCenter),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -8,11 +8,13 @@ import androidx.compose.foundation.lazy.rememberLazyListState
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
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.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import eu.kanade.presentation.components.AdaptiveSheet
|
import eu.kanade.presentation.components.AdaptiveSheet
|
||||||
import eu.kanade.presentation.manga.components.MangaChapterListItem
|
import eu.kanade.presentation.manga.components.MangaChapterListItem
|
||||||
|
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||||
import eu.kanade.tachiyomi.data.download.model.Download
|
import eu.kanade.tachiyomi.data.download.model.Download
|
||||||
import eu.kanade.tachiyomi.ui.reader.chapter.ReaderChapterItem
|
import eu.kanade.tachiyomi.ui.reader.chapter.ReaderChapterItem
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel
|
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel
|
||||||
@@ -20,8 +22,13 @@ import eu.kanade.tachiyomi.util.lang.toRelativeString
|
|||||||
import exh.metadata.MetadataUtil
|
import exh.metadata.MetadataUtil
|
||||||
import exh.source.isEhBasedManga
|
import exh.source.isEhBasedManga
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.coroutines.flow.filter
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
import tachiyomi.domain.chapter.model.Chapter
|
import tachiyomi.domain.chapter.model.Chapter
|
||||||
import tachiyomi.domain.library.service.LibraryPreferences
|
import tachiyomi.domain.library.service.LibraryPreferences
|
||||||
|
import tachiyomi.source.local.isLocal
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
import java.time.ZoneId
|
import java.time.ZoneId
|
||||||
@@ -39,6 +46,8 @@ fun ChapterListDialog(
|
|||||||
val manga by screenModel.mangaFlow.collectAsState()
|
val manga by screenModel.mangaFlow.collectAsState()
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val state = rememberLazyListState(chapters.indexOfFirst { it.isCurrent }.coerceAtLeast(0))
|
val state = rememberLazyListState(chapters.indexOfFirst { it.isCurrent }.coerceAtLeast(0))
|
||||||
|
val downloadManager: DownloadManager = remember { Injekt.get() }
|
||||||
|
val downloadQueueState by downloadManager.queueState.collectAsState()
|
||||||
|
|
||||||
AdaptiveSheet(
|
AdaptiveSheet(
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
@@ -52,6 +61,28 @@ fun ChapterListDialog(
|
|||||||
items = chapters,
|
items = chapters,
|
||||||
key = { "chapter-${it.chapter.id}" },
|
key = { "chapter-${it.chapter.id}" },
|
||||||
) { chapterItem ->
|
) { chapterItem ->
|
||||||
|
val activeDownload = downloadQueueState.find { it.chapter.id == chapterItem.chapter.id }
|
||||||
|
val progress = activeDownload?.let {
|
||||||
|
downloadManager.progressFlow()
|
||||||
|
.filter { it.chapter.id == chapterItem.chapter.id }
|
||||||
|
.map { it.progress }
|
||||||
|
.collectAsState(0).value
|
||||||
|
} ?: 0
|
||||||
|
val downloaded = if (chapterItem.manga.isLocal()) {
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
downloadManager.isChapterDownloaded(
|
||||||
|
chapterItem.chapter.name,
|
||||||
|
chapterItem.chapter.scanlator,
|
||||||
|
chapterItem.manga.ogTitle,
|
||||||
|
chapterItem.manga.source,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val downloadState = when {
|
||||||
|
activeDownload != null -> activeDownload.status
|
||||||
|
downloaded -> Download.State.DOWNLOADED
|
||||||
|
else -> Download.State.NOT_DOWNLOADED
|
||||||
|
}
|
||||||
MangaChapterListItem(
|
MangaChapterListItem(
|
||||||
title = chapterItem.chapter.name,
|
title = chapterItem.chapter.name,
|
||||||
date = chapterItem.chapter.dateUpload
|
date = chapterItem.chapter.dateUpload
|
||||||
@@ -76,8 +107,8 @@ fun ChapterListDialog(
|
|||||||
bookmark = chapterItem.chapter.bookmark,
|
bookmark = chapterItem.chapter.bookmark,
|
||||||
selected = false,
|
selected = false,
|
||||||
downloadIndicatorEnabled = false,
|
downloadIndicatorEnabled = false,
|
||||||
downloadStateProvider = { Download.State.NOT_DOWNLOADED },
|
downloadStateProvider = { downloadState },
|
||||||
downloadProgressProvider = { 0 },
|
downloadProgressProvider = { progress },
|
||||||
chapterSwipeStartAction = LibraryPreferences.ChapterSwipeAction.ToggleBookmark,
|
chapterSwipeStartAction = LibraryPreferences.ChapterSwipeAction.ToggleBookmark,
|
||||||
chapterSwipeEndAction = LibraryPreferences.ChapterSwipeAction.ToggleBookmark,
|
chapterSwipeEndAction = LibraryPreferences.ChapterSwipeAction.ToggleBookmark,
|
||||||
onLongClick = { /*TODO*/ },
|
onLongClick = { /*TODO*/ },
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ fun BottomReaderBar(
|
|||||||
doublePages: Boolean,
|
doublePages: Boolean,
|
||||||
onClickChapterList: () -> Unit,
|
onClickChapterList: () -> Unit,
|
||||||
onClickWebView: (() -> Unit)?,
|
onClickWebView: (() -> Unit)?,
|
||||||
|
onClickBrowser: (() -> Unit)?,
|
||||||
onClickShare: (() -> Unit)?,
|
onClickShare: (() -> Unit)?,
|
||||||
onClickPageLayout: () -> Unit,
|
onClickPageLayout: () -> Unit,
|
||||||
onClickShiftPage: () -> Unit,
|
onClickShiftPage: () -> Unit,
|
||||||
@@ -78,6 +79,15 @@ fun BottomReaderBar(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ReaderBottomButton.Browser.isIn(enabledButtons) && onClickBrowser != null) {
|
||||||
|
IconButton(onClick = onClickBrowser) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.Public,
|
||||||
|
contentDescription = stringResource(MR.strings.action_open_in_browser),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (ReaderBottomButton.Share.isIn(enabledButtons) && onClickShare != null) {
|
if (ReaderBottomButton.Share.isIn(enabledButtons) && onClickShare != null) {
|
||||||
IconButton(onClick = onClickShare) {
|
IconButton(onClick = onClickShare) {
|
||||||
Icon(
|
Icon(
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ fun ReaderAppBars(
|
|||||||
// bookmarked: Boolean,
|
// bookmarked: Boolean,
|
||||||
// onToggleBookmarked: () -> Unit,
|
// onToggleBookmarked: () -> Unit,
|
||||||
onOpenInWebView: (() -> Unit)?,
|
onOpenInWebView: (() -> Unit)?,
|
||||||
|
onOpenInBrowser: (() -> Unit)?,
|
||||||
onShare: (() -> Unit)?,
|
onShare: (() -> Unit)?,
|
||||||
|
|
||||||
viewer: Viewer?,
|
viewer: Viewer?,
|
||||||
@@ -83,7 +84,7 @@ fun ReaderAppBars(
|
|||||||
enabledPrevious: Boolean,
|
enabledPrevious: Boolean,
|
||||||
currentPage: Int,
|
currentPage: Int,
|
||||||
totalPages: Int,
|
totalPages: Int,
|
||||||
onSliderValueChange: (Int) -> Unit,
|
onPageIndexChange: (Int) -> Unit,
|
||||||
|
|
||||||
readingMode: ReadingMode,
|
readingMode: ReadingMode,
|
||||||
onClickReadingMode: () -> Unit,
|
onClickReadingMode: () -> Unit,
|
||||||
@@ -153,7 +154,7 @@ fun ReaderAppBars(
|
|||||||
enabledPrevious = enabledPrevious,
|
enabledPrevious = enabledPrevious,
|
||||||
currentPage = currentPage,
|
currentPage = currentPage,
|
||||||
totalPages = totalPages,
|
totalPages = totalPages,
|
||||||
onSliderValueChange = onSliderValueChange,
|
onPageIndexChange = onPageIndexChange,
|
||||||
isVerticalSlider = true,
|
isVerticalSlider = true,
|
||||||
currentPageText = currentPageText,
|
currentPageText = currentPageText,
|
||||||
)
|
)
|
||||||
@@ -181,7 +182,7 @@ fun ReaderAppBars(
|
|||||||
enabledPrevious = enabledPrevious,
|
enabledPrevious = enabledPrevious,
|
||||||
currentPage = currentPage,
|
currentPage = currentPage,
|
||||||
totalPages = totalPages,
|
totalPages = totalPages,
|
||||||
onSliderValueChange = onSliderValueChange,
|
onPageIndexChange = onPageIndexChange,
|
||||||
isVerticalSlider = true,
|
isVerticalSlider = true,
|
||||||
currentPageText = currentPageText,
|
currentPageText = currentPageText,
|
||||||
)
|
)
|
||||||
@@ -284,7 +285,7 @@ fun ReaderAppBars(
|
|||||||
enabledPrevious = enabledPrevious,
|
enabledPrevious = enabledPrevious,
|
||||||
currentPage = currentPage,
|
currentPage = currentPage,
|
||||||
totalPages = totalPages,
|
totalPages = totalPages,
|
||||||
onSliderValueChange = onSliderValueChange,
|
onPageIndexChange = onPageIndexChange,
|
||||||
isVerticalSlider = false,
|
isVerticalSlider = false,
|
||||||
currentPageText = currentPageText,
|
currentPageText = currentPageText,
|
||||||
)
|
)
|
||||||
@@ -308,6 +309,7 @@ fun ReaderAppBars(
|
|||||||
doublePages = doublePages,
|
doublePages = doublePages,
|
||||||
onClickChapterList = onClickChapterList,
|
onClickChapterList = onClickChapterList,
|
||||||
onClickWebView = onOpenInWebView,
|
onClickWebView = onOpenInWebView,
|
||||||
|
onClickBrowser = onOpenInBrowser,
|
||||||
onClickShare = onShare,
|
onClickShare = onShare,
|
||||||
onClickPageLayout = onClickPageLayout,
|
onClickPageLayout = onClickPageLayout,
|
||||||
onClickShiftPage = onClickShiftPage,
|
onClickShiftPage = onClickShiftPage,
|
||||||
|
|||||||
@@ -18,14 +18,15 @@ import androidx.compose.material3.FilledIconButton
|
|||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButtonDefaults
|
import androidx.compose.material3.IconButtonDefaults
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Slider
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.surfaceColorAtElevation
|
import androidx.compose.material3.surfaceColorAtElevation
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
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.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
@@ -36,13 +37,15 @@ import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
|||||||
import androidx.compose.ui.layout.layout
|
import androidx.compose.ui.layout.layout
|
||||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.Constraints
|
import androidx.compose.ui.unit.Constraints
|
||||||
import androidx.compose.ui.unit.LayoutDirection
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import eu.kanade.presentation.theme.TachiyomiPreviewTheme
|
||||||
import eu.kanade.presentation.util.isTabletUi
|
import eu.kanade.presentation.util.isTabletUi
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
|
import tachiyomi.presentation.core.components.material.Slider
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
import kotlin.math.roundToInt
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ChapterNavigator(
|
fun ChapterNavigator(
|
||||||
@@ -57,7 +60,7 @@ fun ChapterNavigator(
|
|||||||
currentPageText: String,
|
currentPageText: String,
|
||||||
// SY <--
|
// SY <--
|
||||||
totalPages: Int,
|
totalPages: Int,
|
||||||
onSliderValueChange: (Int) -> Unit,
|
onPageIndexChange: (Int) -> Unit,
|
||||||
) {
|
) {
|
||||||
// SY -->
|
// SY -->
|
||||||
if (isVerticalSlider) {
|
if (isVerticalSlider) {
|
||||||
@@ -69,13 +72,13 @@ fun ChapterNavigator(
|
|||||||
currentPage = currentPage,
|
currentPage = currentPage,
|
||||||
currentPageText = currentPageText,
|
currentPageText = currentPageText,
|
||||||
totalPages = totalPages,
|
totalPages = totalPages,
|
||||||
onSliderValueChange = onSliderValueChange,
|
onPageIndexChange = onPageIndexChange,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// SY <--
|
// SY <--
|
||||||
val isTabletUi = isTabletUi()
|
val isTabletUi = isTabletUi()
|
||||||
val horizontalPadding = if (isTabletUi) 24.dp else 16.dp
|
val horizontalPadding = if (isTabletUi) 24.dp else 8.dp
|
||||||
val layoutDirection = if (isRtl) LayoutDirection.Rtl else LayoutDirection.Ltr
|
val layoutDirection = if (isRtl) LayoutDirection.Rtl else LayoutDirection.Ltr
|
||||||
val haptic = LocalHapticFeedback.current
|
val haptic = LocalHapticFeedback.current
|
||||||
|
|
||||||
@@ -134,11 +137,11 @@ fun ChapterNavigator(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.padding(horizontal = 8.dp),
|
.padding(horizontal = 8.dp),
|
||||||
value = currentPage.toFloat(),
|
value = currentPage,
|
||||||
valueRange = 1f..totalPages.toFloat(),
|
valueRange = 1..totalPages,
|
||||||
steps = totalPages - 2,
|
onValueChange = f@{
|
||||||
onValueChange = {
|
if (it == currentPage) return@f
|
||||||
onSliderValueChange(it.roundToInt() - 1)
|
onPageIndexChange(it - 1)
|
||||||
},
|
},
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
)
|
)
|
||||||
@@ -177,10 +180,10 @@ fun ChapterNavigatorVert(
|
|||||||
currentPageText: String,
|
currentPageText: String,
|
||||||
// SY <--
|
// SY <--
|
||||||
totalPages: Int,
|
totalPages: Int,
|
||||||
onSliderValueChange: (Int) -> Unit,
|
onPageIndexChange: (Int) -> Unit,
|
||||||
) {
|
) {
|
||||||
val isTabletUi = isTabletUi()
|
val isTabletUi = isTabletUi()
|
||||||
val verticalPadding = if (isTabletUi) 24.dp else 16.dp
|
val verticalPadding = if (isTabletUi) 24.dp else 8.dp
|
||||||
|
|
||||||
val haptic = LocalHapticFeedback.current
|
val haptic = LocalHapticFeedback.current
|
||||||
|
|
||||||
@@ -252,11 +255,11 @@ fun ChapterNavigatorVert(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.weight(1f),
|
.weight(1f),
|
||||||
value = currentPage.toFloat(),
|
value = currentPage,
|
||||||
valueRange = 1f..totalPages.toFloat(),
|
valueRange = 1..totalPages,
|
||||||
steps = totalPages,
|
onValueChange = f@{
|
||||||
onValueChange = {
|
if (it == currentPage) return@f
|
||||||
onSliderValueChange(it.roundToInt() - 1)
|
onPageIndexChange(it - 1)
|
||||||
},
|
},
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
)
|
)
|
||||||
@@ -280,3 +283,25 @@ fun ChapterNavigatorVert(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
private fun ChapterNavigatorPreview() {
|
||||||
|
var currentPage by remember { mutableIntStateOf(1) }
|
||||||
|
TachiyomiPreviewTheme {
|
||||||
|
ChapterNavigator(
|
||||||
|
isRtl = false,
|
||||||
|
onNextChapter = {},
|
||||||
|
enabledNext = true,
|
||||||
|
onPreviousChapter = {},
|
||||||
|
enabledPrevious = true,
|
||||||
|
currentPage = currentPage,
|
||||||
|
totalPages = 10,
|
||||||
|
onPageIndexChange = { currentPage = (it + 1) },
|
||||||
|
// SY -->
|
||||||
|
currentPageText = "1",
|
||||||
|
isVerticalSlider = false,
|
||||||
|
// SY <--
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -43,8 +43,6 @@ import tachiyomi.presentation.core.components.WheelTextPicker
|
|||||||
import tachiyomi.presentation.core.components.material.AlertDialogContent
|
import tachiyomi.presentation.core.components.material.AlertDialogContent
|
||||||
import tachiyomi.presentation.core.components.material.padding
|
import tachiyomi.presentation.core.components.material.padding
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
import tachiyomi.presentation.core.util.isScrolledToEnd
|
|
||||||
import tachiyomi.presentation.core.util.isScrolledToStart
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TrackStatusSelector(
|
fun TrackStatusSelector(
|
||||||
@@ -86,8 +84,8 @@ fun TrackStatusSelector(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!state.isScrolledToStart()) HorizontalDivider(modifier = Modifier.align(Alignment.TopCenter))
|
if (state.canScrollBackward) HorizontalDivider(modifier = Modifier.align(Alignment.TopCenter))
|
||||||
if (!state.isScrolledToEnd()) HorizontalDivider(modifier = Modifier.align(Alignment.BottomCenter))
|
if (state.canScrollForward) HorizontalDivider(modifier = Modifier.align(Alignment.BottomCenter))
|
||||||
},
|
},
|
||||||
onConfirm = onConfirm,
|
onConfirm = onConfirm,
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
|
|||||||
@@ -25,8 +25,10 @@ import androidx.compose.foundation.layout.windowInsetsPadding
|
|||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.text.BasicTextField
|
import androidx.compose.foundation.text.BasicTextField
|
||||||
import androidx.compose.foundation.text.KeyboardActions
|
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.foundation.text.input.TextFieldLineLimits
|
||||||
|
import androidx.compose.foundation.text.input.TextFieldState
|
||||||
|
import androidx.compose.foundation.text.input.clearText
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.outlined.ArrowBack
|
import androidx.compose.material.icons.automirrored.outlined.ArrowBack
|
||||||
import androidx.compose.material.icons.filled.CheckCircle
|
import androidx.compose.material.icons.filled.CheckCircle
|
||||||
@@ -59,7 +61,6 @@ import androidx.compose.ui.platform.LocalFocusManager
|
|||||||
import androidx.compose.ui.text.AnnotatedString
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
import androidx.compose.ui.text.capitalize
|
import androidx.compose.ui.text.capitalize
|
||||||
import androidx.compose.ui.text.input.ImeAction
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
import androidx.compose.ui.text.input.TextFieldValue
|
|
||||||
import androidx.compose.ui.text.intl.Locale
|
import androidx.compose.ui.text.intl.Locale
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.text.toLowerCase
|
import androidx.compose.ui.text.toLowerCase
|
||||||
@@ -84,8 +85,7 @@ import tachiyomi.presentation.core.util.secondaryItemAlpha
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TrackerSearch(
|
fun TrackerSearch(
|
||||||
query: TextFieldValue,
|
state: TextFieldState,
|
||||||
onQueryChange: (TextFieldValue) -> Unit,
|
|
||||||
onDispatchQuery: () -> Unit,
|
onDispatchQuery: () -> Unit,
|
||||||
queryResult: Result<List<TrackSearch>>?,
|
queryResult: Result<List<TrackSearch>>?,
|
||||||
selected: TrackSearch?,
|
selected: TrackSearch?,
|
||||||
@@ -115,20 +115,19 @@ fun TrackerSearch(
|
|||||||
},
|
},
|
||||||
title = {
|
title = {
|
||||||
BasicTextField(
|
BasicTextField(
|
||||||
value = query,
|
state = state,
|
||||||
onValueChange = onQueryChange,
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.focusRequester(focusRequester)
|
.focusRequester(focusRequester)
|
||||||
.runOnEnterKeyPressed(action = dispatchQueryAndClearFocus),
|
.runOnEnterKeyPressed(action = dispatchQueryAndClearFocus),
|
||||||
textStyle = MaterialTheme.typography.bodyLarge
|
textStyle = MaterialTheme.typography.bodyLarge
|
||||||
.copy(color = MaterialTheme.colorScheme.onSurface),
|
.copy(color = MaterialTheme.colorScheme.onSurface),
|
||||||
singleLine = true,
|
lineLimits = TextFieldLineLimits.SingleLine,
|
||||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
|
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
|
||||||
keyboardActions = KeyboardActions(onSearch = { dispatchQueryAndClearFocus() }),
|
onKeyboardAction = { dispatchQueryAndClearFocus() },
|
||||||
cursorBrush = SolidColor(MaterialTheme.colorScheme.primary),
|
cursorBrush = SolidColor(MaterialTheme.colorScheme.primary),
|
||||||
decorationBox = {
|
decorator = {
|
||||||
if (query.text.isEmpty()) {
|
if (state.text.isEmpty()) {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(MR.strings.action_search_hint),
|
text = stringResource(MR.strings.action_search_hint),
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
@@ -140,10 +139,10 @@ fun TrackerSearch(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
actions = {
|
actions = {
|
||||||
if (query.text.isNotEmpty()) {
|
if (state.text.isNotEmpty()) {
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
onQueryChange(TextFieldValue())
|
state.clearText()
|
||||||
focusRequester.requestFocus()
|
focusRequester.requestFocus()
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package eu.kanade.presentation.track
|
package eu.kanade.presentation.track
|
||||||
|
|
||||||
|
import androidx.compose.foundation.text.input.TextFieldState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.text.input.TextFieldValue
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import androidx.compose.ui.tooling.preview.datasource.LoremIpsum
|
import androidx.compose.ui.tooling.preview.datasource.LoremIpsum
|
||||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
@@ -13,8 +13,7 @@ internal class TrackerSearchPreviewProvider : PreviewParameterProvider<@Composab
|
|||||||
private val fullPageWithSecondSelected = @Composable {
|
private val fullPageWithSecondSelected = @Composable {
|
||||||
val items = someTrackSearches().take(30).toList()
|
val items = someTrackSearches().take(30).toList()
|
||||||
TrackerSearch(
|
TrackerSearch(
|
||||||
query = TextFieldValue(text = "search text"),
|
state = TextFieldState(initialText = "search text"),
|
||||||
onQueryChange = {},
|
|
||||||
onDispatchQuery = {},
|
onDispatchQuery = {},
|
||||||
queryResult = Result.success(items),
|
queryResult = Result.success(items),
|
||||||
selected = items[1],
|
selected = items[1],
|
||||||
@@ -25,8 +24,7 @@ internal class TrackerSearchPreviewProvider : PreviewParameterProvider<@Composab
|
|||||||
}
|
}
|
||||||
private val fullPageWithoutSelected = @Composable {
|
private val fullPageWithoutSelected = @Composable {
|
||||||
TrackerSearch(
|
TrackerSearch(
|
||||||
query = TextFieldValue(text = ""),
|
state = TextFieldState(),
|
||||||
onQueryChange = {},
|
|
||||||
onDispatchQuery = {},
|
onDispatchQuery = {},
|
||||||
queryResult = Result.success(someTrackSearches().take(30).toList()),
|
queryResult = Result.success(someTrackSearches().take(30).toList()),
|
||||||
selected = null,
|
selected = null,
|
||||||
@@ -37,8 +35,7 @@ internal class TrackerSearchPreviewProvider : PreviewParameterProvider<@Composab
|
|||||||
}
|
}
|
||||||
private val loading = @Composable {
|
private val loading = @Composable {
|
||||||
TrackerSearch(
|
TrackerSearch(
|
||||||
query = TextFieldValue(),
|
state = TextFieldState(),
|
||||||
onQueryChange = {},
|
|
||||||
onDispatchQuery = {},
|
onDispatchQuery = {},
|
||||||
queryResult = null,
|
queryResult = null,
|
||||||
selected = null,
|
selected = null,
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ fun UpdateScreen(
|
|||||||
isRefreshing = false
|
isRefreshing = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
enabled = { !state.selectionMode },
|
enabled = !state.selectionMode,
|
||||||
indicatorPadding = contentPadding,
|
indicatorPadding = contentPadding,
|
||||||
) {
|
) {
|
||||||
FastScrollLazyColumn(
|
FastScrollLazyColumn(
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import eu.kanade.presentation.manga.components.ChapterDownloadAction
|
|||||||
import eu.kanade.presentation.manga.components.ChapterDownloadIndicator
|
import eu.kanade.presentation.manga.components.ChapterDownloadIndicator
|
||||||
import eu.kanade.presentation.manga.components.DotSeparatorText
|
import eu.kanade.presentation.manga.components.DotSeparatorText
|
||||||
import eu.kanade.presentation.manga.components.MangaCover
|
import eu.kanade.presentation.manga.components.MangaCover
|
||||||
|
import eu.kanade.presentation.util.animateItemFastScroll
|
||||||
import eu.kanade.presentation.util.relativeTimeSpanString
|
import eu.kanade.presentation.util.relativeTimeSpanString
|
||||||
import eu.kanade.tachiyomi.data.download.model.Download
|
import eu.kanade.tachiyomi.data.download.model.Download
|
||||||
import eu.kanade.tachiyomi.ui.updates.UpdatesItem
|
import eu.kanade.tachiyomi.ui.updates.UpdatesItem
|
||||||
@@ -54,7 +55,7 @@ internal fun LazyListScope.updatesLastUpdatedItem(
|
|||||||
item(key = "updates-lastUpdated") {
|
item(key = "updates-lastUpdated") {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.animateItemPlacement()
|
.animateItem(fadeInSpec = null, fadeOutSpec = null)
|
||||||
.padding(horizontal = MaterialTheme.padding.medium, vertical = MaterialTheme.padding.small),
|
.padding(horizontal = MaterialTheme.padding.medium, vertical = MaterialTheme.padding.small),
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
@@ -94,14 +95,14 @@ internal fun LazyListScope.updatesUiItems(
|
|||||||
when (item) {
|
when (item) {
|
||||||
is UpdatesUiModel.Header -> {
|
is UpdatesUiModel.Header -> {
|
||||||
ListGroupHeader(
|
ListGroupHeader(
|
||||||
modifier = Modifier.animateItemPlacement(),
|
modifier = Modifier.animateItemFastScroll(),
|
||||||
text = relativeDateText(item.date),
|
text = relativeDateText(item.date),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is UpdatesUiModel.Item -> {
|
is UpdatesUiModel.Item -> {
|
||||||
val updatesItem = item.item
|
val updatesItem = item.item
|
||||||
UpdatesUiItem(
|
UpdatesUiItem(
|
||||||
modifier = Modifier.animateItemPlacement(),
|
modifier = Modifier.animateItemFastScroll(),
|
||||||
update = updatesItem.update,
|
update = updatesItem.update,
|
||||||
selected = updatesItem.selected,
|
selected = updatesItem.selected,
|
||||||
readProgress = updatesItem.update.lastPageRead
|
readProgress = updatesItem.update.lastPageRead
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package eu.kanade.presentation.util
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import eu.kanade.tachiyomi.network.HttpException
|
import eu.kanade.tachiyomi.network.HttpException
|
||||||
import eu.kanade.tachiyomi.source.online.LicensedMangaChaptersException
|
|
||||||
import eu.kanade.tachiyomi.util.system.isOnline
|
import eu.kanade.tachiyomi.util.system.isOnline
|
||||||
import tachiyomi.core.common.i18n.stringResource
|
import tachiyomi.core.common.i18n.stringResource
|
||||||
import tachiyomi.data.source.NoResultsException
|
import tachiyomi.data.source.NoResultsException
|
||||||
@@ -25,7 +24,6 @@ val Throwable.formattedMessage: String
|
|||||||
|
|
||||||
is NoResultsException -> return stringResource(MR.strings.no_results_found)
|
is NoResultsException -> return stringResource(MR.strings.no_results_found)
|
||||||
is SourceNotInstalledException -> return stringResource(MR.strings.loader_not_implemented_error)
|
is SourceNotInstalledException -> return stringResource(MR.strings.loader_not_implemented_error)
|
||||||
is LicensedMangaChaptersException -> return stringResource(MR.strings.licensed_manga_chapters_error)
|
|
||||||
}
|
}
|
||||||
return when (val className = this::class.simpleName) {
|
return when (val className = this::class.simpleName) {
|
||||||
"Exception", "IOException" -> message ?: className
|
"Exception", "IOException" -> message ?: className
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package eu.kanade.presentation.util
|
||||||
|
|
||||||
|
import androidx.compose.foundation.lazy.LazyItemScope
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
|
||||||
|
// https://issuetracker.google.com/352584409
|
||||||
|
context(LazyItemScope)
|
||||||
|
fun Modifier.animateItemFastScroll() = this.animateItem(fadeInSpec = null, fadeOutSpec = null)
|
||||||
@@ -60,7 +60,10 @@ interface AssistContentScreen {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DefaultNavigatorScreenTransition(navigator: Navigator) {
|
fun DefaultNavigatorScreenTransition(
|
||||||
|
navigator: Navigator,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
val slideDistance = rememberSlideDistance()
|
val slideDistance = rememberSlideDistance()
|
||||||
ScreenTransition(
|
ScreenTransition(
|
||||||
navigator = navigator,
|
navigator = navigator,
|
||||||
@@ -70,6 +73,7 @@ fun DefaultNavigatorScreenTransition(navigator: Navigator) {
|
|||||||
slideDistance = slideDistance,
|
slideDistance = slideDistance,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
modifier = modifier,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ import androidx.compose.runtime.mutableStateOf
|
|||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
|
||||||
import androidx.lifecycle.DefaultLifecycleObserver
|
import androidx.lifecycle.DefaultLifecycleObserver
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun rememberRequestPackageInstallsPermissionState(initialValue: Boolean = false): Boolean {
|
fun rememberRequestPackageInstallsPermissionState(initialValue: Boolean = false): Boolean {
|
||||||
|
|||||||
@@ -104,6 +104,11 @@ fun WebViewScreenContent(
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ignore intents urls
|
||||||
|
if (it.url.toString().startsWith("intent://")) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
// Continue with request, but with custom headers
|
// Continue with request, but with custom headers
|
||||||
view?.loadUrl(it.url.toString(), headers)
|
view?.loadUrl(it.url.toString(), headers)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ import androidx.lifecycle.DefaultLifecycleObserver
|
|||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import androidx.lifecycle.ProcessLifecycleOwner
|
import androidx.lifecycle.ProcessLifecycleOwner
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.work.Configuration
|
||||||
|
import androidx.work.WorkManager
|
||||||
import coil3.ImageLoader
|
import coil3.ImageLoader
|
||||||
import coil3.SingletonImageLoader
|
import coil3.SingletonImageLoader
|
||||||
import coil3.network.okhttp.OkHttpNetworkFetcherFactory
|
import coil3.network.okhttp.OkHttpNetworkFetcherFactory
|
||||||
@@ -28,14 +30,13 @@ import com.elvishew.xlog.printer.AndroidPrinter
|
|||||||
import com.elvishew.xlog.printer.Printer
|
import com.elvishew.xlog.printer.Printer
|
||||||
import com.elvishew.xlog.printer.file.backup.NeverBackupStrategy
|
import com.elvishew.xlog.printer.file.backup.NeverBackupStrategy
|
||||||
import com.elvishew.xlog.printer.file.naming.DateFileNameGenerator
|
import com.elvishew.xlog.printer.file.naming.DateFileNameGenerator
|
||||||
import com.google.firebase.crashlytics.ktx.crashlytics
|
|
||||||
import com.google.firebase.ktx.Firebase
|
|
||||||
import eu.kanade.domain.DomainModule
|
import eu.kanade.domain.DomainModule
|
||||||
import eu.kanade.domain.SYDomainModule
|
import eu.kanade.domain.SYDomainModule
|
||||||
import eu.kanade.domain.base.BasePreferences
|
import eu.kanade.domain.base.BasePreferences
|
||||||
import eu.kanade.domain.sync.SyncPreferences
|
import eu.kanade.domain.sync.SyncPreferences
|
||||||
import eu.kanade.domain.ui.UiPreferences
|
import eu.kanade.domain.ui.UiPreferences
|
||||||
import eu.kanade.domain.ui.model.setAppCompatDelegateThemeMode
|
import eu.kanade.domain.ui.model.setAppCompatDelegateThemeMode
|
||||||
|
import eu.kanade.tachiyomi.core.security.PrivacyPreferences
|
||||||
import eu.kanade.tachiyomi.crash.CrashActivity
|
import eu.kanade.tachiyomi.crash.CrashActivity
|
||||||
import eu.kanade.tachiyomi.crash.GlobalExceptionHandler
|
import eu.kanade.tachiyomi.crash.GlobalExceptionHandler
|
||||||
import eu.kanade.tachiyomi.data.coil.BufferedSourceFetcher
|
import eu.kanade.tachiyomi.data.coil.BufferedSourceFetcher
|
||||||
@@ -48,17 +49,19 @@ import eu.kanade.tachiyomi.data.coil.TachiyomiImageDecoder
|
|||||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
import eu.kanade.tachiyomi.data.sync.SyncDataJob
|
import eu.kanade.tachiyomi.data.sync.SyncDataJob
|
||||||
import eu.kanade.tachiyomi.di.AppModule
|
import eu.kanade.tachiyomi.di.AppModule
|
||||||
|
import eu.kanade.tachiyomi.di.InjektKoinBridge
|
||||||
import eu.kanade.tachiyomi.di.PreferenceModule
|
import eu.kanade.tachiyomi.di.PreferenceModule
|
||||||
import eu.kanade.tachiyomi.di.SYPreferenceModule
|
import eu.kanade.tachiyomi.di.SYPreferenceModule
|
||||||
|
import eu.kanade.tachiyomi.di.importModule
|
||||||
|
import eu.kanade.tachiyomi.di.initExpensiveComponents
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
import eu.kanade.tachiyomi.network.NetworkPreferences
|
import eu.kanade.tachiyomi.network.NetworkPreferences
|
||||||
import eu.kanade.tachiyomi.ui.base.delegate.SecureActivityDelegate
|
import eu.kanade.tachiyomi.ui.base.delegate.SecureActivityDelegate
|
||||||
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
||||||
|
import eu.kanade.tachiyomi.util.system.GLUtil
|
||||||
import eu.kanade.tachiyomi.util.system.WebViewUtil
|
import eu.kanade.tachiyomi.util.system.WebViewUtil
|
||||||
import eu.kanade.tachiyomi.util.system.animatorDurationScale
|
import eu.kanade.tachiyomi.util.system.animatorDurationScale
|
||||||
import eu.kanade.tachiyomi.util.system.cancelNotification
|
import eu.kanade.tachiyomi.util.system.cancelNotification
|
||||||
import eu.kanade.tachiyomi.util.system.isDevFlavor
|
|
||||||
import eu.kanade.tachiyomi.util.system.isReleaseBuildType
|
|
||||||
import eu.kanade.tachiyomi.util.system.notify
|
import eu.kanade.tachiyomi.util.system.notify
|
||||||
import exh.log.CrashlyticsPrinter
|
import exh.log.CrashlyticsPrinter
|
||||||
import exh.log.EHLogLevel
|
import exh.log.EHLogLevel
|
||||||
@@ -71,12 +74,14 @@ import kotlinx.coroutines.flow.launchIn
|
|||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import logcat.LogcatLogger
|
import logcat.LogcatLogger
|
||||||
|
import mihon.core.firebase.FirebaseConfig
|
||||||
import mihon.core.migration.Migrator
|
import mihon.core.migration.Migrator
|
||||||
import mihon.core.migration.migrations.migrations
|
import mihon.core.migration.migrations.migrations
|
||||||
import org.conscrypt.Conscrypt
|
import org.conscrypt.Conscrypt
|
||||||
import tachiyomi.core.common.i18n.stringResource
|
import tachiyomi.core.common.i18n.stringResource
|
||||||
import tachiyomi.core.common.preference.Preference
|
import tachiyomi.core.common.preference.Preference
|
||||||
import tachiyomi.core.common.preference.PreferenceStore
|
import tachiyomi.core.common.preference.PreferenceStore
|
||||||
|
import tachiyomi.core.common.util.system.ImageUtil
|
||||||
import tachiyomi.core.common.util.system.logcat
|
import tachiyomi.core.common.util.system.logcat
|
||||||
import tachiyomi.domain.storage.service.StorageManager
|
import tachiyomi.domain.storage.service.StorageManager
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
@@ -91,6 +96,7 @@ import java.util.Locale
|
|||||||
class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factory {
|
class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factory {
|
||||||
|
|
||||||
private val basePreferences: BasePreferences by injectLazy()
|
private val basePreferences: BasePreferences by injectLazy()
|
||||||
|
private val privacyPreferences: PrivacyPreferences by injectLazy()
|
||||||
private val networkPreferences: NetworkPreferences by injectLazy()
|
private val networkPreferences: NetworkPreferences by injectLazy()
|
||||||
|
|
||||||
private val disableIncognitoReceiver = DisableIncognitoReceiver()
|
private val disableIncognitoReceiver = DisableIncognitoReceiver()
|
||||||
@@ -98,12 +104,8 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
|
|||||||
@SuppressLint("LaunchActivityFromNotification")
|
@SuppressLint("LaunchActivityFromNotification")
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super<Application>.onCreate()
|
super<Application>.onCreate()
|
||||||
|
FirebaseConfig.init(applicationContext)
|
||||||
|
|
||||||
// SY -->
|
|
||||||
if (!isDevFlavor) {
|
|
||||||
Firebase.crashlytics.setCrashlyticsCollectionEnabled(isReleaseBuildType)
|
|
||||||
}
|
|
||||||
// SY <--
|
|
||||||
GlobalExceptionHandler.initialize(applicationContext, CrashActivity::class.java)
|
GlobalExceptionHandler.initialize(applicationContext, CrashActivity::class.java)
|
||||||
|
|
||||||
// TLS 1.3 support for Android < 10
|
// TLS 1.3 support for Android < 10
|
||||||
@@ -123,6 +125,8 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
|
|||||||
// SY -->
|
// SY -->
|
||||||
Injekt.importModule(SYPreferenceModule(this))
|
Injekt.importModule(SYPreferenceModule(this))
|
||||||
Injekt.importModule(SYDomainModule())
|
Injekt.importModule(SYDomainModule())
|
||||||
|
InjektKoinBridge.startKoin(this)
|
||||||
|
initExpensiveComponents(this)
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
setupExhLogging() // EXH logging
|
setupExhLogging() // EXH logging
|
||||||
@@ -132,6 +136,8 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
|
|||||||
|
|
||||||
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
|
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
|
||||||
|
|
||||||
|
val scope = ProcessLifecycleOwner.get().lifecycleScope
|
||||||
|
|
||||||
// Show notification to disable Incognito Mode when it's enabled
|
// Show notification to disable Incognito Mode when it's enabled
|
||||||
basePreferences.incognitoMode().changes()
|
basePreferences.incognitoMode().changes()
|
||||||
.onEach { enabled ->
|
.onEach { enabled ->
|
||||||
@@ -159,19 +165,38 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
|
|||||||
cancelNotification(Notifications.ID_INCOGNITO_MODE)
|
cancelNotification(Notifications.ID_INCOGNITO_MODE)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.launchIn(ProcessLifecycleOwner.get().lifecycleScope)
|
.launchIn(scope)
|
||||||
|
|
||||||
|
privacyPreferences.analytics()
|
||||||
|
.changes()
|
||||||
|
.onEach(FirebaseConfig::setAnalyticsEnabled)
|
||||||
|
.launchIn(scope)
|
||||||
|
|
||||||
|
privacyPreferences.crashlytics()
|
||||||
|
.changes()
|
||||||
|
.onEach(FirebaseConfig::setCrashlyticsEnabled)
|
||||||
|
.launchIn(scope)
|
||||||
|
|
||||||
|
basePreferences.hardwareBitmapThreshold().let { preference ->
|
||||||
|
if (!preference.isSet()) preference.set(GLUtil.DEVICE_TEXTURE_LIMIT)
|
||||||
|
}
|
||||||
|
|
||||||
|
basePreferences.hardwareBitmapThreshold().changes()
|
||||||
|
.onEach { ImageUtil.hardwareBitmapThreshold = it }
|
||||||
|
.launchIn(scope)
|
||||||
|
|
||||||
setAppCompatDelegateThemeMode(Injekt.get<UiPreferences>().themeMode().get())
|
setAppCompatDelegateThemeMode(Injekt.get<UiPreferences>().themeMode().get())
|
||||||
|
|
||||||
// Updates widget update
|
// Updates widget update
|
||||||
with(WidgetManager(Injekt.get(), Injekt.get())) {
|
WidgetManager(Injekt.get(), Injekt.get()).apply { init(scope) }
|
||||||
init(ProcessLifecycleOwner.get().lifecycleScope)
|
|
||||||
}
|
|
||||||
|
|
||||||
/*if (!LogcatLogger.isInstalled && networkPreferences.verboseLogging().get()) {
|
/*if (!LogcatLogger.isInstalled && networkPreferences.verboseLogging().get()) {
|
||||||
LogcatLogger.install(AndroidLogcatLogger(LogPriority.VERBOSE))
|
LogcatLogger.install(AndroidLogcatLogger(LogPriority.VERBOSE))
|
||||||
}*/
|
}*/
|
||||||
|
|
||||||
|
if (!WorkManager.isInitialized()) {
|
||||||
|
WorkManager.initialize(this, Configuration.Builder().build())
|
||||||
|
}
|
||||||
val syncPreferences: SyncPreferences = Injekt.get()
|
val syncPreferences: SyncPreferences = Injekt.get()
|
||||||
val syncTriggerOpt = syncPreferences.getSyncTriggerOptions()
|
val syncTriggerOpt = syncPreferences.getSyncTriggerOptions()
|
||||||
if (syncPreferences.isSyncEnabled() && syncTriggerOpt.syncOnAppStart) {
|
if (syncPreferences.isSyncEnabled() && syncTriggerOpt.syncOnAppStart) {
|
||||||
@@ -273,12 +298,6 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
|
|||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logcat(LogPriority.ERROR, e) { "Failed to modify notification channels" }
|
logcat(LogPriority.ERROR, e) { "Failed to modify notification channels" }
|
||||||
}
|
}
|
||||||
|
|
||||||
val syncPreferences: SyncPreferences = Injekt.get()
|
|
||||||
val syncTriggerOpt = syncPreferences.getSyncTriggerOptions()
|
|
||||||
if (syncPreferences.isSyncEnabled() && syncTriggerOpt.syncOnAppStart) {
|
|
||||||
SyncDataJob.startNow(this@App)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// EXH
|
// EXH
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ package eu.kanade.tachiyomi.crash
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import com.google.firebase.crashlytics.ktx.crashlytics
|
|
||||||
import com.google.firebase.ktx.Firebase
|
|
||||||
import kotlinx.serialization.KSerializer
|
import kotlinx.serialization.KSerializer
|
||||||
import kotlinx.serialization.descriptors.PrimitiveKind
|
import kotlinx.serialization.descriptors.PrimitiveKind
|
||||||
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
|
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
|
||||||
@@ -13,7 +11,6 @@ import kotlinx.serialization.encoding.Encoder
|
|||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import tachiyomi.core.common.util.system.logcat
|
import tachiyomi.core.common.util.system.logcat
|
||||||
import kotlin.system.exitProcess
|
|
||||||
|
|
||||||
class GlobalExceptionHandler private constructor(
|
class GlobalExceptionHandler private constructor(
|
||||||
private val applicationContext: Context,
|
private val applicationContext: Context,
|
||||||
@@ -33,14 +30,9 @@ class GlobalExceptionHandler private constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun uncaughtException(thread: Thread, exception: Throwable) {
|
override fun uncaughtException(thread: Thread, exception: Throwable) {
|
||||||
try {
|
logcat(priority = LogPriority.ERROR, throwable = exception)
|
||||||
logcat(priority = LogPriority.ERROR, throwable = exception)
|
launchActivity(applicationContext, activityToBeLaunched, exception)
|
||||||
Firebase.crashlytics.recordException(exception)
|
defaultHandler.uncaughtException(thread, exception)
|
||||||
launchActivity(applicationContext, activityToBeLaunched, exception)
|
|
||||||
exitProcess(0)
|
|
||||||
} catch (_: Exception) {
|
|
||||||
defaultHandler.uncaughtException(thread, exception)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun launchActivity(
|
private fun launchActivity(
|
||||||
|
|||||||
@@ -27,11 +27,11 @@ import okio.sink
|
|||||||
import tachiyomi.core.common.i18n.stringResource
|
import tachiyomi.core.common.i18n.stringResource
|
||||||
import tachiyomi.core.common.util.system.logcat
|
import tachiyomi.core.common.util.system.logcat
|
||||||
import tachiyomi.data.DatabaseHandler
|
import tachiyomi.data.DatabaseHandler
|
||||||
import tachiyomi.data.manga.MangaMapper
|
|
||||||
import tachiyomi.domain.backup.service.BackupPreferences
|
import tachiyomi.domain.backup.service.BackupPreferences
|
||||||
import tachiyomi.domain.manga.interactor.GetFavorites
|
import tachiyomi.domain.manga.interactor.GetFavorites
|
||||||
import tachiyomi.domain.manga.interactor.GetMergedManga
|
import tachiyomi.domain.manga.interactor.GetMergedManga
|
||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
|
import tachiyomi.domain.manga.repository.MangaRepository
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
@@ -48,6 +48,7 @@ class BackupCreator(
|
|||||||
private val parser: ProtoBuf = Injekt.get(),
|
private val parser: ProtoBuf = Injekt.get(),
|
||||||
private val getFavorites: GetFavorites = Injekt.get(),
|
private val getFavorites: GetFavorites = Injekt.get(),
|
||||||
private val backupPreferences: BackupPreferences = Injekt.get(),
|
private val backupPreferences: BackupPreferences = Injekt.get(),
|
||||||
|
private val mangaRepository: MangaRepository = Injekt.get(),
|
||||||
|
|
||||||
private val categoriesBackupCreator: CategoriesBackupCreator = CategoriesBackupCreator(),
|
private val categoriesBackupCreator: CategoriesBackupCreator = CategoriesBackupCreator(),
|
||||||
private val mangaBackupCreator: MangaBackupCreator = MangaBackupCreator(),
|
private val mangaBackupCreator: MangaBackupCreator = MangaBackupCreator(),
|
||||||
@@ -85,15 +86,13 @@ class BackupCreator(
|
|||||||
throw IllegalStateException(context.stringResource(MR.strings.create_backup_file_error))
|
throw IllegalStateException(context.stringResource(MR.strings.create_backup_file_error))
|
||||||
}
|
}
|
||||||
|
|
||||||
val backupManga = backupMangas(
|
val nonFavoriteManga = if (options.readEntries) mangaRepository.getReadMangaNotInLibrary() else emptyList()
|
||||||
getFavorites.await() /* SY --> */ +
|
// SY -->
|
||||||
if (options.readEntries) {
|
val mergedManga = getMergedManga.await()
|
||||||
handler.awaitList { mangasQueries.getReadMangaNotInLibrary(MangaMapper::mapManga) }
|
// SY <--
|
||||||
} else {
|
val backupManga =
|
||||||
emptyList()
|
backupMangas(getFavorites.await() + nonFavoriteManga /* SY --> */ + mergedManga /* SY <-- */, options)
|
||||||
} + getMergedManga.await(), // SY <--
|
|
||||||
options,
|
|
||||||
)
|
|
||||||
val backup = Backup(
|
val backup = Backup(
|
||||||
backupManga = backupManga,
|
backupManga = backupManga,
|
||||||
backupCategories = backupCategories(options),
|
backupCategories = backupCategories(options),
|
||||||
|
|||||||
@@ -11,13 +11,13 @@ data class BackupOptions(
|
|||||||
val chapters: Boolean = true,
|
val chapters: Boolean = true,
|
||||||
val tracking: Boolean = true,
|
val tracking: Boolean = true,
|
||||||
val history: Boolean = true,
|
val history: Boolean = true,
|
||||||
|
val readEntries: Boolean = true,
|
||||||
val appSettings: Boolean = true,
|
val appSettings: Boolean = true,
|
||||||
val extensionRepoSettings: Boolean = true,
|
val extensionRepoSettings: Boolean = true,
|
||||||
val sourceSettings: Boolean = true,
|
val sourceSettings: Boolean = true,
|
||||||
val privateSettings: Boolean = false,
|
val privateSettings: Boolean = false,
|
||||||
// SY -->
|
// SY -->
|
||||||
val customInfo: Boolean = true,
|
val customInfo: Boolean = true,
|
||||||
val readEntries: Boolean = true,
|
|
||||||
val savedSearches: Boolean = true,
|
val savedSearches: Boolean = true,
|
||||||
// SY <--
|
// SY <--
|
||||||
) {
|
) {
|
||||||
@@ -28,13 +28,13 @@ data class BackupOptions(
|
|||||||
chapters,
|
chapters,
|
||||||
tracking,
|
tracking,
|
||||||
history,
|
history,
|
||||||
|
readEntries,
|
||||||
appSettings,
|
appSettings,
|
||||||
extensionRepoSettings,
|
extensionRepoSettings,
|
||||||
sourceSettings,
|
sourceSettings,
|
||||||
privateSettings,
|
privateSettings,
|
||||||
// SY -->
|
// SY -->
|
||||||
customInfo,
|
customInfo,
|
||||||
readEntries,
|
|
||||||
savedSearches,
|
savedSearches,
|
||||||
// SY <--
|
// SY <--
|
||||||
)
|
)
|
||||||
@@ -72,6 +72,12 @@ data class BackupOptions(
|
|||||||
getter = BackupOptions::categories,
|
getter = BackupOptions::categories,
|
||||||
setter = { options, enabled -> options.copy(categories = enabled) },
|
setter = { options, enabled -> options.copy(categories = enabled) },
|
||||||
),
|
),
|
||||||
|
Entry(
|
||||||
|
label = MR.strings.non_library_settings,
|
||||||
|
getter = BackupOptions::readEntries,
|
||||||
|
setter = { options, enabled -> options.copy(readEntries = enabled) },
|
||||||
|
enabled = { it.libraryEntries },
|
||||||
|
),
|
||||||
// SY -->
|
// SY -->
|
||||||
Entry(
|
Entry(
|
||||||
label = SYMR.strings.custom_entry_info,
|
label = SYMR.strings.custom_entry_info,
|
||||||
@@ -79,12 +85,6 @@ data class BackupOptions(
|
|||||||
setter = { options, enabled -> options.copy(customInfo = enabled) },
|
setter = { options, enabled -> options.copy(customInfo = enabled) },
|
||||||
enabled = { it.libraryEntries },
|
enabled = { it.libraryEntries },
|
||||||
),
|
),
|
||||||
Entry(
|
|
||||||
label = SYMR.strings.all_read_entries,
|
|
||||||
getter = BackupOptions::readEntries,
|
|
||||||
setter = { options, enabled -> options.copy(readEntries = enabled) },
|
|
||||||
enabled = { it.libraryEntries },
|
|
||||||
),
|
|
||||||
Entry(
|
Entry(
|
||||||
label = SYMR.strings.saved_searches,
|
label = SYMR.strings.saved_searches,
|
||||||
getter = BackupOptions::savedSearches,
|
getter = BackupOptions::savedSearches,
|
||||||
@@ -123,13 +123,13 @@ data class BackupOptions(
|
|||||||
chapters = array[2],
|
chapters = array[2],
|
||||||
tracking = array[3],
|
tracking = array[3],
|
||||||
history = array[4],
|
history = array[4],
|
||||||
appSettings = array[5],
|
readEntries = array[5],
|
||||||
extensionRepoSettings = array[6],
|
appSettings = array[6],
|
||||||
sourceSettings = array[7],
|
extensionRepoSettings = array[7],
|
||||||
privateSettings = array[8],
|
sourceSettings = array[8],
|
||||||
|
privateSettings = array[9],
|
||||||
// SY -->
|
// SY -->
|
||||||
customInfo = array[9],
|
customInfo = array[10],
|
||||||
readEntries = array[10],
|
|
||||||
savedSearches = array[11],
|
savedSearches = array[11],
|
||||||
// SY <--
|
// SY <--
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import coil3.request.bitmapConfig
|
|||||||
import com.hippo.unifile.UniFile
|
import com.hippo.unifile.UniFile
|
||||||
import eu.kanade.tachiyomi.util.storage.CbzCrypto
|
import eu.kanade.tachiyomi.util.storage.CbzCrypto
|
||||||
import eu.kanade.tachiyomi.util.storage.CbzCrypto.getCoverStream
|
import eu.kanade.tachiyomi.util.storage.CbzCrypto.getCoverStream
|
||||||
import eu.kanade.tachiyomi.util.system.GLUtil
|
|
||||||
import mihon.core.common.archive.archiveReader
|
import mihon.core.common.archive.archiveReader
|
||||||
import okio.BufferedSource
|
import okio.BufferedSource
|
||||||
import tachiyomi.core.common.util.system.ImageUtil
|
import tachiyomi.core.common.util.system.ImageUtil
|
||||||
@@ -71,7 +70,7 @@ class TachiyomiImageDecoder(private val resources: ImageSource, private val opti
|
|||||||
if (
|
if (
|
||||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O &&
|
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O &&
|
||||||
options.bitmapConfig == Bitmap.Config.HARDWARE &&
|
options.bitmapConfig == Bitmap.Config.HARDWARE &&
|
||||||
maxOf(bitmap.width, bitmap.height) <= GLUtil.maxTextureSize
|
ImageUtil.canUseHardwareBitmap(bitmap)
|
||||||
) {
|
) {
|
||||||
val hwBitmap = bitmap.copy(Bitmap.Config.HARDWARE, false)
|
val hwBitmap = bitmap.copy(Bitmap.Config.HARDWARE, false)
|
||||||
if (hwBitmap != null) {
|
if (hwBitmap != null) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@file:Suppress("PropertyName", "ktlint:standard:property-naming")
|
@file:Suppress("PropertyName")
|
||||||
|
|
||||||
package eu.kanade.tachiyomi.data.database.models
|
package eu.kanade.tachiyomi.data.database.models
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@file:Suppress("PropertyName", "ktlint:standard:property-naming")
|
@file:Suppress("PropertyName")
|
||||||
|
|
||||||
package eu.kanade.tachiyomi.data.database.models
|
package eu.kanade.tachiyomi.data.database.models
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@file:Suppress("PropertyName", "ktlint:standard:property-naming")
|
@file:Suppress("PropertyName")
|
||||||
|
|
||||||
package eu.kanade.tachiyomi.data.database.models
|
package eu.kanade.tachiyomi.data.database.models
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@file:Suppress("PropertyName", "ktlint:standard:property-naming")
|
@file:Suppress("PropertyName")
|
||||||
|
|
||||||
package eu.kanade.tachiyomi.data.database.models
|
package eu.kanade.tachiyomi.data.database.models
|
||||||
|
|
||||||
|
|||||||
@@ -96,13 +96,13 @@ class DownloadCache(
|
|||||||
private val diskCacheFile: File
|
private val diskCacheFile: File
|
||||||
get() = File(context.cacheDir, "dl_index_cache_v3")
|
get() = File(context.cacheDir, "dl_index_cache_v3")
|
||||||
|
|
||||||
private val rootDownloadsDirLock = Mutex()
|
private val rootDownloadsDirMutex = Mutex()
|
||||||
private var rootDownloadsDir = RootDirectory(storageManager.getDownloadsDirectory())
|
private var rootDownloadsDir = RootDirectory(storageManager.getDownloadsDirectory())
|
||||||
|
|
||||||
init {
|
init {
|
||||||
// Attempt to read cache file
|
// Attempt to read cache file
|
||||||
scope.launch {
|
scope.launch {
|
||||||
rootDownloadsDirLock.withLock {
|
rootDownloadsDirMutex.withLock {
|
||||||
try {
|
try {
|
||||||
if (diskCacheFile.exists()) {
|
if (diskCacheFile.exists()) {
|
||||||
val diskCache = diskCacheFile.inputStream().use {
|
val diskCache = diskCacheFile.inputStream().use {
|
||||||
@@ -112,7 +112,7 @@ class DownloadCache(
|
|||||||
lastRenew = System.currentTimeMillis()
|
lastRenew = System.currentTimeMillis()
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
logcat(LogPriority.ERROR, e) { "Failed to initialize disk cache" }
|
logcat(LogPriority.ERROR, e) { "Failed to initialize from disk cache" }
|
||||||
diskCacheFile.delete()
|
diskCacheFile.delete()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -200,7 +200,7 @@ class DownloadCache(
|
|||||||
* @param manga the manga of the chapter.
|
* @param manga the manga of the chapter.
|
||||||
*/
|
*/
|
||||||
suspend fun addChapter(chapterDirName: String, mangaUniFile: UniFile, manga: Manga) {
|
suspend fun addChapter(chapterDirName: String, mangaUniFile: UniFile, manga: Manga) {
|
||||||
rootDownloadsDirLock.withLock {
|
rootDownloadsDirMutex.withLock {
|
||||||
// Retrieve the cached source directory or cache a new one
|
// Retrieve the cached source directory or cache a new one
|
||||||
var sourceDir = rootDownloadsDir.sourceDirs[manga.source]
|
var sourceDir = rootDownloadsDir.sourceDirs[manga.source]
|
||||||
if (sourceDir == null) {
|
if (sourceDir == null) {
|
||||||
@@ -232,7 +232,7 @@ class DownloadCache(
|
|||||||
* @param manga the manga of the chapter.
|
* @param manga the manga of the chapter.
|
||||||
*/
|
*/
|
||||||
suspend fun removeChapter(chapter: Chapter, manga: Manga) {
|
suspend fun removeChapter(chapter: Chapter, manga: Manga) {
|
||||||
rootDownloadsDirLock.withLock {
|
rootDownloadsDirMutex.withLock {
|
||||||
val sourceDir = rootDownloadsDir.sourceDirs[manga.source] ?: return
|
val sourceDir = rootDownloadsDir.sourceDirs[manga.source] ?: return
|
||||||
val mangaDir = sourceDir.mangaDirs[
|
val mangaDir = sourceDir.mangaDirs[
|
||||||
provider.getMangaDirName(
|
provider.getMangaDirName(
|
||||||
@@ -251,7 +251,7 @@ class DownloadCache(
|
|||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
suspend fun removeFolders(folders: List<String>, manga: Manga) {
|
suspend fun removeFolders(folders: List<String>, manga: Manga) {
|
||||||
rootDownloadsDirLock.withLock {
|
rootDownloadsDirMutex.withLock {
|
||||||
val sourceDir = rootDownloadsDir.sourceDirs[manga.source] ?: return
|
val sourceDir = rootDownloadsDir.sourceDirs[manga.source] ?: return
|
||||||
val mangaDir = sourceDir.mangaDirs[provider.getMangaDirName(manga.ogTitle)] ?: return
|
val mangaDir = sourceDir.mangaDirs[provider.getMangaDirName(manga.ogTitle)] ?: return
|
||||||
folders.forEach { chapter ->
|
folders.forEach { chapter ->
|
||||||
@@ -271,7 +271,7 @@ class DownloadCache(
|
|||||||
* @param manga the manga of the chapter.
|
* @param manga the manga of the chapter.
|
||||||
*/
|
*/
|
||||||
suspend fun removeChapters(chapters: List<Chapter>, manga: Manga) {
|
suspend fun removeChapters(chapters: List<Chapter>, manga: Manga) {
|
||||||
rootDownloadsDirLock.withLock {
|
rootDownloadsDirMutex.withLock {
|
||||||
val sourceDir = rootDownloadsDir.sourceDirs[manga.source] ?: return
|
val sourceDir = rootDownloadsDir.sourceDirs[manga.source] ?: return
|
||||||
val mangaDir = sourceDir.mangaDirs[
|
val mangaDir = sourceDir.mangaDirs[
|
||||||
provider.getMangaDirName(
|
provider.getMangaDirName(
|
||||||
@@ -296,7 +296,7 @@ class DownloadCache(
|
|||||||
* @param manga the manga to remove.
|
* @param manga the manga to remove.
|
||||||
*/
|
*/
|
||||||
suspend fun removeManga(manga: Manga) {
|
suspend fun removeManga(manga: Manga) {
|
||||||
rootDownloadsDirLock.withLock {
|
rootDownloadsDirMutex.withLock {
|
||||||
val sourceDir = rootDownloadsDir.sourceDirs[manga.source] ?: return
|
val sourceDir = rootDownloadsDir.sourceDirs[manga.source] ?: return
|
||||||
val mangaDirName = provider.getMangaDirName(/* SY --> */ manga.ogTitle /* SY <-- */)
|
val mangaDirName = provider.getMangaDirName(/* SY --> */ manga.ogTitle /* SY <-- */)
|
||||||
if (sourceDir.mangaDirs.containsKey(mangaDirName)) {
|
if (sourceDir.mangaDirs.containsKey(mangaDirName)) {
|
||||||
@@ -308,7 +308,7 @@ class DownloadCache(
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun removeSource(source: Source) {
|
suspend fun removeSource(source: Source) {
|
||||||
rootDownloadsDirLock.withLock {
|
rootDownloadsDirMutex.withLock {
|
||||||
rootDownloadsDir.sourceDirs -= source.id
|
rootDownloadsDir.sourceDirs -= source.id
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -349,10 +349,10 @@ class DownloadCache(
|
|||||||
|
|
||||||
val sourceMap = sources.associate { provider.getSourceDirName(it).lowercase() to it.id }
|
val sourceMap = sources.associate { provider.getSourceDirName(it).lowercase() to it.id }
|
||||||
|
|
||||||
rootDownloadsDirLock.withLock {
|
rootDownloadsDirMutex.withLock {
|
||||||
rootDownloadsDir = RootDirectory(storageManager.getDownloadsDirectory())
|
val updatedRootDir = RootDirectory(storageManager.getDownloadsDirectory())
|
||||||
|
|
||||||
val sourceDirs = rootDownloadsDir.dir?.listFiles().orEmpty()
|
updatedRootDir.sourceDirs = updatedRootDir.dir?.listFiles().orEmpty()
|
||||||
.filter { it.isDirectory && !it.name.isNullOrBlank() }
|
.filter { it.isDirectory && !it.name.isNullOrBlank() }
|
||||||
.mapNotNull { dir ->
|
.mapNotNull { dir ->
|
||||||
val sourceId = sourceMap[dir.name!!.lowercase()]
|
val sourceId = sourceMap[dir.name!!.lowercase()]
|
||||||
@@ -360,36 +360,35 @@ class DownloadCache(
|
|||||||
}
|
}
|
||||||
.toMap()
|
.toMap()
|
||||||
|
|
||||||
rootDownloadsDir.sourceDirs = sourceDirs
|
updatedRootDir.sourceDirs.values.map { sourceDir ->
|
||||||
|
async {
|
||||||
|
sourceDir.mangaDirs = sourceDir.dir?.listFiles().orEmpty()
|
||||||
|
.filter { it.isDirectory && !it.name.isNullOrBlank() }
|
||||||
|
.associate { it.name!! to MangaDirectory(it) }
|
||||||
|
|
||||||
sourceDirs.values
|
sourceDir.mangaDirs.values.forEach { mangaDir ->
|
||||||
.map { sourceDir ->
|
val chapterDirs = mangaDir.dir?.listFiles().orEmpty()
|
||||||
async {
|
.mapNotNull {
|
||||||
sourceDir.mangaDirs = sourceDir.dir?.listFiles().orEmpty()
|
when {
|
||||||
.filter { it.isDirectory && !it.name.isNullOrBlank() }
|
// Ignore incomplete downloads
|
||||||
.associate { it.name!! to MangaDirectory(it) }
|
it.name?.endsWith(Downloader.TMP_DIR_SUFFIX) == true -> null
|
||||||
|
// Folder of images
|
||||||
sourceDir.mangaDirs.values.forEach { mangaDir ->
|
it.isDirectory -> it.name
|
||||||
val chapterDirs = mangaDir.dir?.listFiles().orEmpty()
|
// CBZ files
|
||||||
.mapNotNull {
|
it.isFile && it.extension == "cbz" -> it.nameWithoutExtension
|
||||||
when {
|
// Anything else is irrelevant
|
||||||
// Ignore incomplete downloads
|
else -> null
|
||||||
it.name?.endsWith(Downloader.TMP_DIR_SUFFIX) == true -> null
|
|
||||||
// Folder of images
|
|
||||||
it.isDirectory -> it.name
|
|
||||||
// CBZ files
|
|
||||||
it.isFile && it.extension == "cbz" -> it.nameWithoutExtension
|
|
||||||
// Anything else is irrelevant
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.toMutableSet()
|
}
|
||||||
|
.toMutableSet()
|
||||||
|
|
||||||
mangaDir.chapterDirs = chapterDirs
|
mangaDir.chapterDirs = chapterDirs
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
.awaitAll()
|
.awaitAll()
|
||||||
|
|
||||||
|
rootDownloadsDir = updatedRootDir
|
||||||
}
|
}
|
||||||
|
|
||||||
_isInitializing.emit(false)
|
_isInitializing.emit(false)
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package eu.kanade.tachiyomi.data.library
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.pm.ServiceInfo
|
import android.content.pm.ServiceInfo
|
||||||
|
import android.net.NetworkCapabilities
|
||||||
|
import android.net.NetworkRequest
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.work.BackoffPolicy
|
import androidx.work.BackoffPolicy
|
||||||
import androidx.work.Constraints
|
import androidx.work.Constraints
|
||||||
@@ -135,10 +137,12 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
|||||||
|
|
||||||
override suspend fun doWork(): Result {
|
override suspend fun doWork(): Result {
|
||||||
if (tags.contains(WORK_NAME_AUTO)) {
|
if (tags.contains(WORK_NAME_AUTO)) {
|
||||||
val preferences = Injekt.get<LibraryPreferences>()
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
|
||||||
val restrictions = preferences.autoUpdateDeviceRestrictions().get()
|
val preferences = Injekt.get<LibraryPreferences>()
|
||||||
if ((DEVICE_ONLY_ON_WIFI in restrictions) && !context.isConnectedToWifi()) {
|
val restrictions = preferences.autoUpdateDeviceRestrictions().get()
|
||||||
return Result.retry()
|
if ((DEVICE_ONLY_ON_WIFI in restrictions) && !context.isConnectedToWifi()) {
|
||||||
|
return Result.retry()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find a running manual worker. If exists, try again later
|
// Find a running manual worker. If exists, try again later
|
||||||
@@ -404,7 +408,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
|||||||
it.read
|
it.read
|
||||||
}
|
}
|
||||||
val newReadChapters = this.filter { chapter ->
|
val newReadChapters = this.filter { chapter ->
|
||||||
chapter.chapterNumber > 0 &&
|
chapter.chapterNumber >= 0 &&
|
||||||
readChapters.any {
|
readChapters.any {
|
||||||
it.chapterNumber == chapter.chapterNumber
|
it.chapterNumber == chapter.chapterNumber
|
||||||
}
|
}
|
||||||
@@ -768,15 +772,24 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
|||||||
val interval = prefInterval ?: preferences.autoUpdateInterval().get()
|
val interval = prefInterval ?: preferences.autoUpdateInterval().get()
|
||||||
if (interval > 0) {
|
if (interval > 0) {
|
||||||
val restrictions = preferences.autoUpdateDeviceRestrictions().get()
|
val restrictions = preferences.autoUpdateDeviceRestrictions().get()
|
||||||
val constraints = Constraints(
|
val networkType = if (DEVICE_NETWORK_NOT_METERED in restrictions) {
|
||||||
requiredNetworkType = if (DEVICE_NETWORK_NOT_METERED in restrictions) {
|
NetworkType.UNMETERED
|
||||||
NetworkType.UNMETERED
|
} else {
|
||||||
} else {
|
NetworkType.CONNECTED
|
||||||
NetworkType.CONNECTED
|
}
|
||||||
},
|
val networkRequestBuilder = NetworkRequest.Builder()
|
||||||
requiresCharging = DEVICE_CHARGING in restrictions,
|
if (DEVICE_ONLY_ON_WIFI in restrictions) {
|
||||||
requiresBatteryNotLow = true,
|
networkRequestBuilder.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
|
||||||
)
|
}
|
||||||
|
if (DEVICE_NETWORK_NOT_METERED in restrictions) {
|
||||||
|
networkRequestBuilder.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
|
||||||
|
}
|
||||||
|
val constraints = Constraints.Builder()
|
||||||
|
// 'networkRequest' only applies to Android 9+, otherwise 'networkType' is used
|
||||||
|
.setRequiredNetworkRequest(networkRequestBuilder.build(), networkType)
|
||||||
|
.setRequiresCharging(DEVICE_CHARGING in restrictions)
|
||||||
|
.setRequiresBatteryNotLow(true)
|
||||||
|
.build()
|
||||||
|
|
||||||
val request = PeriodicWorkRequestBuilder<LibraryUpdateJob>(
|
val request = PeriodicWorkRequestBuilder<LibraryUpdateJob>(
|
||||||
interval.toLong(),
|
interval.toLong(),
|
||||||
|
|||||||
@@ -30,6 +30,9 @@ object Notifications {
|
|||||||
const val ID_LIBRARY_SIZE_WARNING = -103
|
const val ID_LIBRARY_SIZE_WARNING = -103
|
||||||
const val CHANNEL_LIBRARY_ERROR = "library_errors_channel"
|
const val CHANNEL_LIBRARY_ERROR = "library_errors_channel"
|
||||||
const val ID_LIBRARY_ERROR = -102
|
const val ID_LIBRARY_ERROR = -102
|
||||||
|
const val CHANNEL_LIBRARY_EHENTAI = "library_ehentai_channel"
|
||||||
|
const val ID_EHENTAI_PROGRESS = -199
|
||||||
|
const val ID_EHENTAI_ERROR = -198
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Notification channel and ids used by the downloader.
|
* Notification channel and ids used by the downloader.
|
||||||
@@ -71,6 +74,7 @@ object Notifications {
|
|||||||
const val CHANNEL_APP_UPDATE = "app_apk_update_channel"
|
const val CHANNEL_APP_UPDATE = "app_apk_update_channel"
|
||||||
const val ID_APP_UPDATER = 1
|
const val ID_APP_UPDATER = 1
|
||||||
const val ID_APP_UPDATE_PROMPT = 2
|
const val ID_APP_UPDATE_PROMPT = 2
|
||||||
|
const val ID_APP_UPDATE_ERROR = 3
|
||||||
const val CHANNEL_EXTENSIONS_UPDATE = "ext_apk_update_channel"
|
const val CHANNEL_EXTENSIONS_UPDATE = "ext_apk_update_channel"
|
||||||
const val ID_UPDATES_TO_EXTS = -401
|
const val ID_UPDATES_TO_EXTS = -401
|
||||||
const val ID_EXTENSION_INSTALLER = -402
|
const val ID_EXTENSION_INSTALLER = -402
|
||||||
@@ -166,6 +170,13 @@ object Notifications {
|
|||||||
setGroup(GROUP_APK_UPDATES)
|
setGroup(GROUP_APK_UPDATES)
|
||||||
setName(context.stringResource(MR.strings.channel_ext_updates))
|
setName(context.stringResource(MR.strings.channel_ext_updates))
|
||||||
},
|
},
|
||||||
|
// SY -->
|
||||||
|
buildNotificationChannel(CHANNEL_LIBRARY_EHENTAI, IMPORTANCE_LOW) {
|
||||||
|
setName("EHentai")
|
||||||
|
setGroup(GROUP_LIBRARY)
|
||||||
|
setShowBadge(false)
|
||||||
|
},
|
||||||
|
// SY <--
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -175,6 +175,7 @@ sealed class Image(
|
|||||||
}
|
}
|
||||||
|
|
||||||
sealed interface Location {
|
sealed interface Location {
|
||||||
|
@ConsistentCopyVisibility
|
||||||
data class Pictures private constructor(val relativePath: String) : Location {
|
data class Pictures private constructor(val relativePath: String) : Location {
|
||||||
companion object {
|
companion object {
|
||||||
fun create(relativePath: String = ""): Pictures {
|
fun create(relativePath: String = ""): Pictures {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import androidx.work.WorkerParameters
|
|||||||
import eu.kanade.domain.sync.SyncPreferences
|
import eu.kanade.domain.sync.SyncPreferences
|
||||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
import eu.kanade.tachiyomi.util.system.cancelNotification
|
import eu.kanade.tachiyomi.util.system.cancelNotification
|
||||||
|
import eu.kanade.tachiyomi.util.system.isOnline
|
||||||
import eu.kanade.tachiyomi.util.system.isRunning
|
import eu.kanade.tachiyomi.util.system.isRunning
|
||||||
import eu.kanade.tachiyomi.util.system.setForegroundSafely
|
import eu.kanade.tachiyomi.util.system.setForegroundSafely
|
||||||
import eu.kanade.tachiyomi.util.system.workManager
|
import eu.kanade.tachiyomi.util.system.workManager
|
||||||
@@ -31,6 +32,9 @@ class SyncDataJob(private val context: Context, workerParams: WorkerParameters)
|
|||||||
|
|
||||||
override suspend fun doWork(): Result {
|
override suspend fun doWork(): Result {
|
||||||
if (tags.contains(TAG_AUTO)) {
|
if (tags.contains(TAG_AUTO)) {
|
||||||
|
if (!context.isOnline()) {
|
||||||
|
return Result.retry()
|
||||||
|
}
|
||||||
// Find a running manual worker. If exists, try again later
|
// Find a running manual worker. If exists, try again later
|
||||||
if (context.workManager.isRunning(TAG_MANUAL)) {
|
if (context.workManager.isRunning(TAG_MANUAL)) {
|
||||||
return Result.retry()
|
return Result.retry()
|
||||||
@@ -93,17 +97,18 @@ class SyncDataJob(private val context: Context, workerParams: WorkerParameters)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startNow(context: Context) {
|
fun startNow(context: Context, manual: Boolean = false) {
|
||||||
val wm = context.workManager
|
val wm = context.workManager
|
||||||
if (wm.isRunning(TAG_JOB)) {
|
if (wm.isRunning(TAG_JOB)) {
|
||||||
// Already running either as a scheduled or manual job
|
// Already running either as a scheduled or manual job
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
val tag = if (manual) TAG_MANUAL else TAG_AUTO
|
||||||
val request = OneTimeWorkRequestBuilder<SyncDataJob>()
|
val request = OneTimeWorkRequestBuilder<SyncDataJob>()
|
||||||
.addTag(TAG_JOB)
|
.addTag(TAG_JOB)
|
||||||
.addTag(TAG_MANUAL)
|
.addTag(tag)
|
||||||
.build()
|
.build()
|
||||||
context.workManager.enqueueUniqueWork(TAG_MANUAL, ExistingWorkPolicy.KEEP, request)
|
context.workManager.enqueueUniqueWork(tag, ExistingWorkPolicy.KEEP, request)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun stop(context: Context) {
|
fun stop(context: Context) {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import eu.kanade.domain.track.interactor.AddTracks
|
|||||||
import eu.kanade.domain.track.model.toDomainTrack
|
import eu.kanade.domain.track.model.toDomainTrack
|
||||||
import eu.kanade.domain.track.service.TrackPreferences
|
import eu.kanade.domain.track.service.TrackPreferences
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
|
import eu.kanade.tachiyomi.data.track.model.TrackMangaMetadata
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
@@ -120,6 +121,10 @@ abstract class BaseTracker(
|
|||||||
updateRemote(track)
|
updateRemote(track)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun getMangaMetadata(track: DomainTrack): TrackMangaMetadata? {
|
||||||
|
throw NotImplementedError("Not implemented.")
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun updateRemote(track: Track): Unit = withIOContext {
|
private suspend fun updateRemote(track: Track): Unit = withIOContext {
|
||||||
try {
|
try {
|
||||||
update(track)
|
update(track)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import androidx.annotation.ColorInt
|
|||||||
import androidx.annotation.DrawableRes
|
import androidx.annotation.DrawableRes
|
||||||
import dev.icerock.moko.resources.StringResource
|
import dev.icerock.moko.resources.StringResource
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
|
import eu.kanade.tachiyomi.data.track.model.TrackMangaMetadata
|
||||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
@@ -82,4 +83,6 @@ interface Tracker {
|
|||||||
suspend fun setRemoteStartDate(track: Track, epochMillis: Long)
|
suspend fun setRemoteStartDate(track: Track, epochMillis: Long)
|
||||||
|
|
||||||
suspend fun setRemoteFinishDate(track: Track, epochMillis: Long)
|
suspend fun setRemoteFinishDate(track: Track, epochMillis: Long)
|
||||||
|
|
||||||
|
suspend fun getMangaMetadata(track: DomainTrack): TrackMangaMetadata?
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import eu.kanade.tachiyomi.R
|
|||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
import eu.kanade.tachiyomi.data.track.BaseTracker
|
import eu.kanade.tachiyomi.data.track.BaseTracker
|
||||||
import eu.kanade.tachiyomi.data.track.DeletableTracker
|
import eu.kanade.tachiyomi.data.track.DeletableTracker
|
||||||
|
import eu.kanade.tachiyomi.data.track.anilist.dto.ALOAuth
|
||||||
|
import eu.kanade.tachiyomi.data.track.model.TrackMangaMetadata
|
||||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
@@ -129,13 +131,15 @@ class Anilist(id: Long) : BaseTracker(id, "AniList"), DeletableTracker {
|
|||||||
0.0 -> "0 ★"
|
0.0 -> "0 ★"
|
||||||
else -> "${((score + 10) / 20).toInt()} ★"
|
else -> "${((score + 10) / 20).toInt()} ★"
|
||||||
}
|
}
|
||||||
|
|
||||||
POINT_3 -> when {
|
POINT_3 -> when {
|
||||||
score == 0.0 -> "0"
|
score == 0.0 -> "0"
|
||||||
score <= 35 -> "😦"
|
score <= 35 -> "😦"
|
||||||
score <= 60 -> "😐"
|
score <= 60 -> "😐"
|
||||||
else -> "😊"
|
else -> "😊"
|
||||||
}
|
}
|
||||||
else -> track.toAnilistScore()
|
|
||||||
|
else -> track.toApiScore()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,7 +221,7 @@ class Anilist(id: Long) : BaseTracker(id, "AniList"), DeletableTracker {
|
|||||||
interceptor.setAuth(oauth)
|
interceptor.setAuth(oauth)
|
||||||
val (username, scoreType) = api.getCurrentUser()
|
val (username, scoreType) = api.getCurrentUser()
|
||||||
scorePreference.set(scoreType)
|
scorePreference.set(scoreType)
|
||||||
saveCredentials(username.toString(), oauth.access_token)
|
saveCredentials(username.toString(), oauth.accessToken)
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
logout()
|
logout()
|
||||||
}
|
}
|
||||||
@@ -229,13 +233,17 @@ class Anilist(id: Long) : BaseTracker(id, "AniList"), DeletableTracker {
|
|||||||
interceptor.setAuth(null)
|
interceptor.setAuth(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveOAuth(oAuth: OAuth?) {
|
override suspend fun getMangaMetadata(track: DomainTrack): TrackMangaMetadata? {
|
||||||
trackPreferences.trackToken(this).set(json.encodeToString(oAuth))
|
return api.getMangaMetadata(track)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadOAuth(): OAuth? {
|
fun saveOAuth(alOAuth: ALOAuth?) {
|
||||||
|
trackPreferences.trackToken(this).set(json.encodeToString(alOAuth))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadOAuth(): ALOAuth? {
|
||||||
return try {
|
return try {
|
||||||
json.decodeFromString<OAuth>(trackPreferences.trackToken(this).get())
|
json.decodeFromString<ALOAuth>(trackPreferences.trackToken(this).get())
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,24 +3,24 @@ package eu.kanade.tachiyomi.data.track.anilist
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
|
import eu.kanade.tachiyomi.data.track.anilist.dto.ALAddMangaResult
|
||||||
|
import eu.kanade.tachiyomi.data.track.anilist.dto.ALCurrentUserResult
|
||||||
|
import eu.kanade.tachiyomi.data.track.anilist.dto.ALMangaMetadata
|
||||||
|
import eu.kanade.tachiyomi.data.track.anilist.dto.ALOAuth
|
||||||
|
import eu.kanade.tachiyomi.data.track.anilist.dto.ALSearchResult
|
||||||
|
import eu.kanade.tachiyomi.data.track.anilist.dto.ALUserListMangaQueryResult
|
||||||
|
import eu.kanade.tachiyomi.data.track.model.TrackMangaMetadata
|
||||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
import eu.kanade.tachiyomi.network.POST
|
import eu.kanade.tachiyomi.network.POST
|
||||||
import eu.kanade.tachiyomi.network.awaitSuccess
|
import eu.kanade.tachiyomi.network.awaitSuccess
|
||||||
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
||||||
import eu.kanade.tachiyomi.network.jsonMime
|
import eu.kanade.tachiyomi.network.jsonMime
|
||||||
import eu.kanade.tachiyomi.network.parseAs
|
import eu.kanade.tachiyomi.network.parseAs
|
||||||
|
import eu.kanade.tachiyomi.util.lang.htmlDecode
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.JsonNull
|
import kotlinx.serialization.json.JsonNull
|
||||||
import kotlinx.serialization.json.JsonObject
|
import kotlinx.serialization.json.JsonObject
|
||||||
import kotlinx.serialization.json.buildJsonObject
|
import kotlinx.serialization.json.buildJsonObject
|
||||||
import kotlinx.serialization.json.contentOrNull
|
|
||||||
import kotlinx.serialization.json.int
|
|
||||||
import kotlinx.serialization.json.intOrNull
|
|
||||||
import kotlinx.serialization.json.jsonArray
|
|
||||||
import kotlinx.serialization.json.jsonObject
|
|
||||||
import kotlinx.serialization.json.jsonPrimitive
|
|
||||||
import kotlinx.serialization.json.long
|
|
||||||
import kotlinx.serialization.json.longOrNull
|
|
||||||
import kotlinx.serialization.json.put
|
import kotlinx.serialization.json.put
|
||||||
import kotlinx.serialization.json.putJsonObject
|
import kotlinx.serialization.json.putJsonObject
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
@@ -28,7 +28,6 @@ import okhttp3.RequestBody.Companion.toRequestBody
|
|||||||
import tachiyomi.core.common.util.lang.withIOContext
|
import tachiyomi.core.common.util.lang.withIOContext
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.time.LocalDate
|
|
||||||
import java.time.ZoneId
|
import java.time.ZoneId
|
||||||
import java.time.ZonedDateTime
|
import java.time.ZonedDateTime
|
||||||
import kotlin.time.Duration.Companion.minutes
|
import kotlin.time.Duration.Companion.minutes
|
||||||
@@ -59,7 +58,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
|||||||
putJsonObject("variables") {
|
putJsonObject("variables") {
|
||||||
put("mangaId", track.remote_id)
|
put("mangaId", track.remote_id)
|
||||||
put("progress", track.last_chapter_read.toInt())
|
put("progress", track.last_chapter_read.toInt())
|
||||||
put("status", track.toAnilistStatus())
|
put("status", track.toApiStatus())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
with(json) {
|
with(json) {
|
||||||
@@ -70,10 +69,9 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
.awaitSuccess()
|
.awaitSuccess()
|
||||||
.parseAs<JsonObject>()
|
.parseAs<ALAddMangaResult>()
|
||||||
.let {
|
.let {
|
||||||
track.library_id =
|
track.library_id = it.data.entry.id
|
||||||
it["data"]!!.jsonObject["SaveMediaListEntry"]!!.jsonObject["id"]!!.jsonPrimitive.long
|
|
||||||
track
|
track
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -103,7 +101,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
|||||||
putJsonObject("variables") {
|
putJsonObject("variables") {
|
||||||
put("listId", track.library_id)
|
put("listId", track.library_id)
|
||||||
put("progress", track.last_chapter_read.toInt())
|
put("progress", track.last_chapter_read.toInt())
|
||||||
put("status", track.toAnilistStatus())
|
put("status", track.toApiStatus())
|
||||||
put("score", track.score.toInt())
|
put("score", track.score.toInt())
|
||||||
put("startedAt", createDate(track.started_reading_date))
|
put("startedAt", createDate(track.started_reading_date))
|
||||||
put("completedAt", createDate(track.finished_reading_date))
|
put("completedAt", createDate(track.finished_reading_date))
|
||||||
@@ -135,6 +133,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
|||||||
.awaitSuccess()
|
.awaitSuccess()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun search(search: String): List<TrackSearch> {
|
suspend fun search(search: String): List<TrackSearch> {
|
||||||
return withIOContext {
|
return withIOContext {
|
||||||
val query = """
|
val query = """
|
||||||
@@ -177,14 +176,9 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
.awaitSuccess()
|
.awaitSuccess()
|
||||||
.parseAs<JsonObject>()
|
.parseAs<ALSearchResult>()
|
||||||
.let { response ->
|
.data.page.media
|
||||||
val data = response["data"]!!.jsonObject
|
.map { it.toALManga().toTrack() }
|
||||||
val page = data["Page"]!!.jsonObject
|
|
||||||
val media = page["media"]!!.jsonArray
|
|
||||||
val entries = media.map { jsonToALManga(it.jsonObject) }
|
|
||||||
entries.map { it.toTrack() }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -247,14 +241,11 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
.awaitSuccess()
|
.awaitSuccess()
|
||||||
.parseAs<JsonObject>()
|
.parseAs<ALUserListMangaQueryResult>()
|
||||||
.let { response ->
|
.data.page.mediaList
|
||||||
val data = response["data"]!!.jsonObject
|
.map { it.toALUserManga() }
|
||||||
val page = data["Page"]!!.jsonObject
|
.firstOrNull()
|
||||||
val media = page["mediaList"]!!.jsonArray
|
?.toTrack()
|
||||||
val entries = media.map { jsonToALUserManga(it.jsonObject) }
|
|
||||||
entries.firstOrNull()?.toTrack()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -263,8 +254,8 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
|||||||
return findLibManga(track, userId) ?: throw Exception("Could not find manga")
|
return findLibManga(track, userId) ?: throw Exception("Could not find manga")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createOAuth(token: String): OAuth {
|
fun createOAuth(token: String): ALOAuth {
|
||||||
return OAuth(token, "Bearer", System.currentTimeMillis() + 31536000000, 31536000000)
|
return ALOAuth(token, "Bearer", System.currentTimeMillis() + 31536000000, 31536000000)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getCurrentUser(): Pair<Int, String> {
|
suspend fun getCurrentUser(): Pair<Int, String> {
|
||||||
@@ -291,58 +282,77 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
.awaitSuccess()
|
.awaitSuccess()
|
||||||
.parseAs<JsonObject>()
|
.parseAs<ALCurrentUserResult>()
|
||||||
.let {
|
.let {
|
||||||
val data = it["data"]!!.jsonObject
|
val viewer = it.data.viewer
|
||||||
val viewer = data["Viewer"]!!.jsonObject
|
Pair(viewer.id, viewer.mediaListOptions.scoreFormat)
|
||||||
Pair(
|
|
||||||
viewer["id"]!!.jsonPrimitive.int,
|
|
||||||
viewer["mediaListOptions"]!!.jsonObject["scoreFormat"]!!.jsonPrimitive.content,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun jsonToALManga(struct: JsonObject): ALManga {
|
suspend fun getMangaMetadata(track: DomainTrack): TrackMangaMetadata {
|
||||||
return ALManga(
|
return withIOContext {
|
||||||
struct["id"]!!.jsonPrimitive.long,
|
val query = """
|
||||||
struct["title"]!!.jsonObject["userPreferred"]!!.jsonPrimitive.content,
|
|query (${'$'}mangaId: Int!) {
|
||||||
struct["coverImage"]!!.jsonObject["large"]!!.jsonPrimitive.content,
|
|Media (id: ${'$'}mangaId) {
|
||||||
struct["description"]!!.jsonPrimitive.contentOrNull,
|
|id
|
||||||
struct["format"]!!.jsonPrimitive.content.replace("_", "-"),
|
|title {
|
||||||
struct["status"]!!.jsonPrimitive.contentOrNull ?: "",
|
|userPreferred
|
||||||
parseDate(struct, "startDate"),
|
|}
|
||||||
struct["chapters"]!!.jsonPrimitive.longOrNull ?: 0,
|
|coverImage {
|
||||||
struct["averageScore"]?.jsonPrimitive?.intOrNull ?: -1,
|
|large
|
||||||
)
|
|}
|
||||||
}
|
|description
|
||||||
|
|staff {
|
||||||
private fun jsonToALUserManga(struct: JsonObject): ALUserManga {
|
|edges {
|
||||||
return ALUserManga(
|
|role
|
||||||
struct["id"]!!.jsonPrimitive.long,
|
|node {
|
||||||
struct["status"]!!.jsonPrimitive.content,
|
|name {
|
||||||
struct["scoreRaw"]!!.jsonPrimitive.int,
|
|userPreferred
|
||||||
struct["progress"]!!.jsonPrimitive.int,
|
|}
|
||||||
parseDate(struct, "startedAt"),
|
|}
|
||||||
parseDate(struct, "completedAt"),
|
|}
|
||||||
jsonToALManga(struct["media"]!!.jsonObject),
|
|}
|
||||||
)
|
|}
|
||||||
}
|
|}
|
||||||
|
|
|
||||||
private fun parseDate(struct: JsonObject, dateKey: String): Long {
|
""".trimMargin()
|
||||||
return try {
|
val payload = buildJsonObject {
|
||||||
return LocalDate
|
put("query", query)
|
||||||
.of(
|
putJsonObject("variables") {
|
||||||
struct[dateKey]!!.jsonObject["year"]!!.jsonPrimitive.int,
|
put("mangaId", track.remoteId)
|
||||||
struct[dateKey]!!.jsonObject["month"]!!.jsonPrimitive.int,
|
}
|
||||||
struct[dateKey]!!.jsonObject["day"]!!.jsonPrimitive.int,
|
}
|
||||||
|
with(json) {
|
||||||
|
authClient.newCall(
|
||||||
|
POST(
|
||||||
|
API_URL,
|
||||||
|
body = payload.toString().toRequestBody(jsonMime),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.atStartOfDay(ZoneId.systemDefault())
|
.awaitSuccess()
|
||||||
.toInstant()
|
.parseAs<ALMangaMetadata>()
|
||||||
.toEpochMilli()
|
.let {
|
||||||
} catch (_: Exception) {
|
val media = it.data.media
|
||||||
0L
|
TrackMangaMetadata(
|
||||||
|
remoteId = media.id,
|
||||||
|
title = media.title.userPreferred,
|
||||||
|
thumbnailUrl = media.coverImage.large,
|
||||||
|
description = media.description?.htmlDecode()?.ifEmpty { null },
|
||||||
|
authors = media.staff.edges
|
||||||
|
.filter { it.role == "Story" || it.role == "Story & Art" }
|
||||||
|
.map { it.node.name.userPreferred }
|
||||||
|
.joinToString(", ")
|
||||||
|
.ifEmpty { null },
|
||||||
|
artists = media.staff.edges
|
||||||
|
.filter { it.role == "Art" || it.role == "Story & Art" }
|
||||||
|
.map { it.node.name.userPreferred }
|
||||||
|
.joinToString(", ")
|
||||||
|
.ifEmpty { null },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package eu.kanade.tachiyomi.data.track.anilist
|
package eu.kanade.tachiyomi.data.track.anilist
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.BuildConfig
|
import eu.kanade.tachiyomi.BuildConfig
|
||||||
|
import eu.kanade.tachiyomi.data.track.anilist.dto.ALOAuth
|
||||||
|
import eu.kanade.tachiyomi.data.track.anilist.dto.isExpired
|
||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
@@ -13,7 +15,7 @@ class AnilistInterceptor(val anilist: Anilist, private var token: String?) : Int
|
|||||||
* Anilist returns the date without milliseconds. We fix that and make the token expire 1 minute
|
* Anilist returns the date without milliseconds. We fix that and make the token expire 1 minute
|
||||||
* before its original expiration date.
|
* before its original expiration date.
|
||||||
*/
|
*/
|
||||||
private var oauth: OAuth? = null
|
private var oauth: ALOAuth? = null
|
||||||
set(value) {
|
set(value) {
|
||||||
field = value?.copy(expires = value.expires * 1000 - 60 * 1000)
|
field = value?.copy(expires = value.expires * 1000 - 60 * 1000)
|
||||||
}
|
}
|
||||||
@@ -40,7 +42,7 @@ class AnilistInterceptor(val anilist: Anilist, private var token: String?) : Int
|
|||||||
|
|
||||||
// Add the authorization header to the original request.
|
// Add the authorization header to the original request.
|
||||||
val authRequest = originalRequest.newBuilder()
|
val authRequest = originalRequest.newBuilder()
|
||||||
.addHeader("Authorization", "Bearer ${oauth!!.access_token}")
|
.addHeader("Authorization", "Bearer ${oauth!!.accessToken}")
|
||||||
.header("User-Agent", "TachiSY v${BuildConfig.VERSION_NAME} (${BuildConfig.APPLICATION_ID})")
|
.header("User-Agent", "TachiSY v${BuildConfig.VERSION_NAME} (${BuildConfig.APPLICATION_ID})")
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
@@ -51,8 +53,8 @@ class AnilistInterceptor(val anilist: Anilist, private var token: String?) : Int
|
|||||||
* Called when the user authenticates with Anilist for the first time. Sets the refresh token
|
* Called when the user authenticates with Anilist for the first time. Sets the refresh token
|
||||||
* and the oauth object.
|
* and the oauth object.
|
||||||
*/
|
*/
|
||||||
fun setAuth(oauth: OAuth?) {
|
fun setAuth(oauth: ALOAuth?) {
|
||||||
token = oauth?.access_token
|
token = oauth?.accessToken
|
||||||
this.oauth = oauth
|
this.oauth = oauth
|
||||||
anilist.saveOAuth(oauth)
|
anilist.saveOAuth(oauth)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,126 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.track.anilist
|
|
||||||
|
|
||||||
import eu.kanade.domain.track.service.TrackPreferences
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
|
||||||
import eu.kanade.tachiyomi.data.track.TrackerManager
|
|
||||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
|
||||||
import eu.kanade.tachiyomi.util.lang.htmlDecode
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.Locale
|
|
||||||
import tachiyomi.domain.track.model.Track as DomainTrack
|
|
||||||
|
|
||||||
data class ALManga(
|
|
||||||
val remote_id: Long,
|
|
||||||
val title_user_pref: String,
|
|
||||||
val image_url_lge: String,
|
|
||||||
val description: String?,
|
|
||||||
val format: String,
|
|
||||||
val publishing_status: String,
|
|
||||||
val start_date_fuzzy: Long,
|
|
||||||
val total_chapters: Long,
|
|
||||||
val average_score: Int,
|
|
||||||
) {
|
|
||||||
|
|
||||||
fun toTrack() = TrackSearch.create(TrackerManager.ANILIST).apply {
|
|
||||||
remote_id = this@ALManga.remote_id
|
|
||||||
title = title_user_pref
|
|
||||||
total_chapters = this@ALManga.total_chapters
|
|
||||||
cover_url = image_url_lge
|
|
||||||
summary = description?.htmlDecode() ?: ""
|
|
||||||
score = average_score.toDouble()
|
|
||||||
tracking_url = AnilistApi.mangaUrl(remote_id)
|
|
||||||
publishing_status = this@ALManga.publishing_status
|
|
||||||
publishing_type = format
|
|
||||||
if (start_date_fuzzy != 0L) {
|
|
||||||
start_date = try {
|
|
||||||
val outputDf = SimpleDateFormat("yyyy-MM-dd", Locale.US)
|
|
||||||
outputDf.format(start_date_fuzzy)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data class ALUserManga(
|
|
||||||
val library_id: Long,
|
|
||||||
val list_status: String,
|
|
||||||
val score_raw: Int,
|
|
||||||
val chapters_read: Int,
|
|
||||||
val start_date_fuzzy: Long,
|
|
||||||
val completed_date_fuzzy: Long,
|
|
||||||
val manga: ALManga,
|
|
||||||
) {
|
|
||||||
|
|
||||||
fun toTrack() = Track.create(TrackerManager.ANILIST).apply {
|
|
||||||
remote_id = manga.remote_id
|
|
||||||
title = manga.title_user_pref
|
|
||||||
status = toTrackStatus()
|
|
||||||
score = score_raw.toDouble()
|
|
||||||
started_reading_date = start_date_fuzzy
|
|
||||||
finished_reading_date = completed_date_fuzzy
|
|
||||||
last_chapter_read = chapters_read.toDouble()
|
|
||||||
library_id = this@ALUserManga.library_id
|
|
||||||
total_chapters = manga.total_chapters
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun toTrackStatus() = when (list_status) {
|
|
||||||
"CURRENT" -> Anilist.READING
|
|
||||||
"COMPLETED" -> Anilist.COMPLETED
|
|
||||||
"PAUSED" -> Anilist.ON_HOLD
|
|
||||||
"DROPPED" -> Anilist.DROPPED
|
|
||||||
"PLANNING" -> Anilist.PLAN_TO_READ
|
|
||||||
"REPEATING" -> Anilist.REREADING
|
|
||||||
else -> throw NotImplementedError("Unknown status: $list_status")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class OAuth(
|
|
||||||
val access_token: String,
|
|
||||||
val token_type: String,
|
|
||||||
val expires: Long,
|
|
||||||
val expires_in: Long,
|
|
||||||
)
|
|
||||||
|
|
||||||
fun OAuth.isExpired() = System.currentTimeMillis() > expires
|
|
||||||
|
|
||||||
fun Track.toAnilistStatus() = when (status) {
|
|
||||||
Anilist.READING -> "CURRENT"
|
|
||||||
Anilist.COMPLETED -> "COMPLETED"
|
|
||||||
Anilist.ON_HOLD -> "PAUSED"
|
|
||||||
Anilist.DROPPED -> "DROPPED"
|
|
||||||
Anilist.PLAN_TO_READ -> "PLANNING"
|
|
||||||
Anilist.REREADING -> "REPEATING"
|
|
||||||
else -> throw NotImplementedError("Unknown status: $status")
|
|
||||||
}
|
|
||||||
|
|
||||||
private val preferences: TrackPreferences by injectLazy()
|
|
||||||
|
|
||||||
fun DomainTrack.toAnilistScore(): String = when (preferences.anilistScoreType().get()) {
|
|
||||||
// 10 point
|
|
||||||
"POINT_10" -> (score.toInt() / 10).toString()
|
|
||||||
// 100 point
|
|
||||||
"POINT_100" -> score.toInt().toString()
|
|
||||||
// 5 stars
|
|
||||||
"POINT_5" -> when {
|
|
||||||
score == 0.0 -> "0"
|
|
||||||
score < 30 -> "1"
|
|
||||||
score < 50 -> "2"
|
|
||||||
score < 70 -> "3"
|
|
||||||
score < 90 -> "4"
|
|
||||||
else -> "5"
|
|
||||||
}
|
|
||||||
// Smiley
|
|
||||||
"POINT_3" -> when {
|
|
||||||
score == 0.0 -> "0"
|
|
||||||
score <= 35 -> ":("
|
|
||||||
score <= 60 -> ":|"
|
|
||||||
else -> ":)"
|
|
||||||
}
|
|
||||||
// 10 point decimal
|
|
||||||
"POINT_10_DECIMAL" -> (score / 10).toString()
|
|
||||||
else -> throw NotImplementedError("Unknown score type")
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.track.anilist
|
||||||
|
|
||||||
|
import eu.kanade.domain.track.service.TrackPreferences
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import tachiyomi.domain.track.model.Track as DomainTrack
|
||||||
|
|
||||||
|
fun Track.toApiStatus() = when (status) {
|
||||||
|
Anilist.READING -> "CURRENT"
|
||||||
|
Anilist.COMPLETED -> "COMPLETED"
|
||||||
|
Anilist.ON_HOLD -> "PAUSED"
|
||||||
|
Anilist.DROPPED -> "DROPPED"
|
||||||
|
Anilist.PLAN_TO_READ -> "PLANNING"
|
||||||
|
Anilist.REREADING -> "REPEATING"
|
||||||
|
else -> throw NotImplementedError("Unknown status: $status")
|
||||||
|
}
|
||||||
|
|
||||||
|
private val preferences: TrackPreferences by injectLazy()
|
||||||
|
|
||||||
|
fun DomainTrack.toApiScore(): String = when (preferences.anilistScoreType().get()) {
|
||||||
|
// 10 point
|
||||||
|
"POINT_10" -> (score.toInt() / 10).toString()
|
||||||
|
// 100 point
|
||||||
|
"POINT_100" -> score.toInt().toString()
|
||||||
|
// 5 stars
|
||||||
|
"POINT_5" -> when {
|
||||||
|
score == 0.0 -> "0"
|
||||||
|
score < 30 -> "1"
|
||||||
|
score < 50 -> "2"
|
||||||
|
score < 70 -> "3"
|
||||||
|
score < 90 -> "4"
|
||||||
|
else -> "5"
|
||||||
|
}
|
||||||
|
// Smiley
|
||||||
|
"POINT_3" -> when {
|
||||||
|
score == 0.0 -> "0"
|
||||||
|
score <= 35 -> ":("
|
||||||
|
score <= 60 -> ":|"
|
||||||
|
else -> ":)"
|
||||||
|
}
|
||||||
|
// 10 point decimal
|
||||||
|
"POINT_10_DECIMAL" -> (score / 10).toString()
|
||||||
|
else -> throw NotImplementedError("Unknown score type")
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.track.anilist.dto
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ALAddMangaResult(
|
||||||
|
val data: ALAddMangaData,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ALAddMangaData(
|
||||||
|
@SerialName("SaveMediaListEntry")
|
||||||
|
val entry: ALAddMangaEntry,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ALAddMangaEntry(
|
||||||
|
val id: Long,
|
||||||
|
)
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.track.anilist.dto
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import java.time.LocalDate
|
||||||
|
import java.time.ZoneId
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ALFuzzyDate(
|
||||||
|
val year: Int?,
|
||||||
|
val month: Int?,
|
||||||
|
val day: Int?,
|
||||||
|
) {
|
||||||
|
fun toEpochMilli(): Long = try {
|
||||||
|
LocalDate.of(year!!, month!!, day!!)
|
||||||
|
.atStartOfDay(ZoneId.systemDefault())
|
||||||
|
.toInstant()
|
||||||
|
.toEpochMilli()
|
||||||
|
} catch (_: Exception) {
|
||||||
|
0L
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.track.anilist.dto
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
|
import eu.kanade.tachiyomi.data.track.TrackerManager
|
||||||
|
import eu.kanade.tachiyomi.data.track.anilist.Anilist
|
||||||
|
import eu.kanade.tachiyomi.data.track.anilist.AnilistApi
|
||||||
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
|
import eu.kanade.tachiyomi.util.lang.htmlDecode
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
data class ALManga(
|
||||||
|
val remoteId: Long,
|
||||||
|
val title: String,
|
||||||
|
val imageUrl: String,
|
||||||
|
val description: String?,
|
||||||
|
val format: String,
|
||||||
|
val publishingStatus: String,
|
||||||
|
val startDateFuzzy: Long,
|
||||||
|
val totalChapters: Long,
|
||||||
|
val averageScore: Int,
|
||||||
|
) {
|
||||||
|
fun toTrack() = TrackSearch.create(TrackerManager.ANILIST).apply {
|
||||||
|
remote_id = remoteId
|
||||||
|
title = this@ALManga.title
|
||||||
|
total_chapters = totalChapters
|
||||||
|
cover_url = imageUrl
|
||||||
|
summary = description?.htmlDecode() ?: ""
|
||||||
|
score = averageScore.toDouble()
|
||||||
|
tracking_url = AnilistApi.mangaUrl(remote_id)
|
||||||
|
publishing_status = publishingStatus
|
||||||
|
publishing_type = format
|
||||||
|
if (startDateFuzzy != 0L) {
|
||||||
|
start_date = try {
|
||||||
|
val outputDf = SimpleDateFormat("yyyy-MM-dd", Locale.US)
|
||||||
|
outputDf.format(startDateFuzzy)
|
||||||
|
} catch (e: IllegalArgumentException) {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class ALUserManga(
|
||||||
|
val libraryId: Long,
|
||||||
|
val listStatus: String,
|
||||||
|
val scoreRaw: Int,
|
||||||
|
val chaptersRead: Int,
|
||||||
|
val startDateFuzzy: Long,
|
||||||
|
val completedDateFuzzy: Long,
|
||||||
|
val manga: ALManga,
|
||||||
|
) {
|
||||||
|
fun toTrack() = Track.create(TrackerManager.ANILIST).apply {
|
||||||
|
remote_id = manga.remoteId
|
||||||
|
title = manga.title
|
||||||
|
status = toTrackStatus()
|
||||||
|
score = scoreRaw.toDouble()
|
||||||
|
started_reading_date = startDateFuzzy
|
||||||
|
finished_reading_date = completedDateFuzzy
|
||||||
|
last_chapter_read = chaptersRead.toDouble()
|
||||||
|
library_id = libraryId
|
||||||
|
total_chapters = manga.totalChapters
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun toTrackStatus() = when (listStatus) {
|
||||||
|
"CURRENT" -> Anilist.READING
|
||||||
|
"COMPLETED" -> Anilist.COMPLETED
|
||||||
|
"PAUSED" -> Anilist.ON_HOLD
|
||||||
|
"DROPPED" -> Anilist.DROPPED
|
||||||
|
"PLANNING" -> Anilist.PLAN_TO_READ
|
||||||
|
"REPEATING" -> Anilist.REREADING
|
||||||
|
else -> throw NotImplementedError("Unknown status: $listStatus")
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user