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 5225c46d3..d1eef52a3 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,7 @@ 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.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource @@ -14,27 +14,22 @@ 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, // SY --> onClickSources: (Boolean, List) -> Unit, // SY <-- ) { - val context = LocalContext.current Scaffold( topBar = { scrollBehavior -> AppBar( @@ -44,41 +39,30 @@ 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, - // SY --> - onClickSources = onClickSources, - // SY <-- - ) - } - } - } - 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, + // SY --> + onClickSources = onClickSources, + // SY <-- + ) } } @Composable private fun SourcesFilterContent( contentPadding: PaddingValues, - state: SourcesFilterState, - onClickLang: (String) -> Unit, + state: SourcesFilterState.Success, + onClickLanguage: (String) -> Unit, onClickSource: (Source) -> Unit, // SY --> onClickSources: (Boolean, List) -> Unit, @@ -87,44 +71,46 @@ private fun SourcesFilterContent( FastScrollLazyColumn( contentPadding = contentPadding, ) { - items( - items = state.items, - contentType = { - when (it) { - is FilterUiModel.Header -> "header" - // SY --> - is FilterUiModel.ToggleHeader -> "toggle" - // SY <-- - is FilterUiModel.Item -> "item" - } - }, - key = { - when (it) { - is FilterUiModel.Header, is FilterUiModel.ToggleHeader -> 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, ) - // SY --> - is FilterUiModel.ToggleHeader -> SourcesFilterToggle( + } + if (!enabled) return@forEach + // SY --> + item( + key = "toggle-$language", + contentType = "source-filter-toggle", + ) { + val toggleEnabled = remember(state.disabledSources) { + sources.none { it.id.toString() in state.disabledSources } + } + SourcesFilterToggle( modifier = Modifier.animateItemPlacement(), - isEnabled = model.enabled, + isEnabled = toggleEnabled, onClickItem = { - onClickSources(!model.enabled, model.sources) + onClickSources(!toggleEnabled, sources) }, ) - // SY <-- - is FilterUiModel.Item -> SourcesFilterItem( + } + // SY <-- + 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 02c9760a1..227a257d7 100755 --- 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,40 +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) - }, - // SY --> - onClickSources = { enable, sources -> - presenter.toggleSources(enable, sources) - }, - // SY <-- - ) + CompositionLocalProvider(LocalRouter provides router) { + Navigator(screen = SourcesFilterScreen()) + } } } - -sealed class FilterUiModel { - data class Header(val language: String, val enabled: Boolean) : FilterUiModel() - - // SY --> - data class ToggleHeader(val sources: List, val enabled: Boolean) : FilterUiModel() - - // SY <-- - 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 d62560d8a..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesFilterPresenter.kt +++ /dev/null @@ -1,88 +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)) - // SY --> - val disabledSources = preferences.disabledSources().get() - val toggleHeader = listOf( - FilterUiModel.ToggleHeader( - it.value, - it.value.none { (id) -> id.toString() in disabledSources }, - ), - ) - // SY <-- - - if (isLangEnabled.not()) return@flatMap header - header + toggleHeader + 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) - } - - // SY --> - fun toggleSources(enable: Boolean, sources: List) { - toggleSource.await(sources.map { it.id }, enable) - } - // SY <-- - - 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..16bb1df4a --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesFilterScreen.kt @@ -0,0 +1,51 @@ +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, + // SY --> + onClickSources = screenModel::toggleSources, + // SY <-- + ) + } +} 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..f7df8f3c2 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesFilterScreenModel.kt @@ -0,0 +1,83 @@ +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) + } + + // SY --> + fun toggleSources(enable: Boolean, sources: List) { + toggleSource.await(sources.map { it.id }, enable) + } + // SY <-- +} + +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() + } +}