Make ExtensionStoresViewModel collect stores only while subscribed (#3717)

Assisted-by: Claude:claude-opus-5
This commit is contained in:
AntsyLich
2026-08-08 11:58:06 +06:00
committed by GitHub
parent 0834884335
commit f6aad7497e
2 changed files with 46 additions and 68 deletions
@@ -2,9 +2,9 @@ package eu.kanade.presentation.more.settings.screen.browse
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.currentOrThrow import cafe.adriel.voyager.navigator.currentOrThrow
@@ -27,7 +27,7 @@ class ExtensionStoresScreen(
val navigator = LocalNavigator.currentOrThrow val navigator = LocalNavigator.currentOrThrow
val viewModel = viewModel<ExtensionStoresViewModel>() val viewModel = viewModel<ExtensionStoresViewModel>()
val state by viewModel.state.collectAsState() val state by viewModel.state.collectAsStateWithLifecycle()
LaunchedEffect(url) { LaunchedEffect(url) {
url?.let { viewModel.addFromDeeplink(url) } url?.let { viewModel.addFromDeeplink(url) }
@@ -1,12 +1,19 @@
package eu.kanade.presentation.more.settings.screen.browse package eu.kanade.presentation.more.settings.screen.browse
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.tachiyomi.extension.ExtensionManager import eu.kanade.tachiyomi.extension.ExtensionManager
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.WhileSubscribed
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import mihon.core.viewmodel.StateViewModel
import mihon.domain.extension.interactor.AddExtensionStore import mihon.domain.extension.interactor.AddExtensionStore
import mihon.domain.extension.interactor.GetExtensionStores import mihon.domain.extension.interactor.GetExtensionStores
import mihon.domain.extension.interactor.RemoveExtensionStore import mihon.domain.extension.interactor.RemoveExtensionStore
@@ -15,6 +22,7 @@ import mihon.domain.extension.model.ExtensionStore
import tachiyomi.core.common.util.lang.launchIO import tachiyomi.core.common.util.lang.launchIO
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 ExtensionStoresViewModel( class ExtensionStoresViewModel(
private val getExtensionStores: GetExtensionStores = Injekt.get(), private val getExtensionStores: GetExtensionStores = Injekt.get(),
@@ -22,32 +30,18 @@ class ExtensionStoresViewModel(
private val removeExtensionStore: RemoveExtensionStore = Injekt.get(), private val removeExtensionStore: RemoveExtensionStore = Injekt.get(),
private val updateExtensionStores: UpdateExtensionStores = Injekt.get(), private val updateExtensionStores: UpdateExtensionStores = Injekt.get(),
private val extensionManager: ExtensionManager = Injekt.get(), private val extensionManager: ExtensionManager = Injekt.get(),
) : StateViewModel<ExtensionStoreScreenState>(ExtensionStoreScreenState.Loading) { ) : ViewModel() {
private inline fun updateSuccessState( private val dialog = MutableStateFlow<ExtensionStoreDialog?>(null)
func: (ExtensionStoreScreenState.Success) -> ExtensionStoreScreenState.Success,
) {
mutableState.update {
when (it) {
ExtensionStoreScreenState.Loading -> it
is ExtensionStoreScreenState.Success -> func(it)
}
}
}
init { val state: StateFlow<ExtensionStoreScreenState> = combine(
viewModelScope.launchIO { getExtensionStores.subscribe(),
getExtensionStores.subscribe() dialog,
.collectLatest { stores -> ) { stores, dialog ->
mutableState.update { ExtensionStoreScreenState.Success(stores = stores, dialog = dialog)
when (it) {
ExtensionStoreScreenState.Loading -> ExtensionStoreScreenState.Success(stores = stores)
is ExtensionStoreScreenState.Success -> it.copy(stores = stores)
}
}
}
}
} }
.flowOn(Dispatchers.IO)
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5.seconds), ExtensionStoreScreenState.Loading)
/** /**
* Creates and adds a new repo to the database. * Creates and adds a new repo to the database.
@@ -56,14 +50,12 @@ class ExtensionStoresViewModel(
*/ */
fun createRepo(baseUrl: String) { fun createRepo(baseUrl: String) {
viewModelScope.launch { viewModelScope.launch {
updateSuccessState { dialog.update {
it.copy( when (it) {
dialog = when (it.dialog) { is ExtensionStoreDialog.Create -> it.copy(processing = true)
is ExtensionStoreDialog.Create -> it.dialog.copy(processing = true) is ExtensionStoreDialog.Confirm -> it.copy(processing = true)
is ExtensionStoreDialog.Confirm -> it.dialog.copy(processing = true) else -> it
else -> it.dialog }
},
)
} }
addExtensionStore(baseUrl) addExtensionStore(baseUrl)
.onSuccess { .onSuccess {
@@ -71,20 +63,18 @@ class ExtensionStoresViewModel(
dismissDialog() dismissDialog()
} }
.onFailure { throwable -> .onFailure { throwable ->
updateSuccessState { dialog.update {
it.copy( when (it) {
dialog = when (it.dialog) { is ExtensionStoreDialog.Create -> it.copy(
is ExtensionStoreDialog.Create -> it.dialog.copy(
processing = false, processing = false,
errorMessage = throwable.message ?: "unknown error", errorMessage = throwable.message ?: "unknown error",
) )
is ExtensionStoreDialog.Confirm -> it.dialog.copy( is ExtensionStoreDialog.Confirm -> it.copy(
processing = false, processing = false,
errorMessage = throwable.message ?: "unknown error", errorMessage = throwable.message ?: "unknown error",
) )
else -> it.dialog else -> it
}, }
)
} }
} }
} }
@@ -94,14 +84,10 @@ class ExtensionStoresViewModel(
* Refreshes information for each repository. * Refreshes information for each repository.
*/ */
fun refreshRepos() { fun refreshRepos() {
val status = state.value
if (status is ExtensionStoreScreenState.Success) {
viewModelScope.launchIO { viewModelScope.launchIO {
updateExtensionStores() updateExtensionStores()
} }
} }
}
/** /**
* Deletes the given repo from the database * Deletes the given repo from the database
@@ -114,26 +100,18 @@ class ExtensionStoresViewModel(
} }
fun addFromDeeplink(storeIndexUrl: String) { fun addFromDeeplink(storeIndexUrl: String) {
updateSuccessState { state -> viewModelScope.launchIO {
state.copy( val alreadyExists = getExtensionStores.get().any { it.indexUrl == storeIndexUrl }
dialog = ExtensionStoreDialog.Confirm( dialog.update { ExtensionStoreDialog.Confirm(url = storeIndexUrl, alreadyExists = alreadyExists) }
url = storeIndexUrl,
alreadyExists = state.stores.any { it.indexUrl == storeIndexUrl },
),
)
} }
} }
fun showDialog(dialog: ExtensionStoreDialog) { fun showDialog(dialog: ExtensionStoreDialog) {
updateSuccessState { state -> this.dialog.update { dialog }
state.copy(dialog = dialog)
}
} }
fun dismissDialog() { fun dismissDialog() {
updateSuccessState { dialog.update { null }
it.copy(dialog = null)
}
} }
} }