refactor: history entity
* Added HistoryWithBook that fetches both HistoryEntity and BookEntity * Removed nullability from History
This commit is contained in:
parent
a45078cae5
commit
56b5aba67a
14 changed files with 106 additions and 73 deletions
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ import java.io.File
|
|||
)
|
||||
abstract class BookDatabase : RoomDatabase() {
|
||||
abstract val bookDao: BookDao
|
||||
abstract val historyDao: HistoryDao
|
||||
abstract val colorPresetDao: ColorPresetDao
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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.")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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}")
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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 ->
|
||||
getBookUseCase(bookId)?.let { book ->
|
||||
addHistoryUseCase(
|
||||
History(
|
||||
bookId = bookId,
|
||||
book = null,
|
||||
id = 0,
|
||||
book = book,
|
||||
time = Date().time
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
delay(500)
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -72,14 +72,14 @@ fun HistoryLayout(
|
|||
onBodyClick = {
|
||||
navigateToBookInfo(
|
||||
HistoryEvent.OnNavigateToBookInfo(
|
||||
historyEntry.bookId
|
||||
historyEntry.book.id
|
||||
)
|
||||
)
|
||||
},
|
||||
onTitleClick = {
|
||||
navigateToReader(
|
||||
HistoryEvent.OnNavigateToReader(
|
||||
historyEntry.bookId
|
||||
historyEntry.book.id
|
||||
)
|
||||
)
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue