From bdf035d60a039ac6aa38f3978691079082ff8e12 Mon Sep 17 00:00:00 2001 From: Andreas Date: Sat, 12 Nov 2022 15:47:19 +0100 Subject: [PATCH] Use Voyager on Source Filter screen (#8511) --- .../browse/SourcesFilterScreen.kt | 88 +++++++------------ .../presentation/browse/SourcesFilterState.kt | 23 ----- .../browse/source/SourcesFilterController.kt | 29 ++---- .../browse/source/SourcesFilterPresenter.kt | 73 --------------- .../ui/browse/source/SourcesFilterScreen.kt | 48 ++++++++++ .../browse/source/SourcesFilterScreenModel.kt | 77 ++++++++++++++++ 6 files changed, 167 insertions(+), 171 deletions(-) delete mode 100644 app/src/main/java/eu/kanade/presentation/browse/SourcesFilterState.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesFilterPresenter.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesFilterScreen.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesFilterScreenModel.kt diff --git a/app/src/main/java/eu/kanade/presentation/browse/SourcesFilterScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/SourcesFilterScreen.kt index c9d7c4d8b..65b5bf908 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/SourcesFilterScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/SourcesFilterScreen.kt @@ -5,7 +5,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.items import androidx.compose.material3.Checkbox 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 @@ -14,24 +13,19 @@ import eu.kanade.presentation.browse.components.BaseSourceItem 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.source.FilterUiModel -import eu.kanade.tachiyomi.ui.browse.source.SourcesFilterPresenter +import eu.kanade.tachiyomi.ui.browse.source.SourcesFilterState import eu.kanade.tachiyomi.util.system.LocaleHelper -import eu.kanade.tachiyomi.util.system.toast -import kotlinx.coroutines.flow.collectLatest @Composable fun SourcesFilterScreen( navigateUp: () -> Unit, - presenter: SourcesFilterPresenter, - onClickLang: (String) -> Unit, + state: SourcesFilterState.Success, + onClickLanguage: (String) -> Unit, onClickSource: (Source) -> Unit, ) { - val context = LocalContext.current Scaffold( topBar = { scrollBehavior -> AppBar( @@ -41,69 +35,55 @@ fun SourcesFilterScreen( ) }, ) { contentPadding -> - when { - presenter.isLoading -> LoadingScreen() - presenter.isEmpty -> EmptyScreen( + if (state.isEmpty) { + EmptyScreen( textResource = R.string.source_filter_empty_screen, modifier = Modifier.padding(contentPadding), ) - else -> { - SourcesFilterContent( - contentPadding = contentPadding, - state = presenter, - onClickLang = onClickLang, - onClickSource = onClickSource, - ) - } - } - } - LaunchedEffect(Unit) { - presenter.events.collectLatest { event -> - when (event) { - SourcesFilterPresenter.Event.FailedFetchingLanguages -> { - context.toast(R.string.internal_error) - } - } + return@Scaffold } + SourcesFilterContent( + contentPadding = contentPadding, + state = state, + onClickLanguage = onClickLanguage, + onClickSource = onClickSource, + ) } } @Composable private fun SourcesFilterContent( contentPadding: PaddingValues, - state: SourcesFilterState, - onClickLang: (String) -> Unit, + state: SourcesFilterState.Success, + onClickLanguage: (String) -> Unit, onClickSource: (Source) -> Unit, ) { FastScrollLazyColumn( contentPadding = contentPadding, ) { - items( - items = state.items, - contentType = { - when (it) { - is FilterUiModel.Header -> "header" - is FilterUiModel.Item -> "item" - } - }, - key = { - when (it) { - is FilterUiModel.Header -> it.hashCode() - is FilterUiModel.Item -> "source-filter-${it.source.key()}" - } - }, - ) { model -> - when (model) { - is FilterUiModel.Header -> SourcesFilterHeader( + state.items.forEach { (language, sources) -> + val enabled = language in state.enabledLanguages + item( + key = language.hashCode(), + contentType = "source-filter-header", + ) { + SourcesFilterHeader( modifier = Modifier.animateItemPlacement(), - language = model.language, - enabled = model.enabled, - onClickItem = onClickLang, + language = language, + enabled = enabled, + onClickItem = onClickLanguage, ) - is FilterUiModel.Item -> SourcesFilterItem( + } + if (!enabled) return@forEach + items( + items = sources, + key = { "source-filter-${it.key()}" }, + contentType = { "source-filter-item" }, + ) { source -> + SourcesFilterItem( modifier = Modifier.animateItemPlacement(), - source = model.source, - enabled = model.enabled, + source = source, + enabled = "${source.id}" !in state.disabledSources, onClickItem = onClickSource, ) } diff --git a/app/src/main/java/eu/kanade/presentation/browse/SourcesFilterState.kt b/app/src/main/java/eu/kanade/presentation/browse/SourcesFilterState.kt deleted file mode 100644 index 46668bcf7..000000000 --- a/app/src/main/java/eu/kanade/presentation/browse/SourcesFilterState.kt +++ /dev/null @@ -1,23 +0,0 @@ -package eu.kanade.presentation.browse - -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.source.FilterUiModel - -interface SourcesFilterState { - val isLoading: Boolean - val items: List - val isEmpty: Boolean -} - -fun SourcesFilterState(): SourcesFilterState { - return SourcesFilterStateImpl() -} - -class SourcesFilterStateImpl : SourcesFilterState { - 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/source/SourcesFilterController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesFilterController.kt index ed7bda0f3..227a257d7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesFilterController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesFilterController.kt @@ -1,30 +1,17 @@ package eu.kanade.tachiyomi.ui.browse.source import androidx.compose.runtime.Composable -import eu.kanade.domain.source.model.Source -import eu.kanade.presentation.browse.SourcesFilterScreen -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 SourceFilterController : FullComposeController() { - - override fun createPresenter(): SourcesFilterPresenter = SourcesFilterPresenter() +class SourceFilterController : BasicFullComposeController() { @Composable override fun ComposeContent() { - SourcesFilterScreen( - navigateUp = router::popCurrentController, - presenter = presenter, - onClickLang = { language -> - presenter.toggleLanguage(language) - }, - onClickSource = { source -> - presenter.toggleSource(source) - }, - ) + CompositionLocalProvider(LocalRouter provides router) { + Navigator(screen = SourcesFilterScreen()) + } } } - -sealed class FilterUiModel { - data class Header(val language: String, val enabled: Boolean) : FilterUiModel() - data class Item(val source: Source, val enabled: Boolean) : FilterUiModel() -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesFilterPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesFilterPresenter.kt deleted file mode 100644 index 5cf6e4a5a..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesFilterPresenter.kt +++ /dev/null @@ -1,73 +0,0 @@ -package eu.kanade.tachiyomi.ui.browse.source - -import android.os.Bundle -import eu.kanade.domain.source.interactor.GetLanguagesWithSources -import eu.kanade.domain.source.interactor.ToggleLanguage -import eu.kanade.domain.source.interactor.ToggleSource -import eu.kanade.domain.source.model.Source -import eu.kanade.domain.source.service.SourcePreferences -import eu.kanade.presentation.browse.SourcesFilterState -import eu.kanade.presentation.browse.SourcesFilterStateImpl -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 SourcesFilterPresenter( - private val state: SourcesFilterStateImpl = SourcesFilterState() as SourcesFilterStateImpl, - private val getLanguagesWithSources: GetLanguagesWithSources = Injekt.get(), - private val toggleSource: ToggleSource = Injekt.get(), - private val toggleLanguage: ToggleLanguage = Injekt.get(), - private val preferences: SourcePreferences = Injekt.get(), -) : BasePresenter(), SourcesFilterState by state { - - private val _events = Channel(Int.MAX_VALUE) - val events = _events.receiveAsFlow() - - override fun onCreate(savedState: Bundle?) { - super.onCreate(savedState) - - presenterScope.launchIO { - getLanguagesWithSources.subscribe() - .catch { exception -> - logcat(LogPriority.ERROR, exception) - _events.send(Event.FailedFetchingLanguages) - } - .collectLatest(::collectLatestSourceLangMap) - } - } - - private fun collectLatestSourceLangMap(sourceLangMap: Map>) { - state.items = sourceLangMap.flatMap { - val isLangEnabled = it.key in preferences.enabledLanguages().get() - val header = listOf(FilterUiModel.Header(it.key, isLangEnabled)) - - if (isLangEnabled.not()) return@flatMap header - header + it.value.map { source -> - FilterUiModel.Item( - source, - source.id.toString() !in preferences.disabledSources().get(), - ) - } - } - state.isLoading = false - } - - fun toggleSource(source: Source) { - toggleSource.await(source) - } - - 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/source/SourcesFilterScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesFilterScreen.kt new file mode 100644 index 000000000..846bf9ca4 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesFilterScreen.kt @@ -0,0 +1,48 @@ +package eu.kanade.tachiyomi.ui.browse.source + +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.SourcesFilterScreen +import eu.kanade.presentation.components.LoadingScreen +import eu.kanade.presentation.util.LocalRouter +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.util.system.toast + +class SourcesFilterScreen : Screen { + + @Composable + override fun Content() { + val router = LocalRouter.currentOrThrow + val screenModel = rememberScreenModel { SourcesFilterScreenModel() } + val state by screenModel.state.collectAsState() + + if (state is SourcesFilterState.Loading) { + LoadingScreen() + return + } + + if (state is SourcesFilterState.Error) { + val context = LocalContext.current + LaunchedEffect(Unit) { + context.toast(R.string.internal_error) + router.popCurrentController() + } + return + } + + val successState = state as SourcesFilterState.Success + + SourcesFilterScreen( + navigateUp = router::popCurrentController, + state = successState, + onClickLanguage = screenModel::toggleLanguage, + onClickSource = screenModel::toggleSource, + ) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesFilterScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesFilterScreenModel.kt new file mode 100644 index 000000000..2754cee6a --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesFilterScreenModel.kt @@ -0,0 +1,77 @@ +package eu.kanade.tachiyomi.ui.browse.source + +import cafe.adriel.voyager.core.model.StateScreenModel +import cafe.adriel.voyager.core.model.coroutineScope +import eu.kanade.domain.source.interactor.GetLanguagesWithSources +import eu.kanade.domain.source.interactor.ToggleLanguage +import eu.kanade.domain.source.interactor.ToggleSource +import eu.kanade.domain.source.model.Source +import eu.kanade.domain.source.service.SourcePreferences +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +class SourcesFilterScreenModel( + private val preferences: SourcePreferences = Injekt.get(), + private val getLanguagesWithSources: GetLanguagesWithSources = Injekt.get(), + private val toggleSource: ToggleSource = Injekt.get(), + private val toggleLanguage: ToggleLanguage = Injekt.get(), +) : StateScreenModel(SourcesFilterState.Loading) { + + init { + coroutineScope.launch { + combine( + getLanguagesWithSources.subscribe(), + preferences.enabledLanguages().changes(), + preferences.disabledSources().changes(), + ) { a, b, c -> Triple(a, b, c) } + .catch { throwable -> + mutableState.update { + SourcesFilterState.Error( + throwable = throwable, + ) + } + } + .collectLatest { (languagesWithSources, enabledLanguages, disabledSources) -> + mutableState.update { + SourcesFilterState.Success( + items = languagesWithSources, + enabledLanguages = enabledLanguages, + disabledSources = disabledSources, + ) + } + } + } + } + + fun toggleSource(source: Source) { + toggleSource.await(source) + } + + fun toggleLanguage(language: String) { + toggleLanguage.await(language) + } +} + +sealed class SourcesFilterState { + + object Loading : SourcesFilterState() + + data class Error( + val throwable: Throwable, + ) : SourcesFilterState() + + data class Success( + val items: Map>, + val enabledLanguages: Set, + val disabledSources: Set, + ) : SourcesFilterState() { + + val isEmpty: Boolean + get() = items.isEmpty() + } +}