Maintain source info in the database. (#6389)

* Maintain Source Info in database

* Review changes and cleanups

* Review changes 2

* Review Changes 3

(cherry picked from commit 9d5b7de1d89a353d0bf73ce6befd9f5667d94d17)

# Conflicts:
#	app/src/main/java/eu/kanade/domain/DomainModule.kt
#	app/src/main/java/eu/kanade/presentation/browse/MigrateSourceScreen.kt
#	app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt
#	app/src/main/java/eu/kanade/tachiyomi/source/SourceManager.kt
This commit is contained in:
AntsyLich 2022-06-14 19:10:40 +06:00 committed by Jobobby04
parent 62873c4e67
commit 8a06bc42d8
28 changed files with 312 additions and 49 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
@ -50,4 +51,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

@ -30,6 +30,7 @@ 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.GetShowLatest import eu.kanade.domain.source.interactor.GetShowLatest
import eu.kanade.domain.source.interactor.GetSourceCategories import eu.kanade.domain.source.interactor.GetSourceCategories
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
@ -39,6 +40,7 @@ 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.ToggleSources import eu.kanade.domain.source.interactor.ToggleSources
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
@ -77,12 +79,14 @@ 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()) }
// SY --> // SY -->
addFactory { GetSourceCategories(get()) } addFactory { GetSourceCategories(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,
// SY --> // SY -->

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
@ -12,13 +16,18 @@ import androidx.compose.material3.TextButton
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
@ -30,6 +39,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(
@ -114,8 +124,20 @@ 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 = { action = {
ItemBadges(primaryText = "$count") ItemBadges(primaryText = "$count")
// SY -->
TextButton(onClick = onClickAll) { TextButton(onClick = onClickAll) {
Text( Text(
text = stringResource(id = R.string.all), text = stringResource(id = R.string.all),
@ -124,6 +146,43 @@ fun MigrateSourceItem(
), ),
) )
} }
// SY <--
},
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
} }
@ -126,11 +126,15 @@ class DownloadCache(
val onlineSources = sourceManager.getVisibleOnlineSources() val onlineSources = sourceManager.getVisibleOnlineSources()
// SY <-- // SY <--
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

@ -4,6 +4,7 @@ import android.content.Context
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
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
@ -104,8 +105,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]
// SY --> // SY -->
var unalteredAvailableExtensions = emptyList<Extension.Available>() var unalteredAvailableExtensions = emptyList<Extension.Available>()
// SY <-- // SY <--

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
@ -23,6 +24,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
@ -69,15 +71,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) }
}
// SY --> // SY -->
val blacklistEnabled = preferences.enableSourceBlacklist().get() val blacklistEnabled = preferences.enableSourceBlacklist().get()
@ -136,11 +140,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,
) )
} }
@ -188,7 +193,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 {
@ -43,7 +44,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,
// SY --> // SY -->
@ -65,8 +66,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,8 +1,12 @@
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.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
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
@ -17,6 +21,7 @@ import eu.kanade.tachiyomi.source.online.english.EightMuses
import eu.kanade.tachiyomi.source.online.english.HBrowse import eu.kanade.tachiyomi.source.online.english.HBrowse
import eu.kanade.tachiyomi.source.online.english.Pururin import eu.kanade.tachiyomi.source.online.english.Pururin
import eu.kanade.tachiyomi.source.online.english.Tsumino import eu.kanade.tachiyomi.source.online.english.Tsumino
import eu.kanade.tachiyomi.util.lang.launchIO
import exh.log.xLogD import exh.log.xLogD
import exh.source.BlacklistedSources import exh.source.BlacklistedSources
import exh.source.DelegatedHttpSource import exh.source.DelegatedHttpSource
@ -40,6 +45,7 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
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
@ -48,6 +54,10 @@ import kotlin.reflect.KClass
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>()
@ -90,19 +100,24 @@ 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) }
} }
} }
fun getOnlineSources() = sourcesMap.values.filterIsInstance<HttpSource>() fun getOnlineSources() = sourcesMap.values.filterIsInstance<HttpSource>()
fun getCatalogueSources() = sourcesMap.values.filterIsInstance<CatalogueSource>()
fun getStubSources(): List<StubSource> {
val onlineSourceIds = getOnlineSources().map { it.id }
return stubSourcesMap.values.filterNot { it.id in onlineSourceIds }
}
// SY -->
fun getVisibleOnlineSources() = sourcesMap.values.filterIsInstance<HttpSource>().filter { fun getVisibleOnlineSources() = sourcesMap.values.filterIsInstance<HttpSource>().filter {
it.id !in BlacklistedSources.HIDDEN_SOURCES it.id !in BlacklistedSources.HIDDEN_SOURCES
} }
fun getCatalogueSources() = sourcesMap.values.filterIsInstance<CatalogueSource>()
// SY -->
fun getVisibleCatalogueSources() = sourcesMap.values.filterIsInstance<CatalogueSource>().filter { fun getVisibleCatalogueSources() = sourcesMap.values.filterIsInstance<CatalogueSource>().filter {
it.id !in BlacklistedSources.HIDDEN_SOURCES it.id !in BlacklistedSources.HIDDEN_SOURCES
} }
@ -148,9 +163,23 @@ class SourceManager(private val context: Context) {
if (!sourcesMap.containsKey(source.id)) { if (!sourcesMap.containsKey(source.id)) {
sourcesMap[source.id] = newSource sourcesMap[source.id] = newSource
} }
registerStubSource(source.toSourceData())
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()
@ -181,11 +210,25 @@ class SourceManager(private val context: Context) {
// SY <-- // SY <--
@Suppress("OverridingDeprecatedMember") private suspend fun createStubSource(id: Long): StubSource {
inner class StubSource(override val id: Long) : Source { getSourceData.await(id)?.let {
return StubSource(it)
}
extensionManager.getSourceData(id)?.let {
registerStubSource(it)
return StubSource(it)
}
return StubSource(SourceData(id, "", ""))
}
override val name: String @Suppress("OverridingDeprecatedMember")
get() = id.toString() open inner class StubSource(val sourceData: SourceData) : Source {
override val name: String = sourceData.name
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()
@ -212,16 +255,19 @@ 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))
// SY --> // SY -->
companion object { companion object {

View File

@ -420,7 +420,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

@ -1360,7 +1360,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

@ -287,11 +287,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()) {
@ -308,6 +304,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

@ -120,6 +120,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
);