fix: Switch to xpath map for paragraph level syncing in KOSync (#1686)
Switch KOReader sync progress mapping from chapter matching to XPath-based mapping. - resolves KOReader positions using real XHTML ancestry paths - supports paragraph-based upload mapping with text offsets where needed - passes the current paragraph index into sync so uploads map back to KOReader more accurately No HTTP client changes are included. No reader-state or resume-flow changes are included. --------- Co-authored-by: jpirnay <jens@pirnay.com>
This commit is contained in:
co-authored by
jpirnay
parent
e8645ed92e
commit
302dea1eea
@@ -10,6 +10,8 @@
|
||||
#include <Logging.h>
|
||||
#include <esp_system.h>
|
||||
|
||||
#include <limits>
|
||||
|
||||
#include "CrossPointSettings.h"
|
||||
#include "CrossPointState.h"
|
||||
#include "EpubReaderChapterSelectionActivity.h"
|
||||
@@ -63,6 +65,13 @@ void EpubReaderActivity::onEnter() {
|
||||
if (dataSize == 4 || dataSize == 6) {
|
||||
currentSpineIndex = data[0] + (data[1] << 8);
|
||||
nextPageNumber = data[2] + (data[3] << 8);
|
||||
if (nextPageNumber == UINT16_MAX) {
|
||||
// UINT16_MAX is an in-memory navigation sentinel for "open previous
|
||||
// chapter on its last page". It should never be treated as persisted
|
||||
// resume state after sleep or reopen.
|
||||
LOG_DBG("ERS", "Ignoring stale last-page sentinel from progress cache");
|
||||
nextPageNumber = 0;
|
||||
}
|
||||
cachedSpineIndex = currentSpineIndex;
|
||||
LOG_DBG("ERS", "Loaded cache: %d, %d", currentSpineIndex, nextPageNumber);
|
||||
}
|
||||
@@ -186,7 +195,8 @@ void EpubReaderActivity::loop() {
|
||||
onGoHome();
|
||||
} else {
|
||||
currentSpineIndex = epub->getSpineItemsCount() - 1;
|
||||
nextPageNumber = UINT16_MAX;
|
||||
nextPageNumber = 0;
|
||||
pendingPageJump = std::numeric_limits<uint16_t>::max();
|
||||
requestUpdate();
|
||||
}
|
||||
return;
|
||||
@@ -390,11 +400,19 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction
|
||||
}
|
||||
case EpubReaderMenuActivity::MenuAction::SYNC: {
|
||||
if (KOREADER_STORE.hasCredentials()) {
|
||||
const int currentPage = section ? section->currentPage : 0;
|
||||
const int totalPages = section ? section->pageCount : 0;
|
||||
const int currentPage = section ? section->currentPage : nextPageNumber;
|
||||
const int totalPages = section ? section->pageCount : cachedChapterTotalPageCount;
|
||||
std::optional<uint16_t> paragraphIndex;
|
||||
if (section && currentPage >= 0 && currentPage < section->pageCount) {
|
||||
const uint16_t paragraphPage =
|
||||
currentPage > 0 ? static_cast<uint16_t>(currentPage - 1) : static_cast<uint16_t>(currentPage);
|
||||
if (const auto pIdx = section->getParagraphIndexForPage(paragraphPage)) {
|
||||
paragraphIndex = *pIdx;
|
||||
}
|
||||
}
|
||||
startActivityForResult(
|
||||
std::make_unique<KOReaderSyncActivity>(renderer, mappedInput, epub, epub->getPath(), currentSpineIndex,
|
||||
currentPage, totalPages),
|
||||
currentPage, totalPages, paragraphIndex),
|
||||
[this](const ActivityResult& result) {
|
||||
if (!result.isCancelled) {
|
||||
const auto& sync = std::get<SyncResult>(result.data);
|
||||
@@ -402,6 +420,9 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction
|
||||
RenderLock lock(*this);
|
||||
currentSpineIndex = sync.spineIndex;
|
||||
nextPageNumber = sync.page;
|
||||
cachedChapterTotalPageCount = 0; // Prevent rescaling sync page
|
||||
pendingPageJump.reset();
|
||||
saveProgress(currentSpineIndex, nextPageNumber, 0);
|
||||
section.reset();
|
||||
}
|
||||
}
|
||||
@@ -484,7 +505,8 @@ void EpubReaderActivity::pageTurn(bool isForwardTurn) {
|
||||
// We don't want to delete the section mid-render, so grab the semaphore
|
||||
{
|
||||
RenderLock lock(*this);
|
||||
nextPageNumber = UINT16_MAX;
|
||||
nextPageNumber = 0;
|
||||
pendingPageJump = std::numeric_limits<uint16_t>::max();
|
||||
currentSpineIndex--;
|
||||
section.reset();
|
||||
}
|
||||
@@ -566,10 +588,21 @@ void EpubReaderActivity::render(RenderLock&& lock) {
|
||||
LOG_DBG("ERS", "Cache found, skipping build...");
|
||||
}
|
||||
|
||||
if (nextPageNumber == UINT16_MAX) {
|
||||
section->currentPage = section->pageCount - 1;
|
||||
if (pendingPageJump.has_value()) {
|
||||
if (*pendingPageJump >= section->pageCount && section->pageCount > 0) {
|
||||
section->currentPage = section->pageCount - 1;
|
||||
} else {
|
||||
section->currentPage = *pendingPageJump;
|
||||
}
|
||||
pendingPageJump.reset();
|
||||
} else {
|
||||
section->currentPage = nextPageNumber;
|
||||
if (section->currentPage < 0) {
|
||||
section->currentPage = 0;
|
||||
} else if (section->currentPage >= section->pageCount && section->pageCount > 0) {
|
||||
LOG_DBG("ERS", "Clamping cached page %d to %d", section->currentPage, section->pageCount - 1);
|
||||
section->currentPage = section->pageCount - 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (!pendingAnchor.empty()) {
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
#include <Epub/FootnoteEntry.h>
|
||||
#include <Epub/Section.h>
|
||||
|
||||
#include <optional>
|
||||
|
||||
#include "EpubReaderMenuActivity.h"
|
||||
#include "activities/Activity.h"
|
||||
|
||||
@@ -11,6 +13,7 @@ class EpubReaderActivity final : public Activity {
|
||||
std::unique_ptr<Section> section = nullptr;
|
||||
int currentSpineIndex = 0;
|
||||
int nextPageNumber = 0;
|
||||
std::optional<uint16_t> pendingPageJump;
|
||||
// Set when navigating to a footnote href with a fragment (e.g. #note1).
|
||||
// Cleared on the next render after the new section loads and resolves it to a page.
|
||||
std::string pendingAnchor;
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <WiFi.h>
|
||||
#include <esp_sntp.h>
|
||||
|
||||
#include "Epub/Section.h"
|
||||
#include "KOReaderCredentialStore.h"
|
||||
#include "KOReaderDocumentId.h"
|
||||
#include "MappedInputManager.h"
|
||||
@@ -14,6 +15,16 @@
|
||||
#include "fontIds.h"
|
||||
|
||||
namespace {
|
||||
CrossPointPosition makeLocalPositionWithParagraph(const int spineIndex, const int page, const int totalPages,
|
||||
const std::optional<uint16_t>& paragraphIndex) {
|
||||
CrossPointPosition pos = {spineIndex, page, totalPages};
|
||||
if (paragraphIndex.has_value()) {
|
||||
pos.paragraphIndex = *paragraphIndex;
|
||||
pos.hasParagraphIndex = true;
|
||||
}
|
||||
return pos;
|
||||
}
|
||||
|
||||
void syncTimeWithNTP() {
|
||||
// Stop SNTP if already running (can't reconfigure while running)
|
||||
if (esp_sntp_enabled()) {
|
||||
@@ -135,8 +146,21 @@ void KOReaderSyncActivity::performSync() {
|
||||
KOReaderPosition koPos = {remoteProgress.progress, remoteProgress.percentage};
|
||||
remotePosition = ProgressMapper::toCrossPoint(epub, koPos, currentSpineIndex, totalPagesInSpine);
|
||||
|
||||
// If XPath carried a paragraph index, refine the page using the section cache's
|
||||
// per-page paragraph LUT instead of anchor matching.
|
||||
if (remotePosition.hasParagraphIndex) {
|
||||
Section tempSection(epub, remotePosition.spineIndex, renderer);
|
||||
const auto paragraphPage = tempSection.getPageForParagraphIndex(remotePosition.paragraphIndex);
|
||||
if (paragraphPage.has_value()) {
|
||||
LOG_DBG("KOSync", "Paragraph %u resolved to page %d (was %d)", remotePosition.paragraphIndex, *paragraphPage,
|
||||
remotePosition.pageNumber);
|
||||
remotePosition.pageNumber = *paragraphPage;
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate local progress in KOReader format (for display)
|
||||
CrossPointPosition localPos = {currentSpineIndex, currentPage, totalPagesInSpine};
|
||||
CrossPointPosition localPos =
|
||||
makeLocalPositionWithParagraph(currentSpineIndex, currentPage, totalPagesInSpine, currentParagraphIndex);
|
||||
localProgress = ProgressMapper::toKOReader(epub, localPos);
|
||||
|
||||
{
|
||||
@@ -162,7 +186,8 @@ void KOReaderSyncActivity::performUpload() {
|
||||
requestUpdateAndWait();
|
||||
|
||||
// Convert current position to KOReader format
|
||||
CrossPointPosition localPos = {currentSpineIndex, currentPage, totalPagesInSpine};
|
||||
CrossPointPosition localPos =
|
||||
makeLocalPositionWithParagraph(currentSpineIndex, currentPage, totalPagesInSpine, currentParagraphIndex);
|
||||
KOReaderPosition koPos = ProgressMapper::toKOReader(epub, localPos);
|
||||
|
||||
KOReaderProgress progress;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
|
||||
#include "KOReaderSyncClient.h"
|
||||
#include "ProgressMapper.h"
|
||||
@@ -22,13 +23,15 @@ class KOReaderSyncActivity final : public Activity {
|
||||
public:
|
||||
explicit KOReaderSyncActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
|
||||
const std::shared_ptr<Epub>& epub, const std::string& epubPath, int currentSpineIndex,
|
||||
int currentPage, int totalPagesInSpine)
|
||||
int currentPage, int totalPagesInSpine,
|
||||
std::optional<uint16_t> currentParagraphIndex = std::nullopt)
|
||||
: Activity("KOReaderSync", renderer, mappedInput),
|
||||
epub(epub),
|
||||
epubPath(epubPath),
|
||||
currentSpineIndex(currentSpineIndex),
|
||||
currentPage(currentPage),
|
||||
totalPagesInSpine(totalPagesInSpine),
|
||||
currentParagraphIndex(currentParagraphIndex),
|
||||
remoteProgress{},
|
||||
remotePosition{},
|
||||
localProgress{} {}
|
||||
@@ -58,6 +61,7 @@ class KOReaderSyncActivity final : public Activity {
|
||||
int currentSpineIndex;
|
||||
int currentPage;
|
||||
int totalPagesInSpine;
|
||||
std::optional<uint16_t> currentParagraphIndex;
|
||||
|
||||
State state = WIFI_SELECTION;
|
||||
std::string statusMessage;
|
||||
|
||||
Reference in New Issue
Block a user