🚀 Reformat parsers
* Reformatted parsers * Direct parsing * Scalable * Slightly faster parsing (no optimizations yet) Part-of: #134
This commit is contained in:
parent
3853727ae7
commit
fb39d4fe15
32 changed files with 594 additions and 528 deletions
|
|
@ -1,9 +1,8 @@
|
|||
package ua.acclorite.book_story.data.parser
|
||||
|
||||
import ua.acclorite.book_story.domain.reader.ChapterWithText
|
||||
import ua.acclorite.book_story.domain.util.Resource
|
||||
import ua.acclorite.book_story.domain.reader.ReaderText
|
||||
import java.io.File
|
||||
|
||||
interface TextParser {
|
||||
suspend fun parse(file: File): Resource<List<ChapterWithText>>
|
||||
suspend fun parse(file: File): List<ReaderText>
|
||||
}
|
||||
|
|
@ -1,15 +1,12 @@
|
|||
package ua.acclorite.book_story.data.parser
|
||||
|
||||
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.fb2.Fb2TextParser
|
||||
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.txt.TxtTextParser
|
||||
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.domain.reader.ReaderText
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
@ -20,14 +17,12 @@ class TextParserImpl @Inject constructor(
|
|||
private val pdfTextParser: PdfTextParser,
|
||||
private val epubTextParser: EpubTextParser,
|
||||
private val fb2TextParser: Fb2TextParser,
|
||||
private val htmlTextParser: HtmlTextParser,
|
||||
private val htmlTextParser: HtmlTextParser
|
||||
) : TextParser {
|
||||
override suspend fun parse(file: File): Resource<List<ChapterWithText>> {
|
||||
override suspend fun parse(file: File): List<ReaderText> {
|
||||
if (!file.exists()) {
|
||||
Log.e(TEXT_PARSER, "File does not exist.")
|
||||
return Resource.Error(
|
||||
UIText.StringResource(R.string.error_something_went_wrong_with_file)
|
||||
)
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
val fileFormat = ".${file.extension}".lowercase().trim()
|
||||
|
|
@ -66,7 +61,7 @@ class TextParserImpl @Inject constructor(
|
|||
|
||||
else -> {
|
||||
Log.e(TEXT_PARSER, "Wrong file format, could not find supported extension.")
|
||||
Resource.Error(UIText.StringResource(R.string.error_wrong_file_format))
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,14 +12,12 @@ import kotlinx.coroutines.coroutineScope
|
|||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.yield
|
||||
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.MarkdownParser
|
||||
import ua.acclorite.book_story.data.parser.TextParser
|
||||
import ua.acclorite.book_story.domain.reader.Chapter
|
||||
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.domain.reader.ReaderText
|
||||
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 java.io.File
|
||||
import java.util.concurrent.ConcurrentLinkedQueue
|
||||
|
|
@ -34,16 +32,16 @@ private typealias Title = String
|
|||
private val dispatcher = Dispatchers.IO.limitedParallelism(2)
|
||||
|
||||
class EpubTextParser @Inject constructor(
|
||||
private val markdownParser: MarkdownParser,
|
||||
private val documentParser: DocumentParser
|
||||
) : 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}.")
|
||||
|
||||
return try {
|
||||
val chapters = mutableListOf<ChapterWithText>()
|
||||
|
||||
yield()
|
||||
var readerText = listOf<ReaderText>()
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
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, "Title entries, size: ${chapterTitleEntries?.size}")
|
||||
|
||||
zip.parseEpub(
|
||||
readerText = zip.parseEpub(
|
||||
chapterEntries = chapterEntries,
|
||||
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()
|
||||
|
||||
if (chapters.isEmpty()) {
|
||||
return Resource.Error(UIText.StringResource(R.string.error_file_empty))
|
||||
if (
|
||||
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.")
|
||||
Resource.Success(chapters)
|
||||
readerText
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Resource.Error(
|
||||
UIText.StringResource(
|
||||
R.string.error_query,
|
||||
e.message?.take(40)?.trim() ?: ""
|
||||
)
|
||||
)
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -107,18 +98,18 @@ class EpubTextParser @Inject constructor(
|
|||
private suspend fun ZipFile.parseEpub(
|
||||
chapterEntries: List<ZipEntry>,
|
||||
chapterTitleEntries: Map<Title, List<String>>?
|
||||
): List<ChapterWithText>? {
|
||||
): List<ReaderText> {
|
||||
|
||||
val chapters = mutableListOf<ChapterWithText>()
|
||||
val readerText = mutableListOf<ReaderText>()
|
||||
coroutineScope {
|
||||
val unformattedChapters = ConcurrentLinkedQueue<ChapterWithText>()
|
||||
val unformattedText = ConcurrentLinkedQueue<Pair<Int, List<ReaderText>>>()
|
||||
|
||||
// Asynchronously getting all chapters with text
|
||||
val jobs = chapterEntries.mapIndexed { index, entry ->
|
||||
async(dispatcher) {
|
||||
yield()
|
||||
|
||||
unformattedChapters.parseZipEntry(
|
||||
unformattedText.parseZipEntry(
|
||||
zip = this@parseEpub,
|
||||
index = index,
|
||||
entry = entry,
|
||||
|
|
@ -131,27 +122,15 @@ class EpubTextParser @Inject constructor(
|
|||
jobs.awaitAll()
|
||||
|
||||
// Sorting chapters in correct order
|
||||
chapters.addAll {
|
||||
var textIndex = -1
|
||||
unformattedChapters.toList()
|
||||
.sortedBy { it.chapter.index }
|
||||
.mapIndexed { index, item ->
|
||||
item.copy(
|
||||
chapter = item.chapter.copy(
|
||||
index = index,
|
||||
startIndex = textIndex + 1,
|
||||
endIndex = textIndex + item.text.size
|
||||
)
|
||||
).also { textIndex += item.text.size }
|
||||
}
|
||||
readerText.addAll {
|
||||
unformattedText.toList()
|
||||
.sortedBy { (index, _) -> index }
|
||||
.map { it.second }
|
||||
.flatten()
|
||||
}
|
||||
}
|
||||
|
||||
if (chapters.isEmpty()) {
|
||||
return null
|
||||
}
|
||||
|
||||
return chapters
|
||||
return readerText
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -163,7 +142,7 @@ class EpubTextParser @Inject constructor(
|
|||
* @param entry [ZipEntry].
|
||||
* @param chapterTitleMap Titles from [getChapterTitleMapFromToc].
|
||||
*/
|
||||
private suspend fun ConcurrentLinkedQueue<ChapterWithText>.parseZipEntry(
|
||||
private suspend fun ConcurrentLinkedQueue<Pair<Int, List<ReaderText>>>.parseZipEntry(
|
||||
zip: ZipFile,
|
||||
index: Int,
|
||||
entry: ZipEntry,
|
||||
|
|
@ -171,46 +150,67 @@ class EpubTextParser @Inject constructor(
|
|||
) {
|
||||
// Getting all text
|
||||
val content = zip.getInputStream(entry).bufferedReader().use { it.readText() }
|
||||
var chapter = documentParser.run {
|
||||
var text = documentParser.run {
|
||||
Jsoup.parse(content).parseDocument()
|
||||
}
|
||||
val readerText = mutableListOf<ReaderText>()
|
||||
|
||||
if (chapter.isEmpty()) {
|
||||
Log.w(EPUB_TAG, "Chapter ${entry.name} is empty.")
|
||||
return
|
||||
}
|
||||
|
||||
// Getting title and removing first line (if matches title)
|
||||
val chapterTitle = getChapterTitleFromToc(
|
||||
// Adding chapter title from TOC if found
|
||||
var chapterAdded = false
|
||||
getChapterTitleFromToc(
|
||||
chapterSource = entry.name,
|
||||
chapterTitleMap = chapterTitleMap
|
||||
).run {
|
||||
if (this != null) {
|
||||
return@run this
|
||||
}
|
||||
chapter.first().clearMarkdown()
|
||||
}.also { title ->
|
||||
chapter = chapter.dropWhile { line ->
|
||||
line.clearMarkdown().lowercase() == title.lowercase()
|
||||
).apply {
|
||||
if (this == null) return@apply
|
||||
readerText.add(
|
||||
ReaderText.Chapter(
|
||||
title = this
|
||||
)
|
||||
)
|
||||
chapterAdded = true
|
||||
|
||||
text = text.dropWhile { line ->
|
||||
line.clearMarkdown().lowercase() == this.lowercase()
|
||||
}
|
||||
}
|
||||
|
||||
if (chapter.isEmpty()) {
|
||||
Log.w(EPUB_TAG, "Chapter ${entry.name} is empty.")
|
||||
// Format and add text
|
||||
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
|
||||
}
|
||||
|
||||
add(
|
||||
ChapterWithText(
|
||||
Chapter(
|
||||
index = index,
|
||||
title = chapterTitle,
|
||||
startIndex = 0,
|
||||
endIndex = 0
|
||||
),
|
||||
text = chapter
|
||||
)
|
||||
)
|
||||
add(index to readerText)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -6,12 +6,9 @@ import kotlinx.coroutines.withContext
|
|||
import kotlinx.coroutines.yield
|
||||
import org.w3c.dom.Element
|
||||
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.domain.reader.Chapter
|
||||
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.domain.reader.ReaderText
|
||||
import ua.acclorite.book_story.presentation.core.util.clearAllMarkdown
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
|
|
@ -19,9 +16,11 @@ import javax.xml.parsers.DocumentBuilderFactory
|
|||
|
||||
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}.")
|
||||
|
||||
return try {
|
||||
|
|
@ -31,13 +30,11 @@ class Fb2TextParser @Inject constructor() : TextParser {
|
|||
builder.parse(file)
|
||||
}
|
||||
|
||||
val formattedLines = mutableListOf<String>()
|
||||
val readerText = mutableListOf<ReaderText>()
|
||||
|
||||
val bodyNodes = document.getElementsByTagName("body")
|
||||
if (bodyNodes.length == 0) {
|
||||
return Resource.Error(
|
||||
UIText.StringResource(R.string.error_file_empty)
|
||||
)
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
yield()
|
||||
|
|
@ -108,45 +105,49 @@ class Fb2TextParser @Inject constructor() : TextParser {
|
|||
|
||||
yield()
|
||||
|
||||
var chapterAdded = false
|
||||
lines.forEach { line ->
|
||||
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()
|
||||
|
||||
if (formattedLines.size < 2) {
|
||||
return Resource.Error(UIText.StringResource(R.string.error_file_empty))
|
||||
}
|
||||
|
||||
val title = formattedLines.first().clearAllMarkdown().let { title ->
|
||||
formattedLines.removeAt(0)
|
||||
if (title.isBlank()) return@let "Chapter 1"
|
||||
return@let title
|
||||
if (
|
||||
readerText.filterIsInstance<ReaderText.Text>().isEmpty() ||
|
||||
readerText.filterIsInstance<ReaderText.Chapter>().isEmpty()
|
||||
) {
|
||||
Log.e(FB2_TAG, "Could not extract text from FB2.")
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
Log.i(FB2_TAG, "Successfully finished FB2 parsing.")
|
||||
Resource.Success(
|
||||
listOf(
|
||||
ChapterWithText(
|
||||
chapter = Chapter(
|
||||
index = 0,
|
||||
title = title,
|
||||
startIndex = 0,
|
||||
endIndex = formattedLines.lastIndex
|
||||
),
|
||||
text = formattedLines
|
||||
)
|
||||
)
|
||||
)
|
||||
readerText
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Resource.Error(
|
||||
UIText.StringResource(
|
||||
R.string.error_query,
|
||||
e.message?.take(40)?.trim() ?: ""
|
||||
)
|
||||
)
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,13 +3,10 @@ package ua.acclorite.book_story.data.parser.html
|
|||
import android.util.Log
|
||||
import kotlinx.coroutines.yield
|
||||
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.MarkdownParser
|
||||
import ua.acclorite.book_story.data.parser.TextParser
|
||||
import ua.acclorite.book_story.domain.reader.Chapter
|
||||
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.domain.reader.ReaderText
|
||||
import ua.acclorite.book_story.presentation.core.util.clearAllMarkdown
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
|
|
@ -17,52 +14,62 @@ import javax.inject.Inject
|
|||
private const val HTML_TAG = "HTML Parser"
|
||||
|
||||
class HtmlTextParser @Inject constructor(
|
||||
private val markdownParser: MarkdownParser,
|
||||
private val documentParser: DocumentParser
|
||||
) : 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}.")
|
||||
|
||||
return try {
|
||||
val lines = documentParser.run {
|
||||
var chapterAdded = false
|
||||
val documentLines = documentParser.run {
|
||||
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()
|
||||
|
||||
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.")
|
||||
return Resource.Error(UIText.StringResource(R.string.error_file_empty))
|
||||
}
|
||||
|
||||
val title = lines.first().clearAllMarkdown().let { title ->
|
||||
lines.removeAt(0)
|
||||
if (title.isBlank()) return@let "Chapter 1"
|
||||
return@let title
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
Log.i(HTML_TAG, "Successfully finished HTML parsing.")
|
||||
Resource.Success(
|
||||
listOf(
|
||||
ChapterWithText(
|
||||
chapter = Chapter(
|
||||
index = 0,
|
||||
title = title,
|
||||
startIndex = 0,
|
||||
endIndex = lines.lastIndex
|
||||
),
|
||||
text = lines
|
||||
)
|
||||
)
|
||||
)
|
||||
readerText
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Resource.Error(
|
||||
UIText.StringResource(
|
||||
R.string.error_query,
|
||||
e.message?.take(40)?.trim() ?: ""
|
||||
)
|
||||
)
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -6,12 +6,9 @@ import com.tom_roush.pdfbox.android.PDFBoxResourceLoader
|
|||
import com.tom_roush.pdfbox.pdmodel.PDDocument
|
||||
import com.tom_roush.pdfbox.text.PDFTextStripper
|
||||
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.domain.reader.Chapter
|
||||
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.domain.reader.ReaderText
|
||||
import ua.acclorite.book_story.presentation.core.util.clearAllMarkdown
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
|
|
@ -19,10 +16,11 @@ import javax.inject.Inject
|
|||
private const val PDF_TAG = "PDF Parser"
|
||||
|
||||
class PdfTextParser @Inject constructor(
|
||||
private val markdownParser: MarkdownParser,
|
||||
private val application: Application
|
||||
) : 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}.")
|
||||
|
||||
return try {
|
||||
|
|
@ -44,7 +42,7 @@ class PdfTextParser @Inject constructor(
|
|||
|
||||
yield()
|
||||
|
||||
val strings = mutableListOf<String>()
|
||||
val readerText = mutableListOf<ReaderText>()
|
||||
val text = oldText.filterIndexed { index, c ->
|
||||
yield()
|
||||
|
||||
|
|
@ -110,45 +108,49 @@ class PdfTextParser @Inject constructor(
|
|||
|
||||
yield()
|
||||
|
||||
var chapterAdded = false
|
||||
lines.forEach { line ->
|
||||
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()
|
||||
|
||||
if (strings.size < 2) {
|
||||
return Resource.Error(UIText.StringResource(R.string.error_file_empty))
|
||||
}
|
||||
|
||||
val title = strings.first().clearAllMarkdown().let { title ->
|
||||
strings.removeAt(0)
|
||||
if (title.isBlank()) return@let "Chapter 1"
|
||||
return@let title
|
||||
if (
|
||||
readerText.filterIsInstance<ReaderText.Text>().isEmpty() ||
|
||||
readerText.filterIsInstance<ReaderText.Chapter>().isEmpty()
|
||||
) {
|
||||
Log.e(PDF_TAG, "Could not extract text from PDF.")
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
Log.i(PDF_TAG, "Successfully finished PDF parsing.")
|
||||
Resource.Success(
|
||||
listOf(
|
||||
ChapterWithText(
|
||||
chapter = Chapter(
|
||||
index = 0,
|
||||
title = title,
|
||||
startIndex = 0,
|
||||
endIndex = strings.lastIndex
|
||||
),
|
||||
text = strings
|
||||
)
|
||||
)
|
||||
)
|
||||
readerText
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Resource.Error(
|
||||
UIText.StringResource(
|
||||
R.string.error_query,
|
||||
e.message?.take(40)?.trim() ?: ""
|
||||
)
|
||||
)
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,12 +4,9 @@ import android.util.Log
|
|||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
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.domain.reader.Chapter
|
||||
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.domain.reader.ReaderText
|
||||
import ua.acclorite.book_story.presentation.core.util.clearAllMarkdown
|
||||
import java.io.BufferedReader
|
||||
import java.io.File
|
||||
|
|
@ -18,58 +15,59 @@ import javax.inject.Inject
|
|||
|
||||
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}.")
|
||||
|
||||
return try {
|
||||
val lines = mutableListOf<String>()
|
||||
val readerText = mutableListOf<ReaderText>()
|
||||
var chapterAdded = false
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
BufferedReader(FileReader(file)).forEachLine { line ->
|
||||
if (line.isNotBlank()) {
|
||||
lines.add(
|
||||
line.trim()
|
||||
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()
|
||||
|
||||
if (lines.size < 2) {
|
||||
return Resource.Error(UIText.StringResource(R.string.error_file_empty))
|
||||
}
|
||||
|
||||
val title = lines.first().clearAllMarkdown().let { title ->
|
||||
lines.removeAt(0)
|
||||
if (title.isBlank()) return@let "Chapter 1"
|
||||
return@let title
|
||||
if (
|
||||
readerText.filterIsInstance<ReaderText.Text>().isEmpty() ||
|
||||
readerText.filterIsInstance<ReaderText.Chapter>().isEmpty()
|
||||
) {
|
||||
Log.e(TXT_TAG, "Could not extract text from TXT.")
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
Log.i(TXT_TAG, "Successfully finished TXT parsing.")
|
||||
Resource.Success(
|
||||
listOf(
|
||||
ChapterWithText(
|
||||
chapter = Chapter(
|
||||
index = 0,
|
||||
title = title,
|
||||
startIndex = 0,
|
||||
endIndex = lines.lastIndex
|
||||
),
|
||||
text = lines
|
||||
)
|
||||
)
|
||||
)
|
||||
readerText
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Resource.Error(
|
||||
UIText.StringResource(
|
||||
R.string.error_query,
|
||||
e.message?.take(40)?.trim() ?: ""
|
||||
)
|
||||
)
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -6,20 +6,17 @@ import android.graphics.BitmapFactory
|
|||
import android.net.Uri
|
||||
import android.provider.MediaStore
|
||||
import android.util.Log
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import ua.acclorite.book_story.data.local.room.BookDao
|
||||
import ua.acclorite.book_story.data.mapper.book.BookMapper
|
||||
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.domain.library.book.Book
|
||||
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.util.CoverImage
|
||||
import ua.acclorite.book_story.domain.util.Resource
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
|
|
@ -45,8 +42,7 @@ class BookRepositoryImpl @Inject constructor(
|
|||
private val bookMapper: BookMapper,
|
||||
|
||||
private val fileParser: FileParser,
|
||||
private val textParser: TextParser,
|
||||
private val markdownParser: MarkdownParser
|
||||
private val textParser: TextParser
|
||||
) : 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 {
|
||||
if (bookId == -1) return ChaptersAndText(chapters = emptyList(), text = emptyList())
|
||||
override suspend fun getBookText(bookId: Int): List<ReaderText> {
|
||||
if (bookId == -1) return emptyList()
|
||||
|
||||
val book = database.findBookById(bookId)
|
||||
val file = File(book.filePath)
|
||||
|
||||
if (!file.exists()) {
|
||||
Log.e(GET_TEXT, "File [$bookId] does not exist")
|
||||
return ChaptersAndText(chapters = emptyList(), text = emptyList())
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
val parsedText = textParser.parse(file)
|
||||
if (parsedText is Resource.Error) {
|
||||
Log.e(GET_TEXT, "Failed to load text: $bookId")
|
||||
return ChaptersAndText(chapters = emptyList(), text = emptyList())
|
||||
}
|
||||
val readerText = textParser.parse(file)
|
||||
|
||||
val chapters = parsedText.data!!.map { it.chapter }
|
||||
val markdownLines = mutableListOf<AnnotatedString>()
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
for (line in parsedText.data.map { it.text }.flatten()) {
|
||||
if (line.isNotBlank()) {
|
||||
markdownLines.add(
|
||||
markdownParser.parse(line.trim())
|
||||
)
|
||||
}
|
||||
}
|
||||
if (
|
||||
readerText.filterIsInstance<ReaderText.Text>().isEmpty() ||
|
||||
readerText.filterIsInstance<ReaderText.Chapter>().isEmpty()
|
||||
) {
|
||||
Log.e(GET_TEXT, "Could not load text from [$bookId].")
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
Log.i(GET_TEXT, "Successfully loaded text of [$bookId] with markdown.")
|
||||
return ChaptersAndText(
|
||||
chapters = chapters,
|
||||
text = markdownLines
|
||||
)
|
||||
return readerText
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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>
|
||||
)
|
||||
|
|
@ -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>
|
||||
)
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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.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
|
||||
|
||||
interface BookRepository {
|
||||
|
|
@ -17,7 +17,7 @@ interface BookRepository {
|
|||
|
||||
suspend fun getBookText(
|
||||
bookId: Int
|
||||
): ChaptersAndText
|
||||
): List<ReaderText>
|
||||
|
||||
suspend fun insertBook(
|
||||
bookWithCover: BookWithCover
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
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 javax.inject.Inject
|
||||
|
||||
|
|
@ -8,7 +8,7 @@ class GetText @Inject constructor(
|
|||
private val repository: BookRepository
|
||||
) {
|
||||
|
||||
suspend fun execute(bookId: Int): ChaptersAndText {
|
||||
suspend fun execute(bookId: Int): List<ReaderText> {
|
||||
return repository.getBookText(bookId = bookId)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -18,7 +18,8 @@ fun Float.calculateProgress(digits: Int): String {
|
|||
.dropWhile { it == '-' }
|
||||
}
|
||||
|
||||
fun Float.coerceAndPreventNaN(): Float {
|
||||
fun Float?.coerceAndPreventNaN(): Float {
|
||||
if (this == null) return 0f
|
||||
if (isNaN()) return 0f
|
||||
return this.coerceIn(0f, 1f)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,13 +22,13 @@ import androidx.compose.runtime.derivedStateOf
|
|||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import ua.acclorite.book_story.R
|
||||
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.ReaderText
|
||||
import ua.acclorite.book_story.domain.reader.ReaderText.Chapter
|
||||
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.util.calculateProgress
|
||||
|
|
@ -40,7 +40,7 @@ import ua.acclorite.book_story.ui.theme.HorizontalExpandingTransition
|
|||
@Composable
|
||||
fun ReaderBottomBar(
|
||||
book: Book,
|
||||
text: List<AnnotatedString>,
|
||||
text: List<ReaderText>,
|
||||
listState: LazyListState,
|
||||
lockMenu: Boolean,
|
||||
currentChapter: Chapter?,
|
||||
|
|
|
|||
|
|
@ -2,16 +2,18 @@ package ua.acclorite.book_story.presentation.reader
|
|||
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
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.runtime.Composable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
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.ModalDrawerSelectableItem
|
||||
import ua.acclorite.book_story.presentation.core.components.modal_drawer.ModalDrawerTitleItem
|
||||
|
|
@ -27,9 +29,15 @@ fun ReaderChaptersDrawer(
|
|||
scrollToChapter: (ReaderEvent.OnScrollToChapter) -> Unit,
|
||||
dismissDrawer: (ReaderEvent.OnDismissDrawer) -> Unit
|
||||
) {
|
||||
val currentChapterIndex = remember(chapters, currentChapter) {
|
||||
derivedStateOf {
|
||||
chapters.indexOf(currentChapter).takeIf { it != -1 } ?: 0
|
||||
}
|
||||
}
|
||||
|
||||
ModalDrawer(
|
||||
show = show,
|
||||
startIndex = currentChapter?.index ?: 0,
|
||||
startIndex = currentChapterIndex.value,
|
||||
onDismissRequest = { dismissDrawer(ReaderEvent.OnDismissDrawer) },
|
||||
header = {
|
||||
ModalDrawerTitleItem(
|
||||
|
|
@ -37,9 +45,9 @@ fun ReaderChaptersDrawer(
|
|||
)
|
||||
}
|
||||
) {
|
||||
items(chapters, key = { it.index }) { chapter ->
|
||||
val selected = rememberSaveable(currentChapter) {
|
||||
chapter.index == currentChapter?.index
|
||||
itemsIndexed(chapters, key = { index, _ -> index }) { index, chapter ->
|
||||
val selected = rememberSaveable(index, currentChapterIndex) {
|
||||
index == currentChapterIndex.value
|
||||
}
|
||||
|
||||
ModalDrawerSelectableItem(
|
||||
|
|
@ -47,7 +55,7 @@ fun ReaderChaptersDrawer(
|
|||
onClick = {
|
||||
scrollToChapter(
|
||||
ReaderEvent.OnScrollToChapter(
|
||||
chapterStartIndex = chapter.startIndex
|
||||
chapter = chapter
|
||||
)
|
||||
)
|
||||
dismissDrawer(ReaderEvent.OnDismissDrawer)
|
||||
|
|
|
|||
|
|
@ -3,17 +3,18 @@ package ua.acclorite.book_story.presentation.reader
|
|||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
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.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.FontWithName
|
||||
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.ui.UIText
|
||||
import ua.acclorite.book_story.domain.util.BottomSheet
|
||||
|
|
@ -24,7 +25,7 @@ import ua.acclorite.book_story.ui.settings.SettingsEvent
|
|||
@Composable
|
||||
fun ReaderContent(
|
||||
book: Book,
|
||||
text: List<AnnotatedString>,
|
||||
text: List<ReaderText>,
|
||||
bottomSheet: BottomSheet?,
|
||||
drawer: Drawer?,
|
||||
listState: LazyListState,
|
||||
|
|
@ -40,7 +41,6 @@ fun ReaderContent(
|
|||
checkpoint: Checkpoint,
|
||||
showMenu: Boolean,
|
||||
lockMenu: Boolean,
|
||||
chapters: Map<Int, Chapter>,
|
||||
contentPadding: PaddingValues,
|
||||
verticalPadding: Dp,
|
||||
horizontalGesture: ReaderHorizontalGesture,
|
||||
|
|
@ -102,7 +102,6 @@ fun ReaderContent(
|
|||
checkpoint = checkpoint,
|
||||
showMenu = showMenu,
|
||||
lockMenu = lockMenu,
|
||||
chapters = chapters,
|
||||
contentPadding = contentPadding,
|
||||
verticalPadding = verticalPadding,
|
||||
horizontalGesture = horizontalGesture,
|
||||
|
|
@ -141,7 +140,7 @@ fun ReaderContent(
|
|||
|
||||
ReaderDrawer(
|
||||
drawer = drawer,
|
||||
chapters = chapters.values.toList(),
|
||||
chapters = remember(text) { text.filterIsInstance<Chapter>() },
|
||||
currentChapter = currentChapter,
|
||||
currentChapterProgress = currentChapterProgress,
|
||||
scrollToChapter = scrollToChapter,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package ua.acclorite.book_story.presentation.reader
|
||||
|
||||
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.ui.reader.ReaderEvent
|
||||
import ua.acclorite.book_story.ui.reader.ReaderScreen
|
||||
|
|
|
|||
|
|
@ -14,16 +14,15 @@ import androidx.compose.foundation.lazy.itemsIndexed
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import androidx.compose.ui.unit.coerceAtLeast
|
||||
import androidx.compose.ui.unit.dp
|
||||
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.ReaderHorizontalGesture
|
||||
import ua.acclorite.book_story.domain.reader.ReaderText
|
||||
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.SelectionContainer
|
||||
|
|
@ -34,8 +33,7 @@ import ua.acclorite.book_story.ui.reader.ReaderEvent
|
|||
|
||||
@Composable
|
||||
fun ReaderLayout(
|
||||
text: List<AnnotatedString>,
|
||||
chapters: Map<Int, Chapter>,
|
||||
text: List<ReaderText>,
|
||||
listState: LazyListState,
|
||||
contentPadding: PaddingValues,
|
||||
verticalPadding: Dp,
|
||||
|
|
@ -148,17 +146,11 @@ fun ReaderLayout(
|
|||
) {
|
||||
itemsIndexed(
|
||||
text, key = { index, _ -> index }
|
||||
) { index, line ->
|
||||
ReaderLayoutChapter(
|
||||
chapter = chapters[index], // Shows only when matching startIndex of chapter.
|
||||
fontColor = fontColor,
|
||||
sidePadding = sidePadding
|
||||
)
|
||||
|
||||
ReaderLayoutParagraph(
|
||||
) { index, textEntry ->
|
||||
ReaderLayoutText(
|
||||
activity = activity,
|
||||
showMenu = showMenu,
|
||||
line = line,
|
||||
textEntry = textEntry,
|
||||
fontFamily = fontFamily,
|
||||
fontColor = fontColor,
|
||||
lineHeight = lineHeight,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -11,15 +11,14 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.graphics.Color
|
||||
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
|
||||
fun ReaderLayoutChapter(
|
||||
chapter: Chapter?,
|
||||
fun ReaderLayoutTextChapter(
|
||||
chapter: Chapter,
|
||||
fontColor: Color,
|
||||
sidePadding: Dp
|
||||
) {
|
||||
chapter?.let {
|
||||
Spacer(modifier = Modifier.height(22.dp))
|
||||
Text(
|
||||
text = chapter.title,
|
||||
|
|
@ -31,4 +30,3 @@ fun ReaderLayoutChapter(
|
|||
HorizontalDivider(color = fontColor.copy(0.4f))
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
|
|
@ -14,15 +14,15 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
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.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.FontWithName
|
||||
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.ui.UIText
|
||||
import ua.acclorite.book_story.presentation.core.components.common.AnimatedVisibility
|
||||
|
|
@ -33,7 +33,7 @@ import ua.acclorite.book_story.ui.settings.SettingsEvent
|
|||
@Composable
|
||||
fun ReaderScaffold(
|
||||
book: Book,
|
||||
text: List<AnnotatedString>,
|
||||
text: List<ReaderText>,
|
||||
listState: LazyListState,
|
||||
currentChapter: Chapter?,
|
||||
nestedScrollConnection: NestedScrollConnection,
|
||||
|
|
@ -47,7 +47,6 @@ fun ReaderScaffold(
|
|||
checkpoint: Checkpoint,
|
||||
showMenu: Boolean,
|
||||
lockMenu: Boolean,
|
||||
chapters: Map<Int, Chapter>,
|
||||
contentPadding: PaddingValues,
|
||||
verticalPadding: Dp,
|
||||
horizontalGesture: ReaderHorizontalGesture,
|
||||
|
|
@ -136,7 +135,6 @@ fun ReaderScaffold(
|
|||
) {
|
||||
ReaderLayout(
|
||||
text = text,
|
||||
chapters = chapters,
|
||||
listState = listState,
|
||||
contentPadding = contentPadding,
|
||||
verticalPadding = verticalPadding,
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ import androidx.compose.ui.unit.dp
|
|||
import androidx.compose.ui.unit.sp
|
||||
import ua.acclorite.book_story.R
|
||||
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.util.LocalActivity
|
||||
import ua.acclorite.book_story.presentation.core.util.noRippleClickable
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package ua.acclorite.book_story.ui.reader
|
|||
import android.content.Context
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.runtime.Immutable
|
||||
import ua.acclorite.book_story.domain.reader.ReaderText.Chapter
|
||||
|
||||
@Immutable
|
||||
sealed class ReaderEvent {
|
||||
|
|
@ -25,7 +26,7 @@ sealed class ReaderEvent {
|
|||
) : ReaderEvent()
|
||||
|
||||
data class OnScrollToChapter(
|
||||
val chapterStartIndex: Int
|
||||
val chapter: Chapter
|
||||
) : ReaderEvent()
|
||||
|
||||
data class OnScroll(
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package ua.acclorite.book_story.ui.reader
|
|||
import android.app.SearchManager
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
|
|
@ -28,8 +29,8 @@ import kotlinx.coroutines.sync.withLock
|
|||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.yield
|
||||
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.ReaderText.Chapter
|
||||
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.GetText
|
||||
|
|
@ -44,6 +45,8 @@ import ua.acclorite.book_story.ui.library.LibraryScreen
|
|||
import javax.inject.Inject
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
private const val READER = "READER, MODEL"
|
||||
|
||||
@HiltViewModel
|
||||
class ReaderModel @Inject constructor(
|
||||
private val getBookById: GetBookById,
|
||||
|
|
@ -67,20 +70,19 @@ class ReaderModel @Inject constructor(
|
|||
when (event) {
|
||||
is ReaderEvent.OnLoadText -> {
|
||||
launch(Dispatchers.IO) {
|
||||
val chaptersAndText = getText.execute(_state.value.book.id)
|
||||
val text = getText.execute(_state.value.book.id)
|
||||
yield()
|
||||
|
||||
if (chaptersAndText.text.isEmpty()) {
|
||||
if (text.isEmpty()) {
|
||||
_state.update {
|
||||
it.copy(
|
||||
isLoading = false,
|
||||
errorMessage = UIText.StringResource(R.string.error_no_text) //todo rename
|
||||
errorMessage = UIText.StringResource(R.string.error_no_text)
|
||||
)
|
||||
}
|
||||
return@launch
|
||||
}
|
||||
|
||||
yield()
|
||||
val lastOpened = getLatestHistory.execute(_state.value.book.id)?.time
|
||||
yield()
|
||||
|
||||
|
|
@ -89,8 +91,7 @@ class ReaderModel @Inject constructor(
|
|||
book = it.book.copy(
|
||||
lastOpened = lastOpened
|
||||
),
|
||||
chapters = chaptersAndText.chapters,
|
||||
text = chaptersAndText.text
|
||||
text = text
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -177,25 +178,30 @@ class ReaderModel @Inject constructor(
|
|||
|
||||
is ReaderEvent.OnScrollToChapter -> {
|
||||
launch {
|
||||
_state.value.listState.requestScrollToItem(
|
||||
event.chapterStartIndex
|
||||
)
|
||||
updateChapter(index = event.chapterStartIndex)
|
||||
_state.value.apply {
|
||||
val chapterIndex = text.indexOf(event.chapter).takeIf { it != -1 }
|
||||
|
||||
if (chapterIndex == null) {
|
||||
return@launch
|
||||
}
|
||||
|
||||
listState.requestScrollToItem(chapterIndex)
|
||||
updateChapter(index = chapterIndex)
|
||||
onEvent(
|
||||
ReaderEvent.OnChangeProgress(
|
||||
progress = calculateProgress(event.chapterStartIndex),
|
||||
firstVisibleItemIndex = event.chapterStartIndex,
|
||||
progress = calculateProgress(chapterIndex),
|
||||
firstVisibleItemIndex = chapterIndex,
|
||||
firstVisibleItemOffset = 0
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is ReaderEvent.OnScroll -> {
|
||||
scrollJob?.cancel()
|
||||
scrollJob = launch {
|
||||
delay(300)
|
||||
|
||||
yield()
|
||||
|
||||
val scrollTo = (_state.value.text.size * event.progress).roundToInt()
|
||||
|
|
@ -211,6 +217,7 @@ class ReaderModel @Inject constructor(
|
|||
checkpoint.offset
|
||||
)
|
||||
|
||||
updateChapter(checkpoint.index)
|
||||
onEvent(
|
||||
ReaderEvent.OnChangeProgress(
|
||||
progress = calculateProgress(checkpoint.index),
|
||||
|
|
@ -234,9 +241,9 @@ class ReaderModel @Inject constructor(
|
|||
|
||||
_state.value.listState.apply {
|
||||
if (
|
||||
_state.value.isLoading
|
||||
|| layoutInfo.totalItemsCount < 1
|
||||
|| _state.value.text.isEmpty()
|
||||
_state.value.isLoading ||
|
||||
layoutInfo.totalItemsCount < 1 ||
|
||||
_state.value.text.isEmpty()
|
||||
) return@apply
|
||||
|
||||
_state.update {
|
||||
|
|
@ -284,6 +291,8 @@ class ReaderModel @Inject constructor(
|
|||
"translate: ${event.textToTranslate.trim()}"
|
||||
)
|
||||
|
||||
yield()
|
||||
|
||||
translatorIntent.launchActivity(
|
||||
activity = event.activity,
|
||||
createChooser = !event.translateWholeParagraph,
|
||||
|
|
@ -320,6 +329,8 @@ class ReaderModel @Inject constructor(
|
|||
event.textToShare.trim()
|
||||
)
|
||||
|
||||
yield()
|
||||
|
||||
shareIntent.launchActivity(
|
||||
activity = event.activity,
|
||||
createChooser = true,
|
||||
|
|
@ -345,6 +356,8 @@ class ReaderModel @Inject constructor(
|
|||
event.textToSearch
|
||||
)
|
||||
|
||||
yield()
|
||||
|
||||
browserIntent.launchActivity(
|
||||
activity = event.activity,
|
||||
success = {
|
||||
|
|
@ -376,6 +389,8 @@ class ReaderModel @Inject constructor(
|
|||
val text = event.textToDefine.trim().replace(" ", "+")
|
||||
browserIntent.data = Uri.parse("https://www.onelook.com/?w=$text")
|
||||
|
||||
yield()
|
||||
|
||||
dictionaryIntent.launchActivity(
|
||||
activity = event.activity,
|
||||
createChooser = true,
|
||||
|
|
@ -485,6 +500,10 @@ class ReaderModel @Inject constructor(
|
|||
if (progress == _state.value.book.progress) return@collectLatest
|
||||
val (currentChapter, currentChapterProgress) = calculateCurrentChapter(index)
|
||||
|
||||
Log.i(
|
||||
READER,
|
||||
"Changed progress|currentChapter: $progress; ${currentChapter?.title}"
|
||||
)
|
||||
_state.update {
|
||||
it.copy(
|
||||
book = it.book.copy(
|
||||
|
|
@ -509,6 +528,11 @@ class ReaderModel @Inject constructor(
|
|||
viewModelScope.launch {
|
||||
val (currentChapter, currentChapterProgress) = calculateCurrentChapter(index)
|
||||
_state.update {
|
||||
Log.i(
|
||||
READER,
|
||||
"Changed currentChapter|currentChapterProgress:" +
|
||||
" ${currentChapter?.title}($currentChapterProgress)"
|
||||
)
|
||||
it.copy(
|
||||
currentChapter = currentChapter,
|
||||
currentChapterProgress = currentChapterProgress
|
||||
|
|
@ -518,20 +542,33 @@ class ReaderModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun calculateCurrentChapter(index: Int): Pair<Chapter?, Float> {
|
||||
val currentChapter = _state.value.chapters.find { chapter ->
|
||||
index in chapter.startIndex..chapter.endIndex
|
||||
}
|
||||
val currentChapterProgress = currentChapter.run {
|
||||
if (this == null) return@run 0f
|
||||
val currentChapter = findCurrentChapter(index)
|
||||
val currentChapterProgress = currentChapter?.let { chapter ->
|
||||
_state.value.text.run {
|
||||
val startIndex = (indexOf(chapter) + 1).coerceAtMost(count())
|
||||
val endIndex = (indexOfFirst {
|
||||
it is Chapter && indexOf(it) > startIndex
|
||||
}.takeIf { it != -1 }?.minus(1)) ?: lastIndex
|
||||
|
||||
val currentIndex = index - startIndex
|
||||
val endIndex = endIndex - startIndex
|
||||
(currentIndex / endIndex.toFloat())
|
||||
val currentIndexInChapter = index - startIndex
|
||||
val chapterLength = endIndex - startIndex
|
||||
(currentIndexInChapter / chapterLength.toFloat())
|
||||
}
|
||||
}.coerceAndPreventNaN()
|
||||
|
||||
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 {
|
||||
return _state.value.run {
|
||||
if (
|
||||
|
|
|
|||
|
|
@ -34,7 +34,6 @@ import androidx.hilt.navigation.compose.hiltViewModel
|
|||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import kotlinx.parcelize.Parcelize
|
||||
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.presentation.core.constants.Constants
|
||||
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
|
||||
}
|
||||
|
||||
val chapters = remember(state.value.chapters) {
|
||||
val chapters = mutableMapOf<Int, Chapter>()
|
||||
state.value.chapters.forEach {
|
||||
chapters[it.startIndex] = it
|
||||
}
|
||||
chapters
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
screenModel.init(
|
||||
bookId = bookId,
|
||||
|
|
@ -313,7 +304,6 @@ data class ReaderScreen(val bookId: Int) : Screen, Parcelable {
|
|||
checkpoint = state.value.checkpoint,
|
||||
showMenu = state.value.showMenu,
|
||||
lockMenu = state.value.lockMenu,
|
||||
chapters = chapters,
|
||||
contentPadding = contentPadding,
|
||||
verticalPadding = verticalPadding,
|
||||
horizontalGesture = mainState.value.horizontalGesture,
|
||||
|
|
|
|||
|
|
@ -2,10 +2,10 @@ package ua.acclorite.book_story.ui.reader
|
|||
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
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.reader.Chapter
|
||||
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.util.BottomSheet
|
||||
import ua.acclorite.book_story.domain.util.Drawer
|
||||
|
|
@ -15,8 +15,7 @@ import ua.acclorite.book_story.presentation.core.constants.provideEmptyBook
|
|||
@Immutable
|
||||
data class ReaderState(
|
||||
val book: Book = Constants.provideEmptyBook(),
|
||||
val chapters: List<Chapter> = emptyList(),
|
||||
val text: List<AnnotatedString> = emptyList(),
|
||||
val text: List<ReaderText> = emptyList(),
|
||||
val listState: LazyListState = LazyListState(),
|
||||
|
||||
val currentChapter: Chapter? = null,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue