mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-10-31 06:17:57 +01:00 
			
		
		
		
	Use Voyager on Source Filter screen (#8511)
This commit is contained in:
		| @@ -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, | ||||
|                 ) | ||||
|             } | ||||
|   | ||||
| @@ -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<FilterUiModel> | ||||
|     val isEmpty: Boolean | ||||
| } | ||||
|  | ||||
| fun SourcesFilterState(): SourcesFilterState { | ||||
|     return SourcesFilterStateImpl() | ||||
| } | ||||
|  | ||||
| class SourcesFilterStateImpl : SourcesFilterState { | ||||
|     override var isLoading: Boolean by mutableStateOf(true) | ||||
|     override var items: List<FilterUiModel> by mutableStateOf(emptyList()) | ||||
|     override val isEmpty: Boolean by derivedStateOf { items.isEmpty() } | ||||
| } | ||||
| @@ -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<SourcesFilterPresenter>() { | ||||
|  | ||||
|     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() | ||||
| } | ||||
|   | ||||
| @@ -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<SourceFilterController>(), SourcesFilterState by state { | ||||
|  | ||||
|     private val _events = Channel<Event>(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<String, List<Source>>) { | ||||
|         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() | ||||
|     } | ||||
| } | ||||
| @@ -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, | ||||
|         ) | ||||
|     } | ||||
| } | ||||
| @@ -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>(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<String, List<Source>>, | ||||
|         val enabledLanguages: Set<String>, | ||||
|         val disabledSources: Set<String>, | ||||
|     ) : SourcesFilterState() { | ||||
|  | ||||
|         val isEmpty: Boolean | ||||
|             get() = items.isEmpty() | ||||
|     } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user