diff --git a/app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsScreen.kt index 79352cdbd..2522c6c87 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsScreen.kt @@ -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, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsScreen.kt index 7ee326fb8..d7e5df0de 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsScreen.kt @@ -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,32 +26,28 @@ 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 - ExtensionDetailsScreen( - navigateUp = navigator::pop, - state = state, - onClickSourcePreferences = { navigator.push(SourcePreferencesScreen(it)) }, - onClickEnableAll = { viewModel.toggleSources(true) }, - onClickDisableAll = { viewModel.toggleSources(false) }, - onClickClearCookies = viewModel::clearCookies, - onClickUninstall = viewModel::uninstallExtension, - onClickSource = viewModel::toggleSource, - onClickIncognito = viewModel::toggleIncognito, - ) - - LaunchedEffect(Unit) { - viewModel.events.collectLatest { event -> - if (event is ExtensionDetailsEvent.Uninstalled) { - navigator.pop() - } + 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, + onClickSourcePreferences = { navigator.push(SourcePreferencesScreen(it)) }, + onClickEnableAll = { viewModel.toggleSources(true) }, + onClickDisableAll = { viewModel.toggleSources(false) }, + onClickClearCookies = viewModel::clearCookies, + onClickUninstall = viewModel::uninstallExtension, + onClickSource = viewModel::toggleSource, + onClickIncognito = viewModel::toggleIncognito, + ) } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsViewModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsViewModel.kt index a1ab13351..0a9f7fc35 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsViewModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsViewModel.kt @@ -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(State()) { +) : ViewModel() { companion object { val PKG_NAME_KEY = CreationExtras.Key() @@ -57,62 +60,44 @@ class ExtensionDetailsViewModel( } } - private val _events: Channel = Channel() - val events: Flow = _events.receiveAsFlow() - - init { - viewModelScope.launch { - launch { - 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) - } - } - } - launch { - state.collectLatest { state -> - if (state.extension == null) return@collectLatest - getExtensionSources.subscribe(state.extension) - .map { - it.sortedWith( - compareBy( - { !it.enabled }, - { item -> - item.source.name.takeIf { item.labelAsName } - ?: LocaleHelper.getSourceDisplayName(item.source.lang, context).lowercase() - }, - ), - ) - } - .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) } - } + val state: StateFlow = extensionManager.installedExtensionsFlow + .map { it.firstOrNull { extension -> extension.pkgName == pkgName } } + .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) } } + .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> { + return getExtensionSources.subscribe(extension) + .map { + it.sortedWith( + compareBy( + { !it.enabled }, + { item -> + item.source.name.takeIf { item.labelAsName } + ?: LocaleHelper.getSourceDisplayName(item.source.lang, context).lowercase() + }, + ), + ) + } + .catch { throwable -> + logcat(LogPriority.ERROR, throwable) + emit(listOf()) + } } fun clearCookies() { - val extension = state.value.extension ?: return + val extension = successState?.extension ?: return val urls = extension.sources .filterIsInstance() @@ -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) } } - @Immutable - data class State( - val extension: Extension.Installed? = null, - val isIncognito: Boolean = false, - private val _sources: List? = null, - ) { + sealed interface State { - val sources: List - get() = _sources ?: listOf() + data object Loading : State - val isLoading: Boolean - get() = extension == null || _sources == null + data object Uninstalled : State + + @Immutable + data class Success( + val extension: Extension.Installed, + val isIncognito: Boolean, + val sources: List, + ) : State } } - -sealed interface ExtensionDetailsEvent { - data object Uninstalled : ExtensionDetailsEvent -}