refactor: history entity

* Added HistoryWithBook that fetches both HistoryEntity and BookEntity
* Removed nullability from History
This commit is contained in:
Acclorite 2025-08-29 13:51:39 +03:00
parent a45078cae5
commit 56b5aba67a
No known key found for this signature in database
GPG key ID: 6E54C611F6EE8593
14 changed files with 106 additions and 73 deletions

View file

@ -15,4 +15,4 @@ data class HistoryEntity(
val id: Int = 0,
val bookId: Int,
val time: Long
)
)

View file

@ -0,0 +1,19 @@
/*
* 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.Embedded
import androidx.room.Relation
data class HistoryWithBook(
@Embedded val history: HistoryEntity,
@Relation(
parentColumn = "bookId",
entityColumn = "id"
)
val book: BookEntity
)

View file

@ -16,7 +16,6 @@ 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
import ua.acclorite.book_story.data.local.dto.HistoryEntity
@Dao
interface BookDao {
@ -46,29 +45,6 @@ interface BookDao {
/* - - - - - - - - - - - - - - - - - - - - - - */
/* ------ HistoryEntity --------------------- */
@Query("SELECT * FROM historyentity")
suspend fun getHistory(): List<HistoryEntity>
@Query("SELECT * FROM historyentity WHERE bookId = :bookId ORDER BY time DESC LIMIT 1")
fun getHistoryForBook(bookId: Int): HistoryEntity?
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertHistory(
history: HistoryEntity
)
@Query("DELETE FROM historyentity")
suspend fun deleteWholeHistory(): Int
@Query("DELETE FROM historyentity WHERE bookId = :bookId")
suspend fun deleteHistoryForBook(bookId: Int): Int
@Delete
suspend fun deleteHistory(history: HistoryEntity): Int
/* - - - - - - - - - - - - - - - - - - - - - - */
/* ------ CategoryEntity ----------------- */
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertCategory(

View file

@ -50,6 +50,7 @@ import java.io.File
)
abstract class BookDatabase : RoomDatabase() {
abstract val bookDao: BookDao
abstract val historyDao: HistoryDao
abstract val colorPresetDao: ColorPresetDao
}

View file

@ -0,0 +1,41 @@
/*
* 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.Transaction
import ua.acclorite.book_story.data.local.dto.HistoryEntity
import ua.acclorite.book_story.data.local.dto.HistoryWithBook
@Dao
interface HistoryDao {
@Transaction
@Query("SELECT * FROM historyentity")
suspend fun getHistoryWithBook(): List<HistoryWithBook>
@Transaction
@Query("SELECT * FROM historyentity WHERE bookId = :bookId ORDER BY time DESC LIMIT 1")
suspend fun getHistoryForBook(bookId: Int): HistoryWithBook?
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertHistory(
history: HistoryEntity
)
@Query("DELETE FROM historyentity")
suspend fun deleteWholeHistory(): Int
@Query("DELETE FROM historyentity WHERE bookId = :bookId")
suspend fun deleteHistoryForBook(bookId: Int): Int
@Delete
suspend fun deleteHistory(history: HistoryEntity): Int
}

View file

@ -7,10 +7,10 @@
package ua.acclorite.book_story.data.mapper.history
import ua.acclorite.book_story.data.local.dto.HistoryEntity
import ua.acclorite.book_story.data.local.dto.HistoryWithBook
import ua.acclorite.book_story.domain.model.history.History
interface HistoryMapper {
suspend fun toHistoryEntity(history: History): HistoryEntity
suspend fun toHistory(historyEntity: HistoryEntity): History
suspend fun toHistory(historyWithBook: HistoryWithBook): History
}

View file

@ -7,24 +7,27 @@
package ua.acclorite.book_story.data.mapper.history
import ua.acclorite.book_story.data.local.dto.HistoryEntity
import ua.acclorite.book_story.data.local.dto.HistoryWithBook
import ua.acclorite.book_story.data.mapper.book.BookMapper
import ua.acclorite.book_story.domain.model.history.History
import javax.inject.Inject
class HistoryMapperImpl @Inject constructor() : HistoryMapper {
class HistoryMapperImpl @Inject constructor(
private val bookMapper: BookMapper
) : HistoryMapper {
override suspend fun toHistoryEntity(history: History): HistoryEntity {
return HistoryEntity(
id = history.id,
bookId = history.bookId,
bookId = history.book.id,
time = history.time
)
}
override suspend fun toHistory(historyEntity: HistoryEntity): History {
override suspend fun toHistory(historyWithBook: HistoryWithBook): History {
return History(
historyEntity.id,
bookId = historyEntity.bookId,
book = null,
time = historyEntity.time
id = historyWithBook.history.id,
book = bookMapper.toBook(historyWithBook.book),
time = historyWithBook.history.time
)
}
}

View file

@ -23,7 +23,7 @@ class HistoryRepositoryImpl @Inject constructor(
override suspend fun getHistoryForBook(bookId: Int): Result<History> = runCatching {
withContext(Dispatchers.IO) {
database.bookDao.getHistoryForBook(bookId).let {
database.historyDao.getHistoryForBook(bookId).let {
if (it == null) throw NoSuchElementException("Could not get history from [$bookId].")
else historyMapper.toHistory(it)
}
@ -32,19 +32,19 @@ class HistoryRepositoryImpl @Inject constructor(
override suspend fun addHistory(history: History): Result<Unit> = runCatching {
withContext(Dispatchers.IO) {
database.bookDao.insertHistory(historyMapper.toHistoryEntity(history))
database.historyDao.insertHistory(historyMapper.toHistoryEntity(history))
}
}
override suspend fun getHistory(): Result<List<History>> = runCatching {
withContext(Dispatchers.IO) {
database.bookDao.getHistory().map { historyMapper.toHistory(it) }
database.historyDao.getHistoryWithBook().map { historyMapper.toHistory(it) }
}
}
override suspend fun deleteWholeHistory(): Result<Unit> = runCatching {
withContext(Dispatchers.IO) {
database.bookDao.deleteWholeHistory().also {
database.historyDao.deleteWholeHistory().also {
if (it == 0) throw Exception("Could not delete whole history in database.")
}
}
@ -52,7 +52,7 @@ class HistoryRepositoryImpl @Inject constructor(
override suspend fun deleteHistoryForBook(bookId: Int): Result<Unit> = runCatching {
withContext(Dispatchers.IO) {
database.bookDao.deleteHistoryForBook(bookId = bookId).also {
database.historyDao.deleteHistoryForBook(bookId = bookId).also {
if (it == 0) throw Exception("Could not delete history for book [$bookId] in database.")
}
}
@ -60,7 +60,7 @@ class HistoryRepositoryImpl @Inject constructor(
override suspend fun deleteHistory(history: History): Result<Unit> = runCatching {
withContext(Dispatchers.IO) {
database.bookDao.deleteHistory(historyMapper.toHistoryEntity(history)).also {
database.historyDao.deleteHistory(historyMapper.toHistoryEntity(history)).also {
if (it == 0) throw Exception("Could not delete history in database.")
}
}

View file

@ -11,8 +11,7 @@ import ua.acclorite.book_story.domain.model.library.Book
@Immutable
data class History(
val id: Int = 0,
val bookId: Int,
val book: Book?,
val id: Int,
val book: Book,
val time: Long
)

View file

@ -17,14 +17,14 @@ class AddHistoryUseCase @Inject constructor(
) {
suspend operator fun invoke(history: History) {
logI("Inserting history for [${history.bookId}].")
logI("Inserting history for [${history.book.id}].")
historyRepository.addHistory(history = history).fold(
onSuccess = {
logI("Successfully inserted history for [${history.bookId}].")
logI("Successfully inserted history for [${history.book.id}].")
},
onFailure = {
logE("Could not insert history for [${history.bookId}] with error: ${it.message}")
logE("Could not insert history for [${history.book.id}] with error: ${it.message}")
}
)
}

View file

@ -9,7 +9,6 @@ package ua.acclorite.book_story.domain.use_case.history
import ua.acclorite.book_story.core.log.logE
import ua.acclorite.book_story.core.log.logI
import ua.acclorite.book_story.domain.model.history.History
import ua.acclorite.book_story.domain.repository.BookRepository
import ua.acclorite.book_story.domain.repository.HistoryRepository
import ua.acclorite.book_story.presentation.history.model.GroupedHistory
import java.time.Instant
@ -19,7 +18,6 @@ import java.time.format.DateTimeFormatter
import javax.inject.Inject
class GetHistoryUseCase @Inject constructor(
private val bookRepository: BookRepository,
private val historyRepository: HistoryRepository
) {
@ -40,27 +38,19 @@ class GetHistoryUseCase @Inject constructor(
}
fun filterMaxElementsById(elements: List<History>): List<History> {
val groupedById = elements.groupBy { it.bookId }
val groupedById = elements.groupBy { it.book.id }
val maxElementsById = groupedById.map { (_, values) ->
values.maxByOrNull { it.time }
}
return maxElementsById.filterNotNull()
}
val query = query.lowercase().trim()
return runCatching {
historyRepository.getHistory().getOrThrow().sortedByDescending { it.time }
.mapNotNull { history ->
val book = bookRepository.getBook(history.bookId).getOrNull()
if (
book == null
|| !book.title.lowercase().trim().contains(query.lowercase().trim())
) return@mapNotNull null
history.copy(book = book)
}
.groupBy { history ->
getDayLabel(history.time)
}
historyRepository.getHistory().getOrThrow()
.filter { history -> history.book.title.lowercase().trim().contains(query) }
.sortedByDescending { history -> history.time }
.groupBy { history -> getDayLabel(history.time) }
.map { (day, history) -> GroupedHistory(day, filterMaxElementsById(history)) }
}.fold(
onSuccess = {

View file

@ -24,6 +24,7 @@ import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import ua.acclorite.book_story.domain.model.history.History
import ua.acclorite.book_story.domain.use_case.book.GetBookUseCase
import ua.acclorite.book_story.domain.use_case.history.AddHistoryUseCase
import ua.acclorite.book_story.domain.use_case.history.DeleteHistoryUseCase
import ua.acclorite.book_story.domain.use_case.history.DeleteWholeHistoryUseCase
@ -38,7 +39,8 @@ class HistoryModel @Inject constructor(
private val getHistoryUseCase: GetHistoryUseCase,
private val addHistoryUseCase: AddHistoryUseCase,
private val deleteHistoryUseCase: DeleteHistoryUseCase,
private val deleteWholeHistoryUseCase: DeleteWholeHistoryUseCase
private val deleteWholeHistoryUseCase: DeleteWholeHistoryUseCase,
private val getBookUseCase: GetBookUseCase
) : ViewModel() {
private val mutex = Mutex()
@ -72,13 +74,15 @@ class HistoryModel @Inject constructor(
}
viewModelScope.launch {
HistoryScreen.insertHistoryChannel.receiveAsFlow().collectLatest { bookId ->
addHistoryUseCase(
History(
bookId = bookId,
book = null,
time = Date().time
getBookUseCase(bookId)?.let { book ->
addHistoryUseCase(
History(
id = 0,
book = book,
time = Date().time
)
)
)
}
delay(500)

View file

@ -79,7 +79,7 @@ fun LazyItemScope.HistoryItem(
RoundedCornerShape(10.dp)
)
) {
if (historyEntry.book?.coverImage != null) {
if (historyEntry.book.coverImage != null) {
AsyncCoverImage(
uri = historyEntry.book.coverImage,
modifier = Modifier
@ -108,7 +108,7 @@ fun LazyItemScope.HistoryItem(
modifier = Modifier.fillMaxHeight()
) {
StyledText(
text = historyEntry.book?.title ?: return,
text = historyEntry.book.title,
modifier = Modifier
.fillMaxWidth()
.noRippleClickable(

View file

@ -72,14 +72,14 @@ fun HistoryLayout(
onBodyClick = {
navigateToBookInfo(
HistoryEvent.OnNavigateToBookInfo(
historyEntry.bookId
historyEntry.book.id
)
)
},
onTitleClick = {
navigateToReader(
HistoryEvent.OnNavigateToReader(
historyEntry.bookId
historyEntry.book.id
)
)
},