Use Voyager on Source Category screen

This commit is contained in:
Jobobby04 2022-11-10 23:17:20 -05:00
parent 0d638b1c1e
commit 02954670d4
7 changed files with 256 additions and 203 deletions

View File

@ -1,32 +1,28 @@
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.CategoryRenameDialog
import eu.kanade.presentation.category.components.sources.SourceCategoryContent
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.sources.SourceCategoryPresenter
import eu.kanade.tachiyomi.ui.category.sources.SourceCategoryPresenter.Dialog
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.flow.collectLatest
import eu.kanade.tachiyomi.ui.category.sources.SourceCategoryScreenState
@Composable
fun SourceCategoryScreen(
presenter: SourceCategoryPresenter,
state: SourceCategoryScreenState.Success,
onClickCreate: () -> Unit,
onClickRename: (String) -> Unit,
onClickDelete: (String) -> Unit,
navigateUp: () -> Unit,
) {
val lazyListState = rememberLazyListState()
@ -41,63 +37,24 @@ fun SourceCategoryScreen(
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 -> {
SourceCategoryContent(
state = presenter,
lazyListState = lazyListState,
paddingValues = paddingValues + topPaddingValues + PaddingValues(horizontal = horizontalPadding),
)
}
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.createCategory(it) },
title = stringResource(R.string.action_add_category),
)
}
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) },
title = stringResource(R.string.delete_category),
text = stringResource(R.string.delete_category_confirmation, dialog.category),
)
}
else -> {}
}
LaunchedEffect(Unit) {
presenter.events.collectLatest { event ->
when (event) {
is SourceCategoryPresenter.Event.CategoryExists -> {
context.toast(R.string.error_category_exists)
}
is SourceCategoryPresenter.Event.InternalError -> {
context.toast(R.string.internal_error)
}
SourceCategoryPresenter.Event.InvalidName -> {
context.toast(R.string.invalid_category_name)
}
}
}
}
SourceCategoryContent(
categories = state.categories,
lazyListState = lazyListState,
paddingValues = paddingValues + topPaddingValues + PaddingValues(horizontal = horizontalPadding),
onClickRename = onClickRename,
onClickDelete = onClickDelete,
)
}
}

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.sources.SourceCategoryPresenter
@Stable
interface SourceCategoryState {
val isLoading: Boolean
var dialog: SourceCategoryPresenter.Dialog?
val categories: List<String>
val isEmpty: Boolean
}
fun SourceCategoryState(): SourceCategoryState {
return SourceCategoryStateImpl()
}
class SourceCategoryStateImpl : SourceCategoryState {
override var isLoading: Boolean by mutableStateOf(true)
override var dialog: SourceCategoryPresenter.Dialog? by mutableStateOf(null)
override var categories: List<String> by mutableStateOf(emptyList())
override val isEmpty: Boolean by derivedStateOf { categories.isEmpty() }
}

View File

@ -7,28 +7,27 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import eu.kanade.presentation.category.SourceCategoryState
import eu.kanade.presentation.components.LazyColumn
import eu.kanade.tachiyomi.ui.category.sources.SourceCategoryPresenter
@Composable
fun SourceCategoryContent(
state: SourceCategoryState,
categories: List<String>,
lazyListState: LazyListState,
paddingValues: PaddingValues,
onClickRename: (String) -> Unit,
onClickDelete: (String) -> Unit,
) {
val categories = state.categories
LazyColumn(
state = lazyListState,
contentPadding = paddingValues,
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
items(categories) { category ->
items(categories, key = { it }) { category ->
SourceCategoryListItem(
modifier = Modifier.animateItemPlacement(),
category = category,
onRename = { state.dialog = SourceCategoryPresenter.Dialog.Rename(category) },
onDelete = { state.dialog = SourceCategoryPresenter.Dialog.Delete(category) },
onRename = { onClickRename(category) },
onDelete = { onClickDelete(category) },
)
}
}

View File

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

View File

@ -1,96 +0,0 @@
package eu.kanade.tachiyomi.ui.category.sources
import android.os.Bundle
import eu.kanade.domain.source.interactor.CreateSourceCategory
import eu.kanade.domain.source.interactor.DeleteSourceCategory
import eu.kanade.domain.source.interactor.GetSourceCategories
import eu.kanade.domain.source.interactor.RenameSourceCategory
import eu.kanade.presentation.category.SourceCategoryState
import eu.kanade.presentation.category.SourceCategoryStateImpl
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 [SourceCategoryController]. Used to manage the categories of the library.
*/
class SourceCategoryPresenter(
private val state: SourceCategoryStateImpl = SourceCategoryState() as SourceCategoryStateImpl,
private val getSourceCategories: GetSourceCategories = Injekt.get(),
private val createSourceCategory: CreateSourceCategory = Injekt.get(),
private val renameSourceCategory: RenameSourceCategory = Injekt.get(),
private val deleteSourceCategory: DeleteSourceCategory = Injekt.get(),
) : BasePresenter<SourceCategoryController>(), SourceCategoryState 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 {
getSourceCategories.subscribe()
.collectLatest {
state.isLoading = false
state.categories = it
}
}
}
/**
* Creates and adds a new category to the database.
*
* @param name The name of the category to create.
*/
fun createCategory(name: String) {
presenterScope.launchIO {
when (createSourceCategory.await(name)) {
is CreateSourceCategory.Result.CategoryExists -> _events.send(Event.CategoryExists)
is CreateSourceCategory.Result.InvalidName -> _events.send(Event.InvalidName)
else -> {}
}
}
}
/**
* Deletes the given categories from the database.
*
* @param categories The list of categories to delete.
*/
fun deleteCategory(categories: String) {
presenterScope.launchIO {
deleteSourceCategory.await(categories)
}
}
/**
* Renames a category.
*
* @param categoryOld The category to rename.
* @param categoryNew The new name of the category.
*/
fun renameCategory(categoryOld: String, categoryNew: String) {
presenterScope.launchIO {
when (renameSourceCategory.await(categoryOld, categoryNew)) {
is CreateSourceCategory.Result.CategoryExists -> _events.send(Event.CategoryExists)
is CreateSourceCategory.Result.InvalidName -> _events.send(Event.InvalidName)
else -> {}
}
}
}
sealed class Event {
object CategoryExists : Event()
object InvalidName : Event()
object InternalError : Event()
}
sealed class Dialog {
object Create : Dialog()
data class Rename(val category: String) : Dialog()
data class Delete(val category: String) : Dialog()
}
}

View File

@ -0,0 +1,87 @@
package eu.kanade.tachiyomi.ui.category.sources
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.SourceCategoryScreen
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.R
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.flow.collectLatest
class SourceCategoryScreen : Screen {
@Composable
override fun Content() {
val context = LocalContext.current
val router = LocalRouter.currentOrThrow
val screenModel = rememberScreenModel { SourceCategoryScreenModel() }
val state by screenModel.state.collectAsState()
if (state is SourceCategoryScreenState.Loading) {
LoadingScreen()
return
}
val successState = state as SourceCategoryScreenState.Success
SourceCategoryScreen(
state = successState,
onClickCreate = { screenModel.showDialog(SourceCategoryDialog.Create) },
onClickRename = { screenModel.showDialog(SourceCategoryDialog.Rename(it)) },
onClickDelete = { screenModel.showDialog(SourceCategoryDialog.Delete(it)) },
navigateUp = router::popCurrentController,
)
when (val dialog = successState.dialog) {
null -> {}
SourceCategoryDialog.Create -> {
CategoryCreateDialog(
onDismissRequest = screenModel::dismissDialog,
onCreate = { screenModel.createCategory(it) },
// SY -->
title = stringResource(R.string.action_add_category),
// SY <--
)
}
is SourceCategoryDialog.Rename -> {
CategoryRenameDialog(
onDismissRequest = screenModel::dismissDialog,
onRename = { screenModel.renameCategory(dialog.category, it) },
// SY -->
category = dialog.category,
// SY <--
)
}
is SourceCategoryDialog.Delete -> {
CategoryDeleteDialog(
onDismissRequest = screenModel::dismissDialog,
onDelete = { screenModel.deleteCategory(dialog.category) },
// SY -->
title = stringResource(R.string.delete_category),
text = stringResource(R.string.delete_category_confirmation, dialog.category),
// SY <--
)
}
}
LaunchedEffect(Unit) {
screenModel.events.collectLatest { event ->
if (event is SourceCategoryEvent.LocalizedMessage) {
context.toast(event.stringRes)
}
}
}
}
}

View File

@ -0,0 +1,134 @@
package eu.kanade.tachiyomi.ui.category.sources
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.source.interactor.CreateSourceCategory
import eu.kanade.domain.source.interactor.DeleteSourceCategory
import eu.kanade.domain.source.interactor.GetSourceCategories
import eu.kanade.domain.source.interactor.RenameSourceCategory
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 [SourceCategoryController]. Used to manage the categories of the library.
*/
class SourceCategoryScreenModel(
private val getSourceCategories: GetSourceCategories = Injekt.get(),
private val createSourceCategory: CreateSourceCategory = Injekt.get(),
private val renameSourceCategory: RenameSourceCategory = Injekt.get(),
private val deleteSourceCategory: DeleteSourceCategory = Injekt.get(),
) : StateScreenModel<SourceCategoryScreenState>(SourceCategoryScreenState.Loading) {
private val _events: Channel<SourceCategoryEvent> = Channel(Int.MAX_VALUE)
val events = _events.consumeAsFlow()
init {
coroutineScope.launchIO {
getSourceCategories.subscribe()
.collectLatest { categories ->
mutableState.update {
SourceCategoryScreenState.Success(
categories = categories,
)
}
}
}
}
/**
* Creates and adds a new category to the database.
*
* @param name The name of the category to create.
*/
fun createCategory(name: String) {
coroutineScope.launchIO {
when (createSourceCategory.await(name)) {
is CreateSourceCategory.Result.CategoryExists -> _events.send(SourceCategoryEvent.CategoryExists)
is CreateSourceCategory.Result.InvalidName -> _events.send(SourceCategoryEvent.InvalidName)
else -> {}
}
}
}
/**
* Deletes the given categories from the database.
*
* @param categories The list of categories to delete.
*/
fun deleteCategory(categories: String) {
coroutineScope.launchIO {
deleteSourceCategory.await(categories)
}
}
/**
* Renames a category.
*
* @param categoryOld The category to rename.
* @param categoryNew The new name of the category.
*/
fun renameCategory(categoryOld: String, categoryNew: String) {
coroutineScope.launchIO {
when (renameSourceCategory.await(categoryOld, categoryNew)) {
is CreateSourceCategory.Result.CategoryExists -> _events.send(SourceCategoryEvent.CategoryExists)
is CreateSourceCategory.Result.InvalidName -> _events.send(SourceCategoryEvent.InvalidName)
else -> {}
}
}
}
fun showDialog(dialog: SourceCategoryDialog) {
mutableState.update {
when (it) {
SourceCategoryScreenState.Loading -> it
is SourceCategoryScreenState.Success -> it.copy(dialog = dialog)
}
}
}
fun dismissDialog() {
mutableState.update {
when (it) {
SourceCategoryScreenState.Loading -> it
is SourceCategoryScreenState.Success -> it.copy(dialog = null)
}
}
}
}
sealed class SourceCategoryEvent {
sealed class LocalizedMessage(@StringRes val stringRes: Int) : SourceCategoryEvent()
object CategoryExists : LocalizedMessage(R.string.error_category_exists)
object InvalidName : LocalizedMessage(R.string.invalid_category_name)
object InternalError : LocalizedMessage(R.string.internal_error)
}
sealed class SourceCategoryDialog {
object Create : SourceCategoryDialog()
data class Rename(val category: String) : SourceCategoryDialog()
data class Delete(val category: String) : SourceCategoryDialog()
}
sealed class SourceCategoryScreenState {
@Immutable
object Loading : SourceCategoryScreenState()
@Immutable
data class Success(
val categories: List<String>,
val dialog: SourceCategoryDialog? = null,
) : SourceCategoryScreenState() {
val isEmpty: Boolean
get() = categories.isEmpty()
}
}