fix: force disk fallback in LocatorConverter to fix MD/TXT reading position (#40)

Background pre-processing for single-file imports (MD/TXT) was generating empty DB cache entries because it only looked at RAM content. This caused position saves in Vertical Mode to fail. This fix ensures that an empty cache entry triggers a disk-read fallback to generate valid semantic blocks on-demand.
This commit is contained in:
Aryan 2026-03-07 22:34:08 +05:30 committed by GitHub
parent b9f8825ad7
commit 6bc4f9bafd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 104 additions and 51 deletions

View file

@ -60,7 +60,7 @@ class SingleFileImporter(private val context: Context) {
Timber.d("Parsing Markdown with Page-Level Chaptering: $originalBookNameHint")
val title = originalBookNameHint.substringBeforeLast(".")
// Read the full markdown content
// Read the full Markdown content
val markdownContent = inputStream.bufferedReader().use { it.readText() }
// Flexmark Setup
@ -311,8 +311,8 @@ class SingleFileImporter(private val context: Context) {
private fun createBookFromHtmlBody(
title: String,
bodyContent: String?,
cssStyle: String?,
@Suppress("SameParameterValue") bodyContent: String?,
@Suppress("SameParameterValue")cssStyle: String?,
fileName: String,
preGeneratedFullHtml: String? = null,
author: String? = null

View file

@ -181,6 +181,9 @@ class CfiJsBridge(
@JavascriptInterface
fun onCfiExtracted(jsonResponse: String) {
try {
// --- ADDED LOG ---
Timber.tag("PosSaveDiag").d("CfiJsBridge.onCfiExtracted: Raw JSON received from JS: $jsonResponse")
val json = JSONObject(jsonResponse)
val cfi = json.optString("cfi", "/4")
val logArray = json.optJSONArray("log")
@ -200,7 +203,7 @@ class CfiJsBridge(
onCfiReady(cfi)
}
} catch (e: Exception) {
Timber.e(e, "Error parsing CFI JSON response: $jsonResponse")
Timber.tag("PosSaveDiag").e(e, "CfiJsBridge.onCfiExtracted: Error parsing CFI JSON response: $jsonResponse")
onCfiReady("/4")
}
}

View file

@ -2161,9 +2161,10 @@ fun EpubReaderHost(
showDictionaryUpsellDialog = true
},
onCfiGenerated = { cfi ->
Timber.tag("PosSaveDiag").d("EpubReaderScreen: onCfiGenerated callback triggered with CFI: '$cfi'")
if (cfi.isBlank() || !cfi.startsWith('/')) {
Timber.w("onCfiGenerated received an invalid CFI, aborting save: '$cfi'"
)
Timber.tag("PosSaveDiag").w("EpubReaderScreen: onCfiGenerated received an invalid CFI, aborting save: '$cfi'")
if (isSavingAndExiting) {
isSavingAndExiting = false
onNavigateBack()
@ -2172,6 +2173,7 @@ fun EpubReaderHost(
}
scope.launch {
Timber.tag("PosSaveDiag").d("EpubReaderScreen: Requesting locator conversion for chapter $latestChapterIndex")
val locator =
locatorConverter.getLocatorFromCfi(
epubBook,
@ -2179,10 +2181,11 @@ fun EpubReaderHost(
cfi
)
Timber.tag("PosSaveDiag").d("EpubReaderScreen: Locator conversion returned: $locator")
if (locator != null) {
lastKnownLocator = locator
// Calculate progress (Logic moved out of `val progress` block for scope visibility)
val progressWithinChapter =
if (currentScrollHeightValue > currentClientHeightValue) {
val scrollableHeight =
@ -2945,7 +2948,7 @@ fun EpubReaderHost(
} else {
scope.launch {
lastKnownLocator?.let { locator ->
val cfi = locatorConverter.getCfiFromLocator(epubBook.title, locator)
val cfi = locatorConverter.getCfiFromLocator(epubBook, locator)
if (cfi != null) {
val targetChunk = locator.blockIndex / 20
chunkTargetOverride = targetChunk

View file

@ -33,6 +33,7 @@ import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.decodeFromByteArray
import kotlinx.serialization.encodeToByteArray
import kotlinx.serialization.protobuf.ProtoBuf
import java.io.File
data class Locator(
val chapterIndex: Int,
@ -50,8 +51,41 @@ class LocatorConverter(
private val context: Context
) {
private suspend fun processAndCacheChapter(book: EpubBook, chapterIndex: Int): List<SemanticBlock>? = withContext(Dispatchers.IO) {
Timber.tag("PosSaveDiag").d("processAndCacheChapter: STARTED for book='${book.title}', chapterIndex=$chapterIndex")
try {
val chapter = book.chapters.getOrNull(chapterIndex) ?: return@withContext null
val chapter = book.chapters.getOrNull(chapterIndex)
if (chapter == null) {
Timber.tag("PosSaveDiag").e("processAndCacheChapter: FAILED. Chapter is null for index $chapterIndex")
return@withContext null
}
Timber.tag("PosSaveDiag").d("processAndCacheChapter: Checking HTML content. RAM content length: ${chapter.htmlContent.length}")
val htmlToParse = if (chapter.htmlContent.isNotBlank()) {
Timber.tag("PosSaveDiag").d("processAndCacheChapter: Using HTML from RAM.")
chapter.htmlContent
} else {
Timber.tag("PosSaveDiag").d("processAndCacheChapter: RAM HTML is blank. Falling back to disk. Path: ${book.extractionBasePath} / ${chapter.htmlFilePath}")
try {
val file = File(book.extractionBasePath, chapter.htmlFilePath)
if (file.exists()) {
val content = file.readText()
Timber.tag("PosSaveDiag").d("processAndCacheChapter: Read file from disk SUCCESS. Content length: ${content.length}")
content
} else {
Timber.tag("PosSaveDiag").e("processAndCacheChapter: File DOES NOT EXIST at ${file.absolutePath}")
""
}
} catch (e: Exception) {
Timber.tag("PosSaveDiag").e(e, "processAndCacheChapter: Exception reading chapter file from disk")
""
}
}
if (htmlToParse.isBlank()) {
Timber.tag("PosSaveDiag").w("processAndCacheChapter: Final HTML to parse is blank. Aborting semantic block generation.")
return@withContext null
}
val mergedByTag = mutableMapOf<String, MutableList<CssRule>>()
val mergedByClass = mutableMapOf<String, MutableList<CssRule>>()
@ -95,8 +129,9 @@ class LocatorConverter(
otherComplex = mergedOtherComplex
)
Timber.tag("PosSaveDiag").d("processAndCacheChapter: Calling htmlToSemanticBlocks...")
val semanticBlocks = htmlToSemanticBlocks(
html = chapter.htmlContent,
html = htmlToParse,
cssRules = parsingCssRules,
textStyle = TextStyle(),
chapterAbsPath = chapter.absPath,
@ -105,8 +140,11 @@ class LocatorConverter(
fontFamilyMap = emptyMap(),
constraints = constraints
)
Timber.tag("PosSaveDiag").d("processAndCacheChapter: htmlToSemanticBlocks returned ${semanticBlocks.size} blocks.")
val protoBytes = proto.encodeToByteArray(semanticBlocks)
Timber.tag("PosSaveDiag").d("processAndCacheChapter: Encoded blocks to protoBytes (size: ${protoBytes.size} bytes).")
val newCacheEntry = ProcessedChapter(
bookId = book.title,
chapterIndex = chapterIndex,
@ -114,50 +152,48 @@ class LocatorConverter(
estimatedPageCount = 0
)
bookCacheDao.insertProcessedChapters(listOf(newCacheEntry))
Timber.i("On-demand processing SUCCESS for chapter $chapterIndex.")
Timber.tag("PosSaveDiag").i("processAndCacheChapter: On-demand processing and DB caching SUCCESS for chapter $chapterIndex.")
semanticBlocks
} catch (e: Exception) {
Timber.e(e, "On-demand processing FAILED for chapter $chapterIndex")
Timber.tag("PosSaveDiag").e(e, "processAndCacheChapter: FAILED for chapter $chapterIndex")
null
}
}
/**
* Converts a CFI string from the WebView into an abstract Locator.
*/
suspend fun getLocatorFromCfi(book: EpubBook, chapterIndex: Int, cfi: String): Locator? = withContext(Dispatchers.IO) {
Timber.d("getLocatorFromCfi: Starting conversion for book='${book.title}', chapter=$chapterIndex, cfi='$cfi'")
val processedChapter = bookCacheDao.getProcessedChapter(bookId = book.title, chapterIndex = chapterIndex)
val allBlocks = if (processedChapter != null) {
var allBlocks: List<SemanticBlock>? = null
if (processedChapter != null && processedChapter.contentBlocksProto.isNotEmpty()) {
allBlocks = try {
proto.decodeFromByteArray<List<SemanticBlock>>(processedChapter.contentBlocksProto)
} else {
Timber.w("getLocatorFromCfi: Chapter $chapterIndex not in DB. Triggering on-demand processing.")
processAndCacheChapter(book, chapterIndex)
} catch (_: Exception) { null }
}
if (allBlocks == null) {
Timber.w("getLocatorFromCfi: FAILED. Could not get or process semantic blocks for chapter $chapterIndex.")
if (allBlocks.isNullOrEmpty()) {
Timber.tag("PosSaveDiag").w("getLocatorFromCfi: Cache missing or empty for chapter $chapterIndex. Triggering on-demand processing.")
allBlocks = processAndCacheChapter(book, chapterIndex)
}
if (allBlocks.isNullOrEmpty()) {
Timber.tag("PosSaveDiag").e("getLocatorFromCfi: FAILED. Could not get or process semantic blocks.")
return@withContext null
}
val (baseCfiPath, charOffset) = cfi.split(':').let {
it[0] to (it.getOrNull(1)?.toIntOrNull() ?: 0)
}
Timber.d("getLocatorFromCfi: Parsed CFI into basePath='$baseCfiPath' and charOffset=$charOffset")
val bestMatch = findBestMatchingBlock(allBlocks, baseCfiPath)
if (bestMatch != null) {
Timber.i("getLocatorFromCfi: SUCCESS. Found best match. Block index: ${bestMatch.blockIndex}, Block CFI: '${bestMatch.cfi}'")
Locator(
chapterIndex = chapterIndex,
blockIndex = bestMatch.blockIndex,
charOffset = charOffset
)
} else {
Timber.w("getLocatorFromCfi: FAILED. No matching block found for CFI base path '$baseCfiPath'.")
null
}
}
@ -200,29 +236,31 @@ class LocatorConverter(
return bestMatch
}
suspend fun getCfiFromLocator(bookId: String, locator: Locator): String? = withContext(Dispatchers.IO) {
Timber.d("getCfiFromLocator: Attempting to get CFI from locator: $locator")
val processedChapter = bookCacheDao.getProcessedChapter(bookId = bookId, chapterIndex = locator.chapterIndex)
if (processedChapter == null) {
Timber.w("getCfiFromLocator: FAILED. Could not find processed chapter ${locator.chapterIndex} in database.")
suspend fun getCfiFromLocator(book: EpubBook, locator: Locator): String? = withContext(Dispatchers.IO) {
val processedChapter = bookCacheDao.getProcessedChapter(bookId = book.title, chapterIndex = locator.chapterIndex)
var blocks: List<SemanticBlock>? = null
if (processedChapter != null && processedChapter.contentBlocksProto.isNotEmpty()) {
blocks = try {
proto.decodeFromByteArray<List<SemanticBlock>>(processedChapter.contentBlocksProto)
} catch (_: Exception) { null }
}
if (blocks.isNullOrEmpty()) {
blocks = processAndCacheChapter(book, locator.chapterIndex)
}
if (blocks.isNullOrEmpty()) {
return@withContext null
}
val blocks = proto.decodeFromByteArray<List<SemanticBlock>>(processedChapter.contentBlocksProto)
val foundBlock = findBlockByBlockIndex(blocks, locator.blockIndex)
if (foundBlock != null) {
foundBlock.cfi?.let { cfi ->
val finalCfi = if (locator.charOffset > 0) {
foundBlock?.cfi?.let { cfi ->
if (locator.charOffset > 0) {
"$cfi:${locator.charOffset}"
} else {
cfi
}
Timber.i("getCfiFromLocator: SUCCESS. Found block ${foundBlock.blockIndex} with CFI '${foundBlock.cfi}'. Final CFI: '$finalCfi'")
finalCfi
}
} else {
Timber.w("getCfiFromLocator: FAILED. Could not find block with index ${locator.blockIndex} in chapter ${locator.chapterIndex}.")
null
}
}
@ -249,11 +287,19 @@ class LocatorConverter(
suspend fun getTextOffset(book: EpubBook, locator: Locator): Int? = withContext(Dispatchers.IO) {
val processedChapter = bookCacheDao.getProcessedChapter(bookId = book.title, chapterIndex = locator.chapterIndex)
val allBlocks = if (processedChapter != null) {
var allBlocks: List<SemanticBlock>? = null
if (processedChapter != null && processedChapter.contentBlocksProto.isNotEmpty()) {
allBlocks = try {
proto.decodeFromByteArray<List<SemanticBlock>>(processedChapter.contentBlocksProto)
} else {
processAndCacheChapter(book, locator.chapterIndex)
} ?: return@withContext null
} catch(_: Exception) { null }
}
if (allBlocks.isNullOrEmpty()) {
allBlocks = processAndCacheChapter(book, locator.chapterIndex)
}
if (allBlocks.isNullOrEmpty()) return@withContext null
var offset = 0
val separatorLength = 1

View file

@ -1677,6 +1677,7 @@ fun PdfViewerScreen(
)
if (result == SnackbarResult.ActionPerformed) {
snackbarHostState.currentSnackbarData?.dismiss()
val item = uiState.recentFiles.find { it.bookId == reflowBookId }
if (item != null) {
viewModel.onRecentFileClicked(item)