🚀 Images in Reader
* Added Images in Reader * Alternative(caption) text below Image Resolves: #69
This commit is contained in:
parent
a407e54c56
commit
c9832c3183
7 changed files with 147 additions and 10 deletions
|
|
@ -1,11 +1,16 @@
|
||||||
package ua.acclorite.book_story.data.parser
|
package ua.acclorite.book_story.data.parser
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import androidx.compose.ui.graphics.asImageBitmap
|
||||||
import kotlinx.coroutines.yield
|
import kotlinx.coroutines.yield
|
||||||
import org.jsoup.nodes.Document
|
import org.jsoup.nodes.Document
|
||||||
import ua.acclorite.book_story.domain.reader.ReaderText
|
import ua.acclorite.book_story.domain.reader.ReaderText
|
||||||
import ua.acclorite.book_story.presentation.core.util.clearAllMarkdown
|
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 ua.acclorite.book_story.presentation.core.util.containsVisibleText
|
import ua.acclorite.book_story.presentation.core.util.containsVisibleText
|
||||||
|
import java.util.zip.ZipEntry
|
||||||
|
import java.util.zip.ZipFile
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class DocumentParser @Inject constructor(
|
class DocumentParser @Inject constructor(
|
||||||
|
|
@ -18,7 +23,11 @@ class DocumentParser @Inject constructor(
|
||||||
*
|
*
|
||||||
* @return Parsed text line by line with Markdown(all lines are not blank).
|
* @return Parsed text line by line with Markdown(all lines are not blank).
|
||||||
*/
|
*/
|
||||||
suspend fun Document.parseDocument(includeChapter: Boolean = true): List<ReaderText> {
|
suspend fun Document.parseDocument(
|
||||||
|
zipFile: ZipFile? = null,
|
||||||
|
imageEntries: List<ZipEntry>? = null,
|
||||||
|
includeChapter: Boolean = true
|
||||||
|
): List<ReaderText> {
|
||||||
yield()
|
yield()
|
||||||
|
|
||||||
val readerText = mutableListOf<ReaderText>()
|
val readerText = mutableListOf<ReaderText>()
|
||||||
|
|
@ -53,7 +62,26 @@ class DocumentParser @Inject constructor(
|
||||||
element.prepend("[")
|
element.prepend("[")
|
||||||
element.append("]($link)")
|
element.append("]($link)")
|
||||||
}
|
}
|
||||||
}.wholeText().lines().forEachIndexed { index, line ->
|
|
||||||
|
// Image
|
||||||
|
select("img").forEach { element ->
|
||||||
|
val src = element.attr("src")
|
||||||
|
.trim()
|
||||||
|
.substringAfterLast("/")
|
||||||
|
.lowercase()
|
||||||
|
.takeIf {
|
||||||
|
it.containsVisibleText() && imageEntries?.any { image ->
|
||||||
|
it == image.name.substringAfterLast('/').lowercase()
|
||||||
|
} == true
|
||||||
|
} ?: return@forEach
|
||||||
|
|
||||||
|
val alt = element.attr("alt").trim().takeIf {
|
||||||
|
it.clearMarkdown().containsVisibleText()
|
||||||
|
} ?: src.substringBeforeLast(".")
|
||||||
|
|
||||||
|
element.append("\n[[$src|$alt]]\n")
|
||||||
|
}
|
||||||
|
}.wholeText().lines().forEach { line ->
|
||||||
yield()
|
yield()
|
||||||
|
|
||||||
val formattedLine = line.replace(
|
val formattedLine = line.replace(
|
||||||
|
|
@ -64,9 +92,54 @@ class DocumentParser @Inject constructor(
|
||||||
Regex("""_\s*(.*?)\s*_"""), "_$1_"
|
Regex("""_\s*(.*?)\s*_"""), "_$1_"
|
||||||
).trim()
|
).trim()
|
||||||
|
|
||||||
|
val imageRegex = Regex("""\[\[(.*?)\|(.*?)]]""")
|
||||||
|
|
||||||
if (line.containsVisibleText()) {
|
if (line.containsVisibleText()) {
|
||||||
when (line) {
|
when {
|
||||||
"***", "---" -> readerText.add(ReaderText.Separator)
|
imageRegex.matches(line) -> {
|
||||||
|
val trimmedLine = line.removeSurrounding("[[", "]]")
|
||||||
|
val src = trimmedLine.substringBefore("|")
|
||||||
|
val alt = "_${trimmedLine.substringAfter("|")}_"
|
||||||
|
|
||||||
|
val image = try {
|
||||||
|
val imageEntry = imageEntries?.find { image ->
|
||||||
|
src == image.name.substringAfterLast('/').lowercase()
|
||||||
|
} ?: return@forEach
|
||||||
|
|
||||||
|
zipFile?.getInputStream(imageEntry)?.use { inputStream ->
|
||||||
|
val options = BitmapFactory.Options().apply {
|
||||||
|
inSampleSize = 2
|
||||||
|
inPreferredConfig = Bitmap.Config.RGB_565
|
||||||
|
}
|
||||||
|
BitmapFactory.decodeStream(
|
||||||
|
/* is = */ inputStream,
|
||||||
|
/* outPadding = */ null,
|
||||||
|
/* opts = */ options
|
||||||
|
)?.asImageBitmap()
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
return@forEach
|
||||||
|
}
|
||||||
|
|
||||||
|
if (image == null) return@forEach
|
||||||
|
|
||||||
|
// Optimize
|
||||||
|
image.prepareToDraw()
|
||||||
|
|
||||||
|
readerText.add( // Adding image
|
||||||
|
ReaderText.Image(
|
||||||
|
imageBitmap = image
|
||||||
|
)
|
||||||
|
)
|
||||||
|
readerText.add( // Adding alternative text (caption) for image
|
||||||
|
ReaderText.Text(
|
||||||
|
markdownParser.parse(alt)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
line == "---" || line == "***" -> readerText.add(ReaderText.Separator)
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
if (
|
if (
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,8 @@ import org.jsoup.Jsoup
|
||||||
import ua.acclorite.book_story.data.parser.DocumentParser
|
import ua.acclorite.book_story.data.parser.DocumentParser
|
||||||
import ua.acclorite.book_story.data.parser.TextParser
|
import ua.acclorite.book_story.data.parser.TextParser
|
||||||
import ua.acclorite.book_story.domain.reader.ReaderText
|
import ua.acclorite.book_story.domain.reader.ReaderText
|
||||||
|
import ua.acclorite.book_story.presentation.core.constants.Constants
|
||||||
|
import ua.acclorite.book_story.presentation.core.constants.provideImageExtensions
|
||||||
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.containsVisibleText
|
import ua.acclorite.book_story.presentation.core.util.containsVisibleText
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
@ -50,6 +52,11 @@ class EpubTextParser @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
val chapterEntries = zip.getChapterEntries(opfEntry)
|
val chapterEntries = zip.getChapterEntries(opfEntry)
|
||||||
|
val imageEntries = zip.entries().toList().filter {
|
||||||
|
Constants.provideImageExtensions().any { format ->
|
||||||
|
it.name.endsWith(format, ignoreCase = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
val chapterTitleEntries = zip.getChapterTitleMapFromToc(tocEntry)
|
val chapterTitleEntries = zip.getChapterTitleMapFromToc(tocEntry)
|
||||||
|
|
||||||
Log.i(EPUB_TAG, "TOC Entry: ${tocEntry?.name ?: "no toc.ncx"}")
|
Log.i(EPUB_TAG, "TOC Entry: ${tocEntry?.name ?: "no toc.ncx"}")
|
||||||
|
|
@ -59,6 +66,7 @@ class EpubTextParser @Inject constructor(
|
||||||
|
|
||||||
readerText = zip.parseEpub(
|
readerText = zip.parseEpub(
|
||||||
chapterEntries = chapterEntries,
|
chapterEntries = chapterEntries,
|
||||||
|
imageEntries = imageEntries,
|
||||||
chapterTitleEntries = chapterTitleEntries
|
chapterTitleEntries = chapterTitleEntries
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -94,6 +102,7 @@ class EpubTextParser @Inject constructor(
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
private suspend fun ZipFile.parseEpub(
|
private suspend fun ZipFile.parseEpub(
|
||||||
chapterEntries: List<ZipEntry>,
|
chapterEntries: List<ZipEntry>,
|
||||||
|
imageEntries: List<ZipEntry>,
|
||||||
chapterTitleEntries: Map<Title, List<String>>?
|
chapterTitleEntries: Map<Title, List<String>>?
|
||||||
): List<ReaderText> {
|
): List<ReaderText> {
|
||||||
|
|
||||||
|
|
@ -110,6 +119,7 @@ class EpubTextParser @Inject constructor(
|
||||||
zip = this@parseEpub,
|
zip = this@parseEpub,
|
||||||
index = index,
|
index = index,
|
||||||
entry = entry,
|
entry = entry,
|
||||||
|
imageEntries = imageEntries,
|
||||||
chapterTitleMap = chapterTitleEntries
|
chapterTitleMap = chapterTitleEntries
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -143,12 +153,17 @@ class EpubTextParser @Inject constructor(
|
||||||
zip: ZipFile,
|
zip: ZipFile,
|
||||||
index: Int,
|
index: Int,
|
||||||
entry: ZipEntry,
|
entry: ZipEntry,
|
||||||
|
imageEntries: List<ZipEntry>,
|
||||||
chapterTitleMap: Map<Title, List<String>>?
|
chapterTitleMap: Map<Title, List<String>>?
|
||||||
) {
|
) {
|
||||||
// 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 readerText = documentParser.run {
|
var readerText = documentParser.run {
|
||||||
Jsoup.parse(content).parseDocument(includeChapter = false)
|
Jsoup.parse(content).parseDocument(
|
||||||
|
zipFile = zip,
|
||||||
|
imageEntries = imageEntries,
|
||||||
|
includeChapter = false
|
||||||
|
)
|
||||||
}.toMutableList()
|
}.toMutableList()
|
||||||
|
|
||||||
// Adding chapter title from TOC if found
|
// Adding chapter title from TOC if found
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package ua.acclorite.book_story.domain.reader
|
package ua.acclorite.book_story.domain.reader
|
||||||
|
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
|
import androidx.compose.ui.graphics.ImageBitmap
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
|
|
@ -13,4 +14,9 @@ sealed class ReaderText {
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
data object Separator : ReaderText()
|
data object Separator : ReaderText()
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
data class Image(
|
||||||
|
val imageBitmap: ImageBitmap
|
||||||
|
) : ReaderText()
|
||||||
}
|
}
|
||||||
|
|
@ -9,4 +9,10 @@ fun Constants.provideExtensions() = listOf(
|
||||||
".html",
|
".html",
|
||||||
".htm",
|
".htm",
|
||||||
".md"
|
".md"
|
||||||
|
)
|
||||||
|
|
||||||
|
fun Constants.provideImageExtensions() = listOf(
|
||||||
|
".png",
|
||||||
|
".jpg",
|
||||||
|
".jpeg"
|
||||||
)
|
)
|
||||||
|
|
@ -156,7 +156,7 @@ fun ReaderLayout(
|
||||||
ReaderLayoutText(
|
ReaderLayoutText(
|
||||||
activity = activity,
|
activity = activity,
|
||||||
showMenu = showMenu,
|
showMenu = showMenu,
|
||||||
textEntry = textEntry,
|
entry = textEntry,
|
||||||
fontFamily = fontFamily,
|
fontFamily = fontFamily,
|
||||||
fontColor = fontColor,
|
fontColor = fontColor,
|
||||||
lineHeight = lineHeight,
|
lineHeight = lineHeight,
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ import ua.acclorite.book_story.ui.reader.ReaderEvent
|
||||||
fun LazyItemScope.ReaderLayoutText(
|
fun LazyItemScope.ReaderLayoutText(
|
||||||
activity: ComponentActivity,
|
activity: ComponentActivity,
|
||||||
showMenu: Boolean,
|
showMenu: Boolean,
|
||||||
textEntry: ReaderText,
|
entry: ReaderText,
|
||||||
fontFamily: FontWithName,
|
fontFamily: FontWithName,
|
||||||
fontColor: Color,
|
fontColor: Color,
|
||||||
lineHeight: TextUnit,
|
lineHeight: TextUnit,
|
||||||
|
|
@ -38,7 +38,14 @@ fun LazyItemScope.ReaderLayoutText(
|
||||||
openTranslator: (ReaderEvent.OnOpenTranslator) -> Unit,
|
openTranslator: (ReaderEvent.OnOpenTranslator) -> Unit,
|
||||||
menuVisibility: (ReaderEvent.OnMenuVisibility) -> Unit
|
menuVisibility: (ReaderEvent.OnMenuVisibility) -> Unit
|
||||||
) {
|
) {
|
||||||
when (textEntry) {
|
when (entry) {
|
||||||
|
is ReaderText.Image -> {
|
||||||
|
ReaderLayoutTextImage(
|
||||||
|
entry = entry,
|
||||||
|
sidePadding = sidePadding
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
is ReaderText.Separator -> {
|
is ReaderText.Separator -> {
|
||||||
ReaderLayoutTextSeparator(
|
ReaderLayoutTextSeparator(
|
||||||
sidePadding = sidePadding,
|
sidePadding = sidePadding,
|
||||||
|
|
@ -48,7 +55,7 @@ fun LazyItemScope.ReaderLayoutText(
|
||||||
|
|
||||||
is ReaderText.Chapter -> {
|
is ReaderText.Chapter -> {
|
||||||
ReaderLayoutTextChapter(
|
ReaderLayoutTextChapter(
|
||||||
chapter = textEntry,
|
chapter = entry,
|
||||||
chapterTitleAlignment = chapterTitleAlignment,
|
chapterTitleAlignment = chapterTitleAlignment,
|
||||||
fontColor = fontColor,
|
fontColor = fontColor,
|
||||||
sidePadding = sidePadding,
|
sidePadding = sidePadding,
|
||||||
|
|
@ -59,7 +66,7 @@ fun LazyItemScope.ReaderLayoutText(
|
||||||
|
|
||||||
is ReaderText.Text -> {
|
is ReaderText.Text -> {
|
||||||
ReaderLayoutTextParagraph(
|
ReaderLayoutTextParagraph(
|
||||||
paragraph = textEntry,
|
paragraph = entry,
|
||||||
activity = activity,
|
activity = activity,
|
||||||
showMenu = showMenu,
|
showMenu = showMenu,
|
||||||
fontFamily = fontFamily,
|
fontFamily = fontFamily,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
package ua.acclorite.book_story.presentation.reader
|
||||||
|
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyItemScope
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import ua.acclorite.book_story.domain.reader.ReaderText
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LazyItemScope.ReaderLayoutTextImage(
|
||||||
|
entry: ReaderText.Image,
|
||||||
|
sidePadding: Dp
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
modifier = Modifier
|
||||||
|
.animateItem(
|
||||||
|
fadeInSpec = null,
|
||||||
|
fadeOutSpec = null
|
||||||
|
)
|
||||||
|
.padding(horizontal = sidePadding)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
bitmap = entry.imageBitmap,
|
||||||
|
contentDescription = null,
|
||||||
|
contentScale = ContentScale.FillWidth
|
||||||
|
)
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue