Improving KOReader sync
This commit is contained in:
@@ -12,7 +12,7 @@
|
|||||||
#include "parsers/ChapterHtmlSlimParser.h"
|
#include "parsers/ChapterHtmlSlimParser.h"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
constexpr uint8_t SECTION_FILE_VERSION = 25;
|
constexpr uint8_t SECTION_FILE_VERSION = 26;
|
||||||
constexpr uint32_t HEADER_SIZE = sizeof(uint8_t) + // SECTION_FILE_VERSION
|
constexpr uint32_t HEADER_SIZE = sizeof(uint8_t) + // SECTION_FILE_VERSION
|
||||||
sizeof(int) + // fontId
|
sizeof(int) + // fontId
|
||||||
sizeof(float) + // lineCompression
|
sizeof(float) + // lineCompression
|
||||||
@@ -29,8 +29,11 @@ constexpr uint32_t HEADER_SIZE = sizeof(uint8_t) + // SECTION_FILE_VERSION
|
|||||||
sizeof(uint32_t) + // anchor map offset
|
sizeof(uint32_t) + // anchor map offset
|
||||||
sizeof(uint32_t); // paragraph LUT offset
|
sizeof(uint32_t); // paragraph LUT offset
|
||||||
|
|
||||||
// On-disk paragraph LUT entry: u32 xhtmlByteOffset + u16 paragraphIndex.
|
// On-disk paragraph LUT entry: u32 xhtmlByteOffset + u16 paragraphIndex + u16 listItemIndex.
|
||||||
constexpr uint32_t PARAGRAPH_LUT_ENTRY_SIZE = sizeof(uint32_t) + sizeof(uint16_t);
|
// listItemIndex is the running <li> count at page-break time; together with
|
||||||
|
// paragraphIndex it lets KOReader-supplied <p>- and <li>-anchored XPaths snap to
|
||||||
|
// the exact page on download.
|
||||||
|
constexpr uint32_t PARAGRAPH_LUT_ENTRY_SIZE = sizeof(uint32_t) + sizeof(uint16_t) + sizeof(uint16_t);
|
||||||
inline uint32_t paragraphLutEntryOffset(uint32_t lutStart, uint16_t page) {
|
inline uint32_t paragraphLutEntryOffset(uint32_t lutStart, uint16_t page) {
|
||||||
return lutStart + page * PARAGRAPH_LUT_ENTRY_SIZE;
|
return lutStart + page * PARAGRAPH_LUT_ENTRY_SIZE;
|
||||||
}
|
}
|
||||||
@@ -471,6 +474,7 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
|
|||||||
for (const auto& entry : paragraphLut) {
|
for (const auto& entry : paragraphLut) {
|
||||||
serialization::writePod(file, entry.xhtmlByteOffset);
|
serialization::writePod(file, entry.xhtmlByteOffset);
|
||||||
serialization::writePod(file, entry.paragraphIndex);
|
serialization::writePod(file, entry.paragraphIndex);
|
||||||
|
serialization::writePod(file, entry.listItemIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Patch header with final pageCount, lutOffset, anchorMapOffset, and paragraphLutOffset
|
// Patch header with final pageCount, lutOffset, anchorMapOffset, and paragraphLutOffset
|
||||||
@@ -803,6 +807,42 @@ std::optional<uint16_t> Section::getParagraphIndexForPage(const uint16_t page) c
|
|||||||
return pIdx;
|
return pIdx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::optional<uint16_t> Section::getPageForListItemIndex(const uint16_t liIndex) const {
|
||||||
|
if (liIndex == 0) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
FsFile f;
|
||||||
|
uint16_t count = 0;
|
||||||
|
uint32_t lutStart = 0;
|
||||||
|
if (!readParagraphLutHeader(f, count, lutStart)) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
const uint32_t fileSize = f.size();
|
||||||
|
|
||||||
|
// Mirror getPageForParagraphIndex: each entry stores the running li count at page-break
|
||||||
|
// time, so the target li first appears on the smallest i where storedLiIdx[i] >= liIndex.
|
||||||
|
// The listItemIndex field follows xhtmlByteOffset + paragraphIndex within each entry.
|
||||||
|
for (uint16_t i = 0; i < count; i++) {
|
||||||
|
const uint32_t entryOffset = paragraphLutEntryOffset(lutStart, i) + sizeof(uint32_t) + sizeof(uint16_t);
|
||||||
|
const uint64_t requiredOffset = static_cast<uint64_t>(entryOffset) + sizeof(uint16_t);
|
||||||
|
if (requiredOffset > fileSize) {
|
||||||
|
f.close();
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
f.seek(entryOffset);
|
||||||
|
uint16_t pageLiIdx;
|
||||||
|
serialization::readPod(f, pageLiIdx);
|
||||||
|
if (pageLiIdx >= liIndex) {
|
||||||
|
f.close();
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
f.close();
|
||||||
|
return static_cast<uint16_t>(count - 1);
|
||||||
|
}
|
||||||
|
|
||||||
std::optional<uint32_t> Section::getXhtmlByteOffsetForPage(const uint16_t page) const {
|
std::optional<uint32_t> Section::getXhtmlByteOffsetForPage(const uint16_t page) const {
|
||||||
FsFile f;
|
FsFile f;
|
||||||
uint16_t count = 0;
|
uint16_t count = 0;
|
||||||
|
|||||||
@@ -90,6 +90,12 @@ class Section {
|
|||||||
// Returns nullopt if the paragraph LUT is not available (old cache format).
|
// Returns nullopt if the paragraph LUT is not available (old cache format).
|
||||||
std::optional<uint16_t> getPageForParagraphIndex(uint16_t pIndex) const;
|
std::optional<uint16_t> getPageForParagraphIndex(uint16_t pIndex) const;
|
||||||
|
|
||||||
|
// Look up the page number for a running <li> index (1-based, the Nth <li> at any depth
|
||||||
|
// in the chapter). Used to snap KOReader-supplied list-item XPaths to a precise page
|
||||||
|
// the same way getPageForParagraphIndex handles <p>-anchored XPaths.
|
||||||
|
// Returns nullopt if the LUT is not available or the index is out of range.
|
||||||
|
std::optional<uint16_t> getPageForListItemIndex(uint16_t liIndex) const;
|
||||||
|
|
||||||
// Look up the paragraph index for a given page number.
|
// Look up the paragraph index for a given page number.
|
||||||
// Returns the 1-based paragraph index of the last <p> element on or before the page.
|
// Returns the 1-based paragraph index of the last <p> element on or before the page.
|
||||||
// Returns nullopt if the paragraph LUT is not available (old cache format).
|
// Returns nullopt if the paragraph LUT is not available (old cache format).
|
||||||
|
|||||||
@@ -250,7 +250,7 @@ void ChapterHtmlSlimParser::flushPartWordBuffer() {
|
|||||||
// Callers must ensure currentPage is non-null and carries content; the helper resets
|
// Callers must ensure currentPage is non-null and carries content; the helper resets
|
||||||
// currentPage to a fresh Page and zeroes currentPageNextY so the caller can keep building.
|
// currentPage to a fresh Page and zeroes currentPageNextY so the caller can keep building.
|
||||||
void ChapterHtmlSlimParser::emitPage(uint32_t xhtmlByteOffset) {
|
void ChapterHtmlSlimParser::emitPage(uint32_t xhtmlByteOffset) {
|
||||||
paragraphLutPerPage.push_back({xhtmlByteOffset, xpathParagraphIndex});
|
paragraphLutPerPage.push_back({xhtmlByteOffset, xpathParagraphIndex, xpathListItemIndex});
|
||||||
completePageFn(std::move(currentPage));
|
completePageFn(std::move(currentPage));
|
||||||
completedPageCount++;
|
completedPageCount++;
|
||||||
currentPage.reset(new Page());
|
currentPage.reset(new Page());
|
||||||
@@ -814,6 +814,13 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// <li> can appear nested inside <ul>/<ol> at any depth, so count it globally —
|
||||||
|
// not at body-child level. The running count must match what the runtime reverse
|
||||||
|
// mapper sees so getPageForListItemIndex can snap a KOReader li XPath to a page.
|
||||||
|
if (self->xpathBodyDepth >= 0 && strcmp(name, "li") == 0) {
|
||||||
|
self->xpathListItemIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
if (matches(name, SKIP_TAGS, NUM_SKIP_TAGS)) {
|
if (matches(name, SKIP_TAGS, NUM_SKIP_TAGS)) {
|
||||||
// start skip
|
// start skip
|
||||||
self->skipUntilDepth = self->depth;
|
self->skipUntilDepth = self->depth;
|
||||||
|
|||||||
@@ -108,7 +108,11 @@ class ChapterHtmlSlimParser final : public Print {
|
|||||||
// Stored per page in the section cache so that XPath p[N] can be resolved to a page
|
// Stored per page in the section cache so that XPath p[N] can be resolved to a page
|
||||||
// without reparsing, and current page can generate an XPath without reparsing.
|
// without reparsing, and current page can generate an XPath without reparsing.
|
||||||
uint16_t xpathParagraphIndex = 0; // current <p> sibling index (1-based)
|
uint16_t xpathParagraphIndex = 0; // current <p> sibling index (1-based)
|
||||||
int xpathBodyDepth = -1; // depth of the <body> element (-1 = not yet seen)
|
// Running count of <li> elements opened anywhere in the chapter (1-based, any depth).
|
||||||
|
// Used by the section LUT so KOReader-supplied list-item XPaths can snap to the exact
|
||||||
|
// page on download, the same way <p>-anchored XPaths use xpathParagraphIndex.
|
||||||
|
uint16_t xpathListItemIndex = 0;
|
||||||
|
int xpathBodyDepth = -1; // depth of the <body> element (-1 = not yet seen)
|
||||||
// Byte offset of the most recent direct-body-child element start (any tag at xpathBodyDepth+1).
|
// Byte offset of the most recent direct-body-child element start (any tag at xpathBodyDepth+1).
|
||||||
// Recorded at the same depth condition that increments xpathParagraphIndex, so the stored
|
// Recorded at the same depth condition that increments xpathParagraphIndex, so the stored
|
||||||
// offset is guaranteed to land on a body-child element boundary. This keeps the XPath forward
|
// offset is guaranteed to land on a body-child element boundary. This keeps the XPath forward
|
||||||
@@ -119,6 +123,7 @@ class ChapterHtmlSlimParser final : public Print {
|
|||||||
struct ParagraphLutEntry {
|
struct ParagraphLutEntry {
|
||||||
uint32_t xhtmlByteOffset; // byte offset of most recent body-child element start at page break
|
uint32_t xhtmlByteOffset; // byte offset of most recent body-child element start at page break
|
||||||
uint16_t paragraphIndex; // 1-based <p> index at page completion
|
uint16_t paragraphIndex; // 1-based <p> index at page completion
|
||||||
|
uint16_t listItemIndex; // running <li> count at page completion (any depth)
|
||||||
};
|
};
|
||||||
std::vector<ParagraphLutEntry> paragraphLutPerPage; // deep LUT: one entry per page
|
std::vector<ParagraphLutEntry> paragraphLutPerPage; // deep LUT: one entry per page
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,14 @@ namespace {
|
|||||||
// Strategy:
|
// Strategy:
|
||||||
// 1) Count total visible text bytes in chapter.
|
// 1) Count total visible text bytes in chapter.
|
||||||
// 2) Stream parse again and stop when target byte offset is reached.
|
// 2) Stream parse again and stop when target byte offset is reached.
|
||||||
// 3) Emit either an element path or /text()[N].M when at body text-node level.
|
// 3) Emit /text()[N].M relative to the deepest open element so KOReader can
|
||||||
|
// place the cursor at character precision regardless of nesting depth.
|
||||||
|
//
|
||||||
|
// Text-node counting matches KOReader/crengine: the Nth XML text node within
|
||||||
|
// an element, including whitespace-only nodes (those are still real DOM text
|
||||||
|
// nodes). Empty (len=0) text isn't emitted by expat at all, which mirrors
|
||||||
|
// KOReader's behavior of skipping the empty text nodes that bare
|
||||||
|
// <a id="anchor"/> elements would otherwise produce.
|
||||||
|
|
||||||
struct ForwardState : StackState {
|
struct ForwardState : StackState {
|
||||||
int spineIndex;
|
int spineIndex;
|
||||||
@@ -28,20 +35,32 @@ struct ForwardState : StackState {
|
|||||||
bool found = false;
|
bool found = false;
|
||||||
XML_Parser parser = nullptr;
|
XML_Parser parser = nullptr;
|
||||||
|
|
||||||
int bodyTextNodeCount = 0;
|
// Per-element text-node bookkeeping. Mirrors `stack` 1:1 — every push/pop
|
||||||
size_t codepointsInBodyTextNode = 0;
|
// appends/removes a counter so the top of the stack always refers to the
|
||||||
bool inBodyTextNode = false;
|
// currently open element. `pendingTextNode` is set after every element
|
||||||
|
// boundary so the next char data starts a fresh text node within whatever
|
||||||
|
// element is currently on top.
|
||||||
|
std::vector<int> textNodeIndexStack;
|
||||||
|
std::vector<size_t> codepointsInTextNodeStack;
|
||||||
|
bool pendingTextNode = true;
|
||||||
|
|
||||||
ForwardState(const int spineIndex, const size_t targetOffset) : spineIndex(spineIndex), targetOffset(targetOffset) {}
|
ForwardState(const int spineIndex, const size_t targetOffset) : spineIndex(spineIndex), targetOffset(targetOffset) {
|
||||||
|
textNodeIndexStack.reserve(32);
|
||||||
|
codepointsInTextNodeStack.reserve(32);
|
||||||
|
}
|
||||||
|
|
||||||
void onStartElement(const XML_Char* rawName) {
|
void onStartElement(const XML_Char* rawName) {
|
||||||
inBodyTextNode = false;
|
|
||||||
pushElement(rawName);
|
pushElement(rawName);
|
||||||
|
textNodeIndexStack.push_back(0);
|
||||||
|
codepointsInTextNodeStack.push_back(0);
|
||||||
|
pendingTextNode = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void onEndElement() {
|
void onEndElement() {
|
||||||
inBodyTextNode = false;
|
|
||||||
popElement();
|
popElement();
|
||||||
|
if (!textNodeIndexStack.empty()) textNodeIndexStack.pop_back();
|
||||||
|
if (!codepointsInTextNodeStack.empty()) codepointsInTextNodeStack.pop_back();
|
||||||
|
pendingTextNode = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void onCharData(const XML_Char* text, const int len) {
|
void onCharData(const XML_Char* text, const int len) {
|
||||||
@@ -49,29 +68,29 @@ struct ForwardState : StackState {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const bool atBodyLevel = bodyIdx() + 1 == static_cast<int>(stack.size());
|
if (pendingTextNode) {
|
||||||
if (atBodyLevel && !inBodyTextNode) {
|
if (!textNodeIndexStack.empty()) textNodeIndexStack.back()++;
|
||||||
inBodyTextNode = true;
|
if (!codepointsInTextNodeStack.empty()) codepointsInTextNodeStack.back() = 0;
|
||||||
bodyTextNodeCount++;
|
pendingTextNode = false;
|
||||||
codepointsInBodyTextNode = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const size_t cpCount = countUtf8Codepoints(text, len);
|
||||||
|
|
||||||
if (isWhitespaceOnly(text, len)) {
|
if (isWhitespaceOnly(text, len)) {
|
||||||
if (atBodyLevel) {
|
if (!codepointsInTextNodeStack.empty()) codepointsInTextNodeStack.back() += cpCount;
|
||||||
codepointsInBodyTextNode += countUtf8Codepoints(text, len);
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const size_t visible = countVisibleBytes(text, len);
|
const size_t visible = countVisibleBytes(text, len);
|
||||||
if (totalTextBytes + visible >= targetOffset) {
|
if (totalTextBytes + visible >= targetOffset) {
|
||||||
if (atBodyLevel && bodyTextNodeCount > 0) {
|
const int textNode = textNodeIndexStack.empty() ? 0 : textNodeIndexStack.back();
|
||||||
// KOReader/crengine text-point semantics use codepoint offsets.
|
const size_t cpsInNode = codepointsInTextNodeStack.empty() ? 0 : codepointsInTextNodeStack.back();
|
||||||
const size_t targetVisibleByteInChunk = targetOffset - totalTextBytes;
|
// KOReader/crengine text-point semantics use codepoint offsets.
|
||||||
const size_t cpInChunk = codepointAtVisibleByte(text, len, targetVisibleByteInChunk);
|
const size_t targetVisibleByteInChunk = targetOffset - totalTextBytes;
|
||||||
const size_t charOff = codepointsInBodyTextNode + cpInChunk;
|
const size_t cpInChunk = codepointAtVisibleByte(text, len, targetVisibleByteInChunk);
|
||||||
result =
|
const size_t charOff = cpsInNode + cpInChunk;
|
||||||
currentXPath(spineIndex) + "/text()[" + std::to_string(bodyTextNodeCount) + "]." + std::to_string(charOff);
|
if (textNode > 0) {
|
||||||
|
result = currentXPath(spineIndex) + "/text()[" + std::to_string(textNode) + "]." + std::to_string(charOff);
|
||||||
} else {
|
} else {
|
||||||
result = currentXPath(spineIndex);
|
result = currentXPath(spineIndex);
|
||||||
}
|
}
|
||||||
@@ -83,9 +102,7 @@ struct ForwardState : StackState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
totalTextBytes += visible;
|
totalTextBytes += visible;
|
||||||
if (atBodyLevel) {
|
if (!codepointsInTextNodeStack.empty()) codepointsInTextNodeStack.back() += cpCount;
|
||||||
codepointsInBodyTextNode += countUtf8Codepoints(text, len);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -29,8 +29,8 @@ std::string ChapterXPathIndexer::findXPathForParagraph(const std::shared_ptr<Epu
|
|||||||
|
|
||||||
bool ChapterXPathIndexer::findProgressForXPath(const std::shared_ptr<Epub>& epub, const int spineIndex,
|
bool ChapterXPathIndexer::findProgressForXPath(const std::shared_ptr<Epub>& epub, const int spineIndex,
|
||||||
const std::string& xpath, float& outIntraSpineProgress,
|
const std::string& xpath, float& outIntraSpineProgress,
|
||||||
bool& outExactMatch) {
|
bool& outExactMatch, uint16_t* outListItemIndex) {
|
||||||
return findProgressForXPathInternal(epub, spineIndex, xpath, outIntraSpineProgress, outExactMatch);
|
return findProgressForXPathInternal(epub, spineIndex, xpath, outIntraSpineProgress, outExactMatch, outListItemIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ChapterXPathIndexer::tryExtractSpineIndexFromXPath(const std::string& xpath, int& outSpineIndex) {
|
bool ChapterXPathIndexer::tryExtractSpineIndexFromXPath(const std::string& xpath, int& outSpineIndex) {
|
||||||
|
|||||||
@@ -46,10 +46,15 @@ class ChapterXPathIndexer {
|
|||||||
* @param xpath Incoming KOReader XPath
|
* @param xpath Incoming KOReader XPath
|
||||||
* @param outIntraSpineProgress Resolved position within spine [0.0, 1.0]
|
* @param outIntraSpineProgress Resolved position within spine [0.0, 1.0]
|
||||||
* @param outExactMatch True only for full exact path match
|
* @param outExactMatch True only for full exact path match
|
||||||
|
* @param outListItemIndex Optional. When non-null and the target XPath's deepest element
|
||||||
|
* is /li[N], receives the running <li> count at the matched element so the
|
||||||
|
* caller can snap to the precise page via Section::getPageForListItemIndex().
|
||||||
|
* Set to 0 if the target wasn't <li>-anchored.
|
||||||
* @return true if any match was resolved; false means caller should fallback
|
* @return true if any match was resolved; false means caller should fallback
|
||||||
*/
|
*/
|
||||||
static bool findProgressForXPath(const std::shared_ptr<Epub>& epub, int spineIndex, const std::string& xpath,
|
static bool findProgressForXPath(const std::shared_ptr<Epub>& epub, int spineIndex, const std::string& xpath,
|
||||||
float& outIntraSpineProgress, bool& outExactMatch);
|
float& outIntraSpineProgress, bool& outExactMatch,
|
||||||
|
uint16_t* outListItemIndex = nullptr);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse DocFragment index from KOReader-style path segment:
|
* Parse DocFragment index from KOReader-style path segment:
|
||||||
|
|||||||
@@ -40,11 +40,22 @@ struct ReverseState : StackState {
|
|||||||
size_t codepointsInCurrentTextNode = 0;
|
size_t codepointsInCurrentTextNode = 0;
|
||||||
int currentTextNodeCount = 0;
|
int currentTextNodeCount = 0;
|
||||||
|
|
||||||
|
// Running <li> count at any depth. Mirrors xpathListItemIndex in ChapterHtmlSlimParser
|
||||||
|
// so the index captured at match time can be used as a key into the section's li LUT.
|
||||||
|
int liCount = 0;
|
||||||
|
// True when the deepest element of targetNorm is /li[N]. Only then is bestLiIndex
|
||||||
|
// meaningful — for <p>-anchored targets we use the existing paragraph index path.
|
||||||
|
bool targetEndsInLi = false;
|
||||||
|
|
||||||
MatchTier bestTier = MatchTier::NONE;
|
MatchTier bestTier = MatchTier::NONE;
|
||||||
int bestDepth = -1;
|
int bestDepth = -1;
|
||||||
size_t bestOffset = 0;
|
size_t bestOffset = 0;
|
||||||
bool bestExact = false;
|
bool bestExact = false;
|
||||||
const char* bestTierName = nullptr;
|
const char* bestTierName = nullptr;
|
||||||
|
// Snapshot of liCount at the moment the best match was captured. The reverse
|
||||||
|
// mapper surfaces this so the runtime can call Section::getPageForListItemIndex()
|
||||||
|
// and snap a list-item XPath to the precise page on download.
|
||||||
|
int bestLiIndex = 0;
|
||||||
|
|
||||||
ReverseState(const int spineIndex, const std::string& xpath) : spineIndex(spineIndex) {
|
ReverseState(const int spineIndex, const std::string& xpath) : spineIndex(spineIndex) {
|
||||||
// Parse optional text-node suffix before normalizing for element matching.
|
// Parse optional text-node suffix before normalizing for element matching.
|
||||||
@@ -91,11 +102,27 @@ struct ReverseState : StackState {
|
|||||||
}
|
}
|
||||||
targetNorm = normalizeXPath(xpath);
|
targetNorm = normalizeXPath(xpath);
|
||||||
targetNoIndex = removeIndices(targetNorm);
|
targetNoIndex = removeIndices(targetNorm);
|
||||||
|
|
||||||
|
// Detect /li[N] as the deepest segment of targetNorm. normalizeXPath has already
|
||||||
|
// stripped any /text() and /text()[N].M suffix and lower-cased the tag names, so
|
||||||
|
// a simple tail check is enough.
|
||||||
|
const size_t lastSlash = targetNorm.rfind('/');
|
||||||
|
if (lastSlash != std::string::npos) {
|
||||||
|
const std::string tail = targetNorm.substr(lastSlash + 1);
|
||||||
|
if (tail.size() >= 2 && tail.compare(0, 2, "li") == 0 && (tail.size() == 2 || tail[2] == '[')) {
|
||||||
|
targetEndsInLi = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void onStartElement(const XML_Char* rawName) {
|
void onStartElement(const XML_Char* rawName) {
|
||||||
inParentTextNode = false;
|
inParentTextNode = false;
|
||||||
pushElement(rawName);
|
pushElement(rawName);
|
||||||
|
// Increment after pushElement so stack.back().tag is already lowercased and
|
||||||
|
// matches the parser-side counter, which also fires on startElement.
|
||||||
|
if (!stack.empty() && stack.back().tag == "li") {
|
||||||
|
liCount++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void onEndElement() {
|
void onEndElement() {
|
||||||
@@ -134,6 +161,7 @@ struct ReverseState : StackState {
|
|||||||
bestOffset = pos;
|
bestOffset = pos;
|
||||||
bestExact = true;
|
bestExact = true;
|
||||||
bestTierName = "text-node-exact";
|
bestTierName = "text-node-exact";
|
||||||
|
bestLiIndex = liCount;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
codepointsInCurrentTextNode += codepoints;
|
codepointsInCurrentTextNode += codepoints;
|
||||||
@@ -191,6 +219,7 @@ struct ReverseState : StackState {
|
|||||||
bestOffset = totalTextBytes;
|
bestOffset = totalTextBytes;
|
||||||
bestExact = isExact;
|
bestExact = isExact;
|
||||||
bestTierName = tierName;
|
bestTierName = tierName;
|
||||||
|
bestLiIndex = liCount;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -198,9 +227,12 @@ struct ReverseState : StackState {
|
|||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
bool findProgressForXPathInternal(const std::shared_ptr<Epub>& epub, const int spineIndex, const std::string& xpath,
|
bool findProgressForXPathInternal(const std::shared_ptr<Epub>& epub, const int spineIndex, const std::string& xpath,
|
||||||
float& outIntraSpineProgress, bool& outExactMatch) {
|
float& outIntraSpineProgress, bool& outExactMatch, uint16_t* outListItemIndex) {
|
||||||
outIntraSpineProgress = 0.0f;
|
outIntraSpineProgress = 0.0f;
|
||||||
outExactMatch = false;
|
outExactMatch = false;
|
||||||
|
if (outListItemIndex) {
|
||||||
|
*outListItemIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
if (xpath.empty()) {
|
if (xpath.empty()) {
|
||||||
return false;
|
return false;
|
||||||
@@ -244,13 +276,23 @@ bool findProgressForXPathInternal(const std::shared_ptr<Epub>& epub, const int s
|
|||||||
outIntraSpineProgress = std::max(0.0f, std::min(1.0f, outIntraSpineProgress));
|
outIntraSpineProgress = std::max(0.0f, std::min(1.0f, outIntraSpineProgress));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only surface the li index when the target was actually <li>-anchored AND we
|
||||||
|
// captured a count > 0. NO_IDX fallback tiers can match the wrong <li> sibling,
|
||||||
|
// so restrict to tiers that imply we were inside the target element itself.
|
||||||
|
if (outListItemIndex && state.targetEndsInLi && state.bestLiIndex > 0 &&
|
||||||
|
(state.bestTier == MatchTier::EXACT || state.bestTier == MatchTier::ANCESTOR) &&
|
||||||
|
state.bestLiIndex <= UINT16_MAX) {
|
||||||
|
*outListItemIndex = static_cast<uint16_t>(state.bestLiIndex);
|
||||||
|
}
|
||||||
|
|
||||||
if (state.targetTextNodeIndex > 0) {
|
if (state.targetTextNodeIndex > 0) {
|
||||||
LOG_DBG("KOX", "Reverse: spine=%d %s match textNode=%d char=%d offset=%zu/%zu -> progress=%.3f for '%s'",
|
LOG_DBG("KOX", "Reverse: spine=%d %s match textNode=%d char=%d offset=%zu/%zu -> progress=%.3f li=%d for '%s'",
|
||||||
spineIndex, state.bestTierName, state.targetTextNodeIndex, state.targetCharOffset, state.bestOffset,
|
spineIndex, state.bestTierName, state.targetTextNodeIndex, state.targetCharOffset, state.bestOffset,
|
||||||
state.totalTextBytes, outIntraSpineProgress, xpath.c_str());
|
state.totalTextBytes, outIntraSpineProgress, state.bestLiIndex, xpath.c_str());
|
||||||
} else {
|
} else {
|
||||||
LOG_DBG("KOX", "Reverse: spine=%d %s match offset=%zu/%zu -> progress=%.3f for '%s'", spineIndex,
|
LOG_DBG("KOX", "Reverse: spine=%d %s match offset=%zu/%zu -> progress=%.3f li=%d for '%s'", spineIndex,
|
||||||
state.bestTierName, state.bestOffset, state.totalTextBytes, outIntraSpineProgress, xpath.c_str());
|
state.bestTierName, state.bestOffset, state.totalTextBytes, outIntraSpineProgress, state.bestLiIndex,
|
||||||
|
xpath.c_str());
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,11 @@
|
|||||||
|
|
||||||
namespace ChapterXPathIndexerInternal {
|
namespace ChapterXPathIndexerInternal {
|
||||||
|
|
||||||
|
// outListItemIndex (when non-null) receives the running <li> count at the matched
|
||||||
|
// element's position whenever the target XPath's deepest element is /li[N]. Set to
|
||||||
|
// 0 if the target wasn't <li>-anchored or no match was found.
|
||||||
bool findProgressForXPathInternal(const std::shared_ptr<Epub>& epub, int spineIndex, const std::string& xpath,
|
bool findProgressForXPathInternal(const std::shared_ptr<Epub>& epub, int spineIndex, const std::string& xpath,
|
||||||
float& outIntraSpineProgress, bool& outExactMatch);
|
float& outIntraSpineProgress, bool& outExactMatch,
|
||||||
|
uint16_t* outListItemIndex = nullptr);
|
||||||
|
|
||||||
} // namespace ChapterXPathIndexerInternal
|
} // namespace ChapterXPathIndexerInternal
|
||||||
|
|||||||
@@ -48,10 +48,17 @@ bool resolveFromPercentage(const std::shared_ptr<Epub>& epub, const float percen
|
|||||||
KOReaderPosition ProgressMapper::toKOReader(const std::shared_ptr<Epub>& epub, const CrossPointPosition& pos) {
|
KOReaderPosition ProgressMapper::toKOReader(const std::shared_ptr<Epub>& epub, const CrossPointPosition& pos) {
|
||||||
KOReaderPosition result;
|
KOReaderPosition result;
|
||||||
|
|
||||||
// Calculate page progress within current spine item
|
// Calculate page progress within current spine item.
|
||||||
|
// Page numbers are 0-based and totalPages is 1-based, so the last page is
|
||||||
|
// (totalPages - 1). Dividing by (totalPages - 1) maps page 0 to intra=0 and the
|
||||||
|
// last page to intra=1, which is what KOReader expects when round-tripping.
|
||||||
|
// Dividing by totalPages would peg the last page short of 100%, so a user who
|
||||||
|
// finished the chapter would only show ~94% on KOReader.
|
||||||
float intraSpineProgress = 0.0f;
|
float intraSpineProgress = 0.0f;
|
||||||
if (pos.totalPages > 0) {
|
if (pos.totalPages > 1) {
|
||||||
intraSpineProgress = static_cast<float>(pos.pageNumber) / static_cast<float>(pos.totalPages);
|
intraSpineProgress = static_cast<float>(pos.pageNumber) / static_cast<float>(pos.totalPages - 1);
|
||||||
|
} else if (pos.totalPages == 1) {
|
||||||
|
intraSpineProgress = 0.0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate overall book progress (0.0-1.0)
|
// Calculate overall book progress (0.0-1.0)
|
||||||
@@ -98,11 +105,16 @@ CrossPointPosition ProgressMapper::toCrossPoint(const std::shared_ptr<Epub>& epu
|
|||||||
if (ChapterXPathIndexer::tryExtractSpineIndexFromXPath(koPos.xpath, xpathSpineIndex) && xpathSpineIndex >= 0 &&
|
if (ChapterXPathIndexer::tryExtractSpineIndexFromXPath(koPos.xpath, xpathSpineIndex) && xpathSpineIndex >= 0 &&
|
||||||
xpathSpineIndex < spineCount) {
|
xpathSpineIndex < spineCount) {
|
||||||
float intraFromXPath = 0.0f;
|
float intraFromXPath = 0.0f;
|
||||||
if (ChapterXPathIndexer::findProgressForXPath(epub, xpathSpineIndex, koPos.xpath, intraFromXPath,
|
uint16_t liIndexFromXPath = 0;
|
||||||
xpathExactMatch)) {
|
if (ChapterXPathIndexer::findProgressForXPath(epub, xpathSpineIndex, koPos.xpath, intraFromXPath, xpathExactMatch,
|
||||||
|
&liIndexFromXPath)) {
|
||||||
result.spineIndex = xpathSpineIndex;
|
result.spineIndex = xpathSpineIndex;
|
||||||
resolvedIntraSpineProgress = intraFromXPath;
|
resolvedIntraSpineProgress = intraFromXPath;
|
||||||
usedXPathMapping = true;
|
usedXPathMapping = true;
|
||||||
|
if (liIndexFromXPath > 0) {
|
||||||
|
result.listItemIndex = liIndexFromXPath;
|
||||||
|
result.hasListItemIndex = true;
|
||||||
|
}
|
||||||
|
|
||||||
// KOReader's text-node indexing can differ across renderers/parsers in some
|
// KOReader's text-node indexing can differ across renderers/parsers in some
|
||||||
// XHTML shapes. When an XPath-resolved position disagrees materially with
|
// XHTML shapes. When an XPath-resolved position disagrees materially with
|
||||||
@@ -177,15 +189,23 @@ CrossPointPosition ProgressMapper::toCrossPoint(const std::shared_ptr<Epub>& epu
|
|||||||
|
|
||||||
if (estimatedTotalPages > 0 && resolvedIntraSpineProgress >= 0.0f) {
|
if (estimatedTotalPages > 0 && resolvedIntraSpineProgress >= 0.0f) {
|
||||||
const float clampedProgress = std::max(0.0f, std::min(1.0f, resolvedIntraSpineProgress));
|
const float clampedProgress = std::max(0.0f, std::min(1.0f, resolvedIntraSpineProgress));
|
||||||
result.pageNumber = static_cast<int>(clampedProgress * static_cast<float>(estimatedTotalPages));
|
// Symmetric inverse of the toKOReader formula: intra=1.0 should land on the
|
||||||
|
// last page (totalPages - 1), intra=0 on page 0. Round-to-nearest avoids
|
||||||
|
// truncating mid-page progress down to the previous page.
|
||||||
|
if (estimatedTotalPages > 1) {
|
||||||
|
result.pageNumber = static_cast<int>(clampedProgress * static_cast<float>(estimatedTotalPages - 1) + 0.5f);
|
||||||
|
} else {
|
||||||
|
result.pageNumber = 0;
|
||||||
|
}
|
||||||
result.pageNumber = std::max(0, std::min(result.pageNumber, estimatedTotalPages - 1));
|
result.pageNumber = std::max(0, std::min(result.pageNumber, estimatedTotalPages - 1));
|
||||||
} else if (spineSize > 0 && estimatedTotalPages > 0) {
|
} else if (spineSize > 0 && estimatedTotalPages > 0) {
|
||||||
result.pageNumber = 0;
|
result.pageNumber = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG_DBG("ProgressMapper", "Resolved KOReader position: spine=%d intra=%.3f hasPIdx=%s pIdx=%u", result.spineIndex,
|
LOG_DBG("ProgressMapper", "Resolved KOReader position: spine=%d intra=%.3f hasPIdx=%s pIdx=%u hasLiIdx=%s liIdx=%u",
|
||||||
resolvedIntraSpineProgress, result.hasParagraphIndex ? "yes" : "no", result.paragraphIndex);
|
result.spineIndex, resolvedIntraSpineProgress, result.hasParagraphIndex ? "yes" : "no", result.paragraphIndex,
|
||||||
|
result.hasListItemIndex ? "yes" : "no", result.listItemIndex);
|
||||||
|
|
||||||
const char* mappingSource =
|
const char* mappingSource =
|
||||||
usedXPathMapping ? (usedPercentageReconcile ? "xpath+percentage" : "xpath") : "percentage";
|
usedXPathMapping ? (usedPercentageReconcile ? "xpath+percentage" : "xpath") : "percentage";
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ struct CrossPointPosition {
|
|||||||
int totalPages; // Total pages in the current spine item
|
int totalPages; // Total pages in the current spine item
|
||||||
uint16_t paragraphIndex = 0; // 1-based <p> index (0 if unavailable)
|
uint16_t paragraphIndex = 0; // 1-based <p> index (0 if unavailable)
|
||||||
bool hasParagraphIndex = false; // True when paragraphIndex is valid
|
bool hasParagraphIndex = false; // True when paragraphIndex is valid
|
||||||
|
uint16_t listItemIndex = 0; // 1-based running <li> count when target XPath ends in /li[N]
|
||||||
|
bool hasListItemIndex = false; // True when listItemIndex is valid
|
||||||
uint32_t xhtmlSeekHint = 0; // Byte offset hint for findXPathForParagraph (0 = no hint)
|
uint32_t xhtmlSeekHint = 0; // Byte offset hint for findXPathForParagraph (0 = no hint)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -52,6 +52,11 @@ struct KOReaderSyncSessionState {
|
|||||||
int resultPage = 0;
|
int resultPage = 0;
|
||||||
uint16_t resultParagraphIndex = 0;
|
uint16_t resultParagraphIndex = 0;
|
||||||
bool resultHasParagraphIndex = false;
|
bool resultHasParagraphIndex = false;
|
||||||
|
// Running <li> count for the matched element, used by EpubReaderActivity to snap a
|
||||||
|
// KOReader-supplied list-item XPath to the precise page via Section::getPageForListItemIndex.
|
||||||
|
// Preferred over resultParagraphIndex when the deepest target element is /li[N].
|
||||||
|
uint16_t resultListItemIndex = 0;
|
||||||
|
bool resultHasListItemIndex = false;
|
||||||
// When true (auto-push-on-close), the sync activity goes to home instead of the reader on
|
// When true (auto-push-on-close), the sync activity goes to home instead of the reader on
|
||||||
// completion. Without this, AUTO_PUSH would bounce back into the reader the user just left.
|
// completion. Without this, AUTO_PUSH would bounce back into the reader the user just left.
|
||||||
bool exitToHomeAfterSync = false;
|
bool exitToHomeAfterSync = false;
|
||||||
@@ -74,6 +79,8 @@ struct KOReaderSyncSessionState {
|
|||||||
resultPage = 0;
|
resultPage = 0;
|
||||||
resultParagraphIndex = 0;
|
resultParagraphIndex = 0;
|
||||||
resultHasParagraphIndex = false;
|
resultHasParagraphIndex = false;
|
||||||
|
resultListItemIndex = 0;
|
||||||
|
resultHasListItemIndex = false;
|
||||||
exitToHomeAfterSync = false;
|
exitToHomeAfterSync = false;
|
||||||
autoPullEpubPath.clear();
|
autoPullEpubPath.clear();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -101,6 +101,8 @@ bool JsonSettingsIO::saveState(const CrossPointState& s, const char* path) {
|
|||||||
sync["resultPage"] = s.koReaderSyncSession.resultPage;
|
sync["resultPage"] = s.koReaderSyncSession.resultPage;
|
||||||
sync["resultParagraphIndex"] = s.koReaderSyncSession.resultParagraphIndex;
|
sync["resultParagraphIndex"] = s.koReaderSyncSession.resultParagraphIndex;
|
||||||
sync["resultHasParagraphIndex"] = s.koReaderSyncSession.resultHasParagraphIndex;
|
sync["resultHasParagraphIndex"] = s.koReaderSyncSession.resultHasParagraphIndex;
|
||||||
|
sync["resultListItemIndex"] = s.koReaderSyncSession.resultListItemIndex;
|
||||||
|
sync["resultHasListItemIndex"] = s.koReaderSyncSession.resultHasListItemIndex;
|
||||||
sync["exitToHomeAfterSync"] = s.koReaderSyncSession.exitToHomeAfterSync;
|
sync["exitToHomeAfterSync"] = s.koReaderSyncSession.exitToHomeAfterSync;
|
||||||
sync["autoPullEpubPath"] = s.koReaderSyncSession.autoPullEpubPath;
|
sync["autoPullEpubPath"] = s.koReaderSyncSession.autoPullEpubPath;
|
||||||
// Information about a pending bookmark jump
|
// Information about a pending bookmark jump
|
||||||
@@ -158,6 +160,8 @@ bool JsonSettingsIO::loadState(CrossPointState& s, const char* json) {
|
|||||||
s.koReaderSyncSession.resultPage = sync["resultPage"] | 0;
|
s.koReaderSyncSession.resultPage = sync["resultPage"] | 0;
|
||||||
s.koReaderSyncSession.resultParagraphIndex = sync["resultParagraphIndex"] | (uint16_t)0;
|
s.koReaderSyncSession.resultParagraphIndex = sync["resultParagraphIndex"] | (uint16_t)0;
|
||||||
s.koReaderSyncSession.resultHasParagraphIndex = sync["resultHasParagraphIndex"] | false;
|
s.koReaderSyncSession.resultHasParagraphIndex = sync["resultHasParagraphIndex"] | false;
|
||||||
|
s.koReaderSyncSession.resultListItemIndex = sync["resultListItemIndex"] | (uint16_t)0;
|
||||||
|
s.koReaderSyncSession.resultHasListItemIndex = sync["resultHasListItemIndex"] | false;
|
||||||
s.koReaderSyncSession.exitToHomeAfterSync = sync["exitToHomeAfterSync"] | false;
|
s.koReaderSyncSession.exitToHomeAfterSync = sync["exitToHomeAfterSync"] | false;
|
||||||
s.koReaderSyncSession.autoPullEpubPath = sync["autoPullEpubPath"] | std::string("");
|
s.koReaderSyncSession.autoPullEpubPath = sync["autoPullEpubPath"] | std::string("");
|
||||||
if (s.koReaderSyncSession.autoPullEpubPath.empty() && (sync["autoPullOnOpen"] | false)) {
|
if (s.koReaderSyncSession.autoPullEpubPath.empty() && (sync["autoPullOnOpen"] | false)) {
|
||||||
|
|||||||
@@ -50,6 +50,8 @@ struct SyncResult {
|
|||||||
int page = 0; // estimated page (fallback)
|
int page = 0; // estimated page (fallback)
|
||||||
uint16_t paragraphIndex = 0; // 1-based <p> index from XPath
|
uint16_t paragraphIndex = 0; // 1-based <p> index from XPath
|
||||||
bool hasParagraphIndex = false; // true when paragraphIndex is available
|
bool hasParagraphIndex = false; // true when paragraphIndex is available
|
||||||
|
uint16_t listItemIndex = 0; // running <li> count when XPath ends in /li[N]
|
||||||
|
bool hasListItemIndex = false; // true when listItemIndex is available
|
||||||
};
|
};
|
||||||
|
|
||||||
enum class NetworkMode;
|
enum class NetworkMode;
|
||||||
|
|||||||
@@ -925,6 +925,8 @@ void EpubReaderActivity::applyPendingSyncSession() {
|
|||||||
int restorePage = sync.page;
|
int restorePage = sync.page;
|
||||||
pendingParagraphLookup = false;
|
pendingParagraphLookup = false;
|
||||||
pendingParagraphIndex = 0;
|
pendingParagraphIndex = 0;
|
||||||
|
pendingListItemLookup = false;
|
||||||
|
pendingListItemIndex = 0;
|
||||||
|
|
||||||
if (restoreSpineIndex < 0 || restoreSpineIndex >= epub->getSpineItemsCount()) {
|
if (restoreSpineIndex < 0 || restoreSpineIndex >= epub->getSpineItemsCount()) {
|
||||||
LOG_ERR("ERS", "Invalid sync restore spine index %d, resetting to 0", restoreSpineIndex);
|
LOG_ERR("ERS", "Invalid sync restore spine index %d, resetting to 0", restoreSpineIndex);
|
||||||
@@ -932,6 +934,8 @@ void EpubReaderActivity::applyPendingSyncSession() {
|
|||||||
restorePage = 0;
|
restorePage = 0;
|
||||||
pendingParagraphLookup = false;
|
pendingParagraphLookup = false;
|
||||||
pendingParagraphIndex = 0;
|
pendingParagraphIndex = 0;
|
||||||
|
pendingListItemLookup = false;
|
||||||
|
pendingListItemIndex = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sync.outcome == KOReaderSyncOutcomeState::APPLIED_REMOTE) {
|
if (sync.outcome == KOReaderSyncOutcomeState::APPLIED_REMOTE) {
|
||||||
@@ -939,8 +943,11 @@ void EpubReaderActivity::applyPendingSyncSession() {
|
|||||||
restorePage = sync.resultPage;
|
restorePage = sync.resultPage;
|
||||||
pendingParagraphLookup = sync.resultHasParagraphIndex;
|
pendingParagraphLookup = sync.resultHasParagraphIndex;
|
||||||
pendingParagraphIndex = sync.resultParagraphIndex;
|
pendingParagraphIndex = sync.resultParagraphIndex;
|
||||||
LOG_DBG("ERS", "Applied synced remote position: spine=%d page=%d paragraph=%u hasParagraph=%s", restoreSpineIndex,
|
pendingListItemLookup = sync.resultHasListItemIndex;
|
||||||
restorePage, pendingParagraphIndex, pendingParagraphLookup ? "yes" : "no");
|
pendingListItemIndex = sync.resultListItemIndex;
|
||||||
|
LOG_DBG("ERS", "Applied synced remote position: spine=%d page=%d paragraph=%u hasParagraph=%s liIdx=%u hasLi=%s",
|
||||||
|
restoreSpineIndex, restorePage, pendingParagraphIndex, pendingParagraphLookup ? "yes" : "no",
|
||||||
|
pendingListItemIndex, pendingListItemLookup ? "yes" : "no");
|
||||||
} else {
|
} else {
|
||||||
LOG_DBG("ERS", "Restored local pre-sync position: spine=%d page=%d", restoreSpineIndex, restorePage);
|
LOG_DBG("ERS", "Restored local pre-sync position: spine=%d page=%d", restoreSpineIndex, restorePage);
|
||||||
}
|
}
|
||||||
@@ -1369,16 +1376,30 @@ void EpubReaderActivity::render(RenderLock&& lock) {
|
|||||||
pendingAnchor.clear();
|
pendingAnchor.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve pending KOReader sync paragraph index to accurate page via Section paragraph LUT
|
// Resolve pending KOReader sync position via Section LUTs.
|
||||||
if (pendingParagraphLookup) {
|
// <li>-anchored XPaths can't be expressed in the body-child <p> LUT, so try the
|
||||||
|
// li LUT first when set; fall back to the paragraph LUT (which handles direct
|
||||||
|
// <p> children of <body>) on miss.
|
||||||
|
bool resolvedFromLut = false;
|
||||||
|
if (pendingListItemLookup) {
|
||||||
|
if (const auto page = section->getPageForListItemIndex(pendingListItemIndex)) {
|
||||||
|
section->currentPage = *page;
|
||||||
|
LOG_DBG("ERS", "Resolved li[%u] to page %d (was %d)", pendingListItemIndex, *page, nextPageNumber);
|
||||||
|
resolvedFromLut = true;
|
||||||
|
} else {
|
||||||
|
LOG_DBG("ERS", "Li index %u not found in section LUT", pendingListItemIndex);
|
||||||
|
}
|
||||||
|
pendingListItemLookup = false;
|
||||||
|
}
|
||||||
|
if (!resolvedFromLut && pendingParagraphLookup) {
|
||||||
if (const auto page = section->getPageForParagraphIndex(pendingParagraphIndex)) {
|
if (const auto page = section->getPageForParagraphIndex(pendingParagraphIndex)) {
|
||||||
section->currentPage = *page;
|
section->currentPage = *page;
|
||||||
LOG_DBG("ERS", "Resolved p[%u] to page %d (was %d)", pendingParagraphIndex, *page, nextPageNumber);
|
LOG_DBG("ERS", "Resolved p[%u] to page %d (was %d)", pendingParagraphIndex, *page, nextPageNumber);
|
||||||
} else {
|
} else {
|
||||||
LOG_DBG("ERS", "Paragraph LUT not available, using estimated page %d", nextPageNumber);
|
LOG_DBG("ERS", "Paragraph LUT not available, using estimated page %d", nextPageNumber);
|
||||||
}
|
}
|
||||||
pendingParagraphLookup = false;
|
|
||||||
}
|
}
|
||||||
|
pendingParagraphLookup = false;
|
||||||
|
|
||||||
// handles changes in reader settings and reset to approximate position based on cached progress
|
// handles changes in reader settings and reset to approximate position based on cached progress
|
||||||
if (cachedChapterTotalPageCount > 0) {
|
if (cachedChapterTotalPageCount > 0) {
|
||||||
|
|||||||
@@ -122,6 +122,11 @@ class EpubReaderActivity final : public Activity {
|
|||||||
// Pending paragraph index from KOReader sync (resolved to page via Section paragraph LUT)
|
// Pending paragraph index from KOReader sync (resolved to page via Section paragraph LUT)
|
||||||
bool pendingParagraphLookup = false;
|
bool pendingParagraphLookup = false;
|
||||||
uint16_t pendingParagraphIndex = 0;
|
uint16_t pendingParagraphIndex = 0;
|
||||||
|
// Pending list-item index for KOReader-supplied XPaths whose deepest element is /li[N].
|
||||||
|
// Preferred over pendingParagraphLookup when set because <li>-anchored XPaths are not
|
||||||
|
// representable in the body-child <p> LUT.
|
||||||
|
bool pendingListItemLookup = false;
|
||||||
|
uint16_t pendingListItemIndex = 0;
|
||||||
bool pendingScreenshot = false;
|
bool pendingScreenshot = false;
|
||||||
bool skipNextButtonCheck = false; // Skip button processing for one frame after subactivity exit
|
bool skipNextButtonCheck = false; // Skip button processing for one frame after subactivity exit
|
||||||
ReaderUtils::InputDrainGuard inputDrainGuard;
|
ReaderUtils::InputDrainGuard inputDrainGuard;
|
||||||
|
|||||||
@@ -220,6 +220,8 @@ void KOReaderSyncActivity::performFetchAndCompare() {
|
|||||||
remotePosition.totalPages = 0;
|
remotePosition.totalPages = 0;
|
||||||
remotePosition.paragraphIndex = 0;
|
remotePosition.paragraphIndex = 0;
|
||||||
remotePosition.hasParagraphIndex = false;
|
remotePosition.hasParagraphIndex = false;
|
||||||
|
remotePosition.listItemIndex = 0;
|
||||||
|
remotePosition.hasListItemIndex = false;
|
||||||
remoteChapterLabel.clear();
|
remoteChapterLabel.clear();
|
||||||
|
|
||||||
if (syncIntent == KOReaderSyncIntentState::PULL_REMOTE || syncIntent == KOReaderSyncIntentState::AUTO_PULL) {
|
if (syncIntent == KOReaderSyncIntentState::PULL_REMOTE || syncIntent == KOReaderSyncIntentState::AUTO_PULL) {
|
||||||
@@ -249,6 +251,8 @@ void KOReaderSyncActivity::performFetchAndCompare() {
|
|||||||
sync.resultPage = remotePosition.pageNumber;
|
sync.resultPage = remotePosition.pageNumber;
|
||||||
sync.resultParagraphIndex = remotePosition.paragraphIndex;
|
sync.resultParagraphIndex = remotePosition.paragraphIndex;
|
||||||
sync.resultHasParagraphIndex = remotePosition.hasParagraphIndex;
|
sync.resultHasParagraphIndex = remotePosition.hasParagraphIndex;
|
||||||
|
sync.resultListItemIndex = remotePosition.listItemIndex;
|
||||||
|
sync.resultHasListItemIndex = remotePosition.hasListItemIndex;
|
||||||
APP_STATE.saveToFile();
|
APP_STATE.saveToFile();
|
||||||
|
|
||||||
if (syncIntent == KOReaderSyncIntentState::AUTO_PULL) {
|
if (syncIntent == KOReaderSyncIntentState::AUTO_PULL) {
|
||||||
@@ -503,6 +507,8 @@ void KOReaderSyncActivity::resumeReader(const KOReaderSyncOutcomeState outcome,
|
|||||||
sync.resultPage = appliedResult->page;
|
sync.resultPage = appliedResult->page;
|
||||||
sync.resultParagraphIndex = appliedResult->paragraphIndex;
|
sync.resultParagraphIndex = appliedResult->paragraphIndex;
|
||||||
sync.resultHasParagraphIndex = appliedResult->hasParagraphIndex;
|
sync.resultHasParagraphIndex = appliedResult->hasParagraphIndex;
|
||||||
|
sync.resultListItemIndex = appliedResult->listItemIndex;
|
||||||
|
sync.resultHasListItemIndex = appliedResult->hasListItemIndex;
|
||||||
} else if (outcome != KOReaderSyncOutcomeState::APPLIED_REMOTE) {
|
} else if (outcome != KOReaderSyncOutcomeState::APPLIED_REMOTE) {
|
||||||
// Only zero the result fields when not resuming an already-applied remote
|
// Only zero the result fields when not resuming an already-applied remote
|
||||||
// position. The PULL_REMOTE path pre-saves the mapped result into APP_STATE
|
// position. The PULL_REMOTE path pre-saves the mapped result into APP_STATE
|
||||||
@@ -511,6 +517,8 @@ void KOReaderSyncActivity::resumeReader(const KOReaderSyncOutcomeState outcome,
|
|||||||
sync.resultPage = 0;
|
sync.resultPage = 0;
|
||||||
sync.resultParagraphIndex = 0;
|
sync.resultParagraphIndex = 0;
|
||||||
sync.resultHasParagraphIndex = false;
|
sync.resultHasParagraphIndex = false;
|
||||||
|
sync.resultListItemIndex = 0;
|
||||||
|
sync.resultHasListItemIndex = false;
|
||||||
}
|
}
|
||||||
// Honor exit-to-home flag set by reader-close auto-sync — bouncing back into the reader
|
// Honor exit-to-home flag set by reader-close auto-sync — bouncing back into the reader
|
||||||
// the user just left would be jarring. The session state is consumed and cleared by the
|
// the user just left would be jarring. The session state is consumed and cleared by the
|
||||||
@@ -792,8 +800,9 @@ void KOReaderSyncActivity::loop() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Wifi will be turned off in onExit()
|
// Wifi will be turned off in onExit()
|
||||||
const SyncResult result = {remotePosition.spineIndex, remotePosition.pageNumber, remotePosition.paragraphIndex,
|
const SyncResult result = {remotePosition.spineIndex, remotePosition.pageNumber,
|
||||||
remotePosition.hasParagraphIndex};
|
remotePosition.paragraphIndex, remotePosition.hasParagraphIndex,
|
||||||
|
remotePosition.listItemIndex, remotePosition.hasListItemIndex};
|
||||||
resumeReader(KOReaderSyncOutcomeState::APPLIED_REMOTE, &result);
|
resumeReader(KOReaderSyncOutcomeState::APPLIED_REMOTE, &result);
|
||||||
} else if (selectedOption == 1) {
|
} else if (selectedOption == 1) {
|
||||||
// Upload local progress
|
// Upload local progress
|
||||||
|
|||||||
@@ -149,6 +149,8 @@ void ReaderActivity::onGoToEpubReader(std::unique_ptr<Epub> epub) {
|
|||||||
sync.resultPage = 0;
|
sync.resultPage = 0;
|
||||||
sync.resultParagraphIndex = 0;
|
sync.resultParagraphIndex = 0;
|
||||||
sync.resultHasParagraphIndex = false;
|
sync.resultHasParagraphIndex = false;
|
||||||
|
sync.resultListItemIndex = 0;
|
||||||
|
sync.resultHasListItemIndex = false;
|
||||||
sync.exitToHomeAfterSync = false;
|
sync.exitToHomeAfterSync = false;
|
||||||
APP_STATE.saveToFile();
|
APP_STATE.saveToFile();
|
||||||
// Drop the loaded Epub before TLS — sync activity will reload it for remote-position
|
// Drop the loaded Epub before TLS — sync activity will reload it for remote-position
|
||||||
|
|||||||
Reference in New Issue
Block a user