Merge master into feature/settings-categories

This commit is contained in:
dpoulter 2026-01-12 11:30:12 +01:00
commit 828de5f2c2
14 changed files with 64 additions and 22 deletions

View File

@ -67,6 +67,7 @@ The Settings screen allows you to configure the device's behavior. There are a f
- "Light" - The same default sleep screen, on a white background - "Light" - The same default sleep screen, on a white background
- "Custom" - Custom images from the SD card, see [Sleep Screen](#36-sleep-screen) below for more information - "Custom" - Custom images from the SD card, see [Sleep Screen](#36-sleep-screen) below for more information
- "Cover" - The book cover image (Note: this is experimental and may not work as expected) - "Cover" - The book cover image (Note: this is experimental and may not work as expected)
- "Blank" - A blank screen
- **Status Bar**: Configure the status bar displayed while reading: - **Status Bar**: Configure the status bar displayed while reading:
- "None" - No status bar - "None" - No status bar
- "No Progress" - Show status bar without reading progress - "No Progress" - Show status bar without reading progress

View File

@ -345,11 +345,14 @@ const std::string& Epub::getAuthor() const {
return bookMetadataCache->coreMetadata.author; return bookMetadataCache->coreMetadata.author;
} }
std::string Epub::getCoverBmpPath() const { return cachePath + "/cover.bmp"; } std::string Epub::getCoverBmpPath(bool cropped) const {
const auto coverFileName = "cover" + cropped ? "_crop" : "";
return cachePath + "/" + coverFileName + ".bmp";
}
bool Epub::generateCoverBmp() const { bool Epub::generateCoverBmp(bool cropped) const {
// Already generated, return true // Already generated, return true
if (SdMan.exists(getCoverBmpPath().c_str())) { if (SdMan.exists(getCoverBmpPath(cropped).c_str())) {
return true; return true;
} }
@ -381,7 +384,7 @@ bool Epub::generateCoverBmp() const {
} }
FsFile coverBmp; FsFile coverBmp;
if (!SdMan.openFileForWrite("EBP", getCoverBmpPath(), coverBmp)) { if (!SdMan.openFileForWrite("EBP", getCoverBmpPath(cropped), coverBmp)) {
coverJpg.close(); coverJpg.close();
return false; return false;
} }
@ -392,7 +395,7 @@ bool Epub::generateCoverBmp() const {
if (!success) { if (!success) {
Serial.printf("[%lu] [EBP] Failed to generate BMP from JPG cover image\n", millis()); Serial.printf("[%lu] [EBP] Failed to generate BMP from JPG cover image\n", millis());
SdMan.remove(getCoverBmpPath().c_str()); SdMan.remove(getCoverBmpPath(cropped).c_str());
} }
Serial.printf("[%lu] [EBP] Generated BMP from JPG cover image, success: %s\n", millis(), success ? "yes" : "no"); Serial.printf("[%lu] [EBP] Generated BMP from JPG cover image, success: %s\n", millis(), success ? "yes" : "no");
return success; return success;

View File

@ -44,8 +44,8 @@ class Epub {
const std::string& getPath() const; const std::string& getPath() const;
const std::string& getTitle() const; const std::string& getTitle() const;
const std::string& getAuthor() const; const std::string& getAuthor() const;
std::string getCoverBmpPath() const; std::string getCoverBmpPath(bool cropped = false) const;
bool generateCoverBmp() const; bool generateCoverBmp(bool cropped = false) const;
uint8_t* readItemContentsToBytes(const std::string& itemHref, size_t* size = nullptr, uint8_t* readItemContentsToBytes(const std::string& itemHref, size_t* size = nullptr,
bool trailingNullByte = false) const; bool trailingNullByte = false) const;
bool readItemContentsToStream(const std::string& itemHref, Print& out, size_t chunkSize) const; bool readItemContentsToStream(const std::string& itemHref, Print& out, size_t chunkSize) const;

View File

@ -46,6 +46,7 @@ bool CrossPointSettings::saveToFile() const {
serialization::writePod(outputFile, sleepScreenCoverMode); serialization::writePod(outputFile, sleepScreenCoverMode);
serialization::writeString(outputFile, std::string(opdsServerUrl)); serialization::writeString(outputFile, std::string(opdsServerUrl));
serialization::writePod(outputFile, textAntiAliasing); serialization::writePod(outputFile, textAntiAliasing);
serialization::writePod(outputFile, hideBatteryPercentage);
outputFile.close(); outputFile.close();
Serial.printf("[%lu] [CPS] Settings saved to file\n", millis()); Serial.printf("[%lu] [CPS] Settings saved to file\n", millis());
@ -110,6 +111,8 @@ bool CrossPointSettings::loadFromFile() {
} }
serialization::readPod(inputFile, textAntiAliasing); serialization::readPod(inputFile, textAntiAliasing);
if (++settingsRead >= fileSettingsCount) break; if (++settingsRead >= fileSettingsCount) break;
serialization::readPod(inputFile, hideBatteryPercentage);
if (++settingsRead >= fileSettingsCount) break;
} while (false); } while (false);
inputFile.close(); inputFile.close();

View File

@ -52,6 +52,12 @@ class CrossPointSettings {
// E-ink refresh frequency (pages between full refreshes) // 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 }; enum REFRESH_FREQUENCY { REFRESH_1 = 0, REFRESH_5 = 1, REFRESH_10 = 2, REFRESH_15 = 3, REFRESH_30 = 4 };
// Short power button press actions
enum SHORT_PWRBTN { IGNORE = 0, SLEEP = 1, PAGE_TURN = 2 };
// Hide battery percentage
enum HIDE_BATTERY_PERCENTAGE { HIDE_NEVER = 0, HIDE_READER = 1, HIDE_ALWAYS = 2 };
// Sleep screen settings // Sleep screen settings
uint8_t sleepScreen = DARK; uint8_t sleepScreen = DARK;
// Sleep screen cover mode settings // Sleep screen cover mode settings
@ -61,8 +67,8 @@ class CrossPointSettings {
// Text rendering settings // Text rendering settings
uint8_t extraParagraphSpacing = 1; uint8_t extraParagraphSpacing = 1;
uint8_t textAntiAliasing = 1; uint8_t textAntiAliasing = 1;
// Duration of the power button press // Short power button click behaviour
uint8_t shortPwrBtn = 0; uint8_t shortPwrBtn = IGNORE;
// EPUB reading orientation settings // EPUB reading orientation settings
// 0 = portrait (default), 1 = landscape clockwise, 2 = inverted, 3 = landscape counter-clockwise // 0 = portrait (default), 1 = landscape clockwise, 2 = inverted, 3 = landscape counter-clockwise
uint8_t orientation = PORTRAIT; uint8_t orientation = PORTRAIT;
@ -82,13 +88,17 @@ class CrossPointSettings {
uint8_t screenMargin = 5; uint8_t screenMargin = 5;
// OPDS browser settings // OPDS browser settings
char opdsServerUrl[128] = ""; char opdsServerUrl[128] = "";
// Hide battery percentage
uint8_t hideBatteryPercentage = HIDE_NEVER;
~CrossPointSettings() = default; ~CrossPointSettings() = default;
// Get singleton instance // Get singleton instance
static CrossPointSettings& getInstance() { return instance; } static CrossPointSettings& getInstance() { return instance; }
uint16_t getPowerButtonDuration() const { return shortPwrBtn ? 10 : 400; } uint16_t getPowerButtonDuration() const {
return (shortPwrBtn == CrossPointSettings::SHORT_PWRBTN::SLEEP) ? 10 : 400;
}
int getReaderFontId() const; int getReaderFontId() const;
bool saveToFile() const; bool saveToFile() const;

View File

@ -8,10 +8,11 @@
#include "Battery.h" #include "Battery.h"
#include "fontIds.h" #include "fontIds.h"
void ScreenComponents::drawBattery(const GfxRenderer& renderer, const int left, const int top) { void ScreenComponents::drawBattery(const GfxRenderer& renderer, const int left, const int top,
const bool showPercentage) {
// Left aligned battery icon and percentage // Left aligned battery icon and percentage
const uint16_t percentage = battery.readPercentage(); const uint16_t percentage = battery.readPercentage();
const auto percentageText = std::to_string(percentage) + "%"; const auto percentageText = showPercentage ? std::to_string(percentage) + "%" : "";
renderer.drawText(SMALL_FONT_ID, left + 20, top, percentageText.c_str()); renderer.drawText(SMALL_FONT_ID, left + 20, top, percentageText.c_str());
// 1 column on left, 2 columns on right, 5 columns of battery body // 1 column on left, 2 columns on right, 5 columns of battery body

View File

@ -7,7 +7,7 @@ class GfxRenderer;
class ScreenComponents { class ScreenComponents {
public: public:
static void drawBattery(const GfxRenderer& renderer, int left, int top); static void drawBattery(const GfxRenderer& renderer, int left, int top, bool showPercentage = true);
/** /**
* Draw a progress bar with percentage text. * Draw a progress bar with percentage text.

View File

@ -199,6 +199,7 @@ void SleepActivity::renderCoverSleepScreen() const {
} }
std::string coverBmpPath; std::string coverBmpPath;
bool cropped = SETTINGS.sleepScreenCoverMode == CrossPointSettings::SLEEP_SCREEN_COVER_MODE::CROP;
if (StringUtils::checkFileExtension(APP_STATE.openEpubPath, ".xtc") || if (StringUtils::checkFileExtension(APP_STATE.openEpubPath, ".xtc") ||
StringUtils::checkFileExtension(APP_STATE.openEpubPath, ".xtch")) { StringUtils::checkFileExtension(APP_STATE.openEpubPath, ".xtch")) {
@ -223,12 +224,12 @@ void SleepActivity::renderCoverSleepScreen() const {
return renderDefaultSleepScreen(); return renderDefaultSleepScreen();
} }
if (!lastEpub.generateCoverBmp()) { if (!lastEpub.generateCoverBmp(cropped)) {
Serial.println("[SLP] Failed to generate cover bmp"); Serial.println("[SLP] Failed to generate cover bmp");
return renderDefaultSleepScreen(); return renderDefaultSleepScreen();
} }
coverBmpPath = lastEpub.getCoverBmpPath(); coverBmpPath = lastEpub.getCoverBmpPath(cropped);
} else { } else {
return renderDefaultSleepScreen(); return renderDefaultSleepScreen();
} }

View File

@ -7,6 +7,7 @@
#include <cstring> #include <cstring>
#include <vector> #include <vector>
#include "Battery.h"
#include "CrossPointSettings.h" #include "CrossPointSettings.h"
#include "CrossPointState.h" #include "CrossPointState.h"
#include "MappedInputManager.h" #include "MappedInputManager.h"
@ -332,8 +333,13 @@ void HomeActivity::render() const {
const auto labels = mappedInput.mapLabels("", "Confirm", "Up", "Down"); const auto labels = mappedInput.mapLabels("", "Confirm", "Up", "Down");
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
const auto batteryX = pageWidth - 25 - renderer.getTextWidth(SMALL_FONT_ID, "100 %"); const bool showBatteryPercentage =
ScreenComponents::drawBattery(renderer, batteryX, 10); SETTINGS.hideBatteryPercentage != CrossPointSettings::HIDE_BATTERY_PERCENTAGE::HIDE_ALWAYS;
// get percentage so we can align text properly
const uint16_t percentage = battery.readPercentage();
const auto percentageText = showBatteryPercentage ? std::to_string(percentage) + "%" : "";
const auto batteryX = pageWidth - 25 - renderer.getTextWidth(SMALL_FONT_ID, percentageText.c_str());
ScreenComponents::drawBattery(renderer, batteryX, 10, showBatteryPercentage);
renderer.displayBuffer(); renderer.displayBuffer();
} }

View File

@ -152,6 +152,8 @@ void EpubReaderActivity::loop() {
const bool prevReleased = mappedInput.wasReleased(MappedInputManager::Button::PageBack) || const bool prevReleased = mappedInput.wasReleased(MappedInputManager::Button::PageBack) ||
mappedInput.wasReleased(MappedInputManager::Button::Left); mappedInput.wasReleased(MappedInputManager::Button::Left);
const bool nextReleased = mappedInput.wasReleased(MappedInputManager::Button::PageForward) || const bool nextReleased = mappedInput.wasReleased(MappedInputManager::Button::PageForward) ||
(SETTINGS.shortPwrBtn == CrossPointSettings::SHORT_PWRBTN::PAGE_TURN &&
mappedInput.wasReleased(MappedInputManager::Button::Power)) ||
mappedInput.wasReleased(MappedInputManager::Button::Right); mappedInput.wasReleased(MappedInputManager::Button::Right);
if (!prevReleased && !nextReleased) { if (!prevReleased && !nextReleased) {
@ -417,6 +419,8 @@ void EpubReaderActivity::renderStatusBar(const int orientedMarginRight, const in
SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL; SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL;
const bool showChapterTitle = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::NO_PROGRESS || const bool showChapterTitle = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::NO_PROGRESS ||
SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL; SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL;
const bool showBatteryPercentage =
SETTINGS.hideBatteryPercentage == CrossPointSettings::HIDE_BATTERY_PERCENTAGE::HIDE_NEVER;
// Position status bar near the bottom of the logical screen, regardless of orientation // Position status bar near the bottom of the logical screen, regardless of orientation
const auto screenHeight = renderer.getScreenHeight(); const auto screenHeight = renderer.getScreenHeight();
@ -437,7 +441,7 @@ void EpubReaderActivity::renderStatusBar(const int orientedMarginRight, const in
} }
if (showBattery) { if (showBattery) {
ScreenComponents::drawBattery(renderer, orientedMarginLeft + 1, textY); ScreenComponents::drawBattery(renderer, orientedMarginLeft + 1, textY, showBatteryPercentage);
} }
if (showChapterTitle) { if (showChapterTitle) {

View File

@ -16,7 +16,9 @@ int EpubReaderChapterSelectionActivity::getPageItems() const {
constexpr int lineHeight = 30; constexpr int lineHeight = 30;
const int screenHeight = renderer.getScreenHeight(); const int screenHeight = renderer.getScreenHeight();
const int availableHeight = screenHeight - startY; const int endY = screenHeight - lineHeight;
const int availableHeight = endY - startY;
int items = availableHeight / lineHeight; int items = availableHeight / lineHeight;
// Ensure we always have at least one item per page to avoid division by zero // Ensure we always have at least one item per page to avoid division by zero
@ -134,5 +136,8 @@ void EpubReaderChapterSelectionActivity::renderScreen() {
tocIndex != selectorIndex); tocIndex != selectorIndex);
} }
const auto labels = mappedInput.mapLabels("« Back", "Select", "Up", "Down");
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer(); renderer.displayBuffer();
} }

View File

@ -112,6 +112,8 @@ void XtcReaderActivity::loop() {
const bool prevReleased = mappedInput.wasReleased(MappedInputManager::Button::PageBack) || const bool prevReleased = mappedInput.wasReleased(MappedInputManager::Button::PageBack) ||
mappedInput.wasReleased(MappedInputManager::Button::Left); mappedInput.wasReleased(MappedInputManager::Button::Left);
const bool nextReleased = mappedInput.wasReleased(MappedInputManager::Button::PageForward) || const bool nextReleased = mappedInput.wasReleased(MappedInputManager::Button::PageForward) ||
(SETTINGS.shortPwrBtn == CrossPointSettings::SHORT_PWRBTN::PAGE_TURN &&
mappedInput.wasReleased(MappedInputManager::Button::Power)) ||
mappedInput.wasReleased(MappedInputManager::Button::Right); mappedInput.wasReleased(MappedInputManager::Button::Right);
if (!prevReleased && !nextReleased) { if (!prevReleased && !nextReleased) {

View File

@ -14,7 +14,9 @@ int XtcReaderChapterSelectionActivity::getPageItems() const {
constexpr int lineHeight = 30; constexpr int lineHeight = 30;
const int screenHeight = renderer.getScreenHeight(); const int screenHeight = renderer.getScreenHeight();
const int availableHeight = screenHeight - startY; const int endY = screenHeight - lineHeight;
const int availableHeight = endY - startY;
int items = availableHeight / lineHeight; int items = availableHeight / lineHeight;
if (items < 1) { if (items < 1) {
items = 1; items = 1;
@ -147,5 +149,8 @@ void XtcReaderChapterSelectionActivity::renderScreen() {
renderer.drawText(UI_10_FONT_ID, 20, 60 + (i % pageItems) * 30, title, i != selectorIndex); renderer.drawText(UI_10_FONT_ID, 20, 60 + (i % pageItems) * 30, title, i != selectorIndex);
} }
const auto labels = mappedInput.mapLabels("« Back", "Select", "Up", "Down");
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer(); renderer.displayBuffer();
} }

View File

@ -13,12 +13,13 @@
const char* SettingsActivity::categoryNames[categoryCount] = {"Display", "Reader", "Controls", "System"}; const char* SettingsActivity::categoryNames[categoryCount] = {"Display", "Reader", "Controls", "System"};
namespace { namespace {
constexpr int displaySettingsCount = 4; constexpr int displaySettingsCount = 5;
const SettingInfo displaySettings[displaySettingsCount] = { const SettingInfo displaySettings[displaySettingsCount] = {
// Should match with SLEEP_SCREEN_MODE // Should match with SLEEP_SCREEN_MODE
SettingInfo::Enum("Sleep Screen", &CrossPointSettings::sleepScreen, {"Dark", "Light", "Custom", "Cover", "None"}), SettingInfo::Enum("Sleep Screen", &CrossPointSettings::sleepScreen, {"Dark", "Light", "Custom", "Cover", "None"}),
SettingInfo::Enum("Sleep Screen Cover Mode", &CrossPointSettings::sleepScreenCoverMode, {"Fit", "Crop"}), SettingInfo::Enum("Sleep Screen Cover Mode", &CrossPointSettings::sleepScreenCoverMode, {"Fit", "Crop"}),
SettingInfo::Enum("Status Bar", &CrossPointSettings::statusBar, {"None", "No Progress", "Full"}), SettingInfo::Enum("Status Bar", &CrossPointSettings::statusBar, {"None", "No Progress", "Full"}),
SettingInfo::Enum("Hide Battery %", &CrossPointSettings::hideBatteryPercentage, {"Never", "In Reader", "Always"}),
SettingInfo::Enum("Refresh Frequency", &CrossPointSettings::refreshFrequency, SettingInfo::Enum("Refresh Frequency", &CrossPointSettings::refreshFrequency,
{"1 page", "5 pages", "10 pages", "15 pages", "30 pages"})}; {"1 page", "5 pages", "10 pages", "15 pages", "30 pages"})};
@ -41,7 +42,7 @@ const SettingInfo controlsSettings[controlsSettingsCount] = {
{"Bck, Cnfrm, Lft, Rght", "Lft, Rght, Bck, Cnfrm", "Lft, Bck, Cnfrm, Rght"}), {"Bck, Cnfrm, Lft, Rght", "Lft, Rght, Bck, Cnfrm", "Lft, Bck, Cnfrm, Rght"}),
SettingInfo::Enum("Side Button Layout (reader)", &CrossPointSettings::sideButtonLayout, SettingInfo::Enum("Side Button Layout (reader)", &CrossPointSettings::sideButtonLayout,
{"Prev, Next", "Next, Prev"}), {"Prev, Next", "Next, Prev"}),
SettingInfo::Toggle("Short Power Button Click", &CrossPointSettings::shortPwrBtn)}; SettingInfo::Enum("Short Power Button Click", &CrossPointSettings::shortPwrBtn, {"Ignore", "Sleep", "Page Turn"})};
constexpr int systemSettingsCount = 3; constexpr int systemSettingsCount = 3;
const SettingInfo systemSettings[systemSettingsCount] = { const SettingInfo systemSettings[systemSettingsCount] = {