diff --git a/ef-CHANGELOG.md b/ef-CHANGELOG.md new file mode 100644 index 0000000..097d560 --- /dev/null +++ b/ef-CHANGELOG.md @@ -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. \ No newline at end of file diff --git a/platformio.ini b/platformio.ini index cb9963a..94d1111 100644 --- a/platformio.ini +++ b/platformio.ini @@ -2,7 +2,8 @@ default_envs = default [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] platform = espressif32 @ 6.12.0 diff --git a/src/CrossPointSettings.h b/src/CrossPointSettings.h index e345220..45a20bc 100644 --- a/src/CrossPointSettings.h +++ b/src/CrossPointSettings.h @@ -155,6 +155,11 @@ class CrossPointSettings { // Pinned list name (empty = none pinned) 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; // Get singleton instance diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index 97b6cea..41dad88 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -496,6 +496,28 @@ void EpubReaderActivity::loop() { self->onGoToClearCache(); 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; } else if (action == QuickMenuAction::GO_TO_SETTINGS) { // Navigate to Settings activity diff --git a/src/activities/util/QuickMenuActivity.cpp b/src/activities/util/QuickMenuActivity.cpp index 03fb9e6..d7c2c94 100644 --- a/src/activities/util/QuickMenuActivity.cpp +++ b/src/activities/util/QuickMenuActivity.cpp @@ -2,16 +2,25 @@ #include +#include "CrossPointSettings.h" #include "MappedInputManager.h" #include "fontIds.h" namespace { -constexpr int MENU_ITEM_COUNT = 4; -const char* MENU_ITEMS[MENU_ITEM_COUNT] = {"Dictionary", "Bookmark", "Clear Cache", "Settings"}; -const char* MENU_DESCRIPTIONS_ADD[MENU_ITEM_COUNT] = {"Look up a word", "Add bookmark to this page", - "Free up storage space", "Open settings menu"}; -const char* MENU_DESCRIPTIONS_REMOVE[MENU_ITEM_COUNT] = {"Look up a word", "Remove bookmark from this page", - "Free up storage space", "Open settings menu"}; +// Base menu item count (reorderable items) +constexpr int BASE_MENU_ITEM_COUNT = 5; +// Total display count including "Edit List Order" +constexpr int DISPLAY_ITEM_COUNT = 6; + +// 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 void QuickMenuActivity::taskTrampoline(void* param) { @@ -53,6 +62,16 @@ void QuickMenuActivity::onExit() { } void QuickMenuActivity::loop() { + if (editMode) { + // Edit mode logic + handleEditMode(); + } else { + // Normal mode logic + handleNormalMode(); + } +} + +void QuickMenuActivity::handleNormalMode() { // Handle back button - cancel if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { onCancel(); @@ -61,8 +80,22 @@ void QuickMenuActivity::loop() { // Handle confirm button - select current option 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; - switch (selectedIndex) { + switch (actionIndex) { case 0: action = QuickMenuAction::DICTIONARY; break; @@ -73,6 +106,9 @@ void QuickMenuActivity::loop() { action = QuickMenuAction::CLEAR_CACHE; break; case 3: + action = QuickMenuAction::TOGGLE_ORIENTATION; + break; + case 4: default: action = QuickMenuAction::GO_TO_SETTINGS; break; @@ -88,10 +124,69 @@ void QuickMenuActivity::loop() { mappedInput.wasPressed(MappedInputManager::Button::Right); if (prevPressed) { - selectedIndex = (selectedIndex + MENU_ITEM_COUNT - 1) % MENU_ITEM_COUNT; + selectedIndex = (selectedIndex + DISPLAY_ITEM_COUNT - 1) % DISPLAY_ITEM_COUNT; updateRequired = true; } 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; } } @@ -120,46 +215,110 @@ void QuickMenuActivity::render() const { const int bezelRight = renderer.getBezelOffsetRight(); const int bezelBottom = renderer.getBezelOffsetBottom(); - // Calculate usable content area - const int marginLeft = 20 + bezelLeft; - const int marginRight = 20 + bezelRight; - const int marginTop = 15 + bezelTop; - const int contentWidth = pageWidth - marginLeft - marginRight; - const int contentHeight = pageHeight - marginTop - 60 - bezelBottom; // 60 for button hints + // Button hint space constants + constexpr int FRONT_BUTTON_SPACE = 45; // 40px button height + 5px padding + constexpr int SIDE_BUTTON_SPACE = 50; // 45px button area + 5px padding - // Draw header - renderer.drawCenteredText(UI_12_FONT_ID, marginTop, "Quick Menu", true, EpdFontFamily::BOLD); + // Calculate button hint margins based on orientation + // 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 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 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 bool isSelected = (i == selectedIndex); + const bool isBeingMoved = (editMode && i == movingIndex); // Draw selection highlight (black fill) for selected item if (isSelected) { renderer.fillRect(marginLeft + 10, itemY - 2, contentWidth - 20, itemHeight - 6); } - - // Draw menu item text - const char* itemText = MENU_ITEMS[i]; - // For bookmark item, show different text based on state - if (i == 1) { - itemText = isPageBookmarked ? "Remove Bookmark" : "Add Bookmark"; + // Draw outline for item being moved (when cursor is elsewhere) + if (isBeingMoved && !isSelected) { + renderer.drawRect(marginLeft + 10, itemY - 2, contentWidth - 20, itemHeight - 6); } - renderer.drawText(UI_10_FONT_ID, marginLeft + 20, itemY, itemText, !isSelected); - renderer.drawText(SMALL_FONT_ID, marginLeft + 20, itemY + 22, descriptions[i], !isSelected); + // Last item is always "Edit List Order" (fixed, not in the order array) + 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 - const auto labels = mappedInput.mapLabels("\xc2\xab Back", "Select", "", ""); - renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); + // Draw help text at bottom - different hints for edit mode + if (editMode) { + 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(); } diff --git a/src/activities/util/QuickMenuActivity.h b/src/activities/util/QuickMenuActivity.h index 708f80c..b9fac71 100644 --- a/src/activities/util/QuickMenuActivity.h +++ b/src/activities/util/QuickMenuActivity.h @@ -8,7 +8,7 @@ #include "../Activity.h" // 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. @@ -28,9 +28,16 @@ class QuickMenuActivity final : public Activity { const std::function onCancel; 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); [[noreturn]] void displayTaskLoop(); void render() const; + void handleNormalMode(); + void handleEditMode(); public: explicit QuickMenuActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,