Merge branch 'feature/related-mangas'
This commit is contained in:
commit
af4e58a789
12 changed files with 389 additions and 1 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -82,4 +82,8 @@ class SourcePreferences(
|
|||
WSRV_NL,
|
||||
}
|
||||
// SY <--
|
||||
|
||||
// KMK -->
|
||||
fun relatedMangas() = preferenceStore.getBoolean("related_mangas", false)
|
||||
// KMK <--
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Manga>),
|
||||
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<Manga>),
|
||||
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<Manga>),
|
||||
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<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>,
|
||||
|
|
|
|||
|
|
@ -32,4 +32,8 @@ enum class MangaScreenItem {
|
|||
// SY <--
|
||||
CHAPTER_HEADER,
|
||||
CHAPTER,
|
||||
|
||||
// KMK -->
|
||||
RELATED_TITLES,
|
||||
// KMK <--
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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?) {
|
||||
|
|
|
|||
|
|
@ -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<ChapterList.Item>?
|
||||
get() = successState?.processedChapters
|
||||
|
||||
// KMK -->
|
||||
private val relatedMangas: List<Manga>
|
||||
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<Manga> {
|
||||
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<SourcePreferences>().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<Manga>? = null,
|
||||
// KMK <--
|
||||
) : State {
|
||||
val processedChapters by lazy {
|
||||
chapters.applyFilters(manga).toList()
|
||||
|
|
|
|||
|
|
@ -198,6 +198,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>
|
||||
|
||||
<!-- Backup settings -->
|
||||
<string name="custom_entry_info">Custom entry info</string>
|
||||
|
|
|
|||
|
|
@ -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<SManga> {
|
||||
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<List<SChapter>> =
|
||||
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<List<SManga>> =
|
||||
throw IllegalStateException("Not used")
|
||||
// KMK <--
|
||||
|
||||
@Deprecated(
|
||||
"Use the non-RxJava API instead",
|
||||
ReplaceWith("getPageList"),
|
||||
|
|
|
|||
|
|
@ -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<SManga> {
|
||||
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<SManga> {
|
||||
fun String.stripKeyword(): List<String> {
|
||||
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<String>()
|
||||
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<List<SManga>> {
|
||||
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<SManga> = popularMangaParse(response).mangas
|
||||
// KMK <--
|
||||
|
||||
/**
|
||||
* Get all the available chapters for a manga.
|
||||
* Normally it's not needed to override this method.
|
||||
|
|
|
|||
|
|
@ -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<SManga> {
|
||||
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.
|
||||
*
|
||||
|
|
|
|||
Loading…
Reference in a new issue