Merge branch 'master' of https://github.com/jpirnay/crosspoint-reader into feat-expose-override
This commit is contained in:
@@ -44,6 +44,10 @@ struct PercentResult {
|
||||
int percent = 0;
|
||||
};
|
||||
|
||||
struct PrintedPageResult {
|
||||
std::string label;
|
||||
};
|
||||
|
||||
struct PageResult {
|
||||
uint32_t page = 0;
|
||||
};
|
||||
@@ -78,7 +82,7 @@ struct StarredPageResult {
|
||||
|
||||
using ResultVariant =
|
||||
std::variant<std::monostate, WifiResult, KeyboardResult, MenuResult, ChapterResult, PercentResult, PageResult,
|
||||
SyncResult, NetworkModeResult, FootnoteResult, FilePathResult, StarredPageResult>;
|
||||
SyncResult, NetworkModeResult, FootnoteResult, FilePathResult, StarredPageResult, PrintedPageResult>;
|
||||
|
||||
struct ActivityResult {
|
||||
bool isCancelled = false;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "SleepActivity.h"
|
||||
|
||||
#include <Epub.h>
|
||||
#include <Epub/Section.h>
|
||||
#include <Epub/converters/PngToFramebufferConverter.h>
|
||||
#include <FsHelpers.h>
|
||||
#include <GfxRenderer.h>
|
||||
@@ -553,6 +554,16 @@ BookOverlayInfo SleepActivity::getBookOverlayInfo(const std::string& bookPath) c
|
||||
float chapterProgress = static_cast<float>(currentPage) / static_cast<float>(pageCount);
|
||||
float bookProgress = epub.calculateProgress(currentSpineIndex, chapterProgress) * 100.0f;
|
||||
|
||||
// Pull the printed-page label (NCX <pageList> / EPUB 3 nav page-list /
|
||||
// EPUB 2.01 page-map / inline doc-pagebreak) directly from the section
|
||||
// cache so the sleep overlay can show e.g. "(42)" without instantiating
|
||||
// a Section + render parameters.
|
||||
std::string printedPagePrefix;
|
||||
if (const auto label = Section::getPrintedPageLabelFromCache(
|
||||
epub.getCachePath() + "/sections", currentSpineIndex, static_cast<uint16_t>(currentPage))) {
|
||||
printedPagePrefix = *label + " ";
|
||||
}
|
||||
|
||||
const int tocIndex = epub.getTocIndexForSpineIndex(currentSpineIndex);
|
||||
if (tocIndex != -1) {
|
||||
const auto tocItem = epub.getTocItem(tocIndex);
|
||||
@@ -560,13 +571,13 @@ BookOverlayInfo SleepActivity::getBookOverlayInfo(const std::string& bookPath) c
|
||||
char suffix[64];
|
||||
snprintf(suffix, sizeof(suffix), tr(STR_OVERLAY_CHAPTER_PAGE_SUFFIX), currentPage + 1, pageCount,
|
||||
bookProgress);
|
||||
info.progressSuffix = suffix;
|
||||
info.progressSuffix = printedPagePrefix + suffix;
|
||||
info.progressText = info.chapterName + info.progressSuffix;
|
||||
} else {
|
||||
char buf[80];
|
||||
snprintf(buf, sizeof(buf), tr(STR_OVERLAY_READING_PROGRESS), (unsigned long)currentPage + 1,
|
||||
(unsigned)pageCount, bookProgress);
|
||||
info.progressText = buf;
|
||||
info.progressText = printedPagePrefix + buf;
|
||||
}
|
||||
} else {
|
||||
char buf[64];
|
||||
|
||||
@@ -19,13 +19,16 @@
|
||||
#include <esp_system.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
|
||||
#include "CrossPointSettings.h"
|
||||
#include "CrossPointState.h"
|
||||
#include "EpubReaderChapterSelectionActivity.h"
|
||||
#include "EpubReaderFootnotesActivity.h"
|
||||
#include "EpubReaderPercentSelectionActivity.h"
|
||||
#include "EpubReaderPrintedPageInputActivity.h"
|
||||
#include "EpubRenderBenchmarkActivity.h"
|
||||
#include "FinishedBookActivity.h"
|
||||
#include "GlobalBookmarkIndex.h"
|
||||
@@ -48,6 +51,20 @@
|
||||
namespace {
|
||||
// pagesPerRefresh now comes from SETTINGS.getRefreshFrequency()
|
||||
constexpr unsigned long skipChapterMs = 700;
|
||||
|
||||
// Parse a printed-page label as a non-negative integer. Returns nullopt for empty strings,
|
||||
// strings with non-digit characters (e.g. roman "iv"), and overflow. Used both to gate the
|
||||
// "go to printed page" menu item and to compute min/max for the numeric input.
|
||||
std::optional<int> parsePrintedPageLabel(const std::string& label) {
|
||||
if (label.empty()) return std::nullopt;
|
||||
int value = 0;
|
||||
for (char c : label) {
|
||||
if (c < '0' || c > '9') return std::nullopt;
|
||||
value = value * 10 + (c - '0');
|
||||
if (value > 999999) return std::nullopt; // sanity
|
||||
}
|
||||
return value;
|
||||
}
|
||||
// pages per minute, first item is 1 to prevent division by zero if accessed
|
||||
constexpr int PAGE_TURN_LABELS[] = {1, 1, 3, 6, 12};
|
||||
|
||||
@@ -105,6 +122,30 @@ void logReaderMemSnapshot(const char* stage) {
|
||||
inline void logReaderMemSnapshot(const char*) {}
|
||||
#endif
|
||||
|
||||
// Integrity bisector. Logs at every probe site (unconditional, not gated) and
|
||||
// fires an ERR when integrity transitions from ok -> fail so we can pinpoint
|
||||
// which render phase corrupts the heap. Free/contig included so we can see if
|
||||
// the corruption coincides with a specific allocation pattern. Calling
|
||||
// heap_caps_check_integrity_all is ~O(blocks) — not free but fine at phase
|
||||
// boundaries during onEnter / first render.
|
||||
void logIntegrityProbe(const char* stage) {
|
||||
static bool sLastOk = true;
|
||||
const bool ok = heap_caps_check_integrity_all(true);
|
||||
const uint32_t freeHeap = esp_get_free_heap_size();
|
||||
const uint32_t contigHeap = heap_caps_get_largest_free_block(MALLOC_CAP_8BIT | MALLOC_CAP_DEFAULT);
|
||||
if (ok != sLastOk) {
|
||||
if (ok) {
|
||||
LOG_DBG("INTG", "[%s] integrity recovered (free=%lu contig=%lu)", stage, freeHeap, contigHeap);
|
||||
} else {
|
||||
LOG_ERR("INTG", "[%s] integrity FAIL — corruption introduced here (free=%lu contig=%lu)", stage, freeHeap,
|
||||
contigHeap);
|
||||
}
|
||||
sLastOk = ok;
|
||||
} else {
|
||||
LOG_DBG("INTG", "[%s] %s free=%lu contig=%lu", stage, ok ? "ok" : "fail", freeHeap, contigHeap);
|
||||
}
|
||||
}
|
||||
|
||||
// Tiled grayscale: render each plane band-by-band into a small scratch and
|
||||
// stream straight to the controller, leaving the BW framebuffer intact so no
|
||||
// storeBwBuffer / restoreBwBuffer is needed. Controller RAM is re-synced from
|
||||
@@ -157,15 +198,20 @@ bool runTiledGrayscalePass(GfxRenderer& renderer, const Page& page, int fontId,
|
||||
}
|
||||
};
|
||||
|
||||
logIntegrityProbe("tiledGray_after_scratchAlloc");
|
||||
renderPlane(GfxRenderer::GRAYSCALE_LSB, true);
|
||||
logIntegrityProbe("tiledGray_after_lsbPlane");
|
||||
renderPlane(GfxRenderer::GRAYSCALE_MSB, false);
|
||||
logIntegrityProbe("tiledGray_after_msbPlane");
|
||||
|
||||
renderer.setRenderMode(GfxRenderer::BW);
|
||||
renderer.displayGrayBuffer();
|
||||
logIntegrityProbe("tiledGray_after_displayGrayBuffer");
|
||||
|
||||
// BW framebuffer is intact; re-sync controller RAM for the next differential
|
||||
// page turn directly from it.
|
||||
renderer.cleanupGrayscaleWithFrameBuffer();
|
||||
logIntegrityProbe("tiledGray_after_cleanup");
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -254,6 +300,7 @@ int getImageOnlyPageYOffset(const Page& page, const int viewportHeight) {
|
||||
void EpubReaderActivity::onEnter() {
|
||||
Activity::onEnter();
|
||||
logReaderMemSnapshot("onEnter_begin");
|
||||
logIntegrityProbe("onEnter_begin");
|
||||
|
||||
// Drop any input events that arrived from the activity that launched us (e.g. a wake-up power
|
||||
// button hold) before they reach detectPageTurn() — see ReaderUtils::InputDrainGuard.
|
||||
@@ -273,10 +320,13 @@ void EpubReaderActivity::onEnter() {
|
||||
|
||||
epub->setupCacheDir();
|
||||
logReaderMemSnapshot("onEnter_after_setupCacheDir");
|
||||
applyPendingSyncSession();
|
||||
applyPendingBookmarkJump();
|
||||
logReaderMemSnapshot("onEnter_after_pending_sync");
|
||||
|
||||
// Load the persistent baseline (progress.bin) first. Pending session state
|
||||
// (sync result, bookmark jump) is then overlaid on top — this is the only order
|
||||
// that lets a Kind::Paragraph / Kind::ListItem navTarget set by applyPendingSyncSession
|
||||
// survive into render(). The previous order (apply then load) clobbered the LUT
|
||||
// target with Kind::Page from progress.bin, which is why XPath-precision sync
|
||||
// silently degraded to the rough page estimate.
|
||||
FsFile f;
|
||||
if (Storage.openFileForRead("ERS", epub->getCachePath() + "/progress.bin", f)) {
|
||||
uint8_t data[6];
|
||||
@@ -301,6 +351,10 @@ void EpubReaderActivity::onEnter() {
|
||||
navTarget = NavigationTarget::makePage(0);
|
||||
}
|
||||
|
||||
applyPendingSyncSession();
|
||||
applyPendingBookmarkJump();
|
||||
logReaderMemSnapshot("onEnter_after_pending_sync");
|
||||
|
||||
if (currentSpineIndex == 0) {
|
||||
int textSpineIndex = epub->getSpineIndexForTextReference();
|
||||
if (textSpineIndex != 0) {
|
||||
@@ -684,6 +738,67 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction
|
||||
});
|
||||
break;
|
||||
}
|
||||
case EpubReaderMenuActivity::MenuAction::GO_TO_PRINTED_PAGE: {
|
||||
if (!epub) break;
|
||||
auto entries = epub->loadPrintedPageList();
|
||||
// Compute the integer label range from parseable entries; non-integer labels are
|
||||
// ignored (the dialog is numeric-only).
|
||||
int minLabel = std::numeric_limits<int>::max();
|
||||
int maxLabel = std::numeric_limits<int>::min();
|
||||
for (const auto& entry : entries) {
|
||||
if (const auto n = parsePrintedPageLabel(entry.label)) {
|
||||
if (*n < minLabel) minLabel = *n;
|
||||
if (*n > maxLabel) maxLabel = *n;
|
||||
}
|
||||
}
|
||||
if (maxLabel < minLabel) break; // no integer labels — shouldn't happen if menu item was shown
|
||||
|
||||
// Pre-fill with the printed page the reader is currently on (or the nearest one before
|
||||
// it — rendered device pages rarely carry an anchor themselves, but they sit between
|
||||
// two printed pages, so the closest prior anchor is the "you're here" hint). Falls
|
||||
// back to the lowest integer label in the book if no prior anchor exists.
|
||||
int initialValue = minLabel;
|
||||
if (section) {
|
||||
if (const auto rawLabel =
|
||||
section->getNearestPrintedPageLabelAtOrBefore(static_cast<uint16_t>(section->currentPage))) {
|
||||
if (const auto n = parsePrintedPageLabel(*rawLabel)) {
|
||||
initialValue = *n;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
startActivityForResult(
|
||||
std::make_unique<EpubReaderPrintedPageInputActivity>(renderer, mappedInput, initialValue, minLabel, maxLabel),
|
||||
[this, entries = std::move(entries)](const ActivityResult& result) {
|
||||
if (result.isCancelled) return;
|
||||
const auto& pick = std::get<PrintedPageResult>(result.data);
|
||||
// Resolve the typed label back to a (href, anchor) by linear scan. Entries are
|
||||
// small (typically <500 even for long books) and this fires once per user action.
|
||||
for (const auto& entry : entries) {
|
||||
const auto entryLabelValue = parsePrintedPageLabel(entry.label);
|
||||
const auto pickLabelValue = parsePrintedPageLabel(pick.label);
|
||||
if (entry.label == pick.label ||
|
||||
(entryLabelValue && pickLabelValue && *entryLabelValue == *pickLabelValue)) {
|
||||
const int spineIdx = epub->resolveHrefToSpineIndex(entry.href);
|
||||
if (spineIdx < 0) {
|
||||
LOG_DBG("ERS", "printed-page jump: could not resolve spine for href=%s", entry.href.c_str());
|
||||
return;
|
||||
}
|
||||
{
|
||||
RenderLock lock(*this);
|
||||
currentSpineIndex = spineIdx;
|
||||
navTarget =
|
||||
entry.anchor.empty() ? NavigationTarget::makePage(0) : NavigationTarget::makeAnchor(entry.anchor);
|
||||
section.reset();
|
||||
}
|
||||
requestUpdate();
|
||||
return;
|
||||
}
|
||||
}
|
||||
LOG_DBG("ERS", "printed-page jump: label '%s' not found in pagelist", pick.label.c_str());
|
||||
});
|
||||
break;
|
||||
}
|
||||
case EpubReaderMenuActivity::MenuAction::DISPLAY_QR: {
|
||||
if (section && section->currentPage >= 0 && section->currentPage < section->pageCount) {
|
||||
auto p = section->loadPageFromSectionFile();
|
||||
@@ -1186,7 +1301,9 @@ void EpubReaderActivity::applyPendingSyncSession() {
|
||||
restorePage = 0;
|
||||
}
|
||||
|
||||
// Build the navigation target from the sync result.
|
||||
// Build the navigation target from the sync result. For LUT-anchored targets the
|
||||
// estimated restorePage is plumbed through as fallbackPage so a LUT miss in the
|
||||
// target spine still lands the user on a sensible page rather than page 0.
|
||||
NavigationTarget restoreTarget;
|
||||
if (sync.outcome == KOReaderSyncOutcomeState::APPLIED_REMOTE) {
|
||||
const int spineCount = epub->getSpineItemsCount();
|
||||
@@ -1199,11 +1316,11 @@ void EpubReaderActivity::applyPendingSyncSession() {
|
||||
restorePage = sync.resultPage;
|
||||
}
|
||||
if (sync.resultHasListItemIndex) {
|
||||
restoreTarget = NavigationTarget::makeListItem(sync.resultListItemIndex);
|
||||
restoreTarget = NavigationTarget::makeListItem(sync.resultListItemIndex, restorePage);
|
||||
LOG_DBG("ERS", "Applied synced remote position: spine=%d page=%d li[%u]", restoreSpineIndex, restorePage,
|
||||
sync.resultListItemIndex);
|
||||
} else if (sync.resultHasParagraphIndex) {
|
||||
restoreTarget = NavigationTarget::makeParagraph(sync.resultParagraphIndex);
|
||||
restoreTarget = NavigationTarget::makeParagraph(sync.resultParagraphIndex, restorePage);
|
||||
LOG_DBG("ERS", "Applied synced remote position: spine=%d page=%d p[%u]", restoreSpineIndex, restorePage,
|
||||
sync.resultParagraphIndex);
|
||||
} else {
|
||||
@@ -1217,21 +1334,26 @@ void EpubReaderActivity::applyPendingSyncSession() {
|
||||
|
||||
// sync.totalPagesInSpine is the page count of the local spine at launch time.
|
||||
// When the restore targets a different spine, that count is meaningless for
|
||||
// rescaling. Store 0 to disable rescaling; the LUT lookup handles precise positioning.
|
||||
// rescaling the fallbackPage estimate (which was estimated from cross-spine
|
||||
// density anyway). Store 0 to disable rescaling — the LUT lookup is the precise
|
||||
// path, and the cross-spine fallback can't usefully be rescaled here.
|
||||
const int restorePageCount = (restoreSpineIndex == sync.spineIndex) ? sync.totalPagesInSpine : 0;
|
||||
restoreTarget.cachedPageCount = restorePageCount;
|
||||
restoreTarget.cachedSpineIdx = restoreSpineIndex;
|
||||
|
||||
// Transient write — the next render's saveProgress() supplies the real percent before the user
|
||||
// can return to the home screen, so a placeholder 0 here is harmless.
|
||||
if (writeReaderProgressCache(epub->getCachePath(), restoreSpineIndex, restorePage, restorePageCount, 0)) {
|
||||
navTarget = restoreTarget;
|
||||
// Seed live state directly — the previous write-then-reload-from-disk pattern relied
|
||||
// on progress.bin being read after this function ran, which clobbered the LUT target.
|
||||
// Live-state seeding is authoritative; the persistent write below is just for crash
|
||||
// recovery so a power loss before the next saveProgress() doesn't lose the synced
|
||||
// spine/page. The next render's saveProgress() supplies the real percent before
|
||||
// the user can return to the home screen.
|
||||
currentSpineIndex = restoreSpineIndex;
|
||||
navTarget = restoreTarget;
|
||||
if (!writeReaderProgressCache(epub->getCachePath(), restoreSpineIndex, restorePage, restorePageCount, 0)) {
|
||||
LOG_ERR("ERS", "Failed to persist sync restore to progress.bin; live state still seeded");
|
||||
} else {
|
||||
LOG_DBG("ERS", "Prepared progress.bin for sync restore: spine=%d page=%d/%d", restoreSpineIndex, restorePage,
|
||||
sync.totalPagesInSpine);
|
||||
} else {
|
||||
// Fall back to directly seeding live state if cache write fails.
|
||||
currentSpineIndex = restoreSpineIndex;
|
||||
navTarget = restoreTarget;
|
||||
}
|
||||
|
||||
sync.clear();
|
||||
@@ -1250,14 +1372,13 @@ void EpubReaderActivity::applyPendingBookmarkJump() {
|
||||
jump.spineIndex = 0;
|
||||
jump.pageNumber = 0;
|
||||
}
|
||||
// Transient write before initializeReader; saveProgress() overwrites with the real percent.
|
||||
if (writeReaderProgressCache(epub->getCachePath(), jump.spineIndex, jump.pageNumber, 0, 0)) {
|
||||
navTarget = NavigationTarget::makePage(jump.pageNumber);
|
||||
navTarget.cachedSpineIdx = jump.spineIndex;
|
||||
} else {
|
||||
currentSpineIndex = jump.spineIndex;
|
||||
navTarget = NavigationTarget::makePage(jump.pageNumber);
|
||||
navTarget.cachedSpineIdx = jump.spineIndex;
|
||||
// Seed live state directly; the persistent write is for crash recovery only.
|
||||
// saveProgress() on the next render overwrites with the real percent.
|
||||
currentSpineIndex = jump.spineIndex;
|
||||
navTarget = NavigationTarget::makePage(jump.pageNumber);
|
||||
navTarget.cachedSpineIdx = jump.spineIndex;
|
||||
if (!writeReaderProgressCache(epub->getCachePath(), jump.spineIndex, jump.pageNumber, 0, 0)) {
|
||||
LOG_ERR("ERS", "Failed to persist bookmark jump to progress.bin; live state still seeded");
|
||||
}
|
||||
jump.clear();
|
||||
APP_STATE.saveToFile();
|
||||
@@ -1502,58 +1623,95 @@ int EpubReaderActivity::getEffectiveReaderFontId() const {
|
||||
}
|
||||
|
||||
void EpubReaderActivity::NavigationTarget::resolveInto(Section& sec, int spineIndex) const {
|
||||
if (kind == Kind::LastPage) {
|
||||
sec.currentPage = (sec.pageCount > 0) ? sec.pageCount - 1 : 0;
|
||||
return;
|
||||
}
|
||||
if (kind == Kind::TocIndex) {
|
||||
if (const auto p = sec.getPageForTocIndex(tocIndex)) sec.currentPage = *p;
|
||||
return;
|
||||
}
|
||||
if (kind == Kind::Anchor) {
|
||||
if (const auto p = sec.getPageForAnchor(anchorStr)) {
|
||||
sec.currentPage = *p;
|
||||
LOG_DBG("ERS", "Resolved anchor '%s' -> page %d", anchorStr.c_str(), *p);
|
||||
} else {
|
||||
LOG_DBG("ERS", "Anchor '%s' not found in section", anchorStr.c_str());
|
||||
// Resolve to a baseline page first. Each branch records whether it produced a
|
||||
// precise page (LUT/anchor hit, percent jump, explicit page) or only an estimate.
|
||||
// The estimate path runs cross-spine rescale + clamp at the end; the precise path
|
||||
// skips both because LUT pages are already in the target spine's coordinate system.
|
||||
bool isEstimate = false;
|
||||
|
||||
switch (kind) {
|
||||
case Kind::LastPage: {
|
||||
sec.currentPage = (sec.pageCount > 0) ? sec.pageCount - 1 : 0;
|
||||
break;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (kind == Kind::ListItem) {
|
||||
if (const auto p = sec.getPageForListItemIndex(lutIndex)) {
|
||||
sec.currentPage = *p;
|
||||
LOG_DBG("ERS", "Resolved li[%u] -> page %d", lutIndex, *p);
|
||||
} else {
|
||||
LOG_DBG("ERS", "Li index %u not found in section LUT", lutIndex);
|
||||
|
||||
case Kind::TocIndex: {
|
||||
if (const auto p = sec.getPageForTocIndex(tocIndex)) {
|
||||
sec.currentPage = *p;
|
||||
}
|
||||
break;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (kind == Kind::Paragraph) {
|
||||
if (const auto p = sec.getPageForParagraphIndex(lutIndex)) {
|
||||
sec.currentPage = *p;
|
||||
LOG_DBG("ERS", "Resolved p[%u] -> page %d", lutIndex, *p);
|
||||
} else {
|
||||
LOG_DBG("ERS", "Paragraph LUT miss, using page %d", sec.currentPage);
|
||||
|
||||
case Kind::Anchor: {
|
||||
if (const auto p = sec.getPageForAnchor(anchorStr)) {
|
||||
sec.currentPage = *p;
|
||||
LOG_DBG("ERS", "Resolved anchor '%s' -> page %d", anchorStr.c_str(), *p);
|
||||
} else {
|
||||
LOG_DBG("ERS", "Anchor '%s' not found; using fallback page %d", anchorStr.c_str(), fallbackPage);
|
||||
sec.currentPage = fallbackPage;
|
||||
isEstimate = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (kind == Kind::Percent) {
|
||||
if (sec.pageCount > 0) {
|
||||
int newPage = static_cast<int>(spineProgress * static_cast<float>(sec.pageCount));
|
||||
if (newPage >= sec.pageCount) newPage = sec.pageCount - 1;
|
||||
sec.currentPage = newPage;
|
||||
|
||||
case Kind::ListItem: {
|
||||
if (const auto p = sec.getPageForListItemIndex(lutIndex)) {
|
||||
sec.currentPage = *p;
|
||||
LOG_DBG("ERS", "Resolved li[%u] -> page %d", lutIndex, *p);
|
||||
} else if (const auto pp = sec.getPageForParagraphIndex(lutIndex)) {
|
||||
// Some <li>-anchored XPaths land in books where the LI LUT is empty (no <li>
|
||||
// inside <body>'s direct children, or all <li>s skipped). Fall back to the
|
||||
// paragraph LUT — the running indices coincide often enough to help, and
|
||||
// it's strictly better than dropping back to the estimate.
|
||||
sec.currentPage = *pp;
|
||||
LOG_DBG("ERS", "Li LUT miss for li[%u]; paragraph LUT -> page %d", lutIndex, *pp);
|
||||
} else {
|
||||
LOG_DBG("ERS", "Li[%u] not in LUT; using fallback page %d", lutIndex, fallbackPage);
|
||||
sec.currentPage = fallbackPage;
|
||||
isEstimate = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Kind::Page — apply baseline, then cross-font rescale if we have a cached page count.
|
||||
sec.currentPage = page;
|
||||
if (cachedPageCount > 0 && cachedSpineIdx == spineIndex) {
|
||||
if (sec.pageCount != cachedPageCount) {
|
||||
const float progress = static_cast<float>(sec.currentPage) / static_cast<float>(cachedPageCount);
|
||||
sec.currentPage = static_cast<int>(progress * static_cast<float>(sec.pageCount));
|
||||
|
||||
case Kind::Paragraph: {
|
||||
if (const auto p = sec.getPageForParagraphIndex(lutIndex)) {
|
||||
sec.currentPage = *p;
|
||||
LOG_DBG("ERS", "Resolved p[%u] -> page %d", lutIndex, *p);
|
||||
} else {
|
||||
LOG_DBG("ERS", "Paragraph LUT miss for p[%u]; using fallback page %d", lutIndex, fallbackPage);
|
||||
sec.currentPage = fallbackPage;
|
||||
isEstimate = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case Kind::Percent: {
|
||||
if (sec.pageCount > 0) {
|
||||
int newPage = static_cast<int>(spineProgress * static_cast<float>(sec.pageCount));
|
||||
if (newPage >= sec.pageCount) newPage = sec.pageCount - 1;
|
||||
sec.currentPage = newPage;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case Kind::Page: {
|
||||
sec.currentPage = page;
|
||||
isEstimate = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Safety clamp.
|
||||
|
||||
// Cross-font / cross-spine rescaling: only for estimated pages. cachedPageCount
|
||||
// is the page count at the time the estimate was made — when it disagrees with
|
||||
// the section's current page count (reflow / different spine entirely), rescale
|
||||
// the estimate proportionally before clamping.
|
||||
if (isEstimate && cachedPageCount > 0 && cachedSpineIdx == spineIndex && sec.pageCount != cachedPageCount) {
|
||||
const float progress = static_cast<float>(sec.currentPage) / static_cast<float>(cachedPageCount);
|
||||
sec.currentPage = static_cast<int>(progress * static_cast<float>(sec.pageCount));
|
||||
}
|
||||
|
||||
// Safety clamp for all paths — a LUT-derived page is also defensively clamped in
|
||||
// case the cache is somehow stale.
|
||||
if (sec.currentPage < 0) {
|
||||
LOG_DBG("ERS", "Clamping negative page %d to 0 (spine=%d cachedPageCount=%d)", sec.currentPage, spineIndex,
|
||||
cachedPageCount);
|
||||
@@ -1635,6 +1793,7 @@ void EpubReaderActivity::render(RenderLock&& lock) {
|
||||
if (!epub) {
|
||||
return;
|
||||
}
|
||||
logIntegrityProbe("render_entry");
|
||||
|
||||
const int spineCount = epub->getSpineItemsCount();
|
||||
if (spineCount <= 0) {
|
||||
@@ -1779,11 +1938,13 @@ void EpubReaderActivity::render(RenderLock&& lock) {
|
||||
auto p = section->loadPageFromSectionFile();
|
||||
section->currentPage = savedPage;
|
||||
if (p && !p->hasImages()) {
|
||||
logIntegrityProbe("preRender_before_renderPageContentOnly");
|
||||
section->currentPage = nextPage;
|
||||
renderPageContentOnly(*p, orientedMarginTop, orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
|
||||
section->currentPage = savedPage;
|
||||
preRenderedPage = {true, currentSpineIndex, nextPage};
|
||||
LOG_DBG("ERS", "Pre-rendered page %d/%d", nextPage, section->pageCount - 1);
|
||||
logIntegrityProbe("preRender_after_renderPageContentOnly");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1866,6 +2027,7 @@ void EpubReaderActivity::render(RenderLock&& lock) {
|
||||
LOG_DBG("ERS", "Cache found, skipping build...");
|
||||
}
|
||||
lastRenderStats.sectionLoadMs = millis() - sectionStart;
|
||||
logIntegrityProbe("render_after_sectionLoad");
|
||||
|
||||
if (section->isTruncatedCache() && currentSpineIndex != lastWarnedTruncatedSpineIndex) {
|
||||
lastWarnedTruncatedSpineIndex = currentSpineIndex;
|
||||
@@ -1903,6 +2065,7 @@ void EpubReaderActivity::render(RenderLock&& lock) {
|
||||
const unsigned long pageLoadStart = millis();
|
||||
auto p = section->loadPageFromSectionFile();
|
||||
lastRenderStats.pageLoadMs = millis() - pageLoadStart;
|
||||
logIntegrityProbe("render_after_pageLoad");
|
||||
if (!p) {
|
||||
LOG_ERR("ERS", "Failed to load page from SD - clearing section cache");
|
||||
section->clearCache();
|
||||
@@ -1929,8 +2092,10 @@ void EpubReaderActivity::render(RenderLock&& lock) {
|
||||
truncatedSectionHintRendersRemaining--;
|
||||
}
|
||||
LOG_DBG("ERS", "Rendered page in %dms", lastRenderStats.requestRenderMs);
|
||||
logIntegrityProbe("render_after_renderContents");
|
||||
}
|
||||
silentIndexNextChapterIfNeeded(viewportWidth, viewportHeight);
|
||||
logIntegrityProbe("render_after_silentIndex");
|
||||
pendingProgressSave.spineIndex = currentSpineIndex;
|
||||
pendingProgressSave.page = section->currentPage;
|
||||
pendingProgressSave.pageCount = section->pageCount;
|
||||
@@ -2010,6 +2175,7 @@ void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int or
|
||||
const int orientedMarginLeft) {
|
||||
const auto t0 = millis();
|
||||
logReaderMemSnapshot("render_start");
|
||||
logIntegrityProbe("renderContents_entry");
|
||||
auto* fcm = renderer.getFontCacheManager();
|
||||
fcm->resetStats();
|
||||
|
||||
@@ -2025,6 +2191,7 @@ void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int or
|
||||
const bool warmForceLoad = forceLoadLargeImages || !SETTINGS.largeImagePlaceholder;
|
||||
page->warmImageCaches(renderer, orientedMarginLeft, contentTop, warmForceLoad);
|
||||
renderer.clearScreen();
|
||||
logIntegrityProbe("renderContents_after_warmImages");
|
||||
|
||||
logReaderMemSnapshot("prewarm_begin");
|
||||
|
||||
@@ -2044,6 +2211,7 @@ void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int or
|
||||
LOG_DBG("ERS", "Heap: before=%lu (contig=%lu) after=%lu (contig=%lu) delta=%ld", heapBefore, contigBefore, heapAfter,
|
||||
contigAfter, (int32_t)heapAfter - (int32_t)heapBefore);
|
||||
logReaderMemSnapshot("prewarm_end");
|
||||
logIntegrityProbe("renderContents_after_fontPrewarm");
|
||||
|
||||
const bool aaConfigured = getEffectiveTextAntiAliasing();
|
||||
bool aaEnabledForThisRender = aaConfigured;
|
||||
@@ -2095,6 +2263,7 @@ void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int or
|
||||
fcm->logStats("bw_render");
|
||||
const auto tBwRender = millis();
|
||||
logReaderMemSnapshot("after_bw_render");
|
||||
logIntegrityProbe("renderContents_after_bwRender");
|
||||
|
||||
if (imagePageWithAA) {
|
||||
// Double FAST_REFRESH with selective image blanking (pablohc's technique):
|
||||
@@ -2142,6 +2311,7 @@ void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int or
|
||||
uint32_t tiledGrayMs = 0;
|
||||
if (aaEnabledForThisRender) {
|
||||
logReaderMemSnapshot("tiled_gray_begin");
|
||||
logIntegrityProbe("renderContents_before_tiledGray");
|
||||
const auto tTiledBegin = millis();
|
||||
grayscaleDone = runTiledGrayscalePass(renderer, *page, getEffectiveReaderFontId(), orientedMarginLeft, contentTop,
|
||||
SETTINGS.fastAntiAliasing);
|
||||
@@ -2149,6 +2319,7 @@ void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int or
|
||||
tiledGrayMs = millis() - tTiledBegin;
|
||||
fcm->logStats("tiled_gray");
|
||||
logReaderMemSnapshot("tiled_gray_end");
|
||||
logIntegrityProbe("renderContents_after_tiledGray");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2405,7 +2576,13 @@ void EpubReaderActivity::renderStatusBar() const {
|
||||
|
||||
const bool isStarred = section && bookmarkStore.has(static_cast<uint16_t>(currentSpineIndex),
|
||||
static_cast<uint16_t>(section->currentPage));
|
||||
GUI.drawStatusBar(renderer, bookProgress, currentPage, pageCount, title, 0, isStarred);
|
||||
std::string printedPageLabel;
|
||||
if (section) {
|
||||
if (const auto label = section->getPrintedPageLabelForPage(static_cast<uint16_t>(section->currentPage))) {
|
||||
printedPageLabel = *label;
|
||||
}
|
||||
}
|
||||
GUI.drawStatusBar(renderer, bookProgress, currentPage, pageCount, title, 0, isStarred, printedPageLabel);
|
||||
|
||||
lastStatusBarPage = currentPage;
|
||||
lastStatusBarBattery = SETTINGS.statusBarBattery ? static_cast<int>(powerManager.getBatteryPercentage()) : -1;
|
||||
@@ -2629,6 +2806,11 @@ void EpubReaderActivity::openQuickOverrides() {
|
||||
void EpubReaderActivity::openReaderMenu() {
|
||||
const int currentPage = section ? section->currentPage + 1 : 0;
|
||||
const int totalPages = section ? section->pageCount : 0;
|
||||
|
||||
if (!epub) {
|
||||
return;
|
||||
}
|
||||
|
||||
float bookProgress = 0.0f;
|
||||
if (epub->getBookSize() > 0 && section && section->pageCount > 0) {
|
||||
const float chapterProgress = static_cast<float>(section->currentPage) / static_cast<float>(section->pageCount);
|
||||
@@ -2637,13 +2819,22 @@ void EpubReaderActivity::openReaderMenu() {
|
||||
const int bookProgressPercent = clampPercent(static_cast<int>(bookProgress + 0.5f));
|
||||
const bool isCurrentPageStarred = section && bookmarkStore.has(static_cast<uint16_t>(currentSpineIndex),
|
||||
static_cast<uint16_t>(section->currentPage));
|
||||
|
||||
// Show the "Go to printed page" item only when this book has at least one integer-labelled
|
||||
// entry in pagelist.bin. Roman-only or empty page lists are excluded — the numeric input
|
||||
// dialog can't address them anyway.
|
||||
const auto printedPageList = epub->loadPrintedPageList();
|
||||
const bool hasPrintedPages = std::any_of(printedPageList.begin(), printedPageList.end(), [](const auto& entry) {
|
||||
return parsePrintedPageLabel(entry.label).has_value();
|
||||
});
|
||||
|
||||
ReaderUtils::enforceExitFullRefresh(renderer);
|
||||
startActivityForResult(
|
||||
std::make_unique<EpubReaderMenuActivity>(
|
||||
renderer, mappedInput, epub->getTitle(), currentPage, totalPages, bookProgressPercent, SETTINGS.orientation,
|
||||
!currentPageFootnotes.empty(), bookEmbeddedStyleOverride, bookImageRenderingOverride, bookFontFamilyOverride,
|
||||
bookSdFontFamilyOverride, bookFontSizeOverride, SETTINGS.textDarkness, getEffectiveBionicReading(),
|
||||
bookParagraphAlignmentOverride, !bookmarkStore.isEmpty(), isCurrentPageStarred),
|
||||
bookParagraphAlignmentOverride, !bookmarkStore.isEmpty(), isCurrentPageStarred, hasPrintedPages),
|
||||
[this](const ActivityResult& result) {
|
||||
const auto& menu = std::get<MenuResult>(result.data);
|
||||
applyOrientation(menu.orientation);
|
||||
|
||||
@@ -46,9 +46,15 @@ class EpubReaderActivity final : public Activity {
|
||||
};
|
||||
std::string anchorStr; // Kind::Anchor; empty for all others
|
||||
// Cross-font rescaling: page count of this spine at save time.
|
||||
// Non-zero only for Kind::Page when loaded from progress.bin or written during reflow.
|
||||
// Non-zero for Kind::Page when loaded from progress.bin or written during reflow.
|
||||
// Also set for Kind::Paragraph / Kind::ListItem / Kind::Anchor so a LUT miss
|
||||
// still rescales the estimated fallbackPage instead of stranding at 0.
|
||||
int cachedPageCount = 0;
|
||||
int cachedSpineIdx = 0;
|
||||
// Estimated page used as a baseline before LUT/anchor lookup, and as a fallback
|
||||
// when the lookup misses. Only meaningful for Kind::Paragraph / Kind::ListItem /
|
||||
// Kind::Anchor — for Kind::Page the `page` field is the baseline.
|
||||
int fallbackPage = 0;
|
||||
|
||||
NavigationTarget() : kind(Kind::Page), page(0) {}
|
||||
|
||||
@@ -64,11 +70,12 @@ class EpubReaderActivity final : public Activity {
|
||||
t.page = 0;
|
||||
return t;
|
||||
}
|
||||
static NavigationTarget makeAnchor(std::string a) {
|
||||
static NavigationTarget makeAnchor(std::string a, int fallback = 0) {
|
||||
NavigationTarget t;
|
||||
t.kind = Kind::Anchor;
|
||||
t.page = 0;
|
||||
t.anchorStr = std::move(a);
|
||||
t.fallbackPage = fallback;
|
||||
return t;
|
||||
}
|
||||
static NavigationTarget makeTocIndex(int idx) {
|
||||
@@ -83,16 +90,18 @@ class EpubReaderActivity final : public Activity {
|
||||
t.spineProgress = sp;
|
||||
return t;
|
||||
}
|
||||
static NavigationTarget makeParagraph(uint16_t i) {
|
||||
static NavigationTarget makeParagraph(uint16_t i, int fallback = 0) {
|
||||
NavigationTarget t;
|
||||
t.kind = Kind::Paragraph;
|
||||
t.lutIndex = i;
|
||||
t.fallbackPage = fallback;
|
||||
return t;
|
||||
}
|
||||
static NavigationTarget makeListItem(uint16_t i) {
|
||||
static NavigationTarget makeListItem(uint16_t i, int fallback = 0) {
|
||||
NavigationTarget t;
|
||||
t.kind = Kind::ListItem;
|
||||
t.lutIndex = i;
|
||||
t.fallbackPage = fallback;
|
||||
return t;
|
||||
}
|
||||
|
||||
|
||||
@@ -40,7 +40,8 @@ EpubReaderMenuActivity::EpubReaderMenuActivity(
|
||||
const int8_t initialEmbeddedStyleOverride, const int8_t initialImageRenderingOverride,
|
||||
const int8_t initialFontFamilyOverride, const std::string& initialSdFontFamilyOverride,
|
||||
const int8_t initialFontSizeOverride, const uint8_t initialTextDarkness, const bool initialBionicReadingOverride,
|
||||
const int8_t initialParagraphAlignmentOverride, const bool hasStarredPages, const bool isCurrentPageStarred)
|
||||
const int8_t initialParagraphAlignmentOverride, const bool hasStarredPages, const bool isCurrentPageStarred,
|
||||
const bool hasPrintedPages)
|
||||
: MenuListActivity("EpubReaderMenu", renderer, mappedInput),
|
||||
currentPageStarred(isCurrentPageStarred),
|
||||
pendingOrientation(currentOrientation),
|
||||
@@ -56,16 +57,19 @@ EpubReaderMenuActivity::EpubReaderMenuActivity(
|
||||
currentPage(currentPage),
|
||||
totalPages(totalPages),
|
||||
bookProgressPercent(bookProgressPercent) {
|
||||
buildMenuItems(hasFootnotes, hasStarredPages);
|
||||
buildMenuItems(hasFootnotes, hasStarredPages, hasPrintedPages);
|
||||
}
|
||||
|
||||
void EpubReaderMenuActivity::buildMenuItems(bool hasFootnotes, bool hasStarredPages) {
|
||||
void EpubReaderMenuActivity::buildMenuItems(bool hasFootnotes, bool hasStarredPages, bool hasPrintedPages) {
|
||||
menuItems.reserve(20);
|
||||
|
||||
// --- Navigation ---
|
||||
menuItems.push_back(SettingInfo::Separator(StrId::STR_READER_NAVIGATION));
|
||||
menuItems.push_back(SettingInfo::Action(StrId::STR_SELECT_CHAPTER, SettingAction::None));
|
||||
menuItems.push_back(SettingInfo::Action(StrId::STR_GO_TO_PERCENT, SettingAction::None));
|
||||
if (hasPrintedPages) {
|
||||
menuItems.push_back(SettingInfo::Action(StrId::STR_GO_TO_PRINTED_PAGE, SettingAction::None));
|
||||
}
|
||||
|
||||
// Bookmarks, footnotes
|
||||
menuItems.push_back(SettingInfo::Separator(StrId::STR_READER_BOOKMARKS));
|
||||
@@ -277,6 +281,8 @@ EpubReaderMenuActivity::MenuAction EpubReaderMenuActivity::actionForNameId(StrId
|
||||
return MenuAction::SELECT_CHAPTER;
|
||||
case StrId::STR_GO_TO_PERCENT:
|
||||
return MenuAction::GO_TO_PERCENT;
|
||||
case StrId::STR_GO_TO_PRINTED_PAGE:
|
||||
return MenuAction::GO_TO_PRINTED_PAGE;
|
||||
case StrId::STR_STARRED_PAGES:
|
||||
return MenuAction::STARRED_PAGES;
|
||||
case StrId::STR_STAR_PAGE:
|
||||
|
||||
@@ -19,6 +19,7 @@ class EpubReaderMenuActivity final : public MenuListActivity {
|
||||
IMAGE_RENDERING,
|
||||
TEXT_DARKNESS,
|
||||
GO_TO_PERCENT,
|
||||
GO_TO_PRINTED_PAGE,
|
||||
AUTO_PAGE_TURN,
|
||||
ROTATE_SCREEN,
|
||||
SCREENSHOT,
|
||||
@@ -42,13 +43,13 @@ class EpubReaderMenuActivity final : public MenuListActivity {
|
||||
const std::string& initialSdFontFamilyOverride, const int8_t initialFontSizeOverride,
|
||||
const uint8_t initialTextDarkness, const bool initialBionicReadingOverride,
|
||||
const int8_t initialParagraphAlignmentOverride, const bool hasStarredPages,
|
||||
const bool isCurrentPageStarred);
|
||||
const bool isCurrentPageStarred, const bool hasPrintedPages);
|
||||
|
||||
void onEnter() override;
|
||||
void render(RenderLock&&) override;
|
||||
|
||||
private:
|
||||
void buildMenuItems(bool hasFootnotes, bool hasStarredPages);
|
||||
void buildMenuItems(bool hasFootnotes, bool hasStarredPages, bool hasPrintedPages);
|
||||
|
||||
bool currentPageStarred = false;
|
||||
void finishWithAction(MenuAction action);
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
#include "EpubReaderPrintedPageInputActivity.h"
|
||||
|
||||
#include <GfxRenderer.h>
|
||||
#include <I18n.h>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "ButtonEventManager.h"
|
||||
#include "MappedInputManager.h"
|
||||
#include "components/UITheme.h"
|
||||
#include "fontIds.h"
|
||||
|
||||
int EpubReaderPrintedPageInputActivity::powTen(int exponent) {
|
||||
int result = 1;
|
||||
for (int i = 0; i < exponent; i++) result *= 10;
|
||||
return result;
|
||||
}
|
||||
|
||||
int EpubReaderPrintedPageInputActivity::digitCount() const {
|
||||
int n = (value > 0) ? value : 1;
|
||||
int count = 0;
|
||||
while (n > 0) {
|
||||
count++;
|
||||
n /= 10;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
int EpubReaderPrintedPageInputActivity::maxCursorDigit() const {
|
||||
// Bound the reachable cursor position by maxValue's digit count, not the current value's.
|
||||
// This lets the user step into "empty" higher digits (e.g. cursor sits over the tens
|
||||
// place while value is still 1, and pressing Up turns it into 11). Without this you
|
||||
// could never grow a 1 into a 3-digit number without first single-pressing into double
|
||||
// digits, which defeats the point of having a digit cursor.
|
||||
int n = (maxValue > 0) ? maxValue : 1;
|
||||
int count = 0;
|
||||
while (n > 0) {
|
||||
count++;
|
||||
n /= 10;
|
||||
}
|
||||
return count - 1; // 0-based: ones=0, tens=1, hundreds=2, ...
|
||||
}
|
||||
|
||||
void EpubReaderPrintedPageInputActivity::clampValue() {
|
||||
if (value < minValue) value = minValue;
|
||||
if (value > maxValue) value = maxValue;
|
||||
}
|
||||
|
||||
void EpubReaderPrintedPageInputActivity::adjustDigit(int delta) {
|
||||
value += delta * powTen(cursorDigit);
|
||||
clampValue();
|
||||
// Don't pull the cursor in toward the new digit count — leave it where the user put it.
|
||||
// The cursor is a position the user navigated to, not a property of the value.
|
||||
if (cursorDigit > maxCursorDigit()) cursorDigit = maxCursorDigit();
|
||||
requestUpdate();
|
||||
}
|
||||
|
||||
void EpubReaderPrintedPageInputActivity::adjustDigitTimes(int multiplier, int sign) {
|
||||
// Used by the double-click handler: step is multiplier × 10^cursorDigit (e.g. 10 at the
|
||||
// ones place, 100 at the tens place). Sign is +1 or -1.
|
||||
value += sign * multiplier * powTen(cursorDigit);
|
||||
clampValue();
|
||||
if (cursorDigit > maxCursorDigit()) cursorDigit = maxCursorDigit();
|
||||
requestUpdate();
|
||||
}
|
||||
|
||||
void EpubReaderPrintedPageInputActivity::moveCursor(int delta) {
|
||||
cursorDigit += delta;
|
||||
if (cursorDigit < 0) cursorDigit = 0;
|
||||
if (cursorDigit > maxCursorDigit()) cursorDigit = maxCursorDigit();
|
||||
requestUpdate();
|
||||
}
|
||||
|
||||
void EpubReaderPrintedPageInputActivity::onEnter() {
|
||||
Activity::onEnter();
|
||||
// Force the FSM to wait for the double-click window on Up/Down so we can distinguish
|
||||
// Short (±1) from Double (±10). Adds ~300ms latency to single Up/Down presses; that's
|
||||
// the price for the larger step. Back/Confirm/Left/Right stay on the immediate
|
||||
// wasPressed path — no latency for navigation.
|
||||
buttonEvents.forceDoubleAction(MappedInputManager::Button::Up, true);
|
||||
buttonEvents.forceDoubleAction(MappedInputManager::Button::Down, true);
|
||||
requestUpdate();
|
||||
}
|
||||
|
||||
void EpubReaderPrintedPageInputActivity::onExit() {
|
||||
buttonEvents.forceDoubleAction(MappedInputManager::Button::Up, false);
|
||||
buttonEvents.forceDoubleAction(MappedInputManager::Button::Down, false);
|
||||
Activity::onExit();
|
||||
}
|
||||
|
||||
void EpubReaderPrintedPageInputActivity::loop() {
|
||||
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
|
||||
ActivityResult result;
|
||||
result.isCancelled = true;
|
||||
setResult(std::move(result));
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
|
||||
setResult(PrintedPageResult{std::to_string(value)});
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
if (mappedInput.wasPressed(MappedInputManager::Button::Left)) {
|
||||
moveCursor(1); // cursor moves toward higher digits on Left, matching screen layout
|
||||
return;
|
||||
}
|
||||
if (mappedInput.wasPressed(MappedInputManager::Button::Right)) {
|
||||
moveCursor(-1);
|
||||
return;
|
||||
}
|
||||
|
||||
// Up/Down come through the FSM-backed event queue so we can react to Short vs Double.
|
||||
// Short = ±1, Double = ±10. Both Up and PageBack map to the same hardware button; the
|
||||
// FSM emits an event for each logical button, but we only handle the Up/Down variants
|
||||
// here. The global dispatcher consumes PageBack/PageForward variants (they're configured
|
||||
// as page-turn actions in the reader) and dispatch is a no-op while this dialog is
|
||||
// current, so they're effectively swallowed.
|
||||
ButtonEventManager::ButtonEvent ev;
|
||||
while (buttonEvents.consumeEvent(ev)) {
|
||||
if (ev.button == MappedInputManager::Button::Up) {
|
||||
if (ev.type == ButtonEventManager::PressType::Short) {
|
||||
adjustDigit(1);
|
||||
} else if (ev.type == ButtonEventManager::PressType::Double) {
|
||||
adjustDigitTimes(10, 1);
|
||||
}
|
||||
} else if (ev.button == MappedInputManager::Button::Down) {
|
||||
if (ev.type == ButtonEventManager::PressType::Short) {
|
||||
adjustDigit(-1);
|
||||
} else if (ev.type == ButtonEventManager::PressType::Double) {
|
||||
adjustDigitTimes(10, -1);
|
||||
}
|
||||
}
|
||||
// Other queued events (PageBack/PageForward variants and any stray) are discarded;
|
||||
// the wasPressed path above already handled Back/Confirm/Left/Right.
|
||||
}
|
||||
}
|
||||
|
||||
void EpubReaderPrintedPageInputActivity::render(RenderLock&&) {
|
||||
renderer.clearScreen();
|
||||
|
||||
renderer.drawCenteredText(UI_12_FONT_ID, 15, tr(STR_GO_TO_PRINTED_PAGE), true, EpdFontFamily::BOLD);
|
||||
|
||||
// Big centred numeric value with an underline under the active digit position.
|
||||
// When the cursor sits over a digit position past the current value (e.g. cursor at the
|
||||
// tens place while value is still 1), we pad the displayed number with leading "·" dots
|
||||
// so the underline can mark the empty position it'll grow into on the next Up press.
|
||||
const std::string rawValueText = std::to_string(value);
|
||||
const int visibleDigits = std::max(digitCount(), cursorDigit + 1);
|
||||
std::string valueText;
|
||||
for (int i = 0; i < visibleDigits - digitCount(); i++) valueText += "0"; // leading zeros
|
||||
valueText += rawValueText;
|
||||
const int valueY = 110;
|
||||
renderer.drawCenteredText(UI_12_FONT_ID, valueY, valueText.c_str(), true, EpdFontFamily::BOLD);
|
||||
|
||||
// Place the underline under the active digit. Width-based positioning approximates a
|
||||
// monospaced grid using the total rendered width / digit count; small visual mismatch on
|
||||
// proportional fonts is acceptable for a one-character indicator.
|
||||
const int totalWidth = renderer.getTextWidth(UI_12_FONT_ID, valueText.c_str());
|
||||
const int screenWidth = renderer.getScreenWidth();
|
||||
const int startX = (screenWidth - totalWidth) / 2;
|
||||
const int digitIndexFromLeft = visibleDigits - 1 - cursorDigit; // 0-based, from the left
|
||||
const int avgDigitWidth = (visibleDigits > 0) ? totalWidth / visibleDigits : 0;
|
||||
const int underlineX = startX + digitIndexFromLeft * avgDigitWidth;
|
||||
const int underlineWidth = avgDigitWidth;
|
||||
const int underlineY = valueY + renderer.getLineHeight(UI_12_FONT_ID) + 2;
|
||||
renderer.fillRect(underlineX, underlineY, underlineWidth, 3, true);
|
||||
|
||||
// Range hint underneath: "Range: 1 - 305"
|
||||
char rangeBuf[48];
|
||||
snprintf(rangeBuf, sizeof(rangeBuf), tr(STR_GO_TO_PRINTED_PAGE_RANGE), (unsigned)minValue, (unsigned)maxValue);
|
||||
renderer.drawCenteredText(SMALL_FONT_ID, underlineY + 25, rangeBuf, true);
|
||||
|
||||
// Step hint.
|
||||
renderer.drawCenteredText(SMALL_FONT_ID, underlineY + 50, tr(STR_GO_TO_PRINTED_PAGE_HINT), true);
|
||||
|
||||
// Button hints.
|
||||
const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), "-", "+");
|
||||
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||
|
||||
renderer.displayBuffer();
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
#pragma once
|
||||
|
||||
#include "MappedInputManager.h"
|
||||
#include "activities/Activity.h"
|
||||
|
||||
// Numeric input dialog for "jump to printed page".
|
||||
// User adjusts a single integer (mirroring the printed-page label as shown in the book).
|
||||
// Up/Down change the digit under the cursor by ±1 (single) or ±10 (double-click).
|
||||
// Left/Right move the cursor between digits; Confirm returns the typed string.
|
||||
// Books with non-integer labels (roman numerals, etc.) are not addressable via this dialog;
|
||||
// the menu item is hidden if the book has no integer-parseable printed-page labels.
|
||||
class EpubReaderPrintedPageInputActivity final : public Activity {
|
||||
public:
|
||||
explicit EpubReaderPrintedPageInputActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, int initialValue,
|
||||
int minValue, int maxValue)
|
||||
: Activity("EpubReaderPrintedPageInput", renderer, mappedInput),
|
||||
value(initialValue),
|
||||
minValue(minValue),
|
||||
maxValue(maxValue) {
|
||||
clampValue();
|
||||
cursorDigit = 0; // ones place
|
||||
}
|
||||
|
||||
void onEnter() override;
|
||||
void onExit() override;
|
||||
void loop() override;
|
||||
void render(RenderLock&&) override;
|
||||
|
||||
private:
|
||||
int value = 0;
|
||||
int minValue = 1;
|
||||
int maxValue = 1;
|
||||
int cursorDigit = 0; // 0 = ones, 1 = tens, 2 = hundreds, ...
|
||||
|
||||
void clampValue();
|
||||
void adjustDigit(int delta);
|
||||
void adjustDigitTimes(int multiplier, int sign); // delta = sign * multiplier * 10^cursorDigit
|
||||
void moveCursor(int delta);
|
||||
static int powTen(int exponent);
|
||||
int digitCount() const;
|
||||
// Highest cursor position the user can reach: one less than the digit count of maxValue.
|
||||
// Lets the user move the cursor "past" the current value to higher digit positions that
|
||||
// don't exist yet, so they can grow the number quickly (e.g. start at 1, move cursor left,
|
||||
// press Up to make 11, etc.) without single-pressing dozens of times.
|
||||
int maxCursorDigit() const;
|
||||
};
|
||||
@@ -163,8 +163,10 @@ void KOReaderSyncActivity::performFetchAndCompare() {
|
||||
// avoid a second TLS handshake under fragmented heap.
|
||||
KOReaderSyncClient::beginPersistentSession();
|
||||
|
||||
logSyncMemSnapshot("before_getProgress");
|
||||
// Fetch remote progress
|
||||
const auto result = KOReaderSyncClient::getProgress(documentHash, remoteProgress);
|
||||
logSyncMemSnapshot("after_getProgress");
|
||||
|
||||
if (result == KOReaderSyncClient::NOT_FOUND) {
|
||||
if (syncIntent == KOReaderSyncIntentState::PULL_REMOTE) {
|
||||
@@ -278,7 +280,16 @@ void KOReaderSyncActivity::performFetchAndCompare() {
|
||||
// still useful for manual conflict decisions.
|
||||
// Pre-map remote progress now so compare UI always shows concrete chapter/
|
||||
// page data. The mapped result is cached and reused if Apply is chosen.
|
||||
if (!ensureRemotePositionMapped(false)) {
|
||||
// closeSessionBeforeMapping=true tears down the warmed TLS session before
|
||||
// reverse XPath mapping so the 32 KB inflate ring buffer can allocate.
|
||||
// Trade-off: if the user later picks Upload, we eat one extra TLS handshake
|
||||
// (~1.7s). That's the less-common choice — Apply is what users usually want —
|
||||
// and silent inflate failures here previously caused syncs to land on the
|
||||
// wrong page. See logSyncMemSnapshot("after_getProgress") for the heap drop
|
||||
// a held-open session causes (~36 KB contig consumed by esp_http_client
|
||||
// state and response buffer that aren't released until cleanup).
|
||||
logSyncMemSnapshot("before_compare_map");
|
||||
if (!ensureRemotePositionMapped(true)) {
|
||||
{
|
||||
RenderLock lock(*this);
|
||||
state = SYNC_FAILED;
|
||||
@@ -704,11 +715,19 @@ bool KOReaderSyncActivity::ensureRemotePositionMapped(const bool closeSessionBef
|
||||
return true;
|
||||
}
|
||||
|
||||
// Diagnostic snapshots around each phase of remote->local mapping. The reverse
|
||||
// XPath mapper needs a 32 KB contiguous block for the inflate ring buffer; if
|
||||
// that allocation fails we silently degrade to percentage-only mapping and
|
||||
// round-trip accuracy suffers. Snapshots here let us see exactly which phase
|
||||
// fragments the heap so the fix can target the actual culprit.
|
||||
logSyncMemSnapshot("ensureRemoteMap_entry");
|
||||
|
||||
// Mapping remote->local can trigger EPUB inflate work. For apply/pull paths,
|
||||
// release HTTP/TLS first to maximize heap headroom. Compare pre-map keeps
|
||||
// the warmed session alive so Upload can reuse it without a fresh handshake.
|
||||
if (closeSessionBeforeMapping) {
|
||||
KOReaderSyncClient::endPersistentSession();
|
||||
logSyncMemSnapshot("ensureRemoteMap_after_endSession");
|
||||
}
|
||||
|
||||
{
|
||||
@@ -716,12 +735,17 @@ bool KOReaderSyncActivity::ensureRemotePositionMapped(const bool closeSessionBef
|
||||
statusMessage = tr(STR_MAPPING_REMOTE);
|
||||
}
|
||||
requestUpdateAndWait();
|
||||
logSyncMemSnapshot("ensureRemoteMap_after_statusUpdate");
|
||||
|
||||
KOReaderPosition koPos = {remoteProgress.progress, remoteProgress.percentage};
|
||||
if (!ensureEpubLoadedForMapping()) {
|
||||
return false;
|
||||
}
|
||||
logSyncMemSnapshot("ensureRemoteMap_after_epubLoad");
|
||||
|
||||
remotePosition = ProgressMapper::toCrossPoint(epub, koPos, currentSpineIndex, totalPagesInSpine);
|
||||
logSyncMemSnapshot("ensureRemoteMap_after_toCrossPoint");
|
||||
|
||||
computeRemoteChapter();
|
||||
releaseEpubForMapping();
|
||||
hasRemoteProgress = true;
|
||||
|
||||
Reference in New Issue
Block a user