diff --git a/app/src/main/java/eu/kanade/presentation/category/CategoryScreen.kt b/app/src/main/java/eu/kanade/presentation/category/CategoryScreen.kt index d189e3430..d67a16282 100644 --- a/app/src/main/java/eu/kanade/presentation/category/CategoryScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/category/CategoryScreen.kt @@ -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, + ) } } diff --git a/app/src/main/java/eu/kanade/presentation/category/CategoryState.kt b/app/src/main/java/eu/kanade/presentation/category/CategoryState.kt deleted file mode 100644 index 5873f6bde..000000000 --- a/app/src/main/java/eu/kanade/presentation/category/CategoryState.kt +++ /dev/null @@ -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 - 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 by mutableStateOf(emptyList()) - override val isEmpty: Boolean by derivedStateOf { categories.isEmpty() } -} diff --git a/app/src/main/java/eu/kanade/presentation/category/components/CategoryContent.kt b/app/src/main/java/eu/kanade/presentation/category/components/CategoryContent.kt index b1cc6c97b..23fb9f1e8 100644 --- a/app/src/main/java/eu/kanade/presentation/category/components/CategoryContent.kt +++ b/app/src/main/java/eu/kanade/presentation/category/components/CategoryContent.kt @@ -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, 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) }, ) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryController.kt index c905db5f3..9a5357931 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryController.kt @@ -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() { - - override fun createPresenter() = CategoryPresenter() +class CategoryController : BasicFullComposeController() { @Composable override fun ComposeContent() { - CategoryScreen( - presenter = presenter, - navigateUp = router::popCurrentController, - ) + CompositionLocalProvider(LocalRouter provides router) { + Navigator(screen = CategoryScreen()) + } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryPresenter.kt deleted file mode 100644 index f0809a9fe..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryPresenter.kt +++ /dev/null @@ -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(), CategoryState by state { - - private val _events: Channel = 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() - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryScreen.kt new file mode 100644 index 000000000..eb19a475a --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryScreen.kt @@ -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) + } + } + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryScreenModel.kt new file mode 100644 index 000000000..9b6cdb679 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryScreenModel.kt @@ -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.Loading) { + + private val _events: Channel = 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, + val dialog: CategoryDialog? = null, + ) : CategoryScreenState() { + + val isEmpty: Boolean + get() = categories.isEmpty() + } +}