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.domain.manga.model.toDomainManga import eu.kanade.presentation.util.ioCoroutineScope import eu.kanade.tachiyomi.source.CatalogueSource import exh.recs.sources.RecommendationPagingSource 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 tachiyomi.domain.manga.interactor.GetManga import tachiyomi.domain.manga.interactor.NetworkToLocalManga import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.source.service.SourceManager import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get open class RecommendsScreenModel( val mangaId: Long, val sourceId: Long, sourceManager: SourceManager = Injekt.get(), private val getManga: GetManga = Injekt.get(), internal val networkToLocalManga: NetworkToLocalManga = Injekt.get(), ) : StateScreenModel(State()) { val source = sourceManager.getOrStub(sourceId) as CatalogueSource 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, source) 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 titles = page.mangas.map { val recSourceId = recSource.associatedSourceId // KMK --> // If the recommendation is associated with a source, resolve it // Otherwise, skip this step. The user will be prompted to choose a source via SmartSearch it.toDomainManga(recSourceId ?: -1) // KMK <-- } 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) } }