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(State()) { // KMK --> private val sourceCatalogue = SourceCatalogue(sourceId) // KMK <-- private val coroutineDispatcher = Dispatchers.IO.limitedParallelism(5) private val sortComparator = { map: Map -> compareBy( { (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 { return produceState(initialValue = initialManga) { getManga.subscribe(initialManga.url, initialManga.source) .filterNotNull() .collectLatest { manga -> value = manga } } } private fun updateItems(items: PersistentMap) { 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 = 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, ) : RecommendationItemResult { val isEmpty: Boolean get() = result.isEmpty() } fun isVisible(onlyShowHasResults: Boolean): Boolean { return !onlyShowHasResults || (this is Success && !this.isEmpty) } } const val RECOMMENDS_SOURCE = -1L