diff --git a/app/src/main/java/eu/kanade/data/category/CategoryMapper.kt b/app/src/main/java/eu/kanade/data/category/CategoryMapper.kt new file mode 100644 index 000000000..3f9a3ec51 --- /dev/null +++ b/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, + ) +} diff --git a/app/src/main/java/eu/kanade/data/category/CategoryRepositoryImpl.kt b/app/src/main/java/eu/kanade/data/category/CategoryRepositoryImpl.kt new file mode 100644 index 000000000..f7a44ad54 --- /dev/null +++ b/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> { + 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 } + } +} diff --git a/app/src/main/java/eu/kanade/domain/DomainModule.kt b/app/src/main/java/eu/kanade/domain/DomainModule.kt index b024347c7..931a1dc13 100644 --- a/app/src/main/java/eu/kanade/domain/DomainModule.kt +++ b/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 { CategoryRepositoryImpl(get()) } + addFactory { GetCategories(get()) } + addFactory { InsertCategory(get()) } + addFactory { UpdateCategory(get()) } + addFactory { DeleteCategory(get()) } + addSingletonFactory { MangaRepositoryImpl(get()) } addFactory { GetFavoritesBySourceId(get()) } addFactory { GetMangaById(get()) } diff --git a/app/src/main/java/eu/kanade/domain/category/interactor/DeleteCategory.kt b/app/src/main/java/eu/kanade/domain/category/interactor/DeleteCategory.kt new file mode 100644 index 000000000..f44369ac2 --- /dev/null +++ b/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) + } +} diff --git a/app/src/main/java/eu/kanade/domain/category/interactor/GetCategories.kt b/app/src/main/java/eu/kanade/domain/category/interactor/GetCategories.kt new file mode 100644 index 000000000..6b0d5400e --- /dev/null +++ b/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> { + return categoryRepository.getAll() + } +} diff --git a/app/src/main/java/eu/kanade/domain/category/interactor/InsertCategory.kt b/app/src/main/java/eu/kanade/domain/category/interactor/InsertCategory.kt new file mode 100644 index 000000000..0a659d0e5 --- /dev/null +++ b/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() + } +} diff --git a/app/src/main/java/eu/kanade/domain/category/interactor/UpdateCategory.kt b/app/src/main/java/eu/kanade/domain/category/interactor/UpdateCategory.kt new file mode 100644 index 000000000..bff2a7902 --- /dev/null +++ b/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() + } +} diff --git a/app/src/main/java/eu/kanade/domain/category/model/Category.kt b/app/src/main/java/eu/kanade/domain/category/model/Category.kt new file mode 100644 index 000000000..296ac877e --- /dev/null +++ b/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 diff --git a/app/src/main/java/eu/kanade/domain/category/model/CategoryUpdate.kt b/app/src/main/java/eu/kanade/domain/category/model/CategoryUpdate.kt new file mode 100644 index 000000000..fc9bc25f0 --- /dev/null +++ b/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, +) diff --git a/app/src/main/java/eu/kanade/domain/category/repository/CategoryRepository.kt b/app/src/main/java/eu/kanade/domain/category/repository/CategoryRepository.kt new file mode 100644 index 000000000..7d2256bf8 --- /dev/null +++ b/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> + + @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") diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/CategoryQueries.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/CategoryQueries.kt index 167a8241e..78755ed83 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/CategoryQueries.kt +++ b/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) = db.put().objects(categories).prepare() - - fun deleteCategories(categories: List) = db.delete().objects(categories).prepare() } 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 7781ac365..c32ae9167 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 @@ -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) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryHolder.kt index be6483e85..9005229c5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryHolder.kt +++ b/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 /** diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryItem.kt index 27578c201..7b6b3bf34 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryItem.kt +++ b/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() { - /** - * List containing categories. - */ - private var categories: List = emptyList() + private val _categories: MutableStateFlow> = 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) { - 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) { - 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 } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryRenameDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryRenameDialog.kt index ae17260dd..a2946812b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryRenameDialog.kt +++ b/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 diff --git a/app/src/main/sqldelight/data/categories.sq b/app/src/main/sqldelight/data/categories.sq index eb7baf351..e75cdaa85 100644 --- a/app/src/main/sqldelight/data/categories.sq +++ b/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(); \ No newline at end of file