mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-10-30 22:07:57 +01:00 
			
		
		
		
	Use Voyager on Extension Filter screen (#8503)
- Use sealed class for state - Minor changes
This commit is contained in:
		| @@ -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) }, | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
|   | ||||
| @@ -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<FilterUiModel> | ||||
|     val isEmpty: Boolean | ||||
| } | ||||
|  | ||||
| fun ExtensionFilterState(): ExtensionFilterState { | ||||
|     return ExtensionFilterStateImpl() | ||||
| } | ||||
|  | ||||
| class ExtensionFilterStateImpl : ExtensionFilterState { | ||||
|     override var isLoading: Boolean by mutableStateOf(true) | ||||
|     override var items: List<FilterUiModel> by mutableStateOf(emptyList()) | ||||
|     override val isEmpty: Boolean by derivedStateOf { items.isEmpty() } | ||||
| } | ||||
| @@ -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<ExtensionFilterPresenter>() { | ||||
|  | ||||
|     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) | ||||
|   | ||||
| @@ -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<ExtensionFilterController>(), ExtensionFilterState by state { | ||||
|  | ||||
|     private val _events = Channel<Event>(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<String>) { | ||||
|         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() | ||||
|     } | ||||
| } | ||||
| @@ -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) | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -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>(ExtensionFilterState.Loading) { | ||||
|  | ||||
|     private val _events: Channel<ExtensionFilterEvent> = Channel() | ||||
|     val events: Flow<ExtensionFilterEvent> = _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<String>, | ||||
|         val enabledLanguages: Set<String> = emptySet(), | ||||
|     ) : ExtensionFilterState() { | ||||
|  | ||||
|         val isEmpty: Boolean | ||||
|             get() = languages.isEmpty() | ||||
|     } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user