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 <--
|
||||
|
||||
// KMK -->
|
||||
fun relatedMangas() = preferenceStore.getBoolean("related_mangas", false)
|
||||
fun relatedMangas() = preferenceStore.getBoolean("related_mangas", true)
|
||||
// KMK <--
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,8 +58,9 @@ fun GlobalSearchScreen(
|
|||
when (result) {
|
||||
is SearchItemResult.Success -> {
|
||||
result.result.forEach { manga ->
|
||||
if (!bulkFavoriteState.selection.contains(manga))
|
||||
if (!bulkFavoriteState.selection.contains(manga)) {
|
||||
bulkFavoriteScreenModel.select(manga)
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {}
|
||||
|
|
|
|||
|
|
@ -48,8 +48,9 @@ fun MigrateSearchScreen(
|
|||
when (result) {
|
||||
is SearchItemResult.Success -> {
|
||||
result.result.forEach { manga ->
|
||||
if (!bulkFavoriteState.selection.contains(manga))
|
||||
if (!bulkFavoriteState.selection.contains(manga)) {
|
||||
bulkFavoriteScreenModel.select(manga)
|
||||
}
|
||||
}
|
||||
}
|
||||
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 = {
|
||||
items.forEach {
|
||||
it.results?.forEach { manga ->
|
||||
if (!bulkFavoriteState.selection.contains(manga))
|
||||
if (!bulkFavoriteState.selection.contains(manga)) {
|
||||
bulkFavoriteScreenModel.select(manga)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ fun BrowseSourceComfortableGrid(
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun BrowseSourceComfortableGridItem(
|
||||
internal fun BrowseSourceComfortableGridItem(
|
||||
manga: Manga,
|
||||
// SY -->
|
||||
metadata: RaisedSearchMetadata?,
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ fun BrowseSourceCompactGrid(
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun BrowseSourceCompactGridItem(
|
||||
internal fun BrowseSourceCompactGridItem(
|
||||
manga: Manga,
|
||||
// SY -->
|
||||
metadata: RaisedSearchMetadata?,
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ fun BrowseSourceList(
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun BrowseSourceListItem(
|
||||
internal fun BrowseSourceListItem(
|
||||
manga: Manga,
|
||||
// SY -->
|
||||
metadata: RaisedSearchMetadata?,
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ fun GlobalSearchCardRow(
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun MangaItem(
|
||||
internal fun MangaItem(
|
||||
title: String,
|
||||
cover: MangaCover,
|
||||
isFavorite: Boolean,
|
||||
|
|
@ -92,7 +92,7 @@ private fun MangaItem(
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun EmptyResultItem() {
|
||||
internal fun EmptyResultItem() {
|
||||
Text(
|
||||
text = stringResource(MR.strings.no_results_found),
|
||||
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 = {
|
||||
feedState.items?.forEach {
|
||||
it.results?.forEach { manga ->
|
||||
if (!bulkFavoriteState.selection.contains(manga))
|
||||
if (!bulkFavoriteState.selection.contains(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.fastAny
|
||||
import androidx.compose.ui.util.fastMap
|
||||
import eu.kanade.presentation.browse.components.GlobalSearchCardRow
|
||||
import eu.kanade.presentation.browse.components.GlobalSearchResultItem
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import eu.kanade.presentation.browse.RelatedMangaTitle
|
||||
import eu.kanade.presentation.components.relativeDateText
|
||||
import eu.kanade.presentation.manga.components.ChapterDownloadAction
|
||||
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.PagePreviewItems
|
||||
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.util.formatChapterNumber
|
||||
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.source.model.StubSource
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.i18n.sy.SYMR
|
||||
import tachiyomi.presentation.core.components.TwoPanelBox
|
||||
import tachiyomi.presentation.core.components.VerticalFastScroller
|
||||
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.util.shouldExpandFAB
|
||||
import tachiyomi.source.local.isLocal
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.time.Instant
|
||||
import java.time.ZoneId
|
||||
import java.time.ZonedDateTime
|
||||
|
|
@ -171,7 +175,8 @@ fun MangaScreen(
|
|||
onInvertSelection: () -> Unit,
|
||||
|
||||
// KMK -->
|
||||
getMangaState: @Composable ((Manga) -> State<Manga>),
|
||||
getMangaState: @Composable (Manga) -> State<Manga>,
|
||||
onRelatedMangasScreenClick: () -> Unit,
|
||||
onRelatedMangaClick: (Manga) -> Unit,
|
||||
onRelatedMangaLongClick: (Manga) -> Unit,
|
||||
onSourceClick: () -> Unit,
|
||||
|
|
@ -231,6 +236,7 @@ fun MangaScreen(
|
|||
onInvertSelection = onInvertSelection,
|
||||
// KMK -->
|
||||
getMangaState = getMangaState,
|
||||
onRelatedMangasScreenClick = onRelatedMangasScreenClick,
|
||||
onRelatedMangaClick = onRelatedMangaClick,
|
||||
onRelatedMangaLongClick = onRelatedMangaLongClick,
|
||||
onSourceClick = onSourceClick,
|
||||
|
|
@ -283,6 +289,7 @@ fun MangaScreen(
|
|||
onInvertSelection = onInvertSelection,
|
||||
// KMK -->
|
||||
getMangaState = getMangaState,
|
||||
onRelatedMangasScreenClick = onRelatedMangasScreenClick,
|
||||
onRelatedMangaClick = onRelatedMangaClick,
|
||||
onRelatedMangaLongClick = onRelatedMangaLongClick,
|
||||
onSourceClick = onSourceClick,
|
||||
|
|
@ -352,6 +359,7 @@ private fun MangaScreenSmallImpl(
|
|||
|
||||
// KMK -->
|
||||
getMangaState: @Composable ((Manga) -> State<Manga>),
|
||||
onRelatedMangasScreenClick: () -> Unit,
|
||||
onRelatedMangaClick: (Manga) -> Unit,
|
||||
onRelatedMangaLongClick: (Manga) -> Unit,
|
||||
onSourceClick: () -> Unit,
|
||||
|
|
@ -372,6 +380,9 @@ private fun MangaScreenSmallImpl(
|
|||
mutableStateOf(Dp.Hairline)
|
||||
}
|
||||
// SY <--
|
||||
// KMK -->
|
||||
val relatedMangasEnabled = Injekt.get<SourcePreferences>().relatedMangas().get()
|
||||
// KMK <--
|
||||
|
||||
val internalOnBackPressed = {
|
||||
if (isAnySelected) {
|
||||
|
|
@ -566,22 +577,23 @@ private fun MangaScreenSmallImpl(
|
|||
}
|
||||
|
||||
// KMK -->
|
||||
if (state.relatedMangas?.isNotEmpty() == true) {
|
||||
if (state.source !is StubSource && relatedMangasEnabled) {
|
||||
item(
|
||||
key = MangaScreenItem.RELATED_TITLES,
|
||||
contentType = MangaScreenItem.RELATED_TITLES,
|
||||
) {
|
||||
GlobalSearchResultItem(
|
||||
title = "Related titles",
|
||||
subtitle = null,
|
||||
onLongClick = null,
|
||||
onClick = { /* Should show a page with grid/list of all the related manga */ },
|
||||
) {
|
||||
Column {
|
||||
RelatedMangaTitle(
|
||||
title = stringResource(SYMR.strings.pref_source_related_mangas),
|
||||
subtitle = null,
|
||||
onClick = onRelatedMangasScreenClick,
|
||||
onLongClick = null,
|
||||
)
|
||||
RelatedMangas(
|
||||
mangas = state.relatedMangas.map { it.value },
|
||||
getMangaState = { getMangaState(it) },
|
||||
onClickManga = onRelatedMangaClick,
|
||||
onLongClickManga = onRelatedMangaLongClick,
|
||||
relatedMangas = state.relatedMangasSorted,
|
||||
getMangaState = getMangaState,
|
||||
onMangaClick = onRelatedMangaClick,
|
||||
onMangaLongClick = onRelatedMangaLongClick
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -651,7 +663,7 @@ private fun MangaScreenSmallImpl(
|
|||
}
|
||||
|
||||
@Composable
|
||||
fun MangaScreenLargeImpl(
|
||||
private fun MangaScreenLargeImpl(
|
||||
state: MangaScreenModel.State.Success,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
nextUpdate: Instant?,
|
||||
|
|
@ -711,6 +723,7 @@ fun MangaScreenLargeImpl(
|
|||
|
||||
// KMK -->
|
||||
getMangaState: @Composable ((Manga) -> State<Manga>),
|
||||
onRelatedMangasScreenClick: () -> Unit,
|
||||
onRelatedMangaClick: (Manga) -> Unit,
|
||||
onRelatedMangaLongClick: (Manga) -> Unit,
|
||||
onSourceClick: () -> Unit,
|
||||
|
|
@ -730,6 +743,9 @@ fun MangaScreenLargeImpl(
|
|||
// SY -->
|
||||
val metadataDescription = metadataDescription(state.source)
|
||||
// SY <--
|
||||
// KMK -->
|
||||
val relatedMangasEnabled = Injekt.get<SourcePreferences>().relatedMangas().get()
|
||||
// KMK <--
|
||||
|
||||
val insetPadding = WindowInsets.systemBars.only(WindowInsetsSides.Horizontal).asPaddingValues()
|
||||
var topBarHeight by remember { mutableIntStateOf(0) }
|
||||
|
|
@ -928,22 +944,23 @@ fun MangaScreenLargeImpl(
|
|||
),
|
||||
) {
|
||||
// KMK -->
|
||||
if (state.relatedMangas?.isNotEmpty() == true) {
|
||||
if (state.source !is StubSource && relatedMangasEnabled) {
|
||||
item(
|
||||
key = MangaScreenItem.RELATED_TITLES,
|
||||
contentType = MangaScreenItem.RELATED_TITLES,
|
||||
) {
|
||||
GlobalSearchResultItem(
|
||||
title = "Related titles",
|
||||
subtitle = null,
|
||||
onLongClick = null,
|
||||
onClick = { /* Should show a page with grid/list of all the related manga */ },
|
||||
) {
|
||||
Column {
|
||||
RelatedMangaTitle(
|
||||
title = stringResource(SYMR.strings.pref_source_related_mangas),
|
||||
subtitle = null,
|
||||
onClick = onRelatedMangasScreenClick,
|
||||
onLongClick = null,
|
||||
)
|
||||
RelatedMangas(
|
||||
mangas = state.relatedMangas.map { it.value },
|
||||
getMangaState = { getMangaState(it) },
|
||||
onClickManga = onRelatedMangaClick,
|
||||
onLongClickManga = onRelatedMangaLongClick,
|
||||
relatedMangas = state.relatedMangasSorted,
|
||||
getMangaState = getMangaState,
|
||||
onMangaClick = onRelatedMangaClick,
|
||||
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
|
||||
private fun SharedMangaBottomActionMenu(
|
||||
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)
|
||||
|
||||
// KMK -->
|
||||
private val downloadPreferences: DownloadPreferences = Injekt.get()
|
||||
// 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,
|
||||
onSelectAll = {
|
||||
state.mangaDisplayingList.forEach { manga ->
|
||||
if (!bulkFavoriteState.selection.contains(manga))
|
||||
if (!bulkFavoriteState.selection.contains(manga)) {
|
||||
bulkFavoriteScreenModel.select(manga)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -173,8 +173,9 @@ data class BrowseSourceScreen(
|
|||
onChangeCategoryClicked = bulkFavoriteScreenModel::addFavorite,
|
||||
onSelectAll = {
|
||||
state.mangaDisplayingList.forEach { manga ->
|
||||
if (!bulkFavoriteState.selection.contains(manga))
|
||||
if (!bulkFavoriteState.selection.contains(manga)) {
|
||||
bulkFavoriteScreenModel.select(manga)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -474,7 +474,7 @@ open class BrowseSourceScreenModel(
|
|||
val filterable: Boolean = true,
|
||||
// SY <--
|
||||
// KMK -->
|
||||
val mangaDisplayingList: MutableSet<Manga> = emptySet<Manga>().toMutableSet()
|
||||
val mangaDisplayingList: MutableSet<Manga> = mutableSetOf()
|
||||
// KMK <--
|
||||
) {
|
||||
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.ChangeMangaCategoryDialog
|
||||
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.extension.ExtensionsScreen
|
||||
import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.PreMigrationScreen
|
||||
|
|
@ -243,6 +244,19 @@ class MangaScreen(
|
|||
onInvertSelection = screenModel::invertSelection,
|
||||
// KMK -->
|
||||
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)) },
|
||||
onRelatedMangaLongClick = { bulkFavoriteScreenModel.addRemoveManga(it, haptic) },
|
||||
onSourceClick = {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.toMutableStateList
|
||||
import androidx.compose.ui.util.fastAny
|
||||
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||
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.Source
|
||||
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.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.reader.setting.ReaderPreferences
|
||||
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.applyFilter
|
||||
import tachiyomi.domain.manga.repository.MangaRepository
|
||||
import tachiyomi.domain.source.model.StubSource
|
||||
import tachiyomi.domain.source.service.SourceManager
|
||||
import tachiyomi.domain.track.interactor.GetTracks
|
||||
import tachiyomi.domain.track.interactor.InsertTrack
|
||||
|
|
@ -435,12 +441,17 @@ class MangaScreenModel(
|
|||
|
||||
// Fetch info-chapters when needed
|
||||
if (screenModelScope.isActive) {
|
||||
// KMK -->
|
||||
launch {
|
||||
launch {
|
||||
fetchRelatedMangasFromSource()
|
||||
}.join()
|
||||
updateSuccessState { it.copy(isFetchingRelatedMangas = false) }
|
||||
}
|
||||
// KMK <--
|
||||
val fetchFromSourceTasks = listOf(
|
||||
async { if (needRefreshInfo) fetchMangaFromSource() },
|
||||
async { if (needRefreshChapter) fetchChaptersFromSource() },
|
||||
// KMK -->
|
||||
async { fetchRelatedMangasFromSource() },
|
||||
// KMK <--
|
||||
)
|
||||
fetchFromSourceTasks.awaitAll()
|
||||
}
|
||||
|
|
@ -456,9 +467,6 @@ class MangaScreenModel(
|
|||
val fetchFromSourceTasks = listOf(
|
||||
async { fetchMangaFromSource(manualFetch) },
|
||||
async { fetchChaptersFromSource(manualFetch) },
|
||||
// KMK -->
|
||||
async { fetchRelatedMangasFromSource() },
|
||||
// KMK <--
|
||||
)
|
||||
fetchFromSourceTasks.awaitAll()
|
||||
updateSuccessState { it.copy(isRefreshingData = false) }
|
||||
|
|
@ -1101,25 +1109,29 @@ class MangaScreenModel(
|
|||
*/
|
||||
private suspend fun fetchRelatedMangasFromSource() {
|
||||
val state = successState ?: return
|
||||
val relatedMangasEnabled = Injekt.get<SourcePreferences>().relatedMangas().get()
|
||||
try {
|
||||
withIOContext {
|
||||
if (state.source !is MergedSource) {
|
||||
val relatedMangas = if (Injekt.get<SourcePreferences>().relatedMangas().get()) {
|
||||
val map = mutableMapOf<Long, Manga>()
|
||||
val list = state.source.getRelatedMangaList(state.manga.toSManga())
|
||||
.map {
|
||||
networkToLocalManga.await(it.toDomainManga(state.source.id))
|
||||
if (state.source !is StubSource) {
|
||||
if (relatedMangasEnabled) {
|
||||
state.source.getRelatedMangaList(state.manga.toSManga()) { pair, _ ->
|
||||
/* Push found related mangas into collection */
|
||||
val relatedManga = RelatedManga.Success.fromPair(pair) { mangaList ->
|
||||
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) {
|
||||
|
|
@ -1129,8 +1141,6 @@ class MangaScreenModel(
|
|||
screenModelScope.launch {
|
||||
snackbarHostState.showSnackbar(message = message)
|
||||
}
|
||||
val newManga = mangaRepository.getMangaById(mangaId)
|
||||
updateSuccessState { it.copy(manga = newManga) }
|
||||
}
|
||||
}
|
||||
// KMK <--
|
||||
|
|
@ -1714,9 +1724,22 @@ class MangaScreenModel(
|
|||
val previewsRowCount: Int,
|
||||
// SY <--
|
||||
// KMK -->
|
||||
val relatedMangas: MutableMap<Long, Manga>? = null,
|
||||
val isFetchingRelatedMangas: Boolean = true,
|
||||
/**
|
||||
* a list of <keyword, related mangas>
|
||||
*/
|
||||
val relatedMangaCollection: List<RelatedManga>? = null,
|
||||
// KMK <--
|
||||
) : State {
|
||||
// KMK ->>
|
||||
val relatedMangasSorted = relatedMangaCollection
|
||||
?.sorted(manga)
|
||||
?.removeDuplicates(manga)
|
||||
?.filter { it.isVisible() }
|
||||
?.isLoading(isFetchingRelatedMangas)
|
||||
?: if (!isFetchingRelatedMangas) emptyList() else null
|
||||
// KMK <--
|
||||
|
||||
val processedChapters by lazy {
|
||||
chapters.applyFilters(manga).toList()
|
||||
}
|
||||
|
|
@ -1820,3 +1843,69 @@ sealed interface PagePreviewState {
|
|||
data class Error(val error: Throwable) : PagePreviewState
|
||||
}
|
||||
// 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,
|
||||
onSelectAll = {
|
||||
state.mangaDisplayingList.forEach { manga ->
|
||||
if (!bulkFavoriteState.selection.contains(manga))
|
||||
if (!bulkFavoriteState.selection.contains(manga)) {
|
||||
bulkFavoriteScreenModel.select(manga)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -71,8 +71,9 @@ class MangaDexSimilarScreen(val mangaId: Long, val sourceId: Long) : Screen() {
|
|||
onChangeCategoryClicked = bulkFavoriteScreenModel::addFavorite,
|
||||
onSelectAll = {
|
||||
state.mangaDisplayingList.forEach { manga ->
|
||||
if (!bulkFavoriteState.selection.contains(manga))
|
||||
if (!bulkFavoriteState.selection.contains(manga)) {
|
||||
bulkFavoriteScreenModel.select(manga)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -70,8 +70,9 @@ class RecommendsScreen(val mangaId: Long, val sourceId: Long) : Screen() {
|
|||
onChangeCategoryClicked = bulkFavoriteScreenModel::addFavorite,
|
||||
onSelectAll = {
|
||||
state.mangaDisplayingList.forEach { manga ->
|
||||
if (!bulkFavoriteState.selection.contains(manga))
|
||||
if (!bulkFavoriteState.selection.contains(manga)) {
|
||||
bulkFavoriteScreenModel.select(manga)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
|
@ -119,10 +120,8 @@ class RecommendsScreen(val mangaId: Long, val sourceId: Long) : Screen() {
|
|||
// KMK -->
|
||||
if (!bulkFavoriteState.selectionMode) {
|
||||
bulkFavoriteScreenModel.addRemoveManga(manga, haptic)
|
||||
} else {
|
||||
// KMK <--
|
||||
onMangaClick(manga)
|
||||
}
|
||||
// KMK <--
|
||||
},
|
||||
// KMK -->
|
||||
selection = bulkFavoriteState.selection,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,12 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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>
|
||||
</changelogversion>
|
||||
<changelogversion versionName="1.10.5" changeDate="May 9,2024">
|
||||
|
|
|
|||
|
|
@ -1,5 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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">
|
||||
<changelogtext>Show related mangas for all sources</changelogtext>
|
||||
<changelogtext>Bulk select multiple entries to favorite/category</changelogtext>
|
||||
|
|
|
|||
|
|
@ -22,8 +22,10 @@ class StubSource(
|
|||
throw SourceNotInstalledException()
|
||||
|
||||
// KMK -->
|
||||
override suspend fun getRelatedMangaList(manga: SManga): List<SManga> =
|
||||
throw SourceNotInstalledException()
|
||||
override suspend fun getRelatedMangaList(
|
||||
manga: SManga,
|
||||
pushResults: suspend (relatedManga: Pair<String, List<SManga>>, completed: Boolean) -> Unit,
|
||||
) = throw SourceNotInstalledException()
|
||||
// KMK <--
|
||||
|
||||
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_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_source_related_mangas">Related mangas</string>
|
||||
<string name="pref_source_related_mangas_summary">Show related mangas provided by extensions</string>
|
||||
<string name="pref_source_related_mangas">Related titles</string>
|
||||
<string name="pref_source_related_mangas_summary">Show related titles while viewing entry</string>
|
||||
|
||||
<!-- Backup settings -->
|
||||
<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.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import logcat.LogPriority
|
||||
import rx.Observable
|
||||
import tachiyomi.core.common.util.lang.awaitSingle
|
||||
import tachiyomi.core.common.util.system.logcat
|
||||
|
||||
interface CatalogueSource : Source {
|
||||
|
||||
|
|
@ -57,6 +62,132 @@ interface CatalogueSource : Source {
|
|||
*/
|
||||
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(
|
||||
"Use the non-RxJava API instead",
|
||||
ReplaceWith("getPopularManga"),
|
||||
|
|
|
|||
|
|
@ -54,10 +54,12 @@ interface Source {
|
|||
*
|
||||
* @since komikku/extensions-lib 1.6
|
||||
* @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> =
|
||||
throw IllegalStateException("Not used")
|
||||
suspend fun getRelatedMangaList(
|
||||
manga: SManga,
|
||||
pushResults: suspend (relatedManga: Pair<String, List<SManga>>, completed: Boolean) -> Unit,
|
||||
): Unit = throw IllegalStateException("Not used")
|
||||
// KMK <--
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -16,18 +16,14 @@ import eu.kanade.tachiyomi.source.model.SManga
|
|||
import exh.log.maybeInjectEHLogger
|
||||
import exh.pref.DelegateSourcePreferences
|
||||
import exh.source.DelegatedHttpSource
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import logcat.LogPriority
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import okhttp3.Headers
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import rx.Observable
|
||||
import tachiyomi.core.common.util.lang.awaitSingle
|
||||
import tachiyomi.core.common.util.system.logcat
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.net.URI
|
||||
|
|
@ -282,45 +278,11 @@ abstract class HttpSource : CatalogueSource {
|
|||
|
||||
// KMK -->
|
||||
/**
|
||||
* Whether parsing related mangas in manga page or extension provide custom related mangas request (true)
|
||||
* or using search keyword to search for related mangas (false - default)
|
||||
* @default false
|
||||
* Whether parsing related mangas in manga page or extension provide custom related mangas request.
|
||||
* @default true
|
||||
* @since komikku/extensions-lib 1.6
|
||||
*/
|
||||
protected open val supportsRelatedMangas: Boolean get() = false
|
||||
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
}
|
||||
override val supportsRelatedMangas: Boolean get() = true
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @throws UnsupportedOperationException if a source doesn't support related mangas.
|
||||
*/
|
||||
protected open suspend fun fetchRelatedMangaList(manga: SManga): List<SManga> {
|
||||
return client.newCall(relatedMangaListRequest(manga))
|
||||
.execute()
|
||||
.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()
|
||||
override suspend fun fetchRelatedMangaList(manga: SManga): List<SManga> = coroutineScope {
|
||||
async {
|
||||
client.newCall(relatedMangaListRequest(manga))
|
||||
.execute()
|
||||
.let { response ->
|
||||
relatedMangaListParse(response)
|
||||
}
|
||||
// exclude single character
|
||||
.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 }
|
||||
}.await()
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Reference in a new issue