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.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.HistoryEntity
|
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
interface BookDao {
|
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 ----------------- */
|
/* ------ CategoryEntity ----------------- */
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
suspend fun insertCategory(
|
suspend fun insertCategory(
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,7 @@ import java.io.File
|
||||||
)
|
)
|
||||||
abstract class BookDatabase : RoomDatabase() {
|
abstract class BookDatabase : RoomDatabase() {
|
||||||
abstract val bookDao: BookDao
|
abstract val bookDao: BookDao
|
||||||
|
abstract val historyDao: HistoryDao
|
||||||
abstract val colorPresetDao: ColorPresetDao
|
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
|
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.HistoryEntity
|
||||||
|
import ua.acclorite.book_story.data.local.dto.HistoryWithBook
|
||||||
import ua.acclorite.book_story.domain.model.history.History
|
import ua.acclorite.book_story.domain.model.history.History
|
||||||
|
|
||||||
interface HistoryMapper {
|
interface HistoryMapper {
|
||||||
suspend fun toHistoryEntity(history: History): HistoryEntity
|
suspend fun toHistoryEntity(history: History): HistoryEntity
|
||||||
|
suspend fun toHistory(historyWithBook: HistoryWithBook): History
|
||||||
suspend fun toHistory(historyEntity: HistoryEntity): History
|
|
||||||
}
|
}
|
||||||
|
|
@ -7,24 +7,27 @@
|
||||||
package ua.acclorite.book_story.data.mapper.history
|
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.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 ua.acclorite.book_story.domain.model.history.History
|
||||||
import javax.inject.Inject
|
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 {
|
override suspend fun toHistoryEntity(history: History): HistoryEntity {
|
||||||
return HistoryEntity(
|
return HistoryEntity(
|
||||||
id = history.id,
|
id = history.id,
|
||||||
bookId = history.bookId,
|
bookId = history.book.id,
|
||||||
time = history.time
|
time = history.time
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun toHistory(historyEntity: HistoryEntity): History {
|
override suspend fun toHistory(historyWithBook: HistoryWithBook): History {
|
||||||
return History(
|
return History(
|
||||||
historyEntity.id,
|
id = historyWithBook.history.id,
|
||||||
bookId = historyEntity.bookId,
|
book = bookMapper.toBook(historyWithBook.book),
|
||||||
book = null,
|
time = historyWithBook.history.time
|
||||||
time = historyEntity.time
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -23,7 +23,7 @@ class HistoryRepositoryImpl @Inject constructor(
|
||||||
|
|
||||||
override suspend fun getHistoryForBook(bookId: Int): Result<History> = runCatching {
|
override suspend fun getHistoryForBook(bookId: Int): Result<History> = runCatching {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
database.bookDao.getHistoryForBook(bookId).let {
|
database.historyDao.getHistoryForBook(bookId).let {
|
||||||
if (it == null) throw NoSuchElementException("Could not get history from [$bookId].")
|
if (it == null) throw NoSuchElementException("Could not get history from [$bookId].")
|
||||||
else historyMapper.toHistory(it)
|
else historyMapper.toHistory(it)
|
||||||
}
|
}
|
||||||
|
|
@ -32,19 +32,19 @@ class HistoryRepositoryImpl @Inject constructor(
|
||||||
|
|
||||||
override suspend fun addHistory(history: History): Result<Unit> = runCatching {
|
override suspend fun addHistory(history: History): Result<Unit> = runCatching {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
database.bookDao.insertHistory(historyMapper.toHistoryEntity(history))
|
database.historyDao.insertHistory(historyMapper.toHistoryEntity(history))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getHistory(): Result<List<History>> = runCatching {
|
override suspend fun getHistory(): Result<List<History>> = runCatching {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
database.bookDao.getHistory().map { historyMapper.toHistory(it) }
|
database.historyDao.getHistoryWithBook().map { historyMapper.toHistory(it) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun deleteWholeHistory(): Result<Unit> = runCatching {
|
override suspend fun deleteWholeHistory(): Result<Unit> = runCatching {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
database.bookDao.deleteWholeHistory().also {
|
database.historyDao.deleteWholeHistory().also {
|
||||||
if (it == 0) throw Exception("Could not delete whole history in database.")
|
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 {
|
override suspend fun deleteHistoryForBook(bookId: Int): Result<Unit> = runCatching {
|
||||||
withContext(Dispatchers.IO) {
|
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.")
|
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 {
|
override suspend fun deleteHistory(history: History): Result<Unit> = runCatching {
|
||||||
withContext(Dispatchers.IO) {
|
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.")
|
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
|
@Immutable
|
||||||
data class History(
|
data class History(
|
||||||
val id: Int = 0,
|
val id: Int,
|
||||||
val bookId: Int,
|
val book: Book,
|
||||||
val book: Book?,
|
|
||||||
val time: Long
|
val time: Long
|
||||||
)
|
)
|
||||||
|
|
@ -17,14 +17,14 @@ class AddHistoryUseCase @Inject constructor(
|
||||||
) {
|
) {
|
||||||
|
|
||||||
suspend operator fun invoke(history: History) {
|
suspend operator fun invoke(history: History) {
|
||||||
logI("Inserting history for [${history.bookId}].")
|
logI("Inserting history for [${history.book.id}].")
|
||||||
|
|
||||||
historyRepository.addHistory(history = history).fold(
|
historyRepository.addHistory(history = history).fold(
|
||||||
onSuccess = {
|
onSuccess = {
|
||||||
logI("Successfully inserted history for [${history.bookId}].")
|
logI("Successfully inserted history for [${history.book.id}].")
|
||||||
},
|
},
|
||||||
onFailure = {
|
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.logE
|
||||||
import ua.acclorite.book_story.core.log.logI
|
import ua.acclorite.book_story.core.log.logI
|
||||||
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.BookRepository
|
|
||||||
import ua.acclorite.book_story.domain.repository.HistoryRepository
|
import ua.acclorite.book_story.domain.repository.HistoryRepository
|
||||||
import ua.acclorite.book_story.presentation.history.model.GroupedHistory
|
import ua.acclorite.book_story.presentation.history.model.GroupedHistory
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
|
@ -19,7 +18,6 @@ import java.time.format.DateTimeFormatter
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class GetHistoryUseCase @Inject constructor(
|
class GetHistoryUseCase @Inject constructor(
|
||||||
private val bookRepository: BookRepository,
|
|
||||||
private val historyRepository: HistoryRepository
|
private val historyRepository: HistoryRepository
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
|
@ -40,27 +38,19 @@ class GetHistoryUseCase @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun filterMaxElementsById(elements: List<History>): List<History> {
|
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) ->
|
val maxElementsById = groupedById.map { (_, values) ->
|
||||||
values.maxByOrNull { it.time }
|
values.maxByOrNull { it.time }
|
||||||
}
|
}
|
||||||
return maxElementsById.filterNotNull()
|
return maxElementsById.filterNotNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val query = query.lowercase().trim()
|
||||||
return runCatching {
|
return runCatching {
|
||||||
historyRepository.getHistory().getOrThrow().sortedByDescending { it.time }
|
historyRepository.getHistory().getOrThrow()
|
||||||
.mapNotNull { history ->
|
.filter { history -> history.book.title.lowercase().trim().contains(query) }
|
||||||
val book = bookRepository.getBook(history.bookId).getOrNull()
|
.sortedByDescending { history -> history.time }
|
||||||
if (
|
.groupBy { history -> getDayLabel(history.time) }
|
||||||
book == null
|
|
||||||
|| !book.title.lowercase().trim().contains(query.lowercase().trim())
|
|
||||||
) return@mapNotNull null
|
|
||||||
|
|
||||||
history.copy(book = book)
|
|
||||||
}
|
|
||||||
.groupBy { history ->
|
|
||||||
getDayLabel(history.time)
|
|
||||||
}
|
|
||||||
.map { (day, history) -> GroupedHistory(day, filterMaxElementsById(history)) }
|
.map { (day, history) -> GroupedHistory(day, filterMaxElementsById(history)) }
|
||||||
}.fold(
|
}.fold(
|
||||||
onSuccess = {
|
onSuccess = {
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import ua.acclorite.book_story.domain.model.history.History
|
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.AddHistoryUseCase
|
||||||
import ua.acclorite.book_story.domain.use_case.history.DeleteHistoryUseCase
|
import ua.acclorite.book_story.domain.use_case.history.DeleteHistoryUseCase
|
||||||
import ua.acclorite.book_story.domain.use_case.history.DeleteWholeHistoryUseCase
|
import ua.acclorite.book_story.domain.use_case.history.DeleteWholeHistoryUseCase
|
||||||
|
|
@ -38,7 +39,8 @@ class HistoryModel @Inject constructor(
|
||||||
private val getHistoryUseCase: GetHistoryUseCase,
|
private val getHistoryUseCase: GetHistoryUseCase,
|
||||||
private val addHistoryUseCase: AddHistoryUseCase,
|
private val addHistoryUseCase: AddHistoryUseCase,
|
||||||
private val deleteHistoryUseCase: DeleteHistoryUseCase,
|
private val deleteHistoryUseCase: DeleteHistoryUseCase,
|
||||||
private val deleteWholeHistoryUseCase: DeleteWholeHistoryUseCase
|
private val deleteWholeHistoryUseCase: DeleteWholeHistoryUseCase,
|
||||||
|
private val getBookUseCase: GetBookUseCase
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private val mutex = Mutex()
|
private val mutex = Mutex()
|
||||||
|
|
@ -72,13 +74,15 @@ class HistoryModel @Inject constructor(
|
||||||
}
|
}
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
HistoryScreen.insertHistoryChannel.receiveAsFlow().collectLatest { bookId ->
|
HistoryScreen.insertHistoryChannel.receiveAsFlow().collectLatest { bookId ->
|
||||||
addHistoryUseCase(
|
getBookUseCase(bookId)?.let { book ->
|
||||||
History(
|
addHistoryUseCase(
|
||||||
bookId = bookId,
|
History(
|
||||||
book = null,
|
id = 0,
|
||||||
time = Date().time
|
book = book,
|
||||||
|
time = Date().time
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
}
|
||||||
|
|
||||||
delay(500)
|
delay(500)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,7 @@ fun LazyItemScope.HistoryItem(
|
||||||
RoundedCornerShape(10.dp)
|
RoundedCornerShape(10.dp)
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
if (historyEntry.book?.coverImage != null) {
|
if (historyEntry.book.coverImage != null) {
|
||||||
AsyncCoverImage(
|
AsyncCoverImage(
|
||||||
uri = historyEntry.book.coverImage,
|
uri = historyEntry.book.coverImage,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|
@ -108,7 +108,7 @@ fun LazyItemScope.HistoryItem(
|
||||||
modifier = Modifier.fillMaxHeight()
|
modifier = Modifier.fillMaxHeight()
|
||||||
) {
|
) {
|
||||||
StyledText(
|
StyledText(
|
||||||
text = historyEntry.book?.title ?: return,
|
text = historyEntry.book.title,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.noRippleClickable(
|
.noRippleClickable(
|
||||||
|
|
|
||||||
|
|
@ -72,14 +72,14 @@ fun HistoryLayout(
|
||||||
onBodyClick = {
|
onBodyClick = {
|
||||||
navigateToBookInfo(
|
navigateToBookInfo(
|
||||||
HistoryEvent.OnNavigateToBookInfo(
|
HistoryEvent.OnNavigateToBookInfo(
|
||||||
historyEntry.bookId
|
historyEntry.book.id
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
onTitleClick = {
|
onTitleClick = {
|
||||||
navigateToReader(
|
navigateToReader(
|
||||||
HistoryEvent.OnNavigateToReader(
|
HistoryEvent.OnNavigateToReader(
|
||||||
historyEntry.bookId
|
historyEntry.book.id
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue