浏览代码

Use Voyager on Category screen (#8472)

Andreas 2 年之前
父节点
当前提交
bf9edda04c

+ 21 - 61
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,
+        )
     }
 }

+ 0 - 28
app/src/main/java/eu/kanade/presentation/category/CategoryState.kt

@@ -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() }
-}

+ 5 - 6
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<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) },
             )
         }
     }

+ 8 - 9
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<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())
+        }
     }
 }

+ 0 - 100
app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryPresenter.kt

@@ -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()
-    }
-}

+ 79 - 0
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)
+                }
+            }
+        }
+    }
+}

+ 140 - 0
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>(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()
+    }
+}