Use Voyager on Extension Filter screen (#8503)
- Use sealed class for state - Minor changes
This commit is contained in:
parent
6ada3c90ff
commit
0270878748
6 changed files with 150 additions and 127 deletions
|
@ -4,28 +4,24 @@ import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import eu.kanade.presentation.components.AppBar
|
import eu.kanade.presentation.components.AppBar
|
||||||
import eu.kanade.presentation.components.EmptyScreen
|
import eu.kanade.presentation.components.EmptyScreen
|
||||||
import eu.kanade.presentation.components.FastScrollLazyColumn
|
import eu.kanade.presentation.components.FastScrollLazyColumn
|
||||||
import eu.kanade.presentation.components.LoadingScreen
|
|
||||||
import eu.kanade.presentation.components.Scaffold
|
import eu.kanade.presentation.components.Scaffold
|
||||||
import eu.kanade.presentation.more.settings.widget.SwitchPreferenceWidget
|
import eu.kanade.presentation.more.settings.widget.SwitchPreferenceWidget
|
||||||
import eu.kanade.tachiyomi.R
|
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.LocaleHelper
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ExtensionFilterScreen(
|
fun ExtensionFilterScreen(
|
||||||
navigateUp: () -> Unit,
|
navigateUp: () -> Unit,
|
||||||
presenter: ExtensionFilterPresenter,
|
state: ExtensionFilterState.Success,
|
||||||
|
onClickToggle: (String) -> Unit,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = { scrollBehavior ->
|
topBar = { scrollBehavior ->
|
||||||
AppBar(
|
AppBar(
|
||||||
|
@ -35,50 +31,37 @@ fun ExtensionFilterScreen(
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
) { contentPadding ->
|
) { contentPadding ->
|
||||||
when {
|
if (state.isEmpty) {
|
||||||
presenter.isLoading -> LoadingScreen()
|
EmptyScreen(
|
||||||
presenter.isEmpty -> EmptyScreen(
|
|
||||||
textResource = R.string.empty_screen,
|
textResource = R.string.empty_screen,
|
||||||
modifier = Modifier.padding(contentPadding),
|
modifier = Modifier.padding(contentPadding),
|
||||||
)
|
)
|
||||||
else -> ExtensionFilterContent(
|
return@Scaffold
|
||||||
contentPadding = contentPadding,
|
|
||||||
state = presenter,
|
|
||||||
onClickLang = {
|
|
||||||
presenter.toggleLanguage(it)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
presenter.events.collectLatest {
|
|
||||||
when (it) {
|
|
||||||
ExtensionFilterPresenter.Event.FailedFetchingLanguages -> {
|
|
||||||
context.toast(R.string.internal_error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
ExtensionFilterContent(
|
||||||
|
contentPadding = contentPadding,
|
||||||
|
state = state,
|
||||||
|
onClickLang = onClickToggle,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ExtensionFilterContent(
|
private fun ExtensionFilterContent(
|
||||||
contentPadding: PaddingValues,
|
contentPadding: PaddingValues,
|
||||||
state: ExtensionFilterState,
|
state: ExtensionFilterState.Success,
|
||||||
onClickLang: (String) -> Unit,
|
onClickLang: (String) -> Unit,
|
||||||
) {
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
FastScrollLazyColumn(
|
FastScrollLazyColumn(
|
||||||
contentPadding = contentPadding,
|
contentPadding = contentPadding,
|
||||||
) {
|
) {
|
||||||
items(
|
items(state.languages) { language ->
|
||||||
items = state.items,
|
|
||||||
) { model ->
|
|
||||||
val lang = model.lang
|
|
||||||
SwitchPreferenceWidget(
|
SwitchPreferenceWidget(
|
||||||
modifier = Modifier.animateItemPlacement(),
|
modifier = Modifier.animateItemPlacement(),
|
||||||
title = LocaleHelper.getSourceDisplayName(lang, LocalContext.current),
|
title = LocaleHelper.getSourceDisplayName(language, context),
|
||||||
checked = model.enabled,
|
checked = language in state.enabledLanguages,
|
||||||
onCheckedChanged = { onClickLang(lang) },
|
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
|
package eu.kanade.tachiyomi.ui.browse.extension
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import eu.kanade.presentation.browse.ExtensionFilterScreen
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.FullComposeController
|
import cafe.adriel.voyager.navigator.Navigator
|
||||||
|
import eu.kanade.presentation.util.LocalRouter
|
||||||
|
import eu.kanade.tachiyomi.ui.base.controller.BasicFullComposeController
|
||||||
|
|
||||||
class ExtensionFilterController : FullComposeController<ExtensionFilterPresenter>() {
|
class ExtensionFilterController : BasicFullComposeController() {
|
||||||
|
|
||||||
override fun createPresenter() = ExtensionFilterPresenter()
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun ComposeContent() {
|
override fun ComposeContent() {
|
||||||
ExtensionFilterScreen(
|
CompositionLocalProvider(LocalRouter provides router) {
|
||||||
navigateUp = router::popCurrentController,
|
Navigator(screen = ExtensionFilterScreen())
|
||||||
presenter = presenter,
|
}
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 a new issue