From 6d255f25ad1e95ca333af2b8ca4d5515055b685f Mon Sep 17 00:00:00 2001 From: "Cuong M. Tran" Date: Mon, 18 Mar 2024 14:11:59 +0700 Subject: [PATCH 001/106] Revert "Remove custom extension readme/changelog URLs" This reverts commit cf5bb4dc9be69e2712674573e9a38c73168b38e9. # Conflicts: # app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsScreen.kt # app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsScreen.kt # app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsScreenModel.kt This reverts commit cf5bb4dc9be69e2712674573e9a38c73168b38e9. # Conflicts: # app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsScreen.kt # app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsScreen.kt # app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsScreenModel.kt --- .../browse/ExtensionDetailsScreen.kt | 24 ++++++++-- .../tachiyomi/extension/api/ExtensionApi.kt | 4 ++ .../tachiyomi/extension/model/Extension.kt | 8 ++++ .../extension/util/ExtensionLoader.kt | 7 +++ .../details/ExtensionDetailsScreen.kt | 4 ++ .../details/ExtensionDetailsScreenModel.kt | 46 +++++++++++++++++++ .../commonMain/resources/MR/base/strings.xml | 1 + 7 files changed, 91 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsScreen.kt index 16871306e..bbd9715e5 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsScreen.kt @@ -16,7 +16,8 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.outlined.Launch +import androidx.compose.material.icons.automirrored.outlined.HelpOutline +import androidx.compose.material.icons.outlined.History import androidx.compose.material.icons.outlined.Settings import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button @@ -69,6 +70,8 @@ fun ExtensionDetailsScreen( navigateUp: () -> Unit, state: ExtensionDetailsScreenModel.State, onClickSourcePreferences: (sourceId: Long) -> Unit, + onClickWhatsNew: () -> Unit, + onClickReadme: () -> Unit, onClickEnableAll: () -> Unit, onClickDisableAll: () -> Unit, onClickClearCookies: () -> Unit, @@ -95,11 +98,26 @@ fun ExtensionDetailsScreen( AppBarActions( actions = persistentListOf().builder() .apply { - if (url != null) { + if (state.extension?.isUnofficial == false) { add( AppBar.Action( + title = stringResource(MR.strings.whats_new), + icon = Icons.Outlined.History, + onClick = onClickWhatsNew, + ), + ) + add( + AppBar.Action( + title = stringResource(MR.strings.action_faq_and_guides), + icon = Icons.AutoMirrored.Outlined.HelpOutline, + onClick = onClickReadme, + ), + ) + } + if (url != null) { + add( + AppBar.OverflowAction( title = stringResource(MR.strings.action_open_repo), - icon = Icons.AutoMirrored.Outlined.Launch, onClick = { uriHandler.openUri(url) }, diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionApi.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionApi.kt index e43ec5cd9..d32f6f0c0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionApi.kt @@ -126,6 +126,8 @@ internal class ExtensionApi { libVersion = it.extractLibVersion(), lang = it.lang, isNsfw = it.nsfw == 1, + hasReadme = it.hasReadme == 1, + hasChangelog = it.hasChangelog == 1, sources = it.sources?.map(extensionSourceMapper).orEmpty(), apkName = it.apk, iconUrl = "$repoUrl/icon/${it.pkg}.png", @@ -160,6 +162,8 @@ private data class ExtensionJsonObject( val code: Long, val version: String, val nsfw: Int, + val hasReadme: Int = 0, + val hasChangelog: Int = 0, val sources: List?, ) diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/model/Extension.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/model/Extension.kt index 1789f8887..6c2478141 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/model/Extension.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/model/Extension.kt @@ -13,6 +13,8 @@ sealed class Extension { abstract val libVersion: Double abstract val lang: String? abstract val isNsfw: Boolean + abstract val hasReadme: Boolean + abstract val hasChangelog: Boolean data class Installed( override val name: String, @@ -22,6 +24,8 @@ sealed class Extension { override val libVersion: Double, override val lang: String, override val isNsfw: Boolean, + override val hasReadme: Boolean, + override val hasChangelog: Boolean, val pkgFactory: String?, val sources: List, val icon: Drawable?, @@ -43,6 +47,8 @@ sealed class Extension { override val libVersion: Double, override val lang: String, override val isNsfw: Boolean, + override val hasReadme: Boolean, + override val hasChangelog: Boolean, val sources: List, val apkName: String, val iconUrl: String, @@ -74,5 +80,7 @@ sealed class Extension { val signatureHash: String, override val lang: String? = null, override val isNsfw: Boolean = false, + override val hasReadme: Boolean = false, + override val hasChangelog: Boolean = false, ) : Extension() } diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionLoader.kt index 2b607427c..4a54317f8 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionLoader.kt @@ -50,6 +50,8 @@ internal object ExtensionLoader { private const val METADATA_SOURCE_CLASS = "tachiyomi.extension.class" private const val METADATA_SOURCE_FACTORY = "tachiyomi.extension.factory" private const val METADATA_NSFW = "tachiyomi.extension.nsfw" + private const val METADATA_HAS_README = "tachiyomi.extension.hasReadme" + private const val METADATA_HAS_CHANGELOG = "tachiyomi.extension.hasChangelog" const val LIB_VERSION_MIN = 1.4 const val LIB_VERSION_MAX = 1.5 @@ -275,6 +277,9 @@ internal object ExtensionLoader { return LoadResult.Error } + val hasReadme = appInfo.metaData.getInt(METADATA_HAS_README, 0) == 1 + val hasChangelog = appInfo.metaData.getInt(METADATA_HAS_CHANGELOG, 0) == 1 + val classLoader = try { ChildFirstPathClassLoader(appInfo.sourceDir, null, context.classLoader) } catch (e: Exception) { @@ -322,6 +327,8 @@ internal object ExtensionLoader { libVersion = libVersion, lang = lang, isNsfw = isNsfw, + hasReadme = hasReadme, + hasChangelog = hasChangelog, sources = sources, pkgFactory = appInfo.metaData.getString(METADATA_SOURCE_FACTORY), isUnofficial = !isOfficiallySigned(signatures), diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsScreen.kt index 4a4a78cde..1797dc89d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsScreen.kt @@ -5,6 +5,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalUriHandler import cafe.adriel.voyager.core.model.rememberScreenModel import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow @@ -29,11 +30,14 @@ data class ExtensionDetailsScreen( } val navigator = LocalNavigator.currentOrThrow + val uriHandler = LocalUriHandler.current ExtensionDetailsScreen( navigateUp = navigator::pop, state = state, onClickSourcePreferences = { navigator.push(SourcePreferencesScreen(it)) }, + onClickWhatsNew = { uriHandler.openUri(screenModel.getChangelogUrl()) }, + onClickReadme = { uriHandler.openUri(screenModel.getReadmeUrl()) }, onClickEnableAll = { screenModel.toggleSources(true) }, onClickDisableAll = { screenModel.toggleSources(false) }, onClickClearCookies = screenModel::clearCookies, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsScreenModel.kt index 4266c4730..7f9145b28 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsScreenModel.kt @@ -29,6 +29,11 @@ import tachiyomi.core.common.util.system.logcat import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get +private const val URL_EXTENSION_COMMITS = + "https://github.com/komikku-app/komikku-extensions/commits/master" +private const val URL_EXTENSION_BLOB = + "https://github.com/komikku-app/komikku-extensions/blob/master" + class ExtensionDetailsScreenModel( pkgName: String, context: Context, @@ -83,6 +88,31 @@ class ExtensionDetailsScreenModel( } } + fun getChangelogUrl(): String { + val extension = state.value.extension ?: return "" + + val pkgName = extension.pkgName.substringAfter("eu.kanade.tachiyomi.extension.") + val pkgFactory = extension.pkgFactory + if (extension.hasChangelog) { + return createUrl(URL_EXTENSION_BLOB, pkgName, pkgFactory, "/CHANGELOG.md") + } + + // Falling back on GitHub commit history because there is no explicit changelog in extension + return createUrl(URL_EXTENSION_COMMITS, pkgName, pkgFactory) + } + + fun getReadmeUrl(): String { + val extension = state.value.extension ?: return "" + + if (!extension.hasReadme) { + return "https://tachiyomi.org/docs/faq/browse/extensions" + } + + val pkgName = extension.pkgName.substringAfter("eu.kanade.tachiyomi.extension.") + val pkgFactory = extension.pkgFactory + return createUrl(URL_EXTENSION_BLOB, pkgName, pkgFactory, "/README.md") + } + fun clearCookies() { val extension = state.value.extension ?: return @@ -118,6 +148,22 @@ class ExtensionDetailsScreenModel( ?.let { toggleSource.await(it, enable) } } + private fun createUrl( + url: String, + pkgName: String, + pkgFactory: String?, + path: String = "", + ): String { + return if (!pkgFactory.isNullOrEmpty()) { + when (path.isEmpty()) { + true -> "$url/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/$pkgFactory" + else -> "$url/multisrc/overrides/$pkgFactory/" + (pkgName.split(".").lastOrNull() ?: "") + path + } + } else { + url + "/src/" + pkgName.replace(".", "/") + path + } + } + @Immutable data class State( val extension: Extension.Installed? = null, diff --git a/i18n/src/commonMain/resources/MR/base/strings.xml b/i18n/src/commonMain/resources/MR/base/strings.xml index af56253ca..42bdc9bca 100644 --- a/i18n/src/commonMain/resources/MR/base/strings.xml +++ b/i18n/src/commonMain/resources/MR/base/strings.xml @@ -162,6 +162,7 @@ Forward Refresh Start downloading now + FAQ and Guides Not now From 7c11ddfc1e3f7552da0bf8b7b44003336b412880 Mon Sep 17 00:00:00 2001 From: "Cuong M. Tran" Date: Mon, 18 Mar 2024 14:30:57 +0700 Subject: [PATCH 002/106] =?UTF-8?q?Update=20CrashLog=20to=20include=20Unof?= =?UTF-8?q?ficial=20ext=E2=80=99s=20info?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/java/eu/kanade/tachiyomi/util/CrashLogUtil.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/CrashLogUtil.kt b/app/src/main/java/eu/kanade/tachiyomi/util/CrashLogUtil.kt index c911e3dc2..a8a5018c6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/CrashLogUtil.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/CrashLogUtil.kt @@ -63,7 +63,7 @@ class CrashLogUtil( """ - ${it.name} Installed: ${it.versionName} / Available: ${availableExtension?.versionName ?: "?"} - Obsolete: ${it.isObsolete} + Obsolete: ${it.isObsolete} / Unofficial: ${it.isUnofficial} """.trimIndent() } From f6dcb2431ffd41010448ea9a754d0a398fd823c9 Mon Sep 17 00:00:00 2001 From: Cuong Tran Date: Sat, 24 Feb 2024 00:45:42 +0700 Subject: [PATCH 003/106] =?UTF-8?q?download=E2=80=99s=20cache=20renew=20in?= =?UTF-8?q?terval=20configurable?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../settings/screen/SettingsDownloadScreen.kt | 29 +++++++++++++++++++ .../tachiyomi/data/download/DownloadCache.kt | 16 +++++++--- .../download/service/DownloadPreferences.kt | 4 +++ .../commonMain/resources/MR/base/strings.xml | 8 +++++ 4 files changed, 53 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDownloadScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDownloadScreen.kt index 072013415..98eaf84de 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDownloadScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDownloadScreen.kt @@ -61,6 +61,9 @@ object SettingsDownloadScreen : SearchableSettings { allCategories = allCategories, ), getDownloadAheadGroup(downloadPreferences = downloadPreferences), + // KMK --> + getDownloadCacheRenewInterval(downloadPreferences = downloadPreferences), + // KMK <-- ) } @@ -190,4 +193,30 @@ object SettingsDownloadScreen : SearchableSettings { ), ) } + + // KMK --> + @Composable + private fun getDownloadCacheRenewInterval( + downloadPreferences: DownloadPreferences, + ): Preference.PreferenceGroup { + return Preference.PreferenceGroup( + title = stringResource(MR.strings.download_cache_renew_interval), + preferenceItems = persistentListOf( + Preference.PreferenceItem.ListPreference( + pref = downloadPreferences.downloadCacheRenewInterval(), + title = stringResource(MR.strings.download_cache_renew_interval), + entries = persistentMapOf( + -1 to stringResource(MR.strings.download_cache_renew_interval_manual), + 1 to stringResource(MR.strings.download_cache_renew_interval_1hour), + 2 to stringResource(MR.strings.download_cache_renew_interval_2hour), + 6 to stringResource(MR.strings.download_cache_renew_interval_6hour), + 12 to stringResource(MR.strings.download_cache_renew_interval_12hour), + 24 to stringResource(MR.strings.download_cache_renew_interval_24hour), + ), + ), + Preference.PreferenceItem.InfoPreference(stringResource(MR.strings.download_cache_renew_interval_info)), + ), + ) + } + // KMK <-- } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadCache.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadCache.kt index 57c416bde..75c0468c8 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadCache.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadCache.kt @@ -45,6 +45,7 @@ import tachiyomi.core.common.util.lang.launchIO import tachiyomi.core.common.util.lang.launchNonCancellable import tachiyomi.core.common.util.system.logcat import tachiyomi.domain.chapter.model.Chapter +import tachiyomi.domain.download.service.DownloadPreferences import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.source.service.SourceManager import tachiyomi.domain.storage.service.StorageManager @@ -69,6 +70,9 @@ class DownloadCache( ) { private val scope = CoroutineScope(Dispatchers.IO) + // KMK --> + private val downloadPreferences: DownloadPreferences = Injekt.get() + // KMK <-- private val _changes: Channel = Channel(Channel.UNLIMITED) val changes = _changes.receiveAsFlow() @@ -79,7 +83,11 @@ class DownloadCache( * The interval after which this cache should be invalidated. 1 hour shouldn't cause major * issues, as the cache is only used for UI feedback. */ - private val renewInterval = 1.hours.inWholeMilliseconds + private val renewInterval //= 1.hours.inWholeMilliseconds + // KMK --> + get() = downloadPreferences.downloadCacheRenewInterval().get() + .hours.inWholeMilliseconds + // KMK <-- /** * The last time the cache was refreshed. @@ -320,7 +328,7 @@ class DownloadCache( lastRenew = 0L renewalJob?.cancel() diskCacheFile.delete() - renewCache() + renewCache(/* KMK --> */ 0L /* KMK <-- */) } // KMK --> @@ -335,9 +343,9 @@ class DownloadCache( /** * Renews the downloads cache. */ - private fun renewCache() { + private fun renewCache(/* KMK --> */ renewInterval: Long = this.renewInterval /* KMK <-- */) { // Avoid renewing cache if in the process nor too often - if (lastRenew + renewInterval >= System.currentTimeMillis() || renewalJob?.isActive == true) { + if (/* KMK --> */ renewInterval < 0L || /* KMK <-- */ lastRenew + renewInterval >= System.currentTimeMillis() || renewalJob?.isActive == true) { return } diff --git a/domain/src/main/java/tachiyomi/domain/download/service/DownloadPreferences.kt b/domain/src/main/java/tachiyomi/domain/download/service/DownloadPreferences.kt index a0625e5a7..96af591e5 100644 --- a/domain/src/main/java/tachiyomi/domain/download/service/DownloadPreferences.kt +++ b/domain/src/main/java/tachiyomi/domain/download/service/DownloadPreferences.kt @@ -42,4 +42,8 @@ class DownloadPreferences( "download_new_categories_exclude", emptySet(), ) + + // KMK --> + fun downloadCacheRenewInterval() = preferenceStore.getInt("download_cache_renew_interval", 1) + // KMK <-- } diff --git a/i18n/src/commonMain/resources/MR/base/strings.xml b/i18n/src/commonMain/resources/MR/base/strings.xml index 42bdc9bca..5c7ff31cf 100644 --- a/i18n/src/commonMain/resources/MR/base/strings.xml +++ b/i18n/src/commonMain/resources/MR/base/strings.xml @@ -489,6 +489,14 @@ Save as CBZ archive Split tall images Improves reader performance + Download cache renew interval + Manual + 1 hour + 2 hours + 6 hours + 12 hours + 1 day + If set to "manual", either refresh each manga or using the "Invalidate downloads index" action in "Advanced" Tracking guide From f1ed6c0128bb968db1f04fa3e57b511ab9f835e3 Mon Sep 17 00:00:00 2001 From: az4521 <18432684+az4521@users.noreply.github.com> Date: Sat, 9 Mar 2024 10:08:57 -0500 Subject: [PATCH 004/106] Update image-decoder (#466) Use newer image-decoder lib fixes crashing when trying to load corrupt images below 12 bytes in size # Conflicts: # gradle/libs.versions.toml --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ddce7c38e..2703acc59 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -52,7 +52,7 @@ coil-compose = { module = "io.coil-kt.coil3:coil-compose" } coil-network-okhttp = { module = "io.coil-kt.coil3:coil-network-okhttp" } subsamplingscaleimageview = "com.github.tachiyomiorg:subsampling-scale-image-view:7e57335" -image-decoder = "com.github.tachiyomiorg:image-decoder:fbd6601290" +image-decoder = "com.github.tachiyomiorg:image-decoder:398d3c074f" exifinterface = "androidx.exifinterface:exifinterface:1.3.6" natural-comparator = "com.github.gpanther:java-nat-sort:natural-comparator-1.1" From 95250670dc8f7be656571720bfbf969e26266f9f Mon Sep 17 00:00:00 2001 From: Jobobby04 Date: Fri, 15 Mar 2024 19:51:56 -0400 Subject: [PATCH 005/106] Fix multiple issues regarding sources loading too late --- .../tachiyomi/data/download/DownloadCache.kt | 19 +++++------ .../tachiyomi/extension/ExtensionManager.kt | 11 +++--- .../tachiyomi/source/AndroidSourceManager.kt | 6 ++++ .../details/SourcePreferencesScreen.kt | 10 ++++-- .../migration/search/SourceSearchScreen.kt | 7 ++++ .../source/browse/BrowseSourceScreen.kt | 7 ++++ .../ui/browse/source/feed/SourceFeedScreen.kt | 7 ++++ .../source/globalsearch/GlobalSearchScreen.kt | 6 ++++ .../kanade/tachiyomi/ui/manga/MangaScreen.kt | 6 ++++ .../tachiyomi/ui/reader/ReaderActivity.kt | 5 +++ .../tachiyomi/ui/reader/ReaderViewModel.kt | 2 ++ .../exh/md/follows/MangaDexFollowsScreen.kt | 7 ++++ .../exh/md/similar/MangaDexSimilarScreen.kt | 7 ++++ .../main/java/exh/recs/RecommendsScreen.kt | 7 ++++ app/src/main/java/exh/ui/SourceUtil.kt | 13 +++++++ .../exh/ui/intercept/InterceptActivity.kt | 34 +++++++++++-------- .../domain/source/service/SourceManager.kt | 3 ++ 17 files changed, 126 insertions(+), 31 deletions(-) create mode 100644 app/src/main/java/exh/ui/SourceUtil.kt diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadCache.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadCache.kt index 75c0468c8..ef8bec2b7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadCache.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadCache.kt @@ -18,6 +18,7 @@ import kotlinx.coroutines.ensureActive import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onStart @@ -355,19 +356,15 @@ class DownloadCache( } // Try to wait until extensions and sources have loaded - var sources = getSources() - if (sources.isEmpty()) { - withTimeoutOrNull(30.seconds) { - while (!extensionManager.isInitialized) { - delay(2.seconds) - } + // SY --> + var sources = emptyList() + withTimeoutOrNull(30.seconds) { + extensionManager.isInitialized.first { it } + sourceManager.isInitialized.first { it } - while (extensionManager.availableExtensionsFlow.value.isNotEmpty() && sources.isEmpty()) { - delay(2.seconds) - sources = getSources() - } - } + sources = getSources() } + // SY <-- val sourceMap = sources.associate { provider.getSourceDirName(it).lowercase() to it.id } diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt index 302d269a4..588dd98c5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt @@ -26,6 +26,7 @@ import kotlinx.coroutines.async import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.map @@ -53,8 +54,10 @@ class ExtensionManager( private val trustExtension: TrustExtension = Injekt.get(), ) { - var isInitialized = false - private set + // SY --> + private val _isInitialized = MutableStateFlow(false) + val isInitialized: StateFlow = _isInitialized.asStateFlow() + // SY <-- /** * API where all the available extensions can be found. @@ -135,9 +138,9 @@ class ExtensionManager( .map { it.extension } // SY --> .filterNotBlacklisted() - // SY <-- - isInitialized = true + _isInitialized.value = true + // SY <-- } // EXH --> diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/AndroidSourceManager.kt b/app/src/main/java/eu/kanade/tachiyomi/source/AndroidSourceManager.kt index 1c0f131ac..139034770 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/AndroidSourceManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/AndroidSourceManager.kt @@ -29,6 +29,8 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map @@ -103,6 +105,7 @@ class AndroidSourceManager( } } sourcesMapFlow.value = mutableMap + _isInitialized.value = true } } @@ -186,6 +189,9 @@ class AndroidSourceManager( } // SY --> + private val _isInitialized = MutableStateFlow(false) + override val isInitialized: StateFlow = _isInitialized.asStateFlow() + override fun getVisibleOnlineSources() = sourcesMapFlow.value.values .filterIsInstance() .filter { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/SourcePreferencesScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/SourcePreferencesScreen.kt index 9d5b8c759..2f08f074e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/SourcePreferencesScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/SourcePreferencesScreen.kt @@ -39,8 +39,10 @@ import eu.kanade.tachiyomi.source.ConfigurableSource import eu.kanade.tachiyomi.source.sourcePreferences import eu.kanade.tachiyomi.widget.TachiyomiTextInputEditText.Companion.setIncognito import exh.source.EnhancedHttpSource +import exh.ui.ifSourcesLoaded import tachiyomi.domain.source.service.SourceManager import tachiyomi.presentation.core.components.material.Scaffold +import tachiyomi.presentation.core.screens.LoadingScreen import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get @@ -48,6 +50,11 @@ class SourcePreferencesScreen(val sourceId: Long) : Screen() { @Composable override fun Content() { + if (!ifSourcesLoaded()) { + LoadingScreen() + return + } + val context = LocalContext.current val navigator = LocalNavigator.currentOrThrow @@ -130,7 +137,7 @@ class SourcePreferencesFragment : PreferenceFragmentCompat() { // SY --> val source = Injekt.get() .getOrStub(sourceId) - ?.let { source -> + .let { source -> if (source is EnhancedHttpSource) { if (source.enhancedSource is ConfigurableSource) { source.source() @@ -141,7 +148,6 @@ class SourcePreferencesFragment : PreferenceFragmentCompat() { source } } - ?: throw NullPointerException("source = null, SOURCE_ID = $SOURCE_ID") // SY <-- val sourceScreen = preferenceManager.createPreferenceScreen(requireContext()) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SourceSearchScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SourceSearchScreen.kt index 1b7083e9e..d000613ee 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SourceSearchScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SourceSearchScreen.kt @@ -29,10 +29,12 @@ import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreenModel import eu.kanade.tachiyomi.ui.browse.source.browse.SourceFilterDialog import eu.kanade.tachiyomi.ui.manga.MangaScreen import eu.kanade.tachiyomi.ui.webview.WebViewScreen +import exh.ui.ifSourcesLoaded import kotlinx.collections.immutable.persistentListOf import tachiyomi.core.common.Constants import tachiyomi.domain.manga.model.Manga import tachiyomi.presentation.core.components.material.Scaffold +import tachiyomi.presentation.core.screens.LoadingScreen import tachiyomi.source.local.LocalSource data class SourceSearchScreen( @@ -43,6 +45,11 @@ data class SourceSearchScreen( @Composable override fun Content() { + if (!ifSourcesLoaded()) { + LoadingScreen() + return + } + val uriHandler = LocalUriHandler.current val navigator = LocalNavigator.currentOrThrow diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt index 0ed54e43a..98aff3e56 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt @@ -61,6 +61,7 @@ import eu.kanade.tachiyomi.ui.manga.MangaScreen import eu.kanade.tachiyomi.ui.webview.WebViewScreen import eu.kanade.tachiyomi.util.system.toast import exh.md.follows.MangaDexFollowsScreen +import exh.ui.ifSourcesLoaded import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.receiveAsFlow @@ -71,6 +72,7 @@ import tachiyomi.i18n.MR import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.i18n.stringResource +import tachiyomi.presentation.core.screens.LoadingScreen import tachiyomi.source.local.LocalSource data class BrowseSourceScreen( @@ -89,6 +91,11 @@ data class BrowseSourceScreen( @Composable override fun Content() { + if (!ifSourcesLoaded()) { + LoadingScreen() + return + } + val screenModel = rememberScreenModel { BrowseSourceScreenModel( sourceId = sourceId, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedScreen.kt index dfedfc57a..feb5976ae 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedScreen.kt @@ -26,15 +26,22 @@ import eu.kanade.tachiyomi.ui.browse.source.browse.SourceFilterDialog import eu.kanade.tachiyomi.ui.manga.MangaScreen import eu.kanade.tachiyomi.util.system.toast import exh.md.follows.MangaDexFollowsScreen +import exh.ui.ifSourcesLoaded import exh.util.nullIfBlank import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.source.interactor.GetRemoteManga import tachiyomi.domain.source.model.SavedSearch +import tachiyomi.presentation.core.screens.LoadingScreen class SourceFeedScreen(val sourceId: Long) : Screen() { @Composable override fun Content() { + if (!ifSourcesLoaded()) { + LoadingScreen() + return + } + val screenModel = rememberScreenModel { SourceFeedScreenModel(sourceId) } val state by screenModel.state.collectAsState() val navigator = LocalNavigator.currentOrThrow diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchScreen.kt index a641c59dd..92f021efc 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchScreen.kt @@ -22,6 +22,7 @@ import eu.kanade.tachiyomi.ui.browse.ChangeMangasCategoryDialog import eu.kanade.tachiyomi.ui.browse.RemoveMangaDialog import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreen import eu.kanade.tachiyomi.ui.manga.MangaScreen +import exh.ui.ifSourcesLoaded import tachiyomi.presentation.core.screens.LoadingScreen class GlobalSearchScreen( @@ -31,6 +32,11 @@ class GlobalSearchScreen( @Composable override fun Content() { + if (!ifSourcesLoaded()) { + LoadingScreen() + return + } + val navigator = LocalNavigator.currentOrThrow val screenModel = rememberScreenModel { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt index 4c4e0a5f2..b4bfba757 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt @@ -64,6 +64,7 @@ import exh.recs.RecommendsScreen import exh.source.MERGED_SOURCE_ID import exh.source.getMainSource import exh.source.isMdBasedSource +import exh.ui.ifSourcesLoaded import exh.ui.metadata.MetadataViewScreen import kotlinx.coroutines.CancellationException import kotlinx.coroutines.flow.launchIn @@ -98,6 +99,11 @@ class MangaScreen( @Composable override fun Content() { + if (!ifSourcesLoaded()) { + LoadingScreen() + return + } + val navigator = LocalNavigator.currentOrThrow val context = LocalContext.current val haptic = LocalHapticFeedback.current diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt index f1313527c..d25e731fc 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt @@ -91,6 +91,7 @@ import eu.kanade.tachiyomi.util.system.toShareIntent import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.view.setComposeContent import exh.source.isEhBasedSource +import exh.ui.ifSourcesLoaded import exh.util.defaultReaderType import exh.util.mangaType import kotlinx.collections.immutable.persistentSetOf @@ -391,6 +392,10 @@ class ReaderActivity : BaseActivity() { ) } + if (!ifSourcesLoaded()) { + return@setComposeContent + } + val isHttpSource = viewModel.getSource() is HttpSource val isFullscreen by readerPreferences.fullscreen().collectAsState() val flashOnPageChange by readerPreferences.flashOnPageChange().collectAsState() diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt index e37870783..7188bd39f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt @@ -63,6 +63,7 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapLatest @@ -336,6 +337,7 @@ class ReaderViewModel @JvmOverloads constructor( val manga = getManga.await(mangaId) if (manga != null) { // SY --> + sourceManager.isInitialized.first { it } val source = sourceManager.getOrStub(manga.source) val metadataSource = source.getMainSource>() val metadata = if (metadataSource != null) { diff --git a/app/src/main/java/exh/md/follows/MangaDexFollowsScreen.kt b/app/src/main/java/exh/md/follows/MangaDexFollowsScreen.kt index ed30d3919..e38a5fef0 100644 --- a/app/src/main/java/exh/md/follows/MangaDexFollowsScreen.kt +++ b/app/src/main/java/exh/md/follows/MangaDexFollowsScreen.kt @@ -28,15 +28,22 @@ import eu.kanade.tachiyomi.ui.browse.ChangeMangasCategoryDialog import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreenModel import eu.kanade.tachiyomi.ui.category.CategoryScreen import eu.kanade.tachiyomi.ui.manga.MangaScreen +import exh.ui.ifSourcesLoaded import tachiyomi.core.common.util.lang.launchIO import tachiyomi.i18n.sy.SYMR import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.i18n.stringResource +import tachiyomi.presentation.core.screens.LoadingScreen class MangaDexFollowsScreen(private val sourceId: Long) : Screen() { @Composable override fun Content() { + if (!ifSourcesLoaded()) { + LoadingScreen() + return + } + val navigator = LocalNavigator.currentOrThrow val scope = rememberCoroutineScope() val haptic = LocalHapticFeedback.current diff --git a/app/src/main/java/exh/md/similar/MangaDexSimilarScreen.kt b/app/src/main/java/exh/md/similar/MangaDexSimilarScreen.kt index 772d5c0b5..7ef6cb3c4 100644 --- a/app/src/main/java/exh/md/similar/MangaDexSimilarScreen.kt +++ b/app/src/main/java/exh/md/similar/MangaDexSimilarScreen.kt @@ -24,15 +24,22 @@ import eu.kanade.tachiyomi.ui.browse.ChangeMangaCategoryDialog import eu.kanade.tachiyomi.ui.browse.ChangeMangasCategoryDialog import eu.kanade.tachiyomi.ui.browse.RemoveMangaDialog import eu.kanade.tachiyomi.ui.manga.MangaScreen +import exh.ui.ifSourcesLoaded import tachiyomi.domain.manga.model.Manga import tachiyomi.i18n.sy.SYMR import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.i18n.stringResource +import tachiyomi.presentation.core.screens.LoadingScreen class MangaDexSimilarScreen(val mangaId: Long, val sourceId: Long) : Screen() { @Composable override fun Content() { + if (!ifSourcesLoaded()) { + LoadingScreen() + return + } + val screenModel = rememberScreenModel { MangaDexSimilarScreenModel(mangaId, sourceId) } val state by screenModel.state.collectAsState() val navigator = LocalNavigator.currentOrThrow diff --git a/app/src/main/java/exh/recs/RecommendsScreen.kt b/app/src/main/java/exh/recs/RecommendsScreen.kt index c1d385aac..9743809ac 100644 --- a/app/src/main/java/exh/recs/RecommendsScreen.kt +++ b/app/src/main/java/exh/recs/RecommendsScreen.kt @@ -25,13 +25,20 @@ import eu.kanade.tachiyomi.ui.browse.ChangeMangaCategoryDialog import eu.kanade.tachiyomi.ui.browse.ChangeMangasCategoryDialog import eu.kanade.tachiyomi.ui.browse.RemoveMangaDialog import eu.kanade.tachiyomi.ui.browse.source.SourcesScreen +import exh.ui.ifSourcesLoaded import tachiyomi.domain.manga.model.Manga import tachiyomi.presentation.core.components.material.Scaffold +import tachiyomi.presentation.core.screens.LoadingScreen class RecommendsScreen(val mangaId: Long, val sourceId: Long) : Screen() { @Composable override fun Content() { + if (!ifSourcesLoaded()) { + LoadingScreen() + return + } + val screenModel = rememberScreenModel { RecommendsScreenModel(mangaId, sourceId) } val state by screenModel.state.collectAsState() val navigator = LocalNavigator.currentOrThrow diff --git a/app/src/main/java/exh/ui/SourceUtil.kt b/app/src/main/java/exh/ui/SourceUtil.kt new file mode 100644 index 000000000..653ea61d2 --- /dev/null +++ b/app/src/main/java/exh/ui/SourceUtil.kt @@ -0,0 +1,13 @@ +package exh.ui + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.remember +import tachiyomi.domain.source.service.SourceManager +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +@Composable +fun ifSourcesLoaded(): Boolean { + return remember { Injekt.get().isInitialized }.collectAsState().value +} diff --git a/app/src/main/java/exh/ui/intercept/InterceptActivity.kt b/app/src/main/java/exh/ui/intercept/InterceptActivity.kt index a9341acf7..ba6c2d145 100644 --- a/app/src/main/java/exh/ui/intercept/InterceptActivity.kt +++ b/app/src/main/java/exh/ui/intercept/InterceptActivity.kt @@ -28,19 +28,22 @@ import eu.kanade.tachiyomi.ui.reader.ReaderActivity import eu.kanade.tachiyomi.util.view.setComposeContent import exh.GalleryAddEvent import exh.GalleryAdder -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch import tachiyomi.core.common.Constants import tachiyomi.core.common.i18n.stringResource +import tachiyomi.core.common.util.lang.launchIO import tachiyomi.domain.chapter.model.Chapter import tachiyomi.domain.manga.model.Manga +import tachiyomi.domain.source.service.SourceManager import tachiyomi.i18n.MR import tachiyomi.i18n.sy.SYMR import tachiyomi.presentation.core.components.material.Scaffold +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get class InterceptActivity : BaseActivity() { private var statusJob: Job? = null @@ -108,7 +111,11 @@ class InterceptActivity : BaseActivity() { private fun processLink() { if (Intent.ACTION_VIEW == intent.action) { - loadGallery(intent.dataString!!) + lifecycleScope.launchIO { + // wait for sources to load + Injekt.get().isInitialized.first { it } + loadGallery(intent.dataString!!) + } } } @@ -167,8 +174,7 @@ class InterceptActivity : BaseActivity() { private val galleryAdder = GalleryAdder() - @Synchronized - fun loadGallery(gallery: String) { + suspend fun loadGallery(gallery: String) { // Do not load gallery if already loading if (status.value is InterceptResult.Idle) { status.value = InterceptResult.Loading @@ -178,7 +184,10 @@ class InterceptActivity : BaseActivity() { .setTitle(MR.strings.label_sources.getString(this)) .setSingleChoiceItems(sources.map { it.toString() }.toTypedArray(), 0) { dialog, index -> dialog.dismiss() - loadGalleryEnd(gallery, sources[index]) + lifecycleScope.launchIO { + loadGalleryEnd(gallery, sources[index]) + } + } .show() } else { @@ -187,15 +196,12 @@ class InterceptActivity : BaseActivity() { } } - private fun loadGalleryEnd(gallery: String, source: UrlImportableSource? = null) { - // Load gallery async - lifecycleScope.launch(Dispatchers.IO) { - val result = galleryAdder.addGallery(this@InterceptActivity, gallery, forceSource = source) + private suspend fun loadGalleryEnd(gallery: String, source: UrlImportableSource? = null) { + val result = galleryAdder.addGallery(this@InterceptActivity, gallery, forceSource = source) - status.value = when (result) { - is GalleryAddEvent.Success -> InterceptResult.Success(result.manga.id, result.manga, result.chapter) - is GalleryAddEvent.Fail -> InterceptResult.Failure(result.logMessage) - } + status.value = when (result) { + is GalleryAddEvent.Success -> InterceptResult.Success(result.manga.id, result.manga, result.chapter) + is GalleryAddEvent.Fail -> InterceptResult.Failure(result.logMessage) } } diff --git a/domain/src/main/java/tachiyomi/domain/source/service/SourceManager.kt b/domain/src/main/java/tachiyomi/domain/source/service/SourceManager.kt index ee5727173..349506ee7 100644 --- a/domain/src/main/java/tachiyomi/domain/source/service/SourceManager.kt +++ b/domain/src/main/java/tachiyomi/domain/source/service/SourceManager.kt @@ -4,6 +4,7 @@ import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.online.HttpSource import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow import tachiyomi.domain.source.model.StubSource interface SourceManager { @@ -19,6 +20,8 @@ interface SourceManager { fun getCatalogueSources(): List // SY --> + val isInitialized: StateFlow + fun getVisibleOnlineSources(): List fun getVisibleCatalogueSources(): List From df864011e350bce40ec6fb80061def4a9f30aff7 Mon Sep 17 00:00:00 2001 From: az4521 <18432684+az4521@users.noreply.github.com> Date: Sat, 9 Mar 2024 10:08:57 -0500 Subject: [PATCH 006/106] Update image-decoder (#466) Use newer image-decoder lib fixes crashing when trying to load corrupt images below 12 bytes in size (cherry picked from commit 154f4d327caea9ceef6a53e739324ee0a9ed959d) # Conflicts: # gradle/libs.versions.toml --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2703acc59..b15969b0b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -116,4 +116,4 @@ shizuku = ["shizuku-api", "shizuku-provider"] sqldelight = ["sqldelight-android-driver", "sqldelight-coroutines", "sqldelight-android-paging"] voyager = ["voyager-navigator", "voyager-screenmodel", "voyager-tab-navigator", "voyager-transitions"] richtext = ["richtext-commonmark", "richtext-m3"] -test = ["junit", "kotest-assertions", "mockk"] \ No newline at end of file +test = ["junit", "kotest-assertions", "mockk"] From 748902e105754c789e54ae3afb6f13c38f0e4142 Mon Sep 17 00:00:00 2001 From: FooIbar <118464521+FooIbar@users.noreply.github.com> Date: Sat, 9 Mar 2024 23:09:06 +0800 Subject: [PATCH 007/106] Fix dual page split for local source (#485) `InputStream.available()` is implementation-dependent, should never assume it will return the total number of bytes in the stream. (cherry picked from commit d0e64d3a66d227ca61fc8d956b03cab5ac3b84f0) # Conflicts: # core/common/src/main/kotlin/tachiyomi/core/common/util/system/ImageUtil.kt --- .../main/kotlin/tachiyomi/core/common/util/system/ImageUtil.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/common/src/main/kotlin/tachiyomi/core/common/util/system/ImageUtil.kt b/core/common/src/main/kotlin/tachiyomi/core/common/util/system/ImageUtil.kt index 4f7f90017..6f207afc8 100644 --- a/core/common/src/main/kotlin/tachiyomi/core/common/util/system/ImageUtil.kt +++ b/core/common/src/main/kotlin/tachiyomi/core/common/util/system/ImageUtil.kt @@ -652,7 +652,7 @@ object ImageUtil { if (zip4jFile != null && zip4jEntry != null) return extractImageOptionsZip4j(zip4jFile, zip4jEntry) // SY <-- - imageStream.mark(imageStream.available() + 1) + imageStream.mark(Int.MAX_VALUE) val imageBytes = imageStream.readBytes() val options = BitmapFactory.Options().apply { inJustDecodeBounds = true } From 38213ce0a68c6a5e2e06ec3cf01cc72b931e2691 Mon Sep 17 00:00:00 2001 From: Redjard Date: Sat, 9 Mar 2024 19:48:43 +0100 Subject: [PATCH 008/106] Fix shizuku being buggy for multi user setups (#494) * Fix #493 Fetch the current userid separately because shizuku always runs as the main user and would otherwise install and update for the main user * Update app/src/main/java/eu/kanade/tachiyomi/extension/installer/ShizukuInstaller.kt --------- Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com> (cherry picked from commit 402e579a69391e7633754066eb8e6480bd9f247d) # Conflicts: # app/src/main/java/eu/kanade/tachiyomi/extension/installer/ShizukuInstaller.kt --- .../kanade/tachiyomi/extension/installer/ShizukuInstaller.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/installer/ShizukuInstaller.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/installer/ShizukuInstaller.kt index cd41f78f8..eda627589 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/installer/ShizukuInstaller.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/installer/ShizukuInstaller.kt @@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.extension.installer import android.app.Service import android.content.pm.PackageManager import android.os.Build +import android.os.Process import eu.kanade.tachiyomi.extension.model.InstallStep import eu.kanade.tachiyomi.util.system.getUriSize import eu.kanade.tachiyomi.util.system.toast @@ -51,7 +52,8 @@ class ShizukuInstaller(private val service: Service) : Installer(service) { val size = service.getUriSize(entry.uri) ?: throw IllegalStateException() service.contentResolver.openInputStream(entry.uri)!!.use { val createCommand = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - "pm install-create --user current -r -i ${service.packageName} -S $size" + val userId = Process.myUserHandle().hashCode() + "pm install-create --user $userId -r -i ${service.packageName} -S $size" } else { "pm install-create -r -i ${service.packageName} -S $size" } From caec0afcb1cc391e3cc3eaff3a6e9ecbdd66eb0a Mon Sep 17 00:00:00 2001 From: KaiserBh <41852205+kaiserbh@users.noreply.github.com> Date: Sun, 10 Mar 2024 06:45:41 +1100 Subject: [PATCH 009/106] feat: db changes to accommodate new cross device syncing logic. (#450) * feat: db changes to accommodate new syncing logic. Using timestamp to sync is a bit skewed due to system clock etc and therefore there was a lot of issues with it such as removing a manga that shouldn't have been removed. Marking chapters as unread even though it was marked as a read. Hopefully by using versioning system it should eliminate those issues. * chore: add new line. * chore: remove isSyncing from Chapter/Manga model. * chore: remove isSyncing leftover. * chore: remove isSyncing. * refactor: remove isSync guard. Just use it directly to 1 now since we don't have the isSyncing field in Manga or Chapter. * Lint and stuff * Add missing , --------- Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com> (cherry picked from commit 4ae9dbe52487185ef9ee25f58fabe5025bb2278b) # Conflicts: # app/build.gradle.kts # app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/MangaBackupCreator.kt # app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupChapter.kt # app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupManga.kt # data/src/main/java/tachiyomi/data/chapter/ChapterRepositoryImpl.kt # data/src/main/sqldelight/tachiyomi/migrations/2.sqm # domain/src/main/java/tachiyomi/domain/manga/model/MangaUpdate.kt --- app/build.gradle.kts | 2 +- .../create/creators/MangaBackupCreator.kt | 1 + .../data/backup/models/BackupChapter.kt | 64 ++++++++++--------- .../data/backup/models/BackupManga.kt | 7 +- .../backup/restore/restorers/MangaRestorer.kt | 12 +++- .../tachiyomi/data/database/models/Chapter.kt | 3 + .../data/database/models/ChapterImpl.kt | 2 + .../main/java/exh/eh/EHentaiUpdateHelper.kt | 1 + .../main/java/tachiyomi/data/LibraryQuery.kt | 16 +++-- .../tachiyomi/data/chapter/ChapterMapper.kt | 4 ++ .../data/chapter/ChapterRepositoryImpl.kt | 3 + .../java/tachiyomi/data/manga/MangaMapper.kt | 11 ++++ .../data/manga/MangaRepositoryImpl.kt | 3 + .../sqldelight/tachiyomi/data/chapters.sq | 31 ++++++++- .../main/sqldelight/tachiyomi/data/mangas.sq | 27 ++++++-- .../tachiyomi/data/mangas_categories.sq | 15 ++--- .../sqldelight/tachiyomi/migrations/31.sqm | 46 +++++++++++++ .../tachiyomi/domain/chapter/model/Chapter.kt | 2 + .../domain/chapter/model/ChapterUpdate.kt | 2 + .../tachiyomi/domain/manga/model/Manga.kt | 2 + .../domain/manga/model/MangaUpdate.kt | 2 + 21 files changed, 200 insertions(+), 56 deletions(-) create mode 100644 data/src/main/sqldelight/tachiyomi/migrations/31.sqm diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 694cbf384..6facac8c6 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -26,7 +26,7 @@ android { defaultConfig { applicationId = "app.komikku" - versionCode = 65 + versionCode = 66 versionName = "1.10.5" buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"") diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/MangaBackupCreator.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/MangaBackupCreator.kt index ba7379c9f..395393f3e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/MangaBackupCreator.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/MangaBackupCreator.kt @@ -134,6 +134,7 @@ private fun Manga.toBackupManga(/* SY --> */customMangaInfo: CustomMangaInfo?/* updateStrategy = this.updateStrategy, lastModifiedAt = this.lastModifiedAt, favoriteModifiedAt = this.favoriteModifiedAt, + version = this.version, // SY --> ).also { backupManga -> customMangaInfo?.let { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupChapter.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupChapter.kt index 3bf95c6c0..d729efe16 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupChapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupChapter.kt @@ -4,6 +4,7 @@ import kotlinx.serialization.Serializable import kotlinx.serialization.protobuf.ProtoNumber import tachiyomi.domain.chapter.model.Chapter +@Suppress("MagicNumber") @Serializable data class BackupChapter( // in 1.x some of these values have different names @@ -21,6 +22,7 @@ data class BackupChapter( @ProtoNumber(9) var chapterNumber: Float = 0F, @ProtoNumber(10) var sourceOrder: Long = 0, @ProtoNumber(11) var lastModifiedAt: Long = 0, + @ProtoNumber(12) var version: Long = 0, ) { fun toChapterImpl(): Chapter { return Chapter.create().copy( @@ -35,36 +37,40 @@ data class BackupChapter( dateUpload = this@BackupChapter.dateUpload, sourceOrder = this@BackupChapter.sourceOrder, lastModifiedAt = this@BackupChapter.lastModifiedAt, + version = this@BackupChapter.version, ) } } -val backupChapterMapper = - { _: Long, - _: Long, - url: String, - name: String, - scanlator: String?, - read: Boolean, - bookmark: Boolean, - lastPageRead: Long, - chapterNumber: Double, - source_order: Long, - dateFetch: Long, - dateUpload: Long, - lastModifiedAt: Long, - -> - BackupChapter( - url = url, - name = name, - chapterNumber = chapterNumber.toFloat(), - scanlator = scanlator, - read = read, - bookmark = bookmark, - lastPageRead = lastPageRead, - dateFetch = dateFetch, - dateUpload = dateUpload, - sourceOrder = source_order, - lastModifiedAt = lastModifiedAt, - ) - } +val backupChapterMapper = { + _: Long, + _: Long, + url: String, + name: String, + scanlator: String?, + read: Boolean, + bookmark: Boolean, + lastPageRead: Long, + chapterNumber: Double, + sourceOrder: Long, + dateFetch: Long, + dateUpload: Long, + lastModifiedAt: Long, + version: Long, + _: Long, + -> + BackupChapter( + url = url, + name = name, + chapterNumber = chapterNumber.toFloat(), + scanlator = scanlator, + read = read, + bookmark = bookmark, + lastPageRead = lastPageRead, + dateFetch = dateFetch, + dateUpload = dateUpload, + sourceOrder = sourceOrder, + lastModifiedAt = lastModifiedAt, + version = version, + ) +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupManga.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupManga.kt index e6a4f6948..a62bc942e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupManga.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupManga.kt @@ -6,7 +6,10 @@ import kotlinx.serialization.protobuf.ProtoNumber import tachiyomi.domain.manga.model.CustomMangaInfo import tachiyomi.domain.manga.model.Manga -@Suppress("DEPRECATION") +@Suppress( + "DEPRECATION", + "MagicNumber", +) @Serializable data class BackupManga( // in 1.x some of these values have different names @@ -40,6 +43,7 @@ data class BackupManga( @ProtoNumber(106) var lastModifiedAt: Long = 0, @ProtoNumber(107) var favoriteModifiedAt: Long? = null, @ProtoNumber(108) var excludedScanlators: List = emptyList(), + @ProtoNumber(109) var version: Long = 0, // SY specific values @ProtoNumber(600) var mergedMangaReferences: List = emptyList(), @@ -76,6 +80,7 @@ data class BackupManga( updateStrategy = this@BackupManga.updateStrategy, lastModifiedAt = this@BackupManga.lastModifiedAt, favoriteModifiedAt = this@BackupManga.favoriteModifiedAt, + version = this@BackupManga.version, ) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/MangaRestorer.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/MangaRestorer.kt index 79e5def68..855a59a4e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/MangaRestorer.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/MangaRestorer.kt @@ -105,7 +105,7 @@ class MangaRestorer( } private suspend fun restoreExistingManga(manga: Manga, dbManga: Manga): Manga { - return if (manga.lastModifiedAt > dbManga.lastModifiedAt) { + return if (manga.version > dbManga.version) { updateManga(dbManga.copyFrom(manga).copy(id = dbManga.id)) } else { updateManga(manga.copyFrom(dbManga).copy(id = dbManga.id)) @@ -124,6 +124,7 @@ class MangaRestorer( ogStatus = newer.status, // SY <-- initialized = this.initialized || newer.initialized, + version = newer.version, ) } @@ -150,6 +151,8 @@ class MangaRestorer( dateAdded = manga.dateAdded, mangaId = manga.id, updateStrategy = manga.updateStrategy.let(UpdateStrategyColumnAdapter::encode), + version = manga.version, + isSyncing = 1, ) } return manga @@ -161,6 +164,7 @@ class MangaRestorer( return manga.copy( initialized = manga.description != null, id = insertManga(manga), + version = manga.version, ) } @@ -207,7 +211,7 @@ class MangaRestorer( } private fun Chapter.forComparison() = - this.copy(id = 0L, mangaId = 0L, dateFetch = 0L, dateUpload = 0L, lastModifiedAt = 0L) + this.copy(id = 0L, mangaId = 0L, dateFetch = 0L, dateUpload = 0L, lastModifiedAt = 0L, version = 0L) private suspend fun insertNewChapters(chapters: List) { handler.await(true) { @@ -224,6 +228,7 @@ class MangaRestorer( chapter.sourceOrder, chapter.dateFetch, chapter.dateUpload, + chapter.version, ) } } @@ -245,6 +250,8 @@ class MangaRestorer( dateFetch = null, dateUpload = null, chapterId = chapter.id, + version = chapter.version, + isSyncing = 0, ) } } @@ -277,6 +284,7 @@ class MangaRestorer( coverLastModified = manga.coverLastModified, dateAdded = manga.dateAdded, updateStrategy = manga.updateStrategy, + version = manga.version, ) mangasQueries.selectLastInsertedRowId() } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Chapter.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Chapter.kt index 4ff50483e..f91368084 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Chapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Chapter.kt @@ -21,6 +21,8 @@ interface Chapter : SChapter, Serializable { var source_order: Int var last_modified: Long + + var version: Long } fun Chapter.toDomainChapter(): DomainChapter? { @@ -39,5 +41,6 @@ fun Chapter.toDomainChapter(): DomainChapter? { chapterNumber = chapter_number.toDouble(), scanlator = scanlator, lastModifiedAt = last_modified, + version = version, ) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/ChapterImpl.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/ChapterImpl.kt index 58ba41dec..a92dd56df 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/ChapterImpl.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/ChapterImpl.kt @@ -28,6 +28,8 @@ class ChapterImpl : Chapter { override var last_modified: Long = 0 + override var version: Long = 0 + override fun equals(other: Any?): Boolean { if (this === other) return true if (other == null || javaClass != other.javaClass) return false diff --git a/app/src/main/java/exh/eh/EHentaiUpdateHelper.kt b/app/src/main/java/exh/eh/EHentaiUpdateHelper.kt index dc28a993d..d06566bc7 100644 --- a/app/src/main/java/exh/eh/EHentaiUpdateHelper.kt +++ b/app/src/main/java/exh/eh/EHentaiUpdateHelper.kt @@ -254,6 +254,7 @@ class EHentaiUpdateHelper(context: Context) { scanlator = null, sourceOrder = -1, lastModifiedAt = 0, + version = 0, ) } } diff --git a/data/src/main/java/tachiyomi/data/LibraryQuery.kt b/data/src/main/java/tachiyomi/data/LibraryQuery.kt index b631a2005..64f7b40c4 100644 --- a/data/src/main/java/tachiyomi/data/LibraryQuery.kt +++ b/data/src/main/java/tachiyomi/data/LibraryQuery.kt @@ -32,13 +32,15 @@ private val mapper = { cursor: SqlCursor -> calculate_interval = cursor.getLong(20)!!, last_modified_at = cursor.getLong(21)!!, favorite_modified_at = cursor.getLong(22), - totalCount = cursor.getLong(23)!!, - readCount = cursor.getDouble(24)!!, - latestUpload = cursor.getLong(25)!!, - chapterFetchedAt = cursor.getLong(26)!!, - lastRead = cursor.getLong(27)!!, - bookmarkCount = cursor.getDouble(28)!!, - category = cursor.getLong(29)!!, + version = cursor.getLong(23)!!, + is_syncing = cursor.getLong(24)!!, + totalCount = cursor.getLong(25)!!, + readCount = cursor.getDouble(26)!!, + latestUpload = cursor.getLong(27)!!, + chapterFetchedAt = cursor.getLong(28)!!, + lastRead = cursor.getLong(29)!!, + bookmarkCount = cursor.getDouble(30)!!, + category = cursor.getLong(31)!!, ) } diff --git a/data/src/main/java/tachiyomi/data/chapter/ChapterMapper.kt b/data/src/main/java/tachiyomi/data/chapter/ChapterMapper.kt index f17f2a833..70980f7ae 100644 --- a/data/src/main/java/tachiyomi/data/chapter/ChapterMapper.kt +++ b/data/src/main/java/tachiyomi/data/chapter/ChapterMapper.kt @@ -17,6 +17,9 @@ object ChapterMapper { dateFetch: Long, dateUpload: Long, lastModifiedAt: Long, + version: Long, + @Suppress("UNUSED_PARAMETER") + isSyncing: Long, ): Chapter = Chapter( id = id, mangaId = mangaId, @@ -31,5 +34,6 @@ object ChapterMapper { chapterNumber = chapterNumber, scanlator = scanlator, lastModifiedAt = lastModifiedAt, + version = version ) } diff --git a/data/src/main/java/tachiyomi/data/chapter/ChapterRepositoryImpl.kt b/data/src/main/java/tachiyomi/data/chapter/ChapterRepositoryImpl.kt index f01db7670..133d6a939 100644 --- a/data/src/main/java/tachiyomi/data/chapter/ChapterRepositoryImpl.kt +++ b/data/src/main/java/tachiyomi/data/chapter/ChapterRepositoryImpl.kt @@ -29,6 +29,7 @@ class ChapterRepositoryImpl( chapter.sourceOrder, chapter.dateFetch, chapter.dateUpload, + chapter.version, ) val lastInsertId = chaptersQueries.selectLastInsertedRowId().executeAsOne() chapter.copy(id = lastInsertId) @@ -64,6 +65,8 @@ class ChapterRepositoryImpl( dateFetch = chapterUpdate.dateFetch, dateUpload = chapterUpdate.dateUpload, chapterId = chapterUpdate.id, + version = chapterUpdate.version, + isSyncing = 0, ) } } diff --git a/data/src/main/java/tachiyomi/data/manga/MangaMapper.kt b/data/src/main/java/tachiyomi/data/manga/MangaMapper.kt index b9718f86f..40e4c5313 100644 --- a/data/src/main/java/tachiyomi/data/manga/MangaMapper.kt +++ b/data/src/main/java/tachiyomi/data/manga/MangaMapper.kt @@ -6,6 +6,7 @@ import tachiyomi.domain.manga.model.Manga import tachiyomi.view.LibraryView object MangaMapper { + @Suppress("LongParameterList") fun mapManga( id: Long, source: Long, @@ -33,6 +34,9 @@ object MangaMapper { calculateInterval: Long, lastModifiedAt: Long, favoriteModifiedAt: Long?, + version: Long, + @Suppress("UNUSED_PARAMETER") + isSyncing: Long, ): Manga = Manga( id = id, source = source, @@ -58,8 +62,10 @@ object MangaMapper { initialized = initialized, lastModifiedAt = lastModifiedAt, favoriteModifiedAt = favoriteModifiedAt, + version = version, ) + @Suppress("LongParameterList") fun mapLibraryManga( id: Long, source: Long, @@ -87,6 +93,8 @@ object MangaMapper { calculateInterval: Long, lastModifiedAt: Long, favoriteModifiedAt: Long?, + version: Long, + isSyncing: Long, totalCount: Long, readCount: Double, latestUpload: Long, @@ -121,6 +129,8 @@ object MangaMapper { calculateInterval, lastModifiedAt, favoriteModifiedAt, + version, + isSyncing, ), category = category, totalChapters = totalCount, @@ -156,6 +166,7 @@ object MangaMapper { fetchInterval = libraryView.calculate_interval.toInt(), lastModifiedAt = libraryView.last_modified_at, favoriteModifiedAt = libraryView.favorite_modified_at, + version = libraryView.version, ), category = libraryView.category, totalChapters = libraryView.totalCount, diff --git a/data/src/main/java/tachiyomi/data/manga/MangaRepositoryImpl.kt b/data/src/main/java/tachiyomi/data/manga/MangaRepositoryImpl.kt index 46d7eb785..d44487323 100644 --- a/data/src/main/java/tachiyomi/data/manga/MangaRepositoryImpl.kt +++ b/data/src/main/java/tachiyomi/data/manga/MangaRepositoryImpl.kt @@ -119,6 +119,7 @@ class MangaRepositoryImpl( coverLastModified = manga.coverLastModified, dateAdded = manga.dateAdded, updateStrategy = manga.updateStrategy, + version = manga.version, ) mangasQueries.selectLastInsertedRowId() } @@ -168,6 +169,8 @@ class MangaRepositoryImpl( dateAdded = value.dateAdded, mangaId = value.id, updateStrategy = value.updateStrategy?.let(UpdateStrategyColumnAdapter::encode), + version = value.version, + isSyncing = 0, ) } } diff --git a/data/src/main/sqldelight/tachiyomi/data/chapters.sq b/data/src/main/sqldelight/tachiyomi/data/chapters.sq index 8202c3b67..cc9a979dd 100644 --- a/data/src/main/sqldelight/tachiyomi/data/chapters.sq +++ b/data/src/main/sqldelight/tachiyomi/data/chapters.sq @@ -14,6 +14,8 @@ CREATE TABLE chapters( date_fetch INTEGER NOT NULL, date_upload INTEGER NOT NULL, last_modified_at INTEGER NOT NULL DEFAULT 0, + version INTEGER NOT NULL DEFAULT 0, + is_syncing INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(manga_id) REFERENCES mangas (_id) ON DELETE CASCADE ); @@ -30,6 +32,22 @@ BEGIN WHERE _id = new._id; END; +CREATE TRIGGER update_chapter_and_manga_version AFTER UPDATE ON chapters +WHEN new.is_syncing = 0 AND ( + new.read != old.read OR + new.bookmark != old.bookmark OR + new.last_page_read != old.last_page_read +) +BEGIN + -- Update the chapter version + UPDATE chapters SET version = version + 1 + WHERE _id = new._id; + + -- Update the manga version + UPDATE mangas SET version = version + 1 + WHERE _id = new.manga_id AND (SELECT is_syncing FROM mangas WHERE _id = new.manga_id) = 0; +END; + getChapterById: SELECT * FROM chapters @@ -97,9 +115,14 @@ removeChaptersWithIds: DELETE FROM chapters WHERE _id IN :chapterIds; +resetIsSyncing: +UPDATE chapters +SET is_syncing = 0 +WHERE is_syncing = 1; + insert: -INSERT INTO chapters(manga_id, url, name, scanlator, read, bookmark, last_page_read, chapter_number, source_order, date_fetch, date_upload, last_modified_at) -VALUES (:mangaId, :url, :name, :scanlator, :read, :bookmark, :lastPageRead, :chapterNumber, :sourceOrder, :dateFetch, :dateUpload, 0); +INSERT INTO chapters(manga_id, url, name, scanlator, read, bookmark, last_page_read, chapter_number, source_order, date_fetch, date_upload, last_modified_at, version, is_syncing) +VALUES (:mangaId, :url, :name, :scanlator, :read, :bookmark, :lastPageRead, :chapterNumber, :sourceOrder, :dateFetch, :dateUpload, 0, :version, 0); update: UPDATE chapters @@ -113,7 +136,9 @@ SET manga_id = coalesce(:mangaId, manga_id), chapter_number = coalesce(:chapterNumber, chapter_number), source_order = coalesce(:sourceOrder, source_order), date_fetch = coalesce(:dateFetch, date_fetch), - date_upload = coalesce(:dateUpload, date_upload) + date_upload = coalesce(:dateUpload, date_upload), + version = coalesce(:version, version), + is_syncing = coalesce(:isSyncing, is_syncing) WHERE _id = :chapterId; selectLastInsertedRowId: diff --git a/data/src/main/sqldelight/tachiyomi/data/mangas.sq b/data/src/main/sqldelight/tachiyomi/data/mangas.sq index 5bc9de175..9de2174d1 100644 --- a/data/src/main/sqldelight/tachiyomi/data/mangas.sq +++ b/data/src/main/sqldelight/tachiyomi/data/mangas.sq @@ -26,7 +26,9 @@ CREATE TABLE mangas( update_strategy INTEGER AS UpdateStrategy NOT NULL DEFAULT 0, calculate_interval INTEGER DEFAULT 0 NOT NULL, last_modified_at INTEGER NOT NULL DEFAULT 0, - favorite_modified_at INTEGER + favorite_modified_at INTEGER, + version INTEGER NOT NULL DEFAULT 0, + is_syncing INTEGER NOT NULL DEFAULT 0 ); CREATE INDEX library_favorite_index ON mangas(favorite) WHERE favorite = 1; @@ -49,6 +51,16 @@ BEGIN WHERE _id = new._id; END; +CREATE TRIGGER update_manga_version AFTER UPDATE ON mangas +BEGIN + UPDATE mangas SET version = version + 1 + WHERE _id = new._id AND new.is_syncing = 0 AND ( + new.url != old.url OR + new.description != old.description OR + new.favorite != old.favorite + ); +END; + getMangaById: SELECT * FROM mangas @@ -112,6 +124,11 @@ resetViewerFlags: UPDATE mangas SET viewer = 0; +resetIsSyncing: +UPDATE mangas +SET is_syncing = 0 +WHERE is_syncing = 1; + getSourceIdsWithNonLibraryManga: SELECT source, COUNT(*) AS manga_count FROM mangas @@ -135,8 +152,8 @@ WHERE favorite = 0 AND source IN :sourceIdsAND AND _id NOT IN ( ); insert: -INSERT INTO mangas(source, url, artist, author, description, genre, title, status, thumbnail_url, favorite, last_update, next_update, initialized, viewer, chapter_flags, cover_last_modified, date_added, update_strategy, calculate_interval, last_modified_at) -VALUES (:source, :url, :artist, :author, :description, :genre, :title, :status, :thumbnailUrl, :favorite, :lastUpdate, :nextUpdate, :initialized, :viewerFlags, :chapterFlags, :coverLastModified, :dateAdded, :updateStrategy, :calculateInterval, 0); +INSERT INTO mangas(source, url, artist, author, description, genre, title, status, thumbnail_url, favorite, last_update, next_update, initialized, viewer, chapter_flags, cover_last_modified, date_added, update_strategy, calculate_interval, last_modified_at, version) +VALUES (:source, :url, :artist, :author, :description, :genre, :title, :status, :thumbnailUrl, :favorite, :lastUpdate, :nextUpdate, :initialized, :viewerFlags, :chapterFlags, :coverLastModified, :dateAdded, :updateStrategy, :calculateInterval, 0, :version); update: UPDATE mangas SET @@ -158,7 +175,9 @@ UPDATE mangas SET cover_last_modified = coalesce(:coverLastModified, cover_last_modified), date_added = coalesce(:dateAdded, date_added), update_strategy = coalesce(:updateStrategy, update_strategy), - calculate_interval = coalesce(:calculateInterval, calculate_interval) + calculate_interval = coalesce(:calculateInterval, calculate_interval), + version = coalesce(:version, version), + is_syncing = coalesce(:isSyncing, is_syncing) WHERE _id = :mangaId; selectLastInsertedRowId: diff --git a/data/src/main/sqldelight/tachiyomi/data/mangas_categories.sq b/data/src/main/sqldelight/tachiyomi/data/mangas_categories.sq index b908e3f86..3d8c815c9 100644 --- a/data/src/main/sqldelight/tachiyomi/data/mangas_categories.sq +++ b/data/src/main/sqldelight/tachiyomi/data/mangas_categories.sq @@ -2,25 +2,22 @@ CREATE TABLE mangas_categories( _id INTEGER NOT NULL PRIMARY KEY, manga_id INTEGER NOT NULL, category_id INTEGER NOT NULL, - last_modified_at INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(category_id) REFERENCES categories (_id) ON DELETE CASCADE, FOREIGN KEY(manga_id) REFERENCES mangas (_id) ON DELETE CASCADE ); -CREATE TRIGGER update_last_modified_at_mangas_categories -AFTER UPDATE ON mangas_categories -FOR EACH ROW +CREATE TRIGGER insert_manga_category_update_version AFTER INSERT ON mangas_categories BEGIN - UPDATE mangas_categories - SET last_modified_at = strftime('%s', 'now') - WHERE _id = new._id; + UPDATE mangas + SET version = version + 1 + WHERE _id = new.manga_id AND (SELECT is_syncing FROM mangas WHERE _id = new.manga_id) = 0; END; insert: -INSERT INTO mangas_categories(manga_id, category_id, last_modified_at) -VALUES (:mangaId, :categoryId, 0); +INSERT INTO mangas_categories(manga_id, category_id) +VALUES (:mangaId, :categoryId); deleteMangaCategoryByMangaId: DELETE FROM mangas_categories diff --git a/data/src/main/sqldelight/tachiyomi/migrations/31.sqm b/data/src/main/sqldelight/tachiyomi/migrations/31.sqm new file mode 100644 index 000000000..1a05f72e4 --- /dev/null +++ b/data/src/main/sqldelight/tachiyomi/migrations/31.sqm @@ -0,0 +1,46 @@ +-- Mangas table +ALTER TABLE mangas ADD COLUMN version INTEGER NOT NULL DEFAULT 0; +ALTER TABLE mangas ADD COLUMN is_syncing INTEGER NOT NULL DEFAULT 0; + +-- Chapters table +ALTER TABLE chapters ADD COLUMN version INTEGER NOT NULL DEFAULT 0; +ALTER TABLE chapters ADD COLUMN is_syncing INTEGER NOT NULL DEFAULT 0; + +-- Mangas triggers +DROP TRIGGER IF EXISTS update_manga_version; +CREATE TRIGGER update_manga_version AFTER UPDATE ON mangas +BEGIN + UPDATE mangas SET version = version + 1 + WHERE _id = new._id AND new.is_syncing = 0 AND ( + new.url != old.url OR + new.description != old.description OR + new.favorite != old.favorite + ); +END; + +-- Chapters triggers +DROP TRIGGER IF EXISTS update_chapter_and_manga_version; +CREATE TRIGGER update_chapter_and_manga_version AFTER UPDATE ON chapters +WHEN new.is_syncing = 0 AND ( + new.read != old.read OR + new.bookmark != old.bookmark OR + new.last_page_read != old.last_page_read +) +BEGIN + -- Update the chapter version + UPDATE chapters SET version = version + 1 + WHERE _id = new._id; + + -- Update the manga version + UPDATE mangas SET version = version + 1 + WHERE _id = new.manga_id AND (SELECT is_syncing FROM mangas WHERE _id = new.manga_id) = 0; +END; + +-- manga_categories table +DROP TRIGGER IF EXISTS insert_manga_category_update_version; +CREATE TRIGGER insert_manga_category_update_version AFTER INSERT ON mangas_categories +BEGIN + UPDATE mangas + SET version = version + 1 + WHERE _id = new.manga_id AND (SELECT is_syncing FROM mangas WHERE _id = new.manga_id) = 0; +END; diff --git a/domain/src/main/java/tachiyomi/domain/chapter/model/Chapter.kt b/domain/src/main/java/tachiyomi/domain/chapter/model/Chapter.kt index 3a4a8c4a4..f993e0256 100644 --- a/domain/src/main/java/tachiyomi/domain/chapter/model/Chapter.kt +++ b/domain/src/main/java/tachiyomi/domain/chapter/model/Chapter.kt @@ -14,6 +14,7 @@ data class Chapter( val chapterNumber: Double, val scanlator: String?, val lastModifiedAt: Long, + val version: Long, ) { val isRecognizedNumber: Boolean get() = chapterNumber >= 0f @@ -43,6 +44,7 @@ data class Chapter( chapterNumber = -1.0, scanlator = null, lastModifiedAt = 0, + version = 1, ) } } diff --git a/domain/src/main/java/tachiyomi/domain/chapter/model/ChapterUpdate.kt b/domain/src/main/java/tachiyomi/domain/chapter/model/ChapterUpdate.kt index 33d1d4fba..5a9193dc6 100644 --- a/domain/src/main/java/tachiyomi/domain/chapter/model/ChapterUpdate.kt +++ b/domain/src/main/java/tachiyomi/domain/chapter/model/ChapterUpdate.kt @@ -13,6 +13,7 @@ data class ChapterUpdate( val dateUpload: Long? = null, val chapterNumber: Double? = null, val scanlator: String? = null, + val version: Long? = null, ) fun Chapter.toChapterUpdate(): ChapterUpdate { @@ -29,5 +30,6 @@ fun Chapter.toChapterUpdate(): ChapterUpdate { dateUpload, chapterNumber, scanlator, + version, ) } diff --git a/domain/src/main/java/tachiyomi/domain/manga/model/Manga.kt b/domain/src/main/java/tachiyomi/domain/manga/model/Manga.kt index 23ae9ebb9..4f41b18d6 100644 --- a/domain/src/main/java/tachiyomi/domain/manga/model/Manga.kt +++ b/domain/src/main/java/tachiyomi/domain/manga/model/Manga.kt @@ -33,6 +33,7 @@ data class Manga( val initialized: Boolean, val lastModifiedAt: Long, val favoriteModifiedAt: Long?, + val version: Long, ) : Serializable { // SY --> @@ -159,6 +160,7 @@ data class Manga( initialized = false, lastModifiedAt = 0L, favoriteModifiedAt = null, + version = 0L, ) // SY --> diff --git a/domain/src/main/java/tachiyomi/domain/manga/model/MangaUpdate.kt b/domain/src/main/java/tachiyomi/domain/manga/model/MangaUpdate.kt index e2d1c803b..7fa5ce4ad 100644 --- a/domain/src/main/java/tachiyomi/domain/manga/model/MangaUpdate.kt +++ b/domain/src/main/java/tachiyomi/domain/manga/model/MangaUpdate.kt @@ -23,6 +23,7 @@ data class MangaUpdate( val thumbnailUrl: String? = null, val updateStrategy: UpdateStrategy? = null, val initialized: Boolean? = null, + val version: Long? = null, // SY --> val filteredScanlators: List? = null, // SY <-- @@ -52,5 +53,6 @@ fun Manga.toMangaUpdate(): MangaUpdate { // SY <-- updateStrategy = updateStrategy, initialized = initialized, + version = version, ) } From b261b75c5aebdbbfeb477f3446071b687f0e872a Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Mon, 11 Mar 2024 12:51:54 +0100 Subject: [PATCH 010/106] Translations update from Hosted Weblate (#445) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Translated using Weblate (Esperanto) Currently translated at 65.1% (517 of 794 strings) Translation: Mihon/Mihon Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/eo/ * Translated using Weblate (Spanish) Currently translated at 100.0% (794 of 794 strings) Translation: Mihon/Mihon Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/es/ * Translated using Weblate (Croatian) Currently translated at 100.0% (794 of 794 strings) Translation: Mihon/Mihon Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/hr/ * Translated using Weblate (Serbian) Currently translated at 100.0% (794 of 794 strings) Translation: Mihon/Mihon Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/sr/ * Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (794 of 794 strings) Translation: Mihon/Mihon Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/zh_Hans/ * Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (794 of 794 strings) Translation: Mihon/Mihon Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/zh_Hant/ * Translated using Weblate (Spanish) Currently translated at 100.0% (18 of 18 strings) Translation: Mihon/Mihon Plurals Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/es/ * Translated using Weblate (Croatian) Currently translated at 100.0% (18 of 18 strings) Translation: Mihon/Mihon Plurals Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/hr/ * Translated using Weblate (Serbian) Currently translated at 100.0% (18 of 18 strings) Translation: Mihon/Mihon Plurals Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/sr/ * Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (18 of 18 strings) Translation: Mihon/Mihon Plurals Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/zh_Hans/ * Translated using Weblate (Filipino) Currently translated at 100.0% (794 of 794 strings) Translation: Mihon/Mihon Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/fil/ * Translated using Weblate (Filipino) Currently translated at 100.0% (18 of 18 strings) Translation: Mihon/Mihon Plurals Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/fil/ * Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (18 of 18 strings) Translation: Mihon/Mihon Plurals Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/zh_Hant/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (794 of 794 strings) Translation: Mihon/Mihon Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/hu/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (18 of 18 strings) Translation: Mihon/Mihon Plurals Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/hu/ * Translated using Weblate (Nepali) Currently translated at 100.0% (794 of 794 strings) Translation: Mihon/Mihon Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ne/ * Translated using Weblate (Polish) Currently translated at 100.0% (794 of 794 strings) Translation: Mihon/Mihon Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/pl/ * Translated using Weblate (Polish) Currently translated at 100.0% (18 of 18 strings) Translation: Mihon/Mihon Plurals Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/pl/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (794 of 794 strings) Translation: Mihon/Mihon Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/hu/ * Translated using Weblate (Russian) Currently translated at 100.0% (794 of 794 strings) Translation: Mihon/Mihon Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ru/ * Translated using Weblate (Russian) Currently translated at 100.0% (18 of 18 strings) Translation: Mihon/Mihon Plurals Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/ru/ * Translated using Weblate (Indonesian) Currently translated at 100.0% (794 of 794 strings) Translation: Mihon/Mihon Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/id/ * Translated using Weblate (Japanese) Currently translated at 100.0% (794 of 794 strings) Translation: Mihon/Mihon Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ja/ * Translated using Weblate (Indonesian) Currently translated at 100.0% (18 of 18 strings) Translation: Mihon/Mihon Plurals Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/id/ * Translated using Weblate (Dutch) Currently translated at 94.2% (748 of 794 strings) Translation: Mihon/Mihon Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/nl/ * Translated using Weblate (Dutch) Currently translated at 94.4% (17 of 18 strings) Translation: Mihon/Mihon Plurals Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/nl/ * Translated using Weblate (German) Currently translated at 100.0% (794 of 794 strings) Translation: Mihon/Mihon Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/de/ --------- Co-authored-by: Radoŝ Porka Co-authored-by: gallegonovato Co-authored-by: Milo Ivir Co-authored-by: David Katrinka Co-authored-by: gekka <1778962971@qq.com> Co-authored-by: ɴᴇᴋᴏ Co-authored-by: Infy's Tagalog Translations Co-authored-by: Lzmxya Co-authored-by: B4LiN7 Co-authored-by: FateXBlood Co-authored-by: Eryk Michalak Co-authored-by: Dexroneum Co-authored-by: TheKingTermux Co-authored-by: Tim Bolhoeve Co-authored-by: Lyfja <45209212+lyfja@users.noreply.github.com> (cherry picked from commit edd7d0522c305a8aec8ab524214d3a26106dac31) --- .../commonMain/resources/MR/de/strings.xml | 1 + .../commonMain/resources/MR/eo/strings.xml | 12 +++ .../commonMain/resources/MR/es/plurals.xml | 5 + .../commonMain/resources/MR/es/strings.xml | 11 +- .../commonMain/resources/MR/fil/plurals.xml | 6 +- .../commonMain/resources/MR/fil/strings.xml | 7 +- .../commonMain/resources/MR/hr/plurals.xml | 5 + .../commonMain/resources/MR/hr/strings.xml | 3 +- .../commonMain/resources/MR/hu/plurals.xml | 16 ++- .../commonMain/resources/MR/hu/strings.xml | 100 +++++++++++------- .../commonMain/resources/MR/in/plurals.xml | 5 +- .../commonMain/resources/MR/in/strings.xml | 1 + .../commonMain/resources/MR/ja/strings.xml | 1 + .../commonMain/resources/MR/ne/strings.xml | 1 + .../commonMain/resources/MR/nl/plurals.xml | 4 + .../commonMain/resources/MR/nl/strings.xml | 9 ++ .../commonMain/resources/MR/pl/plurals.xml | 6 ++ .../commonMain/resources/MR/pl/strings.xml | 1 + .../commonMain/resources/MR/ru/plurals.xml | 6 ++ .../commonMain/resources/MR/ru/strings.xml | 1 + .../commonMain/resources/MR/sr/plurals.xml | 11 +- .../commonMain/resources/MR/sr/strings.xml | 60 +++++++---- .../resources/MR/zh-rCN/plurals.xml | 3 + .../resources/MR/zh-rCN/strings.xml | 1 + .../resources/MR/zh-rTW/plurals.xml | 3 + .../resources/MR/zh-rTW/strings.xml | 2 +- 26 files changed, 200 insertions(+), 81 deletions(-) diff --git a/i18n/src/commonMain/resources/MR/de/strings.xml b/i18n/src/commonMain/resources/MR/de/strings.xml index 21a0e82b1..193718065 100644 --- a/i18n/src/commonMain/resources/MR/de/strings.xml +++ b/i18n/src/commonMain/resources/MR/de/strings.xml @@ -798,4 +798,5 @@ Open-Source-Repository Bald Zertifikatsbestätigungen zurücknehmen + Rauszoomen deaktivieren \ No newline at end of file diff --git a/i18n/src/commonMain/resources/MR/eo/strings.xml b/i18n/src/commonMain/resources/MR/eo/strings.xml index 57643acd9..991249e0e 100644 --- a/i18n/src/commonMain/resources/MR/eo/strings.xml +++ b/i18n/src/commonMain/resources/MR/eo/strings.xml @@ -505,4 +505,16 @@ Lingvo Instalilo Malmoderna + Libera: %1$s / Tuta: %2$s + Popularaj + Havantaj rezultoj + Nekonata titolo + Sen priskribo + Baldaŭ + Ĉu vi certas? + Ĵus + Neniam + %1$s eraro: %2$s + Neniu fonto trovita + Ups! \ No newline at end of file diff --git a/i18n/src/commonMain/resources/MR/es/plurals.xml b/i18n/src/commonMain/resources/MR/es/plurals.xml index 31c76cb44..08f01feea 100644 --- a/i18n/src/commonMain/resources/MR/es/plurals.xml +++ b/i18n/src/commonMain/resources/MR/es/plurals.xml @@ -85,4 +85,9 @@ %d repositorios %d repositorios + + Mañana + Dentro de %1$d días + Dentro de %1$d días + \ No newline at end of file diff --git a/i18n/src/commonMain/resources/MR/es/strings.xml b/i18n/src/commonMain/resources/MR/es/strings.xml index 2d3777790..3763777c8 100644 --- a/i18n/src/commonMain/resources/MR/es/strings.xml +++ b/i18n/src/commonMain/resources/MR/es/strings.xml @@ -214,7 +214,7 @@ ¿Quieres borrar los capítulos descargados? Pausado Seguimiento - Ya existe una categoría con este nombre + ¡Ya existe una categoría con este nombre! Categorías eliminadas ¿Añadir manga a la biblioteca\? Imagen guardada @@ -758,15 +758,15 @@ ¿Es la primera vez que instalas %s? Te recomendamos leer la guía de introducción. Comenzar Tienes que elegir una carpeta - Te damos la bienvenida + ¡Bienvenid@s! ¿No es la primera vez que instalas %s? Saltar Siguiente Lo primero de todo es dejar las cosas a tu gusto. Siempre puedes volver a cambiarlas más tarde en los ajustes. Todavía no has proporcionado ninguna carpeta - Selecciona una carpeta donde %1$s guardará los capítulos descargados, las copias de seguridad y otras cosas. + Seleccione una carpeta donde %1$s almacenará las descargas de capítulos, copias de seguridad, etc. \n -\nTe recomendamos que sea solo para %1$s. +\nSe recomienda una carpeta dedicada. \n \nCarpeta seleccionada: %2$s Permiso para instalar aplicaciones @@ -782,7 +782,7 @@ Descripción completa del problema: Se espera que el siguiente número salga en aproximadamente %1$s, la aplicación busca actualizaciones cada %2$s. Frecuencia de actualización personalizada: - El repositorio ya existe + ¡Este repositorio ya existe! Actualizaciones inteligentes La dirección URL del repositorio no parece ser correcta Añade más repositorios a Komikku; la dirección URL tiene que terminar en «index.min.json». @@ -798,4 +798,5 @@ Pronto Dejar de marcar todas la extensiones desconocidas como de confianza Abrir repositorio + Desactivar alejar \ No newline at end of file diff --git a/i18n/src/commonMain/resources/MR/fil/plurals.xml b/i18n/src/commonMain/resources/MR/fil/plurals.xml index 04755d125..4d10091a1 100644 --- a/i18n/src/commonMain/resources/MR/fil/plurals.xml +++ b/i18n/src/commonMain/resources/MR/fil/plurals.xml @@ -68,4 +68,8 @@ %d na repo %d na mga repo - + + Bukas + Sa loob ng %1$d araw + + \ No newline at end of file diff --git a/i18n/src/commonMain/resources/MR/fil/strings.xml b/i18n/src/commonMain/resources/MR/fil/strings.xml index d62696591..358de739f 100644 --- a/i18n/src/commonMain/resources/MR/fil/strings.xml +++ b/i18n/src/commonMain/resources/MR/fil/strings.xml @@ -184,8 +184,8 @@ \nSa pamamagitan ng pagtitiwala sa extension na ito, tinatanggap mo ang mga panganib na ito. Di-pinagkakatiwalaang extension I-uninstall - Tiwala - Hindi pinagkakatiwalaan + Itiwala + Di pinagkakatiwalaan Naka-install Ini-install Dina-download @@ -798,4 +798,5 @@ Malapit na Bawiin ang mga pinagkakatiwalaang hindi kilalang extension Open source na repo - + Patayin ang pag-zoom out + \ No newline at end of file diff --git a/i18n/src/commonMain/resources/MR/hr/plurals.xml b/i18n/src/commonMain/resources/MR/hr/plurals.xml index a404dab79..1bddc175f 100644 --- a/i18n/src/commonMain/resources/MR/hr/plurals.xml +++ b/i18n/src/commonMain/resources/MR/hr/plurals.xml @@ -85,4 +85,9 @@ %d repozitorija %d repozitorija + + Sutra + Za %1$d dana + Za %1$d dana + \ No newline at end of file diff --git a/i18n/src/commonMain/resources/MR/hr/strings.xml b/i18n/src/commonMain/resources/MR/hr/strings.xml index 2070572d0..560477070 100644 --- a/i18n/src/commonMain/resources/MR/hr/strings.xml +++ b/i18n/src/commonMain/resources/MR/hr/strings.xml @@ -511,7 +511,7 @@ Invertirano Danas Potpuno crna tamna tema - Strawberry Daiquiri + Daiquiri jagoda Izgled Vodič za pokretanje Ovjeri za potvrditi promjenu @@ -798,4 +798,5 @@ Predviđa se da će nova poglavlja biti objavljena za oko %1$s, provjera svakih %2$s. Uskoro Dostupno: %1$s / Ukupno: %2$s + Deaktiviraj smanjivanje zumiranja \ No newline at end of file diff --git a/i18n/src/commonMain/resources/MR/hu/plurals.xml b/i18n/src/commonMain/resources/MR/hu/plurals.xml index 469179e28..ee1f7b9a8 100644 --- a/i18n/src/commonMain/resources/MR/hu/plurals.xml +++ b/i18n/src/commonMain/resources/MR/hu/plurals.xml @@ -25,12 +25,12 @@ %1$d napja - %d tracker - %d tracker + %d nyilvántartó + %d nyilvántartó - %d-nak/nek - %d-nak/nek + %d elemnek + %d elemeknek %d fejezet kihagyása, hiányzik a forrás, vagy ki lett szűrve @@ -64,4 +64,12 @@ 1 nap %d nap + + Holnap + %1$d nap múlva + + + %d repo + %d repok + \ No newline at end of file diff --git a/i18n/src/commonMain/resources/MR/hu/strings.xml b/i18n/src/commonMain/resources/MR/hu/strings.xml index 95dd76704..b86f87a67 100644 --- a/i18n/src/commonMain/resources/MR/hu/strings.xml +++ b/i18n/src/commonMain/resources/MR/hu/strings.xml @@ -4,7 +4,7 @@ Kategóriák Könyvtár bejegyzések Fejezetek - Követés + Nyilvántartás Előzmények Beállítások Előzmények @@ -40,7 +40,7 @@ Összes Frissítések korlátozása Töltés közben - Csak a jelenleg is folyamatban lévőket frissítse + \"Befejezett\" állapotú elemek kihagyása Alapértelmezett kategória Mindig kérdezzen rá Teljes képernyő @@ -76,8 +76,8 @@ Olvasatlannak jelölés Előző olvasottnak jelölése Letöltés - A fejezet könyvjelzőzése - A fejezet eltávolítása a könyvjelzők közül + Fejezet könyvjelzőzése + Fejezet eltávolítása a könyvjelzők közül Törlés Könyvtár frissítése Szerkesztés @@ -266,16 +266,14 @@ Felhasználónév Sütik törölve A változtatások érvénybe lépéséhez újra kell indítania az applikációt - Érvénytelen backup fájl + Érvénytelen biztonsági mentési fájl: Él Könyvtár frissítése Nem található fejezet Licenszelt - Ezt a kiterjesztést nem megbízható tanúsítvánnyal írták alá, és nem lett aktiválva. + Egy rosszindulatú bővítmény hozzáférhet a tárolt bejelentkezési adatokhoz, vagy tetszőleges kódot futtathat. \n -\nEgy rosszindulatú kiterjesztés elolvashatja a Komikkuban tárolt bejelentkezési adatokat, vagy tetszőleges kódot futtathat. -\n -\nAz ebben a tanúsítványban való megbízással, elfogadja ezeket a kockázatokat. +\nHa megbízik ebben a bővítményben, elfogadja ezeket a kockázatokat. Bizalom Tiltott: %s Kedvelt: %s @@ -324,16 +322,16 @@ Kinézet Letöltés elkezdése most Helyi forrás - Követve + Nyilvántartva Hitelesítsd magad a változtatás elfogadásához Alapértelmezett Szolgáltatások - Útmutató a követéshez + Nyilvántartási útmutató Új fejezetek letöltése Automatikus letöltés Hátulról az ötödik fejezet Hátulról a negyedik befejezett fejezet - Utolsó előtti előtti befejezett fejezet + Harmadikként olvasott fejezet Utolsó előtti befejezett fejezet Kikapcsolva Tiltott kategóriák @@ -407,7 +405,7 @@ Figyelmeztetés A Shizuku nem fut Régi - Egyirányú szinkronizálás, hogy a fejezetben való előrehaladás frissítve legyen a követési szolgáltatásokban. Állíts be követést egyéni bejegyzésekre, a követési gomb lenyomásával. + Egyirányú szinkronizálás, hogy a fejezetben való előrehaladás frissítve legyen a követési szolgáltatásokban. Állíts be követést egyéni elemekre, a követés gomb lenyomásával. 3 naponta Csak Wi-Fi-n keresztül Ajánlott az automatikus biztonsági mentése. Ezen kívül máshol is legyenek másolatok. @@ -428,10 +426,10 @@ Globális keresés… Feltöltési dátum szerint Ez eltávolítja a fejezet olvasási dátumát. Biztos benne\? - Csak a befejezett fejezeteket() + Olvasatlan fejezet(ek)et tartalmazó elemek kihagyása Törölni akarja a %s kategóriát \? InternalError: Nézze meg a hibaüzenetet további információért - Csak nem-lemért hálózatokon + Csak korlátlan hálózatokon Mit tartalmazzon a biztonsági mentés\? Biztonsági mentés/helyreállítás nem biztos,hogy működik ha a MIUI Optimalizáció ki van kapcsolva. Helyreállítás folyamatban van @@ -457,16 +455,14 @@ Automatikusan nagyítsa a széles képeket Helyreállítás befejeződött %02d perc, %02d másodperc - Adatok visszaállítása a biztonsági mentésből. -\n -\nEzek után telepíteni kell a hiányzó bővítményeket és be kell jelentkezni a tracking szolgáltatásokba, hogy újra használhassa őket. + Előfordulhat, hogy telepítenie kell a hiányzó bővítményeket, majd később be kell jelentkeznie a nyilvántartási szolgáltatásokba a használatukhoz. Könyvtár fedők frissítése Hibaüzenetetek törlése Sorozat beállításainak visszaállítása Néhány gyártónak extra korlátozása van arra, hogy kikapcsolja a háttér folyamatokat. Ezen a web oldalon több információt találsz, hogy hogyan oldható meg. Olvasási előzmények megállítása Fedlap - Tartva + Szüneteltetve Kategóriák törölve Válasszon képet a fedlapnak Nem található új frissítés @@ -479,7 +475,7 @@ A forrás nem található Utolsó frissítés megtekintése Olvasatlanok száma - Bejegyzés mutatása + Elem mutatása Kizárólag fedlapok mutatása Hiányzó források: DNS a HTTPS mellett (DoH) @@ -502,14 +498,14 @@ Fej. %1$s - %2$s Kezdési útmutató Adatbázis törlése - Még nem lett elindítva + El nem kezdett elemek kihagyása Új fejezet Biztonsági mentés már folyamatban van Nincs telepítve Figyelmeztetés: a tömeges letöltések a források lelassulásához és/vagy a Komikku leállásához vezethetnek. Koppintson további információért. Koppintson további információért Nem sikerült a fedlap frissítése - Először adja a mangát a könyvtárhoz + Először adja az elemet a könyvtárhoz Átugorva, mivel tartalmaz olvasatlan fejezeteket Átugorva, mivel nincs olvasott fejezet Új hivatalos verzió érhető el. Koppintson további információért, hogy hogyan importáljon nem hivatalos F-Droid verziókról. @@ -530,7 +526,7 @@ Befejezett Nincs leírás Letöltés (%1$d/%2$d) - Tracking hozzáadása + Felvétel nyilvántartáshoz Elkezdett Válaszon beletartozó adatokat Fejezet beállításai frissültek @@ -544,11 +540,11 @@ Olvasó mód Befejezve: Jelenlegi: - Hát ez kínos... + Hát, ez kínos A nagy frissítések kárt okoznak a forrásoknak, és lassabb frissítésekhez, valamint megnövekedett akkumulátorhasználathoz vezethetnek. Koppintson további információért. %1$d frissítés sikertelen - Növeli a teljesítményt a nagy képek elválasztásával. - Nem bejelentkezett trackerek: + Javítja az olvasó teljesítményét + Nem bejelentkezett nyilvántartó: Inkognitó mód Inkognitó mód kikapcsolása Szűri a könyvtár összes tartalmát @@ -567,9 +563,9 @@ A forrás nem támogatott Nincs egyezés Ilyen nevű kategória már létezik! - Mangában lévő összes fejezet visszaállítása + Összes fejezet visszaállítása ebben az elemben Hiba a kép mentése közben - Tartva lista + Szüneteltettek listája Egyéni fedlap Következő: Előző: @@ -577,13 +573,13 @@ Nincs előző fejezet %1$s oldalt nem lehetett betölteni Oldalak betöltése… - Követ + Nyilvántart Nem sikerült a kiterjesztési lista megnyitása Kikapcsolva - Szolgáltatások, melyek fejlett funkciókat kínálnak. A bejegyzés automatikusan könyvtárhoz lesz adva és frissítve lesz. + Szolgáltatások, amelyek fejlett funkciókat kínálnak. Az elemek automatikusan könyvtárhoz lesznek adva és frissítve lesznek. Biztonsági mentés készítése Felhasználható az aktuális könyvtár visszaállítására - Biztonsági mentés visszaállítása + Biztonsági mentésből visszaállítás Könyvtár visszaállítása biztonsági mentésről Biztonsági mentések gyakorisága Biztonsági mentés helyreállítása @@ -632,9 +628,9 @@ Összes Olvasott Átlagos értékelés - Véletlenszerű manga megnyitása + Véletlenszerű elem megnyitása Könyvtár utoljára frissítve: %s - Követők + Nyilvántartók Az olvasatlanok számának megjelenítése a Frissítések ikonon Olvasás folytatása gomb Kategóriák, globális frissítés, fejezet csúsztatás @@ -654,10 +650,10 @@ Másolás a vágólapra Kategória frissítése Vágólapra másolva - Van már ilyen nevű ... a könyvtáradban. + Van már egy ilyen nevű elem a könyvtáradban. \n -\nBiztos folytatni akarod\? - Ez eltávolítja az eddig kiválasztott kezdeti dátumot a/az %s +\nBiztos folytatni akarod? + Ez eltávolítja az eddig kiválasztott kezdeti dátumot a(z) %s szolgáltatásból F-Droid csomagok nem hivatalosan támogatottak. \nKattincs további információért. Befejezett olvasmányok @@ -665,10 +661,10 @@ Elérhető de a forrás nincs telepítve: %s Tárhely engedély nincs megadva Használt - %1$s error: %2$s + %1$s hiba: %2$s Biztos vagy benne\? Kategória üres - Ez eltávolítja az eddig kiválasztott befejezési dátumot a/az %s + Ez eltávolítja az eddig kiválasztott befejezési dátumot a(z) %s szolgáltatásból Olvasási idő Nem található bejegyzés ebben a kategóriában A/az \"%s\"-t elfogod távolítani a könyvtáradból @@ -686,15 +682,15 @@ Balra csúsztatási cselekmény Duplikált fejezetek átugrása Időköz állítása - Egyedi lekérési időköz + Egyedi frissítési időköz Következő várt frissítés Dupla koppintás a nagyításhoz Már könyvtári bejegyzések elrejtése - Követéshez bejelentkezés + Bejelentkezés nyilvántartóhoz Letöltött törlése Magas képek szétvágása %d soronként - Kivül az elvárt kijönési időszakon + Következő kiadás idejének megjóslása Széles oldalak forgatása az illeszkedéshez Jobbra csúsztatási cselekmény Forgatott széles oldalak megfordítása @@ -781,4 +777,26 @@ Létrehoz Teljes hiba: Kezdetnek állítsunk be néhány dolgot. Ezeket később bármikor módosíthatja a beállításokban. + Felület + Licencelt – Nincs megjeleníthető fejezet + Törlés a(z) %s szolgáltatásból + Ezzel leállítja a helyi nyilvántartást. + N/A + %d perc + %d mp. + %d nap + %d óra + Nyilvántartási pont + Nord + Érzékeny beállítások megadása (Pl. szolgáltatások bejelentkezési tokenei) + Letöltési index érvénytelenítése + Új fejezetek megjelenése %1$s múlva várható, ellenőrzési időköz: %2$s. + Leállítja a(z) %s nyilvántartását? + Nyilvántartott elemek + Kicsinyítés letiltása + Elérhető: %1$s / Összesen: %2$s + Letöltési index érvénytelenítve + Mind becslése + Hamarosan + Egyedi frissítési időköz: \ No newline at end of file diff --git a/i18n/src/commonMain/resources/MR/in/plurals.xml b/i18n/src/commonMain/resources/MR/in/plurals.xml index ff5428138..cb31ac13d 100644 --- a/i18n/src/commonMain/resources/MR/in/plurals.xml +++ b/i18n/src/commonMain/resources/MR/in/plurals.xml @@ -43,7 +43,7 @@ berikutnya %d chapter - Bab %1$s yang hilang + %1$s bab hilang %d hari @@ -51,4 +51,7 @@ %d repo + + Dalam %1$d hari + \ No newline at end of file diff --git a/i18n/src/commonMain/resources/MR/in/strings.xml b/i18n/src/commonMain/resources/MR/in/strings.xml index 9ac55dde6..4fcccb295 100644 --- a/i18n/src/commonMain/resources/MR/in/strings.xml +++ b/i18n/src/commonMain/resources/MR/in/strings.xml @@ -798,4 +798,5 @@ Segera Cabut izin ekstensi tidak dikenal yang tepercaya Repo sumber terbuka + Menonaktifkan zoom out \ No newline at end of file diff --git a/i18n/src/commonMain/resources/MR/ja/strings.xml b/i18n/src/commonMain/resources/MR/ja/strings.xml index e62273fe8..2d411da38 100644 --- a/i18n/src/commonMain/resources/MR/ja/strings.xml +++ b/i18n/src/commonMain/resources/MR/ja/strings.xml @@ -798,4 +798,5 @@ リポジトリURL Nord スマート・アップデート + ズームアウトを無効にする \ No newline at end of file diff --git a/i18n/src/commonMain/resources/MR/ne/strings.xml b/i18n/src/commonMain/resources/MR/ne/strings.xml index f7b46424b..aa98d1a15 100644 --- a/i18n/src/commonMain/resources/MR/ne/strings.xml +++ b/i18n/src/commonMain/resources/MR/ne/strings.xml @@ -802,4 +802,5 @@ सिर्जना गर्नुहोस् पूर्ण त्रुटि: संवेदनशील सेटिङहरू समावेश गर्नुहोस् (जस्तै, ट्र्याकर लगइन टोकनहरू) + जूम आउट असक्षम गर्नुहोस् \ No newline at end of file diff --git a/i18n/src/commonMain/resources/MR/nl/plurals.xml b/i18n/src/commonMain/resources/MR/nl/plurals.xml index 426f1662a..a6aedc6dd 100644 --- a/i18n/src/commonMain/resources/MR/nl/plurals.xml +++ b/i18n/src/commonMain/resources/MR/nl/plurals.xml @@ -64,4 +64,8 @@ %1$s hoofdstuk mist %1$s hoofdstukken missen + + Morgen + Over %1$d dagen + \ No newline at end of file diff --git a/i18n/src/commonMain/resources/MR/nl/strings.xml b/i18n/src/commonMain/resources/MR/nl/strings.xml index 7bb491b2c..69803d6f1 100644 --- a/i18n/src/commonMain/resources/MR/nl/strings.xml +++ b/i18n/src/commonMain/resources/MR/nl/strings.xml @@ -744,4 +744,13 @@ App-instellingen Relatieve tijdstempels \"%1$s\" in plaats van \"%2$s\" + Geselecteerd + Niet geselecteerd + Meer opties + Navigeer omhoog + Scanlator + Toepassen + Naar standaard terugkeren + Onboarding gids + Welkom! \ No newline at end of file diff --git a/i18n/src/commonMain/resources/MR/pl/plurals.xml b/i18n/src/commonMain/resources/MR/pl/plurals.xml index 02c3f4116..da0820219 100644 --- a/i18n/src/commonMain/resources/MR/pl/plurals.xml +++ b/i18n/src/commonMain/resources/MR/pl/plurals.xml @@ -102,4 +102,10 @@ repos repos + + Jutro + Za %1$d dni + Za %1$d dni + Za %1$d dni + \ No newline at end of file diff --git a/i18n/src/commonMain/resources/MR/pl/strings.xml b/i18n/src/commonMain/resources/MR/pl/strings.xml index a2569acc1..b86212168 100644 --- a/i18n/src/commonMain/resources/MR/pl/strings.xml +++ b/i18n/src/commonMain/resources/MR/pl/strings.xml @@ -802,4 +802,5 @@ Licencjonowany - Brak rozdziałów Pominięto, ponieważ nie spodziewano się dzisiaj żadnej publikacji Wyklucz skanlatorów + Wyłącz oddalenie \ No newline at end of file diff --git a/i18n/src/commonMain/resources/MR/ru/plurals.xml b/i18n/src/commonMain/resources/MR/ru/plurals.xml index 7d85c6f3f..a4f625ebe 100644 --- a/i18n/src/commonMain/resources/MR/ru/plurals.xml +++ b/i18n/src/commonMain/resources/MR/ru/plurals.xml @@ -102,4 +102,10 @@ %d репозиториев %d репозиториев + + Завтра + В течении %1$d дней + В течении %1$d дней + В течении %1$d дней + \ No newline at end of file diff --git a/i18n/src/commonMain/resources/MR/ru/strings.xml b/i18n/src/commonMain/resources/MR/ru/strings.xml index ceb52459d..32588e3e5 100644 --- a/i18n/src/commonMain/resources/MR/ru/strings.xml +++ b/i18n/src/commonMain/resources/MR/ru/strings.xml @@ -798,4 +798,5 @@ Север Убрать доверие к ненадёжным расширениям Открыть репозиторий источника + Отключить уменьшение масштаба \ No newline at end of file diff --git a/i18n/src/commonMain/resources/MR/sr/plurals.xml b/i18n/src/commonMain/resources/MR/sr/plurals.xml index 6a538ebc2..a997e61a4 100644 --- a/i18n/src/commonMain/resources/MR/sr/plurals.xml +++ b/i18n/src/commonMain/resources/MR/sr/plurals.xml @@ -41,9 +41,9 @@ Завршено у %1$s са %2$s грешака - %d трекер - %d трекера - %d трекера + %d пратилац + %d пратиоца + %d пратилаца %1$s поглавље @@ -85,4 +85,9 @@ %d репозиторија %d репозиторија + + За %1$d дан + За %1$d дана + За %1$d дана + \ No newline at end of file diff --git a/i18n/src/commonMain/resources/MR/sr/strings.xml b/i18n/src/commonMain/resources/MR/sr/strings.xml index e214d14b5..5e714af28 100644 --- a/i18n/src/commonMain/resources/MR/sr/strings.xml +++ b/i18n/src/commonMain/resources/MR/sr/strings.xml @@ -49,7 +49,7 @@ Уклони Настави Отвори у претраживачу - Режим дисплеја + Режим приказа Екран Збијена мрежа Листа @@ -62,7 +62,7 @@ Ресетуј Врати назад Отвори записник - Врати се + Врати Учитавање… Апликација није доступна Опште @@ -155,7 +155,7 @@ Четири пре прочитаног поглавља Пет пре прочитаног поглавља Преузми нова поглавља - Сервиси + Сервиси праћења Направи резервну копију Може се користити за враћање на тренутно стање колекције Врати се на резервну копију @@ -235,8 +235,8 @@ Последње поглавље Погледај поглавља Откажи све - Икључено - Укључено + Светла + Тамна По систему Управљај обавештењима Безбедност и приватност @@ -280,7 +280,7 @@ Погл. %1$s - %2$s Ажурирање колекције Учитавање страница није успело: %1$s - Утитавање страница… + Учитавање страница… Нема претходног поглавља Нема следећег поглавља Претходно: @@ -321,7 +321,7 @@ Последње коришћено Филтрира све наслове у колекцији Провери ажурирања - Sajt + Вебсајт Враћање је отказано Враћање на резервну копију није успело Враћање је већ у току @@ -395,7 +395,7 @@ Јин и јанг Потпуно црна тема NSFW - извори за одрасле (18+) - Показати у листи за изворе и додатке + Покажи у листи за изворе и додатке Начин читања Инсталер Аутоматско преузимање @@ -421,7 +421,7 @@ Картице Непознат статус Поређај по - Побољшани сервиси + Побољшани сервиси за праћење Неисправна датотека резервне копије Недостају извори: Резервна копија не садржи ниједан наслов. @@ -571,7 +571,7 @@ Мрежа насловница Померај широке слике Аутоматски зумирај слике у пејзажном облику - Sačuvaj kao CBZ arhivu + Сачувај као CBZ архиву Обрнути портрет Издавање завршено Прескочено јер је наслов завршен @@ -588,7 +588,7 @@ Режим читања, дисплеј, навигација Аутоматско преузимање, преузимање унапред Једносмерна синхронизација напретка, побољшана синхронизација - Извори, екстензије, глобална претрага + Извори, додаци, глобална претрага Ручне и аутоматске резервне копије Преузми аутоматски током читања Копирано @@ -596,7 +596,7 @@ Присили апликацију да поново провери преузета поглавља Није инсталирано %s је наишао на неочекивану грешку. Предлажемо да поделите запис о прекиду програма на нашем каналу за подршку на Discord-у. - Widget није доступан када је омогућено закључавање апликације + Виџет није доступан када је омогућено закључавање апликације Прокажи број непрочитаних на икони ажурирања Листа жеља Доступно, али извор није преузет: %s @@ -633,11 +633,11 @@ Просечна оцена Прочитано Коришћено - %d дана - %d сати + %d д + %dч N/A - %d минута - %d секунди + %dмин + %dс Нема описа Дугме за наставак читања InternalError: Провери записнике о прекиду програма за даље информације @@ -655,7 +655,7 @@ Није могуће ресетовати подешавања читача Управо ћете уклонити \"%s\" из своје колекције Погледај твоје недавно ажуриране наслове у колекцији - Неважећи индекс преузимања + Поништи индекс преузимања Број непрочитаних Последња провера ажурирања Обриши категорију @@ -710,7 +710,7 @@ Има резултата Прескочено јер данас није очекивано издање Предвиди очекивано време изласка - Постави интервалу + Постави интервал Интервали Такође уклони из %s Запис праћења @@ -735,7 +735,7 @@ Паметно ажурирање Забљесни приликом листања Смањује артефакте на е-инк екранима - Репозиторије екстензија + Репозиторије додатака Место складиштења Користи се за аутоматске резервне копије, преузимаље поглавља и локални извор. Опозови поверење непознатим екстензијама @@ -761,7 +761,7 @@ Дозвола за инсталацију апликација Пресокочи Следеће - За инсталирање екстензија. + За инсталирање додатака. За примање обавештења о ажурурању библиотеке и др. Одобри Добро дошли! @@ -780,9 +780,27 @@ Норд Ажурураш са старије верзије и не знаш шта да изабереш? Потражи информације у водичу складиштења. Водич складиштења - За инсталацију екстензија потребне су дозволе. Додирни овде за одобрење + За инсталацију додатака потребне су дозволе. Додирни овде за одобрење Грешка: Ова репозиторија већ постоји Прилагођена учесталост ажурирања: Отвори репозиторију + Није одабрано + Одабрано + Више опција + Навигација према горе + Сортирање категорија + Да ли желиш да сортираш категорије по абецеди? + Премести серију на крај + Подешавања апликације + Подешавања извора + Последња аутоматска резервна копија:%s + Никада + Ажурирање библиотеке... (%s) + Онемогући умањивање + Створи + Коришћење складишта + Ни једна датотека није одабрана + Примени + Врати на подразумевано \ No newline at end of file diff --git a/i18n/src/commonMain/resources/MR/zh-rCN/plurals.xml b/i18n/src/commonMain/resources/MR/zh-rCN/plurals.xml index 93ef73539..64caf1d6b 100644 --- a/i18n/src/commonMain/resources/MR/zh-rCN/plurals.xml +++ b/i18n/src/commonMain/resources/MR/zh-rCN/plurals.xml @@ -51,4 +51,7 @@ %d 个仓库 + + %1$d 天后 + \ No newline at end of file diff --git a/i18n/src/commonMain/resources/MR/zh-rCN/strings.xml b/i18n/src/commonMain/resources/MR/zh-rCN/strings.xml index 015b3dc24..37ee723ac 100644 --- a/i18n/src/commonMain/resources/MR/zh-rCN/strings.xml +++ b/i18n/src/commonMain/resources/MR/zh-rCN/strings.xml @@ -798,4 +798,5 @@ 即将更新 撤销已信任的未知插件 打开图源仓库 + 禁用缩小 \ No newline at end of file diff --git a/i18n/src/commonMain/resources/MR/zh-rTW/plurals.xml b/i18n/src/commonMain/resources/MR/zh-rTW/plurals.xml index f3d9a6f0b..ee3847fac 100644 --- a/i18n/src/commonMain/resources/MR/zh-rTW/plurals.xml +++ b/i18n/src/commonMain/resources/MR/zh-rTW/plurals.xml @@ -51,4 +51,7 @@ %d 處儲存庫 + + %1$d 天後 + \ No newline at end of file diff --git a/i18n/src/commonMain/resources/MR/zh-rTW/strings.xml b/i18n/src/commonMain/resources/MR/zh-rTW/strings.xml index 6e42bb805..119c5a467 100644 --- a/i18n/src/commonMain/resources/MR/zh-rTW/strings.xml +++ b/i18n/src/commonMain/resources/MR/zh-rTW/strings.xml @@ -710,7 +710,7 @@ 下次預期更新 刊期 預計每個 - 預估出刊日 + 自訂預估出刊日 設定為每個 由於未臨出刊日,因此略過 有結果 From 09174c5dcb04abe9dadc85bb6e876bb320028a49 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 11 Mar 2024 17:52:41 +0600 Subject: [PATCH 011/106] Update dependency io.nlopez.compose.rules:detekt to v0.3.12 (#500) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> (cherry picked from commit 0d6f426dbd8874c7861b6cc245f9e6ff43dbbe2c) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b15969b0b..9d1f10995 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,7 +10,7 @@ sqldelight = "2.0.0" sqlite = "2.4.0" voyager = "1.0.0" detekt = "1.23.5" -detektCompose = "0.3.11" +detektCompose = "0.3.12" [libraries] desugar = "com.android.tools:desugar_jdk_libs:2.0.4" From 43a11da69491928c5b5cac4f0d2c450b11b25d33 Mon Sep 17 00:00:00 2001 From: AntsyLich <59261191+AntsyLich@users.noreply.github.com> Date: Thu, 14 Mar 2024 03:27:03 +0600 Subject: [PATCH 012/106] Bump default user agent (cherry picked from commit f66f52c244b786ae09f8e4ae683575907068d15f) --- .../kotlin/eu/kanade/tachiyomi/network/NetworkPreferences.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/NetworkPreferences.kt b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/NetworkPreferences.kt index 4c839b90d..c32864aec 100644 --- a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/NetworkPreferences.kt +++ b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/NetworkPreferences.kt @@ -19,7 +19,7 @@ class NetworkPreferences( fun defaultUserAgent(): Preference { return preferenceStore.getString( "default_user_agent", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/121.0", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:123.0) Gecko/20100101 Firefox/123.0", ) } } From 6a6a9dac9cd109eae8352c66212b6c03d5db5886 Mon Sep 17 00:00:00 2001 From: AntsyLich <59261191+AntsyLich@users.noreply.github.com> Date: Thu, 14 Mar 2024 03:28:48 +0600 Subject: [PATCH 013/106] Fix crash in track date selection dialog Co-authored-by: Ivan Iskandar <12537387+ivaniskandar@users.noreply.github.com> (cherry picked from commit f08713587685ddb27cb8ce7184e2dd21ae7968ae) --- .../eu/kanade/tachiyomi/ui/manga/track/TrackInfoDialog.kt | 1 + domain/src/main/java/tachiyomi/domain/track/model/Track.kt | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackInfoDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackInfoDialog.kt index e0ad25182..1ba697f24 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackInfoDialog.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackInfoDialog.kt @@ -427,6 +427,7 @@ private data class TrackDateSelectorScreen( private val start: Boolean, ) : Screen() { + @Transient private val selectableDates = object : SelectableDates { override fun isSelectableDate(utcTimeMillis: Long): Boolean { val dateToCheck = Instant.ofEpochMilli(utcTimeMillis) diff --git a/domain/src/main/java/tachiyomi/domain/track/model/Track.kt b/domain/src/main/java/tachiyomi/domain/track/model/Track.kt index 1a656fcad..a92390494 100644 --- a/domain/src/main/java/tachiyomi/domain/track/model/Track.kt +++ b/domain/src/main/java/tachiyomi/domain/track/model/Track.kt @@ -1,5 +1,7 @@ package tachiyomi.domain.track.model +import java.io.Serializable + data class Track( val id: Long, val mangaId: Long, @@ -14,4 +16,4 @@ data class Track( val remoteUrl: String, val startDate: Long, val finishDate: Long, -) +) : Serializable From 1d1761049c3264717576c4b464dcf9194e839391 Mon Sep 17 00:00:00 2001 From: Jobobby04 Date: Fri, 15 Mar 2024 20:18:44 -0400 Subject: [PATCH 014/106] Use github run_number to create tag --- .github/workflows/build_push_preview.yml | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build_push_preview.yml b/.github/workflows/build_push_preview.yml index 493741bdd..42fec2a2f 100644 --- a/.github/workflows/build_push_preview.yml +++ b/.github/workflows/build_push_preview.yml @@ -18,17 +18,14 @@ jobs: - name: Validate Gradle Wrapper uses: gradle/wrapper-validation-action@v1 - - name: TAG - Bump version and push tag - uses: anothrNick/github-tag-action@1.67.0 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - WITH_V: true - RELEASE_BRANCHES: preview - DEFAULT_BUMP: patch + - name: Create Tag + run: | + git tag "${{ github.run_number }}" + git push origin "${{ github.run_number }}" - name: PING - Dispatch initiating repository event run: | - curl -X POST https://api.github.com/repos/jobobby04/TachiyomiSYPreview/dispatches \ + curl -X POST https://api.github.com/repos/komikku-app/KomikkuPreview/dispatches \ -H 'Accept: application/vnd.github.everest-preview+json' \ -u ${{ secrets.ACCESS_TOKEN }} \ --data '{"event_type": "ping", "client_payload": { "repository": "'"$GITHUB_REPOSITORY"'" }}' From c87b68f445e0d06cf0646e16e44e4e5a4a935d49 Mon Sep 17 00:00:00 2001 From: Shamicen <84282253+Shamicen@users.noreply.github.com> Date: Sat, 16 Mar 2024 01:28:37 +0100 Subject: [PATCH 015/106] Change preferences containing passwords to appStateKeys (#1083) * Change preferences containing passwords to appStateKeys * Change versionCode to 65 * fix merge conflict and add instructions to get library back after migration --- app/src/main/java/exh/EXHMigrations.kt | 23 +++++++++++++++++++ .../core/security/SecurityPreferences.kt | 6 ++--- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/exh/EXHMigrations.kt b/app/src/main/java/exh/EXHMigrations.kt index 334618982..fe3e513cc 100644 --- a/app/src/main/java/exh/EXHMigrations.kt +++ b/app/src/main/java/exh/EXHMigrations.kt @@ -1,6 +1,7 @@ package exh import android.content.Context +import android.widget.Toast import androidx.core.content.edit import androidx.preference.PreferenceManager import eu.kanade.domain.base.BasePreferences @@ -20,6 +21,7 @@ import eu.kanade.tachiyomi.source.online.all.NHentai 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 exh.eh.EHentaiUpdateWorker import exh.log.xLogE @@ -655,6 +657,27 @@ object EXHMigrations { } } + if (oldVersion under 66) { + if (prefs.getBoolean(Preference.privateKey("encrypt_database"), false)) { + context.toast( + "Restart the app to load your encrypted library", + Toast.LENGTH_LONG + ) + } + + val appStatePrefsToReplace = listOf( + "__PRIVATE_sql_password", + "__PRIVATE_encrypt_database", + "__PRIVATE_cbz_password", + ) + + replacePreferences( + preferenceStore = preferenceStore, + filterPredicate = { it.key in appStatePrefsToReplace }, + newKey = { Preference.appStateKey(it.replace("__PRIVATE_", "").trim()) }, + ) + } + // if (oldVersion under 1) { } (1 is current release version) // do stuff here when releasing changed crap diff --git a/core/common/src/main/kotlin/eu/kanade/tachiyomi/core/security/SecurityPreferences.kt b/core/common/src/main/kotlin/eu/kanade/tachiyomi/core/security/SecurityPreferences.kt index aaab4a9d4..df266988d 100644 --- a/core/common/src/main/kotlin/eu/kanade/tachiyomi/core/security/SecurityPreferences.kt +++ b/core/common/src/main/kotlin/eu/kanade/tachiyomi/core/security/SecurityPreferences.kt @@ -24,9 +24,9 @@ class SecurityPreferences( fun authenticatorDays() = this.preferenceStore.getInt("biometric_days", 0x7F) - fun encryptDatabase() = this.preferenceStore.getBoolean(Preference.privateKey("encrypt_database"), false) + fun encryptDatabase() = this.preferenceStore.getBoolean(Preference.appStateKey("encrypt_database"), false) - fun sqlPassword() = this.preferenceStore.getString(Preference.privateKey("sql_password"), "") + fun sqlPassword() = this.preferenceStore.getString(Preference.appStateKey("sql_password"), "") fun passwordProtectDownloads() = preferenceStore.getBoolean( Preference.privateKey("password_protect_downloads"), @@ -35,7 +35,7 @@ class SecurityPreferences( fun encryptionType() = this.preferenceStore.getEnum("encryption_type", EncryptionType.AES_256) - fun cbzPassword() = this.preferenceStore.getString(Preference.privateKey("cbz_password"), "") + fun cbzPassword() = this.preferenceStore.getString(Preference.appStateKey("cbz_password"), "") // SY <-- /** From 2951f641c828cf6aea721b1b5b384917ebba8489 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C9=B4=E1=B4=87=E1=B4=8B=E1=B4=8F?= <111511925+NeKoOuO@users.noreply.github.com> Date: Sat, 16 Mar 2024 08:36:28 +0800 Subject: [PATCH 016/106] Update README.md (#1113) Tachi -> Mihon --- README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 8c3bc22b9..877bb4c7b 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ # ![app icon](./.github/readme-images/app-icon.png) Komikku -Tachiyomi is a free and open source manga reader for Android 6.0 and above. This version of Tachiyomi, Komikku was based off TachiyomiSY. This version is meant to push forward in the ways of usability and features. Komikku tries to push forward where it can, but staying in a place where it can easily grab updates and features from the main app, it tries to make new features, or take features from other forks like TachiyomiSY, J2K and Neko. +Mihon/Tachiyomi is a free and open source manga reader for Android 6.0 and above. This version of Mihon/Tachiyomi, Komikku was based off TachiyomiSY. This version is meant to push forward in the ways of usability and features. TachiyomiSY tries to push forward where it can, but staying in a place where it can easily grab updates and features from the main app, it tries to make new features, or take features from other forks like SY, J2K and Neko. ![screenshots of app](./.github/readme-images/screens.png) @@ -25,7 +25,7 @@ Also allow long-click to add/remove single entry to/from library, everywhere. - [ ] Support other sources just like E-Hentai: Lucious... - [ ] History of searched query -Features of Tachiyomi(original) include: +Features of Mihon/Tachiyomi(original) include: * Online reading from a variety of sources * Local reading of downloaded content * A configurable reader with multiple viewers, reading directions and other settings. @@ -57,7 +57,6 @@ Features of TachiyomiSY include: * Page preload customization * Customize image cache size * Batch import of custom sources and featured extensions -* Automatic CAPTCHA solving * Advanced source settings page, searching, enable/disable all * Click tag for local search, long click tag for global search * Merge multiple of the same manga from different sources From d7ed145c827d80361e415381e765b42654b920f0 Mon Sep 17 00:00:00 2001 From: KaiserBh <41852205+kaiserbh@users.noreply.github.com> Date: Sun, 17 Mar 2024 02:53:20 +1100 Subject: [PATCH 017/106] feat: add cross device sync (#1005) * feat: add cross device sync. * chore: add google api. * chore: add SY specifics. * feat: add backupSource, backupPref, and "SY" backupSavedSearches. I forgot to add the data into the merging logic, So remote always have empty value :(. Better late than never. * feat(sync): Allow to choose what to sync. Various improvement and added the option to choose what they want to sync. Added sync library button to LibraryTab as well. Signed-off-by: KaiserBh * oops. Signed-off-by: KaiserBh * refactor: fix up the sync triggers, and update imports. * refactor * chore: review pointers. * refactor: update imports * refactor: add more error guard for gdrive. Also changed it to be app specific, we don't want them to use sync data from SY or other forks as some of the model and backup is different. So if people using other forks they should use the same data and not mismatch. * fix: conflict and refactor. * refactor: update imports. * chore: fix some of detekt error. * refactor: add breaks and max retries. I think we were reaching deadlock or infinite loop causing the sync to go forever. * feat: db changes to accommodate new syncing logic. Using timestamp to sync is a bit skewed due to system clock etc and therefore there was a lot of issues with it such as removing a manga that shouldn't have been removed. Marking chapters as unread even though it was marked as a read. Hopefully by using versioning system it should eliminate those issues. * chore: add migrations * chore: version and is_syncing fields. * chore: add SY only stuff. * fix: oops wrong index. Signed-off-by: KaiserBh * chore: review pointers. Signed-off-by: KaiserBh * chore: remove the option to reset timestamp We don't need this anymore since we are utilizing versioning system. Signed-off-by: KaiserBh * refactor: Forgot to use the new versioning system. I forgot to cherry pick this from mihon branch. * chore: remove isSyncing from Chapter/Manga model. Signed-off-by: KaiserBh * chore: remove unused import. Signed-off-by: KaiserBh * chore: remove isSyncing leftover. Signed-off-by: KaiserBh * chore: remove isSyncing. Signed-off-by: KaiserBh * refactor: make sure the manga version is bumped. When there is changes in the chapters table such as reading or updating last read page we should bump the manga version. This way the manga is synced properly as in the History and last_read history is synced properly. This should fix the sorting issue. Signed-off-by: KaiserBh --------- Signed-off-by: KaiserBh --- app/build.gradle.kts | 3 + app/proguard-rules.pro | 23 +- app/src/main/AndroidManifest.xml | 14 + app/src/main/assets/client_secrets.json | 1 + .../eu/kanade/domain/chapter/model/Chapter.kt | 1 + .../eu/kanade/domain/sync/SyncPreferences.kt | 92 +++ .../kanade/domain/sync/models/SyncSettings.kt | 12 + .../library/components/LibraryToolbar.kt | 8 +- .../settings/screen/SettingsAdvancedScreen.kt | 1 - .../settings/screen/SettingsDataScreen.kt | 237 +++++++- .../screen/data/SyncSettingsSelector.kt | 142 +++++ .../screen/data/SyncTriggerOptionsScreen.kt | 101 ++++ app/src/main/java/eu/kanade/tachiyomi/App.kt | 22 + .../data/backup/create/BackupCreator.kt | 12 +- .../data/backup/restore/BackupRestorer.kt | 2 +- .../backup/restore/restorers/MangaRestorer.kt | 66 ++- .../data/library/LibraryUpdateJob.kt | 42 ++ .../data/notification/NotificationReceiver.kt | 34 ++ .../kanade/tachiyomi/data/sync/SyncDataJob.kt | 102 ++++ .../kanade/tachiyomi/data/sync/SyncManager.kt | 327 +++++++++++ .../tachiyomi/data/sync/SyncNotifier.kt | 86 +++ .../data/sync/models/SyncTriggerOptions.kt | 72 +++ .../sync/service/GoogleDriveSyncService.kt | 522 ++++++++++++++++++ .../data/sync/service/SyncService.kt | 511 +++++++++++++++++ .../data/sync/service/SyncYomiSyncService.kt | 198 +++++++ .../java/eu/kanade/tachiyomi/di/AppModule.kt | 3 + .../kanade/tachiyomi/di/PreferenceModule.kt | 5 + .../kanade/tachiyomi/ui/library/LibraryTab.kt | 9 + .../tachiyomi/ui/reader/ReaderViewModel.kt | 15 + .../setting/track/GoogleDriveLoginActivity.kt | 54 ++ .../eu/kanade/tachiyomi/network/Requests.kt | 13 + .../common/util/system/LogcatExtensions.kt | 20 +- gradle/libs.versions.toml | 3 + .../commonMain/resources/MR/base/strings.xml | 55 +- .../resources/MR/nb-rNO/plurals.xml | 4 + .../commonMain/resources/MR/th/plurals.xml | 3 + .../commonMain/resources/MR/tr/plurals.xml | 4 + 37 files changed, 2772 insertions(+), 47 deletions(-) create mode 100644 app/src/main/assets/client_secrets.json create mode 100644 app/src/main/java/eu/kanade/domain/sync/SyncPreferences.kt create mode 100644 app/src/main/java/eu/kanade/domain/sync/models/SyncSettings.kt create mode 100644 app/src/main/java/eu/kanade/presentation/more/settings/screen/data/SyncSettingsSelector.kt create mode 100644 app/src/main/java/eu/kanade/presentation/more/settings/screen/data/SyncTriggerOptionsScreen.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/sync/SyncDataJob.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/sync/SyncManager.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/sync/SyncNotifier.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/sync/models/SyncTriggerOptions.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/sync/service/GoogleDriveSyncService.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/sync/service/SyncService.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/sync/service/SyncYomiSyncService.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/setting/track/GoogleDriveLoginActivity.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 6facac8c6..8f5254bbf 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -248,6 +248,9 @@ dependencies { implementation(libs.compose.materialmotion) implementation(libs.swipe) + implementation(libs.google.api.services.drive) + implementation(libs.google.api.client.oauth) + // Logging implementation(libs.logcat) diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 072585f59..f4445d017 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -126,6 +126,12 @@ -keep class com.google.firebase.installations.** { *; } -keep interface com.google.firebase.installations.** { *; } +# Google Drive +-keep class com.google.api.services.** { *; } + +# Google OAuth +-keep class com.google.api.client.** { *; } + # SY --> # SqlCipher -keepclassmembers class net.zetetic.database.sqlcipher.SQLiteCustomFunction { *; } @@ -260,6 +266,9 @@ -keep,allowoptimization class * extends uy.kohesive.injekt.api.TypeReference -keep,allowoptimization public class io.requery.android.database.sqlite.SQLiteConnection { *; } + # Keep apache http client + -keep class org.apache.http.** { *; } + # Suggested rules -dontwarn com.oracle.svm.core.annotate.AutomaticFeature -dontwarn com.oracle.svm.core.annotate.Delete @@ -272,4 +281,16 @@ -dontwarn org.slf4j.impl.StaticLoggerBinder -dontwarn java.lang.Module -dontwarn org.graalvm.nativeimage.hosted.RuntimeResourceAccess --dontwarn org.jspecify.annotations.NullMarked \ No newline at end of file +-dontwarn org.jspecify.annotations.NullMarked +-dontwarn javax.naming.InvalidNameException +-dontwarn javax.naming.NamingException +-dontwarn javax.naming.directory.Attribute +-dontwarn javax.naming.directory.Attributes +-dontwarn javax.naming.ldap.LdapName +-dontwarn javax.naming.ldap.Rdn +-dontwarn org.ietf.jgss.GSSContext +-dontwarn org.ietf.jgss.GSSCredential +-dontwarn org.ietf.jgss.GSSException +-dontwarn org.ietf.jgss.GSSManager +-dontwarn org.ietf.jgss.GSSName +-dontwarn org.ietf.jgss.Oid \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 172b84191..1f5dfb478 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -189,6 +189,20 @@ + + + + + + + + + + Unit, onClickGlobalUpdate: () -> Unit, onClickOpenRandomManga: () -> Unit, + onClickSyncNow: () -> Unit, // SY --> onClickSyncExh: (() -> Unit)?, // SY <-- @@ -60,6 +61,7 @@ fun LibraryToolbar( onClickRefresh = onClickRefresh, onClickGlobalUpdate = onClickGlobalUpdate, onClickOpenRandomManga = onClickOpenRandomManga, + onClickSyncNow = onClickSyncNow, // SY --> onClickSyncExh = onClickSyncExh, // SY <-- @@ -77,6 +79,7 @@ private fun LibraryRegularToolbar( onClickRefresh: () -> Unit, onClickGlobalUpdate: () -> Unit, onClickOpenRandomManga: () -> Unit, + onClickSyncNow: () -> Unit, // SY --> onClickSyncExh: (() -> Unit)?, // SY <-- @@ -125,7 +128,10 @@ private fun LibraryRegularToolbar( title = stringResource(MR.strings.action_open_random_manga), onClick = onClickOpenRandomManga, ), - + AppBar.OverflowAction( + title = stringResource(MR.strings.sync_library), + onClick = onClickSyncNow, + ), ).builder().apply { // SY --> if (onClickSyncExh != null) { diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsAdvancedScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsAdvancedScreen.kt index 07af01ea8..1ebd67f4b 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsAdvancedScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsAdvancedScreen.kt @@ -60,7 +60,6 @@ import eu.kanade.tachiyomi.util.CrashLogUtil import eu.kanade.tachiyomi.util.storage.DiskUtil import eu.kanade.tachiyomi.util.system.isDevFlavor import eu.kanade.tachiyomi.util.system.isPreviewBuildType -import eu.kanade.tachiyomi.util.system.isReleaseBuildType import eu.kanade.tachiyomi.util.system.isShizukuInstalled import eu.kanade.tachiyomi.util.system.powerManager import eu.kanade.tachiyomi.util.system.setDefaultSettings diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDataScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDataScreen.kt index a6f9955a0..9e51db038 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDataScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDataScreen.kt @@ -15,16 +15,19 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.outlined.HelpOutline +import androidx.compose.material3.AlertDialog import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MultiChoiceSegmentedButtonRow import androidx.compose.material3.SegmentedButton import androidx.compose.material3.SegmentedButtonDefaults import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue @@ -35,10 +38,13 @@ import androidx.core.net.toUri import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow import com.hippo.unifile.UniFile +import eu.kanade.domain.sync.SyncPreferences import eu.kanade.presentation.more.settings.Preference import eu.kanade.presentation.more.settings.screen.data.CreateBackupScreen import eu.kanade.presentation.more.settings.screen.data.RestoreBackupScreen import eu.kanade.presentation.more.settings.screen.data.StorageInfo +import eu.kanade.presentation.more.settings.screen.data.SyncSettingsSelector +import eu.kanade.presentation.more.settings.screen.data.SyncTriggerOptionsScreen import eu.kanade.presentation.more.settings.widget.BasePreferenceWidget import eu.kanade.presentation.more.settings.widget.PrefsHorizontalPadding import eu.kanade.presentation.util.relativeTimeSpanString @@ -46,10 +52,15 @@ import eu.kanade.tachiyomi.data.backup.create.BackupCreateJob import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob import eu.kanade.tachiyomi.data.cache.ChapterCache import eu.kanade.tachiyomi.data.cache.PagePreviewCache +import eu.kanade.tachiyomi.data.sync.SyncDataJob +import eu.kanade.tachiyomi.data.sync.SyncManager +import eu.kanade.tachiyomi.data.sync.service.GoogleDriveService +import eu.kanade.tachiyomi.data.sync.service.GoogleDriveSyncService import eu.kanade.tachiyomi.util.system.DeviceUtil import eu.kanade.tachiyomi.util.system.toast import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentMapOf +import kotlinx.coroutines.launch import logcat.LogPriority import tachiyomi.core.common.i18n.stringResource import tachiyomi.core.common.storage.displayablePath @@ -91,13 +102,16 @@ object SettingsDataScreen : SearchableSettings { val backupPreferences = Injekt.get() val storagePreferences = Injekt.get() + val syncPreferences = remember { Injekt.get() } + val syncService by syncPreferences.syncService().collectAsState() + return persistentListOf( getStorageLocationPref(storagePreferences = storagePreferences), Preference.PreferenceItem.InfoPreference(stringResource(MR.strings.pref_storage_location_info)), getBackupAndRestoreGroup(backupPreferences = backupPreferences), getDataGroup(), - ) + ) + getSyncPreferences(syncPreferences = syncPreferences, syncService = syncService) } @Composable @@ -330,4 +344,225 @@ object SettingsDataScreen : SearchableSettings { ), ) } + + @Composable + private fun getSyncPreferences(syncPreferences: SyncPreferences, syncService: Int): List { + return listOf( + Preference.PreferenceGroup( + title = stringResource(MR.strings.pref_sync_service_category), + preferenceItems = persistentListOf( + Preference.PreferenceItem.ListPreference( + pref = syncPreferences.syncService(), + title = stringResource(MR.strings.pref_sync_service), + entries = persistentMapOf( + SyncManager.SyncService.NONE.value to stringResource(MR.strings.off), + SyncManager.SyncService.SYNCYOMI.value to stringResource(MR.strings.syncyomi), + SyncManager.SyncService.GOOGLE_DRIVE.value to stringResource(MR.strings.google_drive), + ), + onValueChanged = { true }, + ), + ), + ), + ) + getSyncServicePreferences(syncPreferences, syncService) + } + + @Composable + private fun getSyncServicePreferences(syncPreferences: SyncPreferences, syncService: Int): List { + val syncServiceType = SyncManager.SyncService.fromInt(syncService) + + val basePreferences = getBasePreferences(syncServiceType, syncPreferences) + + return if (syncServiceType != SyncManager.SyncService.NONE) { + basePreferences + getAdditionalPreferences(syncPreferences) + } else { + basePreferences + } + } + + @Composable + private fun getBasePreferences( + syncServiceType: SyncManager.SyncService, + syncPreferences: SyncPreferences, + ): List { + return when (syncServiceType) { + SyncManager.SyncService.NONE -> emptyList() + SyncManager.SyncService.SYNCYOMI -> getSelfHostPreferences(syncPreferences) + SyncManager.SyncService.GOOGLE_DRIVE -> getGoogleDrivePreferences() + } + } + + @Composable + private fun getAdditionalPreferences(syncPreferences: SyncPreferences): List { + return listOf(getSyncNowPref(), getAutomaticSyncGroup(syncPreferences)) + } + + @Composable + private fun getGoogleDrivePreferences(): List { + val context = LocalContext.current + val googleDriveSync = Injekt.get() + return listOf( + Preference.PreferenceItem.TextPreference( + title = stringResource(MR.strings.pref_google_drive_sign_in), + onClick = { + val intent = googleDriveSync.getSignInIntent() + context.startActivity(intent) + }, + ), + getGoogleDrivePurge(), + ) + } + + @Composable + private fun getGoogleDrivePurge(): Preference.PreferenceItem.TextPreference { + val scope = rememberCoroutineScope() + val context = LocalContext.current + val googleDriveSync = remember { GoogleDriveSyncService(context) } + var showPurgeDialog by remember { mutableStateOf(false) } + + if (showPurgeDialog) { + PurgeConfirmationDialog( + onConfirm = { + showPurgeDialog = false + scope.launch { + val result = googleDriveSync.deleteSyncDataFromGoogleDrive() + when (result) { + GoogleDriveSyncService.DeleteSyncDataStatus.NOT_INITIALIZED -> context.toast( + MR.strings.google_drive_not_signed_in, + duration = 5000, + ) + GoogleDriveSyncService.DeleteSyncDataStatus.NO_FILES -> context.toast( + MR.strings.google_drive_sync_data_not_found, + duration = 5000, + ) + GoogleDriveSyncService.DeleteSyncDataStatus.SUCCESS -> context.toast( + MR.strings.google_drive_sync_data_purged, + duration = 5000, + ) + GoogleDriveSyncService.DeleteSyncDataStatus.ERROR -> context.toast( + MR.strings.google_drive_sync_data_purge_error, + duration = 10000, + ) + } + } + }, + onDismissRequest = { showPurgeDialog = false }, + ) + } + + return Preference.PreferenceItem.TextPreference( + title = stringResource(MR.strings.pref_google_drive_purge_sync_data), + onClick = { showPurgeDialog = true }, + ) + } + + @Composable + private fun PurgeConfirmationDialog( + onConfirm: () -> Unit, + onDismissRequest: () -> Unit, + ) { + AlertDialog( + onDismissRequest = onDismissRequest, + title = { Text(text = stringResource(MR.strings.pref_purge_confirmation_title)) }, + text = { Text(text = stringResource(MR.strings.pref_purge_confirmation_message)) }, + dismissButton = { + TextButton(onClick = onDismissRequest) { + Text(text = stringResource(MR.strings.action_cancel)) + } + }, + confirmButton = { + TextButton(onClick = onConfirm) { + Text(text = stringResource(MR.strings.action_ok)) + } + }, + ) + } + + @Composable + private fun getSelfHostPreferences(syncPreferences: SyncPreferences): List { + val scope = rememberCoroutineScope() + return listOf( + Preference.PreferenceItem.EditTextPreference( + title = stringResource(MR.strings.pref_sync_host), + subtitle = stringResource(MR.strings.pref_sync_host_summ), + pref = syncPreferences.clientHost(), + onValueChanged = { newValue -> + scope.launch { + // Trim spaces at the beginning and end, then remove trailing slash if present + val trimmedValue = newValue.trim() + val modifiedValue = trimmedValue.trimEnd { it == '/' } + syncPreferences.clientHost().set(modifiedValue) + } + true + }, + ), + Preference.PreferenceItem.EditTextPreference( + title = stringResource(MR.strings.pref_sync_api_key), + subtitle = stringResource(MR.strings.pref_sync_api_key_summ), + pref = syncPreferences.clientAPIKey(), + ), + ) + } + + @Composable + private fun getSyncNowPref(): Preference.PreferenceGroup { + val navigator = LocalNavigator.currentOrThrow + return Preference.PreferenceGroup( + title = stringResource(MR.strings.pref_sync_now_group_title), + preferenceItems = persistentListOf( + getSyncOptionsPref(), + Preference.PreferenceItem.TextPreference( + title = stringResource(MR.strings.pref_sync_now), + subtitle = stringResource(MR.strings.pref_sync_now_subtitle), + onClick = { + navigator.push(SyncSettingsSelector()) + }, + ), + ), + ) + } + + @Composable + private fun getSyncOptionsPref(): Preference.PreferenceItem.TextPreference { + val navigator = LocalNavigator.currentOrThrow + return Preference.PreferenceItem.TextPreference( + title = stringResource(MR.strings.pref_sync_options), + subtitle = stringResource(MR.strings.pref_sync_options_summ), + onClick = { navigator.push(SyncTriggerOptionsScreen()) }, + ) + } + + @Composable + private fun getAutomaticSyncGroup(syncPreferences: SyncPreferences): Preference.PreferenceGroup { + val context = LocalContext.current + val syncIntervalPref = syncPreferences.syncInterval() + val lastSync by syncPreferences.lastSyncTimestamp().collectAsState() + + return Preference.PreferenceGroup( + title = stringResource(MR.strings.pref_sync_automatic_category), + preferenceItems = persistentListOf( + Preference.PreferenceItem.ListPreference( + pref = syncIntervalPref, + title = stringResource(MR.strings.pref_sync_interval), + entries = persistentMapOf( + 0 to stringResource(MR.strings.off), + 30 to stringResource(MR.strings.update_30min), + 60 to stringResource(MR.strings.update_1hour), + 180 to stringResource(MR.strings.update_3hour), + 360 to stringResource(MR.strings.update_6hour), + 720 to stringResource(MR.strings.update_12hour), + 1440 to stringResource(MR.strings.update_24hour), + 2880 to stringResource(MR.strings.update_48hour), + 10080 to stringResource(MR.strings.update_weekly), + ), + onValueChanged = { + SyncDataJob.setupTask(context, it) + true + }, + ), + Preference.PreferenceItem.InfoPreference( + stringResource(MR.strings.last_synchronization, relativeTimeSpanString(lastSync)), + ), + ), + ) + } } diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/SyncSettingsSelector.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/SyncSettingsSelector.kt new file mode 100644 index 000000000..86865113f --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/SyncSettingsSelector.kt @@ -0,0 +1,142 @@ +package eu.kanade.presentation.more.settings.screen.data + +import android.content.Context +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.platform.LocalContext +import cafe.adriel.voyager.core.model.StateScreenModel +import cafe.adriel.voyager.core.model.rememberScreenModel +import cafe.adriel.voyager.navigator.LocalNavigator +import cafe.adriel.voyager.navigator.currentOrThrow +import eu.kanade.domain.sync.SyncPreferences +import eu.kanade.domain.sync.models.SyncSettings +import eu.kanade.presentation.components.AppBar +import eu.kanade.presentation.util.Screen +import eu.kanade.tachiyomi.data.backup.create.BackupOptions +import eu.kanade.tachiyomi.data.sync.SyncDataJob +import eu.kanade.tachiyomi.util.system.toast +import kotlinx.collections.immutable.ImmutableList +import kotlinx.coroutines.flow.update +import tachiyomi.i18n.MR +import tachiyomi.presentation.core.components.LabeledCheckbox +import tachiyomi.presentation.core.components.LazyColumnWithAction +import tachiyomi.presentation.core.components.SectionCard +import tachiyomi.presentation.core.components.material.Scaffold +import tachiyomi.presentation.core.i18n.stringResource +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +class SyncSettingsSelector : Screen() { + @Composable + override fun Content() { + val context = LocalContext.current + val navigator = LocalNavigator.currentOrThrow + val model = rememberScreenModel { SyncSettingsSelectorModel() } + val state by model.state.collectAsState() + + Scaffold( + topBar = { + AppBar( + title = stringResource(MR.strings.pref_choose_what_to_sync), + navigateUp = navigator::pop, + scrollBehavior = it, + ) + }, + ) { contentPadding -> + LazyColumnWithAction( + contentPadding = contentPadding, + actionLabel = stringResource(MR.strings.label_sync), + actionEnabled = state.options.anyEnabled(), + onClickAction = { + if (!SyncDataJob.isAnyJobRunning(context)) { + model.syncNow(context) + navigator.pop() + } else { + context.toast(MR.strings.sync_in_progress) + } + }, + ) { + item { + SectionCard(MR.strings.label_library) { + Options(BackupOptions.libraryOptions, state, model) + } + } + + item { + SectionCard(MR.strings.label_settings) { + Options(BackupOptions.settingsOptions, state, model) + } + } + } + } + } + + @Composable + private fun Options( + options: ImmutableList, + state: SyncSettingsSelectorModel.State, + model: SyncSettingsSelectorModel, + ) { + options.forEach { option -> + LabeledCheckbox( + label = stringResource(option.label), + checked = option.getter(state.options), + onCheckedChange = { + model.toggle(option.setter, it) + }, + enabled = option.enabled(state.options), + ) + } + } +} + +private class SyncSettingsSelectorModel( + val syncPreferences: SyncPreferences = Injekt.get(), +) : StateScreenModel( + State(syncOptionsToBackupOptions(syncPreferences.getSyncSettings())), +) { + fun toggle(setter: (BackupOptions, Boolean) -> BackupOptions, enabled: Boolean) { + mutableState.update { + val updatedOptions = setter(it.options, enabled) + syncPreferences.setSyncSettings(backupOptionsToSyncOptions(updatedOptions)) + it.copy(options = updatedOptions) + } + } + + fun syncNow(context: Context) { + SyncDataJob.startNow(context) + } + + @Immutable + data class State( + val options: BackupOptions = BackupOptions(), + ) companion object { + private fun syncOptionsToBackupOptions(syncSettings: SyncSettings): BackupOptions { + return BackupOptions( + libraryEntries = syncSettings.libraryEntries, + categories = syncSettings.categories, + chapters = syncSettings.chapters, + tracking = syncSettings.tracking, + history = syncSettings.history, + appSettings = syncSettings.appSettings, + sourceSettings = syncSettings.sourceSettings, + privateSettings = syncSettings.privateSettings, + ) + } + + private fun backupOptionsToSyncOptions(backupOptions: BackupOptions): SyncSettings { + return SyncSettings( + libraryEntries = backupOptions.libraryEntries, + categories = backupOptions.categories, + chapters = backupOptions.chapters, + tracking = backupOptions.tracking, + history = backupOptions.history, + appSettings = backupOptions.appSettings, + sourceSettings = backupOptions.sourceSettings, + privateSettings = backupOptions.privateSettings, + ) + } + } +} diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/SyncTriggerOptionsScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/SyncTriggerOptionsScreen.kt new file mode 100644 index 000000000..27136eceb --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/SyncTriggerOptionsScreen.kt @@ -0,0 +1,101 @@ +package eu.kanade.presentation.more.settings.screen.data + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import cafe.adriel.voyager.core.model.StateScreenModel +import cafe.adriel.voyager.core.model.rememberScreenModel +import cafe.adriel.voyager.navigator.LocalNavigator +import cafe.adriel.voyager.navigator.currentOrThrow +import eu.kanade.domain.sync.SyncPreferences +import eu.kanade.presentation.components.AppBar +import eu.kanade.presentation.util.Screen +import eu.kanade.tachiyomi.data.sync.models.SyncTriggerOptions +import kotlinx.collections.immutable.ImmutableList +import kotlinx.coroutines.flow.update +import tachiyomi.i18n.MR +import tachiyomi.presentation.core.components.LabeledCheckbox +import tachiyomi.presentation.core.components.LazyColumnWithAction +import tachiyomi.presentation.core.components.SectionCard +import tachiyomi.presentation.core.components.material.Scaffold +import tachiyomi.presentation.core.i18n.stringResource +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +class SyncTriggerOptionsScreen : Screen() { + + @Composable + override fun Content() { + val navigator = LocalNavigator.currentOrThrow + val model = rememberScreenModel { SyncOptionsScreenModel() } + val state by model.state.collectAsState() + + Scaffold( + topBar = { + AppBar( + title = stringResource(MR.strings.pref_sync_options), + navigateUp = navigator::pop, + scrollBehavior = it, + ) + }, + ) { contentPadding -> + LazyColumnWithAction( + contentPadding = contentPadding, + actionLabel = stringResource(MR.strings.action_save), + actionEnabled = state.options.anyEnabled(), + onClickAction = { + navigator.pop() + }, + ) { + item { + SectionCard(MR.strings.label_triggers) { + Options(SyncTriggerOptions.mainOptions, state, model) + } + } + } + } + } + + @Composable + private fun Options( + options: ImmutableList, + state: SyncOptionsScreenModel.State, + model: SyncOptionsScreenModel, + ) { + options.forEach { option -> + LabeledCheckbox( + label = stringResource(option.label), + checked = option.getter(state.options), + onCheckedChange = { + model.toggle(option.setter, it) + }, + enabled = option.enabled(state.options), + ) + } + } +} + +private class SyncOptionsScreenModel( + val syncPreferences: SyncPreferences = Injekt.get(), +) : StateScreenModel( + State( + syncPreferences.getSyncTriggerOptions(), + ), +) { + + fun toggle(setter: (SyncTriggerOptions, Boolean) -> SyncTriggerOptions, enabled: Boolean) { + mutableState.update { + val updatedTriggerOptions = setter(it.options, enabled) + syncPreferences.setSyncTriggerOptions(updatedTriggerOptions) + it.copy( + options = updatedTriggerOptions, + ) + } + } + + @Immutable + data class State( + val options: SyncTriggerOptions = SyncTriggerOptions(), + ) +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/App.kt b/app/src/main/java/eu/kanade/tachiyomi/App.kt index 22ec6fb41..a619d617f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/App.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/App.kt @@ -36,6 +36,7 @@ import com.google.firebase.ktx.initialize import eu.kanade.domain.DomainModule import eu.kanade.domain.SYDomainModule import eu.kanade.domain.base.BasePreferences +import eu.kanade.domain.sync.SyncPreferences import eu.kanade.domain.ui.UiPreferences import eu.kanade.domain.ui.model.setAppCompatDelegateThemeMode import eu.kanade.tachiyomi.crash.CrashActivity @@ -47,6 +48,7 @@ import eu.kanade.tachiyomi.data.coil.PagePreviewFetcher import eu.kanade.tachiyomi.data.coil.PagePreviewKeyer import eu.kanade.tachiyomi.data.coil.TachiyomiImageDecoder import eu.kanade.tachiyomi.data.notification.Notifications +import eu.kanade.tachiyomi.data.sync.SyncDataJob import eu.kanade.tachiyomi.di.AppModule import eu.kanade.tachiyomi.di.PreferenceModule import eu.kanade.tachiyomi.di.SYPreferenceModule @@ -168,6 +170,13 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor /*if (!LogcatLogger.isInstalled && networkPreferences.verboseLogging().get()) { LogcatLogger.install(AndroidLogcatLogger(LogPriority.VERBOSE)) }*/ + + val syncPreferences: SyncPreferences = Injekt.get() + val syncTriggerOpt = syncPreferences.getSyncTriggerOptions() + if (syncPreferences.isSyncEnabled() && syncTriggerOpt.syncOnAppStart + ) { + SyncDataJob.startNow(this@App) + } } override fun newImageLoader(context: Context): ImageLoader { @@ -199,6 +208,13 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor override fun onStart(owner: LifecycleOwner) { SecureActivityDelegate.onApplicationStart() + + val syncPreferences: SyncPreferences = Injekt.get() + val syncTriggerOpt = syncPreferences.getSyncTriggerOptions() + if (syncPreferences.isSyncEnabled() && syncTriggerOpt.syncOnAppResume + ) { + SyncDataJob.startNow(this@App) + } } override fun onStop(owner: LifecycleOwner) { @@ -232,6 +248,12 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor } catch (e: Exception) { logcat(LogPriority.ERROR, e) { "Failed to modify notification channels" } } + + val syncPreferences: SyncPreferences = Injekt.get() + val syncTriggerOpt = syncPreferences.getSyncTriggerOptions() + if (syncPreferences.isSyncEnabled() && syncTriggerOpt.syncOnAppStart) { + SyncDataJob.startNow(this@App) + } } // EXH diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupCreator.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupCreator.kt index 73ccbd3dc..182f840fb 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupCreator.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupCreator.kt @@ -132,34 +132,34 @@ class BackupCreator( } } - private suspend fun backupCategories(options: BackupOptions): List { + suspend fun backupCategories(options: BackupOptions): List { if (!options.categories) return emptyList() return categoriesBackupCreator.backupCategories() } - private suspend fun backupMangas(mangas: List, options: BackupOptions): List { + suspend fun backupMangas(mangas: List, options: BackupOptions): List { return mangaBackupCreator.backupMangas(mangas, options) } - private fun backupSources(mangas: List): List { + fun backupSources(mangas: List): List { return sourcesBackupCreator.backupSources(mangas) } - private fun backupAppPreferences(options: BackupOptions): List { + fun backupAppPreferences(options: BackupOptions): List { if (!options.appSettings) return emptyList() return preferenceBackupCreator.backupAppPreferences(includePrivatePreferences = options.privateSettings) } - private fun backupSourcePreferences(options: BackupOptions): List { + fun backupSourcePreferences(options: BackupOptions): List { if (!options.sourceSettings) return emptyList() return preferenceBackupCreator.backupSourcePreferences(includePrivatePreferences = options.privateSettings) } // SY --> - private suspend fun backupSavedSearches(): List { + suspend fun backupSavedSearches(): List { return savedSearchBackupCreator.backupSavedSearches() } // SY <-- diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/BackupRestorer.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/BackupRestorer.kt index a84e6af90..d58213aac 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/BackupRestorer.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/BackupRestorer.kt @@ -34,7 +34,7 @@ class BackupRestorer( private val categoriesRestorer: CategoriesRestorer = CategoriesRestorer(), private val preferenceRestorer: PreferenceRestorer = PreferenceRestorer(context), - private val mangaRestorer: MangaRestorer = MangaRestorer(), + private val mangaRestorer: MangaRestorer = MangaRestorer(isSync), // SY --> private val savedSearchRestorer: SavedSearchRestorer = SavedSearchRestorer(), // SY <-- diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/MangaRestorer.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/MangaRestorer.kt index 855a59a4e..0c9ba8524 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/MangaRestorer.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/MangaRestorer.kt @@ -33,6 +33,8 @@ import java.util.Date import kotlin.math.max class MangaRestorer( + private var isSync: Boolean = false, + private val handler: DatabaseHandler = Injekt.get(), private val getCategories: GetCategories = Injekt.get(), private val getMangaByUrlAndSourceId: GetMangaByUrlAndSourceId = Injekt.get(), @@ -47,7 +49,6 @@ class MangaRestorer( private val getFlatMetadataById: GetFlatMetadataById = Injekt.get(), // SY <-- ) { - private var now = ZonedDateTime.now() private var currentFetchWindow = fetchInterval.getWindow(now) @@ -97,6 +98,11 @@ class MangaRestorer( customManga = backupManga.getCustomMangaInfo(), // SY <-- ) + + if (isSync) { + mangasQueries.resetIsSyncing() + chaptersQueries.resetIsSyncing() + } } } @@ -128,7 +134,7 @@ class MangaRestorer( ) } - private suspend fun updateManga(manga: Manga): Manga { + suspend fun updateManga(manga: Manga): Manga { handler.await(true) { mangasQueries.update( source = manga.source, @@ -173,36 +179,15 @@ class MangaRestorer( .associateBy { it.url } val (existingChapters, newChapters) = backupChapters - .mapNotNull { - val chapter = it.toChapterImpl().copy(mangaId = manga.id) - + .mapNotNull { backupChapter -> + val chapter = backupChapter.toChapterImpl().copy(mangaId = manga.id) val dbChapter = dbChaptersByUrl[chapter.url] - ?: // New chapter - return@mapNotNull chapter - if (chapter.forComparison() == dbChapter.forComparison()) { - // Same state; skip - return@mapNotNull null + when { + dbChapter == null -> chapter // New chapter + chapter.forComparison() == dbChapter.forComparison() -> null // Same state; skip + else -> updateChapterBasedOnSyncState(chapter, dbChapter) } - - // Update to an existing chapter - var updatedChapter = chapter - .copyFrom(dbChapter) - .copy( - id = dbChapter.id, - bookmark = chapter.bookmark || dbChapter.bookmark, - ) - if (dbChapter.read && !updatedChapter.read) { - updatedChapter = updatedChapter.copy( - read = true, - lastPageRead = dbChapter.lastPageRead, - ) - } else if (updatedChapter.lastPageRead == 0L && dbChapter.lastPageRead != 0L) { - updatedChapter = updatedChapter.copy( - lastPageRead = dbChapter.lastPageRead, - ) - } - updatedChapter } .partition { it.id > 0 } @@ -210,6 +195,27 @@ class MangaRestorer( updateExistingChapters(existingChapters) } + private fun updateChapterBasedOnSyncState(chapter: Chapter, dbChapter: Chapter): Chapter { + return if (isSync) { + chapter.copy( + id = dbChapter.id, + bookmark = chapter.bookmark || dbChapter.bookmark, + read = chapter.read, + lastPageRead = chapter.lastPageRead, + ) + } else { + chapter.copyFrom(dbChapter).let { + when { + dbChapter.read && !it.read -> it.copy(read = true, lastPageRead = dbChapter.lastPageRead) + it.lastPageRead == 0L && dbChapter.lastPageRead != 0L -> it.copy( + lastPageRead = dbChapter.lastPageRead, + ) + else -> it + } + } + } + } + private fun Chapter.forComparison() = this.copy(id = 0L, mangaId = 0L, dateFetch = 0L, dateUpload = 0L, lastModifiedAt = 0L, version = 0L) @@ -251,7 +257,7 @@ class MangaRestorer( dateUpload = null, chapterId = chapter.id, version = chapter.version, - isSyncing = 0, + isSyncing = 1, ) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt index 658464d82..76322eefa 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt @@ -21,11 +21,13 @@ import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource import eu.kanade.domain.manga.interactor.UpdateManga import eu.kanade.domain.manga.model.copyFrom import eu.kanade.domain.manga.model.toSManga +import eu.kanade.domain.sync.SyncPreferences import eu.kanade.domain.track.model.toDbTrack import eu.kanade.domain.track.model.toDomainTrack import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.notification.Notifications +import eu.kanade.tachiyomi.data.sync.SyncDataJob import eu.kanade.tachiyomi.data.track.TrackStatus import eu.kanade.tachiyomi.data.track.TrackerManager import eu.kanade.tachiyomi.source.model.SManga @@ -801,6 +803,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet // SY <-- ): Boolean { val wm = context.workManager + // Check if the LibraryUpdateJob is already running if (wm.isRunning(TAG)) { // Already running either as a scheduled or manual job return false @@ -821,6 +824,45 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet .build() wm.enqueueUniqueWork(WORK_NAME_MANUAL, ExistingWorkPolicy.KEEP, request) + val syncPreferences: SyncPreferences = Injekt.get() + + // Only proceed with SyncDataJob if sync is enabled and the specific sync on library update flag is set + val syncTriggerOpt = syncPreferences.getSyncTriggerOptions() + if (syncPreferences.isSyncEnabled() && syncTriggerOpt.syncOnLibraryUpdate + ) { + // Check if SyncDataJob is already running + if (wm.isRunning(SyncDataJob.TAG_MANUAL)) { + // SyncDataJob is already running + return false + } + + // Define the SyncDataJob + val syncDataJob = OneTimeWorkRequestBuilder() + .addTag(SyncDataJob.TAG_MANUAL) + .build() + + // Chain SyncDataJob to run before LibraryUpdateJob + val inputData = workDataOf(KEY_CATEGORY to category?.id) + val libraryUpdateJob = OneTimeWorkRequestBuilder() + .addTag(TAG) + .addTag(WORK_NAME_MANUAL) + .setInputData(inputData) + .build() + + wm.beginUniqueWork(WORK_NAME_MANUAL, ExistingWorkPolicy.KEEP, syncDataJob) + .then(libraryUpdateJob) + .enqueue() + } else { + val inputData = workDataOf(KEY_CATEGORY to category?.id) + val request = OneTimeWorkRequestBuilder() + .addTag(TAG) + .addTag(WORK_NAME_MANUAL) + .setInputData(inputData) + .build() + + wm.enqueueUniqueWork(WORK_NAME_MANUAL, ExistingWorkPolicy.KEEP, request) + } + return true } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt b/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt index df4ca6c97..cd4908141 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt @@ -10,6 +10,7 @@ import androidx.core.net.toUri import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.library.LibraryUpdateJob +import eu.kanade.tachiyomi.data.sync.SyncDataJob import eu.kanade.tachiyomi.data.updater.AppUpdateDownloadJob import eu.kanade.tachiyomi.ui.main.MainActivity import eu.kanade.tachiyomi.ui.reader.ReaderActivity @@ -71,6 +72,8 @@ class NotificationReceiver : BroadcastReceiver() { "application/x-protobuf+gzip", ) ACTION_CANCEL_RESTORE -> cancelRestore(context) + + ACTION_CANCEL_SYNC -> cancelSync(context) // Cancel library update and dismiss notification ACTION_CANCEL_LIBRARY_UPDATE -> cancelLibraryUpdate(context) // Start downloading app update @@ -188,6 +191,15 @@ class NotificationReceiver : BroadcastReceiver() { AppUpdateDownloadJob.stop(context) } + /** + * Method called when user wants to stop a backup restore job. + * + * @param context context of application + */ + private fun cancelSync(context: Context) { + SyncDataJob.stop(context) + } + /** * Method called when user wants to mark manga chapters as read * @@ -240,6 +252,8 @@ class NotificationReceiver : BroadcastReceiver() { private const val ACTION_CANCEL_RESTORE = "$ID.$NAME.CANCEL_RESTORE" + private const val ACTION_CANCEL_SYNC = "$ID.$NAME.CANCEL_SYNC" + private const val ACTION_CANCEL_LIBRARY_UPDATE = "$ID.$NAME.CANCEL_LIBRARY_UPDATE" private const val ACTION_START_APP_UPDATE = "$ID.$NAME.ACTION_START_APP_UPDATE" @@ -618,5 +632,25 @@ class NotificationReceiver : BroadcastReceiver() { PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, ) } + + /** + * Returns [PendingIntent] that cancels a sync restore job. + * + * @param context context of application + * @param notificationId id of notification + * @return [PendingIntent] + */ + internal fun cancelSyncPendingBroadcast(context: Context, notificationId: Int): PendingIntent { + val intent = Intent(context, NotificationReceiver::class.java).apply { + action = ACTION_CANCEL_SYNC + putExtra(EXTRA_NOTIFICATION_ID, notificationId) + } + return PendingIntent.getBroadcast( + context, + 0, + intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, + ) + } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/sync/SyncDataJob.kt b/app/src/main/java/eu/kanade/tachiyomi/data/sync/SyncDataJob.kt new file mode 100644 index 000000000..e1f949029 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/sync/SyncDataJob.kt @@ -0,0 +1,102 @@ +package eu.kanade.tachiyomi.data.sync + +import android.content.Context +import android.content.pm.ServiceInfo +import android.os.Build +import androidx.work.CoroutineWorker +import androidx.work.ExistingPeriodicWorkPolicy +import androidx.work.ExistingWorkPolicy +import androidx.work.ForegroundInfo +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.PeriodicWorkRequestBuilder +import androidx.work.WorkerParameters +import eu.kanade.domain.sync.SyncPreferences +import eu.kanade.tachiyomi.data.notification.Notifications +import eu.kanade.tachiyomi.util.system.cancelNotification +import eu.kanade.tachiyomi.util.system.isRunning +import eu.kanade.tachiyomi.util.system.workManager +import logcat.LogPriority +import tachiyomi.core.common.util.system.logcat +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get +import java.util.concurrent.TimeUnit + +class SyncDataJob(private val context: Context, workerParams: WorkerParameters) : + CoroutineWorker(context, workerParams) { + + private val notifier = SyncNotifier(context) + + override suspend fun doWork(): Result { + try { + setForeground(getForegroundInfo()) + } catch (e: IllegalStateException) { + logcat(LogPriority.ERROR, e) { "Not allowed to run on foreground service" } + } + + return try { + SyncManager(context).syncData() + Result.success() + } catch (e: Exception) { + logcat(LogPriority.ERROR, e) + notifier.showSyncError(e.message) + Result.failure() + } finally { + context.cancelNotification(Notifications.ID_RESTORE_PROGRESS) + } + } + + override suspend fun getForegroundInfo(): ForegroundInfo { + return ForegroundInfo( + Notifications.ID_RESTORE_PROGRESS, + notifier.showSyncProgress().build(), + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC + } else { + 0 + }, + ) + } + + companion object { + private const val TAG_JOB = "SyncDataJob" + private const val TAG_AUTO = "$TAG_JOB:auto" + const val TAG_MANUAL = "$TAG_JOB:manual" + + private val jobTagList = listOf(TAG_AUTO, TAG_MANUAL) + + fun isAnyJobRunning(context: Context): Boolean { + return jobTagList.any { context.workManager.isRunning(it) } + } + + fun setupTask(context: Context, prefInterval: Int? = null) { + val syncPreferences = Injekt.get() + val interval = prefInterval ?: syncPreferences.syncInterval().get() + + if (interval > 0) { + val request = PeriodicWorkRequestBuilder( + interval.toLong(), + TimeUnit.MINUTES, + 10, + TimeUnit.MINUTES, + ) + .addTag(TAG_AUTO) + .build() + + context.workManager.enqueueUniquePeriodicWork(TAG_AUTO, ExistingPeriodicWorkPolicy.UPDATE, request) + } else { + context.workManager.cancelUniqueWork(TAG_AUTO) + } + } + + fun startNow(context: Context) { + val request = OneTimeWorkRequestBuilder() + .addTag(TAG_MANUAL) + .build() + context.workManager.enqueueUniqueWork(TAG_MANUAL, ExistingWorkPolicy.KEEP, request) + } + + fun stop(context: Context) { + context.workManager.cancelUniqueWork(TAG_MANUAL) + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/sync/SyncManager.kt b/app/src/main/java/eu/kanade/tachiyomi/data/sync/SyncManager.kt new file mode 100644 index 000000000..dba8b4af5 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/sync/SyncManager.kt @@ -0,0 +1,327 @@ +package eu.kanade.tachiyomi.data.sync + +import android.content.Context +import android.net.Uri +import eu.kanade.domain.sync.SyncPreferences +import eu.kanade.tachiyomi.data.backup.create.BackupCreator +import eu.kanade.tachiyomi.data.backup.create.BackupOptions +import eu.kanade.tachiyomi.data.backup.models.Backup +import eu.kanade.tachiyomi.data.backup.models.BackupChapter +import eu.kanade.tachiyomi.data.backup.models.BackupManga +import eu.kanade.tachiyomi.data.backup.models.BackupSerializer +import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob +import eu.kanade.tachiyomi.data.backup.restore.RestoreOptions +import eu.kanade.tachiyomi.data.backup.restore.restorers.MangaRestorer +import eu.kanade.tachiyomi.data.sync.service.GoogleDriveSyncService +import eu.kanade.tachiyomi.data.sync.service.SyncData +import eu.kanade.tachiyomi.data.sync.service.SyncYomiSyncService +import kotlinx.serialization.json.Json +import kotlinx.serialization.protobuf.ProtoBuf +import logcat.LogPriority +import logcat.logcat +import tachiyomi.data.Chapters +import tachiyomi.data.DatabaseHandler +import tachiyomi.data.manga.MangaMapper.mapManga +import tachiyomi.domain.category.interactor.GetCategories +import tachiyomi.domain.manga.model.Manga +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get +import java.io.File +import java.io.IOException +import java.util.Date +import kotlin.system.measureTimeMillis + +/** + * A manager to handle synchronization tasks in the app, such as updating + * sync preferences and performing synchronization with a remote server. + * + * @property context The application context. + */ +class SyncManager( + private val context: Context, + private val handler: DatabaseHandler = Injekt.get(), + private val syncPreferences: SyncPreferences = Injekt.get(), + private var json: Json = Json { + encodeDefaults = true + ignoreUnknownKeys = true + }, + private val getCategories: GetCategories = Injekt.get(), +) { + private val backupCreator: BackupCreator = BackupCreator(context, false) + private val notifier: SyncNotifier = SyncNotifier(context) + private val mangaRestorer: MangaRestorer = MangaRestorer() + + enum class SyncService(val value: Int) { + NONE(0), + SYNCYOMI(1), + GOOGLE_DRIVE(2), + ; + + companion object { + fun fromInt(value: Int) = entries.firstOrNull { it.value == value } ?: NONE + } + } + + /** + * Syncs data with a sync service. + * + * This function retrieves local data (favorites, manga, extensions, and categories) + * from the database using the BackupManager, then synchronizes the data with a sync service. + */ + suspend fun syncData() { + // Reset isSyncing in case it was left over or failed syncing during restore. + handler.await(inTransaction = true) { + mangasQueries.resetIsSyncing() + chaptersQueries.resetIsSyncing() + } + + val syncOptions = syncPreferences.getSyncSettings() + val databaseManga = getAllMangaThatNeedsSync() + + val backupOptions = BackupOptions( + libraryEntries = syncOptions.libraryEntries, + categories = syncOptions.categories, + chapters = syncOptions.chapters, + tracking = syncOptions.tracking, + history = syncOptions.history, + appSettings = syncOptions.appSettings, + sourceSettings = syncOptions.sourceSettings, + privateSettings = syncOptions.privateSettings, + ) + val backup = Backup( + backupManga = backupCreator.backupMangas(databaseManga, backupOptions), + backupCategories = backupCreator.backupCategories(backupOptions), + backupSources = backupCreator.backupSources(databaseManga), + backupPreferences = backupCreator.backupAppPreferences(backupOptions), + backupSourcePreferences = backupCreator.backupSourcePreferences(backupOptions), + + // SY --> + backupSavedSearches = backupCreator.backupSavedSearches(), + // SY <-- + ) + + // Create the SyncData object + val syncData = SyncData( + backup = backup, + ) + + // Handle sync based on the selected service + val syncService = when (val syncService = SyncService.fromInt(syncPreferences.syncService().get())) { + SyncService.SYNCYOMI -> { + SyncYomiSyncService( + context, + json, + syncPreferences, + notifier, + ) + } + + SyncService.GOOGLE_DRIVE -> { + GoogleDriveSyncService(context, json, syncPreferences) + } + + else -> { + logcat(LogPriority.ERROR) { "Invalid sync service type: $syncService" } + null + } + } + + val remoteBackup = syncService?.doSync(syncData) + + // Stop the sync early if the remote backup is null or empty + if (remoteBackup?.backupManga?.size == 0) { + notifier.showSyncError("No data found on remote server.") + return + } + + // Check if it's first sync based on lastSyncTimestamp + if (syncPreferences.lastSyncTimestamp().get() == 0L && databaseManga.isNotEmpty()) { + // It's first sync no need to restore data. (just update remote data) + syncPreferences.lastSyncTimestamp().set(Date().time) + notifier.showSyncSuccess("Updated remote data successfully") + return + } + + if (remoteBackup != null) { + val (filteredFavorites, nonFavorites) = filterFavoritesAndNonFavorites(remoteBackup) + updateNonFavorites(nonFavorites) + + val newSyncData = backup.copy( + backupManga = filteredFavorites, + backupCategories = remoteBackup.backupCategories, + backupSources = remoteBackup.backupSources, + backupPreferences = remoteBackup.backupPreferences, + backupSourcePreferences = remoteBackup.backupSourcePreferences, + + // SY --> + backupSavedSearches = remoteBackup.backupSavedSearches, + // 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, + ), + ) + + // update the sync timestamp + syncPreferences.lastSyncTimestamp().set(Date().time) + } else { + logcat(LogPriority.ERROR) { "Failed to write sync data to file" } + } + } + } + + private fun writeSyncDataToCache(context: Context, backup: Backup): Uri? { + val cacheFile = File(context.cacheDir, "tachiyomi_sync_data.proto.gz") + return try { + cacheFile.outputStream().use { output -> + output.write(ProtoBuf.encodeToByteArray(BackupSerializer, backup)) + Uri.fromFile(cacheFile) + } + } catch (e: IOException) { + logcat(LogPriority.ERROR) { "Failed to write sync data to cache" } + null + } + } + + /** + * Retrieves all manga from the local database. + * + * @return a list of all manga stored in the database + */ + private suspend fun getAllMangaFromDB(): List { + return handler.awaitList { mangasQueries.getAllManga(::mapManga) } + } + + private suspend fun getAllMangaThatNeedsSync(): List { + return handler.awaitList { mangasQueries.getMangasWithFavoriteTimestamp(::mapManga) } + } + + private suspend fun isMangaDifferent(localManga: Manga, remoteManga: BackupManga): Boolean { + val localChapters = handler.await { chaptersQueries.getChaptersByMangaId(localManga.id, 0).executeAsList() } + val localCategories = getCategories.await(localManga.id).map { it.order } + + if (areChaptersDifferent(localChapters, remoteManga.chapters)) { + return true + } + + if (localManga.version != remoteManga.version) { + return true + } + + if (localCategories.toSet() != remoteManga.categories.toSet()) { + return true + } + + return false + } + + private fun areChaptersDifferent(localChapters: List, remoteChapters: List): Boolean { + val localChapterMap = localChapters.associateBy { it.url } + val remoteChapterMap = remoteChapters.associateBy { it.url } + + if (localChapterMap.size != remoteChapterMap.size) { + return true + } + + for ((url, localChapter) in localChapterMap) { + val remoteChapter = remoteChapterMap[url] + + // If a matching remote chapter doesn't exist, or the version numbers are different, consider them different + if (remoteChapter == null || localChapter.version != remoteChapter.version) { + return true + } + } + + return false + } + + /** + * Filters the favorite and non-favorite manga from the backup and checks + * if the favorite manga is different from the local database. + * @param backup the Backup object containing the backup data. + * @return a Pair of lists, where the first list contains different favorite manga + * and the second list contains non-favorite manga. + */ + private suspend fun filterFavoritesAndNonFavorites(backup: Backup): Pair, List> { + val favorites = mutableListOf() + val nonFavorites = mutableListOf() + val logTag = "filterFavoritesAndNonFavorites" + + val elapsedTimeMillis = measureTimeMillis { + val databaseManga = getAllMangaFromDB() + val localMangaMap = databaseManga.associateBy { + Triple(it.source, it.url, it.title) + } + + logcat(LogPriority.DEBUG, logTag) { "Starting to filter favorites and non-favorites from backup data." } + + backup.backupManga.forEach { remoteManga -> + val compositeKey = Triple(remoteManga.source, remoteManga.url, remoteManga.title) + val localManga = localMangaMap[compositeKey] + when { + // Checks if the manga is in favorites and needs updating or adding + remoteManga.favorite -> { + if (localManga == null || isMangaDifferent(localManga, remoteManga)) { + logcat(LogPriority.DEBUG, logTag) { "Adding to favorites: ${remoteManga.title}" } + favorites.add(remoteManga) + } else { + logcat(LogPriority.DEBUG, logTag) { "Already up-to-date favorite: ${remoteManga.title}" } + } + } + // Handle non-favorites + !remoteManga.favorite -> { + logcat(LogPriority.DEBUG, logTag) { "Adding to non-favorites: ${remoteManga.title}" } + nonFavorites.add(remoteManga) + } + } + } + } + + val minutes = elapsedTimeMillis / 60000 + val seconds = (elapsedTimeMillis % 60000) / 1000 + logcat(LogPriority.DEBUG, logTag) { + "Filtering completed in ${minutes}m ${seconds}s. Favorites found: ${favorites.size}, " + + "Non-favorites found: ${nonFavorites.size}" + } + + return Pair(favorites, nonFavorites) + } + + /** + * Updates the non-favorite manga in the local database with their favorite status from the backup. + * @param nonFavorites the list of non-favorite BackupManga objects from the backup. + */ + private suspend fun updateNonFavorites(nonFavorites: List) { + val localMangaList = getAllMangaFromDB() + + val localMangaMap = localMangaList.associateBy { Triple(it.source, it.url, it.title) } + + nonFavorites.forEach { nonFavorite -> + val key = Triple(nonFavorite.source, nonFavorite.url, nonFavorite.title) + localMangaMap[key]?.let { localManga -> + if (localManga.favorite != nonFavorite.favorite) { + val updatedManga = localManga.copy(favorite = nonFavorite.favorite) + mangaRestorer.updateManga(updatedManga) + } + } + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/sync/SyncNotifier.kt b/app/src/main/java/eu/kanade/tachiyomi/data/sync/SyncNotifier.kt new file mode 100644 index 000000000..2321240a6 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/sync/SyncNotifier.kt @@ -0,0 +1,86 @@ +package eu.kanade.tachiyomi.data.sync + +import android.content.Context +import android.graphics.BitmapFactory +import androidx.core.app.NotificationCompat +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.core.security.SecurityPreferences +import eu.kanade.tachiyomi.data.notification.NotificationReceiver +import eu.kanade.tachiyomi.data.notification.Notifications +import eu.kanade.tachiyomi.util.system.cancelNotification +import eu.kanade.tachiyomi.util.system.notificationBuilder +import eu.kanade.tachiyomi.util.system.notify +import uy.kohesive.injekt.injectLazy + +class SyncNotifier(private val context: Context) { + + private val preferences: SecurityPreferences by injectLazy() + + private val progressNotificationBuilder = context.notificationBuilder( + Notifications.CHANNEL_BACKUP_RESTORE_PROGRESS, + ) { + setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher)) + setSmallIcon(R.drawable.ic_tachi) + setAutoCancel(false) + setOngoing(true) + setOnlyAlertOnce(true) + } + + private val completeNotificationBuilder = context.notificationBuilder( + Notifications.CHANNEL_BACKUP_RESTORE_PROGRESS, + ) { + setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher)) + setSmallIcon(R.drawable.ic_tachi) + setAutoCancel(false) + } + + private fun NotificationCompat.Builder.show(id: Int) { + context.notify(id, build()) + } + + fun showSyncProgress(content: String = "", progress: Int = 0, maxAmount: Int = 100): NotificationCompat.Builder { + val builder = with(progressNotificationBuilder) { + setContentTitle(context.getString(R.string.syncing_library)) + + if (!preferences.hideNotificationContent().get()) { + setContentText(content) + } + + setProgress(maxAmount, progress, true) + setOnlyAlertOnce(true) + + clearActions() + addAction( + R.drawable.ic_close_24dp, + context.getString(R.string.action_cancel), + NotificationReceiver.cancelSyncPendingBroadcast(context, Notifications.ID_RESTORE_PROGRESS), + ) + } + + builder.show(Notifications.ID_RESTORE_PROGRESS) + + return builder + } + + fun showSyncError(error: String?) { + context.cancelNotification(Notifications.ID_RESTORE_PROGRESS) + + with(completeNotificationBuilder) { + setContentTitle(context.getString(R.string.sync_error)) + setContentText(error) + + show(Notifications.ID_RESTORE_COMPLETE) + } + } + + fun showSyncSuccess(message: String?) { + context.cancelNotification(Notifications.ID_RESTORE_PROGRESS) + + with(completeNotificationBuilder) { + setContentTitle(context.getString(R.string.sync_complete)) + setContentText(message) + + show(Notifications.ID_RESTORE_COMPLETE) + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/sync/models/SyncTriggerOptions.kt b/app/src/main/java/eu/kanade/tachiyomi/data/sync/models/SyncTriggerOptions.kt new file mode 100644 index 000000000..b5dfaf001 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/sync/models/SyncTriggerOptions.kt @@ -0,0 +1,72 @@ +package eu.kanade.tachiyomi.data.sync.models + +import dev.icerock.moko.resources.StringResource +import kotlinx.collections.immutable.persistentListOf +import tachiyomi.i18n.MR + +data class SyncTriggerOptions( + val syncOnChapterRead: Boolean = false, + val syncOnChapterOpen: Boolean = false, + val syncOnAppStart: Boolean = false, + val syncOnAppResume: Boolean = false, + val syncOnLibraryUpdate: Boolean = false, +) { + fun asBooleanArray() = booleanArrayOf( + syncOnChapterRead, + syncOnChapterOpen, + syncOnAppStart, + syncOnAppResume, + syncOnLibraryUpdate, + ) + + fun anyEnabled() = syncOnChapterRead || + syncOnChapterOpen || + syncOnAppStart || + syncOnAppResume || + syncOnLibraryUpdate + + companion object { + val mainOptions = persistentListOf( + Entry( + label = MR.strings.sync_on_chapter_read, + getter = SyncTriggerOptions::syncOnChapterRead, + setter = { options, enabled -> options.copy(syncOnChapterRead = enabled) }, + ), + Entry( + label = MR.strings.sync_on_chapter_open, + getter = SyncTriggerOptions::syncOnChapterOpen, + setter = { options, enabled -> options.copy(syncOnChapterOpen = enabled) }, + ), + Entry( + label = MR.strings.sync_on_app_start, + getter = SyncTriggerOptions::syncOnAppStart, + setter = { options, enabled -> options.copy(syncOnAppStart = enabled) }, + ), + Entry( + label = MR.strings.sync_on_app_resume, + getter = SyncTriggerOptions::syncOnAppResume, + setter = { options, enabled -> options.copy(syncOnAppResume = enabled) }, + ), + Entry( + label = MR.strings.sync_on_library_update, + getter = SyncTriggerOptions::syncOnLibraryUpdate, + setter = { options, enabled -> options.copy(syncOnLibraryUpdate = enabled) }, + ), + ) + + fun fromBooleanArray(array: BooleanArray) = SyncTriggerOptions( + syncOnChapterRead = array[0], + syncOnChapterOpen = array[1], + syncOnAppStart = array[2], + syncOnAppResume = array[3], + syncOnLibraryUpdate = array[4], + ) + } + + data class Entry( + val label: StringResource, + val getter: (SyncTriggerOptions) -> Boolean, + val setter: (SyncTriggerOptions, Boolean) -> SyncTriggerOptions, + val enabled: (SyncTriggerOptions) -> Boolean = { true }, + ) +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/sync/service/GoogleDriveSyncService.kt b/app/src/main/java/eu/kanade/tachiyomi/data/sync/service/GoogleDriveSyncService.kt new file mode 100644 index 000000000..7beb6d512 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/sync/service/GoogleDriveSyncService.kt @@ -0,0 +1,522 @@ +package eu.kanade.tachiyomi.data.sync.service + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.util.Log +import com.google.api.client.auth.oauth2.TokenResponseException +import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeFlow +import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeTokenRequest +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.GoogleTokenResponse +import com.google.api.client.http.ByteArrayContent +import com.google.api.client.http.javanet.NetHttpTransport +import com.google.api.client.json.JsonFactory +import com.google.api.client.json.jackson2.JacksonFactory +import com.google.api.services.drive.Drive +import com.google.api.services.drive.DriveScopes +import com.google.api.services.drive.model.File +import eu.kanade.domain.sync.SyncPreferences +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.withContext +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import logcat.LogPriority +import logcat.logcat +import tachiyomi.core.common.i18n.stringResource +import tachiyomi.i18n.MR +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.IOException +import java.io.InputStreamReader +import java.time.Instant +import java.util.zip.GZIPInputStream +import java.util.zip.GZIPOutputStream + +class GoogleDriveSyncService(context: Context, json: Json, syncPreferences: SyncPreferences) : SyncService( + context, + json, + syncPreferences, +) { + constructor(context: Context) : this( + context, + Json { + encodeDefaults = true + ignoreUnknownKeys = true + }, + Injekt.get(), + ) + + enum class DeleteSyncDataStatus { + NOT_INITIALIZED, + NO_FILES, + SUCCESS, + ERROR, + } + + private val appName = context.stringResource(MR.strings.app_name) + + private val remoteFileName = "${appName}_sync_data.gz" + + private val lockFileName = "${appName}_sync.lock" + + private val googleDriveService = GoogleDriveService(context) + + override suspend fun beforeSync() { + try { + googleDriveService.refreshToken() + val drive = googleDriveService.driveService + ?: throw Exception(context.stringResource(MR.strings.google_drive_not_signed_in)) + + var backoff = 1000L + var retries = 0 // Retry counter + val maxRetries = 10 // Maximum number of retries + + while (retries < maxRetries) { + val lockFiles = findLockFile(drive) + logcat(LogPriority.DEBUG) { "Found ${lockFiles.size} lock file(s)" } + + when { + lockFiles.isEmpty() -> { + logcat(LogPriority.DEBUG) { "No lock file found, creating a new one" } + createLockFile(drive) + break + } + lockFiles.size == 1 -> { + val lockFile = lockFiles.first() + val createdTime = Instant.parse(lockFile.createdTime.toString()) + val ageMinutes = java.time.Duration.between(createdTime, Instant.now()).toMinutes() + logcat(LogPriority.DEBUG) { "Lock file age: $ageMinutes minutes" } + if (ageMinutes <= 3) { + logcat(LogPriority.DEBUG) { "Lock file is new, proceeding with sync" } + break + } else { + logcat(LogPriority.DEBUG) { "Lock file is old, deleting and creating a new one" } + deleteLockFile(drive) + createLockFile(drive) + break + } + } + else -> { + logcat(LogPriority.DEBUG) { "Multiple lock files found, applying backoff" } + delay(backoff) // Apply backoff strategy + backoff = (backoff * 2).coerceAtMost(16000L) + logcat(LogPriority.DEBUG) { "Backoff increased to $backoff milliseconds" } + } + } + retries++ // Increment retry counter + logcat(LogPriority.DEBUG) { "Loop iteration complete, retry count: $retries, backoff time: $backoff" } + } + + if (retries >= maxRetries) { + logcat(LogPriority.ERROR) { "Max retries reached, exiting sync process" } + throw Exception(context.stringResource(MR.strings.error_before_sync_gdrive) + ": Max retries reached.") + } + } catch (e: Exception) { + logcat(LogPriority.ERROR) { "Error in GoogleDrive beforeSync: ${e.message}" } + throw Exception(context.stringResource(MR.strings.error_before_sync_gdrive) + ": ${e.message}") + } + } + + override suspend fun pullSyncData(): SyncData? { + val drive = googleDriveService.driveService + + 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) + if (fileList.isEmpty()) { + logcat(LogPriority.INFO) { "No files found in app data" } + return null + } + + val gdriveFileId = fileList[0].id + logcat(LogPriority.DEBUG) { "Google Drive File ID: $gdriveFileId" } + + val outputStream = ByteArrayOutputStream() + try { + drive.files().get(gdriveFileId).executeMediaAndDownloadTo(outputStream) + logcat(LogPriority.DEBUG) { "File downloaded successfully" } + } catch (e: Exception) { + logcat(LogPriority.ERROR) { "Error downloading file: ${e.message}" } + return null + } + + return withContext(Dispatchers.IO) { + try { + val gzipInputStream = GZIPInputStream(ByteArrayInputStream(outputStream.toByteArray())) + val jsonString = gzipInputStream.bufferedReader(Charsets.UTF_8).use { it.readText() } + val syncData = json.decodeFromString(SyncData.serializer(), jsonString) + logcat(LogPriority.DEBUG) { "JSON deserialized successfully" } + syncData + } catch (e: Exception) { + logcat( + LogPriority.ERROR, + ) { "Failed to convert json to sync data with kotlinx.serialization: ${e.message}" } + throw Exception(e.message) + } + } + } + + override suspend fun pushSyncData(syncData: SyncData) { + val jsonData = json.encodeToString(syncData) + val drive = googleDriveService.driveService + ?: throw Exception(context.stringResource(MR.strings.google_drive_not_signed_in)) + + val fileList = getAppDataFileList(drive) + val byteArrayOutputStream = ByteArrayOutputStream() + withContext(Dispatchers.IO) { + GZIPOutputStream(byteArrayOutputStream).use { gzipOutputStream -> + gzipOutputStream.write(jsonData.toByteArray(Charsets.UTF_8)) + } + logcat(LogPriority.DEBUG) { "JSON serialized successfully" } + } + + val byteArrayContent = ByteArrayContent("application/octet-stream", byteArrayOutputStream.toByteArray()) + + try { + if (fileList.isNotEmpty()) { + // File exists, so update it + val fileId = fileList[0].id + drive.files().update(fileId, null, byteArrayContent).execute() + logcat(LogPriority.DEBUG) { "Updated existing sync data file in Google Drive with file ID: $fileId" } + } else { + // File doesn't exist, so create it + val fileMetadata = File().apply { + name = remoteFileName + mimeType = "application/gzip" + parents = listOf("appDataFolder") + } + 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) { "Failed to push or update sync data: ${e.message}" } + throw Exception(context.stringResource(MR.strings.error_uploading_sync_data) + ": ${e.message}") + } + } + + private fun getAppDataFileList(drive: Drive): MutableList { + try { + // Search for the existing file by name in the appData folder + val query = "mimeType='application/gzip' and name = '$remoteFileName'" + val fileList = drive.files() + .list() + .setSpaces("appDataFolder") + .setQ(query) + .setFields("files(id, name, createdTime)") + .execute() + .files + Log.d("GoogleDrive", "AppData folder file list: $fileList") + + return fileList + } catch (e: Exception) { + Log.e("GoogleDrive", "Error no sync data found in appData folder: ${e.message}") + return mutableListOf() + } + } + + private fun createLockFile(drive: Drive) { + try { + val fileMetadata = File().apply { + name = lockFileName + mimeType = "text/plain" + parents = listOf("appDataFolder") + } + + // Create an empty content to upload as the lock file + val emptyContent = ByteArrayContent.fromString("text/plain", "") + + val file = drive.files().create(fileMetadata, emptyContent) + .setFields("id, name, createdTime") + .execute() + + Log.d("GoogleDrive", "Created lock file with ID: ${file.id}") + } catch (e: Exception) { + Log.e("GoogleDrive", "Error creating lock file: ${e.message}") + throw Exception(e.message) + } + } + + private fun findLockFile(drive: Drive): MutableList { + try { + val query = "mimeType='text/plain' and name = '$lockFileName'" + val fileList = drive.files() + .list() + .setSpaces("appDataFolder") + .setQ(query) + .setFields("files(id, name, createdTime)") + .execute().files + Log.d("GoogleDrive", "Lock file search result: $fileList") + return fileList + } catch (e: Exception) { + Log.e("GoogleDrive", "Error finding lock file: ${e.message}") + return mutableListOf() + } + } + + private fun deleteLockFile(drive: Drive) { + try { + val lockFiles = findLockFile(drive) + + if (lockFiles.isNotEmpty()) { + for (file in lockFiles) { + drive.files().delete(file.id).execute() + Log.d("GoogleDrive", "Deleted lock file with ID: ${file.id}") + } + } else { + Log.d("GoogleDrive", "No lock file found to delete.") + } + } catch (e: Exception) { + Log.e("GoogleDrive", "Error deleting lock file: ${e.message}") + throw Exception(context.stringResource(MR.strings.error_deleting_google_drive_lock_file)) + } + } + + suspend fun deleteSyncDataFromGoogleDrive(): DeleteSyncDataStatus { + val drive = googleDriveService.driveService + + if (drive == null) { + logcat(LogPriority.ERROR) { "Google Drive service not initialized" } + return DeleteSyncDataStatus.NOT_INITIALIZED + } + googleDriveService.refreshToken() + + return withContext(Dispatchers.IO) { + try { + val appDataFileList = getAppDataFileList(drive) + + if (appDataFileList.isEmpty()) { + logcat(LogPriority.DEBUG) { "No sync data file found in appData folder of Google Drive" } + DeleteSyncDataStatus.NO_FILES + } else { + for (file in appDataFileList) { + drive.files().delete(file.id).execute() + logcat( + LogPriority.DEBUG, + ) { "Deleted sync data file in appData folder of Google Drive with file ID: ${file.id}" } + } + DeleteSyncDataStatus.SUCCESS + } + } catch (e: Exception) { + logcat(LogPriority.ERROR) { "Error occurred while interacting with Google Drive: ${e.message}" } + DeleteSyncDataStatus.ERROR + } + } + } +} + +class GoogleDriveService(private val context: Context) { + var driveService: Drive? = null + companion object { + const val REDIRECT_URI = "eu.kanade.google.oauth:/oauth2redirect" + } + private val syncPreferences = Injekt.get() + + init { + initGoogleDriveService() + } + + /** + * Initializes the Google Drive service by obtaining the access token and refresh token from the SyncPreferences + * and setting up the service using the obtained tokens. + */ + private fun initGoogleDriveService() { + val accessToken = syncPreferences.googleDriveAccessToken().get() + val refreshToken = syncPreferences.googleDriveRefreshToken().get() + + if (accessToken == "" || refreshToken == "") { + driveService = null + return + } + + setupGoogleDriveService(accessToken, refreshToken) + } + + /** + * Launches an Intent to open the user's default browser for Google Drive sign-in. + * The Intent carries the authorization URL, which prompts the user to sign in + * and grant the application permission to access their Google Drive account. + * @return An Intent configured to launch a browser for Google Drive OAuth sign-in. + */ + fun getSignInIntent(): Intent { + val authorizationUrl = generateAuthorizationUrl() + + return Intent(Intent.ACTION_VIEW).apply { + data = Uri.parse(authorizationUrl) + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + } + + /** + * Generates the authorization URL required for the user to grant the application + * permission to access their Google Drive account. + * Sets the approval prompt to "force" to ensure that the user is always prompted to grant access, + * even if they have previously granted access. + * @return The authorization URL. + */ + private fun generateAuthorizationUrl(): String { + val jsonFactory: JsonFactory = JacksonFactory.getDefaultInstance() + val secrets = GoogleClientSecrets.load( + jsonFactory, + InputStreamReader(context.assets.open("client_secrets.json")), + ) + + val flow = GoogleAuthorizationCodeFlow.Builder( + NetHttpTransport(), + jsonFactory, + secrets, + listOf(DriveScopes.DRIVE_FILE, DriveScopes.DRIVE_APPDATA), + ).setAccessType("offline").build() + + return flow.newAuthorizationUrl() + .setRedirectUri(REDIRECT_URI) + .setApprovalPrompt("force") + .build() + } + internal suspend fun refreshToken() = withContext(Dispatchers.IO) { + val refreshToken = syncPreferences.googleDriveRefreshToken().get() + val accessToken = syncPreferences.googleDriveAccessToken().get() + + val jsonFactory: JsonFactory = JacksonFactory.getDefaultInstance() + val secrets = GoogleClientSecrets.load( + jsonFactory, + InputStreamReader(context.assets.open("client_secrets.json")), + ) + + val credential = GoogleCredential.Builder() + .setJsonFactory(jsonFactory) + .setTransport(NetHttpTransport()) + .setClientSecrets(secrets) + .build() + + if (refreshToken == "") { + throw Exception(context.stringResource(MR.strings.google_drive_not_signed_in)) + } + + credential.refreshToken = refreshToken + + logcat(LogPriority.DEBUG) { "Refreshing access token with: $refreshToken" } + + try { + credential.refreshToken() + val newAccessToken = credential.accessToken + // Save the new access token + syncPreferences.googleDriveAccessToken().set(newAccessToken) + setupGoogleDriveService(newAccessToken, credential.refreshToken) + logcat(LogPriority.DEBUG) { "Google Access token refreshed old: $accessToken new: $newAccessToken" } + } catch (e: TokenResponseException) { + if (e.details.error == "invalid_grant") { + // The refresh token is invalid, prompt the user to sign in again + logcat(LogPriority.ERROR) { "Refresh token is invalid, prompt user to sign in again" } + throw e.message?.let { Exception(it) } ?: Exception("Unknown error") + } else { + // Token refresh failed; handle this situation + logcat(LogPriority.ERROR) { "Failed to refresh access token ${e.message}" } + logcat(LogPriority.ERROR) { "Google Drive sync will be disabled" } + throw e.message?.let { Exception(it) } ?: Exception("Unknown error") + } + } catch (e: IOException) { + // Token refresh failed; handle this situation + logcat(LogPriority.ERROR) { "Failed to refresh access token ${e.message}" } + logcat(LogPriority.ERROR) { "Google Drive sync will be disabled" } + throw e.message?.let { Exception(it) } ?: Exception("Unknown error") + } + } + + /** + * Sets up the Google Drive service using the provided access token and refresh token. + * @param accessToken The access token obtained from the SyncPreferences. + * @param refreshToken The refresh token obtained from the SyncPreferences. + */ + private fun setupGoogleDriveService(accessToken: String, refreshToken: String) { + val jsonFactory: JsonFactory = JacksonFactory.getDefaultInstance() + val secrets = GoogleClientSecrets.load( + jsonFactory, + InputStreamReader(context.assets.open("client_secrets.json")), + ) + + val credential = GoogleCredential.Builder() + .setJsonFactory(jsonFactory) + .setTransport(NetHttpTransport()) + .setClientSecrets(secrets) + .build() + + credential.accessToken = accessToken + credential.refreshToken = refreshToken + + driveService = Drive.Builder( + NetHttpTransport(), + jsonFactory, + credential, + ).setApplicationName(context.stringResource(MR.strings.app_name)) + .build() + } + + /** + * Handles the authorization code returned after the user has granted the application permission to access their Google Drive account. + * It obtains the access token and refresh token using the authorization code, saves the tokens to the SyncPreferences, + * sets up the Google Drive service using the obtained tokens, and initializes the service. + * @param authorizationCode The authorization code obtained from the OAuthCallbackServer. + * @param activity The current activity. + * @param onSuccess A callback function to be called on successful authorization. + * @param onFailure A callback function to be called on authorization failure. + */ + fun handleAuthorizationCode( + authorizationCode: String, + activity: Activity, + onSuccess: () -> Unit, + onFailure: (String) -> Unit, + ) { + val jsonFactory: JsonFactory = JacksonFactory.getDefaultInstance() + val secrets = GoogleClientSecrets.load( + jsonFactory, + InputStreamReader(context.assets.open("client_secrets.json")), + ) + + val tokenResponse: GoogleTokenResponse = GoogleAuthorizationCodeTokenRequest( + NetHttpTransport(), + jsonFactory, + secrets.installed.clientId, + secrets.installed.clientSecret, + authorizationCode, + REDIRECT_URI, + ).setGrantType("authorization_code").execute() + + try { + // Save the access token and refresh token + val accessToken = tokenResponse.accessToken + val refreshToken = tokenResponse.refreshToken + + // Save the tokens to SyncPreferences + syncPreferences.googleDriveAccessToken().set(accessToken) + syncPreferences.googleDriveRefreshToken().set(refreshToken) + + setupGoogleDriveService(accessToken, refreshToken) + initGoogleDriveService() + + activity.runOnUiThread { + onSuccess() + } + } catch (e: Exception) { + activity.runOnUiThread { + onFailure(e.localizedMessage ?: "Unknown error") + } + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/sync/service/SyncService.kt b/app/src/main/java/eu/kanade/tachiyomi/data/sync/service/SyncService.kt new file mode 100644 index 000000000..cf61869fa --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/sync/service/SyncService.kt @@ -0,0 +1,511 @@ +package eu.kanade.tachiyomi.data.sync.service + +import android.content.Context +import eu.kanade.domain.sync.SyncPreferences +import eu.kanade.tachiyomi.data.backup.models.Backup +import eu.kanade.tachiyomi.data.backup.models.BackupCategory +import eu.kanade.tachiyomi.data.backup.models.BackupChapter +import eu.kanade.tachiyomi.data.backup.models.BackupManga +import eu.kanade.tachiyomi.data.backup.models.BackupPreference +import eu.kanade.tachiyomi.data.backup.models.BackupSavedSearch +import eu.kanade.tachiyomi.data.backup.models.BackupSource +import eu.kanade.tachiyomi.data.backup.models.BackupSourcePreferences +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json +import logcat.LogPriority +import logcat.logcat + +@Serializable +data class SyncData( + val backup: Backup? = null, +) + +abstract class SyncService( + val context: Context, + val json: Json, + val syncPreferences: SyncPreferences, +) { + open 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. + * + * @param localSyncData The SData containing the local sync data. + * @param remoteSyncData The SData containing the remote sync data. + * @return The JSON string containing the merged sync data. + */ + private fun mergeSyncData(localSyncData: SyncData, remoteSyncData: SyncData): SyncData { + val mergedMangaList = mergeMangaLists(localSyncData.backup?.backupManga, remoteSyncData.backup?.backupManga) + val mergedCategoriesList = + mergeCategoriesLists(localSyncData.backup?.backupCategories, remoteSyncData.backup?.backupCategories) + + val mergedSourcesList = mergeSourcesLists(localSyncData.backup?.backupSources, remoteSyncData.backup?.backupSources) + val mergedPreferencesList = + mergePreferencesLists(localSyncData.backup?.backupPreferences, remoteSyncData.backup?.backupPreferences) + val mergedSourcePreferencesList = mergeSourcePreferencesLists( + localSyncData.backup?.backupSourcePreferences, + remoteSyncData.backup?.backupSourcePreferences, + ) + + // SY --> + val mergedSavedSearchesList = mergeSavedSearchesLists( + localSyncData.backup?.backupSavedSearches, + remoteSyncData.backup?.backupSavedSearches, + ) + // SY <-- + + + // Create the merged Backup object + val mergedBackup = Backup( + backupManga = mergedMangaList, + backupCategories = mergedCategoriesList, + backupSources = mergedSourcesList, + backupPreferences = mergedPreferencesList, + backupSourcePreferences = mergedSourcePreferencesList, + + // SY --> + backupSavedSearches = mergedSavedSearchesList, + // SY <-- + ) + + // Create the merged SData object + return SyncData( + backup = mergedBackup, + ) + } + + /** + * Merges two lists of BackupManga objects, selecting the most recent manga based on the lastModifiedAt value. + * If lastModifiedAt is null for a manga, it treats that manga as the oldest possible for comparison purposes. + * This function is designed to reconcile local and remote manga lists, ensuring the most up-to-date manga is retained. + * + * @param localMangaList The list of local BackupManga objects or null. + * @param remoteMangaList The list of remote BackupManga objects or null. + * @return A list of BackupManga objects, each representing the most recent version of the manga from either local or remote sources. + */ + private fun mergeMangaLists( + localMangaList: List?, + remoteMangaList: List?, + ): List { + val logTag = "MergeMangaLists" + + val localMangaListSafe = localMangaList.orEmpty() + val remoteMangaListSafe = remoteMangaList.orEmpty() + + logcat(LogPriority.DEBUG, logTag) { + "Starting merge. Local list size: ${localMangaListSafe.size}, Remote list size: ${remoteMangaListSafe.size}" + } + + fun mangaCompositeKey(manga: BackupManga): String { + return "${manga.source}|${manga.url}|${manga.title.lowercase().trim()}|${manga.author?.lowercase()?.trim()}" + } + + // Create maps using composite keys + val localMangaMap = localMangaListSafe.associateBy { mangaCompositeKey(it) } + val remoteMangaMap = remoteMangaListSafe.associateBy { mangaCompositeKey(it) } + + logcat(LogPriority.DEBUG, logTag) { + "Starting merge. Local list size: ${localMangaListSafe.size}, Remote list size: ${remoteMangaListSafe.size}" + } + + val mergedList = (localMangaMap.keys + remoteMangaMap.keys).distinct().mapNotNull { compositeKey -> + val local = localMangaMap[compositeKey] + val remote = remoteMangaMap[compositeKey] + + // New version comparison logic + when { + local != null && remote == null -> local + local == null && remote != null -> remote + local != null && remote != null -> { + // Compare versions to decide which manga to keep + if (local.version >= remote.version) { + logcat(LogPriority.DEBUG, logTag) { "Keeping local version of ${local.title} with merged chapters." } + local.copy(chapters = mergeChapters(local.chapters, remote.chapters)) + } else { + logcat(LogPriority.DEBUG, logTag) { "Keeping remote version of ${remote.title} with merged chapters." } + remote.copy(chapters = mergeChapters(local.chapters, remote.chapters)) + } + } + else -> null // No manga found for key + } + } + + // Counting favorites and non-favorites + val (favorites, nonFavorites) = mergedList.partition { it.favorite } + + logcat(LogPriority.DEBUG, logTag) { + "Merge completed. Total merged manga: ${mergedList.size}, Favorites: ${favorites.size}, " + + "Non-Favorites: ${nonFavorites.size}" + } + + return mergedList + } + +/** + * Merges two lists of BackupChapter objects, selecting the most recent chapter based on the lastModifiedAt value. + * If lastModifiedAt is null for a chapter, it treats that chapter as the oldest possible for comparison purposes. + * This function is designed to reconcile local and remote chapter lists, ensuring the most up-to-date chapter is retained. + * + * @param localChapters The list of local BackupChapter objects. + * @param remoteChapters The list of remote BackupChapter objects. + * @return A list of BackupChapter objects, each representing the most recent version of the chapter from either local or remote sources. + * + * - This function is used in scenarios where local and remote chapter lists need to be synchronized. + * - It iterates over the union of the URLs from both local and remote chapters. + * - For each URL, it compares the corresponding local and remote chapters based on the lastModifiedAt value. + * - If only one source (local or remote) has the chapter for a URL, that chapter is used. + * - If both sources have the chapter, the one with the more recent lastModifiedAt value is chosen. + * - If lastModifiedAt is null or missing, the chapter is considered the oldest for safety, ensuring that any chapter with a valid timestamp is preferred. + * - The resulting list contains the most recent chapters from the combined set of local and remote chapters. + */ + private fun mergeChapters( + localChapters: List, + remoteChapters: List, + ): List { + val logTag = "MergeChapters" + + fun chapterCompositeKey(chapter: BackupChapter): String { + return "${chapter.url}|${chapter.name}|${chapter.chapterNumber}" + } + + val localChapterMap = localChapters.associateBy { chapterCompositeKey(it) } + val remoteChapterMap = remoteChapters.associateBy { chapterCompositeKey(it) } + + logcat(LogPriority.DEBUG, logTag) { + "Starting chapter merge. Local chapters: ${localChapters.size}, Remote chapters: ${remoteChapters.size}" + } + + // Merge both chapter maps based on version numbers + val mergedChapters = (localChapterMap.keys + remoteChapterMap.keys).distinct().mapNotNull { compositeKey -> + val localChapter = localChapterMap[compositeKey] + val remoteChapter = remoteChapterMap[compositeKey] + + logcat(LogPriority.DEBUG, logTag) { + "Processing chapter key: $compositeKey. Local chapter: ${localChapter != null}, " + + "Remote chapter: ${remoteChapter != null}" + } + + when { + localChapter != null && remoteChapter == null -> { + logcat(LogPriority.DEBUG, logTag) { "Keeping local chapter: ${localChapter.name}." } + localChapter + } + localChapter == null && remoteChapter != null -> { + logcat(LogPriority.DEBUG, logTag) { "Taking remote chapter: ${remoteChapter.name}." } + remoteChapter + } + localChapter != null && remoteChapter != null -> { + // Use version number to decide which chapter to keep + val chosenChapter = if (localChapter.version >= remoteChapter.version) localChapter else remoteChapter + logcat(LogPriority.DEBUG, logTag) { + "Merging chapter: ${chosenChapter.name}. Chosen version from: ${ + if (localChapter.version >= remoteChapter.version) "Local" else "Remote" + }, Local version: ${localChapter.version}, Remote version: ${remoteChapter.version}." + } + chosenChapter + } + else -> { + logcat(LogPriority.DEBUG, logTag) { + "No chapter found for composite key: $compositeKey. Skipping." + } + null + } + } + } + + logcat(LogPriority.DEBUG, logTag) { "Chapter merge completed. Total merged chapters: ${mergedChapters.size}" } + + return mergedChapters + } + + /** + * Merges two lists of SyncCategory objects, prioritizing the category with the most recent order value. + * + * @param localCategoriesList The list of local SyncCategory objects. + * @param remoteCategoriesList The list of remote SyncCategory objects. + * @return The merged list of SyncCategory objects. + */ + private fun mergeCategoriesLists( + localCategoriesList: List?, + remoteCategoriesList: List?, + ): List { + if (localCategoriesList == null) return remoteCategoriesList ?: emptyList() + if (remoteCategoriesList == null) return localCategoriesList + val localCategoriesMap = localCategoriesList.associateBy { it.name } + val remoteCategoriesMap = remoteCategoriesList.associateBy { it.name } + + val mergedCategoriesMap = mutableMapOf() + + localCategoriesMap.forEach { (name, localCategory) -> + val remoteCategory = remoteCategoriesMap[name] + if (remoteCategory != null) { + // Compare and merge local and remote categories + val mergedCategory = if (localCategory.order > remoteCategory.order) { + localCategory + } else { + remoteCategory + } + mergedCategoriesMap[name] = mergedCategory + } else { + // If the category is only in the local list, add it to the merged list + mergedCategoriesMap[name] = localCategory + } + } + + // Add any categories from the remote list that are not in the local list + remoteCategoriesMap.forEach { (name, remoteCategory) -> + if (!mergedCategoriesMap.containsKey(name)) { + mergedCategoriesMap[name] = remoteCategory + } + } + + return mergedCategoriesMap.values.toList() + } + + private fun mergeSourcesLists( + localSources: List?, + remoteSources: List? + ): List { + val logTag = "MergeSources" + + // Create maps using sourceId as key + val localSourceMap = localSources?.associateBy { it.sourceId } ?: emptyMap() + val remoteSourceMap = remoteSources?.associateBy { it.sourceId } ?: emptyMap() + + logcat(LogPriority.DEBUG, logTag) { + "Starting source merge. Local sources: ${localSources?.size}, Remote sources: ${remoteSources?.size}" + } + + // Merge both source maps + val mergedSources = (localSourceMap.keys + remoteSourceMap.keys).distinct().mapNotNull { sourceId -> + val localSource = localSourceMap[sourceId] + val remoteSource = remoteSourceMap[sourceId] + + logcat(LogPriority.DEBUG, logTag) { + "Processing source ID: $sourceId. Local source: ${localSource != null}, " + + "Remote source: ${remoteSource != null}" + } + + when { + localSource != null && remoteSource == null -> { + logcat(LogPriority.DEBUG, logTag) { "Using local source: ${localSource.name}." } + localSource + } + remoteSource != null && localSource == null -> { + logcat(LogPriority.DEBUG, logTag) { "Using remote source: ${remoteSource.name}." } + remoteSource + } + else -> { + logcat(LogPriority.DEBUG, logTag) { "Remote and local is not empty: $sourceId. Skipping." } + null + } + } + } + + logcat(LogPriority.DEBUG, logTag) { "Source merge completed. Total merged sources: ${mergedSources.size}" } + + return mergedSources + } + + private fun mergePreferencesLists( + localPreferences: List?, + remotePreferences: List? + ): List { + val logTag = "MergePreferences" + + // Create maps using key as the unique identifier + val localPreferencesMap = localPreferences?.associateBy { it.key } ?: emptyMap() + val remotePreferencesMap = remotePreferences?.associateBy { it.key } ?: emptyMap() + + logcat(LogPriority.DEBUG, logTag) { + "Starting preferences merge. Local preferences: ${localPreferences?.size}, " + + "Remote preferences: ${remotePreferences?.size}" + } + + // Merge both preferences maps + val mergedPreferences = (localPreferencesMap.keys + remotePreferencesMap.keys).distinct().mapNotNull { key -> + val localPreference = localPreferencesMap[key] + val remotePreference = remotePreferencesMap[key] + + logcat(LogPriority.DEBUG, logTag) { + "Processing preference key: $key. Local preference: ${localPreference != null}, " + + "Remote preference: ${remotePreference != null}" + } + + when { + localPreference != null && remotePreference == null -> { + logcat(LogPriority.DEBUG, logTag) { "Using local preference: ${localPreference.key}." } + localPreference + } + remotePreference != null && localPreference == null -> { + logcat(LogPriority.DEBUG, logTag) { "Using remote preference: ${remotePreference.key}." } + remotePreference + } + else -> { + logcat(LogPriority.DEBUG, logTag) { "Both remote and local have keys. Skipping: $key" } + null + } + } + } + + logcat(LogPriority.DEBUG, logTag) { + "Preferences merge completed. Total merged preferences: ${mergedPreferences.size}" + } + + return mergedPreferences + } + + private fun mergeSourcePreferencesLists( + localPreferences: List?, + remotePreferences: List? + ): List { + val logTag = "MergeSourcePreferences" + + // Create maps using sourceKey as the unique identifier + val localPreferencesMap = localPreferences?.associateBy { it.sourceKey } ?: emptyMap() + val remotePreferencesMap = remotePreferences?.associateBy { it.sourceKey } ?: emptyMap() + + logcat(LogPriority.DEBUG, logTag) { + "Starting source preferences merge. Local source preferences: ${localPreferences?.size}, " + + "Remote source preferences: ${remotePreferences?.size}" + } + + // Merge both source preferences maps + val mergedSourcePreferences = (localPreferencesMap.keys + remotePreferencesMap.keys).distinct().mapNotNull { sourceKey -> + val localSourcePreference = localPreferencesMap[sourceKey] + val remoteSourcePreference = remotePreferencesMap[sourceKey] + + logcat(LogPriority.DEBUG, logTag) { + "Processing source preference key: $sourceKey. " + + "Local source preference: ${localSourcePreference != null}, " + + "Remote source preference: ${remoteSourcePreference != null}" + } + + when { + localSourcePreference != null && remoteSourcePreference == null -> { + logcat(LogPriority.DEBUG, logTag) { + "Using local source preference: ${localSourcePreference.sourceKey}." + } + localSourcePreference + } + remoteSourcePreference != null && localSourcePreference == null -> { + logcat(LogPriority.DEBUG, logTag) { + "Using remote source preference: ${remoteSourcePreference.sourceKey}." + } + remoteSourcePreference + } + localSourcePreference != null && remoteSourcePreference != null -> { + // Merge the individual preferences within the source preferences + val mergedPrefs = mergeIndividualPreferences(localSourcePreference.prefs, remoteSourcePreference.prefs) + BackupSourcePreferences(sourceKey, mergedPrefs) + } + else -> null + } + } + + logcat(LogPriority.DEBUG, logTag) { + "Source preferences merge completed. Total merged source preferences: ${mergedSourcePreferences.size}" + } + + return mergedSourcePreferences + } + + private fun mergeIndividualPreferences( + localPrefs: List, + remotePrefs: List + ): List { + val mergedPrefsMap = (localPrefs + remotePrefs).associateBy { it.key } + return mergedPrefsMap.values.toList() + } + + + + // SY --> + private fun mergeSavedSearchesLists( + localSearches: List?, + remoteSearches: List? + ): List { + val logTag = "MergeSavedSearches" + + // Define a function to create a composite key from a BackupSavedSearch + fun searchCompositeKey(search: BackupSavedSearch): String { + return "${search.name}|${search.source}" + } + + // Create maps using the composite key + val localSearchMap = localSearches?.associateBy { searchCompositeKey(it) } ?: emptyMap() + val remoteSearchMap = remoteSearches?.associateBy { searchCompositeKey(it) } ?: emptyMap() + + logcat(LogPriority.DEBUG, logTag) { + "Starting saved searches merge. Local saved searches: ${localSearches?.size}, " + + "Remote saved searches: ${remoteSearches?.size}" + } + + // Merge both saved searches maps + val mergedSearches = (localSearchMap.keys + remoteSearchMap.keys).distinct().mapNotNull { compositeKey -> + val localSearch = localSearchMap[compositeKey] + val remoteSearch = remoteSearchMap[compositeKey] + + logcat(LogPriority.DEBUG, logTag) { + "Processing saved search key: $compositeKey. Local search: ${localSearch != null}, " + + "Remote search: ${remoteSearch != null}" + } + + when { + localSearch != null && remoteSearch == null -> { + logcat(LogPriority.DEBUG, logTag) { "Using local saved search: ${localSearch.name}." } + localSearch + } + remoteSearch != null && localSearch == null -> { + logcat(LogPriority.DEBUG, logTag) { "Using remote saved search: ${remoteSearch.name}." } + remoteSearch + } + else -> { + logcat(LogPriority.DEBUG, logTag) { + "No saved search found for composite key: $compositeKey. Skipping." + } + null + } + } + } + + logcat(LogPriority.DEBUG, logTag) { + "Saved searches merge completed. Total merged saved searches: ${mergedSearches.size}" + } + + return mergedSearches + } + // SY <-- + +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/sync/service/SyncYomiSyncService.kt b/app/src/main/java/eu/kanade/tachiyomi/data/sync/service/SyncYomiSyncService.kt new file mode 100644 index 000000000..2db2cf203 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/sync/service/SyncYomiSyncService.kt @@ -0,0 +1,198 @@ +package eu.kanade.tachiyomi.data.sync.service + +import android.content.Context +import eu.kanade.domain.sync.SyncPreferences +import eu.kanade.tachiyomi.data.sync.SyncNotifier +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.network.PATCH +import eu.kanade.tachiyomi.network.POST +import kotlinx.coroutines.delay +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import logcat.LogPriority +import okhttp3.Headers +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.OkHttpClient +import okhttp3.RequestBody.Companion.gzip +import okhttp3.RequestBody.Companion.toRequestBody +import tachiyomi.core.common.util.system.logcat +import java.util.concurrent.TimeUnit + +class SyncYomiSyncService( + context: Context, + json: Json, + syncPreferences: SyncPreferences, + private val notifier: SyncNotifier, +) : SyncService(context, json, syncPreferences) { + + @Serializable + enum class SyncStatus { + @SerialName("pending") + Pending, + + @SerialName("syncing") + Syncing, + + @SerialName("success") + Success, + } + + @Serializable + data class LockFile( + @SerialName("id") + val id: Int?, + @SerialName("user_api_key") + val userApiKey: String?, + @SerialName("acquired_by") + val acquiredBy: String?, + @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).execute() + // update lock file acquired_by + client.newCall(lockFileUpdate).execute() + + 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).execute() + val responseBody = response.body.string() + lockFile = json.decodeFromString(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 + client.newCall(lockFileUpdate).execute() + } + + override suspend fun pullSyncData(): SyncData? { + val host = syncPreferences.clientHost().get() + val apiKey = syncPreferences.clientAPIKey().get() + val downloadUrl = "$host/api/sync/download" + + val client = OkHttpClient() + val headers = Headers.Builder().add("X-API-Token", apiKey).build() + + val downloadRequest = GET( + url = downloadUrl, + headers = headers, + ) + + client.newCall(downloadRequest).execute().use { response -> + val responseBody = response.body.string() + + if (response.isSuccessful) { + return json.decodeFromString(responseBody) + } else { + notifier.showSyncError("Failed to download sync data: $responseBody") + responseBody.let { logcat(LogPriority.ERROR) { "SyncError:$it" } } + return null + } + } + } + + override suspend fun pushSyncData(syncData: SyncData) { + val host = syncPreferences.clientHost().get() + val apiKey = syncPreferences.clientAPIKey().get() + val uploadUrl = "$host/api/sync/upload" + val timeout = 30L + + // Set timeout to 30 seconds + val client = OkHttpClient.Builder() + .connectTimeout(timeout, TimeUnit.SECONDS) + .readTimeout(timeout, TimeUnit.SECONDS) + .writeTimeout(timeout, TimeUnit.SECONDS) + .build() + + val headers = Headers.Builder().add( + "Content-Type", + "application/gzip", + ).add("Content-Encoding", "gzip").add("X-API-Token", apiKey).build() + + val mediaType = "application/gzip".toMediaTypeOrNull() + + val jsonData = json.encodeToString(syncData) + val body = jsonData.toRequestBody(mediaType).gzip() + + val uploadRequest = POST( + url = uploadUrl, + headers = headers, + body = body, + ) + + client.newCall(uploadRequest).execute().use { + if (it.isSuccessful) { + logcat( + LogPriority.DEBUG, + ) { "SyncYomi sync completed!" } + } else { + val responseBody = it.body.string() + notifier.showSyncError("Failed to upload sync data: $responseBody") + responseBody.let { logcat(LogPriority.ERROR) { "SyncError:$it" } } + } + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/di/AppModule.kt b/app/src/main/java/eu/kanade/tachiyomi/di/AppModule.kt index 688142689..0e80d4ff1 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/di/AppModule.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/di/AppModule.kt @@ -17,6 +17,7 @@ import eu.kanade.tachiyomi.data.download.DownloadCache import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.download.DownloadProvider import eu.kanade.tachiyomi.data.saver.ImageSaver +import eu.kanade.tachiyomi.data.sync.service.GoogleDriveService import eu.kanade.tachiyomi.data.track.TrackerManager import eu.kanade.tachiyomi.extension.ExtensionManager import eu.kanade.tachiyomi.network.JavaScriptEngine @@ -185,5 +186,7 @@ class AppModule(val app: Application) : InjektModule { get() // SY <-- } + + addSingletonFactory { GoogleDriveService(app) } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/di/PreferenceModule.kt b/app/src/main/java/eu/kanade/tachiyomi/di/PreferenceModule.kt index b56c16cae..f358f8ff5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/di/PreferenceModule.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/di/PreferenceModule.kt @@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.di import android.app.Application import eu.kanade.domain.base.BasePreferences import eu.kanade.domain.source.service.SourcePreferences +import eu.kanade.domain.sync.SyncPreferences import eu.kanade.domain.track.service.TrackPreferences import eu.kanade.domain.ui.UiPreferences import eu.kanade.tachiyomi.core.security.SecurityPreferences @@ -66,5 +67,9 @@ class PreferenceModule(val app: Application) : InjektModule { addSingletonFactory { BasePreferences(app, get()) } + + addSingletonFactory { + SyncPreferences(get()) + } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryTab.kt index 56e4040f1..731624c08 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryTab.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryTab.kt @@ -42,6 +42,7 @@ import eu.kanade.presentation.util.Tab import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.library.LibraryUpdateJob import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.PreMigrationScreen +import eu.kanade.tachiyomi.data.sync.SyncDataJob import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchScreen import eu.kanade.tachiyomi.ui.category.CategoryScreen import eu.kanade.tachiyomi.ui.home.HomeScreen @@ -51,6 +52,7 @@ import eu.kanade.tachiyomi.ui.reader.ReaderActivity import eu.kanade.tachiyomi.util.system.toast import exh.favorites.FavoritesSyncStatus import exh.source.MERGED_SOURCE_ID +import eu.kanade.tachiyomi.util.system.toast import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.collectLatest @@ -162,6 +164,13 @@ object LibraryTab : Tab { } } }, + onClickSyncNow = { + if (!SyncDataJob.isAnyJobRunning(context)) { + SyncDataJob.startNow(context) + } else { + context.toast(MR.strings.sync_in_progress) + } + }, // SY --> onClickSyncExh = screenModel::openFavoritesSyncDialog.takeIf { state.showSyncExh }, // SY <-- diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt index 7188bd39f..52ca44179 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt @@ -14,6 +14,7 @@ import eu.kanade.domain.chapter.model.toDbChapter import eu.kanade.domain.manga.interactor.SetMangaViewerFlags import eu.kanade.domain.manga.model.readerOrientation import eu.kanade.domain.manga.model.readingMode +import eu.kanade.domain.sync.SyncPreferences import eu.kanade.domain.track.interactor.TrackChapter import eu.kanade.domain.track.service.TrackPreferences import eu.kanade.domain.ui.UiPreferences @@ -24,6 +25,7 @@ import eu.kanade.tachiyomi.data.download.model.Download import eu.kanade.tachiyomi.data.saver.Image import eu.kanade.tachiyomi.data.saver.ImageSaver import eu.kanade.tachiyomi.data.saver.Location +import eu.kanade.tachiyomi.data.sync.SyncDataJob import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.MetadataSource @@ -125,6 +127,7 @@ class ReaderViewModel @JvmOverloads constructor( private val upsertHistory: UpsertHistory = Injekt.get(), private val updateChapter: UpdateChapter = Injekt.get(), private val setMangaViewerFlags: SetMangaViewerFlags = Injekt.get(), + private val syncPreferences: SyncPreferences = Injekt.get(), // SY --> private val uiPreferences: UiPreferences = Injekt.get(), private val getFlatMetadataById: GetFlatMetadataById = Injekt.get(), @@ -677,6 +680,8 @@ class ReaderViewModel @JvmOverloads constructor( hasExtraPage: Boolean, /* SY <-- */ ) { val pageIndex = page.index + val syncTriggerOpt = syncPreferences.getSyncTriggerOptions() + val isSyncEnabled = syncPreferences.isSyncEnabled() mutableState.update { it.copy(currentPage = pageIndex + 1) @@ -721,6 +726,11 @@ class ReaderViewModel @JvmOverloads constructor( // SY <-- updateTrackChapterRead(readerChapter) deleteChapterIfNeeded(readerChapter) + + // Check if syncing is enabled for chapter read: + if (isSyncEnabled && syncTriggerOpt.syncOnChapterRead) { + SyncDataJob.startNow(Injekt.get()) + } } updateChapter.await( @@ -730,6 +740,11 @@ class ReaderViewModel @JvmOverloads constructor( lastPageRead = readerChapter.chapter.last_page_read.toLong(), ), ) + + // Check if syncing is enabled for chapter open: + if (isSyncEnabled && syncTriggerOpt.syncOnChapterOpen && readerChapter.chapter.last_page_read == 0) { + SyncDataJob.startNow(Injekt.get()) + } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/track/GoogleDriveLoginActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/track/GoogleDriveLoginActivity.kt new file mode 100644 index 000000000..65ffa5ae2 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/track/GoogleDriveLoginActivity.kt @@ -0,0 +1,54 @@ +package eu.kanade.tachiyomi.ui.setting.track + +import android.net.Uri +import android.widget.Toast +import androidx.lifecycle.lifecycleScope +import eu.kanade.tachiyomi.data.sync.service.GoogleDriveService +import tachiyomi.core.common.i18n.stringResource +import tachiyomi.core.common.util.lang.launchIO +import tachiyomi.i18n.MR +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +class GoogleDriveLoginActivity : BaseOAuthLoginActivity() { + private val googleDriveService = Injekt.get() + override fun handleResult(data: Uri?) { + val code = data?.getQueryParameter("code") + val error = data?.getQueryParameter("error") + if (code != null) { + lifecycleScope.launchIO { + googleDriveService.handleAuthorizationCode( + code, + this@GoogleDriveLoginActivity, + onSuccess = { + Toast.makeText( + this@GoogleDriveLoginActivity, + stringResource(MR.strings.google_drive_login_success), + Toast.LENGTH_LONG, + ).show() + + returnToSettings() + }, + onFailure = { error -> + Toast.makeText( + this@GoogleDriveLoginActivity, + stringResource(MR.strings.google_drive_login_failed, error), + Toast.LENGTH_LONG, + ).show() + returnToSettings() + }, + ) + } + } else if (error != null) { + Toast.makeText( + this@GoogleDriveLoginActivity, + stringResource(MR.strings.google_drive_login_failed, error), + Toast.LENGTH_LONG, + ).show() + + returnToSettings() + } else { + returnToSettings() + } + } +} diff --git a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/Requests.kt b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/Requests.kt index 6adb0de8e..75886950a 100644 --- a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/Requests.kt +++ b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/Requests.kt @@ -63,6 +63,19 @@ fun PUT( .cacheControl(cache) .build() } +fun PATCH( + url: String, + headers: Headers = DEFAULT_HEADERS, + body: RequestBody = DEFAULT_BODY, + cache: CacheControl = DEFAULT_CACHE_CONTROL, +): Request { + return Request.Builder() + .url(url) + .patch(body) + .headers(headers) + .cacheControl(cache) + .build() +} fun DELETE( url: String, diff --git a/core/common/src/main/kotlin/tachiyomi/core/common/util/system/LogcatExtensions.kt b/core/common/src/main/kotlin/tachiyomi/core/common/util/system/LogcatExtensions.kt index 115f647f5..683aa8422 100644 --- a/core/common/src/main/kotlin/tachiyomi/core/common/util/system/LogcatExtensions.kt +++ b/core/common/src/main/kotlin/tachiyomi/core/common/util/system/LogcatExtensions.kt @@ -7,12 +7,22 @@ import logcat.logcat inline fun Any.logcat( priority: LogPriority = LogPriority.DEBUG, throwable: Throwable? = null, + tag: String? = null, message: () -> String = { "" }, ) = logcat(priority = priority) { - var msg = message() - if (throwable != null) { - if (msg.isNotBlank()) msg += "\n" - msg += throwable.asLog() + val logMessage = StringBuilder() + + if (!tag.isNullOrEmpty()) { + logMessage.append("[$tag] ") } - msg + + val msg = message() + logMessage.append(msg) + + if (throwable != null) { + if (msg.isNotBlank()) logMessage.append("\n") + logMessage.append(throwable.asLog()) + } + + logMessage.toString() } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9d1f10995..ec8557f6a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -106,6 +106,9 @@ detekt-gradlePlugin = { module = "io.gitlab.arturbosch.detekt:detekt-gradle-plug detekt-rules-formatting = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", version.ref = "detekt" } detekt-rules-compose = { module = "io.nlopez.compose.rules:detekt", version.ref = "detektCompose" } +google-api-services-drive = "com.google.apis:google-api-services-drive:v3-rev197-1.25.0" +google-api-client-oauth = "com.google.oauth-client:google-oauth-client:1.34.1" + [bundles] acra = ["acra-http", "acra-scheduler"] okhttp = ["okhttp-core", "okhttp-logging", "okhttp-brotli", "okhttp-dnsoverhttps"] diff --git a/i18n/src/commonMain/resources/MR/base/strings.xml b/i18n/src/commonMain/resources/MR/base/strings.xml index 5c7ff31cf..d1f945ed3 100644 --- a/i18n/src/commonMain/resources/MR/base/strings.xml +++ b/i18n/src/commonMain/resources/MR/base/strings.xml @@ -28,8 +28,10 @@ Updates History Sources - Backup and restore Data and storage + Backup + Sync + Triggers Statistics Migrate Extensions @@ -212,6 +214,7 @@ One-way progress sync, enhanced sync Sources, extensions, global search Manual & automatic backups, storage space + Manual & automatic backups and sync App lock, secure screen Dump crash logs, battery optimizations @@ -266,6 +269,9 @@ Global update Automatic updates Off + Every 30 minutes + Every hour + Every 3 hours Every 6 hours Every 12 hours Daily @@ -559,6 +565,53 @@ Syncing library Library sync complete + Syncing library failed + Syncing library complete + Sync is already in progress + Host + Enter the host address for synchronizing your library + API key + Enter the API key to synchronize your library + Sync Actions + Sync now + Sync confirmation + Initiate immediate synchronization of your data + Syncing will overwrite your local library with the remote library. Are you sure you want to continue? + Service + Select the service to sync your library with + Sync + Automatic Synchronization + Synchronization frequency + Choose what to sync + Last sync timestamp reset + SyncYomi + Done in %1$s + Last Synchronization: %1$s + Google Drive + Sign in + Signed in successfully + Sign in failed + Authentication + Clear Sync Data from Google Drive + Sync data purged from Google Drive + No sync data found in Google Drive + Error purging sync data from Google Drive, Try to sign in again. + Logged in to Google Drive + Failed to log in to Google Drive: %s + Not signed in to Google Drive + Error uploading sync data to Google Drive + Error Deleting Google Drive Lock File + Error before sync: %s + Purge confirmation + Purging sync data will delete all your sync data from Google Drive. Are you sure you want to continue? + Create sync triggers + Can be used to set sync triggers + Sync on Chapter Read + Sync on Chapter Open + Sync on App Start + Sync on App Resume + Sync on Library Update + Sync library Networking diff --git a/i18n/src/commonMain/resources/MR/nb-rNO/plurals.xml b/i18n/src/commonMain/resources/MR/nb-rNO/plurals.xml index 1d7bf5b64..323002f46 100644 --- a/i18n/src/commonMain/resources/MR/nb-rNO/plurals.xml +++ b/i18n/src/commonMain/resources/MR/nb-rNO/plurals.xml @@ -68,4 +68,8 @@ %d pakkebrønn %d pakkebrønner + + %d pakkebr├╕nn + %d pakkebr├╕nner + \ No newline at end of file diff --git a/i18n/src/commonMain/resources/MR/th/plurals.xml b/i18n/src/commonMain/resources/MR/th/plurals.xml index 78a0c5bdc..d6f26cdb7 100644 --- a/i18n/src/commonMain/resources/MR/th/plurals.xml +++ b/i18n/src/commonMain/resources/MR/th/plurals.xml @@ -51,4 +51,7 @@ %d รีโพ + + %d α╕úα╕╡α╣éα╕₧ + \ No newline at end of file diff --git a/i18n/src/commonMain/resources/MR/tr/plurals.xml b/i18n/src/commonMain/resources/MR/tr/plurals.xml index 7e40a5df6..60e2098c0 100644 --- a/i18n/src/commonMain/resources/MR/tr/plurals.xml +++ b/i18n/src/commonMain/resources/MR/tr/plurals.xml @@ -68,4 +68,8 @@ %d depo %d depo + + %d depo + %d depo + \ No newline at end of file From 02775c7c99dbba971782a631deeeba69bd65432e Mon Sep 17 00:00:00 2001 From: Shamicen <84282253+Shamicen@users.noreply.github.com> Date: Sat, 16 Mar 2024 16:59:00 +0100 Subject: [PATCH 018/106] implement mihonapp/mihon#326 (#1104) * implement mihonapp/mihon#326 Archives are now being read from channels Co-authored-by: FooIbar <118464521+FooIbar@users.noreply.github.com> Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com> * disable parallelisms for loading into memory * switched to mutex * detekt changes * more detekt baseline changes --------- Co-authored-by: FooIbar <118464521+FooIbar@users.noreply.github.com> Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com> --- app/build.gradle.kts | 2 +- app/proguard-rules.pro | 3 + .../settings/screen/SettingsReaderScreen.kt | 12 +- .../tachiyomi/data/download/Downloader.kt | 6 +- .../tachiyomi/ui/reader/ReaderViewModel.kt | 1 - .../ui/reader/loader/ChapterLoader.kt | 21 +- .../ui/reader/loader/DownloadPageLoader.kt | 6 +- .../ui/reader/loader/EpubPageLoader.kt | 9 +- .../ui/reader/loader/RarPageLoader.kt | 71 +- .../ui/reader/loader/ZipPageLoader.kt | 172 ++- .../tachiyomi/ui/reader/model/ReaderPage.kt | 5 - .../ui/reader/setting/ReaderPreferences.kt | 16 +- .../ui/reader/viewer/pager/PagerPageHolder.kt | 24 +- .../viewer/webtoon/WebtoonPageHolder.kt | 16 +- app/src/main/java/exh/EXHMigrations.kt | 6 + .../exh/ui/intercept/InterceptActivity.kt | 1 - config/detekt/baseline.xml | 1337 ++++++++++++++--- core/common/build.gradle.kts | 1 + .../tachiyomi/util/storage/CbzCrypto.kt | 167 +- .../kanade/tachiyomi/util/storage/EpubFile.kt | 26 +- .../core/common/storage/UniFileExtensions.kt | 213 +++ .../core/common/util/system/ImageUtil.kt | 66 +- gradle/libs.versions.toml | 2 + .../commonMain/resources/MR/base/strings.xml | 10 +- source-local/build.gradle.kts | 2 +- .../tachiyomi/source/local/LocalSource.kt | 42 +- .../source/local/image/LocalCoverManager.kt | 2 +- 27 files changed, 1656 insertions(+), 583 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 8f5254bbf..1292fbb3e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -215,7 +215,7 @@ dependencies { // Disk implementation(libs.disklrucache) implementation(libs.unifile) - implementation(libs.junrar) + implementation(libs.bundles.archive) // SY --> implementation(libs.zip4j) // SY <-- diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index f4445d017..94692d207 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -122,6 +122,9 @@ # XmlUtil -keep public enum nl.adaptivity.xmlutil.EventType { *; } +# Apache Commons Compress +-keep class * extends org.apache.commons.compress.archivers.zip.ZipExtraField { (); } + # Firebase -keep class com.google.firebase.installations.** { *; } -keep interface com.google.firebase.installations.** { *; } diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsReaderScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsReaderScreen.kt index 3acc668c3..49788eb7a 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsReaderScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsReaderScreen.kt @@ -570,10 +570,14 @@ object SettingsReaderScreen : SearchableSettings { .toMap() .toImmutableMap(), ), - Preference.PreferenceItem.SwitchPreference( - pref = readerPreferences.cacheArchiveMangaOnDisk(), - title = stringResource(SYMR.strings.cache_archived_manga_to_disk), - subtitle = stringResource(SYMR.strings.cache_archived_manga_to_disk_subtitle), + Preference.PreferenceItem.ListPreference( + pref = readerPreferences.archiveReaderMode(), + title = stringResource(SYMR.strings.pref_archive_reader_mode), + subtitle = stringResource(SYMR.strings.pref_archive_reader_mode_summary), + entries = ReaderPreferences.archiveModeTypes + .mapIndexed { index, it -> index to stringResource(it) } + .toMap() + .toImmutableMap(), ), ), ) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt index 577adbdeb..90ef63638 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt @@ -13,7 +13,6 @@ import eu.kanade.tachiyomi.source.UnmeteredSource import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.util.storage.CbzCrypto -import eu.kanade.tachiyomi.util.storage.CbzCrypto.addFilesToZip import eu.kanade.tachiyomi.util.storage.DiskUtil import eu.kanade.tachiyomi.util.storage.DiskUtil.NOMEDIA_FILE import eu.kanade.tachiyomi.util.storage.saveTo @@ -46,6 +45,7 @@ import logcat.LogPriority import nl.adaptivity.xmlutil.serialization.XML import okhttp3.Response import tachiyomi.core.common.i18n.stringResource +import tachiyomi.core.common.storage.addFilesToZip import tachiyomi.core.common.storage.extension import tachiyomi.core.common.util.lang.launchIO import tachiyomi.core.common.util.lang.launchNow @@ -572,10 +572,6 @@ class Downloader( tmpDir, imageFile, filenamePrefix, - // SY --> - zip4jFile = null, - zip4jEntry = null, - // SY <-- ) } catch (e: Exception) { logcat(LogPriority.ERROR, e) { "Failed to split downloaded image" } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt index 52ca44179..38ffb1ace 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt @@ -389,7 +389,6 @@ class ReaderViewModel @JvmOverloads constructor( context = context, downloadManager = downloadManager, downloadProvider = downloadProvider, - tempFileManager = tempFileManager, manga = manga, source = source, /* SY --> */ sourceManager = sourceManager, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ChapterLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ChapterLoader.kt index 0778e75b8..c05563f63 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ChapterLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ChapterLoader.kt @@ -10,7 +10,6 @@ import eu.kanade.tachiyomi.source.online.all.MergedSource import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences import tachiyomi.core.common.i18n.stringResource -import tachiyomi.core.common.storage.UniFileTempFileManager import tachiyomi.core.common.util.lang.withIOContext import tachiyomi.core.common.util.system.logcat import tachiyomi.domain.manga.model.Manga @@ -28,7 +27,6 @@ class ChapterLoader( private val context: Context, private val downloadManager: DownloadManager, private val downloadProvider: DownloadProvider, - private val tempFileManager: UniFileTempFileManager, private val manga: Manga, private val source: Source, // SY --> @@ -121,36 +119,39 @@ class ChapterLoader( source = source, downloadManager = downloadManager, downloadProvider = downloadProvider, - tempFileManager = tempFileManager, ) source is HttpSource -> HttpPageLoader(chapter, source) source is LocalSource -> source.getFormat(chapter.chapter).let { format -> when (format) { is Format.Directory -> DirectoryPageLoader(format.file) - is Format.Zip -> ZipPageLoader(tempFileManager.createTempFile(format.file)) + is Format.Zip -> ZipPageLoader(format.file, context) is Format.Rar -> try { - RarPageLoader(tempFileManager.createTempFile(format.file)) + RarPageLoader(format.file) } catch (e: UnsupportedRarV5Exception) { error(context.stringResource(MR.strings.loader_rar5_error)) } - is Format.Epub -> EpubPageLoader(tempFileManager.createTempFile(format.file)) + is Format.Epub -> EpubPageLoader(format.file, context) } } else -> error(context.stringResource(MR.strings.loader_not_implemented_error)) } } // SY <-- - isDownloaded -> DownloadPageLoader(chapter, manga, source, downloadManager, downloadProvider, tempFileManager) + isDownloaded -> DownloadPageLoader(chapter, manga, source, downloadManager, downloadProvider) source is LocalSource -> source.getFormat(chapter.chapter).let { format -> when (format) { is Format.Directory -> DirectoryPageLoader(format.file) - is Format.Zip -> ZipPageLoader(tempFileManager.createTempFile(format.file)) + // SY --> + is Format.Zip -> ZipPageLoader(format.file, context) is Format.Rar -> try { - RarPageLoader(tempFileManager.createTempFile(format.file)) + RarPageLoader(format.file) + // SY <-- } catch (e: UnsupportedRarV5Exception) { error(context.stringResource(MR.strings.loader_rar5_error)) } - is Format.Epub -> EpubPageLoader(tempFileManager.createTempFile(format.file)) + // SY --> + is Format.Epub -> EpubPageLoader(format.file, context) + // SY <-- } } source is HttpSource -> HttpPageLoader(chapter, source) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/DownloadPageLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/DownloadPageLoader.kt index d0af290f2..ff70fec7f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/DownloadPageLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/DownloadPageLoader.kt @@ -10,7 +10,6 @@ import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter import eu.kanade.tachiyomi.ui.reader.model.ReaderPage -import tachiyomi.core.common.storage.UniFileTempFileManager import tachiyomi.domain.manga.model.Manga import uy.kohesive.injekt.injectLazy @@ -23,7 +22,6 @@ internal class DownloadPageLoader( private val source: Source, private val downloadManager: DownloadManager, private val downloadProvider: DownloadProvider, - private val tempFileManager: UniFileTempFileManager, ) : PageLoader() { private val context: Application by injectLazy() @@ -48,7 +46,9 @@ internal class DownloadPageLoader( } private suspend fun getPagesFromArchive(file: UniFile): List { - val loader = ZipPageLoader(tempFileManager.createTempFile(file)).also { zipPageLoader = it } + // SY --> + val loader = ZipPageLoader(file, context).also { zipPageLoader = it } + // SY <-- return loader.getPages() } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/EpubPageLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/EpubPageLoader.kt index 324af51bf..3a4d7388c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/EpubPageLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/EpubPageLoader.kt @@ -1,16 +1,19 @@ package eu.kanade.tachiyomi.ui.reader.loader +import android.content.Context +import com.hippo.unifile.UniFile import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.ui.reader.model.ReaderPage import eu.kanade.tachiyomi.util.storage.EpubFile -import java.io.File /** * Loader used to load a chapter from a .epub file. */ -internal class EpubPageLoader(file: File) : PageLoader() { +// SY --> +internal class EpubPageLoader(file: UniFile, context: Context) : PageLoader() { - private val epub = EpubFile(file) + private val epub = EpubFile(file, context) + // SY <-- override var isLocal: Boolean = true diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/RarPageLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/RarPageLoader.kt index 9ca277f78..04c357a0e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/RarPageLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/RarPageLoader.kt @@ -1,6 +1,7 @@ package eu.kanade.tachiyomi.ui.reader.loader import android.app.Application +import android.os.Build import com.github.junrar.Archive import com.github.junrar.rarfile.FileHeader import com.hippo.unifile.UniFile @@ -8,6 +9,14 @@ import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.ui.reader.model.ReaderPage import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import tachiyomi.core.common.storage.UniFileTempFileManager import tachiyomi.core.common.util.system.ImageUtil import uy.kohesive.injekt.injectLazy import java.io.File @@ -19,11 +28,17 @@ import java.util.concurrent.Executors /** * Loader used to load a chapter from a .rar or .cbr file. */ -internal class RarPageLoader(file: File) : PageLoader() { - - private val rar = Archive(file) +internal class RarPageLoader(file: UniFile) : PageLoader() { // SY --> + private val tempFileManager: UniFileTempFileManager by injectLazy() + + private val rar = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { + Archive(tempFileManager.createTempFile(file)) + } else { + Archive(file.openInputStream()) + } + private val context: Application by injectLazy() private val readerPreferences: ReaderPreferences by injectLazy() private val tmpDir = File(context.externalCacheDir, "reader_${file.hashCode()}").also { @@ -31,20 +46,21 @@ internal class RarPageLoader(file: File) : PageLoader() { } init { - if (readerPreferences.cacheArchiveMangaOnDisk().get()) { + if (readerPreferences.archiveReaderMode().get() == ReaderPreferences.ArchiveReaderMode.CACHE_TO_DISK) { tmpDir.mkdirs() - Archive(file).use { rar -> - rar.fileHeaders.asSequence() - .filterNot { it.isDirectory } - .forEach { header -> - val pageOutputStream = File(tmpDir, header.fileName.substringAfterLast("/")) - .also { it.createNewFile() } - .outputStream() - getStream(header).use { - it.copyTo(pageOutputStream) + rar.fileHeaders.asSequence() + .filter { !it.isDirectory && ImageUtil.isImage(it.fileName) { rar.getInputStream(it) } } + .sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) } + .forEach { header -> + File(tmpDir, header.fileName.substringAfterLast("/")) + .also { it.createNewFile() } + .outputStream() + .use { output -> + rar.getInputStream(header).use { input -> + input.copyTo(output) + } } - } - } + } } } // SY <-- @@ -58,16 +74,37 @@ internal class RarPageLoader(file: File) : PageLoader() { override suspend fun getPages(): List { // SY --> - if (readerPreferences.cacheArchiveMangaOnDisk().get()) { + if (readerPreferences.archiveReaderMode().get() == ReaderPreferences.ArchiveReaderMode.CACHE_TO_DISK) { return DirectoryPageLoader(UniFile.fromFile(tmpDir)!!).getPages() } + val mutex = Mutex() // SY <-- return rar.fileHeaders.asSequence() .filter { !it.isDirectory && ImageUtil.isImage(it.fileName) { rar.getInputStream(it) } } .sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) } .mapIndexed { i, header -> + // SY --> + val imageBytesDeferred: Deferred? = + when (readerPreferences.archiveReaderMode().get()) { + ReaderPreferences.ArchiveReaderMode.LOAD_INTO_MEMORY -> { + CoroutineScope(Dispatchers.IO).async { + mutex.withLock { + getStream(header).buffered().use { stream -> + stream.readBytes() + } + } + } + } + + else -> null + } + + val imageBytes by lazy { runBlocking { imageBytesDeferred?.await() } } + // SY <-- ReaderPage(i).apply { - stream = { getStream(header) } + // SY --> + stream = { imageBytes?.copyOf()?.inputStream() ?: getStream(header) } + // SY <-- status = Page.State.READY } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ZipPageLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ZipPageLoader.kt index 6b5b6ec01..049018ffb 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ZipPageLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ZipPageLoader.kt @@ -1,6 +1,6 @@ package eu.kanade.tachiyomi.ui.reader.loader -import android.app.Application +import android.content.Context import android.os.Build import com.hippo.unifile.UniFile import eu.kanade.tachiyomi.source.model.Page @@ -8,107 +8,155 @@ import eu.kanade.tachiyomi.ui.reader.model.ReaderPage import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder import eu.kanade.tachiyomi.util.storage.CbzCrypto +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import org.apache.commons.compress.archivers.zip.ZipFile import tachiyomi.core.common.i18n.stringResource +import tachiyomi.core.common.storage.UniFileTempFileManager +import tachiyomi.core.common.storage.isEncryptedZip +import tachiyomi.core.common.storage.openReadOnlyChannel +import tachiyomi.core.common.storage.testCbzPassword +import tachiyomi.core.common.storage.unzip import tachiyomi.core.common.util.system.ImageUtil import tachiyomi.i18n.sy.SYMR import uy.kohesive.injekt.injectLazy import java.io.File -import java.nio.charset.StandardCharsets -import java.util.zip.ZipFile +import java.nio.channels.SeekableByteChannel import net.lingala.zip4j.ZipFile as Zip4jFile /** * Loader used to load a chapter from a .zip or .cbz file. */ -internal class ZipPageLoader(file: File) : PageLoader() { +internal class ZipPageLoader(file: UniFile, context: Context) : PageLoader() { // SY --> - private val context: Application by injectLazy() + private val channel: SeekableByteChannel = file.openReadOnlyChannel(context) + private val tempFileManager: UniFileTempFileManager by injectLazy() private val readerPreferences: ReaderPreferences by injectLazy() private val tmpDir = File(context.externalCacheDir, "reader_${file.hashCode()}").also { it.deleteRecursively() } - private val zip4j: Zip4jFile = Zip4jFile(file) - private val zip: ZipFile? = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - if (!zip4j.isEncrypted) ZipFile(file, StandardCharsets.ISO_8859_1) else null + + private val apacheZip: ZipFile? = if (!file.isEncryptedZip() && Build.VERSION.SDK_INT > Build.VERSION_CODES.N) { + ZipFile(channel) + } else { + null + } + + private val tmpFile = + if ( + apacheZip == null && + readerPreferences.archiveReaderMode().get() != ReaderPreferences.ArchiveReaderMode.CACHE_TO_DISK + ) { + tempFileManager.createTempFile(file) } else { - if (!zip4j.isEncrypted) ZipFile(file) else null + null + } + + private val zip4j = + if (apacheZip == null && tmpFile != null) { + Zip4jFile(tmpFile) + } else { + null } init { - Zip4jFile(file).use { zip -> - if (zip.isEncrypted) { - if (!CbzCrypto.checkCbzPassword(zip, CbzCrypto.getDecryptedPasswordCbz())) { - this.recycle() - throw IllegalStateException(context.stringResource(SYMR.strings.wrong_cbz_archive_password)) - } - zip4j.setPassword(CbzCrypto.getDecryptedPasswordCbz()) - if (readerPreferences.cacheArchiveMangaOnDisk().get()) { - unzip() - } - } else { - if (readerPreferences.cacheArchiveMangaOnDisk().get()) { - unzip() - } + if (file.isEncryptedZip()) { + if (!file.testCbzPassword()) { + this.recycle() + throw IllegalStateException(context.stringResource(SYMR.strings.wrong_cbz_archive_password)) } + zip4j?.setPassword(CbzCrypto.getDecryptedPasswordCbz()) + } + if (readerPreferences.archiveReaderMode().get() == ReaderPreferences.ArchiveReaderMode.CACHE_TO_DISK) { + file.unzip(tmpDir, onlyCopyImages = true) } } // SY <-- override fun recycle() { super.recycle() - zip?.close() + apacheZip?.close() // SY --> - zip4j.close() + zip4j?.close() tmpDir.deleteRecursively() } - private fun unzip() { - tmpDir.mkdirs() - zip4j.fileHeaders.asSequence() - .filter { !it.isDirectory && ImageUtil.isImage(it.fileName) { zip4j.getInputStream(it) } } - .forEach { entry -> - zip4j.extractFile(entry, tmpDir.absolutePath) - } - } override var isLocal: Boolean = true override suspend fun getPages(): List { - if (readerPreferences.cacheArchiveMangaOnDisk().get()) { + if (readerPreferences.archiveReaderMode().get() == ReaderPreferences.ArchiveReaderMode.CACHE_TO_DISK) { return DirectoryPageLoader(UniFile.fromFile(tmpDir)!!).getPages() } - - if (zip == null) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - zip4j.charset = StandardCharsets.ISO_8859_1 - } - - return zip4j.fileHeaders.asSequence() - .filter { !it.isDirectory && ImageUtil.isImage(it.fileName) { zip4j.getInputStream(it) } } - .sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) } - .mapIndexed { i, entry -> - ReaderPage(i).apply { - stream = { zip4j.getInputStream(entry) } - status = Page.State.READY - zip4jFile = zip4j - zip4jEntry = entry - } - }.toList() + return if (apacheZip == null) { + loadZip4j() } else { - // SY <-- - return zip.entries().asSequence() - .filter { !it.isDirectory && ImageUtil.isImage(it.name) { zip.getInputStream(it) } } - .sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) } - .mapIndexed { i, entry -> - ReaderPage(i).apply { - stream = { zip.getInputStream(entry) } - status = Page.State.READY - } - }.toList() + loadApacheZip(apacheZip) } } + private fun loadZip4j(): List { + val mutex = Mutex() + return zip4j!!.fileHeaders.asSequence() + .filter { !it.isDirectory && ImageUtil.isImage(it.fileName) { zip4j.getInputStream(it) } } + .sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) } + .mapIndexed { i, entry -> + val imageBytesDeferred: Deferred? = + when (readerPreferences.archiveReaderMode().get()) { + ReaderPreferences.ArchiveReaderMode.LOAD_INTO_MEMORY -> { + CoroutineScope(Dispatchers.IO).async { + mutex.withLock { + zip4j.getInputStream(entry).buffered().use { stream -> + stream.readBytes() + } + } + } + } + + else -> null + } + val imageBytes by lazy { runBlocking { imageBytesDeferred?.await() } } + ReaderPage(i).apply { + stream = { imageBytes?.copyOf()?.inputStream() ?: zip4j.getInputStream(entry) } + status = Page.State.READY + } + }.toList() + } + + private fun loadApacheZip(zip: ZipFile): List { + val mutex = Mutex() + return zip.entries.asSequence() + .filter { !it.isDirectory && ImageUtil.isImage(it.name) { zip.getInputStream(it) } } + .sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) } + .mapIndexed { i, entry -> + val imageBytesDeferred: Deferred? = + when (readerPreferences.archiveReaderMode().get()) { + ReaderPreferences.ArchiveReaderMode.LOAD_INTO_MEMORY -> { + CoroutineScope(Dispatchers.IO).async { + mutex.withLock { + zip.getInputStream(entry).buffered().use { stream -> + stream.readBytes() + } + } + } + } + + else -> null + } + val imageBytes by lazy { runBlocking { imageBytesDeferred?.await() } } + ReaderPage(i).apply { + stream = { imageBytes?.copyOf()?.inputStream() ?: zip.getInputStream(entry) } + status = Page.State.READY + } + }.toList() + } + // SY <-- + /** * No additional action required to load the page */ diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/model/ReaderPage.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/model/ReaderPage.kt index 20b99d9cb..0a6f2d414 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/model/ReaderPage.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/model/ReaderPage.kt @@ -1,8 +1,6 @@ package eu.kanade.tachiyomi.ui.reader.model import eu.kanade.tachiyomi.source.model.Page -import net.lingala.zip4j.ZipFile -import net.lingala.zip4j.model.FileHeader import java.io.InputStream open class ReaderPage( @@ -10,9 +8,6 @@ open class ReaderPage( url: String = "", imageUrl: String? = null, // SY --> - /** zip4j inputStreams do not support mark() and release(), so they must be passed to ImageUtil */ - var zip4jFile: ZipFile? = null, - var zip4jEntry: FileHeader? = null, /** Value to check if this page is used to as if it was too wide */ var shiftedPage: Boolean = false, /** Value to check if a page is can be doubled up, but can't because the next page is too wide */ diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderPreferences.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderPreferences.kt index 9203ed1b0..149027acc 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderPreferences.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderPreferences.kt @@ -180,9 +180,9 @@ class ReaderPreferences( fun centerMarginType() = preferenceStore.getInt("center_margin_type", PagerConfig.CenterMarginType.NONE) - fun cacheArchiveMangaOnDisk() = preferenceStore.getBoolean("cache_archive_manga_on_disk", false) + fun archiveReaderMode() = preferenceStore.getInt("archive_reader_mode", ArchiveReaderMode.LOAD_FROM_FILE) - fun markReadDupe() = preferenceStore.getBoolean("mark_read_dupe", false) + fun markReadDupe() = preferenceStore.getBoolean("mark_read_dupe", false) // SY <-- enum class TappingInvertMode( @@ -203,6 +203,12 @@ class ReaderPreferences( LOWEST(47), } + object ArchiveReaderMode { + const val LOAD_FROM_FILE = 0 + const val LOAD_INTO_MEMORY = 1 + const val CACHE_TO_DISK = 2 + } + companion object { const val WEBTOON_PADDING_MIN = 0 const val WEBTOON_PADDING_MAX = 25 @@ -264,6 +270,12 @@ class ReaderPreferences( SYMR.strings.center_margin_wide_page, SYMR.strings.center_margin_double_and_wide_page, ) + + val archiveModeTypes = listOf( + SYMR.strings.archive_mode_load_from_file, + SYMR.strings.archive_mode_load_into_memory, + SYMR.strings.archive_mode_cache_to_disk + ) // SY <-- } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt index 9cb629736..4f5636779 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt @@ -230,13 +230,7 @@ class PagerPageHolder( return splitInHalf(imageStream) } - val isDoublePage = ImageUtil.isWideImage( - imageStream, - // SY --> - page.zip4jFile, - page.zip4jEntry, - // SY <-- - ) + val isDoublePage = ImageUtil.isWideImage(imageStream) if (!isDoublePage) { return imageStream } @@ -247,13 +241,7 @@ class PagerPageHolder( } private fun rotateDualPage(imageStream: BufferedInputStream): InputStream { - val isDoublePage = ImageUtil.isWideImage( - imageStream, - // SY --> - page.zip4jFile, - page.zip4jEntry, - // SY <-- - ) + val isDoublePage = ImageUtil.isWideImage(imageStream) return if (isDoublePage) { val rotation = if (viewer.config.dualPageRotateToFitInvert) -90f else 90f ImageUtil.rotateImage(imageStream, rotation) @@ -267,13 +255,7 @@ class PagerPageHolder( if (imageStream2 == null) { return if (imageStream is BufferedInputStream && !ImageUtil.isAnimatedAndSupported(imageStream) && - ImageUtil.isWideImage( - imageStream, - // SY --> - page.zip4jFile, - page.zip4jEntry, - // SY <-- - ) && + ImageUtil.isWideImage(imageStream) && viewer.config.centerMarginType and PagerConfig.CenterMarginType.WIDE_PAGE_CENTER_MARGIN > 0 && !viewer.config.imageCropBorders ) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonPageHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonPageHolder.kt index 2dcf62723..a192f7853 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonPageHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonPageHolder.kt @@ -224,13 +224,7 @@ class WebtoonPageHolder( } if (viewer.config.dualPageSplit) { - val isDoublePage = ImageUtil.isWideImage( - imageStream, - // SY --> - page?.zip4jFile, - page?.zip4jEntry, - // SY <-- - ) + val isDoublePage = ImageUtil.isWideImage(imageStream) if (isDoublePage) { val upperSide = if (viewer.config.dualPageInvert) ImageUtil.Side.LEFT else ImageUtil.Side.RIGHT return ImageUtil.splitAndMerge(imageStream, upperSide) @@ -241,13 +235,7 @@ class WebtoonPageHolder( } private fun rotateDualPage(imageStream: BufferedInputStream): InputStream { - val isDoublePage = ImageUtil.isWideImage( - imageStream, - // SY --> - page?.zip4jFile, - page?.zip4jEntry, - // SY <-- - ) + val isDoublePage = ImageUtil.isWideImage(imageStream) return if (isDoublePage) { val rotation = if (viewer.config.dualPageRotateToFitInvert) -90f else 90f ImageUtil.rotateImage(imageStream, rotation) diff --git a/app/src/main/java/exh/EXHMigrations.kt b/app/src/main/java/exh/EXHMigrations.kt index fe3e513cc..f1e57fbb3 100644 --- a/app/src/main/java/exh/EXHMigrations.kt +++ b/app/src/main/java/exh/EXHMigrations.kt @@ -656,6 +656,12 @@ object EXHMigrations { remove(Preference.appStateKey("trusted_signatures")) } } + if (oldVersion under 66) { + val cacheImagesToDisk = prefs.getBoolean("cache_archive_manga_on_disk", false) + if (cacheImagesToDisk) { + readerPreferences.archiveReaderMode().set(ReaderPreferences.ArchiveReaderMode.CACHE_TO_DISK) + } + } if (oldVersion under 66) { if (prefs.getBoolean(Preference.privateKey("encrypt_database"), false)) { diff --git a/app/src/main/java/exh/ui/intercept/InterceptActivity.kt b/app/src/main/java/exh/ui/intercept/InterceptActivity.kt index ba6c2d145..7effc5d81 100644 --- a/app/src/main/java/exh/ui/intercept/InterceptActivity.kt +++ b/app/src/main/java/exh/ui/intercept/InterceptActivity.kt @@ -187,7 +187,6 @@ class InterceptActivity : BaseActivity() { lifecycleScope.launchIO { loadGalleryEnd(gallery, sources[index]) } - } .show() } else { diff --git a/config/detekt/baseline.xml b/config/detekt/baseline.xml index 1845cae53..f185da8d9 100644 --- a/config/detekt/baseline.xml +++ b/config/detekt/baseline.xml @@ -2,10 +2,188 @@ + ArgumentListWrapping:AnnotatedString.kt$(SpanStyle(fontWeight = FontWeight.Bold, fontStyle = FontStyle.Italic), start, end) + ArgumentListWrapping:AnnotatedString.kt$(fontWeight = FontWeight.Bold, fontStyle = FontStyle.Italic) + ArgumentListWrapping:ApiMangaParser.kt$ApiMangaParser$("Content Rating", it.capitalize(Locale.US), MangaDexSearchMetadata.TAG_TYPE_DEFAULT) + ArgumentListWrapping:ApiMangaParser.kt$ApiMangaParser$("Demographic", it.capitalize(Locale.US), MangaDexSearchMetadata.TAG_TYPE_DEFAULT) + ArgumentListWrapping:ApiMangaParser.kt$ApiMangaParser$(Locale.US) + ArgumentListWrapping:BatchAddScreen.kt$BatchAddScreen$(SYMR.strings.eh_batch_add_adding_galleries) + ArgumentListWrapping:BatchAddScreen.kt$BatchAddScreen$(SYMR.strings.eh_batch_add_title) + ArgumentListWrapping:BatchAddScreen.kt$BatchAddScreen$(text = stringResource(SYMR.strings.eh_batch_add_adding_galleries), style = MaterialTheme.typography.titleLarge) + ArgumentListWrapping:BatchAddScreen.kt$BatchAddScreen$(text = stringResource(SYMR.strings.eh_batch_add_title), style = MaterialTheme.typography.titleLarge) + ArgumentListWrapping:BiometricTimesScreen.kt$BiometricTimesScreen$(SYMR.strings.delete_time_range_confirmation, dialog.timeRange.formattedString) + ArgumentListWrapping:BiometricTimesScreen.kt$BiometricTimesScreen$(context) + ArgumentListWrapping:BiometricTimesScreen.kt$BiometricTimesScreen$(if (startTime == null) SYMR.strings.biometric_lock_start_time.getString(context) else SYMR.strings.biometric_lock_end_time.getString(context)) + ArgumentListWrapping:ChapterLoader.kt$ChapterLoader$(chapter, manga, source, downloadManager, downloadProvider, tempFileManager) + ArgumentListWrapping:DataSaver.kt$BandwidthHeroDataSaver$(imageUrl) + ArgumentListWrapping:DataSaver.kt$WsrvNlDataSaver$(imageUrl) + ArgumentListWrapping:DownloadPageLoader.kt$DownloadPageLoader$(dbChapter.name, dbChapter.scanlator, /* SY --> */ manga.ogTitle /* SY <-- */, source) + ArgumentListWrapping:EHConfigurator.kt$EHConfigurator$(SYMR.strings.eh_settings_out_of_slots_error, source.name) + ArgumentListWrapping:EHConfigurator.kt$EHConfigurator$(context.stringResource(SYMR.strings.eh_settings_out_of_slots_error, source.name)) + ArgumentListWrapping:EditMergedSettingsDialog.kt$(factoryContext, binding, mergedData.manga.values.toList(), mergedData.references) + ArgumentListWrapping:EnhancedFilePrinter.kt$EnhancedFilePrinter$(!(newFileName == null || newFileName.trim { it <= ' ' }.isEmpty())) + ArgumentListWrapping:ExtensionManager.kt$ExtensionManager$("Removing blacklisted extension: (name: %s, pkgName: %s)!", extension.name, extension.pkgName) + ArgumentListWrapping:FavoritesSyncHelper.kt$FavoritesSyncHelper$(SYMR.strings.favorites_sync_gallery_multiple_categories_error, manga.id) + ArgumentListWrapping:FavoritesSyncHelper.kt$FavoritesSyncHelper$(context.stringResource(SYMR.strings.favorites_sync_gallery_multiple_categories_error, manga.id)) + ArgumentListWrapping:FavoritesSyncHelper.kt$FavoritesSyncStatus.BadLibraryState.MangaInMultipleCategories$( SYMR.strings.favorites_sync_gallery_in_multiple_categories, manga.title, categories.joinToString { it.name }, ) + ArgumentListWrapping:GalleryAdder.kt$GalleryAddEvent.Fail.UnknownSource$(SYMR.strings.batch_add_unknown_source_log_message, galleryUrl) + ArgumentListWrapping:GalleryAdder.kt$GalleryAddEvent.Fail.UnknownType$(SYMR.strings.batch_add_unknown_type_log_message, galleryUrl) + ArgumentListWrapping:GalleryAdder.kt$GalleryAdder$(SYMR.strings.gallery_adder_chapter_fetch_error, url) + ArgumentListWrapping:GalleryAdder.kt$GalleryAdder$(SYMR.strings.gallery_adder_could_not_identify_chapter, url) + ArgumentListWrapping:GalleryAdder.kt$GalleryAdder$(SYMR.strings.gallery_adder_importing_gallery, url, fav.toString(), forceSource?.toString().orEmpty()) + ArgumentListWrapping:GalleryAdder.kt$GalleryAdder$(context.stringResource(SYMR.strings.gallery_adder_importing_gallery, url, fav.toString(), forceSource?.toString().orEmpty())) + ArgumentListWrapping:GalleryAdder.kt$GalleryAdder$(url, context.stringResource(SYMR.strings.gallery_adder_chapter_fetch_error, url)) + ArgumentListWrapping:GalleryAdder.kt$GalleryAdder$(url, context.stringResource(SYMR.strings.gallery_adder_could_not_identify_chapter, url)) + ArgumentListWrapping:LibraryScreenModel.kt$LibraryScreenModel$("\\(.*?\\)".toRegex(), "") + ArgumentListWrapping:LibraryScreenModel.kt$LibraryScreenModel$("\\[.*?]".toRegex(), "") + ArgumentListWrapping:LibraryScreenModel.kt$LibraryScreenModel$("\\{.*?\\}".toRegex(), "") + ArgumentListWrapping:LibraryScreenModel.kt$LibraryScreenModel$(MR.strings.publishing_finished) + ArgumentListWrapping:LibraryScreenModel.kt$LibraryScreenModel$(it.source) + ArgumentListWrapping:LibraryTab.kt$LibraryTab$(state.categories.lastIndex) + ArgumentListWrapping:LibraryTab.kt$LibraryTab$(state.categories[screenModel.activeCategoryIndex.coerceAtMost(state.categories.lastIndex)]) + ArgumentListWrapping:LibraryUpdateJob.kt$LibraryUpdateJob$( listOf(Manga.create().copy(ogTitle = networkManga.title)), count, size, ) + ArgumentListWrapping:LibraryUpdateJob.kt$LibraryUpdateJob$(manga.id) + ArgumentListWrapping:LocalSource.kt$LocalSource$(COMIC_INFO_FILE) + ArgumentListWrapping:LocalSource.kt$LocalSource$(it, manga) + ArgumentListWrapping:MangaScreen.kt$MangaScreen$(context, navigator, screenModel.source?.getMainSource(), successState.manga) + ArgumentListWrapping:MangaScreen.kt$MangaScreen$(navigator, context, successState.manga, screenModel::smartSearchMerge) + ArgumentListWrapping:MangaScreenModel.kt$MangaScreenModel$(getTracks.await(mangaId).first { it.trackerId == trackerManager.mdList.id }, trackerManager.mdList) + ArgumentListWrapping:MangaScreenModel.kt$MangaScreenModel$(manga.source, chapters) + ArgumentListWrapping:MangaScreenModel.kt$MangaScreenModel$(mangaId) + ArgumentListWrapping:MangaScreenModel.kt$MangaScreenModel$(mangaId, applyScanlatorFilter = true) + ArgumentListWrapping:MigrationListScreenModel.kt$MigrationListScreenModel$(localManga.toSManga(), throttleManager::throttle) + ArgumentListWrapping:ReaderActivity.kt$ReaderActivity$(SYMR.strings.share_pages_info, manga.title, chapter.name, if (resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_LTR) "${page.number}-${page.number + 1}" else "${page.number + 1}-${page.number}") + ArgumentListWrapping:ReaderPreferences.kt$ReaderPreferences$("reader_bottom_buttons", ReaderBottomButton.BUTTONS_DEFAULTS) + ArgumentListWrapping:SimilarHandler.kt$SimilarHandler$( mangaList, false, List(mangaList.size) { MangaDexSearchMetadata().also { it.relation = MangaDexRelation.SIMILAR } }, ) + ArgumentListWrapping:SourcesScreenModel.kt$SourcesScreenModel$(CATEGORY_KEY_PREFIX) + ArgumentListWrapping:SourcesScreenModel.kt$SourcesScreenModel$(it.key.removePrefix(CATEGORY_KEY_PREFIX), it.value.firstOrNull()?.category != null) + CommentSpacing:ChapterCache.kt$ChapterCache$//S Y <-- + CommentSpacing:LibraryUpdateJob.kt$LibraryUpdateJob$//SY <-- + CommentWrapping:BackupCreator.kt$BackupCreator$/* SY --> */ + CommentWrapping:BackupRestorer.kt$BackupRestorer$/* SY --> */ + CommentWrapping:BackupRestorer.kt$BackupRestorer$/* SY <-- */ + CommentWrapping:BrowseSourceComfortableGrid.kt$/* SY --> */ + CommentWrapping:BrowseSourceComfortableGrid.kt$/* SY <-- */ + CommentWrapping:BrowseSourceCompactGrid.kt$/* SY --> */ + CommentWrapping:BrowseSourceCompactGrid.kt$/* SY <-- */ + CommentWrapping:BrowseSourceEHentaiList.kt$/* SY --> */ + CommentWrapping:BrowseSourceEHentaiList.kt$/* SY <-- */ + CommentWrapping:BrowseSourceList.kt$/* SY --> */ + CommentWrapping:BrowseSourceList.kt$/* SY <-- */ + CommentWrapping:BrowseSourceScreen.kt$/* SY --> */ + CommentWrapping:BrowseSourceScreen.kt$/* SY <-- */ + CommentWrapping:BrowseSourceScreen.kt$BrowseSourceScreen$/* SY --> */ + CommentWrapping:BrowseSourceScreen.kt$BrowseSourceScreen$/* SY <-- */ + CommentWrapping:BuildConfig.kt$/* SY --> */ + CommentWrapping:BuildConfig.kt$/* SY <-- */ + CommentWrapping:ChapterFilter.kt$/* SY --> */ + CommentWrapping:ChapterFilter.kt$/* SY <-- */ + CommentWrapping:ChapterFilterDownloaded.kt$/* SY --> */ + CommentWrapping:ChapterGetNextUnread.kt$/* SY --> */ + CommentWrapping:ChapterGetNextUnread.kt$/* SY <-- */ + CommentWrapping:ChapterLoader.kt$ChapterLoader$/* --> EH */ + CommentWrapping:ChapterLoader.kt$ChapterLoader$/* SY --> */ + CommentWrapping:ChapterLoader.kt$ChapterLoader$/* SY <-- */ + CommentWrapping:ClearDatabaseScreen.kt$ClearDatabaseScreenModel$/* SY --> */ + CommentWrapping:ClearDatabaseScreen.kt$ClearDatabaseScreenModel$/* SY <-- */ + CommentWrapping:CollapsibleBox.kt$/* SY --> */ + CommentWrapping:CollapsibleBox.kt$/* SY <-- */ + CommentWrapping:CreateCategoryWithName.kt$CreateCategoryWithName$/* SY --> */ + CommentWrapping:CreateCategoryWithName.kt$CreateCategoryWithName$/* SY <-- */ + CommentWrapping:DownloadCache.kt$DownloadCache$/* SY --> */ + CommentWrapping:DownloadCache.kt$DownloadCache$/* SY <-- */ + CommentWrapping:DownloadManager.kt$DownloadManager$/* SY --> */ + CommentWrapping:DownloadManager.kt$DownloadManager$/* SY <-- */ + CommentWrapping:DownloadPageLoader.kt$DownloadPageLoader$/* SY --> */ + CommentWrapping:DownloadPageLoader.kt$DownloadPageLoader$/* SY <-- */ + CommentWrapping:DownloadPendingDeleter.kt$DownloadPendingDeleter$/* SY --> */ + CommentWrapping:DownloadPendingDeleter.kt$DownloadPendingDeleter$/* SY <-- */ + CommentWrapping:DownloadProvider.kt$DownloadProvider$/* SY --> */ + CommentWrapping:DownloadProvider.kt$DownloadProvider$/* SY <-- */ + CommentWrapping:Downloader.kt$Downloader$/* SY --> */ + CommentWrapping:Downloader.kt$Downloader$/* SY <-- */ + CommentWrapping:EXHMigrations.kt$EXHMigrations$/* ALPHABETICAL */ + CommentWrapping:GetExtensionsByType.kt$GetExtensionsByType$/* SY --> */ + CommentWrapping:GetExtensionsByType.kt$GetExtensionsByType$/* SY <-- */ + CommentWrapping:HistoryScreenModelStateProvider.kt$HistoryScreenModelStateProvider.HistoryUiModelExamples$/* SY --> */ + CommentWrapping:HistoryScreenModelStateProvider.kt$HistoryScreenModelStateProvider.HistoryUiModelExamples$/* SY <-- */ + CommentWrapping:HomeScreen.kt$HomeScreen$/* SY --> */ + CommentWrapping:HomeScreen.kt$HomeScreen$/* SY <-- */ + CommentWrapping:HttpPageLoader.kt$HttpPageLoader$/* SY --> */ + CommentWrapping:HttpSource.kt$HttpSource$/* SY <-- */ + CommentWrapping:LibraryScreenModel.kt$LibraryScreenModel$/* SY --> */ + CommentWrapping:LibraryScreenModel.kt$LibraryScreenModel$/* SY <-- */ + CommentWrapping:LibrarySortMode.kt$LibrarySort.Companion$/* SY -->*/ + CommentWrapping:LibrarySortMode.kt$LibrarySort.Companion$/* SY <--*/ + CommentWrapping:LocalSource.kt$LocalSource$/* SY --> */ + CommentWrapping:LocalSource.kt$LocalSource$/* SY <-- */ + CommentWrapping:MangaBackupCreator.kt$/* SY --> */ + CommentWrapping:MangaBackupCreator.kt$/* SY <-- */ + CommentWrapping:MangaBackupCreator.kt$MangaBackupCreator$/* SY <-- */ + CommentWrapping:MangaBottomActionMenu.kt$/* SY --> */ + CommentWrapping:MangaBottomActionMenu.kt$/* SY <-- */ + CommentWrapping:MangaChapterListItem.kt$/* SY --> */ + CommentWrapping:MangaChapterListItem.kt$/* SY <-- */ + CommentWrapping:MangaScreen.kt$/* SY --> */ + CommentWrapping:MangaScreen.kt$/* SY <-- */ + CommentWrapping:MangaScreenModel.kt$MangaScreenModel$/* SY --> */ + CommentWrapping:MangaScreenModel.kt$MangaScreenModel$/* SY <-- */ + CommentWrapping:MangasPage.kt$MangasPage$/* SY <-- */ + CommentWrapping:NetworkHelper.kt$NetworkHelper$/* SY <-- */ + CommentWrapping:NetworkToLocalManga.kt$NetworkToLocalManga$/* SY --> */ + CommentWrapping:NetworkToLocalManga.kt$NetworkToLocalManga$/* SY <-- */ + CommentWrapping:Page.kt$Page$/* SY <-- */ + CommentWrapping:PagePreviewScreen.kt$/* TODO support unknown pageCount || state.hasNextPage*/ + CommentWrapping:ReaderActivity.kt$ReaderActivity$/* SY --> */ + CommentWrapping:ReaderActivity.kt$ReaderActivity$/* SY <-- */ + CommentWrapping:ReaderActivity.kt$ReaderActivity$/*else if (page.status == Page.LOAD_PAGE || page.status == Page.DOWNLOAD_IMAGE) { // Do nothing }*/ + CommentWrapping:ReaderActivity.kt$ReaderActivity.Companion$/* SY --> */ + CommentWrapping:ReaderActivity.kt$ReaderActivity.Companion$/* SY <-- */ + CommentWrapping:ReaderAppBars.kt$/*SY --> */ + CommentWrapping:ReaderAppBars.kt$/*SY <-- */ + CommentWrapping:ReaderTransitionView.kt$ReaderTransitionView$/* SY --> */ + CommentWrapping:ReaderTransitionView.kt$ReaderTransitionView$/* SY <-- */ + CommentWrapping:ReaderViewModel.kt$ReaderViewModel$/* SY --> */ + CommentWrapping:ReaderViewModel.kt$ReaderViewModel$/* SY <-- */ + CommentWrapping:ReaderViewModel.kt$ReaderViewModel.Dialog.PageActions$/* SY --> */ + CommentWrapping:ReaderViewModel.kt$ReaderViewModel.Dialog.PageActions$/* SY <-- */ + CommentWrapping:ReaderViewModel.kt$ReaderViewModel.Event.ShareImage$/* SY --> */ + CommentWrapping:ReaderViewModel.kt$ReaderViewModel.Event.ShareImage$/* SY <-- */ + CommentWrapping:RestoreOptions.kt$RestoreOptions$/* SY --> */ + CommentWrapping:RestoreOptions.kt$RestoreOptions$/* SY <-- */ + CommentWrapping:SecureActivityDelegate.kt$SecureActivityDelegate.Companion$/* SY --> */ + CommentWrapping:SecureActivityDelegate.kt$SecureActivityDelegate.Companion$/* SY <-- */ + CommentWrapping:SourceFilterDialog.kt$/* SY --> */ + CommentWrapping:SourceFilterDialog.kt$/* SY <-- */ + CommentWrapping:SourcePagingSource.kt$SourcePagingSource$/*SY --> */ + CommentWrapping:SourcePagingSource.kt$SourcePagingSource$/*SY <-- */ + CommentWrapping:SourceRepository.kt$/*SY --> */ + CommentWrapping:SourceRepository.kt$/*SY <-- */ + CommentWrapping:SourcesScreen.kt$/* SY --> */ + CommentWrapping:SourcesScreen.kt$/* SY <-- */ + CommentWrapping:SyncChaptersWithSource.kt$SyncChaptersWithSource$/* SY --> */ + CommentWrapping:SyncChaptersWithSource.kt$SyncChaptersWithSource$/* SY <-- */ + CommentWrapping:TrackChapter.kt$TrackChapter$/* SY --> */ + CommentWrapping:TrackChapter.kt$TrackChapter$/* SY <-- */ + CommentWrapping:UpdatesUiItem.kt$/* SY --> */ + CommentWrapping:UpdatesUiItem.kt$/* SY <-- */ + ComplexCondition:CustomMangaRepositoryImpl.kt$CustomMangaRepositoryImpl$mangaInfo.title == null && mangaInfo.author == null && mangaInfo.artist == null && mangaInfo.thumbnailUrl == null && mangaInfo.description == null && mangaInfo.genre == null && mangaInfo.status == null + ComplexCondition:EHentaiUpdateWorker.kt$EHentaiUpdateWorker$(new.isNotEmpty() && manga.id == acceptedRoot.manga.id) || (hasNew && updatedManga.none { it.first.id == acceptedRoot.manga.id }) + ComplexCondition:LibraryUpdateJob.kt$LibraryUpdateJob$group == LibraryGroup.BY_DEFAULT || groupLibraryUpdateType == GroupLibraryMode.GLOBAL || (groupLibraryUpdateType == GroupLibraryMode.ALL_BUT_UNGROUPED && group == LibraryGroup.UNGROUPED) + ComplexCondition:MangaRestorer.kt$MangaRestorer$customTitle != null || customArtist != null || customAuthor != null || customThumbnailUrl != null || customDescription != null || customGenre != null || customStatus != 0 ComplexCondition:MangaScreenModel.kt$MangaScreenModel$(selectedItem.selected && selected) || (!selectedItem.selected && !selected) + ComplexCondition:PagerPageHolder.kt$PagerPageHolder$imageStream is BufferedInputStream && !ImageUtil.isAnimatedAndSupported(imageStream) && ImageUtil.isWideImage(imageStream) && viewer.config.centerMarginType and PagerConfig.CenterMarginType.WIDE_PAGE_CENTER_MARGIN > 0 && !viewer.config.imageCropBorders + ComplexCondition:PagerViewerAdapter.kt$PagerViewerAdapter$items[itemIndex]?.fullPage == true && itemIndex > 0 && items[itemIndex - 1] != null && (itemIndex - 1) % 2 == 0 + ComplexCondition:ReaderActivity.kt$ReaderActivity$readerPreferences.useAutoWebtoon().get() && (manga?.readingMode?.toInt() ?: ReadingMode.DEFAULT.flagValue) == ReadingMode.DEFAULT.flagValue && defaultReaderType != null && defaultReaderType == ReadingMode.WEBTOON.flagValue ComplexCondition:ReaderNavigationOverlayView.kt$ReaderNavigationOverlayView$isVisible || (!showOnStart && firstLaunch) || navigation is DisabledNavigation ComplexCondition:ReaderPageImageView.kt$ReaderPageImageView$config != null && config!!.landscapeZoom && config!!.minimumScaleType == SCALE_TYPE_CENTER_INSIDE && sWidth > sHeight && scale == minScale + ComplexCondition:SearchEngine.kt$SearchEngine$char == '-' && !inQuotes && (queuedRawText.isBlank() || queuedRawText.last() == ' ') + ComplexCondition:SourceTagsUtil.kt$SourceTagsUtil$sourceId == EXH_SOURCE_ID || sourceId == EH_SOURCE_ID || sourceId in nHentaiSourceIds || sourceId in mangaDexSourceIds || sourceId == PURURIN_SOURCE_ID || sourceId == TSUMINO_SOURCE_ID + ComplexCondition:TrackChapter.kt$TrackChapter$service == null || !service.isLoggedIn || chapterNumber <= track.lastChapterRead /* SY --> */ || (service is MdList && track.status == FollowStatus.UNFOLLOWED.long) + ComposableParamOrder:AutoCompleteItem.kt$AutoCompleteTextField ComposableParamOrder:ChapterSettingsDialog.kt$ChapterSettingsDialog + ComposableParamOrder:FeedScreen.kt$RadioSelector + ComposableParamOrder:GlobalSearchResultItems.kt$GlobalSearchResultItem ComposableParamOrder:LazyLibraryGrid.kt$LazyLibraryGrid ComposableParamOrder:SwitchPreferenceWidget.kt$SwitchPreferenceWidget ComposableParamOrder:TrackingPreferenceWidget.kt$TrackingPreferenceWidget @@ -50,45 +228,94 @@ ConstructorParameterNaming:MyAnimeListModels.kt$OAuth$val expires_in: Long ConstructorParameterNaming:MyAnimeListModels.kt$OAuth$val refresh_token: String ConstructorParameterNaming:MyAnimeListModels.kt$OAuth$val token_type: String + ConstructorParameterNaming:PagePreviewSource.kt$PagePreviewInfo$@Transient private val _progress: MutableStateFlow<Int> = MutableStateFlow(-1) ConstructorParameterNaming:ShikimoriModels.kt$OAuth$val access_token: String ConstructorParameterNaming:ShikimoriModels.kt$OAuth$val created_at: Long ConstructorParameterNaming:ShikimoriModels.kt$OAuth$val expires_in: Long ConstructorParameterNaming:ShikimoriModels.kt$OAuth$val refresh_token: String? ConstructorParameterNaming:ShikimoriModels.kt$OAuth$val token_type: String CyclomaticComplexMethod:AnilistModels.kt$fun DomainTrack.toAnilistScore(): String - CyclomaticComplexMethod:BrowseSourceScreen.kt$@Composable fun BrowseSourceContent( source: Source?, mangaList: LazyPagingItems<StateFlow<Manga>>, columns: GridCells, displayMode: LibraryDisplayMode, snackbarHostState: SnackbarHostState, contentPadding: PaddingValues, onWebViewClick: () -> Unit, onHelpClick: () -> Unit, onLocalSourceHelpClick: () -> Unit, onMangaClick: (Manga) -> Unit, onMangaLongClick: (Manga) -> Unit, ) + CyclomaticComplexMethod:ApiMangaParser.kt$ApiMangaParser$fun parseIntoMetadata( metadata: MangaDexSearchMetadata, mangaDto: MangaDto, simpleChapters: List<String>, statistics: StatisticsMangaDto?, ) + CyclomaticComplexMethod:BottomReaderBar.kt$@Composable fun BottomReaderBar( // SY --> enabledButtons: ImmutableSet<String>, // SY <-- backgroundColor: Color, readingMode: ReadingMode, onClickReadingMode: () -> Unit, orientation: ReaderOrientation, onClickOrientation: () -> Unit, cropEnabled: Boolean, onClickCropBorder: () -> Unit, onClickSettings: () -> Unit, // SY --> currentReadingMode: ReadingMode, dualPageSplitEnabled: Boolean, doublePages: Boolean, onClickChapterList: () -> Unit, onClickWebView: (() -> Unit)?, onClickShare: (() -> Unit)?, onClickPageLayout: () -> Unit, onClickShiftPage: () -> Unit, // SY <-- ) + CyclomaticComplexMethod:BrowseSourceEHentaiList.kt$@Composable fun BrowseSourceEHentaiListItem( manga: Manga, // SY --> metadata: RaisedSearchMetadata?, // SY <-- onClick: () -> Unit = {}, onLongClick: () -> Unit = onClick, ) + CyclomaticComplexMethod:BrowseSourceScreen.kt$@Composable fun BrowseSourceContent( source: Source?, mangaList: LazyPagingItems<StateFlow</* SY --> */Pair<Manga, RaisedSearchMetadata?>/* SY <-- */>>, columns: GridCells, // SY --> ehentaiBrowseDisplayMode: Boolean, // SY <-- displayMode: LibraryDisplayMode, snackbarHostState: SnackbarHostState, contentPadding: PaddingValues, // SY --> onWebViewClick: (() -> Unit)?, onHelpClick: (() -> Unit)?, onLocalSourceHelpClick: (() -> Unit)?, // SY <-- onMangaClick: (Manga) -> Unit, onMangaLongClick: (Manga) -> Unit, ) CyclomaticComplexMethod:BrowseSourceScreen.kt$BrowseSourceScreen$@Composable override fun Content() CyclomaticComplexMethod:BrowseSourceScreenModel.kt$BrowseSourceScreenModel$fun searchGenre(genreName: String) CyclomaticComplexMethod:Button.kt$ButtonElevation$@Composable private fun animateElevation( enabled: Boolean, interactionSource: InteractionSource, ): State<Dp> + CyclomaticComplexMethod:ChapterLoader.kt$ChapterLoader$private fun getPageLoader(chapter: ReaderChapter): PageLoader + CyclomaticComplexMethod:Chip.kt$ChipElevation$@Composable private fun animateElevation( enabled: Boolean, interactionSource: InteractionSource, ): State<Dp> CyclomaticComplexMethod:DownloadCache.kt$DownloadCache$private fun renewCache() + CyclomaticComplexMethod:EHentai.kt$EHentai$override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request + CyclomaticComplexMethod:EHentai.kt$EHentai$override suspend fun parseIntoMetadata(metadata: EHentaiSearchMetadata, input: Document) + CyclomaticComplexMethod:EHentai.kt$EHentai$private fun extendedGenericMangaParse(doc: Document) + CyclomaticComplexMethod:EHentaiDescriptionAdapter.kt$@Composable fun EHentaiDescription(state: State.Success, openMetadataViewer: () -> Unit, search: (String) -> Unit) + CyclomaticComplexMethod:EHentaiUpdateWorker.kt$EHentaiUpdateWorker$private suspend fun startUpdating() + CyclomaticComplexMethod:EXHMigrations.kt$EXHMigrations$fun upgrade( context: Context, preferenceStore: PreferenceStore, basePreferences: BasePreferences, uiPreferences: UiPreferences, networkPreferences: NetworkPreferences, sourcePreferences: SourcePreferences, securityPreferences: SecurityPreferences, libraryPreferences: LibraryPreferences, readerPreferences: ReaderPreferences, backupPreferences: BackupPreferences, trackerManager: TrackerManager, pagePreviewCache: PagePreviewCache, ): Boolean + CyclomaticComplexMethod:EditMangaDialog.kt$private fun onViewCreated(manga: Manga, context: Context, binding: EditMangaDialogBinding, scope: CoroutineScope) + CyclomaticComplexMethod:EditMergedSettingsHeaderAdapter.kt$EditMergedSettingsHeaderAdapter.HeaderViewHolder$fun bind() + CyclomaticComplexMethod:EhUConfigBuilder.kt$EhUConfigBuilder$fun build(hathPerks: EHHathPerksResponse): FormBody CyclomaticComplexMethod:ExtensionLoader.kt$ExtensionLoader$private fun loadExtension(context: Context, extensionInfo: ExtensionInfo): LoadResult CyclomaticComplexMethod:ExtensionsScreen.kt$@Composable private fun ExtensionContent( state: ExtensionsScreenModel.State, contentPadding: PaddingValues, onLongClickItem: (Extension) -> Unit, onClickItemCancel: (Extension) -> Unit, onOpenWebView: (Extension.Available) -> Unit, onInstallExtension: (Extension.Available) -> Unit, onUninstallExtension: (Extension) -> Unit, onUpdateExtension: (Extension.Installed) -> Unit, onTrustExtension: (Extension.Untrusted) -> Unit, onOpenExtension: (Extension.Installed) -> Unit, onClickUpdateAll: () -> Unit, ) CyclomaticComplexMethod:ExtensionsScreen.kt$@Composable private fun ExtensionItemContent( extension: Extension, installStep: InstallStep, modifier: Modifier = Modifier, ) + CyclomaticComplexMethod:FilterHandler.kt$FilterHandler$fun getQueryMap(filters: FilterList): Map<String, Any> + CyclomaticComplexMethod:FilterSerializer.kt$FilterSerializer$fun deserialize(filter: Filter<Any?>, json: JsonObject) + CyclomaticComplexMethod:GalleryAdder.kt$GalleryAdder$suspend fun addGallery( context: Context, url: String, fav: Boolean = false, forceSource: UrlImportableSource? = null, throttleFunc: suspend () -> Unit = {}, retry: Int = 1, ): GalleryAddEvent CyclomaticComplexMethod:GetSourcesWithFavoriteCount.kt$GetSourcesWithFavoriteCount$private fun sortFn( direction: SetMigrateSorting.Direction, sorting: SetMigrateSorting.Mode, ): java.util.Comparator<Pair<Source, Long>> CyclomaticComplexMethod:ImageUtil.kt$ImageUtil$fun chooseBackground(context: Context, imageStream: InputStream): Drawable - CyclomaticComplexMethod:LibraryScreenModel.kt$LibraryScreenModel$private fun LibraryMap.applySort( // Map<MangaId, List<Track>> trackMap: Map<Long, List<Track>>, ): LibraryMap + CyclomaticComplexMethod:LewdMangaChecker.kt$private fun isHentaiSource(source: String): Boolean + CyclomaticComplexMethod:LibraryScreenModel.kt$LibraryScreenModel$fun getCategoryName( context: Context, category: Category?, groupType: Int, categoryName: String, ): String + CyclomaticComplexMethod:LibraryScreenModel.kt$LibraryScreenModel$private fun LibraryMap.applySort( // Map<MangaId, List<Track>> trackMap: Map<Long, List<Track>>, /* SY --> */ groupSort: LibrarySort? = null, /* SY <-- */ ): LibraryMap + CyclomaticComplexMethod:LibraryScreenModel.kt$LibraryScreenModel$private fun filterManga( queries: List<QueryComponent>, libraryManga: LibraryManga, tracks: List<Track>?, source: Source?, checkGenre: Boolean = true, searchTags: List<SearchTag>? = null, searchTitles: List<SearchTitle>? = null, loggedInTrackServices: Map<Long, TriState>, ): Boolean + CyclomaticComplexMethod:LibraryScreenModel.kt$LibraryScreenModel$private fun getGroupedMangaItems( groupType: Int, libraryManga: List<LibraryItem>, ): LibraryMap CyclomaticComplexMethod:LibraryScreenModel.kt$LibraryScreenModel$private suspend fun LibraryMap.applyFilters( trackMap: Map<Long, List<Track>>, loggedInTrackers: Map<Long, TriState>, ): LibraryMap + CyclomaticComplexMethod:LibrarySortMode.kt$LibrarySort.Companion$fun deserialize(serialized: String): LibrarySort CyclomaticComplexMethod:LibraryTab.kt$LibraryTab$@Composable override fun Content() - CyclomaticComplexMethod:LibraryUpdateJob.kt$LibraryUpdateJob$private suspend fun addMangaToQueue(categoryId: Long) + CyclomaticComplexMethod:LibraryUpdateJob.kt$LibraryUpdateJob$private suspend fun addMangaToQueue(categoryId: Long, group: Int, groupExtra: String?) + CyclomaticComplexMethod:LibraryUpdateJob.kt$LibraryUpdateJob$private suspend fun updateChapterList() + CyclomaticComplexMethod:LocalFavoritesStorage.kt$LocalFavoritesStorage$private fun FavoriteEntry.urlEquals(other: FavoriteEntry) CyclomaticComplexMethod:LocalSource.kt$LocalSource$override suspend fun getMangaDetails(manga: SManga): SManga + CyclomaticComplexMethod:LocalSource.kt$LocalSource$override suspend fun getSearchManga(page: Int, query: String, filters: FilterList): MangasPage CyclomaticComplexMethod:LocalSource.kt$LocalSource$private fun updateCover(chapter: SChapter, manga: SManga): UniFile? CyclomaticComplexMethod:MainActivity.kt$MainActivity$override fun onCreate(savedInstanceState: Bundle?) CyclomaticComplexMethod:MainActivity.kt$MainActivity$private fun handleIntentAction(intent: Intent, navigator: Navigator): Boolean + CyclomaticComplexMethod:MangaBackupCreator.kt$MangaBackupCreator$private suspend fun backupManga(manga: Manga, options: BackupOptions): BackupManga + CyclomaticComplexMethod:MangaChapterListItem.kt$@Composable fun MangaChapterListItem( title: String, date: String?, readProgress: String?, scanlator: String?, // SY --> sourceName: String?, // SY <-- read: Boolean, bookmark: Boolean, selected: Boolean, downloadIndicatorEnabled: Boolean, downloadStateProvider: () -> Download.State, downloadProgressProvider: () -> Int, chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction, chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction, onLongClick: () -> Unit, onClick: () -> Unit, onDownloadClick: ((ChapterDownloadAction) -> Unit)?, onChapterSwipe: (LibraryPreferences.ChapterSwipeAction) -> Unit, modifier: Modifier = Modifier, ) CyclomaticComplexMethod:MangaInfoHeader.kt$@Composable private fun ColumnScope.MangaContentInfo( title: String, doSearch: (query: String, global: Boolean) -> Unit, author: String?, artist: String?, status: Long, sourceName: String, isStubSource: Boolean, textAlign: TextAlign? = LocalTextStyle.current.textAlign, ) CyclomaticComplexMethod:MangaScreen.kt$MangaScreen$@Composable override fun Content() CyclomaticComplexMethod:MangaScreenModel.kt$MangaScreenModel$fun toggleSelection( item: ChapterList.Item, selected: Boolean, userSelected: Boolean = false, fromLongPress: Boolean = false, ) - CyclomaticComplexMethod:MigrateDialog.kt$MigrateDialogScreenModel$private suspend fun migrateMangaInternal( oldSource: Source?, newSource: Source, oldManga: Manga, newManga: Manga, sourceChapters: List<SChapter>, replace: Boolean, flags: Int, ) + CyclomaticComplexMethod:MangaToolbar.kt$@Composable fun MangaToolbar( title: String, titleAlphaProvider: () -> Float, hasFilters: Boolean, onBackClicked: () -> Unit, onClickFilter: () -> Unit, onClickShare: (() -> Unit)?, onClickDownload: ((DownloadAction) -> Unit)?, onClickEditCategory: (() -> Unit)?, onClickRefresh: () -> Unit, onClickMigrate: (() -> Unit)?, // SY --> onClickEditInfo: (() -> Unit)?, onClickRecommend: (() -> Unit)?, onClickMerge: (() -> Unit)?, onClickMergedSettings: (() -> Unit)?, // SY <-- // For action mode actionModeCounter: Int, onSelectAll: () -> Unit, onInvertSelection: () -> Unit, modifier: Modifier = Modifier, backgroundAlphaProvider: () -> Float = titleAlphaProvider, ) + CyclomaticComplexMethod:MangaType.kt$fun Manga.mangaType(sourceName: String? = Injekt.get<SourceManager>().get(source)?.name): MangaType + CyclomaticComplexMethod:MangaType.kt$private fun isComicSource(sourceName: String): Boolean + CyclomaticComplexMethod:MangaType.kt$private fun isManhwaSource(sourceName: String): Boolean + CyclomaticComplexMethod:MetadataUIUtil.kt$MetadataUIUtil$fun getGenreAndColour(context: Context, genre: String) + CyclomaticComplexMethod:MigrationListScreen.kt$MigrationListScreen$@Composable override fun Content() + CyclomaticComplexMethod:MigrationListScreenModel.kt$MigrationListScreenModel$private suspend fun migrateMangaInternal( prevManga: Manga, manga: Manga, replace: Boolean, ) + CyclomaticComplexMethod:MigrationListScreenModel.kt$MigrationListScreenModel$private suspend fun runMigrations(mangas: List<MigratingManga>) + CyclomaticComplexMethod:Migrations.kt$Migrations$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 + CyclomaticComplexMethod:NHentaiDescriptionAdapter.kt$@Composable fun NHentaiDescription(state: State.Success, openMetadataViewer: () -> Unit) + CyclomaticComplexMethod:NHentaiSearchMetadata.kt$NHentaiSearchMetadata$override fun createMangaInfo(manga: SManga): SManga CyclomaticComplexMethod:NotificationReceiver.kt$NotificationReceiver$override fun onReceive(context: Context, intent: Intent) + CyclomaticComplexMethod:PagerPageHolder.kt$PagerPageHolder$private fun mergePages(imageStream: InputStream, imageStream2: InputStream?): InputStream + CyclomaticComplexMethod:PagerPageHolder.kt$PagerPageHolder$private fun splitInHalf(imageStream: InputStream): InputStream CyclomaticComplexMethod:PagerViewer.kt$PagerViewer$override fun handleKeyEvent(event: KeyEvent): Boolean CyclomaticComplexMethod:PagerViewerAdapter.kt$PagerViewerAdapter$fun setChapters(chapters: ViewerChapters, forceTransition: Boolean) + CyclomaticComplexMethod:PagerViewerAdapter.kt$PagerViewerAdapter$private fun setJoinedItems(useSecondPage: Boolean = false) CyclomaticComplexMethod:PreferenceItem.kt$@Composable internal fun PreferenceItem( item: Preference.PreferenceItem<*>, highlightKey: String?, ) + CyclomaticComplexMethod:PururinDescriptionAdapter.kt$@Composable fun PururinDescription(state: State.Success, openMetadataViewer: () -> Unit) CyclomaticComplexMethod:RateLimitInterceptor.kt$RateLimitInterceptor$override fun intercept(chain: Interceptor.Chain): Response CyclomaticComplexMethod:ReaderActivity.kt$ReaderActivity$override fun onCreate(savedInstanceState: Bundle?) CyclomaticComplexMethod:ReaderActivity.kt$ReaderActivity$private fun initializeMenu() CyclomaticComplexMethod:Scaffold.kt$@Composable private fun ScaffoldLayout( fabPosition: FabPosition, topBar: @Composable () -> Unit, startBar: @Composable () -> Unit, content: @Composable (PaddingValues) -> Unit, snackbar: @Composable () -> Unit, fab: @Composable () -> Unit, contentWindowInsets: WindowInsets, bottomBar: @Composable () -> Unit, ) - CyclomaticComplexMethod:SourcesScreenModel.kt$SourcesScreenModel$private fun collectLatestSources(sources: List<Source>) + CyclomaticComplexMethod:SearchEngine.kt$SearchEngine$fun parseQuery(query: String, enableWildcard: Boolean = true) + CyclomaticComplexMethod:SettingsEhScreen.kt$SettingsEhScreen$private fun getRelativeTimeFromNow(then: Duration): RelativeTime + CyclomaticComplexMethod:SettingsEhScreen.kt$SettingsEhScreen$private fun getRelativeTimeString(relativeTime: RelativeTime, context: Context): String + CyclomaticComplexMethod:SourceTagsUtil.kt$SourceTagsUtil$fun getLocaleSourceUtil(language: String?) + CyclomaticComplexMethod:SourceTagsUtil.kt$SourceTagsUtil$fun getWrappedTag( sourceId: Long?, namespace: String? = null, tag: String? = null, fullTag: String? = null, ): String? + CyclomaticComplexMethod:SourcesScreenModel.kt$SourcesScreenModel$private fun collectLatestSources(sources: List<Source>, categories: List<String>, showLatest: Boolean, showPin: Boolean) CyclomaticComplexMethod:SyncChaptersWithSource.kt$SyncChaptersWithSource$suspend fun await( rawSourceChapters: List<SChapter>, manga: Manga, source: Source, manualFetch: Boolean = false, fetchWindow: Pair<Long, Long> = Pair(0, 0), ): List<Chapter> + CyclomaticComplexMethod:TrackStatus.kt$TrackStatus.Companion$fun parseTrackerStatus(trackerManager: TrackerManager, tracker: Long, status: Long): TrackStatus? CyclomaticComplexMethod:TriStateListDialog.kt$@Composable fun <T> TriStateListDialog( title: String, message: String? = null, items: List<T>, initialChecked: List<T>, initialInversed: List<T>, itemLabel: @Composable (T) -> String, onDismissRequest: () -> Unit, onValueChanged: (newIncluded: List<T>, newExcluded: List<T>) -> Unit, ) + CyclomaticComplexMethod:TsuminoDescriptionAdapter.kt$@Composable fun TsuminoDescription(state: State.Success, openMetadataViewer: () -> Unit) CyclomaticComplexMethod:UpdatesScreenModel.kt$UpdatesScreenModel$fun toggleSelection( item: UpdatesItem, selected: Boolean, userSelected: Boolean = false, fromLongPress: Boolean = false, ) CyclomaticComplexMethod:VerticalFastScroller.kt$@Composable fun VerticalFastScroller( listState: LazyListState, modifier: Modifier = Modifier, thumbAllowed: () -> Boolean = { true }, thumbColor: Color = MaterialTheme.colorScheme.primary, topContentPadding: Dp = Dp.Hairline, bottomContentPadding: Dp = Dp.Hairline, endContentPadding: Dp = Dp.Hairline, content: @Composable () -> Unit, ) CyclomaticComplexMethod:VerticalFastScroller.kt$@Composable fun VerticalGridFastScroller( state: LazyGridState, columns: GridCells, arrangement: Arrangement.Horizontal, contentPadding: PaddingValues, modifier: Modifier = Modifier, thumbAllowed: () -> Boolean = { true }, thumbColor: Color = MaterialTheme.colorScheme.primary, topContentPadding: Dp = Dp.Hairline, bottomContentPadding: Dp = Dp.Hairline, endContentPadding: Dp = Dp.Hairline, content: @Composable () -> Unit, ) @@ -96,18 +323,59 @@ CyclomaticComplexMethod:WebtoonViewer.kt$WebtoonViewer$override fun handleKeyEvent(event: KeyEvent): Boolean DestructuringDeclarationWithTooManyEntries:ExtensionsScreenModel.kt$ExtensionsScreenModel$(_updates, _installed, _available, _untrusted) DestructuringDeclarationWithTooManyEntries:MangaInfoHeader.kt$(shrunk, expanded, actual, scrim) + DestructuringDeclarationWithTooManyEntries:MangaScreenModel.kt$MangaScreenModel$(manga, chapters /* SY --> */, flatMetadata, mergedData /* SY <-- */) + DestructuringDeclarationWithTooManyEntries:NamespaceTags.kt$(_, text, search, border) + EmptyCatchBlock:EnhancedFilePrinter.kt$EnhancedFilePrinter.Writer${ } + EmptyCatchBlock:MigrationListScreenModel.kt$MigrationListScreenModel${ } EmptyCatchBlock:RarPageLoader.kt$RarPageLoader${ } + EmptyDefaultConstructor:FavoritesSyncHelper.kt$FavoritesSyncStatus$() + EnumEntryNameCase:EhUConfigBuilder.kt$Entry.ImageSize$1280 : ImageSize + EnumEntryNameCase:EhUConfigBuilder.kt$Entry.ImageSize$1600 : ImageSize + EnumEntryNameCase:EhUConfigBuilder.kt$Entry.ImageSize$2400 : ImageSize + EnumEntryNameCase:EhUConfigBuilder.kt$Entry.ImageSize$780 : ImageSize + EnumEntryNameCase:EhUConfigBuilder.kt$Entry.ImageSize$980 : ImageSize + EnumEntryNameCase:EhUConfigBuilder.kt$Entry.SearchResultsCount$100 : SearchResultsCount + EnumEntryNameCase:EhUConfigBuilder.kt$Entry.SearchResultsCount$200 : SearchResultsCount + EnumEntryNameCase:EhUConfigBuilder.kt$Entry.SearchResultsCount$25 : SearchResultsCount + EnumEntryNameCase:EhUConfigBuilder.kt$Entry.SearchResultsCount$50 : SearchResultsCount + EnumEntryNameCase:EhUConfigBuilder.kt$Entry.ThumbnailRows$10 : ThumbnailRows + EnumEntryNameCase:EhUConfigBuilder.kt$Entry.ThumbnailRows$20 : ThumbnailRows + EnumEntryNameCase:EhUConfigBuilder.kt$Entry.ThumbnailRows$4 : ThumbnailRows + EnumEntryNameCase:EhUConfigBuilder.kt$Entry.ThumbnailRows$40 : ThumbnailRows + EnumNaming:EhUConfigBuilder.kt$Entry.ImageSize$1280 : ImageSize + EnumNaming:EhUConfigBuilder.kt$Entry.ImageSize$1600 : ImageSize + EnumNaming:EhUConfigBuilder.kt$Entry.ImageSize$2400 : ImageSize + EnumNaming:EhUConfigBuilder.kt$Entry.ImageSize$780 : ImageSize + EnumNaming:EhUConfigBuilder.kt$Entry.ImageSize$980 : ImageSize + EnumNaming:EhUConfigBuilder.kt$Entry.SearchResultsCount$100 : SearchResultsCount + EnumNaming:EhUConfigBuilder.kt$Entry.SearchResultsCount$200 : SearchResultsCount + EnumNaming:EhUConfigBuilder.kt$Entry.SearchResultsCount$25 : SearchResultsCount + EnumNaming:EhUConfigBuilder.kt$Entry.SearchResultsCount$50 : SearchResultsCount + EnumNaming:EhUConfigBuilder.kt$Entry.ThumbnailRows$10 : ThumbnailRows + EnumNaming:EhUConfigBuilder.kt$Entry.ThumbnailRows$20 : ThumbnailRows + EnumNaming:EhUConfigBuilder.kt$Entry.ThumbnailRows$4 : ThumbnailRows + EnumNaming:EhUConfigBuilder.kt$Entry.ThumbnailRows$40 : ThumbnailRows EqualsAlwaysReturnsTrueOrFalse:FilterList.kt$FilterList$override fun equals(other: Any?): Boolean ExplicitItLambdaParameter:ColorFilterPage.kt${ index, it -> FilterChip( selected = colorFilterMode == index, onClick = { screenModel.preferences.colorFilterMode().set(index) }, label = { Text(stringResource(it.first)) }, ) } + ExplicitItLambdaParameter:FavoritesSyncHelper.kt$FavoritesSyncHelper${ index, it -> status.value = FavoritesSyncStatus.Processing( context.stringResource(SYMR.strings.favorites_sync_remove_from_local, index + 1, changeSet.removed.size), title = it.title, ) val url = it.getUrl() // Consider both EX and EH sources listOf( EXH_SOURCE_ID, EH_SOURCE_ID, ).forEach { val manga = getManga.await(url, it) if (manga?.favorite == true) { updateManga.awaitUpdateFavorite(manga.id, false) removedManga += manga } } } + ExplicitItLambdaParameter:FavoritesSyncHelper.kt$FavoritesSyncHelper${ index, it -> status.value = FavoritesSyncStatus.Processing( message = context.stringResource(SYMR.strings.favorites_sync_add_to_local, index + 1, changeSet.added.size), isThrottle = needWarnThrottle(), context = context, title = it.title, ) throttleManager.throttle() // Import using gallery adder val result = galleryAdder.addGallery( context = context, url = "${exh.baseUrl}${it.getUrl()}", fav = true, forceSource = exh, throttleFunc = throttleManager::throttle, retry = 3, ) if (result is GalleryAddEvent.Fail) { if (result is GalleryAddEvent.Fail.NotFound) { logger.e(context.stringResource(SYMR.strings.favorites_sync_remote_not_exist, it.getUrl())) // Skip this gallery, it no longer exists return@forEachIndexed } val errorString = context.stringResource(SYMR.strings.favorites_sync_failed_to_add_to_local) + when (result) { is GalleryAddEvent.Fail.Error -> context.stringResource( SYMR.strings.favorites_sync_failed_to_add_to_local_error, it.title, result.logMessage, ) is GalleryAddEvent.Fail.UnknownType -> context.stringResource( SYMR.strings.favorites_sync_failed_to_add_to_local_unknown_type, it.title, result.galleryUrl, ) is GalleryAddEvent.Fail.UnknownSource -> context.stringResource( SYMR.strings.favorites_sync_failed_to_add_to_local_unknown_type, it.title, result.galleryUrl, ) } if (prefs.exhLenientSync().get()) { errorList += errorString } else { status.value = FavoritesSyncStatus.Error(errorString) throw IgnoredException(errorString) } } else if (result is GalleryAddEvent.Success) { insertedMangaCategories += categories[it.category].id to result.manga } } + ExplicitItLambdaParameter:FavoritesSyncHelper.kt$FavoritesSyncHelper${ index, it -> status.value = FavoritesSyncStatus.Processing( message = context.stringResource(SYMR.strings.favorites_sync_adding_to_remote, index + 1, changeSet.added.size), isThrottle = needWarnThrottle(), context = context, title = it.title, ) throttleManager.throttle() addGalleryRemote(errorList, it) } ExplicitItLambdaParameter:LibraryTab.kt$LibraryTab${ it: LibraryManga -> scope.launchIO { val chapter = screenModel.getNextUnreadChapter(it.manga) if (chapter != null) { context.startActivity( ReaderActivity.newIntent(context, chapter.mangaId, chapter.id), ) } else { snackbarHostState.showSnackbar(context.stringResource(MR.strings.no_next_chapter)) } } Unit } + ExplicitItLambdaParameter:ReadingModePage.kt${ index, it -> FilterChip( selected = centerMarginType == index, onClick = { screenModel.preferences.centerMarginType().set(index) }, label = { Text(stringResource(it)) }, ) } ExplicitItLambdaParameter:ReadingModePage.kt${ index, it -> FilterChip( selected = imageScaleType == index + 1, onClick = { screenModel.preferences.imageScaleType().set(index + 1) }, label = { Text(stringResource(it)) }, ) } + ExplicitItLambdaParameter:ReadingModePage.kt${ index, it -> FilterChip( selected = pageLayout == index, onClick = { screenModel.preferences.pageLayout().set(index) }, label = { Text(stringResource(it)) }, ) } ExplicitItLambdaParameter:ReadingModePage.kt${ index, it -> FilterChip( selected = selected == index, onClick = { onSelect(index) }, label = { Text(stringResource(it)) }, ) } ExplicitItLambdaParameter:ReadingModePage.kt${ index, it -> FilterChip( selected = zoomStart == index + 1, onClick = { screenModel.preferences.zoomStart().set(index + 1) }, label = { Text(stringResource(it)) }, ) } ExplicitItLambdaParameter:SettingsReaderScreen.kt$SettingsReaderScreen${ index, it -> index + 1 to stringResource(it) } ExplicitItLambdaParameter:SettingsReaderScreen.kt$SettingsReaderScreen${ index, it -> index to stringResource(it) } + Filename:__Assets.kt$exh.assets.ehassets.exh.__Assets.kt + Filename:__EhAssets.kt$exh.assets.__EhAssets.kt + Filename:__Ehassets.kt$exh.assets.ehassets.exh.assets.__Ehassets.kt + Filename:__Exh.kt$exh.assets.ehassets.__Exh.kt FinalNewline:Commands.kt$.Commands.kt ForEachOnRange:MangaBottomActionMenu.kt$0..<5 ForEachOnRange:MangaBottomActionMenu.kt$0..<7 + ForEachOnRange:PagerViewerAdapter.kt$PagerViewerAdapter$fullPageBeforeIndex until items.size ForEachOnRange:UpdatesWidget.kt$0..<rowCount ForbiddenComment:AddTracks.kt$AddTracks$// TODO: merge into [SyncChapterProgressWithTrack]? ForbiddenComment:AddTracks.kt$AddTracks$// TODO: update all trackers based on common data @@ -132,6 +400,7 @@ ForbiddenComment:Manga.kt$// TODO: move these into the domain model ForbiddenComment:MangaCoverScreenModel.kt$MangaCoverScreenModel$// TODO: Handle animated cover ForbiddenComment:MangaInfoHeader.kt$// TODO: show something better when using custom interval + ForbiddenComment:MigrateSourceTab.kt$// TODO: Jay wtf, need to clean this up sometime ForbiddenComment:PreferenceItem.kt$// TODO: use different composable? ForbiddenComment:ReaderPreferences.kt$ReaderPreferences$// TODO: default this to true if reader long strip ever goes stable ForbiddenComment:SettingsAdvancedScreen.kt$SettingsAdvancedScreen$// TODO: allow private option in stable versions once URL handling is more fleshed out @@ -139,6 +408,7 @@ ForbiddenComment:Tracker.kt$Tracker$// TODO: move this to an interactor, and update all trackers based on common data ForbiddenComment:VerticalFastScroller.kt$// TODO: Sometimes item height is not available when scrolling up FunctionNaming:Downloader.kt$Downloader$private fun _clearQueue() + FunctionNaming:PagePreviews.kt$fun LazyListScope.PagePreviewItems( pagePreviewState: PagePreviewState, onOpenPage: (Int) -> Unit, onMorePreviewsClicked: () -> Unit, maxWidth: Dp, setMaxWidth: (Dp) -> Unit, rowCount: Int, ) FunctionNaming:Pin.kt$fun Pins(vararg pins: Pin) FunctionNaming:PullRefresh.kt$PullToRefreshStateImpl.Companion$fun Saver( extraVerticalOffset: Float, positionalThreshold: Float, enabled: () -> Boolean, onRefresh: () -> Unit, ) FunctionNaming:Requests.kt$fun DELETE( url: String, headers: Headers = DEFAULT_HEADERS, body: RequestBody = DEFAULT_BODY, cache: CacheControl = DEFAULT_CACHE_CONTROL, ): Request @@ -148,194 +418,159 @@ FunctionNaming:Requests.kt$fun PUT( url: String, headers: Headers = DEFAULT_HEADERS, body: RequestBody = DEFAULT_BODY, cache: CacheControl = DEFAULT_CACHE_CONTROL, ): Request FunctionParameterNaming:MangaScreen.kt$MangaScreen$manga_: Manga? FunctionParameterNaming:MangaScreen.kt$MangaScreen$source_: Source? + FunctionParameterNaming:SChapter.kt$SChapter.Companion$chapter_number: Float = -1F + FunctionParameterNaming:SChapter.kt$SChapter.Companion$date_upload: Long = 0 + FunctionParameterNaming:SManga.kt$SManga.Companion$thumbnail_url: String? = null + FunctionParameterNaming:SManga.kt$thumbnail_url: String? = this.originalThumbnailUrl FunctionParameterNaming:WebViewInterceptor.kt$_name: String FunctionParameterNaming:WebViewInterceptor.kt$_value: String + ImplicitDefaultLocale:MetadataUtil.kt$MetadataUtil$String.format("%.1f %sB", bytes / unit.toDouble().pow(exp.toDouble()), pre) + ImplicitDefaultLocale:TimeRange.kt$TimeRange$String.format("%02d:%02d - %02d:%02d", startHour, startMinute, endHour, endMinute) ImportOrdering:Commands.kt$import org.gradle.api.Project import java.io.ByteArrayOutputStream import java.text.SimpleDateFormat import java.util.TimeZone import java.util.Date + ImportOrdering:LocalSource.kt$import android.content.Context import com.hippo.unifile.UniFile import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.UnmeteredSource import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder import eu.kanade.tachiyomi.util.storage.CbzCrypto import eu.kanade.tachiyomi.util.storage.CbzCrypto.addStreamToZip import eu.kanade.tachiyomi.util.storage.CbzCrypto.getCoverStreamFromZip import eu.kanade.tachiyomi.util.storage.CbzCrypto.getZipInputStream import eu.kanade.tachiyomi.util.storage.CbzCrypto.isEncryptedZip import eu.kanade.tachiyomi.util.storage.EpubFile import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.serialization.json.Json import kotlinx.serialization.json.decodeFromStream import kotlinx.serialization.json.encodeToStream import logcat.LogPriority import nl.adaptivity.xmlutil.AndroidXmlReader import nl.adaptivity.xmlutil.serialization.XML import tachiyomi.core.common.i18n.stringResource import tachiyomi.core.metadata.comicinfo.COMIC_INFO_FILE import tachiyomi.core.metadata.comicinfo.ComicInfo import tachiyomi.core.metadata.comicinfo.copyFromComicInfo import tachiyomi.core.metadata.comicinfo.getComicInfo import tachiyomi.core.metadata.tachiyomi.MangaDetails import tachiyomi.core.common.storage.UniFileTempFileManager import tachiyomi.core.common.storage.extension import tachiyomi.core.common.storage.nameWithoutExtension import tachiyomi.core.common.util.lang.withIOContext import tachiyomi.core.common.util.system.ImageUtil import tachiyomi.core.common.util.system.logcat import tachiyomi.domain.chapter.service.ChapterRecognition import tachiyomi.domain.manga.model.Manga import tachiyomi.i18n.MR import tachiyomi.source.local.filter.OrderBy import tachiyomi.source.local.image.LocalCoverManager import tachiyomi.source.local.io.Archive import tachiyomi.source.local.io.Format import tachiyomi.source.local.io.LocalSourceFileSystem import tachiyomi.source.local.metadata.fillMetadata import uy.kohesive.injekt.injectLazy import java.io.InputStream import java.nio.charset.StandardCharsets import kotlin.time.Duration.Companion.days import com.github.junrar.Archive as JunrarArchive import tachiyomi.domain.source.model.Source as DomainSource + Indentation:BackupCreator.kt$BackupCreator$ Indentation:LocalesConfigPlugin.kt$ + Indentation:PagerPageHolder.kt$PagerPageHolder$ + Indentation:ReaderPreferences.kt$ReaderPreferences$ InstanceOfCheckForException:AppUpdateDownloadJob.kt$AppUpdateDownloadJob$e is CancellationException InstanceOfCheckForException:AppUpdateDownloadJob.kt$AppUpdateDownloadJob$e is StreamResetException InstanceOfCheckForException:BackupRestoreJob.kt$BackupRestoreJob$e is CancellationException InstanceOfCheckForException:Downloader.kt$Downloader$e is CancellationException InstanceOfCheckForException:Downloader.kt$Downloader$error is CancellationException + InstanceOfCheckForException:EHentaiUpdateWorker.kt$EHentaiUpdateWorker$t is EHentai.GalleryNotFoundException + InstanceOfCheckForException:GalleryAdder.kt$GalleryAdder$e is EHentai.GalleryNotFoundException InstanceOfCheckForException:HttpPageLoader.kt$HttpPageLoader$e is CancellationException InstanceOfCheckForException:LibraryUpdateJob.kt$LibraryUpdateJob$e is CancellationException + InstanceOfCheckForException:MangaScreen.kt$MangaScreen$e is CancellationException InstanceOfCheckForException:MangaScreenModel.kt$MangaScreenModel$e is HttpException InstanceOfCheckForException:MangaScreenModel.kt$MangaScreenModel$e is NoChaptersException + InstanceOfCheckForException:MergedSource.kt$MergedSource$e is CancellationException InstanceOfCheckForException:MetadataUpdateJob.kt$MetadataUpdateJob$e is CancellationException + InstanceOfCheckForException:MigrationListScreenModel.kt$MigrationListScreenModel$e is CancellationException InstanceOfCheckForException:ReaderViewModel.kt$ReaderViewModel$e is CancellationException + InstanceOfCheckForException:RxCoroutineBridge.kt$e !is CancellationException + InstanceOfCheckForException:SmartSearchScreenModel.kt$SmartSearchScreenModel$e is CancellationException InstanceOfCheckForException:UncaughtExceptionInterceptor.kt$UncaughtExceptionInterceptor$e is IOException + LargeClass:Artist.kt$Artist : TagList + LargeClass:Artist2.kt$Artist2 : TagList + LargeClass:Character.kt$Character : TagList + LargeClass:EHentai.kt$EHentai : HttpSourceMetadataSourceUrlImportableSourceNamespaceSourcePagePreviewSource + LargeClass:EXHMigrations.kt$EXHMigrations + LargeClass:Group.kt$Group : TagList + LargeClass:LibraryScreenModel.kt$LibraryScreenModel : StateScreenModel LargeClass:MangaScreenModel.kt$MangaScreenModel : StateScreenModel - LongMethod:AboutScreen.kt$AboutScreen$@Composable override fun Content() - LongMethod:AdaptiveSheet.kt$@Composable fun AdaptiveSheet( isTabletUi: Boolean, tonalElevation: Dp, enableSwipeDismiss: Boolean, onDismissRequest: () -> Unit, modifier: Modifier = Modifier, content: @Composable () -> Unit, ) + LargeClass:Parody.kt$Parody : TagList + LargeClass:ReaderActivity.kt$ReaderActivity : BaseActivity + LargeClass:ReaderViewModel.kt$ReaderViewModel : ViewModel + LargeClass:SettingsAdvancedScreen.kt$SettingsAdvancedScreen : SearchableSettings + LargeClass:SettingsEhScreen.kt$SettingsEhScreen : SearchableSettings LongMethod:AnilistApi.kt$AnilistApi$suspend fun findLibManga(track: Track, userid: Int): Track? - LongMethod:AppBar.kt$@Composable fun AppBarActions( actions: ImmutableList<AppBar.AppBarAction>, ) - LongMethod:AppBar.kt$@Composable fun SearchToolbar( searchQuery: String?, onChangeSearchQuery: (String?) -> Unit, modifier: Modifier = Modifier, titleContent: @Composable () -> Unit = {}, navigateUp: (() -> Unit)? = null, searchEnabled: Boolean = true, placeholderText: String? = null, onSearch: (String) -> Unit = {}, onClickCloseSearch: () -> Unit = { onChangeSearchQuery(null) }, actions: @Composable RowScope.() -> Unit = {}, scrollBehavior: TopAppBarScrollBehavior? = null, visualTransformation: VisualTransformation = VisualTransformation.None, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, ) + LongMethod:ApiMangaParser.kt$ApiMangaParser$fun parseIntoMetadata( metadata: MangaDexSearchMetadata, mangaDto: MangaDto, simpleChapters: List<String>, statistics: StatisticsMangaDto?, ) LongMethod:AppModule.kt$AppModule$override fun InjektRegistrar.registerInjectables() - LongMethod:AppThemePreferenceWidget.kt$@Composable fun AppThemePreviewItem( selected: Boolean, onClick: () -> Unit, ) - LongMethod:BrowseSourceScreen.kt$@Composable fun BrowseSourceContent( source: Source?, mangaList: LazyPagingItems<StateFlow<Manga>>, columns: GridCells, displayMode: LibraryDisplayMode, snackbarHostState: SnackbarHostState, contentPadding: PaddingValues, onWebViewClick: () -> Unit, onHelpClick: () -> Unit, onLocalSourceHelpClick: () -> Unit, onMangaClick: (Manga) -> Unit, onMangaLongClick: (Manga) -> Unit, ) - LongMethod:BrowseSourceScreen.kt$BrowseSourceScreen$@Composable override fun Content() - LongMethod:BrowseSourceToolbar.kt$@Composable fun BrowseSourceToolbar( searchQuery: String?, onSearchQueryChange: (String?) -> Unit, source: Source?, displayMode: LibraryDisplayMode, onDisplayModeChange: (LibraryDisplayMode) -> Unit, navigateUp: () -> Unit, onWebViewClick: () -> Unit, onHelpClick: () -> Unit, onSettingsClick: () -> Unit, onSearch: (String) -> Unit, scrollBehavior: TopAppBarScrollBehavior? = null, ) - LongMethod:Button.kt$ButtonElevation$@Composable private fun animateElevation( enabled: Boolean, interactionSource: InteractionSource, ): State<Dp> - LongMethod:CategoryDialogs.kt$@Composable fun ChangeCategoryDialog( initialSelection: ImmutableList<CheckboxState<Category>>, onDismissRequest: () -> Unit, onEditCategories: () -> Unit, onConfirm: (List<Long>, List<Long>) -> Unit, ) - LongMethod:ChapterDownloadIndicator.kt$@Composable private fun DownloadingIndicator( enabled: Boolean, downloadState: Download.State, downloadProgressProvider: () -> Int, onClick: (ChapterDownloadAction) -> Unit, modifier: Modifier = Modifier, ) - LongMethod:ChapterNavigator.kt$@Composable fun ChapterNavigator( isRtl: Boolean, onNextChapter: () -> Unit, enabledNext: Boolean, onPreviousChapter: () -> Unit, enabledPrevious: Boolean, currentPage: Int, totalPages: Int, onSliderValueChange: (Int) -> Unit, ) - LongMethod:ChapterSettingsDialog.kt$@Composable fun ChapterSettingsDialog( onDismissRequest: () -> Unit, manga: Manga? = null, onDownloadFilterChanged: (TriState) -> Unit, onUnreadFilterChanged: (TriState) -> Unit, onBookmarkedFilterChanged: (TriState) -> Unit, scanlatorFilterActive: Boolean, onScanlatorFilterClicked: (() -> Unit), onSortModeChanged: (Long) -> Unit, onDisplayModeChanged: (Long) -> Unit, onSetAsDefault: (applyToExistingManga: Boolean) -> Unit, onResetToDefault: () -> Unit, ) - LongMethod:ClearDatabaseScreen.kt$ClearDatabaseScreen$@Composable override fun Content() - LongMethod:ColorFilterPage.kt$@Composable internal fun ColumnScope.ColorFilterPage(screenModel: ReaderSettingsScreenModel) - LongMethod:CreateBackupScreen.kt$CreateBackupScreen$@Composable override fun Content() + LongMethod:Artist.kt$Artist$override fun getTags1(): List<String> + LongMethod:Artist.kt$Artist$override fun getTags2(): List<String> + LongMethod:Artist.kt$Artist$override fun getTags3(): List<String> + LongMethod:Artist2.kt$Artist2$override fun getTags1(): List<String> + LongMethod:Artist2.kt$Artist2$override fun getTags2(): List<String> + LongMethod:Artist2.kt$Artist2$override fun getTags3(): List<String> + LongMethod:BatchAddScreenModel.kt$BatchAddScreenModel$private fun addGalleries(context: Context, galleries: String) + LongMethod:ChapterLoader.kt$ChapterLoader$private fun getPageLoader(chapter: ReaderChapter): PageLoader + LongMethod:Character.kt$Character$override fun getTags1(): List<String> + LongMethod:Character.kt$Character$override fun getTags2(): List<String> + LongMethod:Character.kt$Character$override fun getTags3(): List<String> + LongMethod:Cosplayer.kt$Cosplayer$override fun getTags1(): List<String> LongMethod:DomainModule.kt$DomainModule$override fun InjektRegistrar.registerInjectables() LongMethod:DownloadCache.kt$DownloadCache$private fun renewCache() - LongMethod:DownloadQueueScreen.kt$DownloadQueueScreen$@Composable override fun Content() LongMethod:Downloader.kt$Downloader$private suspend fun downloadChapter(download: Download) - LongMethod:EditTextPreferenceWidget.kt$@Composable fun EditTextPreferenceWidget( title: String, subtitle: String?, icon: ImageVector?, value: String, onConfirm: suspend (String) -> Boolean, ) - LongMethod:ExtensionDetailsScreen.kt$@Composable fun ExtensionDetailsScreen( navigateUp: () -> Unit, state: ExtensionDetailsScreenModel.State, onClickSourcePreferences: (sourceId: Long) -> Unit, onClickEnableAll: () -> Unit, onClickDisableAll: () -> Unit, onClickClearCookies: () -> Unit, onClickUninstall: () -> Unit, onClickSource: (sourceId: Long) -> Unit, ) - LongMethod:ExtensionDetailsScreen.kt$@Composable private fun DetailsHeader( extension: Extension, onClickAgeRating: () -> Unit, onClickUninstall: () -> Unit, onClickAppInfo: (() -> Unit)?, ) + LongMethod:EHentai.kt$EHentai$override suspend fun parseIntoMetadata(metadata: EHentaiSearchMetadata, input: Document) + LongMethod:EHentai.kt$EHentai$private fun extendedGenericMangaParse(doc: Document) + LongMethod:EHentai.kt$EHentai$suspend fun getChapterList(manga: SManga, throttleFunc: suspend () -> Unit): List<SChapter> + LongMethod:EHentaiUpdateHelper.kt$EHentaiUpdateHelper$private fun getChapterList( accepted: ChapterChain, toDiscard: List<ChapterChain>, chainsAsChapters: List<Chapter>, ): Triple<List<ChapterUpdate>, List<Chapter>, Boolean> + LongMethod:EHentaiUpdateHelper.kt$EHentaiUpdateHelper$suspend fun findAcceptedRootAndDiscardOthers( sourceId: Long, chapters: List<Chapter>, ): Triple<ChapterChain, List<ChapterChain>, Boolean> + LongMethod:EHentaiUpdateWorker.kt$EHentaiUpdateWorker$private suspend fun startUpdating() + LongMethod:EXHMigrations.kt$EXHMigrations$fun upgrade( context: Context, preferenceStore: PreferenceStore, basePreferences: BasePreferences, uiPreferences: UiPreferences, networkPreferences: NetworkPreferences, sourcePreferences: SourcePreferences, securityPreferences: SecurityPreferences, libraryPreferences: LibraryPreferences, readerPreferences: ReaderPreferences, backupPreferences: BackupPreferences, trackerManager: TrackerManager, pagePreviewCache: PagePreviewCache, ): Boolean + LongMethod:EditMangaDialog.kt$private fun onViewCreated(manga: Manga, context: Context, binding: EditMangaDialogBinding, scope: CoroutineScope) + LongMethod:EditMergedSettingsHeaderAdapter.kt$EditMergedSettingsHeaderAdapter.HeaderViewHolder$fun bind() + LongMethod:EhUConfigBuilder.kt$EhUConfigBuilder$fun build(hathPerks: EHHathPerksResponse): FormBody LongMethod:ExtensionLoader.kt$ExtensionLoader$private fun loadExtension(context: Context, extensionInfo: ExtensionInfo): LoadResult - LongMethod:ExtensionsScreen.kt$@Composable private fun ExtensionContent( state: ExtensionsScreenModel.State, contentPadding: PaddingValues, onLongClickItem: (Extension) -> Unit, onClickItemCancel: (Extension) -> Unit, onOpenWebView: (Extension.Available) -> Unit, onInstallExtension: (Extension.Available) -> Unit, onUninstallExtension: (Extension) -> Unit, onUpdateExtension: (Extension.Installed) -> Unit, onTrustExtension: (Extension.Untrusted) -> Unit, onOpenExtension: (Extension.Installed) -> Unit, onClickUpdateAll: () -> Unit, ) - LongMethod:ExtensionsScreen.kt$@Composable private fun ExtensionItemActions( extension: Extension, installStep: InstallStep, modifier: Modifier = Modifier, onClickItemCancel: (Extension) -> Unit = {}, onClickItemAction: (Extension) -> Unit = {}, onClickItemSecondaryAction: (Extension) -> Unit = {}, ) - LongMethod:GlobalSearchToolbar.kt$@Composable fun GlobalSearchToolbar( searchQuery: String?, progress: Int, total: Int, navigateUp: () -> Unit, onChangeSearchQuery: (String?) -> Unit, onSearch: (String) -> Unit, sourceFilter: SourceFilter, onChangeSearchFilter: (SourceFilter) -> Unit, onlyShowHasResults: Boolean, onToggleResults: () -> Unit, scrollBehavior: TopAppBarScrollBehavior, ) - LongMethod:HomeScreen.kt$HomeScreen$@Composable override fun Content() + LongMethod:FavoritesSyncHelper.kt$FavoritesSyncHelper$private suspend fun applyChangeSetToLocal(errorList: MutableList<String>, changeSet: ChangeSet) + LongMethod:FavoritesSyncHelper.kt$FavoritesSyncHelper$private suspend fun beginSync() + LongMethod:Female.kt$Female$override fun getTags1(): List<String> + LongMethod:FilterHandler.kt$FilterHandler$fun getQueryMap(filters: FilterList): Map<String, Any> + LongMethod:FilterHandler.kt$FilterHandler$internal fun getTags() + LongMethod:GalleryAdder.kt$GalleryAdder$suspend fun addGallery( context: Context, url: String, fav: Boolean = false, forceSource: UrlImportableSource? = null, throttleFunc: suspend () -> Unit = {}, retry: Int = 1, ): GalleryAddEvent + LongMethod:Group.kt$Group$override fun getTags1(): List<String> + LongMethod:Group.kt$Group$override fun getTags2(): List<String> + LongMethod:Group.kt$Group$override fun getTags3(): List<String> + LongMethod:Group2.kt$Group2$override fun getTags1(): List<String> LongMethod:ImageUtil.kt$ImageUtil$fun chooseBackground(context: Context, imageStream: InputStream): Drawable - LongMethod:InfoScreen.kt$@Composable fun InfoScreen( icon: ImageVector, headingText: String, subtitleText: String, acceptText: String, onAcceptClick: () -> Unit, canAccept: Boolean = true, rejectText: String? = null, onRejectClick: (() -> Unit)? = null, content: @Composable ColumnScope.() -> Unit, ) - LongMethod:LibraryContent.kt$@Composable fun LibraryContent( categories: List<Category>, searchQuery: String?, selection: List<LibraryManga>, contentPadding: PaddingValues, currentPage: () -> Int, hasActiveFilters: Boolean, showPageTabs: Boolean, onChangeCurrentPage: (Int) -> Unit, onMangaClicked: (Long) -> Unit, onContinueReadingClicked: ((LibraryManga) -> Unit)?, onToggleSelection: (LibraryManga) -> Unit, onToggleRangeSelection: (LibraryManga) -> Unit, onRefresh: (Category?) -> Boolean, onGlobalSearchClicked: () -> Unit, getNumberOfMangaForCategory: (Category) -> Int?, getDisplayMode: (Int) -> PreferenceMutableState<LibraryDisplayMode>, getColumnsForOrientation: (Boolean) -> PreferenceMutableState<Int>, getLibraryForPage: (Int) -> List<LibraryItem>, ) - LongMethod:LibraryPager.kt$@Composable fun LibraryPager( state: PagerState, contentPadding: PaddingValues, hasActiveFilters: Boolean, selectedManga: List<LibraryManga>, searchQuery: String?, onGlobalSearchClicked: () -> Unit, getDisplayMode: (Int) -> PreferenceMutableState<LibraryDisplayMode>, getColumnsForOrientation: (Boolean) -> PreferenceMutableState<Int>, getLibraryForPage: (Int) -> List<LibraryItem>, onClickManga: (LibraryManga) -> Unit, onLongClickManga: (LibraryManga) -> Unit, onClickContinueReading: ((LibraryManga) -> Unit)?, ) - LongMethod:LibraryScreenModel.kt$LibraryScreenModel$private fun LibraryMap.applySort( // Map<MangaId, List<Track>> trackMap: Map<Long, List<Track>>, ): LibraryMap + LongMethod:LibraryQuery.kt$LibraryQuery$override fun <R> execute(mapper: (SqlCursor) -> QueryResult<R>): QueryResult<R> + LongMethod:LibraryScreenModel.kt$LibraryScreenModel$private fun LibraryMap.applySort( // Map<MangaId, List<Track>> trackMap: Map<Long, List<Track>>, /* SY --> */ groupSort: LibrarySort? = null, /* SY <-- */ ): LibraryMap + LongMethod:LibraryScreenModel.kt$LibraryScreenModel$private fun filterManga( queries: List<QueryComponent>, libraryManga: LibraryManga, tracks: List<Track>?, source: Source?, checkGenre: Boolean = true, searchTags: List<SearchTag>? = null, searchTitles: List<SearchTitle>? = null, loggedInTrackServices: Map<Long, TriState>, ): Boolean + LongMethod:LibraryScreenModel.kt$LibraryScreenModel$private fun getGroupedMangaItems( groupType: Int, libraryManga: List<LibraryItem>, ): LibraryMap LongMethod:LibraryScreenModel.kt$LibraryScreenModel$private suspend fun LibraryMap.applyFilters( trackMap: Map<Long, List<Track>>, loggedInTrackers: Map<Long, TriState>, ): LibraryMap - LongMethod:LibrarySettingsDialog.kt$@Composable private fun ColumnScope.DisplayPage( screenModel: LibrarySettingsScreenModel, ) - LongMethod:LibrarySettingsDialog.kt$@Composable private fun ColumnScope.FilterPage( screenModel: LibrarySettingsScreenModel, ) - LongMethod:LibraryTab.kt$LibraryTab$@Composable override fun Content() - LongMethod:LibraryUpdateJob.kt$LibraryUpdateJob$private suspend fun addMangaToQueue(categoryId: Long) + LongMethod:LibraryUpdateJob.kt$LibraryUpdateJob$private suspend fun addMangaToQueue(categoryId: Long, group: Int, groupExtra: String?) LongMethod:LibraryUpdateJob.kt$LibraryUpdateJob$private suspend fun updateChapterList() + LongMethod:LocalSource.kt$LocalSource$override suspend fun getMangaDetails(manga: SManga): SManga + LongMethod:LocalSource.kt$LocalSource$override suspend fun getSearchManga(page: Int, query: String, filters: FilterList): MangasPage LongMethod:MainActivity.kt$MainActivity$override fun onCreate(savedInstanceState: Bundle?) LongMethod:MainActivity.kt$MainActivity$private fun handleIntentAction(intent: Intent, navigator: Navigator): Boolean - LongMethod:MangaBottomActionMenu.kt$@Composable fun LibraryBottomActionMenu( visible: Boolean, onChangeCategoryClicked: () -> Unit, onMarkAsReadClicked: () -> Unit, onMarkAsUnreadClicked: () -> Unit, onDownloadClicked: ((DownloadAction) -> Unit)?, onDeleteClicked: () -> Unit, modifier: Modifier = Modifier, ) - LongMethod:MangaBottomActionMenu.kt$@Composable fun MangaBottomActionMenu( visible: Boolean, modifier: Modifier = Modifier, onBookmarkClicked: (() -> Unit)? = null, onRemoveBookmarkClicked: (() -> Unit)? = null, onMarkAsReadClicked: (() -> Unit)? = null, onMarkAsUnreadClicked: (() -> Unit)? = null, onMarkPreviousAsReadClicked: (() -> Unit)? = null, onDownloadClicked: (() -> Unit)? = null, onDeleteClicked: (() -> Unit)? = null, ) - LongMethod:MangaChapterListItem.kt$@Composable fun MangaChapterListItem( title: String, date: String?, readProgress: String?, scanlator: String?, read: Boolean, bookmark: Boolean, selected: Boolean, downloadIndicatorEnabled: Boolean, downloadStateProvider: () -> Download.State, downloadProgressProvider: () -> Int, chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction, chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction, onLongClick: () -> Unit, onClick: () -> Unit, onDownloadClick: ((ChapterDownloadAction) -> Unit)?, onChapterSwipe: (LibraryPreferences.ChapterSwipeAction) -> Unit, modifier: Modifier = Modifier, ) - LongMethod:MangaCoverDialog.kt$@Composable fun MangaCoverDialog( coverDataProvider: () -> Manga, isCustomCover: Boolean, snackbarHostState: SnackbarHostState, onShareClick: () -> Unit, onSaveClick: () -> Unit, onEditClick: ((EditCoverAction) -> Unit)?, onDismissRequest: () -> Unit, ) - LongMethod:MangaDialogs.kt$@Composable fun SetIntervalDialog( interval: Int, nextUpdate: Instant?, onDismissRequest: () -> Unit, onValueChanged: ((Int) -> Unit)? = null, ) - LongMethod:MangaInfoHeader.kt$@Composable fun ExpandableMangaDescription( defaultExpandState: Boolean, description: String?, tagsProvider: () -> List<String>?, onTagSearch: (String) -> Unit, onCopyTagToClipboard: (tag: String) -> Unit, modifier: Modifier = Modifier, ) - LongMethod:MangaInfoHeader.kt$@Composable private fun ColumnScope.MangaContentInfo( title: String, doSearch: (query: String, global: Boolean) -> Unit, author: String?, artist: String?, status: Long, sourceName: String, isStubSource: Boolean, textAlign: TextAlign? = LocalTextStyle.current.textAlign, ) - LongMethod:MangaInfoHeader.kt$@Composable private fun MangaSummary( expandedDescription: String, shrunkDescription: String, expanded: Boolean, modifier: Modifier = Modifier, ) - LongMethod:MangaScreen.kt$@Composable fun MangaScreen( state: MangaScreenModel.State.Success, snackbarHostState: SnackbarHostState, nextUpdate: Instant?, isTabletUi: Boolean, chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction, chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction, onBackClicked: () -> Unit, onChapterClicked: (Chapter) -> Unit, onDownloadChapter: ((List<ChapterList.Item>, ChapterDownloadAction) -> Unit)?, onAddToLibraryClicked: () -> Unit, onWebViewClicked: (() -> Unit)?, onWebViewLongClicked: (() -> Unit)?, onTrackingClicked: () -> Unit, // For tags menu onTagSearch: (String) -> Unit, onFilterButtonClicked: () -> Unit, onRefresh: () -> Unit, onContinueReading: () -> Unit, onSearch: (query: String, global: Boolean) -> Unit, // For cover dialog onCoverClicked: () -> Unit, // For top action menu onShareClicked: (() -> Unit)?, onDownloadActionClicked: ((DownloadAction) -> Unit)?, onEditCategoryClicked: (() -> Unit)?, onEditFetchIntervalClicked: (() -> Unit)?, onMigrateClicked: (() -> Unit)?, // For bottom action menu onMultiBookmarkClicked: (List<Chapter>, bookmarked: Boolean) -> Unit, onMultiMarkAsReadClicked: (List<Chapter>, markAsRead: Boolean) -> Unit, onMarkPreviousAsReadClicked: (Chapter) -> Unit, onMultiDeleteClicked: (List<Chapter>) -> Unit, // For chapter swipe onChapterSwipe: (ChapterList.Item, LibraryPreferences.ChapterSwipeAction) -> Unit, // Chapter selection onChapterSelected: (ChapterList.Item, Boolean, Boolean, Boolean) -> Unit, onAllChapterSelected: (Boolean) -> Unit, onInvertSelection: () -> Unit, ) - LongMethod:MangaScreen.kt$@Composable fun MangaScreenLargeImpl( state: MangaScreenModel.State.Success, snackbarHostState: SnackbarHostState, nextUpdate: Instant?, chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction, chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction, onBackClicked: () -> Unit, onChapterClicked: (Chapter) -> Unit, onDownloadChapter: ((List<ChapterList.Item>, ChapterDownloadAction) -> Unit)?, onAddToLibraryClicked: () -> Unit, onWebViewClicked: (() -> Unit)?, onWebViewLongClicked: (() -> Unit)?, onTrackingClicked: () -> Unit, // For tags menu onTagSearch: (String) -> Unit, onCopyTagToClipboard: (tag: String) -> Unit, onFilterButtonClicked: () -> Unit, onRefresh: () -> Unit, onContinueReading: () -> Unit, onSearch: (query: String, global: Boolean) -> Unit, // For cover dialog onCoverClicked: () -> Unit, // For top action menu onShareClicked: (() -> Unit)?, onDownloadActionClicked: ((DownloadAction) -> Unit)?, onEditCategoryClicked: (() -> Unit)?, onEditIntervalClicked: (() -> Unit)?, onMigrateClicked: (() -> Unit)?, // For bottom action menu onMultiBookmarkClicked: (List<Chapter>, bookmarked: Boolean) -> Unit, onMultiMarkAsReadClicked: (List<Chapter>, markAsRead: Boolean) -> Unit, onMarkPreviousAsReadClicked: (Chapter) -> Unit, onMultiDeleteClicked: (List<Chapter>) -> Unit, // For swipe actions onChapterSwipe: (ChapterList.Item, LibraryPreferences.ChapterSwipeAction) -> Unit, // Chapter selection onChapterSelected: (ChapterList.Item, Boolean, Boolean, Boolean) -> Unit, onAllChapterSelected: (Boolean) -> Unit, onInvertSelection: () -> Unit, ) - LongMethod:MangaScreen.kt$@Composable private fun MangaScreenSmallImpl( state: MangaScreenModel.State.Success, snackbarHostState: SnackbarHostState, nextUpdate: Instant?, chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction, chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction, onBackClicked: () -> Unit, onChapterClicked: (Chapter) -> Unit, onDownloadChapter: ((List<ChapterList.Item>, ChapterDownloadAction) -> Unit)?, onAddToLibraryClicked: () -> Unit, onWebViewClicked: (() -> Unit)?, onWebViewLongClicked: (() -> Unit)?, onTrackingClicked: () -> Unit, // For tags menu onTagSearch: (String) -> Unit, onCopyTagToClipboard: (tag: String) -> Unit, onFilterClicked: () -> Unit, onRefresh: () -> Unit, onContinueReading: () -> Unit, onSearch: (query: String, global: Boolean) -> Unit, // For cover dialog onCoverClicked: () -> Unit, // For top action menu onShareClicked: (() -> Unit)?, onDownloadActionClicked: ((DownloadAction) -> Unit)?, onEditCategoryClicked: (() -> Unit)?, onEditIntervalClicked: (() -> Unit)?, onMigrateClicked: (() -> Unit)?, // For bottom action menu onMultiBookmarkClicked: (List<Chapter>, bookmarked: Boolean) -> Unit, onMultiMarkAsReadClicked: (List<Chapter>, markAsRead: Boolean) -> Unit, onMarkPreviousAsReadClicked: (Chapter) -> Unit, onMultiDeleteClicked: (List<Chapter>) -> Unit, // For chapter swipe onChapterSwipe: (ChapterList.Item, LibraryPreferences.ChapterSwipeAction) -> Unit, // Chapter selection onChapterSelected: (ChapterList.Item, Boolean, Boolean, Boolean) -> Unit, onAllChapterSelected: (Boolean) -> Unit, onInvertSelection: () -> Unit, ) - LongMethod:MangaScreen.kt$MangaScreen$@Composable override fun Content() - LongMethod:MangaScreen.kt$private fun LazyListScope.sharedChapterItems( manga: Manga, chapters: List<ChapterList>, isAnyChapterSelected: Boolean, chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction, chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction, onChapterClicked: (Chapter) -> Unit, onDownloadChapter: ((List<ChapterList.Item>, ChapterDownloadAction) -> Unit)?, onChapterSelected: (ChapterList.Item, Boolean, Boolean, Boolean) -> Unit, onChapterSwipe: (ChapterList.Item, LibraryPreferences.ChapterSwipeAction) -> Unit, ) - LongMethod:MangaToolbar.kt$@Composable fun MangaToolbar( title: String, titleAlphaProvider: () -> Float, hasFilters: Boolean, onBackClicked: () -> Unit, onClickFilter: () -> Unit, onClickShare: (() -> Unit)?, onClickDownload: ((DownloadAction) -> Unit)?, onClickEditCategory: (() -> Unit)?, onClickRefresh: () -> Unit, onClickMigrate: (() -> Unit)?, // For action mode actionModeCounter: Int, onSelectAll: () -> Unit, onInvertSelection: () -> Unit, modifier: Modifier = Modifier, backgroundAlphaProvider: () -> Float = titleAlphaProvider, ) - LongMethod:MigrateDialog.kt$@Composable internal fun MigrateDialog( oldManga: Manga, newManga: Manga, screenModel: MigrateDialogScreenModel, onDismissRequest: () -> Unit, onClickTitle: () -> Unit, onPopScreen: () -> Unit, ) - LongMethod:MigrateDialog.kt$MigrateDialogScreenModel$private suspend fun migrateMangaInternal( oldSource: Source?, newSource: Source, oldManga: Manga, newManga: Manga, sourceChapters: List<SChapter>, replace: Boolean, flags: Int, ) - LongMethod:MoreScreen.kt$@Composable fun MoreScreen( downloadQueueStateProvider: () -> DownloadQueueState, downloadedOnly: Boolean, onDownloadedOnlyChange: (Boolean) -> Unit, incognitoMode: Boolean, onIncognitoModeChange: (Boolean) -> Unit, isFDroid: Boolean, onClickDownloadQueue: () -> Unit, onClickCategories: () -> Unit, onClickStats: () -> Unit, onClickDataAndStorage: () -> Unit, onClickSettings: () -> Unit, onClickAbout: () -> Unit, ) + LongMethod:Male.kt$Male$override fun getTags1(): List<String> + LongMethod:MangaScreen.kt$private fun LazyListScope.sharedChapterItems( manga: Manga, chapters: List<ChapterList>, isAnyChapterSelected: Boolean, chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction, chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction, // SY --> alwaysShowReadingProgress: Boolean, // SY <-- onChapterClicked: (Chapter) -> Unit, onDownloadChapter: ((List<ChapterList.Item>, ChapterDownloadAction) -> Unit)?, onChapterSelected: (ChapterList.Item, Boolean, Boolean, Boolean) -> Unit, onChapterSwipe: (ChapterList.Item, LibraryPreferences.ChapterSwipeAction) -> Unit, ) + LongMethod:MangaScreenModel.kt$MangaScreenModel$suspend fun smartSearchMerge(manga: Manga, originalMangaId: Long): Manga + LongMethod:MigrationListScreenModel.kt$MigrationListScreenModel$private suspend fun migrateMangaInternal( prevManga: Manga, manga: Manga, replace: Boolean, ) + LongMethod:MigrationListScreenModel.kt$MigrationListScreenModel$private suspend fun runMigrations(mangas: List<MigratingManga>) + LongMethod:Migrations.kt$Migrations$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 LongMethod:Notifications.kt$Notifications$fun createChannels(context: Context) - LongMethod:PreferenceItem.kt$@Composable internal fun PreferenceItem( item: Preference.PreferenceItem<*>, highlightKey: String?, ) + LongMethod:Other.kt$Other$override fun getTags1(): List<String> + LongMethod:PagerPageHolder.kt$PagerPageHolder$private fun mergePages(imageStream: InputStream, imageStream2: InputStream?): InputStream + LongMethod:PagerPageHolder.kt$PagerPageHolder$private suspend fun setImage() + LongMethod:PagerViewerAdapter.kt$PagerViewerAdapter$private fun setJoinedItems(useSecondPage: Boolean = false) + LongMethod:Parody.kt$Parody$override fun getTags1(): List<String> + LongMethod:Parody.kt$Parody$override fun getTags2(): List<String> LongMethod:ReaderActivity.kt$ReaderActivity$override fun onCreate(savedInstanceState: Bundle?) LongMethod:ReaderActivity.kt$ReaderActivity$private fun initializeMenu() - LongMethod:ReaderAppBars.kt$@Composable fun ReaderAppBars( visible: Boolean, fullscreen: Boolean, mangaTitle: String?, chapterTitle: String?, navigateUp: () -> Unit, onClickTopAppBar: () -> Unit, bookmarked: Boolean, onToggleBookmarked: () -> Unit, onOpenInWebView: (() -> Unit)?, onShare: (() -> Unit)?, viewer: Viewer?, onNextChapter: () -> Unit, enabledNext: Boolean, onPreviousChapter: () -> Unit, enabledPrevious: Boolean, currentPage: Int, totalPages: Int, onSliderValueChange: (Int) -> Unit, readingMode: ReadingMode, onClickReadingMode: () -> Unit, orientation: ReaderOrientation, onClickOrientation: () -> Unit, cropEnabled: Boolean, onClickCropBorder: () -> Unit, onClickSettings: () -> Unit, ) - LongMethod:ReadingModePage.kt$@Composable private fun ColumnScope.PagerViewerSettings(screenModel: ReaderSettingsScreenModel) - LongMethod:Scaffold.kt$@Composable private fun ScaffoldLayout( fabPosition: FabPosition, topBar: @Composable () -> Unit, startBar: @Composable () -> Unit, content: @Composable (PaddingValues) -> Unit, snackbar: @Composable () -> Unit, fab: @Composable () -> Unit, contentWindowInsets: WindowInsets, bottomBar: @Composable () -> Unit, ) - LongMethod:ScanlatorFilterDialog.kt$@Composable fun ScanlatorFilterDialog( availableScanlators: Set<String>, excludedScanlators: Set<String>, onDismissRequest: () -> Unit, onConfirm: (Set<String>) -> Unit, ) - LongMethod:SettingsAdvancedScreen.kt$SettingsAdvancedScreen$@Composable private fun getExtensionsGroup( basePreferences: BasePreferences, ): Preference.PreferenceGroup - LongMethod:SettingsAdvancedScreen.kt$SettingsAdvancedScreen$@Composable private fun getNetworkGroup( networkPreferences: NetworkPreferences, ): Preference.PreferenceGroup - LongMethod:SettingsDataScreen.kt$SettingsDataScreen$@Composable private fun getBackupAndRestoreGroup(backupPreferences: BackupPreferences): Preference.PreferenceGroup - LongMethod:SettingsLibraryScreen.kt$SettingsLibraryScreen$@Composable private fun getGlobalUpdateGroup( allCategories: List<Category>, libraryPreferences: LibraryPreferences, ): Preference.PreferenceGroup - LongMethod:SettingsMainScreen.kt$SettingsMainScreen$@Composable fun Content(twoPane: Boolean) - LongMethod:SettingsReaderScreen.kt$SettingsReaderScreen$@Composable private fun getPagedGroup(readerPreferences: ReaderPreferences): Preference.PreferenceGroup - LongMethod:SettingsReaderScreen.kt$SettingsReaderScreen$@Composable private fun getWebtoonGroup(readerPreferences: ReaderPreferences): Preference.PreferenceGroup - LongMethod:SettingsSearchScreen.kt$@Composable private fun SearchResult( searchKey: String, modifier: Modifier = Modifier, listState: LazyListState = rememberLazyListState(), contentPadding: PaddingValues = PaddingValues(), onItemClick: (SearchResultItem) -> Unit, ) - LongMethod:SettingsSearchScreen.kt$SettingsSearchScreen$@Composable override fun Content() - LongMethod:SettingsTrackingScreen.kt$SettingsTrackingScreen$@Composable override fun getPreferences(): List<Preference> - LongMethod:SettingsTrackingScreen.kt$SettingsTrackingScreen$@Composable private fun TrackingLoginDialog( tracker: Tracker, uNameStringRes: StringResource, onDismissRequest: () -> Unit, ) - LongMethod:SourceFilterDialog.kt$@Composable private fun FilterItem(filter: Filter<*>, onUpdate: () -> Unit) - LongMethod:SourceSearchScreen.kt$SourceSearchScreen$@Composable override fun Content() + LongMethod:ReaderViewModel.kt$ReaderViewModel$suspend fun init(mangaId: Long, initialChapterId: Long /* SY --> */, page: Int?/* SY <-- */): Result<Boolean> + LongMethod:SYDomainModule.kt$SYDomainModule$override fun InjektRegistrar.registerInjectables() LongMethod:SyncChaptersWithSource.kt$SyncChaptersWithSource$suspend fun await( rawSourceChapters: List<SChapter>, manga: Manga, source: Source, manualFetch: Boolean = false, fetchWindow: Pair<Long, Long> = Pair(0, 0), ): List<Chapter> - LongMethod:TrackInfoDialog.kt$TrackInfoDialogHomeScreen$@Composable override fun Content() - LongMethod:TrackInfoDialog.kt$TrackerRemoveScreen$@Composable override fun Content() - LongMethod:TrackInfoDialogHome.kt$@Composable private fun TrackInfoItem( title: String, tracker: Tracker, status: StringResource?, onStatusClick: () -> Unit, chapters: String, onChaptersClick: () -> Unit, score: String?, onScoreClick: (() -> Unit)?, startDate: String?, onStartDateClick: (() -> Unit)?, endDate: String?, onEndDateClick: (() -> Unit)?, onNewSearch: () -> Unit, onOpenInBrowser: () -> Unit, onRemoved: () -> Unit, ) - LongMethod:TrackerSearch.kt$@Composable fun TrackerSearch( query: TextFieldValue, onQueryChange: (TextFieldValue) -> Unit, onDispatchQuery: () -> Unit, queryResult: Result<List<TrackSearch>>?, selected: TrackSearch?, onSelectedChange: (TrackSearch) -> Unit, onConfirmSelection: () -> Unit, onDismissRequest: () -> Unit, ) - LongMethod:TrackerSearch.kt$@Composable private fun SearchResultItem( trackSearch: TrackSearch, selected: Boolean, onClick: () -> Unit, ) - LongMethod:TriStateListDialog.kt$@Composable fun <T> TriStateListDialog( title: String, message: String? = null, items: List<T>, initialChecked: List<T>, initialInversed: List<T>, itemLabel: @Composable (T) -> String, onDismissRequest: () -> Unit, onValueChanged: (newIncluded: List<T>, newExcluded: List<T>) -> Unit, ) - LongMethod:UpdatesScreen.kt$@Composable fun UpdateScreen( state: UpdatesScreenModel.State, snackbarHostState: SnackbarHostState, lastUpdated: Long, onClickCover: (UpdatesItem) -> Unit, onSelectAll: (Boolean) -> Unit, onInvertSelection: () -> Unit, onUpdateLibrary: () -> Boolean, onDownloadChapter: (List<UpdatesItem>, ChapterDownloadAction) -> Unit, onMultiBookmarkClicked: (List<UpdatesItem>, bookmark: Boolean) -> Unit, onMultiMarkAsReadClicked: (List<UpdatesItem>, read: Boolean) -> Unit, onMultiDeleteClicked: (List<UpdatesItem>) -> Unit, onUpdateSelected: (UpdatesItem, Boolean, Boolean, Boolean) -> Unit, onOpenChapter: (UpdatesItem) -> Unit, ) - LongMethod:UpdatesTab.kt$UpdatesTab$@Composable override fun Content() - LongMethod:UpdatesUiItem.kt$@Composable private fun UpdatesUiItem( update: UpdatesWithRelations, selected: Boolean, readProgress: String?, onClick: () -> Unit, onLongClick: () -> Unit, onClickCover: (() -> Unit)?, onDownloadChapter: ((ChapterDownloadAction) -> Unit)?, // Download Indicator downloadStateProvider: () -> Download.State, downloadProgressProvider: () -> Int, modifier: Modifier = Modifier, ) - LongMethod:UpdatesWidget.kt$@Composable fun UpdatesWidget( data: ImmutableList<Pair<Long, Bitmap?>>?, contentColor: ColorProvider, topPadding: Dp, bottomPadding: Dp, modifier: GlanceModifier = GlanceModifier, ) - LongMethod:VerticalFastScroller.kt$@Composable fun VerticalFastScroller( listState: LazyListState, modifier: Modifier = Modifier, thumbAllowed: () -> Boolean = { true }, thumbColor: Color = MaterialTheme.colorScheme.primary, topContentPadding: Dp = Dp.Hairline, bottomContentPadding: Dp = Dp.Hairline, endContentPadding: Dp = Dp.Hairline, content: @Composable () -> Unit, ) - LongMethod:VerticalFastScroller.kt$@Composable fun VerticalGridFastScroller( state: LazyGridState, columns: GridCells, arrangement: Arrangement.Horizontal, contentPadding: PaddingValues, modifier: Modifier = Modifier, thumbAllowed: () -> Boolean = { true }, thumbColor: Color = MaterialTheme.colorScheme.primary, topContentPadding: Dp = Dp.Hairline, bottomContentPadding: Dp = Dp.Hairline, endContentPadding: Dp = Dp.Hairline, content: @Composable () -> Unit, ) - LongMethod:WebViewScreenContent.kt$@Composable fun WebViewScreenContent( onNavigateUp: () -> Unit, initialTitle: String?, url: String, onShare: (String) -> Unit, onOpenInBrowser: (String) -> Unit, onClearCookies: (String) -> Unit, headers: Map<String, String> = emptyMap(), onUrlChange: (String) -> Unit = {}, ) + LongMethod:Tester.kt$Tester$@Test fun localFavoritesStorageTester(): Unit + LongMethod:TrackStatus.kt$TrackStatus.Companion$fun parseTrackerStatus(trackerManager: TrackerManager, tracker: Long, status: Long): TrackStatus? + LongMethod:Tsumino.kt$Tsumino$override suspend fun parseIntoMetadata(metadata: TsuminoSearchMetadata, input: Document) + LongMethod:UpdatesQuery.kt$UpdatesQuery$override fun <R> execute(mapper: (SqlCursor) -> QueryResult<R>): QueryResult<R> + LongMethod:UpdatesUiItem.kt$internal fun LazyListScope.updatesUiItems( uiModels: List<UpdatesUiModel>, selectionMode: Boolean, // SY --> preserveReadingPosition: Boolean, // SY <-- onUpdateSelected: (UpdatesItem, Boolean, Boolean, Boolean) -> Unit, onClickCover: (UpdatesItem) -> Unit, onClickUpdate: (UpdatesItem) -> Unit, onDownloadChapter: (List<UpdatesItem>, ChapterDownloadAction) -> Unit, ) LongMethod:WebtoonRecyclerView.kt$WebtoonRecyclerView.Detector$override fun onTouchEvent(ev: MotionEvent): Boolean - LongMethod:WheelPicker.kt$@Composable private fun <T> WheelPicker( items: ImmutableList<T>, modifier: Modifier = Modifier, startIndex: Int = 0, size: DpSize = DpSize(128.dp, 128.dp), onSelectionChanged: (index: Int) -> Unit = {}, manualInputType: KeyboardType? = null, backgroundContent: (@Composable (size: DpSize) -> Unit)? = { WheelPickerDefaults.Background(size = it) }, itemContent: @Composable LazyItemScope.(item: T) -> Unit, ) - LongParameterList:BottomReaderBar.kt$( backgroundColor: Color, readingMode: ReadingMode, onClickReadingMode: () -> Unit, orientation: ReaderOrientation, onClickOrientation: () -> Unit, cropEnabled: Boolean, onClickCropBorder: () -> Unit, onClickSettings: () -> Unit, ) - LongParameterList:BrowseSourceScreen.kt$( source: Source?, mangaList: LazyPagingItems<StateFlow<Manga>>, columns: GridCells, displayMode: LibraryDisplayMode, snackbarHostState: SnackbarHostState, contentPadding: PaddingValues, onWebViewClick: () -> Unit, onHelpClick: () -> Unit, onLocalSourceHelpClick: () -> Unit, onMangaClick: (Manga) -> Unit, onMangaLongClick: (Manga) -> Unit, ) - LongParameterList:BrowseSourceToolbar.kt$( searchQuery: String?, onSearchQueryChange: (String?) -> Unit, source: Source?, displayMode: LibraryDisplayMode, onDisplayModeChange: (LibraryDisplayMode) -> Unit, navigateUp: () -> Unit, onWebViewClick: () -> Unit, onHelpClick: () -> Unit, onSettingsClick: () -> Unit, onSearch: (String) -> Unit, scrollBehavior: TopAppBarScrollBehavior? = null, ) - LongParameterList:CategoryListItem.kt$( category: Category, canMoveUp: Boolean, canMoveDown: Boolean, onMoveUp: (Category) -> Unit, onMoveDown: (Category) -> Unit, onRename: () -> Unit, onDelete: () -> Unit, modifier: Modifier = Modifier, ) - LongParameterList:CategoryScreen.kt$( categories: List<Category>, lazyListState: LazyListState, paddingValues: PaddingValues, onClickRename: (Category) -> Unit, onClickDelete: (Category) -> Unit, onMoveUp: (Category) -> Unit, onMoveDown: (Category) -> Unit, ) - LongParameterList:CategoryScreen.kt$( state: CategoryScreenState.Success, onClickCreate: () -> Unit, onClickSortAlphabetically: () -> Unit, onClickRename: (Category) -> Unit, onClickDelete: (Category) -> Unit, onClickMoveUp: (Category) -> Unit, onClickMoveDown: (Category) -> Unit, navigateUp: () -> Unit, ) - LongParameterList:ChapterNavigator.kt$( isRtl: Boolean, onNextChapter: () -> Unit, enabledNext: Boolean, onPreviousChapter: () -> Unit, enabledPrevious: Boolean, currentPage: Int, totalPages: Int, onSliderValueChange: (Int) -> Unit, ) - LongParameterList:ChapterRepositoryImpl.kt$ChapterRepositoryImpl$( id: Long, mangaId: Long, url: String, name: String, scanlator: String?, read: Boolean, bookmark: Boolean, lastPageRead: Long, chapterNumber: Double, sourceOrder: Long, dateFetch: Long, dateUpload: Long, lastModifiedAt: Long, ) - LongParameterList:ChapterSettingsDialog.kt$( downloadFilter: TriState, onDownloadFilterChanged: ((TriState) -> Unit)?, unreadFilter: TriState, onUnreadFilterChanged: (TriState) -> Unit, bookmarkedFilter: TriState, onBookmarkedFilterChanged: (TriState) -> Unit, scanlatorFilterActive: Boolean, onScanlatorFilterClicked: (() -> Unit), ) - LongParameterList:ChapterSettingsDialog.kt$( onDismissRequest: () -> Unit, manga: Manga? = null, onDownloadFilterChanged: (TriState) -> Unit, onUnreadFilterChanged: (TriState) -> Unit, onBookmarkedFilterChanged: (TriState) -> Unit, scanlatorFilterActive: Boolean, onScanlatorFilterClicked: (() -> Unit), onSortModeChanged: (Long) -> Unit, onDisplayModeChanged: (Long) -> Unit, onSetAsDefault: (applyToExistingManga: Boolean) -> Unit, onResetToDefault: () -> Unit, ) - LongParameterList:ChapterTransition.kt$( topLabel: String, topChapter: Chapter?, topChapterDownloaded: Boolean, bottomLabel: String, bottomChapter: Chapter?, bottomChapterDownloaded: Boolean, fallbackLabel: String, chapterGap: Int, ) - LongParameterList:ExtensionDetailsScreen.kt$( contentPadding: PaddingValues, extension: Extension.Installed, sources: ImmutableList<ExtensionSourceItem>, onClickSourcePreferences: (sourceId: Long) -> Unit, onClickUninstall: () -> Unit, onClickSource: (sourceId: Long) -> Unit, ) - LongParameterList:ExtensionDetailsScreen.kt$( navigateUp: () -> Unit, state: ExtensionDetailsScreenModel.State, onClickSourcePreferences: (sourceId: Long) -> Unit, onClickEnableAll: () -> Unit, onClickDisableAll: () -> Unit, onClickClearCookies: () -> Unit, onClickUninstall: () -> Unit, onClickSource: (sourceId: Long) -> Unit, ) - LongParameterList:ExtensionsScreen.kt$( item: ExtensionUiModel.Item, onClickItem: (Extension) -> Unit, onLongClickItem: (Extension) -> Unit, onClickItemCancel: (Extension) -> Unit, onClickItemAction: (Extension) -> Unit, onClickItemSecondaryAction: (Extension) -> Unit, modifier: Modifier = Modifier, ) - LongParameterList:ExtensionsScreen.kt$( state: ExtensionsScreenModel.State, contentPadding: PaddingValues, onLongClickItem: (Extension) -> Unit, onClickItemCancel: (Extension) -> Unit, onOpenWebView: (Extension.Available) -> Unit, onInstallExtension: (Extension.Available) -> Unit, onUninstallExtension: (Extension) -> Unit, onUpdateExtension: (Extension.Installed) -> Unit, onTrustExtension: (Extension.Untrusted) -> Unit, onOpenExtension: (Extension.Installed) -> Unit, onClickUpdateAll: () -> Unit, ) - LongParameterList:ExtensionsScreen.kt$( state: ExtensionsScreenModel.State, contentPadding: PaddingValues, searchQuery: String?, onLongClickItem: (Extension) -> Unit, onClickItemCancel: (Extension) -> Unit, onOpenWebView: (Extension.Available) -> Unit, onInstallExtension: (Extension.Available) -> Unit, onUninstallExtension: (Extension) -> Unit, onUpdateExtension: (Extension.Installed) -> Unit, onTrustExtension: (Extension.Untrusted) -> Unit, onOpenExtension: (Extension.Installed) -> Unit, onClickUpdateAll: () -> Unit, onRefresh: () -> Unit, ) - LongParameterList:GlobalSearchScreen.kt$( items: Map<CatalogueSource, SearchItemResult>, contentPadding: PaddingValues, getManga: @Composable (Manga) -> State<Manga>, onClickSource: (CatalogueSource) -> Unit, onClickItem: (Manga) -> Unit, onLongClickItem: (Manga) -> Unit, fromSourceId: Long? = null, ) - LongParameterList:GlobalSearchScreen.kt$( state: SearchScreenModel.State, navigateUp: () -> Unit, onChangeSearchQuery: (String?) -> Unit, onSearch: (String) -> Unit, onChangeSearchFilter: (SourceFilter) -> Unit, onToggleResults: () -> Unit, getManga: @Composable (Manga) -> State<Manga>, onClickSource: (CatalogueSource) -> Unit, onClickItem: (Manga) -> Unit, onLongClickItem: (Manga) -> Unit, ) - LongParameterList:GlobalSearchToolbar.kt$( searchQuery: String?, progress: Int, total: Int, navigateUp: () -> Unit, onChangeSearchQuery: (String?) -> Unit, onSearch: (String) -> Unit, sourceFilter: SourceFilter, onChangeSearchFilter: (SourceFilter) -> Unit, onlyShowHasResults: Boolean, onToggleResults: () -> Unit, scrollBehavior: TopAppBarScrollBehavior, ) + LongParameterList:ChapterLoader.kt$ChapterLoader$( private val context: Context, private val downloadManager: DownloadManager, private val downloadProvider: DownloadProvider, private val manga: Manga, private val source: Source, // SY --> private val sourceManager: SourceManager, private val readerPrefs: ReaderPreferences, private val mergedReferences: List<MergedMangaReference>, private val mergedManga: Map<Long, Manga>, // SY <-- ) + LongParameterList:ChapterMapper.kt$ChapterMapper$( id: Long, mangaId: Long, url: String, name: String, scanlator: String?, read: Boolean, bookmark: Boolean, lastPageRead: Long, chapterNumber: Double, sourceOrder: Long, dateFetch: Long, dateUpload: Long, lastModifiedAt: Long, version: Long, @Suppress("UNUSED_PARAMETER") isSyncing: Long, ) + LongParameterList:Chip.kt$ChipColors$( private val containerColor: Color, private val labelColor: Color, private val leadingIconContentColor: Color, private val trailingIconContentColor: Color, private val disabledContainerColor: Color, private val disabledLabelColor: Color, private val disabledLeadingIconContentColor: Color, private val disabledTrailingIconContentColor: Color, ) + LongParameterList:EXHMigrations.kt$EXHMigrations$( context: Context, preferenceStore: PreferenceStore, basePreferences: BasePreferences, uiPreferences: UiPreferences, networkPreferences: NetworkPreferences, sourcePreferences: SourcePreferences, securityPreferences: SecurityPreferences, libraryPreferences: LibraryPreferences, readerPreferences: ReaderPreferences, backupPreferences: BackupPreferences, trackerManager: TrackerManager, pagePreviewCache: PagePreviewCache, ) + LongParameterList:FavoritesEntryRepositoryImpl.kt$FavoritesEntryRepositoryImpl$( gid: String, token: String, title: String, category: Long, otherGid: String?, otherToken: String?, ) LongParameterList:HistoryMapper.kt$HistoryMapper$( historyId: Long, mangaId: Long, chapterId: Long, title: String, thumbnailUrl: String?, sourceId: Long, isFavorite: Boolean, coverLastModified: Long, chapterNumber: Double, readAt: Date?, readDuration: Long, ) - LongParameterList:HistoryScreen.kt$( state: HistoryScreenModel.State, snackbarHostState: SnackbarHostState, onSearchQueryChange: (String?) -> Unit, onClickCover: (mangaId: Long) -> Unit, onClickResume: (mangaId: Long, chapterId: Long) -> Unit, onDialogChange: (HistoryScreenModel.Dialog?) -> Unit, ) - LongParameterList:InfoScreen.kt$( icon: ImageVector, headingText: String, subtitleText: String, acceptText: String, onAcceptClick: () -> Unit, canAccept: Boolean = true, rejectText: String? = null, onRejectClick: (() -> Unit)? = null, content: @Composable ColumnScope.() -> Unit, ) - LongParameterList:LibraryComfortableGrid.kt$( items: List<LibraryItem>, columns: Int, contentPadding: PaddingValues, selection: List<LibraryManga>, onClick: (LibraryManga) -> Unit, onLongClick: (LibraryManga) -> Unit, onClickContinueReading: ((LibraryManga) -> Unit)?, searchQuery: String?, onGlobalSearchClicked: () -> Unit, ) - LongParameterList:LibraryCompactGrid.kt$( items: List<LibraryItem>, showTitle: Boolean, columns: Int, contentPadding: PaddingValues, selection: List<LibraryManga>, onClick: (LibraryManga) -> Unit, onLongClick: (LibraryManga) -> Unit, onClickContinueReading: ((LibraryManga) -> Unit)?, searchQuery: String?, onGlobalSearchClicked: () -> Unit, ) - LongParameterList:LibraryContent.kt$( categories: List<Category>, searchQuery: String?, selection: List<LibraryManga>, contentPadding: PaddingValues, currentPage: () -> Int, hasActiveFilters: Boolean, showPageTabs: Boolean, onChangeCurrentPage: (Int) -> Unit, onMangaClicked: (Long) -> Unit, onContinueReadingClicked: ((LibraryManga) -> Unit)?, onToggleSelection: (LibraryManga) -> Unit, onToggleRangeSelection: (LibraryManga) -> Unit, onRefresh: (Category?) -> Boolean, onGlobalSearchClicked: () -> Unit, getNumberOfMangaForCategory: (Category) -> Int?, getDisplayMode: (Int) -> PreferenceMutableState<LibraryDisplayMode>, getColumnsForOrientation: (Boolean) -> PreferenceMutableState<Int>, getLibraryForPage: (Int) -> List<LibraryItem>, ) - LongParameterList:LibraryList.kt$( items: List<LibraryItem>, contentPadding: PaddingValues, selection: List<LibraryManga>, onClick: (LibraryManga) -> Unit, onLongClick: (LibraryManga) -> Unit, onClickContinueReading: ((LibraryManga) -> Unit)?, searchQuery: String?, onGlobalSearchClicked: () -> Unit, ) - LongParameterList:LibraryPager.kt$( state: PagerState, contentPadding: PaddingValues, hasActiveFilters: Boolean, selectedManga: List<LibraryManga>, searchQuery: String?, onGlobalSearchClicked: () -> Unit, getDisplayMode: (Int) -> PreferenceMutableState<LibraryDisplayMode>, getColumnsForOrientation: (Boolean) -> PreferenceMutableState<Int>, getLibraryForPage: (Int) -> List<LibraryItem>, onClickManga: (LibraryManga) -> Unit, onLongClickManga: (LibraryManga) -> Unit, onClickContinueReading: ((LibraryManga) -> Unit)?, ) - LongParameterList:LibraryToolbar.kt$( hasActiveFilters: Boolean, selectedCount: Int, title: LibraryToolbarTitle, onClickUnselectAll: () -> Unit, onClickSelectAll: () -> Unit, onClickInvertSelection: () -> Unit, onClickFilter: () -> Unit, onClickRefresh: () -> Unit, onClickGlobalUpdate: () -> Unit, onClickOpenRandomManga: () -> Unit, searchQuery: String?, onSearchQueryChange: (String?) -> Unit, scrollBehavior: TopAppBarScrollBehavior?, ) - LongParameterList:LibraryToolbar.kt$( title: LibraryToolbarTitle, hasFilters: Boolean, searchQuery: String?, onSearchQueryChange: (String?) -> Unit, onClickFilter: () -> Unit, onClickRefresh: () -> Unit, onClickGlobalUpdate: () -> Unit, onClickOpenRandomManga: () -> Unit, scrollBehavior: TopAppBarScrollBehavior?, ) - LongParameterList:ListPreferenceWidget.kt$( value: T, title: String, subtitle: String?, icon: ImageVector?, entries: Map<out T, String>, onValueChange: (T) -> Unit, ) - LongParameterList:MangaBottomActionMenu.kt$( visible: Boolean, onChangeCategoryClicked: () -> Unit, onMarkAsReadClicked: () -> Unit, onMarkAsUnreadClicked: () -> Unit, onDownloadClicked: ((DownloadAction) -> Unit)?, onDeleteClicked: () -> Unit, modifier: Modifier = Modifier, ) LongParameterList:MangaChapterListItem.kt$( action: LibraryPreferences.ChapterSwipeAction, read: Boolean, bookmark: Boolean, downloadState: Download.State, background: Color, onSwipe: () -> Unit, ) - LongParameterList:MangaChapterListItem.kt$( title: String, date: String?, readProgress: String?, scanlator: String?, read: Boolean, bookmark: Boolean, selected: Boolean, downloadIndicatorEnabled: Boolean, downloadStateProvider: () -> Download.State, downloadProgressProvider: () -> Int, chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction, chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction, onLongClick: () -> Unit, onClick: () -> Unit, onDownloadClick: ((ChapterDownloadAction) -> Unit)?, onChapterSwipe: (LibraryPreferences.ChapterSwipeAction) -> Unit, modifier: Modifier = Modifier, ) - LongParameterList:MangaCoverDialog.kt$( coverDataProvider: () -> Manga, isCustomCover: Boolean, snackbarHostState: SnackbarHostState, onShareClick: () -> Unit, onSaveClick: () -> Unit, onEditClick: ((EditCoverAction) -> Unit)?, onDismissRequest: () -> Unit, ) LongParameterList:MangaCoverFetcher.kt$MangaCoverFetcher$( private val url: String?, private val isLibraryManga: Boolean, private val options: Options, private val coverFileLazy: Lazy<File?>, private val customCoverFileLazy: Lazy<File>, private val diskCacheKeyLazy: Lazy<String>, private val sourceLazy: Lazy<HttpSource?>, private val callFactoryLazy: Lazy<Call.Factory>, private val diskCacheLazy: Lazy<DiskCache>, ) - LongParameterList:MangaInfoHeader.kt$( appBarPadding: Dp, coverDataProvider: () -> Manga, onCoverClick: () -> Unit, title: String, doSearch: (query: String, global: Boolean) -> Unit, author: String?, artist: String?, status: Long, sourceName: String, isStubSource: Boolean, ) - LongParameterList:MangaInfoHeader.kt$( favorite: Boolean, trackingCount: Int, nextUpdate: Instant?, isUserIntervalMode: Boolean, onAddToLibraryClicked: () -> Unit, onWebViewClicked: (() -> Unit)?, onWebViewLongClicked: (() -> Unit)?, onTrackingClicked: () -> Unit, onEditIntervalClicked: (() -> Unit)?, onEditCategory: (() -> Unit)?, modifier: Modifier = Modifier, ) - LongParameterList:MangaInfoHeader.kt$( isTabletUi: Boolean, appBarPadding: Dp, title: String, author: String?, artist: String?, sourceName: String, isStubSource: Boolean, coverDataProvider: () -> Manga, status: Long, onCoverClick: () -> Unit, doSearch: (query: String, global: Boolean) -> Unit, modifier: Modifier = Modifier, ) - LongParameterList:MangaInfoHeader.kt$( title: String, doSearch: (query: String, global: Boolean) -> Unit, author: String?, artist: String?, status: Long, sourceName: String, isStubSource: Boolean, textAlign: TextAlign? = LocalTextStyle.current.textAlign, ) - LongParameterList:MangaMapper.kt$MangaMapper$( id: Long, source: Long, url: String, artist: String?, author: String?, description: String?, genre: List<String>?, title: String, status: Long, thumbnailUrl: String?, favorite: Boolean, lastUpdate: Long?, nextUpdate: Long?, initialized: Boolean, viewerFlags: Long, chapterFlags: Long, coverLastModified: Long, dateAdded: Long, updateStrategy: UpdateStrategy, calculateInterval: Long, lastModifiedAt: Long, favoriteModifiedAt: Long?, ) - LongParameterList:MangaMapper.kt$MangaMapper$( id: Long, source: Long, url: String, artist: String?, author: String?, description: String?, genre: List<String>?, title: String, status: Long, thumbnailUrl: String?, favorite: Boolean, lastUpdate: Long?, nextUpdate: Long?, initialized: Boolean, viewerFlags: Long, chapterFlags: Long, coverLastModified: Long, dateAdded: Long, updateStrategy: UpdateStrategy, calculateInterval: Long, lastModifiedAt: Long, favoriteModifiedAt: Long?, totalCount: Long, readCount: Double, latestUpload: Long, chapterFetchedAt: Long, lastRead: Long, bookmarkCount: Double, category: Long, ) - LongParameterList:MangaRestorer.kt$MangaRestorer$( manga: Manga, chapters: List<BackupChapter>, categories: List<Long>, backupCategories: List<BackupCategory>, history: List<BackupHistory>, tracks: List<BackupTracking>, excludedScanlators: List<String>, ) - LongParameterList:MangaScreen.kt$( manga: Manga, chapters: List<ChapterList>, isAnyChapterSelected: Boolean, chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction, chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction, onChapterClicked: (Chapter) -> Unit, onDownloadChapter: ((List<ChapterList.Item>, ChapterDownloadAction) -> Unit)?, onChapterSelected: (ChapterList.Item, Boolean, Boolean, Boolean) -> Unit, onChapterSwipe: (ChapterList.Item, LibraryPreferences.ChapterSwipeAction) -> Unit, ) - LongParameterList:MangaScreen.kt$( selected: List<ChapterList.Item>, onMultiBookmarkClicked: (List<Chapter>, bookmarked: Boolean) -> Unit, onMultiMarkAsReadClicked: (List<Chapter>, markAsRead: Boolean) -> Unit, onMarkPreviousAsReadClicked: (Chapter) -> Unit, onDownloadChapter: ((List<ChapterList.Item>, ChapterDownloadAction) -> Unit)?, onMultiDeleteClicked: (List<Chapter>) -> Unit, fillFraction: Float, modifier: Modifier = Modifier, ) - LongParameterList:MangaScreen.kt$( state: MangaScreenModel.State.Success, snackbarHostState: SnackbarHostState, nextUpdate: Instant?, chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction, chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction, onBackClicked: () -> Unit, onChapterClicked: (Chapter) -> Unit, onDownloadChapter: ((List<ChapterList.Item>, ChapterDownloadAction) -> Unit)?, onAddToLibraryClicked: () -> Unit, onWebViewClicked: (() -> Unit)?, onWebViewLongClicked: (() -> Unit)?, onTrackingClicked: () -> Unit, // For tags menu onTagSearch: (String) -> Unit, onCopyTagToClipboard: (tag: String) -> Unit, onFilterButtonClicked: () -> Unit, onRefresh: () -> Unit, onContinueReading: () -> Unit, onSearch: (query: String, global: Boolean) -> Unit, // For cover dialog onCoverClicked: () -> Unit, // For top action menu onShareClicked: (() -> Unit)?, onDownloadActionClicked: ((DownloadAction) -> Unit)?, onEditCategoryClicked: (() -> Unit)?, onEditIntervalClicked: (() -> Unit)?, onMigrateClicked: (() -> Unit)?, // For bottom action menu onMultiBookmarkClicked: (List<Chapter>, bookmarked: Boolean) -> Unit, onMultiMarkAsReadClicked: (List<Chapter>, markAsRead: Boolean) -> Unit, onMarkPreviousAsReadClicked: (Chapter) -> Unit, onMultiDeleteClicked: (List<Chapter>) -> Unit, // For swipe actions onChapterSwipe: (ChapterList.Item, LibraryPreferences.ChapterSwipeAction) -> Unit, // Chapter selection onChapterSelected: (ChapterList.Item, Boolean, Boolean, Boolean) -> Unit, onAllChapterSelected: (Boolean) -> Unit, onInvertSelection: () -> Unit, ) - LongParameterList:MangaScreen.kt$( state: MangaScreenModel.State.Success, snackbarHostState: SnackbarHostState, nextUpdate: Instant?, chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction, chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction, onBackClicked: () -> Unit, onChapterClicked: (Chapter) -> Unit, onDownloadChapter: ((List<ChapterList.Item>, ChapterDownloadAction) -> Unit)?, onAddToLibraryClicked: () -> Unit, onWebViewClicked: (() -> Unit)?, onWebViewLongClicked: (() -> Unit)?, onTrackingClicked: () -> Unit, // For tags menu onTagSearch: (String) -> Unit, onCopyTagToClipboard: (tag: String) -> Unit, onFilterClicked: () -> Unit, onRefresh: () -> Unit, onContinueReading: () -> Unit, onSearch: (query: String, global: Boolean) -> Unit, // For cover dialog onCoverClicked: () -> Unit, // For top action menu onShareClicked: (() -> Unit)?, onDownloadActionClicked: ((DownloadAction) -> Unit)?, onEditCategoryClicked: (() -> Unit)?, onEditIntervalClicked: (() -> Unit)?, onMigrateClicked: (() -> Unit)?, // For bottom action menu onMultiBookmarkClicked: (List<Chapter>, bookmarked: Boolean) -> Unit, onMultiMarkAsReadClicked: (List<Chapter>, markAsRead: Boolean) -> Unit, onMarkPreviousAsReadClicked: (Chapter) -> Unit, onMultiDeleteClicked: (List<Chapter>) -> Unit, // For chapter swipe onChapterSwipe: (ChapterList.Item, LibraryPreferences.ChapterSwipeAction) -> Unit, // Chapter selection onChapterSelected: (ChapterList.Item, Boolean, Boolean, Boolean) -> Unit, onAllChapterSelected: (Boolean) -> Unit, onInvertSelection: () -> Unit, ) - LongParameterList:MangaScreen.kt$( state: MangaScreenModel.State.Success, snackbarHostState: SnackbarHostState, nextUpdate: Instant?, isTabletUi: Boolean, chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction, chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction, onBackClicked: () -> Unit, onChapterClicked: (Chapter) -> Unit, onDownloadChapter: ((List<ChapterList.Item>, ChapterDownloadAction) -> Unit)?, onAddToLibraryClicked: () -> Unit, onWebViewClicked: (() -> Unit)?, onWebViewLongClicked: (() -> Unit)?, onTrackingClicked: () -> Unit, // For tags menu onTagSearch: (String) -> Unit, onFilterButtonClicked: () -> Unit, onRefresh: () -> Unit, onContinueReading: () -> Unit, onSearch: (query: String, global: Boolean) -> Unit, // For cover dialog onCoverClicked: () -> Unit, // For top action menu onShareClicked: (() -> Unit)?, onDownloadActionClicked: ((DownloadAction) -> Unit)?, onEditCategoryClicked: (() -> Unit)?, onEditFetchIntervalClicked: (() -> Unit)?, onMigrateClicked: (() -> Unit)?, // For bottom action menu onMultiBookmarkClicked: (List<Chapter>, bookmarked: Boolean) -> Unit, onMultiMarkAsReadClicked: (List<Chapter>, markAsRead: Boolean) -> Unit, onMarkPreviousAsReadClicked: (Chapter) -> Unit, onMultiDeleteClicked: (List<Chapter>) -> Unit, // For chapter swipe onChapterSwipe: (ChapterList.Item, LibraryPreferences.ChapterSwipeAction) -> Unit, // Chapter selection onChapterSelected: (ChapterList.Item, Boolean, Boolean, Boolean) -> Unit, onAllChapterSelected: (Boolean) -> Unit, onInvertSelection: () -> Unit, ) - LongParameterList:MangaToolbar.kt$( title: String, titleAlphaProvider: () -> Float, hasFilters: Boolean, onBackClicked: () -> Unit, onClickFilter: () -> Unit, onClickShare: (() -> Unit)?, onClickDownload: ((DownloadAction) -> Unit)?, onClickEditCategory: (() -> Unit)?, onClickRefresh: () -> Unit, onClickMigrate: (() -> Unit)?, // For action mode actionModeCounter: Int, onSelectAll: () -> Unit, onInvertSelection: () -> Unit, modifier: Modifier = Modifier, backgroundAlphaProvider: () -> Float = titleAlphaProvider, ) - LongParameterList:MigrateDialog.kt$( oldManga: Manga, newManga: Manga, screenModel: MigrateDialogScreenModel, onDismissRequest: () -> Unit, onClickTitle: () -> Unit, onPopScreen: () -> Unit, ) - LongParameterList:MigrateDialog.kt$MigrateDialogScreenModel$( oldSource: Source?, newSource: Source, oldManga: Manga, newManga: Manga, sourceChapters: List<SChapter>, replace: Boolean, flags: Int, ) - LongParameterList:MigrateSearchScreen.kt$( state: SearchScreenModel.State, fromSourceId: Long?, navigateUp: () -> Unit, onChangeSearchQuery: (String?) -> Unit, onSearch: (String) -> Unit, onChangeSearchFilter: (SourceFilter) -> Unit, onToggleResults: () -> Unit, getManga: @Composable (Manga) -> State<Manga>, onClickSource: (CatalogueSource) -> Unit, onClickItem: (Manga) -> Unit, onLongClickItem: (Manga) -> Unit, ) - LongParameterList:MigrateSourceScreen.kt$( list: ImmutableList<Pair<Source, Long>>, contentPadding: PaddingValues, onClickItem: (Source) -> Unit, onLongClickItem: (Source) -> Unit, sortingMode: SetMigrateSorting.Mode, onToggleSortingMode: () -> Unit, sortingDirection: SetMigrateSorting.Direction, onToggleSortingDirection: () -> Unit, ) - LongParameterList:MoreScreen.kt$( downloadQueueStateProvider: () -> DownloadQueueState, downloadedOnly: Boolean, onDownloadedOnlyChange: (Boolean) -> Unit, incognitoMode: Boolean, onIncognitoModeChange: (Boolean) -> Unit, isFDroid: Boolean, onClickDownloadQueue: () -> Unit, onClickCategories: () -> Unit, onClickStats: () -> Unit, onClickDataAndStorage: () -> Unit, onClickSettings: () -> Unit, onClickAbout: () -> Unit, ) - LongParameterList:ReaderAppBars.kt$( visible: Boolean, fullscreen: Boolean, mangaTitle: String?, chapterTitle: String?, navigateUp: () -> Unit, onClickTopAppBar: () -> Unit, bookmarked: Boolean, onToggleBookmarked: () -> Unit, onOpenInWebView: (() -> Unit)?, onShare: (() -> Unit)?, viewer: Viewer?, onNextChapter: () -> Unit, enabledNext: Boolean, onPreviousChapter: () -> Unit, enabledPrevious: Boolean, currentPage: Int, totalPages: Int, onSliderValueChange: (Int) -> Unit, readingMode: ReadingMode, onClickReadingMode: () -> Unit, orientation: ReaderOrientation, onClickOrientation: () -> Unit, cropEnabled: Boolean, onClickCropBorder: () -> Unit, onClickSettings: () -> Unit, ) - LongParameterList:Scaffold.kt$( fabPosition: FabPosition, topBar: @Composable () -> Unit, startBar: @Composable () -> Unit, content: @Composable (PaddingValues) -> Unit, snackbar: @Composable () -> Unit, fab: @Composable () -> Unit, contentWindowInsets: WindowInsets, bottomBar: @Composable () -> Unit, ) + LongParameterList:MangaMapper.kt$MangaMapper$( id: Long, source: Long, url: String, artist: String?, author: String?, description: String?, genre: List<String>?, title: String, status: Long, thumbnailUrl: String?, favorite: Boolean, lastUpdate: Long?, nextUpdate: Long?, initialized: Boolean, viewerFlags: Long, chapterFlags: Long, coverLastModified: Long, dateAdded: Long, // SY --> @Suppress("UNUSED_PARAMETER") filteredScanlators: String?, // SY <-- updateStrategy: UpdateStrategy, calculateInterval: Long, lastModifiedAt: Long, favoriteModifiedAt: Long?, ) + LongParameterList:MangaMapper.kt$MangaMapper$( id: Long, source: Long, url: String, artist: String?, author: String?, description: String?, genre: List<String>?, title: String, status: Long, thumbnailUrl: String?, favorite: Boolean, lastUpdate: Long?, nextUpdate: Long?, initialized: Boolean, viewerFlags: Long, chapterFlags: Long, coverLastModified: Long, dateAdded: Long, // SY --> @Suppress("UNUSED_PARAMETER") filteredScanlators: String?, // SY <-- updateStrategy: UpdateStrategy, calculateInterval: Long, lastModifiedAt: Long, favoriteModifiedAt: Long?, totalCount: Long, readCount: Double, latestUpload: Long, chapterFetchedAt: Long, lastRead: Long, bookmarkCount: Double, category: Long, ) + LongParameterList:MangaRestorer.kt$MangaRestorer$( manga: Manga, chapters: List<BackupChapter>, categories: List<Long>, backupCategories: List<BackupCategory>, history: List<BackupHistory>, tracks: List<BackupTracking>, excludedScanlators: List<String>, // SY --> mergedMangaReferences: List<BackupMergedMangaReference>, flatMetadata: BackupFlatMetadata?, customManga: CustomMangaInfo?, // SY <-- ) + LongParameterList:MangaScreen.kt$( manga: Manga, chapters: List<ChapterList>, isAnyChapterSelected: Boolean, chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction, chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction, // SY --> alwaysShowReadingProgress: Boolean, // SY <-- onChapterClicked: (Chapter) -> Unit, onDownloadChapter: ((List<ChapterList.Item>, ChapterDownloadAction) -> Unit)?, onChapterSelected: (ChapterList.Item, Boolean, Boolean, Boolean) -> Unit, onChapterSwipe: (ChapterList.Item, LibraryPreferences.ChapterSwipeAction) -> Unit, ) + LongParameterList:MangaScreenModel.kt$MangaScreenModel$( title: String?, author: String?, artist: String?, thumbnailUrl: String?, description: String?, tags: List<String>?, status: Long?, ) + LongParameterList:MergedMangaMapper.kt$MergedMangaMapper$( id: Long, isInfoManga: Boolean, getChapterUpdates: Boolean, chapterSortMode: Long, chapterPriority: Long, downloadChapters: Boolean, mergeId: Long, mergeUrl: String, mangaId: Long?, mangaUrl: String, mangaSourceId: Long, ) + LongParameterList:Migrations.kt$Migrations$( context: Context, preferenceStore: PreferenceStore, basePreferences: BasePreferences, uiPreferences: UiPreferences, networkPreferences: NetworkPreferences, sourcePreferences: SourcePreferences, securityPreferences: SecurityPreferences, libraryPreferences: LibraryPreferences, readerPreferences: ReaderPreferences, backupPreferences: BackupPreferences, trackerManager: TrackerManager, ) + LongParameterList:PageHandler.kt$PageHandler$( private val headers: Headers, private val service: MangaDexService, private val mangaPlusHandler: MangaPlusHandler, private val comikeyHandler: ComikeyHandler, private val bilibiliHandler: BilibiliHandler, private val azukiHandler: AzukiHandler, private val mangaHotHandler: MangaHotHandler, private val preferences: TrackPreferences, private val mdList: MdList, ) + LongParameterList:PagePreviewFetcher.kt$PagePreviewFetcher$( private val page: PagePreview, private val options: Options, private val pagePreviewFile: () -> File, private val isInCache: () -> Boolean, private val writeToCache: (Source) -> Unit, private val diskCacheKeyLazy: Lazy<String>, private val sourceLazy: Lazy<PagePreviewSource?>, private val callFactoryLazy: Lazy<Call.Factory>, private val diskCacheLazy: Lazy<DiskCache>, ) + LongParameterList:PagePreviews.kt$( pagePreviewState: PagePreviewState, onOpenPage: (Int) -> Unit, onMorePreviewsClicked: () -> Unit, maxWidth: Dp, setMaxWidth: (Dp) -> Unit, rowCount: Int, ) + LongParameterList:ReaderViewModel.kt$ReaderViewModel$( page1: ReaderPage, page2: ReaderPage, isLTR: Boolean, @ColorInt bg: Int, location: Location, manga: Manga, ) LongParameterList:Scrollbar.kt$( orientation: Orientation, reverseDirection: Boolean, atEnd: Boolean, showScrollbar: Boolean, thickness: Float, color: Color, alpha: () -> Float, thumbSize: Float, scrollOffset: Float, positionOffset: Float, ) LongParameterList:SetMangaChapterFlags.kt$SetMangaChapterFlags$( mangaId: Long, unreadFilter: Long, downloadedFilter: Long, bookmarkedFilter: Long, sortingMode: Long, sortingDirection: Long, displayMode: Long, ) LongParameterList:SyncChaptersWithSource.kt$SyncChaptersWithSource$( private val downloadManager: DownloadManager, private val downloadProvider: DownloadProvider, private val chapterRepository: ChapterRepository, private val shouldUpdateDbChapter: ShouldUpdateDbChapter, private val updateManga: UpdateManga, private val updateChapter: UpdateChapter, private val getChaptersByMangaId: GetChaptersByMangaId, private val getExcludedScanlators: GetExcludedScanlators, ) - LongParameterList:TrackInfoDialogHome.kt$( title: String, tracker: Tracker, status: StringResource?, onStatusClick: () -> Unit, chapters: String, onChaptersClick: () -> Unit, score: String?, onScoreClick: (() -> Unit)?, startDate: String?, onStartDateClick: (() -> Unit)?, endDate: String?, onEndDateClick: (() -> Unit)?, onNewSearch: () -> Unit, onOpenInBrowser: () -> Unit, onRemoved: () -> Unit, ) - LongParameterList:TrackInfoDialogHome.kt$( trackItems: List<TrackItem>, dateFormat: DateFormat, onStatusClick: (TrackItem) -> Unit, onChapterClick: (TrackItem) -> Unit, onScoreClick: (TrackItem) -> Unit, onStartDateEdit: (TrackItem) -> Unit, onEndDateEdit: (TrackItem) -> Unit, onNewSearch: (TrackItem) -> Unit, onOpenInBrowser: (TrackItem) -> Unit, onRemoved: (TrackItem) -> Unit, ) - LongParameterList:TrackInfoDialogSelector.kt$( title: String, initialSelectedDateMillis: Long, selectableDates: SelectableDates, onConfirm: (Long) -> Unit, onRemove: (() -> Unit)?, onDismissRequest: () -> Unit, ) LongParameterList:TrackMapper.kt$TrackMapper$( id: Long, mangaId: Long, syncId: Long, remoteId: Long, libraryId: Long?, title: String, lastChapterRead: Double, totalChapters: Long, status: Long, score: Double, remoteUrl: String, startDate: Long, finishDate: Long, ) - LongParameterList:TrackerSearch.kt$( query: TextFieldValue, onQueryChange: (TextFieldValue) -> Unit, onDispatchQuery: () -> Unit, queryResult: Result<List<TrackSearch>>?, selected: TrackSearch?, onSelectedChange: (TrackSearch) -> Unit, onConfirmSelection: () -> Unit, onDismissRequest: () -> Unit, ) - LongParameterList:TriStateListDialog.kt$( title: String, message: String? = null, items: List<T>, initialChecked: List<T>, initialInversed: List<T>, itemLabel: @Composable (T) -> String, onDismissRequest: () -> Unit, onValueChanged: (newIncluded: List<T>, newExcluded: List<T>) -> Unit, ) LongParameterList:UpdatesRepositoryImpl.kt$UpdatesRepositoryImpl$( mangaId: Long, mangaTitle: String, chapterId: Long, chapterName: String, scanlator: String?, read: Boolean, bookmark: Boolean, lastPageRead: Long, sourceId: Long, favorite: Boolean, thumbnailUrl: String?, coverLastModified: Long, dateUpload: Long, dateFetch: Long, ) - LongParameterList:UpdatesScreen.kt$( onUpdateLibrary: () -> Unit, // For action mode actionModeCounter: Int, onSelectAll: () -> Unit, onInvertSelection: () -> Unit, onCancelActionMode: () -> Unit, scrollBehavior: TopAppBarScrollBehavior, modifier: Modifier = Modifier, ) - LongParameterList:UpdatesScreen.kt$( state: UpdatesScreenModel.State, snackbarHostState: SnackbarHostState, lastUpdated: Long, onClickCover: (UpdatesItem) -> Unit, onSelectAll: (Boolean) -> Unit, onInvertSelection: () -> Unit, onUpdateLibrary: () -> Boolean, onDownloadChapter: (List<UpdatesItem>, ChapterDownloadAction) -> Unit, onMultiBookmarkClicked: (List<UpdatesItem>, bookmark: Boolean) -> Unit, onMultiMarkAsReadClicked: (List<UpdatesItem>, read: Boolean) -> Unit, onMultiDeleteClicked: (List<UpdatesItem>) -> Unit, onUpdateSelected: (UpdatesItem, Boolean, Boolean, Boolean) -> Unit, onOpenChapter: (UpdatesItem) -> Unit, ) - LongParameterList:UpdatesUiItem.kt$( uiModels: List<UpdatesUiModel>, selectionMode: Boolean, onUpdateSelected: (UpdatesItem, Boolean, Boolean, Boolean) -> Unit, onClickCover: (UpdatesItem) -> Unit, onClickUpdate: (UpdatesItem) -> Unit, onDownloadChapter: (List<UpdatesItem>, ChapterDownloadAction) -> Unit, ) - LongParameterList:UpdatesUiItem.kt$( update: UpdatesWithRelations, selected: Boolean, readProgress: String?, onClick: () -> Unit, onLongClick: () -> Unit, onClickCover: (() -> Unit)?, onDownloadChapter: ((ChapterDownloadAction) -> Unit)?, // Download Indicator downloadStateProvider: () -> Download.State, downloadProgressProvider: () -> Int, modifier: Modifier = Modifier, ) - LongParameterList:WebViewScreenContent.kt$( onNavigateUp: () -> Unit, initialTitle: String?, url: String, onShare: (String) -> Unit, onOpenInBrowser: (String) -> Unit, onClearCookies: (String) -> Unit, headers: Map<String, String> = emptyMap(), onUrlChange: (String) -> Unit = {}, ) + LongParameterList:UpdatesUiItem.kt$( uiModels: List<UpdatesUiModel>, selectionMode: Boolean, // SY --> preserveReadingPosition: Boolean, // SY <-- onUpdateSelected: (UpdatesItem, Boolean, Boolean, Boolean) -> Unit, onClickCover: (UpdatesItem) -> Unit, onClickUpdate: (UpdatesItem) -> Unit, onDownloadChapter: (List<UpdatesItem>, ChapterDownloadAction) -> Unit, ) LongParameterList:WebtoonRecyclerView.kt$WebtoonRecyclerView$( fromRate: Float, toRate: Float, fromX: Float, toX: Float, fromY: Float, toY: Float, ) LoopWithTooManyJumpStatements:DownloadStore.kt$DownloadStore$for + LoopWithTooManyJumpStatements:EHentaiUpdateWorker.kt$EHentaiUpdateWorker$for LoopWithTooManyJumpStatements:ImageUtil.kt$ImageUtil$for LoopWithTooManyJumpStatements:Kavita.kt$Kavita$for + LoopWithTooManyJumpStatements:MemAutoFlushingLookupTable.kt$MemAutoFlushingLookupTable$while + LoopWithTooManyJumpStatements:MigrationListScreenModel.kt$MigrationListScreenModel$for LoopWithTooManyJumpStatements:RateLimitInterceptor.kt$RateLimitInterceptor$while LoopWithTooManyJumpStatements:UpdatesScreenModel.kt$UpdatesScreenModel$for MagicNumber:AdaptiveSheet.kt$220 @@ -362,6 +597,7 @@ MagicNumber:AnilistModels.kt$90 MagicNumber:App.kt$App$300 MagicNumber:App.kt$App$8 + MagicNumber:AppModule.kt$AppModule$25 MagicNumber:AppThemePreferenceWidget.kt$0.3f MagicNumber:AppThemePreferenceWidget.kt$0.5f MagicNumber:AppThemePreferenceWidget.kt$0.6f @@ -372,10 +608,12 @@ MagicNumber:AppUpdateDownloadJob.kt$AppUpdateDownloadJob.<no name provided>$100 MagicNumber:AppUpdateDownloadJob.kt$AppUpdateDownloadJob.<no name provided>$200 MagicNumber:AppUpdateNotifier.kt$AppUpdateNotifier$100 + MagicNumber:AutoCompleteItem.kt$100 MagicNumber:Backup.kt$Backup$100 MagicNumber:Backup.kt$Backup$101 MagicNumber:Backup.kt$Backup$104 MagicNumber:Backup.kt$Backup$105 + MagicNumber:Backup.kt$Backup$600 MagicNumber:BackupCategory.kt$BackupCategory$100 MagicNumber:BackupChapter.kt$BackupChapter$10 MagicNumber:BackupChapter.kt$BackupChapter$11 @@ -388,6 +626,7 @@ MagicNumber:BackupChapter.kt$BackupChapter$9 MagicNumber:BackupCreateJob.kt$BackupCreateJob.Companion$10 MagicNumber:BackupDecoder.kt$BackupDecoder$0x1f8b + MagicNumber:BackupFlatMetadata.kt$BackupFlatMetadata$3 MagicNumber:BackupHistory.kt$BackupHistory$3 MagicNumber:BackupManga.kt$BackupManga$100 MagicNumber:BackupManga.kt$BackupManga$101 @@ -407,10 +646,30 @@ MagicNumber:BackupManga.kt$BackupManga$4 MagicNumber:BackupManga.kt$BackupManga$5 MagicNumber:BackupManga.kt$BackupManga$6 + MagicNumber:BackupManga.kt$BackupManga$600 + MagicNumber:BackupManga.kt$BackupManga$601 + MagicNumber:BackupManga.kt$BackupManga$602 + MagicNumber:BackupManga.kt$BackupManga$603 MagicNumber:BackupManga.kt$BackupManga$7 MagicNumber:BackupManga.kt$BackupManga$8 + MagicNumber:BackupManga.kt$BackupManga$800 + MagicNumber:BackupManga.kt$BackupManga$801 + MagicNumber:BackupManga.kt$BackupManga$802 + MagicNumber:BackupManga.kt$BackupManga$804 + MagicNumber:BackupManga.kt$BackupManga$805 MagicNumber:BackupManga.kt$BackupManga$9 + MagicNumber:BackupMergedMangaReference.kt$BackupMergedMangaReference$3 + MagicNumber:BackupMergedMangaReference.kt$BackupMergedMangaReference$4 + MagicNumber:BackupMergedMangaReference.kt$BackupMergedMangaReference$5 + MagicNumber:BackupMergedMangaReference.kt$BackupMergedMangaReference$6 + MagicNumber:BackupMergedMangaReference.kt$BackupMergedMangaReference$7 + MagicNumber:BackupMergedMangaReference.kt$BackupMergedMangaReference$8 MagicNumber:BackupPreferences.kt$BackupPreferences$12 + MagicNumber:BackupSavedSearch.kt$BackupSavedSearch$3 + MagicNumber:BackupSavedSearch.kt$BackupSavedSearch$4 + MagicNumber:BackupSearchMetadata.kt$BackupSearchMetadata$3 + MagicNumber:BackupSearchMetadata.kt$BackupSearchMetadata$4 + MagicNumber:BackupSearchTag.kt$BackupSearchTag$3 MagicNumber:BackupTracking.kt$BackupTracking$10 MagicNumber:BackupTracking.kt$BackupTracking$100 MagicNumber:BackupTracking.kt$BackupTracking$11 @@ -429,8 +688,19 @@ MagicNumber:BangumiModels.kt$3600 MagicNumber:BangumiModels.kt$OAuth$1000 MagicNumber:BasePreferenceWidget.kt$200 + MagicNumber:BatchAddScreen.kt$BatchAddScreen$0.15f MagicNumber:BrowseIcons.kt$0x1F888888 + MagicNumber:BrowseSourceEHentaiList.kt$0.5F + MagicNumber:BrowseSourceEHentaiList.kt$0xE1E2ECFF + MagicNumber:BrowseSourceEHentaiList.kt$0xFF005ED7 + MagicNumber:BrowseSourceEHentaiList.kt$5 + MagicNumber:CbzCrypto.kt$CbzCrypto$100 + MagicNumber:CbzCrypto.kt$CbzCrypto$128 + MagicNumber:CbzCrypto.kt$CbzCrypto$32 + MagicNumber:CbzCrypto.kt$CbzCrypto$42 + MagicNumber:ChapterCache.kt$ChapterCache$1024 MagicNumber:ChapterDownloadIndicator.kt$0.5f + MagicNumber:ChapterNavigator.kt$90f MagicNumber:ChapterRecognition.kt$ChapterRecognition$10 MagicNumber:ChapterRecognition.kt$ChapterRecognition$10.0 MagicNumber:ChapterRecognition.kt$ChapterRecognition$2.0 @@ -450,6 +720,8 @@ MagicNumber:DeviceUtil.kt$DeviceUtil$3L MagicNumber:DeviceUtil.kt$DeviceUtil$90000 MagicNumber:DiskUtil.kt$DiskUtil$240 + MagicNumber:DomainSourceHelpers.kt$6900 + MagicNumber:DomainSourceHelpers.kt$6999 MagicNumber:Download.kt$Download.State.DOWNLOADED$3 MagicNumber:Download.kt$Download.State.ERROR$4 MagicNumber:DownloadCache.kt$DownloadCache$1000 @@ -468,13 +740,135 @@ MagicNumber:Downloader.kt$Downloader$3 MagicNumber:Downloader.kt$Downloader$5 MagicNumber:DropdownMenu.kt$56 + MagicNumber:EHConfigurator.kt$EHConfigurator$3 + MagicNumber:EHDebugModeOverlay.kt$0x7F000000 + MagicNumber:EHDebugModeOverlay.kt$FpsState$1000f + MagicNumber:EHentai.kt$EHentai$0.5 + MagicNumber:EHentai.kt$EHentai$14 + MagicNumber:EHentai.kt$EHentai$16 + MagicNumber:EHentai.kt$EHentai$17 + MagicNumber:EHentai.kt$EHentai$200 + MagicNumber:EHentai.kt$EHentai$2007 + MagicNumber:EHentai.kt$EHentai$2099 + MagicNumber:EHentai.kt$EHentai$21 + MagicNumber:EHentai.kt$EHentai$3 + MagicNumber:EHentai.kt$EHentai$4 + MagicNumber:EHentai.kt$EHentai$404 + MagicNumber:EHentai.kt$EHentai$5 + MagicNumber:EHentai.kt$EHentai$8 + MagicNumber:EHentai.kt$EHentai$9 + MagicNumber:EHentai.kt$EHentai.GenreGroup$128 + MagicNumber:EHentai.kt$EHentai.GenreGroup$16 + MagicNumber:EHentai.kt$EHentai.GenreGroup$256 + MagicNumber:EHentai.kt$EHentai.GenreGroup$32 + MagicNumber:EHentai.kt$EHentai.GenreGroup$4 + MagicNumber:EHentai.kt$EHentai.GenreGroup$512 + MagicNumber:EHentai.kt$EHentai.GenreGroup$64 + MagicNumber:EHentai.kt$EHentai.GenreGroup$8 + MagicNumber:EHentai.kt$EHentai.ThumbnailPreviewInterceptor$100 + MagicNumber:EHentai.kt$EHentai.ToplistOption.ALL_TIME$11 + MagicNumber:EHentai.kt$EHentai.ToplistOption.PAST_MONTH$13 + MagicNumber:EHentai.kt$EHentai.ToplistOption.PAST_YEAR$12 + MagicNumber:EHentai.kt$EHentai.ToplistOption.YESTERDAY$15 + MagicNumber:EHentaiUpdateWorker.kt$EHentaiUpdateWorker.Companion$10 + MagicNumber:EXHMigrations.kt$EXHMigrations$0b00100000L + MagicNumber:EXHMigrations.kt$EXHMigrations$0b00111100L + MagicNumber:EXHMigrations.kt$EXHMigrations$10 + MagicNumber:EXHMigrations.kt$EXHMigrations$12 + MagicNumber:EXHMigrations.kt$EXHMigrations$14 + MagicNumber:EXHMigrations.kt$EXHMigrations$16 + MagicNumber:EXHMigrations.kt$EXHMigrations$17 + MagicNumber:EXHMigrations.kt$EXHMigrations$18 + MagicNumber:EXHMigrations.kt$EXHMigrations$20 + MagicNumber:EXHMigrations.kt$EXHMigrations$22 + MagicNumber:EXHMigrations.kt$EXHMigrations$23 + MagicNumber:EXHMigrations.kt$EXHMigrations$24 + MagicNumber:EXHMigrations.kt$EXHMigrations$27 + MagicNumber:EXHMigrations.kt$EXHMigrations$28 + MagicNumber:EXHMigrations.kt$EXHMigrations$29 + MagicNumber:EXHMigrations.kt$EXHMigrations$3 + MagicNumber:EXHMigrations.kt$EXHMigrations$31 + MagicNumber:EXHMigrations.kt$EXHMigrations$32 + MagicNumber:EXHMigrations.kt$EXHMigrations$38 + MagicNumber:EXHMigrations.kt$EXHMigrations$39 + MagicNumber:EXHMigrations.kt$EXHMigrations$4 + MagicNumber:EXHMigrations.kt$EXHMigrations$40 + MagicNumber:EXHMigrations.kt$EXHMigrations$41 + MagicNumber:EXHMigrations.kt$EXHMigrations$42 + MagicNumber:EXHMigrations.kt$EXHMigrations$43 + MagicNumber:EXHMigrations.kt$EXHMigrations$44 + MagicNumber:EXHMigrations.kt$EXHMigrations$45 + MagicNumber:EXHMigrations.kt$EXHMigrations$5 + MagicNumber:EXHMigrations.kt$EXHMigrations$52 + MagicNumber:EXHMigrations.kt$EXHMigrations$56 + MagicNumber:EXHMigrations.kt$EXHMigrations$57 + MagicNumber:EXHMigrations.kt$EXHMigrations$58 + MagicNumber:EXHMigrations.kt$EXHMigrations$59 + MagicNumber:EXHMigrations.kt$EXHMigrations$6 + MagicNumber:EXHMigrations.kt$EXHMigrations$60 + MagicNumber:EXHMigrations.kt$EXHMigrations$66 + MagicNumber:EXHMigrations.kt$EXHMigrations$6907 + MagicNumber:EXHMigrations.kt$EXHMigrations$6907L + MagicNumber:EXHMigrations.kt$EXHMigrations$6909L + MagicNumber:EXHMigrations.kt$EXHMigrations$6912 + MagicNumber:EXHMigrations.kt$EXHMigrations$6912L + MagicNumber:EXHMigrations.kt$EXHMigrations$7 + MagicNumber:EXHMigrations.kt$EXHMigrations$8 + MagicNumber:EXHMigrations.kt$EXHMigrations$9 + MagicNumber:EditMangaDialog.kt$20 + MagicNumber:EditMangaDialog.kt$3 + MagicNumber:EditMangaDialog.kt$4 + MagicNumber:EditMangaDialog.kt$40 + MagicNumber:EditMangaDialog.kt$46 + MagicNumber:EditMangaDialog.kt$5 + MagicNumber:EditMangaDialog.kt$6 + MagicNumber:EditMangaDialog.kt$61 + MagicNumber:EditMangaDialog.kt$62 + MagicNumber:EditMangaDialog.kt$63 + MagicNumber:EditMergedMangaHolder.kt$EditMergedMangaHolder$0.5F + MagicNumber:EditMergedSettingsHeaderAdapter.kt$EditMergedSettingsHeaderAdapter.HeaderViewHolder$0.5F + MagicNumber:EditMergedSettingsHeaderAdapter.kt$EditMergedSettingsHeaderAdapter.HeaderViewHolder$3 + MagicNumber:EhLoginWebViewScreen.kt$0.5F + MagicNumber:EhLoginWebViewScreen.kt$0.8F + MagicNumber:EhLoginWebViewScreen.kt$0xb5000000 + MagicNumber:EhUConfigBuilder.kt$Entry.Categories$3 + MagicNumber:EhUConfigBuilder.kt$Entry.Categories$4 + MagicNumber:EhUConfigBuilder.kt$Entry.Categories$5 + MagicNumber:EhUConfigBuilder.kt$Entry.Categories$6 + MagicNumber:EhUConfigBuilder.kt$Entry.Categories$7 + MagicNumber:EhUConfigBuilder.kt$Entry.Categories$8 + MagicNumber:EhUConfigBuilder.kt$Entry.Categories$9 + MagicNumber:EhUConfigBuilder.kt$Entry.LanguageSystem$10 + MagicNumber:EhUConfigBuilder.kt$Entry.LanguageSystem$11 + MagicNumber:EhUConfigBuilder.kt$Entry.LanguageSystem$12 + MagicNumber:EhUConfigBuilder.kt$Entry.LanguageSystem$13 + MagicNumber:EhUConfigBuilder.kt$Entry.LanguageSystem$14 + MagicNumber:EhUConfigBuilder.kt$Entry.LanguageSystem$15 + MagicNumber:EhUConfigBuilder.kt$Entry.LanguageSystem$16 + MagicNumber:EhUConfigBuilder.kt$Entry.LanguageSystem$3 + MagicNumber:EhUConfigBuilder.kt$Entry.LanguageSystem$4 + MagicNumber:EhUConfigBuilder.kt$Entry.LanguageSystem$5 + MagicNumber:EhUConfigBuilder.kt$Entry.LanguageSystem$6 + MagicNumber:EhUConfigBuilder.kt$Entry.LanguageSystem$7 + MagicNumber:EhUConfigBuilder.kt$Entry.LanguageSystem$8 + MagicNumber:EhUConfigBuilder.kt$Entry.LanguageSystem$9 + MagicNumber:ExhUtils.kt$0.5f + MagicNumber:ExhUtils.kt$0.75f + MagicNumber:ExhUtils.kt$0.9f + MagicNumber:ExhUtils.kt$3f MagicNumber:ExtensionDetailsScreen.kt$1.5f MagicNumber:ExtensionInstallService.kt$ExtensionInstallService$100 + MagicNumber:FavoritesSyncHelper.kt$FavoritesSyncHelper$10 + MagicNumber:FeedScreenModel.kt$FeedScreenModel$10 MagicNumber:FetchInterval.kt$FetchInterval$10 MagicNumber:FetchInterval.kt$FetchInterval$1000 MagicNumber:FetchInterval.kt$FetchInterval$3 MagicNumber:FetchInterval.kt$FetchInterval$7 MagicNumber:FetchInterval.kt$FetchInterval$8 + MagicNumber:FollowStatus.kt$FollowStatus.DROPPED$5L + MagicNumber:FollowStatus.kt$FollowStatus.ON_HOLD$3L + MagicNumber:FollowStatus.kt$FollowStatus.PLAN_TO_READ$4L + MagicNumber:FollowStatus.kt$FollowStatus.RE_READING$6L MagicNumber:GetApplicationRelease.kt$GetApplicationRelease$3 MagicNumber:GlanceUtils.kt$10 MagicNumber:GlanceUtils.kt$64 @@ -490,6 +884,7 @@ MagicNumber:ImageUtil.kt$ImageUtil$0.0275 MagicNumber:ImageUtil.kt$ImageUtil$100 MagicNumber:ImageUtil.kt$ImageUtil$14 + MagicNumber:ImageUtil.kt$ImageUtil$16384 MagicNumber:ImageUtil.kt$ImageUtil$18 MagicNumber:ImageUtil.kt$ImageUtil$200 MagicNumber:ImageUtil.kt$ImageUtil$22 @@ -503,6 +898,10 @@ MagicNumber:ImageUtil.kt$ImageUtil$6 MagicNumber:ImageUtil.kt$ImageUtil$740 MagicNumber:ImageUtil.kt$ImageUtil$9 + MagicNumber:ImageUtil.kt$ImageUtil$96 + MagicNumber:ImageUtil.kt$ImageUtil$98 + MagicNumber:ImageUtil.kt$ImageUtil$99 + MagicNumber:IsTrackUnfollowed.kt$IsTrackUnfollowed$60L MagicNumber:Kavita.kt$Kavita$0xff MagicNumber:Kavita.kt$Kavita$148 MagicNumber:Kavita.kt$Kavita$198 @@ -524,15 +923,19 @@ MagicNumber:Komga.kt$Komga$37 MagicNumber:Komga.kt$Komga$50 MagicNumber:Komga.kt$Komga$51 + MagicNumber:LewdMangaChecker.kt$6905L + MagicNumber:LewdMangaChecker.kt$6913L MagicNumber:LibraryScreenModel.kt$LibraryScreenModel$10 MagicNumber:LibraryScreenModel.kt$LibraryScreenModel$25 MagicNumber:LibraryScreenModel.kt$LibraryScreenModel$5 + MagicNumber:LibrarySettingsDialog.kt$3 MagicNumber:LibrarySortMode.kt$LibrarySort.Direction.Ascending$0b01000000 MagicNumber:LibrarySortMode.kt$LibrarySort.Type.ChapterFetchDate$0b00011000 MagicNumber:LibrarySortMode.kt$LibrarySort.Type.DateAdded$0b00011100 MagicNumber:LibrarySortMode.kt$LibrarySort.Type.LastRead$0b00000100 MagicNumber:LibrarySortMode.kt$LibrarySort.Type.LastUpdate$0b00001000 MagicNumber:LibrarySortMode.kt$LibrarySort.Type.LatestChapter$0b00010100 + MagicNumber:LibrarySortMode.kt$LibrarySort.Type.TagList$0b00100100 MagicNumber:LibrarySortMode.kt$LibrarySort.Type.TotalChapters$0b00010000 MagicNumber:LibrarySortMode.kt$LibrarySort.Type.TrackerMean$0b000100000 MagicNumber:LibrarySortMode.kt$LibrarySort.Type.UnreadCount$0b00001100 @@ -547,8 +950,11 @@ MagicNumber:MangaBottomActionMenu.kt$6 MagicNumber:MangaBottomActionMenu.kt$7 MagicNumber:MangaCover.kt$MangaCover.Book$3f + MagicNumber:MangaDexAuthInterceptor.kt$MangaDexAuthInterceptor$401 + MagicNumber:MangaDexDescriptionAdapter.kt$100.0 MagicNumber:MangaInfoHeader.kt$0.2f MagicNumber:MangaInfoHeader.kt$0.65f + MagicNumber:MangaPlusHandler.kt$MangaPlusHandler$16 MagicNumber:MangaScreenModel.kt$MangaScreenModel$10 MagicNumber:MangaScreenModel.kt$MangaScreenModel$103 MagicNumber:MangaScreenModel.kt$MangaScreenModel$25 @@ -557,7 +963,70 @@ MagicNumber:MangaUpdates.kt$MangaUpdates$160 MagicNumber:MangaUpdates.kt$MangaUpdates$173 MagicNumber:MangaUpdatesApi.kt$MangaUpdatesApi$200 + MagicNumber:MdList.kt$MdList$3L + MagicNumber:MdList.kt$MdList$43 + MagicNumber:MdList.kt$MdList$48 + MagicNumber:MdList.kt$MdList$4L + MagicNumber:MdList.kt$MdList$53 + MagicNumber:MdList.kt$MdList$5L + MagicNumber:MdList.kt$MdList$6L + MagicNumber:MemAutoFlushingLookupTable.kt$MemAutoFlushingLookupTable$4 + MagicNumber:MemAutoFlushingLookupTable.kt$MemAutoFlushingLookupTable$8 + MagicNumber:MergedSource.kt$MergedSource$5 + MagicNumber:MetadataUIUtil.kt$MetadataUIUtil$10 + MagicNumber:MetadataUIUtil.kt$MetadataUIUtil$3 + MagicNumber:MetadataUIUtil.kt$MetadataUIUtil$4 + MagicNumber:MetadataUIUtil.kt$MetadataUIUtil$5 + MagicNumber:MetadataUIUtil.kt$MetadataUIUtil$6 + MagicNumber:MetadataUIUtil.kt$MetadataUIUtil$7 + MagicNumber:MetadataUIUtil.kt$MetadataUIUtil$8 + MagicNumber:MetadataUIUtil.kt$MetadataUIUtil$9 MagicNumber:MetadataUpdateJob.kt$MetadataUpdateJob$5 + MagicNumber:MetadataUtil.kt$MetadataUtil$1000 + MagicNumber:MetadataUtil.kt$MetadataUtil$1024 + MagicNumber:MigrationActionIcon.kt$56 + MagicNumber:MigrationItem.kt$0.33f + MagicNumber:MigrationItem.kt$0xAA000000 + MagicNumber:MigrationListScreen.kt$0.2f + MagicNumber:MigrationListScreenModel.kt$MigrationListScreenModel$3 + MagicNumber:Migrations.kt$Migrations$105 + MagicNumber:Migrations.kt$Migrations$106 + MagicNumber:Migrations.kt$Migrations$113 + MagicNumber:Migrations.kt$Migrations$114 + MagicNumber:Migrations.kt$Migrations$116 + MagicNumber:Migrations.kt$Migrations$117 + MagicNumber:Migrations.kt$Migrations$12 + MagicNumber:Migrations.kt$Migrations$15 + MagicNumber:Migrations.kt$Migrations$19 + MagicNumber:Migrations.kt$Migrations$26 + MagicNumber:Migrations.kt$Migrations$3 + MagicNumber:Migrations.kt$Migrations$4 + MagicNumber:Migrations.kt$Migrations$44 + MagicNumber:Migrations.kt$Migrations$5 + MagicNumber:Migrations.kt$Migrations$52 + MagicNumber:Migrations.kt$Migrations$54 + MagicNumber:Migrations.kt$Migrations$57 + MagicNumber:Migrations.kt$Migrations$59 + MagicNumber:Migrations.kt$Migrations$6 + MagicNumber:Migrations.kt$Migrations$60 + MagicNumber:Migrations.kt$Migrations$61 + MagicNumber:Migrations.kt$Migrations$64 + MagicNumber:Migrations.kt$Migrations$7 + MagicNumber:Migrations.kt$Migrations$70 + MagicNumber:Migrations.kt$Migrations$71 + MagicNumber:Migrations.kt$Migrations$72 + MagicNumber:Migrations.kt$Migrations$75 + MagicNumber:Migrations.kt$Migrations$77 + MagicNumber:Migrations.kt$Migrations$8 + MagicNumber:Migrations.kt$Migrations$81 + MagicNumber:Migrations.kt$Migrations$82 + MagicNumber:Migrations.kt$Migrations$84 + MagicNumber:Migrations.kt$Migrations$85 + MagicNumber:Migrations.kt$Migrations$86 + MagicNumber:Migrations.kt$Migrations$92 + MagicNumber:Migrations.kt$Migrations$96 + MagicNumber:Migrations.kt$Migrations$97 + MagicNumber:Migrations.kt$Migrations$99 MagicNumber:Modifier.kt$0.16f MagicNumber:Modifier.kt$0.22f MagicNumber:MonetColorScheme.kt$MonetColorScheme.Companion$128 @@ -566,19 +1035,29 @@ MagicNumber:MyAnimeList.kt$MyAnimeList$81 MagicNumber:MyAnimeListApi.kt$MyAnimeListApi$64 MagicNumber:MyAnimeListInterceptor.kt$MyAnimeListInterceptor$401 - MagicNumber:MyAnimeListModels.kt$1000 MagicNumber:NetworkHelper.kt$NetworkHelper$30 MagicNumber:Page.kt$Page$100 MagicNumber:PageIndicatorText.kt$235 MagicNumber:PageIndicatorText.kt$45 + MagicNumber:PagePreviewCache.kt$PagePreviewCache$1024 + MagicNumber:PagePreviewSource.kt$PagePreviewInfo$100 + MagicNumber:PagerConfig.kt$PagerConfig$0x202125 MagicNumber:PagerConfig.kt$PagerConfig$3 MagicNumber:PagerConfig.kt$PagerConfig$4 MagicNumber:PagerConfig.kt$PagerConfig$5 + MagicNumber:PagerPageHolder.kt$PagerPageHolder$100 MagicNumber:PagerPageHolder.kt$PagerPageHolder$16 + MagicNumber:PagerPageHolder.kt$PagerPageHolder$48 MagicNumber:PagerPageHolder.kt$PagerPageHolder$90f + MagicNumber:PagerPageHolder.kt$PagerPageHolder$95 + MagicNumber:PagerPageHolder.kt$PagerPageHolder$96 + MagicNumber:PagerPageHolder.kt$PagerPageHolder$97 MagicNumber:PagerViewer.kt$PagerViewer$5 + MagicNumber:PagerViewerAdapter.kt$PagerViewerAdapter$100 MagicNumber:PkceUtil.kt$PkceUtil$50 MagicNumber:PullRefresh.kt$PullToRefreshStateImpl$4 + MagicNumber:PururinDescriptionAdapter.kt$100.0 + MagicNumber:ReaderActivity.kt$ReaderActivity$100 MagicNumber:ReaderActivity.kt$ReaderActivity$230 MagicNumber:ReaderActivity.kt$ReaderActivity$242 MagicNumber:ReaderActivity.kt$ReaderActivity.ReaderConfig$0.01f @@ -596,6 +1075,8 @@ MagicNumber:ReaderPageImageView.kt$ReaderPageImageView$180 MagicNumber:ReaderPageImageView.kt$ReaderPageImageView$250 MagicNumber:ReaderPageImageView.kt$ReaderPageImageView$500 + MagicNumber:ReaderPreferences.kt$ReaderPreferences$10 + MagicNumber:ReaderPreferences.kt$ReaderPreferences$3f MagicNumber:ReaderPreferences.kt$ReaderPreferences$500 MagicNumber:ReaderPreferences.kt$ReaderPreferences.ReaderHideThreshold.HIGH$13 MagicNumber:ReaderPreferences.kt$ReaderPreferences.ReaderHideThreshold.HIGHEST$5 @@ -604,6 +1085,7 @@ MagicNumber:ReaderProgressIndicator.kt$ReaderProgressIndicator$100f MagicNumber:ReaderSettingsDialog.kt$0.5f MagicNumber:ReaderViewModel.kt$ReaderViewModel$0.25 + MagicNumber:ReaderViewModel.kt$ReaderViewModel$9999 MagicNumber:ReadingMode.kt$ReadingMode.CONTINUOUS_VERTICAL$0x00000005 MagicNumber:ReadingMode.kt$ReadingMode.VERTICAL$0x00000003 MagicNumber:ReadingMode.kt$ReadingMode.WEBTOON$0x00000004 @@ -621,6 +1103,7 @@ MagicNumber:Scrollbar.kt$100 MagicNumber:Scrollbar.kt$50 MagicNumber:SecureActivityDelegate.kt$SecureActivityDelegate.Companion$60_000 + MagicNumber:SecurityPreferences.kt$SecurityPreferences$0x7F MagicNumber:SettingsDataScreen.kt$SettingsDataScreen$12 MagicNumber:SettingsDataScreen.kt$SettingsDataScreen$168 MagicNumber:SettingsDataScreen.kt$SettingsDataScreen$24 @@ -630,6 +1113,28 @@ MagicNumber:SettingsDownloadScreen.kt$SettingsDownloadScreen$3 MagicNumber:SettingsDownloadScreen.kt$SettingsDownloadScreen$4 MagicNumber:SettingsDownloadScreen.kt$SettingsDownloadScreen$5 + MagicNumber:SettingsEhScreen.kt$SettingsEhScreen$12 + MagicNumber:SettingsEhScreen.kt$SettingsEhScreen$24 + MagicNumber:SettingsEhScreen.kt$SettingsEhScreen$3 + MagicNumber:SettingsEhScreen.kt$SettingsEhScreen$30 + MagicNumber:SettingsEhScreen.kt$SettingsEhScreen$365 + MagicNumber:SettingsEhScreen.kt$SettingsEhScreen$48 + MagicNumber:SettingsEhScreen.kt$SettingsEhScreen$6 + MagicNumber:SettingsEhScreen.kt$SettingsEhScreen$7 + MagicNumber:SettingsEhScreen.kt$SettingsEhScreen.LanguageDialogState$10 + MagicNumber:SettingsEhScreen.kt$SettingsEhScreen.LanguageDialogState$11 + MagicNumber:SettingsEhScreen.kt$SettingsEhScreen.LanguageDialogState$12 + MagicNumber:SettingsEhScreen.kt$SettingsEhScreen.LanguageDialogState$13 + MagicNumber:SettingsEhScreen.kt$SettingsEhScreen.LanguageDialogState$14 + MagicNumber:SettingsEhScreen.kt$SettingsEhScreen.LanguageDialogState$15 + MagicNumber:SettingsEhScreen.kt$SettingsEhScreen.LanguageDialogState$16 + MagicNumber:SettingsEhScreen.kt$SettingsEhScreen.LanguageDialogState$3 + MagicNumber:SettingsEhScreen.kt$SettingsEhScreen.LanguageDialogState$4 + MagicNumber:SettingsEhScreen.kt$SettingsEhScreen.LanguageDialogState$5 + MagicNumber:SettingsEhScreen.kt$SettingsEhScreen.LanguageDialogState$6 + MagicNumber:SettingsEhScreen.kt$SettingsEhScreen.LanguageDialogState$7 + MagicNumber:SettingsEhScreen.kt$SettingsEhScreen.LanguageDialogState$8 + MagicNumber:SettingsEhScreen.kt$SettingsEhScreen.LanguageDialogState$9 MagicNumber:SettingsItems.kt$0.5f MagicNumber:SettingsItems.kt$1.5f MagicNumber:SettingsLibraryScreen.kt$SettingsLibraryScreen$12 @@ -640,14 +1145,27 @@ MagicNumber:SettingsMainScreen.kt$SettingsMainScreen$0.02f MagicNumber:SettingsMainScreen.kt$SettingsMainScreen$0.05f MagicNumber:SettingsMainScreen.kt$SettingsMainScreen$3 + MagicNumber:SettingsMangadexScreen.kt$SettingsMangadexScreen$5 + MagicNumber:SettingsReaderScreen.kt$SettingsReaderScreen$10 MagicNumber:SettingsReaderScreen.kt$SettingsReaderScreen$100f + MagicNumber:SettingsReaderScreen.kt$SettingsReaderScreen$12 + MagicNumber:SettingsReaderScreen.kt$SettingsReaderScreen$14 + MagicNumber:SettingsReaderScreen.kt$SettingsReaderScreen$16 + MagicNumber:SettingsReaderScreen.kt$SettingsReaderScreen$20 MagicNumber:SettingsReaderScreen.kt$SettingsReaderScreen$250 MagicNumber:SettingsReaderScreen.kt$SettingsReaderScreen$3 + MagicNumber:SettingsReaderScreen.kt$SettingsReaderScreen$4 + MagicNumber:SettingsReaderScreen.kt$SettingsReaderScreen$5 MagicNumber:SettingsReaderScreen.kt$SettingsReaderScreen$500 + MagicNumber:SettingsReaderScreen.kt$SettingsReaderScreen$6 + MagicNumber:SettingsReaderScreen.kt$SettingsReaderScreen$8 MagicNumber:SettingsSearchScreen.kt$10 MagicNumber:Shikimori.kt$Shikimori$40 MagicNumber:ShikimoriModels.kt$1000 MagicNumber:ShikimoriModels.kt$3600 + MagicNumber:SmartSearchEngine.kt$SmartSearchEngine$5 + MagicNumber:SourceFeedScreenModel.kt$SourceFeedScreenModel$10 + MagicNumber:SourcePreferences.kt$SourcePreferences$80 MagicNumber:Surface.kt$100f MagicNumber:Surface.kt$4.5f MagicNumber:Suwayomi.kt$Suwayomi$255 @@ -660,6 +1178,11 @@ MagicNumber:TrackInfoDialogSelector.kt$4L MagicNumber:TrackInfoDialogSelector.kt$5L MagicNumber:TrackInfoDialogSelector.kt$6L + MagicNumber:TrackStatus.kt$TrackStatus.COMPLETED$5 + MagicNumber:TrackStatus.kt$TrackStatus.DROPPED$6 + MagicNumber:TrackStatus.kt$TrackStatus.OTHER$7 + MagicNumber:TrackStatus.kt$TrackStatus.PAUSED$4 + MagicNumber:TrackStatus.kt$TrackStatus.PLAN_TO_READ$3 MagicNumber:TrackerSearchPreviewProvider.kt$TrackerSearchPreviewProvider$10 MagicNumber:TrackerSearchPreviewProvider.kt$TrackerSearchPreviewProvider$100 MagicNumber:TrackerSearchPreviewProvider.kt$TrackerSearchPreviewProvider$1000L @@ -668,6 +1191,8 @@ MagicNumber:TrackerSearchPreviewProvider.kt$TrackerSearchPreviewProvider$365 MagicNumber:TrackerSearchPreviewProvider.kt$TrackerSearchPreviewProvider$40 MagicNumber:TrackingPreferenceWidget.kt$0xFF4CAF50 + MagicNumber:TsuminoDescriptionAdapter.kt$100.0 + MagicNumber:UiPreferences.kt$UiPreferences$4 MagicNumber:UniFileTempFileManager.kt$UniFileTempFileManager$3 MagicNumber:UniFileTempFileManager.kt$UniFileTempFileManager$8192 MagicNumber:UpdatesScreenModel.kt$UpdatesScreenModel$3 @@ -710,33 +1235,249 @@ MagicNumber:WorkManagerExtensions.kt$500 MatchingDeclarationName:ChapterDownloadIndicator.kt$ChapterDownloadAction MatchingDeclarationName:CommonMangaItem.kt$CommonMangaItemDefaults + MatchingDeclarationName:EditMergedSettingsDialog.kt$EditMergedSettingsState : EditMergedMangaItemListener MatchingDeclarationName:EmptyScreen.kt$EmptyScreenAction + MatchingDeclarationName:FeedScreen.kt$FeedItemUI MatchingDeclarationName:LibrarySortMode.kt$LibrarySort : FlagWithMask MatchingDeclarationName:MyAnimeListModels.kt$OAuth MatchingDeclarationName:NetworkStateTracker.kt$NetworkState MatchingDeclarationName:SettingsItems.kt$SettingsItemsPaddings MatchingDeclarationName:ShikimoriModels.kt$OAuth + MatchingDeclarationName:SourceFeedScreen.kt$SourceFeedUI + MatchingDeclarationName:SyncFavoritesProgressDialog.kt$SyncFavoritesProgressProperties MatchingDeclarationName:TabbedDialog.kt$TabbedDialogPaddings + MatchingDeclarationName:__Assets.kt$AssetsGroup + MatchingDeclarationName:__EhAssets.kt$EhAssets + MatchingDeclarationName:__Ehassets.kt$EhassetsGroup + MatchingDeclarationName:__Exh.kt$ExhGroup + MaxLineLength:AnnotatedString.kt$Typeface.BOLD_ITALIC -> addStyle(SpanStyle(fontWeight = FontWeight.Bold, fontStyle = FontStyle.Italic), start, end) + MaxLineLength:ApiMangaParser.kt$ApiMangaParser$/*private fun isOneShot(chapter: ChapterSerializer, finalChapterNumber: String): Boolean { return chapter.title.equals("oneshot", true) || ((chapter.chapter.isNullOrEmpty() || chapter.chapter == "0") && MdUtil.validOneShotFinalChapters.contains(finalChapterNumber)) }*/ + MaxLineLength:ApiMangaParser.kt$ApiMangaParser$?. + MaxLineLength:AzukiHandler.kt$AzukiHandler$val url = element.jsonObject["image_wm"]!!.jsonObject["webp"]!!.jsonArray[1].jsonObject["url"]!!.jsonPrimitive.content + MaxLineLength:BackupTracking.kt$_: Long, _: Long, syncId: Long, mediaId: Long, libraryId: Long?, title: String, lastChapterRead: Double, totalChapters: Long, status: Long, score: Double, remoteUrl: String, startDate: Long, finishDate: Long + MaxLineLength:BangumiInterceptor.kt$BangumiInterceptor$"jobobby04/TachiyomiSY/v${BuildConfig.VERSION_NAME} (Android) (http://github.com/jobobby04/tachiyomisy)" + MaxLineLength:BatchAddScreen.kt$BatchAddScreen$Text(text = stringResource(SYMR.strings.eh_batch_add_adding_galleries), style = MaterialTheme.typography.titleLarge) + MaxLineLength:BatchAddScreen.kt$BatchAddScreen$Text(text = stringResource(SYMR.strings.eh_batch_add_title), style = MaterialTheme.typography.titleLarge) + MaxLineLength:BiometricTimesScreen.kt$BiometricTimesScreen$. + MaxLineLength:BiometricTimesScreen.kt$BiometricTimesScreen$text = stringResource(SYMR.strings.delete_time_range_confirmation, dialog.timeRange.formattedString) + MaxLineLength:ChapterLoader.kt$ChapterLoader$isDownloaded -> DownloadPageLoader(chapter, manga, source, downloadManager, downloadProvider, tempFileManager) + MaxLineLength:ComikeyHandler.kt$ComikeyHandler$private val urlForbidden = "https://fakeimg.pl/1800x2252/FFFFFF/000000/?font_size=120&text=This%20chapter%20is%20not%20available%20for%20free.%0A%0AIf%20you%20have%20purchased%20this%20chapter%2C%20please%20%0Aopen%20the%20website%20in%20web%20view%20and%20log%20in." MaxLineLength:CoroutinesExtensions.kt$* + MaxLineLength:DataSaver.kt$BandwidthHeroDataSaver$imageUrl.contains(".jpeg", true) || imageUrl.contains(".jpg", true) -> if (ignoreJpg) imageUrl else getUrl(imageUrl) + MaxLineLength:DataSaver.kt$WsrvNlDataSaver$imageUrl.contains(".jpeg", true) || imageUrl.contains(".jpg", true) -> if (ignoreJpg) imageUrl else getUrl(imageUrl) + MaxLineLength:DataSaver.kt$WsrvNlDataSaver$return + MaxLineLength:DebugToggles.kt$DebugToggles.ENABLE_EXH_ROOT_REDIRECT$// Redirect to master version of gallery when encountering a gallery that has a parent/child that is already in the library + MaxLineLength:DownloadPageLoader.kt$DownloadPageLoader$val chapterPath = downloadProvider.findChapterDir(dbChapter.name, dbChapter.scanlator, /* SY --> */ manga.ogTitle /* SY <-- */, source) MaxLineLength:Downloader.kt$Downloader$. - MaxLineLength:LibraryUpdateJob.kt$LibraryUpdateJob$// failedUpdates will already have the source, don't need to copy it into the message + MaxLineLength:Downloader.kt$Downloader$// using ImageUtils isImage and findImageType functions causes IO errors when deleting files to set Exif Metadata + MaxLineLength:EHConfigurator.kt$EHConfigurator$throw IllegalStateException(context.stringResource(SYMR.strings.eh_settings_out_of_slots_error, source.name)) + MaxLineLength:EHHathPerksResponse.kt$EHHathPerksResponse$"EHHathPerksResponse(moreThumbs=$moreThumbs, thumbsUp=$thumbsUp, allThumbs=$allThumbs, pagingEnlargementI=$pagingEnlargementI, pagingEnlargementII=$pagingEnlargementII, pagingEnlargementIII=$pagingEnlargementIII)" + MaxLineLength:EHentaiDescriptionAdapter.kt$binding.rating.text = (ratingFloat ?: 0F).toString() + " - " + MetadataUIUtil.getRatingString(context, ratingFloat?.times(2)) + MaxLineLength:EHentaiDescriptionAdapter.kt$binding.visible.text = context.stringResource(SYMR.strings.is_visible, meta.visible ?: context.stringResource(MR.strings.unknown)) + MaxLineLength:EHentaiUpdateWorker.kt$EHentaiUpdateWorker$"No chapters found for gallery (manga.id: %s, meta.gId: %s, meta.gToken: %s, failures-so-far: %s)!" + MaxLineLength:EHentaiUpdateWorker.kt$EHentaiUpdateWorker$"Updating gallery (index: %s, manga.id: %s, meta.gId: %s, meta.gToken: %s, failures-so-far: %s, modifiedThisIteration.size: %s)..." + MaxLineLength:EHentaiUpdateWorker.kt$EHentaiUpdateWorker$?: + MaxLineLength:EHentaiUpdateWorker.kt$EHentaiUpdateWorker$if + MaxLineLength:EditMergedMangaItem.kt$EditMergedMangaItem$class + MaxLineLength:EditMergedMangaItem.kt$EditMergedMangaItem$override + MaxLineLength:EditMergedSettingsDialog.kt$EditMergedSettingsState$val isPriorityOrder = mergeReference?.let { it.chapterSortMode == MergedMangaReference.CHAPTER_SORT_PRIORITY } ?: false + MaxLineLength:EditMergedSettingsDialog.kt$state.onViewCreated(factoryContext, binding, mergedData.manga.values.toList(), mergedData.references) + MaxLineLength:EditMergedSettingsHeaderAdapter.kt$EditMergedSettingsHeaderAdapter$class + MaxLineLength:EditMergedSettingsHeaderAdapter.kt$EditMergedSettingsHeaderAdapter$fun canMove() + MaxLineLength:EnhancedFilePrinter.kt$EnhancedFilePrinter$require(!(newFileName == null || newFileName.trim { it <= ' ' }.isEmpty())) { "File name should not be empty." } + MaxLineLength:ExtensionInstaller.kt$ExtensionInstaller$if + MaxLineLength:ExtensionManager.kt$ExtensionManager$if (it) this@ExtensionManager.xLogD("Removing blacklisted extension: (name: %s, pkgName: %s)!", extension.name, extension.pkgName) + MaxLineLength:ExtensionManager.kt$ExtensionManager$private + MaxLineLength:ExtensionsScreenModel.kt$ExtensionsScreenModel$ExtensionUiModel.Header.Text(LocaleHelper.getSourceDisplayName(lang, context)) to exts.map(extensionMapper(downloads)) + MaxLineLength:FavoritesSyncHelper.kt$FavoritesSyncHelper$SYMR.strings.favorites_sync_failed_to_add_to_local_unknown_type + MaxLineLength:FavoritesSyncHelper.kt$FavoritesSyncHelper$context.stringResource(SYMR.strings.favorites_sync_remove_from_local, index + 1, changeSet.removed.size) + MaxLineLength:FavoritesSyncHelper.kt$FavoritesSyncHelper$logger.w(context.stringResource(SYMR.strings.favorites_sync_gallery_multiple_categories_error, manga.id)) + MaxLineLength:FavoritesSyncHelper.kt$FavoritesSyncHelper$message = context.stringResource(SYMR.strings.favorites_sync_add_to_local, index + 1, changeSet.added.size) + MaxLineLength:FavoritesSyncHelper.kt$FavoritesSyncHelper$message = context.stringResource(SYMR.strings.favorites_sync_adding_to_remote, index + 1, changeSet.added.size) + MaxLineLength:FavoritesSyncHelper.kt$FavoritesSyncHelper$status.value = FavoritesSyncStatus.Error(context.stringResource(SYMR.strings.favorites_sync_failed_to_featch)) + MaxLineLength:FavoritesSyncHelper.kt$FavoritesSyncHelper$status.value = FavoritesSyncStatus.Processing(context.stringResource(SYMR.strings.favorites_sync_cleaning_up)) + MaxLineLength:FavoritesSyncHelper.kt$FavoritesSyncHelper$status.value = FavoritesSyncStatus.Processing(context.stringResource(SYMR.strings.favorites_sync_downloading)) + MaxLineLength:FavoritesSyncHelper.kt$FavoritesSyncHelper$status.value = FavoritesSyncStatus.Processing(context.stringResource(SYMR.strings.favorites_sync_verifying_library)) + MaxLineLength:FeedScreenModel.kt$FeedScreenModel$private suspend + MaxLineLength:GalleryAdder.kt$GalleryAddEvent.Fail.UnknownSource$override val logMessage = context.stringResource(SYMR.strings.batch_add_unknown_source_log_message, galleryUrl) + MaxLineLength:GalleryAdder.kt$GalleryAddEvent.Fail.UnknownType$override val logMessage = context.stringResource(SYMR.strings.batch_add_unknown_type_log_message, galleryUrl) + MaxLineLength:GalleryAdder.kt$GalleryAdder$GalleryAddEvent.Fail.Error(url, context.stringResource(SYMR.strings.gallery_adder_could_not_identify_chapter, url)) + MaxLineLength:GalleryAdder.kt$GalleryAdder$logger.d(context.stringResource(SYMR.strings.gallery_adder_importing_gallery, url, fav.toString(), forceSource?.toString().orEmpty())) + MaxLineLength:GalleryAdder.kt$GalleryAdder$return GalleryAddEvent.Fail.Error(url, context.stringResource(SYMR.strings.gallery_adder_chapter_fetch_error, url)) + MaxLineLength:HBrowseDescriptionAdapter.kt$binding.pages.text = context.pluralStringResource(SYMR.plurals.num_pages, meta.length ?: 0, meta.length ?: 0) + MaxLineLength:LibraryScreenModel.kt$LibraryScreenModel$!mangaTag.name.contains(searchedTag, true) || !mangaTag.namespace.equals(queryComponent.namespace, true) + MaxLineLength:LibraryScreenModel.kt$LibraryScreenModel$(it.namespace.equals(queryComponent.namespace, true) && tag?.run { it.name.contains(tag.asQuery(), true) } == true) + MaxLineLength:LibraryScreenModel.kt$LibraryScreenModel$(list.getOrNull(0)?.toIntOrNull() ?: return@mapNotNull null) to (list.getOrNull(1) ?: return@mapNotNull null) + MaxLineLength:LibraryScreenModel.kt$LibraryScreenModel$(loggedInTrackServices.isEmpty() || tracks == null || !filterTracks(query, tracks, context)) + MaxLineLength:LibraryScreenModel.kt$LibraryScreenModel$(loggedInTrackServices.isNotEmpty() && tracks != null && filterTracks(query, tracks, context)) + MaxLineLength:LibraryScreenModel.kt$LibraryScreenModel$SManga.PUBLISHING_FINISHED.toLong() -> context.stringResource(MR.strings.publishing_finished) + MaxLineLength:LibraryScreenModel.kt$LibraryScreenModel$searchTags == null || (queryComponent.namespace.isBlank() && searchedTag.isNullOrBlank()) + MaxLineLength:LibraryScreenModel.kt$LibraryScreenModel$suspend + MaxLineLength:LibraryScreenModel.kt$LibraryScreenModel$val + MaxLineLength:LibraryScreenModel.kt$LibraryScreenModel$val manga1IndexOfTag = listOfTags.indexOfFirst { i1.libraryManga.manga.genre?.contains(it) ?: false } + MaxLineLength:LibraryScreenModel.kt$LibraryScreenModel$val manga2IndexOfTag = listOfTags.indexOfFirst { i2.libraryManga.manga.genre?.contains(it) ?: false } + MaxLineLength:LibraryScreenModel.kt$LibraryScreenModel$val mergedSource = sources.firstOrNull { mergedManga.source == it.id } as? HttpSource ?: return@merge + MaxLineLength:LibraryScreenModel.kt$LibraryScreenModel$val sources = mergedMangas.distinctBy { it.source }.map { sourceManager.getOrStub(it.source) } + MaxLineLength:LibraryTab.kt$LibraryTab$onClickRefresh(state.categories[screenModel.activeCategoryIndex.coerceAtMost(state.categories.lastIndex)]) + MaxLineLength:LibraryUpdateJob.kt$LibraryUpdateJob$if + MaxLineLength:LibraryUpdateJob.kt$LibraryUpdateJob$readChapters.any { it.chapterNumber == chapter.chapterNumber } + MaxLineLength:LibraryUpdateJob.kt$LibraryUpdateJob$val readChapters = getChaptersByMangaId.await(manga.id).filter { it.read } + MaxLineLength:LibraryUpdateNotifier.kt$LibraryUpdateNotifier.Companion$const val HELP_WARNING_URL = "https://mihon.app/docs/faq/library#why-am-i-warned-about-large-bulk-updates-and-downloads" + MaxLineLength:LocalFavoritesStorage.kt$LocalFavoritesStorage$(otherGid != null && otherToken != null && other.otherGid != null && other.otherToken != null && otherGid == other.otherGid && otherToken == other.otherToken) + MaxLineLength:LocalSource.kt$LocalSource$copiedFile.getZipInputStream(COMIC_INFO_FILE)?.let { setMangaDetailsFromComicInfoFile(it, manga) } MaxLineLength:MainActivity.kt$MainActivity$// or the Google-specific search intent (triggered by saying or typing "search *query* on *Tachiyomi*" in Google Search/Google Assistant) + MaxLineLength:MangaChapterListItem.kt$if (readProgress != null || scanlator != null/* SY --> */ || sourceName != null/* SY <-- */) DotSeparatorText() + MaxLineLength:MangaDex.kt$MangaDex$override suspend + MaxLineLength:MangaDexDescriptionAdapter.kt$binding.rating.text = (round((ratingFloat ?: 0F) * 100.0) / 100.0).toString() + " - " + getRatingString(context, ratingFloat) + MaxLineLength:MangaDexFollowsScreenModel.kt$MangaDexFollowsScreenModel$override + MaxLineLength:MangaDexSimilarScreenModel.kt$MangaDexSimilarScreenModel$override + MaxLineLength:MangaScreen.kt$MangaScreen$onMergeWithAnotherClicked = { mergeWithAnother(navigator, context, successState.manga, screenModel::smartSearchMerge) } + MaxLineLength:MangaScreen.kt$MangaScreen$onRecommendClicked = { openRecommends(context, navigator, screenModel.source?.getMainSource(), successState.manga) } + MaxLineLength:MangaScreenModel.kt$MangaScreenModel$/* SY --> */ if (isMergedSource) it.manga.id in mergedIds else /* SY <-- */ it.manga.id == successState?.manga?.id + MaxLineLength:MangaScreenModel.kt$MangaScreenModel$alwaysShowReadingProgress = readerPreferences.preserveReadingPosition().get() && manga.isEhBasedManga() + MaxLineLength:MangaScreenModel.kt$MangaScreenModel$if + MaxLineLength:MangaScreenModel.kt$MangaScreenModel$if (chapters.isEmpty() || !manga.shouldDownloadNewChapters(categories, downloadPreferences) || manga.isEhBasedManga()) return@launchNonCancellable + MaxLineLength:MangaScreenModel.kt$MangaScreenModel$return TrackItem(getTracks.await(mangaId).first { it.trackerId == trackerManager.mdList.id }, trackerManager.mdList) + MaxLineLength:MangaScreenModel.kt$MangaScreenModel$val + MaxLineLength:MangaScreenModel.kt$MangaScreenModel$val (acceptedChain) = updateHelper.findAcceptedRootAndDiscardOthers(manga.source, chapters) + MaxLineLength:MangaScreenModel.kt$MangaScreenModel$val isUpdateIntervalEnabled = LibraryPreferences.MANGA_OUTSIDE_RELEASE_PERIOD in libraryPreferences.autoUpdateMangaRestrictions().get() + MaxLineLength:MangaScreenModel.kt$MangaScreenModel.State.Success$it.track != null && ((it.tracker is MdList && it.track.status != FollowStatus.UNFOLLOWED.long) || it.tracker !is MdList) + MaxLineLength:MdUtil.kt$MdUtil.Companion$fun + MaxLineLength:MemAutoFlushingLookupTable.kt$MemAutoFlushingLookupTable$* + MaxLineLength:MetadataViewScreen.kt$MetadataViewScreen$contentPadding = paddingValues + WindowInsets.navigationBars.asPaddingValues() + topSmallPaddingValues + MaxLineLength:MigrationListScreen.kt$MigrationListScreen$val mangaId = (items.orEmpty().firstOrNull()?.searchResult?.value as? MigratingManga.SearchResult.Result)?.id + MaxLineLength:MigrationListScreenModel.kt$MigrationListScreenModel$if + MaxLineLength:MigrationListScreenModel.kt$MigrationListScreenModel$manga.progress.value = validSources.size to processedSources.incrementAndGet() + MaxLineLength:MigrationListScreenModel.kt$MigrationListScreenModel$source.getChapterList(localManga.toSManga(), throttleManager::throttle) + MaxLineLength:MigrationListScreenModel.kt$MigrationListScreenModel$val prevChapter = prevMangaChapters.find { it.isRecognizedNumber && it.chapterNumber == chapter.chapterNumber } + MaxLineLength:MigrationSourceItem.kt$MigrationSourceItem$class + MaxLineLength:MigrationSourceItem.kt$MigrationSourceItem$override + MaxLineLength:NotificationExtensions.kt$if + MaxLineLength:PageHandler.kt$PageHandler$suspend + MaxLineLength:PagePreviewScreen.kt$(state.pageCount != null && state.pageCount > 1 /* TODO support unknown pageCount || state.hasNextPage*/) + MaxLineLength:PagerConfig.kt$PagerConfig$var + MaxLineLength:PagerPageHolder.kt$PagerPageHolder$val + MaxLineLength:PagerViewer.kt$PagerViewer$val forceTransition = config.alwaysShowChapterTransition || adapter.joinedItems.getOrNull(pager.currentItem)?.first is ChapterTransition MaxLineLength:PreferenceScreen.kt$* + MaxLineLength:PururinDescriptionAdapter.kt$binding.genre.text + MaxLineLength:PururinDescriptionAdapter.kt$binding.rating.text = (round((ratingFloat ?: 0F) * 100.0) / 100.0).toString() + " - " + MetadataUIUtil.getRatingString(context, ratingFloat?.times(2)) + MaxLineLength:ReaderActivity.kt$ReaderActivity$if ((resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_LTR) xor invertDoublePage) "${page.number}-${page.number + 1}" else "${page.number + 1}-${page.number}" + MaxLineLength:ReaderActivity.kt$ReaderActivity$stringResource(SYMR.strings.share_pages_info, manga.title, chapter.name, if (resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_LTR) "${page.number}-${page.number + 1}" else "${page.number + 1}-${page.number}") + MaxLineLength:ReaderActivity.kt$ReaderActivity$val currentPage = (((viewer as? PagerViewer)?.currentPage ?: (viewer as? WebtoonViewer)?.currentPage) as? ReaderPage)?.index + MaxLineLength:ReaderActivity.kt$ReaderActivity$val showVerticalSeekbar = !forceHorizontalSeekbar && (verticalSeekbarLandscape || verticalSeekbarHorizontal) && viewerIsVertical + MaxLineLength:ReaderActivity.kt$ReaderActivity$val verticalSeekbarLandscape = configuration.orientation == Configuration.ORIENTATION_LANDSCAPE && landscapeVerticalSeekbar + MaxLineLength:ReaderActivity.kt$ReaderActivity$windowInsetsController.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE + MaxLineLength:ReaderActivity.kt$ReaderActivity.Companion$fun + MaxLineLength:ReaderActivity.kt$ReaderActivity.ReaderConfig$PagerConfig.PageLayout.AUTOMATIC -> resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE + MaxLineLength:ReaderAppBars.kt$/* SY --> actions = { AppBarActions( listOfNotNull( AppBar.Action( title = stringResource( if (bookmarked) MR.strings.action_remove_bookmark else MR.strings.action_bookmark ), icon = if (bookmarked) Icons.Outlined.Bookmark else Icons.Outlined.BookmarkBorder, onClick = onToggleBookmarked, ), onOpenInWebView?.let { AppBar.OverflowAction( title = stringResource(MR.strings.action_open_in_web_view), onClick = it, ) }, onShare?.let { AppBar.OverflowAction( title = stringResource(MR.strings.action_share), onClick = it, ) }, ), ) }, SY <-- */ + MaxLineLength:ReaderPageImageView.kt$ReaderPageImageView$if + MaxLineLength:ReaderPreferences.kt$ReaderPreferences$fun readerBottomButtons() + MaxLineLength:ReaderViewModel.kt$ReaderViewModel$getMergedChaptersByMangaId.await(manga.id, applyScanlatorFilter = true) MaxLineLength:Scaffold.kt$* + MaxLineLength:SecureActivityDelegate.kt$SecureActivityDelegate.Companion$requireUnlock + MaxLineLength:ShikimoriApi.kt$ShikimoriApi$track.library_id = it["id"]!!.jsonPrimitive.long // save id of the entry for possible future delete request + MaxLineLength:SourceFeedScreen.kt$SourceFeedScreen$fun + MaxLineLength:SourcesScreenModel.kt$SourcesScreenModel$SourceUiModel.Header(it.key.removePrefix(CATEGORY_KEY_PREFIX), it.value.firstOrNull()?.category != null) + MaxLineLength:SourcesScreenModel.kt$SourcesScreenModel$private MaxLineLength:SpecificHostRateLimitInterceptor.kt$* + MaxLineLength:TsuminoDescriptionAdapter.kt$binding.pages.text = context.pluralStringResource(SYMR.plurals.num_pages, meta.length ?: 0, meta.length ?: 0) + MaxLineLength:TsuminoDescriptionAdapter.kt$binding.rating.text = (round((meta.averageRating ?: 0F) * 100.0) / 100.0).toString() + " - " + MetadataUIUtil.getRatingString(context, meta.averageRating?.times(2)) MaxLineLength:WebViewUtil.kt$WebViewUtil$* + MaxLineLength:WebtoonPageHolder.kt$WebtoonPageHolder$cropBorders = (viewer.config.imageCropBorders && viewer.isContinuous) || (viewer.config.continuousCropBorders && !viewer.isContinuous) + MaximumLineLength:AnnotatedString.kt$ + MaximumLineLength:ApiMangaParser.kt$ApiMangaParser$ + MaximumLineLength:AzukiHandler.kt$AzukiHandler$ + MaximumLineLength:BackupTracking.kt$ + MaximumLineLength:BangumiInterceptor.kt$BangumiInterceptor$ + MaximumLineLength:BatchAddScreen.kt$BatchAddScreen$ + MaximumLineLength:BiometricTimesScreen.kt$BiometricTimesScreen$ + MaximumLineLength:ChapterLoader.kt$ChapterLoader$ + MaximumLineLength:ComikeyHandler.kt$ComikeyHandler$ + MaximumLineLength:DataSaver.kt$BandwidthHeroDataSaver$ + MaximumLineLength:DataSaver.kt$WsrvNlDataSaver$ + MaximumLineLength:DownloadPageLoader.kt$DownloadPageLoader$ + MaximumLineLength:EHConfigurator.kt$EHConfigurator$ + MaximumLineLength:EHHathPerksResponse.kt$EHHathPerksResponse$ + MaximumLineLength:EHentaiDescriptionAdapter.kt$ + MaximumLineLength:EHentaiUpdateWorker.kt$EHentaiUpdateWorker$ + MaximumLineLength:EditMergedMangaItem.kt$EditMergedMangaItem$ + MaximumLineLength:EditMergedMangaItem.kt$EditMergedMangaItem$class + MaximumLineLength:EditMergedSettingsDialog.kt$ + MaximumLineLength:EditMergedSettingsDialog.kt$EditMergedSettingsState$ + MaximumLineLength:EditMergedSettingsHeaderAdapter.kt$EditMergedSettingsHeaderAdapter$ + MaximumLineLength:EditMergedSettingsHeaderAdapter.kt$EditMergedSettingsHeaderAdapter$class + MaximumLineLength:EnhancedFilePrinter.kt$EnhancedFilePrinter$ + MaximumLineLength:ExtensionInstaller.kt$ExtensionInstaller$ + MaximumLineLength:ExtensionManager.kt$ExtensionManager$ + MaximumLineLength:ExtensionsScreenModel.kt$ExtensionsScreenModel$ + MaximumLineLength:FavoritesSyncHelper.kt$FavoritesSyncHelper$ + MaximumLineLength:FeedScreenModel.kt$FeedScreenModel$ + MaximumLineLength:GalleryAdder.kt$GalleryAddEvent.Fail.UnknownSource$ + MaximumLineLength:GalleryAdder.kt$GalleryAddEvent.Fail.UnknownType$ + MaximumLineLength:GalleryAdder.kt$GalleryAdder$ + MaximumLineLength:HBrowseDescriptionAdapter.kt$ + MaximumLineLength:LibraryScreenModel.kt$LibraryScreenModel$ + MaximumLineLength:LibraryTab.kt$LibraryTab$ + MaximumLineLength:LibraryUpdateJob.kt$LibraryUpdateJob$ + MaximumLineLength:LibraryUpdateNotifier.kt$LibraryUpdateNotifier.Companion$ + MaximumLineLength:LocalFavoritesStorage.kt$LocalFavoritesStorage$ + MaximumLineLength:LocalSource.kt$LocalSource$ + MaximumLineLength:MangaChapterListItem.kt$ + MaximumLineLength:MangaDex.kt$MangaDex$ + MaximumLineLength:MangaDexDescriptionAdapter.kt$ + MaximumLineLength:MangaDexFollowsScreenModel.kt$MangaDexFollowsScreenModel$ + MaximumLineLength:MangaDexSimilarScreenModel.kt$MangaDexSimilarScreenModel$ + MaximumLineLength:MangaScreen.kt$MangaScreen$ + MaximumLineLength:MangaScreenModel.kt$MangaScreenModel$ + MaximumLineLength:MangaScreenModel.kt$MangaScreenModel.State.Success$ + MaximumLineLength:MdUtil.kt$MdUtil.Companion$ + MaximumLineLength:MetadataViewScreen.kt$MetadataViewScreen$ + MaximumLineLength:MigrationListScreen.kt$MigrationListScreen$ + MaximumLineLength:MigrationListScreenModel.kt$MigrationListScreenModel$ + MaximumLineLength:MigrationSourceItem.kt$MigrationSourceItem$ + MaximumLineLength:MigrationSourceItem.kt$MigrationSourceItem$class + MaximumLineLength:NotificationExtensions.kt$ + MaximumLineLength:PageHandler.kt$PageHandler$ + MaximumLineLength:PagePreviewScreen.kt$ + MaximumLineLength:PagerConfig.kt$PagerConfig$ + MaximumLineLength:PagerPageHolder.kt$PagerPageHolder$ + MaximumLineLength:PagerViewer.kt$PagerViewer$ + MaximumLineLength:PururinDescriptionAdapter.kt$ + MaximumLineLength:ReaderActivity.kt$ReaderActivity$ + MaximumLineLength:ReaderActivity.kt$ReaderActivity.Companion$ + MaximumLineLength:ReaderActivity.kt$ReaderActivity.ReaderConfig$ + MaximumLineLength:ReaderPageImageView.kt$ReaderPageImageView$ + MaximumLineLength:ReaderPreferences.kt$ReaderPreferences$ + MaximumLineLength:ReaderViewModel.kt$ReaderViewModel$ + MaximumLineLength:SecureActivityDelegate.kt$SecureActivityDelegate.Companion$ + MaximumLineLength:ShikimoriApi.kt$ShikimoriApi$ + MaximumLineLength:SourceFeedScreen.kt$SourceFeedScreen$ + MaximumLineLength:SourcesScreenModel.kt$SourcesScreenModel$ + MaximumLineLength:TsuminoDescriptionAdapter.kt$ + MaximumLineLength:WebtoonPageHolder.kt$WebtoonPageHolder$ + MayBeConst:TsuminoSearchMetadata.kt$TsuminoSearchMetadata.Companion$val BASE_URL = "https://www.tsumino.com" MemberNameEqualsClassName:ExtensionInstaller.kt$ExtensionInstaller$private val extensionInstaller = Injekt.get<BasePreferences>().extensionInstaller() ModifierClickableOrder:ScanlatorFilterDialog.kt$clickable { if (isExcluded) { mutableExcludedScanlators.remove(scanlator) } else { mutableExcludedScanlators.add(scanlator) } } ModifierMissing:AppBar.kt$AppBarActions ModifierMissing:AppThemePreferenceWidget.kt$AppThemePreviewItem + ModifierMissing:AutoCompleteItem.kt$AutoCompleteItem + ModifierMissing:AutoCompleteItem.kt$AutoCompleteTextField + ModifierMissing:BiometricTimesContent.kt$BiometricTimesContent + ModifierMissing:BiometricTimesScreen.kt$BiometricTimesScreen ModifierMissing:BottomReaderBar.kt$BottomReaderBar ModifierMissing:BrowseSourceComfortableGrid.kt$BrowseSourceComfortableGrid ModifierMissing:BrowseSourceCompactGrid.kt$BrowseSourceCompactGrid + ModifierMissing:BrowseSourceEHentaiList.kt$BrowseSourceEHentaiList + ModifierMissing:BrowseSourceEHentaiList.kt$BrowseSourceEHentaiListItem ModifierMissing:BrowseSourceList.kt$BrowseSourceList ModifierMissing:BrowseSourceScreen.kt$BrowseSourceContent + ModifierMissing:BrowseSourceSimpleToolbar.kt$BrowseSourceSimpleToolbar ModifierMissing:BrowseSourceToolbar.kt$BrowseSourceToolbar + ModifierMissing:BrowseTabWrapper.kt$BrowseTabWrapper ModifierMissing:CategoryScreen.kt$CategoryScreen + ModifierMissing:ChapterListDialog.kt$ChapterListDialog ModifierMissing:ChapterNavigator.kt$ChapterNavigator + ModifierMissing:ChapterNavigator.kt$ChapterNavigatorVert ModifierMissing:ChapterSettingsDialog.kt$ChapterSettingsDialog ModifierMissing:ChapterSettingsDialog.kt$ScanlatorFilterItem ModifierMissing:CollapsibleBox.kt$CollapsibleBox @@ -744,16 +1485,22 @@ ModifierMissing:CommonMangaItem.kt$MangaCompactGridItem ModifierMissing:CommonMangaItem.kt$MangaListItem ModifierMissing:CrashScreen.kt$CrashScreen + ModifierMissing:EHDebugModeOverlay.kt$DebugModeOverlay + ModifierMissing:EHentaiDescriptionAdapter.kt$EHentaiDescription + ModifierMissing:EhLoginWebViewScreen.kt$EhLoginWebViewScreen + ModifierMissing:EightMusesDescriptionAdapter.kt$EightMusesDescription ModifierMissing:ExtensionDetailsScreen.kt$ExtensionDetailsScreen ModifierMissing:ExtensionFilterScreen.kt$ExtensionFilterScreen ModifierMissing:ExtensionReposScreen.kt$ExtensionReposScreen ModifierMissing:ExtensionsScreen.kt$ExtensionScreen + ModifierMissing:FeedScreen.kt$FeedScreen + ModifierMissing:FeedScreen.kt$RadioSelector ModifierMissing:GlobalSearchCardRow.kt$GlobalSearchCardRow ModifierMissing:GlobalSearchResultItems.kt$GlobalSearchErrorResultItem ModifierMissing:GlobalSearchResultItems.kt$GlobalSearchLoadingResultItem - ModifierMissing:GlobalSearchResultItems.kt$GlobalSearchResultItem ModifierMissing:GlobalSearchScreen.kt$GlobalSearchScreen ModifierMissing:GlobalSearchToolbar.kt$GlobalSearchToolbar + ModifierMissing:HBrowseDescriptionAdapter.kt$HBrowseDescription ModifierMissing:HistoryScreen.kt$HistoryScreen ModifierMissing:HomeScreen.kt$HomeScreen$NavigationRailItem ModifierMissing:InfoScreen.kt$InfoScreen @@ -762,21 +1509,37 @@ ModifierMissing:LibrarySettingsDialog.kt$LibrarySettingsDialog ModifierMissing:LogoHeader.kt$LogoHeader ModifierMissing:MangaCoverDialog.kt$MangaCoverDialog + ModifierMissing:MangaDexDescriptionAdapter.kt$MangaDexDescription + ModifierMissing:MangaDexFilterHeader.kt$MangaDexFilterHeader + ModifierMissing:MangaInfoButtons.kt$MangaInfoButtons ModifierMissing:MangaScreen.kt$MangaScreenLargeImpl ModifierMissing:MigrateMangaScreen.kt$MigrateMangaScreen ModifierMissing:MigrateSearchScreen.kt$MigrateSearchScreen ModifierMissing:MigrateSourceScreen.kt$MigrateSourceScreen + ModifierMissing:MigrationBottomSheetDialog.kt$MigrationBottomSheetDialog + ModifierMissing:MigrationListScreen.kt$MigrationListScreen ModifierMissing:ModeSelectionDialog.kt$ModeSelectionDialog ModifierMissing:MoreScreen.kt$MoreScreen + ModifierMissing:NHentaiDescriptionAdapter.kt$NHentaiDescription + ModifierMissing:NamespaceTags.kt$NamespaceTags ModifierMissing:NewUpdateScreen.kt$NewUpdateScreen ModifierMissing:OnboardingScreen.kt$OnboardingScreen ModifierMissing:PageIndicatorText.kt$PageIndicatorText + ModifierMissing:PagePreviewScreen.kt$PagePreviewScreen + ModifierMissing:PagePreviews.kt$PagePreviews ModifierMissing:PreferenceGroupHeader.kt$PreferenceGroupHeader ModifierMissing:PreferenceScaffold.kt$PreferenceScaffold + ModifierMissing:PururinDescriptionAdapter.kt$PururinDescription ModifierMissing:ReaderAppBars.kt$ReaderAppBars ModifierMissing:ReaderPageActionsDialog.kt$ReaderPageActionsDialog ModifierMissing:ReaderSettingsDialog.kt$ReaderSettingsDialog + ModifierMissing:SavedSearchItem.kt$SavedSearchItem ModifierMissing:SectionCard.kt$SectionCard + ModifierMissing:SettingsDebugScreen.kt$SettingsDebugScreen$FunctionList + ModifierMissing:SettingsEhScreen.kt$SettingsEhScreen$FrontPageCategoriesDialogRow + ModifierMissing:SettingsEhScreen.kt$SettingsEhScreen$LanguageDialogRow + ModifierMissing:SettingsEhScreen.kt$SettingsEhScreen$LanguageDialogRowCheckbox + ModifierMissing:SettingsEhScreen.kt$SettingsEhScreen$UpdaterStatisticsLoadingDialog ModifierMissing:SettingsItems.kt$CheckboxItem ModifierMissing:SettingsItems.kt$HeadingItem ModifierMissing:SettingsItems.kt$IconItem @@ -789,6 +1552,12 @@ ModifierMissing:SettingsItems.kt$TextItem ModifierMissing:SettingsItems.kt$TriStateItem ModifierMissing:SettingsMainScreen.kt$SettingsMainScreen$Content + ModifierMissing:SortTagContent.kt$SortTagContent + ModifierMissing:SortTagScreen.kt$SortTagScreen + ModifierMissing:SourceCategoryContent.kt$SourceCategoryContent + ModifierMissing:SourceCategoryScreen.kt$SourceCategoryScreen + ModifierMissing:SourceFeedScreen.kt$SourceFeedList + ModifierMissing:SourceFeedScreen.kt$SourceFeedScreen ModifierMissing:SourceFilterDialog.kt$SourceFilterDialog ModifierMissing:SourcesFilterScreen.kt$SourcesFilterScreen ModifierMissing:SourcesScreen.kt$SourcesScreen @@ -802,6 +1571,7 @@ ModifierMissing:TrackInfoDialogSelector.kt$TrackStatusSelector ModifierMissing:TrackLogoIcon.kt$TrackLogoIcon ModifierMissing:TrackerSearch.kt$TrackerSearch + ModifierMissing:TsuminoDescriptionAdapter.kt$TsuminoDescription ModifierMissing:UpdatesScreen.kt$UpdateScreen ModifierMissing:WebViewScreenContent.kt$WebViewScreenContent ModifierMissing:WheelPicker.kt$WheelPickerDefaults$Background @@ -809,7 +1579,23 @@ ModifierNotUsedAtRoot:DropdownMenu.kt$modifier = modifier ModifierNotUsedAtRoot:MangaChapterListItem.kt$modifier = modifier .selectedBackground(selected) .combinedClickable( onClick = onClick, onLongClick = onLongClick, ) .padding(start = 16.dp, top = 12.dp, end = 8.dp, bottom = 12.dp) ModifierNotUsedAtRoot:TrackingPreferenceWidget.kt$modifier = modifier .clickable(enabled = onClick != null, onClick = { onClick?.invoke() }) .fillMaxWidth() .padding(horizontal = PrefsHorizontalPadding, vertical = 8.dp) + ModifierWithoutDefault:BiometricTimesListItem.kt$modifier + ModifierWithoutDefault:Chip.kt$modifier + ModifierWithoutDefault:MigrationActionIcon.kt$modifier + ModifierWithoutDefault:MigrationItem.kt$modifier + ModifierWithoutDefault:MigrationItemResult.kt$modifier + ModifierWithoutDefault:PagePreviews.kt$modifier + ModifierWithoutDefault:ReaderAppBars.kt$modifier + ModifierWithoutDefault:SortTagListItem.kt$modifier + ModifierWithoutDefault:SourceCategoryListItem.kt$modifier + ModifierWithoutDefault:SourcesFilterScreen.kt$modifier + MultiLineIfElse:CbzCrypto.kt$CbzCrypto$ZipOutputStream(this.openOutputStream()) + MultiLineIfElse:CbzCrypto.kt$CbzCrypto$ZipOutputStream(this.openOutputStream(), password) + MultiLineIfElse:CbzCrypto.kt$CbzCrypto$throw zipException + MultiLineIfElse:SettingsAppearanceScreen.kt$SettingsAppearanceScreen$pluralStringResource( SYMR.plurals.row_count, previewsRowCount, previewsRowCount, ) + MultiLineIfElse:SettingsAppearanceScreen.kt$SettingsAppearanceScreen$stringResource(MR.strings.disabled) NestedBlockDepth:Anilist.kt$Anilist$override suspend fun update(track: Track, didReadChapter: Boolean): Track + NestedBlockDepth:ApiMangaParser.kt$ApiMangaParser$fun parseIntoMetadata( metadata: MangaDexSearchMetadata, mangaDto: MangaDto, simpleChapters: List<String>, statistics: StatisticsMangaDto?, ) NestedBlockDepth:AppLanguageScreen.kt$AppLanguageScreen$private fun getLangs(context: Context): ImmutableList<Language> NestedBlockDepth:BackupRestorer.kt$BackupRestorer$private fun writeErrorLog(): File NestedBlockDepth:BrowseSourceScreenModel.kt$BrowseSourceScreenModel$fun searchGenre(genreName: String) @@ -818,85 +1604,112 @@ NestedBlockDepth:ContextExtensions.kt$fun Context.openInBrowser(uri: Uri, forceDefaultBrowser: Boolean = false) NestedBlockDepth:DownloadQueueScreenModel.kt$DownloadQueueScreenModel.<no name provided>$override fun onMenuItemClick(position: Int, menuItem: MenuItem) NestedBlockDepth:Downloader.kt$Downloader$private fun archiveChapter( mangaDir: UniFile, dirname: String, tmpDir: UniFile, ) + NestedBlockDepth:EHentai.kt$EHentai$override suspend fun parseIntoMetadata(metadata: EHentaiSearchMetadata, input: Document) + NestedBlockDepth:EHentai.kt$EHentai$private fun extendedGenericMangaParse(doc: Document) + NestedBlockDepth:EHentaiUpdateWorker.kt$EHentaiUpdateWorker$private suspend fun startUpdating() + NestedBlockDepth:EXHMigrations.kt$EXHMigrations$fun upgrade( context: Context, preferenceStore: PreferenceStore, basePreferences: BasePreferences, uiPreferences: UiPreferences, networkPreferences: NetworkPreferences, sourcePreferences: SourcePreferences, securityPreferences: SecurityPreferences, libraryPreferences: LibraryPreferences, readerPreferences: ReaderPreferences, backupPreferences: BackupPreferences, trackerManager: TrackerManager, pagePreviewCache: PagePreviewCache, ): Boolean NestedBlockDepth:ExtensionInstaller.kt$ExtensionInstaller$fun installApk(downloadId: Long, uri: Uri) + NestedBlockDepth:FilterHandler.kt$FilterHandler$fun getQueryMap(filters: FilterList): Map<String, Any> + NestedBlockDepth:GalleryAdder.kt$GalleryAdder$suspend fun addGallery( context: Context, url: String, fav: Boolean = false, forceSource: UrlImportableSource? = null, throttleFunc: suspend () -> Unit = {}, retry: Int = 1, ): GalleryAddEvent + NestedBlockDepth:GetMergedChaptersByMangaId.kt$GetMergedChaptersByMangaId$private fun dedupeByPriority( mangaReferences: List<MergedMangaReference>, chapterList: List<Chapter>, ): List<Chapter> + NestedBlockDepth:HBrowse.kt$HBrowse$override suspend fun parseIntoMetadata(metadata: HBrowseSearchMetadata, input: Document) NestedBlockDepth:ImageUtil.kt$ImageUtil$fun chooseBackground(context: Context, imageStream: InputStream): Drawable NestedBlockDepth:KavitaApi.kt$KavitaApi$fun getNewToken(apiUrl: String, apiKey: String): String? NestedBlockDepth:KavitaApi.kt$KavitaApi$private fun getLatestChapterRead(url: String): Double NestedBlockDepth:Kitsu.kt$Kitsu$override suspend fun update(track: Track, didReadChapter: Boolean): Track NestedBlockDepth:LibraryUpdateJob.kt$LibraryUpdateJob$private fun writeErrorFile(errors: List<Pair<Manga, String?>>): File - NestedBlockDepth:LocalSource.kt$LocalSource$private fun copyComicInfoFileFromArchive(chapterArchives: List<UniFile>, folderPath: String?): File? + NestedBlockDepth:LocalSource.kt$LocalSource$private fun copyComicInfoFile(comicInfoFileStream: InputStream, folder: UniFile): UniFile? + NestedBlockDepth:LocalSource.kt$LocalSource$private fun copyComicInfoFileFromArchive(chapterArchives: List<UniFile>, folder: UniFile): UniFile? NestedBlockDepth:LocalSource.kt$LocalSource$private fun updateCover(chapter: SChapter, manga: SManga): UniFile? + NestedBlockDepth:MigrationListScreenModel.kt$MigrationListScreenModel$private suspend fun migrateMangaInternal( prevManga: Manga, manga: Manga, replace: Boolean, ) + NestedBlockDepth:MigrationListScreenModel.kt$MigrationListScreenModel$private suspend fun runMigrations(mangas: List<MigratingManga>) + NestedBlockDepth:Migrations.kt$Migrations$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 NestedBlockDepth:MyAnimeList.kt$MyAnimeList$override suspend fun update(track: Track, didReadChapter: Boolean): Track + NestedBlockDepth:PagerViewerAdapter.kt$PagerViewerAdapter$private fun setJoinedItems(useSecondPage: Boolean = false) + NestedBlockDepth:Pururin.kt$Pururin$override suspend fun parseIntoMetadata(metadata: PururinSearchMetadata, input: Document) + NestedBlockDepth:RaisedSearchMetadata.kt$RaisedSearchMetadata$fun tagsToDescription() + NestedBlockDepth:SearchEngine.kt$SearchEngine$fun queryToSql(q: List<QueryComponent>): Pair<String, List<String>> + NestedBlockDepth:SmartSearchEngine.kt$SmartSearchEngine$private fun removeTextInBrackets(text: String, readForward: Boolean): String NestedBlockDepth:SyncChaptersWithSource.kt$SyncChaptersWithSource$suspend fun await( rawSourceChapters: List<SChapter>, manga: Manga, source: Source, manualFetch: Boolean = false, fetchWindow: Pair<Long, Long> = Pair(0, 0), ): List<Chapter> + NestedBlockDepth:UniFileExtensions.kt$fun UniFile.unzip(destination: File, onlyCopyImages: Boolean = false) NestedBlockDepth:UniFileTempFileManager.kt$UniFileTempFileManager$fun createTempFile(file: UniFile): File NestedBlockDepth:WebtoonRecyclerView.kt$WebtoonRecyclerView.Detector$override fun onTouchEvent(ev: MotionEvent): Boolean + NestedBlockDepth:WebtoonViewer.kt$WebtoonViewer$fun scrollDown() NewLineAtEndOfFile:Commands.kt$.Commands.kt + NoBlankLineBeforeRbrace:CbzCrypto.kt$CbzCrypto$ + NoConsecutiveBlankLines:CbzCrypto.kt$CbzCrypto$ NoConsecutiveBlankLines:LocalesConfigPlugin.kt$ + NoTrailingSpaces:ReaderViewModel.kt$ReaderViewModel$ + NoUnusedImports:BackupManga.kt$eu.kanade.tachiyomi.data.backup.models.BackupManga.kt + NoUnusedImports:CreateBackupScreen.kt$eu.kanade.presentation.more.settings.screen.data.CreateBackupScreen.kt + NoUnusedImports:SettingsAdvancedScreen.kt$eu.kanade.presentation.more.settings.screen.SettingsAdvancedScreen.kt + NoUnusedImports:TachiyomiImageDecoder.kt$eu.kanade.tachiyomi.data.coil.TachiyomiImageDecoder.kt + PreviewPublic:NamespaceTags.kt$NamespaceTagsPreview PreviewPublic:Scrollbar.kt$LazyListHorizontalScrollbarPreview PreviewPublic:Scrollbar.kt$LazyListScrollbarPreview + PrintStackTrace:EnhancedFilePrinter.kt$EnhancedFilePrinter.Worker$e + PrintStackTrace:EnhancedFilePrinter.kt$EnhancedFilePrinter.Writer$e RethrowCaughtException:LocalSource.kt$LocalSource$throw e - ReturnCount:AndroidDatabaseHandler.kt$AndroidDatabaseHandler$private suspend fun <T> dispatch(inTransaction: Boolean, block: suspend Database.() -> T): T ReturnCount:AndroidSourceManager.kt$AndroidSourceManager$private suspend fun createStubSource(id: Long): StubSource ReturnCount:BackupCreateJob.kt$BackupCreateJob$override suspend fun doWork(): Result + ReturnCount:BrowseSourceScreen.kt$@Composable fun BrowseSourceContent( source: Source?, mangaList: LazyPagingItems<StateFlow</* SY --> */Pair<Manga, RaisedSearchMetadata?>/* SY <-- */>>, columns: GridCells, // SY --> ehentaiBrowseDisplayMode: Boolean, // SY <-- displayMode: LibraryDisplayMode, snackbarHostState: SnackbarHostState, contentPadding: PaddingValues, // SY --> onWebViewClick: (() -> Unit)?, onHelpClick: (() -> Unit)?, onLocalSourceHelpClick: (() -> Unit)?, // SY <-- onMangaClick: (Manga) -> Unit, onMangaLongClick: (Manga) -> Unit, ) ReturnCount:ChapterRecognition.kt$ChapterRecognition$fun parseChapterNumber( mangaTitle: String, chapterName: String, chapterNumber: Double? = null, ): Double ReturnCount:ChapterRecognition.kt$ChapterRecognition$private fun checkForDecimal(decimal: String?, alpha: String?): Double - ReturnCount:Download.kt$Download.Companion$suspend fun fromChapterId( chapterId: Long, getChapter: GetChapter = Injekt.get(), getManga: GetManga = Injekt.get(), sourceManager: SourceManager = Injekt.get(), ): Download? - ReturnCount:DownloadCache.kt$DownloadCache$fun isChapterDownloaded( chapterName: String, chapterScanlator: String?, mangaTitle: String, sourceId: Long, skipCache: Boolean, ): Boolean - ReturnCount:DownloadManager.kt$DownloadManager$fun renameSource(oldSource: Source, newSource: Source) - ReturnCount:Downloader.kt$Downloader$private fun isDownloadSuccessful( download: Download, tmpDir: UniFile, ): Boolean - ReturnCount:ExtensionInstallReceiver.kt$ExtensionInstallReceiver$override fun onReceive(context: Context, intent: Intent?) + ReturnCount:EHentai.kt$EHentai.ThumbnailPreviewInterceptor$override fun intercept(chain: Interceptor.Chain): Response + ReturnCount:EXHMigrations.kt$EXHMigrations$fun upgrade( context: Context, preferenceStore: PreferenceStore, basePreferences: BasePreferences, uiPreferences: UiPreferences, networkPreferences: NetworkPreferences, sourcePreferences: SourcePreferences, securityPreferences: SecurityPreferences, libraryPreferences: LibraryPreferences, readerPreferences: ReaderPreferences, backupPreferences: BackupPreferences, trackerManager: TrackerManager, pagePreviewCache: PagePreviewCache, ): Boolean + ReturnCount:EhLoginActivity.kt$EhLoginActivity$private fun applyExHentaiCookies(url: String, customIgneous: String?): Boolean ReturnCount:ExtensionInstallService.kt$ExtensionInstallService$override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int - ReturnCount:ExtensionInstaller.kt$ExtensionInstaller.DownloadCompletionReceiver$override fun onReceive(context: Context, intent: Intent?) ReturnCount:ExtensionInstallerPreference.kt$ExtensionInstallerPreference$private fun check(value: ExtensionInstaller): ExtensionInstaller ReturnCount:ExtensionLoader.kt$ExtensionLoader$fun installPrivateExtensionFile(context: Context, file: File): Boolean ReturnCount:ExtensionLoader.kt$ExtensionLoader$private fun loadExtension(context: Context, extensionInfo: ExtensionInfo): LoadResult ReturnCount:ExtensionLoader.kt$ExtensionLoader$private fun selectExtensionPackage(shared: ExtensionInfo?, private: ExtensionInfo?): ExtensionInfo? + ReturnCount:FavoritesSyncHelper.kt$FavoritesSyncHelper$private suspend fun beginSync() + ReturnCount:GalleryAdder.kt$GalleryAdder$suspend fun addGallery( context: Context, url: String, fav: Boolean = false, forceSource: UrlImportableSource? = null, throttleFunc: suspend () -> Unit = {}, retry: Int = 1, ): GalleryAddEvent ReturnCount:HttpPageLoader.kt$HttpPageLoader$private fun preloadNextPages(currentPage: ReaderPage, amount: Int): List<PriorityPage> ReturnCount:ImageUtil.kt$ImageUtil$fun chooseBackground(context: Context, imageStream: InputStream): Drawable - ReturnCount:ImageUtil.kt$ImageUtil$fun isAnimatedAndSupported(stream: InputStream): Boolean - ReturnCount:ImageUtil.kt$ImageUtil$fun splitTallImage(tmpDir: UniFile, imageFile: UniFile, filenamePrefix: String): Boolean ReturnCount:KavitaApi.kt$KavitaApi$fun getNewToken(apiUrl: String, apiKey: String): String? ReturnCount:KavitaApi.kt$KavitaApi$private fun getLatestChapterRead(url: String): Double - ReturnCount:KitsuDateHelper.kt$KitsuDateHelper$fun parse(dateString: String?): Long ReturnCount:LibraryUpdateJob.kt$LibraryUpdateJob$override suspend fun doWork(): Result - ReturnCount:LocalSource.kt$LocalSource$private fun copyComicInfoFileFromArchive(chapterArchives: List<UniFile>, folderPath: String?): File? + ReturnCount:LibraryUpdateJob.kt$LibraryUpdateJob$private suspend fun updateManga(manga: Manga, fetchWindow: Pair<Long, Long>): List<Chapter> + ReturnCount:LocalCoverManager.kt$LocalCoverManager$actual fun update( manga: SManga, inputStream: InputStream, // SY --> encrypted: Boolean, // SY <-- ): UniFile? + ReturnCount:LocalSource.kt$LocalSource$private fun copyComicInfoFileFromArchive(chapterArchives: List<UniFile>, folder: UniFile): UniFile? ReturnCount:MainActivity.kt$MainActivity$private fun handleIntentAction(intent: Intent, navigator: Navigator): Boolean ReturnCount:MangaCoverFetcher.kt$MangaCoverFetcher$private suspend fun httpLoader(): FetchResult - ReturnCount:MangaExtensions.kt$fun Manga.prepUpdateCover(coverCache: CoverCache, remoteManga: SManga, refreshSameUrl: Boolean): Manga - ReturnCount:MangaExtensions.kt$fun Manga.removeCovers(coverCache: CoverCache = Injekt.get()): Manga + ReturnCount:MangaDexAuthInterceptor.kt$MangaDexAuthInterceptor$override fun intercept(chain: Interceptor.Chain): Response ReturnCount:MangaExtensions.kt$fun Manga.shouldDownloadNewChapters(dbCategories: List<Long>, preferences: DownloadPreferences): Boolean - ReturnCount:MangaScreen.kt$MangaScreen$private fun getMangaUrl(manga_: Manga?, source_: Source?): String? - ReturnCount:MissingChapters.kt$fun List<Double>.missingChaptersCount(): Int - ReturnCount:MissingChapters.kt$fun calculateChapterGap(higherChapter: Chapter?, lowerChapter: Chapter?): Int + ReturnCount:MdUtil.kt$MdUtil.Companion$fun getMissingChapterCount(chapters: List<SChapter>, mangaStatus: Int): String? + ReturnCount:Migrations.kt$Migrations$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 ReturnCount:MyAnimeList.kt$MyAnimeList$override suspend fun search(query: String): List<TrackSearch> ReturnCount:NetworkExtensions.kt$fun Context.isConnectedToWifi(): Boolean - ReturnCount:NetworkExtensions.kt$fun Context.isOnline(): Boolean - ReturnCount:PagerPageHolder.kt$PagerPageHolder$private fun process(page: ReaderPage, imageStream: BufferedInputStream): InputStream + ReturnCount:PageHandler.kt$PageHandler$@Suppress("UNCHECKED_CAST") private fun updateExtensionVariable(mangadex: Source, atHomeRequestUrl: String) + ReturnCount:PagePreviewFetcher.kt$PagePreviewFetcher$private suspend fun httpLoader(): FetchResult + ReturnCount:PagerPageHolder.kt$PagerPageHolder$private fun mergePages(imageStream: InputStream, imageStream2: InputStream?): InputStream ReturnCount:PagerViewer.kt$PagerViewer$override fun handleKeyEvent(event: KeyEvent): Boolean - ReturnCount:PagerViewer.kt$PagerViewer$private fun checkAllowPreload(page: ReaderPage?): Boolean ReturnCount:PagerViewerAdapter.kt$PagerViewerAdapter$fun onPageSplit(currentPage: Any?, newPage: InsertPage) ReturnCount:ReaderActivity.kt$ReaderActivity$override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean - ReturnCount:ReaderActivity.kt$ReaderActivity$private fun moveToPageIndex(index: Int) - ReturnCount:ReaderViewModel.kt$ReaderViewModel$fun getChapterUrl(): String? - ReturnCount:ReaderViewModel.kt$ReaderViewModel$fun setAsCover() - ReturnCount:ReaderViewModel.kt$ReaderViewModel$private fun downloadNextChapters() - ReturnCount:ReaderViewModel.kt$ReaderViewModel$private fun updateTrackChapterRead(readerChapter: ReaderChapter) - ReturnCount:ReaderViewModel.kt$ReaderViewModel$suspend fun preload(chapter: ReaderChapter) - ReturnCount:ViewExtensions.kt$fun View?.isVisibleOnScreen(): Boolean + ReturnCount:ReaderViewModel.kt$ReaderViewModel$fun saveImages() + ReturnCount:ReaderViewModel.kt$ReaderViewModel$fun setAsCover(useExtraPage: Boolean) + ReturnCount:ReaderViewModel.kt$ReaderViewModel$fun shareImages() + ReturnCount:ReorderSortTag.kt$ReorderSortTag$fun await(tag: String, newPosition: Int): Result + ReturnCount:TimeRange.kt$TimeRange.Companion$fun fromPreferenceString(timeRange: String): TimeRange? ReturnCount:WebViewInterceptor.kt$WebViewInterceptor$override fun intercept(chain: Interceptor.Chain): Response ReturnCount:WebViewInterceptor.kt$private fun isRequestHeaderSafe(_name: String, _value: String): Boolean ReturnCount:WebtoonFrame.kt$WebtoonFrame$override fun dispatchTouchEvent(ev: MotionEvent): Boolean - ReturnCount:WebtoonPageHolder.kt$WebtoonPageHolder$private fun process(imageStream: BufferedInputStream): InputStream ReturnCount:WebtoonRecyclerView.kt$WebtoonRecyclerView.Detector$override fun onTouchEvent(ev: MotionEvent): Boolean ReturnCount:WebtoonViewer.kt$WebtoonViewer$override fun handleKeyEvent(event: KeyEvent): Boolean - ReturnCount:WebtoonViewer.kt$WebtoonViewer$private fun checkAllowPreload(page: ReaderPage?): Boolean SerialVersionUIDInSerializableClass:Category.kt$Category : Serializable SerialVersionUIDInSerializableClass:Manga.kt$Manga : Serializable + SerialVersionUIDInSerializableClass:MigrationProcedureConfig.kt$MigrationProcedureConfig : Serializable + SerialVersionUIDInSerializableClass:SourcesScreen.kt$SourcesScreen$SmartSearchConfig : Serializable + SpreadOperator:App.kt$App$( logConfig, *printers.toTypedArray(), ) SpreadOperator:ChapterRepositoryImpl.kt$ChapterRepositoryImpl$(*chapterUpdates.toTypedArray()) SpreadOperator:ChapterSanitizer.kt$ChapterSanitizer$(*CHAPTER_TRIM_CHARS) SpreadOperator:LibraryScreenModel.kt$LibraryScreenModel$(*prefFlows) + SpreadOperator:LibraryUpdateJob.kt$LibraryUpdateJob$(true, *newReadChapters.toTypedArray()) + SpreadOperator:MangaMergeRepositoryImpl.kt$MangaMergeRepositoryImpl$(*values.toTypedArray()) SpreadOperator:MangaRepositoryImpl.kt$MangaRepositoryImpl$(*mangaUpdates.toTypedArray()) - SpreadOperator:SourcesScreenModel.kt$SourcesScreenModel$( SourceUiModel.Header(it.key), *it.value.map { source -> SourceUiModel.Item(source) }.toTypedArray(), ) + SpreadOperator:ReaderViewModel.kt$ReaderViewModel$(true, *it.toTypedArray()) + SpreadOperator:SourcesScreenModel.kt$SourcesScreenModel$( SourceUiModel.Header(it.key.removePrefix(CATEGORY_KEY_PREFIX), it.value.firstOrNull()?.category != null), *it.value.map { source -> SourceUiModel.Item(source) }.toTypedArray(), ) SpreadOperator:TrackRepositoryImpl.kt$TrackRepositoryImpl$(*tracks.toTypedArray()) SwallowedException:AboutScreen.kt$AboutScreen$e: Exception SwallowedException:AndroidPreference.kt$AndroidPreference$e: ClassCastException @@ -909,18 +1722,30 @@ SwallowedException:Bangumi.kt$Bangumi$e: Exception SwallowedException:Bangumi.kt$Bangumi$e: Throwable SwallowedException:BrowseIcons.kt$e: Exception + SwallowedException:CbzCrypto.kt$CbzCrypto$e: Exception SwallowedException:ChapterCache.kt$ChapterCache$e: IOException SwallowedException:ChapterLoader.kt$ChapterLoader$e: UnsupportedRarV5Exception SwallowedException:ContextExtensions.kt$e: Exception SwallowedException:ContextExtensions.kt$e: PackageManager.NameNotFoundException SwallowedException:CrashLogUtil.kt$CrashLogUtil$e: Throwable SwallowedException:CreateBackupScreen.kt$CreateBackupScreen$e: ActivityNotFoundException + SwallowedException:CustomMangaRepositoryImpl.kt$CustomMangaRepositoryImpl$e: Exception SwallowedException:DeviceUtil.kt$DeviceUtil$e: Exception SwallowedException:DownloadPendingDeleter.kt$DownloadPendingDeleter$e: Exception SwallowedException:DownloadStore.kt$DownloadStore$e: Exception SwallowedException:Downloader.kt$Downloader$e: Throwable + SwallowedException:EHNetworkLogging.kt$ex: Exception + SwallowedException:EHentaiUpdateWorker.kt$EHentaiUpdateWorker$e: Exception + SwallowedException:EXHMigrations.kt$EXHMigrations$e: Exception + SwallowedException:EXHMigrations.kt$EXHMigrations$e: URISyntaxException + SwallowedException:EXHMigrations.kt$EXHMigrations.MangaConfig.Companion$e: Exception + SwallowedException:EnhancedFilePrinter.kt$EnhancedFilePrinter.Writer$e: IOException + SwallowedException:ExceptionUtil.kt$t: Throwable SwallowedException:ExtensionLoader.kt$ExtensionLoader$error: PackageManager.NameNotFoundException + SwallowedException:FeedScreenModel.kt$FeedScreenModel$e: Exception + SwallowedException:GalleryAdder.kt$GalleryAdder$e: Exception SwallowedException:GetChapterByUrlAndMangaId.kt$GetChapterByUrlAndMangaId$e: Exception + SwallowedException:GetPagePreviews.kt$GetPagePreviews$e: Exception SwallowedException:HttpSource.kt$HttpSource$e: URISyntaxException SwallowedException:ImageUtil.kt$ImageUtil$e: Exception SwallowedException:Kavita.kt$Kavita$e: Exception @@ -929,15 +1754,21 @@ SwallowedException:KitsuModels.kt$KitsuSearchManga$e: IllegalArgumentException SwallowedException:Komga.kt$Komga$e: Exception SwallowedException:LibrarySortMode.kt$LibrarySort.Companion$e: Exception + SwallowedException:LibraryUpdateJob.kt$LibraryUpdateJob$e: Exception SwallowedException:LocalSource.kt$LocalSource$e: Format.UnknownFormatException SwallowedException:MangaScreen.kt$MangaScreen$e: Exception SwallowedException:MangaUpdates.kt$MangaUpdates$e: Exception SwallowedException:MangaUpdatesApi.kt$MangaUpdatesApi$e: Exception + SwallowedException:MdUtil.kt$MdUtil.Companion$e: Exception SwallowedException:MetadataUpdateJob.kt$MetadataUpdateJob$e: Exception + SwallowedException:MigrationListScreenModel.kt$MigrationListScreenModel$e: CancellationException + SwallowedException:MigrationListScreenModel.kt$MigrationListScreenModel$e: Exception SwallowedException:MyAnimeList.kt$MyAnimeList$e: Exception SwallowedException:MyAnimeList.kt$MyAnimeList$e: Throwable SwallowedException:MyAnimeListApi.kt$MyAnimeListApi$e: Exception SwallowedException:OkHttpExtensions.kt$ex: Throwable + SwallowedException:PagePreviewCache.kt$PagePreviewCache$e: Exception + SwallowedException:PagePreviewCache.kt$PagePreviewCache$e: IOException SwallowedException:Pager.kt$Pager$e: IllegalArgumentException SwallowedException:Pager.kt$Pager$e: IndexOutOfBoundsException SwallowedException:Pager.kt$Pager$e: NullPointerException @@ -949,24 +1780,30 @@ SwallowedException:SettingsDataScreen.kt$SettingsDataScreen$e: ActivityNotFoundException SwallowedException:Shikimori.kt$Shikimori$e: Exception SwallowedException:Shikimori.kt$Shikimori$e: Throwable + SwallowedException:SourceFeedScreenModel.kt$SourceFeedScreenModel$e: Exception SwallowedException:StorageStep.kt$StorageStep$e: ActivityNotFoundException SwallowedException:Suwayomi.kt$Suwayomi$e: Exception SwallowedException:TrackInfoDialog.kt$TrackInfoDialogHomeScreen.Model$e: Exception + SwallowedException:UrlImportableSource.kt$UrlImportableSource$e: URISyntaxException ThrowingExceptionsWithoutMessageOrCause:MangaScreenModel.kt$MangaScreenModel$IllegalStateException() ThrowingExceptionsWithoutMessageOrCause:OkHttpExtensions.kt$Exception() ThrowingExceptionsWithoutMessageOrCause:PackageInstallerInstaller.kt$PackageInstallerInstaller$IllegalStateException() ThrowingExceptionsWithoutMessageOrCause:ShizukuInstaller.kt$ShizukuInstaller$IllegalStateException() ThrowingExceptionsWithoutMessageOrCause:TrackInfoDialog.kt$TrackInfoDialogHomeScreen.Model$Exception() ThrowsCount:BackupCreator.kt$BackupCreator$suspend fun backup(uri: Uri, options: BackupOptions): String + ThrowsCount:EHentaiUpdateWorker.kt$EHentaiUpdateWorker$private suspend fun updateEntryAndGetChapters(manga: Manga): Pair<List<Chapter>, List<Chapter>> ThrowsCount:FileExtensions.kt$fun File.copyAndSetReadOnlyTo(target: File, overwrite: Boolean = false, bufferSize: Int = DEFAULT_BUFFER_SIZE): File ThrowsCount:KavitaApi.kt$KavitaApi$fun getNewToken(apiUrl: String, apiKey: String): String? ThrowsCount:LocalSource.kt$LocalSource$fun getFormat(chapter: SChapter): Format + ThrowsCount:MangaScreenModel.kt$MangaScreenModel$suspend fun smartSearchMerge(manga: Manga, originalMangaId: Long): Manga + ThrowsCount:MigrationListScreenModel.kt$MigrationListScreenModel$private suspend fun runMigrations(mangas: List<MigratingManga>) TooGenericExceptionCaught:AboutScreen.kt$AboutScreen$e: Exception TooGenericExceptionCaught:AddTracks.kt$AddTracks$e: Exception TooGenericExceptionCaught:AndroidPreference.kt$AndroidPreference.Object$e: Exception TooGenericExceptionCaught:Anilist.kt$Anilist$e: Exception TooGenericExceptionCaught:Anilist.kt$Anilist$e: Throwable TooGenericExceptionCaught:AnilistModels.kt$ALManga$e: Exception + TooGenericExceptionCaught:ApiMangaParser.kt$ApiMangaParser$e: Exception TooGenericExceptionCaught:App.kt$App$e: Exception TooGenericExceptionCaught:AppUpdateDownloadJob.kt$AppUpdateDownloadJob$e: Exception TooGenericExceptionCaught:BackupCreateJob.kt$BackupCreateJob$e: Exception @@ -979,16 +1816,20 @@ TooGenericExceptionCaught:BaseTracker.kt$BaseTracker$e: Exception TooGenericExceptionCaught:BaseTracker.kt$BaseTracker$e: Throwable TooGenericExceptionCaught:BrowseIcons.kt$e: Exception + TooGenericExceptionCaught:CbzCrypto.kt$CbzCrypto$e: Exception TooGenericExceptionCaught:ChapterCache.kt$ChapterCache$e: Exception TooGenericExceptionCaught:ChapterLoader.kt$ChapterLoader$e: Throwable TooGenericExceptionCaught:ChapterRepositoryImpl.kt$ChapterRepositoryImpl$e: Exception TooGenericExceptionCaught:CloseableExtensions.kt$closeException: Throwable TooGenericExceptionCaught:CloseableExtensions.kt$e: Throwable TooGenericExceptionCaught:CloudflareInterceptor.kt$CloudflareInterceptor$e: Exception + TooGenericExceptionCaught:ConfigureExhDialog.kt$e: Exception TooGenericExceptionCaught:ContextExtensions.kt$e: Exception TooGenericExceptionCaught:ContextExtensions.kt$e: Throwable TooGenericExceptionCaught:CrashLogUtil.kt$CrashLogUtil$e: Throwable + TooGenericExceptionCaught:CrashlyticsPrinter.kt$CrashlyticsPrinter$t: Throwable TooGenericExceptionCaught:CreateCategoryWithName.kt$CreateCategoryWithName$e: Exception + TooGenericExceptionCaught:CustomMangaRepositoryImpl.kt$CustomMangaRepositoryImpl$e: Exception TooGenericExceptionCaught:DeleteCategory.kt$DeleteCategory$e: Exception TooGenericExceptionCaught:DeleteTrack.kt$DeleteTrack$e: Exception TooGenericExceptionCaught:DeviceUtil.kt$DeviceUtil$e: Exception @@ -999,6 +1840,13 @@ TooGenericExceptionCaught:Downloader.kt$Downloader$e: Exception TooGenericExceptionCaught:Downloader.kt$Downloader$e: Throwable TooGenericExceptionCaught:Downloader.kt$Downloader$error: Throwable + TooGenericExceptionCaught:EHNetworkLogging.kt$ex: Exception + TooGenericExceptionCaught:EHentaiUpdateWorker.kt$EHentaiUpdateWorker$e: Exception + TooGenericExceptionCaught:EHentaiUpdateWorker.kt$EHentaiUpdateWorker$t: Throwable + TooGenericExceptionCaught:EXHMigrations.kt$EXHMigrations$e: Exception + TooGenericExceptionCaught:EXHMigrations.kt$EXHMigrations.MangaConfig.Companion$e: Exception + TooGenericExceptionCaught:EnhancedFilePrinter.kt$EnhancedFilePrinter.Writer$e: Exception + TooGenericExceptionCaught:ExceptionUtil.kt$t: Throwable TooGenericExceptionCaught:ExtensionApi.kt$ExtensionApi$e: Throwable TooGenericExceptionCaught:ExtensionDetailsScreenModel.kt$ExtensionDetailsScreenModel$e: Exception TooGenericExceptionCaught:ExtensionInstallActivity.kt$ExtensionInstallActivity$error: Exception @@ -1006,16 +1854,30 @@ TooGenericExceptionCaught:ExtensionLoader.kt$ExtensionLoader$e: Exception TooGenericExceptionCaught:ExtensionLoader.kt$ExtensionLoader$e: Throwable TooGenericExceptionCaught:ExtensionManager.kt$ExtensionManager$e: Exception + TooGenericExceptionCaught:FavoritesEntryRepositoryImpl.kt$FavoritesEntryRepositoryImpl$e: Exception + TooGenericExceptionCaught:FavoritesSyncHelper.kt$FavoritesSyncHelper$e: Exception + TooGenericExceptionCaught:FeedScreenModel.kt$FeedScreenModel$e: Exception + TooGenericExceptionCaught:GalleryAdder.kt$GalleryAdder$e: Exception TooGenericExceptionCaught:GetChapter.kt$GetChapter$e: Exception + TooGenericExceptionCaught:GetChapterByUrl.kt$GetChapterByUrl$e: Exception TooGenericExceptionCaught:GetChapterByUrlAndMangaId.kt$GetChapterByUrlAndMangaId$e: Exception TooGenericExceptionCaught:GetChaptersByMangaId.kt$GetChaptersByMangaId$e: Exception + TooGenericExceptionCaught:GetFlatMetadataById.kt$GetFlatMetadataById$e: Exception TooGenericExceptionCaught:GetManga.kt$GetManga$e: Exception + TooGenericExceptionCaught:GetMergedChaptersByMangaId.kt$GetMergedChaptersByMangaId$e: Exception + TooGenericExceptionCaught:GetMergedManga.kt$GetMergedManga$e: Exception + TooGenericExceptionCaught:GetMergedMangaById.kt$GetMergedMangaById$e: Exception + TooGenericExceptionCaught:GetMergedReferencesById.kt$GetMergedReferencesById$e: Exception + TooGenericExceptionCaught:GetPagePreviews.kt$GetPagePreviews$e: Exception TooGenericExceptionCaught:GetTracks.kt$GetTracks$e: Exception TooGenericExceptionCaught:GlobalExceptionHandler.kt$GlobalExceptionHandler.Companion$e: Exception TooGenericExceptionCaught:HistoryRepositoryImpl.kt$HistoryRepositoryImpl$e: Exception TooGenericExceptionCaught:HttpPageLoader.kt$HttpPageLoader$e: Throwable TooGenericExceptionCaught:ImageSaver.kt$ImageSaver$e: Exception TooGenericExceptionCaught:ImageUtil.kt$ImageUtil$e: Exception + TooGenericExceptionCaught:InsertFeedSavedSearch.kt$InsertFeedSavedSearch$e: Exception + TooGenericExceptionCaught:InsertFlatMetadata.kt$InsertFlatMetadata$e: Exception + TooGenericExceptionCaught:InsertSavedSearch.kt$InsertSavedSearch$e: Exception TooGenericExceptionCaught:InsertTrack.kt$InsertTrack$e: Exception TooGenericExceptionCaught:Kavita.kt$Kavita$e: Exception TooGenericExceptionCaught:KavitaApi.kt$KavitaApi$e: Exception @@ -1031,41 +1893,59 @@ TooGenericExceptionCaught:MangaCoverFetcher.kt$MangaCoverFetcher$e: Exception TooGenericExceptionCaught:MangaCoverScreenModel.kt$MangaCoverScreenModel$e: Exception TooGenericExceptionCaught:MangaCoverScreenModel.kt$MangaCoverScreenModel$e: Throwable + TooGenericExceptionCaught:MangaMergeRepositoryImpl.kt$MangaMergeRepositoryImpl$e: Exception TooGenericExceptionCaught:MangaRepositoryImpl.kt$MangaRepositoryImpl$e: Exception TooGenericExceptionCaught:MangaScreen.kt$MangaScreen$e: Exception + TooGenericExceptionCaught:MangaScreenModel.kt$MangaScreenModel$e: Exception TooGenericExceptionCaught:MangaScreenModel.kt$MangaScreenModel$e: Throwable TooGenericExceptionCaught:MangaUpdates.kt$MangaUpdates$e: Exception TooGenericExceptionCaught:MangaUpdatesApi.kt$MangaUpdatesApi$e: Exception + TooGenericExceptionCaught:MdUtil.kt$MdUtil.Companion$e: Exception + TooGenericExceptionCaught:MemAutoFlushingLookupTable.kt$MemAutoFlushingLookupTable$t: Throwable + TooGenericExceptionCaught:MergedSource.kt$MergedSource$e: Exception TooGenericExceptionCaught:MetadataUpdateJob.kt$MetadataUpdateJob$e: Exception TooGenericExceptionCaught:MetadataUpdateJob.kt$MetadataUpdateJob$e: Throwable + TooGenericExceptionCaught:MigrationListScreenModel.kt$MigrationListScreenModel$e: Exception TooGenericExceptionCaught:MyAnimeList.kt$MyAnimeList$e: Exception TooGenericExceptionCaught:MyAnimeList.kt$MyAnimeList$e: Throwable TooGenericExceptionCaught:MyAnimeListApi.kt$MyAnimeListApi$e: Exception TooGenericExceptionCaught:OkHttpExtensions.kt$<no name provided>$e: Exception + TooGenericExceptionCaught:OkHttpExtensions.kt$<no name provided>$error: Throwable TooGenericExceptionCaught:OkHttpExtensions.kt$ex: Throwable + TooGenericExceptionCaught:OkHttpUtil.kt$t: Throwable TooGenericExceptionCaught:OkioExtensions.kt$e: Exception TooGenericExceptionCaught:PackageInstallerInstaller.kt$PackageInstallerInstaller$e: Exception + TooGenericExceptionCaught:PagePreviewCache.kt$PagePreviewCache$e: Exception + TooGenericExceptionCaught:PagePreviewFetcher.kt$PagePreviewFetcher$e: Exception TooGenericExceptionCaught:Pager.kt$Pager$e: IndexOutOfBoundsException TooGenericExceptionCaught:Pager.kt$Pager$e: NullPointerException + TooGenericExceptionCaught:PagerPageHolder.kt$PagerPageHolder$e: Exception TooGenericExceptionCaught:PagerPageHolder.kt$PagerPageHolder$e: Throwable TooGenericExceptionCaught:QueryPagingSource.kt$QueryPagingSource$e: Exception TooGenericExceptionCaught:RarPageLoader.kt$RarPageLoader$e: Exception TooGenericExceptionCaught:ReaderActivity.kt$ReaderActivity$e: ArrayIndexOutOfBoundsException TooGenericExceptionCaught:ReaderViewModel.kt$ReaderViewModel$e: Exception TooGenericExceptionCaught:ReaderViewModel.kt$ReaderViewModel$e: Throwable + TooGenericExceptionCaught:RecommendsPagingSource.kt$RecommendsPagingSource$e: Exception TooGenericExceptionCaught:RefreshTracks.kt$RefreshTracks$e: Throwable TooGenericExceptionCaught:RenameCategory.kt$RenameCategory$e: Exception TooGenericExceptionCaught:ReorderCategory.kt$ReorderCategory$e: Exception TooGenericExceptionCaught:RestoreBackupScreen.kt$RestoreBackupScreenModel$e: Exception + TooGenericExceptionCaught:RxCoroutineBridge.kt$e: Throwable TooGenericExceptionCaught:SearchScreenModel.kt$SearchScreenModel$e: Exception TooGenericExceptionCaught:SetMangaCategories.kt$SetMangaCategories$e: Exception TooGenericExceptionCaught:SetReadStatus.kt$SetReadStatus$e: Exception TooGenericExceptionCaught:SettingsAdvancedScreen.kt$SettingsAdvancedScreen$e: Throwable TooGenericExceptionCaught:SettingsDataScreen.kt$SettingsDataScreen$e: Throwable + TooGenericExceptionCaught:SettingsDebugScreen.kt$SettingsDebugScreen$e: Exception + TooGenericExceptionCaught:SettingsEhScreen.kt$SettingsEhScreen$e: Exception + TooGenericExceptionCaught:SettingsMangadexScreen.kt$SettingsMangadexScreen$e: Exception TooGenericExceptionCaught:SettingsTrackingScreen.kt$SettingsTrackingScreen$e: Throwable TooGenericExceptionCaught:Shikimori.kt$Shikimori$e: Exception TooGenericExceptionCaught:Shikimori.kt$Shikimori$e: Throwable TooGenericExceptionCaught:ShizukuInstaller.kt$ShizukuInstaller$e: Exception + TooGenericExceptionCaught:SmartSearchScreenModel.kt$SmartSearchScreenModel$e: Exception + TooGenericExceptionCaught:SourceFeedScreenModel.kt$SourceFeedScreenModel$e: Exception TooGenericExceptionCaught:SourcePagingSource.kt$SourcePagingSource$e: Exception TooGenericExceptionCaught:Suwayomi.kt$Suwayomi$e: Exception TooGenericExceptionCaught:SyncChapterProgressWithTrack.kt$SyncChapterProgressWithTrack$e: Throwable @@ -1087,10 +1967,16 @@ TooGenericExceptionThrown:AppUpdateDownloadJob.kt$AppUpdateDownloadJob$throw Exception("Unsuccessful response") TooGenericExceptionThrown:BangumiApi.kt$BangumiApi$throw Exception("Null Response") TooGenericExceptionThrown:BangumiInterceptor.kt$BangumiInterceptor$throw Exception("Not authenticated with Bangumi") + TooGenericExceptionThrown:BilibiliHandler.kt$BilibiliHandler$throw Exception("Unknown chapter $chapterNumber") TooGenericExceptionThrown:ChapterLoader.kt$ChapterLoader$throw Exception(context.stringResource(MR.strings.page_list_empty_error)) TooGenericExceptionThrown:DownloadManager.kt$DownloadManager$throw Exception(context.stringResource(MR.strings.page_list_empty_error)) TooGenericExceptionThrown:DownloadProvider.kt$DownloadProvider$throw Exception( context.stringResource( MR.strings.invalid_location, downloadsDir?.displayablePath ?: "", ), ) TooGenericExceptionThrown:Downloader.kt$Downloader$throw Exception(context.stringResource(MR.strings.page_list_empty_error)) + TooGenericExceptionThrown:EHentai.kt$EHentai$throw Exception( "Invalid igneous cookie, try re-logging or finding a correct one to input in the login menu", ) + TooGenericExceptionThrown:EHentai.kt$EHentai$throw Exception("Exceeded page quota") + TooGenericExceptionThrown:EHentai.kt$EHentai$throw Exception("HTTP error ${response.code}", exception) + TooGenericExceptionThrown:EHentai.kt$EHentai$throw Exception("HTTP error ${response.code}", stacktrace) + TooGenericExceptionThrown:EHentai.kt$EHentai$throw Exception(it.text()) TooGenericExceptionThrown:ExtensionLoader.kt$ExtensionLoader$throw Exception("Unknown source class type: ${obj.javaClass}") TooGenericExceptionThrown:HttpSource.kt$HttpSource$throw RuntimeException(e) TooGenericExceptionThrown:KitsuApi.kt$KitsuApi$throw Exception("Could not find manga") @@ -1099,23 +1985,35 @@ TooGenericExceptionThrown:KitsuModels.kt$throw Exception("Unknown status") TooGenericExceptionThrown:LocalSource.kt$LocalSource$throw Exception(context.stringResource(MR.strings.chapter_not_found)) TooGenericExceptionThrown:LocalSource.kt$LocalSource$throw Exception(context.stringResource(MR.strings.local_invalid_format)) + TooGenericExceptionThrown:MangaPlusHandler.kt$MangaPlusHandler$throw Exception("error getting images") TooGenericExceptionThrown:MangaUpdates.kt$MangaUpdates$throw Throwable("Unable to login") + TooGenericExceptionThrown:MdList.kt$MdList$throw Exception("not used") + TooGenericExceptionThrown:PageHandler.kt$PageHandler$throw Exception("${chapter.scanlator} not supported") + TooGenericExceptionThrown:ReaderViewModel.kt$ReaderViewModel$throw Exception("Not an image") + TooGenericExceptionThrown:RecommendsPagingSource.kt$Anilist$throw Exception("'$queryParam' not found") TooGenericExceptionThrown:ShikimoriApi.kt$ShikimoriApi$throw Exception("Too much mangas in response") TooGenericExceptionThrown:ShikimoriInterceptor.kt$ShikimoriInterceptor$throw Exception("Not authenticated with Shikimori") TooGenericExceptionThrown:ShizukuInstaller.kt$ShizukuInstaller$throw RuntimeException("Failed to commit install session $sessionId") TooGenericExceptionThrown:ShizukuInstaller.kt$ShizukuInstaller$throw RuntimeException("Failed to create install session") TooGenericExceptionThrown:ShizukuInstaller.kt$ShizukuInstaller$throw RuntimeException("Failed to write APK to session $sessionId") TooGenericExceptionThrown:TrackInfoDialog.kt$TrackInfoDialogHomeScreen.Model$throw Exception() + TooGenericExceptionThrown:WhatsNewDialog.kt$throw Exception("Exception on ${version.versionName}:$index:$charIndex", e) TooManyFunctions:AndroidDatabaseHandler.kt$AndroidDatabaseHandler : DatabaseHandler + TooManyFunctions:AndroidSourceManager.kt$AndroidSourceManager : SourceManager TooManyFunctions:Anilist.kt$Anilist : BaseTrackerDeletableTracker TooManyFunctions:AnilistApi.kt$AnilistApi TooManyFunctions:Bangumi.kt$Bangumi : BaseTracker TooManyFunctions:BaseTracker.kt$BaseTracker : Tracker + TooManyFunctions:BilibiliHandler.kt$BilibiliHandler TooManyFunctions:BrowseSourceScreenModel.kt$BrowseSourceScreenModel : StateScreenModel TooManyFunctions:CategoryRepositoryImpl.kt$CategoryRepositoryImpl : CategoryRepository + TooManyFunctions:CbzCrypto.kt$CbzCrypto TooManyFunctions:ChapterRepository.kt$ChapterRepository TooManyFunctions:ChapterRepositoryImpl.kt$ChapterRepositoryImpl : ChapterRepository TooManyFunctions:ChapterTransition.kt$eu.kanade.presentation.reader.ChapterTransition.kt + TooManyFunctions:DatabaseHandler.kt$DatabaseHandler + TooManyFunctions:DebugFunctions.kt$DebugFunctions + TooManyFunctions:DelegatedHttpSource.kt$DelegatedHttpSource : HttpSource TooManyFunctions:DohProviders.kt$eu.kanade.tachiyomi.network.DohProviders.kt TooManyFunctions:DownloadCache.kt$DownloadCache TooManyFunctions:DownloadManager.kt$DownloadManager @@ -1123,10 +2021,15 @@ TooManyFunctions:DownloadProvider.kt$DownloadProvider TooManyFunctions:DownloadQueueScreenModel.kt$DownloadQueueScreenModel : ScreenModel TooManyFunctions:Downloader.kt$Downloader + TooManyFunctions:EHentai.kt$EHentai : HttpSourceMetadataSourceUrlImportableSourceNamespaceSourcePagePreviewSource + TooManyFunctions:EnhancedHttpSource.kt$EnhancedHttpSource : HttpSource TooManyFunctions:EpubFile.kt$EpubFile : Closeable TooManyFunctions:ExtensionLoader.kt$ExtensionLoader TooManyFunctions:ExtensionManager.kt$ExtensionManager TooManyFunctions:ExtensionsScreenModel.kt$ExtensionsScreenModel : StateScreenModel + TooManyFunctions:FeedSavedSearchRepository.kt$FeedSavedSearchRepository + TooManyFunctions:FeedSavedSearchRepositoryImpl.kt$FeedSavedSearchRepositoryImpl : FeedSavedSearchRepository + TooManyFunctions:FeedScreenModel.kt$FeedScreenModel : StateScreenModel TooManyFunctions:HttpSource.kt$HttpSource : CatalogueSource TooManyFunctions:ImageUtil.kt$ImageUtil TooManyFunctions:Kavita.kt$Kavita : BaseTrackerEnhancedTracker @@ -1134,36 +2037,61 @@ TooManyFunctions:Komga.kt$Komga : BaseTrackerEnhancedTracker TooManyFunctions:LibraryPreferences.kt$LibraryPreferences TooManyFunctions:LibraryScreenModel.kt$LibraryScreenModel : StateScreenModel + TooManyFunctions:LibraryUpdateJob.kt$LibraryUpdateJob : CoroutineWorker TooManyFunctions:LocalSource.kt$LocalSource : CatalogueSourceUnmeteredSource + TooManyFunctions:Logging.kt$exh.log.Logging.kt TooManyFunctions:MangaCoverFetcher.kt$MangaCoverFetcher : Fetcher + TooManyFunctions:MangaDex.kt$MangaDex : DelegatedHttpSourceMetadataSourceUrlImportableSourceFollowsSourceLoginSourceRandomMangaSourceNamespaceSource + TooManyFunctions:MangaDexAuthService.kt$MangaDexAuthService + TooManyFunctions:MangaDexService.kt$MangaDexService + TooManyFunctions:MangaMergeRepository.kt$MangaMergeRepository + TooManyFunctions:MangaMergeRepositoryImpl.kt$MangaMergeRepositoryImpl : MangaMergeRepository + TooManyFunctions:MangaMetadataRepository.kt$MangaMetadataRepository + TooManyFunctions:MangaMetadataRepositoryImpl.kt$MangaMetadataRepositoryImpl : MangaMetadataRepository TooManyFunctions:MangaRepository.kt$MangaRepository TooManyFunctions:MangaRepositoryImpl.kt$MangaRepositoryImpl : MangaRepository TooManyFunctions:MangaRestorer.kt$MangaRestorer + TooManyFunctions:MangaScreen.kt$MangaScreen : ScreenAssistContentScreen TooManyFunctions:MangaScreenModel.kt$MangaScreenModel : StateScreenModel + TooManyFunctions:MangaType.kt$exh.util.MangaType.kt TooManyFunctions:MangaUpdates.kt$MangaUpdates : BaseTrackerDeletableTracker + TooManyFunctions:MdList.kt$MdList : BaseTracker + TooManyFunctions:MdUtil.kt$MdUtil$Companion + TooManyFunctions:MergedSource.kt$MergedSource : HttpSource + TooManyFunctions:MigrationListScreenModel.kt$MigrationListScreenModel : ScreenModel TooManyFunctions:MyAnimeList.kt$MyAnimeList : BaseTrackerDeletableTracker TooManyFunctions:MyAnimeListApi.kt$MyAnimeListApi TooManyFunctions:NotificationReceiver.kt$NotificationReceiver : BroadcastReceiver TooManyFunctions:NotificationReceiver.kt$NotificationReceiver$Companion + TooManyFunctions:PagePreviewFetcher.kt$PagePreviewFetcher : Fetcher TooManyFunctions:PagerPageHolder.kt$PagerPageHolder : ReaderPageImageViewPositionableView TooManyFunctions:PagerViewer.kt$PagerViewer : Viewer TooManyFunctions:ParsedHttpSource.kt$ParsedHttpSource : HttpSource + TooManyFunctions:RaisedSearchMetadata.kt$RaisedSearchMetadata TooManyFunctions:ReaderActivity.kt$ReaderActivity : BaseActivity TooManyFunctions:ReaderPageImageView.kt$ReaderPageImageView : FrameLayout TooManyFunctions:ReaderPreferences.kt$ReaderPreferences TooManyFunctions:ReaderViewModel.kt$ReaderViewModel : ViewModel + TooManyFunctions:SecurityPreferences.kt$SecurityPreferences + TooManyFunctions:SettingsAdvancedScreen.kt$SettingsAdvancedScreen : SearchableSettings + TooManyFunctions:SettingsEhScreen.kt$SettingsEhScreen : SearchableSettings TooManyFunctions:SettingsItems.kt$tachiyomi.presentation.core.components.SettingsItems.kt + TooManyFunctions:SettingsReaderScreen.kt$SettingsReaderScreen : SearchableSettings TooManyFunctions:SharedPreferencesDataStore.kt$SharedPreferencesDataStore : PreferenceDataStore TooManyFunctions:Shikimori.kt$Shikimori : BaseTrackerDeletableTracker + TooManyFunctions:SourceFeedScreenModel.kt$SourceFeedScreenModel : StateScreenModel TooManyFunctions:SourcePreferences.kt$SourcePreferences TooManyFunctions:Suwayomi.kt$Suwayomi : BaseTrackerEnhancedTracker TooManyFunctions:Tracker.kt$Tracker + TooManyFunctions:UiPreferences.kt$UiPreferences + TooManyFunctions:UnsortedPreferences.kt$UnsortedPreferences TooManyFunctions:UpdatesScreenModel.kt$UpdatesScreenModel : StateScreenModel TooManyFunctions:WebtoonPageHolder.kt$WebtoonPageHolder : WebtoonBaseHolder TooManyFunctions:WebtoonRecyclerView.kt$WebtoonRecyclerView : RecyclerView TooManyFunctions:WebtoonViewer.kt$WebtoonViewer : Viewer TopLevelPropertyNaming:App.kt$private const val ACTION_DISABLE_INCOGNITO_MODE = "tachi.action.DISABLE_INCOGNITO_MODE" TopLevelPropertyNaming:AppBar.kt$const val SEARCH_DEBOUNCE_MILLIS = 250L + TopLevelPropertyNaming:AppModule.kt$// SY --> private const val LEGACY_DATABASE_NAME = "tachiyomi.db" TopLevelPropertyNaming:BackupCreateJob.kt$private const val IS_AUTO_BACKUP_KEY = "is_auto_backup" // Boolean TopLevelPropertyNaming:BackupCreateJob.kt$private const val LOCATION_URI_KEY = "location_uri" // String TopLevelPropertyNaming:BackupCreateJob.kt$private const val OPTIONS_KEY = "options" // BooleanArray @@ -1172,6 +2100,14 @@ TopLevelPropertyNaming:BackupRestoreJob.kt$private const val LOCATION_URI_KEY = "location_uri" // String TopLevelPropertyNaming:BackupRestoreJob.kt$private const val OPTIONS_KEY = "options" // BooleanArray TopLevelPropertyNaming:BackupRestoreJob.kt$private const val SYNC_KEY = "sync" // Boolean + TopLevelPropertyNaming:CbzCrypto.kt$private const val ALIAS_CBZ = "cbzPw" + TopLevelPropertyNaming:CbzCrypto.kt$private const val ALIAS_SQL = "sqlPw" + TopLevelPropertyNaming:CbzCrypto.kt$private const val BLOCK_MODE = KeyProperties.BLOCK_MODE_CBC + TopLevelPropertyNaming:CbzCrypto.kt$private const val BUFFER_SIZE = 2048 + TopLevelPropertyNaming:CbzCrypto.kt$private const val CRYPTO_SETTINGS = "$ALGORITHM/$BLOCK_MODE/$PADDING" + TopLevelPropertyNaming:CbzCrypto.kt$private const val IV_SIZE = 16 + TopLevelPropertyNaming:CbzCrypto.kt$private const val KEY_SIZE = 256 + TopLevelPropertyNaming:CbzCrypto.kt$private const val SQL_PASSWORD_LENGTH = 32 TopLevelPropertyNaming:ChapterCache.kt$/** Application cache version. */ private const val PARAMETER_APP_VERSION = 1 TopLevelPropertyNaming:ChapterCache.kt$/** The maximum number of bytes this cache should use to store. */ private const val PARAMETER_CACHE_SIZE = 100L * 1024 * 1024 TopLevelPropertyNaming:ChapterCache.kt$/** The number of values per cache entry. Must be positive. */ private const val PARAMETER_VALUE_COUNT = 1 @@ -1181,7 +2117,6 @@ TopLevelPropertyNaming:ColorFilterPage.kt$private const val RED_MASK: Long = 0x00FF0000 TopLevelPropertyNaming:ComicInfo.kt$const val COMIC_INFO_FILE = "ComicInfo.xml" TopLevelPropertyNaming:DatabaseAdapter.kt$private const val LIST_OF_STRINGS_SEPARATOR = ", " - TopLevelPropertyNaming:DateExtensions.kt$private const val MILLISECONDS_IN_DAY = 86_400_000L TopLevelPropertyNaming:DisplayExtensions.kt$// make sure icons on the nav rail fit private const val TABLET_UI_MIN_SCREEN_WIDTH_LANDSCAPE_DP = 600 TopLevelPropertyNaming:DisplayExtensions.kt$// some tablets have screen width like 711dp = 1600px / 2.25 private const val TABLET_UI_MIN_SCREEN_WIDTH_PORTRAIT_DP = 700 TopLevelPropertyNaming:DisplayExtensions.kt$private const val TABLET_UI_REQUIRED_SCREEN_WIDTH_DP = 720 @@ -1206,6 +2141,7 @@ TopLevelPropertyNaming:LibraryUpdateNotifier.kt$private const val NOTIF_ICON_SIZE = 192 TopLevelPropertyNaming:LibraryUpdateNotifier.kt$private const val NOTIF_MAX_CHAPTERS = 5 TopLevelPropertyNaming:LibraryUpdateNotifier.kt$private const val NOTIF_TITLE_MAX_LEN = 45 + TopLevelPropertyNaming:LocalCoverManager.kt$private const val COVER_ARCHIVE_NAME = "cover.cbi" TopLevelPropertyNaming:LocalCoverManager.kt$private const val DEFAULT_COVER_NAME = "cover.jpg" TopLevelPropertyNaming:MainActivity.kt$// Splash screen private const val SPLASH_MIN_DURATION = 500 // ms TopLevelPropertyNaming:MainActivity.kt$private const val SPLASH_EXIT_ANIM_DURATION = 400L // ms @@ -1214,6 +2150,15 @@ TopLevelPropertyNaming:ReaderNavigationOverlayView.kt$private const val FADE_DURATION = 1000L TopLevelPropertyNaming:ReaderPageImageView.kt$private const val MAX_ZOOM_SCALE = 5F TopLevelPropertyNaming:ShizukuInstaller.kt$private const val SHIZUKU_PERMISSION_REQUEST_CODE = 14045 + TopLevelPropertyNaming:SourceIds.kt$// Lewd source IDs const val LEWD_SOURCE_SERIES = 6900L + TopLevelPropertyNaming:SourceIds.kt$const val EH_SOURCE_ID = LEWD_SOURCE_SERIES + 1 + TopLevelPropertyNaming:SourceIds.kt$const val EIGHTMUSES_SOURCE_ID = 1802675169972965535 + TopLevelPropertyNaming:SourceIds.kt$const val EXH_SOURCE_ID = LEWD_SOURCE_SERIES + 2 + TopLevelPropertyNaming:SourceIds.kt$const val HBROWSE_SOURCE_ID = 1401584337232758222 + TopLevelPropertyNaming:SourceIds.kt$const val MERGED_SOURCE_ID = LEWD_SOURCE_SERIES + 69 + TopLevelPropertyNaming:SourceIds.kt$const val PURURIN_SOURCE_ID = 2221515250486218861 + TopLevelPropertyNaming:SourceIds.kt$const val TSUMINO_SOURCE_ID = 6707338697138388238 + TopLevelPropertyNaming:StorageManager.kt$// SY --> private const val LOGS_PATH = "logs" TopLevelPropertyNaming:StorageManager.kt$private const val AUTOMATIC_BACKUPS_PATH = "autobackup" TopLevelPropertyNaming:StorageManager.kt$private const val DOWNLOADS_PATH = "downloads" TopLevelPropertyNaming:StorageManager.kt$private const val LOCAL_SOURCE_PATH = "local" @@ -1223,18 +2168,21 @@ TopLevelPropertyNaming:SuwayomiApi.kt$private const val LOGIN_TITLE = "Login (Basic Auth)" TopLevelPropertyNaming:SuwayomiApi.kt$private const val PASSWORD_DEFAULT = "" TopLevelPropertyNaming:SuwayomiApi.kt$private const val PASSWORD_TITLE = "Password (Basic Auth)" + TopLevelPropertyNaming:Version.kt$const val syDebugVersion: String = "0" TopLevelPropertyNaming:WebtoonAdapter.kt$/** * View holder type of a chapter page view. */ private const val PAGE_VIEW = 0 TopLevelPropertyNaming:WebtoonAdapter.kt$/** * View holder type of a chapter transition view. */ private const val TRANSITION_VIEW = 1 TopLevelPropertyNaming:WebtoonRecyclerView.kt$private const val ANIMATOR_DURATION_TIME = 200 TopLevelPropertyNaming:WebtoonRecyclerView.kt$private const val DEFAULT_RATE = 1f TopLevelPropertyNaming:WebtoonRecyclerView.kt$private const val MAX_SCALE_RATE = 3f TopLevelPropertyNaming:WebtoonRecyclerView.kt$private const val MIN_RATE = 0.5f - UnstableCollections:CategoryScreen.kt$List<Category> + TopLevelPropertyNaming:WhatsNewDialog.kt$private const val bullet = "\u2022" + TopLevelPropertyNaming:__Assets.kt$private var __AllAssets: ____KtList<ImageVector>? = null + TopLevelPropertyNaming:__EhAssets.kt$private var __AllAssets: ____KtList<ImageVector>? = null + TopLevelPropertyNaming:__Ehassets.kt$private var __AllAssets: ____KtList<ImageVector>? = null + TopLevelPropertyNaming:__Exh.kt$private var __AllAssets: ____KtList<ImageVector>? = null UnstableCollections:Commons.kt$List<Category> UnstableCollections:Commons.kt$Set<String> UnstableCollections:GlobalSearchCardRow.kt$List<Manga> - UnstableCollections:GlobalSearchScreen.kt$Map<CatalogueSource, SearchItemResult> - UnstableCollections:HistoryScreen.kt$List<HistoryUiModel> UnstableCollections:LibraryComfortableGrid.kt$List<LibraryItem> UnstableCollections:LibraryComfortableGrid.kt$List<LibraryManga> UnstableCollections:LibraryCompactGrid.kt$List<LibraryItem> @@ -1249,7 +2197,6 @@ UnstableCollections:MangaScreen.kt$List<ChapterList.Item> UnstableCollections:MultiSelectListPreferenceWidget.kt$Set<String> UnstableCollections:PreferenceScreen.kt$List<Preference> - UnstableCollections:ScanlatorFilterDialog.kt$Set<String> UnstableCollections:SettingsDownloadScreen.kt$SettingsDownloadScreen$List<Category> UnstableCollections:SettingsLibraryScreen.kt$SettingsLibraryScreen$List<Category> UnstableCollections:TrackInfoDialogHome.kt$List<TrackItem> @@ -1257,6 +2204,9 @@ UnstableCollections:TriStateListDialog.kt$List<T> UnstableCollections:UpdatesScreen.kt$List<UpdatesItem> UnstableCollections:WebViewScreenContent.kt$Map<String, String> + UnusedParameter:EditMangaDialog.kt$context: Context + UnusedParameter:MergedSource.kt$MergedSource$dedupe: Boolean = true + UnusedParameter:MergedSource.kt$MergedSource$editScanlators: Boolean = false UnusedParameter:UpdatesRepositoryImpl.kt$UpdatesRepositoryImpl$dateUpload: Long UnusedPrivateMember:AppThemePreferenceWidget.kt$@PreviewLightDark @Composable private fun AppThemesListPreview() UnusedPrivateMember:ChapterTransition.kt$@PreviewLightDark @Composable private fun TransitionTextLongTitlePreview() @@ -1267,6 +2217,7 @@ UnusedPrivateMember:CrashScreen.kt$@PreviewLightDark @Composable private fun CrashScreenPreview() UnusedPrivateMember:EmptyScreen.kt$@PreviewLightDark @Composable private fun NoActionPreview() UnusedPrivateMember:EmptyScreen.kt$@PreviewLightDark @Composable private fun WithActionPreview() + UnusedPrivateMember:ExhUtils.kt$@Composable @PreviewLightDark private fun ExhUtilsPreview() UnusedPrivateMember:GuidesStep.kt$@PreviewLightDark @Composable private fun GuidesStepPreview() UnusedPrivateMember:HistoryDialogs.kt$@PreviewLightDark @Composable private fun HistoryDeleteDialogPreview() UnusedPrivateMember:HistoryItem.kt$@PreviewLightDark @Composable private fun HistoryItemPreviews( @PreviewParameter(HistoryWithRelationsProvider::class) historyWithRelations: HistoryWithRelations, ) @@ -1280,18 +2231,25 @@ UnusedPrivateMember:PageIndicatorText.kt$@PreviewLightDark @Composable private fun PageIndicatorTextPreview() UnusedPrivateMember:PermissionStep.kt$PermissionStep$@Composable private fun SectionHeader( text: String, modifier: Modifier = Modifier, ) UnusedPrivateMember:ReadingModeSelectDialog.kt$@PreviewLightDark @Composable private fun DialogContentPreview() + UnusedPrivateMember:SettingsAdvancedScreen.kt$SettingsAdvancedScreen$@Composable private fun getDownloaderGroup(): Preference.PreferenceGroup UnusedPrivateMember:SwitchPreferenceWidget.kt$@PreviewLightDark @Composable private fun SwitchPreferenceWidgetPreview() UnusedPrivateMember:TextPreferenceWidget.kt$@PreviewLightDark @Composable private fun TextPreferenceWidgetPreview() UnusedPrivateMember:TrackInfoDialogHome.kt$@PreviewLightDark @Composable private fun TrackInfoDialogHomePreviews( @PreviewParameter(TrackInfoDialogHomePreviewProvider::class) content: @Composable () -> Unit, ) UnusedPrivateMember:TrackInfoDialogSelector.kt$@PreviewLightDark @Composable private fun TrackStatusSelectorPreviews() UnusedPrivateMember:TrackLogoIcon.kt$@PreviewLightDark @Composable private fun TrackLogoIconPreviews( @PreviewParameter(TrackLogoIconPreviewProvider::class) tracker: Tracker, ) UnusedPrivateMember:TrackerSearch.kt$@PreviewLightDark @Composable private fun TrackerSearchPreviews( @PreviewParameter(TrackerSearchPreviewProvider::class) content: @Composable () -> Unit, ) + UnusedPrivateProperty:ChapterCache.kt$/** The maximum number of bytes this cache should use to store. */ private const val PARAMETER_CACHE_SIZE = 100L * 1024 * 1024 + UnusedPrivateProperty:FavoritesSyncHelper.kt$FavoritesSyncHelper$i + UnusedPrivateProperty:FeedScreenModel.kt$FeedScreenModel$private val updateManga: UpdateManga = Injekt.get() + UnusedPrivateProperty:GalleryAdder.kt$GalleryAdder$i UnusedPrivateProperty:JavaScriptEngine.kt$JavaScriptEngine$context: Context UnusedPrivateProperty:LibraryUpdateJob.kt$LibraryUpdateJob.Companion$private const val MANGA_PER_SOURCE_QUEUE_WARNING_THRESHOLD = 60 - UnusedPrivateProperty:MainActivity.kt$MainActivity$private val sourcePreferences: SourcePreferences by injectLazy() - UnusedPrivateProperty:MainActivity.kt$MainActivity$private val uiPreferences: UiPreferences by injectLazy() UnusedPrivateProperty:MetadataUpdateJob.kt$MetadataUpdateJob.Companion$private const val MANGA_PER_SOURCE_QUEUE_WARNING_THRESHOLD = 60 + UnusedPrivateProperty:PageHandler.kt$PageHandler$private val mdList: MdList + UnusedPrivateProperty:PageHandler.kt$PageHandler$private val preferences: TrackPreferences + UnusedPrivateProperty:SourceFeedScreenModel.kt$SourceFeedScreenModel$private val updateManga: UpdateManga = Injekt.get() UseCheckOrError:CatalogueSource.kt$CatalogueSource$throw IllegalStateException("Not used") + UseCheckOrError:MainActivity.kt$MainActivity$throw IllegalStateException("Can only be called on main thread!") UseCheckOrError:MangaScreenModel.kt$MangaScreenModel$throw IllegalStateException() UseCheckOrError:PackageInstallerInstaller.kt$PackageInstallerInstaller$throw IllegalStateException() UseCheckOrError:ReadingMode.kt$ReadingMode.Companion$throw IllegalStateException("Preference value must be resolved: $preference") @@ -1299,6 +2257,7 @@ UseCheckOrError:Source.kt$Source$throw IllegalStateException("Not used") UseCheckOrError:SourceFilterDialog.kt$throw IllegalStateException("Unknown TriState state: $this") UseCheckOrError:TransactionContext.kt$TransactionElement$throw IllegalStateException("Transaction was never started or was already released") + UtilityClassWithPublicConstructor:MdUtil.kt$MdUtil VariableNaming:Chapter.kt$Chapter$var date_fetch: Long VariableNaming:Chapter.kt$Chapter$var last_modified: Long VariableNaming:Chapter.kt$Chapter$var last_page_read: Int @@ -1323,5 +2282,7 @@ VariableNaming:TrackSearch.kt$TrackSearch$var publishing_status: String = "" VariableNaming:TrackSearch.kt$TrackSearch$var publishing_type: String = "" VariableNaming:TrackSearch.kt$TrackSearch$var start_date: String = "" + Wrapping:CbzCrypto.kt$CbzCrypto$( + Wrapping:LibraryScreenModel.kt$LibraryScreenModel$-> diff --git a/core/common/build.gradle.kts b/core/common/build.gradle.kts index 9b23bda71..d3245202e 100644 --- a/core/common/build.gradle.kts +++ b/core/common/build.gradle.kts @@ -36,6 +36,7 @@ dependencies { implementation(libs.image.decoder) implementation(libs.unifile) + implementation(libs.bundles.archive) api(kotlinx.coroutines.core) api(kotlinx.serialization.json) diff --git a/core/common/src/main/kotlin/eu/kanade/tachiyomi/util/storage/CbzCrypto.kt b/core/common/src/main/kotlin/eu/kanade/tachiyomi/util/storage/CbzCrypto.kt index c1f442c99..1f8a3f503 100644 --- a/core/common/src/main/kotlin/eu/kanade/tachiyomi/util/storage/CbzCrypto.kt +++ b/core/common/src/main/kotlin/eu/kanade/tachiyomi/util/storage/CbzCrypto.kt @@ -3,25 +3,15 @@ package eu.kanade.tachiyomi.util.storage import android.security.keystore.KeyGenParameterSpec import android.security.keystore.KeyProperties import android.util.Base64 -import com.hippo.unifile.UniFile import eu.kanade.tachiyomi.core.security.SecurityPreferences -import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn -import logcat.LogPriority -import net.lingala.zip4j.ZipFile -import net.lingala.zip4j.exception.ZipException -import net.lingala.zip4j.io.inputstream.ZipInputStream -import net.lingala.zip4j.io.outputstream.ZipOutputStream -import net.lingala.zip4j.model.LocalFileHeader import net.lingala.zip4j.model.ZipParameters import net.lingala.zip4j.model.enums.AesKeyStrength import net.lingala.zip4j.model.enums.EncryptionMethod -import tachiyomi.core.common.util.system.ImageUtil -import tachiyomi.core.common.util.system.logcat import uy.kohesive.injekt.injectLazy import java.io.ByteArrayInputStream import java.io.ByteArrayOutputStream @@ -136,12 +126,15 @@ object CbzCrypto { } fun getDecryptedPasswordCbz(): CharArray { - return decrypt(securityPreferences.cbzPassword().get(), ALIAS_CBZ).toCharArray() + val encryptedPassword = securityPreferences.cbzPassword().get() + if (encryptedPassword.isBlank()) error("This archive is encrypted please set a password") + + return decrypt(encryptedPassword, ALIAS_CBZ).toCharArray() } private fun generateAndEncryptSqlPw() { val charPool: List = ('a'..'z') + ('A'..'Z') + ('0'..'9') - val password = (1..32).map { + val password = (1..SQL_PASSWORD_LENGTH).map { charPool[SecureRandom().nextInt(charPool.size)] }.joinToString("", transform = { it.toString() }) securityPreferences.sqlPassword().set(encrypt(password, encryptionCipherSql)) @@ -152,27 +145,6 @@ object CbzCrypto { return decrypt(securityPreferences.sqlPassword().get(), ALIAS_SQL).toByteArray() } - /** - * Function that returns true when the supplied password - * can Successfully decrypt the supplied zip archive - * not very elegant but this is the solution recommended by the maintainer for checking passwords - * a real password check will likely be implemented in the future though - */ - fun checkCbzPassword(zip4j: ZipFile, password: CharArray): Boolean { - try { - zip4j.setPassword(password) - zip4j.use { zip -> - zip.getInputStream(zip.fileHeaders.firstOrNull()) - } - return true - } catch (e: Exception) { - logcat(LogPriority.WARN) { - "Wrong CBZ archive password for: ${zip4j.file.name} in: ${zip4j.file.parentFile?.name}" - } - } - return false - } - fun isPasswordSet(): Boolean { return securityPreferences.cbzPassword().get().isNotEmpty() } @@ -228,133 +200,6 @@ object CbzCrypto { } return String(bytes).contains(DEFAULT_COVER_NAME, ignoreCase = true) } - - fun UniFile.isEncryptedZip(): Boolean { - return try { - val stream = ZipInputStream(this.openInputStream()) - stream.nextEntry - stream.close() - false - } catch (zipException: ZipException) { - if (zipException.type == ZipException.Type.WRONG_PASSWORD) { - true - } else throw zipException - } - } - - fun UniFile.testCbzPassword(): Boolean { - return try { - val stream = ZipInputStream(this.openInputStream()) - stream.setPassword(getDecryptedPasswordCbz()) - stream.nextEntry - stream.close() - true - } catch (zipException: ZipException) { - if (zipException.type == ZipException.Type.WRONG_PASSWORD) { - false - } else throw zipException - } - } - - fun UniFile.addStreamToZip(inputStream: InputStream, filename: String, password: CharArray? = null) { - val zipOutputStream = - if (password != null) ZipOutputStream(this.openOutputStream(), password) - else ZipOutputStream(this.openOutputStream()) - - val zipParameters = ZipParameters() - zipParameters.fileNameInZip = filename - - if (password != null) setZipParametersEncrypted(zipParameters) - zipOutputStream.putNextEntry(zipParameters) - - zipOutputStream.use { output -> - inputStream.use { input -> - input.copyTo(output) - } - } - } - - - fun UniFile.addFilesToZip(files: List, password: CharArray? = null) { - val zipOutputStream = - if (password != null) ZipOutputStream(this.openOutputStream(), password) - else ZipOutputStream(this.openOutputStream()) - - - files.forEach { - val zipParameters = ZipParameters() - if (password != null) setZipParametersEncrypted(zipParameters) - zipParameters.fileNameInZip = it.name - - zipOutputStream.putNextEntry(zipParameters) - - it.openInputStream().use { input -> - input.copyTo(zipOutputStream) - } - zipOutputStream.closeEntry() - } - zipOutputStream.close() - } - - fun UniFile.getZipInputStream(filename: String): InputStream? { - val zipInputStream = ZipInputStream(this.openInputStream()) - var fileHeader: LocalFileHeader? - - if (this.isEncryptedZip()) zipInputStream.setPassword(getDecryptedPasswordCbz()) - - try { - while (run { - fileHeader = zipInputStream.nextEntry - fileHeader != null - }) { - if (fileHeader?.fileName == filename) return zipInputStream - } - - } catch (zipException: ZipException) { - if (zipException.type == ZipException.Type.WRONG_PASSWORD) { - logcat(LogPriority.WARN) { - "Wrong CBZ archive password for: ${this.name} in: ${this.parentFile?.name}" - } - } else throw zipException - } - return null - } - fun UniFile.getCoverStreamFromZip(): InputStream? { - val zipInputStream = ZipInputStream(this.openInputStream()) - var fileHeader: LocalFileHeader? - val fileHeaderList: MutableList = mutableListOf() - - if (this.isEncryptedZip()) zipInputStream.setPassword(getDecryptedPasswordCbz()) - - try { - while (run { - fileHeader = zipInputStream.nextEntry - fileHeader != null - }) { - fileHeaderList.add(fileHeader) - } - - var coverHeader = fileHeaderList - .mapNotNull { it } - .sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) } - .find { !it.isDirectory && ImageUtil.isImage(it.fileName) } - - - val coverStream = coverHeader?.fileName?.let { this.getZipInputStream(it) } - if (coverStream != null) { - if (!ImageUtil.isImage(coverHeader?.fileName) { coverStream }) coverHeader = null - } - return coverHeader?.fileName?.let { getZipInputStream(it) } - - } catch (zipException: ZipException) { - if (zipException.type == ZipException.Type.WRONG_PASSWORD) { - logcat(LogPriority.WARN) { - "Wrong CBZ archive password for: ${this.name} in: ${this.parentFile?.name}" - } - return null - } else throw zipException - } - } } private const val BUFFER_SIZE = 2048 @@ -369,4 +214,6 @@ private const val CRYPTO_SETTINGS = "$ALGORITHM/$BLOCK_MODE/$PADDING" private const val KEYSTORE = "AndroidKeyStore" private const val ALIAS_CBZ = "cbzPw" private const val ALIAS_SQL = "sqlPw" + +private const val SQL_PASSWORD_LENGTH = 32 // SY <-- diff --git a/core/common/src/main/kotlin/eu/kanade/tachiyomi/util/storage/EpubFile.kt b/core/common/src/main/kotlin/eu/kanade/tachiyomi/util/storage/EpubFile.kt index a00ee69e7..8f2e1dcda 100644 --- a/core/common/src/main/kotlin/eu/kanade/tachiyomi/util/storage/EpubFile.kt +++ b/core/common/src/main/kotlin/eu/kanade/tachiyomi/util/storage/EpubFile.kt @@ -1,22 +1,36 @@ package eu.kanade.tachiyomi.util.storage +import android.content.Context +import android.os.Build +import com.hippo.unifile.UniFile +import org.apache.commons.compress.archivers.zip.ZipArchiveEntry +import org.apache.commons.compress.archivers.zip.ZipFile import org.jsoup.Jsoup import org.jsoup.nodes.Document +import tachiyomi.core.common.storage.UniFileTempFileManager +import tachiyomi.core.common.storage.openReadOnlyChannel +import uy.kohesive.injekt.injectLazy import java.io.Closeable import java.io.File import java.io.InputStream -import java.util.zip.ZipEntry -import java.util.zip.ZipFile /** * Wrapper over ZipFile to load files in epub format. */ -class EpubFile(file: File) : Closeable { +// SY --> +class EpubFile(file: UniFile, context: Context) : Closeable { + + private val tempFileManager: UniFileTempFileManager by injectLazy() /** * Zip file of this epub. */ - private val zip = ZipFile(file) + private val zip = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { + ZipFile(tempFileManager.createTempFile(file)) + } else { + ZipFile(file.openReadOnlyChannel(context)) + } + // SY <-- /** * Path separator used by this epub. @@ -33,14 +47,14 @@ class EpubFile(file: File) : Closeable { /** * Returns an input stream for reading the contents of the specified zip file entry. */ - fun getInputStream(entry: ZipEntry): InputStream { + fun getInputStream(entry: ZipArchiveEntry): InputStream { return zip.getInputStream(entry) } /** * Returns the zip file entry for the specified name, or null if not found. */ - fun getEntry(name: String): ZipEntry? { + fun getEntry(name: String): ZipArchiveEntry? { return zip.getEntry(name) } diff --git a/core/common/src/main/kotlin/tachiyomi/core/common/storage/UniFileExtensions.kt b/core/common/src/main/kotlin/tachiyomi/core/common/storage/UniFileExtensions.kt index 8bbd9b3a7..f99ad117c 100644 --- a/core/common/src/main/kotlin/tachiyomi/core/common/storage/UniFileExtensions.kt +++ b/core/common/src/main/kotlin/tachiyomi/core/common/storage/UniFileExtensions.kt @@ -1,6 +1,21 @@ package tachiyomi.core.common.storage +import android.content.Context +import android.os.ParcelFileDescriptor import com.hippo.unifile.UniFile +import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder +import eu.kanade.tachiyomi.util.storage.CbzCrypto +import logcat.LogPriority +import net.lingala.zip4j.exception.ZipException +import net.lingala.zip4j.io.inputstream.ZipInputStream +import net.lingala.zip4j.io.outputstream.ZipOutputStream +import net.lingala.zip4j.model.LocalFileHeader +import net.lingala.zip4j.model.ZipParameters +import tachiyomi.core.common.util.system.ImageUtil +import tachiyomi.core.common.util.system.logcat +import java.io.File +import java.io.InputStream +import java.nio.channels.FileChannel val UniFile.extension: String? get() = name?.substringAfterLast('.') @@ -10,3 +25,201 @@ val UniFile.nameWithoutExtension: String? val UniFile.displayablePath: String get() = filePath ?: uri.toString() + +fun UniFile.openReadOnlyChannel(context: Context): FileChannel { + return ParcelFileDescriptor.AutoCloseInputStream(context.contentResolver.openFileDescriptor(uri, "r")).channel +// SY --> +} + +fun UniFile.isEncryptedZip(): Boolean { + return try { + val stream = ZipInputStream(this.openInputStream()) + stream.nextEntry + stream.close() + false + } catch (zipException: ZipException) { + if (zipException.type == ZipException.Type.WRONG_PASSWORD) { + true + } else { + throw zipException + } + } +} + +fun UniFile.testCbzPassword(): Boolean { + return try { + val stream = ZipInputStream(this.openInputStream()) + stream.setPassword(CbzCrypto.getDecryptedPasswordCbz()) + stream.nextEntry + stream.close() + true + } catch (zipException: ZipException) { + if (zipException.type == ZipException.Type.WRONG_PASSWORD) { + false + } else { + throw zipException + } + } +} + +fun UniFile.addStreamToZip(inputStream: InputStream, filename: String, password: CharArray? = null) { + val zipOutputStream = + if (password != null) { + ZipOutputStream(this.openOutputStream(), password) + } else { + ZipOutputStream(this.openOutputStream()) + } + + val zipParameters = ZipParameters() + zipParameters.fileNameInZip = filename + + if (password != null) CbzCrypto.setZipParametersEncrypted(zipParameters) + zipOutputStream.putNextEntry(zipParameters) + + zipOutputStream.use { output -> + inputStream.use { input -> + input.copyTo(output) + } + } +} + +/** + * Unzips encrypted or unencrypted zip files using zip4j. + * The caller is responsible to ensure, that the file this is called from is a zip archive + */ +fun UniFile.unzip(destination: File, onlyCopyImages: Boolean = false) { + destination.mkdirs() + if (!destination.isDirectory) return + + val zipInputStream = ZipInputStream(this.openInputStream()) + var fileHeader: LocalFileHeader? + + if (this.isEncryptedZip()) { + zipInputStream.setPassword(CbzCrypto.getDecryptedPasswordCbz()) + } + try { + while ( + run { + fileHeader = zipInputStream.nextEntry + fileHeader != null + } + ) { + val tmpFile = File("${destination.absolutePath}/${fileHeader!!.fileName}") + + if (onlyCopyImages) { + if (!fileHeader!!.isDirectory && ImageUtil.isImage(fileHeader!!.fileName)) { + tmpFile.createNewFile() + tmpFile.outputStream().buffered().use { tmpOut -> + zipInputStream.buffered().copyTo(tmpOut) + } + } + } else { + if (!fileHeader!!.isDirectory && ImageUtil.isImage(fileHeader!!.fileName)) { + tmpFile.createNewFile() + tmpFile + .outputStream() + .buffered() + .use { zipInputStream.buffered().copyTo(it) } + } + } + } + zipInputStream.close() + } catch (zipException: ZipException) { + if (zipException.type == ZipException.Type.WRONG_PASSWORD) { + logcat(LogPriority.WARN) { + "Wrong CBZ archive password for: ${this.name} in: ${this.parentFile?.name}" + } + } else { + throw zipException + } + } +} + +fun UniFile.addFilesToZip(files: List, password: CharArray? = null) { + val zipOutputStream = + if (password != null) { + ZipOutputStream(this.openOutputStream(), password) + } else { + ZipOutputStream(this.openOutputStream()) + } + + files.forEach { + val zipParameters = ZipParameters() + if (password != null) CbzCrypto.setZipParametersEncrypted(zipParameters) + zipParameters.fileNameInZip = it.name + + zipOutputStream.putNextEntry(zipParameters) + + it.openInputStream().use { input -> + input.copyTo(zipOutputStream) + } + zipOutputStream.closeEntry() + } + zipOutputStream.close() +} + +fun UniFile.getZipInputStream(filename: String): InputStream? { + val zipInputStream = ZipInputStream(this.openInputStream()) + var fileHeader: LocalFileHeader? + + if (this.isEncryptedZip()) zipInputStream.setPassword(CbzCrypto.getDecryptedPasswordCbz()) + + try { + while ( + run { + fileHeader = zipInputStream.nextEntry + fileHeader != null + } + ) { + if (fileHeader?.fileName == filename) return zipInputStream + } + } catch (zipException: ZipException) { + if (zipException.type == ZipException.Type.WRONG_PASSWORD) { + logcat(LogPriority.WARN) { + "Wrong CBZ archive password for: ${this.name} in: ${this.parentFile?.name}" + } + } else { + throw zipException + } + } + return null +} + +fun UniFile.getCoverStreamFromZip(): InputStream? { + val zipInputStream = ZipInputStream(this.openInputStream()) + var fileHeader: LocalFileHeader? + val fileHeaderList: MutableList = mutableListOf() + + if (this.isEncryptedZip()) zipInputStream.setPassword(CbzCrypto.getDecryptedPasswordCbz()) + + try { + while ( + run { + fileHeader = zipInputStream.nextEntry + fileHeader != null + } + ) { + fileHeaderList.add(fileHeader) + } + var coverHeader = fileHeaderList + .mapNotNull { it } + .sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) } + .find { !it.isDirectory && ImageUtil.isImage(it.fileName) } + + val coverStream = coverHeader?.fileName?.let { this.getZipInputStream(it) } + if (coverStream != null) { + if (!ImageUtil.isImage(coverHeader?.fileName) { coverStream }) coverHeader = null + } + return coverHeader?.fileName?.let { getZipInputStream(it) } + } catch (zipException: ZipException) { + if (zipException.type == ZipException.Type.WRONG_PASSWORD) { + logcat(LogPriority.WARN) { + "Wrong CBZ archive password for: ${this.name} in: ${this.parentFile?.name}" + } + return null + } else { + throw zipException + } + } +} +// SY <-- diff --git a/core/common/src/main/kotlin/tachiyomi/core/common/util/system/ImageUtil.kt b/core/common/src/main/kotlin/tachiyomi/core/common/util/system/ImageUtil.kt index 6f207afc8..053a54b41 100644 --- a/core/common/src/main/kotlin/tachiyomi/core/common/util/system/ImageUtil.kt +++ b/core/common/src/main/kotlin/tachiyomi/core/common/util/system/ImageUtil.kt @@ -26,8 +26,6 @@ import androidx.core.graphics.red import androidx.exifinterface.media.ExifInterface import com.hippo.unifile.UniFile import logcat.LogPriority -import net.lingala.zip4j.ZipFile -import net.lingala.zip4j.model.FileHeader import tachiyomi.decoder.Format import tachiyomi.decoder.ImageDecoder import java.io.BufferedInputStream @@ -133,20 +131,8 @@ object ImageUtil { * * @return true if the width is greater than the height */ - fun isWideImage( - imageStream: BufferedInputStream, - // SY --> - zip4jFile: ZipFile?, - zip4jEntry: FileHeader?, - // SY <-- - ): Boolean { - val options = extractImageOptions( - imageStream, - // SY --> - zip4jFile, - zip4jEntry, - // SY <-- - ) + fun isWideImage(imageStream: BufferedInputStream): Boolean { + val options = extractImageOptions(imageStream) return options.outWidth > options.outHeight } @@ -271,19 +257,9 @@ object ImageUtil { * * @return true if the height:width ratio is greater than 3. */ - private fun isTallImage( - imageStream: InputStream, - // SY --> - zip4jFile: ZipFile?, - zip4jEntry: FileHeader?, - // SY <-- - ): Boolean { + private fun isTallImage(imageStream: InputStream): Boolean { val options = extractImageOptions( imageStream, - // SY --> - zip4jFile, - zip4jEntry, - // SY <-- resetAfterExtraction = false, ) @@ -297,18 +273,9 @@ object ImageUtil { tmpDir: UniFile, imageFile: UniFile, filenamePrefix: String, - // SY --> - zip4jFile: ZipFile?, - zip4jEntry: FileHeader?, - // SY <-- ): Boolean { - if (isAnimatedAndSupported(imageFile.openInputStream()) || !isTallImage( - imageFile.openInputStream(), - // SY --> - zip4jFile, - zip4jEntry, - // SY <-- - ) + if (isAnimatedAndSupported(imageFile.openInputStream()) || + !isTallImage(imageFile.openInputStream()) ) { return true } @@ -321,10 +288,6 @@ object ImageUtil { val options = extractImageOptions( imageFile.openInputStream(), - // SY --> - zip4jFile, - zip4jEntry, - // SY <-- resetAfterExtraction = false, ).apply { inJustDecodeBounds = false @@ -641,17 +604,8 @@ object ImageUtil { */ private fun extractImageOptions( imageStream: InputStream, - // SY --> - zip4jFile: ZipFile?, - zip4jEntry: FileHeader?, - // SY <-- resetAfterExtraction: Boolean = true, ): BitmapFactory.Options { - // SY --> - // zip4j does currently not support mark() and reset() - if (zip4jFile != null && zip4jEntry != null) return extractImageOptionsZip4j(zip4jFile, zip4jEntry) - // SY <-- - imageStream.mark(Int.MAX_VALUE) val imageBytes = imageStream.readBytes() @@ -661,16 +615,6 @@ object ImageUtil { return options } - // SY --> - private fun extractImageOptionsZip4j(zip4jFile: ZipFile?, zip4jEntry: FileHeader?): BitmapFactory.Options { - zip4jFile?.getInputStream(zip4jEntry).use { imageStream -> - val imageBytes = imageStream?.readBytes() - val options = BitmapFactory.Options().apply { inJustDecodeBounds = true } - imageBytes?.size?.let { BitmapFactory.decodeByteArray(imageBytes, 0, it, options) } - return options - } - } - /** * Creates random exif metadata used as padding to make * the size of files inside CBZ archives unique diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ec8557f6a..a3a8ff728 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -33,6 +33,7 @@ jsoup = "org.jsoup:jsoup:1.17.2" disklrucache = "com.jakewharton:disklrucache:2.0.2" unifile = "com.github.tachiyomiorg:unifile:7c257e1c64" +common-compress = "org.apache.commons:commons-compress:1.26.0" junrar = "com.github.junrar:junrar:7.5.5" zip4j = "net.lingala.zip4j:zip4j:2.11.5" @@ -111,6 +112,7 @@ google-api-client-oauth = "com.google.oauth-client:google-oauth-client:1.34.1" [bundles] acra = ["acra-http", "acra-scheduler"] +archive = ["common-compress", "junrar"] okhttp = ["okhttp-core", "okhttp-logging", "okhttp-brotli", "okhttp-dnsoverhttps"] js-engine = ["quickjs-android"] sqlite = ["sqlite-framework", "sqlite-ktx", "sqlite-android"] diff --git a/i18n-sy/src/commonMain/resources/MR/base/strings.xml b/i18n-sy/src/commonMain/resources/MR/base/strings.xml index a4dcc92d4..773aa60ac 100644 --- a/i18n-sy/src/commonMain/resources/MR/base/strings.xml +++ b/i18n-sy/src/commonMain/resources/MR/base/strings.xml @@ -324,9 +324,13 @@ Center margin type Insert spacer to accommodate deadspace on foldable devices. - - Cache images inside CBZ/CBR archives on disk - Temporarily copy images inside comic book archives to disk while reading \nMay improve reader performance + + Load from file + Load into memory + Copy to disk + Archive reader mode + The way in which images inside archives, such as CBZ or CBR, are being loaded + diff --git a/source-local/build.gradle.kts b/source-local/build.gradle.kts index 727d2a9c3..9409b5207 100644 --- a/source-local/build.gradle.kts +++ b/source-local/build.gradle.kts @@ -15,7 +15,7 @@ kotlin { // SY <-- implementation(libs.unifile) - implementation(libs.junrar) + implementation(libs.bundles.archive) // SY --> implementation(libs.zip4j) // SY <-- diff --git a/source-local/src/androidMain/kotlin/tachiyomi/source/local/LocalSource.kt b/source-local/src/androidMain/kotlin/tachiyomi/source/local/LocalSource.kt index 6036ab6e3..2859a3d1b 100644 --- a/source-local/src/androidMain/kotlin/tachiyomi/source/local/LocalSource.kt +++ b/source-local/src/androidMain/kotlin/tachiyomi/source/local/LocalSource.kt @@ -1,6 +1,7 @@ package tachiyomi.source.local import android.content.Context +import android.os.Build import com.hippo.unifile.UniFile import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.Source @@ -11,10 +12,6 @@ import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder import eu.kanade.tachiyomi.util.storage.CbzCrypto -import eu.kanade.tachiyomi.util.storage.CbzCrypto.addStreamToZip -import eu.kanade.tachiyomi.util.storage.CbzCrypto.getCoverStreamFromZip -import eu.kanade.tachiyomi.util.storage.CbzCrypto.getZipInputStream -import eu.kanade.tachiyomi.util.storage.CbzCrypto.isEncryptedZip import eu.kanade.tachiyomi.util.storage.EpubFile import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll @@ -31,7 +28,11 @@ import tachiyomi.core.metadata.comicinfo.copyFromComicInfo import tachiyomi.core.metadata.comicinfo.getComicInfo import tachiyomi.core.metadata.tachiyomi.MangaDetails import tachiyomi.core.common.storage.UniFileTempFileManager +import tachiyomi.core.common.storage.addStreamToZip import tachiyomi.core.common.storage.extension +import tachiyomi.core.common.storage.getCoverStreamFromZip +import tachiyomi.core.common.storage.getZipInputStream +import tachiyomi.core.common.storage.isEncryptedZip import tachiyomi.core.common.storage.nameWithoutExtension import tachiyomi.core.common.util.lang.withIOContext import tachiyomi.core.common.util.system.ImageUtil @@ -268,7 +269,13 @@ actual class LocalSource( } } is Format.Rar -> { - JunrarArchive(tempFileManager.createTempFile(chapter)).use { rar -> + val archive = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { + JunrarArchive(tempFileManager.createTempFile(chapter)) + } else { + JunrarArchive(chapter.openInputStream()) + } + + archive.use { rar -> rar.fileHeaders.firstOrNull { it.fileName == COMIC_INFO_FILE }?.let { comicInfoFile -> rar.getInputStream(comicInfoFile).buffered().use { stream -> return copyComicInfoFile(stream, folder) @@ -330,7 +337,7 @@ actual class LocalSource( val format = Format.valueOf(chapterFile) if (format is Format.Epub) { - EpubFile(tempFileManager.createTempFile(format.file)).use { epub -> + EpubFile(format.file, context).use { epub -> epub.fillMetadata(manga, this) } } @@ -397,10 +404,15 @@ actual class LocalSource( format.file.isEncryptedZip() ) } - // SY <-- } is Format.Rar -> { - JunrarArchive(tempFileManager.createTempFile(format.file)).use { archive -> + val rarArchive = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { + JunrarArchive(tempFileManager.createTempFile(format.file)) + } else { + JunrarArchive(format.file.openInputStream()) + } + rarArchive.use { archive -> + // SY <-- val entry = archive.fileHeaders .sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) } .find { !it.isDirectory && ImageUtil.isImage(it.fileName) { archive.getInputStream(it) } } @@ -409,15 +421,17 @@ actual class LocalSource( } } is Format.Epub -> { - EpubFile(tempFileManager.createTempFile(format.file)).use { epub -> - val entry = epub.getImagesFromPages() - .firstOrNull() - ?.let { epub.getEntry(it) } + // SY --> + EpubFile(format.file, context).use { epub -> + // SY <-- + val entry = epub.getImagesFromPages() + .firstOrNull() + ?.let { epub.getEntry(it) } - entry?.let { coverManager.update(manga, epub.getInputStream(it)) } + entry?.let { coverManager.update(manga, epub.getInputStream(it)) } + } } } - } } catch (e: Throwable) { logcat(LogPriority.ERROR, e) { "Error updating cover for ${manga.title}" } null diff --git a/source-local/src/androidMain/kotlin/tachiyomi/source/local/image/LocalCoverManager.kt b/source-local/src/androidMain/kotlin/tachiyomi/source/local/image/LocalCoverManager.kt index 0738ce1c4..390e901dc 100644 --- a/source-local/src/androidMain/kotlin/tachiyomi/source/local/image/LocalCoverManager.kt +++ b/source-local/src/androidMain/kotlin/tachiyomi/source/local/image/LocalCoverManager.kt @@ -4,7 +4,7 @@ import android.content.Context import com.hippo.unifile.UniFile import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.util.storage.CbzCrypto -import eu.kanade.tachiyomi.util.storage.CbzCrypto.addStreamToZip +import tachiyomi.core.common.storage.addStreamToZip import eu.kanade.tachiyomi.util.storage.DiskUtil import tachiyomi.core.common.storage.nameWithoutExtension import tachiyomi.core.common.util.system.ImageUtil From d5ba5a14b482829e4696c56d50d5016c6b79284e Mon Sep 17 00:00:00 2001 From: Jobobby04 Date: Sat, 16 Mar 2024 12:21:26 -0400 Subject: [PATCH 019/106] Move strings to SY strings --- .../more/settings/screen/SettingsLibraryScreen.kt | 6 ++++-- .../more/settings/screen/SettingsReaderScreen.kt | 6 ++++-- i18n-sy/src/commonMain/resources/MR/base/strings.xml | 4 ++++ i18n/src/commonMain/resources/MR/base/strings.xml | 5 ----- 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsLibraryScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsLibraryScreen.kt index 6e9c1c53e..00c657a05 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsLibraryScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsLibraryScreen.kt @@ -235,11 +235,13 @@ object SettingsLibraryScreen : SearchableSettings { pref = libraryPreferences.newShowUpdatesCount(), title = stringResource(MR.strings.pref_library_update_show_tab_badge), ), + // SY --> Preference.PreferenceItem.SwitchPreference( pref = libraryPreferences.libraryReadDuplicateChapters(), - title = stringResource(MR.strings.pref_library_mark_duplicate_chapters), - subtitle = stringResource(MR.strings.pref_library_mark_duplicate_chapters_summary), + title = stringResource(SYMR.strings.pref_library_mark_duplicate_chapters), + subtitle = stringResource(SYMR.strings.pref_library_mark_duplicate_chapters_summary), ), + // SY <-- ), ) } diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsReaderScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsReaderScreen.kt index 49788eb7a..2be2d8b75 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsReaderScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsReaderScreen.kt @@ -178,11 +178,13 @@ object SettingsReaderScreen : SearchableSettings { pref = readerPreferences.skipDupe(), title = stringResource(MR.strings.pref_skip_dupe_chapters), ), + // SY --> Preference.PreferenceItem.SwitchPreference( pref = readerPreferences.markReadDupe(), - title = stringResource(MR.strings.pref_mark_read_dupe_chapters), - subtitle = stringResource(MR.strings.pref_mark_read_dupe_chapters_summary), + title = stringResource(SYMR.strings.pref_mark_read_dupe_chapters), + subtitle = stringResource(SYMR.strings.pref_mark_read_dupe_chapters_summary), ), + // SY <-- Preference.PreferenceItem.SwitchPreference( pref = readerPreferences.alwaysShowChapterTransition(), title = stringResource(MR.strings.pref_always_show_chapter_transition), diff --git a/i18n-sy/src/commonMain/resources/MR/base/strings.xml b/i18n-sy/src/commonMain/resources/MR/base/strings.xml index 773aa60ac..e62b17496 100644 --- a/i18n-sy/src/commonMain/resources/MR/base/strings.xml +++ b/i18n-sy/src/commonMain/resources/MR/base/strings.xml @@ -183,6 +183,10 @@ Always launch global updates Launch global updates only for ungrouped, category updates for others Launch category updates all the time + Mark duplicate chapters as read + Mark duplicate chapters as read after reading + Mark new duplicate chapters as read + Automatically mark new chapters as read if it has been read before Hide Feed tab diff --git a/i18n/src/commonMain/resources/MR/base/strings.xml b/i18n/src/commonMain/resources/MR/base/strings.xml index d1f945ed3..4945edab9 100644 --- a/i18n/src/commonMain/resources/MR/base/strings.xml +++ b/i18n/src/commonMain/resources/MR/base/strings.xml @@ -294,9 +294,6 @@ Automatically refresh metadata Check for new cover and details when updating library - Mark new duplicate chapters as read - Automatically mark new chapters as read if it has been read before - Default category Always ask Per-category settings for sort @@ -396,8 +393,6 @@ Skip chapters marked read Skip filtered chapters Skip duplicate chapters - Mark duplicate chapters as read - Mark duplicate chapters as read after reading Navigation Volume keys Invert volume keys From bd9f1c23e53fb4790af8eb96f6bbbb29021afabd Mon Sep 17 00:00:00 2001 From: Jobobby04 Date: Sat, 16 Mar 2024 12:22:18 -0400 Subject: [PATCH 020/106] Update Detekt baseline --- .../sync/service/GoogleDriveSyncService.kt | 7 +- .../data/sync/service/SyncService.kt | 24 ++-- .../kanade/tachiyomi/ui/library/LibraryTab.kt | 3 +- config/detekt/baseline.xml | 108 +++++++++--------- 4 files changed, 72 insertions(+), 70 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/sync/service/GoogleDriveSyncService.kt b/app/src/main/java/eu/kanade/tachiyomi/data/sync/service/GoogleDriveSyncService.kt index 7beb6d512..3cd705652 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/sync/service/GoogleDriveSyncService.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/sync/service/GoogleDriveSyncService.kt @@ -469,9 +469,10 @@ class GoogleDriveService(private val context: Context) { } /** - * Handles the authorization code returned after the user has granted the application permission to access their Google Drive account. - * It obtains the access token and refresh token using the authorization code, saves the tokens to the SyncPreferences, - * sets up the Google Drive service using the obtained tokens, and initializes the service. + * Handles the authorization code returned after the user has granted the application permission to access their + * Google Drive account. + * It obtains the access token and refresh token using the authorization code, saves the tokens to the + * SyncPreferences, sets up the Google Drive service using the obtained tokens, and initializes the service. * @param authorizationCode The authorization code obtained from the OAuthCallbackServer. * @param activity The current activity. * @param onSuccess A callback function to be called on successful authorization. diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/sync/service/SyncService.kt b/app/src/main/java/eu/kanade/tachiyomi/data/sync/service/SyncService.kt index cf61869fa..c99dd902c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/sync/service/SyncService.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/sync/service/SyncService.kt @@ -70,7 +70,8 @@ abstract class SyncService( val mergedCategoriesList = mergeCategoriesLists(localSyncData.backup?.backupCategories, remoteSyncData.backup?.backupCategories) - val mergedSourcesList = mergeSourcesLists(localSyncData.backup?.backupSources, remoteSyncData.backup?.backupSources) + val mergedSourcesList = + mergeSourcesLists(localSyncData.backup?.backupSources, remoteSyncData.backup?.backupSources) val mergedPreferencesList = mergePreferencesLists(localSyncData.backup?.backupPreferences, remoteSyncData.backup?.backupPreferences) val mergedSourcePreferencesList = mergeSourcePreferencesLists( @@ -85,7 +86,6 @@ abstract class SyncService( ) // SY <-- - // Create the merged Backup object val mergedBackup = Backup( backupManga = mergedMangaList, @@ -150,10 +150,14 @@ abstract class SyncService( local != null && remote != null -> { // Compare versions to decide which manga to keep if (local.version >= remote.version) { - logcat(LogPriority.DEBUG, logTag) { "Keeping local version of ${local.title} with merged chapters." } + logcat(LogPriority.DEBUG, logTag) { + "Keeping local version of ${local.title} with merged chapters." + } local.copy(chapters = mergeChapters(local.chapters, remote.chapters)) } else { - logcat(LogPriority.DEBUG, logTag) { "Keeping remote version of ${remote.title} with merged chapters." } + logcat(LogPriority.DEBUG, logTag) { + "Keeping remote version of ${remote.title} with merged chapters." + } remote.copy(chapters = mergeChapters(local.chapters, remote.chapters)) } } @@ -227,7 +231,11 @@ abstract class SyncService( } localChapter != null && remoteChapter != null -> { // Use version number to decide which chapter to keep - val chosenChapter = if (localChapter.version >= remoteChapter.version) localChapter else remoteChapter + val chosenChapter = if (localChapter.version >= remoteChapter.version) { + localChapter + } else { + remoteChapter + } logcat(LogPriority.DEBUG, logTag) { "Merging chapter: ${chosenChapter.name}. Chosen version from: ${ if (localChapter.version >= remoteChapter.version) "Local" else "Remote" @@ -427,7 +435,8 @@ abstract class SyncService( } localSourcePreference != null && remoteSourcePreference != null -> { // Merge the individual preferences within the source preferences - val mergedPrefs = mergeIndividualPreferences(localSourcePreference.prefs, remoteSourcePreference.prefs) + val mergedPrefs = + mergeIndividualPreferences(localSourcePreference.prefs, remoteSourcePreference.prefs) BackupSourcePreferences(sourceKey, mergedPrefs) } else -> null @@ -449,8 +458,6 @@ abstract class SyncService( return mergedPrefsMap.values.toList() } - - // SY --> private fun mergeSavedSearchesLists( localSearches: List?, @@ -507,5 +514,4 @@ abstract class SyncService( return mergedSearches } // SY <-- - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryTab.kt index 731624c08..e666a3beb 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryTab.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryTab.kt @@ -41,8 +41,8 @@ import eu.kanade.presentation.more.onboarding.GETTING_STARTED_URL import eu.kanade.presentation.util.Tab import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.library.LibraryUpdateJob -import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.PreMigrationScreen import eu.kanade.tachiyomi.data.sync.SyncDataJob +import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.PreMigrationScreen import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchScreen import eu.kanade.tachiyomi.ui.category.CategoryScreen import eu.kanade.tachiyomi.ui.home.HomeScreen @@ -52,7 +52,6 @@ import eu.kanade.tachiyomi.ui.reader.ReaderActivity import eu.kanade.tachiyomi.util.system.toast import exh.favorites.FavoritesSyncStatus import exh.source.MERGED_SOURCE_ID -import eu.kanade.tachiyomi.util.system.toast import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.collectLatest diff --git a/config/detekt/baseline.xml b/config/detekt/baseline.xml index f185da8d9..3ee4081b7 100644 --- a/config/detekt/baseline.xml +++ b/config/detekt/baseline.xml @@ -14,7 +14,6 @@ ArgumentListWrapping:BiometricTimesScreen.kt$BiometricTimesScreen$(SYMR.strings.delete_time_range_confirmation, dialog.timeRange.formattedString) ArgumentListWrapping:BiometricTimesScreen.kt$BiometricTimesScreen$(context) ArgumentListWrapping:BiometricTimesScreen.kt$BiometricTimesScreen$(if (startTime == null) SYMR.strings.biometric_lock_start_time.getString(context) else SYMR.strings.biometric_lock_end_time.getString(context)) - ArgumentListWrapping:ChapterLoader.kt$ChapterLoader$(chapter, manga, source, downloadManager, downloadProvider, tempFileManager) ArgumentListWrapping:DataSaver.kt$BandwidthHeroDataSaver$(imageUrl) ArgumentListWrapping:DataSaver.kt$WsrvNlDataSaver$(imageUrl) ArgumentListWrapping:DownloadPageLoader.kt$DownloadPageLoader$(dbChapter.name, dbChapter.scanlator, /* SY --> */ manga.ogTitle /* SY <-- */, source) @@ -305,6 +304,7 @@ CyclomaticComplexMethod:RateLimitInterceptor.kt$RateLimitInterceptor$override fun intercept(chain: Interceptor.Chain): Response CyclomaticComplexMethod:ReaderActivity.kt$ReaderActivity$override fun onCreate(savedInstanceState: Bundle?) CyclomaticComplexMethod:ReaderActivity.kt$ReaderActivity$private fun initializeMenu() + CyclomaticComplexMethod:ReaderViewModel.kt$ReaderViewModel$private suspend fun updateChapterProgress( readerChapter: ReaderChapter, page: Page/* SY --> */, hasExtraPage: Boolean, /* SY <-- */ ) CyclomaticComplexMethod:Scaffold.kt$@Composable private fun ScaffoldLayout( fabPosition: FabPosition, topBar: @Composable () -> Unit, startBar: @Composable () -> Unit, content: @Composable (PaddingValues) -> Unit, snackbar: @Composable () -> Unit, fab: @Composable () -> Unit, contentWindowInsets: WindowInsets, bottomBar: @Composable () -> Unit, ) CyclomaticComplexMethod:SearchEngine.kt$SearchEngine$fun parseQuery(query: String, enableWildcard: Boolean = true) CyclomaticComplexMethod:SettingsEhScreen.kt$SettingsEhScreen$private fun getRelativeTimeFromNow(then: Duration): RelativeTime @@ -414,6 +414,7 @@ FunctionNaming:Requests.kt$fun DELETE( url: String, headers: Headers = DEFAULT_HEADERS, body: RequestBody = DEFAULT_BODY, cache: CacheControl = DEFAULT_CACHE_CONTROL, ): Request FunctionNaming:Requests.kt$fun GET( url: HttpUrl, headers: Headers = DEFAULT_HEADERS, cache: CacheControl = DEFAULT_CACHE_CONTROL, ): Request FunctionNaming:Requests.kt$fun GET( url: String, headers: Headers = DEFAULT_HEADERS, cache: CacheControl = DEFAULT_CACHE_CONTROL, ): Request + FunctionNaming:Requests.kt$fun PATCH( url: String, headers: Headers = DEFAULT_HEADERS, body: RequestBody = DEFAULT_BODY, cache: CacheControl = DEFAULT_CACHE_CONTROL, ): Request FunctionNaming:Requests.kt$fun POST( url: String, headers: Headers = DEFAULT_HEADERS, body: RequestBody = DEFAULT_BODY, cache: CacheControl = DEFAULT_CACHE_CONTROL, ): Request FunctionNaming:Requests.kt$fun PUT( url: String, headers: Headers = DEFAULT_HEADERS, body: RequestBody = DEFAULT_BODY, cache: CacheControl = DEFAULT_CACHE_CONTROL, ): Request FunctionParameterNaming:MangaScreen.kt$MangaScreen$manga_: Manga? @@ -427,11 +428,12 @@ ImplicitDefaultLocale:MetadataUtil.kt$MetadataUtil$String.format("%.1f %sB", bytes / unit.toDouble().pow(exp.toDouble()), pre) ImplicitDefaultLocale:TimeRange.kt$TimeRange$String.format("%02d:%02d - %02d:%02d", startHour, startMinute, endHour, endMinute) ImportOrdering:Commands.kt$import org.gradle.api.Project import java.io.ByteArrayOutputStream import java.text.SimpleDateFormat import java.util.TimeZone import java.util.Date - ImportOrdering:LocalSource.kt$import android.content.Context import com.hippo.unifile.UniFile import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.UnmeteredSource import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder import eu.kanade.tachiyomi.util.storage.CbzCrypto import eu.kanade.tachiyomi.util.storage.CbzCrypto.addStreamToZip import eu.kanade.tachiyomi.util.storage.CbzCrypto.getCoverStreamFromZip import eu.kanade.tachiyomi.util.storage.CbzCrypto.getZipInputStream import eu.kanade.tachiyomi.util.storage.CbzCrypto.isEncryptedZip import eu.kanade.tachiyomi.util.storage.EpubFile import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.serialization.json.Json import kotlinx.serialization.json.decodeFromStream import kotlinx.serialization.json.encodeToStream import logcat.LogPriority import nl.adaptivity.xmlutil.AndroidXmlReader import nl.adaptivity.xmlutil.serialization.XML import tachiyomi.core.common.i18n.stringResource import tachiyomi.core.metadata.comicinfo.COMIC_INFO_FILE import tachiyomi.core.metadata.comicinfo.ComicInfo import tachiyomi.core.metadata.comicinfo.copyFromComicInfo import tachiyomi.core.metadata.comicinfo.getComicInfo import tachiyomi.core.metadata.tachiyomi.MangaDetails import tachiyomi.core.common.storage.UniFileTempFileManager import tachiyomi.core.common.storage.extension import tachiyomi.core.common.storage.nameWithoutExtension import tachiyomi.core.common.util.lang.withIOContext import tachiyomi.core.common.util.system.ImageUtil import tachiyomi.core.common.util.system.logcat import tachiyomi.domain.chapter.service.ChapterRecognition import tachiyomi.domain.manga.model.Manga import tachiyomi.i18n.MR import tachiyomi.source.local.filter.OrderBy import tachiyomi.source.local.image.LocalCoverManager import tachiyomi.source.local.io.Archive import tachiyomi.source.local.io.Format import tachiyomi.source.local.io.LocalSourceFileSystem import tachiyomi.source.local.metadata.fillMetadata import uy.kohesive.injekt.injectLazy import java.io.InputStream import java.nio.charset.StandardCharsets import kotlin.time.Duration.Companion.days import com.github.junrar.Archive as JunrarArchive import tachiyomi.domain.source.model.Source as DomainSource + ImportOrdering:LocalCoverManager.kt$import android.content.Context import com.hippo.unifile.UniFile import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.util.storage.CbzCrypto import tachiyomi.core.common.storage.addStreamToZip import eu.kanade.tachiyomi.util.storage.DiskUtil import tachiyomi.core.common.storage.nameWithoutExtension import tachiyomi.core.common.util.system.ImageUtil import tachiyomi.source.local.io.LocalSourceFileSystem import java.io.InputStream + ImportOrdering:LocalSource.kt$import android.content.Context import android.os.Build import com.hippo.unifile.UniFile import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.UnmeteredSource import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder import eu.kanade.tachiyomi.util.storage.CbzCrypto import eu.kanade.tachiyomi.util.storage.EpubFile import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.serialization.json.Json import kotlinx.serialization.json.decodeFromStream import kotlinx.serialization.json.encodeToStream import logcat.LogPriority import nl.adaptivity.xmlutil.AndroidXmlReader import nl.adaptivity.xmlutil.serialization.XML import tachiyomi.core.common.i18n.stringResource import tachiyomi.core.metadata.comicinfo.COMIC_INFO_FILE import tachiyomi.core.metadata.comicinfo.ComicInfo import tachiyomi.core.metadata.comicinfo.copyFromComicInfo import tachiyomi.core.metadata.comicinfo.getComicInfo import tachiyomi.core.metadata.tachiyomi.MangaDetails import tachiyomi.core.common.storage.UniFileTempFileManager import tachiyomi.core.common.storage.addStreamToZip import tachiyomi.core.common.storage.extension import tachiyomi.core.common.storage.getCoverStreamFromZip import tachiyomi.core.common.storage.getZipInputStream import tachiyomi.core.common.storage.isEncryptedZip import tachiyomi.core.common.storage.nameWithoutExtension import tachiyomi.core.common.util.lang.withIOContext import tachiyomi.core.common.util.system.ImageUtil import tachiyomi.core.common.util.system.logcat import tachiyomi.domain.chapter.service.ChapterRecognition import tachiyomi.domain.manga.model.Manga import tachiyomi.i18n.MR import tachiyomi.source.local.filter.OrderBy import tachiyomi.source.local.image.LocalCoverManager import tachiyomi.source.local.io.Archive import tachiyomi.source.local.io.Format import tachiyomi.source.local.io.LocalSourceFileSystem import tachiyomi.source.local.metadata.fillMetadata import uy.kohesive.injekt.injectLazy import java.io.InputStream import java.nio.charset.StandardCharsets import kotlin.time.Duration.Companion.days import com.github.junrar.Archive as JunrarArchive import tachiyomi.domain.source.model.Source as DomainSource Indentation:BackupCreator.kt$BackupCreator$ + Indentation:LocalSource.kt$LocalSource$ Indentation:LocalesConfigPlugin.kt$ Indentation:PagerPageHolder.kt$PagerPageHolder$ - Indentation:ReaderPreferences.kt$ReaderPreferences$ InstanceOfCheckForException:AppUpdateDownloadJob.kt$AppUpdateDownloadJob$e is CancellationException InstanceOfCheckForException:AppUpdateDownloadJob.kt$AppUpdateDownloadJob$e is StreamResetException InstanceOfCheckForException:BackupRestoreJob.kt$BackupRestoreJob$e is CancellationException @@ -533,6 +535,7 @@ LongMethod:ReaderViewModel.kt$ReaderViewModel$suspend fun init(mangaId: Long, initialChapterId: Long /* SY --> */, page: Int?/* SY <-- */): Result<Boolean> LongMethod:SYDomainModule.kt$SYDomainModule$override fun InjektRegistrar.registerInjectables() LongMethod:SyncChaptersWithSource.kt$SyncChaptersWithSource$suspend fun await( rawSourceChapters: List<SChapter>, manga: Manga, source: Source, manualFetch: Boolean = false, fetchWindow: Pair<Long, Long> = Pair(0, 0), ): List<Chapter> + LongMethod:SyncManager.kt$SyncManager$suspend fun syncData() LongMethod:Tester.kt$Tester$@Test fun localFavoritesStorageTester(): Unit LongMethod:TrackStatus.kt$TrackStatus.Companion$fun parseTrackerStatus(trackerManager: TrackerManager, tracker: Long, status: Long): TrackStatus? LongMethod:Tsumino.kt$Tsumino$override suspend fun parseIntoMetadata(metadata: TsuminoSearchMetadata, input: Document) @@ -547,8 +550,6 @@ LongParameterList:HistoryMapper.kt$HistoryMapper$( historyId: Long, mangaId: Long, chapterId: Long, title: String, thumbnailUrl: String?, sourceId: Long, isFavorite: Boolean, coverLastModified: Long, chapterNumber: Double, readAt: Date?, readDuration: Long, ) LongParameterList:MangaChapterListItem.kt$( action: LibraryPreferences.ChapterSwipeAction, read: Boolean, bookmark: Boolean, downloadState: Download.State, background: Color, onSwipe: () -> Unit, ) LongParameterList:MangaCoverFetcher.kt$MangaCoverFetcher$( private val url: String?, private val isLibraryManga: Boolean, private val options: Options, private val coverFileLazy: Lazy<File?>, private val customCoverFileLazy: Lazy<File>, private val diskCacheKeyLazy: Lazy<String>, private val sourceLazy: Lazy<HttpSource?>, private val callFactoryLazy: Lazy<Call.Factory>, private val diskCacheLazy: Lazy<DiskCache>, ) - LongParameterList:MangaMapper.kt$MangaMapper$( id: Long, source: Long, url: String, artist: String?, author: String?, description: String?, genre: List<String>?, title: String, status: Long, thumbnailUrl: String?, favorite: Boolean, lastUpdate: Long?, nextUpdate: Long?, initialized: Boolean, viewerFlags: Long, chapterFlags: Long, coverLastModified: Long, dateAdded: Long, // SY --> @Suppress("UNUSED_PARAMETER") filteredScanlators: String?, // SY <-- updateStrategy: UpdateStrategy, calculateInterval: Long, lastModifiedAt: Long, favoriteModifiedAt: Long?, ) - LongParameterList:MangaMapper.kt$MangaMapper$( id: Long, source: Long, url: String, artist: String?, author: String?, description: String?, genre: List<String>?, title: String, status: Long, thumbnailUrl: String?, favorite: Boolean, lastUpdate: Long?, nextUpdate: Long?, initialized: Boolean, viewerFlags: Long, chapterFlags: Long, coverLastModified: Long, dateAdded: Long, // SY --> @Suppress("UNUSED_PARAMETER") filteredScanlators: String?, // SY <-- updateStrategy: UpdateStrategy, calculateInterval: Long, lastModifiedAt: Long, favoriteModifiedAt: Long?, totalCount: Long, readCount: Double, latestUpload: Long, chapterFetchedAt: Long, lastRead: Long, bookmarkCount: Double, category: Long, ) LongParameterList:MangaRestorer.kt$MangaRestorer$( manga: Manga, chapters: List<BackupChapter>, categories: List<Long>, backupCategories: List<BackupCategory>, history: List<BackupHistory>, tracks: List<BackupTracking>, excludedScanlators: List<String>, // SY --> mergedMangaReferences: List<BackupMergedMangaReference>, flatMetadata: BackupFlatMetadata?, customManga: CustomMangaInfo?, // SY <-- ) LongParameterList:MangaScreen.kt$( manga: Manga, chapters: List<ChapterList>, isAnyChapterSelected: Boolean, chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction, chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction, // SY --> alwaysShowReadingProgress: Boolean, // SY <-- onChapterClicked: (Chapter) -> Unit, onDownloadChapter: ((List<ChapterList.Item>, ChapterDownloadAction) -> Unit)?, onChapterSelected: (ChapterList.Item, Boolean, Boolean, Boolean) -> Unit, onChapterSwipe: (ChapterList.Item, LibraryPreferences.ChapterSwipeAction) -> Unit, ) LongParameterList:MangaScreenModel.kt$MangaScreenModel$( title: String?, author: String?, artist: String?, thumbnailUrl: String?, description: String?, tags: List<String>?, status: Long?, ) @@ -567,6 +568,7 @@ LongParameterList:WebtoonRecyclerView.kt$WebtoonRecyclerView$( fromRate: Float, toRate: Float, fromX: Float, toX: Float, fromY: Float, toY: Float, ) LoopWithTooManyJumpStatements:DownloadStore.kt$DownloadStore$for LoopWithTooManyJumpStatements:EHentaiUpdateWorker.kt$EHentaiUpdateWorker$for + LoopWithTooManyJumpStatements:GoogleDriveSyncService.kt$GoogleDriveSyncService$while LoopWithTooManyJumpStatements:ImageUtil.kt$ImageUtil$for LoopWithTooManyJumpStatements:Kavita.kt$Kavita$for LoopWithTooManyJumpStatements:MemAutoFlushingLookupTable.kt$MemAutoFlushingLookupTable$while @@ -615,49 +617,10 @@ MagicNumber:Backup.kt$Backup$105 MagicNumber:Backup.kt$Backup$600 MagicNumber:BackupCategory.kt$BackupCategory$100 - MagicNumber:BackupChapter.kt$BackupChapter$10 - MagicNumber:BackupChapter.kt$BackupChapter$11 - MagicNumber:BackupChapter.kt$BackupChapter$3 - MagicNumber:BackupChapter.kt$BackupChapter$4 - MagicNumber:BackupChapter.kt$BackupChapter$5 - MagicNumber:BackupChapter.kt$BackupChapter$6 - MagicNumber:BackupChapter.kt$BackupChapter$7 - MagicNumber:BackupChapter.kt$BackupChapter$8 - MagicNumber:BackupChapter.kt$BackupChapter$9 MagicNumber:BackupCreateJob.kt$BackupCreateJob.Companion$10 MagicNumber:BackupDecoder.kt$BackupDecoder$0x1f8b MagicNumber:BackupFlatMetadata.kt$BackupFlatMetadata$3 MagicNumber:BackupHistory.kt$BackupHistory$3 - MagicNumber:BackupManga.kt$BackupManga$100 - MagicNumber:BackupManga.kt$BackupManga$101 - MagicNumber:BackupManga.kt$BackupManga$102 - MagicNumber:BackupManga.kt$BackupManga$103 - MagicNumber:BackupManga.kt$BackupManga$104 - MagicNumber:BackupManga.kt$BackupManga$105 - MagicNumber:BackupManga.kt$BackupManga$106 - MagicNumber:BackupManga.kt$BackupManga$107 - MagicNumber:BackupManga.kt$BackupManga$108 - MagicNumber:BackupManga.kt$BackupManga$13 - MagicNumber:BackupManga.kt$BackupManga$14 - MagicNumber:BackupManga.kt$BackupManga$16 - MagicNumber:BackupManga.kt$BackupManga$17 - MagicNumber:BackupManga.kt$BackupManga$18 - MagicNumber:BackupManga.kt$BackupManga$3 - MagicNumber:BackupManga.kt$BackupManga$4 - MagicNumber:BackupManga.kt$BackupManga$5 - MagicNumber:BackupManga.kt$BackupManga$6 - MagicNumber:BackupManga.kt$BackupManga$600 - MagicNumber:BackupManga.kt$BackupManga$601 - MagicNumber:BackupManga.kt$BackupManga$602 - MagicNumber:BackupManga.kt$BackupManga$603 - MagicNumber:BackupManga.kt$BackupManga$7 - MagicNumber:BackupManga.kt$BackupManga$8 - MagicNumber:BackupManga.kt$BackupManga$800 - MagicNumber:BackupManga.kt$BackupManga$801 - MagicNumber:BackupManga.kt$BackupManga$802 - MagicNumber:BackupManga.kt$BackupManga$804 - MagicNumber:BackupManga.kt$BackupManga$805 - MagicNumber:BackupManga.kt$BackupManga$9 MagicNumber:BackupMergedMangaReference.kt$BackupMergedMangaReference$3 MagicNumber:BackupMergedMangaReference.kt$BackupMergedMangaReference$4 MagicNumber:BackupMergedMangaReference.kt$BackupMergedMangaReference$5 @@ -696,7 +659,6 @@ MagicNumber:BrowseSourceEHentaiList.kt$5 MagicNumber:CbzCrypto.kt$CbzCrypto$100 MagicNumber:CbzCrypto.kt$CbzCrypto$128 - MagicNumber:CbzCrypto.kt$CbzCrypto$32 MagicNumber:CbzCrypto.kt$CbzCrypto$42 MagicNumber:ChapterCache.kt$ChapterCache$1024 MagicNumber:ChapterDownloadIndicator.kt$0.5f @@ -874,6 +836,10 @@ MagicNumber:GlanceUtils.kt$64 MagicNumber:GlanceUtils.kt$95 MagicNumber:GlobalSearchItem.kt$99f + MagicNumber:GoogleDriveSyncService.kt$GoogleDriveSyncService$10 + MagicNumber:GoogleDriveSyncService.kt$GoogleDriveSyncService$1000L + MagicNumber:GoogleDriveSyncService.kt$GoogleDriveSyncService$16000L + MagicNumber:GoogleDriveSyncService.kt$GoogleDriveSyncService$3 MagicNumber:Hash.kt$Hash$15 MagicNumber:Hash.kt$Hash$240 MagicNumber:Hash.kt$Hash$4 @@ -1104,11 +1070,19 @@ MagicNumber:Scrollbar.kt$50 MagicNumber:SecureActivityDelegate.kt$SecureActivityDelegate.Companion$60_000 MagicNumber:SecurityPreferences.kt$SecurityPreferences$0x7F + MagicNumber:SettingsDataScreen.kt$SettingsDataScreen$10080 MagicNumber:SettingsDataScreen.kt$SettingsDataScreen$12 + MagicNumber:SettingsDataScreen.kt$SettingsDataScreen$1440 MagicNumber:SettingsDataScreen.kt$SettingsDataScreen$168 + MagicNumber:SettingsDataScreen.kt$SettingsDataScreen$180 MagicNumber:SettingsDataScreen.kt$SettingsDataScreen$24 + MagicNumber:SettingsDataScreen.kt$SettingsDataScreen$2880 + MagicNumber:SettingsDataScreen.kt$SettingsDataScreen$30 + MagicNumber:SettingsDataScreen.kt$SettingsDataScreen$360 MagicNumber:SettingsDataScreen.kt$SettingsDataScreen$48 MagicNumber:SettingsDataScreen.kt$SettingsDataScreen$6 + MagicNumber:SettingsDataScreen.kt$SettingsDataScreen$60 + MagicNumber:SettingsDataScreen.kt$SettingsDataScreen$720 MagicNumber:SettingsDownloadScreen.kt$SettingsDownloadScreen$10 MagicNumber:SettingsDownloadScreen.kt$SettingsDownloadScreen$3 MagicNumber:SettingsDownloadScreen.kt$SettingsDownloadScreen$4 @@ -1170,6 +1144,12 @@ MagicNumber:Surface.kt$4.5f MagicNumber:Suwayomi.kt$Suwayomi$255 MagicNumber:Suwayomi.kt$Suwayomi$35 + MagicNumber:SyncDataJob.kt$SyncDataJob.Companion$10 + MagicNumber:SyncManager.kt$SyncManager$1000 + MagicNumber:SyncManager.kt$SyncManager$60000 + MagicNumber:SyncYomiSyncService.kt$SyncYomiSyncService$2000L + MagicNumber:SyncYomiSyncService.kt$SyncYomiSyncService$30L + MagicNumber:SyncYomiSyncService.kt$SyncYomiSyncService$32000L MagicNumber:Tabs.kt$0.08f MagicNumber:Tabs.kt$0.12f MagicNumber:TimeUtils.kt$4 @@ -1260,7 +1240,6 @@ MaxLineLength:BatchAddScreen.kt$BatchAddScreen$Text(text = stringResource(SYMR.strings.eh_batch_add_title), style = MaterialTheme.typography.titleLarge) MaxLineLength:BiometricTimesScreen.kt$BiometricTimesScreen$. MaxLineLength:BiometricTimesScreen.kt$BiometricTimesScreen$text = stringResource(SYMR.strings.delete_time_range_confirmation, dialog.timeRange.formattedString) - MaxLineLength:ChapterLoader.kt$ChapterLoader$isDownloaded -> DownloadPageLoader(chapter, manga, source, downloadManager, downloadProvider, tempFileManager) MaxLineLength:ComikeyHandler.kt$ComikeyHandler$private val urlForbidden = "https://fakeimg.pl/1800x2252/FFFFFF/000000/?font_size=120&text=This%20chapter%20is%20not%20available%20for%20free.%0A%0AIf%20you%20have%20purchased%20this%20chapter%2C%20please%20%0Aopen%20the%20website%20in%20web%20view%20and%20log%20in." MaxLineLength:CoroutinesExtensions.kt$* MaxLineLength:DataSaver.kt$BandwidthHeroDataSaver$imageUrl.contains(".jpeg", true) || imageUrl.contains(".jpg", true) -> if (ignoreJpg) imageUrl else getUrl(imageUrl) @@ -1380,6 +1359,8 @@ MaxLineLength:SourcesScreenModel.kt$SourcesScreenModel$SourceUiModel.Header(it.key.removePrefix(CATEGORY_KEY_PREFIX), it.value.firstOrNull()?.category != null) MaxLineLength:SourcesScreenModel.kt$SourcesScreenModel$private MaxLineLength:SpecificHostRateLimitInterceptor.kt$* + MaxLineLength:SyncService.kt$SyncService$* + MaxLineLength:SyncService.kt$SyncService$val MaxLineLength:TsuminoDescriptionAdapter.kt$binding.pages.text = context.pluralStringResource(SYMR.plurals.num_pages, meta.length ?: 0, meta.length ?: 0) MaxLineLength:TsuminoDescriptionAdapter.kt$binding.rating.text = (round((meta.averageRating ?: 0F) * 100.0) / 100.0).toString() + " - " + MetadataUIUtil.getRatingString(context, meta.averageRating?.times(2)) MaxLineLength:WebViewUtil.kt$WebViewUtil$* @@ -1391,7 +1372,6 @@ MaximumLineLength:BangumiInterceptor.kt$BangumiInterceptor$ MaximumLineLength:BatchAddScreen.kt$BatchAddScreen$ MaximumLineLength:BiometricTimesScreen.kt$BiometricTimesScreen$ - MaximumLineLength:ChapterLoader.kt$ChapterLoader$ MaximumLineLength:ComikeyHandler.kt$ComikeyHandler$ MaximumLineLength:DataSaver.kt$BandwidthHeroDataSaver$ MaximumLineLength:DataSaver.kt$WsrvNlDataSaver$ @@ -1453,6 +1433,7 @@ MaximumLineLength:ShikimoriApi.kt$ShikimoriApi$ MaximumLineLength:SourceFeedScreen.kt$SourceFeedScreen$ MaximumLineLength:SourcesScreenModel.kt$SourcesScreenModel$ + MaximumLineLength:SyncService.kt$SyncService$ MaximumLineLength:TsuminoDescriptionAdapter.kt$ MaximumLineLength:WebtoonPageHolder.kt$WebtoonPageHolder$ MayBeConst:TsuminoSearchMetadata.kt$TsuminoSearchMetadata.Companion$val BASE_URL = "https://www.tsumino.com" @@ -1589,9 +1570,6 @@ ModifierWithoutDefault:SortTagListItem.kt$modifier ModifierWithoutDefault:SourceCategoryListItem.kt$modifier ModifierWithoutDefault:SourcesFilterScreen.kt$modifier - MultiLineIfElse:CbzCrypto.kt$CbzCrypto$ZipOutputStream(this.openOutputStream()) - MultiLineIfElse:CbzCrypto.kt$CbzCrypto$ZipOutputStream(this.openOutputStream(), password) - MultiLineIfElse:CbzCrypto.kt$CbzCrypto$throw zipException MultiLineIfElse:SettingsAppearanceScreen.kt$SettingsAppearanceScreen$pluralStringResource( SYMR.plurals.row_count, previewsRowCount, previewsRowCount, ) MultiLineIfElse:SettingsAppearanceScreen.kt$SettingsAppearanceScreen$stringResource(MR.strings.disabled) NestedBlockDepth:Anilist.kt$Anilist$override suspend fun update(track: Track, didReadChapter: Boolean): Track @@ -1612,6 +1590,7 @@ NestedBlockDepth:FilterHandler.kt$FilterHandler$fun getQueryMap(filters: FilterList): Map<String, Any> NestedBlockDepth:GalleryAdder.kt$GalleryAdder$suspend fun addGallery( context: Context, url: String, fav: Boolean = false, forceSource: UrlImportableSource? = null, throttleFunc: suspend () -> Unit = {}, retry: Int = 1, ): GalleryAddEvent NestedBlockDepth:GetMergedChaptersByMangaId.kt$GetMergedChaptersByMangaId$private fun dedupeByPriority( mangaReferences: List<MergedMangaReference>, chapterList: List<Chapter>, ): List<Chapter> + NestedBlockDepth:GoogleDriveSyncService.kt$GoogleDriveSyncService$override suspend fun beforeSync() NestedBlockDepth:HBrowse.kt$HBrowse$override suspend fun parseIntoMetadata(metadata: HBrowseSearchMetadata, input: Document) NestedBlockDepth:ImageUtil.kt$ImageUtil$fun chooseBackground(context: Context, imageStream: InputStream): Drawable NestedBlockDepth:KavitaApi.kt$KavitaApi$fun getNewToken(apiUrl: String, apiKey: String): String? @@ -1636,13 +1615,10 @@ NestedBlockDepth:WebtoonRecyclerView.kt$WebtoonRecyclerView.Detector$override fun onTouchEvent(ev: MotionEvent): Boolean NestedBlockDepth:WebtoonViewer.kt$WebtoonViewer$fun scrollDown() NewLineAtEndOfFile:Commands.kt$.Commands.kt - NoBlankLineBeforeRbrace:CbzCrypto.kt$CbzCrypto$ - NoConsecutiveBlankLines:CbzCrypto.kt$CbzCrypto$ NoConsecutiveBlankLines:LocalesConfigPlugin.kt$ NoTrailingSpaces:ReaderViewModel.kt$ReaderViewModel$ NoUnusedImports:BackupManga.kt$eu.kanade.tachiyomi.data.backup.models.BackupManga.kt NoUnusedImports:CreateBackupScreen.kt$eu.kanade.presentation.more.settings.screen.data.CreateBackupScreen.kt - NoUnusedImports:SettingsAdvancedScreen.kt$eu.kanade.presentation.more.settings.screen.SettingsAdvancedScreen.kt NoUnusedImports:TachiyomiImageDecoder.kt$eu.kanade.tachiyomi.data.coil.TachiyomiImageDecoder.kt PreviewPublic:NamespaceTags.kt$NamespaceTagsPreview PreviewPublic:Scrollbar.kt$LazyListHorizontalScrollbarPreview @@ -1665,12 +1641,14 @@ ReturnCount:ExtensionLoader.kt$ExtensionLoader$private fun selectExtensionPackage(shared: ExtensionInfo?, private: ExtensionInfo?): ExtensionInfo? ReturnCount:FavoritesSyncHelper.kt$FavoritesSyncHelper$private suspend fun beginSync() ReturnCount:GalleryAdder.kt$GalleryAdder$suspend fun addGallery( context: Context, url: String, fav: Boolean = false, forceSource: UrlImportableSource? = null, throttleFunc: suspend () -> Unit = {}, retry: Int = 1, ): GalleryAddEvent + ReturnCount:GoogleDriveSyncService.kt$GoogleDriveSyncService$override suspend fun pullSyncData(): SyncData? ReturnCount:HttpPageLoader.kt$HttpPageLoader$private fun preloadNextPages(currentPage: ReaderPage, amount: Int): List<PriorityPage> ReturnCount:ImageUtil.kt$ImageUtil$fun chooseBackground(context: Context, imageStream: InputStream): Drawable ReturnCount:KavitaApi.kt$KavitaApi$fun getNewToken(apiUrl: String, apiKey: String): String? ReturnCount:KavitaApi.kt$KavitaApi$private fun getLatestChapterRead(url: String): Double ReturnCount:LibraryUpdateJob.kt$LibraryUpdateJob$override suspend fun doWork(): Result ReturnCount:LibraryUpdateJob.kt$LibraryUpdateJob$private suspend fun updateManga(manga: Manga, fetchWindow: Pair<Long, Long>): List<Chapter> + ReturnCount:LibraryUpdateJob.kt$LibraryUpdateJob.Companion$fun startNow( context: Context, category: Category? = null, target: Target = Target.CHAPTERS, // SY --> group: Int = LibraryGroup.BY_DEFAULT, groupExtra: String? = null, // SY <-- ): Boolean ReturnCount:LocalCoverManager.kt$LocalCoverManager$actual fun update( manga: SManga, inputStream: InputStream, // SY --> encrypted: Boolean, // SY <-- ): UniFile? ReturnCount:LocalSource.kt$LocalSource$private fun copyComicInfoFileFromArchive(chapterArchives: List<UniFile>, folder: UniFile): UniFile? ReturnCount:MainActivity.kt$MainActivity$private fun handleIntentAction(intent: Intent, navigator: Navigator): Boolean @@ -1691,6 +1669,9 @@ ReturnCount:ReaderViewModel.kt$ReaderViewModel$fun setAsCover(useExtraPage: Boolean) ReturnCount:ReaderViewModel.kt$ReaderViewModel$fun shareImages() ReturnCount:ReorderSortTag.kt$ReorderSortTag$fun await(tag: String, newPosition: Int): Result + ReturnCount:SyncManager.kt$SyncManager$private fun areChaptersDifferent(localChapters: List<Chapters>, remoteChapters: List<BackupChapter>): Boolean + ReturnCount:SyncManager.kt$SyncManager$private suspend fun isMangaDifferent(localManga: Manga, remoteManga: BackupManga): Boolean + ReturnCount:SyncManager.kt$SyncManager$suspend fun syncData() ReturnCount:TimeRange.kt$TimeRange.Companion$fun fromPreferenceString(timeRange: String): TimeRange? ReturnCount:WebViewInterceptor.kt$WebViewInterceptor$override fun intercept(chain: Interceptor.Chain): Response ReturnCount:WebViewInterceptor.kt$private fun isRequestHeaderSafe(_name: String, _value: String): Boolean @@ -1701,6 +1682,7 @@ SerialVersionUIDInSerializableClass:Manga.kt$Manga : Serializable SerialVersionUIDInSerializableClass:MigrationProcedureConfig.kt$MigrationProcedureConfig : Serializable SerialVersionUIDInSerializableClass:SourcesScreen.kt$SourcesScreen$SmartSearchConfig : Serializable + SerialVersionUIDInSerializableClass:Track.kt$Track : Serializable SpreadOperator:App.kt$App$( logConfig, *printers.toTypedArray(), ) SpreadOperator:ChapterRepositoryImpl.kt$ChapterRepositoryImpl$(*chapterUpdates.toTypedArray()) SpreadOperator:ChapterSanitizer.kt$ChapterSanitizer$(*CHAPTER_TRIM_CHARS) @@ -1722,7 +1704,6 @@ SwallowedException:Bangumi.kt$Bangumi$e: Exception SwallowedException:Bangumi.kt$Bangumi$e: Throwable SwallowedException:BrowseIcons.kt$e: Exception - SwallowedException:CbzCrypto.kt$CbzCrypto$e: Exception SwallowedException:ChapterCache.kt$ChapterCache$e: IOException SwallowedException:ChapterLoader.kt$ChapterLoader$e: UnsupportedRarV5Exception SwallowedException:ContextExtensions.kt$e: Exception @@ -1746,6 +1727,9 @@ SwallowedException:GalleryAdder.kt$GalleryAdder$e: Exception SwallowedException:GetChapterByUrlAndMangaId.kt$GetChapterByUrlAndMangaId$e: Exception SwallowedException:GetPagePreviews.kt$GetPagePreviews$e: Exception + SwallowedException:GoogleDriveSyncService.kt$GoogleDriveService$e: IOException + SwallowedException:GoogleDriveSyncService.kt$GoogleDriveService$e: TokenResponseException + SwallowedException:GoogleDriveSyncService.kt$GoogleDriveSyncService$e: Exception SwallowedException:HttpSource.kt$HttpSource$e: URISyntaxException SwallowedException:ImageUtil.kt$ImageUtil$e: Exception SwallowedException:Kavita.kt$Kavita$e: Exception @@ -1783,6 +1767,7 @@ SwallowedException:SourceFeedScreenModel.kt$SourceFeedScreenModel$e: Exception SwallowedException:StorageStep.kt$StorageStep$e: ActivityNotFoundException SwallowedException:Suwayomi.kt$Suwayomi$e: Exception + SwallowedException:SyncManager.kt$SyncManager$e: IOException SwallowedException:TrackInfoDialog.kt$TrackInfoDialogHomeScreen.Model$e: Exception SwallowedException:UrlImportableSource.kt$UrlImportableSource$e: URISyntaxException ThrowingExceptionsWithoutMessageOrCause:MangaScreenModel.kt$MangaScreenModel$IllegalStateException() @@ -1816,7 +1801,6 @@ TooGenericExceptionCaught:BaseTracker.kt$BaseTracker$e: Exception TooGenericExceptionCaught:BaseTracker.kt$BaseTracker$e: Throwable TooGenericExceptionCaught:BrowseIcons.kt$e: Exception - TooGenericExceptionCaught:CbzCrypto.kt$CbzCrypto$e: Exception TooGenericExceptionCaught:ChapterCache.kt$ChapterCache$e: Exception TooGenericExceptionCaught:ChapterLoader.kt$ChapterLoader$e: Throwable TooGenericExceptionCaught:ChapterRepositoryImpl.kt$ChapterRepositoryImpl$e: Exception @@ -1871,6 +1855,8 @@ TooGenericExceptionCaught:GetPagePreviews.kt$GetPagePreviews$e: Exception TooGenericExceptionCaught:GetTracks.kt$GetTracks$e: Exception TooGenericExceptionCaught:GlobalExceptionHandler.kt$GlobalExceptionHandler.Companion$e: Exception + TooGenericExceptionCaught:GoogleDriveSyncService.kt$GoogleDriveService$e: Exception + TooGenericExceptionCaught:GoogleDriveSyncService.kt$GoogleDriveSyncService$e: Exception TooGenericExceptionCaught:HistoryRepositoryImpl.kt$HistoryRepositoryImpl$e: Exception TooGenericExceptionCaught:HttpPageLoader.kt$HttpPageLoader$e: Throwable TooGenericExceptionCaught:ImageSaver.kt$ImageSaver$e: Exception @@ -1949,6 +1935,7 @@ TooGenericExceptionCaught:SourcePagingSource.kt$SourcePagingSource$e: Exception TooGenericExceptionCaught:Suwayomi.kt$Suwayomi$e: Exception TooGenericExceptionCaught:SyncChapterProgressWithTrack.kt$SyncChapterProgressWithTrack$e: Throwable + TooGenericExceptionCaught:SyncDataJob.kt$SyncDataJob$e: Exception TooGenericExceptionCaught:TrackChapter.kt$TrackChapter$e: Exception TooGenericExceptionCaught:TrackInfoDialog.kt$TrackInfoDialogHomeScreen.Model$e: Exception TooGenericExceptionCaught:TrackInfoDialog.kt$TrackerSearchScreen.Model$e: Throwable @@ -1978,6 +1965,13 @@ TooGenericExceptionThrown:EHentai.kt$EHentai$throw Exception("HTTP error ${response.code}", stacktrace) TooGenericExceptionThrown:EHentai.kt$EHentai$throw Exception(it.text()) TooGenericExceptionThrown:ExtensionLoader.kt$ExtensionLoader$throw Exception("Unknown source class type: ${obj.javaClass}") + TooGenericExceptionThrown:GoogleDriveSyncService.kt$GoogleDriveService$throw Exception(context.stringResource(MR.strings.google_drive_not_signed_in)) + TooGenericExceptionThrown:GoogleDriveSyncService.kt$GoogleDriveSyncService$throw Exception(context.stringResource(MR.strings.error_before_sync_gdrive) + ": ${e.message}") + TooGenericExceptionThrown:GoogleDriveSyncService.kt$GoogleDriveSyncService$throw Exception(context.stringResource(MR.strings.error_before_sync_gdrive) + ": Max retries reached.") + TooGenericExceptionThrown:GoogleDriveSyncService.kt$GoogleDriveSyncService$throw Exception(context.stringResource(MR.strings.error_deleting_google_drive_lock_file)) + TooGenericExceptionThrown:GoogleDriveSyncService.kt$GoogleDriveSyncService$throw Exception(context.stringResource(MR.strings.error_uploading_sync_data) + ": ${e.message}") + TooGenericExceptionThrown:GoogleDriveSyncService.kt$GoogleDriveSyncService$throw Exception(context.stringResource(MR.strings.google_drive_not_signed_in)) + TooGenericExceptionThrown:GoogleDriveSyncService.kt$GoogleDriveSyncService$throw Exception(e.message) TooGenericExceptionThrown:HttpSource.kt$HttpSource$throw RuntimeException(e) TooGenericExceptionThrown:KitsuApi.kt$KitsuApi$throw Exception("Could not find manga") TooGenericExceptionThrown:KitsuInterceptor.kt$KitsuInterceptor$throw Exception("Not authenticated with Kitsu") @@ -2074,6 +2068,7 @@ TooManyFunctions:ReaderViewModel.kt$ReaderViewModel : ViewModel TooManyFunctions:SecurityPreferences.kt$SecurityPreferences TooManyFunctions:SettingsAdvancedScreen.kt$SettingsAdvancedScreen : SearchableSettings + TooManyFunctions:SettingsDataScreen.kt$SettingsDataScreen : SearchableSettings TooManyFunctions:SettingsEhScreen.kt$SettingsEhScreen : SearchableSettings TooManyFunctions:SettingsItems.kt$tachiyomi.presentation.core.components.SettingsItems.kt TooManyFunctions:SettingsReaderScreen.kt$SettingsReaderScreen : SearchableSettings @@ -2082,6 +2077,8 @@ TooManyFunctions:SourceFeedScreenModel.kt$SourceFeedScreenModel : StateScreenModel TooManyFunctions:SourcePreferences.kt$SourcePreferences TooManyFunctions:Suwayomi.kt$Suwayomi : BaseTrackerEnhancedTracker + TooManyFunctions:SyncPreferences.kt$SyncPreferences + TooManyFunctions:SyncService.kt$SyncService TooManyFunctions:Tracker.kt$Tracker TooManyFunctions:UiPreferences.kt$UiPreferences TooManyFunctions:UnsortedPreferences.kt$UnsortedPreferences @@ -2282,7 +2279,6 @@ VariableNaming:TrackSearch.kt$TrackSearch$var publishing_status: String = "" VariableNaming:TrackSearch.kt$TrackSearch$var publishing_type: String = "" VariableNaming:TrackSearch.kt$TrackSearch$var start_date: String = "" - Wrapping:CbzCrypto.kt$CbzCrypto$( Wrapping:LibraryScreenModel.kt$LibraryScreenModel$-> From 5bcaf4ea4b11a9c66350a189e3fd74cf5cb0bbef Mon Sep 17 00:00:00 2001 From: Jobobby04 Date: Sat, 16 Mar 2024 13:14:40 -0400 Subject: [PATCH 021/106] Cleanup sync code --- .../kanade/tachiyomi/data/sync/SyncManager.kt | 3 +- .../sync/service/GoogleDriveSyncService.kt | 109 +++++++++--------- .../data/sync/service/SyncYomiSyncService.kt | 28 ++--- 3 files changed, 73 insertions(+), 67 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/sync/SyncManager.kt b/app/src/main/java/eu/kanade/tachiyomi/data/sync/SyncManager.kt index dba8b4af5..e57800658 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/sync/SyncManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/sync/SyncManager.kt @@ -19,6 +19,7 @@ import kotlinx.serialization.json.Json import kotlinx.serialization.protobuf.ProtoBuf import logcat.LogPriority import logcat.logcat +import tachiyomi.core.common.util.system.logcat import tachiyomi.data.Chapters import tachiyomi.data.DatabaseHandler import tachiyomi.data.manga.MangaMapper.mapManga @@ -196,7 +197,7 @@ class SyncManager( Uri.fromFile(cacheFile) } } catch (e: IOException) { - logcat(LogPriority.ERROR) { "Failed to write sync data to cache" } + logcat(LogPriority.ERROR, throwable = e) { "Failed to write sync data to cache" } null } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/sync/service/GoogleDriveSyncService.kt b/app/src/main/java/eu/kanade/tachiyomi/data/sync/service/GoogleDriveSyncService.kt index 3cd705652..5a3fd5bf8 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/sync/service/GoogleDriveSyncService.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/sync/service/GoogleDriveSyncService.kt @@ -4,7 +4,6 @@ import android.app.Activity import android.content.Context import android.content.Intent import android.net.Uri -import android.util.Log import com.google.api.client.auth.oauth2.TokenResponseException import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeFlow import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeTokenRequest @@ -19,21 +18,19 @@ import com.google.api.services.drive.Drive import com.google.api.services.drive.DriveScopes import com.google.api.services.drive.model.File import eu.kanade.domain.sync.SyncPreferences -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay -import kotlinx.coroutines.withContext import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import logcat.LogPriority import logcat.logcat import tachiyomi.core.common.i18n.stringResource +import tachiyomi.core.common.util.lang.withIOContext +import tachiyomi.core.common.util.system.logcat import tachiyomi.i18n.MR import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get -import java.io.ByteArrayInputStream import java.io.ByteArrayOutputStream import java.io.IOException -import java.io.InputStreamReader import java.time.Instant import java.util.zip.GZIPInputStream import java.util.zip.GZIPOutputStream @@ -118,8 +115,8 @@ class GoogleDriveSyncService(context: Context, json: Json, syncPreferences: Sync throw Exception(context.stringResource(MR.strings.error_before_sync_gdrive) + ": Max retries reached.") } } catch (e: Exception) { - logcat(LogPriority.ERROR) { "Error in GoogleDrive beforeSync: ${e.message}" } - throw Exception(context.stringResource(MR.strings.error_before_sync_gdrive) + ": ${e.message}") + logcat(LogPriority.ERROR, throwable = e) { "Error in GoogleDrive beforeSync" } + throw Exception(context.stringResource(MR.strings.error_before_sync_gdrive) + ": ${e.message}", e) } } @@ -145,22 +142,23 @@ class GoogleDriveSyncService(context: Context, json: Json, syncPreferences: Sync drive.files().get(gdriveFileId).executeMediaAndDownloadTo(outputStream) logcat(LogPriority.DEBUG) { "File downloaded successfully" } } catch (e: Exception) { - logcat(LogPriority.ERROR) { "Error downloading file: ${e.message}" } + logcat(LogPriority.ERROR, throwable = e) { "Error downloading file" } return null } - return withContext(Dispatchers.IO) { + return withIOContext { try { - val gzipInputStream = GZIPInputStream(ByteArrayInputStream(outputStream.toByteArray())) - val jsonString = gzipInputStream.bufferedReader(Charsets.UTF_8).use { it.readText() } + val gzipInputStream = GZIPInputStream(outputStream.toByteArray().inputStream()) + val jsonString = gzipInputStream.bufferedReader().use { it.readText() } val syncData = json.decodeFromString(SyncData.serializer(), jsonString) - logcat(LogPriority.DEBUG) { "JSON deserialized successfully" } + this@GoogleDriveSyncService.logcat(LogPriority.DEBUG) { "JSON deserialized successfully" } syncData } catch (e: Exception) { - logcat( + this@GoogleDriveSyncService.logcat( LogPriority.ERROR, - ) { "Failed to convert json to sync data with kotlinx.serialization: ${e.message}" } - throw Exception(e.message) + throwable = e, + ) { "Failed to convert json to sync data with kotlinx.serialization" } + throw Exception(e.message, e) } } } @@ -172,11 +170,11 @@ class GoogleDriveSyncService(context: Context, json: Json, syncPreferences: Sync val fileList = getAppDataFileList(drive) val byteArrayOutputStream = ByteArrayOutputStream() - withContext(Dispatchers.IO) { + withIOContext { GZIPOutputStream(byteArrayOutputStream).use { gzipOutputStream -> gzipOutputStream.write(jsonData.toByteArray(Charsets.UTF_8)) } - logcat(LogPriority.DEBUG) { "JSON serialized successfully" } + this@GoogleDriveSyncService.logcat(LogPriority.DEBUG) { "JSON serialized successfully" } } val byteArrayContent = ByteArrayContent("application/octet-stream", byteArrayOutputStream.toByteArray()) @@ -206,8 +204,8 @@ class GoogleDriveSyncService(context: Context, json: Json, syncPreferences: Sync // Data has been successfully pushed or updated, delete the lock file deleteLockFile(drive) } catch (e: Exception) { - logcat(LogPriority.ERROR) { "Failed to push or update sync data: ${e.message}" } - throw Exception(context.stringResource(MR.strings.error_uploading_sync_data) + ": ${e.message}") + 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) } } @@ -222,11 +220,11 @@ class GoogleDriveSyncService(context: Context, json: Json, syncPreferences: Sync .setFields("files(id, name, createdTime)") .execute() .files - Log.d("GoogleDrive", "AppData folder file list: $fileList") + logcat { "AppData folder file list: $fileList" } return fileList } catch (e: Exception) { - Log.e("GoogleDrive", "Error no sync data found in appData folder: ${e.message}") + logcat(LogPriority.ERROR, throwable = e) { "Error no sync data found in appData folder" } return mutableListOf() } } @@ -246,15 +244,15 @@ class GoogleDriveSyncService(context: Context, json: Json, syncPreferences: Sync .setFields("id, name, createdTime") .execute() - Log.d("GoogleDrive", "Created lock file with ID: ${file.id}") + logcat { "Created lock file with ID: ${file.id}" } } catch (e: Exception) { - Log.e("GoogleDrive", "Error creating lock file: ${e.message}") - throw Exception(e.message) + logcat(LogPriority.ERROR, throwable = e) { "Error creating lock file" } + throw Exception(e.message, e) } } private fun findLockFile(drive: Drive): MutableList { - try { + return try { val query = "mimeType='text/plain' and name = '$lockFileName'" val fileList = drive.files() .list() @@ -262,11 +260,11 @@ class GoogleDriveSyncService(context: Context, json: Json, syncPreferences: Sync .setQ(query) .setFields("files(id, name, createdTime)") .execute().files - Log.d("GoogleDrive", "Lock file search result: $fileList") - return fileList + logcat { "Lock file search result: $fileList" } + fileList } catch (e: Exception) { - Log.e("GoogleDrive", "Error finding lock file: ${e.message}") - return mutableListOf() + logcat(LogPriority.ERROR, throwable = e) { "Error finding lock file" } + mutableListOf() } } @@ -277,14 +275,14 @@ class GoogleDriveSyncService(context: Context, json: Json, syncPreferences: Sync if (lockFiles.isNotEmpty()) { for (file in lockFiles) { drive.files().delete(file.id).execute() - Log.d("GoogleDrive", "Deleted lock file with ID: ${file.id}") + logcat { "Deleted lock file with ID: ${file.id}" } } } else { - Log.d("GoogleDrive", "No lock file found to delete.") + logcat { "No lock file found to delete." } } } catch (e: Exception) { - Log.e("GoogleDrive", "Error deleting lock file: ${e.message}") - throw Exception(context.stringResource(MR.strings.error_deleting_google_drive_lock_file)) + logcat(LogPriority.ERROR, throwable = e) { "Error deleting lock file" } + throw Exception(context.stringResource(MR.strings.error_deleting_google_drive_lock_file), e) } } @@ -297,24 +295,27 @@ class GoogleDriveSyncService(context: Context, json: Json, syncPreferences: Sync } googleDriveService.refreshToken() - return withContext(Dispatchers.IO) { + return withIOContext { try { val appDataFileList = getAppDataFileList(drive) if (appDataFileList.isEmpty()) { - logcat(LogPriority.DEBUG) { "No sync data file found in appData folder of Google Drive" } + this@GoogleDriveSyncService + .logcat(LogPriority.DEBUG) { "No sync data file found in appData folder of Google Drive" } DeleteSyncDataStatus.NO_FILES } else { for (file in appDataFileList) { drive.files().delete(file.id).execute() - logcat( + this@GoogleDriveSyncService.logcat( LogPriority.DEBUG, ) { "Deleted sync data file in appData folder of Google Drive with file ID: ${file.id}" } } DeleteSyncDataStatus.SUCCESS } } catch (e: Exception) { - logcat(LogPriority.ERROR) { "Error occurred while interacting with Google Drive: ${e.message}" } + this@GoogleDriveSyncService.logcat(LogPriority.ERROR, throwable = e) { + "Error occurred while interacting with Google Drive" + } DeleteSyncDataStatus.ERROR } } @@ -374,7 +375,7 @@ class GoogleDriveService(private val context: Context) { val jsonFactory: JsonFactory = JacksonFactory.getDefaultInstance() val secrets = GoogleClientSecrets.load( jsonFactory, - InputStreamReader(context.assets.open("client_secrets.json")), + context.assets.open("client_secrets.json").reader(), ) val flow = GoogleAuthorizationCodeFlow.Builder( @@ -389,14 +390,14 @@ class GoogleDriveService(private val context: Context) { .setApprovalPrompt("force") .build() } - internal suspend fun refreshToken() = withContext(Dispatchers.IO) { + internal suspend fun refreshToken() = withIOContext { val refreshToken = syncPreferences.googleDriveRefreshToken().get() val accessToken = syncPreferences.googleDriveAccessToken().get() val jsonFactory: JsonFactory = JacksonFactory.getDefaultInstance() val secrets = GoogleClientSecrets.load( jsonFactory, - InputStreamReader(context.assets.open("client_secrets.json")), + context.assets.open("client_secrets.json").reader(), ) val credential = GoogleCredential.Builder() @@ -411,7 +412,7 @@ class GoogleDriveService(private val context: Context) { credential.refreshToken = refreshToken - logcat(LogPriority.DEBUG) { "Refreshing access token with: $refreshToken" } + this@GoogleDriveService.logcat(LogPriority.DEBUG) { "Refreshing access token with: $refreshToken" } try { credential.refreshToken() @@ -419,23 +420,26 @@ class GoogleDriveService(private val context: Context) { // Save the new access token syncPreferences.googleDriveAccessToken().set(newAccessToken) setupGoogleDriveService(newAccessToken, credential.refreshToken) - logcat(LogPriority.DEBUG) { "Google Access token refreshed old: $accessToken new: $newAccessToken" } + this@GoogleDriveService + .logcat(LogPriority.DEBUG) { "Google Access token refreshed old: $accessToken new: $newAccessToken" } } catch (e: TokenResponseException) { if (e.details.error == "invalid_grant") { // The refresh token is invalid, prompt the user to sign in again - logcat(LogPriority.ERROR) { "Refresh token is invalid, prompt user to sign in again" } - throw e.message?.let { Exception(it) } ?: Exception("Unknown error") + this@GoogleDriveService.logcat(LogPriority.ERROR, throwable = e) { + "Refresh token is invalid, prompt user to sign in again" + } + throw e.message?.let { Exception(it, e) } ?: Exception("Unknown error", e) } else { // Token refresh failed; handle this situation - logcat(LogPriority.ERROR) { "Failed to refresh access token ${e.message}" } - logcat(LogPriority.ERROR) { "Google Drive sync will be disabled" } - throw e.message?.let { Exception(it) } ?: Exception("Unknown error") + this@GoogleDriveService.logcat(LogPriority.ERROR) { "Failed to refresh access token ${e.message}" } + this@GoogleDriveService.logcat(LogPriority.ERROR) { "Google Drive sync will be disabled" } + throw e.message?.let { Exception(it, e) } ?: Exception("Unknown error", e) } } catch (e: IOException) { // Token refresh failed; handle this situation - logcat(LogPriority.ERROR) { "Failed to refresh access token ${e.message}" } - logcat(LogPriority.ERROR) { "Google Drive sync will be disabled" } - throw e.message?.let { Exception(it) } ?: Exception("Unknown error") + this@GoogleDriveService.logcat(LogPriority.ERROR, throwable = e) { "Failed to refresh access token" } + this@GoogleDriveService.logcat(LogPriority.ERROR) { "Google Drive sync will be disabled" } + throw e.message?.let { Exception(it, e) } ?: Exception("Unknown error", e) } } @@ -448,7 +452,7 @@ class GoogleDriveService(private val context: Context) { val jsonFactory: JsonFactory = JacksonFactory.getDefaultInstance() val secrets = GoogleClientSecrets.load( jsonFactory, - InputStreamReader(context.assets.open("client_secrets.json")), + context.assets.open("client_secrets.json").reader(), ) val credential = GoogleCredential.Builder() @@ -487,7 +491,7 @@ class GoogleDriveService(private val context: Context) { val jsonFactory: JsonFactory = JacksonFactory.getDefaultInstance() val secrets = GoogleClientSecrets.load( jsonFactory, - InputStreamReader(context.assets.open("client_secrets.json")), + context.assets.open("client_secrets.json").reader(), ) val tokenResponse: GoogleTokenResponse = GoogleAuthorizationCodeTokenRequest( @@ -515,6 +519,7 @@ class GoogleDriveService(private val context: Context) { onSuccess() } } catch (e: Exception) { + logcat(LogPriority.ERROR, throwable = e) { "Failed to handle authorization code" } activity.runOnUiThread { onFailure(e.localizedMessage ?: "Unknown error") } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/sync/service/SyncYomiSyncService.kt b/app/src/main/java/eu/kanade/tachiyomi/data/sync/service/SyncYomiSyncService.kt index 2db2cf203..b878d3941 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/sync/service/SyncYomiSyncService.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/sync/service/SyncYomiSyncService.kt @@ -6,6 +6,7 @@ import eu.kanade.tachiyomi.data.sync.SyncNotifier import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.PATCH import eu.kanade.tachiyomi.network.POST +import eu.kanade.tachiyomi.network.await import kotlinx.coroutines.delay import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -104,15 +105,15 @@ class SyncYomiSyncService( ) // create lock file first - client.newCall(lockFileCreate).execute() + client.newCall(lockFileCreate).await() // update lock file acquired_by - client.newCall(lockFileUpdate).execute() + 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).execute() + val response = client.newCall(lockFileRequest).await() val responseBody = response.body.string() lockFile = json.decodeFromString(responseBody) logcat(LogPriority.DEBUG) { "SyncYomi lock file status: ${lockFile.status}" } @@ -125,7 +126,7 @@ class SyncYomiSyncService( } while (lockFile.status != SyncStatus.Success) // update lock file acquired_by - client.newCall(lockFileUpdate).execute() + client.newCall(lockFileUpdate).await() } override suspend fun pullSyncData(): SyncData? { @@ -141,16 +142,15 @@ class SyncYomiSyncService( headers = headers, ) - client.newCall(downloadRequest).execute().use { response -> - val responseBody = response.body.string() + val response = client.newCall(downloadRequest).await() + val responseBody = response.body.string() - if (response.isSuccessful) { - return json.decodeFromString(responseBody) - } else { - notifier.showSyncError("Failed to download sync data: $responseBody") - responseBody.let { logcat(LogPriority.ERROR) { "SyncError:$it" } } - return null - } + return if (response.isSuccessful) { + json.decodeFromString(responseBody) + } else { + notifier.showSyncError("Failed to download sync data: $responseBody") + responseBody.let { logcat(LogPriority.ERROR) { "SyncError:$it" } } + null } } @@ -183,7 +183,7 @@ class SyncYomiSyncService( body = body, ) - client.newCall(uploadRequest).execute().use { + client.newCall(uploadRequest).await().use { if (it.isSuccessful) { logcat( LogPriority.DEBUG, From a6a712901cc620999eea6b892ea1efd4cbb7e941 Mon Sep 17 00:00:00 2001 From: Jobobby04 Date: Sat, 16 Mar 2024 13:32:58 -0400 Subject: [PATCH 022/106] Remove Client Secrets --- .gitignore | 1 + app/src/main/assets/client_secrets.json | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 app/src/main/assets/client_secrets.json diff --git a/.gitignore b/.gitignore index b8b26f36d..2329333da 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,4 @@ TODO.md CHANGELOG.md /captures build.sh +/app/src/main/assets/client_secrets.json diff --git a/app/src/main/assets/client_secrets.json b/app/src/main/assets/client_secrets.json deleted file mode 100644 index 0bec78a2a..000000000 --- a/app/src/main/assets/client_secrets.json +++ /dev/null @@ -1 +0,0 @@ -{"installed":{"client_id":"1046609911130-tbp79niehhuii976ekep1us06e9a8lne.apps.googleusercontent.com","project_id":"tachiyomi","auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://oauth2.googleapis.com/token","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs"}} \ No newline at end of file From 53a8beaa1c9622c8684bc8e2617fc6e847f10c1d Mon Sep 17 00:00:00 2001 From: Jobobby04 Date: Sat, 16 Mar 2024 13:37:41 -0400 Subject: [PATCH 023/106] Write client_secrets.json on build --- .github/workflows/build_push.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/build_push.yml b/.github/workflows/build_push.yml index fcd55962d..8d9925808 100644 --- a/.github/workflows/build_push.yml +++ b/.github/workflows/build_push.yml @@ -75,6 +75,13 @@ jobs: path: app/google-services.json contents: ${{ secrets.GOOGLE_SERVICES_TEXT }} write-mode: overwrite + + - name: Write client_secrets.json + uses: DamianReeves/write-file-action@v1.2 + with: + path: app/src/main/assets/client_secrets.json + contents: ${{ secrets.CLIENT_SECRETS_TEXT }} + write-mode: overwrite # SY --> - name: Build app and run unit tests From f6c6881a157e358fbaeede559cc12121b0976e3a Mon Sep 17 00:00:00 2001 From: Jobobby04 Date: Sat, 16 Mar 2024 13:45:47 -0400 Subject: [PATCH 024/106] Add preview prefix --- .github/workflows/build_push_preview.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build_push_preview.yml b/.github/workflows/build_push_preview.yml index 42fec2a2f..d8357484a 100644 --- a/.github/workflows/build_push_preview.yml +++ b/.github/workflows/build_push_preview.yml @@ -21,7 +21,7 @@ jobs: - name: Create Tag run: | git tag "${{ github.run_number }}" - git push origin "${{ github.run_number }}" + git push origin "preview-${{ github.run_number }}" - name: PING - Dispatch initiating repository event run: | From 2da0505c414acb1470365d057c4ba042ea0fc1f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C9=B4=E1=B4=87=E1=B4=8B=E1=B4=8F?= <111511925+NeKoOuO@users.noreply.github.com> Date: Sun, 17 Mar 2024 01:46:27 +0800 Subject: [PATCH 025/106] Update zh-rTW (#1117) * Update plurals.xml * Update strings.xml * Update plurals.xml * Update strings.xml * Update plurals.xml * Update plurals.xml * Update strings.xml * Update plurals.xml * plurals.xml * plurals.xml * Delete i18n/src/commonMain/resources/MR/zh-rTW/strings.xml * Add files via upload --- i18n-sy/src/commonMain/resources/MR/zh-rTW/plurals.xml | 3 +++ i18n-sy/src/commonMain/resources/MR/zh-rTW/strings.xml | 1 + 2 files changed, 4 insertions(+) diff --git a/i18n-sy/src/commonMain/resources/MR/zh-rTW/plurals.xml b/i18n-sy/src/commonMain/resources/MR/zh-rTW/plurals.xml index ee6a6f534..8e25253da 100644 --- a/i18n-sy/src/commonMain/resources/MR/zh-rTW/plurals.xml +++ b/i18n-sy/src/commonMain/resources/MR/zh-rTW/plurals.xml @@ -55,4 +55,7 @@ %1$d 秒前 + + %d 行 + diff --git a/i18n-sy/src/commonMain/resources/MR/zh-rTW/strings.xml b/i18n-sy/src/commonMain/resources/MR/zh-rTW/strings.xml index 189353059..8935b5523 100644 --- a/i18n-sy/src/commonMain/resources/MR/zh-rTW/strings.xml +++ b/i18n-sy/src/commonMain/resources/MR/zh-rTW/strings.xml @@ -157,6 +157,7 @@ 將推薦按鈕放在彈出選單而不是作品頁面 在選單顯示合併 將合併按鈕放在彈出選單而不是作品頁面 + 預覽行數 導覽列 From ab4703530c49a12cccaefc67a094c40e6f760264 Mon Sep 17 00:00:00 2001 From: Jobobby04 Date: Sat, 16 Mar 2024 13:47:25 -0400 Subject: [PATCH 026/106] Oops --- .github/workflows/build_push_preview.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build_push_preview.yml b/.github/workflows/build_push_preview.yml index d8357484a..94abbc817 100644 --- a/.github/workflows/build_push_preview.yml +++ b/.github/workflows/build_push_preview.yml @@ -20,7 +20,7 @@ jobs: - name: Create Tag run: | - git tag "${{ github.run_number }}" + git tag "preview-${{ github.run_number }}" git push origin "preview-${{ github.run_number }}" - name: PING - Dispatch initiating repository event From d8fe39b45583abc20710b6257e5290516e10e0f5 Mon Sep 17 00:00:00 2001 From: Jobobby04 Date: Sat, 16 Mar 2024 15:59:00 -0400 Subject: [PATCH 027/106] [skip ci] Add instructions for supporting Cloud Sync Google Drive Impl --- CONTRIBUTING.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cbf8dffff..81286a443 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -52,3 +52,20 @@ When creating a fork, remember to: - To avoid having your data polluting the main app's analytics and crash report services: - If you want to use Firebase analytics, replace [`google-services.json`](https://github.com/komikku-app/komikku/blob/master/app/src/standard/google-services.json) with your own - If you want to use ACRA crash reporting, replace the `ACRA_URI` endpoint in [`build.gradle.kts`](https://github.com/komikku-app/komikku/blob/master/app/build.gradle.kts) with your own + + +### Supporting Cloud Sync - Google Drive Implementation +1. Go to [Google Cloud Console](https://console.cloud.google.com) +2. Create a new project +3. Go to API & Services -> Library -> Google Drive API and click enable +4. Go to API & Services -> Oauth consent screen +5. Create it, fill in the app name, user support email, and developer contact information +6. In the next screen, click add or remove scopes, and add the `.../auth/drive.appdata` and `.../auth/drive.file` scopes +7. Don't add any test users and go back to the dashboard +8. Click publish +9. Go to API & Services -> Credentials +10. Click Create credentials -> Oauth client ID +11. Select Android, give it a name, and set eu.kanade.google.oauth as the package name +12. To get the SHA-1 key, run `keytool -printcert -jarfile app-standard-universal-release.apk` on your apk, and copy the listed SHA-1 +13. Expand advanced settings, and enable Custom URL scheme +14. After that just download the json, name it to `client_secrets.json` and put it in `app/src/main/assets/` From aec3f2483124128f0865354c694d3480afff45d5 Mon Sep 17 00:00:00 2001 From: KaiserBh <41852205+kaiserbh@users.noreply.github.com> Date: Sun, 17 Mar 2024 14:56:00 +1100 Subject: [PATCH 028/106] fix: The trigger for library update wasn't working properly. (#1119) Missed them, so it was always updating library without actually syncing even when the trigger was on. Signed-off-by: KaiserBh --- .../java/eu/kanade/domain/sync/SyncPreferences.kt | 3 --- .../tachiyomi/data/library/LibraryUpdateJob.kt | 14 ++------------ .../data/sync/models/SyncTriggerOptions.kt | 11 +---------- i18n/src/commonMain/resources/MR/base/strings.xml | 1 - 4 files changed, 3 insertions(+), 26 deletions(-) diff --git a/app/src/main/java/eu/kanade/domain/sync/SyncPreferences.kt b/app/src/main/java/eu/kanade/domain/sync/SyncPreferences.kt index affe09bb6..d83a2b38b 100644 --- a/app/src/main/java/eu/kanade/domain/sync/SyncPreferences.kt +++ b/app/src/main/java/eu/kanade/domain/sync/SyncPreferences.kt @@ -73,7 +73,6 @@ class SyncPreferences( syncOnChapterOpen = preferenceStore.getBoolean("sync_on_chapter_open", false).get(), syncOnAppStart = preferenceStore.getBoolean("sync_on_app_start", false).get(), syncOnAppResume = preferenceStore.getBoolean("sync_on_app_resume", false).get(), - syncOnLibraryUpdate = preferenceStore.getBoolean("sync_on_library_update", false).get(), ) } @@ -86,7 +85,5 @@ class SyncPreferences( .set(syncTriggerOptions.syncOnAppStart) preferenceStore.getBoolean("sync_on_app_resume", false) .set(syncTriggerOptions.syncOnAppResume) - preferenceStore.getBoolean("sync_on_library_update", false) - .set(syncTriggerOptions.syncOnLibraryUpdate) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt index 76322eefa..e17e56823 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt @@ -817,19 +817,11 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet KEY_GROUP_EXTRA to groupExtra, // SY <-- ) - val request = OneTimeWorkRequestBuilder() - .addTag(TAG) - .addTag(WORK_NAME_MANUAL) - .setInputData(inputData) - .build() - wm.enqueueUniqueWork(WORK_NAME_MANUAL, ExistingWorkPolicy.KEEP, request) val syncPreferences: SyncPreferences = Injekt.get() - // Only proceed with SyncDataJob if sync is enabled and the specific sync on library update flag is set - val syncTriggerOpt = syncPreferences.getSyncTriggerOptions() - if (syncPreferences.isSyncEnabled() && syncTriggerOpt.syncOnLibraryUpdate - ) { + // Always sync the data before library update if syncing is enabled. + if (syncPreferences.isSyncEnabled()) { // Check if SyncDataJob is already running if (wm.isRunning(SyncDataJob.TAG_MANUAL)) { // SyncDataJob is already running @@ -842,7 +834,6 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet .build() // Chain SyncDataJob to run before LibraryUpdateJob - val inputData = workDataOf(KEY_CATEGORY to category?.id) val libraryUpdateJob = OneTimeWorkRequestBuilder() .addTag(TAG) .addTag(WORK_NAME_MANUAL) @@ -853,7 +844,6 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet .then(libraryUpdateJob) .enqueue() } else { - val inputData = workDataOf(KEY_CATEGORY to category?.id) val request = OneTimeWorkRequestBuilder() .addTag(TAG) .addTag(WORK_NAME_MANUAL) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/sync/models/SyncTriggerOptions.kt b/app/src/main/java/eu/kanade/tachiyomi/data/sync/models/SyncTriggerOptions.kt index b5dfaf001..7d8a9bdb8 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/sync/models/SyncTriggerOptions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/sync/models/SyncTriggerOptions.kt @@ -9,21 +9,18 @@ data class SyncTriggerOptions( val syncOnChapterOpen: Boolean = false, val syncOnAppStart: Boolean = false, val syncOnAppResume: Boolean = false, - val syncOnLibraryUpdate: Boolean = false, ) { fun asBooleanArray() = booleanArrayOf( syncOnChapterRead, syncOnChapterOpen, syncOnAppStart, syncOnAppResume, - syncOnLibraryUpdate, ) fun anyEnabled() = syncOnChapterRead || syncOnChapterOpen || syncOnAppStart || - syncOnAppResume || - syncOnLibraryUpdate + syncOnAppResume companion object { val mainOptions = persistentListOf( @@ -47,11 +44,6 @@ data class SyncTriggerOptions( getter = SyncTriggerOptions::syncOnAppResume, setter = { options, enabled -> options.copy(syncOnAppResume = enabled) }, ), - Entry( - label = MR.strings.sync_on_library_update, - getter = SyncTriggerOptions::syncOnLibraryUpdate, - setter = { options, enabled -> options.copy(syncOnLibraryUpdate = enabled) }, - ), ) fun fromBooleanArray(array: BooleanArray) = SyncTriggerOptions( @@ -59,7 +51,6 @@ data class SyncTriggerOptions( syncOnChapterOpen = array[1], syncOnAppStart = array[2], syncOnAppResume = array[3], - syncOnLibraryUpdate = array[4], ) } diff --git a/i18n/src/commonMain/resources/MR/base/strings.xml b/i18n/src/commonMain/resources/MR/base/strings.xml index 4945edab9..3ee05b61c 100644 --- a/i18n/src/commonMain/resources/MR/base/strings.xml +++ b/i18n/src/commonMain/resources/MR/base/strings.xml @@ -605,7 +605,6 @@ Sync on Chapter Open Sync on App Start Sync on App Resume - Sync on Library Update Sync library From aba59b55229e555ea1b8b5b1de1a7aae2fcce178 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ferm=C3=ADn=20Cirella?= Date: Sun, 17 Mar 2024 00:59:19 -0300 Subject: [PATCH 029/106] Add option to reset custom manga info (#1118) * Add option to reset custom manga info * Remove extra line * Update app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryScreenModel.kt --------- Co-authored-by: jobobby04 --- .../manga/components/MangaBottomActionMenu.kt | 9 +++++- .../ui/library/LibraryScreenModel.kt | 30 +++++++++++++++++++ .../kanade/tachiyomi/ui/library/LibraryTab.kt | 1 + .../tachiyomi/ui/manga/EditMangaDialog.kt | 10 +++++++ app/src/main/res/layout/edit_manga_dialog.xml | 10 +++++++ .../commonMain/resources/MR/base/strings.xml | 1 + 6 files changed, 60 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/eu/kanade/presentation/manga/components/MangaBottomActionMenu.kt b/app/src/main/java/eu/kanade/presentation/manga/components/MangaBottomActionMenu.kt index a8f39e896..151af0927 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/components/MangaBottomActionMenu.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/components/MangaBottomActionMenu.kt @@ -239,6 +239,7 @@ fun LibraryBottomActionMenu( onClickCleanTitles: (() -> Unit)?, onClickMigrate: (() -> Unit)?, onClickAddToMangaDex: (() -> Unit)?, + onClickResetInfo: (() -> Unit)?, // SY <-- modifier: Modifier = Modifier, ) { @@ -267,7 +268,7 @@ fun LibraryBottomActionMenu( } } // SY --> - val showOverflow = onClickCleanTitles != null || onClickAddToMangaDex != null + val showOverflow = onClickCleanTitles != null || onClickAddToMangaDex != null || onClickResetInfo != null val configuration = LocalConfiguration.current val moveMarkPrev = remember { !configuration.isTabletUi() } var overFlowOpen by remember { mutableStateOf(false) } @@ -364,6 +365,12 @@ fun LibraryBottomActionMenu( onClick = onClickAddToMangaDex, ) } + if (onClickResetInfo != null) { + DropdownMenuItem( + text = { Text(text = stringResource(SYMR.strings.reset_info)) }, + onClick = onClickResetInfo, + ) + } } } else { Button( diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryScreenModel.kt index 43a94a80a..22488c82a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryScreenModel.kt @@ -750,6 +750,24 @@ class LibraryScreenModel( clearSelection() } } + + fun resetInfo() { + state.value.selection.fastForEach { (manga) -> + val mangaInfo = CustomMangaInfo( + id = manga.id, + title = null, + author = null, + artist = null, + thumbnailUrl = null, + description = null, + genre = null, + status = null, + ) + + setCustomMangaInfo.set(mangaInfo) + } + clearSelection() + } // SY <-- /** @@ -1336,6 +1354,18 @@ class LibraryScreenModel( val showAddToMangadex: Boolean by lazy { selection.any { it.manga.source in mangaDexSourceIds } } + + val showResetInfo: Boolean by lazy { + selection.fastAny { (manga) -> + manga.title != manga.ogTitle || + manga.author != manga.ogAuthor || + manga.artist != manga.ogArtist || + manga.thumbnailUrl != manga.ogThumbnailUrl || + manga.description != manga.ogDescription || + manga.genre != manga.ogGenre || + manga.status != manga.ogStatus + } + } // SY <-- fun getLibraryItemsByCategoryId(categoryId: Long): List? { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryTab.kt index e666a3beb..0a3a39a1f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryTab.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryTab.kt @@ -205,6 +205,7 @@ object LibraryTab : Tab { } }, onClickAddToMangaDex = screenModel::syncMangaToDex.takeIf { state.showAddToMangadex }, + onClickResetInfo = screenModel::resetInfo.takeIf { state.showResetInfo }, // SY <-- ) }, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/EditMangaDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/EditMangaDialog.kt index 456456ac5..0c1f429e1 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/EditMangaDialog.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/EditMangaDialog.kt @@ -201,6 +201,7 @@ private fun onViewCreated(manga: Manga, context: Context, binding: EditMangaDial binding.mangaGenresTags.clearFocus() binding.resetTags.setOnClickListener { resetTags(manga, binding, scope) } + binding.resetInfo.setOnClickListener { resetInfo(manga, binding, scope) } } private fun resetTags(manga: Manga, binding: EditMangaDialogBinding, scope: CoroutineScope) { @@ -217,6 +218,15 @@ private fun loadCover(manga: Manga, context: Context, binding: EditMangaDialogBi } } +private fun resetInfo(manga: Manga, binding: EditMangaDialogBinding, scope: CoroutineScope) { + binding.title.setText("") + binding.mangaAuthor.setText("") + binding.mangaArtist.setText("") + binding.thumbnailUrl.setText("") + binding.mangaDescription.setText("") + resetTags(manga, binding, scope) +} + private fun ChipGroup.setChips(items: List, scope: CoroutineScope) { removeAllViews() diff --git a/app/src/main/res/layout/edit_manga_dialog.xml b/app/src/main/res/layout/edit_manga_dialog.xml index 205207812..1f7d67ebb 100644 --- a/app/src/main/res/layout/edit_manga_dialog.xml +++ b/app/src/main/res/layout/edit_manga_dialog.xml @@ -139,6 +139,16 @@ android:text="@string/reset_tags" android:textAllCaps="false" /> +