refactor: color preset entity

* I/O for repository calls
* Removed nullability from ColorPresetEntity
This commit is contained in:
Acclorite 2025-08-27 23:20:07 +03:00
parent b0c567bdc9
commit a45078cae5
No known key found for this signature in database
GPG key ID: 6E54C611F6EE8593
18 changed files with 468 additions and 151 deletions

View file

@ -0,0 +1,220 @@
{
"formatVersion": 1,
"database": {
"version": 14,
"identityHash": "cd99cc3f6288826f0ad96bc8fe7aca3a",
"entities": [
{
"tableName": "BookEntity",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `title` TEXT NOT NULL, `author` TEXT, `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"
},
{
"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)",
"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
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
},
{
"tableName": "CategorySortEntity",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`categoryId` INTEGER NOT NULL, `sortOrder` TEXT NOT NULL, `sortOrderDescending` INTEGER NOT NULL, PRIMARY KEY(`categoryId`))",
"fields": [
{
"fieldPath": "categoryId",
"columnName": "categoryId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "sortOrder",
"columnName": "sortOrder",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "sortOrderDescending",
"columnName": "sortOrderDescending",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"categoryId"
]
}
}
],
"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, 'cd99cc3f6288826f0ad96bc8fe7aca3a')"
]
}
}

View file

@ -9,7 +9,6 @@ package ua.acclorite.book_story.core
import android.graphics.Bitmap import android.graphics.Bitmap
typealias CoverImage = Bitmap typealias CoverImage = Bitmap
typealias Selected = Boolean
typealias BottomSheet = String typealias BottomSheet = String
typealias Dialog = String typealias Dialog = String
typealias Drawer = String typealias Drawer = String

View file

@ -19,7 +19,6 @@ import org.commonmark.node.HtmlBlock
import org.commonmark.node.IndentedCodeBlock import org.commonmark.node.IndentedCodeBlock
import org.commonmark.node.ThematicBreak import org.commonmark.node.ThematicBreak
import org.commonmark.parser.Parser import org.commonmark.parser.Parser
import ua.acclorite.book_story.data.local.room.BookDao
import ua.acclorite.book_story.data.local.room.BookDatabase import ua.acclorite.book_story.data.local.room.BookDatabase
import ua.acclorite.book_story.data.local.room.DatabaseHelper import ua.acclorite.book_story.data.local.room.DatabaseHelper
import javax.inject.Singleton import javax.inject.Singleton
@ -49,7 +48,7 @@ object AppModule {
@Provides @Provides
@Singleton @Singleton
fun provideBookDao(app: Application): BookDao { fun provideBookDatabase(app: Application): BookDatabase {
// Additional Migrations // Additional Migrations
DatabaseHelper.MIGRATION_7_8.removeBooksDir(app) DatabaseHelper.MIGRATION_7_8.removeBooksDir(app)
@ -57,14 +56,11 @@ object AppModule {
app, app,
BookDatabase::class.java, BookDatabase::class.java,
"book_db" "book_db"
) ).addMigrations(
.addMigrations( DatabaseHelper.MIGRATION_2_3, // creates LanguageHistoryEntity table(if does not exist)
DatabaseHelper.MIGRATION_2_3, // creates LanguageHistoryEntity table(if does not exist) DatabaseHelper.MIGRATION_4_5, // creates ColorPresetEntity 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_5_6, // creates FavoriteDirectoryEntity table(if does not exist) DatabaseHelper.MIGRATION_13_14, // remove nullability from ColorPresetEntity
) ).allowMainThreadQueries().build()
.allowMainThreadQueries()
.build()
.dao
} }
} }

View file

@ -13,7 +13,7 @@ import androidx.room.PrimaryKey
data class ColorPresetEntity( data class ColorPresetEntity(
@PrimaryKey(true) @PrimaryKey(true)
val id: Int? = null, val id: Int? = null,
val name: String?, val name: String,
val backgroundColor: Long, val backgroundColor: Long,
val fontColor: Long, val fontColor: Long,
val isSelected: Boolean, val isSelected: Boolean,

View file

@ -16,12 +16,8 @@ import androidx.room.Upsert
import ua.acclorite.book_story.data.local.dto.BookEntity 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.CategoryEntity
import ua.acclorite.book_story.data.local.dto.CategorySortEntity 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 ua.acclorite.book_story.data.local.dto.HistoryEntity
/**
* Class to manipulate Room database.
*/
@Dao @Dao
interface BookDao { interface BookDao {
@ -73,27 +69,6 @@ interface BookDao {
/* - - - - - - - - - - - - - - - - - - - - - - */ /* - - - - - - - - - - - - - - - - - - - - - - */
/* ------ ColorPresetEntity ----------------- */
@Upsert
suspend fun updateColorPreset(colorPreset: ColorPresetEntity)
@Query("SELECT `order` FROM colorpresetentity WHERE :id=id")
suspend fun getColorPresetOrder(id: Int): Int
@Query("SELECT COUNT(*) FROM colorpresetentity")
suspend fun getColorPresetsSize(): Int
@Query("SELECT * FROM colorpresetentity")
suspend fun getColorPresets(): List<ColorPresetEntity>
@Delete
suspend fun deleteColorPreset(colorPreset: ColorPresetEntity)
@Query("DELETE FROM colorpresetentity")
suspend fun deleteColorPresets()
/* - - - - - - - - - - - - - - - - - - - - - - */
/* ------ CategoryEntity ----------------- */ /* ------ CategoryEntity ----------------- */
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertCategory( suspend fun insertCategory(

View file

@ -30,7 +30,7 @@ import java.io.File
CategoryEntity::class, CategoryEntity::class,
CategorySortEntity::class CategorySortEntity::class
], ],
version = 13, version = 14,
autoMigrations = [ autoMigrations = [
AutoMigration(1, 2), AutoMigration(1, 2),
AutoMigration(2, 3), AutoMigration(2, 3),
@ -44,11 +44,13 @@ import java.io.File
AutoMigration(10, 11), AutoMigration(10, 11),
AutoMigration(11, 12), AutoMigration(11, 12),
AutoMigration(12, 13), AutoMigration(12, 13),
AutoMigration(13, 14),
], ],
exportSchema = true exportSchema = true
) )
abstract class BookDatabase : RoomDatabase() { abstract class BookDatabase : RoomDatabase() {
abstract val dao: BookDao abstract val bookDao: BookDao
abstract val colorPresetDao: ColorPresetDao
} }
@Suppress("ClassName") @Suppress("ClassName")
@ -124,4 +126,36 @@ object DatabaseHelper {
@DeleteColumn("BookEntity", "category") @DeleteColumn("BookEntity", "category")
class MIGRATION_9_10 : AutoMigrationSpec class MIGRATION_9_10 : AutoMigrationSpec
val MIGRATION_13_14 = object : Migration(13, 14) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL(
"""
CREATE TABLE IF NOT EXISTS ColorPresetEntity (
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
)
"""
)
database.execSQL(
"""
INSERT INTO ColorPresetEntity_new (id, name, backgroundColor, fontColor, isSelected, `order`)
SELECT
id,
COALESCE(name, ''),
backgroundColor,
fontColor,
isSelected,
`order`
FROM ColorPresetEntity
"""
)
database.execSQL("DROP TABLE ColorPresetEntity")
database.execSQL("ALTER TABLE ColorPresetEntity_new RENAME TO ColorPresetEntity")
}
}
} }

View file

@ -0,0 +1,34 @@
/*
* 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.Query
import androidx.room.Upsert
import ua.acclorite.book_story.data.local.dto.ColorPresetEntity
@Dao
interface ColorPresetDao {
@Upsert
suspend fun updateColorPreset(colorPreset: ColorPresetEntity)
@Query("SELECT `order` FROM colorpresetentity WHERE id = :id")
suspend fun getColorPresetOrder(id: Int): Int
@Query("SELECT COUNT(*) FROM colorpresetentity")
suspend fun getColorPresetsSize(): Int
@Query("SELECT * FROM colorpresetentity ORDER BY `order` ASC")
suspend fun getColorPresets(): List<ColorPresetEntity>
@Delete
suspend fun deleteColorPreset(colorPreset: ColorPresetEntity)
@Query("DELETE FROM colorpresetentity")
suspend fun deleteColorPresets()
}

View file

@ -6,8 +6,10 @@
package ua.acclorite.book_story.data.repository package ua.acclorite.book_story.data.repository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import ua.acclorite.book_story.core.CoverImage import ua.acclorite.book_story.core.CoverImage
import ua.acclorite.book_story.data.local.room.BookDao import ua.acclorite.book_story.data.local.room.BookDatabase
import ua.acclorite.book_story.data.mapper.book.BookMapper import ua.acclorite.book_story.data.mapper.book.BookMapper
import ua.acclorite.book_story.data.mapper.file.FileMapper import ua.acclorite.book_story.data.mapper.file.FileMapper
import ua.acclorite.book_story.data.parser.FileParser import ua.acclorite.book_story.data.parser.FileParser
@ -22,7 +24,7 @@ import javax.inject.Singleton
@Singleton @Singleton
class BookRepositoryImpl @Inject constructor( class BookRepositoryImpl @Inject constructor(
private val database: BookDao, private val database: BookDatabase,
private val bookMapper: BookMapper, private val bookMapper: BookMapper,
private val fileMapper: FileMapper, private val fileMapper: FileMapper,
private val fileParser: FileParser, private val fileParser: FileParser,
@ -31,46 +33,62 @@ class BookRepositoryImpl @Inject constructor(
) : BookRepository { ) : BookRepository {
override suspend fun searchBooks(query: String): Result<List<Book>> = runCatching { override suspend fun searchBooks(query: String): Result<List<Book>> = runCatching {
database.searchBooks(query).map { bookMapper.toBook(it) } withContext(Dispatchers.IO) {
database.bookDao.searchBooks(query).map { bookMapper.toBook(it) }
}
} }
override suspend fun getBook(bookId: Int): Result<Book> = runCatching { override suspend fun getBook(bookId: Int): Result<Book> = runCatching {
database.findBookById(bookId).let { withContext(Dispatchers.IO) {
if (it == null) throw NoSuchElementException("Couldn't get book [$bookId].") database.bookDao.findBookById(bookId).let {
else bookMapper.toBook(it) if (it == null) throw NoSuchElementException("Couldn't get book [$bookId].")
else bookMapper.toBook(it)
}
} }
} }
override suspend fun getText(bookId: Int): Result<List<ReaderText>> { override suspend fun getText(bookId: Int): Result<List<ReaderText>> {
return getBook(bookId) return withContext(Dispatchers.IO) {
.mapCatching { fileProvider.getFileFromBook(it).getOrThrow() } getBook(bookId)
.mapCatching { textParser.parse(it) } .mapCatching { fileProvider.getFileFromBook(it).getOrThrow() }
.mapCatching { textParser.parse(it) }
}
} }
override suspend fun getFileFromBook(bookId: Int): Result<File> { override suspend fun getFileFromBook(bookId: Int): Result<File> {
return getBook(bookId) return withContext(Dispatchers.IO) {
.mapCatching { fileProvider.getFileFromBook(it).getOrThrow() } getBook(bookId)
.mapCatching { fileMapper.toFile(it) } .mapCatching { fileProvider.getFileFromBook(it).getOrThrow() }
.mapCatching { fileMapper.toFile(it) }
}
} }
override suspend fun addBook(book: Book): Result<Unit> = runCatching { override suspend fun addBook(book: Book): Result<Unit> = runCatching {
database.insertBook(bookMapper.toBookEntity(book)) withContext(Dispatchers.IO) {
database.bookDao.insertBook(bookMapper.toBookEntity(book))
}
} }
override suspend fun updateBook(book: Book): Result<Unit> = runCatching { override suspend fun updateBook(book: Book): Result<Unit> = runCatching {
database.updateBook(bookMapper.toBookEntity(book)).also { withContext(Dispatchers.IO) {
if (it == 0) throw Exception("Could not update book in database.") database.bookDao.updateBook(bookMapper.toBookEntity(book)).also {
if (it == 0) throw Exception("Could not update book in database.")
}
} }
} }
override suspend fun deleteBook(book: Book): Result<Unit> = runCatching { override suspend fun deleteBook(book: Book): Result<Unit> = runCatching {
database.deleteBook(bookMapper.toBookEntity(book)).also { withContext(Dispatchers.IO) {
if (it == 0) throw Exception("Could not delete book in database.") database.bookDao.deleteBook(bookMapper.toBookEntity(book)).also {
if (it == 0) throw Exception("Could not delete book in database.")
}
} }
} }
override suspend fun getDefaultCover(book: Book): Result<CoverImage?> = runCatching { override suspend fun getDefaultCover(book: Book): Result<CoverImage?> = runCatching {
return fileProvider.getFileFromBook(book) return withContext(Dispatchers.IO) {
.mapCatching { fileParser.parse(it)?.coverImage } fileProvider.getFileFromBook(book)
.mapCatching { fileParser.parse(it)?.coverImage }
}
} }
} }

View file

@ -6,7 +6,9 @@
package ua.acclorite.book_story.data.repository package ua.acclorite.book_story.data.repository
import ua.acclorite.book_story.data.local.room.BookDao 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.CategoryMapper
import ua.acclorite.book_story.data.mapper.category_sort.CategorySortMapper 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.Category
@ -17,57 +19,72 @@ import javax.inject.Singleton
@Singleton @Singleton
class CategoryRepositoryImpl @Inject constructor( class CategoryRepositoryImpl @Inject constructor(
private val database: BookDao, private val database: BookDatabase,
private val categoryMapper: CategoryMapper, private val categoryMapper: CategoryMapper,
private val categorySortMapper: CategorySortMapper, private val categorySortMapper: CategorySortMapper,
) : CategoryRepository { ) : CategoryRepository {
override suspend fun addCategory(category: Category): Result<Unit> = runCatching { override suspend fun addCategory(category: Category): Result<Unit> = runCatching {
database.insertCategory( withContext(Dispatchers.IO) {
categoryMapper.toCategoryEntity( database.bookDao.insertCategory(
category.copy( categoryMapper.toCategoryEntity(
order = database.getCategoriesCount() category.copy(
order = database.bookDao.getCategoriesCount()
)
) )
) )
) }
} }
override suspend fun getCategories(): Result<List<Category>> = runCatching { override suspend fun getCategories(): Result<List<Category>> = runCatching {
database.getCategories().map { categoryMapper.toCategory(it) }.sortedBy { it.order } withContext(Dispatchers.IO) {
database.bookDao.getCategories().map { categoryMapper.toCategory(it) }
.sortedBy { it.order }
}
} }
override suspend fun updateCategory(category: Category): Result<Unit> = runCatching { override suspend fun updateCategory(category: Category): Result<Unit> = runCatching {
database.updateCategoryTitle( withContext(Dispatchers.IO) {
id = category.id, database.bookDao.updateCategoryTitle(
title = category.title id = category.id,
) title = category.title
updateOrder(getCategories().getOrThrow()) )
updateOrder(getCategories().getOrThrow())
}
} }
override suspend fun updateOrder(categories: List<Category>): Result<Unit> = runCatching { override suspend fun updateOrder(categories: List<Category>): Result<Unit> = runCatching {
categories.forEachIndexed { index, category -> withContext(Dispatchers.IO) {
database.updateCategoryOrder(category.id, index) categories.forEachIndexed { index, category ->
database.bookDao.updateCategoryOrder(category.id, index)
}
} }
} }
override suspend fun deleteCategory(category: Category): Result<Unit> = runCatching { override suspend fun deleteCategory(category: Category): Result<Unit> = runCatching {
database.deleteCategory(categoryMapper.toCategoryEntity(category)) withContext(Dispatchers.IO) {
database.deleteCategorySortEntity(category.id) database.bookDao.deleteCategory(categoryMapper.toCategoryEntity(category))
database.bookDao.deleteCategorySortEntity(category.id)
updateOrder(getCategories().getOrThrow()) updateOrder(getCategories().getOrThrow())
}
} }
override suspend fun updateCategorySorting( override suspend fun updateCategorySorting(
categorySort: CategorySort categorySort: CategorySort
): Result<Unit> = runCatching { ): Result<Unit> = runCatching {
database.updateCategorySort( withContext(Dispatchers.IO) {
categorySortMapper.toCategorySortEntity(categorySort) database.bookDao.updateCategorySort(
) categorySortMapper.toCategorySortEntity(categorySort)
)
}
} }
override suspend fun getCategorySorting(): Result<List<CategorySort>> = runCatching { override suspend fun getCategorySorting(): Result<List<CategorySort>> = runCatching {
database.getCategorySortEntities().map { withContext(Dispatchers.IO) {
categorySortMapper.toCategorySort(it) database.bookDao.getCategorySortEntities().map {
categorySortMapper.toCategorySort(it)
}
} }
} }
} }

View file

@ -6,7 +6,9 @@
package ua.acclorite.book_story.data.repository package ua.acclorite.book_story.data.repository
import ua.acclorite.book_story.data.local.room.BookDao import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import ua.acclorite.book_story.data.local.room.BookDatabase
import ua.acclorite.book_story.data.mapper.color_preset.ColorPresetMapper import ua.acclorite.book_story.data.mapper.color_preset.ColorPresetMapper
import ua.acclorite.book_story.domain.model.reader.ColorPreset import ua.acclorite.book_story.domain.model.reader.ColorPreset
import ua.acclorite.book_story.domain.repository.ColorPresetRepository import ua.acclorite.book_story.domain.repository.ColorPresetRepository
@ -15,56 +17,67 @@ import javax.inject.Singleton
@Singleton @Singleton
class ColorPresetRepositoryImpl @Inject constructor( class ColorPresetRepositoryImpl @Inject constructor(
private val database: BookDao, private val database: BookDatabase,
private val colorPresetMapper: ColorPresetMapper private val colorPresetMapper: ColorPresetMapper
) : ColorPresetRepository { ) : ColorPresetRepository {
override suspend fun getColorPresets(): Result<List<ColorPreset>> = runCatching { override suspend fun getColorPresets(): Result<List<ColorPreset>> = runCatching {
database.getColorPresets() withContext(Dispatchers.IO) {
.sortedBy { it.order } database.colorPresetDao.getColorPresets().map {
.map { colorPresetMapper.toColorPreset(it) } colorPresetMapper.toColorPreset(it)
}
}
} }
override suspend fun updateColorPreset( override suspend fun updateColorPreset(
colorPreset: ColorPreset colorPreset: ColorPreset
): Result<Unit> = runCatching { ): Result<Unit> = runCatching {
database.updateColorPreset( withContext(Dispatchers.IO) {
colorPresetMapper.toColorPresetEntity( database.colorPresetDao.updateColorPreset(
colorPreset = colorPreset, colorPresetMapper.toColorPresetEntity(
order = if (colorPreset.id != -1) database.getColorPresetOrder(colorPreset.id) colorPreset = colorPreset,
else database.getColorPresetsSize() order = if (colorPreset.id != -1) {
database.colorPresetDao.getColorPresetOrder(colorPreset.id)
} else database.colorPresetDao.getColorPresetsSize()
)
) )
) }
} }
override suspend fun selectColorPreset( override suspend fun selectColorPreset(
colorPreset: ColorPreset colorPreset: ColorPreset
): Result<Unit> = runCatching { ): Result<Unit> = runCatching {
database.getColorPresets().map { withContext(Dispatchers.IO) {
it.copy(isSelected = it.id == colorPreset.id) database.colorPresetDao.getColorPresets().map {
}.forEach { it.copy(isSelected = it.id == colorPreset.id)
database.updateColorPreset(it) }.forEach {
database.colorPresetDao.updateColorPreset(it)
}
} }
} }
override suspend fun reorderColorPresets( override suspend fun reorderColorPresets(
colorPresets: List<ColorPreset> colorPresets: List<ColorPreset>
): Result<Unit> = runCatching { ): Result<Unit> = runCatching {
database.deleteColorPresets() withContext(Dispatchers.IO) {
colorPresets.forEachIndexed { index, colorPreset -> database.colorPresetDao.deleteColorPresets()
database.updateColorPreset( colorPresets.forEachIndexed { index, colorPreset ->
colorPresetMapper.toColorPresetEntity(colorPreset, order = index) database.colorPresetDao.updateColorPreset(
) colorPresetMapper.toColorPresetEntity(colorPreset, order = index)
)
}
} }
} }
override suspend fun deleteColorPreset( override suspend fun deleteColorPreset(
colorPreset: ColorPreset colorPreset: ColorPreset
): Result<Unit> = runCatching { ): Result<Unit> = runCatching {
database.deleteColorPreset( withContext(Dispatchers.IO) {
colorPresetMapper.toColorPresetEntity( database.colorPresetDao.deleteColorPreset(
colorPreset, -1 colorPresetMapper.toColorPresetEntity(
colorPreset, -1
)
) )
) }
} }
} }

View file

@ -6,8 +6,10 @@
package ua.acclorite.book_story.data.repository package ua.acclorite.book_story.data.repository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import ua.acclorite.book_story.core.data.ExtensionsData import ua.acclorite.book_story.core.data.ExtensionsData
import ua.acclorite.book_story.data.local.room.BookDao import ua.acclorite.book_story.data.local.room.BookDatabase
import ua.acclorite.book_story.data.mapper.file.FileMapper import ua.acclorite.book_story.data.mapper.file.FileMapper
import ua.acclorite.book_story.data.model.common.BookWithCover import ua.acclorite.book_story.data.model.common.BookWithCover
import ua.acclorite.book_story.data.model.file.CachedFile import ua.acclorite.book_story.data.model.file.CachedFile
@ -18,28 +20,26 @@ import ua.acclorite.book_story.domain.service.FileProvider
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
/**
* File System repository.
* Manages all File System related work.
*/
@Singleton @Singleton
class FileSystemRepositoryImpl @Inject constructor( class FileSystemRepositoryImpl @Inject constructor(
private val database: BookDao, private val database: BookDatabase,
private val fileMapper: FileMapper, private val fileMapper: FileMapper,
private val fileParser: FileParser, private val fileParser: FileParser,
private val fileProvider: FileProvider private val fileProvider: FileProvider
) : FileSystemRepository { ) : FileSystemRepository {
override suspend fun searchFiles(query: String): Result<List<File>> { override suspend fun searchFiles(query: String): Result<List<File>> {
return fileProvider.getStorageFiles().mapCatching { storages -> return withContext(Dispatchers.IO) {
val existingFiles = database.searchBooks("").map { it.filePath } fileProvider.getStorageFiles().mapCatching { storages ->
val existingFiles = database.bookDao.searchBooks("").map { it.filePath }
storages.map { storage -> storages.map { storage ->
storage.getFilesFromStorage( storage.getFilesFromStorage(
query = query, query = query,
existingFiles = existingFiles existingFiles = existingFiles
) )
}.flatten() }.flatten()
}
} }
} }
@ -74,7 +74,9 @@ class FileSystemRepositoryImpl @Inject constructor(
} }
override suspend fun getBookFromFile(file: File): Result<BookWithCover> = runCatching { override suspend fun getBookFromFile(file: File): Result<BookWithCover> = runCatching {
fileParser.parse(fileMapper.toCachedFile(file)) withContext(Dispatchers.IO) {
?: throw Exception("Could not parse ${file.name}.") fileParser.parse(fileMapper.toCachedFile(file))
?: throw Exception("Could not parse ${file.name}.")
}
} }
} }

View file

@ -6,7 +6,9 @@
package ua.acclorite.book_story.data.repository package ua.acclorite.book_story.data.repository
import ua.acclorite.book_story.data.local.room.BookDao import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import ua.acclorite.book_story.data.local.room.BookDatabase
import ua.acclorite.book_story.data.mapper.history.HistoryMapper import ua.acclorite.book_story.data.mapper.history.HistoryMapper
import ua.acclorite.book_story.domain.model.history.History import ua.acclorite.book_story.domain.model.history.History
import ua.acclorite.book_story.domain.repository.HistoryRepository import ua.acclorite.book_story.domain.repository.HistoryRepository
@ -15,40 +17,52 @@ import javax.inject.Singleton
@Singleton @Singleton
class HistoryRepositoryImpl @Inject constructor( class HistoryRepositoryImpl @Inject constructor(
private val database: BookDao, private val database: BookDatabase,
private val historyMapper: HistoryMapper private val historyMapper: HistoryMapper
) : HistoryRepository { ) : HistoryRepository {
override suspend fun getHistoryForBook(bookId: Int): Result<History> = runCatching { override suspend fun getHistoryForBook(bookId: Int): Result<History> = runCatching {
database.getHistoryForBook(bookId).let { withContext(Dispatchers.IO) {
if (it == null) throw NoSuchElementException("Could not get history from [$bookId].") database.bookDao.getHistoryForBook(bookId).let {
else historyMapper.toHistory(it) if (it == null) throw NoSuchElementException("Could not get history from [$bookId].")
else historyMapper.toHistory(it)
}
} }
} }
override suspend fun addHistory(history: History): Result<Unit> = runCatching { override suspend fun addHistory(history: History): Result<Unit> = runCatching {
database.insertHistory(historyMapper.toHistoryEntity(history)) withContext(Dispatchers.IO) {
database.bookDao.insertHistory(historyMapper.toHistoryEntity(history))
}
} }
override suspend fun getHistory(): Result<List<History>> = runCatching { override suspend fun getHistory(): Result<List<History>> = runCatching {
database.getHistory().map { historyMapper.toHistory(it) } withContext(Dispatchers.IO) {
database.bookDao.getHistory().map { historyMapper.toHistory(it) }
}
} }
override suspend fun deleteWholeHistory(): Result<Unit> = runCatching { override suspend fun deleteWholeHistory(): Result<Unit> = runCatching {
database.deleteWholeHistory().also { withContext(Dispatchers.IO) {
if (it == 0) throw Exception("Could not delete whole history in database.") database.bookDao.deleteWholeHistory().also {
if (it == 0) throw Exception("Could not delete whole history in database.")
}
} }
} }
override suspend fun deleteHistoryForBook(bookId: Int): Result<Unit> = runCatching { override suspend fun deleteHistoryForBook(bookId: Int): Result<Unit> = runCatching {
database.deleteHistoryForBook(bookId = bookId).also { withContext(Dispatchers.IO) {
if (it == 0) throw Exception("Could not delete history for book [$bookId] in database.") database.bookDao.deleteHistoryForBook(bookId = bookId).also {
if (it == 0) throw Exception("Could not delete history for book [$bookId] in database.")
}
} }
} }
override suspend fun deleteHistory(history: History): Result<Unit> = runCatching { override suspend fun deleteHistory(history: History): Result<Unit> = runCatching {
database.deleteHistory(historyMapper.toHistoryEntity(history)).also { withContext(Dispatchers.IO) {
if (it == 0) throw Exception("Could not delete history in database.") database.bookDao.deleteHistory(historyMapper.toHistoryEntity(history)).also {
if (it == 0) throw Exception("Could not delete history in database.")
}
} }
} }
} }

View file

@ -8,20 +8,19 @@ package ua.acclorite.book_story.domain.model.reader
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import ua.acclorite.book_story.core.Selected
@Immutable @Immutable
data class ColorPreset( data class ColorPreset(
val id: Int, val id: Int,
val name: String?, val name: String,
val backgroundColor: Color, val backgroundColor: Color,
val fontColor: Color, val fontColor: Color,
val isSelected: Selected val isSelected: Boolean
) { ) {
companion object { companion object {
val default = ColorPreset( val default = ColorPreset(
id = -1, id = -1,
name = null, name = "",
backgroundColor = Color(0xFFFAF8FF), // Blue Light Surface (hardcoded) backgroundColor = Color(0xFFFAF8FF), // Blue Light Surface (hardcoded)
fontColor = Color(0xFF44464F), // Blue Light OnSurfaceVariant (hardcoded) fontColor = Color(0xFF44464F), // Blue Light OnSurfaceVariant (hardcoded)
isSelected = false isSelected = false

View file

@ -7,11 +7,10 @@
package ua.acclorite.book_story.presentation.library.model package ua.acclorite.book_story.presentation.library.model
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import ua.acclorite.book_story.core.Selected
import ua.acclorite.book_story.domain.model.library.Book import ua.acclorite.book_story.domain.model.library.Book
@Immutable @Immutable
data class SelectableBook( data class SelectableBook(
val data: Book, val data: Book,
val selected: Selected val selected: Boolean
) )

View file

@ -7,11 +7,10 @@
package ua.acclorite.book_story.presentation.library.model package ua.acclorite.book_story.presentation.library.model
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import ua.acclorite.book_story.core.Selected
import ua.acclorite.book_story.data.model.common.NullableBook import ua.acclorite.book_story.data.model.common.NullableBook
@Immutable @Immutable
data class SelectableNullableBook( data class SelectableNullableBook(
val data: NullableBook, val data: NullableBook,
val selected: Selected val selected: Boolean
) )

View file

@ -23,7 +23,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import ua.acclorite.book_story.core.Selected
/** /**
* Modal Drawer Selectable Item. * Modal Drawer Selectable Item.
@ -38,7 +37,7 @@ import ua.acclorite.book_story.core.Selected
@Composable @Composable
fun ModalDrawerSelectableItem( fun ModalDrawerSelectableItem(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
selected: Selected, selected: Boolean,
enabled: Boolean = true, enabled: Boolean = true,
onClick: () -> Unit, onClick: () -> Unit,
content: @Composable RowScope.() -> Unit content: @Composable RowScope.() -> Unit

View file

@ -28,7 +28,7 @@ fun SettingsEffects(effects: SharedFlow<SettingsEffect>) {
is SettingsEffect.OnSwitchedColorPreset -> { is SettingsEffect.OnSwitchedColorPreset -> {
context.getString( context.getString(
R.string.color_preset_selected_query, R.string.color_preset_selected_query,
if (effect.newColorPreset.name.isNullOrBlank()) { if (effect.newColorPreset.name.isBlank()) {
context.getString( context.getString(
R.string.color_preset_query, R.string.color_preset_query,
effect.newColorPreset.id.toString() effect.newColorPreset.id.toString()

View file

@ -63,7 +63,6 @@ import sh.calvin.reorderable.ReorderableCollectionItemScope
import sh.calvin.reorderable.ReorderableItem import sh.calvin.reorderable.ReorderableItem
import sh.calvin.reorderable.rememberReorderableLazyListState import sh.calvin.reorderable.rememberReorderableLazyListState
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.core.Selected
import ua.acclorite.book_story.domain.model.reader.ColorPreset import ua.acclorite.book_story.domain.model.reader.ColorPreset
import ua.acclorite.book_story.presentation.settings.SettingsEvent import ua.acclorite.book_story.presentation.settings.SettingsEvent
import ua.acclorite.book_story.presentation.settings.SettingsModel import ua.acclorite.book_story.presentation.settings.SettingsModel
@ -233,7 +232,7 @@ fun ColorPresetOption(backgroundColor: Color) {
@Composable @Composable
private fun ReorderableCollectionItemScope.ColorPresetOptionRowItem( private fun ReorderableCollectionItemScope.ColorPresetOptionRowItem(
colorPreset: ColorPreset, colorPreset: ColorPreset,
isSelected: Selected, isSelected: Boolean,
canDrag: Boolean, canDrag: Boolean,
enableAnimation: Boolean, enableAnimation: Boolean,
onDragStopped: () -> Unit, onDragStopped: () -> Unit,
@ -241,14 +240,14 @@ private fun ReorderableCollectionItemScope.ColorPresetOptionRowItem(
) { ) {
val context = LocalContext.current val context = LocalContext.current
val title = remember(colorPreset) { val title = remember(colorPreset) {
if ((colorPreset.name ?: "").isBlank()) { if (colorPreset.name.isBlank()) {
return@remember context.getString( return@remember context.getString(
R.string.color_preset_query, R.string.color_preset_query,
colorPreset.id.toString() colorPreset.id.toString()
) )
} }
colorPreset.name!! colorPreset.name
} }
val borderColor = remember(isSelected, colorPreset.fontColor) { val borderColor = remember(isSelected, colorPreset.fontColor) {
@ -344,7 +343,7 @@ private fun ColorPresetOptionConfigurationItem(
onAdd: () -> Unit onAdd: () -> Unit
) { ) {
val title = remember(selectedColorPreset.id) { val title = remember(selectedColorPreset.id) {
mutableStateOf(selectedColorPreset.name ?: "") mutableStateOf(selectedColorPreset.name)
} }
LaunchedEffect(title) { LaunchedEffect(title) {