Merge remote-tracking branch 'upstream/master'

This commit is contained in:
jmir1 2022-06-17 01:15:02 +02:00
commit 16b64e0d9e
31 changed files with 554 additions and 116 deletions

View file

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

View file

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

View file

@ -0,0 +1,56 @@
package eu.kanade.data.category
import eu.kanade.data.AnimeDatabaseHandler
import eu.kanade.domain.category.model.Category
import eu.kanade.domain.category.model.CategoryUpdate
import eu.kanade.domain.category.repository.CategoryRepositoryAnime
import eu.kanade.domain.category.repository.DuplicateNameException
import kotlinx.coroutines.flow.Flow
class CategoryRepositoryImplAnime(
private val handler: AnimeDatabaseHandler,
) : CategoryRepositoryAnime {
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 }
}
}

View file

@ -3,6 +3,8 @@ package eu.kanade.domain
import eu.kanade.data.anime.AnimeRepositoryImpl
import eu.kanade.data.animehistory.AnimeHistoryRepositoryImpl
import eu.kanade.data.animesource.AnimeSourceRepositoryImpl
import eu.kanade.data.category.CategoryRepositoryImpl
import eu.kanade.data.category.CategoryRepositoryImplAnime
import eu.kanade.data.chapter.ChapterRepositoryImpl
import eu.kanade.data.episode.EpisodeRepositoryImpl
import eu.kanade.data.history.HistoryRepositoryImpl
@ -31,6 +33,16 @@ import eu.kanade.domain.animesource.interactor.ToggleAnimeSource
import eu.kanade.domain.animesource.interactor.ToggleAnimeSourcePin
import eu.kanade.domain.animesource.interactor.UpsertAnimeSourceData
import eu.kanade.domain.animesource.repository.AnimeSourceRepository
import eu.kanade.domain.category.interactor.DeleteCategory
import eu.kanade.domain.category.interactor.DeleteCategoryAnime
import eu.kanade.domain.category.interactor.GetCategories
import eu.kanade.domain.category.interactor.GetCategoriesAnime
import eu.kanade.domain.category.interactor.InsertCategory
import eu.kanade.domain.category.interactor.InsertCategoryAnime
import eu.kanade.domain.category.interactor.UpdateCategory
import eu.kanade.domain.category.interactor.UpdateCategoryAnime
import eu.kanade.domain.category.repository.CategoryRepository
import eu.kanade.domain.category.repository.CategoryRepositoryAnime
import eu.kanade.domain.chapter.interactor.GetChapterByMangaId
import eu.kanade.domain.chapter.interactor.ShouldUpdateDbChapter
import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource
@ -92,6 +104,18 @@ class DomainModule : InjektModule {
addFactory { ShouldUpdateDbEpisode() }
addFactory { SyncEpisodesWithSource(get(), get(), get(), get()) }
addSingletonFactory<CategoryRepositoryAnime> { CategoryRepositoryImplAnime(get()) }
addFactory { GetCategoriesAnime(get()) }
addFactory { InsertCategoryAnime(get()) }
addFactory { UpdateCategoryAnime(get()) }
addFactory { DeleteCategoryAnime(get()) }
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()) }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,22 @@
package eu.kanade.domain.category.interactor
import eu.kanade.domain.category.repository.CategoryRepositoryAnime
class InsertCategoryAnime(
private val categoryRepository: CategoryRepositoryAnime,
) {
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()
}
}

View file

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

View file

@ -0,0 +1,23 @@
package eu.kanade.domain.category.interactor
import eu.kanade.domain.category.model.CategoryUpdate
import eu.kanade.domain.category.repository.CategoryRepositoryAnime
class UpdateCategoryAnime(
private val categoryRepository: CategoryRepositoryAnime,
) {
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()
}
}

View file

@ -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

View file

@ -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,
)

View file

@ -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")

View file

@ -0,0 +1,20 @@
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 CategoryRepositoryAnime {
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
}

View file

@ -14,7 +14,6 @@ fun TachiyomiTheme(content: @Composable () -> Unit) {
val (colorScheme, typography) = createMdc3Theme(
context = context,
layoutDirection = layoutDirection,
setTextColors = true,
)
MaterialTheme(

View file

@ -41,8 +41,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()
}

View file

@ -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) {

View file

@ -3,7 +3,7 @@ package eu.kanade.tachiyomi.ui.animecategory
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
/**

View file

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

View file

@ -1,11 +1,21 @@
package eu.kanade.tachiyomi.ui.animecategory
import android.os.Bundle
import eu.kanade.tachiyomi.data.database.AnimeDatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.domain.category.interactor.DeleteCategoryAnime
import eu.kanade.domain.category.interactor.GetCategoriesAnime
import eu.kanade.domain.category.interactor.InsertCategoryAnime
import eu.kanade.domain.category.interactor.UpdateCategoryAnime
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: AnimeDatabaseHelper = Injekt.get(),
private val getCategories: GetCategoriesAnime = Injekt.get(),
private val insertCategory: InsertCategoryAnime = Injekt.get(),
private val updateCategory: UpdateCategoryAnime = Injekt.get(),
private val deleteCategory: DeleteCategoryAnime = 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 InsertCategoryAnime.Result.Success -> {}
is InsertCategoryAnime.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 UpdateCategoryAnime.Result.Success -> {}
is UpdateCategoryAnime.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 }
}
}

View file

@ -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
@ -78,8 +78,6 @@ class CategoryRenameDialog<T>(bundle: Bundle? = null) : DialogController(bundle)
interface Listener {
fun renameCategory(category: Category, name: String)
}
private companion object {
const val CATEGORY_KEY = "CategoryRenameDialog.category"
}
}
private const val CATEGORY_KEY = "CategoryRenameDialog.category"

View file

@ -4,7 +4,10 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
@ -32,7 +35,9 @@ abstract class ComposeController<P : Presenter<*>>(bundle: Bundle? = null) :
setContent {
val nestedScrollInterop = rememberNestedScrollInteropConnection()
TachiyomiTheme {
ComposeContent(nestedScrollInterop)
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onBackground) {
ComposeContent(nestedScrollInterop)
}
}
}
}
@ -58,7 +63,9 @@ abstract class BasicComposeController :
setContent {
val nestedScrollInterop = rememberNestedScrollInteropConnection()
TachiyomiTheme {
ComposeContent(nestedScrollInterop)
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onBackground) {
ComposeContent(nestedScrollInterop)
}
}
}
}
@ -81,7 +88,9 @@ abstract class SearchableComposeController<P : BasePresenter<*>>(bundle: Bundle?
setContent {
val nestedScrollInterop = rememberNestedScrollInteropConnection()
TachiyomiTheme {
ComposeContent(nestedScrollInterop)
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onBackground) {
ComposeContent(nestedScrollInterop)
}
}
}
}

View file

@ -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) {

View file

@ -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
/**

View file

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

View file

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

View file

@ -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

View file

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

View file

@ -11,7 +11,8 @@ _id AS id,
name,
sort AS `order`,
flags
FROM categories;
FROM categories
ORDER BY sort;
getCategoriesByAnimeId:
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();