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 {
|
||||
namespace = "com.aryan.reader"
|
||||
compileSdk = 35
|
||||
ndkVersion = "29.0.14206865"
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.aryan.reader"
|
||||
|
|
|
|||
|
|
@ -1820,15 +1820,7 @@
|
|||
observer: null,
|
||||
|
||||
init: function (initialChunkIndex, total) {
|
||||
console.log(`Virtualization: Init with $ {
|
||||
total
|
||||
}
|
||||
|
||||
chunks. Anchor: $ {
|
||||
initialChunkIndex
|
||||
}
|
||||
|
||||
`);
|
||||
console.log(`Virtualization: Init with ${total} chunks. Anchor: ${initialChunkIndex}`);
|
||||
this.totalChunks = total;
|
||||
this.chunksData = new Array(total).fill(null);
|
||||
this.chunkHeights = new Array(total).fill(0);
|
||||
|
|
@ -1838,11 +1830,12 @@
|
|||
if (container) {
|
||||
container.querySelectorAll(".chunk-container").forEach((div) => {
|
||||
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) {
|
||||
this.chunksData = content;
|
||||
this.chunkHeights = div.getBoundingClientRect().height;
|
||||
if (content.trim().length > 0) {
|
||||
// FIX: Assign to specific index, don't overwrite the whole array
|
||||
this.chunksData[idx] = content;
|
||||
this.chunkHeights[idx] = div.getBoundingClientRect().height;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -1862,26 +1855,31 @@
|
|||
let idx = parseInt(div.dataset.chunkIndex, 10);
|
||||
|
||||
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) {
|
||||
window.ContentBridge.requestChunk(idx);
|
||||
}
|
||||
} else if (div.innerHTML === "") {
|
||||
// Restore content from cache
|
||||
let oldHeight = div.getBoundingClientRect().height;
|
||||
div.innerHTML = this.chunksData;
|
||||
div.style.height = "";
|
||||
let newHeight = div.getBoundingClientRect().height;
|
||||
this.chunkHeights = newHeight;
|
||||
div.innerHTML = this.chunksData[idx]; // FIX: Access by index
|
||||
div.style.height = ""; // Allow auto height
|
||||
|
||||
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) {
|
||||
scrollAdjust += newHeight - oldHeight;
|
||||
scrollAdjust += (newHeight - oldHeight);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Unload content to save memory/DOM weight
|
||||
if (div.innerHTML !== "") {
|
||||
let oldHeight = div.getBoundingClientRect().height;
|
||||
this.chunkHeights = oldHeight;
|
||||
div.style.height = oldHeight + "px";
|
||||
this.chunkHeights[idx] = oldHeight;
|
||||
div.style.height = oldHeight + "px"; // Fix height to placeholder
|
||||
div.innerHTML = "";
|
||||
}
|
||||
}
|
||||
|
|
@ -1891,8 +1889,7 @@
|
|||
window.scrollBy(0, scrollAdjust);
|
||||
}
|
||||
},
|
||||
|
||||
{ rootMargin: "2500px 0px" },
|
||||
{ rootMargin: "2500px 0px" } // Keep large margin for smooth scrolling
|
||||
);
|
||||
|
||||
document.querySelectorAll(".chunk-container").forEach((div) => {
|
||||
|
|
|
|||
|
|
@ -105,17 +105,8 @@ class SingleFileImporter(private val context: Context) {
|
|||
var chapterCounter = 1
|
||||
|
||||
val cssStyle = """
|
||||
body { margin: 0; padding: 0; }
|
||||
pre {
|
||||
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;
|
||||
}
|
||||
body { font-family: sans-serif; line-height: 1.6; padding: 1em; max-width: 800px; margin: 0 auto; }
|
||||
p { margin-bottom: 1em; text-indent: 1.5em; }
|
||||
""".trimIndent()
|
||||
|
||||
val currentChapterContent = StringBuilder()
|
||||
|
|
@ -135,7 +126,9 @@ class SingleFileImporter(private val context: Context) {
|
|||
<title>$chapterTitle</title>
|
||||
<style>$cssStyle</style>
|
||||
</head>
|
||||
<body><pre>${currentChapterContent}</pre></body>
|
||||
<body>
|
||||
$currentChapterContent
|
||||
</body>
|
||||
</html>
|
||||
""".trimIndent()
|
||||
|
||||
|
|
@ -160,37 +153,55 @@ class SingleFileImporter(private val context: Context) {
|
|||
chapterCounter++
|
||||
}
|
||||
|
||||
fun escapeHtml(text: String): String {
|
||||
return text.replace("&", "&")
|
||||
.replace("<", "<")
|
||||
.replace(">", ">")
|
||||
}
|
||||
|
||||
val reader = inputStream.bufferedReader()
|
||||
val buffer = CharArray(8192)
|
||||
var inParagraph = false
|
||||
|
||||
while (true) {
|
||||
val readCount = reader.read(buffer)
|
||||
if (readCount == -1) break
|
||||
|
||||
for (i in 0 until readCount) {
|
||||
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)
|
||||
val line = reader.readLine()
|
||||
if (line == null) {
|
||||
if (inParagraph) {
|
||||
currentChapterContent.append("</p>\n")
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
if (currentChapterContent.length >= chapterTargetSize) {
|
||||
flushChapter()
|
||||
val trimmed = line.trim()
|
||||
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()
|
||||
|
||||
if (chapters.isEmpty()) {
|
||||
currentChapterContent.append("(Empty File)")
|
||||
currentChapterContent.append("<p>(Empty File)</p>")
|
||||
flushChapter()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -31,6 +31,8 @@ import android.widget.Toast
|
|||
import androidx.annotation.RequiresApi
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
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.border
|
||||
import androidx.compose.foundation.clickable
|
||||
|
|
@ -544,7 +546,12 @@ fun PaginatedReaderScreen(
|
|||
lineHeight = adjustedLineHeight,
|
||||
fontFamily = debouncedFontFamily,
|
||||
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,
|
||||
expectedHeight: 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) {
|
||||
val diff = actualHeight - expectedHeight
|
||||
Timber.tag("PAGINATION_MISMATCH").e(
|
||||
"OVERFLOW DETECTED! Block #$blockIndex ($blockType)\n" +
|
||||
" -> Expected: ${expectedHeight}px\n" +
|
||||
" -> Actual: ${actualHeight}px\n" +
|
||||
" -> Diff: +${diff}px"
|
||||
" -> Diff: +${diff}px\n" +
|
||||
" -> Content: '$textSnippet'"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1560,8 +1574,7 @@ internal fun PaginatedReaderContent(
|
|||
Modifier.fillMaxWidth()
|
||||
}
|
||||
|
||||
val boxModifier = marginModifier
|
||||
.then(alignModifier)
|
||||
val styleModifier = alignModifier
|
||||
.then(if (block.style.horizontalAlign == "center") widthModifier else Modifier)
|
||||
.then(
|
||||
if (block.style.borderRadius > 0.dp) Modifier.clip(RoundedCornerShape(block.style.borderRadius))
|
||||
|
|
@ -1588,15 +1601,27 @@ internal fun PaginatedReaderContent(
|
|||
.onGloballyPositioned { coordinates ->
|
||||
val actualHeight = coordinates.size.height
|
||||
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(
|
||||
blockIndex = block.blockIndex,
|
||||
blockType = block::class.simpleName ?: "Block",
|
||||
expectedHeight = block.expectedHeight,
|
||||
actualHeight = actualHeight
|
||||
actualHeight = actualHeight,
|
||||
textSnippet = snippet,
|
||||
tolerance = 2
|
||||
)
|
||||
}
|
||||
}
|
||||
.then(boxModifier)
|
||||
.then(marginModifier)
|
||||
.then(styleModifier)
|
||||
|
||||
Box(modifier = diagnosticModifier) {
|
||||
val borderWidth = block.style.border?.width ?: 0.dp
|
||||
|
|
|
|||
|
|
@ -710,7 +710,8 @@ private suspend fun measureBlockHeight(
|
|||
}
|
||||
|
||||
val adjustedConstraints = constraints.copy(
|
||||
maxWidth = contentMaxWidth.roundToInt().coerceAtLeast(0)
|
||||
maxWidth = contentMaxWidth.roundToInt().coerceAtLeast(0),
|
||||
maxHeight = Constraints.Infinity
|
||||
)
|
||||
|
||||
val contentHeight = when (block) {
|
||||
|
|
@ -1021,7 +1022,7 @@ private suspend fun splitParagraphBlock(
|
|||
textMeasurer.measure(
|
||||
text = text,
|
||||
style = textStyle,
|
||||
constraints = constraints
|
||||
constraints = constraints.copy(maxHeight = Constraints.Infinity)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue