diff --git a/.gitignore b/.gitignore index 71d1217..71a7172 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ build **/__pycache__/ test/epubs/ CrossPoint-ef.md +Serial_print.code-search # Gitea Actions runner config (contains credentials) .runner diff --git a/claude_notes/missing-serial-guards-2026-01-28.md b/claude_notes/missing-serial-guards-2026-01-28.md new file mode 100644 index 0000000..47ed30c --- /dev/null +++ b/claude_notes/missing-serial-guards-2026-01-28.md @@ -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. diff --git a/claude_notes/serial-blocking-debug-2026-01-28.md b/claude_notes/serial-blocking-debug-2026-01-28.md new file mode 100644 index 0000000..198f33a --- /dev/null +++ b/claude_notes/serial-blocking-debug-2026-01-28.md @@ -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) diff --git a/lib/Epub/Epub.cpp b/lib/Epub/Epub.cpp index 312b8bd..d2d0ad4 100644 --- a/lib/Epub/Epub.cpp +++ b/lib/Epub/Epub.cpp @@ -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& 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& 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) { diff --git a/lib/Epub/Epub/Page.h b/lib/Epub/Epub/Page.h index 969bb73..f362a80 100644 --- a/lib/Epub/Epub/Page.h +++ b/lib/Epub/Epub/Page.h @@ -57,12 +57,12 @@ class Page { public: // the list of block index and line numbers on this page std::vector> elements; - + // Byte offset in source HTML where this page's content begins // Used for restoring reading position after re-indexing due to font/setting changes // This is stored in the Section file's LUT, not in Page serialization uint32_t firstContentOffset = 0; - + void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) const; bool serialize(FsFile& file) const; static std::unique_ptr deserialize(FsFile& file); diff --git a/lib/Epub/Epub/Section.cpp b/lib/Epub/Epub/Section.cpp index ee8b91a..b8f38df 100644 --- a/lib/Epub/Epub/Section.cpp +++ b/lib/Epub/Epub/Section.cpp @@ -186,7 +186,7 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c } writeSectionFileHeader(fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth, viewportHeight, hyphenationEnabled); - + // LUT entries: { filePosition, contentOffset } pairs struct LutEntry { uint32_t filePos; @@ -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(); @@ -217,7 +217,7 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c // Add placeholder to LUT const uint32_t filePos = this->onPageComplete(std::move(placeholderPage)); lut.push_back({filePos, 0}); - + // If we still have no pages, the placeholder creation failed if (pageCount == 0) { Serial.printf("[%lu] [SCT] Failed to create placeholder page\n", millis()); @@ -262,13 +262,13 @@ std::unique_ptr Section::loadPageFromSectionFile() { file.seek(HEADER_SIZE - sizeof(uint32_t)); uint32_t lutOffset; serialization::readPod(file, lutOffset); - + // LUT entries are now 8 bytes each: { filePos (4), contentOffset (4) } file.seek(lutOffset + LUT_ENTRY_SIZE * currentPage); uint32_t pagePos; serialization::readPod(file, pagePos); // Skip contentOffset for now - we don't need it when just loading the page - + file.seek(pagePos); auto page = Page::deserialize(file); @@ -300,15 +300,15 @@ int Section::findPageForContentOffset(uint32_t targetOffset) const { while (left <= right) { const int mid = left + (right - left) / 2; - + // Read content offset for page 'mid' // LUT entry format: { filePos (4), contentOffset (4) } f.seek(lutOffset + LUT_ENTRY_SIZE * mid + sizeof(uint32_t)); // Skip filePos uint32_t midOffset; serialization::readPod(f, midOffset); - + if (midOffset <= targetOffset) { - result = mid; // This page could be the answer + result = mid; // This page could be the answer left = mid + 1; // Look for a later page that might also qualify } else { right = mid - 1; // Look for an earlier page @@ -322,7 +322,7 @@ int Section::findPageForContentOffset(uint32_t targetOffset) const { f.seek(lutOffset + LUT_ENTRY_SIZE * result + sizeof(uint32_t)); uint32_t resultOffset; serialization::readPod(f, resultOffset); - + while (result > 0) { f.seek(lutOffset + LUT_ENTRY_SIZE * (result - 1) + sizeof(uint32_t)); uint32_t prevOffset; diff --git a/lib/Epub/Epub/Section.h b/lib/Epub/Epub/Section.h index 4294ef1..daebbdc 100644 --- a/lib/Epub/Epub/Section.h +++ b/lib/Epub/Epub/Section.h @@ -36,7 +36,7 @@ class Section { const std::function& progressSetupFn = nullptr, const std::function& progressFn = nullptr); std::unique_ptr loadPageFromSectionFile(); - + // Methods for content offset-based position tracking // Used to restore reading position after re-indexing due to font/setting changes int findPageForContentOffset(uint32_t targetOffset) const; diff --git a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp index d77b0c0..63483f4 100644 --- a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp +++ b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp @@ -83,7 +83,7 @@ void ChapterHtmlSlimParser::updateEffectiveInlineStyle() { // Flush the contents of partWordBuffer to currentTextBlock void ChapterHtmlSlimParser::flushPartWordBuffer() { if (partWordBufferIndex == 0) return; - + // Determine font style using effective styles EpdFontFamily::Style fontStyle = EpdFontFamily::REGULAR; if (effectiveBold && effectiveItalic) { @@ -93,7 +93,7 @@ void ChapterHtmlSlimParser::flushPartWordBuffer() { } else if (effectiveItalic) { fontStyle = EpdFontFamily::ITALIC; } - + // Flush the buffer partWordBuffer[partWordBufferIndex] = '\0'; currentTextBlock->addWord(partWordBuffer, fontStyle, effectiveUnderline); @@ -290,7 +290,7 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* } else { placeholder = "[Image unavailable]"; } - + self->startNewTextBlock(TextBlock::CENTER_ALIGN); self->italicUntilDepth = std::min(self->italicUntilDepth, self->depth); self->depth += 1; @@ -478,7 +478,7 @@ void XMLCALL ChapterHtmlSlimParser::characterData(void* userData, const XML_Char if (self->skipUntilDepth < self->depth) { return; } - + // Capture byte offset of this character data for page position tracking if (self->xmlParser) { self->lastCharDataOffset = XML_GetCurrentByteIndex(self->xmlParser); @@ -647,7 +647,7 @@ bool ChapterHtmlSlimParser::parseAndBuildPages() { const size_t totalSize = file.size(); size_t bytesRead = 0; int lastProgress = -1; - + // Initialize offset tracking - first page starts at offset 0 currentPageStartOffset = 0; lastCharDataOffset = 0; @@ -739,7 +739,7 @@ void ChapterHtmlSlimParser::addLineToPage(std::shared_ptr line) { currentPage->firstContentOffset = static_cast(currentPageStartOffset); } completePageFn(std::move(currentPage)); - + // Start new page - offset will be set when first content is added currentPage.reset(new Page()); currentPageStartOffset = lastCharDataOffset; // Use offset from when content was parsed diff --git a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h index cad8fad..0573d0e 100644 --- a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h +++ b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h @@ -58,11 +58,11 @@ class ChapterHtmlSlimParser { bool effectiveBold = false; bool effectiveItalic = false; bool effectiveUnderline = false; - + // Byte offset tracking for position restoration after re-indexing - XML_Parser xmlParser = nullptr; // Store parser for getting current byte index - size_t currentPageStartOffset = 0; // Byte offset when current page was started - size_t lastCharDataOffset = 0; // Byte offset of last character data (captured during parsing) + XML_Parser xmlParser = nullptr; // Store parser for getting current byte index + size_t currentPageStartOffset = 0; // Byte offset when current page was started + size_t lastCharDataOffset = 0; // Byte offset of last character data (captured during parsing) void updateEffectiveInlineStyle(); void startNewTextBlock(TextBlock::Style style); diff --git a/lib/GfxRenderer/BitmapHelpers.cpp b/lib/GfxRenderer/BitmapHelpers.cpp index e3a45be..97bab40 100644 --- a/lib/GfxRenderer/BitmapHelpers.cpp +++ b/lib/GfxRenderer/BitmapHelpers.cpp @@ -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 diff --git a/lib/GfxRenderer/GfxRenderer.cpp b/lib/GfxRenderer/GfxRenderer.cpp index 9683ba8..f98807c 100644 --- a/lib/GfxRenderer/GfxRenderer.cpp +++ b/lib/GfxRenderer/GfxRenderer.cpp @@ -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(std::floor(logicalY * scale)); // For upscaling, calculate the end position for this source row - const int screenYEnd = isUpscaling ? (y + static_cast(std::floor((logicalY + 1) * scale))) : (screenYStart + 1); + const int screenYEnd = + isUpscaling ? (y + static_cast(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(std::floor(srcX * scale)); // For upscaling, calculate the end position for this source pixel - const int screenXEnd = isUpscaling ? (x + static_cast(std::floor((srcX + 1) * scale))) : (screenXStart + 1); + const int screenXEnd = + isUpscaling ? (x + static_cast(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(std::floor(logicalY * scale)); // For upscaling, calculate the end position for this source row - const int screenYEnd = isUpscaling ? (y + static_cast(std::floor((logicalY + 1) * scale))) : (screenYStart + 1); + const int screenYEnd = + isUpscaling ? (y + static_cast(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(std::floor(bmpX * scale)); // For upscaling, calculate the end position for this source pixel - const int screenXEnd = isUpscaling ? (x + static_cast(std::floor((bmpX + 1) * scale))) : (screenXStart + 1); + const int screenXEnd = + isUpscaling ? (x + static_cast(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; } } - diff --git a/lib/GfxRenderer/GfxRenderer.h b/lib/GfxRenderer/GfxRenderer.h index 4f37531..9b82ec9 100644 --- a/lib/GfxRenderer/GfxRenderer.h +++ b/lib/GfxRenderer/GfxRenderer.h @@ -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; diff --git a/lib/StarDict/DictHtmlParser.cpp b/lib/StarDict/DictHtmlParser.cpp index 0e5c5dd..55d9d35 100644 --- a/lib/StarDict/DictHtmlParser.cpp +++ b/lib/StarDict/DictHtmlParser.cpp @@ -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"; } diff --git a/lib/StarDict/DictHtmlParser.h b/lib/StarDict/DictHtmlParser.h index 6eb501c..081c815 100644 --- a/lib/StarDict/DictHtmlParser.h +++ b/lib/StarDict/DictHtmlParser.h @@ -10,7 +10,7 @@ class GfxRenderer; /** * DictHtmlParser parses HTML dictionary definitions into ParsedText. - * + * * Supports: * - Bold: , * - Italic: , @@ -25,7 +25,7 @@ class DictHtmlParser { /** * Parse HTML definition and populate ParsedText with styled words. * Each paragraph/block creates a separate ParsedText via the callback. - * + * * @param html The HTML definition text * @param fontId Font ID for text width calculations * @param renderer Reference to renderer for layout diff --git a/lib/StarDict/StarDict.cpp b/lib/StarDict/StarDict.cpp index 510bedd..3c49456 100644 --- a/lib/StarDict/StarDict.cpp +++ b/lib/StarDict/StarDict.cpp @@ -588,8 +588,12 @@ static std::string decodeHtmlEntity(const std::string& html, size_t& i) { const char* replacement; }; static const EntityMapping entities[] = { - {" ", " "}, {"<", "<"}, {">", ">"}, - {"&", "&"}, {""", "\""}, {"'", "'"}, + {" ", " "}, + {"<", "<"}, + {">", ">"}, + {"&", "&"}, + {""", "\""}, + {"'", "'"}, {"—", "\xe2\x80\x94"}, // — {"–", "\xe2\x80\x93"}, // – {"…", "\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(html[tagEnd])) && - html[tagEnd] != '>' && html[tagEnd] != '/') { + while (tagEnd < html.length() && !std::isspace(static_cast(html[tagEnd])) && html[tagEnd] != '>' && + html[tagEnd] != '/') { tagEnd++; } diff --git a/lib/StarDict/StarDict.h b/lib/StarDict/StarDict.h index 4668c90..d3358c4 100644 --- a/lib/StarDict/StarDict.h +++ b/lib/StarDict/StarDict.h @@ -32,7 +32,7 @@ class StarDict { struct DictzipInfo { uint32_t chunkLength = 0; // Uncompressed chunk size (usually 58315) uint16_t chunkCount = 0; - uint32_t headerSize = 0; // Total header size to skip + uint32_t headerSize = 0; // Total header size to skip uint16_t* chunkSizes = nullptr; // Array of compressed chunk sizes bool loaded = false; }; diff --git a/lib/Txt/Txt.cpp b/lib/Txt/Txt.cpp index c0fa8df..bd58572 100644 --- a/lib/Txt/Txt.cpp +++ b/lib/Txt/Txt.cpp @@ -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(); diff --git a/platformio.ini b/platformio.ini index bad506d..27a7dcb 100644 --- a/platformio.ini +++ b/platformio.ini @@ -2,7 +2,7 @@ default_envs = default [crosspoint] -version = 0.15.0 +version = ef-0.15.9 [base] platform = espressif32 @ 6.12.0 diff --git a/src/BookListStore.cpp b/src/BookListStore.cpp index f46609c..2fd44d9 100644 --- a/src/BookListStore.cpp +++ b/src/BookListStore.cpp @@ -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); diff --git a/src/BookListStore.h b/src/BookListStore.h index f37d9eb..a630e38 100644 --- a/src/BookListStore.h +++ b/src/BookListStore.h @@ -81,5 +81,4 @@ class BookListStore { * @return Book count, or -1 if list doesn't exist */ static int getBookCount(const std::string& name); - }; diff --git a/src/BookManager.cpp b/src/BookManager.cpp index f93153f..83fe271 100644 --- a/src/BookManager.cpp +++ b/src/BookManager.cpp @@ -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{}(path); -} +size_t BookManager::computePathHash(const std::string& path) { return std::hash{}(path); } std::string BookManager::getCachePrefix(const std::string& path) { const std::string ext = getExtension(path); diff --git a/src/BookmarkStore.cpp b/src/BookmarkStore.cpp index 693f176..76b0027 100644 --- a/src/BookmarkStore.cpp +++ b/src/BookmarkStore.cpp @@ -20,11 +20,10 @@ constexpr int MAX_BOOKMARKS_PER_BOOK = 100; // Get cache directory path for a book (same logic as BookManager) std::string getCacheDir(const std::string& bookPath) { const size_t hash = std::hash{}(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); } @@ -47,21 +46,21 @@ std::vector BookmarkStore::getBookmarks(const std::string& bookPath) { bool BookmarkStore::addBookmark(const std::string& bookPath, const Bookmark& bookmark) { std::vector bookmarks; loadBookmarks(bookPath, bookmarks); - + // Check if bookmark already exists at this location auto it = std::find_if(bookmarks.begin(), bookmarks.end(), [&](const Bookmark& b) { return b.spineIndex == bookmark.spineIndex && b.contentOffset == bookmark.contentOffset; }); - + 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; } - + // Add new bookmark bookmarks.push_back(bookmark); - + // Trim to max size (remove oldest) if (bookmarks.size() > MAX_BOOKMARKS_PER_BOOK) { // Sort by timestamp and remove oldest @@ -70,100 +69,99 @@ bool BookmarkStore::addBookmark(const std::string& bookPath, const Bookmark& boo }); bookmarks.resize(MAX_BOOKMARKS_PER_BOOK); } - + return saveBookmarks(bookPath, bookmarks); } bool BookmarkStore::removeBookmark(const std::string& bookPath, uint16_t spineIndex, uint32_t contentOffset) { std::vector bookmarks; loadBookmarks(bookPath, bookmarks); - + auto it = std::find_if(bookmarks.begin(), bookmarks.end(), [&](const Bookmark& b) { return b.spineIndex == spineIndex && b.contentOffset == contentOffset; }); - + if (it == bookmarks.end()) { return false; } - + bookmarks.erase(it); Serial.printf("[%lu] [BMS] Removed bookmark at spine %u, offset %u\n", millis(), spineIndex, contentOffset); - + return saveBookmarks(bookPath, bookmarks); } bool BookmarkStore::isPageBookmarked(const std::string& bookPath, uint16_t spineIndex, uint32_t contentOffset) { std::vector 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) { const std::string filePath = getBookmarksFilePath(bookPath); if (filePath.empty()) return 0; - + FsFile inputFile; if (!SdMan.openFileForRead("BMS", filePath, inputFile)) { return 0; } - + uint8_t version; serialization::readPod(inputFile, version); if (version != BOOKMARKS_FILE_VERSION) { inputFile.close(); return 0; } - + uint8_t count; serialization::readPod(inputFile, count); inputFile.close(); - + return count; } std::vector BookmarkStore::getBooksWithBookmarks() { std::vector result; - + // Scan /.crosspoint/ directory for cache folders with bookmarks auto crosspoint = SdMan.open("/.crosspoint"); if (!crosspoint || !crosspoint.isDirectory()) { if (crosspoint) crosspoint.close(); return result; } - + crosspoint.rewindDirectory(); char name[256]; - + for (auto entry = crosspoint.openNextFile(); entry; entry = crosspoint.openNextFile()) { entry.getName(name, sizeof(name)); - + if (!entry.isDirectory()) { entry.close(); continue; } - + // Check if this directory has a bookmarks file std::string dirPath = "/.crosspoint/"; dirPath += name; std::string bookmarksPath = dirPath + "/" + BOOKMARKS_FILENAME; - + if (SdMan.exists(bookmarksPath.c_str())) { // Read the bookmarks file to get count and book info FsFile bookmarksFile; if (SdMan.openFileForRead("BMS", bookmarksPath, bookmarksFile)) { uint8_t version; serialization::readPod(bookmarksFile, version); - + if (version == BOOKMARKS_FILE_VERSION) { uint8_t count; serialization::readPod(bookmarksFile, count); - + // Read book metadata (stored at end of file) std::string bookPath, bookTitle, bookAuthor; - + // Skip bookmark entries to get to metadata for (uint8_t i = 0; i < count; i++) { std::string tempName; @@ -176,12 +174,12 @@ std::vector BookmarkStore::getBooksWithBookmarks() { serialization::readPod(bookmarksFile, tempPage); serialization::readPod(bookmarksFile, tempTimestamp); } - + // Read book metadata serialization::readString(bookmarksFile, bookPath); serialization::readString(bookmarksFile, bookTitle); serialization::readString(bookmarksFile, bookAuthor); - + if (!bookPath.empty() && count > 0) { BookmarkedBook book; book.path = bookPath; @@ -197,19 +195,18 @@ std::vector BookmarkStore::getBooksWithBookmarks() { entry.close(); } 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; } void BookmarkStore::clearBookmarks(const std::string& bookPath) { const std::string filePath = getBookmarksFilePath(bookPath); if (filePath.empty()) return; - + SdMan.remove(filePath.c_str()); Serial.printf("[%lu] [BMS] Cleared all bookmarks for %s\n", millis(), bookPath.c_str()); } @@ -217,21 +214,21 @@ void BookmarkStore::clearBookmarks(const std::string& bookPath) { bool BookmarkStore::saveBookmarks(const std::string& bookPath, const std::vector& bookmarks) { const std::string cacheDir = getCacheDir(bookPath); if (cacheDir.empty()) return false; - + // Make sure the directory exists SdMan.mkdir(cacheDir.c_str()); - + const std::string filePath = cacheDir + "/" + BOOKMARKS_FILENAME; - + FsFile outputFile; if (!SdMan.openFileForWrite("BMS", filePath, outputFile)) { return false; } - + serialization::writePod(outputFile, BOOKMARKS_FILE_VERSION); const uint8_t count = static_cast(std::min(bookmarks.size(), static_cast(255))); serialization::writePod(outputFile, count); - + for (size_t i = 0; i < count; i++) { const auto& bookmark = bookmarks[i]; serialization::writeString(outputFile, bookmark.name); @@ -240,7 +237,7 @@ bool BookmarkStore::saveBookmarks(const std::string& bookPath, const std::vector serialization::writePod(outputFile, bookmark.pageNumber); serialization::writePod(outputFile, bookmark.timestamp); } - + // Store book metadata at end (for getBooksWithBookmarks to read) // Extract title from path if we don't have it std::string title = bookPath; @@ -252,11 +249,11 @@ bool BookmarkStore::saveBookmarks(const std::string& bookPath, const std::vector if (dot != std::string::npos) { title.resize(dot); } - + serialization::writeString(outputFile, bookPath); serialization::writeString(outputFile, title); serialization::writeString(outputFile, ""); // Author (not always available) - + outputFile.close(); Serial.printf("[%lu] [BMS] Bookmarks saved for %s (%d entries)\n", millis(), bookPath.c_str(), count); return true; @@ -264,15 +261,15 @@ bool BookmarkStore::saveBookmarks(const std::string& bookPath, const std::vector bool BookmarkStore::loadBookmarks(const std::string& bookPath, std::vector& bookmarks) { bookmarks.clear(); - + const std::string filePath = getBookmarksFilePath(bookPath); if (filePath.empty()) return false; - + FsFile inputFile; if (!SdMan.openFileForRead("BMS", filePath, inputFile)) { return false; } - + uint8_t version; serialization::readPod(inputFile, version); if (version != BOOKMARKS_FILE_VERSION) { @@ -280,11 +277,11 @@ bool BookmarkStore::loadBookmarks(const std::string& bookPath, std::vector/bookmarks.bin - * + * * This is a static utility class, not a singleton, since bookmarks * are loaded/saved on demand for specific books. */ @@ -30,34 +30,34 @@ class BookmarkStore { public: // Get all bookmarks for a book static std::vector getBookmarks(const std::string& bookPath); - + // Add a bookmark to a book // Returns true if added, false if bookmark already exists at that location static bool addBookmark(const std::string& bookPath, const Bookmark& bookmark); - + // Remove a bookmark from a book by content offset // Returns true if removed, false if not found static bool removeBookmark(const std::string& bookPath, uint16_t spineIndex, uint32_t contentOffset); - + // Check if a specific page is bookmarked static bool isPageBookmarked(const std::string& bookPath, uint16_t spineIndex, uint32_t contentOffset); - + // Get count of bookmarks for a book (without loading all data) static int getBookmarkCount(const std::string& bookPath); - + // Get all books that have bookmarks (for Bookmarks tab) static std::vector getBooksWithBookmarks(); - + // Delete all bookmarks for a book static void clearBookmarks(const std::string& bookPath); - + private: // Get the bookmarks file path for a book static std::string getBookmarksFilePath(const std::string& bookPath); - + // Save bookmarks to file static bool saveBookmarks(const std::string& bookPath, const std::vector& bookmarks); - + // Load bookmarks from file static bool loadBookmarks(const std::string& bookPath, std::vector& bookmarks); }; diff --git a/src/CrossPointSettings.cpp b/src/CrossPointSettings.cpp index dd26c82..c47d632 100644 --- a/src/CrossPointSettings.cpp +++ b/src/CrossPointSettings.cpp @@ -193,7 +193,7 @@ bool CrossPointSettings::loadFromFile() { float CrossPointSettings::getReaderLineCompression() const { // For custom fonts, use the fallback font's line compression const uint8_t effectiveFamily = (fontFamily == CUSTOM_FONT) ? fallbackFontFamily : fontFamily; - + switch (effectiveFamily) { case BOOKERLY: default: diff --git a/src/CrossPointSettings.h b/src/CrossPointSettings.h index 1ebf254..e345220 100644 --- a/src/CrossPointSettings.h +++ b/src/CrossPointSettings.h @@ -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) @@ -116,7 +123,7 @@ class CrossPointSettings { uint8_t sideButtonLayout = PREV_NEXT; // Reader font settings uint8_t fontFamily = BOOKERLY; - uint8_t customFontIndex = 0; // Which custom font to use (0 to CUSTOM_FONT_COUNT-1) + uint8_t customFontIndex = 0; // Which custom font to use (0 to CUSTOM_FONT_COUNT-1) uint8_t fallbackFontFamily = BOOKERLY; // Fallback for missing glyphs/weights in custom fonts uint8_t fontSize = MEDIUM; uint8_t lineSpacing = NORMAL; diff --git a/src/ScreenComponents.cpp b/src/ScreenComponents.cpp index d6cf35f..c46a767 100644 --- a/src/ScreenComponents.cpp +++ b/src/ScreenComponents.cpp @@ -90,13 +90,14 @@ 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& 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 - constexpr int underlineHeight = 2; // Height of selection underline - constexpr int underlineGap = 4; // Gap between text and underline - constexpr int cursorPadding = 4; // Space between bullet cursor and tab text +int ScreenComponents::drawTabBar(const GfxRenderer& renderer, const int y, const std::vector& 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 + constexpr int underlineHeight = 2; // Height of selection underline + constexpr int underlineGap = 4; // Gap between text and underline + constexpr int cursorPadding = 4; // Space between bullet cursor and tab text constexpr int overflowIndicatorWidth = 16; // Space reserved for < > indicators const int lineHeight = renderer.getLineHeight(UI_12_FONT_ID); @@ -120,7 +121,8 @@ int ScreenComponents::drawTabBar(const GfxRenderer& renderer, const int y, const std::vector 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; } @@ -151,13 +153,13 @@ int ScreenComponents::drawTabBar(const GfxRenderer& renderer, const int y, const // Bullet cursor settings constexpr int bulletRadius = 3; const int bulletCenterY = y + lineHeight / 2; - + // Calculate visible area boundaries (leave room for overflow indicators) const bool hasLeftOverflow = scrollOffset > 0; const bool hasRightOverflow = totalWidth > availableWidth && scrollOffset < totalWidth - availableWidth; const int visibleLeft = bezelLeft + (hasLeftOverflow ? overflowIndicatorWidth : 0); const int visibleRight = screenWidth - bezelRight - (hasRightOverflow ? overflowIndicatorWidth : 0); - + for (size_t i = 0; i < tabs.size(); i++) { const auto& tab = tabs[i]; const int textWidth = tabWidths[i]; @@ -177,7 +179,7 @@ int ScreenComponents::drawTabBar(const GfxRenderer& renderer, const int y, const } } } - + // Draw tab label renderer.drawText(UI_12_FONT_ID, currentX, y, tab.label, true, tab.selected ? EpdFontFamily::BOLD : EpdFontFamily::REGULAR); @@ -195,7 +197,7 @@ int ScreenComponents::drawTabBar(const GfxRenderer& renderer, const int y, const } } } - + // Draw underline for selected tab if (tab.selected) { renderer.fillRect(currentX, y + lineHeight + underlineGap, textWidth, underlineHeight); @@ -210,7 +212,7 @@ int ScreenComponents::drawTabBar(const GfxRenderer& renderer, const int y, const constexpr int triangleHeight = 12; // Height of the triangle (vertical) constexpr int triangleWidth = 6; // Width of the triangle (horizontal) - thin/elongated const int triangleCenterY = y + lineHeight / 2; - + // Left overflow indicator (more content to the left) - thin triangle pointing left if (scrollOffset > 0) { // Clear background behind indicator to hide any overlapping text @@ -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); } } } diff --git a/src/ScreenComponents.h b/src/ScreenComponents.h index 7e46926..886f1d4 100644 --- a/src/ScreenComponents.h +++ b/src/ScreenComponents.h @@ -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& tabs, int selectedIndex = -1, bool showCursor = false); + static int drawTabBar(const GfxRenderer& renderer, int y, const std::vector& 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") diff --git a/src/activities/Activity.h b/src/activities/Activity.h index 7be7350..d15113c 100644 --- a/src/activities/Activity.h +++ b/src/activities/Activity.h @@ -12,13 +12,13 @@ class GfxRenderer; // Helper macro to log stack high-water mark for a task // Usage: LOG_STACK_WATERMARK("ActivityName", taskHandle); -#define LOG_STACK_WATERMARK(name, handle) \ - do { \ - if (handle) { \ - UBaseType_t remaining = uxTaskGetStackHighWaterMark(handle); \ +#define LOG_STACK_WATERMARK(name, handle) \ + do { \ + if (handle) { \ + UBaseType_t remaining = uxTaskGetStackHighWaterMark(handle); \ Serial.printf("[%lu] [STACK] %s: %u bytes remaining\n", millis(), name, remaining * sizeof(StackType_t)); \ - } \ - } while(0) + } \ + } while (0) class Activity { protected: diff --git a/src/activities/boot_sleep/SleepActivity.cpp b/src/activities/boot_sleep/SleepActivity.cpp index 1ee3af5..0ad9d8b 100644 --- a/src/activities/boot_sleep/SleepActivity.cpp +++ b/src/activities/boot_sleep/SleepActivity.cpp @@ -184,7 +184,8 @@ void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap, const std::str drawHeight = 0; fillWidth = static_cast(bitmap.getWidth()); fillHeight = static_cast(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() && @@ -418,10 +418,10 @@ std::string SleepActivity::getEdgeCachePath(const std::string& bmpPath) { uint8_t SleepActivity::quantizeGray(uint8_t lum) { // Quantize luminance (0-255) to 4-level grayscale (0-3) // Thresholds tuned for X4 display gray levels - if (lum < 43) return 0; // black - if (lum < 128) return 1; // dark gray - if (lum < 213) return 2; // light gray - return 3; // white + if (lum < 43) return 0; // black + if (lum < 128) return 1; // dark gray + if (lum < 213) return 2; // light gray + return 3; // white } EdgeLuminance SleepActivity::getEdgeLuminance(const Bitmap& bitmap, const std::string& bmpPath) const { @@ -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(cacheData[0]) | - (static_cast(cacheData[1]) << 8) | + const uint32_t cachedSize = static_cast(cacheData[0]) | (static_cast(cacheData[1]) << 8) | (static_cast(cacheData[2]) << 16) | (static_cast(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(cacheData[0]) | - (static_cast(cacheData[1]) << 8) | + const uint32_t cachedBmpSize = static_cast(cacheData[0]) | (static_cast(cacheData[1]) << 8) | (static_cast(cacheData[2]) << 16) | (static_cast(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(cachedBmpSize), static_cast(currentBmpSize)); + Serial.printf("[SLP] BMP size mismatch (cached=%lu, current=%lu)\n", static_cast(cachedBmpSize), + static_cast(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, diff --git a/src/activities/boot_sleep/SleepActivity.h b/src/activities/boot_sleep/SleepActivity.h index 288727b..77d0177 100644 --- a/src/activities/boot_sleep/SleepActivity.h +++ b/src/activities/boot_sleep/SleepActivity.h @@ -1,10 +1,10 @@ #pragma once -#include "../Activity.h" - #include #include +#include "../Activity.h" + class SleepActivity final : public Activity { public: explicit SleepActivity(GfxRenderer& renderer, MappedInputManager& mappedInput) diff --git a/src/activities/browser/OpdsBookBrowserActivity.cpp b/src/activities/browser/OpdsBookBrowserActivity.cpp index 1dfbc1c..d9679a2 100644 --- a/src/activities/browser/OpdsBookBrowserActivity.cpp +++ b/src/activities/browser/OpdsBookBrowserActivity.cpp @@ -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(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(selectorIndex)); } diff --git a/src/activities/dictionary/DictionaryMenuActivity.cpp b/src/activities/dictionary/DictionaryMenuActivity.cpp index 943f3c6..287a055 100644 --- a/src/activities/dictionary/DictionaryMenuActivity.cpp +++ b/src/activities/dictionary/DictionaryMenuActivity.cpp @@ -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; } diff --git a/src/activities/dictionary/DictionaryResultActivity.cpp b/src/activities/dictionary/DictionaryResultActivity.cpp index 7ccde44..c0294c2 100644 --- a/src/activities/dictionary/DictionaryResultActivity.cpp +++ b/src/activities/dictionary/DictionaryResultActivity.cpp @@ -93,8 +93,8 @@ void DictionaryResultActivity::paginateDefinition() { const auto pageHeight = renderer.getScreenHeight(); // Calculate available area for text (must match render() layout) - constexpr int headerHeight = 80; // Space for word and header (relative to marginTop) - constexpr int footerHeight = 30; // Space for page indicator + constexpr int headerHeight = 80; // Space for word and header (relative to marginTop) + constexpr int footerHeight = 30; // Space for page indicator const int textMargin = marginLeft + 10; const int textWidth = pageWidth - textMargin - marginRight - 10; const int textHeight = pageHeight - marginTop - marginBottom - headerHeight - footerHeight; diff --git a/src/activities/dictionary/DictionaryResultActivity.h b/src/activities/dictionary/DictionaryResultActivity.h index 17040e6..e8bee0a 100644 --- a/src/activities/dictionary/DictionaryResultActivity.h +++ b/src/activities/dictionary/DictionaryResultActivity.h @@ -1,10 +1,9 @@ #pragma once +#include #include #include #include -#include - #include #include #include @@ -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& onBack, - const std::function& onSearchAnother) + const std::function& onBack, const std::function& onSearchAnother) : Activity("DictionaryResult", renderer, mappedInput), lookupWord(wordToLookup), rawDefinition(definition), diff --git a/src/activities/dictionary/EpubWordSelectionActivity.cpp b/src/activities/dictionary/EpubWordSelectionActivity.cpp index b030d22..bd48b0a 100644 --- a/src/activities/dictionary/EpubWordSelectionActivity.cpp +++ b/src/activities/dictionary/EpubWordSelectionActivity.cpp @@ -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(c)); - }); + const bool hasAlpha = std::any_of(wordText.begin(), wordText.end(), + [](char c) { return std::isalpha(static_cast(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", "< >", ""); diff --git a/src/activities/home/BookmarkListActivity.cpp b/src/activities/home/BookmarkListActivity.cpp index 4974a0c..db7c54b 100644 --- a/src/activities/home/BookmarkListActivity.cpp +++ b/src/activities/home/BookmarkListActivity.cpp @@ -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(param); @@ -95,7 +93,7 @@ void BookmarkListActivity::loop() { const auto& bm = bookmarks[selectorIndex]; BookmarkStore::removeBookmark(bookPath, bm.spineIndex, bm.contentOffset); loadBookmarks(); - + // Adjust selector if needed if (selectorIndex >= static_cast(bookmarks.size()) && !bookmarks.empty()) { selectorIndex = static_cast(bookmarks.size()) - 1; @@ -115,9 +113,8 @@ void BookmarkListActivity::loop() { const int itemCount = static_cast(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; @@ -128,7 +125,7 @@ void BookmarkListActivity::loop() { if (mappedInput.getHeldTime() >= ACTION_MENU_MS) { return; // Was a long press } - + if (!bookmarks.empty() && selectorIndex < itemCount) { const auto& bm = bookmarks[selectorIndex]; onSelectBookmark(bm.spineIndex, bm.contentOffset); @@ -190,12 +187,14 @@ 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"); - + const auto labels = mappedInput.mapLabels("\xc2\xab Back", "", "", ""); renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); renderer.displayBuffer(); diff --git a/src/activities/home/BookmarkListActivity.h b/src/activities/home/BookmarkListActivity.h index 75b62a2..da959d8 100644 --- a/src/activities/home/BookmarkListActivity.h +++ b/src/activities/home/BookmarkListActivity.h @@ -50,10 +50,10 @@ class BookmarkListActivity final : public Activity { void renderConfirmation() const; public: - explicit BookmarkListActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, - const std::string& bookPath, const std::string& bookTitle, - const std::function& onGoBack, - const std::function& onSelectBookmark) + explicit BookmarkListActivity( + GfxRenderer& renderer, MappedInputManager& mappedInput, const std::string& bookPath, const std::string& bookTitle, + const std::function& onGoBack, + const std::function& onSelectBookmark) : Activity("BookmarkList", renderer, mappedInput), bookPath(bookPath), bookTitle(bookTitle), diff --git a/src/activities/home/HomeActivity.cpp b/src/activities/home/HomeActivity.cpp index c69d57e..cf546a1 100644 --- a/src/activities/home/HomeActivity.cpp +++ b/src/activities/home/HomeActivity.cpp @@ -157,7 +157,7 @@ bool HomeActivity::storeCoverBuffer() { } const size_t bufferSize = GfxRenderer::getBufferSize(); - + // Reuse existing buffer if already allocated (avoids fragmentation from free+malloc) if (!coverBuffer) { coverBuffer = static_cast(malloc(bufferSize)); @@ -270,7 +270,7 @@ bool HomeActivity::preloadCoverBuffer() { cachedCoverPath = thumbPath; coverBufferStored = false; // Will be set true after actual render in HomeActivity coverRendered = false; // Will trigger load from disk in render() - + Serial.printf("[%lu] [HOME] [MEM] Cover buffer pre-allocated for: %s\n", millis(), thumbPath.c_str()); return true; } @@ -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(fullWidthItems.size()) * (menuTileHeight + menuSpacing); + const int totalMenuHeight = menuTileHeight + static_cast(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 += "..."; } @@ -750,7 +749,7 @@ void HomeActivity::render() { // Draw battery in bottom-left where the back button hint would normally be const bool showBatteryPercentage = SETTINGS.hideBatteryPercentage != CrossPointSettings::HIDE_BATTERY_PERCENTAGE::HIDE_ALWAYS; - constexpr int batteryX = 25; // Align with first button hint position + constexpr int batteryX = 25; // Align with first button hint position const int batteryY = pageHeight - 34; // Vertically centered in button hint area ScreenComponents::drawBatteryLarge(renderer, batteryX, batteryY, showBatteryPercentage); diff --git a/src/activities/home/HomeActivity.h b/src/activities/home/HomeActivity.h index d69c3e6..cf99294 100644 --- a/src/activities/home/HomeActivity.h +++ b/src/activities/home/HomeActivity.h @@ -17,9 +17,9 @@ class HomeActivity final : public Activity { bool hasCoverImage = false; // Static cover buffer - persists across activity changes to avoid reloading from SD - static bool coverRendered; // Track if cover has been rendered once - static bool coverBufferStored; // Track if cover buffer is stored - static uint8_t* coverBuffer; // HomeActivity's own buffer for cover image + static bool coverRendered; // Track if cover has been rendered once + static bool coverBufferStored; // Track if cover buffer is stored + static uint8_t* coverBuffer; // HomeActivity's own buffer for cover image static std::string cachedCoverPath; // Path of the cached cover (to detect book changes) std::string lastBookTitle; @@ -43,15 +43,14 @@ class HomeActivity final : public Activity { public: // Free cover buffer from external activities (e.g., when entering reader to reclaim memory) static void freeCoverBufferIfAllocated(); - + // Preload cover buffer from external activities (e.g., MyLibraryActivity) for instant Home screen // Returns true if cover was successfully preloaded or already cached static bool preloadCoverBuffer(); explicit HomeActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const std::function& onContinueReading, const std::function& onListsOpen, const std::function& onMyLibraryOpen, const std::function& onSettingsOpen, - const std::function& onFileTransferOpen, - const std::function& onOpdsBrowserOpen) + const std::function& onFileTransferOpen, const std::function& onOpdsBrowserOpen) : Activity("Home", renderer, mappedInput), onContinueReading(onContinueReading), onListsOpen(onListsOpen), diff --git a/src/activities/home/ListViewActivity.cpp b/src/activities/home/ListViewActivity.cpp index f5261c3..9ec2dae 100644 --- a/src/activities/home/ListViewActivity.cpp +++ b/src/activities/home/ListViewActivity.cpp @@ -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; @@ -262,8 +262,8 @@ void ListViewActivity::render() const { } // Extract tags for badges (only if we'll show them - when NOT selected) - constexpr int badgeSpacing = 4; // Gap between badges - constexpr int badgePadding = 10; // Horizontal padding inside badge (5 each side) + constexpr int badgeSpacing = 4; // Gap between badges + constexpr int badgePadding = 10; // Horizontal padding inside badge (5 each side) constexpr int badgeToThumbGap = 8; // Gap between rightmost badge and cover art int totalBadgeWidth = 0; BookTags tags; @@ -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()) { diff --git a/src/activities/home/MyLibraryActivity.cpp b/src/activities/home/MyLibraryActivity.cpp index ebe3e86..dc35823 100644 --- a/src/activities/home/MyLibraryActivity.cpp +++ b/src/activities/home/MyLibraryActivity.cpp @@ -85,22 +85,23 @@ int MyLibraryActivity::getPageItems() const { const int bottomBarHeight = 60; // Space for button hints const int bezelTop = renderer.getBezelOffsetTop(); const int bezelBottom = renderer.getBezelOffsetBottom(); - + // Search tab has compact layout: character picker (~30px) + query (~25px) + results if (currentTab == Tab::Search) { // 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; } - + 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; @@ -152,7 +153,7 @@ void MyLibraryActivity::loadLists() { lists = BookListStore::listAllLists(); } void MyLibraryActivity::loadBookmarkedBooks() { bookmarkedBooks = BookmarkStore::getBooksWithBookmarks(); - + // Try to get better metadata from recent books for (auto& book : bookmarkedBooks) { auto it = std::find_if(recentBooks.begin(), recentBooks.end(), @@ -167,7 +168,7 @@ void MyLibraryActivity::loadBookmarkedBooks() { void MyLibraryActivity::loadAllBooks() { // Build index of all books on SD card for search allBooks.clear(); - + // Helper lambda to recursively scan directories std::function scanDirectory = [&](const std::string& path) { auto dir = SdMan.open(path.c_str()); @@ -175,37 +176,36 @@ void MyLibraryActivity::loadAllBooks() { if (dir) dir.close(); return; } - + dir.rewindDirectory(); char name[500]; - + for (auto file = dir.openNextFile(); file; file = dir.openNextFile()) { file.getName(name, sizeof(name)); if (name[0] == '.' || strcmp(name, "System Volume Information") == 0) { file.close(); continue; } - + std::string fullPath = (path.back() == '/') ? path + name : path + "/" + name; - + if (file.isDirectory()) { file.close(); 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; - + // Extract title from filename (remove extension) result.title = filename; const size_t dot = result.title.find_last_of('.'); if (dot != std::string::npos) { result.title.resize(dot); } - + // Try to get metadata from recent books if available auto it = std::find_if(recentBooks.begin(), recentBooks.end(), [&fullPath](const RecentBook& recent) { return recent.path == fullPath; }); @@ -213,7 +213,7 @@ void MyLibraryActivity::loadAllBooks() { if (!it->title.empty()) result.title = it->title; if (!it->author.empty()) result.author = it->author; } - + allBooks.push_back(result); } file.close(); @@ -221,16 +221,15 @@ void MyLibraryActivity::loadAllBooks() { } dir.close(); }; - + scanDirectory("/"); - + // 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(), - [](char c1, char c2) { return tolower(c1) < tolower(c2); }); + return lexicographical_compare(a.title.begin(), a.title.end(), b.title.begin(), b.title.end(), + [](char c1, char c2) { return tolower(c1) < tolower(c2); }); }); - + // Build character set after loading books buildSearchCharacters(); } @@ -238,7 +237,7 @@ void MyLibraryActivity::loadAllBooks() { void MyLibraryActivity::buildSearchCharacters() { // Build a set of unique characters from all book titles and authors std::set charSet; - + for (const auto& book : allBooks) { for (char c : book.title) { // Convert to uppercase for display, store as uppercase @@ -262,29 +261,29 @@ void MyLibraryActivity::buildSearchCharacters() { } } } - + // Convert set to vector, sorted: A-Z, then 0-9, then symbols searchCharacters.clear(); - + // Add letters A-Z for (char c = 'A'; c <= 'Z'; c++) { if (charSet.count(c)) { searchCharacters.push_back(c); } } - + // Add digits 0-9 for (char c = '0'; c <= '9'; c++) { if (charSet.count(c)) { searchCharacters.push_back(c); } } - + // Add symbols (anything else in the set) std::copy_if(charSet.begin(), charSet.end(), std::back_inserter(searchCharacters), [](char c) { return !std::isalpha(static_cast(c)) && !std::isdigit(static_cast(c)); }); - + // Reset character index if it's out of bounds if (searchCharIndex >= static_cast(searchCharacters.size()) + 3) { // +3 for special keys searchCharIndex = 0; @@ -293,17 +292,17 @@ void MyLibraryActivity::buildSearchCharacters() { void MyLibraryActivity::updateSearchResults() { searchResults.clear(); - + if (searchQuery.empty()) { // Don't show any results when query is empty - user needs to type something return; } - + // Convert query to lowercase for case-insensitive matching std::string queryLower = searchQuery; std::transform(queryLower.begin(), queryLower.end(), queryLower.begin(), [](unsigned char c) { return std::tolower(c); }); - + for (const auto& book : allBooks) { // Convert title, author, and path to lowercase std::string titleLower = book.title; @@ -315,9 +314,9 @@ void MyLibraryActivity::updateSearchResults() { [](unsigned char c) { return std::tolower(c); }); std::transform(pathLower.begin(), pathLower.end(), pathLower.begin(), [](unsigned char c) { return std::tolower(c); }); - + int score = 0; - + // Check for matches if (titleLower.find(queryLower) != std::string::npos) { score += 100; @@ -331,19 +330,17 @@ void MyLibraryActivity::updateSearchResults() { if (pathLower.find(queryLower) != std::string::npos) { score += 30; } - + if (score > 0) { SearchResult result = book; result.matchScore = score; searchResults.push_back(result); } } - + // Sort by match score (descending) - std::sort(searchResults.begin(), searchResults.end(), - [](const SearchResult& a, const SearchResult& b) { - return a.matchScore > b.matchScore; - }); + std::sort(searchResults.begin(), searchResults.end(), + [](const SearchResult& a, const SearchResult& b) { return a.matchScore > b.matchScore; }); } void MyLibraryActivity::loadFiles() { @@ -407,14 +404,14 @@ void MyLibraryActivity::onEnter() { loadFiles(); selectorIndex = 0; - + // If entering Search tab, start in character picker mode if (currentTab == Tab::Search) { searchInResults = false; inTabBar = false; searchCharIndex = 0; } - + updateRequired = true; xTaskCreate(&MyLibraryActivity::taskTrampoline, "MyLibraryActivityTask", @@ -491,7 +488,7 @@ void MyLibraryActivity::openActionMenu() { } uiState = UIState::ActionMenu; - menuSelection = 0; // Default to Archive + menuSelection = 0; // Default to Archive ignoreNextConfirmRelease = true; // Ignore the release from the long-press that opened this menu updateRequired = true; } @@ -766,7 +763,7 @@ void MyLibraryActivity::loop() { updateRequired = true; return; } - + if (mappedInput.wasReleased(MappedInputManager::Button::Right)) { switch (currentTab) { case Tab::Recent: @@ -788,7 +785,7 @@ void MyLibraryActivity::loop() { updateRequired = true; return; } - + // Down exits tab bar, enters list at top if (mappedInput.wasReleased(MappedInputManager::Button::Down)) { inTabBar = false; @@ -796,7 +793,7 @@ void MyLibraryActivity::loop() { updateRequired = true; return; } - + // Up exits tab bar, jumps to bottom of list if (mappedInput.wasReleased(MappedInputManager::Button::Up)) { inTabBar = false; @@ -806,13 +803,13 @@ void MyLibraryActivity::loop() { updateRequired = true; return; } - + // Back goes home if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { onGoHome(); return; } - + return; } @@ -820,7 +817,7 @@ void MyLibraryActivity::loop() { if (currentTab == Tab::Search) { const int charCount = static_cast(searchCharacters.size()); const int totalPickerItems = charCount + 3; // +3 for SPC, <-, CLR - + if (inTabBar) { // In tab bar mode - Left/Right switch tabs, Down goes to picker // Use wasReleased for consistency with other tab switching code @@ -830,21 +827,21 @@ void MyLibraryActivity::loop() { updateRequired = true; return; } - + if (mappedInput.wasReleased(MappedInputManager::Button::Right)) { currentTab = Tab::Files; selectorIndex = 0; updateRequired = true; return; } - + // Down exits tab bar, goes to character picker if (mappedInput.wasReleased(MappedInputManager::Button::Down)) { inTabBar = false; updateRequired = true; return; } - + // Up exits tab bar, jumps to bottom of results (if any) if (mappedInput.wasReleased(MappedInputManager::Button::Up)) { inTabBar = false; @@ -855,33 +852,31 @@ void MyLibraryActivity::loop() { updateRequired = true; return; } - + // Back goes home if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { onGoHome(); return; } - + return; } else if (!searchInResults) { // 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; } - + // Left/Right navigate through characters (with wrap) if (mappedInput.wasPressed(MappedInputManager::Button::Left)) { if (searchCharIndex > 0) { @@ -892,7 +887,7 @@ void MyLibraryActivity::loop() { updateRequired = true; return; } - + if (mappedInput.wasPressed(MappedInputManager::Button::Right)) { if (searchCharIndex < totalPickerItems - 1) { searchCharIndex++; @@ -902,7 +897,7 @@ void MyLibraryActivity::loop() { updateRequired = true; return; } - + // Down moves to results (if any exist) if (mappedInput.wasPressed(MappedInputManager::Button::Down)) { if (!searchResults.empty()) { @@ -912,14 +907,14 @@ void MyLibraryActivity::loop() { } return; } - + // Up moves to tab bar if (mappedInput.wasReleased(MappedInputManager::Button::Up)) { inTabBar = true; updateRequired = true; return; } - + // Confirm adds selected character or performs special action if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { if (searchCharIndex < charCount) { @@ -944,10 +939,9 @@ void MyLibraryActivity::loop() { updateRequired = true; return; } - + // 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(); @@ -955,7 +949,7 @@ void MyLibraryActivity::loop() { } return; } - + // Short press Back = backspace (delete one char) if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { if (mappedInput.getHeldTime() >= 700) { @@ -972,29 +966,27 @@ void MyLibraryActivity::loop() { } return; } - + return; // Don't process other input while in picker } else { // 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(searchResults.size()) - 1; } updateRequired = true; return; } - + // Up/Down navigate through results if (mappedInput.wasPressed(MappedInputManager::Button::Up)) { if (selectorIndex > 0) { @@ -1006,7 +998,7 @@ void MyLibraryActivity::loop() { updateRequired = true; return; } - + if (mappedInput.wasPressed(MappedInputManager::Button::Down)) { if (selectorIndex < static_cast(searchResults.size()) - 1) { selectorIndex++; @@ -1017,13 +1009,13 @@ void MyLibraryActivity::loop() { updateRequired = true; return; } - + // Left/Right do nothing in results (or could page?) if (mappedInput.wasPressed(MappedInputManager::Button::Left) || mappedInput.wasPressed(MappedInputManager::Button::Right)) { return; } - + // Confirm opens the selected book if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { if (!searchResults.empty() && selectorIndex < static_cast(searchResults.size())) { @@ -1031,14 +1023,14 @@ void MyLibraryActivity::loop() { } return; } - + // Back button - go back to character picker if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { searchInResults = false; updateRequired = true; return; } - + return; // Don't process other input } } @@ -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; } @@ -1096,7 +1088,7 @@ void MyLibraryActivity::loop() { } else if (currentTab == Tab::Files && selectorIndex == static_cast(files.size())) { isSearchShortcut = true; } - + if (isSearchShortcut) { // Switch to Search tab with character picker active currentTab = Tab::Search; @@ -1107,7 +1099,7 @@ void MyLibraryActivity::loop() { updateRequired = true; return; } - + if (currentTab == Tab::Recent) { if (!recentBooks.empty() && selectorIndex < static_cast(recentBooks.size())) { onSelectBook(recentBooks[selectorIndex].path, currentTab); @@ -1258,14 +1250,14 @@ void MyLibraryActivity::loop() { void MyLibraryActivity::displayTaskLoop() { bool coverPreloaded = false; - + while (true) { if (updateRequired) { updateRequired = false; xSemaphoreTake(renderingMutex, portMAX_DELAY); render(); xSemaphoreGive(renderingMutex); - + // After first render, pre-allocate cover buffer for Home screen // This happens in background so Home screen loads faster when user navigates there if (!coverPreloaded) { @@ -1487,8 +1479,8 @@ void MyLibraryActivity::renderRecentTab() const { } // Extract tags for badges (only if we'll show them - when NOT selected) - constexpr int badgeSpacing = 4; // Gap between badges - constexpr int badgePadding = 10; // Horizontal padding inside badge (5 each side) + constexpr int badgeSpacing = 4; // Gap between badges + constexpr int badgePadding = 10; // Horizontal padding inside badge (5 each side) constexpr int badgeToThumbGap = 8; // Gap between rightmost badge and cover art int totalBadgeWidth = 0; BookTags tags; @@ -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()) { @@ -1542,7 +1534,7 @@ void MyLibraryActivity::renderRecentTab() const { renderer.drawText(UI_10_FONT_ID, LEFT_MARGIN, y + 32, truncatedAuthor.c_str(), !isSelected); } } - + // Draw "Search..." shortcut if it's on the current page const int searchIndex = bookCount; // Last item if (searchIndex >= pageStartIndex && searchIndex < pageStartIndex + pageItems) { @@ -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++) { @@ -1595,7 +1587,7 @@ void MyLibraryActivity::renderListsTab() const { renderer.drawText(UI_10_FONT_ID, LEFT_MARGIN, CONTENT_START_Y + (i % pageItems) * LINE_HEIGHT, item.c_str(), i != selectorIndex); } - + // Draw "Search..." shortcut if it's on the current page const int searchIndex = listCount; // Last item if (searchIndex >= pageStartIndex && searchIndex < pageStartIndex + pageItems) { @@ -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++) { @@ -1641,7 +1633,7 @@ void MyLibraryActivity::renderFilesTab() const { renderer.drawText(UI_10_FONT_ID, LEFT_MARGIN, CONTENT_START_Y + (i % pageItems) * LINE_HEIGHT, item.c_str(), i != selectorIndex); } - + // Draw "Search..." shortcut if it's on the current page const int searchIndex = fileCount; // Last item if (searchIndex >= pageStartIndex && searchIndex < pageStartIndex + pageItems) { @@ -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 @@ -1885,7 +1880,7 @@ void MyLibraryActivity::renderBookmarksTab() const { std::string countText = std::to_string(book.bookmarkCount) + " bookmark" + (book.bookmarkCount != 1 ? "s" : ""); renderer.drawText(UI_10_FONT_ID, LEFT_MARGIN, y + 32, countText.c_str(), !isSelected); } - + // Draw "Search..." shortcut if it's on the current page const int searchIndex = bookCount; // Last item if (searchIndex >= pageStartIndex && searchIndex < pageStartIndex + pageItems) { @@ -1924,12 +1919,13 @@ 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 const int resultsStartY = queryY + QUERY_HEIGHT; - + // Draw results section if (resultCount == 0) { if (searchQuery.empty()) { @@ -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()) { @@ -2016,15 +2012,15 @@ void MyLibraryActivity::renderCharacterPicker(int y) const { const auto pageWidth = renderer.getScreenWidth(); const int bezelLeft = renderer.getBezelOffsetLeft(); const int bezelRight = renderer.getBezelOffsetRight(); - - constexpr int charSpacing = 6; // Spacing between characters - constexpr int specialKeyPadding = 8; // Extra padding around special keys + + constexpr int charSpacing = 6; // Spacing between characters + constexpr int specialKeyPadding = 8; // Extra padding around special keys constexpr int overflowIndicatorWidth = 16; // Space reserved for < > indicators - + // Calculate total width needed const int charCount = static_cast(searchCharacters.size()); const int totalItems = charCount + 3; // +3 for SPC, <-, CLR - + // Calculate character widths int totalWidth = 0; for (char c : searchCharacters) { @@ -2035,14 +2031,14 @@ void MyLibraryActivity::renderCharacterPicker(int y) const { totalWidth += renderer.getTextWidth(UI_10_FONT_ID, "SPC") + specialKeyPadding; totalWidth += renderer.getTextWidth(UI_10_FONT_ID, "<-") + specialKeyPadding; totalWidth += renderer.getTextWidth(UI_10_FONT_ID, "CLR") + specialKeyPadding; - + // Calculate visible window - we'll scroll the character row const int availableWidth = pageWidth - bezelLeft - bezelRight - 40; // 40 for margins (20 each side) - + // Determine scroll offset to keep selected character visible int scrollOffset = 0; int currentX = 0; - + // Calculate position of selected item for (int i = 0; i < totalItems; i++) { int itemWidth; @@ -2056,7 +2052,7 @@ void MyLibraryActivity::renderCharacterPicker(int y) const { } else { itemWidth = renderer.getTextWidth(UI_10_FONT_ID, "CLR") + specialKeyPadding; } - + if (i == searchCharIndex) { // Center the selected item in the visible area scrollOffset = currentX - availableWidth / 2 + itemWidth / 2; @@ -2068,26 +2064,27 @@ void MyLibraryActivity::renderCharacterPicker(int y) const { } currentX += itemWidth; } - + // Draw separator line renderer.drawLine(bezelLeft + 20, y + 22, pageWidth - bezelRight - 20, y + 22); - + // Calculate visible area boundaries (leave room for overflow indicators) const bool hasLeftOverflow = scrollOffset > 0; const bool hasRightOverflow = totalWidth > availableWidth && scrollOffset < totalWidth - availableWidth; const int visibleLeft = bezelLeft + 20 + (hasLeftOverflow ? overflowIndicatorWidth : 0); const int visibleRight = pageWidth - bezelRight - 20 - (hasRightOverflow ? overflowIndicatorWidth : 0); - + // 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; int itemWidth; bool isSpecial = false; - + if (i < charCount) { label = std::string(1, searchCharacters[i]); itemWidth = renderer.getTextWidth(UI_10_FONT_ID, label.c_str()); @@ -2104,12 +2101,12 @@ void MyLibraryActivity::renderCharacterPicker(int y) const { itemWidth = renderer.getTextWidth(UI_10_FONT_ID, label.c_str()); isSpecial = true; } - + // Only draw if visible (accounting for overflow indicator space) const int drawX = currentX + (isSpecial ? specialKeyPadding / 2 : 0); if (drawX + itemWidth > visibleLeft && drawX < visibleRight) { const bool isSelected = showSelection && (i == searchCharIndex); - + if (isSelected) { // Draw inverted background for selection constexpr int padding = 2; @@ -2121,17 +2118,17 @@ void MyLibraryActivity::renderCharacterPicker(int y) const { renderer.drawText(UI_10_FONT_ID, drawX, y, label.c_str()); } } - + currentX += itemWidth + (isSpecial ? specialKeyPadding : charSpacing); } - + // Draw overflow indicators if content extends beyond visible area if (totalWidth > availableWidth) { constexpr int triangleHeight = 12; // Height of the triangle (vertical) constexpr int triangleWidth = 6; // Width of the triangle (horizontal) - thin/elongated const int pickerLineHeight = renderer.getLineHeight(UI_10_FONT_ID); const int triangleCenterY = y + pickerLineHeight / 2; - + // Left overflow indicator (more content to the left) - thin triangle pointing left if (hasLeftOverflow) { // Clear background behind indicator to hide any overlapping text @@ -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); } } } diff --git a/src/activities/home/MyLibraryActivity.h b/src/activities/home/MyLibraryActivity.h index c9fd48e..bf15cc2 100644 --- a/src/activities/home/MyLibraryActivity.h +++ b/src/activities/home/MyLibraryActivity.h @@ -13,10 +13,10 @@ // Cached thumbnail existence info for Recent tab struct ThumbExistsCache { - std::string bookPath; // Book path this cache entry belongs to - std::string thumbPath; // Path to micro-thumbnail (if exists) - bool checked = false; // Whether we've checked for this book - bool exists = false; // Whether thumbnail exists + std::string bookPath; // Book path this cache entry belongs to + std::string thumbPath; // Path to micro-thumbnail (if exists) + bool checked = false; // Whether we've checked for this book + bool exists = false; // Whether thumbnail exists }; // Search result for the Search tab @@ -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: @@ -55,7 +62,7 @@ class MyLibraryActivity final : public Activity { ActionType selectedAction = ActionType::Archive; std::string actionTargetPath; std::string actionTargetName; - int menuSelection = 0; // 0 = Archive, 1 = Delete + int menuSelection = 0; // 0 = Archive, 1 = Delete bool ignoreNextConfirmRelease = false; // Prevents immediate selection after long-press opens menu // Recent tab state @@ -64,13 +71,12 @@ class MyLibraryActivity final : public Activity { // Static thumbnail existence cache - persists across activity enter/exit static constexpr int MAX_THUMB_CACHE = 10; static ThumbExistsCache thumbExistsCache[MAX_THUMB_CACHE]; - + public: // Clear the thumbnail existence cache (call when disk cache is cleared) static void clearThumbExistsCache(); - - private: + private: // Lists tab state std::vector lists; @@ -148,12 +154,12 @@ class MyLibraryActivity final : public Activity { void renderClearAllRecentsConfirmation() const; public: - explicit MyLibraryActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, - const std::function& onGoHome, - const std::function& onSelectBook, - const std::function& onSelectList, - const std::function& onSelectBookmarkedBook = nullptr, - Tab initialTab = Tab::Recent, std::string initialPath = "/") + explicit MyLibraryActivity( + GfxRenderer& renderer, MappedInputManager& mappedInput, const std::function& onGoHome, + const std::function& onSelectBook, + const std::function& onSelectList, + const std::function& onSelectBookmarkedBook = nullptr, + Tab initialTab = Tab::Recent, std::string initialPath = "/") : Activity("MyLibrary", renderer, mappedInput), currentTab(initialTab), basepath(initialPath.empty() ? "/" : std::move(initialPath)), diff --git a/src/activities/network/CrossPointWebServerActivity.cpp b/src/activities/network/CrossPointWebServerActivity.cpp index 671c767..9488bbc 100644 --- a/src/activities/network/CrossPointWebServerActivity.cpp +++ b/src/activities/network/CrossPointWebServerActivity.cpp @@ -600,9 +600,9 @@ void CrossPointWebServerActivity::renderWebBrowserScreen() const { // Landscape layout (800x480): QR on left, text on right constexpr int QR_X = 15; constexpr int QR_Y = 15; - constexpr int QR_PX = 7; // pixels per QR module - constexpr int QR_SIZE = 33 * QR_PX; // QR version 4 = 33 modules = 231px - constexpr int TEXT_X = QR_X + QR_SIZE + 35; // Text starts after QR + margin + constexpr int QR_PX = 7; // pixels per QR module + constexpr int QR_SIZE = 33 * QR_PX; // QR version 4 = 33 modules = 231px + constexpr int TEXT_X = QR_X + QR_SIZE + 35; // Text starts after QR + margin constexpr int LINE_SPACING = 32; // Draw title on right side @@ -667,9 +667,9 @@ void CrossPointWebServerActivity::renderCompanionAppScreen() const { // Landscape layout (800x480): QR on left, text on right constexpr int QR_X = 15; constexpr int QR_Y = 15; - constexpr int QR_PX = 7; // pixels per QR module - constexpr int QR_SIZE = 33 * QR_PX; // QR version 4 = 33 modules = 231px - constexpr int TEXT_X = QR_X + QR_SIZE + 35; // Text starts after QR + margin + constexpr int QR_PX = 7; // pixels per QR module + constexpr int QR_SIZE = 33 * QR_PX; // QR version 4 = 33 modules = 231px + constexpr int TEXT_X = QR_X + QR_SIZE + 35; // Text starts after QR + margin constexpr int LINE_SPACING = 32; // Draw title on right side @@ -717,9 +717,9 @@ void CrossPointWebServerActivity::renderCompanionAppLibraryScreen() const { // Landscape layout (800x480): QR on left, text on right constexpr int QR_X = 15; constexpr int QR_Y = 15; - constexpr int QR_PX = 7; // pixels per QR module - constexpr int QR_SIZE = 33 * QR_PX; // QR version 4 = 33 modules = 231px - constexpr int TEXT_X = QR_X + QR_SIZE + 35; // Text starts after QR + margin + constexpr int QR_PX = 7; // pixels per QR module + constexpr int QR_SIZE = 33 * QR_PX; // QR version 4 = 33 modules = 231px + constexpr int TEXT_X = QR_X + QR_SIZE + 35; // Text starts after QR + margin constexpr int LINE_SPACING = 32; // Draw title on right side diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index 6cc7a89..d142d2a 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -111,24 +111,25 @@ void EpubReaderActivity::onEnter() { FsFile f; if (SdMan.openFileForRead("ERS", epub->getCachePath() + "/progress.bin", f)) { const size_t fileSize = f.size(); - + if (fileSize >= 9) { // New format: version (1) + spineIndex (2) + pageNumber (2) + contentOffset (4) = 9 bytes uint8_t version; serialization::readPod(f, version); - + if (version == EPUB_PROGRESS_VERSION) { uint16_t spineIndex, pageNumber; serialization::readPod(f, spineIndex); serialization::readPod(f, pageNumber); serialization::readPod(f, savedContentOffset); - + currentSpineIndex = spineIndex; 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,20 +150,23 @@ 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; - Serial.printf("[%lu] [ERS] Opened for first time, navigating to text reference at index %d\n", millis(), - textSpineIndex); + if (Serial) + Serial.printf("[%lu] [ERS] Opened for first time, navigating to text reference at index %d\n", millis(), + textSpineIndex); } } @@ -300,19 +305,20 @@ void EpubReaderActivity::loop() { Section* cachedSection = section.get(); SemaphoreHandle_t cachedMutex = renderingMutex; EpubReaderActivity* self = this; - + // Handle dictionary mode selection - exitActivity deletes DictionaryMenuActivity exitActivity(); - + if (mode == DictionaryMode::ENTER_WORD) { // Enter word mode - show keyboard and search - self->enterNewActivity(new DictionarySearchActivity(cachedRenderer, cachedMappedInput, - [self]() { - // On back from dictionary - self->exitActivity(); - self->updateRequired = true; - }, - "")); // Empty string = show keyboard + self->enterNewActivity(new DictionarySearchActivity( + cachedRenderer, cachedMappedInput, + [self]() { + // On back from dictionary + self->exitActivity(); + self->updateRequired = true; + }, + "")); // Empty string = show keyboard } else { // Select from screen mode - show word selection on current page if (cachedSection) { @@ -322,7 +328,7 @@ void EpubReaderActivity::loop() { // Get margins for word selection positioning int orientedMarginTop, orientedMarginRight, orientedMarginBottom, orientedMarginLeft; cachedRenderer.getOrientedViewableTRBL(&orientedMarginTop, &orientedMarginRight, &orientedMarginBottom, - &orientedMarginLeft); + &orientedMarginLeft); orientedMarginTop += SETTINGS.screenMargin; orientedMarginLeft += SETTINGS.screenMargin; @@ -335,12 +341,13 @@ void EpubReaderActivity::loop() { [self](const std::string& selectedWord) { // Word selected - look it up self->exitActivity(); - self->enterNewActivity(new DictionarySearchActivity(self->renderer, self->mappedInput, - [self]() { - self->exitActivity(); - self->updateRequired = true; - }, - selectedWord)); + self->enterNewActivity(new DictionarySearchActivity( + self->renderer, self->mappedInput, + [self]() { + self->exitActivity(); + self->updateRequired = true; + }, + selectedWord)); }, [self]() { // Cancelled word selection @@ -372,14 +379,14 @@ void EpubReaderActivity::loop() { if (SETTINGS.shortPwrBtn == CrossPointSettings::SHORT_PWRBTN::QUICK_MENU && mappedInput.wasReleased(MappedInputManager::Button::Power)) { xSemaphoreTake(renderingMutex, portMAX_DELAY); - + // Check if current page is bookmarked bool isBookmarked = false; if (section) { const uint32_t contentOffset = section->getContentOffsetForPage(section->currentPage); isBookmarked = BookmarkStore::isPageBookmarked(epub->getPath(), currentSpineIndex, contentOffset); } - + exitActivity(); enterNewActivity(new QuickMenuActivity( renderer, mappedInput, @@ -387,9 +394,9 @@ void EpubReaderActivity::loop() { // Cache values before exitActivity EpubReaderActivity* self = this; SemaphoreHandle_t cachedMutex = renderingMutex; - + exitActivity(); - + if (action == QuickMenuAction::DICTIONARY) { // Open dictionary menu - cache renderer/input for this scope GfxRenderer& cachedRenderer = self->renderer; @@ -402,15 +409,17 @@ void EpubReaderActivity::loop() { MappedInputManager& m = self->mappedInput; Section* s = self->section.get(); SemaphoreHandle_t mtx = self->renderingMutex; - + 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(); @@ -420,7 +429,7 @@ void EpubReaderActivity::loop() { mt += SETTINGS.screenMargin; ml += SETTINGS.screenMargin; const int fontId = SETTINGS.getReaderFontId(); - + self->enterNewActivity(new EpubWordSelectionActivity( r, m, std::move(page), fontId, ml, mt, [self](const std::string& word) { @@ -430,7 +439,8 @@ void EpubReaderActivity::loop() { [self]() { self->exitActivity(); self->updateRequired = true; - }, word)); + }, + word)); }, [self]() { self->exitActivity(); @@ -455,7 +465,7 @@ void EpubReaderActivity::loop() { if (self->section) { const uint32_t contentOffset = self->section->getContentOffsetForPage(self->section->currentPage); const std::string& bookPath = self->epub->getPath(); - + if (BookmarkStore::isPageBookmarked(bookPath, self->currentSpineIndex, contentOffset)) { // Remove bookmark BookmarkStore::removeBookmark(bookPath, self->currentSpineIndex, contentOffset); @@ -466,7 +476,7 @@ void EpubReaderActivity::loop() { bm.contentOffset = contentOffset; bm.pageNumber = self->section->currentPage; bm.timestamp = millis() / 1000; // Approximate timestamp - + // Generate name: "Chapter - Page X" or fallback std::string chapterTitle; const int tocIndex = self->epub->getTocIndexForSpineIndex(self->currentSpineIndex); @@ -478,7 +488,7 @@ void EpubReaderActivity::loop() { } else { bm.name = "Page " + std::to_string(self->section->currentPage + 1); } - + BookmarkStore::addBookmark(bookPath, bm); } } @@ -629,18 +639,19 @@ void EpubReaderActivity::renderScreen() { if (!section) { const auto filepath = epub->getSpineItem(currentSpineIndex).href; - Serial.printf("[%lu] [ERS] Loading file: %s, index: %d\n", millis(), filepath.c_str(), currentSpineIndex); + if (Serial) + Serial.printf("[%lu] [ERS] Loading file: %s, index: %d\n", millis(), filepath.c_str(), currentSpineIndex); section = std::unique_ptr
(new Section(epub, currentSpineIndex, renderer)); const uint16_t viewportWidth = renderer.getScreenWidth() - orientedMarginLeft - orientedMarginRight; const uint16_t viewportHeight = renderer.getScreenHeight() - orientedMarginTop - orientedMarginBottom; bool sectionWasReIndexed = false; - + 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 @@ -683,15 +694,15 @@ void EpubReaderActivity::renderScreen() { renderer.displayBuffer(EInkDisplay::FAST_REFRESH); }; - if (!section->createSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(), + 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,25 +748,25 @@ 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(); } - + // 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); renderer.displayBuffer(); return; } - + 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 @@ -760,16 +774,17 @@ void EpubReaderActivity::renderScreen() { if (SdMan.openFileForWrite("ERS", epub->getCachePath() + "/progress.bin", f)) { // Get content offset for current page const uint32_t contentOffset = section->getContentOffsetForPage(section->currentPage); - + // New format: version (1) + spineIndex (2) + pageNumber (2) + contentOffset (4) = 9 bytes serialization::writePod(f, EPUB_PROGRESS_VERSION); serialization::writePod(f, static_cast(currentSpineIndex)); serialization::writePod(f, static_cast(section->currentPage)); 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); } } @@ -777,7 +792,7 @@ void EpubReaderActivity::renderContents(std::unique_ptr page, const int or const int orientedMarginRight, const int orientedMarginBottom, const int orientedMarginLeft) { page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop); - + // Draw bookmark indicator (folded corner) if this page is bookmarked if (section) { const uint32_t contentOffset = section->getContentOffsetForPage(section->currentPage); @@ -787,14 +802,14 @@ void EpubReaderActivity::renderContents(std::unique_ptr page, const int or constexpr int cornerSize = 20; const int cornerX = screenWidth - orientedMarginRight - cornerSize; const int cornerY = orientedMarginTop; - + // Draw triangle (folded corner effect) const int xPoints[3] = {cornerX, cornerX + cornerSize, cornerX + cornerSize}; const int yPoints[3] = {cornerY, cornerY, cornerY + cornerSize}; renderer.fillPolygon(xPoints, yPoints, 3, true); // Black triangle } } - + renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft); if (pagesUntilFullRefresh <= 1) { renderer.displayBuffer(EInkDisplay::HALF_REFRESH); diff --git a/src/activities/reader/EpubReaderActivity.h b/src/activities/reader/EpubReaderActivity.h index 32f861e..77bb5ca 100644 --- a/src/activities/reader/EpubReaderActivity.h +++ b/src/activities/reader/EpubReaderActivity.h @@ -24,7 +24,7 @@ class EpubReaderActivity final : public ActivityWithSubactivity { // End-of-book prompt state bool showingEndOfBookPrompt = false; int endOfBookSelection = 2; // 0=Archive, 1=Delete, 2=Keep (default to safe option) - + // Content offset for position restoration after re-indexing uint32_t savedContentOffset = 0; bool hasContentOffset = false; // True if we have a valid content offset to use diff --git a/src/activities/reader/EpubReaderChapterSelectionActivity.cpp b/src/activities/reader/EpubReaderChapterSelectionActivity.cpp index 57a4dd2..85224a7 100644 --- a/src/activities/reader/EpubReaderChapterSelectionActivity.cpp +++ b/src/activities/reader/EpubReaderChapterSelectionActivity.cpp @@ -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); } diff --git a/src/activities/reader/ReaderActivity.cpp b/src/activities/reader/ReaderActivity.cpp index d425498..df1e64a 100644 --- a/src/activities/reader/ReaderActivity.cpp +++ b/src/activities/reader/ReaderActivity.cpp @@ -62,11 +62,8 @@ void ReaderActivity::onGoToEpubReader(std::unique_ptr 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) { diff --git a/src/activities/reader/TxtReaderActivity.cpp b/src/activities/reader/TxtReaderActivity.cpp index 34b2903..4aaf765 100644 --- a/src/activities/reader/TxtReaderActivity.cpp +++ b/src/activities/reader/TxtReaderActivity.cpp @@ -658,15 +658,15 @@ void TxtReaderActivity::saveProgress() const { if (SdMan.openFileForWrite("TRS", txt->getCachePath() + "/progress.bin", f)) { // New format: version + byte offset + page number (for backwards compatibility debugging) serialization::writePod(f, PROGRESS_VERSION); - + // Store byte offset - this is stable across font/setting changes - const size_t byteOffset = (currentPage >= 0 && currentPage < static_cast(pageOffsets.size())) - ? pageOffsets[currentPage] : 0; + const size_t byteOffset = + (currentPage >= 0 && currentPage < static_cast(pageOffsets.size())) ? pageOffsets[currentPage] : 0; serialization::writePod(f, static_cast(byteOffset)); - + // Also store page number for debugging/logging purposes serialization::writePod(f, static_cast(currentPage)); - + f.close(); Serial.printf("[%lu] [TRS] Saved progress: page %d, offset %zu\n", millis(), currentPage, byteOffset); } @@ -677,24 +677,24 @@ void TxtReaderActivity::loadProgress() { if (SdMan.openFileForRead("TRS", txt->getCachePath() + "/progress.bin", f)) { // Check file size to determine format const size_t fileSize = f.size(); - + if (fileSize >= 7) { // New format: version (1) + byte offset (4) + page number (2) = 7 bytes uint8_t version; serialization::readPod(f, version); - + if (version == PROGRESS_VERSION) { uint32_t savedOffset; serialization::readPod(f, savedOffset); - + uint16_t savedPage; serialization::readPod(f, savedPage); - + // 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); @@ -708,7 +708,7 @@ void TxtReaderActivity::loadProgress() { Serial.printf("[%lu] [TRS] Loaded legacy progress: page %d/%d\n", millis(), currentPage, totalPages); } } - + // Bounds check if (currentPage >= totalPages) { currentPage = totalPages - 1; @@ -716,7 +716,7 @@ void TxtReaderActivity::loadProgress() { if (currentPage < 0) { currentPage = 0; } - + f.close(); } } @@ -725,16 +725,16 @@ int TxtReaderActivity::findPageForOffset(size_t targetOffset) const { if (pageOffsets.empty()) { return 0; } - + // Binary search: find the largest offset that is <= targetOffset // This finds the page that contains or starts at the target offset auto it = std::upper_bound(pageOffsets.begin(), pageOffsets.end(), targetOffset); - + if (it == pageOffsets.begin()) { // Target is before the first page, return page 0 return 0; } - + // upper_bound returns iterator to first element > targetOffset // So we need the element before it (which is <= targetOffset) return static_cast(std::distance(pageOffsets.begin(), it) - 1); diff --git a/src/activities/settings/CategorySettingsActivity.cpp b/src/activities/settings/CategorySettingsActivity.cpp index 4d059ef..eb5efd4 100644 --- a/src/activities/settings/CategorySettingsActivity.cpp +++ b/src/activities/settings/CategorySettingsActivity.cpp @@ -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", "", ""); diff --git a/src/activities/settings/OtaUpdateActivity.cpp b/src/activities/settings/OtaUpdateActivity.cpp index fee931b..67a0d20 100644 --- a/src/activities/settings/OtaUpdateActivity.cpp +++ b/src/activities/settings/OtaUpdateActivity.cpp @@ -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(updaterProgress * static_cast(pageWidth - 44 - bezelLeft - bezelRight)), 42); + renderer.fillRect(24 + bezelLeft, centerY + 4, + static_cast(updaterProgress * static_cast(pageWidth - 44 - bezelLeft - bezelRight)), + 42); renderer.drawCenteredText(UI_10_FONT_ID, centerY + 70, (std::to_string(static_cast(updaterProgress * 100)) + "%").c_str()); renderer.drawCenteredText( diff --git a/src/activities/settings/SettingsActivity.cpp b/src/activities/settings/SettingsActivity.cpp index dd81491..93af6e7 100644 --- a/src/activities/settings/SettingsActivity.cpp +++ b/src/activities/settings/SettingsActivity.cpp @@ -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 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 diff --git a/src/activities/util/KeyboardEntryActivity.cpp b/src/activities/util/KeyboardEntryActivity.cpp index fa3b66a..67775b3 100644 --- a/src/activities/util/KeyboardEntryActivity.cpp +++ b/src/activities/util/KeyboardEntryActivity.cpp @@ -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 diff --git a/src/activities/util/QuickMenuActivity.cpp b/src/activities/util/QuickMenuActivity.cpp index bfaa896..03fb9e6 100644 --- a/src/activities/util/QuickMenuActivity.cpp +++ b/src/activities/util/QuickMenuActivity.cpp @@ -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) { @@ -121,7 +113,7 @@ void QuickMenuActivity::render() const { const auto pageWidth = renderer.getScreenWidth(); const auto pageHeight = renderer.getScreenHeight(); - + // Get bezel offsets const int bezelTop = renderer.getBezelOffsetTop(); const int bezelLeft = renderer.getBezelOffsetLeft(); @@ -160,7 +152,7 @@ void QuickMenuActivity::render() const { if (i == 1) { itemText = isPageBookmarked ? "Remove Bookmark" : "Add Bookmark"; } - + renderer.drawText(UI_10_FONT_ID, marginLeft + 20, itemY, itemText, !isSelected); renderer.drawText(SMALL_FONT_ID, marginLeft + 20, itemY + 22, descriptions[i], !isSelected); } diff --git a/src/customFonts.cpp b/src/customFonts.cpp index 7050b0f..d1a5e43 100644 --- a/src/customFonts.cpp +++ b/src/customFonts.cpp @@ -2,8 +2,9 @@ * Generated by convert-builtin-fonts.sh * Custom font definitions */ -#include #include +#include + #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 } - diff --git a/src/fontIds.h b/src/fontIds.h index 5610417..0ea609d 100644 --- a/src/fontIds.h +++ b/src/fontIds.h @@ -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}, - {FERNMICRO_12_FONT_ID, FERNMICRO_14_FONT_ID, FERNMICRO_16_FONT_ID, FERNMICRO_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}, }; diff --git a/src/images/LockIcon.h b/src/images/LockIcon.h index 32d5f2d..3d31e95 100644 --- a/src/images/LockIcon.h +++ b/src/images/LockIcon.h @@ -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, }; diff --git a/src/main.cpp b/src/main.cpp index 962418c..9f2912e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -10,8 +11,6 @@ #include -#include - #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 @@ -196,7 +192,7 @@ void checkForFlashCommand() { // USB port locations: Portrait=bottom-left, PortraitInverted=top-right, // LandscapeCW=top-left, LandscapeCCW=bottom-right // Position offsets: edge margin + half-width offset to center on USB port - constexpr int edgeMargin = 28; // Distance from screen edge + constexpr int edgeMargin = 28; // Distance from screen edge constexpr int halfWidth = LOCK_ICON_WIDTH / 2; // 16px offset for centering int iconX, iconY; GfxRenderer::ImageRotation rotation; @@ -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) { @@ -542,14 +541,13 @@ void loop() { // Basic heap info Serial.printf("[%lu] [MEM] Free: %d bytes, Total: %d bytes, Min Free: %d bytes\n", millis(), ESP.getFreeHeap(), ESP.getHeapSize(), ESP.getMinFreeHeap()); - + // Detailed fragmentation info using ESP-IDF heap caps API 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,7 +560,8 @@ void loop() { const unsigned long sleepTimeoutMs = SETTINGS.getSleepTimeoutMs(); if (millis() - lastActivityTime >= sleepTimeoutMs) { - Serial.printf("[%lu] [SLP] Auto-sleep triggered after %lu ms of inactivity\n", millis(), 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 return; @@ -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); }