diff --git a/app/src/main/java/eu/kanade/data/source/SourceMapper.kt b/app/src/main/java/eu/kanade/data/source/SourceMapper.kt index e03e1c854..20ae07288 100644 --- a/app/src/main/java/eu/kanade/data/source/SourceMapper.kt +++ b/app/src/main/java/eu/kanade/data/source/SourceMapper.kt @@ -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) +} diff --git a/app/src/main/java/eu/kanade/data/source/SourceRepositoryImpl.kt b/app/src/main/java/eu/kanade/data/source/SourceRepositoryImpl.kt index 87db87325..8c4881b25 100644 --- a/app/src/main/java/eu/kanade/data/source/SourceRepositoryImpl.kt +++ b/app/src/main/java/eu/kanade/data/source/SourceRepositoryImpl.kt @@ -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) } + } } diff --git a/app/src/main/java/eu/kanade/domain/DomainModule.kt b/app/src/main/java/eu/kanade/domain/DomainModule.kt index 4af894e6b..b024347c7 100644 --- a/app/src/main/java/eu/kanade/domain/DomainModule.kt +++ b/app/src/main/java/eu/kanade/domain/DomainModule.kt @@ -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 { 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()) } } } diff --git a/app/src/main/java/eu/kanade/domain/source/interactor/GetSourceData.kt b/app/src/main/java/eu/kanade/domain/source/interactor/GetSourceData.kt new file mode 100644 index 000000000..0cbe17608 --- /dev/null +++ b/app/src/main/java/eu/kanade/domain/source/interactor/GetSourceData.kt @@ -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 + } + } +} diff --git a/app/src/main/java/eu/kanade/domain/source/interactor/GetSourcesWithFavoriteCount.kt b/app/src/main/java/eu/kanade/domain/source/interactor/GetSourcesWithFavoriteCount.kt index e88012cad..6af6bd4a8 100644 --- a/app/src/main/java/eu/kanade/domain/source/interactor/GetSourcesWithFavoriteCount.kt +++ b/app/src/main/java/eu/kanade/domain/source/interactor/GetSourcesWithFavoriteCount.kt @@ -33,20 +33,18 @@ class GetSourcesWithFavoriteCount( strength = Collator.PRIMARY } val sortFn: (Pair, Pair) -> 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) } } diff --git a/app/src/main/java/eu/kanade/domain/source/interactor/UpsertSourceData.kt b/app/src/main/java/eu/kanade/domain/source/interactor/UpsertSourceData.kt new file mode 100644 index 000000000..a0f74ffda --- /dev/null +++ b/app/src/main/java/eu/kanade/domain/source/interactor/UpsertSourceData.kt @@ -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) + } + } +} diff --git a/app/src/main/java/eu/kanade/domain/source/model/Source.kt b/app/src/main/java/eu/kanade/domain/source/model/Source.kt index 00b494ba4..a698c07c0 100644 --- a/app/src/main/java/eu/kanade/domain/source/model/Source.kt +++ b/app/src/main/java/eu/kanade/domain/source/model/Source.kt @@ -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, ) { diff --git a/app/src/main/java/eu/kanade/domain/source/model/SourceData.kt b/app/src/main/java/eu/kanade/domain/source/model/SourceData.kt new file mode 100644 index 000000000..f9fcbf37f --- /dev/null +++ b/app/src/main/java/eu/kanade/domain/source/model/SourceData.kt @@ -0,0 +1,7 @@ +package eu.kanade.domain.source.model + +data class SourceData( + val id: Long, + val lang: String, + val name: String, +) diff --git a/app/src/main/java/eu/kanade/domain/source/repository/SourceRepository.kt b/app/src/main/java/eu/kanade/domain/source/repository/SourceRepository.kt index 00590ca9b..153ba5984 100644 --- a/app/src/main/java/eu/kanade/domain/source/repository/SourceRepository.kt +++ b/app/src/main/java/eu/kanade/domain/source/repository/SourceRepository.kt @@ -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>> fun getSourcesWithNonLibraryManga(): Flow>> + + suspend fun getSourceData(id: Long): SourceData? + + suspend fun upsertSourceData(id: Long, lang: String, name: String) } diff --git a/app/src/main/java/eu/kanade/presentation/browse/MigrateSourceScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/MigrateSourceScreen.kt index 20a6c5826..3f8dabda6 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/MigrateSourceScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/MigrateSourceScreen.kt @@ -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, + ) + } + } + } + }, ) } diff --git a/app/src/main/java/eu/kanade/presentation/browse/components/BaseSourceItem.kt b/app/src/main/java/eu/kanade/presentation/browse/components/BaseSourceItem.kt index fed26e836..9fc9254ab 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/components/BaseSourceItem.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/components/BaseSourceItem.kt @@ -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, diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/full/FullBackupRestoreValidator.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/full/FullBackupRestoreValidator.kt index 351673f09..dfe715c6e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/full/FullBackupRestoreValidator.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/full/FullBackupRestoreValidator.kt @@ -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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadCache.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadCache.kt index 2f01b89c6..4e3a3613d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadCache.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadCache.kt @@ -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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt index b9ba0c80c..c4a0c6cde 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt @@ -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 = mapOf() + + private fun setupAvailableExtensionsSourcesDataMap(extensions: List) { + 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. */ diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionGithubApi.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionGithubApi.kt index 92bae0dcb..f1fa01451 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionGithubApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionGithubApi.kt @@ -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? { + suspend fun checkForUpdates(context: Context, fromAvailableExtensionList: Boolean = false): List? { // 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() @@ -105,11 +109,12 @@ internal class ExtensionGithubApi { } } - private fun List.toExtensionSources(): List { + private fun List.toExtensionSources(): List { 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, ) diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/model/Extension.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/model/Extension.kt index c8e051a93..5157175a4 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/model/Extension.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/model/Extension.kt @@ -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, + val sources: List, 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, + ) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/Source.kt b/app/src/main/java/eu/kanade/tachiyomi/source/Source.kt index c08a610dd..4ea66c81f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/Source.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/Source.kt @@ -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().getAppIconForSource(this) fun Source.getPreferenceKey(): String = "source_$id" + +fun Source.toSourceData(): SourceData = SourceData(id = id, lang = lang, name = name) diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/SourceManager.kt b/app/src/main/java/eu/kanade/tachiyomi/source/SourceManager.kt index 87e1a7100..e471ccfca 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/SourceManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/SourceManager.kt @@ -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() private val stubSourcesMap = mutableMapOf() @@ -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() + fun getStubSources(): List { + 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)) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt index 34a107880..0fa44486f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt @@ -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) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt index 37bf74ffe..74ee7a171 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt @@ -1140,7 +1140,9 @@ class MangaController : private fun downloadChapters(chapters: List) { 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 } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoHeaderAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoHeaderAdapter.kt index 35ac3adc8..8b1fd2e5f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoHeaderAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoHeaderAdapter.kt @@ -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() diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ChapterLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ChapterLoader.kt index d214f889a..eb423ade6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ChapterLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ChapterLoader.kt @@ -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)) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/database/ClearDatabaseSourceItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/database/ClearDatabaseSourceItem.kt index 9a7933d77..d06af1259 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/database/ClearDatabaseSourceItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/database/ClearDatabaseSourceItem.kt @@ -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() { @@ -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) } } diff --git a/app/src/main/res/layout-sw720dp/manga_info_header.xml b/app/src/main/res/layout-sw720dp/manga_info_header.xml index 1d6ac9ce0..add76742e 100644 --- a/app/src/main/res/layout-sw720dp/manga_info_header.xml +++ b/app/src/main/res/layout-sw720dp/manga_info_header.xml @@ -117,6 +117,15 @@ android:textIsSelectable="false" tools:text="Status" /> + + + + Migrate Copy Well, this is awkward + Not installed Couldn\'t download chapters. You can try again in the downloads section diff --git a/app/src/main/sqldelight/data/sources.sq b/app/src/main/sqldelight/data/sources.sq new file mode 100644 index 000000000..cbbb54c5f --- /dev/null +++ b/app/src/main/sqldelight/data/sources.sq @@ -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; \ No newline at end of file diff --git a/app/src/main/sqldelight/migrations/16.sqm b/app/src/main/sqldelight/migrations/16.sqm new file mode 100644 index 000000000..6a40f9bff --- /dev/null +++ b/app/src/main/sqldelight/migrations/16.sqm @@ -0,0 +1,5 @@ +CREATE TABLE sources( + _id INTEGER NOT NULL PRIMARY KEY, + lang TEXT NOT NULL, + name TEXT NOT NULL +); \ No newline at end of file