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.foundation.lazy.items
import androidx.compose.material3.Checkbox import androidx.compose.material3.Checkbox
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
@ -14,24 +13,19 @@ import eu.kanade.presentation.browse.components.BaseSourceItem
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.source.FilterUiModel import eu.kanade.tachiyomi.ui.browse.source.SourcesFilterState
import eu.kanade.tachiyomi.ui.browse.source.SourcesFilterPresenter
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 SourcesFilterScreen( fun SourcesFilterScreen(
navigateUp: () -> Unit, navigateUp: () -> Unit,
presenter: SourcesFilterPresenter, state: SourcesFilterState.Success,
onClickLang: (String) -> Unit, onClickLanguage: (String) -> Unit,
onClickSource: (Source) -> Unit, onClickSource: (Source) -> Unit,
) { ) {
val context = LocalContext.current
Scaffold( Scaffold(
topBar = { scrollBehavior -> topBar = { scrollBehavior ->
AppBar( AppBar(
@ -41,69 +35,55 @@ fun SourcesFilterScreen(
) )
}, },
) { contentPadding -> ) { contentPadding ->
when { if (state.isEmpty) {
presenter.isLoading -> LoadingScreen() EmptyScreen(
presenter.isEmpty -> EmptyScreen(
textResource = R.string.source_filter_empty_screen, textResource = R.string.source_filter_empty_screen,
modifier = Modifier.padding(contentPadding), modifier = Modifier.padding(contentPadding),
) )
else -> { return@Scaffold
}
SourcesFilterContent( SourcesFilterContent(
contentPadding = contentPadding, contentPadding = contentPadding,
state = presenter, state = state,
onClickLang = onClickLang, onClickLanguage = onClickLanguage,
onClickSource = onClickSource, onClickSource = onClickSource,
) )
} }
}
}
LaunchedEffect(Unit) {
presenter.events.collectLatest { event ->
when (event) {
SourcesFilterPresenter.Event.FailedFetchingLanguages -> {
context.toast(R.string.internal_error)
}
}
}
}
} }
@Composable @Composable
private fun SourcesFilterContent( private fun SourcesFilterContent(
contentPadding: PaddingValues, contentPadding: PaddingValues,
state: SourcesFilterState, state: SourcesFilterState.Success,
onClickLang: (String) -> Unit, onClickLanguage: (String) -> Unit,
onClickSource: (Source) -> Unit, onClickSource: (Source) -> Unit,
) { ) {
FastScrollLazyColumn( FastScrollLazyColumn(
contentPadding = contentPadding, contentPadding = contentPadding,
) { ) {
items( state.items.forEach { (language, sources) ->
items = state.items, val enabled = language in state.enabledLanguages
contentType = { item(
when (it) { key = language.hashCode(),
is FilterUiModel.Header -> "header" contentType = "source-filter-header",
is FilterUiModel.Item -> "item" ) {
} SourcesFilterHeader(
},
key = {
when (it) {
is FilterUiModel.Header -> it.hashCode()
is FilterUiModel.Item -> "source-filter-${it.source.key()}"
}
},
) { model ->
when (model) {
is FilterUiModel.Header -> SourcesFilterHeader(
modifier = Modifier.animateItemPlacement(), modifier = Modifier.animateItemPlacement(),
language = model.language, language = language,
enabled = model.enabled, enabled = enabled,
onClickItem = onClickLang, 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(), modifier = Modifier.animateItemPlacement(),
source = model.source, source = source,
enabled = model.enabled, enabled = "${source.id}" !in state.disabledSources,
onClickItem = onClickSource, 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 package eu.kanade.tachiyomi.ui.browse.source
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import eu.kanade.domain.source.model.Source import androidx.compose.runtime.CompositionLocalProvider
import eu.kanade.presentation.browse.SourcesFilterScreen import cafe.adriel.voyager.navigator.Navigator
import eu.kanade.tachiyomi.ui.base.controller.FullComposeController import eu.kanade.presentation.util.LocalRouter
import eu.kanade.tachiyomi.ui.base.controller.BasicFullComposeController
class SourceFilterController : FullComposeController<SourcesFilterPresenter>() { class SourceFilterController : BasicFullComposeController() {
override fun createPresenter(): SourcesFilterPresenter = SourcesFilterPresenter()
@Composable @Composable
override fun ComposeContent() { override fun ComposeContent() {
SourcesFilterScreen( CompositionLocalProvider(LocalRouter provides router) {
navigateUp = router::popCurrentController, Navigator(screen = SourcesFilterScreen())
presenter = presenter, }
onClickLang = { language ->
presenter.toggleLanguage(language)
},
onClickSource = { source ->
presenter.toggleSource(source)
},
)
} }
} }
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()
}
}