Compare commits

..

2 Commits

Author SHA1 Message Date
ratedcounsel
73959ea9ac feat: add Refresh Frequency user preference setting 2025-12-31 10:49:52 +00:00
ratedcounsel
00369161f9 style: improve EPUB reader footer layout with delimiter line and consistent spacing 2025-12-31 10:45:27 +00:00
9 changed files with 124 additions and 41 deletions

View File

@ -4,6 +4,7 @@
#include <EpdFontFamily.h>
#include <map>
#include <string>
#include "Bitmap.h"

View File

@ -4,6 +4,7 @@ default_envs = default
[base]
platform = espressif32 @ 6.12.0
platform_packages = espressif/toolchain-riscv32-esp@11.2.0+2022r1
board = esp32-c3-devkitm-1
framework = arduino
monitor_speed = 115200
@ -43,6 +44,7 @@ lib_deps =
InputManager=symlink://open-x4-sdk/libs/hardware/InputManager
EInkDisplay=symlink://open-x4-sdk/libs/display/EInkDisplay
SDCardManager=symlink://open-x4-sdk/libs/hardware/SDCardManager
SdFat @ 2.2.3
ArduinoJson @ 7.4.2
QRCode @ 0.0.1

View File

@ -12,7 +12,7 @@ CrossPointSettings CrossPointSettings::instance;
namespace {
constexpr uint8_t SETTINGS_FILE_VERSION = 1;
// Increment this when adding new persisted settings fields
constexpr uint8_t SETTINGS_COUNT = 11;
constexpr uint8_t SETTINGS_COUNT = 12;
constexpr char SETTINGS_FILE[] = "/.crosspoint/settings.bin";
} // namespace
@ -38,6 +38,7 @@ bool CrossPointSettings::saveToFile() const {
serialization::writePod(outputFile, fontSize);
serialization::writePod(outputFile, lineSpacing);
serialization::writePod(outputFile, sleepTimeout);
serialization::writePod(outputFile, refreshFrequency);
outputFile.close();
Serial.printf("[%lu] [CPS] Settings saved to file\n", millis());
@ -86,6 +87,8 @@ bool CrossPointSettings::loadFromFile() {
if (++settingsRead >= fileSettingsCount) break;
serialization::readPod(inputFile, sleepTimeout);
if (++settingsRead >= fileSettingsCount) break;
serialization::readPod(inputFile, refreshFrequency);
if (++settingsRead >= fileSettingsCount) break;
} while (false);
inputFile.close();
@ -145,6 +148,22 @@ unsigned long CrossPointSettings::getSleepTimeoutMs() const {
}
}
int CrossPointSettings::getRefreshFrequency() const {
switch (refreshFrequency) {
case REFRESH_1:
return 1;
case REFRESH_5:
return 5;
case REFRESH_10:
return 10;
case REFRESH_15:
default:
return 15;
case REFRESH_30:
return 30;
}
}
int CrossPointSettings::getReaderFontId() const {
switch (fontFamily) {
case BOOKERLY:

View File

@ -47,6 +47,9 @@ class CrossPointSettings {
// Auto-sleep timeout options (in minutes)
enum SLEEP_TIMEOUT { SLEEP_1_MIN = 0, SLEEP_5_MIN = 1, SLEEP_10_MIN = 2, SLEEP_15_MIN = 3, SLEEP_30_MIN = 4 };
// E-ink refresh frequency (pages between full refreshes)
enum REFRESH_FREQUENCY { REFRESH_1 = 0, REFRESH_5 = 1, REFRESH_10 = 2, REFRESH_15 = 3, REFRESH_30 = 4 };
// Sleep screen settings
uint8_t sleepScreen = DARK;
// Status bar settings
@ -67,6 +70,8 @@ class CrossPointSettings {
uint8_t lineSpacing = NORMAL;
// Auto-sleep timeout setting (default 10 minutes)
uint8_t sleepTimeout = SLEEP_10_MIN;
// E-ink refresh frequency (default 15 pages)
uint8_t refreshFrequency = REFRESH_15;
~CrossPointSettings() = default;
@ -81,6 +86,7 @@ class CrossPointSettings {
float getReaderLineCompression() const;
unsigned long getSleepTimeoutMs() const;
int getRefreshFrequency() const;
};
// Helper macro to access settings

View File

@ -134,8 +134,8 @@ void HomeActivity::render() const {
const auto pageWidth = renderer.getScreenWidth();
const auto pageHeight = renderer.getScreenHeight();
constexpr int margin = 20;
constexpr int bottomMargin = 60;
constexpr int margin = 16;
constexpr int bottomMargin = 56;
// --- Top "book" card for the current title (selectorIndex == 0) ---
const int bookWidth = pageWidth / 2;
@ -279,8 +279,8 @@ void HomeActivity::render() const {
// --- Bottom menu tiles (indices 1-3) ---
const int menuTileWidth = pageWidth - 2 * margin;
constexpr int menuTileHeight = 50;
constexpr int menuSpacing = 10;
constexpr int menuTileHeight = 44;
constexpr int menuSpacing = 8;
constexpr int totalMenuHeight = 3 * menuTileHeight + 2 * menuSpacing;
int menuStartY = bookY + bookHeight + 20;
@ -316,7 +316,7 @@ void HomeActivity::render() const {
const auto labels = mappedInput.mapLabels("", "Confirm", "Up", "Down");
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
ScreenComponents::drawBattery(renderer, 20, pageHeight - 70);
ScreenComponents::drawBattery(renderer, margin, pageHeight - 68);
renderer.displayBuffer();
}

View File

@ -13,12 +13,18 @@
#include "fontIds.h"
namespace {
constexpr int pagesPerRefresh = 15;
// pagesPerRefresh now comes from SETTINGS.getRefreshFrequency()
constexpr unsigned long skipChapterMs = 700;
constexpr unsigned long goHomeMs = 1000;
constexpr int topPadding = 5;
constexpr int horizontalPadding = 5;
constexpr int statusBarMargin = 19;
// Footer layout: total height reserved for footer area
// contentGap = space between last line of book text and delimiter line
// lineToText = space between delimiter line and footer text
// footerTextHeight ~= 12px for small font
constexpr int footerHeight = 34; // total footer area height (includes bottom margin)
constexpr int contentGap = 6; // gap above delimiter line
constexpr int lineToText = 6; // gap below delimiter line to text
} // namespace
void EpubReaderActivity::taskTrampoline(void* param) {
@ -256,7 +262,7 @@ void EpubReaderActivity::renderScreen() {
orientedMarginTop += topPadding;
orientedMarginLeft += horizontalPadding;
orientedMarginRight += horizontalPadding;
orientedMarginBottom += statusBarMargin;
orientedMarginBottom += footerHeight + contentGap;
if (!section) {
const auto filepath = epub->getSpineItem(currentSpineIndex).href;
@ -378,7 +384,7 @@ void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int or
renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
if (pagesUntilFullRefresh <= 1) {
renderer.displayBuffer(EInkDisplay::HALF_REFRESH);
pagesUntilFullRefresh = pagesPerRefresh;
pagesUntilFullRefresh = SETTINGS.getRefreshFrequency();
} else {
renderer.displayBuffer();
pagesUntilFullRefresh--;
@ -419,9 +425,20 @@ void EpubReaderActivity::renderStatusBar(const int orientedMarginRight, const in
const bool showChapterTitle = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::NO_PROGRESS ||
SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL;
// Position status bar near the bottom of the logical screen, regardless of orientation
// Footer layout calculation:
// Screen bottom -> viewable margin -> footer text -> lineToText -> line -> contentGap -> book content
// orientedMarginBottom already includes (footerHeight + contentGap)
// So book content ends at: screenHeight - orientedMarginBottom
// Line should be at: screenHeight - orientedMarginBottom + contentGap (just below content)
// Footer text at: lineY + lineToText + some offset for text baseline
const auto screenHeight = renderer.getScreenHeight();
const auto textY = screenHeight - orientedMarginBottom - 4;
const int contentBottom = screenHeight - orientedMarginBottom;
const int lineY = contentBottom + contentGap;
const int textY = lineY + lineToText;
renderer.drawLine(orientedMarginLeft, lineY, renderer.getScreenWidth() - orientedMarginRight, lineY);
int progressTextWidth = 0;
if (showProgress) {

View File

@ -8,16 +8,17 @@
namespace {
// Time threshold for treating a long press as a page-up/page-down
constexpr int SKIP_PAGE_MS = 700;
constexpr int headerY = 16;
constexpr int separatorY = 42;
constexpr int listStartY = 54;
constexpr int rowHeight = 28;
constexpr int horizontalMargin = 16;
} // namespace
int EpubReaderChapterSelectionActivity::getPageItems() const {
// Layout constants used in renderScreen
constexpr int startY = 60;
constexpr int lineHeight = 30;
const int screenHeight = renderer.getScreenHeight();
const int availableHeight = screenHeight - startY;
int items = availableHeight / lineHeight;
const int availableHeight = screenHeight - listStartY;
int items = availableHeight / rowHeight;
// Ensure we always have at least one item per page to avoid division by zero
if (items < 1) {
@ -121,17 +122,25 @@ void EpubReaderChapterSelectionActivity::renderScreen() {
const auto pageWidth = renderer.getScreenWidth();
const int pageItems = getPageItems();
const std::string title =
renderer.truncatedText(UI_12_FONT_ID, epub->getTitle().c_str(), pageWidth - 40, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_12_FONT_ID, 15, title.c_str(), true, EpdFontFamily::BOLD);
// Draw header with book title
const std::string title = renderer.truncatedText(UI_12_FONT_ID, epub->getTitle().c_str(),
pageWidth - horizontalMargin * 2, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_12_FONT_ID, headerY, title.c_str(), true, EpdFontFamily::BOLD);
// Subtle separator line under header
renderer.drawLine(horizontalMargin, separatorY, pageWidth - horizontalMargin, separatorY);
// Draw selection highlight
const auto pageStartIndex = selectorIndex / pageItems * pageItems;
renderer.fillRect(0, 60 + (selectorIndex % pageItems) * 30 - 2, pageWidth - 1, 30);
renderer.fillRect(0, listStartY + (selectorIndex % pageItems) * rowHeight - 2, pageWidth - 1, rowHeight);
// Draw chapter list
for (int tocIndex = pageStartIndex; tocIndex < epub->getTocItemsCount() && tocIndex < pageStartIndex + pageItems;
tocIndex++) {
auto item = epub->getTocItem(tocIndex);
renderer.drawText(UI_10_FONT_ID, 20 + (item.level - 1) * 15, 60 + (tocIndex % pageItems) * 30, item.title.c_str(),
tocIndex != selectorIndex);
const int indentPx = (item.level - 1) * 12;
renderer.drawText(UI_10_FONT_ID, horizontalMargin + 4 + indentPx, listStartY + (tocIndex % pageItems) * rowHeight,
item.title.c_str(), tocIndex != selectorIndex);
}
renderer.displayBuffer();

View File

@ -7,9 +7,14 @@
#include "fontIds.h"
namespace {
constexpr int PAGE_ITEMS = 23;
constexpr int PAGE_ITEMS = 20;
constexpr int SKIP_PAGE_MS = 700;
constexpr unsigned long GO_HOME_MS = 1000;
constexpr int headerY = 16;
constexpr int separatorY = 42;
constexpr int listStartY = 54;
constexpr int rowHeight = 28;
constexpr int horizontalMargin = 16;
} // namespace
void sortFileList(std::vector<std::string>& strs) {
@ -173,23 +178,29 @@ void FileSelectionActivity::render() const {
renderer.clearScreen();
const auto pageWidth = renderer.getScreenWidth();
renderer.drawCenteredText(UI_12_FONT_ID, 15, "Books", true, EpdFontFamily::BOLD);
// Draw header
renderer.drawCenteredText(UI_12_FONT_ID, headerY, "Books", true, EpdFontFamily::BOLD);
// Subtle separator line under header
renderer.drawLine(horizontalMargin, separatorY, pageWidth - horizontalMargin, separatorY);
// Help text
const auto labels = mappedInput.mapLabels("« Home", "Open", "", "");
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
if (files.empty()) {
renderer.drawText(UI_10_FONT_ID, 20, 60, "No books found");
renderer.drawText(UI_10_FONT_ID, horizontalMargin + 4, listStartY, "No books found");
renderer.displayBuffer();
return;
}
const auto pageStartIndex = selectorIndex / PAGE_ITEMS * PAGE_ITEMS;
renderer.fillRect(0, 60 + (selectorIndex % PAGE_ITEMS) * 30 - 2, pageWidth - 1, 30);
renderer.fillRect(0, listStartY + (selectorIndex % PAGE_ITEMS) * rowHeight - 2, pageWidth - 1, rowHeight);
for (int i = pageStartIndex; i < files.size() && i < pageStartIndex + PAGE_ITEMS; i++) {
auto item = renderer.truncatedText(UI_10_FONT_ID, files[i].c_str(), renderer.getScreenWidth() - 40);
renderer.drawText(UI_10_FONT_ID, 20, 60 + (i % PAGE_ITEMS) * 30, item.c_str(), i != selectorIndex);
auto item = renderer.truncatedText(UI_10_FONT_ID, files[i].c_str(), pageWidth - horizontalMargin * 2 - 8);
renderer.drawText(UI_10_FONT_ID, horizontalMargin + 4, listStartY + (i % PAGE_ITEMS) * rowHeight, item.c_str(),
i != selectorIndex);
}
renderer.displayBuffer();

View File

@ -9,7 +9,7 @@
// Define the static settings list
namespace {
constexpr int settingsCount = 12;
constexpr int settingsCount = 13;
const SettingInfo settingsList[settingsCount] = {
// Should match with SLEEP_SCREEN_MODE
{"Sleep Screen", SettingType::ENUM, &CrossPointSettings::sleepScreen, {"Dark", "Light", "Custom", "Cover"}},
@ -34,7 +34,14 @@ const SettingInfo settingsList[settingsCount] = {
{"Bookerly", "Noto Sans", "Open Dyslexic"}},
{"Reader Font Size", SettingType::ENUM, &CrossPointSettings::fontSize, {"Small", "Medium", "Large", "X Large"}},
{"Reader Line Spacing", SettingType::ENUM, &CrossPointSettings::lineSpacing, {"Tight", "Normal", "Wide"}},
{"Time to Sleep", SettingType::ENUM, &CrossPointSettings::sleepTimeout, {"1 min", "5 min", "10 min", "15 min", "30 min"}},
{"Time to Sleep",
SettingType::ENUM,
&CrossPointSettings::sleepTimeout,
{"1 min", "5 min", "10 min", "15 min", "30 min"}},
{"Refresh Frequency",
SettingType::ENUM,
&CrossPointSettings::refreshFrequency,
{"1 page", "5 pages", "10 pages", "15 pages", "30 pages"}},
{"Check for updates", SettingType::ACTION, nullptr, {}},
};
} // namespace
@ -163,18 +170,28 @@ void SettingsActivity::render() const {
const auto pageWidth = renderer.getScreenWidth();
const auto pageHeight = renderer.getScreenHeight();
// Draw header
renderer.drawCenteredText(UI_12_FONT_ID, 15, "Settings", true, EpdFontFamily::BOLD);
// Layout constants
constexpr int headerY = 16;
constexpr int separatorY = 42;
constexpr int listStartY = 54;
constexpr int rowHeight = 28;
constexpr int horizontalMargin = 16;
// Draw selection
renderer.fillRect(0, 60 + selectedSettingIndex * 30 - 2, pageWidth - 1, 30);
// Draw header
renderer.drawCenteredText(UI_12_FONT_ID, headerY, "Settings", true, EpdFontFamily::BOLD);
// Subtle separator line under header
renderer.drawLine(horizontalMargin, separatorY, pageWidth - horizontalMargin, separatorY);
// Draw selection highlight
renderer.fillRect(0, listStartY + selectedSettingIndex * rowHeight - 2, pageWidth - 1, rowHeight);
// Draw all settings
for (int i = 0; i < settingsCount; i++) {
const int settingY = 60 + i * 30; // 30 pixels between settings
const int settingY = listStartY + i * rowHeight;
// Draw setting name
renderer.drawText(UI_10_FONT_ID, 20, settingY, settingsList[i].name, i != selectedSettingIndex);
renderer.drawText(UI_10_FONT_ID, horizontalMargin + 4, settingY, settingsList[i].name, i != selectedSettingIndex);
// Draw value based on setting type
std::string valueText = "";
@ -186,12 +203,13 @@ void SettingsActivity::render() const {
valueText = settingsList[i].enumValues[value];
}
const auto width = renderer.getTextWidth(UI_10_FONT_ID, valueText.c_str());
renderer.drawText(UI_10_FONT_ID, pageWidth - 20 - width, settingY, valueText.c_str(), i != selectedSettingIndex);
renderer.drawText(UI_10_FONT_ID, pageWidth - horizontalMargin - 4 - width, settingY, valueText.c_str(),
i != selectedSettingIndex);
}
// Draw version text above button hints
renderer.drawText(SMALL_FONT_ID, pageWidth - 20 - renderer.getTextWidth(SMALL_FONT_ID, CROSSPOINT_VERSION),
pageHeight - 60, CROSSPOINT_VERSION);
renderer.drawText(SMALL_FONT_ID, pageWidth - 16 - renderer.getTextWidth(SMALL_FONT_ID, CROSSPOINT_VERSION),
pageHeight - 58, CROSSPOINT_VERSION);
// Draw help text
const auto labels = mappedInput.mapLabels("« Save", "Toggle", "", "");