2020-10-26 07:13:02 +01:00
|
|
|
package exh.recs
|
|
|
|
|
|
2025-01-21 20:29:32 +01:00
|
|
|
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
|
2023-02-16 04:47:47 +01:00
|
|
|
import eu.kanade.tachiyomi.source.CatalogueSource
|
2025-01-21 20:29:32 +01:00
|
|
|
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.Job
|
|
|
|
|
import kotlinx.coroutines.asCoroutineDispatcher
|
|
|
|
|
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
|
2022-07-03 05:52:03 +02:00
|
|
|
import kotlinx.coroutines.runBlocking
|
2025-01-21 20:29:32 +01:00
|
|
|
import kotlinx.coroutines.withContext
|
2023-02-18 21:14:04 +01:00
|
|
|
import tachiyomi.domain.manga.interactor.GetManga
|
2025-01-21 20:29:32 +01:00
|
|
|
import tachiyomi.domain.manga.interactor.NetworkToLocalManga
|
|
|
|
|
import tachiyomi.domain.manga.model.Manga
|
|
|
|
|
import tachiyomi.domain.source.service.SourceManager
|
2022-07-03 05:52:03 +02:00
|
|
|
import uy.kohesive.injekt.Injekt
|
|
|
|
|
import uy.kohesive.injekt.api.get
|
2025-01-21 20:29:32 +01:00
|
|
|
import java.util.concurrent.Executors
|
2020-10-26 07:13:02 +01:00
|
|
|
|
2025-01-21 20:29:32 +01:00
|
|
|
open class RecommendsScreenModel(
|
2022-07-03 05:52:03 +02:00
|
|
|
val mangaId: Long,
|
2025-01-21 20:29:32 +01:00
|
|
|
val sourceId: Long,
|
|
|
|
|
initialState: State = State(),
|
|
|
|
|
sourceManager: SourceManager = Injekt.get(),
|
2022-07-03 19:37:27 +02:00
|
|
|
private val getManga: GetManga = Injekt.get(),
|
2025-01-21 20:29:32 +01:00
|
|
|
internal val networkToLocalManga: NetworkToLocalManga = Injekt.get(),
|
|
|
|
|
) : StateScreenModel<RecommendsScreenModel.State>(initialState) {
|
2020-10-26 07:13:02 +01:00
|
|
|
|
2022-12-01 05:05:11 +01:00
|
|
|
val manga = runBlocking { getManga.await(mangaId) }!!
|
2025-01-21 20:29:32 +01:00
|
|
|
val source = sourceManager.getOrStub(sourceId) as CatalogueSource
|
2022-08-31 20:41:35 +02:00
|
|
|
|
2025-01-21 20:29:32 +01:00
|
|
|
private val coroutineDispatcher = Executors.newFixedThreadPool(5).asCoroutineDispatcher()
|
|
|
|
|
private var findRecsJob: Job? = null
|
|
|
|
|
|
|
|
|
|
private val sortComparator = { map: Map<RecommendationPagingSource, RecommendationItemResult> ->
|
|
|
|
|
compareBy<RecommendationPagingSource>(
|
|
|
|
|
{ (map[it] as? RecommendationItemResult.Success)?.isEmpty ?: true },
|
|
|
|
|
{ it.name },
|
|
|
|
|
{ it.category.resourceId },
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
init {
|
|
|
|
|
findRecsJob?.cancel()
|
|
|
|
|
|
|
|
|
|
val recommendationSources = RecommendationPagingSource.createSources(manga, source)
|
|
|
|
|
|
|
|
|
|
updateItems(
|
|
|
|
|
recommendationSources
|
|
|
|
|
.associateWith { RecommendationItemResult.Loading }
|
|
|
|
|
.toPersistentMap(),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
findRecsJob = ioCoroutineScope.launch {
|
|
|
|
|
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
|
|
|
|
|
if (recSourceId != null) {
|
|
|
|
|
// If the recommendation is associated with a source, resolve it
|
|
|
|
|
networkToLocalManga.await(it.toDomainManga(recSourceId))
|
|
|
|
|
} else {
|
|
|
|
|
// Otherwise, skip this step. The user will be prompted to choose a source via SmartSearch
|
|
|
|
|
it.toDomainManga(-1)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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 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)
|
2020-10-26 07:13:02 +01:00
|
|
|
}
|
|
|
|
|
}
|