Compare commits
123 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 31f967235f | |||
| 7d6e746257 | |||
| d306139047 | |||
| 7e6811692e | |||
| 8df0446020 | |||
| 8ccf8fc74d | |||
| 4deaa41c53 | |||
| c843789f66 | |||
| 78da81fa42 | |||
| 4fc96f263d | |||
| 6a12b54ecb | |||
| 8ffcd5efec | |||
| 26121efeb4 | |||
| 1ecf3a567b | |||
| 20fff5798d | |||
| 0ba580ba30 | |||
| aaf28ee4f1 | |||
| 8558c110a9 | |||
| 3865583c28 | |||
| b2cc61f6fd | |||
| 47dd58de2a | |||
| 67d42c9c2b | |||
| b97f322d6f | |||
| d8cc4f8b45 | |||
| f78752fbdf | |||
| d968d58cd6 | |||
| 54d5f9baaf | |||
| c1bf53e28a | |||
| 517fd3a8f4 | |||
| 089d1aba57 | |||
| 46bf139f01 | |||
| c3fb5c0bec | |||
| 000a4ffc3f | |||
| 7b0b879d65 | |||
| 8a622f6c7d | |||
| 253060a3bc | |||
| b6b33e8c00 | |||
| 2e4f811090 | |||
| 215a1908f7 | |||
| 082acf000c | |||
| 2d1240b274 | |||
| 1e98709cc3 | |||
| 5550ddad4e | |||
| f9148c0c5e | |||
| 089e6268e7 | |||
| 712cd1493f | |||
| bbc8adc3e8 | |||
| 077b673c0a | |||
| 49eacf5178 | |||
| 98d1dddf4a | |||
| 37a616f3db | |||
| ad18696a1a | |||
| 34bb012a1c | |||
| 08c4989aa3 | |||
| 14dae420f5 | |||
| 65ed3c5ae6 | |||
| 5ae3508665 | |||
| e32eb0e009 | |||
| e0812ab5c8 | |||
| df9f79c120 | |||
| 990eb33b98 | |||
| e1bab1172a | |||
| aeeff72bed | |||
| 5895e78b39 | |||
| b24719a3e9 | |||
| d551619d9d | |||
| 06ad6c2e16 | |||
| df7e470e08 | |||
| 03f32ebffd | |||
| ed20d25452 | |||
| 596a8d002f | |||
| 206d824ed2 | |||
| 97ed4e55ad | |||
| 739f7bc848 | |||
| e866e60b19 | |||
| f135daeca5 | |||
| d80c19eb03 | |||
| 2d47147172 | |||
| de3570107e | |||
| 5480495619 | |||
| 694ef5f285 | |||
| 472c97c580 | |||
| 8b098b38f8 | |||
| 5e0585d724 | |||
| 3e438a9e87 | |||
| 8046c1a540 | |||
| 1f3f6cd4df | |||
| a62a5ed650 | |||
| a320903bc0 | |||
| a6c4f01c74 | |||
| a657c65261 | |||
| 5455daf96b | |||
| d40bc2b41b | |||
| 527ca85c39 | |||
| 189714eaf1 | |||
| 90d5104bdc | |||
| ceff887a10 | |||
| 2197bd0451 | |||
| 861a810961 | |||
| 81984c25df | |||
| b21d685a37 | |||
| fb4d9209f8 | |||
| 9ee0034c9a | |||
| 268b483182 | |||
| 2af6e7be32 | |||
| 3ecf86ae35 | |||
| 41bb0e08ba | |||
| 0bb1eb2da1 | |||
| 919df9a7cf | |||
| 2ea488bff5 | |||
| ec30ccccc2 | |||
| 780bdcbe55 | |||
| a90bc4c7fa | |||
| 5e421c6f0e | |||
| 742fdc19ca | |||
| 74505565ef | |||
| f041ed5b2a | |||
| 8762b20ab6 | |||
| 5a71889679 | |||
| a4983eb004 | |||
| 818bc7f75a | |||
| 7de6fa8c23 | |||
| edca9039e5 |
@@ -40,6 +40,12 @@ jobs:
|
|||||||
"ignoreCase": true,
|
"ignoreCase": true,
|
||||||
"labels": ["Cloudflare protected"],
|
"labels": ["Cloudflare protected"],
|
||||||
"message": "Refer to the **Solving Cloudflare issues** section at https://mihon.app/docs/guides/troubleshooting/#cloudflare. If it doesn't work, migrate to other sources or wait until they lower their protection."
|
"message": "Refer to the **Solving Cloudflare issues** section at https://mihon.app/docs/guides/troubleshooting/#cloudflare. If it doesn't work, migrate to other sources or wait until they lower their protection."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "both",
|
||||||
|
"regex": "^.*(myanimelist|mal).*$",
|
||||||
|
"ignoreCase": true,
|
||||||
|
"message": "For issues with linking MyAnimeList, please follow these steps:\n1. Update Mihon to version 0.16.4 or newer\n2. Change your default User-Agent (`More → Settings → Advanced → Default user agent string`)\n3. Close and restart Mihon\n4. Attempt to link MyAnimeList again\n\nIf you had MyAnimeList linked before, try to unlink it first before trying these steps."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
auto-close-ignore-label: do-not-autoclose
|
auto-close-ignore-label: do-not-autoclose
|
||||||
|
|||||||
@@ -88,9 +88,7 @@ Please make sure to read the full guidelines. Your issue may be closed without w
|
|||||||
* If it could be device-dependent, try reproducing on another device (if possible)
|
* If it could be device-dependent, try reproducing on another device (if possible)
|
||||||
* Don't group unrelated requests into one issue
|
* Don't group unrelated requests into one issue
|
||||||
|
|
||||||
DO: https://github.com/tachiyomiorg/tachiyomi/issues/24 https://github.com/tachiyomiorg/tachiyomi/issues/71
|
Use the [issue forms](https://github.com/jobobby04/TachiyomiSY/issues/new/choose) to submit a bug.
|
||||||
|
|
||||||
DON'T: https://github.com/tachiyomiorg/tachiyomi/issues/75
|
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
|||||||
+11
-32
@@ -1,9 +1,12 @@
|
|||||||
|
import mihon.buildlogic.getBuildTime
|
||||||
|
import mihon.buildlogic.getCommitCount
|
||||||
|
import mihon.buildlogic.getGitSha
|
||||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("com.android.application")
|
id("mihon.android.application")
|
||||||
|
id("mihon.android.application.compose")
|
||||||
id("com.mikepenz.aboutlibraries.plugin")
|
id("com.mikepenz.aboutlibraries.plugin")
|
||||||
kotlin("android")
|
|
||||||
kotlin("plugin.parcelize")
|
kotlin("plugin.parcelize")
|
||||||
kotlin("plugin.serialization")
|
kotlin("plugin.serialization")
|
||||||
// id("com.github.zellius.shortcut-helper")
|
// id("com.github.zellius.shortcut-helper")
|
||||||
@@ -26,7 +29,7 @@ android {
|
|||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "eu.kanade.tachiyomi.sy"
|
applicationId = "eu.kanade.tachiyomi.sy"
|
||||||
|
|
||||||
versionCode = 66
|
versionCode = 68
|
||||||
versionName = "1.10.5"
|
versionName = "1.10.5"
|
||||||
|
|
||||||
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
||||||
@@ -120,7 +123,6 @@ android {
|
|||||||
|
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
viewBinding = true
|
viewBinding = true
|
||||||
compose = true
|
|
||||||
buildConfig = true
|
buildConfig = true
|
||||||
|
|
||||||
// Disable some unused things
|
// Disable some unused things
|
||||||
@@ -133,10 +135,6 @@ android {
|
|||||||
abortOnError = false
|
abortOnError = false
|
||||||
checkReleaseBuilds = false
|
checkReleaseBuilds = false
|
||||||
}
|
}
|
||||||
|
|
||||||
composeOptions {
|
|
||||||
kotlinCompilerExtensionVersion = compose.versions.compiler.get()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
@@ -154,18 +152,15 @@ dependencies {
|
|||||||
implementation(projects.presentationWidget)
|
implementation(projects.presentationWidget)
|
||||||
|
|
||||||
// Compose
|
// Compose
|
||||||
implementation(platform(compose.bom))
|
|
||||||
implementation(compose.activity)
|
implementation(compose.activity)
|
||||||
implementation(compose.foundation)
|
implementation(compose.foundation)
|
||||||
implementation(compose.material3.core)
|
implementation(compose.material3.core)
|
||||||
implementation(compose.material.core)
|
|
||||||
implementation(compose.material.icons)
|
implementation(compose.material.icons)
|
||||||
implementation(compose.animation)
|
implementation(compose.animation)
|
||||||
implementation(compose.animation.graphics)
|
implementation(compose.animation.graphics)
|
||||||
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.webview)
|
|
||||||
implementation(compose.accompanist.systemuicontroller)
|
implementation(compose.accompanist.systemuicontroller)
|
||||||
|
|
||||||
implementation(androidx.paging.runtime)
|
implementation(androidx.paging.runtime)
|
||||||
@@ -247,6 +242,9 @@ dependencies {
|
|||||||
implementation(libs.bundles.voyager)
|
implementation(libs.bundles.voyager)
|
||||||
implementation(libs.compose.materialmotion)
|
implementation(libs.compose.materialmotion)
|
||||||
implementation(libs.swipe)
|
implementation(libs.swipe)
|
||||||
|
implementation(libs.compose.webview)
|
||||||
|
implementation(libs.compose.grid)
|
||||||
|
|
||||||
|
|
||||||
implementation(libs.google.api.services.drive)
|
implementation(libs.google.api.services.drive)
|
||||||
implementation(libs.google.api.client.oauth)
|
implementation(libs.google.api.client.oauth)
|
||||||
@@ -255,7 +253,6 @@ dependencies {
|
|||||||
implementation(libs.logcat)
|
implementation(libs.logcat)
|
||||||
|
|
||||||
// Crash reports/analytics
|
// Crash reports/analytics
|
||||||
// implementation(libs.bundles.acra)
|
|
||||||
// "standardImplementation"(libs.firebase.analytics)
|
// "standardImplementation"(libs.firebase.analytics)
|
||||||
|
|
||||||
// Shizuku
|
// Shizuku
|
||||||
@@ -268,6 +265,8 @@ dependencies {
|
|||||||
// debugImplementation(libs.leakcanary.android)
|
// debugImplementation(libs.leakcanary.android)
|
||||||
implementation(libs.leakcanary.plumber)
|
implementation(libs.leakcanary.plumber)
|
||||||
|
|
||||||
|
testImplementation(kotlinx.coroutines.test)
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
// Text distance (EH)
|
// Text distance (EH)
|
||||||
implementation(sylibs.simularity)
|
implementation(sylibs.simularity)
|
||||||
@@ -314,31 +313,11 @@ tasks {
|
|||||||
"-opt-in=androidx.compose.animation.ExperimentalAnimationApi",
|
"-opt-in=androidx.compose.animation.ExperimentalAnimationApi",
|
||||||
"-opt-in=androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi",
|
"-opt-in=androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi",
|
||||||
"-opt-in=coil3.annotation.ExperimentalCoilApi",
|
"-opt-in=coil3.annotation.ExperimentalCoilApi",
|
||||||
"-opt-in=com.google.accompanist.permissions.ExperimentalPermissionsApi",
|
|
||||||
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
||||||
"-opt-in=kotlinx.coroutines.FlowPreview",
|
"-opt-in=kotlinx.coroutines.FlowPreview",
|
||||||
"-opt-in=kotlinx.coroutines.InternalCoroutinesApi",
|
"-opt-in=kotlinx.coroutines.InternalCoroutinesApi",
|
||||||
"-opt-in=kotlinx.serialization.ExperimentalSerializationApi",
|
"-opt-in=kotlinx.serialization.ExperimentalSerializationApi",
|
||||||
)
|
)
|
||||||
|
|
||||||
if (project.findProperty("tachiyomi.enableComposeCompilerMetrics") == "true") {
|
|
||||||
kotlinOptions.freeCompilerArgs += listOf(
|
|
||||||
"-P",
|
|
||||||
"plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=" +
|
|
||||||
project.layout.buildDirectory.dir("compose_metrics").get().asFile.absolutePath,
|
|
||||||
)
|
|
||||||
kotlinOptions.freeCompilerArgs += listOf(
|
|
||||||
"-P",
|
|
||||||
"plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=" +
|
|
||||||
project.layout.buildDirectory.dir("compose_metrics").get().asFile.absolutePath,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://developer.android.com/jetpack/androidx/releases/compose-compiler#1.5.9
|
|
||||||
kotlinOptions.freeCompilerArgs += listOf(
|
|
||||||
"-P",
|
|
||||||
"plugin:androidx.compose.compiler.plugins.kotlin:nonSkippingGroupOptimization=true",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Vendored
+5
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
-keep,allowoptimization class eu.kanade.**
|
-keep,allowoptimization class eu.kanade.**
|
||||||
-keep,allowoptimization class tachiyomi.**
|
-keep,allowoptimization class tachiyomi.**
|
||||||
|
-keep,allowoptimization class mihon.**
|
||||||
|
|
||||||
# Keep common dependencies used in extensions
|
# Keep common dependencies used in extensions
|
||||||
-keep,allowoptimization class androidx.preference.** { public protected *; }
|
-keep,allowoptimization class androidx.preference.** { public protected *; }
|
||||||
@@ -46,6 +47,10 @@
|
|||||||
-dontnote rx.internal.util.PlatformDependent
|
-dontnote rx.internal.util.PlatformDependent
|
||||||
##---------------End: proguard configuration for RxJava 1.x ----------
|
##---------------End: proguard configuration for RxJava 1.x ----------
|
||||||
|
|
||||||
|
##---------------Begin: proguard configuration for okhttp ----------
|
||||||
|
-keepclasseswithmembers class okhttp3.MultipartBody$Builder { *; }
|
||||||
|
##---------------End: proguard configuration for okhttp ----------
|
||||||
|
|
||||||
##---------------Begin: proguard configuration for kotlinx.serialization ----------
|
##---------------Begin: proguard configuration for kotlinx.serialization ----------
|
||||||
-keepattributes *Annotation*, InnerClasses
|
-keepattributes *Annotation*, InnerClasses
|
||||||
-dontnote kotlinx.serialization.** # core serialization annotations
|
-dontnote kotlinx.serialization.** # core serialization annotations
|
||||||
|
|||||||
@@ -4,10 +4,7 @@ import eu.kanade.domain.chapter.interactor.GetAvailableScanlators
|
|||||||
import eu.kanade.domain.chapter.interactor.SetReadStatus
|
import eu.kanade.domain.chapter.interactor.SetReadStatus
|
||||||
import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource
|
import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource
|
||||||
import eu.kanade.domain.download.interactor.DeleteDownload
|
import eu.kanade.domain.download.interactor.DeleteDownload
|
||||||
import eu.kanade.domain.extension.interactor.CreateExtensionRepo
|
|
||||||
import eu.kanade.domain.extension.interactor.DeleteExtensionRepo
|
|
||||||
import eu.kanade.domain.extension.interactor.GetExtensionLanguages
|
import eu.kanade.domain.extension.interactor.GetExtensionLanguages
|
||||||
import eu.kanade.domain.extension.interactor.GetExtensionRepos
|
|
||||||
import eu.kanade.domain.extension.interactor.GetExtensionSources
|
import eu.kanade.domain.extension.interactor.GetExtensionSources
|
||||||
import eu.kanade.domain.extension.interactor.GetExtensionsByType
|
import eu.kanade.domain.extension.interactor.GetExtensionsByType
|
||||||
import eu.kanade.domain.extension.interactor.TrustExtension
|
import eu.kanade.domain.extension.interactor.TrustExtension
|
||||||
@@ -26,6 +23,16 @@ 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 mihon.data.repository.ExtensionRepoRepositoryImpl
|
||||||
|
import mihon.domain.extensionrepo.interactor.CreateExtensionRepo
|
||||||
|
import mihon.domain.extensionrepo.interactor.DeleteExtensionRepo
|
||||||
|
import mihon.domain.extensionrepo.interactor.GetExtensionRepo
|
||||||
|
import mihon.domain.extensionrepo.interactor.GetExtensionRepoCount
|
||||||
|
import mihon.domain.extensionrepo.interactor.ReplaceExtensionRepo
|
||||||
|
import mihon.domain.extensionrepo.interactor.UpdateExtensionRepo
|
||||||
|
import mihon.domain.extensionrepo.repository.ExtensionRepoRepository
|
||||||
|
import mihon.domain.extensionrepo.service.ExtensionRepoService
|
||||||
|
import mihon.domain.upcoming.interactor.GetUpcomingManga
|
||||||
import tachiyomi.data.category.CategoryRepositoryImpl
|
import tachiyomi.data.category.CategoryRepositoryImpl
|
||||||
import tachiyomi.data.chapter.ChapterRepositoryImpl
|
import tachiyomi.data.chapter.ChapterRepositoryImpl
|
||||||
import tachiyomi.data.history.HistoryRepositoryImpl
|
import tachiyomi.data.history.HistoryRepositoryImpl
|
||||||
@@ -111,6 +118,7 @@ class DomainModule : InjektModule {
|
|||||||
addFactory { GetMangaByUrlAndSourceId(get()) }
|
addFactory { GetMangaByUrlAndSourceId(get()) }
|
||||||
addFactory { GetManga(get()) }
|
addFactory { GetManga(get()) }
|
||||||
addFactory { GetNextChapters(get(), get(), get(), get()) }
|
addFactory { GetNextChapters(get(), get(), get(), get()) }
|
||||||
|
addFactory { GetUpcomingManga(get()) }
|
||||||
addFactory { ResetViewerFlags(get()) }
|
addFactory { ResetViewerFlags(get()) }
|
||||||
addFactory { SetMangaChapterFlags(get()) }
|
addFactory { SetMangaChapterFlags(get()) }
|
||||||
addFactory { FetchInterval(get()) }
|
addFactory { FetchInterval(get()) }
|
||||||
@@ -171,10 +179,15 @@ class DomainModule : InjektModule {
|
|||||||
addFactory { ToggleLanguage(get()) }
|
addFactory { ToggleLanguage(get()) }
|
||||||
addFactory { ToggleSource(get()) }
|
addFactory { ToggleSource(get()) }
|
||||||
addFactory { ToggleSourcePin(get()) }
|
addFactory { ToggleSourcePin(get()) }
|
||||||
addFactory { TrustExtension(get()) }
|
addFactory { TrustExtension(get(), get()) }
|
||||||
|
|
||||||
addFactory { CreateExtensionRepo(get()) }
|
addSingletonFactory<ExtensionRepoRepository> { ExtensionRepoRepositoryImpl(get()) }
|
||||||
|
addFactory { ExtensionRepoService(get(), get()) }
|
||||||
|
addFactory { GetExtensionRepo(get()) }
|
||||||
|
addFactory { GetExtensionRepoCount(get()) }
|
||||||
|
addFactory { CreateExtensionRepo(get(), get()) }
|
||||||
addFactory { DeleteExtensionRepo(get()) }
|
addFactory { DeleteExtensionRepo(get()) }
|
||||||
addFactory { GetExtensionRepos(get()) }
|
addFactory { ReplaceExtensionRepo(get()) }
|
||||||
|
addFactory { UpdateExtensionRepo(get(), get()) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ 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.isPreviewBuildType
|
|
||||||
import eu.kanade.tachiyomi.util.system.isReleaseBuildType
|
|
||||||
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
|
||||||
@@ -22,8 +20,6 @@ class BasePreferences(
|
|||||||
|
|
||||||
fun extensionInstaller() = ExtensionInstallerPreference(context, preferenceStore)
|
fun extensionInstaller() = ExtensionInstallerPreference(context, preferenceStore)
|
||||||
|
|
||||||
fun acraEnabled() = preferenceStore.getBoolean("acra.enable", isPreviewBuildType || isReleaseBuildType)
|
|
||||||
|
|
||||||
fun shownOnboardingFlow() = preferenceStore.getBoolean(Preference.appStateKey("onboarding_complete"), false)
|
fun shownOnboardingFlow() = preferenceStore.getBoolean(Preference.appStateKey("onboarding_complete"), false)
|
||||||
|
|
||||||
enum class ExtensionInstaller(val titleRes: StringResource, val requiresSystemPermission: Boolean) {
|
enum class ExtensionInstaller(val titleRes: StringResource, val requiresSystemPermission: Boolean) {
|
||||||
@@ -32,4 +28,6 @@ class BasePreferences(
|
|||||||
SHIZUKU(MR.strings.ext_installer_shizuku, false),
|
SHIZUKU(MR.strings.ext_installer_shizuku, false),
|
||||||
PRIVATE(MR.strings.ext_installer_private, false),
|
PRIVATE(MR.strings.ext_installer_private, false),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun displayProfile() = preferenceStore.getString("pref_display_profile_key", "")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
package eu.kanade.domain.extension.interactor
|
|
||||||
|
|
||||||
import eu.kanade.domain.source.service.SourcePreferences
|
|
||||||
import tachiyomi.core.common.preference.plusAssign
|
|
||||||
|
|
||||||
class CreateExtensionRepo(private val preferences: SourcePreferences) {
|
|
||||||
|
|
||||||
fun await(name: String): Result {
|
|
||||||
// Do not allow invalid formats
|
|
||||||
if (!name.matches(repoRegex)) {
|
|
||||||
return Result.InvalidUrl
|
|
||||||
}
|
|
||||||
|
|
||||||
preferences.extensionRepos() += name.removeSuffix("/index.min.json")
|
|
||||||
|
|
||||||
return Result.Success
|
|
||||||
}
|
|
||||||
|
|
||||||
sealed interface Result {
|
|
||||||
data object InvalidUrl : Result
|
|
||||||
data object Success : Result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val repoRegex = """^https://.*/index\.min\.json$""".toRegex()
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
package eu.kanade.domain.extension.interactor
|
|
||||||
|
|
||||||
import eu.kanade.domain.source.service.SourcePreferences
|
|
||||||
import tachiyomi.core.common.preference.minusAssign
|
|
||||||
|
|
||||||
class DeleteExtensionRepo(private val preferences: SourcePreferences) {
|
|
||||||
|
|
||||||
fun await(repo: String) {
|
|
||||||
preferences.extensionRepos() -= repo
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
package eu.kanade.domain.extension.interactor
|
|
||||||
|
|
||||||
import eu.kanade.domain.source.service.SourcePreferences
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
|
|
||||||
class GetExtensionRepos(private val preferences: SourcePreferences) {
|
|
||||||
|
|
||||||
fun subscribe(): Flow<Set<String>> {
|
|
||||||
return preferences.extensionRepos().changes()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -20,7 +20,7 @@ class GetExtensionsByType(
|
|||||||
extensionManager.installedExtensionsFlow,
|
extensionManager.installedExtensionsFlow,
|
||||||
extensionManager.untrustedExtensionsFlow,
|
extensionManager.untrustedExtensionsFlow,
|
||||||
extensionManager.availableExtensionsFlow,
|
extensionManager.availableExtensionsFlow,
|
||||||
) { _activeLanguages, _installed, _untrusted, _available ->
|
) { enabledLanguages, _installed, _untrusted, _available ->
|
||||||
val (updates, installed) = _installed
|
val (updates, installed) = _installed
|
||||||
.filter { (showNsfwSources || !it.isNsfw) }
|
.filter { (showNsfwSources || !it.isNsfw) }
|
||||||
.sortedWith(
|
.sortedWith(
|
||||||
@@ -41,9 +41,9 @@ class GetExtensionsByType(
|
|||||||
}
|
}
|
||||||
.flatMap { ext ->
|
.flatMap { ext ->
|
||||||
if (ext.sources.isEmpty()) {
|
if (ext.sources.isEmpty()) {
|
||||||
return@flatMap if (ext.lang in _activeLanguages) listOf(ext) else emptyList()
|
return@flatMap if (ext.lang in enabledLanguages) listOf(ext) else emptyList()
|
||||||
}
|
}
|
||||||
ext.sources.filter { it.lang in _activeLanguages }
|
ext.sources.filter { it.lang in enabledLanguages }
|
||||||
.map {
|
.map {
|
||||||
ext.copy(
|
ext.copy(
|
||||||
name = it.name,
|
name = it.name,
|
||||||
|
|||||||
@@ -3,15 +3,18 @@ package eu.kanade.domain.extension.interactor
|
|||||||
import android.content.pm.PackageInfo
|
import android.content.pm.PackageInfo
|
||||||
import androidx.core.content.pm.PackageInfoCompat
|
import androidx.core.content.pm.PackageInfoCompat
|
||||||
import eu.kanade.domain.source.service.SourcePreferences
|
import eu.kanade.domain.source.service.SourcePreferences
|
||||||
|
import mihon.domain.extensionrepo.repository.ExtensionRepoRepository
|
||||||
import tachiyomi.core.common.preference.getAndSet
|
import tachiyomi.core.common.preference.getAndSet
|
||||||
|
|
||||||
class TrustExtension(
|
class TrustExtension(
|
||||||
|
private val extensionRepoRepository: ExtensionRepoRepository,
|
||||||
private val preferences: SourcePreferences,
|
private val preferences: SourcePreferences,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun isTrusted(pkgInfo: PackageInfo, signatureHash: String): Boolean {
|
suspend fun isTrusted(pkgInfo: PackageInfo, fingerprints: List<String>): Boolean {
|
||||||
val key = "${pkgInfo.packageName}:${PackageInfoCompat.getLongVersionCode(pkgInfo)}:$signatureHash"
|
val trustedFingerprints = extensionRepoRepository.getAll().map { it.signingKeyFingerprint }.toHashSet()
|
||||||
return key in preferences.trustedExtensions().get()
|
val key = "${pkgInfo.packageName}:${PackageInfoCompat.getLongVersionCode(pkgInfo)}:${fingerprints.last()}"
|
||||||
|
return trustedFingerprints.any { fingerprints.contains(it) } || key in preferences.trustedExtensions().get()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun trust(pkgName: String, versionCode: Long, signatureHash: String) {
|
fun trust(pkgName: String, versionCode: Long, signatureHash: String) {
|
||||||
@@ -19,9 +22,7 @@ class TrustExtension(
|
|||||||
// Remove previously trusted versions
|
// Remove previously trusted versions
|
||||||
val removed = exts.filterNot { it.startsWith("$pkgName:") }.toMutableSet()
|
val removed = exts.filterNot { it.startsWith("$pkgName:") }.toMutableSet()
|
||||||
|
|
||||||
removed.also {
|
removed.also { it += "$pkgName:$versionCode:$signatureHash" }
|
||||||
it += "$pkgName:$versionCode:$signatureHash"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ class SyncPreferences(
|
|||||||
fun clientAPIKey() = preferenceStore.getString("sync_client_api_key", "")
|
fun clientAPIKey() = preferenceStore.getString("sync_client_api_key", "")
|
||||||
fun lastSyncTimestamp() = preferenceStore.getLong(Preference.appStateKey("last_sync_timestamp"), 0L)
|
fun lastSyncTimestamp() = preferenceStore.getLong(Preference.appStateKey("last_sync_timestamp"), 0L)
|
||||||
|
|
||||||
|
fun lastSyncEtag() = preferenceStore.getString("sync_etag", "")
|
||||||
|
|
||||||
fun syncInterval() = preferenceStore.getInt("sync_interval", 0)
|
fun syncInterval() = preferenceStore.getInt("sync_interval", 0)
|
||||||
fun syncService() = preferenceStore.getInt("sync_service", 0)
|
fun syncService() = preferenceStore.getInt("sync_service", 0)
|
||||||
|
|
||||||
@@ -53,6 +55,11 @@ class SyncPreferences(
|
|||||||
appSettings = preferenceStore.getBoolean("appSettings", true).get(),
|
appSettings = preferenceStore.getBoolean("appSettings", true).get(),
|
||||||
sourceSettings = preferenceStore.getBoolean("sourceSettings", true).get(),
|
sourceSettings = preferenceStore.getBoolean("sourceSettings", true).get(),
|
||||||
privateSettings = preferenceStore.getBoolean("privateSettings", true).get(),
|
privateSettings = preferenceStore.getBoolean("privateSettings", true).get(),
|
||||||
|
|
||||||
|
// SY -->
|
||||||
|
customInfo = preferenceStore.getBoolean("customInfo", true).get(),
|
||||||
|
readEntries = preferenceStore.getBoolean("readEntries", true).get()
|
||||||
|
// SY <--
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,6 +72,11 @@ class SyncPreferences(
|
|||||||
preferenceStore.getBoolean("appSettings", true).set(syncSettings.appSettings)
|
preferenceStore.getBoolean("appSettings", true).set(syncSettings.appSettings)
|
||||||
preferenceStore.getBoolean("sourceSettings", true).set(syncSettings.sourceSettings)
|
preferenceStore.getBoolean("sourceSettings", true).set(syncSettings.sourceSettings)
|
||||||
preferenceStore.getBoolean("privateSettings", true).set(syncSettings.privateSettings)
|
preferenceStore.getBoolean("privateSettings", true).set(syncSettings.privateSettings)
|
||||||
|
|
||||||
|
// SY -->
|
||||||
|
preferenceStore.getBoolean("customInfo", true).set(syncSettings.customInfo)
|
||||||
|
preferenceStore.getBoolean("readEntries", true).set(syncSettings.readEntries)
|
||||||
|
// SY <--
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getSyncTriggerOptions(): SyncTriggerOptions {
|
fun getSyncTriggerOptions(): SyncTriggerOptions {
|
||||||
|
|||||||
@@ -9,4 +9,9 @@ data class SyncSettings(
|
|||||||
val appSettings: Boolean = true,
|
val appSettings: Boolean = true,
|
||||||
val sourceSettings: Boolean = true,
|
val sourceSettings: Boolean = true,
|
||||||
val privateSettings: Boolean = false,
|
val privateSettings: Boolean = false,
|
||||||
|
|
||||||
|
// SY -->
|
||||||
|
val customInfo: Boolean = true,
|
||||||
|
val readEntries: Boolean = true
|
||||||
|
// SY <--
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import android.net.Uri
|
|||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
import android.util.DisplayMetrics
|
import android.util.DisplayMetrics
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
@@ -362,10 +361,8 @@ private fun InfoText(
|
|||||||
primaryTextStyle: TextStyle = MaterialTheme.typography.bodyLarge,
|
primaryTextStyle: TextStyle = MaterialTheme.typography.bodyLarge,
|
||||||
onClick: (() -> Unit)? = null,
|
onClick: (() -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
val interactionSource = remember { MutableInteractionSource() }
|
|
||||||
|
|
||||||
val clickableModifier = if (onClick != null) {
|
val clickableModifier = if (onClick != null) {
|
||||||
Modifier.clickable(interactionSource, indication = null) { onClick() }
|
Modifier.clickable(interactionSource = null, indication = null, onClick = onClick)
|
||||||
} else {
|
} else {
|
||||||
Modifier
|
Modifier
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ fun FeedScreen(
|
|||||||
onClickDelete: (FeedSavedSearch) -> Unit,
|
onClickDelete: (FeedSavedSearch) -> Unit,
|
||||||
onClickManga: (Manga) -> Unit,
|
onClickManga: (Manga) -> Unit,
|
||||||
onRefresh: () -> Unit,
|
onRefresh: () -> Unit,
|
||||||
getMangaState: @Composable (Manga, CatalogueSource?) -> State<Manga>,
|
getMangaState: @Composable (Manga) -> State<Manga>,
|
||||||
) {
|
) {
|
||||||
when {
|
when {
|
||||||
state.isLoading -> LoadingScreen()
|
state.isLoading -> LoadingScreen()
|
||||||
@@ -119,7 +119,7 @@ fun FeedScreen(
|
|||||||
) {
|
) {
|
||||||
FeedItem(
|
FeedItem(
|
||||||
item = item,
|
item = item,
|
||||||
getMangaState = { getMangaState(it, item.source) },
|
getMangaState = { getMangaState(it) },
|
||||||
onClickManga = onClickManga,
|
onClickManga = onClickManga,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,8 +7,6 @@ import androidx.compose.animation.fadeOut
|
|||||||
import androidx.compose.animation.togetherWith
|
import androidx.compose.animation.togetherWith
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.Dp
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.window.Dialog
|
import androidx.compose.ui.window.Dialog
|
||||||
import androidx.compose.ui.window.DialogProperties
|
import androidx.compose.ui.window.DialogProperties
|
||||||
import cafe.adriel.voyager.core.annotation.InternalVoyagerApi
|
import cafe.adriel.voyager.core.annotation.InternalVoyagerApi
|
||||||
@@ -23,7 +21,6 @@ import tachiyomi.presentation.core.components.AdaptiveSheet as AdaptiveSheetImpl
|
|||||||
@Composable
|
@Composable
|
||||||
fun NavigatorAdaptiveSheet(
|
fun NavigatorAdaptiveSheet(
|
||||||
screen: Screen,
|
screen: Screen,
|
||||||
tonalElevation: Dp = 1.dp,
|
|
||||||
enableSwipeDismiss: (Navigator) -> Boolean = { true },
|
enableSwipeDismiss: (Navigator) -> Boolean = { true },
|
||||||
onDismissRequest: () -> Unit,
|
onDismissRequest: () -> Unit,
|
||||||
) {
|
) {
|
||||||
@@ -31,7 +28,6 @@ fun NavigatorAdaptiveSheet(
|
|||||||
screen = screen,
|
screen = screen,
|
||||||
content = { sheetNavigator ->
|
content = { sheetNavigator ->
|
||||||
AdaptiveSheet(
|
AdaptiveSheet(
|
||||||
tonalElevation = tonalElevation,
|
|
||||||
enableSwipeDismiss = enableSwipeDismiss(sheetNavigator),
|
enableSwipeDismiss = enableSwipeDismiss(sheetNavigator),
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
) {
|
) {
|
||||||
@@ -73,7 +69,6 @@ fun NavigatorAdaptiveSheet(
|
|||||||
fun AdaptiveSheet(
|
fun AdaptiveSheet(
|
||||||
onDismissRequest: () -> Unit,
|
onDismissRequest: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
tonalElevation: Dp = 1.dp,
|
|
||||||
enableSwipeDismiss: Boolean = true,
|
enableSwipeDismiss: Boolean = true,
|
||||||
content: @Composable () -> Unit,
|
content: @Composable () -> Unit,
|
||||||
) {
|
) {
|
||||||
@@ -86,7 +81,6 @@ fun AdaptiveSheet(
|
|||||||
AdaptiveSheetImpl(
|
AdaptiveSheetImpl(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
isTabletUi = isTabletUi,
|
isTabletUi = isTabletUi,
|
||||||
tonalElevation = tonalElevation,
|
|
||||||
enableSwipeDismiss = enableSwipeDismiss,
|
enableSwipeDismiss = enableSwipeDismiss,
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|||||||
import androidx.compose.foundation.text.BasicTextField
|
import androidx.compose.foundation.text.BasicTextField
|
||||||
import androidx.compose.foundation.text.KeyboardActions
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.material.TextFieldDefaults
|
|
||||||
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.outlined.Close
|
import androidx.compose.material.icons.outlined.Close
|
||||||
@@ -21,6 +20,7 @@ import androidx.compose.material3.LocalContentColor
|
|||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.PlainTooltip
|
import androidx.compose.material3.PlainTooltip
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextFieldDefaults
|
||||||
import androidx.compose.material3.TooltipBox
|
import androidx.compose.material3.TooltipBox
|
||||||
import androidx.compose.material3.TooltipDefaults
|
import androidx.compose.material3.TooltipDefaults
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
@@ -312,7 +312,7 @@ fun SearchToolbar(
|
|||||||
visualTransformation = visualTransformation,
|
visualTransformation = visualTransformation,
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
decorationBox = { innerTextField ->
|
decorationBox = { innerTextField ->
|
||||||
TextFieldDefaults.TextFieldDecorationBox(
|
TextFieldDefaults.DecorationBox(
|
||||||
value = searchQuery,
|
value = searchQuery,
|
||||||
innerTextField = innerTextField,
|
innerTextField = innerTextField,
|
||||||
enabled = true,
|
enabled = true,
|
||||||
@@ -331,6 +331,7 @@ fun SearchToolbar(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
container = {},
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ fun TabbedDialog(
|
|||||||
PrimaryTabRow(
|
PrimaryTabRow(
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
selectedTabIndex = pagerState.currentPage,
|
selectedTabIndex = pagerState.currentPage,
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||||
divider = {},
|
divider = {},
|
||||||
) {
|
) {
|
||||||
tabTitles.fastForEachIndexed { index, tab ->
|
tabTitles.fastForEachIndexed { index, tab ->
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ fun CrashScreen(
|
|||||||
acceptText = stringResource(MR.strings.pref_dump_crash_logs),
|
acceptText = stringResource(MR.strings.pref_dump_crash_logs),
|
||||||
onAcceptClick = {
|
onAcceptClick = {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
CrashLogUtil(context).dumpLogs()
|
CrashLogUtil(context).dumpLogs(exception)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
rejectText = stringResource(MR.strings.crash_screen_restart_application),
|
rejectText = stringResource(MR.strings.crash_screen_restart_application),
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ private fun LibraryRegularToolbar(
|
|||||||
onClick = onClickOpenRandomManga,
|
onClick = onClickOpenRandomManga,
|
||||||
),
|
),
|
||||||
AppBar.OverflowAction(
|
AppBar.OverflowAction(
|
||||||
title = stringResource(MR.strings.sync_library),
|
title = stringResource(SYMR.strings.sync_library),
|
||||||
onClick = onClickSyncNow,
|
onClick = onClickSyncNow,
|
||||||
),
|
),
|
||||||
).builder().apply {
|
).builder().apply {
|
||||||
|
|||||||
@@ -1,16 +1,33 @@
|
|||||||
package eu.kanade.presentation.manga
|
package eu.kanade.presentation.manga
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.FlowRow
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.sizeIn
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.Add
|
||||||
|
import androidx.compose.material.icons.outlined.Book
|
||||||
|
import androidx.compose.material.icons.outlined.SwapVert
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedButton
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import eu.kanade.presentation.components.AdaptiveSheet
|
||||||
|
import eu.kanade.presentation.components.TabbedDialogPaddings
|
||||||
|
import eu.kanade.presentation.more.settings.LocalPreferenceMinHeight
|
||||||
|
import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.components.material.padding
|
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -18,42 +35,92 @@ fun DuplicateMangaDialog(
|
|||||||
onDismissRequest: () -> Unit,
|
onDismissRequest: () -> Unit,
|
||||||
onConfirm: () -> Unit,
|
onConfirm: () -> Unit,
|
||||||
onOpenManga: () -> Unit,
|
onOpenManga: () -> Unit,
|
||||||
|
onMigrate: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
AlertDialog(
|
val minHeight = LocalPreferenceMinHeight.current
|
||||||
|
|
||||||
|
AdaptiveSheet(
|
||||||
|
modifier = modifier,
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
title = {
|
) {
|
||||||
Text(text = stringResource(MR.strings.are_you_sure))
|
Column(
|
||||||
},
|
modifier = Modifier
|
||||||
text = {
|
.padding(
|
||||||
Text(text = stringResource(MR.strings.confirm_add_duplicate_manga))
|
vertical = TabbedDialogPaddings.Vertical,
|
||||||
},
|
horizontal = TabbedDialogPaddings.Horizontal,
|
||||||
confirmButton = {
|
)
|
||||||
FlowRow(
|
.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
|
) {
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.padding(TitlePadding),
|
||||||
|
text = stringResource(MR.strings.are_you_sure),
|
||||||
|
style = MaterialTheme.typography.headlineMedium,
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = stringResource(MR.strings.confirm_add_duplicate_manga),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(Modifier.height(PaddingSize))
|
||||||
|
|
||||||
|
TextPreferenceWidget(
|
||||||
|
title = stringResource(MR.strings.action_show_manga),
|
||||||
|
icon = Icons.Outlined.Book,
|
||||||
|
onPreferenceClick = {
|
||||||
|
onDismissRequest()
|
||||||
|
onOpenManga()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
HorizontalDivider()
|
||||||
|
|
||||||
|
TextPreferenceWidget(
|
||||||
|
title = stringResource(MR.strings.action_migrate_duplicate),
|
||||||
|
icon = Icons.Outlined.SwapVert,
|
||||||
|
onPreferenceClick = {
|
||||||
|
onDismissRequest()
|
||||||
|
onMigrate()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
HorizontalDivider()
|
||||||
|
|
||||||
|
TextPreferenceWidget(
|
||||||
|
title = stringResource(MR.strings.action_add_anyway),
|
||||||
|
icon = Icons.Outlined.Add,
|
||||||
|
onPreferenceClick = {
|
||||||
|
onDismissRequest()
|
||||||
|
onConfirm()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.sizeIn(minHeight = minHeight)
|
||||||
|
.clickable { onDismissRequest.invoke() }
|
||||||
|
.padding(ButtonPadding)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.Center,
|
||||||
) {
|
) {
|
||||||
TextButton(
|
OutlinedButton(onClick = onDismissRequest, modifier = Modifier.fillMaxWidth()) {
|
||||||
onClick = {
|
Text(
|
||||||
onDismissRequest()
|
modifier = Modifier
|
||||||
onOpenManga()
|
.padding(vertical = 8.dp),
|
||||||
},
|
text = stringResource(MR.strings.action_cancel),
|
||||||
) {
|
color = MaterialTheme.colorScheme.primary,
|
||||||
Text(text = stringResource(MR.strings.action_show_manga))
|
style = MaterialTheme.typography.titleLarge,
|
||||||
}
|
fontSize = 16.sp,
|
||||||
|
)
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
|
||||||
|
|
||||||
TextButton(onClick = onDismissRequest) {
|
|
||||||
Text(text = stringResource(MR.strings.action_cancel))
|
|
||||||
}
|
|
||||||
TextButton(
|
|
||||||
onClick = {
|
|
||||||
onDismissRequest()
|
|
||||||
onConfirm()
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
Text(text = stringResource(MR.strings.action_add))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val PaddingSize = 16.dp
|
||||||
|
|
||||||
|
private val ButtonPadding = PaddingValues(top = 16.dp, bottom = 16.dp)
|
||||||
|
private val TitlePadding = PaddingValues(bottom = 16.dp, top = 8.dp)
|
||||||
|
|||||||
+1
-1
@@ -9,13 +9,13 @@ import androidx.compose.material.icons.Icons
|
|||||||
import androidx.compose.material.icons.filled.CheckCircle
|
import androidx.compose.material.icons.filled.CheckCircle
|
||||||
import androidx.compose.material.icons.outlined.ArrowDownward
|
import androidx.compose.material.icons.outlined.ArrowDownward
|
||||||
import androidx.compose.material.icons.outlined.ErrorOutline
|
import androidx.compose.material.icons.outlined.ErrorOutline
|
||||||
import androidx.compose.material.ripple
|
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.ProgressIndicatorDefaults
|
import androidx.compose.material3.ProgressIndicatorDefaults
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.ripple
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import androidx.compose.animation.fadeIn
|
|||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
import androidx.compose.animation.shrinkVertically
|
import androidx.compose.animation.shrinkVertically
|
||||||
import androidx.compose.foundation.combinedClickable
|
import androidx.compose.foundation.combinedClickable
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
@@ -33,12 +32,12 @@ import androidx.compose.material.icons.outlined.Label
|
|||||||
import androidx.compose.material.icons.outlined.MoreVert
|
import androidx.compose.material.icons.outlined.MoreVert
|
||||||
import androidx.compose.material.icons.outlined.RemoveDone
|
import androidx.compose.material.icons.outlined.RemoveDone
|
||||||
import androidx.compose.material.icons.outlined.SwapCalls
|
import androidx.compose.material.icons.outlined.SwapCalls
|
||||||
import androidx.compose.material.ripple
|
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
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.material3.ripple
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateListOf
|
import androidx.compose.runtime.mutableStateListOf
|
||||||
@@ -90,7 +89,7 @@ fun MangaBottomActionMenu(
|
|||||||
Surface(
|
Surface(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
shape = MaterialTheme.shapes.large.copy(bottomEnd = ZeroCornerSize, bottomStart = ZeroCornerSize),
|
shape = MaterialTheme.shapes.large.copy(bottomEnd = ZeroCornerSize, bottomStart = ZeroCornerSize),
|
||||||
tonalElevation = 3.dp,
|
color = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||||
) {
|
) {
|
||||||
val haptic = LocalHapticFeedback.current
|
val haptic = LocalHapticFeedback.current
|
||||||
val confirm = remember { mutableStateListOf(false, false, false, false, false, false, false) }
|
val confirm = remember { mutableStateListOf(false, false, false, false, false, false, false) }
|
||||||
@@ -199,7 +198,7 @@ private fun RowScope.Button(
|
|||||||
.size(48.dp)
|
.size(48.dp)
|
||||||
.weight(animatedWeight)
|
.weight(animatedWeight)
|
||||||
.combinedClickable(
|
.combinedClickable(
|
||||||
interactionSource = remember { MutableInteractionSource() },
|
interactionSource = null,
|
||||||
indication = ripple(bounded = false),
|
indication = ripple(bounded = false),
|
||||||
onLongClick = onLongClick,
|
onLongClick = onLongClick,
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
@@ -252,7 +251,7 @@ fun LibraryBottomActionMenu(
|
|||||||
Surface(
|
Surface(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
shape = MaterialTheme.shapes.large.copy(bottomEnd = ZeroCornerSize, bottomStart = ZeroCornerSize),
|
shape = MaterialTheme.shapes.large.copy(bottomEnd = ZeroCornerSize, bottomStart = ZeroCornerSize),
|
||||||
tonalElevation = 3.dp,
|
color = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||||
) {
|
) {
|
||||||
val haptic = LocalHapticFeedback.current
|
val haptic = LocalHapticFeedback.current
|
||||||
val confirm =
|
val confirm =
|
||||||
|
|||||||
+78
-65
@@ -8,6 +8,8 @@ import android.provider.Settings
|
|||||||
import android.webkit.WebStorage
|
import android.webkit.WebStorage
|
||||||
import android.webkit.WebView
|
import android.webkit.WebView
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
@@ -112,71 +114,54 @@ object SettingsAdvancedScreen : SearchableSettings {
|
|||||||
val basePreferences = remember { Injekt.get<BasePreferences>() }
|
val basePreferences = remember { Injekt.get<BasePreferences>() }
|
||||||
val networkPreferences = remember { Injekt.get<NetworkPreferences>() }
|
val networkPreferences = remember { Injekt.get<NetworkPreferences>() }
|
||||||
|
|
||||||
return buildList {
|
return listOf(
|
||||||
addAll(
|
Preference.PreferenceItem.TextPreference(
|
||||||
listOf(
|
title = stringResource(MR.strings.pref_dump_crash_logs),
|
||||||
/* SY --> Preference.PreferenceItem.SwitchPreference(
|
subtitle = stringResource(MR.strings.pref_dump_crash_logs_summary),
|
||||||
pref = basePreferences.acraEnabled(),
|
onClick = {
|
||||||
title = stringResource(MR.strings.pref_enable_acra),
|
scope.launch {
|
||||||
subtitle = stringResource(MR.strings.pref_acra_summary),
|
CrashLogUtil(context).dumpLogs()
|
||||||
enabled = isPreviewBuildType || isReleaseBuildType,
|
}
|
||||||
), SY <-- */
|
},
|
||||||
Preference.PreferenceItem.TextPreference(
|
),
|
||||||
title = stringResource(MR.strings.pref_dump_crash_logs),
|
/* SY --> Preference.PreferenceItem.SwitchPreference(
|
||||||
subtitle = stringResource(MR.strings.pref_dump_crash_logs_summary),
|
pref = networkPreferences.verboseLogging(),
|
||||||
onClick = {
|
title = stringResource(MR.strings.pref_verbose_logging),
|
||||||
scope.launch {
|
subtitle = stringResource(MR.strings.pref_verbose_logging_summary),
|
||||||
CrashLogUtil(context).dumpLogs()
|
onValueChanged = {
|
||||||
}
|
context.toast(MR.strings.requires_app_restart)
|
||||||
},
|
true
|
||||||
),
|
},
|
||||||
/* SY --> Preference.PreferenceItem.SwitchPreference(
|
), SY <-- */
|
||||||
pref = networkPreferences.verboseLogging(),
|
Preference.PreferenceItem.TextPreference(
|
||||||
title = stringResource(MR.strings.pref_verbose_logging),
|
title = stringResource(MR.strings.pref_debug_info),
|
||||||
subtitle = stringResource(MR.strings.pref_verbose_logging_summary),
|
onClick = { navigator.push(DebugInfoScreen()) },
|
||||||
onValueChanged = {
|
),
|
||||||
context.toast(MR.strings.requires_app_restart)
|
Preference.PreferenceItem.TextPreference(
|
||||||
true
|
title = stringResource(MR.strings.pref_onboarding_guide),
|
||||||
},
|
onClick = { navigator.push(OnboardingScreen()) },
|
||||||
), SY <-- */
|
),
|
||||||
Preference.PreferenceItem.TextPreference(
|
Preference.PreferenceItem.TextPreference(
|
||||||
title = stringResource(MR.strings.pref_debug_info),
|
title = stringResource(MR.strings.pref_manage_notifications),
|
||||||
onClick = { navigator.push(DebugInfoScreen()) },
|
onClick = {
|
||||||
),
|
val intent = Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS).apply {
|
||||||
Preference.PreferenceItem.TextPreference(
|
putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName)
|
||||||
title = stringResource(MR.strings.pref_onboarding_guide),
|
}
|
||||||
onClick = { navigator.push(OnboardingScreen()) },
|
context.startActivity(intent)
|
||||||
),
|
},
|
||||||
),
|
),
|
||||||
)
|
getBackgroundActivityGroup(),
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
getDataGroup(),
|
||||||
add(
|
getNetworkGroup(networkPreferences = networkPreferences),
|
||||||
Preference.PreferenceItem.TextPreference(
|
getLibraryGroup(),
|
||||||
title = stringResource(MR.strings.pref_manage_notifications),
|
getReaderGroup(basePreferences = basePreferences),
|
||||||
onClick = {
|
getExtensionsGroup(basePreferences = basePreferences),
|
||||||
val intent = Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS).apply {
|
// SY -->
|
||||||
putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName)
|
// getDownloaderGroup(),
|
||||||
}
|
getDataSaverGroup(),
|
||||||
context.startActivity(intent)
|
getDeveloperToolsGroup(),
|
||||||
},
|
// SY <--
|
||||||
),
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
addAll(
|
|
||||||
listOf(
|
|
||||||
getBackgroundActivityGroup(),
|
|
||||||
getDataGroup(),
|
|
||||||
getNetworkGroup(networkPreferences = networkPreferences),
|
|
||||||
getLibraryGroup(),
|
|
||||||
getExtensionsGroup(basePreferences = basePreferences),
|
|
||||||
// SY -->
|
|
||||||
// getDownloaderGroup(),
|
|
||||||
getDataSaverGroup(),
|
|
||||||
getDeveloperToolsGroup(),
|
|
||||||
// SY <--
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -367,6 +352,34 @@ object SettingsAdvancedScreen : SearchableSettings {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun getReaderGroup(
|
||||||
|
basePreferences: BasePreferences,
|
||||||
|
): Preference.PreferenceGroup {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val chooseColorProfile = rememberLauncherForActivityResult(
|
||||||
|
contract = ActivityResultContracts.OpenDocument(),
|
||||||
|
) { uri ->
|
||||||
|
uri?.let {
|
||||||
|
val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||||
|
context.contentResolver.takePersistableUriPermission(uri, flags)
|
||||||
|
basePreferences.displayProfile().set(uri.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Preference.PreferenceGroup(
|
||||||
|
title = stringResource(MR.strings.pref_category_reader),
|
||||||
|
preferenceItems = persistentListOf(
|
||||||
|
Preference.PreferenceItem.TextPreference(
|
||||||
|
title = stringResource(MR.strings.pref_display_profile),
|
||||||
|
subtitle = basePreferences.displayProfile().get(),
|
||||||
|
onClick = {
|
||||||
|
chooseColorProfile.launch(arrayOf("*/*"))
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun getExtensionsGroup(
|
private fun getExtensionsGroup(
|
||||||
basePreferences: BasePreferences,
|
basePreferences: BasePreferences,
|
||||||
|
|||||||
+6
-2
@@ -2,6 +2,7 @@ package eu.kanade.presentation.more.settings.screen
|
|||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.ReadOnlyComposable
|
import androidx.compose.runtime.ReadOnlyComposable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
@@ -17,6 +18,7 @@ import eu.kanade.presentation.more.settings.screen.browse.ExtensionReposScreen
|
|||||||
import eu.kanade.tachiyomi.ui.category.sources.SourceCategoryScreen
|
import eu.kanade.tachiyomi.ui.category.sources.SourceCategoryScreen
|
||||||
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil.authenticate
|
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil.authenticate
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
import mihon.domain.extensionrepo.interactor.GetExtensionRepoCount
|
||||||
import tachiyomi.core.common.i18n.stringResource
|
import tachiyomi.core.common.i18n.stringResource
|
||||||
import tachiyomi.domain.UnsortedPreferences
|
import tachiyomi.domain.UnsortedPreferences
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
@@ -39,7 +41,9 @@ object SettingsBrowseScreen : SearchableSettings {
|
|||||||
val navigator = LocalNavigator.currentOrThrow
|
val navigator = LocalNavigator.currentOrThrow
|
||||||
|
|
||||||
val sourcePreferences = remember { Injekt.get<SourcePreferences>() }
|
val sourcePreferences = remember { Injekt.get<SourcePreferences>() }
|
||||||
val reposCount by sourcePreferences.extensionRepos().collectAsState()
|
val getExtensionRepoCount = remember { Injekt.get<GetExtensionRepoCount>() }
|
||||||
|
|
||||||
|
val reposCount by getExtensionRepoCount.subscribe().collectAsState(0)
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
@@ -104,7 +108,7 @@ object SettingsBrowseScreen : SearchableSettings {
|
|||||||
),
|
),
|
||||||
Preference.PreferenceItem.TextPreference(
|
Preference.PreferenceItem.TextPreference(
|
||||||
title = stringResource(MR.strings.label_extension_repos),
|
title = stringResource(MR.strings.label_extension_repos),
|
||||||
subtitle = pluralStringResource(MR.plurals.num_repos, reposCount.size, reposCount.size),
|
subtitle = pluralStringResource(MR.plurals.num_repos, reposCount, reposCount),
|
||||||
onClick = {
|
onClick = {
|
||||||
navigator.push(ExtensionReposScreen())
|
navigator.push(ExtensionReposScreen())
|
||||||
},
|
},
|
||||||
|
|||||||
+27
-27
@@ -349,15 +349,15 @@ object SettingsDataScreen : SearchableSettings {
|
|||||||
private fun getSyncPreferences(syncPreferences: SyncPreferences, syncService: Int): List<Preference> {
|
private fun getSyncPreferences(syncPreferences: SyncPreferences, syncService: Int): List<Preference> {
|
||||||
return listOf(
|
return listOf(
|
||||||
Preference.PreferenceGroup(
|
Preference.PreferenceGroup(
|
||||||
title = stringResource(MR.strings.pref_sync_service_category),
|
title = stringResource(SYMR.strings.pref_sync_service_category),
|
||||||
preferenceItems = persistentListOf(
|
preferenceItems = persistentListOf(
|
||||||
Preference.PreferenceItem.ListPreference(
|
Preference.PreferenceItem.ListPreference(
|
||||||
pref = syncPreferences.syncService(),
|
pref = syncPreferences.syncService(),
|
||||||
title = stringResource(MR.strings.pref_sync_service),
|
title = stringResource(SYMR.strings.pref_sync_service),
|
||||||
entries = persistentMapOf(
|
entries = persistentMapOf(
|
||||||
SyncManager.SyncService.NONE.value to stringResource(MR.strings.off),
|
SyncManager.SyncService.NONE.value to stringResource(MR.strings.off),
|
||||||
SyncManager.SyncService.SYNCYOMI.value to stringResource(MR.strings.syncyomi),
|
SyncManager.SyncService.SYNCYOMI.value to stringResource(SYMR.strings.syncyomi),
|
||||||
SyncManager.SyncService.GOOGLE_DRIVE.value to stringResource(MR.strings.google_drive),
|
SyncManager.SyncService.GOOGLE_DRIVE.value to stringResource(SYMR.strings.google_drive),
|
||||||
),
|
),
|
||||||
onValueChanged = { true },
|
onValueChanged = { true },
|
||||||
),
|
),
|
||||||
@@ -402,7 +402,7 @@ object SettingsDataScreen : SearchableSettings {
|
|||||||
val googleDriveSync = Injekt.get<GoogleDriveService>()
|
val googleDriveSync = Injekt.get<GoogleDriveService>()
|
||||||
return listOf(
|
return listOf(
|
||||||
Preference.PreferenceItem.TextPreference(
|
Preference.PreferenceItem.TextPreference(
|
||||||
title = stringResource(MR.strings.pref_google_drive_sign_in),
|
title = stringResource(SYMR.strings.pref_google_drive_sign_in),
|
||||||
onClick = {
|
onClick = {
|
||||||
val intent = googleDriveSync.getSignInIntent()
|
val intent = googleDriveSync.getSignInIntent()
|
||||||
context.startActivity(intent)
|
context.startActivity(intent)
|
||||||
@@ -427,19 +427,19 @@ object SettingsDataScreen : SearchableSettings {
|
|||||||
val result = googleDriveSync.deleteSyncDataFromGoogleDrive()
|
val result = googleDriveSync.deleteSyncDataFromGoogleDrive()
|
||||||
when (result) {
|
when (result) {
|
||||||
GoogleDriveSyncService.DeleteSyncDataStatus.NOT_INITIALIZED -> context.toast(
|
GoogleDriveSyncService.DeleteSyncDataStatus.NOT_INITIALIZED -> context.toast(
|
||||||
MR.strings.google_drive_not_signed_in,
|
SYMR.strings.google_drive_not_signed_in,
|
||||||
duration = 5000,
|
duration = 5000,
|
||||||
)
|
)
|
||||||
GoogleDriveSyncService.DeleteSyncDataStatus.NO_FILES -> context.toast(
|
GoogleDriveSyncService.DeleteSyncDataStatus.NO_FILES -> context.toast(
|
||||||
MR.strings.google_drive_sync_data_not_found,
|
SYMR.strings.google_drive_sync_data_not_found,
|
||||||
duration = 5000,
|
duration = 5000,
|
||||||
)
|
)
|
||||||
GoogleDriveSyncService.DeleteSyncDataStatus.SUCCESS -> context.toast(
|
GoogleDriveSyncService.DeleteSyncDataStatus.SUCCESS -> context.toast(
|
||||||
MR.strings.google_drive_sync_data_purged,
|
SYMR.strings.google_drive_sync_data_purged,
|
||||||
duration = 5000,
|
duration = 5000,
|
||||||
)
|
)
|
||||||
GoogleDriveSyncService.DeleteSyncDataStatus.ERROR -> context.toast(
|
GoogleDriveSyncService.DeleteSyncDataStatus.ERROR -> context.toast(
|
||||||
MR.strings.google_drive_sync_data_purge_error,
|
SYMR.strings.google_drive_sync_data_purge_error,
|
||||||
duration = 10000,
|
duration = 10000,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -450,7 +450,7 @@ object SettingsDataScreen : SearchableSettings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return Preference.PreferenceItem.TextPreference(
|
return Preference.PreferenceItem.TextPreference(
|
||||||
title = stringResource(MR.strings.pref_google_drive_purge_sync_data),
|
title = stringResource(SYMR.strings.pref_google_drive_purge_sync_data),
|
||||||
onClick = { showPurgeDialog = true },
|
onClick = { showPurgeDialog = true },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -462,8 +462,8 @@ object SettingsDataScreen : SearchableSettings {
|
|||||||
) {
|
) {
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
title = { Text(text = stringResource(MR.strings.pref_purge_confirmation_title)) },
|
title = { Text(text = stringResource(SYMR.strings.pref_purge_confirmation_title)) },
|
||||||
text = { Text(text = stringResource(MR.strings.pref_purge_confirmation_message)) },
|
text = { Text(text = stringResource(SYMR.strings.pref_purge_confirmation_message)) },
|
||||||
dismissButton = {
|
dismissButton = {
|
||||||
TextButton(onClick = onDismissRequest) {
|
TextButton(onClick = onDismissRequest) {
|
||||||
Text(text = stringResource(MR.strings.action_cancel))
|
Text(text = stringResource(MR.strings.action_cancel))
|
||||||
@@ -482,8 +482,8 @@ object SettingsDataScreen : SearchableSettings {
|
|||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
return listOf(
|
return listOf(
|
||||||
Preference.PreferenceItem.EditTextPreference(
|
Preference.PreferenceItem.EditTextPreference(
|
||||||
title = stringResource(MR.strings.pref_sync_host),
|
title = stringResource(SYMR.strings.pref_sync_host),
|
||||||
subtitle = stringResource(MR.strings.pref_sync_host_summ),
|
subtitle = stringResource(SYMR.strings.pref_sync_host_summ),
|
||||||
pref = syncPreferences.clientHost(),
|
pref = syncPreferences.clientHost(),
|
||||||
onValueChanged = { newValue ->
|
onValueChanged = { newValue ->
|
||||||
scope.launch {
|
scope.launch {
|
||||||
@@ -496,8 +496,8 @@ object SettingsDataScreen : SearchableSettings {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
Preference.PreferenceItem.EditTextPreference(
|
Preference.PreferenceItem.EditTextPreference(
|
||||||
title = stringResource(MR.strings.pref_sync_api_key),
|
title = stringResource(SYMR.strings.pref_sync_api_key),
|
||||||
subtitle = stringResource(MR.strings.pref_sync_api_key_summ),
|
subtitle = stringResource(SYMR.strings.pref_sync_api_key_summ),
|
||||||
pref = syncPreferences.clientAPIKey(),
|
pref = syncPreferences.clientAPIKey(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -507,12 +507,12 @@ object SettingsDataScreen : SearchableSettings {
|
|||||||
private fun getSyncNowPref(): Preference.PreferenceGroup {
|
private fun getSyncNowPref(): Preference.PreferenceGroup {
|
||||||
val navigator = LocalNavigator.currentOrThrow
|
val navigator = LocalNavigator.currentOrThrow
|
||||||
return Preference.PreferenceGroup(
|
return Preference.PreferenceGroup(
|
||||||
title = stringResource(MR.strings.pref_sync_now_group_title),
|
title = stringResource(SYMR.strings.pref_sync_now_group_title),
|
||||||
preferenceItems = persistentListOf(
|
preferenceItems = persistentListOf(
|
||||||
getSyncOptionsPref(),
|
getSyncOptionsPref(),
|
||||||
Preference.PreferenceItem.TextPreference(
|
Preference.PreferenceItem.TextPreference(
|
||||||
title = stringResource(MR.strings.pref_sync_now),
|
title = stringResource(SYMR.strings.pref_sync_now),
|
||||||
subtitle = stringResource(MR.strings.pref_sync_now_subtitle),
|
subtitle = stringResource(SYMR.strings.pref_sync_now_subtitle),
|
||||||
onClick = {
|
onClick = {
|
||||||
navigator.push(SyncSettingsSelector())
|
navigator.push(SyncSettingsSelector())
|
||||||
},
|
},
|
||||||
@@ -525,8 +525,8 @@ object SettingsDataScreen : SearchableSettings {
|
|||||||
private fun getSyncOptionsPref(): Preference.PreferenceItem.TextPreference {
|
private fun getSyncOptionsPref(): Preference.PreferenceItem.TextPreference {
|
||||||
val navigator = LocalNavigator.currentOrThrow
|
val navigator = LocalNavigator.currentOrThrow
|
||||||
return Preference.PreferenceItem.TextPreference(
|
return Preference.PreferenceItem.TextPreference(
|
||||||
title = stringResource(MR.strings.pref_sync_options),
|
title = stringResource(SYMR.strings.pref_sync_options),
|
||||||
subtitle = stringResource(MR.strings.pref_sync_options_summ),
|
subtitle = stringResource(SYMR.strings.pref_sync_options_summ),
|
||||||
onClick = { navigator.push(SyncTriggerOptionsScreen()) },
|
onClick = { navigator.push(SyncTriggerOptionsScreen()) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -538,16 +538,16 @@ object SettingsDataScreen : SearchableSettings {
|
|||||||
val lastSync by syncPreferences.lastSyncTimestamp().collectAsState()
|
val lastSync by syncPreferences.lastSyncTimestamp().collectAsState()
|
||||||
|
|
||||||
return Preference.PreferenceGroup(
|
return Preference.PreferenceGroup(
|
||||||
title = stringResource(MR.strings.pref_sync_automatic_category),
|
title = stringResource(SYMR.strings.pref_sync_automatic_category),
|
||||||
preferenceItems = persistentListOf(
|
preferenceItems = persistentListOf(
|
||||||
Preference.PreferenceItem.ListPreference(
|
Preference.PreferenceItem.ListPreference(
|
||||||
pref = syncIntervalPref,
|
pref = syncIntervalPref,
|
||||||
title = stringResource(MR.strings.pref_sync_interval),
|
title = stringResource(SYMR.strings.pref_sync_interval),
|
||||||
entries = persistentMapOf(
|
entries = persistentMapOf(
|
||||||
0 to stringResource(MR.strings.off),
|
0 to stringResource(MR.strings.off),
|
||||||
30 to stringResource(MR.strings.update_30min),
|
30 to stringResource(SYMR.strings.update_30min),
|
||||||
60 to stringResource(MR.strings.update_1hour),
|
60 to stringResource(SYMR.strings.update_1hour),
|
||||||
180 to stringResource(MR.strings.update_3hour),
|
180 to stringResource(SYMR.strings.update_3hour),
|
||||||
360 to stringResource(MR.strings.update_6hour),
|
360 to stringResource(MR.strings.update_6hour),
|
||||||
720 to stringResource(MR.strings.update_12hour),
|
720 to stringResource(MR.strings.update_12hour),
|
||||||
1440 to stringResource(MR.strings.update_24hour),
|
1440 to stringResource(MR.strings.update_24hour),
|
||||||
@@ -560,7 +560,7 @@ object SettingsDataScreen : SearchableSettings {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
Preference.PreferenceItem.InfoPreference(
|
Preference.PreferenceItem.InfoPreference(
|
||||||
stringResource(MR.strings.last_synchronization, relativeTimeSpanString(lastSync)),
|
stringResource(SYMR.strings.last_synchronization, relativeTimeSpanString(lastSync)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
+1
-6
@@ -35,6 +35,7 @@ object SettingsReaderScreen : SearchableSettings {
|
|||||||
// SY -->
|
// SY -->
|
||||||
val forceHorizontalSeekbar by readerPref.forceHorizontalSeekbar().collectAsState()
|
val forceHorizontalSeekbar by readerPref.forceHorizontalSeekbar().collectAsState()
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
return listOf(
|
return listOf(
|
||||||
Preference.PreferenceItem.ListPreference(
|
Preference.PreferenceItem.ListPreference(
|
||||||
pref = readerPref.defaultReadingMode(),
|
pref = readerPref.defaultReadingMode(),
|
||||||
@@ -81,12 +82,6 @@ object SettingsReaderScreen : SearchableSettings {
|
|||||||
enabled = !forceHorizontalSeekbar,
|
enabled = !forceHorizontalSeekbar,
|
||||||
),
|
),
|
||||||
// SY <--
|
// SY <--
|
||||||
Preference.PreferenceItem.SwitchPreference(
|
|
||||||
pref = readerPref.trueColor(),
|
|
||||||
title = stringResource(MR.strings.pref_true_color),
|
|
||||||
subtitle = stringResource(MR.strings.pref_true_color_summary),
|
|
||||||
enabled = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O,
|
|
||||||
),
|
|
||||||
/* SY -->
|
/* SY -->
|
||||||
Preference.PreferenceItem.SwitchPreference(
|
Preference.PreferenceItem.SwitchPreference(
|
||||||
pref = readerPref.pageTransitions(),
|
pref = readerPref.pageTransitions(),
|
||||||
|
|||||||
@@ -56,10 +56,9 @@ import tachiyomi.presentation.core.icons.Reddit
|
|||||||
import tachiyomi.presentation.core.icons.X
|
import tachiyomi.presentation.core.icons.X
|
||||||
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.Instant
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
import java.time.ZoneId
|
import java.time.ZoneId
|
||||||
import java.time.format.DateTimeFormatter
|
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
object AboutScreen : Screen() {
|
object AboutScreen : Screen() {
|
||||||
|
|
||||||
@@ -293,11 +292,15 @@ object AboutScreen : Screen() {
|
|||||||
|
|
||||||
internal fun getFormattedBuildTime(): String {
|
internal fun getFormattedBuildTime(): String {
|
||||||
return try {
|
return try {
|
||||||
val df = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm'Z'", Locale.US)
|
LocalDateTime.ofInstant(
|
||||||
.withZone(ZoneId.of("UTC"))
|
Instant.parse(BuildConfig.BUILD_TIME),
|
||||||
val buildTime = LocalDateTime.from(df.parse(BuildConfig.BUILD_TIME))
|
ZoneId.systemDefault(),
|
||||||
|
)
|
||||||
buildTime!!.toDateTimestampString(UiPreferences.dateFormat(Injekt.get<UiPreferences>().dateFormat().get()))
|
.toDateTimestampString(
|
||||||
|
UiPreferences.dateFormat(
|
||||||
|
Injekt.get<UiPreferences>().dateFormat().get(),
|
||||||
|
),
|
||||||
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
BuildConfig.BUILD_TIME
|
BuildConfig.BUILD_TIME
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-5
@@ -32,12 +32,13 @@ class OpenSourceLicensesScreen : Screen() {
|
|||||||
.fillMaxSize(),
|
.fillMaxSize(),
|
||||||
contentPadding = contentPadding,
|
contentPadding = contentPadding,
|
||||||
onLibraryClick = {
|
onLibraryClick = {
|
||||||
val libraryLicenseScreen = OpenSourceLibraryLicenseScreen(
|
navigator.push(
|
||||||
name = it.library.name,
|
OpenSourceLibraryLicenseScreen(
|
||||||
website = it.library.website,
|
name = it.name,
|
||||||
license = it.library.licenses.firstOrNull()?.htmlReadyLicenseContent.orEmpty(),
|
website = it.website,
|
||||||
|
license = it.licenses.firstOrNull()?.htmlReadyLicenseContent.orEmpty(),
|
||||||
|
)
|
||||||
)
|
)
|
||||||
navigator.push(libraryLicenseScreen)
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-2
@@ -8,11 +8,14 @@ import androidx.compose.ui.platform.LocalContext
|
|||||||
import cafe.adriel.voyager.core.model.rememberScreenModel
|
import cafe.adriel.voyager.core.model.rememberScreenModel
|
||||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||||
|
import eu.kanade.presentation.more.settings.screen.browse.components.ExtensionRepoConflictDialog
|
||||||
import eu.kanade.presentation.more.settings.screen.browse.components.ExtensionRepoCreateDialog
|
import eu.kanade.presentation.more.settings.screen.browse.components.ExtensionRepoCreateDialog
|
||||||
import eu.kanade.presentation.more.settings.screen.browse.components.ExtensionRepoDeleteDialog
|
import eu.kanade.presentation.more.settings.screen.browse.components.ExtensionRepoDeleteDialog
|
||||||
import eu.kanade.presentation.more.settings.screen.browse.components.ExtensionReposScreen
|
import eu.kanade.presentation.more.settings.screen.browse.components.ExtensionReposScreen
|
||||||
import eu.kanade.presentation.util.Screen
|
import eu.kanade.presentation.util.Screen
|
||||||
|
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.toImmutableSet
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import tachiyomi.presentation.core.screens.LoadingScreen
|
import tachiyomi.presentation.core.screens.LoadingScreen
|
||||||
|
|
||||||
@@ -42,17 +45,19 @@ class ExtensionReposScreen(
|
|||||||
ExtensionReposScreen(
|
ExtensionReposScreen(
|
||||||
state = successState,
|
state = successState,
|
||||||
onClickCreate = { screenModel.showDialog(RepoDialog.Create) },
|
onClickCreate = { screenModel.showDialog(RepoDialog.Create) },
|
||||||
|
onOpenWebsite = { context.openInBrowser(it.website) },
|
||||||
onClickDelete = { screenModel.showDialog(RepoDialog.Delete(it)) },
|
onClickDelete = { screenModel.showDialog(RepoDialog.Delete(it)) },
|
||||||
|
onClickRefresh = { screenModel.refreshRepos() },
|
||||||
navigateUp = navigator::pop,
|
navigateUp = navigator::pop,
|
||||||
)
|
)
|
||||||
|
|
||||||
when (val dialog = successState.dialog) {
|
when (val dialog = successState.dialog) {
|
||||||
null -> {}
|
null -> {}
|
||||||
RepoDialog.Create -> {
|
is RepoDialog.Create -> {
|
||||||
ExtensionRepoCreateDialog(
|
ExtensionRepoCreateDialog(
|
||||||
onDismissRequest = screenModel::dismissDialog,
|
onDismissRequest = screenModel::dismissDialog,
|
||||||
onCreate = { screenModel.createRepo(it) },
|
onCreate = { screenModel.createRepo(it) },
|
||||||
repos = successState.repos,
|
repoUrls = successState.repos.map { it.baseUrl }.toImmutableSet(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is RepoDialog.Delete -> {
|
is RepoDialog.Delete -> {
|
||||||
@@ -62,6 +67,15 @@ class ExtensionReposScreen(
|
|||||||
repo = dialog.repo,
|
repo = dialog.repo,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is RepoDialog.Conflict -> {
|
||||||
|
ExtensionRepoConflictDialog(
|
||||||
|
onDismissRequest = screenModel::dismissDialog,
|
||||||
|
onMigrate = { screenModel.replaceRepo(dialog.newRepo) },
|
||||||
|
oldRepo = dialog.oldRepo,
|
||||||
|
newRepo = dialog.newRepo,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
|
|||||||
+48
-14
@@ -4,24 +4,29 @@ import androidx.compose.runtime.Immutable
|
|||||||
import cafe.adriel.voyager.core.model.StateScreenModel
|
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||||
import cafe.adriel.voyager.core.model.screenModelScope
|
import cafe.adriel.voyager.core.model.screenModelScope
|
||||||
import dev.icerock.moko.resources.StringResource
|
import dev.icerock.moko.resources.StringResource
|
||||||
import eu.kanade.domain.extension.interactor.CreateExtensionRepo
|
|
||||||
import eu.kanade.domain.extension.interactor.DeleteExtensionRepo
|
|
||||||
import eu.kanade.domain.extension.interactor.GetExtensionRepos
|
|
||||||
import kotlinx.collections.immutable.ImmutableSet
|
import kotlinx.collections.immutable.ImmutableSet
|
||||||
import kotlinx.collections.immutable.toImmutableSet
|
import kotlinx.collections.immutable.toImmutableSet
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.flow.receiveAsFlow
|
import kotlinx.coroutines.flow.receiveAsFlow
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
|
import mihon.domain.extensionrepo.interactor.CreateExtensionRepo
|
||||||
|
import mihon.domain.extensionrepo.interactor.DeleteExtensionRepo
|
||||||
|
import mihon.domain.extensionrepo.interactor.GetExtensionRepo
|
||||||
|
import mihon.domain.extensionrepo.interactor.ReplaceExtensionRepo
|
||||||
|
import mihon.domain.extensionrepo.interactor.UpdateExtensionRepo
|
||||||
|
import mihon.domain.extensionrepo.model.ExtensionRepo
|
||||||
import tachiyomi.core.common.util.lang.launchIO
|
import tachiyomi.core.common.util.lang.launchIO
|
||||||
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
|
||||||
|
|
||||||
class ExtensionReposScreenModel(
|
class ExtensionReposScreenModel(
|
||||||
private val getExtensionRepos: GetExtensionRepos = Injekt.get(),
|
private val getExtensionRepo: GetExtensionRepo = Injekt.get(),
|
||||||
private val createExtensionRepo: CreateExtensionRepo = Injekt.get(),
|
private val createExtensionRepo: CreateExtensionRepo = Injekt.get(),
|
||||||
private val deleteExtensionRepo: DeleteExtensionRepo = Injekt.get(),
|
private val deleteExtensionRepo: DeleteExtensionRepo = Injekt.get(),
|
||||||
|
private val replaceExtensionRepo: ReplaceExtensionRepo = Injekt.get(),
|
||||||
|
private val updateExtensionRepo: UpdateExtensionRepo = Injekt.get(),
|
||||||
) : StateScreenModel<RepoScreenState>(RepoScreenState.Loading) {
|
) : StateScreenModel<RepoScreenState>(RepoScreenState.Loading) {
|
||||||
|
|
||||||
private val _events: Channel<RepoEvent> = Channel(Int.MAX_VALUE)
|
private val _events: Channel<RepoEvent> = Channel(Int.MAX_VALUE)
|
||||||
@@ -29,7 +34,7 @@ class ExtensionReposScreenModel(
|
|||||||
|
|
||||||
init {
|
init {
|
||||||
screenModelScope.launchIO {
|
screenModelScope.launchIO {
|
||||||
getExtensionRepos.subscribe()
|
getExtensionRepo.subscribeAll()
|
||||||
.collectLatest { repos ->
|
.collectLatest { repos ->
|
||||||
mutableState.update {
|
mutableState.update {
|
||||||
RepoScreenState.Success(
|
RepoScreenState.Success(
|
||||||
@@ -43,25 +48,51 @@ class ExtensionReposScreenModel(
|
|||||||
/**
|
/**
|
||||||
* Creates and adds a new repo to the database.
|
* Creates and adds a new repo to the database.
|
||||||
*
|
*
|
||||||
* @param name The name of the repo to create.
|
* @param baseUrl The baseUrl of the repo to create.
|
||||||
*/
|
*/
|
||||||
fun createRepo(name: String) {
|
fun createRepo(baseUrl: String) {
|
||||||
screenModelScope.launchIO {
|
screenModelScope.launchIO {
|
||||||
when (createExtensionRepo.await(name)) {
|
when (val result = createExtensionRepo.await(baseUrl)) {
|
||||||
is CreateExtensionRepo.Result.InvalidUrl -> _events.send(RepoEvent.InvalidUrl)
|
CreateExtensionRepo.Result.InvalidUrl -> _events.send(RepoEvent.InvalidUrl)
|
||||||
|
CreateExtensionRepo.Result.RepoAlreadyExists -> _events.send(RepoEvent.RepoAlreadyExists)
|
||||||
|
is CreateExtensionRepo.Result.DuplicateFingerprint -> {
|
||||||
|
showDialog(RepoDialog.Conflict(result.oldRepo, result.newRepo))
|
||||||
|
}
|
||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes the given repo from the database.
|
* Inserts a repo to the database, replace a matching repo with the same signing key fingerprint if found.
|
||||||
*
|
*
|
||||||
* @param repo The repo to delete.
|
* @param newRepo The repo to insert
|
||||||
*/
|
*/
|
||||||
fun deleteRepo(repo: String) {
|
fun replaceRepo(newRepo: ExtensionRepo) {
|
||||||
screenModelScope.launchIO {
|
screenModelScope.launchIO {
|
||||||
deleteExtensionRepo.await(repo)
|
replaceExtensionRepo.await(newRepo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refreshes information for each repository.
|
||||||
|
*/
|
||||||
|
fun refreshRepos() {
|
||||||
|
val status = state.value
|
||||||
|
|
||||||
|
if (status is RepoScreenState.Success) {
|
||||||
|
screenModelScope.launchIO {
|
||||||
|
updateExtensionRepo.awaitAll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes the given repo from the database
|
||||||
|
*/
|
||||||
|
fun deleteRepo(baseUrl: String) {
|
||||||
|
screenModelScope.launchIO {
|
||||||
|
deleteExtensionRepo.await(baseUrl)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,11 +118,13 @@ class ExtensionReposScreenModel(
|
|||||||
sealed class RepoEvent {
|
sealed class RepoEvent {
|
||||||
sealed class LocalizedMessage(val stringRes: StringResource) : RepoEvent()
|
sealed class LocalizedMessage(val stringRes: StringResource) : RepoEvent()
|
||||||
data object InvalidUrl : LocalizedMessage(MR.strings.invalid_repo_name)
|
data object InvalidUrl : LocalizedMessage(MR.strings.invalid_repo_name)
|
||||||
|
data object RepoAlreadyExists : LocalizedMessage(MR.strings.error_repo_exists)
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed class RepoDialog {
|
sealed class RepoDialog {
|
||||||
data object Create : RepoDialog()
|
data object Create : RepoDialog()
|
||||||
data class Delete(val repo: String) : RepoDialog()
|
data class Delete(val repo: String) : RepoDialog()
|
||||||
|
data class Conflict(val oldRepo: ExtensionRepo, val newRepo: ExtensionRepo) : RepoDialog()
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed class RepoScreenState {
|
sealed class RepoScreenState {
|
||||||
@@ -101,7 +134,8 @@ sealed class RepoScreenState {
|
|||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
data class Success(
|
data class Success(
|
||||||
val repos: ImmutableSet<String>,
|
val repos: ImmutableSet<ExtensionRepo>,
|
||||||
|
val oldRepos: ImmutableSet<String>? = null,
|
||||||
val dialog: RepoDialog? = null,
|
val dialog: RepoDialog? = null,
|
||||||
) : RepoScreenState() {
|
) : RepoScreenState() {
|
||||||
|
|
||||||
|
|||||||
+21
-5
@@ -9,6 +9,7 @@ import androidx.compose.foundation.lazy.LazyColumn
|
|||||||
import androidx.compose.foundation.lazy.LazyListState
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.outlined.Label
|
import androidx.compose.material.icons.automirrored.outlined.Label
|
||||||
|
import androidx.compose.material.icons.automirrored.outlined.OpenInNew
|
||||||
import androidx.compose.material.icons.outlined.ContentCopy
|
import androidx.compose.material.icons.outlined.ContentCopy
|
||||||
import androidx.compose.material.icons.outlined.Delete
|
import androidx.compose.material.icons.outlined.Delete
|
||||||
import androidx.compose.material3.ElevatedCard
|
import androidx.compose.material3.ElevatedCard
|
||||||
@@ -22,15 +23,17 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||||
import kotlinx.collections.immutable.ImmutableSet
|
import kotlinx.collections.immutable.ImmutableSet
|
||||||
|
import mihon.domain.extensionrepo.model.ExtensionRepo
|
||||||
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
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ExtensionReposContent(
|
fun ExtensionReposContent(
|
||||||
repos: ImmutableSet<String>,
|
repos: ImmutableSet<ExtensionRepo>,
|
||||||
lazyListState: LazyListState,
|
lazyListState: LazyListState,
|
||||||
paddingValues: PaddingValues,
|
paddingValues: PaddingValues,
|
||||||
|
onOpenWebsite: (ExtensionRepo) -> Unit,
|
||||||
onClickDelete: (String) -> Unit,
|
onClickDelete: (String) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
@@ -45,7 +48,8 @@ fun ExtensionReposContent(
|
|||||||
ExtensionRepoListItem(
|
ExtensionRepoListItem(
|
||||||
modifier = Modifier.animateItemPlacement(),
|
modifier = Modifier.animateItemPlacement(),
|
||||||
repo = it,
|
repo = it,
|
||||||
onDelete = { onClickDelete(it) },
|
onOpenWebsite = { onOpenWebsite(it) },
|
||||||
|
onDelete = { onClickDelete(it.baseUrl) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -54,7 +58,8 @@ fun ExtensionReposContent(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ExtensionRepoListItem(
|
private fun ExtensionRepoListItem(
|
||||||
repo: String,
|
repo: ExtensionRepo,
|
||||||
|
onOpenWebsite: () -> Unit,
|
||||||
onDelete: () -> Unit,
|
onDelete: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
@@ -74,16 +79,27 @@ private fun ExtensionRepoListItem(
|
|||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
Icon(imageVector = Icons.AutoMirrored.Outlined.Label, contentDescription = null)
|
Icon(imageVector = Icons.AutoMirrored.Outlined.Label, contentDescription = null)
|
||||||
Text(text = repo, modifier = Modifier.padding(start = MaterialTheme.padding.medium))
|
Text(
|
||||||
|
text = repo.name,
|
||||||
|
modifier = Modifier.padding(start = MaterialTheme.padding.medium),
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.End,
|
horizontalArrangement = Arrangement.End,
|
||||||
) {
|
) {
|
||||||
|
IconButton(onClick = onOpenWebsite) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.AutoMirrored.Outlined.OpenInNew,
|
||||||
|
contentDescription = stringResource(MR.strings.action_open_in_browser),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
val url = "$repo/index.min.json"
|
val url = "${repo.baseUrl}/index.min.json"
|
||||||
context.copyToClipboard(url, url)
|
context.copyToClipboard(url, url)
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
|
|||||||
+39
-2
@@ -1,6 +1,7 @@
|
|||||||
package eu.kanade.presentation.more.settings.screen.browse.components
|
package eu.kanade.presentation.more.settings.screen.browse.components
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
@@ -14,8 +15,10 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
import kotlinx.collections.immutable.ImmutableSet
|
import kotlinx.collections.immutable.ImmutableSet
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
import mihon.domain.extensionrepo.model.ExtensionRepo
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
@@ -24,12 +27,12 @@ import kotlin.time.Duration.Companion.seconds
|
|||||||
fun ExtensionRepoCreateDialog(
|
fun ExtensionRepoCreateDialog(
|
||||||
onDismissRequest: () -> Unit,
|
onDismissRequest: () -> Unit,
|
||||||
onCreate: (String) -> Unit,
|
onCreate: (String) -> Unit,
|
||||||
repos: ImmutableSet<String>,
|
repoUrls: ImmutableSet<String>,
|
||||||
) {
|
) {
|
||||||
var name by remember { mutableStateOf("") }
|
var name by remember { mutableStateOf("") }
|
||||||
|
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
val nameAlreadyExists = remember(name) { repos.contains(name) }
|
val nameAlreadyExists = remember(name) { repoUrls.contains(name) }
|
||||||
|
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
@@ -73,6 +76,7 @@ fun ExtensionRepoCreateDialog(
|
|||||||
Text(text = stringResource(msgRes))
|
Text(text = stringResource(msgRes))
|
||||||
},
|
},
|
||||||
isError = name.isNotEmpty() && nameAlreadyExists,
|
isError = name.isNotEmpty() && nameAlreadyExists,
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Uri),
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -115,3 +119,36 @@ fun ExtensionRepoDeleteDialog(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ExtensionRepoConflictDialog(
|
||||||
|
oldRepo: ExtensionRepo,
|
||||||
|
newRepo: ExtensionRepo,
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
onMigrate: () -> Unit,
|
||||||
|
) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
onMigrate()
|
||||||
|
onDismissRequest()
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(MR.strings.action_replace_repo))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = onDismissRequest) {
|
||||||
|
Text(text = stringResource(MR.strings.action_cancel))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
title = {
|
||||||
|
Text(text = stringResource(MR.strings.action_replace_repo_title))
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
Text(text = stringResource(MR.strings.action_replace_repo_message, newRepo.name, oldRepo.name))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
+16
@@ -5,12 +5,17 @@ package eu.kanade.presentation.more.settings.screen.browse.components
|
|||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.Refresh
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import eu.kanade.presentation.category.components.CategoryFloatingActionButton
|
import eu.kanade.presentation.category.components.CategoryFloatingActionButton
|
||||||
import eu.kanade.presentation.components.AppBar
|
import eu.kanade.presentation.components.AppBar
|
||||||
import eu.kanade.presentation.more.settings.screen.browse.RepoScreenState
|
import eu.kanade.presentation.more.settings.screen.browse.RepoScreenState
|
||||||
|
import mihon.domain.extensionrepo.model.ExtensionRepo
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.components.material.Scaffold
|
import tachiyomi.presentation.core.components.material.Scaffold
|
||||||
import tachiyomi.presentation.core.components.material.padding
|
import tachiyomi.presentation.core.components.material.padding
|
||||||
@@ -23,7 +28,9 @@ import tachiyomi.presentation.core.util.plus
|
|||||||
fun ExtensionReposScreen(
|
fun ExtensionReposScreen(
|
||||||
state: RepoScreenState.Success,
|
state: RepoScreenState.Success,
|
||||||
onClickCreate: () -> Unit,
|
onClickCreate: () -> Unit,
|
||||||
|
onOpenWebsite: (ExtensionRepo) -> Unit,
|
||||||
onClickDelete: (String) -> Unit,
|
onClickDelete: (String) -> Unit,
|
||||||
|
onClickRefresh: () -> Unit,
|
||||||
navigateUp: () -> Unit,
|
navigateUp: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val lazyListState = rememberLazyListState()
|
val lazyListState = rememberLazyListState()
|
||||||
@@ -33,6 +40,14 @@ fun ExtensionReposScreen(
|
|||||||
navigateUp = navigateUp,
|
navigateUp = navigateUp,
|
||||||
title = stringResource(MR.strings.label_extension_repos),
|
title = stringResource(MR.strings.label_extension_repos),
|
||||||
scrollBehavior = scrollBehavior,
|
scrollBehavior = scrollBehavior,
|
||||||
|
actions = {
|
||||||
|
IconButton(onClick = onClickRefresh) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.Refresh,
|
||||||
|
contentDescription = stringResource(resource = MR.strings.action_webview_refresh),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
@@ -55,6 +70,7 @@ fun ExtensionReposScreen(
|
|||||||
lazyListState = lazyListState,
|
lazyListState = lazyListState,
|
||||||
paddingValues = paddingValues + topSmallPaddingValues +
|
paddingValues = paddingValues + topSmallPaddingValues +
|
||||||
PaddingValues(horizontal = MaterialTheme.padding.medium),
|
PaddingValues(horizontal = MaterialTheme.padding.medium),
|
||||||
|
onOpenWebsite = onOpenWebsite,
|
||||||
onClickDelete = onClickDelete,
|
onClickDelete = onClickDelete,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-4
@@ -20,6 +20,7 @@ import eu.kanade.tachiyomi.util.system.toast
|
|||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
|
import tachiyomi.i18n.sy.SYMR
|
||||||
import tachiyomi.presentation.core.components.LabeledCheckbox
|
import tachiyomi.presentation.core.components.LabeledCheckbox
|
||||||
import tachiyomi.presentation.core.components.LazyColumnWithAction
|
import tachiyomi.presentation.core.components.LazyColumnWithAction
|
||||||
import tachiyomi.presentation.core.components.SectionCard
|
import tachiyomi.presentation.core.components.SectionCard
|
||||||
@@ -39,7 +40,7 @@ class SyncSettingsSelector : Screen() {
|
|||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
AppBar(
|
AppBar(
|
||||||
title = stringResource(MR.strings.pref_choose_what_to_sync),
|
title = stringResource(SYMR.strings.pref_choose_what_to_sync),
|
||||||
navigateUp = navigator::pop,
|
navigateUp = navigator::pop,
|
||||||
scrollBehavior = it,
|
scrollBehavior = it,
|
||||||
)
|
)
|
||||||
@@ -47,14 +48,14 @@ class SyncSettingsSelector : Screen() {
|
|||||||
) { contentPadding ->
|
) { contentPadding ->
|
||||||
LazyColumnWithAction(
|
LazyColumnWithAction(
|
||||||
contentPadding = contentPadding,
|
contentPadding = contentPadding,
|
||||||
actionLabel = stringResource(MR.strings.label_sync),
|
actionLabel = stringResource(SYMR.strings.label_sync),
|
||||||
actionEnabled = state.options.anyEnabled(),
|
actionEnabled = state.options.anyEnabled(),
|
||||||
onClickAction = {
|
onClickAction = {
|
||||||
if (!SyncDataJob.isAnyJobRunning(context)) {
|
if (!SyncDataJob.isRunning(context)) {
|
||||||
model.syncNow(context)
|
model.syncNow(context)
|
||||||
navigator.pop()
|
navigator.pop()
|
||||||
} else {
|
} else {
|
||||||
context.toast(MR.strings.sync_in_progress)
|
context.toast(SYMR.strings.sync_in_progress)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
@@ -123,6 +124,11 @@ private class SyncSettingsSelectorModel(
|
|||||||
appSettings = syncSettings.appSettings,
|
appSettings = syncSettings.appSettings,
|
||||||
sourceSettings = syncSettings.sourceSettings,
|
sourceSettings = syncSettings.sourceSettings,
|
||||||
privateSettings = syncSettings.privateSettings,
|
privateSettings = syncSettings.privateSettings,
|
||||||
|
|
||||||
|
// SY -->
|
||||||
|
customInfo = syncSettings.customInfo,
|
||||||
|
readEntries = syncSettings.readEntries,
|
||||||
|
// SY <--
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,6 +142,11 @@ private class SyncSettingsSelectorModel(
|
|||||||
appSettings = backupOptions.appSettings,
|
appSettings = backupOptions.appSettings,
|
||||||
sourceSettings = backupOptions.sourceSettings,
|
sourceSettings = backupOptions.sourceSettings,
|
||||||
privateSettings = backupOptions.privateSettings,
|
privateSettings = backupOptions.privateSettings,
|
||||||
|
|
||||||
|
// SY -->
|
||||||
|
customInfo = backupOptions.customInfo,
|
||||||
|
readEntries = backupOptions.readEntries,
|
||||||
|
// SY <--
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-3
@@ -15,6 +15,7 @@ import eu.kanade.tachiyomi.data.sync.models.SyncTriggerOptions
|
|||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
|
import tachiyomi.i18n.sy.SYMR
|
||||||
import tachiyomi.presentation.core.components.LabeledCheckbox
|
import tachiyomi.presentation.core.components.LabeledCheckbox
|
||||||
import tachiyomi.presentation.core.components.LazyColumnWithAction
|
import tachiyomi.presentation.core.components.LazyColumnWithAction
|
||||||
import tachiyomi.presentation.core.components.SectionCard
|
import tachiyomi.presentation.core.components.SectionCard
|
||||||
@@ -34,7 +35,7 @@ class SyncTriggerOptionsScreen : Screen() {
|
|||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
AppBar(
|
AppBar(
|
||||||
title = stringResource(MR.strings.pref_sync_options),
|
title = stringResource(SYMR.strings.pref_sync_options),
|
||||||
navigateUp = navigator::pop,
|
navigateUp = navigator::pop,
|
||||||
scrollBehavior = it,
|
scrollBehavior = it,
|
||||||
)
|
)
|
||||||
@@ -43,13 +44,13 @@ class SyncTriggerOptionsScreen : Screen() {
|
|||||||
LazyColumnWithAction(
|
LazyColumnWithAction(
|
||||||
contentPadding = contentPadding,
|
contentPadding = contentPadding,
|
||||||
actionLabel = stringResource(MR.strings.action_save),
|
actionLabel = stringResource(MR.strings.action_save),
|
||||||
actionEnabled = state.options.anyEnabled(),
|
actionEnabled = true,
|
||||||
onClickAction = {
|
onClickAction = {
|
||||||
navigator.pop()
|
navigator.pop()
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
item {
|
item {
|
||||||
SectionCard(MR.strings.label_triggers) {
|
SectionCard(SYMR.strings.label_triggers) {
|
||||||
Options(SyncTriggerOptions.mainOptions, state, model)
|
Options(SyncTriggerOptions.mainOptions, state, model)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-2
@@ -223,13 +223,12 @@ fun AppThemePreviewItem(
|
|||||||
contentAlignment = Alignment.BottomCenter,
|
contentAlignment = Alignment.BottomCenter,
|
||||||
) {
|
) {
|
||||||
Surface(
|
Surface(
|
||||||
tonalElevation = 3.dp,
|
color = MaterialTheme.colorScheme.surfaceContainer,
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.height(32.dp)
|
.height(32.dp)
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.background(MaterialTheme.colorScheme.surfaceVariant)
|
|
||||||
.padding(horizontal = 8.dp),
|
.padding(horizontal = 8.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -43,9 +43,7 @@ fun ReaderPageActionsDialog(
|
|||||||
var useExtraPage by remember { mutableStateOf(false) }
|
var useExtraPage by remember { mutableStateOf(false) }
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
AdaptiveSheet(
|
AdaptiveSheet(onDismissRequest = onDismissRequest) {
|
||||||
onDismissRequest = onDismissRequest,
|
|
||||||
) {
|
|
||||||
Column(modifier = Modifier.padding(vertical = 16.dp)) {
|
Column(modifier = Modifier.padding(vertical = 16.dp)) {
|
||||||
Row(
|
Row(
|
||||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
||||||
|
|||||||
@@ -8,6 +8,12 @@ internal abstract class BaseColorScheme {
|
|||||||
abstract val darkScheme: ColorScheme
|
abstract val darkScheme: ColorScheme
|
||||||
abstract val lightScheme: ColorScheme
|
abstract val lightScheme: ColorScheme
|
||||||
|
|
||||||
|
// Cannot be pure black as there's content scrolling behind it
|
||||||
|
// https://m3.material.io/components/navigation-bar/guidelines#90615a71-607e-485e-9e09-778bfc080563
|
||||||
|
private val surfaceContainer = Color(0xFF0C0C0C)
|
||||||
|
private val surfaceContainerHigh = Color(0xFF131313)
|
||||||
|
private val surfaceContainerHighest = Color(0xFF1B1B1B)
|
||||||
|
|
||||||
fun getColorScheme(isDark: Boolean, isAmoled: Boolean): ColorScheme {
|
fun getColorScheme(isDark: Boolean, isAmoled: Boolean): ColorScheme {
|
||||||
if (!isDark) return lightScheme
|
if (!isDark) return lightScheme
|
||||||
|
|
||||||
@@ -18,6 +24,12 @@ internal abstract class BaseColorScheme {
|
|||||||
onBackground = Color.White,
|
onBackground = Color.White,
|
||||||
surface = Color.Black,
|
surface = Color.Black,
|
||||||
onSurface = Color.White,
|
onSurface = Color.White,
|
||||||
|
surfaceVariant = surfaceContainer, // Navigation bar background (ThemePrefWidget)
|
||||||
|
surfaceContainerLowest = surfaceContainer,
|
||||||
|
surfaceContainerLow = surfaceContainer,
|
||||||
|
surfaceContainer = surfaceContainer, // Navigation bar background
|
||||||
|
surfaceContainerHigh = surfaceContainerHigh,
|
||||||
|
surfaceContainerHighest = surfaceContainerHighest,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+67
-43
@@ -19,53 +19,77 @@ internal object GreenAppleColorScheme : BaseColorScheme() {
|
|||||||
|
|
||||||
override val darkScheme = darkColorScheme(
|
override val darkScheme = darkColorScheme(
|
||||||
primary = Color(0xFF7ADB8F),
|
primary = Color(0xFF7ADB8F),
|
||||||
onPrimary = Color(0xFF003915),
|
onPrimary = Color(0xFF003917),
|
||||||
primaryContainer = Color(0xFF005322),
|
primaryContainer = Color(0xFF017737),
|
||||||
onPrimaryContainer = Color(0xFF96F8A9),
|
onPrimaryContainer = Color(0xFFFFFFFF),
|
||||||
inversePrimary = Color(0xFF006D2F),
|
secondary = Color(0xFF7ADB8F), // Unread badge
|
||||||
secondary = Color(0xFF7ADB8F),
|
onSecondary = Color(0xFF003917), // Unread badge text
|
||||||
onSecondary = Color(0xFF003915),
|
secondaryContainer = Color(0xFF017737), // Navigation bar selector pill & progress indicator (remaining)
|
||||||
secondaryContainer = Color(0xFF005322),
|
onSecondaryContainer = Color(0xFFFFFFFF), // Navigation bar selected icon
|
||||||
onSecondaryContainer = Color(0xFF96F8A9),
|
tertiary = Color(0xFFFFB3AC), // Downloaded badge
|
||||||
tertiary = Color(0xFFFFB3AA),
|
onTertiary = Color(0xFF680008), // Downloaded badge text
|
||||||
onTertiary = Color(0xFF680006),
|
tertiaryContainer = Color(0xFFC7282A),
|
||||||
tertiaryContainer = Color(0xFF93000D),
|
onTertiaryContainer = Color(0xFFFFFFFF),
|
||||||
onTertiaryContainer = Color(0xFFFFDAD5),
|
error = Color(0xFFFFB4AB),
|
||||||
background = Color(0xFF1A1C19),
|
onError = Color(0xFF690005),
|
||||||
onBackground = Color(0xFFE1E3DD),
|
errorContainer = Color(0xFF93000A),
|
||||||
surface = Color(0xFF1A1C19),
|
onErrorContainer = Color(0xFFFFDAD6),
|
||||||
onSurface = Color(0xFFE1E3DD),
|
background = Color(0xFF0F1510),
|
||||||
surfaceVariant = Color(0xFF414941),
|
onBackground = Color(0xFFDFE4DB),
|
||||||
onSurfaceVariant = Color(0xFFC1C8BE),
|
surface = Color(0xFF0F1510),
|
||||||
surfaceTint = Color(0xFF7ADB8F),
|
onSurface = Color(0xFFDFE4DB),
|
||||||
inverseSurface = Color(0xFFE1E3DD),
|
surfaceVariant = Color(0xFF3F493F), // Navigation bar background (ThemePrefWidget)
|
||||||
inverseOnSurface = Color(0xFF1A1C19),
|
onSurfaceVariant = Color(0xFFBECABC),
|
||||||
outline = Color(0xFF8B9389),
|
outline = Color(0xFF889487),
|
||||||
|
outlineVariant = Color(0xFF3F493F),
|
||||||
|
scrim = Color(0xFF000000),
|
||||||
|
inverseSurface = Color(0xFFDFE4DB),
|
||||||
|
inverseOnSurface = Color(0xFF2C322C),
|
||||||
|
inversePrimary = Color(0xFF006D32),
|
||||||
|
surfaceDim = Color(0xFF0F1510),
|
||||||
|
surfaceBright = Color(0xFF353B35),
|
||||||
|
surfaceContainerLowest = Color(0xFF0A0F0B),
|
||||||
|
surfaceContainerLow = Color(0xFF181D18),
|
||||||
|
surfaceContainer = Color(0xFF1C211C), // Navigation bar background
|
||||||
|
surfaceContainerHigh = Color(0xFF262B26),
|
||||||
|
surfaceContainerHighest = Color(0xFF313630),
|
||||||
)
|
)
|
||||||
|
|
||||||
override val lightScheme = lightColorScheme(
|
override val lightScheme = lightColorScheme(
|
||||||
primary = Color(0xFF006D2F),
|
primary = Color(0xFF005927),
|
||||||
onPrimary = Color(0xFFFFFFFF),
|
onPrimary = Color(0xFFFFFFFF),
|
||||||
primaryContainer = Color(0xFF96F8A9),
|
primaryContainer = Color(0xFF188140),
|
||||||
onPrimaryContainer = Color(0xFF002109),
|
onPrimaryContainer = Color(0xFFFFFFFF),
|
||||||
|
secondary = Color(0xFF005927), // Unread badge
|
||||||
|
onSecondary = Color(0xFFFFFFFF), // Unread badge text
|
||||||
|
secondaryContainer = Color(0xFF97f7a9), // Navigation bar selector pill & progress indicator (remaining)
|
||||||
|
onSecondaryContainer = Color(0xFF000000), // Navigation bar selected icon
|
||||||
|
tertiary = Color(0xFF9D0012), // Downloaded badge
|
||||||
|
onTertiary = Color(0xFFFFFFFF), // Downloaded badge text
|
||||||
|
tertiaryContainer = Color(0xFFD33131),
|
||||||
|
onTertiaryContainer = Color(0xFFFFFFFF),
|
||||||
|
error = Color(0xFFBA1A1A),
|
||||||
|
onError = Color(0xFFFFFFFF),
|
||||||
|
errorContainer = Color(0xFFFFDAD6),
|
||||||
|
onErrorContainer = Color(0xFF410002),
|
||||||
|
background = Color(0xFFF6FBF2),
|
||||||
|
onBackground = Color(0xFF181D18),
|
||||||
|
surface = Color(0xFFF6FBF2),
|
||||||
|
onSurface = Color(0xFF181D18),
|
||||||
|
surfaceVariant = Color(0xFFDAE6D7), // Navigation bar background (ThemePrefWidget)
|
||||||
|
onSurfaceVariant = Color(0xFF3F493F),
|
||||||
|
outline = Color(0xFF6F7A6E),
|
||||||
|
outlineVariant = Color(0xFFBECABC),
|
||||||
|
scrim = Color(0xFF000000),
|
||||||
|
inverseSurface = Color(0xFF2C322C),
|
||||||
|
inverseOnSurface = Color(0xFFEDF2E9),
|
||||||
inversePrimary = Color(0xFF7ADB8F),
|
inversePrimary = Color(0xFF7ADB8F),
|
||||||
secondary = Color(0xFF006D2F),
|
surfaceDim = Color(0xFFD6DCD3),
|
||||||
onSecondary = Color(0xFFFFFFFF),
|
surfaceBright = Color(0xFFF6FBF2),
|
||||||
secondaryContainer = Color(0xFF96F8A9),
|
surfaceContainerLowest = Color(0xFFFFFFFF),
|
||||||
onSecondaryContainer = Color(0xFF002109),
|
surfaceContainerLow = Color(0xFFF0F5EC),
|
||||||
tertiary = Color(0xFFB91D22),
|
surfaceContainer = Color(0xFFEAEFE6), // Navigation bar background
|
||||||
onTertiary = Color(0xFFFFFFFF),
|
surfaceContainerHigh = Color(0xFFE4EAE1),
|
||||||
tertiaryContainer = Color(0xFFFFDAD5),
|
surfaceContainerHighest = Color(0xFFDFE4DB),
|
||||||
onTertiaryContainer = Color(0xFF410003),
|
|
||||||
background = Color(0xFFFBFDF7),
|
|
||||||
onBackground = Color(0xFF1A1C19),
|
|
||||||
surface = Color(0xFFFBFDF7),
|
|
||||||
onSurface = Color(0xFF1A1C19),
|
|
||||||
surfaceVariant = Color(0xFFDDE5DA),
|
|
||||||
onSurfaceVariant = Color(0xFF414941),
|
|
||||||
surfaceTint = Color(0xFF006D2F),
|
|
||||||
inverseSurface = Color(0xFF2F312E),
|
|
||||||
inverseOnSurface = Color(0xFFF0F2EC),
|
|
||||||
outline = Color(0xFF717970),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,53 +18,77 @@ internal object LavenderColorScheme : BaseColorScheme() {
|
|||||||
|
|
||||||
override val darkScheme = darkColorScheme(
|
override val darkScheme = darkColorScheme(
|
||||||
primary = Color(0xFFA177FF),
|
primary = Color(0xFFA177FF),
|
||||||
onPrimary = Color(0xFF111129),
|
onPrimary = Color(0xFF3D0090),
|
||||||
primaryContainer = Color(0xFFA177FF),
|
primaryContainer = Color(0xFFA177FF),
|
||||||
onPrimaryContainer = Color(0xFF111129),
|
onPrimaryContainer = Color(0xFFFFFFFF),
|
||||||
inversePrimary = Color(0xFF006D2F),
|
secondary = Color(0xFFA177FF), // Unread badge
|
||||||
secondary = Color(0xFFA177FF),
|
onSecondary = Color(0xFFFFFFFF), // Unread badge text
|
||||||
onSecondary = Color(0xFF111129),
|
secondaryContainer = Color(0xFF423271), // Navigation bar selector pill & progress indicator (remaining)
|
||||||
secondaryContainer = Color(0xFFA177FF),
|
onSecondaryContainer = Color(0xFFA177FF), // Navigation bar selected icon
|
||||||
onSecondaryContainer = Color(0xFF111129),
|
tertiary = Color(0xFFCDBDFF), // Downloaded badge
|
||||||
tertiary = Color(0xFF5E25E1),
|
onTertiary = Color(0xFF360096), // Downloaded badge text
|
||||||
onTertiary = Color(0xFFE8E8E8),
|
tertiaryContainer = Color(0xFF5512D8),
|
||||||
tertiaryContainer = Color(0xFF111129),
|
onTertiaryContainer = Color(0xFFEFE6FF),
|
||||||
onTertiaryContainer = Color(0xFFDEE8FF),
|
error = Color(0xFFFFB4AB),
|
||||||
|
onError = Color(0xFF690005),
|
||||||
|
errorContainer = Color(0xFF93000A),
|
||||||
|
onErrorContainer = Color(0xFFFFDAD6),
|
||||||
background = Color(0xFF111129),
|
background = Color(0xFF111129),
|
||||||
onBackground = Color(0xFFDEE8FF),
|
onBackground = Color(0xFFE7E0EC),
|
||||||
surface = Color(0xFF111129),
|
surface = Color(0xFF111129),
|
||||||
onSurface = Color(0xFFDEE8FF),
|
onSurface = Color(0xFFE7E0EC),
|
||||||
surfaceVariant = Color(0x2CB6B6B6),
|
surfaceVariant = Color(0xFF3D2F6B), // Navigation bar background (ThemePrefWidget)
|
||||||
onSurfaceVariant = Color(0xFFE8E8E8),
|
onSurfaceVariant = Color(0xFFCBC3D6),
|
||||||
surfaceTint = Color(0xFFA177FF),
|
outline = Color(0xFF958E9F),
|
||||||
inverseSurface = Color(0xFF221247),
|
outlineVariant = Color(0xFF4A4453),
|
||||||
inverseOnSurface = Color(0xFFDEE8FF),
|
scrim = Color(0xFF000000),
|
||||||
outline = Color(0xA8905FFF),
|
inverseSurface = Color(0xFFE7E0EC),
|
||||||
|
inverseOnSurface = Color(0xFF322F38),
|
||||||
|
inversePrimary = Color(0xFF6D41C8),
|
||||||
|
surfaceDim = Color(0xFF111129),
|
||||||
|
surfaceBright = Color(0xFF3B3841),
|
||||||
|
surfaceContainerLowest = Color(0xFF15132d),
|
||||||
|
surfaceContainerLow = Color(0xFF171531),
|
||||||
|
surfaceContainer = Color(0xFF1D193B), // Navigation bar background
|
||||||
|
surfaceContainerHigh = Color(0xFF241f41),
|
||||||
|
surfaceContainerHighest = Color(0xFF282446),
|
||||||
)
|
)
|
||||||
|
|
||||||
override val lightScheme = lightColorScheme(
|
override val lightScheme = lightColorScheme(
|
||||||
primary = Color(0xFF7B46AF),
|
primary = Color(0xFF6D41C8),
|
||||||
onPrimary = Color(0xFFEDE2FF),
|
onPrimary = Color(0xFFFFFFFF),
|
||||||
primaryContainer = Color(0xFF7B46AF),
|
primaryContainer = Color(0xFF7B46AF),
|
||||||
onPrimaryContainer = Color(0xFFEDE2FF),
|
onPrimaryContainer = Color(0xFF130038),
|
||||||
inversePrimary = Color(0xFFD6BAFF),
|
secondary = Color(0xFF7B46AF), // Unread badge
|
||||||
secondary = Color(0xFF7B46AF),
|
onSecondary = Color(0xFFEDE2FF), // Unread badge text
|
||||||
onSecondary = Color(0xFFEDE2FF),
|
secondaryContainer = Color(0xFFC9B0E6), // Navigation bar selector pill & progress indicator (remaining)
|
||||||
secondaryContainer = Color(0xFF7B46AF),
|
onSecondaryContainer = Color(0xFF7B46AF), // Navigation bar selector icon
|
||||||
onSecondaryContainer = Color(0xFFEDE2FF),
|
tertiary = Color(0xFFEDE2FF), // Downloaded badge
|
||||||
tertiary = Color(0xFFEDE2FF),
|
onTertiary = Color(0xFF7B46AF), // Downloaded badge text
|
||||||
onTertiary = Color(0xFF7B46AF),
|
tertiaryContainer = Color(0xFF6D3BF0),
|
||||||
tertiaryContainer = Color(0xFFEDE2FF),
|
onTertiaryContainer = Color(0xFFFFFFFF),
|
||||||
onTertiaryContainer = Color(0xFF7B46AF),
|
error = Color(0xFFBA1A1A),
|
||||||
|
onError = Color(0xFFFFFFFF),
|
||||||
|
errorContainer = Color(0xFFFFDAD6),
|
||||||
|
onErrorContainer = Color(0xFF410002),
|
||||||
background = Color(0xFFEDE2FF),
|
background = Color(0xFFEDE2FF),
|
||||||
onBackground = Color(0xFF1B1B22),
|
onBackground = Color(0xFF1D1A22),
|
||||||
surface = Color(0xFFEDE2FF),
|
surface = Color(0xFFEDE2FF),
|
||||||
onSurface = Color(0xFF1B1B22),
|
onSurface = Color(0xFF1D1A22),
|
||||||
surfaceVariant = Color(0xFFB9B0CC),
|
surfaceVariant = Color(0xFFE4D5F8), // Navigation bar background (ThemePrefWidget)
|
||||||
onSurfaceVariant = Color(0xD849454E),
|
onSurfaceVariant = Color(0xFF4A4453),
|
||||||
surfaceTint = Color(0xFF7B46AF),
|
outline = Color(0xFF7B7485),
|
||||||
inverseSurface = Color(0xFF313033),
|
outlineVariant = Color(0xFFCBC3D6),
|
||||||
inverseOnSurface = Color(0xFFF3EFF4),
|
scrim = Color(0xFF000000),
|
||||||
outline = Color(0xFF7B46AF),
|
inverseSurface = Color(0xFF322F38),
|
||||||
|
inverseOnSurface = Color(0xFFF5EEFA),
|
||||||
|
inversePrimary = Color(0xFFA177FF),
|
||||||
|
surfaceDim = Color(0xFFDED7E3),
|
||||||
|
surfaceBright = Color(0xFFEDE2FF),
|
||||||
|
surfaceContainerLowest = Color(0xFFDACCEC),
|
||||||
|
surfaceContainerLow = Color(0xFFDED0F1),
|
||||||
|
surfaceContainer = Color(0xFFE4D5F8), // Navigation bar background
|
||||||
|
surfaceContainerHigh = Color(0xFFEADCFD),
|
||||||
|
surfaceContainerHighest = Color(0xFFEEE2FF),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+24
-14
@@ -23,24 +23,29 @@ internal object MidnightDuskColorScheme : BaseColorScheme() {
|
|||||||
primaryContainer = Color(0xFFBD1C5C),
|
primaryContainer = Color(0xFFBD1C5C),
|
||||||
onPrimaryContainer = Color(0xFFFFFFFF),
|
onPrimaryContainer = Color(0xFFFFFFFF),
|
||||||
inversePrimary = Color(0xFFF02475),
|
inversePrimary = Color(0xFFF02475),
|
||||||
secondary = Color(0xFFF02475),
|
secondary = Color(0xFFF02475), // Unread badge
|
||||||
onSecondary = Color(0xFFFFFFFF),
|
onSecondary = Color(0xFF16151D), // Unread badge text
|
||||||
secondaryContainer = Color(0xFFF02475),
|
secondaryContainer = Color(0xFF66183C), // Navigation bar selector pill & progress indicator (remaining)
|
||||||
onSecondaryContainer = Color(0xFFFFFFFF),
|
onSecondaryContainer = Color(0xFFF02475), // Navigation bar selector icon
|
||||||
tertiary = Color(0xFF55971C),
|
tertiary = Color(0xFF55971C), // Downloaded badge
|
||||||
onTertiary = Color(0xFFFFFFFF),
|
onTertiary = Color(0xFF16151D), // Downloaded badge text
|
||||||
tertiaryContainer = Color(0xFF386412),
|
tertiaryContainer = Color(0xFF386412),
|
||||||
onTertiaryContainer = Color(0xFFE5E1E5),
|
onTertiaryContainer = Color(0xFFE5E1E5),
|
||||||
background = Color(0xFF16151D),
|
background = Color(0xFF16151D),
|
||||||
onBackground = Color(0xFFE5E1E5),
|
onBackground = Color(0xFFE5E1E5),
|
||||||
surface = Color(0xFF16151D),
|
surface = Color(0xFF16151D),
|
||||||
onSurface = Color(0xFFE5E1E5),
|
onSurface = Color(0xFFE5E1E5),
|
||||||
surfaceVariant = Color(0xFF524346),
|
surfaceVariant = Color(0xFF281624), // Navigation bar background (ThemePrefWidget)
|
||||||
onSurfaceVariant = Color(0xFFD6C1C4),
|
onSurfaceVariant = Color(0xFFD6C1C4),
|
||||||
surfaceTint = Color(0xFFF02475),
|
surfaceTint = Color(0xFFF02475),
|
||||||
inverseSurface = Color(0xFF333043),
|
inverseSurface = Color(0xFF333043),
|
||||||
inverseOnSurface = Color(0xFFFFFFFF),
|
inverseOnSurface = Color(0xFFFFFFFF),
|
||||||
outline = Color(0xFF9F8C8F),
|
outline = Color(0xFF9F8C8F),
|
||||||
|
surfaceContainerLowest = Color(0xFF221320),
|
||||||
|
surfaceContainerLow = Color(0xFF251522),
|
||||||
|
surfaceContainer = Color(0xFF281624), // Navigation bar background
|
||||||
|
surfaceContainerHigh = Color(0xFF2D1C2A),
|
||||||
|
surfaceContainerHighest = Color(0xFF2F1F2C),
|
||||||
)
|
)
|
||||||
|
|
||||||
override val lightScheme = lightColorScheme(
|
override val lightScheme = lightColorScheme(
|
||||||
@@ -49,23 +54,28 @@ internal object MidnightDuskColorScheme : BaseColorScheme() {
|
|||||||
primaryContainer = Color(0xFFFFD9E1),
|
primaryContainer = Color(0xFFFFD9E1),
|
||||||
onPrimaryContainer = Color(0xFF3F0017),
|
onPrimaryContainer = Color(0xFF3F0017),
|
||||||
inversePrimary = Color(0xFFFFB1C4),
|
inversePrimary = Color(0xFFFFB1C4),
|
||||||
secondary = Color(0xFFBB0054),
|
secondary = Color(0xFFBB0054), // Unread badge
|
||||||
onSecondary = Color(0xFFFFFFFF),
|
onSecondary = Color(0xFFFFFFFF), // Unread badge text
|
||||||
secondaryContainer = Color(0xFFFFD9E1),
|
secondaryContainer = Color(0xFFEFBAD4), // Navigation bar selector pill & progress indicator (remaining)
|
||||||
onSecondaryContainer = Color(0xFF3F0017),
|
onSecondaryContainer = Color(0xFFD1377C), // Navigation bar selector icon
|
||||||
tertiary = Color(0xFF006638),
|
tertiary = Color(0xFF006638), // Downloaded badge
|
||||||
onTertiary = Color(0xFFFFFFFF),
|
onTertiary = Color(0xFFFFFFFF), // Downloaded badge text
|
||||||
tertiaryContainer = Color(0xFF00894b),
|
tertiaryContainer = Color(0xFF00894b),
|
||||||
onTertiaryContainer = Color(0xFF2D1600),
|
onTertiaryContainer = Color(0xFF2D1600),
|
||||||
background = Color(0xFFFFFBFF),
|
background = Color(0xFFFFFBFF),
|
||||||
onBackground = Color(0xFF1C1B1F),
|
onBackground = Color(0xFF1C1B1F),
|
||||||
surface = Color(0xFFFFFBFF),
|
surface = Color(0xFFFFFBFF),
|
||||||
onSurface = Color(0xFF1C1B1F),
|
onSurface = Color(0xFF1C1B1F),
|
||||||
surfaceVariant = Color(0xFFF3DDE0),
|
surfaceVariant = Color(0xFFF9E6F1), // Navigation bar background (ThemePrefWidget)
|
||||||
onSurfaceVariant = Color(0xFF524346),
|
onSurfaceVariant = Color(0xFF524346),
|
||||||
surfaceTint = Color(0xFFBB0054),
|
surfaceTint = Color(0xFFBB0054),
|
||||||
inverseSurface = Color(0xFF313033),
|
inverseSurface = Color(0xFF313033),
|
||||||
inverseOnSurface = Color(0xFFF4F0F4),
|
inverseOnSurface = Color(0xFFF4F0F4),
|
||||||
outline = Color(0xFF847376),
|
outline = Color(0xFF847376),
|
||||||
|
surfaceContainerLowest = Color(0xFFDAC0CD),
|
||||||
|
surfaceContainerLow = Color(0xFFE8D1DD),
|
||||||
|
surfaceContainer = Color(0xFFF9E6F1), // Navigation bar background
|
||||||
|
surfaceContainerHigh = Color(0xFFFCF3F8),
|
||||||
|
surfaceContainerHighest = Color(0xFFFEF9FC),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,19 +17,19 @@ internal object NordColorScheme : BaseColorScheme() {
|
|||||||
primaryContainer = Color(0xFF88C0D0),
|
primaryContainer = Color(0xFF88C0D0),
|
||||||
onPrimaryContainer = Color(0xFF2E3440),
|
onPrimaryContainer = Color(0xFF2E3440),
|
||||||
inversePrimary = Color(0xFF397E91),
|
inversePrimary = Color(0xFF397E91),
|
||||||
secondary = Color(0xFF81A1C1),
|
secondary = Color(0xFF81A1C1), // Unread badge
|
||||||
onSecondary = Color(0xFF2E3440),
|
onSecondary = Color(0xFF2E3440), // Unread badge text
|
||||||
secondaryContainer = Color(0xFF81A1C1),
|
secondaryContainer = Color(0xFF81A1C1), // Navigation bar selector pill & progress indicator (remaining)
|
||||||
onSecondaryContainer = Color(0xFF2E3440),
|
onSecondaryContainer = Color(0xFF2E3440), // Navigation bar selector icon
|
||||||
tertiary = Color(0xFF5E81AC),
|
tertiary = Color(0xFF5E81AC), // Downloaded badge
|
||||||
onTertiary = Color(0xFF000000),
|
onTertiary = Color(0xFF000000), // Downloaded badge text
|
||||||
tertiaryContainer = Color(0xFF5E81AC),
|
tertiaryContainer = Color(0xFF5E81AC),
|
||||||
onTertiaryContainer = Color(0xFF000000),
|
onTertiaryContainer = Color(0xFF000000),
|
||||||
background = Color(0xFF2E3440),
|
background = Color(0xFF2E3440),
|
||||||
onBackground = Color(0xFFECEFF4),
|
onBackground = Color(0xFFECEFF4),
|
||||||
surface = Color(0xFF3B4252),
|
surface = Color(0xFF2E3440),
|
||||||
onSurface = Color(0xFFECEFF4),
|
onSurface = Color(0xFFECEFF4),
|
||||||
surfaceVariant = Color(0xFF2E3440),
|
surfaceVariant = Color(0xFF414C5C), // Navigation bar background (ThemePrefWidget)
|
||||||
onSurfaceVariant = Color(0xFFECEFF4),
|
onSurfaceVariant = Color(0xFFECEFF4),
|
||||||
surfaceTint = Color(0xFF88C0D0),
|
surfaceTint = Color(0xFF88C0D0),
|
||||||
inverseSurface = Color(0xFFD8DEE9),
|
inverseSurface = Color(0xFFD8DEE9),
|
||||||
@@ -39,6 +39,11 @@ internal object NordColorScheme : BaseColorScheme() {
|
|||||||
onError = Color(0xFF2E3440),
|
onError = Color(0xFF2E3440),
|
||||||
errorContainer = Color(0xFFBF616A),
|
errorContainer = Color(0xFFBF616A),
|
||||||
onErrorContainer = Color(0xFF000000),
|
onErrorContainer = Color(0xFF000000),
|
||||||
|
surfaceContainerLowest = Color(0xFF373F4D),
|
||||||
|
surfaceContainerLow = Color(0xFF3E4756),
|
||||||
|
surfaceContainer = Color(0xFF414C5C),
|
||||||
|
surfaceContainerHigh = Color(0xFF4E5766),
|
||||||
|
surfaceContainerHighest = Color(0xFF505968), // Navigation bar background
|
||||||
)
|
)
|
||||||
|
|
||||||
override val lightScheme = lightColorScheme(
|
override val lightScheme = lightColorScheme(
|
||||||
@@ -47,19 +52,19 @@ internal object NordColorScheme : BaseColorScheme() {
|
|||||||
primaryContainer = Color(0xFF5E81AC),
|
primaryContainer = Color(0xFF5E81AC),
|
||||||
onPrimaryContainer = Color(0xFF000000),
|
onPrimaryContainer = Color(0xFF000000),
|
||||||
inversePrimary = Color(0xFF8CA8CD),
|
inversePrimary = Color(0xFF8CA8CD),
|
||||||
secondary = Color(0xFF81A1C1),
|
secondary = Color(0xFF81A1C1), // Unread badge
|
||||||
onSecondary = Color(0xFF2E3440),
|
onSecondary = Color(0xFF2E3440), // Unread badge text
|
||||||
secondaryContainer = Color(0xFF81A1C1),
|
secondaryContainer = Color(0xFF81A1C1), // Navigation bar selector pill & progress indicator (remaining)
|
||||||
onSecondaryContainer = Color(0xFF2E3440),
|
onSecondaryContainer = Color(0xFF2E3440), // Navigation bar selector icon
|
||||||
tertiary = Color(0xFF88C0D0),
|
tertiary = Color(0xFF88C0D0), // Downloaded badge
|
||||||
onTertiary = Color(0xFF2E3440),
|
onTertiary = Color(0xFF2E3440), // Downloaded badge text
|
||||||
tertiaryContainer = Color(0xFF88C0D0),
|
tertiaryContainer = Color(0xFF88C0D0),
|
||||||
onTertiaryContainer = Color(0xFF2E3440),
|
onTertiaryContainer = Color(0xFF2E3440),
|
||||||
background = Color(0xFFECEFF4),
|
background = Color(0xFFECEFF4),
|
||||||
onBackground = Color(0xFF2E3440),
|
onBackground = Color(0xFF2E3440),
|
||||||
surface = Color(0xFFE5E9F0),
|
surface = Color(0xFFE5E9F0),
|
||||||
onSurface = Color(0xFF2E3440),
|
onSurface = Color(0xFF2E3440),
|
||||||
surfaceVariant = Color(0xFFffffff),
|
surfaceVariant = Color(0xFFDAE0EA), // Navigation bar background (ThemePrefWidget)
|
||||||
onSurfaceVariant = Color(0xFF2E3440),
|
onSurfaceVariant = Color(0xFF2E3440),
|
||||||
surfaceTint = Color(0xFF5E81AC),
|
surfaceTint = Color(0xFF5E81AC),
|
||||||
inverseSurface = Color(0xFF3B4252),
|
inverseSurface = Color(0xFF3B4252),
|
||||||
@@ -68,5 +73,10 @@ internal object NordColorScheme : BaseColorScheme() {
|
|||||||
onError = Color(0xFFECEFF4),
|
onError = Color(0xFFECEFF4),
|
||||||
errorContainer = Color(0xFFBF616A),
|
errorContainer = Color(0xFFBF616A),
|
||||||
onErrorContainer = Color(0xFF000000),
|
onErrorContainer = Color(0xFF000000),
|
||||||
|
surfaceContainerLowest = Color(0xFFD1D7E0),
|
||||||
|
surfaceContainerLow = Color(0xFFD6DCE6),
|
||||||
|
surfaceContainer = Color(0xFFDAE0EA), // Navigation bar background
|
||||||
|
surfaceContainerHigh = Color(0xFFE9EDF3),
|
||||||
|
surfaceContainerHighest = Color(0xFFF2F4F8),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+67
-43
@@ -18,54 +18,78 @@ import androidx.compose.ui.graphics.Color
|
|||||||
internal object StrawberryColorScheme : BaseColorScheme() {
|
internal object StrawberryColorScheme : BaseColorScheme() {
|
||||||
|
|
||||||
override val darkScheme = darkColorScheme(
|
override val darkScheme = darkColorScheme(
|
||||||
primary = Color(0xFFFFB2B9),
|
primary = Color(0xFFFFB2B8),
|
||||||
onPrimary = Color(0xFF67001B),
|
onPrimary = Color(0xFF67001D),
|
||||||
primaryContainer = Color(0xFF91002A),
|
primaryContainer = Color(0xFFD53855),
|
||||||
onPrimaryContainer = Color(0xFFFFDADD),
|
onPrimaryContainer = Color(0xFFFFFFFF),
|
||||||
inversePrimary = Color(0xFFB61E40),
|
secondary = Color(0xFFED4A65), // Unread badge
|
||||||
secondary = Color(0xFFFFB2B9),
|
onSecondary = Color(0xFF201A1A), // Unread badge text
|
||||||
onSecondary = Color(0xFF67001B),
|
secondaryContainer = Color(0xFF91002A), // Navigation bar selector pill & progress indicator (remaining)
|
||||||
secondaryContainer = Color(0xFF91002A),
|
onSecondaryContainer = Color(0xFFFFFFFF), // Navigation bar selector icon
|
||||||
onSecondaryContainer = Color(0xFFFFDADD),
|
tertiary = Color(0xFFE8C08E), // Downloaded badge
|
||||||
tertiary = Color(0xFFE8C08E),
|
onTertiary = Color(0xFF201A1A), // Downloaded badge text
|
||||||
onTertiary = Color(0xFF432C06),
|
tertiaryContainer = Color(0xFF775930),
|
||||||
tertiaryContainer = Color(0xFF5D421B),
|
onTertiaryContainer = Color(0xFFFFF7F1),
|
||||||
onTertiaryContainer = Color(0xFFFFDDB1),
|
error = Color(0xFFFFB4AB),
|
||||||
|
onError = Color(0xFF690005),
|
||||||
|
errorContainer = Color(0xFF93000A),
|
||||||
|
onErrorContainer = Color(0xFFFFDAD6),
|
||||||
background = Color(0xFF201A1A),
|
background = Color(0xFF201A1A),
|
||||||
onBackground = Color(0xFFECDFDF),
|
onBackground = Color(0xFFF7DCDD),
|
||||||
surface = Color(0xFF201A1A),
|
surface = Color(0xFF201A1A),
|
||||||
onSurface = Color(0xFFECDFDF),
|
onSurface = Color(0xFFF7DCDD),
|
||||||
surfaceVariant = Color(0xFF534344),
|
surfaceVariant = Color(0xFF322727), // Navigation bar background (ThemePrefWidget)
|
||||||
onSurfaceVariant = Color(0xFFD7C1C2),
|
onSurfaceVariant = Color(0xFFE1BEC0),
|
||||||
surfaceTint = Color(0xFFFFB2B9),
|
outline = Color(0xFFA9898B),
|
||||||
inverseSurface = Color(0xFFECDFDF),
|
outlineVariant = Color(0xFF594042),
|
||||||
inverseOnSurface = Color(0xFF201A1A),
|
scrim = Color(0xFF000000),
|
||||||
outline = Color(0xFFA08C8D),
|
inverseSurface = Color(0xFFF7DCDD),
|
||||||
|
inverseOnSurface = Color(0xFF3D2C2D),
|
||||||
|
inversePrimary = Color(0xFFB61F40),
|
||||||
|
surfaceDim = Color(0xFF1D1011),
|
||||||
|
surfaceBright = Color(0xFF463536),
|
||||||
|
surfaceContainerLowest = Color(0xFF2C2222),
|
||||||
|
surfaceContainerLow = Color(0xFF302525),
|
||||||
|
surfaceContainer = Color(0xFF322727), // Navigation bar background
|
||||||
|
surfaceContainerHigh = Color(0xFF3C2F2F),
|
||||||
|
surfaceContainerHighest = Color(0xFF463737),
|
||||||
)
|
)
|
||||||
|
|
||||||
override val lightScheme = lightColorScheme(
|
override val lightScheme = lightColorScheme(
|
||||||
primary = Color(0xFFB61E40),
|
primary = Color(0xFFA10833),
|
||||||
onPrimary = Color(0xFFFFFFFF),
|
onPrimary = Color(0xFFFFFFFF),
|
||||||
primaryContainer = Color(0xFFFFDADD),
|
primaryContainer = Color(0xFFD53855),
|
||||||
onPrimaryContainer = Color(0xFF40000D),
|
onPrimaryContainer = Color(0xFFFFFFFF),
|
||||||
inversePrimary = Color(0xFFFFB2B9),
|
secondary = Color(0xFFA10833), // Unread badge
|
||||||
secondary = Color(0xFFB61E40),
|
onSecondary = Color(0xFFFFFFFF), // Unread badge text
|
||||||
onSecondary = Color(0xFFFFFFFF),
|
secondaryContainer = Color(0xFFD53855), // Navigation bar selector pill & progress indicator (remaining)
|
||||||
secondaryContainer = Color(0xFFFFDADD),
|
onSecondaryContainer = Color(0xFFF6EAED), // Navigation bar selector icon
|
||||||
onSecondaryContainer = Color(0xFF40000D),
|
tertiary = Color(0xFF5F441D), // Downloaded badge
|
||||||
tertiary = Color(0xFF775930),
|
onTertiary = Color(0xFFFFFFFF), // Downloaded badge text
|
||||||
onTertiary = Color(0xFFFFFFFF),
|
tertiaryContainer = Color(0xFF87683D),
|
||||||
tertiaryContainer = Color(0xFFFFDDB1),
|
onTertiaryContainer = Color(0xFFFFFFFF),
|
||||||
onTertiaryContainer = Color(0xFF2A1800),
|
error = Color(0xFFBA1A1A),
|
||||||
background = Color(0xFFFCFCFC),
|
onError = Color(0xFFFFFFFF),
|
||||||
onBackground = Color(0xFF201A1A),
|
errorContainer = Color(0xFFFFDAD6),
|
||||||
surface = Color(0xFFFCFCFC),
|
onErrorContainer = Color(0xFF410002),
|
||||||
onSurface = Color(0xFF201A1A),
|
background = Color(0xFFFAFAFA),
|
||||||
surfaceVariant = Color(0xFFF4DDDD),
|
onBackground = Color(0xFF261819),
|
||||||
onSurfaceVariant = Color(0xFF534344),
|
surface = Color(0xFFFAFAFA),
|
||||||
surfaceTint = Color(0xFFB61E40),
|
onSurface = Color(0xFF261819),
|
||||||
inverseSurface = Color(0xFF362F2F),
|
surfaceVariant = Color(0xFFF6EAED), // Navigation bar background (ThemePrefWidget)
|
||||||
inverseOnSurface = Color(0xFFFBEDED),
|
onSurfaceVariant = Color(0xFF594042),
|
||||||
outline = Color(0xFF857374),
|
outline = Color(0xFF8D7071),
|
||||||
|
outlineVariant = Color(0xFFE1BEC0),
|
||||||
|
scrim = Color(0xFF000000),
|
||||||
|
inverseSurface = Color(0xFF3D2C2D),
|
||||||
|
inverseOnSurface = Color(0xFFFFECED),
|
||||||
|
inversePrimary = Color(0xFFFFB2B8),
|
||||||
|
surfaceDim = Color(0xFFEED4D5),
|
||||||
|
surfaceBright = Color(0xFFFFF8F7),
|
||||||
|
surfaceContainerLowest = Color(0xFFF7DCDD),
|
||||||
|
surfaceContainerLow = Color(0xFFFDE2E3),
|
||||||
|
surfaceContainer = Color(0xFFF6EAED), // Navigation bar background
|
||||||
|
surfaceContainerHigh = Color(0xFFFFF0F0),
|
||||||
|
surfaceContainerHighest = Color(0xFFFFFFFF),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+24
-14
@@ -22,19 +22,19 @@ internal object TachiyomiColorScheme : BaseColorScheme() {
|
|||||||
primaryContainer = Color(0xFF00429B),
|
primaryContainer = Color(0xFF00429B),
|
||||||
onPrimaryContainer = Color(0xFFD9E2FF),
|
onPrimaryContainer = Color(0xFFD9E2FF),
|
||||||
inversePrimary = Color(0xFF0058CA),
|
inversePrimary = Color(0xFF0058CA),
|
||||||
secondary = Color(0xFFB0C6FF),
|
secondary = Color(0xFFB0C6FF), // Unread badge
|
||||||
onSecondary = Color(0xFF002D6E),
|
onSecondary = Color(0xFF002D6E), // Unread badge text
|
||||||
secondaryContainer = Color(0xFF00429B),
|
secondaryContainer = Color(0xFF00429B), // Navigation bar selector pill & pro
|
||||||
onSecondaryContainer = Color(0xFFD9E2FF),
|
onSecondaryContainer = Color(0xFFD9E2FF), // Navigation bar selector icon
|
||||||
tertiary = Color(0xFF7ADC77),
|
tertiary = Color(0xFF7ADC77), // Downloaded badge
|
||||||
onTertiary = Color(0xFF003909),
|
onTertiary = Color(0xFF003909), // Downloaded badge text
|
||||||
tertiaryContainer = Color(0xFF005312),
|
tertiaryContainer = Color(0xFF005312),
|
||||||
onTertiaryContainer = Color(0xFF95F990),
|
onTertiaryContainer = Color(0xFF95F990),
|
||||||
background = Color(0xFF1B1B1F),
|
background = Color(0xFF1B1B1F),
|
||||||
onBackground = Color(0xFFE3E2E6),
|
onBackground = Color(0xFFE3E2E6),
|
||||||
surface = Color(0xFF1B1B1F),
|
surface = Color(0xFF1B1B1F),
|
||||||
onSurface = Color(0xFFE3E2E6),
|
onSurface = Color(0xFFE3E2E6),
|
||||||
surfaceVariant = Color(0xFF44464F),
|
surfaceVariant = Color(0xFF211F26), // Navigation bar background (ThemePrefWidget)
|
||||||
onSurfaceVariant = Color(0xFFC5C6D0),
|
onSurfaceVariant = Color(0xFFC5C6D0),
|
||||||
surfaceTint = Color(0xFFB0C6FF),
|
surfaceTint = Color(0xFFB0C6FF),
|
||||||
inverseSurface = Color(0xFFE3E2E6),
|
inverseSurface = Color(0xFFE3E2E6),
|
||||||
@@ -45,6 +45,11 @@ internal object TachiyomiColorScheme : BaseColorScheme() {
|
|||||||
onErrorContainer = Color(0xFFFFDAD6),
|
onErrorContainer = Color(0xFFFFDAD6),
|
||||||
outline = Color(0xFF8F9099),
|
outline = Color(0xFF8F9099),
|
||||||
outlineVariant = Color(0xFF44464F),
|
outlineVariant = Color(0xFF44464F),
|
||||||
|
surfaceContainerLowest = Color(0xFF1A181D),
|
||||||
|
surfaceContainerLow = Color(0xFF1E1C22),
|
||||||
|
surfaceContainer = Color(0xFF211F26), // Navigation bar background
|
||||||
|
surfaceContainerHigh = Color(0xFF292730),
|
||||||
|
surfaceContainerHighest = Color(0xFF302E38),
|
||||||
)
|
)
|
||||||
|
|
||||||
override val lightScheme = lightColorScheme(
|
override val lightScheme = lightColorScheme(
|
||||||
@@ -53,19 +58,19 @@ internal object TachiyomiColorScheme : BaseColorScheme() {
|
|||||||
primaryContainer = Color(0xFFD9E2FF),
|
primaryContainer = Color(0xFFD9E2FF),
|
||||||
onPrimaryContainer = Color(0xFF001945),
|
onPrimaryContainer = Color(0xFF001945),
|
||||||
inversePrimary = Color(0xFFB0C6FF),
|
inversePrimary = Color(0xFFB0C6FF),
|
||||||
secondary = Color(0xFF0058CA),
|
secondary = Color(0xFF0058CA), // Unread badge
|
||||||
onSecondary = Color(0xFFFFFFFF),
|
onSecondary = Color(0xFFFFFFFF), // Unread badge text
|
||||||
secondaryContainer = Color(0xFFD9E2FF),
|
secondaryContainer = Color(0xFFD9E2FF), // Navigation bar selector pill & progress indicator (remaining)
|
||||||
onSecondaryContainer = Color(0xFF001945),
|
onSecondaryContainer = Color(0xFF001945), // Navigation bar selector icon
|
||||||
tertiary = Color(0xFF006E1B),
|
tertiary = Color(0xFF006E1B), // Downloaded badge
|
||||||
onTertiary = Color(0xFFFFFFFF),
|
onTertiary = Color(0xFFFFFFFF), // Downloaded badge text
|
||||||
tertiaryContainer = Color(0xFF95F990),
|
tertiaryContainer = Color(0xFF95F990),
|
||||||
onTertiaryContainer = Color(0xFF002203),
|
onTertiaryContainer = Color(0xFF002203),
|
||||||
background = Color(0xFFFEFBFF),
|
background = Color(0xFFFEFBFF),
|
||||||
onBackground = Color(0xFF1B1B1F),
|
onBackground = Color(0xFF1B1B1F),
|
||||||
surface = Color(0xFFFEFBFF),
|
surface = Color(0xFFFEFBFF),
|
||||||
onSurface = Color(0xFF1B1B1F),
|
onSurface = Color(0xFF1B1B1F),
|
||||||
surfaceVariant = Color(0xFFE1E2EC),
|
surfaceVariant = Color(0xFFF3EDF7), // Navigation bar background (ThemePrefWidget)
|
||||||
onSurfaceVariant = Color(0xFF44464F),
|
onSurfaceVariant = Color(0xFF44464F),
|
||||||
surfaceTint = Color(0xFF0058CA),
|
surfaceTint = Color(0xFF0058CA),
|
||||||
inverseSurface = Color(0xFF303034),
|
inverseSurface = Color(0xFF303034),
|
||||||
@@ -76,5 +81,10 @@ internal object TachiyomiColorScheme : BaseColorScheme() {
|
|||||||
onErrorContainer = Color(0xFF410002),
|
onErrorContainer = Color(0xFF410002),
|
||||||
outline = Color(0xFF757780),
|
outline = Color(0xFF757780),
|
||||||
outlineVariant = Color(0xFFC5C6D0),
|
outlineVariant = Color(0xFFC5C6D0),
|
||||||
|
surfaceContainerLowest = Color(0xFFF5F1F8),
|
||||||
|
surfaceContainerLow = Color(0xFFF7F2FA),
|
||||||
|
surfaceContainer = Color(0xFFF3EDF7), // Navigation bar background
|
||||||
|
surfaceContainerHigh = Color(0xFFFCF7FF),
|
||||||
|
surfaceContainerHighest = Color(0xFFFCF7FF),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,24 +23,29 @@ internal object TakoColorScheme : BaseColorScheme() {
|
|||||||
primaryContainer = Color(0xFFF3B375),
|
primaryContainer = Color(0xFFF3B375),
|
||||||
onPrimaryContainer = Color(0xFF38294E),
|
onPrimaryContainer = Color(0xFF38294E),
|
||||||
inversePrimary = Color(0xFF84531E),
|
inversePrimary = Color(0xFF84531E),
|
||||||
secondary = Color(0xFFF3B375),
|
secondary = Color(0xFFF3B375), // Unread badge
|
||||||
onSecondary = Color(0xFF38294E),
|
onSecondary = Color(0xFF38294E), // Unread badge text
|
||||||
secondaryContainer = Color(0xFFF3B375),
|
secondaryContainer = Color(0xFF5C4D4B), // Navigation bar selector pill & progress indicator (remaining)
|
||||||
onSecondaryContainer = Color(0xFF38294E),
|
onSecondaryContainer = Color(0xFFF3B375), // Navigation bar selector icon
|
||||||
tertiary = Color(0xFF66577E),
|
tertiary = Color(0xFF66577E), // Downloaded badge
|
||||||
onTertiary = Color(0xFFF3B375),
|
onTertiary = Color(0xFFF3B375), // Downloaded badge text
|
||||||
tertiaryContainer = Color(0xFF4E4065),
|
tertiaryContainer = Color(0xFF4E4065),
|
||||||
onTertiaryContainer = Color(0xFFEDDCFF),
|
onTertiaryContainer = Color(0xFFEDDCFF),
|
||||||
background = Color(0xFF21212E),
|
background = Color(0xFF21212E),
|
||||||
onBackground = Color(0xFFE3E0F2),
|
onBackground = Color(0xFFE3E0F2),
|
||||||
surface = Color(0xFF21212E),
|
surface = Color(0xFF21212E),
|
||||||
onSurface = Color(0xFFE3E0F2),
|
onSurface = Color(0xFFE3E0F2),
|
||||||
surfaceVariant = Color(0xFF49454E),
|
surfaceVariant = Color(0xFF2A2A3C), // Navigation bar background (ThemePrefWidget)
|
||||||
onSurfaceVariant = Color(0xFFCBC4CE),
|
onSurfaceVariant = Color(0xFFCBC4CE),
|
||||||
surfaceTint = Color(0xFF66577E),
|
surfaceTint = Color(0xFF66577E),
|
||||||
inverseSurface = Color(0xFFE5E1E6),
|
inverseSurface = Color(0xFFE5E1E6),
|
||||||
inverseOnSurface = Color(0xFF1B1B1E),
|
inverseOnSurface = Color(0xFF1B1B1E),
|
||||||
outline = Color(0xFF958F99),
|
outline = Color(0xFF958F99),
|
||||||
|
surfaceContainerLowest = Color(0xFF20202E),
|
||||||
|
surfaceContainerLow = Color(0xFF262636),
|
||||||
|
surfaceContainer = Color(0xFF2A2A3C), // Navigation bar background
|
||||||
|
surfaceContainerHigh = Color(0xFF303044),
|
||||||
|
surfaceContainerHighest = Color(0xFF36364D),
|
||||||
)
|
)
|
||||||
|
|
||||||
override val lightScheme = lightColorScheme(
|
override val lightScheme = lightColorScheme(
|
||||||
@@ -49,23 +54,28 @@ internal object TakoColorScheme : BaseColorScheme() {
|
|||||||
primaryContainer = Color(0xFF66577E),
|
primaryContainer = Color(0xFF66577E),
|
||||||
onPrimaryContainer = Color(0xFFF3B375),
|
onPrimaryContainer = Color(0xFFF3B375),
|
||||||
inversePrimary = Color(0xFFD6BAFF),
|
inversePrimary = Color(0xFFD6BAFF),
|
||||||
secondary = Color(0xFF66577E),
|
secondary = Color(0xFF66577E), // Unread badge
|
||||||
onSecondary = Color(0xFFF3B375),
|
onSecondary = Color(0xFFF3B375), // Unread badge text
|
||||||
secondaryContainer = Color(0xFF66577E),
|
secondaryContainer = Color(0xFFC8BED0), // Navigation bar selector pill & progress indicator (remaining)
|
||||||
onSecondaryContainer = Color(0xFFF3B375),
|
onSecondaryContainer = Color(0xFF66577E), // Navigation bar selector icon
|
||||||
tertiary = Color(0xFFF3B375),
|
tertiary = Color(0xFFF3B375), // Downloaded badge
|
||||||
onTertiary = Color(0xFF574360),
|
onTertiary = Color(0xFF574360), // Downloaded badge text
|
||||||
tertiaryContainer = Color(0xFFFDD6B0),
|
tertiaryContainer = Color(0xFFFDD6B0),
|
||||||
onTertiaryContainer = Color(0xFF221437),
|
onTertiaryContainer = Color(0xFF221437),
|
||||||
background = Color(0xFFF7F5FF),
|
background = Color(0xFFF7F5FF),
|
||||||
onBackground = Color(0xFF1B1B22),
|
onBackground = Color(0xFF1B1B22),
|
||||||
surface = Color(0xFFF7F5FF),
|
surface = Color(0xFFF7F5FF),
|
||||||
onSurface = Color(0xFF1B1B22),
|
onSurface = Color(0xFF1B1B22),
|
||||||
surfaceVariant = Color(0xFFE8E0EB),
|
surfaceVariant = Color(0xFFE8E0EB), // Navigation bar background (ThemePrefWidget)
|
||||||
onSurfaceVariant = Color(0xFF49454E),
|
onSurfaceVariant = Color(0xFF49454E),
|
||||||
surfaceTint = Color(0xFF66577E),
|
surfaceTint = Color(0xFF66577E),
|
||||||
inverseSurface = Color(0xFF313033),
|
inverseSurface = Color(0xFF313033),
|
||||||
inverseOnSurface = Color(0xFFF3EFF4),
|
inverseOnSurface = Color(0xFFF3EFF4),
|
||||||
outline = Color(0xFF7A757E),
|
outline = Color(0xFF7A757E),
|
||||||
|
surfaceContainerLowest = Color(0xFFD7D0DA),
|
||||||
|
surfaceContainerLow = Color(0xFFDFD8E2),
|
||||||
|
surfaceContainer = Color(0xFFE8E0EB), // Navigation bar background
|
||||||
|
surfaceContainerHigh = Color(0xFFEEE6F1),
|
||||||
|
surfaceContainerHighest = Color(0xFFF7EEFA),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+24
-14
@@ -15,24 +15,29 @@ internal object TealTurqoiseColorScheme : BaseColorScheme() {
|
|||||||
primaryContainer = Color(0xFF40E0D0),
|
primaryContainer = Color(0xFF40E0D0),
|
||||||
onPrimaryContainer = Color(0xFF000000),
|
onPrimaryContainer = Color(0xFF000000),
|
||||||
inversePrimary = Color(0xFF008080),
|
inversePrimary = Color(0xFF008080),
|
||||||
secondary = Color(0xFF40E0D0),
|
secondary = Color(0xFF40E0D0), // Unread badge
|
||||||
onSecondary = Color(0xFF000000),
|
onSecondary = Color(0xFF000000), // Unread badge text
|
||||||
secondaryContainer = Color(0xFF18544E),
|
secondaryContainer = Color(0xFF18544E), // Navigation bar selector pill & progress indicator (remaining)
|
||||||
onSecondaryContainer = Color(0xFF40E0D0),
|
onSecondaryContainer = Color(0xFF40E0D0), // Navigation bar selector icon
|
||||||
tertiary = Color(0xFFBF1F2F),
|
tertiary = Color(0xFFBF1F2F), // Downloaded badge
|
||||||
onTertiary = Color(0xFFFFFFFF),
|
onTertiary = Color(0xFFFFFFFF), // Downloaded badge text
|
||||||
tertiaryContainer = Color(0xFF200508),
|
tertiaryContainer = Color(0xFF200508),
|
||||||
onTertiaryContainer = Color(0xFFBF1F2F),
|
onTertiaryContainer = Color(0xFFBF1F2F),
|
||||||
background = Color(0xFF202125),
|
background = Color(0xFF202125),
|
||||||
onBackground = Color(0xFFDFDEDA),
|
onBackground = Color(0xFFDFDEDA),
|
||||||
surface = Color(0xFF202125),
|
surface = Color(0xFF202125),
|
||||||
onSurface = Color(0xFFDFDEDA),
|
onSurface = Color(0xFFDFDEDA),
|
||||||
surfaceVariant = Color(0xFF3F4947),
|
surfaceVariant = Color(0xFF233133), // Navigation bar background (ThemePrefWidget)
|
||||||
onSurfaceVariant = Color(0xFFDFDEDA),
|
onSurfaceVariant = Color(0xFFDFDEDA),
|
||||||
surfaceTint = Color(0xFF40E0D0),
|
surfaceTint = Color(0xFF40E0D0),
|
||||||
inverseSurface = Color(0xFFDFDEDA),
|
inverseSurface = Color(0xFFDFDEDA),
|
||||||
inverseOnSurface = Color(0xFF202125),
|
inverseOnSurface = Color(0xFF202125),
|
||||||
outline = Color(0xFF899391),
|
outline = Color(0xFF899391),
|
||||||
|
surfaceContainerLowest = Color(0xFF202C2E),
|
||||||
|
surfaceContainerLow = Color(0xFF222F31),
|
||||||
|
surfaceContainer = Color(0xFF233133), // Navigation bar background
|
||||||
|
surfaceContainerHigh = Color(0xFF28383A),
|
||||||
|
surfaceContainerHighest = Color(0xFF2F4244),
|
||||||
)
|
)
|
||||||
|
|
||||||
override val lightScheme = lightColorScheme(
|
override val lightScheme = lightColorScheme(
|
||||||
@@ -41,23 +46,28 @@ internal object TealTurqoiseColorScheme : BaseColorScheme() {
|
|||||||
primaryContainer = Color(0xFF008080),
|
primaryContainer = Color(0xFF008080),
|
||||||
onPrimaryContainer = Color(0xFFFFFFFF),
|
onPrimaryContainer = Color(0xFFFFFFFF),
|
||||||
inversePrimary = Color(0xFF40E0D0),
|
inversePrimary = Color(0xFF40E0D0),
|
||||||
secondary = Color(0xFF008080),
|
secondary = Color(0xFF008080), // Unread badge text
|
||||||
onSecondary = Color(0xFFFFFFFF),
|
onSecondary = Color(0xFFFFFFFF), // Unread badge text
|
||||||
secondaryContainer = Color(0xFFBFDFDF),
|
secondaryContainer = Color(0xFFCFE5E4), // Navigation bar selector pill & progress indicator (remaining)
|
||||||
onSecondaryContainer = Color(0xFF008080),
|
onSecondaryContainer = Color(0xFF008080), // Navigation bar selector icon
|
||||||
tertiary = Color(0xFFFF7F7F),
|
tertiary = Color(0xFFFF7F7F), // Downloaded badge
|
||||||
onTertiary = Color(0xFF000000),
|
onTertiary = Color(0xFF000000), // Downloaded badge text
|
||||||
tertiaryContainer = Color(0xFF2A1616),
|
tertiaryContainer = Color(0xFF2A1616),
|
||||||
onTertiaryContainer = Color(0xFFFF7F7F),
|
onTertiaryContainer = Color(0xFFFF7F7F),
|
||||||
background = Color(0xFFFAFAFA),
|
background = Color(0xFFFAFAFA),
|
||||||
onBackground = Color(0xFF050505),
|
onBackground = Color(0xFF050505),
|
||||||
surface = Color(0xFFFAFAFA),
|
surface = Color(0xFFFAFAFA),
|
||||||
onSurface = Color(0xFF050505),
|
onSurface = Color(0xFF050505),
|
||||||
surfaceVariant = Color(0xFFDAE5E2),
|
surfaceVariant = Color(0xFFEBF3F1), // Navigation bar background (ThemePrefWidget)
|
||||||
onSurfaceVariant = Color(0xFF050505),
|
onSurfaceVariant = Color(0xFF050505),
|
||||||
surfaceTint = Color(0xFFBFDFDF),
|
surfaceTint = Color(0xFFBFDFDF),
|
||||||
inverseSurface = Color(0xFF050505),
|
inverseSurface = Color(0xFF050505),
|
||||||
inverseOnSurface = Color(0xFFFAFAFA),
|
inverseOnSurface = Color(0xFFFAFAFA),
|
||||||
outline = Color(0xFF6F7977),
|
outline = Color(0xFF6F7977),
|
||||||
|
surfaceContainerLowest = Color(0xFFE1E9E7),
|
||||||
|
surfaceContainerLow = Color(0xFFE6EEEC),
|
||||||
|
surfaceContainer = Color(0xFFEBF3F1), // Navigation bar background
|
||||||
|
surfaceContainerHigh = Color(0xFFF0F8F6),
|
||||||
|
surfaceContainerHighest = Color(0xFFF7FFFD),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+24
-14
@@ -22,24 +22,29 @@ internal object TidalWaveColorScheme : BaseColorScheme() {
|
|||||||
primaryContainer = Color(0xFF004d61),
|
primaryContainer = Color(0xFF004d61),
|
||||||
onPrimaryContainer = Color(0xFFb8eaff),
|
onPrimaryContainer = Color(0xFFb8eaff),
|
||||||
inversePrimary = Color(0xFFa12b03),
|
inversePrimary = Color(0xFFa12b03),
|
||||||
secondary = Color(0xFF5ed4fc),
|
secondary = Color(0xFF5ed4fc), // Unread badge
|
||||||
onSecondary = Color(0xFF003544),
|
onSecondary = Color(0xFF003544), // Unread badge text
|
||||||
secondaryContainer = Color(0xFF004d61),
|
secondaryContainer = Color(0xFF004d61), // Navigation bar selector pill & progress indicator (remaining)
|
||||||
onSecondaryContainer = Color(0xFFb8eaff),
|
onSecondaryContainer = Color(0xFFb8eaff), // Navigation bar selector icon
|
||||||
tertiary = Color(0xFF92f7bc),
|
tertiary = Color(0xFF92f7bc), // Downloaded badge
|
||||||
onTertiary = Color(0xFF001c3b),
|
onTertiary = Color(0xFF001c3b), // Downloaded badge text
|
||||||
tertiaryContainer = Color(0xFFc3fada),
|
tertiaryContainer = Color(0xFFc3fada),
|
||||||
onTertiaryContainer = Color(0xFF78ffd6),
|
onTertiaryContainer = Color(0xFF78ffd6),
|
||||||
background = Color(0xFF001c3b),
|
background = Color(0xFF001c3b),
|
||||||
onBackground = Color(0xFFd5e3ff),
|
onBackground = Color(0xFFd5e3ff),
|
||||||
surface = Color(0xFF001c3b),
|
surface = Color(0xFF001c3b),
|
||||||
onSurface = Color(0xFFd5e3ff),
|
onSurface = Color(0xFFd5e3ff),
|
||||||
surfaceVariant = Color(0xFF40484c),
|
surfaceVariant = Color(0xFF082b4b), // Navigation bar background (ThemePrefWidget)
|
||||||
onSurfaceVariant = Color(0xFFbfc8cc),
|
onSurfaceVariant = Color(0xFFbfc8cc),
|
||||||
surfaceTint = Color(0xFF5ed4fc),
|
surfaceTint = Color(0xFF5ed4fc),
|
||||||
inverseSurface = Color(0xFFffe3c4),
|
inverseSurface = Color(0xFFffe3c4),
|
||||||
inverseOnSurface = Color(0xFF001c3b),
|
inverseOnSurface = Color(0xFF001c3b),
|
||||||
outline = Color(0xFF8a9296),
|
outline = Color(0xFF8a9296),
|
||||||
|
surfaceContainerLowest = Color(0xFF072642),
|
||||||
|
surfaceContainerLow = Color(0xFF072947),
|
||||||
|
surfaceContainer = Color(0xFF082b4b), // Navigation bar background
|
||||||
|
surfaceContainerHigh = Color(0xFF093257),
|
||||||
|
surfaceContainerHighest = Color(0xFF0A3861),
|
||||||
)
|
)
|
||||||
|
|
||||||
override val lightScheme = lightColorScheme(
|
override val lightScheme = lightColorScheme(
|
||||||
@@ -48,23 +53,28 @@ internal object TidalWaveColorScheme : BaseColorScheme() {
|
|||||||
primaryContainer = Color(0xFFB4D4DF),
|
primaryContainer = Color(0xFFB4D4DF),
|
||||||
onPrimaryContainer = Color(0xFF001f28),
|
onPrimaryContainer = Color(0xFF001f28),
|
||||||
inversePrimary = Color(0xFFff987f),
|
inversePrimary = Color(0xFFff987f),
|
||||||
secondary = Color(0xFF006780),
|
secondary = Color(0xFF006780), // Unread badge
|
||||||
onSecondary = Color(0xFFffffff),
|
onSecondary = Color(0xFFffffff), // Unread badge text
|
||||||
secondaryContainer = Color(0xFFb8eaff),
|
secondaryContainer = Color(0xFF9AE1FF), // Navigation bar selector pill & progress indicator (remaining)
|
||||||
onSecondaryContainer = Color(0xFF001f28),
|
onSecondaryContainer = Color(0xFF001f28), // Navigation bar selector icon
|
||||||
tertiary = Color(0xFF92f7bc),
|
tertiary = Color(0xFF92f7bc), // Downloaded badge
|
||||||
onTertiary = Color(0xFF001c3b),
|
onTertiary = Color(0xFF001c3b), // Downloaded badge text
|
||||||
tertiaryContainer = Color(0xFFc3fada),
|
tertiaryContainer = Color(0xFFc3fada),
|
||||||
onTertiaryContainer = Color(0xFF78ffd6),
|
onTertiaryContainer = Color(0xFF78ffd6),
|
||||||
background = Color(0xFFfdfbff),
|
background = Color(0xFFfdfbff),
|
||||||
onBackground = Color(0xFF001c3b),
|
onBackground = Color(0xFF001c3b),
|
||||||
surface = Color(0xFFfdfbff),
|
surface = Color(0xFFfdfbff),
|
||||||
onSurface = Color(0xFF001c3b),
|
onSurface = Color(0xFF001c3b),
|
||||||
surfaceVariant = Color(0xFFdce4e8),
|
surfaceVariant = Color(0xFFe8eff5), // Navigation bar background (ThemePrefWidget)
|
||||||
onSurfaceVariant = Color(0xFF40484c),
|
onSurfaceVariant = Color(0xFF40484c),
|
||||||
surfaceTint = Color(0xFF006780),
|
surfaceTint = Color(0xFF006780),
|
||||||
inverseSurface = Color(0xFF020400),
|
inverseSurface = Color(0xFF020400),
|
||||||
inverseOnSurface = Color(0xFFffe3c4),
|
inverseOnSurface = Color(0xFFffe3c4),
|
||||||
outline = Color(0xFF70787c),
|
outline = Color(0xFF70787c),
|
||||||
|
surfaceContainerLowest = Color(0xFFe2e8ec),
|
||||||
|
surfaceContainerLow = Color(0xFFe5ecf1),
|
||||||
|
surfaceContainer = Color(0xFFe8eff5), // Navigation bar background
|
||||||
|
surfaceContainerHigh = Color(0xFFedf4fA),
|
||||||
|
surfaceContainerHighest = Color(0xFFf5faff),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,24 +17,29 @@ internal object YinYangColorScheme : BaseColorScheme() {
|
|||||||
primaryContainer = Color(0xFFFFFFFF),
|
primaryContainer = Color(0xFFFFFFFF),
|
||||||
onPrimaryContainer = Color(0xFF000000),
|
onPrimaryContainer = Color(0xFF000000),
|
||||||
inversePrimary = Color(0xFFCECECE),
|
inversePrimary = Color(0xFFCECECE),
|
||||||
secondary = Color(0xFFFFFFFF),
|
secondary = Color(0xFFFFFFFF), // Unread badge
|
||||||
onSecondary = Color(0xFF5A5A5A),
|
onSecondary = Color(0xFF5A5A5A), // Unread badge text
|
||||||
secondaryContainer = Color(0xFF717171),
|
secondaryContainer = Color(0xFF717171), // Navigation bar selector pill & progress indicator (remaining)
|
||||||
onSecondaryContainer = Color(0xFFE4E4E4),
|
onSecondaryContainer = Color(0xFFE4E4E4), // Navigation bar selector icon
|
||||||
tertiary = Color(0xFF000000),
|
tertiary = Color(0xFF000000), // Downloaded badge
|
||||||
onTertiary = Color(0xFFFFFFFF),
|
onTertiary = Color(0xFFFFFFFF), // Downloaded badge text
|
||||||
tertiaryContainer = Color(0xFF00419E),
|
tertiaryContainer = Color(0xFF00419E),
|
||||||
onTertiaryContainer = Color(0xFFD8E2FF),
|
onTertiaryContainer = Color(0xFFD8E2FF),
|
||||||
background = Color(0xFF1E1E1E),
|
background = Color(0xFF1E1E1E),
|
||||||
onBackground = Color(0xFFE6E6E6),
|
onBackground = Color(0xFFE6E6E6),
|
||||||
surface = Color(0xFF1E1E1E),
|
surface = Color(0xFF1E1E1E),
|
||||||
onSurface = Color(0xFFE6E6E6),
|
onSurface = Color(0xFFE6E6E6),
|
||||||
surfaceVariant = Color(0xFF4E4E4E),
|
surfaceVariant = Color(0xFF313131), // Navigation bar background (ThemePrefWidget)
|
||||||
onSurfaceVariant = Color(0xFFD1D1D1),
|
onSurfaceVariant = Color(0xFFD1D1D1),
|
||||||
surfaceTint = Color(0xFFFFFFFF),
|
surfaceTint = Color(0xFFFFFFFF),
|
||||||
inverseSurface = Color(0xFFE6E6E6),
|
inverseSurface = Color(0xFFE6E6E6),
|
||||||
inverseOnSurface = Color(0xFF1E1E1E),
|
inverseOnSurface = Color(0xFF1E1E1E),
|
||||||
outline = Color(0xFF999999),
|
outline = Color(0xFF999999),
|
||||||
|
surfaceContainerLowest = Color(0xFF2A2A2A),
|
||||||
|
surfaceContainerLow = Color(0xFF2D2D2D),
|
||||||
|
surfaceContainer = Color(0xFF313131), // Navigation bar background
|
||||||
|
surfaceContainerHigh = Color(0xFF383838),
|
||||||
|
surfaceContainerHighest = Color(0xFF3F3F3F),
|
||||||
)
|
)
|
||||||
|
|
||||||
override val lightScheme = lightColorScheme(
|
override val lightScheme = lightColorScheme(
|
||||||
@@ -43,23 +48,28 @@ internal object YinYangColorScheme : BaseColorScheme() {
|
|||||||
primaryContainer = Color(0xFF000000),
|
primaryContainer = Color(0xFF000000),
|
||||||
onPrimaryContainer = Color(0xFFFFFFFF),
|
onPrimaryContainer = Color(0xFFFFFFFF),
|
||||||
inversePrimary = Color(0xFFA6A6A6),
|
inversePrimary = Color(0xFFA6A6A6),
|
||||||
secondary = Color(0xFF000000),
|
secondary = Color(0xFF000000), // Unread badge
|
||||||
onSecondary = Color(0xFFFFFFFF),
|
onSecondary = Color(0xFFFFFFFF), // Unread badge text
|
||||||
secondaryContainer = Color(0xFFDDDDDD),
|
secondaryContainer = Color(0xFFDDDDDD), // Navigation bar selector pill & progress indicator (remaining)
|
||||||
onSecondaryContainer = Color(0xFF0C0C0C),
|
onSecondaryContainer = Color(0xFF0C0C0C), // Navigation bar selector icon
|
||||||
tertiary = Color(0xFFFFFFFF),
|
tertiary = Color(0xFFFFFFFF), // Downloaded badge
|
||||||
onTertiary = Color(0xFF000000),
|
onTertiary = Color(0xFF000000), // Downloaded badge text
|
||||||
tertiaryContainer = Color(0xFFD8E2FF),
|
tertiaryContainer = Color(0xFFD8E2FF),
|
||||||
onTertiaryContainer = Color(0xFF001947),
|
onTertiaryContainer = Color(0xFF001947),
|
||||||
background = Color(0xFFFDFDFD),
|
background = Color(0xFFFDFDFD),
|
||||||
onBackground = Color(0xFF222222),
|
onBackground = Color(0xFF222222),
|
||||||
surface = Color(0xFFFDFDFD),
|
surface = Color(0xFFFDFDFD),
|
||||||
onSurface = Color(0xFF222222),
|
onSurface = Color(0xFF222222),
|
||||||
surfaceVariant = Color(0xFFEDEDED),
|
surfaceVariant = Color(0xFFE8E8E8), // Navigation bar background (ThemePrefWidget)
|
||||||
onSurfaceVariant = Color(0xFF515151),
|
onSurfaceVariant = Color(0xFF515151),
|
||||||
surfaceTint = Color(0xFF000000),
|
surfaceTint = Color(0xFF000000),
|
||||||
inverseSurface = Color(0xFF333333),
|
inverseSurface = Color(0xFF333333),
|
||||||
inverseOnSurface = Color(0xFFF4F4F4),
|
inverseOnSurface = Color(0xFFF4F4F4),
|
||||||
outline = Color(0xFF838383),
|
outline = Color(0xFF838383),
|
||||||
|
surfaceContainerLowest = Color(0xFFCFCFCF),
|
||||||
|
surfaceContainerLow = Color(0xFFDADADA),
|
||||||
|
surfaceContainer = Color(0xFFE8E8E8), // Navigation bar background
|
||||||
|
surfaceContainerHigh = Color(0xFFECECEC),
|
||||||
|
surfaceContainerHighest = Color(0xFFEFEFEF),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,24 +23,29 @@ internal object YotsubaColorScheme : BaseColorScheme() {
|
|||||||
primaryContainer = Color(0xFF862200),
|
primaryContainer = Color(0xFF862200),
|
||||||
onPrimaryContainer = Color(0xFFFFDBCF),
|
onPrimaryContainer = Color(0xFFFFDBCF),
|
||||||
inversePrimary = Color(0xFFAE3200),
|
inversePrimary = Color(0xFFAE3200),
|
||||||
secondary = Color(0xFFFFB59D),
|
secondary = Color(0xFFFFB59D), // Unread badge
|
||||||
onSecondary = Color(0xFF5F1600),
|
onSecondary = Color(0xFF5F1600), // Unread badge text
|
||||||
secondaryContainer = Color(0xFF862200),
|
secondaryContainer = Color(0xFF862200), // Navigation bar selector pill & progress indicator (remaining)
|
||||||
onSecondaryContainer = Color(0xFFFFDBCF),
|
onSecondaryContainer = Color(0xFFFFDBCF), // Navigation bar selector icon
|
||||||
tertiary = Color(0xFFD7C68D),
|
tertiary = Color(0xFFD7C68D), // Downloaded badge
|
||||||
onTertiary = Color(0xFF3A2F05),
|
onTertiary = Color(0xFF3A2F05), // Downloaded badge text
|
||||||
tertiaryContainer = Color(0xFF524619),
|
tertiaryContainer = Color(0xFF524619),
|
||||||
onTertiaryContainer = Color(0xFFF5E2A7),
|
onTertiaryContainer = Color(0xFFF5E2A7),
|
||||||
background = Color(0xFF211A18),
|
background = Color(0xFF211A18),
|
||||||
onBackground = Color(0xFFEDE0DD),
|
onBackground = Color(0xFFEDE0DD),
|
||||||
surface = Color(0xFF211A18),
|
surface = Color(0xFF211A18),
|
||||||
onSurface = Color(0xFFEDE0DD),
|
onSurface = Color(0xFFEDE0DD),
|
||||||
surfaceVariant = Color(0xFF53433F),
|
surfaceVariant = Color(0xFF332723), // Navigation bar background (ThemePrefWidget)
|
||||||
onSurfaceVariant = Color(0xFFD8C2BC),
|
onSurfaceVariant = Color(0xFFD8C2BC),
|
||||||
surfaceTint = Color(0xFFFFB59D),
|
surfaceTint = Color(0xFFFFB59D),
|
||||||
inverseSurface = Color(0xFFEDE0DD),
|
inverseSurface = Color(0xFFEDE0DD),
|
||||||
inverseOnSurface = Color(0xFF211A18),
|
inverseOnSurface = Color(0xFF211A18),
|
||||||
outline = Color(0xFFA08C87),
|
outline = Color(0xFFA08C87),
|
||||||
|
surfaceContainerLowest = Color(0xFF2E221F),
|
||||||
|
surfaceContainerLow = Color(0xFF312521),
|
||||||
|
surfaceContainer = Color(0xFF332723), // Navigation bar background
|
||||||
|
surfaceContainerHigh = Color(0xFF413531),
|
||||||
|
surfaceContainerHighest = Color(0xFF4C403D),
|
||||||
)
|
)
|
||||||
|
|
||||||
override val lightScheme = lightColorScheme(
|
override val lightScheme = lightColorScheme(
|
||||||
@@ -49,23 +54,28 @@ internal object YotsubaColorScheme : BaseColorScheme() {
|
|||||||
primaryContainer = Color(0xFFFFDBCF),
|
primaryContainer = Color(0xFFFFDBCF),
|
||||||
onPrimaryContainer = Color(0xFF3B0A00),
|
onPrimaryContainer = Color(0xFF3B0A00),
|
||||||
inversePrimary = Color(0xFFFFB59D),
|
inversePrimary = Color(0xFFFFB59D),
|
||||||
secondary = Color(0xFFAE3200),
|
secondary = Color(0xFFAE3200), // Unread badge
|
||||||
onSecondary = Color(0xFFFFFFFF),
|
onSecondary = Color(0xFFFFFFFF), // Unread badge text
|
||||||
secondaryContainer = Color(0xFFFFDBCF),
|
secondaryContainer = Color(0xFFEBCDC2), // Navigation bar selector pill & progress indicator (remaining)
|
||||||
onSecondaryContainer = Color(0xFF3B0A00),
|
onSecondaryContainer = Color(0xFF3B0A00), // Navigation bar selector icon
|
||||||
tertiary = Color(0xFF6B5E2F),
|
tertiary = Color(0xFF6B5E2F), // Downloaded badge
|
||||||
onTertiary = Color(0xFFFFFFFF),
|
onTertiary = Color(0xFFFFFFFF), // Downloaded badge text
|
||||||
tertiaryContainer = Color(0xFFF5E2A7),
|
tertiaryContainer = Color(0xFFF5E2A7),
|
||||||
onTertiaryContainer = Color(0xFF231B00),
|
onTertiaryContainer = Color(0xFF231B00),
|
||||||
background = Color(0xFFFCFCFC),
|
background = Color(0xFFFCFCFC),
|
||||||
onBackground = Color(0xFF211A18),
|
onBackground = Color(0xFF211A18),
|
||||||
surface = Color(0xFFFCFCFC),
|
surface = Color(0xFFFCFCFC),
|
||||||
onSurface = Color(0xFF211A18),
|
onSurface = Color(0xFF211A18),
|
||||||
surfaceVariant = Color(0xFFF5DED8),
|
surfaceVariant = Color(0xFFF6EBE7), // Navigation bar background (ThemePrefWidget)
|
||||||
onSurfaceVariant = Color(0xFF53433F),
|
onSurfaceVariant = Color(0xFF53433F),
|
||||||
surfaceTint = Color(0xFFAE3200),
|
surfaceTint = Color(0xFFAE3200),
|
||||||
inverseSurface = Color(0xFF362F2D),
|
inverseSurface = Color(0xFF362F2D),
|
||||||
inverseOnSurface = Color(0xFFFBEEEB),
|
inverseOnSurface = Color(0xFFFBEEEB),
|
||||||
outline = Color(0xFF85736E),
|
outline = Color(0xFF85736E),
|
||||||
|
surfaceContainerLowest = Color(0xFFECE3E0),
|
||||||
|
surfaceContainerLow = Color(0xFFF1E7E4),
|
||||||
|
surfaceContainer = Color(0xFFF6EBE7), // Navigation bar background
|
||||||
|
surfaceContainerHigh = Color(0xFFFAF4F2),
|
||||||
|
surfaceContainerHighest = Color(0xFFFBF6F4),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -186,7 +186,7 @@ private fun TrackInfoItem(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(top = 12.dp)
|
.padding(top = 12.dp)
|
||||||
.clip(MaterialTheme.shapes.medium)
|
.clip(MaterialTheme.shapes.medium)
|
||||||
.background(MaterialTheme.colorScheme.surface)
|
.background(MaterialTheme.colorScheme.surfaceContainerHighest)
|
||||||
.padding(8.dp)
|
.padding(8.dp)
|
||||||
.clip(RoundedCornerShape(6.dp)),
|
.clip(RoundedCornerShape(6.dp)),
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import androidx.activity.compose.BackHandler
|
|||||||
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.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.CalendarMonth
|
||||||
import androidx.compose.material.icons.outlined.FlipToBack
|
import androidx.compose.material.icons.outlined.FlipToBack
|
||||||
import androidx.compose.material.icons.outlined.Refresh
|
import androidx.compose.material.icons.outlined.Refresh
|
||||||
import androidx.compose.material.icons.outlined.SelectAll
|
import androidx.compose.material.icons.outlined.SelectAll
|
||||||
@@ -50,6 +51,7 @@ fun UpdateScreen(
|
|||||||
onClickCover: (UpdatesItem) -> Unit,
|
onClickCover: (UpdatesItem) -> Unit,
|
||||||
onSelectAll: (Boolean) -> Unit,
|
onSelectAll: (Boolean) -> Unit,
|
||||||
onInvertSelection: () -> Unit,
|
onInvertSelection: () -> Unit,
|
||||||
|
onCalendarClicked: () -> Unit,
|
||||||
onUpdateLibrary: () -> Boolean,
|
onUpdateLibrary: () -> Boolean,
|
||||||
onDownloadChapter: (List<UpdatesItem>, ChapterDownloadAction) -> Unit,
|
onDownloadChapter: (List<UpdatesItem>, ChapterDownloadAction) -> Unit,
|
||||||
onMultiBookmarkClicked: (List<UpdatesItem>, bookmark: Boolean) -> Unit,
|
onMultiBookmarkClicked: (List<UpdatesItem>, bookmark: Boolean) -> Unit,
|
||||||
@@ -63,6 +65,7 @@ fun UpdateScreen(
|
|||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = { scrollBehavior ->
|
topBar = { scrollBehavior ->
|
||||||
UpdatesAppBar(
|
UpdatesAppBar(
|
||||||
|
onCalendarClicked = { onCalendarClicked() },
|
||||||
onUpdateLibrary = { onUpdateLibrary() },
|
onUpdateLibrary = { onUpdateLibrary() },
|
||||||
actionModeCounter = state.selected.size,
|
actionModeCounter = state.selected.size,
|
||||||
onSelectAll = { onSelectAll(true) },
|
onSelectAll = { onSelectAll(true) },
|
||||||
@@ -132,6 +135,7 @@ fun UpdateScreen(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun UpdatesAppBar(
|
private fun UpdatesAppBar(
|
||||||
|
onCalendarClicked: () -> Unit,
|
||||||
onUpdateLibrary: () -> Unit,
|
onUpdateLibrary: () -> Unit,
|
||||||
// For action mode
|
// For action mode
|
||||||
actionModeCounter: Int,
|
actionModeCounter: Int,
|
||||||
@@ -147,6 +151,11 @@ private fun UpdatesAppBar(
|
|||||||
actions = {
|
actions = {
|
||||||
AppBarActions(
|
AppBarActions(
|
||||||
persistentListOf(
|
persistentListOf(
|
||||||
|
AppBar.Action(
|
||||||
|
title = stringResource(MR.strings.action_view_upcoming),
|
||||||
|
icon = Icons.Outlined.CalendarMonth,
|
||||||
|
onClick = onCalendarClicked,
|
||||||
|
),
|
||||||
AppBar.Action(
|
AppBar.Action(
|
||||||
title = stringResource(MR.strings.action_update_library),
|
title = stringResource(MR.strings.action_update_library),
|
||||||
icon = Icons.Outlined.Refresh,
|
icon = Icons.Outlined.Refresh,
|
||||||
|
|||||||
@@ -30,12 +30,12 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.window.Dialog
|
import androidx.compose.ui.window.Dialog
|
||||||
import com.google.accompanist.web.AccompanistWebViewClient
|
import com.kevinnzou.web.AccompanistWebViewClient
|
||||||
import com.google.accompanist.web.LoadingState
|
import com.kevinnzou.web.LoadingState
|
||||||
import com.google.accompanist.web.WebContent
|
import com.kevinnzou.web.WebContent
|
||||||
import com.google.accompanist.web.WebView
|
import com.kevinnzou.web.WebView
|
||||||
import com.google.accompanist.web.rememberWebViewNavigator
|
import com.kevinnzou.web.rememberWebViewNavigator
|
||||||
import com.google.accompanist.web.rememberWebViewState
|
import com.kevinnzou.web.rememberWebViewState
|
||||||
import eu.kanade.presentation.components.AppBar
|
import eu.kanade.presentation.components.AppBar
|
||||||
import eu.kanade.tachiyomi.BuildConfig
|
import eu.kanade.tachiyomi.BuildConfig
|
||||||
import eu.kanade.tachiyomi.util.system.setDefaultSettings
|
import eu.kanade.tachiyomi.util.system.setDefaultSettings
|
||||||
|
|||||||
@@ -28,11 +28,11 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.platform.LocalUriHandler
|
import androidx.compose.ui.platform.LocalUriHandler
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.google.accompanist.web.AccompanistWebViewClient
|
import com.kevinnzou.web.AccompanistWebViewClient
|
||||||
import com.google.accompanist.web.LoadingState
|
import com.kevinnzou.web.LoadingState
|
||||||
import com.google.accompanist.web.WebView
|
import com.kevinnzou.web.WebView
|
||||||
import com.google.accompanist.web.rememberWebViewNavigator
|
import com.kevinnzou.web.rememberWebViewNavigator
|
||||||
import com.google.accompanist.web.rememberWebViewState
|
import com.kevinnzou.web.rememberWebViewState
|
||||||
import eu.kanade.presentation.components.AppBar
|
import eu.kanade.presentation.components.AppBar
|
||||||
import eu.kanade.presentation.components.AppBarActions
|
import eu.kanade.presentation.components.AppBarActions
|
||||||
import eu.kanade.presentation.components.WarningBanner
|
import eu.kanade.presentation.components.WarningBanner
|
||||||
|
|||||||
@@ -17,8 +17,6 @@ import androidx.lifecycle.ProcessLifecycleOwner
|
|||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import coil3.ImageLoader
|
import coil3.ImageLoader
|
||||||
import coil3.SingletonImageLoader
|
import coil3.SingletonImageLoader
|
||||||
import coil3.disk.DiskCache
|
|
||||||
import coil3.disk.directory
|
|
||||||
import coil3.network.okhttp.OkHttpNetworkFetcherFactory
|
import coil3.network.okhttp.OkHttpNetworkFetcherFactory
|
||||||
import coil3.request.allowRgb565
|
import coil3.request.allowRgb565
|
||||||
import coil3.request.crossfade
|
import coil3.request.crossfade
|
||||||
@@ -40,6 +38,7 @@ import eu.kanade.domain.ui.UiPreferences
|
|||||||
import eu.kanade.domain.ui.model.setAppCompatDelegateThemeMode
|
import eu.kanade.domain.ui.model.setAppCompatDelegateThemeMode
|
||||||
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.MangaCoverFetcher
|
import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher
|
||||||
import eu.kanade.tachiyomi.data.coil.MangaCoverKeyer
|
import eu.kanade.tachiyomi.data.coil.MangaCoverKeyer
|
||||||
import eu.kanade.tachiyomi.data.coil.MangaKeyer
|
import eu.kanade.tachiyomi.data.coil.MangaKeyer
|
||||||
@@ -72,8 +71,12 @@ 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.migration.Migrator
|
||||||
|
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.PreferenceStore
|
||||||
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
|
||||||
@@ -175,25 +178,43 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
|
|||||||
) {
|
) {
|
||||||
SyncDataJob.startNow(this@App)
|
SyncDataJob.startNow(this@App)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
initializeMigrator()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initializeMigrator() {
|
||||||
|
val preferenceStore = Injekt.get<PreferenceStore>()
|
||||||
|
// SY -->
|
||||||
|
val preference = preferenceStore.getInt(Preference.appStateKey("eh_last_version_code"), 0)
|
||||||
|
// SY <--
|
||||||
|
logcat { "Migration from ${preference.get()} to ${BuildConfig.VERSION_CODE}" }
|
||||||
|
Migrator.initialize(
|
||||||
|
old = preference.get(),
|
||||||
|
new = BuildConfig.VERSION_CODE,
|
||||||
|
migrations = migrations,
|
||||||
|
onMigrationComplete = {
|
||||||
|
logcat { "Updating last version to ${BuildConfig.VERSION_CODE}" }
|
||||||
|
preference.set(BuildConfig.VERSION_CODE)
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun newImageLoader(context: Context): ImageLoader {
|
override fun newImageLoader(context: Context): ImageLoader {
|
||||||
return ImageLoader.Builder(this).apply {
|
return ImageLoader.Builder(this).apply {
|
||||||
val callFactoryLazy = lazy { Injekt.get<NetworkHelper>().client }
|
val callFactoryLazy = lazy { Injekt.get<NetworkHelper>().client }
|
||||||
val diskCacheLazy = lazy { CoilDiskCache.get(this@App) }
|
|
||||||
components {
|
components {
|
||||||
add(OkHttpNetworkFetcherFactory(callFactoryLazy::value))
|
add(OkHttpNetworkFetcherFactory(callFactoryLazy::value))
|
||||||
add(TachiyomiImageDecoder.Factory())
|
add(TachiyomiImageDecoder.Factory())
|
||||||
add(MangaCoverFetcher.MangaFactory(callFactoryLazy, diskCacheLazy))
|
add(MangaCoverFetcher.MangaFactory(callFactoryLazy))
|
||||||
add(MangaCoverFetcher.MangaCoverFactory(callFactoryLazy, diskCacheLazy))
|
add(MangaCoverFetcher.MangaCoverFactory(callFactoryLazy))
|
||||||
add(MangaKeyer())
|
add(MangaKeyer())
|
||||||
add(MangaCoverKeyer())
|
add(MangaCoverKeyer())
|
||||||
|
add(BufferedSourceFetcher.Factory())
|
||||||
// SY -->
|
// SY -->
|
||||||
add(PagePreviewKeyer())
|
add(PagePreviewKeyer())
|
||||||
add(PagePreviewFetcher.Factory(callFactoryLazy, diskCacheLazy))
|
add(PagePreviewFetcher.Factory(callFactoryLazy))
|
||||||
// SY <--
|
// SY <--
|
||||||
}
|
}
|
||||||
diskCache(diskCacheLazy::value)
|
|
||||||
crossfade((300 * this@App.animatorDurationScale).toInt())
|
crossfade((300 * this@App.animatorDurationScale).toInt())
|
||||||
allowRgb565(DeviceUtil.isLowRamDevice(this@App))
|
allowRgb565(DeviceUtil.isLowRamDevice(this@App))
|
||||||
if (networkPreferences.verboseLogging().get()) logger(DebugLogger())
|
if (networkPreferences.verboseLogging().get()) logger(DebugLogger())
|
||||||
@@ -349,24 +370,3 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
|
|||||||
}
|
}
|
||||||
|
|
||||||
private const val ACTION_DISABLE_INCOGNITO_MODE = "tachi.action.DISABLE_INCOGNITO_MODE"
|
private const val ACTION_DISABLE_INCOGNITO_MODE = "tachi.action.DISABLE_INCOGNITO_MODE"
|
||||||
|
|
||||||
/**
|
|
||||||
* Direct copy of Coil's internal SingletonDiskCache so that [MangaCoverFetcher] can access it.
|
|
||||||
*/
|
|
||||||
private object CoilDiskCache {
|
|
||||||
|
|
||||||
private const val FOLDER_NAME = "image_cache"
|
|
||||||
private var instance: DiskCache? = null
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
fun get(context: Context): DiskCache {
|
|
||||||
return instance ?: run {
|
|
||||||
val safeCacheDir = context.cacheDir.apply { mkdirs() }
|
|
||||||
// Create the singleton disk cache instance.
|
|
||||||
DiskCache.Builder()
|
|
||||||
.directory(safeCacheDir.resolve(FOLDER_NAME))
|
|
||||||
.build()
|
|
||||||
.also { instance = it }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,464 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.core.content.edit
|
|
||||||
import androidx.preference.PreferenceManager
|
|
||||||
import eu.kanade.domain.base.BasePreferences
|
|
||||||
import eu.kanade.domain.source.service.SourcePreferences
|
|
||||||
import eu.kanade.domain.ui.UiPreferences
|
|
||||||
import eu.kanade.tachiyomi.core.security.SecurityPreferences
|
|
||||||
import eu.kanade.tachiyomi.data.backup.create.BackupCreateJob
|
|
||||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
|
|
||||||
import eu.kanade.tachiyomi.data.track.TrackerManager
|
|
||||||
import eu.kanade.tachiyomi.network.NetworkPreferences
|
|
||||||
import eu.kanade.tachiyomi.network.PREF_DOH_CLOUDFLARE
|
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation
|
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
|
|
||||||
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
|
||||||
import eu.kanade.tachiyomi.util.system.workManager
|
|
||||||
import tachiyomi.core.common.preference.Preference
|
|
||||||
import tachiyomi.core.common.preference.PreferenceStore
|
|
||||||
import tachiyomi.core.common.preference.TriState
|
|
||||||
import tachiyomi.core.common.preference.getAndSet
|
|
||||||
import tachiyomi.core.common.preference.getEnum
|
|
||||||
import tachiyomi.core.common.preference.minusAssign
|
|
||||||
import tachiyomi.core.common.preference.plusAssign
|
|
||||||
import tachiyomi.domain.backup.service.BackupPreferences
|
|
||||||
import tachiyomi.domain.library.service.LibraryPreferences
|
|
||||||
import tachiyomi.domain.library.service.LibraryPreferences.Companion.MANGA_NON_COMPLETED
|
|
||||||
import tachiyomi.i18n.MR
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
object Migrations {
|
|
||||||
|
|
||||||
// TODO NATIVE TACHIYOMI MIGRATIONS ARE FUCKED UP DUE TO DIFFERING VERSION NUMBERS
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Performs a migration when the application is updated.
|
|
||||||
*
|
|
||||||
* @return true if a migration is performed, false otherwise.
|
|
||||||
*/
|
|
||||||
fun upgrade(
|
|
||||||
context: Context,
|
|
||||||
preferenceStore: PreferenceStore,
|
|
||||||
basePreferences: BasePreferences,
|
|
||||||
uiPreferences: UiPreferences,
|
|
||||||
networkPreferences: NetworkPreferences,
|
|
||||||
sourcePreferences: SourcePreferences,
|
|
||||||
securityPreferences: SecurityPreferences,
|
|
||||||
libraryPreferences: LibraryPreferences,
|
|
||||||
readerPreferences: ReaderPreferences,
|
|
||||||
backupPreferences: BackupPreferences,
|
|
||||||
trackerManager: TrackerManager,
|
|
||||||
): Boolean {
|
|
||||||
val lastVersionCode = preferenceStore.getInt(Preference.appStateKey("last_version_code"), 0)
|
|
||||||
val oldVersion = lastVersionCode.get()
|
|
||||||
if (oldVersion < BuildConfig.VERSION_CODE) {
|
|
||||||
lastVersionCode.set(BuildConfig.VERSION_CODE)
|
|
||||||
|
|
||||||
// Always set up background tasks to ensure they're running
|
|
||||||
LibraryUpdateJob.setupTask(context)
|
|
||||||
BackupCreateJob.setupTask(context)
|
|
||||||
|
|
||||||
// Fresh install
|
|
||||||
if (oldVersion == 0) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
|
||||||
|
|
||||||
if (oldVersion < 15) {
|
|
||||||
// Delete internal chapter cache dir.
|
|
||||||
File(context.cacheDir, "chapter_disk_cache").deleteRecursively()
|
|
||||||
}
|
|
||||||
if (oldVersion < 19) {
|
|
||||||
// Move covers to external files dir.
|
|
||||||
val oldDir = File(context.externalCacheDir, "cover_disk_cache")
|
|
||||||
if (oldDir.exists()) {
|
|
||||||
val destDir = context.getExternalFilesDir("covers")
|
|
||||||
if (destDir != null) {
|
|
||||||
oldDir.listFiles()?.forEach {
|
|
||||||
it.renameTo(File(destDir, it.name))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (oldVersion < 26) {
|
|
||||||
// Delete external chapter cache dir.
|
|
||||||
val extCache = context.externalCacheDir
|
|
||||||
if (extCache != null) {
|
|
||||||
val chapterCache = File(extCache, "chapter_disk_cache")
|
|
||||||
if (chapterCache.exists()) {
|
|
||||||
chapterCache.deleteRecursively()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (oldVersion < 44) {
|
|
||||||
// Reset sorting preference if using removed sort by source
|
|
||||||
val oldSortingMode = prefs.getInt(libraryPreferences.sortingMode().key(), 0)
|
|
||||||
|
|
||||||
if (oldSortingMode == 5) { // SOURCE = 5
|
|
||||||
prefs.edit {
|
|
||||||
putInt(libraryPreferences.sortingMode().key(), 0) // ALPHABETICAL = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (oldVersion < 52) {
|
|
||||||
// Migrate library filters to tri-state versions
|
|
||||||
fun convertBooleanPrefToTriState(key: String): Int {
|
|
||||||
val oldPrefValue = prefs.getBoolean(key, false)
|
|
||||||
return if (oldPrefValue) {
|
|
||||||
1
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
prefs.edit {
|
|
||||||
putInt(
|
|
||||||
libraryPreferences.filterDownloaded().key(),
|
|
||||||
convertBooleanPrefToTriState("pref_filter_downloaded_key"),
|
|
||||||
)
|
|
||||||
remove("pref_filter_downloaded_key")
|
|
||||||
|
|
||||||
putInt(
|
|
||||||
libraryPreferences.filterUnread().key(),
|
|
||||||
convertBooleanPrefToTriState("pref_filter_unread_key"),
|
|
||||||
)
|
|
||||||
remove("pref_filter_unread_key")
|
|
||||||
|
|
||||||
putInt(
|
|
||||||
libraryPreferences.filterCompleted().key(),
|
|
||||||
convertBooleanPrefToTriState("pref_filter_completed_key"),
|
|
||||||
)
|
|
||||||
remove("pref_filter_completed_key")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (oldVersion < 54) {
|
|
||||||
// Force MAL log out due to login flow change
|
|
||||||
// v52: switched from scraping to WebView
|
|
||||||
// v53: switched from WebView to OAuth
|
|
||||||
if (trackerManager.myAnimeList.isLoggedIn) {
|
|
||||||
trackerManager.myAnimeList.logout()
|
|
||||||
context.toast(MR.strings.myanimelist_relogin)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (oldVersion < 57) {
|
|
||||||
// Migrate DNS over HTTPS setting
|
|
||||||
val wasDohEnabled = prefs.getBoolean("enable_doh", false)
|
|
||||||
if (wasDohEnabled) {
|
|
||||||
prefs.edit {
|
|
||||||
putInt(networkPreferences.dohProvider().key(), PREF_DOH_CLOUDFLARE)
|
|
||||||
remove("enable_doh")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (oldVersion < 59) {
|
|
||||||
// Reset rotation to Free after replacing Lock
|
|
||||||
if (prefs.contains("pref_rotation_type_key")) {
|
|
||||||
prefs.edit {
|
|
||||||
putInt("pref_rotation_type_key", 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (oldVersion < 60) {
|
|
||||||
// Migrate Rotation and Viewer values to default values for viewer_flags
|
|
||||||
val newOrientation = when (prefs.getInt("pref_rotation_type_key", 1)) {
|
|
||||||
1 -> ReaderOrientation.FREE.flagValue
|
|
||||||
2 -> ReaderOrientation.PORTRAIT.flagValue
|
|
||||||
3 -> ReaderOrientation.LANDSCAPE.flagValue
|
|
||||||
4 -> ReaderOrientation.LOCKED_PORTRAIT.flagValue
|
|
||||||
5 -> ReaderOrientation.LOCKED_LANDSCAPE.flagValue
|
|
||||||
else -> ReaderOrientation.FREE.flagValue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reading mode flag and prefValue is the same value
|
|
||||||
val newReadingMode = prefs.getInt("pref_default_viewer_key", 1)
|
|
||||||
|
|
||||||
prefs.edit {
|
|
||||||
putInt("pref_default_orientation_type_key", newOrientation)
|
|
||||||
remove("pref_rotation_type_key")
|
|
||||||
putInt("pref_default_reading_mode_key", newReadingMode)
|
|
||||||
remove("pref_default_viewer_key")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (oldVersion < 61) {
|
|
||||||
// Handle removed every 1 or 2 hour library updates
|
|
||||||
val updateInterval = libraryPreferences.autoUpdateInterval().get()
|
|
||||||
if (updateInterval == 1 || updateInterval == 2) {
|
|
||||||
libraryPreferences.autoUpdateInterval().set(3)
|
|
||||||
LibraryUpdateJob.setupTask(context, 3)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (oldVersion < 64) {
|
|
||||||
val oldSortingMode = prefs.getInt(libraryPreferences.sortingMode().key(), 0)
|
|
||||||
val oldSortingDirection = prefs.getBoolean("library_sorting_ascending", true)
|
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
val newSortingMode = when (oldSortingMode) {
|
|
||||||
0 -> "ALPHABETICAL"
|
|
||||||
1 -> "LAST_READ"
|
|
||||||
2 -> "LAST_CHECKED"
|
|
||||||
3 -> "UNREAD"
|
|
||||||
4 -> "TOTAL_CHAPTERS"
|
|
||||||
6 -> "LATEST_CHAPTER"
|
|
||||||
8 -> "DATE_FETCHED"
|
|
||||||
7 -> "DATE_ADDED"
|
|
||||||
else -> "ALPHABETICAL"
|
|
||||||
}
|
|
||||||
|
|
||||||
val newSortingDirection = when (oldSortingDirection) {
|
|
||||||
true -> "ASCENDING"
|
|
||||||
else -> "DESCENDING"
|
|
||||||
}
|
|
||||||
|
|
||||||
prefs.edit(commit = true) {
|
|
||||||
remove(libraryPreferences.sortingMode().key())
|
|
||||||
remove("library_sorting_ascending")
|
|
||||||
}
|
|
||||||
|
|
||||||
prefs.edit {
|
|
||||||
putString(libraryPreferences.sortingMode().key(), newSortingMode)
|
|
||||||
putString("library_sorting_ascending", newSortingDirection)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (oldVersion < 70) {
|
|
||||||
if (sourcePreferences.enabledLanguages().isSet()) {
|
|
||||||
sourcePreferences.enabledLanguages() += "all"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (oldVersion < 71) {
|
|
||||||
// Handle removed every 3, 4, 6, and 8 hour library updates
|
|
||||||
val updateInterval = libraryPreferences.autoUpdateInterval().get()
|
|
||||||
if (updateInterval in listOf(3, 4, 6, 8)) {
|
|
||||||
libraryPreferences.autoUpdateInterval().set(12)
|
|
||||||
LibraryUpdateJob.setupTask(context, 12)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (oldVersion < 72) {
|
|
||||||
val oldUpdateOngoingOnly = prefs.getBoolean("pref_update_only_non_completed_key", true)
|
|
||||||
if (!oldUpdateOngoingOnly) {
|
|
||||||
libraryPreferences.autoUpdateMangaRestrictions() -= MANGA_NON_COMPLETED
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (oldVersion < 75) {
|
|
||||||
val oldSecureScreen = prefs.getBoolean("secure_screen", false)
|
|
||||||
if (oldSecureScreen) {
|
|
||||||
securityPreferences.secureScreen().set(SecurityPreferences.SecureScreenMode.ALWAYS)
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
DeviceUtil.isMiui &&
|
|
||||||
basePreferences.extensionInstaller().get() == BasePreferences.ExtensionInstaller.PACKAGEINSTALLER
|
|
||||||
) {
|
|
||||||
basePreferences.extensionInstaller().set(BasePreferences.ExtensionInstaller.LEGACY)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (oldVersion < 77) {
|
|
||||||
val oldReaderTap = prefs.getBoolean("reader_tap", false)
|
|
||||||
if (!oldReaderTap) {
|
|
||||||
readerPreferences.navigationModePager().set(5)
|
|
||||||
readerPreferences.navigationModeWebtoon().set(5)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (oldVersion < 81) {
|
|
||||||
// Handle renamed enum values
|
|
||||||
prefs.edit {
|
|
||||||
val newSortingMode = when (
|
|
||||||
val oldSortingMode = prefs.getString(
|
|
||||||
libraryPreferences.sortingMode().key(),
|
|
||||||
"ALPHABETICAL",
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
"LAST_CHECKED" -> "LAST_MANGA_UPDATE"
|
|
||||||
"UNREAD" -> "UNREAD_COUNT"
|
|
||||||
"DATE_FETCHED" -> "CHAPTER_FETCH_DATE"
|
|
||||||
else -> oldSortingMode
|
|
||||||
}
|
|
||||||
putString(libraryPreferences.sortingMode().key(), newSortingMode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (oldVersion < 82) {
|
|
||||||
prefs.edit {
|
|
||||||
val sort = prefs.getString(libraryPreferences.sortingMode().key(), null) ?: return@edit
|
|
||||||
val direction = prefs.getString("library_sorting_ascending", "ASCENDING")!!
|
|
||||||
putString(libraryPreferences.sortingMode().key(), "$sort,$direction")
|
|
||||||
remove("library_sorting_ascending")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (oldVersion < 84) {
|
|
||||||
if (backupPreferences.backupInterval().get() == 0) {
|
|
||||||
backupPreferences.backupInterval().set(12)
|
|
||||||
BackupCreateJob.setupTask(context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (oldVersion < 85) {
|
|
||||||
val preferences = listOf(
|
|
||||||
libraryPreferences.filterChapterByRead(),
|
|
||||||
libraryPreferences.filterChapterByDownloaded(),
|
|
||||||
libraryPreferences.filterChapterByBookmarked(),
|
|
||||||
libraryPreferences.sortChapterBySourceOrNumber(),
|
|
||||||
libraryPreferences.displayChapterByNameOrNumber(),
|
|
||||||
libraryPreferences.sortChapterByAscendingOrDescending(),
|
|
||||||
)
|
|
||||||
|
|
||||||
prefs.edit {
|
|
||||||
preferences.forEach { preference ->
|
|
||||||
val key = preference.key()
|
|
||||||
val value = prefs.getInt(key, Int.MIN_VALUE)
|
|
||||||
if (value == Int.MIN_VALUE) return@forEach
|
|
||||||
remove(key)
|
|
||||||
putLong(key, value.toLong())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (oldVersion < 86) {
|
|
||||||
if (uiPreferences.themeMode().isSet()) {
|
|
||||||
prefs.edit {
|
|
||||||
val themeMode = prefs.getString(uiPreferences.themeMode().key(), null) ?: return@edit
|
|
||||||
putString(uiPreferences.themeMode().key(), themeMode.uppercase())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (oldVersion < 92) {
|
|
||||||
val trackingQueuePref = context.getSharedPreferences("tracking_queue", Context.MODE_PRIVATE)
|
|
||||||
trackingQueuePref.all.forEach {
|
|
||||||
val (_, lastChapterRead) = it.value.toString().split(":")
|
|
||||||
trackingQueuePref.edit {
|
|
||||||
remove(it.key)
|
|
||||||
putFloat(it.key, lastChapterRead.toFloat())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (oldVersion < 96) {
|
|
||||||
LibraryUpdateJob.cancelAllWorks(context)
|
|
||||||
LibraryUpdateJob.setupTask(context)
|
|
||||||
}
|
|
||||||
if (oldVersion < 97) {
|
|
||||||
// Removed background jobs
|
|
||||||
context.workManager.cancelAllWorkByTag("UpdateChecker")
|
|
||||||
context.workManager.cancelAllWorkByTag("ExtensionUpdate")
|
|
||||||
prefs.edit {
|
|
||||||
remove("automatic_ext_updates")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (oldVersion < 99) {
|
|
||||||
val prefKeys = listOf(
|
|
||||||
"pref_filter_library_downloaded",
|
|
||||||
"pref_filter_library_unread",
|
|
||||||
"pref_filter_library_started",
|
|
||||||
"pref_filter_library_bookmarked",
|
|
||||||
"pref_filter_library_completed",
|
|
||||||
) + trackerManager.trackers.map { "pref_filter_library_tracked_${it.id}" }
|
|
||||||
|
|
||||||
prefKeys.forEach { key ->
|
|
||||||
val pref = preferenceStore.getInt(key, 0)
|
|
||||||
prefs.edit {
|
|
||||||
remove(key)
|
|
||||||
|
|
||||||
val newValue = when (pref.get()) {
|
|
||||||
1 -> TriState.ENABLED_IS
|
|
||||||
2 -> TriState.ENABLED_NOT
|
|
||||||
else -> TriState.DISABLED
|
|
||||||
}
|
|
||||||
|
|
||||||
preferenceStore.getEnum("${key}_v2", TriState.DISABLED).set(newValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (oldVersion < 105) {
|
|
||||||
val pref = libraryPreferences.autoUpdateDeviceRestrictions()
|
|
||||||
if (pref.isSet() && "battery_not_low" in pref.get()) {
|
|
||||||
pref.getAndSet { it - "battery_not_low" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (oldVersion < 106) {
|
|
||||||
val pref = preferenceStore.getInt("relative_time", 7)
|
|
||||||
if (pref.get() == 0) {
|
|
||||||
uiPreferences.relativeTime().set(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (oldVersion < 113) {
|
|
||||||
val prefsToReplace = listOf(
|
|
||||||
"pref_download_only",
|
|
||||||
"incognito_mode",
|
|
||||||
"last_catalogue_source",
|
|
||||||
"trusted_signatures",
|
|
||||||
"last_app_closed",
|
|
||||||
"library_update_last_timestamp",
|
|
||||||
"library_unseen_updates_count",
|
|
||||||
"last_used_category",
|
|
||||||
"last_app_check",
|
|
||||||
"last_ext_check",
|
|
||||||
"last_version_code",
|
|
||||||
"storage_dir",
|
|
||||||
)
|
|
||||||
replacePreferences(
|
|
||||||
preferenceStore = preferenceStore,
|
|
||||||
filterPredicate = { it.key in prefsToReplace },
|
|
||||||
newKey = { Preference.appStateKey(it) },
|
|
||||||
)
|
|
||||||
|
|
||||||
// Deleting old download cache index files, but might as well clear it all out
|
|
||||||
context.cacheDir.deleteRecursively()
|
|
||||||
}
|
|
||||||
if (oldVersion < 114) {
|
|
||||||
sourcePreferences.extensionRepos().getAndSet {
|
|
||||||
it.map { repo -> "https://raw.githubusercontent.com/$repo/repo" }.toSet()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (oldVersion < 116) {
|
|
||||||
replacePreferences(
|
|
||||||
preferenceStore = preferenceStore,
|
|
||||||
filterPredicate = { it.key.startsWith("pref_mangasync_") || it.key.startsWith("track_token_") },
|
|
||||||
newKey = { Preference.privateKey(it) },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (oldVersion < 117) {
|
|
||||||
prefs.edit {
|
|
||||||
remove(Preference.appStateKey("trusted_signatures"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
private fun replacePreferences(
|
|
||||||
preferenceStore: PreferenceStore,
|
|
||||||
filterPredicate: (Map.Entry<String, Any?>) -> Boolean,
|
|
||||||
newKey: (String) -> String,
|
|
||||||
) {
|
|
||||||
preferenceStore.getAll()
|
|
||||||
.filter(filterPredicate)
|
|
||||||
.forEach { (key, value) ->
|
|
||||||
when (value) {
|
|
||||||
is Int -> {
|
|
||||||
preferenceStore.getInt(newKey(key)).set(value)
|
|
||||||
preferenceStore.getInt(key).delete()
|
|
||||||
}
|
|
||||||
is Long -> {
|
|
||||||
preferenceStore.getLong(newKey(key)).set(value)
|
|
||||||
preferenceStore.getLong(key).delete()
|
|
||||||
}
|
|
||||||
is Float -> {
|
|
||||||
preferenceStore.getFloat(newKey(key)).set(value)
|
|
||||||
preferenceStore.getFloat(key).delete()
|
|
||||||
}
|
|
||||||
is String -> {
|
|
||||||
preferenceStore.getString(newKey(key)).set(value)
|
|
||||||
preferenceStore.getString(key).delete()
|
|
||||||
}
|
|
||||||
is Boolean -> {
|
|
||||||
preferenceStore.getBoolean(newKey(key)).set(value)
|
|
||||||
preferenceStore.getBoolean(key).delete()
|
|
||||||
}
|
|
||||||
is Set<*> -> (value as? Set<String>)?.let {
|
|
||||||
preferenceStore.getStringSet(newKey(key)).set(value)
|
|
||||||
preferenceStore.getStringSet(key).delete()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+12
-7
@@ -17,14 +17,19 @@ class CategoriesRestorer(
|
|||||||
if (backupCategories.isNotEmpty()) {
|
if (backupCategories.isNotEmpty()) {
|
||||||
val dbCategories = getCategories.await()
|
val dbCategories = getCategories.await()
|
||||||
val dbCategoriesByName = dbCategories.associateBy { it.name }
|
val dbCategoriesByName = dbCategories.associateBy { it.name }
|
||||||
|
var nextOrder = dbCategories.maxOfOrNull { it.order }?.plus(1) ?: 0
|
||||||
|
|
||||||
val categories = backupCategories.map {
|
val categories = backupCategories
|
||||||
dbCategoriesByName[it.name]
|
.sortedBy { it.order }
|
||||||
?: handler.awaitOneExecutable {
|
.distinctBy { it.name }
|
||||||
categoriesQueries.insert(it.name, it.order, it.flags)
|
.map {
|
||||||
categoriesQueries.selectLastInsertedRowId()
|
val newOrder = nextOrder++
|
||||||
}.let { id -> it.toCategory(id) }
|
dbCategoriesByName[it.name]
|
||||||
}
|
?: handler.awaitOneExecutable {
|
||||||
|
categoriesQueries.insert(it.name, newOrder, it.flags)
|
||||||
|
categoriesQueries.selectLastInsertedRowId()
|
||||||
|
}.let { id -> it.toCategory(id).copy(order = newOrder) }
|
||||||
|
}
|
||||||
|
|
||||||
libraryPreferences.categorizedDisplaySettings().set(
|
libraryPreferences.categorizedDisplaySettings().set(
|
||||||
(dbCategories + categories)
|
(dbCategories + categories)
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.coil
|
||||||
|
|
||||||
|
import coil3.ImageLoader
|
||||||
|
import coil3.decode.DataSource
|
||||||
|
import coil3.decode.ImageSource
|
||||||
|
import coil3.fetch.FetchResult
|
||||||
|
import coil3.fetch.Fetcher
|
||||||
|
import coil3.fetch.SourceFetchResult
|
||||||
|
import coil3.request.Options
|
||||||
|
import okio.BufferedSource
|
||||||
|
|
||||||
|
class BufferedSourceFetcher(
|
||||||
|
private val data: BufferedSource,
|
||||||
|
private val options: Options,
|
||||||
|
) : Fetcher {
|
||||||
|
|
||||||
|
override suspend fun fetch(): FetchResult {
|
||||||
|
return SourceFetchResult(
|
||||||
|
source = ImageSource(
|
||||||
|
source = data,
|
||||||
|
fileSystem = options.fileSystem,
|
||||||
|
),
|
||||||
|
mimeType = null,
|
||||||
|
dataSource = DataSource.MEMORY,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
class Factory : Fetcher.Factory<BufferedSource> {
|
||||||
|
|
||||||
|
override fun create(
|
||||||
|
data: BufferedSource,
|
||||||
|
options: Options,
|
||||||
|
imageLoader: ImageLoader,
|
||||||
|
): Fetcher {
|
||||||
|
return BufferedSourceFetcher(data, options)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -46,6 +46,7 @@ import java.io.IOException
|
|||||||
* Available request parameter:
|
* Available request parameter:
|
||||||
* - [USE_CUSTOM_COVER_KEY]: Use custom cover if set by user, default is true
|
* - [USE_CUSTOM_COVER_KEY]: Use custom cover if set by user, default is true
|
||||||
*/
|
*/
|
||||||
|
@Suppress("LongParameterList")
|
||||||
class MangaCoverFetcher(
|
class MangaCoverFetcher(
|
||||||
private val url: String?,
|
private val url: String?,
|
||||||
private val isLibraryManga: Boolean,
|
private val isLibraryManga: Boolean,
|
||||||
@@ -55,7 +56,7 @@ class MangaCoverFetcher(
|
|||||||
private val diskCacheKeyLazy: Lazy<String>,
|
private val diskCacheKeyLazy: Lazy<String>,
|
||||||
private val sourceLazy: Lazy<HttpSource?>,
|
private val sourceLazy: Lazy<HttpSource?>,
|
||||||
private val callFactoryLazy: Lazy<Call.Factory>,
|
private val callFactoryLazy: Lazy<Call.Factory>,
|
||||||
private val diskCacheLazy: Lazy<DiskCache>,
|
private val imageLoader: ImageLoader,
|
||||||
) : Fetcher {
|
) : Fetcher {
|
||||||
|
|
||||||
private val diskCacheKey: String
|
private val diskCacheKey: String
|
||||||
@@ -207,7 +208,7 @@ class MangaCoverFetcher(
|
|||||||
private fun moveSnapshotToCoverCache(snapshot: DiskCache.Snapshot, cacheFile: File?): File? {
|
private fun moveSnapshotToCoverCache(snapshot: DiskCache.Snapshot, cacheFile: File?): File? {
|
||||||
if (cacheFile == null) return null
|
if (cacheFile == null) return null
|
||||||
return try {
|
return try {
|
||||||
diskCacheLazy.value.run {
|
imageLoader.diskCache?.run {
|
||||||
fileSystem.source(snapshot.data).use { input ->
|
fileSystem.source(snapshot.data).use { input ->
|
||||||
writeSourceToCoverCache(input, cacheFile)
|
writeSourceToCoverCache(input, cacheFile)
|
||||||
}
|
}
|
||||||
@@ -248,7 +249,7 @@ class MangaCoverFetcher(
|
|||||||
|
|
||||||
private fun readFromDiskCache(): DiskCache.Snapshot? {
|
private fun readFromDiskCache(): DiskCache.Snapshot? {
|
||||||
return if (options.diskCachePolicy.readEnabled) {
|
return if (options.diskCachePolicy.readEnabled) {
|
||||||
diskCacheLazy.value.openSnapshot(diskCacheKey)
|
imageLoader.diskCache?.openSnapshot(diskCacheKey)
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
@@ -257,9 +258,10 @@ class MangaCoverFetcher(
|
|||||||
private fun writeToDiskCache(
|
private fun writeToDiskCache(
|
||||||
response: Response,
|
response: Response,
|
||||||
): DiskCache.Snapshot? {
|
): DiskCache.Snapshot? {
|
||||||
val editor = diskCacheLazy.value.openEditor(diskCacheKey) ?: return null
|
val diskCache = imageLoader.diskCache
|
||||||
|
val editor = diskCache?.openEditor(diskCacheKey) ?: return null
|
||||||
try {
|
try {
|
||||||
diskCacheLazy.value.fileSystem.write(editor.data) {
|
diskCache.fileSystem.write(editor.data) {
|
||||||
response.body.source().readAll(this)
|
response.body.source().readAll(this)
|
||||||
}
|
}
|
||||||
return editor.commitAndOpenSnapshot()
|
return editor.commitAndOpenSnapshot()
|
||||||
@@ -299,7 +301,6 @@ class MangaCoverFetcher(
|
|||||||
|
|
||||||
class MangaFactory(
|
class MangaFactory(
|
||||||
private val callFactoryLazy: Lazy<Call.Factory>,
|
private val callFactoryLazy: Lazy<Call.Factory>,
|
||||||
private val diskCacheLazy: Lazy<DiskCache>,
|
|
||||||
) : Fetcher.Factory<Manga> {
|
) : Fetcher.Factory<Manga> {
|
||||||
|
|
||||||
private val coverCache: CoverCache by injectLazy()
|
private val coverCache: CoverCache by injectLazy()
|
||||||
@@ -312,17 +313,16 @@ class MangaCoverFetcher(
|
|||||||
options = options,
|
options = options,
|
||||||
coverFileLazy = lazy { coverCache.getCoverFile(data.thumbnailUrl) },
|
coverFileLazy = lazy { coverCache.getCoverFile(data.thumbnailUrl) },
|
||||||
customCoverFileLazy = lazy { coverCache.getCustomCoverFile(data.id) },
|
customCoverFileLazy = lazy { coverCache.getCustomCoverFile(data.id) },
|
||||||
diskCacheKeyLazy = lazy { MangaKeyer().key(data, options) },
|
diskCacheKeyLazy = lazy { imageLoader.components.key(data, options)!! },
|
||||||
sourceLazy = lazy { sourceManager.get(data.source) as? HttpSource },
|
sourceLazy = lazy { sourceManager.get(data.source) as? HttpSource },
|
||||||
callFactoryLazy = callFactoryLazy,
|
callFactoryLazy = callFactoryLazy,
|
||||||
diskCacheLazy = diskCacheLazy,
|
imageLoader = imageLoader,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class MangaCoverFactory(
|
class MangaCoverFactory(
|
||||||
private val callFactoryLazy: Lazy<Call.Factory>,
|
private val callFactoryLazy: Lazy<Call.Factory>,
|
||||||
private val diskCacheLazy: Lazy<DiskCache>,
|
|
||||||
) : Fetcher.Factory<MangaCover> {
|
) : Fetcher.Factory<MangaCover> {
|
||||||
|
|
||||||
private val coverCache: CoverCache by injectLazy()
|
private val coverCache: CoverCache by injectLazy()
|
||||||
@@ -335,10 +335,10 @@ class MangaCoverFetcher(
|
|||||||
options = options,
|
options = options,
|
||||||
coverFileLazy = lazy { coverCache.getCoverFile(data.url) },
|
coverFileLazy = lazy { coverCache.getCoverFile(data.url) },
|
||||||
customCoverFileLazy = lazy { coverCache.getCustomCoverFile(data.mangaId) },
|
customCoverFileLazy = lazy { coverCache.getCustomCoverFile(data.mangaId) },
|
||||||
diskCacheKeyLazy = lazy { MangaCoverKeyer().key(data, options) },
|
diskCacheKeyLazy = lazy { imageLoader.components.key(data, options)!! },
|
||||||
sourceLazy = lazy { sourceManager.get(data.sourceId) as? HttpSource },
|
sourceLazy = lazy { sourceManager.get(data.sourceId) as? HttpSource },
|
||||||
callFactoryLazy = callFactoryLazy,
|
callFactoryLazy = callFactoryLazy,
|
||||||
diskCacheLazy = diskCacheLazy,
|
imageLoader = imageLoader,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import java.io.IOException
|
|||||||
* Disk caching is handled by [PagePreviewCache], otherwise
|
* Disk caching is handled by [PagePreviewCache], otherwise
|
||||||
* handled by Coil's [DiskCache].
|
* handled by Coil's [DiskCache].
|
||||||
*/
|
*/
|
||||||
|
@Suppress("LongParameterList")
|
||||||
class PagePreviewFetcher(
|
class PagePreviewFetcher(
|
||||||
private val page: PagePreview,
|
private val page: PagePreview,
|
||||||
private val options: Options,
|
private val options: Options,
|
||||||
@@ -43,7 +44,7 @@ class PagePreviewFetcher(
|
|||||||
private val diskCacheKeyLazy: Lazy<String>,
|
private val diskCacheKeyLazy: Lazy<String>,
|
||||||
private val sourceLazy: Lazy<PagePreviewSource?>,
|
private val sourceLazy: Lazy<PagePreviewSource?>,
|
||||||
private val callFactoryLazy: Lazy<Call.Factory>,
|
private val callFactoryLazy: Lazy<Call.Factory>,
|
||||||
private val diskCacheLazy: Lazy<DiskCache>,
|
private val imageLoader: ImageLoader,
|
||||||
) : Fetcher {
|
) : Fetcher {
|
||||||
|
|
||||||
private val diskCacheKey: String
|
private val diskCacheKey: String
|
||||||
@@ -164,7 +165,7 @@ class PagePreviewFetcher(
|
|||||||
|
|
||||||
private fun moveSnapshotToPagePreviewCache(snapshot: DiskCache.Snapshot): File? {
|
private fun moveSnapshotToPagePreviewCache(snapshot: DiskCache.Snapshot): File? {
|
||||||
return try {
|
return try {
|
||||||
diskCacheLazy.value.run {
|
imageLoader.diskCache?.run {
|
||||||
fileSystem.source(snapshot.data).use { input ->
|
fileSystem.source(snapshot.data).use { input ->
|
||||||
writeSourceToPagePreviewCache(input)
|
writeSourceToPagePreviewCache(input)
|
||||||
}
|
}
|
||||||
@@ -203,15 +204,16 @@ class PagePreviewFetcher(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun readFromDiskCache(): DiskCache.Snapshot? {
|
private fun readFromDiskCache(): DiskCache.Snapshot? {
|
||||||
return if (options.diskCachePolicy.readEnabled) diskCacheLazy.value.openSnapshot(diskCacheKey) else null
|
return if (options.diskCachePolicy.readEnabled) imageLoader.diskCache?.openSnapshot(diskCacheKey) else null
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun writeToDiskCache(
|
private fun writeToDiskCache(
|
||||||
response: Response,
|
response: Response,
|
||||||
): DiskCache.Snapshot? {
|
): DiskCache.Snapshot? {
|
||||||
val editor = diskCacheLazy.value.openEditor(diskCacheKey) ?: return null
|
val diskCache = imageLoader.diskCache
|
||||||
|
val editor = diskCache?.openEditor(diskCacheKey) ?: return null
|
||||||
try {
|
try {
|
||||||
diskCacheLazy.value.fileSystem.write(editor.data) {
|
diskCache.fileSystem.write(editor.data) {
|
||||||
response.body.source().readAll(this)
|
response.body.source().readAll(this)
|
||||||
}
|
}
|
||||||
return editor.commitAndOpenSnapshot()
|
return editor.commitAndOpenSnapshot()
|
||||||
@@ -235,7 +237,6 @@ class PagePreviewFetcher(
|
|||||||
|
|
||||||
class Factory(
|
class Factory(
|
||||||
private val callFactoryLazy: Lazy<Call.Factory>,
|
private val callFactoryLazy: Lazy<Call.Factory>,
|
||||||
private val diskCacheLazy: Lazy<DiskCache>,
|
|
||||||
) : Fetcher.Factory<PagePreview> {
|
) : Fetcher.Factory<PagePreview> {
|
||||||
|
|
||||||
private val pagePreviewCache: PagePreviewCache by injectLazy()
|
private val pagePreviewCache: PagePreviewCache by injectLazy()
|
||||||
@@ -248,10 +249,10 @@ class PagePreviewFetcher(
|
|||||||
pagePreviewFile = { pagePreviewCache.getImageFile(data.imageUrl) },
|
pagePreviewFile = { pagePreviewCache.getImageFile(data.imageUrl) },
|
||||||
isInCache = { pagePreviewCache.isImageInCache(data.imageUrl) },
|
isInCache = { pagePreviewCache.isImageInCache(data.imageUrl) },
|
||||||
writeToCache = { pagePreviewCache.putImageToCache(data.imageUrl, it) },
|
writeToCache = { pagePreviewCache.putImageToCache(data.imageUrl, it) },
|
||||||
diskCacheKeyLazy = lazy { PagePreviewKeyer().key(data, options) },
|
diskCacheKeyLazy = lazy { imageLoader.components.key(data, options)!! },
|
||||||
sourceLazy = lazy { sourceManager.get(data.source) as? PagePreviewSource },
|
sourceLazy = lazy { sourceManager.get(data.source) as? PagePreviewSource },
|
||||||
callFactoryLazy = callFactoryLazy,
|
callFactoryLazy = callFactoryLazy,
|
||||||
diskCacheLazy = diskCacheLazy,
|
imageLoader = imageLoader,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
package eu.kanade.tachiyomi.data.coil
|
package eu.kanade.tachiyomi.data.coil
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.core.graphics.drawable.toDrawable
|
|
||||||
import coil3.ImageLoader
|
import coil3.ImageLoader
|
||||||
import coil3.asCoilImage
|
import coil3.asCoilImage
|
||||||
import coil3.decode.DecodeResult
|
import coil3.decode.DecodeResult
|
||||||
|
import coil3.decode.DecodeUtils
|
||||||
import coil3.decode.Decoder
|
import coil3.decode.Decoder
|
||||||
import coil3.decode.ImageSource
|
import coil3.decode.ImageSource
|
||||||
import coil3.fetch.SourceFetchResult
|
import coil3.fetch.SourceFetchResult
|
||||||
import coil3.request.Options
|
import coil3.request.Options
|
||||||
import coil3.request.allowRgb565
|
import coil3.request.bitmapConfig
|
||||||
import eu.kanade.tachiyomi.util.storage.CbzCrypto
|
import eu.kanade.tachiyomi.util.storage.CbzCrypto
|
||||||
|
import eu.kanade.tachiyomi.util.system.GLUtil
|
||||||
import net.lingala.zip4j.ZipFile
|
import net.lingala.zip4j.ZipFile
|
||||||
import net.lingala.zip4j.model.FileHeader
|
import net.lingala.zip4j.model.FileHeader
|
||||||
import okio.BufferedSource
|
import okio.BufferedSource
|
||||||
@@ -39,29 +41,58 @@ class TachiyomiImageDecoder(private val resources: ImageSource, private val opti
|
|||||||
}
|
}
|
||||||
val decoder = resources.sourceOrNull()?.use {
|
val decoder = resources.sourceOrNull()?.use {
|
||||||
zip4j.use { zipFile ->
|
zip4j.use { zipFile ->
|
||||||
ImageDecoder.newInstance(zipFile?.getInputStream(entry) ?: it.inputStream())
|
ImageDecoder.newInstance(zipFile?.getInputStream(entry) ?: it.inputStream(), options.cropBorders, displayProfile)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
check(decoder != null && decoder.width > 0 && decoder.height > 0) { "Failed to initialize decoder" }
|
check(decoder != null && decoder.width > 0 && decoder.height > 0) { "Failed to initialize decoder" }
|
||||||
|
|
||||||
val bitmap = decoder.decode(rgb565 = options.allowRgb565)
|
val srcWidth = decoder.width
|
||||||
|
val srcHeight = decoder.height
|
||||||
|
|
||||||
|
val dstWidth = options.size.widthPx(options.scale) { srcWidth }
|
||||||
|
val dstHeight = options.size.heightPx(options.scale) { srcHeight }
|
||||||
|
|
||||||
|
val sampleSize = DecodeUtils.calculateInSampleSize(
|
||||||
|
srcWidth = srcWidth,
|
||||||
|
srcHeight = srcHeight,
|
||||||
|
dstWidth = dstWidth,
|
||||||
|
dstHeight = dstHeight,
|
||||||
|
scale = options.scale,
|
||||||
|
)
|
||||||
|
|
||||||
|
var bitmap = decoder.decode(sampleSize = sampleSize)
|
||||||
decoder.recycle()
|
decoder.recycle()
|
||||||
|
|
||||||
check(bitmap != null) { "Failed to decode image" }
|
check(bitmap != null) { "Failed to decode image" }
|
||||||
|
|
||||||
|
if (
|
||||||
|
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O &&
|
||||||
|
options.bitmapConfig == Bitmap.Config.HARDWARE &&
|
||||||
|
maxOf(bitmap.width, bitmap.height) <= GLUtil.maxTextureSize
|
||||||
|
) {
|
||||||
|
val hwBitmap = bitmap.copy(Bitmap.Config.HARDWARE, false)
|
||||||
|
if (hwBitmap != null) {
|
||||||
|
bitmap.recycle()
|
||||||
|
bitmap = hwBitmap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return DecodeResult(
|
return DecodeResult(
|
||||||
image = bitmap.asCoilImage(),
|
image = bitmap.asCoilImage(),
|
||||||
isSampled = false,
|
isSampled = sampleSize > 1,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
class Factory : Decoder.Factory {
|
class Factory : Decoder.Factory {
|
||||||
|
|
||||||
override fun create(result: SourceFetchResult, options: Options, imageLoader: ImageLoader): Decoder? {
|
override fun create(result: SourceFetchResult, options: Options, imageLoader: ImageLoader): Decoder? {
|
||||||
if (!isApplicable(result.source.source())) return null
|
return if (options.customDecoder || isApplicable(result.source.source())) {
|
||||||
return TachiyomiImageDecoder(result.source, options)
|
TachiyomiImageDecoder(result.source, options)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isApplicable(source: BufferedSource): Boolean {
|
private fun isApplicable(source: BufferedSource): Boolean {
|
||||||
@@ -84,4 +115,8 @@ class TachiyomiImageDecoder(private val resources: ImageSource, private val opti
|
|||||||
|
|
||||||
override fun hashCode() = javaClass.hashCode()
|
override fun hashCode() = javaClass.hashCode()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
var displayProfile: ByteArray? = null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.coil
|
||||||
|
|
||||||
|
import coil3.Extras
|
||||||
|
import coil3.getExtra
|
||||||
|
import coil3.request.ImageRequest
|
||||||
|
import coil3.request.Options
|
||||||
|
import coil3.size.Dimension
|
||||||
|
import coil3.size.Scale
|
||||||
|
import coil3.size.Size
|
||||||
|
import coil3.size.isOriginal
|
||||||
|
import coil3.size.pxOrElse
|
||||||
|
|
||||||
|
internal inline fun Size.widthPx(scale: Scale, original: () -> Int): Int {
|
||||||
|
return if (isOriginal) original() else width.toPx(scale)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal inline fun Size.heightPx(scale: Scale, original: () -> Int): Int {
|
||||||
|
return if (isOriginal) original() else height.toPx(scale)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun Dimension.toPx(scale: Scale): Int = pxOrElse {
|
||||||
|
when (scale) {
|
||||||
|
Scale.FILL -> Int.MIN_VALUE
|
||||||
|
Scale.FIT -> Int.MAX_VALUE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ImageRequest.Builder.cropBorders(enable: Boolean) = apply {
|
||||||
|
extras[cropBordersKey] = enable
|
||||||
|
}
|
||||||
|
|
||||||
|
val Options.cropBorders: Boolean
|
||||||
|
get() = getExtra(cropBordersKey)
|
||||||
|
|
||||||
|
private val cropBordersKey = Extras.Key(default = false)
|
||||||
|
|
||||||
|
fun ImageRequest.Builder.customDecoder(enable: Boolean) = apply {
|
||||||
|
extras[customDecoderKey] = enable
|
||||||
|
}
|
||||||
|
|
||||||
|
val Options.customDecoder: Boolean
|
||||||
|
get() = getExtra(customDecoderKey)
|
||||||
|
|
||||||
|
private val customDecoderKey = Extras.Key(default = false)
|
||||||
@@ -7,6 +7,7 @@ import eu.kanade.tachiyomi.source.Source
|
|||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||||
import exh.log.xLogE
|
import exh.log.xLogE
|
||||||
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.asFlow
|
import kotlinx.coroutines.flow.asFlow
|
||||||
import kotlinx.coroutines.flow.drop
|
import kotlinx.coroutines.flow.drop
|
||||||
@@ -35,6 +36,7 @@ import uy.kohesive.injekt.api.get
|
|||||||
* and retrieved through dependency injection. You can use this class to queue new chapters or query
|
* and retrieved through dependency injection. You can use this class to queue new chapters or query
|
||||||
* downloaded chapters.
|
* downloaded chapters.
|
||||||
*/
|
*/
|
||||||
|
@OptIn(DelicateCoroutinesApi::class)
|
||||||
class DownloadManager(
|
class DownloadManager(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val provider: DownloadProvider = Injekt.get(),
|
private val provider: DownloadProvider = Injekt.get(),
|
||||||
@@ -475,7 +477,7 @@ class DownloadManager(
|
|||||||
|
|
||||||
fun renameMangaDir(oldTitle: String, newTitle: String, source: Long) {
|
fun renameMangaDir(oldTitle: String, newTitle: String, source: Long) {
|
||||||
val sourceDir = provider.findSourceDir(sourceManager.getOrStub(source)) ?: return
|
val sourceDir = provider.findSourceDir(sourceManager.getOrStub(source)) ?: return
|
||||||
val mangaDir = sourceDir.findFile(DiskUtil.buildValidFilename(oldTitle), true) ?: return
|
val mangaDir = sourceDir.findFile(DiskUtil.buildValidFilename(oldTitle)) ?: return
|
||||||
mangaDir.renameTo(DiskUtil.buildValidFilename(newTitle))
|
mangaDir.renameTo(DiskUtil.buildValidFilename(newTitle))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ class DownloadProvider(
|
|||||||
* @param source the source to query.
|
* @param source the source to query.
|
||||||
*/
|
*/
|
||||||
fun findSourceDir(source: Source): UniFile? {
|
fun findSourceDir(source: Source): UniFile? {
|
||||||
return downloadsDir?.findFile(getSourceDirName(source), true)
|
return downloadsDir?.findFile(getSourceDirName(source))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -68,7 +68,7 @@ class DownloadProvider(
|
|||||||
*/
|
*/
|
||||||
fun findMangaDir(mangaTitle: String, source: Source): UniFile? {
|
fun findMangaDir(mangaTitle: String, source: Source): UniFile? {
|
||||||
val sourceDir = findSourceDir(source)
|
val sourceDir = findSourceDir(source)
|
||||||
return sourceDir?.findFile(getMangaDirName(mangaTitle), true)
|
return sourceDir?.findFile(getMangaDirName(mangaTitle))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -82,7 +82,7 @@ class DownloadProvider(
|
|||||||
fun findChapterDir(chapterName: String, chapterScanlator: String?, mangaTitle: String, source: Source): UniFile? {
|
fun findChapterDir(chapterName: String, chapterScanlator: String?, mangaTitle: String, source: Source): UniFile? {
|
||||||
val mangaDir = findMangaDir(mangaTitle, source)
|
val mangaDir = findMangaDir(mangaTitle, source)
|
||||||
return getValidChapterDirNames(chapterName, chapterScanlator).asSequence()
|
return getValidChapterDirNames(chapterName, chapterScanlator).asSequence()
|
||||||
.mapNotNull { mangaDir?.findFile(it, true) }
|
.mapNotNull { mangaDir?.findFile(it) }
|
||||||
.firstOrNull()
|
.firstOrNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,7 +97,7 @@ class DownloadProvider(
|
|||||||
val mangaDir = findMangaDir(/* SY --> */ manga.ogTitle /* SY <-- */, source) ?: return null to emptyList()
|
val mangaDir = findMangaDir(/* SY --> */ manga.ogTitle /* SY <-- */, source) ?: return null to emptyList()
|
||||||
return mangaDir to chapters.mapNotNull { chapter ->
|
return mangaDir to chapters.mapNotNull { chapter ->
|
||||||
getValidChapterDirNames(chapter.name, chapter.scanlator).asSequence()
|
getValidChapterDirNames(chapter.name, chapter.scanlator).asSequence()
|
||||||
.mapNotNull { mangaDir.findFile(it, true) }
|
.mapNotNull { mangaDir.findFile(it) }
|
||||||
.firstOrNull()
|
.firstOrNull()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,10 +16,12 @@ import eu.kanade.tachiyomi.util.storage.CbzCrypto
|
|||||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||||
import eu.kanade.tachiyomi.util.storage.DiskUtil.NOMEDIA_FILE
|
import eu.kanade.tachiyomi.util.storage.DiskUtil.NOMEDIA_FILE
|
||||||
import eu.kanade.tachiyomi.util.storage.saveTo
|
import eu.kanade.tachiyomi.util.storage.saveTo
|
||||||
|
import exh.source.isEhBasedSource
|
||||||
import exh.util.DataSaver
|
import exh.util.DataSaver
|
||||||
import exh.util.DataSaver.Companion.getImage
|
import exh.util.DataSaver.Companion.getImage
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
@@ -75,6 +77,7 @@ import java.util.zip.ZipOutputStream
|
|||||||
*
|
*
|
||||||
* Its queue contains the list of chapters to download.
|
* Its queue contains the list of chapters to download.
|
||||||
*/
|
*/
|
||||||
|
@OptIn(DelicateCoroutinesApi::class)
|
||||||
class Downloader(
|
class Downloader(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val provider: DownloadProvider,
|
private val provider: DownloadProvider,
|
||||||
@@ -511,6 +514,9 @@ class Downloader(
|
|||||||
.retryWhen { _, attempt ->
|
.retryWhen { _, attempt ->
|
||||||
if (attempt < 3) {
|
if (attempt < 3) {
|
||||||
delay((2L shl attempt.toInt()) * 1000)
|
delay((2L shl attempt.toInt()) * 1000)
|
||||||
|
if (source.isEhBasedSource()) {
|
||||||
|
page.imageUrl = source.getImageUrl(page)
|
||||||
|
}
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
@@ -709,7 +715,7 @@ class Downloader(
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Remove the old file
|
// Remove the old file
|
||||||
dir.findFile(COMIC_INFO_FILE, true)?.delete()
|
dir.findFile(COMIC_INFO_FILE)?.delete()
|
||||||
dir.createFile(COMIC_INFO_FILE)!!.openOutputStream().use {
|
dir.createFile(COMIC_INFO_FILE)!!.openOutputStream().use {
|
||||||
val comicInfoString = xml.encodeToString(ComicInfo.serializer(), comicInfo)
|
val comicInfoString = xml.encodeToString(ComicInfo.serializer(), comicInfo)
|
||||||
it.write(comicInfoString.toByteArray())
|
it.write(comicInfoString.toByteArray())
|
||||||
|
|||||||
@@ -823,7 +823,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
|||||||
// Always sync the data before library update if syncing is enabled.
|
// Always sync the data before library update if syncing is enabled.
|
||||||
if (syncPreferences.isSyncEnabled()) {
|
if (syncPreferences.isSyncEnabled()) {
|
||||||
// Check if SyncDataJob is already running
|
// Check if SyncDataJob is already running
|
||||||
if (wm.isRunning(SyncDataJob.TAG_MANUAL)) {
|
if (SyncDataJob.isRunning(context)) {
|
||||||
// SyncDataJob is already running
|
// SyncDataJob is already running
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import eu.kanade.tachiyomi.util.system.cancelNotification
|
|||||||
import eu.kanade.tachiyomi.util.system.getBitmapOrNull
|
import eu.kanade.tachiyomi.util.system.getBitmapOrNull
|
||||||
import eu.kanade.tachiyomi.util.system.notificationBuilder
|
import eu.kanade.tachiyomi.util.system.notificationBuilder
|
||||||
import eu.kanade.tachiyomi.util.system.notify
|
import eu.kanade.tachiyomi.util.system.notify
|
||||||
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
import tachiyomi.core.common.Constants
|
import tachiyomi.core.common.Constants
|
||||||
import tachiyomi.core.common.i18n.pluralStringResource
|
import tachiyomi.core.common.i18n.pluralStringResource
|
||||||
import tachiyomi.core.common.i18n.stringResource
|
import tachiyomi.core.common.i18n.stringResource
|
||||||
@@ -41,6 +42,7 @@ import uy.kohesive.injekt.api.get
|
|||||||
import java.math.RoundingMode
|
import java.math.RoundingMode
|
||||||
import java.text.NumberFormat
|
import java.text.NumberFormat
|
||||||
|
|
||||||
|
@OptIn(DelicateCoroutinesApi::class)
|
||||||
class LibraryUpdateNotifier(
|
class LibraryUpdateNotifier(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import eu.kanade.tachiyomi.util.system.getParcelableExtraCompat
|
|||||||
import eu.kanade.tachiyomi.util.system.notificationManager
|
import eu.kanade.tachiyomi.util.system.notificationManager
|
||||||
import eu.kanade.tachiyomi.util.system.toShareIntent
|
import eu.kanade.tachiyomi.util.system.toShareIntent
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import tachiyomi.core.common.Constants
|
import tachiyomi.core.common.Constants
|
||||||
import tachiyomi.core.common.util.lang.launchIO
|
import tachiyomi.core.common.util.lang.launchIO
|
||||||
@@ -41,6 +42,7 @@ import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID
|
|||||||
* Pending Broadcasts should be made from here.
|
* Pending Broadcasts should be made from here.
|
||||||
* NOTE: Use local broadcasts if possible.
|
* NOTE: Use local broadcasts if possible.
|
||||||
*/
|
*/
|
||||||
|
@OptIn(DelicateCoroutinesApi::class)
|
||||||
class NotificationReceiver : BroadcastReceiver() {
|
class NotificationReceiver : BroadcastReceiver() {
|
||||||
|
|
||||||
private val getManga: GetManga by injectLazy()
|
private val getManga: GetManga by injectLazy()
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ class ImageSaver(
|
|||||||
MediaStore.Images.Media.RELATIVE_PATH to relativePath,
|
MediaStore.Images.Media.RELATIVE_PATH to relativePath,
|
||||||
MediaStore.Images.Media.DISPLAY_NAME to image.name,
|
MediaStore.Images.Media.DISPLAY_NAME to image.name,
|
||||||
MediaStore.Images.Media.MIME_TYPE to type.mime,
|
MediaStore.Images.Media.MIME_TYPE to type.mime,
|
||||||
MediaStore.Images.Media.DATE_MODIFIED to Instant.now().toEpochMilli(),
|
MediaStore.Images.Media.DATE_MODIFIED to Instant.now().epochSecond,
|
||||||
)
|
)
|
||||||
|
|
||||||
val picture = findUriOrDefault(relativePath, filename) {
|
val picture = findUriOrDefault(relativePath, filename) {
|
||||||
|
|||||||
@@ -9,11 +9,14 @@ import androidx.work.ExistingWorkPolicy
|
|||||||
import androidx.work.ForegroundInfo
|
import androidx.work.ForegroundInfo
|
||||||
import androidx.work.OneTimeWorkRequestBuilder
|
import androidx.work.OneTimeWorkRequestBuilder
|
||||||
import androidx.work.PeriodicWorkRequestBuilder
|
import androidx.work.PeriodicWorkRequestBuilder
|
||||||
|
import androidx.work.WorkInfo
|
||||||
|
import androidx.work.WorkQuery
|
||||||
import androidx.work.WorkerParameters
|
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.isRunning
|
import eu.kanade.tachiyomi.util.system.isRunning
|
||||||
|
import eu.kanade.tachiyomi.util.system.setForegroundSafely
|
||||||
import eu.kanade.tachiyomi.util.system.workManager
|
import eu.kanade.tachiyomi.util.system.workManager
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import tachiyomi.core.common.util.system.logcat
|
import tachiyomi.core.common.util.system.logcat
|
||||||
@@ -27,19 +30,22 @@ class SyncDataJob(private val context: Context, workerParams: WorkerParameters)
|
|||||||
private val notifier = SyncNotifier(context)
|
private val notifier = SyncNotifier(context)
|
||||||
|
|
||||||
override suspend fun doWork(): Result {
|
override suspend fun doWork(): Result {
|
||||||
try {
|
if (tags.contains(TAG_AUTO)) {
|
||||||
setForeground(getForegroundInfo())
|
// Find a running manual worker. If exists, try again later
|
||||||
} catch (e: IllegalStateException) {
|
if (context.workManager.isRunning(TAG_MANUAL)) {
|
||||||
logcat(LogPriority.ERROR, e) { "Not allowed to run on foreground service" }
|
return Result.retry()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setForegroundSafely()
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
SyncManager(context).syncData()
|
SyncManager(context).syncData()
|
||||||
Result.success()
|
Result.success()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logcat(LogPriority.ERROR, e)
|
logcat(LogPriority.ERROR, e)
|
||||||
notifier.showSyncError(e.message)
|
notifier.showSyncError(e.message)
|
||||||
Result.failure()
|
Result.success() // try again next time
|
||||||
} finally {
|
} finally {
|
||||||
context.cancelNotification(Notifications.ID_RESTORE_PROGRESS)
|
context.cancelNotification(Notifications.ID_RESTORE_PROGRESS)
|
||||||
}
|
}
|
||||||
@@ -62,10 +68,8 @@ class SyncDataJob(private val context: Context, workerParams: WorkerParameters)
|
|||||||
private const val TAG_AUTO = "$TAG_JOB:auto"
|
private const val TAG_AUTO = "$TAG_JOB:auto"
|
||||||
const val TAG_MANUAL = "$TAG_JOB:manual"
|
const val TAG_MANUAL = "$TAG_JOB:manual"
|
||||||
|
|
||||||
private val jobTagList = listOf(TAG_AUTO, TAG_MANUAL)
|
fun isRunning(context: Context): Boolean {
|
||||||
|
return context.workManager.isRunning(TAG_JOB)
|
||||||
fun isAnyJobRunning(context: Context): Boolean {
|
|
||||||
return jobTagList.any { context.workManager.isRunning(it) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setupTask(context: Context, prefInterval: Int? = null) {
|
fun setupTask(context: Context, prefInterval: Int? = null) {
|
||||||
@@ -79,6 +83,7 @@ class SyncDataJob(private val context: Context, workerParams: WorkerParameters)
|
|||||||
10,
|
10,
|
||||||
TimeUnit.MINUTES,
|
TimeUnit.MINUTES,
|
||||||
)
|
)
|
||||||
|
.addTag(TAG_JOB)
|
||||||
.addTag(TAG_AUTO)
|
.addTag(TAG_AUTO)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
@@ -89,14 +94,33 @@ class SyncDataJob(private val context: Context, workerParams: WorkerParameters)
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun startNow(context: Context) {
|
fun startNow(context: Context) {
|
||||||
|
val wm = context.workManager
|
||||||
|
if (wm.isRunning(TAG_JOB)) {
|
||||||
|
// Already running either as a scheduled or manual job
|
||||||
|
return
|
||||||
|
}
|
||||||
val request = OneTimeWorkRequestBuilder<SyncDataJob>()
|
val request = OneTimeWorkRequestBuilder<SyncDataJob>()
|
||||||
|
.addTag(TAG_JOB)
|
||||||
.addTag(TAG_MANUAL)
|
.addTag(TAG_MANUAL)
|
||||||
.build()
|
.build()
|
||||||
context.workManager.enqueueUniqueWork(TAG_MANUAL, ExistingWorkPolicy.KEEP, request)
|
context.workManager.enqueueUniqueWork(TAG_MANUAL, ExistingWorkPolicy.KEEP, request)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun stop(context: Context) {
|
fun stop(context: Context) {
|
||||||
context.workManager.cancelUniqueWork(TAG_MANUAL)
|
val wm = context.workManager
|
||||||
|
val workQuery = WorkQuery.Builder.fromTags(listOf(TAG_JOB, TAG_AUTO, TAG_MANUAL))
|
||||||
|
.addStates(listOf(WorkInfo.State.RUNNING))
|
||||||
|
.build()
|
||||||
|
wm.getWorkInfos(workQuery).get()
|
||||||
|
// Should only return one work but just in case
|
||||||
|
.forEach {
|
||||||
|
wm.cancelWorkById(it.id)
|
||||||
|
|
||||||
|
// Re-enqueue cancelled scheduled work
|
||||||
|
if (it.tags.contains(TAG_AUTO)) {
|
||||||
|
setupTask(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,7 +88,14 @@ class SyncManager(
|
|||||||
appSettings = syncOptions.appSettings,
|
appSettings = syncOptions.appSettings,
|
||||||
sourceSettings = syncOptions.sourceSettings,
|
sourceSettings = syncOptions.sourceSettings,
|
||||||
privateSettings = syncOptions.privateSettings,
|
privateSettings = syncOptions.privateSettings,
|
||||||
|
|
||||||
|
// SY -->
|
||||||
|
customInfo = syncOptions.customInfo,
|
||||||
|
readEntries = syncOptions.readEntries,
|
||||||
|
// SY <--
|
||||||
)
|
)
|
||||||
|
|
||||||
|
logcat(LogPriority.DEBUG) { "Begin create backup" }
|
||||||
val backup = Backup(
|
val backup = Backup(
|
||||||
backupManga = backupCreator.backupMangas(databaseManga, backupOptions),
|
backupManga = backupCreator.backupMangas(databaseManga, backupOptions),
|
||||||
backupCategories = backupCreator.backupCategories(backupOptions),
|
backupCategories = backupCreator.backupCategories(backupOptions),
|
||||||
@@ -100,9 +107,11 @@ class SyncManager(
|
|||||||
backupSavedSearches = backupCreator.backupSavedSearches(),
|
backupSavedSearches = backupCreator.backupSavedSearches(),
|
||||||
// SY <--
|
// SY <--
|
||||||
)
|
)
|
||||||
|
logcat(LogPriority.DEBUG) { "End create backup" }
|
||||||
|
|
||||||
// Create the SyncData object
|
// Create the SyncData object
|
||||||
val syncData = SyncData(
|
val syncData = SyncData(
|
||||||
|
deviceId = syncPreferences.uniqueDeviceID(),
|
||||||
backup = backup,
|
backup = backup,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -129,8 +138,22 @@ class SyncManager(
|
|||||||
|
|
||||||
val remoteBackup = syncService?.doSync(syncData)
|
val remoteBackup = syncService?.doSync(syncData)
|
||||||
|
|
||||||
|
if (remoteBackup == null) {
|
||||||
|
logcat(LogPriority.DEBUG) { "Skip restore due to network issues" }
|
||||||
|
// should we call showSyncError?
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remoteBackup === syncData.backup){
|
||||||
|
// nothing changed
|
||||||
|
logcat(LogPriority.DEBUG) { "Skip restore due to remote was overwrite from local" }
|
||||||
|
syncPreferences.lastSyncTimestamp().set(Date().time)
|
||||||
|
notifier.showSyncSuccess("Sync completed successfully")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Stop the sync early if the remote backup is null or empty
|
// Stop the sync early if the remote backup is null or empty
|
||||||
if (remoteBackup?.backupManga?.size == 0) {
|
if (remoteBackup.backupManga.size == 0) {
|
||||||
notifier.showSyncError("No data found on remote server.")
|
notifier.showSyncError("No data found on remote server.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -143,49 +166,47 @@ class SyncManager(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (remoteBackup != null) {
|
val (filteredFavorites, nonFavorites) = filterFavoritesAndNonFavorites(remoteBackup)
|
||||||
val (filteredFavorites, nonFavorites) = filterFavoritesAndNonFavorites(remoteBackup)
|
updateNonFavorites(nonFavorites)
|
||||||
updateNonFavorites(nonFavorites)
|
|
||||||
|
|
||||||
val newSyncData = backup.copy(
|
val newSyncData = backup.copy(
|
||||||
backupManga = filteredFavorites,
|
backupManga = filteredFavorites,
|
||||||
backupCategories = remoteBackup.backupCategories,
|
backupCategories = remoteBackup.backupCategories,
|
||||||
backupSources = remoteBackup.backupSources,
|
backupSources = remoteBackup.backupSources,
|
||||||
backupPreferences = remoteBackup.backupPreferences,
|
backupPreferences = remoteBackup.backupPreferences,
|
||||||
backupSourcePreferences = remoteBackup.backupSourcePreferences,
|
backupSourcePreferences = remoteBackup.backupSourcePreferences,
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
backupSavedSearches = remoteBackup.backupSavedSearches,
|
backupSavedSearches = remoteBackup.backupSavedSearches,
|
||||||
// SY <--
|
// SY <--
|
||||||
|
)
|
||||||
|
|
||||||
|
// It's local sync no need to restore data. (just update remote data)
|
||||||
|
if (filteredFavorites.isEmpty()) {
|
||||||
|
// update the sync timestamp
|
||||||
|
syncPreferences.lastSyncTimestamp().set(Date().time)
|
||||||
|
notifier.showSyncSuccess("Sync completed successfully")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val backupUri = writeSyncDataToCache(context, newSyncData)
|
||||||
|
logcat(LogPriority.DEBUG) { "Got Backup Uri: $backupUri" }
|
||||||
|
if (backupUri != null) {
|
||||||
|
BackupRestoreJob.start(
|
||||||
|
context,
|
||||||
|
backupUri,
|
||||||
|
sync = true,
|
||||||
|
options = RestoreOptions(
|
||||||
|
appSettings = true,
|
||||||
|
sourceSettings = true,
|
||||||
|
library = true,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
// It's local sync no need to restore data. (just update remote data)
|
// update the sync timestamp
|
||||||
if (filteredFavorites.isEmpty()) {
|
syncPreferences.lastSyncTimestamp().set(Date().time)
|
||||||
// update the sync timestamp
|
} else {
|
||||||
syncPreferences.lastSyncTimestamp().set(Date().time)
|
logcat(LogPriority.ERROR) { "Failed to write sync data to file" }
|
||||||
notifier.showSyncSuccess("Sync completed successfully")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val backupUri = writeSyncDataToCache(context, newSyncData)
|
|
||||||
logcat(LogPriority.DEBUG) { "Got Backup Uri: $backupUri" }
|
|
||||||
if (backupUri != null) {
|
|
||||||
BackupRestoreJob.start(
|
|
||||||
context,
|
|
||||||
backupUri,
|
|
||||||
sync = true,
|
|
||||||
options = RestoreOptions(
|
|
||||||
appSettings = true,
|
|
||||||
sourceSettings = true,
|
|
||||||
library = true,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
// update the sync timestamp
|
|
||||||
syncPreferences.lastSyncTimestamp().set(Date().time)
|
|
||||||
} else {
|
|
||||||
logcat(LogPriority.ERROR) { "Failed to write sync data to file" }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.data.sync.models
|
|||||||
|
|
||||||
import dev.icerock.moko.resources.StringResource
|
import dev.icerock.moko.resources.StringResource
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.sy.SYMR
|
||||||
|
|
||||||
data class SyncTriggerOptions(
|
data class SyncTriggerOptions(
|
||||||
val syncOnChapterRead: Boolean = false,
|
val syncOnChapterRead: Boolean = false,
|
||||||
@@ -25,22 +25,22 @@ data class SyncTriggerOptions(
|
|||||||
companion object {
|
companion object {
|
||||||
val mainOptions = persistentListOf(
|
val mainOptions = persistentListOf(
|
||||||
Entry(
|
Entry(
|
||||||
label = MR.strings.sync_on_chapter_read,
|
label = SYMR.strings.sync_on_chapter_read,
|
||||||
getter = SyncTriggerOptions::syncOnChapterRead,
|
getter = SyncTriggerOptions::syncOnChapterRead,
|
||||||
setter = { options, enabled -> options.copy(syncOnChapterRead = enabled) },
|
setter = { options, enabled -> options.copy(syncOnChapterRead = enabled) },
|
||||||
),
|
),
|
||||||
Entry(
|
Entry(
|
||||||
label = MR.strings.sync_on_chapter_open,
|
label = SYMR.strings.sync_on_chapter_open,
|
||||||
getter = SyncTriggerOptions::syncOnChapterOpen,
|
getter = SyncTriggerOptions::syncOnChapterOpen,
|
||||||
setter = { options, enabled -> options.copy(syncOnChapterOpen = enabled) },
|
setter = { options, enabled -> options.copy(syncOnChapterOpen = enabled) },
|
||||||
),
|
),
|
||||||
Entry(
|
Entry(
|
||||||
label = MR.strings.sync_on_app_start,
|
label = SYMR.strings.sync_on_app_start,
|
||||||
getter = SyncTriggerOptions::syncOnAppStart,
|
getter = SyncTriggerOptions::syncOnAppStart,
|
||||||
setter = { options, enabled -> options.copy(syncOnAppStart = enabled) },
|
setter = { options, enabled -> options.copy(syncOnAppStart = enabled) },
|
||||||
),
|
),
|
||||||
Entry(
|
Entry(
|
||||||
label = MR.strings.sync_on_app_resume,
|
label = SYMR.strings.sync_on_app_resume,
|
||||||
getter = SyncTriggerOptions::syncOnAppResume,
|
getter = SyncTriggerOptions::syncOnAppResume,
|
||||||
setter = { options, enabled -> options.copy(syncOnAppResume = enabled) },
|
setter = { options, enabled -> options.copy(syncOnAppResume = enabled) },
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import com.google.api.client.googleapis.auth.oauth2.GoogleClientSecrets
|
|||||||
import com.google.api.client.googleapis.auth.oauth2.GoogleCredential
|
import com.google.api.client.googleapis.auth.oauth2.GoogleCredential
|
||||||
import com.google.api.client.googleapis.auth.oauth2.GoogleTokenResponse
|
import com.google.api.client.googleapis.auth.oauth2.GoogleTokenResponse
|
||||||
import com.google.api.client.http.ByteArrayContent
|
import com.google.api.client.http.ByteArrayContent
|
||||||
|
import com.google.api.client.http.InputStreamContent
|
||||||
import com.google.api.client.http.javanet.NetHttpTransport
|
import com.google.api.client.http.javanet.NetHttpTransport
|
||||||
import com.google.api.client.json.JsonFactory
|
import com.google.api.client.json.JsonFactory
|
||||||
import com.google.api.client.json.jackson2.JacksonFactory
|
import com.google.api.client.json.jackson2.JacksonFactory
|
||||||
@@ -18,19 +19,24 @@ import com.google.api.services.drive.Drive
|
|||||||
import com.google.api.services.drive.DriveScopes
|
import com.google.api.services.drive.DriveScopes
|
||||||
import com.google.api.services.drive.model.File
|
import com.google.api.services.drive.model.File
|
||||||
import eu.kanade.domain.sync.SyncPreferences
|
import eu.kanade.domain.sync.SyncPreferences
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.Backup
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlinx.serialization.json.decodeFromStream
|
||||||
|
import kotlinx.serialization.json.encodeToStream
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import logcat.logcat
|
import logcat.logcat
|
||||||
import tachiyomi.core.common.i18n.stringResource
|
import tachiyomi.core.common.i18n.stringResource
|
||||||
import tachiyomi.core.common.util.lang.withIOContext
|
import tachiyomi.core.common.util.lang.withIOContext
|
||||||
import tachiyomi.core.common.util.system.logcat
|
import tachiyomi.core.common.util.system.logcat
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
|
import tachiyomi.i18n.sy.SYMR
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.io.ByteArrayOutputStream
|
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
import java.io.PipedInputStream
|
||||||
|
import java.io.PipedOutputStream
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.util.zip.GZIPInputStream
|
import java.util.zip.GZIPInputStream
|
||||||
import java.util.zip.GZIPOutputStream
|
import java.util.zip.GZIPOutputStream
|
||||||
@@ -64,11 +70,47 @@ class GoogleDriveSyncService(context: Context, json: Json, syncPreferences: Sync
|
|||||||
|
|
||||||
private val googleDriveService = GoogleDriveService(context)
|
private val googleDriveService = GoogleDriveService(context)
|
||||||
|
|
||||||
override suspend fun beforeSync() {
|
override suspend fun doSync(syncData: SyncData): Backup? {
|
||||||
|
beforeSync()
|
||||||
|
|
||||||
|
try {
|
||||||
|
val remoteSData = pullSyncData()
|
||||||
|
|
||||||
|
if (remoteSData != null ){
|
||||||
|
// Get local unique device ID
|
||||||
|
val localDeviceId = syncPreferences.uniqueDeviceID()
|
||||||
|
val lastSyncDeviceId = remoteSData.deviceId
|
||||||
|
|
||||||
|
// Log the device IDs
|
||||||
|
logcat(LogPriority.DEBUG, "SyncService") {
|
||||||
|
"Local device ID: $localDeviceId, Last sync device ID: $lastSyncDeviceId"
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if the last sync was done by the same device if so overwrite the remote data with the local data
|
||||||
|
return if (lastSyncDeviceId == localDeviceId) {
|
||||||
|
pushSyncData(syncData)
|
||||||
|
syncData.backup
|
||||||
|
}else{
|
||||||
|
// Merge the local and remote sync data
|
||||||
|
val mergedSyncData = mergeSyncData(syncData, remoteSData)
|
||||||
|
pushSyncData(mergedSyncData)
|
||||||
|
mergedSyncData.backup
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pushSyncData(syncData)
|
||||||
|
return syncData.backup
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logcat(LogPriority.ERROR, "SyncService") { "Error syncing: ${e.message}" }
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun beforeSync() {
|
||||||
try {
|
try {
|
||||||
googleDriveService.refreshToken()
|
googleDriveService.refreshToken()
|
||||||
val drive = googleDriveService.driveService
|
val drive = googleDriveService.driveService
|
||||||
?: throw Exception(context.stringResource(MR.strings.google_drive_not_signed_in))
|
?: throw Exception(context.stringResource(SYMR.strings.google_drive_not_signed_in))
|
||||||
|
|
||||||
var backoff = 1000L
|
var backoff = 1000L
|
||||||
var retries = 0 // Retry counter
|
var retries = 0 // Retry counter
|
||||||
@@ -112,21 +154,17 @@ class GoogleDriveSyncService(context: Context, json: Json, syncPreferences: Sync
|
|||||||
|
|
||||||
if (retries >= maxRetries) {
|
if (retries >= maxRetries) {
|
||||||
logcat(LogPriority.ERROR) { "Max retries reached, exiting sync process" }
|
logcat(LogPriority.ERROR) { "Max retries reached, exiting sync process" }
|
||||||
throw Exception(context.stringResource(MR.strings.error_before_sync_gdrive) + ": Max retries reached.")
|
throw Exception(context.stringResource(SYMR.strings.error_before_sync_gdrive) + ": Max retries reached.")
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logcat(LogPriority.ERROR, throwable = e) { "Error in GoogleDrive beforeSync" }
|
logcat(LogPriority.ERROR, throwable = e) { "Error in GoogleDrive beforeSync" }
|
||||||
throw Exception(context.stringResource(MR.strings.error_before_sync_gdrive) + ": ${e.message}", e)
|
throw Exception(context.stringResource(SYMR.strings.error_before_sync_gdrive) + ": ${e.message}", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun pullSyncData(): SyncData? {
|
private fun pullSyncData(): SyncData? {
|
||||||
val drive = googleDriveService.driveService
|
val drive = googleDriveService.driveService ?:
|
||||||
|
throw Exception(context.stringResource(SYMR.strings.google_drive_not_signed_in))
|
||||||
if (drive == null) {
|
|
||||||
logcat(LogPriority.DEBUG) { "Google Drive service not initialized" }
|
|
||||||
throw Exception(context.stringResource(MR.strings.google_drive_not_signed_in))
|
|
||||||
}
|
|
||||||
|
|
||||||
val fileList = getAppDataFileList(drive)
|
val fileList = getAppDataFileList(drive)
|
||||||
if (fileList.isEmpty()) {
|
if (fileList.isEmpty()) {
|
||||||
@@ -137,75 +175,53 @@ class GoogleDriveSyncService(context: Context, json: Json, syncPreferences: Sync
|
|||||||
val gdriveFileId = fileList[0].id
|
val gdriveFileId = fileList[0].id
|
||||||
logcat(LogPriority.DEBUG) { "Google Drive File ID: $gdriveFileId" }
|
logcat(LogPriority.DEBUG) { "Google Drive File ID: $gdriveFileId" }
|
||||||
|
|
||||||
val outputStream = ByteArrayOutputStream()
|
|
||||||
try {
|
try {
|
||||||
drive.files().get(gdriveFileId).executeMediaAndDownloadTo(outputStream)
|
drive.files().get(gdriveFileId).executeMediaAsInputStream().use { inputStream ->
|
||||||
logcat(LogPriority.DEBUG) { "File downloaded successfully" }
|
GZIPInputStream(inputStream).use { gzipInputStream ->
|
||||||
|
return Json.decodeFromStream(SyncData.serializer(), gzipInputStream)
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logcat(LogPriority.ERROR, throwable = e) { "Error downloading file" }
|
logcat(LogPriority.ERROR, throwable = e) { "Error downloading file" }
|
||||||
return null
|
throw Exception("Failed to download sync data: ${e.message}", e)
|
||||||
}
|
|
||||||
|
|
||||||
return withIOContext {
|
|
||||||
try {
|
|
||||||
val gzipInputStream = GZIPInputStream(outputStream.toByteArray().inputStream())
|
|
||||||
val jsonString = gzipInputStream.bufferedReader().use { it.readText() }
|
|
||||||
val syncData = json.decodeFromString(SyncData.serializer(), jsonString)
|
|
||||||
this@GoogleDriveSyncService.logcat(LogPriority.DEBUG) { "JSON deserialized successfully" }
|
|
||||||
syncData
|
|
||||||
} catch (e: Exception) {
|
|
||||||
this@GoogleDriveSyncService.logcat(
|
|
||||||
LogPriority.ERROR,
|
|
||||||
throwable = e,
|
|
||||||
) { "Failed to convert json to sync data with kotlinx.serialization" }
|
|
||||||
throw Exception(e.message, e)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun pushSyncData(syncData: SyncData) {
|
private suspend fun pushSyncData(syncData: SyncData) {
|
||||||
val jsonData = json.encodeToString(syncData)
|
|
||||||
val drive = googleDriveService.driveService
|
val drive = googleDriveService.driveService
|
||||||
?: throw Exception(context.stringResource(MR.strings.google_drive_not_signed_in))
|
?: throw Exception(context.stringResource(SYMR.strings.google_drive_not_signed_in))
|
||||||
|
|
||||||
val fileList = getAppDataFileList(drive)
|
val fileList = getAppDataFileList(drive)
|
||||||
val byteArrayOutputStream = ByteArrayOutputStream()
|
|
||||||
withIOContext {
|
|
||||||
GZIPOutputStream(byteArrayOutputStream).use { gzipOutputStream ->
|
|
||||||
gzipOutputStream.write(jsonData.toByteArray(Charsets.UTF_8))
|
|
||||||
}
|
|
||||||
this@GoogleDriveSyncService.logcat(LogPriority.DEBUG) { "JSON serialized successfully" }
|
|
||||||
}
|
|
||||||
|
|
||||||
val byteArrayContent = ByteArrayContent("application/octet-stream", byteArrayOutputStream.toByteArray())
|
PipedOutputStream().use { pos ->
|
||||||
|
PipedInputStream(pos).use { pis ->
|
||||||
|
withIOContext {
|
||||||
|
// Start a coroutine or a background thread to write JSON to the PipedOutputStream
|
||||||
|
launch {
|
||||||
|
GZIPOutputStream(pos).use { gzipOutputStream ->
|
||||||
|
Json.encodeToStream(SyncData.serializer(), syncData, gzipOutputStream)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
if (fileList.isNotEmpty()) {
|
||||||
if (fileList.isNotEmpty()) {
|
val fileId = fileList[0].id
|
||||||
// File exists, so update it
|
val mediaContent = InputStreamContent("application/gzip", pis)
|
||||||
val fileId = fileList[0].id
|
drive.files().update(fileId, null, mediaContent).execute()
|
||||||
drive.files().update(fileId, null, byteArrayContent).execute()
|
logcat(LogPriority.DEBUG) { "Updated existing sync data file in Google Drive with file ID: $fileId" }
|
||||||
logcat(LogPriority.DEBUG) { "Updated existing sync data file in Google Drive with file ID: $fileId" }
|
} else {
|
||||||
} else {
|
val fileMetadata = File().apply {
|
||||||
// File doesn't exist, so create it
|
name = remoteFileName
|
||||||
val fileMetadata = File().apply {
|
mimeType = "application/gzip"
|
||||||
name = remoteFileName
|
parents = listOf("appDataFolder")
|
||||||
mimeType = "application/gzip"
|
}
|
||||||
parents = listOf("appDataFolder")
|
val mediaContent = InputStreamContent("application/gzip", pis)
|
||||||
|
val uploadedFile = drive.files().create(fileMetadata, mediaContent)
|
||||||
|
.setFields("id")
|
||||||
|
.execute()
|
||||||
|
logcat(LogPriority.DEBUG) { "Created new sync data file in Google Drive with file ID: ${uploadedFile.id}" }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
val uploadedFile = drive.files().create(fileMetadata, byteArrayContent)
|
|
||||||
.setFields("id")
|
|
||||||
.execute()
|
|
||||||
|
|
||||||
logcat(
|
|
||||||
LogPriority.DEBUG,
|
|
||||||
) { "Created new sync data file in Google Drive with file ID: ${uploadedFile.id}" }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Data has been successfully pushed or updated, delete the lock file
|
|
||||||
deleteLockFile(drive)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
logcat(LogPriority.ERROR, throwable = e) { "Failed to push or update sync data" }
|
|
||||||
throw Exception(context.stringResource(MR.strings.error_uploading_sync_data) + ": ${e.message}", e)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -282,7 +298,7 @@ class GoogleDriveSyncService(context: Context, json: Json, syncPreferences: Sync
|
|||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logcat(LogPriority.ERROR, throwable = e) { "Error deleting lock file" }
|
logcat(LogPriority.ERROR, throwable = e) { "Error deleting lock file" }
|
||||||
throw Exception(context.stringResource(MR.strings.error_deleting_google_drive_lock_file), e)
|
throw Exception(context.stringResource(SYMR.strings.error_deleting_google_drive_lock_file), e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -392,7 +408,6 @@ class GoogleDriveService(private val context: Context) {
|
|||||||
}
|
}
|
||||||
internal suspend fun refreshToken() = withIOContext {
|
internal suspend fun refreshToken() = withIOContext {
|
||||||
val refreshToken = syncPreferences.googleDriveRefreshToken().get()
|
val refreshToken = syncPreferences.googleDriveRefreshToken().get()
|
||||||
val accessToken = syncPreferences.googleDriveAccessToken().get()
|
|
||||||
|
|
||||||
val jsonFactory: JsonFactory = JacksonFactory.getDefaultInstance()
|
val jsonFactory: JsonFactory = JacksonFactory.getDefaultInstance()
|
||||||
val secrets = GoogleClientSecrets.load(
|
val secrets = GoogleClientSecrets.load(
|
||||||
@@ -407,21 +422,17 @@ class GoogleDriveService(private val context: Context) {
|
|||||||
.build()
|
.build()
|
||||||
|
|
||||||
if (refreshToken == "") {
|
if (refreshToken == "") {
|
||||||
throw Exception(context.stringResource(MR.strings.google_drive_not_signed_in))
|
throw Exception(context.stringResource(SYMR.strings.google_drive_not_signed_in))
|
||||||
}
|
}
|
||||||
|
|
||||||
credential.refreshToken = refreshToken
|
credential.refreshToken = refreshToken
|
||||||
|
|
||||||
this@GoogleDriveService.logcat(LogPriority.DEBUG) { "Refreshing access token with: $refreshToken" }
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
credential.refreshToken()
|
credential.refreshToken()
|
||||||
val newAccessToken = credential.accessToken
|
val newAccessToken = credential.accessToken
|
||||||
// Save the new access token
|
// Save the new access token
|
||||||
syncPreferences.googleDriveAccessToken().set(newAccessToken)
|
syncPreferences.googleDriveAccessToken().set(newAccessToken)
|
||||||
setupGoogleDriveService(newAccessToken, credential.refreshToken)
|
setupGoogleDriveService(newAccessToken, credential.refreshToken)
|
||||||
this@GoogleDriveService
|
|
||||||
.logcat(LogPriority.DEBUG) { "Google Access token refreshed old: $accessToken new: $newAccessToken" }
|
|
||||||
} catch (e: TokenResponseException) {
|
} catch (e: TokenResponseException) {
|
||||||
if (e.details.error == "invalid_grant") {
|
if (e.details.error == "invalid_grant") {
|
||||||
// The refresh token is invalid, prompt the user to sign in again
|
// The refresh token is invalid, prompt the user to sign in again
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import logcat.logcat
|
|||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class SyncData(
|
data class SyncData(
|
||||||
|
val deviceId: String = "",
|
||||||
val backup: Backup? = null,
|
val backup: Backup? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -25,38 +26,7 @@ abstract class SyncService(
|
|||||||
val json: Json,
|
val json: Json,
|
||||||
val syncPreferences: SyncPreferences,
|
val syncPreferences: SyncPreferences,
|
||||||
) {
|
) {
|
||||||
open suspend fun doSync(syncData: SyncData): Backup? {
|
abstract suspend fun doSync(syncData: SyncData): Backup?;
|
||||||
beforeSync()
|
|
||||||
|
|
||||||
val remoteSData = pullSyncData()
|
|
||||||
|
|
||||||
val finalSyncData =
|
|
||||||
if (remoteSData == null) {
|
|
||||||
pushSyncData(syncData)
|
|
||||||
syncData
|
|
||||||
} else {
|
|
||||||
val mergedSyncData = mergeSyncData(syncData, remoteSData)
|
|
||||||
pushSyncData(mergedSyncData)
|
|
||||||
mergedSyncData
|
|
||||||
}
|
|
||||||
|
|
||||||
return finalSyncData.backup
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* For refreshing tokens and other possible operations before connecting to the remote storage
|
|
||||||
*/
|
|
||||||
open suspend fun beforeSync() {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Download sync data from the remote storage
|
|
||||||
*/
|
|
||||||
abstract suspend fun pullSyncData(): SyncData?
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Upload sync data to the remote storage
|
|
||||||
*/
|
|
||||||
abstract suspend fun pushSyncData(syncData: SyncData)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Merges the local and remote sync data into a single JSON string.
|
* Merges the local and remote sync data into a single JSON string.
|
||||||
@@ -65,11 +35,17 @@ abstract class SyncService(
|
|||||||
* @param remoteSyncData The SData containing the remote sync data.
|
* @param remoteSyncData The SData containing the remote sync data.
|
||||||
* @return The JSON string containing the merged sync data.
|
* @return The JSON string containing the merged sync data.
|
||||||
*/
|
*/
|
||||||
private fun mergeSyncData(localSyncData: SyncData, remoteSyncData: SyncData): SyncData {
|
protected fun mergeSyncData(localSyncData: SyncData, remoteSyncData: SyncData): SyncData {
|
||||||
val mergedMangaList = mergeMangaLists(localSyncData.backup?.backupManga, remoteSyncData.backup?.backupManga)
|
|
||||||
val mergedCategoriesList =
|
val mergedCategoriesList =
|
||||||
mergeCategoriesLists(localSyncData.backup?.backupCategories, remoteSyncData.backup?.backupCategories)
|
mergeCategoriesLists(localSyncData.backup?.backupCategories, remoteSyncData.backup?.backupCategories)
|
||||||
|
|
||||||
|
val mergedMangaList = mergeMangaLists(
|
||||||
|
localSyncData.backup?.backupManga,
|
||||||
|
remoteSyncData.backup?.backupManga,
|
||||||
|
localSyncData.backup?.backupCategories ?: emptyList(),
|
||||||
|
remoteSyncData.backup?.backupCategories ?: emptyList(),
|
||||||
|
mergedCategoriesList)
|
||||||
|
|
||||||
val mergedSourcesList =
|
val mergedSourcesList =
|
||||||
mergeSourcesLists(localSyncData.backup?.backupSources, remoteSyncData.backup?.backupSources)
|
mergeSourcesLists(localSyncData.backup?.backupSources, remoteSyncData.backup?.backupSources)
|
||||||
val mergedPreferencesList =
|
val mergedPreferencesList =
|
||||||
@@ -101,6 +77,7 @@ abstract class SyncService(
|
|||||||
|
|
||||||
// Create the merged SData object
|
// Create the merged SData object
|
||||||
return SyncData(
|
return SyncData(
|
||||||
|
deviceId = syncPreferences.uniqueDeviceID(),
|
||||||
backup = mergedBackup,
|
backup = mergedBackup,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -117,6 +94,9 @@ abstract class SyncService(
|
|||||||
private fun mergeMangaLists(
|
private fun mergeMangaLists(
|
||||||
localMangaList: List<BackupManga>?,
|
localMangaList: List<BackupManga>?,
|
||||||
remoteMangaList: List<BackupManga>?,
|
remoteMangaList: List<BackupManga>?,
|
||||||
|
localCategories: List<BackupCategory>,
|
||||||
|
remoteCategories: List<BackupCategory>,
|
||||||
|
mergedCategories: List<BackupCategory>,
|
||||||
): List<BackupManga> {
|
): List<BackupManga> {
|
||||||
val logTag = "MergeMangaLists"
|
val logTag = "MergeMangaLists"
|
||||||
|
|
||||||
@@ -135,6 +115,18 @@ abstract class SyncService(
|
|||||||
val localMangaMap = localMangaListSafe.associateBy { mangaCompositeKey(it) }
|
val localMangaMap = localMangaListSafe.associateBy { mangaCompositeKey(it) }
|
||||||
val remoteMangaMap = remoteMangaListSafe.associateBy { mangaCompositeKey(it) }
|
val remoteMangaMap = remoteMangaListSafe.associateBy { mangaCompositeKey(it) }
|
||||||
|
|
||||||
|
val localCategoriesMapByOrder = localCategories.associateBy { it.order }
|
||||||
|
val remoteCategoriesMapByOrder = remoteCategories.associateBy { it.order }
|
||||||
|
val mergedCategoriesMapByName = mergedCategories.associateBy { it.name }
|
||||||
|
|
||||||
|
fun updateCategories(theManga: BackupManga, theMap: Map<Long, BackupCategory>): BackupManga {
|
||||||
|
return theManga.copy(categories = theManga.categories.mapNotNull {
|
||||||
|
theMap[it]?.let { category ->
|
||||||
|
mergedCategoriesMapByName[category.name]?.order
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
logcat(LogPriority.DEBUG, logTag) {
|
logcat(LogPriority.DEBUG, logTag) {
|
||||||
"Starting merge. Local list size: ${localMangaListSafe.size}, Remote list size: ${remoteMangaListSafe.size}"
|
"Starting merge. Local list size: ${localMangaListSafe.size}, Remote list size: ${remoteMangaListSafe.size}"
|
||||||
}
|
}
|
||||||
@@ -145,20 +137,26 @@ abstract class SyncService(
|
|||||||
|
|
||||||
// New version comparison logic
|
// New version comparison logic
|
||||||
when {
|
when {
|
||||||
local != null && remote == null -> local
|
local != null && remote == null -> updateCategories(local, localCategoriesMapByOrder)
|
||||||
local == null && remote != null -> remote
|
local == null && remote != null -> updateCategories(remote, remoteCategoriesMapByOrder)
|
||||||
local != null && remote != null -> {
|
local != null && remote != null -> {
|
||||||
// Compare versions to decide which manga to keep
|
// Compare versions to decide which manga to keep
|
||||||
if (local.version >= remote.version) {
|
if (local.version >= remote.version) {
|
||||||
logcat(LogPriority.DEBUG, logTag) {
|
logcat(LogPriority.DEBUG, logTag) {
|
||||||
"Keeping local version of ${local.title} with merged chapters."
|
"Keeping local version of ${local.title} with merged chapters."
|
||||||
}
|
}
|
||||||
local.copy(chapters = mergeChapters(local.chapters, remote.chapters))
|
updateCategories(
|
||||||
|
local.copy(chapters = mergeChapters(local.chapters, remote.chapters)),
|
||||||
|
localCategoriesMapByOrder
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
logcat(LogPriority.DEBUG, logTag) {
|
logcat(LogPriority.DEBUG, logTag) {
|
||||||
"Keeping remote version of ${remote.title} with merged chapters."
|
"Keeping remote version of ${remote.title} with merged chapters."
|
||||||
}
|
}
|
||||||
remote.copy(chapters = mergeChapters(local.chapters, remote.chapters))
|
updateCategories(
|
||||||
|
remote.copy(chapters = mergeChapters(local.chapters, remote.chapters)),
|
||||||
|
remoteCategoriesMapByOrder
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else -> null // No manga found for key
|
else -> null // No manga found for key
|
||||||
|
|||||||
@@ -2,23 +2,26 @@ package eu.kanade.tachiyomi.data.sync.service
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import eu.kanade.domain.sync.SyncPreferences
|
import eu.kanade.domain.sync.SyncPreferences
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.Backup
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.BackupSerializer
|
||||||
import eu.kanade.tachiyomi.data.sync.SyncNotifier
|
import eu.kanade.tachiyomi.data.sync.SyncNotifier
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.network.PATCH
|
import eu.kanade.tachiyomi.network.PUT
|
||||||
import eu.kanade.tachiyomi.network.POST
|
|
||||||
import eu.kanade.tachiyomi.network.await
|
import eu.kanade.tachiyomi.network.await
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.serialization.SerializationException
|
||||||
import kotlinx.serialization.SerialName
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import kotlinx.serialization.encodeToString
|
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlinx.serialization.protobuf.ProtoBuf
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
|
import logcat.logcat
|
||||||
import okhttp3.Headers
|
import okhttp3.Headers
|
||||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.RequestBody.Companion.gzip
|
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
import tachiyomi.core.common.util.system.logcat
|
import org.apache.http.HttpStatus
|
||||||
|
import tachiyomi.core.common.i18n.stringResource
|
||||||
|
import tachiyomi.i18n.MR
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
class SyncYomiSyncService(
|
class SyncYomiSyncService(
|
||||||
@@ -26,140 +29,117 @@ class SyncYomiSyncService(
|
|||||||
json: Json,
|
json: Json,
|
||||||
syncPreferences: SyncPreferences,
|
syncPreferences: SyncPreferences,
|
||||||
private val notifier: SyncNotifier,
|
private val notifier: SyncNotifier,
|
||||||
|
|
||||||
|
private val protoBuf: ProtoBuf = Injekt.get(),
|
||||||
) : SyncService(context, json, syncPreferences) {
|
) : SyncService(context, json, syncPreferences) {
|
||||||
|
|
||||||
@Serializable
|
private class SyncYomiException(message: String?) : Exception(message)
|
||||||
enum class SyncStatus {
|
|
||||||
@SerialName("pending")
|
|
||||||
Pending,
|
|
||||||
|
|
||||||
@SerialName("syncing")
|
override suspend fun doSync(syncData: SyncData): Backup? {
|
||||||
Syncing,
|
try {
|
||||||
|
val (remoteData, etag) = pullSyncData()
|
||||||
|
|
||||||
@SerialName("success")
|
val finalSyncData = if (remoteData != null){
|
||||||
Success,
|
assert(etag.isNotEmpty()) { "ETag should never be empty if remote data is not null" }
|
||||||
}
|
logcat(LogPriority.DEBUG, "SyncService") {
|
||||||
|
"Try update remote data with ETag($etag)"
|
||||||
@Serializable
|
}
|
||||||
data class LockFile(
|
mergeSyncData(syncData, remoteData)
|
||||||
@SerialName("id")
|
} else {
|
||||||
val id: Int?,
|
// init or overwrite remote data
|
||||||
@SerialName("user_api_key")
|
logcat(LogPriority.DEBUG) {
|
||||||
val userApiKey: String?,
|
"Try overwrite remote data with ETag($etag)"
|
||||||
@SerialName("acquired_by")
|
}
|
||||||
val acquiredBy: String?,
|
syncData
|
||||||
@SerialName("last_synced")
|
|
||||||
val lastSynced: String?,
|
|
||||||
@SerialName("status")
|
|
||||||
val status: SyncStatus,
|
|
||||||
@SerialName("acquired_at")
|
|
||||||
val acquiredAt: String?,
|
|
||||||
@SerialName("expires_at")
|
|
||||||
val expiresAt: String?,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class LockfileCreateRequest(
|
|
||||||
@SerialName("acquired_by")
|
|
||||||
val acquiredBy: String,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class LockfilePatchRequest(
|
|
||||||
@SerialName("user_api_key")
|
|
||||||
val userApiKey: String,
|
|
||||||
@SerialName("acquired_by")
|
|
||||||
val acquiredBy: String,
|
|
||||||
)
|
|
||||||
|
|
||||||
override suspend fun beforeSync() {
|
|
||||||
val host = syncPreferences.clientHost().get()
|
|
||||||
val apiKey = syncPreferences.clientAPIKey().get()
|
|
||||||
val lockFileApi = "$host/api/sync/lock"
|
|
||||||
val deviceId = syncPreferences.uniqueDeviceID()
|
|
||||||
val client = OkHttpClient()
|
|
||||||
val headers = Headers.Builder().add("X-API-Token", apiKey).build()
|
|
||||||
val json = Json { ignoreUnknownKeys = true }
|
|
||||||
|
|
||||||
val createLockfileRequest = LockfileCreateRequest(deviceId)
|
|
||||||
val createLockfileJson = json.encodeToString(createLockfileRequest)
|
|
||||||
|
|
||||||
val patchRequest = LockfilePatchRequest(apiKey, deviceId)
|
|
||||||
val patchJson = json.encodeToString(patchRequest)
|
|
||||||
|
|
||||||
val lockFileRequest = GET(
|
|
||||||
url = lockFileApi,
|
|
||||||
headers = headers,
|
|
||||||
)
|
|
||||||
|
|
||||||
val lockFileCreate = POST(
|
|
||||||
url = lockFileApi,
|
|
||||||
headers = headers,
|
|
||||||
body = createLockfileJson.toRequestBody("application/json; charset=utf-8".toMediaTypeOrNull()),
|
|
||||||
)
|
|
||||||
|
|
||||||
val lockFileUpdate = PATCH(
|
|
||||||
url = lockFileApi,
|
|
||||||
headers = headers,
|
|
||||||
body = patchJson.toRequestBody("application/json; charset=utf-8".toMediaTypeOrNull()),
|
|
||||||
)
|
|
||||||
|
|
||||||
// create lock file first
|
|
||||||
client.newCall(lockFileCreate).await()
|
|
||||||
// update lock file acquired_by
|
|
||||||
client.newCall(lockFileUpdate).await()
|
|
||||||
|
|
||||||
var backoff = 2000L // Start with 2 seconds
|
|
||||||
val maxBackoff = 32000L // Maximum backoff time e.g., 32 seconds
|
|
||||||
var lockFile: LockFile
|
|
||||||
do {
|
|
||||||
val response = client.newCall(lockFileRequest).await()
|
|
||||||
val responseBody = response.body.string()
|
|
||||||
lockFile = json.decodeFromString<LockFile>(responseBody)
|
|
||||||
logcat(LogPriority.DEBUG) { "SyncYomi lock file status: ${lockFile.status}" }
|
|
||||||
|
|
||||||
if (lockFile.status != SyncStatus.Success) {
|
|
||||||
logcat(LogPriority.DEBUG) { "Lock file not ready, retrying in $backoff ms..." }
|
|
||||||
delay(backoff)
|
|
||||||
backoff = (backoff * 2).coerceAtMost(maxBackoff)
|
|
||||||
}
|
}
|
||||||
} while (lockFile.status != SyncStatus.Success)
|
|
||||||
|
|
||||||
// update lock file acquired_by
|
pushSyncData(finalSyncData, etag)
|
||||||
client.newCall(lockFileUpdate).await()
|
return finalSyncData.backup
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logcat(LogPriority.ERROR) { "Error syncing: ${e.message}" }
|
||||||
|
notifier.showSyncError(e.message)
|
||||||
|
return null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun pullSyncData(): SyncData? {
|
private suspend fun pullSyncData(): Pair<SyncData?, String> {
|
||||||
val host = syncPreferences.clientHost().get()
|
val host = syncPreferences.clientHost().get()
|
||||||
val apiKey = syncPreferences.clientAPIKey().get()
|
val apiKey = syncPreferences.clientAPIKey().get()
|
||||||
val downloadUrl = "$host/api/sync/download"
|
val downloadUrl = "$host/api/sync/content"
|
||||||
|
|
||||||
val client = OkHttpClient()
|
val headersBuilder = Headers.Builder().add("X-API-Token", apiKey)
|
||||||
val headers = Headers.Builder().add("X-API-Token", apiKey).build()
|
val lastETag = syncPreferences.lastSyncEtag().get()
|
||||||
|
if (lastETag != "") {
|
||||||
|
headersBuilder.add("If-None-Match", lastETag)
|
||||||
|
}
|
||||||
|
val headers = headersBuilder.build()
|
||||||
|
|
||||||
val downloadRequest = GET(
|
val downloadRequest = GET(
|
||||||
url = downloadUrl,
|
url = downloadUrl,
|
||||||
headers = headers,
|
headers = headers,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val client = OkHttpClient()
|
||||||
val response = client.newCall(downloadRequest).await()
|
val response = client.newCall(downloadRequest).await()
|
||||||
val responseBody = response.body.string()
|
|
||||||
|
|
||||||
return if (response.isSuccessful) {
|
if (response.code == HttpStatus.SC_NOT_MODIFIED) {
|
||||||
json.decodeFromString<SyncData>(responseBody)
|
// not modified
|
||||||
|
assert(lastETag.isNotEmpty())
|
||||||
|
logcat(LogPriority.INFO) {
|
||||||
|
"Remote server not modified"
|
||||||
|
}
|
||||||
|
return Pair(null, lastETag)
|
||||||
|
} else if (response.code == HttpStatus.SC_NOT_FOUND) {
|
||||||
|
// maybe got deleted from remote
|
||||||
|
return Pair(null, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.isSuccessful) {
|
||||||
|
val newETag = response.headers["ETag"]
|
||||||
|
.takeIf { it?.isNotEmpty() == true } ?: throw SyncYomiException("Missing ETag")
|
||||||
|
|
||||||
|
val byteArray = response.body.byteStream().use {
|
||||||
|
return@use it.readBytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
return try {
|
||||||
|
val backup = protoBuf.decodeFromByteArray(BackupSerializer, byteArray)
|
||||||
|
return Pair(SyncData(backup = backup), newETag)
|
||||||
|
} catch (_: SerializationException) {
|
||||||
|
logcat(LogPriority.INFO) {
|
||||||
|
"Bad content responsed from server"
|
||||||
|
}
|
||||||
|
// the body is invalid
|
||||||
|
// return default value so we can overwrite it
|
||||||
|
Pair(null, "")
|
||||||
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
val responseBody = response.body.string()
|
||||||
notifier.showSyncError("Failed to download sync data: $responseBody")
|
notifier.showSyncError("Failed to download sync data: $responseBody")
|
||||||
responseBody.let { logcat(LogPriority.ERROR) { "SyncError:$it" } }
|
logcat(LogPriority.ERROR) { "SyncError: $responseBody" }
|
||||||
null
|
throw SyncYomiException("Failed to download sync data: $responseBody")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun pushSyncData(syncData: SyncData) {
|
/**
|
||||||
|
* Return true if update success
|
||||||
|
*/
|
||||||
|
private suspend fun pushSyncData(syncData: SyncData, eTag: String) {
|
||||||
|
val backup = syncData.backup ?: return
|
||||||
|
|
||||||
val host = syncPreferences.clientHost().get()
|
val host = syncPreferences.clientHost().get()
|
||||||
val apiKey = syncPreferences.clientAPIKey().get()
|
val apiKey = syncPreferences.clientAPIKey().get()
|
||||||
val uploadUrl = "$host/api/sync/upload"
|
val uploadUrl = "$host/api/sync/content"
|
||||||
val timeout = 30L
|
val timeout = 30L
|
||||||
|
|
||||||
|
val headersBuilder = Headers.Builder().add("X-API-Token", apiKey)
|
||||||
|
if (eTag.isNotEmpty()) {
|
||||||
|
headersBuilder.add("If-Match", eTag)
|
||||||
|
}
|
||||||
|
val headers = headersBuilder.build()
|
||||||
|
|
||||||
// Set timeout to 30 seconds
|
// Set timeout to 30 seconds
|
||||||
val client = OkHttpClient.Builder()
|
val client = OkHttpClient.Builder()
|
||||||
.connectTimeout(timeout, TimeUnit.SECONDS)
|
.connectTimeout(timeout, TimeUnit.SECONDS)
|
||||||
@@ -167,32 +147,34 @@ class SyncYomiSyncService(
|
|||||||
.writeTimeout(timeout, TimeUnit.SECONDS)
|
.writeTimeout(timeout, TimeUnit.SECONDS)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val headers = Headers.Builder().add(
|
val byteArray = protoBuf.encodeToByteArray(BackupSerializer, backup)
|
||||||
"Content-Type",
|
if (byteArray.isEmpty()) {
|
||||||
"application/gzip",
|
throw IllegalStateException(context.stringResource(MR.strings.empty_backup_error))
|
||||||
).add("Content-Encoding", "gzip").add("X-API-Token", apiKey).build()
|
}
|
||||||
|
val body = byteArray.toRequestBody("application/octet-stream".toMediaType())
|
||||||
|
|
||||||
val mediaType = "application/gzip".toMediaTypeOrNull()
|
val uploadRequest = PUT(
|
||||||
|
|
||||||
val jsonData = json.encodeToString(syncData)
|
|
||||||
val body = jsonData.toRequestBody(mediaType).gzip()
|
|
||||||
|
|
||||||
val uploadRequest = POST(
|
|
||||||
url = uploadUrl,
|
url = uploadUrl,
|
||||||
headers = headers,
|
headers = headers,
|
||||||
body = body,
|
body = body,
|
||||||
)
|
)
|
||||||
|
|
||||||
client.newCall(uploadRequest).await().use {
|
val response = client.newCall(uploadRequest).await()
|
||||||
if (it.isSuccessful) {
|
|
||||||
logcat(
|
if (response.isSuccessful) {
|
||||||
LogPriority.DEBUG,
|
val newETag = response.headers["ETag"]
|
||||||
) { "SyncYomi sync completed!" }
|
.takeIf { it?.isNotEmpty() == true } ?: throw SyncYomiException("Missing ETag")
|
||||||
} else {
|
syncPreferences.lastSyncEtag().set(newETag)
|
||||||
val responseBody = it.body.string()
|
logcat(LogPriority.DEBUG) { "SyncYomi sync completed" }
|
||||||
notifier.showSyncError("Failed to upload sync data: $responseBody")
|
|
||||||
responseBody.let { logcat(LogPriority.ERROR) { "SyncError:$it" } }
|
} else if (response.code == HttpStatus.SC_PRECONDITION_FAILED) {
|
||||||
}
|
// other clients updated remote data, will try next time
|
||||||
|
logcat(LogPriority.DEBUG) { "SyncYomi sync failed with 412" }
|
||||||
|
|
||||||
|
} else {
|
||||||
|
val responseBody = response.body.string()
|
||||||
|
notifier.showSyncError("Failed to upload sync data: $responseBody")
|
||||||
|
logcat(LogPriority.ERROR) { "SyncError: $responseBody" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,9 +20,8 @@ import exh.source.BlacklistedSources
|
|||||||
import exh.source.EH_SOURCE_ID
|
import exh.source.EH_SOURCE_ID
|
||||||
import exh.source.EXH_SOURCE_ID
|
import exh.source.EXH_SOURCE_ID
|
||||||
import exh.source.MERGED_SOURCE_ID
|
import exh.source.MERGED_SOURCE_ID
|
||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.async
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
@@ -32,7 +31,6 @@ import kotlinx.coroutines.flow.emptyFlow
|
|||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import tachiyomi.core.common.util.lang.launchNow
|
|
||||||
import tachiyomi.core.common.util.lang.withUIContext
|
import tachiyomi.core.common.util.lang.withUIContext
|
||||||
import tachiyomi.core.common.util.system.logcat
|
import tachiyomi.core.common.util.system.logcat
|
||||||
import tachiyomi.domain.source.model.StubSource
|
import tachiyomi.domain.source.model.StubSource
|
||||||
@@ -54,10 +52,10 @@ class ExtensionManager(
|
|||||||
private val trustExtension: TrustExtension = Injekt.get(),
|
private val trustExtension: TrustExtension = Injekt.get(),
|
||||||
) {
|
) {
|
||||||
|
|
||||||
// SY -->
|
val scope = CoroutineScope(SupervisorJob())
|
||||||
|
|
||||||
private val _isInitialized = MutableStateFlow(false)
|
private val _isInitialized = MutableStateFlow(false)
|
||||||
val isInitialized: StateFlow<Boolean> = _isInitialized.asStateFlow()
|
val isInitialized: StateFlow<Boolean> = _isInitialized.asStateFlow()
|
||||||
// SY <--
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* API where all the available extensions can be found.
|
* API where all the available extensions can be found.
|
||||||
@@ -71,13 +69,31 @@ class ExtensionManager(
|
|||||||
|
|
||||||
private val iconMap = mutableMapOf<String, Drawable>()
|
private val iconMap = mutableMapOf<String, Drawable>()
|
||||||
|
|
||||||
private val _installedExtensionsFlow = MutableStateFlow(emptyList<Extension.Installed>())
|
private val _installedExtensionsMapFlow = MutableStateFlow(emptyMap<String, Extension.Installed>())
|
||||||
val installedExtensionsFlow = _installedExtensionsFlow.asStateFlow()
|
val installedExtensionsFlow = _installedExtensionsMapFlow.mapExtensions(scope)
|
||||||
|
|
||||||
|
private val _availableExtensionsMapFlow = MutableStateFlow(emptyMap<String, Extension.Available>())
|
||||||
|
// SY -->
|
||||||
|
val availableExtensionsFlow = _availableExtensionsMapFlow.map { it.filterNotBlacklisted().values.toList() }
|
||||||
|
.stateIn(scope, SharingStarted.Lazily, _availableExtensionsMapFlow.value.values.toList())
|
||||||
|
// SY <--
|
||||||
|
|
||||||
|
private val _untrustedExtensionsMapFlow = MutableStateFlow(emptyMap<String, Extension.Untrusted>())
|
||||||
|
val untrustedExtensionsFlow = _untrustedExtensionsMapFlow.mapExtensions(scope)
|
||||||
|
|
||||||
|
init {
|
||||||
|
initExtensions()
|
||||||
|
ExtensionInstallReceiver(InstallationListener()).register(context)
|
||||||
|
}
|
||||||
|
|
||||||
private var subLanguagesEnabledOnFirstRun = preferences.enabledLanguages().isSet()
|
private var subLanguagesEnabledOnFirstRun = preferences.enabledLanguages().isSet()
|
||||||
|
|
||||||
fun getAppIconForSource(sourceId: Long): Drawable? {
|
fun getAppIconForSource(sourceId: Long): Drawable? {
|
||||||
val pkgName = _installedExtensionsFlow.value.find { ext -> ext.sources.any { it.id == sourceId } }?.pkgName
|
val pkgName = _installedExtensionsMapFlow.value.values
|
||||||
|
.find { ext ->
|
||||||
|
ext.sources.any { it.id == sourceId }
|
||||||
|
}
|
||||||
|
?.pkgName
|
||||||
if (pkgName != null) {
|
if (pkgName != null) {
|
||||||
return iconMap[pkgName] ?: iconMap.getOrPut(pkgName) {
|
return iconMap[pkgName] ?: iconMap.getOrPut(pkgName) {
|
||||||
ExtensionLoader.getExtensionPackageInfoFromPkgName(context, pkgName)!!.applicationInfo
|
ExtensionLoader.getExtensionPackageInfoFromPkgName(context, pkgName)!!.applicationInfo
|
||||||
@@ -95,15 +111,6 @@ class ExtensionManager(
|
|||||||
// SY <--
|
// SY <--
|
||||||
}
|
}
|
||||||
|
|
||||||
private val _availableExtensionsFlow = MutableStateFlow(emptyList<Extension.Available>())
|
|
||||||
|
|
||||||
// SY -->
|
|
||||||
@OptIn(DelicateCoroutinesApi::class)
|
|
||||||
val availableExtensionsFlow = _availableExtensionsFlow
|
|
||||||
.map { it.filterNotBlacklisted() }
|
|
||||||
.stateIn(GlobalScope, SharingStarted.Eagerly, emptyList())
|
|
||||||
// SY <--
|
|
||||||
|
|
||||||
private var availableExtensionsSourcesData: Map<Long, StubSource> = emptyMap()
|
private var availableExtensionsSourcesData: Map<Long, StubSource> = emptyMap()
|
||||||
|
|
||||||
private fun setupAvailableExtensionsSourcesDataMap(extensions: List<Extension.Available>) {
|
private fun setupAvailableExtensionsSourcesDataMap(extensions: List<Extension.Available>) {
|
||||||
@@ -115,38 +122,30 @@ class ExtensionManager(
|
|||||||
|
|
||||||
fun getSourceData(id: Long) = availableExtensionsSourcesData[id]
|
fun getSourceData(id: Long) = availableExtensionsSourcesData[id]
|
||||||
|
|
||||||
private val _untrustedExtensionsFlow = MutableStateFlow(emptyList<Extension.Untrusted>())
|
|
||||||
val untrustedExtensionsFlow = _untrustedExtensionsFlow.asStateFlow()
|
|
||||||
|
|
||||||
init {
|
|
||||||
initExtensions()
|
|
||||||
ExtensionInstallReceiver(InstallationListener()).register(context)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads and registers the installed extensions.
|
* Loads and registers the installed extensions.
|
||||||
*/
|
*/
|
||||||
private fun initExtensions() {
|
private fun initExtensions() {
|
||||||
val extensions = ExtensionLoader.loadExtensions(context)
|
val extensions = ExtensionLoader.loadExtensions(context)
|
||||||
|
|
||||||
_installedExtensionsFlow.value = extensions
|
_installedExtensionsMapFlow.value = extensions
|
||||||
.filterIsInstance<LoadResult.Success>()
|
.filterIsInstance<LoadResult.Success>()
|
||||||
.map { it.extension }
|
.associate { it.extension.pkgName to it.extension }
|
||||||
|
|
||||||
_untrustedExtensionsFlow.value = extensions
|
_untrustedExtensionsMapFlow.value = extensions
|
||||||
.filterIsInstance<LoadResult.Untrusted>()
|
.filterIsInstance<LoadResult.Untrusted>()
|
||||||
.map { it.extension }
|
.associate { it.extension.pkgName to it.extension }
|
||||||
// SY -->
|
// SY -->
|
||||||
.filterNotBlacklisted()
|
.filterNotBlacklisted()
|
||||||
|
// SY <--
|
||||||
|
|
||||||
_isInitialized.value = true
|
_isInitialized.value = true
|
||||||
// SY <--
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// EXH -->
|
// EXH -->
|
||||||
private fun <T : Extension> Iterable<T>.filterNotBlacklisted(): List<T> {
|
private fun <T : Extension> Map<String, T>.filterNotBlacklisted(): Map<String, T> {
|
||||||
val blacklistEnabled = preferences.enableSourceBlacklist().get()
|
val blacklistEnabled = preferences.enableSourceBlacklist().get()
|
||||||
return filterNot { extension ->
|
return filterNot { (_, extension) ->
|
||||||
extension.isBlacklisted(blacklistEnabled)
|
extension.isBlacklisted(blacklistEnabled)
|
||||||
.also {
|
.also {
|
||||||
if (it) this@ExtensionManager.xLogD("Removing blacklisted extension: (name: %s, pkgName: %s)!", extension.name, extension.pkgName)
|
if (it) this@ExtensionManager.xLogD("Removing blacklisted extension: (name: %s, pkgName: %s)!", extension.name, extension.pkgName)
|
||||||
@@ -160,7 +159,7 @@ class ExtensionManager(
|
|||||||
// EXH <--
|
// EXH <--
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Finds the available extensions in the [api] and updates [availableExtensions].
|
* Finds the available extensions in the [api] and updates [_availableExtensionsMapFlow].
|
||||||
*/
|
*/
|
||||||
suspend fun findAvailableExtensions() {
|
suspend fun findAvailableExtensions() {
|
||||||
val extensions: List<Extension.Available> = try {
|
val extensions: List<Extension.Available> = try {
|
||||||
@@ -173,7 +172,7 @@ class ExtensionManager(
|
|||||||
|
|
||||||
enableAdditionalSubLanguages(extensions)
|
enableAdditionalSubLanguages(extensions)
|
||||||
|
|
||||||
_availableExtensionsFlow.value = extensions
|
_availableExtensionsMapFlow.value = extensions.associateBy { it.pkgName }
|
||||||
updatedInstalledExtensionsStatuses(extensions)
|
updatedInstalledExtensionsStatuses(extensions)
|
||||||
setupAvailableExtensionsSourcesDataMap(extensions)
|
setupAvailableExtensionsSourcesDataMap(extensions)
|
||||||
}
|
}
|
||||||
@@ -219,42 +218,36 @@ class ExtensionManager(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val mutInstalledExtensions = _installedExtensionsFlow.value.toMutableList()
|
val installedExtensionsMap = _installedExtensionsMapFlow.value.toMutableMap()
|
||||||
var changed = false
|
var changed = false
|
||||||
|
for ((pkgName, extension) in installedExtensionsMap) {
|
||||||
|
val availableExt = availableExtensions.find { it.pkgName == pkgName }
|
||||||
|
|
||||||
for ((index, installedExt) in mutInstalledExtensions.withIndex()) {
|
if (availableExt == null && !extension.isObsolete) {
|
||||||
val pkgName = installedExt.pkgName
|
installedExtensionsMap[pkgName] = extension.copy(isObsolete = true)
|
||||||
// SY -->
|
|
||||||
val availableExt = _availableExtensionsFlow.value.find { it.pkgName == pkgName }
|
|
||||||
// SY <--
|
|
||||||
|
|
||||||
if (availableExt == null && !installedExt.isObsolete) {
|
|
||||||
mutInstalledExtensions[index] = installedExt.copy(isObsolete = true)
|
|
||||||
changed = true
|
changed = true
|
||||||
// SY -->
|
// SY -->
|
||||||
} else if (installedExt.isBlacklisted() && !installedExt.isRedundant) {
|
} else if (extension.isBlacklisted() && !extension.isRedundant) {
|
||||||
mutInstalledExtensions[index] = installedExt.copy(isRedundant = true)
|
installedExtensionsMap[pkgName] = extension.copy(isRedundant = true)
|
||||||
changed = true
|
changed = true
|
||||||
// SY <--
|
// SY <--
|
||||||
} else if (availableExt != null) {
|
} else if (availableExt != null) {
|
||||||
val hasUpdate = installedExt.updateExists(availableExt)
|
val hasUpdate = extension.updateExists(availableExt)
|
||||||
|
if (extension.hasUpdate != hasUpdate) {
|
||||||
if (installedExt.hasUpdate != hasUpdate) {
|
installedExtensionsMap[pkgName] = extension.copy(
|
||||||
mutInstalledExtensions[index] = installedExt.copy(
|
|
||||||
hasUpdate = hasUpdate,
|
hasUpdate = hasUpdate,
|
||||||
repoUrl = availableExt.repoUrl,
|
repoUrl = availableExt.repoUrl,
|
||||||
)
|
)
|
||||||
changed = true
|
|
||||||
} else {
|
} else {
|
||||||
mutInstalledExtensions[index] = installedExt.copy(
|
installedExtensionsMap[pkgName] = extension.copy(
|
||||||
repoUrl = availableExt.repoUrl,
|
repoUrl = availableExt.repoUrl,
|
||||||
)
|
)
|
||||||
changed = true
|
|
||||||
}
|
}
|
||||||
|
changed = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (changed) {
|
if (changed) {
|
||||||
_installedExtensionsFlow.value = mutInstalledExtensions
|
_installedExtensionsMapFlow.value = installedExtensionsMap
|
||||||
}
|
}
|
||||||
updatePendingUpdatesCount()
|
updatePendingUpdatesCount()
|
||||||
}
|
}
|
||||||
@@ -278,8 +271,7 @@ class ExtensionManager(
|
|||||||
* @param extension The extension to be updated.
|
* @param extension The extension to be updated.
|
||||||
*/
|
*/
|
||||||
fun updateExtension(extension: Extension.Installed): Flow<InstallStep> {
|
fun updateExtension(extension: Extension.Installed): Flow<InstallStep> {
|
||||||
val availableExt = _availableExtensionsFlow.value.find { it.pkgName == extension.pkgName }
|
val availableExt = _availableExtensionsMapFlow.value[extension.pkgName] ?: return emptyFlow()
|
||||||
?: return emptyFlow()
|
|
||||||
return installExtension(availableExt)
|
return installExtension(availableExt)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -315,24 +307,16 @@ class ExtensionManager(
|
|||||||
*
|
*
|
||||||
* @param extension the extension to trust
|
* @param extension the extension to trust
|
||||||
*/
|
*/
|
||||||
fun trust(extension: Extension.Untrusted) {
|
suspend fun trust(extension: Extension.Untrusted) {
|
||||||
val untrustedPkgNames = _untrustedExtensionsFlow.value.map { it.pkgName }.toSet()
|
_untrustedExtensionsMapFlow.value[extension.pkgName] ?: return
|
||||||
if (extension.pkgName !in untrustedPkgNames) return
|
|
||||||
|
|
||||||
trustExtension.trust(extension.pkgName, extension.versionCode, extension.signatureHash)
|
trustExtension.trust(extension.pkgName, extension.versionCode, extension.signatureHash)
|
||||||
|
|
||||||
val nowTrustedExtensions = _untrustedExtensionsFlow.value
|
_untrustedExtensionsMapFlow.value -= extension.pkgName
|
||||||
.filter { it.pkgName == extension.pkgName && it.versionCode == extension.versionCode }
|
|
||||||
_untrustedExtensionsFlow.value -= nowTrustedExtensions
|
|
||||||
|
|
||||||
launchNow {
|
ExtensionLoader.loadExtensionFromPkgName(context, extension.pkgName)
|
||||||
nowTrustedExtensions
|
.let { it as? LoadResult.Success }
|
||||||
.map { extension ->
|
?.let { registerNewExtension(it.extension) }
|
||||||
async { ExtensionLoader.loadExtensionFromPkgName(context, extension.pkgName) }.await()
|
|
||||||
}
|
|
||||||
.filterIsInstance<LoadResult.Success>()
|
|
||||||
.forEach { registerNewExtension(it.extension) }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -348,7 +332,7 @@ class ExtensionManager(
|
|||||||
}
|
}
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
_installedExtensionsFlow.value += extension
|
_installedExtensionsMapFlow.value += extension
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -365,13 +349,7 @@ class ExtensionManager(
|
|||||||
}
|
}
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
val mutInstalledExtensions = _installedExtensionsFlow.value.toMutableList()
|
_installedExtensionsMapFlow.value += extension
|
||||||
val oldExtension = mutInstalledExtensions.find { it.pkgName == extension.pkgName }
|
|
||||||
if (oldExtension != null) {
|
|
||||||
mutInstalledExtensions -= oldExtension
|
|
||||||
}
|
|
||||||
mutInstalledExtensions += extension
|
|
||||||
_installedExtensionsFlow.value = mutInstalledExtensions
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -381,14 +359,8 @@ class ExtensionManager(
|
|||||||
* @param pkgName The package name of the uninstalled application.
|
* @param pkgName The package name of the uninstalled application.
|
||||||
*/
|
*/
|
||||||
private fun unregisterExtension(pkgName: String) {
|
private fun unregisterExtension(pkgName: String) {
|
||||||
val installedExtension = _installedExtensionsFlow.value.find { it.pkgName == pkgName }
|
_installedExtensionsMapFlow.value -= pkgName
|
||||||
if (installedExtension != null) {
|
_untrustedExtensionsMapFlow.value -= pkgName
|
||||||
_installedExtensionsFlow.value -= installedExtension
|
|
||||||
}
|
|
||||||
val untrustedExtension = _untrustedExtensionsFlow.value.find { it.pkgName == pkgName }
|
|
||||||
if (untrustedExtension != null) {
|
|
||||||
_untrustedExtensionsFlow.value -= untrustedExtension
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -407,14 +379,9 @@ class ExtensionManager(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onExtensionUntrusted(extension: Extension.Untrusted) {
|
override fun onExtensionUntrusted(extension: Extension.Untrusted) {
|
||||||
val installedExtension = _installedExtensionsFlow.value
|
_installedExtensionsMapFlow.value -= extension.pkgName
|
||||||
.find { it.pkgName == extension.pkgName }
|
_untrustedExtensionsMapFlow.value += extension
|
||||||
|
updatePendingUpdatesCount()
|
||||||
if (installedExtension != null) {
|
|
||||||
_installedExtensionsFlow.value -= installedExtension
|
|
||||||
} else {
|
|
||||||
_untrustedExtensionsFlow.value += extension
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPackageUninstalled(pkgName: String) {
|
override fun onPackageUninstalled(pkgName: String) {
|
||||||
@@ -436,17 +403,24 @@ class ExtensionManager(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun Extension.Installed.updateExists(availableExtension: Extension.Available? = null): Boolean {
|
private fun Extension.Installed.updateExists(availableExtension: Extension.Available? = null): Boolean {
|
||||||
val availableExt = availableExtension ?: _availableExtensionsFlow.value.find { it.pkgName == pkgName }
|
val availableExt = availableExtension
|
||||||
|
?: _availableExtensionsMapFlow.value[pkgName]
|
||||||
?: return false
|
?: return false
|
||||||
|
|
||||||
return (availableExt.versionCode > versionCode || availableExt.libVersion > libVersion)
|
return (availableExt.versionCode > versionCode || availableExt.libVersion > libVersion)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updatePendingUpdatesCount() {
|
private fun updatePendingUpdatesCount() {
|
||||||
val pendingUpdateCount = _installedExtensionsFlow.value.count { it.hasUpdate }
|
val pendingUpdateCount = _installedExtensionsMapFlow.value.values.count { it.hasUpdate }
|
||||||
preferences.extensionUpdatesCount().set(pendingUpdateCount)
|
preferences.extensionUpdatesCount().set(pendingUpdateCount)
|
||||||
if (pendingUpdateCount == 0) {
|
if (pendingUpdateCount == 0) {
|
||||||
ExtensionUpdateNotifier(context).dismiss()
|
ExtensionUpdateNotifier(context).dismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private operator fun <T : Extension> Map<String, T>.plus(extension: T) = plus(extension.pkgName to extension)
|
||||||
|
|
||||||
|
private fun <T : Extension> StateFlow<Map<String, T>>.mapExtensions(scope: CoroutineScope): StateFlow<List<T>> {
|
||||||
|
return map { it.values.toList() }.stateIn(scope, SharingStarted.Lazily, value.values.toList())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,9 +11,14 @@ import eu.kanade.tachiyomi.network.NetworkHelper
|
|||||||
import eu.kanade.tachiyomi.network.awaitSuccess
|
import eu.kanade.tachiyomi.network.awaitSuccess
|
||||||
import eu.kanade.tachiyomi.network.parseAs
|
import eu.kanade.tachiyomi.network.parseAs
|
||||||
import exh.source.BlacklistedSources
|
import exh.source.BlacklistedSources
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.awaitAll
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
|
import mihon.domain.extensionrepo.interactor.GetExtensionRepo
|
||||||
|
import mihon.domain.extensionrepo.interactor.UpdateExtensionRepo
|
||||||
|
import mihon.domain.extensionrepo.model.ExtensionRepo
|
||||||
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.lang.withIOContext
|
import tachiyomi.core.common.util.lang.withIOContext
|
||||||
@@ -26,8 +31,12 @@ internal class ExtensionApi {
|
|||||||
|
|
||||||
private val networkService: NetworkHelper by injectLazy()
|
private val networkService: NetworkHelper by injectLazy()
|
||||||
private val preferenceStore: PreferenceStore by injectLazy()
|
private val preferenceStore: PreferenceStore by injectLazy()
|
||||||
private val sourcePreferences: SourcePreferences by injectLazy()
|
private val getExtensionRepo: GetExtensionRepo by injectLazy()
|
||||||
|
private val updateExtensionRepo: UpdateExtensionRepo by injectLazy()
|
||||||
private val extensionManager: ExtensionManager by injectLazy()
|
private val extensionManager: ExtensionManager by injectLazy()
|
||||||
|
// SY -->
|
||||||
|
private val sourcePreferences: SourcePreferences by injectLazy()
|
||||||
|
// SY <--
|
||||||
private val json: Json by injectLazy()
|
private val json: Json by injectLazy()
|
||||||
|
|
||||||
private val lastExtCheck: Preference<Long> by lazy {
|
private val lastExtCheck: Preference<Long> by lazy {
|
||||||
@@ -36,11 +45,15 @@ internal class ExtensionApi {
|
|||||||
|
|
||||||
suspend fun findExtensions(): List<Extension.Available> {
|
suspend fun findExtensions(): List<Extension.Available> {
|
||||||
return withIOContext {
|
return withIOContext {
|
||||||
sourcePreferences.extensionRepos().get().flatMap { getExtensions(it) }
|
getExtensionRepo.getAll()
|
||||||
|
.map { async { getExtensions(it) } }
|
||||||
|
.awaitAll()
|
||||||
|
.flatten()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getExtensions(repoBaseUrl: String): List<Extension.Available> {
|
private suspend fun getExtensions(extRepo: ExtensionRepo): List<Extension.Available> {
|
||||||
|
val repoBaseUrl = extRepo.baseUrl
|
||||||
return try {
|
return try {
|
||||||
val response = networkService.client
|
val response = networkService.client
|
||||||
.newCall(GET("$repoBaseUrl/index.min.json"))
|
.newCall(GET("$repoBaseUrl/index.min.json"))
|
||||||
@@ -68,6 +81,9 @@ internal class ExtensionApi {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update extension repo details
|
||||||
|
updateExtensionRepo.awaitAll()
|
||||||
|
|
||||||
val extensions = if (fromAvailableExtensionList) {
|
val extensions = if (fromAvailableExtensionList) {
|
||||||
extensionManager.availableExtensionsFlow.value
|
extensionManager.availableExtensionsFlow.value
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -9,12 +9,10 @@ import androidx.core.content.ContextCompat
|
|||||||
import eu.kanade.tachiyomi.BuildConfig
|
import eu.kanade.tachiyomi.BuildConfig
|
||||||
import eu.kanade.tachiyomi.extension.model.Extension
|
import eu.kanade.tachiyomi.extension.model.Extension
|
||||||
import eu.kanade.tachiyomi.extension.model.LoadResult
|
import eu.kanade.tachiyomi.extension.model.LoadResult
|
||||||
import kotlinx.coroutines.CoroutineStart
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.async
|
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import tachiyomi.core.common.util.lang.launchNow
|
|
||||||
import tachiyomi.core.common.util.system.logcat
|
import tachiyomi.core.common.util.system.logcat
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -23,29 +21,23 @@ import tachiyomi.core.common.util.system.logcat
|
|||||||
*
|
*
|
||||||
* @param listener The listener that should be notified of extension installation events.
|
* @param listener The listener that should be notified of extension installation events.
|
||||||
*/
|
*/
|
||||||
internal class ExtensionInstallReceiver(private val listener: Listener) :
|
internal class ExtensionInstallReceiver(private val listener: Listener) : BroadcastReceiver() {
|
||||||
BroadcastReceiver() {
|
|
||||||
|
val scope = CoroutineScope(SupervisorJob())
|
||||||
|
|
||||||
/**
|
|
||||||
* Registers this broadcast receiver
|
|
||||||
*/
|
|
||||||
fun register(context: Context) {
|
fun register(context: Context) {
|
||||||
ContextCompat.registerReceiver(context, this, filter, ContextCompat.RECEIVER_NOT_EXPORTED)
|
ContextCompat.registerReceiver(context, this, filter, ContextCompat.RECEIVER_NOT_EXPORTED)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private val filter = IntentFilter().apply {
|
||||||
* Returns the intent filter this receiver should subscribe to.
|
addAction(Intent.ACTION_PACKAGE_ADDED)
|
||||||
*/
|
addAction(Intent.ACTION_PACKAGE_REPLACED)
|
||||||
private val filter
|
addAction(Intent.ACTION_PACKAGE_REMOVED)
|
||||||
get() = IntentFilter().apply {
|
addAction(ACTION_EXTENSION_ADDED)
|
||||||
addAction(Intent.ACTION_PACKAGE_ADDED)
|
addAction(ACTION_EXTENSION_REPLACED)
|
||||||
addAction(Intent.ACTION_PACKAGE_REPLACED)
|
addAction(ACTION_EXTENSION_REMOVED)
|
||||||
addAction(Intent.ACTION_PACKAGE_REMOVED)
|
addDataScheme("package")
|
||||||
addAction(ACTION_EXTENSION_ADDED)
|
}
|
||||||
addAction(ACTION_EXTENSION_REPLACED)
|
|
||||||
addAction(ACTION_EXTENSION_REMOVED)
|
|
||||||
addDataScheme("package")
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when one of the events of the [filter] is received. When the package is an extension,
|
* Called when one of the events of the [filter] is received. When the package is an extension,
|
||||||
@@ -58,7 +50,7 @@ internal class ExtensionInstallReceiver(private val listener: Listener) :
|
|||||||
Intent.ACTION_PACKAGE_ADDED, ACTION_EXTENSION_ADDED -> {
|
Intent.ACTION_PACKAGE_ADDED, ACTION_EXTENSION_ADDED -> {
|
||||||
if (isReplacing(intent)) return
|
if (isReplacing(intent)) return
|
||||||
|
|
||||||
launchNow {
|
scope.launch {
|
||||||
when (val result = getExtensionFromIntent(context, intent)) {
|
when (val result = getExtensionFromIntent(context, intent)) {
|
||||||
is LoadResult.Success -> listener.onExtensionInstalled(result.extension)
|
is LoadResult.Success -> listener.onExtensionInstalled(result.extension)
|
||||||
is LoadResult.Untrusted -> listener.onExtensionUntrusted(result.extension)
|
is LoadResult.Untrusted -> listener.onExtensionUntrusted(result.extension)
|
||||||
@@ -67,7 +59,7 @@ internal class ExtensionInstallReceiver(private val listener: Listener) :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Intent.ACTION_PACKAGE_REPLACED, ACTION_EXTENSION_REPLACED -> {
|
Intent.ACTION_PACKAGE_REPLACED, ACTION_EXTENSION_REPLACED -> {
|
||||||
launchNow {
|
scope.launch {
|
||||||
when (val result = getExtensionFromIntent(context, intent)) {
|
when (val result = getExtensionFromIntent(context, intent)) {
|
||||||
is LoadResult.Success -> listener.onExtensionUpdated(result.extension)
|
is LoadResult.Success -> listener.onExtensionUpdated(result.extension)
|
||||||
is LoadResult.Untrusted -> listener.onExtensionUntrusted(result.extension)
|
is LoadResult.Untrusted -> listener.onExtensionUntrusted(result.extension)
|
||||||
@@ -107,9 +99,7 @@ internal class ExtensionInstallReceiver(private val listener: Listener) :
|
|||||||
logcat(LogPriority.WARN) { "Package name not found" }
|
logcat(LogPriority.WARN) { "Package name not found" }
|
||||||
return LoadResult.Error
|
return LoadResult.Error
|
||||||
}
|
}
|
||||||
return GlobalScope.async(Dispatchers.Default, CoroutineStart.DEFAULT) {
|
return ExtensionLoader.loadExtensionFromPkgName(context, pkgName)
|
||||||
ExtensionLoader.loadExtensionFromPkgName(context, pkgName)
|
|
||||||
}.await()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -172,7 +172,7 @@ internal object ExtensionLoader {
|
|||||||
* Attempts to load an extension from the given package name. It checks if the extension
|
* Attempts to load an extension from the given package name. It checks if the extension
|
||||||
* contains the required feature flag before trying to load it.
|
* contains the required feature flag before trying to load it.
|
||||||
*/
|
*/
|
||||||
fun loadExtensionFromPkgName(context: Context, pkgName: String): LoadResult {
|
suspend fun loadExtensionFromPkgName(context: Context, pkgName: String): LoadResult {
|
||||||
val extensionPackage = getExtensionInfoFromPkgName(context, pkgName)
|
val extensionPackage = getExtensionInfoFromPkgName(context, pkgName)
|
||||||
if (extensionPackage == null) {
|
if (extensionPackage == null) {
|
||||||
logcat(LogPriority.ERROR) { "Extension package is not found ($pkgName)" }
|
logcat(LogPriority.ERROR) { "Extension package is not found ($pkgName)" }
|
||||||
@@ -223,7 +223,8 @@ internal object ExtensionLoader {
|
|||||||
* @param context The application context.
|
* @param context The application context.
|
||||||
* @param extensionInfo The extension to load.
|
* @param extensionInfo The extension to load.
|
||||||
*/
|
*/
|
||||||
private fun loadExtension(context: Context, extensionInfo: ExtensionInfo): LoadResult {
|
@Suppress("LongMethod", "CyclomaticComplexMethod", "ReturnCount")
|
||||||
|
private suspend fun loadExtension(context: Context, extensionInfo: ExtensionInfo): LoadResult {
|
||||||
val pkgManager = context.packageManager
|
val pkgManager = context.packageManager
|
||||||
val pkgInfo = extensionInfo.packageInfo
|
val pkgInfo = extensionInfo.packageInfo
|
||||||
val appInfo = pkgInfo.applicationInfo
|
val appInfo = pkgInfo.applicationInfo
|
||||||
@@ -252,7 +253,7 @@ internal object ExtensionLoader {
|
|||||||
if (signatures.isNullOrEmpty()) {
|
if (signatures.isNullOrEmpty()) {
|
||||||
logcat(LogPriority.WARN) { "Package $pkgName isn't signed" }
|
logcat(LogPriority.WARN) { "Package $pkgName isn't signed" }
|
||||||
return LoadResult.Error
|
return LoadResult.Error
|
||||||
} else if (!trustExtension.isTrusted(pkgInfo, signatures.last())) {
|
} else if (!trustExtension.isTrusted(pkgInfo, signatures)) {
|
||||||
val extension = Extension.Untrusted(
|
val extension = Extension.Untrusted(
|
||||||
extName,
|
extName,
|
||||||
pkgName,
|
pkgName,
|
||||||
|
|||||||
@@ -806,6 +806,11 @@ class EHentai(
|
|||||||
override fun pageListParse(response: Response) =
|
override fun pageListParse(response: Response) =
|
||||||
throw UnsupportedOperationException("Unused method was called somehow!")
|
throw UnsupportedOperationException("Unused method was called somehow!")
|
||||||
|
|
||||||
|
override suspend fun getImageUrl(page: Page): String {
|
||||||
|
val imageUrlResponse = client.newCall(imageUrlRequest(page)).awaitSuccess()
|
||||||
|
return realImageUrlParse(imageUrlResponse, page)
|
||||||
|
}
|
||||||
|
|
||||||
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getImageUrl"))
|
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getImageUrl"))
|
||||||
override fun fetchImageUrl(page: Page): Observable<String> {
|
override fun fetchImageUrl(page: Page): Observable<String> {
|
||||||
return client.newCall(imageUrlRequest(page))
|
return client.newCall(imageUrlRequest(page))
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import exh.md.handlers.FollowsHandler
|
|||||||
import exh.md.handlers.MangaHandler
|
import exh.md.handlers.MangaHandler
|
||||||
import exh.md.handlers.MangaHotHandler
|
import exh.md.handlers.MangaHotHandler
|
||||||
import exh.md.handlers.MangaPlusHandler
|
import exh.md.handlers.MangaPlusHandler
|
||||||
|
import exh.md.handlers.NamicomiHandler
|
||||||
import exh.md.handlers.PageHandler
|
import exh.md.handlers.PageHandler
|
||||||
import exh.md.handlers.SimilarHandler
|
import exh.md.handlers.SimilarHandler
|
||||||
import exh.md.network.MangaDexLoginHelper
|
import exh.md.network.MangaDexLoginHelper
|
||||||
@@ -123,6 +124,9 @@ class MangaDex(delegate: HttpSource, val context: Context) :
|
|||||||
private val mangaHotHandler by lazy {
|
private val mangaHotHandler by lazy {
|
||||||
MangaHotHandler(network.client, network.defaultUserAgentProvider())
|
MangaHotHandler(network.client, network.defaultUserAgentProvider())
|
||||||
}
|
}
|
||||||
|
private val namicomiHandler by lazy {
|
||||||
|
NamicomiHandler(network.client, network.defaultUserAgentProvider())
|
||||||
|
}
|
||||||
private val pageHandler by lazy {
|
private val pageHandler by lazy {
|
||||||
PageHandler(
|
PageHandler(
|
||||||
headers,
|
headers,
|
||||||
@@ -132,6 +136,7 @@ class MangaDex(delegate: HttpSource, val context: Context) :
|
|||||||
bilibiliHandler,
|
bilibiliHandler,
|
||||||
azukHandler,
|
azukHandler,
|
||||||
mangaHotHandler,
|
mangaHotHandler,
|
||||||
|
namicomiHandler,
|
||||||
trackPreferences,
|
trackPreferences,
|
||||||
mdList,
|
mdList,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -109,8 +109,6 @@ class MergedSource : HttpSource() {
|
|||||||
suspend fun fetchChaptersForMergedManga(
|
suspend fun fetchChaptersForMergedManga(
|
||||||
manga: Manga,
|
manga: Manga,
|
||||||
downloadChapters: Boolean = true,
|
downloadChapters: Boolean = true,
|
||||||
editScanlators: Boolean = false,
|
|
||||||
dedupe: Boolean = true,
|
|
||||||
) {
|
) {
|
||||||
fetchChaptersAndSync(manga, downloadChapters)
|
fetchChaptersAndSync(manga, downloadChapters)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import kotlinx.coroutines.flow.map
|
|||||||
import kotlinx.coroutines.flow.onCompletion
|
import kotlinx.coroutines.flow.onCompletion
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import tachiyomi.core.common.util.lang.launchIO
|
import tachiyomi.core.common.util.lang.launchIO
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
@@ -195,7 +196,9 @@ class ExtensionsScreenModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun trustExtension(extension: Extension.Untrusted) {
|
fun trustExtension(extension: Extension.Untrusted) {
|
||||||
extensionManager.trust(extension)
|
screenModelScope.launch {
|
||||||
|
extensionManager.trust(extension)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
|
|||||||
@@ -280,7 +280,7 @@ open class FeedScreenModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun getManga(initialManga: DomainManga, source: CatalogueSource?): State<DomainManga> {
|
fun getManga(initialManga: DomainManga): State<DomainManga> {
|
||||||
return produceState(initialValue = initialManga) {
|
return produceState(initialValue = initialManga) {
|
||||||
getManga.subscribe(initialManga.url, initialManga.source)
|
getManga.subscribe(initialManga.url, initialManga.source)
|
||||||
.collectLatest { manga ->
|
.collectLatest { manga ->
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ fun Screen.feedTab(): TabContent {
|
|||||||
navigator.push(MangaScreen(manga.id, true))
|
navigator.push(MangaScreen(manga.id, true))
|
||||||
},
|
},
|
||||||
onRefresh = screenModel::init,
|
onRefresh = screenModel::init,
|
||||||
getMangaState = { manga, source -> screenModel.getManga(initialManga = manga, source = source) },
|
getMangaState = { manga -> screenModel.getManga(initialManga = manga) },
|
||||||
)
|
)
|
||||||
|
|
||||||
state.dialog?.let { dialog ->
|
state.dialog?.let { dialog ->
|
||||||
|
|||||||
+22
-4
@@ -46,9 +46,15 @@ import tachiyomi.i18n.sy.SYMR
|
|||||||
import tachiyomi.presentation.core.components.material.ExtendedFloatingActionButton
|
import tachiyomi.presentation.core.components.material.ExtendedFloatingActionButton
|
||||||
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 java.io.Serializable
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
class PreMigrationScreen(val mangaIds: List<Long>) : Screen() {
|
sealed class MigrationType : Serializable {
|
||||||
|
data class MangaList(val mangaIds: List<Long>) : MigrationType()
|
||||||
|
data class MangaSingle(val fromMangaId: Long, val toManga: Long?) : MigrationType()
|
||||||
|
}
|
||||||
|
|
||||||
|
class PreMigrationScreen(val migration: MigrationType) : Screen() {
|
||||||
@Composable
|
@Composable
|
||||||
override fun Content() {
|
override fun Content() {
|
||||||
val screenModel = rememberScreenModel { PreMigrationScreenModel() }
|
val screenModel = rememberScreenModel { PreMigrationScreenModel() }
|
||||||
@@ -173,7 +179,7 @@ class PreMigrationScreen(val mangaIds: List<Long>) : Screen() {
|
|||||||
screenModel.onMigrationSheet(false)
|
screenModel.onMigrationSheet(false)
|
||||||
screenModel.saveEnabledSources()
|
screenModel.saveEnabledSources()
|
||||||
|
|
||||||
navigator replace MigrationListScreen(MigrationProcedureConfig(mangaIds, extraParam))
|
navigator replace MigrationListScreen(MigrationProcedureConfig(migration, extraParam))
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -184,10 +190,22 @@ class PreMigrationScreen(val mangaIds: List<Long>) : Screen() {
|
|||||||
navigator.push(
|
navigator.push(
|
||||||
if (skipPre) {
|
if (skipPre) {
|
||||||
MigrationListScreen(
|
MigrationListScreen(
|
||||||
MigrationProcedureConfig(mangaIds, null),
|
MigrationProcedureConfig(MigrationType.MangaList(mangaIds), null),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
PreMigrationScreen(mangaIds)
|
PreMigrationScreen(MigrationType.MangaList(mangaIds))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun navigateToMigration(skipPre: Boolean, navigator: Navigator, fromMangaId: Long, toManga: Long?) {
|
||||||
|
navigator.push(
|
||||||
|
if (skipPre) {
|
||||||
|
MigrationListScreen(
|
||||||
|
MigrationProcedureConfig(MigrationType.MangaSingle(fromMangaId, toManga), null),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
PreMigrationScreen(MigrationType.MangaSingle(fromMangaId, toManga))
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -121,7 +121,7 @@ class MigrationListScreen(private val config: MigrationProcedureConfig) : Screen
|
|||||||
)
|
)
|
||||||
|
|
||||||
val onDismissRequest = { screenModel.dialog.value = null }
|
val onDismissRequest = { screenModel.dialog.value = null }
|
||||||
when (val dialog = dialog) {
|
when (@Suppress("NAME_SHADOWING") val dialog = dialog) {
|
||||||
is MigrationListScreenModel.Dialog.MigrateMangaDialog -> {
|
is MigrationListScreenModel.Dialog.MigrateMangaDialog -> {
|
||||||
MigrationMangaDialog(
|
MigrationMangaDialog(
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
|
|||||||
+48
-10
@@ -14,6 +14,7 @@ import eu.kanade.tachiyomi.source.CatalogueSource
|
|||||||
import eu.kanade.tachiyomi.source.getNameForMangaInfo
|
import eu.kanade.tachiyomi.source.getNameForMangaInfo
|
||||||
import eu.kanade.tachiyomi.source.online.all.EHentai
|
import eu.kanade.tachiyomi.source.online.all.EHentai
|
||||||
import eu.kanade.tachiyomi.ui.browse.migration.MigrationFlags
|
import eu.kanade.tachiyomi.ui.browse.migration.MigrationFlags
|
||||||
|
import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.MigrationType
|
||||||
import eu.kanade.tachiyomi.ui.browse.migration.advanced.process.MigratingManga.SearchResult
|
import eu.kanade.tachiyomi.ui.browse.migration.advanced.process.MigratingManga.SearchResult
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
import exh.eh.EHentaiThrottleManager
|
import exh.eh.EHentaiThrottleManager
|
||||||
@@ -104,8 +105,14 @@ class MigrationListScreenModel(
|
|||||||
|
|
||||||
init {
|
init {
|
||||||
screenModelScope.launchIO {
|
screenModelScope.launchIO {
|
||||||
|
val mangaIds = when (val migration = config.migration) {
|
||||||
|
is MigrationType.MangaList -> {
|
||||||
|
migration.mangaIds
|
||||||
|
}
|
||||||
|
is MigrationType.MangaSingle -> listOf(migration.fromMangaId)
|
||||||
|
}
|
||||||
runMigrations(
|
runMigrations(
|
||||||
config.mangaIds
|
mangaIds
|
||||||
.map {
|
.map {
|
||||||
async {
|
async {
|
||||||
val manga = getManga.await(it) ?: return@async null
|
val manga = getManga.await(it) ?: return@async null
|
||||||
@@ -161,9 +168,13 @@ class MigrationListScreenModel(
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
// in case it was removed
|
// in case it was removed
|
||||||
if (manga.manga.id !in config.mangaIds) {
|
when (val migration = config.migration) {
|
||||||
continue
|
is MigrationType.MangaList -> if (manga.manga.id !in migration.mangaIds) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
else -> Unit
|
||||||
}
|
}
|
||||||
|
|
||||||
if (manga.searchResult.value == SearchResult.Searching && manga.migrationScope.isActive) {
|
if (manga.searchResult.value == SearchResult.Searching && manga.migrationScope.isActive) {
|
||||||
val mangaObj = manga.manga
|
val mangaObj = manga.manga
|
||||||
val mangaSource = sourceManager.getOrStub(mangaObj.source)
|
val mangaSource = sourceManager.getOrStub(mangaObj.source)
|
||||||
@@ -175,6 +186,28 @@ class MigrationListScreenModel(
|
|||||||
} else {
|
} else {
|
||||||
sources.filter { it.id != mangaSource.id }
|
sources.filter { it.id != mangaSource.id }
|
||||||
}
|
}
|
||||||
|
when (val migration = config.migration) {
|
||||||
|
is MigrationType.MangaSingle -> if (migration.toManga != null) {
|
||||||
|
val localManga = getManga.await(migration.toManga)
|
||||||
|
if (localManga != null) {
|
||||||
|
val source = sourceManager.get(localManga.source) as? CatalogueSource
|
||||||
|
if (source != null) {
|
||||||
|
val chapters = if (source is EHentai) {
|
||||||
|
source.getChapterList(localManga.toSManga(), throttleManager::throttle)
|
||||||
|
} else {
|
||||||
|
source.getChapterList(localManga.toSManga())
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
syncChaptersWithSource.await(chapters, localManga, source)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
}
|
||||||
|
manga.progress.value = validSources.size to validSources.size
|
||||||
|
return@async localManga
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> Unit
|
||||||
|
}
|
||||||
if (useSourceWithMost) {
|
if (useSourceWithMost) {
|
||||||
val sourceSemaphore = Semaphore(3)
|
val sourceSemaphore = Semaphore(3)
|
||||||
val processedSources = AtomicInteger()
|
val processedSources = AtomicInteger()
|
||||||
@@ -523,13 +556,18 @@ class MigrationListScreenModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun removeManga(item: MigratingManga) {
|
fun removeManga(item: MigratingManga) {
|
||||||
val ids = config.mangaIds.toMutableList()
|
when (val migration = config.migration) {
|
||||||
val index = ids.indexOf(item.manga.id)
|
is MigrationType.MangaList -> {
|
||||||
if (index > -1) {
|
val ids = migration.mangaIds.toMutableList()
|
||||||
ids.removeAt(index)
|
val index = ids.indexOf(item.manga.id)
|
||||||
config.mangaIds = ids
|
if (index > -1) {
|
||||||
val index2 = migratingItems.value.orEmpty().indexOf(item)
|
ids.removeAt(index)
|
||||||
if (index2 > -1) migratingItems.value = (migratingItems.value.orEmpty() - item).toImmutableList()
|
config.migration = MigrationType.MangaList(ids)
|
||||||
|
val index2 = migratingItems.value.orEmpty().indexOf(item)
|
||||||
|
if (index2 > -1) migratingItems.value = (migratingItems.value.orEmpty() - item).toImmutableList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is MigrationType.MangaSingle -> Unit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -1,8 +1,9 @@
|
|||||||
package eu.kanade.tachiyomi.ui.browse.migration.advanced.process
|
package eu.kanade.tachiyomi.ui.browse.migration.advanced.process
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.MigrationType
|
||||||
import java.io.Serializable
|
import java.io.Serializable
|
||||||
|
|
||||||
data class MigrationProcedureConfig(
|
data class MigrationProcedureConfig(
|
||||||
var mangaIds: List<Long>,
|
var migration: MigrationType,
|
||||||
val extraSearchParams: String?,
|
val extraSearchParams: String?,
|
||||||
) : Serializable
|
) : Serializable
|
||||||
|
|||||||
+1
-1
@@ -109,7 +109,7 @@ data class SourceSearchScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val onDismissRequest = { screenModel.setDialog(null) }
|
val onDismissRequest = { screenModel.setDialog(null) }
|
||||||
when (val dialog = state.dialog) {
|
when (state.dialog) {
|
||||||
is BrowseSourceScreenModel.Dialog.Filter -> {
|
is BrowseSourceScreenModel.Dialog.Filter -> {
|
||||||
SourceFilterDialog(
|
SourceFilterDialog(
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import eu.kanade.presentation.components.TabContent
|
|||||||
import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.PreMigrationScreen
|
import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.PreMigrationScreen
|
||||||
import eu.kanade.tachiyomi.ui.browse.migration.manga.MigrateMangaScreen
|
import eu.kanade.tachiyomi.ui.browse.migration.manga.MigrateMangaScreen
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
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.UnsortedPreferences
|
import tachiyomi.domain.UnsortedPreferences
|
||||||
@@ -55,6 +56,7 @@ fun Screen.migrateSourceTab(): TabContent {
|
|||||||
// SY -->
|
// SY -->
|
||||||
onClickAll = { source ->
|
onClickAll = { source ->
|
||||||
// TODO: Jay wtf, need to clean this up sometime
|
// TODO: Jay wtf, need to clean this up sometime
|
||||||
|
@OptIn(DelicateCoroutinesApi::class)
|
||||||
launchIO {
|
launchIO {
|
||||||
val manga = Injekt.get<GetFavorites>().await()
|
val manga = Injekt.get<GetFavorites>().await()
|
||||||
val sourceMangas =
|
val sourceMangas =
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ import eu.kanade.presentation.util.Screen
|
|||||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.ui.browse.extension.details.SourcePreferencesScreen
|
import eu.kanade.tachiyomi.ui.browse.extension.details.SourcePreferencesScreen
|
||||||
|
import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.PreMigrationScreen
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.SourcesScreen
|
import eu.kanade.tachiyomi.ui.browse.source.SourcesScreen
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreenModel.Listing
|
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreenModel.Listing
|
||||||
import eu.kanade.tachiyomi.ui.category.CategoryScreen
|
import eu.kanade.tachiyomi.ui.category.CategoryScreen
|
||||||
@@ -62,6 +63,7 @@ import kotlinx.coroutines.flow.collectLatest
|
|||||||
import kotlinx.coroutines.flow.receiveAsFlow
|
import kotlinx.coroutines.flow.receiveAsFlow
|
||||||
import tachiyomi.core.common.Constants
|
import tachiyomi.core.common.Constants
|
||||||
import tachiyomi.core.common.util.lang.launchIO
|
import tachiyomi.core.common.util.lang.launchIO
|
||||||
|
import tachiyomi.domain.UnsortedPreferences
|
||||||
import tachiyomi.domain.source.model.StubSource
|
import tachiyomi.domain.source.model.StubSource
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.components.material.Scaffold
|
import tachiyomi.presentation.core.components.material.Scaffold
|
||||||
@@ -69,6 +71,8 @@ import tachiyomi.presentation.core.components.material.padding
|
|||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
import tachiyomi.presentation.core.screens.LoadingScreen
|
import tachiyomi.presentation.core.screens.LoadingScreen
|
||||||
import tachiyomi.source.local.LocalSource
|
import tachiyomi.source.local.LocalSource
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
data class BrowseSourceScreen(
|
data class BrowseSourceScreen(
|
||||||
private val sourceId: Long,
|
private val sourceId: Long,
|
||||||
@@ -319,6 +323,16 @@ data class BrowseSourceScreen(
|
|||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
onConfirm = { screenModel.addFavorite(dialog.manga) },
|
onConfirm = { screenModel.addFavorite(dialog.manga) },
|
||||||
onOpenManga = { navigator.push(MangaScreen(dialog.duplicate.id)) },
|
onOpenManga = { navigator.push(MangaScreen(dialog.duplicate.id)) },
|
||||||
|
onMigrate = {
|
||||||
|
// SY -->
|
||||||
|
PreMigrationScreen.navigateToMigration(
|
||||||
|
Injekt.get<UnsortedPreferences>().skipPreMigration().get(),
|
||||||
|
navigator,
|
||||||
|
dialog.duplicate.id,
|
||||||
|
dialog.manga.id,
|
||||||
|
)
|
||||||
|
// SY <--
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is BrowseSourceScreenModel.Dialog.RemoveManga -> {
|
is BrowseSourceScreenModel.Dialog.RemoveManga -> {
|
||||||
|
|||||||
+1
-1
@@ -455,7 +455,7 @@ open class BrowseSourceScreenModel(
|
|||||||
val manga: Manga,
|
val manga: Manga,
|
||||||
val initialSelection: ImmutableList<CheckboxState.State<Category>>,
|
val initialSelection: ImmutableList<CheckboxState.State<Category>>,
|
||||||
) : Dialog
|
) : Dialog
|
||||||
data class Migrate(val newManga: Manga) : Dialog
|
data class Migrate(val newManga: Manga, val oldManga: Manga) : Dialog
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
data class DeleteSavedSearch(val idToDelete: Long, val name: String) : Dialog
|
data class DeleteSavedSearch(val idToDelete: Long, val name: String) : Dialog
|
||||||
|
|||||||
@@ -56,9 +56,7 @@ fun SourceFilterDialog(
|
|||||||
) {
|
) {
|
||||||
val updateFilters = { onUpdate(filters) }
|
val updateFilters = { onUpdate(filters) }
|
||||||
|
|
||||||
AdaptiveSheet(
|
AdaptiveSheet(onDismissRequest = onDismissRequest) {
|
||||||
onDismissRequest = onDismissRequest,
|
|
||||||
) {
|
|
||||||
LazyColumn {
|
LazyColumn {
|
||||||
stickyHeader {
|
stickyHeader {
|
||||||
Row(
|
Row(
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ import kotlinx.collections.immutable.PersistentList
|
|||||||
import kotlinx.collections.immutable.mutate
|
import kotlinx.collections.immutable.mutate
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.asFlow
|
import kotlinx.coroutines.flow.asFlow
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
@@ -233,6 +234,9 @@ class LibraryScreenModel(
|
|||||||
prefs.filterBookmarked,
|
prefs.filterBookmarked,
|
||||||
prefs.filterCompleted,
|
prefs.filterCompleted,
|
||||||
prefs.filterIntervalCustom,
|
prefs.filterIntervalCustom,
|
||||||
|
// SY -->
|
||||||
|
prefs.filterLewd,
|
||||||
|
// SY <--
|
||||||
) + trackFilter.values
|
) + trackFilter.values
|
||||||
).any { it != TriState.DISABLED }
|
).any { it != TriState.DISABLED }
|
||||||
}
|
}
|
||||||
@@ -740,6 +744,7 @@ class LibraryScreenModel(
|
|||||||
clearSelection()
|
clearSelection()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(DelicateCoroutinesApi::class)
|
||||||
fun syncMangaToDex() {
|
fun syncMangaToDex() {
|
||||||
launchIO {
|
launchIO {
|
||||||
MdUtil.getEnabledMangaDex(unsortedPreferences, sourcePreferences, sourceManager)?.let { mdex ->
|
MdUtil.getEnabledMangaDex(unsortedPreferences, sourcePreferences, sourceManager)?.let { mdex ->
|
||||||
|
|||||||
@@ -164,10 +164,10 @@ object LibraryTab : Tab {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onClickSyncNow = {
|
onClickSyncNow = {
|
||||||
if (!SyncDataJob.isAnyJobRunning(context)) {
|
if (!SyncDataJob.isRunning(context)) {
|
||||||
SyncDataJob.startNow(context)
|
SyncDataJob.startNow(context)
|
||||||
} else {
|
} else {
|
||||||
context.toast(MR.strings.sync_in_progress)
|
context.toast(SYMR.strings.sync_in_progress)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// SY -->
|
// SY -->
|
||||||
|
|||||||
@@ -50,8 +50,6 @@ import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
|||||||
import com.google.firebase.analytics.ktx.analytics
|
import com.google.firebase.analytics.ktx.analytics
|
||||||
import com.google.firebase.ktx.Firebase
|
import com.google.firebase.ktx.Firebase
|
||||||
import eu.kanade.domain.base.BasePreferences
|
import eu.kanade.domain.base.BasePreferences
|
||||||
import eu.kanade.domain.source.service.SourcePreferences
|
|
||||||
import eu.kanade.domain.ui.UiPreferences
|
|
||||||
import eu.kanade.presentation.components.AppStateBanners
|
import eu.kanade.presentation.components.AppStateBanners
|
||||||
import eu.kanade.presentation.components.DownloadedOnlyBannerBackgroundColor
|
import eu.kanade.presentation.components.DownloadedOnlyBannerBackgroundColor
|
||||||
import eu.kanade.presentation.components.IncognitoModeBannerBackgroundColor
|
import eu.kanade.presentation.components.IncognitoModeBannerBackgroundColor
|
||||||
@@ -80,7 +78,6 @@ import eu.kanade.tachiyomi.util.system.dpToPx
|
|||||||
import eu.kanade.tachiyomi.util.system.isNavigationBarNeedsScrim
|
import eu.kanade.tachiyomi.util.system.isNavigationBarNeedsScrim
|
||||||
import eu.kanade.tachiyomi.util.system.isPreviewBuildType
|
import eu.kanade.tachiyomi.util.system.isPreviewBuildType
|
||||||
import eu.kanade.tachiyomi.util.view.setComposeContent
|
import eu.kanade.tachiyomi.util.view.setComposeContent
|
||||||
import exh.EXHMigrations
|
|
||||||
import exh.debug.DebugToggles
|
import exh.debug.DebugToggles
|
||||||
import exh.eh.EHentaiUpdateWorker
|
import exh.eh.EHentaiUpdateWorker
|
||||||
import exh.log.DebugModeOverlay
|
import exh.log.DebugModeOverlay
|
||||||
@@ -97,6 +94,7 @@ import kotlinx.coroutines.flow.launchIn
|
|||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
|
import mihon.core.migration.Migrator
|
||||||
import tachiyomi.core.common.Constants
|
import tachiyomi.core.common.Constants
|
||||||
import tachiyomi.core.common.util.lang.launchIO
|
import tachiyomi.core.common.util.lang.launchIO
|
||||||
import tachiyomi.core.common.util.system.logcat
|
import tachiyomi.core.common.util.system.logcat
|
||||||
@@ -105,17 +103,13 @@ import tachiyomi.domain.library.service.LibraryPreferences
|
|||||||
import tachiyomi.domain.release.interactor.GetApplicationRelease
|
import tachiyomi.domain.release.interactor.GetApplicationRelease
|
||||||
import tachiyomi.presentation.core.components.material.Scaffold
|
import tachiyomi.presentation.core.components.material.Scaffold
|
||||||
import tachiyomi.presentation.core.util.collectAsState
|
import tachiyomi.presentation.core.util.collectAsState
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.util.LinkedList
|
import java.util.LinkedList
|
||||||
import androidx.compose.ui.graphics.Color.Companion as ComposeColor
|
import androidx.compose.ui.graphics.Color.Companion as ComposeColor
|
||||||
|
|
||||||
class MainActivity : BaseActivity() {
|
class MainActivity : BaseActivity() {
|
||||||
|
|
||||||
private val sourcePreferences: SourcePreferences by injectLazy()
|
|
||||||
private val libraryPreferences: LibraryPreferences by injectLazy()
|
private val libraryPreferences: LibraryPreferences by injectLazy()
|
||||||
private val uiPreferences: UiPreferences by injectLazy()
|
|
||||||
private val preferences: BasePreferences by injectLazy()
|
private val preferences: BasePreferences by injectLazy()
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
@@ -165,20 +159,7 @@ class MainActivity : BaseActivity() {
|
|||||||
|
|
||||||
val didMigration = if (isLaunch) {
|
val didMigration = if (isLaunch) {
|
||||||
addAnalytics()
|
addAnalytics()
|
||||||
EXHMigrations.upgrade(
|
Migrator.awaitAndRelease()
|
||||||
context = applicationContext,
|
|
||||||
basePreferences = preferences,
|
|
||||||
uiPreferences = uiPreferences,
|
|
||||||
preferenceStore = Injekt.get(),
|
|
||||||
networkPreferences = Injekt.get(),
|
|
||||||
sourcePreferences = sourcePreferences,
|
|
||||||
securityPreferences = Injekt.get(),
|
|
||||||
libraryPreferences = libraryPreferences,
|
|
||||||
readerPreferences = Injekt.get(),
|
|
||||||
backupPreferences = Injekt.get(),
|
|
||||||
trackerManager = Injekt.get(),
|
|
||||||
pagePreviewCache = Injekt.get(),
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ fun EditMangaDialog(
|
|||||||
confirmButton = {
|
confirmButton = {
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
|
@Suppress("NAME_SHADOWING")
|
||||||
val binding = binding ?: return@TextButton
|
val binding = binding ?: return@TextButton
|
||||||
onPositiveClick(
|
onPositiveClick(
|
||||||
binding.title.text.toString(),
|
binding.title.text.toString(),
|
||||||
@@ -120,7 +121,7 @@ fun EditMangaDialog(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun onViewCreated(manga: Manga, context: Context, binding: EditMangaDialogBinding, scope: CoroutineScope) {
|
private fun onViewCreated(manga: Manga, context: Context, binding: EditMangaDialogBinding, scope: CoroutineScope) {
|
||||||
loadCover(manga, context, binding)
|
loadCover(manga, binding)
|
||||||
|
|
||||||
val statusAdapter: ArrayAdapter<String> = ArrayAdapter(
|
val statusAdapter: ArrayAdapter<String> = ArrayAdapter(
|
||||||
context,
|
context,
|
||||||
@@ -188,7 +189,7 @@ private fun onViewCreated(manga: Manga, context: Context, binding: EditMangaDial
|
|||||||
binding.mangaDescription.hint =
|
binding.mangaDescription.hint =
|
||||||
context.stringResource(
|
context.stringResource(
|
||||||
SYMR.strings.description_hint,
|
SYMR.strings.description_hint,
|
||||||
manga.ogDescription?.takeIf { it.isNotBlank() }?.let { it.replace("\n", " ").chop(20) } ?: ""
|
manga.ogDescription?.takeIf { it.isNotBlank() }?.replace("\n", " ")?.chop(20) ?: ""
|
||||||
)
|
)
|
||||||
binding.thumbnailUrl.hint =
|
binding.thumbnailUrl.hint =
|
||||||
context.stringResource(
|
context.stringResource(
|
||||||
@@ -212,7 +213,7 @@ private fun resetTags(manga: Manga, binding: EditMangaDialogBinding, scope: Coro
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadCover(manga: Manga, context: Context, binding: EditMangaDialogBinding) {
|
private fun loadCover(manga: Manga, binding: EditMangaDialogBinding) {
|
||||||
binding.mangaCover.load(manga) {
|
binding.mangaCover.load(manga) {
|
||||||
transformations(RoundedCornersTransformation(4.dpToPx.toFloat()))
|
transformations(RoundedCornersTransformation(4.dpToPx.toFloat()))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ import exh.source.isMdBasedSource
|
|||||||
import exh.ui.ifSourcesLoaded
|
import exh.ui.ifSourcesLoaded
|
||||||
import exh.ui.metadata.MetadataViewScreen
|
import exh.ui.metadata.MetadataViewScreen
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.flow.take
|
import kotlinx.coroutines.flow.take
|
||||||
@@ -251,11 +252,19 @@ class MangaScreen(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is MangaScreenModel.Dialog.DuplicateManga -> DuplicateMangaDialog(
|
|
||||||
onDismissRequest = onDismissRequest,
|
is MangaScreenModel.Dialog.DuplicateManga -> {
|
||||||
onConfirm = { screenModel.toggleFavorite(onRemoved = {}, checkDuplicate = false) },
|
DuplicateMangaDialog(
|
||||||
onOpenManga = { navigator.push(MangaScreen(dialog.duplicate.id)) },
|
onDismissRequest = onDismissRequest,
|
||||||
)
|
onConfirm = { screenModel.toggleFavorite(onRemoved = {}, checkDuplicate = false) },
|
||||||
|
onOpenManga = { navigator.push(MangaScreen(dialog.duplicate.id)) },
|
||||||
|
onMigrate = {
|
||||||
|
// SY -->
|
||||||
|
migrateManga(navigator, dialog.duplicate, screenModel.manga!!.id)
|
||||||
|
// SY <--
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
MangaScreenModel.Dialog.SettingsSheet -> ChapterSettingsDialog(
|
MangaScreenModel.Dialog.SettingsSheet -> ChapterSettingsDialog(
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
manga = successState.manga,
|
manga = successState.manga,
|
||||||
@@ -457,12 +466,13 @@ class MangaScreen(
|
|||||||
/**
|
/**
|
||||||
* Initiates source migration for the specific manga.
|
* Initiates source migration for the specific manga.
|
||||||
*/
|
*/
|
||||||
private fun migrateManga(navigator: Navigator, manga: Manga) {
|
private fun migrateManga(navigator: Navigator, manga: Manga, toMangaId: Long? = null) {
|
||||||
// SY -->
|
// SY -->
|
||||||
PreMigrationScreen.navigateToMigration(
|
PreMigrationScreen.navigateToMigration(
|
||||||
Injekt.get<UnsortedPreferences>().skipPreMigration().get(),
|
Injekt.get<UnsortedPreferences>().skipPreMigration().get(),
|
||||||
navigator,
|
navigator,
|
||||||
listOf(manga.id),
|
manga.id,
|
||||||
|
toMangaId,
|
||||||
)
|
)
|
||||||
// SY <--
|
// SY <--
|
||||||
}
|
}
|
||||||
@@ -505,6 +515,7 @@ class MangaScreen(
|
|||||||
navigator.push(SourcesScreen(smartSearchConfig))
|
navigator.push(SourcesScreen(smartSearchConfig))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(DelicateCoroutinesApi::class)
|
||||||
private fun mergeWithAnother(
|
private fun mergeWithAnother(
|
||||||
navigator: Navigator,
|
navigator: Navigator,
|
||||||
context: Context,
|
context: Context,
|
||||||
|
|||||||
@@ -976,6 +976,7 @@ class MangaScreenModel(
|
|||||||
downloadManager.getQueuedDownloadOrNull(chapter.id)
|
downloadManager.getQueuedDownloadOrNull(chapter.id)
|
||||||
}
|
}
|
||||||
// SY -->
|
// SY -->
|
||||||
|
@Suppress("NAME_SHADOWING")
|
||||||
val manga = mergedData?.manga?.get(chapter.mangaId) ?: manga
|
val manga = mergedData?.manga?.get(chapter.mangaId) ?: manga
|
||||||
val source = mergedData?.sources?.find { manga.source == it.id }?.takeIf { mergedData.sources.size > 2 }
|
val source = mergedData?.sources?.find { manga.source == it.id }?.takeIf { mergedData.sources.size > 2 }
|
||||||
// SY <--
|
// SY <--
|
||||||
@@ -1049,7 +1050,7 @@ class MangaScreenModel(
|
|||||||
downloadNewChapters(newChapters)
|
downloadNewChapters(newChapters)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
state.source.fetchChaptersForMergedManga(state.manga, manualFetch, true, dedupe)
|
state.source.fetchChaptersForMergedManga(state.manga, manualFetch)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
@@ -1548,6 +1549,9 @@ class MangaScreenModel(
|
|||||||
) : Dialog
|
) : Dialog
|
||||||
data class DeleteChapters(val chapters: List<Chapter>) : Dialog
|
data class DeleteChapters(val chapters: List<Chapter>) : Dialog
|
||||||
data class DuplicateManga(val manga: Manga, val duplicate: Manga) : Dialog
|
data class DuplicateManga(val manga: Manga, val duplicate: Manga) : Dialog
|
||||||
|
/* SY -->
|
||||||
|
data class Migrate(val newManga: Manga, val oldManga: Manga) : Dialog
|
||||||
|
SY <-- */
|
||||||
data class SetFetchInterval(val manga: Manga) : Dialog
|
data class SetFetchInterval(val manga: Manga) : Dialog
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
@@ -1580,6 +1584,12 @@ class MangaScreenModel(
|
|||||||
updateSuccessState { it.copy(dialog = Dialog.FullCover) }
|
updateSuccessState { it.copy(dialog = Dialog.FullCover) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* SY -->
|
||||||
|
fun showMigrateDialog(duplicate: Manga) {
|
||||||
|
val manga = successState?.manga ?: return
|
||||||
|
updateSuccessState { it.copy(dialog = Dialog.Migrate(newManga = manga, oldManga = duplicate)) }
|
||||||
|
} SY <-- */
|
||||||
|
|
||||||
fun setExcludedScanlators(excludedScanlators: Set<String>) {
|
fun setExcludedScanlators(excludedScanlators: Set<String>) {
|
||||||
screenModelScope.launchIO {
|
screenModelScope.launchIO {
|
||||||
setExcludedScanlators.await(mangaId, excludedScanlators)
|
setExcludedScanlators.await(mangaId, excludedScanlators)
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user