Merge pull request #156 from jpirnay/feat-bionic

feat: Add yet another bionic reader implementation
This commit is contained in:
jpirnay
2026-04-30 15:11:10 +02:00
committed by GitHub
17 changed files with 285 additions and 54 deletions
+3
View File
@@ -249,6 +249,8 @@ class CrossPointSettings {
uint8_t imageDithering = IMAGE_DITHER_BAYER;
// Enable synthetic TOC fallback for malformed/sparse TOC books (1 = enabled, 0 = disabled)
uint8_t syntheticTocFallback = 1;
// Default bionic reading in EPUB pages when no per-book override is set (1 = enabled, 0 = disabled)
uint8_t bionicReading = 0;
// Automatically push reading progress to the KOReader sync server when leaving the reader
// (1 = enabled, 0 = disabled). The push only fires when credentials are configured and the
// reader session advanced at least 3 pages, and is skipped when remote progress is already ahead.
@@ -285,6 +287,7 @@ class CrossPointSettings {
BTN_EXIT_READER,
BTN_READER_MENU,
BTN_KOREADER_SYNC,
BTN_TOGGLE_BIONIC_READING,
BUTTON_ACTION_COUNT
};
+2
View File
@@ -418,6 +418,7 @@ bool JsonSettingsIO::saveRecentBooks(const RecentBooksStore& store, const char*
obj["imageRenderingOverride"] = book.imageRenderingOverride;
obj["fontFamilyOverride"] = book.fontFamilyOverride;
obj["fontSizeOverride"] = book.fontSizeOverride;
obj["bionicReadingOverride"] = book.bionicReadingOverride;
}
String json;
@@ -455,6 +456,7 @@ bool JsonSettingsIO::loadRecentBooks(RecentBooksStore& store, const char* json)
book.fontFamilyOverride =
clampInt8(obj["fontFamilyOverride"] | -1, -1, CrossPointSettings::FONT_FAMILY_COUNT - 1, -1);
book.fontSizeOverride = clampInt8(obj["fontSizeOverride"] | -1, -1, CrossPointSettings::FONT_SIZE_COUNT - 1, -1);
book.bionicReadingOverride = clampInt8(obj["bionicReadingOverride"] | -1, -1, 1, -1);
store.recentBooks.push_back(book);
}
+30 -3
View File
@@ -26,6 +26,7 @@ void RecentBooksStore::addBook(const std::string& path, const std::string& title
int8_t imageRenderingOverride = -1;
int8_t fontFamilyOverride = -1;
int8_t fontSizeOverride = -1;
int8_t bionicReadingOverride = -1;
// Remove existing entry if present
auto it =
@@ -35,12 +36,14 @@ void RecentBooksStore::addBook(const std::string& path, const std::string& title
imageRenderingOverride = it->imageRenderingOverride;
fontFamilyOverride = it->fontFamilyOverride;
fontSizeOverride = it->fontSizeOverride;
bionicReadingOverride = it->bionicReadingOverride;
recentBooks.erase(it);
}
// Add to front
recentBooks.insert(recentBooks.begin(), {path, title, author, series, coverBmpPath, embeddedStyleOverride,
imageRenderingOverride, fontFamilyOverride, fontSizeOverride});
recentBooks.insert(recentBooks.begin(),
{path, title, author, series, coverBmpPath, embeddedStyleOverride, imageRenderingOverride,
fontFamilyOverride, fontSizeOverride, bionicReadingOverride});
// Trim to max size
if (recentBooks.size() > MAX_RECENT_BOOKS) {
@@ -90,7 +93,7 @@ bool RecentBooksStore::setReaderOverrides(const std::string& path, const int8_t
return false;
}
return setReaderOverrides(path, embeddedStyleOverride, imageRenderingOverride, it->fontFamilyOverride,
it->fontSizeOverride);
it->fontSizeOverride, it->bionicReadingOverride);
}
bool RecentBooksStore::setReaderOverrides(const std::string& path, const int8_t embeddedStyleOverride,
@@ -101,11 +104,35 @@ bool RecentBooksStore::setReaderOverrides(const std::string& path, const int8_t
if (it == recentBooks.end()) {
return false;
}
return setReaderOverrides(path, embeddedStyleOverride, imageRenderingOverride, fontFamilyOverride, fontSizeOverride,
it->bionicReadingOverride);
}
bool RecentBooksStore::setReaderOverrides(const std::string& path, const int8_t embeddedStyleOverride,
const int8_t imageRenderingOverride, const bool bionicReadingOverride) {
auto it =
std::find_if(recentBooks.begin(), recentBooks.end(), [&](const RecentBook& book) { return book.path == path; });
if (it == recentBooks.end()) {
return false;
}
return setReaderOverrides(path, embeddedStyleOverride, imageRenderingOverride, it->fontFamilyOverride,
it->fontSizeOverride, bionicReadingOverride);
}
bool RecentBooksStore::setReaderOverrides(const std::string& path, const int8_t embeddedStyleOverride,
const int8_t imageRenderingOverride, const int8_t fontFamilyOverride,
const int8_t fontSizeOverride, const bool bionicReadingOverride) {
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;
it->fontFamilyOverride = fontFamilyOverride;
it->fontSizeOverride = fontSizeOverride;
it->bionicReadingOverride = bionicReadingOverride;
return saveToFile();
}
+6
View File
@@ -17,6 +17,8 @@ struct RecentBook {
int8_t fontFamilyOverride = -1;
// -1 = use global setting, otherwise CrossPointSettings::FONT_SIZE value.
int8_t fontSizeOverride = -1;
// -1 = use global default, otherwise explicit per-book override (0 = off, 1 = on).
int8_t bionicReadingOverride = -1;
bool operator==(const RecentBook& other) const { return path == other.path; }
};
@@ -64,6 +66,10 @@ class RecentBooksStore {
bool setReaderOverrides(const std::string& path, int8_t embeddedStyleOverride, int8_t imageRenderingOverride);
bool setReaderOverrides(const std::string& path, int8_t embeddedStyleOverride, int8_t imageRenderingOverride,
int8_t fontFamilyOverride, int8_t fontSizeOverride);
bool setReaderOverrides(const std::string& path, int8_t embeddedStyleOverride, int8_t imageRenderingOverride,
bool bionicReadingOverride);
bool setReaderOverrides(const std::string& path, int8_t embeddedStyleOverride, int8_t imageRenderingOverride,
int8_t fontFamilyOverride, int8_t fontSizeOverride, bool bionicReadingOverride);
private:
bool loadFromBinaryFile();
+3 -1
View File
@@ -117,6 +117,8 @@ inline const std::vector<SettingInfo> list = {
StrId::STR_CAT_READER),
SettingInfo::Toggle(StrId::STR_HYPHENATION, &CrossPointSettings::hyphenationEnabled, "hyphenationEnabled",
StrId::STR_CAT_READER),
SettingInfo::Toggle(StrId::STR_BIONIC_READING, &CrossPointSettings::bionicReading, "bionicReading",
StrId::STR_CAT_READER),
SettingInfo::Enum(StrId::STR_IMAGES, &CrossPointSettings::imageRendering,
{StrId::STR_IMAGES_DISPLAY, StrId::STR_IMAGES_PLACEHOLDER, StrId::STR_IMAGES_SUPPRESS},
"imageRendering", StrId::STR_CAT_READER),
@@ -145,7 +147,7 @@ inline const std::vector<SettingInfo> list = {
StrId::STR_BTN_ACT_FORCE_REFRESH, StrId::STR_BTN_ACT_OPEN_TOC, StrId::STR_BTN_ACT_OPEN_BOOKMARKS, \
StrId::STR_BTN_ACT_STAR_PAGE, StrId::STR_BTN_ACT_FOOTNOTES, StrId::STR_BTN_ACT_NEXT_SECTION, \
StrId::STR_BTN_ACT_PREV_SECTION, StrId::STR_BTN_ACT_EXIT_READER, StrId::STR_BTN_ACT_READER_MENU, \
StrId::STR_BTN_ACT_KOREADER_SYNC
StrId::STR_BTN_ACT_TOGGLE_BIONIC_READING, StrId::STR_BTN_ACT_KOREADER_SYNC
// Back button: short=exit reader, double=ignore, long=go home
SettingInfo::Enum(StrId::STR_BTN_SHORT_PRESS, &CrossPointSettings::btnShortBack, {StrId::STR_BTN_DEF_EXIT_READER},
+1
View File
@@ -28,6 +28,7 @@ struct MenuResult {
int8_t fontFamilyOverride = -1;
int8_t fontSizeOverride = -1;
uint8_t textDarkness = 1;
uint8_t bionicReadingOverride = 0;
};
struct ChapterResult {
+29 -12
View File
@@ -191,6 +191,9 @@ void EpubReaderActivity::onEnter() {
bookImageRenderingOverride = currentBook.imageRenderingOverride;
bookFontFamilyOverride = currentBook.fontFamilyOverride;
bookFontSizeOverride = currentBook.fontSizeOverride;
bookBionicReadingOverride = (currentBook.bionicReadingOverride >= 0)
? static_cast<bool>(currentBook.bionicReadingOverride)
: static_cast<bool>(SETTINGS.bionicReading);
logReaderMemSnapshot("onEnter_after_recent_books");
// Trigger first update
@@ -1033,13 +1036,14 @@ void EpubReaderActivity::toggleAutoPageTurn(const uint8_t selectedPageTurnOption
void EpubReaderActivity::applyBookReaderOverrides(const int8_t embeddedStyleOverride,
const int8_t imageRenderingOverride, const int8_t fontFamilyOverride,
const int8_t fontSizeOverride) {
const int8_t fontSizeOverride, const bool bionicReadingOverride) {
if (!epub) {
return;
}
if (bookEmbeddedStyleOverride == embeddedStyleOverride && bookImageRenderingOverride == imageRenderingOverride &&
bookFontFamilyOverride == fontFamilyOverride && bookFontSizeOverride == fontSizeOverride) {
bookFontFamilyOverride == fontFamilyOverride && bookFontSizeOverride == fontSizeOverride &&
bookBionicReadingOverride == bionicReadingOverride) {
return;
}
@@ -1047,8 +1051,9 @@ void EpubReaderActivity::applyBookReaderOverrides(const int8_t embeddedStyleOver
bookImageRenderingOverride = imageRenderingOverride;
bookFontFamilyOverride = fontFamilyOverride;
bookFontSizeOverride = fontSizeOverride;
bookBionicReadingOverride = bionicReadingOverride;
RECENT_BOOKS.setReaderOverrides(epub->getPath(), bookEmbeddedStyleOverride, bookImageRenderingOverride,
bookFontFamilyOverride, bookFontSizeOverride);
bookFontFamilyOverride, bookFontSizeOverride, bookBionicReadingOverride);
RenderLock lock(*this);
if (section) {
@@ -1251,7 +1256,8 @@ void EpubReaderActivity::render(RenderLock&& lock) {
if (!section->loadSectionFile(getEffectiveReaderFontId(), getEffectiveReaderLineCompression(),
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
viewportHeight, SETTINGS.hyphenationEnabled, embeddedStyle, imageRendering)) {
viewportHeight, SETTINGS.hyphenationEnabled, embeddedStyle, bookBionicReadingOverride,
imageRendering)) {
LOG_DBG("ERS", "Cache not found, building...");
lastRenderStats.cacheRebuilt = true;
@@ -1272,8 +1278,8 @@ void EpubReaderActivity::render(RenderLock&& lock) {
renderer.clearSdCardFontAccumulation();
if (!section->createSectionFile(getEffectiveReaderFontId(), getEffectiveReaderLineCompression(),
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
viewportHeight, SETTINGS.hyphenationEnabled, embeddedStyle, imageRendering,
progressFn)) {
viewportHeight, SETTINGS.hyphenationEnabled, embeddedStyle,
bookBionicReadingOverride, imageRendering, progressFn)) {
LOG_ERR("ERS", "Failed to persist page data to SD");
section.reset();
return;
@@ -1427,7 +1433,8 @@ void EpubReaderActivity::silentIndexNextChapterIfNeeded(const uint16_t viewportW
Section nextSection(epub, nextSpineIndex, renderer);
if (nextSection.loadSectionFile(getEffectiveReaderFontId(), getEffectiveReaderLineCompression(),
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
viewportHeight, SETTINGS.hyphenationEnabled, embeddedStyle, imageRendering)) {
viewportHeight, SETTINGS.hyphenationEnabled, embeddedStyle, bookBionicReadingOverride,
imageRendering)) {
return;
}
@@ -1436,7 +1443,8 @@ void EpubReaderActivity::silentIndexNextChapterIfNeeded(const uint16_t viewportW
renderer.clearSdCardFontAccumulation();
if (!nextSection.createSectionFile(getEffectiveReaderFontId(), getEffectiveReaderLineCompression(),
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
viewportHeight, SETTINGS.hyphenationEnabled, embeddedStyle, imageRendering)) {
viewportHeight, SETTINGS.hyphenationEnabled, embeddedStyle,
bookBionicReadingOverride, imageRendering)) {
LOG_ERR("ERS", "Failed silent indexing for chapter: %d", nextSpineIndex);
}
}
@@ -1819,12 +1827,13 @@ bool EpubReaderActivity::drawCurrentPageToBuffer(const std::string& filePath, Gf
if (!section->loadSectionFile(getEffectiveFontId(effectiveFontFamily, effectiveFontSize), effectiveLineCompression,
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle,
SETTINGS.imageRendering)) {
static_cast<bool>(SETTINGS.bionicReading), SETTINGS.imageRendering)) {
LOG_DBG("SLP", "EPUB: section cache not found for spine %d, rebuilding", spineIndex);
if (!section->createSectionFile(getEffectiveFontId(effectiveFontFamily, effectiveFontSize),
effectiveLineCompression, SETTINGS.extraParagraphSpacing,
SETTINGS.paragraphAlignment, viewportWidth, viewportHeight,
SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle, SETTINGS.imageRendering)) {
SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle,
static_cast<bool>(SETTINGS.bionicReading), SETTINGS.imageRendering)) {
LOG_ERR("SLP", "EPUB: failed to rebuild section cache for spine %d", spineIndex);
return false;
}
@@ -1861,14 +1870,15 @@ void EpubReaderActivity::openReaderMenu() {
std::make_unique<EpubReaderMenuActivity>(
renderer, mappedInput, epub->getTitle(), currentPage, totalPages, bookProgressPercent, SETTINGS.orientation,
!currentPageFootnotes.empty(), bookEmbeddedStyleOverride, bookImageRenderingOverride, bookFontFamilyOverride,
bookFontSizeOverride, SETTINGS.textDarkness, !bookmarkStore.isEmpty(), isCurrentPageStarred),
bookFontSizeOverride, SETTINGS.textDarkness, bookBionicReadingOverride, !bookmarkStore.isEmpty(),
isCurrentPageStarred),
[this](const ActivityResult& result) {
const auto& menu = std::get<MenuResult>(result.data);
applyOrientation(menu.orientation);
applyTextDarkness(menu.textDarkness);
toggleAutoPageTurn(menu.pageTurnOption);
applyBookReaderOverrides(menu.embeddedStyleOverride, menu.imageRenderingOverride, menu.fontFamilyOverride,
menu.fontSizeOverride);
menu.fontSizeOverride, static_cast<bool>(menu.bionicReadingOverride));
if (!result.isCancelled) {
onReaderMenuConfirm(static_cast<EpubReaderMenuActivity::MenuAction>(menu.action));
}
@@ -1997,6 +2007,13 @@ void EpubReaderActivity::onButtonAction(const CrossPointSettings::BUTTON_ACTION
openReaderMenu();
}
break;
case BA::BTN_TOGGLE_BIONIC_READING:
if (epub) {
applyBookReaderOverrides(bookEmbeddedStyleOverride, bookImageRenderingOverride, bookFontFamilyOverride,
bookFontSizeOverride, !bookBionicReadingOverride);
requestUpdate();
}
break;
case BA::BTN_KOREADER_SYNC:
launchKOReaderSync(SyncLaunchMode::COMPARE);
break;
+2 -1
View File
@@ -135,6 +135,7 @@ class EpubReaderActivity final : public Activity {
int8_t bookImageRenderingOverride = -1;
int8_t bookFontFamilyOverride = -1;
int8_t bookFontSizeOverride = -1;
bool bookBionicReadingOverride = false;
// Bookmarks (starred pages)
BookmarkStore bookmarkStore;
@@ -176,7 +177,7 @@ class EpubReaderActivity final : public Activity {
void applyTextDarkness(uint8_t textDarkness);
void toggleAutoPageTurn(uint8_t selectedPageTurnOption);
void applyBookReaderOverrides(int8_t embeddedStyleOverride, int8_t imageRenderingOverride, int8_t fontFamilyOverride,
int8_t fontSizeOverride);
int8_t fontSizeOverride, bool bionicReadingOverride);
void openReaderMenu();
bool getEffectiveEmbeddedStyle() const;
uint8_t getEffectiveImageRendering() const;
@@ -30,14 +30,12 @@ std::string defaultFontFamilyLabel(const SettingInfo& item) {
}
} // namespace
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 int8_t initialEmbeddedStyleOverride,
const int8_t initialImageRenderingOverride,
const int8_t initialFontFamilyOverride,
const int8_t initialFontSizeOverride, const uint8_t initialTextDarkness,
const bool hasStarredPages, const bool isCurrentPageStarred)
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 int8_t initialEmbeddedStyleOverride, const int8_t initialImageRenderingOverride,
const int8_t initialFontFamilyOverride, const int8_t initialFontSizeOverride, const uint8_t initialTextDarkness,
const bool initialBionicReadingOverride, const bool hasStarredPages, const bool isCurrentPageStarred)
: MenuListActivity("EpubReaderMenu", renderer, mappedInput),
currentPageStarred(isCurrentPageStarred),
pendingOrientation(currentOrientation),
@@ -46,6 +44,7 @@ EpubReaderMenuActivity::EpubReaderMenuActivity(GfxRenderer& renderer, MappedInpu
pendingFontFamilyOverride(initialFontFamilyOverride),
pendingFontSizeOverride(initialFontSizeOverride),
pendingTextDarkness(initialTextDarkness),
pendingBionicReading(initialBionicReadingOverride),
title(title),
currentPage(currentPage),
totalPages(totalPages),
@@ -161,6 +160,15 @@ void EpubReaderMenuActivity::buildMenuItems(bool hasFootnotes, bool hasStarredPa
[](void* ctx, uint8_t v) { static_cast<EpubReaderMenuActivity*>(ctx)->pendingTextDarkness = v; })
.withSubmenu(StrId::STR_READER_OVERRIDES));
menuItems.push_back(
SettingInfo::DynamicEnumCtx(
StrId::STR_BIONIC_READING, {StrId::STR_STATE_OFF, StrId::STR_STATE_ON}, self,
[](const void* ctx) -> uint8_t {
return static_cast<const EpubReaderMenuActivity*>(ctx)->pendingBionicReading ? 1 : 0;
},
[](void* ctx, uint8_t v) { static_cast<EpubReaderMenuActivity*>(ctx)->pendingBionicReading = (v != 0); })
.withSubmenu(StrId::STR_READER_OVERRIDES));
// Helper functions, reading ruler, auto page turn, orientation
menuItems.push_back(SettingInfo::Separator(StrId::STR_READER_UTILS));
// Auto page turn: ACTION type with custom cycling in onActionSelected
@@ -240,7 +248,7 @@ EpubReaderMenuActivity::MenuAction EpubReaderMenuActivity::actionForSettingActio
void EpubReaderMenuActivity::finishWithAction(MenuAction action) {
setResult(MenuResult{static_cast<int>(action), -1, pendingOrientation, selectedPageTurnOption,
pendingEmbeddedStyleOverride, pendingImageRenderingOverride, pendingFontFamilyOverride,
pendingFontSizeOverride, pendingTextDarkness});
pendingFontSizeOverride, pendingTextDarkness, static_cast<uint8_t>(pendingBionicReading)});
finish();
}
@@ -273,7 +281,8 @@ void EpubReaderMenuActivity::onBackPressed() {
pendingImageRenderingOverride,
pendingFontFamilyOverride,
pendingFontSizeOverride,
pendingTextDarkness};
pendingTextDarkness,
static_cast<uint8_t>(pendingBionicReading)};
setResult(std::move(result));
finish();
}
@@ -37,8 +37,8 @@ class EpubReaderMenuActivity final : public MenuListActivity {
const uint8_t currentOrientation, const bool hasFootnotes,
const int8_t initialEmbeddedStyleOverride, const int8_t initialImageRenderingOverride,
const int8_t initialFontFamilyOverride, const int8_t initialFontSizeOverride,
const uint8_t initialTextDarkness, const bool hasStarredPages,
const bool isCurrentPageStarred);
const uint8_t initialTextDarkness, const bool initialBionicReadingOverride,
const bool hasStarredPages, const bool isCurrentPageStarred);
void onEnter() override;
void render(RenderLock&&) override;
@@ -69,6 +69,7 @@ class EpubReaderMenuActivity final : public MenuListActivity {
int8_t pendingFontFamilyOverride = -1;
int8_t pendingFontSizeOverride = -1;
uint8_t pendingTextDarkness = 1;
bool pendingBionicReading = false;
static constexpr const char* pageTurnLabels[] = {"", "1", "3", "6", "12"};