Txt file fix (#35)
* Fixed virtualization data indexing and improved text file to EPUB conversion logic. - Corrected array indexing for `chunksData` and `chunkHeights` in `epub_reader.js` to prevent overwriting entire arrays. - Refined virtualization intersection observer to properly restore and unload chunk content using index-based caching. - Updated `SingleFileImporter.kt` to convert plain text into structured HTML paragraphs instead of a single `pre` tag. - Enhanced CSS styles for imported single-file documents and added HTML entity escaping. * Adjusted text layout and enhanced pagination mismatch logging * Configured `PlatformTextStyle` and `LineHeightStyle` to improve text measurement consistency. * Updated `checkLayoutMismatch` to include text snippets and handle zero expected height. * Refined `Modifier` application order in the reader view. * Ensured `maxHeight` is set to `Infinity` during text measurement in `Paginator`.
This commit is contained in:
parent
506f60473c
commit
8f52549c19
5 changed files with 98 additions and 63 deletions
|
|
@ -20,6 +20,7 @@ if (localPropertiesFile.exists()) {
|
||||||
android {
|
android {
|
||||||
namespace = "com.aryan.reader"
|
namespace = "com.aryan.reader"
|
||||||
compileSdk = 35
|
compileSdk = 35
|
||||||
|
ndkVersion = "29.0.14206865"
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "com.aryan.reader"
|
applicationId = "com.aryan.reader"
|
||||||
|
|
|
||||||
|
|
@ -1820,15 +1820,7 @@
|
||||||
observer: null,
|
observer: null,
|
||||||
|
|
||||||
init: function (initialChunkIndex, total) {
|
init: function (initialChunkIndex, total) {
|
||||||
console.log(`Virtualization: Init with $ {
|
console.log(`Virtualization: Init with ${total} chunks. Anchor: ${initialChunkIndex}`);
|
||||||
total
|
|
||||||
}
|
|
||||||
|
|
||||||
chunks. Anchor: $ {
|
|
||||||
initialChunkIndex
|
|
||||||
}
|
|
||||||
|
|
||||||
`);
|
|
||||||
this.totalChunks = total;
|
this.totalChunks = total;
|
||||||
this.chunksData = new Array(total).fill(null);
|
this.chunksData = new Array(total).fill(null);
|
||||||
this.chunkHeights = new Array(total).fill(0);
|
this.chunkHeights = new Array(total).fill(0);
|
||||||
|
|
@ -1838,11 +1830,12 @@
|
||||||
if (container) {
|
if (container) {
|
||||||
container.querySelectorAll(".chunk-container").forEach((div) => {
|
container.querySelectorAll(".chunk-container").forEach((div) => {
|
||||||
let idx = parseInt(div.dataset.chunkIndex, 10);
|
let idx = parseInt(div.dataset.chunkIndex, 10);
|
||||||
let content = div.innerHTML.trim();
|
let content = div.innerHTML; // Don't trim, keep original HTML structure
|
||||||
|
|
||||||
if (content.length > 0) {
|
if (content.trim().length > 0) {
|
||||||
this.chunksData = content;
|
// FIX: Assign to specific index, don't overwrite the whole array
|
||||||
this.chunkHeights = div.getBoundingClientRect().height;
|
this.chunksData[idx] = content;
|
||||||
|
this.chunkHeights[idx] = div.getBoundingClientRect().height;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -1862,26 +1855,31 @@
|
||||||
let idx = parseInt(div.dataset.chunkIndex, 10);
|
let idx = parseInt(div.dataset.chunkIndex, 10);
|
||||||
|
|
||||||
if (entry.isIntersecting) {
|
if (entry.isIntersecting) {
|
||||||
if (!this.chunksData) {
|
// Check if we have data for this chunk at specific index
|
||||||
|
if (!this.chunksData[idx]) {
|
||||||
if (window.ContentBridge && window.ContentBridge.requestChunk) {
|
if (window.ContentBridge && window.ContentBridge.requestChunk) {
|
||||||
window.ContentBridge.requestChunk(idx);
|
window.ContentBridge.requestChunk(idx);
|
||||||
}
|
}
|
||||||
} else if (div.innerHTML === "") {
|
} else if (div.innerHTML === "") {
|
||||||
|
// Restore content from cache
|
||||||
let oldHeight = div.getBoundingClientRect().height;
|
let oldHeight = div.getBoundingClientRect().height;
|
||||||
div.innerHTML = this.chunksData;
|
div.innerHTML = this.chunksData[idx]; // FIX: Access by index
|
||||||
div.style.height = "";
|
div.style.height = ""; // Allow auto height
|
||||||
let newHeight = div.getBoundingClientRect().height;
|
|
||||||
this.chunkHeights = newHeight;
|
|
||||||
|
|
||||||
|
let newHeight = div.getBoundingClientRect().height;
|
||||||
|
this.chunkHeights[idx] = newHeight; // Update cached height
|
||||||
|
|
||||||
|
// Adjust scroll if this expansion happened above our viewport
|
||||||
if (div.getBoundingClientRect().top < 0) {
|
if (div.getBoundingClientRect().top < 0) {
|
||||||
scrollAdjust += newHeight - oldHeight;
|
scrollAdjust += (newHeight - oldHeight);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// Unload content to save memory/DOM weight
|
||||||
if (div.innerHTML !== "") {
|
if (div.innerHTML !== "") {
|
||||||
let oldHeight = div.getBoundingClientRect().height;
|
let oldHeight = div.getBoundingClientRect().height;
|
||||||
this.chunkHeights = oldHeight;
|
this.chunkHeights[idx] = oldHeight;
|
||||||
div.style.height = oldHeight + "px";
|
div.style.height = oldHeight + "px"; // Fix height to placeholder
|
||||||
div.innerHTML = "";
|
div.innerHTML = "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1891,8 +1889,7 @@
|
||||||
window.scrollBy(0, scrollAdjust);
|
window.scrollBy(0, scrollAdjust);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{ rootMargin: "2500px 0px" } // Keep large margin for smooth scrolling
|
||||||
{ rootMargin: "2500px 0px" },
|
|
||||||
);
|
);
|
||||||
|
|
||||||
document.querySelectorAll(".chunk-container").forEach((div) => {
|
document.querySelectorAll(".chunk-container").forEach((div) => {
|
||||||
|
|
|
||||||
|
|
@ -105,17 +105,8 @@ class SingleFileImporter(private val context: Context) {
|
||||||
var chapterCounter = 1
|
var chapterCounter = 1
|
||||||
|
|
||||||
val cssStyle = """
|
val cssStyle = """
|
||||||
body { margin: 0; padding: 0; }
|
body { font-family: sans-serif; line-height: 1.6; padding: 1em; max-width: 800px; margin: 0 auto; }
|
||||||
pre {
|
p { margin-bottom: 1em; text-indent: 1.5em; }
|
||||||
font-family: 'Courier New', Courier, monospace;
|
|
||||||
font-size: 1em;
|
|
||||||
line-height: 1.5;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
overflow-wrap: anywhere;
|
|
||||||
word-break: break-word;
|
|
||||||
padding: 1em;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
|
|
||||||
val currentChapterContent = StringBuilder()
|
val currentChapterContent = StringBuilder()
|
||||||
|
|
@ -135,7 +126,9 @@ class SingleFileImporter(private val context: Context) {
|
||||||
<title>$chapterTitle</title>
|
<title>$chapterTitle</title>
|
||||||
<style>$cssStyle</style>
|
<style>$cssStyle</style>
|
||||||
</head>
|
</head>
|
||||||
<body><pre>${currentChapterContent}</pre></body>
|
<body>
|
||||||
|
$currentChapterContent
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
|
|
||||||
|
|
@ -160,37 +153,55 @@ class SingleFileImporter(private val context: Context) {
|
||||||
chapterCounter++
|
chapterCounter++
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun escapeHtml(text: String): String {
|
||||||
|
return text.replace("&", "&")
|
||||||
|
.replace("<", "<")
|
||||||
|
.replace(">", ">")
|
||||||
|
}
|
||||||
|
|
||||||
val reader = inputStream.bufferedReader()
|
val reader = inputStream.bufferedReader()
|
||||||
val buffer = CharArray(8192)
|
var inParagraph = false
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
val readCount = reader.read(buffer)
|
val line = reader.readLine()
|
||||||
if (readCount == -1) break
|
if (line == null) {
|
||||||
|
if (inParagraph) {
|
||||||
for (i in 0 until readCount) {
|
currentChapterContent.append("</p>\n")
|
||||||
val c = buffer[i]
|
|
||||||
|
|
||||||
if ((c < ' ' && c != '\t' && c != '\n' && c != '\r')) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
when (c) {
|
|
||||||
'<' -> currentChapterContent.append("<")
|
|
||||||
'>' -> currentChapterContent.append(">")
|
|
||||||
'&' -> currentChapterContent.append("&")
|
|
||||||
else -> currentChapterContent.append(c)
|
|
||||||
}
|
}
|
||||||
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentChapterContent.length >= chapterTargetSize) {
|
val trimmed = line.trim()
|
||||||
flushChapter()
|
if (trimmed.isEmpty()) {
|
||||||
|
if (inParagraph) {
|
||||||
|
currentChapterContent.append("</p>\n")
|
||||||
|
inParagraph = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentChapterContent.length >= chapterTargetSize) {
|
||||||
|
flushChapter()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!inParagraph) {
|
||||||
|
currentChapterContent.append("<p>")
|
||||||
|
inParagraph = true
|
||||||
|
} else {
|
||||||
|
currentChapterContent.append(" ")
|
||||||
|
}
|
||||||
|
currentChapterContent.append(escapeHtml(trimmed))
|
||||||
|
|
||||||
|
if (currentChapterContent.length >= chapterTargetSize * 2) {
|
||||||
|
currentChapterContent.append("</p>\n")
|
||||||
|
flushChapter()
|
||||||
|
inParagraph = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
flushChapter()
|
flushChapter()
|
||||||
|
|
||||||
if (chapters.isEmpty()) {
|
if (chapters.isEmpty()) {
|
||||||
currentChapterContent.append("(Empty File)")
|
currentChapterContent.append("<p>(Empty File)</p>")
|
||||||
flushChapter()
|
flushChapter()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,8 @@ import android.widget.Toast
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.compose.foundation.BorderStroke
|
import androidx.compose.foundation.BorderStroke
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.ui.text.PlatformTextStyle
|
||||||
|
import androidx.compose.ui.text.style.LineHeightStyle
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.border
|
import androidx.compose.foundation.border
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
|
@ -544,7 +546,12 @@ fun PaginatedReaderScreen(
|
||||||
lineHeight = adjustedLineHeight,
|
lineHeight = adjustedLineHeight,
|
||||||
fontFamily = debouncedFontFamily,
|
fontFamily = debouncedFontFamily,
|
||||||
lineBreak = LineBreak.Paragraph,
|
lineBreak = LineBreak.Paragraph,
|
||||||
letterSpacing = TextUnit.Unspecified
|
letterSpacing = TextUnit.Unspecified,
|
||||||
|
platformStyle = PlatformTextStyle(includeFontPadding = false),
|
||||||
|
lineHeightStyle = LineHeightStyle(
|
||||||
|
alignment = LineHeightStyle.Alignment.Proportional,
|
||||||
|
trim = LineHeightStyle.Trim.None
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1302,15 +1309,22 @@ private fun checkLayoutMismatch(
|
||||||
blockType: String,
|
blockType: String,
|
||||||
expectedHeight: Int,
|
expectedHeight: Int,
|
||||||
actualHeight: Int,
|
actualHeight: Int,
|
||||||
tolerance: Int = 2
|
textSnippet: String,
|
||||||
|
@Suppress("SameParameterValue") tolerance: Int = 2
|
||||||
) {
|
) {
|
||||||
|
if (expectedHeight == 0) {
|
||||||
|
Timber.tag("PAGINATION_MISMATCH").w("Block #$blockIndex ($blockType) has expectedHeight=0. Skipping check. Text: '$textSnippet'")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (actualHeight > expectedHeight + tolerance) {
|
if (actualHeight > expectedHeight + tolerance) {
|
||||||
val diff = actualHeight - expectedHeight
|
val diff = actualHeight - expectedHeight
|
||||||
Timber.tag("PAGINATION_MISMATCH").e(
|
Timber.tag("PAGINATION_MISMATCH").e(
|
||||||
"OVERFLOW DETECTED! Block #$blockIndex ($blockType)\n" +
|
"OVERFLOW DETECTED! Block #$blockIndex ($blockType)\n" +
|
||||||
" -> Expected: ${expectedHeight}px\n" +
|
" -> Expected: ${expectedHeight}px\n" +
|
||||||
" -> Actual: ${actualHeight}px\n" +
|
" -> Actual: ${actualHeight}px\n" +
|
||||||
" -> Diff: +${diff}px"
|
" -> Diff: +${diff}px\n" +
|
||||||
|
" -> Content: '$textSnippet'"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1560,8 +1574,7 @@ internal fun PaginatedReaderContent(
|
||||||
Modifier.fillMaxWidth()
|
Modifier.fillMaxWidth()
|
||||||
}
|
}
|
||||||
|
|
||||||
val boxModifier = marginModifier
|
val styleModifier = alignModifier
|
||||||
.then(alignModifier)
|
|
||||||
.then(if (block.style.horizontalAlign == "center") widthModifier else Modifier)
|
.then(if (block.style.horizontalAlign == "center") widthModifier else Modifier)
|
||||||
.then(
|
.then(
|
||||||
if (block.style.borderRadius > 0.dp) Modifier.clip(RoundedCornerShape(block.style.borderRadius))
|
if (block.style.borderRadius > 0.dp) Modifier.clip(RoundedCornerShape(block.style.borderRadius))
|
||||||
|
|
@ -1588,15 +1601,27 @@ internal fun PaginatedReaderContent(
|
||||||
.onGloballyPositioned { coordinates ->
|
.onGloballyPositioned { coordinates ->
|
||||||
val actualHeight = coordinates.size.height
|
val actualHeight = coordinates.size.height
|
||||||
if (block.expectedHeight > 0) {
|
if (block.expectedHeight > 0) {
|
||||||
|
val snippet = when(block) {
|
||||||
|
is ParagraphBlock -> block.content.text.take(50)
|
||||||
|
is HeaderBlock -> block.content.text.take(50)
|
||||||
|
is QuoteBlock -> block.content.text.take(50)
|
||||||
|
is ListItemBlock -> block.content.text.take(50)
|
||||||
|
is TextContentBlock -> block.content.text.take(50)
|
||||||
|
else -> "Non-text content"
|
||||||
|
}
|
||||||
|
|
||||||
checkLayoutMismatch(
|
checkLayoutMismatch(
|
||||||
blockIndex = block.blockIndex,
|
blockIndex = block.blockIndex,
|
||||||
blockType = block::class.simpleName ?: "Block",
|
blockType = block::class.simpleName ?: "Block",
|
||||||
expectedHeight = block.expectedHeight,
|
expectedHeight = block.expectedHeight,
|
||||||
actualHeight = actualHeight
|
actualHeight = actualHeight,
|
||||||
|
textSnippet = snippet,
|
||||||
|
tolerance = 2
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.then(boxModifier)
|
.then(marginModifier)
|
||||||
|
.then(styleModifier)
|
||||||
|
|
||||||
Box(modifier = diagnosticModifier) {
|
Box(modifier = diagnosticModifier) {
|
||||||
val borderWidth = block.style.border?.width ?: 0.dp
|
val borderWidth = block.style.border?.width ?: 0.dp
|
||||||
|
|
|
||||||
|
|
@ -710,7 +710,8 @@ private suspend fun measureBlockHeight(
|
||||||
}
|
}
|
||||||
|
|
||||||
val adjustedConstraints = constraints.copy(
|
val adjustedConstraints = constraints.copy(
|
||||||
maxWidth = contentMaxWidth.roundToInt().coerceAtLeast(0)
|
maxWidth = contentMaxWidth.roundToInt().coerceAtLeast(0),
|
||||||
|
maxHeight = Constraints.Infinity
|
||||||
)
|
)
|
||||||
|
|
||||||
val contentHeight = when (block) {
|
val contentHeight = when (block) {
|
||||||
|
|
@ -1021,7 +1022,7 @@ private suspend fun splitParagraphBlock(
|
||||||
textMeasurer.measure(
|
textMeasurer.measure(
|
||||||
text = text,
|
text = text,
|
||||||
style = textStyle,
|
style = textStyle,
|
||||||
constraints = constraints
|
constraints = constraints.copy(maxHeight = Constraints.Infinity)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue