From 02708787483b6dee9d42e39c984ed8c4beaacb40 Mon Sep 17 00:00:00 2001 From: Andreas Date: Fri, 11 Nov 2022 22:57:31 +0100 Subject: [PATCH] Use Voyager on Extension Filter screen (#8503) - Use sealed class for state - Minor changes --- .../browse/ExtensionFilterScreen.kt | 51 +++++-------- .../browse/ExtensionFilterState.kt | 25 ------- .../extension/ExtensionFilterController.kt | 19 ++--- .../extension/ExtensionFilterPresenter.kt | 57 -------------- .../browse/extension/ExtensionFilterScreen.kt | 50 +++++++++++++ .../extension/ExtensionFilterScreenModel.kt | 75 +++++++++++++++++++ 6 files changed, 150 insertions(+), 127 deletions(-) delete mode 100644 app/src/main/java/eu/kanade/presentation/browse/ExtensionFilterState.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionFilterPresenter.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionFilterScreen.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionFilterScreenModel.kt diff --git a/app/src/main/java/eu/kanade/presentation/browse/ExtensionFilterScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/ExtensionFilterScreen.kt index 3893cfc674..7b9a2d6652 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/ExtensionFilterScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/ExtensionFilterScreen.kt @@ -4,28 +4,24 @@ import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.items import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import eu.kanade.presentation.components.AppBar import eu.kanade.presentation.components.EmptyScreen import eu.kanade.presentation.components.FastScrollLazyColumn -import eu.kanade.presentation.components.LoadingScreen import eu.kanade.presentation.components.Scaffold import eu.kanade.presentation.more.settings.widget.SwitchPreferenceWidget import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.ui.browse.extension.ExtensionFilterPresenter +import eu.kanade.tachiyomi.ui.browse.extension.ExtensionFilterState import eu.kanade.tachiyomi.util.system.LocaleHelper -import eu.kanade.tachiyomi.util.system.toast -import kotlinx.coroutines.flow.collectLatest @Composable fun ExtensionFilterScreen( navigateUp: () -> Unit, - presenter: ExtensionFilterPresenter, + state: ExtensionFilterState.Success, + onClickToggle: (String) -> Unit, ) { - val context = LocalContext.current Scaffold( topBar = { scrollBehavior -> AppBar( @@ -35,50 +31,37 @@ fun ExtensionFilterScreen( ) }, ) { contentPadding -> - when { - presenter.isLoading -> LoadingScreen() - presenter.isEmpty -> EmptyScreen( + if (state.isEmpty) { + EmptyScreen( textResource = R.string.empty_screen, modifier = Modifier.padding(contentPadding), ) - else -> ExtensionFilterContent( - contentPadding = contentPadding, - state = presenter, - onClickLang = { - presenter.toggleLanguage(it) - }, - ) - } - } - LaunchedEffect(Unit) { - presenter.events.collectLatest { - when (it) { - ExtensionFilterPresenter.Event.FailedFetchingLanguages -> { - context.toast(R.string.internal_error) - } - } + return@Scaffold } + ExtensionFilterContent( + contentPadding = contentPadding, + state = state, + onClickLang = onClickToggle, + ) } } @Composable private fun ExtensionFilterContent( contentPadding: PaddingValues, - state: ExtensionFilterState, + state: ExtensionFilterState.Success, onClickLang: (String) -> Unit, ) { + val context = LocalContext.current FastScrollLazyColumn( contentPadding = contentPadding, ) { - items( - items = state.items, - ) { model -> - val lang = model.lang + items(state.languages) { language -> SwitchPreferenceWidget( modifier = Modifier.animateItemPlacement(), - title = LocaleHelper.getSourceDisplayName(lang, LocalContext.current), - checked = model.enabled, - onCheckedChanged = { onClickLang(lang) }, + title = LocaleHelper.getSourceDisplayName(language, context), + checked = language in state.enabledLanguages, + onCheckedChanged = { onClickLang(language) }, ) } } diff --git a/app/src/main/java/eu/kanade/presentation/browse/ExtensionFilterState.kt b/app/src/main/java/eu/kanade/presentation/browse/ExtensionFilterState.kt deleted file mode 100644 index b5d03ca87b..0000000000 --- a/app/src/main/java/eu/kanade/presentation/browse/ExtensionFilterState.kt +++ /dev/null @@ -1,25 +0,0 @@ -package eu.kanade.presentation.browse - -import androidx.compose.runtime.Stable -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue -import eu.kanade.tachiyomi.ui.browse.extension.FilterUiModel - -@Stable -interface ExtensionFilterState { - val isLoading: Boolean - val items: List - val isEmpty: Boolean -} - -fun ExtensionFilterState(): ExtensionFilterState { - return ExtensionFilterStateImpl() -} - -class ExtensionFilterStateImpl : ExtensionFilterState { - override var isLoading: Boolean by mutableStateOf(true) - override var items: List by mutableStateOf(emptyList()) - override val isEmpty: Boolean by derivedStateOf { items.isEmpty() } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionFilterController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionFilterController.kt index fab410d769..ccebb88212 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionFilterController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionFilterController.kt @@ -1,20 +1,17 @@ package eu.kanade.tachiyomi.ui.browse.extension import androidx.compose.runtime.Composable -import eu.kanade.presentation.browse.ExtensionFilterScreen -import eu.kanade.tachiyomi.ui.base.controller.FullComposeController +import androidx.compose.runtime.CompositionLocalProvider +import cafe.adriel.voyager.navigator.Navigator +import eu.kanade.presentation.util.LocalRouter +import eu.kanade.tachiyomi.ui.base.controller.BasicFullComposeController -class ExtensionFilterController : FullComposeController() { - - override fun createPresenter() = ExtensionFilterPresenter() +class ExtensionFilterController : BasicFullComposeController() { @Composable override fun ComposeContent() { - ExtensionFilterScreen( - navigateUp = router::popCurrentController, - presenter = presenter, - ) + CompositionLocalProvider(LocalRouter provides router) { + Navigator(screen = ExtensionFilterScreen()) + } } } - -data class FilterUiModel(val lang: String, val enabled: Boolean) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionFilterPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionFilterPresenter.kt deleted file mode 100644 index 41f132a29d..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionFilterPresenter.kt +++ /dev/null @@ -1,57 +0,0 @@ -package eu.kanade.tachiyomi.ui.browse.extension - -import android.os.Bundle -import eu.kanade.domain.extension.interactor.GetExtensionLanguages -import eu.kanade.domain.source.interactor.ToggleLanguage -import eu.kanade.domain.source.service.SourcePreferences -import eu.kanade.presentation.browse.ExtensionFilterState -import eu.kanade.presentation.browse.ExtensionFilterStateImpl -import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter -import eu.kanade.tachiyomi.util.lang.launchIO -import eu.kanade.tachiyomi.util.system.logcat -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.receiveAsFlow -import logcat.LogPriority -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get - -class ExtensionFilterPresenter( - private val state: ExtensionFilterStateImpl = ExtensionFilterState() as ExtensionFilterStateImpl, - private val getExtensionLanguages: GetExtensionLanguages = Injekt.get(), - private val toggleLanguage: ToggleLanguage = Injekt.get(), - private val preferences: SourcePreferences = Injekt.get(), -) : BasePresenter(), ExtensionFilterState by state { - - private val _events = Channel(Int.MAX_VALUE) - val events = _events.receiveAsFlow() - - override fun onCreate(savedState: Bundle?) { - super.onCreate(savedState) - presenterScope.launchIO { - getExtensionLanguages.subscribe() - .catch { exception -> - logcat(LogPriority.ERROR, exception) - _events.send(Event.FailedFetchingLanguages) - } - .collectLatest(::collectLatestSourceLangMap) - } - } - - private fun collectLatestSourceLangMap(extLangs: List) { - val enabledLanguages = preferences.enabledLanguages().get() - state.items = extLangs.map { - FilterUiModel(it, it in enabledLanguages) - } - state.isLoading = false - } - - fun toggleLanguage(language: String) { - toggleLanguage.await(language) - } - - sealed class Event { - object FailedFetchingLanguages : Event() - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionFilterScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionFilterScreen.kt new file mode 100644 index 0000000000..928d47929d --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionFilterScreen.kt @@ -0,0 +1,50 @@ +package eu.kanade.tachiyomi.ui.browse.extension + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.platform.LocalContext +import cafe.adriel.voyager.core.model.rememberScreenModel +import cafe.adriel.voyager.core.screen.Screen +import cafe.adriel.voyager.navigator.currentOrThrow +import eu.kanade.presentation.browse.ExtensionFilterScreen +import eu.kanade.presentation.components.LoadingScreen +import eu.kanade.presentation.util.LocalRouter +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.util.system.toast +import kotlinx.coroutines.flow.collectLatest + +class ExtensionFilterScreen : Screen { + + @Composable + override fun Content() { + val context = LocalContext.current + val router = LocalRouter.currentOrThrow + val screenModel = rememberScreenModel { ExtensionFilterScreenModel() } + val state by screenModel.state.collectAsState() + + if (state is ExtensionFilterState.Loading) { + LoadingScreen() + return + } + + val successState = state as ExtensionFilterState.Success + + ExtensionFilterScreen( + navigateUp = router::popCurrentController, + state = successState, + onClickToggle = { screenModel.toggle(it) }, + ) + + LaunchedEffect(Unit) { + screenModel.events.collectLatest { + when (it) { + ExtensionFilterEvent.FailedFetchingLanguages -> { + context.toast(R.string.internal_error) + } + } + } + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionFilterScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionFilterScreenModel.kt new file mode 100644 index 0000000000..d5a93bd76a --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionFilterScreenModel.kt @@ -0,0 +1,75 @@ +package eu.kanade.tachiyomi.ui.browse.extension + +import androidx.compose.runtime.Immutable +import cafe.adriel.voyager.core.model.StateScreenModel +import cafe.adriel.voyager.core.model.coroutineScope +import eu.kanade.domain.extension.interactor.GetExtensionLanguages +import eu.kanade.domain.source.interactor.ToggleLanguage +import eu.kanade.domain.source.service.SourcePreferences +import eu.kanade.tachiyomi.util.system.logcat +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import logcat.LogPriority +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +class ExtensionFilterScreenModel( + private val preferences: SourcePreferences = Injekt.get(), + private val getExtensionLanguages: GetExtensionLanguages = Injekt.get(), + private val toggleLanguage: ToggleLanguage = Injekt.get(), +) : StateScreenModel(ExtensionFilterState.Loading) { + + private val _events: Channel = Channel() + val events: Flow = _events.receiveAsFlow() + + init { + coroutineScope.launch { + combine( + getExtensionLanguages.subscribe(), + preferences.enabledLanguages().changes(), + ) { a, b -> a to b } + .catch { throwable -> + logcat(LogPriority.ERROR, throwable) + _events.send(ExtensionFilterEvent.FailedFetchingLanguages) + } + .collectLatest { (extensionLanguages, enabledLanguages) -> + mutableState.update { + ExtensionFilterState.Success( + languages = extensionLanguages, + enabledLanguages = enabledLanguages, + ) + } + } + } + } + + fun toggle(language: String) { + toggleLanguage.await(language) + } +} + +sealed class ExtensionFilterEvent { + object FailedFetchingLanguages : ExtensionFilterEvent() +} + +sealed class ExtensionFilterState { + + @Immutable + object Loading : ExtensionFilterState() + + @Immutable + data class Success( + val languages: List, + val enabledLanguages: Set = emptySet(), + ) : ExtensionFilterState() { + + val isEmpty: Boolean + get() = languages.isEmpty() + } +}