Compare commits

..

1 Commits

Author SHA1 Message Date
Jonas Diemer
a7fb41a3ac
Merge 077526a126 into 3ce11f14ce 2026-01-21 16:25:36 +01:00
16 changed files with 40 additions and 81 deletions

1
.gitignore vendored
View File

@ -4,6 +4,5 @@
.vscode
lib/EpdFont/fontsrc
*.generated.h
.vs
build
**/__pycache__/

View File

@ -116,7 +116,6 @@ The Settings screen allows you to configure the device's behavior. There are a f
- Back, Confirm, Left, Right (default)
- Left, Right, Back, Confirm
- Left, Back, Confirm, Right
- Back, Confirm, Right, Left
- **Side Button Layout (reader)**: Swap the order of the up and down volume buttons from Previous/Next to Next/Previous. This change is only in effect when reading.
- **Long-press Chapter Skip**: Set whether long-pressing page turn buttons skip to the next/previous chapter.
- "Chapter Skip" (default) - Long-pressing skips to next/previous chapter

View File

@ -382,7 +382,7 @@ bool Epub::generateCoverBmp(bool cropped) const {
if (coverImageHref.substr(coverImageHref.length() - 4) == ".jpg" ||
coverImageHref.substr(coverImageHref.length() - 5) == ".jpeg") {
Serial.printf("[%lu] [EBP] Generating BMP from JPG cover image (%s mode)\n", millis(), cropped ? "cropped" : "fit");
Serial.printf("[%lu] [EBP] Generating BMP from JPG cover image\n", millis());
const auto coverJpgTempPath = getCachePath() + "/.cover.jpg";
FsFile coverJpg;
@ -401,7 +401,7 @@ bool Epub::generateCoverBmp(bool cropped) const {
coverJpg.close();
return false;
}
const bool success = JpegToBmpConverter::jpegFileToBmpStream(coverJpg, coverBmp, cropped);
const bool success = JpegToBmpConverter::jpegFileToBmpStream(coverJpg, coverBmp);
coverJpg.close();
coverBmp.close();
SdMan.remove(coverJpgTempPath.c_str());

View File

@ -125,8 +125,6 @@ bool isExplicitHyphen(const uint32_t cp) {
case 0xFE58: // small em dash
case 0xFE63: // small hyphen-minus
case 0xFF0D: // fullwidth hyphen-minus
case 0x005F: // Underscore
case 0x2026: // Ellipsis
return true;
default:
return false;

View File

@ -200,7 +200,7 @@ unsigned char JpegToBmpConverter::jpegReadCallback(unsigned char* pBuf, const un
// Internal implementation with configurable target size and bit depth
bool JpegToBmpConverter::jpegFileToBmpStreamInternal(FsFile& jpegFile, Print& bmpOut, int targetWidth, int targetHeight,
bool oneBit, bool crop) {
bool oneBit) {
Serial.printf("[%lu] [JPG] Converting JPEG to %s BMP (target: %dx%d)\n", millis(), oneBit ? "1-bit" : "2-bit",
targetWidth, targetHeight);
@ -242,12 +242,8 @@ bool JpegToBmpConverter::jpegFileToBmpStreamInternal(FsFile& jpegFile, Print& bm
const float scaleToFitWidth = static_cast<float>(targetWidth) / imageInfo.m_width;
const float scaleToFitHeight = static_cast<float>(targetHeight) / imageInfo.m_height;
// We scale to the smaller dimension, so we can potentially crop later.
float scale = 1.0;
if (crop) { // if we will crop, scale to the smaller dimension
scale = (scaleToFitWidth > scaleToFitHeight) ? scaleToFitWidth : scaleToFitHeight;
} else { // else, scale to the larger dimension to fit
scale = (scaleToFitWidth < scaleToFitHeight) ? scaleToFitWidth : scaleToFitHeight;
}
// TODO: ideally, we already crop here.
const float scale = (scaleToFitWidth > scaleToFitHeight) ? scaleToFitWidth : scaleToFitHeight;
outWidth = static_cast<int>(imageInfo.m_width * scale);
outHeight = static_cast<int>(imageInfo.m_height * scale);
@ -554,8 +550,8 @@ bool JpegToBmpConverter::jpegFileToBmpStreamInternal(FsFile& jpegFile, Print& bm
}
// Core function: Convert JPEG file to 2-bit BMP (uses default target size)
bool JpegToBmpConverter::jpegFileToBmpStream(FsFile& jpegFile, Print& bmpOut, bool crop) {
return jpegFileToBmpStreamInternal(jpegFile, bmpOut, TARGET_MAX_WIDTH, TARGET_MAX_HEIGHT, false, crop);
bool JpegToBmpConverter::jpegFileToBmpStream(FsFile& jpegFile, Print& bmpOut) {
return jpegFileToBmpStreamInternal(jpegFile, bmpOut, TARGET_MAX_WIDTH, TARGET_MAX_HEIGHT, false);
}
// Convert with custom target size (for thumbnails, 2-bit)

View File

@ -8,10 +8,10 @@ class JpegToBmpConverter {
static unsigned char jpegReadCallback(unsigned char* pBuf, unsigned char buf_size,
unsigned char* pBytes_actually_read, void* pCallback_data);
static bool jpegFileToBmpStreamInternal(class FsFile& jpegFile, Print& bmpOut, int targetWidth, int targetHeight,
bool oneBit, bool crop = true);
bool oneBit);
public:
static bool jpegFileToBmpStream(FsFile& jpegFile, Print& bmpOut, bool crop = true);
static bool jpegFileToBmpStream(FsFile& jpegFile, Print& bmpOut);
// Convert with custom target size (for thumbnails)
static bool jpegFileToBmpStreamWithSize(FsFile& jpegFile, Print& bmpOut, int targetMaxWidth, int targetMaxHeight);
// Convert to 1-bit BMP (black and white only, no grays) for fast home screen rendering

View File

@ -32,12 +32,7 @@ class CrossPointSettings {
// Front button layout options
// Default: Back, Confirm, Left, Right
// Swapped: Left, Right, Back, Confirm
enum FRONT_BUTTON_LAYOUT {
BACK_CONFIRM_LEFT_RIGHT = 0,
LEFT_RIGHT_BACK_CONFIRM = 1,
LEFT_BACK_CONFIRM_RIGHT = 2,
BACK_CONFIRM_RIGHT_LEFT = 3
};
enum FRONT_BUTTON_LAYOUT { BACK_CONFIRM_LEFT_RIGHT = 0, LEFT_RIGHT_BACK_CONFIRM = 1, LEFT_BACK_CONFIRM_RIGHT = 2 };
// Side button layout options
// Default: Previous, Next

View File

@ -14,9 +14,6 @@ decltype(InputManager::BTN_BACK) MappedInputManager::mapButton(const Button butt
case CrossPointSettings::LEFT_BACK_CONFIRM_RIGHT:
return InputManager::BTN_CONFIRM;
case CrossPointSettings::BACK_CONFIRM_LEFT_RIGHT:
/* fall through */
case CrossPointSettings::BACK_CONFIRM_RIGHT_LEFT:
/* fall through */
default:
return InputManager::BTN_BACK;
}
@ -27,22 +24,15 @@ decltype(InputManager::BTN_BACK) MappedInputManager::mapButton(const Button butt
case CrossPointSettings::LEFT_BACK_CONFIRM_RIGHT:
return InputManager::BTN_LEFT;
case CrossPointSettings::BACK_CONFIRM_LEFT_RIGHT:
/* fall through */
case CrossPointSettings::BACK_CONFIRM_RIGHT_LEFT:
/* fall through */
default:
return InputManager::BTN_CONFIRM;
}
case Button::Left:
switch (frontLayout) {
case CrossPointSettings::LEFT_RIGHT_BACK_CONFIRM:
/* fall through */
case CrossPointSettings::LEFT_BACK_CONFIRM_RIGHT:
return InputManager::BTN_BACK;
case CrossPointSettings::BACK_CONFIRM_RIGHT_LEFT:
return InputManager::BTN_RIGHT;
case CrossPointSettings::BACK_CONFIRM_LEFT_RIGHT:
/* fall through */
default:
return InputManager::BTN_LEFT;
}
@ -50,12 +40,8 @@ decltype(InputManager::BTN_BACK) MappedInputManager::mapButton(const Button butt
switch (frontLayout) {
case CrossPointSettings::LEFT_RIGHT_BACK_CONFIRM:
return InputManager::BTN_CONFIRM;
case CrossPointSettings::BACK_CONFIRM_RIGHT_LEFT:
return InputManager::BTN_LEFT;
case CrossPointSettings::BACK_CONFIRM_LEFT_RIGHT:
/* fall through */
case CrossPointSettings::LEFT_BACK_CONFIRM_RIGHT:
/* fall through */
default:
return InputManager::BTN_RIGHT;
}
@ -70,7 +56,6 @@ decltype(InputManager::BTN_BACK) MappedInputManager::mapButton(const Button butt
case CrossPointSettings::NEXT_PREV:
return InputManager::BTN_DOWN;
case CrossPointSettings::PREV_NEXT:
/* fall through */
default:
return InputManager::BTN_UP;
}
@ -79,7 +64,6 @@ decltype(InputManager::BTN_BACK) MappedInputManager::mapButton(const Button butt
case CrossPointSettings::NEXT_PREV:
return InputManager::BTN_UP;
case CrossPointSettings::PREV_NEXT:
/* fall through */
default:
return InputManager::BTN_DOWN;
}

View File

@ -261,7 +261,6 @@ void SleepActivity::renderCoverSleepScreen() const {
if (SdMan.openFileForRead("SLP", coverBmpPath, file)) {
Bitmap bitmap(file);
if (bitmap.parseHeaders() == BmpReaderError::Ok) {
Serial.printf("[SLP] Rendering sleep cover: %s\n", coverBmpPath);
renderBitmapSleepScreen(bitmap);
return;
}

View File

@ -120,8 +120,7 @@ void MyLibraryActivity::loadFiles() {
} else {
auto filename = std::string(name);
if (StringUtils::checkFileExtension(filename, ".epub") || StringUtils::checkFileExtension(filename, ".xtch") ||
StringUtils::checkFileExtension(filename, ".xtc") || StringUtils::checkFileExtension(filename, ".txt") ||
StringUtils::checkFileExtension(filename, ".md")) {
StringUtils::checkFileExtension(filename, ".xtc") || StringUtils::checkFileExtension(filename, ".txt")) {
files.emplace_back(filename);
}
}

View File

@ -354,8 +354,8 @@ void WifiSelectionActivity::loop() {
updateRequired = true;
}
} else if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
if (forgetPromptSelection == 1) {
// User chose "Forget network" - forget the network
if (forgetPromptSelection == 0) {
// User chose "Yes" - forget the network
xSemaphoreTake(renderingMutex, portMAX_DELAY);
WIFI_STORE.removeCredential(selectedSSID);
xSemaphoreGive(renderingMutex);
@ -366,7 +366,7 @@ void WifiSelectionActivity::loop() {
network->hasSavedPassword = false;
}
}
// Go back to network list (whether Cancel or Forget network was selected)
// Go back to network list
state = WifiSelectionState::NETWORK_LIST;
updateRequired = true;
} else if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
@ -391,7 +391,7 @@ void WifiSelectionActivity::loop() {
// If we used saved credentials, offer to forget the network
if (usedSavedPassword) {
state = WifiSelectionState::FORGET_PROMPT;
forgetPromptSelection = 0; // Default to "Cancel"
forgetPromptSelection = 0; // Default to "Yes"
} else {
// Go back to network list on failure
state = WifiSelectionState::NETWORK_LIST;
@ -623,9 +623,7 @@ void WifiSelectionActivity::renderConnected() const {
const std::string ipInfo = "IP Address: " + connectedIP;
renderer.drawCenteredText(UI_10_FONT_ID, top + 40, ipInfo.c_str());
// Use centralized button hints
const auto labels = mappedInput.mapLabels("", "Continue", "", "");
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.drawCenteredText(SMALL_FONT_ID, pageHeight - 30, "Press any button to continue");
}
void WifiSelectionActivity::renderSavePrompt() const {
@ -665,9 +663,7 @@ void WifiSelectionActivity::renderSavePrompt() const {
renderer.drawText(UI_10_FONT_ID, startX + buttonWidth + buttonSpacing + 4, buttonY, "No");
}
// Use centralized button hints
const auto labels = mappedInput.mapLabels("« Skip", "Select", "Left", "Right");
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.drawCenteredText(SMALL_FONT_ID, pageHeight - 30, "LEFT/RIGHT: Select | OK: Confirm");
}
void WifiSelectionActivity::renderConnectionFailed() const {
@ -677,10 +673,7 @@ void WifiSelectionActivity::renderConnectionFailed() const {
renderer.drawCenteredText(UI_12_FONT_ID, top - 20, "Connection Failed", true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_10_FONT_ID, top + 20, connectionError.c_str());
// Use centralized button hints
const auto labels = mappedInput.mapLabels("« Back", "Continue", "", "");
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.drawCenteredText(SMALL_FONT_ID, pageHeight - 30, "Press any button to continue");
}
void WifiSelectionActivity::renderForgetPrompt() const {
@ -699,28 +692,26 @@ void WifiSelectionActivity::renderForgetPrompt() const {
renderer.drawCenteredText(UI_10_FONT_ID, top + 40, "Remove saved password?");
// Draw Cancel/Forget network buttons
// Draw Yes/No buttons
const int buttonY = top + 80;
constexpr int buttonWidth = 120;
constexpr int buttonWidth = 60;
constexpr int buttonSpacing = 30;
constexpr int totalWidth = buttonWidth * 2 + buttonSpacing;
const int startX = (pageWidth - totalWidth) / 2;
// Draw "Cancel" button
// Draw "Yes" button
if (forgetPromptSelection == 0) {
renderer.drawText(UI_10_FONT_ID, startX, buttonY, "[Cancel]");
renderer.drawText(UI_10_FONT_ID, startX, buttonY, "[Yes]");
} else {
renderer.drawText(UI_10_FONT_ID, startX + 4, buttonY, "Cancel");
renderer.drawText(UI_10_FONT_ID, startX + 4, buttonY, "Yes");
}
// Draw "Forget network" button
// Draw "No" button
if (forgetPromptSelection == 1) {
renderer.drawText(UI_10_FONT_ID, startX + buttonWidth + buttonSpacing, buttonY, "[Forget network]");
renderer.drawText(UI_10_FONT_ID, startX + buttonWidth + buttonSpacing, buttonY, "[No]");
} else {
renderer.drawText(UI_10_FONT_ID, startX + buttonWidth + buttonSpacing + 4, buttonY, "Forget network");
renderer.drawText(UI_10_FONT_ID, startX + buttonWidth + buttonSpacing + 4, buttonY, "No");
}
// Use centralized button hints
const auto labels = mappedInput.mapLabels("« Back", "Select", "Left", "Right");
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.drawCenteredText(SMALL_FONT_ID, pageHeight - 30, "LEFT/RIGHT: Select | OK: Confirm");
}

View File

@ -448,7 +448,7 @@ void EpubReaderActivity::renderStatusBar(const int orientedMarginRight, const in
// Right aligned text for progress counter
char progressStr[32];
snprintf(progressStr, sizeof(progressStr), "%d/%d %.0f%%", section->currentPage + 1, section->pageCount,
snprintf(progressStr, sizeof(progressStr), "%d/%d %.1f%%", section->currentPage + 1, section->pageCount,
bookProgress);
const std::string progress = progressStr;
progressTextWidth = renderer.getTextWidth(SMALL_FONT_ID, progress.c_str());

View File

@ -188,23 +188,22 @@ void EpubReaderChapterSelectionActivity::renderScreen() {
const auto pageStartIndex = selectorIndex / pageItems * pageItems;
renderer.fillRect(0, 60 + (selectorIndex % pageItems) * 30 - 2, pageWidth - 1, 30);
for (int i = 0; i < pageItems; i++) {
int itemIndex = pageStartIndex + i;
if (itemIndex >= totalItems) break;
const int displayY = 60 + i * 30;
for (int itemIndex = pageStartIndex; itemIndex < totalItems && itemIndex < pageStartIndex + pageItems; itemIndex++) {
const int displayY = 60 + (itemIndex % pageItems) * 30;
const bool isSelected = (itemIndex == selectorIndex);
if (isSyncItem(itemIndex)) {
// Draw sync option (at top or bottom)
renderer.drawText(UI_10_FONT_ID, 20, displayY, ">> Sync Progress", !isSelected);
} else {
// Draw TOC item (account for top sync offset)
const int tocIndex = tocIndexFromItemIndex(itemIndex);
auto item = epub->getTocItem(tocIndex);
const int indentSize = 20 + (item.level - 1) * 15;
const std::string chapterName =
renderer.truncatedText(UI_10_FONT_ID, item.title.c_str(), pageWidth - 40 - indentSize);
renderer.drawText(UI_10_FONT_ID, indentSize, displayY, chapterName.c_str(), !isSelected);
renderer.drawText(UI_10_FONT_ID, indentSize, 60 + (tocIndex % pageItems) * 30, chapterName.c_str(),
tocIndex != selectorIndex);
}
}

View File

@ -22,8 +22,9 @@ bool ReaderActivity::isXtcFile(const std::string& path) {
}
bool ReaderActivity::isTxtFile(const std::string& path) {
return StringUtils::checkFileExtension(path, ".txt") ||
StringUtils::checkFileExtension(path, ".md"); // Treat .md as txt files (until we have a markdown reader)
if (path.length() < 4) return false;
std::string ext4 = path.substr(path.length() - 4);
return ext4 == ".txt" || ext4 == ".TXT";
}
std::unique_ptr<Epub> ReaderActivity::loadEpub(const std::string& path) {

View File

@ -194,7 +194,7 @@ void KOReaderSettingsActivity::render() {
} else if (i == 1) {
status = KOREADER_STORE.getPassword().empty() ? "[Not Set]" : "[Set]";
} else if (i == 2) {
status = KOREADER_STORE.getServerUrl().empty() ? "[Default]" : "[Custom]";
status = KOREADER_STORE.getServerUrl().empty() ? "[Not Set]" : "[Set]";
} else if (i == 3) {
status = KOREADER_STORE.getMatchMethod() == DocumentMatchMethod::FILENAME ? "[Filename]" : "[Binary]";
} else if (i == 4) {

View File

@ -37,9 +37,8 @@ const SettingInfo readerSettings[readerSettingsCount] = {
constexpr int controlsSettingsCount = 4;
const SettingInfo controlsSettings[controlsSettingsCount] = {
SettingInfo::Enum(
"Front Button Layout", &CrossPointSettings::frontButtonLayout,
{"Bck, Cnfrm, Lft, Rght", "Lft, Rght, Bck, Cnfrm", "Lft, Bck, Cnfrm, Rght", "Bck, Cnfrm, Rght, Lft"}),
SettingInfo::Enum("Front Button Layout", &CrossPointSettings::frontButtonLayout,
{"Bck, Cnfrm, Lft, Rght", "Lft, Rght, Bck, Cnfrm", "Lft, Bck, Cnfrm, Rght"}),
SettingInfo::Enum("Side Button Layout (reader)", &CrossPointSettings::sideButtonLayout,
{"Prev, Next", "Next, Prev"}),
SettingInfo::Toggle("Long-press Chapter Skip", &CrossPointSettings::longPressChapterSkip),