Use Voyager on Sort Tags screen

This commit is contained in:
Jobobby04 2022-11-10 23:48:03 -05:00
parent 4a1a1301ff
commit fd99a5f502
7 changed files with 232 additions and 183 deletions

View File

@ -1,31 +1,29 @@
package eu.kanade.presentation.category
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import eu.kanade.presentation.category.components.CategoryCreateDialog
import eu.kanade.presentation.category.components.CategoryDeleteDialog
import eu.kanade.presentation.category.components.CategoryFloatingActionButton
import eu.kanade.presentation.category.components.genre.SortTagContent
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.EmptyScreen
import eu.kanade.presentation.components.LoadingScreen
import eu.kanade.presentation.components.Scaffold
import eu.kanade.presentation.util.horizontalPadding
import eu.kanade.presentation.util.plus
import eu.kanade.presentation.util.topPaddingValues
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.category.genre.SortTagPresenter
import eu.kanade.tachiyomi.ui.category.genre.SortTagPresenter.Dialog
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.flow.collectLatest
import eu.kanade.tachiyomi.ui.category.genre.SortTagScreenState
@Composable
fun SortTagScreen(
presenter: SortTagPresenter,
state: SortTagScreenState.Success,
onClickCreate: () -> Unit,
onClickDelete: (String) -> Unit,
onClickMoveUp: (String, Int) -> Unit,
onClickMoveDown: (String, Int) -> Unit,
navigateUp: () -> Unit,
) {
val lazyListState = rememberLazyListState()
@ -40,56 +38,25 @@ fun SortTagScreen(
floatingActionButton = {
CategoryFloatingActionButton(
lazyListState = lazyListState,
onCreate = { presenter.dialog = Dialog.Create },
onCreate = onClickCreate,
)
},
) { paddingValues ->
val context = LocalContext.current
when {
presenter.isLoading -> LoadingScreen()
presenter.isEmpty -> EmptyScreen(textResource = R.string.information_empty_category)
else -> {
SortTagContent(
state = presenter,
lazyListState = lazyListState,
paddingValues = paddingValues + topPaddingValues + PaddingValues(horizontal = horizontalPadding),
onMoveUp = { tag, index -> presenter.moveUp(tag, index) },
onMoveDown = { tag, index -> presenter.moveDown(tag, index) },
)
}
if (state.isEmpty) {
EmptyScreen(
textResource = R.string.information_empty_category,
modifier = Modifier.padding(paddingValues),
)
return@Scaffold
}
val onDismissRequest = { presenter.dialog = null }
when (val dialog = presenter.dialog) {
Dialog.Create -> {
CategoryCreateDialog(
onDismissRequest = onDismissRequest,
onCreate = { presenter.createTag(it) },
title = stringResource(R.string.add_tag),
extraMessage = stringResource(R.string.action_add_tags_message),
)
}
is Dialog.Delete -> {
CategoryDeleteDialog(
onDismissRequest = onDismissRequest,
onDelete = { presenter.delete(dialog.tag) },
title = stringResource(R.string.delete_tag),
text = stringResource(R.string.delete_tag_confirmation, dialog.tag),
)
}
else -> {}
}
LaunchedEffect(Unit) {
presenter.events.collectLatest { event ->
when (event) {
is SortTagPresenter.Event.TagExists -> {
context.toast(R.string.error_tag_exists)
}
is SortTagPresenter.Event.InternalError -> {
context.toast(R.string.internal_error)
}
}
}
}
SortTagContent(
tags = state.tags,
lazyListState = lazyListState,
paddingValues = paddingValues + topPaddingValues + PaddingValues(horizontal = horizontalPadding),
onClickDelete = onClickDelete,
onMoveUp = onClickMoveUp,
onMoveDown = onClickMoveDown,
)
}
}

View File

@ -1,27 +0,0 @@
package eu.kanade.presentation.category
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.category.genre.SortTagPresenter
@Stable
interface SortTagState {
val isLoading: Boolean
var dialog: SortTagPresenter.Dialog?
val tags: List<String>
val isEmpty: Boolean
}
fun SortTagState(): SortTagState {
return SortTagStateImpl()
}
class SortTagStateImpl : SortTagState {
override var isLoading: Boolean by mutableStateOf(true)
override var dialog: SortTagPresenter.Dialog? by mutableStateOf(null)
override var tags: List<String> by mutableStateOf(emptyList())
override val isEmpty: Boolean by derivedStateOf { tags.isEmpty() }
}

View File

@ -7,25 +7,23 @@ import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import eu.kanade.presentation.category.SortTagState
import eu.kanade.presentation.components.LazyColumn
import eu.kanade.tachiyomi.ui.category.genre.SortTagPresenter
@Composable
fun SortTagContent(
state: SortTagState,
tags: List<String>,
lazyListState: LazyListState,
paddingValues: PaddingValues,
onClickDelete: (String) -> Unit,
onMoveUp: (String, Int) -> Unit,
onMoveDown: (String, Int) -> Unit,
) {
val tags = state.tags
LazyColumn(
state = lazyListState,
contentPadding = paddingValues,
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
itemsIndexed(tags) { index, tag ->
itemsIndexed(tags, key = { _, tag -> tag }) { index, tag ->
SortTagListItem(
modifier = Modifier.animateItemPlacement(),
tag = tag,
@ -33,7 +31,7 @@ fun SortTagContent(
canMoveDown = index != tags.lastIndex,
onMoveUp = { onMoveUp(tag, index) },
onMoveDown = { onMoveDown(tag, index) },
onDelete = { state.dialog = SortTagPresenter.Dialog.Delete(tag) },
onDelete = { onClickDelete(tag) },
)
}
}

View File

@ -1,21 +1,20 @@
package eu.kanade.tachiyomi.ui.category.genre
import androidx.compose.runtime.Composable
import eu.kanade.presentation.category.SortTagScreen
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
/**
* Controller to manage the categories for the users' library.
*/
class SortTagController : FullComposeController<SortTagPresenter>() {
override fun createPresenter() = SortTagPresenter()
class SortTagController : BasicFullComposeController() {
@Composable
override fun ComposeContent() {
SortTagScreen(
presenter = presenter,
navigateUp = router::popCurrentController,
)
CompositionLocalProvider(LocalRouter provides router) {
Navigator(screen = SortTagScreen())
}
}
}

View File

@ -1,85 +0,0 @@
package eu.kanade.tachiyomi.ui.category.genre
import android.os.Bundle
import eu.kanade.domain.manga.interactor.CreateSortTag
import eu.kanade.domain.manga.interactor.DeleteSortTag
import eu.kanade.domain.manga.interactor.GetSortTag
import eu.kanade.domain.manga.interactor.ReorderSortTag
import eu.kanade.presentation.category.SortTagState
import eu.kanade.presentation.category.SortTagStateImpl
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.util.lang.launchIO
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.consumeAsFlow
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
/**
* Presenter of [SortTagController]. Used to manage the categories of the library.
*/
class SortTagPresenter(
private val state: SortTagStateImpl = SortTagState() as SortTagStateImpl,
private val getSortTag: GetSortTag = Injekt.get(),
private val createSortTag: CreateSortTag = Injekt.get(),
private val deleteSortTag: DeleteSortTag = Injekt.get(),
private val reorderSortTag: ReorderSortTag = Injekt.get(),
) : BasePresenter<SortTagController>(), SortTagState by state {
private val _events: Channel<Event> = Channel(Int.MAX_VALUE)
val events = _events.consumeAsFlow()
override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState)
presenterScope.launchIO {
getSortTag.subscribe()
.collectLatest {
state.isLoading = false
state.tags = it
}
}
}
fun createTag(name: String) {
presenterScope.launchIO {
when (createSortTag.await(name)) {
is CreateSortTag.Result.TagExists -> _events.send(Event.TagExists)
else -> {}
}
}
}
fun delete(tag: String) {
presenterScope.launchIO {
deleteSortTag.await(tag)
}
}
fun moveUp(tag: String, index: Int) {
presenterScope.launchIO {
when (reorderSortTag.await(tag, index - 1)) {
is ReorderSortTag.Result.InternalError -> _events.send(Event.InternalError)
else -> {}
}
}
}
fun moveDown(tag: String, index: Int) {
presenterScope.launchIO {
when (reorderSortTag.await(tag, index + 1)) {
is ReorderSortTag.Result.InternalError -> _events.send(Event.InternalError)
else -> {}
}
}
}
sealed class Event {
object TagExists : Event()
object InternalError : Event()
}
sealed class Dialog {
object Create : Dialog()
data class Delete(val tag: String) : Dialog()
}
}

View File

@ -0,0 +1,74 @@
package eu.kanade.tachiyomi.ui.category.genre
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 androidx.compose.ui.res.stringResource
import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.presentation.category.components.CategoryCreateDialog
import eu.kanade.presentation.category.components.CategoryDeleteDialog
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 SortTagScreen : Screen {
@Composable
override fun Content() {
val context = LocalContext.current
val router = LocalRouter.currentOrThrow
val screenModel = rememberScreenModel { SortTagScreenModel() }
val state by screenModel.state.collectAsState()
if (state is SortTagScreenState.Loading) {
LoadingScreen()
return
}
val successState = state as SortTagScreenState.Success
eu.kanade.presentation.category.SortTagScreen(
state = successState,
onClickCreate = { screenModel.showDialog(SortTagDialog.Create) },
onClickDelete = { screenModel.showDialog(SortTagDialog.Delete(it)) },
onClickMoveUp = screenModel::moveUp,
onClickMoveDown = screenModel::moveDown,
navigateUp = router::popCurrentController,
)
when (val dialog = successState.dialog) {
null -> {}
SortTagDialog.Create -> {
CategoryCreateDialog(
onDismissRequest = screenModel::dismissDialog,
onCreate = { screenModel.createTag(it) },
title = stringResource(R.string.add_tag),
extraMessage = stringResource(R.string.action_add_tags_message),
)
}
is SortTagDialog.Delete -> {
CategoryDeleteDialog(
onDismissRequest = screenModel::dismissDialog,
onDelete = { screenModel.delete(dialog.tag) },
title = stringResource(R.string.delete_tag),
text = stringResource(R.string.delete_tag_confirmation, dialog.tag),
)
}
}
LaunchedEffect(Unit) {
screenModel.events.collectLatest { event ->
if (event is SortTagEvent.LocalizedMessage) {
context.toast(event.stringRes)
}
}
}
}
}

View File

@ -0,0 +1,123 @@
package eu.kanade.tachiyomi.ui.category.genre
import androidx.annotation.StringRes
import androidx.compose.runtime.Immutable
import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.coroutineScope
import eu.kanade.domain.manga.interactor.CreateSortTag
import eu.kanade.domain.manga.interactor.DeleteSortTag
import eu.kanade.domain.manga.interactor.GetSortTag
import eu.kanade.domain.manga.interactor.ReorderSortTag
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.lang.launchIO
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.consumeAsFlow
import kotlinx.coroutines.flow.update
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
/**
* Presenter of [SortTagController]. Used to manage the categories of the library.
*/
class SortTagScreenModel(
private val getSortTag: GetSortTag = Injekt.get(),
private val createSortTag: CreateSortTag = Injekt.get(),
private val deleteSortTag: DeleteSortTag = Injekt.get(),
private val reorderSortTag: ReorderSortTag = Injekt.get(),
) : StateScreenModel<SortTagScreenState>(SortTagScreenState.Loading) {
private val _events: Channel<SortTagEvent> = Channel(Int.MAX_VALUE)
val events = _events.consumeAsFlow()
init {
coroutineScope.launchIO {
getSortTag.subscribe()
.collectLatest { tags ->
mutableState.update {
SortTagScreenState.Success(
tags = tags,
)
}
}
}
}
fun createTag(name: String) {
coroutineScope.launchIO {
when (createSortTag.await(name)) {
is CreateSortTag.Result.TagExists -> _events.send(SortTagEvent.TagExists)
else -> {}
}
}
}
fun delete(tag: String) {
coroutineScope.launchIO {
deleteSortTag.await(tag)
}
}
fun moveUp(tag: String, index: Int) {
coroutineScope.launchIO {
when (reorderSortTag.await(tag, index - 1)) {
is ReorderSortTag.Result.InternalError -> _events.send(SortTagEvent.InternalError)
else -> {}
}
}
}
fun moveDown(tag: String, index: Int) {
coroutineScope.launchIO {
when (reorderSortTag.await(tag, index + 1)) {
is ReorderSortTag.Result.InternalError -> _events.send(SortTagEvent.InternalError)
else -> {}
}
}
}
fun showDialog(dialog: SortTagDialog) {
mutableState.update {
when (it) {
SortTagScreenState.Loading -> it
is SortTagScreenState.Success -> it.copy(dialog = dialog)
}
}
}
fun dismissDialog() {
mutableState.update {
when (it) {
SortTagScreenState.Loading -> it
is SortTagScreenState.Success -> it.copy(dialog = null)
}
}
}
}
sealed class SortTagEvent {
sealed class LocalizedMessage(@StringRes val stringRes: Int) : SortTagEvent()
object TagExists : LocalizedMessage(R.string.error_tag_exists)
object InternalError : LocalizedMessage(R.string.internal_error)
}
sealed class SortTagDialog {
object Create : SortTagDialog()
data class Delete(val tag: String) : SortTagDialog()
}
sealed class SortTagScreenState {
@Immutable
object Loading : SortTagScreenState()
@Immutable
data class Success(
val tags: List<String>,
val dialog: SortTagDialog? = null,
) : SortTagScreenState() {
val isEmpty: Boolean
get() = tags.isEmpty()
}
}