fix: prevent Serial.printf from blocking when USB disconnected
Some checks failed
CI / build (push) Failing after 2m16s
Some checks failed
CI / build (push) Failing after 2m16s
On ESP32-C3 with USB CDC, Serial.printf() blocks indefinitely when USB is not connected. This caused device freezes when booted without USB. Solution: Call Serial.setTxTimeoutMs(0) after Serial.begin() to make all Serial output non-blocking. Also added if (Serial) guards to high-traffic logging paths in EpubReaderActivity as belt-and-suspenders protection. Includes documentation of the debugging process and Serial call inventory. Also applies clang-format to fix pre-existing formatting issues.
This commit is contained in:
parent
f3075002c1
commit
8fa01bc83a
1
.gitignore
vendored
1
.gitignore
vendored
@ -11,6 +11,7 @@ build
|
|||||||
**/__pycache__/
|
**/__pycache__/
|
||||||
test/epubs/
|
test/epubs/
|
||||||
CrossPoint-ef.md
|
CrossPoint-ef.md
|
||||||
|
Serial_print.code-search
|
||||||
|
|
||||||
# Gitea Actions runner config (contains credentials)
|
# Gitea Actions runner config (contains credentials)
|
||||||
.runner
|
.runner
|
||||||
|
|||||||
70
claude_notes/missing-serial-guards-2026-01-28.md
Normal file
70
claude_notes/missing-serial-guards-2026-01-28.md
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
# Serial.printf Calls Without `if (Serial)` Guards
|
||||||
|
|
||||||
|
**Date:** 2026-01-28
|
||||||
|
**Status:** Informational (not blocking issues)
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
The codebase contains **408 Serial print calls** across 27 files in `src/`. Of these, only **16 calls** (in 2 files) have explicit `if (Serial)` guards.
|
||||||
|
|
||||||
|
**This is not a problem** because `Serial.setTxTimeoutMs(0)` is called in `setup()` before any activity code runs, making all Serial output non-blocking globally.
|
||||||
|
|
||||||
|
## Protection Mechanism
|
||||||
|
|
||||||
|
In `src/main.cpp` (lines 467-468):
|
||||||
|
```cpp
|
||||||
|
Serial.begin(115200);
|
||||||
|
Serial.setTxTimeoutMs(0); // Non-blocking TX - critical for USB disconnect handling
|
||||||
|
```
|
||||||
|
|
||||||
|
This ensures that even without `if (Serial)` guards, Serial.printf calls will return immediately when USB is disconnected instead of blocking indefinitely.
|
||||||
|
|
||||||
|
## Files with `if (Serial)` Guards (16 calls)
|
||||||
|
|
||||||
|
| File | Protected Calls |
|
||||||
|
|------|-----------------|
|
||||||
|
| `src/activities/reader/EpubReaderActivity.cpp` | 15 |
|
||||||
|
| `src/main.cpp` | 1 |
|
||||||
|
|
||||||
|
## Files Without Guards (392 calls)
|
||||||
|
|
||||||
|
These calls are protected by `Serial.setTxTimeoutMs(0)` but don't have explicit guards:
|
||||||
|
|
||||||
|
| File | Unguarded Calls |
|
||||||
|
|------|-----------------|
|
||||||
|
| `src/network/CrossPointWebServer.cpp` | 106 |
|
||||||
|
| `src/activities/network/CrossPointWebServerActivity.cpp` | 49 |
|
||||||
|
| `src/activities/boot_sleep/SleepActivity.cpp` | 33 |
|
||||||
|
| `src/BookManager.cpp` | 25 |
|
||||||
|
| `src/activities/reader/TxtReaderActivity.cpp` | 20 |
|
||||||
|
| `src/activities/home/HomeActivity.cpp` | 16 |
|
||||||
|
| `src/network/OtaUpdater.cpp` | 16 |
|
||||||
|
| `src/util/Md5Utils.cpp` | 15 |
|
||||||
|
| `src/main.cpp` | 13 (plus 1 guarded) |
|
||||||
|
| `src/WifiCredentialStore.cpp` | 12 |
|
||||||
|
| `src/network/HttpDownloader.cpp` | 12 |
|
||||||
|
| `src/BookListStore.cpp` | 11 |
|
||||||
|
| `src/activities/network/WifiSelectionActivity.cpp` | 11 |
|
||||||
|
| `src/activities/settings/OtaUpdateActivity.cpp` | 9 |
|
||||||
|
| `src/activities/browser/OpdsBookBrowserActivity.cpp` | 9 |
|
||||||
|
| `src/activities/settings/ClearCacheActivity.cpp` | 7 |
|
||||||
|
| `src/BookmarkStore.cpp` | 6 |
|
||||||
|
| `src/RecentBooksStore.cpp` | 5 |
|
||||||
|
| `src/activities/reader/ReaderActivity.cpp` | 4 |
|
||||||
|
| `src/activities/Activity.h` | 3 |
|
||||||
|
| `src/CrossPointSettings.cpp` | 3 |
|
||||||
|
| `src/activities/network/CalibreConnectActivity.cpp` | 2 |
|
||||||
|
| `src/activities/home/ListViewActivity.cpp` | 2 |
|
||||||
|
| `src/activities/home/MyLibraryActivity.cpp` | 1 |
|
||||||
|
| `src/activities/dictionary/DictionarySearchActivity.cpp` | 1 |
|
||||||
|
| `src/CrossPointState.cpp` | 1 |
|
||||||
|
|
||||||
|
## Recommendation
|
||||||
|
|
||||||
|
No immediate action required. The global `Serial.setTxTimeoutMs(0)` protection is sufficient.
|
||||||
|
|
||||||
|
If desired, `if (Serial)` guards could be added to high-frequency logging paths for minor performance optimization (skipping format string processing), but this is low priority.
|
||||||
|
|
||||||
|
## Note on open-x4-sdk
|
||||||
|
|
||||||
|
The `open-x4-sdk` submodule also contains Serial calls (in `EInkDisplay.cpp`, `SDCardManager.cpp`). These are also protected by the global timeout setting since `Serial.begin()` and `setTxTimeoutMs()` are called before any SDK code executes.
|
||||||
125
claude_notes/serial-blocking-debug-2026-01-28.md
Normal file
125
claude_notes/serial-blocking-debug-2026-01-28.md
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
# Serial Blocking Debug Session Summary
|
||||||
|
|
||||||
|
**Date:** 2026-01-28
|
||||||
|
**Issue:** Device freezes when booted without USB connected
|
||||||
|
**Resolution:** `Serial.setTxTimeoutMs(0)` - make Serial TX non-blocking
|
||||||
|
|
||||||
|
## Problem Description
|
||||||
|
|
||||||
|
During release preparation for ef-0.15.9, the device was discovered to freeze completely when:
|
||||||
|
1. Unplugged from USB
|
||||||
|
2. Powered on via power button
|
||||||
|
3. Book page displays, then device becomes unresponsive
|
||||||
|
4. No button presses register
|
||||||
|
|
||||||
|
The device worked perfectly when USB was connected.
|
||||||
|
|
||||||
|
## Investigation Process
|
||||||
|
|
||||||
|
### Initial Hypotheses Tested
|
||||||
|
|
||||||
|
Multiple hypotheses were systematically investigated:
|
||||||
|
|
||||||
|
1. **Hypothesis A-D:** Display/rendering mutex issues
|
||||||
|
- Added mutex logging to SD card
|
||||||
|
- Mutex operations completed successfully
|
||||||
|
- Ruled out as root cause
|
||||||
|
|
||||||
|
2. **Hypothesis E:** FreeRTOS task creation issues
|
||||||
|
- Task created and ran successfully
|
||||||
|
- First render completed normally
|
||||||
|
- Ruled out
|
||||||
|
|
||||||
|
3. **Hypothesis F-G:** Main loop execution
|
||||||
|
- Added loop counter logging to SD card
|
||||||
|
- **Key finding:** Main loop never started logging
|
||||||
|
- Setup() completed but loop() never executed meaningful work
|
||||||
|
|
||||||
|
4. **Hypothesis H-J:** Various timing and initialization issues
|
||||||
|
- Tested different delays and initialization orders
|
||||||
|
- No improvement
|
||||||
|
|
||||||
|
### Root Cause Discovery
|
||||||
|
|
||||||
|
The breakthrough came from analyzing the boot sequence:
|
||||||
|
|
||||||
|
1. `setup()` completes successfully
|
||||||
|
2. `EpubReaderActivity::onEnter()` runs and calls `Serial.printf()` to log progress
|
||||||
|
3. **Device hangs at Serial.printf() call**
|
||||||
|
|
||||||
|
On ESP32-C3 with USB CDC (USB serial), `Serial.printf()` blocks indefinitely waiting for the TX buffer to drain when USB is not connected. The default behavior expects a host to read the data.
|
||||||
|
|
||||||
|
### Evidence
|
||||||
|
|
||||||
|
- When USB connected: `Serial.printf()` returns immediately (data sent to host)
|
||||||
|
- When USB disconnected: `Serial.printf()` blocks forever waiting for TX buffer space
|
||||||
|
- The hang occurred specifically in `EpubReaderActivity.cpp` during progress logging
|
||||||
|
|
||||||
|
## Solution
|
||||||
|
|
||||||
|
### Primary Fix
|
||||||
|
|
||||||
|
Configure Serial to be non-blocking in `src/main.cpp`:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// Always initialize Serial but make it non-blocking
|
||||||
|
Serial.begin(115200);
|
||||||
|
Serial.setTxTimeoutMs(0); // Non-blocking TX - critical for USB disconnect handling
|
||||||
|
```
|
||||||
|
|
||||||
|
`Serial.setTxTimeoutMs(0)` tells the ESP32 Arduino core to return immediately from Serial write operations if the buffer is full, rather than blocking.
|
||||||
|
|
||||||
|
### Secondary Protection (Belt and Suspenders)
|
||||||
|
|
||||||
|
Added `if (Serial)` guards to high-traffic Serial calls in `EpubReaderActivity.cpp`:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
if (Serial) Serial.printf("[%lu] [ERS] Loaded progress...\n", millis());
|
||||||
|
```
|
||||||
|
|
||||||
|
This provides an additional check before attempting to print, though it's not strictly necessary with the timeout set to 0.
|
||||||
|
|
||||||
|
## Files Changed
|
||||||
|
|
||||||
|
| File | Change |
|
||||||
|
|------|--------|
|
||||||
|
| `src/main.cpp` | Added `Serial.setTxTimeoutMs(0)` after `Serial.begin()` |
|
||||||
|
| `src/main.cpp` | Added `if (Serial)` guard to auto-sleep log |
|
||||||
|
| `src/main.cpp` | Added `if (Serial)` guard to max loop duration log |
|
||||||
|
| `src/activities/reader/EpubReaderActivity.cpp` | Added 16 `if (Serial)` guards |
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
After applying the fix:
|
||||||
|
1. Device boots successfully when unplugged from USB
|
||||||
|
2. Book pages render correctly
|
||||||
|
3. Button presses register normally
|
||||||
|
4. Sleep/wake cycle works
|
||||||
|
5. No functionality lost when USB is connected
|
||||||
|
|
||||||
|
## Lessons Learned
|
||||||
|
|
||||||
|
1. **ESP32-C3 USB CDC behavior:** Serial output can block indefinitely without a connected host
|
||||||
|
2. **Always set non-blocking:** `Serial.setTxTimeoutMs(0)` should be standard for battery-powered devices
|
||||||
|
3. **Debug logging location matters:** When debugging hangs, SD card logging proved essential since Serial was the problem
|
||||||
|
4. **Systematic hypothesis testing:** Ruled out many red herrings (mutex, task, rendering) before finding the true cause
|
||||||
|
|
||||||
|
## Technical Details
|
||||||
|
|
||||||
|
### Why This Affects ESP32-C3 Specifically
|
||||||
|
|
||||||
|
The ESP32-C3 uses native USB CDC for serial communication (no external USB-UART chip). The Arduino core's default behavior is to wait for TX buffer space, which requires an active USB host connection.
|
||||||
|
|
||||||
|
### Alternative Approaches Considered
|
||||||
|
|
||||||
|
1. **Only initialize Serial when USB connected:** Partially implemented, but insufficient because USB can be disconnected after boot
|
||||||
|
2. **Add `if (Serial)` guards everywhere:** Too invasive (400+ calls)
|
||||||
|
3. **Disable Serial entirely:** Would lose debug output when USB connected
|
||||||
|
|
||||||
|
The chosen solution (`setTxTimeoutMs(0)`) provides the best balance: debug output works when USB is connected, device operates normally when disconnected.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- ESP32 Arduino Core Serial documentation
|
||||||
|
- ESP-IDF USB CDC documentation
|
||||||
|
- FreeRTOS queue behavior (initial red herring investigation)
|
||||||
@ -251,8 +251,8 @@ bool Epub::parseCssFiles() {
|
|||||||
SdMan.remove(tmpCssPath.c_str());
|
SdMan.remove(tmpCssPath.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
Serial.printf("[%lu] [EBP] Loaded %zu CSS style rules from %zu files (~%zu bytes)\n", millis(), cssParser->ruleCount(),
|
Serial.printf("[%lu] [EBP] Loaded %zu CSS style rules from %zu files (~%zu bytes)\n", millis(),
|
||||||
cssFiles.size(), cssParser->estimateMemoryUsage());
|
cssParser->ruleCount(), cssFiles.size(), cssParser->estimateMemoryUsage());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -757,8 +757,8 @@ bool Epub::generateAllCovers(const std::function<void(int)>& progressCallback) c
|
|||||||
SdMan.openFileForWrite("EBP", getCoverBmpPath(false), coverBmp)) {
|
SdMan.openFileForWrite("EBP", getCoverBmpPath(false), coverBmp)) {
|
||||||
const int targetWidth = 480;
|
const int targetWidth = 480;
|
||||||
const int targetHeight = (480 * jpegHeight) / jpegWidth;
|
const int targetHeight = (480 * jpegHeight) / jpegWidth;
|
||||||
const bool success =
|
const bool success = JpegToBmpConverter::jpegFileToBmpStreamWithSize(coverJpg, coverBmp, targetWidth,
|
||||||
JpegToBmpConverter::jpegFileToBmpStreamWithSize(coverJpg, coverBmp, targetWidth, targetHeight, makeSubProgress(50, 75));
|
targetHeight, makeSubProgress(50, 75));
|
||||||
coverJpg.close();
|
coverJpg.close();
|
||||||
coverBmp.close();
|
coverBmp.close();
|
||||||
if (!success) {
|
if (!success) {
|
||||||
@ -776,8 +776,8 @@ bool Epub::generateAllCovers(const std::function<void(int)>& progressCallback) c
|
|||||||
SdMan.openFileForWrite("EBP", getCoverBmpPath(true), coverBmp)) {
|
SdMan.openFileForWrite("EBP", getCoverBmpPath(true), coverBmp)) {
|
||||||
const int targetHeight = 800;
|
const int targetHeight = 800;
|
||||||
const int targetWidth = (800 * jpegWidth) / jpegHeight;
|
const int targetWidth = (800 * jpegWidth) / jpegHeight;
|
||||||
const bool success =
|
const bool success = JpegToBmpConverter::jpegFileToBmpStreamWithSize(coverJpg, coverBmp, targetWidth,
|
||||||
JpegToBmpConverter::jpegFileToBmpStreamWithSize(coverJpg, coverBmp, targetWidth, targetHeight, makeSubProgress(75, 100));
|
targetHeight, makeSubProgress(75, 100));
|
||||||
coverJpg.close();
|
coverJpg.close();
|
||||||
coverBmp.close();
|
coverBmp.close();
|
||||||
if (!success) {
|
if (!success) {
|
||||||
|
|||||||
@ -57,12 +57,12 @@ class Page {
|
|||||||
public:
|
public:
|
||||||
// the list of block index and line numbers on this page
|
// the list of block index and line numbers on this page
|
||||||
std::vector<std::shared_ptr<PageElement>> elements;
|
std::vector<std::shared_ptr<PageElement>> elements;
|
||||||
|
|
||||||
// Byte offset in source HTML where this page's content begins
|
// Byte offset in source HTML where this page's content begins
|
||||||
// Used for restoring reading position after re-indexing due to font/setting changes
|
// 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
|
// This is stored in the Section file's LUT, not in Page serialization
|
||||||
uint32_t firstContentOffset = 0;
|
uint32_t firstContentOffset = 0;
|
||||||
|
|
||||||
void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) const;
|
void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) const;
|
||||||
bool serialize(FsFile& file) const;
|
bool serialize(FsFile& file) const;
|
||||||
static std::unique_ptr<Page> deserialize(FsFile& file);
|
static std::unique_ptr<Page> deserialize(FsFile& file);
|
||||||
|
|||||||
@ -186,7 +186,7 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
|
|||||||
}
|
}
|
||||||
writeSectionFileHeader(fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth,
|
writeSectionFileHeader(fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth,
|
||||||
viewportHeight, hyphenationEnabled);
|
viewportHeight, hyphenationEnabled);
|
||||||
|
|
||||||
// LUT entries: { filePosition, contentOffset } pairs
|
// LUT entries: { filePosition, contentOffset } pairs
|
||||||
struct LutEntry {
|
struct LutEntry {
|
||||||
uint32_t filePos;
|
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 contentOffset = page->firstContentOffset;
|
||||||
const uint32_t filePos = this->onPageComplete(std::move(page));
|
const uint32_t filePos = this->onPageComplete(std::move(page));
|
||||||
lut.push_back({filePos, contentOffset});
|
lut.push_back({filePos, contentOffset});
|
||||||
}, progressFn,
|
},
|
||||||
epub->getCssParser());
|
progressFn, epub->getCssParser());
|
||||||
Hyphenator::setPreferredLanguage(epub->getLanguage());
|
Hyphenator::setPreferredLanguage(epub->getLanguage());
|
||||||
success = visitor.parseAndBuildPages();
|
success = visitor.parseAndBuildPages();
|
||||||
|
|
||||||
@ -217,7 +217,7 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
|
|||||||
// Add placeholder to LUT
|
// Add placeholder to LUT
|
||||||
const uint32_t filePos = this->onPageComplete(std::move(placeholderPage));
|
const uint32_t filePos = this->onPageComplete(std::move(placeholderPage));
|
||||||
lut.push_back({filePos, 0});
|
lut.push_back({filePos, 0});
|
||||||
|
|
||||||
// If we still have no pages, the placeholder creation failed
|
// If we still have no pages, the placeholder creation failed
|
||||||
if (pageCount == 0) {
|
if (pageCount == 0) {
|
||||||
Serial.printf("[%lu] [SCT] Failed to create placeholder page\n", millis());
|
Serial.printf("[%lu] [SCT] Failed to create placeholder page\n", millis());
|
||||||
@ -262,13 +262,13 @@ std::unique_ptr<Page> Section::loadPageFromSectionFile() {
|
|||||||
file.seek(HEADER_SIZE - sizeof(uint32_t));
|
file.seek(HEADER_SIZE - sizeof(uint32_t));
|
||||||
uint32_t lutOffset;
|
uint32_t lutOffset;
|
||||||
serialization::readPod(file, lutOffset);
|
serialization::readPod(file, lutOffset);
|
||||||
|
|
||||||
// LUT entries are now 8 bytes each: { filePos (4), contentOffset (4) }
|
// LUT entries are now 8 bytes each: { filePos (4), contentOffset (4) }
|
||||||
file.seek(lutOffset + LUT_ENTRY_SIZE * currentPage);
|
file.seek(lutOffset + LUT_ENTRY_SIZE * currentPage);
|
||||||
uint32_t pagePos;
|
uint32_t pagePos;
|
||||||
serialization::readPod(file, pagePos);
|
serialization::readPod(file, pagePos);
|
||||||
// Skip contentOffset for now - we don't need it when just loading the page
|
// Skip contentOffset for now - we don't need it when just loading the page
|
||||||
|
|
||||||
file.seek(pagePos);
|
file.seek(pagePos);
|
||||||
|
|
||||||
auto page = Page::deserialize(file);
|
auto page = Page::deserialize(file);
|
||||||
@ -300,15 +300,15 @@ int Section::findPageForContentOffset(uint32_t targetOffset) const {
|
|||||||
|
|
||||||
while (left <= right) {
|
while (left <= right) {
|
||||||
const int mid = left + (right - left) / 2;
|
const int mid = left + (right - left) / 2;
|
||||||
|
|
||||||
// Read content offset for page 'mid'
|
// Read content offset for page 'mid'
|
||||||
// LUT entry format: { filePos (4), contentOffset (4) }
|
// LUT entry format: { filePos (4), contentOffset (4) }
|
||||||
f.seek(lutOffset + LUT_ENTRY_SIZE * mid + sizeof(uint32_t)); // Skip filePos
|
f.seek(lutOffset + LUT_ENTRY_SIZE * mid + sizeof(uint32_t)); // Skip filePos
|
||||||
uint32_t midOffset;
|
uint32_t midOffset;
|
||||||
serialization::readPod(f, midOffset);
|
serialization::readPod(f, midOffset);
|
||||||
|
|
||||||
if (midOffset <= targetOffset) {
|
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
|
left = mid + 1; // Look for a later page that might also qualify
|
||||||
} else {
|
} else {
|
||||||
right = mid - 1; // Look for an earlier page
|
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));
|
f.seek(lutOffset + LUT_ENTRY_SIZE * result + sizeof(uint32_t));
|
||||||
uint32_t resultOffset;
|
uint32_t resultOffset;
|
||||||
serialization::readPod(f, resultOffset);
|
serialization::readPod(f, resultOffset);
|
||||||
|
|
||||||
while (result > 0) {
|
while (result > 0) {
|
||||||
f.seek(lutOffset + LUT_ENTRY_SIZE * (result - 1) + sizeof(uint32_t));
|
f.seek(lutOffset + LUT_ENTRY_SIZE * (result - 1) + sizeof(uint32_t));
|
||||||
uint32_t prevOffset;
|
uint32_t prevOffset;
|
||||||
|
|||||||
@ -36,7 +36,7 @@ class Section {
|
|||||||
const std::function<void()>& progressSetupFn = nullptr,
|
const std::function<void()>& progressSetupFn = nullptr,
|
||||||
const std::function<void(int)>& progressFn = nullptr);
|
const std::function<void(int)>& progressFn = nullptr);
|
||||||
std::unique_ptr<Page> loadPageFromSectionFile();
|
std::unique_ptr<Page> loadPageFromSectionFile();
|
||||||
|
|
||||||
// Methods for content offset-based position tracking
|
// Methods for content offset-based position tracking
|
||||||
// Used to restore reading position after re-indexing due to font/setting changes
|
// Used to restore reading position after re-indexing due to font/setting changes
|
||||||
int findPageForContentOffset(uint32_t targetOffset) const;
|
int findPageForContentOffset(uint32_t targetOffset) const;
|
||||||
|
|||||||
@ -83,7 +83,7 @@ void ChapterHtmlSlimParser::updateEffectiveInlineStyle() {
|
|||||||
// Flush the contents of partWordBuffer to currentTextBlock
|
// Flush the contents of partWordBuffer to currentTextBlock
|
||||||
void ChapterHtmlSlimParser::flushPartWordBuffer() {
|
void ChapterHtmlSlimParser::flushPartWordBuffer() {
|
||||||
if (partWordBufferIndex == 0) return;
|
if (partWordBufferIndex == 0) return;
|
||||||
|
|
||||||
// Determine font style using effective styles
|
// Determine font style using effective styles
|
||||||
EpdFontFamily::Style fontStyle = EpdFontFamily::REGULAR;
|
EpdFontFamily::Style fontStyle = EpdFontFamily::REGULAR;
|
||||||
if (effectiveBold && effectiveItalic) {
|
if (effectiveBold && effectiveItalic) {
|
||||||
@ -93,7 +93,7 @@ void ChapterHtmlSlimParser::flushPartWordBuffer() {
|
|||||||
} else if (effectiveItalic) {
|
} else if (effectiveItalic) {
|
||||||
fontStyle = EpdFontFamily::ITALIC;
|
fontStyle = EpdFontFamily::ITALIC;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Flush the buffer
|
// Flush the buffer
|
||||||
partWordBuffer[partWordBufferIndex] = '\0';
|
partWordBuffer[partWordBufferIndex] = '\0';
|
||||||
currentTextBlock->addWord(partWordBuffer, fontStyle, effectiveUnderline);
|
currentTextBlock->addWord(partWordBuffer, fontStyle, effectiveUnderline);
|
||||||
@ -290,7 +290,7 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
|||||||
} else {
|
} else {
|
||||||
placeholder = "[Image unavailable]";
|
placeholder = "[Image unavailable]";
|
||||||
}
|
}
|
||||||
|
|
||||||
self->startNewTextBlock(TextBlock::CENTER_ALIGN);
|
self->startNewTextBlock(TextBlock::CENTER_ALIGN);
|
||||||
self->italicUntilDepth = std::min(self->italicUntilDepth, self->depth);
|
self->italicUntilDepth = std::min(self->italicUntilDepth, self->depth);
|
||||||
self->depth += 1;
|
self->depth += 1;
|
||||||
@ -478,7 +478,7 @@ void XMLCALL ChapterHtmlSlimParser::characterData(void* userData, const XML_Char
|
|||||||
if (self->skipUntilDepth < self->depth) {
|
if (self->skipUntilDepth < self->depth) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Capture byte offset of this character data for page position tracking
|
// Capture byte offset of this character data for page position tracking
|
||||||
if (self->xmlParser) {
|
if (self->xmlParser) {
|
||||||
self->lastCharDataOffset = XML_GetCurrentByteIndex(self->xmlParser);
|
self->lastCharDataOffset = XML_GetCurrentByteIndex(self->xmlParser);
|
||||||
@ -647,7 +647,7 @@ bool ChapterHtmlSlimParser::parseAndBuildPages() {
|
|||||||
const size_t totalSize = file.size();
|
const size_t totalSize = file.size();
|
||||||
size_t bytesRead = 0;
|
size_t bytesRead = 0;
|
||||||
int lastProgress = -1;
|
int lastProgress = -1;
|
||||||
|
|
||||||
// Initialize offset tracking - first page starts at offset 0
|
// Initialize offset tracking - first page starts at offset 0
|
||||||
currentPageStartOffset = 0;
|
currentPageStartOffset = 0;
|
||||||
lastCharDataOffset = 0;
|
lastCharDataOffset = 0;
|
||||||
@ -739,7 +739,7 @@ void ChapterHtmlSlimParser::addLineToPage(std::shared_ptr<TextBlock> line) {
|
|||||||
currentPage->firstContentOffset = static_cast<uint32_t>(currentPageStartOffset);
|
currentPage->firstContentOffset = static_cast<uint32_t>(currentPageStartOffset);
|
||||||
}
|
}
|
||||||
completePageFn(std::move(currentPage));
|
completePageFn(std::move(currentPage));
|
||||||
|
|
||||||
// Start new page - offset will be set when first content is added
|
// Start new page - offset will be set when first content is added
|
||||||
currentPage.reset(new Page());
|
currentPage.reset(new Page());
|
||||||
currentPageStartOffset = lastCharDataOffset; // Use offset from when content was parsed
|
currentPageStartOffset = lastCharDataOffset; // Use offset from when content was parsed
|
||||||
|
|||||||
@ -58,11 +58,11 @@ class ChapterHtmlSlimParser {
|
|||||||
bool effectiveBold = false;
|
bool effectiveBold = false;
|
||||||
bool effectiveItalic = false;
|
bool effectiveItalic = false;
|
||||||
bool effectiveUnderline = false;
|
bool effectiveUnderline = false;
|
||||||
|
|
||||||
// Byte offset tracking for position restoration after re-indexing
|
// Byte offset tracking for position restoration after re-indexing
|
||||||
XML_Parser xmlParser = nullptr; // Store parser for getting current byte index
|
XML_Parser xmlParser = nullptr; // Store parser for getting current byte index
|
||||||
size_t currentPageStartOffset = 0; // Byte offset when current page was started
|
size_t currentPageStartOffset = 0; // Byte offset when current page was started
|
||||||
size_t lastCharDataOffset = 0; // Byte offset of last character data (captured during parsing)
|
size_t lastCharDataOffset = 0; // Byte offset of last character data (captured during parsing)
|
||||||
|
|
||||||
void updateEffectiveInlineStyle();
|
void updateEffectiveInlineStyle();
|
||||||
void startNewTextBlock(TextBlock::Style style);
|
void startNewTextBlock(TextBlock::Style style);
|
||||||
|
|||||||
@ -5,13 +5,9 @@
|
|||||||
// Global high contrast mode flag
|
// Global high contrast mode flag
|
||||||
static bool g_highContrastMode = false;
|
static bool g_highContrastMode = false;
|
||||||
|
|
||||||
void setHighContrastMode(bool enabled) {
|
void setHighContrastMode(bool enabled) { g_highContrastMode = enabled; }
|
||||||
g_highContrastMode = enabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool isHighContrastMode() {
|
bool isHighContrastMode() { return g_highContrastMode; }
|
||||||
return g_highContrastMode;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Brightness/Contrast adjustments:
|
// Brightness/Contrast adjustments:
|
||||||
constexpr bool USE_BRIGHTNESS = false; // true: apply brightness/gamma adjustments
|
constexpr bool USE_BRIGHTNESS = false; // true: apply brightness/gamma adjustments
|
||||||
|
|||||||
@ -327,7 +327,8 @@ void GfxRenderer::drawBitmap(const Bitmap& bitmap, const int x, const int y, con
|
|||||||
// Calculate screen Y position
|
// Calculate screen Y position
|
||||||
const int screenYStart = y + static_cast<int>(std::floor(logicalY * scale));
|
const int screenYStart = y + static_cast<int>(std::floor(logicalY * scale));
|
||||||
// For upscaling, calculate the end position for this source row
|
// For upscaling, calculate the end position for this source row
|
||||||
const int screenYEnd = isUpscaling ? (y + static_cast<int>(std::floor((logicalY + 1) * scale))) : (screenYStart + 1);
|
const int screenYEnd =
|
||||||
|
isUpscaling ? (y + static_cast<int>(std::floor((logicalY + 1) * scale))) : (screenYStart + 1);
|
||||||
|
|
||||||
// Draw to all Y positions this source row maps to (for upscaling, this fills gaps)
|
// Draw to all Y positions this source row maps to (for upscaling, this fills gaps)
|
||||||
for (int screenY = screenYStart; screenY < screenYEnd; screenY++) {
|
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
|
// Calculate screen X position
|
||||||
const int screenXStart = x + static_cast<int>(std::floor(srcX * scale));
|
const int screenXStart = x + static_cast<int>(std::floor(srcX * scale));
|
||||||
// For upscaling, calculate the end position for this source pixel
|
// For upscaling, calculate the end position for this source pixel
|
||||||
const int screenXEnd = isUpscaling ? (x + static_cast<int>(std::floor((srcX + 1) * scale))) : (screenXStart + 1);
|
const int screenXEnd =
|
||||||
|
isUpscaling ? (x + static_cast<int>(std::floor((srcX + 1) * scale))) : (screenXStart + 1);
|
||||||
|
|
||||||
const uint8_t val = outputRow[bmpX / 4] >> (6 - ((bmpX * 2) % 8)) & 0x3;
|
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
|
// Calculate screen Y position
|
||||||
const int screenYStart = y + static_cast<int>(std::floor(logicalY * scale));
|
const int screenYStart = y + static_cast<int>(std::floor(logicalY * scale));
|
||||||
// For upscaling, calculate the end position for this source row
|
// For upscaling, calculate the end position for this source row
|
||||||
const int screenYEnd = isUpscaling ? (y + static_cast<int>(std::floor((logicalY + 1) * scale))) : (screenYStart + 1);
|
const int screenYEnd =
|
||||||
|
isUpscaling ? (y + static_cast<int>(std::floor((logicalY + 1) * scale))) : (screenYStart + 1);
|
||||||
|
|
||||||
// Draw to all Y positions this source row maps to (for upscaling, this fills gaps)
|
// Draw to all Y positions this source row maps to (for upscaling, this fills gaps)
|
||||||
for (int screenY = screenYStart; screenY < screenYEnd; screenY++) {
|
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
|
// Calculate screen X position
|
||||||
const int screenXStart = x + static_cast<int>(std::floor(bmpX * scale));
|
const int screenXStart = x + static_cast<int>(std::floor(bmpX * scale));
|
||||||
// For upscaling, calculate the end position for this source pixel
|
// For upscaling, calculate the end position for this source pixel
|
||||||
const int screenXEnd = isUpscaling ? (x + static_cast<int>(std::floor((bmpX + 1) * scale))) : (screenXStart + 1);
|
const int screenXEnd =
|
||||||
|
isUpscaling ? (x + static_cast<int>(std::floor((bmpX + 1) * scale))) : (screenXStart + 1);
|
||||||
|
|
||||||
// Get 2-bit value (result of readNextRow quantization)
|
// Get 2-bit value (result of readNextRow quantization)
|
||||||
const uint8_t val = outputRow[bmpX / 4] >> (6 - ((bmpX * 2) % 8)) & 0x3;
|
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;
|
return bezelEdge;
|
||||||
case GfxRenderer::LandscapeClockwise:
|
case GfxRenderer::LandscapeClockwise:
|
||||||
switch (bezelEdge) {
|
switch (bezelEdge) {
|
||||||
case 0: return 2; // Physical bottom -> logical left
|
case 0:
|
||||||
case 1: return 3; // Physical top -> logical right
|
return 2; // Physical bottom -> logical left
|
||||||
case 2: return 1; // Physical left -> logical top
|
case 1:
|
||||||
case 3: return 0; // Physical right -> logical bottom
|
return 3; // Physical top -> logical right
|
||||||
|
case 2:
|
||||||
|
return 1; // Physical left -> logical top
|
||||||
|
case 3:
|
||||||
|
return 0; // Physical right -> logical bottom
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case GfxRenderer::PortraitInverted:
|
case GfxRenderer::PortraitInverted:
|
||||||
switch (bezelEdge) {
|
switch (bezelEdge) {
|
||||||
case 0: return 1; // Physical bottom -> logical top
|
case 0:
|
||||||
case 1: return 0; // Physical top -> logical bottom
|
return 1; // Physical bottom -> logical top
|
||||||
case 2: return 3; // Physical left -> logical right
|
case 1:
|
||||||
case 3: return 2; // Physical right -> logical left
|
return 0; // Physical top -> logical bottom
|
||||||
|
case 2:
|
||||||
|
return 3; // Physical left -> logical right
|
||||||
|
case 3:
|
||||||
|
return 2; // Physical right -> logical left
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case GfxRenderer::LandscapeCounterClockwise:
|
case GfxRenderer::LandscapeCounterClockwise:
|
||||||
switch (bezelEdge) {
|
switch (bezelEdge) {
|
||||||
case 0: return 3; // Physical bottom -> logical right
|
case 0:
|
||||||
case 1: return 2; // Physical top -> logical left
|
return 3; // Physical bottom -> logical right
|
||||||
case 2: return 0; // Physical left -> logical bottom
|
case 1:
|
||||||
case 3: return 1; // Physical right -> logical top
|
return 2; // Physical top -> logical left
|
||||||
|
case 2:
|
||||||
|
return 0; // Physical left -> logical bottom
|
||||||
|
case 3:
|
||||||
|
return 1; // Physical right -> logical top
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -1074,23 +1090,34 @@ void GfxRenderer::getOrientedViewableTRBL(int* outTop, int* outRight, int* outBo
|
|||||||
*outLeft = getViewableMarginLeft();
|
*outLeft = getViewableMarginLeft();
|
||||||
break;
|
break;
|
||||||
case LandscapeClockwise:
|
case LandscapeClockwise:
|
||||||
*outTop = BASE_VIEWABLE_MARGIN_LEFT + (mapPhysicalToLogicalEdge(bezelEdge, orientation) == 1 ? bezelCompensation : 0);
|
*outTop =
|
||||||
*outRight = BASE_VIEWABLE_MARGIN_TOP + (mapPhysicalToLogicalEdge(bezelEdge, orientation) == 3 ? bezelCompensation : 0);
|
BASE_VIEWABLE_MARGIN_LEFT + (mapPhysicalToLogicalEdge(bezelEdge, orientation) == 1 ? bezelCompensation : 0);
|
||||||
*outBottom = BASE_VIEWABLE_MARGIN_RIGHT + (mapPhysicalToLogicalEdge(bezelEdge, orientation) == 0 ? bezelCompensation : 0);
|
*outRight =
|
||||||
*outLeft = BASE_VIEWABLE_MARGIN_BOTTOM + (mapPhysicalToLogicalEdge(bezelEdge, orientation) == 2 ? bezelCompensation : 0);
|
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;
|
break;
|
||||||
case PortraitInverted:
|
case PortraitInverted:
|
||||||
*outTop = BASE_VIEWABLE_MARGIN_BOTTOM + (mapPhysicalToLogicalEdge(bezelEdge, orientation) == 1 ? bezelCompensation : 0);
|
*outTop =
|
||||||
*outRight = BASE_VIEWABLE_MARGIN_LEFT + (mapPhysicalToLogicalEdge(bezelEdge, orientation) == 3 ? bezelCompensation : 0);
|
BASE_VIEWABLE_MARGIN_BOTTOM + (mapPhysicalToLogicalEdge(bezelEdge, orientation) == 1 ? bezelCompensation : 0);
|
||||||
*outBottom = BASE_VIEWABLE_MARGIN_TOP + (mapPhysicalToLogicalEdge(bezelEdge, orientation) == 0 ? bezelCompensation : 0);
|
*outRight =
|
||||||
*outLeft = BASE_VIEWABLE_MARGIN_RIGHT + (mapPhysicalToLogicalEdge(bezelEdge, orientation) == 2 ? bezelCompensation : 0);
|
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;
|
break;
|
||||||
case LandscapeCounterClockwise:
|
case LandscapeCounterClockwise:
|
||||||
*outTop = BASE_VIEWABLE_MARGIN_RIGHT + (mapPhysicalToLogicalEdge(bezelEdge, orientation) == 1 ? bezelCompensation : 0);
|
*outTop =
|
||||||
*outRight = BASE_VIEWABLE_MARGIN_BOTTOM + (mapPhysicalToLogicalEdge(bezelEdge, orientation) == 3 ? bezelCompensation : 0);
|
BASE_VIEWABLE_MARGIN_RIGHT + (mapPhysicalToLogicalEdge(bezelEdge, orientation) == 1 ? bezelCompensation : 0);
|
||||||
*outBottom = BASE_VIEWABLE_MARGIN_LEFT + (mapPhysicalToLogicalEdge(bezelEdge, orientation) == 0 ? bezelCompensation : 0);
|
*outRight =
|
||||||
*outLeft = BASE_VIEWABLE_MARGIN_TOP + (mapPhysicalToLogicalEdge(bezelEdge, orientation) == 2 ? bezelCompensation : 0);
|
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;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -94,10 +94,10 @@ class GfxRenderer {
|
|||||||
// Handles current render mode (BW, GRAYSCALE_MSB, GRAYSCALE_LSB)
|
// Handles current render mode (BW, GRAYSCALE_MSB, GRAYSCALE_LSB)
|
||||||
void fillRectGray(int x, int y, int width, int height, uint8_t grayLevel) const;
|
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 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,
|
void drawImageRotated(const uint8_t bitmap[], int x, int y, int width, int height, ImageRotation rotation,
|
||||||
ImageRotation rotation, bool invert = false) const;
|
bool invert = false) const;
|
||||||
void drawBitmap(const Bitmap& bitmap, int x, int y, int maxWidth, int maxHeight, float cropX = 0,
|
void drawBitmap(const Bitmap& bitmap, int x, int y, int maxWidth, int maxHeight, float cropX = 0, float cropY = 0,
|
||||||
float cropY = 0, bool invert = false) const;
|
bool invert = false) const;
|
||||||
void drawBitmap1Bit(const Bitmap& bitmap, int x, int y, int maxWidth, int maxHeight, 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;
|
void fillPolygon(const int* xPoints, const int* yPoints, int numPoints, bool state = true) const;
|
||||||
|
|
||||||
|
|||||||
@ -150,8 +150,7 @@ std::string DictHtmlParser::extractTagName(const std::string& html, size_t start
|
|||||||
|
|
||||||
std::string tagName = html.substr(nameStart, pos - nameStart);
|
std::string tagName = html.substr(nameStart, pos - nameStart);
|
||||||
// Convert to lowercase
|
// Convert to lowercase
|
||||||
std::transform(tagName.begin(), tagName.end(), tagName.begin(),
|
std::transform(tagName.begin(), tagName.end(), tagName.begin(), [](unsigned char c) { return std::tolower(c); });
|
||||||
[](unsigned char c) { return std::tolower(c); });
|
|
||||||
return tagName;
|
return tagName;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -160,17 +159,11 @@ bool DictHtmlParser::isBlockTag(const std::string& tagName) {
|
|||||||
tagName == "ol" || tagName == "ul" || tagName == "dt" || tagName == "dd" || tagName == "html";
|
tagName == "ol" || tagName == "ul" || tagName == "dt" || tagName == "dd" || tagName == "html";
|
||||||
}
|
}
|
||||||
|
|
||||||
bool DictHtmlParser::isBoldTag(const std::string& tagName) {
|
bool DictHtmlParser::isBoldTag(const std::string& tagName) { return tagName == "b" || tagName == "strong"; }
|
||||||
return tagName == "b" || tagName == "strong";
|
|
||||||
}
|
|
||||||
|
|
||||||
bool DictHtmlParser::isItalicTag(const std::string& tagName) {
|
bool DictHtmlParser::isItalicTag(const std::string& tagName) { return tagName == "i" || tagName == "em"; }
|
||||||
return tagName == "i" || tagName == "em";
|
|
||||||
}
|
|
||||||
|
|
||||||
bool DictHtmlParser::isUnderlineTag(const std::string& tagName) {
|
bool DictHtmlParser::isUnderlineTag(const std::string& tagName) { return tagName == "u" || tagName == "ins"; }
|
||||||
return tagName == "u" || tagName == "ins";
|
|
||||||
}
|
|
||||||
|
|
||||||
bool DictHtmlParser::isSuperscriptTag(const std::string& tagName) { return tagName == "sup"; }
|
bool DictHtmlParser::isSuperscriptTag(const std::string& tagName) { return tagName == "sup"; }
|
||||||
|
|
||||||
|
|||||||
@ -10,7 +10,7 @@ class GfxRenderer;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* DictHtmlParser parses HTML dictionary definitions into ParsedText.
|
* DictHtmlParser parses HTML dictionary definitions into ParsedText.
|
||||||
*
|
*
|
||||||
* Supports:
|
* Supports:
|
||||||
* - Bold: <b>, <strong>
|
* - Bold: <b>, <strong>
|
||||||
* - Italic: <i>, <em>
|
* - Italic: <i>, <em>
|
||||||
@ -25,7 +25,7 @@ class DictHtmlParser {
|
|||||||
/**
|
/**
|
||||||
* Parse HTML definition and populate ParsedText with styled words.
|
* Parse HTML definition and populate ParsedText with styled words.
|
||||||
* Each paragraph/block creates a separate ParsedText via the callback.
|
* Each paragraph/block creates a separate ParsedText via the callback.
|
||||||
*
|
*
|
||||||
* @param html The HTML definition text
|
* @param html The HTML definition text
|
||||||
* @param fontId Font ID for text width calculations
|
* @param fontId Font ID for text width calculations
|
||||||
* @param renderer Reference to renderer for layout
|
* @param renderer Reference to renderer for layout
|
||||||
|
|||||||
@ -588,8 +588,12 @@ static std::string decodeHtmlEntity(const std::string& html, size_t& i) {
|
|||||||
const char* replacement;
|
const char* replacement;
|
||||||
};
|
};
|
||||||
static const EntityMapping entities[] = {
|
static const EntityMapping entities[] = {
|
||||||
{" ", " "}, {"<", "<"}, {">", ">"},
|
{" ", " "},
|
||||||
{"&", "&"}, {""", "\""}, {"'", "'"},
|
{"<", "<"},
|
||||||
|
{">", ">"},
|
||||||
|
{"&", "&"},
|
||||||
|
{""", "\""},
|
||||||
|
{"'", "'"},
|
||||||
{"—", "\xe2\x80\x94"}, // —
|
{"—", "\xe2\x80\x94"}, // —
|
||||||
{"–", "\xe2\x80\x93"}, // –
|
{"–", "\xe2\x80\x93"}, // –
|
||||||
{"…", "\xe2\x80\xa6"}, // …
|
{"…", "\xe2\x80\xa6"}, // …
|
||||||
@ -688,8 +692,8 @@ std::string StarDict::stripHtml(const std::string& html) {
|
|||||||
|
|
||||||
// Extract tag name
|
// Extract tag name
|
||||||
size_t tagEnd = tagStart;
|
size_t tagEnd = tagStart;
|
||||||
while (tagEnd < html.length() && !std::isspace(static_cast<unsigned char>(html[tagEnd])) &&
|
while (tagEnd < html.length() && !std::isspace(static_cast<unsigned char>(html[tagEnd])) && html[tagEnd] != '>' &&
|
||||||
html[tagEnd] != '>' && html[tagEnd] != '/') {
|
html[tagEnd] != '/') {
|
||||||
tagEnd++;
|
tagEnd++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -32,7 +32,7 @@ class StarDict {
|
|||||||
struct DictzipInfo {
|
struct DictzipInfo {
|
||||||
uint32_t chunkLength = 0; // Uncompressed chunk size (usually 58315)
|
uint32_t chunkLength = 0; // Uncompressed chunk size (usually 58315)
|
||||||
uint16_t chunkCount = 0;
|
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
|
uint16_t* chunkSizes = nullptr; // Array of compressed chunk sizes
|
||||||
bool loaded = false;
|
bool loaded = false;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -205,8 +205,8 @@ bool Txt::generateThumbBmp() const {
|
|||||||
}
|
}
|
||||||
constexpr int THUMB_TARGET_WIDTH = 240;
|
constexpr int THUMB_TARGET_WIDTH = 240;
|
||||||
constexpr int THUMB_TARGET_HEIGHT = 400;
|
constexpr int THUMB_TARGET_HEIGHT = 400;
|
||||||
const bool success =
|
const bool success = JpegToBmpConverter::jpegFileTo1BitBmpStreamWithSize(coverJpg, thumbBmp, THUMB_TARGET_WIDTH,
|
||||||
JpegToBmpConverter::jpegFileTo1BitBmpStreamWithSize(coverJpg, thumbBmp, THUMB_TARGET_WIDTH, THUMB_TARGET_HEIGHT);
|
THUMB_TARGET_HEIGHT);
|
||||||
coverJpg.close();
|
coverJpg.close();
|
||||||
thumbBmp.close();
|
thumbBmp.close();
|
||||||
|
|
||||||
@ -276,8 +276,8 @@ bool Txt::generateMicroThumbBmp() const {
|
|||||||
}
|
}
|
||||||
constexpr int MICRO_THUMB_TARGET_WIDTH = 45;
|
constexpr int MICRO_THUMB_TARGET_WIDTH = 45;
|
||||||
constexpr int MICRO_THUMB_TARGET_HEIGHT = 60;
|
constexpr int MICRO_THUMB_TARGET_HEIGHT = 60;
|
||||||
const bool success = JpegToBmpConverter::jpegFileTo1BitBmpStreamWithSize(coverJpg, microThumbBmp,
|
const bool success = JpegToBmpConverter::jpegFileTo1BitBmpStreamWithSize(
|
||||||
MICRO_THUMB_TARGET_WIDTH, MICRO_THUMB_TARGET_HEIGHT);
|
coverJpg, microThumbBmp, MICRO_THUMB_TARGET_WIDTH, MICRO_THUMB_TARGET_HEIGHT);
|
||||||
coverJpg.close();
|
coverJpg.close();
|
||||||
microThumbBmp.close();
|
microThumbBmp.close();
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
default_envs = default
|
default_envs = default
|
||||||
|
|
||||||
[crosspoint]
|
[crosspoint]
|
||||||
version = 0.15.0
|
version = ef-0.15.9
|
||||||
|
|
||||||
[base]
|
[base]
|
||||||
platform = espressif32 @ 6.12.0
|
platform = espressif32 @ 6.12.0
|
||||||
|
|||||||
@ -219,9 +219,7 @@ bool BookListStore::listExists(const std::string& name) {
|
|||||||
return SdMan.exists(path.c_str());
|
return SdMan.exists(path.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string BookListStore::getListPath(const std::string& name) {
|
std::string BookListStore::getListPath(const std::string& name) { return std::string(LISTS_DIR) + "/" + name + ".bin"; }
|
||||||
return std::string(LISTS_DIR) + "/" + name + ".bin";
|
|
||||||
}
|
|
||||||
|
|
||||||
int BookListStore::getBookCount(const std::string& name) {
|
int BookListStore::getBookCount(const std::string& name) {
|
||||||
const std::string path = getListPath(name);
|
const std::string path = getListPath(name);
|
||||||
|
|||||||
@ -81,5 +81,4 @@ class BookListStore {
|
|||||||
* @return Book count, or -1 if list doesn't exist
|
* @return Book count, or -1 if list doesn't exist
|
||||||
*/
|
*/
|
||||||
static int getBookCount(const std::string& name);
|
static int getBookCount(const std::string& name);
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|||||||
@ -35,9 +35,7 @@ std::string BookManager::getExtension(const std::string& path) {
|
|||||||
return ext;
|
return ext;
|
||||||
}
|
}
|
||||||
|
|
||||||
size_t BookManager::computePathHash(const std::string& path) {
|
size_t BookManager::computePathHash(const std::string& path) { return std::hash<std::string>{}(path); }
|
||||||
return std::hash<std::string>{}(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string BookManager::getCachePrefix(const std::string& path) {
|
std::string BookManager::getCachePrefix(const std::string& path) {
|
||||||
const std::string ext = getExtension(path);
|
const std::string ext = getExtension(path);
|
||||||
|
|||||||
@ -20,11 +20,10 @@ constexpr int MAX_BOOKMARKS_PER_BOOK = 100;
|
|||||||
// Get cache directory path for a book (same logic as BookManager)
|
// Get cache directory path for a book (same logic as BookManager)
|
||||||
std::string getCacheDir(const std::string& bookPath) {
|
std::string getCacheDir(const std::string& bookPath) {
|
||||||
const size_t hash = std::hash<std::string>{}(bookPath);
|
const size_t hash = std::hash<std::string>{}(bookPath);
|
||||||
|
|
||||||
if (StringUtils::checkFileExtension(bookPath, ".epub")) {
|
if (StringUtils::checkFileExtension(bookPath, ".epub")) {
|
||||||
return "/.crosspoint/epub_" + std::to_string(hash);
|
return "/.crosspoint/epub_" + std::to_string(hash);
|
||||||
} else if (StringUtils::checkFileExtension(bookPath, ".txt") ||
|
} else if (StringUtils::checkFileExtension(bookPath, ".txt") || StringUtils::checkFileExtension(bookPath, ".TXT") ||
|
||||||
StringUtils::checkFileExtension(bookPath, ".TXT") ||
|
|
||||||
StringUtils::checkFileExtension(bookPath, ".md")) {
|
StringUtils::checkFileExtension(bookPath, ".md")) {
|
||||||
return "/.crosspoint/txt_" + std::to_string(hash);
|
return "/.crosspoint/txt_" + std::to_string(hash);
|
||||||
}
|
}
|
||||||
@ -47,21 +46,21 @@ std::vector<Bookmark> BookmarkStore::getBookmarks(const std::string& bookPath) {
|
|||||||
bool BookmarkStore::addBookmark(const std::string& bookPath, const Bookmark& bookmark) {
|
bool BookmarkStore::addBookmark(const std::string& bookPath, const Bookmark& bookmark) {
|
||||||
std::vector<Bookmark> bookmarks;
|
std::vector<Bookmark> bookmarks;
|
||||||
loadBookmarks(bookPath, bookmarks);
|
loadBookmarks(bookPath, bookmarks);
|
||||||
|
|
||||||
// Check if bookmark already exists at this location
|
// Check if bookmark already exists at this location
|
||||||
auto it = std::find_if(bookmarks.begin(), bookmarks.end(), [&](const Bookmark& b) {
|
auto it = std::find_if(bookmarks.begin(), bookmarks.end(), [&](const Bookmark& b) {
|
||||||
return b.spineIndex == bookmark.spineIndex && b.contentOffset == bookmark.contentOffset;
|
return b.spineIndex == bookmark.spineIndex && b.contentOffset == bookmark.contentOffset;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (it != bookmarks.end()) {
|
if (it != bookmarks.end()) {
|
||||||
Serial.printf("[%lu] [BMS] Bookmark already exists at spine %u, offset %u\n",
|
Serial.printf("[%lu] [BMS] Bookmark already exists at spine %u, offset %u\n", millis(), bookmark.spineIndex,
|
||||||
millis(), bookmark.spineIndex, bookmark.contentOffset);
|
bookmark.contentOffset);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add new bookmark
|
// Add new bookmark
|
||||||
bookmarks.push_back(bookmark);
|
bookmarks.push_back(bookmark);
|
||||||
|
|
||||||
// Trim to max size (remove oldest)
|
// Trim to max size (remove oldest)
|
||||||
if (bookmarks.size() > MAX_BOOKMARKS_PER_BOOK) {
|
if (bookmarks.size() > MAX_BOOKMARKS_PER_BOOK) {
|
||||||
// Sort by timestamp and remove oldest
|
// 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);
|
bookmarks.resize(MAX_BOOKMARKS_PER_BOOK);
|
||||||
}
|
}
|
||||||
|
|
||||||
return saveBookmarks(bookPath, bookmarks);
|
return saveBookmarks(bookPath, bookmarks);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool BookmarkStore::removeBookmark(const std::string& bookPath, uint16_t spineIndex, uint32_t contentOffset) {
|
bool BookmarkStore::removeBookmark(const std::string& bookPath, uint16_t spineIndex, uint32_t contentOffset) {
|
||||||
std::vector<Bookmark> bookmarks;
|
std::vector<Bookmark> bookmarks;
|
||||||
loadBookmarks(bookPath, bookmarks);
|
loadBookmarks(bookPath, bookmarks);
|
||||||
|
|
||||||
auto it = std::find_if(bookmarks.begin(), bookmarks.end(), [&](const Bookmark& b) {
|
auto it = std::find_if(bookmarks.begin(), bookmarks.end(), [&](const Bookmark& b) {
|
||||||
return b.spineIndex == spineIndex && b.contentOffset == contentOffset;
|
return b.spineIndex == spineIndex && b.contentOffset == contentOffset;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (it == bookmarks.end()) {
|
if (it == bookmarks.end()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
bookmarks.erase(it);
|
bookmarks.erase(it);
|
||||||
Serial.printf("[%lu] [BMS] Removed bookmark at spine %u, offset %u\n", millis(), spineIndex, contentOffset);
|
Serial.printf("[%lu] [BMS] Removed bookmark at spine %u, offset %u\n", millis(), spineIndex, contentOffset);
|
||||||
|
|
||||||
return saveBookmarks(bookPath, bookmarks);
|
return saveBookmarks(bookPath, bookmarks);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool BookmarkStore::isPageBookmarked(const std::string& bookPath, uint16_t spineIndex, uint32_t contentOffset) {
|
bool BookmarkStore::isPageBookmarked(const std::string& bookPath, uint16_t spineIndex, uint32_t contentOffset) {
|
||||||
std::vector<Bookmark> bookmarks;
|
std::vector<Bookmark> bookmarks;
|
||||||
loadBookmarks(bookPath, bookmarks);
|
loadBookmarks(bookPath, bookmarks);
|
||||||
|
|
||||||
return std::any_of(bookmarks.begin(), bookmarks.end(), [&](const Bookmark& b) {
|
return std::any_of(bookmarks.begin(), bookmarks.end(),
|
||||||
return b.spineIndex == spineIndex && b.contentOffset == contentOffset;
|
[&](const Bookmark& b) { return b.spineIndex == spineIndex && b.contentOffset == contentOffset; });
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int BookmarkStore::getBookmarkCount(const std::string& bookPath) {
|
int BookmarkStore::getBookmarkCount(const std::string& bookPath) {
|
||||||
const std::string filePath = getBookmarksFilePath(bookPath);
|
const std::string filePath = getBookmarksFilePath(bookPath);
|
||||||
if (filePath.empty()) return 0;
|
if (filePath.empty()) return 0;
|
||||||
|
|
||||||
FsFile inputFile;
|
FsFile inputFile;
|
||||||
if (!SdMan.openFileForRead("BMS", filePath, inputFile)) {
|
if (!SdMan.openFileForRead("BMS", filePath, inputFile)) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t version;
|
uint8_t version;
|
||||||
serialization::readPod(inputFile, version);
|
serialization::readPod(inputFile, version);
|
||||||
if (version != BOOKMARKS_FILE_VERSION) {
|
if (version != BOOKMARKS_FILE_VERSION) {
|
||||||
inputFile.close();
|
inputFile.close();
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t count;
|
uint8_t count;
|
||||||
serialization::readPod(inputFile, count);
|
serialization::readPod(inputFile, count);
|
||||||
inputFile.close();
|
inputFile.close();
|
||||||
|
|
||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<BookmarkedBook> BookmarkStore::getBooksWithBookmarks() {
|
std::vector<BookmarkedBook> BookmarkStore::getBooksWithBookmarks() {
|
||||||
std::vector<BookmarkedBook> result;
|
std::vector<BookmarkedBook> result;
|
||||||
|
|
||||||
// Scan /.crosspoint/ directory for cache folders with bookmarks
|
// Scan /.crosspoint/ directory for cache folders with bookmarks
|
||||||
auto crosspoint = SdMan.open("/.crosspoint");
|
auto crosspoint = SdMan.open("/.crosspoint");
|
||||||
if (!crosspoint || !crosspoint.isDirectory()) {
|
if (!crosspoint || !crosspoint.isDirectory()) {
|
||||||
if (crosspoint) crosspoint.close();
|
if (crosspoint) crosspoint.close();
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
crosspoint.rewindDirectory();
|
crosspoint.rewindDirectory();
|
||||||
char name[256];
|
char name[256];
|
||||||
|
|
||||||
for (auto entry = crosspoint.openNextFile(); entry; entry = crosspoint.openNextFile()) {
|
for (auto entry = crosspoint.openNextFile(); entry; entry = crosspoint.openNextFile()) {
|
||||||
entry.getName(name, sizeof(name));
|
entry.getName(name, sizeof(name));
|
||||||
|
|
||||||
if (!entry.isDirectory()) {
|
if (!entry.isDirectory()) {
|
||||||
entry.close();
|
entry.close();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if this directory has a bookmarks file
|
// Check if this directory has a bookmarks file
|
||||||
std::string dirPath = "/.crosspoint/";
|
std::string dirPath = "/.crosspoint/";
|
||||||
dirPath += name;
|
dirPath += name;
|
||||||
std::string bookmarksPath = dirPath + "/" + BOOKMARKS_FILENAME;
|
std::string bookmarksPath = dirPath + "/" + BOOKMARKS_FILENAME;
|
||||||
|
|
||||||
if (SdMan.exists(bookmarksPath.c_str())) {
|
if (SdMan.exists(bookmarksPath.c_str())) {
|
||||||
// Read the bookmarks file to get count and book info
|
// Read the bookmarks file to get count and book info
|
||||||
FsFile bookmarksFile;
|
FsFile bookmarksFile;
|
||||||
if (SdMan.openFileForRead("BMS", bookmarksPath, bookmarksFile)) {
|
if (SdMan.openFileForRead("BMS", bookmarksPath, bookmarksFile)) {
|
||||||
uint8_t version;
|
uint8_t version;
|
||||||
serialization::readPod(bookmarksFile, version);
|
serialization::readPod(bookmarksFile, version);
|
||||||
|
|
||||||
if (version == BOOKMARKS_FILE_VERSION) {
|
if (version == BOOKMARKS_FILE_VERSION) {
|
||||||
uint8_t count;
|
uint8_t count;
|
||||||
serialization::readPod(bookmarksFile, count);
|
serialization::readPod(bookmarksFile, count);
|
||||||
|
|
||||||
// Read book metadata (stored at end of file)
|
// Read book metadata (stored at end of file)
|
||||||
std::string bookPath, bookTitle, bookAuthor;
|
std::string bookPath, bookTitle, bookAuthor;
|
||||||
|
|
||||||
// Skip bookmark entries to get to metadata
|
// Skip bookmark entries to get to metadata
|
||||||
for (uint8_t i = 0; i < count; i++) {
|
for (uint8_t i = 0; i < count; i++) {
|
||||||
std::string tempName;
|
std::string tempName;
|
||||||
@ -176,12 +174,12 @@ std::vector<BookmarkedBook> BookmarkStore::getBooksWithBookmarks() {
|
|||||||
serialization::readPod(bookmarksFile, tempPage);
|
serialization::readPod(bookmarksFile, tempPage);
|
||||||
serialization::readPod(bookmarksFile, tempTimestamp);
|
serialization::readPod(bookmarksFile, tempTimestamp);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read book metadata
|
// Read book metadata
|
||||||
serialization::readString(bookmarksFile, bookPath);
|
serialization::readString(bookmarksFile, bookPath);
|
||||||
serialization::readString(bookmarksFile, bookTitle);
|
serialization::readString(bookmarksFile, bookTitle);
|
||||||
serialization::readString(bookmarksFile, bookAuthor);
|
serialization::readString(bookmarksFile, bookAuthor);
|
||||||
|
|
||||||
if (!bookPath.empty() && count > 0) {
|
if (!bookPath.empty() && count > 0) {
|
||||||
BookmarkedBook book;
|
BookmarkedBook book;
|
||||||
book.path = bookPath;
|
book.path = bookPath;
|
||||||
@ -197,19 +195,18 @@ std::vector<BookmarkedBook> BookmarkStore::getBooksWithBookmarks() {
|
|||||||
entry.close();
|
entry.close();
|
||||||
}
|
}
|
||||||
crosspoint.close();
|
crosspoint.close();
|
||||||
|
|
||||||
// Sort by title
|
// Sort by title
|
||||||
std::sort(result.begin(), result.end(), [](const BookmarkedBook& a, const BookmarkedBook& b) {
|
std::sort(result.begin(), result.end(),
|
||||||
return a.title < b.title;
|
[](const BookmarkedBook& a, const BookmarkedBook& b) { return a.title < b.title; });
|
||||||
});
|
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
void BookmarkStore::clearBookmarks(const std::string& bookPath) {
|
void BookmarkStore::clearBookmarks(const std::string& bookPath) {
|
||||||
const std::string filePath = getBookmarksFilePath(bookPath);
|
const std::string filePath = getBookmarksFilePath(bookPath);
|
||||||
if (filePath.empty()) return;
|
if (filePath.empty()) return;
|
||||||
|
|
||||||
SdMan.remove(filePath.c_str());
|
SdMan.remove(filePath.c_str());
|
||||||
Serial.printf("[%lu] [BMS] Cleared all bookmarks for %s\n", millis(), bookPath.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<Bookmark>& bookmarks) {
|
bool BookmarkStore::saveBookmarks(const std::string& bookPath, const std::vector<Bookmark>& bookmarks) {
|
||||||
const std::string cacheDir = getCacheDir(bookPath);
|
const std::string cacheDir = getCacheDir(bookPath);
|
||||||
if (cacheDir.empty()) return false;
|
if (cacheDir.empty()) return false;
|
||||||
|
|
||||||
// Make sure the directory exists
|
// Make sure the directory exists
|
||||||
SdMan.mkdir(cacheDir.c_str());
|
SdMan.mkdir(cacheDir.c_str());
|
||||||
|
|
||||||
const std::string filePath = cacheDir + "/" + BOOKMARKS_FILENAME;
|
const std::string filePath = cacheDir + "/" + BOOKMARKS_FILENAME;
|
||||||
|
|
||||||
FsFile outputFile;
|
FsFile outputFile;
|
||||||
if (!SdMan.openFileForWrite("BMS", filePath, outputFile)) {
|
if (!SdMan.openFileForWrite("BMS", filePath, outputFile)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
serialization::writePod(outputFile, BOOKMARKS_FILE_VERSION);
|
serialization::writePod(outputFile, BOOKMARKS_FILE_VERSION);
|
||||||
const uint8_t count = static_cast<uint8_t>(std::min(bookmarks.size(), static_cast<size_t>(255)));
|
const uint8_t count = static_cast<uint8_t>(std::min(bookmarks.size(), static_cast<size_t>(255)));
|
||||||
serialization::writePod(outputFile, count);
|
serialization::writePod(outputFile, count);
|
||||||
|
|
||||||
for (size_t i = 0; i < count; i++) {
|
for (size_t i = 0; i < count; i++) {
|
||||||
const auto& bookmark = bookmarks[i];
|
const auto& bookmark = bookmarks[i];
|
||||||
serialization::writeString(outputFile, bookmark.name);
|
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.pageNumber);
|
||||||
serialization::writePod(outputFile, bookmark.timestamp);
|
serialization::writePod(outputFile, bookmark.timestamp);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store book metadata at end (for getBooksWithBookmarks to read)
|
// Store book metadata at end (for getBooksWithBookmarks to read)
|
||||||
// Extract title from path if we don't have it
|
// Extract title from path if we don't have it
|
||||||
std::string title = bookPath;
|
std::string title = bookPath;
|
||||||
@ -252,11 +249,11 @@ bool BookmarkStore::saveBookmarks(const std::string& bookPath, const std::vector
|
|||||||
if (dot != std::string::npos) {
|
if (dot != std::string::npos) {
|
||||||
title.resize(dot);
|
title.resize(dot);
|
||||||
}
|
}
|
||||||
|
|
||||||
serialization::writeString(outputFile, bookPath);
|
serialization::writeString(outputFile, bookPath);
|
||||||
serialization::writeString(outputFile, title);
|
serialization::writeString(outputFile, title);
|
||||||
serialization::writeString(outputFile, ""); // Author (not always available)
|
serialization::writeString(outputFile, ""); // Author (not always available)
|
||||||
|
|
||||||
outputFile.close();
|
outputFile.close();
|
||||||
Serial.printf("[%lu] [BMS] Bookmarks saved for %s (%d entries)\n", millis(), bookPath.c_str(), count);
|
Serial.printf("[%lu] [BMS] Bookmarks saved for %s (%d entries)\n", millis(), bookPath.c_str(), count);
|
||||||
return true;
|
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<Bookmark>& bookmarks) {
|
bool BookmarkStore::loadBookmarks(const std::string& bookPath, std::vector<Bookmark>& bookmarks) {
|
||||||
bookmarks.clear();
|
bookmarks.clear();
|
||||||
|
|
||||||
const std::string filePath = getBookmarksFilePath(bookPath);
|
const std::string filePath = getBookmarksFilePath(bookPath);
|
||||||
if (filePath.empty()) return false;
|
if (filePath.empty()) return false;
|
||||||
|
|
||||||
FsFile inputFile;
|
FsFile inputFile;
|
||||||
if (!SdMan.openFileForRead("BMS", filePath, inputFile)) {
|
if (!SdMan.openFileForRead("BMS", filePath, inputFile)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t version;
|
uint8_t version;
|
||||||
serialization::readPod(inputFile, version);
|
serialization::readPod(inputFile, version);
|
||||||
if (version != BOOKMARKS_FILE_VERSION) {
|
if (version != BOOKMARKS_FILE_VERSION) {
|
||||||
@ -280,11 +277,11 @@ bool BookmarkStore::loadBookmarks(const std::string& bookPath, std::vector<Bookm
|
|||||||
inputFile.close();
|
inputFile.close();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t count;
|
uint8_t count;
|
||||||
serialization::readPod(inputFile, count);
|
serialization::readPod(inputFile, count);
|
||||||
bookmarks.reserve(count);
|
bookmarks.reserve(count);
|
||||||
|
|
||||||
for (uint8_t i = 0; i < count; i++) {
|
for (uint8_t i = 0; i < count; i++) {
|
||||||
Bookmark bookmark;
|
Bookmark bookmark;
|
||||||
serialization::readString(inputFile, bookmark.name);
|
serialization::readString(inputFile, bookmark.name);
|
||||||
@ -294,7 +291,7 @@ bool BookmarkStore::loadBookmarks(const std::string& bookPath, std::vector<Bookm
|
|||||||
serialization::readPod(inputFile, bookmark.timestamp);
|
serialization::readPod(inputFile, bookmark.timestamp);
|
||||||
bookmarks.push_back(bookmark);
|
bookmarks.push_back(bookmark);
|
||||||
}
|
}
|
||||||
|
|
||||||
inputFile.close();
|
inputFile.close();
|
||||||
Serial.printf("[%lu] [BMS] Bookmarks loaded for %s (%d entries)\n", millis(), bookPath.c_str(), count);
|
Serial.printf("[%lu] [BMS] Bookmarks loaded for %s (%d entries)\n", millis(), bookPath.c_str(), count);
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@ -7,12 +7,12 @@ struct BookmarkedBook;
|
|||||||
|
|
||||||
// A single bookmark within a book
|
// A single bookmark within a book
|
||||||
struct Bookmark {
|
struct Bookmark {
|
||||||
std::string name; // Display name (e.g., "Chapter 1 - Page 42")
|
std::string name; // Display name (e.g., "Chapter 1 - Page 42")
|
||||||
uint16_t spineIndex = 0; // For EPUB: which spine item
|
uint16_t spineIndex = 0; // For EPUB: which spine item
|
||||||
uint32_t contentOffset = 0; // Content offset for stable positioning
|
uint32_t contentOffset = 0; // Content offset for stable positioning
|
||||||
uint16_t pageNumber = 0; // Page number at time of bookmark (for display)
|
uint16_t pageNumber = 0; // Page number at time of bookmark (for display)
|
||||||
uint32_t timestamp = 0; // Unix timestamp when created
|
uint32_t timestamp = 0; // Unix timestamp when created
|
||||||
|
|
||||||
bool operator==(const Bookmark& other) const {
|
bool operator==(const Bookmark& other) const {
|
||||||
return spineIndex == other.spineIndex && contentOffset == other.contentOffset;
|
return spineIndex == other.spineIndex && contentOffset == other.contentOffset;
|
||||||
}
|
}
|
||||||
@ -22,7 +22,7 @@ struct Bookmark {
|
|||||||
* BookmarkStore manages bookmarks for books.
|
* BookmarkStore manages bookmarks for books.
|
||||||
* Bookmarks are stored per-book in the book's cache directory:
|
* Bookmarks are stored per-book in the book's cache directory:
|
||||||
* /.crosspoint/{epub_|txt_}<hash>/bookmarks.bin
|
* /.crosspoint/{epub_|txt_}<hash>/bookmarks.bin
|
||||||
*
|
*
|
||||||
* This is a static utility class, not a singleton, since bookmarks
|
* This is a static utility class, not a singleton, since bookmarks
|
||||||
* are loaded/saved on demand for specific books.
|
* are loaded/saved on demand for specific books.
|
||||||
*/
|
*/
|
||||||
@ -30,34 +30,34 @@ class BookmarkStore {
|
|||||||
public:
|
public:
|
||||||
// Get all bookmarks for a book
|
// Get all bookmarks for a book
|
||||||
static std::vector<Bookmark> getBookmarks(const std::string& bookPath);
|
static std::vector<Bookmark> getBookmarks(const std::string& bookPath);
|
||||||
|
|
||||||
// Add a bookmark to a book
|
// Add a bookmark to a book
|
||||||
// Returns true if added, false if bookmark already exists at that location
|
// Returns true if added, false if bookmark already exists at that location
|
||||||
static bool addBookmark(const std::string& bookPath, const Bookmark& bookmark);
|
static bool addBookmark(const std::string& bookPath, const Bookmark& bookmark);
|
||||||
|
|
||||||
// Remove a bookmark from a book by content offset
|
// Remove a bookmark from a book by content offset
|
||||||
// Returns true if removed, false if not found
|
// Returns true if removed, false if not found
|
||||||
static bool removeBookmark(const std::string& bookPath, uint16_t spineIndex, uint32_t contentOffset);
|
static bool removeBookmark(const std::string& bookPath, uint16_t spineIndex, uint32_t contentOffset);
|
||||||
|
|
||||||
// Check if a specific page is bookmarked
|
// Check if a specific page is bookmarked
|
||||||
static bool isPageBookmarked(const std::string& bookPath, uint16_t spineIndex, uint32_t contentOffset);
|
static bool isPageBookmarked(const std::string& bookPath, uint16_t spineIndex, uint32_t contentOffset);
|
||||||
|
|
||||||
// Get count of bookmarks for a book (without loading all data)
|
// Get count of bookmarks for a book (without loading all data)
|
||||||
static int getBookmarkCount(const std::string& bookPath);
|
static int getBookmarkCount(const std::string& bookPath);
|
||||||
|
|
||||||
// Get all books that have bookmarks (for Bookmarks tab)
|
// Get all books that have bookmarks (for Bookmarks tab)
|
||||||
static std::vector<BookmarkedBook> getBooksWithBookmarks();
|
static std::vector<BookmarkedBook> getBooksWithBookmarks();
|
||||||
|
|
||||||
// Delete all bookmarks for a book
|
// Delete all bookmarks for a book
|
||||||
static void clearBookmarks(const std::string& bookPath);
|
static void clearBookmarks(const std::string& bookPath);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// Get the bookmarks file path for a book
|
// Get the bookmarks file path for a book
|
||||||
static std::string getBookmarksFilePath(const std::string& bookPath);
|
static std::string getBookmarksFilePath(const std::string& bookPath);
|
||||||
|
|
||||||
// Save bookmarks to file
|
// Save bookmarks to file
|
||||||
static bool saveBookmarks(const std::string& bookPath, const std::vector<Bookmark>& bookmarks);
|
static bool saveBookmarks(const std::string& bookPath, const std::vector<Bookmark>& bookmarks);
|
||||||
|
|
||||||
// Load bookmarks from file
|
// Load bookmarks from file
|
||||||
static bool loadBookmarks(const std::string& bookPath, std::vector<Bookmark>& bookmarks);
|
static bool loadBookmarks(const std::string& bookPath, std::vector<Bookmark>& bookmarks);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -193,7 +193,7 @@ bool CrossPointSettings::loadFromFile() {
|
|||||||
float CrossPointSettings::getReaderLineCompression() const {
|
float CrossPointSettings::getReaderLineCompression() const {
|
||||||
// For custom fonts, use the fallback font's line compression
|
// For custom fonts, use the fallback font's line compression
|
||||||
const uint8_t effectiveFamily = (fontFamily == CUSTOM_FONT) ? fallbackFontFamily : fontFamily;
|
const uint8_t effectiveFamily = (fontFamily == CUSTOM_FONT) ? fallbackFontFamily : fontFamily;
|
||||||
|
|
||||||
switch (effectiveFamily) {
|
switch (effectiveFamily) {
|
||||||
case BOOKERLY:
|
case BOOKERLY:
|
||||||
default:
|
default:
|
||||||
|
|||||||
@ -26,7 +26,14 @@ class CrossPointSettings {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Status bar display type enum
|
// 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 {
|
enum ORIENTATION {
|
||||||
PORTRAIT = 0, // 480x800 logical coordinates (current default)
|
PORTRAIT = 0, // 480x800 logical coordinates (current default)
|
||||||
@ -116,7 +123,7 @@ class CrossPointSettings {
|
|||||||
uint8_t sideButtonLayout = PREV_NEXT;
|
uint8_t sideButtonLayout = PREV_NEXT;
|
||||||
// Reader font settings
|
// Reader font settings
|
||||||
uint8_t fontFamily = BOOKERLY;
|
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 fallbackFontFamily = BOOKERLY; // Fallback for missing glyphs/weights in custom fonts
|
||||||
uint8_t fontSize = MEDIUM;
|
uint8_t fontSize = MEDIUM;
|
||||||
uint8_t lineSpacing = NORMAL;
|
uint8_t lineSpacing = NORMAL;
|
||||||
|
|||||||
@ -90,13 +90,14 @@ void ScreenComponents::drawBookProgressBar(const GfxRenderer& renderer, const si
|
|||||||
renderer.fillRect(vieweableMarginLeft, progressBarY, barWidth, BOOK_PROGRESS_BAR_HEIGHT, true);
|
renderer.fillRect(vieweableMarginLeft, progressBarY, barWidth, BOOK_PROGRESS_BAR_HEIGHT, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
int ScreenComponents::drawTabBar(const GfxRenderer& renderer, const int y, const std::vector<TabInfo>& tabs, int selectedIndex, bool showCursor) {
|
int ScreenComponents::drawTabBar(const GfxRenderer& renderer, const int y, const std::vector<TabInfo>& tabs,
|
||||||
constexpr int tabPadding = 20; // Horizontal padding between tabs
|
int selectedIndex, bool showCursor) {
|
||||||
constexpr int leftMargin = 20; // Left margin for first tab
|
constexpr int tabPadding = 20; // Horizontal padding between tabs
|
||||||
constexpr int rightMargin = 20; // Right margin
|
constexpr int leftMargin = 20; // Left margin for first tab
|
||||||
constexpr int underlineHeight = 2; // Height of selection underline
|
constexpr int rightMargin = 20; // Right margin
|
||||||
constexpr int underlineGap = 4; // Gap between text and underline
|
constexpr int underlineHeight = 2; // Height of selection underline
|
||||||
constexpr int cursorPadding = 4; // Space between bullet cursor and tab text
|
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
|
constexpr int overflowIndicatorWidth = 16; // Space reserved for < > indicators
|
||||||
|
|
||||||
const int lineHeight = renderer.getLineHeight(UI_12_FONT_ID);
|
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<int> tabWidths;
|
std::vector<int> tabWidths;
|
||||||
int totalWidth = 0;
|
int totalWidth = 0;
|
||||||
for (const auto& tab : tabs) {
|
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);
|
tabWidths.push_back(textWidth);
|
||||||
totalWidth += textWidth;
|
totalWidth += textWidth;
|
||||||
}
|
}
|
||||||
@ -151,13 +153,13 @@ int ScreenComponents::drawTabBar(const GfxRenderer& renderer, const int y, const
|
|||||||
// Bullet cursor settings
|
// Bullet cursor settings
|
||||||
constexpr int bulletRadius = 3;
|
constexpr int bulletRadius = 3;
|
||||||
const int bulletCenterY = y + lineHeight / 2;
|
const int bulletCenterY = y + lineHeight / 2;
|
||||||
|
|
||||||
// Calculate visible area boundaries (leave room for overflow indicators)
|
// Calculate visible area boundaries (leave room for overflow indicators)
|
||||||
const bool hasLeftOverflow = scrollOffset > 0;
|
const bool hasLeftOverflow = scrollOffset > 0;
|
||||||
const bool hasRightOverflow = totalWidth > availableWidth && scrollOffset < totalWidth - availableWidth;
|
const bool hasRightOverflow = totalWidth > availableWidth && scrollOffset < totalWidth - availableWidth;
|
||||||
const int visibleLeft = bezelLeft + (hasLeftOverflow ? overflowIndicatorWidth : 0);
|
const int visibleLeft = bezelLeft + (hasLeftOverflow ? overflowIndicatorWidth : 0);
|
||||||
const int visibleRight = screenWidth - bezelRight - (hasRightOverflow ? overflowIndicatorWidth : 0);
|
const int visibleRight = screenWidth - bezelRight - (hasRightOverflow ? overflowIndicatorWidth : 0);
|
||||||
|
|
||||||
for (size_t i = 0; i < tabs.size(); i++) {
|
for (size_t i = 0; i < tabs.size(); i++) {
|
||||||
const auto& tab = tabs[i];
|
const auto& tab = tabs[i];
|
||||||
const int textWidth = tabWidths[i];
|
const int textWidth = tabWidths[i];
|
||||||
@ -177,7 +179,7 @@ int ScreenComponents::drawTabBar(const GfxRenderer& renderer, const int y, const
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw tab label
|
// Draw tab label
|
||||||
renderer.drawText(UI_12_FONT_ID, currentX, y, tab.label, true,
|
renderer.drawText(UI_12_FONT_ID, currentX, y, tab.label, true,
|
||||||
tab.selected ? EpdFontFamily::BOLD : EpdFontFamily::REGULAR);
|
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
|
// Draw underline for selected tab
|
||||||
if (tab.selected) {
|
if (tab.selected) {
|
||||||
renderer.fillRect(currentX, y + lineHeight + underlineGap, textWidth, underlineHeight);
|
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 triangleHeight = 12; // Height of the triangle (vertical)
|
||||||
constexpr int triangleWidth = 6; // Width of the triangle (horizontal) - thin/elongated
|
constexpr int triangleWidth = 6; // Width of the triangle (horizontal) - thin/elongated
|
||||||
const int triangleCenterY = y + lineHeight / 2;
|
const int triangleCenterY = y + lineHeight / 2;
|
||||||
|
|
||||||
// Left overflow indicator (more content to the left) - thin triangle pointing left
|
// Left overflow indicator (more content to the left) - thin triangle pointing left
|
||||||
if (scrollOffset > 0) {
|
if (scrollOffset > 0) {
|
||||||
// Clear background behind indicator to hide any overlapping text
|
// 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) {
|
for (int i = 0; i < triangleWidth; ++i) {
|
||||||
// Scale height based on position (0 at tip, full height at base)
|
// Scale height based on position (0 at tip, full height at base)
|
||||||
const int lineHalfHeight = (triangleHeight * i) / (triangleWidth * 2);
|
const int lineHalfHeight = (triangleHeight * i) / (triangleWidth * 2);
|
||||||
renderer.drawLine(tipX + i, triangleCenterY - lineHalfHeight,
|
renderer.drawLine(tipX + i, triangleCenterY - lineHalfHeight, tipX + i, triangleCenterY + lineHalfHeight);
|
||||||
tipX + i, triangleCenterY + lineHalfHeight);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Right overflow indicator (more content to the right) - thin triangle pointing right
|
// Right overflow indicator (more content to the right) - thin triangle pointing right
|
||||||
if (scrollOffset < totalWidth - availableWidth) {
|
if (scrollOffset < totalWidth - availableWidth) {
|
||||||
// Clear background behind indicator to hide any overlapping text
|
// 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
|
// Draw right-pointing triangle: base on left, point on right
|
||||||
const int baseX = screenWidth - bezelRight - 2 - triangleWidth;
|
const int baseX = screenWidth - bezelRight - 2 - triangleWidth;
|
||||||
for (int i = 0; i < triangleWidth; ++i) {
|
for (int i = 0; i < triangleWidth; ++i) {
|
||||||
// Scale height based on position (full height at base, 0 at tip)
|
// Scale height based on position (full height at base, 0 at tip)
|
||||||
const int lineHalfHeight = (triangleHeight * (triangleWidth - 1 - i)) / (triangleWidth * 2);
|
const int lineHalfHeight = (triangleHeight * (triangleWidth - 1 - i)) / (triangleWidth * 2);
|
||||||
renderer.drawLine(baseX + i, triangleCenterY - lineHalfHeight,
|
renderer.drawLine(baseX + i, triangleCenterY - lineHalfHeight, baseX + i, triangleCenterY + lineHalfHeight);
|
||||||
baseX + i, triangleCenterY + lineHalfHeight);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,7 +25,8 @@ class ScreenComponents {
|
|||||||
// Returns the height of the tab bar (for positioning content below)
|
// Returns the height of the tab bar (for positioning content below)
|
||||||
// When selectedIndex is provided, tabs scroll so the selected tab is visible
|
// When selectedIndex is provided, tabs scroll so the selected tab is visible
|
||||||
// When showCursor is true, bullet indicators are drawn around the selected tab
|
// When showCursor is true, bullet indicators are drawn around the selected tab
|
||||||
static int drawTabBar(const GfxRenderer& renderer, int y, const std::vector<TabInfo>& tabs, int selectedIndex = -1, bool showCursor = false);
|
static int drawTabBar(const GfxRenderer& renderer, int y, const std::vector<TabInfo>& tabs, int selectedIndex = -1,
|
||||||
|
bool showCursor = false);
|
||||||
|
|
||||||
// Draw a scroll/page indicator on the right side of the screen
|
// Draw a scroll/page indicator on the right side of the screen
|
||||||
// Shows up/down arrows and current page fraction (e.g., "1/3")
|
// Shows up/down arrows and current page fraction (e.g., "1/3")
|
||||||
|
|||||||
@ -12,13 +12,13 @@ class GfxRenderer;
|
|||||||
|
|
||||||
// Helper macro to log stack high-water mark for a task
|
// Helper macro to log stack high-water mark for a task
|
||||||
// Usage: LOG_STACK_WATERMARK("ActivityName", taskHandle);
|
// Usage: LOG_STACK_WATERMARK("ActivityName", taskHandle);
|
||||||
#define LOG_STACK_WATERMARK(name, handle) \
|
#define LOG_STACK_WATERMARK(name, handle) \
|
||||||
do { \
|
do { \
|
||||||
if (handle) { \
|
if (handle) { \
|
||||||
UBaseType_t remaining = uxTaskGetStackHighWaterMark(handle); \
|
UBaseType_t remaining = uxTaskGetStackHighWaterMark(handle); \
|
||||||
Serial.printf("[%lu] [STACK] %s: %u bytes remaining\n", millis(), name, remaining * sizeof(StackType_t)); \
|
Serial.printf("[%lu] [STACK] %s: %u bytes remaining\n", millis(), name, remaining * sizeof(StackType_t)); \
|
||||||
} \
|
} \
|
||||||
} while(0)
|
} while (0)
|
||||||
|
|
||||||
class Activity {
|
class Activity {
|
||||||
protected:
|
protected:
|
||||||
|
|||||||
@ -184,7 +184,8 @@ void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap, const std::str
|
|||||||
drawHeight = 0;
|
drawHeight = 0;
|
||||||
fillWidth = static_cast<int>(bitmap.getWidth());
|
fillWidth = static_cast<int>(bitmap.getWidth());
|
||||||
fillHeight = static_cast<int>(bitmap.getHeight());
|
fillHeight = static_cast<int>(bitmap.getHeight());
|
||||||
Serial.printf("[%lu] [SLP] ACTUAL mode: centering at %d, %d (fill: %dx%d)\n", millis(), x, y, fillWidth, fillHeight);
|
Serial.printf("[%lu] [SLP] ACTUAL mode: centering at %d, %d (fill: %dx%d)\n", millis(), x, y, fillWidth,
|
||||||
|
fillHeight);
|
||||||
} else if (coverMode == CrossPointSettings::SLEEP_SCREEN_COVER_MODE::CROP) {
|
} else if (coverMode == CrossPointSettings::SLEEP_SCREEN_COVER_MODE::CROP) {
|
||||||
// CROP mode: Scale to fill screen completely (may crop edges)
|
// CROP mode: Scale to fill screen completely (may crop edges)
|
||||||
// Calculate crop values to fill the screen while maintaining aspect ratio
|
// 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
|
// Center the scaled image
|
||||||
x = (pageWidth - fillWidth) / 2;
|
x = (pageWidth - fillWidth) / 2;
|
||||||
y = (pageHeight - fillHeight) / 2;
|
y = (pageHeight - fillHeight) / 2;
|
||||||
Serial.printf("[%lu] [SLP] FIT mode: scale %f, scaled size %d x %d, position %d, %d\n", millis(), scale,
|
Serial.printf("[%lu] [SLP] FIT mode: scale %f, scaled size %d x %d, position %d, %d\n", millis(), scale, fillWidth,
|
||||||
fillWidth, fillHeight, x, y);
|
fillHeight, x, y);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get edge luminance values (from cache or calculate)
|
// 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 leftGray = quantizeGray(edges.left);
|
||||||
const uint8_t rightGray = quantizeGray(edges.right);
|
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",
|
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(),
|
||||||
millis(), edges.top, edges.bottom, edges.left, edges.right,
|
edges.top, edges.bottom, edges.left, edges.right, topGray, bottomGray, leftGray, rightGray);
|
||||||
topGray, bottomGray, leftGray, rightGray);
|
|
||||||
|
|
||||||
// Check if greyscale pass should be used (PR #476: skip if filter is applied)
|
// Check if greyscale pass should be used (PR #476: skip if filter is applied)
|
||||||
const bool hasGreyscale = bitmap.hasGreyscale() &&
|
const bool hasGreyscale = bitmap.hasGreyscale() &&
|
||||||
@ -418,10 +418,10 @@ std::string SleepActivity::getEdgeCachePath(const std::string& bmpPath) {
|
|||||||
uint8_t SleepActivity::quantizeGray(uint8_t lum) {
|
uint8_t SleepActivity::quantizeGray(uint8_t lum) {
|
||||||
// Quantize luminance (0-255) to 4-level grayscale (0-3)
|
// Quantize luminance (0-255) to 4-level grayscale (0-3)
|
||||||
// Thresholds tuned for X4 display gray levels
|
// Thresholds tuned for X4 display gray levels
|
||||||
if (lum < 43) return 0; // black
|
if (lum < 43) return 0; // black
|
||||||
if (lum < 128) return 1; // dark gray
|
if (lum < 128) return 1; // dark gray
|
||||||
if (lum < 213) return 2; // light gray
|
if (lum < 213) return 2; // light gray
|
||||||
return 3; // white
|
return 3; // white
|
||||||
}
|
}
|
||||||
|
|
||||||
EdgeLuminance SleepActivity::getEdgeLuminance(const Bitmap& bitmap, const std::string& bmpPath) const {
|
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];
|
uint8_t cacheData[EDGE_CACHE_SIZE];
|
||||||
if (cacheFile.read(cacheData, EDGE_CACHE_SIZE) == EDGE_CACHE_SIZE) {
|
if (cacheFile.read(cacheData, EDGE_CACHE_SIZE) == EDGE_CACHE_SIZE) {
|
||||||
// Extract cached file size
|
// Extract cached file size
|
||||||
const uint32_t cachedSize = static_cast<uint32_t>(cacheData[0]) |
|
const uint32_t cachedSize = static_cast<uint32_t>(cacheData[0]) | (static_cast<uint32_t>(cacheData[1]) << 8) |
|
||||||
(static_cast<uint32_t>(cacheData[1]) << 8) |
|
|
||||||
(static_cast<uint32_t>(cacheData[2]) << 16) |
|
(static_cast<uint32_t>(cacheData[2]) << 16) |
|
||||||
(static_cast<uint32_t>(cacheData[3]) << 24);
|
(static_cast<uint32_t>(cacheData[3]) << 24);
|
||||||
|
|
||||||
@ -448,8 +447,8 @@ EdgeLuminance SleepActivity::getEdgeLuminance(const Bitmap& bitmap, const std::s
|
|||||||
result.bottom = cacheData[5];
|
result.bottom = cacheData[5];
|
||||||
result.left = cacheData[6];
|
result.left = cacheData[6];
|
||||||
result.right = cacheData[7];
|
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(),
|
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.top, result.bottom, result.left, result.right);
|
result.bottom, result.left, result.right);
|
||||||
cacheFile.close();
|
cacheFile.close();
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@ -462,8 +461,8 @@ EdgeLuminance SleepActivity::getEdgeLuminance(const Bitmap& bitmap, const std::s
|
|||||||
// Cache miss - calculate edge luminance
|
// Cache miss - calculate edge luminance
|
||||||
Serial.printf("[%lu] [SLP] Calculating edge luminance for %s\n", millis(), bmpPath.c_str());
|
Serial.printf("[%lu] [SLP] Calculating edge luminance for %s\n", millis(), bmpPath.c_str());
|
||||||
result = bitmap.detectEdgeLuminance(2); // Sample 2 pixels deep for stability
|
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(),
|
Serial.printf("[%lu] [SLP] Edge luminance detected: T=%d B=%d L=%d R=%d\n", millis(), result.top, result.bottom,
|
||||||
result.top, result.bottom, result.left, result.right);
|
result.left, result.right);
|
||||||
|
|
||||||
// Get BMP file size from already-opened bitmap for cache
|
// Get BMP file size from already-opened bitmap for cache
|
||||||
const uint32_t fileSize = bitmap.getFileSize();
|
const uint32_t fileSize = bitmap.getFileSize();
|
||||||
@ -534,8 +533,7 @@ bool SleepActivity::tryRenderCachedCoverSleep(const std::string& bookPath, bool
|
|||||||
cacheFile.close();
|
cacheFile.close();
|
||||||
|
|
||||||
// Extract cached values
|
// Extract cached values
|
||||||
const uint32_t cachedBmpSize = static_cast<uint32_t>(cacheData[0]) |
|
const uint32_t cachedBmpSize = static_cast<uint32_t>(cacheData[0]) | (static_cast<uint32_t>(cacheData[1]) << 8) |
|
||||||
(static_cast<uint32_t>(cacheData[1]) << 8) |
|
|
||||||
(static_cast<uint32_t>(cacheData[2]) << 16) |
|
(static_cast<uint32_t>(cacheData[2]) << 16) |
|
||||||
(static_cast<uint32_t>(cacheData[3]) << 24);
|
(static_cast<uint32_t>(cacheData[3]) << 24);
|
||||||
EdgeLuminance cachedEdges;
|
EdgeLuminance cachedEdges;
|
||||||
@ -548,8 +546,8 @@ bool SleepActivity::tryRenderCachedCoverSleep(const std::string& bookPath, bool
|
|||||||
// Check if cover mode matches (for EPUB)
|
// Check if cover mode matches (for EPUB)
|
||||||
const uint8_t currentCoverMode = cropped ? 1 : 0;
|
const uint8_t currentCoverMode = cropped ? 1 : 0;
|
||||||
if (StringUtils::checkFileExtension(bookPath, ".epub") && cachedCoverMode != currentCoverMode) {
|
if (StringUtils::checkFileExtension(bookPath, ".epub") && cachedCoverMode != currentCoverMode) {
|
||||||
Serial.printf("[SLP] Cover mode changed (cached=%d, current=%d), invalidating cache\n",
|
Serial.printf("[SLP] Cover mode changed (cached=%d, current=%d), invalidating cache\n", cachedCoverMode,
|
||||||
cachedCoverMode, currentCoverMode);
|
currentCoverMode);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -571,8 +569,8 @@ bool SleepActivity::tryRenderCachedCoverSleep(const std::string& bookPath, bool
|
|||||||
// Check if BMP file size matches cache
|
// Check if BMP file size matches cache
|
||||||
const uint32_t currentBmpSize = bmpFile.size();
|
const uint32_t currentBmpSize = bmpFile.size();
|
||||||
if (currentBmpSize != cachedBmpSize || currentBmpSize == 0) {
|
if (currentBmpSize != cachedBmpSize || currentBmpSize == 0) {
|
||||||
Serial.printf("[SLP] BMP size mismatch (cached=%lu, current=%lu)\n",
|
Serial.printf("[SLP] BMP size mismatch (cached=%lu, current=%lu)\n", static_cast<unsigned long>(cachedBmpSize),
|
||||||
static_cast<unsigned long>(cachedBmpSize), static_cast<unsigned long>(currentBmpSize));
|
static_cast<unsigned long>(currentBmpSize));
|
||||||
bmpFile.close();
|
bmpFile.close();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -585,8 +583,8 @@ bool SleepActivity::tryRenderCachedCoverSleep(const std::string& bookPath, bool
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Serial.printf("[%lu] [SLP] Using cached cover sleep: %s (T=%d B=%d L=%d R=%d)\n", millis(),
|
Serial.printf("[%lu] [SLP] Using cached cover sleep: %s (T=%d B=%d L=%d R=%d)\n", millis(), coverBmpPath.c_str(),
|
||||||
coverBmpPath.c_str(), cachedEdges.top, cachedEdges.bottom, cachedEdges.left, cachedEdges.right);
|
cachedEdges.top, cachedEdges.bottom, cachedEdges.left, cachedEdges.right);
|
||||||
|
|
||||||
// Render the bitmap with cached edge values
|
// Render the bitmap with cached edge values
|
||||||
// We call renderBitmapSleepScreen which will use getEdgeLuminance internally,
|
// We call renderBitmapSleepScreen which will use getEdgeLuminance internally,
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include "../Activity.h"
|
|
||||||
|
|
||||||
#include <Bitmap.h>
|
#include <Bitmap.h>
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
|
#include "../Activity.h"
|
||||||
|
|
||||||
class SleepActivity final : public Activity {
|
class SleepActivity final : public Activity {
|
||||||
public:
|
public:
|
||||||
explicit SleepActivity(GfxRenderer& renderer, MappedInputManager& mappedInput)
|
explicit SleepActivity(GfxRenderer& renderer, MappedInputManager& mappedInput)
|
||||||
|
|||||||
@ -236,7 +236,8 @@ void OpdsBookBrowserActivity::render() const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const auto pageStartIndex = selectorIndex / PAGE_ITEMS * PAGE_ITEMS;
|
const auto pageStartIndex = selectorIndex / PAGE_ITEMS * PAGE_ITEMS;
|
||||||
renderer.fillRect(bezelLeft, 60 + bezelTop + (selectorIndex % PAGE_ITEMS) * 30 - 2, pageWidth - 1 - bezelLeft - bezelRight, 30);
|
renderer.fillRect(bezelLeft, 60 + bezelTop + (selectorIndex % PAGE_ITEMS) * 30 - 2,
|
||||||
|
pageWidth - 1 - bezelLeft - bezelRight, 30);
|
||||||
|
|
||||||
for (size_t i = pageStartIndex; i < entries.size() && i < static_cast<size_t>(pageStartIndex + PAGE_ITEMS); i++) {
|
for (size_t i = pageStartIndex; i < entries.size() && i < static_cast<size_t>(pageStartIndex + PAGE_ITEMS); i++) {
|
||||||
const auto& entry = entries[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(),
|
renderer.drawText(UI_10_FONT_ID, 20 + bezelLeft, 60 + bezelTop + (i % PAGE_ITEMS) * 30, item.c_str(),
|
||||||
i != static_cast<size_t>(selectorIndex));
|
i != static_cast<size_t>(selectorIndex));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,8 +9,7 @@
|
|||||||
namespace {
|
namespace {
|
||||||
constexpr int MAX_MENU_ITEM_COUNT = 2;
|
constexpr int MAX_MENU_ITEM_COUNT = 2;
|
||||||
const char* MENU_ITEMS[MAX_MENU_ITEM_COUNT] = {"Select from Screen", "Enter a Word"};
|
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",
|
const char* MENU_DESCRIPTIONS[MAX_MENU_ITEM_COUNT] = {"Choose a word from the current page", "Type a word to look up"};
|
||||||
"Type a word to look up"};
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
void DictionaryMenuActivity::taskTrampoline(void* param) {
|
void DictionaryMenuActivity::taskTrampoline(void* param) {
|
||||||
@ -64,8 +63,7 @@ void DictionaryMenuActivity::loop() {
|
|||||||
// Handle confirm button - select current option
|
// Handle confirm button - select current option
|
||||||
// Use wasReleased to consume the full button event
|
// Use wasReleased to consume the full button event
|
||||||
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||||
const DictionaryMode mode =
|
const DictionaryMode mode = (selectedIndex == 0) ? DictionaryMode::SELECT_FROM_SCREEN : DictionaryMode::ENTER_WORD;
|
||||||
(selectedIndex == 0) ? DictionaryMode::SELECT_FROM_SCREEN : DictionaryMode::ENTER_WORD;
|
|
||||||
onModeSelected(mode);
|
onModeSelected(mode);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -93,8 +93,8 @@ void DictionaryResultActivity::paginateDefinition() {
|
|||||||
const auto pageHeight = renderer.getScreenHeight();
|
const auto pageHeight = renderer.getScreenHeight();
|
||||||
|
|
||||||
// Calculate available area for text (must match render() layout)
|
// Calculate available area for text (must match render() layout)
|
||||||
constexpr int headerHeight = 80; // Space for word and header (relative to marginTop)
|
constexpr int headerHeight = 80; // Space for word and header (relative to marginTop)
|
||||||
constexpr int footerHeight = 30; // Space for page indicator
|
constexpr int footerHeight = 30; // Space for page indicator
|
||||||
const int textMargin = marginLeft + 10;
|
const int textMargin = marginLeft + 10;
|
||||||
const int textWidth = pageWidth - textMargin - marginRight - 10;
|
const int textWidth = pageWidth - textMargin - marginRight - 10;
|
||||||
const int textHeight = pageHeight - marginTop - marginBottom - headerHeight - footerHeight;
|
const int textHeight = pageHeight - marginTop - marginBottom - headerHeight - footerHeight;
|
||||||
|
|||||||
@ -1,10 +1,9 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
#include <Epub/blocks/TextBlock.h>
|
||||||
#include <freertos/FreeRTOS.h>
|
#include <freertos/FreeRTOS.h>
|
||||||
#include <freertos/semphr.h>
|
#include <freertos/semphr.h>
|
||||||
#include <freertos/task.h>
|
#include <freertos/task.h>
|
||||||
|
|
||||||
#include <Epub/blocks/TextBlock.h>
|
|
||||||
|
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
@ -48,8 +47,7 @@ class DictionaryResultActivity final : public Activity {
|
|||||||
*/
|
*/
|
||||||
explicit DictionaryResultActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
|
explicit DictionaryResultActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
|
||||||
const std::string& wordToLookup, const std::string& definition,
|
const std::string& wordToLookup, const std::string& definition,
|
||||||
const std::function<void()>& onBack,
|
const std::function<void()>& onBack, const std::function<void()>& onSearchAnother)
|
||||||
const std::function<void()>& onSearchAnother)
|
|
||||||
: Activity("DictionaryResult", renderer, mappedInput),
|
: Activity("DictionaryResult", renderer, mappedInput),
|
||||||
lookupWord(wordToLookup),
|
lookupWord(wordToLookup),
|
||||||
rawDefinition(definition),
|
rawDefinition(definition),
|
||||||
|
|||||||
@ -77,9 +77,8 @@ void EpubWordSelectionActivity::buildWordList() {
|
|||||||
while (wordIt != words.end() && xPosIt != xPositions.end() && styleIt != styles.end()) {
|
while (wordIt != words.end() && xPosIt != xPositions.end() && styleIt != styles.end()) {
|
||||||
// Skip whitespace-only words
|
// Skip whitespace-only words
|
||||||
const std::string& wordText = *wordIt;
|
const std::string& wordText = *wordIt;
|
||||||
const bool hasAlpha = std::any_of(wordText.begin(), wordText.end(), [](char c) {
|
const bool hasAlpha = std::any_of(wordText.begin(), wordText.end(),
|
||||||
return std::isalpha(static_cast<unsigned char>(c));
|
[](char c) { return std::isalpha(static_cast<unsigned char>(c)); });
|
||||||
});
|
|
||||||
|
|
||||||
if (hasAlpha) {
|
if (hasAlpha) {
|
||||||
WordInfo info;
|
WordInfo info;
|
||||||
@ -249,7 +248,8 @@ void EpubWordSelectionActivity::render() const {
|
|||||||
|
|
||||||
// Draw instruction text - position it just above the front button area
|
// Draw instruction text - position it just above the front button area
|
||||||
const auto screenHeight = renderer.getScreenHeight();
|
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
|
// Draw button hints
|
||||||
const auto labels = mappedInput.mapLabels("\xc2\xab Cancel", "Select", "< >", "");
|
const auto labels = mappedInput.mapLabels("\xc2\xab Cancel", "Select", "< >", "");
|
||||||
|
|||||||
@ -39,9 +39,7 @@ int BookmarkListActivity::getCurrentPage() const {
|
|||||||
return selectorIndex / pageItems + 1;
|
return selectorIndex / pageItems + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
void BookmarkListActivity::loadBookmarks() {
|
void BookmarkListActivity::loadBookmarks() { bookmarks = BookmarkStore::getBookmarks(bookPath); }
|
||||||
bookmarks = BookmarkStore::getBookmarks(bookPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
void BookmarkListActivity::taskTrampoline(void* param) {
|
void BookmarkListActivity::taskTrampoline(void* param) {
|
||||||
auto* self = static_cast<BookmarkListActivity*>(param);
|
auto* self = static_cast<BookmarkListActivity*>(param);
|
||||||
@ -95,7 +93,7 @@ void BookmarkListActivity::loop() {
|
|||||||
const auto& bm = bookmarks[selectorIndex];
|
const auto& bm = bookmarks[selectorIndex];
|
||||||
BookmarkStore::removeBookmark(bookPath, bm.spineIndex, bm.contentOffset);
|
BookmarkStore::removeBookmark(bookPath, bm.spineIndex, bm.contentOffset);
|
||||||
loadBookmarks();
|
loadBookmarks();
|
||||||
|
|
||||||
// Adjust selector if needed
|
// Adjust selector if needed
|
||||||
if (selectorIndex >= static_cast<int>(bookmarks.size()) && !bookmarks.empty()) {
|
if (selectorIndex >= static_cast<int>(bookmarks.size()) && !bookmarks.empty()) {
|
||||||
selectorIndex = static_cast<int>(bookmarks.size()) - 1;
|
selectorIndex = static_cast<int>(bookmarks.size()) - 1;
|
||||||
@ -115,9 +113,8 @@ void BookmarkListActivity::loop() {
|
|||||||
const int itemCount = static_cast<int>(bookmarks.size());
|
const int itemCount = static_cast<int>(bookmarks.size());
|
||||||
|
|
||||||
// Long press Confirm to delete bookmark
|
// Long press Confirm to delete bookmark
|
||||||
if (mappedInput.isPressed(MappedInputManager::Button::Confirm) &&
|
if (mappedInput.isPressed(MappedInputManager::Button::Confirm) && mappedInput.getHeldTime() >= ACTION_MENU_MS &&
|
||||||
mappedInput.getHeldTime() >= ACTION_MENU_MS && !bookmarks.empty() &&
|
!bookmarks.empty() && selectorIndex < itemCount) {
|
||||||
selectorIndex < itemCount) {
|
|
||||||
uiState = UIState::Confirming;
|
uiState = UIState::Confirming;
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
return;
|
return;
|
||||||
@ -128,7 +125,7 @@ void BookmarkListActivity::loop() {
|
|||||||
if (mappedInput.getHeldTime() >= ACTION_MENU_MS) {
|
if (mappedInput.getHeldTime() >= ACTION_MENU_MS) {
|
||||||
return; // Was a long press
|
return; // Was a long press
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!bookmarks.empty() && selectorIndex < itemCount) {
|
if (!bookmarks.empty() && selectorIndex < itemCount) {
|
||||||
const auto& bm = bookmarks[selectorIndex];
|
const auto& bm = bookmarks[selectorIndex];
|
||||||
onSelectBookmark(bm.spineIndex, bm.contentOffset);
|
onSelectBookmark(bm.spineIndex, bm.contentOffset);
|
||||||
@ -190,12 +187,14 @@ void BookmarkListActivity::render() const {
|
|||||||
const int RIGHT_MARGIN = BASE_RIGHT_MARGIN + bezelRight;
|
const int RIGHT_MARGIN = BASE_RIGHT_MARGIN + bezelRight;
|
||||||
|
|
||||||
// Draw title
|
// Draw title
|
||||||
auto truncatedTitle = renderer.truncatedText(UI_12_FONT_ID, bookTitle.c_str(), pageWidth - LEFT_MARGIN - RIGHT_MARGIN);
|
auto truncatedTitle =
|
||||||
renderer.drawText(UI_12_FONT_ID, LEFT_MARGIN, BASE_TAB_BAR_Y + bezelTop, truncatedTitle.c_str(), true, EpdFontFamily::BOLD);
|
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) {
|
if (itemCount == 0) {
|
||||||
renderer.drawText(UI_10_FONT_ID, LEFT_MARGIN, CONTENT_START_Y, "No bookmarks");
|
renderer.drawText(UI_10_FONT_ID, LEFT_MARGIN, CONTENT_START_Y, "No bookmarks");
|
||||||
|
|
||||||
const auto labels = mappedInput.mapLabels("\xc2\xab Back", "", "", "");
|
const auto labels = mappedInput.mapLabels("\xc2\xab Back", "", "", "");
|
||||||
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||||
renderer.displayBuffer();
|
renderer.displayBuffer();
|
||||||
|
|||||||
@ -50,10 +50,10 @@ class BookmarkListActivity final : public Activity {
|
|||||||
void renderConfirmation() const;
|
void renderConfirmation() const;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit BookmarkListActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
|
explicit BookmarkListActivity(
|
||||||
const std::string& bookPath, const std::string& bookTitle,
|
GfxRenderer& renderer, MappedInputManager& mappedInput, const std::string& bookPath, const std::string& bookTitle,
|
||||||
const std::function<void()>& onGoBack,
|
const std::function<void()>& onGoBack,
|
||||||
const std::function<void(uint16_t spineIndex, uint32_t contentOffset)>& onSelectBookmark)
|
const std::function<void(uint16_t spineIndex, uint32_t contentOffset)>& onSelectBookmark)
|
||||||
: Activity("BookmarkList", renderer, mappedInput),
|
: Activity("BookmarkList", renderer, mappedInput),
|
||||||
bookPath(bookPath),
|
bookPath(bookPath),
|
||||||
bookTitle(bookTitle),
|
bookTitle(bookTitle),
|
||||||
|
|||||||
@ -157,7 +157,7 @@ bool HomeActivity::storeCoverBuffer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const size_t bufferSize = GfxRenderer::getBufferSize();
|
const size_t bufferSize = GfxRenderer::getBufferSize();
|
||||||
|
|
||||||
// Reuse existing buffer if already allocated (avoids fragmentation from free+malloc)
|
// Reuse existing buffer if already allocated (avoids fragmentation from free+malloc)
|
||||||
if (!coverBuffer) {
|
if (!coverBuffer) {
|
||||||
coverBuffer = static_cast<uint8_t*>(malloc(bufferSize));
|
coverBuffer = static_cast<uint8_t*>(malloc(bufferSize));
|
||||||
@ -270,7 +270,7 @@ bool HomeActivity::preloadCoverBuffer() {
|
|||||||
cachedCoverPath = thumbPath;
|
cachedCoverPath = thumbPath;
|
||||||
coverBufferStored = false; // Will be set true after actual render in HomeActivity
|
coverBufferStored = false; // Will be set true after actual render in HomeActivity
|
||||||
coverRendered = false; // Will trigger load from disk in render()
|
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());
|
Serial.printf("[%lu] [HOME] [MEM] Cover buffer pre-allocated for: %s\n", millis(), thumbPath.c_str());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -374,8 +374,7 @@ void HomeActivity::render() {
|
|||||||
constexpr int menuSpacing = 8;
|
constexpr int menuSpacing = 8;
|
||||||
const int halfTileWidth = (menuTileWidth - menuSpacing) / 2; // Account for spacing between halves
|
const int halfTileWidth = (menuTileWidth - menuSpacing) / 2; // Account for spacing between halves
|
||||||
// 1 row for split buttons + full-width rows
|
// 1 row for split buttons + full-width rows
|
||||||
const int totalMenuHeight =
|
const int totalMenuHeight = menuTileHeight + static_cast<int>(fullWidthItems.size()) * (menuTileHeight + menuSpacing);
|
||||||
menuTileHeight + static_cast<int>(fullWidthItems.size()) * (menuTileHeight + menuSpacing);
|
|
||||||
|
|
||||||
// Anchor menu to bottom of screen
|
// Anchor menu to bottom of screen
|
||||||
const int menuStartY = pageHeight - bottomMargin - totalMenuHeight;
|
const int menuStartY = pageHeight - bottomMargin - totalMenuHeight;
|
||||||
@ -581,8 +580,7 @@ void HomeActivity::render() {
|
|||||||
// Still have words left, so add ellipsis to last line
|
// Still have words left, so add ellipsis to last line
|
||||||
lines.back().append("...");
|
lines.back().append("...");
|
||||||
|
|
||||||
while (!lines.back().empty() &&
|
while (!lines.back().empty() && renderer.getTextWidth(UI_12_FONT_ID, lines.back().c_str()) > maxLineWidth) {
|
||||||
renderer.getTextWidth(UI_12_FONT_ID, lines.back().c_str()) > maxLineWidth) {
|
|
||||||
// Remove "..." first, then remove one UTF-8 char, then add "..." back
|
// Remove "..." first, then remove one UTF-8 char, then add "..." back
|
||||||
lines.back().resize(lines.back().size() - 3); // Remove "..."
|
lines.back().resize(lines.back().size() - 3); // Remove "..."
|
||||||
StringUtils::utf8RemoveLastChar(lines.back());
|
StringUtils::utf8RemoveLastChar(lines.back());
|
||||||
@ -690,7 +688,8 @@ void HomeActivity::render() {
|
|||||||
// Truncate lists label if needed
|
// Truncate lists label if needed
|
||||||
std::string truncatedLabel = listsLabel;
|
std::string truncatedLabel = listsLabel;
|
||||||
const int maxLabelWidth = halfTileWidth - 16; // Padding
|
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.resize(truncatedLabel.length() - 4);
|
||||||
truncatedLabel += "...";
|
truncatedLabel += "...";
|
||||||
}
|
}
|
||||||
@ -750,7 +749,7 @@ void HomeActivity::render() {
|
|||||||
// Draw battery in bottom-left where the back button hint would normally be
|
// Draw battery in bottom-left where the back button hint would normally be
|
||||||
const bool showBatteryPercentage =
|
const bool showBatteryPercentage =
|
||||||
SETTINGS.hideBatteryPercentage != CrossPointSettings::HIDE_BATTERY_PERCENTAGE::HIDE_ALWAYS;
|
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
|
const int batteryY = pageHeight - 34; // Vertically centered in button hint area
|
||||||
ScreenComponents::drawBatteryLarge(renderer, batteryX, batteryY, showBatteryPercentage);
|
ScreenComponents::drawBatteryLarge(renderer, batteryX, batteryY, showBatteryPercentage);
|
||||||
|
|
||||||
|
|||||||
@ -17,9 +17,9 @@ class HomeActivity final : public Activity {
|
|||||||
bool hasCoverImage = false;
|
bool hasCoverImage = false;
|
||||||
|
|
||||||
// Static cover buffer - persists across activity changes to avoid reloading from SD
|
// Static cover buffer - persists across activity changes to avoid reloading from SD
|
||||||
static bool coverRendered; // Track if cover has been rendered once
|
static bool coverRendered; // Track if cover has been rendered once
|
||||||
static bool coverBufferStored; // Track if cover buffer is stored
|
static bool coverBufferStored; // Track if cover buffer is stored
|
||||||
static uint8_t* coverBuffer; // HomeActivity's own buffer for cover image
|
static uint8_t* coverBuffer; // HomeActivity's own buffer for cover image
|
||||||
static std::string cachedCoverPath; // Path of the cached cover (to detect book changes)
|
static std::string cachedCoverPath; // Path of the cached cover (to detect book changes)
|
||||||
|
|
||||||
std::string lastBookTitle;
|
std::string lastBookTitle;
|
||||||
@ -43,15 +43,14 @@ class HomeActivity final : public Activity {
|
|||||||
public:
|
public:
|
||||||
// Free cover buffer from external activities (e.g., when entering reader to reclaim memory)
|
// Free cover buffer from external activities (e.g., when entering reader to reclaim memory)
|
||||||
static void freeCoverBufferIfAllocated();
|
static void freeCoverBufferIfAllocated();
|
||||||
|
|
||||||
// Preload cover buffer from external activities (e.g., MyLibraryActivity) for instant Home screen
|
// Preload cover buffer from external activities (e.g., MyLibraryActivity) for instant Home screen
|
||||||
// Returns true if cover was successfully preloaded or already cached
|
// Returns true if cover was successfully preloaded or already cached
|
||||||
static bool preloadCoverBuffer();
|
static bool preloadCoverBuffer();
|
||||||
explicit HomeActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
|
explicit HomeActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
|
||||||
const std::function<void()>& onContinueReading, const std::function<void()>& onListsOpen,
|
const std::function<void()>& onContinueReading, const std::function<void()>& onListsOpen,
|
||||||
const std::function<void()>& onMyLibraryOpen, const std::function<void()>& onSettingsOpen,
|
const std::function<void()>& onMyLibraryOpen, const std::function<void()>& onSettingsOpen,
|
||||||
const std::function<void()>& onFileTransferOpen,
|
const std::function<void()>& onFileTransferOpen, const std::function<void()>& onOpdsBrowserOpen)
|
||||||
const std::function<void()>& onOpdsBrowserOpen)
|
|
||||||
: Activity("Home", renderer, mappedInput),
|
: Activity("Home", renderer, mappedInput),
|
||||||
onContinueReading(onContinueReading),
|
onContinueReading(onContinueReading),
|
||||||
onListsOpen(onListsOpen),
|
onListsOpen(onListsOpen),
|
||||||
|
|||||||
@ -202,8 +202,8 @@ void ListViewActivity::render() const {
|
|||||||
const auto pageStartIndex = selectorIndex / pageItems * pageItems;
|
const auto pageStartIndex = selectorIndex / pageItems * pageItems;
|
||||||
|
|
||||||
// Draw selection highlight
|
// Draw selection highlight
|
||||||
renderer.fillRect(bezelLeft, CONTENT_START_Y + (selectorIndex % pageItems) * LINE_HEIGHT - 2, pageWidth - RIGHT_MARGIN - bezelLeft,
|
renderer.fillRect(bezelLeft, CONTENT_START_Y + (selectorIndex % pageItems) * LINE_HEIGHT - 2,
|
||||||
LINE_HEIGHT);
|
pageWidth - RIGHT_MARGIN - bezelLeft, LINE_HEIGHT);
|
||||||
|
|
||||||
// Calculate available text width
|
// Calculate available text width
|
||||||
const int textMaxWidth = pageWidth - LEFT_MARGIN - RIGHT_MARGIN - MICRO_THUMB_WIDTH - 10;
|
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)
|
// Extract tags for badges (only if we'll show them - when NOT selected)
|
||||||
constexpr int badgeSpacing = 4; // Gap between badges
|
constexpr int badgeSpacing = 4; // Gap between badges
|
||||||
constexpr int badgePadding = 10; // Horizontal padding inside badge (5 each side)
|
constexpr int badgePadding = 10; // Horizontal padding inside badge (5 each side)
|
||||||
constexpr int badgeToThumbGap = 8; // Gap between rightmost badge and cover art
|
constexpr int badgeToThumbGap = 8; // Gap between rightmost badge and cover art
|
||||||
int totalBadgeWidth = 0;
|
int totalBadgeWidth = 0;
|
||||||
BookTags tags;
|
BookTags tags;
|
||||||
@ -302,8 +302,8 @@ void ListViewActivity::render() const {
|
|||||||
const int badgeY = y + 2 + (titleLineHeight - badgeHeight) / 2;
|
const int badgeY = y + 2 + (titleLineHeight - badgeHeight) / 2;
|
||||||
|
|
||||||
if (!tags.extensionTag.empty()) {
|
if (!tags.extensionTag.empty()) {
|
||||||
int badgeWidth = ScreenComponents::drawPillBadge(renderer, badgeX, badgeY, tags.extensionTag.c_str(),
|
int badgeWidth =
|
||||||
SMALL_FONT_ID, false);
|
ScreenComponents::drawPillBadge(renderer, badgeX, badgeY, tags.extensionTag.c_str(), SMALL_FONT_ID, false);
|
||||||
badgeX += badgeWidth + badgeSpacing;
|
badgeX += badgeWidth + badgeSpacing;
|
||||||
}
|
}
|
||||||
if (!tags.suffixTag.empty()) {
|
if (!tags.suffixTag.empty()) {
|
||||||
|
|||||||
@ -85,22 +85,23 @@ int MyLibraryActivity::getPageItems() const {
|
|||||||
const int bottomBarHeight = 60; // Space for button hints
|
const int bottomBarHeight = 60; // Space for button hints
|
||||||
const int bezelTop = renderer.getBezelOffsetTop();
|
const int bezelTop = renderer.getBezelOffsetTop();
|
||||||
const int bezelBottom = renderer.getBezelOffsetBottom();
|
const int bezelBottom = renderer.getBezelOffsetBottom();
|
||||||
|
|
||||||
// Search tab has compact layout: character picker (~30px) + query (~25px) + results
|
// Search tab has compact layout: character picker (~30px) + query (~25px) + results
|
||||||
if (currentTab == Tab::Search) {
|
if (currentTab == Tab::Search) {
|
||||||
// Character picker: ~30px, Query: ~25px = 55px overhead
|
// Character picker: ~30px, Query: ~25px = 55px overhead
|
||||||
// Much more room for results than the old 5-row keyboard
|
// Much more room for results than the old 5-row keyboard
|
||||||
constexpr int SEARCH_OVERHEAD = 55;
|
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;
|
int items = availableHeight / RECENTS_LINE_HEIGHT;
|
||||||
if (items < 1) items = 1;
|
if (items < 1) items = 1;
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
const int availableHeight = screenHeight - (BASE_CONTENT_START_Y + bezelTop) - bottomBarHeight - bezelBottom;
|
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
|
// 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)
|
const int lineHeight =
|
||||||
? RECENTS_LINE_HEIGHT : LINE_HEIGHT;
|
(currentTab == Tab::Recent || currentTab == Tab::Bookmarks) ? RECENTS_LINE_HEIGHT : LINE_HEIGHT;
|
||||||
int items = availableHeight / lineHeight;
|
int items = availableHeight / lineHeight;
|
||||||
if (items < 1) {
|
if (items < 1) {
|
||||||
items = 1;
|
items = 1;
|
||||||
@ -152,7 +153,7 @@ void MyLibraryActivity::loadLists() { lists = BookListStore::listAllLists(); }
|
|||||||
|
|
||||||
void MyLibraryActivity::loadBookmarkedBooks() {
|
void MyLibraryActivity::loadBookmarkedBooks() {
|
||||||
bookmarkedBooks = BookmarkStore::getBooksWithBookmarks();
|
bookmarkedBooks = BookmarkStore::getBooksWithBookmarks();
|
||||||
|
|
||||||
// Try to get better metadata from recent books
|
// Try to get better metadata from recent books
|
||||||
for (auto& book : bookmarkedBooks) {
|
for (auto& book : bookmarkedBooks) {
|
||||||
auto it = std::find_if(recentBooks.begin(), recentBooks.end(),
|
auto it = std::find_if(recentBooks.begin(), recentBooks.end(),
|
||||||
@ -167,7 +168,7 @@ void MyLibraryActivity::loadBookmarkedBooks() {
|
|||||||
void MyLibraryActivity::loadAllBooks() {
|
void MyLibraryActivity::loadAllBooks() {
|
||||||
// Build index of all books on SD card for search
|
// Build index of all books on SD card for search
|
||||||
allBooks.clear();
|
allBooks.clear();
|
||||||
|
|
||||||
// Helper lambda to recursively scan directories
|
// Helper lambda to recursively scan directories
|
||||||
std::function<void(const std::string&)> scanDirectory = [&](const std::string& path) {
|
std::function<void(const std::string&)> scanDirectory = [&](const std::string& path) {
|
||||||
auto dir = SdMan.open(path.c_str());
|
auto dir = SdMan.open(path.c_str());
|
||||||
@ -175,37 +176,36 @@ void MyLibraryActivity::loadAllBooks() {
|
|||||||
if (dir) dir.close();
|
if (dir) dir.close();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
dir.rewindDirectory();
|
dir.rewindDirectory();
|
||||||
char name[500];
|
char name[500];
|
||||||
|
|
||||||
for (auto file = dir.openNextFile(); file; file = dir.openNextFile()) {
|
for (auto file = dir.openNextFile(); file; file = dir.openNextFile()) {
|
||||||
file.getName(name, sizeof(name));
|
file.getName(name, sizeof(name));
|
||||||
if (name[0] == '.' || strcmp(name, "System Volume Information") == 0) {
|
if (name[0] == '.' || strcmp(name, "System Volume Information") == 0) {
|
||||||
file.close();
|
file.close();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string fullPath = (path.back() == '/') ? path + name : path + "/" + name;
|
std::string fullPath = (path.back() == '/') ? path + name : path + "/" + name;
|
||||||
|
|
||||||
if (file.isDirectory()) {
|
if (file.isDirectory()) {
|
||||||
file.close();
|
file.close();
|
||||||
scanDirectory(fullPath);
|
scanDirectory(fullPath);
|
||||||
} else {
|
} else {
|
||||||
auto filename = std::string(name);
|
auto filename = std::string(name);
|
||||||
if (StringUtils::checkFileExtension(filename, ".epub") ||
|
if (StringUtils::checkFileExtension(filename, ".epub") || StringUtils::checkFileExtension(filename, ".txt") ||
|
||||||
StringUtils::checkFileExtension(filename, ".txt") ||
|
|
||||||
StringUtils::checkFileExtension(filename, ".md")) {
|
StringUtils::checkFileExtension(filename, ".md")) {
|
||||||
SearchResult result;
|
SearchResult result;
|
||||||
result.path = fullPath;
|
result.path = fullPath;
|
||||||
|
|
||||||
// Extract title from filename (remove extension)
|
// Extract title from filename (remove extension)
|
||||||
result.title = filename;
|
result.title = filename;
|
||||||
const size_t dot = result.title.find_last_of('.');
|
const size_t dot = result.title.find_last_of('.');
|
||||||
if (dot != std::string::npos) {
|
if (dot != std::string::npos) {
|
||||||
result.title.resize(dot);
|
result.title.resize(dot);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to get metadata from recent books if available
|
// Try to get metadata from recent books if available
|
||||||
auto it = std::find_if(recentBooks.begin(), recentBooks.end(),
|
auto it = std::find_if(recentBooks.begin(), recentBooks.end(),
|
||||||
[&fullPath](const RecentBook& recent) { return recent.path == fullPath; });
|
[&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->title.empty()) result.title = it->title;
|
||||||
if (!it->author.empty()) result.author = it->author;
|
if (!it->author.empty()) result.author = it->author;
|
||||||
}
|
}
|
||||||
|
|
||||||
allBooks.push_back(result);
|
allBooks.push_back(result);
|
||||||
}
|
}
|
||||||
file.close();
|
file.close();
|
||||||
@ -221,16 +221,15 @@ void MyLibraryActivity::loadAllBooks() {
|
|||||||
}
|
}
|
||||||
dir.close();
|
dir.close();
|
||||||
};
|
};
|
||||||
|
|
||||||
scanDirectory("/");
|
scanDirectory("/");
|
||||||
|
|
||||||
// Sort alphabetically by title
|
// Sort alphabetically by title
|
||||||
std::sort(allBooks.begin(), allBooks.end(), [](const SearchResult& a, const SearchResult& b) {
|
std::sort(allBooks.begin(), allBooks.end(), [](const SearchResult& a, const SearchResult& b) {
|
||||||
return lexicographical_compare(
|
return lexicographical_compare(a.title.begin(), a.title.end(), b.title.begin(), b.title.end(),
|
||||||
a.title.begin(), a.title.end(), b.title.begin(), b.title.end(),
|
[](char c1, char c2) { return tolower(c1) < tolower(c2); });
|
||||||
[](char c1, char c2) { return tolower(c1) < tolower(c2); });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Build character set after loading books
|
// Build character set after loading books
|
||||||
buildSearchCharacters();
|
buildSearchCharacters();
|
||||||
}
|
}
|
||||||
@ -238,7 +237,7 @@ void MyLibraryActivity::loadAllBooks() {
|
|||||||
void MyLibraryActivity::buildSearchCharacters() {
|
void MyLibraryActivity::buildSearchCharacters() {
|
||||||
// Build a set of unique characters from all book titles and authors
|
// Build a set of unique characters from all book titles and authors
|
||||||
std::set<char> charSet;
|
std::set<char> charSet;
|
||||||
|
|
||||||
for (const auto& book : allBooks) {
|
for (const auto& book : allBooks) {
|
||||||
for (char c : book.title) {
|
for (char c : book.title) {
|
||||||
// Convert to uppercase for display, store as uppercase
|
// 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
|
// Convert set to vector, sorted: A-Z, then 0-9, then symbols
|
||||||
searchCharacters.clear();
|
searchCharacters.clear();
|
||||||
|
|
||||||
// Add letters A-Z
|
// Add letters A-Z
|
||||||
for (char c = 'A'; c <= 'Z'; c++) {
|
for (char c = 'A'; c <= 'Z'; c++) {
|
||||||
if (charSet.count(c)) {
|
if (charSet.count(c)) {
|
||||||
searchCharacters.push_back(c);
|
searchCharacters.push_back(c);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add digits 0-9
|
// Add digits 0-9
|
||||||
for (char c = '0'; c <= '9'; c++) {
|
for (char c = '0'; c <= '9'; c++) {
|
||||||
if (charSet.count(c)) {
|
if (charSet.count(c)) {
|
||||||
searchCharacters.push_back(c);
|
searchCharacters.push_back(c);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add symbols (anything else in the set)
|
// Add symbols (anything else in the set)
|
||||||
std::copy_if(charSet.begin(), charSet.end(), std::back_inserter(searchCharacters), [](char c) {
|
std::copy_if(charSet.begin(), charSet.end(), std::back_inserter(searchCharacters), [](char c) {
|
||||||
return !std::isalpha(static_cast<unsigned char>(c)) && !std::isdigit(static_cast<unsigned char>(c));
|
return !std::isalpha(static_cast<unsigned char>(c)) && !std::isdigit(static_cast<unsigned char>(c));
|
||||||
});
|
});
|
||||||
|
|
||||||
// Reset character index if it's out of bounds
|
// Reset character index if it's out of bounds
|
||||||
if (searchCharIndex >= static_cast<int>(searchCharacters.size()) + 3) { // +3 for special keys
|
if (searchCharIndex >= static_cast<int>(searchCharacters.size()) + 3) { // +3 for special keys
|
||||||
searchCharIndex = 0;
|
searchCharIndex = 0;
|
||||||
@ -293,17 +292,17 @@ void MyLibraryActivity::buildSearchCharacters() {
|
|||||||
|
|
||||||
void MyLibraryActivity::updateSearchResults() {
|
void MyLibraryActivity::updateSearchResults() {
|
||||||
searchResults.clear();
|
searchResults.clear();
|
||||||
|
|
||||||
if (searchQuery.empty()) {
|
if (searchQuery.empty()) {
|
||||||
// Don't show any results when query is empty - user needs to type something
|
// Don't show any results when query is empty - user needs to type something
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert query to lowercase for case-insensitive matching
|
// Convert query to lowercase for case-insensitive matching
|
||||||
std::string queryLower = searchQuery;
|
std::string queryLower = searchQuery;
|
||||||
std::transform(queryLower.begin(), queryLower.end(), queryLower.begin(),
|
std::transform(queryLower.begin(), queryLower.end(), queryLower.begin(),
|
||||||
[](unsigned char c) { return std::tolower(c); });
|
[](unsigned char c) { return std::tolower(c); });
|
||||||
|
|
||||||
for (const auto& book : allBooks) {
|
for (const auto& book : allBooks) {
|
||||||
// Convert title, author, and path to lowercase
|
// Convert title, author, and path to lowercase
|
||||||
std::string titleLower = book.title;
|
std::string titleLower = book.title;
|
||||||
@ -315,9 +314,9 @@ void MyLibraryActivity::updateSearchResults() {
|
|||||||
[](unsigned char c) { return std::tolower(c); });
|
[](unsigned char c) { return std::tolower(c); });
|
||||||
std::transform(pathLower.begin(), pathLower.end(), pathLower.begin(),
|
std::transform(pathLower.begin(), pathLower.end(), pathLower.begin(),
|
||||||
[](unsigned char c) { return std::tolower(c); });
|
[](unsigned char c) { return std::tolower(c); });
|
||||||
|
|
||||||
int score = 0;
|
int score = 0;
|
||||||
|
|
||||||
// Check for matches
|
// Check for matches
|
||||||
if (titleLower.find(queryLower) != std::string::npos) {
|
if (titleLower.find(queryLower) != std::string::npos) {
|
||||||
score += 100;
|
score += 100;
|
||||||
@ -331,19 +330,17 @@ void MyLibraryActivity::updateSearchResults() {
|
|||||||
if (pathLower.find(queryLower) != std::string::npos) {
|
if (pathLower.find(queryLower) != std::string::npos) {
|
||||||
score += 30;
|
score += 30;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (score > 0) {
|
if (score > 0) {
|
||||||
SearchResult result = book;
|
SearchResult result = book;
|
||||||
result.matchScore = score;
|
result.matchScore = score;
|
||||||
searchResults.push_back(result);
|
searchResults.push_back(result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort by match score (descending)
|
// Sort by match score (descending)
|
||||||
std::sort(searchResults.begin(), searchResults.end(),
|
std::sort(searchResults.begin(), searchResults.end(),
|
||||||
[](const SearchResult& a, const SearchResult& b) {
|
[](const SearchResult& a, const SearchResult& b) { return a.matchScore > b.matchScore; });
|
||||||
return a.matchScore > b.matchScore;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void MyLibraryActivity::loadFiles() {
|
void MyLibraryActivity::loadFiles() {
|
||||||
@ -407,14 +404,14 @@ void MyLibraryActivity::onEnter() {
|
|||||||
loadFiles();
|
loadFiles();
|
||||||
|
|
||||||
selectorIndex = 0;
|
selectorIndex = 0;
|
||||||
|
|
||||||
// If entering Search tab, start in character picker mode
|
// If entering Search tab, start in character picker mode
|
||||||
if (currentTab == Tab::Search) {
|
if (currentTab == Tab::Search) {
|
||||||
searchInResults = false;
|
searchInResults = false;
|
||||||
inTabBar = false;
|
inTabBar = false;
|
||||||
searchCharIndex = 0;
|
searchCharIndex = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
|
|
||||||
xTaskCreate(&MyLibraryActivity::taskTrampoline, "MyLibraryActivityTask",
|
xTaskCreate(&MyLibraryActivity::taskTrampoline, "MyLibraryActivityTask",
|
||||||
@ -491,7 +488,7 @@ void MyLibraryActivity::openActionMenu() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
uiState = UIState::ActionMenu;
|
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
|
ignoreNextConfirmRelease = true; // Ignore the release from the long-press that opened this menu
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
}
|
}
|
||||||
@ -766,7 +763,7 @@ void MyLibraryActivity::loop() {
|
|||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mappedInput.wasReleased(MappedInputManager::Button::Right)) {
|
if (mappedInput.wasReleased(MappedInputManager::Button::Right)) {
|
||||||
switch (currentTab) {
|
switch (currentTab) {
|
||||||
case Tab::Recent:
|
case Tab::Recent:
|
||||||
@ -788,7 +785,7 @@ void MyLibraryActivity::loop() {
|
|||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Down exits tab bar, enters list at top
|
// Down exits tab bar, enters list at top
|
||||||
if (mappedInput.wasReleased(MappedInputManager::Button::Down)) {
|
if (mappedInput.wasReleased(MappedInputManager::Button::Down)) {
|
||||||
inTabBar = false;
|
inTabBar = false;
|
||||||
@ -796,7 +793,7 @@ void MyLibraryActivity::loop() {
|
|||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Up exits tab bar, jumps to bottom of list
|
// Up exits tab bar, jumps to bottom of list
|
||||||
if (mappedInput.wasReleased(MappedInputManager::Button::Up)) {
|
if (mappedInput.wasReleased(MappedInputManager::Button::Up)) {
|
||||||
inTabBar = false;
|
inTabBar = false;
|
||||||
@ -806,13 +803,13 @@ void MyLibraryActivity::loop() {
|
|||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Back goes home
|
// Back goes home
|
||||||
if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
||||||
onGoHome();
|
onGoHome();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -820,7 +817,7 @@ void MyLibraryActivity::loop() {
|
|||||||
if (currentTab == Tab::Search) {
|
if (currentTab == Tab::Search) {
|
||||||
const int charCount = static_cast<int>(searchCharacters.size());
|
const int charCount = static_cast<int>(searchCharacters.size());
|
||||||
const int totalPickerItems = charCount + 3; // +3 for SPC, <-, CLR
|
const int totalPickerItems = charCount + 3; // +3 for SPC, <-, CLR
|
||||||
|
|
||||||
if (inTabBar) {
|
if (inTabBar) {
|
||||||
// In tab bar mode - Left/Right switch tabs, Down goes to picker
|
// In tab bar mode - Left/Right switch tabs, Down goes to picker
|
||||||
// Use wasReleased for consistency with other tab switching code
|
// Use wasReleased for consistency with other tab switching code
|
||||||
@ -830,21 +827,21 @@ void MyLibraryActivity::loop() {
|
|||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mappedInput.wasReleased(MappedInputManager::Button::Right)) {
|
if (mappedInput.wasReleased(MappedInputManager::Button::Right)) {
|
||||||
currentTab = Tab::Files;
|
currentTab = Tab::Files;
|
||||||
selectorIndex = 0;
|
selectorIndex = 0;
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Down exits tab bar, goes to character picker
|
// Down exits tab bar, goes to character picker
|
||||||
if (mappedInput.wasReleased(MappedInputManager::Button::Down)) {
|
if (mappedInput.wasReleased(MappedInputManager::Button::Down)) {
|
||||||
inTabBar = false;
|
inTabBar = false;
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Up exits tab bar, jumps to bottom of results (if any)
|
// Up exits tab bar, jumps to bottom of results (if any)
|
||||||
if (mappedInput.wasReleased(MappedInputManager::Button::Up)) {
|
if (mappedInput.wasReleased(MappedInputManager::Button::Up)) {
|
||||||
inTabBar = false;
|
inTabBar = false;
|
||||||
@ -855,33 +852,31 @@ void MyLibraryActivity::loop() {
|
|||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Back goes home
|
// Back goes home
|
||||||
if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
||||||
onGoHome();
|
onGoHome();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
} else if (!searchInResults) {
|
} else if (!searchInResults) {
|
||||||
// In character picker mode
|
// In character picker mode
|
||||||
|
|
||||||
// Long press Left = jump to start
|
// Long press Left = jump to start
|
||||||
if (mappedInput.isPressed(MappedInputManager::Button::Left) &&
|
if (mappedInput.isPressed(MappedInputManager::Button::Left) && mappedInput.getHeldTime() >= 700) {
|
||||||
mappedInput.getHeldTime() >= 700) {
|
|
||||||
searchCharIndex = 0;
|
searchCharIndex = 0;
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Long press Right = jump to end
|
// Long press Right = jump to end
|
||||||
if (mappedInput.isPressed(MappedInputManager::Button::Right) &&
|
if (mappedInput.isPressed(MappedInputManager::Button::Right) && mappedInput.getHeldTime() >= 700) {
|
||||||
mappedInput.getHeldTime() >= 700) {
|
|
||||||
searchCharIndex = totalPickerItems - 1;
|
searchCharIndex = totalPickerItems - 1;
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Left/Right navigate through characters (with wrap)
|
// Left/Right navigate through characters (with wrap)
|
||||||
if (mappedInput.wasPressed(MappedInputManager::Button::Left)) {
|
if (mappedInput.wasPressed(MappedInputManager::Button::Left)) {
|
||||||
if (searchCharIndex > 0) {
|
if (searchCharIndex > 0) {
|
||||||
@ -892,7 +887,7 @@ void MyLibraryActivity::loop() {
|
|||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mappedInput.wasPressed(MappedInputManager::Button::Right)) {
|
if (mappedInput.wasPressed(MappedInputManager::Button::Right)) {
|
||||||
if (searchCharIndex < totalPickerItems - 1) {
|
if (searchCharIndex < totalPickerItems - 1) {
|
||||||
searchCharIndex++;
|
searchCharIndex++;
|
||||||
@ -902,7 +897,7 @@ void MyLibraryActivity::loop() {
|
|||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Down moves to results (if any exist)
|
// Down moves to results (if any exist)
|
||||||
if (mappedInput.wasPressed(MappedInputManager::Button::Down)) {
|
if (mappedInput.wasPressed(MappedInputManager::Button::Down)) {
|
||||||
if (!searchResults.empty()) {
|
if (!searchResults.empty()) {
|
||||||
@ -912,14 +907,14 @@ void MyLibraryActivity::loop() {
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Up moves to tab bar
|
// Up moves to tab bar
|
||||||
if (mappedInput.wasReleased(MappedInputManager::Button::Up)) {
|
if (mappedInput.wasReleased(MappedInputManager::Button::Up)) {
|
||||||
inTabBar = true;
|
inTabBar = true;
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Confirm adds selected character or performs special action
|
// Confirm adds selected character or performs special action
|
||||||
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||||
if (searchCharIndex < charCount) {
|
if (searchCharIndex < charCount) {
|
||||||
@ -944,10 +939,9 @@ void MyLibraryActivity::loop() {
|
|||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Long press Back = clear entire query
|
// Long press Back = clear entire query
|
||||||
if (mappedInput.isPressed(MappedInputManager::Button::Back) &&
|
if (mappedInput.isPressed(MappedInputManager::Button::Back) && mappedInput.getHeldTime() >= 700) {
|
||||||
mappedInput.getHeldTime() >= 700) {
|
|
||||||
if (!searchQuery.empty()) {
|
if (!searchQuery.empty()) {
|
||||||
searchQuery.clear();
|
searchQuery.clear();
|
||||||
updateSearchResults();
|
updateSearchResults();
|
||||||
@ -955,7 +949,7 @@ void MyLibraryActivity::loop() {
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Short press Back = backspace (delete one char)
|
// Short press Back = backspace (delete one char)
|
||||||
if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
||||||
if (mappedInput.getHeldTime() >= 700) {
|
if (mappedInput.getHeldTime() >= 700) {
|
||||||
@ -972,29 +966,27 @@ void MyLibraryActivity::loop() {
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return; // Don't process other input while in picker
|
return; // Don't process other input while in picker
|
||||||
} else {
|
} else {
|
||||||
// In results mode
|
// In results mode
|
||||||
|
|
||||||
// Long press PageBack (side button) = jump to first result
|
// Long press PageBack (side button) = jump to first result
|
||||||
if (mappedInput.isPressed(MappedInputManager::Button::PageBack) &&
|
if (mappedInput.isPressed(MappedInputManager::Button::PageBack) && mappedInput.getHeldTime() >= 700) {
|
||||||
mappedInput.getHeldTime() >= 700) {
|
|
||||||
selectorIndex = 0;
|
selectorIndex = 0;
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Long press PageForward (side button) = jump to last result
|
// Long press PageForward (side button) = jump to last result
|
||||||
if (mappedInput.isPressed(MappedInputManager::Button::PageForward) &&
|
if (mappedInput.isPressed(MappedInputManager::Button::PageForward) && mappedInput.getHeldTime() >= 700) {
|
||||||
mappedInput.getHeldTime() >= 700) {
|
|
||||||
if (!searchResults.empty()) {
|
if (!searchResults.empty()) {
|
||||||
selectorIndex = static_cast<int>(searchResults.size()) - 1;
|
selectorIndex = static_cast<int>(searchResults.size()) - 1;
|
||||||
}
|
}
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Up/Down navigate through results
|
// Up/Down navigate through results
|
||||||
if (mappedInput.wasPressed(MappedInputManager::Button::Up)) {
|
if (mappedInput.wasPressed(MappedInputManager::Button::Up)) {
|
||||||
if (selectorIndex > 0) {
|
if (selectorIndex > 0) {
|
||||||
@ -1006,7 +998,7 @@ void MyLibraryActivity::loop() {
|
|||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mappedInput.wasPressed(MappedInputManager::Button::Down)) {
|
if (mappedInput.wasPressed(MappedInputManager::Button::Down)) {
|
||||||
if (selectorIndex < static_cast<int>(searchResults.size()) - 1) {
|
if (selectorIndex < static_cast<int>(searchResults.size()) - 1) {
|
||||||
selectorIndex++;
|
selectorIndex++;
|
||||||
@ -1017,13 +1009,13 @@ void MyLibraryActivity::loop() {
|
|||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Left/Right do nothing in results (or could page?)
|
// Left/Right do nothing in results (or could page?)
|
||||||
if (mappedInput.wasPressed(MappedInputManager::Button::Left) ||
|
if (mappedInput.wasPressed(MappedInputManager::Button::Left) ||
|
||||||
mappedInput.wasPressed(MappedInputManager::Button::Right)) {
|
mappedInput.wasPressed(MappedInputManager::Button::Right)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Confirm opens the selected book
|
// Confirm opens the selected book
|
||||||
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||||
if (!searchResults.empty() && selectorIndex < static_cast<int>(searchResults.size())) {
|
if (!searchResults.empty() && selectorIndex < static_cast<int>(searchResults.size())) {
|
||||||
@ -1031,14 +1023,14 @@ void MyLibraryActivity::loop() {
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Back button - go back to character picker
|
// Back button - go back to character picker
|
||||||
if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
||||||
searchInResults = false;
|
searchInResults = false;
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return; // Don't process other input
|
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)
|
// Long press Confirm to open action menu (only for files, not directories)
|
||||||
if (mappedInput.isPressed(MappedInputManager::Button::Confirm) &&
|
if (mappedInput.isPressed(MappedInputManager::Button::Confirm) && mappedInput.getHeldTime() >= ACTION_MENU_MS &&
|
||||||
mappedInput.getHeldTime() >= ACTION_MENU_MS && isSelectedItemAFile()) {
|
isSelectedItemAFile()) {
|
||||||
openActionMenu();
|
openActionMenu();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -1096,7 +1088,7 @@ void MyLibraryActivity::loop() {
|
|||||||
} else if (currentTab == Tab::Files && selectorIndex == static_cast<int>(files.size())) {
|
} else if (currentTab == Tab::Files && selectorIndex == static_cast<int>(files.size())) {
|
||||||
isSearchShortcut = true;
|
isSearchShortcut = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isSearchShortcut) {
|
if (isSearchShortcut) {
|
||||||
// Switch to Search tab with character picker active
|
// Switch to Search tab with character picker active
|
||||||
currentTab = Tab::Search;
|
currentTab = Tab::Search;
|
||||||
@ -1107,7 +1099,7 @@ void MyLibraryActivity::loop() {
|
|||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentTab == Tab::Recent) {
|
if (currentTab == Tab::Recent) {
|
||||||
if (!recentBooks.empty() && selectorIndex < static_cast<int>(recentBooks.size())) {
|
if (!recentBooks.empty() && selectorIndex < static_cast<int>(recentBooks.size())) {
|
||||||
onSelectBook(recentBooks[selectorIndex].path, currentTab);
|
onSelectBook(recentBooks[selectorIndex].path, currentTab);
|
||||||
@ -1258,14 +1250,14 @@ void MyLibraryActivity::loop() {
|
|||||||
|
|
||||||
void MyLibraryActivity::displayTaskLoop() {
|
void MyLibraryActivity::displayTaskLoop() {
|
||||||
bool coverPreloaded = false;
|
bool coverPreloaded = false;
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
if (updateRequired) {
|
if (updateRequired) {
|
||||||
updateRequired = false;
|
updateRequired = false;
|
||||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||||
render();
|
render();
|
||||||
xSemaphoreGive(renderingMutex);
|
xSemaphoreGive(renderingMutex);
|
||||||
|
|
||||||
// After first render, pre-allocate cover buffer for Home screen
|
// After first render, pre-allocate cover buffer for Home screen
|
||||||
// This happens in background so Home screen loads faster when user navigates there
|
// This happens in background so Home screen loads faster when user navigates there
|
||||||
if (!coverPreloaded) {
|
if (!coverPreloaded) {
|
||||||
@ -1487,8 +1479,8 @@ void MyLibraryActivity::renderRecentTab() const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Extract tags for badges (only if we'll show them - when NOT selected)
|
// Extract tags for badges (only if we'll show them - when NOT selected)
|
||||||
constexpr int badgeSpacing = 4; // Gap between badges
|
constexpr int badgeSpacing = 4; // Gap between badges
|
||||||
constexpr int badgePadding = 10; // Horizontal padding inside badge (5 each side)
|
constexpr int badgePadding = 10; // Horizontal padding inside badge (5 each side)
|
||||||
constexpr int badgeToThumbGap = 8; // Gap between rightmost badge and cover art
|
constexpr int badgeToThumbGap = 8; // Gap between rightmost badge and cover art
|
||||||
int totalBadgeWidth = 0;
|
int totalBadgeWidth = 0;
|
||||||
BookTags tags;
|
BookTags tags;
|
||||||
@ -1527,8 +1519,8 @@ void MyLibraryActivity::renderRecentTab() const {
|
|||||||
const int badgeY = y + 2 + (titleLineHeight - badgeHeight) / 2;
|
const int badgeY = y + 2 + (titleLineHeight - badgeHeight) / 2;
|
||||||
|
|
||||||
if (!tags.extensionTag.empty()) {
|
if (!tags.extensionTag.empty()) {
|
||||||
int badgeWidth = ScreenComponents::drawPillBadge(renderer, badgeX, badgeY, tags.extensionTag.c_str(),
|
int badgeWidth =
|
||||||
SMALL_FONT_ID, false);
|
ScreenComponents::drawPillBadge(renderer, badgeX, badgeY, tags.extensionTag.c_str(), SMALL_FONT_ID, false);
|
||||||
badgeX += badgeWidth + badgeSpacing;
|
badgeX += badgeWidth + badgeSpacing;
|
||||||
}
|
}
|
||||||
if (!tags.suffixTag.empty()) {
|
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);
|
renderer.drawText(UI_10_FONT_ID, LEFT_MARGIN, y + 32, truncatedAuthor.c_str(), !isSelected);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw "Search..." shortcut if it's on the current page
|
// Draw "Search..." shortcut if it's on the current page
|
||||||
const int searchIndex = bookCount; // Last item
|
const int searchIndex = bookCount; // Last item
|
||||||
if (searchIndex >= pageStartIndex && searchIndex < pageStartIndex + pageItems) {
|
if (searchIndex >= pageStartIndex && searchIndex < pageStartIndex + pageItems) {
|
||||||
@ -1581,8 +1573,8 @@ void MyLibraryActivity::renderListsTab() const {
|
|||||||
const auto pageStartIndex = selectorIndex / pageItems * pageItems;
|
const auto pageStartIndex = selectorIndex / pageItems * pageItems;
|
||||||
|
|
||||||
// Draw selection highlight
|
// Draw selection highlight
|
||||||
renderer.fillRect(bezelLeft, CONTENT_START_Y + (selectorIndex % pageItems) * LINE_HEIGHT - 2, pageWidth - RIGHT_MARGIN - bezelLeft,
|
renderer.fillRect(bezelLeft, CONTENT_START_Y + (selectorIndex % pageItems) * LINE_HEIGHT - 2,
|
||||||
LINE_HEIGHT);
|
pageWidth - RIGHT_MARGIN - bezelLeft, LINE_HEIGHT);
|
||||||
|
|
||||||
// Draw items
|
// Draw items
|
||||||
for (int i = pageStartIndex; i < listCount && i < pageStartIndex + pageItems; i++) {
|
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(),
|
renderer.drawText(UI_10_FONT_ID, LEFT_MARGIN, CONTENT_START_Y + (i % pageItems) * LINE_HEIGHT, item.c_str(),
|
||||||
i != selectorIndex);
|
i != selectorIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw "Search..." shortcut if it's on the current page
|
// Draw "Search..." shortcut if it's on the current page
|
||||||
const int searchIndex = listCount; // Last item
|
const int searchIndex = listCount; // Last item
|
||||||
if (searchIndex >= pageStartIndex && searchIndex < pageStartIndex + pageItems) {
|
if (searchIndex >= pageStartIndex && searchIndex < pageStartIndex + pageItems) {
|
||||||
@ -1632,8 +1624,8 @@ void MyLibraryActivity::renderFilesTab() const {
|
|||||||
const auto pageStartIndex = selectorIndex / pageItems * pageItems;
|
const auto pageStartIndex = selectorIndex / pageItems * pageItems;
|
||||||
|
|
||||||
// Draw selection highlight
|
// Draw selection highlight
|
||||||
renderer.fillRect(bezelLeft, CONTENT_START_Y + (selectorIndex % pageItems) * LINE_HEIGHT - 2, pageWidth - RIGHT_MARGIN - bezelLeft,
|
renderer.fillRect(bezelLeft, CONTENT_START_Y + (selectorIndex % pageItems) * LINE_HEIGHT - 2,
|
||||||
LINE_HEIGHT);
|
pageWidth - RIGHT_MARGIN - bezelLeft, LINE_HEIGHT);
|
||||||
|
|
||||||
// Draw items
|
// Draw items
|
||||||
for (int i = pageStartIndex; i < fileCount && i < pageStartIndex + pageItems; i++) {
|
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(),
|
renderer.drawText(UI_10_FONT_ID, LEFT_MARGIN, CONTENT_START_Y + (i % pageItems) * LINE_HEIGHT, item.c_str(),
|
||||||
i != selectorIndex);
|
i != selectorIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw "Search..." shortcut if it's on the current page
|
// Draw "Search..." shortcut if it's on the current page
|
||||||
const int searchIndex = fileCount; // Last item
|
const int searchIndex = fileCount; // Last item
|
||||||
if (searchIndex >= pageStartIndex && searchIndex < pageStartIndex + pageItems) {
|
if (searchIndex >= pageStartIndex && searchIndex < pageStartIndex + pageItems) {
|
||||||
@ -1665,7 +1657,8 @@ void MyLibraryActivity::renderActionMenu() const {
|
|||||||
|
|
||||||
// Show filename
|
// Show filename
|
||||||
const int filenameY = 70 + bezelTop;
|
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());
|
renderer.drawCenteredText(UI_10_FONT_ID, filenameY, truncatedName.c_str());
|
||||||
|
|
||||||
// Menu options - 4 for Recent tab, 2 for Files tab
|
// Menu options - 4 for Recent tab, 2 for Files tab
|
||||||
@ -1694,7 +1687,8 @@ void MyLibraryActivity::renderActionMenu() const {
|
|||||||
if (menuSelection == 2) {
|
if (menuSelection == 2) {
|
||||||
renderer.fillRect(menuX - 10, menuStartY + menuLineHeight * 2 - 5, menuItemWidth + 20, menuLineHeight);
|
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
|
// Clear All Recents option
|
||||||
if (menuSelection == 3) {
|
if (menuSelection == 3) {
|
||||||
@ -1808,7 +1802,8 @@ void MyLibraryActivity::renderListDeleteConfirmation() const {
|
|||||||
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 - 40, truncatedName.c_str());
|
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 - 40, truncatedName.c_str());
|
||||||
|
|
||||||
// Warning text
|
// 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.");
|
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 + 25, "This cannot be undone.");
|
||||||
|
|
||||||
// Draw bottom button hints
|
// 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" : "");
|
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);
|
renderer.drawText(UI_10_FONT_ID, LEFT_MARGIN, y + 32, countText.c_str(), !isSelected);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw "Search..." shortcut if it's on the current page
|
// Draw "Search..." shortcut if it's on the current page
|
||||||
const int searchIndex = bookCount; // Last item
|
const int searchIndex = bookCount; // Last item
|
||||||
if (searchIndex >= pageStartIndex && searchIndex < pageStartIndex + pageItems) {
|
if (searchIndex >= pageStartIndex && searchIndex < pageStartIndex + pageItems) {
|
||||||
@ -1924,12 +1919,13 @@ void MyLibraryActivity::renderSearchTab() const {
|
|||||||
if (!searchInResults) {
|
if (!searchInResults) {
|
||||||
displayQuery = searchQuery + "_"; // Show cursor when in picker
|
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());
|
renderer.drawText(UI_10_FONT_ID, LEFT_MARGIN, queryY, truncatedQuery.c_str());
|
||||||
|
|
||||||
// Draw results below query
|
// Draw results below query
|
||||||
const int resultsStartY = queryY + QUERY_HEIGHT;
|
const int resultsStartY = queryY + QUERY_HEIGHT;
|
||||||
|
|
||||||
// Draw results section
|
// Draw results section
|
||||||
if (resultCount == 0) {
|
if (resultCount == 0) {
|
||||||
if (searchQuery.empty()) {
|
if (searchQuery.empty()) {
|
||||||
@ -1996,8 +1992,8 @@ void MyLibraryActivity::renderSearchTab() const {
|
|||||||
const int badgeY = y + 2 + (titleLineHeight - badgeHeight) / 2;
|
const int badgeY = y + 2 + (titleLineHeight - badgeHeight) / 2;
|
||||||
|
|
||||||
if (!tags.extensionTag.empty()) {
|
if (!tags.extensionTag.empty()) {
|
||||||
int badgeWidth = ScreenComponents::drawPillBadge(renderer, badgeX, badgeY, tags.extensionTag.c_str(),
|
int badgeWidth =
|
||||||
SMALL_FONT_ID, false);
|
ScreenComponents::drawPillBadge(renderer, badgeX, badgeY, tags.extensionTag.c_str(), SMALL_FONT_ID, false);
|
||||||
badgeX += badgeWidth + badgeSpacing;
|
badgeX += badgeWidth + badgeSpacing;
|
||||||
}
|
}
|
||||||
if (!tags.suffixTag.empty()) {
|
if (!tags.suffixTag.empty()) {
|
||||||
@ -2016,15 +2012,15 @@ void MyLibraryActivity::renderCharacterPicker(int y) const {
|
|||||||
const auto pageWidth = renderer.getScreenWidth();
|
const auto pageWidth = renderer.getScreenWidth();
|
||||||
const int bezelLeft = renderer.getBezelOffsetLeft();
|
const int bezelLeft = renderer.getBezelOffsetLeft();
|
||||||
const int bezelRight = renderer.getBezelOffsetRight();
|
const int bezelRight = renderer.getBezelOffsetRight();
|
||||||
|
|
||||||
constexpr int charSpacing = 6; // Spacing between characters
|
constexpr int charSpacing = 6; // Spacing between characters
|
||||||
constexpr int specialKeyPadding = 8; // Extra padding around special keys
|
constexpr int specialKeyPadding = 8; // Extra padding around special keys
|
||||||
constexpr int overflowIndicatorWidth = 16; // Space reserved for < > indicators
|
constexpr int overflowIndicatorWidth = 16; // Space reserved for < > indicators
|
||||||
|
|
||||||
// Calculate total width needed
|
// Calculate total width needed
|
||||||
const int charCount = static_cast<int>(searchCharacters.size());
|
const int charCount = static_cast<int>(searchCharacters.size());
|
||||||
const int totalItems = charCount + 3; // +3 for SPC, <-, CLR
|
const int totalItems = charCount + 3; // +3 for SPC, <-, CLR
|
||||||
|
|
||||||
// Calculate character widths
|
// Calculate character widths
|
||||||
int totalWidth = 0;
|
int totalWidth = 0;
|
||||||
for (char c : searchCharacters) {
|
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, "SPC") + specialKeyPadding;
|
||||||
totalWidth += renderer.getTextWidth(UI_10_FONT_ID, "<-") + specialKeyPadding;
|
totalWidth += renderer.getTextWidth(UI_10_FONT_ID, "<-") + specialKeyPadding;
|
||||||
totalWidth += renderer.getTextWidth(UI_10_FONT_ID, "CLR") + specialKeyPadding;
|
totalWidth += renderer.getTextWidth(UI_10_FONT_ID, "CLR") + specialKeyPadding;
|
||||||
|
|
||||||
// Calculate visible window - we'll scroll the character row
|
// Calculate visible window - we'll scroll the character row
|
||||||
const int availableWidth = pageWidth - bezelLeft - bezelRight - 40; // 40 for margins (20 each side)
|
const int availableWidth = pageWidth - bezelLeft - bezelRight - 40; // 40 for margins (20 each side)
|
||||||
|
|
||||||
// Determine scroll offset to keep selected character visible
|
// Determine scroll offset to keep selected character visible
|
||||||
int scrollOffset = 0;
|
int scrollOffset = 0;
|
||||||
int currentX = 0;
|
int currentX = 0;
|
||||||
|
|
||||||
// Calculate position of selected item
|
// Calculate position of selected item
|
||||||
for (int i = 0; i < totalItems; i++) {
|
for (int i = 0; i < totalItems; i++) {
|
||||||
int itemWidth;
|
int itemWidth;
|
||||||
@ -2056,7 +2052,7 @@ void MyLibraryActivity::renderCharacterPicker(int y) const {
|
|||||||
} else {
|
} else {
|
||||||
itemWidth = renderer.getTextWidth(UI_10_FONT_ID, "CLR") + specialKeyPadding;
|
itemWidth = renderer.getTextWidth(UI_10_FONT_ID, "CLR") + specialKeyPadding;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (i == searchCharIndex) {
|
if (i == searchCharIndex) {
|
||||||
// Center the selected item in the visible area
|
// Center the selected item in the visible area
|
||||||
scrollOffset = currentX - availableWidth / 2 + itemWidth / 2;
|
scrollOffset = currentX - availableWidth / 2 + itemWidth / 2;
|
||||||
@ -2068,26 +2064,27 @@ void MyLibraryActivity::renderCharacterPicker(int y) const {
|
|||||||
}
|
}
|
||||||
currentX += itemWidth;
|
currentX += itemWidth;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw separator line
|
// Draw separator line
|
||||||
renderer.drawLine(bezelLeft + 20, y + 22, pageWidth - bezelRight - 20, y + 22);
|
renderer.drawLine(bezelLeft + 20, y + 22, pageWidth - bezelRight - 20, y + 22);
|
||||||
|
|
||||||
// Calculate visible area boundaries (leave room for overflow indicators)
|
// Calculate visible area boundaries (leave room for overflow indicators)
|
||||||
const bool hasLeftOverflow = scrollOffset > 0;
|
const bool hasLeftOverflow = scrollOffset > 0;
|
||||||
const bool hasRightOverflow = totalWidth > availableWidth && scrollOffset < totalWidth - availableWidth;
|
const bool hasRightOverflow = totalWidth > availableWidth && scrollOffset < totalWidth - availableWidth;
|
||||||
const int visibleLeft = bezelLeft + 20 + (hasLeftOverflow ? overflowIndicatorWidth : 0);
|
const int visibleLeft = bezelLeft + 20 + (hasLeftOverflow ? overflowIndicatorWidth : 0);
|
||||||
const int visibleRight = pageWidth - bezelRight - 20 - (hasRightOverflow ? overflowIndicatorWidth : 0);
|
const int visibleRight = pageWidth - bezelRight - 20 - (hasRightOverflow ? overflowIndicatorWidth : 0);
|
||||||
|
|
||||||
// Draw characters
|
// Draw characters
|
||||||
const int startX = bezelLeft + 20 - scrollOffset;
|
const int startX = bezelLeft + 20 - scrollOffset;
|
||||||
currentX = startX;
|
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++) {
|
for (int i = 0; i < totalItems; i++) {
|
||||||
std::string label;
|
std::string label;
|
||||||
int itemWidth;
|
int itemWidth;
|
||||||
bool isSpecial = false;
|
bool isSpecial = false;
|
||||||
|
|
||||||
if (i < charCount) {
|
if (i < charCount) {
|
||||||
label = std::string(1, searchCharacters[i]);
|
label = std::string(1, searchCharacters[i]);
|
||||||
itemWidth = renderer.getTextWidth(UI_10_FONT_ID, label.c_str());
|
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());
|
itemWidth = renderer.getTextWidth(UI_10_FONT_ID, label.c_str());
|
||||||
isSpecial = true;
|
isSpecial = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only draw if visible (accounting for overflow indicator space)
|
// Only draw if visible (accounting for overflow indicator space)
|
||||||
const int drawX = currentX + (isSpecial ? specialKeyPadding / 2 : 0);
|
const int drawX = currentX + (isSpecial ? specialKeyPadding / 2 : 0);
|
||||||
if (drawX + itemWidth > visibleLeft && drawX < visibleRight) {
|
if (drawX + itemWidth > visibleLeft && drawX < visibleRight) {
|
||||||
const bool isSelected = showSelection && (i == searchCharIndex);
|
const bool isSelected = showSelection && (i == searchCharIndex);
|
||||||
|
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
// Draw inverted background for selection
|
// Draw inverted background for selection
|
||||||
constexpr int padding = 2;
|
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());
|
renderer.drawText(UI_10_FONT_ID, drawX, y, label.c_str());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
currentX += itemWidth + (isSpecial ? specialKeyPadding : charSpacing);
|
currentX += itemWidth + (isSpecial ? specialKeyPadding : charSpacing);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw overflow indicators if content extends beyond visible area
|
// Draw overflow indicators if content extends beyond visible area
|
||||||
if (totalWidth > availableWidth) {
|
if (totalWidth > availableWidth) {
|
||||||
constexpr int triangleHeight = 12; // Height of the triangle (vertical)
|
constexpr int triangleHeight = 12; // Height of the triangle (vertical)
|
||||||
constexpr int triangleWidth = 6; // Width of the triangle (horizontal) - thin/elongated
|
constexpr int triangleWidth = 6; // Width of the triangle (horizontal) - thin/elongated
|
||||||
const int pickerLineHeight = renderer.getLineHeight(UI_10_FONT_ID);
|
const int pickerLineHeight = renderer.getLineHeight(UI_10_FONT_ID);
|
||||||
const int triangleCenterY = y + pickerLineHeight / 2;
|
const int triangleCenterY = y + pickerLineHeight / 2;
|
||||||
|
|
||||||
// Left overflow indicator (more content to the left) - thin triangle pointing left
|
// Left overflow indicator (more content to the left) - thin triangle pointing left
|
||||||
if (hasLeftOverflow) {
|
if (hasLeftOverflow) {
|
||||||
// Clear background behind indicator to hide any overlapping text
|
// 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) {
|
for (int i = 0; i < triangleWidth; ++i) {
|
||||||
// Scale height based on position (0 at tip, full height at base)
|
// Scale height based on position (0 at tip, full height at base)
|
||||||
const int lineHalfHeight = (triangleHeight * i) / (triangleWidth * 2);
|
const int lineHalfHeight = (triangleHeight * i) / (triangleWidth * 2);
|
||||||
renderer.drawLine(tipX + i, triangleCenterY - lineHalfHeight,
|
renderer.drawLine(tipX + i, triangleCenterY - lineHalfHeight, tipX + i, triangleCenterY + lineHalfHeight);
|
||||||
tipX + i, triangleCenterY + lineHalfHeight);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Right overflow indicator (more content to the right) - thin triangle pointing right
|
// Right overflow indicator (more content to the right) - thin triangle pointing right
|
||||||
if (hasRightOverflow) {
|
if (hasRightOverflow) {
|
||||||
// Clear background behind indicator to hide any overlapping text
|
// 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
|
// Draw right-pointing triangle: base on left, point on right
|
||||||
const int baseX = pageWidth - bezelRight - 2 - triangleWidth;
|
const int baseX = pageWidth - bezelRight - 2 - triangleWidth;
|
||||||
for (int i = 0; i < triangleWidth; ++i) {
|
for (int i = 0; i < triangleWidth; ++i) {
|
||||||
// Scale height based on position (full height at base, 0 at tip)
|
// Scale height based on position (full height at base, 0 at tip)
|
||||||
const int lineHalfHeight = (triangleHeight * (triangleWidth - 1 - i)) / (triangleWidth * 2);
|
const int lineHalfHeight = (triangleHeight * (triangleWidth - 1 - i)) / (triangleWidth * 2);
|
||||||
renderer.drawLine(baseX + i, triangleCenterY - lineHalfHeight,
|
renderer.drawLine(baseX + i, triangleCenterY - lineHalfHeight, baseX + i, triangleCenterY + lineHalfHeight);
|
||||||
baseX + i, triangleCenterY + lineHalfHeight);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,10 +13,10 @@
|
|||||||
|
|
||||||
// Cached thumbnail existence info for Recent tab
|
// Cached thumbnail existence info for Recent tab
|
||||||
struct ThumbExistsCache {
|
struct ThumbExistsCache {
|
||||||
std::string bookPath; // Book path this cache entry belongs to
|
std::string bookPath; // Book path this cache entry belongs to
|
||||||
std::string thumbPath; // Path to micro-thumbnail (if exists)
|
std::string thumbPath; // Path to micro-thumbnail (if exists)
|
||||||
bool checked = false; // Whether we've checked for this book
|
bool checked = false; // Whether we've checked for this book
|
||||||
bool exists = false; // Whether thumbnail exists
|
bool exists = false; // Whether thumbnail exists
|
||||||
};
|
};
|
||||||
|
|
||||||
// Search result for the Search tab
|
// Search result for the Search tab
|
||||||
@ -38,7 +38,14 @@ struct BookmarkedBook {
|
|||||||
class MyLibraryActivity final : public Activity {
|
class MyLibraryActivity final : public Activity {
|
||||||
public:
|
public:
|
||||||
enum class Tab { Recent, Lists, Bookmarks, Search, Files };
|
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 };
|
enum class ActionType { Archive, Delete, RemoveFromRecents, ClearAllRecents };
|
||||||
|
|
||||||
private:
|
private:
|
||||||
@ -55,7 +62,7 @@ class MyLibraryActivity final : public Activity {
|
|||||||
ActionType selectedAction = ActionType::Archive;
|
ActionType selectedAction = ActionType::Archive;
|
||||||
std::string actionTargetPath;
|
std::string actionTargetPath;
|
||||||
std::string actionTargetName;
|
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
|
bool ignoreNextConfirmRelease = false; // Prevents immediate selection after long-press opens menu
|
||||||
|
|
||||||
// Recent tab state
|
// Recent tab state
|
||||||
@ -64,13 +71,12 @@ class MyLibraryActivity final : public Activity {
|
|||||||
// Static thumbnail existence cache - persists across activity enter/exit
|
// Static thumbnail existence cache - persists across activity enter/exit
|
||||||
static constexpr int MAX_THUMB_CACHE = 10;
|
static constexpr int MAX_THUMB_CACHE = 10;
|
||||||
static ThumbExistsCache thumbExistsCache[MAX_THUMB_CACHE];
|
static ThumbExistsCache thumbExistsCache[MAX_THUMB_CACHE];
|
||||||
|
|
||||||
public:
|
public:
|
||||||
// Clear the thumbnail existence cache (call when disk cache is cleared)
|
// Clear the thumbnail existence cache (call when disk cache is cleared)
|
||||||
static void clearThumbExistsCache();
|
static void clearThumbExistsCache();
|
||||||
|
|
||||||
private:
|
|
||||||
|
|
||||||
|
private:
|
||||||
// Lists tab state
|
// Lists tab state
|
||||||
std::vector<std::string> lists;
|
std::vector<std::string> lists;
|
||||||
|
|
||||||
@ -148,12 +154,12 @@ class MyLibraryActivity final : public Activity {
|
|||||||
void renderClearAllRecentsConfirmation() const;
|
void renderClearAllRecentsConfirmation() const;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit MyLibraryActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
|
explicit MyLibraryActivity(
|
||||||
const std::function<void()>& onGoHome,
|
GfxRenderer& renderer, MappedInputManager& mappedInput, const std::function<void()>& onGoHome,
|
||||||
const std::function<void(const std::string& path, Tab fromTab)>& onSelectBook,
|
const std::function<void(const std::string& path, Tab fromTab)>& onSelectBook,
|
||||||
const std::function<void(const std::string& listName)>& onSelectList,
|
const std::function<void(const std::string& listName)>& onSelectList,
|
||||||
const std::function<void(const std::string& path, const std::string& title)>& onSelectBookmarkedBook = nullptr,
|
const std::function<void(const std::string& path, const std::string& title)>& onSelectBookmarkedBook = nullptr,
|
||||||
Tab initialTab = Tab::Recent, std::string initialPath = "/")
|
Tab initialTab = Tab::Recent, std::string initialPath = "/")
|
||||||
: Activity("MyLibrary", renderer, mappedInput),
|
: Activity("MyLibrary", renderer, mappedInput),
|
||||||
currentTab(initialTab),
|
currentTab(initialTab),
|
||||||
basepath(initialPath.empty() ? "/" : std::move(initialPath)),
|
basepath(initialPath.empty() ? "/" : std::move(initialPath)),
|
||||||
|
|||||||
@ -600,9 +600,9 @@ void CrossPointWebServerActivity::renderWebBrowserScreen() const {
|
|||||||
// Landscape layout (800x480): QR on left, text on right
|
// Landscape layout (800x480): QR on left, text on right
|
||||||
constexpr int QR_X = 15;
|
constexpr int QR_X = 15;
|
||||||
constexpr int QR_Y = 15;
|
constexpr int QR_Y = 15;
|
||||||
constexpr int QR_PX = 7; // pixels per QR module
|
constexpr int QR_PX = 7; // pixels per QR module
|
||||||
constexpr int QR_SIZE = 33 * QR_PX; // QR version 4 = 33 modules = 231px
|
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 TEXT_X = QR_X + QR_SIZE + 35; // Text starts after QR + margin
|
||||||
constexpr int LINE_SPACING = 32;
|
constexpr int LINE_SPACING = 32;
|
||||||
|
|
||||||
// Draw title on right side
|
// Draw title on right side
|
||||||
@ -667,9 +667,9 @@ void CrossPointWebServerActivity::renderCompanionAppScreen() const {
|
|||||||
// Landscape layout (800x480): QR on left, text on right
|
// Landscape layout (800x480): QR on left, text on right
|
||||||
constexpr int QR_X = 15;
|
constexpr int QR_X = 15;
|
||||||
constexpr int QR_Y = 15;
|
constexpr int QR_Y = 15;
|
||||||
constexpr int QR_PX = 7; // pixels per QR module
|
constexpr int QR_PX = 7; // pixels per QR module
|
||||||
constexpr int QR_SIZE = 33 * QR_PX; // QR version 4 = 33 modules = 231px
|
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 TEXT_X = QR_X + QR_SIZE + 35; // Text starts after QR + margin
|
||||||
constexpr int LINE_SPACING = 32;
|
constexpr int LINE_SPACING = 32;
|
||||||
|
|
||||||
// Draw title on right side
|
// Draw title on right side
|
||||||
@ -717,9 +717,9 @@ void CrossPointWebServerActivity::renderCompanionAppLibraryScreen() const {
|
|||||||
// Landscape layout (800x480): QR on left, text on right
|
// Landscape layout (800x480): QR on left, text on right
|
||||||
constexpr int QR_X = 15;
|
constexpr int QR_X = 15;
|
||||||
constexpr int QR_Y = 15;
|
constexpr int QR_Y = 15;
|
||||||
constexpr int QR_PX = 7; // pixels per QR module
|
constexpr int QR_PX = 7; // pixels per QR module
|
||||||
constexpr int QR_SIZE = 33 * QR_PX; // QR version 4 = 33 modules = 231px
|
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 TEXT_X = QR_X + QR_SIZE + 35; // Text starts after QR + margin
|
||||||
constexpr int LINE_SPACING = 32;
|
constexpr int LINE_SPACING = 32;
|
||||||
|
|
||||||
// Draw title on right side
|
// Draw title on right side
|
||||||
|
|||||||
@ -111,24 +111,25 @@ void EpubReaderActivity::onEnter() {
|
|||||||
FsFile f;
|
FsFile f;
|
||||||
if (SdMan.openFileForRead("ERS", epub->getCachePath() + "/progress.bin", f)) {
|
if (SdMan.openFileForRead("ERS", epub->getCachePath() + "/progress.bin", f)) {
|
||||||
const size_t fileSize = f.size();
|
const size_t fileSize = f.size();
|
||||||
|
|
||||||
if (fileSize >= 9) {
|
if (fileSize >= 9) {
|
||||||
// New format: version (1) + spineIndex (2) + pageNumber (2) + contentOffset (4) = 9 bytes
|
// New format: version (1) + spineIndex (2) + pageNumber (2) + contentOffset (4) = 9 bytes
|
||||||
uint8_t version;
|
uint8_t version;
|
||||||
serialization::readPod(f, version);
|
serialization::readPod(f, version);
|
||||||
|
|
||||||
if (version == EPUB_PROGRESS_VERSION) {
|
if (version == EPUB_PROGRESS_VERSION) {
|
||||||
uint16_t spineIndex, pageNumber;
|
uint16_t spineIndex, pageNumber;
|
||||||
serialization::readPod(f, spineIndex);
|
serialization::readPod(f, spineIndex);
|
||||||
serialization::readPod(f, pageNumber);
|
serialization::readPod(f, pageNumber);
|
||||||
serialization::readPod(f, savedContentOffset);
|
serialization::readPod(f, savedContentOffset);
|
||||||
|
|
||||||
currentSpineIndex = spineIndex;
|
currentSpineIndex = spineIndex;
|
||||||
nextPageNumber = pageNumber;
|
nextPageNumber = pageNumber;
|
||||||
hasContentOffset = true;
|
hasContentOffset = true;
|
||||||
|
|
||||||
Serial.printf("[%lu] [ERS] Loaded progress v1: spine %d, page %d, offset %u\n",
|
if (Serial)
|
||||||
millis(), currentSpineIndex, nextPageNumber, savedContentOffset);
|
Serial.printf("[%lu] [ERS] Loaded progress v1: spine %d, page %d, offset %u\n", millis(), currentSpineIndex,
|
||||||
|
nextPageNumber, savedContentOffset);
|
||||||
} else {
|
} else {
|
||||||
// Unknown version, try legacy format
|
// Unknown version, try legacy format
|
||||||
f.seek(0);
|
f.seek(0);
|
||||||
@ -137,8 +138,9 @@ void EpubReaderActivity::onEnter() {
|
|||||||
currentSpineIndex = data[0] + (data[1] << 8);
|
currentSpineIndex = data[0] + (data[1] << 8);
|
||||||
nextPageNumber = data[2] + (data[3] << 8);
|
nextPageNumber = data[2] + (data[3] << 8);
|
||||||
hasContentOffset = false;
|
hasContentOffset = false;
|
||||||
Serial.printf("[%lu] [ERS] Loaded legacy progress (unknown version %d): spine %d, page %d\n",
|
if (Serial)
|
||||||
millis(), version, currentSpineIndex, nextPageNumber);
|
Serial.printf("[%lu] [ERS] Loaded legacy progress (unknown version %d): spine %d, page %d\n", millis(),
|
||||||
|
version, currentSpineIndex, nextPageNumber);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (fileSize >= 4) {
|
} else if (fileSize >= 4) {
|
||||||
@ -148,20 +150,23 @@ void EpubReaderActivity::onEnter() {
|
|||||||
currentSpineIndex = data[0] + (data[1] << 8);
|
currentSpineIndex = data[0] + (data[1] << 8);
|
||||||
nextPageNumber = data[2] + (data[3] << 8);
|
nextPageNumber = data[2] + (data[3] << 8);
|
||||||
hasContentOffset = false;
|
hasContentOffset = false;
|
||||||
Serial.printf("[%lu] [ERS] Loaded legacy progress: spine %d, page %d\n",
|
if (Serial)
|
||||||
millis(), currentSpineIndex, nextPageNumber);
|
Serial.printf("[%lu] [ERS] Loaded legacy progress: spine %d, page %d\n", millis(), currentSpineIndex,
|
||||||
|
nextPageNumber);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
f.close();
|
f.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
// We may want a better condition to detect if we are opening for the first time.
|
// 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.
|
// This will trigger if the book is re-opened at Chapter 0.
|
||||||
if (currentSpineIndex == 0) {
|
if (currentSpineIndex == 0) {
|
||||||
int textSpineIndex = epub->getSpineIndexForTextReference();
|
int textSpineIndex = epub->getSpineIndexForTextReference();
|
||||||
if (textSpineIndex != 0) {
|
if (textSpineIndex != 0) {
|
||||||
currentSpineIndex = textSpineIndex;
|
currentSpineIndex = textSpineIndex;
|
||||||
Serial.printf("[%lu] [ERS] Opened for first time, navigating to text reference at index %d\n", millis(),
|
if (Serial)
|
||||||
textSpineIndex);
|
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();
|
Section* cachedSection = section.get();
|
||||||
SemaphoreHandle_t cachedMutex = renderingMutex;
|
SemaphoreHandle_t cachedMutex = renderingMutex;
|
||||||
EpubReaderActivity* self = this;
|
EpubReaderActivity* self = this;
|
||||||
|
|
||||||
// Handle dictionary mode selection - exitActivity deletes DictionaryMenuActivity
|
// Handle dictionary mode selection - exitActivity deletes DictionaryMenuActivity
|
||||||
exitActivity();
|
exitActivity();
|
||||||
|
|
||||||
if (mode == DictionaryMode::ENTER_WORD) {
|
if (mode == DictionaryMode::ENTER_WORD) {
|
||||||
// Enter word mode - show keyboard and search
|
// Enter word mode - show keyboard and search
|
||||||
self->enterNewActivity(new DictionarySearchActivity(cachedRenderer, cachedMappedInput,
|
self->enterNewActivity(new DictionarySearchActivity(
|
||||||
[self]() {
|
cachedRenderer, cachedMappedInput,
|
||||||
// On back from dictionary
|
[self]() {
|
||||||
self->exitActivity();
|
// On back from dictionary
|
||||||
self->updateRequired = true;
|
self->exitActivity();
|
||||||
},
|
self->updateRequired = true;
|
||||||
"")); // Empty string = show keyboard
|
},
|
||||||
|
"")); // Empty string = show keyboard
|
||||||
} else {
|
} else {
|
||||||
// Select from screen mode - show word selection on current page
|
// Select from screen mode - show word selection on current page
|
||||||
if (cachedSection) {
|
if (cachedSection) {
|
||||||
@ -322,7 +328,7 @@ void EpubReaderActivity::loop() {
|
|||||||
// Get margins for word selection positioning
|
// Get margins for word selection positioning
|
||||||
int orientedMarginTop, orientedMarginRight, orientedMarginBottom, orientedMarginLeft;
|
int orientedMarginTop, orientedMarginRight, orientedMarginBottom, orientedMarginLeft;
|
||||||
cachedRenderer.getOrientedViewableTRBL(&orientedMarginTop, &orientedMarginRight, &orientedMarginBottom,
|
cachedRenderer.getOrientedViewableTRBL(&orientedMarginTop, &orientedMarginRight, &orientedMarginBottom,
|
||||||
&orientedMarginLeft);
|
&orientedMarginLeft);
|
||||||
orientedMarginTop += SETTINGS.screenMargin;
|
orientedMarginTop += SETTINGS.screenMargin;
|
||||||
orientedMarginLeft += SETTINGS.screenMargin;
|
orientedMarginLeft += SETTINGS.screenMargin;
|
||||||
|
|
||||||
@ -335,12 +341,13 @@ void EpubReaderActivity::loop() {
|
|||||||
[self](const std::string& selectedWord) {
|
[self](const std::string& selectedWord) {
|
||||||
// Word selected - look it up
|
// Word selected - look it up
|
||||||
self->exitActivity();
|
self->exitActivity();
|
||||||
self->enterNewActivity(new DictionarySearchActivity(self->renderer, self->mappedInput,
|
self->enterNewActivity(new DictionarySearchActivity(
|
||||||
[self]() {
|
self->renderer, self->mappedInput,
|
||||||
self->exitActivity();
|
[self]() {
|
||||||
self->updateRequired = true;
|
self->exitActivity();
|
||||||
},
|
self->updateRequired = true;
|
||||||
selectedWord));
|
},
|
||||||
|
selectedWord));
|
||||||
},
|
},
|
||||||
[self]() {
|
[self]() {
|
||||||
// Cancelled word selection
|
// Cancelled word selection
|
||||||
@ -372,14 +379,14 @@ void EpubReaderActivity::loop() {
|
|||||||
if (SETTINGS.shortPwrBtn == CrossPointSettings::SHORT_PWRBTN::QUICK_MENU &&
|
if (SETTINGS.shortPwrBtn == CrossPointSettings::SHORT_PWRBTN::QUICK_MENU &&
|
||||||
mappedInput.wasReleased(MappedInputManager::Button::Power)) {
|
mappedInput.wasReleased(MappedInputManager::Button::Power)) {
|
||||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||||
|
|
||||||
// Check if current page is bookmarked
|
// Check if current page is bookmarked
|
||||||
bool isBookmarked = false;
|
bool isBookmarked = false;
|
||||||
if (section) {
|
if (section) {
|
||||||
const uint32_t contentOffset = section->getContentOffsetForPage(section->currentPage);
|
const uint32_t contentOffset = section->getContentOffsetForPage(section->currentPage);
|
||||||
isBookmarked = BookmarkStore::isPageBookmarked(epub->getPath(), currentSpineIndex, contentOffset);
|
isBookmarked = BookmarkStore::isPageBookmarked(epub->getPath(), currentSpineIndex, contentOffset);
|
||||||
}
|
}
|
||||||
|
|
||||||
exitActivity();
|
exitActivity();
|
||||||
enterNewActivity(new QuickMenuActivity(
|
enterNewActivity(new QuickMenuActivity(
|
||||||
renderer, mappedInput,
|
renderer, mappedInput,
|
||||||
@ -387,9 +394,9 @@ void EpubReaderActivity::loop() {
|
|||||||
// Cache values before exitActivity
|
// Cache values before exitActivity
|
||||||
EpubReaderActivity* self = this;
|
EpubReaderActivity* self = this;
|
||||||
SemaphoreHandle_t cachedMutex = renderingMutex;
|
SemaphoreHandle_t cachedMutex = renderingMutex;
|
||||||
|
|
||||||
exitActivity();
|
exitActivity();
|
||||||
|
|
||||||
if (action == QuickMenuAction::DICTIONARY) {
|
if (action == QuickMenuAction::DICTIONARY) {
|
||||||
// Open dictionary menu - cache renderer/input for this scope
|
// Open dictionary menu - cache renderer/input for this scope
|
||||||
GfxRenderer& cachedRenderer = self->renderer;
|
GfxRenderer& cachedRenderer = self->renderer;
|
||||||
@ -402,15 +409,17 @@ void EpubReaderActivity::loop() {
|
|||||||
MappedInputManager& m = self->mappedInput;
|
MappedInputManager& m = self->mappedInput;
|
||||||
Section* s = self->section.get();
|
Section* s = self->section.get();
|
||||||
SemaphoreHandle_t mtx = self->renderingMutex;
|
SemaphoreHandle_t mtx = self->renderingMutex;
|
||||||
|
|
||||||
self->exitActivity();
|
self->exitActivity();
|
||||||
|
|
||||||
if (mode == DictionaryMode::ENTER_WORD) {
|
if (mode == DictionaryMode::ENTER_WORD) {
|
||||||
self->enterNewActivity(new DictionarySearchActivity(r, m,
|
self->enterNewActivity(new DictionarySearchActivity(
|
||||||
|
r, m,
|
||||||
[self]() {
|
[self]() {
|
||||||
self->exitActivity();
|
self->exitActivity();
|
||||||
self->updateRequired = true;
|
self->updateRequired = true;
|
||||||
}, ""));
|
},
|
||||||
|
""));
|
||||||
} else if (s) {
|
} else if (s) {
|
||||||
xSemaphoreTake(mtx, portMAX_DELAY);
|
xSemaphoreTake(mtx, portMAX_DELAY);
|
||||||
auto page = s->loadPageFromSectionFile();
|
auto page = s->loadPageFromSectionFile();
|
||||||
@ -420,7 +429,7 @@ void EpubReaderActivity::loop() {
|
|||||||
mt += SETTINGS.screenMargin;
|
mt += SETTINGS.screenMargin;
|
||||||
ml += SETTINGS.screenMargin;
|
ml += SETTINGS.screenMargin;
|
||||||
const int fontId = SETTINGS.getReaderFontId();
|
const int fontId = SETTINGS.getReaderFontId();
|
||||||
|
|
||||||
self->enterNewActivity(new EpubWordSelectionActivity(
|
self->enterNewActivity(new EpubWordSelectionActivity(
|
||||||
r, m, std::move(page), fontId, ml, mt,
|
r, m, std::move(page), fontId, ml, mt,
|
||||||
[self](const std::string& word) {
|
[self](const std::string& word) {
|
||||||
@ -430,7 +439,8 @@ void EpubReaderActivity::loop() {
|
|||||||
[self]() {
|
[self]() {
|
||||||
self->exitActivity();
|
self->exitActivity();
|
||||||
self->updateRequired = true;
|
self->updateRequired = true;
|
||||||
}, word));
|
},
|
||||||
|
word));
|
||||||
},
|
},
|
||||||
[self]() {
|
[self]() {
|
||||||
self->exitActivity();
|
self->exitActivity();
|
||||||
@ -455,7 +465,7 @@ void EpubReaderActivity::loop() {
|
|||||||
if (self->section) {
|
if (self->section) {
|
||||||
const uint32_t contentOffset = self->section->getContentOffsetForPage(self->section->currentPage);
|
const uint32_t contentOffset = self->section->getContentOffsetForPage(self->section->currentPage);
|
||||||
const std::string& bookPath = self->epub->getPath();
|
const std::string& bookPath = self->epub->getPath();
|
||||||
|
|
||||||
if (BookmarkStore::isPageBookmarked(bookPath, self->currentSpineIndex, contentOffset)) {
|
if (BookmarkStore::isPageBookmarked(bookPath, self->currentSpineIndex, contentOffset)) {
|
||||||
// Remove bookmark
|
// Remove bookmark
|
||||||
BookmarkStore::removeBookmark(bookPath, self->currentSpineIndex, contentOffset);
|
BookmarkStore::removeBookmark(bookPath, self->currentSpineIndex, contentOffset);
|
||||||
@ -466,7 +476,7 @@ void EpubReaderActivity::loop() {
|
|||||||
bm.contentOffset = contentOffset;
|
bm.contentOffset = contentOffset;
|
||||||
bm.pageNumber = self->section->currentPage;
|
bm.pageNumber = self->section->currentPage;
|
||||||
bm.timestamp = millis() / 1000; // Approximate timestamp
|
bm.timestamp = millis() / 1000; // Approximate timestamp
|
||||||
|
|
||||||
// Generate name: "Chapter - Page X" or fallback
|
// Generate name: "Chapter - Page X" or fallback
|
||||||
std::string chapterTitle;
|
std::string chapterTitle;
|
||||||
const int tocIndex = self->epub->getTocIndexForSpineIndex(self->currentSpineIndex);
|
const int tocIndex = self->epub->getTocIndexForSpineIndex(self->currentSpineIndex);
|
||||||
@ -478,7 +488,7 @@ void EpubReaderActivity::loop() {
|
|||||||
} else {
|
} else {
|
||||||
bm.name = "Page " + std::to_string(self->section->currentPage + 1);
|
bm.name = "Page " + std::to_string(self->section->currentPage + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
BookmarkStore::addBookmark(bookPath, bm);
|
BookmarkStore::addBookmark(bookPath, bm);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -629,18 +639,19 @@ void EpubReaderActivity::renderScreen() {
|
|||||||
|
|
||||||
if (!section) {
|
if (!section) {
|
||||||
const auto filepath = epub->getSpineItem(currentSpineIndex).href;
|
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<Section>(new Section(epub, currentSpineIndex, renderer));
|
section = std::unique_ptr<Section>(new Section(epub, currentSpineIndex, renderer));
|
||||||
|
|
||||||
const uint16_t viewportWidth = renderer.getScreenWidth() - orientedMarginLeft - orientedMarginRight;
|
const uint16_t viewportWidth = renderer.getScreenWidth() - orientedMarginLeft - orientedMarginRight;
|
||||||
const uint16_t viewportHeight = renderer.getScreenHeight() - orientedMarginTop - orientedMarginBottom;
|
const uint16_t viewportHeight = renderer.getScreenHeight() - orientedMarginTop - orientedMarginBottom;
|
||||||
|
|
||||||
bool sectionWasReIndexed = false;
|
bool sectionWasReIndexed = false;
|
||||||
|
|
||||||
if (!section->loadSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
|
if (!section->loadSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
|
||||||
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
|
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
|
||||||
viewportHeight, SETTINGS.hyphenationEnabled)) {
|
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;
|
sectionWasReIndexed = true;
|
||||||
|
|
||||||
// Progress bar dimensions
|
// Progress bar dimensions
|
||||||
@ -683,15 +694,15 @@ void EpubReaderActivity::renderScreen() {
|
|||||||
renderer.displayBuffer(EInkDisplay::FAST_REFRESH);
|
renderer.displayBuffer(EInkDisplay::FAST_REFRESH);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!section->createSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
|
if (!section->createSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
|
||||||
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
|
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
|
||||||
viewportHeight, SETTINGS.hyphenationEnabled, progressSetup, progressCallback)) {
|
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();
|
section.reset();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else {
|
} 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
|
// Determine the correct page to display
|
||||||
@ -703,8 +714,9 @@ void EpubReaderActivity::renderScreen() {
|
|||||||
// Use the offset to find the correct page
|
// Use the offset to find the correct page
|
||||||
const int restoredPage = section->findPageForContentOffset(savedContentOffset);
|
const int restoredPage = section->findPageForContentOffset(savedContentOffset);
|
||||||
section->currentPage = restoredPage;
|
section->currentPage = restoredPage;
|
||||||
Serial.printf("[%lu] [ERS] Restored position via offset: %u -> page %d (was page %d)\n",
|
if (Serial)
|
||||||
millis(), savedContentOffset, restoredPage, nextPageNumber);
|
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
|
// Clear the offset flag since we've used it
|
||||||
hasContentOffset = false;
|
hasContentOffset = false;
|
||||||
} else {
|
} else {
|
||||||
@ -716,7 +728,7 @@ void EpubReaderActivity::renderScreen() {
|
|||||||
renderer.clearScreen();
|
renderer.clearScreen();
|
||||||
|
|
||||||
if (section->pageCount == 0) {
|
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);
|
renderer.drawCenteredText(UI_12_FONT_ID, 300, "Empty chapter", true, EpdFontFamily::BOLD);
|
||||||
renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
|
renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
|
||||||
renderer.displayBuffer();
|
renderer.displayBuffer();
|
||||||
@ -724,7 +736,9 @@ void EpubReaderActivity::renderScreen() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (section->currentPage < 0 || section->currentPage >= section->pageCount) {
|
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);
|
renderer.drawCenteredText(UI_12_FONT_ID, 300, "Out of bounds", true, EpdFontFamily::BOLD);
|
||||||
renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
|
renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
|
||||||
renderer.displayBuffer();
|
renderer.displayBuffer();
|
||||||
@ -734,25 +748,25 @@ void EpubReaderActivity::renderScreen() {
|
|||||||
{
|
{
|
||||||
auto p = section->loadPageFromSectionFile();
|
auto p = section->loadPageFromSectionFile();
|
||||||
if (!p) {
|
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->clearCache();
|
||||||
section.reset();
|
section.reset();
|
||||||
return renderScreen();
|
return renderScreen();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle empty pages (e.g., from malformed chapters that couldn't be parsed)
|
// Handle empty pages (e.g., from malformed chapters that couldn't be parsed)
|
||||||
if (p->elements.empty()) {
|
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_12_FONT_ID, 280, "Chapter content unavailable", true, EpdFontFamily::BOLD);
|
||||||
renderer.drawCenteredText(UI_10_FONT_ID, 320, "(File may be malformed)");
|
renderer.drawCenteredText(UI_10_FONT_ID, 320, "(File may be malformed)");
|
||||||
renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
|
renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
|
||||||
renderer.displayBuffer();
|
renderer.displayBuffer();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto start = millis();
|
const auto start = millis();
|
||||||
renderContents(std::move(p), orientedMarginTop, orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
|
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
|
// 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)) {
|
if (SdMan.openFileForWrite("ERS", epub->getCachePath() + "/progress.bin", f)) {
|
||||||
// Get content offset for current page
|
// Get content offset for current page
|
||||||
const uint32_t contentOffset = section->getContentOffsetForPage(section->currentPage);
|
const uint32_t contentOffset = section->getContentOffsetForPage(section->currentPage);
|
||||||
|
|
||||||
// New format: version (1) + spineIndex (2) + pageNumber (2) + contentOffset (4) = 9 bytes
|
// New format: version (1) + spineIndex (2) + pageNumber (2) + contentOffset (4) = 9 bytes
|
||||||
serialization::writePod(f, EPUB_PROGRESS_VERSION);
|
serialization::writePod(f, EPUB_PROGRESS_VERSION);
|
||||||
serialization::writePod(f, static_cast<uint16_t>(currentSpineIndex));
|
serialization::writePod(f, static_cast<uint16_t>(currentSpineIndex));
|
||||||
serialization::writePod(f, static_cast<uint16_t>(section->currentPage));
|
serialization::writePod(f, static_cast<uint16_t>(section->currentPage));
|
||||||
serialization::writePod(f, contentOffset);
|
serialization::writePod(f, contentOffset);
|
||||||
|
|
||||||
f.close();
|
f.close();
|
||||||
Serial.printf("[%lu] [ERS] Saved progress: spine %d, page %d, offset %u\n",
|
if (Serial)
|
||||||
millis(), currentSpineIndex, section->currentPage, contentOffset);
|
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> page, const int or
|
|||||||
const int orientedMarginRight, const int orientedMarginBottom,
|
const int orientedMarginRight, const int orientedMarginBottom,
|
||||||
const int orientedMarginLeft) {
|
const int orientedMarginLeft) {
|
||||||
page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop);
|
page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop);
|
||||||
|
|
||||||
// Draw bookmark indicator (folded corner) if this page is bookmarked
|
// Draw bookmark indicator (folded corner) if this page is bookmarked
|
||||||
if (section) {
|
if (section) {
|
||||||
const uint32_t contentOffset = section->getContentOffsetForPage(section->currentPage);
|
const uint32_t contentOffset = section->getContentOffsetForPage(section->currentPage);
|
||||||
@ -787,14 +802,14 @@ void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int or
|
|||||||
constexpr int cornerSize = 20;
|
constexpr int cornerSize = 20;
|
||||||
const int cornerX = screenWidth - orientedMarginRight - cornerSize;
|
const int cornerX = screenWidth - orientedMarginRight - cornerSize;
|
||||||
const int cornerY = orientedMarginTop;
|
const int cornerY = orientedMarginTop;
|
||||||
|
|
||||||
// Draw triangle (folded corner effect)
|
// Draw triangle (folded corner effect)
|
||||||
const int xPoints[3] = {cornerX, cornerX + cornerSize, cornerX + cornerSize};
|
const int xPoints[3] = {cornerX, cornerX + cornerSize, cornerX + cornerSize};
|
||||||
const int yPoints[3] = {cornerY, cornerY, cornerY + cornerSize};
|
const int yPoints[3] = {cornerY, cornerY, cornerY + cornerSize};
|
||||||
renderer.fillPolygon(xPoints, yPoints, 3, true); // Black triangle
|
renderer.fillPolygon(xPoints, yPoints, 3, true); // Black triangle
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
|
renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
|
||||||
if (pagesUntilFullRefresh <= 1) {
|
if (pagesUntilFullRefresh <= 1) {
|
||||||
renderer.displayBuffer(EInkDisplay::HALF_REFRESH);
|
renderer.displayBuffer(EInkDisplay::HALF_REFRESH);
|
||||||
|
|||||||
@ -24,7 +24,7 @@ class EpubReaderActivity final : public ActivityWithSubactivity {
|
|||||||
// End-of-book prompt state
|
// End-of-book prompt state
|
||||||
bool showingEndOfBookPrompt = false;
|
bool showingEndOfBookPrompt = false;
|
||||||
int endOfBookSelection = 2; // 0=Archive, 1=Delete, 2=Keep (default to safe option)
|
int endOfBookSelection = 2; // 0=Archive, 1=Delete, 2=Keep (default to safe option)
|
||||||
|
|
||||||
// Content offset for position restoration after re-indexing
|
// Content offset for position restoration after re-indexing
|
||||||
uint32_t savedContentOffset = 0;
|
uint32_t savedContentOffset = 0;
|
||||||
bool hasContentOffset = false; // True if we have a valid content offset to use
|
bool hasContentOffset = false; // True if we have a valid content offset to use
|
||||||
|
|||||||
@ -160,12 +160,13 @@ void EpubReaderChapterSelectionActivity::renderScreen() {
|
|||||||
const int bezelLeft = renderer.getBezelOffsetLeft();
|
const int bezelLeft = renderer.getBezelOffsetLeft();
|
||||||
const int bezelRight = renderer.getBezelOffsetRight();
|
const int bezelRight = renderer.getBezelOffsetRight();
|
||||||
|
|
||||||
const std::string title =
|
const std::string title = renderer.truncatedText(UI_12_FONT_ID, epub->getTitle().c_str(),
|
||||||
renderer.truncatedText(UI_12_FONT_ID, epub->getTitle().c_str(), pageWidth - 40 - bezelLeft - bezelRight, EpdFontFamily::BOLD);
|
pageWidth - 40 - bezelLeft - bezelRight, EpdFontFamily::BOLD);
|
||||||
renderer.drawCenteredText(UI_12_FONT_ID, 15 + bezelTop, title.c_str(), true, EpdFontFamily::BOLD);
|
renderer.drawCenteredText(UI_12_FONT_ID, 15 + bezelTop, title.c_str(), true, EpdFontFamily::BOLD);
|
||||||
|
|
||||||
const auto pageStartIndex = selectorIndex / pageItems * pageItems;
|
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++) {
|
for (int itemIndex = pageStartIndex; itemIndex < totalItems && itemIndex < pageStartIndex + pageItems; itemIndex++) {
|
||||||
const int displayY = 60 + bezelTop + (itemIndex % pageItems) * 30;
|
const int displayY = 60 + bezelTop + (itemIndex % pageItems) * 30;
|
||||||
@ -175,8 +176,8 @@ void EpubReaderChapterSelectionActivity::renderScreen() {
|
|||||||
const int tocIndex = tocIndexFromItemIndex(itemIndex);
|
const int tocIndex = tocIndexFromItemIndex(itemIndex);
|
||||||
auto item = epub->getTocItem(tocIndex);
|
auto item = epub->getTocItem(tocIndex);
|
||||||
const int indentSize = 20 + bezelLeft + (item.level - 1) * 15;
|
const int indentSize = 20 + bezelLeft + (item.level - 1) * 15;
|
||||||
const std::string chapterName =
|
const std::string chapterName = renderer.truncatedText(
|
||||||
renderer.truncatedText(UI_10_FONT_ID, item.title.c_str(), pageWidth - 40 - bezelLeft - bezelRight - indentSize + 20 + bezelLeft);
|
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);
|
renderer.drawText(UI_10_FONT_ID, indentSize, displayY, chapterName.c_str(), !isSelected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -62,11 +62,8 @@ void ReaderActivity::onGoToEpubReader(std::unique_ptr<Epub> epub) {
|
|||||||
currentBookPath = epubPath;
|
currentBookPath = epubPath;
|
||||||
exitActivity();
|
exitActivity();
|
||||||
enterNewActivity(new EpubReaderActivity(
|
enterNewActivity(new EpubReaderActivity(
|
||||||
renderer, mappedInput, std::move(epub),
|
renderer, mappedInput, std::move(epub), [this, epubPath] { goToLibrary(epubPath); }, [this] { onGoBack(); },
|
||||||
[this, epubPath] { goToLibrary(epubPath); },
|
onGoToClearCache, onGoToSettings));
|
||||||
[this] { onGoBack(); },
|
|
||||||
onGoToClearCache,
|
|
||||||
onGoToSettings));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ReaderActivity::onGoToTxtReader(std::unique_ptr<Txt> txt) {
|
void ReaderActivity::onGoToTxtReader(std::unique_ptr<Txt> txt) {
|
||||||
|
|||||||
@ -658,15 +658,15 @@ void TxtReaderActivity::saveProgress() const {
|
|||||||
if (SdMan.openFileForWrite("TRS", txt->getCachePath() + "/progress.bin", f)) {
|
if (SdMan.openFileForWrite("TRS", txt->getCachePath() + "/progress.bin", f)) {
|
||||||
// New format: version + byte offset + page number (for backwards compatibility debugging)
|
// New format: version + byte offset + page number (for backwards compatibility debugging)
|
||||||
serialization::writePod(f, PROGRESS_VERSION);
|
serialization::writePod(f, PROGRESS_VERSION);
|
||||||
|
|
||||||
// Store byte offset - this is stable across font/setting changes
|
// Store byte offset - this is stable across font/setting changes
|
||||||
const size_t byteOffset = (currentPage >= 0 && currentPage < static_cast<int>(pageOffsets.size()))
|
const size_t byteOffset =
|
||||||
? pageOffsets[currentPage] : 0;
|
(currentPage >= 0 && currentPage < static_cast<int>(pageOffsets.size())) ? pageOffsets[currentPage] : 0;
|
||||||
serialization::writePod(f, static_cast<uint32_t>(byteOffset));
|
serialization::writePod(f, static_cast<uint32_t>(byteOffset));
|
||||||
|
|
||||||
// Also store page number for debugging/logging purposes
|
// Also store page number for debugging/logging purposes
|
||||||
serialization::writePod(f, static_cast<uint16_t>(currentPage));
|
serialization::writePod(f, static_cast<uint16_t>(currentPage));
|
||||||
|
|
||||||
f.close();
|
f.close();
|
||||||
Serial.printf("[%lu] [TRS] Saved progress: page %d, offset %zu\n", millis(), currentPage, byteOffset);
|
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)) {
|
if (SdMan.openFileForRead("TRS", txt->getCachePath() + "/progress.bin", f)) {
|
||||||
// Check file size to determine format
|
// Check file size to determine format
|
||||||
const size_t fileSize = f.size();
|
const size_t fileSize = f.size();
|
||||||
|
|
||||||
if (fileSize >= 7) {
|
if (fileSize >= 7) {
|
||||||
// New format: version (1) + byte offset (4) + page number (2) = 7 bytes
|
// New format: version (1) + byte offset (4) + page number (2) = 7 bytes
|
||||||
uint8_t version;
|
uint8_t version;
|
||||||
serialization::readPod(f, version);
|
serialization::readPod(f, version);
|
||||||
|
|
||||||
if (version == PROGRESS_VERSION) {
|
if (version == PROGRESS_VERSION) {
|
||||||
uint32_t savedOffset;
|
uint32_t savedOffset;
|
||||||
serialization::readPod(f, savedOffset);
|
serialization::readPod(f, savedOffset);
|
||||||
|
|
||||||
uint16_t savedPage;
|
uint16_t savedPage;
|
||||||
serialization::readPod(f, savedPage);
|
serialization::readPod(f, savedPage);
|
||||||
|
|
||||||
// Use byte offset to find the correct page (works even if re-indexed)
|
// Use byte offset to find the correct page (works even if re-indexed)
|
||||||
currentPage = findPageForOffset(savedOffset);
|
currentPage = findPageForOffset(savedOffset);
|
||||||
|
|
||||||
Serial.printf("[%lu] [TRS] Loaded progress: offset %u -> page %d/%d (was page %d)\n",
|
Serial.printf("[%lu] [TRS] Loaded progress: offset %u -> page %d/%d (was page %d)\n", millis(), savedOffset,
|
||||||
millis(), savedOffset, currentPage, totalPages, savedPage);
|
currentPage, totalPages, savedPage);
|
||||||
} else {
|
} else {
|
||||||
// Unknown version, fall back to legacy behavior
|
// Unknown version, fall back to legacy behavior
|
||||||
Serial.printf("[%lu] [TRS] Unknown progress version %d, ignoring\n", millis(), version);
|
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);
|
Serial.printf("[%lu] [TRS] Loaded legacy progress: page %d/%d\n", millis(), currentPage, totalPages);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bounds check
|
// Bounds check
|
||||||
if (currentPage >= totalPages) {
|
if (currentPage >= totalPages) {
|
||||||
currentPage = totalPages - 1;
|
currentPage = totalPages - 1;
|
||||||
@ -716,7 +716,7 @@ void TxtReaderActivity::loadProgress() {
|
|||||||
if (currentPage < 0) {
|
if (currentPage < 0) {
|
||||||
currentPage = 0;
|
currentPage = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
f.close();
|
f.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -725,16 +725,16 @@ int TxtReaderActivity::findPageForOffset(size_t targetOffset) const {
|
|||||||
if (pageOffsets.empty()) {
|
if (pageOffsets.empty()) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Binary search: find the largest offset that is <= targetOffset
|
// Binary search: find the largest offset that is <= targetOffset
|
||||||
// This finds the page that contains or starts at the target offset
|
// This finds the page that contains or starts at the target offset
|
||||||
auto it = std::upper_bound(pageOffsets.begin(), pageOffsets.end(), targetOffset);
|
auto it = std::upper_bound(pageOffsets.begin(), pageOffsets.end(), targetOffset);
|
||||||
|
|
||||||
if (it == pageOffsets.begin()) {
|
if (it == pageOffsets.begin()) {
|
||||||
// Target is before the first page, return page 0
|
// Target is before the first page, return page 0
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// upper_bound returns iterator to first element > targetOffset
|
// upper_bound returns iterator to first element > targetOffset
|
||||||
// So we need the element before it (which is <= targetOffset)
|
// So we need the element before it (which is <= targetOffset)
|
||||||
return static_cast<int>(std::distance(pageOffsets.begin(), it) - 1);
|
return static_cast<int>(std::distance(pageOffsets.begin(), it) - 1);
|
||||||
|
|||||||
@ -201,7 +201,8 @@ void CategorySettingsActivity::render() const {
|
|||||||
renderer.drawCenteredText(UI_12_FONT_ID, 15 + bezelTop, categoryName, true, EpdFontFamily::BOLD);
|
renderer.drawCenteredText(UI_12_FONT_ID, 15 + bezelTop, categoryName, true, EpdFontFamily::BOLD);
|
||||||
|
|
||||||
// Draw selection highlight
|
// 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
|
// Draw only visible settings
|
||||||
int visibleIndex = 0;
|
int visibleIndex = 0;
|
||||||
@ -237,7 +238,8 @@ void CategorySettingsActivity::render() const {
|
|||||||
visibleIndex++;
|
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);
|
pageHeight - 60 - bezelBottom, CROSSPOINT_VERSION);
|
||||||
|
|
||||||
const auto labels = mappedInput.mapLabels("« Back", "Toggle", "", "");
|
const auto labels = mappedInput.mapLabels("« Back", "Toggle", "", "");
|
||||||
|
|||||||
@ -147,7 +147,8 @@ void OtaUpdateActivity::render() {
|
|||||||
if (state == WAITING_CONFIRMATION) {
|
if (state == WAITING_CONFIRMATION) {
|
||||||
renderer.drawCenteredText(UI_10_FONT_ID, centerY - 100, "New update available!", true, EpdFontFamily::BOLD);
|
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 - 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", "", "");
|
const auto labels = mappedInput.mapLabels("Cancel", "Update", "", "");
|
||||||
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||||
@ -158,7 +159,9 @@ void OtaUpdateActivity::render() {
|
|||||||
if (state == UPDATE_IN_PROGRESS) {
|
if (state == UPDATE_IN_PROGRESS) {
|
||||||
renderer.drawCenteredText(UI_10_FONT_ID, centerY - 40, "Updating...", true, EpdFontFamily::BOLD);
|
renderer.drawCenteredText(UI_10_FONT_ID, centerY - 40, "Updating...", true, EpdFontFamily::BOLD);
|
||||||
renderer.drawRect(20 + bezelLeft, centerY, pageWidth - 40 - bezelLeft - bezelRight, 50);
|
renderer.drawRect(20 + bezelLeft, centerY, pageWidth - 40 - bezelLeft - bezelRight, 50);
|
||||||
renderer.fillRect(24 + bezelLeft, centerY + 4, static_cast<int>(updaterProgress * static_cast<float>(pageWidth - 44 - bezelLeft - bezelRight)), 42);
|
renderer.fillRect(24 + bezelLeft, centerY + 4,
|
||||||
|
static_cast<int>(updaterProgress * static_cast<float>(pageWidth - 44 - bezelLeft - bezelRight)),
|
||||||
|
42);
|
||||||
renderer.drawCenteredText(UI_10_FONT_ID, centerY + 70,
|
renderer.drawCenteredText(UI_10_FONT_ID, centerY + 70,
|
||||||
(std::to_string(static_cast<int>(updaterProgress * 100)) + "%").c_str());
|
(std::to_string(static_cast<int>(updaterProgress * 100)) + "%").c_str());
|
||||||
renderer.drawCenteredText(
|
renderer.drawCenteredText(
|
||||||
|
|||||||
@ -29,8 +29,8 @@ const SettingInfo displaySettings[displaySettingsCount] = {
|
|||||||
SettingInfo::Enum("Refresh Frequency", &CrossPointSettings::refreshFrequency,
|
SettingInfo::Enum("Refresh Frequency", &CrossPointSettings::refreshFrequency,
|
||||||
{"1 page", "5 pages", "10 pages", "15 pages", "30 pages"}),
|
{"1 page", "5 pages", "10 pages", "15 pages", "30 pages"}),
|
||||||
SettingInfo::Value("Bezel Compensation", &CrossPointSettings::bezelCompensation, {0, 10, 1}),
|
SettingInfo::Value("Bezel Compensation", &CrossPointSettings::bezelCompensation, {0, 10, 1}),
|
||||||
SettingInfo::Enum("Bezel Edge", &CrossPointSettings::bezelCompensationEdge,
|
SettingInfo::Enum("Bezel Edge", &CrossPointSettings::bezelCompensationEdge, {"Bottom", "Top", "Left", "Right"},
|
||||||
{"Bottom", "Top", "Left", "Right"}, isBezelCompensationEnabled)};
|
isBezelCompensationEnabled)};
|
||||||
|
|
||||||
// Helper to get custom font names as a vector
|
// Helper to get custom font names as a vector
|
||||||
std::vector<std::string> getCustomFontNamesVector() {
|
std::vector<std::string> getCustomFontNamesVector() {
|
||||||
@ -62,8 +62,8 @@ const SettingInfo readerSettings[readerSettingsCount] = {
|
|||||||
SettingInfo::Enum("Font Family", &CrossPointSettings::fontFamily, getFontFamilyOptions()),
|
SettingInfo::Enum("Font Family", &CrossPointSettings::fontFamily, getFontFamilyOptions()),
|
||||||
SettingInfo::Enum("Custom Font", &CrossPointSettings::customFontIndex, getCustomFontNamesVector(),
|
SettingInfo::Enum("Custom Font", &CrossPointSettings::customFontIndex, getCustomFontNamesVector(),
|
||||||
isCustomFontSelected),
|
isCustomFontSelected),
|
||||||
SettingInfo::Enum("Fallback Font", &CrossPointSettings::fallbackFontFamily,
|
SettingInfo::Enum("Fallback Font", &CrossPointSettings::fallbackFontFamily, {"Bookerly", "Noto Sans"},
|
||||||
{"Bookerly", "Noto Sans"}, isCustomFontSelected),
|
isCustomFontSelected),
|
||||||
SettingInfo::Enum("Font Size", &CrossPointSettings::fontSize, {"Small", "Medium", "Large", "X Large"}),
|
SettingInfo::Enum("Font Size", &CrossPointSettings::fontSize, {"Small", "Medium", "Large", "X Large"}),
|
||||||
SettingInfo::Enum("Line Spacing", &CrossPointSettings::lineSpacing, {"Tight", "Normal", "Wide"}),
|
SettingInfo::Enum("Line Spacing", &CrossPointSettings::lineSpacing, {"Tight", "Normal", "Wide"}),
|
||||||
SettingInfo::Value("Screen Margin", &CrossPointSettings::screenMargin, {5, 40, 5}),
|
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);
|
renderer.drawCenteredText(UI_12_FONT_ID, 15 + bezelTop, "Settings", true, EpdFontFamily::BOLD);
|
||||||
|
|
||||||
// Draw selection
|
// 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
|
// Draw all categories
|
||||||
for (int i = 0; i < categoryCount; i++) {
|
for (int i = 0; i < categoryCount; i++) {
|
||||||
@ -240,7 +241,8 @@ void SettingsActivity::render() const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Draw version text above button hints
|
// 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);
|
pageHeight - 60 - bezelBottom, CROSSPOINT_VERSION);
|
||||||
|
|
||||||
// Draw help text
|
// Draw help text
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
#include "KeyboardEntryActivity.h"
|
#include "KeyboardEntryActivity.h"
|
||||||
|
|
||||||
#include "activities/dictionary/DictionaryMargins.h"
|
|
||||||
#include "MappedInputManager.h"
|
#include "MappedInputManager.h"
|
||||||
|
#include "activities/dictionary/DictionaryMargins.h"
|
||||||
#include "fontIds.h"
|
#include "fontIds.h"
|
||||||
|
|
||||||
// Keyboard layouts - lowercase
|
// Keyboard layouts - lowercase
|
||||||
|
|||||||
@ -8,18 +8,10 @@
|
|||||||
namespace {
|
namespace {
|
||||||
constexpr int MENU_ITEM_COUNT = 4;
|
constexpr int MENU_ITEM_COUNT = 4;
|
||||||
const char* MENU_ITEMS[MENU_ITEM_COUNT] = {"Dictionary", "Bookmark", "Clear Cache", "Settings"};
|
const char* MENU_ITEMS[MENU_ITEM_COUNT] = {"Dictionary", "Bookmark", "Clear Cache", "Settings"};
|
||||||
const char* MENU_DESCRIPTIONS_ADD[MENU_ITEM_COUNT] = {
|
const char* MENU_DESCRIPTIONS_ADD[MENU_ITEM_COUNT] = {"Look up a word", "Add bookmark to this page",
|
||||||
"Look up a word",
|
"Free up storage space", "Open settings menu"};
|
||||||
"Add bookmark to this page",
|
const char* MENU_DESCRIPTIONS_REMOVE[MENU_ITEM_COUNT] = {"Look up a word", "Remove bookmark from this page",
|
||||||
"Free up storage space",
|
"Free up storage space", "Open settings menu"};
|
||||||
"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
|
} // namespace
|
||||||
|
|
||||||
void QuickMenuActivity::taskTrampoline(void* param) {
|
void QuickMenuActivity::taskTrampoline(void* param) {
|
||||||
@ -121,7 +113,7 @@ void QuickMenuActivity::render() const {
|
|||||||
|
|
||||||
const auto pageWidth = renderer.getScreenWidth();
|
const auto pageWidth = renderer.getScreenWidth();
|
||||||
const auto pageHeight = renderer.getScreenHeight();
|
const auto pageHeight = renderer.getScreenHeight();
|
||||||
|
|
||||||
// Get bezel offsets
|
// Get bezel offsets
|
||||||
const int bezelTop = renderer.getBezelOffsetTop();
|
const int bezelTop = renderer.getBezelOffsetTop();
|
||||||
const int bezelLeft = renderer.getBezelOffsetLeft();
|
const int bezelLeft = renderer.getBezelOffsetLeft();
|
||||||
@ -160,7 +152,7 @@ void QuickMenuActivity::render() const {
|
|||||||
if (i == 1) {
|
if (i == 1) {
|
||||||
itemText = isPageBookmarked ? "Remove Bookmark" : "Add Bookmark";
|
itemText = isPageBookmarked ? "Remove Bookmark" : "Add Bookmark";
|
||||||
}
|
}
|
||||||
|
|
||||||
renderer.drawText(UI_10_FONT_ID, marginLeft + 20, itemY, itemText, !isSelected);
|
renderer.drawText(UI_10_FONT_ID, marginLeft + 20, itemY, itemText, !isSelected);
|
||||||
renderer.drawText(SMALL_FONT_ID, marginLeft + 20, itemY + 22, descriptions[i], !isSelected);
|
renderer.drawText(SMALL_FONT_ID, marginLeft + 20, itemY + 22, descriptions[i], !isSelected);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,8 +2,9 @@
|
|||||||
* Generated by convert-builtin-fonts.sh
|
* Generated by convert-builtin-fonts.sh
|
||||||
* Custom font definitions
|
* Custom font definitions
|
||||||
*/
|
*/
|
||||||
#include <builtinFonts/custom/customFonts.h>
|
|
||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
|
#include <builtinFonts/custom/customFonts.h>
|
||||||
|
|
||||||
#include "fontIds.h"
|
#include "fontIds.h"
|
||||||
|
|
||||||
// EpdFont definitions for custom fonts
|
// EpdFont definitions for custom fonts
|
||||||
@ -41,14 +42,30 @@ EpdFont fernmicro18BoldFont(&fernmicro_18_bold);
|
|||||||
EpdFont fernmicro18BoldItalicFont(&fernmicro_18_bolditalic);
|
EpdFont fernmicro18BoldItalicFont(&fernmicro_18_bolditalic);
|
||||||
|
|
||||||
// EpdFontFamily definitions for custom fonts
|
// EpdFontFamily definitions for custom fonts
|
||||||
EpdFontFamily atkinsonhyperlegiblenext12FontFamily(&atkinsonhyperlegiblenext12RegularFont, &atkinsonhyperlegiblenext12BoldFont, &atkinsonhyperlegiblenext12ItalicFont, &atkinsonhyperlegiblenext12BoldItalicFont);
|
EpdFontFamily atkinsonhyperlegiblenext12FontFamily(&atkinsonhyperlegiblenext12RegularFont,
|
||||||
EpdFontFamily atkinsonhyperlegiblenext14FontFamily(&atkinsonhyperlegiblenext14RegularFont, &atkinsonhyperlegiblenext14BoldFont, &atkinsonhyperlegiblenext14ItalicFont, &atkinsonhyperlegiblenext14BoldItalicFont);
|
&atkinsonhyperlegiblenext12BoldFont,
|
||||||
EpdFontFamily atkinsonhyperlegiblenext16FontFamily(&atkinsonhyperlegiblenext16RegularFont, &atkinsonhyperlegiblenext16BoldFont, &atkinsonhyperlegiblenext16ItalicFont, &atkinsonhyperlegiblenext16BoldItalicFont);
|
&atkinsonhyperlegiblenext12ItalicFont,
|
||||||
EpdFontFamily atkinsonhyperlegiblenext18FontFamily(&atkinsonhyperlegiblenext18RegularFont, &atkinsonhyperlegiblenext18BoldFont, &atkinsonhyperlegiblenext18ItalicFont, &atkinsonhyperlegiblenext18BoldItalicFont);
|
&atkinsonhyperlegiblenext12BoldItalicFont);
|
||||||
EpdFontFamily fernmicro12FontFamily(&fernmicro12RegularFont, &fernmicro12BoldFont, &fernmicro12ItalicFont, &fernmicro12BoldItalicFont);
|
EpdFontFamily atkinsonhyperlegiblenext14FontFamily(&atkinsonhyperlegiblenext14RegularFont,
|
||||||
EpdFontFamily fernmicro14FontFamily(&fernmicro14RegularFont, &fernmicro14BoldFont, &fernmicro14ItalicFont, &fernmicro14BoldItalicFont);
|
&atkinsonhyperlegiblenext14BoldFont,
|
||||||
EpdFontFamily fernmicro16FontFamily(&fernmicro16RegularFont, &fernmicro16BoldFont, &fernmicro16ItalicFont, &fernmicro16BoldItalicFont);
|
&atkinsonhyperlegiblenext14ItalicFont,
|
||||||
EpdFontFamily fernmicro18FontFamily(&fernmicro18RegularFont, &fernmicro18BoldFont, &fernmicro18ItalicFont, &fernmicro18BoldItalicFont);
|
&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) {
|
void registerCustomFonts(GfxRenderer& renderer) {
|
||||||
#if CUSTOM_FONT_COUNT > 0
|
#if CUSTOM_FONT_COUNT > 0
|
||||||
@ -64,4 +81,3 @@ void registerCustomFonts(GfxRenderer& renderer) {
|
|||||||
(void)renderer; // Suppress unused parameter warning
|
(void)renderer; // Suppress unused parameter warning
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -30,6 +30,7 @@
|
|||||||
// Custom font ID lookup array: CUSTOM_FONT_IDS[fontIndex][sizeIndex]
|
// Custom font ID lookup array: CUSTOM_FONT_IDS[fontIndex][sizeIndex]
|
||||||
// Size indices: 0=12pt, 1=14pt, 2=16pt, 3=18pt
|
// Size indices: 0=12pt, 1=14pt, 2=16pt, 3=18pt
|
||||||
static const int CUSTOM_FONT_IDS[][4] = {
|
static const int CUSTOM_FONT_IDS[][4] = {
|
||||||
{ATKINSONHYPERLEGIBLENEXT_12_FONT_ID, ATKINSONHYPERLEGIBLENEXT_14_FONT_ID, ATKINSONHYPERLEGIBLENEXT_16_FONT_ID, ATKINSONHYPERLEGIBLENEXT_18_FONT_ID},
|
{ATKINSONHYPERLEGIBLENEXT_12_FONT_ID, ATKINSONHYPERLEGIBLENEXT_14_FONT_ID, ATKINSONHYPERLEGIBLENEXT_16_FONT_ID,
|
||||||
{FERNMICRO_12_FONT_ID, FERNMICRO_14_FONT_ID, FERNMICRO_16_FONT_ID, FERNMICRO_18_FONT_ID},
|
ATKINSONHYPERLEGIBLENEXT_18_FONT_ID},
|
||||||
|
{FERNMICRO_12_FONT_ID, FERNMICRO_14_FONT_ID, FERNMICRO_16_FONT_ID, FERNMICRO_18_FONT_ID},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -11,55 +11,175 @@ static constexpr int LOCK_ICON_HEIGHT = 40;
|
|||||||
// Use drawImageRotated() to rotate as needed for different screen orientations
|
// Use drawImageRotated() to rotate as needed for different screen orientations
|
||||||
static const uint8_t LockIcon[] = {
|
static const uint8_t LockIcon[] = {
|
||||||
// Row 0-1: Empty space above shackle
|
// 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
|
// Row 2-3: Shackle top curve
|
||||||
0x00, 0x0F, 0xF0, 0x00, // ....####....
|
0x00,
|
||||||
0x00, 0x3F, 0xFC, 0x00, // ..########..
|
0x0F,
|
||||||
|
0xF0,
|
||||||
|
0x00, // ....####....
|
||||||
|
0x00,
|
||||||
|
0x3F,
|
||||||
|
0xFC,
|
||||||
|
0x00, // ..########..
|
||||||
// Row 4-5: Shackle upper sides
|
// Row 4-5: Shackle upper sides
|
||||||
0x00, 0x78, 0x1E, 0x00, // .####..####.
|
0x00,
|
||||||
0x00, 0xE0, 0x07, 0x00, // ###......###
|
0x78,
|
||||||
|
0x1E,
|
||||||
|
0x00, // .####..####.
|
||||||
|
0x00,
|
||||||
|
0xE0,
|
||||||
|
0x07,
|
||||||
|
0x00, // ###......###
|
||||||
// Row 6-9: Extended shackle legs (longer for better visual)
|
// Row 6-9: Extended shackle legs (longer for better visual)
|
||||||
0x00, 0xC0, 0x03, 0x00, // ##........##
|
0x00,
|
||||||
0x01, 0xC0, 0x03, 0x80, // ###......###
|
0xC0,
|
||||||
0x01, 0x80, 0x01, 0x80, // ##........##
|
0x03,
|
||||||
0x01, 0x80, 0x01, 0x80, // ##........##
|
0x00, // ##........##
|
||||||
|
0x01,
|
||||||
|
0xC0,
|
||||||
|
0x03,
|
||||||
|
0x80, // ###......###
|
||||||
|
0x01,
|
||||||
|
0x80,
|
||||||
|
0x01,
|
||||||
|
0x80, // ##........##
|
||||||
|
0x01,
|
||||||
|
0x80,
|
||||||
|
0x01,
|
||||||
|
0x80, // ##........##
|
||||||
// Row 10-13: Shackle legs continue into body
|
// Row 10-13: Shackle legs continue into body
|
||||||
0x01, 0x80, 0x01, 0x80, // ##........##
|
0x01,
|
||||||
0x01, 0x80, 0x01, 0x80, // ##........##
|
0x80,
|
||||||
0x01, 0x80, 0x01, 0x80, // ##........##
|
0x01,
|
||||||
0x01, 0x80, 0x01, 0x80, // ##........##
|
0x80, // ##........##
|
||||||
|
0x01,
|
||||||
|
0x80,
|
||||||
|
0x01,
|
||||||
|
0x80, // ##........##
|
||||||
|
0x01,
|
||||||
|
0x80,
|
||||||
|
0x01,
|
||||||
|
0x80, // ##........##
|
||||||
|
0x01,
|
||||||
|
0x80,
|
||||||
|
0x01,
|
||||||
|
0x80, // ##........##
|
||||||
// Row 14-15: Body top
|
// Row 14-15: Body top
|
||||||
0x0F, 0xFF, 0xFF, 0xF0, // ############
|
0x0F,
|
||||||
0x1F, 0xFF, 0xFF, 0xF8, // ##############
|
0xFF,
|
||||||
|
0xFF,
|
||||||
|
0xF0, // ############
|
||||||
|
0x1F,
|
||||||
|
0xFF,
|
||||||
|
0xFF,
|
||||||
|
0xF8, // ##############
|
||||||
// Row 16-17: Body top edge
|
// Row 16-17: Body top edge
|
||||||
0x3F, 0xFF, 0xFF, 0xFC, // ################
|
0x3F,
|
||||||
0x3F, 0xFF, 0xFF, 0xFC, // ################
|
0xFF,
|
||||||
|
0xFF,
|
||||||
|
0xFC, // ################
|
||||||
|
0x3F,
|
||||||
|
0xFF,
|
||||||
|
0xFF,
|
||||||
|
0xFC, // ################
|
||||||
// Row 18-29: Solid body (no keyhole)
|
// Row 18-29: Solid body (no keyhole)
|
||||||
0x3F, 0xFF, 0xFF, 0xFC,
|
0x3F,
|
||||||
0x3F, 0xFF, 0xFF, 0xFC,
|
0xFF,
|
||||||
0x3F, 0xFF, 0xFF, 0xFC,
|
0xFF,
|
||||||
0x3F, 0xFF, 0xFF, 0xFC,
|
0xFC,
|
||||||
0x3F, 0xFF, 0xFF, 0xFC,
|
0x3F,
|
||||||
0x3F, 0xFF, 0xFF, 0xFC,
|
0xFF,
|
||||||
0x3F, 0xFF, 0xFF, 0xFC,
|
0xFF,
|
||||||
0x3F, 0xFF, 0xFF, 0xFC,
|
0xFC,
|
||||||
0x3F, 0xFF, 0xFF, 0xFC,
|
0x3F,
|
||||||
0x3F, 0xFF, 0xFF, 0xFC,
|
0xFF,
|
||||||
0x3F, 0xFF, 0xFF, 0xFC,
|
0xFF,
|
||||||
0x3F, 0xFF, 0xFF, 0xFC,
|
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
|
// Row 30-33: Body lower section
|
||||||
0x3F, 0xFF, 0xFF, 0xFC,
|
0x3F,
|
||||||
0x3F, 0xFF, 0xFF, 0xFC,
|
0xFF,
|
||||||
0x3F, 0xFF, 0xFF, 0xFC,
|
0xFF,
|
||||||
0x3F, 0xFF, 0xFF, 0xFC,
|
0xFC,
|
||||||
|
0x3F,
|
||||||
|
0xFF,
|
||||||
|
0xFF,
|
||||||
|
0xFC,
|
||||||
|
0x3F,
|
||||||
|
0xFF,
|
||||||
|
0xFF,
|
||||||
|
0xFC,
|
||||||
|
0x3F,
|
||||||
|
0xFF,
|
||||||
|
0xFF,
|
||||||
|
0xFC,
|
||||||
// Row 34-35: Body bottom edge
|
// Row 34-35: Body bottom edge
|
||||||
0x3F, 0xFF, 0xFF, 0xFC,
|
0x3F,
|
||||||
0x1F, 0xFF, 0xFF, 0xF8,
|
0xFF,
|
||||||
|
0xFF,
|
||||||
|
0xFC,
|
||||||
|
0x1F,
|
||||||
|
0xFF,
|
||||||
|
0xFF,
|
||||||
|
0xF8,
|
||||||
// Row 36-37: Body bottom
|
// Row 36-37: Body bottom
|
||||||
0x0F, 0xFF, 0xFF, 0xF0,
|
0x0F,
|
||||||
0x00, 0x00, 0x00, 0x00,
|
0xFF,
|
||||||
|
0xFF,
|
||||||
|
0xF0,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
// Row 38-39: Empty space below
|
// Row 38-39: Empty space below
|
||||||
0x00, 0x00, 0x00, 0x00,
|
0x00,
|
||||||
0x00, 0x00, 0x00, 0x00,
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
};
|
};
|
||||||
|
|||||||
51
src/main.cpp
51
src/main.cpp
@ -1,4 +1,5 @@
|
|||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
|
#include <BitmapHelpers.h>
|
||||||
#include <EInkDisplay.h>
|
#include <EInkDisplay.h>
|
||||||
#include <Epub.h>
|
#include <Epub.h>
|
||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
@ -10,8 +11,6 @@
|
|||||||
|
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
|
||||||
#include <BitmapHelpers.h>
|
|
||||||
|
|
||||||
#include "Battery.h"
|
#include "Battery.h"
|
||||||
#include "BookListStore.h"
|
#include "BookListStore.h"
|
||||||
#include "CrossPointSettings.h"
|
#include "CrossPointSettings.h"
|
||||||
@ -123,11 +122,8 @@ unsigned long t2 = 0;
|
|||||||
// Memory debugging helper - logs heap state for tracking leaks
|
// Memory debugging helper - logs heap state for tracking leaks
|
||||||
#ifdef DEBUG_MEMORY
|
#ifdef DEBUG_MEMORY
|
||||||
void logMemoryState(const char* tag, const char* context) {
|
void logMemoryState(const char* tag, const char* context) {
|
||||||
Serial.printf("[%lu] [%s] [MEM] %s - Free: %d, Largest: %d, MinFree: %d\n",
|
Serial.printf("[%lu] [%s] [MEM] %s - Free: %d, Largest: %d, MinFree: %d\n", millis(), tag, context, ESP.getFreeHeap(),
|
||||||
millis(), tag, context,
|
ESP.getMaxAllocHeap(), ESP.getMinFreeHeap());
|
||||||
ESP.getFreeHeap(),
|
|
||||||
ESP.getMaxAllocHeap(),
|
|
||||||
ESP.getMinFreeHeap());
|
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
// No-op when not in debug mode
|
// No-op when not in debug mode
|
||||||
@ -196,7 +192,7 @@ void checkForFlashCommand() {
|
|||||||
// USB port locations: Portrait=bottom-left, PortraitInverted=top-right,
|
// USB port locations: Portrait=bottom-left, PortraitInverted=top-right,
|
||||||
// LandscapeCW=top-left, LandscapeCCW=bottom-right
|
// LandscapeCW=top-left, LandscapeCCW=bottom-right
|
||||||
// Position offsets: edge margin + half-width offset to center on USB port
|
// 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
|
constexpr int halfWidth = LOCK_ICON_WIDTH / 2; // 16px offset for centering
|
||||||
int iconX, iconY;
|
int iconX, iconY;
|
||||||
GfxRenderer::ImageRotation rotation;
|
GfxRenderer::ImageRotation rotation;
|
||||||
@ -334,9 +330,8 @@ void onGoToClearCache();
|
|||||||
void onGoToSettings();
|
void onGoToSettings();
|
||||||
void onGoToReader(const std::string& initialEpubPath, MyLibraryActivity::Tab fromTab) {
|
void onGoToReader(const std::string& initialEpubPath, MyLibraryActivity::Tab fromTab) {
|
||||||
exitActivity();
|
exitActivity();
|
||||||
enterNewActivity(
|
enterNewActivity(new ReaderActivity(renderer, mappedInputManager, initialEpubPath, fromTab, onGoHome,
|
||||||
new ReaderActivity(renderer, mappedInputManager, initialEpubPath, fromTab, onGoHome, onGoToMyLibraryWithTab,
|
onGoToMyLibraryWithTab, onGoToClearCache, onGoToSettings));
|
||||||
onGoToClearCache, onGoToSettings));
|
|
||||||
}
|
}
|
||||||
void onContinueReading() { onGoToReader(APP_STATE.openEpubPath, MyLibraryActivity::Tab::Recent); }
|
void onContinueReading() { onGoToReader(APP_STATE.openEpubPath, MyLibraryActivity::Tab::Recent); }
|
||||||
|
|
||||||
@ -351,8 +346,7 @@ void onGoToReaderFromList(const std::string& bookPath) {
|
|||||||
// View a specific list
|
// View a specific list
|
||||||
void onGoToListView(const std::string& listName) {
|
void onGoToListView(const std::string& listName) {
|
||||||
exitActivity();
|
exitActivity();
|
||||||
enterNewActivity(
|
enterNewActivity(new ListViewActivity(renderer, mappedInputManager, listName, onGoToMyLibrary, onGoToReaderFromList));
|
||||||
new ListViewActivity(renderer, mappedInputManager, listName, onGoToMyLibrary, onGoToReaderFromList));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// View bookmarks for a specific book
|
// 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
|
// Navigate to bookmark location in the book
|
||||||
// For now, just open the book (TODO: pass bookmark location to reader)
|
// For now, just open the book (TODO: pass bookmark location to reader)
|
||||||
exitActivity();
|
exitActivity();
|
||||||
enterNewActivity(new ReaderActivity(renderer, mappedInputManager, bookPath,
|
enterNewActivity(new ReaderActivity(renderer, mappedInputManager, bookPath, MyLibraryActivity::Tab::Bookmarks,
|
||||||
MyLibraryActivity::Tab::Bookmarks, onGoHome, onGoToMyLibraryWithTab,
|
onGoHome, onGoToMyLibraryWithTab, onGoToClearCache, onGoToSettings));
|
||||||
onGoToClearCache, onGoToSettings));
|
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -402,12 +395,14 @@ void onGoToClearCache() {
|
|||||||
|
|
||||||
void onGoToMyLibrary() {
|
void onGoToMyLibrary() {
|
||||||
exitActivity();
|
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) {
|
void onGoToMyLibraryWithTab(const std::string& path, MyLibraryActivity::Tab tab) {
|
||||||
exitActivity();
|
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() {
|
void onGoToBrowser() {
|
||||||
@ -462,10 +457,14 @@ bool isWakeupByPowerButton() {
|
|||||||
void setup() {
|
void setup() {
|
||||||
t1 = millis();
|
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);
|
pinMode(UART0_RXD, INPUT);
|
||||||
if (isUsbConnected()) {
|
if (isUsbConnected()) {
|
||||||
Serial.begin(115200);
|
|
||||||
// Wait up to 3 seconds for Serial to be ready to catch early logs
|
// Wait up to 3 seconds for Serial to be ready to catch early logs
|
||||||
unsigned long start = millis();
|
unsigned long start = millis();
|
||||||
while (!Serial && (millis() - start) < 3000) {
|
while (!Serial && (millis() - start) < 3000) {
|
||||||
@ -542,14 +541,13 @@ void loop() {
|
|||||||
// Basic heap info
|
// Basic heap info
|
||||||
Serial.printf("[%lu] [MEM] Free: %d bytes, Total: %d bytes, Min Free: %d bytes\n", millis(), ESP.getFreeHeap(),
|
Serial.printf("[%lu] [MEM] Free: %d bytes, Total: %d bytes, Min Free: %d bytes\n", millis(), ESP.getFreeHeap(),
|
||||||
ESP.getHeapSize(), ESP.getMinFreeHeap());
|
ESP.getHeapSize(), ESP.getMinFreeHeap());
|
||||||
|
|
||||||
// Detailed fragmentation info using ESP-IDF heap caps API
|
// Detailed fragmentation info using ESP-IDF heap caps API
|
||||||
multi_heap_info_t info;
|
multi_heap_info_t info;
|
||||||
heap_caps_get_info(&info, MALLOC_CAP_8BIT);
|
heap_caps_get_info(&info, MALLOC_CAP_8BIT);
|
||||||
Serial.printf("[%lu] [HEAP] Largest: %d, Allocated: %d, Blocks: %d, Free blocks: %d\n", millis(),
|
Serial.printf("[%lu] [HEAP] Largest: %d, Allocated: %d, Blocks: %d, Free blocks: %d\n", millis(),
|
||||||
info.largest_free_block, info.total_allocated_bytes,
|
info.largest_free_block, info.total_allocated_bytes, info.allocated_blocks, info.free_blocks);
|
||||||
info.allocated_blocks, info.free_blocks);
|
|
||||||
|
|
||||||
lastMemPrint = millis();
|
lastMemPrint = millis();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -562,7 +560,8 @@ void loop() {
|
|||||||
|
|
||||||
const unsigned long sleepTimeoutMs = SETTINGS.getSleepTimeoutMs();
|
const unsigned long sleepTimeoutMs = SETTINGS.getSleepTimeoutMs();
|
||||||
if (millis() - lastActivityTime >= sleepTimeoutMs) {
|
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();
|
enterDeepSleep();
|
||||||
// This should never be hit as `enterDeepSleep` calls esp_deep_sleep_start
|
// This should never be hit as `enterDeepSleep` calls esp_deep_sleep_start
|
||||||
return;
|
return;
|
||||||
@ -584,7 +583,7 @@ void loop() {
|
|||||||
const unsigned long loopDuration = millis() - loopStartTime;
|
const unsigned long loopDuration = millis() - loopStartTime;
|
||||||
if (loopDuration > maxLoopDuration) {
|
if (loopDuration > maxLoopDuration) {
|
||||||
maxLoopDuration = loopDuration;
|
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,
|
Serial.printf("[%lu] [LOOP] New max loop duration: %lu ms (activity: %lu ms)\n", millis(), maxLoopDuration,
|
||||||
activityDuration);
|
activityDuration);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user