feat: silent pre-indexing with configurable status bar indicator

Port PR #979's silent pre-indexing and add an Indexing Display setting
(Popup / Status Bar Text / Status Bar Icon) so users can choose how
indexing feedback is shown.

Silent pre-indexing runs on text-only penultimate pages when a status
bar option is selected, with a standard requestUpdate to clear the
indicator. Image pages skip silent indexing to avoid e-ink grayscale
pipeline conflicts; the normal popup handles those transitions. Direct
chapter jumps always show the original small popup regardless of setting.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
cottongin
2026-02-19 09:30:29 -05:00
parent a1ac11ab51
commit 426a978e44
14 changed files with 143 additions and 3 deletions

View File

@@ -36,6 +36,11 @@ constexpr unsigned long longPressConfirmMs = 700;
constexpr int statusBarMargin = 19;
constexpr int progressBarMarginTop = 1;
// 8x8 1-bit hourglass icon for the indexing status bar indicator.
// Format: MSB-first, 0 = black pixel, 1 = white pixel (e-ink convention).
constexpr uint8_t kIndexingIcon[] = {0x00, 0x81, 0xC3, 0xE7, 0xE7, 0xC3, 0x81, 0x00};
constexpr int kIndexingIconSize = 8;
int clampPercent(int percent) {
if (percent < 0) {
return 0;
@@ -838,16 +843,17 @@ void EpubReaderActivity::render(Activity::RenderLock&& lock) {
(showProgressBar ? (metrics.bookProgressBarHeight + progressBarMarginTop) : 0);
}
const uint16_t viewportWidth = renderer.getScreenWidth() - orientedMarginLeft - orientedMarginRight;
const uint16_t viewportHeight = renderer.getScreenHeight() - orientedMarginTop - orientedMarginBottom;
if (!section) {
loadingSection = true;
preIndexedNextSpine = -1;
const auto filepath = epub->getSpineItem(currentSpineIndex).href;
LOG_DBG("ERS", "Loading file: %s, index: %d", filepath.c_str(), currentSpineIndex);
section = std::unique_ptr<Section>(new Section(epub, currentSpineIndex, renderer));
const uint16_t viewportWidth = renderer.getScreenWidth() - orientedMarginLeft - orientedMarginRight;
const uint16_t viewportHeight = renderer.getScreenHeight() - orientedMarginTop - orientedMarginBottom;
if (!section->loadSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle)) {
@@ -921,17 +927,85 @@ void EpubReaderActivity::render(Activity::RenderLock&& lock) {
LOG_ERR("ERS", "Failed to load page from SD - clearing section cache");
section->clearCache();
section.reset();
silentIndexingActive = false;
requestUpdate(); // Try again after clearing cache
// TODO: prevent infinite loop if the page keeps failing to load for some reason
return;
}
silentIndexingActive = false;
const bool textOnlyPage = !p->hasImages();
if (textOnlyPage &&
SETTINGS.indexingDisplay != CrossPointSettings::INDEXING_DISPLAY::INDEXING_POPUP &&
section->pageCount >= 1 &&
((section->pageCount == 1 && section->currentPage == 0) ||
(section->pageCount >= 2 && section->currentPage == section->pageCount - 2)) &&
currentSpineIndex + 1 < epub->getSpineItemsCount() &&
preIndexedNextSpine != currentSpineIndex + 1) {
Section probe(epub, currentSpineIndex + 1, renderer);
if (probe.loadSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle)) {
preIndexedNextSpine = currentSpineIndex + 1;
} else {
silentIndexingActive = true;
}
}
const auto start = millis();
renderContents(std::move(p), orientedMarginTop, orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
LOG_DBG("ERS", "Rendered page in %dms", millis() - start);
if (silentIndexingActive) {
silentIndexNextChapterIfNeeded(viewportWidth, viewportHeight);
requestUpdate();
}
}
saveProgress(currentSpineIndex, section->currentPage, section->pageCount);
}
bool EpubReaderActivity::silentIndexNextChapterIfNeeded(const uint16_t viewportWidth, const uint16_t viewportHeight) {
if (preIndexedNextSpine == currentSpineIndex + 1) {
silentIndexingActive = false;
return false;
}
const bool shouldPreIndex =
(section->pageCount == 1 && section->currentPage == 0) ||
(section->pageCount >= 2 && section->currentPage == section->pageCount - 2);
if (!epub || !section || !shouldPreIndex) {
silentIndexingActive = false;
return false;
}
const int nextSpineIndex = currentSpineIndex + 1;
if (nextSpineIndex < 0 || nextSpineIndex >= epub->getSpineItemsCount()) {
silentIndexingActive = false;
return false;
}
Section nextSection(epub, nextSpineIndex, renderer);
if (nextSection.loadSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle)) {
preIndexedNextSpine = nextSpineIndex;
silentIndexingActive = false;
return false;
}
LOG_DBG("ERS", "Silently indexing next chapter: %d", nextSpineIndex);
if (!nextSection.createSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle)) {
LOG_ERR("ERS", "Failed silent indexing for chapter: %d", nextSpineIndex);
silentIndexingActive = false;
return false;
}
preIndexedNextSpine = nextSpineIndex;
silentIndexingActive = false;
return true;
}
void EpubReaderActivity::saveProgress(int spineIndex, int currentPage, int pageCount) {
FsFile f;
if (Storage.openFileForWrite("ERS", epub->getCachePath() + "/progress.bin", f)) {
@@ -1151,4 +1225,15 @@ void EpubReaderActivity::renderStatusBar(const int orientedMarginRight, const in
titleMarginLeftAdjusted + orientedMarginLeft + (availableTitleSpace - titleWidth) / 2, textY,
title.c_str());
}
if (silentIndexingActive && SETTINGS.statusBar != CrossPointSettings::STATUS_BAR_MODE::NONE) {
const int batteryWidth = showBattery ? (showBatteryPercentage ? 50 : 20) : 0;
const int indicatorX = orientedMarginLeft + batteryWidth + 8;
if (SETTINGS.indexingDisplay == CrossPointSettings::INDEXING_DISPLAY::INDEXING_STATUS_TEXT) {
renderer.drawText(SMALL_FONT_ID, indicatorX, textY, tr(STR_INDEXING));
} else if (SETTINGS.indexingDisplay == CrossPointSettings::INDEXING_DISPLAY::INDEXING_STATUS_ICON) {
renderer.drawIcon(kIndexingIcon, indicatorX, textY - kIndexingIconSize + 2, kIndexingIconSize,
kIndexingIconSize);
}
}
}

View File

@@ -25,12 +25,15 @@ class EpubReaderActivity final : public ActivityWithSubactivity {
bool skipNextButtonCheck = false; // Skip button processing for one frame after subactivity exit
bool ignoreNextConfirmRelease = false; // Suppress short-press after long-press Confirm
volatile bool loadingSection = false; // True during the entire !section block (read from main loop)
bool silentIndexingActive = false; // True while silently pre-indexing the next chapter
int preIndexedNextSpine = -1; // Spine index already pre-indexed (prevents re-render loop)
const std::function<void()> onGoBack;
const std::function<void()> onGoHome;
void renderContents(std::unique_ptr<Page> page, int orientedMarginTop, int orientedMarginRight,
int orientedMarginBottom, int orientedMarginLeft);
void renderStatusBar(int orientedMarginRight, int orientedMarginBottom, int orientedMarginLeft) const;
bool silentIndexNextChapterIfNeeded(uint16_t viewportWidth, uint16_t viewportHeight);
void saveProgress(int spineIndex, int currentPage, int pageCount);
// Jump to a percentage of the book (0-100), mapping it to spine and page.
void jumpToPercent(int percent);