Make HistoryViewModel collect history only while subscribed (#3728)

Assisted-by: Claude:claude-opus-5
This commit is contained in:
AntsyLich
2026-08-08 20:04:41 +06:00
committed by GitHub
parent c68efd0e86
commit a494ebd791
2 changed files with 40 additions and 29 deletions
@@ -7,9 +7,9 @@ import androidx.compose.animation.graphics.vector.AnimatedImageVector
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
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.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.Navigator import cafe.adriel.voyager.navigator.Navigator
@@ -63,7 +63,7 @@ data object HistoryTab : Tab {
val navigator = LocalNavigator.currentOrThrow val navigator = LocalNavigator.currentOrThrow
val context = LocalContext.current val context = LocalContext.current
val viewModel = viewModel<HistoryViewModel>() val viewModel = viewModel<HistoryViewModel>()
val state by viewModel.state.collectAsState() val state by viewModel.state.collectAsStateWithLifecycle()
HistoryScreen( HistoryScreen(
state = state, state = state,
@@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.ui.history
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
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.core.util.insertSeparators import eu.kanade.core.util.insertSeparators
import eu.kanade.domain.manga.interactor.UpdateManga import eu.kanade.domain.manga.interactor.UpdateManga
@@ -11,16 +12,21 @@ import eu.kanade.tachiyomi.util.lang.toLocalDate
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
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.combine
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map 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 kotlinx.coroutines.launch import kotlinx.coroutines.launch
import logcat.LogPriority import logcat.LogPriority
import mihon.core.viewmodel.StateViewModel
import tachiyomi.core.common.preference.CheckboxState import tachiyomi.core.common.preference.CheckboxState
import tachiyomi.core.common.preference.mapAsCheckboxState import tachiyomi.core.common.preference.mapAsCheckboxState
import tachiyomi.core.common.util.lang.launchIO import tachiyomi.core.common.util.lang.launchIO
@@ -42,6 +48,7 @@ import tachiyomi.domain.manga.model.MangaWithChapterCount
import tachiyomi.domain.source.service.SourceManager import tachiyomi.domain.source.service.SourceManager
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import kotlin.time.Duration.Companion.seconds
class HistoryViewModel( class HistoryViewModel(
private val addTracks: AddTracks = Injekt.get(), private val addTracks: AddTracks = Injekt.get(),
@@ -56,28 +63,36 @@ class HistoryViewModel(
private val updateManga: UpdateManga = Injekt.get(), private val updateManga: UpdateManga = Injekt.get(),
val snackbarHostState: SnackbarHostState = SnackbarHostState(), val snackbarHostState: SnackbarHostState = SnackbarHostState(),
private val sourceManager: SourceManager = Injekt.get(), private val sourceManager: SourceManager = Injekt.get(),
) : StateViewModel<HistoryViewModel.State>(State()) { ) : ViewModel() {
private val _events: Channel<Event> = Channel(Channel.UNLIMITED) private val _events: Channel<Event> = Channel(Channel.UNLIMITED)
val events: Flow<Event> = _events.receiveAsFlow() val events: Flow<Event> = _events.receiveAsFlow()
init { private val searchQuery = MutableStateFlow<String?>(null)
viewModelScope.launch {
state.map { it.searchQuery } private val dialog = MutableStateFlow<Dialog?>(null)
private val history = searchQuery
.flatMapLatest { query ->
getHistory.subscribe(query ?: "")
.distinctUntilChanged() .distinctUntilChanged()
.flatMapLatest { query -> .catch { error ->
getHistory.subscribe(query ?: "") logcat(LogPriority.ERROR, error)
.distinctUntilChanged() _events.send(Event.InternalError)
.catch { error ->
logcat(LogPriority.ERROR, error)
_events.send(Event.InternalError)
}
.map { it.toHistoryUiModels() }
.flowOn(Dispatchers.IO)
} }
.collect { newList -> mutableState.update { it.copy(list = newList) } } .map { it.toHistoryUiModels() }
.flowOn(Dispatchers.IO)
} }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5.seconds), emptyList())
val state: StateFlow<State> = combine(
searchQuery,
history,
dialog,
) { searchQuery, history, dialog ->
State(searchQuery = searchQuery, list = history, dialog = dialog)
} }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5.seconds), State())
private fun List<HistoryWithRelations>.toHistoryUiModels(): List<HistoryUiModel> { private fun List<HistoryWithRelations>.toHistoryUiModels(): List<HistoryUiModel> {
return map { HistoryUiModel.Item(it) } return map { HistoryUiModel.Item(it) }
@@ -128,11 +143,11 @@ class HistoryViewModel(
} }
fun updateSearchQuery(query: String?) { fun updateSearchQuery(query: String?) {
mutableState.update { it.copy(searchQuery = query) } searchQuery.update { query }
} }
fun setDialog(dialog: Dialog?) { fun setDialog(dialog: Dialog?) {
mutableState.update { it.copy(dialog = dialog) } this.dialog.update { dialog }
} }
/** /**
@@ -175,7 +190,7 @@ class HistoryViewModel(
val duplicates = getDuplicateLibraryManga(manga) val duplicates = getDuplicateLibraryManga(manga)
if (duplicates.isNotEmpty()) { if (duplicates.isNotEmpty()) {
mutableState.update { it.copy(dialog = Dialog.DuplicateManga(manga, duplicates)) } dialog.update { Dialog.DuplicateManga(manga, duplicates) }
return@launchIO return@launchIO
} }
@@ -215,21 +230,17 @@ class HistoryViewModel(
} }
fun showMigrateDialog(target: Manga, current: Manga) { fun showMigrateDialog(target: Manga, current: Manga) {
mutableState.update { currentState -> dialog.update { Dialog.Migrate(target = target, current = current) }
currentState.copy(dialog = Dialog.Migrate(target = target, current = current))
}
} }
fun showChangeCategoryDialog(manga: Manga) { fun showChangeCategoryDialog(manga: Manga) {
viewModelScope.launch { viewModelScope.launch {
val categories = getCategories() val categories = getCategories()
val selection = getMangaCategoryIds(manga) val selection = getMangaCategoryIds(manga)
mutableState.update { currentState -> dialog.update {
currentState.copy( Dialog.ChangeCategory(
dialog = Dialog.ChangeCategory( manga = manga,
manga = manga, initialSelection = categories.mapAsCheckboxState { it.id in selection },
initialSelection = categories.mapAsCheckboxState { it.id in selection },
),
) )
} }
} }