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(
key: Any? = null,
content: @Composable LazyGridItemScope.() -> Unit,
) {
item(span = { GridItemSpan(this.maxLineSpan) }, content = content)
item(key = key, span = { GridItemSpan(this.maxLineSpan) }, content = content)
}
@Composable

View file

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

View file

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

View file

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

View file

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

View file

@ -452,10 +452,9 @@ class MangaScreenModel(
if (screenModelScope.isActive) {
// KMK -->
launch {
launch {
fetchRelatedMangasFromSource()
}.join()
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<SourcePreferences>().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<RelatedManga>.sorted(manga: Manga): List<RelatedManga> {
internal fun List<RelatedManga>.sorted(manga: Manga): List<RelatedManga> {
val success = filterIsInstance<Success>()
val loading = filterIsInstance<Loading>()
val title = manga.title.lowercase()
@ -1943,7 +1951,7 @@ sealed interface RelatedManga {
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) }
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) {
this + listOf(Loading)
} else {

View file

@ -24,6 +24,7 @@ class StubSource(
// KMK -->
override suspend fun getRelatedMangaList(
manga: SManga,
exceptionHandler: (Throwable) -> Unit,
pushResults: suspend (relatedManga: Pair<String, List<SManga>>, completed: Boolean) -> Unit,
) = throw SourceNotInstalledException()
// 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_source_related_mangas">Related titles</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 -->
<string name="custom_entry_info">Custom entry info</string>

View file

@ -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<String, List<SManga>>, 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 ->
@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(
manga: SManga,
exceptionHandler: (Throwable) -> Unit,
pushResults: suspend (relatedManga: Pair<String, List<SManga>>, completed: Boolean) -> Unit,
): Unit = throw IllegalStateException("Not used")
// KMK <--

View file

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