Make ExtensionDetailsViewModel collect sources only while subscribed (#3726)
Assisted-by: Claude:claude-opus-5
This commit is contained in:
@@ -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,
|
||||
|
||||
+22
-25
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+57
-76
@@ -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,62 +60,44 @@ class ExtensionDetailsViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
private val _events: Channel<ExtensionDetailsEvent> = Channel()
|
||||
val events: Flow<ExtensionDetailsEvent> = _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<State> = 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<List<ExtensionSourceItem>> {
|
||||
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<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)
|
||||
}
|
||||
}
|
||||
|
||||
@Immutable
|
||||
data class State(
|
||||
val extension: Extension.Installed? = null,
|
||||
val isIncognito: Boolean = false,
|
||||
private val _sources: List<ExtensionSourceItem>? = null,
|
||||
) {
|
||||
sealed interface State {
|
||||
|
||||
val sources: List<ExtensionSourceItem>
|
||||
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<ExtensionSourceItem>,
|
||||
) : State
|
||||
}
|
||||
}
|
||||
|
||||
sealed interface ExtensionDetailsEvent {
|
||||
data object Uninstalled : ExtensionDetailsEvent
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user