Use Voyager on Source Filter screen (#8511)
This commit is contained in:
parent
0270878748
commit
bdf035d60a
@ -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(
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
SourcesFilterContent(
|
||||||
|
contentPadding = contentPadding,
|
||||||
|
state = state,
|
||||||
|
onClickLanguage = onClickLanguage,
|
||||||
|
onClickSource = onClickSource,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -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() }
|
|
||||||
}
|
|
@ -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()
|
|
||||||
}
|
|
||||||
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user