refactor: category entity

* Merge CategorySortEntity with CategoryEntity
This commit is contained in:
Acclorite 2025-09-23 16:59:24 +03:00
parent 1d9f5514ff
commit 6d7e178d4c
No known key found for this signature in database
GPG key ID: 6E54C611F6EE8593
31 changed files with 421 additions and 425 deletions

View file

@ -0,0 +1,205 @@
{
"formatVersion": 1,
"database": {
"version": 16,
"identityHash": "6e3e3d0d7b68361faad48f4fe9b8a0b2",
"entities": [
{
"tableName": "BookEntity",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `title` TEXT NOT NULL, `author` TEXT NOT NULL, `description` TEXT, `filePath` TEXT NOT NULL, `scrollIndex` INTEGER NOT NULL, `scrollOffset` INTEGER NOT NULL, `progress` REAL NOT NULL, `image` TEXT, `categories` TEXT NOT NULL DEFAULT '[]')",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "author",
"columnName": "author",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "description",
"columnName": "description",
"affinity": "TEXT"
},
{
"fieldPath": "filePath",
"columnName": "filePath",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "scrollIndex",
"columnName": "scrollIndex",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "scrollOffset",
"columnName": "scrollOffset",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "progress",
"columnName": "progress",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "image",
"columnName": "image",
"affinity": "TEXT"
},
{
"fieldPath": "categories",
"columnName": "categories",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "'[]'"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
},
{
"tableName": "HistoryEntity",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `bookId` INTEGER NOT NULL, `time` INTEGER NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "bookId",
"columnName": "bookId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "time",
"columnName": "time",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
},
{
"tableName": "ColorPresetEntity",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `name` TEXT NOT NULL, `backgroundColor` INTEGER NOT NULL, `fontColor` INTEGER NOT NULL, `isSelected` INTEGER NOT NULL, `order` INTEGER NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER"
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "backgroundColor",
"columnName": "backgroundColor",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "fontColor",
"columnName": "fontColor",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isSelected",
"columnName": "isSelected",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "order",
"columnName": "order",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
},
{
"tableName": "CategoryEntity",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `title` TEXT NOT NULL, `order` INTEGER NOT NULL, `sortOrder` TEXT NOT NULL DEFAULT 'LAST_READ', `sortOrderDescending` INTEGER NOT NULL DEFAULT 1)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "order",
"columnName": "order",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "sortOrder",
"columnName": "sortOrder",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "'LAST_READ'"
},
{
"fieldPath": "sortOrderDescending",
"columnName": "sortOrderDescending",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "1"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
}
],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '6e3e3d0d7b68361faad48f4fe9b8a0b2')"
]
}
}

View file

@ -50,18 +50,19 @@ object AppModule {
@Singleton
fun provideBookDatabase(app: Application): BookDatabase {
// Additional Migrations
DatabaseHelper.MIGRATION_7_8.removeBooksDir(app)
DatabaseHelper.AUTO_MIGRATION_7_8.removeBooksDir(app)
return Room.databaseBuilder(
app,
BookDatabase::class.java,
"book_db"
).addMigrations(
DatabaseHelper.MIGRATION_2_3, // creates LanguageHistoryEntity table(if does not exist)
DatabaseHelper.MIGRATION_4_5, // creates ColorPresetEntity table(if does not exist)
DatabaseHelper.MIGRATION_5_6, // creates FavoriteDirectoryEntity table(if does not exist)
DatabaseHelper.MIGRATION_13_14, // remove nullability from ColorPresetEntity
DatabaseHelper.MIGRATION_14_15, // remove author nullability from BookEntity
DatabaseHelper.MANUAL_MIGRATION_2_3, // creates LanguageHistoryEntity table(if does not exist)
DatabaseHelper.MANUAL_MIGRATION_4_5, // creates ColorPresetEntity table(if does not exist)
DatabaseHelper.MANUAL_MIGRATION_5_6, // creates FavoriteDirectoryEntity table(if does not exist)
DatabaseHelper.MANUAL_MIGRATION_13_14, // remove nullability from ColorPresetEntity
DatabaseHelper.MANUAL_MIGRATION_14_15, // remove author nullability from BookEntity
DatabaseHelper.MANUAL_MIGRATION_15_16, // merge CategoryEntity and CategorySortEntity
).allowMainThreadQueries().build()
}
}

View file

@ -16,8 +16,6 @@ import ua.acclorite.book_story.data.mapper.book.BookMapper
import ua.acclorite.book_story.data.mapper.book.BookMapperImpl
import ua.acclorite.book_story.data.mapper.category.CategoryMapper
import ua.acclorite.book_story.data.mapper.category.CategoryMapperImpl
import ua.acclorite.book_story.data.mapper.category_sort.CategorySortMapper
import ua.acclorite.book_story.data.mapper.category_sort.CategorySortSortMapperImpl
import ua.acclorite.book_story.data.mapper.color_preset.ColorPresetMapper
import ua.acclorite.book_story.data.mapper.color_preset.ColorPresetMapperImpl
import ua.acclorite.book_story.data.mapper.file.FileMapper
@ -111,12 +109,6 @@ abstract class RepositoryModule {
categoryMapperImpl: CategoryMapperImpl
): CategoryMapper
@Binds
@Singleton
abstract fun bindCategorySortMapper(
categorySortSortMapperImpl: CategorySortSortMapperImpl
): CategorySortMapper
@Binds
@Singleton
abstract fun bindFileMapper(

View file

@ -6,13 +6,16 @@
package ua.acclorite.book_story.data.local.dto
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import ua.acclorite.book_story.presentation.library.model.LibrarySortOrder
@Entity
data class CategoryEntity(
@PrimaryKey(true)
val id: Int = 0,
@PrimaryKey(true) val id: Int = 0,
val title: String,
val order: Int
val order: Int,
@ColumnInfo(defaultValue = "LAST_READ") val sortOrder: LibrarySortOrder,
@ColumnInfo(defaultValue = "1") val sortOrderDescending: Boolean
)

View file

@ -1,19 +0,0 @@
/*
* Book's Story free and open-source Material You eBook reader.
* Copyright (C) 2024-2025 Acclorite
* SPDX-License-Identifier: GPL-3.0-only
*/
package ua.acclorite.book_story.data.local.dto
import androidx.room.Entity
import androidx.room.PrimaryKey
import ua.acclorite.book_story.presentation.library.model.LibrarySortOrder
@Entity
data class CategorySortEntity(
@PrimaryKey(false)
val categoryId: Int,
val sortOrder: LibrarySortOrder,
val sortOrderDescending: Boolean
)

View file

@ -12,15 +12,10 @@ import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Update
import androidx.room.Upsert
import ua.acclorite.book_story.data.local.dto.BookEntity
import ua.acclorite.book_story.data.local.dto.CategoryEntity
import ua.acclorite.book_story.data.local.dto.CategorySortEntity
@Dao
interface BookDao {
/* ------ BookEntity ------------------------ */
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertBook(
book: BookEntity
@ -42,48 +37,4 @@ interface BookDao {
@Update
suspend fun updateBook(book: BookEntity): Int
/* - - - - - - - - - - - - - - - - - - - - - - */
/* ------ CategoryEntity ----------------- */
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertCategory(
category: CategoryEntity
)
@Query("SELECT * FROM categoryentity")
suspend fun getCategories(): List<CategoryEntity>
@Query("SELECT COUNT(*) FROM categoryentity")
suspend fun getCategoriesCount(): Int
@Query("UPDATE categoryentity SET title=:title WHERE id=:id")
suspend fun updateCategoryTitle(
id: Int,
title: String
)
@Query("UPDATE categoryentity SET `order`=:order WHERE id=:id")
suspend fun updateCategoryOrder(
id: Int,
order: Int
)
@Delete
suspend fun deleteCategory(
category: CategoryEntity
)
/* - - - - - - - - - - - - - - - - - - - - - - */
/* ------ CategorySortEntity ----------------- */
@Upsert
suspend fun updateCategorySort(categorySort: CategorySortEntity)
@Query("SELECT * FROM categorysortentity")
suspend fun getCategorySortEntities(): List<CategorySortEntity>
@Query("DELETE FROM categorysortentity WHERE categoryId=:categoryId")
suspend fun deleteCategorySortEntity(categoryId: Int)
/* - - - - - - - - - - - - - - - - - - - - - - */
}

View file

@ -17,7 +17,6 @@ import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import ua.acclorite.book_story.data.local.dto.BookEntity
import ua.acclorite.book_story.data.local.dto.CategoryEntity
import ua.acclorite.book_story.data.local.dto.CategorySortEntity
import ua.acclorite.book_story.data.local.dto.ColorPresetEntity
import ua.acclorite.book_story.data.local.dto.HistoryEntity
import java.io.File
@ -27,25 +26,25 @@ import java.io.File
BookEntity::class,
HistoryEntity::class,
ColorPresetEntity::class,
CategoryEntity::class,
CategorySortEntity::class
CategoryEntity::class
],
version = 15,
version = 16,
autoMigrations = [
AutoMigration(1, 2),
AutoMigration(2, 3),
AutoMigration(3, 4, spec = DatabaseHelper.MIGRATION_3_4::class),
AutoMigration(3, 4, spec = DatabaseHelper.AUTO_MIGRATION_3_4::class),
AutoMigration(4, 5),
AutoMigration(5, 6),
AutoMigration(6, 7),
AutoMigration(7, 8, spec = DatabaseHelper.MIGRATION_7_8::class),
AutoMigration(8, 9, spec = DatabaseHelper.MIGRATION_8_9::class),
AutoMigration(9, 10, spec = DatabaseHelper.MIGRATION_9_10::class),
AutoMigration(7, 8, spec = DatabaseHelper.AUTO_MIGRATION_7_8::class),
AutoMigration(8, 9, spec = DatabaseHelper.AUTO_MIGRATION_8_9::class),
AutoMigration(9, 10, spec = DatabaseHelper.AUTO_MIGRATION_9_10::class),
AutoMigration(10, 11),
AutoMigration(11, 12),
AutoMigration(12, 13),
AutoMigration(13, 14),
AutoMigration(14, 15),
AutoMigration(15, 16, spec = DatabaseHelper.AUTO_MIGRATION_15_16::class),
],
exportSchema = true
)
@ -53,12 +52,13 @@ abstract class BookDatabase : RoomDatabase() {
abstract val bookDao: BookDao
abstract val historyDao: HistoryDao
abstract val colorPresetDao: ColorPresetDao
abstract val categoryDao: CategoryDao
}
@Suppress("ClassName")
object DatabaseHelper {
val MIGRATION_2_3 = object : Migration(2, 3) {
val MANUAL_MIGRATION_2_3 = object : Migration(2, 3) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL(
"CREATE TABLE IF NOT EXISTS `LanguageHistoryEntity` (" +
@ -76,9 +76,9 @@ object DatabaseHelper {
@DeleteColumn("BookEntity", "doubleClickTranslation")
@DeleteColumn("BookEntity", "translateWhenOpen")
@DeleteTable("LanguageHistoryEntity")
class MIGRATION_3_4 : AutoMigrationSpec
class AUTO_MIGRATION_3_4 : AutoMigrationSpec
val MIGRATION_4_5 = object : Migration(4, 5) {
val MANUAL_MIGRATION_4_5 = object : Migration(4, 5) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL(
"CREATE TABLE IF NOT EXISTS `ColorPresetEntity` (" +
@ -93,7 +93,7 @@ object DatabaseHelper {
}
}
val MIGRATION_5_6 = object : Migration(5, 6) {
val MANUAL_MIGRATION_5_6 = object : Migration(5, 6) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL(
"CREATE TABLE IF NOT EXISTS `FavoriteDirectoryEntity` (" +
@ -106,7 +106,7 @@ object DatabaseHelper {
@DeleteColumn("BookEntity", "textPath")
@DeleteColumn("BookEntity", "chapters")
class MIGRATION_7_8 : AutoMigrationSpec {
class AUTO_MIGRATION_7_8 : AutoMigrationSpec {
companion object {
/**
* Along with textPath deletion,
@ -124,12 +124,12 @@ object DatabaseHelper {
}
@DeleteTable("FavoriteDirectoryEntity")
class MIGRATION_8_9 : AutoMigrationSpec
class AUTO_MIGRATION_8_9 : AutoMigrationSpec
@DeleteColumn("BookEntity", "category")
class MIGRATION_9_10 : AutoMigrationSpec
class AUTO_MIGRATION_9_10 : AutoMigrationSpec
val MIGRATION_13_14 = object : Migration(13, 14) {
val MANUAL_MIGRATION_13_14 = object : Migration(13, 14) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL(
"""
@ -161,7 +161,7 @@ object DatabaseHelper {
}
}
val MIGRATION_14_15 = object : Migration(14, 15) {
val MANUAL_MIGRATION_14_15 = object : Migration(14, 15) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL(
"""
@ -190,4 +190,46 @@ object DatabaseHelper {
database.execSQL("ALTER TABLE BookEntity_new RENAME TO BookEntity")
}
}
@DeleteTable("CategorySortEntity")
class AUTO_MIGRATION_15_16 : AutoMigrationSpec
val MANUAL_MIGRATION_15_16 = object : Migration(15, 16) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL(
"""
CREATE TABLE IF NOT EXISTS CategoryEntity_new (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
title TEXT NOT NULL,
`order` INTEGER NOT NULL,
sortOrder TEXT NOT NULL DEFAULT 'LAST_READ',
sortOrderDescending INTEGER NOT NULL DEFAULT 1
)
"""
)
database.execSQL(
"""
INSERT INTO CategoryEntity_new (id, title, `order`, sortOrder, sortOrderDescending)
SELECT
c.id,
c.title,
c.`order`,
COALESCE(cs.sortOrder, 'LAST_READ'),
COALESCE(cs.sortOrderDescending, 1)
FROM CategoryEntity c
LEFT JOIN CategorySortEntity cs
ON c.id = cs.categoryId
"""
)
database.execSQL(
"""
INSERT INTO CategoryEntity_new (id, title, `order`, sortOrder, sortOrderDescending)
VALUES (-1, '', -1, 'LAST_READ', 1)
"""
)
database.execSQL("DROP TABLE CategoryEntity")
database.execSQL("DROP TABLE CategorySortEntity")
database.execSQL("ALTER TABLE CategoryEntity_new RENAME TO CategoryEntity")
}
}
}

View file

@ -0,0 +1,39 @@
/*
* Book's Story free and open-source Material You eBook reader.
* Copyright (C) 2024-2025 Acclorite
* SPDX-License-Identifier: GPL-3.0-only
*/
package ua.acclorite.book_story.data.local.room
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Update
import ua.acclorite.book_story.data.local.dto.CategoryEntity
@Dao
interface CategoryDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertCategory(
category: CategoryEntity
)
@Query("SELECT * FROM categoryentity ORDER BY `order` ASC")
suspend fun getCategories(): List<CategoryEntity>
@Query("SELECT COUNT(*) FROM categoryentity")
suspend fun getCategoriesCount(): Int
@Update
suspend fun updateCategory(
category: CategoryEntity
)
@Delete
suspend fun deleteCategory(
category: CategoryEntity
)
}

View file

@ -15,7 +15,9 @@ class CategoryMapperImpl @Inject constructor() : CategoryMapper {
return CategoryEntity(
id = category.id,
title = category.title,
order = category.order
order = category.order,
sortOrder = category.sortOrder,
sortOrderDescending = category.sortOrderDescending
)
}
@ -23,7 +25,9 @@ class CategoryMapperImpl @Inject constructor() : CategoryMapper {
return Category(
id = categoryEntity.id,
title = categoryEntity.title,
order = categoryEntity.order
order = categoryEntity.order,
sortOrder = categoryEntity.sortOrder,
sortOrderDescending = categoryEntity.sortOrderDescending
)
}
}

View file

@ -1,15 +0,0 @@
/*
* Book's Story free and open-source Material You eBook reader.
* Copyright (C) 2024-2025 Acclorite
* SPDX-License-Identifier: GPL-3.0-only
*/
package ua.acclorite.book_story.data.mapper.category_sort
import ua.acclorite.book_story.data.local.dto.CategorySortEntity
import ua.acclorite.book_story.domain.model.library.CategorySort
interface CategorySortMapper {
fun toCategorySortEntity(categorySort: CategorySort): CategorySortEntity
fun toCategorySort(categorySortEntity: CategorySortEntity): CategorySort
}

View file

@ -1,29 +0,0 @@
/*
* Book's Story free and open-source Material You eBook reader.
* Copyright (C) 2024-2025 Acclorite
* SPDX-License-Identifier: GPL-3.0-only
*/
package ua.acclorite.book_story.data.mapper.category_sort
import ua.acclorite.book_story.data.local.dto.CategorySortEntity
import ua.acclorite.book_story.domain.model.library.CategorySort
import javax.inject.Inject
class CategorySortSortMapperImpl @Inject constructor() : CategorySortMapper {
override fun toCategorySortEntity(categorySort: CategorySort): CategorySortEntity {
return CategorySortEntity(
categoryId = categorySort.categoryId,
sortOrder = categorySort.sortOrder,
sortOrderDescending = categorySort.sortOrderDescending
)
}
override fun toCategorySort(categorySortEntity: CategorySortEntity): CategorySort {
return CategorySort(
categoryId = categorySortEntity.categoryId,
sortOrder = categorySortEntity.sortOrder,
sortOrderDescending = categorySortEntity.sortOrderDescending
)
}
}

View file

@ -10,9 +10,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import ua.acclorite.book_story.data.local.room.BookDatabase
import ua.acclorite.book_story.data.mapper.category.CategoryMapper
import ua.acclorite.book_story.data.mapper.category_sort.CategorySortMapper
import ua.acclorite.book_story.domain.model.library.Category
import ua.acclorite.book_story.domain.model.library.CategorySort
import ua.acclorite.book_story.domain.repository.CategoryRepository
import javax.inject.Inject
import javax.inject.Singleton
@ -20,16 +18,15 @@ import javax.inject.Singleton
@Singleton
class CategoryRepositoryImpl @Inject constructor(
private val database: BookDatabase,
private val categoryMapper: CategoryMapper,
private val categorySortMapper: CategorySortMapper,
private val categoryMapper: CategoryMapper
) : CategoryRepository {
override suspend fun addCategory(category: Category): Result<Unit> = runCatching {
withContext(Dispatchers.IO) {
database.bookDao.insertCategory(
database.categoryDao.insertCategory(
categoryMapper.toCategoryEntity(
category.copy(
order = database.bookDao.getCategoriesCount()
order = database.categoryDao.getCategoriesCount()
)
)
)
@ -38,16 +35,19 @@ class CategoryRepositoryImpl @Inject constructor(
override suspend fun getCategories(): Result<List<Category>> = runCatching {
withContext(Dispatchers.IO) {
database.bookDao.getCategories().map { categoryMapper.toCategory(it) }
.sortedBy { it.order }
database.categoryDao.getCategories().map { categoryMapper.toCategory(it) }
}
}
override suspend fun updateCategory(category: Category): Result<Unit> = runCatching {
withContext(Dispatchers.IO) {
database.bookDao.updateCategoryTitle(
id = category.id,
title = category.title
database.categoryDao.updateCategory(
category = categoryMapper.toCategoryEntity(
if (category.id != -1) category else category.copy(
title = "",
order = -1
)
)
)
updateOrder(getCategories().getOrThrow())
}
@ -56,35 +56,21 @@ class CategoryRepositoryImpl @Inject constructor(
override suspend fun updateOrder(categories: List<Category>): Result<Unit> = runCatching {
withContext(Dispatchers.IO) {
categories.forEachIndexed { index, category ->
database.bookDao.updateCategoryOrder(category.id, index)
if (category.id == -1) return@forEachIndexed
database.categoryDao.updateCategory(
categoryMapper.toCategoryEntity(
category.copy(order = index)
)
)
}
}
}
override suspend fun deleteCategory(category: Category): Result<Unit> = runCatching {
withContext(Dispatchers.IO) {
database.bookDao.deleteCategory(categoryMapper.toCategoryEntity(category))
database.bookDao.deleteCategorySortEntity(category.id)
if (category.id == -1) throw IllegalArgumentException("Id should not be -1.")
database.categoryDao.deleteCategory(categoryMapper.toCategoryEntity(category))
updateOrder(getCategories().getOrThrow())
}
}
override suspend fun updateCategorySorting(
categorySort: CategorySort
): Result<Unit> = runCatching {
withContext(Dispatchers.IO) {
database.bookDao.updateCategorySort(
categorySortMapper.toCategorySortEntity(categorySort)
)
}
}
override suspend fun getCategorySorting(): Result<List<CategorySort>> = runCatching {
withContext(Dispatchers.IO) {
database.bookDao.getCategorySortEntities().map {
categorySortMapper.toCategorySort(it)
}
}
}
}

View file

@ -7,10 +7,13 @@
package ua.acclorite.book_story.domain.model.library
import androidx.compose.runtime.Immutable
import ua.acclorite.book_story.presentation.library.model.LibrarySortOrder
@Immutable
data class Category(
val id: Int = 0,
val title: String,
val order: Int = 0
val order: Int = 0,
val sortOrder: LibrarySortOrder = LibrarySortOrder.LAST_READ,
val sortOrderDescending: Boolean = true
)

View file

@ -1,17 +0,0 @@
/*
* Book's Story free and open-source Material You eBook reader.
* Copyright (C) 2024-2025 Acclorite
* SPDX-License-Identifier: GPL-3.0-only
*/
package ua.acclorite.book_story.domain.model.library
import androidx.compose.runtime.Immutable
import ua.acclorite.book_story.presentation.library.model.LibrarySortOrder
@Immutable
data class CategorySort(
val categoryId: Int,
val sortOrder: LibrarySortOrder,
val sortOrderDescending: Boolean
)

View file

@ -7,7 +7,6 @@
package ua.acclorite.book_story.domain.repository
import ua.acclorite.book_story.domain.model.library.Category
import ua.acclorite.book_story.domain.model.library.CategorySort
interface CategoryRepository {
suspend fun addCategory(
@ -27,10 +26,4 @@ interface CategoryRepository {
suspend fun deleteCategory(
category: Category
): Result<Unit>
suspend fun updateCategorySorting(
categorySort: CategorySort
): Result<Unit>
suspend fun getCategorySorting(): Result<List<CategorySort>>
}

View file

@ -1,33 +0,0 @@
/*
* Book's Story free and open-source Material You eBook reader.
* Copyright (C) 2024-2025 Acclorite
* SPDX-License-Identifier: GPL-3.0-only
*/
package ua.acclorite.book_story.domain.use_case.category
import ua.acclorite.book_story.core.log.logE
import ua.acclorite.book_story.core.log.logI
import ua.acclorite.book_story.domain.model.library.CategorySort
import ua.acclorite.book_story.domain.repository.CategoryRepository
import javax.inject.Inject
class GetCategorySortingUseCase @Inject constructor(
private val categoryRepository: CategoryRepository
) {
suspend operator fun invoke(): List<CategorySort> {
logI("Getting category sorting.")
return categoryRepository.getCategorySorting().fold(
onSuccess = {
logI("Successfully got category sorting.")
it
},
onFailure = {
logE("Could not get category sorting with error: ${it.message}")
emptyList()
}
)
}
}

View file

@ -1,33 +0,0 @@
/*
* Book's Story free and open-source Material You eBook reader.
* Copyright (C) 2024-2025 Acclorite
* SPDX-License-Identifier: GPL-3.0-only
*/
package ua.acclorite.book_story.domain.use_case.category
import ua.acclorite.book_story.core.log.logE
import ua.acclorite.book_story.core.log.logI
import ua.acclorite.book_story.domain.model.library.CategorySort
import ua.acclorite.book_story.domain.repository.CategoryRepository
import javax.inject.Inject
class UpdateCategorySortingUseCase @Inject constructor(
private val categoryRepository: CategoryRepository
) {
suspend operator fun invoke(categorySort: CategorySort) {
logI("Updating category sorting of [${categorySort.categoryId}].")
categoryRepository.updateCategorySorting(categorySort).fold(
onSuccess = {
logI("Successfully updated category sorting of [${categorySort.categoryId}].")
},
onFailure = {
logE(
"Could not update category sorting of [${categorySort.categoryId}] with error: ${it.message}"
)
}
)
}
}

View file

@ -15,15 +15,15 @@ class UpdateCategoryUseCase @Inject constructor(
private val categoryRepository: CategoryRepository
) {
suspend operator fun invoke(categoryId: Int, newTitle: String) {
logI("Updating category [$categoryId].")
suspend operator fun invoke(category: Category) {
logI("Updating category [${category.id}].")
categoryRepository.updateCategory(Category(id = categoryId, title = newTitle)).fold(
categoryRepository.updateCategory(category).fold(
onSuccess = {
logI("Successfully updated [$categoryId].")
logI("Successfully updated [${category.id}].")
},
onFailure = {
logI("Could not update [$categoryId] with error: ${it.message}")
logI("Could not update [${category.id}] with error: ${it.message}")
}
)
}

View file

@ -13,6 +13,7 @@ import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@ -50,6 +51,10 @@ data class BookInfoScreen(val bookId: Int) : Screen, Parcelable {
val state = screenModel.state.collectAsStateWithLifecycle()
val listState = rememberLazyListState()
val categories = remember(settingsState.value.categories) {
settingsState.value.categories.filterNot { it.id == -1 }
}
LaunchedEffect(Unit) {
screenModel.init(
bookId = bookId,
@ -74,7 +79,7 @@ data class BookInfoScreen(val bookId: Int) : Screen, Parcelable {
BookInfoContent(
book = state.value.book,
file = state.value.file,
categories = settingsState.value.categories,
categories = categories,
bottomSheet = state.value.bottomSheet,
dialog = state.value.dialog,
listState = listState,

View file

@ -89,7 +89,7 @@ object LibraryScreen : Screen, Parcelable {
val pagerState = rememberPagerState(
initialPage = initialPage
) {
settingsState.value.categories.count().plus(if (showDefaultCategory.value) 1 else 0)
settingsState.value.categories.count().minus(if (!showDefaultCategory.value) 1 else 0)
}
DisposableEffect(Unit) { onDispose { initialPage = pagerState.currentPage } }
@ -126,14 +126,13 @@ object LibraryScreen : Screen, Parcelable {
autoGridSize = settings.libraryAutoGridSize.value,
categories = settingsState.value.categories,
showDefaultCategory = showDefaultCategory.value,
categoriesSort = settingsState.value.categoriesSort,
sortOrder = settings.librarySortOrder.value,
sortOrderDescending = settings.librarySortOrderDescending.value,
perCategorySort = settings.libraryPerCategorySort.value,
refreshState = refreshState,
dialog = state.value.dialog,
bottomSheet = state.value.bottomSheet,
updateCategorySort = settingsModel::onEvent,
updateCategory = settingsModel::onEvent,
changeSortOrder = { settings.librarySortOrder.update(it) },
changeSortOrderDescending = { settings.librarySortOrderDescending.update(it) },
selectBook = screenModel::onEvent,

View file

@ -10,7 +10,6 @@ import androidx.compose.runtime.Immutable
import androidx.compose.ui.graphics.Color
import ua.acclorite.book_story.core.language.Language
import ua.acclorite.book_story.domain.model.library.Category
import ua.acclorite.book_story.presentation.library.model.LibrarySortOrder
@Immutable
sealed class SettingsEvent {
@ -30,9 +29,8 @@ sealed class SettingsEvent {
val title: String
) : SettingsEvent()
data class OnUpdateCategoryTitle(
val id: Int,
val title: String
data class OnUpdateCategory(
val category: Category
) : SettingsEvent()
data class OnUpdateCategoryOrder(
@ -43,12 +41,6 @@ sealed class SettingsEvent {
val category: Category
) : SettingsEvent()
data class OnUpdateCategorySort(
val categoryId: Int,
val sortOrder: LibrarySortOrder,
val sortOrderDescending: Boolean
) : SettingsEvent()
data class OnSelectColorPreset(
val id: Int,
) : SettingsEvent()

View file

@ -20,14 +20,11 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import ua.acclorite.book_story.domain.model.library.CategorySort
import ua.acclorite.book_story.domain.model.reader.ColorPreset
import ua.acclorite.book_story.domain.use_case.category.AddCategoryUseCase
import ua.acclorite.book_story.domain.use_case.category.DeleteCategoryUseCase
import ua.acclorite.book_story.domain.use_case.category.GetCategoriesUseCase
import ua.acclorite.book_story.domain.use_case.category.GetCategorySortingUseCase
import ua.acclorite.book_story.domain.use_case.category.UpdateCategoriesOrderUseCase
import ua.acclorite.book_story.domain.use_case.category.UpdateCategorySortingUseCase
import ua.acclorite.book_story.domain.use_case.category.UpdateCategoryUseCase
import ua.acclorite.book_story.domain.use_case.color_preset.DeleteColorPresetUseCase
import ua.acclorite.book_story.domain.use_case.color_preset.GetColorPresetsUseCase
@ -55,9 +52,7 @@ class SettingsModel @Inject constructor(
private val getCategoriesUseCase: GetCategoriesUseCase,
private val updateCategoryUseCase: UpdateCategoryUseCase,
private val updateCategoriesOrderUseCase: UpdateCategoriesOrderUseCase,
private val deleteCategoryUseCase: DeleteCategoryUseCase,
private val updateCategorySortingUseCase: UpdateCategorySortingUseCase,
private val getCategorySortingUseCase: GetCategorySortingUseCase
private val deleteCategoryUseCase: DeleteCategoryUseCase
) : ViewModel() {
private val mutex = Mutex()
@ -98,8 +93,7 @@ class SettingsModel @Inject constructor(
it.copy(
selectedColorPreset = colorPresets.getSelectedColorPreset(),
colorPresets = colorPresets,
categories = getCategoriesUseCase(),
categoriesSort = getCategorySortingUseCase()
categories = getCategoriesUseCase()
)
}
@ -131,10 +125,9 @@ class SettingsModel @Inject constructor(
}
}
is SettingsEvent.OnUpdateCategoryTitle -> {
is SettingsEvent.OnUpdateCategory -> {
updateCategoryUseCase(
categoryId = event.id,
newTitle = event.title
category = event.category
)
_state.update {
it.copy(
@ -165,21 +158,6 @@ class SettingsModel @Inject constructor(
}
}
is SettingsEvent.OnUpdateCategorySort -> {
updateCategorySortingUseCase(
categorySort = CategorySort(
categoryId = event.categoryId,
sortOrder = event.sortOrder,
sortOrderDescending = event.sortOrderDescending
)
)
_state.update {
it.copy(
categoriesSort = getCategorySortingUseCase()
)
}
}
is SettingsEvent.OnSelectColorPreset -> {
withContext(Dispatchers.IO) {
colorPresetJob?.join()

View file

@ -9,7 +9,6 @@ package ua.acclorite.book_story.presentation.settings
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.runtime.Immutable
import ua.acclorite.book_story.domain.model.library.Category
import ua.acclorite.book_story.domain.model.library.CategorySort
import ua.acclorite.book_story.domain.model.reader.ColorPreset
@Immutable
@ -19,6 +18,5 @@ data class SettingsState(
val animateColorPreset: Boolean = false,
val colorPresetListState: LazyListState = LazyListState(),
val categories: List<Category> = emptyList(),
val categoriesSort: List<CategorySort> = emptyList(),
val categories: List<Category> = emptyList()
)

View file

@ -10,7 +10,6 @@ import androidx.compose.foundation.pager.PagerState
import androidx.compose.runtime.Composable
import ua.acclorite.book_story.core.BottomSheet
import ua.acclorite.book_story.domain.model.library.Category
import ua.acclorite.book_story.domain.model.library.CategorySort
import ua.acclorite.book_story.presentation.library.LibraryEvent
import ua.acclorite.book_story.presentation.library.LibraryScreen
import ua.acclorite.book_story.presentation.library.model.LibrarySortOrder
@ -22,13 +21,12 @@ fun LibraryBottomSheet(
categories: List<Category>,
showDefaultCategory: Boolean,
pagerState: PagerState,
categoriesSort: List<CategorySort>,
sortOrder: LibrarySortOrder,
sortOrderDescending: Boolean,
perCategorySort: Boolean,
changeSortOrder: (LibrarySortOrder) -> Unit,
changeSortOrderDescending: (Boolean) -> Unit,
updateCategorySort: (SettingsEvent.OnUpdateCategorySort) -> Unit,
updateCategory: (SettingsEvent.OnUpdateCategory) -> Unit,
dismissBottomSheet: (LibraryEvent.OnDismissBottomSheet) -> Unit
) {
when (bottomSheet) {
@ -39,11 +37,10 @@ fun LibraryBottomSheet(
categoriesPagerState = pagerState,
sortOrder = sortOrder,
sortOrderDescending = sortOrderDescending,
categoriesSort = categoriesSort,
perCategorySort = perCategorySort,
changeSortOrder = changeSortOrder,
changeSortOrderDescending = changeSortOrderDescending,
updateCategorySort = updateCategorySort,
updateCategory = updateCategory,
dismissBottomSheet = dismissBottomSheet
)
}

View file

@ -14,7 +14,6 @@ import androidx.compose.ui.focus.FocusRequester
import ua.acclorite.book_story.core.BottomSheet
import ua.acclorite.book_story.core.Dialog
import ua.acclorite.book_story.domain.model.library.Category
import ua.acclorite.book_story.domain.model.library.CategorySort
import ua.acclorite.book_story.presentation.library.LibraryEvent
import ua.acclorite.book_story.presentation.library.model.LibraryLayout
import ua.acclorite.book_story.presentation.library.model.LibrarySortOrder
@ -44,7 +43,6 @@ fun LibraryContent(
categories: List<Category>,
showDefaultCategory: Boolean,
perCategorySort: Boolean,
categoriesSort: List<CategorySort>,
sortOrder: LibrarySortOrder,
sortOrderDescending: Boolean,
layout: LibraryLayout,
@ -53,7 +51,7 @@ fun LibraryContent(
refreshState: PullRefreshState,
dialog: Dialog?,
bottomSheet: BottomSheet?,
updateCategorySort: (SettingsEvent.OnUpdateCategorySort) -> Unit,
updateCategory: (SettingsEvent.OnUpdateCategory) -> Unit,
changeSortOrder: (LibrarySortOrder) -> Unit,
changeSortOrderDescending: (Boolean) -> Unit,
selectBook: (LibraryEvent.OnSelectBook) -> Unit,
@ -90,13 +88,12 @@ fun LibraryContent(
categories = categories,
showDefaultCategory = showDefaultCategory,
pagerState = pagerState,
categoriesSort = categoriesSort,
sortOrder = sortOrder,
sortOrderDescending = sortOrderDescending,
perCategorySort = perCategorySort,
changeSortOrder = changeSortOrder,
changeSortOrderDescending = changeSortOrderDescending,
updateCategorySort = updateCategorySort,
updateCategory = updateCategory,
dismissBottomSheet = dismissBottomSheet
)
@ -122,7 +119,6 @@ fun LibraryContent(
categories = categories,
showDefaultCategory = showDefaultCategory,
perCategorySort = perCategorySort,
categoriesSort = categoriesSort,
sortOrder = sortOrder,
sortOrderDescending = sortOrderDescending,
searchVisibility = searchVisibility,

View file

@ -23,7 +23,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch
import ua.acclorite.book_story.domain.model.library.Category
import ua.acclorite.book_story.domain.model.library.CategorySort
import ua.acclorite.book_story.presentation.library.LibraryEvent
import ua.acclorite.book_story.presentation.library.model.LibrarySortOrder
import ua.acclorite.book_story.presentation.settings.SettingsEvent
@ -42,11 +41,10 @@ fun LibraryFilterBottomSheet(
categoriesPagerState: PagerState,
sortOrder: LibrarySortOrder,
sortOrderDescending: Boolean,
categoriesSort: List<CategorySort>,
perCategorySort: Boolean,
changeSortOrder: (LibrarySortOrder) -> Unit,
changeSortOrderDescending: (Boolean) -> Unit,
updateCategorySort: (SettingsEvent.OnUpdateCategorySort) -> Unit,
updateCategory: (SettingsEvent.OnUpdateCategory) -> Unit,
dismissBottomSheet: (LibraryEvent.OnDismissBottomSheet) -> Unit
) {
val currentCategory = remember(
@ -55,25 +53,12 @@ fun LibraryFilterBottomSheet(
categoriesPagerState.currentPage
) {
derivedStateOf {
categories.toMutableList().apply {
if (showDefaultCategory) {
add(0, Category(id = -1, title = ""))
}
categories.filterNot {
if (!showDefaultCategory) it.id == -1 else false
}[categoriesPagerState.currentPage]
}
}
val currentCategorySort = remember(currentCategory, categoriesSort) {
derivedStateOf {
categoriesSort.firstOrNull { it.categoryId == currentCategory.value.id }
?: CategorySort(
categoryId = currentCategory.value.id,
sortOrder = LibrarySortOrder.LAST_READ,
sortOrderDescending = true
)
}
}
val scope = rememberCoroutineScope()
val pagerState = rememberPagerState(initialPage) { 2 }
DisposableEffect(Unit) { onDispose { initialPage = pagerState.currentPage } }
@ -106,21 +91,20 @@ fun LibraryFilterBottomSheet(
contentPadding = PaddingValues(top = 18.dp)
) {
LibrarySortOption(
sortOrder = if (perCategorySort) {
currentCategorySort.value.sortOrder
} else sortOrder,
sortOrderDescending = if (perCategorySort) {
currentCategorySort.value.sortOrderDescending
} else sortOrderDescending,
sortOrder = if (perCategorySort) currentCategory.value.sortOrder
else sortOrder,
sortOrderDescending = if (perCategorySort) currentCategory.value.sortOrderDescending
else sortOrderDescending,
onChange = { sortOrder, sortOrderDescending ->
if (perCategorySort) {
updateCategorySort(
SettingsEvent.OnUpdateCategorySort(
categoryId = currentCategorySort.value.categoryId,
updateCategory(
SettingsEvent.OnUpdateCategory(
currentCategory.value.copy(
sortOrder = sortOrder,
sortOrderDescending = sortOrderDescending
)
)
)
} else {
changeSortOrder(sortOrder)
changeSortOrderDescending(sortOrderDescending)

View file

@ -40,7 +40,7 @@ fun LibraryMoveDialog(
val selectedCategories = remember {
mutableStateListOf<Category>().apply {
clear()
categories.forEach { category ->
categories.filterNot { it.id == -1 }.forEach { category ->
if (selectedBooks.value.all { it.data.categories.any { it == category.id } }) {
add(category)
}
@ -69,7 +69,7 @@ fun LibraryMoveDialog(
},
withContent = true,
items = {
items(categories) { category ->
items(categories.filterNot { it.id == -1 }) { category ->
val selected = remember(
category,
selectedCategories

View file

@ -16,7 +16,6 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import ua.acclorite.book_story.core.helpers.compareByWithOrder
import ua.acclorite.book_story.domain.model.library.Category
import ua.acclorite.book_story.domain.model.library.CategorySort
import ua.acclorite.book_story.presentation.library.LibraryEvent
import ua.acclorite.book_story.presentation.library.model.LibraryLayout
import ua.acclorite.book_story.presentation.library.model.LibrarySortOrder
@ -31,7 +30,6 @@ fun LibraryPager(
categories: List<Category>,
showDefaultCategory: Boolean,
perCategorySort: Boolean,
categoriesSort: List<CategorySort>,
sortOrder: LibrarySortOrder,
sortOrderDescending: Boolean,
layout: LibraryLayout,
@ -51,7 +49,6 @@ fun LibraryPager(
val categorizedBooks = remember(
books,
categories,
categoriesSort,
perCategorySort,
sortOrder,
sortOrderDescending,
@ -76,61 +73,35 @@ fun LibraryPager(
derivedStateOf {
val categorizedBooks = mutableListOf<List<SelectableBook>>()
if (showDefaultCategory) {
val categoryIds = categories.map { it.id }.toSet()
categories
.filterNot { if (!showDefaultCategory) it.id == -1 else false }
.sortedBy { it.order }
.forEach { category ->
categorizedBooks.add(
books.filter { book ->
book.data.categories.none { category -> category in categoryIds }
}.let {
if (category.id == -1) {
book.data.categories.none { it in categoryIds }
} else {
book.data.categories.any { it == category.id }
}
}.let { books ->
if (perCategorySort) {
val categorySort = categoriesSort.firstOrNull {
it.categoryId == -1
} ?: CategorySort(
categoryId = -1,
sortOrder = LibrarySortOrder.LAST_READ,
sortOrderDescending = true
)
return@let it.sortBooks(
categorySort.sortOrder,
categorySort.sortOrderDescending
return@let books.sortBooks(
category.sortOrder,
category.sortOrderDescending
)
}
return@let it
}
)
}
categories.sortedBy { it.order }.forEach { category ->
categorizedBooks.add(
books
.filter { it.data.categories.any { category.id == it } }
.let {
if (perCategorySort) {
val categorySort = categoriesSort.firstOrNull {
it.categoryId == category.id
} ?: CategorySort(
categoryId = category.id,
sortOrder = LibrarySortOrder.LAST_READ,
sortOrderDescending = true
)
return@let it.sortBooks(
categorySort.sortOrder,
categorySort.sortOrderDescending
)
}
return@let it
return@let books
}
)
}
return@derivedStateOf categorizedBooks.let {
if (!perCategorySort) {
return@let it.map {
it.sortBooks(
return@let it.map { books ->
books.sortBooks(
sortOrder,
sortOrderDescending
)

View file

@ -19,7 +19,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import ua.acclorite.book_story.domain.model.library.Category
import ua.acclorite.book_story.domain.model.library.CategorySort
import ua.acclorite.book_story.presentation.library.LibraryEvent
import ua.acclorite.book_story.presentation.library.model.LibraryLayout
import ua.acclorite.book_story.presentation.library.model.LibrarySortOrder
@ -51,7 +50,6 @@ fun LibraryScaffold(
categories: List<Category>,
showDefaultCategory: Boolean,
perCategorySort: Boolean,
categoriesSort: List<CategorySort>,
sortOrder: LibrarySortOrder,
sortOrderDescending: Boolean,
searchVisibility: (LibraryEvent.OnSearchVisibility) -> Unit,
@ -110,7 +108,6 @@ fun LibraryScaffold(
categories = categories,
showDefaultCategory = showDefaultCategory,
perCategorySort = perCategorySort,
categoriesSort = categoriesSort,
sortOrder = sortOrder,
sortOrderDescending = sortOrderDescending,
layout = layout,

View file

@ -85,7 +85,7 @@ fun LibraryTopBar(
defaultCategory
) {
derivedStateOf {
categories.map { category ->
categories.filterNot { it.id == -1 }.map { category ->
category to books.count { it.data.categories.any { it == category.id } }
}.toMutableList().apply {
if (showDefaultCategory) {

View file

@ -68,6 +68,9 @@ fun LibraryCategoriesOption() {
val settingsModel = hiltViewModel<SettingsModel>()
val state = settingsModel.state.collectAsStateWithLifecycle()
val categories = remember(state.value.categories) {
state.value.categories.filterNot { it.id == -1 }
}
val dialog = remember {
mutableStateOf<String?>(null)
}
@ -94,12 +97,15 @@ fun LibraryCategoriesOption() {
initialValue = selectedCategory.value?.title ?: "",
onDismiss = { dialog.value = null },
onAction = { title ->
selectedCategory.value?.let { selectedCategory ->
settingsModel.onEvent(
SettingsEvent.OnUpdateCategoryTitle(
id = selectedCategory.value?.id ?: -1,
SettingsEvent.OnUpdateCategory(
category = selectedCategory.copy(
title = title
)
)
)
}
}
)
}
@ -108,10 +114,10 @@ fun LibraryCategoriesOption() {
LibraryCategoriesRemoveDialog(
onDismiss = { dialog.value = null },
onAction = {
if (selectedCategory.value != null) {
selectedCategory.value?.let { selectedCategory ->
LibraryScreen.scrollToPageCompositionChannel.trySend(0)
settingsModel.onEvent(
SettingsEvent.OnRemoveCategory(selectedCategory.value!!)
SettingsEvent.OnRemoveCategory(selectedCategory)
)
}
dialog.value = null
@ -124,12 +130,12 @@ fun LibraryCategoriesOption() {
modifier = Modifier
.fillMaxWidth()
.animateContentSize(),
list = state.value.categories,
list = categories,
onSettle = { from, to ->
LibraryScreen.scrollToPageCompositionChannel.trySend(0)
settingsModel.onEvent(
SettingsEvent.OnUpdateCategoryOrder(
state.value.categories.toMutableList().apply {
categories.toMutableList().apply {
add(to, removeAt(from))
}
)