From 6c0058d2302259fb92d7398da0e19605b2e49771 Mon Sep 17 00:00:00 2001 From: AntsyLich <59261191+AntsyLich@users.noreply.github.com> Date: Sat, 8 Aug 2026 11:58:52 +0600 Subject: [PATCH] Make SourcesViewModel collect sources only while subscribed (#3721) Assisted-by: Claude:claude-opus-5 --- .../tachiyomi/ui/browse/source/SourcesTab.kt | 4 +- .../ui/browse/source/SourcesViewModel.kt | 97 ++++++++++--------- 2 files changed, 55 insertions(+), 46 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesTab.kt index 13cea082c..4326d3fbc 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesTab.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesTab.kt @@ -5,8 +5,8 @@ import androidx.compose.material.icons.outlined.FilterList import androidx.compose.material.icons.outlined.TravelExplore import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.navigator.LocalNavigator @@ -26,7 +26,7 @@ import tachiyomi.presentation.core.i18n.stringResource fun Screen.sourcesTab(): TabContent { val navigator = LocalNavigator.currentOrThrow val viewModel = viewModel() - val state by viewModel.state.collectAsState() + val state by viewModel.state.collectAsStateWithLifecycle() return TabContent( titleRes = MR.strings.label_sources, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesViewModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesViewModel.kt index 00eda6973..d13923904 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesViewModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesViewModel.kt @@ -1,79 +1,88 @@ package eu.kanade.tachiyomi.ui.browse.source import androidx.compose.runtime.Immutable +import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import eu.kanade.domain.source.interactor.GetEnabledSources import eu.kanade.domain.source.interactor.ToggleSource import eu.kanade.domain.source.interactor.ToggleSourcePin import eu.kanade.presentation.browse.SourceUiModel +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.WhileSubscribed import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import logcat.LogPriority -import mihon.core.viewmodel.StateViewModel -import tachiyomi.core.common.util.lang.launchIO import tachiyomi.core.common.util.system.logcat import tachiyomi.domain.source.model.Pin import tachiyomi.domain.source.model.Source import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import java.util.TreeMap +import kotlin.time.Duration.Companion.seconds class SourcesViewModel( private val getEnabledSources: GetEnabledSources = Injekt.get(), private val toggleSource: ToggleSource = Injekt.get(), private val toggleSourcePin: ToggleSourcePin = Injekt.get(), -) : StateViewModel(State()) { +) : ViewModel() { private val _events = Channel(Int.MAX_VALUE) val events = _events.receiveAsFlow() - init { - viewModelScope.launchIO { - getEnabledSources.subscribe() - .catch { - logcat(LogPriority.ERROR, it) - _events.send(Event.FailedFetchingSources) - } - .collectLatest(::collectLatestSources) + private val dialog = MutableStateFlow(null) + + private val enabledSources = getEnabledSources.subscribe() + .catch { + logcat(LogPriority.ERROR, it) + _events.send(Event.FailedFetchingSources) } + .map(::toSourceUiModels) + + val state: StateFlow = combine( + enabledSources, + dialog, + ) { items, dialog -> + State(dialog = dialog, isLoading = false, items = items) } + .flowOn(Dispatchers.IO) + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5.seconds), State()) - private fun collectLatestSources(sources: List) { - mutableState.update { state -> - val map = TreeMap> { d1, d2 -> - // Sources without a lang defined will be placed at the end - when { - d1 == LAST_USED_KEY && d2 != LAST_USED_KEY -> -1 - d2 == LAST_USED_KEY && d1 != LAST_USED_KEY -> 1 - d1 == PINNED_KEY && d2 != PINNED_KEY -> -1 - d2 == PINNED_KEY && d1 != PINNED_KEY -> 1 - d1 == "" && d2 != "" -> 1 - d2 == "" && d1 != "" -> -1 - else -> d1.compareTo(d2) - } + private fun toSourceUiModels(sources: List): List { + val map = TreeMap> { d1, d2 -> + // Sources without a lang defined will be placed at the end + when { + d1 == LAST_USED_KEY && d2 != LAST_USED_KEY -> -1 + d2 == LAST_USED_KEY && d1 != LAST_USED_KEY -> 1 + d1 == PINNED_KEY && d2 != PINNED_KEY -> -1 + d2 == PINNED_KEY && d1 != PINNED_KEY -> 1 + d1 == "" && d2 != "" -> 1 + d2 == "" && d1 != "" -> -1 + else -> d1.compareTo(d2) } - val byLang = sources.groupByTo(map) { - when { - it.isUsedLast -> LAST_USED_KEY - Pin.Actual in it.pin -> PINNED_KEY - else -> it.lang - } + } + val byLang = sources.groupByTo(map) { + when { + it.isUsedLast -> LAST_USED_KEY + Pin.Actual in it.pin -> PINNED_KEY + else -> it.lang } + } - state.copy( - isLoading = false, - items = byLang - .flatMap { - listOf( - SourceUiModel.Header(it.key), - *it.value.map { source -> - SourceUiModel.Item(source) - }.toTypedArray(), - ) - }, + return byLang.flatMap { + listOf( + SourceUiModel.Header(it.key), + *it.value.map { source -> + SourceUiModel.Item(source) + }.toTypedArray(), ) } } @@ -87,11 +96,11 @@ class SourcesViewModel( } fun showSourceDialog(source: Source) { - mutableState.update { it.copy(dialog = Dialog(source)) } + dialog.update { Dialog(source) } } fun closeDialog() { - mutableState.update { it.copy(dialog = null) } + dialog.update { null } } sealed interface Event {