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:
parent
b9f8825ad7
commit
6bc4f9bafd
5 changed files with 104 additions and 51 deletions
|
|
@ -60,7 +60,7 @@ class SingleFileImporter(private val context: Context) {
|
||||||
Timber.d("Parsing Markdown with Page-Level Chaptering: $originalBookNameHint")
|
Timber.d("Parsing Markdown with Page-Level Chaptering: $originalBookNameHint")
|
||||||
val title = originalBookNameHint.substringBeforeLast(".")
|
val title = originalBookNameHint.substringBeforeLast(".")
|
||||||
|
|
||||||
// Read the full markdown content
|
// Read the full Markdown content
|
||||||
val markdownContent = inputStream.bufferedReader().use { it.readText() }
|
val markdownContent = inputStream.bufferedReader().use { it.readText() }
|
||||||
|
|
||||||
// Flexmark Setup
|
// Flexmark Setup
|
||||||
|
|
@ -311,8 +311,8 @@ class SingleFileImporter(private val context: Context) {
|
||||||
|
|
||||||
private fun createBookFromHtmlBody(
|
private fun createBookFromHtmlBody(
|
||||||
title: String,
|
title: String,
|
||||||
bodyContent: String?,
|
@Suppress("SameParameterValue") bodyContent: String?,
|
||||||
cssStyle: String?,
|
@Suppress("SameParameterValue")cssStyle: String?,
|
||||||
fileName: String,
|
fileName: String,
|
||||||
preGeneratedFullHtml: String? = null,
|
preGeneratedFullHtml: String? = null,
|
||||||
author: String? = null
|
author: String? = null
|
||||||
|
|
|
||||||
|
|
@ -181,6 +181,9 @@ class CfiJsBridge(
|
||||||
@JavascriptInterface
|
@JavascriptInterface
|
||||||
fun onCfiExtracted(jsonResponse: String) {
|
fun onCfiExtracted(jsonResponse: String) {
|
||||||
try {
|
try {
|
||||||
|
// --- ADDED LOG ---
|
||||||
|
Timber.tag("PosSaveDiag").d("CfiJsBridge.onCfiExtracted: Raw JSON received from JS: $jsonResponse")
|
||||||
|
|
||||||
val json = JSONObject(jsonResponse)
|
val json = JSONObject(jsonResponse)
|
||||||
val cfi = json.optString("cfi", "/4")
|
val cfi = json.optString("cfi", "/4")
|
||||||
val logArray = json.optJSONArray("log")
|
val logArray = json.optJSONArray("log")
|
||||||
|
|
@ -200,7 +203,7 @@ class CfiJsBridge(
|
||||||
onCfiReady(cfi)
|
onCfiReady(cfi)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} 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")
|
onCfiReady("/4")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2161,9 +2161,10 @@ fun EpubReaderHost(
|
||||||
showDictionaryUpsellDialog = true
|
showDictionaryUpsellDialog = true
|
||||||
},
|
},
|
||||||
onCfiGenerated = { cfi ->
|
onCfiGenerated = { cfi ->
|
||||||
|
Timber.tag("PosSaveDiag").d("EpubReaderScreen: onCfiGenerated callback triggered with CFI: '$cfi'")
|
||||||
|
|
||||||
if (cfi.isBlank() || !cfi.startsWith('/')) {
|
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) {
|
if (isSavingAndExiting) {
|
||||||
isSavingAndExiting = false
|
isSavingAndExiting = false
|
||||||
onNavigateBack()
|
onNavigateBack()
|
||||||
|
|
@ -2172,6 +2173,7 @@ fun EpubReaderHost(
|
||||||
}
|
}
|
||||||
|
|
||||||
scope.launch {
|
scope.launch {
|
||||||
|
Timber.tag("PosSaveDiag").d("EpubReaderScreen: Requesting locator conversion for chapter $latestChapterIndex")
|
||||||
val locator =
|
val locator =
|
||||||
locatorConverter.getLocatorFromCfi(
|
locatorConverter.getLocatorFromCfi(
|
||||||
epubBook,
|
epubBook,
|
||||||
|
|
@ -2179,10 +2181,11 @@ fun EpubReaderHost(
|
||||||
cfi
|
cfi
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Timber.tag("PosSaveDiag").d("EpubReaderScreen: Locator conversion returned: $locator")
|
||||||
|
|
||||||
if (locator != null) {
|
if (locator != null) {
|
||||||
lastKnownLocator = locator
|
lastKnownLocator = locator
|
||||||
|
|
||||||
// Calculate progress (Logic moved out of `val progress` block for scope visibility)
|
|
||||||
val progressWithinChapter =
|
val progressWithinChapter =
|
||||||
if (currentScrollHeightValue > currentClientHeightValue) {
|
if (currentScrollHeightValue > currentClientHeightValue) {
|
||||||
val scrollableHeight =
|
val scrollableHeight =
|
||||||
|
|
@ -2945,7 +2948,7 @@ fun EpubReaderHost(
|
||||||
} else {
|
} else {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
lastKnownLocator?.let { locator ->
|
lastKnownLocator?.let { locator ->
|
||||||
val cfi = locatorConverter.getCfiFromLocator(epubBook.title, locator)
|
val cfi = locatorConverter.getCfiFromLocator(epubBook, locator)
|
||||||
if (cfi != null) {
|
if (cfi != null) {
|
||||||
val targetChunk = locator.blockIndex / 20
|
val targetChunk = locator.blockIndex / 20
|
||||||
chunkTargetOverride = targetChunk
|
chunkTargetOverride = targetChunk
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
import kotlinx.serialization.decodeFromByteArray
|
import kotlinx.serialization.decodeFromByteArray
|
||||||
import kotlinx.serialization.encodeToByteArray
|
import kotlinx.serialization.encodeToByteArray
|
||||||
import kotlinx.serialization.protobuf.ProtoBuf
|
import kotlinx.serialization.protobuf.ProtoBuf
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
data class Locator(
|
data class Locator(
|
||||||
val chapterIndex: Int,
|
val chapterIndex: Int,
|
||||||
|
|
@ -50,8 +51,41 @@ class LocatorConverter(
|
||||||
private val context: Context
|
private val context: Context
|
||||||
) {
|
) {
|
||||||
private suspend fun processAndCacheChapter(book: EpubBook, chapterIndex: Int): List<SemanticBlock>? = withContext(Dispatchers.IO) {
|
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 {
|
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 mergedByTag = mutableMapOf<String, MutableList<CssRule>>()
|
||||||
val mergedByClass = mutableMapOf<String, MutableList<CssRule>>()
|
val mergedByClass = mutableMapOf<String, MutableList<CssRule>>()
|
||||||
|
|
@ -95,8 +129,9 @@ class LocatorConverter(
|
||||||
otherComplex = mergedOtherComplex
|
otherComplex = mergedOtherComplex
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Timber.tag("PosSaveDiag").d("processAndCacheChapter: Calling htmlToSemanticBlocks...")
|
||||||
val semanticBlocks = htmlToSemanticBlocks(
|
val semanticBlocks = htmlToSemanticBlocks(
|
||||||
html = chapter.htmlContent,
|
html = htmlToParse,
|
||||||
cssRules = parsingCssRules,
|
cssRules = parsingCssRules,
|
||||||
textStyle = TextStyle(),
|
textStyle = TextStyle(),
|
||||||
chapterAbsPath = chapter.absPath,
|
chapterAbsPath = chapter.absPath,
|
||||||
|
|
@ -105,8 +140,11 @@ class LocatorConverter(
|
||||||
fontFamilyMap = emptyMap(),
|
fontFamilyMap = emptyMap(),
|
||||||
constraints = constraints
|
constraints = constraints
|
||||||
)
|
)
|
||||||
|
Timber.tag("PosSaveDiag").d("processAndCacheChapter: htmlToSemanticBlocks returned ${semanticBlocks.size} blocks.")
|
||||||
|
|
||||||
val protoBytes = proto.encodeToByteArray(semanticBlocks)
|
val protoBytes = proto.encodeToByteArray(semanticBlocks)
|
||||||
|
Timber.tag("PosSaveDiag").d("processAndCacheChapter: Encoded blocks to protoBytes (size: ${protoBytes.size} bytes).")
|
||||||
|
|
||||||
val newCacheEntry = ProcessedChapter(
|
val newCacheEntry = ProcessedChapter(
|
||||||
bookId = book.title,
|
bookId = book.title,
|
||||||
chapterIndex = chapterIndex,
|
chapterIndex = chapterIndex,
|
||||||
|
|
@ -114,50 +152,48 @@ class LocatorConverter(
|
||||||
estimatedPageCount = 0
|
estimatedPageCount = 0
|
||||||
)
|
)
|
||||||
bookCacheDao.insertProcessedChapters(listOf(newCacheEntry))
|
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
|
semanticBlocks
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e, "On-demand processing FAILED for chapter $chapterIndex")
|
Timber.tag("PosSaveDiag").e(e, "processAndCacheChapter: FAILED for chapter $chapterIndex")
|
||||||
null
|
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) {
|
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 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)
|
proto.decodeFromByteArray<List<SemanticBlock>>(processedChapter.contentBlocksProto)
|
||||||
} else {
|
} catch (_: Exception) { null }
|
||||||
Timber.w("getLocatorFromCfi: Chapter $chapterIndex not in DB. Triggering on-demand processing.")
|
|
||||||
processAndCacheChapter(book, chapterIndex)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (allBlocks == null) {
|
if (allBlocks.isNullOrEmpty()) {
|
||||||
Timber.w("getLocatorFromCfi: FAILED. Could not get or process semantic blocks for chapter $chapterIndex.")
|
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
|
return@withContext null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
val (baseCfiPath, charOffset) = cfi.split(':').let {
|
val (baseCfiPath, charOffset) = cfi.split(':').let {
|
||||||
it[0] to (it.getOrNull(1)?.toIntOrNull() ?: 0)
|
it[0] to (it.getOrNull(1)?.toIntOrNull() ?: 0)
|
||||||
}
|
}
|
||||||
Timber.d("getLocatorFromCfi: Parsed CFI into basePath='$baseCfiPath' and charOffset=$charOffset")
|
|
||||||
|
|
||||||
val bestMatch = findBestMatchingBlock(allBlocks, baseCfiPath)
|
val bestMatch = findBestMatchingBlock(allBlocks, baseCfiPath)
|
||||||
|
|
||||||
if (bestMatch != null) {
|
if (bestMatch != null) {
|
||||||
Timber.i("getLocatorFromCfi: SUCCESS. Found best match. Block index: ${bestMatch.blockIndex}, Block CFI: '${bestMatch.cfi}'")
|
|
||||||
Locator(
|
Locator(
|
||||||
chapterIndex = chapterIndex,
|
chapterIndex = chapterIndex,
|
||||||
blockIndex = bestMatch.blockIndex,
|
blockIndex = bestMatch.blockIndex,
|
||||||
charOffset = charOffset
|
charOffset = charOffset
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
Timber.w("getLocatorFromCfi: FAILED. No matching block found for CFI base path '$baseCfiPath'.")
|
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -200,29 +236,31 @@ class LocatorConverter(
|
||||||
return bestMatch
|
return bestMatch
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getCfiFromLocator(bookId: String, locator: Locator): String? = withContext(Dispatchers.IO) {
|
suspend fun getCfiFromLocator(book: EpubBook, locator: Locator): String? = withContext(Dispatchers.IO) {
|
||||||
Timber.d("getCfiFromLocator: Attempting to get CFI from locator: $locator")
|
val processedChapter = bookCacheDao.getProcessedChapter(bookId = book.title, chapterIndex = locator.chapterIndex)
|
||||||
val processedChapter = bookCacheDao.getProcessedChapter(bookId = bookId, chapterIndex = locator.chapterIndex)
|
|
||||||
if (processedChapter == null) {
|
var blocks: List<SemanticBlock>? = null
|
||||||
Timber.w("getCfiFromLocator: FAILED. Could not find processed chapter ${locator.chapterIndex} in database.")
|
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
|
return@withContext null
|
||||||
}
|
}
|
||||||
val blocks = proto.decodeFromByteArray<List<SemanticBlock>>(processedChapter.contentBlocksProto)
|
|
||||||
|
|
||||||
val foundBlock = findBlockByBlockIndex(blocks, locator.blockIndex)
|
val foundBlock = findBlockByBlockIndex(blocks, locator.blockIndex)
|
||||||
if (foundBlock != null) {
|
foundBlock?.cfi?.let { cfi ->
|
||||||
foundBlock.cfi?.let { cfi ->
|
if (locator.charOffset > 0) {
|
||||||
val finalCfi = if (locator.charOffset > 0) {
|
|
||||||
"$cfi:${locator.charOffset}"
|
"$cfi:${locator.charOffset}"
|
||||||
} else {
|
} else {
|
||||||
cfi
|
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) {
|
suspend fun getTextOffset(book: EpubBook, locator: Locator): Int? = withContext(Dispatchers.IO) {
|
||||||
val processedChapter = bookCacheDao.getProcessedChapter(bookId = book.title, chapterIndex = locator.chapterIndex)
|
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)
|
proto.decodeFromByteArray<List<SemanticBlock>>(processedChapter.contentBlocksProto)
|
||||||
} else {
|
} catch(_: Exception) { null }
|
||||||
processAndCacheChapter(book, locator.chapterIndex)
|
}
|
||||||
} ?: return@withContext null
|
|
||||||
|
if (allBlocks.isNullOrEmpty()) {
|
||||||
|
allBlocks = processAndCacheChapter(book, locator.chapterIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allBlocks.isNullOrEmpty()) return@withContext null
|
||||||
|
|
||||||
var offset = 0
|
var offset = 0
|
||||||
val separatorLength = 1
|
val separatorLength = 1
|
||||||
|
|
|
||||||
|
|
@ -1677,6 +1677,7 @@ fun PdfViewerScreen(
|
||||||
)
|
)
|
||||||
|
|
||||||
if (result == SnackbarResult.ActionPerformed) {
|
if (result == SnackbarResult.ActionPerformed) {
|
||||||
|
snackbarHostState.currentSnackbarData?.dismiss()
|
||||||
val item = uiState.recentFiles.find { it.bookId == reflowBookId }
|
val item = uiState.recentFiles.find { it.bookId == reflowBookId }
|
||||||
if (item != null) {
|
if (item != null) {
|
||||||
viewModel.onRecentFileClicked(item)
|
viewModel.onRecentFileClicked(item)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue