Merge pull request #163 from jpirnay/feat-sdfonts-ui
feat: Complete sdfonts integration
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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
@@ -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(¬osans_10_regular);
|
||||
EpdFont notosans10BoldFont(¬osans_10_bold);
|
||||
EpdFont notosans10ItalicFont(¬osans_10_italic);
|
||||
EpdFont notosans10BoldItalicFont(¬osans_10_bolditalic);
|
||||
EpdFontFamily notosans10FontFamily(¬osans10RegularFont, ¬osans10BoldFont, ¬osans10ItalicFont,
|
||||
¬osans10BoldItalicFont);
|
||||
EpdFont notosans12RegularFont(¬osans_12_regular);
|
||||
EpdFont notosans12BoldFont(¬osans_12_bold);
|
||||
EpdFont notosans12ItalicFont(¬osans_12_italic);
|
||||
@@ -96,41 +108,17 @@ EpdFont notosans18BoldItalicFont(¬osans_18_bolditalic);
|
||||
EpdFontFamily notosans18FontFamily(¬osans18RegularFont, ¬osans18BoldFont, ¬osans18ItalicFont,
|
||||
¬osans18BoldItalicFont);
|
||||
|
||||
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(¬osans_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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user