Browse Source

Use Voyager on Extension Filter screen (#8503)

- Use sealed class for state
- Minor changes
Andreas 2 years ago
parent
commit
0270878748

+ 17 - 34
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) },
             )
         }
     }

+ 0 - 25
app/src/main/java/eu/kanade/presentation/browse/ExtensionFilterState.kt

@@ -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() }
-}

+ 8 - 11
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<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)

+ 0 - 57
app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionFilterPresenter.kt

@@ -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()
-    }
-}

+ 50 - 0
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)
+                    }
+                }
+            }
+        }
+    }
+}

+ 75 - 0
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>(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()
+    }
+}