Make SourcesViewModel collect sources only while subscribed (#3721)
Assisted-by: Claude:claude-opus-5
This commit is contained in:
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user