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:
|
Features of Komikku include:
|
||||||
- [x] Built-in & official extensions repository
|
- [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.
|
- [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.
|
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.
|
Also allow long-click to add/remove single entry to/from library, everywhere.
|
||||||
|
|
|
||||||
|
|
@ -82,4 +82,8 @@ class SourcePreferences(
|
||||||
WSRV_NL,
|
WSRV_NL,
|
||||||
}
|
}
|
||||||
// SY <--
|
// 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.SnackbarHostState
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.State
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
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.fastAll
|
||||||
import androidx.compose.ui.util.fastAny
|
import androidx.compose.ui.util.fastAny
|
||||||
import androidx.compose.ui.util.fastMap
|
import androidx.compose.ui.util.fastMap
|
||||||
|
import eu.kanade.presentation.browse.components.GlobalSearchCardRow
|
||||||
|
import eu.kanade.presentation.browse.components.GlobalSearchResultItem
|
||||||
import eu.kanade.presentation.components.relativeDateText
|
import eu.kanade.presentation.components.relativeDateText
|
||||||
import eu.kanade.presentation.manga.components.ChapterDownloadAction
|
import eu.kanade.presentation.manga.components.ChapterDownloadAction
|
||||||
import eu.kanade.presentation.manga.components.ChapterHeader
|
import eu.kanade.presentation.manga.components.ChapterHeader
|
||||||
|
|
@ -167,6 +170,12 @@ fun MangaScreen(
|
||||||
onChapterSelected: (ChapterList.Item, Boolean, Boolean, Boolean) -> Unit,
|
onChapterSelected: (ChapterList.Item, Boolean, Boolean, Boolean) -> Unit,
|
||||||
onAllChapterSelected: (Boolean) -> Unit,
|
onAllChapterSelected: (Boolean) -> Unit,
|
||||||
onInvertSelection: () -> Unit,
|
onInvertSelection: () -> Unit,
|
||||||
|
|
||||||
|
// KMK -->
|
||||||
|
getMangaState: @Composable ((Manga) -> State<Manga>),
|
||||||
|
onRelatedMangaClick: (Manga) -> Unit,
|
||||||
|
onRelatedMangaLongClick: (Manga) -> Unit,
|
||||||
|
// KMK <--
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val onCopyTagToClipboard: (tag: String) -> Unit = {
|
val onCopyTagToClipboard: (tag: String) -> Unit = {
|
||||||
|
|
@ -220,6 +229,11 @@ fun MangaScreen(
|
||||||
onChapterSelected = onChapterSelected,
|
onChapterSelected = onChapterSelected,
|
||||||
onAllChapterSelected = onAllChapterSelected,
|
onAllChapterSelected = onAllChapterSelected,
|
||||||
onInvertSelection = onInvertSelection,
|
onInvertSelection = onInvertSelection,
|
||||||
|
// KMK -->
|
||||||
|
getMangaState = getMangaState,
|
||||||
|
onRelatedMangaClick = onRelatedMangaClick,
|
||||||
|
onRelatedMangaLongClick = onRelatedMangaLongClick,
|
||||||
|
// KMK <--
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
MangaScreenLargeImpl(
|
MangaScreenLargeImpl(
|
||||||
|
|
@ -266,6 +280,11 @@ fun MangaScreen(
|
||||||
onChapterSelected = onChapterSelected,
|
onChapterSelected = onChapterSelected,
|
||||||
onAllChapterSelected = onAllChapterSelected,
|
onAllChapterSelected = onAllChapterSelected,
|
||||||
onInvertSelection = onInvertSelection,
|
onInvertSelection = onInvertSelection,
|
||||||
|
// KMK -->
|
||||||
|
getMangaState = getMangaState,
|
||||||
|
onRelatedMangaClick = onRelatedMangaClick,
|
||||||
|
onRelatedMangaLongClick = onRelatedMangaLongClick,
|
||||||
|
// KMK <--
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -328,6 +347,12 @@ private fun MangaScreenSmallImpl(
|
||||||
onChapterSelected: (ChapterList.Item, Boolean, Boolean, Boolean) -> Unit,
|
onChapterSelected: (ChapterList.Item, Boolean, Boolean, Boolean) -> Unit,
|
||||||
onAllChapterSelected: (Boolean) -> Unit,
|
onAllChapterSelected: (Boolean) -> Unit,
|
||||||
onInvertSelection: () -> Unit,
|
onInvertSelection: () -> Unit,
|
||||||
|
|
||||||
|
// KMK -->
|
||||||
|
getMangaState: @Composable ((Manga) -> State<Manga>),
|
||||||
|
onRelatedMangaClick: (Manga) -> Unit,
|
||||||
|
onRelatedMangaLongClick: (Manga) -> Unit,
|
||||||
|
// KMK <--
|
||||||
) {
|
) {
|
||||||
val chapterListState = rememberLazyListState()
|
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 -->
|
// SY -->
|
||||||
if (!state.showRecommendationsInOverflow || state.showMergeWithAnother) {
|
if (!state.showRecommendationsInOverflow || state.showMergeWithAnother) {
|
||||||
item(
|
item(
|
||||||
|
|
@ -654,6 +702,12 @@ fun MangaScreenLargeImpl(
|
||||||
onChapterSelected: (ChapterList.Item, Boolean, Boolean, Boolean) -> Unit,
|
onChapterSelected: (ChapterList.Item, Boolean, Boolean, Boolean) -> Unit,
|
||||||
onAllChapterSelected: (Boolean) -> Unit,
|
onAllChapterSelected: (Boolean) -> Unit,
|
||||||
onInvertSelection: () -> Unit,
|
onInvertSelection: () -> Unit,
|
||||||
|
|
||||||
|
// KMK -->
|
||||||
|
getMangaState: @Composable ((Manga) -> State<Manga>),
|
||||||
|
onRelatedMangaClick: (Manga) -> Unit,
|
||||||
|
onRelatedMangaLongClick: (Manga) -> Unit,
|
||||||
|
// KMK <--
|
||||||
) {
|
) {
|
||||||
val layoutDirection = LocalLayoutDirection.current
|
val layoutDirection = LocalLayoutDirection.current
|
||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
|
|
@ -863,6 +917,29 @@ fun MangaScreenLargeImpl(
|
||||||
bottom = contentPadding.calculateBottomPadding(),
|
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(
|
item(
|
||||||
key = MangaScreenItem.CHAPTER_HEADER,
|
key = MangaScreenItem.CHAPTER_HEADER,
|
||||||
contentType = 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
|
@Composable
|
||||||
private fun SharedMangaBottomActionMenu(
|
private fun SharedMangaBottomActionMenu(
|
||||||
selected: List<ChapterList.Item>,
|
selected: List<ChapterList.Item>,
|
||||||
|
|
|
||||||
|
|
@ -32,4 +32,8 @@ enum class MangaScreenItem {
|
||||||
// SY <--
|
// SY <--
|
||||||
CHAPTER_HEADER,
|
CHAPTER_HEADER,
|
||||||
CHAPTER,
|
CHAPTER,
|
||||||
|
|
||||||
|
// KMK -->
|
||||||
|
RELATED_TITLES,
|
||||||
|
// KMK <--
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,13 @@ object SettingsBrowseScreen : SearchableSettings {
|
||||||
Preference.PreferenceGroup(
|
Preference.PreferenceGroup(
|
||||||
title = stringResource(MR.strings.label_sources),
|
title = stringResource(MR.strings.label_sources),
|
||||||
preferenceItems = persistentListOf(
|
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 {
|
kotlin.run {
|
||||||
val count by sourcePreferences.sourcesTabCategories().collectAsState()
|
val count by sourcePreferences.sourcesTabCategories().collectAsState()
|
||||||
Preference.PreferenceItem.TextPreference(
|
Preference.PreferenceItem.TextPreference(
|
||||||
|
|
|
||||||
|
|
@ -252,7 +252,7 @@ class BulkFavoriteScreenModel(
|
||||||
.orEmpty()
|
.orEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getDuplicateLibraryManga(manga: Manga): Manga? {
|
private suspend fun getDuplicateLibraryManga(manga: Manga): Manga? {
|
||||||
return getDuplicateLibraryManga.await(manga).getOrNull(0)
|
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.Source
|
||||||
import eu.kanade.tachiyomi.source.isLocalOrStub
|
import eu.kanade.tachiyomi.source.isLocalOrStub
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
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.migration.advanced.design.PreMigrationScreen
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.SourcesScreen
|
import eu.kanade.tachiyomi.ui.browse.source.SourcesScreen
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreen
|
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreen
|
||||||
|
|
@ -121,6 +127,11 @@ class MangaScreen(
|
||||||
val successState = state as MangaScreenModel.State.Success
|
val successState = state as MangaScreenModel.State.Success
|
||||||
val isHttpSource = remember { successState.source is HttpSource }
|
val isHttpSource = remember { successState.source is HttpSource }
|
||||||
|
|
||||||
|
// KMK -->
|
||||||
|
val bulkFavoriteScreenModel = rememberScreenModel { BulkFavoriteScreenModel() }
|
||||||
|
val bulkFavoriteState by bulkFavoriteScreenModel.state.collectAsState()
|
||||||
|
// KMK <--
|
||||||
|
|
||||||
LaunchedEffect(successState.manga, screenModel.source) {
|
LaunchedEffect(successState.manga, screenModel.source) {
|
||||||
if (isHttpSource) {
|
if (isHttpSource) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -225,6 +236,11 @@ class MangaScreen(
|
||||||
onChapterSelected = screenModel::toggleSelection,
|
onChapterSelected = screenModel::toggleSelection,
|
||||||
onAllChapterSelected = screenModel::toggleAllSelection,
|
onAllChapterSelected = screenModel::toggleAllSelection,
|
||||||
onInvertSelection = screenModel::invertSelection,
|
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) }
|
var showScanlatorsDialog by remember { mutableStateOf(false) }
|
||||||
|
|
@ -342,6 +358,22 @@ class MangaScreen(
|
||||||
onConfirm = screenModel::setExcludedScanlators,
|
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?) {
|
private fun continueReading(context: Context, unreadChapter: Chapter?) {
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,10 @@ package eu.kanade.tachiyomi.ui.manga
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.material3.SnackbarResult
|
import androidx.compose.material3.SnackbarResult
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.produceState
|
||||||
import androidx.compose.ui.util.fastAny
|
import androidx.compose.ui.util.fastAny
|
||||||
import cafe.adriel.voyager.core.model.StateScreenModel
|
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||||
import cafe.adriel.voyager.core.model.screenModelScope
|
import cafe.adriel.voyager.core.model.screenModelScope
|
||||||
|
|
@ -22,6 +24,7 @@ import eu.kanade.domain.manga.model.PagePreview
|
||||||
import eu.kanade.domain.manga.model.chaptersFiltered
|
import eu.kanade.domain.manga.model.chaptersFiltered
|
||||||
import eu.kanade.domain.manga.model.copyFrom
|
import eu.kanade.domain.manga.model.copyFrom
|
||||||
import eu.kanade.domain.manga.model.downloadedFilter
|
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.manga.model.toSManga
|
||||||
import eu.kanade.domain.source.service.SourcePreferences
|
import eu.kanade.domain.source.service.SourcePreferences
|
||||||
import eu.kanade.domain.track.interactor.AddTracks
|
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.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.distinctUntilChangedBy
|
import kotlinx.coroutines.flow.distinctUntilChangedBy
|
||||||
import kotlinx.coroutines.flow.filter
|
import kotlinx.coroutines.flow.filter
|
||||||
|
import kotlinx.coroutines.flow.filterNotNull
|
||||||
import kotlinx.coroutines.flow.flatMapConcat
|
import kotlinx.coroutines.flow.flatMapConcat
|
||||||
import kotlinx.coroutines.flow.flowOf
|
import kotlinx.coroutines.flow.flowOf
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
|
@ -135,6 +139,7 @@ import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import kotlin.math.floor
|
import kotlin.math.floor
|
||||||
|
import androidx.compose.runtime.State as RuntimeState
|
||||||
|
|
||||||
class MangaScreenModel(
|
class MangaScreenModel(
|
||||||
val context: Context,
|
val context: Context,
|
||||||
|
|
@ -204,6 +209,11 @@ class MangaScreenModel(
|
||||||
private val filteredChapters: List<ChapterList.Item>?
|
private val filteredChapters: List<ChapterList.Item>?
|
||||||
get() = successState?.processedChapters
|
get() = successState?.processedChapters
|
||||||
|
|
||||||
|
// KMK -->
|
||||||
|
private val relatedMangas: List<Manga>
|
||||||
|
get() = successState?.relatedMangas ?: emptyList()
|
||||||
|
// KMK <--
|
||||||
|
|
||||||
val chapterSwipeStartAction = libraryPreferences.swipeToEndAction().get()
|
val chapterSwipeStartAction = libraryPreferences.swipeToEndAction().get()
|
||||||
val chapterSwipeEndAction = libraryPreferences.swipeToStartAction().get()
|
val chapterSwipeEndAction = libraryPreferences.swipeToStartAction().get()
|
||||||
|
|
||||||
|
|
@ -431,6 +441,9 @@ class MangaScreenModel(
|
||||||
val fetchFromSourceTasks = listOf(
|
val fetchFromSourceTasks = listOf(
|
||||||
async { if (needRefreshInfo) fetchMangaFromSource() },
|
async { if (needRefreshInfo) fetchMangaFromSource() },
|
||||||
async { if (needRefreshChapter) fetchChaptersFromSource() },
|
async { if (needRefreshChapter) fetchChaptersFromSource() },
|
||||||
|
// KMK -->
|
||||||
|
async { fetchRelatedMangasFromSource() },
|
||||||
|
// KMK <--
|
||||||
)
|
)
|
||||||
fetchFromSourceTasks.awaitAll()
|
fetchFromSourceTasks.awaitAll()
|
||||||
}
|
}
|
||||||
|
|
@ -446,6 +459,9 @@ class MangaScreenModel(
|
||||||
val fetchFromSourceTasks = listOf(
|
val fetchFromSourceTasks = listOf(
|
||||||
async { fetchMangaFromSource(manualFetch) },
|
async { fetchMangaFromSource(manualFetch) },
|
||||||
async { fetchChaptersFromSource(manualFetch) },
|
async { fetchChaptersFromSource(manualFetch) },
|
||||||
|
// KMK -->
|
||||||
|
async { fetchRelatedMangasFromSource() },
|
||||||
|
// KMK <--
|
||||||
)
|
)
|
||||||
fetchFromSourceTasks.awaitAll()
|
fetchFromSourceTasks.awaitAll()
|
||||||
updateSuccessState { it.copy(isRefreshingData = false) }
|
updateSuccessState { it.copy(isRefreshingData = false) }
|
||||||
|
|
@ -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 {
|
suspend fun smartSearchMerge(manga: Manga, originalMangaId: Long): Manga {
|
||||||
val originalManga = getManga.await(originalMangaId)
|
val originalManga = getManga.await(originalMangaId)
|
||||||
?: throw IllegalArgumentException(context.stringResource(SYMR.strings.merge_unknown_entry, 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]
|
* @throws IllegalStateException if the swipe action is [LibraryPreferences.ChapterSwipeAction.Disabled]
|
||||||
*/
|
*/
|
||||||
|
|
@ -1637,6 +1701,9 @@ class MangaScreenModel(
|
||||||
val alwaysShowReadingProgress: Boolean,
|
val alwaysShowReadingProgress: Boolean,
|
||||||
val previewsRowCount: Int,
|
val previewsRowCount: Int,
|
||||||
// SY <--
|
// SY <--
|
||||||
|
// KMK -->
|
||||||
|
val relatedMangas: List<Manga>? = null,
|
||||||
|
// KMK <--
|
||||||
) : State {
|
) : State {
|
||||||
val processedChapters by lazy {
|
val processedChapters by lazy {
|
||||||
chapters.applyFilters(manga).toList()
|
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_source_navigation_summery">Replace latest button with a custom browse view that includes both latest and browse</string>
|
||||||
<string name="pref_local_source_hidden_folders">Local source hidden folders</string>
|
<string name="pref_local_source_hidden_folders">Local source hidden folders</string>
|
||||||
<string name="pref_local_source_hidden_folders_summery">Allow local source to read hidden folders</string>
|
<string name="pref_local_source_hidden_folders_summery">Allow local source to read hidden folders</string>
|
||||||
|
<string name="pref_source_related_mangas">Related mangas</string>
|
||||||
|
<string name="pref_source_related_mangas_summary">Show related mangas provided by extensions</string>
|
||||||
|
|
||||||
<!-- Backup settings -->
|
<!-- Backup settings -->
|
||||||
<string name="custom_entry_info">Custom entry info</string>
|
<string name="custom_entry_info">Custom entry info</string>
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,20 @@ interface Source {
|
||||||
return fetchChapterList(manga).awaitSingle()
|
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
|
* Get the list of pages a chapter has. Pages should be returned
|
||||||
* in the expected order; the index is ignored.
|
* in the expected order; the index is ignored.
|
||||||
|
|
@ -75,6 +89,22 @@ interface Source {
|
||||||
fun fetchChapterList(manga: SManga): Observable<List<SChapter>> =
|
fun fetchChapterList(manga: SManga): Observable<List<SChapter>> =
|
||||||
throw IllegalStateException("Not used")
|
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(
|
@Deprecated(
|
||||||
"Use the non-RxJava API instead",
|
"Use the non-RxJava API instead",
|
||||||
ReplaceWith("getPageList"),
|
ReplaceWith("getPageList"),
|
||||||
|
|
|
||||||
|
|
@ -16,17 +16,24 @@ import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import exh.log.maybeInjectEHLogger
|
import exh.log.maybeInjectEHLogger
|
||||||
import exh.pref.DelegateSourcePreferences
|
import exh.pref.DelegateSourcePreferences
|
||||||
import exh.source.DelegatedHttpSource
|
import exh.source.DelegatedHttpSource
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.awaitAll
|
||||||
|
import logcat.LogPriority
|
||||||
import okhttp3.Headers
|
import okhttp3.Headers
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import tachiyomi.core.common.util.lang.awaitSingle
|
import tachiyomi.core.common.util.lang.awaitSingle
|
||||||
|
import tachiyomi.core.common.util.system.logcat
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
import java.net.URISyntaxException
|
import java.net.URISyntaxException
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A simple implementation for sources from a website.
|
* A simple implementation for sources from a website.
|
||||||
|
|
@ -274,6 +281,118 @@ abstract class HttpSource : CatalogueSource {
|
||||||
*/
|
*/
|
||||||
protected abstract fun mangaDetailsParse(response: Response): SManga
|
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.
|
* Get all the available chapters for a manga.
|
||||||
* Normally it's not needed to override this method.
|
* Normally it's not needed to override this method.
|
||||||
|
|
|
||||||
|
|
@ -145,6 +145,33 @@ abstract class ParsedHttpSource : HttpSource() {
|
||||||
*/
|
*/
|
||||||
protected abstract fun mangaDetailsParse(document: Document): SManga
|
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.
|
* Parses the response from the site and returns a list of chapters.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue