Avoid concurrency issues when reordering categories

Maybe fixes #8372
This commit is contained in:
arkon 2022-11-22 23:12:23 -05:00
parent 5c37347cec
commit e2179a6669
4 changed files with 53 additions and 37 deletions

View File

@ -5,46 +5,66 @@ import eu.kanade.domain.category.model.CategoryUpdate
import eu.kanade.domain.category.repository.CategoryRepository import eu.kanade.domain.category.repository.CategoryRepository
import eu.kanade.tachiyomi.util.lang.withNonCancellableContext import eu.kanade.tachiyomi.util.lang.withNonCancellableContext
import eu.kanade.tachiyomi.util.system.logcat import eu.kanade.tachiyomi.util.system.logcat
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import logcat.LogPriority import logcat.LogPriority
import java.util.Collections
class ReorderCategory( class ReorderCategory(
private val categoryRepository: CategoryRepository, private val categoryRepository: CategoryRepository,
) { ) {
suspend fun await(categoryId: Long, newPosition: Int) = withNonCancellableContext { private val mutex = Mutex()
val categories = categoryRepository.getAll().filterNot(Category::isSystemCategory)
val currentIndex = categories.indexOfFirst { it.id == categoryId } suspend fun moveUp(category: Category): Result =
if (currentIndex == newPosition) { await(category, MoveTo.UP)
return@withNonCancellableContext Result.Unchanged
}
val reorderedCategories = categories.toMutableList() suspend fun moveDown(category: Category): Result =
val reorderedCategory = reorderedCategories.removeAt(currentIndex) await(category, MoveTo.DOWN)
reorderedCategories.add(newPosition, reorderedCategory)
val updates = reorderedCategories.mapIndexed { index, category -> private suspend fun await(category: Category, moveTo: MoveTo) = withNonCancellableContext {
CategoryUpdate( mutex.withLock {
id = category.id, val categories = categoryRepository.getAll()
order = index.toLong(), .filterNot(Category::isSystemCategory)
) .toMutableList()
}
try { val newPosition = when (moveTo) {
categoryRepository.updatePartial(updates) MoveTo.UP -> category.order - 1
Result.Success MoveTo.DOWN -> category.order + 1
} catch (e: Exception) { }.toInt()
logcat(LogPriority.ERROR, e)
Result.InternalError(e) val currentIndex = categories.indexOfFirst { it.id == category.id }
if (currentIndex == newPosition) {
return@withNonCancellableContext Result.Unchanged
}
Collections.swap(categories, currentIndex, newPosition)
val updates = categories.mapIndexed { index, category ->
CategoryUpdate(
id = category.id,
order = index.toLong(),
)
}
try {
categoryRepository.updatePartial(updates)
Result.Success
} catch (e: Exception) {
logcat(LogPriority.ERROR, e)
Result.InternalError(e)
}
} }
} }
suspend fun await(category: Category, newPosition: Long): Result =
await(category.id, newPosition.toInt())
sealed class Result { sealed class Result {
object Success : Result() object Success : Result()
object Unchanged : Result() object Unchanged : Result()
data class InternalError(val error: Throwable) : Result() data class InternalError(val error: Throwable) : Result()
} }
private enum class MoveTo {
UP,
DOWN,
}
} }

View File

@ -96,8 +96,7 @@ fun ChangeCategoryDialog(
val index = selection.indexOf(it) val index = selection.indexOf(it)
if (index != -1) { if (index != -1) {
val mutableList = selection.toMutableList() val mutableList = selection.toMutableList()
mutableList.removeAt(index) mutableList[index] = it.next()
mutableList.add(index, it.next())
selection = mutableList.toList() selection = mutableList.toList()
} }
} }

View File

@ -64,8 +64,7 @@ fun DeleteLibraryMangaDialog(
val onCheck = { val onCheck = {
val index = list.indexOf(state) val index = list.indexOf(state)
val mutableList = list.toMutableList() val mutableList = list.toMutableList()
mutableList.removeAt(index) mutableList[index] = state.next() as CheckboxState.State<Int>
mutableList.add(index, state.next() as CheckboxState.State<Int>)
list = mutableList.toList() list = mutableList.toList()
} }

View File

@ -48,7 +48,7 @@ class CategoryScreenModel(
when (createCategoryWithName.await(name)) { when (createCategoryWithName.await(name)) {
is CreateCategoryWithName.Result.InternalError -> _events.send(CategoryEvent.InternalError) is CreateCategoryWithName.Result.InternalError -> _events.send(CategoryEvent.InternalError)
CreateCategoryWithName.Result.NameAlreadyExistsError -> _events.send(CategoryEvent.CategoryWithNameAlreadyExists) CreateCategoryWithName.Result.NameAlreadyExistsError -> _events.send(CategoryEvent.CategoryWithNameAlreadyExists)
CreateCategoryWithName.Result.Success -> {} else -> {}
} }
} }
} }
@ -57,27 +57,25 @@ class CategoryScreenModel(
coroutineScope.launch { coroutineScope.launch {
when (deleteCategory.await(categoryId = categoryId)) { when (deleteCategory.await(categoryId = categoryId)) {
is DeleteCategory.Result.InternalError -> _events.send(CategoryEvent.InternalError) is DeleteCategory.Result.InternalError -> _events.send(CategoryEvent.InternalError)
DeleteCategory.Result.Success -> {} else -> {}
} }
} }
} }
fun moveUp(category: Category) { fun moveUp(category: Category) {
coroutineScope.launch { coroutineScope.launch {
when (reorderCategory.await(category, category.order - 1)) { when (reorderCategory.moveUp(category)) {
is ReorderCategory.Result.InternalError -> _events.send(CategoryEvent.InternalError) is ReorderCategory.Result.InternalError -> _events.send(CategoryEvent.InternalError)
ReorderCategory.Result.Success -> {} else -> {}
ReorderCategory.Result.Unchanged -> {}
} }
} }
} }
fun moveDown(category: Category) { fun moveDown(category: Category) {
coroutineScope.launch { coroutineScope.launch {
when (reorderCategory.await(category, category.order + 1)) { when (reorderCategory.moveDown(category)) {
is ReorderCategory.Result.InternalError -> _events.send(CategoryEvent.InternalError) is ReorderCategory.Result.InternalError -> _events.send(CategoryEvent.InternalError)
ReorderCategory.Result.Success -> {} else -> {}
ReorderCategory.Result.Unchanged -> {}
} }
} }
} }
@ -87,7 +85,7 @@ class CategoryScreenModel(
when (renameCategory.await(category, name)) { when (renameCategory.await(category, name)) {
is RenameCategory.Result.InternalError -> _events.send(CategoryEvent.InternalError) is RenameCategory.Result.InternalError -> _events.send(CategoryEvent.InternalError)
RenameCategory.Result.NameAlreadyExistsError -> _events.send(CategoryEvent.CategoryWithNameAlreadyExists) RenameCategory.Result.NameAlreadyExistsError -> _events.send(CategoryEvent.CategoryWithNameAlreadyExists)
RenameCategory.Result.Success -> {} else -> {}
} }
} }
} }