🛠️ Add logging and reformat data layer
* Reformatted BookRepository, added logs * Improved code readability, added comments
This commit is contained in:
parent
94c49c20c8
commit
100926ce14
25 changed files with 544 additions and 375 deletions
|
|
@ -1,11 +1,10 @@
|
|||
package ua.acclorite.book_story.data.parser
|
||||
|
||||
import ua.acclorite.book_story.domain.model.Book
|
||||
import ua.acclorite.book_story.domain.util.CoverImage
|
||||
import ua.acclorite.book_story.domain.model.BookWithCover
|
||||
import java.io.File
|
||||
|
||||
|
||||
interface FileParser {
|
||||
|
||||
suspend fun parse(file: File): Pair<Book, CoverImage?>?
|
||||
suspend fun parse(file: File): BookWithCover?
|
||||
}
|
||||
|
|
@ -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.pdf.PdfFileParser
|
||||
import ua.acclorite.book_story.data.parser.txt.TxtFileParser
|
||||
import ua.acclorite.book_story.domain.model.Book
|
||||
import ua.acclorite.book_story.domain.util.CoverImage
|
||||
import ua.acclorite.book_story.domain.model.BookWithCover
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
@ -22,7 +21,7 @@ class FileParserImpl @Inject constructor(
|
|||
private val htmlFileParser: HtmlFileParser,
|
||||
private val htmFileParser: HtmFileParser,
|
||||
) : FileParser {
|
||||
override suspend fun parse(file: File): Pair<Book, CoverImage?>? {
|
||||
override suspend fun parse(file: File): BookWithCover? {
|
||||
if (!file.exists()) {
|
||||
Log.e(FILE_PARSER, "File does not exist.")
|
||||
return null
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@ import org.jsoup.Jsoup
|
|||
import ua.acclorite.book_story.R
|
||||
import ua.acclorite.book_story.data.parser.FileParser
|
||||
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.util.CoverImage
|
||||
import ua.acclorite.book_story.domain.util.UIText
|
||||
import java.io.File
|
||||
import java.util.zip.ZipFile
|
||||
|
|
@ -17,9 +17,9 @@ import javax.inject.Inject
|
|||
|
||||
class EpubFileParser @Inject constructor() : FileParser {
|
||||
|
||||
override suspend fun parse(file: File): Pair<Book, CoverImage?>? {
|
||||
override suspend fun parse(file: File): BookWithCover? {
|
||||
return try {
|
||||
var book: Pair<Book, CoverImage?>? = null
|
||||
var book: BookWithCover? = null
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
ZipFile(file).use { zip ->
|
||||
|
|
@ -71,19 +71,22 @@ class EpubFileParser @Inject constructor() : FileParser {
|
|||
.firstOrNull()?.attr("href")
|
||||
}
|
||||
|
||||
book = Book(
|
||||
title = title,
|
||||
author = author,
|
||||
description = description,
|
||||
textPath = "",
|
||||
scrollIndex = 0,
|
||||
scrollOffset = 0,
|
||||
progress = 0f,
|
||||
filePath = file.path,
|
||||
lastOpened = null,
|
||||
category = Category.entries[0],
|
||||
coverImage = null
|
||||
) to extractCoverImageBitmap(file, coverImage)
|
||||
book = BookWithCover(
|
||||
book = Book(
|
||||
title = title,
|
||||
author = author,
|
||||
description = description,
|
||||
textPath = "",
|
||||
scrollIndex = 0,
|
||||
scrollOffset = 0,
|
||||
progress = 0f,
|
||||
filePath = file.path,
|
||||
lastOpened = null,
|
||||
category = Category.entries[0],
|
||||
coverImage = null
|
||||
),
|
||||
coverImage = extractCoverImageBitmap(file, coverImage)
|
||||
)
|
||||
}
|
||||
}
|
||||
book
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@ import org.w3c.dom.Element
|
|||
import ua.acclorite.book_story.R
|
||||
import ua.acclorite.book_story.data.parser.FileParser
|
||||
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.util.CoverImage
|
||||
import ua.acclorite.book_story.domain.util.UIText
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
|
|
@ -16,7 +16,7 @@ import javax.xml.parsers.DocumentBuilderFactory
|
|||
|
||||
class Fb2FileParser @Inject constructor() : FileParser {
|
||||
|
||||
override suspend fun parse(file: File): Pair<Book, CoverImage?>? {
|
||||
override suspend fun parse(file: File): BookWithCover? {
|
||||
return try {
|
||||
val factory = DocumentBuilderFactory.newInstance()
|
||||
val builder = factory.newDocumentBuilder()
|
||||
|
|
@ -50,19 +50,22 @@ class Fb2FileParser @Inject constructor() : FileParser {
|
|||
|
||||
val descriptionFromFile = extractElementContent(document, "annotation")
|
||||
|
||||
Book(
|
||||
title = title,
|
||||
author = author,
|
||||
description = descriptionFromFile,
|
||||
textPath = "",
|
||||
scrollIndex = 0,
|
||||
scrollOffset = 0,
|
||||
progress = 0f,
|
||||
filePath = file.path,
|
||||
lastOpened = null,
|
||||
category = Category.entries[0],
|
||||
BookWithCover(
|
||||
book = Book(
|
||||
title = title,
|
||||
author = author,
|
||||
description = descriptionFromFile,
|
||||
textPath = "",
|
||||
scrollIndex = 0,
|
||||
scrollOffset = 0,
|
||||
progress = 0f,
|
||||
filePath = file.path,
|
||||
lastOpened = null,
|
||||
category = Category.entries[0],
|
||||
coverImage = null
|
||||
),
|
||||
coverImage = null
|
||||
) to null
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
|
|
|
|||
|
|
@ -4,15 +4,15 @@ import org.jsoup.Jsoup
|
|||
import ua.acclorite.book_story.R
|
||||
import ua.acclorite.book_story.data.parser.FileParser
|
||||
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.util.CoverImage
|
||||
import ua.acclorite.book_story.domain.util.UIText
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
|
||||
class HtmFileParser @Inject constructor() : FileParser {
|
||||
|
||||
override suspend fun parse(file: File): Pair<Book, CoverImage?>? {
|
||||
override suspend fun parse(file: File): BookWithCover? {
|
||||
return try {
|
||||
val document = Jsoup.parse(file)
|
||||
|
||||
|
|
@ -22,19 +22,22 @@ class HtmFileParser @Inject constructor() : FileParser {
|
|||
}
|
||||
}
|
||||
|
||||
Book(
|
||||
title = title,
|
||||
author = UIText.StringResource(R.string.unknown_author),
|
||||
description = null,
|
||||
textPath = "",
|
||||
scrollIndex = 0,
|
||||
scrollOffset = 0,
|
||||
progress = 0f,
|
||||
filePath = file.path,
|
||||
lastOpened = null,
|
||||
category = Category.entries[0],
|
||||
BookWithCover(
|
||||
book = Book(
|
||||
title = title,
|
||||
author = UIText.StringResource(R.string.unknown_author),
|
||||
description = null,
|
||||
textPath = "",
|
||||
scrollIndex = 0,
|
||||
scrollOffset = 0,
|
||||
progress = 0f,
|
||||
filePath = file.path,
|
||||
lastOpened = null,
|
||||
category = Category.entries[0],
|
||||
coverImage = null
|
||||
),
|
||||
coverImage = null
|
||||
) to null
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
|
|
|
|||
|
|
@ -4,15 +4,15 @@ import org.jsoup.Jsoup
|
|||
import ua.acclorite.book_story.R
|
||||
import ua.acclorite.book_story.data.parser.FileParser
|
||||
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.util.CoverImage
|
||||
import ua.acclorite.book_story.domain.util.UIText
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
|
||||
class HtmlFileParser @Inject constructor() : FileParser {
|
||||
|
||||
override suspend fun parse(file: File): Pair<Book, CoverImage?>? {
|
||||
override suspend fun parse(file: File): BookWithCover? {
|
||||
return try {
|
||||
val document = Jsoup.parse(file)
|
||||
|
||||
|
|
@ -22,19 +22,22 @@ class HtmlFileParser @Inject constructor() : FileParser {
|
|||
}
|
||||
}
|
||||
|
||||
Book(
|
||||
title = title,
|
||||
author = UIText.StringResource(R.string.unknown_author),
|
||||
description = null,
|
||||
textPath = "",
|
||||
scrollIndex = 0,
|
||||
scrollOffset = 0,
|
||||
progress = 0f,
|
||||
filePath = file.path,
|
||||
lastOpened = null,
|
||||
category = Category.entries[0],
|
||||
BookWithCover(
|
||||
book = Book(
|
||||
title = title,
|
||||
author = UIText.StringResource(R.string.unknown_author),
|
||||
description = null,
|
||||
textPath = "",
|
||||
scrollIndex = 0,
|
||||
scrollOffset = 0,
|
||||
progress = 0f,
|
||||
filePath = file.path,
|
||||
lastOpened = null,
|
||||
category = Category.entries[0],
|
||||
coverImage = null
|
||||
),
|
||||
coverImage = null
|
||||
) to null
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
|
|
|
|||
|
|
@ -6,15 +6,15 @@ import com.tom_roush.pdfbox.pdmodel.PDDocument
|
|||
import ua.acclorite.book_story.R
|
||||
import ua.acclorite.book_story.data.parser.FileParser
|
||||
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.util.CoverImage
|
||||
import ua.acclorite.book_story.domain.util.UIText
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
|
||||
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 {
|
||||
PDFBoxResourceLoader.init(application)
|
||||
|
||||
|
|
@ -29,19 +29,22 @@ class PdfFileParser @Inject constructor(private val application: Application) :
|
|||
|
||||
document.close()
|
||||
|
||||
Book(
|
||||
title = title,
|
||||
author = author,
|
||||
description = description,
|
||||
textPath = "",
|
||||
scrollIndex = 0,
|
||||
scrollOffset = 0,
|
||||
progress = 0f,
|
||||
filePath = file.path,
|
||||
lastOpened = null,
|
||||
category = Category.entries[0],
|
||||
BookWithCover(
|
||||
book = Book(
|
||||
title = title,
|
||||
author = author,
|
||||
description = description,
|
||||
textPath = "",
|
||||
scrollIndex = 0,
|
||||
scrollOffset = 0,
|
||||
progress = 0f,
|
||||
filePath = file.path,
|
||||
lastOpened = null,
|
||||
category = Category.entries[0],
|
||||
coverImage = null
|
||||
),
|
||||
coverImage = null
|
||||
) to null
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
|
|
|
|||
|
|
@ -3,32 +3,35 @@ package ua.acclorite.book_story.data.parser.txt
|
|||
import ua.acclorite.book_story.R
|
||||
import ua.acclorite.book_story.data.parser.FileParser
|
||||
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.util.CoverImage
|
||||
import ua.acclorite.book_story.domain.util.UIText
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
|
||||
class TxtFileParser @Inject constructor() : FileParser {
|
||||
|
||||
override suspend fun parse(file: File): Pair<Book, CoverImage?>? {
|
||||
override suspend fun parse(file: File): BookWithCover? {
|
||||
return try {
|
||||
val title = file.nameWithoutExtension.trim()
|
||||
val author = UIText.StringResource(R.string.unknown_author)
|
||||
|
||||
Book(
|
||||
title = title,
|
||||
author = author,
|
||||
description = null,
|
||||
textPath = "",
|
||||
scrollIndex = 0,
|
||||
scrollOffset = 0,
|
||||
progress = 0f,
|
||||
filePath = file.path,
|
||||
lastOpened = null,
|
||||
category = Category.entries[0],
|
||||
BookWithCover(
|
||||
book = Book(
|
||||
title = title,
|
||||
author = author,
|
||||
description = null,
|
||||
textPath = "",
|
||||
scrollIndex = 0,
|
||||
scrollOffset = 0,
|
||||
progress = 0f,
|
||||
filePath = file.path,
|
||||
lastOpened = null,
|
||||
category = Category.entries[0],
|
||||
coverImage = null
|
||||
),
|
||||
coverImage = null
|
||||
) to null
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
|
|
|
|||
|
|
@ -9,13 +9,9 @@ import android.provider.MediaStore
|
|||
import android.util.Log
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import ua.acclorite.book_story.R
|
||||
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.dto.LatestReleaseInfo
|
||||
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.History
|
||||
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_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")
|
||||
@Singleton
|
||||
|
|
@ -67,13 +75,18 @@ class BookRepositoryImpl @Inject constructor(
|
|||
private val colorPresetMapper: ColorPresetMapper,
|
||||
|
||||
private val fileParser: FileParser,
|
||||
private val textParser: TextParser
|
||||
|
||||
private val textParser: TextParser,
|
||||
) : BookRepository {
|
||||
|
||||
/**
|
||||
* Get all books matching [query] from database.
|
||||
* Empty [query] equals to all books.
|
||||
*/
|
||||
override suspend fun getBooks(query: String): List<Book> {
|
||||
Log.i(GET_BOOKS, "Searching for books with query: \"$query\".")
|
||||
val books = database.searchBooks(query)
|
||||
|
||||
Log.i(GET_BOOKS, "Found ${books.size} books.")
|
||||
return books.map { entity ->
|
||||
val book = bookMapper.toBook(entity)
|
||||
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> {
|
||||
Log.i(GET_BOOKS_BY_ID, "Getting books with ids: $ids.")
|
||||
val books = database.findBooksById(ids)
|
||||
|
||||
return books.map { entity ->
|
||||
|
|
@ -128,26 +145,33 @@ class BookRepositoryImpl @Inject constructor(
|
|||
return lines
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts book in database.
|
||||
* Creates covers and books folders, which contain book's text and cover.
|
||||
*/
|
||||
override suspend fun insertBook(
|
||||
book: Book,
|
||||
coverImage: CoverImage?,
|
||||
text: List<String>
|
||||
bookWithTextAndCover: BookWithTextAndCover
|
||||
): Boolean {
|
||||
Log.i(INSERT_BOOK, "Inserting ${bookWithTextAndCover.book.title}.")
|
||||
|
||||
val filesDir = application.filesDir
|
||||
val coversDir = File(filesDir, "covers")
|
||||
val booksDir = File(filesDir, "books")
|
||||
|
||||
if (!coversDir.exists()) {
|
||||
Log.i(INSERT_BOOK, "Created covers folder.")
|
||||
coversDir.mkdirs()
|
||||
}
|
||||
if (!booksDir.exists()) {
|
||||
Log.i(INSERT_BOOK, "Created books folder.")
|
||||
booksDir.mkdirs()
|
||||
}
|
||||
|
||||
var coverUri = ""
|
||||
val textUri: String
|
||||
|
||||
if (text.isEmpty()) {
|
||||
if (bookWithTextAndCover.text.isEmpty()) {
|
||||
Log.e(INSERT_BOOK, "Text is empty.")
|
||||
return false
|
||||
}
|
||||
|
||||
|
|
@ -157,18 +181,19 @@ class BookRepositoryImpl @Inject constructor(
|
|||
|
||||
withContext(Dispatchers.IO) {
|
||||
FileOutputStream(textPath).use { stream ->
|
||||
text.forEach { line ->
|
||||
bookWithTextAndCover.text.forEach { line ->
|
||||
stream.write(line.toByteArray())
|
||||
stream.write(System.lineSeparator().toByteArray())
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(INSERT_BOOK, "Could not write text.")
|
||||
e.printStackTrace()
|
||||
return false
|
||||
}
|
||||
|
||||
if (coverImage != null) {
|
||||
if (bookWithTextAndCover.coverImage != null) {
|
||||
try {
|
||||
coverUri = "${UUID.randomUUID()}.webp"
|
||||
val cover = File(coversDir, coverUri)
|
||||
|
|
@ -176,23 +201,25 @@ class BookRepositoryImpl @Inject constructor(
|
|||
withContext(Dispatchers.IO) {
|
||||
FileOutputStream(cover).use { stream ->
|
||||
if (
|
||||
!coverImage.copy(Bitmap.Config.RGB_565, false).compress(
|
||||
Bitmap.CompressFormat.WEBP,
|
||||
20,
|
||||
stream
|
||||
)
|
||||
!bookWithTextAndCover.coverImage.copy(Bitmap.Config.RGB_565, false)
|
||||
.compress(
|
||||
Bitmap.CompressFormat.WEBP,
|
||||
20,
|
||||
stream
|
||||
)
|
||||
) {
|
||||
throw Exception("Couldn't save cover image")
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(INSERT_BOOK, "Could not save cover.")
|
||||
coverUri = ""
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
val updatedBook = book.copy(
|
||||
val updatedBook = bookWithTextAndCover.book.copy(
|
||||
textPath = "$booksDir/$textUri",
|
||||
coverImage = if (coverUri.isNotBlank()) {
|
||||
Uri.fromFile(File("$coversDir/$coverUri"))
|
||||
|
|
@ -201,37 +228,48 @@ class BookRepositoryImpl @Inject constructor(
|
|||
|
||||
val bookToInsert = bookMapper.toBookEntity(updatedBook)
|
||||
database.insertBooks(listOf(bookToInsert))
|
||||
Log.i(INSERT_BOOK, "Successfully inserted book.")
|
||||
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
|
||||
val entity = database.findBookById(book.id)
|
||||
database.updateBooks(
|
||||
books.map {
|
||||
val book = database.findBookById(it.id)
|
||||
listOf(
|
||||
bookMapper.toBookEntity(
|
||||
it.copy(
|
||||
textPath = book.textPath,
|
||||
coverImage = if (book.image != null) Uri.parse(book.image) else null
|
||||
book.copy(
|
||||
textPath = entity.textPath,
|
||||
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
|
||||
val filesDir = application.filesDir
|
||||
val booksDir = File(filesDir, "books")
|
||||
|
||||
if (!booksDir.exists()) {
|
||||
Log.i(UPDATE_BOOK, "Created books folder.")
|
||||
booksDir.mkdirs()
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
|
@ -241,33 +279,35 @@ class BookRepositoryImpl @Inject constructor(
|
|||
|
||||
withContext(Dispatchers.IO) {
|
||||
FileOutputStream(textPath).use { stream ->
|
||||
text.forEach { line ->
|
||||
bookWithText.text.forEach { line ->
|
||||
stream.write(line.toByteArray())
|
||||
stream.write(System.lineSeparator().toByteArray())
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(UPDATE_BOOK, "Could not update text.")
|
||||
e.printStackTrace()
|
||||
return false
|
||||
}
|
||||
|
||||
if (book.textPath.isNotBlank()) {
|
||||
if (bookWithText.book.textPath.isNotBlank()) {
|
||||
try {
|
||||
val fileToDelete = File(
|
||||
book.textPath
|
||||
bookWithText.book.textPath
|
||||
)
|
||||
|
||||
if (fileToDelete.exists()) {
|
||||
fileToDelete.delete()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(UPDATE_BOOK, "Failed to delete old text.")
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
val updatedBook = bookMapper.toBookEntity(
|
||||
book.copy(
|
||||
bookWithText.book.copy(
|
||||
textPath = "$booksDir/$textUri",
|
||||
coverImage = if (bookEntity.image != null) Uri.parse(bookEntity.image) else null
|
||||
)
|
||||
|
|
@ -276,13 +316,19 @@ class BookRepositoryImpl @Inject constructor(
|
|||
database.updateBooks(
|
||||
listOf(updatedBook)
|
||||
)
|
||||
Log.i(UPDATE_BOOK, "Successfully updated book.")
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Update cover image of the book. Deletes old cover and replaces with new.
|
||||
*/
|
||||
override suspend fun updateCoverImageOfBook(
|
||||
bookWithOldCover: Book,
|
||||
newCoverImage: CoverImage?
|
||||
) {
|
||||
Log.i(UPDATE_BOOK, "Updating cover image: ${bookWithOldCover.title}.")
|
||||
|
||||
// without text
|
||||
val book = database.findBookById(bookWithOldCover.id)
|
||||
var uri: String? = null
|
||||
|
|
@ -291,6 +337,7 @@ class BookRepositoryImpl @Inject constructor(
|
|||
val coversDir = File(filesDir, "covers")
|
||||
|
||||
if (!coversDir.exists()) {
|
||||
Log.i(UPDATE_BOOK, "Created covers folder.")
|
||||
coversDir.mkdirs()
|
||||
}
|
||||
|
||||
|
|
@ -313,6 +360,7 @@ class BookRepositoryImpl @Inject constructor(
|
|||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(UPDATE_BOOK, "Could not save new cover.")
|
||||
e.printStackTrace()
|
||||
return
|
||||
}
|
||||
|
|
@ -328,6 +376,7 @@ class BookRepositoryImpl @Inject constructor(
|
|||
fileToDelete.delete()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(UPDATE_BOOK, "Could not delete old cover.")
|
||||
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>) {
|
||||
Log.i(DELETE_BOOKS, "Deleting books.")
|
||||
|
||||
val filesDir = application.filesDir
|
||||
val coversDir = File(filesDir, "covers")
|
||||
val booksDir = File(filesDir, "books")
|
||||
|
||||
if (!coversDir.exists()) {
|
||||
Log.i(DELETE_BOOKS, "Created covers folder.")
|
||||
coversDir.mkdirs()
|
||||
}
|
||||
if (!booksDir.exists()) {
|
||||
Log.i(DELETE_BOOKS, "Created books folder.")
|
||||
booksDir.mkdirs()
|
||||
}
|
||||
|
||||
|
|
@ -378,6 +436,7 @@ class BookRepositoryImpl @Inject constructor(
|
|||
fileToDelete.delete()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(DELETE_BOOKS, "Could not delete cover image.")
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
|
@ -390,6 +449,7 @@ class BookRepositoryImpl @Inject constructor(
|
|||
fileToDelete.delete()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(DELETE_BOOKS, "Could not delete text.")
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
|
@ -397,15 +457,21 @@ class BookRepositoryImpl @Inject constructor(
|
|||
book
|
||||
}
|
||||
)
|
||||
|
||||
Log.i(DELETE_BOOKS, "Successfully deleted books.")
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Whether can reset cover image (restore default).
|
||||
*/
|
||||
override suspend fun canResetCover(bookId: Int): Boolean {
|
||||
val book = database.findBookById(bookId)
|
||||
|
||||
val defaultCoverUncompressed = fileParser.parse(File(book.filePath))?.second
|
||||
val defaultCoverUncompressed = fileParser.parse(File(book.filePath))?.coverImage
|
||||
?: return false
|
||||
|
||||
if (book.image == null) {
|
||||
Log.i(CAN_RESET_COVER, "Can reset cover image. (current is null)")
|
||||
return true
|
||||
}
|
||||
|
||||
|
|
@ -424,6 +490,7 @@ class BookRepositoryImpl @Inject constructor(
|
|||
Uri.parse(book.image)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.i(CAN_RESET_COVER, "Can reset cover image. (could not get current)")
|
||||
e.printStackTrace()
|
||||
return true
|
||||
}
|
||||
|
|
@ -431,30 +498,45 @@ class BookRepositoryImpl @Inject constructor(
|
|||
return !defaultCover.sameAs(currentCover)
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset cover image to default.
|
||||
* If there is no default cover, returns false.
|
||||
*/
|
||||
override suspend fun resetCoverImage(bookId: Int): Boolean {
|
||||
if (!canResetCover(bookId)) {
|
||||
Log.w(RESET_COVER, "Cannot reset cover image.")
|
||||
return false
|
||||
}
|
||||
|
||||
val book = database.findBookById(bookId)
|
||||
val defaultCover = fileParser.parse(File(book.filePath))?.second
|
||||
val defaultCover = fileParser.parse(File(book.filePath))?.coverImage
|
||||
?: return false
|
||||
updateCoverImageOfBook(bookMapper.toBook(book), defaultCover)
|
||||
|
||||
Log.i(RESET_COVER, "Successfully reset cover image.")
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Puts DataStore constant to [DataStore].
|
||||
*/
|
||||
override suspend fun <T> putDataToDataStore(key: Preferences.Key<T>, value: T) {
|
||||
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>()
|
||||
|
||||
scope.launch {
|
||||
withContext(Dispatchers.Default) {
|
||||
val keys = dataStore.getAllData()
|
||||
val data = mutableMapOf<String, Any>()
|
||||
|
||||
Log.i(GET_ALL_SETTINGS, "Got ${keys?.size} settings keys.")
|
||||
|
||||
val jobs = keys?.map { key ->
|
||||
async {
|
||||
val nullableData = dataStore.getNullableData(key)
|
||||
|
|
@ -474,7 +556,13 @@ class BookRepositoryImpl @Inject constructor(
|
|||
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> {
|
||||
Log.i(GET_FILES_FROM_DEVICE, "Getting files from device by query: \"$query\".")
|
||||
|
||||
val existingBooks = database
|
||||
.searchBooks("")
|
||||
.map { bookMapper.toBook(it) }
|
||||
|
|
@ -564,9 +652,11 @@ class BookRepositoryImpl @Inject constructor(
|
|||
(Environment.getExternalStorageState() != Environment.MEDIA_MOUNTED &&
|
||||
Environment.getExternalStorageState() != Environment.MEDIA_MOUNTED_READ_ONLY)
|
||||
) {
|
||||
Log.e(GET_FILES_FROM_DEVICE, "Could not correctly get root directory.")
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
Log.i(GET_FILES_FROM_DEVICE, "Successfully got all matching files.")
|
||||
return rootDirectory.getAllFiles()
|
||||
}
|
||||
|
||||
|
|
@ -576,74 +666,106 @@ class BookRepositoryImpl @Inject constructor(
|
|||
override suspend fun getBookFromFile(file: File): NullableBook {
|
||||
val parsedBook = fileParser.parse(file)
|
||||
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(
|
||||
file.name,
|
||||
UIText.StringResource(R.string.error_wrong_file_format)
|
||||
UIText.StringResource(R.string.error_something_went_wrong)
|
||||
)
|
||||
}
|
||||
|
||||
val parsedText = textParser.parse(file)
|
||||
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(
|
||||
file.name,
|
||||
parsedText.message
|
||||
)
|
||||
}
|
||||
|
||||
Log.i(GET_BOOK_FROM_FILE, "Successfully got book from file.")
|
||||
return NullableBook.NotNull(
|
||||
book = parsedBook.first.copy(
|
||||
chapters = parsedText.data!!.map { it.chapter }.run {
|
||||
if (this.size == 1) return@run emptyList()
|
||||
this
|
||||
}
|
||||
),
|
||||
coverImage = parsedBook.second,
|
||||
text = parsedText.data.map {
|
||||
it.text
|
||||
}.flatten()
|
||||
bookWithTextAndCover = BookWithTextAndCover(
|
||||
book = parsedBook.book.copy(
|
||||
chapters = parsedText.data!!.map { it.chapter }.run {
|
||||
if (this.size == 1) return@run emptyList()
|
||||
this
|
||||
}
|
||||
),
|
||||
coverImage = parsedBook.coverImage,
|
||||
text = parsedText.data.map {
|
||||
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 {
|
||||
val history = database.getHistory()
|
||||
|
||||
emit(
|
||||
Resource.Success(
|
||||
data = history.map {
|
||||
historyMapper.toHistory(
|
||||
it
|
||||
)
|
||||
}
|
||||
)
|
||||
/**
|
||||
* Get all history from database.
|
||||
*/
|
||||
override suspend fun getHistory(): List<History> {
|
||||
return database.getHistory().map {
|
||||
historyMapper.toHistory(
|
||||
it
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get latest history of the matching [bookId].
|
||||
*/
|
||||
override suspend fun getLatestBookHistory(bookId: Int): History? {
|
||||
val history = database.getLatestHistoryForBook(bookId)
|
||||
return history?.let { historyMapper.toHistory(it) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete whole history.
|
||||
*/
|
||||
override suspend fun deleteWholeHistory() {
|
||||
database.deleteWholeHistory()
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all history of the matching [bookId].
|
||||
*/
|
||||
override suspend fun deleteBookHistory(bookId: Int) {
|
||||
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? {
|
||||
Log.i(CHECK_FOR_UPDATES, "Checking for updates. Post notification: $postNotification")
|
||||
|
||||
return withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val result = githubAPI.getLatestRelease()
|
||||
|
|
@ -652,6 +774,7 @@ class BookRepositoryImpl @Inject constructor(
|
|||
val currentVersion = application.getString(R.string.app_version)
|
||||
|
||||
if (version != currentVersion && postNotification) {
|
||||
Log.i(CHECK_FOR_UPDATES, "Posting notification.")
|
||||
updatesNotificationService.postNotification(
|
||||
result
|
||||
)
|
||||
|
|
@ -659,12 +782,16 @@ class BookRepositoryImpl @Inject constructor(
|
|||
|
||||
result
|
||||
} catch (e: Exception) {
|
||||
Log.e(CHECK_FOR_UPDATES, "Could not get latest release information.")
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update color preset.
|
||||
*/
|
||||
override suspend fun updateColorPreset(colorPreset: ColorPreset) {
|
||||
database.updateColorPreset(
|
||||
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) {
|
||||
database.getColorPresets().map {
|
||||
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> {
|
||||
return database.getColorPresets()
|
||||
.sortedBy { it.order }
|
||||
.map { colorPresetMapper.toColorPreset(it) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Reorder color presets.
|
||||
* Changes the order of the color presets.
|
||||
*/
|
||||
override suspend fun reorderColorPresets(orderedColorPresets: List<ColorPreset>) {
|
||||
database.deleteColorPresets()
|
||||
|
||||
|
|
@ -701,6 +839,9 @@ class BookRepositoryImpl @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete color preset.
|
||||
*/
|
||||
override suspend fun deleteColorPreset(colorPreset: ColorPreset) {
|
||||
database.deleteColorPreset(
|
||||
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) {
|
||||
if (database.favoriteDirectoryExits(path)) {
|
||||
database.deleteFavoriteDirectory(
|
||||
|
|
@ -722,18 +868,3 @@ class BookRepositoryImpl @Inject constructor(
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package ua.acclorite.book_story.domain.model
|
|||
|
||||
import android.net.Uri
|
||||
import androidx.compose.runtime.Immutable
|
||||
import ua.acclorite.book_story.domain.util.CoverImage
|
||||
import ua.acclorite.book_story.domain.util.UIText
|
||||
|
||||
@Immutable
|
||||
|
|
@ -24,3 +25,46 @@ data class Book(
|
|||
val lastOpened: Long?,
|
||||
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
|
||||
)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -1,21 +1,21 @@
|
|||
package ua.acclorite.book_story.domain.repository
|
||||
|
||||
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.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.History
|
||||
import ua.acclorite.book_story.domain.model.NullableBook
|
||||
import ua.acclorite.book_story.domain.model.SelectableFile
|
||||
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 java.io.File
|
||||
|
||||
interface BookRepository {
|
||||
|
||||
/* ------ Books ------------------------------ */
|
||||
suspend fun getBooks(
|
||||
query: String
|
||||
): List<Book>
|
||||
|
|
@ -29,18 +29,15 @@ interface BookRepository {
|
|||
): List<String>
|
||||
|
||||
suspend fun insertBook(
|
||||
book: Book,
|
||||
coverImage: CoverImage?,
|
||||
text: List<String>
|
||||
bookWithTextAndCover: BookWithTextAndCover
|
||||
): Boolean
|
||||
|
||||
suspend fun updateBooks(
|
||||
books: List<Book>
|
||||
suspend fun updateBook(
|
||||
book: Book
|
||||
)
|
||||
|
||||
suspend fun updateBookWithText(
|
||||
book: Book,
|
||||
text: List<String>
|
||||
bookWithText: BookWithText
|
||||
): Boolean
|
||||
|
||||
suspend fun updateCoverImageOfBook(
|
||||
|
|
@ -56,49 +53,85 @@ interface BookRepository {
|
|||
bookId: Int
|
||||
): Boolean
|
||||
|
||||
suspend fun resetCoverImage(bookId: Int): Boolean
|
||||
suspend fun resetCoverImage(
|
||||
bookId: Int
|
||||
): Boolean
|
||||
/* - - - - - - - - - - - - - - - - - - - - - - */
|
||||
|
||||
|
||||
/* ------ DataStore -------------------------- */
|
||||
suspend fun <T> putDataToDataStore(
|
||||
key: Preferences.Key<T>,
|
||||
value: T
|
||||
)
|
||||
|
||||
|
||||
suspend fun getAllSettings(scope: CoroutineScope): MainState
|
||||
|
||||
suspend fun getFilesFromDevice(query: String = ""): List<SelectableFile>
|
||||
|
||||
suspend fun getBookFromFile(file: File): NullableBook
|
||||
suspend fun getAllSettings(): MainState
|
||||
/* - - - - - - - - - - - - - - - - - - - - - - */
|
||||
|
||||
|
||||
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 deleteBookHistory(bookId: Int)
|
||||
|
||||
suspend fun deleteHistory(
|
||||
history: List<History>
|
||||
suspend fun deleteBookHistory(
|
||||
bookId: Int
|
||||
)
|
||||
|
||||
|
||||
suspend fun checkForUpdates(postNotification: Boolean): LatestReleaseInfo?
|
||||
suspend fun deleteHistory(
|
||||
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 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)
|
||||
/* - - - - - - - - - - - - - - - - - - - - - - */
|
||||
}
|
||||
|
|
@ -6,7 +6,7 @@ import javax.inject.Inject
|
|||
|
||||
class DeleteHistory @Inject constructor(private val repository: BookRepository) {
|
||||
|
||||
suspend fun execute(history: List<History>) {
|
||||
suspend fun execute(history: History) {
|
||||
repository.deleteHistory(history)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
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.presentation.data.MainState
|
||||
import javax.inject.Inject
|
||||
|
|
@ -9,7 +8,7 @@ class GetAllSettings @Inject constructor(
|
|||
private val repository: BookRepository
|
||||
) {
|
||||
|
||||
suspend fun execute(scope: CoroutineScope): MainState {
|
||||
return repository.getAllSettings(scope)
|
||||
suspend fun execute(): MainState {
|
||||
return repository.getAllSettings()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +1,12 @@
|
|||
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.repository.BookRepository
|
||||
import ua.acclorite.book_story.domain.util.Resource
|
||||
import javax.inject.Inject
|
||||
|
||||
class GetHistory @Inject constructor(private val repository: BookRepository) {
|
||||
|
||||
suspend fun execute(): Flow<Resource<List<History>>> {
|
||||
suspend fun execute(): List<History> {
|
||||
return repository.getHistory()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +1,13 @@
|
|||
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.util.CoverImage
|
||||
import javax.inject.Inject
|
||||
|
||||
class InsertBook @Inject constructor(private val repository: BookRepository) {
|
||||
suspend fun execute(
|
||||
book: Book,
|
||||
coverImage: CoverImage?,
|
||||
text: List<String>
|
||||
bookWithTextAndCover: BookWithTextAndCover
|
||||
): Boolean {
|
||||
return repository.insertBook(book, coverImage, text)
|
||||
return repository.insertBook(bookWithTextAndCover)
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,7 @@ import ua.acclorite.book_story.domain.repository.BookRepository
|
|||
import javax.inject.Inject
|
||||
|
||||
class InsertHistory @Inject constructor(private val repository: BookRepository) {
|
||||
suspend fun execute(history: List<History>) {
|
||||
suspend fun execute(history: History) {
|
||||
repository.insertHistory(history)
|
||||
}
|
||||
}
|
||||
|
|
@ -7,6 +7,6 @@ import javax.inject.Inject
|
|||
class UpdateBook @Inject constructor(private val repository: BookRepository) {
|
||||
|
||||
suspend fun execute(book: Book) {
|
||||
repository.updateBooks(listOf(book))
|
||||
repository.updateBook(book)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
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 javax.inject.Inject
|
||||
|
||||
class UpdateBookWithText @Inject constructor(private val repository: BookRepository) {
|
||||
|
||||
suspend fun execute(book: Book, text: List<String>): Boolean {
|
||||
return repository.updateBookWithText(book, text)
|
||||
suspend fun execute(bookWithText: BookWithText): Boolean {
|
||||
return repository.updateBookWithText(bookWithText)
|
||||
}
|
||||
}
|
||||
|
|
@ -468,7 +468,7 @@ class MainViewModel @Inject constructor(
|
|||
settingsViewModel: SettingsViewModel,
|
||||
) {
|
||||
viewModelScope.launch(Dispatchers.Main) {
|
||||
val settings = getAllSettings.execute(viewModelScope)
|
||||
val settings = getAllSettings.execute()
|
||||
|
||||
// All additional execution
|
||||
changeLanguage.execute(settings.language)
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import kotlinx.coroutines.flow.update
|
|||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.yield
|
||||
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.History
|
||||
import ua.acclorite.book_story.domain.model.NullableBook
|
||||
|
|
@ -531,7 +532,7 @@ class BookInfoViewModel @Inject constructor(
|
|||
|
||||
var textUpdated = false
|
||||
|
||||
val updatedText = updatedBook.text
|
||||
val updatedText = updatedBook.bookWithTextAndCover!!.text
|
||||
val text = getText.execute(book.textPath)
|
||||
|
||||
if (updatedText != text) {
|
||||
|
|
@ -560,7 +561,7 @@ class BookInfoViewModel @Inject constructor(
|
|||
yield()
|
||||
onEvent(
|
||||
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 isSuccess = updateBookWithText.execute(
|
||||
book = _state.value.book,
|
||||
text = updatedText
|
||||
BookWithText(
|
||||
book = _state.value.book,
|
||||
text = updatedText
|
||||
)
|
||||
)
|
||||
|
||||
if (!isSuccess) {
|
||||
|
|
@ -732,12 +735,10 @@ class BookInfoViewModel @Inject constructor(
|
|||
onEvent(BookInfoEvent.OnCancelUpdate)
|
||||
_state.value.book.id.let {
|
||||
insertHistory.execute(
|
||||
listOf(
|
||||
History(
|
||||
bookId = it,
|
||||
book = null,
|
||||
time = Date().time
|
||||
)
|
||||
History(
|
||||
bookId = it,
|
||||
book = null,
|
||||
time = Date().time
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,14 +46,14 @@ fun BrowseAddingDialogItem(result: Pair<NullableBook, Selected>, onClick: (Boole
|
|||
Modifier.weight(1f)
|
||||
) {
|
||||
Text(
|
||||
text = result.first.book!!.title,
|
||||
text = result.first.bookWithTextAndCover!!.book.title,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Text(
|
||||
text = result.first.book!!.author.asString(),
|
||||
text = result.first.bookWithTextAndCover!!.book.author.asString(),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
maxLines = 1,
|
||||
|
|
|
|||
|
|
@ -467,12 +467,11 @@ class BrowseViewModel @Inject constructor(
|
|||
return@launch
|
||||
}
|
||||
|
||||
val failed = booksToInsert.any {
|
||||
!insertBook.execute(
|
||||
it.book!!,
|
||||
it.coverImage,
|
||||
it.text
|
||||
)
|
||||
var failed = false
|
||||
booksToInsert.forEach {
|
||||
if (!insertBook.execute(it.bookWithTextAndCover!!)) {
|
||||
failed = true
|
||||
}
|
||||
}
|
||||
|
||||
if (failed) {
|
||||
|
|
|
|||
|
|
@ -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.GetHistory
|
||||
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.util.BaseViewModel
|
||||
import java.text.SimpleDateFormat
|
||||
|
|
@ -119,9 +118,7 @@ class HistoryViewModel @Inject constructor(
|
|||
|
||||
is HistoryEvent.OnDeleteHistoryElement -> {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
deleteHistory.execute(
|
||||
listOf(event.historyToDelete)
|
||||
)
|
||||
deleteHistory.execute(event.historyToDelete)
|
||||
|
||||
_state.update {
|
||||
it.copy(
|
||||
|
|
@ -154,9 +151,7 @@ class HistoryViewModel @Inject constructor(
|
|||
when (snackbar) {
|
||||
SnackbarResult.Dismissed -> Unit
|
||||
SnackbarResult.ActionPerformed -> {
|
||||
insertHistory.execute(
|
||||
listOf(event.historyToDelete)
|
||||
)
|
||||
insertHistory.execute(event.historyToDelete)
|
||||
event.refreshList()
|
||||
_state.update {
|
||||
it.copy(
|
||||
|
|
@ -256,12 +251,10 @@ class HistoryViewModel @Inject constructor(
|
|||
viewModelScope.launch {
|
||||
event.book.id.let {
|
||||
insertHistory.execute(
|
||||
listOf(
|
||||
History(
|
||||
bookId = it,
|
||||
book = null,
|
||||
time = Date().time
|
||||
)
|
||||
History(
|
||||
bookId = it,
|
||||
book = null,
|
||||
time = Date().time
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
@ -300,91 +293,82 @@ class HistoryViewModel @Inject constructor(
|
|||
return maxElementsById.filterNotNull()
|
||||
}
|
||||
|
||||
getHistory.execute().collect { result ->
|
||||
when (result) {
|
||||
is Resource.Success -> {
|
||||
val historyWithoutBook =
|
||||
result.data?.sortedByDescending { it.time } ?: emptyList()
|
||||
val historyWithoutBooks = getHistory.execute()
|
||||
.sortedByDescending { it.time }
|
||||
|
||||
if (historyWithoutBook.isEmpty()) {
|
||||
_state.update {
|
||||
it.copy(
|
||||
history = emptyList(),
|
||||
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
|
||||
if (historyWithoutBooks.isEmpty()) {
|
||||
_state.update {
|
||||
it.copy(
|
||||
history = emptyList(),
|
||||
isLoading = false
|
||||
)
|
||||
}
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -328,12 +328,10 @@ class LibraryViewModel @Inject constructor(
|
|||
viewModelScope.launch {
|
||||
event.book.id.let {
|
||||
insertHistory.execute(
|
||||
listOf(
|
||||
History(
|
||||
bookId = it,
|
||||
book = null,
|
||||
time = Date().time
|
||||
)
|
||||
History(
|
||||
bookId = it,
|
||||
book = null,
|
||||
time = Date().time
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue