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. 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/manga/MangaScreen.kt b/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt index 0f1cc335b..4a9ac700b 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.relatedMangas?.isNotEmpty() == true) { + 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.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.relatedMangas?.isNotEmpty() == true) { + 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.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/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/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 b4bfba757..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 { @@ -225,6 +236,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 = { bulkFavoriteScreenModel.addRemoveManga(it, haptic) }, + // KMK <-- ) var showScanlatorsDialog by remember { mutableStateOf(false) } @@ -342,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?) { 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..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 @@ -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 @@ -22,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 @@ -72,6 +75,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 +139,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, @@ -204,6 +209,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() @@ -431,6 +441,9 @@ class MangaScreenModel( val fetchFromSourceTasks = listOf( async { if (needRefreshInfo) fetchMangaFromSource() }, async { if (needRefreshChapter) fetchChaptersFromSource() }, + // KMK --> + async { fetchRelatedMangasFromSource() }, + // KMK <-- ) fetchFromSourceTasks.awaitAll() } @@ -446,6 +459,9 @@ class MangaScreenModel( val fetchFromSourceTasks = listOf( async { fetchMangaFromSource(manualFetch) }, async { fetchChaptersFromSource(manualFetch) }, + // KMK --> + async { fetchRelatedMangasFromSource() }, + // KMK <-- ) fetchFromSourceTasks.awaitAll() updateSuccessState { it.copy(isRefreshingData = false) } @@ -553,6 +569,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)) @@ -1068,6 +1097,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 (Injekt.get().relatedMangas().get()) + withIOContext { + state.source.getRelatedMangaList(state.manga.toSManga()) + .map { + networkToLocalManga.await(it.toDomainManga(state.source.id)) + } + } + else null + updateSuccessState { it.copy(relatedMangas = relatedMangas) } + } 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) } + } + } + // KMK <-- + /** * @throws IllegalStateException if the swipe action is [LibraryPreferences.ChapterSwipeAction.Disabled] */ @@ -1637,6 +1701,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/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 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..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 @@ -48,6 +48,20 @@ interface Source { return fetchChapterList(manga).awaitSingle() } + // 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. + */ + @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 +89,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..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 @@ -16,17 +16,24 @@ 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 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 import java.net.URISyntaxException import java.security.MessageDigest +import java.util.Locale /** * A simple implementation for sources from a website. @@ -274,6 +281,118 @@ 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 + 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. + * + * @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 when { + supportsRelatedMangas -> fetchRelatedMangaList(manga).awaitSingle() + disableRelatedMangas -> emptyList() + else -> { + try { + fetchRelatedMangaListBySearch(manga) + } catch (e: Exception) { + logcat(LogPriority.ERROR, e) + throw UnsupportedOperationException("Error getting related titles.") + } + } + } + } + + /** + * 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+") + val regexSpecialCharacters = Regex("[\\[(!~@#$%^&*|,?:\"<>)\\]]") + val regexNumberOnly = Regex("^\\d+$") + + return replace(regexSpecialCharacters, " ") + .split(regexWhitespace) + .map { + // remove number only + it.replace(regexNumberOnly, "") + .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() + } + + return 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 } + } + + /** + * 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)) + .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. + * If using this, must also: 'override val supportsRelatedMangas = true' + * + * @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. + * If using this, must also: 'override val supportsRelatedMangas = true' + * + * @param response the response from the site. + */ + protected open fun relatedMangaListParse(response: Response): List = popularMangaParse(response).mangas + // 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..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 @@ -145,6 +145,33 @@ 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. + * If using this, must also: 'override val supportsRelatedMangas = true' + * + * @param response the response from the site. + */ + override fun relatedMangaListParse(response: Response): List { + 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]. + */ + protected open fun relatedMangaFromElement(element: Element): SManga = popularMangaFromElement(element) + // KMK <-- + /** * Parses the response from the site and returns a list of chapters. *