From 56b5aba67a4de7fa68f887bef808765ab4b41515 Mon Sep 17 00:00:00 2001 From: Acclorite <2ma0bhpk@anonaddy.com> Date: Fri, 29 Aug 2025 13:51:39 +0300 Subject: [PATCH] refactor: history entity * Added HistoryWithBook that fetches both HistoryEntity and BookEntity * Removed nullability from History --- .../data/local/dto/HistoryEntity.kt | 2 +- .../data/local/dto/HistoryWithBook.kt | 19 +++++++++ .../book_story/data/local/room/BookDao.kt | 24 ----------- .../data/local/room/BookDatabase.kt | 1 + .../book_story/data/local/room/HistoryDao.kt | 41 +++++++++++++++++++ .../data/mapper/history/HistoryMapper.kt | 4 +- .../data/mapper/history/HistoryMapperImpl.kt | 17 ++++---- .../data/repository/HistoryRepositoryImpl.kt | 12 +++--- .../domain/model/history/History.kt | 5 +-- .../use_case/history/AddHistoryUseCase.kt | 6 +-- .../use_case/history/GetHistoryUseCase.kt | 22 +++------- .../presentation/history/HistoryModel.kt | 18 ++++---- .../book_story/ui/history/HistoryItem.kt | 4 +- .../book_story/ui/history/HistoryLayout.kt | 4 +- 14 files changed, 106 insertions(+), 73 deletions(-) create mode 100644 app/src/main/java/ua/acclorite/book_story/data/local/dto/HistoryWithBook.kt create mode 100644 app/src/main/java/ua/acclorite/book_story/data/local/room/HistoryDao.kt diff --git a/app/src/main/java/ua/acclorite/book_story/data/local/dto/HistoryEntity.kt b/app/src/main/java/ua/acclorite/book_story/data/local/dto/HistoryEntity.kt index 3fa9499d..946114e6 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/local/dto/HistoryEntity.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/local/dto/HistoryEntity.kt @@ -15,4 +15,4 @@ data class HistoryEntity( val id: Int = 0, val bookId: Int, val time: Long -) +) \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/data/local/dto/HistoryWithBook.kt b/app/src/main/java/ua/acclorite/book_story/data/local/dto/HistoryWithBook.kt new file mode 100644 index 00000000..a09e4a72 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/data/local/dto/HistoryWithBook.kt @@ -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 +) \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/data/local/room/BookDao.kt b/app/src/main/java/ua/acclorite/book_story/data/local/room/BookDao.kt index 58f1859e..94a5075f 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/local/room/BookDao.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/local/room/BookDao.kt @@ -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 - - @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( diff --git a/app/src/main/java/ua/acclorite/book_story/data/local/room/BookDatabase.kt b/app/src/main/java/ua/acclorite/book_story/data/local/room/BookDatabase.kt index 0dc4aa37..5ffc2983 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/local/room/BookDatabase.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/local/room/BookDatabase.kt @@ -50,6 +50,7 @@ import java.io.File ) abstract class BookDatabase : RoomDatabase() { abstract val bookDao: BookDao + abstract val historyDao: HistoryDao abstract val colorPresetDao: ColorPresetDao } diff --git a/app/src/main/java/ua/acclorite/book_story/data/local/room/HistoryDao.kt b/app/src/main/java/ua/acclorite/book_story/data/local/room/HistoryDao.kt new file mode 100644 index 00000000..7220818e --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/data/local/room/HistoryDao.kt @@ -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 + + @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 +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/data/mapper/history/HistoryMapper.kt b/app/src/main/java/ua/acclorite/book_story/data/mapper/history/HistoryMapper.kt index aa606d4f..fc402359 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/mapper/history/HistoryMapper.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/mapper/history/HistoryMapper.kt @@ -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 } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/data/mapper/history/HistoryMapperImpl.kt b/app/src/main/java/ua/acclorite/book_story/data/mapper/history/HistoryMapperImpl.kt index 60736be8..07e3abd4 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/mapper/history/HistoryMapperImpl.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/mapper/history/HistoryMapperImpl.kt @@ -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 ) } } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/data/repository/HistoryRepositoryImpl.kt b/app/src/main/java/ua/acclorite/book_story/data/repository/HistoryRepositoryImpl.kt index eefcc4d0..3efbd2b1 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/repository/HistoryRepositoryImpl.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/repository/HistoryRepositoryImpl.kt @@ -23,7 +23,7 @@ class HistoryRepositoryImpl @Inject constructor( override suspend fun getHistoryForBook(bookId: Int): Result = 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 = runCatching { withContext(Dispatchers.IO) { - database.bookDao.insertHistory(historyMapper.toHistoryEntity(history)) + database.historyDao.insertHistory(historyMapper.toHistoryEntity(history)) } } override suspend fun getHistory(): Result> = runCatching { withContext(Dispatchers.IO) { - database.bookDao.getHistory().map { historyMapper.toHistory(it) } + database.historyDao.getHistoryWithBook().map { historyMapper.toHistory(it) } } } override suspend fun deleteWholeHistory(): Result = 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 = 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 = 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.") } } diff --git a/app/src/main/java/ua/acclorite/book_story/domain/model/history/History.kt b/app/src/main/java/ua/acclorite/book_story/domain/model/history/History.kt index c997bc93..3ae13e49 100644 --- a/app/src/main/java/ua/acclorite/book_story/domain/model/history/History.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/model/history/History.kt @@ -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 ) \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/domain/use_case/history/AddHistoryUseCase.kt b/app/src/main/java/ua/acclorite/book_story/domain/use_case/history/AddHistoryUseCase.kt index a1f7ac27..e6c9cf94 100644 --- a/app/src/main/java/ua/acclorite/book_story/domain/use_case/history/AddHistoryUseCase.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/use_case/history/AddHistoryUseCase.kt @@ -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}") } ) } diff --git a/app/src/main/java/ua/acclorite/book_story/domain/use_case/history/GetHistoryUseCase.kt b/app/src/main/java/ua/acclorite/book_story/domain/use_case/history/GetHistoryUseCase.kt index d33c81c4..1fb12046 100644 --- a/app/src/main/java/ua/acclorite/book_story/domain/use_case/history/GetHistoryUseCase.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/use_case/history/GetHistoryUseCase.kt @@ -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): List { - 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 = { diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/history/HistoryModel.kt b/app/src/main/java/ua/acclorite/book_story/presentation/history/HistoryModel.kt index dd6e73e3..d978ec15 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/history/HistoryModel.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/history/HistoryModel.kt @@ -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) diff --git a/app/src/main/java/ua/acclorite/book_story/ui/history/HistoryItem.kt b/app/src/main/java/ua/acclorite/book_story/ui/history/HistoryItem.kt index c19701e9..a5869759 100644 --- a/app/src/main/java/ua/acclorite/book_story/ui/history/HistoryItem.kt +++ b/app/src/main/java/ua/acclorite/book_story/ui/history/HistoryItem.kt @@ -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( diff --git a/app/src/main/java/ua/acclorite/book_story/ui/history/HistoryLayout.kt b/app/src/main/java/ua/acclorite/book_story/ui/history/HistoryLayout.kt index 17de2e35..b9a637c7 100644 --- a/app/src/main/java/ua/acclorite/book_story/ui/history/HistoryLayout.kt +++ b/app/src/main/java/ua/acclorite/book_story/ui/history/HistoryLayout.kt @@ -72,14 +72,14 @@ fun HistoryLayout( onBodyClick = { navigateToBookInfo( HistoryEvent.OnNavigateToBookInfo( - historyEntry.bookId + historyEntry.book.id ) ) }, onTitleClick = { navigateToReader( HistoryEvent.OnNavigateToReader( - historyEntry.bookId + historyEntry.book.id ) ) },