mirror of
https://github.com/aniyomiorg/aniyomi.git
synced 2024-11-27 08:16:36 +03:00
Use Voyager on Source Filter screen (#8511)
This commit is contained in:
parent
0270878748
commit
bdf035d60a
6 changed files with 167 additions and 171 deletions
|
@ -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 -> {
|
||||
return@Scaffold
|
||||
}
|
||||
SourcesFilterContent(
|
||||
contentPadding = contentPadding,
|
||||
state = presenter,
|
||||
onClickLang = onClickLang,
|
||||
state = state,
|
||||
onClickLanguage = onClickLanguage,
|
||||
onClickSource = onClickSource,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
presenter.events.collectLatest { event ->
|
||||
when (event) {
|
||||
SourcesFilterPresenter.Event.FailedFetchingLanguages -> {
|
||||
context.toast(R.string.internal_error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
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()
|
||||
}
|
||||
|
|
|
@ -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 a new issue