diff --git a/app/src/main/java/eu/kanade/presentation/category/SourceCategoryScreen.kt b/app/src/main/java/eu/kanade/presentation/category/SourceCategoryScreen.kt index bf7429d78..797c7801f 100644 --- a/app/src/main/java/eu/kanade/presentation/category/SourceCategoryScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/category/SourceCategoryScreen.kt @@ -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, + ) } } diff --git a/app/src/main/java/eu/kanade/presentation/category/SourceCategoryState.kt b/app/src/main/java/eu/kanade/presentation/category/SourceCategoryState.kt deleted file mode 100644 index 88f7480fa..000000000 --- a/app/src/main/java/eu/kanade/presentation/category/SourceCategoryState.kt +++ /dev/null @@ -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 - 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 by mutableStateOf(emptyList()) - override val isEmpty: Boolean by derivedStateOf { categories.isEmpty() } -} diff --git a/app/src/main/java/eu/kanade/presentation/category/components/sources/SourceCategoryContent.kt b/app/src/main/java/eu/kanade/presentation/category/components/sources/SourceCategoryContent.kt index 66c7760e4..19cb795d6 100644 --- a/app/src/main/java/eu/kanade/presentation/category/components/sources/SourceCategoryContent.kt +++ b/app/src/main/java/eu/kanade/presentation/category/components/sources/SourceCategoryContent.kt @@ -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, 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) }, ) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/sources/SourceCategoryController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/sources/SourceCategoryController.kt index bb9222422..5bedb1407 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/category/sources/SourceCategoryController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/category/sources/SourceCategoryController.kt @@ -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() { - - override fun createPresenter() = SourceCategoryPresenter() +class SourceCategoryController : BasicFullComposeController() { @Composable override fun ComposeContent() { - SourceCategoryScreen( - presenter = presenter, - navigateUp = router::popCurrentController, - ) + CompositionLocalProvider(LocalRouter provides router) { + Navigator(screen = SourceCategoryScreen()) + } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/sources/SourceCategoryPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/sources/SourceCategoryPresenter.kt deleted file mode 100644 index dcc47456b..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/category/sources/SourceCategoryPresenter.kt +++ /dev/null @@ -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(), SourceCategoryState by state { - - private val _events: Channel = 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() - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/sources/SourceCategoryScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/sources/SourceCategoryScreen.kt new file mode 100644 index 000000000..4859f9b83 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/category/sources/SourceCategoryScreen.kt @@ -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) + } + } + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/sources/SourceCategoryScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/sources/SourceCategoryScreenModel.kt new file mode 100644 index 000000000..e3c1716bd --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/category/sources/SourceCategoryScreenModel.kt @@ -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.Loading) { + + private val _events: Channel = 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, + val dialog: SourceCategoryDialog? = null, + ) : SourceCategoryScreenState() { + + val isEmpty: Boolean + get() = categories.isEmpty() + } +}