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:
@@ -390,6 +390,10 @@ enum class StrId : uint16_t {
|
||||
STR_TZ_HAWAII,
|
||||
STR_TZ_CUSTOM,
|
||||
STR_SET_UTC_OFFSET,
|
||||
STR_INDEXING_DISPLAY,
|
||||
STR_INDEXING_POPUP,
|
||||
STR_INDEXING_STATUS_TEXT,
|
||||
STR_INDEXING_STATUS_ICON,
|
||||
// Sentinel - must be last
|
||||
_COUNT
|
||||
};
|
||||
|
||||
@@ -335,3 +335,7 @@ STR_TZ_ALASKA: "Alaska"
|
||||
STR_TZ_HAWAII: "Hawaii"
|
||||
STR_TZ_CUSTOM: "Custom"
|
||||
STR_SET_UTC_OFFSET: "Set UTC Offset"
|
||||
STR_INDEXING_DISPLAY: "Zobrazení indexování"
|
||||
STR_INDEXING_POPUP: "Popup"
|
||||
STR_INDEXING_STATUS_TEXT: "Text stavového řádku"
|
||||
STR_INDEXING_STATUS_ICON: "Ikona stavového řádku"
|
||||
|
||||
@@ -356,3 +356,7 @@ STR_TZ_ALASKA: "Alaska"
|
||||
STR_TZ_HAWAII: "Hawaii"
|
||||
STR_TZ_CUSTOM: "Custom"
|
||||
STR_SET_UTC_OFFSET: "Set UTC Offset"
|
||||
STR_INDEXING_DISPLAY: "Indexing Display"
|
||||
STR_INDEXING_POPUP: "Popup"
|
||||
STR_INDEXING_STATUS_TEXT: "Status Bar Text"
|
||||
STR_INDEXING_STATUS_ICON: "Status Bar Icon"
|
||||
|
||||
@@ -335,3 +335,7 @@ STR_TZ_ALASKA: "Alaska"
|
||||
STR_TZ_HAWAII: "Hawaii"
|
||||
STR_TZ_CUSTOM: "Custom"
|
||||
STR_SET_UTC_OFFSET: "Set UTC Offset"
|
||||
STR_INDEXING_DISPLAY: "Affichage indexation"
|
||||
STR_INDEXING_POPUP: "Popup"
|
||||
STR_INDEXING_STATUS_TEXT: "Texte barre d'état"
|
||||
STR_INDEXING_STATUS_ICON: "Icône barre d'état"
|
||||
|
||||
@@ -335,3 +335,7 @@ STR_TZ_ALASKA: "Alaska"
|
||||
STR_TZ_HAWAII: "Hawaii"
|
||||
STR_TZ_CUSTOM: "Custom"
|
||||
STR_SET_UTC_OFFSET: "Set UTC Offset"
|
||||
STR_INDEXING_DISPLAY: "Indexierungsanzeige"
|
||||
STR_INDEXING_POPUP: "Popup"
|
||||
STR_INDEXING_STATUS_TEXT: "Statusleistentext"
|
||||
STR_INDEXING_STATUS_ICON: "Statusleistensymbol"
|
||||
|
||||
@@ -335,3 +335,7 @@ STR_TZ_ALASKA: "Alaska"
|
||||
STR_TZ_HAWAII: "Hawaii"
|
||||
STR_TZ_CUSTOM: "Custom"
|
||||
STR_SET_UTC_OFFSET: "Set UTC Offset"
|
||||
STR_INDEXING_DISPLAY: "Exibição de indexação"
|
||||
STR_INDEXING_POPUP: "Popup"
|
||||
STR_INDEXING_STATUS_TEXT: "Texto da barra"
|
||||
STR_INDEXING_STATUS_ICON: "Ícone da barra"
|
||||
|
||||
@@ -335,3 +335,7 @@ STR_TZ_ALASKA: "Alaska"
|
||||
STR_TZ_HAWAII: "Hawaii"
|
||||
STR_TZ_CUSTOM: "Custom"
|
||||
STR_SET_UTC_OFFSET: "Set UTC Offset"
|
||||
STR_INDEXING_DISPLAY: "Отображение индексации"
|
||||
STR_INDEXING_POPUP: "Popup"
|
||||
STR_INDEXING_STATUS_TEXT: "Текст в строке"
|
||||
STR_INDEXING_STATUS_ICON: "Иконка в строке"
|
||||
|
||||
@@ -335,3 +335,7 @@ STR_TZ_ALASKA: "Alaska"
|
||||
STR_TZ_HAWAII: "Hawaii"
|
||||
STR_TZ_CUSTOM: "Custom"
|
||||
STR_SET_UTC_OFFSET: "Set UTC Offset"
|
||||
STR_INDEXING_DISPLAY: "Mostrar indexación"
|
||||
STR_INDEXING_POPUP: "Popup"
|
||||
STR_INDEXING_STATUS_TEXT: "Texto barra estado"
|
||||
STR_INDEXING_STATUS_ICON: "Icono barra estado"
|
||||
|
||||
@@ -335,3 +335,7 @@ STR_TZ_ALASKA: "Alaska"
|
||||
STR_TZ_HAWAII: "Hawaii"
|
||||
STR_TZ_CUSTOM: "Custom"
|
||||
STR_SET_UTC_OFFSET: "Set UTC Offset"
|
||||
STR_INDEXING_DISPLAY: "Indexeringsvisning"
|
||||
STR_INDEXING_POPUP: "Popup"
|
||||
STR_INDEXING_STATUS_TEXT: "Statusfältstext"
|
||||
STR_INDEXING_STATUS_ICON: "Statusfältsikon"
|
||||
|
||||
@@ -143,6 +143,7 @@ uint8_t CrossPointSettings::writeSettings(FsFile& file, bool count_only) const {
|
||||
writer.writeItem(file, clockSize);
|
||||
writer.writeItem(file, timezone);
|
||||
writer.writeItem(file, timezoneOffsetHours);
|
||||
writer.writeItem(file, indexingDisplay);
|
||||
|
||||
return writer.item_count;
|
||||
}
|
||||
@@ -285,6 +286,8 @@ bool CrossPointSettings::loadFromFile() {
|
||||
serialization::readPod(inputFile, timezoneOffsetHours);
|
||||
if (timezoneOffsetHours < -12 || timezoneOffsetHours > 14) timezoneOffsetHours = 0;
|
||||
if (++settingsRead >= fileSettingsCount) break;
|
||||
readAndValidate(inputFile, indexingDisplay, INDEXING_DISPLAY_COUNT);
|
||||
if (++settingsRead >= fileSettingsCount) break;
|
||||
} while (false);
|
||||
|
||||
if (frontButtonMappingRead) {
|
||||
|
||||
@@ -52,6 +52,13 @@ class CrossPointSettings {
|
||||
STATUS_BAR_MODE_COUNT
|
||||
};
|
||||
|
||||
enum INDEXING_DISPLAY {
|
||||
INDEXING_POPUP = 0,
|
||||
INDEXING_STATUS_TEXT = 1,
|
||||
INDEXING_STATUS_ICON = 2,
|
||||
INDEXING_DISPLAY_COUNT
|
||||
};
|
||||
|
||||
enum ORIENTATION {
|
||||
PORTRAIT = 0, // 480x800 logical coordinates (current default)
|
||||
LANDSCAPE_CW = 1, // 800x480 logical coordinates, rotated 180° (swap top/bottom)
|
||||
@@ -157,6 +164,8 @@ class CrossPointSettings {
|
||||
uint8_t sleepScreenLetterboxFill = LETTERBOX_DITHERED;
|
||||
// Status bar settings
|
||||
uint8_t statusBar = FULL;
|
||||
// Indexing feedback display mode (popup, status bar text, status bar icon)
|
||||
uint8_t indexingDisplay = INDEXING_POPUP;
|
||||
// Text rendering settings
|
||||
uint8_t extraParagraphSpacing = 1;
|
||||
uint8_t textAntiAliasing = 1;
|
||||
|
||||
@@ -65,6 +65,10 @@ inline std::vector<SettingInfo> getSettingsList() {
|
||||
{StrId::STR_NONE_OPT, StrId::STR_NO_PROGRESS, StrId::STR_STATUS_BAR_FULL_PERCENT,
|
||||
StrId::STR_STATUS_BAR_FULL_BOOK, StrId::STR_STATUS_BAR_BOOK_ONLY, StrId::STR_STATUS_BAR_FULL_CHAPTER},
|
||||
"statusBar", StrId::STR_CAT_DISPLAY),
|
||||
SettingInfo::Enum(
|
||||
StrId::STR_INDEXING_DISPLAY, &CrossPointSettings::indexingDisplay,
|
||||
{StrId::STR_INDEXING_POPUP, StrId::STR_INDEXING_STATUS_TEXT, StrId::STR_INDEXING_STATUS_ICON},
|
||||
"indexingDisplay", StrId::STR_CAT_DISPLAY),
|
||||
SettingInfo::Enum(StrId::STR_HIDE_BATTERY, &CrossPointSettings::hideBatteryPercentage,
|
||||
{StrId::STR_NEVER, StrId::STR_IN_READER, StrId::STR_ALWAYS}, "hideBatteryPercentage",
|
||||
StrId::STR_CAT_DISPLAY),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user