Use Voyager on Category screen (#8472)

This commit is contained in:
Andreas 2022-11-08 04:13:14 +01:00 committed by GitHub
parent 9c9357639a
commit bf9edda04c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 253 additions and 204 deletions

View file

@ -4,31 +4,28 @@ 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.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import eu.kanade.domain.category.model.Category
import eu.kanade.presentation.category.components.CategoryContent
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.CategoryRenameDialog
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.CategoryPresenter
import eu.kanade.tachiyomi.ui.category.CategoryPresenter.Dialog
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.flow.collectLatest
import eu.kanade.tachiyomi.ui.category.CategoryScreenState
@Composable
fun CategoryScreen(
presenter: CategoryPresenter,
state: CategoryScreenState.Success,
onClickCreate: () -> Unit,
onClickRename: (Category) -> Unit,
onClickDelete: (Category) -> Unit,
onClickMoveUp: (Category) -> Unit,
onClickMoveDown: (Category) -> Unit,
navigateUp: () -> Unit,
) {
val lazyListState = rememberLazyListState()
@ -43,63 +40,26 @@ fun CategoryScreen(
floatingActionButton = {
CategoryFloatingActionButton(
lazyListState = lazyListState,
onCreate = { presenter.dialog = Dialog.Create },
onCreate = onClickCreate,
)
},
) { paddingValues ->
val context = LocalContext.current
when {
presenter.isLoading -> LoadingScreen()
presenter.isEmpty -> EmptyScreen(
if (state.isEmpty) {
EmptyScreen(
textResource = R.string.information_empty_category,
modifier = Modifier.padding(paddingValues),
)
else -> {
CategoryContent(
state = presenter,
lazyListState = lazyListState,
paddingValues = paddingValues + topPaddingValues + PaddingValues(horizontal = horizontalPadding),
onMoveUp = { presenter.moveUp(it) },
onMoveDown = { presenter.moveDown(it) },
)
}
return@Scaffold
}
val onDismissRequest = { presenter.dialog = null }
when (val dialog = presenter.dialog) {
Dialog.Create -> {
CategoryCreateDialog(
onDismissRequest = onDismissRequest,
onCreate = { presenter.createCategory(it) },
)
}
is Dialog.Rename -> {
CategoryRenameDialog(
onDismissRequest = onDismissRequest,
onRename = { presenter.renameCategory(dialog.category, it) },
category = dialog.category,
)
}
is Dialog.Delete -> {
CategoryDeleteDialog(
onDismissRequest = onDismissRequest,
onDelete = { presenter.deleteCategory(dialog.category) },
category = dialog.category,
)
}
else -> {}
}
LaunchedEffect(Unit) {
presenter.events.collectLatest { event ->
when (event) {
is CategoryPresenter.Event.CategoryWithNameAlreadyExists -> {
context.toast(R.string.error_category_exists)
}
is CategoryPresenter.Event.InternalError -> {
context.toast(R.string.internal_error)
}
}
}
}
CategoryContent(
categories = state.categories,
lazyListState = lazyListState,
paddingValues = paddingValues + topPaddingValues + PaddingValues(horizontal = horizontalPadding),
onClickRename = onClickRename,
onClickDelete = onClickDelete,
onMoveUp = onClickMoveUp,
onMoveDown = onClickMoveDown,
)
}
}

View file

@ -1,28 +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.domain.category.model.Category
import eu.kanade.tachiyomi.ui.category.CategoryPresenter
@Stable
interface CategoryState {
val isLoading: Boolean
var dialog: CategoryPresenter.Dialog?
val categories: List<Category>
val isEmpty: Boolean
}
fun CategoryState(): CategoryState {
return CategoryStateImpl()
}
class CategoryStateImpl : CategoryState {
override var isLoading: Boolean by mutableStateOf(true)
override var dialog: CategoryPresenter.Dialog? by mutableStateOf(null)
override var categories: List<Category> by mutableStateOf(emptyList())
override val isEmpty: Boolean by derivedStateOf { categories.isEmpty() }
}

View file

@ -8,19 +8,18 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import eu.kanade.domain.category.model.Category
import eu.kanade.presentation.category.CategoryState
import eu.kanade.presentation.components.LazyColumn
import eu.kanade.tachiyomi.ui.category.CategoryPresenter.Dialog
@Composable
fun CategoryContent(
state: CategoryState,
categories: List<Category>,
lazyListState: LazyListState,
paddingValues: PaddingValues,
onClickRename: (Category) -> Unit,
onClickDelete: (Category) -> Unit,
onMoveUp: (Category) -> Unit,
onMoveDown: (Category) -> Unit,
) {
val categories = state.categories
LazyColumn(
state = lazyListState,
contentPadding = paddingValues,
@ -37,8 +36,8 @@ fun CategoryContent(
canMoveDown = index != categories.lastIndex,
onMoveUp = onMoveUp,
onMoveDown = onMoveDown,
onRename = { state.dialog = Dialog.Rename(category) },
onDelete = { state.dialog = Dialog.Delete(category) },
onRename = { onClickRename(category) },
onDelete = { onClickDelete(category) },
)
}
}

View file

@ -1,18 +1,17 @@
package eu.kanade.tachiyomi.ui.category
import androidx.compose.runtime.Composable
import eu.kanade.presentation.category.CategoryScreen
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 CategoryController : FullComposeController<CategoryPresenter>() {
override fun createPresenter() = CategoryPresenter()
class CategoryController : BasicFullComposeController() {
@Composable
override fun ComposeContent() {
CategoryScreen(
presenter = presenter,
navigateUp = router::popCurrentController,
)
CompositionLocalProvider(LocalRouter provides router) {
Navigator(screen = CategoryScreen())
}
}
}

View file

@ -1,100 +0,0 @@
package eu.kanade.tachiyomi.ui.category
import android.os.Bundle
import eu.kanade.domain.category.interactor.CreateCategoryWithName
import eu.kanade.domain.category.interactor.DeleteCategory
import eu.kanade.domain.category.interactor.GetCategories
import eu.kanade.domain.category.interactor.RenameCategory
import eu.kanade.domain.category.interactor.ReorderCategory
import eu.kanade.domain.category.model.Category
import eu.kanade.presentation.category.CategoryState
import eu.kanade.presentation.category.CategoryStateImpl
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
class CategoryPresenter(
private val state: CategoryStateImpl = CategoryState() as CategoryStateImpl,
private val getCategories: GetCategories = Injekt.get(),
private val createCategoryWithName: CreateCategoryWithName = Injekt.get(),
private val renameCategory: RenameCategory = Injekt.get(),
private val reorderCategory: ReorderCategory = Injekt.get(),
private val deleteCategory: DeleteCategory = Injekt.get(),
) : BasePresenter<CategoryController>(), CategoryState 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 {
getCategories.subscribe()
.collectLatest {
state.isLoading = false
state.categories = it.filterNot(Category::isSystemCategory)
}
}
}
fun createCategory(name: String) {
presenterScope.launchIO {
when (createCategoryWithName.await(name)) {
is CreateCategoryWithName.Result.NameAlreadyExistsError -> _events.send(Event.CategoryWithNameAlreadyExists)
is CreateCategoryWithName.Result.InternalError -> _events.send(Event.InternalError)
else -> {}
}
}
}
fun deleteCategory(category: Category) {
presenterScope.launchIO {
when (deleteCategory.await(category.id)) {
is DeleteCategory.Result.InternalError -> _events.send(Event.InternalError)
else -> {}
}
}
}
fun moveUp(category: Category) {
presenterScope.launchIO {
when (reorderCategory.await(category, category.order - 1)) {
is ReorderCategory.Result.InternalError -> _events.send(Event.InternalError)
else -> {}
}
}
}
fun moveDown(category: Category) {
presenterScope.launchIO {
when (reorderCategory.await(category, category.order + 1)) {
is ReorderCategory.Result.InternalError -> _events.send(Event.InternalError)
else -> {}
}
}
}
fun renameCategory(category: Category, name: String) {
presenterScope.launchIO {
when (renameCategory.await(category, name)) {
RenameCategory.Result.NameAlreadyExistsError -> _events.send(Event.CategoryWithNameAlreadyExists)
is RenameCategory.Result.InternalError -> _events.send(Event.InternalError)
else -> {}
}
}
}
sealed class Dialog {
object Create : Dialog()
data class Rename(val category: Category) : Dialog()
data class Delete(val category: Category) : Dialog()
}
sealed class Event {
object CategoryWithNameAlreadyExists : Event()
object InternalError : Event()
}
}

View file

@ -0,0 +1,79 @@
package eu.kanade.tachiyomi.ui.category
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.category.CategoryScreen
import eu.kanade.presentation.category.components.CategoryCreateDialog
import eu.kanade.presentation.category.components.CategoryDeleteDialog
import eu.kanade.presentation.category.components.CategoryRenameDialog
import eu.kanade.presentation.components.LoadingScreen
import eu.kanade.presentation.util.LocalRouter
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.flow.collectLatest
class CategoryScreen : Screen {
@Composable
override fun Content() {
val context = LocalContext.current
val router = LocalRouter.currentOrThrow
val screenModel = rememberScreenModel { CategoryScreenModel() }
val state by screenModel.state.collectAsState()
if (state is CategoryScreenState.Loading) {
LoadingScreen()
return
}
val successState = state as CategoryScreenState.Success
CategoryScreen(
state = successState,
onClickCreate = { screenModel.showDialog(CategoryDialog.Create) },
onClickRename = { screenModel.showDialog(CategoryDialog.Rename(it)) },
onClickDelete = { screenModel.showDialog(CategoryDialog.Delete(it)) },
onClickMoveUp = screenModel::moveUp,
onClickMoveDown = screenModel::moveDown,
navigateUp = router::popCurrentController,
)
when (val dialog = successState.dialog) {
null -> {}
CategoryDialog.Create -> {
CategoryCreateDialog(
onDismissRequest = screenModel::dismissDialog,
onCreate = { screenModel.createCategory(it) },
)
}
is CategoryDialog.Rename -> {
CategoryRenameDialog(
onDismissRequest = screenModel::dismissDialog,
onRename = { screenModel.renameCategory(dialog.category, it) },
category = dialog.category,
)
}
is CategoryDialog.Delete -> {
CategoryDeleteDialog(
onDismissRequest = screenModel::dismissDialog,
onDelete = { screenModel.deleteCategory(dialog.category.id) },
category = dialog.category,
)
}
}
LaunchedEffect(Unit) {
screenModel.events.collectLatest { event ->
if (event is CategoryEvent.LocalizedMessage) {
context.toast(event.stringRes)
}
}
}
}
}

View file

@ -0,0 +1,140 @@
package eu.kanade.tachiyomi.ui.category
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.category.interactor.CreateCategoryWithName
import eu.kanade.domain.category.interactor.DeleteCategory
import eu.kanade.domain.category.interactor.GetCategories
import eu.kanade.domain.category.interactor.RenameCategory
import eu.kanade.domain.category.interactor.ReorderCategory
import eu.kanade.domain.category.model.Category
import eu.kanade.tachiyomi.R
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.consumeAsFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class CategoryScreenModel(
private val getCategories: GetCategories = Injekt.get(),
private val createCategoryWithName: CreateCategoryWithName = Injekt.get(),
private val deleteCategory: DeleteCategory = Injekt.get(),
private val reorderCategory: ReorderCategory = Injekt.get(),
private val renameCategory: RenameCategory = Injekt.get(),
) : StateScreenModel<CategoryScreenState>(CategoryScreenState.Loading) {
private val _events: Channel<CategoryEvent> = Channel()
val events = _events.consumeAsFlow()
init {
coroutineScope.launch {
getCategories.subscribe()
.collectLatest { categories ->
mutableState.update {
CategoryScreenState.Success(
categories = categories.filterNot(Category::isSystemCategory),
)
}
}
}
}
fun createCategory(name: String) {
coroutineScope.launch {
when (createCategoryWithName.await(name)) {
is CreateCategoryWithName.Result.InternalError -> _events.send(CategoryEvent.InternalError)
CreateCategoryWithName.Result.NameAlreadyExistsError -> _events.send(CategoryEvent.CategoryWithNameAlreadyExists)
CreateCategoryWithName.Result.Success -> {}
}
}
}
fun deleteCategory(categoryId: Long) {
coroutineScope.launch {
when (deleteCategory.await(categoryId = categoryId)) {
is DeleteCategory.Result.InternalError -> _events.send(CategoryEvent.InternalError)
DeleteCategory.Result.Success -> {}
}
}
}
fun moveUp(category: Category) {
coroutineScope.launch {
when (reorderCategory.await(category, category.order - 1)) {
is ReorderCategory.Result.InternalError -> _events.send(CategoryEvent.InternalError)
ReorderCategory.Result.Success -> {}
ReorderCategory.Result.Unchanged -> {}
}
}
}
fun moveDown(category: Category) {
coroutineScope.launch {
when (reorderCategory.await(category, category.order + 1)) {
is ReorderCategory.Result.InternalError -> _events.send(CategoryEvent.InternalError)
ReorderCategory.Result.Success -> {}
ReorderCategory.Result.Unchanged -> {}
}
}
}
fun renameCategory(category: Category, name: String) {
coroutineScope.launch {
when (renameCategory.await(category, name)) {
is RenameCategory.Result.InternalError -> _events.send(CategoryEvent.InternalError)
RenameCategory.Result.NameAlreadyExistsError -> _events.send(CategoryEvent.CategoryWithNameAlreadyExists)
RenameCategory.Result.Success -> {}
}
}
}
fun showDialog(dialog: CategoryDialog) {
mutableState.update {
when (it) {
CategoryScreenState.Loading -> it
is CategoryScreenState.Success -> it.copy(dialog = dialog)
}
}
}
fun dismissDialog() {
mutableState.update {
when (it) {
CategoryScreenState.Loading -> it
is CategoryScreenState.Success -> it.copy(dialog = null)
}
}
}
}
sealed class CategoryDialog {
object Create : CategoryDialog()
data class Rename(val category: Category) : CategoryDialog()
data class Delete(val category: Category) : CategoryDialog()
}
sealed class CategoryEvent {
sealed class LocalizedMessage(@StringRes val stringRes: Int) : CategoryEvent()
object CategoryWithNameAlreadyExists : LocalizedMessage(R.string.error_category_exists)
object InternalError : LocalizedMessage(R.string.internal_error)
}
sealed class CategoryScreenState {
@Immutable
object Loading : CategoryScreenState()
@Immutable
data class Success(
val categories: List<Category>,
val dialog: CategoryDialog? = null,
) : CategoryScreenState() {
val isEmpty: Boolean
get() = categories.isEmpty()
}
}