diff --git a/src/activities/boot_sleep/SleepActivity.cpp b/src/activities/boot_sleep/SleepActivity.cpp index ff91a209..55b93f5b 100644 --- a/src/activities/boot_sleep/SleepActivity.cpp +++ b/src/activities/boot_sleep/SleepActivity.cpp @@ -12,6 +12,7 @@ #include "CrossPointSettings.h" #include "CrossPointState.h" +#include "util/BookSettings.h" #include "components/UITheme.h" #include "fontIds.h" #include "images/Logo120.h" @@ -450,7 +451,8 @@ void SleepActivity::renderDefaultSleepScreen() const { renderer.displayBuffer(HalDisplay::HALF_REFRESH); } -void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap, const std::string& edgeCachePath) const { +void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap, const std::string& edgeCachePath, + uint8_t fillModeOverride) const { int x, y; const auto pageWidth = renderer.getScreenWidth(); const auto pageHeight = renderer.getScreenHeight(); @@ -494,8 +496,11 @@ void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap, const std::str const float scale = std::min(static_cast(pageWidth) / effectiveWidth, static_cast(pageHeight) / effectiveHeight); - // Determine letterbox fill settings - const uint8_t fillMode = SETTINGS.sleepScreenLetterboxFill; + // Determine letterbox fill settings (per-book override takes precedence) + const uint8_t fillMode = (fillModeOverride != BookSettings::USE_GLOBAL && + fillModeOverride < CrossPointSettings::SLEEP_SCREEN_LETTERBOX_FILL_COUNT) + ? fillModeOverride + : SETTINGS.sleepScreenLetterboxFill; const bool wantFill = (fillMode != CrossPointSettings::SLEEP_SCREEN_LETTERBOX_FILL::LETTERBOX_NONE); static const char* fillModeNames[] = {"dithered", "solid", "none"}; @@ -582,6 +587,7 @@ void SleepActivity::renderCoverSleepScreen() const { } std::string coverBmpPath; + std::string bookCachePath; bool cropped = SETTINGS.sleepScreenCoverMode == CrossPointSettings::SLEEP_SCREEN_COVER_MODE::CROP; // Check if the current book is XTC, TXT, or EPUB @@ -600,6 +606,7 @@ void SleepActivity::renderCoverSleepScreen() const { } coverBmpPath = lastXtc.getCoverBmpPath(); + bookCachePath = lastXtc.getCachePath(); } else if (StringUtils::checkFileExtension(APP_STATE.openEpubPath, ".txt")) { // Handle TXT file - looks for cover image in the same folder Txt lastTxt(APP_STATE.openEpubPath, "/.crosspoint"); @@ -614,6 +621,7 @@ void SleepActivity::renderCoverSleepScreen() const { } coverBmpPath = lastTxt.getCoverBmpPath(); + bookCachePath = lastTxt.getCachePath(); } else if (StringUtils::checkFileExtension(APP_STATE.openEpubPath, ".epub")) { // Handle EPUB file Epub lastEpub(APP_STATE.openEpubPath, "/.crosspoint"); @@ -629,10 +637,18 @@ void SleepActivity::renderCoverSleepScreen() const { } coverBmpPath = lastEpub.getCoverBmpPath(cropped); + bookCachePath = lastEpub.getCachePath(); } else { return (this->*renderNoCoverSleepScreen)(); } + // Load per-book letterbox fill override (falls back to global if not set) + uint8_t fillModeOverride = BookSettings::USE_GLOBAL; + if (!bookCachePath.empty()) { + auto bookSettings = BookSettings::load(bookCachePath); + fillModeOverride = bookSettings.letterboxFillOverride; + } + FsFile file; if (Storage.openFileForRead("SLP", coverBmpPath, file)) { Bitmap bitmap(file); @@ -644,7 +660,7 @@ void SleepActivity::renderCoverSleepScreen() const { if (dotPos != std::string::npos) { edgeCachePath = coverBmpPath.substr(0, dotPos) + "_edges.bin"; } - renderBitmapSleepScreen(bitmap, edgeCachePath); + renderBitmapSleepScreen(bitmap, edgeCachePath, fillModeOverride); return; } } diff --git a/src/activities/boot_sleep/SleepActivity.h b/src/activities/boot_sleep/SleepActivity.h index a39ac448..36dd25f5 100644 --- a/src/activities/boot_sleep/SleepActivity.h +++ b/src/activities/boot_sleep/SleepActivity.h @@ -16,6 +16,8 @@ class SleepActivity final : public Activity { void renderDefaultSleepScreen() const; void renderCustomSleepScreen() const; void renderCoverSleepScreen() const; - void renderBitmapSleepScreen(const Bitmap& bitmap, const std::string& edgeCachePath = "") const; + // fillModeOverride: 0xFF = use global setting, otherwise a SLEEP_SCREEN_LETTERBOX_FILL value. + void renderBitmapSleepScreen(const Bitmap& bitmap, const std::string& edgeCachePath = "", + uint8_t fillModeOverride = 0xFF) const; void renderBlankSleepScreen() const; }; diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index 398095e3..3837966f 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -242,7 +242,7 @@ void EpubReaderActivity::loop() { exitActivity(); enterNewActivity(new EpubReaderMenuActivity( this->renderer, this->mappedInput, epub->getTitle(), currentPage, totalPages, bookProgressPercent, - SETTINGS.orientation, hasDictionary, isBookmarked, + SETTINGS.orientation, hasDictionary, isBookmarked, epub->getCachePath(), [this](const uint8_t orientation) { onReaderMenuBack(orientation); }, [this](EpubReaderMenuActivity::MenuAction action) { onReaderMenuConfirm(action); })); xSemaphoreGive(renderingMutex); @@ -776,6 +776,10 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction } break; } + // Handled locally in the menu activity (cycle on Confirm, never dispatched here) + case EpubReaderMenuActivity::MenuAction::ROTATE_SCREEN: + case EpubReaderMenuActivity::MenuAction::LETTERBOX_FILL: + break; } } diff --git a/src/activities/reader/EpubReaderMenuActivity.cpp b/src/activities/reader/EpubReaderMenuActivity.cpp index 58ec6c4e..3aacef2f 100644 --- a/src/activities/reader/EpubReaderMenuActivity.cpp +++ b/src/activities/reader/EpubReaderMenuActivity.cpp @@ -68,6 +68,14 @@ void EpubReaderMenuActivity::loop() { updateRequired = true; return; } + if (selectedAction == MenuAction::LETTERBOX_FILL) { + // Cycle through: Default -> Dithered -> Solid -> None -> Default ... + int idx = (letterboxFillToIndex() + 1) % LETTERBOX_FILL_OPTION_COUNT; + pendingLetterboxFill = indexToLetterboxFill(idx); + saveLetterboxFill(); + updateRequired = true; + return; + } // 1. Capture the callback and action locally auto actionCallback = onAction; @@ -139,6 +147,12 @@ void EpubReaderMenuActivity::renderScreen() { const auto width = renderer.getTextWidth(UI_10_FONT_ID, value); renderer.drawText(UI_10_FONT_ID, contentX + contentWidth - 20 - width, displayY, value, !isSelected); } + if (menuItems[i].action == MenuAction::LETTERBOX_FILL) { + // Render current letterbox fill value on the right edge of the content area. + const auto value = letterboxFillLabels[letterboxFillToIndex()]; + const auto width = renderer.getTextWidth(UI_10_FONT_ID, value); + renderer.drawText(UI_10_FONT_ID, contentX + contentWidth - 20 - width, displayY, value, !isSelected); + } } // Footer / Hints diff --git a/src/activities/reader/EpubReaderMenuActivity.h b/src/activities/reader/EpubReaderMenuActivity.h index 9a002039..f4b511e5 100644 --- a/src/activities/reader/EpubReaderMenuActivity.h +++ b/src/activities/reader/EpubReaderMenuActivity.h @@ -9,6 +9,7 @@ #include #include "../ActivityWithSubactivity.h" +#include "util/BookSettings.h" #include "util/ButtonNavigator.h" class EpubReaderMenuActivity final : public ActivityWithSubactivity { @@ -20,6 +21,7 @@ class EpubReaderMenuActivity final : public ActivityWithSubactivity { LOOKUP, LOOKED_UP_WORDS, ROTATE_SCREEN, + LETTERBOX_FILL, SELECT_CHAPTER, GO_TO_BOOKMARK, GO_TO_PERCENT, @@ -32,18 +34,23 @@ class EpubReaderMenuActivity final : public ActivityWithSubactivity { explicit EpubReaderMenuActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const std::string& title, const int currentPage, const int totalPages, const int bookProgressPercent, const uint8_t currentOrientation, const bool hasDictionary, - const bool isBookmarked, + const bool isBookmarked, const std::string& bookCachePath, const std::function& onBack, const std::function& onAction) : ActivityWithSubactivity("EpubReaderMenu", renderer, mappedInput), menuItems(buildMenuItems(hasDictionary, isBookmarked)), title(title), pendingOrientation(currentOrientation), + bookCachePath(bookCachePath), currentPage(currentPage), totalPages(totalPages), bookProgressPercent(bookProgressPercent), onBack(onBack), - onAction(onAction) {} + onAction(onAction) { + // Load per-book settings to initialize the letterbox fill override + auto bookSettings = BookSettings::load(bookCachePath); + pendingLetterboxFill = bookSettings.letterboxFillOverride; + } void onEnter() override; void onExit() override; @@ -65,6 +72,11 @@ class EpubReaderMenuActivity final : public ActivityWithSubactivity { std::string title = "Reader Menu"; uint8_t pendingOrientation = 0; const std::vector orientationLabels = {"Portrait", "Landscape CW", "Inverted", "Landscape CCW"}; + std::string bookCachePath; + // Letterbox fill override: 0xFF = Default (use global), 0 = Dithered, 1 = Solid, 2 = None + uint8_t pendingLetterboxFill = BookSettings::USE_GLOBAL; + static constexpr int LETTERBOX_FILL_OPTION_COUNT = 4; // Default + 3 modes + const std::vector letterboxFillLabels = {"Default", "Dithered", "Solid", "None"}; int currentPage = 0; int totalPages = 0; int bookProgressPercent = 0; @@ -72,6 +84,25 @@ class EpubReaderMenuActivity final : public ActivityWithSubactivity { const std::function onBack; const std::function onAction; + // Map the internal override value to an index into letterboxFillLabels. + int letterboxFillToIndex() const { + if (pendingLetterboxFill == BookSettings::USE_GLOBAL) return 0; // "Default" + return pendingLetterboxFill + 1; // 0->1 (Dithered), 1->2 (Solid), 2->3 (None) + } + + // Map an index from letterboxFillLabels back to an override value. + static uint8_t indexToLetterboxFill(int index) { + if (index == 0) return BookSettings::USE_GLOBAL; + return static_cast(index - 1); + } + + // Save the current letterbox fill override to the book's settings file. + void saveLetterboxFill() const { + auto bookSettings = BookSettings::load(bookCachePath); + bookSettings.letterboxFillOverride = pendingLetterboxFill; + BookSettings::save(bookCachePath, bookSettings); + } + static std::vector buildMenuItems(bool hasDictionary, bool isBookmarked) { std::vector items; if (isBookmarked) { @@ -84,6 +115,7 @@ class EpubReaderMenuActivity final : public ActivityWithSubactivity { items.push_back({MenuAction::LOOKED_UP_WORDS, "Lookup Word History"}); } items.push_back({MenuAction::ROTATE_SCREEN, "Reading Orientation"}); + items.push_back({MenuAction::LETTERBOX_FILL, "Letterbox Fill"}); items.push_back({MenuAction::SELECT_CHAPTER, "Table of Contents"}); items.push_back({MenuAction::GO_TO_BOOKMARK, "Go to Bookmark"}); items.push_back({MenuAction::GO_TO_PERCENT, "Go to %"}); diff --git a/src/util/BookSettings.cpp b/src/util/BookSettings.cpp new file mode 100644 index 00000000..ef196a05 --- /dev/null +++ b/src/util/BookSettings.cpp @@ -0,0 +1,60 @@ +#include "BookSettings.h" + +#include +#include +#include + +namespace { +constexpr uint8_t BOOK_SETTINGS_VERSION = 1; +constexpr uint8_t BOOK_SETTINGS_COUNT = 1; // Number of persisted fields +} // namespace + +std::string BookSettings::filePath(const std::string& cachePath) { return cachePath + "/book_settings.bin"; } + +BookSettings BookSettings::load(const std::string& cachePath) { + BookSettings settings; + FsFile f; + if (!Storage.openFileForRead("BST", filePath(cachePath), f)) { + return settings; + } + + uint8_t version; + serialization::readPod(f, version); + if (version != BOOK_SETTINGS_VERSION) { + f.close(); + return settings; + } + + uint8_t fieldCount; + serialization::readPod(f, fieldCount); + + // Read fields that exist (supports older files with fewer fields) + uint8_t fieldsRead = 0; + do { + serialization::readPod(f, settings.letterboxFillOverride); + if (++fieldsRead >= fieldCount) break; + // New fields added here for forward compatibility + } while (false); + + f.close(); + Serial.printf("[%lu] [BST] Loaded book settings from %s (letterboxFill=%d)\n", millis(), filePath(cachePath).c_str(), + settings.letterboxFillOverride); + return settings; +} + +bool BookSettings::save(const std::string& cachePath, const BookSettings& settings) { + FsFile f; + if (!Storage.openFileForWrite("BST", filePath(cachePath), f)) { + Serial.printf("[%lu] [BST] Could not save book settings!\n", millis()); + return false; + } + + serialization::writePod(f, BOOK_SETTINGS_VERSION); + serialization::writePod(f, BOOK_SETTINGS_COUNT); + serialization::writePod(f, settings.letterboxFillOverride); + // New fields added here + f.close(); + + Serial.printf("[%lu] [BST] Saved book settings to %s\n", millis(), filePath(cachePath).c_str()); + return true; +} diff --git a/src/util/BookSettings.h b/src/util/BookSettings.h new file mode 100644 index 00000000..64516499 --- /dev/null +++ b/src/util/BookSettings.h @@ -0,0 +1,31 @@ +#pragma once +#include +#include + +#include "CrossPointSettings.h" + +// Per-book settings stored in the book's cache directory. +// Fields default to sentinel values (0xFF) meaning "use global setting". +class BookSettings { + public: + // 0xFF = use global default; otherwise one of SLEEP_SCREEN_LETTERBOX_FILL values (0-2). + uint8_t letterboxFillOverride = USE_GLOBAL; + + static constexpr uint8_t USE_GLOBAL = 0xFF; + + // Returns the effective letterbox fill mode: the per-book override if set, + // otherwise the global setting from CrossPointSettings. + uint8_t getEffectiveLetterboxFill() const { + if (letterboxFillOverride != USE_GLOBAL && + letterboxFillOverride < CrossPointSettings::SLEEP_SCREEN_LETTERBOX_FILL_COUNT) { + return letterboxFillOverride; + } + return SETTINGS.sleepScreenLetterboxFill; + } + + static BookSettings load(const std::string& cachePath); + static bool save(const std::string& cachePath, const BookSettings& settings); + + private: + static std::string filePath(const std::string& cachePath); +};