Use Voyager on Source Filter screen (#8511)

This commit is contained in:
Andreas 2022-11-12 15:47:19 +01:00 committed by GitHub
parent 0270878748
commit bdf035d60a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 167 additions and 171 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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