chore(network-local-manga): Reintroduce (in-memory) fast browsing (#909)

* measure time

* Revert "Revert "Fast browsing (#612)""

This reverts commit d23130b317.

* Fix networkToLocal for bulk-selection selectAll/reverseSelection

* fix comments
This commit is contained in:
Cuong-Tran 2025-05-13 23:18:01 +07:00 committed by GitHub
parent 7dd57c54bc
commit 3310784faf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 431 additions and 170 deletions

View file

@ -6,6 +6,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.State import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import eu.kanade.domain.source.model.installedExtension import eu.kanade.domain.source.model.installedExtension
import eu.kanade.presentation.browse.components.GlobalSearchCardRow import eu.kanade.presentation.browse.components.GlobalSearchCardRow
@ -21,6 +22,7 @@ import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchScreenModel
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SourceFilter import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SourceFilter
import eu.kanade.tachiyomi.util.system.LocaleHelper import eu.kanade.tachiyomi.util.system.LocaleHelper
import kotlinx.collections.immutable.ImmutableMap import kotlinx.collections.immutable.ImmutableMap
import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.domain.source.model.Source as DomainSource import tachiyomi.domain.source.model.Source as DomainSource
@ -43,6 +45,7 @@ fun GlobalSearchScreen(
// KMK <-- // KMK <--
) { ) {
// KMK --> // KMK -->
val scope = rememberCoroutineScope()
val bulkFavoriteState by bulkFavoriteScreenModel.state.collectAsState() val bulkFavoriteState by bulkFavoriteScreenModel.state.collectAsState()
// KMK <-- // KMK <--
@ -59,13 +62,24 @@ fun GlobalSearchScreen(
state.filteredItems.values state.filteredItems.values
.filterIsInstance<SearchItemResult.Success>() .filterIsInstance<SearchItemResult.Success>()
.flatMap { it.result } .flatMap { it.result }
.forEach { bulkFavoriteScreenModel.select(it) } .let {
scope.launchIO {
bulkFavoriteScreenModel.networkToLocalManga.getLocal(it)
.forEach { bulkFavoriteScreenModel.select(it) }
}
}
}, },
onReverseSelection = { onReverseSelection = {
state.filteredItems.values state.filteredItems.values
.filterIsInstance<SearchItemResult.Success>() .filterIsInstance<SearchItemResult.Success>()
.flatMap { it.result } .flatMap { it.result }
.let { bulkFavoriteScreenModel.reverseSelection(it) } .let {
scope.launchIO {
bulkFavoriteScreenModel.reverseSelection(
bulkFavoriteScreenModel.networkToLocalManga.getLocal(it),
)
}
}
}, },
) )
} else { } else {

View file

@ -4,6 +4,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.State import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
import eu.kanade.presentation.browse.components.GlobalSearchToolbar import eu.kanade.presentation.browse.components.GlobalSearchToolbar
import eu.kanade.presentation.components.BulkSelectionToolbar import eu.kanade.presentation.components.BulkSelectionToolbar
import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.CatalogueSource
@ -11,6 +12,7 @@ import eu.kanade.tachiyomi.ui.browse.BulkFavoriteScreenModel
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchItemResult import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchItemResult
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchScreenModel import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchScreenModel
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SourceFilter import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SourceFilter
import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.components.material.Scaffold
@ -33,6 +35,7 @@ fun MigrateSearchScreen(
// KMK <-- // KMK <--
) { ) {
// KMK --> // KMK -->
val scope = rememberCoroutineScope()
val bulkFavoriteState by bulkFavoriteScreenModel.state.collectAsState() val bulkFavoriteState by bulkFavoriteScreenModel.state.collectAsState()
// KMK <-- // KMK <--
@ -49,13 +52,24 @@ fun MigrateSearchScreen(
state.filteredItems.values state.filteredItems.values
.filterIsInstance<SearchItemResult.Success>() .filterIsInstance<SearchItemResult.Success>()
.flatMap { it.result } .flatMap { it.result }
.forEach { bulkFavoriteScreenModel.select(it) } .let {
scope.launchIO {
bulkFavoriteScreenModel.networkToLocalManga.getLocal(it)
.forEach { bulkFavoriteScreenModel.select(it) }
}
}
}, },
onReverseSelection = { onReverseSelection = {
state.filteredItems.values state.filteredItems.values
.filterIsInstance<SearchItemResult.Success>() .filterIsInstance<SearchItemResult.Success>()
.flatMap { it.result } .flatMap { it.result }
.let { bulkFavoriteScreenModel.reverseSelection(it) } .let {
scope.launchIO {
bulkFavoriteScreenModel.reverseSelection(
bulkFavoriteScreenModel.networkToLocalManga.getLocal(it),
)
}
}
}, },
) )
} else { } else {

View file

@ -10,6 +10,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.State import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import dev.icerock.moko.resources.StringResource import dev.icerock.moko.resources.StringResource
import eu.kanade.presentation.browse.components.BrowseSourceFloatingActionButton import eu.kanade.presentation.browse.components.BrowseSourceFloatingActionButton
@ -26,6 +27,7 @@ import eu.kanade.presentation.components.SearchToolbar
import eu.kanade.tachiyomi.ui.browse.BulkFavoriteScreenModel import eu.kanade.tachiyomi.ui.browse.BulkFavoriteScreenModel
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.source.model.FeedSavedSearch import tachiyomi.domain.source.model.FeedSavedSearch
import tachiyomi.domain.source.model.SavedSearch import tachiyomi.domain.source.model.SavedSearch
@ -112,6 +114,7 @@ fun SourceFeedScreen(
// KMK <-- // KMK <--
) { ) {
// KMK --> // KMK -->
val scope = rememberCoroutineScope()
val bulkFavoriteState by bulkFavoriteScreenModel.state.collectAsState() val bulkFavoriteState by bulkFavoriteScreenModel.state.collectAsState()
// KMK <-- // KMK <--
@ -127,12 +130,23 @@ fun SourceFeedScreen(
onSelectAll = { onSelectAll = {
items.mapNotNull { it.results } items.mapNotNull { it.results }
.flatten() .flatten()
.forEach { bulkFavoriteScreenModel.select(it) } .let {
scope.launchIO {
bulkFavoriteScreenModel.networkToLocalManga.getLocal(it)
.forEach { bulkFavoriteScreenModel.select(it) }
}
}
}, },
onReverseSelection = { onReverseSelection = {
items.mapNotNull { it.results } items.mapNotNull { it.results }
.flatten() .flatten()
.let { bulkFavoriteScreenModel.reverseSelection(it) } .let {
scope.launchIO {
bulkFavoriteScreenModel.reverseSelection(
bulkFavoriteScreenModel.networkToLocalManga.getLocal(it),
)
}
}
}, },
) )
} else { } else {

View file

@ -80,7 +80,7 @@ fun RelatedMangasComfortableGrid(
) )
} }
items( items(
key = { "related-comfort-${relatedManga.mangaList[it].id}" }, key = { "related-comfort-${relatedManga.mangaList[it].url.hashCode()}" },
count = relatedManga.mangaList.size, count = relatedManga.mangaList.size,
) { index -> ) { index ->
val manga by getManga(relatedManga.mangaList[index]) val manga by getManga(relatedManga.mangaList[index])

View file

@ -79,7 +79,7 @@ fun RelatedMangasCompactGrid(
) )
} }
items( items(
key = { "related-compact-${relatedManga.mangaList[it].id}" }, key = { "related-compact-${relatedManga.mangaList[it].url.hashCode()}" },
count = relatedManga.mangaList.size, count = relatedManga.mangaList.size,
) { index -> ) { index ->
val manga by getManga(relatedManga.mangaList[index]) val manga by getManga(relatedManga.mangaList[index])

View file

@ -82,7 +82,7 @@ fun RelatedMangasList(
) )
} }
items( items(
key = { "related-list-${relatedManga.mangaList[it].id}" }, key = { "related-list-${relatedManga.mangaList[it].url.hashCode()}" },
count = relatedManga.mangaList.size, count = relatedManga.mangaList.size,
) { index -> ) { index ->
val manga by getManga(relatedManga.mangaList[index]) val manga by getManga(relatedManga.mangaList[index])

View file

@ -29,6 +29,7 @@ import eu.kanade.tachiyomi.ui.browse.feed.FeedScreenModel
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.components.material.TabText import tachiyomi.presentation.core.components.material.TabText
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
@ -68,14 +69,25 @@ fun TabbedScreen(
feedState.items?.let { result -> feedState.items?.let { result ->
result.mapNotNull { it.results } result.mapNotNull { it.results }
.flatten() .flatten()
.forEach { bulkFavoriteScreenModel.select(it) } .let {
scope.launchIO {
bulkFavoriteScreenModel.networkToLocalManga.getLocal(it)
.forEach { bulkFavoriteScreenModel.select(it) }
}
}
} }
}, },
onReverseSelection = { onReverseSelection = {
feedState.items?.let { result -> feedState.items?.let { result ->
result.mapNotNull { it.results } result.mapNotNull { it.results }
.flatten() .flatten()
.let { bulkFavoriteScreenModel.reverseSelection(it) } .let {
scope.launchIO {
bulkFavoriteScreenModel.reverseSelection(
bulkFavoriteScreenModel.networkToLocalManga.getLocal(it),
)
}
}
} }
}, },
) )

View file

@ -66,7 +66,7 @@ fun RelatedMangaCardRow(
contentPadding = PaddingValues(MaterialTheme.padding.small), contentPadding = PaddingValues(MaterialTheme.padding.small),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall), horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
) { ) {
items(mangas, key = { "related-row-${it.id}" }) { items(mangas, key = { "related-row-${it.url.hashCode()}" }) {
val manga by getManga(it) val manga by getManga(it)
MangaItem( MangaItem(
title = manga.title, title = manga.title,

View file

@ -34,6 +34,7 @@ import tachiyomi.domain.category.model.Category
import tachiyomi.domain.chapter.interactor.SetMangaDefaultChapterFlags import tachiyomi.domain.chapter.interactor.SetMangaDefaultChapterFlags
import tachiyomi.domain.library.service.LibraryPreferences import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.domain.manga.interactor.GetDuplicateLibraryManga import tachiyomi.domain.manga.interactor.GetDuplicateLibraryManga
import tachiyomi.domain.manga.interactor.NetworkToLocalManga
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.manga.model.MangaWithChapterCount import tachiyomi.domain.manga.model.MangaWithChapterCount
import tachiyomi.domain.manga.model.toMangaUpdate import tachiyomi.domain.manga.model.toMangaUpdate
@ -53,6 +54,10 @@ class BulkFavoriteScreenModel(
private val coverCache: CoverCache = Injekt.get(), private val coverCache: CoverCache = Injekt.get(),
private val setMangaDefaultChapterFlags: SetMangaDefaultChapterFlags = Injekt.get(), private val setMangaDefaultChapterFlags: SetMangaDefaultChapterFlags = Injekt.get(),
private val addTracks: AddTracks = Injekt.get(), private val addTracks: AddTracks = Injekt.get(),
// KMK -->
val networkToLocalManga: NetworkToLocalManga = Injekt.get(),
// KMK <--
) : StateScreenModel<BulkFavoriteScreenModel.State>(initialState) { ) : StateScreenModel<BulkFavoriteScreenModel.State>(initialState) {
fun backHandler() { fun backHandler() {

View file

@ -60,7 +60,7 @@ open class FeedScreenModel(
val sourceManager: SourceManager = Injekt.get(), val sourceManager: SourceManager = Injekt.get(),
val sourcePreferences: SourcePreferences = Injekt.get(), val sourcePreferences: SourcePreferences = Injekt.get(),
private val getManga: GetManga = Injekt.get(), private val getManga: GetManga = Injekt.get(),
private val networkToLocalManga: NetworkToLocalManga = Injekt.get(), val networkToLocalManga: NetworkToLocalManga = Injekt.get(),
getFeedSavedSearchGlobal: GetFeedSavedSearchGlobal = Injekt.get(), getFeedSavedSearchGlobal: GetFeedSavedSearchGlobal = Injekt.get(),
private val getSavedSearchGlobalFeed: GetSavedSearchGlobalFeed = Injekt.get(), private val getSavedSearchGlobalFeed: GetSavedSearchGlobalFeed = Injekt.get(),
private val countFeedSavedSearchGlobal: CountFeedSavedSearchGlobal = Injekt.get(), private val countFeedSavedSearchGlobal: CountFeedSavedSearchGlobal = Injekt.get(),
@ -304,8 +304,8 @@ open class FeedScreenModel(
results = page results = page
.map { it.toDomainManga(itemUI.source!!.id) } .map { it.toDomainManga(itemUI.source!!.id) }
.distinctBy { it.url } .distinctBy { it.url }
.let { networkToLocalManga(it) }
// KMK --> // KMK -->
// .let { networkToLocalManga(it) }
.filter { !hideInLibraryFeedItems.get() || !it.favorite }, .filter { !hideInLibraryFeedItems.get() || !it.favorite },
// KMK <-- // KMK <--
) )
@ -343,8 +343,11 @@ open class FeedScreenModel(
return produceState(initialValue = initialManga) { return produceState(initialValue = initialManga) {
getManga.subscribe(initialManga.url, initialManga.source) getManga.subscribe(initialManga.url, initialManga.source)
.collectLatest { manga -> .collectLatest { manga ->
if (manga == null) return@collectLatest /* KMK --> if (manga == null) return@collectLatest KMK <-- */
value = manga value = manga
// KMK -->
?: initialManga
// KMK <--
} }
} }
} }

View file

@ -13,6 +13,7 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalHapticFeedback
import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.core.screen.Screen
@ -36,6 +37,7 @@ import eu.kanade.tachiyomi.ui.manga.MangaScreen
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.domain.source.interactor.GetRemoteManga import tachiyomi.domain.source.interactor.GetRemoteManga
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.i18n.kmk.KMR import tachiyomi.i18n.kmk.KMR
@ -53,6 +55,7 @@ fun Screen.feedTab(
val state by screenModel.state.collectAsState() val state by screenModel.state.collectAsState()
// KMK --> // KMK -->
val scope = rememberCoroutineScope()
val bulkFavoriteState by bulkFavoriteScreenModel.state.collectAsState() val bulkFavoriteState by bulkFavoriteScreenModel.state.collectAsState()
val showingFeedOrderScreen = rememberSaveable { mutableStateOf(false) } val showingFeedOrderScreen = rememberSaveable { mutableStateOf(false) }
@ -165,21 +168,27 @@ fun Screen.feedTab(
// KMK --> // KMK -->
onLongClickFeed = screenModel::openActionsDialog, onLongClickFeed = screenModel::openActionsDialog,
// KMK <-- // KMK <--
onClickManga = { manga -> onClickManga = {
// KMK --> // KMK -->
if (bulkFavoriteState.selectionMode) { scope.launchIO {
bulkFavoriteScreenModel.toggleSelection(manga) val manga = screenModel.networkToLocalManga.getLocal(it)
} else { if (bulkFavoriteState.selectionMode) {
// KMK <-- bulkFavoriteScreenModel.toggleSelection(manga)
navigator.push(MangaScreen(manga.id, true)) } else {
// KMK <--
navigator.push(MangaScreen(manga.id, true))
}
} }
}, },
// KMK --> // KMK -->
onLongClickManga = { manga -> onLongClickManga = {
if (!bulkFavoriteState.selectionMode) { scope.launchIO {
bulkFavoriteScreenModel.addRemoveManga(manga, haptic) val manga = screenModel.networkToLocalManga.getLocal(it)
} else { if (!bulkFavoriteState.selectionMode) {
navigator.push(MangaScreen(manga.id, true)) bulkFavoriteScreenModel.addRemoveManga(manga, haptic)
} else {
navigator.push(MangaScreen(manga.id, true))
}
} }
}, },
selection = bulkFavoriteState.selection, selection = bulkFavoriteState.selection,

View file

@ -4,6 +4,7 @@ import androidx.activity.compose.BackHandler
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
import cafe.adriel.voyager.core.model.rememberScreenModel import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.currentOrThrow
@ -13,6 +14,7 @@ import eu.kanade.presentation.util.Screen
import eu.kanade.tachiyomi.ui.browse.BulkFavoriteScreenModel import eu.kanade.tachiyomi.ui.browse.BulkFavoriteScreenModel
import eu.kanade.tachiyomi.ui.browse.migration.advanced.process.MigrationListScreen import eu.kanade.tachiyomi.ui.browse.migration.advanced.process.MigrationListScreen
import eu.kanade.tachiyomi.ui.manga.MangaScreen import eu.kanade.tachiyomi.ui.manga.MangaScreen
import tachiyomi.core.common.util.lang.launchIO
/** /**
* Manual search [validSources] for manga to migrate to. * Manual search [validSources] for manga to migrate to.
@ -30,6 +32,7 @@ class MigrateSearchScreen(private val mangaId: Long, private val validSources: L
val dialogState by dialogScreenModel.state.collectAsState() val dialogState by dialogScreenModel.state.collectAsState()
// KMK --> // KMK -->
val scope = rememberCoroutineScope()
val bulkFavoriteScreenModel = rememberScreenModel { BulkFavoriteScreenModel() } val bulkFavoriteScreenModel = rememberScreenModel { BulkFavoriteScreenModel() }
val bulkFavoriteState by bulkFavoriteScreenModel.state.collectAsState() val bulkFavoriteState by bulkFavoriteScreenModel.state.collectAsState()
@ -52,23 +55,33 @@ class MigrateSearchScreen(private val mangaId: Long, private val validSources: L
navigator.push(SourceSearchScreen(dialogState.manga!!, it.id, state.searchQuery)) navigator.push(SourceSearchScreen(dialogState.manga!!, it.id, state.searchQuery))
// SY <-- // SY <--
}, },
onClickItem = { manga -> onClickItem = {
// KMK --> // KMK -->
if (bulkFavoriteState.selectionMode) { scope.launchIO {
bulkFavoriteScreenModel.toggleSelection(manga) val manga = screenModel.networkToLocalManga.getLocal(it)
} else if (bulkFavoriteState.selectionMode) {
// KMK <-- bulkFavoriteScreenModel.toggleSelection(manga)
{ } else
// SY --> // KMK <--
navigator.items {
.filterIsInstance<MigrationListScreen>() // SY -->
.last() navigator.items
.newSelectedItem = mangaId to manga.id .filterIsInstance<MigrationListScreen>()
navigator.popUntil { it is MigrationListScreen } .last()
// SY <-- .newSelectedItem = mangaId to manga.id
} navigator.popUntil { it is MigrationListScreen }
// SY <--
}
}
},
onLongClickItem = {
// KMK -->
scope.launchIO {
val manga = screenModel.networkToLocalManga.getLocal(it)
// KMK <--
navigator.push(MangaScreen(manga.id, true))
}
}, },
onLongClickItem = { navigator.push(MangaScreen(it.id, true)) },
// KMK --> // KMK -->
bulkFavoriteScreenModel = bulkFavoriteScreenModel, bulkFavoriteScreenModel = bulkFavoriteScreenModel,
hasPinnedSources = screenModel.hasPinnedSources(), hasPinnedSources = screenModel.hasPinnedSources(),

View file

@ -7,6 +7,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.platform.LocalUriHandler
@ -33,6 +34,7 @@ import eu.kanade.tachiyomi.util.system.toast
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import mihon.presentation.core.util.collectAsLazyPagingItems import mihon.presentation.core.util.collectAsLazyPagingItems
import tachiyomi.core.common.Constants import tachiyomi.core.common.Constants
import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import tachiyomi.i18n.sy.SYMR import tachiyomi.i18n.sy.SYMR
import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.components.material.Scaffold
@ -66,6 +68,7 @@ data class SourceSearchScreen(
// KMK --> // KMK -->
val context = LocalContext.current val context = LocalContext.current
val scope = rememberCoroutineScope()
val bulkFavoriteScreenModel = rememberScreenModel { BulkFavoriteScreenModel() } val bulkFavoriteScreenModel = rememberScreenModel { BulkFavoriteScreenModel() }
val bulkFavoriteState by bulkFavoriteScreenModel.state.collectAsState() val bulkFavoriteState by bulkFavoriteScreenModel.state.collectAsState()
@ -89,12 +92,23 @@ data class SourceSearchScreen(
onSelectAll = { onSelectAll = {
mangaList.itemSnapshotList.items mangaList.itemSnapshotList.items
.map { it.value.first } .map { it.value.first }
.forEach { bulkFavoriteScreenModel.select(it) } .let {
scope.launchIO {
bulkFavoriteScreenModel.networkToLocalManga.getLocal(it)
.forEach { bulkFavoriteScreenModel.select(it) }
}
}
}, },
onReverseSelection = { onReverseSelection = {
mangaList.itemSnapshotList.items mangaList.itemSnapshotList.items
.map { it.value.first } .map { it.value.first }
.let { bulkFavoriteScreenModel.reverseSelection(it) } .let {
scope.launchIO {
bulkFavoriteScreenModel.reverseSelection(
bulkFavoriteScreenModel.networkToLocalManga.getLocal(it),
)
}
}
}, },
) )
} else { } else {
@ -161,16 +175,26 @@ data class SourceSearchScreen(
}, },
onHelpClick = { uriHandler.openUri(Constants.URL_HELP) }, onHelpClick = { uriHandler.openUri(Constants.URL_HELP) },
onLocalSourceHelpClick = { uriHandler.openUri(LocalSource.HELP_URL) }, onLocalSourceHelpClick = { uriHandler.openUri(LocalSource.HELP_URL) },
onMangaClick = { manga -> onMangaClick = {
// KMK --> // KMK -->
if (bulkFavoriteState.selectionMode) { scope.launchIO {
bulkFavoriteScreenModel.toggleSelection(manga) val manga = screenModel.networkToLocalManga.getLocal(it)
} else { if (bulkFavoriteState.selectionMode) {
// KMK <-- bulkFavoriteScreenModel.toggleSelection(manga)
openMigrateDialog(manga) } else {
// KMK <--
openMigrateDialog(manga)
}
}
},
onMangaLongClick = {
// KMK -->
scope.launchIO {
val manga = screenModel.networkToLocalManga.getLocal(it)
// KMK <--
navigator.push(MangaScreen(manga.id, true))
} }
}, },
onMangaLongClick = { navigator.push(MangaScreen(it.id, true)) },
// KMK --> // KMK -->
selection = bulkFavoriteState.selection, selection = bulkFavoriteState.selection,
// KMK <-- // KMK <--

View file

@ -202,12 +202,23 @@ data class BrowseSourceScreen(
onSelectAll = { onSelectAll = {
mangaList.itemSnapshotList.items mangaList.itemSnapshotList.items
.map { it.value.first } .map { it.value.first }
.forEach { bulkFavoriteScreenModel.select(it) } .let {
scope.launchIO {
bulkFavoriteScreenModel.networkToLocalManga.getLocal(it)
.forEach { bulkFavoriteScreenModel.select(it) }
}
}
}, },
onReverseSelection = { onReverseSelection = {
mangaList.itemSnapshotList.items mangaList.itemSnapshotList.items
.map { it.value.first } .map { it.value.first }
.let { bulkFavoriteScreenModel.reverseSelection(it) } .let {
scope.launchIO {
bulkFavoriteScreenModel.reverseSelection(
bulkFavoriteScreenModel.networkToLocalManga.getLocal(it),
)
}
}
}, },
) )
} else { } else {
@ -356,32 +367,36 @@ data class BrowseSourceScreen(
onWebViewClick = onWebViewClick, onWebViewClick = onWebViewClick,
onHelpClick = { uriHandler.openUri(Constants.URL_HELP) }, onHelpClick = { uriHandler.openUri(Constants.URL_HELP) },
onLocalSourceHelpClick = onHelpClick, onLocalSourceHelpClick = onHelpClick,
onMangaClick = { manga -> onMangaClick = {
// KMK --> // KMK -->
if (bulkFavoriteState.selectionMode) { scope.launchIO {
bulkFavoriteScreenModel.toggleSelection(manga) val manga = screenModel.networkToLocalManga.getLocal(it)
} else { if (bulkFavoriteState.selectionMode) {
// KMK <-- bulkFavoriteScreenModel.toggleSelection(manga)
navigator.push( } else {
MangaScreen( // KMK <--
mangaId = manga.id, navigator.push(
// KMK --> MangaScreen(
// Finding the entry to be merged to, so we don't want to expand description mangaId = manga.id,
// so that user can see the `Merge to another` button // KMK -->
fromSource = smartSearchConfig == null, // Finding the entry to be merged to, so we don't want to expand description
// KMK <-- // so that user can see the `Merge to another` button
smartSearchConfig = smartSearchConfig, fromSource = smartSearchConfig == null,
), // KMK <--
) smartSearchConfig = smartSearchConfig,
),
)
}
} }
}, },
onMangaLongClick = { manga -> onMangaLongClick = {
// KMK --> // KMK -->
if (bulkFavoriteState.selectionMode) { scope.launchIO {
navigator.push(MangaScreen(manga.id, true)) val manga = screenModel.networkToLocalManga.getLocal(it)
} else { if (bulkFavoriteState.selectionMode) {
// KMK <-- navigator.push(MangaScreen(manga.id, true))
scope.launchIO { } else {
// KMK <--
val duplicates = screenModel.getDuplicateLibraryManga(manga) val duplicates = screenModel.getDuplicateLibraryManga(manga)
when { when {
manga.favorite -> screenModel.setDialog(BrowseSourceScreenModel.Dialog.RemoveManga(manga)) manga.favorite -> screenModel.setDialog(BrowseSourceScreenModel.Dialog.RemoveManga(manga))

View file

@ -70,6 +70,7 @@ import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.domain.manga.interactor.GetDuplicateLibraryManga import tachiyomi.domain.manga.interactor.GetDuplicateLibraryManga
import tachiyomi.domain.manga.interactor.GetFlatMetadataById import tachiyomi.domain.manga.interactor.GetFlatMetadataById
import tachiyomi.domain.manga.interactor.GetManga import tachiyomi.domain.manga.interactor.GetManga
import tachiyomi.domain.manga.interactor.NetworkToLocalManga
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.manga.model.MangaWithChapterCount import tachiyomi.domain.manga.model.MangaWithChapterCount
import tachiyomi.domain.manga.model.toMangaUpdate import tachiyomi.domain.manga.model.toMangaUpdate
@ -114,6 +115,9 @@ open class BrowseSourceScreenModel(
private val extensionManager: ExtensionManager = Injekt.get(), private val extensionManager: ExtensionManager = Injekt.get(),
// KMK <-- // KMK <--
// KMK -->
val networkToLocalManga: NetworkToLocalManga = Injekt.get(),
// KMK <--
// SY --> // SY -->
unsortedPreferences: UnsortedPreferences = Injekt.get(), unsortedPreferences: UnsortedPreferences = Injekt.get(),
uiPreferences: UiPreferences = Injekt.get(), uiPreferences: UiPreferences = Injekt.get(),

View file

@ -7,6 +7,7 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalHapticFeedback
@ -39,6 +40,7 @@ import exh.md.follows.MangaDexFollowsScreen
import exh.source.anyIs import exh.source.anyIs
import exh.source.isEhBasedSource import exh.source.isEhBasedSource
import exh.util.nullIfBlank import exh.util.nullIfBlank
import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.domain.UnsortedPreferences import tachiyomi.domain.UnsortedPreferences
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.source.interactor.GetRemoteManga import tachiyomi.domain.source.interactor.GetRemoteManga
@ -65,6 +67,7 @@ class SourceFeedScreen(val sourceId: Long) : Screen() {
val context = LocalContext.current val context = LocalContext.current
// KMK --> // KMK -->
val scope = rememberCoroutineScope()
screenModel.source.let { screenModel.source.let {
if (it is StubSource) { if (it is StubSource) {
MissingSourceScreen( MissingSourceScreen(
@ -123,13 +126,16 @@ class SourceFeedScreen(val sourceId: Long) : Screen() {
// onClickDelete = screenModel::openDeleteFeed, // onClickDelete = screenModel::openDeleteFeed,
onLongClickFeed = screenModel::openActionsDialog, onLongClickFeed = screenModel::openActionsDialog,
// KMK <-- // KMK <--
onClickManga = { manga -> onClickManga = {
// KMK --> // KMK -->
if (bulkFavoriteState.selectionMode) { scope.launchIO {
bulkFavoriteScreenModel.toggleSelection(manga) val manga = screenModel.networkToLocalManga.getLocal(it)
} else { if (bulkFavoriteState.selectionMode) {
// KMK <-- bulkFavoriteScreenModel.toggleSelection(manga)
onMangaClick(navigator, manga) } else {
// KMK <--
onMangaClick(navigator, manga)
}
} }
}, },
onClickSearch = { onSearchClick(navigator, screenModel.source, it) }, onClickSearch = { onSearchClick(navigator, screenModel.source, it) },
@ -164,11 +170,14 @@ class SourceFeedScreen(val sourceId: Long) : Screen() {
.filterIsInstance<SourceFeedUI.SourceSavedSearch>() .filterIsInstance<SourceFeedUI.SourceSavedSearch>()
.isNotEmpty() .isNotEmpty()
}, },
onLongClickManga = { manga -> onLongClickManga = {
if (!bulkFavoriteState.selectionMode) { scope.launchIO {
bulkFavoriteScreenModel.addRemoveManga(manga, haptic) val manga = screenModel.networkToLocalManga.getLocal(it)
} else { if (!bulkFavoriteState.selectionMode) {
navigator.push(MangaScreen(manga.id, true)) bulkFavoriteScreenModel.addRemoveManga(manga, haptic)
} else {
navigator.push(MangaScreen(manga.id, true))
}
} }
}, },
bulkFavoriteScreenModel = bulkFavoriteScreenModel, bulkFavoriteScreenModel = bulkFavoriteScreenModel,

View file

@ -73,7 +73,7 @@ open class SourceFeedScreenModel(
uiPreferences: UiPreferences = Injekt.get(), uiPreferences: UiPreferences = Injekt.get(),
private val sourceManager: SourceManager = Injekt.get(), private val sourceManager: SourceManager = Injekt.get(),
private val getManga: GetManga = Injekt.get(), private val getManga: GetManga = Injekt.get(),
private val networkToLocalManga: NetworkToLocalManga = Injekt.get(), val networkToLocalManga: NetworkToLocalManga = Injekt.get(),
private val getFeedSavedSearchBySourceId: GetFeedSavedSearchBySourceId = Injekt.get(), private val getFeedSavedSearchBySourceId: GetFeedSavedSearchBySourceId = Injekt.get(),
private val getSavedSearchBySourceIdFeed: GetSavedSearchBySourceIdFeed = Injekt.get(), private val getSavedSearchBySourceIdFeed: GetSavedSearchBySourceIdFeed = Injekt.get(),
private val countFeedSavedSearchBySourceId: CountFeedSavedSearchBySourceId = Injekt.get(), private val countFeedSavedSearchBySourceId: CountFeedSavedSearchBySourceId = Injekt.get(),
@ -250,7 +250,7 @@ open class SourceFeedScreenModel(
val titles = withIOContext { val titles = withIOContext {
page.map { it.toDomainManga(source.id) } page.map { it.toDomainManga(source.id) }
.distinctBy { it.url } .distinctBy { it.url }
.let { networkToLocalManga(it) } /* KMK --> .let { networkToLocalManga(it) } KMK <-- */
} }
mutableState.update { state -> mutableState.update { state ->
@ -284,8 +284,11 @@ open class SourceFeedScreenModel(
return produceState(initialValue = initialManga) { return produceState(initialValue = initialManga) {
getManga.subscribe(initialManga.url, initialManga.source) getManga.subscribe(initialManga.url, initialManga.source)
.collectLatest { manga -> .collectLatest { manga ->
if (manga == null) return@collectLatest /* KMK --> if (manga == null) return@collectLatest KMK <-- */
value = manga value = manga
// KMK -->
?: initialManga
// KMK <--
} }
} }
} }

View file

@ -7,6 +7,7 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalHapticFeedback
import cafe.adriel.voyager.core.model.rememberScreenModel import cafe.adriel.voyager.core.model.rememberScreenModel
@ -19,6 +20,7 @@ import eu.kanade.presentation.util.Screen
import eu.kanade.tachiyomi.ui.browse.BulkFavoriteScreenModel import eu.kanade.tachiyomi.ui.browse.BulkFavoriteScreenModel
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreen import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreen
import eu.kanade.tachiyomi.ui.manga.MangaScreen import eu.kanade.tachiyomi.ui.manga.MangaScreen
import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.presentation.core.screens.LoadingScreen import tachiyomi.presentation.core.screens.LoadingScreen
class GlobalSearchScreen( class GlobalSearchScreen(
@ -47,6 +49,7 @@ class GlobalSearchScreen(
} }
// KMK --> // KMK -->
val scope = rememberCoroutineScope()
val bulkFavoriteScreenModel = rememberScreenModel { BulkFavoriteScreenModel() } val bulkFavoriteScreenModel = rememberScreenModel { BulkFavoriteScreenModel() }
val bulkFavoriteState by bulkFavoriteScreenModel.state.collectAsState() val bulkFavoriteState by bulkFavoriteScreenModel.state.collectAsState()
@ -66,7 +69,12 @@ class GlobalSearchScreen(
is SearchItemResult.Success -> { is SearchItemResult.Success -> {
val manga = result.result.singleOrNull() val manga = result.result.singleOrNull()
if (manga != null) { if (manga != null) {
navigator.replace(MangaScreen(manga.id, true)) // KMK -->
scope.launchIO {
val localManga = screenModel.networkToLocalManga.getLocal(manga)
// KMK <--
navigator.replace(MangaScreen(localManga.id, true))
}
} else { } else {
// Backoff to result screen // Backoff to result screen
showSingleLoadingScreen = false showSingleLoadingScreen = false
@ -87,22 +95,28 @@ class GlobalSearchScreen(
onClickSource = { onClickSource = {
navigator.push(BrowseSourceScreen(it.id, state.searchQuery)) navigator.push(BrowseSourceScreen(it.id, state.searchQuery))
}, },
onClickItem = { manga -> onClickItem = {
// KMK --> // KMK -->
if (bulkFavoriteState.selectionMode) { scope.launchIO {
bulkFavoriteScreenModel.toggleSelection(manga) val manga = screenModel.networkToLocalManga.getLocal(it)
} else { if (bulkFavoriteState.selectionMode) {
// KMK <-- bulkFavoriteScreenModel.toggleSelection(manga)
navigator.push(MangaScreen(manga.id, true)) } else {
// KMK <--
navigator.push(MangaScreen(manga.id, true))
}
} }
}, },
onLongClickItem = { manga -> onLongClickItem = {
// KMK --> // KMK -->
if (!bulkFavoriteState.selectionMode) { scope.launchIO {
bulkFavoriteScreenModel.addRemoveManga(manga, haptic) val manga = screenModel.networkToLocalManga.getLocal(it)
} else { if (!bulkFavoriteState.selectionMode) {
// KMK <-- bulkFavoriteScreenModel.addRemoveManga(manga, haptic)
navigator.push(MangaScreen(manga.id, true)) } else {
// KMK <--
navigator.push(MangaScreen(manga.id, true))
}
} }
}, },
// KMK --> // KMK -->

View file

@ -19,7 +19,7 @@ import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -39,7 +39,7 @@ abstract class SearchScreenModel(
sourcePreferences: SourcePreferences = Injekt.get(), sourcePreferences: SourcePreferences = Injekt.get(),
private val sourceManager: SourceManager = Injekt.get(), private val sourceManager: SourceManager = Injekt.get(),
private val extensionManager: ExtensionManager = Injekt.get(), private val extensionManager: ExtensionManager = Injekt.get(),
private val networkToLocalManga: NetworkToLocalManga = Injekt.get(), val networkToLocalManga: NetworkToLocalManga = Injekt.get(),
private val getManga: GetManga = Injekt.get(), private val getManga: GetManga = Injekt.get(),
private val preferences: SourcePreferences = Injekt.get(), private val preferences: SourcePreferences = Injekt.get(),
) : StateScreenModel<SearchScreenModel.State>(initialState) { ) : StateScreenModel<SearchScreenModel.State>(initialState) {
@ -83,9 +83,12 @@ abstract class SearchScreenModel(
fun getManga(initialManga: Manga): androidx.compose.runtime.State<Manga> { fun getManga(initialManga: Manga): androidx.compose.runtime.State<Manga> {
return produceState(initialValue = initialManga) { return produceState(initialValue = initialManga) {
getManga.subscribe(initialManga.url, initialManga.source) getManga.subscribe(initialManga.url, initialManga.source)
.filterNotNull() /* KMK --> .filterNotNull() KMK <-- */
.collectLatest { manga -> .collectLatest { manga ->
value = manga value = manga
// KMK -->
?: initialManga
// KMK <--
} }
} }
} }
@ -189,7 +192,7 @@ abstract class SearchScreenModel(
val titles = page.mangas val titles = page.mangas
.map { it.toDomainManga(source.id) } .map { it.toDomainManga(source.id) }
.distinctBy { it.url } .distinctBy { it.url }
.let { networkToLocalManga(it) } /* KMK --> .let { networkToLocalManga(it) } KMK <-- */
if (isActive) { if (isActive) {
updateItem(source, SearchItemResult.Success(titles)) updateItem(source, SearchItemResult.Success(titles))

View file

@ -389,8 +389,18 @@ class MangaScreen(
} }
showRelatedMangasScreen() showRelatedMangasScreen()
}, },
onRelatedMangaClick = { navigator.push(MangaScreen(it.id, true)) }, onRelatedMangaClick = {
onRelatedMangaLongClick = { bulkFavoriteScreenModel.addRemoveManga(it, haptic) }, scope.launchIO {
val manga = screenModel.networkToLocalManga.getLocal(it)
navigator.push(MangaScreen(manga.id, true))
}
},
onRelatedMangaLongClick = {
scope.launchIO {
val manga = screenModel.networkToLocalManga.getLocal(it)
bulkFavoriteScreenModel.addRemoveManga(manga, haptic)
}
},
onSourceClick = { onSourceClick = {
if (successState.source !is StubSource) { if (successState.source !is StubSource) {
val screen = when { val screen = when {

View file

@ -94,7 +94,6 @@ 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
@ -195,7 +194,7 @@ class MangaScreenModel(
private val smartSearchMerge: SmartSearchMerge = Injekt.get(), private val smartSearchMerge: SmartSearchMerge = Injekt.get(),
// KMK <-- // KMK <--
private val updateMergedSettings: UpdateMergedSettings = Injekt.get(), private val updateMergedSettings: UpdateMergedSettings = Injekt.get(),
private val networkToLocalManga: NetworkToLocalManga = Injekt.get(), val networkToLocalManga: NetworkToLocalManga = Injekt.get(),
private val deleteMergeById: DeleteMergeById = Injekt.get(), private val deleteMergeById: DeleteMergeById = Injekt.get(),
private val getFlatMetadata: GetFlatMetadataById = Injekt.get(), private val getFlatMetadata: GetFlatMetadataById = Injekt.get(),
private val getPagePreviews: GetPagePreviews = Injekt.get(), private val getPagePreviews: GetPagePreviews = Injekt.get(),
@ -705,10 +704,13 @@ class MangaScreenModel(
fun getManga(initialManga: Manga): RuntimeState<Manga> { fun getManga(initialManga: Manga): RuntimeState<Manga> {
return produceState(initialValue = initialManga) { return produceState(initialValue = initialManga) {
getManga.subscribe(initialManga.url, initialManga.source) getManga.subscribe(initialManga.url, initialManga.source)
.filterNotNull() /* KMK --> .filterNotNull() KMK <-- */
.flowWithLifecycle(lifecycle) .flowWithLifecycle(lifecycle)
.collectLatest { manga -> .collectLatest { manga ->
value = manga value = manga
// KMK -->
?: initialManga
// KMK <--
} }
} }
} }
@ -1155,7 +1157,7 @@ class MangaScreenModel(
mangaList mangaList
.map { it.toDomainManga(state.source.id) } .map { it.toDomainManga(state.source.id) }
.distinctBy { it.url } .distinctBy { it.url }
.let { networkToLocalManga(it) } /* KMK --> .let { networkToLocalManga(it) } KMK <-- */
} }
updateSuccessState { successState -> updateSuccessState { successState ->
@ -2008,14 +2010,14 @@ sealed interface RelatedManga {
} }
internal fun List<RelatedManga>.removeDuplicates(manga: Manga): List<RelatedManga> { internal fun List<RelatedManga>.removeDuplicates(manga: Manga): List<RelatedManga> {
val mangaIds = HashSet<Long>().apply { add(manga.id) } val mangaHashes = HashSet<Int>().apply { add(manga.url.hashCode()) }
return map { relatedManga -> return map { relatedManga ->
if (relatedManga is Success) { if (relatedManga is Success) {
Success( Success(
relatedManga.keyword, relatedManga.keyword,
relatedManga.mangaList relatedManga.mangaList
.filter { mangaIds.add(it.id) }, .filter { mangaHashes.add(it.url.hashCode()) },
) )
} else { } else {
relatedManga relatedManga

View file

@ -22,6 +22,7 @@ import eu.kanade.tachiyomi.ui.browse.BulkFavoriteScreenModel
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreen import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreen
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchScreen import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchScreen
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.domain.library.service.LibraryPreferences import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.components.material.Scaffold
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
@ -57,14 +58,25 @@ fun RelatedMangasScreen(
successState.relatedMangasSorted?.let { result -> successState.relatedMangasSorted?.let { result ->
result.map { it as RelatedManga.Success } result.map { it as RelatedManga.Success }
.flatMap { it.mangaList } .flatMap { it.mangaList }
.forEach { bulkFavoriteScreenModel.select(it) } .let {
scope.launchIO {
bulkFavoriteScreenModel.networkToLocalManga.getLocal(it)
.forEach { bulkFavoriteScreenModel.select(it) }
}
}
} }
}, },
onReverseSelection = { onReverseSelection = {
successState.relatedMangasSorted?.let { result -> successState.relatedMangasSorted?.let { result ->
result.map { it as RelatedManga.Success } result.map { it as RelatedManga.Success }
.flatMap { it.mangaList } .flatMap { it.mangaList }
.let { bulkFavoriteScreenModel.reverseSelection(it) } .let {
scope.launchIO {
bulkFavoriteScreenModel.reverseSelection(
bulkFavoriteScreenModel.networkToLocalManga.getLocal(it),
)
}
}
} }
}, },
) )
@ -88,18 +100,24 @@ fun RelatedMangasScreen(
columns = getColumnsPreference(LocalConfiguration.current.orientation), columns = getColumnsPreference(LocalConfiguration.current.orientation),
displayMode = displayMode, displayMode = displayMode,
contentPadding = paddingValues, contentPadding = paddingValues,
onMangaClick = { manga -> onMangaClick = {
if (bulkFavoriteState.selectionMode) { scope.launchIO {
bulkFavoriteScreenModel.toggleSelection(manga) val manga = screenModel.networkToLocalManga.getLocal(it)
} else { if (bulkFavoriteState.selectionMode) {
navigator.push(MangaScreen(manga.id, true)) bulkFavoriteScreenModel.toggleSelection(manga)
} else {
navigator.push(MangaScreen(manga.id, true))
}
} }
}, },
onMangaLongClick = { manga -> onMangaLongClick = {
if (!bulkFavoriteState.selectionMode) { scope.launchIO {
bulkFavoriteScreenModel.addRemoveManga(manga, haptic) val manga = screenModel.networkToLocalManga.getLocal(it)
} else { if (!bulkFavoriteState.selectionMode) {
navigator.push(MangaScreen(manga.id, true)) bulkFavoriteScreenModel.addRemoveManga(manga, haptic)
} else {
navigator.push(MangaScreen(manga.id, true))
}
} }
}, },
onKeywordClick = { query -> onKeywordClick = { query ->

View file

@ -77,12 +77,23 @@ class MangaDexFollowsScreen(private val sourceId: Long) : Screen() {
onSelectAll = { onSelectAll = {
mangaList.itemSnapshotList.items mangaList.itemSnapshotList.items
.map { it.value.first } .map { it.value.first }
.forEach { bulkFavoriteScreenModel.select(it) } .let {
scope.launchIO {
bulkFavoriteScreenModel.networkToLocalManga.getLocal(it)
.forEach { bulkFavoriteScreenModel.select(it) }
}
}
}, },
onReverseSelection = { onReverseSelection = {
mangaList.itemSnapshotList.items mangaList.itemSnapshotList.items
.map { it.value.first } .map { it.value.first }
.let { bulkFavoriteScreenModel.reverseSelection(it) } .let {
scope.launchIO {
bulkFavoriteScreenModel.reverseSelection(
bulkFavoriteScreenModel.networkToLocalManga.getLocal(it),
)
}
}
}, },
) )
} else { } else {
@ -117,22 +128,26 @@ class MangaDexFollowsScreen(private val sourceId: Long) : Screen() {
onWebViewClick = null, onWebViewClick = null,
onHelpClick = null, onHelpClick = null,
onLocalSourceHelpClick = null, onLocalSourceHelpClick = null,
onMangaClick = { manga -> onMangaClick = {
// KMK --> // KMK -->
if (bulkFavoriteState.selectionMode) { scope.launchIO {
bulkFavoriteScreenModel.toggleSelection(manga) val manga = screenModel.networkToLocalManga.getLocal(it)
} else { if (bulkFavoriteState.selectionMode) {
// KMK <-- bulkFavoriteScreenModel.toggleSelection(manga)
navigator.push(MangaScreen(manga.id, true)) } else {
// KMK <--
navigator.push(MangaScreen(manga.id, true))
}
} }
}, },
onMangaLongClick = { manga -> onMangaLongClick = {
// KMK --> // KMK -->
if (bulkFavoriteState.selectionMode) { scope.launchIO {
navigator.push(MangaScreen(manga.id, true)) val manga = screenModel.networkToLocalManga.getLocal(it)
} else { if (bulkFavoriteState.selectionMode) {
// KMK <-- navigator.push(MangaScreen(manga.id, true))
scope.launchIO { } else {
// KMK <--
val duplicates = screenModel.getDuplicateLibraryManga(manga) val duplicates = screenModel.getDuplicateLibraryManga(manga)
when { when {
manga.favorite -> screenModel.setDialog(BrowseSourceScreenModel.Dialog.RemoveManga(manga)) manga.favorite -> screenModel.setDialog(BrowseSourceScreenModel.Dialog.RemoveManga(manga))

View file

@ -7,6 +7,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalHapticFeedback
@ -24,6 +25,7 @@ import eu.kanade.tachiyomi.ui.browse.source.SourcesScreen
import eu.kanade.tachiyomi.ui.manga.MangaScreen import eu.kanade.tachiyomi.ui.manga.MangaScreen
import eu.kanade.tachiyomi.ui.webview.WebViewActivity import eu.kanade.tachiyomi.ui.webview.WebViewActivity
import mihon.presentation.core.util.collectAsLazyPagingItems import mihon.presentation.core.util.collectAsLazyPagingItems
import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.screens.LoadingScreen import tachiyomi.presentation.core.screens.LoadingScreen
@ -50,6 +52,7 @@ class BrowseRecommendsScreen(
} }
// KMK --> // KMK -->
val scope = rememberCoroutineScope()
val bulkFavoriteScreenModel = rememberScreenModel { BulkFavoriteScreenModel() } val bulkFavoriteScreenModel = rememberScreenModel { BulkFavoriteScreenModel() }
val bulkFavoriteState by bulkFavoriteScreenModel.state.collectAsState() val bulkFavoriteState by bulkFavoriteScreenModel.state.collectAsState()
@ -94,12 +97,23 @@ class BrowseRecommendsScreen(
onSelectAll = { onSelectAll = {
mangaList.itemSnapshotList.items mangaList.itemSnapshotList.items
.map { it.value.first } .map { it.value.first }
.forEach { bulkFavoriteScreenModel.select(it) } .let {
scope.launchIO {
bulkFavoriteScreenModel.networkToLocalManga.getLocal(it)
.forEach { bulkFavoriteScreenModel.select(it) }
}
}
}, },
onReverseSelection = { onReverseSelection = {
mangaList.itemSnapshotList.items mangaList.itemSnapshotList.items
.map { it.value.first } .map { it.value.first }
.let { bulkFavoriteScreenModel.reverseSelection(it) } .let {
scope.launchIO {
bulkFavoriteScreenModel.reverseSelection(
bulkFavoriteScreenModel.networkToLocalManga.getLocal(it),
)
}
}
}, },
) )
} else { } else {
@ -139,23 +153,37 @@ class BrowseRecommendsScreen(
onWebViewClick = null, onWebViewClick = null,
onHelpClick = null, onHelpClick = null,
onLocalSourceHelpClick = null, onLocalSourceHelpClick = null,
onMangaClick = { manga -> onMangaClick = {
// KMK --> // KMK -->
if (bulkFavoriteState.selectionMode) { if (isExternalSource) {
bulkFavoriteScreenModel.toggleSelection(manga) onClickItem(it)
} else { } else {
// KMK <-- scope.launchIO {
onClickItem(manga) val manga = screenModel.networkToLocalManga.getLocal(it)
if (bulkFavoriteState.selectionMode) {
bulkFavoriteScreenModel.toggleSelection(manga)
} else {
// KMK <--
onClickItem(manga)
}
}
} }
}, },
onMangaLongClick = { manga -> onMangaLongClick = {
// KMK --> // KMK -->
if (!bulkFavoriteState.selectionMode) { if (isExternalSource) {
bulkFavoriteScreenModel.addRemoveManga(manga, haptic) onLongClickItem(it)
} else { } else {
onLongClickItem(manga) scope.launchIO {
val manga = screenModel.networkToLocalManga.getLocal(it)
if (!bulkFavoriteState.selectionMode) {
bulkFavoriteScreenModel.addRemoveManga(manga, haptic)
} else {
onLongClickItem(manga)
}
}
// KMK <--
} }
// KMK <--
}, },
// KMK --> // KMK -->
selection = bulkFavoriteState.selection, selection = bulkFavoriteState.selection,

View file

@ -4,6 +4,7 @@ import androidx.activity.compose.BackHandler
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalHapticFeedback
import cafe.adriel.voyager.core.model.rememberScreenModel import cafe.adriel.voyager.core.model.rememberScreenModel
@ -17,6 +18,7 @@ import eu.kanade.tachiyomi.ui.browse.source.SourcesScreen
import eu.kanade.tachiyomi.ui.manga.MangaScreen import eu.kanade.tachiyomi.ui.manga.MangaScreen
import eu.kanade.tachiyomi.ui.webview.WebViewActivity import eu.kanade.tachiyomi.ui.webview.WebViewActivity
import exh.recs.components.RecommendsScreen import exh.recs.components.RecommendsScreen
import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import tachiyomi.presentation.core.screens.LoadingScreen import tachiyomi.presentation.core.screens.LoadingScreen
@ -38,6 +40,7 @@ class RecommendsScreen(val mangaId: Long, val sourceId: Long) : Screen() {
val state by screenModel.state.collectAsState() val state by screenModel.state.collectAsState()
// KMK --> // KMK -->
val scope = rememberCoroutineScope()
val bulkFavoriteScreenModel = rememberScreenModel { BulkFavoriteScreenModel() } val bulkFavoriteScreenModel = rememberScreenModel { BulkFavoriteScreenModel() }
val bulkFavoriteState by bulkFavoriteScreenModel.state.collectAsState() val bulkFavoriteState by bulkFavoriteScreenModel.state.collectAsState()
@ -49,12 +52,21 @@ class RecommendsScreen(val mangaId: Long, val sourceId: Long) : Screen() {
// KMK <-- // KMK <--
val onClickItem = { manga: Manga -> val onClickItem = { manga: Manga ->
navigator.push( when (manga.source) {
when (manga.source) { RECOMMENDS_SOURCE -> navigator.push(
RECOMMENDS_SOURCE -> SourcesScreen(SourcesScreen.SmartSearchConfig(manga.ogTitle)) SourcesScreen(SourcesScreen.SmartSearchConfig(manga.ogTitle)),
else -> MangaScreen(manga.id, true) )
}, else -> {
) // KMK -->
scope.launchIO {
val localManga = screenModel.networkToLocalManga.getLocal(manga)
navigator.push(
// KMK <--
MangaScreen(localManga.id, true),
)
}
}
}
} }
val onLongClickItem = { manga: Manga -> val onLongClickItem = { manga: Manga ->
@ -62,11 +74,13 @@ class RecommendsScreen(val mangaId: Long, val sourceId: Long) : Screen() {
RECOMMENDS_SOURCE -> WebViewActivity.newIntent(context, manga.url, title = manga.title).let(context::startActivity) RECOMMENDS_SOURCE -> WebViewActivity.newIntent(context, manga.url, title = manga.title).let(context::startActivity)
else -> { else -> {
// KMK --> // KMK -->
// Add to favorite scope.launchIO {
bulkFavoriteScreenModel.addRemoveManga( val localManga = screenModel.networkToLocalManga.getLocal(manga)
manga, bulkFavoriteScreenModel.addRemoveManga(
haptic, localManga,
) haptic,
)
}
// KMK <-- // KMK <--
} }
} }

View file

@ -32,7 +32,7 @@ open class RecommendsScreenModel(
val mangaId: Long, val mangaId: Long,
val sourceId: Long, val sourceId: Long,
private val getManga: GetManga = Injekt.get(), private val getManga: GetManga = Injekt.get(),
protected val networkToLocalManga: NetworkToLocalManga = Injekt.get(), val networkToLocalManga: NetworkToLocalManga = Injekt.get(),
) : StateScreenModel<RecommendsScreenModel.State>(State()) { ) : StateScreenModel<RecommendsScreenModel.State>(State()) {
// KMK --> // KMK -->
@ -78,14 +78,14 @@ open class RecommendsScreenModel(
} }
val recSourceId = recSource.associatedSourceId val recSourceId = recSource.associatedSourceId
val titles = if (recSourceId != null) { // KMK -->
val titles = page.mangas.map {
// If the recommendation is associated with a source, resolve it // If the recommendation is associated with a source, resolve it
page.mangas.map { it.toDomainManga(recSourceId) }
.let { networkToLocalManga(it) }
} else {
// Otherwise, skip this step. The user will be prompted to choose a source via SmartSearch // Otherwise, skip this step. The user will be prompted to choose a source via SmartSearch
page.mangas.map { it.toDomainManga(RECOMMENDS_SOURCE) } it.toDomainManga(recSourceId ?: RECOMMENDS_SOURCE)
/* KMK --> .let { networkToLocalManga(it) } KMK <-- */
} }
// KMK <--
.distinctBy { it.url } .distinctBy { it.url }
if (isActive) { if (isActive) {

View file

@ -20,7 +20,7 @@ abstract class EHentaiPagingSource(override val source: CatalogueSource) : BaseS
val manga = mangasPage.mangas val manga = mangasPage.mangas
.map { it.toDomainManga(source.id) } .map { it.toDomainManga(source.id) }
.filter { seenManga.add(it.url) } .filter { seenManga.add(it.url) }
.let { networkToLocalManga(it) } /* KMK --> .let { networkToLocalManga(it) } KMK <-- */
// SY --> // SY -->
.mapIndexed { index, manga -> manga to metadata.getOrNull(index) } .mapIndexed { index, manga -> manga to metadata.getOrNull(index) }
// SY <-- // SY <--

View file

@ -92,7 +92,7 @@ abstract class BaseSourcePagingSource(
val manga = mangasPage.mangas val manga = mangasPage.mangas
.map { it.toDomainManga(source!!.id) } .map { it.toDomainManga(source!!.id) }
.filter { seenManga.add(it.url) } .filter { seenManga.add(it.url) }
.let { networkToLocalManga(it) } /* KMK --> .let { networkToLocalManga(it) } KMK <-- */
// SY --> // SY -->
.mapIndexed { index, manga -> manga to metadata.getOrNull(index) } .mapIndexed { index, manga -> manga to metadata.getOrNull(index) }
// SY <-- // SY <--

View file

@ -14,4 +14,22 @@ class NetworkToLocalManga(
suspend operator fun invoke(manga: List<Manga>): List<Manga> { suspend operator fun invoke(manga: List<Manga>): List<Manga> {
return mangaRepository.insertNetworkManga(manga) return mangaRepository.insertNetworkManga(manga)
} }
// KMK -->
suspend fun getLocal(manga: Manga): Manga = if (manga.id <= 0) {
invoke(manga)
} else {
manga
}
suspend fun getLocal(mangas: List<Manga>): List<Manga> {
return mangas.map { manga ->
if (manga.id <= 0) {
invoke(manga)
} else {
manga
}
}
}
// KMK <--
} }