diff --git a/lib/I18n/I18nKeys.h b/lib/I18n/I18nKeys.h index de304830..323d5741 100644 --- a/lib/I18n/I18nKeys.h +++ b/lib/I18n/I18nKeys.h @@ -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 }; diff --git a/lib/I18n/translations/czech.yaml b/lib/I18n/translations/czech.yaml index 394d6a47..647f584b 100644 --- a/lib/I18n/translations/czech.yaml +++ b/lib/I18n/translations/czech.yaml @@ -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" diff --git a/lib/I18n/translations/english.yaml b/lib/I18n/translations/english.yaml index 7ebbcfdd..fa7e4f08 100644 --- a/lib/I18n/translations/english.yaml +++ b/lib/I18n/translations/english.yaml @@ -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" diff --git a/lib/I18n/translations/french.yaml b/lib/I18n/translations/french.yaml index 8b2eebec..9ca39276 100644 --- a/lib/I18n/translations/french.yaml +++ b/lib/I18n/translations/french.yaml @@ -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" diff --git a/lib/I18n/translations/german.yaml b/lib/I18n/translations/german.yaml index e4ac32f4..ceb9e4f3 100644 --- a/lib/I18n/translations/german.yaml +++ b/lib/I18n/translations/german.yaml @@ -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" diff --git a/lib/I18n/translations/portuguese.yaml b/lib/I18n/translations/portuguese.yaml index 0f5d5690..6b516fd3 100644 --- a/lib/I18n/translations/portuguese.yaml +++ b/lib/I18n/translations/portuguese.yaml @@ -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" diff --git a/lib/I18n/translations/russian.yaml b/lib/I18n/translations/russian.yaml index 30811196..25110a90 100644 --- a/lib/I18n/translations/russian.yaml +++ b/lib/I18n/translations/russian.yaml @@ -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: "Иконка в строке" diff --git a/lib/I18n/translations/spanish.yaml b/lib/I18n/translations/spanish.yaml index 2cd81e42..73e70e15 100644 --- a/lib/I18n/translations/spanish.yaml +++ b/lib/I18n/translations/spanish.yaml @@ -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" diff --git a/lib/I18n/translations/swedish.yaml b/lib/I18n/translations/swedish.yaml index b9aae499..b25a5b26 100644 --- a/lib/I18n/translations/swedish.yaml +++ b/lib/I18n/translations/swedish.yaml @@ -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" diff --git a/src/CrossPointSettings.cpp b/src/CrossPointSettings.cpp index b5c0fa40..efd9aa68 100644 --- a/src/CrossPointSettings.cpp +++ b/src/CrossPointSettings.cpp @@ -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) { diff --git a/src/CrossPointSettings.h b/src/CrossPointSettings.h index 2d4b10e5..31cd644c 100644 --- a/src/CrossPointSettings.h +++ b/src/CrossPointSettings.h @@ -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; diff --git a/src/SettingsList.h b/src/SettingsList.h index a5d1527b..fe39a489 100644 --- a/src/SettingsList.h +++ b/src/SettingsList.h @@ -65,6 +65,10 @@ inline std::vector 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), diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index 041135d2..01b845ca 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -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
(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); + } + } } diff --git a/src/activities/reader/EpubReaderActivity.h b/src/activities/reader/EpubReaderActivity.h index 754f3756..589825d6 100644 --- a/src/activities/reader/EpubReaderActivity.h +++ b/src/activities/reader/EpubReaderActivity.h @@ -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 onGoBack; const std::function onGoHome; void renderContents(std::unique_ptr 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);