🚀 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
|
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>
|
||||||
}
|
}
|
||||||
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -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() ?: ""
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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() ?: ""
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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() ?: ""
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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() ?: ""
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -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.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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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 == '-' }
|
.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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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?,
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,24 +11,22 @@ 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,
|
style = MaterialTheme.typography.headlineMedium,
|
||||||
style = MaterialTheme.typography.headlineMedium,
|
color = fontColor,
|
||||||
color = fontColor,
|
modifier = Modifier.padding(horizontal = sidePadding)
|
||||||
modifier = Modifier.padding(horizontal = sidePadding)
|
)
|
||||||
)
|
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))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -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.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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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,17 +178,23 @@ 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) {
|
||||||
onEvent(
|
return@launch
|
||||||
ReaderEvent.OnChangeProgress(
|
}
|
||||||
progress = calculateProgress(event.chapterStartIndex),
|
|
||||||
firstVisibleItemIndex = event.chapterStartIndex,
|
listState.requestScrollToItem(chapterIndex)
|
||||||
firstVisibleItemOffset = 0
|
updateChapter(index = chapterIndex)
|
||||||
|
onEvent(
|
||||||
|
ReaderEvent.OnChangeProgress(
|
||||||
|
progress = calculateProgress(chapterIndex),
|
||||||
|
firstVisibleItemIndex = chapterIndex,
|
||||||
|
firstVisibleItemOffset = 0
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -195,7 +202,6 @@ class ReaderModel @Inject constructor(
|
||||||
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 (
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue