🚀 Reformat parsers

* Reformatted parsers
* Direct parsing
* Scalable
* Slightly faster parsing (no optimizations yet)

Part-of: #134
This commit is contained in:
Acclorite 2025-01-01 12:49:58 +02:00
parent 3853727ae7
commit fb39d4fe15
32 changed files with 594 additions and 528 deletions

View file

@ -1,9 +1,8 @@
package ua.acclorite.book_story.data.parser package ua.acclorite.book_story.data.parser
import ua.acclorite.book_story.domain.reader.ChapterWithText import ua.acclorite.book_story.domain.reader.ReaderText
import ua.acclorite.book_story.domain.util.Resource
import java.io.File import java.io.File
interface TextParser { interface TextParser {
suspend fun parse(file: File): Resource<List<ChapterWithText>> suspend fun parse(file: File): List<ReaderText>
} }

View file

@ -1,15 +1,12 @@
package ua.acclorite.book_story.data.parser package ua.acclorite.book_story.data.parser
import android.util.Log import android.util.Log
import ua.acclorite.book_story.R
import ua.acclorite.book_story.data.parser.epub.EpubTextParser import ua.acclorite.book_story.data.parser.epub.EpubTextParser
import ua.acclorite.book_story.data.parser.fb2.Fb2TextParser import ua.acclorite.book_story.data.parser.fb2.Fb2TextParser
import ua.acclorite.book_story.data.parser.html.HtmlTextParser import ua.acclorite.book_story.data.parser.html.HtmlTextParser
import ua.acclorite.book_story.data.parser.pdf.PdfTextParser import ua.acclorite.book_story.data.parser.pdf.PdfTextParser
import ua.acclorite.book_story.data.parser.txt.TxtTextParser import ua.acclorite.book_story.data.parser.txt.TxtTextParser
import ua.acclorite.book_story.domain.reader.ChapterWithText import ua.acclorite.book_story.domain.reader.ReaderText
import ua.acclorite.book_story.domain.ui.UIText
import ua.acclorite.book_story.domain.util.Resource
import java.io.File import java.io.File
import javax.inject.Inject import javax.inject.Inject
@ -20,14 +17,12 @@ class TextParserImpl @Inject constructor(
private val pdfTextParser: PdfTextParser, private val pdfTextParser: PdfTextParser,
private val epubTextParser: EpubTextParser, private val epubTextParser: EpubTextParser,
private val fb2TextParser: Fb2TextParser, private val fb2TextParser: Fb2TextParser,
private val htmlTextParser: HtmlTextParser, private val htmlTextParser: HtmlTextParser
) : TextParser { ) : TextParser {
override suspend fun parse(file: File): Resource<List<ChapterWithText>> { override suspend fun parse(file: File): List<ReaderText> {
if (!file.exists()) { if (!file.exists()) {
Log.e(TEXT_PARSER, "File does not exist.") Log.e(TEXT_PARSER, "File does not exist.")
return Resource.Error( return emptyList()
UIText.StringResource(R.string.error_something_went_wrong_with_file)
)
} }
val fileFormat = ".${file.extension}".lowercase().trim() val fileFormat = ".${file.extension}".lowercase().trim()
@ -66,7 +61,7 @@ class TextParserImpl @Inject constructor(
else -> { else -> {
Log.e(TEXT_PARSER, "Wrong file format, could not find supported extension.") Log.e(TEXT_PARSER, "Wrong file format, could not find supported extension.")
Resource.Error(UIText.StringResource(R.string.error_wrong_file_format)) emptyList()
} }
} }
} }

View file

@ -12,14 +12,12 @@ import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.coroutines.yield import kotlinx.coroutines.yield
import org.jsoup.Jsoup import org.jsoup.Jsoup
import ua.acclorite.book_story.R
import ua.acclorite.book_story.data.parser.DocumentParser import ua.acclorite.book_story.data.parser.DocumentParser
import ua.acclorite.book_story.data.parser.MarkdownParser
import ua.acclorite.book_story.data.parser.TextParser import ua.acclorite.book_story.data.parser.TextParser
import ua.acclorite.book_story.domain.reader.Chapter import ua.acclorite.book_story.domain.reader.ReaderText
import ua.acclorite.book_story.domain.reader.ChapterWithText
import ua.acclorite.book_story.domain.ui.UIText
import ua.acclorite.book_story.domain.util.Resource
import ua.acclorite.book_story.presentation.core.util.addAll import ua.acclorite.book_story.presentation.core.util.addAll
import ua.acclorite.book_story.presentation.core.util.clearAllMarkdown
import ua.acclorite.book_story.presentation.core.util.clearMarkdown import ua.acclorite.book_story.presentation.core.util.clearMarkdown
import java.io.File import java.io.File
import java.util.concurrent.ConcurrentLinkedQueue import java.util.concurrent.ConcurrentLinkedQueue
@ -34,16 +32,16 @@ private typealias Title = String
private val dispatcher = Dispatchers.IO.limitedParallelism(2) private val dispatcher = Dispatchers.IO.limitedParallelism(2)
class EpubTextParser @Inject constructor( class EpubTextParser @Inject constructor(
private val markdownParser: MarkdownParser,
private val documentParser: DocumentParser private val documentParser: DocumentParser
) : TextParser { ) : TextParser {
override suspend fun parse(file: File): Resource<List<ChapterWithText>> { override suspend fun parse(file: File): List<ReaderText> {
Log.i(EPUB_TAG, "Started EPUB parsing: ${file.name}.") Log.i(EPUB_TAG, "Started EPUB parsing: ${file.name}.")
return try { return try {
val chapters = mutableListOf<ChapterWithText>()
yield() yield()
var readerText = listOf<ReaderText>()
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
ZipFile(file).use { zip -> ZipFile(file).use { zip ->
@ -62,35 +60,28 @@ class EpubTextParser @Inject constructor(
Log.i(EPUB_TAG, "Chapter entries, size: ${chapterEntries.size}") Log.i(EPUB_TAG, "Chapter entries, size: ${chapterEntries.size}")
Log.i(EPUB_TAG, "Title entries, size: ${chapterTitleEntries?.size}") Log.i(EPUB_TAG, "Title entries, size: ${chapterTitleEntries?.size}")
zip.parseEpub( readerText = zip.parseEpub(
chapterEntries = chapterEntries, chapterEntries = chapterEntries,
chapterTitleEntries = chapterTitleEntries chapterTitleEntries = chapterTitleEntries
).let { )
if (it == null || it.isEmpty()) {
Log.e(EPUB_TAG, "Could not parse EPUB (null or empty).")
return@withContext
}
chapters.addAll(it)
}
} }
} }
yield() yield()
if (chapters.isEmpty()) { if (
return Resource.Error(UIText.StringResource(R.string.error_file_empty)) readerText.filterIsInstance<ReaderText.Text>().isEmpty() ||
readerText.filterIsInstance<ReaderText.Chapter>().isEmpty()
) {
Log.e(EPUB_TAG, "Could not extract text from EPUB.")
return emptyList()
} }
Log.i(EPUB_TAG, "Successfully finished EPUB parsing.") Log.i(EPUB_TAG, "Successfully finished EPUB parsing.")
Resource.Success(chapters) readerText
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
Resource.Error( emptyList()
UIText.StringResource(
R.string.error_query,
e.message?.take(40)?.trim() ?: ""
)
)
} }
} }
@ -107,18 +98,18 @@ class EpubTextParser @Inject constructor(
private suspend fun ZipFile.parseEpub( private suspend fun ZipFile.parseEpub(
chapterEntries: List<ZipEntry>, chapterEntries: List<ZipEntry>,
chapterTitleEntries: Map<Title, List<String>>? chapterTitleEntries: Map<Title, List<String>>?
): List<ChapterWithText>? { ): List<ReaderText> {
val chapters = mutableListOf<ChapterWithText>() val readerText = mutableListOf<ReaderText>()
coroutineScope { coroutineScope {
val unformattedChapters = ConcurrentLinkedQueue<ChapterWithText>() val unformattedText = ConcurrentLinkedQueue<Pair<Int, List<ReaderText>>>()
// Asynchronously getting all chapters with text // Asynchronously getting all chapters with text
val jobs = chapterEntries.mapIndexed { index, entry -> val jobs = chapterEntries.mapIndexed { index, entry ->
async(dispatcher) { async(dispatcher) {
yield() yield()
unformattedChapters.parseZipEntry( unformattedText.parseZipEntry(
zip = this@parseEpub, zip = this@parseEpub,
index = index, index = index,
entry = entry, entry = entry,
@ -131,27 +122,15 @@ class EpubTextParser @Inject constructor(
jobs.awaitAll() jobs.awaitAll()
// Sorting chapters in correct order // Sorting chapters in correct order
chapters.addAll { readerText.addAll {
var textIndex = -1 unformattedText.toList()
unformattedChapters.toList() .sortedBy { (index, _) -> index }
.sortedBy { it.chapter.index } .map { it.second }
.mapIndexed { index, item -> .flatten()
item.copy(
chapter = item.chapter.copy(
index = index,
startIndex = textIndex + 1,
endIndex = textIndex + item.text.size
)
).also { textIndex += item.text.size }
}
} }
} }
if (chapters.isEmpty()) { return readerText
return null
}
return chapters
} }
/** /**
@ -163,7 +142,7 @@ class EpubTextParser @Inject constructor(
* @param entry [ZipEntry]. * @param entry [ZipEntry].
* @param chapterTitleMap Titles from [getChapterTitleMapFromToc]. * @param chapterTitleMap Titles from [getChapterTitleMapFromToc].
*/ */
private suspend fun ConcurrentLinkedQueue<ChapterWithText>.parseZipEntry( private suspend fun ConcurrentLinkedQueue<Pair<Int, List<ReaderText>>>.parseZipEntry(
zip: ZipFile, zip: ZipFile,
index: Int, index: Int,
entry: ZipEntry, entry: ZipEntry,
@ -171,46 +150,67 @@ class EpubTextParser @Inject constructor(
) { ) {
// Getting all text // Getting all text
val content = zip.getInputStream(entry).bufferedReader().use { it.readText() } val content = zip.getInputStream(entry).bufferedReader().use { it.readText() }
var chapter = documentParser.run { var text = documentParser.run {
Jsoup.parse(content).parseDocument() Jsoup.parse(content).parseDocument()
} }
val readerText = mutableListOf<ReaderText>()
if (chapter.isEmpty()) { // Adding chapter title from TOC if found
Log.w(EPUB_TAG, "Chapter ${entry.name} is empty.") var chapterAdded = false
return getChapterTitleFromToc(
}
// Getting title and removing first line (if matches title)
val chapterTitle = getChapterTitleFromToc(
chapterSource = entry.name, chapterSource = entry.name,
chapterTitleMap = chapterTitleMap chapterTitleMap = chapterTitleMap
).run { ).apply {
if (this != null) { if (this == null) return@apply
return@run this readerText.add(
} ReaderText.Chapter(
chapter.first().clearMarkdown() title = this
}.also { title -> )
chapter = chapter.dropWhile { line -> )
line.clearMarkdown().lowercase() == title.lowercase() chapterAdded = true
text = text.dropWhile { line ->
line.clearMarkdown().lowercase() == this.lowercase()
} }
} }
if (chapter.isEmpty()) { // Format and add text
Log.w(EPUB_TAG, "Chapter ${entry.name} is empty.") text.forEach { line ->
yield()
if (line.isNotBlank()) {
when (line) {
"***", "---" -> readerText.add(
ReaderText.Separator
)
else -> {
if (!chapterAdded && line.clearAllMarkdown().isNotBlank()) {
readerText.add(
0, ReaderText.Chapter(
title = line.clearAllMarkdown()
)
)
chapterAdded = true
} else readerText.add(
ReaderText.Text(
line = markdownParser.parse(line)
)
)
}
}
}
}
if (
readerText.filterIsInstance<ReaderText.Text>().isEmpty() ||
readerText.filterIsInstance<ReaderText.Chapter>().isEmpty()
) {
Log.w(EPUB_TAG, "Could not extract text from [${entry.name}].")
return return
} }
add( add(index to readerText)
ChapterWithText(
Chapter(
index = index,
title = chapterTitle,
startIndex = 0,
endIndex = 0
),
text = chapter
)
)
} }
/** /**

View file

@ -6,12 +6,9 @@ import kotlinx.coroutines.withContext
import kotlinx.coroutines.yield import kotlinx.coroutines.yield
import org.w3c.dom.Element import org.w3c.dom.Element
import org.w3c.dom.NodeList import org.w3c.dom.NodeList
import ua.acclorite.book_story.R import ua.acclorite.book_story.data.parser.MarkdownParser
import ua.acclorite.book_story.data.parser.TextParser import ua.acclorite.book_story.data.parser.TextParser
import ua.acclorite.book_story.domain.reader.Chapter import ua.acclorite.book_story.domain.reader.ReaderText
import ua.acclorite.book_story.domain.reader.ChapterWithText
import ua.acclorite.book_story.domain.ui.UIText
import ua.acclorite.book_story.domain.util.Resource
import ua.acclorite.book_story.presentation.core.util.clearAllMarkdown import ua.acclorite.book_story.presentation.core.util.clearAllMarkdown
import java.io.File import java.io.File
import javax.inject.Inject import javax.inject.Inject
@ -19,9 +16,11 @@ import javax.xml.parsers.DocumentBuilderFactory
private const val FB2_TAG = "FB2 Parser" private const val FB2_TAG = "FB2 Parser"
class Fb2TextParser @Inject constructor() : TextParser { class Fb2TextParser @Inject constructor(
private val markdownParser: MarkdownParser
) : TextParser {
override suspend fun parse(file: File): Resource<List<ChapterWithText>> { override suspend fun parse(file: File): List<ReaderText> {
Log.i(FB2_TAG, "Started FB2 parsing: ${file.name}.") Log.i(FB2_TAG, "Started FB2 parsing: ${file.name}.")
return try { return try {
@ -31,13 +30,11 @@ class Fb2TextParser @Inject constructor() : TextParser {
builder.parse(file) builder.parse(file)
} }
val formattedLines = mutableListOf<String>() val readerText = mutableListOf<ReaderText>()
val bodyNodes = document.getElementsByTagName("body") val bodyNodes = document.getElementsByTagName("body")
if (bodyNodes.length == 0) { if (bodyNodes.length == 0) {
return Resource.Error( return emptyList()
UIText.StringResource(R.string.error_file_empty)
)
} }
yield() yield()
@ -108,45 +105,49 @@ class Fb2TextParser @Inject constructor() : TextParser {
yield() yield()
var chapterAdded = false
lines.forEach { line -> lines.forEach { line ->
yield() yield()
formattedLines.add(line.trim())
if (line.isNotBlank()) {
when (line) {
"***", "---" -> readerText.add(
ReaderText.Separator
)
else -> {
if (!chapterAdded && line.clearAllMarkdown().isNotBlank()) {
readerText.add(
0, ReaderText.Chapter(
title = line.clearAllMarkdown()
)
)
chapterAdded = true
} else readerText.add(
ReaderText.Text(
line = markdownParser.parse(line)
)
)
}
}
}
} }
yield() yield()
if (formattedLines.size < 2) { if (
return Resource.Error(UIText.StringResource(R.string.error_file_empty)) readerText.filterIsInstance<ReaderText.Text>().isEmpty() ||
} readerText.filterIsInstance<ReaderText.Chapter>().isEmpty()
) {
val title = formattedLines.first().clearAllMarkdown().let { title -> Log.e(FB2_TAG, "Could not extract text from FB2.")
formattedLines.removeAt(0) return emptyList()
if (title.isBlank()) return@let "Chapter 1"
return@let title
} }
Log.i(FB2_TAG, "Successfully finished FB2 parsing.") Log.i(FB2_TAG, "Successfully finished FB2 parsing.")
Resource.Success( readerText
listOf(
ChapterWithText(
chapter = Chapter(
index = 0,
title = title,
startIndex = 0,
endIndex = formattedLines.lastIndex
),
text = formattedLines
)
)
)
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
Resource.Error( emptyList()
UIText.StringResource(
R.string.error_query,
e.message?.take(40)?.trim() ?: ""
)
)
} }
} }

View file

@ -3,13 +3,10 @@ package ua.acclorite.book_story.data.parser.html
import android.util.Log import android.util.Log
import kotlinx.coroutines.yield import kotlinx.coroutines.yield
import org.jsoup.Jsoup import org.jsoup.Jsoup
import ua.acclorite.book_story.R
import ua.acclorite.book_story.data.parser.DocumentParser import ua.acclorite.book_story.data.parser.DocumentParser
import ua.acclorite.book_story.data.parser.MarkdownParser
import ua.acclorite.book_story.data.parser.TextParser import ua.acclorite.book_story.data.parser.TextParser
import ua.acclorite.book_story.domain.reader.Chapter import ua.acclorite.book_story.domain.reader.ReaderText
import ua.acclorite.book_story.domain.reader.ChapterWithText
import ua.acclorite.book_story.domain.ui.UIText
import ua.acclorite.book_story.domain.util.Resource
import ua.acclorite.book_story.presentation.core.util.clearAllMarkdown import ua.acclorite.book_story.presentation.core.util.clearAllMarkdown
import java.io.File import java.io.File
import javax.inject.Inject import javax.inject.Inject
@ -17,52 +14,62 @@ import javax.inject.Inject
private const val HTML_TAG = "HTML Parser" private const val HTML_TAG = "HTML Parser"
class HtmlTextParser @Inject constructor( class HtmlTextParser @Inject constructor(
private val markdownParser: MarkdownParser,
private val documentParser: DocumentParser private val documentParser: DocumentParser
) : TextParser { ) : TextParser {
override suspend fun parse(file: File): Resource<List<ChapterWithText>> { override suspend fun parse(file: File): List<ReaderText> {
Log.i(HTML_TAG, "Started HTML parsing: ${file.name}.") Log.i(HTML_TAG, "Started HTML parsing: ${file.name}.")
return try { return try {
val lines = documentParser.run { var chapterAdded = false
val documentLines = documentParser.run {
Jsoup.parse(file).parseDocument() Jsoup.parse(file).parseDocument()
}.toMutableList() }
val readerText = mutableListOf<ReaderText>()
for (line in documentLines) {
yield()
if (line.isNotBlank()) {
when (line) {
"***", "---" -> readerText.add(
ReaderText.Separator
)
else -> {
if (!chapterAdded && line.clearAllMarkdown().isNotBlank()) {
readerText.add(
0, ReaderText.Chapter(
title = line.clearAllMarkdown()
)
)
chapterAdded = true
} else readerText.add(
ReaderText.Text(
line = markdownParser.parse(line)
)
)
}
}
}
}
yield() yield()
if (lines.size < 2) { if (
readerText.filterIsInstance<ReaderText.Text>().isEmpty() ||
readerText.filterIsInstance<ReaderText.Chapter>().isEmpty()
) {
Log.e(HTML_TAG, "Could not extract text from HTML.") Log.e(HTML_TAG, "Could not extract text from HTML.")
return Resource.Error(UIText.StringResource(R.string.error_file_empty)) return emptyList()
}
val title = lines.first().clearAllMarkdown().let { title ->
lines.removeAt(0)
if (title.isBlank()) return@let "Chapter 1"
return@let title
} }
Log.i(HTML_TAG, "Successfully finished HTML parsing.") Log.i(HTML_TAG, "Successfully finished HTML parsing.")
Resource.Success( readerText
listOf(
ChapterWithText(
chapter = Chapter(
index = 0,
title = title,
startIndex = 0,
endIndex = lines.lastIndex
),
text = lines
)
)
)
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
Resource.Error( emptyList()
UIText.StringResource(
R.string.error_query,
e.message?.take(40)?.trim() ?: ""
)
)
} }
} }
} }

View file

@ -6,12 +6,9 @@ import com.tom_roush.pdfbox.android.PDFBoxResourceLoader
import com.tom_roush.pdfbox.pdmodel.PDDocument import com.tom_roush.pdfbox.pdmodel.PDDocument
import com.tom_roush.pdfbox.text.PDFTextStripper import com.tom_roush.pdfbox.text.PDFTextStripper
import kotlinx.coroutines.yield import kotlinx.coroutines.yield
import ua.acclorite.book_story.R import ua.acclorite.book_story.data.parser.MarkdownParser
import ua.acclorite.book_story.data.parser.TextParser import ua.acclorite.book_story.data.parser.TextParser
import ua.acclorite.book_story.domain.reader.Chapter import ua.acclorite.book_story.domain.reader.ReaderText
import ua.acclorite.book_story.domain.reader.ChapterWithText
import ua.acclorite.book_story.domain.ui.UIText
import ua.acclorite.book_story.domain.util.Resource
import ua.acclorite.book_story.presentation.core.util.clearAllMarkdown import ua.acclorite.book_story.presentation.core.util.clearAllMarkdown
import java.io.File import java.io.File
import javax.inject.Inject import javax.inject.Inject
@ -19,10 +16,11 @@ import javax.inject.Inject
private const val PDF_TAG = "PDF Parser" private const val PDF_TAG = "PDF Parser"
class PdfTextParser @Inject constructor( class PdfTextParser @Inject constructor(
private val markdownParser: MarkdownParser,
private val application: Application private val application: Application
) : TextParser { ) : TextParser {
override suspend fun parse(file: File): Resource<List<ChapterWithText>> { override suspend fun parse(file: File): List<ReaderText> {
Log.i(PDF_TAG, "Started PDF parsing: ${file.name}.") Log.i(PDF_TAG, "Started PDF parsing: ${file.name}.")
return try { return try {
@ -44,7 +42,7 @@ class PdfTextParser @Inject constructor(
yield() yield()
val strings = mutableListOf<String>() val readerText = mutableListOf<ReaderText>()
val text = oldText.filterIndexed { index, c -> val text = oldText.filterIndexed { index, c ->
yield() yield()
@ -110,45 +108,49 @@ class PdfTextParser @Inject constructor(
yield() yield()
var chapterAdded = false
lines.forEach { line -> lines.forEach { line ->
yield() yield()
strings.add(line.trim())
if (line.isNotBlank()) {
when (line) {
"***", "---" -> readerText.add(
ReaderText.Separator
)
else -> {
if (!chapterAdded && line.clearAllMarkdown().isNotBlank()) {
readerText.add(
0, ReaderText.Chapter(
title = line.clearAllMarkdown()
)
)
chapterAdded = true
} else readerText.add(
ReaderText.Text(
line = markdownParser.parse(line)
)
)
}
}
}
} }
yield() yield()
if (strings.size < 2) { if (
return Resource.Error(UIText.StringResource(R.string.error_file_empty)) readerText.filterIsInstance<ReaderText.Text>().isEmpty() ||
} readerText.filterIsInstance<ReaderText.Chapter>().isEmpty()
) {
val title = strings.first().clearAllMarkdown().let { title -> Log.e(PDF_TAG, "Could not extract text from PDF.")
strings.removeAt(0) return emptyList()
if (title.isBlank()) return@let "Chapter 1"
return@let title
} }
Log.i(PDF_TAG, "Successfully finished PDF parsing.") Log.i(PDF_TAG, "Successfully finished PDF parsing.")
Resource.Success( readerText
listOf(
ChapterWithText(
chapter = Chapter(
index = 0,
title = title,
startIndex = 0,
endIndex = strings.lastIndex
),
text = strings
)
)
)
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
Resource.Error( emptyList()
UIText.StringResource(
R.string.error_query,
e.message?.take(40)?.trim() ?: ""
)
)
} }
} }
} }

View file

@ -4,12 +4,9 @@ import android.util.Log
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.coroutines.yield import kotlinx.coroutines.yield
import ua.acclorite.book_story.R import ua.acclorite.book_story.data.parser.MarkdownParser
import ua.acclorite.book_story.data.parser.TextParser import ua.acclorite.book_story.data.parser.TextParser
import ua.acclorite.book_story.domain.reader.Chapter import ua.acclorite.book_story.domain.reader.ReaderText
import ua.acclorite.book_story.domain.reader.ChapterWithText
import ua.acclorite.book_story.domain.ui.UIText
import ua.acclorite.book_story.domain.util.Resource
import ua.acclorite.book_story.presentation.core.util.clearAllMarkdown import ua.acclorite.book_story.presentation.core.util.clearAllMarkdown
import java.io.BufferedReader import java.io.BufferedReader
import java.io.File import java.io.File
@ -18,58 +15,59 @@ import javax.inject.Inject
private const val TXT_TAG = "TXT Parser" private const val TXT_TAG = "TXT Parser"
class TxtTextParser @Inject constructor() : TextParser { class TxtTextParser @Inject constructor(
private val markdownParser: MarkdownParser
) : TextParser {
override suspend fun parse(file: File): Resource<List<ChapterWithText>> { override suspend fun parse(file: File): List<ReaderText> {
Log.i(TXT_TAG, "Started TXT parsing: ${file.name}.") Log.i(TXT_TAG, "Started TXT parsing: ${file.name}.")
return try { return try {
val lines = mutableListOf<String>() val readerText = mutableListOf<ReaderText>()
var chapterAdded = false
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
BufferedReader(FileReader(file)).forEachLine { line -> BufferedReader(FileReader(file)).forEachLine { line ->
if (line.isNotBlank()) { if (line.isNotBlank()) {
lines.add( when (line) {
line.trim() "***", "---" -> readerText.add(
ReaderText.Separator
) )
else -> {
if (!chapterAdded && line.clearAllMarkdown().isNotBlank()) {
readerText.add(
0, ReaderText.Chapter(
title = line.clearAllMarkdown()
)
)
chapterAdded = true
} else readerText.add(
ReaderText.Text(
line = markdownParser.parse(line)
)
)
}
}
} }
} }
} }
yield() yield()
if (lines.size < 2) { if (
return Resource.Error(UIText.StringResource(R.string.error_file_empty)) readerText.filterIsInstance<ReaderText.Text>().isEmpty() ||
} readerText.filterIsInstance<ReaderText.Chapter>().isEmpty()
) {
val title = lines.first().clearAllMarkdown().let { title -> Log.e(TXT_TAG, "Could not extract text from TXT.")
lines.removeAt(0) return emptyList()
if (title.isBlank()) return@let "Chapter 1"
return@let title
} }
Log.i(TXT_TAG, "Successfully finished TXT parsing.") Log.i(TXT_TAG, "Successfully finished TXT parsing.")
Resource.Success( readerText
listOf(
ChapterWithText(
chapter = Chapter(
index = 0,
title = title,
startIndex = 0,
endIndex = lines.lastIndex
),
text = lines
)
)
)
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
Resource.Error( emptyList()
UIText.StringResource(
R.string.error_query,
e.message?.take(40)?.trim() ?: ""
)
)
} }
} }
} }

View file

@ -6,20 +6,17 @@ import android.graphics.BitmapFactory
import android.net.Uri import android.net.Uri
import android.provider.MediaStore import android.provider.MediaStore
import android.util.Log import android.util.Log
import androidx.compose.ui.text.AnnotatedString
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import ua.acclorite.book_story.data.local.room.BookDao import ua.acclorite.book_story.data.local.room.BookDao
import ua.acclorite.book_story.data.mapper.book.BookMapper import ua.acclorite.book_story.data.mapper.book.BookMapper
import ua.acclorite.book_story.data.parser.FileParser import ua.acclorite.book_story.data.parser.FileParser
import ua.acclorite.book_story.data.parser.MarkdownParser
import ua.acclorite.book_story.data.parser.TextParser import ua.acclorite.book_story.data.parser.TextParser
import ua.acclorite.book_story.domain.library.book.Book import ua.acclorite.book_story.domain.library.book.Book
import ua.acclorite.book_story.domain.library.book.BookWithCover import ua.acclorite.book_story.domain.library.book.BookWithCover
import ua.acclorite.book_story.domain.reader.ChaptersAndText import ua.acclorite.book_story.domain.reader.ReaderText
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 ua.acclorite.book_story.domain.util.CoverImage
import ua.acclorite.book_story.domain.util.Resource
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
@ -45,8 +42,7 @@ class BookRepositoryImpl @Inject constructor(
private val bookMapper: BookMapper, private val bookMapper: BookMapper,
private val fileParser: FileParser, private val fileParser: FileParser,
private val textParser: TextParser, private val textParser: TextParser
private val markdownParser: MarkdownParser
) : BookRepository { ) : BookRepository {
/** /**
@ -90,43 +86,31 @@ class BookRepositoryImpl @Inject constructor(
} }
/** /**
* Loads text from the book. * Loads text from the book. Already formatted.
*/ */
override suspend fun getBookText(bookId: Int): ChaptersAndText { override suspend fun getBookText(bookId: Int): List<ReaderText> {
if (bookId == -1) return ChaptersAndText(chapters = emptyList(), text = emptyList()) if (bookId == -1) return emptyList()
val book = database.findBookById(bookId) val book = database.findBookById(bookId)
val file = File(book.filePath) val file = File(book.filePath)
if (!file.exists()) { if (!file.exists()) {
Log.e(GET_TEXT, "File [$bookId] does not exist") Log.e(GET_TEXT, "File [$bookId] does not exist")
return ChaptersAndText(chapters = emptyList(), text = emptyList()) return emptyList()
} }
val parsedText = textParser.parse(file) val readerText = textParser.parse(file)
if (parsedText is Resource.Error) {
Log.e(GET_TEXT, "Failed to load text: $bookId")
return ChaptersAndText(chapters = emptyList(), text = emptyList())
}
val chapters = parsedText.data!!.map { it.chapter } if (
val markdownLines = mutableListOf<AnnotatedString>() readerText.filterIsInstance<ReaderText.Text>().isEmpty() ||
readerText.filterIsInstance<ReaderText.Chapter>().isEmpty()
withContext(Dispatchers.IO) { ) {
for (line in parsedText.data.map { it.text }.flatten()) { Log.e(GET_TEXT, "Could not load text from [$bookId].")
if (line.isNotBlank()) { return emptyList()
markdownLines.add(
markdownParser.parse(line.trim())
)
}
}
} }
Log.i(GET_TEXT, "Successfully loaded text of [$bookId] with markdown.") Log.i(GET_TEXT, "Successfully loaded text of [$bookId] with markdown.")
return ChaptersAndText( return readerText
chapters = chapters,
text = markdownLines
)
} }
/** /**

View file

@ -1,14 +0,0 @@
package ua.acclorite.book_story.domain.reader
import android.os.Parcelable
import androidx.compose.runtime.Immutable
import kotlinx.parcelize.Parcelize
@Parcelize
@Immutable
data class Chapter(
val index: Int = 0,
val title: String,
val startIndex: Int,
val endIndex: Int
) : Parcelable

View file

@ -1,9 +0,0 @@
package ua.acclorite.book_story.domain.reader
import androidx.compose.runtime.Immutable
@Immutable
data class ChapterWithText(
val chapter: Chapter,
val text: List<String>
)

View file

@ -1,10 +0,0 @@
package ua.acclorite.book_story.domain.reader
import androidx.compose.runtime.Immutable
import androidx.compose.ui.text.AnnotatedString
@Immutable
data class ChaptersAndText(
val chapters: List<Chapter>,
val text: List<AnnotatedString>
)

View file

@ -0,0 +1,16 @@
package ua.acclorite.book_story.domain.reader
import androidx.compose.runtime.Immutable
import androidx.compose.ui.text.AnnotatedString
@Immutable
sealed class ReaderText {
@Immutable
data class Chapter(val title: String) : ReaderText()
@Immutable
data class Text(val line: AnnotatedString) : ReaderText()
@Immutable
data object Separator : ReaderText()
}

View file

@ -2,7 +2,7 @@ package ua.acclorite.book_story.domain.repository
import ua.acclorite.book_story.domain.library.book.Book import ua.acclorite.book_story.domain.library.book.Book
import ua.acclorite.book_story.domain.library.book.BookWithCover import ua.acclorite.book_story.domain.library.book.BookWithCover
import ua.acclorite.book_story.domain.reader.ChaptersAndText import ua.acclorite.book_story.domain.reader.ReaderText
import ua.acclorite.book_story.domain.util.CoverImage import ua.acclorite.book_story.domain.util.CoverImage
interface BookRepository { interface BookRepository {
@ -17,7 +17,7 @@ interface BookRepository {
suspend fun getBookText( suspend fun getBookText(
bookId: Int bookId: Int
): ChaptersAndText ): List<ReaderText>
suspend fun insertBook( suspend fun insertBook(
bookWithCover: BookWithCover bookWithCover: BookWithCover

View file

@ -1,6 +1,6 @@
package ua.acclorite.book_story.domain.use_case.book package ua.acclorite.book_story.domain.use_case.book
import ua.acclorite.book_story.domain.reader.ChaptersAndText import ua.acclorite.book_story.domain.reader.ReaderText
import ua.acclorite.book_story.domain.repository.BookRepository import ua.acclorite.book_story.domain.repository.BookRepository
import javax.inject.Inject import javax.inject.Inject
@ -8,7 +8,7 @@ class GetText @Inject constructor(
private val repository: BookRepository private val repository: BookRepository
) { ) {
suspend fun execute(bookId: Int): ChaptersAndText { suspend fun execute(bookId: Int): List<ReaderText> {
return repository.getBookText(bookId = bookId) return repository.getBookText(bookId = bookId)
} }
} }

View file

@ -1,8 +0,0 @@
package ua.acclorite.book_story.domain.util
import ua.acclorite.book_story.domain.ui.UIText
sealed class Resource<T>(val data: T? = null, val message: UIText? = null) {
class Success<T>(data: T?) : Resource<T>(data)
class Error<T>(message: UIText, data: T? = null) : Resource<T>(data, message)
}

View file

@ -18,7 +18,8 @@ fun Float.calculateProgress(digits: Int): String {
.dropWhile { it == '-' } .dropWhile { it == '-' }
} }
fun Float.coerceAndPreventNaN(): Float { fun Float?.coerceAndPreventNaN(): Float {
if (this == null) return 0f
if (isNaN()) return 0f if (isNaN()) return 0f
return this.coerceIn(0f, 1f) return this.coerceIn(0f, 1f)
} }

View file

@ -22,13 +22,13 @@ import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.library.book.Book import ua.acclorite.book_story.domain.library.book.Book
import ua.acclorite.book_story.domain.reader.Chapter
import ua.acclorite.book_story.domain.reader.Checkpoint import ua.acclorite.book_story.domain.reader.Checkpoint
import ua.acclorite.book_story.domain.reader.ReaderText
import ua.acclorite.book_story.domain.reader.ReaderText.Chapter
import ua.acclorite.book_story.domain.util.Direction import ua.acclorite.book_story.domain.util.Direction
import ua.acclorite.book_story.presentation.core.components.common.IconButton import ua.acclorite.book_story.presentation.core.components.common.IconButton
import ua.acclorite.book_story.presentation.core.util.calculateProgress import ua.acclorite.book_story.presentation.core.util.calculateProgress
@ -40,7 +40,7 @@ import ua.acclorite.book_story.ui.theme.HorizontalExpandingTransition
@Composable @Composable
fun ReaderBottomBar( fun ReaderBottomBar(
book: Book, book: Book,
text: List<AnnotatedString>, text: List<ReaderText>,
listState: LazyListState, listState: LazyListState,
lockMenu: Boolean, lockMenu: Boolean,
currentChapter: Chapter?, currentChapter: Chapter?,

View file

@ -2,16 +2,18 @@ package ua.acclorite.book_story.presentation.reader
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.reader.Chapter import ua.acclorite.book_story.domain.reader.ReaderText.Chapter
import ua.acclorite.book_story.presentation.core.components.modal_drawer.ModalDrawer import ua.acclorite.book_story.presentation.core.components.modal_drawer.ModalDrawer
import ua.acclorite.book_story.presentation.core.components.modal_drawer.ModalDrawerSelectableItem import ua.acclorite.book_story.presentation.core.components.modal_drawer.ModalDrawerSelectableItem
import ua.acclorite.book_story.presentation.core.components.modal_drawer.ModalDrawerTitleItem import ua.acclorite.book_story.presentation.core.components.modal_drawer.ModalDrawerTitleItem
@ -27,9 +29,15 @@ fun ReaderChaptersDrawer(
scrollToChapter: (ReaderEvent.OnScrollToChapter) -> Unit, scrollToChapter: (ReaderEvent.OnScrollToChapter) -> Unit,
dismissDrawer: (ReaderEvent.OnDismissDrawer) -> Unit dismissDrawer: (ReaderEvent.OnDismissDrawer) -> Unit
) { ) {
val currentChapterIndex = remember(chapters, currentChapter) {
derivedStateOf {
chapters.indexOf(currentChapter).takeIf { it != -1 } ?: 0
}
}
ModalDrawer( ModalDrawer(
show = show, show = show,
startIndex = currentChapter?.index ?: 0, startIndex = currentChapterIndex.value,
onDismissRequest = { dismissDrawer(ReaderEvent.OnDismissDrawer) }, onDismissRequest = { dismissDrawer(ReaderEvent.OnDismissDrawer) },
header = { header = {
ModalDrawerTitleItem( ModalDrawerTitleItem(
@ -37,9 +45,9 @@ fun ReaderChaptersDrawer(
) )
} }
) { ) {
items(chapters, key = { it.index }) { chapter -> itemsIndexed(chapters, key = { index, _ -> index }) { index, chapter ->
val selected = rememberSaveable(currentChapter) { val selected = rememberSaveable(index, currentChapterIndex) {
chapter.index == currentChapter?.index index == currentChapterIndex.value
} }
ModalDrawerSelectableItem( ModalDrawerSelectableItem(
@ -47,7 +55,7 @@ fun ReaderChaptersDrawer(
onClick = { onClick = {
scrollToChapter( scrollToChapter(
ReaderEvent.OnScrollToChapter( ReaderEvent.OnScrollToChapter(
chapterStartIndex = chapter.startIndex chapter = chapter
) )
) )
dismissDrawer(ReaderEvent.OnDismissDrawer) dismissDrawer(ReaderEvent.OnDismissDrawer)

View file

@ -3,17 +3,18 @@ package ua.acclorite.book_story.presentation.reader
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.TextUnit
import ua.acclorite.book_story.domain.library.book.Book import ua.acclorite.book_story.domain.library.book.Book
import ua.acclorite.book_story.domain.reader.Chapter
import ua.acclorite.book_story.domain.reader.Checkpoint import ua.acclorite.book_story.domain.reader.Checkpoint
import ua.acclorite.book_story.domain.reader.FontWithName import ua.acclorite.book_story.domain.reader.FontWithName
import ua.acclorite.book_story.domain.reader.ReaderHorizontalGesture import ua.acclorite.book_story.domain.reader.ReaderHorizontalGesture
import ua.acclorite.book_story.domain.reader.ReaderText
import ua.acclorite.book_story.domain.reader.ReaderText.Chapter
import ua.acclorite.book_story.domain.reader.ReaderTextAlignment import ua.acclorite.book_story.domain.reader.ReaderTextAlignment
import ua.acclorite.book_story.domain.ui.UIText import ua.acclorite.book_story.domain.ui.UIText
import ua.acclorite.book_story.domain.util.BottomSheet import ua.acclorite.book_story.domain.util.BottomSheet
@ -24,7 +25,7 @@ import ua.acclorite.book_story.ui.settings.SettingsEvent
@Composable @Composable
fun ReaderContent( fun ReaderContent(
book: Book, book: Book,
text: List<AnnotatedString>, text: List<ReaderText>,
bottomSheet: BottomSheet?, bottomSheet: BottomSheet?,
drawer: Drawer?, drawer: Drawer?,
listState: LazyListState, listState: LazyListState,
@ -40,7 +41,6 @@ fun ReaderContent(
checkpoint: Checkpoint, checkpoint: Checkpoint,
showMenu: Boolean, showMenu: Boolean,
lockMenu: Boolean, lockMenu: Boolean,
chapters: Map<Int, Chapter>,
contentPadding: PaddingValues, contentPadding: PaddingValues,
verticalPadding: Dp, verticalPadding: Dp,
horizontalGesture: ReaderHorizontalGesture, horizontalGesture: ReaderHorizontalGesture,
@ -102,7 +102,6 @@ fun ReaderContent(
checkpoint = checkpoint, checkpoint = checkpoint,
showMenu = showMenu, showMenu = showMenu,
lockMenu = lockMenu, lockMenu = lockMenu,
chapters = chapters,
contentPadding = contentPadding, contentPadding = contentPadding,
verticalPadding = verticalPadding, verticalPadding = verticalPadding,
horizontalGesture = horizontalGesture, horizontalGesture = horizontalGesture,
@ -141,7 +140,7 @@ fun ReaderContent(
ReaderDrawer( ReaderDrawer(
drawer = drawer, drawer = drawer,
chapters = chapters.values.toList(), chapters = remember(text) { text.filterIsInstance<Chapter>() },
currentChapter = currentChapter, currentChapter = currentChapter,
currentChapterProgress = currentChapterProgress, currentChapterProgress = currentChapterProgress,
scrollToChapter = scrollToChapter, scrollToChapter = scrollToChapter,

View file

@ -1,7 +1,7 @@
package ua.acclorite.book_story.presentation.reader package ua.acclorite.book_story.presentation.reader
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import ua.acclorite.book_story.domain.reader.Chapter import ua.acclorite.book_story.domain.reader.ReaderText.Chapter
import ua.acclorite.book_story.domain.util.Drawer import ua.acclorite.book_story.domain.util.Drawer
import ua.acclorite.book_story.ui.reader.ReaderEvent import ua.acclorite.book_story.ui.reader.ReaderEvent
import ua.acclorite.book_story.ui.reader.ReaderScreen import ua.acclorite.book_story.ui.reader.ReaderScreen

View file

@ -14,16 +14,15 @@ import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.coerceAtLeast import androidx.compose.ui.unit.coerceAtLeast
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.reader.Chapter
import ua.acclorite.book_story.domain.reader.FontWithName import ua.acclorite.book_story.domain.reader.FontWithName
import ua.acclorite.book_story.domain.reader.ReaderHorizontalGesture import ua.acclorite.book_story.domain.reader.ReaderHorizontalGesture
import ua.acclorite.book_story.domain.reader.ReaderText
import ua.acclorite.book_story.domain.reader.ReaderTextAlignment import ua.acclorite.book_story.domain.reader.ReaderTextAlignment
import ua.acclorite.book_story.presentation.core.components.common.LazyColumnWithScrollbar import ua.acclorite.book_story.presentation.core.components.common.LazyColumnWithScrollbar
import ua.acclorite.book_story.presentation.core.components.common.SelectionContainer import ua.acclorite.book_story.presentation.core.components.common.SelectionContainer
@ -34,8 +33,7 @@ import ua.acclorite.book_story.ui.reader.ReaderEvent
@Composable @Composable
fun ReaderLayout( fun ReaderLayout(
text: List<AnnotatedString>, text: List<ReaderText>,
chapters: Map<Int, Chapter>,
listState: LazyListState, listState: LazyListState,
contentPadding: PaddingValues, contentPadding: PaddingValues,
verticalPadding: Dp, verticalPadding: Dp,
@ -148,17 +146,11 @@ fun ReaderLayout(
) { ) {
itemsIndexed( itemsIndexed(
text, key = { index, _ -> index } text, key = { index, _ -> index }
) { index, line -> ) { index, textEntry ->
ReaderLayoutChapter( ReaderLayoutText(
chapter = chapters[index], // Shows only when matching startIndex of chapter.
fontColor = fontColor,
sidePadding = sidePadding
)
ReaderLayoutParagraph(
activity = activity, activity = activity,
showMenu = showMenu, showMenu = showMenu,
line = line, textEntry = textEntry,
fontFamily = fontFamily, fontFamily = fontFamily,
fontColor = fontColor, fontColor = fontColor,
lineHeight = lineHeight, lineHeight = lineHeight,

View file

@ -1,120 +0,0 @@
package ua.acclorite.book_story.presentation.reader
import androidx.activity.ComponentActivity
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.text.BasicText
import androidx.compose.material3.HorizontalDivider
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.style.LineBreak
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextIndent
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.dp
import ua.acclorite.book_story.domain.reader.FontWithName
import ua.acclorite.book_story.domain.reader.ReaderTextAlignment
import ua.acclorite.book_story.presentation.core.util.noRippleClickable
import ua.acclorite.book_story.ui.reader.ReaderEvent
@Composable
fun LazyItemScope.ReaderLayoutParagraph(
activity: ComponentActivity,
showMenu: Boolean,
line: AnnotatedString,
fontFamily: FontWithName,
fontColor: Color,
lineHeight: TextUnit,
fontStyle: FontStyle,
textAlignment: ReaderTextAlignment,
fontSize: TextUnit,
letterSpacing: TextUnit,
sidePadding: Dp,
paragraphIndentation: TextUnit,
fullscreenMode: Boolean,
doubleClickTranslation: Boolean,
toolbarHidden: Boolean,
openTranslator: (ReaderEvent.OnOpenTranslator) -> Unit,
menuVisibility: (ReaderEvent.OnMenuVisibility) -> Unit
) {
Column(
modifier = Modifier
.animateItem(fadeInSpec = null, fadeOutSpec = null)
.fillMaxWidth()
.padding(horizontal = sidePadding),
verticalArrangement = Arrangement.Center,
horizontalAlignment = when (textAlignment) {
ReaderTextAlignment.START, ReaderTextAlignment.JUSTIFY -> Alignment.Start
ReaderTextAlignment.CENTER -> Alignment.CenterHorizontally
ReaderTextAlignment.END -> Alignment.End
}
) {
when (line.text) {
"---" -> {
HorizontalDivider(
thickness = 3.dp,
modifier = Modifier.clip(CircleShape),
color = fontColor.copy(0.3f)
)
}
else -> {
BasicText(
text = line,
modifier = Modifier.then(
if (doubleClickTranslation && toolbarHidden) {
Modifier.noRippleClickable(
onDoubleClick = {
openTranslator(
ReaderEvent.OnOpenTranslator(
textToTranslate = line.text,
translateWholeParagraph = true,
activity = activity
)
)
},
onClick = {
menuVisibility(
ReaderEvent.OnMenuVisibility(
show = !showMenu,
fullscreenMode = fullscreenMode,
saveCheckpoint = true,
activity = activity
)
)
}
)
} else Modifier
),
style = TextStyle(
fontFamily = fontFamily.font,
textAlign = when (textAlignment) {
ReaderTextAlignment.START -> TextAlign.Start
ReaderTextAlignment.JUSTIFY -> TextAlign.Justify
ReaderTextAlignment.CENTER -> TextAlign.Center
ReaderTextAlignment.END -> TextAlign.End
},
textIndent = TextIndent(firstLine = paragraphIndentation),
fontStyle = fontStyle,
letterSpacing = letterSpacing,
fontSize = fontSize,
lineHeight = lineHeight,
color = fontColor,
lineBreak = LineBreak.Paragraph
)
)
}
}
}
}

View file

@ -0,0 +1,73 @@
package ua.acclorite.book_story.presentation.reader
import androidx.activity.ComponentActivity
import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.TextUnit
import ua.acclorite.book_story.domain.reader.FontWithName
import ua.acclorite.book_story.domain.reader.ReaderText
import ua.acclorite.book_story.domain.reader.ReaderTextAlignment
import ua.acclorite.book_story.ui.reader.ReaderEvent
@Composable
fun LazyItemScope.ReaderLayoutText(
activity: ComponentActivity,
showMenu: Boolean,
textEntry: ReaderText,
fontFamily: FontWithName,
fontColor: Color,
lineHeight: TextUnit,
fontStyle: FontStyle,
textAlignment: ReaderTextAlignment,
fontSize: TextUnit,
letterSpacing: TextUnit,
sidePadding: Dp,
paragraphIndentation: TextUnit,
fullscreenMode: Boolean,
doubleClickTranslation: Boolean,
toolbarHidden: Boolean,
openTranslator: (ReaderEvent.OnOpenTranslator) -> Unit,
menuVisibility: (ReaderEvent.OnMenuVisibility) -> Unit
) {
when (textEntry) {
is ReaderText.Separator -> {
ReaderLayoutTextSeparator(
sidePadding = sidePadding,
fontColor = fontColor
)
}
is ReaderText.Chapter -> {
ReaderLayoutTextChapter(
chapter = textEntry,
fontColor = fontColor,
sidePadding = sidePadding
)
}
is ReaderText.Text -> {
ReaderLayoutTextParagraph(
paragraph = textEntry,
activity = activity,
showMenu = showMenu,
fontFamily = fontFamily,
fontColor = fontColor,
lineHeight = lineHeight,
fontStyle = fontStyle,
textAlignment = textAlignment,
fontSize = fontSize,
letterSpacing = letterSpacing,
sidePadding = sidePadding,
paragraphIndentation = paragraphIndentation,
fullscreenMode = fullscreenMode,
doubleClickTranslation = doubleClickTranslation,
toolbarHidden = toolbarHidden,
openTranslator = openTranslator,
menuVisibility = menuVisibility
)
}
}
}

View file

@ -11,15 +11,14 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import ua.acclorite.book_story.domain.reader.Chapter import ua.acclorite.book_story.domain.reader.ReaderText.Chapter
@Composable @Composable
fun ReaderLayoutChapter( fun ReaderLayoutTextChapter(
chapter: Chapter?, chapter: Chapter,
fontColor: Color, fontColor: Color,
sidePadding: Dp sidePadding: Dp
) { ) {
chapter?.let {
Spacer(modifier = Modifier.height(22.dp)) Spacer(modifier = Modifier.height(22.dp))
Text( Text(
text = chapter.title, text = chapter.title,
@ -30,5 +29,4 @@ fun ReaderLayoutChapter(
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
HorizontalDivider(color = fontColor.copy(0.4f)) HorizontalDivider(color = fontColor.copy(0.4f))
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
}
} }

View file

@ -0,0 +1,104 @@
package ua.acclorite.book_story.presentation.reader
import androidx.activity.ComponentActivity
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.style.LineBreak
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextIndent
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.TextUnit
import ua.acclorite.book_story.domain.reader.FontWithName
import ua.acclorite.book_story.domain.reader.ReaderText.Text
import ua.acclorite.book_story.domain.reader.ReaderTextAlignment
import ua.acclorite.book_story.presentation.core.util.noRippleClickable
import ua.acclorite.book_story.ui.reader.ReaderEvent
@Composable
fun LazyItemScope.ReaderLayoutTextParagraph(
paragraph: Text,
activity: ComponentActivity,
showMenu: Boolean,
fontFamily: FontWithName,
fontColor: Color,
lineHeight: TextUnit,
fontStyle: FontStyle,
textAlignment: ReaderTextAlignment,
fontSize: TextUnit,
letterSpacing: TextUnit,
sidePadding: Dp,
paragraphIndentation: TextUnit,
fullscreenMode: Boolean,
doubleClickTranslation: Boolean,
toolbarHidden: Boolean,
openTranslator: (ReaderEvent.OnOpenTranslator) -> Unit,
menuVisibility: (ReaderEvent.OnMenuVisibility) -> Unit
) {
Column(
modifier = Modifier
.animateItem(fadeInSpec = null, fadeOutSpec = null)
.fillMaxWidth()
.padding(horizontal = sidePadding),
verticalArrangement = Arrangement.Center,
horizontalAlignment = when (textAlignment) {
ReaderTextAlignment.START, ReaderTextAlignment.JUSTIFY -> Alignment.Start
ReaderTextAlignment.CENTER -> Alignment.CenterHorizontally
ReaderTextAlignment.END -> Alignment.End
}
) {
BasicText(
text = paragraph.line,
modifier = Modifier.then(
if (doubleClickTranslation && toolbarHidden) {
Modifier.noRippleClickable(
onDoubleClick = {
openTranslator(
ReaderEvent.OnOpenTranslator(
textToTranslate = paragraph.line.text,
translateWholeParagraph = true,
activity = activity
)
)
},
onClick = {
menuVisibility(
ReaderEvent.OnMenuVisibility(
show = !showMenu,
fullscreenMode = fullscreenMode,
saveCheckpoint = true,
activity = activity
)
)
}
)
} else Modifier
),
style = TextStyle(
fontFamily = fontFamily.font,
textAlign = when (textAlignment) {
ReaderTextAlignment.START -> TextAlign.Start
ReaderTextAlignment.JUSTIFY -> TextAlign.Justify
ReaderTextAlignment.CENTER -> TextAlign.Center
ReaderTextAlignment.END -> TextAlign.End
},
textIndent = TextIndent(firstLine = paragraphIndentation),
fontStyle = fontStyle,
letterSpacing = letterSpacing,
fontSize = fontSize,
lineHeight = lineHeight,
color = fontColor,
lineBreak = LineBreak.Paragraph
)
)
}
}

View file

@ -0,0 +1,25 @@
package ua.acclorite.book_story.presentation.reader
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.HorizontalDivider
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
@Composable
fun ReaderLayoutTextSeparator(
sidePadding: Dp,
fontColor: Color
) {
HorizontalDivider(
thickness = 3.dp,
modifier = Modifier
.padding(horizontal = sidePadding)
.clip(CircleShape),
color = fontColor.copy(0.3f)
)
}

View file

@ -14,15 +14,15 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.TextUnit
import ua.acclorite.book_story.domain.library.book.Book import ua.acclorite.book_story.domain.library.book.Book
import ua.acclorite.book_story.domain.reader.Chapter
import ua.acclorite.book_story.domain.reader.Checkpoint import ua.acclorite.book_story.domain.reader.Checkpoint
import ua.acclorite.book_story.domain.reader.FontWithName import ua.acclorite.book_story.domain.reader.FontWithName
import ua.acclorite.book_story.domain.reader.ReaderHorizontalGesture import ua.acclorite.book_story.domain.reader.ReaderHorizontalGesture
import ua.acclorite.book_story.domain.reader.ReaderText
import ua.acclorite.book_story.domain.reader.ReaderText.Chapter
import ua.acclorite.book_story.domain.reader.ReaderTextAlignment import ua.acclorite.book_story.domain.reader.ReaderTextAlignment
import ua.acclorite.book_story.domain.ui.UIText import ua.acclorite.book_story.domain.ui.UIText
import ua.acclorite.book_story.presentation.core.components.common.AnimatedVisibility import ua.acclorite.book_story.presentation.core.components.common.AnimatedVisibility
@ -33,7 +33,7 @@ import ua.acclorite.book_story.ui.settings.SettingsEvent
@Composable @Composable
fun ReaderScaffold( fun ReaderScaffold(
book: Book, book: Book,
text: List<AnnotatedString>, text: List<ReaderText>,
listState: LazyListState, listState: LazyListState,
currentChapter: Chapter?, currentChapter: Chapter?,
nestedScrollConnection: NestedScrollConnection, nestedScrollConnection: NestedScrollConnection,
@ -47,7 +47,6 @@ fun ReaderScaffold(
checkpoint: Checkpoint, checkpoint: Checkpoint,
showMenu: Boolean, showMenu: Boolean,
lockMenu: Boolean, lockMenu: Boolean,
chapters: Map<Int, Chapter>,
contentPadding: PaddingValues, contentPadding: PaddingValues,
verticalPadding: Dp, verticalPadding: Dp,
horizontalGesture: ReaderHorizontalGesture, horizontalGesture: ReaderHorizontalGesture,
@ -136,7 +135,6 @@ fun ReaderScaffold(
) { ) {
ReaderLayout( ReaderLayout(
text = text, text = text,
chapters = chapters,
listState = listState, listState = listState,
contentPadding = contentPadding, contentPadding = contentPadding,
verticalPadding = verticalPadding, verticalPadding = verticalPadding,

View file

@ -25,7 +25,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.library.book.Book import ua.acclorite.book_story.domain.library.book.Book
import ua.acclorite.book_story.domain.reader.Chapter import ua.acclorite.book_story.domain.reader.ReaderText.Chapter
import ua.acclorite.book_story.presentation.core.components.common.IconButton import ua.acclorite.book_story.presentation.core.components.common.IconButton
import ua.acclorite.book_story.presentation.core.util.LocalActivity import ua.acclorite.book_story.presentation.core.util.LocalActivity
import ua.acclorite.book_story.presentation.core.util.noRippleClickable import ua.acclorite.book_story.presentation.core.util.noRippleClickable

View file

@ -3,6 +3,7 @@ package ua.acclorite.book_story.ui.reader
import android.content.Context import android.content.Context
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import ua.acclorite.book_story.domain.reader.ReaderText.Chapter
@Immutable @Immutable
sealed class ReaderEvent { sealed class ReaderEvent {
@ -25,7 +26,7 @@ sealed class ReaderEvent {
) : ReaderEvent() ) : ReaderEvent()
data class OnScrollToChapter( data class OnScrollToChapter(
val chapterStartIndex: Int val chapter: Chapter
) : ReaderEvent() ) : ReaderEvent()
data class OnScroll( data class OnScroll(

View file

@ -3,6 +3,7 @@ package ua.acclorite.book_story.ui.reader
import android.app.SearchManager import android.app.SearchManager
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.util.Log
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.runtime.snapshotFlow import androidx.compose.runtime.snapshotFlow
@ -28,8 +29,8 @@ import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
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.reader.Chapter
import ua.acclorite.book_story.domain.reader.Checkpoint import ua.acclorite.book_story.domain.reader.Checkpoint
import ua.acclorite.book_story.domain.reader.ReaderText.Chapter
import ua.acclorite.book_story.domain.ui.UIText import ua.acclorite.book_story.domain.ui.UIText
import ua.acclorite.book_story.domain.use_case.book.GetBookById import ua.acclorite.book_story.domain.use_case.book.GetBookById
import ua.acclorite.book_story.domain.use_case.book.GetText import ua.acclorite.book_story.domain.use_case.book.GetText
@ -44,6 +45,8 @@ import ua.acclorite.book_story.ui.library.LibraryScreen
import javax.inject.Inject import javax.inject.Inject
import kotlin.math.roundToInt import kotlin.math.roundToInt
private const val READER = "READER, MODEL"
@HiltViewModel @HiltViewModel
class ReaderModel @Inject constructor( class ReaderModel @Inject constructor(
private val getBookById: GetBookById, private val getBookById: GetBookById,
@ -67,20 +70,19 @@ class ReaderModel @Inject constructor(
when (event) { when (event) {
is ReaderEvent.OnLoadText -> { is ReaderEvent.OnLoadText -> {
launch(Dispatchers.IO) { launch(Dispatchers.IO) {
val chaptersAndText = getText.execute(_state.value.book.id) val text = getText.execute(_state.value.book.id)
yield() yield()
if (chaptersAndText.text.isEmpty()) { if (text.isEmpty()) {
_state.update { _state.update {
it.copy( it.copy(
isLoading = false, isLoading = false,
errorMessage = UIText.StringResource(R.string.error_no_text) //todo rename errorMessage = UIText.StringResource(R.string.error_no_text)
) )
} }
return@launch return@launch
} }
yield()
val lastOpened = getLatestHistory.execute(_state.value.book.id)?.time val lastOpened = getLatestHistory.execute(_state.value.book.id)?.time
yield() yield()
@ -89,8 +91,7 @@ class ReaderModel @Inject constructor(
book = it.book.copy( book = it.book.copy(
lastOpened = lastOpened lastOpened = lastOpened
), ),
chapters = chaptersAndText.chapters, text = text
text = chaptersAndText.text
) )
} }
@ -177,25 +178,30 @@ class ReaderModel @Inject constructor(
is ReaderEvent.OnScrollToChapter -> { is ReaderEvent.OnScrollToChapter -> {
launch { launch {
_state.value.listState.requestScrollToItem( _state.value.apply {
event.chapterStartIndex val chapterIndex = text.indexOf(event.chapter).takeIf { it != -1 }
)
updateChapter(index = event.chapterStartIndex) if (chapterIndex == null) {
return@launch
}
listState.requestScrollToItem(chapterIndex)
updateChapter(index = chapterIndex)
onEvent( onEvent(
ReaderEvent.OnChangeProgress( ReaderEvent.OnChangeProgress(
progress = calculateProgress(event.chapterStartIndex), progress = calculateProgress(chapterIndex),
firstVisibleItemIndex = event.chapterStartIndex, firstVisibleItemIndex = chapterIndex,
firstVisibleItemOffset = 0 firstVisibleItemOffset = 0
) )
) )
} }
} }
}
is ReaderEvent.OnScroll -> { is ReaderEvent.OnScroll -> {
scrollJob?.cancel() scrollJob?.cancel()
scrollJob = launch { scrollJob = launch {
delay(300) delay(300)
yield() yield()
val scrollTo = (_state.value.text.size * event.progress).roundToInt() val scrollTo = (_state.value.text.size * event.progress).roundToInt()
@ -211,6 +217,7 @@ class ReaderModel @Inject constructor(
checkpoint.offset checkpoint.offset
) )
updateChapter(checkpoint.index)
onEvent( onEvent(
ReaderEvent.OnChangeProgress( ReaderEvent.OnChangeProgress(
progress = calculateProgress(checkpoint.index), progress = calculateProgress(checkpoint.index),
@ -234,9 +241,9 @@ class ReaderModel @Inject constructor(
_state.value.listState.apply { _state.value.listState.apply {
if ( if (
_state.value.isLoading _state.value.isLoading ||
|| layoutInfo.totalItemsCount < 1 layoutInfo.totalItemsCount < 1 ||
|| _state.value.text.isEmpty() _state.value.text.isEmpty()
) return@apply ) return@apply
_state.update { _state.update {
@ -284,6 +291,8 @@ class ReaderModel @Inject constructor(
"translate: ${event.textToTranslate.trim()}" "translate: ${event.textToTranslate.trim()}"
) )
yield()
translatorIntent.launchActivity( translatorIntent.launchActivity(
activity = event.activity, activity = event.activity,
createChooser = !event.translateWholeParagraph, createChooser = !event.translateWholeParagraph,
@ -320,6 +329,8 @@ class ReaderModel @Inject constructor(
event.textToShare.trim() event.textToShare.trim()
) )
yield()
shareIntent.launchActivity( shareIntent.launchActivity(
activity = event.activity, activity = event.activity,
createChooser = true, createChooser = true,
@ -345,6 +356,8 @@ class ReaderModel @Inject constructor(
event.textToSearch event.textToSearch
) )
yield()
browserIntent.launchActivity( browserIntent.launchActivity(
activity = event.activity, activity = event.activity,
success = { success = {
@ -376,6 +389,8 @@ class ReaderModel @Inject constructor(
val text = event.textToDefine.trim().replace(" ", "+") val text = event.textToDefine.trim().replace(" ", "+")
browserIntent.data = Uri.parse("https://www.onelook.com/?w=$text") browserIntent.data = Uri.parse("https://www.onelook.com/?w=$text")
yield()
dictionaryIntent.launchActivity( dictionaryIntent.launchActivity(
activity = event.activity, activity = event.activity,
createChooser = true, createChooser = true,
@ -485,6 +500,10 @@ class ReaderModel @Inject constructor(
if (progress == _state.value.book.progress) return@collectLatest if (progress == _state.value.book.progress) return@collectLatest
val (currentChapter, currentChapterProgress) = calculateCurrentChapter(index) val (currentChapter, currentChapterProgress) = calculateCurrentChapter(index)
Log.i(
READER,
"Changed progress|currentChapter: $progress; ${currentChapter?.title}"
)
_state.update { _state.update {
it.copy( it.copy(
book = it.book.copy( book = it.book.copy(
@ -509,6 +528,11 @@ class ReaderModel @Inject constructor(
viewModelScope.launch { viewModelScope.launch {
val (currentChapter, currentChapterProgress) = calculateCurrentChapter(index) val (currentChapter, currentChapterProgress) = calculateCurrentChapter(index)
_state.update { _state.update {
Log.i(
READER,
"Changed currentChapter|currentChapterProgress:" +
" ${currentChapter?.title}($currentChapterProgress)"
)
it.copy( it.copy(
currentChapter = currentChapter, currentChapter = currentChapter,
currentChapterProgress = currentChapterProgress currentChapterProgress = currentChapterProgress
@ -518,20 +542,33 @@ class ReaderModel @Inject constructor(
} }
private fun calculateCurrentChapter(index: Int): Pair<Chapter?, Float> { private fun calculateCurrentChapter(index: Int): Pair<Chapter?, Float> {
val currentChapter = _state.value.chapters.find { chapter -> val currentChapter = findCurrentChapter(index)
index in chapter.startIndex..chapter.endIndex val currentChapterProgress = currentChapter?.let { chapter ->
} _state.value.text.run {
val currentChapterProgress = currentChapter.run { val startIndex = (indexOf(chapter) + 1).coerceAtMost(count())
if (this == null) return@run 0f val endIndex = (indexOfFirst {
it is Chapter && indexOf(it) > startIndex
}.takeIf { it != -1 }?.minus(1)) ?: lastIndex
val currentIndex = index - startIndex val currentIndexInChapter = index - startIndex
val endIndex = endIndex - startIndex val chapterLength = endIndex - startIndex
(currentIndex / endIndex.toFloat()) (currentIndexInChapter / chapterLength.toFloat())
}
}.coerceAndPreventNaN() }.coerceAndPreventNaN()
return currentChapter to currentChapterProgress return currentChapter to currentChapterProgress
} }
private fun findCurrentChapter(index: Int): Chapter? {
for (textIndex in index downTo 0) {
val readerText = _state.value.text[textIndex]
if (readerText is Chapter) {
return readerText
}
}
return null
}
private fun calculateProgress(firstVisibleItemIndex: Int? = null): Float { private fun calculateProgress(firstVisibleItemIndex: Int? = null): Float {
return _state.value.run { return _state.value.run {
if ( if (

View file

@ -34,7 +34,6 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import ua.acclorite.book_story.domain.navigator.Screen import ua.acclorite.book_story.domain.navigator.Screen
import ua.acclorite.book_story.domain.reader.Chapter
import ua.acclorite.book_story.domain.reader.ReaderTextAlignment import ua.acclorite.book_story.domain.reader.ReaderTextAlignment
import ua.acclorite.book_story.presentation.core.constants.Constants import ua.acclorite.book_story.presentation.core.constants.Constants
import ua.acclorite.book_story.presentation.core.constants.provideFonts import ua.acclorite.book_story.presentation.core.constants.provideFonts
@ -224,14 +223,6 @@ data class ReaderScreen(val bookId: Int) : Screen, Parcelable {
(mainState.value.bottomBarPadding * 4f).dp (mainState.value.bottomBarPadding * 4f).dp
} }
val chapters = remember(state.value.chapters) {
val chapters = mutableMapOf<Int, Chapter>()
state.value.chapters.forEach {
chapters[it.startIndex] = it
}
chapters
}
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
screenModel.init( screenModel.init(
bookId = bookId, bookId = bookId,
@ -313,7 +304,6 @@ data class ReaderScreen(val bookId: Int) : Screen, Parcelable {
checkpoint = state.value.checkpoint, checkpoint = state.value.checkpoint,
showMenu = state.value.showMenu, showMenu = state.value.showMenu,
lockMenu = state.value.lockMenu, lockMenu = state.value.lockMenu,
chapters = chapters,
contentPadding = contentPadding, contentPadding = contentPadding,
verticalPadding = verticalPadding, verticalPadding = verticalPadding,
horizontalGesture = mainState.value.horizontalGesture, horizontalGesture = mainState.value.horizontalGesture,

View file

@ -2,10 +2,10 @@ package ua.acclorite.book_story.ui.reader
import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import androidx.compose.ui.text.AnnotatedString
import ua.acclorite.book_story.domain.library.book.Book import ua.acclorite.book_story.domain.library.book.Book
import ua.acclorite.book_story.domain.reader.Chapter
import ua.acclorite.book_story.domain.reader.Checkpoint import ua.acclorite.book_story.domain.reader.Checkpoint
import ua.acclorite.book_story.domain.reader.ReaderText
import ua.acclorite.book_story.domain.reader.ReaderText.Chapter
import ua.acclorite.book_story.domain.ui.UIText import ua.acclorite.book_story.domain.ui.UIText
import ua.acclorite.book_story.domain.util.BottomSheet import ua.acclorite.book_story.domain.util.BottomSheet
import ua.acclorite.book_story.domain.util.Drawer import ua.acclorite.book_story.domain.util.Drawer
@ -15,8 +15,7 @@ import ua.acclorite.book_story.presentation.core.constants.provideEmptyBook
@Immutable @Immutable
data class ReaderState( data class ReaderState(
val book: Book = Constants.provideEmptyBook(), val book: Book = Constants.provideEmptyBook(),
val chapters: List<Chapter> = emptyList(), val text: List<ReaderText> = emptyList(),
val text: List<AnnotatedString> = emptyList(),
val listState: LazyListState = LazyListState(), val listState: LazyListState = LazyListState(),
val currentChapter: Chapter? = null, val currentChapter: Chapter? = null,