Make SourcesViewModel collect sources only while subscribed (#3721)

Assisted-by: Claude:claude-opus-5
This commit is contained in:
AntsyLich
2026-08-08 11:58:52 +06:00
committed by GitHub
parent 2aec7d19c0
commit 6c0058d230
2 changed files with 55 additions and 46 deletions
@@ -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<SourcesViewModel>()
val state by viewModel.state.collectAsState()
val state by viewModel.state.collectAsStateWithLifecycle()
return TabContent(
titleRes = MR.strings.label_sources,
@@ -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<SourcesViewModel.State>(State()) {
) : ViewModel() {
private val _events = Channel<Event>(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<Dialog?>(null)
private val enabledSources = getEnabledSources.subscribe()
.catch {
logcat(LogPriority.ERROR, it)
_events.send(Event.FailedFetchingSources)
}
.map(::toSourceUiModels)
val state: StateFlow<State> = 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<Source>) {
mutableState.update { state ->
val map = TreeMap<String, MutableList<Source>> { 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<Source>): List<SourceUiModel> {
val map = TreeMap<String, MutableList<Source>> { 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 {