🛠️ Add logging and reformat data layer

* Reformatted BookRepository, added logs
* Improved code readability, added comments
This commit is contained in:
Acclorite 2024-09-17 23:17:41 +03:00
parent 94c49c20c8
commit 100926ce14
25 changed files with 544 additions and 375 deletions

View file

@ -1,11 +1,10 @@
package ua.acclorite.book_story.data.parser package ua.acclorite.book_story.data.parser
import ua.acclorite.book_story.domain.model.Book import ua.acclorite.book_story.domain.model.BookWithCover
import ua.acclorite.book_story.domain.util.CoverImage
import java.io.File import java.io.File
interface FileParser { interface FileParser {
suspend fun parse(file: File): Pair<Book, CoverImage?>? suspend fun parse(file: File): BookWithCover?
} }

View file

@ -7,8 +7,7 @@ import ua.acclorite.book_story.data.parser.htm.HtmFileParser
import ua.acclorite.book_story.data.parser.html.HtmlFileParser import ua.acclorite.book_story.data.parser.html.HtmlFileParser
import ua.acclorite.book_story.data.parser.pdf.PdfFileParser import ua.acclorite.book_story.data.parser.pdf.PdfFileParser
import ua.acclorite.book_story.data.parser.txt.TxtFileParser import ua.acclorite.book_story.data.parser.txt.TxtFileParser
import ua.acclorite.book_story.domain.model.Book import ua.acclorite.book_story.domain.model.BookWithCover
import ua.acclorite.book_story.domain.util.CoverImage
import java.io.File import java.io.File
import javax.inject.Inject import javax.inject.Inject
@ -22,7 +21,7 @@ class FileParserImpl @Inject constructor(
private val htmlFileParser: HtmlFileParser, private val htmlFileParser: HtmlFileParser,
private val htmFileParser: HtmFileParser, private val htmFileParser: HtmFileParser,
) : FileParser { ) : FileParser {
override suspend fun parse(file: File): Pair<Book, CoverImage?>? { override suspend fun parse(file: File): BookWithCover? {
if (!file.exists()) { if (!file.exists()) {
Log.e(FILE_PARSER, "File does not exist.") Log.e(FILE_PARSER, "File does not exist.")
return null return null

View file

@ -8,8 +8,8 @@ import org.jsoup.Jsoup
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.data.parser.FileParser import ua.acclorite.book_story.data.parser.FileParser
import ua.acclorite.book_story.domain.model.Book import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.domain.model.BookWithCover
import ua.acclorite.book_story.domain.model.Category import ua.acclorite.book_story.domain.model.Category
import ua.acclorite.book_story.domain.util.CoverImage
import ua.acclorite.book_story.domain.util.UIText import ua.acclorite.book_story.domain.util.UIText
import java.io.File import java.io.File
import java.util.zip.ZipFile import java.util.zip.ZipFile
@ -17,9 +17,9 @@ import javax.inject.Inject
class EpubFileParser @Inject constructor() : FileParser { class EpubFileParser @Inject constructor() : FileParser {
override suspend fun parse(file: File): Pair<Book, CoverImage?>? { override suspend fun parse(file: File): BookWithCover? {
return try { return try {
var book: Pair<Book, CoverImage?>? = null var book: BookWithCover? = null
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
ZipFile(file).use { zip -> ZipFile(file).use { zip ->
@ -71,19 +71,22 @@ class EpubFileParser @Inject constructor() : FileParser {
.firstOrNull()?.attr("href") .firstOrNull()?.attr("href")
} }
book = Book( book = BookWithCover(
title = title, book = Book(
author = author, title = title,
description = description, author = author,
textPath = "", description = description,
scrollIndex = 0, textPath = "",
scrollOffset = 0, scrollIndex = 0,
progress = 0f, scrollOffset = 0,
filePath = file.path, progress = 0f,
lastOpened = null, filePath = file.path,
category = Category.entries[0], lastOpened = null,
coverImage = null category = Category.entries[0],
) to extractCoverImageBitmap(file, coverImage) coverImage = null
),
coverImage = extractCoverImageBitmap(file, coverImage)
)
} }
} }
book book

View file

@ -7,8 +7,8 @@ import org.w3c.dom.Element
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.data.parser.FileParser import ua.acclorite.book_story.data.parser.FileParser
import ua.acclorite.book_story.domain.model.Book import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.domain.model.BookWithCover
import ua.acclorite.book_story.domain.model.Category import ua.acclorite.book_story.domain.model.Category
import ua.acclorite.book_story.domain.util.CoverImage
import ua.acclorite.book_story.domain.util.UIText import ua.acclorite.book_story.domain.util.UIText
import java.io.File import java.io.File
import javax.inject.Inject import javax.inject.Inject
@ -16,7 +16,7 @@ import javax.xml.parsers.DocumentBuilderFactory
class Fb2FileParser @Inject constructor() : FileParser { class Fb2FileParser @Inject constructor() : FileParser {
override suspend fun parse(file: File): Pair<Book, CoverImage?>? { override suspend fun parse(file: File): BookWithCover? {
return try { return try {
val factory = DocumentBuilderFactory.newInstance() val factory = DocumentBuilderFactory.newInstance()
val builder = factory.newDocumentBuilder() val builder = factory.newDocumentBuilder()
@ -50,19 +50,22 @@ class Fb2FileParser @Inject constructor() : FileParser {
val descriptionFromFile = extractElementContent(document, "annotation") val descriptionFromFile = extractElementContent(document, "annotation")
Book( BookWithCover(
title = title, book = Book(
author = author, title = title,
description = descriptionFromFile, author = author,
textPath = "", description = descriptionFromFile,
scrollIndex = 0, textPath = "",
scrollOffset = 0, scrollIndex = 0,
progress = 0f, scrollOffset = 0,
filePath = file.path, progress = 0f,
lastOpened = null, filePath = file.path,
category = Category.entries[0], lastOpened = null,
category = Category.entries[0],
coverImage = null
),
coverImage = null coverImage = null
) to null )
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
null null

View file

@ -4,15 +4,15 @@ import org.jsoup.Jsoup
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.data.parser.FileParser import ua.acclorite.book_story.data.parser.FileParser
import ua.acclorite.book_story.domain.model.Book import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.domain.model.BookWithCover
import ua.acclorite.book_story.domain.model.Category import ua.acclorite.book_story.domain.model.Category
import ua.acclorite.book_story.domain.util.CoverImage
import ua.acclorite.book_story.domain.util.UIText import ua.acclorite.book_story.domain.util.UIText
import java.io.File import java.io.File
import javax.inject.Inject import javax.inject.Inject
class HtmFileParser @Inject constructor() : FileParser { class HtmFileParser @Inject constructor() : FileParser {
override suspend fun parse(file: File): Pair<Book, CoverImage?>? { override suspend fun parse(file: File): BookWithCover? {
return try { return try {
val document = Jsoup.parse(file) val document = Jsoup.parse(file)
@ -22,19 +22,22 @@ class HtmFileParser @Inject constructor() : FileParser {
} }
} }
Book( BookWithCover(
title = title, book = Book(
author = UIText.StringResource(R.string.unknown_author), title = title,
description = null, author = UIText.StringResource(R.string.unknown_author),
textPath = "", description = null,
scrollIndex = 0, textPath = "",
scrollOffset = 0, scrollIndex = 0,
progress = 0f, scrollOffset = 0,
filePath = file.path, progress = 0f,
lastOpened = null, filePath = file.path,
category = Category.entries[0], lastOpened = null,
category = Category.entries[0],
coverImage = null
),
coverImage = null coverImage = null
) to null )
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
null null

View file

@ -4,15 +4,15 @@ import org.jsoup.Jsoup
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.data.parser.FileParser import ua.acclorite.book_story.data.parser.FileParser
import ua.acclorite.book_story.domain.model.Book import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.domain.model.BookWithCover
import ua.acclorite.book_story.domain.model.Category import ua.acclorite.book_story.domain.model.Category
import ua.acclorite.book_story.domain.util.CoverImage
import ua.acclorite.book_story.domain.util.UIText import ua.acclorite.book_story.domain.util.UIText
import java.io.File import java.io.File
import javax.inject.Inject import javax.inject.Inject
class HtmlFileParser @Inject constructor() : FileParser { class HtmlFileParser @Inject constructor() : FileParser {
override suspend fun parse(file: File): Pair<Book, CoverImage?>? { override suspend fun parse(file: File): BookWithCover? {
return try { return try {
val document = Jsoup.parse(file) val document = Jsoup.parse(file)
@ -22,19 +22,22 @@ class HtmlFileParser @Inject constructor() : FileParser {
} }
} }
Book( BookWithCover(
title = title, book = Book(
author = UIText.StringResource(R.string.unknown_author), title = title,
description = null, author = UIText.StringResource(R.string.unknown_author),
textPath = "", description = null,
scrollIndex = 0, textPath = "",
scrollOffset = 0, scrollIndex = 0,
progress = 0f, scrollOffset = 0,
filePath = file.path, progress = 0f,
lastOpened = null, filePath = file.path,
category = Category.entries[0], lastOpened = null,
category = Category.entries[0],
coverImage = null
),
coverImage = null coverImage = null
) to null )
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
null null

View file

@ -6,15 +6,15 @@ import com.tom_roush.pdfbox.pdmodel.PDDocument
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.data.parser.FileParser import ua.acclorite.book_story.data.parser.FileParser
import ua.acclorite.book_story.domain.model.Book import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.domain.model.BookWithCover
import ua.acclorite.book_story.domain.model.Category import ua.acclorite.book_story.domain.model.Category
import ua.acclorite.book_story.domain.util.CoverImage
import ua.acclorite.book_story.domain.util.UIText import ua.acclorite.book_story.domain.util.UIText
import java.io.File import java.io.File
import javax.inject.Inject import javax.inject.Inject
class PdfFileParser @Inject constructor(private val application: Application) : FileParser { class PdfFileParser @Inject constructor(private val application: Application) : FileParser {
override suspend fun parse(file: File): Pair<Book, CoverImage?>? { override suspend fun parse(file: File): BookWithCover? {
return try { return try {
PDFBoxResourceLoader.init(application) PDFBoxResourceLoader.init(application)
@ -29,19 +29,22 @@ class PdfFileParser @Inject constructor(private val application: Application) :
document.close() document.close()
Book( BookWithCover(
title = title, book = Book(
author = author, title = title,
description = description, author = author,
textPath = "", description = description,
scrollIndex = 0, textPath = "",
scrollOffset = 0, scrollIndex = 0,
progress = 0f, scrollOffset = 0,
filePath = file.path, progress = 0f,
lastOpened = null, filePath = file.path,
category = Category.entries[0], lastOpened = null,
category = Category.entries[0],
coverImage = null
),
coverImage = null coverImage = null
) to null )
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
null null

View file

@ -3,32 +3,35 @@ package ua.acclorite.book_story.data.parser.txt
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.data.parser.FileParser import ua.acclorite.book_story.data.parser.FileParser
import ua.acclorite.book_story.domain.model.Book import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.domain.model.BookWithCover
import ua.acclorite.book_story.domain.model.Category import ua.acclorite.book_story.domain.model.Category
import ua.acclorite.book_story.domain.util.CoverImage
import ua.acclorite.book_story.domain.util.UIText import ua.acclorite.book_story.domain.util.UIText
import java.io.File import java.io.File
import javax.inject.Inject import javax.inject.Inject
class TxtFileParser @Inject constructor() : FileParser { class TxtFileParser @Inject constructor() : FileParser {
override suspend fun parse(file: File): Pair<Book, CoverImage?>? { override suspend fun parse(file: File): BookWithCover? {
return try { return try {
val title = file.nameWithoutExtension.trim() val title = file.nameWithoutExtension.trim()
val author = UIText.StringResource(R.string.unknown_author) val author = UIText.StringResource(R.string.unknown_author)
Book( BookWithCover(
title = title, book = Book(
author = author, title = title,
description = null, author = author,
textPath = "", description = null,
scrollIndex = 0, textPath = "",
scrollOffset = 0, scrollIndex = 0,
progress = 0f, scrollOffset = 0,
filePath = file.path, progress = 0f,
lastOpened = null, filePath = file.path,
category = Category.entries[0], lastOpened = null,
category = Category.entries[0],
coverImage = null
),
coverImage = null coverImage = null
) to null )
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
null null

View file

@ -9,13 +9,9 @@ import android.provider.MediaStore
import android.util.Log import android.util.Log
import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.Preferences
import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.data.local.data_store.DataStore import ua.acclorite.book_story.data.local.data_store.DataStore
@ -30,6 +26,8 @@ import ua.acclorite.book_story.data.parser.TextParser
import ua.acclorite.book_story.data.remote.GithubAPI import ua.acclorite.book_story.data.remote.GithubAPI
import ua.acclorite.book_story.data.remote.dto.LatestReleaseInfo import ua.acclorite.book_story.data.remote.dto.LatestReleaseInfo
import ua.acclorite.book_story.domain.model.Book import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.domain.model.BookWithText
import ua.acclorite.book_story.domain.model.BookWithTextAndCover
import ua.acclorite.book_story.domain.model.ColorPreset import ua.acclorite.book_story.domain.model.ColorPreset
import ua.acclorite.book_story.domain.model.History import ua.acclorite.book_story.domain.model.History
import ua.acclorite.book_story.domain.model.NullableBook import ua.acclorite.book_story.domain.model.NullableBook
@ -51,6 +49,16 @@ import javax.inject.Singleton
private const val GET_BOOK_FROM_FILE = "GET BOOK FROM FILE, REPOSITORY" private const val GET_BOOK_FROM_FILE = "GET BOOK FROM FILE, REPOSITORY"
private const val GET_TEXT = "GET TEXT, REPOSITORY" private const val GET_TEXT = "GET TEXT, REPOSITORY"
private const val GET_BOOKS = "GET BOOKS, REPOSITORY"
private const val GET_BOOKS_BY_ID = "GET BOOKS, REPOSITORY"
private const val INSERT_BOOK = "INSERT BOOK, REPOSITORY"
private const val UPDATE_BOOK = "UPDATE BOOK, REPOSITORY"
private const val DELETE_BOOKS = "DELETE BOOKS, REPOSITORY"
private const val CAN_RESET_COVER = "CAN RESET COVER, REPOSITORY"
private const val RESET_COVER = "RESET COVER, REPOSITORY"
private const val GET_ALL_SETTINGS = "GET ALL SETTINGS, REPOSITORY"
private const val GET_FILES_FROM_DEVICE = "GET FILES FROM DEVICE, REPOSITORY"
private const val CHECK_FOR_UPDATES = "CHECK FOR UPDATES, REPOSITORY"
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
@Singleton @Singleton
@ -67,13 +75,18 @@ class BookRepositoryImpl @Inject constructor(
private val colorPresetMapper: ColorPresetMapper, private val colorPresetMapper: ColorPresetMapper,
private val fileParser: FileParser, private val fileParser: FileParser,
private val textParser: TextParser private val textParser: TextParser,
) : BookRepository { ) : BookRepository {
/**
* Get all books matching [query] from database.
* Empty [query] equals to all books.
*/
override suspend fun getBooks(query: String): List<Book> { override suspend fun getBooks(query: String): List<Book> {
Log.i(GET_BOOKS, "Searching for books with query: \"$query\".")
val books = database.searchBooks(query) val books = database.searchBooks(query)
Log.i(GET_BOOKS, "Found ${books.size} books.")
return books.map { entity -> return books.map { entity ->
val book = bookMapper.toBook(entity) val book = bookMapper.toBook(entity)
val lastHistory = database.getLatestHistoryForBook( val lastHistory = database.getLatestHistoryForBook(
@ -86,7 +99,11 @@ class BookRepositoryImpl @Inject constructor(
} }
} }
/**
* Get all books that match given [ids].
*/
override suspend fun getBooksById(ids: List<Int>): List<Book> { override suspend fun getBooksById(ids: List<Int>): List<Book> {
Log.i(GET_BOOKS_BY_ID, "Getting books with ids: $ids.")
val books = database.findBooksById(ids) val books = database.findBooksById(ids)
return books.map { entity -> return books.map { entity ->
@ -128,26 +145,33 @@ class BookRepositoryImpl @Inject constructor(
return lines return lines
} }
/**
* Inserts book in database.
* Creates covers and books folders, which contain book's text and cover.
*/
override suspend fun insertBook( override suspend fun insertBook(
book: Book, bookWithTextAndCover: BookWithTextAndCover
coverImage: CoverImage?,
text: List<String>
): Boolean { ): Boolean {
Log.i(INSERT_BOOK, "Inserting ${bookWithTextAndCover.book.title}.")
val filesDir = application.filesDir val filesDir = application.filesDir
val coversDir = File(filesDir, "covers") val coversDir = File(filesDir, "covers")
val booksDir = File(filesDir, "books") val booksDir = File(filesDir, "books")
if (!coversDir.exists()) { if (!coversDir.exists()) {
Log.i(INSERT_BOOK, "Created covers folder.")
coversDir.mkdirs() coversDir.mkdirs()
} }
if (!booksDir.exists()) { if (!booksDir.exists()) {
Log.i(INSERT_BOOK, "Created books folder.")
booksDir.mkdirs() booksDir.mkdirs()
} }
var coverUri = "" var coverUri = ""
val textUri: String val textUri: String
if (text.isEmpty()) { if (bookWithTextAndCover.text.isEmpty()) {
Log.e(INSERT_BOOK, "Text is empty.")
return false return false
} }
@ -157,18 +181,19 @@ class BookRepositoryImpl @Inject constructor(
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
FileOutputStream(textPath).use { stream -> FileOutputStream(textPath).use { stream ->
text.forEach { line -> bookWithTextAndCover.text.forEach { line ->
stream.write(line.toByteArray()) stream.write(line.toByteArray())
stream.write(System.lineSeparator().toByteArray()) stream.write(System.lineSeparator().toByteArray())
} }
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(INSERT_BOOK, "Could not write text.")
e.printStackTrace() e.printStackTrace()
return false return false
} }
if (coverImage != null) { if (bookWithTextAndCover.coverImage != null) {
try { try {
coverUri = "${UUID.randomUUID()}.webp" coverUri = "${UUID.randomUUID()}.webp"
val cover = File(coversDir, coverUri) val cover = File(coversDir, coverUri)
@ -176,23 +201,25 @@ class BookRepositoryImpl @Inject constructor(
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
FileOutputStream(cover).use { stream -> FileOutputStream(cover).use { stream ->
if ( if (
!coverImage.copy(Bitmap.Config.RGB_565, false).compress( !bookWithTextAndCover.coverImage.copy(Bitmap.Config.RGB_565, false)
Bitmap.CompressFormat.WEBP, .compress(
20, Bitmap.CompressFormat.WEBP,
stream 20,
) stream
)
) { ) {
throw Exception("Couldn't save cover image") throw Exception("Couldn't save cover image")
} }
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(INSERT_BOOK, "Could not save cover.")
coverUri = "" coverUri = ""
e.printStackTrace() e.printStackTrace()
} }
} }
val updatedBook = book.copy( val updatedBook = bookWithTextAndCover.book.copy(
textPath = "$booksDir/$textUri", textPath = "$booksDir/$textUri",
coverImage = if (coverUri.isNotBlank()) { coverImage = if (coverUri.isNotBlank()) {
Uri.fromFile(File("$coversDir/$coverUri")) Uri.fromFile(File("$coversDir/$coverUri"))
@ -201,37 +228,48 @@ class BookRepositoryImpl @Inject constructor(
val bookToInsert = bookMapper.toBookEntity(updatedBook) val bookToInsert = bookMapper.toBookEntity(updatedBook)
database.insertBooks(listOf(bookToInsert)) database.insertBooks(listOf(bookToInsert))
Log.i(INSERT_BOOK, "Successfully inserted book.")
return true return true
} }
override suspend fun updateBooks(books: List<Book>) { /**
* Update book without text or cover image.
*/
override suspend fun updateBook(book: Book) {
// without text and cover image // without text and cover image
val entity = database.findBookById(book.id)
database.updateBooks( database.updateBooks(
books.map { listOf(
val book = database.findBookById(it.id)
bookMapper.toBookEntity( bookMapper.toBookEntity(
it.copy( book.copy(
textPath = book.textPath, textPath = entity.textPath,
coverImage = if (book.image != null) Uri.parse(book.image) else null coverImage = if (entity.image != null) Uri.parse(entity.image) else null
) )
) )
} )
) )
} }
override suspend fun updateBookWithText(book: Book, text: List<String>): Boolean { /**
* Update book with text. Deletes old text file and replaces it with new.
*/
override suspend fun updateBookWithText(bookWithText: BookWithText): Boolean {
Log.i(UPDATE_BOOK, "Updating book with text: ${bookWithText.book.title}.")
// without cover image // without cover image
val filesDir = application.filesDir val filesDir = application.filesDir
val booksDir = File(filesDir, "books") val booksDir = File(filesDir, "books")
if (!booksDir.exists()) { if (!booksDir.exists()) {
Log.i(UPDATE_BOOK, "Created books folder.")
booksDir.mkdirs() booksDir.mkdirs()
} }
val textUri: String val textUri: String
val bookEntity = database.findBookById(book.id) val bookEntity = database.findBookById(bookWithText.book.id)
if (text.isEmpty()) { if (bookWithText.text.isEmpty()) {
Log.e(UPDATE_BOOK, "Text is empty.")
return false return false
} }
@ -241,33 +279,35 @@ class BookRepositoryImpl @Inject constructor(
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
FileOutputStream(textPath).use { stream -> FileOutputStream(textPath).use { stream ->
text.forEach { line -> bookWithText.text.forEach { line ->
stream.write(line.toByteArray()) stream.write(line.toByteArray())
stream.write(System.lineSeparator().toByteArray()) stream.write(System.lineSeparator().toByteArray())
} }
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(UPDATE_BOOK, "Could not update text.")
e.printStackTrace() e.printStackTrace()
return false return false
} }
if (book.textPath.isNotBlank()) { if (bookWithText.book.textPath.isNotBlank()) {
try { try {
val fileToDelete = File( val fileToDelete = File(
book.textPath bookWithText.book.textPath
) )
if (fileToDelete.exists()) { if (fileToDelete.exists()) {
fileToDelete.delete() fileToDelete.delete()
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(UPDATE_BOOK, "Failed to delete old text.")
e.printStackTrace() e.printStackTrace()
} }
} }
val updatedBook = bookMapper.toBookEntity( val updatedBook = bookMapper.toBookEntity(
book.copy( bookWithText.book.copy(
textPath = "$booksDir/$textUri", textPath = "$booksDir/$textUri",
coverImage = if (bookEntity.image != null) Uri.parse(bookEntity.image) else null coverImage = if (bookEntity.image != null) Uri.parse(bookEntity.image) else null
) )
@ -276,13 +316,19 @@ class BookRepositoryImpl @Inject constructor(
database.updateBooks( database.updateBooks(
listOf(updatedBook) listOf(updatedBook)
) )
Log.i(UPDATE_BOOK, "Successfully updated book.")
return true return true
} }
/**
* Update cover image of the book. Deletes old cover and replaces with new.
*/
override suspend fun updateCoverImageOfBook( override suspend fun updateCoverImageOfBook(
bookWithOldCover: Book, bookWithOldCover: Book,
newCoverImage: CoverImage? newCoverImage: CoverImage?
) { ) {
Log.i(UPDATE_BOOK, "Updating cover image: ${bookWithOldCover.title}.")
// without text // without text
val book = database.findBookById(bookWithOldCover.id) val book = database.findBookById(bookWithOldCover.id)
var uri: String? = null var uri: String? = null
@ -291,6 +337,7 @@ class BookRepositoryImpl @Inject constructor(
val coversDir = File(filesDir, "covers") val coversDir = File(filesDir, "covers")
if (!coversDir.exists()) { if (!coversDir.exists()) {
Log.i(UPDATE_BOOK, "Created covers folder.")
coversDir.mkdirs() coversDir.mkdirs()
} }
@ -313,6 +360,7 @@ class BookRepositoryImpl @Inject constructor(
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(UPDATE_BOOK, "Could not save new cover.")
e.printStackTrace() e.printStackTrace()
return return
} }
@ -328,6 +376,7 @@ class BookRepositoryImpl @Inject constructor(
fileToDelete.delete() fileToDelete.delete()
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(UPDATE_BOOK, "Could not delete old cover.")
e.printStackTrace() e.printStackTrace()
} }
} }
@ -350,17 +399,26 @@ class BookRepositoryImpl @Inject constructor(
) )
) )
) )
Log.i(UPDATE_BOOK, "Successfully updated cover image.")
} }
/**
* Delete books.
* Also deletes cover image and text from internal storage.
*/
override suspend fun deleteBooks(books: List<Book>) { override suspend fun deleteBooks(books: List<Book>) {
Log.i(DELETE_BOOKS, "Deleting books.")
val filesDir = application.filesDir val filesDir = application.filesDir
val coversDir = File(filesDir, "covers") val coversDir = File(filesDir, "covers")
val booksDir = File(filesDir, "books") val booksDir = File(filesDir, "books")
if (!coversDir.exists()) { if (!coversDir.exists()) {
Log.i(DELETE_BOOKS, "Created covers folder.")
coversDir.mkdirs() coversDir.mkdirs()
} }
if (!booksDir.exists()) { if (!booksDir.exists()) {
Log.i(DELETE_BOOKS, "Created books folder.")
booksDir.mkdirs() booksDir.mkdirs()
} }
@ -378,6 +436,7 @@ class BookRepositoryImpl @Inject constructor(
fileToDelete.delete() fileToDelete.delete()
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(DELETE_BOOKS, "Could not delete cover image.")
e.printStackTrace() e.printStackTrace()
} }
} }
@ -390,6 +449,7 @@ class BookRepositoryImpl @Inject constructor(
fileToDelete.delete() fileToDelete.delete()
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(DELETE_BOOKS, "Could not delete text.")
e.printStackTrace() e.printStackTrace()
} }
} }
@ -397,15 +457,21 @@ class BookRepositoryImpl @Inject constructor(
book book
} }
) )
Log.i(DELETE_BOOKS, "Successfully deleted books.")
} }
/**
* @return Whether can reset cover image (restore default).
*/
override suspend fun canResetCover(bookId: Int): Boolean { override suspend fun canResetCover(bookId: Int): Boolean {
val book = database.findBookById(bookId) val book = database.findBookById(bookId)
val defaultCoverUncompressed = fileParser.parse(File(book.filePath))?.second val defaultCoverUncompressed = fileParser.parse(File(book.filePath))?.coverImage
?: return false ?: return false
if (book.image == null) { if (book.image == null) {
Log.i(CAN_RESET_COVER, "Can reset cover image. (current is null)")
return true return true
} }
@ -424,6 +490,7 @@ class BookRepositoryImpl @Inject constructor(
Uri.parse(book.image) Uri.parse(book.image)
) )
} catch (e: Exception) { } catch (e: Exception) {
Log.i(CAN_RESET_COVER, "Can reset cover image. (could not get current)")
e.printStackTrace() e.printStackTrace()
return true return true
} }
@ -431,30 +498,45 @@ class BookRepositoryImpl @Inject constructor(
return !defaultCover.sameAs(currentCover) return !defaultCover.sameAs(currentCover)
} }
/**
* Reset cover image to default.
* If there is no default cover, returns false.
*/
override suspend fun resetCoverImage(bookId: Int): Boolean { override suspend fun resetCoverImage(bookId: Int): Boolean {
if (!canResetCover(bookId)) { if (!canResetCover(bookId)) {
Log.w(RESET_COVER, "Cannot reset cover image.")
return false return false
} }
val book = database.findBookById(bookId) val book = database.findBookById(bookId)
val defaultCover = fileParser.parse(File(book.filePath))?.second val defaultCover = fileParser.parse(File(book.filePath))?.coverImage
?: return false ?: return false
updateCoverImageOfBook(bookMapper.toBook(book), defaultCover) updateCoverImageOfBook(bookMapper.toBook(book), defaultCover)
Log.i(RESET_COVER, "Successfully reset cover image.")
return true return true
} }
/**
* Puts DataStore constant to [DataStore].
*/
override suspend fun <T> putDataToDataStore(key: Preferences.Key<T>, value: T) { override suspend fun <T> putDataToDataStore(key: Preferences.Key<T>, value: T) {
dataStore.putData(key, value) dataStore.putData(key, value)
} }
override suspend fun getAllSettings(scope: CoroutineScope): MainState { /**
* Gets all settings from DataStore and returns [MainState].
*/
override suspend fun getAllSettings(): MainState {
Log.i(GET_ALL_SETTINGS, "Getting all settings.")
val result = CompletableDeferred<MainState>() val result = CompletableDeferred<MainState>()
scope.launch { withContext(Dispatchers.Default) {
val keys = dataStore.getAllData() val keys = dataStore.getAllData()
val data = mutableMapOf<String, Any>() val data = mutableMapOf<String, Any>()
Log.i(GET_ALL_SETTINGS, "Got ${keys?.size} settings keys.")
val jobs = keys?.map { key -> val jobs = keys?.map { key ->
async { async {
val nullableData = dataStore.getNullableData(key) val nullableData = dataStore.getNullableData(key)
@ -474,7 +556,13 @@ class BookRepositoryImpl @Inject constructor(
return result.await() return result.await()
} }
/**
* Get all matching files from device.
* Filters by [query] and sorts out not supported file formats and already added files.
*/
override suspend fun getFilesFromDevice(query: String): List<SelectableFile> { override suspend fun getFilesFromDevice(query: String): List<SelectableFile> {
Log.i(GET_FILES_FROM_DEVICE, "Getting files from device by query: \"$query\".")
val existingBooks = database val existingBooks = database
.searchBooks("") .searchBooks("")
.map { bookMapper.toBook(it) } .map { bookMapper.toBook(it) }
@ -564,9 +652,11 @@ class BookRepositoryImpl @Inject constructor(
(Environment.getExternalStorageState() != Environment.MEDIA_MOUNTED && (Environment.getExternalStorageState() != Environment.MEDIA_MOUNTED &&
Environment.getExternalStorageState() != Environment.MEDIA_MOUNTED_READ_ONLY) Environment.getExternalStorageState() != Environment.MEDIA_MOUNTED_READ_ONLY)
) { ) {
Log.e(GET_FILES_FROM_DEVICE, "Could not correctly get root directory.")
return emptyList() return emptyList()
} }
Log.i(GET_FILES_FROM_DEVICE, "Successfully got all matching files.")
return rootDirectory.getAllFiles() return rootDirectory.getAllFiles()
} }
@ -576,74 +666,106 @@ class BookRepositoryImpl @Inject constructor(
override suspend fun getBookFromFile(file: File): NullableBook { override suspend fun getBookFromFile(file: File): NullableBook {
val parsedBook = fileParser.parse(file) val parsedBook = fileParser.parse(file)
if (parsedBook == null) { if (parsedBook == null) {
Log.w(GET_BOOK_FROM_FILE, "Parsed book(${file.name}) is null.") Log.e(GET_BOOK_FROM_FILE, "Parsed file(${file.name}) is null.")
return NullableBook.Null( return NullableBook.Null(
file.name, file.name,
UIText.StringResource(R.string.error_wrong_file_format) UIText.StringResource(R.string.error_something_went_wrong)
) )
} }
val parsedText = textParser.parse(file) val parsedText = textParser.parse(file)
if (parsedText is Resource.Error) { if (parsedText is Resource.Error) {
Log.w(GET_BOOK_FROM_FILE, "Parsed text(${file.name}) has error.") Log.e(GET_BOOK_FROM_FILE, "Parsed text(${file.name}) has error.")
return NullableBook.Null( return NullableBook.Null(
file.name, file.name,
parsedText.message parsedText.message
) )
} }
Log.i(GET_BOOK_FROM_FILE, "Successfully got book from file.")
return NullableBook.NotNull( return NullableBook.NotNull(
book = parsedBook.first.copy( bookWithTextAndCover = BookWithTextAndCover(
chapters = parsedText.data!!.map { it.chapter }.run { book = parsedBook.book.copy(
if (this.size == 1) return@run emptyList() chapters = parsedText.data!!.map { it.chapter }.run {
this if (this.size == 1) return@run emptyList()
} this
), }
coverImage = parsedBook.second, ),
text = parsedText.data.map { coverImage = parsedBook.coverImage,
it.text text = parsedText.data.map {
}.flatten() it.text
}.flatten()
)
) )
} }
override suspend fun insertHistory(history: List<History>) { /**
database.insertHistory(history.map { historyMapper.toHistoryEntity(it) }) * Insert history in database.
*/
override suspend fun insertHistory(history: History) {
database.insertHistory(
listOf(
historyMapper.toHistoryEntity(
history
)
)
)
} }
override suspend fun getHistory(): Flow<Resource<List<History>>> { /**
return flow { * Get all history from database.
val history = database.getHistory() */
override suspend fun getHistory(): List<History> {
emit( return database.getHistory().map {
Resource.Success( historyMapper.toHistory(
data = history.map { it
historyMapper.toHistory(
it
)
}
)
) )
} }
} }
/**
* Get latest history of the matching [bookId].
*/
override suspend fun getLatestBookHistory(bookId: Int): History? { override suspend fun getLatestBookHistory(bookId: Int): History? {
val history = database.getLatestHistoryForBook(bookId) val history = database.getLatestHistoryForBook(bookId)
return history?.let { historyMapper.toHistory(it) } return history?.let { historyMapper.toHistory(it) }
} }
/**
* Delete whole history.
*/
override suspend fun deleteWholeHistory() { override suspend fun deleteWholeHistory() {
database.deleteWholeHistory() database.deleteWholeHistory()
} }
/**
* Delete all history of the matching [bookId].
*/
override suspend fun deleteBookHistory(bookId: Int) { override suspend fun deleteBookHistory(bookId: Int) {
database.deleteBookHistory(bookId) database.deleteBookHistory(bookId)
} }
override suspend fun deleteHistory(history: List<History>) { /**
database.deleteHistory(history.map { historyMapper.toHistoryEntity(it) }) * Delete specific history item.
*/
override suspend fun deleteHistory(history: History) {
database.deleteHistory(
listOf(
historyMapper.toHistoryEntity(
history
)
)
)
} }
/**
* Check for updates from GitHub.
*
* @param postNotification Whether notification should be send.
*/
override suspend fun checkForUpdates(postNotification: Boolean): LatestReleaseInfo? { override suspend fun checkForUpdates(postNotification: Boolean): LatestReleaseInfo? {
Log.i(CHECK_FOR_UPDATES, "Checking for updates. Post notification: $postNotification")
return withContext(Dispatchers.IO) { return withContext(Dispatchers.IO) {
try { try {
val result = githubAPI.getLatestRelease() val result = githubAPI.getLatestRelease()
@ -652,6 +774,7 @@ class BookRepositoryImpl @Inject constructor(
val currentVersion = application.getString(R.string.app_version) val currentVersion = application.getString(R.string.app_version)
if (version != currentVersion && postNotification) { if (version != currentVersion && postNotification) {
Log.i(CHECK_FOR_UPDATES, "Posting notification.")
updatesNotificationService.postNotification( updatesNotificationService.postNotification(
result result
) )
@ -659,12 +782,16 @@ class BookRepositoryImpl @Inject constructor(
result result
} catch (e: Exception) { } catch (e: Exception) {
Log.e(CHECK_FOR_UPDATES, "Could not get latest release information.")
e.printStackTrace() e.printStackTrace()
null null
} }
} }
} }
/**
* Update color preset.
*/
override suspend fun updateColorPreset(colorPreset: ColorPreset) { override suspend fun updateColorPreset(colorPreset: ColorPreset) {
database.updateColorPreset( database.updateColorPreset(
colorPresetMapper.toColorPresetEntity( colorPresetMapper.toColorPresetEntity(
@ -675,6 +802,9 @@ class BookRepositoryImpl @Inject constructor(
) )
} }
/**
* Select color preset. Only one can be selected at time.
*/
override suspend fun selectColorPreset(colorPreset: ColorPreset) { override suspend fun selectColorPreset(colorPreset: ColorPreset) {
database.getColorPresets().map { database.getColorPresets().map {
it.copy( it.copy(
@ -685,12 +815,20 @@ class BookRepositoryImpl @Inject constructor(
} }
} }
/**
* Get all color presets.
* Sorted by order (either manual or newest ones at the end).
*/
override suspend fun getColorPresets(): List<ColorPreset> { override suspend fun getColorPresets(): List<ColorPreset> {
return database.getColorPresets() return database.getColorPresets()
.sortedBy { it.order } .sortedBy { it.order }
.map { colorPresetMapper.toColorPreset(it) } .map { colorPresetMapper.toColorPreset(it) }
} }
/**
* Reorder color presets.
* Changes the order of the color presets.
*/
override suspend fun reorderColorPresets(orderedColorPresets: List<ColorPreset>) { override suspend fun reorderColorPresets(orderedColorPresets: List<ColorPreset>) {
database.deleteColorPresets() database.deleteColorPresets()
@ -701,6 +839,9 @@ class BookRepositoryImpl @Inject constructor(
} }
} }
/**
* Delete color preset.
*/
override suspend fun deleteColorPreset(colorPreset: ColorPreset) { override suspend fun deleteColorPreset(colorPreset: ColorPreset) {
database.deleteColorPreset( database.deleteColorPreset(
colorPresetMapper.toColorPresetEntity( colorPresetMapper.toColorPresetEntity(
@ -709,6 +850,11 @@ class BookRepositoryImpl @Inject constructor(
) )
} }
/**
* Create or delete favorite directory if already exists.
*
* @param path Path to directory.
*/
override suspend fun updateFavoriteDirectory(path: String) { override suspend fun updateFavoriteDirectory(path: String) {
if (database.favoriteDirectoryExits(path)) { if (database.favoriteDirectoryExits(path)) {
database.deleteFavoriteDirectory( database.deleteFavoriteDirectory(
@ -721,19 +867,4 @@ class BookRepositoryImpl @Inject constructor(
FavoriteDirectoryEntity(path) FavoriteDirectoryEntity(path)
) )
} }
} }

View file

@ -2,6 +2,7 @@ package ua.acclorite.book_story.domain.model
import android.net.Uri import android.net.Uri
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import ua.acclorite.book_story.domain.util.CoverImage
import ua.acclorite.book_story.domain.util.UIText import ua.acclorite.book_story.domain.util.UIText
@Immutable @Immutable
@ -23,4 +24,47 @@ data class Book(
val lastOpened: Long?, val lastOpened: Long?,
val category: Category, val category: Category,
) )
@Immutable
data class BookWithText(
val book: Book,
val text: List<String>
)
@Immutable
data class BookWithCover(
val book: Book,
val coverImage: CoverImage?
)
@Immutable
data class BookWithTextAndCover(
val book: Book,
val coverImage: CoverImage?,
val text: List<String>
)
@Immutable
sealed class NullableBook(
val bookWithTextAndCover: BookWithTextAndCover?,
val fileName: String?,
val message: UIText?
) {
class NotNull(
bookWithTextAndCover: BookWithTextAndCover
) : NullableBook(
bookWithTextAndCover = bookWithTextAndCover,
fileName = null,
message = null
)
class Null(
fileName: String,
message: UIText?
) : NullableBook(
bookWithTextAndCover = null,
fileName = fileName,
message = message
)
}

View file

@ -1,31 +0,0 @@
package ua.acclorite.book_story.domain.model
import androidx.compose.runtime.Immutable
import ua.acclorite.book_story.domain.util.CoverImage
import ua.acclorite.book_story.domain.util.UIText
@Immutable
sealed class NullableBook(
val book: Book?,
val coverImage: CoverImage? = null,
val text: List<String> = emptyList(),
val fileName: String?,
val message: UIText?
) {
class NotNull(
book: Book,
coverImage: CoverImage?,
text: List<String>
) : NullableBook(
book = book,
text = text,
coverImage = coverImage,
fileName = null,
message = null
)
class Null(
fileName: String,
message: UIText?
) : NullableBook(null, text = emptyList(), fileName = fileName, message = message)
}

View file

@ -1,21 +1,21 @@
package ua.acclorite.book_story.domain.repository package ua.acclorite.book_story.domain.repository
import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.Preferences
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import ua.acclorite.book_story.data.remote.dto.LatestReleaseInfo import ua.acclorite.book_story.data.remote.dto.LatestReleaseInfo
import ua.acclorite.book_story.domain.model.Book import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.domain.model.BookWithText
import ua.acclorite.book_story.domain.model.BookWithTextAndCover
import ua.acclorite.book_story.domain.model.ColorPreset import ua.acclorite.book_story.domain.model.ColorPreset
import ua.acclorite.book_story.domain.model.History import ua.acclorite.book_story.domain.model.History
import ua.acclorite.book_story.domain.model.NullableBook import ua.acclorite.book_story.domain.model.NullableBook
import ua.acclorite.book_story.domain.model.SelectableFile import ua.acclorite.book_story.domain.model.SelectableFile
import ua.acclorite.book_story.domain.util.CoverImage import ua.acclorite.book_story.domain.util.CoverImage
import ua.acclorite.book_story.domain.util.Resource
import ua.acclorite.book_story.presentation.data.MainState import ua.acclorite.book_story.presentation.data.MainState
import java.io.File import java.io.File
interface BookRepository { interface BookRepository {
/* ------ Books ------------------------------ */
suspend fun getBooks( suspend fun getBooks(
query: String query: String
): List<Book> ): List<Book>
@ -29,18 +29,15 @@ interface BookRepository {
): List<String> ): List<String>
suspend fun insertBook( suspend fun insertBook(
book: Book, bookWithTextAndCover: BookWithTextAndCover
coverImage: CoverImage?,
text: List<String>
): Boolean ): Boolean
suspend fun updateBooks( suspend fun updateBook(
books: List<Book> book: Book
) )
suspend fun updateBookWithText( suspend fun updateBookWithText(
book: Book, bookWithText: BookWithText
text: List<String>
): Boolean ): Boolean
suspend fun updateCoverImageOfBook( suspend fun updateCoverImageOfBook(
@ -56,49 +53,85 @@ interface BookRepository {
bookId: Int bookId: Int
): Boolean ): Boolean
suspend fun resetCoverImage(bookId: Int): Boolean suspend fun resetCoverImage(
bookId: Int
): Boolean
/* - - - - - - - - - - - - - - - - - - - - - - */
/* ------ DataStore -------------------------- */
suspend fun <T> putDataToDataStore( suspend fun <T> putDataToDataStore(
key: Preferences.Key<T>, key: Preferences.Key<T>,
value: T value: T
) )
suspend fun getAllSettings(): MainState
suspend fun getAllSettings(scope: CoroutineScope): MainState /* - - - - - - - - - - - - - - - - - - - - - - */
suspend fun getFilesFromDevice(query: String = ""): List<SelectableFile>
suspend fun getBookFromFile(file: File): NullableBook
suspend fun insertHistory(history: List<History>) /* ------ File System ------------------------ */
suspend fun getFilesFromDevice(
query: String = ""
): List<SelectableFile>
suspend fun getHistory(): Flow<Resource<List<History>>> suspend fun getBookFromFile(
file: File
): NullableBook
/* - - - - - - - - - - - - - - - - - - - - - - */
suspend fun getLatestBookHistory(bookId: Int): History?
/* ------ History ---------------------------- */
suspend fun insertHistory(
history: History
)
suspend fun getHistory(): List<History>
suspend fun getLatestBookHistory(
bookId: Int
): History?
suspend fun deleteWholeHistory() suspend fun deleteWholeHistory()
suspend fun deleteBookHistory(bookId: Int) suspend fun deleteBookHistory(
bookId: Int
suspend fun deleteHistory(
history: List<History>
) )
suspend fun deleteHistory(
suspend fun checkForUpdates(postNotification: Boolean): LatestReleaseInfo? history: History
)
/* - - - - - - - - - - - - - - - - - - - - - - */
suspend fun updateColorPreset(colorPreset: ColorPreset) /* ------ API (GitHub) ----------------------- */
suspend fun checkForUpdates(
postNotification: Boolean
): LatestReleaseInfo?
/* - - - - - - - - - - - - - - - - - - - - - - */
suspend fun selectColorPreset(colorPreset: ColorPreset)
/* ------ Color Presets ---------------------- */
suspend fun updateColorPreset(
colorPreset: ColorPreset
)
suspend fun selectColorPreset(
colorPreset: ColorPreset
)
suspend fun getColorPresets(): List<ColorPreset> suspend fun getColorPresets(): List<ColorPreset>
suspend fun reorderColorPresets(orderedColorPresets: List<ColorPreset>) suspend fun reorderColorPresets(
orderedColorPresets: List<ColorPreset>
)
suspend fun deleteColorPreset(colorPreset: ColorPreset) suspend fun deleteColorPreset(
colorPreset: ColorPreset
)
/* - - - - - - - - - - - - - - - - - - - - - - */
/* ------ Favorite Directories --------------- */
suspend fun updateFavoriteDirectory(path: String) suspend fun updateFavoriteDirectory(path: String)
/* - - - - - - - - - - - - - - - - - - - - - - */
} }

View file

@ -6,7 +6,7 @@ import javax.inject.Inject
class DeleteHistory @Inject constructor(private val repository: BookRepository) { class DeleteHistory @Inject constructor(private val repository: BookRepository) {
suspend fun execute(history: List<History>) { suspend fun execute(history: History) {
repository.deleteHistory(history) repository.deleteHistory(history)
} }
} }

View file

@ -1,6 +1,5 @@
package ua.acclorite.book_story.domain.use_case package ua.acclorite.book_story.domain.use_case
import kotlinx.coroutines.CoroutineScope
import ua.acclorite.book_story.domain.repository.BookRepository import ua.acclorite.book_story.domain.repository.BookRepository
import ua.acclorite.book_story.presentation.data.MainState import ua.acclorite.book_story.presentation.data.MainState
import javax.inject.Inject import javax.inject.Inject
@ -9,7 +8,7 @@ class GetAllSettings @Inject constructor(
private val repository: BookRepository private val repository: BookRepository
) { ) {
suspend fun execute(scope: CoroutineScope): MainState { suspend fun execute(): MainState {
return repository.getAllSettings(scope) return repository.getAllSettings()
} }
} }

View file

@ -1,14 +1,12 @@
package ua.acclorite.book_story.domain.use_case package ua.acclorite.book_story.domain.use_case
import kotlinx.coroutines.flow.Flow
import ua.acclorite.book_story.domain.model.History import ua.acclorite.book_story.domain.model.History
import ua.acclorite.book_story.domain.repository.BookRepository import ua.acclorite.book_story.domain.repository.BookRepository
import ua.acclorite.book_story.domain.util.Resource
import javax.inject.Inject import javax.inject.Inject
class GetHistory @Inject constructor(private val repository: BookRepository) { class GetHistory @Inject constructor(private val repository: BookRepository) {
suspend fun execute(): Flow<Resource<List<History>>> { suspend fun execute(): List<History> {
return repository.getHistory() return repository.getHistory()
} }
} }

View file

@ -1,16 +1,13 @@
package ua.acclorite.book_story.domain.use_case package ua.acclorite.book_story.domain.use_case
import ua.acclorite.book_story.domain.model.Book import ua.acclorite.book_story.domain.model.BookWithTextAndCover
import ua.acclorite.book_story.domain.repository.BookRepository import ua.acclorite.book_story.domain.repository.BookRepository
import ua.acclorite.book_story.domain.util.CoverImage
import javax.inject.Inject import javax.inject.Inject
class InsertBook @Inject constructor(private val repository: BookRepository) { class InsertBook @Inject constructor(private val repository: BookRepository) {
suspend fun execute( suspend fun execute(
book: Book, bookWithTextAndCover: BookWithTextAndCover
coverImage: CoverImage?,
text: List<String>
): Boolean { ): Boolean {
return repository.insertBook(book, coverImage, text) return repository.insertBook(bookWithTextAndCover)
} }
} }

View file

@ -5,7 +5,7 @@ import ua.acclorite.book_story.domain.repository.BookRepository
import javax.inject.Inject import javax.inject.Inject
class InsertHistory @Inject constructor(private val repository: BookRepository) { class InsertHistory @Inject constructor(private val repository: BookRepository) {
suspend fun execute(history: List<History>) { suspend fun execute(history: History) {
repository.insertHistory(history) repository.insertHistory(history)
} }
} }

View file

@ -7,6 +7,6 @@ import javax.inject.Inject
class UpdateBook @Inject constructor(private val repository: BookRepository) { class UpdateBook @Inject constructor(private val repository: BookRepository) {
suspend fun execute(book: Book) { suspend fun execute(book: Book) {
repository.updateBooks(listOf(book)) repository.updateBook(book)
} }
} }

View file

@ -1,12 +1,12 @@
package ua.acclorite.book_story.domain.use_case package ua.acclorite.book_story.domain.use_case
import ua.acclorite.book_story.domain.model.Book import ua.acclorite.book_story.domain.model.BookWithText
import ua.acclorite.book_story.domain.repository.BookRepository import ua.acclorite.book_story.domain.repository.BookRepository
import javax.inject.Inject import javax.inject.Inject
class UpdateBookWithText @Inject constructor(private val repository: BookRepository) { class UpdateBookWithText @Inject constructor(private val repository: BookRepository) {
suspend fun execute(book: Book, text: List<String>): Boolean { suspend fun execute(bookWithText: BookWithText): Boolean {
return repository.updateBookWithText(book, text) return repository.updateBookWithText(bookWithText)
} }
} }

View file

@ -468,7 +468,7 @@ class MainViewModel @Inject constructor(
settingsViewModel: SettingsViewModel, settingsViewModel: SettingsViewModel,
) { ) {
viewModelScope.launch(Dispatchers.Main) { viewModelScope.launch(Dispatchers.Main) {
val settings = getAllSettings.execute(viewModelScope) val settings = getAllSettings.execute()
// All additional execution // All additional execution
changeLanguage.execute(settings.language) changeLanguage.execute(settings.language)

View file

@ -20,6 +20,7 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.yield import kotlinx.coroutines.yield
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.model.BookWithText
import ua.acclorite.book_story.domain.model.Category import ua.acclorite.book_story.domain.model.Category
import ua.acclorite.book_story.domain.model.History import ua.acclorite.book_story.domain.model.History
import ua.acclorite.book_story.domain.model.NullableBook import ua.acclorite.book_story.domain.model.NullableBook
@ -531,7 +532,7 @@ class BookInfoViewModel @Inject constructor(
var textUpdated = false var textUpdated = false
val updatedText = updatedBook.text val updatedText = updatedBook.bookWithTextAndCover!!.text
val text = getText.execute(book.textPath) val text = getText.execute(book.textPath)
if (updatedText != text) { if (updatedText != text) {
@ -560,7 +561,7 @@ class BookInfoViewModel @Inject constructor(
yield() yield()
onEvent( onEvent(
BookInfoEvent.OnShowConfirmUpdateDialog( BookInfoEvent.OnShowConfirmUpdateDialog(
updatedText = updatedBook.text, updatedText = updatedBook.bookWithTextAndCover.text,
) )
) )
@ -629,8 +630,10 @@ class BookInfoViewModel @Inject constructor(
val updatedText = _state.value.updatedText ?: return@launch val updatedText = _state.value.updatedText ?: return@launch
val isSuccess = updateBookWithText.execute( val isSuccess = updateBookWithText.execute(
book = _state.value.book, BookWithText(
text = updatedText book = _state.value.book,
text = updatedText
)
) )
if (!isSuccess) { if (!isSuccess) {
@ -732,12 +735,10 @@ class BookInfoViewModel @Inject constructor(
onEvent(BookInfoEvent.OnCancelUpdate) onEvent(BookInfoEvent.OnCancelUpdate)
_state.value.book.id.let { _state.value.book.id.let {
insertHistory.execute( insertHistory.execute(
listOf( History(
History( bookId = it,
bookId = it, book = null,
book = null, time = Date().time
time = Date().time
)
) )
) )
} }

View file

@ -46,14 +46,14 @@ fun BrowseAddingDialogItem(result: Pair<NullableBook, Selected>, onClick: (Boole
Modifier.weight(1f) Modifier.weight(1f)
) { ) {
Text( Text(
text = result.first.book!!.title, text = result.first.bookWithTextAndCover!!.book.title,
style = MaterialTheme.typography.bodyLarge, style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface, color = MaterialTheme.colorScheme.onSurface,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis overflow = TextOverflow.Ellipsis
) )
Text( Text(
text = result.first.book!!.author.asString(), text = result.first.bookWithTextAndCover!!.book.author.asString(),
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 1, maxLines = 1,

View file

@ -467,12 +467,11 @@ class BrowseViewModel @Inject constructor(
return@launch return@launch
} }
val failed = booksToInsert.any { var failed = false
!insertBook.execute( booksToInsert.forEach {
it.book!!, if (!insertBook.execute(it.bookWithTextAndCover!!)) {
it.coverImage, failed = true
it.text }
)
} }
if (failed) { if (failed) {

View file

@ -20,7 +20,6 @@ import ua.acclorite.book_story.domain.use_case.DeleteWholeHistory
import ua.acclorite.book_story.domain.use_case.GetBooksById import ua.acclorite.book_story.domain.use_case.GetBooksById
import ua.acclorite.book_story.domain.use_case.GetHistory import ua.acclorite.book_story.domain.use_case.GetHistory
import ua.acclorite.book_story.domain.use_case.InsertHistory import ua.acclorite.book_story.domain.use_case.InsertHistory
import ua.acclorite.book_story.domain.util.Resource
import ua.acclorite.book_story.presentation.core.navigation.Screen import ua.acclorite.book_story.presentation.core.navigation.Screen
import ua.acclorite.book_story.presentation.core.util.BaseViewModel import ua.acclorite.book_story.presentation.core.util.BaseViewModel
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
@ -119,9 +118,7 @@ class HistoryViewModel @Inject constructor(
is HistoryEvent.OnDeleteHistoryElement -> { is HistoryEvent.OnDeleteHistoryElement -> {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
deleteHistory.execute( deleteHistory.execute(event.historyToDelete)
listOf(event.historyToDelete)
)
_state.update { _state.update {
it.copy( it.copy(
@ -154,9 +151,7 @@ class HistoryViewModel @Inject constructor(
when (snackbar) { when (snackbar) {
SnackbarResult.Dismissed -> Unit SnackbarResult.Dismissed -> Unit
SnackbarResult.ActionPerformed -> { SnackbarResult.ActionPerformed -> {
insertHistory.execute( insertHistory.execute(event.historyToDelete)
listOf(event.historyToDelete)
)
event.refreshList() event.refreshList()
_state.update { _state.update {
it.copy( it.copy(
@ -256,12 +251,10 @@ class HistoryViewModel @Inject constructor(
viewModelScope.launch { viewModelScope.launch {
event.book.id.let { event.book.id.let {
insertHistory.execute( insertHistory.execute(
listOf( History(
History( bookId = it,
bookId = it, book = null,
book = null, time = Date().time
time = Date().time
)
) )
) )
} }
@ -300,91 +293,82 @@ class HistoryViewModel @Inject constructor(
return maxElementsById.filterNotNull() return maxElementsById.filterNotNull()
} }
getHistory.execute().collect { result -> val historyWithoutBooks = getHistory.execute()
when (result) { .sortedByDescending { it.time }
is Resource.Success -> {
val historyWithoutBook =
result.data?.sortedByDescending { it.time } ?: emptyList()
if (historyWithoutBook.isEmpty()) { if (historyWithoutBooks.isEmpty()) {
_state.update { _state.update {
it.copy( it.copy(
history = emptyList(), history = emptyList(),
isLoading = false isLoading = false
) )
}
return@collect
}
val books = getBooksById.execute(
historyWithoutBook.map { it.bookId }.distinct()
)
if (books.isEmpty()) {
_state.update {
it.copy(
history = emptyList(),
isLoading = false
)
}
return@collect
}
val history = historyWithoutBook.map {
val book = books.find { book -> book.id == it.bookId }!!
it.copy(
book = book
)
}
val groupedHistory = mutableListOf<GroupedHistory>()
history
.filter {
val book = books.find { book -> book.id == it.bookId }!!
book.title.lowercase().trim().contains(query.lowercase().trim())
}.groupBy { item ->
val calendar = Calendar.getInstance().apply {
timeInMillis = item.time
}
val now = Calendar.getInstance()
when {
isSameDay(calendar, now) -> "today"
isSameDay(
calendar,
now.apply {
add(
Calendar.DAY_OF_YEAR,
-1
)
}) -> "yesterday"
else -> SimpleDateFormat(
"dd.MM.yy",
Locale.getDefault()
).format(item.time)
}
}.forEach { (key, value) ->
groupedHistory.add(
GroupedHistory(
key,
filterMaxElementsById(value)
)
)
}
_state.update {
it.copy(
history = groupedHistory,
isLoading = false
)
}
}
is Resource.Error -> Unit
} }
return
}
val books = getBooksById.execute(
historyWithoutBooks.map { it.bookId }.distinct()
)
if (books.isEmpty()) {
_state.update {
it.copy(
history = emptyList(),
isLoading = false
)
}
return
}
val history = historyWithoutBooks.map {
val book = books.find { book -> book.id == it.bookId }!!
it.copy(
book = book
)
}
val groupedHistory = mutableListOf<GroupedHistory>()
history
.filter {
val book = books.find { book -> book.id == it.bookId }!!
book.title.lowercase().trim().contains(query.lowercase().trim())
}.groupBy { item ->
val calendar = Calendar.getInstance().apply {
timeInMillis = item.time
}
val now = Calendar.getInstance()
when {
isSameDay(calendar, now) -> "today"
isSameDay(
calendar,
now.apply {
add(
Calendar.DAY_OF_YEAR,
-1
)
}) -> "yesterday"
else -> SimpleDateFormat(
"dd.MM.yy",
Locale.getDefault()
).format(item.time)
}
}.forEach { (key, value) ->
groupedHistory.add(
GroupedHistory(
key,
filterMaxElementsById(value)
)
)
}
_state.update {
it.copy(
history = groupedHistory,
isLoading = false
)
} }
} }
} }

View file

@ -328,12 +328,10 @@ class LibraryViewModel @Inject constructor(
viewModelScope.launch { viewModelScope.launch {
event.book.id.let { event.book.id.let {
insertHistory.execute( insertHistory.execute(
listOf( History(
History( bookId = it,
bookId = it, book = null,
book = null, time = Date().time
time = Date().time
)
) )
) )
} }