浏览代码

Use Voyager on Source Filter screen (#8511)

Andreas 2 年之前
父节点
当前提交
bdf035d60a

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

+ 0 - 23
app/src/main/java/eu/kanade/presentation/browse/SourcesFilterState.kt

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

+ 8 - 21
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<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()
-}

+ 0 - 73
app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesFilterPresenter.kt

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

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

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