mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-11-03 23:58:55 +01:00 
			
		
		
		
	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:
		@@ -1,17 +1,24 @@
 | 
			
		||||
package eu.kanade.data.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.SourceManager
 | 
			
		||||
 | 
			
		||||
val sourceMapper: (eu.kanade.tachiyomi.source.Source) -> Source = { source ->
 | 
			
		||||
    Source(
 | 
			
		||||
        source.id,
 | 
			
		||||
        source.lang,
 | 
			
		||||
        source.name,
 | 
			
		||||
        false,
 | 
			
		||||
        supportsLatest = false,
 | 
			
		||||
        isStub = source is SourceManager.StubSource,
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
val catalogueSourceMapper: (CatalogueSource) -> Source = { source ->
 | 
			
		||||
    sourceMapper(source).copy(supportsLatest = source.supportsLatest)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
val sourceDataMapper: (Long, String, String) -> SourceData = { id, lang, name ->
 | 
			
		||||
    SourceData(id, lang, name)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,7 @@ package eu.kanade.data.source
 | 
			
		||||
 | 
			
		||||
import eu.kanade.data.DatabaseHandler
 | 
			
		||||
import eu.kanade.domain.source.model.Source
 | 
			
		||||
import eu.kanade.domain.source.model.SourceData
 | 
			
		||||
import eu.kanade.domain.source.repository.SourceRepository
 | 
			
		||||
import eu.kanade.tachiyomi.source.LocalSource
 | 
			
		||||
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) }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -27,12 +27,14 @@ import eu.kanade.domain.manga.interactor.UpdateManga
 | 
			
		||||
import eu.kanade.domain.manga.repository.MangaRepository
 | 
			
		||||
import eu.kanade.domain.source.interactor.GetEnabledSources
 | 
			
		||||
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.GetSourcesWithNonLibraryManga
 | 
			
		||||
import eu.kanade.domain.source.interactor.SetMigrateSorting
 | 
			
		||||
import eu.kanade.domain.source.interactor.ToggleLanguage
 | 
			
		||||
import eu.kanade.domain.source.interactor.ToggleSource
 | 
			
		||||
import eu.kanade.domain.source.interactor.ToggleSourcePin
 | 
			
		||||
import eu.kanade.domain.source.interactor.UpsertSourceData
 | 
			
		||||
import eu.kanade.domain.source.repository.SourceRepository
 | 
			
		||||
import uy.kohesive.injekt.api.InjektModule
 | 
			
		||||
import uy.kohesive.injekt.api.InjektRegistrar
 | 
			
		||||
@@ -71,11 +73,13 @@ class DomainModule : InjektModule {
 | 
			
		||||
        addSingletonFactory<SourceRepository> { SourceRepositoryImpl(get(), get()) }
 | 
			
		||||
        addFactory { GetEnabledSources(get(), get()) }
 | 
			
		||||
        addFactory { GetLanguagesWithSources(get(), get()) }
 | 
			
		||||
        addFactory { GetSourceData(get()) }
 | 
			
		||||
        addFactory { GetSourcesWithFavoriteCount(get(), get()) }
 | 
			
		||||
        addFactory { GetSourcesWithNonLibraryManga(get()) }
 | 
			
		||||
        addFactory { SetMigrateSorting(get()) }
 | 
			
		||||
        addFactory { ToggleLanguage(get()) }
 | 
			
		||||
        addFactory { ToggleSource(get()) }
 | 
			
		||||
        addFactory { ToggleSourcePin(get()) }
 | 
			
		||||
        addFactory { UpsertSourceData(get()) }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -33,20 +33,18 @@ class GetSourcesWithFavoriteCount(
 | 
			
		||||
            strength = Collator.PRIMARY
 | 
			
		||||
        }
 | 
			
		||||
        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) {
 | 
			
		||||
                SetMigrateSorting.Mode.ALPHABETICAL -> {
 | 
			
		||||
                    when {
 | 
			
		||||
                        id1 != null && id2 == null -> -1
 | 
			
		||||
                        id2 != null && id1 == null -> 1
 | 
			
		||||
                        a.first.isStub && b.first.isStub.not() -> -1
 | 
			
		||||
                        b.first.isStub && a.first.isStub.not() -> 1
 | 
			
		||||
                        else -> collator.compare(a.first.name.lowercase(locale), b.first.name.lowercase(locale))
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                SetMigrateSorting.Mode.TOTAL -> {
 | 
			
		||||
                    when {
 | 
			
		||||
                        id1 != null && id2 == null -> -1
 | 
			
		||||
                        id2 != null && id1 == null -> 1
 | 
			
		||||
                        a.first.isStub && b.first.isStub.not() -> -1
 | 
			
		||||
                        b.first.isStub && a.first.isStub.not() -> 1
 | 
			
		||||
                        else -> a.second.compareTo(b.second)
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -12,6 +12,7 @@ data class Source(
 | 
			
		||||
    val lang: String,
 | 
			
		||||
    val name: String,
 | 
			
		||||
    val supportsLatest: Boolean,
 | 
			
		||||
    val isStub: Boolean,
 | 
			
		||||
    val pin: Pins = Pins.unpinned,
 | 
			
		||||
    val isUsedLast: Boolean = false,
 | 
			
		||||
) {
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,7 @@
 | 
			
		||||
package eu.kanade.domain.source.model
 | 
			
		||||
 | 
			
		||||
data class SourceData(
 | 
			
		||||
    val id: Long,
 | 
			
		||||
    val lang: String,
 | 
			
		||||
    val name: String,
 | 
			
		||||
)
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
package eu.kanade.domain.source.repository
 | 
			
		||||
 | 
			
		||||
import eu.kanade.domain.source.model.Source
 | 
			
		||||
import eu.kanade.domain.source.model.SourceData
 | 
			
		||||
import kotlinx.coroutines.flow.Flow
 | 
			
		||||
import eu.kanade.tachiyomi.source.Source as LoadedSource
 | 
			
		||||
 | 
			
		||||
@@ -13,4 +14,8 @@ interface SourceRepository {
 | 
			
		||||
    fun getSourcesWithFavoriteCount(): Flow<List<Pair<Source, Long>>>
 | 
			
		||||
 | 
			
		||||
    fun getSourcesWithNonLibraryManga(): Flow<List<Pair<LoadedSource, Long>>>
 | 
			
		||||
 | 
			
		||||
    suspend fun getSourceData(id: Long): SourceData?
 | 
			
		||||
 | 
			
		||||
    suspend fun upsertSourceData(id: Long, lang: String, name: String)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,9 @@
 | 
			
		||||
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.asPaddingValues
 | 
			
		||||
import androidx.compose.foundation.layout.navigationBars
 | 
			
		||||
@@ -10,13 +14,18 @@ import androidx.compose.material3.Text
 | 
			
		||||
import androidx.compose.runtime.Composable
 | 
			
		||||
import androidx.compose.runtime.collectAsState
 | 
			
		||||
import androidx.compose.runtime.getValue
 | 
			
		||||
import androidx.compose.ui.Alignment
 | 
			
		||||
import androidx.compose.ui.Modifier
 | 
			
		||||
import androidx.compose.ui.graphics.ColorFilter
 | 
			
		||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
 | 
			
		||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
 | 
			
		||||
import androidx.compose.ui.res.painterResource
 | 
			
		||||
import androidx.compose.ui.res.stringResource
 | 
			
		||||
import androidx.compose.ui.text.style.TextOverflow
 | 
			
		||||
import androidx.compose.ui.unit.dp
 | 
			
		||||
import eu.kanade.domain.source.model.Source
 | 
			
		||||
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.ItemBadges
 | 
			
		||||
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.ui.browse.migration.sources.MigrateSourceState
 | 
			
		||||
import eu.kanade.tachiyomi.ui.browse.migration.sources.MigrationSourcesPresenter
 | 
			
		||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun MigrateSourceScreen(
 | 
			
		||||
@@ -107,6 +117,53 @@ fun MigrateSourceItem(
 | 
			
		||||
        showLanguageInContent = source.lang != "",
 | 
			
		||||
        onClickItem = onClickItem,
 | 
			
		||||
        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") },
 | 
			
		||||
        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,
 | 
			
		||||
                        )
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -44,7 +44,7 @@ private val defaultContent: @Composable RowScope.(Source, Boolean) -> Unit = { s
 | 
			
		||||
            .weight(1f),
 | 
			
		||||
    ) {
 | 
			
		||||
        Text(
 | 
			
		||||
            text = source.name,
 | 
			
		||||
            text = source.name.ifBlank { source.id.toString() },
 | 
			
		||||
            maxLines = 1,
 | 
			
		||||
            overflow = TextOverflow.Ellipsis,
 | 
			
		||||
            style = MaterialTheme.typography.bodyMedium,
 | 
			
		||||
 
 | 
			
		||||
@@ -43,7 +43,12 @@ class FullBackupRestoreValidator : AbstractBackupRestoreValidator() {
 | 
			
		||||
        val sources = backup.backupSources.associate { it.sourceId to it.name }
 | 
			
		||||
        val missingSources = sources
 | 
			
		||||
            .filter { sourceManager.get(it.key) == null }
 | 
			
		||||
            .values
 | 
			
		||||
            .values.map {
 | 
			
		||||
                val id = it.toLongOrNull()
 | 
			
		||||
                if (id == null) it
 | 
			
		||||
                else sourceManager.getOrStub(id).toString()
 | 
			
		||||
            }
 | 
			
		||||
            .distinct()
 | 
			
		||||
            .sorted()
 | 
			
		||||
 | 
			
		||||
        val trackers = backup.backupManga
 | 
			
		||||
 
 | 
			
		||||
@@ -71,7 +71,7 @@ class DownloadCache(
 | 
			
		||||
     */
 | 
			
		||||
    fun isChapterDownloaded(chapter: Chapter, manga: Manga, skipCache: Boolean): Boolean {
 | 
			
		||||
        if (skipCache) {
 | 
			
		||||
            val source = sourceManager.get(manga.source) ?: return false
 | 
			
		||||
            val source = sourceManager.getOrStub(manga.source)
 | 
			
		||||
            return provider.findChapterDir(chapter, manga, source) != null
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@@ -124,11 +124,15 @@ class DownloadCache(
 | 
			
		||||
    private fun renew() {
 | 
			
		||||
        val onlineSources = sourceManager.getOnlineSources()
 | 
			
		||||
 | 
			
		||||
        val stubSources = sourceManager.getStubSources()
 | 
			
		||||
 | 
			
		||||
        val allSource = onlineSources + stubSources
 | 
			
		||||
 | 
			
		||||
        val sourceDirs = rootDir.dir.listFiles()
 | 
			
		||||
            .orEmpty()
 | 
			
		||||
            .associate { it.name to SourceDirectory(it) }
 | 
			
		||||
            .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
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.extension
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.graphics.drawable.Drawable
 | 
			
		||||
import com.jakewharton.rxrelay.BehaviorRelay
 | 
			
		||||
import eu.kanade.domain.source.model.SourceData
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
 | 
			
		||||
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
 | 
			
		||||
@@ -90,8 +91,20 @@ class ExtensionManager(
 | 
			
		||||
            field = value
 | 
			
		||||
            availableExtensionsRelay.call(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.
 | 
			
		||||
     */
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,8 @@ package eu.kanade.tachiyomi.extension.api
 | 
			
		||||
 | 
			
		||||
import android.content.Context
 | 
			
		||||
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.LoadResult
 | 
			
		||||
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
 | 
			
		||||
@@ -22,6 +23,7 @@ internal class ExtensionGithubApi {
 | 
			
		||||
 | 
			
		||||
    private val networkService: NetworkHelper by injectLazy()
 | 
			
		||||
    private val preferences: PreferencesHelper by injectLazy()
 | 
			
		||||
    private val extensionManager: ExtensionManager by injectLazy()
 | 
			
		||||
 | 
			
		||||
    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
 | 
			
		||||
        if (Date().time < preferences.lastExtCheck().get() + TimeUnit.DAYS.toMillis(1)) {
 | 
			
		||||
        if (fromAvailableExtensionList.not() && Date().time < preferences.lastExtCheck().get() + TimeUnit.DAYS.toMillis(1)) {
 | 
			
		||||
            return null
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        val extensions = findExtensions()
 | 
			
		||||
 | 
			
		||||
        preferences.lastExtCheck().set(Date().time)
 | 
			
		||||
        val extensions = if (fromAvailableExtensionList) {
 | 
			
		||||
            extensionManager.availableExtensions
 | 
			
		||||
        } else {
 | 
			
		||||
            findExtensions().also { preferences.lastExtCheck().set(Date().time) }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        val installedExtensions = ExtensionLoader.loadExtensions(context)
 | 
			
		||||
            .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 {
 | 
			
		||||
            AvailableExtensionSources(
 | 
			
		||||
                name = it.name,
 | 
			
		||||
            AvailableSources(
 | 
			
		||||
                id = it.id,
 | 
			
		||||
                lang = it.lang,
 | 
			
		||||
                name = it.name,
 | 
			
		||||
                baseUrl = it.baseUrl,
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
@@ -147,7 +152,8 @@ private data class ExtensionJsonObject(
 | 
			
		||||
 | 
			
		||||
@Serializable
 | 
			
		||||
private data class ExtensionSourceJsonObject(
 | 
			
		||||
    val name: String,
 | 
			
		||||
    val id: Long,
 | 
			
		||||
    val lang: String,
 | 
			
		||||
    val name: String,
 | 
			
		||||
    val baseUrl: String,
 | 
			
		||||
)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
package eu.kanade.tachiyomi.extension.model
 | 
			
		||||
 | 
			
		||||
import android.graphics.drawable.Drawable
 | 
			
		||||
import eu.kanade.domain.source.model.SourceData
 | 
			
		||||
import eu.kanade.tachiyomi.source.Source
 | 
			
		||||
 | 
			
		||||
sealed class Extension {
 | 
			
		||||
@@ -40,7 +41,7 @@ sealed class Extension {
 | 
			
		||||
        override val isNsfw: Boolean,
 | 
			
		||||
        override val hasReadme: Boolean,
 | 
			
		||||
        override val hasChangelog: Boolean,
 | 
			
		||||
        val sources: List<AvailableExtensionSources>,
 | 
			
		||||
        val sources: List<AvailableSources>,
 | 
			
		||||
        val apkName: String,
 | 
			
		||||
        val iconUrl: String,
 | 
			
		||||
    ) : Extension()
 | 
			
		||||
@@ -58,8 +59,17 @@ sealed class Extension {
 | 
			
		||||
    ) : Extension()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
data class AvailableExtensionSources(
 | 
			
		||||
    val name: String,
 | 
			
		||||
data class AvailableSources(
 | 
			
		||||
    val id: Long,
 | 
			
		||||
    val lang: String,
 | 
			
		||||
    val name: String,
 | 
			
		||||
    val baseUrl: String,
 | 
			
		||||
)
 | 
			
		||||
) {
 | 
			
		||||
    fun toSourceData(): SourceData {
 | 
			
		||||
        return SourceData(
 | 
			
		||||
            id = this.id,
 | 
			
		||||
            lang = this.lang,
 | 
			
		||||
            name = this.name,
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
package eu.kanade.tachiyomi.source
 | 
			
		||||
 | 
			
		||||
import android.graphics.drawable.Drawable
 | 
			
		||||
import eu.kanade.domain.source.model.SourceData
 | 
			
		||||
import eu.kanade.tachiyomi.extension.ExtensionManager
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.Page
 | 
			
		||||
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.getPreferenceKey(): String = "source_$id"
 | 
			
		||||
 | 
			
		||||
fun Source.toSourceData(): SourceData = SourceData(id = id, lang = lang, name = name)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,21 +1,32 @@
 | 
			
		||||
package eu.kanade.tachiyomi.source
 | 
			
		||||
 | 
			
		||||
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.extension.ExtensionManager
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.Page
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.SChapter
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.SManga
 | 
			
		||||
import eu.kanade.tachiyomi.source.online.HttpSource
 | 
			
		||||
import eu.kanade.tachiyomi.util.lang.launchIO
 | 
			
		||||
import kotlinx.coroutines.flow.Flow
 | 
			
		||||
import kotlinx.coroutines.flow.MutableStateFlow
 | 
			
		||||
import kotlinx.coroutines.flow.map
 | 
			
		||||
import kotlinx.coroutines.flow.update
 | 
			
		||||
import kotlinx.coroutines.runBlocking
 | 
			
		||||
import rx.Observable
 | 
			
		||||
import tachiyomi.source.model.ChapterInfo
 | 
			
		||||
import tachiyomi.source.model.MangaInfo
 | 
			
		||||
import uy.kohesive.injekt.injectLazy
 | 
			
		||||
 | 
			
		||||
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 stubSourcesMap = mutableMapOf<Long, StubSource>()
 | 
			
		||||
 | 
			
		||||
@@ -34,7 +45,7 @@ class SourceManager(private val context: Context) {
 | 
			
		||||
 | 
			
		||||
    fun getOrStub(sourceKey: Long): Source {
 | 
			
		||||
        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 getStubSources(): List<StubSource> {
 | 
			
		||||
        val onlineSourceIds = getOnlineSources().map { it.id }
 | 
			
		||||
        return stubSourcesMap.values.filterNot { it.id in onlineSourceIds }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    internal fun registerSource(source: Source) {
 | 
			
		||||
        if (!sourcesMap.containsKey(source.id)) {
 | 
			
		||||
            sourcesMap[source.id] = source
 | 
			
		||||
        }
 | 
			
		||||
        if (!stubSourcesMap.containsKey(source.id)) {
 | 
			
		||||
            stubSourcesMap[source.id] = StubSource(source.id)
 | 
			
		||||
        }
 | 
			
		||||
        registerStubSource(source.toSourceData())
 | 
			
		||||
        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) {
 | 
			
		||||
        sourcesMap.remove(source.id)
 | 
			
		||||
        triggerCatalogueSources()
 | 
			
		||||
@@ -67,11 +94,24 @@ class SourceManager(private val context: 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")
 | 
			
		||||
    inner class StubSource(override val id: Long) : Source {
 | 
			
		||||
    open inner class StubSource(val sourceData: SourceData) : Source {
 | 
			
		||||
 | 
			
		||||
        override val name: String
 | 
			
		||||
            get() = id.toString()
 | 
			
		||||
        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 {
 | 
			
		||||
            throw getSourceNotInstalledException()
 | 
			
		||||
@@ -98,14 +138,17 @@ class SourceManager(private val context: Context) {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        override fun toString(): String {
 | 
			
		||||
            return name
 | 
			
		||||
            if (name.isNotBlank() && lang.isNotBlank()) {
 | 
			
		||||
                return "$name (${lang.uppercase()})"
 | 
			
		||||
            }
 | 
			
		||||
            return id.toString()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private fun getSourceNotInstalledException(): SourceNotInstalledException {
 | 
			
		||||
            return SourceNotInstalledException(id)
 | 
			
		||||
        fun getSourceNotInstalledException(): SourceNotInstalledException {
 | 
			
		||||
            return SourceNotInstalledException(toString())
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    inner class SourceNotInstalledException(val id: Long) :
 | 
			
		||||
        Exception(context.getString(R.string.source_not_installed, id.toString()))
 | 
			
		||||
    inner class SourceNotInstalledException(val sourceString: String) :
 | 
			
		||||
        Exception(context.getString(R.string.source_not_installed, sourceString))
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -366,7 +366,10 @@ class MainActivity : BaseActivity() {
 | 
			
		||||
 | 
			
		||||
            // Extension updates
 | 
			
		||||
            try {
 | 
			
		||||
                ExtensionGithubApi().checkForUpdates(this@MainActivity)?.let { pendingUpdates ->
 | 
			
		||||
                ExtensionGithubApi().checkForUpdates(
 | 
			
		||||
                    this@MainActivity,
 | 
			
		||||
                    fromAvailableExtensionList = true
 | 
			
		||||
                )?.let { pendingUpdates ->
 | 
			
		||||
                    preferences.extensionUpdatesCount().set(pendingUpdates.size)
 | 
			
		||||
                }
 | 
			
		||||
            } catch (e: Exception) {
 | 
			
		||||
 
 | 
			
		||||
@@ -1140,7 +1140,9 @@ class MangaController :
 | 
			
		||||
 | 
			
		||||
    private fun downloadChapters(chapters: List<ChapterItem>) {
 | 
			
		||||
        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
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -228,11 +228,7 @@ class MangaInfoHeaderAdapter(
 | 
			
		||||
         */
 | 
			
		||||
        private fun setMangaInfo() {
 | 
			
		||||
            // Update full title TextView.
 | 
			
		||||
            binding.mangaFullTitle.text = if (manga.title.isBlank()) {
 | 
			
		||||
                view.context.getString(R.string.unknown)
 | 
			
		||||
            } else {
 | 
			
		||||
                manga.title
 | 
			
		||||
            }
 | 
			
		||||
            binding.mangaFullTitle.text = manga.title.ifBlank { view.context.getString(R.string.unknown) }
 | 
			
		||||
 | 
			
		||||
            // Update author TextView.
 | 
			
		||||
            binding.mangaAuthor.text = if (manga.author.isNullOrBlank()) {
 | 
			
		||||
@@ -249,6 +245,8 @@ class MangaInfoHeaderAdapter(
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // If manga source is known update source TextView.
 | 
			
		||||
            binding.mangaMissingSourceIcon.isVisible = source is SourceManager.StubSource
 | 
			
		||||
 | 
			
		||||
            val mangaSource = source.toString()
 | 
			
		||||
            with(binding.mangaSource) {
 | 
			
		||||
                val enabledLanguages = preferences.enabledLanguages().get()
 | 
			
		||||
 
 | 
			
		||||
@@ -6,6 +6,7 @@ import eu.kanade.tachiyomi.data.database.models.Manga
 | 
			
		||||
import eu.kanade.tachiyomi.data.download.DownloadManager
 | 
			
		||||
import eu.kanade.tachiyomi.source.LocalSource
 | 
			
		||||
import eu.kanade.tachiyomi.source.Source
 | 
			
		||||
import eu.kanade.tachiyomi.source.SourceManager
 | 
			
		||||
import eu.kanade.tachiyomi.source.online.HttpSource
 | 
			
		||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
 | 
			
		||||
import eu.kanade.tachiyomi.util.system.logcat
 | 
			
		||||
@@ -87,6 +88,7 @@ class ChapterLoader(
 | 
			
		||||
                    is LocalSource.Format.Epub -> EpubPageLoader(format.file)
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            source is SourceManager.StubSource -> throw source.getSourceNotInstalledException()
 | 
			
		||||
            else -> error(context.getString(R.string.loader_not_implemented_error))
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -10,7 +10,6 @@ import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.databinding.ClearDatabaseSourceItemBinding
 | 
			
		||||
import eu.kanade.tachiyomi.source.LocalSource
 | 
			
		||||
import eu.kanade.tachiyomi.source.Source
 | 
			
		||||
import eu.kanade.tachiyomi.source.SourceManager
 | 
			
		||||
import eu.kanade.tachiyomi.source.icon
 | 
			
		||||
 | 
			
		||||
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 {
 | 
			
		||||
                when {
 | 
			
		||||
                    source.id == LocalSource.ID -> binding.thumbnail.setImageResource(R.mipmap.ic_local_source)
 | 
			
		||||
                    source is SourceManager.StubSource -> binding.thumbnail.setImageDrawable(null)
 | 
			
		||||
                    source.icon() != null -> binding.thumbnail.setImageDrawable(source.icon())
 | 
			
		||||
                    source.icon() != null && source.id != LocalSource.ID ->
 | 
			
		||||
                        binding.thumbnail.setImageDrawable(source.icon())
 | 
			
		||||
                    else -> binding.thumbnail.setImageResource(R.mipmap.ic_local_source)
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user