Make ExtensionDetailsViewModel collect sources only while subscribed (#3726)

Assisted-by: Claude:claude-opus-5
This commit is contained in:
AntsyLich
2026-08-08 20:04:17 +06:00
committed by GitHub
parent 62288c08fb
commit ae37c1651a
3 changed files with 82 additions and 113 deletions
@@ -61,12 +61,11 @@ import tachiyomi.presentation.core.components.ScrollbarLazyColumn
import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.screens.EmptyScreen
@Composable
fun ExtensionDetailsScreen(
navigateUp: () -> Unit,
state: ExtensionDetailsViewModel.State,
state: ExtensionDetailsViewModel.State.Success,
onClickSourcePreferences: (sourceId: Long) -> Unit,
onClickEnableAll: () -> Unit,
onClickDisableAll: () -> Unit,
@@ -78,12 +77,12 @@ fun ExtensionDetailsScreen(
val uriHandler = LocalUriHandler.current
val url = remember(state.extension) {
val regex = """https://raw.githubusercontent.com/(.+?)/(.+?)/.+""".toRegex()
regex.find(state.extension?.store?.indexUrl.orEmpty())
regex.find(state.extension.store?.indexUrl.orEmpty())
?.let {
val (user, repo) = it.destructured
"https://github.com/$user/$repo"
}
?: state.extension?.store?.indexUrl
?: state.extension.store?.indexUrl
}
Scaffold(
@@ -128,14 +127,6 @@ fun ExtensionDetailsScreen(
)
},
) { paddingValues ->
if (state.extension == null) {
EmptyScreen(
MR.strings.empty_screen,
modifier = Modifier.padding(paddingValues),
)
return@Scaffold
}
ExtensionDetails(
contentPadding = paddingValues,
extension = state.extension,
@@ -2,15 +2,16 @@ package eu.kanade.tachiyomi.ui.browse.extension.details
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.CreationExtras
import androidx.lifecycle.viewmodel.compose.viewModel
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.presentation.browse.ExtensionDetailsScreen
import eu.kanade.presentation.util.Screen
import kotlinx.coroutines.flow.collectLatest
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.screens.EmptyScreen
import tachiyomi.presentation.core.screens.LoadingScreen
data class ExtensionDetailsScreen(
@@ -25,15 +26,17 @@ data class ExtensionDetailsScreen(
set(ExtensionDetailsViewModel.PKG_NAME_KEY, pkgName)
},
)
val state by viewModel.state.collectAsState()
if (state.isLoading) {
LoadingScreen()
return
}
val state by viewModel.state.collectAsStateWithLifecycle()
val navigator = LocalNavigator.currentOrThrow
when (val state = state) {
ExtensionDetailsViewModel.State.Loading -> LoadingScreen()
ExtensionDetailsViewModel.State.Uninstalled -> {
LaunchedEffect(Unit) { navigator.pop() }
EmptyScreen(MR.strings.empty_screen)
}
is ExtensionDetailsViewModel.State.Success -> {
ExtensionDetailsScreen(
navigateUp = navigator::pop,
state = state,
@@ -45,12 +48,6 @@ data class ExtensionDetailsScreen(
onClickSource = viewModel::toggleSource,
onClickIncognito = viewModel::toggleIncognito,
)
LaunchedEffect(Unit) {
viewModel.events.collectLatest { event ->
if (event is ExtensionDetailsEvent.Uninstalled) {
navigator.pop()
}
}
}
}
@@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.ui.browse.extension.details
import android.app.Application
import android.content.Context
import androidx.compose.runtime.Immutable
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.CreationExtras
import androidx.lifecycle.viewmodel.initializer
@@ -17,32 +18,34 @@ import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.system.LocaleHelper
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
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.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.flow.stateIn
import logcat.LogPriority
import mihon.core.viewmodel.StateViewModel
import okhttp3.HttpUrl.Companion.toHttpUrl
import tachiyomi.core.common.util.system.logcat
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import kotlin.time.Duration.Companion.seconds
class ExtensionDetailsViewModel(
pkgName: String,
context: Context,
private val context: Context,
private val network: NetworkHelper = Injekt.get(),
private val extensionManager: ExtensionManager = Injekt.get(),
private val getExtensionSources: GetExtensionSources = Injekt.get(),
private val toggleSource: ToggleSource = Injekt.get(),
private val toggleIncognito: ToggleIncognito = Injekt.get(),
private val preferences: SourcePreferences = Injekt.get(),
) : StateViewModel<ExtensionDetailsViewModel.State>(State()) {
) : ViewModel() {
companion object {
val PKG_NAME_KEY = CreationExtras.Key<String>()
@@ -57,28 +60,25 @@ class ExtensionDetailsViewModel(
}
}
private val _events: Channel<ExtensionDetailsEvent> = Channel()
val events: Flow<ExtensionDetailsEvent> = _events.receiveAsFlow()
init {
viewModelScope.launch {
launch {
extensionManager.installedExtensionsFlow
val state: StateFlow<State> = extensionManager.installedExtensionsFlow
.map { it.firstOrNull { extension -> extension.pkgName == pkgName } }
.collectLatest { extension ->
if (extension == null) {
_events.send(ExtensionDetailsEvent.Uninstalled)
return@collectLatest
}
mutableState.update { state ->
state.copy(extension = extension)
.distinctUntilChanged()
.flatMapLatest { extension ->
if (extension == null) return@flatMapLatest flowOf(State.Uninstalled)
combine(
subscribeToSources(extension),
preferences.incognitoExtensions.changes().map { pkgName in it }.distinctUntilChanged(),
) { sources, isIncognito ->
State.Success(extension = extension, isIncognito = isIncognito, sources = sources)
}
}
}
launch {
state.collectLatest { state ->
if (state.extension == null) return@collectLatest
getExtensionSources.subscribe(state.extension)
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5.seconds), State.Loading)
private val successState: State.Success?
get() = state.value as? State.Success
private fun subscribeToSources(extension: Extension.Installed): Flow<List<ExtensionSourceItem>> {
return getExtensionSources.subscribe(extension)
.map {
it.sortedWith(
compareBy(
@@ -92,27 +92,12 @@ class ExtensionDetailsViewModel(
}
.catch { throwable ->
logcat(LogPriority.ERROR, throwable)
mutableState.update { it.copy(_sources = listOf()) }
}
.collectLatest { sources ->
mutableState.update { it.copy(_sources = sources) }
}
}
}
launch {
preferences.incognitoExtensions
.changes()
.map { pkgName in it }
.distinctUntilChanged()
.collectLatest { isIncognito ->
mutableState.update { it.copy(isIncognito = isIncognito) }
}
}
emit(listOf())
}
}
fun clearCookies() {
val extension = state.value.extension ?: return
val extension = successState?.extension ?: return
val urls = extension.sources
.filterIsInstance<HttpSource>()
@@ -133,7 +118,7 @@ class ExtensionDetailsViewModel(
}
fun uninstallExtension() {
val extension = state.value.extension ?: return
val extension = successState?.extension ?: return
extensionManager.uninstallExtension(extension)
}
@@ -142,32 +127,28 @@ class ExtensionDetailsViewModel(
}
fun toggleSources(enable: Boolean) {
state.value.extension?.sources
successState?.extension?.sources
?.map { it.id }
?.let { toggleSource.await(it, enable) }
}
fun toggleIncognito(enable: Boolean) {
state.value.extension?.pkgName?.let { packageName ->
successState?.extension?.pkgName?.let { packageName ->
toggleIncognito.await(packageName, enable)
}
}
sealed interface State {
data object Loading : State
data object Uninstalled : State
@Immutable
data class State(
val extension: Extension.Installed? = null,
val isIncognito: Boolean = false,
private val _sources: List<ExtensionSourceItem>? = null,
) {
val sources: List<ExtensionSourceItem>
get() = _sources ?: listOf()
val isLoading: Boolean
get() = extension == null || _sources == null
data class Success(
val extension: Extension.Installed,
val isIncognito: Boolean,
val sources: List<ExtensionSourceItem>,
) : State
}
}
sealed interface ExtensionDetailsEvent {
data object Uninstalled : ExtensionDetailsEvent
}