A dedicated screen for related entries, fetching directly from sources
* Faster loading of related entries * Enable fetching related entries from site's entry page (things like "More like this", "Recommendations" etc.) * Improve & fix bugs of algorithm looking for related entries (cherry picked from commit 54d91c3f71557becd4fc22ec8500e05f79dc48d8)
This commit is contained in:
parent
d90a4da895
commit
bdd8a652c2
32 changed files with 1034 additions and 191 deletions
|
|
@ -84,6 +84,6 @@ class SourcePreferences(
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
// KMK -->
|
// KMK -->
|
||||||
fun relatedMangas() = preferenceStore.getBoolean("related_mangas", false)
|
fun relatedMangas() = preferenceStore.getBoolean("related_mangas", true)
|
||||||
// KMK <--
|
// KMK <--
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -58,8 +58,9 @@ fun GlobalSearchScreen(
|
||||||
when (result) {
|
when (result) {
|
||||||
is SearchItemResult.Success -> {
|
is SearchItemResult.Success -> {
|
||||||
result.result.forEach { manga ->
|
result.result.forEach { manga ->
|
||||||
if (!bulkFavoriteState.selection.contains(manga))
|
if (!bulkFavoriteState.selection.contains(manga)) {
|
||||||
bulkFavoriteScreenModel.select(manga)
|
bulkFavoriteScreenModel.select(manga)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else -> {}
|
else -> {}
|
||||||
|
|
|
||||||
|
|
@ -48,8 +48,9 @@ fun MigrateSearchScreen(
|
||||||
when (result) {
|
when (result) {
|
||||||
is SearchItemResult.Success -> {
|
is SearchItemResult.Success -> {
|
||||||
result.result.forEach { manga ->
|
result.result.forEach { manga ->
|
||||||
if (!bulkFavoriteState.selection.contains(manga))
|
if (!bulkFavoriteState.selection.contains(manga)) {
|
||||||
bulkFavoriteScreenModel.select(manga)
|
bulkFavoriteScreenModel.select(manga)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else -> {}
|
else -> {}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,176 @@
|
||||||
|
package eu.kanade.presentation.browse
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.combinedClickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.lazy.grid.GridCells
|
||||||
|
import androidx.compose.foundation.lazy.grid.GridItemSpan
|
||||||
|
import androidx.compose.foundation.lazy.grid.LazyGridItemScope
|
||||||
|
import androidx.compose.foundation.lazy.grid.LazyGridScope
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.outlined.ArrowForward
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.State
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import eu.kanade.presentation.browse.components.RelatedMangasComfortableGrid
|
||||||
|
import eu.kanade.presentation.browse.components.RelatedMangasCompactGrid
|
||||||
|
import eu.kanade.presentation.browse.components.RelatedMangasList
|
||||||
|
import eu.kanade.tachiyomi.ui.manga.RelatedManga
|
||||||
|
import exh.util.capitalize
|
||||||
|
import tachiyomi.domain.library.model.LibraryDisplayMode
|
||||||
|
import tachiyomi.domain.manga.model.Manga
|
||||||
|
import tachiyomi.i18n.MR
|
||||||
|
import tachiyomi.presentation.core.components.material.padding
|
||||||
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
|
import tachiyomi.presentation.core.screens.EmptyScreen
|
||||||
|
import tachiyomi.presentation.core.screens.LoadingScreen
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RelatedMangasContent(
|
||||||
|
relatedMangas: List<RelatedManga>?,
|
||||||
|
getMangaState: @Composable (Manga) -> State<Manga>,
|
||||||
|
columns: GridCells,
|
||||||
|
displayMode: LibraryDisplayMode,
|
||||||
|
contentPadding: PaddingValues,
|
||||||
|
onMangaClick: (Manga) -> Unit,
|
||||||
|
onMangaLongClick: (Manga) -> Unit,
|
||||||
|
onKeywordClick: (String) -> Unit,
|
||||||
|
onKeywordLongClick: (String) -> Unit,
|
||||||
|
selection: List<Manga>,
|
||||||
|
) {
|
||||||
|
if (relatedMangas == null) {
|
||||||
|
LoadingScreen(
|
||||||
|
modifier = Modifier.padding(contentPadding),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (relatedMangas.isEmpty()) {
|
||||||
|
EmptyScreen(
|
||||||
|
modifier = Modifier.padding(contentPadding),
|
||||||
|
message = stringResource(MR.strings.no_results_found),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
when (displayMode) {
|
||||||
|
LibraryDisplayMode.ComfortableGrid -> {
|
||||||
|
RelatedMangasComfortableGrid(
|
||||||
|
relatedMangas = relatedMangas,
|
||||||
|
getManga = getMangaState,
|
||||||
|
columns = columns,
|
||||||
|
contentPadding = contentPadding,
|
||||||
|
onMangaClick = onMangaClick,
|
||||||
|
onMangaLongClick = onMangaLongClick,
|
||||||
|
onKeywordClick = onKeywordClick,
|
||||||
|
onKeywordLongClick = onKeywordLongClick,
|
||||||
|
selection = selection,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
LibraryDisplayMode.List -> {
|
||||||
|
RelatedMangasList(
|
||||||
|
relatedMangas = relatedMangas,
|
||||||
|
getManga = getMangaState,
|
||||||
|
contentPadding = contentPadding,
|
||||||
|
onMangaClick = onMangaClick,
|
||||||
|
onMangaLongClick = onMangaLongClick,
|
||||||
|
onKeywordClick = onKeywordClick,
|
||||||
|
onKeywordLongClick = onKeywordLongClick,
|
||||||
|
selection = selection,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
LibraryDisplayMode.CompactGrid, LibraryDisplayMode.CoverOnlyGrid -> {
|
||||||
|
RelatedMangasCompactGrid(
|
||||||
|
relatedMangas = relatedMangas,
|
||||||
|
getManga = getMangaState,
|
||||||
|
columns = columns,
|
||||||
|
contentPadding = contentPadding,
|
||||||
|
onMangaClick = onMangaClick,
|
||||||
|
onMangaLongClick = onMangaLongClick,
|
||||||
|
onKeywordClick = onKeywordClick,
|
||||||
|
onKeywordLongClick = onKeywordLongClick,
|
||||||
|
selection = selection,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RelatedMangaTitle(
|
||||||
|
title: String,
|
||||||
|
subtitle: String?,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
onLongClick: (() -> Unit)?,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = modifier
|
||||||
|
.padding(
|
||||||
|
start = MaterialTheme.padding.medium,
|
||||||
|
end = MaterialTheme.padding.extraSmall,
|
||||||
|
)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.let {
|
||||||
|
if (onLongClick == null) {
|
||||||
|
it.clickable(onClick = onClick)
|
||||||
|
} else {
|
||||||
|
it.combinedClickable(onClick = onClick, onLongClick = onLongClick)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(
|
||||||
|
top = MaterialTheme.padding.small,
|
||||||
|
bottom = MaterialTheme.padding.extraSmall,
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = title.capitalize(),
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
)
|
||||||
|
if (subtitle != null) {
|
||||||
|
Text(text = subtitle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
IconButton(onClick = onClick) {
|
||||||
|
Icon(imageVector = Icons.AutoMirrored.Outlined.ArrowForward, contentDescription = null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun LazyGridScope.header(
|
||||||
|
content: @Composable LazyGridItemScope.() -> Unit,
|
||||||
|
) {
|
||||||
|
item(span = { GridItemSpan(this.maxLineSpan) }, content = content)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RelatedMangasLoadingItem() {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(MaterialTheme.padding.medium),
|
||||||
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
) {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(24.dp),
|
||||||
|
strokeWidth = 2.dp,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -131,8 +131,9 @@ fun SourceFeedScreen(
|
||||||
onSelectAll = {
|
onSelectAll = {
|
||||||
items.forEach {
|
items.forEach {
|
||||||
it.results?.forEach { manga ->
|
it.results?.forEach { manga ->
|
||||||
if (!bulkFavoriteState.selection.contains(manga))
|
if (!bulkFavoriteState.selection.contains(manga)) {
|
||||||
bulkFavoriteScreenModel.select(manga)
|
bulkFavoriteScreenModel.select(manga)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,7 @@ fun BrowseSourceComfortableGrid(
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun BrowseSourceComfortableGridItem(
|
internal fun BrowseSourceComfortableGridItem(
|
||||||
manga: Manga,
|
manga: Manga,
|
||||||
// SY -->
|
// SY -->
|
||||||
metadata: RaisedSearchMetadata?,
|
metadata: RaisedSearchMetadata?,
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,7 @@ fun BrowseSourceCompactGrid(
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun BrowseSourceCompactGridItem(
|
internal fun BrowseSourceCompactGridItem(
|
||||||
manga: Manga,
|
manga: Manga,
|
||||||
// SY -->
|
// SY -->
|
||||||
metadata: RaisedSearchMetadata?,
|
metadata: RaisedSearchMetadata?,
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,7 @@ fun BrowseSourceList(
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun BrowseSourceListItem(
|
internal fun BrowseSourceListItem(
|
||||||
manga: Manga,
|
manga: Manga,
|
||||||
// SY -->
|
// SY -->
|
||||||
metadata: RaisedSearchMetadata?,
|
metadata: RaisedSearchMetadata?,
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@ fun GlobalSearchCardRow(
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun MangaItem(
|
internal fun MangaItem(
|
||||||
title: String,
|
title: String,
|
||||||
cover: MangaCover,
|
cover: MangaCover,
|
||||||
isFavorite: Boolean,
|
isFavorite: Boolean,
|
||||||
|
|
@ -92,7 +92,7 @@ private fun MangaItem(
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun EmptyResultItem() {
|
internal fun EmptyResultItem() {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(MR.strings.no_results_found),
|
text = stringResource(MR.strings.no_results_found),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,80 @@
|
||||||
|
package eu.kanade.presentation.browse.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.lazy.grid.GridCells
|
||||||
|
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.State
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.util.fastAny
|
||||||
|
import eu.kanade.presentation.browse.RelatedMangaTitle
|
||||||
|
import eu.kanade.presentation.browse.RelatedMangasLoadingItem
|
||||||
|
import eu.kanade.presentation.browse.header
|
||||||
|
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.presentation.core.components.material.padding
|
||||||
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
|
import tachiyomi.presentation.core.util.plus
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RelatedMangasComfortableGrid(
|
||||||
|
relatedMangas: List<RelatedManga>,
|
||||||
|
getManga: @Composable (Manga) -> State<Manga>,
|
||||||
|
columns: GridCells,
|
||||||
|
contentPadding: PaddingValues,
|
||||||
|
onMangaClick: (Manga) -> Unit,
|
||||||
|
onMangaLongClick: (Manga) -> Unit,
|
||||||
|
onKeywordClick: (String) -> Unit,
|
||||||
|
onKeywordLongClick: (String) -> Unit,
|
||||||
|
selection: List<Manga>,
|
||||||
|
) {
|
||||||
|
LazyVerticalGrid(
|
||||||
|
columns = columns,
|
||||||
|
contentPadding = contentPadding + PaddingValues(MaterialTheme.padding.small),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(CommonMangaItemDefaults.GridVerticalSpacer),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(CommonMangaItemDefaults.GridHorizontalSpacer),
|
||||||
|
) {
|
||||||
|
relatedMangas.forEach {
|
||||||
|
val isLoading = it is RelatedManga.Loading
|
||||||
|
if (isLoading) {
|
||||||
|
header {
|
||||||
|
RelatedMangaTitle(
|
||||||
|
title = stringResource(MR.strings.loading),
|
||||||
|
subtitle = null,
|
||||||
|
onClick = {},
|
||||||
|
onLongClick = null,
|
||||||
|
modifier = Modifier.background(MaterialTheme.colorScheme.background),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
header { RelatedMangasLoadingItem() }
|
||||||
|
} else {
|
||||||
|
val relatedManga = it as RelatedManga.Success
|
||||||
|
header {
|
||||||
|
RelatedMangaTitle(
|
||||||
|
title = relatedManga.keyword,
|
||||||
|
subtitle = null,
|
||||||
|
onClick = { onKeywordClick(relatedManga.keyword) },
|
||||||
|
onLongClick = { onKeywordLongClick(relatedManga.keyword) },
|
||||||
|
modifier = Modifier.background(MaterialTheme.colorScheme.background),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
items(count = relatedManga.mangaList.size) { index ->
|
||||||
|
val manga by getManga(relatedManga.mangaList[index])
|
||||||
|
BrowseSourceComfortableGridItem(
|
||||||
|
manga = manga,
|
||||||
|
onClick = { onMangaClick(manga) },
|
||||||
|
onLongClick = { onMangaLongClick(manga) },
|
||||||
|
isSelected = selection.fastAny { selected -> selected.id == manga.id },
|
||||||
|
metadata = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,80 @@
|
||||||
|
package eu.kanade.presentation.browse.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.lazy.grid.GridCells
|
||||||
|
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.State
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.util.fastAny
|
||||||
|
import eu.kanade.presentation.browse.RelatedMangaTitle
|
||||||
|
import eu.kanade.presentation.browse.RelatedMangasLoadingItem
|
||||||
|
import eu.kanade.presentation.browse.header
|
||||||
|
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.presentation.core.components.material.padding
|
||||||
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
|
import tachiyomi.presentation.core.util.plus
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RelatedMangasCompactGrid(
|
||||||
|
relatedMangas: List<RelatedManga>,
|
||||||
|
getManga: @Composable (Manga) -> State<Manga>,
|
||||||
|
columns: GridCells,
|
||||||
|
contentPadding: PaddingValues,
|
||||||
|
onMangaClick: (Manga) -> Unit,
|
||||||
|
onMangaLongClick: (Manga) -> Unit,
|
||||||
|
onKeywordClick: (String) -> Unit,
|
||||||
|
onKeywordLongClick: (String) -> Unit,
|
||||||
|
selection: List<Manga>,
|
||||||
|
) {
|
||||||
|
LazyVerticalGrid(
|
||||||
|
columns = columns,
|
||||||
|
contentPadding = contentPadding + PaddingValues(MaterialTheme.padding.small),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(CommonMangaItemDefaults.GridVerticalSpacer),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(CommonMangaItemDefaults.GridHorizontalSpacer),
|
||||||
|
) {
|
||||||
|
relatedMangas.forEach {
|
||||||
|
val isLoading = it is RelatedManga.Loading
|
||||||
|
if (isLoading) {
|
||||||
|
header {
|
||||||
|
RelatedMangaTitle(
|
||||||
|
title = stringResource(MR.strings.loading),
|
||||||
|
subtitle = null,
|
||||||
|
onClick = {},
|
||||||
|
onLongClick = null,
|
||||||
|
modifier = Modifier.background(MaterialTheme.colorScheme.background),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
header { RelatedMangasLoadingItem() }
|
||||||
|
} else {
|
||||||
|
val relatedManga = it as RelatedManga.Success
|
||||||
|
header {
|
||||||
|
RelatedMangaTitle(
|
||||||
|
title = relatedManga.keyword,
|
||||||
|
subtitle = null,
|
||||||
|
onClick = { onKeywordClick(relatedManga.keyword) },
|
||||||
|
onLongClick = { onKeywordLongClick(relatedManga.keyword) },
|
||||||
|
modifier = Modifier.background(MaterialTheme.colorScheme.background),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
items(count = relatedManga.mangaList.size) { index ->
|
||||||
|
val manga by getManga(relatedManga.mangaList[index])
|
||||||
|
BrowseSourceCompactGridItem(
|
||||||
|
manga = manga,
|
||||||
|
onClick = { onMangaClick(manga) },
|
||||||
|
onLongClick = { onMangaLongClick(manga) },
|
||||||
|
isSelected = selection.fastAny { selected -> selected.id == manga.id },
|
||||||
|
metadata = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
package eu.kanade.presentation.browse.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.State
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.util.fastAny
|
||||||
|
import eu.kanade.presentation.browse.RelatedMangaTitle
|
||||||
|
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.presentation.core.components.material.padding
|
||||||
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RelatedMangasList(
|
||||||
|
relatedMangas: List<RelatedManga>,
|
||||||
|
getManga: @Composable (Manga) -> State<Manga>,
|
||||||
|
contentPadding: PaddingValues,
|
||||||
|
onMangaClick: (Manga) -> Unit,
|
||||||
|
onMangaLongClick: (Manga) -> Unit,
|
||||||
|
onKeywordClick: (String) -> Unit,
|
||||||
|
onKeywordLongClick: (String) -> Unit,
|
||||||
|
selection: List<Manga>,
|
||||||
|
) {
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier.padding(
|
||||||
|
top = contentPadding.calculateTopPadding(),
|
||||||
|
),
|
||||||
|
contentPadding = PaddingValues(MaterialTheme.padding.small),
|
||||||
|
) {
|
||||||
|
relatedMangas.forEach {
|
||||||
|
val isLoading = it is RelatedManga.Loading
|
||||||
|
if (isLoading) {
|
||||||
|
stickyHeader {
|
||||||
|
RelatedMangaTitle(
|
||||||
|
title = stringResource(MR.strings.loading),
|
||||||
|
subtitle = null,
|
||||||
|
onClick = {},
|
||||||
|
onLongClick = null,
|
||||||
|
modifier = Modifier.background(MaterialTheme.colorScheme.background),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
item { RelatedMangasLoadingItem() }
|
||||||
|
} else {
|
||||||
|
val relatedManga = it as RelatedManga.Success
|
||||||
|
stickyHeader {
|
||||||
|
RelatedMangaTitle(
|
||||||
|
title = relatedManga.keyword,
|
||||||
|
subtitle = null,
|
||||||
|
onClick = { onKeywordClick(relatedManga.keyword) },
|
||||||
|
onLongClick = { onKeywordLongClick(relatedManga.keyword) },
|
||||||
|
modifier = Modifier.background(MaterialTheme.colorScheme.background),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
items(count = relatedManga.mangaList.size) { index ->
|
||||||
|
val manga by getManga(relatedManga.mangaList[index])
|
||||||
|
BrowseSourceListItem(
|
||||||
|
manga = manga,
|
||||||
|
onClick = { onMangaClick(manga) },
|
||||||
|
onLongClick = { onMangaLongClick(manga) },
|
||||||
|
isSelected = selection.fastAny { selected -> selected.id == manga.id },
|
||||||
|
metadata = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -73,8 +73,9 @@ fun TabbedScreen(
|
||||||
onSelectAll = {
|
onSelectAll = {
|
||||||
feedState.items?.forEach {
|
feedState.items?.forEach {
|
||||||
it.results?.forEach { manga ->
|
it.results?.forEach { manga ->
|
||||||
if (!bulkFavoriteState.selection.contains(manga))
|
if (!bulkFavoriteState.selection.contains(manga)) {
|
||||||
bulkFavoriteScreenModel.select(manga)
|
bulkFavoriteScreenModel.select(manga)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -50,8 +50,8 @@ import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.util.fastAll
|
import androidx.compose.ui.util.fastAll
|
||||||
import androidx.compose.ui.util.fastAny
|
import androidx.compose.ui.util.fastAny
|
||||||
import androidx.compose.ui.util.fastMap
|
import androidx.compose.ui.util.fastMap
|
||||||
import eu.kanade.presentation.browse.components.GlobalSearchCardRow
|
import eu.kanade.domain.source.service.SourcePreferences
|
||||||
import eu.kanade.presentation.browse.components.GlobalSearchResultItem
|
import eu.kanade.presentation.browse.RelatedMangaTitle
|
||||||
import eu.kanade.presentation.components.relativeDateText
|
import eu.kanade.presentation.components.relativeDateText
|
||||||
import eu.kanade.presentation.manga.components.ChapterDownloadAction
|
import eu.kanade.presentation.manga.components.ChapterDownloadAction
|
||||||
import eu.kanade.presentation.manga.components.ChapterHeader
|
import eu.kanade.presentation.manga.components.ChapterHeader
|
||||||
|
|
@ -65,6 +65,7 @@ import eu.kanade.presentation.manga.components.MangaToolbar
|
||||||
import eu.kanade.presentation.manga.components.MissingChapterCountListItem
|
import eu.kanade.presentation.manga.components.MissingChapterCountListItem
|
||||||
import eu.kanade.presentation.manga.components.PagePreviewItems
|
import eu.kanade.presentation.manga.components.PagePreviewItems
|
||||||
import eu.kanade.presentation.manga.components.PagePreviews
|
import eu.kanade.presentation.manga.components.PagePreviews
|
||||||
|
import eu.kanade.presentation.manga.components.RelatedMangas
|
||||||
import eu.kanade.presentation.manga.components.SearchMetadataChips
|
import eu.kanade.presentation.manga.components.SearchMetadataChips
|
||||||
import eu.kanade.presentation.util.formatChapterNumber
|
import eu.kanade.presentation.util.formatChapterNumber
|
||||||
import eu.kanade.tachiyomi.data.download.model.Download
|
import eu.kanade.tachiyomi.data.download.model.Download
|
||||||
|
|
@ -99,6 +100,7 @@ import tachiyomi.domain.library.service.LibraryPreferences
|
||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
import tachiyomi.domain.source.model.StubSource
|
import tachiyomi.domain.source.model.StubSource
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
|
import tachiyomi.i18n.sy.SYMR
|
||||||
import tachiyomi.presentation.core.components.TwoPanelBox
|
import tachiyomi.presentation.core.components.TwoPanelBox
|
||||||
import tachiyomi.presentation.core.components.VerticalFastScroller
|
import tachiyomi.presentation.core.components.VerticalFastScroller
|
||||||
import tachiyomi.presentation.core.components.material.ExtendedFloatingActionButton
|
import tachiyomi.presentation.core.components.material.ExtendedFloatingActionButton
|
||||||
|
|
@ -107,6 +109,8 @@ import tachiyomi.presentation.core.components.material.Scaffold
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
import tachiyomi.presentation.core.util.shouldExpandFAB
|
import tachiyomi.presentation.core.util.shouldExpandFAB
|
||||||
import tachiyomi.source.local.isLocal
|
import tachiyomi.source.local.isLocal
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.time.ZoneId
|
import java.time.ZoneId
|
||||||
import java.time.ZonedDateTime
|
import java.time.ZonedDateTime
|
||||||
|
|
@ -171,7 +175,8 @@ fun MangaScreen(
|
||||||
onInvertSelection: () -> Unit,
|
onInvertSelection: () -> Unit,
|
||||||
|
|
||||||
// KMK -->
|
// KMK -->
|
||||||
getMangaState: @Composable ((Manga) -> State<Manga>),
|
getMangaState: @Composable (Manga) -> State<Manga>,
|
||||||
|
onRelatedMangasScreenClick: () -> Unit,
|
||||||
onRelatedMangaClick: (Manga) -> Unit,
|
onRelatedMangaClick: (Manga) -> Unit,
|
||||||
onRelatedMangaLongClick: (Manga) -> Unit,
|
onRelatedMangaLongClick: (Manga) -> Unit,
|
||||||
onSourceClick: () -> Unit,
|
onSourceClick: () -> Unit,
|
||||||
|
|
@ -231,6 +236,7 @@ fun MangaScreen(
|
||||||
onInvertSelection = onInvertSelection,
|
onInvertSelection = onInvertSelection,
|
||||||
// KMK -->
|
// KMK -->
|
||||||
getMangaState = getMangaState,
|
getMangaState = getMangaState,
|
||||||
|
onRelatedMangasScreenClick = onRelatedMangasScreenClick,
|
||||||
onRelatedMangaClick = onRelatedMangaClick,
|
onRelatedMangaClick = onRelatedMangaClick,
|
||||||
onRelatedMangaLongClick = onRelatedMangaLongClick,
|
onRelatedMangaLongClick = onRelatedMangaLongClick,
|
||||||
onSourceClick = onSourceClick,
|
onSourceClick = onSourceClick,
|
||||||
|
|
@ -283,6 +289,7 @@ fun MangaScreen(
|
||||||
onInvertSelection = onInvertSelection,
|
onInvertSelection = onInvertSelection,
|
||||||
// KMK -->
|
// KMK -->
|
||||||
getMangaState = getMangaState,
|
getMangaState = getMangaState,
|
||||||
|
onRelatedMangasScreenClick = onRelatedMangasScreenClick,
|
||||||
onRelatedMangaClick = onRelatedMangaClick,
|
onRelatedMangaClick = onRelatedMangaClick,
|
||||||
onRelatedMangaLongClick = onRelatedMangaLongClick,
|
onRelatedMangaLongClick = onRelatedMangaLongClick,
|
||||||
onSourceClick = onSourceClick,
|
onSourceClick = onSourceClick,
|
||||||
|
|
@ -352,6 +359,7 @@ private fun MangaScreenSmallImpl(
|
||||||
|
|
||||||
// KMK -->
|
// KMK -->
|
||||||
getMangaState: @Composable ((Manga) -> State<Manga>),
|
getMangaState: @Composable ((Manga) -> State<Manga>),
|
||||||
|
onRelatedMangasScreenClick: () -> Unit,
|
||||||
onRelatedMangaClick: (Manga) -> Unit,
|
onRelatedMangaClick: (Manga) -> Unit,
|
||||||
onRelatedMangaLongClick: (Manga) -> Unit,
|
onRelatedMangaLongClick: (Manga) -> Unit,
|
||||||
onSourceClick: () -> Unit,
|
onSourceClick: () -> Unit,
|
||||||
|
|
@ -372,6 +380,9 @@ private fun MangaScreenSmallImpl(
|
||||||
mutableStateOf(Dp.Hairline)
|
mutableStateOf(Dp.Hairline)
|
||||||
}
|
}
|
||||||
// SY <--
|
// SY <--
|
||||||
|
// KMK -->
|
||||||
|
val relatedMangasEnabled = Injekt.get<SourcePreferences>().relatedMangas().get()
|
||||||
|
// KMK <--
|
||||||
|
|
||||||
val internalOnBackPressed = {
|
val internalOnBackPressed = {
|
||||||
if (isAnySelected) {
|
if (isAnySelected) {
|
||||||
|
|
@ -566,22 +577,23 @@ private fun MangaScreenSmallImpl(
|
||||||
}
|
}
|
||||||
|
|
||||||
// KMK -->
|
// KMK -->
|
||||||
if (state.relatedMangas?.isNotEmpty() == true) {
|
if (state.source !is StubSource && relatedMangasEnabled) {
|
||||||
item(
|
item(
|
||||||
key = MangaScreenItem.RELATED_TITLES,
|
key = MangaScreenItem.RELATED_TITLES,
|
||||||
contentType = MangaScreenItem.RELATED_TITLES,
|
contentType = MangaScreenItem.RELATED_TITLES,
|
||||||
) {
|
) {
|
||||||
GlobalSearchResultItem(
|
Column {
|
||||||
title = "Related titles",
|
RelatedMangaTitle(
|
||||||
subtitle = null,
|
title = stringResource(SYMR.strings.pref_source_related_mangas),
|
||||||
onLongClick = null,
|
subtitle = null,
|
||||||
onClick = { /* Should show a page with grid/list of all the related manga */ },
|
onClick = onRelatedMangasScreenClick,
|
||||||
) {
|
onLongClick = null,
|
||||||
|
)
|
||||||
RelatedMangas(
|
RelatedMangas(
|
||||||
mangas = state.relatedMangas.map { it.value },
|
relatedMangas = state.relatedMangasSorted,
|
||||||
getMangaState = { getMangaState(it) },
|
getMangaState = getMangaState,
|
||||||
onClickManga = onRelatedMangaClick,
|
onMangaClick = onRelatedMangaClick,
|
||||||
onLongClickManga = onRelatedMangaLongClick,
|
onMangaLongClick = onRelatedMangaLongClick
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -651,7 +663,7 @@ private fun MangaScreenSmallImpl(
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MangaScreenLargeImpl(
|
private fun MangaScreenLargeImpl(
|
||||||
state: MangaScreenModel.State.Success,
|
state: MangaScreenModel.State.Success,
|
||||||
snackbarHostState: SnackbarHostState,
|
snackbarHostState: SnackbarHostState,
|
||||||
nextUpdate: Instant?,
|
nextUpdate: Instant?,
|
||||||
|
|
@ -711,6 +723,7 @@ fun MangaScreenLargeImpl(
|
||||||
|
|
||||||
// KMK -->
|
// KMK -->
|
||||||
getMangaState: @Composable ((Manga) -> State<Manga>),
|
getMangaState: @Composable ((Manga) -> State<Manga>),
|
||||||
|
onRelatedMangasScreenClick: () -> Unit,
|
||||||
onRelatedMangaClick: (Manga) -> Unit,
|
onRelatedMangaClick: (Manga) -> Unit,
|
||||||
onRelatedMangaLongClick: (Manga) -> Unit,
|
onRelatedMangaLongClick: (Manga) -> Unit,
|
||||||
onSourceClick: () -> Unit,
|
onSourceClick: () -> Unit,
|
||||||
|
|
@ -730,6 +743,9 @@ fun MangaScreenLargeImpl(
|
||||||
// SY -->
|
// SY -->
|
||||||
val metadataDescription = metadataDescription(state.source)
|
val metadataDescription = metadataDescription(state.source)
|
||||||
// SY <--
|
// SY <--
|
||||||
|
// KMK -->
|
||||||
|
val relatedMangasEnabled = Injekt.get<SourcePreferences>().relatedMangas().get()
|
||||||
|
// KMK <--
|
||||||
|
|
||||||
val insetPadding = WindowInsets.systemBars.only(WindowInsetsSides.Horizontal).asPaddingValues()
|
val insetPadding = WindowInsets.systemBars.only(WindowInsetsSides.Horizontal).asPaddingValues()
|
||||||
var topBarHeight by remember { mutableIntStateOf(0) }
|
var topBarHeight by remember { mutableIntStateOf(0) }
|
||||||
|
|
@ -928,22 +944,23 @@ fun MangaScreenLargeImpl(
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
// KMK -->
|
// KMK -->
|
||||||
if (state.relatedMangas?.isNotEmpty() == true) {
|
if (state.source !is StubSource && relatedMangasEnabled) {
|
||||||
item(
|
item(
|
||||||
key = MangaScreenItem.RELATED_TITLES,
|
key = MangaScreenItem.RELATED_TITLES,
|
||||||
contentType = MangaScreenItem.RELATED_TITLES,
|
contentType = MangaScreenItem.RELATED_TITLES,
|
||||||
) {
|
) {
|
||||||
GlobalSearchResultItem(
|
Column {
|
||||||
title = "Related titles",
|
RelatedMangaTitle(
|
||||||
subtitle = null,
|
title = stringResource(SYMR.strings.pref_source_related_mangas),
|
||||||
onLongClick = null,
|
subtitle = null,
|
||||||
onClick = { /* Should show a page with grid/list of all the related manga */ },
|
onClick = onRelatedMangasScreenClick,
|
||||||
) {
|
onLongClick = null,
|
||||||
|
)
|
||||||
RelatedMangas(
|
RelatedMangas(
|
||||||
mangas = state.relatedMangas.map { it.value },
|
relatedMangas = state.relatedMangasSorted,
|
||||||
getMangaState = { getMangaState(it) },
|
getMangaState = getMangaState,
|
||||||
onClickManga = onRelatedMangaClick,
|
onMangaClick = onRelatedMangaClick,
|
||||||
onLongClickManga = onRelatedMangaLongClick,
|
onMangaLongClick = onRelatedMangaLongClick
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -987,24 +1004,6 @@ fun MangaScreenLargeImpl(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// KMK -->
|
|
||||||
@Composable
|
|
||||||
fun RelatedMangas(
|
|
||||||
mangas: List<Manga>,
|
|
||||||
getMangaState: @Composable ((Manga) -> State<Manga>),
|
|
||||||
onClickManga: (Manga) -> Unit,
|
|
||||||
onLongClickManga: (Manga) -> Unit,
|
|
||||||
) {
|
|
||||||
GlobalSearchCardRow(
|
|
||||||
titles = mangas,
|
|
||||||
getManga = getMangaState,
|
|
||||||
onClick = onClickManga,
|
|
||||||
onLongClick = onLongClickManga,
|
|
||||||
selection = emptyList(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
// KMK <--
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun SharedMangaBottomActionMenu(
|
private fun SharedMangaBottomActionMenu(
|
||||||
selected: List<ChapterList.Item>,
|
selected: List<ChapterList.Item>,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,101 @@
|
||||||
|
package eu.kanade.presentation.manga.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.State
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import eu.kanade.presentation.browse.components.EmptyResultItem
|
||||||
|
import eu.kanade.presentation.browse.components.GlobalSearchLoadingResultItem
|
||||||
|
import eu.kanade.presentation.browse.components.MangaItem
|
||||||
|
import eu.kanade.tachiyomi.ui.manga.RelatedManga
|
||||||
|
import tachiyomi.domain.manga.model.Manga
|
||||||
|
import tachiyomi.domain.manga.model.asMangaCover
|
||||||
|
import tachiyomi.presentation.core.components.material.padding
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RelatedMangas(
|
||||||
|
relatedMangas: List<RelatedManga>?,
|
||||||
|
getMangaState: @Composable (Manga) -> State<Manga>,
|
||||||
|
onMangaClick: (Manga) -> Unit,
|
||||||
|
onMangaLongClick: (Manga) -> Unit,
|
||||||
|
) {
|
||||||
|
when {
|
||||||
|
relatedMangas == null -> {
|
||||||
|
GlobalSearchLoadingResultItem()
|
||||||
|
}
|
||||||
|
|
||||||
|
relatedMangas.isNotEmpty() -> {
|
||||||
|
RelatedMangaCardRow(
|
||||||
|
relatedMangas = relatedMangas,
|
||||||
|
getManga = { getMangaState(it) },
|
||||||
|
onMangaClick = onMangaClick,
|
||||||
|
onMangaLongClick = onMangaLongClick,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> { EmptyResultItem() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RelatedMangaCardRow(
|
||||||
|
relatedMangas: List<RelatedManga>,
|
||||||
|
getManga: @Composable (Manga) -> State<Manga>,
|
||||||
|
onMangaClick: (Manga) -> Unit,
|
||||||
|
onMangaLongClick: (Manga) -> Unit,
|
||||||
|
) {
|
||||||
|
val mangas = relatedMangas.filterIsInstance<RelatedManga.Success>().map { it.mangaList }.flatten()
|
||||||
|
val loading = relatedMangas.filterIsInstance<RelatedManga.Loading>().firstOrNull()
|
||||||
|
|
||||||
|
LazyRow(
|
||||||
|
contentPadding = PaddingValues(MaterialTheme.padding.small),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
|
||||||
|
) {
|
||||||
|
items(mangas) {
|
||||||
|
val manga by getManga(it)
|
||||||
|
MangaItem(
|
||||||
|
title = manga.title,
|
||||||
|
cover = manga.asMangaCover(),
|
||||||
|
isFavorite = manga.favorite,
|
||||||
|
onClick = { onMangaClick(manga) },
|
||||||
|
onLongClick = { onMangaLongClick(manga) },
|
||||||
|
isSelected = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (loading != null) {
|
||||||
|
item {
|
||||||
|
RelatedMangaLoadingItem()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RelatedMangaLoadingItem() {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(96.dp)
|
||||||
|
.aspectRatio(MangaCover.Book.ratio)
|
||||||
|
.padding(vertical = MaterialTheme.padding.medium),
|
||||||
|
) {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(16.dp)
|
||||||
|
.align(Alignment.Center),
|
||||||
|
strokeWidth = 2.dp,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -71,6 +71,7 @@ class DownloadCache(
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private val scope = CoroutineScope(Dispatchers.IO)
|
private val scope = CoroutineScope(Dispatchers.IO)
|
||||||
|
|
||||||
// KMK -->
|
// KMK -->
|
||||||
private val downloadPreferences: DownloadPreferences = Injekt.get()
|
private val downloadPreferences: DownloadPreferences = Injekt.get()
|
||||||
// KMK <--
|
// KMK <--
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,160 @@
|
||||||
|
package eu.kanade.tachiyomi.ui.browse
|
||||||
|
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
|
import androidx.compose.foundation.lazy.grid.GridCells
|
||||||
|
import androidx.compose.material3.SnackbarHost
|
||||||
|
import androidx.compose.material3.SnackbarHostState
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.State
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.produceState
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
|
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import cafe.adriel.voyager.core.model.rememberScreenModel
|
||||||
|
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||||
|
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||||
|
import eu.kanade.core.preference.asState
|
||||||
|
import eu.kanade.domain.source.service.SourcePreferences
|
||||||
|
import eu.kanade.presentation.browse.RelatedMangasContent
|
||||||
|
import eu.kanade.presentation.browse.components.BrowseSourceSimpleToolbar
|
||||||
|
import eu.kanade.presentation.components.SelectionToolbar
|
||||||
|
import eu.kanade.presentation.util.Screen
|
||||||
|
import eu.kanade.tachiyomi.ui.manga.MangaScreen
|
||||||
|
import eu.kanade.tachiyomi.ui.manga.MangaScreenModel
|
||||||
|
import eu.kanade.tachiyomi.ui.manga.RelatedManga
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import kotlinx.coroutines.flow.filterNotNull
|
||||||
|
import tachiyomi.domain.library.service.LibraryPreferences
|
||||||
|
import tachiyomi.domain.manga.interactor.GetManga
|
||||||
|
import tachiyomi.domain.manga.model.Manga
|
||||||
|
import tachiyomi.presentation.core.components.material.Scaffold
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
|
class RelatedMangasScreen(
|
||||||
|
val mangaScreenModel: MangaScreenModel,
|
||||||
|
val onKeywordClick: (String) -> Unit,
|
||||||
|
val onKeywordLongClick: (String) -> Unit,
|
||||||
|
private val getManga: GetManga = Injekt.get(),
|
||||||
|
private val sourcePreferences: SourcePreferences = Injekt.get(),
|
||||||
|
private val libraryPreferences: LibraryPreferences = Injekt.get(),
|
||||||
|
) : Screen() {
|
||||||
|
@Composable
|
||||||
|
override fun Content() {
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
var displayMode by sourcePreferences.sourceDisplayMode().asState(scope)
|
||||||
|
|
||||||
|
val screenState by mangaScreenModel.state.collectAsState()
|
||||||
|
val successState = screenState as? MangaScreenModel.State.Success ?: return
|
||||||
|
|
||||||
|
val bulkFavoriteScreenModel = rememberScreenModel { BulkFavoriteScreenModel() }
|
||||||
|
val bulkFavoriteState by bulkFavoriteScreenModel.state.collectAsState()
|
||||||
|
|
||||||
|
val navigator = LocalNavigator.currentOrThrow
|
||||||
|
val haptic = LocalHapticFeedback.current
|
||||||
|
|
||||||
|
BackHandler(enabled = bulkFavoriteState.selectionMode) {
|
||||||
|
bulkFavoriteScreenModel.toggleSelectionMode()
|
||||||
|
}
|
||||||
|
|
||||||
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = { scrollBehavior ->
|
||||||
|
if (bulkFavoriteState.selectionMode) {
|
||||||
|
SelectionToolbar(
|
||||||
|
selectedCount = bulkFavoriteState.selection.size,
|
||||||
|
onClickClearSelection = bulkFavoriteScreenModel::toggleSelectionMode,
|
||||||
|
onChangeCategoryClicked = bulkFavoriteScreenModel::addFavorite,
|
||||||
|
onSelectAll = {
|
||||||
|
successState.relatedMangasSorted?.forEach {
|
||||||
|
val relatedManga = it as RelatedManga.Success
|
||||||
|
relatedManga.mangaList.forEach { manga ->
|
||||||
|
if (!bulkFavoriteState.selection.contains(manga)) {
|
||||||
|
bulkFavoriteScreenModel.select(manga)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
BrowseSourceSimpleToolbar(
|
||||||
|
navigateUp = navigator::pop,
|
||||||
|
title = successState.manga.title,
|
||||||
|
displayMode = displayMode,
|
||||||
|
onDisplayModeChange = { displayMode = it },
|
||||||
|
scrollBehavior = scrollBehavior,
|
||||||
|
toggleSelectionMode = bulkFavoriteScreenModel::toggleSelectionMode,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
|
||||||
|
) { paddingValues ->
|
||||||
|
RelatedMangasContent(
|
||||||
|
relatedMangas = successState.relatedMangasSorted,
|
||||||
|
getMangaState = { manga -> getMangaState(initialManga = manga) },
|
||||||
|
columns = getColumnsPreference(LocalConfiguration.current.orientation),
|
||||||
|
displayMode = displayMode,
|
||||||
|
contentPadding = paddingValues,
|
||||||
|
onMangaClick = { manga ->
|
||||||
|
if (bulkFavoriteState.selectionMode) {
|
||||||
|
bulkFavoriteScreenModel.toggleSelection(manga)
|
||||||
|
} else {
|
||||||
|
navigator.push(MangaScreen(manga.id, true))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onMangaLongClick = { manga ->
|
||||||
|
if (!bulkFavoriteState.selectionMode) {
|
||||||
|
bulkFavoriteScreenModel.addRemoveManga(manga, haptic)
|
||||||
|
} else {
|
||||||
|
navigator.push(MangaScreen(manga.id, true))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onKeywordClick = onKeywordClick,
|
||||||
|
onKeywordLongClick = onKeywordLongClick,
|
||||||
|
selection = bulkFavoriteState.selection,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
when (bulkFavoriteState.dialog) {
|
||||||
|
is BulkFavoriteScreenModel.Dialog.AddDuplicateManga ->
|
||||||
|
AddDuplicateMangaDialog(bulkFavoriteScreenModel)
|
||||||
|
is BulkFavoriteScreenModel.Dialog.RemoveManga ->
|
||||||
|
RemoveMangaDialog(bulkFavoriteScreenModel)
|
||||||
|
is BulkFavoriteScreenModel.Dialog.ChangeMangaCategory ->
|
||||||
|
ChangeMangaCategoryDialog(bulkFavoriteScreenModel)
|
||||||
|
is BulkFavoriteScreenModel.Dialog.ChangeMangasCategory ->
|
||||||
|
ChangeMangasCategoryDialog(bulkFavoriteScreenModel)
|
||||||
|
is BulkFavoriteScreenModel.Dialog.AllowDuplicate ->
|
||||||
|
AllowDuplicateDialog(bulkFavoriteScreenModel)
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getColumnsPreference(orientation: Int): GridCells {
|
||||||
|
val isLandscape = orientation == Configuration.ORIENTATION_LANDSCAPE
|
||||||
|
val columns = if (isLandscape) {
|
||||||
|
libraryPreferences.landscapeColumns()
|
||||||
|
} else {
|
||||||
|
libraryPreferences.portraitColumns()
|
||||||
|
}.get()
|
||||||
|
return if (columns == 0) GridCells.Adaptive(128.dp) else GridCells.Fixed(columns)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun getMangaState(initialManga: Manga): State<Manga> {
|
||||||
|
return produceState(initialValue = initialManga) {
|
||||||
|
getManga.subscribe(initialManga.url, initialManga.source)
|
||||||
|
.filterNotNull()
|
||||||
|
.collectLatest { manga ->
|
||||||
|
value = manga
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -77,8 +77,9 @@ data class SourceSearchScreen(
|
||||||
onChangeCategoryClicked = bulkFavoriteScreenModel::addFavorite,
|
onChangeCategoryClicked = bulkFavoriteScreenModel::addFavorite,
|
||||||
onSelectAll = {
|
onSelectAll = {
|
||||||
state.mangaDisplayingList.forEach { manga ->
|
state.mangaDisplayingList.forEach { manga ->
|
||||||
if (!bulkFavoriteState.selection.contains(manga))
|
if (!bulkFavoriteState.selection.contains(manga)) {
|
||||||
bulkFavoriteScreenModel.select(manga)
|
bulkFavoriteScreenModel.select(manga)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -173,8 +173,9 @@ data class BrowseSourceScreen(
|
||||||
onChangeCategoryClicked = bulkFavoriteScreenModel::addFavorite,
|
onChangeCategoryClicked = bulkFavoriteScreenModel::addFavorite,
|
||||||
onSelectAll = {
|
onSelectAll = {
|
||||||
state.mangaDisplayingList.forEach { manga ->
|
state.mangaDisplayingList.forEach { manga ->
|
||||||
if (!bulkFavoriteState.selection.contains(manga))
|
if (!bulkFavoriteState.selection.contains(manga)) {
|
||||||
bulkFavoriteScreenModel.select(manga)
|
bulkFavoriteScreenModel.select(manga)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -474,7 +474,7 @@ open class BrowseSourceScreenModel(
|
||||||
val filterable: Boolean = true,
|
val filterable: Boolean = true,
|
||||||
// SY <--
|
// SY <--
|
||||||
// KMK -->
|
// KMK -->
|
||||||
val mangaDisplayingList: MutableSet<Manga> = emptySet<Manga>().toMutableSet()
|
val mangaDisplayingList: MutableSet<Manga> = mutableSetOf()
|
||||||
// KMK <--
|
// KMK <--
|
||||||
) {
|
) {
|
||||||
val isUserQuery get() = listing is Listing.Search && !listing.query.isNullOrEmpty()
|
val isUserQuery get() = listing is Listing.Search && !listing.query.isNullOrEmpty()
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,7 @@ import eu.kanade.tachiyomi.ui.browse.AllowDuplicateDialog
|
||||||
import eu.kanade.tachiyomi.ui.browse.BulkFavoriteScreenModel
|
import eu.kanade.tachiyomi.ui.browse.BulkFavoriteScreenModel
|
||||||
import eu.kanade.tachiyomi.ui.browse.ChangeMangaCategoryDialog
|
import eu.kanade.tachiyomi.ui.browse.ChangeMangaCategoryDialog
|
||||||
import eu.kanade.tachiyomi.ui.browse.ChangeMangasCategoryDialog
|
import eu.kanade.tachiyomi.ui.browse.ChangeMangasCategoryDialog
|
||||||
|
import eu.kanade.tachiyomi.ui.browse.RelatedMangasScreen
|
||||||
import eu.kanade.tachiyomi.ui.browse.RemoveMangaDialog
|
import eu.kanade.tachiyomi.ui.browse.RemoveMangaDialog
|
||||||
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionsScreen
|
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionsScreen
|
||||||
import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.PreMigrationScreen
|
import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.PreMigrationScreen
|
||||||
|
|
@ -243,6 +244,19 @@ class MangaScreen(
|
||||||
onInvertSelection = screenModel::invertSelection,
|
onInvertSelection = screenModel::invertSelection,
|
||||||
// KMK -->
|
// KMK -->
|
||||||
getMangaState = { screenModel.getManga(initialManga = it) },
|
getMangaState = { screenModel.getManga(initialManga = it) },
|
||||||
|
onRelatedMangasScreenClick = {
|
||||||
|
navigator.push(
|
||||||
|
RelatedMangasScreen(
|
||||||
|
mangaScreenModel = screenModel,
|
||||||
|
onKeywordClick = { query ->
|
||||||
|
navigator.push(BrowseSourceScreen(successState.source.id, query))
|
||||||
|
},
|
||||||
|
onKeywordLongClick = { query ->
|
||||||
|
navigator.push(GlobalSearchScreen(query))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
onRelatedMangaClick = { navigator.push(MangaScreen(it.id, true)) },
|
onRelatedMangaClick = { navigator.push(MangaScreen(it.id, true)) },
|
||||||
onRelatedMangaLongClick = { bulkFavoriteScreenModel.addRemoveManga(it, haptic) },
|
onRelatedMangaLongClick = { bulkFavoriteScreenModel.addRemoveManga(it, haptic) },
|
||||||
onSourceClick = {
|
onSourceClick = {
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.produceState
|
import androidx.compose.runtime.produceState
|
||||||
|
import androidx.compose.runtime.toMutableStateList
|
||||||
import androidx.compose.ui.util.fastAny
|
import androidx.compose.ui.util.fastAny
|
||||||
import cafe.adriel.voyager.core.model.StateScreenModel
|
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||||
import cafe.adriel.voyager.core.model.screenModelScope
|
import cafe.adriel.voyager.core.model.screenModelScope
|
||||||
|
|
@ -43,8 +44,12 @@ import eu.kanade.tachiyomi.network.HttpException
|
||||||
import eu.kanade.tachiyomi.source.PagePreviewSource
|
import eu.kanade.tachiyomi.source.PagePreviewSource
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.source.getNameForMangaInfo
|
import eu.kanade.tachiyomi.source.getNameForMangaInfo
|
||||||
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.MetadataSource
|
import eu.kanade.tachiyomi.source.online.MetadataSource
|
||||||
import eu.kanade.tachiyomi.source.online.all.MergedSource
|
import eu.kanade.tachiyomi.source.online.all.MergedSource
|
||||||
|
import eu.kanade.tachiyomi.ui.manga.RelatedManga.Companion.isLoading
|
||||||
|
import eu.kanade.tachiyomi.ui.manga.RelatedManga.Companion.removeDuplicates
|
||||||
|
import eu.kanade.tachiyomi.ui.manga.RelatedManga.Companion.sorted
|
||||||
import eu.kanade.tachiyomi.ui.manga.track.TrackItem
|
import eu.kanade.tachiyomi.ui.manga.track.TrackItem
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
|
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
|
||||||
import eu.kanade.tachiyomi.util.chapter.getNextUnread
|
import eu.kanade.tachiyomi.util.chapter.getNextUnread
|
||||||
|
|
@ -128,6 +133,7 @@ import tachiyomi.domain.manga.model.MergeMangaSettingsUpdate
|
||||||
import tachiyomi.domain.manga.model.MergedMangaReference
|
import tachiyomi.domain.manga.model.MergedMangaReference
|
||||||
import tachiyomi.domain.manga.model.applyFilter
|
import tachiyomi.domain.manga.model.applyFilter
|
||||||
import tachiyomi.domain.manga.repository.MangaRepository
|
import tachiyomi.domain.manga.repository.MangaRepository
|
||||||
|
import tachiyomi.domain.source.model.StubSource
|
||||||
import tachiyomi.domain.source.service.SourceManager
|
import tachiyomi.domain.source.service.SourceManager
|
||||||
import tachiyomi.domain.track.interactor.GetTracks
|
import tachiyomi.domain.track.interactor.GetTracks
|
||||||
import tachiyomi.domain.track.interactor.InsertTrack
|
import tachiyomi.domain.track.interactor.InsertTrack
|
||||||
|
|
@ -435,12 +441,17 @@ class MangaScreenModel(
|
||||||
|
|
||||||
// Fetch info-chapters when needed
|
// Fetch info-chapters when needed
|
||||||
if (screenModelScope.isActive) {
|
if (screenModelScope.isActive) {
|
||||||
|
// KMK -->
|
||||||
|
launch {
|
||||||
|
launch {
|
||||||
|
fetchRelatedMangasFromSource()
|
||||||
|
}.join()
|
||||||
|
updateSuccessState { it.copy(isFetchingRelatedMangas = false) }
|
||||||
|
}
|
||||||
|
// KMK <--
|
||||||
val fetchFromSourceTasks = listOf(
|
val fetchFromSourceTasks = listOf(
|
||||||
async { if (needRefreshInfo) fetchMangaFromSource() },
|
async { if (needRefreshInfo) fetchMangaFromSource() },
|
||||||
async { if (needRefreshChapter) fetchChaptersFromSource() },
|
async { if (needRefreshChapter) fetchChaptersFromSource() },
|
||||||
// KMK -->
|
|
||||||
async { fetchRelatedMangasFromSource() },
|
|
||||||
// KMK <--
|
|
||||||
)
|
)
|
||||||
fetchFromSourceTasks.awaitAll()
|
fetchFromSourceTasks.awaitAll()
|
||||||
}
|
}
|
||||||
|
|
@ -456,9 +467,6 @@ class MangaScreenModel(
|
||||||
val fetchFromSourceTasks = listOf(
|
val fetchFromSourceTasks = listOf(
|
||||||
async { fetchMangaFromSource(manualFetch) },
|
async { fetchMangaFromSource(manualFetch) },
|
||||||
async { fetchChaptersFromSource(manualFetch) },
|
async { fetchChaptersFromSource(manualFetch) },
|
||||||
// KMK -->
|
|
||||||
async { fetchRelatedMangasFromSource() },
|
|
||||||
// KMK <--
|
|
||||||
)
|
)
|
||||||
fetchFromSourceTasks.awaitAll()
|
fetchFromSourceTasks.awaitAll()
|
||||||
updateSuccessState { it.copy(isRefreshingData = false) }
|
updateSuccessState { it.copy(isRefreshingData = false) }
|
||||||
|
|
@ -1101,25 +1109,29 @@ class MangaScreenModel(
|
||||||
*/
|
*/
|
||||||
private suspend fun fetchRelatedMangasFromSource() {
|
private suspend fun fetchRelatedMangasFromSource() {
|
||||||
val state = successState ?: return
|
val state = successState ?: return
|
||||||
|
val relatedMangasEnabled = Injekt.get<SourcePreferences>().relatedMangas().get()
|
||||||
try {
|
try {
|
||||||
withIOContext {
|
withIOContext {
|
||||||
if (state.source !is MergedSource) {
|
if (state.source !is StubSource) {
|
||||||
val relatedMangas = if (Injekt.get<SourcePreferences>().relatedMangas().get()) {
|
if (relatedMangasEnabled) {
|
||||||
val map = mutableMapOf<Long, Manga>()
|
state.source.getRelatedMangaList(state.manga.toSManga()) { pair, _ ->
|
||||||
val list = state.source.getRelatedMangaList(state.manga.toSManga())
|
/* Push found related mangas into collection */
|
||||||
.map {
|
val relatedManga = RelatedManga.Success.fromPair(pair) { mangaList ->
|
||||||
networkToLocalManga.await(it.toDomainManga(state.source.id))
|
mangaList.map {
|
||||||
|
networkToLocalManga.await(it.toDomainManga(state.source.id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSuccessState { successState ->
|
||||||
|
val relatedMangaCollection =
|
||||||
|
successState.relatedMangaCollection
|
||||||
|
?.toMutableStateList()
|
||||||
|
?.apply { add(relatedManga) }
|
||||||
|
?: listOf(relatedManga)
|
||||||
|
successState.copy(relatedMangaCollection = relatedMangaCollection)
|
||||||
}
|
}
|
||||||
list.forEach {
|
|
||||||
map[it.id] = it
|
|
||||||
}
|
}
|
||||||
map
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
}
|
||||||
updateSuccessState { it.copy(relatedMangas = relatedMangas) }
|
|
||||||
} else {
|
|
||||||
throw UnsupportedOperationException("Fetching related titles for merged entry is not supported")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
|
|
@ -1129,8 +1141,6 @@ class MangaScreenModel(
|
||||||
screenModelScope.launch {
|
screenModelScope.launch {
|
||||||
snackbarHostState.showSnackbar(message = message)
|
snackbarHostState.showSnackbar(message = message)
|
||||||
}
|
}
|
||||||
val newManga = mangaRepository.getMangaById(mangaId)
|
|
||||||
updateSuccessState { it.copy(manga = newManga) }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// KMK <--
|
// KMK <--
|
||||||
|
|
@ -1714,9 +1724,22 @@ class MangaScreenModel(
|
||||||
val previewsRowCount: Int,
|
val previewsRowCount: Int,
|
||||||
// SY <--
|
// SY <--
|
||||||
// KMK -->
|
// KMK -->
|
||||||
val relatedMangas: MutableMap<Long, Manga>? = null,
|
val isFetchingRelatedMangas: Boolean = true,
|
||||||
|
/**
|
||||||
|
* a list of <keyword, related mangas>
|
||||||
|
*/
|
||||||
|
val relatedMangaCollection: List<RelatedManga>? = null,
|
||||||
// KMK <--
|
// KMK <--
|
||||||
) : State {
|
) : State {
|
||||||
|
// KMK ->>
|
||||||
|
val relatedMangasSorted = relatedMangaCollection
|
||||||
|
?.sorted(manga)
|
||||||
|
?.removeDuplicates(manga)
|
||||||
|
?.filter { it.isVisible() }
|
||||||
|
?.isLoading(isFetchingRelatedMangas)
|
||||||
|
?: if (!isFetchingRelatedMangas) emptyList() else null
|
||||||
|
// KMK <--
|
||||||
|
|
||||||
val processedChapters by lazy {
|
val processedChapters by lazy {
|
||||||
chapters.applyFilters(manga).toList()
|
chapters.applyFilters(manga).toList()
|
||||||
}
|
}
|
||||||
|
|
@ -1820,3 +1843,69 @@ sealed interface PagePreviewState {
|
||||||
data class Error(val error: Throwable) : PagePreviewState
|
data class Error(val error: Throwable) : PagePreviewState
|
||||||
}
|
}
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
|
// KMK -->
|
||||||
|
sealed interface RelatedManga {
|
||||||
|
data object Loading : RelatedManga
|
||||||
|
|
||||||
|
data class Success(
|
||||||
|
val keyword: String,
|
||||||
|
val mangaList: List<Manga>,
|
||||||
|
) : RelatedManga {
|
||||||
|
val isEmpty: Boolean
|
||||||
|
get() = mangaList.isEmpty()
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
suspend fun fromPair(
|
||||||
|
pair: Pair<String, List<SManga>>,
|
||||||
|
toManga: suspend (mangaList: List<SManga>) -> List<Manga>,
|
||||||
|
) = Success(pair.first, toManga(pair.second))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isVisible(): Boolean {
|
||||||
|
return this is Loading || (this is Success && !this.isEmpty)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun List<RelatedManga>.sorted(manga: Manga): List<RelatedManga> {
|
||||||
|
val success = filterIsInstance<Success>()
|
||||||
|
val loading = filterIsInstance<Loading>()
|
||||||
|
val title = manga.title.lowercase()
|
||||||
|
val ogTitle = manga.ogTitle.lowercase()
|
||||||
|
return success.filter { it.keyword.isEmpty() } +
|
||||||
|
success.filter { it.keyword.lowercase() == title } +
|
||||||
|
success.filter { it.keyword.lowercase() == ogTitle && ogTitle != title } +
|
||||||
|
success.filter { it.keyword.isNotEmpty() && it.keyword.lowercase() !in listOf(title, ogTitle) }
|
||||||
|
.sortedByDescending { it.keyword.length }
|
||||||
|
.sortedBy { it.mangaList.size } +
|
||||||
|
loading
|
||||||
|
}
|
||||||
|
|
||||||
|
fun List<RelatedManga>.removeDuplicates(manga: Manga): List<RelatedManga> {
|
||||||
|
val mangaUrls = HashSet<String>().apply { add(manga.url) }
|
||||||
|
|
||||||
|
return map { relatedManga ->
|
||||||
|
if (relatedManga is Success) {
|
||||||
|
Success(
|
||||||
|
relatedManga.keyword,
|
||||||
|
relatedManga.mangaList
|
||||||
|
.filterNot { mangaUrls.contains(it.url) }
|
||||||
|
.onEach { mangaUrls.add(it.url) },
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
relatedManga
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun List<RelatedManga>.isLoading(isFetchingRelatedMangas: Boolean): List<RelatedManga> {
|
||||||
|
return if (isFetchingRelatedMangas) {
|
||||||
|
this + listOf(Loading)
|
||||||
|
} else {
|
||||||
|
this
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// KMK <--
|
||||||
|
|
|
||||||
|
|
@ -75,8 +75,9 @@ class MangaDexFollowsScreen(private val sourceId: Long) : Screen() {
|
||||||
onChangeCategoryClicked = bulkFavoriteScreenModel::addFavorite,
|
onChangeCategoryClicked = bulkFavoriteScreenModel::addFavorite,
|
||||||
onSelectAll = {
|
onSelectAll = {
|
||||||
state.mangaDisplayingList.forEach { manga ->
|
state.mangaDisplayingList.forEach { manga ->
|
||||||
if (!bulkFavoriteState.selection.contains(manga))
|
if (!bulkFavoriteState.selection.contains(manga)) {
|
||||||
bulkFavoriteScreenModel.select(manga)
|
bulkFavoriteScreenModel.select(manga)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -71,8 +71,9 @@ class MangaDexSimilarScreen(val mangaId: Long, val sourceId: Long) : Screen() {
|
||||||
onChangeCategoryClicked = bulkFavoriteScreenModel::addFavorite,
|
onChangeCategoryClicked = bulkFavoriteScreenModel::addFavorite,
|
||||||
onSelectAll = {
|
onSelectAll = {
|
||||||
state.mangaDisplayingList.forEach { manga ->
|
state.mangaDisplayingList.forEach { manga ->
|
||||||
if (!bulkFavoriteState.selection.contains(manga))
|
if (!bulkFavoriteState.selection.contains(manga)) {
|
||||||
bulkFavoriteScreenModel.select(manga)
|
bulkFavoriteScreenModel.select(manga)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -70,8 +70,9 @@ class RecommendsScreen(val mangaId: Long, val sourceId: Long) : Screen() {
|
||||||
onChangeCategoryClicked = bulkFavoriteScreenModel::addFavorite,
|
onChangeCategoryClicked = bulkFavoriteScreenModel::addFavorite,
|
||||||
onSelectAll = {
|
onSelectAll = {
|
||||||
state.mangaDisplayingList.forEach { manga ->
|
state.mangaDisplayingList.forEach { manga ->
|
||||||
if (!bulkFavoriteState.selection.contains(manga))
|
if (!bulkFavoriteState.selection.contains(manga)) {
|
||||||
bulkFavoriteScreenModel.select(manga)
|
bulkFavoriteScreenModel.select(manga)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
@ -119,10 +120,8 @@ class RecommendsScreen(val mangaId: Long, val sourceId: Long) : Screen() {
|
||||||
// KMK -->
|
// KMK -->
|
||||||
if (!bulkFavoriteState.selectionMode) {
|
if (!bulkFavoriteState.selectionMode) {
|
||||||
bulkFavoriteScreenModel.addRemoveManga(manga, haptic)
|
bulkFavoriteScreenModel.addRemoveManga(manga, haptic)
|
||||||
} else {
|
|
||||||
// KMK <--
|
|
||||||
onMangaClick(manga)
|
|
||||||
}
|
}
|
||||||
|
// KMK <--
|
||||||
},
|
},
|
||||||
// KMK -->
|
// KMK -->
|
||||||
selection = bulkFavoriteState.selection,
|
selection = bulkFavoriteState.selection,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,12 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<changelog bulletedList="true">
|
<changelog bulletedList="true">
|
||||||
<changelogversion versionName="Current" changeDate="May 9,2024">
|
<changelogversion versionName="Latest (r8855)" changeDate="May 17, 2024">
|
||||||
|
<changelogtext>A dedicated screen for related entries, fetching directly from sources</changelogtext>
|
||||||
|
<changelogtext>Faster loading of related entries</changelogtext>
|
||||||
|
<changelogtext>Enable fetching related entries from site's entry page (things like "More like this", "Recommendations" etc.)</changelogtext>
|
||||||
|
<changelogtext>Improve & fix bugs of algorithm looking for related entries</changelogtext>
|
||||||
|
</changelogversion>
|
||||||
|
<changelogversion versionName="r8852" changeDate="May 9,2024">
|
||||||
<changelogtext>Beta build switch to 'preview' repo</changelogtext>
|
<changelogtext>Beta build switch to 'preview' repo</changelogtext>
|
||||||
</changelogversion>
|
</changelogversion>
|
||||||
<changelogversion versionName="1.10.5" changeDate="May 9,2024">
|
<changelogversion versionName="1.10.5" changeDate="May 9,2024">
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,11 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<changelog bulletedList="true">
|
<changelog bulletedList="true">
|
||||||
|
<changelogversion versionName="1.10.6" changeDate="2024">
|
||||||
|
<changelogtext>A dedicated screen for related entries, fetching directly from sources</changelogtext>
|
||||||
|
<changelogtext>Faster loading of related entries</changelogtext>
|
||||||
|
<changelogtext>Enable fetching related entries from site's entry page (things like "More like this", "Recommendations" etc.)</changelogtext>
|
||||||
|
<changelogtext>Improve & fix bugs of algorithm looking for related entries</changelogtext>
|
||||||
|
</changelogversion>
|
||||||
<changelogversion versionName="1.10.5" changeDate="May 9,2024">
|
<changelogversion versionName="1.10.5" changeDate="May 9,2024">
|
||||||
<changelogtext>Show related mangas for all sources</changelogtext>
|
<changelogtext>Show related mangas for all sources</changelogtext>
|
||||||
<changelogtext>Bulk select multiple entries to favorite/category</changelogtext>
|
<changelogtext>Bulk select multiple entries to favorite/category</changelogtext>
|
||||||
|
|
|
||||||
|
|
@ -22,8 +22,10 @@ class StubSource(
|
||||||
throw SourceNotInstalledException()
|
throw SourceNotInstalledException()
|
||||||
|
|
||||||
// KMK -->
|
// KMK -->
|
||||||
override suspend fun getRelatedMangaList(manga: SManga): List<SManga> =
|
override suspend fun getRelatedMangaList(
|
||||||
throw SourceNotInstalledException()
|
manga: SManga,
|
||||||
|
pushResults: suspend (relatedManga: Pair<String, List<SManga>>, completed: Boolean) -> Unit,
|
||||||
|
) = throw SourceNotInstalledException()
|
||||||
// KMK <--
|
// KMK <--
|
||||||
|
|
||||||
override fun toString(): String =
|
override fun toString(): String =
|
||||||
|
|
|
||||||
|
|
@ -202,8 +202,8 @@
|
||||||
<string name="pref_source_navigation_summery">Replace latest button with a custom browse view that includes both latest and browse</string>
|
<string name="pref_source_navigation_summery">Replace latest button with a custom browse view that includes both latest and browse</string>
|
||||||
<string name="pref_local_source_hidden_folders">Local source hidden folders</string>
|
<string name="pref_local_source_hidden_folders">Local source hidden folders</string>
|
||||||
<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 mangas</string>
|
<string name="pref_source_related_mangas">Related titles</string>
|
||||||
<string name="pref_source_related_mangas_summary">Show related mangas provided by extensions</string>
|
<string name="pref_source_related_mangas_summary">Show related titles while viewing entry</string>
|
||||||
|
|
||||||
<!-- Backup settings -->
|
<!-- Backup settings -->
|
||||||
<string name="custom_entry_info">Custom entry info</string>
|
<string name="custom_entry_info">Custom entry info</string>
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,13 @@ package eu.kanade.tachiyomi.source
|
||||||
|
|
||||||
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 kotlinx.coroutines.coroutineScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import logcat.LogPriority
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import tachiyomi.core.common.util.lang.awaitSingle
|
import tachiyomi.core.common.util.lang.awaitSingle
|
||||||
|
import tachiyomi.core.common.util.system.logcat
|
||||||
|
|
||||||
interface CatalogueSource : Source {
|
interface CatalogueSource : Source {
|
||||||
|
|
||||||
|
|
@ -57,6 +62,132 @@ interface CatalogueSource : Source {
|
||||||
*/
|
*/
|
||||||
fun getFilterList(): FilterList
|
fun getFilterList(): FilterList
|
||||||
|
|
||||||
|
// KMK -->
|
||||||
|
/**
|
||||||
|
* Whether parsing related mangas in manga page or extension provide custom related mangas request.
|
||||||
|
* @default false
|
||||||
|
* @since komikku/extensions-lib 1.6
|
||||||
|
*/
|
||||||
|
val supportsRelatedMangas: Boolean get() = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extensions doesn't want to use App's [getRelatedMangaListBySearch].
|
||||||
|
* @default false
|
||||||
|
* @since komikku/extensions-lib 1.6
|
||||||
|
*/
|
||||||
|
val disableRelatedMangasBySearch: Boolean get() = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disable showing any related titles.
|
||||||
|
* @default false
|
||||||
|
* @since komikku/extensions-lib 1.6
|
||||||
|
*/
|
||||||
|
val disableRelatedMangas: Boolean get() = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all the available related mangas for a manga.
|
||||||
|
* Normally it's not needed to override this method.
|
||||||
|
*
|
||||||
|
* @since komikku/extensions-lib 1.6
|
||||||
|
* @param manga the current manga to get related mangas.
|
||||||
|
* @return a list of <keyword, related mangas>
|
||||||
|
* @throws UnsupportedOperationException if a source doesn't support related mangas.
|
||||||
|
*/
|
||||||
|
override suspend fun getRelatedMangaList(
|
||||||
|
manga: SManga,
|
||||||
|
pushResults: suspend (relatedManga: Pair<String, List<SManga>>, completed: Boolean) -> Unit,
|
||||||
|
) {
|
||||||
|
if (!disableRelatedMangas) {
|
||||||
|
if (supportsRelatedMangas) {
|
||||||
|
coroutineScope {
|
||||||
|
launch {
|
||||||
|
runCatching { fetchRelatedMangaList(manga) }
|
||||||
|
.onSuccess { if (it.isNotEmpty()) pushResults(Pair("", it), false) }
|
||||||
|
.onFailure {
|
||||||
|
logcat(LogPriority.ERROR, it) { "## fetchRelatedMangaList: $it" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!disableRelatedMangasBySearch) {
|
||||||
|
getRelatedMangaListBySearch(manga, pushResults)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch related mangas for a manga from source/site.
|
||||||
|
*
|
||||||
|
* @since komikku/extensions-lib 1.6
|
||||||
|
* @param manga the current manga to get related mangas.
|
||||||
|
* @return the related mangas for the current manga.
|
||||||
|
* @throws UnsupportedOperationException if a source doesn't support related mangas.
|
||||||
|
*/
|
||||||
|
suspend fun fetchRelatedMangaList(manga: SManga): List<SManga> =
|
||||||
|
throw IllegalStateException("Not used")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Slit & strip manga's title into separate searchable keywords.
|
||||||
|
* Used for searching related mangas.
|
||||||
|
*
|
||||||
|
* @since komikku/extensions-lib 1.6
|
||||||
|
* @return List of keywords.
|
||||||
|
*/
|
||||||
|
fun String.stripKeywordForRelatedMangas(): List<String> {
|
||||||
|
val regexWhitespace = Regex("\\s+")
|
||||||
|
val regexSpecialCharacters =
|
||||||
|
Regex("([!~#$%^&*+_|/\\\\,?:;'“”‘’\"<>(){}\\[\\]。・~:—!?、―«»《》〘〙【】「」|]|\\s-|-\\s|\\s\\.|\\.\\s)")
|
||||||
|
val regexNumberOnly = Regex("^\\d+$")
|
||||||
|
|
||||||
|
return replace(regexSpecialCharacters, " ")
|
||||||
|
.split(regexWhitespace)
|
||||||
|
.map {
|
||||||
|
// remove number only
|
||||||
|
it.replace(regexNumberOnly, "")
|
||||||
|
.lowercase()
|
||||||
|
}
|
||||||
|
// exclude single character
|
||||||
|
.filter { it.length > 1 }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch related mangas by searching for each keywords from manga's title.
|
||||||
|
*
|
||||||
|
* @return a list of <keyword, related mangas>
|
||||||
|
* @since komikku/extensions-lib 1.6
|
||||||
|
*/
|
||||||
|
private suspend fun getRelatedMangaListBySearch(
|
||||||
|
manga: SManga,
|
||||||
|
pushResults: suspend (relatedManga: Pair<String, List<SManga>>, completed: Boolean) -> Unit,
|
||||||
|
) {
|
||||||
|
val words = HashSet<String>()
|
||||||
|
words.add(manga.title)
|
||||||
|
if (manga.title.lowercase() != manga.originalTitle.lowercase()) words.add(manga.originalTitle)
|
||||||
|
manga.title.stripKeywordForRelatedMangas()
|
||||||
|
.filterNot { word -> words.any { it.lowercase() == word } }
|
||||||
|
.onEach { words.add(it) }
|
||||||
|
manga.originalTitle.stripKeywordForRelatedMangas()
|
||||||
|
.filterNot { word -> words.any { it.lowercase() == word } }
|
||||||
|
.onEach { words.add(it) }
|
||||||
|
if (words.isEmpty()) return
|
||||||
|
|
||||||
|
coroutineScope {
|
||||||
|
words.map { keyword ->
|
||||||
|
launch {
|
||||||
|
runCatching {
|
||||||
|
getSearchManga(1, keyword, FilterList()).mangas
|
||||||
|
}
|
||||||
|
.onSuccess { if (it.isNotEmpty()) pushResults(Pair(keyword, it), false) }
|
||||||
|
.onFailure { e ->
|
||||||
|
logcat(LogPriority.ERROR, e) { "## getRelatedMangaListBySearch: $e" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// KMK <--
|
||||||
|
|
||||||
@Deprecated(
|
@Deprecated(
|
||||||
"Use the non-RxJava API instead",
|
"Use the non-RxJava API instead",
|
||||||
ReplaceWith("getPopularManga"),
|
ReplaceWith("getPopularManga"),
|
||||||
|
|
|
||||||
|
|
@ -54,10 +54,12 @@ interface Source {
|
||||||
*
|
*
|
||||||
* @since komikku/extensions-lib 1.6
|
* @since komikku/extensions-lib 1.6
|
||||||
* @param manga the current manga to get related mangas.
|
* @param manga the current manga to get related mangas.
|
||||||
* @return the related mangas for the current manga.
|
* @return a list of <keyword, related mangas>
|
||||||
*/
|
*/
|
||||||
suspend fun getRelatedMangaList(manga: SManga): List<SManga> =
|
suspend fun getRelatedMangaList(
|
||||||
throw IllegalStateException("Not used")
|
manga: SManga,
|
||||||
|
pushResults: suspend (relatedManga: Pair<String, List<SManga>>, completed: Boolean) -> Unit,
|
||||||
|
): Unit = throw IllegalStateException("Not used")
|
||||||
// KMK <--
|
// KMK <--
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -16,18 +16,14 @@ import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import exh.log.maybeInjectEHLogger
|
import exh.log.maybeInjectEHLogger
|
||||||
import exh.pref.DelegateSourcePreferences
|
import exh.pref.DelegateSourcePreferences
|
||||||
import exh.source.DelegatedHttpSource
|
import exh.source.DelegatedHttpSource
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.awaitAll
|
import kotlinx.coroutines.coroutineScope
|
||||||
import logcat.LogPriority
|
|
||||||
import okhttp3.Headers
|
import okhttp3.Headers
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import tachiyomi.core.common.util.lang.awaitSingle
|
import tachiyomi.core.common.util.lang.awaitSingle
|
||||||
import tachiyomi.core.common.util.system.logcat
|
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
|
|
@ -282,45 +278,11 @@ abstract class HttpSource : CatalogueSource {
|
||||||
|
|
||||||
// KMK -->
|
// KMK -->
|
||||||
/**
|
/**
|
||||||
* Whether parsing related mangas in manga page or extension provide custom related mangas request (true)
|
* Whether parsing related mangas in manga page or extension provide custom related mangas request.
|
||||||
* or using search keyword to search for related mangas (false - default)
|
* @default true
|
||||||
* @default false
|
|
||||||
* @since komikku/extensions-lib 1.6
|
* @since komikku/extensions-lib 1.6
|
||||||
*/
|
*/
|
||||||
protected open val supportsRelatedMangas: Boolean get() = false
|
override val supportsRelatedMangas: Boolean get() = true
|
||||||
|
|
||||||
/**
|
|
||||||
* Extensions provide custom [relatedMangaListRequest] and [relatedMangaListParse]
|
|
||||||
* while also want to use App's [getRelatedMangaListBySearch] together.
|
|
||||||
* @default false
|
|
||||||
* @since komikku/extensions-lib 1.6
|
|
||||||
*/
|
|
||||||
protected open val supportsRelatedMangasAndSearch: Boolean get() = false
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Disable showing any related titles
|
|
||||||
* @default false
|
|
||||||
* @since komikku/extensions-lib 1.6
|
|
||||||
*/
|
|
||||||
protected open val disableRelatedMangas: Boolean get() = false
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all the available related mangas for a manga.
|
|
||||||
* Normally it's not needed to override this method.
|
|
||||||
*
|
|
||||||
* @since komikku/extensions-lib 1.6
|
|
||||||
* @param manga the current manga to get related mangas.
|
|
||||||
* @return the related mangas for the current manga.
|
|
||||||
* @throws UnsupportedOperationException if a source doesn't support related mangas.
|
|
||||||
*/
|
|
||||||
override suspend fun getRelatedMangaList(manga: SManga): List<SManga> {
|
|
||||||
return when {
|
|
||||||
supportsRelatedMangasAndSearch -> fetchRelatedMangaList(manga) + getRelatedMangaListBySearch(manga)
|
|
||||||
supportsRelatedMangas -> fetchRelatedMangaList(manga)
|
|
||||||
disableRelatedMangas -> emptyList()
|
|
||||||
else -> getRelatedMangaListBySearch(manga)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch related mangas for a manga from source/site.
|
* Fetch related mangas for a manga from source/site.
|
||||||
|
|
@ -331,62 +293,14 @@ abstract class HttpSource : CatalogueSource {
|
||||||
* @return the related mangas for the current manga.
|
* @return the related mangas for the current manga.
|
||||||
* @throws UnsupportedOperationException if a source doesn't support related mangas.
|
* @throws UnsupportedOperationException if a source doesn't support related mangas.
|
||||||
*/
|
*/
|
||||||
protected open suspend fun fetchRelatedMangaList(manga: SManga): List<SManga> {
|
override suspend fun fetchRelatedMangaList(manga: SManga): List<SManga> = coroutineScope {
|
||||||
return client.newCall(relatedMangaListRequest(manga))
|
async {
|
||||||
.execute()
|
client.newCall(relatedMangaListRequest(manga))
|
||||||
.let { response ->
|
.execute()
|
||||||
relatedMangaListParse(response)
|
.let { response ->
|
||||||
}
|
relatedMangaListParse(response)
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch related mangas by searching for each keywords from manga's title
|
|
||||||
*
|
|
||||||
* @since komikku/extensions-lib 1.6
|
|
||||||
*/
|
|
||||||
protected open suspend fun getRelatedMangaListBySearch(manga: SManga): List<SManga> {
|
|
||||||
fun String.stripKeyword(): List<String> {
|
|
||||||
val regexWhitespace = Regex("\\s+")
|
|
||||||
val regexSpecialCharacters =
|
|
||||||
Regex("([!~#$%^&*+_|/\\\\,?:;'“”‘’\"<>(){}\\[\\]。・~:—!?、―«»《》〘〙【】「」|]|\\s-|-\\s|\\s\\.|\\.\\s)")
|
|
||||||
val regexNumberOnly = Regex("^\\d+$")
|
|
||||||
|
|
||||||
return replace(regexSpecialCharacters, " ")
|
|
||||||
.split(regexWhitespace)
|
|
||||||
.map {
|
|
||||||
// remove number only
|
|
||||||
it.replace(regexNumberOnly, "")
|
|
||||||
.lowercase()
|
|
||||||
}
|
}
|
||||||
// exclude single character
|
}.await()
|
||||||
.filter { it.length > 1 }
|
|
||||||
}
|
|
||||||
val scope = CoroutineScope(Dispatchers.IO)
|
|
||||||
val words = HashSet<String>()
|
|
||||||
words += manga.title.stripKeyword()
|
|
||||||
manga.originalTitle.let { title ->
|
|
||||||
words += title.stripKeyword()
|
|
||||||
}
|
|
||||||
if (words.isEmpty()) {
|
|
||||||
return emptyList()
|
|
||||||
}
|
|
||||||
|
|
||||||
return words.map { keyword ->
|
|
||||||
// Make multiple search in parallel
|
|
||||||
scope.async {
|
|
||||||
try {
|
|
||||||
getSearchManga(0, keyword, FilterList())
|
|
||||||
.mangas.filter {
|
|
||||||
it.url != manga.url &&
|
|
||||||
it.title.lowercase().contains(keyword)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
logcat(LogPriority.ERROR, e) { "getRelatedMangaListBySearch: $e" }
|
|
||||||
emptyList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.awaitAll()
|
|
||||||
.minBy { if (it.isEmpty()) Int.MAX_VALUE else it.size }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue