Use Voyager on Extension Filter screen (#8503)

- Use sealed class for state
- Minor changes
This commit is contained in:
Andreas 2022-11-11 22:57:31 +01:00 committed by GitHub
parent 6ada3c90ff
commit 0270878748
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 150 additions and 127 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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