diff --git a/app/src/main/java/eu/kanade/presentation/browse/RelatedMangasScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/RelatedMangasScreen.kt index 902671e5d..a0782f936 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/RelatedMangasScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/RelatedMangasScreen.kt @@ -154,9 +154,10 @@ fun RelatedMangaTitle( } fun LazyGridScope.header( + key: Any? = null, content: @Composable LazyGridItemScope.() -> Unit, ) { - item(span = { GridItemSpan(this.maxLineSpan) }, content = content) + item(key = key, span = { GridItemSpan(this.maxLineSpan) }, content = content) } @Composable diff --git a/app/src/main/java/eu/kanade/presentation/browse/components/RelatedMangasComfortableGrid.kt b/app/src/main/java/eu/kanade/presentation/browse/components/RelatedMangasComfortableGrid.kt index daad7929b..51b51a64a 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/components/RelatedMangasComfortableGrid.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/components/RelatedMangasComfortableGrid.kt @@ -18,6 +18,7 @@ import eu.kanade.presentation.library.components.CommonMangaItemDefaults import eu.kanade.tachiyomi.ui.manga.RelatedManga import tachiyomi.domain.manga.model.Manga import tachiyomi.i18n.MR +import tachiyomi.i18n.sy.SYMR import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.util.plus @@ -40,10 +41,10 @@ fun RelatedMangasComfortableGrid( verticalArrangement = Arrangement.spacedBy(CommonMangaItemDefaults.GridVerticalSpacer), horizontalArrangement = Arrangement.spacedBy(CommonMangaItemDefaults.GridHorizontalSpacer), ) { - relatedMangas.forEach { - val isLoading = it is RelatedManga.Loading + relatedMangas.forEach { related -> + val isLoading = related is RelatedManga.Loading if (isLoading) { - header { + header(key = "$related#header") { RelatedMangaTitle( title = stringResource(MR.strings.loading), subtitle = null, @@ -52,19 +53,27 @@ fun RelatedMangasComfortableGrid( modifier = Modifier.background(MaterialTheme.colorScheme.background), ) } - header { RelatedMangasLoadingItem() } + header(key = "$related#content") { RelatedMangasLoadingItem() } } else { - val relatedManga = it as RelatedManga.Success - header { + val relatedManga = related as RelatedManga.Success + header(key = "${related.keyword}#header") { RelatedMangaTitle( - title = relatedManga.keyword, + title = if (relatedManga.keyword.isNotBlank()) { + stringResource(SYMR.strings.related_mangas_more) + } else { + stringResource(SYMR.strings.pref_source_related_mangas) + }, subtitle = null, - onClick = { onKeywordClick(relatedManga.keyword) }, - onLongClick = { onKeywordLongClick(relatedManga.keyword) }, + onClick = { + if (relatedManga.keyword.isNotBlank()) onKeywordClick(relatedManga.keyword) + }, + onLongClick = { + if (relatedManga.keyword.isNotBlank()) onKeywordLongClick(relatedManga.keyword) + }, modifier = Modifier.background(MaterialTheme.colorScheme.background), ) } - items(count = relatedManga.mangaList.size) { index -> + items(key = { relatedManga.mangaList[it].id }, count = relatedManga.mangaList.size) { index -> val manga by getManga(relatedManga.mangaList[index]) BrowseSourceComfortableGridItem( manga = manga, diff --git a/app/src/main/java/eu/kanade/presentation/browse/components/RelatedMangasCompactGrid.kt b/app/src/main/java/eu/kanade/presentation/browse/components/RelatedMangasCompactGrid.kt index 4affe921a..2dafbd8c3 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/components/RelatedMangasCompactGrid.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/components/RelatedMangasCompactGrid.kt @@ -18,6 +18,7 @@ import eu.kanade.presentation.library.components.CommonMangaItemDefaults import eu.kanade.tachiyomi.ui.manga.RelatedManga import tachiyomi.domain.manga.model.Manga import tachiyomi.i18n.MR +import tachiyomi.i18n.sy.SYMR import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.util.plus @@ -40,10 +41,10 @@ fun RelatedMangasCompactGrid( verticalArrangement = Arrangement.spacedBy(CommonMangaItemDefaults.GridVerticalSpacer), horizontalArrangement = Arrangement.spacedBy(CommonMangaItemDefaults.GridHorizontalSpacer), ) { - relatedMangas.forEach { - val isLoading = it is RelatedManga.Loading + relatedMangas.forEach { related -> + val isLoading = related is RelatedManga.Loading if (isLoading) { - header { + header(key = "$related#header") { RelatedMangaTitle( title = stringResource(MR.strings.loading), subtitle = null, @@ -52,19 +53,27 @@ fun RelatedMangasCompactGrid( modifier = Modifier.background(MaterialTheme.colorScheme.background), ) } - header { RelatedMangasLoadingItem() } + header(key = "$related#content") { RelatedMangasLoadingItem() } } else { - val relatedManga = it as RelatedManga.Success - header { + val relatedManga = related as RelatedManga.Success + header(key = "${related.keyword}#header") { RelatedMangaTitle( - title = relatedManga.keyword, + title = if (relatedManga.keyword.isNotBlank()) { + stringResource(SYMR.strings.related_mangas_more) + } else { + stringResource(SYMR.strings.pref_source_related_mangas) + }, subtitle = null, - onClick = { onKeywordClick(relatedManga.keyword) }, - onLongClick = { onKeywordLongClick(relatedManga.keyword) }, + onClick = { + if (relatedManga.keyword.isNotBlank()) onKeywordClick(relatedManga.keyword) + }, + onLongClick = { + if (relatedManga.keyword.isNotBlank()) onKeywordLongClick(relatedManga.keyword) + }, modifier = Modifier.background(MaterialTheme.colorScheme.background), ) } - items(count = relatedManga.mangaList.size) { index -> + items(key = { relatedManga.mangaList[it].id }, count = relatedManga.mangaList.size) { index -> val manga by getManga(relatedManga.mangaList[index]) BrowseSourceCompactGridItem( manga = manga, diff --git a/app/src/main/java/eu/kanade/presentation/browse/components/RelatedMangasList.kt b/app/src/main/java/eu/kanade/presentation/browse/components/RelatedMangasList.kt index c984b3bc1..3c66629c5 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/components/RelatedMangasList.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/components/RelatedMangasList.kt @@ -15,6 +15,7 @@ import eu.kanade.presentation.browse.RelatedMangasLoadingItem import eu.kanade.tachiyomi.ui.manga.RelatedManga import tachiyomi.domain.manga.model.Manga import tachiyomi.i18n.MR +import tachiyomi.i18n.sy.SYMR import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.i18n.stringResource @@ -35,10 +36,10 @@ fun RelatedMangasList( ), contentPadding = PaddingValues(MaterialTheme.padding.small), ) { - relatedMangas.forEach { - val isLoading = it is RelatedManga.Loading + relatedMangas.forEach { related -> + val isLoading = related is RelatedManga.Loading if (isLoading) { - stickyHeader { + stickyHeader(key = "$related#header") { RelatedMangaTitle( title = stringResource(MR.strings.loading), subtitle = null, @@ -47,19 +48,27 @@ fun RelatedMangasList( modifier = Modifier.background(MaterialTheme.colorScheme.background), ) } - item { RelatedMangasLoadingItem() } + item(key = "$related#content") { RelatedMangasLoadingItem() } } else { - val relatedManga = it as RelatedManga.Success - stickyHeader { + val relatedManga = related as RelatedManga.Success + stickyHeader(key = "${related.keyword}#header") { RelatedMangaTitle( - title = relatedManga.keyword, + title = if (relatedManga.keyword.isNotBlank()) { + stringResource(SYMR.strings.related_mangas_more) + } else { + stringResource(SYMR.strings.pref_source_related_mangas) + }, subtitle = null, - onClick = { onKeywordClick(relatedManga.keyword) }, - onLongClick = { onKeywordLongClick(relatedManga.keyword) }, + onClick = { + if (relatedManga.keyword.isNotBlank()) onKeywordClick(relatedManga.keyword) + }, + onLongClick = { + if (relatedManga.keyword.isNotBlank()) onKeywordLongClick(relatedManga.keyword) + }, modifier = Modifier.background(MaterialTheme.colorScheme.background), ) } - items(count = relatedManga.mangaList.size) { index -> + items(key = { relatedManga.mangaList[it].id }, count = relatedManga.mangaList.size) { index -> val manga by getManga(relatedManga.mangaList[index]) BrowseSourceListItem( manga = manga, diff --git a/app/src/main/java/eu/kanade/presentation/manga/components/RelatedMangas.kt b/app/src/main/java/eu/kanade/presentation/manga/components/RelatedMangas.kt index acad1d7ad..39f69bdb2 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/components/RelatedMangas.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/components/RelatedMangas.kt @@ -64,7 +64,7 @@ fun RelatedMangaCardRow( contentPadding = PaddingValues(MaterialTheme.padding.small), horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall), ) { - items(mangas) { + items(mangas, key = { it.id }) { val manga by getManga(it) MangaItem( title = manga.title, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt index 75e88aa21..31745fd2d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt @@ -452,10 +452,9 @@ class MangaScreenModel( if (screenModelScope.isActive) { // KMK --> launch { - launch { - fetchRelatedMangasFromSource() - }.join() - updateSuccessState { it.copy(isFetchingRelatedMangas = false) } + fetchRelatedMangasFromSource(onFinish = { + updateSuccessState { it.copy(isFetchingRelatedMangas = false) } + }) } // KMK <-- val fetchFromSourceTasks = listOf( @@ -1142,13 +1141,21 @@ class MangaScreenModel( /** * Requests an list of related mangas from the source. */ - private suspend fun fetchRelatedMangasFromSource() { + private suspend fun fetchRelatedMangasFromSource(onFinish: () -> Unit) { + fun exceptionHandler(e: Throwable) { + logcat(LogPriority.ERROR, e) + val message = with(context) { e.formattedMessage } + + screenModelScope.launch { + snackbarHostState.showSnackbar(message = message) + } + } val state = successState ?: return val relatedMangasEnabled = Injekt.get().relatedMangas().get() try { if (state.source !is StubSource) { if (relatedMangasEnabled) { - state.source.getRelatedMangaList(state.manga.toSManga()) { pair, _ -> + state.source.getRelatedMangaList(state.manga.toSManga(), { e -> exceptionHandler(e) }) { pair, _ -> /* Push found related mangas into collection */ val relatedManga = RelatedManga.Success.fromPair(pair) { mangaList -> mangaList.map { @@ -1167,13 +1174,10 @@ class MangaScreenModel( } } } - } catch (e: Throwable) { - logcat(LogPriority.ERROR, e) - val message = with(context) { e.formattedMessage } - - screenModelScope.launch { - snackbarHostState.showSnackbar(message = message) - } + } catch (e: Exception) { + exceptionHandler(e) + } finally { + onFinish() } } // KMK <-- @@ -1793,6 +1797,10 @@ class MangaScreenModel( // KMK <-- ) : State { // KMK ->> + /** + * a value of null will be treated as still loading, so if all searching were failed and won't update + * 'relatedMangaCollection` then we should return empty list + */ val relatedMangasSorted = relatedMangaCollection ?.sorted(manga) ?.removeDuplicates(manga) @@ -1929,7 +1937,7 @@ sealed interface RelatedManga { } companion object { - fun List.sorted(manga: Manga): List { + internal fun List.sorted(manga: Manga): List { val success = filterIsInstance() val loading = filterIsInstance() val title = manga.title.lowercase() @@ -1943,7 +1951,7 @@ sealed interface RelatedManga { loading } - fun List.removeDuplicates(manga: Manga): List { + internal fun List.removeDuplicates(manga: Manga): List { val mangaUrls = HashSet().apply { add(manga.url) } return map { relatedManga -> @@ -1960,7 +1968,7 @@ sealed interface RelatedManga { } } - fun List.isLoading(isFetchingRelatedMangas: Boolean): List { + internal fun List.isLoading(isFetchingRelatedMangas: Boolean): List { return if (isFetchingRelatedMangas) { this + listOf(Loading) } else { diff --git a/domain/src/main/java/tachiyomi/domain/source/model/StubSource.kt b/domain/src/main/java/tachiyomi/domain/source/model/StubSource.kt index 91f0bf78d..92c258b24 100644 --- a/domain/src/main/java/tachiyomi/domain/source/model/StubSource.kt +++ b/domain/src/main/java/tachiyomi/domain/source/model/StubSource.kt @@ -24,6 +24,7 @@ class StubSource( // KMK --> override suspend fun getRelatedMangaList( manga: SManga, + exceptionHandler: (Throwable) -> Unit, pushResults: suspend (relatedManga: Pair>, completed: Boolean) -> Unit, ) = throw SourceNotInstalledException() // KMK <-- diff --git a/i18n-sy/src/commonMain/resources/MR/base/strings.xml b/i18n-sy/src/commonMain/resources/MR/base/strings.xml index a9e1e061f..a5136d87d 100644 --- a/i18n-sy/src/commonMain/resources/MR/base/strings.xml +++ b/i18n-sy/src/commonMain/resources/MR/base/strings.xml @@ -204,6 +204,7 @@ Allow local source to read hidden folders Related titles Show related titles while viewing entry + More like these... Custom entry info diff --git a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/CatalogueSource.kt b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/CatalogueSource.kt index 6f0f4a396..cd92a262e 100644 --- a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/CatalogueSource.kt +++ b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/CatalogueSource.kt @@ -1,10 +1,13 @@ package eu.kanade.tachiyomi.source +import dev.icerock.moko.graphics.BuildConfig import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.SManga +import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch +import kotlinx.coroutines.supervisorScope import logcat.LogPriority import rx.Observable import tachiyomi.core.common.util.lang.awaitSingle @@ -95,12 +98,14 @@ interface CatalogueSource : Source { */ override suspend fun getRelatedMangaList( manga: SManga, + exceptionHandler: (Throwable) -> Unit, pushResults: suspend (relatedManga: Pair>, completed: Boolean) -> Unit, ) { + val handler = CoroutineExceptionHandler { _, e -> exceptionHandler(e) } if (!disableRelatedMangas) { - coroutineScope { - if (supportsRelatedMangas) launch { getRelatedMangaListByExtension(manga, pushResults) } - if (!disableRelatedMangasBySearch) launch { getRelatedMangaListBySearch(manga, pushResults) } + supervisorScope { + if (supportsRelatedMangas) launch(handler) { getRelatedMangaListByExtension(manga, pushResults) } + if (!disableRelatedMangasBySearch) launch(handler) { getRelatedMangaListBySearch(manga, pushResults) } } } } @@ -118,7 +123,15 @@ interface CatalogueSource : Source { runCatching { fetchRelatedMangaList(manga) } .onSuccess { if (it.isNotEmpty()) pushResults(Pair("", it), false) } .onFailure { e -> - logcat(LogPriority.ERROR, e) { "## getRelatedMangaListByExtension: $e" } + @Suppress("KotlinConstantConditions") + if (BuildConfig.BUILD_TYPE == "release") { + logcat(LogPriority.ERROR, e) { "## getRelatedMangaListByExtension: $e" } + } else { + throw UnsupportedOperationException( + "Extension doesn't support site's related entries," + + " please report an issue to Komikku." + ) + } } } diff --git a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/Source.kt b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/Source.kt index be1977a4e..9d2ed96bf 100644 --- a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/Source.kt +++ b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/Source.kt @@ -58,6 +58,7 @@ interface Source { */ suspend fun getRelatedMangaList( manga: SManga, + exceptionHandler: (Throwable) -> Unit, pushResults: suspend (relatedManga: Pair>, completed: Boolean) -> Unit, ): Unit = throw IllegalStateException("Not used") // KMK <-- diff --git a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/HttpSource.kt b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/HttpSource.kt index 6394979db..bc93f577d 100644 --- a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/HttpSource.kt +++ b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/HttpSource.kt @@ -312,7 +312,7 @@ abstract class HttpSource : CatalogueSource { * @param manga the manga to look for related mangas. */ protected open fun relatedMangaListRequest(manga: SManga): Request { - return GET(baseUrl + manga.url, headers) + return mangaDetailsRequest(manga) } /**