Use Voyager on Source Category screen
This commit is contained in:
parent
0d638b1c1e
commit
02954670d4
@ -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),
|
||||
SourceCategoryContent(
|
||||
categories = state.categories,
|
||||
lazyListState = lazyListState,
|
||||
paddingValues = paddingValues + topPaddingValues + PaddingValues(horizontal = horizontalPadding),
|
||||
onClickRename = onClickRename,
|
||||
onClickDelete = onClickDelete,
|
||||
)
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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() }
|
||||
}
|
@ -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) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user