Merge branch 'feat-localoverride' of https://github.com/jpirnay/crosspoint-reader into mybuild

This commit is contained in:
jpirnay
2026-03-20 18:38:23 +01:00
8 changed files with 187 additions and 27 deletions
+11
View File
@@ -303,6 +303,8 @@ bool JsonSettingsIO::saveRecentBooks(const RecentBooksStore& store, const char*
obj["author"] = book.author;
obj["series"] = book.series;
obj["coverBmpPath"] = book.coverBmpPath;
obj["embeddedStyleOverride"] = book.embeddedStyleOverride;
obj["imageRenderingOverride"] = book.imageRenderingOverride;
}
String json;
@@ -320,6 +322,13 @@ bool JsonSettingsIO::loadRecentBooks(RecentBooksStore& store, const char* json)
store.recentBooks.clear();
JsonArray arr = doc["books"].as<JsonArray>();
auto clampInt8 = [](int value, int minValue, int maxValue, int8_t fallback) -> int8_t {
if (value < minValue || value > maxValue) {
return fallback;
}
return static_cast<int8_t>(value);
};
for (JsonObject obj : arr) {
if (store.getCount() >= 10) break;
RecentBook book;
@@ -328,6 +337,8 @@ bool JsonSettingsIO::loadRecentBooks(RecentBooksStore& store, const char* json)
book.author = obj["author"] | std::string("");
book.series = obj["series"] | std::string("");
book.coverBmpPath = obj["coverBmpPath"] | std::string("");
book.embeddedStyleOverride = clampInt8(obj["embeddedStyleOverride"] | -1, -1, 1, -1);
book.imageRenderingOverride = clampInt8(obj["imageRenderingOverride"] | -1, -1, 2, -1);
store.recentBooks.push_back(book);
}
+30
View File
@@ -22,15 +22,23 @@ RecentBooksStore RecentBooksStore::instance;
void RecentBooksStore::addBook(const std::string& path, const std::string& title, const std::string& author,
const std::string& series, const std::string& coverBmpPath) {
const std::string& coverBmpPath) {
int8_t embeddedStyleOverride = -1;
int8_t imageRenderingOverride = -1;
// Remove existing entry if present
auto it =
std::find_if(recentBooks.begin(), recentBooks.end(), [&](const RecentBook& book) { return book.path == path; });
if (it != recentBooks.end()) {
embeddedStyleOverride = it->embeddedStyleOverride;
imageRenderingOverride = it->imageRenderingOverride;
recentBooks.erase(it);
}
// Add to front
recentBooks.insert(recentBooks.begin(), {path, title, author, series, coverBmpPath});
recentBooks.insert(recentBooks.begin(),
{path, title, author, coverBmpPath, embeddedStyleOverride, imageRenderingOverride});
// Trim to max size
if (recentBooks.size() > MAX_RECENT_BOOKS) {
@@ -54,6 +62,28 @@ void RecentBooksStore::updateBook(const std::string& path, const std::string& ti
}
}
RecentBook RecentBooksStore::getBookByPath(const std::string& path) const {
auto it =
std::find_if(recentBooks.begin(), recentBooks.end(), [&](const RecentBook& book) { return book.path == path; });
if (it != recentBooks.end()) {
return *it;
}
return RecentBook{};
}
bool RecentBooksStore::setReaderOverrides(const std::string& path, const int8_t embeddedStyleOverride,
const int8_t imageRenderingOverride) {
auto it =
std::find_if(recentBooks.begin(), recentBooks.end(), [&](const RecentBook& book) { return book.path == path; });
if (it == recentBooks.end()) {
return false;
}
it->embeddedStyleOverride = embeddedStyleOverride;
it->imageRenderingOverride = imageRenderingOverride;
return saveToFile();
}
bool RecentBooksStore::saveToFile() const {
Storage.mkdir("/.crosspoint");
return JsonSettingsIO::saveRecentBooks(*this, RECENT_BOOKS_FILE_JSON);
+8
View File
@@ -1,13 +1,19 @@
#pragma once
#include <cstdint>
#include <string>
#include <vector>
struct RecentBook {
std::string path;
std::string title;
std::string author;
std::string series;
std::string coverBmpPath;
// -1 = use global setting, otherwise explicit per-book override.
int8_t embeddedStyleOverride = -1;
// -1 = use global setting, otherwise CrossPointSettings::IMAGE_RENDERING value.
int8_t imageRenderingOverride = -1;
bool operator==(const RecentBook& other) const { return path == other.path; }
};
@@ -48,6 +54,8 @@ class RecentBooksStore {
bool loadFromFile();
RecentBook getDataFromBook(std::string path) const;
RecentBook getBookByPath(const std::string& path) const;
bool setReaderOverrides(const std::string& path, int8_t embeddedStyleOverride, int8_t imageRenderingOverride);
private:
bool loadFromBinaryFile();
+2
View File
@@ -21,6 +21,8 @@ struct MenuResult {
int action = -1;
uint8_t orientation = 0;
uint8_t pageTurnOption = 0;
int8_t embeddedStyleOverride = -1;
int8_t imageRenderingOverride = -1;
};
struct ChapterResult {
+65 -23
View File
@@ -86,9 +86,10 @@ void EpubReaderActivity::onEnter() {
// Save current epub as last opened epub and add to recent books
APP_STATE.openEpubPath = epub->getPath();
APP_STATE.saveToFile();
std::string series = epub->getSeries();
if (!series.empty() && !epub->getSeriesIndex().empty()) series += " #" + epub->getSeriesIndex();
RECENT_BOOKS.addBook(epub->getPath(), epub->getTitle(), epub->getAuthor(), series, epub->getThumbBmpPath());
RECENT_BOOKS.addBook(epub->getPath(), epub->getTitle(), epub->getAuthor(), epub->getThumbBmpPath());
const RecentBook currentBook = RECENT_BOOKS.getBookByPath(epub->getPath());
bookEmbeddedStyleOverride = currentBook.embeddedStyleOverride;
bookImageRenderingOverride = currentBook.imageRenderingOverride;
// Trigger first update
requestUpdate();
@@ -149,18 +150,20 @@ void EpubReaderActivity::loop() {
bookProgress = epub->calculateProgress(currentSpineIndex, chapterProgress) * 100.0f;
}
const int bookProgressPercent = clampPercent(static_cast<int>(bookProgress + 0.5f));
startActivityForResult(std::make_unique<EpubReaderMenuActivity>(
renderer, mappedInput, epub->getTitle(), currentPage, totalPages, bookProgressPercent,
SETTINGS.orientation, !currentPageFootnotes.empty()),
[this](const ActivityResult& result) {
// Always apply orientation change even if the menu was cancelled
const auto& menu = std::get<MenuResult>(result.data);
applyOrientation(menu.orientation);
toggleAutoPageTurn(menu.pageTurnOption);
if (!result.isCancelled) {
onReaderMenuConfirm(static_cast<EpubReaderMenuActivity::MenuAction>(menu.action));
}
});
startActivityForResult(
std::make_unique<EpubReaderMenuActivity>(
renderer, mappedInput, epub->getTitle(), currentPage, totalPages, bookProgressPercent, SETTINGS.orientation,
!currentPageFootnotes.empty(), bookEmbeddedStyleOverride, bookImageRenderingOverride),
[this](const ActivityResult& result) {
// Always apply orientation change even if the menu was cancelled
const auto& menu = std::get<MenuResult>(result.data);
applyOrientation(menu.orientation);
toggleAutoPageTurn(menu.pageTurnOption);
applyBookReaderOverrides(menu.embeddedStyleOverride, menu.imageRenderingOverride);
if (!result.isCancelled) {
onReaderMenuConfirm(static_cast<EpubReaderMenuActivity::MenuAction>(menu.action));
}
});
}
// Long press BACK (1s+) goes to file selection
@@ -460,6 +463,43 @@ void EpubReaderActivity::toggleAutoPageTurn(const uint8_t selectedPageTurnOption
}
}
void EpubReaderActivity::applyBookReaderOverrides(const int8_t embeddedStyleOverride,
const int8_t imageRenderingOverride) {
if (!epub) {
return;
}
if (bookEmbeddedStyleOverride == embeddedStyleOverride && bookImageRenderingOverride == imageRenderingOverride) {
return;
}
bookEmbeddedStyleOverride = embeddedStyleOverride;
bookImageRenderingOverride = imageRenderingOverride;
RECENT_BOOKS.setReaderOverrides(epub->getPath(), bookEmbeddedStyleOverride, bookImageRenderingOverride);
RenderLock lock(*this);
if (section) {
cachedSpineIndex = currentSpineIndex;
cachedChapterTotalPageCount = section->pageCount;
nextPageNumber = section->currentPage;
}
section.reset();
}
bool EpubReaderActivity::getEffectiveEmbeddedStyle() const {
if (bookEmbeddedStyleOverride >= 0) {
return bookEmbeddedStyleOverride != 0;
}
return SETTINGS.embeddedStyle != 0;
}
uint8_t EpubReaderActivity::getEffectiveImageRendering() const {
if (bookImageRenderingOverride >= 0) {
return static_cast<uint8_t>(bookImageRenderingOverride);
}
return SETTINGS.imageRendering;
}
void EpubReaderActivity::pageTurn(bool isForwardTurn) {
if (isForwardTurn) {
if (section->currentPage < section->pageCount - 1) {
@@ -538,22 +578,23 @@ void EpubReaderActivity::render(RenderLock&& lock) {
const uint16_t viewportHeight = renderer.getScreenHeight() - orientedMarginTop - orientedMarginBottom;
if (!section) {
const bool embeddedStyle = getEffectiveEmbeddedStyle();
const uint8_t imageRendering = getEffectiveImageRendering();
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));
if (!section->loadSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle,
SETTINGS.imageRendering)) {
viewportHeight, SETTINGS.hyphenationEnabled, embeddedStyle, imageRendering)) {
LOG_DBG("ERS", "Cache not found, building...");
const auto popupFn = [this]() { GUI.drawPopup(renderer, tr(STR_INDEXING)); };
if (!section->createSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle,
SETTINGS.imageRendering, popupFn)) {
viewportHeight, SETTINGS.hyphenationEnabled, embeddedStyle, imageRendering,
popupFn)) {
LOG_ERR("ERS", "Failed to persist page data to SD");
section.reset();
return;
@@ -663,19 +704,20 @@ void EpubReaderActivity::silentIndexNextChapterIfNeeded(const uint16_t viewportW
return;
}
const bool embeddedStyle = getEffectiveEmbeddedStyle();
const uint8_t imageRendering = getEffectiveImageRendering();
Section nextSection(epub, nextSpineIndex, renderer);
if (nextSection.loadSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle,
SETTINGS.imageRendering)) {
viewportHeight, SETTINGS.hyphenationEnabled, embeddedStyle, imageRendering)) {
return;
}
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,
SETTINGS.imageRendering)) {
viewportHeight, SETTINGS.hyphenationEnabled, embeddedStyle, imageRendering)) {
LOG_ERR("ERS", "Failed silent indexing for chapter: %d", nextSpineIndex);
}
}
@@ -27,6 +27,9 @@ class EpubReaderActivity final : public Activity {
bool pendingScreenshot = false;
bool skipNextButtonCheck = false; // Skip button processing for one frame after subactivity exit
bool automaticPageTurnActive = false;
// -1 means use global SETTINGS value.
int8_t bookEmbeddedStyleOverride = -1;
int8_t bookImageRenderingOverride = -1;
// Footnote support
std::vector<FootnoteEntry> currentPageFootnotes;
@@ -48,6 +51,9 @@ class EpubReaderActivity final : public Activity {
void onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction action);
void applyOrientation(uint8_t orientation);
void toggleAutoPageTurn(uint8_t selectedPageTurnOption);
void applyBookReaderOverrides(int8_t embeddedStyleOverride, int8_t imageRenderingOverride);
bool getEffectiveEmbeddedStyle() const;
uint8_t getEffectiveImageRendering() const;
void pageTurn(bool isForwardTurn);
// Footnote navigation
@@ -10,11 +10,14 @@
EpubReaderMenuActivity::EpubReaderMenuActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
const std::string& title, const int currentPage, const int totalPages,
const int bookProgressPercent, const uint8_t currentOrientation,
const bool hasFootnotes)
const bool hasFootnotes, const int8_t initialEmbeddedStyleOverride,
const int8_t initialImageRenderingOverride)
: Activity("EpubReaderMenu", renderer, mappedInput),
menuItems(buildMenuItems(hasFootnotes)),
title(title),
pendingOrientation(currentOrientation),
pendingEmbeddedStyleOverride(initialEmbeddedStyleOverride),
pendingImageRenderingOverride(initialImageRenderingOverride),
currentPage(currentPage),
totalPages(totalPages),
bookProgressPercent(bookProgressPercent) {}
@@ -26,6 +29,8 @@ std::vector<EpubReaderMenuActivity::MenuItem> EpubReaderMenuActivity::buildMenuI
if (hasFootnotes) {
items.push_back({MenuAction::FOOTNOTES, StrId::STR_FOOTNOTES});
}
items.push_back({MenuAction::EMBEDDED_STYLE, StrId::STR_EMBEDDED_STYLE});
items.push_back({MenuAction::IMAGE_RENDERING, StrId::STR_IMAGES});
items.push_back({MenuAction::ROTATE_SCREEN, StrId::STR_ORIENTATION});
items.push_back({MenuAction::AUTO_PAGE_TURN, StrId::STR_AUTO_TURN_PAGES_PER_MIN});
items.push_back({MenuAction::GO_TO_PERCENT, StrId::STR_GO_TO_PERCENT});
@@ -71,13 +76,41 @@ void EpubReaderMenuActivity::loop() {
return;
}
setResult(MenuResult{static_cast<int>(selectedAction), pendingOrientation, selectedPageTurnOption});
if (selectedAction == MenuAction::EMBEDDED_STYLE) {
// Cycle per-book override: default -> ON -> OFF -> default.
if (pendingEmbeddedStyleOverride < 0) {
pendingEmbeddedStyleOverride = 1;
} else if (pendingEmbeddedStyleOverride > 0) {
pendingEmbeddedStyleOverride = 0;
} else {
pendingEmbeddedStyleOverride = -1;
}
requestUpdate();
return;
}
if (selectedAction == MenuAction::IMAGE_RENDERING) {
// Cycle per-book override: default -> display -> placeholder -> suppress -> default.
if (pendingImageRenderingOverride < 0) {
pendingImageRenderingOverride = 0;
} else if (pendingImageRenderingOverride >= 2) {
pendingImageRenderingOverride = -1;
} else {
pendingImageRenderingOverride++;
}
requestUpdate();
return;
}
setResult(MenuResult{static_cast<int>(selectedAction), pendingOrientation, selectedPageTurnOption,
pendingEmbeddedStyleOverride, pendingImageRenderingOverride});
finish();
return;
} else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
ActivityResult result;
result.isCancelled = true;
result.data = MenuResult{-1, pendingOrientation, selectedPageTurnOption};
result.data = MenuResult{-1, pendingOrientation, selectedPageTurnOption, pendingEmbeddedStyleOverride,
pendingImageRenderingOverride};
setResult(std::move(result));
finish();
return;
@@ -134,6 +167,26 @@ void EpubReaderMenuActivity::render(RenderLock&&) {
const auto width = renderer.getTextWidth(UI_10_FONT_ID, value);
renderer.drawText(UI_10_FONT_ID, contentRect.x + contentRect.width - 20 - width, displayY, value, !isSelected);
}
if (menuItems[i].action == MenuAction::EMBEDDED_STYLE) {
const char* value = tr(STR_DEFAULT_VALUE);
if (pendingEmbeddedStyleOverride == 1) {
value = tr(STR_STATE_ON);
} else if (pendingEmbeddedStyleOverride == 0) {
value = tr(STR_STATE_OFF);
}
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::IMAGE_RENDERING) {
const char* value = tr(STR_DEFAULT_VALUE);
if (pendingImageRenderingOverride >= 0 && pendingImageRenderingOverride < imageRenderingLabels.size()) {
value = I18N.get(imageRenderingLabels[pendingImageRenderingOverride]);
}
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
@@ -14,6 +14,8 @@ class EpubReaderMenuActivity final : public Activity {
enum class MenuAction {
SELECT_CHAPTER,
FOOTNOTES,
EMBEDDED_STYLE,
IMAGE_RENDERING,
GO_TO_PERCENT,
AUTO_PAGE_TURN,
ROTATE_SCREEN,
@@ -26,7 +28,9 @@ class EpubReaderMenuActivity final : public Activity {
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 hasFootnotes);
const uint8_t currentOrientation, const bool hasFootnotes,
const int8_t initialEmbeddedStyleOverride,
const int8_t initialImageRenderingOverride);
void onEnter() override;
void onExit() override;
@@ -50,8 +54,12 @@ class EpubReaderMenuActivity final : public Activity {
std::string title = "Reader Menu";
uint8_t pendingOrientation = 0;
uint8_t selectedPageTurnOption = 0;
int8_t pendingEmbeddedStyleOverride = -1;
int8_t pendingImageRenderingOverride = -1;
const std::vector<StrId> orientationLabels = {StrId::STR_PORTRAIT, StrId::STR_LANDSCAPE_CW, StrId::STR_INVERTED,
StrId::STR_LANDSCAPE_CCW};
const std::vector<StrId> imageRenderingLabels = {StrId::STR_IMAGES_DISPLAY, StrId::STR_IMAGES_PLACEHOLDER,
StrId::STR_IMAGES_SUPPRESS};
const std::vector<const char*> pageTurnLabels = {I18N.get(StrId::STR_STATE_OFF), "1", "3", "6", "12"};
int currentPage = 0;
int totalPages = 0;