Merge staging: ef-1.0.1 and ef-1.0.2
Some checks failed
Compile Release / build-release (push) Successful in 1m22s
CI / build (push) Failing after 1m38s

ef-1.0.2 - Quick Menu Enhancements
- Screen rotation toggle (Portrait/Landscape CCW)
- Customizable menu order with pick-and-place reordering
- Navigation button hints on quick menu

ef-1.0.1 - Dictionary Stability & UX
- Fixed dictionary crashes from heap fragmentation
- Refactored TextBlock/ParsedText to std::vector (~12x fewer allocations)
- Uncompressed .dict support, chunked HTML parsing
- Orientation-aware button hints on dictionary screens
This commit is contained in:
cottongin 2026-01-29 13:09:57 -05:00
commit d5e42b9e40
No known key found for this signature in database
GPG Key ID: 0ECC91FE4655C262
6 changed files with 331 additions and 32 deletions

105
ef-CHANGELOG.md Normal file
View File

@ -0,0 +1,105 @@
# crosspoint-ef Changelog
All notable changes to the crosspoint-ef fork are documented here.
Base: CrossPoint Reader 0.15.0
---
## ef-1.0.2
**Quick Menu Enhancements**
### New Features
- **Screen Rotation Toggle**: Quick toggle between Portrait and Landscape CCW directly from the quick menu
- Automatically reindexes content for new screen dimensions
- Preserves reading position via content offset restoration
- **Customizable Menu Order**: Reorder quick menu items to your preference
- New "Edit List Order" option at bottom of menu
- Pick-and-place reordering: select item, navigate to destination, place
- Order persists across sessions
### UI Improvements
- Added navigation button hints to quick menu (prev/next on front buttons, up/down on side buttons)
- Fixed orientation-aware margins for button hint areas in landscape modes
- New default menu order: Bookmark, Dictionary, Rotate Screen, Settings, Clear Cache
---
## ef-1.0.1
**Dictionary Stability & UX Improvements**
### Bug Fixes - Stability
- Fixed dictionary crashes caused by heap fragmentation from repeated page navigation
- Refactored TextBlock/ParsedText from `std::list` to `std::vector`, reducing heap allocations by ~12x per TextBlock
- Affects EPUB reader page rendering, dictionary definition display, and word selection
- Contiguous memory improves cache locality during text layout and reduces heap fragmentation on the memory-constrained ESP32
- Added uncompressed dictionary (`.dict`) support to avoid decompression memory issues with large dictzip chunks (58KB chunks -> direct read)
- Implemented chunked on-demand HTML parsing for large definitions, parsing pages as user navigates rather than all at once
- Limited cached pages to 4 with re-parse capability for backward navigation beyond cache window
- Fixed double-button press bug when loading new dictionary chunks
### Bug Fixes - UI/Layout
- Restored proper orientation-aware button hint spacing (front: 45px, side: 50px)
- Added side button hints to definition screen with "<" / ">" labels for page navigation
- Added side button hints to word selection screen ("UP"/"DOWN" labels, borderless, small font)
- Added side button hints to dictionary menu ("< Prev", "Next >")
- Moved page indicator up to avoid bezel cutoff in landscape orientations
---
## ef-1.0.0
**First Official Release** (previously ef-0.15.99)
First milestone release of the crosspoint-ef fork, building on CrossPoint Reader 0.15.0 with 14+ major new features and enhancements.
### New Features
- **Dictionary Support**: Offline StarDict dictionary with word selection from reader, fast prefix-indexed search, rich HTML formatting, and multi-page pagination
- **Bookmark System**: Per-book bookmarks with visual folded-corner indicators, dedicated management interface, and auto-generated bookmark names
- **Quick Menu**: In-reader quick access menu for common actions (Dictionary, Bookmark, Clear Cache, Settings) via short power button press
- **Library Search**: Search across all books by title, author, or filename with dynamic character picker and weighted relevance scoring
- **CSS Support**: Parse and apply CSS styles from EPUB stylesheets (text-align, font-style, font-weight, text-decoration, margins, padding)
- **Inline Image Support**: PNG and Baseline JPEG rendering within EPUB content with 2-bit grayscale dithering and caching
- **Custom Fonts**: Atkinson Hyperlegible Next (low-vision readers) and Fern Micro (small screens)
- **Enhanced Web Server**: File management (upload, download, delete, rename, copy, move, mkdir), companion app API, WebSocket uploads, mDNS discovery at `crosspoint.local`
- **Reading Lists**: Create, manage, and pin custom book lists with web API support (CSV format)
- **Enhanced Tab Bar**: Unified tab bar with horizontal scrolling and overflow indicators (Recent, Lists, Bookmarks, Search, Files)
- **Progress Bar Status**: Additional status bar option showing visual reading progress
- **OPDS Browser Enhancements**: Navigation history, page skipping (hold Up/Down), error retry, HTTP Basic Auth support
### Display Enhancements
- **High Contrast Mode**: System-wide contrast adjustment
- **Bezel Compensation**: Configurable margin (0-10px) for physical screen edge defects
- **Sleep Screen Improvements**: Edge-aware color filling for seamless letterbox appearance
### Bug Fixes
- Fixed device hanging when booted without USB connected (Serial.available()/Serial.read() called without Serial.begin())
- Fixed grayscale state corruption causing ghosting artifacts when anti-aliasing enabled under memory pressure
- Memory optimization with graceful degradation when memory is low
### Development Tools
- `pre_flash.py`: Displays "Flashing firmware..." screen during upload
- `debugging_monitor.py`: Enhanced serial monitor with memory graphs
- `pio_helper.py`: Interactive PlatformIO workflow helper
---
## Differences from Upstream 0.16.0
This fork is based on upstream 0.15.0. The following 0.16.0 features are not included:
- KOReader sync support
- Non-English hyphenation patterns (Spanish, German, French, Russian)
- XTC/XTCH file format support
See [crosspoint-ef-features.md](docs/crosspoint-ef-features.md) for complete feature documentation.

View File

@ -2,7 +2,8 @@
default_envs = default default_envs = default
[crosspoint] [crosspoint]
version = ef-0.15.99 # 0.15.0 CrossPoint base, ef-1.0.0 is the first release of the ef branch
version = 0.15.ef-1.0.2
[base] [base]
platform = espressif32 @ 6.12.0 platform = espressif32 @ 6.12.0

View File

@ -155,6 +155,11 @@ class CrossPointSettings {
// Pinned list name (empty = none pinned) // Pinned list name (empty = none pinned)
char pinnedListName[64] = ""; char pinnedListName[64] = "";
// Quick menu item order (indices 0-4 representing the 5 menu items)
// Maps to QuickMenuAction enum: 0=Dictionary, 1=Bookmark, 2=ClearCache, 3=Orientation, 4=Settings
// Default order: Bookmark(1), Dictionary(0), Orientation(3), Settings(4), ClearCache(2)
uint8_t quickMenuOrder[5] = {1, 0, 3, 4, 2};
~CrossPointSettings() = default; ~CrossPointSettings() = default;
// Get singleton instance // Get singleton instance

View File

@ -496,6 +496,28 @@ void EpubReaderActivity::loop() {
self->onGoToClearCache(); self->onGoToClearCache();
return; return;
} }
self->updateRequired = true;
} else if (action == QuickMenuAction::TOGGLE_ORIENTATION) {
// Toggle between Portrait and Landscape CCW
if (SETTINGS.orientation == CrossPointSettings::ORIENTATION::PORTRAIT) {
SETTINGS.orientation = CrossPointSettings::ORIENTATION::LANDSCAPE_CCW;
} else {
SETTINGS.orientation = CrossPointSettings::ORIENTATION::PORTRAIT;
}
SETTINGS.saveToFile();
// Apply new orientation to renderer
if (SETTINGS.orientation == CrossPointSettings::ORIENTATION::PORTRAIT) {
self->renderer.setOrientation(GfxRenderer::Orientation::Portrait);
} else {
self->renderer.setOrientation(GfxRenderer::Orientation::LandscapeCounterClockwise);
}
// Force section reload with new orientation's viewport dimensions
xSemaphoreTake(cachedMutex, portMAX_DELAY);
self->section.reset();
xSemaphoreGive(cachedMutex);
self->updateRequired = true; self->updateRequired = true;
} else if (action == QuickMenuAction::GO_TO_SETTINGS) { } else if (action == QuickMenuAction::GO_TO_SETTINGS) {
// Navigate to Settings activity // Navigate to Settings activity

View File

@ -2,16 +2,25 @@
#include <GfxRenderer.h> #include <GfxRenderer.h>
#include "CrossPointSettings.h"
#include "MappedInputManager.h" #include "MappedInputManager.h"
#include "fontIds.h" #include "fontIds.h"
namespace { namespace {
constexpr int MENU_ITEM_COUNT = 4; // Base menu item count (reorderable items)
const char* MENU_ITEMS[MENU_ITEM_COUNT] = {"Dictionary", "Bookmark", "Clear Cache", "Settings"}; constexpr int BASE_MENU_ITEM_COUNT = 5;
const char* MENU_DESCRIPTIONS_ADD[MENU_ITEM_COUNT] = {"Look up a word", "Add bookmark to this page", // Total display count including "Edit List Order"
"Free up storage space", "Open settings menu"}; constexpr int DISPLAY_ITEM_COUNT = 6;
const char* MENU_DESCRIPTIONS_REMOVE[MENU_ITEM_COUNT] = {"Look up a word", "Remove bookmark from this page",
"Free up storage space", "Open settings menu"}; // Menu items indexed by QuickMenuAction enum value
// 0=Dictionary, 1=Bookmark, 2=ClearCache, 3=Orientation, 4=Settings
const char* MENU_ITEMS[BASE_MENU_ITEM_COUNT] = {"Dictionary", "Bookmark", "Clear Cache", "Rotate Screen", "Settings"};
const char* MENU_DESCRIPTIONS_ADD[BASE_MENU_ITEM_COUNT] = {"Look up a word", "Add bookmark to this page",
"Free up storage space", "Toggle screen orientation",
"Open settings menu"};
const char* MENU_DESCRIPTIONS_REMOVE[BASE_MENU_ITEM_COUNT] = {"Look up a word", "Remove bookmark from this page",
"Free up storage space", "Toggle screen orientation",
"Open settings menu"};
} // namespace } // namespace
void QuickMenuActivity::taskTrampoline(void* param) { void QuickMenuActivity::taskTrampoline(void* param) {
@ -53,6 +62,16 @@ void QuickMenuActivity::onExit() {
} }
void QuickMenuActivity::loop() { void QuickMenuActivity::loop() {
if (editMode) {
// Edit mode logic
handleEditMode();
} else {
// Normal mode logic
handleNormalMode();
}
}
void QuickMenuActivity::handleNormalMode() {
// Handle back button - cancel // Handle back button - cancel
if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
onCancel(); onCancel();
@ -61,8 +80,22 @@ void QuickMenuActivity::loop() {
// Handle confirm button - select current option // Handle confirm button - select current option
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
// Last item is "Edit List Order"
if (selectedIndex == DISPLAY_ITEM_COUNT - 1) {
// Enter edit mode - copy current order to local buffer
for (int i = 0; i < BASE_MENU_ITEM_COUNT; i++) {
localOrder[i] = SETTINGS.quickMenuOrder[i];
}
editMode = true;
selectedIndex = 0; // Start at first item in edit mode
updateRequired = true;
return;
}
// Get the action from the order array
const int actionIndex = SETTINGS.quickMenuOrder[selectedIndex];
QuickMenuAction action; QuickMenuAction action;
switch (selectedIndex) { switch (actionIndex) {
case 0: case 0:
action = QuickMenuAction::DICTIONARY; action = QuickMenuAction::DICTIONARY;
break; break;
@ -73,6 +106,9 @@ void QuickMenuActivity::loop() {
action = QuickMenuAction::CLEAR_CACHE; action = QuickMenuAction::CLEAR_CACHE;
break; break;
case 3: case 3:
action = QuickMenuAction::TOGGLE_ORIENTATION;
break;
case 4:
default: default:
action = QuickMenuAction::GO_TO_SETTINGS; action = QuickMenuAction::GO_TO_SETTINGS;
break; break;
@ -88,10 +124,69 @@ void QuickMenuActivity::loop() {
mappedInput.wasPressed(MappedInputManager::Button::Right); mappedInput.wasPressed(MappedInputManager::Button::Right);
if (prevPressed) { if (prevPressed) {
selectedIndex = (selectedIndex + MENU_ITEM_COUNT - 1) % MENU_ITEM_COUNT; selectedIndex = (selectedIndex + DISPLAY_ITEM_COUNT - 1) % DISPLAY_ITEM_COUNT;
updateRequired = true; updateRequired = true;
} else if (nextPressed) { } else if (nextPressed) {
selectedIndex = (selectedIndex + 1) % MENU_ITEM_COUNT; selectedIndex = (selectedIndex + 1) % DISPLAY_ITEM_COUNT;
updateRequired = true;
}
}
void QuickMenuActivity::handleEditMode() {
// Handle back button - save and exit edit mode
if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
// Save the local order to settings
for (int i = 0; i < BASE_MENU_ITEM_COUNT; i++) {
SETTINGS.quickMenuOrder[i] = localOrder[i];
}
SETTINGS.saveToFile();
editMode = false;
movingIndex = -1;
selectedIndex = DISPLAY_ITEM_COUNT - 1; // Select "Edit List Order" when exiting
updateRequired = true;
return;
}
// Handle confirm button - pick or place item
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
if (movingIndex < 0) {
// No item selected yet - pick up the current item
movingIndex = selectedIndex;
} else {
// Item is being moved - place it at the current position
if (movingIndex != selectedIndex) {
// Remove item from old position and insert at new position
const uint8_t movingItem = localOrder[movingIndex];
if (movingIndex < selectedIndex) {
// Moving down - shift items up
for (int i = movingIndex; i < selectedIndex; i++) {
localOrder[i] = localOrder[i + 1];
}
} else {
// Moving up - shift items down
for (int i = movingIndex; i > selectedIndex; i--) {
localOrder[i] = localOrder[i - 1];
}
}
localOrder[selectedIndex] = movingItem;
}
movingIndex = -1; // Deselect
}
updateRequired = true;
return;
}
// Handle navigation - just move cursor
const bool prevPressed = mappedInput.wasPressed(MappedInputManager::Button::Up) ||
mappedInput.wasPressed(MappedInputManager::Button::Left);
const bool nextPressed = mappedInput.wasPressed(MappedInputManager::Button::Down) ||
mappedInput.wasPressed(MappedInputManager::Button::Right);
if (prevPressed && selectedIndex > 0) {
selectedIndex--;
updateRequired = true;
} else if (nextPressed && selectedIndex < BASE_MENU_ITEM_COUNT - 1) {
selectedIndex++;
updateRequired = true; updateRequired = true;
} }
} }
@ -120,46 +215,110 @@ void QuickMenuActivity::render() const {
const int bezelRight = renderer.getBezelOffsetRight(); const int bezelRight = renderer.getBezelOffsetRight();
const int bezelBottom = renderer.getBezelOffsetBottom(); const int bezelBottom = renderer.getBezelOffsetBottom();
// Calculate usable content area // Button hint space constants
const int marginLeft = 20 + bezelLeft; constexpr int FRONT_BUTTON_SPACE = 45; // 40px button height + 5px padding
const int marginRight = 20 + bezelRight; constexpr int SIDE_BUTTON_SPACE = 50; // 45px button area + 5px padding
const int marginTop = 15 + bezelTop;
const int contentWidth = pageWidth - marginLeft - marginRight;
const int contentHeight = pageHeight - marginTop - 60 - bezelBottom; // 60 for button hints
// Draw header // Calculate button hint margins based on orientation
renderer.drawCenteredText(UI_12_FONT_ID, marginTop, "Quick Menu", true, EpdFontFamily::BOLD); // Physical button locations (fixed on device):
// - Front buttons: physical bottom in portrait
// - Side buttons: physical right in portrait
// These map to different logical edges depending on orientation
int frontBtnMarginTop = 0, frontBtnMarginBottom = 0, frontBtnMarginLeft = 0, frontBtnMarginRight = 0;
int sideBtnMarginTop = 0, sideBtnMarginBottom = 0, sideBtnMarginLeft = 0, sideBtnMarginRight = 0;
switch (renderer.getOrientation()) {
case GfxRenderer::Portrait:
// Front buttons at logical BOTTOM, Side buttons at logical RIGHT
frontBtnMarginBottom = FRONT_BUTTON_SPACE;
sideBtnMarginRight = SIDE_BUTTON_SPACE;
break;
case GfxRenderer::LandscapeClockwise:
// Front buttons at logical LEFT, Side buttons at logical BOTTOM
frontBtnMarginLeft = FRONT_BUTTON_SPACE;
sideBtnMarginBottom = SIDE_BUTTON_SPACE;
break;
case GfxRenderer::PortraitInverted:
// Front buttons at logical TOP, Side buttons at logical LEFT
frontBtnMarginTop = FRONT_BUTTON_SPACE;
sideBtnMarginLeft = SIDE_BUTTON_SPACE;
break;
case GfxRenderer::LandscapeCounterClockwise:
// Front buttons at logical RIGHT, Side buttons at logical TOP
frontBtnMarginRight = FRONT_BUTTON_SPACE;
sideBtnMarginTop = SIDE_BUTTON_SPACE;
break;
}
// Calculate usable content area with bezel and button hint margins
const int marginLeft = 20 + bezelLeft + frontBtnMarginLeft + sideBtnMarginLeft;
const int marginRight = 20 + bezelRight + frontBtnMarginRight + sideBtnMarginRight;
const int marginTop = 15 + bezelTop + frontBtnMarginTop + sideBtnMarginTop;
const int marginBottom = 15 + bezelBottom + frontBtnMarginBottom + sideBtnMarginBottom;
const int contentWidth = pageWidth - marginLeft - marginRight;
const int contentHeight = pageHeight - marginTop - marginBottom;
// Draw header - different text in edit mode
const char* headerText = editMode ? "Edit Menu Order" : "Quick Menu";
renderer.drawCenteredText(UI_12_FONT_ID, marginTop, headerText, true, EpdFontFamily::BOLD);
// Select descriptions based on bookmark state // Select descriptions based on bookmark state
const char* const* descriptions = isPageBookmarked ? MENU_DESCRIPTIONS_REMOVE : MENU_DESCRIPTIONS_ADD; const char* const* descriptions = isPageBookmarked ? MENU_DESCRIPTIONS_REMOVE : MENU_DESCRIPTIONS_ADD;
// Get the order array to use (local copy in edit mode, settings otherwise)
const uint8_t* order = editMode ? localOrder : SETTINGS.quickMenuOrder;
// Draw menu items centered in content area // Draw menu items centered in content area
constexpr int itemHeight = 50; // Height for each menu item (including description) constexpr int itemHeight = 50; // Height for each menu item (including description)
const int startY = marginTop + (contentHeight - (MENU_ITEM_COUNT * itemHeight)) / 2; const int startY = marginTop + (contentHeight - (DISPLAY_ITEM_COUNT * itemHeight)) / 2;
for (int i = 0; i < MENU_ITEM_COUNT; i++) { for (int i = 0; i < DISPLAY_ITEM_COUNT; i++) {
const int itemY = startY + i * itemHeight; const int itemY = startY + i * itemHeight;
const bool isSelected = (i == selectedIndex); const bool isSelected = (i == selectedIndex);
const bool isBeingMoved = (editMode && i == movingIndex);
// Draw selection highlight (black fill) for selected item // Draw selection highlight (black fill) for selected item
if (isSelected) { if (isSelected) {
renderer.fillRect(marginLeft + 10, itemY - 2, contentWidth - 20, itemHeight - 6); renderer.fillRect(marginLeft + 10, itemY - 2, contentWidth - 20, itemHeight - 6);
} }
// Draw outline for item being moved (when cursor is elsewhere)
// Draw menu item text if (isBeingMoved && !isSelected) {
const char* itemText = MENU_ITEMS[i]; renderer.drawRect(marginLeft + 10, itemY - 2, contentWidth - 20, itemHeight - 6);
// For bookmark item, show different text based on state
if (i == 1) {
itemText = isPageBookmarked ? "Remove Bookmark" : "Add Bookmark";
} }
renderer.drawText(UI_10_FONT_ID, marginLeft + 20, itemY, itemText, !isSelected); // Last item is always "Edit List Order" (fixed, not in the order array)
renderer.drawText(SMALL_FONT_ID, marginLeft + 20, itemY + 22, descriptions[i], !isSelected); if (i == DISPLAY_ITEM_COUNT - 1) {
renderer.drawText(UI_10_FONT_ID, marginLeft + 20, itemY, "- Edit List Order -", !isSelected);
renderer.drawText(SMALL_FONT_ID, marginLeft + 20, itemY + 22, "Customize menu order", !isSelected);
} else {
// Get the action index from the order array
const int actionIndex = order[i];
// Draw menu item text - add indicator for item being moved
const char* itemText = MENU_ITEMS[actionIndex];
// For bookmark item (action index 1), show different text based on state
if (actionIndex == 1) {
itemText = isPageBookmarked ? "Remove Bookmark" : "Add Bookmark";
}
renderer.drawText(UI_10_FONT_ID, marginLeft + 20, itemY, itemText, !isSelected);
renderer.drawText(SMALL_FONT_ID, marginLeft + 20, itemY + 22, descriptions[actionIndex], !isSelected);
}
} }
// Draw help text at bottom // Draw help text at bottom - different hints for edit mode
const auto labels = mappedInput.mapLabels("\xc2\xab Back", "Select", "", ""); if (editMode) {
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); const char* confirmLabel = (movingIndex < 0) ? "Pick" : "Place";
const auto labels = mappedInput.mapLabels("\xc2\xab Done", confirmLabel, "", "");
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
// Side button hints for navigation
renderer.drawSideButtonHints(UI_10_FONT_ID, "<", ">");
} else {
const auto labels = mappedInput.mapLabels("\xc2\xab Back", "Select", "< Prev", "Next >");
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
// Side button hints for up/down navigation
renderer.drawSideButtonHints(UI_10_FONT_ID, "<", ">");
}
renderer.displayBuffer(); renderer.displayBuffer();
} }

View File

@ -8,7 +8,7 @@
#include "../Activity.h" #include "../Activity.h"
// Enum for quick menu selection // Enum for quick menu selection
enum class QuickMenuAction { DICTIONARY, ADD_BOOKMARK, CLEAR_CACHE, GO_TO_SETTINGS }; enum class QuickMenuAction { DICTIONARY, ADD_BOOKMARK, CLEAR_CACHE, TOGGLE_ORIENTATION, GO_TO_SETTINGS };
/** /**
* QuickMenuActivity presents a quick access menu triggered by short power button press. * QuickMenuActivity presents a quick access menu triggered by short power button press.
@ -28,9 +28,16 @@ class QuickMenuActivity final : public Activity {
const std::function<void()> onCancel; const std::function<void()> onCancel;
const bool isPageBookmarked; // True if current page already has a bookmark const bool isPageBookmarked; // True if current page already has a bookmark
// Edit mode state
bool editMode = false; // True when in edit mode
int movingIndex = -1; // Index of item being moved (-1 if none)
uint8_t localOrder[5] = {0}; // Local copy of order for editing
static void taskTrampoline(void* param); static void taskTrampoline(void* param);
[[noreturn]] void displayTaskLoop(); [[noreturn]] void displayTaskLoop();
void render() const; void render() const;
void handleNormalMode();
void handleEditMode();
public: public:
explicit QuickMenuActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, explicit QuickMenuActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,