Fix Related Mangas (#29)

* using mangaDetailsRequest for relatedMangaListRequest

* rethrow exception if extension failed to get related mangas

* improve coroutine

* More like these

* Fix missing unique mess up LazyRow/LazyGrid order
This commit is contained in:
Cuong M. Tran 2024-05-23 10:46:28 +07:00
parent ce7177efc6
commit 6876d82982
11 changed files with 105 additions and 53 deletions

View file

@ -154,9 +154,10 @@ fun RelatedMangaTitle(
} }
fun LazyGridScope.header( fun LazyGridScope.header(
key: Any? = null,
content: @Composable LazyGridItemScope.() -> Unit, content: @Composable LazyGridItemScope.() -> Unit,
) { ) {
item(span = { GridItemSpan(this.maxLineSpan) }, content = content) item(key = key, span = { GridItemSpan(this.maxLineSpan) }, content = content)
} }
@Composable @Composable

View file

@ -18,6 +18,7 @@ import eu.kanade.presentation.library.components.CommonMangaItemDefaults
import eu.kanade.tachiyomi.ui.manga.RelatedManga import eu.kanade.tachiyomi.ui.manga.RelatedManga
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.i18n.sy.SYMR
import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.plus import tachiyomi.presentation.core.util.plus
@ -40,10 +41,10 @@ fun RelatedMangasComfortableGrid(
verticalArrangement = Arrangement.spacedBy(CommonMangaItemDefaults.GridVerticalSpacer), verticalArrangement = Arrangement.spacedBy(CommonMangaItemDefaults.GridVerticalSpacer),
horizontalArrangement = Arrangement.spacedBy(CommonMangaItemDefaults.GridHorizontalSpacer), horizontalArrangement = Arrangement.spacedBy(CommonMangaItemDefaults.GridHorizontalSpacer),
) { ) {
relatedMangas.forEach { relatedMangas.forEach { related ->
val isLoading = it is RelatedManga.Loading val isLoading = related is RelatedManga.Loading
if (isLoading) { if (isLoading) {
header { header(key = "$related#header") {
RelatedMangaTitle( RelatedMangaTitle(
title = stringResource(MR.strings.loading), title = stringResource(MR.strings.loading),
subtitle = null, subtitle = null,
@ -52,19 +53,27 @@ fun RelatedMangasComfortableGrid(
modifier = Modifier.background(MaterialTheme.colorScheme.background), modifier = Modifier.background(MaterialTheme.colorScheme.background),
) )
} }
header { RelatedMangasLoadingItem() } header(key = "$related#content") { RelatedMangasLoadingItem() }
} else { } else {
val relatedManga = it as RelatedManga.Success val relatedManga = related as RelatedManga.Success
header { header(key = "${related.keyword}#header") {
RelatedMangaTitle( 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, subtitle = null,
onClick = { onKeywordClick(relatedManga.keyword) }, onClick = {
onLongClick = { onKeywordLongClick(relatedManga.keyword) }, if (relatedManga.keyword.isNotBlank()) onKeywordClick(relatedManga.keyword)
},
onLongClick = {
if (relatedManga.keyword.isNotBlank()) onKeywordLongClick(relatedManga.keyword)
},
modifier = Modifier.background(MaterialTheme.colorScheme.background), 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]) val manga by getManga(relatedManga.mangaList[index])
BrowseSourceComfortableGridItem( BrowseSourceComfortableGridItem(
manga = manga, manga = manga,

View file

@ -18,6 +18,7 @@ import eu.kanade.presentation.library.components.CommonMangaItemDefaults
import eu.kanade.tachiyomi.ui.manga.RelatedManga import eu.kanade.tachiyomi.ui.manga.RelatedManga
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.i18n.sy.SYMR
import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.plus import tachiyomi.presentation.core.util.plus
@ -40,10 +41,10 @@ fun RelatedMangasCompactGrid(
verticalArrangement = Arrangement.spacedBy(CommonMangaItemDefaults.GridVerticalSpacer), verticalArrangement = Arrangement.spacedBy(CommonMangaItemDefaults.GridVerticalSpacer),
horizontalArrangement = Arrangement.spacedBy(CommonMangaItemDefaults.GridHorizontalSpacer), horizontalArrangement = Arrangement.spacedBy(CommonMangaItemDefaults.GridHorizontalSpacer),
) { ) {
relatedMangas.forEach { relatedMangas.forEach { related ->
val isLoading = it is RelatedManga.Loading val isLoading = related is RelatedManga.Loading
if (isLoading) { if (isLoading) {
header { header(key = "$related#header") {
RelatedMangaTitle( RelatedMangaTitle(
title = stringResource(MR.strings.loading), title = stringResource(MR.strings.loading),
subtitle = null, subtitle = null,
@ -52,19 +53,27 @@ fun RelatedMangasCompactGrid(
modifier = Modifier.background(MaterialTheme.colorScheme.background), modifier = Modifier.background(MaterialTheme.colorScheme.background),
) )
} }
header { RelatedMangasLoadingItem() } header(key = "$related#content") { RelatedMangasLoadingItem() }
} else { } else {
val relatedManga = it as RelatedManga.Success val relatedManga = related as RelatedManga.Success
header { header(key = "${related.keyword}#header") {
RelatedMangaTitle( 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, subtitle = null,
onClick = { onKeywordClick(relatedManga.keyword) }, onClick = {
onLongClick = { onKeywordLongClick(relatedManga.keyword) }, if (relatedManga.keyword.isNotBlank()) onKeywordClick(relatedManga.keyword)
},
onLongClick = {
if (relatedManga.keyword.isNotBlank()) onKeywordLongClick(relatedManga.keyword)
},
modifier = Modifier.background(MaterialTheme.colorScheme.background), 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]) val manga by getManga(relatedManga.mangaList[index])
BrowseSourceCompactGridItem( BrowseSourceCompactGridItem(
manga = manga, manga = manga,

View file

@ -15,6 +15,7 @@ import eu.kanade.presentation.browse.RelatedMangasLoadingItem
import eu.kanade.tachiyomi.ui.manga.RelatedManga import eu.kanade.tachiyomi.ui.manga.RelatedManga
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.i18n.sy.SYMR
import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
@ -35,10 +36,10 @@ fun RelatedMangasList(
), ),
contentPadding = PaddingValues(MaterialTheme.padding.small), contentPadding = PaddingValues(MaterialTheme.padding.small),
) { ) {
relatedMangas.forEach { relatedMangas.forEach { related ->
val isLoading = it is RelatedManga.Loading val isLoading = related is RelatedManga.Loading
if (isLoading) { if (isLoading) {
stickyHeader { stickyHeader(key = "$related#header") {
RelatedMangaTitle( RelatedMangaTitle(
title = stringResource(MR.strings.loading), title = stringResource(MR.strings.loading),
subtitle = null, subtitle = null,
@ -47,19 +48,27 @@ fun RelatedMangasList(
modifier = Modifier.background(MaterialTheme.colorScheme.background), modifier = Modifier.background(MaterialTheme.colorScheme.background),
) )
} }
item { RelatedMangasLoadingItem() } item(key = "$related#content") { RelatedMangasLoadingItem() }
} else { } else {
val relatedManga = it as RelatedManga.Success val relatedManga = related as RelatedManga.Success
stickyHeader { stickyHeader(key = "${related.keyword}#header") {
RelatedMangaTitle( 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, subtitle = null,
onClick = { onKeywordClick(relatedManga.keyword) }, onClick = {
onLongClick = { onKeywordLongClick(relatedManga.keyword) }, if (relatedManga.keyword.isNotBlank()) onKeywordClick(relatedManga.keyword)
},
onLongClick = {
if (relatedManga.keyword.isNotBlank()) onKeywordLongClick(relatedManga.keyword)
},
modifier = Modifier.background(MaterialTheme.colorScheme.background), 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]) val manga by getManga(relatedManga.mangaList[index])
BrowseSourceListItem( BrowseSourceListItem(
manga = manga, manga = manga,

View file

@ -64,7 +64,7 @@ fun RelatedMangaCardRow(
contentPadding = PaddingValues(MaterialTheme.padding.small), contentPadding = PaddingValues(MaterialTheme.padding.small),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall), horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
) { ) {
items(mangas) { items(mangas, key = { it.id }) {
val manga by getManga(it) val manga by getManga(it)
MangaItem( MangaItem(
title = manga.title, title = manga.title,

View file

@ -452,10 +452,9 @@ class MangaScreenModel(
if (screenModelScope.isActive) { if (screenModelScope.isActive) {
// KMK --> // KMK -->
launch { launch {
launch { fetchRelatedMangasFromSource(onFinish = {
fetchRelatedMangasFromSource() updateSuccessState { it.copy(isFetchingRelatedMangas = false) }
}.join() })
updateSuccessState { it.copy(isFetchingRelatedMangas = false) }
} }
// KMK <-- // KMK <--
val fetchFromSourceTasks = listOf( val fetchFromSourceTasks = listOf(
@ -1142,13 +1141,21 @@ class MangaScreenModel(
/** /**
* Requests an list of related mangas from the source. * 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 state = successState ?: return
val relatedMangasEnabled = Injekt.get<SourcePreferences>().relatedMangas().get() val relatedMangasEnabled = Injekt.get<SourcePreferences>().relatedMangas().get()
try { try {
if (state.source !is StubSource) { if (state.source !is StubSource) {
if (relatedMangasEnabled) { 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 */ /* Push found related mangas into collection */
val relatedManga = RelatedManga.Success.fromPair(pair) { mangaList -> val relatedManga = RelatedManga.Success.fromPair(pair) { mangaList ->
mangaList.map { mangaList.map {
@ -1167,13 +1174,10 @@ class MangaScreenModel(
} }
} }
} }
} catch (e: Throwable) { } catch (e: Exception) {
logcat(LogPriority.ERROR, e) exceptionHandler(e)
val message = with(context) { e.formattedMessage } } finally {
onFinish()
screenModelScope.launch {
snackbarHostState.showSnackbar(message = message)
}
} }
} }
// KMK <-- // KMK <--
@ -1793,6 +1797,10 @@ class MangaScreenModel(
// KMK <-- // KMK <--
) : State { ) : State {
// KMK ->> // 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 val relatedMangasSorted = relatedMangaCollection
?.sorted(manga) ?.sorted(manga)
?.removeDuplicates(manga) ?.removeDuplicates(manga)
@ -1929,7 +1937,7 @@ sealed interface RelatedManga {
} }
companion object { companion object {
fun List<RelatedManga>.sorted(manga: Manga): List<RelatedManga> { internal fun List<RelatedManga>.sorted(manga: Manga): List<RelatedManga> {
val success = filterIsInstance<Success>() val success = filterIsInstance<Success>()
val loading = filterIsInstance<Loading>() val loading = filterIsInstance<Loading>()
val title = manga.title.lowercase() val title = manga.title.lowercase()
@ -1943,7 +1951,7 @@ sealed interface RelatedManga {
loading loading
} }
fun List<RelatedManga>.removeDuplicates(manga: Manga): List<RelatedManga> { internal fun List<RelatedManga>.removeDuplicates(manga: Manga): List<RelatedManga> {
val mangaUrls = HashSet<String>().apply { add(manga.url) } val mangaUrls = HashSet<String>().apply { add(manga.url) }
return map { relatedManga -> return map { relatedManga ->
@ -1960,7 +1968,7 @@ sealed interface RelatedManga {
} }
} }
fun List<RelatedManga>.isLoading(isFetchingRelatedMangas: Boolean): List<RelatedManga> { internal fun List<RelatedManga>.isLoading(isFetchingRelatedMangas: Boolean): List<RelatedManga> {
return if (isFetchingRelatedMangas) { return if (isFetchingRelatedMangas) {
this + listOf(Loading) this + listOf(Loading)
} else { } else {

View file

@ -24,6 +24,7 @@ class StubSource(
// KMK --> // KMK -->
override suspend fun getRelatedMangaList( override suspend fun getRelatedMangaList(
manga: SManga, manga: SManga,
exceptionHandler: (Throwable) -> Unit,
pushResults: suspend (relatedManga: Pair<String, List<SManga>>, completed: Boolean) -> Unit, pushResults: suspend (relatedManga: Pair<String, List<SManga>>, completed: Boolean) -> Unit,
) = throw SourceNotInstalledException() ) = throw SourceNotInstalledException()
// KMK <-- // KMK <--

View file

@ -204,6 +204,7 @@
<string name="pref_local_source_hidden_folders_summery">Allow local source to read hidden folders</string> <string name="pref_local_source_hidden_folders_summery">Allow local source to read hidden folders</string>
<string name="pref_source_related_mangas">Related titles</string> <string name="pref_source_related_mangas">Related titles</string>
<string name="pref_source_related_mangas_summary">Show related titles while viewing entry</string> <string name="pref_source_related_mangas_summary">Show related titles while viewing entry</string>
<string name="related_mangas_more">More like these...</string>
<!-- Backup settings --> <!-- Backup settings -->
<string name="custom_entry_info">Custom entry info</string> <string name="custom_entry_info">Custom entry info</string>

View file

@ -1,10 +1,13 @@
package eu.kanade.tachiyomi.source package eu.kanade.tachiyomi.source
import dev.icerock.moko.graphics.BuildConfig
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.supervisorScope
import logcat.LogPriority import logcat.LogPriority
import rx.Observable import rx.Observable
import tachiyomi.core.common.util.lang.awaitSingle import tachiyomi.core.common.util.lang.awaitSingle
@ -95,12 +98,14 @@ interface CatalogueSource : Source {
*/ */
override suspend fun getRelatedMangaList( override suspend fun getRelatedMangaList(
manga: SManga, manga: SManga,
exceptionHandler: (Throwable) -> Unit,
pushResults: suspend (relatedManga: Pair<String, List<SManga>>, completed: Boolean) -> Unit, pushResults: suspend (relatedManga: Pair<String, List<SManga>>, completed: Boolean) -> Unit,
) { ) {
val handler = CoroutineExceptionHandler { _, e -> exceptionHandler(e) }
if (!disableRelatedMangas) { if (!disableRelatedMangas) {
coroutineScope { supervisorScope {
if (supportsRelatedMangas) launch { getRelatedMangaListByExtension(manga, pushResults) } if (supportsRelatedMangas) launch(handler) { getRelatedMangaListByExtension(manga, pushResults) }
if (!disableRelatedMangasBySearch) launch { getRelatedMangaListBySearch(manga, pushResults) } if (!disableRelatedMangasBySearch) launch(handler) { getRelatedMangaListBySearch(manga, pushResults) }
} }
} }
} }
@ -118,7 +123,15 @@ interface CatalogueSource : Source {
runCatching { fetchRelatedMangaList(manga) } runCatching { fetchRelatedMangaList(manga) }
.onSuccess { if (it.isNotEmpty()) pushResults(Pair("", it), false) } .onSuccess { if (it.isNotEmpty()) pushResults(Pair("", it), false) }
.onFailure { e -> .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."
)
}
} }
} }

View file

@ -58,6 +58,7 @@ interface Source {
*/ */
suspend fun getRelatedMangaList( suspend fun getRelatedMangaList(
manga: SManga, manga: SManga,
exceptionHandler: (Throwable) -> Unit,
pushResults: suspend (relatedManga: Pair<String, List<SManga>>, completed: Boolean) -> Unit, pushResults: suspend (relatedManga: Pair<String, List<SManga>>, completed: Boolean) -> Unit,
): Unit = throw IllegalStateException("Not used") ): Unit = throw IllegalStateException("Not used")
// KMK <-- // KMK <--

View file

@ -312,7 +312,7 @@ abstract class HttpSource : CatalogueSource {
* @param manga the manga to look for related mangas. * @param manga the manga to look for related mangas.
*/ */
protected open fun relatedMangaListRequest(manga: SManga): Request { protected open fun relatedMangaListRequest(manga: SManga): Request {
return GET(baseUrl + manga.url, headers) return mangaDetailsRequest(manga)
} }
/** /**