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.material.icons.outlined.TravelExplore
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.LocalNavigator
@@ -26,7 +26,7 @@ import tachiyomi.presentation.core.i18n.stringResource
fun Screen.sourcesTab(): TabContent { fun Screen.sourcesTab(): TabContent {
val navigator = LocalNavigator.currentOrThrow val navigator = LocalNavigator.currentOrThrow
val viewModel = viewModel<SourcesViewModel>() val viewModel = viewModel<SourcesViewModel>()
val state by viewModel.state.collectAsState() val state by viewModel.state.collectAsStateWithLifecycle()
return TabContent( return TabContent(
titleRes = MR.strings.label_sources, titleRes = MR.strings.label_sources,
@@ -1,79 +1,88 @@
package eu.kanade.tachiyomi.ui.browse.source package eu.kanade.tachiyomi.ui.browse.source
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import eu.kanade.domain.source.interactor.GetEnabledSources import eu.kanade.domain.source.interactor.GetEnabledSources
import eu.kanade.domain.source.interactor.ToggleSource import eu.kanade.domain.source.interactor.ToggleSource
import eu.kanade.domain.source.interactor.ToggleSourcePin import eu.kanade.domain.source.interactor.ToggleSourcePin
import eu.kanade.presentation.browse.SourceUiModel import eu.kanade.presentation.browse.SourceUiModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.Channel 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.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.receiveAsFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import logcat.LogPriority import logcat.LogPriority
import mihon.core.viewmodel.StateViewModel
import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.core.common.util.system.logcat import tachiyomi.core.common.util.system.logcat
import tachiyomi.domain.source.model.Pin import tachiyomi.domain.source.model.Pin
import tachiyomi.domain.source.model.Source import tachiyomi.domain.source.model.Source
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.util.TreeMap import java.util.TreeMap
import kotlin.time.Duration.Companion.seconds
class SourcesViewModel( class SourcesViewModel(
private val getEnabledSources: GetEnabledSources = Injekt.get(), private val getEnabledSources: GetEnabledSources = Injekt.get(),
private val toggleSource: ToggleSource = Injekt.get(), private val toggleSource: ToggleSource = Injekt.get(),
private val toggleSourcePin: ToggleSourcePin = Injekt.get(), private val toggleSourcePin: ToggleSourcePin = Injekt.get(),
) : StateViewModel<SourcesViewModel.State>(State()) { ) : ViewModel() {
private val _events = Channel<Event>(Int.MAX_VALUE) private val _events = Channel<Event>(Int.MAX_VALUE)
val events = _events.receiveAsFlow() val events = _events.receiveAsFlow()
init { private val dialog = MutableStateFlow<Dialog?>(null)
viewModelScope.launchIO {
getEnabledSources.subscribe() private val enabledSources = getEnabledSources.subscribe()
.catch { .catch {
logcat(LogPriority.ERROR, it) logcat(LogPriority.ERROR, it)
_events.send(Event.FailedFetchingSources) _events.send(Event.FailedFetchingSources)
}
.collectLatest(::collectLatestSources)
} }
.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>) { private fun toSourceUiModels(sources: List<Source>): List<SourceUiModel> {
mutableState.update { state -> val map = TreeMap<String, MutableList<Source>> { d1, d2 ->
val map = TreeMap<String, MutableList<Source>> { d1, d2 -> // Sources without a lang defined will be placed at the end
// Sources without a lang defined will be placed at the end when {
when { d1 == LAST_USED_KEY && d2 != LAST_USED_KEY -> -1
d1 == LAST_USED_KEY && d2 != LAST_USED_KEY -> -1 d2 == LAST_USED_KEY && d1 != LAST_USED_KEY -> 1
d2 == LAST_USED_KEY && d1 != LAST_USED_KEY -> 1 d1 == PINNED_KEY && d2 != PINNED_KEY -> -1
d1 == PINNED_KEY && d2 != PINNED_KEY -> -1 d2 == PINNED_KEY && d1 != PINNED_KEY -> 1
d2 == PINNED_KEY && d1 != PINNED_KEY -> 1 d1 == "" && d2 != "" -> 1
d1 == "" && d2 != "" -> 1 d2 == "" && d1 != "" -> -1
d2 == "" && d1 != "" -> -1 else -> d1.compareTo(d2)
else -> d1.compareTo(d2)
}
} }
val byLang = sources.groupByTo(map) { }
when { val byLang = sources.groupByTo(map) {
it.isUsedLast -> LAST_USED_KEY when {
Pin.Actual in it.pin -> PINNED_KEY it.isUsedLast -> LAST_USED_KEY
else -> it.lang Pin.Actual in it.pin -> PINNED_KEY
} else -> it.lang
} }
}
state.copy( return byLang.flatMap {
isLoading = false, listOf(
items = byLang SourceUiModel.Header(it.key),
.flatMap { *it.value.map { source ->
listOf( SourceUiModel.Item(source)
SourceUiModel.Header(it.key), }.toTypedArray(),
*it.value.map { source ->
SourceUiModel.Item(source)
}.toTypedArray(),
)
},
) )
} }
} }
@@ -87,11 +96,11 @@ class SourcesViewModel(
} }
fun showSourceDialog(source: Source) { fun showSourceDialog(source: Source) {
mutableState.update { it.copy(dialog = Dialog(source)) } dialog.update { Dialog(source) }
} }
fun closeDialog() { fun closeDialog() {
mutableState.update { it.copy(dialog = null) } dialog.update { null }
} }
sealed interface Event { sealed interface Event {