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