Maintain source info in the database. (#6389)

* Maintain Source Info in database

* Review changes and cleanups

* Review changes 2

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

View file

@ -1,17 +1,24 @@
package eu.kanade.data.source
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)
}

View file

@ -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) }
}
}

View file

@ -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()) }
}
}

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
}
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)
}
}

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 name: String,
val supportsLatest: Boolean,
val isStub: Boolean,
val pin: Pins = Pins.unpinned,
val isUsedLast: Boolean = false,
) {

View file

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

View file

@ -1,6 +1,7 @@
package eu.kanade.domain.source.repository
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)
}

View file

@ -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,
)
}
}
}
},
)
}

View file

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

View file

@ -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

View file

@ -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

View file

@ -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.
*/

View file

@ -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,
)

View file

@ -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,
)
}
}

View file

@ -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)

View file

@ -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))
}

View file

@ -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) {

View file

@ -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
}

View file

@ -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()

View file

@ -6,6 +6,7 @@ import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.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))
}
}

View file

@ -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)
}
}

View file

@ -117,6 +117,15 @@
android:textIsSelectable="false"
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
android:layout_width="wrap_content"
android:layout_height="wrap_content"

View file

@ -133,6 +133,15 @@
android:textIsSelectable="false"
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
android:id="@+id/manga_source"
android:layout_width="wrap_content"

View file

@ -737,6 +737,7 @@
<string name="migrate">Migrate</string>
<string name="copy">Copy</string>
<string name="empty_screen">Well, this is awkward</string>
<string name="not_installed">Not installed</string>
<!-- Downloads activity and service -->
<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
);