From d764ac3b63692b748493beb016e42dca9f228838 Mon Sep 17 00:00:00 2001 From: "Cuong M. Tran" Date: Tue, 23 Apr 2024 12:41:08 +0700 Subject: [PATCH 01/10] Related titles screen --- .../kanade/presentation/manga/MangaScreen.kt | 95 +++++++++++++++++++ .../manga/MangaScreenConstants.kt | 4 + .../kanade/tachiyomi/ui/manga/MangaScreen.kt | 5 + .../tachiyomi/ui/manga/MangaScreenModel.kt | 17 ++++ .../tachiyomi/domain/manga/model/Manga.kt | 5 + 5 files changed, 126 insertions(+) diff --git a/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt b/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt index 0f1cc335b..980954edb 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt @@ -31,6 +31,7 @@ import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.State import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf @@ -49,6 +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.presentation.components.relativeDateText import eu.kanade.presentation.manga.components.ChapterDownloadAction import eu.kanade.presentation.manga.components.ChapterHeader @@ -167,6 +170,12 @@ fun MangaScreen( onChapterSelected: (ChapterList.Item, Boolean, Boolean, Boolean) -> Unit, onAllChapterSelected: (Boolean) -> Unit, onInvertSelection: () -> Unit, + + // KMK --> + getMangaState: @Composable ((Manga) -> State), + onRelatedMangaClick: (Manga) -> Unit, + onRelatedMangaLongClick: (Manga) -> Unit, + // KMK <-- ) { val context = LocalContext.current val onCopyTagToClipboard: (tag: String) -> Unit = { @@ -220,6 +229,11 @@ fun MangaScreen( onChapterSelected = onChapterSelected, onAllChapterSelected = onAllChapterSelected, onInvertSelection = onInvertSelection, + // KMK --> + getMangaState = getMangaState, + onRelatedMangaClick = onRelatedMangaClick, + onRelatedMangaLongClick = onRelatedMangaLongClick, + // KMK <-- ) } else { MangaScreenLargeImpl( @@ -266,6 +280,11 @@ fun MangaScreen( onChapterSelected = onChapterSelected, onAllChapterSelected = onAllChapterSelected, onInvertSelection = onInvertSelection, + // KMK --> + getMangaState = getMangaState, + onRelatedMangaClick = onRelatedMangaClick, + onRelatedMangaLongClick = onRelatedMangaLongClick, + // KMK <-- ) } } @@ -328,6 +347,12 @@ private fun MangaScreenSmallImpl( onChapterSelected: (ChapterList.Item, Boolean, Boolean, Boolean) -> Unit, onAllChapterSelected: (Boolean) -> Unit, onInvertSelection: () -> Unit, + + // KMK --> + getMangaState: @Composable ((Manga) -> State), + onRelatedMangaClick: (Manga) -> Unit, + onRelatedMangaLongClick: (Manga) -> Unit, + // KMK <-- ) { val chapterListState = rememberLazyListState() @@ -534,6 +559,29 @@ private fun MangaScreenSmallImpl( ) } + // KMK --> + if (state.manga.relatedMangas!!.isNotEmpty()) { + 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 */ }, + ) { + RelatedMangas( + mangas = state.manga.relatedMangas!!, + getMangaState = { getMangaState(it) }, + onClickManga = onRelatedMangaClick, + onLongClickManga = onRelatedMangaLongClick, + ) + } + } + } + // KMK <-- + // SY --> if (!state.showRecommendationsInOverflow || state.showMergeWithAnother) { item( @@ -654,6 +702,12 @@ fun MangaScreenLargeImpl( onChapterSelected: (ChapterList.Item, Boolean, Boolean, Boolean) -> Unit, onAllChapterSelected: (Boolean) -> Unit, onInvertSelection: () -> Unit, + + // KMK --> + getMangaState: @Composable ((Manga) -> State), + onRelatedMangaClick: (Manga) -> Unit, + onRelatedMangaLongClick: (Manga) -> Unit, + // KMK <-- ) { val layoutDirection = LocalLayoutDirection.current val density = LocalDensity.current @@ -863,6 +917,29 @@ fun MangaScreenLargeImpl( bottom = contentPadding.calculateBottomPadding(), ), ) { + // KMK --> + if (state.manga.relatedMangas!!.isNotEmpty()) { + 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 */ }, + ) { + RelatedMangas( + mangas = state.manga.relatedMangas!!, + getMangaState = { getMangaState(it) }, + onClickManga = onRelatedMangaClick, + onLongClickManga = onRelatedMangaLongClick, + ) + } + } + } + // KMK <-- + item( key = MangaScreenItem.CHAPTER_HEADER, contentType = MangaScreenItem.CHAPTER_HEADER, @@ -900,6 +977,24 @@ fun MangaScreenLargeImpl( } } +// KMK --> +@Composable +fun RelatedMangas( + mangas: List, + getMangaState: @Composable ((Manga) -> State), + onClickManga: (Manga) -> Unit, + onLongClickManga: (Manga) -> Unit, +) { + GlobalSearchCardRow( + titles = mangas, + getManga = getMangaState, + onClick = onClickManga, + onLongClick = onLongClickManga, + selection = emptyList(), + ) +} +// KMK <-- + @Composable private fun SharedMangaBottomActionMenu( selected: List, diff --git a/app/src/main/java/eu/kanade/presentation/manga/MangaScreenConstants.kt b/app/src/main/java/eu/kanade/presentation/manga/MangaScreenConstants.kt index f4671eef4..148db03cf 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/MangaScreenConstants.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/MangaScreenConstants.kt @@ -32,4 +32,8 @@ enum class MangaScreenItem { // SY <-- CHAPTER_HEADER, CHAPTER, + + // KMK --> + RELATED_TITLES, + // KMK <-- } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt index b4bfba757..c365c5c36 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt @@ -225,6 +225,11 @@ class MangaScreen( onChapterSelected = screenModel::toggleSelection, onAllChapterSelected = screenModel::toggleAllSelection, onInvertSelection = screenModel::invertSelection, + // KMK --> + getMangaState = { screenModel.getManga(initialManga = it) }, + onRelatedMangaClick = { navigator.push(MangaScreen(it.id, true)) }, + onRelatedMangaLongClick = { }, + // KMK <-- ) var showScanlatorsDialog by remember { mutableStateOf(false) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt index 33895fb5a..d1d8d2167 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt @@ -3,8 +3,10 @@ package eu.kanade.tachiyomi.ui.manga import android.content.Context import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarResult +import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.runtime.getValue +import androidx.compose.runtime.produceState import androidx.compose.ui.util.fastAny import cafe.adriel.voyager.core.model.StateScreenModel import cafe.adriel.voyager.core.model.screenModelScope @@ -72,6 +74,7 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChangedBy import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.flatMapConcat import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map @@ -135,6 +138,7 @@ import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import uy.kohesive.injekt.injectLazy import kotlin.math.floor +import androidx.compose.runtime.State as RuntimeState class MangaScreenModel( val context: Context, @@ -553,6 +557,19 @@ class MangaScreenModel( } } + // KMK --> + @Composable + fun getManga(initialManga: Manga): RuntimeState { + return produceState(initialValue = initialManga) { + getManga.subscribe(initialManga.url, initialManga.source) + .filterNotNull() + .collectLatest { manga -> + value = manga + } + } + } + // KMK <-- + suspend fun smartSearchMerge(manga: Manga, originalMangaId: Long): Manga { val originalManga = getManga.await(originalMangaId) ?: throw IllegalArgumentException(context.stringResource(SYMR.strings.merge_unknown_entry, originalMangaId)) diff --git a/domain/src/main/java/tachiyomi/domain/manga/model/Manga.kt b/domain/src/main/java/tachiyomi/domain/manga/model/Manga.kt index 4f41b18d6..e2b53b1b9 100644 --- a/domain/src/main/java/tachiyomi/domain/manga/model/Manga.kt +++ b/domain/src/main/java/tachiyomi/domain/manga/model/Manga.kt @@ -65,6 +65,11 @@ data class Manga( get() = customMangaInfo?.status ?: ogStatus // SY <-- + // KMK --> + val relatedMangas: List? + get() = listOf(this, this, this, this, this, this, this, this, this, this, this, this, this) + // KMK <-- + val expectedNextUpdate: Instant? get() = nextUpdate .takeIf { status != SManga.COMPLETED.toLong() } From 4d3eeefb2cfc096c47fcb06293104b21756ae969 Mon Sep 17 00:00:00 2001 From: "Cuong M. Tran" Date: Tue, 23 Apr 2024 14:22:41 +0700 Subject: [PATCH 02/10] LongClick to add/remove related mangas to library MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Can support bulkSelection but let’s not complicated the screen --- .../ui/browse/BulkFavoriteScreenModel.kt | 2 +- .../kanade/tachiyomi/ui/manga/MangaScreen.kt | 29 ++++++++++++++++++- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BulkFavoriteScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BulkFavoriteScreenModel.kt index 28cf67d2c..660ff3eab 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BulkFavoriteScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BulkFavoriteScreenModel.kt @@ -252,7 +252,7 @@ class BulkFavoriteScreenModel( .orEmpty() } - suspend fun getDuplicateLibraryManga(manga: Manga): Manga? { + private suspend fun getDuplicateLibraryManga(manga: Manga): Manga? { return getDuplicateLibraryManga.await(manga).getOrNull(0) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt index c365c5c36..2da3d181e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt @@ -42,6 +42,12 @@ import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.isLocalOrStub import eu.kanade.tachiyomi.source.online.HttpSource +import eu.kanade.tachiyomi.ui.browse.AddDuplicateMangaDialog +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.RemoveMangaDialog import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.PreMigrationScreen import eu.kanade.tachiyomi.ui.browse.source.SourcesScreen import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreen @@ -121,6 +127,11 @@ class MangaScreen( val successState = state as MangaScreenModel.State.Success val isHttpSource = remember { successState.source is HttpSource } + // KMK --> + val bulkFavoriteScreenModel = rememberScreenModel { BulkFavoriteScreenModel() } + val bulkFavoriteState by bulkFavoriteScreenModel.state.collectAsState() + // KMK <-- + LaunchedEffect(successState.manga, screenModel.source) { if (isHttpSource) { try { @@ -228,7 +239,7 @@ class MangaScreen( // KMK --> getMangaState = { screenModel.getManga(initialManga = it) }, onRelatedMangaClick = { navigator.push(MangaScreen(it.id, true)) }, - onRelatedMangaLongClick = { }, + onRelatedMangaLongClick = { bulkFavoriteScreenModel.addRemoveManga(it, haptic) }, // KMK <-- ) @@ -347,6 +358,22 @@ class MangaScreen( onConfirm = screenModel::setExcludedScanlators, ) } + + // KMK --> + 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 -> {} + } + // KMK <-- } private fun continueReading(context: Context, unreadChapter: Chapter?) { From 0df6a7fb09ce02738876927bd3b36639ba48431c Mon Sep 17 00:00:00 2001 From: "Cuong M. Tran" Date: Thu, 25 Apr 2024 11:25:02 +0700 Subject: [PATCH 03/10] Add relatedMangas to MangaScreenModel --- .../main/java/eu/kanade/presentation/manga/MangaScreen.kt | 8 ++++---- .../java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt | 8 ++++++++ .../src/main/java/tachiyomi/domain/manga/model/Manga.kt | 5 ----- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt b/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt index 980954edb..4a9ac700b 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt @@ -560,7 +560,7 @@ private fun MangaScreenSmallImpl( } // KMK --> - if (state.manga.relatedMangas!!.isNotEmpty()) { + if (state.relatedMangas?.isNotEmpty() == true) { item( key = MangaScreenItem.RELATED_TITLES, contentType = MangaScreenItem.RELATED_TITLES, @@ -572,7 +572,7 @@ private fun MangaScreenSmallImpl( onClick = { /* Should show a page with grid/list of all the related manga */ }, ) { RelatedMangas( - mangas = state.manga.relatedMangas!!, + mangas = state.relatedMangas, getMangaState = { getMangaState(it) }, onClickManga = onRelatedMangaClick, onLongClickManga = onRelatedMangaLongClick, @@ -918,7 +918,7 @@ fun MangaScreenLargeImpl( ), ) { // KMK --> - if (state.manga.relatedMangas!!.isNotEmpty()) { + if (state.relatedMangas?.isNotEmpty() == true) { item( key = MangaScreenItem.RELATED_TITLES, contentType = MangaScreenItem.RELATED_TITLES, @@ -930,7 +930,7 @@ fun MangaScreenLargeImpl( onClick = { /* Should show a page with grid/list of all the related manga */ }, ) { RelatedMangas( - mangas = state.manga.relatedMangas!!, + mangas = state.relatedMangas, getMangaState = { getMangaState(it) }, onClickManga = onRelatedMangaClick, onLongClickManga = onRelatedMangaLongClick, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt index d1d8d2167..6267a7ea6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt @@ -208,6 +208,11 @@ class MangaScreenModel( private val filteredChapters: List? get() = successState?.processedChapters + // KMK --> + private val relatedMangas: List + get() = successState?.relatedMangas ?: emptyList() + // KMK <-- + val chapterSwipeStartAction = libraryPreferences.swipeToEndAction().get() val chapterSwipeEndAction = libraryPreferences.swipeToStartAction().get() @@ -1654,6 +1659,9 @@ class MangaScreenModel( val alwaysShowReadingProgress: Boolean, val previewsRowCount: Int, // SY <-- + // KMK --> + val relatedMangas: List? = null, + // KMK <-- ) : State { val processedChapters by lazy { chapters.applyFilters(manga).toList() diff --git a/domain/src/main/java/tachiyomi/domain/manga/model/Manga.kt b/domain/src/main/java/tachiyomi/domain/manga/model/Manga.kt index e2b53b1b9..4f41b18d6 100644 --- a/domain/src/main/java/tachiyomi/domain/manga/model/Manga.kt +++ b/domain/src/main/java/tachiyomi/domain/manga/model/Manga.kt @@ -65,11 +65,6 @@ data class Manga( get() = customMangaInfo?.status ?: ogStatus // SY <-- - // KMK --> - val relatedMangas: List? - get() = listOf(this, this, this, this, this, this, this, this, this, this, this, this, this) - // KMK <-- - val expectedNextUpdate: Instant? get() = nextUpdate .takeIf { status != SManga.COMPLETED.toLong() } From 89f8723d7e0174531e31a15cc8a8a15cda9fcc73 Mon Sep 17 00:00:00 2001 From: "Cuong M. Tran" Date: Thu, 25 Apr 2024 11:25:06 +0700 Subject: [PATCH 04/10] Fetching related mangas from Source --- .../tachiyomi/ui/manga/MangaScreenModel.kt | 47 +++++++++++++++++- .../eu/kanade/tachiyomi/source/Source.kt | 37 ++++++++++++++ .../tachiyomi/source/online/HttpSource.kt | 48 +++++++++++++++++++ .../source/online/ParsedHttpSource.kt | 24 ++++++++++ 4 files changed, 155 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt index 6267a7ea6..1ef329acc 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt @@ -24,6 +24,7 @@ import eu.kanade.domain.manga.model.PagePreview import eu.kanade.domain.manga.model.chaptersFiltered import eu.kanade.domain.manga.model.copyFrom import eu.kanade.domain.manga.model.downloadedFilter +import eu.kanade.domain.manga.model.toDomainManga import eu.kanade.domain.manga.model.toSManga import eu.kanade.domain.source.service.SourcePreferences import eu.kanade.domain.track.interactor.AddTracks @@ -395,6 +396,9 @@ class MangaScreenModel( val needRefreshInfo = !manga.initialized val needRefreshChapter = chapters.isEmpty() + // KMK --> + val needRefreshRelatedMangas = relatedMangas.isEmpty() + // KMK <-- // Show what we have earlier mutableState.update { @@ -412,7 +416,7 @@ class MangaScreenModel( }.toImmutableSet(), // SY <-- excludedScanlators = getExcludedScanlators.await(mangaId).toImmutableSet(), - isRefreshingData = needRefreshInfo || needRefreshChapter, + isRefreshingData = needRefreshInfo || needRefreshChapter /* KMK --> */|| needRefreshRelatedMangas,/* KMK <-- */ dialog = null, // SY --> showRecommendationsInOverflow = uiPreferences.recommendsInOverflow().get(), @@ -440,6 +444,9 @@ class MangaScreenModel( val fetchFromSourceTasks = listOf( async { if (needRefreshInfo) fetchMangaFromSource() }, async { if (needRefreshChapter) fetchChaptersFromSource() }, + // KMK --> + async { if (needRefreshRelatedMangas) fetchRelatedMangasFromSource() }, + // KMK <-- ) fetchFromSourceTasks.awaitAll() } @@ -455,6 +462,9 @@ class MangaScreenModel( val fetchFromSourceTasks = listOf( async { fetchMangaFromSource(manualFetch) }, async { fetchChaptersFromSource(manualFetch) }, + // KMK --> + async { fetchRelatedMangasFromSource() }, + // KMK <-- ) fetchFromSourceTasks.awaitAll() updateSuccessState { it.copy(isRefreshingData = false) } @@ -1090,6 +1100,41 @@ class MangaScreenModel( } } + // KMK --> + /** + * Requests an list of related mangas from the source. + */ + private suspend fun fetchRelatedMangasFromSource() { + val state = successState ?: return + try { + withIOContext { + if (state.source !is MergedSource) { + val relatedMangas = if (state.source.supportsRelatedMangas) + withIOContext { + state.source.getRelatedMangaList(state.manga.toSManga()) + .map { + networkToLocalManga.await(it.toDomainManga(state.source.id)) + } + } + else null + updateSuccessState { it.copy(relatedMangas = relatedMangas, isRefreshingData = false) } + } else { + throw UnsupportedOperationException("Fetching related titles for merged entry is not supported") + } + } + } catch (e: Throwable) { + logcat(LogPriority.ERROR, e) + val message = with(context) { e.formattedMessage } + + screenModelScope.launch { + snackbarHostState.showSnackbar(message = message) + } + val newManga = mangaRepository.getMangaById(mangaId) + updateSuccessState { it.copy(manga = newManga, isRefreshingData = false) } + } + } + // KMK <-- + /** * @throws IllegalStateException if the swipe action is [LibraryPreferences.ChapterSwipeAction.Disabled] */ diff --git a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/Source.kt b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/Source.kt index 83fcd7962..2fb6db2db 100644 --- a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/Source.kt +++ b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/Source.kt @@ -48,6 +48,27 @@ interface Source { return fetchChapterList(manga).awaitSingle() } + // KMK --> + /** + * Whether the source has support for related mangas. + * + * @since extensions-lib 1.6 + */ + val supportsRelatedMangas: Boolean get() = false + + /** + * Get all the available related mangas for a manga. + * + * @since extensions-lib 1.6 + * @param manga the current manga to get related mangas. + * @return the related mangas for the current manga. + */ + @Suppress("DEPRECATION") + suspend fun getRelatedMangaList(manga: SManga): List { + return fetchRelatedMangaList(manga).awaitSingle() + } + // KMK <-- + /** * Get the list of pages a chapter has. Pages should be returned * in the expected order; the index is ignored. @@ -75,6 +96,22 @@ interface Source { fun fetchChapterList(manga: SManga): Observable> = throw IllegalStateException("Not used") + // KMK --> + /** + * Get all the available related mangas for a manga. + * + * @since extensions-lib 1.6 + * @param manga the current manga to get related mangas. + * @return the related mangas for the current manga. + */ + @Deprecated( + "Use the non-RxJava API instead", + ReplaceWith("getRelatedMangaList"), + ) + fun fetchRelatedMangaList(manga: SManga): Observable> = + throw IllegalStateException("Not used") + // KMK <-- + @Deprecated( "Use the non-RxJava API instead", ReplaceWith("getPageList"), diff --git a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/HttpSource.kt b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/HttpSource.kt index 87d7a9491..2a98cf480 100644 --- a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/HttpSource.kt +++ b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/HttpSource.kt @@ -274,6 +274,54 @@ abstract class HttpSource : CatalogueSource { */ protected abstract fun mangaDetailsParse(response: Response): SManga + // KMK --> + /** + * Get all the available related mangas for a manga. + * Normally it's not needed to override this method. + * + * @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. + */ + @Suppress("DEPRECATION") + override suspend fun getRelatedMangaList(manga: SManga): List { + return fetchRelatedMangaList(manga).awaitSingle() + } + + @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getRelatedMangaList")) + override fun fetchRelatedMangaList(manga: SManga): Observable> { + return client.newCall(relatedMangaListRequest(manga)) + .asObservableSuccess() + .map { response -> + relatedMangaListParse(response) + } + } + + /** + * Returns the request for get related manga list. Override only if it's needed to override + * the url, send different headers or request method like POST. + * + * @param manga the manga to look for related mangas. + */ + protected open fun relatedMangaListRequest(manga: SManga): Request { + return GET(baseUrl + manga.url, headers) + } + + /** + * Parses the response from the site and returns a list of related mangas. + * + * @param response the response from the site. + */ + protected open fun relatedMangaListParse(response: Response): List = throw UnsupportedOperationException() + + /** + * Parses the response from the site and returns a SManga Object. + * + * @param response the response from the site. + */ + protected open fun relatedMangaPageParse(response: Response): SManga = throw UnsupportedOperationException() + // KMK <-- + /** * Get all the available chapters for a manga. * Normally it's not needed to override this method. diff --git a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/ParsedHttpSource.kt b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/ParsedHttpSource.kt index 34376f847..97f251a59 100644 --- a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/ParsedHttpSource.kt +++ b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/ParsedHttpSource.kt @@ -145,6 +145,30 @@ abstract class ParsedHttpSource : HttpSource() { */ protected abstract fun mangaDetailsParse(document: Document): SManga + // KMK --> + /** + * Parses the response from the site and returns a list of related mangas. + * + * @param response the response from the site. + */ + override fun relatedMangaListParse(response: Response): List { + val document = response.asJsoup() + return document.select(relatedMangaListSelector()).map { relatedMangaFromElement(it) } + } + + /** + * Returns the Jsoup selector that returns a list of [Element] corresponding to each related mangas. + */ + protected open fun relatedMangaListSelector(): String = popularMangaSelector() + + /** + * Returns a manga from the given element. + * + * @param element an element obtained from [relatedMangaListSelector]. + */ + protected open fun relatedMangaFromElement(element: Element): SManga = popularMangaFromElement(element) + // KMK <-- + /** * Parses the response from the site and returns a list of chapters. * From 0cf256cccebd4a82bb98ea57cd9a52077e5a2fd9 Mon Sep 17 00:00:00 2001 From: "Cuong M. Tran" Date: Thu, 25 Apr 2024 16:25:40 +0700 Subject: [PATCH 05/10] =?UTF-8?q?using=20search=20to=20enable=20all=20exte?= =?UTF-8?q?nsions=E2=80=99=20related=20mangas?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tachiyomi/ui/manga/MangaScreenModel.kt | 2 +- .../eu/kanade/tachiyomi/source/Source.kt | 7 -- .../tachiyomi/source/online/HttpSource.kt | 77 ++++++++++++++++--- .../source/online/ParsedHttpSource.kt | 7 +- 4 files changed, 74 insertions(+), 19 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt index 1ef329acc..6c65a8968 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt @@ -1109,7 +1109,7 @@ class MangaScreenModel( try { withIOContext { if (state.source !is MergedSource) { - val relatedMangas = if (state.source.supportsRelatedMangas) + val relatedMangas = if (true)//state.source.supportsRelatedMangas) withIOContext { state.source.getRelatedMangaList(state.manga.toSManga()) .map { diff --git a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/Source.kt b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/Source.kt index 2fb6db2db..a0b454b7f 100644 --- a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/Source.kt +++ b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/Source.kt @@ -49,13 +49,6 @@ interface Source { } // KMK --> - /** - * Whether the source has support for related mangas. - * - * @since extensions-lib 1.6 - */ - val supportsRelatedMangas: Boolean get() = false - /** * Get all the available related mangas for a manga. * diff --git a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/HttpSource.kt b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/HttpSource.kt index 2a98cf480..99270494d 100644 --- a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/HttpSource.kt +++ b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/HttpSource.kt @@ -16,6 +16,10 @@ 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 okhttp3.Headers import okhttp3.OkHttpClient import okhttp3.Request @@ -27,6 +31,7 @@ import uy.kohesive.injekt.api.get import java.net.URI import java.net.URISyntaxException import java.security.MessageDigest +import java.util.Locale /** * A simple implementation for sources from a website. @@ -275,6 +280,14 @@ abstract class HttpSource : CatalogueSource { protected abstract fun mangaDetailsParse(response: Response): SManga // 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 + * @since extensions-lib 1.6 + */ + protected open val supportsRelatedMangas: Boolean get() = false + /** * Get all the available related mangas for a manga. * Normally it's not needed to override this method. @@ -285,9 +298,60 @@ abstract class HttpSource : CatalogueSource { */ @Suppress("DEPRECATION") override suspend fun getRelatedMangaList(manga: SManga): List { - return fetchRelatedMangaList(manga).awaitSingle() + return if (supportsRelatedMangas) + fetchRelatedMangaList(manga).awaitSingle() + else + return fetchRelatedMangaListBySearch(manga) } + /** + * Fetch related mangas by searching for each keywords from manga's title + */ + protected open suspend fun fetchRelatedMangaListBySearch(manga: SManga): List { + fun String.stripKeyword(): List { + val regexWhitespace = Regex("\\s+") + // remove special character + return replace(Regex("[\\[(!~@#$%^&*|,?:\"<>)\\]]"), " ") + .split(regexWhitespace) + .map { + // remove number only + it.replace(Regex("^\\d+$"), "") + .lowercase(Locale.getDefault()) + } + // exclude single character + .filter { it.length > 1 } + } + val scope = CoroutineScope(Dispatchers.IO) + val words = HashSet() + words += manga.title.stripKeyword() + manga.originalTitle.let { title -> + words += title.stripKeyword() + } + if (words.isEmpty()) { + return emptyList() + } + val results = words.map { keyword -> + scope.async { + client.newCall(searchMangaRequest(0, keyword, FilterList())) + .execute() + .let { response -> + searchMangaParse(response).mangas + .filter { + it.url != manga.url && + it.title.lowercase().contains(keyword) + } + } + } + }.awaitAll() + .minBy { if (it.isEmpty()) Int.MAX_VALUE else it.size } + + return results + } + + /** + * Fetch related mangas found in manga's page which is provided by sites. + * If using this, must also: 'override val supportsRelatedMangas = true' + */ @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getRelatedMangaList")) override fun fetchRelatedMangaList(manga: SManga): Observable> { return client.newCall(relatedMangaListRequest(manga)) @@ -300,6 +364,7 @@ abstract class HttpSource : CatalogueSource { /** * Returns the request for get related manga list. Override only if it's needed to override * the url, send different headers or request method like POST. + * If using this, must also: 'override val supportsRelatedMangas = true' * * @param manga the manga to look for related mangas. */ @@ -309,17 +374,11 @@ abstract class HttpSource : CatalogueSource { /** * Parses the response from the site and returns a list of related mangas. + * If using this, must also: 'override val supportsRelatedMangas = true' * * @param response the response from the site. */ - protected open fun relatedMangaListParse(response: Response): List = throw UnsupportedOperationException() - - /** - * Parses the response from the site and returns a SManga Object. - * - * @param response the response from the site. - */ - protected open fun relatedMangaPageParse(response: Response): SManga = throw UnsupportedOperationException() + protected open fun relatedMangaListParse(response: Response): List = popularMangaParse(response).mangas // KMK <-- /** diff --git a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/ParsedHttpSource.kt b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/ParsedHttpSource.kt index 97f251a59..b87e60f58 100644 --- a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/ParsedHttpSource.kt +++ b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/ParsedHttpSource.kt @@ -148,21 +148,24 @@ abstract class ParsedHttpSource : HttpSource() { // KMK --> /** * Parses the response from the site and returns a list of related mangas. + * If using this, must also: 'override val supportsRelatedMangas = true' * * @param response the response from the site. */ override fun relatedMangaListParse(response: Response): List { - val document = response.asJsoup() - return document.select(relatedMangaListSelector()).map { relatedMangaFromElement(it) } + return response.asJsoup() + .select(relatedMangaListSelector()).map { relatedMangaFromElement(it) } } /** * Returns the Jsoup selector that returns a list of [Element] corresponding to each related mangas. + * If using this, must also: 'override val supportsRelatedMangas = true' */ protected open fun relatedMangaListSelector(): String = popularMangaSelector() /** * Returns a manga from the given element. + * If using this, must also: 'override val supportsRelatedMangas = true' * * @param element an element obtained from [relatedMangaListSelector]. */ From 28ebabffbc79d87d59d02eeca5ba6dfa497a6ddd Mon Sep 17 00:00:00 2001 From: "Cuong M. Tran" Date: Thu, 25 Apr 2024 16:59:50 +0700 Subject: [PATCH 06/10] allow extensions to disable related mangas & catching exception when using search for related mangas --- .../tachiyomi/source/online/HttpSource.kt | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/HttpSource.kt b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/HttpSource.kt index 99270494d..2e258f640 100644 --- a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/HttpSource.kt +++ b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/HttpSource.kt @@ -20,12 +20,14 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll +import logcat.LogPriority 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 @@ -287,6 +289,7 @@ abstract class HttpSource : CatalogueSource { * @since extensions-lib 1.6 */ protected open val supportsRelatedMangas: Boolean get() = false + protected open val disableRelatedMangas: Boolean get() = false /** * Get all the available related mangas for a manga. @@ -298,10 +301,18 @@ abstract class HttpSource : CatalogueSource { */ @Suppress("DEPRECATION") override suspend fun getRelatedMangaList(manga: SManga): List { - return if (supportsRelatedMangas) - fetchRelatedMangaList(manga).awaitSingle() - else - return fetchRelatedMangaListBySearch(manga) + return when { + supportsRelatedMangas -> fetchRelatedMangaList(manga).awaitSingle() + disableRelatedMangas -> emptyList() + else -> { + try { + fetchRelatedMangaListBySearch(manga) + } catch (e: Exception) { + logcat(LogPriority.ERROR, e) + throw UnsupportedOperationException("Error getting related titles.") + } + } + } } /** @@ -330,7 +341,8 @@ abstract class HttpSource : CatalogueSource { if (words.isEmpty()) { return emptyList() } - val results = words.map { keyword -> + + return words.map { keyword -> scope.async { client.newCall(searchMangaRequest(0, keyword, FilterList())) .execute() @@ -344,8 +356,6 @@ abstract class HttpSource : CatalogueSource { } }.awaitAll() .minBy { if (it.isEmpty()) Int.MAX_VALUE else it.size } - - return results } /** From 05aaacb7d1990194bcff02220fb89026f2524159 Mon Sep 17 00:00:00 2001 From: "Cuong M. Tran" Date: Thu, 25 Apr 2024 17:23:24 +0700 Subject: [PATCH 07/10] Preferences for enable/disable related mangas --- .../eu/kanade/domain/source/service/SourcePreferences.kt | 4 ++++ .../more/settings/screen/SettingsBrowseScreen.kt | 7 +++++++ .../java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt | 2 +- i18n-sy/src/commonMain/resources/MR/base/strings.xml | 2 ++ 4 files changed, 14 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/eu/kanade/domain/source/service/SourcePreferences.kt b/app/src/main/java/eu/kanade/domain/source/service/SourcePreferences.kt index 22e9ee05f..9c2413d96 100644 --- a/app/src/main/java/eu/kanade/domain/source/service/SourcePreferences.kt +++ b/app/src/main/java/eu/kanade/domain/source/service/SourcePreferences.kt @@ -82,4 +82,8 @@ class SourcePreferences( WSRV_NL, } // SY <-- + + // KMK --> + fun relatedMangas() = preferenceStore.getBoolean("related_mangas", false) + // KMK <-- } diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsBrowseScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsBrowseScreen.kt index 4deff07a2..303c02557 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsBrowseScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsBrowseScreen.kt @@ -52,6 +52,13 @@ object SettingsBrowseScreen : SearchableSettings { Preference.PreferenceGroup( title = stringResource(MR.strings.label_sources), preferenceItems = persistentListOf( + // KMK --> + Preference.PreferenceItem.SwitchPreference( + pref = sourcePreferences.relatedMangas(), + title = stringResource(SYMR.strings.pref_source_related_mangas), + subtitle = stringResource(SYMR.strings.pref_source_related_mangas_summary), + ), + // KMK <-- kotlin.run { val count by sourcePreferences.sourcesTabCategories().collectAsState() Preference.PreferenceItem.TextPreference( diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt index 6c65a8968..8c8ea5b38 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt @@ -1109,7 +1109,7 @@ class MangaScreenModel( try { withIOContext { if (state.source !is MergedSource) { - val relatedMangas = if (true)//state.source.supportsRelatedMangas) + val relatedMangas = if (Injekt.get().relatedMangas().get()) withIOContext { state.source.getRelatedMangaList(state.manga.toSManga()) .map { diff --git a/i18n-sy/src/commonMain/resources/MR/base/strings.xml b/i18n-sy/src/commonMain/resources/MR/base/strings.xml index 5dd3d79c5..dafa527aa 100644 --- a/i18n-sy/src/commonMain/resources/MR/base/strings.xml +++ b/i18n-sy/src/commonMain/resources/MR/base/strings.xml @@ -198,6 +198,8 @@ Replace latest button with a custom browse view that includes both latest and browse Local source hidden folders Allow local source to read hidden folders + Related mangas + Show related mangas provided by extensions Custom entry info From 89453116d7e8ee0b50e3f30ceb55241f51bba2e4 Mon Sep 17 00:00:00 2001 From: "Cuong M. Tran" Date: Thu, 25 Apr 2024 17:31:23 +0700 Subject: [PATCH 08/10] =?UTF-8?q?Manga=20won=E2=80=99t=20show=20Refreshing?= =?UTF-8?q?=20while=20waiting=20to=20show=20related=20titles?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt index 8c8ea5b38..18c0db420 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt @@ -396,9 +396,6 @@ class MangaScreenModel( val needRefreshInfo = !manga.initialized val needRefreshChapter = chapters.isEmpty() - // KMK --> - val needRefreshRelatedMangas = relatedMangas.isEmpty() - // KMK <-- // Show what we have earlier mutableState.update { @@ -416,7 +413,7 @@ class MangaScreenModel( }.toImmutableSet(), // SY <-- excludedScanlators = getExcludedScanlators.await(mangaId).toImmutableSet(), - isRefreshingData = needRefreshInfo || needRefreshChapter /* KMK --> */|| needRefreshRelatedMangas,/* KMK <-- */ + isRefreshingData = needRefreshInfo || needRefreshChapter, dialog = null, // SY --> showRecommendationsInOverflow = uiPreferences.recommendsInOverflow().get(), @@ -445,7 +442,7 @@ class MangaScreenModel( async { if (needRefreshInfo) fetchMangaFromSource() }, async { if (needRefreshChapter) fetchChaptersFromSource() }, // KMK --> - async { if (needRefreshRelatedMangas) fetchRelatedMangasFromSource() }, + async { fetchRelatedMangasFromSource() }, // KMK <-- ) fetchFromSourceTasks.awaitAll() @@ -1117,7 +1114,7 @@ class MangaScreenModel( } } else null - updateSuccessState { it.copy(relatedMangas = relatedMangas, isRefreshingData = false) } + updateSuccessState { it.copy(relatedMangas = relatedMangas) } } else { throw UnsupportedOperationException("Fetching related titles for merged entry is not supported") } @@ -1130,7 +1127,7 @@ class MangaScreenModel( snackbarHostState.showSnackbar(message = message) } val newManga = mangaRepository.getMangaById(mangaId) - updateSuccessState { it.copy(manga = newManga, isRefreshingData = false) } + updateSuccessState { it.copy(manga = newManga) } } } // KMK <-- From 6a496de18129571703ae49220959eaa9468a76bb Mon Sep 17 00:00:00 2001 From: "Cuong M. Tran" Date: Thu, 25 Apr 2024 17:31:34 +0700 Subject: [PATCH 09/10] Update README --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 877bb4c7b..60e60d979 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ Mihon/Tachiyomi is a free and open source manga reader for Android 6.0 and above Features of Komikku include: - [x] Built-in & official extensions repository +- [x] Show list of related titles (must enable in Settings/Browse) for all sources. Some sources might need fix for it to work. - [x] Bulk selection to add to library & change categories of multiple entries all at once, everywhere. It can detect duplication being added and give option to allow/skip one by one or allow/skip all duplication. Also allow long-click to add/remove single entry to/from library, everywhere. From 6353366e6383ad1f6bcd98b3aab482a8eb211f38 Mon Sep 17 00:00:00 2001 From: Cuong Tran Date: Fri, 26 Apr 2024 01:32:45 +0700 Subject: [PATCH 10/10] Move Regex out of loop --- .../eu/kanade/tachiyomi/source/online/HttpSource.kt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/HttpSource.kt b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/HttpSource.kt index 2e258f640..b5abba5b7 100644 --- a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/HttpSource.kt +++ b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/HttpSource.kt @@ -321,12 +321,14 @@ abstract class HttpSource : CatalogueSource { protected open suspend fun fetchRelatedMangaListBySearch(manga: SManga): List { fun String.stripKeyword(): List { val regexWhitespace = Regex("\\s+") - // remove special character - return replace(Regex("[\\[(!~@#$%^&*|,?:\"<>)\\]]"), " ") + val regexSpecialCharacters = Regex("[\\[(!~@#$%^&*|,?:\"<>)\\]]") + val regexNumberOnly = Regex("^\\d+$") + + return replace(regexSpecialCharacters, " ") .split(regexWhitespace) .map { // remove number only - it.replace(Regex("^\\d+$"), "") + it.replace(regexNumberOnly, "") .lowercase(Locale.getDefault()) } // exclude single character