Changed Reader text system. Now book doesn't hold any text, only ReaderViewModel does. (v1.0.1)

This commit is contained in:
acclorite 2024-04-28 19:52:22 +03:00
parent e0698d2c2f
commit 456d9fc3c9
25 changed files with 257 additions and 279 deletions

View file

@ -25,6 +25,9 @@ android {
} }
buildTypes { buildTypes {
getByName("debug") {
applicationIdSuffix = ".debug"
}
getByName("release") { getByName("release") {
isMinifyEnabled = true isMinifyEnabled = true
isShrinkResources = true isShrinkResources = true

View file

@ -5,7 +5,6 @@ import ua.acclorite.book_story.R
import ua.acclorite.book_story.data.local.dto.BookEntity import ua.acclorite.book_story.data.local.dto.BookEntity
import ua.acclorite.book_story.domain.model.Book import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.domain.util.UIText import ua.acclorite.book_story.domain.util.UIText
import java.io.File
import javax.inject.Inject import javax.inject.Inject
class BookMapperImpl @Inject constructor() : BookMapper { class BookMapperImpl @Inject constructor() : BookMapper {
@ -26,8 +25,6 @@ class BookMapperImpl @Inject constructor() : BookMapper {
} }
override suspend fun toBook(bookEntity: BookEntity): Book { override suspend fun toBook(bookEntity: BookEntity): Book {
val file = File(bookEntity.filePath)
return Book( return Book(
id = bookEntity.id, id = bookEntity.id,
title = bookEntity.title, title = bookEntity.title,
@ -38,11 +35,7 @@ class BookMapperImpl @Inject constructor() : BookMapper {
scrollIndex = bookEntity.scrollIndex, scrollIndex = bookEntity.scrollIndex,
scrollOffset = bookEntity.scrollOffset, scrollOffset = bookEntity.scrollOffset,
progress = bookEntity.progress, progress = bookEntity.progress,
file = if (file.exists()) file else null,
textPath = bookEntity.textPath, textPath = bookEntity.textPath,
text = emptyList(),
letters = 0,
words = 0,
filePath = bookEntity.filePath, filePath = bookEntity.filePath,
lastOpened = null, lastOpened = null,
category = bookEntity.category, category = bookEntity.category,

View file

@ -16,7 +16,7 @@ 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): Pair<Book, CoverImage?>? {
if (!file.name.endsWith(".epub")) { if (!file.name.endsWith(".epub") || !file.exists()) {
return null return null
} }
@ -49,13 +49,9 @@ class EpubFileParser @Inject constructor() : FileParser {
author = author, author = author,
description = description?.toString(), description = description?.toString(),
textPath = "", textPath = "",
text = emptyList(),
letters = 0,
words = 0,
scrollIndex = 0, scrollIndex = 0,
scrollOffset = 0, scrollOffset = 0,
progress = 0f, progress = 0f,
file = file,
filePath = file.path, filePath = file.path,
lastOpened = null, lastOpened = null,
category = Category.entries[0], category = Category.entries[0],

View file

@ -15,7 +15,7 @@ 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): Pair<Book, CoverImage?>? {
if (!file.name.endsWith(".pdf")) { if (!file.name.endsWith(".pdf") || !file.exists()) {
return null return null
} }
@ -37,13 +37,9 @@ class PdfFileParser @Inject constructor(private val application: Application) :
author = author, author = author,
description = description, description = description,
textPath = "", textPath = "",
text = emptyList(),
letters = 0,
words = 0,
scrollIndex = 0, scrollIndex = 0,
scrollOffset = 0, scrollOffset = 0,
progress = 0f, progress = 0f,
file = file,
filePath = file.path, filePath = file.path,
lastOpened = null, lastOpened = null,
category = Category.entries[0], category = Category.entries[0],

View file

@ -12,7 +12,7 @@ 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): Pair<Book, CoverImage?>? {
if (!file.name.endsWith(".txt")) { if (!file.name.endsWith(".txt") || !file.exists()) {
return null return null
} }
@ -25,13 +25,9 @@ class TxtFileParser @Inject constructor() : FileParser {
author = author, author = author,
description = null, description = null,
textPath = "", textPath = "",
text = emptyList(),
letters = 0,
words = 0,
scrollIndex = 0, scrollIndex = 0,
scrollOffset = 0, scrollOffset = 0,
progress = 0f, progress = 0f,
file = file,
filePath = file.path, filePath = file.path,
lastOpened = null, lastOpened = null,
category = Category.entries[0], category = Category.entries[0],

View file

@ -88,7 +88,7 @@ class BookRepositoryImpl @Inject constructor(
} }
} }
override suspend fun getBookTextById(textPath: String): List<StringWithId> { override suspend fun getBookText(textPath: String): List<StringWithId> {
val textFile = File(textPath) val textFile = File(textPath)
if (textPath.isBlank() || !textFile.exists()) { if (textPath.isBlank() || !textFile.exists()) {
@ -105,8 +105,10 @@ class BookRepositoryImpl @Inject constructor(
return text.data return text.data
} }
override suspend fun insertBooks( override suspend fun insertBook(
books: List<Pair<Book, CoverImage?>> book: Book,
coverImage: CoverImage?,
text: List<StringWithId>
): Boolean { ): Boolean {
val filesDir = application.filesDir val filesDir = application.filesDir
val coversDir = File(filesDir, "covers") val coversDir = File(filesDir, "covers")
@ -119,21 +121,20 @@ class BookRepositoryImpl @Inject constructor(
booksDir.mkdirs() booksDir.mkdirs()
} }
val booksWithCoverAndText = books.map {
var coverUri = "" var coverUri = ""
val textUri: String val textUri: String
if (it.first.text.isEmpty()) { if (text.isEmpty()) {
return false return false
} }
try { try {
textUri = "${UUID.randomUUID()}.txt" textUri = "${UUID.randomUUID()}.txt"
val text = File(booksDir, textUri) val textPath = File(booksDir, textUri)
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
FileOutputStream(text).use { stream -> FileOutputStream(textPath).use { stream ->
it.first.text.forEach { line -> text.forEach { line ->
stream.write(line.line.toByteArray()) stream.write(line.line.toByteArray())
stream.write(System.lineSeparator().toByteArray()) stream.write(System.lineSeparator().toByteArray())
} }
@ -144,7 +145,7 @@ class BookRepositoryImpl @Inject constructor(
return false return false
} }
if (it.second != null) { if (coverImage != null) {
try { try {
coverUri = "${UUID.randomUUID()}.webp" coverUri = "${UUID.randomUUID()}.webp"
val cover = File(coversDir, coverUri) val cover = File(coversDir, coverUri)
@ -152,7 +153,7 @@ class BookRepositoryImpl @Inject constructor(
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
FileOutputStream(cover).use { stream -> FileOutputStream(cover).use { stream ->
if ( if (
!it.second!!.copy(Bitmap.Config.RGB_565, false).compress( !coverImage.copy(Bitmap.Config.RGB_565, false).compress(
Bitmap.CompressFormat.WEBP, Bitmap.CompressFormat.WEBP,
20, 20,
stream stream
@ -169,17 +170,15 @@ class BookRepositoryImpl @Inject constructor(
} }
val book = it.first.copy( val updatedBook = 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"))
} else null } else null
) )
bookMapper.toBookEntity(book) val bookToInsert = bookMapper.toBookEntity(updatedBook)
} database.insertBooks(listOf(bookToInsert))
database.insertBooks(booksWithCoverAndText)
return true return true
} }
@ -198,7 +197,7 @@ class BookRepositoryImpl @Inject constructor(
) )
} }
override suspend fun updateBooksWithText(books: List<Book>): Boolean { override suspend fun updateBookWithText(book: Book, text: List<StringWithId>): Boolean {
// without cover image // without cover image
val filesDir = application.filesDir val filesDir = application.filesDir
val booksDir = File(filesDir, "books") val booksDir = File(filesDir, "books")
@ -207,21 +206,20 @@ class BookRepositoryImpl @Inject constructor(
booksDir.mkdirs() booksDir.mkdirs()
} }
val booksWithText = books.map {
val textUri: String val textUri: String
val bookEntity = database.findBookById(it.id) val bookEntity = database.findBookById(book.id)
if (it.text.isEmpty()) { if (text.isEmpty()) {
return false return false
} }
try { try {
textUri = "${UUID.randomUUID()}.txt" textUri = "${UUID.randomUUID()}.txt"
val text = File(booksDir, textUri) val textPath = File(booksDir, textUri)
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
FileOutputStream(text).use { stream -> FileOutputStream(textPath).use { stream ->
it.text.forEach { line -> text.forEach { line ->
stream.write(line.line.toByteArray()) stream.write(line.line.toByteArray())
stream.write(System.lineSeparator().toByteArray()) stream.write(System.lineSeparator().toByteArray())
} }
@ -232,10 +230,10 @@ class BookRepositoryImpl @Inject constructor(
return false return false
} }
if (it.textPath.isNotBlank()) { if (book.textPath.isNotBlank()) {
try { try {
val fileToDelete = File( val fileToDelete = File(
it.textPath book.textPath
) )
if (fileToDelete.exists()) { if (fileToDelete.exists()) {
@ -246,16 +244,15 @@ class BookRepositoryImpl @Inject constructor(
} }
} }
bookMapper.toBookEntity( val updatedBook = bookMapper.toBookEntity(
it.copy( 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
) )
) )
}
database.updateBooks( database.updateBooks(
booksWithText listOf(updatedBook)
) )
return true return true
} }
@ -560,10 +557,9 @@ class BookRepositoryImpl @Inject constructor(
books.add( books.add(
NullableBook.NotNull( NullableBook.NotNull(
Pair( book = parsedBook.first,
parsedBook.first.copy(text = parsedText.data), coverImage = parsedBook.second,
parsedBook.second text = parsedText.data
)
) )
) )
} }
@ -613,7 +609,7 @@ class BookRepositoryImpl @Inject constructor(
try { try {
val result = githubAPI.getLatestRelease() val result = githubAPI.getLatestRelease()
val version = result.tagName.substringAfterLast("v") val version = result.tagName.substringAfter("v")
val currentVersion = application.getString(R.string.app_version) val currentVersion = application.getString(R.string.app_version)
if (version != currentVersion && postNotification) { if (version != currentVersion && postNotification) {

View file

@ -3,7 +3,6 @@ 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.UIText import ua.acclorite.book_story.domain.util.UIText
import java.io.File
@Immutable @Immutable
data class Book( data class Book(
@ -14,17 +13,13 @@ data class Book(
val description: String?, val description: String?,
val textPath: String, val textPath: String,
val text: List<StringWithId> = emptyList(), val filePath: String,
val letters: Int, val coverImage: Uri?,
val words: Int,
val scrollIndex: Int, val scrollIndex: Int,
val scrollOffset: Int, val scrollOffset: Int,
val progress: Float, val progress: Float,
val file: File?,
val filePath: String,
val lastOpened: Long?, val lastOpened: Long?,
val category: Category, val category: Category,
val coverImage: Uri?
) )

View file

@ -6,13 +6,26 @@ import ua.acclorite.book_story.domain.util.UIText
@Immutable @Immutable
sealed class NullableBook( sealed class NullableBook(
val book: Pair<Book, CoverImage?>?, val book: Book?,
val coverImage: CoverImage? = null,
val text: List<StringWithId> = emptyList(),
val fileName: String?, val fileName: String?,
val message: UIText? val message: UIText?
) { ) {
class NotNull(book: Pair<Book, CoverImage?>) : NullableBook(book, null, null) class NotNull(
book: Book,
coverImage: CoverImage?,
text: List<StringWithId>
) : NullableBook(
book = book,
text = text,
coverImage = coverImage,
fileName = null,
message = null
)
class Null( class Null(
fileName: String, fileName: String,
message: UIText? message: UIText?
) : NullableBook(null, fileName, message) ) : NullableBook(null, text = emptyList(), fileName = fileName, message = message)
} }

View file

@ -23,20 +23,23 @@ interface BookRepository {
ids: List<Int> ids: List<Int>
): List<Book> ): List<Book>
suspend fun getBookTextById( suspend fun getBookText(
textPath: String textPath: String
): List<StringWithId> ): List<StringWithId>
suspend fun insertBooks( suspend fun insertBook(
books: List<Pair<Book, CoverImage?>> book: Book,
coverImage: CoverImage?,
text: List<StringWithId>
): Boolean ): Boolean
suspend fun updateBooks( suspend fun updateBooks(
books: List<Book> books: List<Book>
) )
suspend fun updateBooksWithText( suspend fun updateBookWithText(
books: List<Book> book: Book,
text: List<StringWithId>
): Boolean ): Boolean
suspend fun updateCoverImageOfBook( suspend fun updateCoverImageOfBook(

View file

@ -7,6 +7,6 @@ import javax.inject.Inject
class GetText @Inject constructor(private val repository: BookRepository) { class GetText @Inject constructor(private val repository: BookRepository) {
suspend fun execute(textPath: String): List<StringWithId> { suspend fun execute(textPath: String): List<StringWithId> {
return repository.getBookTextById(textPath = textPath) return repository.getBookText(textPath = textPath)
} }
} }

View file

@ -0,0 +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.StringWithId
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<StringWithId>): Boolean {
return repository.insertBook(book, coverImage, text)
}
}

View file

@ -1,12 +0,0 @@
package ua.acclorite.book_story.domain.use_case
import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.domain.repository.BookRepository
import ua.acclorite.book_story.domain.util.CoverImage
import javax.inject.Inject
class InsertBooks @Inject constructor(private val repository: BookRepository) {
suspend fun execute(books: List<Pair<Book, CoverImage?>>): Boolean {
return repository.insertBooks(books)
}
}

View file

@ -0,0 +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.StringWithId
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<StringWithId>): Boolean {
return repository.updateBookWithText(book, text)
}
}

View file

@ -1,12 +0,0 @@
package ua.acclorite.book_story.domain.use_case
import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.domain.repository.BookRepository
import javax.inject.Inject
class UpdateBooksWithText @Inject constructor(private val repository: BookRepository) {
suspend fun execute(books: List<Book>): Boolean {
return repository.updateBooksWithText(books)
}
}

View file

@ -244,21 +244,17 @@ object Constants {
) )
val EMPTY_BOOK = Book( val EMPTY_BOOK = Book(
-1, id = -1,
"", title = "",
UIText.StringValue(""), author = UIText.StringValue(""),
null, description = null,
"", textPath = "",
emptyList(), filePath = "",
0, coverImage = null,
0, scrollIndex = 0,
0, scrollOffset = 0,
0, progress = 0f,
0f, lastOpened = null,
null, category = Category.READING
"",
null,
Category.READING,
null
) )
} }

View file

@ -21,6 +21,7 @@ import androidx.compose.ui.unit.dp
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoEvent import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoEvent
import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoState import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoState
import java.io.File
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Date import java.util.Date
import java.util.Locale import java.util.Locale
@ -46,7 +47,10 @@ fun BookInfoDetailsBottomSheet(
} }
val sizeBytes = remember { val sizeBytes = remember {
state.value.book.file?.length() ?: 0 val file = File(state.value.book.filePath)
if (file.exists()) {
file.length()
} else 0
} }
val fileSizeKB = remember { val fileSizeKB = remember {

View file

@ -7,6 +7,7 @@ import androidx.compose.runtime.Immutable
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import ua.acclorite.book_story.domain.model.Book import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.domain.model.Category import ua.acclorite.book_story.domain.model.Category
import ua.acclorite.book_story.domain.model.StringWithId
import ua.acclorite.book_story.presentation.data.Navigator import ua.acclorite.book_story.presentation.data.Navigator
@Immutable @Immutable
@ -57,7 +58,7 @@ sealed class BookInfoEvent {
data object OnDismissConfirmUpdateDialog : BookInfoEvent() data object OnDismissConfirmUpdateDialog : BookInfoEvent()
data class OnShowConfirmUpdateDialog( data class OnShowConfirmUpdateDialog(
val updatedBook: Book, val updatedBook: Pair<Book, List<StringWithId>>,
val authorUpdated: Boolean, val authorUpdated: Boolean,
val descriptionUpdated: Boolean, val descriptionUpdated: Boolean,
val textUpdated: Boolean, val textUpdated: Boolean,

View file

@ -3,6 +3,7 @@ package ua.acclorite.book_story.presentation.screens.book_info.data
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import ua.acclorite.book_story.domain.model.Book import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.domain.model.Category import ua.acclorite.book_story.domain.model.Category
import ua.acclorite.book_story.domain.model.StringWithId
import ua.acclorite.book_story.domain.util.Constants import ua.acclorite.book_story.domain.util.Constants
@Immutable @Immutable
@ -12,7 +13,7 @@ data class BookInfoState(
val isLoadingUpdate: Boolean = false, val isLoadingUpdate: Boolean = false,
val isRefreshing: Boolean = false, val isRefreshing: Boolean = false,
val showConfirmUpdateDialog: Boolean = false, val showConfirmUpdateDialog: Boolean = false,
val updatedBook: Book? = null, val updatedBook: Pair<Book, List<StringWithId>>? = null,
val authorChanged: Boolean = false, val authorChanged: Boolean = false,
val descriptionChanged: Boolean = false, val descriptionChanged: Boolean = false,
val textChanged: Boolean = false, val textChanged: Boolean = false,

View file

@ -26,12 +26,13 @@ import ua.acclorite.book_story.domain.use_case.GetBookFromFile
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.GetText import ua.acclorite.book_story.domain.use_case.GetText
import ua.acclorite.book_story.domain.use_case.InsertHistory import ua.acclorite.book_story.domain.use_case.InsertHistory
import ua.acclorite.book_story.domain.use_case.UpdateBookWithText
import ua.acclorite.book_story.domain.use_case.UpdateBooks import ua.acclorite.book_story.domain.use_case.UpdateBooks
import ua.acclorite.book_story.domain.use_case.UpdateBooksWithText
import ua.acclorite.book_story.domain.use_case.UpdateCoverImageOfBook import ua.acclorite.book_story.domain.use_case.UpdateCoverImageOfBook
import ua.acclorite.book_story.presentation.data.Argument import ua.acclorite.book_story.presentation.data.Argument
import ua.acclorite.book_story.presentation.data.Navigator import ua.acclorite.book_story.presentation.data.Navigator
import ua.acclorite.book_story.presentation.data.Screen import ua.acclorite.book_story.presentation.data.Screen
import java.io.File
import java.util.Date import java.util.Date
import javax.inject.Inject import javax.inject.Inject
@ -39,7 +40,7 @@ import javax.inject.Inject
@HiltViewModel @HiltViewModel
class BookInfoViewModel @Inject constructor( class BookInfoViewModel @Inject constructor(
private val updateBooks: UpdateBooks, private val updateBooks: UpdateBooks,
private val updateBooksWithText: UpdateBooksWithText, private val updateBookWithText: UpdateBookWithText,
private val updateCoverImageOfBook: UpdateCoverImageOfBook, private val updateCoverImageOfBook: UpdateCoverImageOfBook,
private val insertHistory: InsertHistory, private val insertHistory: InsertHistory,
private val deleteBooks: DeleteBooks, private val deleteBooks: DeleteBooks,
@ -302,7 +303,7 @@ class BookInfoViewModel @Inject constructor(
} }
yield() yield()
if (_state.value.book.file == null) { if (!File(_state.value.book.filePath).exists()) {
onEvent( onEvent(
BookInfoEvent.OnShowSnackbar( BookInfoEvent.OnShowSnackbar(
text = event.context.getString( text = event.context.getString(
@ -334,12 +335,13 @@ class BookInfoViewModel @Inject constructor(
} }
yield() yield()
val nullableBook = getBookFromFile.execute(_state.value.book.file!!) val updatedBook = getBookFromFile.execute(File(_state.value.book.filePath))
yield() yield()
if (nullableBook is NullableBook.Null) {
if (updatedBook is NullableBook.Null) {
onEvent( onEvent(
BookInfoEvent.OnShowSnackbar( BookInfoEvent.OnShowSnackbar(
text = nullableBook.message?.asString(event.context) text = updatedBook.message?.asString(event.context)
?: event.context.getString(R.string.error_something_went_wrong_with_file), ?: event.context.getString(R.string.error_something_went_wrong_with_file),
action = event.context.getString(R.string.retry), action = event.context.getString(R.string.retry),
onAction = { onAction = {
@ -365,7 +367,6 @@ class BookInfoViewModel @Inject constructor(
} }
yield() yield()
val updatedBook = nullableBook.book?.first ?: return@launch
val book = _state.value.book val book = _state.value.book
var authorUpdated = false var authorUpdated = false
@ -373,20 +374,19 @@ class BookInfoViewModel @Inject constructor(
var textUpdated = false var textUpdated = false
if ( if (
updatedBook.author.asString(event.context) != updatedBook.book!!.author.asString(event.context) !=
book.author.asString(event.context) book.author.asString(event.context)
) { ) {
authorUpdated = true authorUpdated = true
} }
if (updatedBook.description != book.description) { if (updatedBook.book.description != book.description) {
descriptionUpdated = true descriptionUpdated = true
} }
if (
updatedBook.text.map { it.line } != val updatedText = updatedBook.text
book.text.ifEmpty { val text = getText.execute(book.textPath)
getText.execute(book.textPath)
}.map { it.line } if (updatedText.map { it.line } != text.map { it.line }) {
) {
textUpdated = true textUpdated = true
} }
@ -412,7 +412,7 @@ class BookInfoViewModel @Inject constructor(
yield() yield()
onEvent( onEvent(
BookInfoEvent.OnShowConfirmUpdateDialog( BookInfoEvent.OnShowConfirmUpdateDialog(
updatedBook = updatedBook, updatedBook = updatedBook.book to updatedBook.text,
authorUpdated = authorUpdated, authorUpdated = authorUpdated,
descriptionUpdated = descriptionUpdated, descriptionUpdated = descriptionUpdated,
textUpdated = textUpdated textUpdated = textUpdated
@ -492,36 +492,29 @@ class BookInfoViewModel @Inject constructor(
val updatedBook = _state.value.updatedBook ?: return@launch val updatedBook = _state.value.updatedBook ?: return@launch
val author = if (_state.value.authorChanged) { val author = if (_state.value.authorChanged) {
updatedBook.author updatedBook.first.author
} else { } else {
book.author book.author
} }
val description = if (_state.value.descriptionChanged) { val description = if (_state.value.descriptionChanged) {
updatedBook.description updatedBook.first.description
} else { } else {
book.description book.description
} }
val text = if (_state.value.textChanged) {
updatedBook.text
} else {
book.text
}
_state.update { _state.update {
it.copy( it.copy(
book = it.book.copy( book = it.book.copy(
author = author, author = author,
description = description, description = description
text = text
) )
) )
} }
if (_state.value.textChanged) { if (_state.value.textChanged) {
val isSuccess = updateBooksWithText.execute( val isSuccess = updateBookWithText.execute(
listOf( book = _state.value.book,
_state.value.book text = updatedBook.second
)
) )
if (!isSuccess) { if (!isSuccess) {

View file

@ -44,14 +44,14 @@ fun BrowseAddingDialogItem(result: Pair<NullableBook, Selected>, onClick: (Boole
Modifier.weight(0.85f) Modifier.weight(0.85f)
) { ) {
Text( Text(
text = result.first.book!!.first.title, text = result.first.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!!.first.author.asString(), text = result.first.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

@ -23,7 +23,7 @@ import kotlinx.coroutines.yield
import ua.acclorite.book_story.domain.model.NullableBook import ua.acclorite.book_story.domain.model.NullableBook
import ua.acclorite.book_story.domain.use_case.GetBooksFromFiles import ua.acclorite.book_story.domain.use_case.GetBooksFromFiles
import ua.acclorite.book_story.domain.use_case.GetFilesFromDevice import ua.acclorite.book_story.domain.use_case.GetFilesFromDevice
import ua.acclorite.book_story.domain.use_case.InsertBooks import ua.acclorite.book_story.domain.use_case.InsertBook
import ua.acclorite.book_story.domain.util.Resource import ua.acclorite.book_story.domain.util.Resource
import ua.acclorite.book_story.presentation.data.Screen import ua.acclorite.book_story.presentation.data.Screen
import javax.inject.Inject import javax.inject.Inject
@ -33,7 +33,7 @@ import javax.inject.Inject
class BrowseViewModel @Inject constructor( class BrowseViewModel @Inject constructor(
private val getBooksFromFiles: GetBooksFromFiles, private val getBooksFromFiles: GetBooksFromFiles,
private val getFilesFromDevice: GetFilesFromDevice, private val getFilesFromDevice: GetFilesFromDevice,
private val insertBooks: InsertBooks private val insertBook: InsertBook
) : ViewModel() { ) : ViewModel() {
private val _state = MutableStateFlow(BrowseState()) private val _state = MutableStateFlow(BrowseState())
@ -369,13 +369,28 @@ class BrowseViewModel @Inject constructor(
val booksToInsert = _state.value.selectedBooks val booksToInsert = _state.value.selectedBooks
.filter { it.first is NullableBook.NotNull } .filter { it.first is NullableBook.NotNull }
.filter { it.second } .filter { it.second }
.map { it.first.book!! } .map { it.first }
if (booksToInsert.isEmpty()) { if (booksToInsert.isEmpty()) {
return@launch return@launch
} }
if (!insertBooks.execute(booksToInsert)) { val failed = booksToInsert.any {
!insertBook.execute(
it.book!!,
it.coverImage,
it.text
)
}
if (failed) {
_state.update {
it.copy(
showAddingDialog = false
)
}
onEvent(BrowseEvent.OnLoadList)
onEvent(BrowseEvent.OnClearSelectedFiles)
event.onFailed() event.onFailed()
return@launch return@launch
} }

View file

@ -201,14 +201,14 @@ private fun ReaderScreen(
snapshotFlow { snapshotFlow {
listState.firstVisibleItemIndex to listState.firstVisibleItemScrollOffset listState.firstVisibleItemIndex to listState.firstVisibleItemScrollOffset
}.debounce(500).collectLatest { items -> }.debounce(500).collectLatest { items ->
if (!loading.value && state.value.book.text.isNotEmpty() && listState.layoutInfo.totalItemsCount > 0) { if (!loading.value && state.value.text.isNotEmpty() && listState.layoutInfo.totalItemsCount > 0) {
val lastVisibleItemIndex = listState.layoutInfo.visibleItemsInfo.last().index val lastVisibleItemIndex = listState.layoutInfo.visibleItemsInfo.last().index
val progress = if (items.first > 0) { val progress = if (items.first > 0) {
if (lastVisibleItemIndex >= (listState.layoutInfo.totalItemsCount - 1)) { if (lastVisibleItemIndex >= (listState.layoutInfo.totalItemsCount - 1)) {
1f 1f
} else { } else {
(items.first.toFloat() / (state.value.book.text.lastIndex).toFloat()) (items.first.toFloat() / (state.value.text.lastIndex).toFloat())
} }
} else { } else {
0f 0f
@ -338,7 +338,7 @@ private fun ReaderScreen(
} }
) )
) { ) {
if (state.value.book.text.isNotEmpty()) { if (state.value.text.isNotEmpty()) {
item { item {
DisableSelection { DisableSelection {
ReaderStartItem(state = state) ReaderStartItem(state = state)
@ -347,9 +347,9 @@ private fun ReaderScreen(
} }
customItemsIndexed( customItemsIndexed(
state.value.book.text, key = { key -> key.id } state.value.text, key = { key -> key.id }
) { index, line -> ) { index, line ->
val text = remember(mainState.value.paragraphIndentation) { val text = remember(mainState.value.paragraphIndentation, line) {
"${if (mainState.value.paragraphIndentation!!) " " else ""}${line.line}" "${if (mainState.value.paragraphIndentation!!) " " else ""}${line.line}"
} }
@ -361,7 +361,7 @@ private fun ReaderScreen(
top = if (index == 0) 18.dp else 0.dp, top = if (index == 0) 18.dp else 0.dp,
start = sidePadding, start = sidePadding,
end = sidePadding, end = sidePadding,
bottom = if (index == state.value.book.text.lastIndex) 18.dp bottom = if (index == state.value.text.lastIndex) 18.dp
else paragraphHeight else paragraphHeight
) )
) { ) {
@ -377,7 +377,7 @@ private fun ReaderScreen(
} }
} }
if (state.value.book.text.isNotEmpty()) { if (state.value.text.isNotEmpty()) {
item { item {
DisableSelection { DisableSelection {
ReaderEndItem( ReaderEndItem(

View file

@ -83,8 +83,8 @@ fun ReaderEndItem(
Text( Text(
stringResource( stringResource(
id = R.string.letters_and_words, id = R.string.letters_and_words,
state.value.book.letters, state.value.letters,
state.value.book.words state.value.words
), ),
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.bodyLarge, style = MaterialTheme.typography.bodyLarge,

View file

@ -2,12 +2,17 @@ package ua.acclorite.book_story.presentation.screens.reader.data
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import ua.acclorite.book_story.domain.model.Book import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.domain.model.StringWithId
import ua.acclorite.book_story.domain.util.Constants import ua.acclorite.book_story.domain.util.Constants
import ua.acclorite.book_story.domain.util.UIText import ua.acclorite.book_story.domain.util.UIText
@Immutable @Immutable
data class ReaderState( data class ReaderState(
val book: Book = Constants.EMPTY_BOOK, val book: Book = Constants.EMPTY_BOOK,
val text: List<StringWithId> = emptyList(),
val words: Int = 0,
val letters: Int = 0,
val errorMessage: UIText? = null, val errorMessage: UIText? = null,
val showMenu: Boolean = false, val showMenu: Boolean = false,

View file

@ -57,11 +57,6 @@ class ReaderViewModel @Inject constructor(
is ReaderEvent.OnLoadText -> { is ReaderEvent.OnLoadText -> {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
if (
_state.value.book.text.isEmpty() ||
_state.value.book.letters < 1 ||
_state.value.book.words < 1
) {
val text = getText.execute(_state.value.book.textPath) val text = getText.execute(_state.value.book.textPath)
if (text.isEmpty()) { if (text.isEmpty()) {
@ -85,13 +80,10 @@ class ReaderViewModel @Inject constructor(
_state.update { _state.update {
it.copy( it.copy(
book = it.book.copy(
text = text, text = text,
letters = letters, letters = letters,
words = words words = words
) )
)
}
} }
val history = _state.value.book.id.let { val history = _state.value.book.id.let {
@ -111,13 +103,7 @@ class ReaderViewModel @Inject constructor(
updateBooks.execute( updateBooks.execute(
listOf(_state.value.book) listOf(_state.value.book)
) )
event.refreshList( event.refreshList(_state.value.book)
_state.value.book.copy(
text = emptyList(),
letters = 0,
words = 0
)
)
viewModelScope.launch { viewModelScope.launch {
snapshotFlow { snapshotFlow {
@ -126,7 +112,7 @@ class ReaderViewModel @Inject constructor(
val index = _state.value.book.scrollIndex val index = _state.value.book.scrollIndex
val offset = _state.value.book.scrollOffset val offset = _state.value.book.scrollOffset
if (itemsCount >= _state.value.book.text.size) { if (itemsCount >= _state.value.text.size) {
if (index > 0 || offset > 0) { if (index > 0 || offset > 0) {
var loaded = false var loaded = false
for (i in 1..100) { for (i in 1..100) {
@ -213,7 +199,7 @@ class ReaderViewModel @Inject constructor(
if (lastVisibleItemIndex >= (event.listState.layoutInfo.totalItemsCount - 1)) { if (lastVisibleItemIndex >= (event.listState.layoutInfo.totalItemsCount - 1)) {
1f 1f
} else { } else {
(firstVisibleItemIndex.toFloat() / (_state.value.book.text.lastIndex) (firstVisibleItemIndex.toFloat() / (_state.value.text.lastIndex)
.toFloat()) .toFloat())
} }
} else { } else {
@ -237,13 +223,7 @@ class ReaderViewModel @Inject constructor(
event.navigator.putArgument( event.navigator.putArgument(
Argument("book", _state.value.book.id) Argument("book", _state.value.book.id)
) )
event.refreshList( event.refreshList(_state.value.book)
_state.value.book.copy(
text = emptyList(),
letters = 0,
words = 0
)
)
insetsController.show(WindowInsetsCompat.Type.systemBars()) insetsController.show(WindowInsetsCompat.Type.systemBars())
event.navigate(event.navigator) event.navigate(event.navigator)
@ -252,7 +232,7 @@ class ReaderViewModel @Inject constructor(
is ReaderEvent.OnScroll -> { is ReaderEvent.OnScroll -> {
viewModelScope.launch { viewModelScope.launch {
val scrollTo = (_state.value.book.text.size * event.progress).roundToInt() val scrollTo = (_state.value.text.size * event.progress).roundToInt()
event.listState.scrollToItem( event.listState.scrollToItem(
scrollTo scrollTo
@ -281,13 +261,7 @@ class ReaderViewModel @Inject constructor(
_state.value.book.id _state.value.book.id
) )
) )
event.refreshList( event.refreshList(_state.value.book)
_state.value.book.copy(
text = emptyList(),
letters = 0,
words = 0
)
)
} }
} }
@ -336,11 +310,7 @@ class ReaderViewModel @Inject constructor(
updateBooks.execute(listOf(_state.value.book)) updateBooks.execute(listOf(_state.value.book))
event.onUpdateCategories( event.onUpdateCategories(
_state.value.book.copy( _state.value.book.copy()
text = emptyList(),
letters = 0,
words = 0
)
) )
event.updatePage( event.updatePage(
Category.entries.dropLastWhile { Category.entries.dropLastWhile {