Browse Source

Use SQLDelight on Category screen (#7310)

* Use SQLDelight on Category screen

* Include category name in DuplicateNameException
Andreas 2 years ago
parent
commit
017f6b22f0

+ 12 - 0
app/src/main/java/eu/kanade/data/category/CategoryMapper.kt

@@ -0,0 +1,12 @@
+package eu.kanade.data.category
+
+import eu.kanade.domain.category.model.Category
+
+val categoryMapper: (Long, String, Long, Long) -> Category = { id, name, order, flags ->
+    Category(
+        id = id,
+        name = name,
+        order = order,
+        flags = flags,
+    )
+}

+ 56 - 0
app/src/main/java/eu/kanade/data/category/CategoryRepositoryImpl.kt

@@ -0,0 +1,56 @@
+package eu.kanade.data.category
+
+import eu.kanade.data.DatabaseHandler
+import eu.kanade.domain.category.model.Category
+import eu.kanade.domain.category.model.CategoryUpdate
+import eu.kanade.domain.category.repository.CategoryRepository
+import eu.kanade.domain.category.repository.DuplicateNameException
+import kotlinx.coroutines.flow.Flow
+
+class CategoryRepositoryImpl(
+    private val handler: DatabaseHandler,
+) : CategoryRepository {
+
+    override fun getAll(): Flow<List<Category>> {
+        return handler.subscribeToList { categoriesQueries.getCategories(categoryMapper) }
+    }
+
+    @Throws(DuplicateNameException::class)
+    override suspend fun insert(name: String, order: Long) {
+        if (checkDuplicateName(name)) throw DuplicateNameException(name)
+        handler.await {
+            categoriesQueries.insert(
+                name = name,
+                order = order,
+                flags = 0L,
+            )
+        }
+    }
+
+    @Throws(DuplicateNameException::class)
+    override suspend fun update(payload: CategoryUpdate) {
+        if (payload.name != null && checkDuplicateName(payload.name)) throw DuplicateNameException(payload.name)
+        handler.await {
+            categoriesQueries.update(
+                name = payload.name,
+                order = payload.order,
+                flags = payload.flags,
+                categoryId = payload.id,
+            )
+        }
+    }
+
+    override suspend fun delete(categoryId: Long) {
+        handler.await {
+            categoriesQueries.delete(
+                categoryId = categoryId,
+            )
+        }
+    }
+
+    override suspend fun checkDuplicateName(name: String): Boolean {
+        return handler
+            .awaitList { categoriesQueries.getCategories() }
+            .any { it.name == name }
+    }
+}

+ 12 - 0
app/src/main/java/eu/kanade/domain/DomainModule.kt

@@ -1,9 +1,15 @@
 package eu.kanade.domain
 
+import eu.kanade.data.category.CategoryRepositoryImpl
 import eu.kanade.data.chapter.ChapterRepositoryImpl
 import eu.kanade.data.history.HistoryRepositoryImpl
 import eu.kanade.data.manga.MangaRepositoryImpl
 import eu.kanade.data.source.SourceRepositoryImpl
+import eu.kanade.domain.category.interactor.DeleteCategory
+import eu.kanade.domain.category.interactor.GetCategories
+import eu.kanade.domain.category.interactor.InsertCategory
+import eu.kanade.domain.category.interactor.UpdateCategory
+import eu.kanade.domain.category.repository.CategoryRepository
 import eu.kanade.domain.chapter.interactor.GetChapterByMangaId
 import eu.kanade.domain.chapter.interactor.ShouldUpdateDbChapter
 import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource
@@ -45,6 +51,12 @@ import uy.kohesive.injekt.api.get
 class DomainModule : InjektModule {
 
     override fun InjektRegistrar.registerInjectables() {
+        addSingletonFactory<CategoryRepository> { CategoryRepositoryImpl(get()) }
+        addFactory { GetCategories(get()) }
+        addFactory { InsertCategory(get()) }
+        addFactory { UpdateCategory(get()) }
+        addFactory { DeleteCategory(get()) }
+
         addSingletonFactory<MangaRepository> { MangaRepositoryImpl(get()) }
         addFactory { GetFavoritesBySourceId(get()) }
         addFactory { GetMangaById(get()) }

+ 12 - 0
app/src/main/java/eu/kanade/domain/category/interactor/DeleteCategory.kt

@@ -0,0 +1,12 @@
+package eu.kanade.domain.category.interactor
+
+import eu.kanade.domain.category.repository.CategoryRepository
+
+class DeleteCategory(
+    private val categoryRepository: CategoryRepository,
+) {
+
+    suspend fun await(categoryId: Long) {
+        categoryRepository.delete(categoryId)
+    }
+}

+ 14 - 0
app/src/main/java/eu/kanade/domain/category/interactor/GetCategories.kt

@@ -0,0 +1,14 @@
+package eu.kanade.domain.category.interactor
+
+import eu.kanade.domain.category.model.Category
+import eu.kanade.domain.category.repository.CategoryRepository
+import kotlinx.coroutines.flow.Flow
+
+class GetCategories(
+    private val categoryRepository: CategoryRepository,
+) {
+
+    fun subscribe(): Flow<List<Category>> {
+        return categoryRepository.getAll()
+    }
+}

+ 22 - 0
app/src/main/java/eu/kanade/domain/category/interactor/InsertCategory.kt

@@ -0,0 +1,22 @@
+package eu.kanade.domain.category.interactor
+
+import eu.kanade.domain.category.repository.CategoryRepository
+
+class InsertCategory(
+    private val categoryRepository: CategoryRepository,
+) {
+
+    suspend fun await(name: String, order: Long): Result {
+        return try {
+            categoryRepository.insert(name, order)
+            Result.Success
+        } catch (e: Exception) {
+            Result.Error(e)
+        }
+    }
+
+    sealed class Result {
+        object Success : Result()
+        data class Error(val error: Exception) : Result()
+    }
+}

+ 23 - 0
app/src/main/java/eu/kanade/domain/category/interactor/UpdateCategory.kt

@@ -0,0 +1,23 @@
+package eu.kanade.domain.category.interactor
+
+import eu.kanade.domain.category.model.CategoryUpdate
+import eu.kanade.domain.category.repository.CategoryRepository
+
+class UpdateCategory(
+    private val categoryRepository: CategoryRepository,
+) {
+
+    suspend fun await(payload: CategoryUpdate): Result {
+        return try {
+            categoryRepository.update(payload)
+            Result.Success
+        } catch (e: Exception) {
+            Result.Error(e)
+        }
+    }
+
+    sealed class Result {
+        object Success : Result()
+        data class Error(val error: Exception) : Result()
+    }
+}

+ 10 - 0
app/src/main/java/eu/kanade/domain/category/model/Category.kt

@@ -0,0 +1,10 @@
+package eu.kanade.domain.category.model
+
+import java.io.Serializable
+
+data class Category(
+    val id: Long,
+    val name: String,
+    val order: Long,
+    val flags: Long,
+) : Serializable

+ 8 - 0
app/src/main/java/eu/kanade/domain/category/model/CategoryUpdate.kt

@@ -0,0 +1,8 @@
+package eu.kanade.domain.category.model
+
+data class CategoryUpdate(
+    val id: Long,
+    val name: String? = null,
+    val order: Long? = null,
+    val flags: Long? = null,
+)

+ 22 - 0
app/src/main/java/eu/kanade/domain/category/repository/CategoryRepository.kt

@@ -0,0 +1,22 @@
+package eu.kanade.domain.category.repository
+
+import eu.kanade.domain.category.model.Category
+import eu.kanade.domain.category.model.CategoryUpdate
+import kotlinx.coroutines.flow.Flow
+
+interface CategoryRepository {
+
+    fun getAll(): Flow<List<Category>>
+
+    @Throws(DuplicateNameException::class)
+    suspend fun insert(name: String, order: Long)
+
+    @Throws(DuplicateNameException::class)
+    suspend fun update(payload: CategoryUpdate)
+
+    suspend fun delete(categoryId: Long)
+
+    suspend fun checkDuplicateName(name: String): Boolean
+}
+
+class DuplicateNameException(name: String) : Exception("There's a category which is named \"$name\" already")

+ 0 - 4
app/src/main/java/eu/kanade/tachiyomi/data/database/queries/CategoryQueries.kt

@@ -30,8 +30,4 @@ interface CategoryQueries : DbProvider {
         .prepare()
 
     fun insertCategory(category: Category) = db.put().`object`(category).prepare()
-
-    fun insertCategories(categories: List<Category>) = db.put().objects(categories).prepare()
-
-    fun deleteCategories(categories: List<Category>) = db.delete().objects(categories).prepare()
 }

+ 8 - 1
app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryController.kt

@@ -14,14 +14,15 @@ import dev.chrisbanes.insetter.applyInsetter
 import eu.davidea.flexibleadapter.FlexibleAdapter
 import eu.davidea.flexibleadapter.SelectableAdapter
 import eu.davidea.flexibleadapter.helpers.UndoHelper
+import eu.kanade.domain.category.model.Category
 import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.database.models.Category
 import eu.kanade.tachiyomi.databinding.CategoriesControllerBinding
 import eu.kanade.tachiyomi.ui.base.controller.FabController
 import eu.kanade.tachiyomi.ui.base.controller.NucleusController
 import eu.kanade.tachiyomi.ui.main.MainActivity
 import eu.kanade.tachiyomi.util.system.toast
 import eu.kanade.tachiyomi.util.view.shrinkOnScroll
+import kotlinx.coroutines.launch
 
 /**
  * Controller to manage the categories for the users' library.
@@ -91,6 +92,12 @@ class CategoryController :
         adapter?.isPermanentDelete = false
 
         actionFabScrollListener = actionFab?.shrinkOnScroll(binding.recycler)
+
+        viewScope.launch {
+            presenter.categories.collect {
+                setCategories(it.map(::CategoryItem))
+            }
+        }
     }
 
     override fun configureFab(fab: ExtendedFloatingActionButton) {

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryHolder.kt

@@ -3,7 +3,7 @@ package eu.kanade.tachiyomi.ui.category
 import android.view.View
 import androidx.recyclerview.widget.ItemTouchHelper
 import eu.davidea.viewholders.FlexibleViewHolder
-import eu.kanade.tachiyomi.data.database.models.Category
+import eu.kanade.domain.category.model.Category
 import eu.kanade.tachiyomi.databinding.CategoriesItemBinding
 
 /**

+ 2 - 2
app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryItem.kt

@@ -5,8 +5,8 @@ import androidx.recyclerview.widget.RecyclerView
 import eu.davidea.flexibleadapter.FlexibleAdapter
 import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
 import eu.davidea.flexibleadapter.items.IFlexible
+import eu.kanade.domain.category.model.Category
 import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.database.models.Category
 
 /**
  * Category item for a recycler view.
@@ -68,6 +68,6 @@ class CategoryItem(val category: Category) : AbstractFlexibleItem<CategoryHolder
     }
 
     override fun hashCode(): Int {
-        return category.id!!
+        return category.id.hashCode()
     }
 }

+ 70 - 46
app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryPresenter.kt

@@ -1,11 +1,21 @@
 package eu.kanade.tachiyomi.ui.category
 
 import android.os.Bundle
-import eu.kanade.tachiyomi.data.database.DatabaseHelper
-import eu.kanade.tachiyomi.data.database.models.Category
+import eu.kanade.domain.category.interactor.DeleteCategory
+import eu.kanade.domain.category.interactor.GetCategories
+import eu.kanade.domain.category.interactor.InsertCategory
+import eu.kanade.domain.category.interactor.UpdateCategory
+import eu.kanade.domain.category.model.Category
+import eu.kanade.domain.category.model.CategoryUpdate
+import eu.kanade.domain.category.repository.DuplicateNameException
 import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
-import rx.Observable
-import rx.android.schedulers.AndroidSchedulers
+import eu.kanade.tachiyomi.util.lang.launchIO
+import eu.kanade.tachiyomi.util.lang.launchUI
+import eu.kanade.tachiyomi.util.system.logcat
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.collectLatest
+import logcat.LogPriority
 import uy.kohesive.injekt.Injekt
 import uy.kohesive.injekt.api.get
 
@@ -13,13 +23,14 @@ import uy.kohesive.injekt.api.get
  * Presenter of [CategoryController]. Used to manage the categories of the library.
  */
 class CategoryPresenter(
-    private val db: DatabaseHelper = Injekt.get(),
+    private val getCategories: GetCategories = Injekt.get(),
+    private val insertCategory: InsertCategory = Injekt.get(),
+    private val updateCategory: UpdateCategory = Injekt.get(),
+    private val deleteCategory: DeleteCategory = Injekt.get(),
 ) : BasePresenter<CategoryController>() {
 
-    /**
-     * List containing categories.
-     */
-    private var categories: List<Category> = emptyList()
+    private val _categories: MutableStateFlow<List<Category>> = MutableStateFlow(listOf())
+    val categories = _categories.asStateFlow()
 
     /**
      * Called when the presenter is created.
@@ -29,11 +40,12 @@ class CategoryPresenter(
     override fun onCreate(savedState: Bundle?) {
         super.onCreate(savedState)
 
-        db.getCategories().asRxObservable()
-            .doOnNext { categories = it }
-            .map { it.map(::CategoryItem) }
-            .observeOn(AndroidSchedulers.mainThread())
-            .subscribeLatestCache(CategoryController::setCategories)
+        presenterScope.launchIO {
+            getCategories.subscribe()
+                .collectLatest { list ->
+                    _categories.value = list
+                }
+        }
     }
 
     /**
@@ -42,20 +54,21 @@ class CategoryPresenter(
      * @param name The name of the category to create.
      */
     fun createCategory(name: String) {
-        // Do not allow duplicate categories.
-        if (categoryExists(name)) {
-            Observable.just(Unit).subscribeFirst({ view, _ -> view.onCategoryExistsError() })
-            return
+        presenterScope.launchIO {
+            val result = insertCategory.await(
+                name = name,
+                order = categories.value.map { it.order + 1L }.maxOrNull() ?: 0L,
+            )
+            when (result) {
+                is InsertCategory.Result.Success -> {}
+                is InsertCategory.Result.Error -> {
+                    logcat(LogPriority.ERROR, result.error)
+                    if (result.error is DuplicateNameException) {
+                        launchUI { view?.onCategoryExistsError() }
+                    }
+                }
+            }
         }
-
-        // Create category.
-        val cat = Category.create(name)
-
-        // Set the new item in the last position.
-        cat.order = categories.map { it.order + 1 }.maxOrNull() ?: 0
-
-        // Insert into database.
-        db.insertCategory(cat).asRxObservable().subscribe()
     }
 
     /**
@@ -64,7 +77,11 @@ class CategoryPresenter(
      * @param categories The list of categories to delete.
      */
     fun deleteCategories(categories: List<Category>) {
-        db.deleteCategories(categories).asRxObservable().subscribe()
+        presenterScope.launchIO {
+            categories.forEach { category ->
+                deleteCategory.await(category.id)
+            }
+        }
     }
 
     /**
@@ -73,11 +90,16 @@ class CategoryPresenter(
      * @param categories The list of categories to reorder.
      */
     fun reorderCategories(categories: List<Category>) {
-        categories.forEachIndexed { i, category ->
-            category.order = i
+        presenterScope.launchIO {
+            categories.forEachIndexed { order, category ->
+                updateCategory.await(
+                    payload = CategoryUpdate(
+                        id = category.id,
+                        order = order.toLong(),
+                    ),
+                )
+            }
         }
-
-        db.insertCategories(categories).asRxObservable().subscribe()
     }
 
     /**
@@ -87,20 +109,22 @@ class CategoryPresenter(
      * @param name The new name of the category.
      */
     fun renameCategory(category: Category, name: String) {
-        // Do not allow duplicate categories.
-        if (categoryExists(name)) {
-            Observable.just(Unit).subscribeFirst({ view, _ -> view.onCategoryExistsError() })
-            return
+        presenterScope.launchIO {
+            val result = updateCategory.await(
+                payload = CategoryUpdate(
+                    id = category.id,
+                    name = name,
+                ),
+            )
+            when (result) {
+                is UpdateCategory.Result.Success -> {}
+                is UpdateCategory.Result.Error -> {
+                    logcat(LogPriority.ERROR, result.error)
+                    if (result.error is DuplicateNameException) {
+                        launchUI { view?.onCategoryExistsError() }
+                    }
+                }
+            }
         }
-
-        category.name = name
-        db.insertCategory(category).asRxObservable().subscribe()
-    }
-
-    /**
-     * Returns true if a category with the given name already exists.
-     */
-    private fun categoryExists(name: String): Boolean {
-        return categories.any { it.name == name }
     }
 }

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryRenameDialog.kt

@@ -4,8 +4,8 @@ import android.app.Dialog
 import android.os.Bundle
 import com.bluelinelabs.conductor.Controller
 import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import eu.kanade.domain.category.model.Category
 import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.database.models.Category
 import eu.kanade.tachiyomi.ui.base.controller.DialogController
 import eu.kanade.tachiyomi.widget.materialdialogs.setTextInput
 

+ 13 - 1
app/src/main/sqldelight/data/categories.sq

@@ -11,7 +11,8 @@ _id AS id,
 name,
 sort AS `order`,
 flags
-FROM categories;
+FROM categories
+ORDER BY sort;
 
 getCategoriesByMangaId:
 SELECT
@@ -28,5 +29,16 @@ insert:
 INSERT INTO categories(name, sort, flags)
 VALUES (:name, :order, :flags);
 
+delete:
+DELETE FROM categories
+WHERE _id = :categoryId;
+
+update:
+UPDATE categories
+SET name = coalesce(:name, name),
+    sort = coalesce(:order, sort),
+    flags = coalesce(:flags, flags)
+WHERE _id = :categoryId;
+
 selectLastInsertedRowId:
 SELECT last_insert_rowid();