From 8f52549c19211fe7e00958afc59bc99ca91e2bb6 Mon Sep 17 00:00:00 2001 From: Aryan Date: Fri, 6 Mar 2026 14:06:29 +0530 Subject: [PATCH] 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`. --- app/build.gradle.kts | 1 + app/src/main/assets/epub_reader.js | 43 +++++------ .../aryan/reader/epub/SingleFileImporter.kt | 73 +++++++++++-------- .../reader/paginatedreader/PaginatedReader.kt | 39 ++++++++-- .../aryan/reader/paginatedreader/Paginator.kt | 5 +- 5 files changed, 98 insertions(+), 63 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d78ee9a..599d366 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -20,6 +20,7 @@ if (localPropertiesFile.exists()) { android { namespace = "com.aryan.reader" compileSdk = 35 + ndkVersion = "29.0.14206865" defaultConfig { applicationId = "com.aryan.reader" diff --git a/app/src/main/assets/epub_reader.js b/app/src/main/assets/epub_reader.js index f97513a..e5a7d7d 100644 --- a/app/src/main/assets/epub_reader.js +++ b/app/src/main/assets/epub_reader.js @@ -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) => { diff --git a/app/src/main/java/com/aryan/reader/epub/SingleFileImporter.kt b/app/src/main/java/com/aryan/reader/epub/SingleFileImporter.kt index 56a0056..e7f3f6b 100644 --- a/app/src/main/java/com/aryan/reader/epub/SingleFileImporter.kt +++ b/app/src/main/java/com/aryan/reader/epub/SingleFileImporter.kt @@ -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) { $chapterTitle -
${currentChapterContent}
+ + $currentChapterContent + """.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("

\n") } + break } - if (currentChapterContent.length >= chapterTargetSize) { - flushChapter() + val trimmed = line.trim() + if (trimmed.isEmpty()) { + if (inParagraph) { + currentChapterContent.append("

\n") + inParagraph = false + } + + if (currentChapterContent.length >= chapterTargetSize) { + flushChapter() + } + } else { + if (!inParagraph) { + currentChapterContent.append("

") + inParagraph = true + } else { + currentChapterContent.append(" ") + } + currentChapterContent.append(escapeHtml(trimmed)) + + if (currentChapterContent.length >= chapterTargetSize * 2) { + currentChapterContent.append("

\n") + flushChapter() + inParagraph = false + } } } flushChapter() if (chapters.isEmpty()) { - currentChapterContent.append("(Empty File)") + currentChapterContent.append("

(Empty File)

") flushChapter() } diff --git a/app/src/main/java/com/aryan/reader/paginatedreader/PaginatedReader.kt b/app/src/main/java/com/aryan/reader/paginatedreader/PaginatedReader.kt index a2e8203..09f34b1 100644 --- a/app/src/main/java/com/aryan/reader/paginatedreader/PaginatedReader.kt +++ b/app/src/main/java/com/aryan/reader/paginatedreader/PaginatedReader.kt @@ -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 diff --git a/app/src/main/java/com/aryan/reader/paginatedreader/Paginator.kt b/app/src/main/java/com/aryan/reader/paginatedreader/Paginator.kt index 37bf74e..a34be24 100644 --- a/app/src/main/java/com/aryan/reader/paginatedreader/Paginator.kt +++ b/app/src/main/java/com/aryan/reader/paginatedreader/Paginator.kt @@ -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) ) }