🚀 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
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import kotlinx.coroutines.yield
|
||||
import org.jsoup.nodes.Document
|
||||
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.clearMarkdown
|
||||
import ua.acclorite.book_story.presentation.core.util.containsVisibleText
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipFile
|
||||
import javax.inject.Inject
|
||||
|
||||
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).
|
||||
*/
|
||||
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()
|
||||
|
||||
val readerText = mutableListOf<ReaderText>()
|
||||
|
|
@ -53,7 +62,26 @@ class DocumentParser @Inject constructor(
|
|||
element.prepend("[")
|
||||
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()
|
||||
|
||||
val formattedLine = line.replace(
|
||||
|
|
@ -64,9 +92,54 @@ class DocumentParser @Inject constructor(
|
|||
Regex("""_\s*(.*?)\s*_"""), "_$1_"
|
||||
).trim()
|
||||
|
||||
val imageRegex = Regex("""\[\[(.*?)\|(.*?)]]""")
|
||||
|
||||
if (line.containsVisibleText()) {
|
||||
when (line) {
|
||||
"***", "---" -> readerText.add(ReaderText.Separator)
|
||||
when {
|
||||
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 -> {
|
||||
if (
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ import org.jsoup.Jsoup
|
|||
import ua.acclorite.book_story.data.parser.DocumentParser
|
||||
import ua.acclorite.book_story.data.parser.TextParser
|
||||
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.containsVisibleText
|
||||
import java.io.File
|
||||
|
|
@ -50,6 +52,11 @@ class EpubTextParser @Inject constructor(
|
|||
}
|
||||
|
||||
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)
|
||||
|
||||
Log.i(EPUB_TAG, "TOC Entry: ${tocEntry?.name ?: "no toc.ncx"}")
|
||||
|
|
@ -59,6 +66,7 @@ class EpubTextParser @Inject constructor(
|
|||
|
||||
readerText = zip.parseEpub(
|
||||
chapterEntries = chapterEntries,
|
||||
imageEntries = imageEntries,
|
||||
chapterTitleEntries = chapterTitleEntries
|
||||
)
|
||||
}
|
||||
|
|
@ -94,6 +102,7 @@ class EpubTextParser @Inject constructor(
|
|||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private suspend fun ZipFile.parseEpub(
|
||||
chapterEntries: List<ZipEntry>,
|
||||
imageEntries: List<ZipEntry>,
|
||||
chapterTitleEntries: Map<Title, List<String>>?
|
||||
): List<ReaderText> {
|
||||
|
||||
|
|
@ -110,6 +119,7 @@ class EpubTextParser @Inject constructor(
|
|||
zip = this@parseEpub,
|
||||
index = index,
|
||||
entry = entry,
|
||||
imageEntries = imageEntries,
|
||||
chapterTitleMap = chapterTitleEntries
|
||||
)
|
||||
|
||||
|
|
@ -143,12 +153,17 @@ class EpubTextParser @Inject constructor(
|
|||
zip: ZipFile,
|
||||
index: Int,
|
||||
entry: ZipEntry,
|
||||
imageEntries: List<ZipEntry>,
|
||||
chapterTitleMap: Map<Title, List<String>>?
|
||||
) {
|
||||
// Getting all text
|
||||
val content = zip.getInputStream(entry).bufferedReader().use { it.readText() }
|
||||
var readerText = documentParser.run {
|
||||
Jsoup.parse(content).parseDocument(includeChapter = false)
|
||||
Jsoup.parse(content).parseDocument(
|
||||
zipFile = zip,
|
||||
imageEntries = imageEntries,
|
||||
includeChapter = false
|
||||
)
|
||||
}.toMutableList()
|
||||
|
||||
// Adding chapter title from TOC if found
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package ua.acclorite.book_story.domain.reader
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
|
||||
@Immutable
|
||||
|
|
@ -13,4 +14,9 @@ sealed class ReaderText {
|
|||
|
||||
@Immutable
|
||||
data object Separator : ReaderText()
|
||||
|
||||
@Immutable
|
||||
data class Image(
|
||||
val imageBitmap: ImageBitmap
|
||||
) : ReaderText()
|
||||
}
|
||||
|
|
@ -10,3 +10,9 @@ fun Constants.provideExtensions() = listOf(
|
|||
".htm",
|
||||
".md"
|
||||
)
|
||||
|
||||
fun Constants.provideImageExtensions() = listOf(
|
||||
".png",
|
||||
".jpg",
|
||||
".jpeg"
|
||||
)
|
||||
|
|
@ -156,7 +156,7 @@ fun ReaderLayout(
|
|||
ReaderLayoutText(
|
||||
activity = activity,
|
||||
showMenu = showMenu,
|
||||
textEntry = textEntry,
|
||||
entry = textEntry,
|
||||
fontFamily = fontFamily,
|
||||
fontColor = fontColor,
|
||||
lineHeight = lineHeight,
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import ua.acclorite.book_story.ui.reader.ReaderEvent
|
|||
fun LazyItemScope.ReaderLayoutText(
|
||||
activity: ComponentActivity,
|
||||
showMenu: Boolean,
|
||||
textEntry: ReaderText,
|
||||
entry: ReaderText,
|
||||
fontFamily: FontWithName,
|
||||
fontColor: Color,
|
||||
lineHeight: TextUnit,
|
||||
|
|
@ -38,7 +38,14 @@ fun LazyItemScope.ReaderLayoutText(
|
|||
openTranslator: (ReaderEvent.OnOpenTranslator) -> Unit,
|
||||
menuVisibility: (ReaderEvent.OnMenuVisibility) -> Unit
|
||||
) {
|
||||
when (textEntry) {
|
||||
when (entry) {
|
||||
is ReaderText.Image -> {
|
||||
ReaderLayoutTextImage(
|
||||
entry = entry,
|
||||
sidePadding = sidePadding
|
||||
)
|
||||
}
|
||||
|
||||
is ReaderText.Separator -> {
|
||||
ReaderLayoutTextSeparator(
|
||||
sidePadding = sidePadding,
|
||||
|
|
@ -48,7 +55,7 @@ fun LazyItemScope.ReaderLayoutText(
|
|||
|
||||
is ReaderText.Chapter -> {
|
||||
ReaderLayoutTextChapter(
|
||||
chapter = textEntry,
|
||||
chapter = entry,
|
||||
chapterTitleAlignment = chapterTitleAlignment,
|
||||
fontColor = fontColor,
|
||||
sidePadding = sidePadding,
|
||||
|
|
@ -59,7 +66,7 @@ fun LazyItemScope.ReaderLayoutText(
|
|||
|
||||
is ReaderText.Text -> {
|
||||
ReaderLayoutTextParagraph(
|
||||
paragraph = textEntry,
|
||||
paragraph = entry,
|
||||
activity = activity,
|
||||
showMenu = showMenu,
|
||||
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