package suwayomi.tachidesk.manga.impl /* * Copyright (C) Contributors to the Suwayomi project * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import androidx.preference.Preference import androidx.preference.PreferenceScreen import eu.kanade.tachiyomi.source.ConfigurableSource import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.sourcePreferences import io.github.oshai.kotlinlogging.KotlinLogging import io.javalin.json.JsonMapper import io.javalin.json.fromJsonString import org.jetbrains.exposed.v1.core.and import org.jetbrains.exposed.v1.core.dao.id.EntityID import org.jetbrains.exposed.v1.core.eq import org.jetbrains.exposed.v1.core.inList import org.jetbrains.exposed.v1.core.statements.BatchUpdateStatement import org.jetbrains.exposed.v1.jdbc.batchInsert import org.jetbrains.exposed.v1.jdbc.selectAll import org.jetbrains.exposed.v1.jdbc.statements.toExecutable import org.jetbrains.exposed.v1.jdbc.transactions.transaction import suwayomi.tachidesk.manga.impl.Source.preferenceScreenMap import suwayomi.tachidesk.manga.impl.extension.Extension.proxyExtensionIconUrl import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrNull import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.unregisterCatalogueSource import suwayomi.tachidesk.manga.model.dataclass.ContentWarning import suwayomi.tachidesk.manga.model.dataclass.SourceDataClass import suwayomi.tachidesk.manga.model.table.ExtensionTable import suwayomi.tachidesk.manga.model.table.SourceMetaTable import suwayomi.tachidesk.manga.model.table.SourceTable import uy.kohesive.injekt.injectLazy import xyz.nulldev.androidcompat.androidimpl.CustomContext object Source { private val logger = KotlinLogging.logger {} fun getSourceList(): List { return transaction { SourceTable.selectAll().mapNotNull { val catalogueSource = getCatalogueSourceOrNull(it[SourceTable.id].value) ?: return@mapNotNull null val sourceExtension = ExtensionTable.selectAll().where { ExtensionTable.id eq it[SourceTable.extension] }.first() SourceDataClass( id = it[SourceTable.id].value.toString(), name = it[SourceTable.name], lang = it[SourceTable.lang], iconUrl = proxyExtensionIconUrl(sourceExtension[ExtensionTable.pkgName]), supportsLatest = catalogueSource.supportsLatest, isConfigurable = catalogueSource is ConfigurableSource, isNsfw = it[SourceTable.contentWarning] >= ContentWarning.MIXED.ordinal, displayName = catalogueSource.toString(), baseUrl = runCatching { (catalogueSource as? HttpSource)?.baseUrl }.getOrNull(), ) } } } fun getSource(sourceId: Long): SourceDataClass? { // all the data extracted fresh form the source instance return transaction { val source = SourceTable.selectAll().where { SourceTable.id eq sourceId }.firstOrNull() ?: return@transaction null val catalogueSource = getCatalogueSourceOrNull(sourceId) ?: return@transaction null val extension = ExtensionTable.selectAll().where { ExtensionTable.id eq source[SourceTable.extension] }.first() SourceDataClass( id = sourceId.toString(), name = source[SourceTable.name], lang = source[SourceTable.lang], iconUrl = proxyExtensionIconUrl(extension[ExtensionTable.pkgName]), supportsLatest = catalogueSource.supportsLatest, isConfigurable = catalogueSource is ConfigurableSource, isNsfw = source[SourceTable.contentWarning] >= ContentWarning.MIXED.ordinal, displayName = catalogueSource.toString(), baseUrl = runCatching { (catalogueSource as? HttpSource)?.baseUrl }.getOrNull(), ) } } private val context: CustomContext by injectLazy() /** * (2021-11) Clients should support these types for extensions to work properly * - EditTextPreference * - SwitchPreferenceCompat * - ListPreference * - CheckBoxPreference * - MultiSelectListPreference */ data class PreferenceObject( val type: String, val props: Any, ) var preferenceScreenMap: MutableMap = mutableMapOf() /** * Gets a source's PreferenceScreen, puts the result into [preferenceScreenMap] */ fun getSourcePreferences(sourceId: Long): List = getSourcePreferencesRaw(sourceId).map { PreferenceObject(it::class.java.simpleName, it) } fun getSourcePreferencesRaw(sourceId: Long): List { val source = getCatalogueSourceOrStub(sourceId) if (source is ConfigurableSource) { val sourceShardPreferences = source.sourcePreferences() val screen = PreferenceScreen(context) screen.sharedPreferences = sourceShardPreferences source.setupPreferenceScreen(screen) preferenceScreenMap[sourceId] = screen return screen.preferences } return emptyList() } data class SourcePreferenceChange( val position: Int, val value: String, ) private val jsonMapper: JsonMapper by injectLazy() fun setSourcePreference( sourceId: Long, position: Int, value: String, getValue: (Preference) -> Any = { pref -> when (pref.defaultValueType) { "String" -> value "Boolean" -> value.toBoolean() "Set" -> jsonMapper.fromJsonString>(value).toSet() else -> throw RuntimeException("Unsupported type conversion") } }, ) { val screen = preferenceScreenMap[sourceId]!! val pref = screen.preferences[position] if (!pref.isEnabled) { return } val newValue = getValue(pref) pref.saveNewValue(newValue) pref.callChangeListener(newValue) // must reload the source because a preference was changed unregisterCatalogueSource(sourceId) } fun getSourcesMetaMaps(ids: List): Map> = transaction { SourceMetaTable .selectAll() .where { SourceMetaTable.ref inList ids } .groupBy { it[SourceMetaTable.ref] } .mapValues { it.value.associate { it[SourceMetaTable.key] to it[SourceMetaTable.value] } } .withDefault { emptyMap() } } fun modifyMeta( sourceId: Long, key: String, value: String, ) { modifySourceMetas(mapOf(sourceId to mapOf(key to value))) } fun modifySourceMetas(metaBySourceIds: Map>) { transaction { val sourceIds = metaBySourceIds.keys val metaKeys = metaBySourceIds.flatMap { it.value.keys } val dbMetaBySourceId = SourceMetaTable .selectAll() .where { (SourceMetaTable.ref inList sourceIds) and (SourceMetaTable.key inList metaKeys) } .groupBy { it[SourceMetaTable.ref] } val existingMetaByMetaId = sourceIds.flatMap { sourceId -> val metaByKey = dbMetaBySourceId[sourceId].orEmpty().associateBy { it[SourceMetaTable.key] } val existingMetas = metaBySourceIds[sourceId].orEmpty().filter { (key) -> key in metaByKey.keys } existingMetas.map { entry -> val metaId = metaByKey[entry.key]!![SourceMetaTable.id].value metaId to entry } } val newMetaBySourceId = sourceIds.flatMap { sourceId -> val metaByKey = dbMetaBySourceId[sourceId].orEmpty().associateBy { it[SourceMetaTable.key] } metaBySourceIds[sourceId] .orEmpty() .filter { entry -> entry.key !in metaByKey.keys } .map { entry -> sourceId to entry } } if (existingMetaByMetaId.isNotEmpty()) { BatchUpdateStatement(SourceMetaTable) .apply { existingMetaByMetaId.forEach { (metaId, entry) -> addBatch(EntityID(metaId, SourceMetaTable)) this[SourceMetaTable.value] = entry.value } }.toExecutable() .execute(this@transaction) } if (newMetaBySourceId.isNotEmpty()) { SourceMetaTable.batchInsert(newMetaBySourceId) { (sourceId, entry) -> this[SourceMetaTable.ref] = sourceId this[SourceMetaTable.key] = entry.key this[SourceMetaTable.value] = entry.value } } } } }