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
+6 -30
View File
@@ -18,14 +18,12 @@ static_assert(BOOKERLY_12_FONT_ID != 0, "Font ID collision with sentinel");
static_assert(BOOKERLY_14_FONT_ID != 0, "Font ID collision with sentinel");
static_assert(BOOKERLY_16_FONT_ID != 0, "Font ID collision with sentinel");
static_assert(BOOKERLY_18_FONT_ID != 0, "Font ID collision with sentinel");
static_assert(BOOKERLY_10_FONT_ID != 0, "Font ID collision with sentinel");
static_assert(NOTOSANS_12_FONT_ID != 0, "Font ID collision with sentinel");
static_assert(NOTOSANS_14_FONT_ID != 0, "Font ID collision with sentinel");
static_assert(NOTOSANS_16_FONT_ID != 0, "Font ID collision with sentinel");
static_assert(NOTOSANS_18_FONT_ID != 0, "Font ID collision with sentinel");
static_assert(OPENDYSLEXIC_8_FONT_ID != 0, "Font ID collision with sentinel");
static_assert(OPENDYSLEXIC_10_FONT_ID != 0, "Font ID collision with sentinel");
static_assert(OPENDYSLEXIC_12_FONT_ID != 0, "Font ID collision with sentinel");
static_assert(OPENDYSLEXIC_14_FONT_ID != 0, "Font ID collision with sentinel");
static_assert(NOTOSANS_10_FONT_ID != 0, "Font ID collision with sentinel");
static_assert(UI_10_FONT_ID != 0, "Font ID collision with sentinel");
static_assert(UI_12_FONT_ID != 0, "Font ID collision with sentinel");
static_assert(SMALL_FONT_ID != 0, "Font ID collision with sentinel");
@@ -265,9 +263,7 @@ bool CrossPointSettings::loadFromBinaryFile() {
float CrossPointSettings::getReaderLineCompression() const {
const int effectiveFontId = getReaderFontId();
const int bookerlyId = getBuiltinReaderFontId(BOOKERLY, fontSize);
const int notosansId = getBuiltinReaderFontId(NOTOSANS, fontSize);
const int opendyslexicId = getBuiltinReaderFontId(OPENDYSLEXIC, fontSize);
if (effectiveFontId == notosansId) {
switch (lineSpacing) {
@@ -281,18 +277,6 @@ float CrossPointSettings::getReaderLineCompression() const {
}
}
if (effectiveFontId == opendyslexicId) {
switch (lineSpacing) {
case TIGHT:
return 0.90f;
case NORMAL:
default:
return 0.95f;
case WIDE:
return 1.0f;
}
}
// Bookerly or any SD card font: use the Bookerly-style neutral values.
switch (lineSpacing) {
case TIGHT:
@@ -342,6 +326,8 @@ int CrossPointSettings::getBuiltinReaderFontId(uint8_t family, uint8_t size) {
case BOOKERLY:
default:
switch (size) {
case TINY:
return BOOKERLY_10_FONT_ID;
case SMALL:
return BOOKERLY_12_FONT_ID;
case MEDIUM:
@@ -354,6 +340,8 @@ int CrossPointSettings::getBuiltinReaderFontId(uint8_t family, uint8_t size) {
}
case NOTOSANS:
switch (size) {
case TINY:
return NOTOSANS_10_FONT_ID;
case SMALL:
return NOTOSANS_12_FONT_ID;
case MEDIUM:
@@ -364,18 +352,6 @@ int CrossPointSettings::getBuiltinReaderFontId(uint8_t family, uint8_t size) {
case EXTRA_LARGE:
return NOTOSANS_18_FONT_ID;
}
case OPENDYSLEXIC:
switch (size) {
case SMALL:
return OPENDYSLEXIC_8_FONT_ID;
case MEDIUM:
default:
return OPENDYSLEXIC_10_FONT_ID;
case LARGE:
return OPENDYSLEXIC_12_FONT_ID;
case EXTRA_LARGE:
return OPENDYSLEXIC_14_FONT_ID;
}
}
}
+2 -2
View File
@@ -89,10 +89,10 @@ class CrossPointSettings {
};
// Font family options (built-in fonts only; SD card fonts use sdFontFamilyName)
enum FONT_FAMILY { BOOKERLY = 0, NOTOSANS = 1, OPENDYSLEXIC = 2, FONT_FAMILY_COUNT };
enum FONT_FAMILY { BOOKERLY = 0, NOTOSANS = 1, FONT_FAMILY_COUNT };
static constexpr uint8_t BUILTIN_FONT_COUNT = FONT_FAMILY_COUNT;
// Font size options
enum FONT_SIZE { SMALL = 0, MEDIUM = 1, LARGE = 2, EXTRA_LARGE = 3, FONT_SIZE_COUNT };
enum FONT_SIZE { SMALL = 0, MEDIUM = 1, LARGE = 2, EXTRA_LARGE = 3, TINY = 4, FONT_SIZE_COUNT };
enum LINE_COMPRESSION { TIGHT = 0, NORMAL = 1, WIDE = 2, LINE_COMPRESSION_COUNT };
enum PARAGRAPH_ALIGNMENT {
JUSTIFIED = 0,
+131
View File
@@ -0,0 +1,131 @@
#include "FontInstaller.h"
#include <HalStorage.h>
#include <Logging.h>
#include <cctype>
#include <cstring>
#include "CrossPointSettings.h"
FontInstaller::FontInstaller(SdCardFontRegistry& registry) : registry_(registry) {}
bool FontInstaller::isValidFamilyName(const char* name) {
if (name == nullptr || name[0] == '\0') return false;
const size_t nameLen = strlen(name);
if (nameLen == 0 || nameLen > MAX_FAMILY_NAME_LEN) return false;
if (strstr(name, "..") != nullptr) return false;
if (strchr(name, '/') != nullptr) return false;
if (strchr(name, '\\') != nullptr) return false;
for (const char* p = name; *p != '\0'; ++p) {
char c = *p;
if (!std::isalnum(static_cast<unsigned char>(c)) && c != '-' && c != '_') {
return false;
}
}
return true;
}
bool FontInstaller::ensureFamilyDir(const char* familyName) {
if (!isValidFamilyName(familyName)) {
LOG_ERR("FONT", "Invalid family name: %s", familyName ? familyName : "<null>");
return false;
}
const size_t baseLen = strlen(SdCardFontRegistry::FONTS_DIR);
const size_t familyLen = strlen(familyName);
const size_t neededLen = baseLen + 1 + familyLen + 1; // "/" + NUL
if (neededLen > 128) {
LOG_ERR("FONT", "Family dir path too long: %s", familyName);
return false;
}
if (!Storage.exists(SdCardFontRegistry::FONTS_DIR)) {
if (!Storage.mkdir(SdCardFontRegistry::FONTS_DIR)) {
LOG_ERR("FONT", "Failed to create fonts dir: %s", SdCardFontRegistry::FONTS_DIR);
return false;
}
}
char dirPath[128];
snprintf(dirPath, sizeof(dirPath), "%s/%s", SdCardFontRegistry::FONTS_DIR, familyName);
if (!Storage.exists(dirPath)) {
if (!Storage.mkdir(dirPath)) {
LOG_ERR("FONT", "Failed to create family dir: %s", dirPath);
return false;
}
}
return true;
}
bool FontInstaller::validateCpfontFile(const char* path) {
FsFile file;
if (!Storage.openFileForRead("FONT", path, file)) {
LOG_ERR("FONT", "Cannot open for validation: %s", path);
return false;
}
uint8_t magic[CPFONT_MAGIC_LEN];
size_t bytesRead = file.read(magic, CPFONT_MAGIC_LEN);
file.close();
if (bytesRead < CPFONT_MAGIC_LEN) {
LOG_ERR("FONT", "File too small: %s (%zu bytes)", path, bytesRead);
return false;
}
if (memcmp(magic, "CPFONT\0\0", CPFONT_MAGIC_LEN) != 0) {
LOG_ERR("FONT", "Bad magic in: %s", path);
return false;
}
return true;
}
void FontInstaller::buildFontPath(const char* family, const char* filename, char* outBuf, size_t outBufSize) {
snprintf(outBuf, outBufSize, "%s/%s/%s", SdCardFontRegistry::FONTS_DIR, family, filename);
}
FontInstaller::Error FontInstaller::deleteFamily(const char* familyName) {
if (!isValidFamilyName(familyName)) {
return Error::INVALID_FAMILY_NAME;
}
const size_t baseLen = strlen(SdCardFontRegistry::FONTS_DIR);
const size_t familyLen = strlen(familyName);
if (baseLen + 1 + familyLen + 1 > 128) {
LOG_ERR("FONT", "Family dir path too long: %s", familyName);
return Error::INVALID_FAMILY_NAME;
}
char dirPath[128];
snprintf(dirPath, sizeof(dirPath), "%s/%s", SdCardFontRegistry::FONTS_DIR, familyName);
if (!Storage.exists(dirPath)) {
LOG_DBG("FONT", "Family dir does not exist: %s", dirPath);
return Error::OK;
}
if (!Storage.removeDir(dirPath)) {
LOG_ERR("FONT", "Failed to remove family dir: %s", dirPath);
return Error::SD_WRITE_ERROR;
}
if (strcmp(SETTINGS.sdFontFamilyName, familyName) == 0) {
SETTINGS.sdFontFamilyName[0] = '\0';
SETTINGS.saveToFile();
LOG_DBG("FONT", "Cleared active SD font (deleted family: %s)", familyName);
}
return Error::OK;
}
void FontInstaller::refreshRegistry() { registry_.discover(); }
bool FontInstaller::isFamilyInstalled(const char* familyName) const {
return registry_.findFamily(familyName) != nullptr;
}
+53
View File
@@ -0,0 +1,53 @@
#pragma once
#include <SdCardFontRegistry.h>
#include <cstddef>
#include <cstdint>
/// Shared utility for font installation (device download + browser upload).
/// Handles directory creation, file validation, deletion, and registry refresh.
class FontInstaller {
public:
enum class Error {
OK,
INVALID_FAMILY_NAME,
INVALID_FILE,
SD_WRITE_ERROR,
MAX_FAMILIES_REACHED,
};
explicit FontInstaller(SdCardFontRegistry& registry);
// Must fit CrossPointSettings::sdFontFamilyName[32] including NUL.
static constexpr size_t MAX_FAMILY_NAME_LEN = 31;
/// Validate a family name: alphanumeric + hyphen + underscore only, no path traversal.
static bool isValidFamilyName(const char* name);
/// Ensure /.crosspoint/fonts/<family>/ directory exists.
bool ensureFamilyDir(const char* familyName);
/// Validate a .cpfont file on disk (check magic bytes).
bool validateCpfontFile(const char* path);
/// Build the full SD path for a font file.
/// Writes "/.crosspoint/fonts/<family>/<filename>" to outBuf.
static void buildFontPath(const char* family, const char* filename, char* outBuf, size_t outBufSize);
/// Delete a family directory and all .cpfont files in it.
/// If the deleted family is the active reader font, clears the setting.
Error deleteFamily(const char* familyName);
/// Re-run registry discovery to pick up new/removed fonts.
void refreshRegistry();
/// Check whether a family name already exists in the registry.
bool isFamilyInstalled(const char* familyName) const;
private:
SdCardFontRegistry& registry_;
static constexpr const char* CPFONT_MAGIC = "CPFONT\0";
static constexpr size_t CPFONT_MAGIC_LEN = 8;
};
+8
View File
@@ -417,6 +417,9 @@ bool JsonSettingsIO::saveRecentBooks(const RecentBooksStore& store, const char*
obj["embeddedStyleOverride"] = book.embeddedStyleOverride;
obj["imageRenderingOverride"] = book.imageRenderingOverride;
obj["fontFamilyOverride"] = book.fontFamilyOverride;
if (!book.sdFontFamilyOverride.empty()) {
obj["sdFontFamilyOverride"] = book.sdFontFamilyOverride;
}
obj["fontSizeOverride"] = book.fontSizeOverride;
obj["bionicReadingOverride"] = book.bionicReadingOverride;
}
@@ -455,6 +458,11 @@ bool JsonSettingsIO::loadRecentBooks(RecentBooksStore& store, const char* json)
book.imageRenderingOverride = clampInt8(obj["imageRenderingOverride"] | -1, -1, 2, -1);
book.fontFamilyOverride =
clampInt8(obj["fontFamilyOverride"] | -1, -1, CrossPointSettings::FONT_FAMILY_COUNT - 1, -1);
book.sdFontFamilyOverride = obj["sdFontFamilyOverride"] | std::string("");
if (!book.sdFontFamilyOverride.empty()) {
// Keep built-in and SD font overrides mutually exclusive.
book.fontFamilyOverride = -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);
+35 -5
View File
@@ -25,6 +25,7 @@ void RecentBooksStore::addBook(const std::string& path, const std::string& title
int8_t embeddedStyleOverride = -1;
int8_t imageRenderingOverride = -1;
int8_t fontFamilyOverride = -1;
std::string sdFontFamilyOverride;
int8_t fontSizeOverride = -1;
int8_t bionicReadingOverride = -1;
@@ -35,6 +36,7 @@ void RecentBooksStore::addBook(const std::string& path, const std::string& title
embeddedStyleOverride = it->embeddedStyleOverride;
imageRenderingOverride = it->imageRenderingOverride;
fontFamilyOverride = it->fontFamilyOverride;
sdFontFamilyOverride = it->sdFontFamilyOverride;
fontSizeOverride = it->fontSizeOverride;
bionicReadingOverride = it->bionicReadingOverride;
recentBooks.erase(it);
@@ -43,7 +45,7 @@ void RecentBooksStore::addBook(const std::string& path, const std::string& title
// Add to front
recentBooks.insert(recentBooks.begin(),
{path, title, author, series, coverBmpPath, embeddedStyleOverride, imageRenderingOverride,
fontFamilyOverride, fontSizeOverride, bionicReadingOverride});
fontFamilyOverride, sdFontFamilyOverride, fontSizeOverride, bionicReadingOverride});
// Trim to max size
if (recentBooks.size() > MAX_RECENT_BOOKS) {
@@ -93,7 +95,7 @@ bool RecentBooksStore::setReaderOverrides(const std::string& path, const int8_t
return false;
}
return setReaderOverrides(path, embeddedStyleOverride, imageRenderingOverride, it->fontFamilyOverride,
it->fontSizeOverride, it->bionicReadingOverride);
it->sdFontFamilyOverride, it->fontSizeOverride, it->bionicReadingOverride);
}
bool RecentBooksStore::setReaderOverrides(const std::string& path, const int8_t embeddedStyleOverride,
@@ -104,8 +106,21 @@ 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);
const std::string sdOverride = (fontFamilyOverride >= 0) ? std::string() : it->sdFontFamilyOverride;
return setReaderOverrides(path, embeddedStyleOverride, imageRenderingOverride, fontFamilyOverride, sdOverride,
fontSizeOverride, it->bionicReadingOverride);
}
bool RecentBooksStore::setReaderOverrides(const std::string& path, const int8_t embeddedStyleOverride,
const int8_t imageRenderingOverride, const int8_t fontFamilyOverride,
const std::string& sdFontFamilyOverride, const int8_t fontSizeOverride) {
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, fontFamilyOverride,
sdFontFamilyOverride, fontSizeOverride, it->bionicReadingOverride);
}
bool RecentBooksStore::setReaderOverrides(const std::string& path, const int8_t embeddedStyleOverride,
@@ -116,7 +131,7 @@ bool RecentBooksStore::setReaderOverrides(const std::string& path, const int8_t
return false;
}
return setReaderOverrides(path, embeddedStyleOverride, imageRenderingOverride, it->fontFamilyOverride,
it->fontSizeOverride, bionicReadingOverride);
it->sdFontFamilyOverride, it->fontSizeOverride, bionicReadingOverride);
}
bool RecentBooksStore::setReaderOverrides(const std::string& path, const int8_t embeddedStyleOverride,
@@ -127,10 +142,25 @@ bool RecentBooksStore::setReaderOverrides(const std::string& path, const int8_t
if (it == recentBooks.end()) {
return false;
}
const std::string sdOverride = (fontFamilyOverride >= 0) ? std::string() : it->sdFontFamilyOverride;
return setReaderOverrides(path, embeddedStyleOverride, imageRenderingOverride, fontFamilyOverride, sdOverride,
fontSizeOverride, bionicReadingOverride);
}
bool RecentBooksStore::setReaderOverrides(const std::string& path, const int8_t embeddedStyleOverride,
const int8_t imageRenderingOverride, const int8_t fontFamilyOverride,
const std::string& sdFontFamilyOverride, 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->sdFontFamilyOverride = sdFontFamilyOverride;
it->fontSizeOverride = fontSizeOverride;
it->bionicReadingOverride = bionicReadingOverride;
return saveToFile();
+7
View File
@@ -15,6 +15,8 @@ struct RecentBook {
int8_t imageRenderingOverride = -1;
// -1 = use global setting, otherwise CrossPointSettings::FONT_FAMILY value.
int8_t fontFamilyOverride = -1;
// Empty = use global setting, otherwise explicit SD-card family name override.
std::string sdFontFamilyOverride;
// -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).
@@ -66,10 +68,15 @@ 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,
int8_t fontFamilyOverride, const std::string& sdFontFamilyOverride, 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);
bool setReaderOverrides(const std::string& path, int8_t embeddedStyleOverride, int8_t imageRenderingOverride,
int8_t fontFamilyOverride, const std::string& sdFontFamilyOverride, int8_t fontSizeOverride,
bool bionicReadingOverride);
private:
bool loadFromBinaryFile();
+3 -3
View File
@@ -60,7 +60,7 @@ uint8_t fontFamilyOptionCount() {
std::string fontFamilyOptionLabel(uint8_t i) {
if (i < CrossPointSettings::BUILTIN_FONT_COUNT) {
static const StrId BUILTIN_LABELS[] = {StrId::STR_BOOKERLY, StrId::STR_NOTO_SANS, StrId::STR_OPEN_DYSLEXIC};
static const StrId BUILTIN_LABELS[] = {StrId::STR_BOOKERLY, StrId::STR_NOTO_SANS};
return I18N.get(BUILTIN_LABELS[i]);
}
const auto& families = sdFontSystem.registry().getFamilies();
@@ -68,8 +68,8 @@ std::string fontFamilyOptionLabel(uint8_t i) {
return sdIdx < families.size() ? families[sdIdx].name : std::string();
}
// Map fontSize enum (SMALL=0, MEDIUM=1, LARGE=2, EXTRA_LARGE=3) to point sizes.
static constexpr uint8_t FONT_SIZE_TO_PT[] = {12, 14, 16, 18};
// Map fontSize enum (SMALL=0, MEDIUM=1, LARGE=2, EXTRA_LARGE=3, TINY=4) to point sizes.
static constexpr uint8_t FONT_SIZE_TO_PT[] = {12, 14, 16, 18, 10};
static uint8_t targetPtSizeFromSettings() {
uint8_t e = SETTINGS.fontSize;
+1
View File
@@ -24,6 +24,7 @@ class SdCardFontSystem {
int resolveFontId(const char* familyName, uint8_t fontSizeEnum) const;
/// Access the registry (e.g. for settings UI to enumerate available fonts).
SdCardFontRegistry& registry() { return registry_; }
const SdCardFontRegistry& registry() const { return registry_; }
private:
+3 -4
View File
@@ -91,13 +91,12 @@ inline const std::vector<SettingInfo> list = {
// side (SettingsActivity / CrossPointWebServer enrich enumLabels before
// iterating). The built-in StrIds are kept as a fallback for code paths that
// don't enrich enumLabels.
SettingInfo::DynamicEnum(StrId::STR_FONT_FAMILY,
{StrId::STR_BOOKERLY, StrId::STR_NOTO_SANS, StrId::STR_OPEN_DYSLEXIC},
SettingInfo::DynamicEnum(StrId::STR_FONT_FAMILY, {StrId::STR_BOOKERLY, StrId::STR_NOTO_SANS},
fontFamilyDynamicGetter, fontFamilyDynamicSetter, "fontFamily", StrId::STR_CAT_READER)
.withSubcategory(StrId::STR_MENU_READER_FONT),
SettingInfo::Enum(StrId::STR_FONT_SIZE, &CrossPointSettings::fontSize,
{StrId::STR_SMALL, StrId::STR_MEDIUM, StrId::STR_LARGE, StrId::STR_X_LARGE}, "fontSize",
StrId::STR_CAT_READER)
{StrId::STR_SMALL, StrId::STR_MEDIUM, StrId::STR_LARGE, StrId::STR_X_LARGE, StrId::STR_TINY},
"fontSize", StrId::STR_CAT_READER)
.withSubmenu(StrId::STR_MENU_READER_FONT_SETTINGS),
SettingInfo::Toggle(StrId::STR_TEXT_AA, &CrossPointSettings::textAntiAliasing, "textAntiAliasing",
StrId::STR_CAT_READER)
+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();
+13 -15
View File
@@ -1,18 +1,16 @@
// The contents of this file are generated by ./lib/EpdFont/scripts/build-font-ids.sh
#pragma once
#define BOOKERLY_12_FONT_ID (-1905494168)
#define BOOKERLY_14_FONT_ID (1233852315)
#define BOOKERLY_16_FONT_ID (1588566790)
#define BOOKERLY_18_FONT_ID (681638548)
#define NOTOSANS_12_FONT_ID (-1559651934)
#define NOTOSANS_14_FONT_ID (-1014561631)
#define NOTOSANS_16_FONT_ID (-1422711852)
#define NOTOSANS_18_FONT_ID (1237754772)
#define OPENDYSLEXIC_8_FONT_ID (1331369208)
#define OPENDYSLEXIC_10_FONT_ID (-1374689004)
#define OPENDYSLEXIC_12_FONT_ID (-795539541)
#define OPENDYSLEXIC_14_FONT_ID (-1676627620)
#define UI_10_FONT_ID (-1246724383)
#define UI_12_FONT_ID (-359249323)
#define SMALL_FONT_ID (1073217904)
#define BOOKERLY_10_FONT_ID (825122367)
#define BOOKERLY_12_FONT_ID (-1084788859)
#define BOOKERLY_14_FONT_ID (-1298909628)
#define BOOKERLY_16_FONT_ID (-1580381194)
#define BOOKERLY_18_FONT_ID (1499238407)
#define NOTOSANS_10_FONT_ID (1337583050)
#define NOTOSANS_12_FONT_ID (-846830324)
#define NOTOSANS_14_FONT_ID (1528995761)
#define NOTOSANS_16_FONT_ID (-1135152666)
#define NOTOSANS_18_FONT_ID (-1632292195)
#define UI_10_FONT_ID (1883578066)
#define UI_12_FONT_ID (376639728)
#define SMALL_FONT_ID (-1860378437)
+18 -32
View File
@@ -52,6 +52,12 @@ EpdFont bookerly14BoldItalicFont(&bookerly_14_bolditalic);
EpdFontFamily bookerly14FontFamily(&bookerly14RegularFont, &bookerly14BoldFont, &bookerly14ItalicFont,
&bookerly14BoldItalicFont);
#ifndef OMIT_FONTS
EpdFont bookerly10RegularFont(&bookerly_10_regular);
EpdFont bookerly10BoldFont(&bookerly_10_bold);
EpdFont bookerly10ItalicFont(&bookerly_10_italic);
EpdFont bookerly10BoldItalicFont(&bookerly_10_bolditalic);
EpdFontFamily bookerly10FontFamily(&bookerly10RegularFont, &bookerly10BoldFont, &bookerly10ItalicFont,
&bookerly10BoldItalicFont);
EpdFont bookerly12RegularFont(&bookerly_12_regular);
EpdFont bookerly12BoldFont(&bookerly_12_bold);
EpdFont bookerly12ItalicFont(&bookerly_12_italic);
@@ -71,6 +77,12 @@ EpdFont bookerly18BoldItalicFont(&bookerly_18_bolditalic);
EpdFontFamily bookerly18FontFamily(&bookerly18RegularFont, &bookerly18BoldFont, &bookerly18ItalicFont,
&bookerly18BoldItalicFont);
EpdFont notosans10RegularFont(&notosans_10_regular);
EpdFont notosans10BoldFont(&notosans_10_bold);
EpdFont notosans10ItalicFont(&notosans_10_italic);
EpdFont notosans10BoldItalicFont(&notosans_10_bolditalic);
EpdFontFamily notosans10FontFamily(&notosans10RegularFont, &notosans10BoldFont, &notosans10ItalicFont,
&notosans10BoldItalicFont);
EpdFont notosans12RegularFont(&notosans_12_regular);
EpdFont notosans12BoldFont(&notosans_12_bold);
EpdFont notosans12ItalicFont(&notosans_12_italic);
@@ -96,41 +108,17 @@ EpdFont notosans18BoldItalicFont(&notosans_18_bolditalic);
EpdFontFamily notosans18FontFamily(&notosans18RegularFont, &notosans18BoldFont, &notosans18ItalicFont,
&notosans18BoldItalicFont);
EpdFont opendyslexic8RegularFont(&opendyslexic_8_regular);
EpdFont opendyslexic8BoldFont(&opendyslexic_8_bold);
EpdFont opendyslexic8ItalicFont(&opendyslexic_8_italic);
EpdFont opendyslexic8BoldItalicFont(&opendyslexic_8_bolditalic);
EpdFontFamily opendyslexic8FontFamily(&opendyslexic8RegularFont, &opendyslexic8BoldFont, &opendyslexic8ItalicFont,
&opendyslexic8BoldItalicFont);
EpdFont opendyslexic10RegularFont(&opendyslexic_10_regular);
EpdFont opendyslexic10BoldFont(&opendyslexic_10_bold);
EpdFont opendyslexic10ItalicFont(&opendyslexic_10_italic);
EpdFont opendyslexic10BoldItalicFont(&opendyslexic_10_bolditalic);
EpdFontFamily opendyslexic10FontFamily(&opendyslexic10RegularFont, &opendyslexic10BoldFont, &opendyslexic10ItalicFont,
&opendyslexic10BoldItalicFont);
EpdFont opendyslexic12RegularFont(&opendyslexic_12_regular);
EpdFont opendyslexic12BoldFont(&opendyslexic_12_bold);
EpdFont opendyslexic12ItalicFont(&opendyslexic_12_italic);
EpdFont opendyslexic12BoldItalicFont(&opendyslexic_12_bolditalic);
EpdFontFamily opendyslexic12FontFamily(&opendyslexic12RegularFont, &opendyslexic12BoldFont, &opendyslexic12ItalicFont,
&opendyslexic12BoldItalicFont);
EpdFont opendyslexic14RegularFont(&opendyslexic_14_regular);
EpdFont opendyslexic14BoldFont(&opendyslexic_14_bold);
EpdFont opendyslexic14ItalicFont(&opendyslexic_14_italic);
EpdFont opendyslexic14BoldItalicFont(&opendyslexic_14_bolditalic);
EpdFontFamily opendyslexic14FontFamily(&opendyslexic14RegularFont, &opendyslexic14BoldFont, &opendyslexic14ItalicFont,
&opendyslexic14BoldItalicFont);
#endif // OMIT_FONTS
EpdFont smallFont(&notosans_8_regular);
EpdFontFamily smallFontFamily(&smallFont);
EpdFont ui10RegularFont(&ubuntu_10_regular);
EpdFont ui10BoldFont(&ubuntu_10_bold);
EpdFont ui10RegularFont(&inter_ui_10_regular);
EpdFont ui10BoldFont(&inter_ui_10_bold);
EpdFontFamily ui10FontFamily(&ui10RegularFont, &ui10BoldFont);
EpdFont ui12RegularFont(&ubuntu_12_regular);
EpdFont ui12BoldFont(&ubuntu_12_bold);
EpdFont ui12RegularFont(&inter_ui_12_regular);
EpdFont ui12BoldFont(&inter_ui_12_bold);
EpdFontFamily ui12FontFamily(&ui12RegularFont, &ui12BoldFont);
// Enter deep sleep mode
@@ -168,18 +156,16 @@ void setupDisplayAndFonts() {
renderer.setFontCacheManager(&fontCacheManager);
renderer.insertFont(BOOKERLY_14_FONT_ID, bookerly14FontFamily);
#ifndef OMIT_FONTS
renderer.insertFont(BOOKERLY_10_FONT_ID, bookerly10FontFamily);
renderer.insertFont(BOOKERLY_12_FONT_ID, bookerly12FontFamily);
renderer.insertFont(BOOKERLY_16_FONT_ID, bookerly16FontFamily);
renderer.insertFont(BOOKERLY_18_FONT_ID, bookerly18FontFamily);
renderer.insertFont(NOTOSANS_10_FONT_ID, notosans10FontFamily);
renderer.insertFont(NOTOSANS_12_FONT_ID, notosans12FontFamily);
renderer.insertFont(NOTOSANS_14_FONT_ID, notosans14FontFamily);
renderer.insertFont(NOTOSANS_16_FONT_ID, notosans16FontFamily);
renderer.insertFont(NOTOSANS_18_FONT_ID, notosans18FontFamily);
renderer.insertFont(OPENDYSLEXIC_8_FONT_ID, opendyslexic8FontFamily);
renderer.insertFont(OPENDYSLEXIC_10_FONT_ID, opendyslexic10FontFamily);
renderer.insertFont(OPENDYSLEXIC_12_FONT_ID, opendyslexic12FontFamily);
renderer.insertFont(OPENDYSLEXIC_14_FONT_ID, opendyslexic14FontFamily);
#endif // OMIT_FONTS
renderer.insertFont(UI_10_FONT_ID, ui10FontFamily);
renderer.insertFont(UI_12_FONT_ID, ui12FontFamily);
+229
View File
@@ -12,12 +12,16 @@
#include <cstring>
#include "CrossPointSettings.h"
#include "FontInstaller.h"
#include "OpdsServerStore.h"
#include "SdCardFontGlobals.h"
#include "SdCardFontRegistry.h"
#include "SettingsList.h"
#include "SystemStatus.h"
#include "WebDAVHandler.h"
#include "WifiCredentialStore.h"
#include "html/FilesPageHtml.generated.h"
#include "html/FontsPageHtml.generated.h"
#include "html/HomePageHtml.generated.h"
#include "html/SettingsPageHtml.generated.h"
#include "html/WelcomePageHtml.generated.h"
@@ -196,6 +200,12 @@ void CrossPointWebServer::begin() {
server->on("/api/settings", HTTP_GET, [this] { handleGetSettings(); });
server->on("/api/settings", HTTP_POST, [this] { handlePostSettings(); });
// Font management endpoints
server->on("/fonts", HTTP_GET, [this] { handleFontsPage(); });
server->on("/api/fonts", HTTP_GET, [this] { handleFontList(); });
server->on("/api/fonts/upload", HTTP_POST, [this] { handleFontUpload(); }, [this] { handleFontUploadData(); });
server->on("/api/fonts/delete", HTTP_POST, [this] { handleFontDelete(); });
// OPDS server endpoints
server->on("/api/opds", HTTP_GET, [this] { handleGetOpdsServers(); });
server->on("/api/opds", HTTP_POST, [this] { handlePostOpdsServer(); });
@@ -1399,6 +1409,225 @@ void CrossPointWebServer::handlePostSettings() {
server->send(200, "text/plain", String("Applied ") + String(applied) + " setting(s)");
}
// ---- Font Management API ----
void CrossPointWebServer::handleFontsPage() const {
sendHtmlContent(server.get(), FontsPageHtml, sizeof(FontsPageHtml));
LOG_DBG("WEB", "Served fonts page");
}
void CrossPointWebServer::handleFontList() {
FontInstaller installer(sdFontSystem.registry());
installer.refreshRegistry();
const auto& families = sdFontSystem.registry().getFamilies();
JsonDocument doc;
JsonArray arr = doc["families"].to<JsonArray>();
doc["maxFamilies"] = SdCardFontRegistry::MAX_SD_FAMILIES;
for (const auto& family : families) {
JsonObject fObj = arr.add<JsonObject>();
fObj["name"] = family.name;
JsonArray sizes = fObj["sizes"].to<JsonArray>();
for (uint8_t s : family.availableSizes()) {
sizes.add(s);
}
JsonArray files = fObj["files"].to<JsonArray>();
for (const auto& file : family.files) {
JsonObject fileObj = files.add<JsonObject>();
const char* name = strrchr(file.path.c_str(), '/');
fileObj["name"] = name ? name + 1 : file.path.c_str();
FsFile f;
if (Storage.openFileForRead("WEB", file.path.c_str(), f)) {
fileObj["size"] = static_cast<unsigned long>(f.size());
f.close();
} else {
fileObj["size"] = 0;
}
}
}
String json;
serializeJson(doc, json);
server->send(200, "application/json", json);
}
void CrossPointWebServer::handleFontUploadData() {
HTTPUpload& up = server->upload();
switch (up.status) {
case UPLOAD_FILE_START: {
esp_task_wdt_reset();
String family = server->arg("family");
fontUpload.valid = false;
fontUpload.magicChecked = false;
fontUpload.headerBytesReceived = 0;
fontUpload.bytesWritten = 0;
fontUpload.bufferPos = 0;
if (!FontInstaller::isValidFamilyName(family.c_str())) {
LOG_ERR("WEB", "Invalid font family name: %s", family.c_str());
break;
}
String filename = up.filename;
if (!filename.endsWith(".cpfont")) {
LOG_ERR("WEB", "Not a .cpfont file: %s", filename.c_str());
break;
}
if (filename.indexOf('/') >= 0 || filename.indexOf('\\') >= 0 || filename.indexOf("..") >= 0) {
LOG_ERR("WEB", "Invalid font filename: %s", filename.c_str());
break;
}
fontUpload.familyName = family.c_str();
FontInstaller installer(sdFontSystem.registry());
if (!installer.ensureFamilyDir(family.c_str())) {
LOG_ERR("WEB", "Failed to create font family dir");
break;
}
char path[128];
FontInstaller::buildFontPath(family.c_str(), filename.c_str(), path, sizeof(path));
fontUpload.filePath = path;
if (!Storage.openFileForWrite("WEB", path, fontUpload.file)) {
LOG_ERR("WEB", "Failed to open font file for write: %s", path);
break;
}
fontUpload.valid = true;
LOG_DBG("WEB", "Font upload started: %s -> %s", filename.c_str(), path);
break;
}
case UPLOAD_FILE_WRITE: {
if (!fontUpload.valid) break;
esp_task_wdt_reset();
if (!fontUpload.magicChecked) {
size_t needed = 8 - fontUpload.headerBytesReceived;
size_t take = (up.currentSize < needed) ? up.currentSize : needed;
if (take > 0) {
memcpy(fontUpload.header + fontUpload.headerBytesReceived, up.buf, take);
fontUpload.headerBytesReceived += take;
}
if (fontUpload.headerBytesReceived == 8) {
if (memcmp(fontUpload.header, "CPFONT\0\0", 8) != 0) {
LOG_ERR("WEB", "Invalid .cpfont magic bytes");
fontUpload.valid = false;
fontUpload.file.close();
return;
}
fontUpload.magicChecked = true;
}
}
size_t remaining = up.currentSize;
const uint8_t* src = up.buf;
while (remaining > 0) {
size_t space = FontUploadState::BUFFER_SIZE - fontUpload.bufferPos;
size_t chunk = (remaining < space) ? remaining : space;
memcpy(fontUpload.buffer.data() + fontUpload.bufferPos, src, chunk);
fontUpload.bufferPos += chunk;
src += chunk;
remaining -= chunk;
if (fontUpload.bufferPos >= FontUploadState::BUFFER_SIZE) {
const size_t expected = fontUpload.bufferPos;
const size_t written = fontUpload.file.write(fontUpload.buffer.data(), expected);
fontUpload.bytesWritten += written;
if (written != expected) {
LOG_ERR("WEB", "Failed writing uploaded font chunk (%u/%u bytes)", static_cast<unsigned>(written),
static_cast<unsigned>(expected));
fontUpload.valid = false;
fontUpload.file.close();
return;
}
fontUpload.bufferPos = 0;
esp_task_wdt_reset();
}
}
break;
}
case UPLOAD_FILE_END: {
if (fontUpload.valid && !fontUpload.magicChecked) {
LOG_ERR("WEB", "Invalid .cpfont upload: header not fully received");
fontUpload.valid = false;
}
if (fontUpload.valid && fontUpload.bufferPos > 0) {
const size_t expected = fontUpload.bufferPos;
const size_t written = fontUpload.file.write(fontUpload.buffer.data(), expected);
fontUpload.bytesWritten += written;
if (written != expected) {
LOG_ERR("WEB", "Failed flushing uploaded font chunk (%u/%u bytes)", static_cast<unsigned>(written),
static_cast<unsigned>(expected));
fontUpload.valid = false;
}
fontUpload.bufferPos = 0;
}
fontUpload.file.close();
if (!fontUpload.valid && !fontUpload.filePath.empty()) {
Storage.remove(fontUpload.filePath.c_str());
}
LOG_DBG("WEB", "Font upload end: valid=%d, %zu bytes", fontUpload.valid, fontUpload.bytesWritten);
break;
}
case UPLOAD_FILE_ABORTED: {
fontUpload.file.close();
if (!fontUpload.filePath.empty()) {
Storage.remove(fontUpload.filePath.c_str());
}
fontUpload.valid = false;
LOG_DBG("WEB", "Font upload aborted");
break;
}
}
}
void CrossPointWebServer::handleFontUpload() {
if (fontUpload.valid) {
FontInstaller installer(sdFontSystem.registry());
installer.refreshRegistry();
server->send(200, "application/json", "{\"ok\":true}");
LOG_DBG("WEB", "Font upload complete: %s", fontUpload.filePath.c_str());
} else {
server->send(400, "application/json", "{\"error\":\"Invalid .cpfont file\"}");
}
}
void CrossPointWebServer::handleFontDelete() {
String body = server->arg("plain");
JsonDocument doc;
DeserializationError err = deserializeJson(doc, body);
if (err || !doc["family"].is<const char*>()) {
server->send(400, "application/json", "{\"error\":\"Invalid request\"}");
return;
}
const char* familyName = doc["family"];
FontInstaller installer(sdFontSystem.registry());
auto result = installer.deleteFamily(familyName);
if (result == FontInstaller::Error::OK) {
installer.refreshRegistry();
server->send(200, "application/json", "{\"ok\":true}");
LOG_DBG("WEB", "Deleted font family: %s", familyName);
} else {
server->send(500, "application/json", "{\"error\":\"Delete failed\"}");
LOG_ERR("WEB", "Failed to delete font family: %s", familyName);
}
}
// ---- Wi-Fi Credentials API ----
void CrossPointWebServer::handleGetWifiNetworks() const {
+23
View File
@@ -110,6 +110,29 @@ class CrossPointWebServer {
void handleGetSettings() const;
void handlePostSettings();
// Font management handlers
void handleFontsPage() const;
void handleFontList();
void handleFontUpload();
void handleFontUploadData();
void handleFontDelete();
struct FontUploadState {
FsFile file;
std::string familyName;
std::string filePath;
bool valid = false;
bool magicChecked = false;
uint8_t header[8] = {0};
size_t headerBytesReceived = 0;
size_t bytesWritten = 0;
static constexpr size_t BUFFER_SIZE = 4096;
std::vector<uint8_t> buffer;
size_t bufferPos = 0;
FontUploadState() { buffer.resize(BUFFER_SIZE); }
} fontUpload;
// OPDS server handlers
void handleGetOpdsServers() const;
void handlePostOpdsServer();
+349
View File
@@ -0,0 +1,349 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>%%CROSSPOINT%% - Fonts</title>
<style>
:root {
--font-color: #333;
--bg: #f5f5f5;
--title-color: #2c3e50;
--card-bg: #FFF;
--label-color: #7f8c8d;
--border-color: #eee;
--accent-color: rgb(110, 154, 130);
--accent-hover-color: #5a8c73;
--danger-color: #e74c3c;
--danger-hover: #c0392b;
--notice-bg: #fff8e1;
--notice-border: #ffd54f;
--notice-color: #5d4e0a;
}
@media (prefers-color-scheme: dark) {
:root {
--font-color: #f5f5f5;
--bg: #333;
--title-color: #ecf0f1;
--card-bg: #444;
--label-color: #bdc3c7;
--border-color: #555;
--notice-bg: #4a4030;
--notice-border: #b89a3e;
--notice-color: #f3e5a0;
color-scheme: dark;
}
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
Oxygen, Ubuntu, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
background-color: var(--bg);
color: var(--font-color);
}
h1 {
color: var(--title-color);
border-bottom: 2px solid var(--accent-color);
padding-bottom: 10px;
}
h2 { color: var(--title-color); margin-top: 0; }
h3 { margin: 0 0 8px 0; }
.card {
background: var(--card-bg);
border-radius: 8px;
padding: 20px;
margin: 15px 0;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.nav-links {
margin: 20px 0;
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.nav-links a {
padding: 10px 20px;
color: var(--font-color);
text-decoration: none;
border-radius: 4px;
}
.nav-links a.active {
background-color: var(--accent-color);
color: white;
}
.nav-links a:not(.active):hover {
background-color: var(--accent-hover-color);
color: white;
}
.family {
border-bottom: 1px solid var(--border-color);
padding: 12px 0;
display: flex;
justify-content: space-between;
align-items: center;
}
.family:last-child { border-bottom: none; }
.family-info { flex: 1; }
.family-meta { color: var(--label-color); font-size: 0.9em; }
.btn {
padding: 6px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.9em;
}
.btn-danger {
background: var(--danger-color);
color: white;
}
.btn-danger:hover { background: var(--danger-hover); }
.btn-primary {
background: var(--accent-color);
color: white;
}
.btn-primary:hover { background: var(--accent-hover-color); }
.upload-form {
display: flex;
gap: 10px;
align-items: center;
flex-wrap: wrap;
}
.upload-form input[type="text"] {
padding: 6px 10px;
border: 1px solid var(--border-color);
border-radius: 4px;
background: var(--bg);
color: var(--font-color);
}
.upload-form input[type="file"] { flex: 1; min-width: 200px; }
#status {
margin-top: 10px;
padding: 8px;
border-radius: 4px;
display: none;
}
.status-ok { background: #d4edda; color: #155724; display: block !important; }
.status-err { background: #f8d7da; color: #721c24; display: block !important; }
.empty { color: var(--label-color); text-align: center; padding: 20px; }
.notice {
background: var(--notice-bg);
border: 1px solid var(--notice-border);
color: var(--notice-color);
border-radius: 6px;
padding: 10px 14px;
margin: 0 0 12px;
font-size: 0.95em;
}
.notice a { color: inherit; font-weight: 600; }
</style>
</head>
<body>
<h1>📚 %%CROSSPOINT%%</h1>
<div class="nav-links">
<a href="/files">File Manager</a>
<a href="/settings">Settings</a>
<a href="/fonts" class="active">Fonts</a>
<a href="/systeminfo">System Info</a>
</div>
<div class="card">
<h2>Installed Fonts</h2>
<div id="families"><p class="empty">Loading...</p></div>
</div>
<div class="card">
<h2>Upload Font</h2>
<p class="notice">
Need a <code>.cpfont</code> file? Convert any TrueType/OpenType font at
<a href="https://crosspointreader.com/fonts" target="_blank" rel="noopener">crosspointreader.com/fonts</a>,
then upload the resulting files here.
</p>
<form class="upload-form" id="uploadForm">
<input type="file" id="fontFiles" accept=".cpfont" multiple required />
<button type="submit" class="btn btn-primary">Upload</button>
</form>
<p id="pickedInfo" class="family-meta" style="margin: 8px 0 0;"></p>
<div id="status"></div>
</div>
<script>
function formatSize(bytes) {
if (bytes >= 1048576) return (bytes / 1048576).toFixed(1) + ' MB';
if (bytes >= 1024) return (bytes / 1024).toFixed(0) + ' KB';
return bytes + ' B';
}
async function loadFonts() {
try {
const res = await fetch('/api/fonts');
const data = await res.json();
const el = document.getElementById('families');
el.innerHTML = '';
if (!data.families || data.families.length === 0) {
const p = document.createElement('p');
p.className = 'empty';
p.textContent = 'No fonts installed';
el.appendChild(p);
return;
}
data.families.forEach(f => {
const familyRow = document.createElement('div');
familyRow.className = 'family';
const info = document.createElement('div');
info.className = 'family-info';
const title = document.createElement('h3');
title.textContent = f.name;
const meta = document.createElement('span');
meta.className = 'family-meta';
const sizes = Array.isArray(f.sizes) ? f.sizes.join(', ') : '';
const fileSizes = Array.isArray(f.files) ? f.files.map(fi => formatSize(fi.size)).join(' + ') : '';
meta.textContent = sizes + 'pt - ' + fileSizes;
info.appendChild(title);
info.appendChild(meta);
const delBtn = document.createElement('button');
delBtn.className = 'btn btn-danger';
delBtn.dataset.family = f.name;
delBtn.textContent = 'Delete';
familyRow.appendChild(info);
familyRow.appendChild(delBtn);
el.appendChild(familyRow);
});
el.querySelectorAll('button[data-family]').forEach(btn => {
btn.addEventListener('click', () => deleteFamily(btn.dataset.family));
});
} catch (e) {
document.getElementById('families').innerHTML =
'<p class="empty">Failed to load font list</p>';
}
}
async function deleteFamily(name) {
if (!confirm('Delete font family "' + name + '"?')) return;
const status = document.getElementById('status');
status.className = '';
status.style.display = 'block';
status.textContent = 'Deleting ' + name + '...';
try {
const res = await fetch('/api/fonts/delete', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({family: name})
});
if (res.ok) {
status.className = 'status-ok';
status.textContent = 'Deleted "' + name + '".';
} else {
status.className = 'status-err';
status.textContent = 'Failed to delete "' + name + '".';
}
} catch (err) {
status.className = 'status-err';
status.textContent = 'Delete error: ' + err.message;
}
await loadFonts();
}
// Derive family name from a .cpfont filename: take everything before the
// last '-' or '_' (separator that precedes the size suffix, e.g. Bookerly_12.cpfont).
function familyFromFilename(name) {
const stem = name.replace(/\.cpfont$/i, '');
const cut = Math.max(stem.lastIndexOf('-'), stem.lastIndexOf('_'));
return cut > 0 ? stem.slice(0, cut) : stem;
}
// Sanitize to match firmware's [A-Za-z0-9_-]+ pattern.
function sanitizeFamily(raw) {
return raw.replace(/[^A-Za-z0-9_-]/g, '_');
}
function cpfontFilesOnly(fileList) {
return Array.from(fileList).filter(f => /\.cpfont$/i.test(f.name));
}
document.getElementById('fontFiles').addEventListener('change', function() {
const info = document.getElementById('pickedInfo');
const files = cpfontFilesOnly(this.files);
if (files.length === 0) {
info.textContent = 'Pick one or more .cpfont files.';
return;
}
const families = new Set(files.map(f => sanitizeFamily(familyFromFilename(f.name))));
if (families.size > 1) {
info.textContent = 'Picked files contain multiple families — please select files from a single family.';
return;
}
const family = families.values().next().value;
info.textContent = files.length + ' file' + (files.length === 1 ? '' : 's') +
' → family "' + family + '"';
});
document.getElementById('uploadForm').addEventListener('submit', async function(e) {
e.preventDefault();
const status = document.getElementById('status');
const files = cpfontFilesOnly(document.getElementById('fontFiles').files);
if (files.length === 0) {
status.className = 'status-err';
status.style.display = 'block';
status.textContent = 'No .cpfont files selected.';
return;
}
const families = files.map(f => sanitizeFamily(familyFromFilename(f.name)));
const uniqueFamilies = new Set(families);
if (uniqueFamilies.size !== 1) {
status.className = 'status-err';
status.style.display = 'block';
status.textContent = 'Selected files belong to multiple families.';
return;
}
const family = families[0];
status.className = '';
status.style.display = 'block';
let uploaded = 0;
for (const file of files) {
status.textContent = 'Uploading ' + (uploaded + 1) + '/' + files.length + ': ' + file.name;
const formData = new FormData();
formData.append('family', family);
formData.append('file', file, file.name);
try {
const res = await fetch('/api/fonts/upload', { method: 'POST', body: formData });
const data = await res.json();
if (!data.ok) {
status.className = 'status-err';
status.textContent = 'Failed on ' + file.name + ': ' + (data.error || 'unknown error');
await loadFonts();
return;
}
} catch (err) {
status.className = 'status-err';
status.textContent = 'Upload error on ' + file.name + ': ' + err.message;
await loadFonts();
return;
}
uploaded++;
}
status.className = 'status-ok';
status.textContent = 'Uploaded ' + uploaded + ' file' + (uploaded === 1 ? '' : 's') +
' to family "' + family + '".';
await loadFonts();
});
loadFonts();
</script>
</body>
</html>
+1
View File
@@ -107,6 +107,7 @@
<div class="nav-links">
<a href="/files">Open File Manager</a>
<a href="/settings">Settings</a>
<a href="/fonts">Font Manager</a>
<a href="/systeminfo">System Info</a>
</div>
<div class="footer">Fast start page designed for weak connections</div>