Maintain source info in the database. (#6389)

* Maintain Source Info in database

* Review changes and cleanups

* Review changes 2

* Review Changes 3
This commit is contained in:
AntsyLich 2022-06-14 19:10:40 +06:00 committed by GitHub
parent a01c370d63
commit 9d5b7de1d8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 307 additions and 48 deletions

View File

@ -1,17 +1,24 @@
package eu.kanade.data.source package eu.kanade.data.source
import eu.kanade.domain.source.model.Source import eu.kanade.domain.source.model.Source
import eu.kanade.domain.source.model.SourceData
import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.SourceManager
val sourceMapper: (eu.kanade.tachiyomi.source.Source) -> Source = { source -> val sourceMapper: (eu.kanade.tachiyomi.source.Source) -> Source = { source ->
Source( Source(
source.id, source.id,
source.lang, source.lang,
source.name, source.name,
false, supportsLatest = false,
isStub = source is SourceManager.StubSource,
) )
} }
val catalogueSourceMapper: (CatalogueSource) -> Source = { source -> val catalogueSourceMapper: (CatalogueSource) -> Source = { source ->
sourceMapper(source).copy(supportsLatest = source.supportsLatest) sourceMapper(source).copy(supportsLatest = source.supportsLatest)
} }
val sourceDataMapper: (Long, String, String) -> SourceData = { id, lang, name ->
SourceData(id, lang, name)
}

View File

@ -2,6 +2,7 @@ package eu.kanade.data.source
import eu.kanade.data.DatabaseHandler import eu.kanade.data.DatabaseHandler
import eu.kanade.domain.source.model.Source import eu.kanade.domain.source.model.Source
import eu.kanade.domain.source.model.SourceData
import eu.kanade.domain.source.repository.SourceRepository import eu.kanade.domain.source.repository.SourceRepository
import eu.kanade.tachiyomi.source.LocalSource import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager
@ -49,4 +50,12 @@ class SourceRepositoryImpl(
} }
} }
} }
override suspend fun getSourceData(id: Long): SourceData? {
return handler.awaitOneOrNull { sourcesQueries.getSourceData(id, sourceDataMapper) }
}
override suspend fun upsertSourceData(id: Long, lang: String, name: String) {
handler.await { sourcesQueries.upsert(id, lang, name) }
}
} }

View File

@ -27,12 +27,14 @@ import eu.kanade.domain.manga.interactor.UpdateManga
import eu.kanade.domain.manga.repository.MangaRepository import eu.kanade.domain.manga.repository.MangaRepository
import eu.kanade.domain.source.interactor.GetEnabledSources import eu.kanade.domain.source.interactor.GetEnabledSources
import eu.kanade.domain.source.interactor.GetLanguagesWithSources import eu.kanade.domain.source.interactor.GetLanguagesWithSources
import eu.kanade.domain.source.interactor.GetSourceData
import eu.kanade.domain.source.interactor.GetSourcesWithFavoriteCount import eu.kanade.domain.source.interactor.GetSourcesWithFavoriteCount
import eu.kanade.domain.source.interactor.GetSourcesWithNonLibraryManga import eu.kanade.domain.source.interactor.GetSourcesWithNonLibraryManga
import eu.kanade.domain.source.interactor.SetMigrateSorting import eu.kanade.domain.source.interactor.SetMigrateSorting
import eu.kanade.domain.source.interactor.ToggleLanguage import eu.kanade.domain.source.interactor.ToggleLanguage
import eu.kanade.domain.source.interactor.ToggleSource import eu.kanade.domain.source.interactor.ToggleSource
import eu.kanade.domain.source.interactor.ToggleSourcePin import eu.kanade.domain.source.interactor.ToggleSourcePin
import eu.kanade.domain.source.interactor.UpsertSourceData
import eu.kanade.domain.source.repository.SourceRepository import eu.kanade.domain.source.repository.SourceRepository
import uy.kohesive.injekt.api.InjektModule import uy.kohesive.injekt.api.InjektModule
import uy.kohesive.injekt.api.InjektRegistrar import uy.kohesive.injekt.api.InjektRegistrar
@ -71,11 +73,13 @@ class DomainModule : InjektModule {
addSingletonFactory<SourceRepository> { SourceRepositoryImpl(get(), get()) } addSingletonFactory<SourceRepository> { SourceRepositoryImpl(get(), get()) }
addFactory { GetEnabledSources(get(), get()) } addFactory { GetEnabledSources(get(), get()) }
addFactory { GetLanguagesWithSources(get(), get()) } addFactory { GetLanguagesWithSources(get(), get()) }
addFactory { GetSourceData(get()) }
addFactory { GetSourcesWithFavoriteCount(get(), get()) } addFactory { GetSourcesWithFavoriteCount(get(), get()) }
addFactory { GetSourcesWithNonLibraryManga(get()) } addFactory { GetSourcesWithNonLibraryManga(get()) }
addFactory { SetMigrateSorting(get()) } addFactory { SetMigrateSorting(get()) }
addFactory { ToggleLanguage(get()) } addFactory { ToggleLanguage(get()) }
addFactory { ToggleSource(get()) } addFactory { ToggleSource(get()) }
addFactory { ToggleSourcePin(get()) } addFactory { ToggleSourcePin(get()) }
addFactory { UpsertSourceData(get()) }
} }
} }

View File

@ -0,0 +1,20 @@
package eu.kanade.domain.source.interactor
import eu.kanade.domain.source.model.SourceData
import eu.kanade.domain.source.repository.SourceRepository
import eu.kanade.tachiyomi.util.system.logcat
import logcat.LogPriority
class GetSourceData(
private val repository: SourceRepository,
) {
suspend fun await(id: Long): SourceData? {
return try {
repository.getSourceData(id)
} catch (e: Exception) {
logcat(LogPriority.ERROR, e)
null
}
}
}

View File

@ -33,20 +33,18 @@ class GetSourcesWithFavoriteCount(
strength = Collator.PRIMARY strength = Collator.PRIMARY
} }
val sortFn: (Pair<Source, Long>, Pair<Source, Long>) -> Int = { a, b -> val sortFn: (Pair<Source, Long>, Pair<Source, Long>) -> Int = { a, b ->
val id1 = a.first.name.toLongOrNull()
val id2 = b.first.name.toLongOrNull()
when (sorting) { when (sorting) {
SetMigrateSorting.Mode.ALPHABETICAL -> { SetMigrateSorting.Mode.ALPHABETICAL -> {
when { when {
id1 != null && id2 == null -> -1 a.first.isStub && b.first.isStub.not() -> -1
id2 != null && id1 == null -> 1 b.first.isStub && a.first.isStub.not() -> 1
else -> collator.compare(a.first.name.lowercase(locale), b.first.name.lowercase(locale)) else -> collator.compare(a.first.name.lowercase(locale), b.first.name.lowercase(locale))
} }
} }
SetMigrateSorting.Mode.TOTAL -> { SetMigrateSorting.Mode.TOTAL -> {
when { when {
id1 != null && id2 == null -> -1 a.first.isStub && b.first.isStub.not() -> -1
id2 != null && id1 == null -> 1 b.first.isStub && a.first.isStub.not() -> 1
else -> a.second.compareTo(b.second) else -> a.second.compareTo(b.second)
} }
} }

View File

@ -0,0 +1,19 @@
package eu.kanade.domain.source.interactor
import eu.kanade.domain.source.model.SourceData
import eu.kanade.domain.source.repository.SourceRepository
import eu.kanade.tachiyomi.util.system.logcat
import logcat.LogPriority
class UpsertSourceData(
private val repository: SourceRepository,
) {
suspend fun await(sourceData: SourceData) {
try {
repository.upsertSourceData(sourceData.id, sourceData.lang, sourceData.name)
} catch (e: Exception) {
logcat(LogPriority.ERROR, e)
}
}
}

View File

@ -12,6 +12,7 @@ data class Source(
val lang: String, val lang: String,
val name: String, val name: String,
val supportsLatest: Boolean, val supportsLatest: Boolean,
val isStub: Boolean,
val pin: Pins = Pins.unpinned, val pin: Pins = Pins.unpinned,
val isUsedLast: Boolean = false, val isUsedLast: Boolean = false,
) { ) {

View File

@ -0,0 +1,7 @@
package eu.kanade.domain.source.model
data class SourceData(
val id: Long,
val lang: String,
val name: String,
)

View File

@ -1,6 +1,7 @@
package eu.kanade.domain.source.repository package eu.kanade.domain.source.repository
import eu.kanade.domain.source.model.Source import eu.kanade.domain.source.model.Source
import eu.kanade.domain.source.model.SourceData
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import eu.kanade.tachiyomi.source.Source as LoadedSource import eu.kanade.tachiyomi.source.Source as LoadedSource
@ -13,4 +14,8 @@ interface SourceRepository {
fun getSourcesWithFavoriteCount(): Flow<List<Pair<Source, Long>>> fun getSourcesWithFavoriteCount(): Flow<List<Pair<Source, Long>>>
fun getSourcesWithNonLibraryManga(): Flow<List<Pair<LoadedSource, Long>>> fun getSourcesWithNonLibraryManga(): Flow<List<Pair<LoadedSource, Long>>>
suspend fun getSourceData(id: Long): SourceData?
suspend fun upsertSourceData(id: Long, lang: String, name: String)
} }

View File

@ -1,5 +1,9 @@
package eu.kanade.presentation.browse package eu.kanade.presentation.browse
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.navigationBars
@ -10,13 +14,18 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import eu.kanade.domain.source.model.Source import eu.kanade.domain.source.model.Source
import eu.kanade.presentation.browse.components.BaseSourceItem import eu.kanade.presentation.browse.components.BaseSourceItem
import eu.kanade.presentation.browse.components.SourceIcon
import eu.kanade.presentation.components.EmptyScreen import eu.kanade.presentation.components.EmptyScreen
import eu.kanade.presentation.components.ItemBadges import eu.kanade.presentation.components.ItemBadges
import eu.kanade.presentation.components.LoadingScreen import eu.kanade.presentation.components.LoadingScreen
@ -28,6 +37,7 @@ import eu.kanade.presentation.util.topPaddingValues
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.browse.migration.sources.MigrateSourceState import eu.kanade.tachiyomi.ui.browse.migration.sources.MigrateSourceState
import eu.kanade.tachiyomi.ui.browse.migration.sources.MigrationSourcesPresenter import eu.kanade.tachiyomi.ui.browse.migration.sources.MigrationSourcesPresenter
import eu.kanade.tachiyomi.util.system.LocaleHelper
@Composable @Composable
fun MigrateSourceScreen( fun MigrateSourceScreen(
@ -107,6 +117,53 @@ fun MigrateSourceItem(
showLanguageInContent = source.lang != "", showLanguageInContent = source.lang != "",
onClickItem = onClickItem, onClickItem = onClickItem,
onLongClickItem = onLongClickItem, onLongClickItem = onLongClickItem,
icon = {
if (source.isStub) {
Image(
painter = painterResource(R.drawable.ic_warning_white_24dp),
contentDescription = "",
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.error),
)
} else {
SourceIcon(source = source)
}
},
action = { ItemBadges(primaryText = "$count") }, action = { ItemBadges(primaryText = "$count") },
content = { source, showLanguageInContent ->
Column(
modifier = Modifier
.padding(horizontal = horizontalPadding)
.weight(1f),
) {
Text(
text = source.name.ifBlank { source.id.toString() },
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.bodyMedium,
)
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
if (showLanguageInContent) {
Text(
text = LocaleHelper.getDisplayName(source.lang),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.bodySmall,
)
}
if (source.isStub) {
Text(
text = stringResource(R.string.not_installed),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.error,
)
}
}
}
},
) )
} }

View File

@ -44,7 +44,7 @@ private val defaultContent: @Composable RowScope.(Source, Boolean) -> Unit = { s
.weight(1f), .weight(1f),
) { ) {
Text( Text(
text = source.name, text = source.name.ifBlank { source.id.toString() },
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,

View File

@ -43,7 +43,12 @@ class FullBackupRestoreValidator : AbstractBackupRestoreValidator() {
val sources = backup.backupSources.associate { it.sourceId to it.name } val sources = backup.backupSources.associate { it.sourceId to it.name }
val missingSources = sources val missingSources = sources
.filter { sourceManager.get(it.key) == null } .filter { sourceManager.get(it.key) == null }
.values .values.map {
val id = it.toLongOrNull()
if (id == null) it
else sourceManager.getOrStub(id).toString()
}
.distinct()
.sorted() .sorted()
val trackers = backup.backupManga val trackers = backup.backupManga

View File

@ -71,7 +71,7 @@ class DownloadCache(
*/ */
fun isChapterDownloaded(chapter: Chapter, manga: Manga, skipCache: Boolean): Boolean { fun isChapterDownloaded(chapter: Chapter, manga: Manga, skipCache: Boolean): Boolean {
if (skipCache) { if (skipCache) {
val source = sourceManager.get(manga.source) ?: return false val source = sourceManager.getOrStub(manga.source)
return provider.findChapterDir(chapter, manga, source) != null return provider.findChapterDir(chapter, manga, source) != null
} }
@ -124,11 +124,15 @@ class DownloadCache(
private fun renew() { private fun renew() {
val onlineSources = sourceManager.getOnlineSources() val onlineSources = sourceManager.getOnlineSources()
val stubSources = sourceManager.getStubSources()
val allSource = onlineSources + stubSources
val sourceDirs = rootDir.dir.listFiles() val sourceDirs = rootDir.dir.listFiles()
.orEmpty() .orEmpty()
.associate { it.name to SourceDirectory(it) } .associate { it.name to SourceDirectory(it) }
.mapNotNullKeys { entry -> .mapNotNullKeys { entry ->
onlineSources.find { provider.getSourceDirName(it).equals(entry.key, ignoreCase = true) }?.id allSource.find { provider.getSourceDirName(it).equals(entry.key, ignoreCase = true) }?.id
} }
rootDir.files = sourceDirs rootDir.files = sourceDirs

View File

@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.extension
import android.content.Context import android.content.Context
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import com.jakewharton.rxrelay.BehaviorRelay import com.jakewharton.rxrelay.BehaviorRelay
import eu.kanade.domain.source.model.SourceData
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
@ -90,8 +91,20 @@ class ExtensionManager(
field = value field = value
availableExtensionsRelay.call(value) availableExtensionsRelay.call(value)
updatedInstalledExtensionsStatuses(value) updatedInstalledExtensionsStatuses(value)
setupAvailableExtensionsSourcesDataMap(value)
} }
private var availableExtensionsSourcesData: Map<Long, SourceData> = mapOf()
private fun setupAvailableExtensionsSourcesDataMap(extensions: List<Extension.Available>) {
if (extensions.isEmpty()) return
availableExtensionsSourcesData = extensions
.flatMap { ext -> ext.sources.map { it.toSourceData() } }
.associateBy { it.id }
}
fun getSourceData(id: Long) = availableExtensionsSourcesData[id]
/** /**
* Relay used to notify the untrusted extensions. * Relay used to notify the untrusted extensions.
*/ */

View File

@ -2,7 +2,8 @@ package eu.kanade.tachiyomi.extension.api
import android.content.Context import android.content.Context
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.extension.model.AvailableExtensionSources import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.extension.model.AvailableSources
import eu.kanade.tachiyomi.extension.model.Extension import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.extension.model.LoadResult import eu.kanade.tachiyomi.extension.model.LoadResult
import eu.kanade.tachiyomi.extension.util.ExtensionLoader import eu.kanade.tachiyomi.extension.util.ExtensionLoader
@ -22,6 +23,7 @@ internal class ExtensionGithubApi {
private val networkService: NetworkHelper by injectLazy() private val networkService: NetworkHelper by injectLazy()
private val preferences: PreferencesHelper by injectLazy() private val preferences: PreferencesHelper by injectLazy()
private val extensionManager: ExtensionManager by injectLazy()
private var requiresFallbackSource = false private var requiresFallbackSource = false
@ -54,15 +56,17 @@ internal class ExtensionGithubApi {
} }
} }
suspend fun checkForUpdates(context: Context): List<Extension.Installed>? { suspend fun checkForUpdates(context: Context, fromAvailableExtensionList: Boolean = false): List<Extension.Installed>? {
// Limit checks to once a day at most // Limit checks to once a day at most
if (Date().time < preferences.lastExtCheck().get() + TimeUnit.DAYS.toMillis(1)) { if (fromAvailableExtensionList.not() && Date().time < preferences.lastExtCheck().get() + TimeUnit.DAYS.toMillis(1)) {
return null return null
} }
val extensions = findExtensions() val extensions = if (fromAvailableExtensionList) {
extensionManager.availableExtensions
preferences.lastExtCheck().set(Date().time) } else {
findExtensions().also { preferences.lastExtCheck().set(Date().time) }
}
val installedExtensions = ExtensionLoader.loadExtensions(context) val installedExtensions = ExtensionLoader.loadExtensions(context)
.filterIsInstance<LoadResult.Success>() .filterIsInstance<LoadResult.Success>()
@ -105,11 +109,12 @@ internal class ExtensionGithubApi {
} }
} }
private fun List<ExtensionSourceJsonObject>.toExtensionSources(): List<AvailableExtensionSources> { private fun List<ExtensionSourceJsonObject>.toExtensionSources(): List<AvailableSources> {
return this.map { return this.map {
AvailableExtensionSources( AvailableSources(
name = it.name,
id = it.id, id = it.id,
lang = it.lang,
name = it.name,
baseUrl = it.baseUrl, baseUrl = it.baseUrl,
) )
} }
@ -147,7 +152,8 @@ private data class ExtensionJsonObject(
@Serializable @Serializable
private data class ExtensionSourceJsonObject( private data class ExtensionSourceJsonObject(
val name: String,
val id: Long, val id: Long,
val lang: String,
val name: String,
val baseUrl: String, val baseUrl: String,
) )

View File

@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.extension.model package eu.kanade.tachiyomi.extension.model
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import eu.kanade.domain.source.model.SourceData
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
sealed class Extension { sealed class Extension {
@ -40,7 +41,7 @@ sealed class Extension {
override val isNsfw: Boolean, override val isNsfw: Boolean,
override val hasReadme: Boolean, override val hasReadme: Boolean,
override val hasChangelog: Boolean, override val hasChangelog: Boolean,
val sources: List<AvailableExtensionSources>, val sources: List<AvailableSources>,
val apkName: String, val apkName: String,
val iconUrl: String, val iconUrl: String,
) : Extension() ) : Extension()
@ -58,8 +59,17 @@ sealed class Extension {
) : Extension() ) : Extension()
} }
data class AvailableExtensionSources( data class AvailableSources(
val name: String,
val id: Long, val id: Long,
val lang: String,
val name: String,
val baseUrl: String, val baseUrl: String,
) ) {
fun toSourceData(): SourceData {
return SourceData(
id = this.id,
lang = this.lang,
name = this.name,
)
}
}

View File

@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.source package eu.kanade.tachiyomi.source
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import eu.kanade.domain.source.model.SourceData
import eu.kanade.tachiyomi.extension.ExtensionManager import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SChapter
@ -102,3 +103,5 @@ interface Source : tachiyomi.source.Source {
fun Source.icon(): Drawable? = Injekt.get<ExtensionManager>().getAppIconForSource(this) fun Source.icon(): Drawable? = Injekt.get<ExtensionManager>().getAppIconForSource(this)
fun Source.getPreferenceKey(): String = "source_$id" fun Source.getPreferenceKey(): String = "source_$id"
fun Source.toSourceData(): SourceData = SourceData(id = id, lang = lang, name = name)

View File

@ -1,21 +1,32 @@
package eu.kanade.tachiyomi.source package eu.kanade.tachiyomi.source
import android.content.Context import android.content.Context
import eu.kanade.domain.source.interactor.GetSourceData
import eu.kanade.domain.source.interactor.UpsertSourceData
import eu.kanade.domain.source.model.SourceData
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.lang.launchIO
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.runBlocking
import rx.Observable import rx.Observable
import tachiyomi.source.model.ChapterInfo import tachiyomi.source.model.ChapterInfo
import tachiyomi.source.model.MangaInfo import tachiyomi.source.model.MangaInfo
import uy.kohesive.injekt.injectLazy
class SourceManager(private val context: Context) { class SourceManager(private val context: Context) {
private val extensionManager: ExtensionManager by injectLazy()
private val getSourceData: GetSourceData by injectLazy()
private val upsertSourceData: UpsertSourceData by injectLazy()
private val sourcesMap = mutableMapOf<Long, Source>() private val sourcesMap = mutableMapOf<Long, Source>()
private val stubSourcesMap = mutableMapOf<Long, StubSource>() private val stubSourcesMap = mutableMapOf<Long, StubSource>()
@ -34,7 +45,7 @@ class SourceManager(private val context: Context) {
fun getOrStub(sourceKey: Long): Source { fun getOrStub(sourceKey: Long): Source {
return sourcesMap[sourceKey] ?: stubSourcesMap.getOrPut(sourceKey) { return sourcesMap[sourceKey] ?: stubSourcesMap.getOrPut(sourceKey) {
StubSource(sourceKey) runBlocking { createStubSource(sourceKey) }
} }
} }
@ -42,16 +53,32 @@ class SourceManager(private val context: Context) {
fun getCatalogueSources() = sourcesMap.values.filterIsInstance<CatalogueSource>() fun getCatalogueSources() = sourcesMap.values.filterIsInstance<CatalogueSource>()
fun getStubSources(): List<StubSource> {
val onlineSourceIds = getOnlineSources().map { it.id }
return stubSourcesMap.values.filterNot { it.id in onlineSourceIds }
}
internal fun registerSource(source: Source) { internal fun registerSource(source: Source) {
if (!sourcesMap.containsKey(source.id)) { if (!sourcesMap.containsKey(source.id)) {
sourcesMap[source.id] = source sourcesMap[source.id] = source
} }
if (!stubSourcesMap.containsKey(source.id)) { registerStubSource(source.toSourceData())
stubSourcesMap[source.id] = StubSource(source.id)
}
triggerCatalogueSources() triggerCatalogueSources()
} }
private fun registerStubSource(sourceData: SourceData) {
launchIO {
val dbSourceData = getSourceData.await(sourceData.id)
if (dbSourceData != sourceData) {
upsertSourceData.await(sourceData)
}
if (stubSourcesMap[sourceData.id]?.toSourceData() != sourceData) {
stubSourcesMap[sourceData.id] = StubSource(sourceData)
}
}
}
internal fun unregisterSource(source: Source) { internal fun unregisterSource(source: Source) {
sourcesMap.remove(source.id) sourcesMap.remove(source.id)
triggerCatalogueSources() triggerCatalogueSources()
@ -67,11 +94,24 @@ class SourceManager(private val context: Context) {
LocalSource(context), LocalSource(context),
) )
private suspend fun createStubSource(id: Long): StubSource {
getSourceData.await(id)?.let {
return StubSource(it)
}
extensionManager.getSourceData(id)?.let {
registerStubSource(it)
return StubSource(it)
}
return StubSource(SourceData(id, "", ""))
}
@Suppress("OverridingDeprecatedMember") @Suppress("OverridingDeprecatedMember")
inner class StubSource(override val id: Long) : Source { open inner class StubSource(val sourceData: SourceData) : Source {
override val name: String override val name: String = sourceData.name
get() = id.toString()
override val lang: String = sourceData.lang
override val id: Long = sourceData.id
override suspend fun getMangaDetails(manga: MangaInfo): MangaInfo { override suspend fun getMangaDetails(manga: MangaInfo): MangaInfo {
throw getSourceNotInstalledException() throw getSourceNotInstalledException()
@ -98,14 +138,17 @@ class SourceManager(private val context: Context) {
} }
override fun toString(): String { override fun toString(): String {
return name if (name.isNotBlank() && lang.isNotBlank()) {
return "$name (${lang.uppercase()})"
}
return id.toString()
} }
private fun getSourceNotInstalledException(): SourceNotInstalledException { fun getSourceNotInstalledException(): SourceNotInstalledException {
return SourceNotInstalledException(id) return SourceNotInstalledException(toString())
} }
} }
inner class SourceNotInstalledException(val id: Long) : inner class SourceNotInstalledException(val sourceString: String) :
Exception(context.getString(R.string.source_not_installed, id.toString())) Exception(context.getString(R.string.source_not_installed, sourceString))
} }

View File

@ -366,7 +366,10 @@ class MainActivity : BaseActivity() {
// Extension updates // Extension updates
try { try {
ExtensionGithubApi().checkForUpdates(this@MainActivity)?.let { pendingUpdates -> ExtensionGithubApi().checkForUpdates(
this@MainActivity,
fromAvailableExtensionList = true
)?.let { pendingUpdates ->
preferences.extensionUpdatesCount().set(pendingUpdates.size) preferences.extensionUpdatesCount().set(pendingUpdates.size)
} }
} catch (e: Exception) { } catch (e: Exception) {

View File

@ -1140,7 +1140,9 @@ class MangaController :
private fun downloadChapters(chapters: List<ChapterItem>) { private fun downloadChapters(chapters: List<ChapterItem>) {
if (source is SourceManager.StubSource) { if (source is SourceManager.StubSource) {
activity?.toast(R.string.loader_not_implemented_error) activity?.let {
it.toast(it.getString(R.string.source_not_installed, source?.toString().orEmpty()))
}
return return
} }

View File

@ -228,11 +228,7 @@ class MangaInfoHeaderAdapter(
*/ */
private fun setMangaInfo() { private fun setMangaInfo() {
// Update full title TextView. // Update full title TextView.
binding.mangaFullTitle.text = if (manga.title.isBlank()) { binding.mangaFullTitle.text = manga.title.ifBlank { view.context.getString(R.string.unknown) }
view.context.getString(R.string.unknown)
} else {
manga.title
}
// Update author TextView. // Update author TextView.
binding.mangaAuthor.text = if (manga.author.isNullOrBlank()) { binding.mangaAuthor.text = if (manga.author.isNullOrBlank()) {
@ -249,6 +245,8 @@ class MangaInfoHeaderAdapter(
} }
// If manga source is known update source TextView. // If manga source is known update source TextView.
binding.mangaMissingSourceIcon.isVisible = source is SourceManager.StubSource
val mangaSource = source.toString() val mangaSource = source.toString()
with(binding.mangaSource) { with(binding.mangaSource) {
val enabledLanguages = preferences.enabledLanguages().get() val enabledLanguages = preferences.enabledLanguages().get()

View File

@ -6,6 +6,7 @@ import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.source.LocalSource import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
import eu.kanade.tachiyomi.util.system.logcat import eu.kanade.tachiyomi.util.system.logcat
@ -87,6 +88,7 @@ class ChapterLoader(
is LocalSource.Format.Epub -> EpubPageLoader(format.file) is LocalSource.Format.Epub -> EpubPageLoader(format.file)
} }
} }
source is SourceManager.StubSource -> throw source.getSourceNotInstalledException()
else -> error(context.getString(R.string.loader_not_implemented_error)) else -> error(context.getString(R.string.loader_not_implemented_error))
} }
} }

View File

@ -10,7 +10,6 @@ import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.databinding.ClearDatabaseSourceItemBinding import eu.kanade.tachiyomi.databinding.ClearDatabaseSourceItemBinding
import eu.kanade.tachiyomi.source.LocalSource import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.icon import eu.kanade.tachiyomi.source.icon
data class ClearDatabaseSourceItem(val source: Source, private val mangaCount: Long) : AbstractFlexibleItem<ClearDatabaseSourceItem.Holder>() { data class ClearDatabaseSourceItem(val source: Source, private val mangaCount: Long) : AbstractFlexibleItem<ClearDatabaseSourceItem.Holder>() {
@ -37,9 +36,9 @@ data class ClearDatabaseSourceItem(val source: Source, private val mangaCount: L
itemView.post { itemView.post {
when { when {
source.id == LocalSource.ID -> binding.thumbnail.setImageResource(R.mipmap.ic_local_source) source.icon() != null && source.id != LocalSource.ID ->
source is SourceManager.StubSource -> binding.thumbnail.setImageDrawable(null) binding.thumbnail.setImageDrawable(source.icon())
source.icon() != null -> binding.thumbnail.setImageDrawable(source.icon()) else -> binding.thumbnail.setImageResource(R.mipmap.ic_local_source)
} }
} }

View File

@ -117,6 +117,15 @@
android:textIsSelectable="false" android:textIsSelectable="false"
tools:text="Status" /> tools:text="Status" />
<ImageView
android:id="@+id/manga_missing_source_icon"
android:layout_width="16dp"
android:layout_height="match_parent"
android:layout_marginEnd="4dp"
app:srcCompat="@drawable/ic_warning_white_24dp"
app:tint="@color/error"
tools:ignore="ContentDescription" />
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"

View File

@ -133,6 +133,15 @@
android:textIsSelectable="false" android:textIsSelectable="false"
tools:ignore="HardcodedText" /> tools:ignore="HardcodedText" />
<ImageView
android:id="@+id/manga_missing_source_icon"
android:layout_width="16dp"
android:layout_height="match_parent"
android:layout_marginEnd="4dp"
app:srcCompat="@drawable/ic_warning_white_24dp"
app:tint="@color/error"
tools:ignore="ContentDescription" />
<TextView <TextView
android:id="@+id/manga_source" android:id="@+id/manga_source"
android:layout_width="wrap_content" android:layout_width="wrap_content"

View File

@ -737,6 +737,7 @@
<string name="migrate">Migrate</string> <string name="migrate">Migrate</string>
<string name="copy">Copy</string> <string name="copy">Copy</string>
<string name="empty_screen">Well, this is awkward</string> <string name="empty_screen">Well, this is awkward</string>
<string name="not_installed">Not installed</string>
<!-- Downloads activity and service --> <!-- Downloads activity and service -->
<string name="download_queue_error">Couldn\'t download chapters. You can try again in the downloads section</string> <string name="download_queue_error">Couldn\'t download chapters. You can try again in the downloads section</string>

View File

@ -0,0 +1,20 @@
CREATE TABLE sources(
_id INTEGER NOT NULL PRIMARY KEY,
lang TEXT NOT NULL,
name TEXT NOT NULL
);
getSourceData:
SELECT *
FROM sources
WHERE _id = :id;
upsert:
INSERT INTO sources(_id, lang, name)
VALUES (:id, :lang, :name)
ON CONFLICT(_id)
DO UPDATE
SET
lang = :lang,
name = :name
WHERE _id = :id;

View File

@ -0,0 +1,5 @@
CREATE TABLE sources(
_id INTEGER NOT NULL PRIMARY KEY,
lang TEXT NOT NULL,
name TEXT NOT NULL
);