Merge pull request #163 from jpirnay/feat-sdfonts-ui

feat: Complete sdfonts integration
This commit is contained in:
jpirnay
2026-05-02 21:08:07 +02:00
committed by GitHub
151 changed files with 49581 additions and 303 deletions
+1
View File
@@ -26,6 +26,7 @@ struct MenuResult {
int8_t embeddedStyleOverride = -1;
int8_t imageRenderingOverride = -1;
int8_t fontFamilyOverride = -1;
std::string sdFontFamilyOverride;
int8_t fontSizeOverride = -1;
uint8_t textDarkness = 1;
uint8_t bionicReadingOverride = 0;
+50 -74
View File
@@ -190,6 +190,7 @@ void EpubReaderActivity::onEnter() {
bookEmbeddedStyleOverride = currentBook.embeddedStyleOverride;
bookImageRenderingOverride = currentBook.imageRenderingOverride;
bookFontFamilyOverride = currentBook.fontFamilyOverride;
bookSdFontFamilyOverride = currentBook.sdFontFamilyOverride;
bookFontSizeOverride = currentBook.fontSizeOverride;
bookBionicReadingOverride = (currentBook.bionicReadingOverride >= 0)
? static_cast<bool>(currentBook.bionicReadingOverride)
@@ -1036,24 +1037,37 @@ void EpubReaderActivity::toggleAutoPageTurn(const uint8_t selectedPageTurnOption
void EpubReaderActivity::applyBookReaderOverrides(const int8_t embeddedStyleOverride,
const int8_t imageRenderingOverride, const int8_t fontFamilyOverride,
const std::string& sdFontFamilyOverride,
const int8_t fontSizeOverride, const bool bionicReadingOverride) {
if (!epub) {
return;
}
// Built-in and SD font overrides are mutually exclusive; explicit built-in wins.
int8_t normalizedFontFamilyOverride = fontFamilyOverride;
std::string normalizedSdFontFamilyOverride = sdFontFamilyOverride;
if (normalizedFontFamilyOverride >= 0) {
normalizedSdFontFamilyOverride.clear();
} else if (!normalizedSdFontFamilyOverride.empty()) {
normalizedFontFamilyOverride = -1;
}
if (bookEmbeddedStyleOverride == embeddedStyleOverride && bookImageRenderingOverride == imageRenderingOverride &&
bookFontFamilyOverride == fontFamilyOverride && bookFontSizeOverride == fontSizeOverride &&
bookFontFamilyOverride == normalizedFontFamilyOverride &&
bookSdFontFamilyOverride == normalizedSdFontFamilyOverride && bookFontSizeOverride == fontSizeOverride &&
bookBionicReadingOverride == bionicReadingOverride) {
return;
}
bookEmbeddedStyleOverride = embeddedStyleOverride;
bookImageRenderingOverride = imageRenderingOverride;
bookFontFamilyOverride = fontFamilyOverride;
bookFontFamilyOverride = normalizedFontFamilyOverride;
bookSdFontFamilyOverride = normalizedSdFontFamilyOverride;
bookFontSizeOverride = fontSizeOverride;
bookBionicReadingOverride = bionicReadingOverride;
RECENT_BOOKS.setReaderOverrides(epub->getPath(), bookEmbeddedStyleOverride, bookImageRenderingOverride,
bookFontFamilyOverride, bookFontSizeOverride, bookBionicReadingOverride);
bookFontFamilyOverride, bookSdFontFamilyOverride, bookFontSizeOverride,
bookBionicReadingOverride);
RenderLock lock(*this);
if (section) {
@@ -1081,9 +1095,7 @@ uint8_t EpubReaderActivity::getEffectiveImageRendering() const {
float EpubReaderActivity::getEffectiveReaderLineCompression() const {
const uint8_t fontSize = (bookFontSizeOverride >= 0) ? static_cast<uint8_t>(bookFontSizeOverride) : SETTINGS.fontSize;
const int effectiveFontId = getEffectiveReaderFontId();
const int bookerlyId = CrossPointSettings::getBuiltinReaderFontId(CrossPointSettings::BOOKERLY, fontSize);
const int notosansId = CrossPointSettings::getBuiltinReaderFontId(CrossPointSettings::NOTOSANS, fontSize);
const int opendyslexicId = CrossPointSettings::getBuiltinReaderFontId(CrossPointSettings::OPENDYSLEXIC, fontSize);
if (effectiveFontId == notosansId) {
switch (SETTINGS.lineSpacing) {
@@ -1097,18 +1109,6 @@ float EpubReaderActivity::getEffectiveReaderLineCompression() const {
}
}
if (effectiveFontId == opendyslexicId) {
switch (SETTINGS.lineSpacing) {
case CrossPointSettings::TIGHT:
return 0.90f;
case CrossPointSettings::NORMAL:
default:
return 0.95f;
case CrossPointSettings::WIDE:
return 1.0f;
}
}
switch (SETTINGS.lineSpacing) {
case CrossPointSettings::TIGHT:
return 0.95f;
@@ -1129,6 +1129,10 @@ int EpubReaderActivity::getEffectiveReaderFontId() const {
if (bookFontFamilyOverride >= 0) {
return CrossPointSettings::getBuiltinReaderFontId(static_cast<uint8_t>(bookFontFamilyOverride), fontSize);
}
if (!bookSdFontFamilyOverride.empty()) {
const int id = resolveSdCardFontId(bookSdFontFamilyOverride.c_str(), fontSize);
if (id != 0) return id;
}
// No override: defer to global resolution (which honors SD card font selection).
// We synthesize a temporary lookup using the override fontSize if it's set; otherwise
// SETTINGS.getReaderFontId() is the canonical answer.
@@ -1747,59 +1751,31 @@ bool EpubReaderActivity::drawCurrentPageToBuffer(const std::string& filePath, Gf
// Load or rebuild the section cache. Rebuilding is needed when the cache is missing or stale
// (e.g. after a firmware update). A no-op popup callback avoids any UI during sleep preparation.
const RecentBook currentBook = RECENT_BOOKS.getBookByPath(filePath);
const bool hasLocalSdOverride = !currentBook.sdFontFamilyOverride.empty();
const uint8_t effectiveFontFamily =
currentBook.fontFamilyOverride >= 0 ? static_cast<uint8_t>(currentBook.fontFamilyOverride) : SETTINGS.fontFamily;
const uint8_t effectiveFontSize =
currentBook.fontSizeOverride >= 0 ? static_cast<uint8_t>(currentBook.fontSizeOverride) : SETTINGS.fontSize;
auto getEffectiveFontId = [&](uint8_t family, uint8_t size) {
switch (family) {
case CrossPointSettings::NOTOSANS:
switch (size) {
case CrossPointSettings::SMALL:
return NOTOSANS_12_FONT_ID;
case CrossPointSettings::LARGE:
return NOTOSANS_16_FONT_ID;
case CrossPointSettings::EXTRA_LARGE:
return NOTOSANS_18_FONT_ID;
case CrossPointSettings::MEDIUM:
default:
return NOTOSANS_14_FONT_ID;
}
case CrossPointSettings::OPENDYSLEXIC:
switch (size) {
case CrossPointSettings::SMALL:
return OPENDYSLEXIC_8_FONT_ID;
case CrossPointSettings::LARGE:
return OPENDYSLEXIC_12_FONT_ID;
case CrossPointSettings::EXTRA_LARGE:
return OPENDYSLEXIC_14_FONT_ID;
case CrossPointSettings::MEDIUM:
default:
return OPENDYSLEXIC_10_FONT_ID;
}
case CrossPointSettings::BOOKERLY:
default:
switch (size) {
case CrossPointSettings::SMALL:
return BOOKERLY_12_FONT_ID;
case CrossPointSettings::LARGE:
return BOOKERLY_16_FONT_ID;
case CrossPointSettings::EXTRA_LARGE:
return BOOKERLY_18_FONT_ID;
case CrossPointSettings::MEDIUM:
default:
return BOOKERLY_14_FONT_ID;
}
}
};
const int effectiveFontId = getEffectiveFontId(effectiveFontFamily, effectiveFontSize);
int effectiveFontId = 0;
if (hasLocalSdOverride) {
effectiveFontId = resolveSdCardFontId(currentBook.sdFontFamilyOverride.c_str(), effectiveFontSize);
}
if (effectiveFontId == 0 && currentBook.fontFamilyOverride >= 0) {
effectiveFontId = CrossPointSettings::getBuiltinReaderFontId(effectiveFontFamily, effectiveFontSize);
}
if (effectiveFontId == 0 && currentBook.fontSizeOverride >= 0 && SETTINGS.sdFontFamilyName[0] != '\0') {
effectiveFontId = resolveSdCardFontId(SETTINGS.sdFontFamilyName, effectiveFontSize);
}
if (effectiveFontId == 0 && currentBook.fontSizeOverride >= 0) {
effectiveFontId = CrossPointSettings::getBuiltinReaderFontId(SETTINGS.fontFamily, effectiveFontSize);
}
if (effectiveFontId == 0) {
effectiveFontId = SETTINGS.getReaderFontId();
}
const auto getEffectiveLineCompression = [&](int fontId) {
const int notosansId = CrossPointSettings::getBuiltinReaderFontId(CrossPointSettings::NOTOSANS, effectiveFontSize);
const int opendyslexicId =
CrossPointSettings::getBuiltinReaderFontId(CrossPointSettings::OPENDYSLEXIC, effectiveFontSize);
if (fontId == notosansId || fontId == opendyslexicId) {
if (fontId == notosansId) {
switch (SETTINGS.lineSpacing) {
case CrossPointSettings::TIGHT:
return 0.90f;
@@ -1824,13 +1800,12 @@ bool EpubReaderActivity::drawCurrentPageToBuffer(const std::string& filePath, Gf
const float effectiveLineCompression = getEffectiveLineCompression(effectiveFontId);
auto section = std::make_unique<Section>(epub, spineIndex, renderer);
if (!section->loadSectionFile(getEffectiveFontId(effectiveFontFamily, effectiveFontSize), effectiveLineCompression,
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle,
static_cast<bool>(SETTINGS.bionicReading), SETTINGS.imageRendering)) {
if (!section->loadSectionFile(effectiveFontId, effectiveLineCompression, SETTINGS.extraParagraphSpacing,
SETTINGS.paragraphAlignment, viewportWidth, viewportHeight, SETTINGS.hyphenationEnabled,
SETTINGS.embeddedStyle, 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,
if (!section->createSectionFile(effectiveFontId, effectiveLineCompression, SETTINGS.extraParagraphSpacing,
SETTINGS.paragraphAlignment, viewportWidth, viewportHeight,
SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle,
static_cast<bool>(SETTINGS.bionicReading), SETTINGS.imageRendering)) {
@@ -1849,7 +1824,7 @@ bool EpubReaderActivity::drawCurrentPageToBuffer(const std::string& filePath, Gf
}
renderer.clearScreen();
page->render(renderer, getEffectiveFontId(effectiveFontFamily, effectiveFontSize), marginLeft, marginTop);
page->render(renderer, effectiveFontId, marginLeft, marginTop);
// No displayBuffer call — caller (SleepActivity) handles that after compositing the overlay
return true;
}
@@ -1870,15 +1845,16 @@ void EpubReaderActivity::openReaderMenu() {
std::make_unique<EpubReaderMenuActivity>(
renderer, mappedInput, epub->getTitle(), currentPage, totalPages, bookProgressPercent, SETTINGS.orientation,
!currentPageFootnotes.empty(), bookEmbeddedStyleOverride, bookImageRenderingOverride, bookFontFamilyOverride,
bookFontSizeOverride, SETTINGS.textDarkness, bookBionicReadingOverride, !bookmarkStore.isEmpty(),
isCurrentPageStarred),
bookSdFontFamilyOverride, 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, static_cast<bool>(menu.bionicReadingOverride));
menu.sdFontFamilyOverride, menu.fontSizeOverride,
static_cast<bool>(menu.bionicReadingOverride));
if (!result.isCancelled) {
onReaderMenuConfirm(static_cast<EpubReaderMenuActivity::MenuAction>(menu.action));
}
@@ -2010,7 +1986,7 @@ void EpubReaderActivity::onButtonAction(const CrossPointSettings::BUTTON_ACTION
case BA::BTN_TOGGLE_BIONIC_READING:
if (epub) {
applyBookReaderOverrides(bookEmbeddedStyleOverride, bookImageRenderingOverride, bookFontFamilyOverride,
bookFontSizeOverride, !bookBionicReadingOverride);
bookSdFontFamilyOverride, bookFontSizeOverride, !bookBionicReadingOverride);
requestUpdate();
}
break;
+3 -1
View File
@@ -134,6 +134,7 @@ class EpubReaderActivity final : public Activity {
int8_t bookEmbeddedStyleOverride = -1;
int8_t bookImageRenderingOverride = -1;
int8_t bookFontFamilyOverride = -1;
std::string bookSdFontFamilyOverride;
int8_t bookFontSizeOverride = -1;
bool bookBionicReadingOverride = false;
@@ -177,7 +178,8 @@ 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, bool bionicReadingOverride);
const std::string& sdFontFamilyOverride, int8_t fontSizeOverride,
bool bionicReadingOverride);
void openReaderMenu();
bool getEffectiveEmbeddedStyle() const;
uint8_t getEffectiveImageRendering() const;
@@ -17,11 +17,15 @@ namespace {
// though the per-book override list itself is built-in only.
std::string defaultFontFamilyLabel(const SettingInfo& item) {
if (SETTINGS.sdFontFamilyName[0] != '\0') {
return std::string(SETTINGS.sdFontFamilyName);
const auto& families = sdFontSystem.registry().getFamilies();
const auto it = std::find_if(families.begin(), families.end(),
[](const auto& family) { return family.name == SETTINGS.sdFontFamilyName; });
if (it != families.end()) {
return std::string(SETTINGS.sdFontFamilyName);
}
}
// Built-in: enumValues[0] is STR_DEFAULT_VALUE, [1..3] are the three families
// in the same order as CrossPointSettings::FONT_FAMILY (BOOKERLY, NOTOSANS,
// OPENDYSLEXIC).
// Built-in: enumValues[0] is STR_DEFAULT_VALUE, [1..] are built-in families
// in CrossPointSettings::FONT_FAMILY order.
const auto idx = static_cast<size_t>(SETTINGS.fontFamily + 1);
if (idx < item.enumValues.size()) {
return I18N.get(item.enumValues[idx]);
@@ -34,14 +38,16 @@ 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)
const int8_t initialFontFamilyOverride, const std::string& initialSdFontFamilyOverride,
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),
pendingEmbeddedStyleOverride(initialEmbeddedStyleOverride),
pendingImageRenderingOverride(initialImageRenderingOverride),
pendingFontFamilyOverride(initialFontFamilyOverride),
pendingSdFontFamilyOverride(initialSdFontFamilyOverride),
pendingFontSizeOverride(initialFontSizeOverride),
pendingTextDarkness(initialTextDarkness),
pendingBionicReading(initialBionicReadingOverride),
@@ -119,35 +125,79 @@ void EpubReaderMenuActivity::buildMenuItems(bool hasFootnotes, bool hasStarredPa
})
.withSubmenu(StrId::STR_READER_OVERRIDES));
// Reader font family: cycles default(-1) -> Bookerly(0) -> Noto Sans(1) -> Open Dyslexic(2)
menuItems.push_back(
SettingInfo::DynamicEnumCtx(
StrId::STR_FONT_FAMILY,
{StrId::STR_DEFAULT_VALUE, StrId::STR_BOOKERLY, StrId::STR_NOTO_SANS, StrId::STR_OPEN_DYSLEXIC}, self,
[](const void* ctx) -> uint8_t {
const auto* s = static_cast<const EpubReaderMenuActivity*>(ctx);
return (s->pendingFontFamilyOverride < 0) ? 0 : static_cast<uint8_t>(s->pendingFontFamilyOverride + 1);
},
[](void* ctx, uint8_t v) {
auto* s = static_cast<EpubReaderMenuActivity*>(ctx);
s->pendingFontFamilyOverride = (v == 0) ? -1 : static_cast<int8_t>(v - 1);
})
.withSubmenu(StrId::STR_READER_OVERRIDES));
// Reader font family: default + built-ins + discovered SD families.
{
std::vector<StrId> values = {StrId::STR_DEFAULT_VALUE, StrId::STR_BOOKERLY, StrId::STR_NOTO_SANS};
const auto& families = sdFontSystem.registry().getFamilies();
values.insert(values.end(), families.size(), StrId::STR_NONE_OPT);
// Reader font size: cycles default(-1) -> Small(0) -> Medium(1) -> Large(2) -> X Large(3)
menuItems.push_back(
SettingInfo::DynamicEnumCtx(
StrId::STR_FONT_SIZE,
{StrId::STR_DEFAULT_VALUE, StrId::STR_SMALL, StrId::STR_MEDIUM, StrId::STR_LARGE, StrId::STR_X_LARGE}, self,
[](const void* ctx) -> uint8_t {
const auto* s = static_cast<const EpubReaderMenuActivity*>(ctx);
return (s->pendingFontSizeOverride < 0) ? 0 : static_cast<uint8_t>(s->pendingFontSizeOverride + 1);
},
[](void* ctx, uint8_t v) {
auto* s = static_cast<EpubReaderMenuActivity*>(ctx);
s->pendingFontSizeOverride = (v == 0) ? -1 : static_cast<int8_t>(v - 1);
})
.withSubmenu(StrId::STR_READER_OVERRIDES));
auto familySetting = SettingInfo::DynamicEnumCtx(
StrId::STR_FONT_FAMILY, values, self,
[](const void* ctx) -> uint8_t {
const auto* s = static_cast<const EpubReaderMenuActivity*>(ctx);
if (s->pendingFontFamilyOverride >= 0) {
return static_cast<uint8_t>(s->pendingFontFamilyOverride + 1);
}
if (!s->pendingSdFontFamilyOverride.empty()) {
const auto& fs = sdFontSystem.registry().getFamilies();
for (size_t i = 0; i < fs.size(); i++) {
if (fs[i].name == s->pendingSdFontFamilyOverride) {
return static_cast<uint8_t>(CrossPointSettings::BUILTIN_FONT_COUNT + 1 + i);
}
}
}
return 0;
},
[](void* ctx, uint8_t v) {
auto* s = static_cast<EpubReaderMenuActivity*>(ctx);
if (v == 0) {
s->pendingFontFamilyOverride = -1;
s->pendingSdFontFamilyOverride.clear();
return;
}
const uint8_t builtinCount = CrossPointSettings::BUILTIN_FONT_COUNT;
if (v <= builtinCount) {
s->pendingFontFamilyOverride = static_cast<int8_t>(v - 1);
s->pendingSdFontFamilyOverride.clear();
return;
}
const size_t sdIdx = static_cast<size_t>(v - (builtinCount + 1));
const auto& fs = sdFontSystem.registry().getFamilies();
s->pendingFontFamilyOverride = -1;
if (sdIdx < fs.size()) {
s->pendingSdFontFamilyOverride = fs[sdIdx].name;
} else {
s->pendingSdFontFamilyOverride.clear();
}
})
.withSubmenu(StrId::STR_READER_OVERRIDES);
familySetting.enumLabels = {tr(STR_DEFAULT_VALUE), tr(STR_BOOKERLY), tr(STR_NOTO_SANS)};
for (const auto& fam : families) {
familySetting.enumLabels.push_back(fam.name);
}
menuItems.push_back(std::move(familySetting));
}
// Reader font size: cycles default(-1) -> Small(0) -> Medium(1) -> Large(2) -> X Large(3) -> Tiny(4)
menuItems.push_back(SettingInfo::DynamicEnumCtx(
StrId::STR_FONT_SIZE,
{StrId::STR_DEFAULT_VALUE, StrId::STR_SMALL, StrId::STR_MEDIUM, StrId::STR_LARGE,
StrId::STR_X_LARGE, StrId::STR_TINY},
self,
[](const void* ctx) -> uint8_t {
const auto* s = static_cast<const EpubReaderMenuActivity*>(ctx);
return (s->pendingFontSizeOverride < 0)
? 0
: static_cast<uint8_t>(s->pendingFontSizeOverride + 1);
},
[](void* ctx, uint8_t v) {
auto* s = static_cast<EpubReaderMenuActivity*>(ctx);
s->pendingFontSizeOverride = (v == 0) ? -1 : static_cast<int8_t>(v - 1);
})
.withSubmenu(StrId::STR_READER_OVERRIDES));
// Text darkness: straightforward 0-3 cycle
menuItems.push_back(
@@ -248,7 +298,8 @@ EpubReaderMenuActivity::MenuAction EpubReaderMenuActivity::actionForSettingActio
void EpubReaderMenuActivity::finishWithAction(MenuAction action) {
setResult(MenuResult{static_cast<int>(action), -1, pendingOrientation, selectedPageTurnOption,
pendingEmbeddedStyleOverride, pendingImageRenderingOverride, pendingFontFamilyOverride,
pendingFontSizeOverride, pendingTextDarkness, static_cast<uint8_t>(pendingBionicReading)});
pendingSdFontFamilyOverride, pendingFontSizeOverride, pendingTextDarkness,
static_cast<uint8_t>(pendingBionicReading)});
finish();
}
@@ -280,6 +331,7 @@ void EpubReaderMenuActivity::onBackPressed() {
pendingEmbeddedStyleOverride,
pendingImageRenderingOverride,
pendingFontFamilyOverride,
pendingSdFontFamilyOverride,
pendingFontSizeOverride,
pendingTextDarkness,
static_cast<uint8_t>(pendingBionicReading)};
@@ -319,7 +371,7 @@ std::string EpubReaderMenuActivity::getItemValueString(int index) const {
return std::string(tr(STR_DEFAULT_VALUE)) + " (" + I18N.get(item.enumValues[defaultIndex]) + ")";
}
}
if (item.nameId == StrId::STR_FONT_FAMILY && pendingFontFamilyOverride < 0) {
if (item.nameId == StrId::STR_FONT_FAMILY && pendingFontFamilyOverride < 0 && pendingSdFontFamilyOverride.empty()) {
const auto label = defaultFontFamilyLabel(item);
if (!label.empty()) {
return std::string(tr(STR_DEFAULT_VALUE)) + " (" + label + ")";
@@ -353,7 +405,7 @@ void EpubReaderMenuActivity::openSubmenu(const SettingInfo& submenuEntry) {
return std::string(tr(STR_DEFAULT_VALUE)) + " (" + I18N.get(item.enumValues[valueIndex]) + ")";
}
}
if (item.nameId == StrId::STR_FONT_FAMILY && pendingFontFamilyOverride < 0) {
if (item.nameId == StrId::STR_FONT_FAMILY && pendingFontFamilyOverride < 0 && pendingSdFontFamilyOverride.empty()) {
const auto label = defaultFontFamilyLabel(item);
if (!label.empty()) {
return std::string(tr(STR_DEFAULT_VALUE)) + " (" + label + ")";
@@ -36,7 +36,8 @@ class EpubReaderMenuActivity final : public MenuListActivity {
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 int8_t initialFontFamilyOverride,
const std::string& initialSdFontFamilyOverride, const int8_t initialFontSizeOverride,
const uint8_t initialTextDarkness, const bool initialBionicReadingOverride,
const bool hasStarredPages, const bool isCurrentPageStarred);
@@ -67,6 +68,7 @@ class EpubReaderMenuActivity final : public MenuListActivity {
int8_t pendingEmbeddedStyleOverride = -1;
int8_t pendingImageRenderingOverride = -1;
int8_t pendingFontFamilyOverride = -1;
std::string pendingSdFontFamilyOverride;
int8_t pendingFontSizeOverride = -1;
uint8_t pendingTextDarkness = 1;
bool pendingBionicReading = false;
@@ -0,0 +1,457 @@
#include "FontDownloadActivity.h"
#include <ArduinoJson.h>
#include <GfxRenderer.h>
#include <HalStorage.h>
#include <I18n.h>
#include <Logging.h>
#include <WiFi.h>
#include <cstring>
#include "MappedInputManager.h"
#include "SdCardFontGlobals.h"
#include "activities/network/WifiSelectionActivity.h"
#include "components/UITheme.h"
#include "fontIds.h"
#include "network/HttpDownloader.h"
FontDownloadActivity::FontDownloadActivity(GfxRenderer& renderer, MappedInputManager& mappedInput)
: Activity("FontDownload", renderer, mappedInput), fontInstaller_(sdFontSystem.registry()) {}
// --- Lifecycle ---
void FontDownloadActivity::onEnter() {
Activity::onEnter();
WiFi.mode(WIFI_STA);
startActivityForResult(std::make_unique<WifiSelectionActivity>(renderer, mappedInput),
[this](const ActivityResult& result) { onWifiSelectionComplete(!result.isCancelled); });
}
void FontDownloadActivity::onExit() {
Activity::onExit();
WiFi.disconnect(false);
delay(100);
WiFi.mode(WIFI_OFF);
delay(100);
}
void FontDownloadActivity::onWifiSelectionComplete(const bool success) {
if (!success) {
finish();
return;
}
{
RenderLock lock(*this);
state_ = LOADING_MANIFEST;
}
requestUpdateAndWait();
if (!fetchAndParseManifest()) {
RenderLock lock(*this);
state_ = ERROR;
return;
}
{
RenderLock lock(*this);
state_ = FAMILY_LIST;
selectedIndex_ = 0;
}
}
// --- Manifest fetching ---
bool FontDownloadActivity::fetchAndParseManifest() {
static constexpr const char* MANIFEST_TMP = "/fonts_manifest.tmp";
auto result = HttpDownloader::downloadToFile(FONT_MANIFEST_URL, MANIFEST_TMP, nullptr);
if (result != HttpDownloader::OK) {
LOG_ERR("FONT", "Failed to fetch manifest from %s", FONT_MANIFEST_URL);
errorMessage_ = "Failed to fetch font list";
Storage.remove(MANIFEST_TMP);
return false;
}
FsFile manifestFile;
if (!Storage.openFileForRead("FONT", MANIFEST_TMP, manifestFile)) {
LOG_ERR("FONT", "Failed to open temp manifest");
Storage.remove(MANIFEST_TMP);
errorMessage_ = "Failed to read font list";
return false;
}
JsonDocument doc;
DeserializationError err = deserializeJson(doc, manifestFile);
manifestFile.close();
Storage.remove(MANIFEST_TMP);
if (err) {
LOG_ERR("FONT", "Manifest parse error: %s", err.c_str());
errorMessage_ = "Invalid font manifest";
return false;
}
int version = doc["version"] | 0;
if (version != 1) {
LOG_ERR("FONT", "Unsupported manifest version: %d", version);
errorMessage_ = "Unsupported manifest version";
return false;
}
baseUrl_ = doc["baseUrl"] | "";
families_.clear();
JsonArray familiesArr = doc["families"].as<JsonArray>();
families_.reserve(familiesArr.size());
for (JsonObject fObj : familiesArr) {
ManifestFamily family;
family.name = fObj["name"] | "";
family.description = fObj["description"] | "";
for (JsonVariant s : fObj["styles"].as<JsonArray>()) {
family.styles.push_back(s.as<std::string>());
}
family.totalSize = 0;
for (JsonObject fileObj : fObj["files"].as<JsonArray>()) {
ManifestFile file;
file.name = fileObj["name"] | "";
file.size = fileObj["size"] | 0;
family.totalSize += file.size;
family.files.push_back(std::move(file));
}
family.installed = fontInstaller_.isFamilyInstalled(family.name.c_str());
if (family.installed) {
for (const auto& file : family.files) {
char path[128];
FontInstaller::buildFontPath(family.name.c_str(), file.name.c_str(), path, sizeof(path));
FsFile f;
if (Storage.openFileForRead("FONT", path, f)) {
size_t actual = f.fileSize();
f.close();
if (actual != file.size) {
family.hasUpdate = true;
break;
}
} else {
family.hasUpdate = true;
break;
}
}
}
families_.push_back(std::move(family));
}
LOG_DBG("FONT", "Manifest loaded: %zu families", families_.size());
return true;
}
// --- Download ---
void FontDownloadActivity::downloadAll() {
for (size_t i = 0; i < families_.size(); i++) {
if (families_[i].installed && !families_[i].hasUpdate) continue;
downloadFamily(families_[i]);
if (state_ == ERROR) return;
}
RenderLock lock(*this);
state_ = COMPLETE;
}
size_t FontDownloadActivity::totalUninstalledSize() const {
size_t total = 0;
for (const auto& f : families_) {
if (!f.installed || f.hasUpdate) total += f.totalSize;
}
return total;
}
void FontDownloadActivity::downloadFamily(ManifestFamily& family) {
{
RenderLock lock(*this);
state_ = DOWNLOADING;
downloadingFamilyIndex_ = static_cast<int>(&family - families_.data());
currentFileIndex_ = 0;
currentFileTotal_ = family.files.size();
fileProgress_ = 0;
fileTotal_ = 0;
}
requestUpdateAndWait();
char liveDir[128];
char stagingDir[128];
char backupDir[128];
snprintf(liveDir, sizeof(liveDir), "%s/%s", SdCardFontRegistry::FONTS_DIR, family.name.c_str());
snprintf(stagingDir, sizeof(stagingDir), "%s/%s__staging", SdCardFontRegistry::FONTS_DIR, family.name.c_str());
snprintf(backupDir, sizeof(backupDir), "%s/%s__backup", SdCardFontRegistry::FONTS_DIR, family.name.c_str());
if (Storage.exists(stagingDir) && !Storage.removeDir(stagingDir)) {
LOG_ERR("FONT", "Failed to clean staging dir: %s", stagingDir);
RenderLock lock(*this);
state_ = ERROR;
errorMessage_ = "Failed to prepare staging area";
return;
}
if (!Storage.mkdir(stagingDir)) {
LOG_ERR("FONT", "Failed to create staging dir: %s", stagingDir);
RenderLock lock(*this);
state_ = ERROR;
errorMessage_ = "Failed to create staging area";
return;
}
for (size_t i = 0; i < family.files.size(); i++) {
const auto& file = family.files[i];
{
RenderLock lock(*this);
currentFileIndex_ = i;
fileProgress_ = 0;
fileTotal_ = file.size;
}
requestUpdateAndWait();
char stagedPath[128];
snprintf(stagedPath, sizeof(stagedPath), "%s/%s", stagingDir, file.name.c_str());
std::string url = baseUrl_ + file.name;
auto result = HttpDownloader::downloadToFile(url, stagedPath, [this](size_t downloaded, size_t total) {
fileProgress_ = downloaded;
fileTotal_ = total;
requestUpdate(true);
});
if (result != HttpDownloader::OK) {
LOG_ERR("FONT", "Download failed: %s (%d)", file.name.c_str(), result);
Storage.removeDir(stagingDir);
RenderLock lock(*this);
state_ = ERROR;
errorMessage_ = "Download failed: " + file.name;
return;
}
if (!fontInstaller_.validateCpfontFile(stagedPath)) {
LOG_ERR("FONT", "Invalid .cpfont: %s", stagedPath);
Storage.removeDir(stagingDir);
RenderLock lock(*this);
state_ = ERROR;
errorMessage_ = "Invalid font file: " + file.name;
return;
}
}
const bool hadLiveDir = Storage.exists(liveDir);
if (Storage.exists(backupDir) && !Storage.removeDir(backupDir)) {
LOG_ERR("FONT", "Failed to clean backup dir: %s", backupDir);
Storage.removeDir(stagingDir);
RenderLock lock(*this);
state_ = ERROR;
errorMessage_ = "Failed to prepare backup area";
return;
}
if (hadLiveDir && !Storage.rename(liveDir, backupDir)) {
LOG_ERR("FONT", "Failed to move live family to backup: %s", liveDir);
Storage.removeDir(stagingDir);
RenderLock lock(*this);
state_ = ERROR;
errorMessage_ = "Failed to replace installed font";
return;
}
if (!Storage.rename(stagingDir, liveDir)) {
LOG_ERR("FONT", "Failed to activate staged family: %s", stagingDir);
if (hadLiveDir && Storage.exists(backupDir)) {
Storage.rename(backupDir, liveDir);
}
Storage.removeDir(stagingDir);
RenderLock lock(*this);
state_ = ERROR;
errorMessage_ = "Failed to finalize font install";
return;
}
if (Storage.exists(backupDir) && !Storage.removeDir(backupDir)) {
LOG_INF("FONT", "Failed to remove backup dir after successful install: %s", backupDir);
}
fontInstaller_.refreshRegistry();
family.installed = true;
family.hasUpdate = false;
RenderLock lock(*this);
state_ = COMPLETE;
}
// --- Input handling ---
void FontDownloadActivity::loop() {
if (state_ == FAMILY_LIST) {
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
finish();
return;
}
buttonNavigator_.onNextRelease([this] {
if (selectedIndex_ < listItemCount() - 1) {
selectedIndex_++;
requestUpdate();
}
});
buttonNavigator_.onPreviousRelease([this] {
if (selectedIndex_ > 0) {
selectedIndex_--;
requestUpdate();
}
});
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
if (!families_.empty()) {
if (isDownloadAllSelected()) {
downloadAll();
} else {
const auto& family = families_[familyIndexFromList(selectedIndex_)];
if (!family.installed || family.hasUpdate) {
downloadFamily(families_[familyIndexFromList(selectedIndex_)]);
}
}
requestUpdateAndWait();
}
}
} else if (state_ == COMPLETE) {
if (mappedInput.wasPressed(MappedInputManager::Button::Back) ||
mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
{
RenderLock lock(*this);
state_ = FAMILY_LIST;
}
requestUpdate();
}
} else if (state_ == ERROR) {
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
{
RenderLock lock(*this);
state_ = FAMILY_LIST;
}
requestUpdate();
} else if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
if (downloadingFamilyIndex_ >= 0 && downloadingFamilyIndex_ < static_cast<int>(families_.size())) {
downloadFamily(families_[downloadingFamilyIndex_]);
requestUpdateAndWait();
} else {
{
RenderLock lock(*this);
state_ = FAMILY_LIST;
}
requestUpdate();
}
}
}
}
// --- Rendering ---
std::string FontDownloadActivity::formatSize(size_t bytes) {
char buf[32];
if (bytes >= 1024 * 1024) {
snprintf(buf, sizeof(buf), "%.1f MB", static_cast<double>(bytes) / (1024.0 * 1024.0));
} else if (bytes >= 1024) {
snprintf(buf, sizeof(buf), "%.0f KB", static_cast<double>(bytes) / 1024.0);
} else {
snprintf(buf, sizeof(buf), "%zu B", bytes);
}
return buf;
}
void FontDownloadActivity::render(RenderLock&&) {
const auto& metrics = UITheme::getInstance().getMetrics();
const auto pageWidth = renderer.getScreenWidth();
const auto pageHeight = renderer.getScreenHeight();
renderer.clearScreen();
GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_FONT_DOWNLOAD));
const auto lineHeight = renderer.getLineHeight(UI_10_FONT_ID);
const auto contentTop = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing;
const auto centerY = (pageHeight - lineHeight) / 2;
if (state_ == LOADING_MANIFEST) {
renderer.drawCenteredText(UI_10_FONT_ID, centerY, tr(STR_LOADING_FONT_LIST));
} else if (state_ == FAMILY_LIST) {
if (families_.empty()) {
renderer.drawCenteredText(UI_10_FONT_ID, centerY, tr(STR_NO_FONTS_AVAILABLE));
const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", "");
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
} else {
GUI.drawList(
renderer,
Rect{0, contentTop, pageWidth, pageHeight - contentTop - metrics.buttonHintsHeight - metrics.verticalSpacing},
listItemCount(), selectedIndex_,
[this](int index) -> std::string {
if (index == 0) {
return std::string(tr(STR_DOWNLOAD_ALL)) + " (" + formatSize(totalUninstalledSize()) + ")";
}
return families_[familyIndexFromList(index)].name;
},
nullptr, nullptr,
[this](int index) -> std::string {
if (index == 0) return "";
const auto& f = families_[familyIndexFromList(index)];
if (f.hasUpdate) return tr(STR_UPDATE_AVAILABLE);
if (f.installed) return tr(STR_INSTALLED);
return f.description;
},
true);
const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_DOWNLOAD), tr(STR_DIR_UP), tr(STR_DIR_DOWN));
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
}
} else if (state_ == DOWNLOADING) {
const auto& family = families_[downloadingFamilyIndex_];
std::string statusText = std::string(tr(STR_DOWNLOADING)) + " " + family.name + " (" +
std::to_string(currentFileIndex_ + 1) + "/" + std::to_string(currentFileTotal_) + ")";
renderer.drawCenteredText(UI_10_FONT_ID, centerY - lineHeight, statusText.c_str());
float progress = 0;
if (fileTotal_ > 0) {
progress = static_cast<float>(fileProgress_) / static_cast<float>(fileTotal_);
}
int barY = centerY + metrics.verticalSpacing;
GUI.drawProgressBar(
renderer,
Rect{metrics.contentSidePadding, barY, pageWidth - metrics.contentSidePadding * 2, metrics.progressBarHeight},
static_cast<int>(progress * 100), 100);
int percentY = barY + metrics.progressBarHeight + metrics.verticalSpacing;
renderer.drawCenteredText(UI_10_FONT_ID, percentY,
(std::to_string(static_cast<int>(progress * 100)) + "%").c_str());
} else if (state_ == COMPLETE) {
renderer.drawCenteredText(UI_10_FONT_ID, centerY, tr(STR_FONT_INSTALLED), true, EpdFontFamily::BOLD);
const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", "");
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
} else if (state_ == ERROR) {
renderer.drawCenteredText(UI_10_FONT_ID, centerY - lineHeight, tr(STR_FONT_INSTALL_FAILED), true,
EpdFontFamily::BOLD);
if (!errorMessage_.empty()) {
renderer.drawCenteredText(UI_10_FONT_ID, centerY + metrics.verticalSpacing, errorMessage_.c_str());
}
const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_RETRY), "", "");
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
}
renderer.displayBuffer();
}
@@ -0,0 +1,74 @@
#pragma once
#include <string>
#include <vector>
#include "../Activity.h"
#include "FontInstaller.h"
#include "util/ButtonNavigator.h"
#ifndef FONT_MANIFEST_URL
#define FONT_MANIFEST_URL "https://github.com/jpirnay/crosspoint-reader/assets/sd-fonts/fonts.json"
#endif
class FontDownloadActivity : public Activity {
public:
explicit FontDownloadActivity(GfxRenderer& renderer, MappedInputManager& mappedInput);
void onEnter() override;
void onExit() override;
void loop() override;
void render(RenderLock&&) override;
bool preventAutoSleep() override { return state_ == LOADING_MANIFEST || state_ == DOWNLOADING; }
bool skipLoopDelay() override { return true; }
private:
enum State {
WIFI_SELECTION,
LOADING_MANIFEST,
FAMILY_LIST,
DOWNLOADING,
COMPLETE,
ERROR,
};
struct ManifestFile {
std::string name;
size_t size = 0;
};
struct ManifestFamily {
std::string name;
std::string description;
std::vector<std::string> styles;
std::vector<ManifestFile> files;
size_t totalSize = 0;
bool installed = false;
bool hasUpdate = false;
};
State state_ = WIFI_SELECTION;
FontInstaller fontInstaller_;
ButtonNavigator buttonNavigator_;
std::string baseUrl_;
std::vector<ManifestFamily> families_;
int selectedIndex_ = 0;
size_t currentFileIndex_ = 0;
size_t currentFileTotal_ = 0;
size_t fileProgress_ = 0;
size_t fileTotal_ = 0;
int downloadingFamilyIndex_ = 0;
std::string errorMessage_;
void onWifiSelectionComplete(bool success);
bool fetchAndParseManifest();
void downloadFamily(ManifestFamily& family);
void downloadAll();
bool isDownloadAllSelected() const { return selectedIndex_ == 0 && !families_.empty(); }
int familyIndexFromList(int listIndex) const { return listIndex - 1; }
int listItemCount() const { return families_.empty() ? 0 : static_cast<int>(families_.size()) + 1; }
size_t totalUninstalledSize() const;
static std::string formatSize(size_t bytes);
};
@@ -0,0 +1,86 @@
#include "FontSelectionActivity.h"
#include <I18n.h>
#include "CrossPointSettings.h"
#include "MappedInputManager.h"
#include "SdCardFontGlobals.h"
#include "components/UITheme.h"
#include "fontIds.h"
namespace {
uint8_t currentFontIndex() {
if (SETTINGS.sdFontFamilyName[0] != '\0') {
const auto& families = sdFontSystem.registry().getFamilies();
for (int i = 0; i < static_cast<int>(families.size()); i++) {
if (families[i].name == SETTINGS.sdFontFamilyName) {
return static_cast<uint8_t>(CrossPointSettings::BUILTIN_FONT_COUNT + i);
}
}
}
return SETTINGS.fontFamily < CrossPointSettings::BUILTIN_FONT_COUNT ? SETTINGS.fontFamily : 0;
}
} // namespace
void FontSelectionActivity::onEnter() {
Activity::onEnter();
fontCount = fontFamilyOptionCount();
selectedIndex = currentFontIndex();
if (selectedIndex >= fontCount) selectedIndex = 0;
requestUpdate();
}
void FontSelectionActivity::onExit() { Activity::onExit(); }
void FontSelectionActivity::loop() {
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
finish();
return;
}
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
handleSelection();
return;
}
buttonNavigator.onNextRelease([this] {
selectedIndex = ButtonNavigator::nextIndex(selectedIndex, fontCount);
requestUpdate();
});
buttonNavigator.onPreviousRelease([this] {
selectedIndex = ButtonNavigator::previousIndex(selectedIndex, fontCount);
requestUpdate();
});
}
void FontSelectionActivity::handleSelection() {
fontFamilyDynamicSetter(nullptr, static_cast<uint8_t>(selectedIndex));
finish();
}
void FontSelectionActivity::render(RenderLock&&) {
renderer.clearScreen();
const auto& metrics = UITheme::getInstance().getMetrics();
const Rect contentRect = UITheme::getContentRect(renderer, true, false);
GUI.drawHeader(renderer, Rect{contentRect.x, metrics.topPadding, contentRect.width, metrics.headerHeight},
tr(STR_FONT_FAMILY));
const int contentTop = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing;
const int contentHeight = contentRect.height - contentTop - metrics.verticalSpacing;
const uint8_t activeIndex = currentFontIndex();
GUI.drawList(
renderer, Rect{contentRect.x, contentTop, contentRect.width, contentHeight}, fontCount, selectedIndex,
[](int index) { return fontFamilyOptionLabel(static_cast<uint8_t>(index)); }, nullptr, nullptr,
[activeIndex](int index) -> std::string { return index == activeIndex ? tr(STR_SELECTED) : ""; }, true);
const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), tr(STR_DIR_UP), tr(STR_DIR_DOWN));
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer();
}
@@ -0,0 +1,28 @@
#pragma once
#include <GfxRenderer.h>
#include "../Activity.h"
#include "util/ButtonNavigator.h"
class MappedInputManager;
/// Full-screen list of all reader fonts (built-in + SD card families).
/// Replaces in-place enum cycling for the Reader Font Family setting.
class FontSelectionActivity final : public Activity {
public:
explicit FontSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput)
: Activity("FontSelect", renderer, mappedInput) {}
void onEnter() override;
void onExit() override;
void loop() override;
void render(RenderLock&&) override;
private:
void handleSelection();
ButtonNavigator buttonNavigator;
int selectedIndex = 0;
uint8_t fontCount = 0;
};
@@ -4,6 +4,7 @@
#include "ClearCacheActivity.h"
#include "ClockSettingsActivity.h"
#include "DetectTimezoneActivity.h"
#include "FontDownloadActivity.h"
#include "KOReaderSettingsActivity.h"
#include "LanguageSelectActivity.h"
#include "OpdsServerListActivity.h"
@@ -21,6 +22,8 @@ std::unique_ptr<Activity> createActivityForAction(SettingAction action, GfxRende
return std::make_unique<ButtonRemapActivity>(renderer, mappedInput);
case SettingAction::CustomiseStatusBar:
return std::make_unique<StatusBarSettingsActivity>(renderer, mappedInput);
case SettingAction::DownloadFonts:
return std::make_unique<FontDownloadActivity>(renderer, mappedInput);
case SettingAction::ClockSettings:
return std::make_unique<ClockSettingsActivity>(renderer, mappedInput);
case SettingAction::KOReaderSync:
+1
View File
@@ -15,6 +15,7 @@ enum class SettingAction {
None,
RemapFrontButtons,
CustomiseStatusBar,
DownloadFonts,
ClockSettings,
KOReaderSync,
OPDSBrowser,
@@ -9,6 +9,7 @@
#include <cstring>
#include "CrossPointSettings.h"
#include "FontSelectionActivity.h"
#include "MappedInputManager.h"
#include "SettingActionDispatch.h"
#include "SettingsList.h"
@@ -61,6 +62,16 @@ void SettingsActivity::onEnter() {
vec.push_back(std::move(s));
};
bool sawReaderFontSection = false;
bool insertedFontDownload = false;
auto insertFontDownloadBelowFontSection = [&]() {
auto fontDownload = SettingInfo::Action(StrId::STR_FONT_DOWNLOAD, SettingAction::DownloadFonts);
fontDownload.withSubcategory(StrId::STR_MENU_READER_FONT);
addToMoved(readerSettings, lastReaderSub, std::move(fontDownload));
insertedFontDownload = true;
};
for (const auto& setting : getSettingsList()) {
if (setting.category == StrId::STR_NONE_OPT) continue;
if (setting.category == StrId::STR_CAT_SYSTEM &&
@@ -77,6 +88,14 @@ void SettingsActivity::onEnter() {
enriched.enumLabels.reserve(n);
for (uint8_t i = 0; i < n; i++) enriched.enumLabels.push_back(fontFamilyOptionLabel(i));
}
const bool isReaderFontEntry =
enriched.category == StrId::STR_CAT_READER && (enriched.subcategory == StrId::STR_MENU_READER_FONT ||
enriched.submenu == StrId::STR_MENU_READER_FONT_SETTINGS);
if (!insertedFontDownload && sawReaderFontSection && !isReaderFontEntry) {
insertFontDownloadBelowFontSection();
}
if (enriched.category == StrId::STR_CAT_DISPLAY) {
addTo(displaySettings, lastDisplaySub, enriched);
} else if (enriched.category == StrId::STR_CAT_READER) {
@@ -86,9 +105,15 @@ void SettingsActivity::onEnter() {
} else if (enriched.category == StrId::STR_CAT_SYSTEM) {
addTo(systemSettings, lastSystemSub, enriched);
}
if (isReaderFontEntry) sawReaderFontSection = true;
// Web-only categories (KOReader Sync, OPDS Browser) are skipped for device UI
}
if (!insertedFontDownload && sawReaderFontSection) {
insertFontDownloadBelowFontSection();
}
// Device-only ACTION items — subcategory drives separator insertion automatically.
controlsSettings.insert(controlsSettings.begin(),
SettingInfo::Action(StrId::STR_REMAP_FRONT_BUTTONS, SettingAction::RemapFrontButtons));
@@ -227,6 +252,15 @@ void SettingsActivity::toggleCurrentSetting() {
const auto& setting = (*currentSettings)[selectedSetting];
if (setting.isSeparator) return;
if (setting.type == SettingType::ENUM && setting.nameId == StrId::STR_FONT_FAMILY) {
startActivityForResult(std::make_unique<FontSelectionActivity>(renderer, mappedInput),
[this](const ActivityResult&) {
SETTINGS.saveToFile();
needsHalfRefresh = true;
});
return;
}
if (setting.type == SettingType::ACTION) {
auto resultHandler = [this](const ActivityResult& result) {
SETTINGS.saveToFile();