fix: prevent Serial.printf from blocking when USB disconnected
All checks were successful
CI / build (push) Successful in 2m23s

On ESP32-C3 with USB CDC, Serial.printf() blocks indefinitely when USB
is not connected. This caused device freezes when booted without USB.

Solution: Call Serial.setTxTimeoutMs(0) after Serial.begin() to make
all Serial output non-blocking.

Also added if (Serial) guards to high-traffic logging paths in
EpubReaderActivity as belt-and-suspenders protection.

Includes documentation of the debugging process and Serial call inventory.

Also applies clang-format to fix pre-existing formatting issues.
This commit is contained in:
cottongin 2026-01-28 15:57:31 -05:00
parent f3075002c1
commit 4db384edb6
No known key found for this signature in database
GPG Key ID: 0ECC91FE4655C262
59 changed files with 989 additions and 621 deletions

View File

@ -33,5 +33,9 @@ jobs:
echo "clang-format not found, skipping format check"
fi
- name: Generate Dictionary Index
run: |
python3 scripts/generate_dict_index.py --zip dict-en-en.zip --output lib/StarDict/DictPrefixIndex.generated.h
- name: Build CrossPoint
run: pio run

View File

@ -21,6 +21,10 @@ jobs:
- name: Install PlatformIO Core
run: python3 -m pip install --upgrade platformio
- name: Generate Dictionary Index
run: |
python3 scripts/generate_dict_index.py --zip dict-en-en.zip --output lib/StarDict/DictPrefixIndex.generated.h
- name: Build CrossPoint
run: pio run -e gh_release

1
.gitignore vendored
View File

@ -11,6 +11,7 @@ build
**/__pycache__/
test/epubs/
CrossPoint-ef.md
Serial_print.code-search
# Gitea Actions runner config (contains credentials)
.runner

View File

@ -0,0 +1,70 @@
# Serial.printf Calls Without `if (Serial)` Guards
**Date:** 2026-01-28
**Status:** Informational (not blocking issues)
## Summary
The codebase contains **408 Serial print calls** across 27 files in `src/`. Of these, only **16 calls** (in 2 files) have explicit `if (Serial)` guards.
**This is not a problem** because `Serial.setTxTimeoutMs(0)` is called in `setup()` before any activity code runs, making all Serial output non-blocking globally.
## Protection Mechanism
In `src/main.cpp` (lines 467-468):
```cpp
Serial.begin(115200);
Serial.setTxTimeoutMs(0); // Non-blocking TX - critical for USB disconnect handling
```
This ensures that even without `if (Serial)` guards, Serial.printf calls will return immediately when USB is disconnected instead of blocking indefinitely.
## Files with `if (Serial)` Guards (16 calls)
| File | Protected Calls |
|------|-----------------|
| `src/activities/reader/EpubReaderActivity.cpp` | 15 |
| `src/main.cpp` | 1 |
## Files Without Guards (392 calls)
These calls are protected by `Serial.setTxTimeoutMs(0)` but don't have explicit guards:
| File | Unguarded Calls |
|------|-----------------|
| `src/network/CrossPointWebServer.cpp` | 106 |
| `src/activities/network/CrossPointWebServerActivity.cpp` | 49 |
| `src/activities/boot_sleep/SleepActivity.cpp` | 33 |
| `src/BookManager.cpp` | 25 |
| `src/activities/reader/TxtReaderActivity.cpp` | 20 |
| `src/activities/home/HomeActivity.cpp` | 16 |
| `src/network/OtaUpdater.cpp` | 16 |
| `src/util/Md5Utils.cpp` | 15 |
| `src/main.cpp` | 13 (plus 1 guarded) |
| `src/WifiCredentialStore.cpp` | 12 |
| `src/network/HttpDownloader.cpp` | 12 |
| `src/BookListStore.cpp` | 11 |
| `src/activities/network/WifiSelectionActivity.cpp` | 11 |
| `src/activities/settings/OtaUpdateActivity.cpp` | 9 |
| `src/activities/browser/OpdsBookBrowserActivity.cpp` | 9 |
| `src/activities/settings/ClearCacheActivity.cpp` | 7 |
| `src/BookmarkStore.cpp` | 6 |
| `src/RecentBooksStore.cpp` | 5 |
| `src/activities/reader/ReaderActivity.cpp` | 4 |
| `src/activities/Activity.h` | 3 |
| `src/CrossPointSettings.cpp` | 3 |
| `src/activities/network/CalibreConnectActivity.cpp` | 2 |
| `src/activities/home/ListViewActivity.cpp` | 2 |
| `src/activities/home/MyLibraryActivity.cpp` | 1 |
| `src/activities/dictionary/DictionarySearchActivity.cpp` | 1 |
| `src/CrossPointState.cpp` | 1 |
## Recommendation
No immediate action required. The global `Serial.setTxTimeoutMs(0)` protection is sufficient.
If desired, `if (Serial)` guards could be added to high-frequency logging paths for minor performance optimization (skipping format string processing), but this is low priority.
## Note on open-x4-sdk
The `open-x4-sdk` submodule also contains Serial calls (in `EInkDisplay.cpp`, `SDCardManager.cpp`). These are also protected by the global timeout setting since `Serial.begin()` and `setTxTimeoutMs()` are called before any SDK code executes.

View File

@ -0,0 +1,125 @@
# Serial Blocking Debug Session Summary
**Date:** 2026-01-28
**Issue:** Device freezes when booted without USB connected
**Resolution:** `Serial.setTxTimeoutMs(0)` - make Serial TX non-blocking
## Problem Description
During release preparation for ef-0.15.9, the device was discovered to freeze completely when:
1. Unplugged from USB
2. Powered on via power button
3. Book page displays, then device becomes unresponsive
4. No button presses register
The device worked perfectly when USB was connected.
## Investigation Process
### Initial Hypotheses Tested
Multiple hypotheses were systematically investigated:
1. **Hypothesis A-D:** Display/rendering mutex issues
- Added mutex logging to SD card
- Mutex operations completed successfully
- Ruled out as root cause
2. **Hypothesis E:** FreeRTOS task creation issues
- Task created and ran successfully
- First render completed normally
- Ruled out
3. **Hypothesis F-G:** Main loop execution
- Added loop counter logging to SD card
- **Key finding:** Main loop never started logging
- Setup() completed but loop() never executed meaningful work
4. **Hypothesis H-J:** Various timing and initialization issues
- Tested different delays and initialization orders
- No improvement
### Root Cause Discovery
The breakthrough came from analyzing the boot sequence:
1. `setup()` completes successfully
2. `EpubReaderActivity::onEnter()` runs and calls `Serial.printf()` to log progress
3. **Device hangs at Serial.printf() call**
On ESP32-C3 with USB CDC (USB serial), `Serial.printf()` blocks indefinitely waiting for the TX buffer to drain when USB is not connected. The default behavior expects a host to read the data.
### Evidence
- When USB connected: `Serial.printf()` returns immediately (data sent to host)
- When USB disconnected: `Serial.printf()` blocks forever waiting for TX buffer space
- The hang occurred specifically in `EpubReaderActivity.cpp` during progress logging
## Solution
### Primary Fix
Configure Serial to be non-blocking in `src/main.cpp`:
```cpp
// Always initialize Serial but make it non-blocking
Serial.begin(115200);
Serial.setTxTimeoutMs(0); // Non-blocking TX - critical for USB disconnect handling
```
`Serial.setTxTimeoutMs(0)` tells the ESP32 Arduino core to return immediately from Serial write operations if the buffer is full, rather than blocking.
### Secondary Protection (Belt and Suspenders)
Added `if (Serial)` guards to high-traffic Serial calls in `EpubReaderActivity.cpp`:
```cpp
if (Serial) Serial.printf("[%lu] [ERS] Loaded progress...\n", millis());
```
This provides an additional check before attempting to print, though it's not strictly necessary with the timeout set to 0.
## Files Changed
| File | Change |
|------|--------|
| `src/main.cpp` | Added `Serial.setTxTimeoutMs(0)` after `Serial.begin()` |
| `src/main.cpp` | Added `if (Serial)` guard to auto-sleep log |
| `src/main.cpp` | Added `if (Serial)` guard to max loop duration log |
| `src/activities/reader/EpubReaderActivity.cpp` | Added 16 `if (Serial)` guards |
## Verification
After applying the fix:
1. Device boots successfully when unplugged from USB
2. Book pages render correctly
3. Button presses register normally
4. Sleep/wake cycle works
5. No functionality lost when USB is connected
## Lessons Learned
1. **ESP32-C3 USB CDC behavior:** Serial output can block indefinitely without a connected host
2. **Always set non-blocking:** `Serial.setTxTimeoutMs(0)` should be standard for battery-powered devices
3. **Debug logging location matters:** When debugging hangs, SD card logging proved essential since Serial was the problem
4. **Systematic hypothesis testing:** Ruled out many red herrings (mutex, task, rendering) before finding the true cause
## Technical Details
### Why This Affects ESP32-C3 Specifically
The ESP32-C3 uses native USB CDC for serial communication (no external USB-UART chip). The Arduino core's default behavior is to wait for TX buffer space, which requires an active USB host connection.
### Alternative Approaches Considered
1. **Only initialize Serial when USB connected:** Partially implemented, but insufficient because USB can be disconnected after boot
2. **Add `if (Serial)` guards everywhere:** Too invasive (400+ calls)
3. **Disable Serial entirely:** Would lose debug output when USB connected
The chosen solution (`setTxTimeoutMs(0)`) provides the best balance: debug output works when USB is connected, device operates normally when disconnected.
## References
- ESP32 Arduino Core Serial documentation
- ESP-IDF USB CDC documentation
- FreeRTOS queue behavior (initial red herring investigation)

View File

@ -251,8 +251,8 @@ bool Epub::parseCssFiles() {
SdMan.remove(tmpCssPath.c_str());
}
Serial.printf("[%lu] [EBP] Loaded %zu CSS style rules from %zu files (~%zu bytes)\n", millis(), cssParser->ruleCount(),
cssFiles.size(), cssParser->estimateMemoryUsage());
Serial.printf("[%lu] [EBP] Loaded %zu CSS style rules from %zu files (~%zu bytes)\n", millis(),
cssParser->ruleCount(), cssFiles.size(), cssParser->estimateMemoryUsage());
return true;
}
@ -757,8 +757,8 @@ bool Epub::generateAllCovers(const std::function<void(int)>& progressCallback) c
SdMan.openFileForWrite("EBP", getCoverBmpPath(false), coverBmp)) {
const int targetWidth = 480;
const int targetHeight = (480 * jpegHeight) / jpegWidth;
const bool success =
JpegToBmpConverter::jpegFileToBmpStreamWithSize(coverJpg, coverBmp, targetWidth, targetHeight, makeSubProgress(50, 75));
const bool success = JpegToBmpConverter::jpegFileToBmpStreamWithSize(coverJpg, coverBmp, targetWidth,
targetHeight, makeSubProgress(50, 75));
coverJpg.close();
coverBmp.close();
if (!success) {
@ -776,8 +776,8 @@ bool Epub::generateAllCovers(const std::function<void(int)>& progressCallback) c
SdMan.openFileForWrite("EBP", getCoverBmpPath(true), coverBmp)) {
const int targetHeight = 800;
const int targetWidth = (800 * jpegWidth) / jpegHeight;
const bool success =
JpegToBmpConverter::jpegFileToBmpStreamWithSize(coverJpg, coverBmp, targetWidth, targetHeight, makeSubProgress(75, 100));
const bool success = JpegToBmpConverter::jpegFileToBmpStreamWithSize(coverJpg, coverBmp, targetWidth,
targetHeight, makeSubProgress(75, 100));
coverJpg.close();
coverBmp.close();
if (!success) {

View File

@ -202,8 +202,8 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
const uint32_t contentOffset = page->firstContentOffset;
const uint32_t filePos = this->onPageComplete(std::move(page));
lut.push_back({filePos, contentOffset});
}, progressFn,
epub->getCssParser());
},
progressFn, epub->getCssParser());
Hyphenator::setPreferredLanguage(epub->getLanguage());
success = visitor.parseAndBuildPages();

View File

@ -5,13 +5,9 @@
// Global high contrast mode flag
static bool g_highContrastMode = false;
void setHighContrastMode(bool enabled) {
g_highContrastMode = enabled;
}
void setHighContrastMode(bool enabled) { g_highContrastMode = enabled; }
bool isHighContrastMode() {
return g_highContrastMode;
}
bool isHighContrastMode() { return g_highContrastMode; }
// Brightness/Contrast adjustments:
constexpr bool USE_BRIGHTNESS = false; // true: apply brightness/gamma adjustments

View File

@ -327,7 +327,8 @@ void GfxRenderer::drawBitmap(const Bitmap& bitmap, const int x, const int y, con
// Calculate screen Y position
const int screenYStart = y + static_cast<int>(std::floor(logicalY * scale));
// For upscaling, calculate the end position for this source row
const int screenYEnd = isUpscaling ? (y + static_cast<int>(std::floor((logicalY + 1) * scale))) : (screenYStart + 1);
const int screenYEnd =
isUpscaling ? (y + static_cast<int>(std::floor((logicalY + 1) * scale))) : (screenYStart + 1);
// Draw to all Y positions this source row maps to (for upscaling, this fills gaps)
for (int screenY = screenYStart; screenY < screenYEnd; screenY++) {
@ -340,7 +341,8 @@ void GfxRenderer::drawBitmap(const Bitmap& bitmap, const int x, const int y, con
// Calculate screen X position
const int screenXStart = x + static_cast<int>(std::floor(srcX * scale));
// For upscaling, calculate the end position for this source pixel
const int screenXEnd = isUpscaling ? (x + static_cast<int>(std::floor((srcX + 1) * scale))) : (screenXStart + 1);
const int screenXEnd =
isUpscaling ? (x + static_cast<int>(std::floor((srcX + 1) * scale))) : (screenXStart + 1);
const uint8_t val = outputRow[bmpX / 4] >> (6 - ((bmpX * 2) % 8)) & 0x3;
@ -409,7 +411,8 @@ void GfxRenderer::drawBitmap1Bit(const Bitmap& bitmap, const int x, const int y,
// Calculate screen Y position
const int screenYStart = y + static_cast<int>(std::floor(logicalY * scale));
// For upscaling, calculate the end position for this source row
const int screenYEnd = isUpscaling ? (y + static_cast<int>(std::floor((logicalY + 1) * scale))) : (screenYStart + 1);
const int screenYEnd =
isUpscaling ? (y + static_cast<int>(std::floor((logicalY + 1) * scale))) : (screenYStart + 1);
// Draw to all Y positions this source row maps to (for upscaling, this fills gaps)
for (int screenY = screenYStart; screenY < screenYEnd; screenY++) {
@ -420,7 +423,8 @@ void GfxRenderer::drawBitmap1Bit(const Bitmap& bitmap, const int x, const int y,
// Calculate screen X position
const int screenXStart = x + static_cast<int>(std::floor(bmpX * scale));
// For upscaling, calculate the end position for this source pixel
const int screenXEnd = isUpscaling ? (x + static_cast<int>(std::floor((bmpX + 1) * scale))) : (screenXStart + 1);
const int screenXEnd =
isUpscaling ? (x + static_cast<int>(std::floor((bmpX + 1) * scale))) : (screenXStart + 1);
// Get 2-bit value (result of readNextRow quantization)
const uint8_t val = outputRow[bmpX / 4] >> (6 - ((bmpX * 2) % 8)) & 0x3;
@ -998,26 +1002,38 @@ int mapPhysicalToLogicalEdge(int bezelEdge, GfxRenderer::Orientation orientation
return bezelEdge;
case GfxRenderer::LandscapeClockwise:
switch (bezelEdge) {
case 0: return 2; // Physical bottom -> logical left
case 1: return 3; // Physical top -> logical right
case 2: return 1; // Physical left -> logical top
case 3: return 0; // Physical right -> logical bottom
case 0:
return 2; // Physical bottom -> logical left
case 1:
return 3; // Physical top -> logical right
case 2:
return 1; // Physical left -> logical top
case 3:
return 0; // Physical right -> logical bottom
}
break;
case GfxRenderer::PortraitInverted:
switch (bezelEdge) {
case 0: return 1; // Physical bottom -> logical top
case 1: return 0; // Physical top -> logical bottom
case 2: return 3; // Physical left -> logical right
case 3: return 2; // Physical right -> logical left
case 0:
return 1; // Physical bottom -> logical top
case 1:
return 0; // Physical top -> logical bottom
case 2:
return 3; // Physical left -> logical right
case 3:
return 2; // Physical right -> logical left
}
break;
case GfxRenderer::LandscapeCounterClockwise:
switch (bezelEdge) {
case 0: return 3; // Physical bottom -> logical right
case 1: return 2; // Physical top -> logical left
case 2: return 0; // Physical left -> logical bottom
case 3: return 1; // Physical right -> logical top
case 0:
return 3; // Physical bottom -> logical right
case 1:
return 2; // Physical top -> logical left
case 2:
return 0; // Physical left -> logical bottom
case 3:
return 1; // Physical right -> logical top
}
break;
}
@ -1074,23 +1090,34 @@ void GfxRenderer::getOrientedViewableTRBL(int* outTop, int* outRight, int* outBo
*outLeft = getViewableMarginLeft();
break;
case LandscapeClockwise:
*outTop = BASE_VIEWABLE_MARGIN_LEFT + (mapPhysicalToLogicalEdge(bezelEdge, orientation) == 1 ? bezelCompensation : 0);
*outRight = BASE_VIEWABLE_MARGIN_TOP + (mapPhysicalToLogicalEdge(bezelEdge, orientation) == 3 ? bezelCompensation : 0);
*outBottom = BASE_VIEWABLE_MARGIN_RIGHT + (mapPhysicalToLogicalEdge(bezelEdge, orientation) == 0 ? bezelCompensation : 0);
*outLeft = BASE_VIEWABLE_MARGIN_BOTTOM + (mapPhysicalToLogicalEdge(bezelEdge, orientation) == 2 ? bezelCompensation : 0);
*outTop =
BASE_VIEWABLE_MARGIN_LEFT + (mapPhysicalToLogicalEdge(bezelEdge, orientation) == 1 ? bezelCompensation : 0);
*outRight =
BASE_VIEWABLE_MARGIN_TOP + (mapPhysicalToLogicalEdge(bezelEdge, orientation) == 3 ? bezelCompensation : 0);
*outBottom =
BASE_VIEWABLE_MARGIN_RIGHT + (mapPhysicalToLogicalEdge(bezelEdge, orientation) == 0 ? bezelCompensation : 0);
*outLeft =
BASE_VIEWABLE_MARGIN_BOTTOM + (mapPhysicalToLogicalEdge(bezelEdge, orientation) == 2 ? bezelCompensation : 0);
break;
case PortraitInverted:
*outTop = BASE_VIEWABLE_MARGIN_BOTTOM + (mapPhysicalToLogicalEdge(bezelEdge, orientation) == 1 ? bezelCompensation : 0);
*outRight = BASE_VIEWABLE_MARGIN_LEFT + (mapPhysicalToLogicalEdge(bezelEdge, orientation) == 3 ? bezelCompensation : 0);
*outBottom = BASE_VIEWABLE_MARGIN_TOP + (mapPhysicalToLogicalEdge(bezelEdge, orientation) == 0 ? bezelCompensation : 0);
*outLeft = BASE_VIEWABLE_MARGIN_RIGHT + (mapPhysicalToLogicalEdge(bezelEdge, orientation) == 2 ? bezelCompensation : 0);
*outTop =
BASE_VIEWABLE_MARGIN_BOTTOM + (mapPhysicalToLogicalEdge(bezelEdge, orientation) == 1 ? bezelCompensation : 0);
*outRight =
BASE_VIEWABLE_MARGIN_LEFT + (mapPhysicalToLogicalEdge(bezelEdge, orientation) == 3 ? bezelCompensation : 0);
*outBottom =
BASE_VIEWABLE_MARGIN_TOP + (mapPhysicalToLogicalEdge(bezelEdge, orientation) == 0 ? bezelCompensation : 0);
*outLeft =
BASE_VIEWABLE_MARGIN_RIGHT + (mapPhysicalToLogicalEdge(bezelEdge, orientation) == 2 ? bezelCompensation : 0);
break;
case LandscapeCounterClockwise:
*outTop = BASE_VIEWABLE_MARGIN_RIGHT + (mapPhysicalToLogicalEdge(bezelEdge, orientation) == 1 ? bezelCompensation : 0);
*outRight = BASE_VIEWABLE_MARGIN_BOTTOM + (mapPhysicalToLogicalEdge(bezelEdge, orientation) == 3 ? bezelCompensation : 0);
*outBottom = BASE_VIEWABLE_MARGIN_LEFT + (mapPhysicalToLogicalEdge(bezelEdge, orientation) == 0 ? bezelCompensation : 0);
*outLeft = BASE_VIEWABLE_MARGIN_TOP + (mapPhysicalToLogicalEdge(bezelEdge, orientation) == 2 ? bezelCompensation : 0);
*outTop =
BASE_VIEWABLE_MARGIN_RIGHT + (mapPhysicalToLogicalEdge(bezelEdge, orientation) == 1 ? bezelCompensation : 0);
*outRight =
BASE_VIEWABLE_MARGIN_BOTTOM + (mapPhysicalToLogicalEdge(bezelEdge, orientation) == 3 ? bezelCompensation : 0);
*outBottom =
BASE_VIEWABLE_MARGIN_LEFT + (mapPhysicalToLogicalEdge(bezelEdge, orientation) == 0 ? bezelCompensation : 0);
*outLeft =
BASE_VIEWABLE_MARGIN_TOP + (mapPhysicalToLogicalEdge(bezelEdge, orientation) == 2 ? bezelCompensation : 0);
break;
}
}

View File

@ -94,10 +94,10 @@ class GfxRenderer {
// Handles current render mode (BW, GRAYSCALE_MSB, GRAYSCALE_LSB)
void fillRectGray(int x, int y, int width, int height, uint8_t grayLevel) const;
void drawImage(const uint8_t bitmap[], int x, int y, int width, int height) const;
void drawImageRotated(const uint8_t bitmap[], int x, int y, int width, int height,
ImageRotation rotation, bool invert = false) const;
void drawBitmap(const Bitmap& bitmap, int x, int y, int maxWidth, int maxHeight, float cropX = 0,
float cropY = 0, bool invert = false) const;
void drawImageRotated(const uint8_t bitmap[], int x, int y, int width, int height, ImageRotation rotation,
bool invert = false) const;
void drawBitmap(const Bitmap& bitmap, int x, int y, int maxWidth, int maxHeight, float cropX = 0, float cropY = 0,
bool invert = false) const;
void drawBitmap1Bit(const Bitmap& bitmap, int x, int y, int maxWidth, int maxHeight, bool invert = false) const;
void fillPolygon(const int* xPoints, const int* yPoints, int numPoints, bool state = true) const;

View File

@ -150,8 +150,7 @@ std::string DictHtmlParser::extractTagName(const std::string& html, size_t start
std::string tagName = html.substr(nameStart, pos - nameStart);
// Convert to lowercase
std::transform(tagName.begin(), tagName.end(), tagName.begin(),
[](unsigned char c) { return std::tolower(c); });
std::transform(tagName.begin(), tagName.end(), tagName.begin(), [](unsigned char c) { return std::tolower(c); });
return tagName;
}
@ -160,17 +159,11 @@ bool DictHtmlParser::isBlockTag(const std::string& tagName) {
tagName == "ol" || tagName == "ul" || tagName == "dt" || tagName == "dd" || tagName == "html";
}
bool DictHtmlParser::isBoldTag(const std::string& tagName) {
return tagName == "b" || tagName == "strong";
}
bool DictHtmlParser::isBoldTag(const std::string& tagName) { return tagName == "b" || tagName == "strong"; }
bool DictHtmlParser::isItalicTag(const std::string& tagName) {
return tagName == "i" || tagName == "em";
}
bool DictHtmlParser::isItalicTag(const std::string& tagName) { return tagName == "i" || tagName == "em"; }
bool DictHtmlParser::isUnderlineTag(const std::string& tagName) {
return tagName == "u" || tagName == "ins";
}
bool DictHtmlParser::isUnderlineTag(const std::string& tagName) { return tagName == "u" || tagName == "ins"; }
bool DictHtmlParser::isSuperscriptTag(const std::string& tagName) { return tagName == "sup"; }

View File

@ -588,8 +588,12 @@ static std::string decodeHtmlEntity(const std::string& html, size_t& i) {
const char* replacement;
};
static const EntityMapping entities[] = {
{"&nbsp;", " "}, {"&lt;", "<"}, {"&gt;", ">"},
{"&amp;", "&"}, {"&quot;", "\""}, {"&apos;", "'"},
{"&nbsp;", " "},
{"&lt;", "<"},
{"&gt;", ">"},
{"&amp;", "&"},
{"&quot;", "\""},
{"&apos;", "'"},
{"&mdash;", "\xe2\x80\x94"}, // —
{"&ndash;", "\xe2\x80\x93"}, //
{"&hellip;", "\xe2\x80\xa6"}, // …
@ -688,8 +692,8 @@ std::string StarDict::stripHtml(const std::string& html) {
// Extract tag name
size_t tagEnd = tagStart;
while (tagEnd < html.length() && !std::isspace(static_cast<unsigned char>(html[tagEnd])) &&
html[tagEnd] != '>' && html[tagEnd] != '/') {
while (tagEnd < html.length() && !std::isspace(static_cast<unsigned char>(html[tagEnd])) && html[tagEnd] != '>' &&
html[tagEnd] != '/') {
tagEnd++;
}

View File

@ -205,8 +205,8 @@ bool Txt::generateThumbBmp() const {
}
constexpr int THUMB_TARGET_WIDTH = 240;
constexpr int THUMB_TARGET_HEIGHT = 400;
const bool success =
JpegToBmpConverter::jpegFileTo1BitBmpStreamWithSize(coverJpg, thumbBmp, THUMB_TARGET_WIDTH, THUMB_TARGET_HEIGHT);
const bool success = JpegToBmpConverter::jpegFileTo1BitBmpStreamWithSize(coverJpg, thumbBmp, THUMB_TARGET_WIDTH,
THUMB_TARGET_HEIGHT);
coverJpg.close();
thumbBmp.close();
@ -276,8 +276,8 @@ bool Txt::generateMicroThumbBmp() const {
}
constexpr int MICRO_THUMB_TARGET_WIDTH = 45;
constexpr int MICRO_THUMB_TARGET_HEIGHT = 60;
const bool success = JpegToBmpConverter::jpegFileTo1BitBmpStreamWithSize(coverJpg, microThumbBmp,
MICRO_THUMB_TARGET_WIDTH, MICRO_THUMB_TARGET_HEIGHT);
const bool success = JpegToBmpConverter::jpegFileTo1BitBmpStreamWithSize(
coverJpg, microThumbBmp, MICRO_THUMB_TARGET_WIDTH, MICRO_THUMB_TARGET_HEIGHT);
coverJpg.close();
microThumbBmp.close();

View File

@ -2,7 +2,7 @@
default_envs = default
[crosspoint]
version = 0.15.0
version = ef-0.15.9
[base]
platform = espressif32 @ 6.12.0

View File

@ -219,9 +219,7 @@ bool BookListStore::listExists(const std::string& name) {
return SdMan.exists(path.c_str());
}
std::string BookListStore::getListPath(const std::string& name) {
return std::string(LISTS_DIR) + "/" + name + ".bin";
}
std::string BookListStore::getListPath(const std::string& name) { return std::string(LISTS_DIR) + "/" + name + ".bin"; }
int BookListStore::getBookCount(const std::string& name) {
const std::string path = getListPath(name);

View File

@ -81,5 +81,4 @@ class BookListStore {
* @return Book count, or -1 if list doesn't exist
*/
static int getBookCount(const std::string& name);
};

View File

@ -35,9 +35,7 @@ std::string BookManager::getExtension(const std::string& path) {
return ext;
}
size_t BookManager::computePathHash(const std::string& path) {
return std::hash<std::string>{}(path);
}
size_t BookManager::computePathHash(const std::string& path) { return std::hash<std::string>{}(path); }
std::string BookManager::getCachePrefix(const std::string& path) {
const std::string ext = getExtension(path);

View File

@ -23,8 +23,7 @@ std::string getCacheDir(const std::string& bookPath) {
if (StringUtils::checkFileExtension(bookPath, ".epub")) {
return "/.crosspoint/epub_" + std::to_string(hash);
} else if (StringUtils::checkFileExtension(bookPath, ".txt") ||
StringUtils::checkFileExtension(bookPath, ".TXT") ||
} else if (StringUtils::checkFileExtension(bookPath, ".txt") || StringUtils::checkFileExtension(bookPath, ".TXT") ||
StringUtils::checkFileExtension(bookPath, ".md")) {
return "/.crosspoint/txt_" + std::to_string(hash);
}
@ -54,8 +53,8 @@ bool BookmarkStore::addBookmark(const std::string& bookPath, const Bookmark& boo
});
if (it != bookmarks.end()) {
Serial.printf("[%lu] [BMS] Bookmark already exists at spine %u, offset %u\n",
millis(), bookmark.spineIndex, bookmark.contentOffset);
Serial.printf("[%lu] [BMS] Bookmark already exists at spine %u, offset %u\n", millis(), bookmark.spineIndex,
bookmark.contentOffset);
return false;
}
@ -96,9 +95,8 @@ bool BookmarkStore::isPageBookmarked(const std::string& bookPath, uint16_t spine
std::vector<Bookmark> bookmarks;
loadBookmarks(bookPath, bookmarks);
return std::any_of(bookmarks.begin(), bookmarks.end(), [&](const Bookmark& b) {
return b.spineIndex == spineIndex && b.contentOffset == contentOffset;
});
return std::any_of(bookmarks.begin(), bookmarks.end(),
[&](const Bookmark& b) { return b.spineIndex == spineIndex && b.contentOffset == contentOffset; });
}
int BookmarkStore::getBookmarkCount(const std::string& bookPath) {
@ -199,9 +197,8 @@ std::vector<BookmarkedBook> BookmarkStore::getBooksWithBookmarks() {
crosspoint.close();
// Sort by title
std::sort(result.begin(), result.end(), [](const BookmarkedBook& a, const BookmarkedBook& b) {
return a.title < b.title;
});
std::sort(result.begin(), result.end(),
[](const BookmarkedBook& a, const BookmarkedBook& b) { return a.title < b.title; });
return result;
}

View File

@ -26,7 +26,14 @@ class CrossPointSettings {
};
// Status bar display type enum
enum STATUS_BAR_MODE { NONE = 0, NO_PROGRESS = 1, FULL = 2, FULL_WITH_PROGRESS_BAR = 3, ONLY_PROGRESS_BAR = 4, STATUS_BAR_MODE_COUNT };
enum STATUS_BAR_MODE {
NONE = 0,
NO_PROGRESS = 1,
FULL = 2,
FULL_WITH_PROGRESS_BAR = 3,
ONLY_PROGRESS_BAR = 4,
STATUS_BAR_MODE_COUNT
};
enum ORIENTATION {
PORTRAIT = 0, // 480x800 logical coordinates (current default)

View File

@ -90,7 +90,8 @@ void ScreenComponents::drawBookProgressBar(const GfxRenderer& renderer, const si
renderer.fillRect(vieweableMarginLeft, progressBarY, barWidth, BOOK_PROGRESS_BAR_HEIGHT, true);
}
int ScreenComponents::drawTabBar(const GfxRenderer& renderer, const int y, const std::vector<TabInfo>& tabs, int selectedIndex, bool showCursor) {
int ScreenComponents::drawTabBar(const GfxRenderer& renderer, const int y, const std::vector<TabInfo>& tabs,
int selectedIndex, bool showCursor) {
constexpr int tabPadding = 20; // Horizontal padding between tabs
constexpr int leftMargin = 20; // Left margin for first tab
constexpr int rightMargin = 20; // Right margin
@ -120,7 +121,8 @@ int ScreenComponents::drawTabBar(const GfxRenderer& renderer, const int y, const
std::vector<int> tabWidths;
int totalWidth = 0;
for (const auto& tab : tabs) {
const int textWidth = renderer.getTextWidth(UI_12_FONT_ID, tab.label, tab.selected ? EpdFontFamily::BOLD : EpdFontFamily::REGULAR);
const int textWidth =
renderer.getTextWidth(UI_12_FONT_ID, tab.label, tab.selected ? EpdFontFamily::BOLD : EpdFontFamily::REGULAR);
tabWidths.push_back(textWidth);
totalWidth += textWidth;
}
@ -220,21 +222,20 @@ int ScreenComponents::drawTabBar(const GfxRenderer& renderer, const int y, const
for (int i = 0; i < triangleWidth; ++i) {
// Scale height based on position (0 at tip, full height at base)
const int lineHalfHeight = (triangleHeight * i) / (triangleWidth * 2);
renderer.drawLine(tipX + i, triangleCenterY - lineHalfHeight,
tipX + i, triangleCenterY + lineHalfHeight);
renderer.drawLine(tipX + i, triangleCenterY - lineHalfHeight, tipX + i, triangleCenterY + lineHalfHeight);
}
}
// Right overflow indicator (more content to the right) - thin triangle pointing right
if (scrollOffset < totalWidth - availableWidth) {
// Clear background behind indicator to hide any overlapping text
renderer.fillRect(screenWidth - bezelRight - overflowIndicatorWidth, y - 2, overflowIndicatorWidth, lineHeight + 4, false);
renderer.fillRect(screenWidth - bezelRight - overflowIndicatorWidth, y - 2, overflowIndicatorWidth,
lineHeight + 4, false);
// Draw right-pointing triangle: base on left, point on right
const int baseX = screenWidth - bezelRight - 2 - triangleWidth;
for (int i = 0; i < triangleWidth; ++i) {
// Scale height based on position (full height at base, 0 at tip)
const int lineHalfHeight = (triangleHeight * (triangleWidth - 1 - i)) / (triangleWidth * 2);
renderer.drawLine(baseX + i, triangleCenterY - lineHalfHeight,
baseX + i, triangleCenterY + lineHalfHeight);
renderer.drawLine(baseX + i, triangleCenterY - lineHalfHeight, baseX + i, triangleCenterY + lineHalfHeight);
}
}
}

View File

@ -25,7 +25,8 @@ class ScreenComponents {
// Returns the height of the tab bar (for positioning content below)
// When selectedIndex is provided, tabs scroll so the selected tab is visible
// When showCursor is true, bullet indicators are drawn around the selected tab
static int drawTabBar(const GfxRenderer& renderer, int y, const std::vector<TabInfo>& tabs, int selectedIndex = -1, bool showCursor = false);
static int drawTabBar(const GfxRenderer& renderer, int y, const std::vector<TabInfo>& tabs, int selectedIndex = -1,
bool showCursor = false);
// Draw a scroll/page indicator on the right side of the screen
// Shows up/down arrows and current page fraction (e.g., "1/3")

View File

@ -184,7 +184,8 @@ void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap, const std::str
drawHeight = 0;
fillWidth = static_cast<int>(bitmap.getWidth());
fillHeight = static_cast<int>(bitmap.getHeight());
Serial.printf("[%lu] [SLP] ACTUAL mode: centering at %d, %d (fill: %dx%d)\n", millis(), x, y, fillWidth, fillHeight);
Serial.printf("[%lu] [SLP] ACTUAL mode: centering at %d, %d (fill: %dx%d)\n", millis(), x, y, fillWidth,
fillHeight);
} else if (coverMode == CrossPointSettings::SLEEP_SCREEN_COVER_MODE::CROP) {
// CROP mode: Scale to fill screen completely (may crop edges)
// Calculate crop values to fill the screen while maintaining aspect ratio
@ -221,8 +222,8 @@ void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap, const std::str
// Center the scaled image
x = (pageWidth - fillWidth) / 2;
y = (pageHeight - fillHeight) / 2;
Serial.printf("[%lu] [SLP] FIT mode: scale %f, scaled size %d x %d, position %d, %d\n", millis(), scale,
fillWidth, fillHeight, x, y);
Serial.printf("[%lu] [SLP] FIT mode: scale %f, scaled size %d x %d, position %d, %d\n", millis(), scale, fillWidth,
fillHeight, x, y);
}
// Get edge luminance values (from cache or calculate)
@ -232,9 +233,8 @@ void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap, const std::str
const uint8_t leftGray = quantizeGray(edges.left);
const uint8_t rightGray = quantizeGray(edges.right);
Serial.printf("[%lu] [SLP] Edge luminance: T=%d B=%d L=%d R=%d -> gray levels T=%d B=%d L=%d R=%d\n",
millis(), edges.top, edges.bottom, edges.left, edges.right,
topGray, bottomGray, leftGray, rightGray);
Serial.printf("[%lu] [SLP] Edge luminance: T=%d B=%d L=%d R=%d -> gray levels T=%d B=%d L=%d R=%d\n", millis(),
edges.top, edges.bottom, edges.left, edges.right, topGray, bottomGray, leftGray, rightGray);
// Check if greyscale pass should be used (PR #476: skip if filter is applied)
const bool hasGreyscale = bitmap.hasGreyscale() &&
@ -434,8 +434,7 @@ EdgeLuminance SleepActivity::getEdgeLuminance(const Bitmap& bitmap, const std::s
uint8_t cacheData[EDGE_CACHE_SIZE];
if (cacheFile.read(cacheData, EDGE_CACHE_SIZE) == EDGE_CACHE_SIZE) {
// Extract cached file size
const uint32_t cachedSize = static_cast<uint32_t>(cacheData[0]) |
(static_cast<uint32_t>(cacheData[1]) << 8) |
const uint32_t cachedSize = static_cast<uint32_t>(cacheData[0]) | (static_cast<uint32_t>(cacheData[1]) << 8) |
(static_cast<uint32_t>(cacheData[2]) << 16) |
(static_cast<uint32_t>(cacheData[3]) << 24);
@ -448,8 +447,8 @@ EdgeLuminance SleepActivity::getEdgeLuminance(const Bitmap& bitmap, const std::s
result.bottom = cacheData[5];
result.left = cacheData[6];
result.right = cacheData[7];
Serial.printf("[%lu] [SLP] Edge cache hit for %s: T=%d B=%d L=%d R=%d\n", millis(), bmpPath.c_str(),
result.top, result.bottom, result.left, result.right);
Serial.printf("[%lu] [SLP] Edge cache hit for %s: T=%d B=%d L=%d R=%d\n", millis(), bmpPath.c_str(), result.top,
result.bottom, result.left, result.right);
cacheFile.close();
return result;
}
@ -462,8 +461,8 @@ EdgeLuminance SleepActivity::getEdgeLuminance(const Bitmap& bitmap, const std::s
// Cache miss - calculate edge luminance
Serial.printf("[%lu] [SLP] Calculating edge luminance for %s\n", millis(), bmpPath.c_str());
result = bitmap.detectEdgeLuminance(2); // Sample 2 pixels deep for stability
Serial.printf("[%lu] [SLP] Edge luminance detected: T=%d B=%d L=%d R=%d\n", millis(),
result.top, result.bottom, result.left, result.right);
Serial.printf("[%lu] [SLP] Edge luminance detected: T=%d B=%d L=%d R=%d\n", millis(), result.top, result.bottom,
result.left, result.right);
// Get BMP file size from already-opened bitmap for cache
const uint32_t fileSize = bitmap.getFileSize();
@ -534,8 +533,7 @@ bool SleepActivity::tryRenderCachedCoverSleep(const std::string& bookPath, bool
cacheFile.close();
// Extract cached values
const uint32_t cachedBmpSize = static_cast<uint32_t>(cacheData[0]) |
(static_cast<uint32_t>(cacheData[1]) << 8) |
const uint32_t cachedBmpSize = static_cast<uint32_t>(cacheData[0]) | (static_cast<uint32_t>(cacheData[1]) << 8) |
(static_cast<uint32_t>(cacheData[2]) << 16) |
(static_cast<uint32_t>(cacheData[3]) << 24);
EdgeLuminance cachedEdges;
@ -548,8 +546,8 @@ bool SleepActivity::tryRenderCachedCoverSleep(const std::string& bookPath, bool
// Check if cover mode matches (for EPUB)
const uint8_t currentCoverMode = cropped ? 1 : 0;
if (StringUtils::checkFileExtension(bookPath, ".epub") && cachedCoverMode != currentCoverMode) {
Serial.printf("[SLP] Cover mode changed (cached=%d, current=%d), invalidating cache\n",
cachedCoverMode, currentCoverMode);
Serial.printf("[SLP] Cover mode changed (cached=%d, current=%d), invalidating cache\n", cachedCoverMode,
currentCoverMode);
return false;
}
@ -571,8 +569,8 @@ bool SleepActivity::tryRenderCachedCoverSleep(const std::string& bookPath, bool
// Check if BMP file size matches cache
const uint32_t currentBmpSize = bmpFile.size();
if (currentBmpSize != cachedBmpSize || currentBmpSize == 0) {
Serial.printf("[SLP] BMP size mismatch (cached=%lu, current=%lu)\n",
static_cast<unsigned long>(cachedBmpSize), static_cast<unsigned long>(currentBmpSize));
Serial.printf("[SLP] BMP size mismatch (cached=%lu, current=%lu)\n", static_cast<unsigned long>(cachedBmpSize),
static_cast<unsigned long>(currentBmpSize));
bmpFile.close();
return false;
}
@ -585,8 +583,8 @@ bool SleepActivity::tryRenderCachedCoverSleep(const std::string& bookPath, bool
return false;
}
Serial.printf("[%lu] [SLP] Using cached cover sleep: %s (T=%d B=%d L=%d R=%d)\n", millis(),
coverBmpPath.c_str(), cachedEdges.top, cachedEdges.bottom, cachedEdges.left, cachedEdges.right);
Serial.printf("[%lu] [SLP] Using cached cover sleep: %s (T=%d B=%d L=%d R=%d)\n", millis(), coverBmpPath.c_str(),
cachedEdges.top, cachedEdges.bottom, cachedEdges.left, cachedEdges.right);
// Render the bitmap with cached edge values
// We call renderBitmapSleepScreen which will use getEdgeLuminance internally,

View File

@ -1,10 +1,10 @@
#pragma once
#include "../Activity.h"
#include <Bitmap.h>
#include <string>
#include "../Activity.h"
class SleepActivity final : public Activity {
public:
explicit SleepActivity(GfxRenderer& renderer, MappedInputManager& mappedInput)

View File

@ -236,7 +236,8 @@ void OpdsBookBrowserActivity::render() const {
}
const auto pageStartIndex = selectorIndex / PAGE_ITEMS * PAGE_ITEMS;
renderer.fillRect(bezelLeft, 60 + bezelTop + (selectorIndex % PAGE_ITEMS) * 30 - 2, pageWidth - 1 - bezelLeft - bezelRight, 30);
renderer.fillRect(bezelLeft, 60 + bezelTop + (selectorIndex % PAGE_ITEMS) * 30 - 2,
pageWidth - 1 - bezelLeft - bezelRight, 30);
for (size_t i = pageStartIndex; i < entries.size() && i < static_cast<size_t>(pageStartIndex + PAGE_ITEMS); i++) {
const auto& entry = entries[i];
@ -253,7 +254,8 @@ void OpdsBookBrowserActivity::render() const {
}
}
auto item = renderer.truncatedText(UI_10_FONT_ID, displayText.c_str(), renderer.getScreenWidth() - 40 - bezelLeft - bezelRight);
auto item = renderer.truncatedText(UI_10_FONT_ID, displayText.c_str(),
renderer.getScreenWidth() - 40 - bezelLeft - bezelRight);
renderer.drawText(UI_10_FONT_ID, 20 + bezelLeft, 60 + bezelTop + (i % PAGE_ITEMS) * 30, item.c_str(),
i != static_cast<size_t>(selectorIndex));
}

View File

@ -9,8 +9,7 @@
namespace {
constexpr int MAX_MENU_ITEM_COUNT = 2;
const char* MENU_ITEMS[MAX_MENU_ITEM_COUNT] = {"Select from Screen", "Enter a Word"};
const char* MENU_DESCRIPTIONS[MAX_MENU_ITEM_COUNT] = {"Choose a word from the current page",
"Type a word to look up"};
const char* MENU_DESCRIPTIONS[MAX_MENU_ITEM_COUNT] = {"Choose a word from the current page", "Type a word to look up"};
} // namespace
void DictionaryMenuActivity::taskTrampoline(void* param) {
@ -64,8 +63,7 @@ void DictionaryMenuActivity::loop() {
// Handle confirm button - select current option
// Use wasReleased to consume the full button event
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
const DictionaryMode mode =
(selectedIndex == 0) ? DictionaryMode::SELECT_FROM_SCREEN : DictionaryMode::ENTER_WORD;
const DictionaryMode mode = (selectedIndex == 0) ? DictionaryMode::SELECT_FROM_SCREEN : DictionaryMode::ENTER_WORD;
onModeSelected(mode);
return;
}

View File

@ -1,10 +1,9 @@
#pragma once
#include <Epub/blocks/TextBlock.h>
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <freertos/task.h>
#include <Epub/blocks/TextBlock.h>
#include <functional>
#include <memory>
#include <string>
@ -48,8 +47,7 @@ class DictionaryResultActivity final : public Activity {
*/
explicit DictionaryResultActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
const std::string& wordToLookup, const std::string& definition,
const std::function<void()>& onBack,
const std::function<void()>& onSearchAnother)
const std::function<void()>& onBack, const std::function<void()>& onSearchAnother)
: Activity("DictionaryResult", renderer, mappedInput),
lookupWord(wordToLookup),
rawDefinition(definition),

View File

@ -77,9 +77,8 @@ void EpubWordSelectionActivity::buildWordList() {
while (wordIt != words.end() && xPosIt != xPositions.end() && styleIt != styles.end()) {
// Skip whitespace-only words
const std::string& wordText = *wordIt;
const bool hasAlpha = std::any_of(wordText.begin(), wordText.end(), [](char c) {
return std::isalpha(static_cast<unsigned char>(c));
});
const bool hasAlpha = std::any_of(wordText.begin(), wordText.end(),
[](char c) { return std::isalpha(static_cast<unsigned char>(c)); });
if (hasAlpha) {
WordInfo info;
@ -249,7 +248,8 @@ void EpubWordSelectionActivity::render() const {
// Draw instruction text - position it just above the front button area
const auto screenHeight = renderer.getScreenHeight();
renderer.drawCenteredText(SMALL_FONT_ID, screenHeight - marginBottom - 10, "Navigate with arrows, select with confirm");
renderer.drawCenteredText(SMALL_FONT_ID, screenHeight - marginBottom - 10,
"Navigate with arrows, select with confirm");
// Draw button hints
const auto labels = mappedInput.mapLabels("\xc2\xab Cancel", "Select", "< >", "");

View File

@ -39,9 +39,7 @@ int BookmarkListActivity::getCurrentPage() const {
return selectorIndex / pageItems + 1;
}
void BookmarkListActivity::loadBookmarks() {
bookmarks = BookmarkStore::getBookmarks(bookPath);
}
void BookmarkListActivity::loadBookmarks() { bookmarks = BookmarkStore::getBookmarks(bookPath); }
void BookmarkListActivity::taskTrampoline(void* param) {
auto* self = static_cast<BookmarkListActivity*>(param);
@ -115,9 +113,8 @@ void BookmarkListActivity::loop() {
const int itemCount = static_cast<int>(bookmarks.size());
// Long press Confirm to delete bookmark
if (mappedInput.isPressed(MappedInputManager::Button::Confirm) &&
mappedInput.getHeldTime() >= ACTION_MENU_MS && !bookmarks.empty() &&
selectorIndex < itemCount) {
if (mappedInput.isPressed(MappedInputManager::Button::Confirm) && mappedInput.getHeldTime() >= ACTION_MENU_MS &&
!bookmarks.empty() && selectorIndex < itemCount) {
uiState = UIState::Confirming;
updateRequired = true;
return;
@ -190,8 +187,10 @@ void BookmarkListActivity::render() const {
const int RIGHT_MARGIN = BASE_RIGHT_MARGIN + bezelRight;
// Draw title
auto truncatedTitle = renderer.truncatedText(UI_12_FONT_ID, bookTitle.c_str(), pageWidth - LEFT_MARGIN - RIGHT_MARGIN);
renderer.drawText(UI_12_FONT_ID, LEFT_MARGIN, BASE_TAB_BAR_Y + bezelTop, truncatedTitle.c_str(), true, EpdFontFamily::BOLD);
auto truncatedTitle =
renderer.truncatedText(UI_12_FONT_ID, bookTitle.c_str(), pageWidth - LEFT_MARGIN - RIGHT_MARGIN);
renderer.drawText(UI_12_FONT_ID, LEFT_MARGIN, BASE_TAB_BAR_Y + bezelTop, truncatedTitle.c_str(), true,
EpdFontFamily::BOLD);
if (itemCount == 0) {
renderer.drawText(UI_10_FONT_ID, LEFT_MARGIN, CONTENT_START_Y, "No bookmarks");

View File

@ -50,8 +50,8 @@ class BookmarkListActivity final : public Activity {
void renderConfirmation() const;
public:
explicit BookmarkListActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
const std::string& bookPath, const std::string& bookTitle,
explicit BookmarkListActivity(
GfxRenderer& renderer, MappedInputManager& mappedInput, const std::string& bookPath, const std::string& bookTitle,
const std::function<void()>& onGoBack,
const std::function<void(uint16_t spineIndex, uint32_t contentOffset)>& onSelectBookmark)
: Activity("BookmarkList", renderer, mappedInput),

View File

@ -374,8 +374,7 @@ void HomeActivity::render() {
constexpr int menuSpacing = 8;
const int halfTileWidth = (menuTileWidth - menuSpacing) / 2; // Account for spacing between halves
// 1 row for split buttons + full-width rows
const int totalMenuHeight =
menuTileHeight + static_cast<int>(fullWidthItems.size()) * (menuTileHeight + menuSpacing);
const int totalMenuHeight = menuTileHeight + static_cast<int>(fullWidthItems.size()) * (menuTileHeight + menuSpacing);
// Anchor menu to bottom of screen
const int menuStartY = pageHeight - bottomMargin - totalMenuHeight;
@ -581,8 +580,7 @@ void HomeActivity::render() {
// Still have words left, so add ellipsis to last line
lines.back().append("...");
while (!lines.back().empty() &&
renderer.getTextWidth(UI_12_FONT_ID, lines.back().c_str()) > maxLineWidth) {
while (!lines.back().empty() && renderer.getTextWidth(UI_12_FONT_ID, lines.back().c_str()) > maxLineWidth) {
// Remove "..." first, then remove one UTF-8 char, then add "..." back
lines.back().resize(lines.back().size() - 3); // Remove "..."
StringUtils::utf8RemoveLastChar(lines.back());
@ -690,7 +688,8 @@ void HomeActivity::render() {
// Truncate lists label if needed
std::string truncatedLabel = listsLabel;
const int maxLabelWidth = halfTileWidth - 16; // Padding
while (renderer.getTextWidth(UI_10_FONT_ID, truncatedLabel.c_str()) > maxLabelWidth && truncatedLabel.length() > 3) {
while (renderer.getTextWidth(UI_10_FONT_ID, truncatedLabel.c_str()) > maxLabelWidth &&
truncatedLabel.length() > 3) {
truncatedLabel.resize(truncatedLabel.length() - 4);
truncatedLabel += "...";
}

View File

@ -50,8 +50,7 @@ class HomeActivity final : public Activity {
explicit HomeActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
const std::function<void()>& onContinueReading, const std::function<void()>& onListsOpen,
const std::function<void()>& onMyLibraryOpen, const std::function<void()>& onSettingsOpen,
const std::function<void()>& onFileTransferOpen,
const std::function<void()>& onOpdsBrowserOpen)
const std::function<void()>& onFileTransferOpen, const std::function<void()>& onOpdsBrowserOpen)
: Activity("Home", renderer, mappedInput),
onContinueReading(onContinueReading),
onListsOpen(onListsOpen),

View File

@ -202,8 +202,8 @@ void ListViewActivity::render() const {
const auto pageStartIndex = selectorIndex / pageItems * pageItems;
// Draw selection highlight
renderer.fillRect(bezelLeft, CONTENT_START_Y + (selectorIndex % pageItems) * LINE_HEIGHT - 2, pageWidth - RIGHT_MARGIN - bezelLeft,
LINE_HEIGHT);
renderer.fillRect(bezelLeft, CONTENT_START_Y + (selectorIndex % pageItems) * LINE_HEIGHT - 2,
pageWidth - RIGHT_MARGIN - bezelLeft, LINE_HEIGHT);
// Calculate available text width
const int textMaxWidth = pageWidth - LEFT_MARGIN - RIGHT_MARGIN - MICRO_THUMB_WIDTH - 10;
@ -302,8 +302,8 @@ void ListViewActivity::render() const {
const int badgeY = y + 2 + (titleLineHeight - badgeHeight) / 2;
if (!tags.extensionTag.empty()) {
int badgeWidth = ScreenComponents::drawPillBadge(renderer, badgeX, badgeY, tags.extensionTag.c_str(),
SMALL_FONT_ID, false);
int badgeWidth =
ScreenComponents::drawPillBadge(renderer, badgeX, badgeY, tags.extensionTag.c_str(), SMALL_FONT_ID, false);
badgeX += badgeWidth + badgeSpacing;
}
if (!tags.suffixTag.empty()) {

View File

@ -91,7 +91,8 @@ int MyLibraryActivity::getPageItems() const {
// Character picker: ~30px, Query: ~25px = 55px overhead
// Much more room for results than the old 5-row keyboard
constexpr int SEARCH_OVERHEAD = 55;
const int availableHeight = screenHeight - (BASE_CONTENT_START_Y + bezelTop) - bottomBarHeight - bezelBottom - SEARCH_OVERHEAD;
const int availableHeight =
screenHeight - (BASE_CONTENT_START_Y + bezelTop) - bottomBarHeight - bezelBottom - SEARCH_OVERHEAD;
int items = availableHeight / RECENTS_LINE_HEIGHT;
if (items < 1) items = 1;
return items;
@ -99,8 +100,8 @@ int MyLibraryActivity::getPageItems() const {
const int availableHeight = screenHeight - (BASE_CONTENT_START_Y + bezelTop) - bottomBarHeight - bezelBottom;
// Recent and Bookmarks tabs use taller items (title + author), Lists and Files use single-line items
const int lineHeight = (currentTab == Tab::Recent || currentTab == Tab::Bookmarks)
? RECENTS_LINE_HEIGHT : LINE_HEIGHT;
const int lineHeight =
(currentTab == Tab::Recent || currentTab == Tab::Bookmarks) ? RECENTS_LINE_HEIGHT : LINE_HEIGHT;
int items = availableHeight / lineHeight;
if (items < 1) {
items = 1;
@ -193,8 +194,7 @@ void MyLibraryActivity::loadAllBooks() {
scanDirectory(fullPath);
} else {
auto filename = std::string(name);
if (StringUtils::checkFileExtension(filename, ".epub") ||
StringUtils::checkFileExtension(filename, ".txt") ||
if (StringUtils::checkFileExtension(filename, ".epub") || StringUtils::checkFileExtension(filename, ".txt") ||
StringUtils::checkFileExtension(filename, ".md")) {
SearchResult result;
result.path = fullPath;
@ -226,8 +226,7 @@ void MyLibraryActivity::loadAllBooks() {
// Sort alphabetically by title
std::sort(allBooks.begin(), allBooks.end(), [](const SearchResult& a, const SearchResult& b) {
return lexicographical_compare(
a.title.begin(), a.title.end(), b.title.begin(), b.title.end(),
return lexicographical_compare(a.title.begin(), a.title.end(), b.title.begin(), b.title.end(),
[](char c1, char c2) { return tolower(c1) < tolower(c2); });
});
@ -341,9 +340,7 @@ void MyLibraryActivity::updateSearchResults() {
// Sort by match score (descending)
std::sort(searchResults.begin(), searchResults.end(),
[](const SearchResult& a, const SearchResult& b) {
return a.matchScore > b.matchScore;
});
[](const SearchResult& a, const SearchResult& b) { return a.matchScore > b.matchScore; });
}
void MyLibraryActivity::loadFiles() {
@ -867,16 +864,14 @@ void MyLibraryActivity::loop() {
// In character picker mode
// Long press Left = jump to start
if (mappedInput.isPressed(MappedInputManager::Button::Left) &&
mappedInput.getHeldTime() >= 700) {
if (mappedInput.isPressed(MappedInputManager::Button::Left) && mappedInput.getHeldTime() >= 700) {
searchCharIndex = 0;
updateRequired = true;
return;
}
// Long press Right = jump to end
if (mappedInput.isPressed(MappedInputManager::Button::Right) &&
mappedInput.getHeldTime() >= 700) {
if (mappedInput.isPressed(MappedInputManager::Button::Right) && mappedInput.getHeldTime() >= 700) {
searchCharIndex = totalPickerItems - 1;
updateRequired = true;
return;
@ -946,8 +941,7 @@ void MyLibraryActivity::loop() {
}
// Long press Back = clear entire query
if (mappedInput.isPressed(MappedInputManager::Button::Back) &&
mappedInput.getHeldTime() >= 700) {
if (mappedInput.isPressed(MappedInputManager::Button::Back) && mappedInput.getHeldTime() >= 700) {
if (!searchQuery.empty()) {
searchQuery.clear();
updateSearchResults();
@ -978,16 +972,14 @@ void MyLibraryActivity::loop() {
// In results mode
// Long press PageBack (side button) = jump to first result
if (mappedInput.isPressed(MappedInputManager::Button::PageBack) &&
mappedInput.getHeldTime() >= 700) {
if (mappedInput.isPressed(MappedInputManager::Button::PageBack) && mappedInput.getHeldTime() >= 700) {
selectorIndex = 0;
updateRequired = true;
return;
}
// Long press PageForward (side button) = jump to last result
if (mappedInput.isPressed(MappedInputManager::Button::PageForward) &&
mappedInput.getHeldTime() >= 700) {
if (mappedInput.isPressed(MappedInputManager::Button::PageForward) && mappedInput.getHeldTime() >= 700) {
if (!searchResults.empty()) {
selectorIndex = static_cast<int>(searchResults.size()) - 1;
}
@ -1065,8 +1057,8 @@ void MyLibraryActivity::loop() {
}
// Long press Confirm to open action menu (only for files, not directories)
if (mappedInput.isPressed(MappedInputManager::Button::Confirm) &&
mappedInput.getHeldTime() >= ACTION_MENU_MS && isSelectedItemAFile()) {
if (mappedInput.isPressed(MappedInputManager::Button::Confirm) && mappedInput.getHeldTime() >= ACTION_MENU_MS &&
isSelectedItemAFile()) {
openActionMenu();
return;
}
@ -1527,8 +1519,8 @@ void MyLibraryActivity::renderRecentTab() const {
const int badgeY = y + 2 + (titleLineHeight - badgeHeight) / 2;
if (!tags.extensionTag.empty()) {
int badgeWidth = ScreenComponents::drawPillBadge(renderer, badgeX, badgeY, tags.extensionTag.c_str(),
SMALL_FONT_ID, false);
int badgeWidth =
ScreenComponents::drawPillBadge(renderer, badgeX, badgeY, tags.extensionTag.c_str(), SMALL_FONT_ID, false);
badgeX += badgeWidth + badgeSpacing;
}
if (!tags.suffixTag.empty()) {
@ -1581,8 +1573,8 @@ void MyLibraryActivity::renderListsTab() const {
const auto pageStartIndex = selectorIndex / pageItems * pageItems;
// Draw selection highlight
renderer.fillRect(bezelLeft, CONTENT_START_Y + (selectorIndex % pageItems) * LINE_HEIGHT - 2, pageWidth - RIGHT_MARGIN - bezelLeft,
LINE_HEIGHT);
renderer.fillRect(bezelLeft, CONTENT_START_Y + (selectorIndex % pageItems) * LINE_HEIGHT - 2,
pageWidth - RIGHT_MARGIN - bezelLeft, LINE_HEIGHT);
// Draw items
for (int i = pageStartIndex; i < listCount && i < pageStartIndex + pageItems; i++) {
@ -1632,8 +1624,8 @@ void MyLibraryActivity::renderFilesTab() const {
const auto pageStartIndex = selectorIndex / pageItems * pageItems;
// Draw selection highlight
renderer.fillRect(bezelLeft, CONTENT_START_Y + (selectorIndex % pageItems) * LINE_HEIGHT - 2, pageWidth - RIGHT_MARGIN - bezelLeft,
LINE_HEIGHT);
renderer.fillRect(bezelLeft, CONTENT_START_Y + (selectorIndex % pageItems) * LINE_HEIGHT - 2,
pageWidth - RIGHT_MARGIN - bezelLeft, LINE_HEIGHT);
// Draw items
for (int i = pageStartIndex; i < fileCount && i < pageStartIndex + pageItems; i++) {
@ -1665,7 +1657,8 @@ void MyLibraryActivity::renderActionMenu() const {
// Show filename
const int filenameY = 70 + bezelTop;
auto truncatedName = renderer.truncatedText(UI_10_FONT_ID, actionTargetName.c_str(), pageWidth - 40 - bezelLeft - bezelRight);
auto truncatedName =
renderer.truncatedText(UI_10_FONT_ID, actionTargetName.c_str(), pageWidth - 40 - bezelLeft - bezelRight);
renderer.drawCenteredText(UI_10_FONT_ID, filenameY, truncatedName.c_str());
// Menu options - 4 for Recent tab, 2 for Files tab
@ -1694,7 +1687,8 @@ void MyLibraryActivity::renderActionMenu() const {
if (menuSelection == 2) {
renderer.fillRect(menuX - 10, menuStartY + menuLineHeight * 2 - 5, menuItemWidth + 20, menuLineHeight);
}
renderer.drawCenteredText(UI_10_FONT_ID, menuStartY + menuLineHeight * 2, "Remove from Recents", menuSelection != 2);
renderer.drawCenteredText(UI_10_FONT_ID, menuStartY + menuLineHeight * 2, "Remove from Recents",
menuSelection != 2);
// Clear All Recents option
if (menuSelection == 3) {
@ -1808,7 +1802,8 @@ void MyLibraryActivity::renderListDeleteConfirmation() const {
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 - 40, truncatedName.c_str());
// Warning text
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, "List will be permanently deleted!", true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, "List will be permanently deleted!", true,
EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 + 25, "This cannot be undone.");
// Draw bottom button hints
@ -1924,7 +1919,8 @@ void MyLibraryActivity::renderSearchTab() const {
if (!searchInResults) {
displayQuery = searchQuery + "_"; // Show cursor when in picker
}
auto truncatedQuery = renderer.truncatedText(UI_10_FONT_ID, displayQuery.c_str(), pageWidth - LEFT_MARGIN - RIGHT_MARGIN);
auto truncatedQuery =
renderer.truncatedText(UI_10_FONT_ID, displayQuery.c_str(), pageWidth - LEFT_MARGIN - RIGHT_MARGIN);
renderer.drawText(UI_10_FONT_ID, LEFT_MARGIN, queryY, truncatedQuery.c_str());
// Draw results below query
@ -1996,8 +1992,8 @@ void MyLibraryActivity::renderSearchTab() const {
const int badgeY = y + 2 + (titleLineHeight - badgeHeight) / 2;
if (!tags.extensionTag.empty()) {
int badgeWidth = ScreenComponents::drawPillBadge(renderer, badgeX, badgeY, tags.extensionTag.c_str(),
SMALL_FONT_ID, false);
int badgeWidth =
ScreenComponents::drawPillBadge(renderer, badgeX, badgeY, tags.extensionTag.c_str(), SMALL_FONT_ID, false);
badgeX += badgeWidth + badgeSpacing;
}
if (!tags.suffixTag.empty()) {
@ -2081,7 +2077,8 @@ void MyLibraryActivity::renderCharacterPicker(int y) const {
// Draw characters
const int startX = bezelLeft + 20 - scrollOffset;
currentX = startX;
const bool showSelection = !searchInResults && !inTabBar; // Only show selection when in picker (not tab bar or results)
const bool showSelection =
!searchInResults && !inTabBar; // Only show selection when in picker (not tab bar or results)
for (int i = 0; i < totalItems; i++) {
std::string label;
@ -2141,21 +2138,20 @@ void MyLibraryActivity::renderCharacterPicker(int y) const {
for (int i = 0; i < triangleWidth; ++i) {
// Scale height based on position (0 at tip, full height at base)
const int lineHalfHeight = (triangleHeight * i) / (triangleWidth * 2);
renderer.drawLine(tipX + i, triangleCenterY - lineHalfHeight,
tipX + i, triangleCenterY + lineHalfHeight);
renderer.drawLine(tipX + i, triangleCenterY - lineHalfHeight, tipX + i, triangleCenterY + lineHalfHeight);
}
}
// Right overflow indicator (more content to the right) - thin triangle pointing right
if (hasRightOverflow) {
// Clear background behind indicator to hide any overlapping text
renderer.fillRect(pageWidth - bezelRight - overflowIndicatorWidth - 4, y - 2, overflowIndicatorWidth + 4, pickerLineHeight + 4, false);
renderer.fillRect(pageWidth - bezelRight - overflowIndicatorWidth - 4, y - 2, overflowIndicatorWidth + 4,
pickerLineHeight + 4, false);
// Draw right-pointing triangle: base on left, point on right
const int baseX = pageWidth - bezelRight - 2 - triangleWidth;
for (int i = 0; i < triangleWidth; ++i) {
// Scale height based on position (full height at base, 0 at tip)
const int lineHalfHeight = (triangleHeight * (triangleWidth - 1 - i)) / (triangleWidth * 2);
renderer.drawLine(baseX + i, triangleCenterY - lineHalfHeight,
baseX + i, triangleCenterY + lineHalfHeight);
renderer.drawLine(baseX + i, triangleCenterY - lineHalfHeight, baseX + i, triangleCenterY + lineHalfHeight);
}
}
}

View File

@ -38,7 +38,14 @@ struct BookmarkedBook {
class MyLibraryActivity final : public Activity {
public:
enum class Tab { Recent, Lists, Bookmarks, Search, Files };
enum class UIState { Normal, ActionMenu, Confirming, ListActionMenu, ListConfirmingDelete, ClearAllRecentsConfirming };
enum class UIState {
Normal,
ActionMenu,
Confirming,
ListActionMenu,
ListConfirmingDelete,
ClearAllRecentsConfirming
};
enum class ActionType { Archive, Delete, RemoveFromRecents, ClearAllRecents };
private:
@ -70,7 +77,6 @@ class MyLibraryActivity final : public Activity {
static void clearThumbExistsCache();
private:
// Lists tab state
std::vector<std::string> lists;
@ -148,8 +154,8 @@ class MyLibraryActivity final : public Activity {
void renderClearAllRecentsConfirmation() const;
public:
explicit MyLibraryActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
const std::function<void()>& onGoHome,
explicit MyLibraryActivity(
GfxRenderer& renderer, MappedInputManager& mappedInput, const std::function<void()>& onGoHome,
const std::function<void(const std::string& path, Tab fromTab)>& onSelectBook,
const std::function<void(const std::string& listName)>& onSelectList,
const std::function<void(const std::string& path, const std::string& title)>& onSelectBookmarkedBook = nullptr,

View File

@ -127,8 +127,9 @@ void EpubReaderActivity::onEnter() {
nextPageNumber = pageNumber;
hasContentOffset = true;
Serial.printf("[%lu] [ERS] Loaded progress v1: spine %d, page %d, offset %u\n",
millis(), currentSpineIndex, nextPageNumber, savedContentOffset);
if (Serial)
Serial.printf("[%lu] [ERS] Loaded progress v1: spine %d, page %d, offset %u\n", millis(), currentSpineIndex,
nextPageNumber, savedContentOffset);
} else {
// Unknown version, try legacy format
f.seek(0);
@ -137,8 +138,9 @@ void EpubReaderActivity::onEnter() {
currentSpineIndex = data[0] + (data[1] << 8);
nextPageNumber = data[2] + (data[3] << 8);
hasContentOffset = false;
Serial.printf("[%lu] [ERS] Loaded legacy progress (unknown version %d): spine %d, page %d\n",
millis(), version, currentSpineIndex, nextPageNumber);
if (Serial)
Serial.printf("[%lu] [ERS] Loaded legacy progress (unknown version %d): spine %d, page %d\n", millis(),
version, currentSpineIndex, nextPageNumber);
}
}
} else if (fileSize >= 4) {
@ -148,18 +150,21 @@ void EpubReaderActivity::onEnter() {
currentSpineIndex = data[0] + (data[1] << 8);
nextPageNumber = data[2] + (data[3] << 8);
hasContentOffset = false;
Serial.printf("[%lu] [ERS] Loaded legacy progress: spine %d, page %d\n",
millis(), currentSpineIndex, nextPageNumber);
if (Serial)
Serial.printf("[%lu] [ERS] Loaded legacy progress: spine %d, page %d\n", millis(), currentSpineIndex,
nextPageNumber);
}
}
f.close();
}
// We may want a better condition to detect if we are opening for the first time.
// This will trigger if the book is re-opened at Chapter 0.
if (currentSpineIndex == 0) {
int textSpineIndex = epub->getSpineIndexForTextReference();
if (textSpineIndex != 0) {
currentSpineIndex = textSpineIndex;
if (Serial)
Serial.printf("[%lu] [ERS] Opened for first time, navigating to text reference at index %d\n", millis(),
textSpineIndex);
}
@ -306,7 +311,8 @@ void EpubReaderActivity::loop() {
if (mode == DictionaryMode::ENTER_WORD) {
// Enter word mode - show keyboard and search
self->enterNewActivity(new DictionarySearchActivity(cachedRenderer, cachedMappedInput,
self->enterNewActivity(new DictionarySearchActivity(
cachedRenderer, cachedMappedInput,
[self]() {
// On back from dictionary
self->exitActivity();
@ -335,7 +341,8 @@ void EpubReaderActivity::loop() {
[self](const std::string& selectedWord) {
// Word selected - look it up
self->exitActivity();
self->enterNewActivity(new DictionarySearchActivity(self->renderer, self->mappedInput,
self->enterNewActivity(new DictionarySearchActivity(
self->renderer, self->mappedInput,
[self]() {
self->exitActivity();
self->updateRequired = true;
@ -406,11 +413,13 @@ void EpubReaderActivity::loop() {
self->exitActivity();
if (mode == DictionaryMode::ENTER_WORD) {
self->enterNewActivity(new DictionarySearchActivity(r, m,
self->enterNewActivity(new DictionarySearchActivity(
r, m,
[self]() {
self->exitActivity();
self->updateRequired = true;
}, ""));
},
""));
} else if (s) {
xSemaphoreTake(mtx, portMAX_DELAY);
auto page = s->loadPageFromSectionFile();
@ -430,7 +439,8 @@ void EpubReaderActivity::loop() {
[self]() {
self->exitActivity();
self->updateRequired = true;
}, word));
},
word));
},
[self]() {
self->exitActivity();
@ -629,6 +639,7 @@ void EpubReaderActivity::renderScreen() {
if (!section) {
const auto filepath = epub->getSpineItem(currentSpineIndex).href;
if (Serial)
Serial.printf("[%lu] [ERS] Loading file: %s, index: %d\n", millis(), filepath.c_str(), currentSpineIndex);
section = std::unique_ptr<Section>(new Section(epub, currentSpineIndex, renderer));
@ -640,7 +651,7 @@ void EpubReaderActivity::renderScreen() {
if (!section->loadSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
viewportHeight, SETTINGS.hyphenationEnabled)) {
Serial.printf("[%lu] [ERS] Cache not found, building...\n", millis());
if (Serial) Serial.printf("[%lu] [ERS] Cache not found, building...\n", millis());
sectionWasReIndexed = true;
// Progress bar dimensions
@ -686,12 +697,12 @@ void EpubReaderActivity::renderScreen() {
if (!section->createSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
viewportHeight, SETTINGS.hyphenationEnabled, progressSetup, progressCallback)) {
Serial.printf("[%lu] [ERS] Failed to persist page data to SD\n", millis());
if (Serial) Serial.printf("[%lu] [ERS] Failed to persist page data to SD\n", millis());
section.reset();
return;
}
} else {
Serial.printf("[%lu] [ERS] Cache found, skipping build...\n", millis());
if (Serial) Serial.printf("[%lu] [ERS] Cache found, skipping build...\n", millis());
}
// Determine the correct page to display
@ -703,8 +714,9 @@ void EpubReaderActivity::renderScreen() {
// Use the offset to find the correct page
const int restoredPage = section->findPageForContentOffset(savedContentOffset);
section->currentPage = restoredPage;
Serial.printf("[%lu] [ERS] Restored position via offset: %u -> page %d (was page %d)\n",
millis(), savedContentOffset, restoredPage, nextPageNumber);
if (Serial)
Serial.printf("[%lu] [ERS] Restored position via offset: %u -> page %d (was page %d)\n", millis(),
savedContentOffset, restoredPage, nextPageNumber);
// Clear the offset flag since we've used it
hasContentOffset = false;
} else {
@ -716,7 +728,7 @@ void EpubReaderActivity::renderScreen() {
renderer.clearScreen();
if (section->pageCount == 0) {
Serial.printf("[%lu] [ERS] No pages to render\n", millis());
if (Serial) Serial.printf("[%lu] [ERS] No pages to render\n", millis());
renderer.drawCenteredText(UI_12_FONT_ID, 300, "Empty chapter", true, EpdFontFamily::BOLD);
renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
renderer.displayBuffer();
@ -724,7 +736,9 @@ void EpubReaderActivity::renderScreen() {
}
if (section->currentPage < 0 || section->currentPage >= section->pageCount) {
Serial.printf("[%lu] [ERS] Page out of bounds: %d (max %d)\n", millis(), section->currentPage, section->pageCount);
if (Serial)
Serial.printf("[%lu] [ERS] Page out of bounds: %d (max %d)\n", millis(), section->currentPage,
section->pageCount);
renderer.drawCenteredText(UI_12_FONT_ID, 300, "Out of bounds", true, EpdFontFamily::BOLD);
renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
renderer.displayBuffer();
@ -734,7 +748,7 @@ void EpubReaderActivity::renderScreen() {
{
auto p = section->loadPageFromSectionFile();
if (!p) {
Serial.printf("[%lu] [ERS] Failed to load page from SD - clearing section cache\n", millis());
if (Serial) Serial.printf("[%lu] [ERS] Failed to load page from SD - clearing section cache\n", millis());
section->clearCache();
section.reset();
return renderScreen();
@ -742,7 +756,7 @@ void EpubReaderActivity::renderScreen() {
// Handle empty pages (e.g., from malformed chapters that couldn't be parsed)
if (p->elements.empty()) {
Serial.printf("[%lu] [ERS] Page has no content (possibly malformed chapter)\n", millis());
if (Serial) Serial.printf("[%lu] [ERS] Page has no content (possibly malformed chapter)\n", millis());
renderer.drawCenteredText(UI_12_FONT_ID, 280, "Chapter content unavailable", true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_10_FONT_ID, 320, "(File may be malformed)");
renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
@ -752,7 +766,7 @@ void EpubReaderActivity::renderScreen() {
const auto start = millis();
renderContents(std::move(p), orientedMarginTop, orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
Serial.printf("[%lu] [ERS] Rendered page in %dms\n", millis(), millis() - start);
if (Serial) Serial.printf("[%lu] [ERS] Rendered page in %dms\n", millis(), millis() - start);
}
// Save progress with content offset for position restoration after re-indexing
@ -768,8 +782,9 @@ void EpubReaderActivity::renderScreen() {
serialization::writePod(f, contentOffset);
f.close();
Serial.printf("[%lu] [ERS] Saved progress: spine %d, page %d, offset %u\n",
millis(), currentSpineIndex, section->currentPage, contentOffset);
if (Serial)
Serial.printf("[%lu] [ERS] Saved progress: spine %d, page %d, offset %u\n", millis(), currentSpineIndex,
section->currentPage, contentOffset);
}
}

View File

@ -160,12 +160,13 @@ void EpubReaderChapterSelectionActivity::renderScreen() {
const int bezelLeft = renderer.getBezelOffsetLeft();
const int bezelRight = renderer.getBezelOffsetRight();
const std::string title =
renderer.truncatedText(UI_12_FONT_ID, epub->getTitle().c_str(), pageWidth - 40 - bezelLeft - bezelRight, EpdFontFamily::BOLD);
const std::string title = renderer.truncatedText(UI_12_FONT_ID, epub->getTitle().c_str(),
pageWidth - 40 - bezelLeft - bezelRight, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_12_FONT_ID, 15 + bezelTop, title.c_str(), true, EpdFontFamily::BOLD);
const auto pageStartIndex = selectorIndex / pageItems * pageItems;
renderer.fillRect(bezelLeft, 60 + bezelTop + (selectorIndex % pageItems) * 30 - 2, pageWidth - 1 - bezelLeft - bezelRight, 30);
renderer.fillRect(bezelLeft, 60 + bezelTop + (selectorIndex % pageItems) * 30 - 2,
pageWidth - 1 - bezelLeft - bezelRight, 30);
for (int itemIndex = pageStartIndex; itemIndex < totalItems && itemIndex < pageStartIndex + pageItems; itemIndex++) {
const int displayY = 60 + bezelTop + (itemIndex % pageItems) * 30;
@ -175,8 +176,8 @@ void EpubReaderChapterSelectionActivity::renderScreen() {
const int tocIndex = tocIndexFromItemIndex(itemIndex);
auto item = epub->getTocItem(tocIndex);
const int indentSize = 20 + bezelLeft + (item.level - 1) * 15;
const std::string chapterName =
renderer.truncatedText(UI_10_FONT_ID, item.title.c_str(), pageWidth - 40 - bezelLeft - bezelRight - indentSize + 20 + bezelLeft);
const std::string chapterName = renderer.truncatedText(
UI_10_FONT_ID, item.title.c_str(), pageWidth - 40 - bezelLeft - bezelRight - indentSize + 20 + bezelLeft);
renderer.drawText(UI_10_FONT_ID, indentSize, displayY, chapterName.c_str(), !isSelected);
}

View File

@ -62,11 +62,8 @@ void ReaderActivity::onGoToEpubReader(std::unique_ptr<Epub> epub) {
currentBookPath = epubPath;
exitActivity();
enterNewActivity(new EpubReaderActivity(
renderer, mappedInput, std::move(epub),
[this, epubPath] { goToLibrary(epubPath); },
[this] { onGoBack(); },
onGoToClearCache,
onGoToSettings));
renderer, mappedInput, std::move(epub), [this, epubPath] { goToLibrary(epubPath); }, [this] { onGoBack(); },
onGoToClearCache, onGoToSettings));
}
void ReaderActivity::onGoToTxtReader(std::unique_ptr<Txt> txt) {

View File

@ -660,8 +660,8 @@ void TxtReaderActivity::saveProgress() const {
serialization::writePod(f, PROGRESS_VERSION);
// Store byte offset - this is stable across font/setting changes
const size_t byteOffset = (currentPage >= 0 && currentPage < static_cast<int>(pageOffsets.size()))
? pageOffsets[currentPage] : 0;
const size_t byteOffset =
(currentPage >= 0 && currentPage < static_cast<int>(pageOffsets.size())) ? pageOffsets[currentPage] : 0;
serialization::writePod(f, static_cast<uint32_t>(byteOffset));
// Also store page number for debugging/logging purposes
@ -693,8 +693,8 @@ void TxtReaderActivity::loadProgress() {
// Use byte offset to find the correct page (works even if re-indexed)
currentPage = findPageForOffset(savedOffset);
Serial.printf("[%lu] [TRS] Loaded progress: offset %u -> page %d/%d (was page %d)\n",
millis(), savedOffset, currentPage, totalPages, savedPage);
Serial.printf("[%lu] [TRS] Loaded progress: offset %u -> page %d/%d (was page %d)\n", millis(), savedOffset,
currentPage, totalPages, savedPage);
} else {
// Unknown version, fall back to legacy behavior
Serial.printf("[%lu] [TRS] Unknown progress version %d, ignoring\n", millis(), version);

View File

@ -201,7 +201,8 @@ void CategorySettingsActivity::render() const {
renderer.drawCenteredText(UI_12_FONT_ID, 15 + bezelTop, categoryName, true, EpdFontFamily::BOLD);
// Draw selection highlight
renderer.fillRect(bezelLeft, 60 + bezelTop + selectedSettingIndex * 30 - 2, pageWidth - 1 - bezelLeft - bezelRight, 30);
renderer.fillRect(bezelLeft, 60 + bezelTop + selectedSettingIndex * 30 - 2, pageWidth - 1 - bezelLeft - bezelRight,
30);
// Draw only visible settings
int visibleIndex = 0;
@ -237,7 +238,8 @@ void CategorySettingsActivity::render() const {
visibleIndex++;
}
renderer.drawText(SMALL_FONT_ID, pageWidth - 20 - bezelRight - renderer.getTextWidth(SMALL_FONT_ID, CROSSPOINT_VERSION),
renderer.drawText(SMALL_FONT_ID,
pageWidth - 20 - bezelRight - renderer.getTextWidth(SMALL_FONT_ID, CROSSPOINT_VERSION),
pageHeight - 60 - bezelBottom, CROSSPOINT_VERSION);
const auto labels = mappedInput.mapLabels("« Back", "Toggle", "", "");

View File

@ -147,7 +147,8 @@ void OtaUpdateActivity::render() {
if (state == WAITING_CONFIRMATION) {
renderer.drawCenteredText(UI_10_FONT_ID, centerY - 100, "New update available!", true, EpdFontFamily::BOLD);
renderer.drawText(UI_10_FONT_ID, 20 + bezelLeft, centerY - 50, "Current Version: " CROSSPOINT_VERSION);
renderer.drawText(UI_10_FONT_ID, 20 + bezelLeft, centerY - 30, ("New Version: " + updater.getLatestVersion()).c_str());
renderer.drawText(UI_10_FONT_ID, 20 + bezelLeft, centerY - 30,
("New Version: " + updater.getLatestVersion()).c_str());
const auto labels = mappedInput.mapLabels("Cancel", "Update", "", "");
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
@ -158,7 +159,9 @@ void OtaUpdateActivity::render() {
if (state == UPDATE_IN_PROGRESS) {
renderer.drawCenteredText(UI_10_FONT_ID, centerY - 40, "Updating...", true, EpdFontFamily::BOLD);
renderer.drawRect(20 + bezelLeft, centerY, pageWidth - 40 - bezelLeft - bezelRight, 50);
renderer.fillRect(24 + bezelLeft, centerY + 4, static_cast<int>(updaterProgress * static_cast<float>(pageWidth - 44 - bezelLeft - bezelRight)), 42);
renderer.fillRect(24 + bezelLeft, centerY + 4,
static_cast<int>(updaterProgress * static_cast<float>(pageWidth - 44 - bezelLeft - bezelRight)),
42);
renderer.drawCenteredText(UI_10_FONT_ID, centerY + 70,
(std::to_string(static_cast<int>(updaterProgress * 100)) + "%").c_str());
renderer.drawCenteredText(

View File

@ -29,8 +29,8 @@ const SettingInfo displaySettings[displaySettingsCount] = {
SettingInfo::Enum("Refresh Frequency", &CrossPointSettings::refreshFrequency,
{"1 page", "5 pages", "10 pages", "15 pages", "30 pages"}),
SettingInfo::Value("Bezel Compensation", &CrossPointSettings::bezelCompensation, {0, 10, 1}),
SettingInfo::Enum("Bezel Edge", &CrossPointSettings::bezelCompensationEdge,
{"Bottom", "Top", "Left", "Right"}, isBezelCompensationEnabled)};
SettingInfo::Enum("Bezel Edge", &CrossPointSettings::bezelCompensationEdge, {"Bottom", "Top", "Left", "Right"},
isBezelCompensationEnabled)};
// Helper to get custom font names as a vector
std::vector<std::string> getCustomFontNamesVector() {
@ -62,8 +62,8 @@ const SettingInfo readerSettings[readerSettingsCount] = {
SettingInfo::Enum("Font Family", &CrossPointSettings::fontFamily, getFontFamilyOptions()),
SettingInfo::Enum("Custom Font", &CrossPointSettings::customFontIndex, getCustomFontNamesVector(),
isCustomFontSelected),
SettingInfo::Enum("Fallback Font", &CrossPointSettings::fallbackFontFamily,
{"Bookerly", "Noto Sans"}, isCustomFontSelected),
SettingInfo::Enum("Fallback Font", &CrossPointSettings::fallbackFontFamily, {"Bookerly", "Noto Sans"},
isCustomFontSelected),
SettingInfo::Enum("Font Size", &CrossPointSettings::fontSize, {"Small", "Medium", "Large", "X Large"}),
SettingInfo::Enum("Line Spacing", &CrossPointSettings::lineSpacing, {"Tight", "Normal", "Wide"}),
SettingInfo::Value("Screen Margin", &CrossPointSettings::screenMargin, {5, 40, 5}),
@ -229,7 +229,8 @@ void SettingsActivity::render() const {
renderer.drawCenteredText(UI_12_FONT_ID, 15 + bezelTop, "Settings", true, EpdFontFamily::BOLD);
// Draw selection
renderer.fillRect(bezelLeft, 60 + bezelTop + selectedCategoryIndex * 30 - 2, pageWidth - 1 - bezelLeft - bezelRight, 30);
renderer.fillRect(bezelLeft, 60 + bezelTop + selectedCategoryIndex * 30 - 2, pageWidth - 1 - bezelLeft - bezelRight,
30);
// Draw all categories
for (int i = 0; i < categoryCount; i++) {
@ -240,7 +241,8 @@ void SettingsActivity::render() const {
}
// Draw version text above button hints
renderer.drawText(SMALL_FONT_ID, pageWidth - 20 - bezelRight - renderer.getTextWidth(SMALL_FONT_ID, CROSSPOINT_VERSION),
renderer.drawText(SMALL_FONT_ID,
pageWidth - 20 - bezelRight - renderer.getTextWidth(SMALL_FONT_ID, CROSSPOINT_VERSION),
pageHeight - 60 - bezelBottom, CROSSPOINT_VERSION);
// Draw help text

View File

@ -1,7 +1,7 @@
#include "KeyboardEntryActivity.h"
#include "activities/dictionary/DictionaryMargins.h"
#include "MappedInputManager.h"
#include "activities/dictionary/DictionaryMargins.h"
#include "fontIds.h"
// Keyboard layouts - lowercase

View File

@ -8,18 +8,10 @@
namespace {
constexpr int MENU_ITEM_COUNT = 4;
const char* MENU_ITEMS[MENU_ITEM_COUNT] = {"Dictionary", "Bookmark", "Clear Cache", "Settings"};
const char* MENU_DESCRIPTIONS_ADD[MENU_ITEM_COUNT] = {
"Look up a word",
"Add bookmark to this page",
"Free up storage space",
"Open settings menu"
};
const char* MENU_DESCRIPTIONS_REMOVE[MENU_ITEM_COUNT] = {
"Look up a word",
"Remove bookmark from this page",
"Free up storage space",
"Open settings menu"
};
const char* MENU_DESCRIPTIONS_ADD[MENU_ITEM_COUNT] = {"Look up a word", "Add bookmark to this page",
"Free up storage space", "Open settings menu"};
const char* MENU_DESCRIPTIONS_REMOVE[MENU_ITEM_COUNT] = {"Look up a word", "Remove bookmark from this page",
"Free up storage space", "Open settings menu"};
} // namespace
void QuickMenuActivity::taskTrampoline(void* param) {

View File

@ -2,8 +2,9 @@
* Generated by convert-builtin-fonts.sh
* Custom font definitions
*/
#include <builtinFonts/custom/customFonts.h>
#include <GfxRenderer.h>
#include <builtinFonts/custom/customFonts.h>
#include "fontIds.h"
// EpdFont definitions for custom fonts
@ -41,14 +42,30 @@ EpdFont fernmicro18BoldFont(&fernmicro_18_bold);
EpdFont fernmicro18BoldItalicFont(&fernmicro_18_bolditalic);
// EpdFontFamily definitions for custom fonts
EpdFontFamily atkinsonhyperlegiblenext12FontFamily(&atkinsonhyperlegiblenext12RegularFont, &atkinsonhyperlegiblenext12BoldFont, &atkinsonhyperlegiblenext12ItalicFont, &atkinsonhyperlegiblenext12BoldItalicFont);
EpdFontFamily atkinsonhyperlegiblenext14FontFamily(&atkinsonhyperlegiblenext14RegularFont, &atkinsonhyperlegiblenext14BoldFont, &atkinsonhyperlegiblenext14ItalicFont, &atkinsonhyperlegiblenext14BoldItalicFont);
EpdFontFamily atkinsonhyperlegiblenext16FontFamily(&atkinsonhyperlegiblenext16RegularFont, &atkinsonhyperlegiblenext16BoldFont, &atkinsonhyperlegiblenext16ItalicFont, &atkinsonhyperlegiblenext16BoldItalicFont);
EpdFontFamily atkinsonhyperlegiblenext18FontFamily(&atkinsonhyperlegiblenext18RegularFont, &atkinsonhyperlegiblenext18BoldFont, &atkinsonhyperlegiblenext18ItalicFont, &atkinsonhyperlegiblenext18BoldItalicFont);
EpdFontFamily fernmicro12FontFamily(&fernmicro12RegularFont, &fernmicro12BoldFont, &fernmicro12ItalicFont, &fernmicro12BoldItalicFont);
EpdFontFamily fernmicro14FontFamily(&fernmicro14RegularFont, &fernmicro14BoldFont, &fernmicro14ItalicFont, &fernmicro14BoldItalicFont);
EpdFontFamily fernmicro16FontFamily(&fernmicro16RegularFont, &fernmicro16BoldFont, &fernmicro16ItalicFont, &fernmicro16BoldItalicFont);
EpdFontFamily fernmicro18FontFamily(&fernmicro18RegularFont, &fernmicro18BoldFont, &fernmicro18ItalicFont, &fernmicro18BoldItalicFont);
EpdFontFamily atkinsonhyperlegiblenext12FontFamily(&atkinsonhyperlegiblenext12RegularFont,
&atkinsonhyperlegiblenext12BoldFont,
&atkinsonhyperlegiblenext12ItalicFont,
&atkinsonhyperlegiblenext12BoldItalicFont);
EpdFontFamily atkinsonhyperlegiblenext14FontFamily(&atkinsonhyperlegiblenext14RegularFont,
&atkinsonhyperlegiblenext14BoldFont,
&atkinsonhyperlegiblenext14ItalicFont,
&atkinsonhyperlegiblenext14BoldItalicFont);
EpdFontFamily atkinsonhyperlegiblenext16FontFamily(&atkinsonhyperlegiblenext16RegularFont,
&atkinsonhyperlegiblenext16BoldFont,
&atkinsonhyperlegiblenext16ItalicFont,
&atkinsonhyperlegiblenext16BoldItalicFont);
EpdFontFamily atkinsonhyperlegiblenext18FontFamily(&atkinsonhyperlegiblenext18RegularFont,
&atkinsonhyperlegiblenext18BoldFont,
&atkinsonhyperlegiblenext18ItalicFont,
&atkinsonhyperlegiblenext18BoldItalicFont);
EpdFontFamily fernmicro12FontFamily(&fernmicro12RegularFont, &fernmicro12BoldFont, &fernmicro12ItalicFont,
&fernmicro12BoldItalicFont);
EpdFontFamily fernmicro14FontFamily(&fernmicro14RegularFont, &fernmicro14BoldFont, &fernmicro14ItalicFont,
&fernmicro14BoldItalicFont);
EpdFontFamily fernmicro16FontFamily(&fernmicro16RegularFont, &fernmicro16BoldFont, &fernmicro16ItalicFont,
&fernmicro16BoldItalicFont);
EpdFontFamily fernmicro18FontFamily(&fernmicro18RegularFont, &fernmicro18BoldFont, &fernmicro18ItalicFont,
&fernmicro18BoldItalicFont);
void registerCustomFonts(GfxRenderer& renderer) {
#if CUSTOM_FONT_COUNT > 0
@ -64,4 +81,3 @@ void registerCustomFonts(GfxRenderer& renderer) {
(void)renderer; // Suppress unused parameter warning
#endif
}

View File

@ -30,6 +30,7 @@
// Custom font ID lookup array: CUSTOM_FONT_IDS[fontIndex][sizeIndex]
// Size indices: 0=12pt, 1=14pt, 2=16pt, 3=18pt
static const int CUSTOM_FONT_IDS[][4] = {
{ATKINSONHYPERLEGIBLENEXT_12_FONT_ID, ATKINSONHYPERLEGIBLENEXT_14_FONT_ID, ATKINSONHYPERLEGIBLENEXT_16_FONT_ID, ATKINSONHYPERLEGIBLENEXT_18_FONT_ID},
{ATKINSONHYPERLEGIBLENEXT_12_FONT_ID, ATKINSONHYPERLEGIBLENEXT_14_FONT_ID, ATKINSONHYPERLEGIBLENEXT_16_FONT_ID,
ATKINSONHYPERLEGIBLENEXT_18_FONT_ID},
{FERNMICRO_12_FONT_ID, FERNMICRO_14_FONT_ID, FERNMICRO_16_FONT_ID, FERNMICRO_18_FONT_ID},
};

View File

@ -11,55 +11,175 @@ static constexpr int LOCK_ICON_HEIGHT = 40;
// Use drawImageRotated() to rotate as needed for different screen orientations
static const uint8_t LockIcon[] = {
// Row 0-1: Empty space above shackle
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
// Row 2-3: Shackle top curve
0x00, 0x0F, 0xF0, 0x00, // ....####....
0x00, 0x3F, 0xFC, 0x00, // ..########..
0x00,
0x0F,
0xF0,
0x00, // ....####....
0x00,
0x3F,
0xFC,
0x00, // ..########..
// Row 4-5: Shackle upper sides
0x00, 0x78, 0x1E, 0x00, // .####..####.
0x00, 0xE0, 0x07, 0x00, // ###......###
0x00,
0x78,
0x1E,
0x00, // .####..####.
0x00,
0xE0,
0x07,
0x00, // ###......###
// Row 6-9: Extended shackle legs (longer for better visual)
0x00, 0xC0, 0x03, 0x00, // ##........##
0x01, 0xC0, 0x03, 0x80, // ###......###
0x01, 0x80, 0x01, 0x80, // ##........##
0x01, 0x80, 0x01, 0x80, // ##........##
0x00,
0xC0,
0x03,
0x00, // ##........##
0x01,
0xC0,
0x03,
0x80, // ###......###
0x01,
0x80,
0x01,
0x80, // ##........##
0x01,
0x80,
0x01,
0x80, // ##........##
// Row 10-13: Shackle legs continue into body
0x01, 0x80, 0x01, 0x80, // ##........##
0x01, 0x80, 0x01, 0x80, // ##........##
0x01, 0x80, 0x01, 0x80, // ##........##
0x01, 0x80, 0x01, 0x80, // ##........##
0x01,
0x80,
0x01,
0x80, // ##........##
0x01,
0x80,
0x01,
0x80, // ##........##
0x01,
0x80,
0x01,
0x80, // ##........##
0x01,
0x80,
0x01,
0x80, // ##........##
// Row 14-15: Body top
0x0F, 0xFF, 0xFF, 0xF0, // ############
0x1F, 0xFF, 0xFF, 0xF8, // ##############
0x0F,
0xFF,
0xFF,
0xF0, // ############
0x1F,
0xFF,
0xFF,
0xF8, // ##############
// Row 16-17: Body top edge
0x3F, 0xFF, 0xFF, 0xFC, // ################
0x3F, 0xFF, 0xFF, 0xFC, // ################
0x3F,
0xFF,
0xFF,
0xFC, // ################
0x3F,
0xFF,
0xFF,
0xFC, // ################
// Row 18-29: Solid body (no keyhole)
0x3F, 0xFF, 0xFF, 0xFC,
0x3F, 0xFF, 0xFF, 0xFC,
0x3F, 0xFF, 0xFF, 0xFC,
0x3F, 0xFF, 0xFF, 0xFC,
0x3F, 0xFF, 0xFF, 0xFC,
0x3F, 0xFF, 0xFF, 0xFC,
0x3F, 0xFF, 0xFF, 0xFC,
0x3F, 0xFF, 0xFF, 0xFC,
0x3F, 0xFF, 0xFF, 0xFC,
0x3F, 0xFF, 0xFF, 0xFC,
0x3F, 0xFF, 0xFF, 0xFC,
0x3F, 0xFF, 0xFF, 0xFC,
0x3F,
0xFF,
0xFF,
0xFC,
0x3F,
0xFF,
0xFF,
0xFC,
0x3F,
0xFF,
0xFF,
0xFC,
0x3F,
0xFF,
0xFF,
0xFC,
0x3F,
0xFF,
0xFF,
0xFC,
0x3F,
0xFF,
0xFF,
0xFC,
0x3F,
0xFF,
0xFF,
0xFC,
0x3F,
0xFF,
0xFF,
0xFC,
0x3F,
0xFF,
0xFF,
0xFC,
0x3F,
0xFF,
0xFF,
0xFC,
0x3F,
0xFF,
0xFF,
0xFC,
0x3F,
0xFF,
0xFF,
0xFC,
// Row 30-33: Body lower section
0x3F, 0xFF, 0xFF, 0xFC,
0x3F, 0xFF, 0xFF, 0xFC,
0x3F, 0xFF, 0xFF, 0xFC,
0x3F, 0xFF, 0xFF, 0xFC,
0x3F,
0xFF,
0xFF,
0xFC,
0x3F,
0xFF,
0xFF,
0xFC,
0x3F,
0xFF,
0xFF,
0xFC,
0x3F,
0xFF,
0xFF,
0xFC,
// Row 34-35: Body bottom edge
0x3F, 0xFF, 0xFF, 0xFC,
0x1F, 0xFF, 0xFF, 0xF8,
0x3F,
0xFF,
0xFF,
0xFC,
0x1F,
0xFF,
0xFF,
0xF8,
// Row 36-37: Body bottom
0x0F, 0xFF, 0xFF, 0xF0,
0x00, 0x00, 0x00, 0x00,
0x0F,
0xFF,
0xFF,
0xF0,
0x00,
0x00,
0x00,
0x00,
// Row 38-39: Empty space below
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
};

View File

@ -1,4 +1,5 @@
#include <Arduino.h>
#include <BitmapHelpers.h>
#include <EInkDisplay.h>
#include <Epub.h>
#include <GfxRenderer.h>
@ -10,8 +11,6 @@
#include <cstring>
#include <BitmapHelpers.h>
#include "Battery.h"
#include "BookListStore.h"
#include "CrossPointSettings.h"
@ -123,11 +122,8 @@ unsigned long t2 = 0;
// Memory debugging helper - logs heap state for tracking leaks
#ifdef DEBUG_MEMORY
void logMemoryState(const char* tag, const char* context) {
Serial.printf("[%lu] [%s] [MEM] %s - Free: %d, Largest: %d, MinFree: %d\n",
millis(), tag, context,
ESP.getFreeHeap(),
ESP.getMaxAllocHeap(),
ESP.getMinFreeHeap());
Serial.printf("[%lu] [%s] [MEM] %s - Free: %d, Largest: %d, MinFree: %d\n", millis(), tag, context, ESP.getFreeHeap(),
ESP.getMaxAllocHeap(), ESP.getMinFreeHeap());
}
#else
// No-op when not in debug mode
@ -334,9 +330,8 @@ void onGoToClearCache();
void onGoToSettings();
void onGoToReader(const std::string& initialEpubPath, MyLibraryActivity::Tab fromTab) {
exitActivity();
enterNewActivity(
new ReaderActivity(renderer, mappedInputManager, initialEpubPath, fromTab, onGoHome, onGoToMyLibraryWithTab,
onGoToClearCache, onGoToSettings));
enterNewActivity(new ReaderActivity(renderer, mappedInputManager, initialEpubPath, fromTab, onGoHome,
onGoToMyLibraryWithTab, onGoToClearCache, onGoToSettings));
}
void onContinueReading() { onGoToReader(APP_STATE.openEpubPath, MyLibraryActivity::Tab::Recent); }
@ -351,8 +346,7 @@ void onGoToReaderFromList(const std::string& bookPath) {
// View a specific list
void onGoToListView(const std::string& listName) {
exitActivity();
enterNewActivity(
new ListViewActivity(renderer, mappedInputManager, listName, onGoToMyLibrary, onGoToReaderFromList));
enterNewActivity(new ListViewActivity(renderer, mappedInputManager, listName, onGoToMyLibrary, onGoToReaderFromList));
}
// View bookmarks for a specific book
@ -365,9 +359,8 @@ void onGoToBookmarkList(const std::string& bookPath, const std::string& bookTitl
// Navigate to bookmark location in the book
// For now, just open the book (TODO: pass bookmark location to reader)
exitActivity();
enterNewActivity(new ReaderActivity(renderer, mappedInputManager, bookPath,
MyLibraryActivity::Tab::Bookmarks, onGoHome, onGoToMyLibraryWithTab,
onGoToClearCache, onGoToSettings));
enterNewActivity(new ReaderActivity(renderer, mappedInputManager, bookPath, MyLibraryActivity::Tab::Bookmarks,
onGoHome, onGoToMyLibraryWithTab, onGoToClearCache, onGoToSettings));
}));
}
@ -402,12 +395,14 @@ void onGoToClearCache() {
void onGoToMyLibrary() {
exitActivity();
enterNewActivity(new MyLibraryActivity(renderer, mappedInputManager, onGoHome, onGoToReader, onGoToListView, onGoToBookmarkList));
enterNewActivity(
new MyLibraryActivity(renderer, mappedInputManager, onGoHome, onGoToReader, onGoToListView, onGoToBookmarkList));
}
void onGoToMyLibraryWithTab(const std::string& path, MyLibraryActivity::Tab tab) {
exitActivity();
enterNewActivity(new MyLibraryActivity(renderer, mappedInputManager, onGoHome, onGoToReader, onGoToListView, onGoToBookmarkList, tab, path));
enterNewActivity(new MyLibraryActivity(renderer, mappedInputManager, onGoHome, onGoToReader, onGoToListView,
onGoToBookmarkList, tab, path));
}
void onGoToBrowser() {
@ -462,10 +457,14 @@ bool isWakeupByPowerButton() {
void setup() {
t1 = millis();
// Only start serial if USB connected
// Always initialize Serial but make it non-blocking
// This prevents Serial.printf from blocking when USB is disconnected
Serial.begin(115200);
Serial.setTxTimeoutMs(0); // Non-blocking TX - critical for USB disconnect handling
// Only wait for Serial to be ready if USB is connected
pinMode(UART0_RXD, INPUT);
if (isUsbConnected()) {
Serial.begin(115200);
// Wait up to 3 seconds for Serial to be ready to catch early logs
unsigned long start = millis();
while (!Serial && (millis() - start) < 3000) {
@ -547,8 +546,7 @@ void loop() {
multi_heap_info_t info;
heap_caps_get_info(&info, MALLOC_CAP_8BIT);
Serial.printf("[%lu] [HEAP] Largest: %d, Allocated: %d, Blocks: %d, Free blocks: %d\n", millis(),
info.largest_free_block, info.total_allocated_bytes,
info.allocated_blocks, info.free_blocks);
info.largest_free_block, info.total_allocated_bytes, info.allocated_blocks, info.free_blocks);
lastMemPrint = millis();
}
@ -562,6 +560,7 @@ void loop() {
const unsigned long sleepTimeoutMs = SETTINGS.getSleepTimeoutMs();
if (millis() - lastActivityTime >= sleepTimeoutMs) {
if (Serial)
Serial.printf("[%lu] [SLP] Auto-sleep triggered after %lu ms of inactivity\n", millis(), sleepTimeoutMs);
enterDeepSleep();
// This should never be hit as `enterDeepSleep` calls esp_deep_sleep_start
@ -584,7 +583,7 @@ void loop() {
const unsigned long loopDuration = millis() - loopStartTime;
if (loopDuration > maxLoopDuration) {
maxLoopDuration = loopDuration;
if (maxLoopDuration > 50) {
if (Serial && maxLoopDuration > 50) {
Serial.printf("[%lu] [LOOP] New max loop duration: %lu ms (activity: %lu ms)\n", millis(), maxLoopDuration,
activityDuration);
}