* Revert "fix(manga-repository): Fix selected entries while bulk-selection won't show as selected (#927)" This reverts commit303a266720. * Revert "refactor(network-to-local): simplify networkToLocalManga (#910)" This reverts commitd33efced68. * Revert "chore(network-local-manga): Reintroduce (in-memory) fast browsing (#909)" This reverts commit3310784faf. * Add updateInfo parameter to insertNetworkManga method
163 lines
5.8 KiB
Kotlin
163 lines
5.8 KiB
Kotlin
package exh.recs
|
|
|
|
import androidx.compose.runtime.Composable
|
|
import androidx.compose.runtime.Immutable
|
|
import androidx.compose.runtime.produceState
|
|
import cafe.adriel.voyager.core.model.StateScreenModel
|
|
import eu.kanade.presentation.util.ioCoroutineScope
|
|
import exh.recs.sources.RecommendationPagingSource
|
|
import exh.recs.sources.SourceCatalogue
|
|
import kotlinx.collections.immutable.PersistentMap
|
|
import kotlinx.collections.immutable.mutate
|
|
import kotlinx.collections.immutable.persistentMapOf
|
|
import kotlinx.collections.immutable.toImmutableMap
|
|
import kotlinx.collections.immutable.toPersistentMap
|
|
import kotlinx.coroutines.Dispatchers
|
|
import kotlinx.coroutines.async
|
|
import kotlinx.coroutines.awaitAll
|
|
import kotlinx.coroutines.flow.collectLatest
|
|
import kotlinx.coroutines.flow.filterNotNull
|
|
import kotlinx.coroutines.flow.update
|
|
import kotlinx.coroutines.isActive
|
|
import kotlinx.coroutines.launch
|
|
import kotlinx.coroutines.withContext
|
|
import mihon.domain.manga.model.toDomainManga
|
|
import tachiyomi.domain.manga.interactor.GetManga
|
|
import tachiyomi.domain.manga.interactor.NetworkToLocalManga
|
|
import tachiyomi.domain.manga.model.Manga
|
|
import uy.kohesive.injekt.Injekt
|
|
import uy.kohesive.injekt.api.get
|
|
|
|
open class RecommendsScreenModel(
|
|
val mangaId: Long,
|
|
val sourceId: Long,
|
|
private val getManga: GetManga = Injekt.get(),
|
|
protected val networkToLocalManga: NetworkToLocalManga = Injekt.get(),
|
|
) : StateScreenModel<RecommendsScreenModel.State>(State()) {
|
|
|
|
// KMK -->
|
|
private val sourceCatalogue = SourceCatalogue(sourceId)
|
|
// KMK <--
|
|
|
|
private val coroutineDispatcher = Dispatchers.IO.limitedParallelism(5)
|
|
|
|
private val sortComparator = { map: Map<RecommendationPagingSource, RecommendationItemResult> ->
|
|
compareBy<RecommendationPagingSource>(
|
|
{ (map[it] as? RecommendationItemResult.Success)?.isEmpty ?: true },
|
|
{ it.name },
|
|
{ it.category.resourceId },
|
|
)
|
|
}
|
|
|
|
init {
|
|
ioCoroutineScope.launch {
|
|
val manga = getManga.await(mangaId)!!
|
|
mutableState.update { it.copy(manga = manga) }
|
|
val recommendationSources = RecommendationPagingSource.createSources(
|
|
manga,
|
|
// KMK -->
|
|
sourceCatalogue,
|
|
// KMK <--
|
|
)
|
|
|
|
updateItems(
|
|
recommendationSources
|
|
.associateWith { RecommendationItemResult.Loading }
|
|
.toPersistentMap(),
|
|
)
|
|
|
|
recommendationSources.map { recSource ->
|
|
async {
|
|
if (state.value.items[recSource] !is RecommendationItemResult.Loading) {
|
|
return@async
|
|
}
|
|
|
|
try {
|
|
val page = withContext(coroutineDispatcher) {
|
|
recSource.requestNextPage(1)
|
|
}
|
|
|
|
val recSourceId = recSource.associatedSourceId
|
|
val titles = if (recSourceId != null) {
|
|
// 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
|
|
page.mangas.map { it.toDomainManga(RECOMMENDS_SOURCE) }
|
|
}
|
|
.distinctBy { it.url }
|
|
|
|
if (isActive) {
|
|
updateItem(recSource, RecommendationItemResult.Success(titles))
|
|
}
|
|
} catch (e: Exception) {
|
|
if (isActive) {
|
|
updateItem(recSource, RecommendationItemResult.Error(e))
|
|
}
|
|
}
|
|
}
|
|
}.awaitAll()
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
fun getManga(initialManga: Manga): androidx.compose.runtime.State<Manga> {
|
|
return produceState(initialValue = initialManga) {
|
|
getManga.subscribe(initialManga.url, initialManga.source)
|
|
.filterNotNull()
|
|
.collectLatest { manga ->
|
|
value = manga
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun updateItems(items: PersistentMap<RecommendationPagingSource, RecommendationItemResult>) {
|
|
mutableState.update {
|
|
it.copy(
|
|
items = items
|
|
.toSortedMap(sortComparator(items))
|
|
.toPersistentMap(),
|
|
)
|
|
}
|
|
}
|
|
|
|
private fun updateItem(source: RecommendationPagingSource, result: RecommendationItemResult) {
|
|
val newItems = state.value.items.mutate {
|
|
it[source] = result
|
|
}
|
|
updateItems(newItems)
|
|
}
|
|
|
|
@Immutable
|
|
data class State(
|
|
val manga: Manga? = null,
|
|
val items: PersistentMap<RecommendationPagingSource, RecommendationItemResult> = persistentMapOf(),
|
|
) {
|
|
val progress: Int = items.count { it.value !is RecommendationItemResult.Loading }
|
|
val total: Int = items.size
|
|
val filteredItems = items.filter { (_, result) -> result.isVisible(false) }
|
|
.toImmutableMap()
|
|
}
|
|
}
|
|
|
|
sealed interface RecommendationItemResult {
|
|
data object Loading : RecommendationItemResult
|
|
|
|
data class Error(
|
|
val throwable: Throwable,
|
|
) : RecommendationItemResult
|
|
|
|
data class Success(
|
|
val result: List<Manga>,
|
|
) : RecommendationItemResult {
|
|
val isEmpty: Boolean
|
|
get() = result.isEmpty()
|
|
}
|
|
|
|
fun isVisible(onlyShowHasResults: Boolean): Boolean {
|
|
return !onlyShowHasResults || (this is Success && !this.isEmpty)
|
|
}
|
|
}
|
|
|
|
const val RECOMMENDS_SOURCE = -1L
|